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 代码组织的核心
对应博客:
- javase第三篇:我发现"面向对象三大特性"居然如此简单
- 【JAVASE | 第四篇】代码块------实例、静态与同步全解析
- 【JAVASE | 第五篇】单例模式------饿汉式与懒汉式详解
- 【JAVASE | 第六篇】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 | 第九篇】ArrayList集合详解
- java-从零打造学生管理系统
- 【JAVASE | 第十篇】HashSet、LinkedHashSet与TreeSet详解
- 【JAVASE | 第十二篇】Map集合详解
集合是 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 个参数从前到后分别是:
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 包