Skip to content

Java 基础:从第一个程序到对象与集合 ​

按顺序完成示例,再进入框架实践。本文先建立语言基础;旧的 Eclipse、SSM 安装截图保留在历史笔记中,不作为学习 Java 的前置条件。

1. 准备 JDK 并运行程序 ​

JDK 提供编译与开发工具,JVM 执行字节码;运行时还需要标准类库。先安装项目要求的 JDK,并确认终端能找到同一套工具:

sh
java -version
javac -version

保存为 Hello.java,公开类名与文件名一致:

java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}
sh
javac Hello.java
java Hello

javac 生成 Hello.class,java 按类名启动程序,不在类名后追加 .class。先跑通终端中的程序,再用 IDE,能更容易分清语言、构建工具和编辑器各自的问题。

2. 类型、变量和流程控制 ​

Java 的八种基本类型是 byte、short、int、long、float、double、char、boolean。类、接口和数组属于引用类型;引用变量可以为 null,使用前要确认。

将 main 的方法体替换为:

java
int[] scores = {80, 90, 100};
int total = 0;
for (int score : scores) {
    total += score;
}
if (total >= 270) {
    System.out.println("passed: " + total);
}

预期输出 passed: 270。&& 和 || 会短路,例如 name != null && !name.isEmpty() 不会在 name 为 null 时调用方法。& 还可用于整数位运算;用于布尔运算时会计算两侧,不等同于短路与。

把处理提取成方法 ​

先把循环逻辑提取成有参数、有返回值的方法,再进入对象与接口。保存为 Count.java:

java
public class Count {
    static int passed(int[] scores) {
        int count = 0;
        for (int score : scores) {
            if (score >= 60) count++;
        }
        return count;
    }

    public static void main(String[] args) {
        System.out.println(passed(new int[] {80, 50, 90}));
    }
}

编译运行后输出 2。int 是返回类型,scores 接收数组参数,return 把计数交给调用方;static 使当前例子能直接从静态入口调用,实例方法会在下一节结合对象解释。不要把 Java 方法和可以脱离类独立声明的顶层函数混为一谈。

3. 类、接口和对象相等 ​

对象保存状态并通过方法提供行为。类可以继承一个父类、实现多个接口;优先先学组合与接口,再讨论复杂继承。

访问控制决定调用方可以看见哪些成员:

写法基本访问边界
public可由其他类访问;跨模块访问还受模块导出规则约束
private限制在所属顶层类的嵌套范围内,不能由普通外部调用方直接访问
不写修饰符包访问,同一包中可见
protected同包可见,也允许子类按语言规则访问;跨包子类访问有接收者等额外约束

下面把实现放在外部类内部,是为了让一个源文件就能运行;static 嵌套类不需要先创建外部类实例。implements 声明实现接口,@Override 让编译器帮助检查是否正确重写了接口方法。

java
public class Greeting {
    interface Sender {
        String send(String name);
    }

    static class ConsoleSender implements Sender {
        @Override
        public String send(String name) {
            return "Hello, " + name;
        }
    }

    public static void main(String[] args) {
        Sender sender = new ConsoleSender();
        System.out.println(sender.send("Ada"));
    }
}

保存为 Greeting.java,编译运行后输出 Hello, Ada。变量类型是接口,实际执行的是对象实现的方法。

  • 重载:同名方法的参数列表不同,编译期根据调用信息选择;仅返回值不同不能构成重载。
  • 重写:子类型提供继承方法的实现,受方法签名、访问权限与返回类型等规则约束。
  • 接口与抽象类:二者都不能直接实例化;抽象类可持有实例字段和构造过程。接口除了抽象方法,也能声明默认方法、静态方法等,不能沿用“接口只有抽象方法”的旧结论。
  • static 与实例成员:前者属于类,后者属于对象;静态字段也可能构成需要同步的共享可变状态。
  • == 与 equals:基本类型按相应值语义比较,引用的 == 判断是否同一对象;equals 的语义取决于类的实现。重写 equals 时通常需要一并重写 hashCode。

String 不可变;需要反复拼接时可使用 StringBuilder。选择 StringBuffer 也不意味着一组业务操作自动具有原子性。

4. 异常、资源与输入输出 ​

异常发生时应保留原因,并在能够处理的边界恢复或向上传递。不要把所有 Throwable 都吞掉,也不要把“错误一定不能捕获”当成语法规则。

类型例子处理重点
检查型异常IOException、SQLException编译期要求捕获或声明抛出
运行时异常NullPointerException、IllegalArgumentException常提示输入、状态或编程约束被破坏
ErrorOutOfMemoryError、NoClassDefFoundError通常不靠普通业务重试解决

