JavaSE 总复习:语法到多线程全梳理

JavaSE 复习不能只按"我学过哪些类"去背。更清晰的方式,是看每一块知识解决什么问题:基础语法负责把代码写出来,面向对象负责把代码组织起来,集合负责管理一批数据,IO 负责读写数据,多线程负责并发执行,反射、注解和动态代理负责理解框架底层。

这篇文章不是 API 大全,而是按"解决什么问题、最小代码怎么写、容易错在哪里、怎么自测"来串 JavaSE 主线。前面已经写过的单篇博客链接会保留下来,读到某个模块时,可以顺着链接回到原来的详细笔记。

一、先建立 JavaSE 的整体知识图

JavaSE 可以先分成几条主线。

模块 解决的问题 复习重点
基础语法 让代码能正确写出来 类型、运算符、流程、数组、方法
面向对象 让代码能被组织和复用 封装、继承、多态、抽象类、接口
常用 API 提高日常编码效率 String、Object、包装类、日期时间、正则
集合 管理一批对象 List、Set、Map、泛型、遍历、排序
Stream 简化集合处理 Lambda、方法引用、过滤、映射、收集
异常 处理程序异常情况 try-catch、throw、throws
IO 读写文件和数据 字节流、字符流、缓冲流、转换流、序列化
多线程 让多个任务同时执行 创建线程、线程安全、锁、线程池
网络编程 让程序之间通信 IP、端口、协议、UDP、TCP
反射/注解/代理 理解框架底层思想 Class、Field、Method、注解解析、动态代理

这张表就是后面复习的主线。基础语法不需要铺太厚,真正需要多花时间的是面向对象、集合、IO、多线程,以及最后的反射、注解、代理。

版本说明:本文示例按 Oracle Java SE 25 API 文档口径核对。涉及 JDK 8、JDK 9、JDK 16 这类版本差异时,会在对应位置单独说明;如果正在看旧教程,要特别注意 JDK 版本不同导致的语法和 API 差异。

自测建议:文中的小代码都可以放到 IDE 里单独运行。类名和文件名一致、包名先去掉、输入输出能对上,说明这个知识点不是只看懂了文字,而是真的能写出来。

二、基础语法:简单但容易出细节错

对应博客:

基础语法主要解决一个问题:一段 Java 代码如何从上到下正确执行。

1. 数据类型和变量

Java 的数据类型可以分成基本数据类型和引用数据类型。

基本数据类型有 8 个:byte、short、int、long、float、double、char、boolean。实际写代码时,整数默认是 int,小数默认是 double。

java 复制代码
public class BasicDemo {
    public static void main(String[] args) {
        int age = 18;
        long count = 10000000000L;
        double price = 19.9;
        char level = 'A';
        boolean passed = true;

        System.out.println(age);
        System.out.println(count);
        System.out.println(price);
        System.out.println(level);
        System.out.println(passed);
    }
}

这里重点不是背范围,而是知道什么时候会出错。比如 long 类型较大的整数后面要加 L,float 类型小数后面要加 F;byte、short、char 参与运算时,结果通常会提升为 int。

2. 运算符和类型转换

运算符里最容易混的是除法、取模、短路逻辑和字符串拼接。

java 复制代码
public class OperatorDemo {
    public static void main(String[] args) {
        System.out.println(10 / 3);      // 3
        System.out.println(10 % 3);      // 1

        int a = 10;
        int b = 20;
        System.out.println(a > 5 && b > 10);

        System.out.println("结果是:" + a + b);      // 结果是:1020
        System.out.println("结果是:" + (a + b));    // 结果是:30
    }
}

整数除法只保留整数部分,取模拿余数。字符串拼接时,只要前面已经变成字符串,后面的加号就会按拼接处理。如果要先算数值,必须加小括号。

类型转换分为自动类型转换和强制类型转换。小范围转大范围通常自动完成,大范围转小范围要强转,但可能丢失数据。

java 复制代码
public class CastDemo {
    public static void main(String[] args) {
        int a = 100;
        double b = a;

        double x = 12.9;
        int y = (int) x;

        System.out.println(b);
        System.out.println(y);
    }
}

3. 流程控制

流程控制分为顺序结构、分支结构和循环结构。

if 适合范围判断,switch 适合固定值匹配。循环里 for 更适合次数明确的场景,while 更适合次数不确定但条件明确的场景。

java 复制代码
public class FlowDemo {
    public static void main(String[] args) {
        int score = 86;

        if (score >= 90) {
            System.out.println("优秀");
        } else if (score >= 60) {
            System.out.println("及格");
        } else {
            System.out.println("不及格");
        }

        for (int i = 1; i <= 5; i++) {
            System.out.println("第" + i + "次循环");
        }
    }
}

这里容易错的是大括号。虽然只有一行代码时可以省略大括号,但复习和写项目时不建议省略。少写一对大括号,后面加代码时很容易改变逻辑。

4. 数组

数组用来存储同一种类型的多个数据。数组一旦创建,长度固定。

java 复制代码
public class ArrayDemo {
    public static void main(String[] args) {
        int[] scores = {90, 80, 70};

        int sum = 0;
        for (int i = 0; i < scores.length; i++) {
            sum += scores[i];
        }

        System.out.println("总分:" + sum);
    }
}

