Java 基础笔记:语法、常用类与面向对象
📝 笔记摘要:Java 基础,从语法到面向对象
Java 语法不难磨人的是「规矩」。什么必须大写、什么不能改、什么能被继承、什么只能实现、哪些异常必须处理——这些约定编译器不提醒的时候没人拦你,等线上炸了才知道。
这篇按入门到进阶的顺序把 32 个知识点捋一遍,每个点都是四步:它是什么、语法长什么样、代码怎么写、容易踩哪。照着敲一遍比读十遍快,纯看很容易觉得都会,写出来才知道哪句没记住。
上篇 语法与常用类
1. 开发环境配置 2. 第一个程序与基本语法 3. 注释
4. 对象和类 5. 基本数据类型 6. 变量类型
7. 变量命名规则 8. 修饰符 9. 运算符
10. 循环结构 11. 条件语句 12. switch case
13. Number & Math 类 14. Character 类 15. String 类
16. StringBuffer 与 StringBuilder 17. 数组 18. 日期时间
19. 正则表达式 20. 方法 21. 构造方法
22. 文件与 IO 23. Scanner 类 24. 异常处理
下篇 面向对象
25. 封装 26. 继承 27. 多态
28. 抽象类 29. 接口 30. 重写与重载
31. 包 32. 反射
1. 开发环境配置
JDK 装完能用了才谈得上写代码。这节定三件事:装哪个版本、环境变量怎么配、怎么确认装对了。
JDK 不是一样东西,是三样的合集:javac 把源码编译成字节码,java 加载字节码交给虚拟机执行,JVM 是真正跑字节码的那个虚拟机。只装 JRE 的话,java 命令有、javac 没有,编译这一步直接断掉。

