JavaSE——基础篇

三大特性

封装

  • 封装的目的:将类的内部结构隐藏起来,对外只开放相关的方法,保证了数据安全,提高了代码复用

  • 修饰符

    1. public:公共访问
    2. default:同一个包下可访问
    3. protected:类本身或者其子类可访问
    4. private:仅类本身可访问

继承

  • 继承的目的:允许一个类继承另一个类的属性和方法,并子类可以重写父类的方法,实现了功能拓展

  • hashCodeequals的重写

    1. 逻辑比较:实例内容的字面值相同,就认为二者逻辑相同,与地址无关
    2. Object.hashCodeObject.equals采用的是地址比较
    3. hashCodeequals要么都不重写,要么同时重写,否则使用MapSetList会有逻辑错误
  • 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;
    }
}

多态

  • 多态的目的:在继承的基础上,实例可以选择调用重写的方法

  • 多态的条件:

    1. 多态发生在父类与子类(接口与实现类)之间
    2. 子类必须 重写 父类的实例方法(非private、非final、非static
    3. 必须通过父类引用子类(声明父类,引用的是子类)
  • 多态的原理:编译看左边,运行看右边

    1. 编译看左边:父类声明本质上创建的对象还是父类的实例,因此其他成员依然是父类的成员
    2. 运行看右边 :父类的方法被子类重写
    3. 通过父类引用无法直接调用子类独有的方法
  • 强制类型转换:子类可以强转为父类,父类不能强转为子类,因为会丢失数据

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();
}

静态

  • 静态的目的:静态成员是类级别的,适合存放全局数据(尤其是多线程环境)

  • 静态访问原则:静态只能访问静态

    1. 静态成员/方法 只能访问 静态成员/方法
    2. 静态代码块只能访问静态成员/方法
  • 如果抽象方法要求静态,就需要在声明抽象类时就实现此方法

静态成员/方法

public class User {
    private Long id;
    private static String schoolName;
}

静态代码块

  • 静态代码块使用场景:初始化静态资源

  • Java类的加载顺序

    1. 静态成员和静态代码块
    2. 子类的静态成员和静态代码块
    3. 父类的普通成员和方法
    4. 父类构造方法
    5. 子类的普通成员和方法
    6. 子类构造方法
    7. 内部静态类不被加载,只要调用时才加载
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 要求资源必须实现 AutoCloseableCloseable 接口,以便 JVM 能自动调用 close() 方法

try(声明资源1;声明资源2;...){
    //执行语句
}catch(Exception e){
    //异常处理
}

数据类型

  • 包装类和基本类型:

    1. 包装类可以表示null值,基本数据类型不支持此业务需求
    2. 包装类提供了丰富的方法(进制转换)
    3. 使用集合框架、反射等技术时,必须使用包装类,否则会有异常
    4. 基本类型存储少性能高,适合用于局部变量
  • 整型包装类缓存机制:创建整型包装类时会先从缓存(-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/null
  • long/Long:数值要求较大时选用,8字节,默认为0/null,直接声明数值时要加后缀L
  • byte/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/null
  • float/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:底层通过常量字符数组存储,创建后不可变

    1. 字符串创建后不可变,因此使用+拼接字符串时,实际上是由新建了若干字符串,大量拼接时很消耗性能
    2. 字符串拼接场景下,编译器会将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));

时间

  • LocalDateTimeDate

    1. LocalDateTime直接获取本地时间,解决了时区问题
    2. LocalDateTime表示的时间更直观,有更方便的API
    3. Date线程不安全,LocalDateTime线程安全
    4. 高版本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类的静态成员
}

随机数

  1. Radom:线程不安全,性能高,适合普通场景
  2. ThreadLocalRandom:封装了Random的线程安全版本,适合并发环境下的普通场景
  3. 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(){...} //生成一个随机布尔值
}

正则表达式