ClassNotFoundException 不是 RuntimeException,NoClassDefFoundError 也不是普通异常。旧笔记将它们放进同一清单,容易误导处理方式。

java
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ReadFirstLine {
    public static void main(String[] args) throws Exception {
        try (BufferedReader reader = Files.newBufferedReader(
                Paths.get("notes.txt"), StandardCharsets.UTF_8)) {
            System.out.println(reader.readLine());
        }
    }
}

准备 notes.txt 后运行。try 的资源声明负责关闭实现 AutoCloseable 的资源;教学示例把错误交给启动入口展示,业务方法应声明或处理更具体的异常。字节流处理原始字节,字符读取需要确认编码。

final 限制再次赋值、重写或继承,具体取决于修饰位置;finally 是异常处理结构的一部分,不能在进程被终止等情况下保证执行。finalize 已弃用并计划移除,不用于资源管理。

5. 集合:先选语义,再选实现 ​

接口需要的语义常见实现
List保留顺序,允许重复ArrayList、LinkedList
Set元素不重复HashSet、LinkedHashSet、TreeSet
Map键映射到值,键不重复HashMap、LinkedHashMap、TreeMap

接口不规定底层一定是数组或散列表,也不保证所有实现都有相同顺序。HashMap 不保证迭代顺序;需要特定顺序时显式选择相应实现。Collection 是接口,Collections 是工具类,Map 不继承 Collection。

先掌握泛型的基本读法:Map<String, Integer> 表示键为字符串、值为整数包装对象,尖括号中的类型参数让编译器检查放入和取出的类型。new LinkedHashMap<>() 根据左侧声明推断类型参数;暂时不需要先掌握泛型通配符和复杂约束。

java
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

public class WordCount {
    public static void main(String[] args) {
        Map<String, Integer> counts = new LinkedHashMap<>();
        for (String word : Arrays.asList("go", "java", "go")) {
            counts.put(word, counts.getOrDefault(word, 0) + 1);
        }
        for (Map.Entry<String, Integer> entry : counts.entrySet()) {
            System.out.println(entry.getKey() + "=" + entry.getValue());
        }
    }
}

预期按插入顺序输出 go=2、java=1。练习改用 TreeMap,观察键排序与插入顺序的区别。

6. 并发与反射的学习边界 ​

先能独立写函数和类,再学习任务执行、共享数据与生命周期。Runnable 表示无返回值任务,Callable 可以返回结果;线程池负责调度任务,不是每个任务都创建一条新线程。提交任务后必须考虑等待结果、失败处理和关闭执行器。

synchronized 的互斥只约束使用同一把锁的代码。实例同步方法锁住接收者实例,静态同步方法锁住对应的 Class 对象。sleep 不释放已持有的监视器锁;wait 需要在持有对应监视器时调用,并释放该监视器,条件等待还要处理唤醒后再次检查。

Java 的线程状态为 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED,不要用一条“就绪→运行→挂起”箭头代替实际状态与转换条件。

反射用于检查或操作运行时类型,入口包括 Type.class、object.getClass() 和 Class.forName(...)。它不意味着可以无条件绕过访问控制与模块边界;初学项目优先显式类型和接口。

7. 练习与后续顺序 ​

  1. 给成绩数组增加不及格分数,写一个函数计算合格人数。
  2. 给 Sender 增加另一个实现,说明重载与重写发生在哪里。
  3. 给词频例子增加大小写归一化,再检查计数结果。
  4. 删除 notes.txt 观察错误,说明错误应在哪一层记录或处理。
参考解答(静态推演)
  1. 复用 passed 方法,条件为 score >= 60;输入 {80, 50, 90} 返回 2,空数组返回 0,正好 60 应计入。
  2. 新类实现 Sender 并声明 public String send(String name),这是重写;同一类另加 send(String name, String prefix) 才是重载,仅改返回类型不成立。
  3. 统计前用 word.toLowerCase(java.util.Locale.ROOT) 归一化;输入 "Go", "java", "go" 仍按首次出现顺序输出 go=2、java=1。显式 Locale 避免结果依赖机器默认地区。
  4. 读文件失败产生 IOException 的具体子类型;当前入口向上抛出,程序失败结束。库层应保留原因并交调用者决定,CLI 可在入口记录一次错误并返回非零状态;不能捕获后输出空报告冒充成功。

接下来按泛型、集合 API、异常与 I/O、并发、构建与测试的顺序深入,再学习 Spring。框架启动不等于掌握了语言基础。

参考:Java 官方学习路线、Object 的相等约定与资源回收说明、Map 契约、线程状态。

继续阅读