数组最重要的是索引从 0 开始。长度是 3 的数组,最后一个索引是 2,不是 3。越界访问会直接抛出数组下标越界异常。

5. 方法和方法重载

方法负责把一段功能封装起来。它可以有参数,可以有返回值,也可以没有。

java 复制代码
public class MethodDemo {
    public static void main(String[] args) {
        int result = add(10, 20);
        System.out.println(result);

        double result2 = add(1.5, 2.5);
        System.out.println(result2);
    }

    public static int add(int a, int b) {
        return a + b;
    }

    public static double add(double a, double b) {
        return a + b;
    }
}

方法重载看的是方法名相同,参数列表不同。参数列表不同包括参数类型不同、数量不同、顺序不同。返回值不同不能单独构成重载。

基础语法易错点

  • 整数除法不会自动保留小数。
  • 字符串拼接会改变加号含义。
  • 数组索引从 0 开始,最大索引是长度减 1。
  • 方法重载只看方法名和参数列表,不看返回值。
  • Scanner 混用 nextInt 和 nextLine 时,容易被前一次输入留下的回车影响。

三、面向对象:Java 代码组织的核心

对应博客:

面向对象不是几个概念的堆叠,它解决的是"代码怎么组织"的问题。

类是对象的设计图,对象是类创建出来的具体实例。成员变量描述对象的数据,成员方法描述对象能做什么。

1. 类、对象和封装

封装的核心是把数据藏起来,通过方法对外提供访问入口。最常见的写法就是成员变量私有化,再提供 get 和 set 方法。

java 复制代码
public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age < 0 || age > 120) {
            System.out.println("年龄不合法");
            return;
        }
        this.age = age;
    }
}
java 复制代码
public class StudentTest {
    public static void main(String[] args) {
        Student s = new Student("小明", 18);
        s.setAge(20);
        System.out.println(s.getName() + ":" + s.getAge());
    }
}

this 表示当前对象。成员变量和局部变量同名时,用 this 可以明确访问成员变量。

2. 构造方法

构造方法在创建对象时执行,主要用来初始化对象。

构造方法有几个特点:方法名和类名一致;没有返回值类型;创建对象时自动调用。如果一个类没有写构造方法,Java 会默认提供无参构造;如果自己写了带参构造,默认无参构造就不会自动提供。

3. static

static 修饰的成员属于类,不属于某一个对象。静态变量被所有对象共享,静态方法可以通过类名直接调用。

java 复制代码
public class Counter {
    static int count = 0;

    public Counter() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

static 适合写工具方法、共享数据、常量配合 final 使用。但静态方法不能直接访问非静态成员,因为非静态成员必须依赖具体对象。

单例模式也和 static 有关系。饿汉式通常在类加载时创建对象,懒汉式通常在第一次使用时创建对象。复习时重点理解"构造方法私有化 + 对外提供唯一对象入口"。

4. 继承和方法重写

继承让子类复用父类已有内容。

java 复制代码
class Animal {
    public void eat() {
        System.out.println("动物吃东西");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }

    public void lookHome() {
        System.out.println("狗看家");
    }
}

方法重写发生在子父类之间。子类重写父类方法时,方法名、参数列表要一致,访问权限不能比父类更小。建议加 Override 注解,让编译器帮忙检查。

5. 多态

多态的写法是父类引用指向子类对象。

java 复制代码
public class PolymorphismDemo {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.eat();

        if (animal instanceof Dog) {
            Dog dog = (Dog) animal;
            dog.lookHome();
        }
    }
}

多态的好处是提高扩展性。方法参数写父类类型,传进来的可以是不同子类对象。运行时调用哪个重写方法,看实际对象类型。

但多态也有限制:父类引用不能直接调用子类独有方法。如果确实要调用,需要向下转型。转型前先用 instanceof 判断,避免类型转换异常。

6. 抽象类

当一个父类只知道子类应该有某个行为,但不知道具体怎么做时,可以把这个行为定义成抽象方法。

java 复制代码
abstract class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public abstract void work();
}

class Teacher extends Person {
    public Teacher(String name) {
        super(name);
    }

    @Override
    public void work() {
        System.out.println("老师讲课");
    }
}

抽象类不能直接创建对象,但可以有构造方法。这个构造方法不是给自己创建对象用的,而是给子类初始化父类部分用的。

面向对象易错点

  • 构造方法没有返回值类型,写了 void 就变成普通方法。
  • private 不是为了麻烦,而是为了控制数据访问。
  • static 成员属于类,非 static 成员属于对象。
  • 子类构造方法默认会先访问父类无参构造。
  • 多态调用成员方法看实际对象,访问成员变量看引用类型。
  • 抽象类不能创建对象,但可以有构造方法、普通方法和成员变量。

四、接口、内部类和常见修饰符

对应博客:

接口更像一种规则。它规定实现类应该具备哪些能力。

1. 接口

java 复制代码
interface Flyable {
    void fly();
}

class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("鸟会飞");
    }
}