Java 正则表达式 | 菜鸟教程

  • 易错点

    1. 正则表达式特殊处理空白字符,因此匹配前后有空白字符必须显式指明
    2. 空白字符包括空格、制表符 \t、换行符 \n、回车符 \r 等;\\s匹配所有空白字符;直接空格字符匹配空格
    3. ^$ 分别匹配整个字符串的开头和结尾;如果需要匹配行首或行尾(多行模式),需要启用 Pattern.MULTILINE 标志
    4. 如果需要匹配特殊字符本身,必须用 \\ 转义,例如\\\n表示\n
    5. 元字符(如 .*+ 等)有默认的匹配行为,需要显式调整以匹配特定模式

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
}

枚举

语法

  • 枚举就是常量的集合,使用枚举保证了代码安全性和可读性

    1. 枚举是天然的单例,防止了序列化和反射攻击
    2. 枚举会在编译时检查类型,防止数据类型引起的异常
  • 枚举类不能被继承

  • 枚举类可以实现其他接口,且每一个枚举项都需要实现

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)

  • 集合的特性

    1. 集合可以动态扩容
    2. 集合有丰富的操作方法,更适合高效地处理数据
    3. 集合可以更灵活方式的存储数据

List接口

  • 基本特性:元素有游标,基于索引查找,可重复

  • 动态数组和链表

    1. 动态数组有扩容机制,默认扩容1.5倍;链表无容量限制
    2. 动态数组查询效率高;链表插入删除效率高,头尾元素操作效率极高

ArrayList

  • 应用场景

    1. 线程不安全
    2. 读操作效率高,适合频繁地随机读取
    3. 可以动态扩容,适合数据量不可预测的场景
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

  • 应用场景

    1. 线程不安全
    2. 写操作效率高,频繁地插入删除操作
    3. 容量无限制,适合总数据容量要求较大的场景
    4. 可以用于实现栈或队列
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

  • 应用场景

    1. 线程安全,适合作为以列表形式保存的公共资源
    2. 使用写时复制技术,适合写少读多
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

  • 应用场景

    1. 使用CAS算法,保证线程安全同时兼顾了读写性能
    2. 本质上是链表,更适合读少写多的场景
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接口基本特征

    1. 元素以键值对形式存储,可以实现高级功能
    2. 元素无游标,键不可重复

HashMap

  • 应用场景

    1. 线程不安全
    2. HashMap基于哈希表实现,查找效率最高,适合随机查找、统计数据
    3. 可以动态扩容,适合数据量不可预测的场景
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

  • 应用场景

    1. 线程不安全
    2. LinkedHashMap维护了一个双向链表,可以设置插入顺序和访问顺序
    3. 遍历时会遵循元素插入/访问的顺序
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

  • 应用场景

    1. 线程不安全
    2. TreeMap基于红黑树实现,默认根据键升序,也可以自定义排序
    3. 遍历时会遵循排序规则顺序
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

  • 应用场景

    1. 线程安全,适合作为以键值对形式保存的公共资源
    2. 采用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接口基本特征

    1. 元素无游标,不可重复
    2. Set本质上是没有值的Map

HashSet

  • 应用场景

    1. 线程不安全
    2. 基于哈希表实现,查找效率最高,适合快速去重
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

  • 应用场景

    1. 线程不安全
    2. TreeMap同理,可以设置排序
    3. 遍历时会遵循设置的排序顺序
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

  • 应用场景

    1. 线程不安全
    2. LinkedHashMap不同的是,LinkedHashSet只能维护插入顺序
    3. 遍历时会遵循设置的操作顺序
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

  • 应用场景

    1. 线程安全
    2. 采用写时复制技术,适合读多写少的场景
    3. 写操作较为频繁时,可以使用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流

  • 目的

    1. 对集合元素进行操作时,简化代码并提高代码可读性
    2. 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类的目的

    1. 替代了null检查,避免null异常
    2. 常与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) {...} //容器有值就执行消费型接口的实现类    
}

泛型

  • 泛型的目的

    1. 数据类型安全:泛型在编译前已经确认的数据类型,避免了数据类型风险
    2. 代码复用:使用泛型通配符,避免了实现通用功能时出现重复代码

泛型集合

List<String> fruits = new ArrayList<>(); 

