学习心得 学习笔记 java基础学习笔记 Lynx Frost 2026-08-16 2026-08-16
持续更新优化,部分内容太过基础没写
Java概述 程序:计算机执行某些操作或者解决某个问题而编写的一系列有序指令的集合
Java诞生与1995年 作者 扎姆斯高斯林
Java技术体系平台
Java SE 标准版 桌面应用
Java EE 企业版 Web应用 主要学习这个
Java ME 小型版 移动应用和嵌入式
Java重要特点
面向对象 oop
语言健壮
跨平台
解释型语言 需要解释器
Java虚拟机 JVM 用于在不同的设备上运行.class字节码文件,屏蔽系统差异
JDK、JRE JDK java开发工具包 Java Development Kit 开发者用
JDK=JRE+java开发工具
JRE java运行环境 Java Runtime Environment 普通用户用
JRE=JVM+Java核心类库
注意事项
文件名必须与public类名一致
一个源文件中最多只能有一个public类
编译之后每一个类对应一个.class文件
Java语言严格区分大小写
每个语句以 “;” 结束
java 命令之后跟类名
main 方法就是 Java 程序的唯一合法入口
原则:一个文件只写一个类
特殊转义字符\r 功能:将光标移动到当前行的开头
不同的操作系统对“换行”的定义曾 各不相同:
Windows : 使用 \r\n(回车 + 换行)代表一行结束。
Unix / Linux / macOS : 统一使用 \n(换行)代表一行结束。
\r 有一个非常酷的用途:制作简单的命令行进度条。
for (int i = 0 ; i <= 100 ; i++) { System.out.print("进度: " + i + "%\r" ); Thread.sleep(100 ); }
注释
单行注释 //
多行注释 / /
文档注释 /* /
被注释的文字,不会被JVM解释执行
Java代码规范
类、方法的注释,要以Javadoc的方式来写
非Java Doc 的注释,往往是给代码维护者看的
使用Tab进行右移,shift+tab 进行左移
运算符和=两边习惯性各加一个空格
源文件使用utf-8编码
行宽度不要超过80个字符
代码编写推荐行尾风格
变量 变量是程序的基本组成单位
三要素 :类型+名称+值
变量必须先声明再使用
同一个作用域之内变量不能重名
数据类型 基本数据类型+引用数据类型
\image-20260413125151360.png)
注意事项
整型常量默认为 int 型,声明 long 型常量须后加‘l’或‘L’
浮点型常量默认为 double 型,声明 float 型常量,须后加‘f’或‘F’
“小向大”是顺水推舟,自动完成;“大向小”是强人所难,必须强转。
浮点数=符号位+指数位+尾数位 尾数部分可能丢失,造成精度损失(小数都是近似值)
永远不要用 == 来比较两个浮点数。应该判断它们的差值是否小于一个极小的数值(如 1e-9)
当我们对运算结果是小数的进行相等判断是,要小心应该是以两个数的差值的绝对值,在某个精度范围类判断
char字符常量只能用单引号''引用,只能存单个字符或者是数字,本质是存储的是一个整数unicode对映的编码,可以进行计算
补充知识浮点数的计算机存储 计算机里的浮点数就像一个精密的二进制科学计数法 ,其存储结构固定为:
符号位 (Sign) :1位。0正1负。
指数位 (Exponent) :
作用 :决定数值范围(小数点移动多少位)。
黑科技 :使用偏移码(Bias) ,通过给真实指数加一个固定值(如127),强行把负指数变成正整数,方便硬件直接比较大小。
尾数位 (Mantissa) :
作用 :决定数值精度(长什么样)。
黑科技 :隐藏整数位的 1 。既然二进制规格化后第一位必然是1,干脆省掉不存,多赚1位精度。
+号的作用
当左右两边都是数值类型时,则做加法运算
当左右两边有一方为字符串,则做拼接运算
运算顺序,从左到右
基本数据类型转换 路径 A: byte → short → int → long → float → double
路径 B: char → int → long → float → double
为什么long是8个字节,float是4个字节,但是long会自动转为float? 它们底层的二进制存储方式完全不同。数值范围的大小并不取决于它占用了多少字节,而取决于它的结构设计
long(64 位定点数) :1 位符号位 + 63 位数值位,纯精确整数。
float(32 位 IEEE 754 浮点数) :1 位符号位 + 8 位指数位 + 23 位尾数位。
long(8 字节):最大值约 $9.22 \times 10^{18}$
float(4 字节):最大值约 $3.4 \times 10^{38}$(范围远大于 long)
引入指数位后,表达范围被几何级放大。
注意看:
byte 可以自动转 short。short 不能 直接转 char。byte 也 不能 直接转 char。但是它们都可以统一自动转为 int 再计算。原因是short和byte有符号。
提升原则:表达式结果的类型自动提升为操作数中最大的类型,最小是int类型
注意事项: byte short char 类型的变量进行计算都会转换成 int 类型
byte short char 类型的常量(字面常量)进行计算会进行优化机制
常量计算不溢出时,类型由左侧接收变量决定(直通不升格);一旦溢出,则被视为 int 类型(导致报错)。
只要公式中出现了一个变量 ,整个公式的“常量优化”特权就会立即消失,全面进入变量运算规则 。
强制类型转化 将容量大的数据类型转换为容量小的数据类型 。使用时要加上强制转换符 ( ),但可能造成精度降低或溢出 ,格外要注意。
Java基本数据类型中,数值类型和boolean不能互相强制转换
基本数据类型和String类型的转换 基本数据类型转String类型:将基本类型的值+“”
String类型转基本数据类型:利用包装类 xxx.parsexxx(“”)
包装类的作用:让基本数据类型能够像对象一样工作,具备属性和方法
基本数据类型
对应的包装类
byte
Byte
short
Short
int
Integer
long
Long
float
Float
double
Double
char
Character
boolean
Boolean
运算符 算术运算符 System.out.println(10 / 4 ); System.out.println(10.0 / 4 );
int total = 4 ;int score = 10 ;double result = score / total; System.out.println(result);
原因: score / total 的计算先发生,因为两个都是 int,所以先算出了 2。
赋值: 算出 2 之后,才把这个整数赋值给 double result。这时 2 变成了 2.0。
在 % 的本质 看一个公式a % b = a - a / b * b
由公式可以知道:在 Java 的取模运算中,结果的符号只取决于左操作数(被除数 $a$)的符号,而与右操作数(除数 $b$)无关。
面试题
int i = 1; i = i++; System.out.println(i);
答案是 1
过程 temp = i i=i+1 i=temp
int i = 1; i = ++i; System.out.println(i);
答案是 2
过程 i=i+1 temp=i i=temp
补充知识点 java和python在处理/的时候采用不同的处理方法
Java:向零取整 (Truncate) Java 的整数除法采用的是 截断除法 。简单来说,就是直接把小数点后面的部分“扔掉”。
计算 -7 / 3: 结果是 -2.333...,截断后变成 -2。
推导 %: $-7 - (3 \times -2) = -7 + 6 = -1$。
Python:向下取整 (Floor) Python 的整数除法(//)采用的是 地板除 。它向数轴的左侧(负无穷方向)取整。
计算 -7 // 3: 结果是 -2.333...,向左取整变成 -3。
推导 %: $-7 - (3 \times -3) = -7 + 9 = 2$。
关系运算符 关系运算符的结果都是boolean类型,也是就是要么true要么false
instanceof
逻辑运算符 1) a&b : & 叫逻辑与:规则:当 a 和 b 同时为 true ,则结果为 true, 否则为 false 2) a&&b : && 叫短路与:规则:当 a 和 b 同时为 true ,则结果为 true,否则为 false 3) a|b : | 叫逻辑或,规则:当 a 和 b ,有一个为 true ,则结果为 true,否则为 false 4) a||b : || 叫短路或,规则:当 a 和 b ,有一个为 true ,则结果为 true,否则为 false 5) !a : 叫取反,或者非运算。当 a 为 true, 则结果为 false, 当 a 为 false 是,结果为 true 6) a^b: 叫逻辑异或,当 a 和 b 不同时,则结果为 true, 否则为 false
对于&&短路与而言,如果第一个条件为 false ,后面的条件不再判断,效率高
对于&逻辑与而言,如果第一个条件为 false ,后面的条件仍然会判断,效率低
||短路或:如果第一个条件为 true,则第二个条件不会判断,最终结果为 true,效率高
| 逻辑或:不管第一个条件是否为 true,第二个条件都要判断,效率低
赋值运算符 赋值运算符特点
1) 运算顺序从右往左
2) 赋值运算符的左边 只能是变量,右边 可以是变量、表达式、常量值
3) 复合赋值运算符等价于下面的效果比如:a+=3;等价于 a=a+3; 4) 复合赋值运算符会进行类型转换。
三元运算符 条件表达式 ? 表达式 1: 表达式 2;
运算规则:
如果条件表达式为 true,运算后的结果是表达式 1;
如果条件表达式为 false,运算后的结果是表达式 2;
注意事项
1) 表达式 1 和表达式 2 要为可以赋给接收变量的类型(或可以自动转换) 2) 三元运算符可以转成 if—else 语句
命名规范
标识符类型
命名范式
示例
备注
包 (Package)
全小写
com.apple.ios
建议使用反向域名
类 (Class)
大驼峰
StudentInfo
名词
方法 (Method)
小驼峰
getScore()
动词
变量 (Variable)
小驼峰
studentAge
描述性名词
常量 (Constant)
大写 + 下划线
MAX_AGE
static final 修饰
抽象类
大驼峰
AbstractService
通常以 Abstract 开头
异常类
大驼峰
LoginException
必须以 Exception 结尾
补充知识点,原码反码补码 原码 (Sign-Magnitude) :最高位为符号位(0正1负),其余位是数值。
反码 (Ones’ Complement) :正数不变;负数符号位不变,数值位按位取反。
补码 (Two’s Complement) :正数不变;负数在反码基础上 +1 。
所有运算都在补码(Two’s Complement)基础上进行
程序控制结构
顺序控制
程序从上到下逐行执行,中间没有任何判断和跳转
分支控制
单分支 if
双分支 if-else
多分支 if-else if- else
switch分支
表达式数据类型应和case后的变量类型一致,或者可以自动转成可以比较的类型
switch(表达式)中的表达式的返回值必须是:byte short int char enum String
case子句中的值必须是常量,而不能是变量
default子句是可选的,当没有匹配的case时,执行default
break语句用来在执行完成一个case分支后使程序跳switch语句块,如果没有break,程序会顺序执行,直到遇到break
循环控制
for循环
for循环四个要素 循环变量初始化 循环条件 循环操作 循环变量迭代
注意 循环里面创建的变量都是临时的,用完就销毁
循环初始值可以有多条初始化语句,但要求类型一样,并且中间用逗号隔开,循环变量迭代也可以有多条变量迭代语句,中间用逗号隔开。
while循环
do while循环
先执行再判断,最少会执行一次
最后 有一个;
多重循环控制
break 终止循环
continue 跳出本次循环
跳转控制语句 - return
跳出所在的方法
数组、排序、查找 数组定义方式
动态分配 数据类型 数组名[] = new 数组类型[大小]
先声明再分配 数据类型 数组名[] 数组名 = new 数组类型[大小]
数组名.length 表示数组的大小/长度
数据类型 数组名[]; 也可以 数据类型[] 数组名;
注意事项
1) 数组是多个相同类型数据的组合,实现对这些数据的统一管理 2) 数组中的元素可以是任何数据类型,包括基本类型和引用类型,但是不能混用。 3) 数组创建后,如果没有赋值,有默认值
int 0,short 0, byte 0, long 0, float 0.0,double 0.0,char \u0000,boolean false,String null
4) 使用数组的步骤 1. 声明数组并开辟空间 2 给数组各个元素赋值 3 使用数组 5) 数组的下标是从 0 开始的 。 6) 数组下标必须在指定范围内使用,否则报:下标越界异常, 7) 数组属引用类型,数组型数据是对象(object)
int a = 1; int b = a; a和b是单独的空间,互不影响 基本数据类型 int[] a = [1,2]; int[] b = a; a和b是引用同一个地址,相互影响 引用数据类型
面向对象编程 组成
属性
对象名.属性名
行为
对象名.方法名
对象与类的关系
类是抽象的,概念的,代表一类事物
对象是具体的,实际的,代表一个具体事务
类是对象的模板,对象是类的一个个体
类和对象的内存分配机制 Java 内存的结构分析
栈:一般存放基本数据类型(局部变量)
堆:存放对象(Cat cat , 数组等)
方法区:常量池(常量,比如字符串),类加载信息
Person p = new Person ();p.name = "jack" ; p.age = 10 ;
先加载Person类信息(属性和方法信息,只会加载一次)
再堆中分配空间,进行默认初始化
把地址赋给p ,p就指向对象
进行指定初始化,比如 p.name=”jack” p.age = 10
方法的调用机制原理
当程序执行到方法时,就会开辟一个独立的空间(栈空间)
当方法执行完毕,或者执行到return语句时,就会返回
返回到调用方法的地方
返回后,继续执行方法后面的代码
当main方法(栈)执行完毕,这个程序退出
方法的访问修饰符
public :公开的。无论是哪个类、哪个包、还是外部项目,都可以直接访问。
protected :受保护的。同一个包内的类可以访问;不同包中的子类 也可以访问。
默认(无修饰符) :包级私有(package-private)。只有同一个包 内的类可以访问,跨包了就不能访问。
private :私有的。只有在定义它的类内部 才能访问,体现了面向对象的封装性。通常属性会设为 private,然后通过 get/set 方法来间接访问。
方法的返回数据类型
1) 一个方法最多有一个返回值 [多个结果 返回数组 ] 2) 返回类型可以为任意类型,包含基本类型或引用类型(数组,对象) 3) 如果方法要求有返回数据类型,则方法体中最后的执行语句必须为 return 值; 而且要求返回值类型必须和 return 的值类型一致或兼容
4) 如果方法是 void,则方法体中可以没有 return 语句,或者 只写 return ;
注意事项
方法不能嵌套定义,方法体中不能再定义方法
基本数据类型,传递的是值(值拷贝),形参的任何变化不影响实参
引用数据类型,传递的是地址(传递也是值,但是值是地址),可以通过形参影响实参
递归重要规则
执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
方法的局部变量时独立的,不会相互影响
如果方法中使用的是引用类型变量(比如数组,对象),就会共享引用类型的数据
递归必须向退出递归条件逼近,否则就是无限递归
当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
方法重载
方法名:必须相同
参数列表:必须不同
返回类型:无要求
可变参数
public int sum (int ... nums) { int res = 0 ; for (int i = 0 ; i < nums.length; i++) { res += nums[i]; } return res; }
int… 表示接受的是可变参数,类型是 int ,即可以接收多个 int(0-多)
使用可变参数时,可以当做数组来使用 即 nums 可以当做数组
遍历 nums 求和即可
注意事项
可变参数的实参可以为0个或者任意多个
可变参数的实参可以为数组
可变参数的本质就是数组
可变参数可以和普通类型的参数一起放在形参列表,但是必须保证可变参数在最后
一个形参列表中只能出现一个可变参数
作用域
Java编程中,主要的变量就是属性(成员变量)和局部变量
局部变量一般是指在成员方法中定义的变量
java中作用域的分类
全局变量:也就是属性,作用域为整个类
局部变量:除了属性之外的其他变量,作用域是定义他的代码块
全局变量可以不赋值,直接使用,因为有默认值,局部变量必须赋值,才能使用,因为没有默认值
注意事项
属性和局部变量可以重名,访问时遵循就近原则
在同一个作用域中,两个变量不能重名
属性生命周期较长,便随着对象的创建而创建,伴随着对象的销毁而销毁。局部变量,生命周期较短,伴随着它的代码块的执行而创建,伴随着代码块的结束而销毁
作用域范围不同
全局变量:可以被本类使用,或其他类使用
局部变量:只能在本类中对应的方法中使用
修饰符不同
全局变量可以加修饰符
局部变量不可用加修饰符
构造方法
构造方法又叫构造器(constructor),是类的一种特殊的方法,它的主要作用是完成对新对象的初始化。它有几个特点:
1) 方法名和类名相同 2) 没有返回值 3) 在创建对象时,系统会自动的调用该类的构造器完成对象的初始化。 4) 构造器没有返回值, 也不能写 void
注意事项
一个类可以定义多个不同的构造器,即构造器重载
构造器名要和类名相同
构造器没有返回值
构造器是完成对象的初始化,并不是创建对象
在创建对象时,系统自动的调用该类的构造方法
如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器(默认构造器)
一旦定义了自己的构造器,默认的构造器就被覆盖了,就不能再使用默认无参构成器了,除非显式的定义一下
this关键字
Java虚拟机会给每个对象分配this代表当前对象
注意事项
this 关键字可以用来访问本类的属性、方法、构造器
2) this 用于区分当前类的属性和局部变量 3) 访问成员方法的语法:this.方法名(参数列表);
4) 访问构造器语法:this(参数列表); 注意只能在构造器中使用(即只能在构造器中访问另外一个构造器, 必须放在第一条语句)
5) this 不能在类定义的外部使用,只能在类定义的方法中使用
补充知识点
==(双等号)
基本数据类型(如 int, double, boolean) :比较的是它们的数值 是否相等。
引用数据类型(如对象、数组、字符串) :比较的是它们的内存地址 是否相同(即是否指向堆内存中的同一个对象)。
equals() 方法
对象专属 :它是 Object 类的一个方法,所有类都继承了它。
默认行为 :如果没有重写(Override),它的底层逻辑和 == 一样,比较的还是内存地址。
常用重写 :像 String、Integer 等类都重写了 equals() 方法,用来比较它们的内容(值)是否相同。
IDEA 常用快捷键
1) 删除当前行, 默认是 ctrl + Y 自己配置 ctrl + d 2) 复制当前行, 自己配置 ctrl + alt + 向下光标 3) 补全代码 alt + / 4) 添加注释和取消注释 ctrl + / 【第一次是添加注释,第二次是取消注释】 5) 导入该行需要的类 先配置 auto import , 然后使用 alt+enter 即可 6) 快速格式化代码 ctrl + alt + L 7) 快速运行程序 自己定义 alt + R 8) 生成构造器等 alt + insert 9) 查看一个类的层级关系 ctrl + H 10) 将光标放在一个方法上,输入 ctrl + B , 可以定位到方法 11) 自动的分配变量名 , 通过 在后面假 .var
包
作用
区分相同名字的类
方便管理类
控制访问范围
基本语法
package cn.itrf
package 关键词,表示打包
cn.itrf 表示包名
注意事项
package的作用是声明当前类所在的包,需要放在类的最上面,一个类中最多只有一句package
import指令 位置放在package的下面,在类定义前面,可以有多句且没有顺序要求
访问修饰符
public :公开的。无论是哪个类、哪个包、还是外部项目,都可以直接访问。
protected :受保护的。同一个包内的类可以访问;不同包中的子类 也可以访问。
默认(无修饰符) :包级私有(package-private)。只有同一个包 内的类可以访问,跨包了就不能访问。
private :私有的。只有在定义它的类内部 才能访问,体现了面向对象的封装性。通常属性会设为 private,然后通过 get/set 方法来间接访问。
面向对象的三大特征
封装、继承、多态
封装
封装就是把抽象出的数据和对数据的操作封装在一起,数据被保护在内部,程序的其他部分只能通过授权的操作,才能对数据进行操作
将属性设置为private 创建setXXX和getXXX的方法 public
继承
子类就是会自动拥有父类定义的属性和方法
父类又叫 超类,基类
子类又叫派生类
注意事项
1) 子类继承了所有的属性和方法,非私有的属性和方法可以在子类直接访问, 但是私有属性和方法不能在子类直接访问,要通过父类提供公共的方法去访问
2) 子类必须调用父类的构造器, 完成父类的初始化 3) 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用 super 去指定使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过。
4) 如果希望指定去调用父类的某个构造器,则显式的调用一下 : super(参数列表) 5) super 在使用时,必须放在构造器第一行(super 只能在构造器中使用) 6) super() 和 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器 7) java 所有类都是 Object 类的子类, Object 是所有类的基类. 8) 父类构造器的调用不限于直接父类!将一直往上追溯直到 Object 类(顶级父类) 9) 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制。思考:如何让 A 类继承 B 类和 C 类? 【A 继承 B, B 继承 C】
10) 不能滥用继承,子类和父类之间必须满足 is-a 的逻辑关系
super关键字
super代表父类的引用,用于访问父类的属性、方法、构造器
不能访问父类的private属性和方法
当子类中有何父类中的成员(属性和方法)重名时,为了访问父类成员,必须通过super。如果没有重名,使用super、this、直接访问的结果一样
super.成员 的查找起点是直接父类,若直接父类没有对应成员,会继续沿着继承链向上查找(可找到爷爷类等上层父类);查找遵循继承链就近原则 ,同时受访问权限约束遇到private就断了报错;无法主动跳过直接父类,强制直接访问爷爷类中被父类重写 / 覆盖的同名成员 。
this和super的区别
对比项
this
super
含义
当前类对象
父类存储空间入口
成员查找起点
当前类
直接父类(跳过当前类)
调用构造
this () 调用本类构造
super () 调用父类构造
使用位置
实例方法、构造器
实例方法、构造器
static 中
不允许
不允许
单独输出
this 可行
super 不能直接打印
方法的重写/覆盖
方法的覆盖就是子类的一个方法和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖了父类的方法
子类重写方法的返回值类型和父类返回值类型一致,或者是其子类
子类的方法不能缩小父类方法的访问范围
多态
方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的。
重点
一个对象的编译类型和运行类型可以不一致
编译类型在定义对象时,就确定了,不能改变
运行类型是可以变化的
编译类型看定义时 = 号 的左边,运行类型看 = 号的 右边
多态前提(四个条件缺一不可)
继承 / 实现 :子类继承父类(类继承)或者类实现接口
方法重写 :子类重写父类方法
父类引用指向子类对象 (向上转型)
调用重写方法
向上转型(自动转型,隐式转型)
格式:父类类型 引用 = new 子类();
Animal animal = new Dog(); // 向上转型
特点:
自动转换,不需要强制
可以调用:父类所有方法 + 子类重写的方法
不能调用子类独有的方法
向下转型(强制转型,显式转型)
作用:向上转型后,想要调用子类独有方法 ,必须向下转回子类类型
格式:子类类型 引用 = (子类)父类引用;
Animal a = new Dog(); //向上转型 Dog dog = (Dog) a; //向下转型 dog.watchDoor(); // 正常执行
巨大坑:类型转换异常 ClassCastException
只有原本就是该子类对象,才能强转;
Animal a = new Cat(); Dog d = (Dog)a; //运行报错!
解决方案:instanceof 判断
判断引用变量指向的实际对象 是不是【某个类 / 它的子类 / 实现类】的实例
转型前先判断引用实际对象类型
if(a instanceof Dog){ Dog d = (Dog)a; d.watchDoor(); }
类型
写法
是否强制
风险
能力
向上转型
父类 引用 = new 子类 ()
自动
无风险
只能访问父类方法,执行子类重写方法;不能访问子类独有方法
向下转型
子类 引用 = (子类) 父类引用
需要强制
可能抛 ClassCastException,配合 instanceof
可以调用子类独有方法
高频易错总结
方法有多态,成员变量没有多态!
class A{ int num = 10; } class B extends A{ int num = 20; } A a = new B(); System.out.println(a.num); //输出10,看左边父类
静态方法重写不叫重写,叫隐藏 ,不存在多态;调用看左边
向上转型本质:多态的基础写法
向下转型不是多态,只是为了访问子类特有功能
Java的动态类型绑定
调用对象【方法】:存在动态绑定
调用方法时,看对象真实运行类型(堆里 new 出来的类型),执行子类重写后的方法。
通俗:方法 → 运行看右边
调用对象【成员属性 (变量)】:没有动态绑定
访问变量时,看引用声明类型(等号左边是什么类型),哪里声明,就取哪里的属性。
通俗:属性 → 编译 / 运行都看左边
类变量
类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。
class Child { private String name; public static int count = 0 ; public Child (String name) { this .name = name; } }
访问类变量
类名.类变量名 对象名.类变量名 推荐用第一种
生命周期
类变量是在类加载时就完成初始化了,也就是说,即使你没有创建对象,只要类加载了,就可以使用类变量
类变量的生命周期是随类的加载开始,随着类消亡而销毁
类方法
类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区:
类方法中无 this 的参数
普通方法中隐含着 this 的参数
类方法可以通过类名调用,也可以通过对象名调用。
普通方法和对象有关,需要通过对象名调用,比如对象名。方法名 (参数),不能通过类名调用。
类方法中不允许使用和对象有关的关键字,比如 this 和 super。普通方法 (成员方法) 可以。
类方法 (静态方法) 中 只能访问 静态变量 或静态方法 。
普通成员方法,既可以访问 非静态成员,也可以访问静态成员。
小结: 静态方法,只能访问静态的成员,非静态的方法,可以访问静态成员和非静态成员 (必须遵守访问权限)
mian方法
public static void main(String[] args)
public :公共访问权限。JVM 需要跨类调用 main 方法,必须对外公开可访问。
static :静态方法。JVM 启动时还没有创建类的实例对象,直接通过类名调用方法 ,不需要 new 对象。
void :无返回值。JVM 不接收 main 函数的返回结果。
main :固定方法名,JVM 识别的程序入口名称,不能修改。
String[] args :命令行参数数组
三种代码块定义
静态代码块
位置:类内部,方法外面 ,加 static
触发时机:类加载的时候执行,只执行 1 次
作用:初始化静态变量
限制:
✅只能访问静态成员
❌不能使用 this、super
❌不可以写 return
构造代码块(实例代码块 / 普通代码块【类内】)
位置:类内部、方法外面,没有 static
触发时机:每次创建对象 new 的时候执行
执行时机:在构造方法之前执行
作用:提取多个构造方法的公共重复代码
✅可以访问静态、非静态成员,可以使用 this
局部代码块(方法内普通代码块)
public void test(){ { //局部代码块 } }
位置:方法内部
作用:限定局部变量生命周期,缩小作用域
考试常考:区分「构造代码块」和「局部代码块」
执行顺序
情况 1:只创建单个对象
1. 静态代码块(类加载,最先执行,只运行一次) ↓ 2. 构造代码块(每次new对象都执行) ↓ 3. 构造方法
示例演示:
public class Test { static { System.out.println("静态代码块" ); } { System.out.println("构造代码块" ); } public Test () { System.out.println("构造方法" ); } public static void main (String[] args) { new Test (); } }
情况 2:多次 new 对象
静态代码块 //类加载,仅第1次执行 构造代码块 构造方法 构造代码块 //第二次new,静态不再执行! 构造方法
💡关键结论:静态代码块随类加载,全局只执行一次;构造代码块每次 new 对象都会执行。
情况 3:存在父子继承关系(高频面试!必考)
继承场景完整顺序口诀:
父静态 → 子静态 → 父构造代码块 → 父构造方法 → 子构造代码块 → 子构造方法
演示代码:
class Father { static {System.out.println("父静态" );} {System.out.println("父构造代码块" );} public Father () {System.out.println("父构造器" );} } class Son extends Father { static {System.out.println("子静态" );} {System.out.println("子构造代码块" );} public Son () {System.out.println("子构造器" );} } public class Main { public static void main (String[] args) { new Son (); } }
运行结果:
父静态 子静态 父构造代码块 父构造器 子构造代码块 子构造器
⚠️易错提醒:
静态代码块只看类加载顺序 ,优先父类静态;静态代码块互不依赖实例,优先全部执行完所有静态,再执行实例相关代码块、构造器。
类型
格式
位置
执行时机
执行次数
静态代码块
static {}
类内,方法外
类加载阶段
整个程序只 1 次
构造代码块
{}
类内,方法外
每次 new 创建实例
每 new 一次执行一次
局部代码块
{}
方法内部
方法运行到该位置
运行到就执行
设计模式
静态方法和属性的经典使用
设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。
单例模式:保证一个类在整个程序中,只有 唯一的一个实例对象,对外提供统一获取该对象的入口。
使用场景:工具类、线程池、数据库连接池、配置文件对象等,不需要多个实例,节约内存。
核心三大要点:
私有构造方法 :private 类名(){},禁止外部new创建对象
私有静态成员变量 :保存唯一实例
公共静态方法 :对外获取这个唯一实例
方式 1:饿汉式(静态常量写法,【线程安全】)
public class SingletonHungry { private static final SingletonHungry instance = new SingletonHungry (); private SingletonHungry () {} public static SingletonHungry getInstance () { return instance; } }
执行时机
类加载的时候直接创建对象 。
✅优点:
写法简单,天然线程安全(类加载只执行一次,不存在多线程冲突)
❌缺点:
类一旦加载,立刻创建对象 。即使程序全程没有用到这个实例,对象依然创建,造成内存浪费。
饿汉:肚子一饿立刻吃饭 → 类加载直接实例化。
方式 2:懒汉式(基础版,⚠️多线程不安全!)
public class SingletonLazy { // 先不创建实例 private static SingletonLazy instance; private SingletonLazy(){} // 调用方法时,才创建对象 public static SingletonLazy getInstance(){ // 第一次调用才new if(instance == null){ instance = new SingletonLazy(); } return instance; } }
执行时机 调用getInstance()方法的时候,才创建实例 (延迟加载)
✅优点:懒加载 ,不用就不创建,节省内存
❌致命缺陷:多线程环境下会创建多个对象,线程不安全
👉线程安全优化:双重检查锁 DCL(面试标准懒汉写法)
public class SingletonLazyDCL { private static volatile SingletonLazyDCL instance; private SingletonLazyDCL () {} public static SingletonLazyDCL getInstance () { if (instance == null ){ synchronized (SingletonLazyDCL.class){ if (instance == null ){ instance = new SingletonLazyDCL (); } } } return instance; } }
懒汉:不到饭点不吃饭 → 需要用到的时候才创建对象
对比
饿汉式
懒汉式 (DCL 双重检查锁)
创建时机
类加载阶段
第一次调用 getInstance ()
懒加载
❌不支持
✅支持,节省内存
线程安全
✅天然安全
需要加锁 + volatile 保证安全
使用场景
确定一定会使用这个对象
不确定是否会使用,希望延迟创建
⚠️拓展面试考点
私有构造方法依然可以被反射暴力破解 ,两种基础实现都不能防止反射创建多个实例;
还有第三种写法:静态内部类单例(推荐,兼顾懒加载与线程安全);
public class Singleton { private Singleton(){} private static class Inner{ private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance(){ return Inner.INSTANCE; } }
Java final 关键字完整讲解
final 表示「最终的、不可修改」,可以修饰:变量、方法、类。
一、final 修饰【变量】
变量一旦赋值成功,不能重新赋值 (常量)
局部变量
public void test(){ final int a; a = 10; // a = 20; // ❌编译报错,不能二次赋值 }
final 局部变量可以先声明、后面一次赋值,但是只能赋值一次 。
成员变量(实例变量)
两种赋值时机,二选一:
定义时直接初始化
在构造方法中赋值
❗不能不赋值!final 成员变量必须保证对象创建完成时有值。
class A{ final int num = 10; // final int num; // 合法,可以在构造器赋值 public A(){ // num = 20; } }
静态成员变量(static final)
俗称常量 ,命名规范:全部大写,下划线分隔 MAX_VALUE
赋值时机:
声明处初始化
静态代码块 初始化
public static final double PI = 3.1415926;
⚠️极易混淆重点:final 修饰引用类型变量
final Person p = new Person(); p.name = "张三"; // ✅合法!! p = new Person(); // ❌报错!
final 限制的是引用地址不能变
对象内部的属性依然可以修改!
形象理解:遥控器不能换电视,但是可以换频道。
二、final 修饰【方法】
public final void show(){}
规则:
✅ 该方法不能被子类重写(override)
仍然可以被继承,子类可以调用,只是不能改写方法实现。
💡补充:
private 方法隐式带有 final 特性 ,子类无法重写私有方法。
三、final 修饰【类】
规则:
✅ 这个类不能被继承,没有子类
例子:String、Integer 都是 final 类,你无法写 class MyStr extends String
四、高频易错题汇总
final 和 static 区别?
static:属于类,所有对象共享一份
final:值不可修改;两者经常组合 static final 定义常量
基本类型 final vs 引用类型 final
基本类型:数值不能变
引用类型:对象地址不能变,对象内部内容可变
final 方法可以重载吗?
✅可以重载!只是禁止重写,重载不受影响。
class A{ public final void test(){} public final void test(int a){} // ✅重载合法 }
final 可以修饰构造方法吗?
❌不允许!构造方法本身不能被继承重写,语法不支持。
五、速记口诀
final 变量:赋值一次,终身不改(引用变量地址不变,对象内容可变)
final 方法:禁止子类重写
final 类:断绝继承,不能有子类
修饰目标
作用
final 变量
只能赋值一次,不可重新赋值
final 方法
不能被子类重写
final 类
不能被继承
Java 抽象类(abstract class)
一、基础概念
用 abstract 关键字修饰的类,叫做抽象类 。
抽象类用来提取多个子类的公共模板,允许定义没有方法体的抽象方法 ,强制子类去实现。
语法格式:
二、抽象方法
语法:abstract 返回值 方法名();
只有方法声明,没有方法体(没有 {})
必须写在抽象类 / 接口 当中
作用:规范子类必须实现这个方法
示例:
abstract class Animal { // 抽象方法:没有方法体 public abstract void shout(); }
三、核心硬性规则(必背考点)
抽象类不能 new 创建对象
原因:类中有未实现的抽象方法,对象不完整。
如果一个类继承抽象类:
方案①:普通子类 → 必须重写父类所有抽象方法
方案②:子类也定义为 abstract 抽象类 → 可以不用实现抽象方法
// 普通子类,必须实现全部抽象方法 class Dog extends Animal{ @Override public void shout() { System.out.println("汪汪汪"); } }
抽象类里可以包含什么?
✅ 普通成员变量、静态变量
✅ 普通实例方法、static 静态方法
✅ 构造方法(供子类创建对象时调用)
✅ 抽象方法
💡重点:抽象类有构造方法 !只是不能自己实例化,子类 new 的时候通过 super() 调用。
几个语法禁忌
❌ abstract 和 final不能一起修饰类
final 类不能被继承,abstract 类要求必须被继承实现,互相矛盾。
❌ abstract 和 private 不能一起修饰方法
private 方法子类看不见,无法重写实现,冲突。
❌ abstract 和 static 不能一起修饰方法
静态方法可以直接类名调用,不需要子类实现,冲突。
类型
能否实例化
能否拥有抽象方法
普通类
✅可以 new
❌不能有抽象方法
抽象类
❌不能直接 new
✅可以定义抽象方法
三、高频易错题
抽象类一定包含抽象方法?
❌错误!抽象类可以没有任何抽象方法,只是很少这么写。
abstract class Test{ // 全部都是普通方法,合法抽象类 public void hello(){} }
有抽象方法的类,一定是抽象类?
✅正确!普通类存在抽象方法直接编译报错。
Java 接口(interface)
接口是行为规范、标准契约,定义一组功能(方法声明),本身不关心怎么实现。
语法:
类实现 接口,关键字:implements
class 类名 implements 接口1,接口2 { }
二、不同 JDK 版本接口内允许的成员(考试重点)
1)JDK7 及以前(经典老式接口)
接口里面只能有:全局常量、抽象方法
常量:public static final 修饰,默认隐藏
interface Usb{ // 默认:public static final int MAX_SPEED = 480; int MAX_SPEED = 480; }
必须初始化,不能修改。
抽象方法:public abstract 修饰,默认隐藏
interface Usb{ // public abstract void work(); void work(); }
2)JDK 8 新增
✅ 默认方法(default) :带方法体,实现类可以选择重写,不重写就用默认实现
default void show(){ System.out.println("默认方法"); }
✅ 静态方法(static) :接口。静态方法 () 调用,实现类不能继承
3)JDK 9 新增
✅ 私有方法 private :供接口内部默认方法复用
💡面试常记:日常开发核心掌握【常量、抽象方法、default 默认方法、static 静态方法】
三、核心语法规则(必背)
接口不能 new 实例化 ,没有构造方法!
类和接口:implements 多实现 (一个类可以同时实现多个接口)
class Phone implements Usb, Bluetooth{}
接口与接口:extends 多继承
interface A{} interface B{} interface C extends A,B{}
实现类要求:
普通类实现接口:必须重写接口中所有抽象方法
抽象类实现接口:可以不实现抽象方法,交给它的子类完成
方法访问权限:
接口抽象方法强制 public,重写的时候不能缩小权限(不能写 protected/private)
对比项
抽象类 abstract class
接口 interface
关键字
class
interface
构造方法
✅有构造方法(供子类 super 调用)
❌无构造方法,不能实例化
成员变量
可以普通变量、静态变量
只能public static final常量
方法 (JDK7)
普通方法 + 抽象方法
只能抽象方法、常量
继承关系
单继承 extends
多实现implements;接口之间多继承extends
设计角度
模板,描述事物是什么
行为标准,描述事物能做什么
设计思想区分:
Animal(抽象类):动物是什么
Run(接口):动物能跑
Dog extends Animal implements Run
四、高频易错考点
default 默认方法冲突问题
一个类实现多个接口,如果多个接口存在同名同参数 default 方法 ,实现类必须手动重写该方法 ,否则编译报错。
接口静态方法不会被继承,普通类的可以被继承但不能重写
interface A{ static void test(){} } class B implements A{} B.test(); // ❌错误,只能 A.test()
接口常量默认public static final,写不写都一样,不能修改。
Java 内部类
一、什么是内部类
把一个类定义在另一个类的内部,这个类就叫内部类;外面的类称为外部类(外围类)。
class Outer { class Inner { } }
分类(四大类):
成员内部类(实例内部类)
静态内部类(静态嵌套类)
局部内部类(方法内定义)
匿名内部类(重点,经常和接口 / 抽象类搭配)
成员内部类(没有 static,写在外部类成员位置)
class Outer { private String name = "外部类成员" ; class Inner { public void show () { System.out.println(name); } } }
核心规则
成员内部类依赖外部类对象,不能直接 new
创建语法:
Outer outer = new Outer ();Outer.Inner inner = outer.new Inner (); inner.show();
简写:Outer.Inner inner = new Outer().new Inner();
内外类出现同名变量怎么区分?
class Outer { String a = "外部a" ; class Inner { String a = "内部a" ; public void test () { System.out.println(a); System.out.println(this .a); System.out.println(Outer.this .a); } } }
限制
❌内部类里面不能定义静态成员(static 变量、静态方法)
因为成员内部类必须依托外部实例,静态属于类,冲突
访问权限
成员内部类可以使用 private / protected / public修饰;
如果内部类被 private 修饰,外部其他类无法访问这个内部类。
静态内部类(static 修饰,也叫嵌套类)
class Outer { private static String msg = "静态外部变量" ; static class StaticInner { public void show () { System.out.println(msg); } } }
核心规则
不依赖外部类对象,直接通过外部类类名创建
Outer.StaticInner inner = new Outer .StaticInner();
❌ 不能写 outer.new StaticInner ()
访问范围
只能直接访问外部类 static 静态属性、静态方法 ;
想要访问外部实例成员,必须传入外部类对象。
可以拥有自己的静态成员。
成员内部类 vs 静态内部类 关键区别
类型
创建方式
依赖外部对象
可访问外部成员
成员内部类
外部对象.new 内部类()
必须有外部实例
所有成员(静态 + 实例 + 私有)
静态内部类
new 外部类.内部类()
不需要外部实例
仅外部静态成员
局部内部类(定义在【方法里面】)
class Outer { public void method () { class LocalInner { public void show () { System.out.println("局部内部类方法" ); } } LocalInner li = new LocalInner (); li.show(); } }
规则
作用域仅限当前方法 ,方法外面无法使用;
不能使用访问修饰符 public private protected;
JDK8 开始,如果访问方法里的局部变量,变量默认隐式 final(不能修改);
public void test () { int num = 10 ; class Inner { void show () { System.out.println(num); } } }
匿名内部类(重中之重!面试、开发高频)
没有类名的内部类,一般用来一次性实现接口 / 抽象类,简化代码。
语法格式:
示例:基于接口
interface Usb { void work () ; } public class Test { public static void main (String[] args) { Usb usb = new Usb (){ @Override public void work () { System.out.println("usb设备工作" ); } }; usb.work(); } }
核心特点
匿名内部类只能使用一次 ,只能创建一个对象;下次创建对象的时候必须重新写一边,没有名字不能重复使用;
没有构造方法;
本质:隐式继承父类 / 实现接口;
属于局部内部类的一种;
💡常用场景:方法传参、线程 new Thread(new Runnable(){})
拓展:JDK8 Lambda 表达式可以简化只有一个抽象方法的接口,替代匿名内部类。
⚠️通用核心考点:内部类访问外部类
内部类天生持有外部类的隐式引用:外部类名.this
外部类想要访问内部类成员:必须先创建内部类对象 ,不能直接访问
class Outer { class Inner { int num = 100 ; } public void test () { Inner in = new Inner (); System.out.println(in.num); } }
类别
定义位置
是否 static
创建对象方式
核心特点
成员内部类
外部类中,方法外
无
new Outer().new Inner()
依赖外部对象,可访问所有外部成员,不能定义 static
静态内部类
外部类中,方法外
static
new Outer.StaticInner()
无需外部对象,仅访问外部静态成员
局部内部类
方法体内
无
方法内 new
仅限方法内使用,不能加权限修饰符
匿名内部类
方法 / 参数中
无
new 父类/接口(){}
无类名,一次性使用,实现抽象方法
枚举和注解 枚举是一种特殊类,用来固定、列出有限、确定的常量。
适合场景:季节、星期、订单状态、性别、方向。
解决普通常量public static final的缺陷(无法限制传入非法值)。
语法定义:
enum Season { SPRING, SUMMER, AUTUMN, WINTER; }
等价理解:每个常量都是 Season 的静态常量实例对象 。
枚举的实现方式
自定义类实现枚举
1) 构造器私有化 2) 本类内部创建一组对象 3) 对外暴露对象(通过为对象添加 public final static 修饰符) 4) 可以提供 get 方法,但是不要提供 set
使用enum关键字实现枚举
1) 当我们使用 enum 关键字开发一个枚举类时,默认会继承 Enum 类, 而且是一个 final 类
2) 传统的 public static final Season2 SPRING = new Season2(“春天”, “温暖”); 简化成 SPRING(“春天”, “温暖”), 这里必须知道,它调用的是哪个构造器.
3) 如果使用无参构造器 创建 枚举对象,则实参列表和小括号都可以省略 4) 当有多个枚举对象时,使用,间隔,最后有一个分号结尾 5) 枚举对象必须放在枚举类的行首
enum 定义的类,编译器自动处理:
隐式继承 java.lang.Enum,不能再继承其他类
所有枚举实例:public static final
构造方法强制私有 private ,就算不写也是 private,禁止外部new
❌ 误区:不能手动写 extends;构造方法不能用 public /protected
示例
enum Season { SPRING("春天" , "温暖" ), SUMMER("夏天" , "炎热" ), AUTUMN("秋天" , "凉爽" ), WINTER("冬天" , "寒冷" ); private String name; private String desc; Season(String name, String desc) { this .name = name; this .desc = desc; } public String getName () { return name; } public String getDesc () { return desc; } }
Enum 自带核心方法(必考)
values()
返回枚举所有常量的数组 ,用于遍历
Season[] arr = Season.values(); for (Season s : arr){ System.out.println(s); }
valueOf(String name)
根据名字字符串获取枚举常量;名字不存在抛出异常IllegalArgumentException
Season s = Season.valueOf("SPRING" );
ordinal()
返回常量序号,从 0 开始 (定义顺序)
System.out.println(Season.SPRING.ordinal());
name() / toString()
name():返回常量定义名称(SPRING),final 无法重写
toString():默认和 name () 一样,可以重写自定义输出
System.out.println(Season.SPRING.name());
枚举可以拥有的特性
✅ 构造方法(只能 private)
✅ 成员变量、普通方法、静态方法
✅ 抽象方法(每个枚举常量必须实现!)
✅ 可以实现接口
❌ 不能继承其他类(隐式继承 Enum,Java 单继承)
❌ 外部无法 new 枚举对象
每个枚举实例单独实现方法:
enum Season { SPRING{ @Override public void show () { System.out.println("春暖花开" ); } }, SUMMER{ @Override public void show () { System.out.println("夏日炎炎" ); } }; public abstract void show () ; }
高频易错总结
枚举构造器只能 private,省略不写也是 private
枚举常量必须放在类最开头,分号可选(后面有代码必须加分号)
枚举不能被继承;不能继承别的类,可以实现接口
valueOf() 参数名称错误会抛运行异常
ordinal () 顺序依赖代码定义顺序,业务不要重度依赖!
枚举常量是单例,全局只有一个对象
注解
1) 注解(Annotation)也被称为元数据(Metadata),用于修饰解释 包、类、方法、属性、构造器、局部变量等数据信息。 2) 和注释一样,注解不影响程序逻辑,但注解可以被编译或运行,相当于嵌入在代码中的补充信息。 3) 在 JavaSE 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 JavaEE 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替 java EE 旧版中所遗留的繁冗代码和 XML 配置等。
注解
作用
@Override
标记方法是重写父类 / 接口方法;编译器校验,如果不是重写直接编译报错
@Deprecated
标记【已过时】元素,使用时编译器弹出警告
@SuppressWarnings("all")
压制编译警告;all 代表抑制全部警告,也可以指定 “unchecked” 等
@SafeVarargs
JDK7,抑制可变泛型参数警告(了解)
@FunctionalInterface
JDK8,标记函数式接口:接口只能有一个抽象方法 ,供 Lambda 使用
元注解(用来修饰【自定义注解】的注解)
元注解:注解的注解 ,专门定义自定义注解的行为,一共 5 个,重点 4 个
@Target
可选值(ElementType 枚举):
TYPE:类、接口、枚举
METHOD:方法
FIELD:成员变量
PARAMETER:方法参数
CONSTRUCTOR:构造方法
LOCAL_VARIABLE:局部变量
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention
指定注解,生命周期(保留到哪个阶段)
SOURCE → 源码阶段,编译后丢弃(注释级别,仅编译器看见,class不存在) CLASS → 编译保留在class,运行时JVM丢弃【默认策略】 RUNTIME → 运行时仍然保留,可以通过反射读取(自定义框架注解几乎都用这个!)
想在运行时通过反射获取注解信息,必须写:@Retention(RetentionPolicy.RUNTIME)
@Documented
使用 javadoc 生成 API 文档时,把注解信息纳入文档
@Inherited
可继承 :父类标记该注解,子类自动继承这个注解
⚠️注意:只对【类】有效;方法、属性无效
@Repeatable(JDK8):可重复注解(拓展了解)
自定义注解(自己创建注解)
语法:@interface
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value () default "" ; int age () default 18 ; String[] tags() ; }
使用自定义注解
@MyAnnotation("测试") public void func () {} @MyAnnotation(value="demo",age=20,tags={"a","b"})
关键语法规则
属性支持类型:基本类型、String、枚举、注解、以上类型数组
如果属性没有设置default默认值,使用注解时必须显式赋值
特殊约定:属性名叫做value,并且只有这一个属性,赋值时可以省略value=
注解如何生效?
注解本身只是标记!必须配合反射机制解析
示例:通过反射读取运行时注解
Method method = Test.class.getMethod("func" );if (method.isAnnotationPresent(MyAnnotation.class)){ MyAnnotation anno = method.getAnnotation(MyAnnotation.class); System.out.println(anno.value()); }
Spring、Mybatis 框架底层大量使用:自定义注解 + 反射实现功能。
异常-Exception 一、什么是异常
程序运行过程中出现的不正常情况(错误) ,会打断正常指令流。
Java 使用面向对象方式 管理异常:异常本身就是一个对象。
区分:
编译期异常 :写代码编译器就报错
运行时异常 :编译正常,运行程序崩溃
二、异常体系结构图(核心)
java.lang.Throwable ├─ Error 【错误】系统级严重问题,程序无力处理,不用捕获 │ StackOverflowError、OutOfMemoryError └─ Exception 【异常】程序层面,可以捕获处理 ├─ RuntimeException 运行时异常(非受检异常) │ NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException └─ 其他Exception:编译期异常(受检异常 CheckedException) IOException、FileNotFoundException、SQLException
概念区分
Error(错误)
虚拟机内部故障、资源耗尽。程序代码无法修复,不需要 try-catch 捕获。
Exception(异常)
程序逻辑问题,可以处理。分为两类:
✅ 运行时异常(RuntimeException)【非受检异常】
编译不强制要求捕获 / 声明;一般由代码书写 bug 导致。
✅ 编译期异常(受检异常 CheckedException)
编译器强制要求:要么try-catch捕获,要么throws抛出,否则编译报错。
三、异常处理两大方案
方案 1:try-catch-finally 捕获处理
语法模板
try { }catch (异常类型 e){ }finally { }
细节考点
try不能单独存在,必须搭配 catch 或 finally
多个 catch:小范围异常写前面,大范围异常写后面 (子类在前,父类在后)
try {}catch (NullPointerException e){ }catch (Exception e){ }
finally 典型用途:释放资源 (文件流、数据库连接、IO 流)
try-catch-resources(JDK7 新特性)
自动实现资源关闭,不用手写 finally 关闭流
try (InputStream is = new FileInputStream ("a.txt" )){}catch (IOException e){ }
方案 2:throws 向上抛出异常
如果当前方法不想处理异常,可以把异常抛给调用者
public void readFile () throws IOException{}
⚠️区别:
try-catch:自己处理异常,异常到此结束
throws:不处理,甩锅给上层调用方法
四、主动抛出异常 throw
手动创建异常对象并抛出
int age = -5 ;if (age < 0 ){ throw new RuntimeException ("年龄不能为负数" ); }
throw(小写):方法内部主动抛出对象
throws(复数):方法声明上,标识该方法可能抛出的异常
关键字
位置
作用
throw
方法内部
主动抛出一个异常实例
throws
方法签名后
声明该方法可能产生哪些异常,向上传递
五、自定义异常
业务场景内置异常不够用时,自己定义异常类
自定义受检异常 :继承 Exception
自定义运行时异常 :继承 RuntimeException(项目最常用)
示例:
public class AgeIllegalException extends RuntimeException { public AgeIllegalException (String msg) { super (msg); } }
使用:
if (age < 0 ){ throw new AgeIllegalException ("年龄非法" ); }
六、高频重点考题
1.finally 一定会执行吗?
✅绝大多数情况一定会执行
❌唯一打断情况:System.exit(0); 终止 JVM,finally 不再执行
2.try 里有 return,finally 执行时机
先执行 finally 代码,再执行 return
public static int test () { try { return 10 ; }finally { System.out.println("finally执行" ); } }
⚠️注意:不要在 finally 中写 return!会覆盖 try 的返回值,屏蔽异常,属于不规范代码。
受检异常 vs 运行时异常总结
运行时异常 (RuntimeException):编译不强制处理;属于代码 bug,尽量提前判断避免
受检异常:编译器强制处理;属于外部不可控情况(文件不存在、网络故障)
常用异常举例
运行时异常:
NullPointerException空指针
当应用程序试图在需要对象的地方使用 null 时
ArrayIndexOutOfBoundsException数组越界
用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引
ClassCastException类型转换异常
当试图将对象强制转换为不是实例的子类时,抛出该异常。
ArithmeticException数学运算异常
当出现异常的运算条件时,抛出此异常。
NumberFormatException数字格式不正确异常
编译异常:
IOException、FileNotFoundException、SQLException
异常链
捕获异常后抛出新异常时,可以把原始异常作为 cause 保存,保留完整堆栈信息
throw new 业务异常("操作失败" ,e);
异常链(Exception Chaining) 是一种将一个异常传递给另一个异常 的技术。它允许我们在捕获一个异常后,抛出另一个新的异常,同时把原始异常(底层原因)完整地保留并记录在新的异常内部 。
为什么需要异常链?(痛点)
在复杂的系统中,异常通常具有层次性。假设这样一个场景:
底层(数据库层) :执行 SQL 时抛出了 SQLException。
中层(服务层) :捕捉到 SQLException,如果直接把它往外抛,会显得太底层、太专业(业务调用方不关心你是不是 SQL 错误)。或者如果你把它吞掉,换成抛出一个自定义的 ServiceException:
catch (SQLException e) { throw new ServiceException ("用户注册失败" ); }
问题来了 :虽然外层成功抛出了符合业务的 ServiceException,但原本的 SQLException 被丢弃了 。如果数据库连接串写错或者表名写错,由于丢失了底层异常的堆栈信息(Stack Trace),排查错误会变得异常困难。
异常链完美解决了这个问题 :它在抛出新异常的同时,把旧异常“栓”在新异常身上,形成一条链条。
Java 是如何支持异常链的?
在 Java 中,所有的异常最终都继承自 Throwable 类。Throwable 及其子类在设计时就内置了对异常链的支持:
构造方法支持传入 cause(原因) : 几乎所有标准异常类和自定义异常类,都提供了一个接收 Throwable 参数的构造方法:
public MyException (String message, Throwable cause) { super (message, cause); }
initCause() 方法 : 如果使用的异常类没有提供带 cause 参数的构造方法,可以通过调用对象的 initCause(Throwable cause) 方法来手动绑定。
代码示例
步骤 1:定义一个自定义业务异常
public class UserServiceException extends RuntimeException { public UserServiceException (String message, Throwable cause) { super (message, cause); } }
步骤 2:在业务代码中捕获并链式抛出
public class UserService { public void registerUser () { try { int i = 1 / 0 ; } catch (Exception e) { throw new UserServiceException ("用户注册模块发生内部错误" , e); } } }
步骤 3:查看运行效果
当外层程序捕获到 UserServiceException 并打印堆栈信息(e.printStackTrace())时,你会看到类似下面这样的输出:
com.example.UserServiceException: 用户注册模块发生内部错误 at com.example.UserService.registerUser(UserService.java:15) ... (省略中间调用栈) Caused by: java.lang.ArithmeticException: / by zero <--- 底层原因清晰可见 at com.example.UserService.registerUser(UserService.java:11) ...
注意看 Caused by: :这就是异常链的核心威力所在。它不仅告诉调用者上层业务出错了,还顺藤摸瓜地把底层的罪魁祸首完整展示了出来。
总结与应用场景
核心作用 :屏蔽底层实现细节,同时保留底层错误根源 ,实现“高层抛出业务异常,底层保留技术异常”。
常见应用 :在现代企业级框架(如 Spring、MyBatis 等)中非常普遍。例如,当你使用 MyBatis 查询数据库出错时,它捕获到底层的 SQLException,并会将其包装并抛出它自己的 DataAccessException(带有异常链),方便开发者通过日志一眼看出是底层 SQL 语法错了、主键冲突了还是连接断了。
常用类 包装类
1) 针对八种基本数据类型相应的引用类型—包装类 2) 有了类的特点,就可以调用类中的方法
java 有 8 种基本数据类型 ,不是对象;
包装类 :把基本类型包装成【对象】,位于java.lang包
基本类型
对应包装类
byte
Byte
short
Short
int
Integer
long
Long
float
Float
double
Double
char
Character
boolean
Boolean
💡记忆:除了 int→Integer、char→Character,其余基本类型首字母大写 就是包装类。
装箱 & 拆箱(核心考点)
手动装箱 / 拆箱(JDK5 之前写法)
装箱 :基本类型 → 包装对象
int a = 10 ;Integer num = Integer.valueOf(a);
拆箱 :包装对象 → 基本类型
Integer num = new Integer (10 );int a = num.intValue();
自动装箱、自动拆箱(JDK5 + 新特性,编译器自动转换)
Integer i = 10 ; int n = i;
⚠️底层本质:自动装箱底层调用valueOf();自动拆箱调用xxxValue()。
Integer 缓存机制
Integer.valueOf(int i) 存在缓存数组 cache[]
默认缓存范围:-128 ~ 127
范围内数字:直接复用缓存对象,==结果 true
超出范围:新建Integer对象,==结果 false
Integer a = 100 ;Integer b = 100 ;System.out.println(a == b); Integer c = 200 ;Integer d = 200 ;System.out.println(c == d);
✅重要提醒:
== 比较:
两边如果是包装对象 → 比较地址
一边包装、一边基本类型 → 触发自动拆箱,比较数值
开发比较数值一律使用 .equals(),不要直接==
拓展缓存范围
只有 Byte、Short、Integer、Long 缓存区间固定[-128,127]
Character缓存 0~127
Float、Double没有缓存 !
字符串 和 基本类型互相转换
基本类型 → String
方式 1:变量 + ""
int num = 99 ;String str = num + "" ;
方式 2:String.valueOf()(推荐)
String str = String.valueOf(num);
String → 基本类型
使用包装类的静态方法 parseXxx()
String s = "123" ;int n = Integer.parseInt(s);double d = Double.parseDouble(s);
⚠️注意:字符串格式不对,抛出 NumberFormatException 数字格式异常。
包装类与基本类型区别汇总
默认值不同
基本类型:有默认值(int=0,boolean=false)
包装类:默认值 null
❗坑:包装类变量为 null 时,执行自动拆箱 → NullPointerException 空指针异常
Integer i = null ;int a = i;
存储位置
基本类型:局部变量存在栈
包装类对象:对象本体在堆
使用场景
集合 List/Map 不能存放基本类型,只能存包装类
List<Integer> list = new ArrayList <>(); List<int > list = new ArrayList <>();
String 类
一、String 基本介绍
java.lang.String,代表字符串 。
字符串本质:字符序列 ,底层:
JDK8:private final char[] value
JDK9+:private final byte[] value(优化存储)
重点:String 对象不可变(immutable)
value 数组被 final 修饰,数组引用不能指向新数组
一旦字符串创建,内容无法修改
⚠️误区:
String s = "abc" ;s = "def" ;
不是修改原来对象!只是让s引用指向新字符串对象 ,旧对象等待 GC 回收。
二、字符串创建两种方式
方式 1:直接赋值(字面量)
执行流程:
先去字符串常量池(堆内存) 查找 “Java”
找到:直接复用常量池中的对象
没找到:在常量池中创建,引用指向常量池对象
方式 2:new String ()
String str2 = new String ("Java" );
执行流程:
先检查常量池有没有"Java",没有则放入常量池;
在堆中创建新 String 对象 ,引用指向堆对象。
经典面试对比
String s1 = "abc" ;String s2 = "abc" ;System.out.println(s1 == s2); String s3 = new String ("abc" );String s4 = new String ("abc" );System.out.println(s3 == s4);
三、字符串拼接规则
规则 1:编译期常量优化
两个字面量直接相加,编译器自动合并
规则 2:存在变量参与拼接 → 在堆创建新对象
String a = "a" ;String s = a + "b" ;
intern () 方法
str.intern():主动把字符串放入常量池
如果常量池已经存在相同内容,返回常量池引用;不存在则将对象放入常量池并返回池内引用。
四、== 和 equals ()
==
基本类型:比较数值
String 引用:比较对象地址
equals()
String s1 = new String ("hello" );String s2 = "hello" ;System.out.println(s1 == s2); System.out.println(s1.equals(s2));
五、常用 API 方法
String str = "HelloJava" ;str.length(); str.charAt(索引); str.indexOf("Java" ); str.substring(begin,end); str.trim(); str.toUpperCase(); str.toLowerCase(); str.contains("He" ); str.startsWith("He" ); str.endsWith("va" ); str.split("," ); str.replace("a" ,"b" );
六、不可变性带来的优缺点
✅优点:
字符串常量池实现复用,节省内存
哈希值只需要计算一次,适合作为 HashMap 的 key
线程安全,多线程并发访问无问题
❌缺点:
频繁字符串拼接,不断产生新对象,产生大量垃圾。
👉大量拼接解决方案:StringBuilder / StringBuffer
Stringbuffer类
一、基础概念
java.lang.StringBuffer,可变字符序列
底层:char[](JDK9 之后byte[]),数组没有被final修饰,可以直接扩容修改内容,不会频繁创建新对象 。
对比:
String:不可变,每次拼接产生新对象,大量拼接效率极低
StringBuffer:可变容器,原地修改字符数组
二、构造方法
StringBuffer sb1 = new StringBuffer ();StringBuffer sb2 = new StringBuffer (50 );StringBuffer sb3 = new StringBuffer ("hello" );
默认容量 16:预留空间,减少频繁扩容。
三、核心常用方法(全部支持链式调用 )
StringBuffer sb = new StringBuffer ();sb.append("java" ); sb.insert(1 ,"A" ); sb.delete(0 ,2 ); sb.replace(0 ,1 ,"Hi" ); sb.reverse(); sb.setCharAt(0 ,'X' ); sb.length(); sb.capacity(); String str = sb.toString();
四、扩容机制(重点)
当 append 后字符超出容量,触发扩容
扩容公式:新容量 = 原有容量 * 2 + 2
如果扩容后依然不够,则直接使用所需最小容量
底层:新建更大数组,复制旧数组内容
示例:初始容量 16 → 扩容后 16*2+2=34
五、StringBuffer 和 String 的互相转换
String str = "hello" ;StringBuffer sb = new StringBuffer (str);String result = sb.toString();
六、高频易错考点
append 返回的是自身 this 对象 ,所以支持链式调用
sb.append("a" ).append("b" ).append("c" );
toString() 会新建 String 对象,不会复用底层数组
不要频繁 new StringBuffer ,预估长度尽量指定初始容量,避免多次扩容
线程不安全场景误用 StringBuffer 不能带来收益,反而降低速度;单线程优先StringBuilder
七、经典误区
❌错误理解:
“StringBuffer 比 String 快”
✅准确说法:
少量拼接差距不大;大量循环拼接时,String 不会产生大量临时对象,性能差距巨大 。
StringBuilder 完整知识点
一、基础介绍
可变字符序列,用于字符串拼接,JDK1.5 推出
底层:字符数组(JDK8 char [] / JDK9 byte []),数组无 final 修饰,可以直接修改,不会频繁创建新字符串对象。
和 StringBuffer API 几乎完全一致,唯一区别:没有 synchronized 同步锁 。
二、构造方法
StringBuilder sb1 = new StringBuilder ();StringBuilder sb2 = new StringBuilder (30 );StringBuilder sb3 = new StringBuilder ("hello" );
三、核心常用方法(支持链式调用)
StringBuilder sb = new StringBuilder ();sb.append("Java" ); sb.insert(1 ,"_" ); sb.delete(0 ,2 ); sb.replace(0 ,1 ,"Hi" ); sb.reverse(); sb.setCharAt(0 ,'A' ); sb.length(); sb.capacity(); String str = sb.toString();
原理:所有修改操作直接操作底层数组 ,不产生新对象。
四、扩容机制
触发时机:追加内容后,字符总数超过当前容量
默认扩容公式:
新容量 = 原有容量 * 2 + 2
如果扩容后的容量依旧不够存放,直接采用所需最小容量 。
示例:
初始容量 16 → 扩容:16×2+2 = 34
💡优化建议:如果你提前预估字符串长度,创建时指定容量,避免多次扩容、数组复制,提升性能。
五、String / StringBuilder / StringBuffer 三者终极对比
类
可变性
线程安全
锁
性能
适用场景
String
不可变
线程安全
无
低
少量拼接、字符串常量
StringBuffer
可变
✅线程安全
方法加synchronized
较低
多线程并发拼接
StringBuilder
可变
❌线程不安全
无锁
最高
单线程拼接(开发首选)
开发准则:
绝大多数业务代码(单线程) → 优先 StringBuilder
六、相互转换
String s = "demo" ;StringBuilder sb = new StringBuilder (s);String res = sb.toString();
七、高频考点 & 易错点
线程不安全典型场景
多个线程同时操作同一个 StringBuilder 对象,会出现数据错乱、数组越界异常。
解决方案:多线程改用 StringBuffer,或者每个线程新建独立 StringBuilder。
循环拼接大坑
String str = "" ;for (int i=0 ;i<10000 ;i++){ str += i; } StringBuilder sb = new StringBuilder ();for (int i=0 ;i<10000 ;i++){ sb.append(i); } String str = sb.toString();
编译器优化误区
String s = "a" +"b" +"c" ;String a = "a" ;String s = a + "b" ;
Math 类
一、基础介绍
提供大量数学计算静态方法 ,全部是 static,不需要 new 对象,直接类名。方法调用
构造方法被 private 修饰,无法实例化
包含两个常用静态常量
调用方式示例:
double num = Math.abs(-10 );
二、常用方法分类
绝对值 abs ()
Math.abs(-5 ); Math.abs(-3.14 );
取最值 max /min
Math.max(10 ,20 ); Math.min(10 ,20 );
四舍五入、向上取整、向下取整
Math.round(3.4 ); Math.round(3.5 ); Math.ceil(3.1 ); Math.ceil(-3.1 ); Math.floor(3.9 ); Math.floor(-3.1 );
⚠️极易踩坑:负数的 ceil、floor 不要凭直觉判断!
幂运算、开平方
Math.pow(2 ,3 ); Math.sqrt(16 );
随机数 random ()
✅业务常用公式:获取 [a,b] 的随机整数
int num = (int )(Math.random() * (max - min + 1 )) + min;int r = (int )(Math.random() * 10 ) + 1 ;
拓展:JDK 推荐也可以使用 java.util.Random 类,功能更强。
三角函数(参数是弧度 ,不是角度)
Math.sin(弧度); Math.cos(弧度); Math.tan(弧度); Math.toRadians(角度); //角度 → 弧度 Math.toDegrees(弧度); //弧度 → 角度
三、Math vs StrictMath(了解)
四、高频易错点
round() 返回 long;ceil() / floor() 返回 double
double d = Math.ceil(2.2 );
Math.random () 左闭右开 [0.0,1.0)
pow 返回 double,即使整数次方,不能直接当 int 使用,需要强转
三角函数传入的是弧度 ,直接传入角度结果错误
java.util.Arrays
一、概述
java.util.Arrays:数组工具类
包:java.util,使用需要导包
所有方法都是 static 静态方法 ,直接 Arrays.方法名() 调用
构造方法私有,不能 new Arrays 对象
作用:提供数组打印、排序、查找、复制、填充、比较等工具方法
二、常用核心方法
toString () 打印数组
普通数组直接打印输出地址,Arrays.toString 打印数组内容
int [] arr = {1 ,3 ,2 };System.out.println(Arrays.toString(arr));
注意:只支持一维数组
多维数组使用:deepToString()
int [][] arr2 = {{1 ,2 },{3 ,4 }};System.out.println(Arrays.deepToString(arr2));
sort () 数组排序
① 基本类型数组:升序排序(快速排序)
int [] arr = {5 ,2 ,9 ,1 };Arrays.sort(arr); System.out.println(Arrays.toString(arr));
② 对象数组 / 自定义排序(引用类型)
Arrays.sort(数组, Comparator) 支持自定义顺序(只能用于包装类型数组,不能用 int [],要用 Integer [] )
Integer[] arr = {5 ,2 ,9 ,1 }; Arrays.sort(arr, (a,b)->b-a);
重点:基本类型 sort 不支持直接传入比较器!
拓展:可以指定区间排序 Arrays.sort(arr,from,to) 左闭右开
binarySearch () 二分查找
前提条件:数组必须先升序排序!否则结果不可预测
找到:返回元素索引
找不到:返回 -(插入点)-1
int [] arr = {1 ,2 ,5 ,9 };int index = Arrays.binarySearch(arr,5 ); int index2 = Arrays.binarySearch(arr,3 );
4. copyOf () /copyOfRange () 数组复制 copyOf (数组,新长度) 从头复制,可扩容 / 缩容
int [] old = {1 ,2 ,3 };int [] newArr = Arrays.copyOf(old,5 );
copyOfRange (数组,起始索引,结束索引) 区间复制,左闭右开 [start,end)
int [] arr = {10 ,20 ,30 ,40 };int [] sub = Arrays.copyOfRange(arr,1 ,3 );
⚠️区别:
数组自身 clone () 和 Arrays.copyOf 效果类似;copyOf 底层调用 System.arraycopy。
fill () 数组填充
给数组全部 / 指定区间统一赋值
int [] arr = new int [5 ];Arrays.fill(arr,8 ); Arrays.fill(arr,1 ,3 ,6 );
equals () 判断两个数组内容是否相同
int [] a = {1 ,2 ,3 };int [] b = {1 ,2 ,3 };System.out.println(Arrays.equals(a,b));
⚠️误区:
a==b 比较数组地址;
Arrays.equals() 比较数组中每个元素的值。
多维数组比较使用:deepEquals()
三、一维数组 vs 多维数组配套方法汇总
功能
一维数组
多维数组
打印内容
toString()
deepToString()
比较内容
equals()
deepEquals()
四、高频易错考点
binarySearch 二分查找必须先排序 ,无序数组结果错误
Arrays.sort 基本类型只能升序;想要降序必须使用包装数组 + Comparator
copyOf 产生新数组 ,原数组不会改变
Arrays.toString 不能打印多维数组,只会输出地址,要用 deepToString
数组是引用类型,equals() 默认比较地址,一定要用 Arrays.equals 比较元素
五、拓展:数组转集合(asList)⭐大坑
Integer[] nums = {1 ,2 ,3 }; List<Integer> list = Arrays.asList(nums);
⚠️重要坑:
asList 接收引用类型数组 ;不能直接传基本类型数组int[]
返回的 List 是 Arrays 内部静态类,固定大小!不能 add/remove ,会抛出异常
集合和原数组互相引用 ,修改数组元素,集合同步变化
Java System 类 完整知识点
一、概述
全部成员都是静态 ,直接 System.xxx 调用,无需实例化
构造方法 private,禁止 new System()
代表程序运行平台系统资源,提供标准输入输出、时间、数组复制、退出程序、环境变量等操作
二、三大静态常量(流对象)
public static final InputStream in;public static final PrintStream out;public static final PrintStream err;
区分 out 和 err:
out:普通信息输出,缓冲区缓存,顺序不一定立刻打印
err:错误信息,立即输出,控制台一般红色
三、核心常用方法
arraycopy () 数组复制⭐底层非常重要
public static void arraycopy ( Object src, //源数组 int srcPos, //源数组起始索引Object dest, //目标数组 int destPos, //目标数组起始索引int length //复制元素个数)
示例:
int [] a = {10 ,20 ,30 ,40 ,50 };int [] b = new int [5 ];System.arraycopy(a,1 ,b,0 ,3 );
✅特点:
获取时间
long time1 = System.currentTimeMillis();long time2 = System.nanoTime();
使用场景:计算程序运行耗时
long start = System.currentTimeMillis();long end = System.currentTimeMillis();System.out.println("耗时:" +(end-start)+"ms" );
区别:
currentTimeMillis:基于系统时钟,可以手动修改系统时间,数值会跳变
nanoTime:基于虚拟机计时器,适合计算时间间隔,不适合获取日期
exit (状态码) 终止 JVM
System.exit(0 ); System.exit(1 );
⚠️重点考点:
执行 System.exit() 直接关闭虚拟机,finally 代码块不会执行!
gc () 建议垃圾回收
作用:向 JVM建议 执行垃圾回收;
只是建议,不保证立刻执行 GC,什么时候回收由虚拟机决定。
获取环境变量、系统属性
String path = System.getProperty("user.dir" ); String os = System.getProperty("os.name" ); String javaHome = System.getenv("JAVA_HOME" );
常用属性:
user.name 当前用户名
java.version Java 版本
四、输入输出重定向(了解)
System.setOut(new PrintStream ("log.txt" ));
五、高频面试易错点汇总
System.arraycopy 是 native 方法,效率高于手动 for 循环复制数组
System.exit(0) 停止 JVM,会跳过 finally 执行
System.gc() 只是通知虚拟机,不能强制垃圾回收
currentTimeMillis() 毫秒时间戳;nanoTime() 仅用于计算间隔
System.out 和 System.err 是两个不同流,打印顺序无法保证
六、拓展对比
Arrays.copyOf → 底层调用 System.arraycopy
System.arraycopy 是底层原生方法,开发中很少直接手写,框架大量使用。
BigInteger 和 BigDecimal
一、背景引入
基本数据类型局限 long、int有取值上限;float/double 浮点运算存在精度丢失(二进制存储小数缺陷)。
System.out.println(0.1 + 0.2 );
解决方案
BigInteger :超大整数(无上限)
BigDecimal :高精度小数,金融、金额计算必须使用 包:java.math
二、BigInteger(超大整数)
创建对象
⚠️不能直接使用 + - * / 运算符!只能调用方法
BigInteger num1 = new BigInteger ("12345678901234567890" );BigInteger num2 = BigInteger.valueOf(100 );
常用运算方法
BigInteger a = new BigInteger ("20" );BigInteger b = new BigInteger ("7" );a.add(b); a.subtract(b); a.multiply(b); a.divide(b); a.remainder(b); a.mod(b); a.abs(); a.max(b); a.min(b);
重点:所有运算返回新 BigInteger 对象 ,原对象不可变!
带余数除法(divideAndRemainder)
BigInteger[] res = a.divideAndRemainder(b); res[0 ] 商,res[1 ]余数
三、BigDecimal(高精度小数 ⭐面试 & 业务重点)
对象创建
BigDecimal num = new BigDecimal (0.1 );BigDecimal num1 = new BigDecimal ("0.1" ); BigDecimal num2 = BigDecimal.valueOf(0.1 );
四则运算
同样不可变,运算返回新对象
BigDecimal a = new BigDecimal ("0.1" );BigDecimal b = new BigDecimal ("0.2" );a.add(b); a.subtract(b); a.multiply(b); a.divide(b);
除法 & 保留小数、舍入模式(核心)
除法必须指定保留位数 + 舍入模式
BigDecimal a = new BigDecimal ("10" );BigDecimal b = new BigDecimal ("3" );a.divide(b,2 ,BigDecimal.ROUND_HALF_UP);
常用舍入模式:
ROUND_HALF_UP:四舍五入(日常最常用)
ROUND_UP:向上取整
ROUND_DOWN:直接截断,舍弃后面小数
ROUND_HALF_EVEN:银行家舍入(JDK 默认)
大小比较
❌禁止使用 ==(比较地址)
❌尽量不要用 equals ()!equals 会比较小数位数
BigDecimal d1 = new BigDecimal ("2.0" );BigDecimal d2 = new BigDecimal ("2.00" );System.out.println(d1.equals(d2));
✅正确方式:compareTo()
if (d1.compareTo(d2) == 0 ){ System.out.println("数值相等" ); }
小数点操作
bd.setScale(2 ,BigDecimal.ROUND_HALF_UP);
四、BigInteger vs BigDecimal 对比
类
适用场景
存储内容
BigInteger
超大整数运算
整数,无小数
BigDecimal
金融金额、高精度小数
支持小数,解决浮点精度丢失
共性:
不可变类 ,每次计算产生新对象
不支持运算符重载,必须调用方法
在java.math包
五、高频易错考点汇总
double 直接传入 BigDecimal 构造器会精度丢失,优先字符串构造
BigDecimal 除法如果无法整除,必须指定精度和舍入模式,否则抛异常
equals 会比较小数位数;数值比较用compareTo
对象不可变,运算结果需要接收返回值
bd.add(xxx); bd = bd.add(xxx);
double/float 不要用于金额计算,强制使用 BigDecimal
六、开发规范(金融项目)
数据库金额:decimal 类型
Java 代码:使用 BigDecimal 存储金额
运算统一指定保留小数位数与四舍五入
Java 日期时间全套知识点
分为两大体系:
旧版(JDK1.0 ~ JDK7) :Date、SimpleDateFormat、Calendar
❌缺点:线程不安全、API 设计差、可变性
新版(JDK8+ 推荐)java.time 包 :LocalDate、LocalTime、LocalDateTime、DateTimeFormatter、Instant、Duration、Period
✅优点:不可变、线程安全、设计优雅,项目优先使用
一、旧 API(了解,尽量不推荐新项目使用)
java.util.Date
代表瞬时时间点(毫秒时间戳)
Date date = new Date ();Date date2 = new Date (1750000000000L );long time = date.getTime();date.setTime(1750000000000L );
⚠️缺点:
Date对象可变 ;setTime 修改原有对象
无法直接获取年、月、日;月份从 0 开始(0=1 月,大坑)
SimpleDateFormat 日期格式化
SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" );String str = sdf.format(new Date ());Date date = sdf.parse("2026-07-28 12:30:00" );
⚠️致命坑:SimpleDateFormat 线程不安全!多线程共用会异常。
常用格式符号:
yyyy 4 位年份
MM 月份 (01~12)
dd 日期
HH 24 小时制;hh12 小时制
mm分钟;ss秒;SSS毫秒
Calendar 日历类(JDK1.1 替代 Date)
抽象类,常用实现 GregorianCalendar
Calendar cal = Calendar.getInstance();int year = cal.get(Calendar.YEAR);int month = cal.get(Calendar.MONTH); int day = cal.get(Calendar.DAY_OF_MONTH);cal.set(2026 ,6 ,28 ); Date date = cal.getTime();
缺点:依旧可变,API 别扭。
二、JDK8 全新时间包 java.time(⭐重点,面试核心)
所有对象不可变 ,修改操作返回新对象;线程安全!
核心类区分
类
作用
Instant
时间戳(UTC 标准时间,毫秒 / 纳秒),对应旧 Date
LocalDate
只有年月日 2026-07-28(无时分秒、无时区)
LocalTime
只有时分秒 14:30:20
LocalDateTime
年月日 + 时分秒,无时区
ZonedDateTime
带时区的完整时间(需要时区运算使用)
DateTimeFormatter
格式化类(替代 SimpleDateFormat,线程安全)
Duration
时间间隔:时分秒(秒、毫秒级别,两个时间点)
Period
日期间隔:年、月、日(两个 LocalDate)
创建对象
LocalDate nowDate = LocalDate.now();LocalTime nowTime = LocalTime.now();LocalDateTime nowDT = LocalDateTime.now();LocalDate date = LocalDate.of(2026 ,7 ,28 );LocalDateTime dt = LocalDateTime.of(2026 ,7 ,28 ,14 ,30 );Instant instant = Instant.now();Instant instant2 = Instant.ofEpochMilli(1750000000000L );
常用运算(不可变!操作返回新对象)
LocalDateTime time = LocalDateTime.now();time.plusDays(3 ); time.minusHours(2 ); time.plusMonths(1 ); int month = time.getMonthValue(); int day = time.getDayOfMonth();time.withYear(2027 );
格式化与解析 DateTimeFormatter ✅线程安全
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" );LocalDateTime now = LocalDateTime.now();String str = now.format(formatter);LocalDateTime dt = LocalDateTime.parse("2026-07-28 15:20:00" ,formatter);
时间间隔计算 Duration / Period
LocalDateTime t1 = LocalDateTime.of(2026 ,7 ,28 ,10 ,0 );LocalDateTime t2 = LocalDateTime.of(2026 ,7 ,28 ,12 ,30 );Duration duration = Duration.between(t1,t2);System.out.println(duration.toHours()); LocalDate d1 = LocalDate.of(2026 ,1 ,1 );LocalDate d2 = LocalDate.of(2026 ,7 ,28 );Period period = Period.between(d1,d2);period.getMonths();
新旧 API 互相转换(项目兼容必备)
Instant instant = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();Date date = Date.from(instant);LocalDateTime ldt = new Date ().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
时区简单说明
LocalDateTime不带时区 ,跨时区业务(海外系统)必须使用 ZonedDateTime。
服务器一般统一使用 UTC 时间存储。
三、新旧 API 核心对比
特性
Date/SimpleDateFormat
java.time(JDK8)
可变性
可变
不可变
线程安全
SimpleDateFormat 不安全
全部安全
月份
0 开始
1 开始,符合直觉
API
难用,功能弱
丰富清晰
四、高频易错考点
旧 Date、Calendar 月份从 0 开始;LocalDateTime 月份 1~12
SimpleDateFormat 禁止多线程共享;新项目一律用 DateTimeFormatter
LocalDateTime 无时区;不要直接用于跨时区业务
Duration 处理时分秒;Period 只处理年月日,不能混用
所有 JDK8 时间对象修改后返回新实例,原对象不变
集合 一、集合概述
集合:java.util包下,容器 ,用来存储多个对象。
区分数组和集合:
数组:长度固定;可以存基本类型 + 引用类型
集合:长度可变;只能存储引用类型(对象) ,基本类型会自动装箱
集合两大根接口:
Collection 单列集合 :一次存一个对象
Map 双列集合 :一次存一对对象(key=value 键值对)
集合整体架构图
Collection(单列) ├─ List【有序、可重复、有索引】 │ ├─ ArrayList 数组实现 │ ├─ LinkedList 双向链表 │ └─ Vector 数组实现(古老、同步) ├─ Set【无序、不可重复、无索引】 │ ├─ HashSet 哈希表 │ │ └─ LinkedHashSet 哈希表+双向链表(有序) │ └─ TreeSet 红黑树(自然排序) └─ Queue【队列】 └─ ArrayDeque、PriorityQueue Map(双列 key-value) ├─ HashMap 哈希表 │ └─ LinkedHashMap 哈希表+链表(有序) ├─ TreeMap 红黑树,按键排序 ├─ Hashtable 古老,同步(淘汰) └─ Properties 键值对,用于配置文件
第一部分:Collection 接口
通用常用方法(所有 List、Set 都拥有)
boolean add (E e) 添加元素boolean remove (Object o) 删除指定元素void clear () 清空集合boolean contains (Object o) 判断是否包含元素boolean isEmpty () 判断是否为空int size () 获取元素个数Object[] toArray() 转数组
List 接口
特点:有序、可重复、带有索引 ,支持根据索引增删查改。
特有方法(带索引操作)
get(int index) set(int index, E element) add(int index, E element) remove(int index)
① ArrayList(重点)
底层:可变 Object 数组
默认初始容量:10
扩容机制:原容量 1.5倍
查询快、随机访问快;中间增删慢 (需要移动元素)
线程不安全 ;效率高
适用:大量查询,少量增删
② LinkedList
底层:双向链表
没有扩容概念,按需创建节点
查询慢(需要遍历);首尾增删速度快
线程不安全
特有方法(队列 / 栈操作)
addFirst() addLast() getFirst() getLast() removeFirst() removeLast()
③ Vector(了解,基本淘汰)
底层数组,扩容 2 倍;方法加synchronized,线程安全,性能差。
特性
ArrayList
LinkedList
底层
动态数组
双向链表
随机访问
快(下标直接定位)
慢(遍历)
中间插入删除
慢(移动大量元素)
快(修改指针)
内存
连续内存,有预留容量
每个节点额外存前后指针
开发 90% 场景优先 ArrayList!
Set 接口
特点:无序、不可重复、无索引 ,不能用普通 for 循环遍历,只能迭代器 / 增强 for。
① HashSet
底层:哈希表(数组 + 链表 + 红黑树)
元素唯一依靠:hashCode() + equals()
判断重复规则:
先比较哈希值 hashCode ()
哈希值不同 → 直接不重复
哈希值相同 → 再调用 equals (),true 判定重复
⚠️自定义类存入 HashSet,必须重写 hashCode 和 equals!
特性:无序,不保证存储顺序;线程不安全。
② LinkedHashSet
继承 HashSet;底层哈希表 + 双向链表
✅存取有序 ;依然不能重复。
适合:需要去重同时保留添加顺序。
③ TreeSet
底层:红黑树
✅自动排序(自然排序)
要求:存储元素实现 Comparable 接口,或者构造传入Comparator
⚠️元素不能为 null;根据比较规则去重,不是 equals。
Queue 队列(简单了解)
先进先出 FIFO
ArrayDeque:数组双端队列
PriorityQueue:优先队列(自动排序)
Collection 遍历方式
增强 for 循环(底层迭代器)
Iterator 迭代器
Iterator<String> it = list.iterator(); while (it.hasNext()){ String s = it.next(); }
⚠️并发异常:迭代遍历集合时,不要用集合 add/remove;想要删除用迭代器it.remove()
List 额外支持普通 for 循环(有索引)
第二部分:Map 双列集合
存储格式:key=value,键唯一,值可重复;
一个 key 只能对应一个 value;后存入的 value 覆盖旧 value。
Map 通用方法
put(k,v) 添加/覆盖键值对 remove(key) 根据key删除 get(key) 根据key获取value containsKey (key) 判断是否存在键containsValue(value) 判断是否存在值 keySet() 获取所有key【重点】 entrySet() 获取所有键值对对象【重点】 clear() size()
Map 三种遍历方式
keySet () 先拿所有键,再 get 取值
Set<String> keys = map.keySet(); for (String key : keys){ map.get(key); }
entrySet () 获取 Entry 键值对(效率更高,推荐)
Set<Map.Entry<String,Integer>> entries = map.entrySet(); for (Map.Entry entry : entries){ entry.getKey(); entry.getValue(); }
values () 只获取所有 value(无法拿到 key)
常用实现类
① HashMap(重中之重)
底层:哈希表(数组 + 链表 + 红黑树)JDK1.8 优化
参数:
特点:
② LinkedHashMap
继承 HashMap;哈希表 + 双向链表
✅存取有序 ;其他特性和 HashMap 一致。
可用来实现 LRU 缓存。
③ TreeMap
底层红黑树;按键自动排序
key 必须实现 Comparable 或传入比较器;key 不能为 null。
④ Hashtable(淘汰)
线程安全;key 和 value 都不允许 null;扩容 2 倍;性能差,基本不用。
⑤ Properties
继承 Hashtable;专门读取配置文件 xxx.properties;key、value 只能是字符串。
三、重要面试高频对比
HashMap vs HashTable
HashMap 线程不安全;Hashtable 方法加 synchronized,安全
HashMap 允许 key 一个 null;Hashtable 键值都不能 null
HashMap 初始容量 16,扩容 ×2;Hashtable 初始 11,扩容 ×2+1
Hashtable 古老,不推荐使用;多线程可用 ConcurrentHashMap
HashSet 和 HashMap 关系
HashSet 底层就是 HashMap!
HashSet 存储元素,作为 HashMap 的 key,value 统一是一个静态 Object 常量。
四、Collections 工具类(区分 Collection 接口!)
java.util.Collections 操作集合的静态工具类
Collections.sort(list); Collections.shuffle(list); Collections.max/min(list); Collections.unmodifiableList() Collections.synchronizedList(list);
⚠️注意:只是方法加锁,性能一般;高并发优先 JUC 工具。
五、高频坑 & 考点总结
迭代遍历集合时,不能直接集合 add/remove → ConcurrentModificationException 并发修改异常
HashSet/HashMap 自定义对象作为 key,必须重写 hashCode 和 equals
ArrayList 初始容量 10,第一次 add 才初始化数组;扩容 1.5 倍
HashMap 负载因子 0.75,树化阈值 8,链表转红黑树
List 有序可重复;Set 无序不可重复;Map 键唯一
TreeSet/TreeMap 依靠比较器排序,不是 equals 去重
集合存储的是对象引用地址 ,不是对象本身
自动装箱:集合不能存基本类型 List<int> 报错,只能包装类 List<Integer>
六、线程安全小结
不安全:ArrayList、LinkedList、HashMap、HashSet
安全方案:
Collections.synchronizedXxx () 包装
JUC 并发包:CopyOnWriteArrayList、ConcurrentHashMap
泛型 一、什么是泛型
泛型:参数化类型 ,把类型当作参数 传入类、接口、方法。
<>内的标识符代表未知类型,编译期确定。
核心目标:
编译期类型检查 ,防止类型转换异常
消除强制类型转换
代码复用,一套代码支持多种数据类型
诞生:JDK 1.5 引入;泛型只存在编译期,运行时被擦除(类型擦除)
二、为什么需要泛型
没有泛型时,集合默认存储 Object:
List list = new ArrayList ();list.add("abc" ); list.add(123 ); String s = (String) list.get(1 );
使用泛型:
List<String> list = new ArrayList <>(); list.add("abc" ); String s = list.get(0 );
三、泛型的三种使用形式
泛型类
格式:类名<泛型标识>
常用标识符约定(只是名字,无语法要求)
E Element 元素(集合)
T Type 类型
K Key 键 、V Value 值(Map)
R Return 返回值
public class MyBox <T> { private T data; public T getData () { return data; } public void setData (T data) { this .data = data; } } MyBox<String> box = new MyBox <>(); box.setData("hello" );
注意:创建对象时指定类型,这个类里面所有 T 都会被替换为指定类型。
泛型接口
interface IStorage <T> { void save (T data) ; } class StringStorage implements IStorage <String>{ @Override public void save (String data) {} } class AnyStorage <T> implements IStorage <T>{ @Override public void save (T data) {} }
泛型方法(⭐重点:和泛型类区分开)
泛型方法可以定义在普通类 / 泛型类上,方法自己拥有独立泛型参数
✅格式:返回值前声明泛型 <T>
public static <T> T getFirst (List<T> list) { return list.get(0 ); }
⚠️关键区分:
泛型类的 T:创建对象时确定
泛型方法的 T:调用方法时自动推导类型
四、类型擦除
泛型是编译期概念,.class 字节码中不存在泛型信息!
编译流程:
编译器利用泛型做类型校验
擦除泛型参数 ,替换为原始类型(上限 Object / 指定边界类型)
自动插入强制类型转换代码
示例:
List<String> list = new ArrayList <>(); list.add("aaa" ); String s = list.get(0 );
编译后字节码等价于:
List list = new ArrayList ();list.add("aaa" ); String s = (String) list.get(0 );
原始类型 RawType
不加泛型参数的集合:List list = new ArrayList();
原始类型会关闭泛型检查,尽量不要使用。
擦除规则
无边界泛型 <T> → 擦除为 Object
有上限 <T extends Number> → 擦除为 Number
有下限不能单独用于类泛型(只能通配符)
五、通配符 ? Wildcard
? 代表任意未知类型 ,主要用于方法参数,不能用于定义泛型类 / 泛型方法。
无界通配符 <?>
public void printList (List<?> list) { for (Object o : list) System.out.println(o); }
限制:
✅可以读取元素,读到 Object
❌不能 add 除 null 以外的任何元素 (编译器不知道真实类型,禁止写入)
上限通配符 ? extends T
? extends Animal:代表T 及其所有子类
public void test (List<? extends Animal> list) { Animal a = list.get(0 ); }
读取安全,写入不安全。生产者场景(读数据)
下限通配符 ? super T
? super Dog:代表Dog 及其所有父类
public void test (List<? super Dog> list) { list.add(new Dog ()); }
写入安全,读取不安全。消费者场景(写入数据)
PECS 原则(高频面试)
Producer Extends,Consumer Super
只读取数据(生产者):? extends T
只写入数据(消费者):? super T
既要读又要写:不能使用通配符,直接确定泛型<T>
六、泛型边界(限定类型范围)
上限限定 <T extends 类/接口>
T 只能是该类或子类
public <T extends Number > void method (T num) {}
⚠️泛型没有多上限直接写法 ;想要同时多个约束,需要 T extends A & B
<T extends Serializable & Comparable>
❗注意:没有 <T super xxx> 写法!
下限只能用于通配符? ,不能用于泛型声明。
七、泛型常见限制(
不能使用泛型创建实例
不能定义泛型数组
静态方法不能使用类上定义的泛型
public class Test <T>{ public static void fun (T data) {} }
原因:类泛型在创建对象时确定,静态方法不需要对象。
✅解决方案:使用独立泛型方法 <E>
基本类型不能作为泛型参数
List<int> ❌;必须使用包装类 List<Integer> ✅
泛型不支持继承多态误区
List<String> list1 = new ArrayList <>(); List<Object> list2 = list1;
虽然 String 是 Object 子类,但是 List<String>不是 List<Object> 的子类 。泛型容器之间不存在继承关系!
重载不能依靠泛型区分(擦除后方法签名一样)
void fun (List<String> list) {}void fun (List<Integer> list) {}
八、泛型方法 vs 通配符怎么选?
示例需求:把 list 所有元素添加到另一个集合
public static <T> void copy (List<T> src, List<T> dest) { dest.addAll(src); } public static <T> void copy2 (List<? extends T> src, List<T> dest)
需要同时读写集合 、类型互相约束 → 使用泛型<T>
只单纯读取或写入 → 使用通配符
九、面试高频问题汇总
泛型实现原理?
答:类型擦除 ,编译期校验,运行时无泛型信息。
List<String> 和 List<Object> 有继承关系吗?
没有,泛型容器不存在多态,擦除后都是 List。
? extends T 和 ? super T 区别?PECS?
原始类型、泛型、通配符区别?
能不能 new T ()?为什么?
为什么泛型不支持基本数据类型?
十、背诵精简提纲
泛型参数化类型,JDK1.5 推出;
编译检查免强转,运行类型被擦除;
三类用法:泛型类、泛型接口、泛型方法;
任意未知类型,extends上界super下界;
PECS 口诀:读用 extends,写用 super;
不能 new 泛型对象、不能泛型数组;
静态方法不能使用类泛型;
容器泛型无继承,List不是 List;
基本类型不能做泛型,需要包装类。
JUnit 完整介绍 一、JUnit 是什么
JUnit 是 Java 单元测试框架 ,用于对方法 / 模块进行自动化单元测试,属于 xUnit 体系。
作用:
独立测试单个方法,验证代码逻辑是否正确
自动化执行,快速回归,避免手动 main 方法测试
支持断言、前置 / 后置操作、忽略测试、异常测试、超时测试
两大主流版本:
JUnit4 :JDK1.5+,核心注解 @Test,基于方法
JUnit5(推荐新项目) ,别名 Jupiter,JDK8+,架构重构,功能更强
Maven 坐标区分:
JUnit4:junit:junit:4.13.2
JUnit5:由多个模块组成 junit-jupiter-api(编写)、junit-jupiter-engine(运行)
二、JUnit4 核心知识点(老项目大量使用)
核心注解
注解
作用
@Test
标记该方法为测试方法 ;方法必须 public void,无参数
@Before
每个测试方法执行前运行 (每个 @Test 都会触发一次)
@After
每个测试方法执行完成后运行
@BeforeClass
所有测试方法执行前只执行一次 ;方法必须 public static void
@AfterClass
所有测试执行完毕后执行一次 ;方法必须 public static void
@Ignore
忽略该测试方法,不执行
@Test(expected = 异常.class)
测试预期抛出指定异常
@Test(timeout = 毫秒)
设置测试超时时间,超时判定失败
执行顺序示例:
@BeforeClass → @Before → @Test → @After @Before → @Test → @After @AfterClass
Assert 断言(单元测试核心)
org.junit.Assert,断言失败测试直接报错
Assert.assertEquals(预期值,实际值); Assert.assertNotEquals(); Assert.assertTrue(布尔条件); Assert.assertFalse(); Assert.assertNull(obj); Assert.assertNotNull(obj); Assert.assertSame(对象1 ,对象2 ); Assert.assertNotSame();
规则:单元测试必须使用断言 ,不能靠 System.out 打印判断结果!
JUnit4 代码示例
import org.junit.*;public class DemoTest { @BeforeClass public static void initAll () { System.out.println("所有测试开始前执行一次" ); } @Before public void init () { System.out.println("每个测试方法之前执行" ); } @Test public void testAdd () { int res = 1 +2 ; Assert.assertEquals(3 ,res); } @Test(expected = ArithmeticException.class) public void testException () { int i = 1 /0 ; } @After public void close () { System.out.println("每个测试方法结束执行" ); } @AfterClass public static void destroyAll () { System.out.println("全部测试结束执行一次" ); } }
三、JUnit5
重要变化
包名更换:org.junit.jupiter.api
注解名称大量调整(去掉旧版部分歧义命名)
不再强制要求方法 public(private 以外都可以)
提供更灵活断言、嵌套测试、参数化测试
支持扩展模型替代 JUnit4 的 Rule
JUnit5 核心注解
JUnit5
等效 JUnit4
说明
@Test
@Test
测试方法
@BeforeEach
@Before
每个测试方法前执行
@AfterEach
@After
每个测试方法后执行
@BeforeAll
@BeforeClass
所有测试前执行一次,建议 static
@AfterAll
@AfterClass
所有测试后执行一次
@Disabled
@Ignore
禁用 / 忽略测试
@DisplayName("测试名称")
无
自定义测试显示名称,可读性强
@Timeout
@Test(timeout)
超时限制
@ExtendWith
@Rule/@ClassRule
扩展机制
JUnit5 断言:Assertions
org.junit.jupiter.api.Assertions
支持 lambda,失败信息更友好
Assertions.assertEquals(3 , 1 +2 ,"相加结果错误" ); Assertions.assertTrue(2 >1 ); Assertions.assertAll( ()->Assertions.assertEquals(2 ,1 +1 ), ()->Assertions.assertTrue(5 >2 ) ); Assertions.assertThrows(ArithmeticException.class,()->{ int a = 1 /0 ; });
JUnit5 简单示例
import org.junit.jupiter.api.*;@DisplayName("计算器单元测试") public class CalcTest { @BeforeAll static void beforeAll () { System.out.println("全局初始化" ); } @BeforeEach void beforeEach () { System.out.println("单个测试前置" ); } @Test @DisplayName("测试加法") void testPlus () { Assertions.assertEquals(10 ,5 +5 ); } @Disabled @Test void skipTest () { } @AfterEach void afterEach () { System.out.println("单个测试后置" ); } @AfterAll static void afterAll () { System.out.println("全局销毁" ); } }
四、重要概念区分
单元测试 vs 集成测试
单元测试:测试单个类 / 单个方法 ,尽量不依赖数据库、网络;可以 Mock 依赖(Mockito)
集成测试:多个模块、数据库、接口联调
测试规范(开发通用规范)
测试类命名:XxxTest
测试方法命名:test方法名 / shouldXXXWhenXXX
一个测试方法只测一个场景
尽量独立:测试方法之间不能互相依赖,执行顺序不影响结果
五、配套常用扩展
Mockito :配合 JUnit,模拟对象(Mock),解除外部依赖
ParameterizedTest 参数化测试 (JUnit5):一组多组输入批量测试,不用重复写多个 @Test
六、高频面试问题汇总
@Before 和 @BeforeClass 区别?
Before:每个测试方法执行前;BeforeClass 全局只执行一次,必须 static。
JUnit5 对应:BeforeEach / BeforeAll。
单元测试中 Assert 断言作用?
验证预期结果;断言失败测试用例标记失败。不能只用打印输出。
JUnit4 和 JUnit5 主要区别?
包路径不同、注解改名、方法不必 public、强大的分组断言、扩展机制替代 Rule。
单元测试方法有什么要求?
JUnit4:public void、无参数;
JUnit5:返回 void,无参,访问修饰符不限。
@Test (expected=xxx) 缺点?
只能捕获任意位置该异常,无法精准定位哪行代码抛出;JUnit5 推荐 assertThrows。
多线程基础 概念
进程 :操作系统资源分配的最小单位(独立内存、CPU 资源),一个程序就是一个进程。
线程 :进程内独立执行路径,CPU 调度最小单位 ,共享进程资源。
关系:一个进程至少包含 1 个主线程;多线程 = 一个进程多条执行路径。
优缺点
✅优点:
充分利用多核 CPU,提高程序吞吐量
异步处理,提升响应速度(GUI、接口)
❌缺点:
线程切换消耗 CPU 资源
共享资源竞争 → 线程安全问题
并发带来死锁、可见性、有序性问题
并发 & 并行
并发 :单核 CPU,时间片轮转,宏观同时执行,微观交替执行
并行 :多核 CPU,多个线程真正同时运行
创建线程的 4 种方式
方式 1:继承 Thread 类
class MyThread extends Thread { @Override public void run () { } } new MyThread ().start();
⚠️调用 start (),不是 run ()!
run () 只是普通方法;start () 向操作系统申请创建新线程,自动调用 run ()
方式 2:实现 Runnable 接口(推荐)
解耦:线程类和任务分离;避免单继承限制
class Task implements Runnable { @Override public void run () { } } new Thread (new Task ()).start();
方式 3:Callable + FutureTask(有返回值、可抛异常)
Runnable 无返回值、不能抛出受检异常;Callable 弥补该缺陷
Callable<Integer> call = ()->{ return 100 ; }; FutureTask<Integer> task = new FutureTask <>(call); new Thread (task).start();Integer res = task.get();
方式 4:线程池 ExecutorService(生产环境强制使用)
频繁创建销毁线程开销巨大,池化复用线程。
Executors工具类 / ThreadPoolExecutor手动创建(阿里规范推荐)
面试重点区分:
Thread 是线程载体;Runnable/Callable 是任务
线程的五大状态(操作系统层面)
新建 NEW :new Thread (),尚未调用 start ()
就绪 RUNNABLE :调用 start (),等待 CPU 时间片
运行 RUNNING :拿到 CPU,执行 run 方法
阻塞 BLOCKED / WAITING / TIMED_WAITING
BLOCKED:等待 synchronized 锁
WAITING:无限等待 wait ()
TIMED_WAITING:限时等待 sleep (time)、wait (time)
终止 TERMINATED :run 执行完毕 / 异常退出,线程结束
⚠️Java Thread.State 将【就绪 + 运行】合并为 RUNNABLE ,JVM 层面看不到操作系统细分状态!
线程常用方法
Thread 类实例方法
start():启动线程
run():任务逻辑
interrupt():中断线程(设置中断标记,不是立刻终止 )
isInterrupted():判断是否被中断(不清空标记)
join():等待其他线程执行完成(底层 wait)
Thread 静态方法
Thread.sleep(ms):限时阻塞,不释放锁
主动让出 CPU,时间到自动恢复就绪;会抛出 InterruptedException,捕获异常时中断标记会被清除
Thread.yield():礼让 CPU,当前线程回到就绪态(不一定生效)
Thread.currentThread():获取当前执行线程
Object 监视器方法(必须在 synchronized 内调用!)
wait():无限等待,释放锁
wait(long):限时等待,释放锁
notify():随机唤醒一个等待线程
notifyAll():唤醒全部等待线程
synchronized
基础概念
synchronized 是 Java 内置监视器锁(互斥锁) ,属于 JVM 层面实现,用来解决多线程原子性、可见性、有序性 问题,保证同一时刻最多只有一个线程执行同步代码。
别称:隐式锁、内置锁、monitor 锁。
作用:
原子性 :同步代码块同一时间仅一个线程执行
可见性 :解锁前强制把共享变量刷新到主内存;加锁清空本地缓存
有序性 :禁止临界区内指令重排序
三种使用方式(锁对象是谁是核心考点)
修饰【实例方法】
public synchronized void test () {}
✅锁对象:当前实例对象 this
同一个对象多个同步方法互斥;不同对象互不影响。
修饰【静态方法】
public static synchronized void test () {}
✅锁对象:当前类的 Class 对象(类锁)
所有实例共用同一把锁,不管创建多少对象,串行执行。
重点区分:
synchronized静态方法 和 synchronized实例方法互不阻塞!两把不同的锁
修饰【同步代码块】(推荐,粒度可控)
synchronized (obj) {} synchronized (ClassName.class) {} synchronized (this ) {}
优势:缩小锁范围,提高并发效率。
什么是线程同步
多个线程同时访问共享资源,控制访问顺序,保证数据安全,避免并发冲突。
根本问题:多线程并发操作共享变量,产生数据竞争 。
三大并发问题:原子性、可见性、有序性 。
同步本质:让并发操作串行化(部分串行) ,牺牲吞吐量换取正确性。
二、什么时候需要同步?
同时满足下面 3 点,一定会出现线程安全问题:
多线程
共享变量(共享资源)
存在写操作(修改)
只读场景不需要同步。
示例不安全代码:
class Count { int num = 0 ; public void add () { num++; } }
三、线程同步的四大实现方案
方案 1:synchronized 内置同步锁(监视器锁)
前面详细讲过,简单回顾同步用法
同步实例方法:锁 this
同步静态方法:锁 Class 对象
同步代码块:自定义锁对象
private final Object lock = new Object ();public void add () { synchronized (lock) { num++; } }
特性:可重入、自动释放锁、异常自动解锁;非公平锁。
配套等待唤醒机制:wait() / notify() / notifyAll()
典型模型:生产者消费者。
方案 2:Lock 显式锁(AQS)
ReentrantLock
Lock lock = new ReentrantLock ();public void add () { lock.lock(); try { num++; }finally { lock.unlock(); } }
扩展能力:
公平锁 / 非公平锁
tryLock() 非阻塞尝试获取锁
lockInterruptibly() 支持中断等待
多个 Condition 条件变量,精准唤醒线程
方案 3:volatile(轻量级同步手段)
⚠️volatile ≠ 锁,不能实现完整同步!
作用:
✅保证可见性、禁止指令重排序
❌无法保证原子性(不能解决 num++)
适用场景:状态标记、DCL 单例。
方案 4:无锁同步:CAS 原子类(乐观同步)
java.util.concurrent.atomic AtomicInteger、AtomicLong
底层:CAS(CompareAndSwap)乐观锁,不阻塞线程。
AtomicInteger num = new AtomicInteger (0 );num.incrementAndGet();
优点:无锁、性能高;
问题:存在 ABA 问题 ,解决方案:AtomicStampedReference增加版本号。
四、同步两大模型:悲观锁 VS 乐观锁
悲观锁
默认一定会发生竞争,访问资源先上锁。
代表:synchronized、ReentrantLock
线程竞争失败阻塞。
乐观锁
默认竞争很少,不加锁;修改时校验数据是否被别人修改,冲突则重试。
代表:CAS 原子类、数据库版本号机制。
五、线程间同步通信(等待唤醒机制)
synchronized + wait/notify
synchronized (lock){ while (条件不满足){ lock.wait(); } lock.notifyAll(); }
关键点:
wait 释放锁 ;sleep 不释放锁
必须在同步代码块中调用
推荐 notifyAll,避免信号丢失
Lock + Condition
Condition condition = lock.newCondition();lock.lock(); try { while (条件不满足){ condition.await(); } condition.signalAll(); }finally { lock.unlock(); }
优势:可以创建多个 Condition,实现精准分组唤醒,粒度更细。
死锁
死锁四大必要条件(全部满足才会死锁)
互斥条件 :锁同一时间只能一个线程持有
请求并持有 :线程持有锁,同时申请其他锁
不可剥夺 :锁不能被其他线程强行抢走
循环等待 :线程互相持有对方需要的锁
解决思路:破坏任意一个条件
最常用:破坏循环等待 → 统一锁获取顺序
释放锁
✅ 一、会释放 synchronized 锁的 4 种情况
同步代码 / 同步方法正常执行完毕
线程执行走出 synchronized{}代码块,或者同步方法运行结束,自动释放监视器锁。
同步区域内遇到 break、return
提前退出同步代码块 / 方法,锁依旧释放。
同步区域抛出未捕获的 Exception / Error
异常导致线程终止,JVM 自动释放锁(这是 synchronized 优势,不会死锁)。
同步区域调用 wait()
线程立刻释放当前 monitor 锁,进入等待池;后续被notify()唤醒后,需要重新竞争锁才能继续执行。
❌ 二、不会释放 synchronized 锁的场景
调用 Thread.sleep() / Thread.yield() 线程暂停执行,但仍然持有锁,其他线程无法进入同步区。
sleep:休眠固定时长,抱着锁睡觉
yield:让出 CPU 时间片,回到就绪态,不释放锁
其他线程调用 suspend() 挂起当前线程
线程被暂停,但持续持有锁。
⚠️官方废弃 API!不推荐使用,容易引发死锁。
操作
是否释放 synchronized 锁
正常执行完同步代码
✅释放
return /break 退出同步区
✅释放
同步代码抛出异常
✅释放
wait()
✅释放
sleep() / yield()
❌不释放
notify()/notifyAll()
❌不释放
suspend()挂起线程
❌不释放
IO 流 一、IO 概述
IO:Input (输入) / Output (输出),以程序 (内存) 为参照物
输入流 :外部 → 内存(读数据)
输出流 :内存 → 外部(写数据)
外部数据源:文件、网络、内存数组、控制台等
Java IO 分为三大体系:
BIO 同步阻塞 IO (传统 java.io,重点学习)
NIO 同步非阻塞 IO (java.nio,通道、缓冲区)
AIO 异步非阻塞 IO(NIO.2)
二、BIO 四大核心抽象父类(java.io)
字节流(处理一切文件:视频、图片、音频、文档)
基类:InputStream(输入) / OutputStream(输出)
单位:byte 字节,8 位
常用实现类:
FileInputStream / FileOutputStream:文件原始字节流
BufferedInputStream / BufferedOutputStream:缓冲字节流(提升性能)
ByteArrayInputStream / ByteArrayOutputStream:内存数组流
ObjectInputStream / ObjectOutputStream:对象序列化流
字符流(仅处理文本文件 :txt、java 等)
基类:Reader(输入) / Writer(输出)
单位:char 字符,适配编码(UTF-8/GBK)
常用实现类:
FileReader / FileWriter:文件字符流(底层依然包装字节流)
BufferedReader / BufferedWriter:缓冲字符流,BufferedReader 独有readLine()
InputStreamReader / OutputStreamWriter:转换流(字节↔字符桥梁)
PrintWriter:打印流,自动换行、自动刷新
💡核心区分:
图片、视频、压缩包 → 字节流
纯文本 → 字符流(更方便)
三、流的分类方式
流向 :输入流、输出流
数据单位 :字节流、字符流
功能 :
节点流:直接对接数据源(FileInputStream、FileReader)
处理流(包装流):包裹其他流,增加功能(BufferedXX、ObjectXX)
装饰器设计模式:创建流时,层层包装。
示例:
FileInputStream fis = new FileInputStream ("a.txt" );BufferedInputStream bis = new BufferedInputStream (fis);
四、重要编码问题
FileReader/FileWriter默认使用系统编码,无法手动指定编码,极易乱码!
✅规范做法:使用转换流 指定编码
InputStreamReader isr = new InputStreamReader (new FileInputStream ("a.txt" ),"UTF-8" );
五、缓冲流原理
普通流:每次读写直接访问磁盘,频繁 IO,性能差
缓冲流:内置缓冲区数组
读取:一次性从磁盘读取一批数据存入缓冲区,程序优先读缓冲区;
写入:数据先存入缓冲区,缓冲区满 / 手动 flush ()/ 关闭流时,一次性写入磁盘。
flush():刷新缓冲区,强制把缓冲数据输出
close():内部会自动调用 flush ()
六、对象流 & 序列化
ObjectInputStream / ObjectOutputStream
作用:把 Java 对象写入文件 / 网络传输(序列化)
序列化要求
对象所属类必须实现接口:java.io.Serializable(标记接口,无方法)
transient修饰的成员变量不会参与序列化 (临时数据,不保存)
静态变量不属于对象,不会序列化
反序列化不需要调用构造方法创建对象。
七、IO 流标准操作步骤
创建流对象(可能抛出 FileNotFoundException)
读写操作(read/write,IOException)
关闭流!释放资源
流占用操作系统文件句柄,不关闭会资源泄漏。
资源关闭写法演进
原始:try-catch-finally 手动 close
推荐:try-with-resources(JDK1.7+)
AutoCloseable接口的流,代码执行完毕自动关闭,无需手动 finally
try (BufferedReader br = new BufferedReader (new FileReader ("a.txt" ))){ String line; while ((line = br.readLine()) != null ){ System.out.println(line); } }catch (IOException e){ e.printStackTrace(); }
八、常用方法汇总
字节流 InputStream
read():读取 1 字节,返回 int;末尾返回 - 1
read(byte[] buf):批量读取到字节数组,返回读取长度
字符流 Reader
read():读取单个字符
read(char[] buf)
BufferedReader:readLine() 读取一行,读到末尾返回 null
输出通用
write ():写入数据
flush ():刷新缓冲区
close ():关闭流
九、IO 常见坑
❌文件复制使用字符流:图片视频损坏
❌FileReader 不指定编码,跨平台乱码
❌忘记关闭流 / 忘记 flush,缓冲数据丢失
❌读取循环判断条件错误(没有判断返回 - 1/null,死循环)
❌序列化类新增属性,未定义serialVersionUID导致反序列化失败
十、BIO、NIO 简单区分
类型
模型
特点
BIO
同步阻塞
一连接一线程;代码简单,高并发性能差
NIO
同步非阻塞
通道 + 缓冲区;单线程轮询,多路复用
AIO
异步非阻塞
操作系统通知回调,适合高并发长连接
基础字节流 文件复制(普通 FileStream)
适用:任意文件(图片 / 视频 / 压缩包)
import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;public class FileStreamDemo { public static void main (String[] args) { String src = "test.jpg" ; String dest = "copy.jpg" ; try (FileInputStream fis = new FileInputStream (src); FileOutputStream fos = new FileOutputStream (dest)) { byte [] buffer = new byte [1024 ]; int len; while ((len = fis.read(buffer)) != -1 ) { fos.write(buffer, 0 , len); } System.out.println("文件复制完成" ); } catch (IOException e) { e.printStackTrace(); } } }
缓冲字节流复制(性能更高,开发常用)
import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;public class BufferedByteDemo { public static void main (String[] args) { try (BufferedInputStream bis = new BufferedInputStream (new FileInputStream ("test.mp4" )); BufferedOutputStream bos = new BufferedOutputStream (new FileOutputStream ("copy.mp4" ))) { byte [] buf = new byte [4096 ]; int len; while ((len = bis.read(buf)) != -1 ) { bos.write(buf, 0 , len); } System.out.println("缓冲流复制成功" ); } catch (IOException e) { e.printStackTrace(); } } }
字符流:读取文本文件 FileReader
⚠️缺陷:无法指定编码,容易乱码!只做学习演示
import java.io.FileReader;import java.io.IOException;public class FileReaderDemo { public static void main (String[] args) { try (FileReader fr = new FileReader ("demo.txt" )) { char [] chars = new char [1024 ]; int len; while ((len = fr.read(chars)) != -1 ) { System.out.print(new String (chars, 0 , len)); } } catch (IOException e) { e.printStackTrace(); } } }
缓冲字符流 BufferedReader(readLine 读取一行)
import java.io.BufferedReader;import java.io.FileReader;import java.io.IOException;public class BufferedReadDemo { public static void main (String[] args) { try (BufferedReader br = new BufferedReader (new FileReader ("demo.txt" ))) { String line; while ((line = br.readLine()) != null ) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } } }
转换流(重点!解决文本乱码,可以指定 UTF-8 编码)
InputStreamReader / OutputStreamWriter 字节→字符桥梁
✅项目标准读写文本写法
import java.io.BufferedReader;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStreamReader;public class ConvertStreamDemo { public static void main (String[] args) { try (BufferedReader br = new BufferedReader ( new InputStreamReader (new FileInputStream ("demo.txt" ), "UTF-8" ) )) { String line; while ((line = br.readLine()) != null ) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } } }
对象序列化 & 反序列化 ObjectStream Demo
第一步:实体类(实现 Serializable)
import java.io.Serializable;public class User implements Serializable { private static final long serialVersionUID = 1L ; private String name; private int age; private transient String tempData; public User (String name, int age, String tempData) { this .name = name; this .age = age; this .tempData = tempData; } @Override public String toString () { return "User{name='" + name + "', age=" + age + ", tempData='" + tempData + "'}" ; } }
第二步:序列化(对象写入文件)+ 反序列化(读取对象)
import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class ObjectIoDemo { public static void main (String[] args) { try (ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("user.dat" ))) { User user = new User ("张三" , 20 , "临时信息" ); oos.writeObject(user); System.out.println("对象序列化完成" ); } catch (IOException e) { e.printStackTrace(); } try (ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("user.dat" ))) { User user = (User) ois.readObject(); System.out.println("读取对象:" + user); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } }
PrintWriter 打印流(方便输出文本,自动换行刷新)
import java.io.FileOutputStream;import java.io.PrintWriter;public class PrintWriterDemo { public static void main (String[] args) { try (PrintWriter pw = new PrintWriter (new FileOutputStream ("out.txt" ))) { pw.println("第一行文字" ); pw.print("不换行内容" ); pw.println(666 ); } catch (Exception e) { e.printStackTrace(); } } }
Properties 完整知识点
一、概述
继承 Hashtable,属于 Map 集合 ,是线程安全的
键和值只能是 String 字符串
专门用来读取 properties 配置文件 (.properties)
文件格式:key=value,注释 #开头
区别普通 Map:
Map 可以存任意 Object;Properties 强制 key/value 为字符串,自带 IO 加载配置文件方法。
二、核心常用方法
存取方法(推荐,自带类型约束)
setProperty(String key, String value):存入键值对
getProperty(String key):根据 key 获取 value,找不到返回 null
getProperty(String key, String defaultValue):找不到 key 返回默认值
❌不推荐直接使用父类 put() / get():可以存入非字符串,运行时异常!
IO 加载配置文件(重点)
load(InputStream inStream):加载 properties 文件(字节流,推荐)
load(Reader reader):字符流加载
store(OutputStream out, String comments):把集合数据写出到配置文件
遍历
propertyNames() 返回所有 key 枚举
stringPropertyNames() 返回 Set(JDK1.6 + 推荐)
三、文件格式示例(db.properties)
username =root password =123456 url =jdbc:mysql://localhost:3306/test driver =com.mysql.cj.jdbc.Driver
四、完整 Demo
Demo1:加载配置文件(最常用)
把 db.properties 放在 项目根目录
import java.io.FileInputStream;import java.io.IOException;import java.util.Properties;public class PropertiesLoadDemo { public static void main (String[] args) { Properties prop = new Properties (); try (FileInputStream fis = new FileInputStream ("db.properties" )){ prop.load(fis); String username = prop.getProperty("username" ); String password = prop.getProperty("password" ); String test = prop.getProperty("noneKey" , "默认值" ); System.out.println(username); System.out.println(password); System.out.println(test); } catch (IOException e) { e.printStackTrace(); } } }
Demo2:存入数据 + 写出到配置文件
import java.io.FileOutputStream;import java.io.IOException;import java.util.Properties;public class PropertiesStoreDemo { public static void main (String[] args) { Properties prop = new Properties (); prop.setProperty("name" ,"张三" ); prop.setProperty("age" ,"20" ); prop.setProperty("gender" ,"男" ); try (FileOutputStream fos = new FileOutputStream ("user.properties" )){ prop.store(fos, "用户信息配置" ); }catch (IOException e){ e.printStackTrace(); } } }
Demo3:遍历 Properties
import java.util.Properties;import java.util.Set;public class PropertiesLoopDemo { public static void main (String[] args) { Properties prop = new Properties (); prop.setProperty("k1" ,"v1" ); prop.setProperty("k2" ,"v2" ); Set<String> keys = prop.stringPropertyNames(); for (String key : keys){ String value = prop.getProperty(key); System.out.println(key + " = " + value); } } }
⚠️高频注意事项
不要使用 put/get
prop.put("a" ,123 ); prop.getProperty("a" );
配置文件默认编码:ISO-8859-1
文件包含中文会乱码!
✅解决方案:
InputStreamReader isr = new InputStreamReader (new FileInputStream ("db.properties" ),"UTF-8" );prop.load(isr);
路径问题
new FileInputStream("xxx.properties"):相对项目根目录
如果放在 resources 资源文件夹(Maven 项目),使用类加载器读取(标准开发写法)
Properties prop = new Properties ();prop.load(Properties.class.getResourceAsStream("/db.properties" ));
properties 文件写法
key=value 也可以 key:value
# 注释,不能放在键值同一行右侧(部分版本识别异常)
等号两边不要随意加空格
网络编程 一、基础概念
网络通信三要素
IP 地址 :定位网络上一台主机
IPv4:4 字节;IPv6:16 字节
本机地址:127.0.0.1 / localhost
端口号 :定位主机上进程 ,范围 0~65535
0~1023:公认端口(80 http,21 ftp,22 ssh)
1024~65535:自定义端口
一个进程可以占用多个端口;一个端口同一时间只能被一个进程占用
通信协议 :数据传输规则
TCP :面向连接、可靠、有序、流式、三次握手四次挥手;不丢包;适合文件传输、聊天
UDP :无连接、不可靠、数据报、速度快;可能丢包;适合直播、语音通话
两个核心协议对比
特性
TCP
UDP
连接
面向连接(通信前建立连接)
无连接,直接发数据包
可靠性
可靠,重传机制、确认应答
不可靠,不确认、不重传
数据形式
字节流
数据包(Datagram)
流量 / 拥塞控制
有
无
适用场景
文件上传下载、网页、数据库
直播、视频通话、游戏
Java 网络包:java.net
二、核心类
InetAddress 代表 IP 地址
不能 new 对象,使用静态方法获取实例
InetAddress local = InetAddress.getLocalHost();InetAddress server = InetAddress.getByName("127.0.0.1" );String hostIp = server.getHostAddress(); String hostName = server.getHostName();
Socket 编程(BIO 阻塞通信,重点)
模型:客户端 Socket | 服务端 ServerSocket
ServerSocket :服务端,监听指定端口,等待客户端连接
Socket :代表一端通信套接字,连接建立后,两端都持有 Socket
Socket 底层封装 InputStream / OutputStream ,使用 IO 流收发数据
通信流程(TCP):
服务端创建ServerSocket(端口),调用accept() 阻塞等待客户端连接
客户端创建Socket(ip,端口)发起连接请求
服务端accept()收到连接,返回一个 Socket 对象,代表与当前客户端通道
两端通过 Socket 的输入输出流收发数据
通信完毕关闭流、关闭 Socket
⚠️ accept()、read() 都是阻塞方法 ,没有连接 / 没有数据时线程卡住。
TCP 代码 Demo
服务端 ServerSocket
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.net.ServerSocket;import java.net.Socket;public class TcpServer { public static void main (String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket (8888 ); System.out.println("服务端启动,监听8888端口..." ); Socket socket = serverSocket.accept(); System.out.println("客户端已连接:" + socket.getInetAddress()); BufferedReader br = new BufferedReader (new InputStreamReader (socket.getInputStream())); String msg = br.readLine(); System.out.println("收到客户端:" + msg); br.close(); socket.close(); serverSocket.close(); } }
客户端 Socket
import java.io.OutputStreamWriter;import java.io.PrintWriter;import java.net.Socket;public class TcpClient { public static void main (String[] args) throws Exception { Socket socket = new Socket ("127.0.0.1" , 8888 ); PrintWriter pw = new PrintWriter (new OutputStreamWriter (socket.getOutputStream())); pw.println("Hello 服务端!" ); pw.flush(); pw.close(); socket.close(); } }
UDP 编程:DatagramSocket + DatagramPacket
UDP 没有建立连接概念,数据包为单位传输
DatagramSocket:收发数据包的套接字
DatagramPacket:数据包(存放字节数据、目标地址端口)
UDP Demo
public class UDPSender { public static void main (String[] args) throws IOException { DatagramSocket socket = new DatagramSocket (); byte [] data = "udp消息" .getBytes(); DatagramPacket packet = new DatagramPacket ( data,data.length, InetAddress.getByName("127.0.0.1" ),9999 ); socket.send(packet); socket.close(); } } public class UDPReceiver { public static void main (String[] args) throws IOException { DatagramSocket socket = new DatagramSocket (9999 ); byte [] buf = new byte [1024 ]; DatagramPacket packet = new DatagramPacket (buf,buf.length); socket.receive(packet); String msg = new String (packet.getData(),0 ,packet.getLength()); System.out.println("收到:" +msg); socket.close(); } }
三、BIO 网络编程存在的问题
原生 BIO:一个客户端连接占用一个独立线程
NIO(同步非阻塞 java.nio)多路复用(Selector、Channel、Buffer)
框架:Netty(封装 NIO,企业主流)
区分:
BIO:同步阻塞 IO
NIO:同步非阻塞(JDK1.4+)
AIO:异步非阻塞(NIO.2 JDK1.7+)
四、重要常见问题
三次握手(TCP 建立连接)
客户端 → 服务端:SYN(请求连接)
服务端 → 客户端:SYN+ACK(同意连接,确认)
客户端 → 服务端:ACK(确认收到)
目的:协商收发能力,防止旧连接报文干扰
四次挥手(TCP 断开连接)
客户端发 FIN:我不再发送数据
服务端回复 ACK:收到关闭请求(服务端还可以继续发剩余数据)
服务端数据发完,发送 FIN:服务端也不再发送
客户端回复 ACK 确认断开
为什么挥手四次?TCP 是全双工,两个方向通道独立关闭。
Socket 关闭注意事项
shutdownOutput():半关闭 ,只关闭输出流,仍然可以接收对方数据
close ():同时关闭输入输出流
粘包问题
TCP 是字节流 ,无消息边界:
多条数据被操作系统合并发送,接收方无法区分第一条、第二条消息。
解决办法:
固定消息长度
特殊分隔符(换行)
消息头部携带数据长度 (Netty 常用方案)
❗UDP 不存在粘包,UDP 基于独立数据包
五、URL & URLConnection(访问网络资源)
java.net.URL 定位网络资源(http/https/file)
URLConnection 发起连接,获取网页流
URL url = new URL ("https://www.baidu.com" );URLConnection conn = url.openConnection();InputStream is = conn.getInputStream();
Java 反射 Reflection 一、什么是反射
反射机制:程序运行期间,可以获取类的完整信息、操作对象(构造方法、成员变量、成员方法)。
正常正向调用:new 对象() → 编译期确定类型
反射逆向操作:运行期才解析类信息 ,动态创建对象、调用方法。
包:java.lang.reflect.*;入口:Class类
二、反射核心作用
运行时获取类名、父类、接口、属性、方法、构造器
运行时动态创建类实例
运行时调用任意对象的方法、读写成员变量(包括私有 private)
主流框架底层原理:Spring IoC、MyBatis、注解、JUnit 都大量使用反射
三、获取 Class 对象的 4 种方式
class Student {}public class GetClassDemo { public static void main (String[] args) throws ClassNotFoundException { Class<Student> clazz1 = Student.class; Student s = new Student (); Class<? extends Student > clazz2 = s.getClass(); Class<?> clazz3 = Class.forName("Student" ); Class<?> clazz4 = ClassLoader.getSystemClassLoader().loadClass("Student" ); } }
一个类在 JVM 中只会加载一次,4 种方式拿到的是同一个 Class 对象 。
四、Class 五大核心组件
通过 Class 可以获取:
Constructor:构造方法
Method:成员方法
Field:成员变量
Annotation:注解
Superclass/Interface:父类、实现接口
五、全套代码 Demo
准备实体类
public class Student { private String name; public int age; public Student () {} public Student (String name, int age) { this .name = name; this .age = age; } private void privateTest () { System.out.println("私有方法执行" ); } public void show (String msg) { System.out.println("show方法:" + msg); } @Override public String toString () { return "Student{name='" + name + "', age=" + age + "}" ; } }
Demo1:反射通过构造器创建对象
import java.lang.reflect.Constructor;public class ReflectConstructorDemo { public static void main (String[] args) throws Exception { Class<Student> clazz = Student.class; Constructor<Student> con1 = clazz.getConstructor(); Student s1 = con1.newInstance(); System.out.println(s1); Constructor<Student> con2 = clazz.getConstructor(String.class, int .class); Student s2 = con2.newInstance("张三" ,18 ); System.out.println(s2); } }
⚠️getConstructor() 只能获取public 构造器
getDeclaredConstructor() 可以获取所有权限(private/public)构造器
Demo2:反射操作成员变量 Field(突破私有)
import java.lang.reflect.Field;public class ReflectFieldDemo { public static void main (String[] args) throws Exception { Class<Student> clazz = Student.class; Student student = clazz.getConstructor().newInstance(); Field ageField = clazz.getField("age" ); ageField.set(student, 20 ); System.out.println(ageField.get(student)); Field nameField = clazz.getDeclaredField("name" ); nameField.setAccessible(true ); nameField.set(student, "李四" ); System.out.println(student); } }
Demo3:反射调用方法 Method
import java.lang.reflect.Method;public class ReflectMethodDemo { public static void main (String[] args) throws Exception { Class<Student> clazz = Student.class; Student stu = clazz.getConstructor().newInstance(); Method showMethod = clazz.getMethod("show" , String.class); showMethod.invoke(stu, "反射调用show" ); Method privateMethod = clazz.getDeclaredMethod("privateTest" ); privateMethod.setAccessible(true ); privateMethod.invoke(stu); } }
六、关键 API 区分
方法
作用
范围
getConstructor / getMethod / getField
获取public
只能访问公共成员
getDeclaredConstructor / getDeclaredMethod / getDeclaredField
获取所有权限 (public/private/protected)
包含私有,但默认不能访问
setAccessible(true)
俗称:暴力反射;作用:取消 Java 访问权限检查 ,可以读写私有属性、调用私有方法。
安全管理器可以禁止暴力反射。
七、反射优缺点
✅优点:
动态性极高 ,框架实现核心;运行期灵活创建对象、调用方法
解耦,无需编译期硬编码 new 对象
❌缺点:
性能较差 :绕过编译优化,需要解析 Class 元数据
破坏封装:暴力反射可以操作私有成员,违背面向对象封装
代码可读性差、复杂;编译期无法校验,异常只能运行时抛出
八、常见面试问题
Q1:Class.forName () 作用?
加载类,执行静态代码块;
JDBC 经典用法:Class.forName("com.mysql.cj.jdbc.Driver"),驱动类自动注册。
Q2:newInstance () 过时了怎么办?
JDK9 之后 clazz.newInstance() 被废弃;推荐获取无参 Constructor 再调用 newInstance ()
Q3:反射和注解怎么配合?
反射可以读取类 / 方法上注解信息,很多框架(Spring)依靠【反射 + 注解】实现功能。
Q4:什么是暴力反射?
setAccessible(true),跳过权限校验访问私有成员。
九、反射典型应用场景
JDBC 加载数据库驱动
Spring:IOC 容器反射创建 Bean 对象、依赖注入
MyBatis:反射实例化实体类,封装数据库结果集
JUnit:反射执行 @Test 标记的测试方法
通用工具:对象属性拷贝
JDBC 全套知识点 + 标准 Demo 一、JDBC 概述
定义
JDBC(Java Database Connectivity) :Java 访问数据库的统一规范接口 ,定义了一套访问数据库的标准 API。
接口:java.sql.*、javax.sql.*,由 JDK 提供
实现:各大数据库厂商提供数据库驱动 (MySQL、Oracle、SQLServer 等),实现 JDBC 接口
核心优势
一套 Java 代码,更换数据库仅需更换驱动与连接参数,无需大量改业务代码
统一屏蔽不同数据库底层差异
核心组件
Driver 驱动 :数据库厂商实现,桥接 Java 与数据库
DriverManager :驱动管理器,注册驱动、获取数据库连接
Connection :数据库连接对象(会话)
Statement / PreparedStatement / CallableStatement :执行 SQL 语句
ResultSet :查询结果集,封装查询返回的表格数据
二、JDBC 完整执行 6 大固定步骤
导入数据库驱动依赖
注册数据库驱动
获取数据库连接 Connection
获取 SQL 执行对象 Statement/PreparedStatement
执行 SQL,处理结果集 ResultSet
关闭所有资源(ResultSet、Statement、Connection)
三、环境准备(MySQL 为例)
Maven 依赖(MySQL8.0+)
<!-- MySQL驱动 8. x --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0 .33 </version> </dependency>
MySQL5.x 驱动类:com.mysql.jdbc.Driver
MySQL8.x 驱动类:com.mysql.cj.jdbc.Driver
连接 URL 模板
jdbc :mysql://ip:端口/数据库名?参数 常用参数: useSSL =false 关闭SSL serverTimezone =Asia/Shanghai 时区(8.0必须加,否则时区报错) useUnicode =true&characterEncoding=utf8 指定编码
示例:
jdbc :mysql://127.0.0.1:3306/testdb?serverTimezone=Asia/Shanghai&useSSL=false&useUnicode=true&characterEncoding=utf8
四、原生 JDBC 代码 Demo
Demo1:Statement(不推荐,存在 SQL 注入)
import java.sql.*;public class JdbcStatementDemo { public static void main (String[] args) { Connection conn = null ; Statement stmt = null ; ResultSet rs = null ; try { Class.forName("com.mysql.cj.jdbc.Driver" ); String url = "jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=Asia/Shanghai" ; String user = "root" ; String pwd = "123456" ; conn = DriverManager.getConnection(url, user, pwd); stmt = conn.createStatement(); String sql = "select id,name,age from user" ; rs = stmt.executeQuery(sql); while (rs.next()) { int id = rs.getInt("id" ); String name = rs.getString("name" ); int age = rs.getInt("age" ); System.out.println(id + " " + name + " " + age); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (rs != null ) rs.close(); if (stmt != null ) stmt.close(); if (conn != null ) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
Statement 致命缺点:SQL 注入
拼接字符串 SQL,恶意输入可篡改 SQL 逻辑,项目禁止使用 Statement 。
Demo2:PreparedStatement(标准推荐,预编译防注入)
原理:SQL 模板预编译,使用?占位符,参数单独赋值,SQL 结构固定,杜绝注入。
import java.sql.*;public class JdbcPreparedDemo { public static void main (String[] args) { Connection conn = null ; PreparedStatement pstmt = null ; ResultSet rs = null ; try { Class.forName("com.mysql.cj.jdbc.Driver" ); String url = "jdbc:mysql://127.0.0.1:3306/testdb?serverTimezone=Asia/Shanghai" ; conn = DriverManager.getConnection(url, "root" , "123456" ); String sql = "select * from user where name = ? and age = ?" ; pstmt = conn.prepareStatement(sql); pstmt.setString(1 , "张三" ); pstmt.setInt(2 , 18 ); rs = pstmt.executeQuery(); while (rs.next()) { System.out.println(rs.getInt("id" ) + " " + rs.getString("name" )); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (rs != null ) rs.close(); if (pstmt != null ) pstmt.close(); if (conn != null ) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
Demo3:增删改(executeUpdate)
String insertSql = "insert into user(name,age) values(?,?)" ;pstmt = conn.prepareStatement(insertSql); pstmt.setString(1 , "李四" ); pstmt.setInt(2 , 22 ); int rows = pstmt.executeUpdate();System.out.println("影响行数:" +rows);
五、三大执行对象对比
对象
特点
安全性
适用场景
Statement
字符串拼接 SQL,每次重新编译
极易 SQL 注入
几乎淘汰
PreparedStatement
预编译,? 占位符,可复用 SQL
防注入,安全
日常 CRUD 首选
CallableStatement
专门调用存储过程
安全
数据库存储过程调用
六、核心 API 详解
Connection
createStatement():创建 Statement
prepareStatement(sql):创建预编译对象
setAutoCommit(false):关闭自动提交,开启事务
commit():事务提交
rollback():事务回滚
PreparedStatement 常用方法
setXxx(下标,值):给?赋值,下标从1 开始
executeQuery():执行查询,返回 ResultSet
executeUpdate():执行增删改,返回 int(影响行数)
ResultSet 结果集
next():指针下移,有数据返回 true,末尾 false
getXxx(列名/列下标):获取值,列下标从 1 开始
beforeFirst():指针回到最开头
七、JDBC 事务管理(核心考点)
JDBC 默认自动提交 :单条 SQL 执行完成立刻提交。
多 SQL 原子操作必须手动事务:
try { conn.setAutoCommit(false ); pstmt1.executeUpdate(); pstmt2.executeUpdate(); conn.commit(); }catch (Exception e){ conn.rollback(); }
八、工具类封装(开发常用,简化重复代码)
把连接、关闭资源抽取为工具类,配合 properties 配置文件
db.properties
driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql: user=root password=123456
JdbcUtil.java
import java.io.InputStream;import java.sql.*;import java.util.Properties;public class JdbcUtil { private static Properties prop = new Properties (); static { try (InputStream is = JdbcUtil.class.getResourceAsStream("/db.properties" )){ prop.load(is); Class.forName(prop.getProperty("driver" )); }catch (Exception e){ e.printStackTrace(); } } public static Connection getConnection () throws SQLException { return DriverManager.getConnection( prop.getProperty("url" ), prop.getProperty("user" ), prop.getProperty("password" ) ); } public static void close (Connection conn, PreparedStatement pstmt, ResultSet rs) { try { if (rs != null ) rs.close(); if (pstmt != null ) pstmt.close(); if (conn != null ) conn.close(); }catch (SQLException e){ e.printStackTrace(); } } public static void close (Connection conn, PreparedStatement pstmt) { close(conn,pstmt,null ); } }
九、常见问题与优化
SQL 注入原因与解决方案
原因:Statement 字符串拼接;解决:强制使用 PreparedStatement 占位符预编译。
原生 JDBC 痛点
频繁创建销毁连接,性能极差
代码冗余,大量重复模板代码
👉 解决方案:
数据库连接池(Druid、HikariCP)、MyBatis/MyBatis-Plus、JPA
驱动自动注册原理
JDBC4.0+ 无需手动Class.forName():
驱动包META-INF/services/java.sql.Driver文件记录驱动类,JVM 自动加载。
十、面试高频问答
PreparedStatement 为什么防注入?
SQL 结构提前预编译固化,? 仅做参数,不会解析为 SQL 关键字,恶意输入只会当成普通字符串。
占位符下标为什么从 1 开始?
JDBC 规范定义,和数据库列下标保持一致。
事务四大特性 ACID?
原子性、一致性、隔离性、持久性。
连接池作用?
复用数据库连接,减少创建销毁开销,控制最大连接数防止数据库崩溃。
Java 正则表达式 一、基础介绍
定义
正则表达式(Regex):一套字符串匹配规则 ,用来检索、替换、截取、校验文本。
Java 正则核心包:java.util.regex
核心两个类:
Pattern:正则表达式编译对象,封装匹配规则
Matcher:匹配器,执行匹配、查找、分组提取
使用场景
手机号 / 邮箱 / 身份证校验、日志提取、敏感词过滤、文本截取、字符串替换。
二、正则语法速查表
普通元字符(单字符匹配)
符号
含义
.
匹配任意单个字符 (换行\n除外)
\d
数字 [0-9]
\D
非数字
\s
空白字符(空格、制表符、换行)
\S
非空白字符
\w
单词字符 [a-z,A-Z,0-9,_]
\W
非单词字符
[]
字符集,匹配括号内任意一个字符,[abc]匹配 a/b/c
[^ ]
取反,[^0-9]匹配非数字
量词(限定前面字符出现次数)
符号
含义
?
0 次 或 1 次
+
至少 1 次(1~ 无数)
*
0 次及以上
{n}
固定 n 次
{n,}
最少 n 次
{n,m}
n~m 次
边界匹配
符号
含义
^
字符串开头
$
字符串结尾
\b
单词边界(空格、标点、首尾)
分组与或
():分组,可单独捕获内容、反向引用
|:或,a|b匹配 a 或者 b
(?:):非捕获分组,只分组不提取
转义说明(Java 独有重点)
正则里\是转义符,Java 字符串中\需要写成\\
例:正则\d → Java 代码必须写成"\\d"
贪婪 / 非贪婪
默认:贪婪匹配,尽可能多匹配;
?放在量词后开启非贪婪(懒惰匹配):.*?尽可能少匹配。
三、Java 三种常用使用方式
方式 1:String 自带简便方法(日常快速校验)
boolean matches(String regex):全串严格匹配 (等价于^正则$)
String replaceAll(String regex,String replacement):正则全局替换
String[] split(String regex):按正则分割字符串
Demo:字符串自带方法
public class StringRegexDemo { public static void main (String[] args) { String phoneReg = "^1[3-9]\\d{9}$" ; String phone = "13812345678" ; System.out.println(phone.matches(phoneReg)); String str = "a1b2c3d4" ; String res = str.replaceAll("\\d" , "" ); System.out.println(res); String s = "java,python;c++ go" ; String[] arr = s.split("[,; ]" ); for (String item : arr) { System.out.print(item + " " ); } } }
方式 2:Pattern + Matcher 精准查找、分组提取(最常用)
核心方法:
Pattern.compile(reg):编译正则
matcher.find():查找下一个匹配片段,找到返回 true
matcher.group():获取匹配到的完整内容
matcher.group(n):获取第 n 个分组内容(括号分组)
Demo1:提取所有数字
import java.util.regex.Matcher;import java.util.regex.Pattern;public class PatternMatcherDemo { public static void main (String[] args) { String content = "订单1001,金额99.9,编号20260802" ; String reg = "\\d+" ; Pattern pattern = Pattern.compile(reg); Matcher matcher = pattern.matcher(content); while (matcher.find()) { System.out.println("匹配:" + matcher.group()); } } }
Demo2:分组捕获(提取邮箱用户名、域名)
public class GroupDemo { public static void main (String[] args) { String email = "zhangsan@qq.com" ; String reg = "^(\\w+)@(.+)$" ; Pattern p = Pattern.compile(reg); Matcher m = p.matcher(email); if (m.matches()) { System.out.println("用户名:" + m.group(1 )); System.out.println("域名:" + m.group(2 )); } } }
方式 3:替换分组内容
public static void main (String[] args) { String phone = "13812345678" ; String res = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})" , "$1****$2" ); System.out.println(res); }
$1代表第 1 分组内容,$2代表第 2 分组内容
四、高频实用正则模板(可直接复制使用)
String phone = "^1[3-9]\\d{9}$" ;String email = "^\\w+@[a-zA-Z0-9]+(\\.[a-zA-Z]{2,})+$" ;String idCard = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$" ;String number = "^\\d+$" ;String pwd = "^(?=.*[0-9])(?=.*[a-zA-Z])[0-9a-zA-Z]{6,16}$" ;
五、贪婪匹配与非贪婪匹配案例
public static void main (String[] args) { String str = "<div>内容1</div><div>内容2</div>" ; Matcher m1 = Pattern.compile("<div>.*</div>" ).matcher(str); m1.find(); System.out.println(m1.group()); Matcher m2 = Pattern.compile("<div>.*?</div>" ).matcher(str); while (m2.find()){ System.out.println(m2.group()); } }
六、面试常见考点
matches () 和 find () 区别
matches():整串完全匹配 ,默认带^$;
find():片段查找,只要字符串存在匹配片段就返回 true。
正则性能问题
复杂正则易出现回溯爆炸 ,大量文本下卡顿;
能用普通字符串方法就不用复杂正则。
分组编号规则
从左到右,左括号(顺序依次为 1、2、3;group (0) 永远代表完整匹配内容。
非捕获分组(?:xxx)
只用来分组限定范围,不会占用分组编号,适合不需要提取、只做规则划分的场景。
Lambda表达式 Lambda表达式的基础语法:Java8中引入了一个新的操作符”->“ 该操作符称为箭头操作符或Lambda操作符
概念
Lambda 表达式是Java 8 新增 的语法,本质是简洁的匿名函数 ,用来简化函数式接口 的实现,替代臃肿的匿名内部类,把代码当成参数传递。
核心定位:行为参数化 ,把一段逻辑当做方法入参。
作用
简化匿名内部类写法,代码更精简;
配合流式编程Stream,实现集合便捷遍历、筛选、排序;
用于线程、回调、Comparator 比较器等场景。
语法格式
->:Lambda 运算符,分为左侧参数、右侧执行体
四种简写规则(重点)
参数只有 1 个 :小括号()可以省略
s -> System.out.println(s);
无参数 :必须保留空括号()
() -> System.out.println("无参" );
参数类型可省略 :编译器自动类型推断,无需声明参数类型
(a,b)->a+b; (Integer a,Integer b)->a+b;
方法体只有一行代码 :大括号{}、return、分号可全部省略
(a,b)->a+b; (a,b)->{ int sum = a+b; return sum*2 ; };
二、Lambda 使用前提:函数式接口
定义
函数式接口 :有且仅有一个抽象方法 的接口,可以有默认方法、静态方法、Object 重写方法。 Lambda 只能绑定函数式接口,用来实现那唯一的抽象方法。
注解标识
@FunctionalInterface:强制校验当前接口是否为函数式接口,编译报错提醒,推荐所有函数式接口都加。
@FunctionalInterface interface MyFunc { void test () ; default void hello () {} static void staticFunc () {} @Override String toString () ; }
Java 内置四大核心函数式接口(高频使用)
接口
抽象方法
作用
适用场景
Consumer<T>消费型
void accept(T t)
接收参数,无返回值
遍历、打印、修改对象
Supplier<T>供给型
T get()
无参数,返回对象
生成对象、随机数
Predicate<T>断定型
boolean test(T t)
入参,返回布尔值
集合筛选、条件判断
Function<T,R>函数型
R apply(T t)
T 入参,R 返回值
类型转换、数据处理
示例
Consumer<String> con = s->System.out.println(s); con.accept("测试" ); Supplier<Integer> sup = ()->new Random ().nextInt(10 ); Predicate<Integer> pre = n->n>5 ; Function<String,Integer> fun = str->Integer.parseInt(str);
三、Lambda 三大引用(方法引用、构造器引用、数组引用)
属于 Lambda 进一步简写,当 Lambda 只是调用已有方法时 使用,格式更简洁。
方法引用 3 种形式
对象::实例方法 :Lambda 调用某个对象的普通成员方法
Consumer<String> c = str->System.out.println(str); Consumer<String> c = System.out::println;
类::静态方法 :调用类静态方法
Comparator<Integer> com = (a,b)->Integer.compare(a,b); Comparator<Integer> com = Integer::compare;
类::实例方法 :参数 1 作为调用者,参数 2 作为方法入参
BiPredicate<String,String> bp = (s1,s2)->s1.equals(s2); BiPredicate<String,String> bp = String::equals;
构造器引用
格式:类名::new,适配 Supplier/Function
Supplier<User> sup = ()->new User (); Supplier<User> sup = User::new ; Function<String,User> fun = name->new User (name); Function<String,User> fun = User::new ;
数组引用
格式:类型[]::new
Function<Integer,String[]> fun = len->new String [len]; Function<Integer,String[]> fun = String[]::new ;
四、Lambda 变量作用域
可访问范围
静态变量、成员变量 :可读可修改;
局部变量 :隐式 final ,Java8 之后局部变量即使不加 final,也不允许二次修改;
int num = 10 ;Runnable run = ()->System.out.println(num);num = 20 ;
禁止使用
Lambda 内部不能定义与外部重名的局部变量 ;不允许使用this指向外部类以外对象,Lambda 没有独立 this。
五、Lambda 与匿名内部类区别
对比项
Lambda
匿名内部类
结构
只能绑定函数式接口
普通接口、抽象类都能用
this 指向
外层类对象
匿名内部类自身
变量
局部变量隐式 final
局部变量必须显式 final (JDK7)
编译
生成 invokedynamic 动态调用
生成独立 class 文件
可读性
极简,逻辑复杂可读性差
结构完整,复杂逻辑清晰
六、Lambda+Stream 流式编程(最常用场景)
Lambda 最大价值就是配合 Stream 处理集合,核心中间操作 + 终止操作
常用中间操作(返回 Stream,可链式调用)
filter (Predicate):过滤元素
map (Function):元素转换
sorted (Comparator):排序
distinct ():去重
limit (n):截取前 n 个
skip (n):跳过前 n 个
常用终止操作(关闭流,不可继续调用)
collect ():转为 List、Set、Map
forEach (Consumer):遍历
count ():统计数量
max/min:最值
anyMatch/allMatch/noneMatch:匹配判断
示例
List<Integer> list = Arrays.asList(1 ,2 ,3 ,4 ,5 ,6 ); List<Integer> res = list.stream() .filter(n->n%2 ==0 ) .map(n->n*n) .sorted() .collect(Collectors.toList());
七、Lambda 优缺点
优点
代码极度精简,消除模板代码;
便于流式编程,集合处理优雅;
延迟执行,性能在大批量集合更优。
缺点
调试困难,无单独类文件,堆栈不直观;
复杂逻辑可读性差,多层嵌套难以理解;
无法重载,仅支持单抽象方法接口;
递归实现繁琐。
八、高频易错点总结
无抽象方法、多个抽象方法的接口不能用 Lambda ;
多行方法体必须加大括号,有返回值必须写 return;
局部变量一旦被 Lambda 引用,就不可二次赋值;
Lambda 不能抛出受检异常,需要手动 try-catch;
不要滥用 Lambda,超长复杂逻辑优先普通方法。
Java Stream API Stream 是 Java8 引入的流式编程 ,用于集合、数组等数据源的遍历、过滤、映射、聚合、排序 等操作,核心思想:把集合转为流式流水线,链式调用,延迟执行 ,大幅简化循环代码,配合 Lambda、方法引用使用。
一、Stream 核心定位
用途 :对集合数据做复杂计算,替代传统 for/foreach 循环;
特点
单向一次性 :流只能遍历一次,消费完毕就关闭,不可重复使用;
惰性求值 :中间操作只记录逻辑,不执行,只有终止操作触发才真正计算 ;
不修改原数据源 :所有操作产生新流 / 新结果,原集合不变;
支持串行、并行流 :stream()串行,parallelStream()多线程并行。
和集合区别
集合:侧重存储数据 ;
Stream:侧重处理数据 。
二、Stream 三要素
数据源 :集合、数组、IO 流、生成器;
中间操作 :链式调用,返回新 Stream,惰性执行;
终止操作 :关闭流,执行计算,返回结果 /void。
三、1. 如何创建 Stream(数据源)
1)集合创建(最常用)
List<String> list = new ArrayList <>(); Stream<String> stream = list.stream(); Stream<String> parallelStream = list.parallelStream();
2)数组创建
int [] arr = {1 ,2 ,3 };IntStream stream = Arrays.stream(arr);Stream<Integer> stream2 = Stream.of(arr);
3)静态方法 Stream.of ()
Stream<Integer> stream = Stream.of(1 ,2 ,3 ,4 ,5 );
4)无限流(生成无限序列)
Stream.iterate(0 , n -> n+2 ).limit(10 ).forEach(System.out::println); Stream.generate(Math::random).limit(5 ).forEach(System.out::println);
5)其他数据源
文件行:Files.lines(Paths.get("xxx.txt")); 枚举:Enum.stream()。
四、2. 中间操作(Intermediate)
调用后返回 Stream,不执行计算 ,可链式拼接,常用分类:过滤、映射、排序、去重、截断、合并。
一.筛选与切片
方法
作用
filter(Predicate)
过滤,保留满足条件元素
limit(n)
截取前 n 个元素
skip(n)
跳过前 n 个元素
distinct()
去重(依赖 equals/hashCode)
示例:
List<Integer> nums = Arrays.asList(1 ,2 ,2 ,3 ,4 ,5 ,6 ); nums.stream() .filter(x -> x>2 ) .distinct() .skip(1 ) .limit(2 ) .forEach(System.out::println);
二.映射 map 系列(元素类型转换)
map(Function) :一对一转换,每个元素映射成一个新元素
Stream.of("java" ,"stream" ).map(String::length).forEach(System.out::println);
flatMap(Function) :一对多,把流中每个元素拆解为流,扁平化
解决:集合嵌套、拆分字符串、拆分数组
List<String> list = Arrays.asList("a,b" ,"c,d" ); list.stream() .flatMap(s -> Arrays.stream(s.split("," ))) .forEach(System.out::println);
基础类型映射:mapToInt/mapToLong/mapToDouble 作用:拆箱,避免频繁包装类,提升性能,对应IntStream/LongStream/DoubleStream。
排序 sorted
sorted():自然排序,元素实现 Comparable
sorted(Comparator):自定义比较器
Stream.of(3 ,1 ,2 ).sorted().forEach(System.out::println); Stream.of(3 ,1 ,2 ).sorted((a,b)->b-a).forEach(System.out::println);
peek 调试
peek(Consumer):遍历查看元素,不改变流,多用于打印日志调试:
Stream.of(1 ,2 ,3 ).peek(System.out::println).map(x->x*2 ).collect(Collectors.toList());
五、3. 终止操作(Terminal)
一旦调用,流关闭,触发所有中间操作执行,不可再使用流 。 分为:遍历、查找匹配、规约、收集、统计。
遍历 forEach /forEachOrdered
forEach:无序遍历,并行流乱序;
forEachOrdered:保证顺序遍历。
查找与匹配(返回布尔 / Optional)
方法
作用
allMatch(Predicate)
全部匹配返回 true
anyMatch(Predicate)
任意一个匹配返回 true
noneMatch(Predicate)
全都不匹配返回 true
findFirst()
获取第一个元素 Optional
findAny()
随机获取一个,并行流效率高
boolean all = Stream.of(1 ,2 ,3 ).allMatch(x->x>0 );Optional<Integer> first = Stream.of(1 ,2 ,3 ).findFirst();
规约 reduce(聚合计算,核心)
将流中元素反复结合,得到一个值,求和、最值、拼接。
单参数:reduce(BinaryOperator) 返回 Optional
Optional<Integer> sum = Stream.of(1 ,2 ,3 ).reduce((a,b)->a+b);
双参数:reduce(初始值, BinaryOperator) 返回具体类型
Integer sum = Stream.of(1 ,2 ,3 ).reduce(0 ,Integer::sum);
三参数:并行规约专用。
收集 collect(最常用,Collectors 工具类)
collect(Collector):把流转为集合、数组、字符串、分组、分区、统计。
(1)转为集合
List<Integer> list = stream.collect(Collectors.toList()); Set<Integer> set = stream.collect(Collectors.toSet()); ArrayList<Integer> arrayList = stream.collect(Collectors.toCollection(ArrayList::new ));
(2)转数组
Integer[] arr = stream.toArray(Integer[]::new );
(3)拼接字符串 joining
String str = Stream.of("a" ,"b" ,"c" ).collect(Collectors.joining("-" ,"[" ,"]" ));
(4)统计类 Collectors
IntSummaryStatistics stat = Stream.of(1 ,2 ,3 ).collect(Collectors.summarizingInt(x->x));stat.getMax(); stat.getMin(); stat.getSum(); stat.getAverage(); stat.getCount();
快捷方法:count()、max()、min()。
(5)分组 groupingBy
List<User> users = ...; Map<Integer,List<User>> group = users.stream().collect(Collectors.groupingBy(User::getAge)); Map<Integer,Map<String,List<User>>> group2 = users.stream() .collect(Collectors.groupingBy(User::getAge, Collectors.groupingBy(User::getGender)));
(6)分区 partitioningBy
按条件分为 true/false 两组
Map<Boolean,List<User>> part = users.stream() .collect(Collectors.partitioningBy(u->u.getAge()>18 ));
(7)聚合收集:求和、平均数、最值
Collectors.summingInt()、averagingInt()、maxBy()、minBy()
六、并行流 parallelStream
开启方式
stream.parallel() list.parallelStream()
底层基于 Fork/Join 框架 ,多线程拆分合并。
注意事项
并行不一定更快:数据量小、简单计算,线程开销 > 计算耗时;
并行无序,需要顺序用 forEachOrdered;
禁止在并行流操作非线程安全变量 (ArrayList、普通成员变量会并发异常);
有状态操作(limit、sorted、distinct)并行效率会大幅下降。
七、Optional 配合 Stream(空指针防护)
findFirst、findAny、reduce 都会返回 Optional,避免 NPE:
Integer num = Stream.of(1 ,2 ,3 ).filter(x->x>10 ).findFirst().orElse(0 );
八、常用实操经典案例
案例 1:去重、过滤、求和
List<Integer> list = Arrays.asList(1 ,1 ,2 ,3 ,4 ,5 ); int sum = list.stream().distinct().filter(x->x%2 ==0 ).reduce(0 ,Integer::sum);
案例 2:对象集合分组统计
class Student {String name;Integer age;}Map<Integer,Long> ageCount = studentList.stream() .collect(Collectors.groupingBy(Student::getAge, Collectors.counting()));
案例 3:扁平化集合
List<List<Integer>> nested = Arrays.asList(Arrays.asList(1 ,2 ),Arrays.asList(3 ,4 )); List<Integer> flat = nested.stream().flatMap(List::stream).collect(Collectors.toList());
九、高频易错点总结
流只能用一次 :中间操作后流消费完毕,重复调用抛 IllegalStateException;
惰性求值:中间操作写了异常逻辑,不调用终止操作不会报错;
map 与 flatMap 混淆:普通映射用 map,拆分层级用 flatMap;
并行流线程不安全:不要修改外部变量;
distinct 依赖 equals,自定义对象重写 equals+hashCode;
基础流(IntStream)无装箱开销,大数据优先使用。
十、Java8~Java11 Stream 新增小特性
Stream.ofNullable():支持 null 数据源,null 会生成空流,防空指针;
takeWhile(predicate):从头截取直到不满足条件;
dropWhile(predicate):从头丢弃直到不满足条件。
Lynx Frost
技术引领,智慧践行
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 RF极客日志 !