一个类可以实现多个接口。接口解决了 Java 类只能单继承的问题,让一个类可以同时具备多种能力。

JDK 8 以后,接口中可以定义默认方法和静态方法。默认方法用 default 修饰,实现类可以直接使用,也可以重写。JDK 9 以后,接口支持私有方法,用来抽取接口内部默认方法中的重复逻辑。

2. final

final 可以修饰类、方法、变量。

修饰位置 含义
不能被继承
方法 不能被重写
变量 只能赋值一次

如果 final 修饰引用类型变量,表示这个引用不能再指向别的对象,不代表对象内部内容完全不能变。

3. 代码块

代码块可以分为局部代码块、构造代码块、静态代码块。总复习里重点记静态代码块:它随着类加载执行,只执行一次,常用于初始化静态资源。

java 复制代码
public class CodeBlockDemo {
    static {
        System.out.println("静态代码块执行");
    }

    public static void main(String[] args) {
        System.out.println("main执行");
    }
}

4. 内部类

内部类就是定义在一个类里面的类,常见分类有成员内部类、静态内部类、局部内部类、匿名内部类。

匿名内部类最常用。它适合只使用一次的实现类,尤其常见于接口或抽象类作为参数的场景。

java 复制代码
interface Task {
    void run();
}

public class InnerDemo {
    public static void main(String[] args) {
        useTask(new Task() {
            @Override
            public void run() {
                System.out.println("执行任务");
            }
        });
    }

    public static void useTask(Task task) {
        task.run();
    }
}

JDK 16 以后,普通内部类声明静态成员的限制被放宽。复习时按当前 JDK 25 口径理解即可,但看旧资料时要注意版本差异。

五、String 和常用 API:日常编码最常碰到

String 是最常用的类之一,也是最容易在面试和细节题里出现的知识点。

1. String 不可变

字符串一旦创建,内容不能改变。看起来像改变字符串,其实是创建了新的字符串对象。

java 复制代码
public class StringDemo {
    public static void main(String[] args) {
        String s = "abc";
        s = s + "d";
        System.out.println(s);
    }
}

这里变量 s 最后指向了新的字符串,不是原来的 abc 被改掉了。

2. == 和 equals

== 比较的是基本类型的值,比较引用类型时比较的是地址。String 重写了 equals,所以通常用 equals 比较字符串内容。

java 复制代码
public class StringEqualsDemo {
    public static void main(String[] args) {
        String a = "hello";
        String b = "hello";
        String c = new String("hello");

        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(a.equals(c));
    }
}

字符串字面量会进入字符串常量池,所以 a 和 b 通常指向同一个对象。new String 会在堆中创建新对象,所以 a 和 c 的地址不同,但内容相同。

3. StringBuilder 和 StringJoiner

频繁拼接字符串时,StringBuilder 更合适。

java 复制代码
public class BuilderDemo {
    public static void main(String[] args) {
        StringBuilder builder = new StringBuilder();
        builder.append("Java");
        builder.append("SE");
        builder.append("复习");

        System.out.println(builder.toString());
    }
}

StringJoiner 适合拼接有分隔符、前缀、后缀的字符串。

java 复制代码
import java.util.StringJoiner;

public class JoinerDemo {
    public static void main(String[] args) {
        StringJoiner joiner = new StringJoiner(", ", "[", "]");
        joiner.add("Java");
        joiner.add("MySQL");
        joiner.add("Spring");

        System.out.println(joiner);
    }
}

4. 常用 API 只抓常用点

Object 是所有类的顶级父类。toString 用来返回对象字符串表示,equals 默认比较地址,实际开发中经常重写 equals 比较对象内容。

Objects 提供了一些空指针安全的工具方法,例如比较两个对象是否相等。

包装类用于基本类型和对象之间转换。现在自动装箱和自动拆箱已经很常见,但要注意包装类对象可能为 null,自动拆箱时容易出现空指针。

日期时间优先复习 JDK 8 之后的 LocalDate、LocalTime、LocalDateTime、DateTimeFormatter。它们比老的 Date、Calendar 更清晰。

正则表达式用于校验和匹配字符串。总复习里不用背所有符号,重点掌握数字、字母、数量词、分组和常用校验写法。

String 和 API 易错点

  • 字符串比较内容用 equals。
  • String 频繁拼接会产生新对象,循环拼接优先 StringBuilder。
  • 包装类自动拆箱前要小心 null。
  • double 和 float 不能直接用于金融金额精确计算,金额建议用整数分或精确小数方案处理。
  • 正则里的反斜杠在 Java 字符串中要写成双反斜杠。

六、集合:重点不是背类名,而是会选

对应博客:

集合是 JavaSE 里非常重要的一块。数组长度固定,集合长度可变,更适合存储对象。

1. 泛型

泛型的作用是把类型检查提前到编译阶段。

java 复制代码
import java.util.ArrayList;

public class GenericDemo {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("小明");
        names.add("小红");

        for (String name : names) {
            System.out.println(name);
        }
    }
}

没有泛型时,集合里什么都能放,取出来还要强转。加上泛型后,集合只能放指定类型的数据,代码更安全。

2. Collection 和常见遍历