泛型类,泛型方法/接口

  • 泛型类型参数:任意大写字母,都可以视为一个数据类型,但没有实际的约束力

    1. E - Element (表示集合的元素)
    2. T - Type(表示Java 类)
    3. K - Key(表示键)
    4. V - Value(表示值)
    5. 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对象包含了类的所有信息,因此可以动态地获取类的信息

  • 应用场景:动态创建实例,减少重复的代码

  • 工作中反射的例子

    1. Json反序列化技术,将Jaon字符串转化为相应实例,使用反射就可以一次覆盖所有实体类的反序列化方法
    2. AOP使用JDK动态代理或CGLIB,通过反射创建目标对象的代理
    3. IOC识别带有@Component@Service等注解的类代替创建实例,覆盖所有类的new方法
    4. JMockit使用反射创建Mock对象,覆盖对应类的new方法

获取Class实例

//方法1(推荐)
Class<?> aClass =.class;
//方法2
Class<?> aClass = Class.forName("对象全类名");
//方法3
Class<?> aClass = 对象.getClass();

获取成员信息

  • 成员信息分为三类

    1. Field:成员信息对象
    2. Method:方法信息对象
    3. 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){...} //获取指定构造方法(私有)
}

创建对象

  • 一般步骤

    1. 创建Class实例
    2. 获取Constructor实例
    3. 调用Constructor.newInstance()方法创建对象
  • 不建议使用Class.newInstance()方创建对象

public final class Constructor<T>{
    
    public T newInstance(Object ... initargs){...} //先获取Constructor对象,再据此创建对象,空参构造可以无入参
}

调用方法

  • 一般步骤

    1. 创建Class实例
    2. 通过Constructor创建对象
    3. 获取Method实例
    4. 调用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() {...} //获取方法的泛型信息
}

举例:覆盖测试类异常情形

  1. Dao层要对异常情况做检查(例如连接失败、SQL非法等),确保异常出现时都会抛出
  2. 所有的Dao类都要专门做一个DaoExceptionTest类,测试其中所有方法是否抛出异常,这很麻烦
  3. 要求只做一个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所在目录)

    1. 正斜杠写法 "resource/file.txt"
    2. 反斜杠写法 "resource\\file.txt"
    3. 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类用于防护攻击者利用 ../ 和分隔符组合尝试跳出或跳进目标目录

  • 防护路径攻击步骤

    1. 使用Paths.get()表示路径
    2. 用户输入路径拼接使用Path.resolve()+Path.normalize(),防止路径遍历
    3. 限制文件操作范围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流的使用场景

    1. JVM内部数据都是运行在内存中的,内存内部数据通信一般不需要使用IO流
    2. JVM与外界数据通信需要IO流实现,例如内存与硬盘之间通信、服务器与客户端通信、压缩/解压缩
  • Java为IO流提供了原始类InputStream/OutputStream,根据传输的数据类型不同,衍生出了许多子类

    1. 文件字节流:针对于一般的文件类型
    2. 文件字符流:针对于纯文本的文件类型
    3. 数据字节流:针对于Java基本数据类型
    4. 转换字符流:针对于字符串类型的数据
    5. 缓冲字节/字符流:针对于数据量较大的场景
    6. 打印字节/字符流:针对于只需要输出打印字符串的场景
    7. 压缩/解压缩字节流:针对于压缩包文件类型
  • 常见IO流类线程安全问题

    流类型常见类线程安全?关键原因
    文件流FileInputStreamFileOutputStream多线程同时读写同一文件流会导致数据混乱或文件指针冲突。
    缓冲流BufferedInputStreamBufferedOutputStream内部缓冲区非线程安全,多线程同时操作会导致数据错误。
    打印流PrintStream(如 System.out)、PrintWriter部分PrintStreamprint()/println() 线程安全,但 write() 不安全;PrintWriter 不安全。
    转换流InputStreamReaderOutputStreamWriter字符编码转换逻辑非线程安全,多线程同时操作会导致编码错误。
    数据流DataInputStreamDataOutputStream读写基本数据类型的方法非线程安全,多线程同时操作会导致数据解析错误。
    Socket 流Socket.getInputStream()Socket.getOutputStream()Socket 流是网络通信的通道,多线程同时读写会导致数据混乱或协议错误。
    压缩流GZIPInputStream GZIPOutputStream它们的设计基于单线程流式操作,内部状态(如压缩/解压缩缓冲区、位流指针)在多线程环境下会被并发

