JAVA学习笔记
JAVA学习笔记
LynxFrostJava学习笔记
Java概述
Java是由Sun Microststems 公司于1995年5月推出的面向对象的程序设计语言,作者 詹姆斯•高斯林。
Java技术体系平台
- Java SE 标准版
- Java EE 企业版 主要学习这个
- 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 程序的唯一合法入口
- 原则:一个文件只写一个类
Java基础语法
命名规范
| 标识符类型 | 命名范式 | 示例 | 备注 |
|---|---|---|---|
| 包 (Package) | 全小写 | com.apple.ios |
建议使用反向域名 |
| 类 (Class) | 大驼峰 | StudentInfo |
名词 |
| 方法 (Method) | 小驼峰 | getScore() |
动词 |
| 变量 (Variable) | 小驼峰 | studentAge |
描述性名词 |
| 常量 (Constant) | 大写 + 下划线 | MAX_AGE |
static final 修饰 |
| 抽象类 | 大驼峰 | AbstractService |
通常以 Abstract 开头 |
| 异常类 | 大驼峰 | LoginException |
必须以 Exception 结尾 |
变量
变量是程序的基本组成单位
三要素 :类型+名称+值
变量必须先声明再使用
同一个作用域之内变量不能重名
数据类型
- 基本数据类型
- 数值型
- 整数类型,存放整数:
byte[1]、short[2]、int[4]、long[8] - 浮点 (小数) 类型:
float[4]、double[8]
- 整数类型,存放整数:
- 字符型:
char[2],存放单个字符'a' - 布尔型:
boolean[1],存放true、false
- 数值型
- 引用数据类型
- 类 (class)
- 接口 (interface)
- 数组 ([])
注意事项
- 整型常量默认为 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 |
+号的作用
- 当左右两边都是数值类型时,则做加法运算
- 当左右两边有一方为字符串,则做拼接运算
- 运算顺序,从左到右
Java 转义字符
| 转义字符 | 含义 |
|---|---|
\' |
单引号 ' |
\" |
双引号 " |
\\ |
反斜杠 \ |
\n |
换行 |
\r |
回车 |
\t |
Tab 制表符(缩进) |
\b |
退格 |
\f |
换页(很少用) |
特殊转义字符\r
功能:将光标移动到当前行的开头
不同的操作系统对“换行”的定义曾各不相同:
- Windows: 使用
\r\n(回车 + 换行)代表一行结束。 - Unix / Linux / macOS: 统一使用
\n(换行)代表一行结束。
\r 有一个非常酷的用途:制作简单的命令行进度条。
for (int i = 0; i <= 100; i++) { |
注释
- 单行注释 //
- 多行注释 / /
- 文档注释 /* /
被注释的文字,不会被JVM解释执行
代码规范
- 类、方法的注释,要以Javadoc的方式来写
- 非Java Doc 的注释,往往是给代码维护者看的
- 使用Tab进行右移,shift+tab 进行左移
- 运算符和=两边习惯性各加一个空格
- 源文件使用utf-8编码
- 行宽度不要超过80个字符
- 代码编写推荐行尾风格
修饰符
public:公开的。无论是哪个类、哪个包、还是外部项目,都可以直接访问。protected:受保护的。同一个包内的类可以访问;不同包中的子类也可以访问。- 默认(无修饰符):包级私有(package-private)。只有同一个包内的类可以访问,跨包了就不能访问。
private:私有的。只有在定义它的类内部才能访问,体现了面向对象的封装性。通常属性会设为private,然后通过get/set方法来间接访问。
运算符
算术运算符
示例
public class Test { |
结果
a + b = 30 |
注意事项
System.out.println(10 / 4); //从数学来看是 2.5, java 中 2 |
int total = 4; |
原因: score / total 的计算先发生,因为两个都是 int,所以先算出了 2。
赋值: 算出 2 之后,才把这个整数赋值给 double result。这时 2 变成了 2.0。
在 % 的本质 看一个公式a % b = a - a / b * b
由公式可以知道:在 Java 的取模运算中,结果的符号只取决于左操作数(被除数 $a$)的符号,而与右操作数(除数 $b$)无关。
面试题
int i = 1; |
答案是 1
过程 temp = i i=i+1 i=temp
int i = 1; |
答案是 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$。
关系运算符
示例
public class Test { |
结果
a == b = false |
关系运算符的结果都是boolean类型,也是就是要么true要么false
instanceof 判断对象是不是某个类 / 子类 / 接口的实例,返回布尔值 true / false 判断运行类型
对象 instanceof 类型 |
Object obj = new String("hello"); |
位运算符
示例
public class Test { |
结果
a & b = 12 |
逻辑运算符
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,第二个条件都要判断,效率低
赋值运算符
示例
public class Test { |
结果
c = a + b = 30 |
赋值运算符特点
1) 运算顺序从右往左
2) 赋值运算符的左边 只能是变量,右边 可以是变量、表达式、常量值
3) 复合赋值运算符等价于下面的效果比如:a+=3;等价于 a=a+3;
4) 复合赋值运算符会进行类型转换。
byte b = 3; |
三元运算符
条件表达式 ? 表达式 1: 表达式 2;
运算规则:
- 如果条件表达式为 true,运算后的结果是表达式 1;
- 如果条件表达式为 false,运算后的结果是表达式 2;
注意事项
1) 表达式 1 和表达式 2 要为可以赋给接收变量的类型(或可以自动转换)
2) 三元运算符可以转成 if—else 语句
运算优先级
| 优先级 | 运算符 | 类别 | 结合性 | ||
|---|---|---|---|---|---|
| 1(最高) | expr++ expr-- |
后置自增、后置自减 | 从左→右 | ||
| 2 | ++expr --expr +expr(正) -expr(负) ~ ! (类型) |
前置单目(自增自减、正负、取反、强制类型转换) | 从右→左 | ||
| 3 | * / % |
乘、除、取模 | 从左→右 | ||
| 4 | + - |
加、减(含字符串拼接) | 从左→右 | ||
| 5 | << >> >>> |
移位运算符 | 从左→右 | ||
| 6 | < <= > >= instanceof |
关系比较、类型判断 | 从左→右 | ||
| 7 | == != |
相等、不相等 | 从左→右 | ||
| 8 | & |
按位与 / 非短路逻辑与 | 从左→右 | ||
| 9 | ^ |
按位异或 | 从左→右 | ||
| 10 | ` | ` | 按位或 / 非短路逻辑或 | 从左→右 | |
| 11 | && |
短路逻辑与 | 从左→右 | ||
| 12 | ` | ` | 短路逻辑或 | 从左→右 | |
| 13 | ? : |
三元条件运算符 | 从右→左 | ||
| 14(最低) | = += -= *= /= %= &= ^= ` |
=`<<= >>= >>>= |
全部赋值复合赋值 | 从右→左 |
补充知识点,原码反码补码
原码 (Sign-Magnitude):最高位为符号位(0正1负),其余位是数值。
- 直观但难算,0有两套写法。
反码 (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 表示数组的大小/长度
数据类型 数组名[]; 也可以 数据类型[] 数组名;
注意事项
数组是多个相同类型数据的组合,实现对这些数据的统一管理
数组中的元素可以是任何数据类型,包括基本类型和引用类型,但是不能混用。
数组创建后,如果没有赋值,有默认值
| 类型 | 默认值 |
| ———- | ——— |
| byte | 0 |
| short | 0 |
| int | 0 |
| long | 0 |
| float | 0.0 |
| double | 0.0 |
| char | \u0000 |
| boolean | false |
| String | null |使用数组的步骤
声明数组并开辟空间
给数组各个元素赋值
- 使用数组
数组的下标是从 0 开始的。
数组下标必须在指定范围内使用,否则报:下标越界异常,
数组属引用类型,数组型数据是对象(object)
int a = 1; |
Arrays类
一.概述
java.util.Arrays:数组工具类
- 包:
java.util,使用需要导包 - 所有方法都是 static 静态方法,直接
Arrays.方法名()调用 - 构造方法私有,不能 new Arrays 对象
- 作用:提供数组打印、排序、查找、复制、填充、比较等工具方法
import java.util.Arrays; |
二、常用核心方法
- toString () 打印数组
普通数组直接打印输出地址,Arrays.toString 打印数组内容
int[] arr = {1,3,2}; |
注意:只支持一维数组
多维数组使用:
deepToString()
int[][] arr2 = {{1,2},{3,4}}; |
- sort () 数组排序
① 基本类型数组:升序排序(快速排序)
int[] arr = {5,2,9,1}; |
② 对象数组 / 自定义排序(引用类型)
Arrays.sort(数组, Comparator) 支持自定义顺序(只能用于包装类型数组,不能用 int [],要用 Integer [])
Integer[] arr = {5,2,9,1}; |
重点:基本类型 sort 不支持直接传入比较器!
拓展:可以指定区间排序
Arrays.sort(arr,from,to)左闭右开
- binarySearch () 二分查找
前提条件:数组必须先升序排序!否则结果不可预测
- 找到:返回元素索引
- 找不到:返回
-(插入点)-1
int[] arr = {1,2,5,9}; |
- copyOf () /copyOfRange () 数组复制
copyOf (数组,新长度) |
从头复制,可扩容 / 缩容
int[] old = {1,2,3}; |
copyOfRange (数组,起始索引,结束索引) |
区间复制,左闭右开 [start,end)
int[] arr = {10,20,30,40}; |
区别:数组自身 clone () 和 Arrays.copyOf 效果类似;copyOf 底层调用 System.arraycopy。
- fill () 数组填充
给数组全部 / 指定区间统一赋值
int[] arr = new int[5]; |
- equals () 判断两个数组内容是否相同
int[] a = {1,2,3}; |
误区:
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}; |
重要坑:
- asList 接收引用类型数组;不能直接传基本类型数组
int[] - 返回的 List 是 Arrays 内部静态类,固定大小!不能 add/remove,会抛出异常
- 集合和原数组互相引用,修改数组元素,集合同步变化
常用类
包装类
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 = new Integer(10); |
- 自动装箱、自动拆箱(JDK5 + 新特性,编译器自动转换)
//自动装箱 |
底层本质:自动装箱底层调用valueOf();自动拆箱调用xxxValue()。
Integer 缓存机制
Integer.valueOf(int i) 存在缓存数组 cache[]
- 默认缓存范围:-128 ~ 127
- 范围内数字:直接复用缓存对象,
==结果 true - 超出范围:新建
Integer对象,==结果 false
Integer a = 100; |
重要提醒:
==比较:两边如果是包装对象 → 比较地址
一边包装、一边基本类型 → 触发自动拆箱,比较数值
开发比较数值一律使用
.equals(),不要直接==
拓展缓存范围
只有 Byte、Short、Integer、Long 缓存区间固定[-128,127]
Character缓存 0~127
Float、Double没有缓存!
字符串 和 基本类型互相转换
- 基本类型 → String
方式 1:变量 + ""
int num = 99; |
方式 2:String.valueOf()(推荐)
String str = String.valueOf(num); |
- String → 基本类型
使用包装类的静态方法 parseXxx()
String s = "123"; |
注意:字符串格式不对,抛出 NumberFormatException 数字格式异常。
包装类与基本类型区别汇总
- 默认值不同
- 基本类型:有默认值(int=0,boolean=false)
- 包装类:默认值
null
坑:包装类变量为 null 时,执行自动拆箱 → NullPointerException 空指针异常
Integer i = null; |
- 存储位置
- 基本类型:局部变量存在栈
- 包装类对象:对象本体在堆
- 使用场景
- 集合
List/Map不能存放基本类型,只能存包装类
List<Integer> list = new ArrayList<>(); //正确 |
String 类
一、String 基本介绍
java.lang.String,代表字符串。- 字符串本质:字符序列,底层:
- JDK8:
private final char[] value - JDK9+:
private final byte[] value(优化存储)
- JDK8:
- 重点:String 对象不可变(immutable)
value数组被final修饰,数组引用不能指向新数组- 一旦字符串创建,内容无法修改
误区:
String s = "abc"; |
不是修改原来对象!只是让s引用指向新字符串对象,旧对象等待 GC 回收。
二、字符串创建两种方式
方式 1:直接赋值(字面量)
String str1 = "Java"; |
先去字符串常量池(堆内存)查找 “Java”
- 找到:直接复用常量池中的对象
- 没找到:在常量池中创建,引用指向常量池对象
方式 2:new String ()
String str2 = new String("Java"); |
- 先检查常量池有没有
"Java",没有则放入常量池; - 在堆中创建新 String 对象,引用指向堆对象。
经典面试对比
String s1 = "abc"; |
三、字符串拼接规则
规则 1:编译期常量优化
两个字面量直接相加,编译器自动合并
String s1 = "a"+"b"+"c"; |
规则 2:存在变量参与拼接 → 在堆创建新对象
String a = "a"; |
intern () 方法
str.intern():主动把字符串放入常量池
如果常量池已经存在相同内容,返回常量池引用;不存在则将对象放入常量池并返回池内引用。
四、== 和 equals ()
==- 基本类型:比较数值
- String 引用:比较对象地址
equals()String 重写了 Object 的 equals,比较字符串内容
开发规范:判断字符串内容相等一律使用
equals()
String s1 = new String("hello"); |
五、常用 API 方法
String str = "HelloJava"; |
六、不可变性带来的优缺点
优点:
- 字符串常量池实现复用,节省内存
- 哈希值只需要计算一次,适合作为 HashMap 的 key
- 线程安全,多线程并发访问无问题
缺点:
频繁字符串拼接,不断产生新对象,产生大量垃圾。
大量拼接解决方案:StringBuilder / StringBuffer
Stringbuffer类
一、基础概念
java.lang.StringBuffer,可变字符序列
底层:char[](JDK9 之后byte[]),数组没有被final修饰,可以直接扩容修改内容,不会频繁创建新对象。
对比:
String:不可变,每次拼接产生新对象,大量拼接效率极低StringBuffer:可变容器,原地修改字符数组
二、构造方法
// 空参,默认容量 16 |
默认容量 16:预留空间,减少频繁扩容。
三、核心常用方法(全部支持链式调用)
StringBuffer sb = new StringBuffer(); |
四、扩容机制(重点)
- 当
append后字符超出容量,触发扩容 - 扩容公式:新容量 = 原有容量 * 2 + 2
- 如果扩容后依然不够,则直接使用所需最小容量
- 底层:新建更大数组,复制旧数组内容
示例:初始容量 16 → 扩容后 16*2+2=34
五、StringBuffer 和 String 的互相转换
// String → StringBuffer |
六、高频易错知识点
- append 返回的是自身 this 对象,所以支持链式调用
sb.append("a").append("b").append("c"); |
toString()会新建 String 对象,不会复用底层数组- 不要频繁 new StringBuffer,预估长度尽量指定初始容量,避免多次扩容
- 线程不安全场景误用 StringBuffer 不能带来收益,反而降低速度;单线程优先
StringBuilder
七、经典误区
错误理解:
“StringBuffer 比 String 快”
准确说法:
少量拼接差距不大;大量循环拼接时,String 不会产生大量临时对象,性能差距巨大。
StringBuilder 类
一、基础介绍
java.lang.StringBuilder |
可变字符序列,用于字符串拼接,JDK1.5 推出
底层:字符数组(JDK8 char [] / JDK9 byte []),数组无 final 修饰,可以直接修改,不会频繁创建新字符串对象。
和 StringBuffer API 几乎完全一致,唯一区别:没有 synchronized 同步锁。
二、构造方法
// 1.无参构造:初始容量 16 |
三、核心常用方法(支持链式调用)
StringBuilder sb = new StringBuilder(); |
原理:所有修改操作直接操作底层数组,不产生新对象。
四、扩容机制
触发时机:追加内容后,字符总数超过当前容量
默认扩容公式:
新容量 = 原有容量 * 2 + 2
如果扩容后的容量依旧不够存放,直接采用所需最小容量。
示例:
初始容量 16 → 扩容:16×2+2 = 34
优化建议:如果你提前预估字符串长度,创建时指定容量,避免多次扩容、数组复制,提升性能。
五、String / StringBuilder / StringBuffer 三者终极对比
| 类 | 可变性 | 线程安全 | 锁 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| String | 不可变 | 线程安全 | 无 | 低 | 少量拼接、字符串常量 |
| StringBuffer | 可变 | 线程安全 | 方法加synchronized |
较低 | 多线程并发拼接 |
| StringBuilder | 可变 | 线程不安全 | 无锁 | 最高 | 单线程拼接(开发首选) |
开发准则:
绝大多数业务代码(单线程) → 优先 StringBuilder
六、相互转换
// String → StringBuilder |
七、高频考点 & 易错点
线程不安全典型场景
多个线程同时操作同一个 StringBuilder 对象,会出现数据错乱、数组越界异常。
解决方案:多线程改用 StringBuffer,或者每个线程新建独立 StringBuilder。
- 循环拼接大坑
// 性能极差!每次循环产生大量中间String对象 |
- 编译器优化误区
String s = "a"+"b"+"c"; |
Math 类
一、基础介绍
java.lang.Math |
- 提供大量数学计算静态方法,全部是
static,不需要 new 对象,直接类名。方法调用 - 构造方法被
private修饰,无法实例化 - 包含两个常用静态常量
Math.PI //圆周率 π ≈ 3.1415926535... |
调用方式示例:
double num = Math.abs(-10); |
二、常用方法分类
- 绝对值 abs ()
Math.abs(-5); //int 绝对值 → 5 |
- 取最值 max /min
Math.max(10,20); //返回较大值 20 |
- 四舍五入、向上取整、向下取整
//1.四舍五入 → long |
极易踩坑:负数的 ceil、floor 不要凭直觉判断!
- 幂运算、开平方
Math.pow(2,3); //2^3 = 8.0 参数、返回值都是double |
- 随机数 random ()
Math.random(); |
业务常用公式:获取 [a,b] 的随机整数
// 范围 [min,max] |
拓展:JDK 推荐也可以使用
java.util.Random类,功能更强。
- 三角函数(参数是弧度,不是角度)
Math.sin(弧度); |
三、Math vs StrictMath(了解)
Math:部分方法平台相关,底层可以使用 CPU 优化,计算结果微小误差
StrictMath:严格遵循浮点运算标准,所有平台计算结果完全一致;速度略慢。
日常开发几乎只用 Math。
四、高频易错点
round()返回 long;ceil()/floor()返回 double
double d = Math.ceil(2.2); //d=3.0 是小数类型,不是int |
- Math.random () 左闭右开
[0.0,1.0) - pow 返回 double,即使整数次方,不能直接当 int 使用,需要强转
- 三角函数传入的是弧度,直接传入角度结果错误
Arrays 类
一、概述
java.util.Arrays:数组工具类
- 包:
java.util,使用需要导包 - 所有方法都是 static 静态方法,直接
Arrays.方法名()调用 - 构造方法私有,不能 new Arrays 对象
- 作用:提供数组打印、排序、查找、复制、填充、比较等工具方法
import java.util.Arrays; |
二、常用核心方法
- toString () 打印数组
普通数组直接打印输出地址,Arrays.toString 打印数组内容
int[] arr = {1,3,2}; |
注意:只支持一维数组
多维数组使用:
deepToString()
int[][] arr2 = {{1,2},{3,4}}; |
- sort () 数组排序
① 基本类型数组:升序排序(快速排序)
int[] arr = {5,2,9,1}; |
② 对象数组 / 自定义排序(引用类型)
Arrays.sort(数组, Comparator) 支持自定义顺序(只能用于包装类型数组,不能用 int [],要用 Integer [])
Integer[] arr = {5,2,9,1}; |
重点:基本类型 sort 不支持直接传入比较器!
拓展:可以指定区间排序
Arrays.sort(arr,from,to)左闭右开
- binarySearch () 二分查找
前提条件:数组必须先升序排序!否则结果不可预测
- 找到:返回元素索引
- 找不到:返回
-(插入点)-1
int[] arr = {1,2,5,9}; |
- copyOf () /copyOfRange () 数组复制
copyOf (数组,新长度) |
从头复制,可扩容 / 缩容
int[] old = {1,2,3}; |
copyOfRange (数组,起始索引,结束索引)
区间复制,左闭右开 [start,end)
int[] arr = {10,20,30,40}; |
区别:
数组自身 clone () 和 Arrays.copyOf 效果类似;copyOf 底层调用 System.arraycopy。
- fill () 数组填充
给数组全部 / 指定区间统一赋值
int[] arr = new int[5]; |
- equals () 判断两个数组内容是否相同
int[] a = {1,2,3}; |
误区:
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}; |
重要坑:
- asList 接收引用类型数组;不能直接传基本类型数组
int[] - 返回的 List 是 Arrays 内部静态类,固定大小!不能 add/remove,会抛出异常
- 集合和原数组互相引用,修改数组元素,集合同步变化
System 类
一、概述
java.lang.System |
- 全部成员都是静态,直接
System.xxx调用,无需实例化 - 构造方法
private,禁止new System() - 代表程序运行平台系统资源,提供标准输入输出、时间、数组复制、退出程序、环境变量等操作
二、三大静态常量(流对象)
// 标准输入流:控制台键盘输入 |
区分 out 和 err:
- out:普通信息输出,缓冲区缓存,顺序不一定立刻打印
- err:错误信息,立即输出,控制台一般红色
三、核心常用方法
- arraycopy () 数组复制,底层非常重要
public static void arraycopy( |
示例:
int[] a = {10,20,30,40,50}; |
特点:
native 本地方法,由 C 实现,速度极快
Arrays.copyOf/copyOfRange 底层本质调用
System.arraycopy注意:自己复制自己、索引越界会抛出
ArrayIndexOutOfBoundsException
- 获取时间
// 当前时间毫秒数:从【1970-01-01 00:00:00 UTC】到现在的毫秒 |
使用场景:计算程序运行耗时
long start = System.currentTimeMillis(); |
区别:
currentTimeMillis:基于系统时钟,可以手动修改系统时间,数值会跳变
nanoTime:基于虚拟机计时器,适合计算时间间隔,不适合获取日期
- exit (状态码) 终止 JVM
System.exit(0); //正常退出 |
重点考点:
执行 System.exit() 直接关闭虚拟机,finally 代码块不会执行!
- gc () 建议垃圾回收
System.gc(); |
作用:向 JVM建议执行垃圾回收;
只是建议,不保证立刻执行 GC,什么时候回收由虚拟机决定。
- 获取环境变量、系统属性
// 获取单个系统属性 |
常用属性:
user.name 当前用户名
java.version Java 版本
四、输入输出重定向(了解)
//把输出写入文件,不再打印控制台 |
五、易错知识点
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(超大整数)
- 创建对象
不能直接使用 + - * / 运算符!只能调用方法
//方式1:字符串构造【推荐】 |
- 常用运算方法
BigInteger a = new BigInteger("20"); |
重点:所有运算返回新 BigInteger 对象,原对象不可变!
- 带余数除法(divideAndRemainder)
BigInteger[] res = a.divideAndRemainder(b); |
三、BigDecimal(高精度小数 )
- 对象创建
// 不推荐!double构造依然存在精度损失 |
- 四则运算
同样不可变,运算返回新对象
BigDecimal a = new BigDecimal("0.1"); |
- 除法 & 保留小数、舍入模式(核心)
除法必须指定保留位数 + 舍入模式
BigDecimal a = new BigDecimal("10"); |
常用舍入模式:
ROUND_HALF_UP:四舍五入(日常最常用)ROUND_UP:向上取整ROUND_DOWN:直接截断,舍弃后面小数ROUND_HALF_EVEN:银行家舍入(JDK 默认)
- 比较大小
禁止使用 ==(比较地址)
尽量不要用 equals ()!equals 会比较小数位数
BigDecimal d1 = new BigDecimal("2.0"); |
正确方式:compareTo()
// 返回值: |
- 小数点操作
bd.setScale(2,BigDecimal.ROUND_HALF_UP); //保留2位小数 |
四、BigInteger vs BigDecimal 对比
| 类 | 适用场景 | 存储内容 |
|---|---|---|
| BigInteger | 超大整数运算 | 整数,无小数 |
| BigDecimal | 金融金额、高精度小数 | 支持小数,解决浮点精度丢失 |
共性:
- 不可变类,每次计算产生新对象
- 不支持运算符重载,必须调用方法
- 在
java.math包
五、高频易错知识点
double 直接传入 BigDecimal 构造器会精度丢失,优先字符串构造
- BigDecimal 除法如果无法整除,必须指定精度和舍入模式,否则抛异常
- equals 会比较小数位数;数值比较用
compareTo - 对象不可变,运算结果需要接收返回值
//错误写法 |
- double/float 不要用于金额计算,强制使用 BigDecimal
六、开发规范(金融项目)
- 数据库金额:decimal 类型
- Java 代码:使用 BigDecimal 存储金额
- 运算统一指定保留小数位数与四舍五入
日期时间类
分为两大体系:
旧版(JDK1.0 ~ JDK7):
Date、SimpleDateFormat、Calendar缺点:线程不安全、API 设计差、可变性
新版(JDK8+ 推荐)java.time 包:
LocalDate、LocalTime、LocalDateTime、DateTimeFormatter、Instant、Duration、Period优点:不可变、线程安全、设计优雅,项目优先使用
一、旧 API(了解,尽量不推荐新项目使用)
- java.util.Date
代表瞬时时间点(毫秒时间戳)
// 当前系统时间 |
缺点:
Date对象可变;setTime 修改原有对象无法直接获取年、月、日;月份从 0 开始(0=1 月,大坑)
SimpleDateFormat 日期格式化
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
致命坑:SimpleDateFormat 线程不安全!多线程共用会异常。
常用格式符号:
yyyy4 位年份MM月份 (01~12)dd日期HH24 小时制;hh12 小时制mm分钟;ss秒;SSS毫秒
- Calendar 日历类(JDK1.1 替代 Date)
抽象类,常用实现 GregorianCalendar
Calendar cal = Calendar.getInstance(); |
缺点:依旧可变,API 别扭。
二、JDK8 全新时间包 java.time
所有对象不可变,修改操作返回新对象;线程安全!
- 核心类区分
| 类 | 作用 |
|---|---|
Instant |
时间戳(UTC 标准时间,毫秒 / 纳秒),对应旧 Date |
LocalDate |
只有年月日 2026-07-28(无时分秒、无时区) |
LocalTime |
只有时分秒 14:30:20 |
LocalDateTime |
年月日 + 时分秒,无时区 |
ZonedDateTime |
带时区的完整时间(需要时区运算使用) |
DateTimeFormatter |
格式化类(替代 SimpleDateFormat,线程安全) |
Duration |
时间间隔:时分秒(秒、毫秒级别,两个时间点) |
Period |
日期间隔:年、月、日(两个 LocalDate) |
- 创建对象
// 获取当前时间 |
- 常用运算(不可变!操作返回新对象)
LocalDateTime time = LocalDateTime.now(); |
- 格式化与解析 DateTimeFormatter 线程安全
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
- 时间间隔计算 Duration / Period
// 时分秒间隔(LocalDateTime / Instant) |
- 新旧 API 互相转换(项目兼容必备)
// LocalDateTime ↔ Date |
- 时区简单说明
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 内存的结构分析
- 栈:一般存放基本数据类型(局部变量)
- 堆:存放对象(Cat cat , 数组等)
- 方法区:常量池(常量,比如字符串),类加载信息
Person p = new Person(); |
- 先加载Person类信息(属性和方法信息,只会加载一次)
- 再堆中分配空间,进行默认初始化
- 把地址赋给p ,p就指向对象
- 进行指定初始化,比如 p.name=”jack” p.age = 10
包
作用
- 区分相同名字的类
- 方便管理类
- 控制访问范围
基本语法
package cn.itrf |
package 关键词,表示打包
cn.itrf 表示包名
注意事项
- package的作用是声明当前类所在的包,需要放在类的最上面,一个类中最多只有一句package
- import指令 位置放在package的下面,在类定义前面,可以有多句且没有顺序要求
方法
方法的调用机制原理
- 当程序执行到方法时,就会开辟一个独立的空间(栈空间)
- 当方法执行完毕,或者执行到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… 表示接受的是可变参数,类型是 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 不能在类定义的外部使用,只能在类定义的方法中使用
==和equals()的区别
==(双等号)- 基本数据类型(如
int,double,boolean):比较的是它们的数值是否相等。 - 引用数据类型(如对象、数组、字符串):比较的是它们的内存地址是否相同(即是否指向堆内存中的同一个对象)。
- 基本数据类型(如
equals()方法- 对象专属:它是
Object类的一个方法,所有类都继承了它。 - 默认行为:如果没有重写(Override),它的底层逻辑和
==一样,比较的还是内存地址。 - 常用重写:像
String、Integer等类都重写了equals()方法,用来比较它们的内容(值)是否相同。
- 对象专属:它是
面向对象的三大特征
封装、继承、多态
封装
封装就是把抽象出的数据和对数据的操作封装在一起,数据被保护在内部,程序的其他部分只能通过授权的操作,才能对数据进行操作
将属性设置为private 创建setXXX和getXXX的方法 public
继承
class 子类 extends 父类{ |
子类就是会自动拥有父类定义的属性和方法
父类又叫 超类,基类
子类又叫派生类
注意事项
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(); //向上转型 |
巨大坑:类型转换异常 ClassCastException
只有原本就是该子类对象,才能强转;
Animal a = new Cat(); |
解决方案:instanceof 判断
判断引用变量指向的实际对象是不是【某个类 / 它的子类 / 实现类】的实例
转型前先判断引用实际对象类型
if(a instanceof Dog){ |
| 类型 | 写法 | 是否强制 | 风险 | 能力 |
|---|---|---|---|---|
| 向上转型 | 父类 引用 = new 子类 () | 自动 | 无风险 | 只能访问父类方法,执行子类重写方法;不能访问子类独有方法 |
| 向下转型 | 子类 引用 = (子类) 父类引用 | 需要强制 | 可能抛 ClassCastException,配合 instanceof | 可以调用子类独有方法 |
易错总结
方法有多态,成员变量没有多态!
class A{ int num = 10; } |
- 静态方法重写不叫重写,叫隐藏,不存在多态;调用看左边
- 向上转型本质:多态的基础写法
- 向下转型不是多态,只是为了访问子类特有功能
Java的动态类型绑定
调用对象【方法】:存在动态绑定
调用方法时,看对象真实运行类型(堆里 new 出来的类型),执行子类重写后的方法。
通俗:方法 → 运行看右边
- 调用对象【成员属性 (变量)】:没有动态绑定
访问变量时,看引用声明类型(等号左边是什么类型),哪里声明,就取哪里的属性。
通俗:属性 → 编译 / 运行都看左边
类变量
类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。
class Child { //类 |
访问类变量
类名.类变量名 对象名.类变量名 推荐用第一种
生命周期
- 类变量是在类加载时就完成初始化了,也就是说,即使你没有创建对象,只要类加载了,就可以使用类变量
- 类变量的生命周期是随类的加载开始,随着类消亡而销毁
类方法
类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区:
类方法中无 this 的参数
普通方法中隐含着 this 的参数
类方法可以通过类名调用,也可以通过对象名调用。
普通方法和对象有关,需要通过对象名调用,比如对象名.方法名 (参数),不能通过类名调用。
类方法中不允许使用和对象有关的关键字,比如 this 和 super。普通方法 (成员方法) 可以。
类方法 (静态方法) 中 只能访问 静态变量 或静态方法 。
普通成员方法,既可以访问 非静态成员,也可以访问静态成员。
小结: 静态方法,只能访问静态的成员,非静态的方法,可以访问静态成员和非静态成员 (必须遵守访问权限)
mian方法
public static void main(String[] args) |
public:公共访问权限。JVM 需要跨类调用 main 方法,必须对外公开可访问。
static:静态方法。JVM 启动时还没有创建类的实例对象,直接通过类名调用方法,不需要 new 对象。
void:无返回值。JVM 不接收 main 函数的返回结果。
main:固定方法名,JVM 识别的程序入口名称,不能修改。
String[] args:命令行参数数组
在控制台执行命令示例:
java TestDemo 张三 20 男
args[0]="张三"args[1]="20"args[2]="男"args 名称可以修改(例如
String[] params),但数组类型不能变。
三种代码块
态代码块
static { |
位置:类内部,方法外面,加
static触发时机:类加载的时候执行,只执行 1 次
作用:初始化静态变量
限制:
只能访问静态成员
不能使用 this、super
不可以写 return
构造代码块(实例代码块 / 普通代码块【类内】)
{ |
- 位置:类内部、方法外面,没有 static
- 触发时机:每次创建对象 new 的时候执行
- 执行时机:在构造方法之前执行
- 作用:提取多个构造方法的公共重复代码
- 可以访问静态、非静态成员,可以使用 this
局部代码块(方法内普通代码块)
public void test(){ |
- 位置:方法内部
- 作用:限定局部变量生命周期,缩小作用域
区分「构造代码块」和「局部代码块」
执行顺序
情况 1:只创建单个对象
1. 静态代码块(类加载,最先执行,只运行一次) |
示例演示:
public class Test { |
静态代码块 |
情况 2:多次 new 对象
new Test(); |
静态代码块 //类加载,仅第1次执行 |
关键结论:静态代码块随类加载,全局只执行一次;构造代码块每次 new 对象都会执行。
情况 3:存在父子继承关系
继承场景完整顺序口诀:
父静态 → 子静态 → 父构造代码块 → 父构造方法 → 子构造代码块 → 子构造方法
演示代码:
class Father{ |
运行结果:
父静态 |
易错提醒:
静态代码块只看类加载顺序,优先父类静态;静态代码块互不依赖实例,优先全部执行完所有静态,再执行实例相关代码块、构造器。
| 类型 | 格式 | 位置 | 执行时机 | 执行次数 |
|---|---|---|---|---|
| 静态代码块 | static {} |
类内,方法外 | 类加载阶段 | 整个程序只 1 次 |
| 构造代码块 | {} |
类内,方法外 | 每次 new 创建实例 | 每 new 一次执行一次 |
| 局部代码块 | {} |
方法内部 | 方法运行到该位置 | 运行到就执行 |
设计模式
- 静态方法和属性的经典使用
- 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。
单例模式
单例模式:保证一个类在整个程序中,只有唯一的一个实例对象,对外提供统一获取该对象的入口。
使用场景:工具类、线程池、数据库连接池、配置文件对象等,不需要多个实例,节约内存。
核心三大要点:
- 私有构造方法:
private 类名(){},禁止外部new创建对象 - 私有静态成员变量:保存唯一实例
- 公共静态方法:对外获取这个唯一实例
方式 1:饿汉式(静态常量写法,【线程安全】)
public class SingletonHungry { |
执行时机
类加载的时候直接创建对象。
优点:
- 写法简单,天然线程安全(类加载只执行一次,不存在多线程冲突)
缺点:
- 类一旦加载,立刻创建对象。即使程序全程没有用到这个实例,对象依然创建,造成内存浪费。
饿汉:肚子一饿立刻吃饭 → 类加载直接实例化。
方式 2:懒汉式(基础版,多线程不安全!)
public class SingletonLazy { |
执行时机
调用getInstance()方法的时候,才创建实例(延迟加载)
优点:懒加载,不用就不创建,节省内存
致命缺陷:多线程环境下会创建多个对象,线程不安全
线程安全优化:双重检查锁 DCL(面试标准懒汉写法)
public class SingletonLazyDCL { |
懒汉:不到饭点不吃饭 → 需要用到的时候才创建对象
| 对比 | 饿汉式 | 懒汉式 (DCL 双重检查锁) |
|---|---|---|
| 创建时机 | 类加载阶段 | 第一次调用 getInstance () |
| 懒加载 | ❌不支持 | ✅支持,节省内存 |
| 线程安全 | ✅天然安全 | 需要加锁 + volatile 保证安全 |
| 使用场景 | 确定一定会使用这个对象 | 不确定是否会使用,希望延迟创建 |
拓展知识点
- 私有构造方法依然可以被反射暴力破解,两种基础实现都不能防止反射创建多个实例;
- 还有第三种写法:静态内部类单例(推荐,兼顾懒加载与线程安全);
public class Singleton { |
final关键字
final 表示「最终的、不可修改」,可以修饰:变量、方法、类。
一、final 修饰【变量】
变量一旦赋值成功,不能重新赋值(常量)
- 局部变量
public void test(){ |
final 局部变量可以先声明、后面一次赋值,但是只能赋值一次。
- 成员变量(实例变量)
两种赋值时机,二选一:
- 定义时直接初始化
- 在构造方法中赋值
不能不赋值!final 成员变量必须保证对象创建完成时有值。
class A{ |
- 静态成员变量(static final)
俗称常量,命名规范:全部大写,下划线分隔 MAX_VALUE
赋值时机:
- 声明处初始化
- 静态代码块初始化
public static final double PI = 3.1415926; |
极易混淆重点:final 修饰引用类型变量
final Person p = new Person(); |
final限制的是引用地址不能变对象内部的属性依然可以修改!
形象理解:遥控器不能换电视,但是可以换频道。
二、final 修饰【方法】
public final void show(){} |
规则:
该方法不能被子类重写(override)
仍然可以被继承,子类可以调用,只是不能改写方法实现。
补充:
private 方法隐式带有 final 特性,子类无法重写私有方法。
三、final 修饰【类】
final class Animal{} |
规则:
这个类不能被继承,没有子类
例子:String、Integer 都是 final 类,你无法写 class MyStr extends String
四、易错知识点汇总
- final 和 static 区别?
- static:属于类,所有对象共享一份
- final:值不可修改;两者经常组合
static final定义常量
- 基本类型 final vs 引用类型 final
- 基本类型:数值不能变
- 引用类型:对象地址不能变,对象内部内容可变
- final 方法可以重载吗?
可以重载!只是禁止重写,重载不受影响。
class A{ |
- final 可以修饰构造方法吗?
不允许!构造方法本身不能被继承重写,语法不支持。
五、速记总结
- final 变量:赋值一次,终身不改(引用变量地址不变,对象内容可变)
- final 方法:禁止子类重写
- final 类:断绝继承,不能有子类
| 修饰目标 | 作用 |
|---|---|
| final 变量 | 只能赋值一次,不可重新赋值 |
| final 方法 | 不能被子类重写 |
| final 类 | 不能被继承 |
Java 抽象类(abstract class)
一、基础概念
用 abstract 关键字修饰的类,叫做抽象类。
抽象类用来提取多个子类的公共模板,允许定义没有方法体的抽象方法,强制子类去实现。
语法格式:
abstract class 类名 { |
二、抽象方法
语法:abstract 返回值 方法名();
- 只有方法声明,没有方法体(没有 {})
- 必须写在抽象类 / 接口当中
- 作用:规范子类必须实现这个方法
示例:
abstract class Animal { |
三、核心硬性规则
抽象类不能 new 创建对象
new Animal(); // 编译报错
原因:类中有未实现的抽象方法,对象不完整。
如果一个类继承抽象类:
- 方案①:普通子类 → 必须重写父类所有抽象方法
- 方案②:子类也定义为
abstract抽象类 → 可以不用实现抽象方法
// 普通子类,必须实现全部抽象方法 |
- 抽象类里可以包含什么?
普通成员变量、静态变量
普通实例方法、static 静态方法
构造方法(供子类创建对象时调用)
抽象方法
重点:抽象类有构造方法!只是不能自己实例化,子类
new的时候通过super()调用。
几个语法禁忌
abstract和final不能一起修饰类
final 类不能被继承,abstract 类要求必须被继承实现,互相矛盾。
abstract 和 private 不能一起修饰方法
private 方法子类看不见,无法重写实现,冲突。
abstract 和 static 不能一起修饰方法
静态方法可以直接类名调用,不需要子类实现,冲突。
| 类型 | 能否实例化 | 能否拥有抽象方法 |
|---|---|---|
| 普通类 | 可以 new | 不能有抽象方法 |
| 抽象类 | 不能直接 new | 可以定义抽象方法 |
四、易错知识点
抽象类一定包含抽象方法?
错误!抽象类可以没有任何抽象方法,只是很少这么写。
abstract class Test{ |
- 有抽象方法的类,一定是抽象类?
正确!普通类存在抽象方法直接编译报错。
Java 接口(interface)
接口是行为规范、标准契约,定义一组功能(方法声明),本身不关心怎么实现。
语法:
interface 接口名 { |
类实现接口,关键字:implements
class 类名 implements 接口1,接口2 { |
不同 JDK 版本接口内允许的成员
1)JDK7 及以前(经典老式接口)
接口里面只能有:全局常量、抽象方法
- 常量:
public static final修饰,默认隐藏
interface Usb{ |
必须初始化,不能修改。
- 抽象方法:
public abstract修饰,默认隐藏
interface Usb{ |
2)JDK 8 新增
默认方法(default):带方法体,实现类可以选择重写,不重写就用默认实现
default void show(){ |
静态方法(static):接口。静态方法 () 调用,实现类不能继承
3)JDK 9 新增
私有方法 private:供接口内部默认方法复用
面试常记:日常开发核心掌握【常量、抽象方法、default 默认方法、static 静态方法】
核心语法规则
- 接口不能 new 实例化,没有构造方法!
new Usb(); // 报错 |
- 类和接口:
implements多实现(一个类可以同时实现多个接口)
class Phone implements Usb, Bluetooth{} |
- 接口与接口:
extends多继承
interface A{} |
- 实现类要求:
- 普通类实现接口:必须重写接口中所有抽象方法
- 抽象类实现接口:可以不实现抽象方法,交给它的子类完成
- 方法访问权限:
接口抽象方法强制 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{ |
- 接口常量默认
public static final,写不写都一样,不能修改。
Java 内部类
一、什么是内部类
把一个类定义在另一个类的内部,这个类就叫内部类;外面的类称为外部类(外围类)。
//外部类 |
分类(四大类)
- 成员内部类(实例内部类)
- 静态内部类(静态嵌套类)
- 局部内部类(方法内定义)
- 匿名内部类(重点,经常和接口 / 抽象类搭配)
成员内部类(没有 static,写在外部类成员位置)
class Outer{ |
核心规则
- 成员内部类依赖外部类对象,不能直接 new
Outer outer = new Outer(); |
简写:Outer.Inner inner = new Outer().new Inner();
- 内外类出现同名变量怎么区分?
class Outer{ |
限制
内部类里面不能定义静态成员(static 变量、静态方法)
因为成员内部类必须依托外部实例,静态属于类,冲突
访问权限
成员内部类可以使用
private / protected / public修饰;如果内部类被 private 修饰,外部其他类无法访问这个内部类。
静态内部类(static 修饰,也叫嵌套类)
class Outer{ |
核心规则
- 不依赖外部类对象,直接通过外部类类名创建
Outer.StaticInner inner = new Outer.StaticInner(); |
不能写 outer.new StaticInner ()
访问范围
只能直接访问外部类 static 静态属性、静态方法;
想要访问外部实例成员,必须传入外部类对象。
可以拥有自己的静态成员。
成员内部类 vs 静态内部类 关键区别
| 类型 | 创建方式 | 依赖外部对象 | 可访问外部成员 |
|---|---|---|---|
| 成员内部类 | 外部对象.new 内部类() |
必须有外部实例 | 所有成员(静态 + 实例 + 私有) |
| 静态内部类 | new 外部类.内部类() |
不需要外部实例 | 仅外部静态成员 |
局部内部类(定义在【方法里面】)
class Outer{ |
规则
- 作用域仅限当前方法,方法外面无法使用;
- 不能使用访问修饰符
public private protected; - JDK8 开始,如果访问方法里的局部变量,变量默认隐式
final(不能修改);
public void test(){ |
匿名内部类(重中之重)
没有类名的内部类,一般用来一次性实现接口 / 抽象类,简化代码。
语法格式:
new 接口/抽象类/父类(){ |
示例:基于接口
interface Usb{ |
核心特点
- 匿名内部类只能使用一次,只能创建一个对象;下次创建对象的时候必须重新写一边,没有名字不能重复使用;
- 没有构造方法;
- 本质:隐式继承父类 / 实现接口;
- 属于局部内部类的一种;
常用场景:方法传参、线程 new Thread(new Runnable(){})
拓展:JDK8 Lambda 表达式可以简化只有一个抽象方法的接口,替代匿名内部类。
通用核心考点:内部类访问外部类
- 内部类天生持有外部类的隐式引用:
外部类名.this - 外部类想要访问内部类成员:必须先创建内部类对象,不能直接访问
class Outer{ |
| 类别 | 定义位置 | 是否 static | 创建对象方式 | 核心特点 |
|---|---|---|---|---|
| 成员内部类 | 外部类中,方法外 | 无 | new Outer().new Inner() |
依赖外部对象,可访问所有外部成员,不能定义 static |
| 静态内部类 | 外部类中,方法外 | static | new Outer.StaticInner() |
无需外部对象,仅访问外部静态成员 |
| 局部内部类 | 方法体内 | 无 | 方法内 new | 仅限方法内使用,不能加权限修饰符 |
| 匿名内部类 | 方法 / 参数中 | 无 | new 父类/接口(){} |
无类名,一次性使用,实现抽象方法 |
Scanner 类
Scanner 是 java.util 包下的工具类,用来读取控制台输入、读取文件、字符串数据,最常用于接收键盘输入。
需要导包:
import java.util.Scanner;
1. 基本使用
import java.util.Scanner; |
2. 常用方法
| 方法 | 作用 |
|---|---|
nextInt() |
读取int 整数 |
nextDouble() |
读取小数 double |
nextBoolean() |
读取布尔 true/false |
next() |
读取字符串,遇到空格停止 |
nextLine() |
读取一整行字符串,包含空格 |
next() 和 nextLine() 重点区别
next():读到空格、Tab、回车就结束,不能获取带空格句子nextLine():读到回车结束,可以读取带空格完整一行
坑:
nextInt()之后直接调用nextLine()会读到空字符串! 原因:nextInt()只读取数字,遗留回车符留在缓冲区,nextLine 直接读到回车返回空。
解决办法:
int a = sc.nextInt(); |
3. hasNext() 判断是否还有输入
循环读取输入,判断是否有数据:
Scanner sc = new Scanner(System.in); |
4. Scanner 读取文件(拓展)
// Scanner sc = new Scanner(new File("test.txt")); |
5. 常见报错
InputMismatchException:输入的数据类型不匹配,比如要 int,你输入字母。- 忘记
sc.close():不会报错,但会资源泄漏。 - nextInt 后 nextLine 得到空字符串:上面说的缓冲区回车问题。
枚举和注解
枚举
枚举是一种特殊类,用来固定、列出有限、确定的常量。
适合场景:季节、星期、订单状态、性别、方向。
解决普通常量public static final的缺陷(无法限制传入非法值)。
语法定义:
enum Season { |
等价理解:每个常量都是 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;
//【构造方法:默认private,不能写public】
Season(String name, String desc) {
this.name = name;
this.desc = desc;
}
//getter,不能提供setter(枚举常量一般不可修改)
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)
根据名字字符串获取枚举常量;名字不存在抛出异常
IllegalArgumentExceptionSeason s = Season.valueOf("SPRING");
ordinal()
返回常量序号,从 0 开始(定义顺序)
System.out.println(Season.SPRING.ordinal()); //0
name()/toString()
name():返回常量定义名称(SPRING),final 无法重写toString():默认和 name() 一样,可以重写自定义输出
System.out.println(Season.SPRING.name()); //SPRING
枚举可以拥有的特性
构造方法(只能 private)
成员变量、普通方法、静态方法
抽象方法(每个枚举常量必须实现!)
可以实现接口
不能继承其他类(隐式继承 Enum,Java 单继承)
外部无法 new 枚举对象
每个枚举实例单独实现方法:
enum Season {
SPRING{
public void show() {
System.out.println("春暖花开");
}
},
SUMMER{
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:局部变量
- @Retention
指定注解,生命周期(保留到哪个阶段)
SOURCE → 源码阶段,编译后丢弃(注释级别,仅编译器看见,class不存在) |
想在运行时通过反射获取注解信息,必须写:
@Retention(RetentionPolicy.RUNTIME)
- @Documented
使用 javadoc 生成 API 文档时,把注解信息纳入文档
- @Inherited
可继承:父类标记该注解,子类自动继承这个注解
注意:只对【类】有效;方法、属性无效
- @Repeatable(JDK8):可重复注解(拓展了解)
自定义注解(自己创建注解)
语法:@interface
// 元注解约束 |
使用自定义注解
// 只有一个属性名叫value,可以省略属性名 |
关键语法规则
属性支持类型:基本类型、String、枚举、注解、以上类型数组
如果属性没有设置
default默认值,使用注解时必须显式赋值特殊约定:属性名叫做
value,并且只有这一个属性,赋值时可以省略value=
注解如何生效?
注解本身只是标记!必须配合反射机制解析
示例:通过反射读取运行时注解
// 获取方法 |
Spring、Mybatis 框架底层大量使用:自定义注解 + 反射实现功能。
异常-Exception
一、什么是异常
程序运行过程中出现的不正常情况(错误),会打断正常指令流。
Java 使用面向对象方式管理异常:异常本身就是一个对象。
区分:
- 编译期异常:写代码编译器就报错
- 运行时异常:编译正常,运行程序崩溃
二、异常体系结构图(核心)
java.lang.Throwable |
概念区分
Error(错误)
虚拟机内部故障、资源耗尽。程序代码无法修复,不需要 try-catch 捕获。
Exception(异常)
程序逻辑问题,可以处理。分为两类:
运行时异常(RuntimeException)【非受检异常】
编译不强制要求捕获 / 声明;一般由代码书写 bug 导致。
编译期异常(受检异常 CheckedException)
编译器强制要求:要么
try-catch捕获,要么throws抛出,否则编译报错。
三、异常处理两大方案
方案 1:try-catch-finally 捕获处理
语法模板
try{ |
细节考点
try不能单独存在,必须搭配 catch 或 finally- 多个 catch:小范围异常写前面,大范围异常写后面(子类在前,父类在后)
try{ |
- finally 典型用途:释放资源(文件流、数据库连接、IO 流)
try-catch-resources(JDK7 新特性)
自动实现资源关闭,不用手写 finally 关闭流
try(InputStream is = new FileInputStream("a.txt")){ |
方案 2:throws 向上抛出异常
如果当前方法不想处理异常,可以把异常抛给调用者
// 抛出受检异常,调用者必须处理 |
区别:
try-catch:自己处理异常,异常到此结束throws:不处理,甩锅给上层调用方法
四、主动抛出异常 throw
手动创建异常对象并抛出
int age = -5; |
throw(小写):方法内部主动抛出对象
throws(复数):方法声明上,标识该方法可能抛出的异常
| 关键字 | 位置 | 作用 |
|---|---|---|
| throw | 方法内部 | 主动抛出一个异常实例 |
| throws | 方法签名后 | 声明该方法可能产生哪些异常,向上传递 |
五、自定义异常
业务场景内置异常不够用时,自己定义异常类
自定义受检异常:继承
Exception自定义运行时异常:继承
RuntimeException(项目最常用)示例:
//自定义运行时异常 |
使用:
if(age < 0){ |
六、重点知识点
1.finally 一定会执行吗?
绝大多数情况一定会执行
唯一打断情况:System.exit(0); 终止 JVM,finally 不再执行
2.try 里有 return,finally 执行时机
先执行 finally 代码,再执行 return
public static int test(){ |
注意:不要在 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); // 传给父类 Throwable 保存
}initCause()方法: 如果使用的异常类没有提供带cause参数的构造方法,可以通过调用对象的initCause(Throwable cause)方法来手动绑定。代码示例
步骤 1:定义一个自定义业务异常
public class UserServiceException extends RuntimeException { |
步骤 2:在业务代码中捕获并链式抛出
public class UserService { |
步骤 3:查看运行效果
当外层程序捕获到 UserServiceException 并打印堆栈信息(e.printStackTrace())时,你会看到类似下面这样的输出:
com.example.UserServiceException: 用户注册模块发生内部错误 |
- 注意看
Caused by::这就是异常链的核心威力所在。它不仅告诉调用者上层业务出错了,还顺藤摸瓜地把底层的罪魁祸首完整展示了出来。
- 总结与应用场景
- 核心作用:屏蔽底层实现细节,同时保留底层错误根源,实现“高层抛出业务异常,底层保留技术异常”。
- 常见应用:在现代企业级框架(如 Spring、MyBatis 等)中非常普遍。例如,当你使用 MyBatis 查询数据库出错时,它捕获到底层的
SQLException,并会将其包装并抛出它自己的DataAccessException(带有异常链),方便开发者通过日志一眼看出是底层 SQL 语法错了、主键冲突了还是连接断了。
集合
一、集合概述
集合:java.util包下,容器,用来存储多个对象。
区分数组和集合:
- 数组:长度固定;可以存基本类型 + 引用类型
- 集合:长度可变;只能存储引用类型(对象),基本类型会自动装箱
集合两大根接口:
- Collection 单列集合:一次存一个对象
- Map 双列集合:一次存一对对象(key=value 键值对)
集合整体架构图
Collection(单列) |
第一部分:Collection 接口
通用常用方法(所有 List、Set 都拥有)
boolean add(E e) 添加元素 |
- List 接口
特点:有序、可重复、带有索引,支持根据索引增删查改。
特有方法(带索引操作)
get(int index) |
① ArrayList(重点)
底层:可变 Object 数组
默认初始容量:10
扩容机制:原容量
1.5倍查询快、随机访问快;中间增删慢(需要移动元素)
线程不安全;效率高
适用:大量查询,少量增删
② LinkedList
底层:双向链表
没有扩容概念,按需创建节点
查询慢(需要遍历);首尾增删速度快
线程不安全
特有方法(队列 / 栈操作)
addFirst() |
③ 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 循环(底层迭代器)
for(Object obj : list){} |
- Iterator 迭代器
Iterator<String> it = list.iterator(); |
并发异常:迭代遍历集合时,不要用集合 add/remove;想要删除用迭代器
it.remove()
- List 额外支持普通 for 循环(有索引)
第二部分:Map 双列集合
存储格式:key=value,键唯一,值可重复;
一个 key 只能对应一个 value;后存入的 value 覆盖旧 value。
Map 通用方法
put(k,v) 添加/覆盖键值对 |
Map 三种遍历方式
- keySet() 先拿所有键,再 get 取值
Set<String> keys = map.keySet(); |
- entrySet() 获取 Entry 键值对(效率更高,推荐)
Set<Map.Entry<String,Integer>> entries = map.entrySet(); |
- values() 只获取所有 value(无法拿到 key)
常用实现类
① HashMap(重中之重)
底层:哈希表(数组 + 链表 + 红黑树)JDK1.8 优化
参数:
默认容量:16
负载因子:0.75
扩容条件:元素个数 > 容量 * 负载因子 → 容量扩大 2 倍
树化阈值:链表长度 ≥8,数组长度≥64 → 链表转为红黑树;
元素≤6 退化为链表。
特点:
key 无序;key 允许一个 null,value 允许多个 null
线程不安全
去重规则:key 对象重写
hashCode() + equals()
② 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); //排序 |
注意:只是方法加锁,性能一般;高并发优先 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<String> list = new ArrayList<>(); |
三、泛型的三种使用形式
- 泛型类
格式:类名<泛型标识>
常用标识符约定(只是名字,无语法要求)
EElement 元素(集合)TType 类型KKey 键 、VValue 值(Map)RReturn 返回值
public class MyBox<T> { |
注意:创建对象时指定类型,这个类里面所有 T 都会被替换为指定类型。
- 泛型接口
interface IStorage<T> { |
- 泛型方法(重点:和泛型类区分开)
泛型方法可以定义在普通类 / 泛型类上,方法自己拥有独立泛型参数
格式:返回值前声明泛型 <T>
//泛型方法 |
关键区分:
- 泛型类的 T:创建对象时确定
- 泛型方法的 T:调用方法时自动推导类型
四、类型擦除
泛型是编译期概念,.class 字节码中不存在泛型信息!
编译流程:
- 编译器利用泛型做类型校验
- 擦除泛型参数,替换为原始类型(上限 Object / 指定边界类型)
- 自动插入强制类型转换代码
示例:
List<String> list = new ArrayList<>(); |
编译后字节码等价于:
List list = new ArrayList(); |
原始类型 RawType
不加泛型参数的集合:List list = new ArrayList();
原始类型会关闭泛型检查,尽量不要使用。
擦除规则
- 无边界泛型
<T>→ 擦除为Object - 有上限
<T extends Number>→ 擦除为Number - 有下限不能单独用于类泛型(只能通配符)
五、通配符 ? Wildcard
? 代表任意未知类型,主要用于方法参数,不能用于定义泛型类 / 泛型方法。
- 无界通配符
<?>
public void printList(List<?> list){ |
限制:
可以读取元素,读到 Object
不能 add 除 null 以外的任何元素(编译器不知道真实类型,禁止写入)
- 上限通配符
? extends T
? extends Animal:代表T 及其所有子类
public void test(List<? extends Animal> list){ |
读取安全,写入不安全。生产者场景(读数据)
- 下限通配符
? super T
? super Dog:代表Dog 及其所有父类
public void test(List<? super Dog> list){ |
写入安全,读取不安全。消费者场景(写入数据)
PECS 原则(高频面试)
Producer Extends,Consumer Super
- 只读取数据(生产者):
? extends T - 只写入数据(消费者):
? super T - 既要读又要写:不能使用通配符,直接确定泛型
<T>
六、泛型边界(限定类型范围)
上限限定 <T extends 类/接口>
T 只能是该类或子类
// T必须是Number或者Number子类(Integer/Double等) |
泛型没有多上限直接写法;想要同时多个约束,需要
T extends A & B
<T extends Serializable & Comparable> |
注意:没有 <T super xxx> 写法!
下限只能用于通配符?,不能用于泛型声明。
七、泛型常见限制
- 不能使用泛型创建实例
T t = new T(); //编译报错!擦除后无法确定类型 |
- 不能定义泛型数组
T[] arr = new T[10]; //报错 |
- 静态方法不能使用类上定义的泛型
public class Test<T>{ |
原因:类泛型在创建对象时确定,静态方法不需要对象。
解决方案:使用独立泛型方法<E>
基本类型不能作为泛型参数
List<int>❌;必须使用包装类List<Integer>✅泛型不支持继承多态误区
List<String> list1 = new ArrayList<>(); |
虽然 String 是 Object 子类,但是 List<String>不是 List<Object> 的子类。泛型容器之间不存在继承关系!
- 重载不能依靠泛型区分(擦除后方法签名一样)
void fun(List<String> list){} |
八、泛型方法 vs 通配符怎么选?
示例需求:把 list 所有元素添加到另一个集合
//方案1:泛型方法 |
- 需要同时读写集合、类型互相约束 → 使用泛型
<T> - 只单纯读取或写入 → 使用通配符
九、易错知识点
泛型实现原理?
答:类型擦除,编译期校验,运行时无泛型信息。
List<String>和List<Object>有继承关系吗?没有,泛型容器不存在多态,擦除后都是 List。
十、知识点总结
泛型参数化类型,JDK1.5 推出;
编译检查免强转,运行类型被擦除;
三类用法:泛型类、泛型接口、泛型方法;
任意未知类型,extends上界super下界;
PECS 口诀:读用 extends,写用 super;
不能 new 泛型对象、不能泛型数组;
静态方法不能使用类泛型;
容器泛型无继承,List<String>不是 List<Object>;
基本类型不能做泛型,需要包装类。
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 = 毫秒) |
设置测试超时时间,超时判定失败 |
执行顺序示例:
→ |
- Assert 断言(单元测试核心)
org.junit.Assert,断言失败测试直接报错
Assert.assertEquals(预期值,实际值); //相等 |
规则:单元测试必须使用断言,不能靠 System.out 打印判断结果!
- JUnit4 代码示例
import org.junit.*; |
三、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,"相加结果错误"); |
JUnit5 简单示例
import org.junit.jupiter.api.*; |
四、重要概念区分
- 单元测试 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{ |
调用 start(),不是 run()!
run() 只是普通方法;start() 向操作系统申请创建新线程,自动调用 run()
方式 2:实现 Runnable 接口(推荐)
解耦:线程类和任务分离;避免单继承限制
class Task implements Runnable{ |
方式 3:Callable + FutureTask(有返回值、可抛异常)
Runnable 无返回值、不能抛出受检异常;Callable 弥补该缺陷
Callable<Integer> call = ()->{ |
方式 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实例方法互不阻塞!两把不同的锁
- 修饰【同步代码块】(推荐,粒度可控)
//锁任意对象 |
优势:缩小锁范围,提高并发效率。
什么是线程同步
多个线程同时访问共享资源,控制访问顺序,保证数据安全,避免并发冲突。
根本问题:多线程并发操作共享变量,产生数据竞争。
三大并发问题:原子性、可见性、有序性。
同步本质:让并发操作串行化(部分串行),牺牲吞吐量换取正确性。
二、什么时候需要同步?
同时满足下面 3 点,一定会出现线程安全问题:
多线程
共享变量(共享资源)
存在写操作(修改)
只读场景不需要同步。
示例不安全代码:
class Count { |
三、线程同步的四大实现方案
方案 1:synchronized 内置同步锁(监视器锁)
前面详细讲过,简单回顾同步用法
- 同步实例方法:锁 this
- 同步静态方法:锁 Class 对象
- 同步代码块:自定义锁对象
//标准写法,锁对象建议 private final |
特性:可重入、自动释放锁、异常自动解锁;非公平锁。
配套等待唤醒机制:
wait() / notify() / notifyAll()典型模型:生产者消费者。
方案 2:Lock 显式锁(AQS)
ReentrantLock
Lock lock = new ReentrantLock(); |
扩展能力:
- 公平锁 / 非公平锁
tryLock()非阻塞尝试获取锁lockInterruptibly()支持中断等待- 多个 Condition 条件变量,精准唤醒线程
方案 3:volatile(轻量级同步手段)
volatile ≠ 锁,不能实现完整同步!
作用:
保证可见性、禁止指令重排序
无法保证原子性(不能解决 num++)
适用场景:状态标记、DCL 单例。
方案 4:无锁同步:CAS 原子类(乐观同步)
java.util.concurrent.atomic |
底层:CAS(CompareAndSwap)乐观锁,不阻塞线程。
AtomicInteger num = new AtomicInteger(0); |
优点:无锁、性能高;
问题:存在 ABA 问题,解决方案:AtomicStampedReference增加版本号。
四、同步两大模型:悲观锁 VS 乐观锁
悲观锁
默认一定会发生竞争,访问资源先上锁。
代表:
synchronized、ReentrantLock线程竞争失败阻塞。
乐观锁
默认竞争很少,不加锁;修改时校验数据是否被别人修改,冲突则重试。
代表:CAS 原子类、数据库版本号机制。
五、线程间同步通信(等待唤醒机制)
- synchronized + wait/notify
synchronized(lock){ |
关键点:
- wait 释放锁;sleep 不释放锁
- 必须在同步代码块中调用
- 推荐 notifyAll,避免信号丢失
- Lock + Condition
Condition condition = lock.newCondition(); |
优势:可以创建多个 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)
装饰器设计模式:创建流时,层层包装。
示例:
//节点流 |
四、重要编码问题
FileReader/FileWriter默认使用系统编码,无法手动指定编码,极易乱码!
规范做法:使用转换流指定编码
//字节流 + 转换流 = 指定编码读取文本 |
五、缓冲流原理
普通流:每次读写直接访问磁盘,频繁 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"))){ |
八、常用方法汇总
字节流 InputStream
read():读取 1 字节,返回 int;末尾返回 - 1read(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.BufferedInputStream; |
- 字符流:读取文本文件 FileReader
缺陷:无法指定编码,容易乱码!
import java.io.FileReader; |
- 缓冲字符流 BufferedReader(readLine 读取一行)
import java.io.BufferedReader; |
- 转换流(重点!解决文本乱码,可以指定 UTF-8 编码)
InputStreamReader / OutputStreamWriter 字节→字符桥梁
项目标准读写文本写法
import java.io.BufferedReader; |
- 对象序列化 & 反序列化 ObjectStream Demo
第一步:实体类(实现 Serializable)
import java.io.Serializable; |
第二步:序列化(对象写入文件)+ 反序列化(读取对象)
import java.io.FileInputStream; |
- PrintWriter 打印流(方便输出文本,自动换行刷新)
import java.io.FileOutputStream; |
Properties
一、概述
java.util.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,找不到返回 nullgetProperty(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)
#数据库配置 |
四、完整 Demo
Demo1:加载配置文件(最常用)
把 db.properties 放在 项目根目录
import java.io.FileInputStream; |
Demo2:存入数据 + 写出到配置文件
import java.io.FileOutputStream; |
Demo3:遍历 Properties
import java.util.Properties; |
高频注意事项
- 不要使用 put/get
prop.put("a",123); //错误!存入Integer |
- 配置文件默认编码:ISO-8859-1
文件包含中文会乱码!
解决方案:
- 使用
Reader配合转换流指定 UTF-8
InputStreamReader isr = new InputStreamReader(new FileInputStream("db.properties"),"UTF-8"); |
- 路径问题
new FileInputStream("xxx.properties"):相对项目根目录- 如果放在 resources 资源文件夹(Maven 项目),使用类加载器读取(标准开发写法)
//读取resources下的配置文件 |
- 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 对象,使用静态方法获取实例
//获取本机 |
- 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; |
客户端 Socket
import java.io.OutputStreamWriter; |
- UDP 编程:DatagramSocket + DatagramPacket
UDP 没有建立连接概念,数据包为单位传输
DatagramSocket:收发数据包的套接字DatagramPacket:数据包(存放字节数据、目标地址端口)
UDP Demo
//UDP 发送端 |
三、BIO 网络编程存在的问题
原生 BIO:一个客户端连接占用一个独立线程
客户端很多 → 创建大量线程,内存暴涨、线程切换开销大
线程大部分时间阻塞在
accept()/read(),资源浪费解决方案:
- 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"); |
Java 反射 Reflection
一、什么是反射
反射机制:程序运行期间,可以获取类的完整信息、操作对象(构造方法、成员变量、成员方法)。
正常正向调用:new 对象() → 编译期确定类型
反射逆向操作:运行期才解析类信息,动态创建对象、调用方法。
包:
java.lang.reflect.*;入口:Class类
二、反射核心作用
- 运行时获取类名、父类、接口、属性、方法、构造器
- 运行时动态创建类实例
- 运行时调用任意对象的方法、读写成员变量(包括私有 private)
- 主流框架底层原理:Spring IoC、MyBatis、注解、JUnit 都大量使用反射
三、获取 Class 对象的 4 种方式
class Student{} |
一个类在 JVM 中只会加载一次,4 种方式拿到的是同一个 Class 对象。
四、Class 五大核心组件
通过 Class 可以获取:
Constructor:构造方法Method:成员方法Field:成员变量Annotation:注解Superclass/Interface:父类、实现接口
五、全套代码 Demo
准备实体类
public class Student { |
Demo1:反射通过构造器创建对象
import java.lang.reflect.Constructor; |
getConstructor()只能获取public 构造器
getDeclaredConstructor()可以获取所有权限(private/public)构造器
Demo2:反射操作成员变量 Field(突破私有)
import java.lang.reflect.Field; |
Demo3:反射调用方法 Method
import java.lang.reflect.Method; |
六、关键 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 全套知识点
一、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 --> |
MySQL5.x 驱动类:
com.mysql.jdbc.DriverMySQL8.x 驱动类:
com.mysql.cj.jdbc.Driver
连接 URL 模板
jdbc:mysql://ip:端口/数据库名?参数 |
示例:
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.*; |
Statement 致命缺点:SQL 注入
拼接字符串 SQL,恶意输入可篡改 SQL 逻辑,项目禁止使用 Statement。
Demo2:PreparedStatement(标准推荐,预编译防注入)
原理:SQL 模板预编译,使用?占位符,参数单独赋值,SQL 结构固定,杜绝注入。
import java.sql.*; |
Demo3:增删改(executeUpdate)
//新增、修改、删除统一使用 executeUpdate(),返回受影响行数 |
五、三大执行对象对比
| 对象 | 特点 | 安全性 | 适用场景 |
|---|---|---|---|
| Statement | 字符串拼接 SQL,每次重新编译 | 极易 SQL 注入 | 几乎淘汰 |
| PreparedStatement | 预编译,? 占位符,可复用 SQL | 防注入,安全 | 日常 CRUD 首选 |
| CallableStatement | 专门调用存储过程 | 安全 | 数据库存储过程调用 |
六、核心 API 详解
- Connection
createStatement():创建 StatementprepareStatement(sql):创建预编译对象setAutoCommit(false):关闭自动提交,开启事务commit():事务提交rollback():事务回滚
- PreparedStatement 常用方法
setXxx(下标,值):给?赋值,下标从1开始executeQuery():执行查询,返回 ResultSetexecuteUpdate():执行增删改,返回 int(影响行数)
- ResultSet 结果集
next():指针下移,有数据返回 true,末尾 falsegetXxx(列名/列下标):获取值,列下标从 1 开始beforeFirst():指针回到最开头
七、JDBC 事务管理(核心考点)
JDBC 默认自动提交:单条 SQL 执行完成立刻提交。
多 SQL 原子操作必须手动事务:
try{ |
八、工具类封装(开发常用,简化重复代码)
把连接、关闭资源抽取为工具类,配合 properties 配置文件
db.properties
driver=com.mysql.cj.jdbc.Driver |
JdbcUtil.java
import java.io.InputStream; |
九、常见问题与优化
- 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 { |
方式 2:Pattern + Matcher 精准查找、分组提取(最常用)
核心方法:
Pattern.compile(reg):编译正则matcher.find():查找下一个匹配片段,找到返回 truematcher.group():获取匹配到的完整内容matcher.group(n):获取第 n 个分组内容(括号分组)
Demo1:提取所有数字
import java.util.regex.Matcher; |
Demo2:分组捕获(提取邮箱用户名、域名)
public class GroupDemo { |
方式 3:替换分组内容
//隐藏手机号中间4位 |
$1代表第 1 分组内容,$2代表第 2 分组内容
四、高频实用正则模板(可直接复制使用)
//1.手机号 |
五、贪婪匹配与非贪婪匹配案例
public static void main(String[] args) { |
六、重要知识点
- 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("无参"); |
- 参数类型可省略:编译器自动类型推断,无需声明参数类型
// 简写 |
- 方法体只有一行代码:大括号
{}、return、分号可全部省略
// 单行有返回值,省略{}和return |
Lambda 使用前提:函数式接口
- 定义
函数式接口:有且仅有一个抽象方法的接口,可以有默认方法、静态方法、Object 重写方法。 Lambda 只能绑定函数式接口,用来实现那唯一的抽象方法。
- 注解标识
@FunctionalInterface:强制校验当前接口是否为函数式接口,编译报错提醒,推荐所有函数式接口都加。
|
- 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 消费 |
Lambda 三大引用(方法引用、构造器引用、数组引用)
属于 Lambda 进一步简写,当 Lambda 只是调用已有方法时使用,格式更简洁。
方法引用 3 种形式
- 对象::实例方法:Lambda 调用某个对象的普通成员方法
Consumer<String> c = str->System.out.println(str); |
- 类::静态方法:调用类静态方法
Comparator<Integer> com = (a,b)->Integer.compare(a,b); |
- 类::实例方法:参数 1 作为调用者,参数 2 作为方法入参
BiPredicate<String,String> bp = (s1,s2)->s1.equals(s2); |
构造器引用
格式:类名::new,适配 Supplier/Function
Supplier<User> sup = ()->new User(); |
数组引用
格式:类型[]::new
Function<Integer,String[]> fun = len->new String[len]; |
Lambda 变量作用域
可访问范围
- 静态变量、成员变量:可读可修改;
- 局部变量:隐式 final,Java8 之后局部变量即使不加 final,也不允许二次修改;
int num = 10; |
禁止使用
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); |
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<>(); |
2)数组创建
int[] arr = {1,2,3}; |
3)静态方法 Stream.of ()
Stream<Integer> stream = Stream.of(1,2,3,4,5); |
4)无限流(生成无限序列)
// 迭代生成:从0开始,每次+2 |
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); |
二.映射 map 系列(元素类型转换)
- map(Function):一对一转换,每个元素映射成一个新元素
// 字符串转长度 |
- flatMap(Function):一对多,把流中每个元素拆解为流,扁平化
解决:集合嵌套、拆分字符串、拆分数组
// 拆分字符串 ["a,b","c,d"] → 平铺为 a,b,c,d |
基础类型映射:mapToInt/mapToLong/mapToDouble 作用:拆箱,避免频繁包装类,提升性能,对应IntStream/LongStream/DoubleStream。
- 排序 sorted
sorted():自然排序,元素实现 Comparable
sorted(Comparator):自定义比较器
Stream.of(3,1,2).sorted().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); |
- 规约 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()); |
(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)); |
快捷方法:count()、max()、min()。
(5)分组 groupingBy
List<User> users = ...; |
(6)分区 partitioningBy
按条件分为 true/false 两组
Map<Boolean,List<User>> part = users.stream() |
(7)聚合收集:求和、平均数、最值
Collectors.summingInt()、averagingInt()、maxBy()、minBy() |
三、并行流 parallelStream
开启方式
stream.parallel() // 串行流转并行 |
底层基于 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); |
案例 2:对象集合分组统计
class Student{String name;Integer age;} |
案例 3:扁平化集合
List<List<Integer>> nested = Arrays.asList(Arrays.asList(1,2),Arrays.asList(3,4)); |
六、重要知识点
- 流只能用一次:中间操作后流消费完毕,重复调用抛
IllegalStateException; - 惰性求值:中间操作写了异常逻辑,不调用终止操作不会报错;
- map 与 flatMap 混淆:普通映射用 map,拆分层级用 flatMap;
- 并行流线程不安全:不要修改外部变量;
- distinct 依赖 equals,自定义对象重写 equals+hashCode;
- 基础流(IntStream)无装箱开销,大数据优先使用。
七、Java8~Java11 Stream 新增小特性
Stream.ofNullable():支持 null 数据源,null 会生成空流,防空指针;takeWhile(predicate):从头截取直到不满足条件;dropWhile(predicate):从头丢弃直到不满足条件。