Collection 是单列集合的顶层接口。常见遍历方式有迭代器、增强 for、Lambda、普通 for。

java 复制代码
import java.util.ArrayList;
import java.util.Iterator;

public class CollectionDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Java");
        list.add("MySQL");
        list.add("Spring");

        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

如果遍历时要删除元素,优先用迭代器自己的 remove 方法,避免一边普通遍历一边直接删集合导致并发修改问题。

3. List

List 的特点是有序、可重复、有索引。

实现类 底层特点 适合场景
ArrayList 数组 查询多、按索引访问多
LinkedList 双向链表 头尾增删较多
java 复制代码
import java.util.ArrayList;

public class ListDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Java");
        list.add("Java");
        list.add("MySQL");

        System.out.println(list.get(0));
        System.out.println(list);
    }
}

ArrayList 底层是数组,所以按索引查询很快。它扩容时会创建新数组,再把旧数据拷贝过去。这里不需要死背源码,但要知道频繁扩容会有成本。

4. Set

Set 的特点是不能存重复元素。

实现类 特点 适合场景
HashSet 无序、不重复、无索引 只关心去重
LinkedHashSet 有序、不重复、无索引 去重并保留添加顺序
TreeSet 排序、不重复、无索引 去重并排序
java 复制代码
import java.util.HashSet;

public class SetDemo {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<>();
        set.add("Java");
        set.add("Java");
        set.add("MySQL");

        System.out.println(set);
    }
}

HashSet 判断重复通常和 hashCode、equals 有关。如果存自定义对象,想按属性去重,就要正确重写这两个方法。

TreeSet 可以自然排序,也可以传入比较器排序。它底层涉及树结构,复习时只需要知道它能排序,排序规则必须明确。至于红黑树这些底层结构,在集合底层理解时顺带知道即可,不需要单独展开成算法章节。

5. Map

Map 是双列集合,用键值对存数据。键不能重复,值可以重复。

实现类 特点 适合场景
HashMap 无序、键不重复 最常用的键值对存储
LinkedHashMap 有序、键不重复 保留添加顺序
TreeMap 按键排序、键不重复 需要按键排序
java 复制代码
import java.util.HashMap;
import java.util.Map;

public class MapDemo {
    public static void main(String[] args) {
        HashMap<String, Integer> scores = new HashMap<>();
        scores.put("小明", 90);
        scores.put("小红", 95);
        scores.put("小明", 100);

        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
    }
}

这里小明 put 了两次,后面的值会覆盖前面的值。Map 复习时一定要记住:键重复不是新增,而是覆盖。

6. Collections 和不可变集合

Collections 是集合工具类,常见功能包括排序、打乱、批量添加等。

不可变集合表示创建后不能再修改。它适合存储固定数据,比如配置项、固定选项等。复习时记住一点:不可变集合不是不能遍历,而是不能增删改。

集合易错点

  • List 有索引,可以重复。
  • Set 不重复,没有普通索引。
  • Map 存键值对,键重复会覆盖。
  • HashSet 存自定义对象时,要考虑 hashCode 和 equals。
  • TreeSet、TreeMap 需要明确排序规则。
  • 遍历集合时删除元素要小心并发修改问题。

七、Lambda、方法引用和 Stream 流

对应博客:

Lambda、方法引用和 Stream 是一条学习线。Lambda 简化函数式接口写法,方法引用继续简化 Lambda,Stream 用来处理集合数据。

1. Lambda

Lambda 的前提是函数式接口,也就是只有一个抽象方法的接口。

java 复制代码
interface Calculator {
    int calc(int a, int b);
}

public class LambdaDemo {
    public static void main(String[] args) {
        Calculator calculator = (a, b) -> a + b;
        System.out.println(calculator.calc(10, 20));
    }
}

Lambda 不是新的万能语法,它只是让匿名内部类在函数式接口场景下写得更简洁。

2. 方法引用

方法引用可以看成 Lambda 的进一步简化。条件是被引用的方法已经存在,并且参数和返回值能匹配函数式接口中的抽象方法。

java 复制代码
import java.util.Arrays;

public class MethodReferenceDemo {
    public static void main(String[] args) {
        String[] names = {"Tom", "Jerry", "Amy"};
        Arrays.sort(names, String::compareToIgnoreCase);
        System.out.println(Arrays.toString(names));
    }
}

3. Stream

Stream 适合对集合进行过滤、映射、排序、去重、收集。

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class StreamDemo {
    public static void main(String[] args) {
        ArrayList<String> names = new ArrayList<>();
        names.add("张三");
        names.add("张无忌");
        names.add("李四");
        names.add("张三丰");

        List<String> result = names.stream()
                .filter(name -> name.startsWith("张"))
                .filter(name -> name.length() == 3)
                .collect(Collectors.toList());

        System.out.println(result);
    }
}

Stream 操作可以分成中间操作和终结操作。filter、map、sorted、distinct 这类是中间操作,collect、forEach、count 这类是终结操作。没有终结操作,中间操作不会真正执行。

