欢迎光临
我们一直在努力

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 里单独运行。类名和文件名一致、包名先去掉、输入输出能对上,说明这个知识点不是只看懂了文字,而是真的能写出来。

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

对应博客:

  • javase第一篇:注释,方法,基础数据类型,输入输出
  • java第二篇:java循环和数组

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

1. 数据类型和变量

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

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

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. 运算符和类型转换

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

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
}
}

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

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

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 更适合次数不确定但条件明确的场景。

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. 数组

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

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. 方法和方法重载

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

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 代码组织的核心

对应博客:

  • javase第三篇:我发现“面向对象三大特性”居然如此简单
  • 【JAVASE | 第四篇】代码块——实例、静态与同步全解析
  • 【JAVASE | 第五篇】单例模式——饿汉式与懒汉式详解
  • 【JAVASE | 第六篇】Java抽象类和抽象方法

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

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

1. 类、对象和封装

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

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;
}
}

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

public class Counter {
static int count = 0;

public Counter() {
count++;
}

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

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

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

4. 继承和方法重写

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

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. 多态

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

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. 抽象类

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

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 成员属于对象。
  • 子类构造方法默认会先访问父类无参构造。
  • 多态调用成员方法看实际对象,访问成员变量看引用类型。
  • 抽象类不能创建对象,但可以有构造方法、普通方法和成员变量。

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

对应博客:

  • 【JAVASE | 第七篇】Java接口
  • 【JAVASE | 第八篇】Java内部类全解析

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

1. 接口

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. 代码块

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

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

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

4. 内部类

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

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

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 不可变

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

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 比较字符串内容。

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 更合适。

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 适合拼接有分隔符、前缀、后缀的字符串。

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 | 第九篇】ArrayList集合详解
  • java-从零打造学生管理系统
  • 【JAVASE | 第十篇】HashSet、LinkedHashSet与TreeSet详解
  • 【JAVASE | 第十二篇】Map集合详解

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

1. 泛型

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

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。

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 双向链表 头尾增删较多

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 排序、不重复、无索引 去重并排序

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 按键排序、键不重复 需要按键排序

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 流

对应博客:

  • 【JAVASE | 第十三篇】Stream流

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

1. Lambda

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

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 的进一步简化。条件是被引用的方法已经存在,并且参数和返回值能匹配函数式接口中的抽象方法。

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 适合对集合进行过滤、映射、排序、去重、收集。

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 重复,要考虑合并规则。

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

对应博客:

  • 【JAVASE | 第十一篇】Java异常处理全攻略

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

1. 异常体系

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

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

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

2. try-catch-finally

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 用在方法声明上,表示这个方法可能抛出异常,提醒调用者处理。

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 流:读写文件和数据

对应博客:

  • 【JAVASE | 第十四篇】IO流:普通流和特殊流总结
  • 【JAVASE | 第十五篇】IO流进阶:缓冲字节流和缓冲字符流

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

1. File

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

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. 字节流

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

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. 字符流

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

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. 缓冲流

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

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. 转换流

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

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。

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

对应博客:

  • 【JAVASE | 第十六篇】多线程

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

1. 创建线程的三种方式

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

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

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

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. 线程安全

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

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。线程池可以复用线程,减少频繁创建和销毁线程的成本。

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

对应博客:

  • 【JAVASE | 第十七篇】Java 网络通信

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

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

1. InetAddress

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

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 是面向连接的可靠通信。通信前要先建立连接,建立后通过输入输出流传输数据。

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

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

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

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

对应博客:

  • 【JAVASE | 第十八篇】Java 反射
  • 【JAVASE | 第十九篇】Java 注解入门

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

1. 反射

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

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。

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。

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. 动态代理

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

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:下面输出什么?

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?

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

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

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

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

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:下面哪行更常用?

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 去重:为什么这里没有去重?

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:下面哪个是真正抛异常?

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 更适合文件传输?

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

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

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

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 个参数:分别代表什么?

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 个参数从前到后分别是:

  • corePoolSize:核心线程数。线程池会尽量先保留这部分线程,就算它们暂时空闲也不会立刻回收。
  • maximumPoolSize:最大线程数。任务很多时,线程池最多能扩到这个数量。
  • keepAliveTime:临时线程的空闲存活时间。
  • unit:第 3 个参数的时间单位。
  • workQueue:任务阻塞队列。核心线程忙不过来时,先把任务放到这里排队。
  • threadFactory:线程工厂。线程池创建新线程时,线程对象由它来生产。
  • handler:拒绝策略。当线程数到上限、队列也满了,再来新任务时,决定怎么处理。
  • 面试里更推荐顺着执行流程回答:先看核心线程够不够,不够时进队列,队列满了再扩到最大线程数,还是放不下才触发拒绝策略。这样回答比死背参数名更像真正理解了线程池。

    十四、最后总结

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

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

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

    参考资料

    • Oracle Java SE 25 API Documentation
    • Oracle Java SE 25 API:java.lang.String
    • Oracle Java SE 25 API:java.util 包
    • Oracle Java SE 25 API:java.util.stream 包
    • Oracle Java SE 25 API:java.io 包
    • Oracle Java SE 25 API:java.net 包
    • Oracle Java SE 25 API:java.util.concurrent 包
    • Oracle Java SE 25 API:java.lang.reflect 包
    • Oracle Java SE 25 API:java.lang.annotation 包
    赞(0)
    未经允许不得转载:171主机测评 » JavaSE 总复习:语法到多线程全梳理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址