三大特性
封装
-
封装的目的:将类的内部结构隐藏起来,对外只开放相关的方法,保证了数据安全,提高了代码复用
-
修饰符
public
:公共访问default
:同一个包下可访问protected
:类本身或者其子类可访问private
:仅类本身可访问
继承
-
继承的目的:允许一个类继承另一个类的属性和方法,并子类可以重写父类的方法,实现了功能拓展
-
hashCode
和equals
的重写- 逻辑比较:实例内容的字面值相同,就认为二者逻辑相同,与地址无关
Object.hashCode
和Object.equals
采用的是地址比较hashCode
和equals
要么都不重写,要么同时重写,否则使用Map
、Set
、List
会有逻辑错误
-
toString
的重写:可读地展示实例内容 -
final
修饰的类:表示此类不能被继承
public final class String{
public boolean equals(Object anObject) {
if (this == anObject) { //如果地址相同,那么逻辑肯定也相同,直接返回true
return true;
}
if (!anObject instanceof String) { //instanceof关键字:判断 实例 是否属于 类/子类
return false;
}
//依次判断字符是否都相同
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
多态
-
多态的目的:在继承的基础上,实例可以选择调用重写的方法
-
多态的条件:
- 多态发生在父类与子类(接口与实现类)之间
- 子类必须 重写 父类的实例方法(非
private
、非final
、非static
) - 必须通过父类引用子类(声明父类,引用的是子类)
-
多态的原理:编译看左边,运行看右边
- 编译看左边:父类声明本质上创建的对象还是父类的实例,因此其他成员依然是父类的成员
- 运行看右边 :父类的方法被子类重写
- 通过父类引用无法直接调用子类独有的方法
-
强制类型转换:子类可以强转为父类,父类不能强转为子类,因为会丢失数据
public class Animal {
Integer id = 0;
public void shout() {
System.out.println("animal sound");
}
}
public class Dog extends Animal {
Integer id = 1;
@Override
public void shout() {
System.out.println("woof");
}
public void unique(){
System.out.println("独有方法");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog(); //dog本质上就是Animal的实例,但shout方法被重写
dog.shout(); //woof
System.out.println(dog.i); //0
}
}
内部类
- 内部类的实例化需要显式地进行,与外部类的实例化不会自动关联
- 静态访问原则:静态内部类不可以引用外部类的普通成员
普通内部类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String schoolName;
@Data
@AllArgsConstructor
@NoArgsConstructor
class School {
private final String name = schoolName; //普通成员内部类可以引用外部类的成员
}
public School getSchool {
return new School();
}
}
静态内部类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String name;
@Data
@AllArgsConstructor
@NoArgsConstructor
static class School{
private Long id; //静态内部类不能引用外部类成员
}
public School getSchool(Long id){
return new School(id);
}
}
局部内部类
public static void main(String[] args) throws Exception {
class LocalInner {
static void method() {
System.out.println("局部内部类方法");
}
}
LocalInner.method();
}
静态
-
静态的目的:静态成员是类级别的,适合存放全局数据(尤其是多线程环境)
-
静态访问原则:静态只能访问静态
- 静态成员/方法 只能访问 静态成员/方法
- 静态代码块只能访问静态成员/方法
-
如果抽象方法要求静态,就需要在声明抽象类时就实现此方法
静态成员/方法
public class User {
private Long id;
private static String schoolName;
}
静态代码块
-
静态代码块使用场景:初始化静态资源
-
Java类的加载顺序
- 静态成员和静态代码块
- 子类的静态成员和静态代码块
- 父类的普通成员和方法
- 父类构造方法
- 子类的普通成员和方法
- 子类构造方法
- 内部静态类不被加载,只要调用时才加载
public class Act {
static String configContent;
static {
try{
byte[] bytes = Files.readAllBytes(Paths.get("config.json"));
configContent = new String(bytes);
} catch (IOException e) {
throw new RuntimeException("配置文件出错");
}
}
}
异常
异常传递机制
throw
主动抛出异常,一般放在catch
中,会中断当前线程throws
是方法可能的异常的声明,发生异常会向下一层传递,不会打断线程,表示暂不处理异常- 如果最后还使用
throws
(如main
方法),异常不会被处理,线程会终止并打印异常信息,但建议异常最终都能被处理
异常堆栈信息定位
- 项目运行时遇到异常会层层传递,显示在控制台/日志中则是自下而上的异常堆栈信息
- 最下面是异常最终抛出/处理的地方,最上面是异常原始出处,建议从下向上看,符合调用链逻辑
java.lang.NullPointerException
at com.wyh.utils.Check.origin(Check.java:18) //method方法调用了origin方法,异常是从origin最开始产生的
at com.wyh.utils.Check.method(Check.java:15) //main方法调用了method方法,异常是method方法传递而来
at com.wyh.utils.Check.main(Check.java:7) //main方法报告异常
自定义异常
// 自定义型异常:继承RuntimeException或其子类
public class MyUncheckedException extends RuntimeException {
// 无参构造方法
public MyUncheckedException() {
super();
}
// 带消息的构造方法
public MyUncheckedException(String message) {
super(message);
}
// 带消息和原因的构造方法,第二个参数是上一级异常,形成异常链方便维护
public MyUncheckedException(String message, Throwable cause) {
super(message, cause);
}
// 带原因的构造方法
public MyUncheckedException(Throwable cause) {
super(cause);
}
}
try-catch-finally
- 不能在 finally 块中使用 return,finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句
try{
//执行语句
}catch(Exception e){
//发生异常处理
}finally{
//无论是否发生异常,最后都会执行,除非JVM关闭
}
try-with-resource
-
有资源管理的场景下,
try-with-resource
进行了简化,无论是否发生异常,声明的资源最后都会自动调用close()
方法 -
try-with-resources
要求资源必须实现AutoCloseable
或Closeable
接口,以便 JVM 能自动调用close()
方法
try(声明资源1;声明资源2;...){
//执行语句
}catch(Exception e){
//异常处理
}
数据类型
-
包装类和基本类型:
- 包装类可以表示
null
值,基本数据类型不支持此业务需求 - 包装类提供了丰富的方法(进制转换)
- 使用集合框架、反射等技术时,必须使用包装类,否则会有异常
- 基本类型存储少性能高,适合用于局部变量
- 包装类可以表示
-
整型包装类缓存机制:创建整型包装类时会先从缓存(-128~127)中找,没有才会创建新对象,因此尽量避免直接使用==比较
Integer i = 128; Integer j = 128; System.out.println(i == j); //false,128超出缓存,i和j是两个实例 System.out.println(i.equals(j); //true,equals方法可以比较 Integer i = 0; Integer j = 0; System.out.println(i == j); //true,0位于缓存区,i和j是同一个实例
-
字符串常量池机制:创建字符串变量时会先在常量池中找值,没有才会在常量池中创建对象
String s1 = "abc"; String s2 = "abc"; System.out.println(s1 == s2); //true,此方式创建的String实例会直接引用常量池对象,s1和s2是同一个实例 String s1 = new String("abc"); String s2 = new String("abc"); System.out.println(s1 == s2); //false,此方式创建的String实例引用的是堆对象,再引用常量池,s1和s2是new了两个堆对象 String s1 = new String("abc"); String s2 = s1.intern(); //intern方法:值在常量池中已有就直接返回,没有则创建常量池对象并返回常量池对象的引用 System.out.println(s1 == s2); //false,此方式创建的s1引用的是堆对象,s2引用的是常量池对象
整形
int
/Integer
:整型的默认类型,4字节,默认为0/nulllong
/Long
:数值要求较大时选用,8字节,默认为0/null,直接声明数值时要加后缀Lbyte
/Byte
:字节,是一种二进制格式数据
public final class Integer{
public static Integer valueOf(int i){...} //返回Integer类型
public String toString(){...} //返回字符串形式
public static String toString(int i, int radix){...} //i是十进制数,返回i的radix进制数的字符串形式
public static int parseInt(int i, int radix){...} //i是radix进制数,返回i的十进制数的字符串形式
}
浮点型
double
/Double
:浮点型的默认类型,8字节,默认为0.0/nullfloat
/Float
:精度要求低时选用,4字节,默认为0.0/null,直接声明数值时要加F后缀BigDecimal
:进行精确的数值运算时选用,可以防止精度丢失
public final class Double{
public static Double valueOf(double d){...} //返回Double类型
public String toString(){...} //返回字符串形式
}
public final class BigDecimal{
public static valueOf(double d){...} //创建BigDecimal对象
public double doubleValue(){...} //返回double类型
public BigDecimal add(BigDecimal b){...} //相加
public BigDecimal subtract(BigDecimal b){...} //相减
public BigDecimal multiply(BihgDecimal b){...} //相乘
public BigDecimal divide(BigDecimal b){...} //相除
}
Math
public final class Math {
public static long abs(long a){...} //取绝对值
public static Number max(Number a, Number b){...} //取较大者
public static double random(){...} //取[0,1)的一个随机数
public static double ceil(Number a){...} //向上取整数
public static double floor(Number a){...} //向下取整数
public static int round(double a){...} //四舍五入取整数
}
字符
char
/Character
:一个字符对应一个Unicode码,可以强转为Unicode码的整数形式
public final class Character{
public String toString(){...} //返回字符的字符串形式
public static boolean isLetter(char ch){...} //判断字符是否为英文字母
public static boolean isDigit(char ch){...} //判断字符是否为数字
public static boolean isWhitespace(char ch){...} //判断字符是否为空格字符
public static boolean isUpperCase(char ch){...} //判断字符是否为大写字母
public static boolean toUpperCase(char ch){...} //返回字母字符的大写形式字符
}
字符串
-
String
:底层通过常量字符数组存储,创建后不可变- 字符串创建后不可变,因此使用
+
拼接字符串时,实际上是由新建了若干字符串,大量拼接时很消耗性能 - 字符串拼接场景下,编译器会将
String
自动转换为StringBuilder
,但建议手动转换
- 字符串创建后不可变,因此使用
-
StringBuffer
:是变量,适合大量使用拼接字符串时使用,可以大幅度提高拼接效率,但线程不安全 -
StringBuilder
:线程安全,效率略低于StringBuffer
public final class String{
public String(byte bytes[], int offset, int length) {...} //offset是字节数组开始游标,length是读取长度
public static String valueOf(Object obj){...} //返回字符串形式
public String split(String regex){...} //根据regex正则匹配,返回一个字符串数组
public int length(){...} //返回字符串长度
public boolean isEmpty() {...} //字符串是否是""
public boolean matches(String regex){...} //判断regex模板是否匹配字符串
public boolean equals(Object anObject){...} //判断字符串字面值中是否相同
public String trim(){...} //去除字符串首尾空格,返回新字符串
public char charAt(int index){...} //返回指定游标位上的字符
public String substring(int beginIndex, int endIndex){...} //返回从beginIndex到endIndex(不含)间的字符串
public int indexOf(String regex){...} //返回第一次出现匹配regex正则的子字符串位置游标
public String replace(char oldChar, char newChar){...} //将字符串中所有oldChar字符替换为newChar,返回新字符串
public String replaceAll(String regex, String replacement){...} //将所有匹配regex正则的字串替换为replacement
public boolean startsWith(String prefix){...} //判断字符串是否以prefix开头
public boolean endsWith(String suffix){...} //判断字符串是否以suffix结尾
public String toUpperCase(){...} //将字符串中所有的英文字母替换为其大写形式,返回新字符串
public String toLowerCase(){...} //将字符串中所有的英文字母替换为其小写形式,返回新字符串
public char[] toCharArray(){...} //返回字符串的字符数组形式
public byte[] getBytes() {...} //返回字符串对应的字节数组,默认编码方式为UTF-8
public byte[] getBytes(Charset charset) {...} //返回字符串对应的字节数组,指定编码方式
}
public final class StringBuilder{
StringBuilder append(String str){...} //末尾拼接str
StringBuilder replace(int start, int end, String str){...} //将字符串从start到end(不含)之间的字符串替换位str
StringBuilder delete(int start, int end){...} //删除字符串start到end(不含)之间的字符串
String toString(){...} //返回对应的String类型
}
字符串格式化
-
String.format()
格式化:数据类型匹配占位符 说明 %s
表示一个字符串 %n
平台无关的换行符(推荐替代 \n
)%d
表示一个十进制整数 %f
表示一个浮点数 %o
表示一个八进制整数 %c
表示一个字符 %b
表示一个布尔值
System.Out.println(String.format("i am %s, i am %d, i have %f", "wyh", 25, 100.54));
System.Out.printf("i am %s, i am %d, i have %f%n", "wyh", 25, 100.54); //简化写法
MessageFormat.format()
格式化:游标匹配,从0开始
System.Out.println(MessageFormat.format("i am {0}, i am {1}, i have {2}", "wyh", 25, 100.54));
时间
-
LocalDateTime
和Date
:LocalDateTime
直接获取本地时间,解决了时区问题LocalDateTime
表示的时间更直观,有更方便的APIDate
线程不安全,LocalDateTime
线程安全- 高版本JDK已将
Date
部分API废弃
-
Instant
:适用于时间戳形式的时间数据,业务场景中常使用Long
类型传参,以Instant
接收
Date
public class Date{
public Date(){...} //直接new Date()即返回当前系统时间对象
public Date(long date){...} //可以指定创建时间new Date(UTC时间戳)
long getTime(){...} //返回UTC时间戳
boolean before(Date when){...} //如果时间对象在when之前,则返回true
}
//直接打印Date不能直观展示时间,可以借助SimpleDateFormat格式化
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); //建议统一使用此格式
String formatTime = simpleDateFormat.format(date); //2025/05/16 01:53:33
LocalDateTime
public final class LocalDateTime{
static LocalDateTime now(){...} //获取当前的本地时间,精确到纳秒
static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute, int second){...} //指定创建
static LocalDateTime parse(String time){...} //根据"yyyy-MM-ddThh:MM:ss"格式的字符串创建时间对象
int getYear() / getMonthValue() / getDayOfMonth() / getHour() / getMinute() / getSecond(){...} //获取年月日时分秒
boolean isBefore(LocalDateTime other){...} //如果时间对象比other早,则返回true
boolean isEqual(LocalDateTime other){...} //判断时间是否相同
static LocalDateTime ofInstant(Instant instant, ZoneId zone){...} //转换instant为LocalDateTime格式
Instant toInstant(ZoneOffset offset){...} //转换Instant为LocalDateTime格式
}
//直接打印LocalDateTime已经可以很好地展示时间了,如果还想再格式化可以借助DateTimeFormatter
LocalDateTime time = LocalDateTime.of(2026, Month.JUNE, 21, 22, 37, 5);
String formatTime = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Instant
public final class Instant{
static Instant now(){...} //获取当前的本地UTC时间戳,精确到纳秒
static Instant ofEpochSecond(long epochSecond){...} //指定创建时间(秒)
public static Instant ofEpochMilli(long epochMilli) {...} //指定创建时间(毫秒)
public static Instant ofEpochSecond(long epochSecond, long nanoAdjustment) {...} //指定创建时间,秒+纳秒
public long getEpochSecond() {...} //转换为秒数时间戳
public long toEpochMilli() {...} //转换为毫秒数时间戳
public int getNano() {...} //获取纳秒部分数值
public long getLong(TemporalField field) {...} //转换为Long时间戳,field表示单位,可以是ChronoField类的静态成员
}
随机数
Radom
:线程不安全,性能高,适合普通场景ThreadLocalRandom
:封装了Random
的线程安全版本,适合并发环境下的普通场景SecureRandom
:线程安全,随机性更高,但性能低,适合安全敏感场景,例如密码令牌
Random
public class Random{
public Random(long seed){...} //创建Random对象时可以指定种子,默认seed为System.nanoTime()
int nextInt(){...} //生成一个随机int值
int nextInt(int bound){...} //生成一个[0,bond)间的随机int值
float/double nextFloat()/nextDouble(){...} //生成一个随机浮点数
long nextLong(){...} //生成一个随机long值
boolean nextBoolean(){...} //生成一个随机布尔值
}
ThreadLocalRandom
- 【强制】避免
Random
实例被多线程使用,会因竞争同一seed
导致的性能下降
public class ThreadLocalRandom extends Random {
public static ThreadLocalRandom current() {...} //获取线程安全的Random对象
int nextInt(){...} //生成一个随机int值
int nextInt(int bound){...} //生成一个[0,bond)间的随机int值
float/double nextFloat()/nextDouble(){...} //生成一个随机浮点数
long nextLong(){...} //生成一个随机long值
boolean nextBoolean(){...} //生成一个随机布尔值
}
SecureRandom
public class SecureRandom{
int nextInt(){...} //生成一个随机int值
int nextInt(int bound){...} //生成一个[0,bond)间的随机int值
float/double nextFloat()/nextDouble(){...} //生成一个随机浮点数
long nextLong(){...} //生成一个随机long值
boolean nextBoolean(){...} //生成一个随机布尔值
}
正则表达式
-
易错点
- 正则表达式特殊处理空白字符,因此匹配前后有空白字符必须显式指明
- 空白字符包括空格、制表符
\t
、换行符\n
、回车符\r
等;\\s
匹配所有空白字符;直接空格字符匹配空格 ^
和$
分别匹配整个字符串的开头和结尾;如果需要匹配行首或行尾(多行模式),需要启用Pattern.MULTILINE
标志- 如果需要匹配特殊字符本身,必须用
\\
转义,例如\\\n
表示\n
- 元字符(如
.
、*
、+
等)有默认的匹配行为,需要显式调整以匹配特定模式
String.matches
System.out.println("2023-10-25".matches("(\\d{4})-(\\d{2})-(\\d{2})")); //true,此方法只能做判断功能
Pattern、Matcher
Pattern
:用于创建正则表达式的模板,用于生成Matcher对象Matcher
:用于对字符串进行匹配、查找、替换等操作
public final class Pattern{
static Pattern compile(String regex){...} //根据模板regex创建Pattern对象
static boolean matches(String regex, String input){...} //隐式创建Pattern对象,根据regex模板是否正则匹配字符串input
Matcher matcher(String input){...} //根据Pattern对象和匹配字符串input,生成对应的Matcher引擎对象,便于再次操作
}
public final class Matcher{
boolean matches(){...} //检查整个输入字符串是否完全正则匹配
boolean find(){...} //检查输入字符串中是否有字串正则匹配,多次调用会遍历除所有匹配的字串
String group(){...} //返回上次匹配到的字符串(例如find方法匹配到的字串)
String group(int group){...} //返回上次匹配到的字符串中,将正则符号匹配到的所有字串形成捕获组,返回捕获组指定内容
int groupCount(){...} //返回捕获组长度
boolean lookingAt(){...} //检查输入字符串开头是否匹配正则表达式
String replaceAll(String replacement){...} //将输入字符串中所有匹配到的字串全部替换为replacement
String replaceFirst(String replacement){...} //将输入字符串中第一个匹配到的字串替换为replacement
Matcher reset(String newRegex){...} //将输入字符串重置修改为newRegex
}
举例:匹配日期数据
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher("今天是2023-10-25,明天是2023-10-26");
if (matcher.find()) { //对匹配成功的字符串进行操作
System.out.println("完整日期: " + matcher.group()); // 2023-10-25
System.out.println("年份: " + matcher.group(1)); // 第一个正则符号匹配到的字串为2023
System.out.println("月份: " + matcher.group(2)); // 第二个正则符号匹配到的字串为10
System.out.println("捕获组数量: " + matcher.groupCount()); // 3
System.out.println("完整日期: " + matcher.group()); // 2023-10-26
}
枚举
语法
-
枚举就是常量的集合,使用枚举保证了代码安全性和可读性
- 枚举是天然的单例,防止了序列化和反射攻击
- 枚举会在编译时检查类型,防止数据类型引起的异常
-
枚举类不能被继承
-
枚举类可以实现其他接口,且每一个枚举项都需要实现
public enum 枚举类名{
枚举项1(成员列表), 枚举项2(成员列表); //枚举项必须在第一行,枚举项之间逗号隔开
private final 成员声明;
public 自定义方法; //枚举可以有自定义方法,枚举项.自定义方法名
public static 自定义方法; //枚举项可以有自定义静态方法,枚举类.自定义静态方法名
private abstract 抽象方法; //枚举项可以有抽象方法,但定义枚举类时每个枚举项就要实现
构造方法; //用于初始化枚举项的成员
public getter; //getter方法用于获取成员
}
举例
public enum People {
ZS(1, "张三") , LS(2, "李四") ;
private final Integer id; //成员
private final String name;
People(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
}
数组
- 数组是引用数据类型,使用
==
比较的是地址,逻辑比较可使用Array.equals
方法
语法
//直接声明并赋值
Integer[] integers1 = {1, 2};
//先声明长度,再初始化值
Integer[] integer2 = new Integer[2];
integer2[0] = 1;
integer2[1] = 2;
//先声明,再单独初始化
Integer[] integers3;
integers3 = new Integer[2];
integer3[0] = 1;
integer3[1] = 2;
Arrays类
- Arrays类是Java用于操作数组的工具类,数组自身没有内置的方法
public class Arrays {
public static String toString(Object[] a) {...} //返回数组的字符串形式
public static boolean equals(Object[] a, Object[] a2) {...} //比较两个数组的字面值
public static void sort(int[] a) {...} //默认 升序/字典 序升序排序
public static <T> void sort(T[] a, Comparator<? super T> c) {...} //自定义排序,Comparator返回负数表示o1在o2前面
public static <T> void parallelSort(T[] a, Comparator<? super T> cmp){...} //并行算法排序,适用于大数据
public static <T> T[] copyOf(T[] original, int newLength) {...} //将original数组前newLength个元素组成一个新数组
public static <T> List<T> asList(T... a) {...} //将数组转化为List,注意这里数组要用包装类,否则返回的List长度为1
public static void fill(Object[] a, Object val) {...} //将数组所有元素都变成val
}
举例:数组排序
String[] strings = {"bbc", "aca", "rf"};
Arrays.sort(strings); //默认按字典序升序排序:[aca, bbc, rf]
Arrays.sort(strings, (o1, o2) -> {o1.length() >= o2.length() ? 1 : -1}); //自定义按长度升序排序:[rf, bbc, aca];
Arrays.sort(strings, (o1, o2) -> o1.length() - o2.length()); //进一步简化
集合框架(Collection)
-
集合的特性
- 集合可以动态扩容
- 集合有丰富的操作方法,更适合高效地处理数据
- 集合可以更灵活方式的存储数据
List接口
-
基本特性:元素有游标,基于索引查找,可重复
-
动态数组和链表
- 动态数组有扩容机制,默认扩容1.5倍;链表无容量限制
- 动态数组查询效率高;链表插入删除效率高,头尾元素操作效率极高
ArrayList
-
应用场景
- 线程不安全
- 读操作效率高,适合频繁地随机读取
- 可以动态扩容,适合数据量不可预测的场景
public class ArrayList<E>{
public boolean add(E e) {...} //默认尾插
public void add(int index, E element) {...} //将element插入到index游标处
public E get(int index) {...} //获取index游标处的元素
public E remove(int index) {...} //删除index游标处的元素
public boolean remove(Object obj) {...} //删除队列中第一次出现obj的元素
public boolean removeIf(Predicate<? super E> filter) {...} //删除断言为true的元素
public E set(int index, E element) {...} //将index游标除元素修改为element
public void clear() {...} //清空列表
public int size() {...} //获取队列长度
public boolean contains(Object o) {...} //判断列表中是否有o元素
public List<E> subList(int fromIndex, int toIndex) {...} //返回从fromIndex到toIndex(不含)间的列表
Object[] toArray(T[] array){...} //转换为数组
}
LinkedList
-
应用场景
- 线程不安全
- 写操作效率高,频繁地插入删除操作
- 容量无限制,适合总数据容量要求较大的场景
- 可以用于实现栈或队列
public class LinkedList<E>{
public void addFirst(E e) {...} //头插
public boolean add(E e) {...} //默认尾插
public void add(int index, E element) {...} //将element插入到index游标处
public E getFirst() {...} //获取头元素
public E getLast() {...} //获取尾元素
public E get(int index) {...} //获取index游标处的元素
public E remove() {...} //默认删除头元素
public E removeLast() {...} //删除尾元素
public E remove(int index) {...} //删除index游标处的元素
public E set(int index, E element) {...} //将index游标除元素修改为element
public boolean contains(Object o) {...} //判断列表中是否有o元素
}
CopyOnWriteArrayList
-
应用场景
- 线程安全,适合作为以列表形式保存的公共资源
- 使用写时复制技术,适合写少读多
public class CopyOnWriteArrayList<E> {
public boolean add(E e) {...} //默认尾插
public void add(int index, E element) {...} //将element插入到index游标处
public E get(int index) {...} //获取index游标处的元素
public E remove(int index) {...} //删除index游标处的元素
public E set(int index, E element) {...} //将index游标除元素修改为element
public boolean contains(Object o) {...} //判断列表中是否有o元素
}
ConcurrentLinkedDeque
-
应用场景
- 使用CAS算法,保证线程安全同时兼顾了读写性能
- 本质上是链表,更适合读少写多的场景
public class LinkedList<E>{
public boolean add(E e) {...} //默认尾插
public void addFirst(E e) {...} //头插
public void addLast(E e) {...} //尾插
public E getFirst() {...} //获取头元素
public E getLast() {...} //获取尾元素
}
Map接口
-
Map
接口基本特征- 元素以键值对形式存储,可以实现高级功能
- 元素无游标,键不可重复
HashMap
-
应用场景
- 线程不安全
HashMap
基于哈希表实现,查找效率最高,适合随机查找、统计数据- 可以动态扩容,适合数据量不可预测的场景
public class HashMap<K,V>{
public V put(K key, V value) {...} //插入元素,如果键重复就覆盖
public V putIfAbsent(K key, V value) {...} //键不存在时才插入
public V get(Object key) {...} //根据键获取值
public V getOrDefault(Object key, V defaultValue) {...} //根据键获取值,如果没有键就返回defaultValue
public V remove(Object key) {...} //根据键删除键值对
public boolean containsKey(Object key) {...} //判断是否含有键key
public boolean containsValue(Object value) {...} //判断是否含有值value
public int size() {...} //返回map长度
public void clear() {...} //清空map
public Set<K> keySet() {...} //返回所有键组成的Set,用于遍历Map的键
public Collection<V> values() {...} //返回所有值组成的集合,用于遍历Map的值
public Set<Map.Entry> entrySet() {...} //返回所有键值对组成的Set,用于遍历Map的键值
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {...} //插入计算出的值
}
LinkedHashMap
-
应用场景
- 线程不安全
LinkedHashMap
维护了一个双向链表,可以设置插入顺序和访问顺序- 遍历时会遵循元素插入/访问的顺序
public class LinkedHashMap<K,V>{
public LinkedHashMap() {...} //默认initialCapacity=16,loadFactor=0.75,维护尾插
//true:将访问过的元素放在最后 ; accessOrder=false:put采用尾插,默认为false
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {...}
public V put(K key, V value) {...} //插入元素,如果键重复就覆盖
public V get(Object key) {...} //根据键获取值
public V remove(Object key) {...} //根据键删除键值对
public boolean containsKey(Object key) {...} //判断是否含有键key
public boolean containsValue(Object value) {...} //判断是否含有值value
}
TreeMap
-
应用场景
- 线程不安全
TreeMap
基于红黑树实现,默认根据键升序,也可以自定义排序- 遍历时会遵循排序规则顺序
public class TreeMap<K,V>{
public TreeMap(Comparator<? super K> comparator) {...} //自定义排序方法构造TreeMap
public TreeMap() {...} //默认构造排序为升序
public V put(K key, V value) {...} //插入元素,如果键重复就覆盖
public V get(Object key) {...} //根据键获取值
public V remove(Object key) {...} //根据键删除键值对
public boolean containsKey(Object key) {...} //判断是否含有键key
public boolean containsValue(Object value) {...} //判断是否含有值value
}
ConcurrentHashMap
-
应用场景
- 线程安全,适合作为以键值对形式保存的公共资源
- 采用CSA算法,兼顾读写性能,高并发环境下推荐使用
public class ConcurrentHashMap<K,V>{
public V put(K key, V value) {...} //插入元素,如果键重复就覆盖
public V putIfAbsent(K key, V value) {...} //键不存在时才插入
public V get(Object key) {...} //根据键获取值
public V getOrDefault(Object key, V defaultValue) {...} //根据键获取值,如果没有键就返回defaultValue
public V remove(Object key) {...} //根据键删除键值对
public boolean containsKey(Object key) {...} //判断是否含有键key
public boolean containsValue(Object value) {...} //判断是否含有值value
public long mappingCount() {...} //高并发场景下map长度可能很大,计算map长度
}
Set接口
-
Set
接口基本特征- 元素无游标,不可重复
Set
本质上是没有值的Map
HashSet
-
应用场景
- 线程不安全
- 基于哈希表实现,查找效率最高,适合快速去重
public class HashSet<E>{
public HashSet(Collection c) {...} //构造入参可以是List,快速去重
public boolean add(E e) {...} //插入元素,默认尾插
public boolean remove(Object o) {...} //删除元素
public boolean contains(Object o) {...} //查询是否含有元素
public void clear() {...} //清空Set
public int size() {...} //返回Set长度
public Object[] toArray() {...} //转化为数组
}
TreeSet
-
应用场景
- 线程不安全
- 和
TreeMap
同理,可以设置排序 - 遍历时会遵循设置的排序顺序
public class TreeSet<E>{
public TreeSet() {...} //默认升序
public TreeSet(Comparator<? super E> comparator) {...} //自定义排序
public boolean add(E e) {...} //插入元素
public boolean remove(Object o) {...} //删除元素
public boolean contains(Object o) {...} //查询是否含有元素
}
LinkedHashSet
-
应用场景
- 线程不安全
- 和
LinkedHashMap
不同的是,LinkedHashSet
只能维护插入顺序 - 遍历时会遵循设置的操作顺序
public class LinkedHashSet<E>{
public LinkedHashSet() {...} //默认super(16, .75f)
public boolean add(E e) {...} //插入元素,只能尾插
public boolean remove(Object o) {...} //删除元素
public boolean contains(Object o) {...} //查询是否含有元素
}
CopyOnWriteArraySet
-
应用场景
- 线程安全
- 采用写时复制技术,适合读多写少的场景
- 写操作较为频繁时,可以使用
ConcurrentHashMap.newKeySet()
public class CopyOnWriteArraySet<E>{
public boolean add(E e) {...} //插入元素,默认尾插
public boolean remove(Object o) {...} //删除元素
public boolean contains(Object o) {...} //查询是否含有元素
}
Collections类
Collections
类是用于处理集合框架的工具类
public class Collections {
public static <T> void sort(List<T> list) {...} //默认升序/字典序升序,只能用于List
public static <T> void sort(List<T> list, Comparator<? super T> c) {...} //自定义排序,只能用于List
public static void reverse(List<?> list) {...} //反转,只能用于List
public static <T> List<T> singletonList(T o) {...} //返回一个单元素不可变的List
public static <T> Set<T> singleton(T o) {...} //返回一个单元素不可变的Set
public static <K,V> Map<K,V> singletonMap(K key, V value) {...} //返回一个单元素不可变的Map
}
举例:List排序
List<User> list = Arrays.asList(new User(1L, "a"), new User(2L, "abc"), new User(3L, "ab"));
//自定义,按User的名字长度倒序:[User(id=2, name=abc), User(id=3, name=ab), User(id=1, name=a)]
Collections.sort(list, (user1, user2) -> user2.getName().length() - user1.getName().length());
集合遍历
-
索引for循环:适合需要索引的场景
-
迭代器循环:适合遍历集合时安全删除元素;特别的,
Map
需要通过entrySet()
、keySet()
、values()
获取迭代器
public interface Collection<E> {
Iterator<E> iterator(); //获取迭代器
}
public interface Iterator<E> {
boolean hasNext(); //迭代器当前指向元素是否还存在
E next(); //获取当前迭代器指向的元素
void remove(); //安全删除当前迭代器指向的元素
}
HashMap<Integer, String> map = new HashMap<Integer, String>() {{
put(1, "wyh1");
put(2, "wyh1");
put(3, "wyh1");
put(4, "wyh1");
}};
Iterator<Integer> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
if (iterator.next() > 3){
iterator.remove();
}
}
- 增强for循环:效率较高,适合仅需访问元素的场景
ArrayList<User> userList = new ArrayList<User>() {{
add(new User(1L, "wyh1"));
add(new User(2L, "wyh2"));
add(new User(3L, "wyh3"));
add(new User(4L, "wyh4"));
}};
for (User user : userList) {
System.out.println(user);
}
- foreach方法:适合快速编写遍历逻辑,减少样板代码
ArrayList<User> userList = new ArrayList<User>() {{
add(new User(1L, "wyh1"));
add(new User(2L, "wyh2"));
add(new User(3L, null));
add(new User(4L, "wyh4"));
}};
userList.forEach(user -> {
if (user == null) {
throw new RuntimeException("姓名不能为null");
}
});
Lambda表达式
-
函数式接口:只有一个抽象方法的接口
-
目的:更简洁地创建函数式接口的实现类
-
常见的函数式接口
public interface Consumer<T> { //消费型 void accept(T t); } public interface Supplier<T> { //提供型 T get(); } public interface Function<T, R> { //函数型 R apply(T t); } public interface Predicate<T> { //断言型 boolean test(T t); }
Lambda创建函数式接口实例
//标准语法,注意入参不需要声明类型
函数式接口 实例 = (抽象方法参数) -> {实现方法体};
//如果入参只有一个
函数式接口 实例 = 参数 -> {实现方法体};
//如果方法体只有一条语句,例如(x,y) -> x+y
函数式接口 实例 = (抽象方法参数) -> 方法体不加return;
//如果方法体只有一条语句,且就是某一个方法
函数式接口 实例 = 对象::方法名;
举例:函数式接口实现方式对比
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.forEach(new Consumer<Integer>() { //原始表达方式
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
list.forEach((element) -> { //标准lambda
System.out.println(element);
});
list.forEach(element -> {
System.out.println(element); //单参数简化lambda
});
list.forEach(System.out::println); //简化为lambda引用
Stream流
-
目的
- 对集合元素进行操作时,简化代码并提高代码可读性
Stream
流提供了多线程处理数据方法,大数据场景下的效率较高
创建Stream流
public interface Collection<E>{
default Stream<E> stream() {...} //创建一个串行流
default Stream<E> parallelStream() {...} //创建一个多线程并行流,注意这样不能保证操作顺序
}
中间操作
public interface Stream<T>{
Stream<T> distinct() {...} //去重
Stream<T> limit(long maxSize) {...} //只保留流中前maxSize元素
Stream<T> skip(long n) {...} //去除流中前n个元素
Stream<T> filter(Predicate<? super T> predicate) {...} //去除流中断言尾false的元素
Stream<T> sorted() {...} //升序排序
Stream<T> sorted(Comparator<? super T> comparator) {...} //自定义排序
Stream<R> map(Function<R> mapper) {...} //将元素转化为mapper接口返回的值
Stream<R> flatMap(Function<R> mapper) {...} //将嵌套结构展平为单层流(如[["a","b"],["c"]]转化为["a","b","c"])
}
操作终端
Stream
流本身不存储数据,终端操作才会触发Stream
的实际执行,返回相应结果collect(Collectors.groupingBy(Function fun))
:返回Map
,键是fun返回值,值是相同fun返回值的流元素组成的List
public interface Stream<T>{
//返回集合实例,参数:Collectors.toList()、Collectors.toSet()、Collectors.groupingBy(Function fun)
R collect(Collector<R> collector) {...}
long count() {...} //返回流中元素个数
Optional<T> reduce(BinaryOperator<T> accumulator) {...} //不设定初始值,将流中元素依次相互操作最终生成一个值
T reduce(T identity, BinaryOperator<T> accumulator) {...} //初始值为identity,再将流中元素依次相互操作最终生成一个值
Optional<T> max(Comparator<? super T> comparator) {...} //根据Comparator实现类返回值,返回流中最大值的Optional
Optional<T> min(Comparator<? super T> comparator) {...} //根据Comparator实现类返回值,返回流中最小值的Optional
Optional<T> findFirst() {...} //返回流中第一个元素的Optional
void forEach(Consumer<? super T> action) {...} //遍历操作
}
举例:根据id奇偶将用户分组
ArrayList<User> userList = new ArrayList<User>() {{
add(new User(1L, "Alice"));
add(new User(2L, "Bob"));
add(new User(3L, null));
add(new User(4L, "Charlie"));
add(new User(null, "Anna"));
}};
userList.removeIf(user -> user.getName() == null); //除去Name为null的用户,removeIf去除断言为true的元素
Map<String, List<User>> userMap = userList.stream()
.filter(user -> user.getId() != null) //除去id为null的用户,filter去除断言为false的元素
.collect(Collectors.groupingBy((user) -> {
long l = user.getId() % 2;
if (l == 0) {
return "偶数";
}
return "奇数";
})); //用户id按奇偶分为两组
System.out.println(userMap); //{偶数=[User(id=2,name=Bob), User(id=4,name=Charlie)], 奇数=[User(id=1,name=Alice)]}
Optional类
-
Optional
类的目的- 替代了
null
检查,避免null
异常 - 常与
Stream
结合,提高了代码简洁性和可读性
- 替代了
创建Optional
public final class Optional<T> {
public static <T> Optional<T> of(T value) {...} //创建值为value的Optional容器,value不能为null
public static<T> Optional<T> empty() {...} //创建空容器
public static <T> Optional<T> ofNullable(T value) {...} //如果value为null则创建空容器
}
中间操作
public final class Optional<T> {
public<U> Optional<U> map(Function<U> mapper) {...} //将元素转化为mapper接口返回的值
public<U> Optional<U> flatMap(Function<Optional<U>> mapper) {...} //将嵌套结构展平为单层(如User(1,"wyh")变为1)
}
终端操作
public final class Optional<T> {
public T get() {...} //返回容器中的值,如果没有抛异常
public T orElse(T other) {...} //返回容器中的值,没有则返回other
public T orElseGet(Supplier<? extends T> other) {...} //返回容器中的值,没有则返回提供型接口实现类的返回值
public T orElseThrow(Supplier exceptionSupplier) throws X {...} //返回容器中的值,没有则抛异常
public boolean isPresent() {...} //容器有值返回true
public void ifPresent(Consumer<? super T> consumer) {...} //容器有值就执行消费型接口的实现类
}
泛型
-
泛型的目的
- 数据类型安全:泛型在编译前已经确认的数据类型,避免了数据类型风险
- 代码复用:使用泛型通配符,避免了实现通用功能时出现重复代码
泛型集合
List<String> fruits = new ArrayList<>();
泛型类,泛型方法/接口
-
泛型类型参数:任意大写字母,都可以视为一个数据类型,但没有实际的约束力
- E - Element (表示集合的元素)
- T - Type(表示Java 类)
- K - Key(表示键)
- V - Value(表示值)
- N - Number(表示数值类型)
-
<T extends Number>
:上界泛型类型参数,注意没有下界泛型类型参数
public class Book<T,N extends Number> { //泛型集合
private T id;
public void method1(N n){ //泛型方法
...
}
public <E> void method2(E e){ //泛型方法,局部声明泛型
...
}
}
public interface IUserService<T> { //泛型接口,实现类将泛型通配符直接换成具体类型即可
T getUserById(Long id);
}
泛型通配符
- PECS 原则:生产者(提供数据方)用
extends
,消费者(接收处理数据方)用super
<?>
是泛型通配符,表示未知的类型,常使用其下界形式,一般和集合搭配使用
public class Demo {
public void method1(List<? extends Number> list){ //输入列表元素可以是Number、Integer、Long等
}
public void method2(List<? super Number> list){ //输入列表元素可以是Number、Object
}
}
反射
-
目的:编译时动态获取信息
-
原理:JVM运行时,会对所有的对象都创建一个
Class
对象,这个Class
对象包含了类的所有信息,因此可以动态地获取类的信息 -
应用场景:动态创建实例,减少重复的代码
-
工作中反射的例子
- Json反序列化技术,将Jaon字符串转化为相应实例,使用反射就可以一次覆盖所有实体类的反序列化方法
- AOP使用JDK动态代理或CGLIB,通过反射创建目标对象的代理
- IOC识别带有
@Component
、@Service
等注解的类代替创建实例,覆盖所有类的new方法 - JMockit使用反射创建Mock对象,覆盖对应类的new方法
获取Class实例
//方法1(推荐)
Class<?> aClass = 类.class;
//方法2
Class<?> aClass = Class.forName("对象全类名");
//方法3
Class<?> aClass = 对象.getClass();
获取成员信息
-
成员信息分为三类
Field
:成员信息对象Method
:方法信息对象Constructor
:构造器成员对象
public final class Class<T>{
public native Class<? super T> getSuperclass(); //获取父类
public Field[] getFields(){...} //获取所有公共属性
public Field getField(String name){...} //获取指定的公共属性
public Field[] getDeclaredFields(){...} //获取所有的属性(包含私有)
public Field getDeclaredField(String name){...} //获取指定的属性(包含私有)
public Method[] getMethods(){...} //获取所有公共方法
public Method getMethod(String name, Class<?>... parameterTypes){...} //获取指定公共方法,参数是入参类型.class
public Method[] getDeclaredMethods(){...} //获取所有的方法(包含私有)
public Method getDeclaredMethod(String name, Class<?>... parameterTypes){...} //获取指定的方法(包含私有)
public Constructor<?>[] getConstructors(){...} //获取所有的公共构造方法
public Constructor<T> getConstructor(Class<?>... parameterTypes){...} //获取指定公共构造方法,无入参即无参构造
public Constructor<?>[] getDeclaredConstructors(){...} //获取所有的构造方法(包含私有)
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes){...} //获取指定构造方法(私有)
}
创建对象
-
一般步骤
- 创建
Class
实例 - 获取
Constructor
实例 - 调用
Constructor.newInstance()
方法创建对象
- 创建
-
不建议使用
Class.newInstance()
方创建对象
public final class Constructor<T>{
public T newInstance(Object ... initargs){...} //先获取Constructor对象,再据此创建对象,空参构造可以无入参
}
调用方法
-
一般步骤
- 创建
Class
实例 - 通过
Constructor
创建对象 - 获取
Method
实例 - 调用
Method.invoke()
方法
- 创建
public final class Method{
public Object invoke(Object obj, Object... args) {...} //第一个参数是创建的对象,之后是方法的入参,无参为null
public void setAccessible(boolean flag) {...} //如果Method对象对应的方法权限是private,调用前需要先设置为true
public Type[] getGenericParameterTypes() {...} //获取方法入参类型
public TypeVariable<Method>[] getTypeParameters() {...} //获取方法的泛型信息
}
举例:覆盖测试类异常情形
- Dao层要对异常情况做检查(例如连接失败、SQL非法等),确保异常出现时都会抛出
- 所有的Dao类都要专门做一个
DaoExceptionTest
类,测试其中所有方法是否抛出异常,这很麻烦- 要求只做一个
AllDaoExceptionTest
类,模拟数据库异常时,利用反射覆盖所有Dao类的所有方法,实现“代码瘦身”
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId> <!--Reflections封装了一些反射操作,用于扫描类和配置文件-->
<version>0.10.2</version>
</dependency>
public class DaoSQLExceptionTest {
static private final Set<Class<? extends BaseDao>> daoClasses;
static private final Map<Type, Object> defaultVlueMap = new HashMap<>();
static {
// 创建 Reflections 实例并指定扫描器
Reflections reflections = new Reflections("com.wyh.dao");
// getSubTypesOf:获取所有子类
daoClasses = reflections.getSubTypesOf(BaseDao.class);
//初始化参数Map
defaultVlueMap.put(Integer.class, 0);
defaultVlueMap.put(int.class, 0);
defaultVlueMap.put(Long.class, 0L);
defaultVlueMap.put(long.class, 0L);
defaultVlueMap.put(Float.class, 0.0F);
defaultVlueMap.put(float.class, 0.0F);
defaultVlueMap.put(Double.class, 0.0);
defaultVlueMap.put(double.class, 0.0);
defaultVlueMap.put(Character.class, 'a');
defaultVlueMap.put(char.class, 'a');
defaultVlueMap.put(String.class, "abc");
}
@Injectable
private DruidDataSource mockDataSource;
@BeforeMethod
public void setUp() throws SQLException {
new Expectations() {{
mockDataSource.getConnection(); //模拟连接异常
result = new SQLException("Simulated connection failure");
}};
}
@Test
public void testException() throws Exception {
for (Class<? extends BaseDao> daoClass : daoClasses) {
// 创建 DAO 实例
BaseDao dao= daoClass.getDeclaredConstructor().newInstance();
// 使用反射获取所有方法
for (Method method : daoClass.getDeclaredMethods()) {
try {
method.invoke(dao, getMethodParameters(method)); // 验证异常
Assert.fail(); //防止mock失效
} catch (Exception e) {
Assert.assertTrue(e.getCause() instanceof PersistenceException);
}
}
}
}
private Object[] getMethodParameters(Method method) {
Type[] parameterTypes = method.getGenericParameterTypes(); //获取入参类型
Object[] parameters = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) { //根据入参类型构建实参数组
Type parameterType = parameterTypes[i];
parameters[i] = defaultVlueMap.getOrDefault(parameterType, null); //不在Map列举范围内的类型入参null
}
return parameters;
}
}
File类
File
实例指代一个文件或目录,其方法可以对文件整体进行操作,但不能访问文件的内容,需要通过IO流才可以读写文件中的内容
创建File
-
路径格式写法:推荐使用相对路径,根目录就是工程根路径(即.idea所在目录)
- 正斜杠写法
"resource/file.txt"
- 反斜杠写法
"resource\\file.txt"
- File类提供了系统分隔符(推荐)
"resource"+File.separator+"file.txt"
- 正斜杠写法
public class File{
public File(String pathname) {...} //创建File实例,系统会识别是绝对路径还是相对路径;若是相对路径,则根目录是当前项目
public File(String parent, String child) {...} //相对路径创建File实例,根目录就是parent
public File(File parent, String child) {...} //相对路径创建File实例,根目录就是parent
}
操作File
public class File{
public boolean createNewFile() throws IOException {...} //在系统中真正创建文件,如果已存在返回false
public boolean mkdir() {...} //创建目录,如果已存在返回false
public boolean exists() {...} //检查路径下是否存在文件/目录
public boolean isFile() {...} //检查路径下存在且是文件
public boolean isDirectory() {...} //检查路径下存在且是目录
public long length() {...} //获取文件大小字节,目录返回0
public boolean delete() {...} //删除文件或空目录
public String getName() {...} //获取文件或目录名
public String getPath() {...} //获取相对路径
public String getAbsolutePath() {...} //获取绝对路径(推荐)
//创建临时文件,JVM关闭时销毁,prefix为文件名至少三个字符,suffix为后缀名,默认.tmp,目录系统自动指定
public static File createTempFile(String prefix, String suffix) {...}
public static File createTempFile(String prefix, String suffix, File directory) {...} //手动指定临时文件目录
public File[] listFiles() {...} //返回目录下的所有子文件和子目录,如果对象是文件则返回null
public File[] listFiles(FileFilter filter) {...} //根据FileFilter接口实现类返回boolean,获取筛选的File数组
}
举例:遍历所有文件
需求:查询多层级目录中有多少文件
public class Main {
static Long fileNum = 0L;
public static void searchFiles(File file) { //递归,深度优先
//递归终点
if (!file.isDirectory()) {
fileNum ++;
return;
}
//递归调用
for (File f : file.listFiles()) {
searchFiles(f);
}
}
public static void main(String[] args) throws Exception {
searchFiles(new File("E:\\JavaCourse"));
System.out.println(fileNum);
}
}
Files类
- Files类是对于File的核心工具类,提供了丰富的静态方法
- 常见的文件操作建议使用此工具类,因为NIO方式更高效
List<String> lines = Files.readAllLines(Paths.get("example.txt")); //读取所有行
byte[] bytes = Files.readAllBytes(Paths.get("example.txt")); //读取所有字节
Files.write(Paths.get("output.txt"), List.of("Hello", "World"), StandardOpenOption.CREATE); //写入字符串
Files.write(Paths.get("binary.dat"), "Hello, World!".getBytes()); //写入字节数组
Files.size(Paths.get("example.txt")); //获取文件大小
Files.copy(Paths.get("source.txt"), Paths.get("target.txt")); //复制文件
Files.move(Paths.get("old.txt"), Paths.get("new.txt")); //移动文件
Files.delete(Paths.get("example.txt")); //删除文件
Stream<Path> stream = Files.walk(Paths.get("path/to/dir")); //递归遍历所有文件,返回Stream流
Path类
-
Path
类用于防护攻击者利用../
和分隔符组合尝试跳出或跳进目标目录 -
防护路径攻击步骤
- 使用
Paths.get()
表示路径 - 用户输入路径拼接使用
Path.resolve()
+Path.normalize()
,防止路径遍历 - 限制文件操作范围
Path.startsWith()
,确保路径在允许的目录内
- 使用
// Paths.get()创建Path对象,如果传入多个参数会自动拼接,且自动处理不同操作系统的路径分隔符(/ 或 \)
Path path1 = Paths.get("C:/data/file.txt"); // Windows 路径
Path path2 = Paths.get("/home/user/docs/file.txt"); // Linux/macOS 路径
Path path3 = Paths.get("data", "subdir", "file.txt"); // 相对路径(自动拼接)
Path relativePath = Paths.get("data/file.txt"); //如果没有根目录就默认相对路径,toAbsolutePath()转换为绝对路径
Path basePath = Paths.get("/home/user");
// 安全拼接路径,避免直接使用 +
Path path = basePath.resolve("docs/file.txt"); // /home/user/docs/file.txt
// 获取路径信息
System.out.println("文件名: " + path.getFileName());
System.out.println("父目录: " + path.getParent());
System.out.println("根目录: " + path.getRoot());
System.out.println("路径字符串: " + path.toString());
//移除多余的 .. 和 .,防止路径遍历攻击
Path normalizedPath = Paths.get("/home/user/../docs/file.txt").normalize(); // /home/docs/file.tx
// 检查 fullPath 是否仍在 basePath 下
if (!path.startsWith(basePath)) {
throw new SecurityException("路径访问被禁止!");
}
举例:路径遍历防护
用户上传文件时,恶意输入
../../etc/passwd
试图覆盖系统文件
public class FileDownloadSecurity {
public static void main(String[] args) throws Exception {
String baseDir = "/var/www/uploads"; // 允许访问的目录
String userInput = "../../etc/passwd"; // 恶意输入
// 安全拼接路径
Path basePath = Paths.get(baseDir).normalize().toRealPath();
Path userPath = Paths.get(userInput).normalize();
Path fullPath = basePath.resolve(userPath);
// 检查路径是否在允许范围内
if (!fullPath.normalize().startsWith(basePath)) {
throw new SecurityException("非法路径访问!");
}
// 检查文件扩展名(可选)
String fileName = fullPath.getFileName().toString();
if (!fileName.matches(".*\\.(jpg|png|pdf)")) {
throw new SecurityException("不支持的文件类型!");
}
System.out.println("安全路径: " + fullPath);
}
}
IO流
-
IO流的目的:实现内存与外界数据相互通信
外界--->字节码--->内存--->字节码--->外界
|------输入流------|------输出流------|
-
IO流的使用场景
JVM
内部数据都是运行在内存中的,内存内部数据通信一般不需要使用IO流JVM
与外界数据通信需要IO流实现,例如内存与硬盘之间通信、服务器与客户端通信、压缩/解压缩
-
Java为IO流提供了原始类
InputStream
/OutputStream
,根据传输的数据类型不同,衍生出了许多子类- 文件字节流:针对于一般的文件类型
- 文件字符流:针对于纯文本的文件类型
- 数据字节流:针对于Java基本数据类型
- 转换字符流:针对于字符串类型的数据
- 缓冲字节/字符流:针对于数据量较大的场景
- 打印字节/字符流:针对于只需要输出打印字符串的场景
- 压缩/解压缩字节流:针对于压缩包文件类型
-
常见IO流类线程安全问题
流类型 常见类 线程安全? 关键原因 文件流 FileInputStream
、FileOutputStream
否 多线程同时读写同一文件流会导致数据混乱或文件指针冲突。 缓冲流 BufferedInputStream
、BufferedOutputStream
否 内部缓冲区非线程安全,多线程同时操作会导致数据错误。 打印流 PrintStream
(如System.out
)、PrintWriter
部分 PrintStream
的print()
/println()
线程安全,但write()
不安全;PrintWriter
不安全。转换流 InputStreamReader
、OutputStreamWriter
否 字符编码转换逻辑非线程安全,多线程同时操作会导致编码错误。 数据流 DataInputStream
、DataOutputStream
否 读写基本数据类型的方法非线程安全,多线程同时操作会导致数据解析错误。 Socket 流 Socket.getInputStream()
、Socket.getOutputStream()
否 Socket 流是网络通信的通道,多线程同时读写会导致数据混乱或协议错误。 压缩流 GZIPInputStream
、GZIPOutputStream
否 它们的设计基于单线程流式操作,内部状态(如压缩/解压缩缓冲区、位流指针)在多线程环境下会被并发
字符流与字节流
-
字节和字符
- 字节是一种二进制码,计算机底层只有二进制形式的数据
- 其他各种类型数据都是根据一定规则由字节码转换而来的“虚假数据”,真正的数据类型只有字节
- 字符是为了可读性,根据字符编码规则将字节码转换而来的一种文本类型
-
**字符和字节的转换原理:
字符 <---> Unicode <--编码规则--> 字节/字节数组 <---> 内存
**- 同一个字符的Unicode是唯一的,字节码因不同的编码可能不一致
- 字节流不需要考虑编码,字符流要注意编码问题
-
常见字符编码规则
- ASCII编码:仅包含英文及符号,一个英文字母对应一个字节(
'a' --- 97
) - GBK编码:包含汉字,兼容ASCII,一个中文对应长度为2的字节数组
- UTF-8编码(推荐):支持全球语言,兼容ACII,一个中文对应长度为3的字节数组(
'你' --- [-28, -67, -96]
)
- ASCII编码:仅包含英文及符号,一个英文字母对应一个字节(
文件流
- 文件写的方式
- 追加写:创建输出流时,如果文件存在且已有数据,则接着数据最后写
- 覆盖写:创建输出流时,如果文件存在且已有数据,则覆盖原有数据重新写
文件字节流
class FileInputStream extends InputStream{ //文件字节输入流
public FileInputStream(String name) {...} //获取构造输入流,name是文件路径
public FileInputStream(File file) {...} //获取构造输入流,file是文件对象
public int read() {...} //每次读取一个字节,每调用一次都会读取下一个字节,返回获取的字节值,到流末就返回-1
public int read(byte[] buffer) {...} //每次读取一组字节,更新在buffer中,返回字节数,到流末就返回-1
public byte[] readAllBytes() {...} //读取所有字节保存在一个字节数组中,返回此数组,要求jdk9以上
public void close() {...} //IO流使用完毕一定要关闭
}
class FileOutputStream extends OutputStream{ //文件字节输出流
public FileOutputStream(String name) {...} //name是文件路径,不需要手动创建文件,但不能创建目录
public FileOutputStream(String name, boolean append) {...} //设置输出流写的方式是追加写,默认覆盖写
public FileOutputStream(File file) {...} //获取构造输出流,file是文件对象
public FileOutputStream(File file, boolean append) {...} //设置输出流写的方式是追加写,默认覆盖写
public void write(int b) {...} //写入一个字节,入参也可以是英文字母,其他符号字符会乱码
public void write(byte b[], int off, int len) {...} //写入字节数组中的一部分,配合一次读取一组字节使用
public void close() {...} //IO流使用完毕一定要关闭
}
举例:备份小文件
现有文件user.txt,要求对此文件做备份,备份文件名为userBackUp
try (FileInputStream fileInputStream = new FileInputStream("user.txt");
FileOutputStream fileOutputStream = new FileOutputStream("userBackUp.txt")) {
byte[] bytes = new byte[512]; //每次读取0.5kb字节
while (true) {
int len = fileInputStream.read(bytes);
if (len == -1){
break;
}
fileOutputStream.write(bytes,0,len); //只移动数据,不对数据有改动,不会乱码
}
fileOutputStream.write("\r\n".getBytes()); //最后换行
}
文件字符流
public class FileReader extends InputStreamReader{ //文件输入字符流
public FileReader(String fileName) {...} //构造方法,参数是文件路径,默认编码规则与系统编码一致
public FileReader(File file) {...} //构造方法,参数是文件对象
public int read() {...} //一次读取一个字符,返回Unicode值,到流末返回-1
public int read(char cbuf[]) {...} //一次读取一组字符,返回实际读取到的字符数,到流末返回-1
public void close() {...} //IO流使用完毕一定要关闭
}
public class FileWriter extends InputStreamWriter{ //文件输出字符流
public FileReader(String fileName) {...} //构造方法,参数是文件路径
public FileReader(String name, boolean append) {...} //设置输出流写的方式是追加写,默认覆盖写
public FileReader(File file) {...} //构造方法,参数是文件对象
public FileReader(File file, boolean append) {...} //设置输出流写的方式是追加写,默认覆盖写
public void write(int c) {...} //写入一个字符,入参是Unicode值
public void write(char cbuf[], int off, int len) {...} //写入字符数组的一部分,配合一次读取一组字符使用
public void write(String str) {...} //写入字符串
public void write(String str, int off, int len) {...} //写入字符串一部分
public void flush() {...} //刷新输入流,写入数据后,必须刷新或者关闭输入流,写入操作才会真正生效
public void close() {...} //IO流使用完毕一定要关闭
}
举例:去除文件中的汉字
try (FileReader reader = new FileReader("properties.txt");
FileWriter fileWriter = new FileWriter("newProperties.txt", true)) {
while (true) {
int read = reader.read();
char c = (char) read;
if (read == -1) {
break;
}
if (c >= '\u4E00' && c <= '\u9FA5') { //字符合法才写入
fileWriter.write(read);
fileWriter.flush(); //可以实时显示写入情况
}
}
} catch (IOException e) {
e.printStackTrace();
}
缓冲流
-
目的:极大地提高了原IO流的读取性能
-
原理:缓冲机制
- 内部维护一个字节数组(默认大小通常为 8KB),通过批量读取数据到缓冲区,减少 I/O 操作次数
- 普通字节流一次读取一定会触发IO接口,缓冲流只有缓冲区满了才会触发
- 小量数据情况下,缓冲流效率不一定比普通字节流高
-
缓冲流应用场景
- 数据量较大时建议加上缓冲流
- 如果需要一行一行地读数据时,用缓冲流的
readLine()
方法
字节缓冲流
public class BufferedInputStream{ //缓冲字节输入流:必须包装另一个 InputStream(如 FileInputStream),不能直接使用
public BufferedInputStream(InputStream in) {...} //构造缓冲输入流,默认缓冲8kb
public BufferedInputStream(InputStream in, int size) {...} //设置缓冲区大小
public int read() {...} //读取一个字节
public int read(byte b[]) {...} //读取一组字节
public void close() {...} //包装的流都会关闭,执行一次即可
}
public class BufferOutputStream{ //缓冲字节输出流:必须包装另一个 OutputStream(如 FileOutputStream),不能直接使用
public BufferedOutputStream(OutputStream out) {...} //构造缓冲输出流,默认缓冲8kb,写方式由OutputStream指定
public BufferedOutputStream(OutputStream out, int size) {...} //设置缓冲区大小
public void write(int b) {...}
public void write(int b) {...} //写入一个字节,入参是ASCII码
public void write(byte b[], int off, int len) {...} //写入字节数组中的一部分
public void flush() {...} //刷新输入流,写入数据后,必须刷新或者关闭输入流,写入操作才会真正生效
public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:备份大文件
try (BufferedInputStream bufferIn = new BufferedInputStream(new FileInputStream("E:"+File.separator+"长视频.mp4"));
BufferedOutputStream bufferOut = new BufferedOutputStream(new FileOutputStream("长视频备份.mp4"))) {
byte[] bytes = new byte[2048];
while (true){
int len = bufferIn.read(bytes);
if(len == -1){
break;
}
bufferOut.write(bytes,0,len);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
字符缓冲流
BufferedReader.readLine()
方法不会读取换行符,此方法的返回值作为写入数据时,需要手动换行或者加上换行符
public class BufferedReader{ //字符缓冲输入流:必须包装一个 `Reader` 对象(如 `FileReader`、`InputStreamReader`)
public BufferedReader(Reader in) {...} //缓冲输入流构造方法,默认缓冲8kb,编码由Reader决定
public BufferedReader(Reader in, int sz) {...} //设置缓冲区大小,单位byte
public String readLine() {...} //读取一行字符串直至换行符(不含),返回内容,到流末返回null
public int read() {...} //一次读取一个字符,返回Unicode值,到流末返回-1
public int read(char cbuf[]) {...} //一次读取多个字符,返回实际读取到的字符数,到流末返回-1
public void close() {...} //包装的流都会关闭,执行一次即可
}
public class BufferWriter{ //字符缓冲输出流:必须包装一个 `Writer` 对象(如 `FileWriter`、`OutputStreamWriter`)
public BufferedWriter(Writer out) {...} //缓冲输出流构造方法,默认缓冲8kb,编码、写入方式由Reader决定
public BufferedWriter(Writer out, int sz) {...} //设置缓冲区大小,单位byte
public void write(int c) {...} //写入一个字符,入参是Unicode值
public void write(char cbuf[], int off, int len) {...} //写入字符数组的一部分
public void write(String str) {...} //写入字符串
public void write(String str, int off, int len) {...} //写入字符串一部分
public void newLine() {...} //换行,相当于\n\r
public void flush() {...} //刷新输入流,写入数据后,必须刷新或者关闭输入流,写入操作才会真正生效
public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:删除配置文件注释
String inputFile = "config.yml";
String outputFile = "simpleConfig.yml"; //将删除注释后的文件保存到simpleConfig.yml
try (BufferedReader reader = new BufferedReader(new FileReader(inputFile));
BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {
String line;
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
//注释行或者空行直接跳过
if (line.trim().startsWith("#") || line.trim().isEmpty()) {
continue;
}
//移除行内注释(假设注释以 # 开头)
String cleanedLine = line.replaceAll("#.*", "");
if (!cleanedLine.isEmpty()) {
writer.write(cleanedLine);
writer.newLine(); //readLine不读取换行符,需要手动换行
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
打印流
-
打印流是文件输出流的包装,意在更方便地进行数据输出,例如记录日志
- 可以设置自动刷新
- 可以快接地格式化输出字符串
- 打印流内部包装了缓冲流,效率也很高
-
使用场景:只需快速打印文本类型数据时建议使用打印流
-
重定向打印流
sout
方法本质上就是使用PrintStream
打印流,只不过是打印到控制台里- 重定向打印流后,
sout
就不会打印在控制台,而是打印到指定的文件中
public final class System { public final static PrintStream out = null; //JVM启动时会调用静态代码块将out初始化到控制台 public static void setOut(PrintStream out) {...} //重定向打印流 }
字节打印流
public class PrintStream extends FilterOutputStream{ //打印流必须包装一个已有的输出流,不能直接使用
public PrintStream(OutputStream out) {...} //构造方法,默认不立即刷新,打印编码UTF-8
public PrintStream(OutputStream out, boolean autoFlush) {...} //一次打印完是否立即刷新
public PrintStream(OutputStream out, boolean autoFlush, String encoding) {...} //如果直接打印字符串,可以指定编码
public void println(XXX x) {...} //打印x数据字符串形式(x可以是任意类型)对应的字节,并换行
public PrintWriter printf(String format, Object ... args) {...} //格式化字符串并打印
public void write(int b/byte b[], int off, int len) {...} //打印字节
public void close() {...} //包装的流都会关闭,执行一次即可
}
字符打印流
public class PrintWriter{ //打印流必须包装一个已有的输出流,不能直接使用
public PrintWriter (Writer out) {...} //构造方法,默认不立即刷新,打印编码UTF-8
public PrintWriter(Writer out, boolean autoFlush) {...} //指定打印完立即刷新
public PrintStream(OutputStream out, boolean autoFlush, String encoding) {...} //可以指定编码
public void println(XXX x) {...} //打印x数据字符串形式(x可以是任意类型),并换行
public void write(int c/char cbuf[], int off, int len) {...} //打印字符
public void write(String str/String str, int off, int len) {...} //打印字符串
public PrintWriter printf(String format, Object ... args) {...} //格式化字符串并打印
public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:记录日志
public class LogHelper {
private static final PrintWriter writer;
private static final String fileName = "myProject.log";
static {
try {
writer = new PrintWriter(new FileWriter(fileName, true), true);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void info(String s) {
//时间 [等级] 日志记录位置: 日志内容
writer.printf("%s [INFO] %s: %s%n",
LocalDateTime.now(),
Thread.currentThread().getStackTrace()[2], //2游标返回上一级调用位置
s);
}
public static void warn(Exception e) {
//时间 [等级] 异常处理位置\n 异常调用链
writer.printf("%s [WARN] %s%n",
LocalDateTime.now(),
Thread.currentThread().getStackTrace()[2]);
e.printStackTrace(writer); //可以指定输出流
}
}
//效果
2025-06-19T19:21:40.961 [INFO] com.wyh.utils.Check.method(Check.java:14): 流程关键节点日志,方便后期维护
2025-06-19T19:21:40.970 [WARN] com.wyh.utils.Check.main(Check.java:9)
java.lang.RuntimeException: 原始异常
at com.wyh.utils.Check.origin(Check.java:18)
at com.wyh.utils.Check.method(Check.java:15)
at com.wyh.utils.Check.main(Check.java:7)
转换流
-
目的:处理编码不一致的问题
-
转换原理
- 转换流是从字节流包装而来的字符流
外界--->编码规则--->内存--->编码规则--->外界
-
应用场景
- 跨平台文本处理时编码可能不一致
- 读写编码不一致的文本文件
- 与数据库进行数据交互时编码可能不一致
转换输入流
- 转换流必须包装一个已有的字节流(如
FileInputStream
),不能直接使用
public class InputStreamReader{
public InputStreamReader(InputStream in) {...} //读取数据,默认UTF-8
public InputStreamReader(InputStream in, Charset cs) {...} //读取数据,并声明该数据的编码
public InputStreamReader(InputStream in, String charsetName) {...} //指定原数据的编码
public int read() {...} //一次读取一个字符,返回Unicode值,到流末返回-1
public int read(char cbuf[]) {...} //一次读取一组字符,返回实际读取到的字符数,到流末返回-1
public void close() {...} //包装的流都会关闭,执行一次即可
}
转换输出流
- 转换流必须包装一个已有的字节流(如
FileOutputStream
),不能直接使用
public class OutputStreamWriter{
public OutputStreamWriter(OutputStream out) {} //写数据,该数据默认以UTF-8格式转换为字节保存
public OutputStreamWriter(OutputStream out, Charset cs) {} //可以指定写数据的编码方式
public OutputStreamWriter(OutputStream in, String charsetName) {...} //可以指定写数据的编码方式
public void write(int c) {...} //写入一个字符,入参是Unicode值
public void write(char cbuf[], int off, int len) {...} //写入字符数组的一部分,配合一次读取一组字符使用
public void write(String str) {...} //写入字符串
public void write(String str, int off, int len) {...} //写入字符串一部分
public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:结合缓冲流转换编码格式
老项目文件old.txt是GBK格式,现要求转换为UTF-8格式文件modern.txt
//缓冲流在最外层
try(InputStreamReader input = new InputStreamReader(new FileInputStream("old.txt"), "GBK");
BufferedReader reader = new BufferedReader(input);
OutputStreamWriter output = new OutputStreamWriter(new FileOutputStream("modern.txt"), StandardCharsets.UTF_8);
BufferedWriter writer = new BufferedWriter(output)) {
while (true){
char[] chars = new char[1024];
int len = reader.read(chars);
if (len == -1){
break;
}
writer.write(chars,0,len);
}
}
数据流
- 目的:一种字节流,传输Java基本数据类型
数据输入流
public class DataInputStream{
public DataInputStream(InputStream in) {...} //必须包装一个已有的输入流
public final boolean readBoolean() {...} //读取布尔类型数据并返回
public final byte readByte() {...} //读取Java字节类型数据并返回
public final int readInt() {...} //读取整数类型数据并返回
public final double readDouble() {...} //读取浮点类型数据并返回
public final String readUTF() {...} //根据UTF-8编码,将字节转换为字符串,并返回
public void close() {...} //包装的流都会关闭,执行一次即可
}
数据输出流
public class DataOutputStream{
public DataOutputStream(OutputStream out) {...} //必须包装一个已有的输入流
public final void writeByte(int v) {...} //写入Java字节型数据
public final void writeBoolean(boolean v) {...} //写入布尔型数据
public final void writeInt(int v) {...} //写入整型数据
public final void writeDouble(double v) {...} //写入浮点型数据
public final boolean readUTF(String str) {...} //根据UTF-8,将字符串转化为字节数组写入内存
public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:数据流必须保证输入输出顺序一致
- 使用数据流,必须保证数据类型写入顺序和读取顺序一致,否则会解析出错误的数据
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data_stream.bin"))) {
// 写入布尔值和整型值
dos.writeBoolean(true); // 写入布尔值
dos.writeInt(123); // 写入整型值
dos.writeBoolean(false); // 写入另一个布尔值
dos.writeInt(456); // 写入另一个整型值
} catch (IOException e) {
e.printStackTrace();
}
// 从文件读取数据
try (DataInputStream dis = new DataInputStream(new FileInputStream("data_stream.bin"))) {
// 读取布尔值和整型值(顺序必须与写入一致)
System.out.println(dis.readInt()); //顺序不一致,读出来的结果变成了16777216
System.out.println(dis.readBoolean());
System.out.println(dis.readBoolean());
System.out.println(dis.readInt());
} catch (IOException e) {
e.printStackTrace();
}
压缩/解压缩流
- 目的:压缩/解压文件,如果仅复制压缩文件用文件流即可
- 原理:将文件的字节序列中相同部分“合并”,极大地减少了空间消耗
ZipEntry
ZipEntry
类是表示 ZIP 文件中的一个条目,可以是文件或目录,通过名称中的分隔符识别文件层级
public class ZipEntry{
public ZipEntry(String name) {...} //构造方法,入参以/结尾就是目录条目,否则为文件条目
public String getName() {...} //返回条目基于zip根目录的相对路径
public long getSize() {...} //获取条目未压缩的大小
public long getCompressedSize() {...} //获取条目压缩后的大小
public boolean isDirectory() {...} //条目是否为目录条目
}
压缩输入流(解压)
public class ZipInputStream{
public ZipInputStream(InputStream in) {...} //必须包装一个已有的输入流,默认编码UTF-8
public ZipInputStream(InputStream in, Charset charset) {...} //可以设置压缩文件编码格式
public ZipEntry getNextEntry() {...} //获取压缩目录中的当前条目(目录/文件),每次调用就会读取下一个条目,直到返回null
public int read(byte[] b) {...} //解压当前文件条目,每次读取一组字节,结束返回-1,不能用于目录条目,在getNextEntry后用
public void closeEntry() {...} //关闭当前条目
public void close() {...} //IO流使用完毕一定要关闭
}
压缩输出流(压缩)
public class ZipOutputStream{
public ZipOutputStream(OutputStream out) {...} //必须包装一个已有的输出流,默认编码UTF-8
public ZipOutputStream(OutputStream out, Charset charset) {...} //可以设置压缩文件编码格式
public void putNextEntry(ZipEntry e) {...} //开始写入一个新的ZIP条目,文件或目录由入参ZipEntry决定
public void write(byte[] b, int off, int len) {...} //写入条目的数据,在putNextEntry文件条目后使用
public void closeEntry() {...} //结束写入当前条目
public void close() {...} //IO流使用完毕一定要关闭
}
压缩炸弹
-
压缩炸弹(Zip Bomb)是一种恶意构造的压缩文件,它解压前并不大,但一旦解压会变超级大,严重的可能会导致相同崩溃
-
压缩炸弹防护措施
- 在解压之前,检查压缩文件的大小
- 在解压过程中,监控解压后的文件大小,可以随时终止解压
- 使用经过验证的第三方库(如Apache Commons Compress)可以提供更多的安全功能和选项来防止压缩炸弹
private static final long MAX_ZIP_SIZE = 100 * 1024 * 1024; // 解压后一旦超过100MB就报异常
public static void checkZipFile(File zipFile) throws IOException { //方法一:解压前检查压缩包大小
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry entry;
long totalSize = 0; //压缩包大小计数器
while ((entry = zis.getNextEntry()) != null) { //遍历压缩目录
if (entry.isDirectory()) {
continue;
}
totalSize += entry.getCompressedSize();
if (totalSize > MAX_ZIP_SIZE) {
throw new IOException("Zip file is too large");
}
zis.closeEntry();
}
}
}
public static void unzip(File zipFile, File srcFile) throws IOException { //方法二:边解压边检查
try (FileInputStream fis = new FileInputStream(zipFile);
ZipInputStream zis = new ZipInputStream(fis)) {
ZipEntry entry;
long totalSize = 0; //压缩包大小计数器
while ((entry = zis.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
try (FileOutputStream fos = new FileOutputStream(new File(srcFile, entry.getName()))) {
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
totalSize += len;
if (totalSize > MAX_UNZIPPED_SIZE) {
throw new IOException("Unzipped size is too large");
}
}
zis.closeEntry();
}
}
}
}
举例:备份压缩日志
- 开启压缩流:流关闭前所有写入的条目都在此目录中
- 开启条目
- 在文件条目下写入数据
- 关闭流:压缩完成
try (ZipOutputStream zipOutputStream = new ZipOutputStream("backup.zip"); //所有写入的文件都会在backup.zip压缩包中
FileInputStream fileInputStream = new FileInputStream("log.txt")) {
LocalDateTime t = LocalDateTime.now(); //将日期作为压缩包内的层级依据
String path = t.getYear() + File.separator + t.getMonth().getValue() + File.separator + t.getDayOfMonth();
zipOutputStream.putNextEntry(new ZipEntry(path + File.separator + "log.txt")); //开启条目,准备写入log.txt文件
byte[] bytes = new byte[1024];
while (true) { //写入数据
int read = fileInputStream.read(bytes);
if (read == -1) {
break;
}
zipOutputStream.write(bytes);
}
zipOutputStream.closeEntry();
}
举例:压缩指定类型文件
压缩一个多层级目录,目录中有各种各样的文件,现只保留.mp4文件
public static void main(String[] args) throws IOException {
File outputFile = new File("E:\\video.zip");
File inputFile = new File("E:\\JavaCourse\\02-二阶段");
try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile));
ZipOutputStream zipOutputStream = new ZipOutputStream(bufferedOutputStream)) { //再包一个缓冲流,提升性能
startZip(inputFile, zipOutputStream, ""); //压缩流输出抽离在外面,避免因递归频繁地开启关闭压缩流
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void startZip(File inputFile, ZipOutputStream zipOutputStream, String parentPath) throws IOException {
if (!inputFile.isDirectory() && inputFile.getName().endsWith(".mp4")) { //只压缩视频文件
byte[] bytes = new byte[2048];
int len;
zipOutputStream.putNextEntry(new ZipEntry(parentPath + File.separator + inputFile.getName()));
try (FileInputStream fileInputStream = new FileInputStream(inputFile);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream)) {
while ((len = bufferedInputStream.read(bytes)) != -1) {
zipOutputStream.write(bytes, 0, len);
}
}
zipOutputStream.closeEntry();
return;
}
if (inputFile.isDirectory()) {
parentPath = parentPath + File.separator + inputFile.getName(); //是目录,将目录层级记录下来,保证结构一致
for (File f : inputFile.listFiles()) {
startZip(f, zipOutputStream, parentPath);
}
}
}
序列化
-
目的:传输Java对象类型的数据
-
原理
- 目前没有针对Java对象类型的IO流,因此需要将对象转换为字节码/字符串,再通过对应的IO流传输
- 序列化仅指此转换的过程,后续IO流操作的实现另说
-
序列化分类
- Java原生序列化:使用字节流(如Socket流)
- Json序列化:对象转换为Json格式字符串,进而通过字符流实现
-
应用场景
- 原生序列化支持所有对象(
Thread
、Socket
、File
等类只能使用原生序列化),但性能、可读性低、有安全风险; - Json序列化性能高,可读性高,适合绝大部分场景
- 原生序列化支持所有对象(
原生序列化
-
开启原生序列化:让一个类可序列化,只需实现
java.io.Serializable
接口即可@Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private Long id; private String name; }
-
如果没有开启序列化,直接将对象通过字节流传输,会报
NotSerializableException
异常public class Main { public static void main(String[] args) { User user = new User(30L,"Alice"); //假设User类没有实现Serializable try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) { oos.writeObject(user); } catch (IOException e) { e.printStackTrace(); } } } java.io.NotSerializableException: com.wyh.entity.User //序列化异常 at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at Main.main(Main.java:24)
Json序列化
-
Json序列化本质是将对象转化为Json格式的字符串,常见第三方库有Jackson、Gson、FastJson
-
Jackson:spring生态默认封装Jackson
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <!--如果是spring boot项目会自带--> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> <!-- 使用最新版本 --> </dependency>
/* **Json属性映射 1. @JsonProperty:设置映射别名 2. @JsonInclude:什么情况下不序列化此成员 3. @JsonFormat:对于时间数据类型,设置输出时间格式 4. @JsonIgnore:json序列化时忽略此成员 */ @Data @ToString @AllArgsConstructor public class User{ @JsonProperty("id") //格式化后的json数据的对应key就是"ID" private Integer userId; @JsonInclude(JsonInclude.Include.NON_NULL) //姓名为空就忽略姓名 private String userName; @JsonIgnore //不转换status属性 private Integer status; @JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",locale = "zh") //指定格式,并指定时区为中国 private Date time; }
public class ObjectMapper { // ObjectMapper:Jackson 的核心类 public String writeValueAsString(Object object) {...} //转换为JSON字符串 public <T> T readValue(String text, Class<T> clazz) {...} //根据class将JSON字符串反序列化为Java实例 public <T> T readValue(JsonParser p, TypeReference<T> valueTypeRef) {...} //多个实例时可以反序列化为对象列表 }
-
FastJson:当前最流行三方库
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> <!-- 使用最新稳定版本 --> </dependency>
/* **Json属性映射@JSONField注解 1. name:映射别名 2. format:对于时间数据类型,设置输出时间格式 3. serialize:是否序列化 4. deserialize:是否反序列化 5. jsonDirect:是否直接序列化字段值(跳过 getter/setter,默认 false) */ public class User { @JSONField(name = "user_name") // 序列化为 JSON 时的字段名 private String name; @JSONField(format = "yyyy-MM-dd") // 日期格式化 private Date birthday; }
public abstract class JSON { public static String toJSONString(Object object) {...} //转换为JSON字符串 public static <T> T parseObject(String text, Class<T> clazz) {...} //根据class将JSON字符串反序列化为Java实例 public static <T> List<T> parseArray(String text, Class<T> clazz) {...} //多个实例时可以反序列化为对象列表 }