Stream 易错点

  • Stream 不直接存储数据,它只是处理数据的流水线。
  • 一个 Stream 通常只能消费一次。
  • 中间操作是懒执行,终结操作触发执行。
  • collect 到 Map 时,如果 key 重复,要考虑合并规则。

八、异常处理:让程序出问题时有处理方式

对应博客:

异常不是为了让程序不出错,而是让程序出错后能被发现、被处理、被传递。

1. 异常体系

Throwable 是异常体系的顶层。它下面主要有 Error 和 Exception。

Error 通常是严重问题,不建议程序自己处理。Exception 是程序可以处理的异常。Exception 又可以分为编译时异常和运行时异常。

运行时异常通常是代码逻辑或参数问题,比如空指针、数组越界、类型转换错误。编译时异常在编译阶段就要求处理,比如文件找不到、IO 读写失败等。

2. try-catch-finally

java 复制代码
public class TryDemo {
    public static void main(String[] args) {
        try {
            int result = 10 / 0;
            System.out.println(result);
        } catch (ArithmeticException e) {
            System.out.println("计算异常:" + e.getMessage());
        } finally {
            System.out.println("释放资源或收尾处理");
        }
    }
}

try 中写可能出异常的代码,catch 捕获并处理异常,finally 通常用于收尾。多个 catch 同时存在时,子类异常要写在父类异常前面。

3. throw 和 throws

throw 用在方法内部,表示主动抛出一个异常对象。throws 用在方法声明上,表示这个方法可能抛出异常,提醒调用者处理。

java 复制代码
public class ThrowDemo {
    public static void main(String[] args) {
        checkAge(15);
    }

    public static void checkAge(int age) {
        if (age < 18) {
            throw new IllegalArgumentException("年龄不能小于18");
        }
        System.out.println("年龄合法");
    }
}

自定义异常可以略写。真正复习时,重点先把 try-catch、throw、throws 的区别搞清楚。

异常易错点

  • catch 中父类异常不能写在子类异常前面。
  • throw 是抛出异常对象,throws 是声明方法可能抛异常。
  • try 中一旦出现异常,异常后面的代码不会继续执行,会跳到对应 catch。
  • finally 常用于释放资源,但不要把所有业务逻辑都塞进 finally。

九、IO 流:读写文件和数据

对应博客:

IO 流解决的是数据读写问题。复习 IO 时不要先背类名,先判断数据类型和方向:读还是写,字节还是字符。

1. File

File 表示文件或文件夹路径。它不代表文件一定存在,只是把路径封装成对象。

java 复制代码
import java.io.File;

public class FileDemo {
    public static void main(String[] args) {
        File file = new File("a.txt");
        System.out.println(file.exists());
        System.out.println(file.getAbsolutePath());
    }
}

2. 字节流

字节流适合处理所有类型文件,尤其是图片、音频、视频、压缩包这类非纯文本文件。

java 复制代码
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteCopyDemo {
    public static void main(String[] args) throws IOException {
        try (
                FileInputStream in = new FileInputStream("source.jpg");
                FileOutputStream out = new FileOutputStream("target.jpg")
        ) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        }
    }
}

读取时 len 表示本次读到的有效字节数,写出时一定要写 out.write(buffer, 0, len)。如果直接写整个 buffer,最后一次可能把无效数据也写进去。

3. 字符流

字符流适合处理纯文本文件。它会考虑字符编码,比直接用字节流读中文文本更方便。

java 复制代码
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CharCopyDemo {
    public static void main(String[] args) throws IOException {
        try (
                FileReader reader = new FileReader("a.txt");
                FileWriter writer = new FileWriter("b.txt")
        ) {
            char[] buffer = new char[1024];
            int len;
            while ((len = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, len);
            }
        }
    }
}

文本用字符流,非文本用字节流,这是最基础的选择规则。

4. 缓冲流

缓冲流是在普通流外面再包一层,减少频繁读写磁盘的次数,提高效率。

java 复制代码
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BufferedDemo {
    public static void main(String[] args) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader("a.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
}

BufferedReader 的 readLine 可以一次读一行,但它不会把换行符读进结果里。如果写出时需要换行,要自己处理。

5. 转换流

转换流用来解决字节流和字符流之间的转换,尤其是指定编码。

java 复制代码
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class ConvertDemo {
    public static void main(String[] args) throws Exception {
        try (
                InputStreamReader reader = new InputStreamReader(
                        new FileInputStream("a.txt"),
                        StandardCharsets.UTF_8
                )
        ) {
            int ch;
            while ((ch = reader.read()) != -1) {
                System.out.print((char) ch);
            }
        }
    }
}

乱码通常不是流本身坏了,而是编码和解码方式不一致。

6. 序列化流和打印流

序列化流可以把对象写到文件中,再读回来。对象要能序列化,需要实现 Serializable。类结构变化后反序列化可能出问题,所以常见做法是声明 serialVersionUID。

打印流 PrintStream、PrintWriter 适合更方便地输出文本内容。比如 System.out 本身就是 PrintStream 类型。

IO 易错点

  • 复制图片、视频等文件用字节流。
  • 读写纯文本优先字符流。
  • 缓冲流不能单独存在,它要包装普通流。
  • flush 是刷新,close 是关闭;close 通常会先刷新再关闭。
  • 乱码优先检查编码和解码是否一致。
  • 序列化对象时要注意版本号和 transient。

十、多线程:从创建线程到线程安全

对应博客:

多线程的主线是:创建线程,执行任务,多个线程共享数据时解决安全问题,任务多了再交给线程池管理。

1. 创建线程的三种方式

第一种是继承 Thread,重写 run 方法。

第二种是实现 Runnable,把任务交给 Thread 执行。

第三种是实现 Callable,配合 FutureTask 获取返回值。

java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadCreateDemo {
    public static void main(String[] args) throws Exception {
        Callable<Integer> task = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) {
                sum += i;
            }
            return sum;
        };

        FutureTask<Integer> futureTask = new FutureTask<>(task);
        Thread thread = new Thread(futureTask);
        thread.start();

        System.out.println(futureTask.get());
    }
}

