Java学习笔记03
Java学习笔记03
Lynx FrostScanner 类
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"); |