# 装完 JDK 配两个环境变量
setx JAVA_HOME "C:\Program Files\Java\jdk-21"
setx PATH "%PATH%;%JAVA_HOME%\bin"
# 验证,两个版本号对得上就算通了
java -version
javac -version
JAVA_HOME 指的是 JDK 根目录,不是 bin 目录
PATH 里加的是%JAVA_HOME%\bin,指错层级 Maven、Gradle 会报找不到 JAVA_HOME
版本按用途挑
长期支持版是 8 / 11 / 17 / 21 / 25,最新的 LTS 是 25,2025 年 9 月发布
非 LTS 版半年一版,2026 年出了 26 和 27,生产环境别用,官方只给 LTS 长期维护
新项目从 21 起步,老系统要兼容才用 8
报「javac 不是内部或外部命令」是装错了,不是环境变量错了
这说明装的是 JRE 或者 PATH 那一行写错了,重装 JDK 比反复调 PATH 快
版本管理工具可以先不装
IDEA 自带 JDK 管理,命令行党再上 jenv 或 sdkman 也来得及
2. 第一个程序与基本语法
写完一个 .java 文件,javac 编译出 .class,java 再把 class 跑起来。两步是分开的,IDE 只是把这两步包起来了。
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
# 编译,生成 HelloWorld.class
javac -encoding UTF-8 HelloWorld.java
# 运行,注意这里写的是类名,不带 .class
java HelloWorld
源文件名必须和 public 类名完全一致
不一致直接编译失败:class HelloWorld is public, should be declared in a file named HelloWorld.java
类名不带 public 时文件名能随意,但同名是好习惯
大小写敏感
Hello 和 hello 是两个东西,类名、变量名、方法名都算
main 是唯一入口,签名不能改
写成public static void main(String[] args),参数名可以换,签名改了报 Main method not found
中文乱码就在编译那一步
javac -encoding UTF-8,漏了这个参数,源码里的中文会变成问号
IDE 里改编码只改文件不解决,终端编译还是得带参数
3. 注释
Java 有三种注释,作用完全不同:单行注释、多行注释、文档注释。前两种给看代码的人,第三种给生成 API 文档的工具。
// 单行注释,解释一行在干什么
/*
多行注释
可以写好几行
*/
/**
* 文档注释
*
* @param name 传入的名字
* @return 处理后的结果
*/
文档注释只认
/***开头
写成/*开头就退化成普通注释,@param这些标签不会被识别
注释写「为什么」,不写「做了什么」
// 累加总分这种等于没写,代码本身就说清了
值得写的是// 这里的分转成厘是历史原因,接口协议改不了
注释掉的代码要删
留着是给三个月后的自己挖坑,版本管理里有历史,查得回来
4. 对象和类
类是模板,对象是按这个模板造出来的实例。一条狗是对象,「狗」这个类是模板;类里装着属性(状态)和方法(行为)。
public class Dog {
String name; // 属性
int age;
public void bark() { // 方法
System.out.println(name + " 在叫");
}
public static void main(String[] args) {
Dog d = new Dog(); // 造一个对象
d.name = "旺财";
d.age = 3;
d.bark();
}
}
成员变量有默认值,局部变量没有
int 是 0、boolean 是 false、char 是 \u0000、对象类型是 null
局部变量必须先赋值再使用,否则编译报错,这是 Java 帮你在编译期拦下的坑
一个类只能有一个 public 类
文件名跟这个 public 类同名,而且大小写要一致
static 方法里不能直接用 this
因为 static 属于类,不属于某个对象,想访问成员得先 new 一个出来
5. 基本数据类型
Java 有 8 种基本类型,4 种整型、2 种浮点型、1 个字符、1 个布尔。剩下的都是对象类型(包装类)。
整数型
byte 8 位 -128 ~ 127
short 16 位 -32768 ~ 32767
int 32 位 约 ±21 亿,日常够用
long 64 位 约 922 亿亿,时间戳、订单号用这个
浮点型
float 32 位 单精度,精度约 7 位十进制
double 64 位 双精度,精度约 15 位,小数默认是它
其他两个
char 16 位 一个字符,用单引号,能存一个汉字
boolean 1 位 只有 true 和 false,不能拿 0 / 1 代替
import java.math.BigDecimal;
public class TypeDemo {
public static void main(String[] args) {
byte b = 127;
short s = 32767;
int i = 2147483647;
long l = 9223372036854775807L; // 不加 L 按 int 解析,直接编译失败
float f = 3.14F; // 不加 F 按 double 解析,再转回 float 会报错
double d = 3.14;
char c = 'A';
boolean flag = true;
System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));
System.out.println("和 0.3 相等吗:" + (0.1 + 0.2 == 0.3));
BigDecimal money = new BigDecimal("0.1").add(new BigDecimal("0.2"));
System.out.println("BigDecimal 求和 = " + money);
}
}
浮点数存的是二进制近似值,不是十进制精确值
所以0.1 + 0.2不等于0.3,这是 IEEE 754 的规则,不是 bug
金额永远别用 double
用BigDecimal,而且必须用字符串构造
new BigDecimal("0.1")对,new BigDecimal(0.1)错——后者把那个近似值原样存进去了
byte 溢出不会报错,会绕回负数
byte n = (byte) 127; n++;得到 -128,做计数和位运算时很容易踩
char 只能存一个字符
'AB'编译不过,多个字符要用 String
6. 变量类型
按位置分三种:方法里的是局部变量,类里非 static 的是成员变量(实例变量),类里带 static 的是类变量。

public class VariableDemo {
int instanceVar = 10; // 成员变量,每个对象一份
static int staticVar = 20; // 类变量,整个类一份
public void method() {
int localVar = 30; // 局部变量,方法结束就没了
System.out.println(instanceVar + staticVar + localVar);
}
public static void main(String[] args) {
VariableDemo d = new VariableDemo();
d.method();
System.out.println(VariableDemo.staticVar); // 类变量用类名访问
}
}
成员变量跟着对象走,类变量跟着类走
类变量被所有对象共享,改一个全体都变,常拿来放常量或者计数器
局部变量有作用域,出了那个大括号就没了
成员变量没有作用域,全类可见
成员变量的默认值是 JVM 帮你初始化的
局部变量不给默认值,这是 Java 和 C 的区别,也是 Java 更安全的地方
7. 变量命名规则
硬规则不多,但违反了一条就编译不过。
只能由字母、数字、$、下划线组成
首字符不能是数字,123abc 是非法的
不能用关键字,class、int、new 这些都别拿来当变量名
大小写敏感,age 和 Age 是两个变量
Java 允许 $ 开头,别的语言不一定允许
合法但不推荐,$ 在正则和 shell 里都有特殊含义
全角空格、中文标点不能出现在标识符里
硬规则之外是命名习惯,团队协作时比编译器更值得较真。
int age = 18; // 变量、方法:小驼峰
int maxSize = 100;
String userName = "mavis"; // 多个单词,第二个起首字母大写
int MAX_RETRY_COUNT = 3; // 常量:全大写,下划线分隔
class OrderService { } // 类:大驼峰,每个单词首字母大写
// int 1age = 18; // 编译错误,数字开头
// int class = 18; // 编译错误,关键字
别用拼音,别用单字母
shuzi、nm这种过三个月自己都看不懂
循环变量用i、j、index是惯例,别的地方就别省了
布尔变量用 is / has / can 开头
isEmpty、hasPermission,读代码时能省一半猜测
长短名的取舍看生命周期
用得越久的名字越要写全,临时变量短一点没关系
8. 修饰符
修饰符分两类:控制可见性的访问控制修饰符,和控制行为的非访问控制修饰符。
访问控制
public 任何地方都能访问
protected 同包 + 子类能访问
默认(不写) 同包能访问,跨包就不行了
private 只有本类能访问
非访问控制
final 不能被继承、方法不能被重写、变量只能赋值一次
abstract 抽象类抽象方法,必须由子类实现
static 属于类不属于对象
synchronized 同步,多线程下加锁用
transient 序列化时忽略这个字段
volatile 保证多线程可见性,不保证原子性
native 方法体由其他语言实现
public class ModifierDemo {
public static final String VERSION = "1.0"; // 常量惯例:全大写
private int count = 0; // 私有,外部改不了
public void add() {
count++;
}
public int getCount() {
return count;
}
}
能用 private 就别用 protected
protected 把实现细节暴露给了子类,改起来到处是牵一发动全身
字段一律 private,靠方法暴露
需要读就写 getter,需要改就写 setter,顺便能在里面加校验
常量用static final
static 保证全类一份,final 保证不会被改
default(不写)不是关键词
Java 里真要写 default 关键字,是用在接口里的默认方法上,含义完全不同
9. 运算符
算术、关系、逻辑、位、赋值、条件、类型转换,七类。位运算平时用得少,但处理标志位、掩码的时候很顺手。
public class OperatorDemo {
public static void main(String[] args) {
int a = 10, b = 3;
System.out.println(a + b); // 13
System.out.println(a / b); // 3,整数除整数取整,不是 3.33
System.out.println(a % b); // 1,取余
System.out.println(a >> 1); // 5,右移一位等于除 2
System.out.println(a << 1); // 20,左移一位等于乘 2
System.out.println(a > b && b > 0); // true,&& 短路
System.out.println(a > b || b > 5); // true,|| 短路
int c = 10;
c += 5; // 复合赋值
System.out.println(c);
int max = a > b ? a : b; // 条件运算符
System.out.println(max);
long big = a; // int 自动转 long
int small = (int) big; // long 转 int 要显式强转
System.out.println(small);
}
}
整数除法是截断,不是四舍五入
10 / 3是 3,10.0 / 3才是 3.333
算钱、算百分比记得有一方是浮点
==比的是引用,不是内容
两个 Integer 装同一个值,引用可能不同,内容才相等
短路是 Java 免掉空指针的神器
obj != null && obj.size() > 0顺序不能反,反了就 NPE
强制转换会静默丢精度
(int) 3.99是 3,不是 4
10. 循环结构
三种循环:for、while、do-while。区别在于判断时机——for 和 while 是先判断再执行,do-while 是先执行再判断,所以至少跑一次。
public class LoopDemo {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
System.out.println("for 第 " + i + " 次");
}
int j = 1;
while (j <= 3) {
System.out.println("while 第 " + j + " 次");
j++;
}
int k = 1;
do {
System.out.println("do-while 第 " + k + " 次");
k++;
} while (k <= 3);
for (String s : new String[]{"a", "b"}) { // 增强 for
System.out.println(s);
}
}
}
break 跳出整个循环,continue 跳到下一轮
continue 后面那半轮代码不执行,但循环本身还在跑
do-while 末尾的分号别漏
漏了编译报错,是新手第一大坑
增强 for 遍历集合时不能增删元素
会抛 ConcurrentModificationException,要改就换普通 for 或者 Iterator
死循环的两种写法
while (true)和for (;;),前者更常见
11. 条件语句
if / else if / else 的核心是只走一条分支,从上往下第一个满足的就执行,后面的全跳过。
public class IfDemo {
public static void main(String[] args) {
int score = 85;
if (score >= 90) {
System.out.println("优秀");
} else if (score >= 60) {
System.out.println("及格");
} else {
System.out.println("不及格");
}
}
}
条件里是布尔值,不是 0 和 1
Java 不允许if (count),必须写if (count > 0)
这是 Java 比 C 严格的地方,少了一整类 bug
条件表达式可以很复杂,但别写
多个条件用 && 和 || 拆开,必要时提成一个有名字的布尔变量
大括号别省
IDE 能补全,但复制粘贴时容易只带走一行
12. switch case
switch 适合「一个值对应一堆选项」的场景,比如状态码、星期、菜单编号。老写法靠 break 防穿透,Java 14 起的箭头写法不穿透。

public class SwitchDemo {
public static void main(String[] args) {
int day = 3;
// 老写法:漏了 break 就会往下穿透
switch (day) {
case 1:
case 2:
System.out.println("工作日");
break;
case 6:
case 7:
System.out.println("周末");
break;
default:
System.out.println("其他");
}
// 箭头写法(Java 14+):不穿透,能当表达式用
String name = switch (day) {
case 6, 7 -> "周末";
default -> "工作日";
};
System.out.println(name);
// 箭头写法里想写多行,用 yield 产值
int level = switch (day) {
case 6, 7 -> {
System.out.println("休息日");
yield 1;
}
default -> {
yield 0;
}
};
System.out.println(level);
}
}
穿透不一定是 bug
case 1:下面不写 break 直接落进case 2:,是合并分支的常用写法
但漏写 break 去做多分支,是最难查的一类错
箭头写法不能和冒号写法混用
同一个 switch 里要么全冒号要么全箭头,混着写编译报错
switch 表达式要穷尽所有情况
枚举类型全覆盖可以不带 default,其他类型不带 default 编译不过
Java 21 起 switch 支持模式匹配
能按类型分派,case Integer i -> ...这种写法
13. Number & Math 类
Number 是所有数字包装类的父类,Integer、Long、Double 这些都继承自它。Math 是一堆静态数学方法,不用 new。
public class NumberDemo {
public static void main(String[] args) {
int a = Integer.parseInt("123"); // 字符串转数字
double b = Double.parseDouble("3.14");
Integer boxed = 42; // 自动装箱
int unboxed = boxed; // 自动拆箱
System.out.println(a + b);
System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.toBinaryString(10)); // 1010
System.out.println(Math.abs(-5));
System.out.println(Math.max(3, 7));
System.out.println(Math.pow(2, 10)); // 1024.0
System.out.println(Math.sqrt(16)); // 4.0
System.out.println(Math.random());
}
}
parseInt 遇到非数字直接抛异常
NumberFormatException,不会像Integer.parseInt那样给你个 0
从外部拿的字符串转数字,一定要包在 try 里
自动装箱有缓存
Integer.valueOf对 -128 到 127 之间的值返回缓存对象,所以这个区间里 == 和 equals 结果一样,超出就不一样了
Math.random()返回的是 [0, 1) 的 double
不是 int,要整数得自己乘完再转,而且转的时候用 (int) 是截断不是四舍五入
包装类和基本类型别混着比大小
包装类之间比较用 equals,比大小要先拆箱
14. Character 类
Character 把 char 那些常用判断包成了静态方法,写字符校验的时候不用自己查 ASCII 表。
public class CharacterDemo {
public static void main(String[] args) {
char c = 'A';
System.out.println(Character.isDigit('5')); // true
System.out.println(Character.isLetter(c)); // true
System.out.println(Character.isUpperCase(c)); // true
System.out.println(Character.isLowerCase(c)); // false
System.out.println(Character.isWhitespace(' ')); // true
System.out.println(Character.isAlphabetic('汉')); // true,中文也算字母
System.out.println(Character.toLowerCase(c)); // a
System.out.println(Character.toUpperCase(c)); // A
System.out.println(Character.getNumericValue('7')); // 7
}
}
isLetter 对汉字返回 true
因为它判断的是 Unicode 字母,不是 ASCII 范围
只认英文字母的话用 isAlphabetic 自己配范围判断
Character 是个类,char 是关键字
两者不一样,方法调用方式也不同,混着写容易把方法调到了包装类上
取 ASCII 码直接强转
(int) 'A'得 65,不需要额外方法
15. String 类
String 是 Java 里最常用的类,它的核心特性是不可变——创建之后内容永远改不了。所谓「修改」其实是造了一个新的字符串。

public class StringDemo {
public static void main(String[] args) {
String s = "Hello, Java";
System.out.println(s.length()); // 11
System.out.println(s.charAt(0)); // H
System.out.println(s.substring(7)); // Java
System.out.println(s.replace("Java", "Go")); // Hello, Go
System.out.println(s.indexOf("Java")); // 7
System.out.println(s.toUpperCase());
System.out.println(s.contains("Java")); // true
System.out.println(s.split(", ")[0]); // Hello
System.out.println(String.join("-", "a", "b", "c")); // a-b-c
System.out.println(String.format("%s 今年 %d 岁", "小明", 18));
String a1 = "abc";
String b1 = new String("abc");
System.out.println(a1 == b1); // false,引用不同
System.out.println(a1.equals(b1)); // true,内容相同
}
}
比内容用 equals,别用 ==
== 比的是引用地址,两个内容一样的字符串用 == 比可能相等也可能不相等,看是不是同一个常量
字符串常量池里放的是字面量,new String("abc")一定在堆上另开一份
字符串拼接在循环里就是性能事故
循环里用+=拼字符串,每次都造新对象,正确做法是 StringBuilder
substring 会越界
"abc".substring(3)直接抛异常,边界要自己判
split 的结果可能为空数组
输入为空串时"".split(",")返回长度 1 的数组,里面是空串,遍历时容易多处理一次
16. StringBuffer 与 StringBuilder
两个都是可变的字符串,区别只有线程安全:StringBuffer 的方法带 synchronized,多线程下安全但慢;StringBuilder 不加锁,单线程下快。
public class BufferDemo {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("hello");
sb.append(" world").insert(0, ">> ").reverse();
System.out.println(sb); // >> dlrow olleh
StringBuffer buf = new StringBuffer();
buf.append("a").append("b");
System.out.println(buf); // ab
}
}
绝大多数场景用 StringBuilder
StringBuffer 是给「多线程共享一个字符串缓冲区」准备的,正常业务里很少遇到
这些方法返回的是自己
所以能一路链式调用,不用每步都赋给变量
toString 才是真正拿字符串的地方
sb.toString(),字符串拼接场景最后都要转一次
Java 字符串连接底层也在用 StringBuilder
但编译器只在能确定是常量拼接时这么优化,循环里的拼接仍然是逐次新建
17. 数组
数组长度创建后不能改,元素类型固定,元素可以是基本类型也可以是对象。数组本身是对象,length 是它的属性。
import java.util.Arrays;
public class ArrayDemo {
public static void main(String[] args) {
int[] nums = {1, 2, 3}; // 声明并初始化
int[] nums2 = new int[5]; // 默认全是 0
String[] names = new String[]{"a", "b"};
int[][] grid = new int[2][3]; // 二维数组
grid[0][1] = 5;
System.out.println(nums.length); // 3
System.out.println(Arrays.toString(nums)); // [1, 2, 3]
System.out.println(Arrays.stream(nums).sum()); // 6
System.out.println(Arrays.stream(nums).max().getAsInt()); // 3
String[] sorted = {"c", "a", "b"};
Arrays.sort(sorted);
System.out.println(Arrays.toString(sorted)); // [a, b, c]
System.out.println(Arrays.toString(nums2)); // [0, 0, 0, 0, 0]
}
}
数组越界是运行时异常
ArrayIndexOutOfBoundsException,编译期查不出来,所以别指望编译器兜底
arr.length和str.length()不一样
数组是属性,字符串是方法,写反了直接编译报错
数组的默认值有,但数组变量本身没有
int[] a = new int[5]里 5 个 0;int[] b直接用会 NPE
二维数组的行可以不等长
int[][] g = new int[2][];之后每行还得单独 new
打印数组别直接 println
出来的是[I@1b6d3586这种地址,要用Arrays.toString
18. 日期时间
老 API 是 java.util.Date 加 SimpleDateFormat,后者不是线程安全的,多线程下格式化会串。新代码一律用 java.time,它从 Java 8 起就是线程安全的。
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateDemo {
public static void main(String[] args) {
LocalDate today = LocalDate.now(); // 只有日期
LocalDateTime now = LocalDateTime.now(); // 日期加时间
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(fmt.format(now));
System.out.println(today.plusDays(30)); // 30 天后
System.out.println(today.isAfter(LocalDate.of(2020, 1, 1)));
LocalDate start = LocalDate.of(2026, 1, 1);
long days = Duration.between(start.atStartOfDay(), today.atStartOfDay()).toDays();
System.out.println("今年过了 " + days + " 天");
}
}
SimpleDateFormat 不是线程安全的
把它做成静态字段,多线程下会得到乱七八糟的时间,要静态就ThreadLocal包一层
新代码直接用DateTimeFormatter,它是线程安全的,可以放心做常量
日期时间分好几种类型
LocalDate日期、LocalDateTime本地日期时间、Instant时间戳瞬间、ZonedDateTime带时区
时区是坑
服务器时间、数据库时间、用户时间三套,跨时区业务一律存 UTC 或时间戳
月份从 0 开始,日期从 1 开始
LocalDate.of(2026, 1, 1)的 1 是一月,但Calendar里是 0,老代码迁移时这里必错
19. 正则表达式
正则用来做字符串的模式匹配。Java 里用 Pattern 编译模式、Matcher 去匹配,两者配合使用。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexDemo {
public static void main(String[] args) {
String text = "订单号 A20260723,金额 129.50 元";
Pattern p = Pattern.compile("A(\\d{8})");
Matcher m = p.matcher(text);
if (m.find()) {
System.out.println("订单号:" + m.group(1)); // 20260723
}
System.out.println(text.replaceAll("\\d+", "*")); // 把所有数字换掉
System.out.println("A20260723".matches("A\\d{8}")); // true,整串匹配
}
}
find 和 matches 不是一个东西
find()是「找一段匹配」,matches()是「整串必须匹配」
校验手机号用 matches,用 find 会因为前面有别的字符而误判通过
group(0) 是整个匹配,group(1) 是第一组括号
想拿到括号里的内容,正则里必须有括号
Pattern 可以预编译复用
把 Pattern 提成静态常量,别在循环里反复Pattern.compile
Java 字符串里的反斜杠要写两个
\d在 Java 字符串里是非法转义,得写成\\d
20. 方法
方法是类里的行为单元。一个方法有名字、参数、返回值,返回值类型写 void 就是不返回。
public class MethodDemo {
// 有返回值
public static int add(int a, int b) {
return a + b;
}
// 可变参数,个数不定
public static int sum(int... nums) {
int total = 0;
for (int n : nums) {
total += n;
}
return total;
}
// 不返回,直接打印
public static void log(String msg) {
System.out.println("[LOG] " + msg);
}
public static void main(String[] args) {
System.out.println(add(1, 2)); // 3
System.out.println(sum(1, 2, 3, 4)); // 10
System.out.println(sum()); // 0,可变参数可以不传
log("方法跑起来了");
}
}
一个方法只干一件事
一个方法同时做三件事,就得拆成三个,调用处才读得懂
可变参数必须是最后一个参数
而且它本身是个数组,一个方法只能有一个可变参数
方法名 + 参数列表相同就是重载
返回类型不同不算重载,编译报错
参数是值传递
传对象进去,改的是对象内容,把对象的引用重新赋值回去是做不到的
返回值尽量不要返回数组本身
调用方一改就改到内部数据,要返回就返回副本
21. 构造方法
构造方法没有返回值类型,名字必须和类名一模一样。new 的时候它负责把对象初始化好。

public class User {
private String name;
private int age;
static {
System.out.println("静态代码块:类加载时执行,整个类只跑一次");
}
{
System.out.println("实例代码块:每次 new 都执行");
}
public User(String name) {
this.name = name;
}
public User(String name, int age) {
this(name); // 调用另一个构造方法,必须是第一句
this.age = age;
}
}
初始化顺序:静态块 → 实例块 → 构造方法
静态块只跑一次,实例块每次 new 都跑,这个顺序在继承里还会再叠一层
不写构造方法,编译器会自动补一个空的
但你一旦自己写了任何一个,编译器就不再补了,new User()会直接编译失败
this(...)必须是构造方法的第一句
因为调用它之前 this 还没初始化好
构造方法里能抛异常
对象创建到一半失败,Java 保证不会返回一个半成品对象
22. 文件与 IO
老 API 是 File 配 FileInputStream,新 API 是 java.nio.file 下的 Path 和 Files。新代码直接用后者,写起来短一大截。
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class FileDemo {
public static void main(String[] args) throws Exception {
Path path = Paths.get("notes.txt");
Files.write(path, List.of("第一行", "第二行"), StandardCharsets.UTF_8);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
System.out.println(lines);
// 流式读,大文件用这个
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
try-with-resources 是必须的
括号里声明的流,用完自动关闭,就算中途抛异常也关
手写finally里 close 的话,close 自己抛异常会把原异常盖掉
字符集永远显式指定
Windows 上默认 GBK,Linux 上默认 UTF-8,不写就是跨平台乱码
读大文件别用 readAllLines
它会把整个文件读进内存,流式一行一行读才是正解
Paths.get 处理分隔符
Windows 写C:\\data\\a.txt,Linux 上要写/data/a.txt,用 Paths.get 拼路径能跨平台
服务端接收上传文件的完整流程见 Spring Boot 文件上传
23. Scanner 类
Scanner 从控制台、文件、字符串里读数据。控制台输入最常用,但它有两个坑几乎是每个人都踩过。
import java.util.Scanner;
public class ScannerDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入姓名:");
String name = sc.nextLine(); // 读整行,含空格
System.out.print("请输入年龄:");
int age = sc.nextInt(); // 只读一个数字
sc.nextLine(); // 把上一行剩下的换行符吃掉
System.out.println(name + " " + age);
sc.close(); // 关掉 System.in,后面就不能再读了
}
}
next 和 nextLine 混用会漏内容
nextInt()读走数字但不读走行尾的换行符
下一句nextLine()读到的是空字符串,多一个sc.nextLine()把它吃掉
next 系列遇到空格就停
输入姓名用nextLine(),next()只能拿到第一个词
Scanner 比 BufferedReader 慢
只是控制台交互用它,读文件还是走BufferedReader
关掉 Scanner 等于关掉 System.in
关了之后整个进程不能再读标准输入,单元测试里踩过这个坑
24. 异常处理
Java 的异常分两种:受检异常(checked)编译期强制处理,非受检异常(RuntimeException)可以不处理但通常应该处理。

public class ExceptionDemo {
public static void main(String[] args) {
try {
int[] arr = {1, 2, 3};
System.out.println(arr[5]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界:" + e.getMessage());
} finally {
System.out.println("不管有没有异常都会执行");
}
try {
System.out.println(Integer.parseInt("abc"));
} catch (NumberFormatException e) {
System.out.println("不是合法数字");
}
}
}
// 自定义异常一般继承 RuntimeException(不受检,不用在每个调用点声明)
class BizException extends RuntimeException {
public BizException(String message) {
super(message);
}
}
// 受检异常用 throws 声明,调用方必须 catch 或者继续 throws
static int readAge() throws java.io.IOException {
return Integer.parseInt("18");
}
catch 要具体,别直接 catch (Exception e)
全都抓住等于没抓住,而且会顺手把 NullPointerException 这种代码 bug 一起吞掉
异常要处理,不要只打印然后继续跑
catch 之后什么都不做,等于把故障藏起来
finally 用来释放资源
但释放资源优先用 try-with-resources,它比 finally 可靠
自定义异常继承 RuntimeException
这样调用方不用被迫写一堆无用的 catch
throw 抛一个,throws 声明会抛
这两个词位置不同:throws 写在方法签名上,throw 写在方法体里
25. 封装
封装是把数据藏在类里面,不让人直接改,只能通过你设计好的方法进出。目的是让内部怎么存不影响外部怎么用。
public class Account {
private double balance; // 私有,外部拿不到也改不了
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("金额必须大于 0");
}
balance += amount;
}
public void withdraw(double amount) {
if (amount > balance) {
throw new IllegalStateException("余额不足");
}
balance -= amount;
}
public double getBalance() {
return balance;
}
}
字段一律 private
对外只暴露方法,内部字段改名、换存储结构都不影响调用方
校验写进方法里,而不是靠调用方自觉
上面那个deposit里金额为负会直接抛异常,比注释里写「请传正数」靠谱得多
getter 不一定都要写
只读的属性给个 getter 就够,setter 能不给就不给,少一个改入口少一个风险
别在 toString 里暴露敏感字段
密码、密钥这类字段要么不打印,要么打码
26. 继承
继承让子类复用父类的属性和方法,也可以加自己的东西。Java 的类只能单继承,接口可以多实现,这是后面多态的基础。
class Animal {
protected String name;
public void eat() {
System.out.println(name + " 在吃东西");
}
}
class Dog extends Animal {
public Dog(String name) {
this.name = name;
}
public void bark() {
System.out.println(name + " 在叫");
}
}
// 用起来
Dog d = new Dog("旺财");
d.eat(); // 继承来的
d.bark(); // 自己加的
子类调不到父类的 private 成员
private 只在本类可见,子类想用得改成 protected,或者父类提供方法
构造方法不继承
子类的构造方法第一句默认调super(),父类没无参构造就编译不过
Java 单继承,接口多实现
需要「多继承」的效果就走接口,这比 C++ 的多继承干净得多
final 类不能被继承
final 方法不能被重写,这两条是给框架和库留的扩展锁
继承表达「是一个」,组合表达「有一个」
能用组合就别用继承,继承把父子绑死,改父类子类全得跟着改
27. 多态
多态是「同一个接口,不同的实现」。父类引用指向子类对象,调用的方法在运行时按对象的实际类型决定。

class Animal {
public void speak() {
System.out.println("动物叫");
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("汪汪");
}
}
class Cat extends Animal {
@Override
public void speak() {
System.out.println("喵喵");
}
}
public class PolyDemo {
public static void main(String[] args) {
Animal a = new Dog(); // 父类引用指向子类对象
a.speak(); // 输出 汪汪
// a.bark(); // 编译不过,a 的类型是 Animal,里面没有 bark
Animal[] animals = {new Dog(), new Cat()};
for (Animal an : animals) {
an.speak(); // 运行时按实际类型分派
}
}
}
编译看引用类型,运行看实际类型
这就是「编译期静态绑定 + 运行期动态绑定」,多态只发生在运行期动态绑定的那一层
引用类型决定你能调哪些方法
实际类型决定跑出来是什么效果
多态的三个前提
有继承(或实现)、有重写、父类引用指向子类对象,少一条都不成立
用父类类型统一处理,是框架能扩展的根本原因
Spring 的 Bean、MyBatis 的 Mapper 全是这套机制,MyBatis 基础那篇里接口加 XML 的组合就是典型例子
28. 抽象类
抽象类不能实例化,但可以有构造函数、成员变量、普通方法。它适合「一类东西有共同的骨架,但关键步骤各不相同」。
abstract class Shape {
protected String name;
public Shape(String name) {
this.name = name;
}
// 抽象方法:只有声明没有实现,子类必须实现
abstract double area();
// 普通方法:所有子类共用的逻辑,直接写实现
public void print() {
System.out.println(name + " 的面积:" + area());
}
}
class Circle extends Shape {
private double r;
public Circle(double r) {
super("圆");
this.r = r;
}
@Override
double area() {
return Math.PI * r * r;
}
}
抽象类可以有构造方法
用来初始化子类共用的字段,但不能 new 出抽象类本身
抽象方法不能是 private、static、final
这三个和「子类必须重写」是矛盾的,编译器会拦
抽象类里的普通方法子类可以重写,也可以不重写
只有抽象方法是强制的
有抽象方法的类必须声明成抽象类
反过来,抽象类里可以一个抽象方法都没有
29. 接口
接口是纯粹的契约,只描述「能做什么」,不关心「怎么做」。Java 8 之后接口可以有 default 方法和静态方法。
interface Flyable {
// 抽象方法,隐式 public abstract
void fly();
// default 方法,有默认实现,实现类可以不重写
default void land() {
System.out.println("开始降落");
}
// static 方法,属于接口本身,不能被实现类继承
static boolean isFlyable(Object o) {
return o instanceof Flyable;
}
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("扑棱棱飞走了");
}
}
接口里的字段隐式是 public static final
写不写修饰符都一样,所以接口常量天然是常量
接口不能有实例字段
状态放接口里不合适,要状态就用抽象类
接口不能有构造方法
它不是类,不能 new
抽象类和接口怎么选
有状态、有共同实现、需要单继承 → 抽象类
纯行为契约、要多实现 → 接口
Java 8 之后接口和抽象类的边界模糊了
default 方法让接口也能带实现,但依然不能有状态
接口可以继承接口,一个类可以实现多个接口
但一个类只能继承一个类,这个限制还在
30. 重写与重载
两个词长得像,意思完全相反:重载是同一个类里方法名相同参数不同,重写是子类重新实现父类的方法。
class Animal {
public void speak() {
System.out.println("动物叫");
}
public int sum(int a, int b) {
return a + b;
}
}
class Dog extends Animal {
// 重写:签名完全一致,子类重新实现
@Override
public void speak() {
System.out.println("汪汪");
}
// 重载:同一个类里方法名相同,参数个数不同
public int sum(int a, int b, int c) {
return a + b + c;
}
}
判断标准
在不在一个类里、在不在父子关系中——重载看类,重写看继承
重写要求方法签名完全一致
返回类型要么一样,要么是协变的(父类返回 Object、子类返回 String 可以)
重写不能缩小可见性
父类 public 的方法,子类重写成 protected 会编译报错
static 方法只能隐藏不能重写
private 方法根本不参与继承,不存在重写
加上@Override注解
方法名写错、参数写错,编译器立刻报错,没有这注解就是「新增了一个重载」,编译能过但逻辑全错
final 方法禁止重写
父类标了 final,子类再写同名方法就是编译错误
31. 包
包用来组织类,避免类名冲突,也限定访问权限。包名必须和目录结构一致。
// 文件路径:com/example/demo/Main.java
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
System.out.println(list);
}
}
包名和目录必须一一对应
包声明是com.example.demo,文件就得放在com/example/demo/下
同一个包里互相用不需要 import
java.lang 包自动导入,String、System 这些不用写 import
默认权限是「包级可见」
同包能用,跨包就得 public,这跟文件系统目录权限是两回事
包名全小写,用域名反写
com.example.demo这种,避免全网冲突
分包按职责分
controller / service / dao / model 这种分层,或者按 feature 分,别把一百个类扔一个包里
32. 反射
反射让程序在运行时去检查类、创建对象、调用方法。日常业务用得不多,框架和工具库靠它干大量底层活。
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class ReflectDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("java.lang.String");
Method[] methods = clazz.getDeclaredMethods();
System.out.println("方法数量:" + methods.length);
Constructor<?>[] ctors = clazz.getDeclaredConstructors();
System.out.println("构造方法数量:" + ctors.length);
// 调用实例方法
Method length = clazz.getDeclaredMethod("length");
length.setAccessible(true); // 绕过访问检查
Object result = length.invoke("hello");
System.out.println(result);
// 反射创建对象
Constructor<?> ctor = clazz.getDeclaredConstructor();
ctor.setAccessible(true);
Object obj = ctor.newInstance();
System.out.println(obj.getClass().getName());
}
}
反射的代价是性能
反射调用比直接调用慢一个数量级,热点路径上别用
setAccessible 能绕过 private
Java 9 之后模块系统会拦一部分,不是万能钥匙
newInstance 已经废弃
用getDeclaredConstructor().newInstance(),老的那个在部分版本里已经抛异常
反射拿到的异常在 InvocationTargetException 里
真正的异常是它的 cause,直接 getMessage() 拿到的是包装信息
它的真正用武之地
Spring 的依赖注入、MyBatis 的 Mapper 代理、JUnit 的断言,全靠反射,日常业务里手写反射的场合不多
💡 补充几个容易忽略的:
编码从项目第一天就定死
源码 UTF-8、编译加-encoding UTF-8、读写文件显式指定字符集,三处对齐才不会出现「我这儿好好的」
异常不要吞,也不要只打日志
该恢复就恢复,该往上抛就往上抛,实在处理不了至少留下足够排查的现场
别在循环里 new 正则、SimpleDateFormat
提成静态常量,这是最常见也最容易忽略的性能坑
集合初始化指定容量
知道大概要放多少就写new ArrayList<>(100),省掉一连串扩容拷贝
官方文档和教程的定位不一样
官方文档按主题排,适合查细节;菜鸟教程的 Java 目录按学习顺序排,适合当路线图
两边对着看,学的顺序和查的效率都能兼顾
学框架之前先把集合和泛型吃透
Spring、MyBatis 里到处是泛型集合,看不懂的话源码根本读不动
🔥 一句话总结:JDK 装 21 或 25 长期支持版,
JAVA_HOME指根目录不指 bin;源文件名必须和 public 类名完全一致,javac编译java运行,中文乱码就加-encoding UTF-8;8 种基本类型加包装类,金额一律BigDecimal且必须用字符串构造,浮点数天生不等于 0.3;成员变量有默认值、局部变量没有;字符串不可变,比内容用equals,循环拼接换StringBuilder;日期时间用java.time,SimpleDateFormat不是线程安全的;读文件用 try-with-resources,字符集永远显式指定;catch 要具体,处理不了就往上抛;面向对象四件套是封装藏数据、继承复用代码、多态看实际类型、抽象和接口定契约,重写是子类换实现、重载是同类换参数,加上@Override让编译器帮你查错;反射是框架的地基,自己写之前先想清楚性能账。