这里重点是 start。调用 start 才是启动新线程;直接调用 run 只是普通方法调用。

2. 常用线程 API

常见 API 包括获取线程名称、设置线程名称、获取当前线程、sleep、join 等。

sleep 让当前线程休眠一段时间,join 让一个线程插队执行。调试多线程时,经常通过打印当前线程名称观察代码到底由哪个线程执行。

3. 线程安全

线程安全问题通常满足三个条件:多个线程、共享数据、修改共享数据。

java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Account {
    private double money;
    private final Lock lock = new ReentrantLock();

    public Account(double money) {
        this.money = money;
    }

    public void draw(double amount) {
        lock.lock();
        try {
            String name = Thread.currentThread().getName();
            if (money >= amount) {
                System.out.println(name + "取钱成功:" + amount);
                money -= amount;
                System.out.println("余额:" + money);
            } else {
                System.out.println(name + "取钱失败,余额不足");
            }
        } finally {
            lock.unlock();
        }
    }
}

public class AccountDemo {
    public static void main(String[] args) {
        Account account = new Account(1000);

        new Thread(() -> account.draw(800), "小明").start();
        new Thread(() -> account.draw(800), "小红").start();
    }
}

这段代码用 Lock 保证同一时间只有一个线程能进入取钱核心逻辑。finally 中释放锁非常关键,否则异常出现后锁可能一直不释放。

同步代码块、同步方法和 Lock 都能解决线程安全问题。同步代码块锁范围更灵活,同步方法写法更简单,Lock 控制更细。

4. 线程池

任务很多时,不建议一直 new Thread。线程池可以复用线程,减少频繁创建和销毁线程的成本。

java 复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,
                5,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        for (int i = 0; i < 8; i++) {
            int taskId = i;
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "执行任务" + taskId);
            });
        }

        pool.shutdown();
    }
}

线程池参数可以按任务执行流程理解:先用核心线程,核心线程满了进队列,队列满了创建临时线程,线程数达到最大还放不下就触发拒绝策略。

多线程易错点

  • start 才会启动新线程,run 只是普通方法。
  • 线程安全不是一定报错,而是结果可能不对。
  • 判断线程安全风险时,看是否多个线程同时修改共享数据。
  • synchronized 的锁对象必须一致。
  • Lock 必须在 finally 中释放。
  • 线程池用完要考虑关闭。

十一、网络编程:重点理解 UDP 和 TCP

对应博客:

网络编程平时写得不多,但它能帮助理解程序之间怎么通信。

网络通信三要素是 IP、端口、协议。IP 定位主机,端口定位程序,协议决定数据传输规则。

1. InetAddress

InetAddress 用来表示主机地址,可以获取本机地址,也可以根据域名获取地址。

java 复制代码
import java.net.InetAddress;

public class InetDemo {
    public static void main(String[] args) throws Exception {
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println(localHost.getHostAddress());
        System.out.println(localHost.getHostName());
    }
}

2. UDP

UDP 是无连接通信。发送端把数据封装成数据包,直接发给目标地址和端口。它速度快,但不保证可靠到达。

适合实时性更重要、允许少量丢失的场景,比如语音、直播、广播类数据。

3. TCP

TCP 是面向连接的可靠通信。通信前要先建立连接,建立后通过输入输出流传输数据。

适合对可靠性要求高的场景,比如登录、支付、文件传输等。

对比点 UDP TCP
是否连接 无连接 面向连接
数据形式 数据包 字节流
可靠性 不保证可靠到达 可靠传输
速度 通常更轻量 相对更重
场景 直播、语音、广播 登录、支付、文件传输

复习网络编程时,重点不是把文件上传、多客户端聊天室全部背下来,而是理解 UDP 和 TCP 的通信逻辑:一个是把数据包发出去,一个是先建立连接再通过流传输。

十二、反射、注解和动态代理:理解框架底层的入口

对应博客:

反射、注解和动态代理属于 JavaSE 进阶收尾内容。平时业务代码不一定天天写,但理解它们以后,再看框架会更清楚。

1. 反射

反射允许程序在运行时获取类的信息,并操作构造方法、成员变量、成员方法。

java 复制代码
import java.lang.reflect.Field;

class User {
    private String name = "小明";
}