字符流与字节流

  • 字节和字符

    1. 字节是一种二进制码,计算机底层只有二进制形式的数据
    2. 其他各种类型数据都是根据一定规则由字节码转换而来的“虚假数据”,真正的数据类型只有字节
    3. 字符是为了可读性,根据字符编码规则将字节码转换而来的一种文本类型
  • **字符和字节的转换原理:字符 <---> Unicode <--编码规则--> 字节/字节数组 <---> 内存 **

    1. 同一个字符的Unicode是唯一的,字节码因不同的编码可能不一致
    2. 字节流不需要考虑编码,字符流要注意编码问题
  • 常见字符编码规则

    1. ASCII编码:仅包含英文及符号,一个英文字母对应一个字节('a' --- 97
    2. GBK编码:包含汉字,兼容ASCII,一个中文对应长度为2的字节数组
    3. UTF-8编码(推荐):支持全球语言,兼容ACII,一个中文对应长度为3的字节数组('你' --- [-28, -67, -96]

文件流

  • 文件写的方式
    1. 追加写:创建输出流时,如果文件存在且已有数据,则接着数据最后写
    2. 覆盖写:创建输出流时,如果文件存在且已有数据,则覆盖原有数据重新写

文件字节流

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流的读取性能

  • 原理:缓冲机制

    1. 内部维护一个字节数组(默认大小通常为 8KB),通过批量读取数据到缓冲区,减少 I/O 操作次数
    2. 普通字节流一次读取一定会触发IO接口,缓冲流只有缓冲区满了才会触发
    3. 小量数据情况下,缓冲流效率不一定比普通字节流高
  • 缓冲流应用场景

    1. 数据量较大时建议加上缓冲流
    2. 如果需要一行一行地读数据时,用缓冲流的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);
}

打印流

  • 打印流是文件输出流的包装,意在更方便地进行数据输出,例如记录日志

    1. 可以设置自动刷新
    2. 可以快接地格式化输出字符串
    3. 打印流内部包装了缓冲流,效率也很高
  • 使用场景:只需快速打印文本类型数据时建议使用打印流

  • 重定向打印流

    1. sout方法本质上就是使用PrintStream打印流,只不过是打印到控制台里
    2. 重定向打印流后,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)

转换流

  • 目的:处理编码不一致的问题

  • 转换原理

    1. 转换流是从字节流包装而来的字符流
    2. 外界--->编码规则--->内存--->编码规则--->外界
  • 应用场景

    1. 跨平台文本处理时编码可能不一致
    2. 读写编码不一致的文本文件
    3. 与数据库进行数据交互时编码可能不一致

转换输入流

  • 转换流必须包装一个已有的字节流(如 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)是一种恶意构造的压缩文件,它解压前并不大,但一旦解压会变超级大,严重的可能会导致相同崩溃

  • 压缩炸弹防护措施

    1. 在解压之前,检查压缩文件的大小
    2. 在解压过程中,监控解压后的文件大小,可以随时终止解压
    3. 使用经过验证的第三方库(如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();
            }
        }
    }
}

举例:备份压缩日志

  1. 开启压缩流:流关闭前所有写入的条目都在此目录中
  2. 开启条目
  3. 在文件条目下写入数据
  4. 关闭流:压缩完成
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对象类型的数据

  • 原理

    1. 目前没有针对Java对象类型的IO流,因此需要将对象转换为字节码/字符串,再通过对应的IO流传输
    2. 序列化仅指此转换的过程,后续IO流操作的实现另说
  • 序列化分类

    1. Java原生序列化:使用字节流(如Socket流)
    2. Json序列化:对象转换为Json格式字符串,进而通过字符流实现
  • 应用场景

    1. 原生序列化支持所有对象(ThreadSocketFile等类只能使用原生序列化),但性能、可读性低、有安全风险;
    2. 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) {...} //多个实例时可以反序列化为对象列表
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值