public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        User user = new User();

        Class<?> clazz = user.getClass();
        Field field = clazz.getDeclaredField("name");
        field.setAccessible(true);

        Object value = field.get(user);
        System.out.println(value);
    }
}

反射的入口是 Class 对象。拿到 Class 后,可以继续获取 Constructor、Field、Method。setAccessible 可以关闭普通访问检查,但也会破坏封装,所以不能在普通业务代码里滥用。

2. 注解

注解是写给程序看的标记。自己定义注解时使用 @interface。

java 复制代码
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyTest {
    int count() default 1;
}

Target 控制注解能写在哪里,Retention 控制注解保留到哪个阶段。如果想在运行时通过反射读取注解,Retention 必须设置为 RUNTIME。

java 复制代码
import java.lang.reflect.Method;

public class AnnotationParseDemo {
    public static void main(String[] args) throws Exception {
        Method method = AnnotationParseDemo.class.getDeclaredMethod("hello");

        if (method.isAnnotationPresent(MyTest.class)) {
            MyTest myTest = method.getDeclaredAnnotation(MyTest.class);
            for (int i = 0; i < myTest.count(); i++) {
                method.invoke(new AnnotationParseDemo());
            }
        }
    }

    @MyTest(count = 3)
    public void hello() {
        System.out.println("hello");
    }
}

这就是注解的核心链路:先做标记,再由程序扫描标记,最后根据标记执行逻辑。很多测试框架、Web 框架、ORM 框架都用到了这种思想。

3. 动态代理

动态代理可以在不修改原对象源码的情况下,对方法调用进行增强。

java 复制代码
import java.lang.reflect.Proxy;

interface Service {
    void save();
}

class UserService implements Service {
    @Override
    public void save() {
        System.out.println("保存用户");
    }
}

public class ProxyDemo {
    public static void main(String[] args) {
        UserService target = new UserService();

        Service proxy = (Service) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{Service.class},
                (proxyObject, method, methodArgs) -> {
                    System.out.println("方法执行前");
                    Object result = method.invoke(target, methodArgs);
                    System.out.println("方法执行后");
                    return result;
                }
        );

        proxy.save();
    }
}

动态代理三要素是真正干活的对象、代理对象、调用处理逻辑。Java 原生动态代理通常要求被代理对象有接口。

反射注解代理易错点

  • Class.forName 要写完整类名。
  • getDeclaredXXX 可以拿到当前类声明的成员,包括私有成员。
  • 访问私有成员前通常要 setAccessible。
  • 注解想运行时读取,Retention 要设置为 RUNTIME。
  • 注解本身不会自动执行逻辑,必须有扫描和解析代码。
  • JDK 动态代理依赖接口,增强逻辑写在 InvocationHandler 中。

十三、重点面试和自测题

这些题不追求数量,重点是能帮自己查漏补缺。建议先遮住答案,自己判断输出结果或错误原因,再看解析。

1. == 和 equals:下面输出什么?

java 复制代码
public class EqualsTest {
    public static void main(String[] args) {
        String a = "java";
        String b = "ja" + "va";
        String c = new String("java");

        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(a.equals(c));
    }
}

答案是 true、false、true。

b 在编译期就能确定为 "java",所以它和 a 指向同一个字符串常量池对象。c 是 new 出来的新对象,地址和 a 不同,但 String 重写了 equals,所以比较内容时结果为 true。

2. double 精度:下面为什么不是 0.3?

java 复制代码
public class DoubleTest {
    public static void main(String[] args) {
        System.out.println(0.1 + 0.2);
    }
}

结果通常不是精确的 0.3,而是类似 0.30000000000000004。

原因是很多十进制小数无法被二进制浮点数精确表示。金额计算不要直接依赖 double 和 float,可以用整数分存储,或者使用精确小数方案。

3. 抽象类和接口:这段设计哪里更合适?

java 复制代码
abstract class Animal {
    abstract void eat();
}

interface Flyable {
    void fly();
}

class Bird extends Animal implements Flyable {
    @Override
    void eat() {
        System.out.println("鸟吃东西");
    }

    @Override
    public void fly() {
        System.out.println("鸟会飞");
    }
}

Animal 更适合做抽象类,因为它描述"是什么";Flyable 更适合做接口,因为它描述"能做什么"。

判断方法很简单:如果是同一类对象的共同父类,优先考虑抽象类;如果是某种能力或规则,优先考虑接口。Java 只能单继承,但可以实现多个接口。

4. ArrayList 和 LinkedList:下面哪行更常用?

java 复制代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ListTest {
    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        List<String> list2 = new LinkedList<>();

        list1.add("A");
        list1.add("B");
        System.out.println(list1.get(1));
    }
}

日常开发里更常用的是 ArrayList。

ArrayList 底层主要是数组,按索引访问方便;LinkedList 底层是链表,适合频繁头尾操作的场景。实际写业务代码时,大量场景都是遍历、按索引访问、追加元素,所以 ArrayList 出场更多。

5. HashSet 去重:为什么这里没有去重?

java 复制代码
import java.util.HashSet;
import java.util.Set;

class Student {
    String name;
    int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class HashSetTest {
    public static void main(String[] args) {
        Set<Student> set = new HashSet<>();
        set.add(new Student("小明", 18));
        set.add(new Student("小明", 18));
        System.out.println(set.size());
    }
}

输出是 2。

HashSet 判断重复时会用到 hashCode 和 equals。Student 没有重写这两个方法时,默认按对象地址判断,两个 new 出来的对象地址不同,所以不会被当成重复元素。如果希望按 name 和 age 去重,就要重写 hashCode 和 equals。

6. throw 和 throws:下面哪个是真正抛异常?

java 复制代码
public class ThrowTest {
    public static void main(String[] args) {
        checkAge(15);
    }

    public static void checkAge(int age) throws IllegalArgumentException {
        if (age < 18) {
            throw new IllegalArgumentException("年龄不能小于18");
        }
    }
}

真正抛出异常的是 throw,throws 只是写在方法声明上,提醒调用者这个方法可能抛出异常。

复习时可以这样记:throw 后面跟的是异常对象,写在方法体内部;throws 后面跟的是异常类型,写在方法声明位置。

7. TCP 和 UDP:为什么 TCP 更适合文件传输?

java 复制代码
// 这里只做理解,不要求背完整代码。
// UDP:把数据打成数据包发出去,不保证一定到达。
// TCP:先建立连接,再通过输入输出流传输数据。

文件传输更适合 TCP,因为文件内容不能随便丢一段。TCP 面向连接,可靠性更强;UDP 无连接,速度轻,但不保证可靠到达,更适合直播、语音、广播这类允许少量丢失的场景。

8. 反射:这段代码为什么要谨慎使用?

java 复制代码
import java.lang.reflect.Field;

class User {
    private String name = "小明";
}

public class ReflectTest {
    public static void main(String[] args) throws Exception {
        User user = new User();
        Field field = User.class.getDeclaredField("name");
        field.setAccessible(true);
        System.out.println(field.get(user));
    }
}

这段代码可以读取 private 字段,说明反射能在运行时操作类结构。

它强大,也要谨慎。反射适合框架和工具做通用能力,比如扫描注解、创建对象、调用方法;但普通业务代码里滥用反射,会绕过封装,让代码更难读,也更容易出安全和维护问题。

9. 线程池 7 个参数:分别代表什么?

java 复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class PoolParamTest {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,
                5,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

这 7 个参数从前到后分别是:

  1. corePoolSize:核心线程数。线程池会尽量先保留这部分线程,就算它们暂时空闲也不会立刻回收。
  2. maximumPoolSize:最大线程数。任务很多时,线程池最多能扩到这个数量。
  3. keepAliveTime:临时线程的空闲存活时间。
  4. unit:第 3 个参数的时间单位。
  5. workQueue:任务阻塞队列。核心线程忙不过来时,先把任务放到这里排队。
  6. threadFactory:线程工厂。线程池创建新线程时,线程对象由它来生产。
  7. handler:拒绝策略。当线程数到上限、队列也满了,再来新任务时,决定怎么处理。

面试里更推荐顺着执行流程回答:先看核心线程够不够,不够时进队列,队列满了再扩到最大线程数,还是放不下才触发拒绝策略。这样回答比死背参数名更像真正理解了线程池。

十四、最后总结

JavaSE 的知识点很多,但复习时可以按主线收束。

基础语法解决"代码怎么写";面向对象解决"代码怎么组织";常用 API 解决"日常功能怎么快速完成";集合解决"一批数据怎么存和取";Stream 解决"集合数据怎么更简洁地处理";异常解决"出错后怎么处理";IO 解决"数据怎么读写";多线程解决"多个任务怎么同时执行";网络编程解决"程序之间怎么通信";反射、注解和动态代理解决"框架为什么能自动做事"。

总复习时,不需要把每个 API 都背成清单。更重要的是知道每个知识点解决什么问题、什么时候使用、容易错在哪里。能顺着这条线复习,JavaSE 就不会散成一堆类名。

参考资料

相关推荐
石榴树下的七彩鱼1 小时前
图片去文字接口,支持去除图片中的文字(附 Python / Java / PHP / JS 示例)
java·python·php·api接口·图片去水印·ai图片修复·图片去文字
zzz_23681 小时前
【Java基础】HashMap——为什么JDK 7扩容会死循环,JDK 8又是怎么修好的
java·开发语言
Sam09271 小时前
1 个 Java 服务可以支撑多少 SSE 连接:从线程模型到容量评估
java·人工智能·ai
云器科技1 小时前
云器技术问答 Vol.2:揭秘通用增量计算
java·开发语言
枫叶v.1 小时前
Agent 开发架构:从增强型 LLM 到可运维的自治系统
开发语言·python
.千余3 小时前
【C++】C++ set 与 multiset 完全指南:关联式容器入门
开发语言·c++·笔记·学习·其他
c++之路6 小时前
CMake 系列教程(二):基础命令详解
开发语言·c++
阿维的博客日记8 小时前
Hippo4j 线程池监控平台部署手册
java·spring boot·后端
南境十里·墨染春水10 小时前
C++ 工厂模式:从入门到进阶,彻底掌握对象创建的艺术
开发语言·c++·算法