← 返回首页

java算法语法速查手册

2026-05-15Java算法语法速查

java速查手册

Java简介

Java的性质

image-20260212085414855

首先,java(比如cshap)其都是由c语言进化而来的,所以一般来说,这俩语言都需要先翻译成C语言,再在不同的平台 (linux/windows)上被翻译成各自平台的内核函数再执行,因此一套代码在不同平台的编译版本是不能互通的,但是java通过初次翻译成一个中间语言实现了跨平台(不彻底的编译),不同平台需要安装各自的JRE,再去通过这个JRE翻译成各自的内核函数进行运行。这个过程是会产生性能损耗的。

java初步打包成class文件,再重新转化成c语言。(高级语言都额外加了一层翻译,因此要慢一些)

idea快捷键

快捷键功能说明示例
将选中的表达式或右侧值提取为新变量,自动生成左侧声明​ →
复制当前行或选中块
逐步扩大选区(单词 → 行 → 块)
自动修复、生成 getter/setter、添加 try-catch 等
打开 TODO这个在项目检查还有哪些代办很有用
跳转定义

java运行环境

java运行在JVM上,JDK包含JRE包含JVM:

image-20260212085429219

image-20260212085440266

SE:基础语法或基础功能函数

EE:web开发

ME:安卓开发,嵌入式,微型设备。

Java的生态发展始于其核心基础——Java SE(Standard Edition),它提供了基础语法和基本功能函数,由原始团队构建并持续演进。随着Java语言的成熟,开源社区和大牛开发者不断贡献代码与工具,推动了大量第三方类库的涌现,这些类库丰富了Java的功能体系,形成了强大的生态系统。

image-20260212085518775

java要求文件的名字和类名完全一致

打开记事本:

public class Test{
    public static void main(String[] xxx}{
        Systom.out.println("大家好");
    }
}

编码改为CRLF,windows环境编码

然后先运行​生成class文件

然后运行​执行代码输出。

public class Test{
    public static void main(String[] xxx}{
        for(int i=0;i<xxx.length;i++){
            System.out.println(xxx[i]);
        }
        
    }
}

操作系统会记录主方法路径,当用户运行一个名为 ​ 的可执行程序时,程序会先将自身解压(.exe实际上就是win系统可识别的一个压缩包),接着会读取一个固定的配置文件(win开发规定),从中获取关键信息,包括主函数所在的路径、支持的可识别文件拓展名以及 logo 路径等。

能记录不同的拓展名哪些程序能识别它,也是在安装程序的过程中操作系统记录的

java基础

面试部分

变量和数据类型*

Java 是一门高级语言,但它最终运行在由 C/C++ 编写的 Java 虚拟机(JVM)之上。这意味着 Java 的所有变量,无论看起来多么抽象,最终都要依赖C语言,并映射到物理内存中的字节。而物理内存的操作,是由 CPU 和操作系统共同决定的。

现代计算机的 CPU 无法直接操作“1 位”或“半个字节”。它能读写的最小单位是 1 个字节(8 位)。这个限制来自硬件架构,也体现在 C 语言中——C 的 char 类型就是 1 字节,是最小可寻址单位。JVM 作为用 C/C++ 写成的程序,自然也继承了这一约束。所以,

boolean 作为作为局部变量或操作数栈中的值时,通常它会被当作 int 来处理,占用 32 位(4 字节)。这是因为 JVM 的字节码指令集(如 istore、iload)是以 32 位为基本操作单元的。用 32 位处理 boolean 可以复用整数指令,简化虚拟机设计,提升执行速度。

类型根据编码决定,默认16位两字节,为了支持国际化字符(Unicode)

类型的占位多少:默认32bit

现代操作系统给的存储最细粒度一般是4kb,因此就算jvm一次申请小于4kb的空间也会分配4kb(比如只申请一个int型变量),然后java底层是c,c语言最细粒度是char类型的8bit,因此java最细粒度也是8bit,这4kb java操作最小粒度是一次操作8bit。

变量赋值可以使用其他进制,比如:

long text = 012; // 8进制
double d = 0x12345678; // 16进制

java在编译时不允许窄化转换,比如

int a = 10;

byte c = a;

但是! 如果满足以下条件,允许将 ​ 类型的赋值给 ​、​ 或 ​:

final int a = 10;

byte c  = a;

byte short char三种类型的数据在运算时,会先直接提升为int再参与运算

数学函数

​ 中的大多数方法(如 ​, ​, ​, ​)是 native 方法,也就是说它们的实际计算逻辑并不在 Java 代码里,而是由 JVM 调用底层 C/C++ 编写的本地库(比如操作系统的数学库 libm,或 JVM 自带的实现)。

Java 的数学函数底层依赖高效的数值逼近算法,这些算法在思想上受到泰勒级数等数学工具的启发,但实际实现中使用的是经过高度优化的多项式逼近或其他方法,而非直接展开泰勒级数。

包装类

Integer a = 12 这种写法是合法的,其背后原理是 Java 5 引入的自动装箱机制,编译器会将其自动翻译为 Integer.valueOf(12) 。由于 12 处于 -128 到 127 的默认缓存范围内,JVM 不会创建新对象,而是直接从 IntegerCache 缓存池中返回一个已存在的 Integer 对象引用。因此,这种写法不仅代码简洁,还能复用对象、节省内存;但需警惕后续使用 == 进行比较时可能产生的逻辑错误(在缓存范围内 == 返回 true ,超出范围则可能返回 false ,应始终使用 equals() 比较值)。

有缓存的: Integer 、 Long 、 Short 、 Byte 、 Character 、 Boolean 。

无缓存的: Float 、 Double 。

包装类型的循环(缓存池发挥作用的地方) // 这种情况通常出现在需要放入集合(如 List)时 List<Integer> list = new ArrayList<>(); for (int i = 0; i < 100; i++) { list.add(i); // 这里发生了自动装箱:Integer.valueOf(i) }在这个循环中, i 被装箱成 Integer 对象放入集合。 如果没有缓存池:循环 100 次会创建 100 个新的 Integer 对象。 有了缓存池:因为 i 的值(0 到 99)都在缓存范围内,所以这 100 次装箱操作,实际上只是从缓存数组中取了 100 次同一个引用(或者少量几个引用),没有创建新对象。

特性基本类型(非包装类)包装类
原生数据类型(非对象)引用类型(​ 中的类,是对象)
栈上(局部变量)或直接嵌入对象中堆上(对象),引用在栈上
局部变量无默认值;成员变量有(如 ​ 默认为 ​)对象引用默认为
❌ 不能✅ 可以为
❌ 不能✅ 可以(如 ​)
❌ 不行(如 ​ 编译错误)✅ 可以(如 ​)
⚡ 高效(无对象开销)🐢 有对象创建/垃圾回收开销
——支持自动装箱(boxing)和拆箱(unboxing)

各个包装类提供方便的方法,比如转二进制等等

自动装箱

在 Java 中,(autoboxing)是指将基本数据类型(如 ​)自动转换为对应的包装类对象(如 ​),而(unboxing)则是将包装类对象自动转换回基本数据类型。例如,​ 会自动调用 ​ 进行装箱,而 ​ 则会自动调用 ​ 进行拆箱。当你使用 ​ 比较一个 ​ 和一个 ​(如 ​)时,Java 会先对 ​ 对象 ​ 执行,将其转为 ​ 值,然后再比较两个 ​ 的数值;由于 ​ 和 ​ 的值都是 ​,因此结果为 ​。需要注意的是,这种行为仅在一方是基本类型、另一方是包装类时发生;若两边都是包装类(如 ​),​ 比较的是对象引用而非值。

运算符和逻辑

初始:x5 = 5

执行 ​ 后:x5 仍然是 5。

这个行为在 Java 中是确定的、可预测的,

在任何赋值语句或表达式中,操作数(operands)总是按照从左到右的顺序被完全求值,然后再执行操作本身(比如赋值、加法等)

在Java中,+运算符会根据操作数的类型呈现不同行为:当两边均为数值类型时执行数学加法,而任意一边为字符串类型时则触发字符串拼接(此时会先把数值转换为字符串)。结合求值顺序,如1 + 99 + "年黑马"会先按从左到右顺序计算1 + 99得到数值100,再处理100 + "年黑马"——因右侧是字符串,100会被自动转为字符串完成拼接,最终结果为"100年黑马"。

byte a = 10;

byte b = 20;

// ❌ 错误!编译不通过

// a = a + b; // 这里 a+b 的结果是 int 类型,不能直接赋给 byte 类型的变量

// ✅ 正确!使用复合运算符

a += b; // 编译器会自动帮你加上强制转换:a = (byte)(a + b);

int x1 = 29; // 二进制: 11101

int x2 = 19; // 二进制: 10011

int x3 = 7; // 二进制: 00111

        int x1 = 29; // 二进制: 11101
        int x2 = 19; // 二进制: 10011
        int x3 = 7;  // 二进制: 00111
        // 按位与(&):全1为1,否则为0
        int andResult = x1 & x2; // 11101 & 10011 = 10001 → 17
        System.out.println("x1 & x2 = " + andResult); // 输出: 17
        // 按位或(|):有1为1,全0为0
        int orResult = x1 | x2;  // 11101 | 10011 = 11111 → 31
        System.out.println("x1 | x2 = " + orResult);  // 输出: 31
        // 按位异或(^):相同为0,不同为1
        int xorResult = x1 ^ x2; // 11101 ^ 10011 = 01110 → 14
        System.out.println("x1 ^ x2 = " + xorResult); // 输出: 14
        // 按位取反(~):0变1,1变0(使用补码表示,结果为负数)
        int notResult = ~x1;     // ~29 = -30 (因为 ~n == -(n+1))
        System.out.println("~x1 = " + notResult);     // 输出: -30

,其核心思想是将乘法转化为加法与左移操作的组合。由于左移 ​ 在二进制层面等价于 ×2,因此将右操作数表示为若干个 2 的幂之和时(即其二进制表示中 1 的位数较少),可通过分解该乘数并累加对应的左移结果高效完成乘法。

>>> 右移补0,>> 补符号位

数组

算法语法查看速查手册

Java 强调安全性,禁止数组越界访问。每次访问 ​ 时,JVM 都会自动插入检查

if (i < 0 || i >= arr.length) {
    throw new ArrayIndexOutOfBoundsException();
}

为了高效完成这个检查,必须能快速获取数组长度。因此,JVM 将长度直接存放在数组对象的头部,通过 ​ 可以 O(1) 时间获取。

问题答案
数组是对象吗?是,由 JVM 特殊实现的对象
length 从哪来?JVM 在数组对象头部存储的元数据
为什么需要 length?支持安全的边界检查、统一访问接口
能修改 length 吗?不能,它是只读的;数组大小一旦创建就固定
和 ArrayList.size() 有何不同?ArrayList 是动态容器,size() 是逻辑大小;数组的 length 是物理容量且不可变
元素类型默认值说明
byte(byte) 0字节型
short(short) 0短整型
int0整型
long0L长整型
float0.0f单精度浮点
double0双精度浮点
char'\u0000'空字符(Unicode 0,不是空格或 null)
booleanFALSE布尔值
引用类型 (如 String, Object, 自定义类等)null所有对象引用默认为 null

数组也是有类型的,只不过这个类型不是有程序员自己定义的类, 也不是jdk里面的类, 而是虚拟机在运行时专门创建的类。类型的命名规则是:每一维度用一个[表示; [后面是数组中元素的类型(包括基本数据类型和引用数据类型) 在java语言层面上,s是数组,也是一个对象,那么他的类型应该是String[], 但是在JVM中,他的类型为[java.lang.String]顺便说一句普通的类在JVM里的类型为 包名+类名, 也就是全限定名

,也称为内置类型,包括byte、short、int、long、float、double、char和boolean。 它们与引用数据类型共同构成Java的数据类型体系

字符串*

字符串值不可变,比如一个字符串值发生改变,实际上是指向发生了改变,而原区域值不变。

字符串String x5 = "";是个char类型 {'\0'}字符串通常以null结尾,即字符'\0'。这是为了方便C风格的字符串处理。

但是x5 = null; 是没有任何指向

字符串常量池

是 JVM 为优化字符串存储而维护的一个特殊内存区域,位于堆中(JDK 7 及以后),用于存放所有字符串字面量(如 ​)和通过 ​ 方法手动加入的字符串;其核心特点是:内容相同的字符串在池中只存一份,多个引用共享同一对象,从而节省内存、提升性能。使用 ​ 会绕过常量池,在堆中创建新对象,因此与字面量引用地址不同,但要注意abc仍然会在常量池放一份。

String a = "aaa";(字面量)❌ 通常不能被 Class 强引用,类不卸载则不回收

例如,在编译期,Java 编译器会直接将 ​ 优化为字面量 ​,并放入字符串常量池。如果有​存在,则会直接返回常量池的引用。

方法

方法是程序中的最小执行单元

方法的重载:同类同名不同参(包括交换形参顺序),且与返回值无关。

方法的重写:子类提供一个与父类中的方法实现

特性重写(Override)重载(Overload)
✅ 是!运行时根据决定调用哪个实现❌ 否!编译时根据决定调用哪个方法
必须相同(方法名 + 参数列表一致)必须不同(参数列表不同)
运行时(动态绑定)编译时(静态绑定)

向上转型

在 Java 中,:当一个方法声明接收父类类型的参数(如 ​),而传入的是其子类对象(如 ​ 继承自 ​),该调用完全合法,因为子类对象会自动向上转型为父类类型。这正是多态的基础——方法内部通过父类引用操作对象,若该方法在子类中被重写,则实际执行子类的实现

传递

Java 中,没有引用传递。具体来说:

引用传递:比如你传入一个 (O a, O b)

值传递下,你a = b,不会影响外部,因为ab存的是引用类型的地址,是值传递

但是引用传递下,a = b,外部对象a也会指向b,那么a原有的信息就会丢失

对象*

成员变量访问的 System.out.println(age);

System.out.println(this.age);

先找有没有局部变量,然后再找成员变量。

标准

用于开发

默认调用toString()方法输出对象类名+计算得出的哈希地址值(先检查非null)

初始化顺序

public class InitializationOrder {

    // ========== 类加载时(仅执行一次)==========
    // 按源码顺序执行:静态变量初始化 + 静态代码块(从上到下)
    static {
        System.out.println("1. 静态代码块");
    }
    private static String staticField1 = "静态字段1";
    private static String staticField2 = initStatic();

    // ========== 对象创建时(每次 new 都执行)==========
    // 按源码顺序执行:实例变量初始化 + 非静态代码块(从上到下),最后执行构造器
    {
        System.out.println("2. 非静态代码块");
    }
    private String instanceField1 = "实例字段1";
    private String instanceField2 = initInstance();

    public InitializationOrder() {
        System.out.println("3. 构造器");
    }

    // ========== 辅助方法 ==========
    private static String initStatic() {
        System.out.println("→ 静态字段初始化方法");
        return "static";
    }

    private String initInstance() {
        System.out.println("→ 实例字段初始化方法");
        return "instance";
    }

    // ========== 测试 ==========
    public static void main(String[] args) {
        new InitializationOrder();
        System.out.println("---");
        new InitializationOrder(); // 静态部分不再执行
    }
}

访问和修改

就是getter和setter方法,帮助我们安全读取和设置对象私有字段值的

比如一个对象内有引用字段

public class Car {
	private int a;
	private int[] arr = {1, 2, 3};
}

如果直接写

public int[] getArr(){
	return arr;
}

那么外部修改了这个arr,别的线程获取这个arr就是被修改了的(浅拷贝),这时需要手写深拷贝去获取arr

public int[] getArr(){
	return arr.clone();
}

在 Java 中,

但是String是不可变实例,是安全的哦

@Data和@Setter, @Getter生成的访问和修改器是不安全的,谨慎使用。

关键字详解

static

static方法一般和final一起用。因为final线程安全且不可变,干脆对外暴露,这样多线程读就无需先生成对象

final

final List<String> list = new ArrayList<>();
list.add("Hello"); // 合法!修改的是对象内部状态
list = new ArrayList<>(); // 编译错误!不能让 list 指向另一个对象

因此final修饰可变引用类型没啥作用,final不修饰可变引用类型

Java 标准库中的 ​、​、​ 等类都是 ​ 的,原因包括:

注意:​ 类仍然可以继承其他类(只要那个父类不是 final),只是自己不能再被继承。

根据 Java 内存模型(JMM) 的规定:

在构造器中正确初始化的 ​ 字段,在对象“安全发布”后,对所有线程都是立即可见的,无需额外的同步(如 volatile 或锁)。

这意味着:

简单说:在对象还没有完全构造好之前,就把它的引用(this)暴露给了其他代码(比如赋值给静态变量、传给其他线程、注册监听器等),这就叫 “this 逸出”。

比如先​; 还没有给value初始化就让一个变量​,于是读取 ​;结果读到了 未初始化的值 0,而不是预期的值。

JMM 。这保证了 final 字段的初始化一定在对象构造完成前完成。

举个例子:

class FinalExample {
    final int x;
    int y;
    FinalExample() {
        x = 3; // final 字段
        y = 4; // 普通字段
    }
}

线程 A 创建对象:​ 线程 B 读取:

因此,​ 不仅是“不可变”的语义标记,更是并发安全的重要工具。它使得 immutable 对象(所有字段都是 final 且无 setter)天然线程安全。

this

在 Java 中,​ 关键字代表当前对象的引用,主要用于在类的内部明确访问本对象的成员变量(尤其在与局部变量或参数同名时)、调用本类的其他构造器(通过 ​ 实现构造器重载)、返回当前对象以支持链式调用,或将当前对象作为参数传递给其他方法或构造器,从而清晰地指代“正在操作的这个实例”。

在 Java 中,所有对象都继承自 ​ 类,其默认的 ​ 方法底层确实是使用 ​ 比较两个引用是否指向同一个对象(即内存地址是否相同)。但像 ​、​ 等类重写了 ​,使其转为比较对象的逻辑内容是否相等。然而,根据 Java 规范,一旦重写了 ​,就必须同时重写 ​,以保证“相等的对象具有相同的哈希码”。这一点至关重要,因为像 ​、​ 等基于哈希表的集合,在存储和查找元素时,先通过 ​ 定位桶(bucket)位置,再用 ​ 判断是否真正相等。如果两个逻辑相等的对象 ​ 返回 ​,但 ​ 不同,它们会被放入不同的桶中,导致 ​ 无法正确检索到已存在的键,从而破坏集合的正确性。因此,​ 和 ​ 必须协同一致地重写,这是实现自定义类作为 Map 键或 Set 元素的前提。

比如同名同姓但是不同时间的数据认为是一个数据,就重写了equals方法,但是没有重写hash,所以我逻辑上同一个数据在桶中可能就不在一起

永久代和元空间

特性永久代(PermGen)元空间(Metaspace)
JDK 7 及之前
(受 ​ 限制)(Native Memory,操作系统内存)
类元数据、常量池、静态变量、JIT 代码等(静态变量、常量池移到堆中)
是(但回收效率低)是(更高效,与类加载器生命周期绑定)
较小(如 64MB~82MB)(受限于系统可用内存)
​, ​,

📌 注意:从 JDK 7 开始, 就已经从 PermGen 移到了堆中;JDK 8 彻底移除 PermGen,用 Metaspace 替代剩余部分。

多线程*

wait和sleep的区别

​ 会释放对象锁并等待其他线程通知,必须在 ​ 块中调用;​ 只是让当前线程暂停执行,,可在任意地方调用。

要想调用wait(),必须先持有锁,但是sleep()不需要

并发和并行

:多线程独立运行,互不竞争资源:AI矩阵运算(GPU)

: 多线程互相竞争资源,共享资源

语法部分

输入

日常开发Scanner

import java.util.Scanner;
Scanner input = new Scanner(System.in);
		//2.接收用户的输入(接收用户输入整数,接收用户输入小数,接收用户输入字符串)
		//2.1接收用户输入整数
		System.out.println("请输入一个整数");
		int n =input.nextInt();
		System.out.println("您输入的是:"+n);
		
		//2.2接收用户输入小数
		System.out.println("请输入一个小数");
		double m = input.nextDouble();
		System.out.println("您输入的是:"+m);
		
		//2.3接收用户输入字符串
		System.out.println("请输入一个字符串");
		String str = input.next(); // 遇到空格结束,不会清空缓冲区的/n!
		String str1 = input.nextLine(); // 遇到换行符结束
		System.out.println("您输入的是:"+str);
		System.out.println("您输入的是:"+str1);

算法竞赛InputStreamReader和StringTokenizer

输入样例:

5
3 1 2 4 5
InputStreamReader in = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(in);
int num = Integer.parseInt(br.readLine());
int[] arr = new int[num];

// 如果输入是 "9,3,6,9,5" (逗号分隔)
String line = "9,3,6,9,5";
// 第二个参数指定分隔符为逗号
StringTokenizer st = new StringTokenizer(line, ","); 

while(st.hasMoreTokens()) {
    System.out.println(st.nextToken());
}

建议用Scanner的nextDouble();

package lanqiaobeiTraining;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Scanner;

public class Main {
	public static void main(String[] args) throws NumberFormatException, IOException {
		Scanner ins = new Scanner(System.in);
		double n = ins.nextDouble();
		double l = -10000, r = 10000;
		while(r - l > 1e-8) { // 比较精度要高一些
			double mid = l + (r - l) / 2;
			if(mid * mid * mid >= n) {
				r = mid; // 不需要再偏移,因为浮点数不会出现偏移问题
			}else {
				l = mid;
			}
		}
		System.out.println(String.format("%.6f", l));
	}
}

String

​ / ​ /

String和StringBuffer,StringBuilder的区别有哪些?所有类名包含Buffer的类的内部实现原理是什么?有什么优势?

特性StringStringBufferStringBuilder
(Immutable) (Mutable) (Mutable)
(因为不可变) (方法加了synchronized锁) (无同步锁)
最低(频繁拼接会产生大量中间对象)较低(有加锁/释放锁的开销)(单线程下操作最快)
final byte[] (Java 9+) 或 final char[]byte[] / char[] (无final)byte[] / char[] (无final)
字符串很少修改,或作为常量时多线程环境下频繁修改字符串单线程环境下频繁修改字符串

String 内部使用一个字符数组 value 来保存字符串的内容。这个数组被 final 修饰并且private 封装 + 不提供 Setter 方法,以及String类为final来保护其内容,但是仍可被反射修改

所有 Buffer 类的内部都会维护一个特定类型的数组(如 char[]、byte[])。这个数组就是所谓的“缓冲区”。

当涉及到读取和写入时,缓冲区能一读/写较多数据从而避免了频繁的IO请求。

StringBuffer,StringBuilder快捷拼接字符串的关键在于其内部维护的缓冲区,因为String是不可变类,String的拼接涉及创建新对象,而缓冲区可避免这一过程,只是偶尔进行一次数组扩容拷贝

以下不做特殊标记均默认所属String

方法说明所属类
字面量(存入字符串常量池)
堆中新建对象(绕过常量池)
​ / 从字符数组构造
​ / 可指定初始容量
​ / 同上,线程安全版本

方法说明
返回字符串长度(字符数)
获取指定索引处的字符(0 起始)
判断是否为空字符串(长度为 0)

方法说明所属类
​ 运算符编译期优化为 ​(仅限常量表达式)​ (底层转换)
拼接返回新
,原地修改​ /
在指定位置插入内容​ /

:循环内拼接务必用 ​,避免 ​ 导致 O(n²) 性能问题。

:

方法说明
​ / 正向查找首次出现位置
反向查找最后一次出现位置
是否包含子串
是否以某前缀开头
是否以某后缀结尾
是否匹配正则表达式

​/上述方法,需 ​ 后调用

方法说明
截取从 begin 到末尾
截取 [begin, end) 子串
返回子序列(通常用于接口兼容)
按正则表达式分割字符串
限制分割段数

​/​ 需转为 ​ 后操作。

方法说明
替换所有指定字符
替换子串(JDK 1.5+)
正则全局替换
正则替换第一个匹配项
(非内容匹配)

方法说明
转小写(默认 Locale)
转大写(默认 Locale)
​ / 指定区域规则转换

方法说明
去除首尾空白字符(Unicode 空白,如 ​, ​ 不包括)

方法说明
转为字符数组
使用默认编码转字节数组
指定编码(如 "UTF-8")转字节数组
将基本类型(int, double 等)转为字符串

方法说明
​*内容相等判断(区分大小写)
忽略大小写比较
字典序比较(返回负/0/正)
忽略大小写的字典序比较

[^字典序]: 字典序就是,如同单词在字典中的排列顺序

需要借助 ​ (Java 国际化 API)

import java.text.Collator;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("中国", "美国", "日本", "德国", "法国");

        // 创建中文(中国)的 Collator,按拼音排序
        Collator collator = Collator.getInstance(Locale.CHINA);
        collator.setStrength(Collator.PRIMARY); // 只比较拼音,忽略声调

        list.sort(collator);
        System.out.println(list);
    }
}

方法说明
(高效)
删除指定区间
删除单个字符
修改指定位置字符

    // 1. 基本用法:仅分隔符
    StringJoiner sj1 = new StringJoiner(", ");
    sj1.add("Apple").add("Banana").add("Orange");
    System.out.println("基本拼接: " + sj1); 
    // 输出: Apple, Banana, Orange

    // 2. 带前缀和后缀(如生成列表或 JSON 数组)
    StringJoiner sj2 = new StringJoiner(", ", "[", "]");
    sj2.add("Java").add("Python").add("Go");
    System.out.println("带括号: " + sj2); 
    // 输出: [Java, Python, Go]

    // 3. 空时返回默认值
    StringJoiner sj3 = new StringJoiner(", ", "{", "}");
    sj3.setEmptyValue("{}"); // 没有 add 任何元素时使用
    System.out.println("空结果处理: " + sj3); 
    // 输出: {}

Math库

方法说明
返回算数平方根
绝对值
向上取整
向下取整
四舍五入
获取较大值/较小值
返回a的b次幂的值

System库

提供一些与系统相关的方法

方法说明
exit(int status)终止当前运行的虚拟机
currentTimeMillis()返回当前系统的时间毫秒值形式(long)
arraycopy(arr1, index, arr2, index, length)数组拷贝

currentTimeMillis()的起始时间原点是1970年1月1日 0:0:0,也就是c的诞生日。

Runtime库

表示虚拟机当前的运行环境

方法名说明
public static Runtime getRuntime()当前系统的运行环境对象
public void exit(int status)停止虚拟机(终止当前运行的虚拟机)
public int availableProcessors()获得CPU的线程数
public long maxMemory()JVM能从系统中获取总内存大小(单位byte)
public long totalMemory()JVM已经从系统中获取总内存大小(单位byte)
public long freeMemory()JVM剩余内存大小(单位byte)
public Process exec(String command)运行cmd命令

Arrays库

操作数组的工具类

方法名说明
把数组拼接成一个字符串(如 ​)
使用二分查找法在已排序数组中查找指定元素,返回索引(未找到返回负值)
创建新数组,复制原数组内容,长度可指定(不足补0,超出截断)
复制原数组指定范围内的元素(左闭右开)
将数组所有元素填充为指定值
按默认升序方式对数组进行排序(适用于基本类型和对象)
按照指定的比较规则对数组排序(需传入 ​)

大数处理

​和​用来处理大型数据,比如超过8字节能表示的

方法名说明
获取随机大整数,范围:[0 ~ 2^num - 1]
获取指定的大整数(默认十进制)
获取指定进制的大整数(如二进制、十六进制等)
静态方法获取 ​ 对象,内部有优化

​​

对象一旦创建,内部记录的值不能发生改变。所有操作(如加、减、乘)都会返回新的 ​ 对象。

其给定的成员方法基本符合英语单词,比如subtract为减法。

BigInteger底层实际是java的int数组,因此存储上线取决于java数组的最大长度(int的最大值2147483647),并且数组每一位表示42亿多(每个数组元素是一个 32 位无符号整数)因此总量来到42亿的21亿次方

方法基本同BigInteger

集合

image-20260212085606728

集合框架

Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 、、、LinkedHashSet、、LinkedHashMap 等等。

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。

image-20260212085616146

Java 集合框架提供了一套性能优良,使用方便的接口和类,java集合框架位于java.util包中, 所以当使用集合框架的时候需要进行导包。

集合里不能直接存基本数据类型

Collections集合工具类

常用API:

方法名称说明
public static <T> boolean (Collection<T> c, T... elements)批量添加元素(单列集合)
public static void (List<?> list)打乱 List 集合元素的顺序
排序(按自然顺序)
根据指定的规则进行排序
public static <T> int (List<T> list, T key)以二分查找法查找元素
拷贝集合中的元素
使用指定的元素填充集合
根据默认的自然排序获取最大/最小值
交换集合中指定位置的元素

集合进阶练习

需要集合,IO,多线程,带权重的随机

不可变集合

如果某个数据不能被修改,把它防御性的拷贝到不可变集合是个很好的实践

比如集合被某个不可信的库调用,不可变形式是安全的

可变参数可传数组,也可一个个传

List<T> list = list.of(...); // 可变参数
该集合是不可更改的
还有Set集合
Set.of(...); // 可变参数
Map.of(...); // 最大支持传入20个,因为底层不是可变参数
Map.ofEntries(Entry<K, V>); // 可变参数 
Map.copyOf(Map<K, V>); // 将一个可变Map转为不可变

List

有序,可重复,有索引

ArrayList
方法说明
(高效)
在指定位置插入元素,原位置及之后元素后移
将指定集合的所有元素追加到列表末尾
从指定位置开始插入集合中的所有元素
替换指定位置的元素,返回被替换的旧值
删除指定位置的元素,返回被删除的元素
删除列表中第一个匹配的元素(按 ​)
删除列表中所有包含在指定集合中的元素
列表中也包含在指定集合中的元素
,移除所有元素
判断列表是否为空(无元素)
判断列表是否包含指定元素(使用 ​)
返回指定元素首次出现的索引,未找到返回 -1
返回指定元素最后一次出现的索引
转换为指定类型的数组(推荐传入 ​)
返回 ​ 的(原列表修改会反映到子列表)
对每个元素执行指定操作(Java 8+)
删除满足条件的所有元素(Java 8+)
使用函数替换每个元素(Java 8+)
根据指定比较器对列表排序(Java 8+)
返回 ArrayList 的浅拷贝(注意:非泛型安全)

💡

  • ​​ 是的,适用于单线程环境。
  • 底层基于,随机访问(​/​)时间复杂度为 ,中间插入/删除为 。
  • 所有索引均从 ​ 开始,​ 在 ​ 等方法中表示(左闭右开)。
LinkedList

底层数据结构是双链表

特有方法说明
public void addFirst(E e)在该列表开头插入指定的元素
public void addLast(E e)将指定的元素追加到此列表的末尾
public E getFirst()返回此列表中的第一个元素
public E getLast()返回此列表中的最后一个元素
public E removeFirst()从此列表中删除并返回第一个元素
public E removeLast()从此列表中删除并返回最后一个元素

Set

无序,不重复,无索引

其基本方法与collection接口类似:

方法名称说明
把给定的对象添加到当前集合中(已存在返回false)
清空集合中所有的元素
把给定的对象从当前集合中删除
判断当前集合中是否包含指定对象
判断当前集合是否为空
返回集合中元素的个数 / 集合的长度

这里介绍三种遍历方法:

       lambda:
       ts.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });
HashSet

无序,不重复,无索引

是哈希表的灵魂,哈希值是对象的整数表现形式,是对象通过哈希函数计算出的一个整数。

按下Alt + Insert,可以让idea生成equals()和hashCode()方法

   @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

因为是数组+链表的方式,因此计算数组下标公式是: int index = (数组长度 - 1) & 哈希值;

如果index重复,先比较属性值(调用equals()方法判断,因此需要重写)如果相同就舍弃,不同就会将新元素用链表挂在老元素下面。

当数组快满时会扩容,当链表长度大于8且数组长度大于64会转成红黑树

因此如果集合中存储的是自定义对象,必须重写hashCode和equals方法

三个问题:

LinkedHashSet

有序,不重复,无索引

继承自HashSet,每个元素又额外多了一个双链表机制记录存储顺序

要求去重且存取有序,才用LinkedHashSet

TreeSet

不重复,无索引,

排序原理: 对于数值类型,默认按照从小到大排序

对于字符,字符串类型,按照ASCLL码表中数字升序进行排序

两种方式

1.实现Comparable接口,重写抽象方法compareTo,默认是这种,因为排序规则内聚于类本身,是很多api以及Array.sort的默认排序规则

  public class Student implements Comparable<Student> {
  	@Override
    public int compareTo(Student o) { // o表示已经在红黑树存在的元素
        return this.getAge() - o.getAge(); // 小的存左边
    }

2.,创建TreeSet对象时,传递比较器Comparator指定规则

比如包装类String默认是按照字典序排序,那么如果想让短的在前,长的在后,一样长的比较字符,那么就需要传递比较器,因为包装类不是自定义类不能修改。方便快速,适合临时使用或临时更改排序规则

        TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                int i = o1.length() - o2.length();
                return i != 0 ? i : o1.compareTo(o2);
            }
        });
        // 推荐使用Lambda 表达式
        TreeSet<String> ts = new TreeSet<>((o1, o2) -> {
            int i = o1.length() - o2.length();
            return i != 0 ? i : o1.compareTo(o2);
        });

Map

双列集合,键值对应,一个键值对java叫对象

image-20260212085631818

常见API,顶层接口

方法名称说明
V put(K key, V value)添加元素,重复会覆盖,返回被覆盖的值
V remove(objcet key)根据键删除元素, 返回删除的值
void clear()移除所有元素
boolean containsKey(Object key)判断集合是否包含指定键
boolean containsValue(Object value)判断集合是否包含指定的值
boolean isEmpty()判断集合是否为空
int size()集合的长度,键值的对数
Set<> keySet();返回键集合
V putIfAbsent(K, V);仅当键不存在才会插入,返回当前已存在的 value

Set<String> keys = map.keySet();
for(String a: map.keySet()){
	String value = map.get(key);
	System.out.println(key + "=" + value);
}

先来介绍Entry:

   // Entry是一个键值对对象,键值对对象有getKey()和getValue()方法
        Map.Entry<String, String> entry;
for(Map.Entry<String, String> entry : mp.entrySet()){
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }

HashMap

1个 null 键和多个 null 值

基本方法和Map一致,基础实现类

特点都是由键决定的:无序,不重复,无索引

,都是哈希表结构,数组+链表+红黑树

LinkedHashMap

和LinkedList一样,有序,不重复,无索引

多一条双向链表记录存储顺序

TreeMap

底层同TreeSet,红黑树

由键决定特性,不重复,无索引,可排序。默认按照键的从小到大排序,也可自定义键的排序规则:

迭代器

在 Java 中,只要一个类实现了 ​ ,它就可以使用 ,也就可以提供 ​。

​,所以它们天然支持迭代器。

ArrayList<Integer> list = new ArrayList<>(); 
Iterator<String> it = list.iterator();
while(it.hasNext()){
	String str = it.next();
	System.out.println(str);
}

注意,当需要在遍历时进行删除/增加/修改等操作,使用,不能用集合的方法

其实for(String s : list)就是采用了迭代器

还可用lambda表示

Stream流

好处是Stream 提供声明式、链式、惰性求值的数据处理方式,代码更简洁、可读性更强,并天然支持并行操作。

ArrayList<String> as = new ArrayList<>();
as.add("张雪放");
as.add("张三");
as.add("张三");
as.add("张五十");
as.stream().filter(s -> s.startsWith("张") && s.length() > 2).forEach(System.out::println);
    

Stream流类似工厂流水线,将目标集合的数据一条条的取出来并进行一系列中间方法后经由终结方法得到最终输出、

获取方式方法名说明
单列集合Collection 中的默认方法
双列集合无法直接使用 stream 流
数组Arrays 工具类中的静态方法
一堆零散数据Stream 接口中的静态方法

方法名称描述返回类型
过滤流中的元素,保留满足条件的元素
对流中的每个元素执行给定的操作,并将结果映射到一个新的值
类似于 map,但每个元素被映射到一个流,然后这些流会被合并成一个流
去重操作,基于元素的 ​ 方法
自然排序(根据元素的 ​ 方法)
根据提供的比较器进行排序
对每个元素执行给定的操作,主要用于调试
返回由该流的前 n 个元素组成的流
跳过前 n 个元素,返回剩余元素组成的流
合并两个流为一个流

中间方法会返回新的Stream流,因此建议使用链式编程

方法名称描述返回类型
对流中的每个元素执行给定的动作
将流中的元素收集到集合、列表、Map等数据结构中
将流中的元素收集到一个数组中
使用指定的归约操作对流中的元素进行累积计算​ 或
根据提供的比较器找到最小元素
根据提供的比较器找到最大元素
计算流中元素的数量
测试流中是否有至少一个元素匹配给定的谓词
测试流中的所有元素是否都匹配给定的谓词
测试流中是否没有元素匹配给定的谓词
返回流的第一个元素,如果流为空则返回空的 Optional
返回当前流中的任意元素,如果流为空则返回空的 Optional。在并行流中,可能提高效率

数组是 Java 最底层的数据结构之一,诞生于 Java 1.0,而 Stream API 是 Java 8 才加入的。 为了,不能给所有数组类型加方法(数组不是类,无法继承或扩展)。

所以,Java 团队在工具类 ​ 中提供了静态方法 ​ 来“桥接”数组和 Stream。

可变参数

要是计算N个数据的和?

没有可变参数前,getSum(int []arr)

在jdk5以后提出可变参数:方法形参个数可以变化

底层就是个数组

方法的形参中最多只能写一个可变参数,且只能写在参数最后面

格式:属性类型...名字:

    public static void main(String[] args) {
        getSum(1,2,3,4,5);
    }
    public static void getSum(int...args) {
        int sum = 0;
        for (int i : args) {
            sum += i;
        }
    }

方法引用

将已有的方法当作函数式接口中的抽象方法的方法体

类名::静态方法名

例: ​ // 字符串转int

对象实例::成员方法

为了创建对象,比如Student::new

list是一个字符串列表
Student[] su =  list.stream().map(Student::new).toArray(Student[]::new);

利用Student类的构造函数创建对象

list.stream().map(String::toUpperCase).forEach(System.out::println);

// String::toUpperCase 看起来像类名引用,但它其实是一个“实例方法引用”,只不过 Java 允许用类名来引用该方法,前提是传入的对象就是该类的实例。

注意:

格式 数据类型[]::new

ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4);
Integer[] arr2 = list.stream().toArray(Integer[]::new);

面向对象

​ Java 构造器的主要作用是在创建对象时初始化该对象的状态,即为对象的成员变量赋予合 适的初始值,并执行必要的设置操作;它与类同名、没有返回类型,且在使用 ​ 关键字 创建对象时自动被调用,确保每个新创建的对象都处于一个有效、可用的初始状态。

修饰符讲解

修饰符可修饰目标核心特性注意事项好处
类、接口、方法、变量完全公开:任何地方可访问。一个文件中的顶级类必须是与文件名相同的那个才能被​修饰提供最大灵活性,便于模块间调用和 API 暴露;支持跨包复用。
方法、变量(不能修饰顶级类)包内 + 所有子类可见(即使子类在不同包)。比默认访问更宽;常用于父类设计供继承使用。在封装性和继承性之间取得平衡,便于扩展而不完全暴露内部实现。
default(无)类、方法、变量包内可见(package-private)。不写修饰符即默认;常被忽略但高频考。限制访问范围到同一包,增强内聚性,适合包内协作而对外隐藏细节。
方法、变量、内部类仅本类可见;最强封装。外部无法直接访问;通过 getter/setter 控制。最大程度保护数据安全和类的内部状态,提升代码健壮性和可维护性。
变量、方法、代码块、内部类属于类,非实例;所有对象共享;无 ​;类加载时初始化。静态方法不能访问非静态成员;工具类常用。节省内存(无需实例)、便于工具方法/常量统一管理、启动时即可使用。
类、方法、变量不可变:
• 类 → 不能被继承
• 方法 → 不能被重写
• 变量 → 只能赋值一次
​ 变量是常量;匿名内部类访问局部变量需 ​(或 effectively final)。增强安全性与稳定性,防止意外修改;利于编译器优化;明确设计意图。
类、方法未实现:
• 抽象类不能实例化
• 抽象方法无方法体,子类必须实现(除非也是抽象类)
​ 与 ​/​/​ 冲突(不能共存)。支持模板方法模式,定义通用结构,强制子类实现特定行为,提高代码复用。
方法、代码块线程同步:确保同一时间只有一个线程执行该段代码。实例方法锁 ​,静态方法锁 ​ 对象。保证多线程环境下的数据一致性,避免竞态条件和脏读。
变量内存可见性 + 禁止指令重排序;不保证原子性。常用于状态标志(如 ​);比 ​ 轻量。轻量级线程通信机制,确保变量更新对所有线程立即可见,提升并发性能。
实例变量不参与序列化;反序列化时为默认值(如 ​, ​)。用于敏感字段(如密码)或临时数据。保护隐私数据不被持久化,节省存储空间,避免序列化无关或临时状态。

顶级类 = 写在最外层的类,不在类的内部;

​ 方法为什么是 ​? → JVM 启动时还没有创建任何对象,必须通过类直接调用。

静态方法能被重写吗? → 不能!子类定义同名静态方法是,不是多态。

工具类为何全用静态方法? → 无需创建对象,节省内存,调用简洁(如 ​)

​ 内部类 ​ 的对象实例,和普通顶级类的对象,在堆内存中的结构完全一样;

继承

image-20260212085658452

创建子类对象一定会创建父类对象,默认super父类的无参构造方法

继承的本质是子类对象使用父类对象的方法和变量(public)

多态

多态是指:。只能调用父类中已有的方法(重写的会走子类版本),不能直接调用子类特有方法;它依赖继承、重写和向上转型,核心价值是——新增子类无需改动原有代码,是开闭原则的体现。

开闭原则(Open-Closed Principle,OCP)是面向对象设计的核心原则之一,由 Bertrand Meyer 提出,意思是:

if (p instanceof Student) {
    ((Student) p).study(); // 安全转换
}

接口

image-20260212085709654

*

内部类

成员内部类(Member Inner Class)

:属于外部类的实例,可访问外部类所有成员(包括 private)。

:必须先有外部类对象,才能创建内部类对象。

public class Outer {
    private int x = 10;
    
    class Inner {
        void print() {
            System.out.println(x); // 可直接访问外部类私有成员
        }
    }
    
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner(); // 必须通过外部类实例创建
        inner.print();
    }
}

static final部分常量是可以的,因为:对于 static final 修饰的基本类型或字符串,Java 编译器在编译阶段就会把它们替换为具体的数值,并且其存在于常量池访问它们不需要触发类的初始化逻辑,也不需要通过类引用


:用 ​ 修饰,不依赖外部类实例,不能访问外部类非 static 成员。 :节省内存,线程安全,常用作工具类或辅助类。

public class Outer {
    private static int y = 20;
    private int x = 10;
    
    static class StaticInner {
        void print() {
            System.out.println(y); // 可访问外部类 static 成员
            // System.out.println(x); // ❌ 编译错误!不能访问非 static
        }
    }
    
    public static void main(String[] args) {
        Outer.StaticInner si = new Outer.StaticInner(); // 直接创建,无需外部实例
        si.print();
    }
}


:定义在方法内部,作用域仅限于该方法。 :只能访问方法中 的局部变量。

public class Outer {
    void method() {
        int a = 100; // effectively final
        
        class LocalInner {
            void print() {
                System.out.println(a); // ✅ 可访问
            }
        }
        
        LocalInner li = new LocalInner();
        li.print();
    }
}


:没有名字,通常用于继承类或实现接口的:继承/实现 + 实例化一步完成。

虽然你没给它起名字,但 Java 是强类型语言,所有的对象必须属于某个类。当你写 new Runnable() { ... } 时,编译器在后台干了这些事:

  • :它偷偷创建一个类,名字通常叫 外部类名$1。
  • :让这个 $1 类去 implements Runnable。
  • :你会发现文件夹里多了一个 Outer$1.class。
  • :在代码运行到那一行时,new 出来的其实是这个 $1 类的对象。

所以,

// 实现接口
Runnable r = new Runnable() { // 这里new的实际上是大括号的匿名内部类
    @Override
    public void run() {
        System.out.println("匿名内部类");
    }
};

// 继承类(如 Thread)
Thread t = new Thread() {
    @Override
    public void run() {
        System.out.println("继承 Thread 的匿名类");
    }
};

Object

Object是java中的顶级父类,所有的类都直接或间接的继承于Object类,其方法可以被所有类访问

成员方法:

方法名说明
public String toString()返回对象的字符串表示形式
public boolean equals(Object obj)比较两个对象是否相等
protected Object clone(int a)对象克隆,完全克隆所有属性值,
public static boolean isNull(Object a, Object b)判断是否为null
public static boolean nonNull(Object obj)判断对象是否为null

这里的clone是保护方法,需要自己重写该方法调用super的clone,同时java提供了一个​,继承了​并重写​方法表示该类可被克隆,这是标准写法:

public class Usr implements Cloneable{ ...
    @Override
    public Usr clone() {
        try {
            return (Usr) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }

浅拷贝对引用类型只是复制指针,还是指向同一对象。深拷贝会创建新的引用对象。

为了使clone有深拷贝,对引用类型的属性单独操作就行了。

是 Java 中 Object 类提供的一个方法,用于在垃圾回收器准备释放对象所占用的内存空间之前调用。其定义如下:

protected void finalize() throws Throwable { }

finalize 方法的调用时机

与 C++ 的析构函数不同,Java 中的 finalize 方法并不能保证会被及时执行。垃圾回收的时机具有不确定性,可能在程序运行期间都未触发垃圾回收。

反射

反射允许对成员变量(包含内部类),成员方法和构造方法的信息进行编程访问。

例如,idea的代码提示就是利用的反射,将类里能调用的成员变量/方法进行展示,还能获取方法所有的形参

image-20260212085727464

万物皆对象,获取以上四个对象分别是Class, Constructor, Field, Method

image-20260212085735486

获取class

三种方式

获取构造方法

方法名说明
返回该类所有的数组。仅包含可被当前类访问的 public 构造函数。
返回该类的数组,包括 private、protected、default 和 public 的,不考虑访问权限。
根据指定的参数类型,返回一个对象。如果找不到匹配的 public 构造方法,则抛出异常。
根据指定的参数类型,返回一个对象(包括 private)。若找不到则抛出异常。
使用此 ​ 对象创建一个新的实例。传入构造方法所需的参数。若构造方法是私有的,需先调用 ​ 才能成功创建对象。
设置是否取消 Java 访问检查。当 ​ 时,允许访问私有(private)构造方法或字段,绕过访问控制限制。

object.getModifiers();

​返回int类型的权限修饰符信息,比如2代表private

获取成员变量

将Constructor改成Field

方法名说明
为对象赋值
获取值
获取成员变量名字
获取成员变量类型
不仅对于private访问,用在static上还可以强制修改变量

获取成员方法

方法签名:方法名+参数列表,java不允许方法签名重复

方法名说明
返回该类及其所有父类中的数组,包括继承的方法。不包含 private、protected 或 default 方法。
返回该类的数组,包括 private、protected、default 和 public,但
根据方法名和参数类型,返回一个。如果找不到匹配的 public 方法,则抛出 ​。
根据方法名和参数类型,返回一个(包括 private),但不包括继承的方法。若找不到则抛出异常。
调用该 ​ 对象所代表的方法。 - 参数一:调用该方法的对象实例(如果是静态方法,传 ​) - 参数二:传递给方法的实际参数(可变参数) - 返回值:方法执行后的返回值(无返回值时为 ​)

反射的作用

    public static String informationToString(Object obj){
        ClassInformation c = GetAllClassInformation.getInformation(obj);
        return "类名:" + c.getClassname() + "\n" +
                "构造器:" + Arrays.toString(c.getDeclaredConstructors()) + "\n" +
                "属性:" + Arrays.toString(c.getDeclaredFields()) + "\n" +
                "方法:" + Arrays.toString(c.getDeclaredMethods());
    }
    public static ClassInformation getInformation(Object obj){
        Class<?> clazz = obj.getClass();
        String classname = clazz.getName();
        Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
        Field[] declaredFields = clazz.getDeclaredFields();
        Method[] declaredMethods = clazz.getDeclaredMethods();
        return new ClassInformation(classname, declaredConstructors, declaredFields, declaredMethods);
    }

代理

:访问一个“代理对象”。由代理对象在调用目标方法前后,添加额外的逻辑(如:日志记录、事务控制、权限检查、性能监控等)。

代理如何知道类的方法/功能呢:通过实现同一个接口

是指在程序运行前,代理类的 .class 文件就已经存在了。

public class UserServiceProxy implements UserService {
    private UserService target; // 引用真实对象

    public UserServiceProxy(UserService target) {
        this.target = target;
    }

    public void save() {

        target.save(); // 调用真实业务

    }

是指代理类是在程序运行期间,通过反射机制动态生成的。

如何创建代理

​类提供了为对象产生代理的核心方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

例子:编写一个通用的代理工厂,它可以为创建一个性能监控代理,记录下每个方法的执行耗时。

public class ProxyFactory {
    public static Object createPerformanceProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 使用目标对象类加载器
                target.getClass().getInterfaces(),  // 实现目标对象所实现的接口
                (proxy, method, args) -> {
                    long startTime = System.currentTimeMillis();
                    
                    // 核心操作:执行目标对象的原始方法
                    Object result = method.invoke(target, args);

                    long endTime = System.currentTimeMillis();
                    System.out.println(method.getName() + " 耗时: " + (endTime - startTime) + "ms");
                    return result;
                }
        );
    }
}

现在可以为不同业务对象创建代理:

public class Main {
    public static void main(String[] args) {
        // 为 OrderService 创建代理
        OrderService orderService = new OrderServiceImpl();
        OrderService orderProxy = (OrderService) ProxyFactory.createPerformanceProxy(orderService);
        orderProxy.createOrder("ORDER_20240521");


        // 为 UserService 创建代理
        IUserService userService = new UserServiceImpl();
        IUserService userProxy = (IUserService) ProxyFactory.createPerformanceProxy(userService);
        userProxy.addUser("Alice");
    }
}

Proxy的缺陷

为什么 JDK 代理要求目标类

问题是,假设 Cat 类实现了 Animal 接口的 eat() 方法,但 Cat 自己还有一个特有的 run() 方法,那么代理类可以调用 eat(),但无法调用 run()

CGLIB

当目标类没有实现接口,或者我们需要调用类特有的方法时, 就派上用场了

CGLIB直接在内存中构建目标类的子类。

在 Spring 框架中,若设置proxy-target-class=false,代理方式是自动切换的:

在 Spring Boot 中,这个配置默认为 true。它的字面意思是:

lambda

是 Java 8 引入的一种,用于表示(即没有名字的方法),主要用于简化(只有一个抽象方法的接口)的实现。

Runnable r = () -> System.out.println("Hello, Lambda!");
r.run();

泛型

泛型是JDK5引入的特性,可以在约束操作的数据类型,并进行检查,因此将运行期的问题提前到了编译期间,避免了强制类型转换出现的异常

且java中的类型是伪泛型,在.class字节码文件时泛型会消失(泛型的擦除),都会被当作Object类型

当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类

泛型不具备继承性,但是数据具备继承性

​ 和 ,不能互相赋值

如何理解?

Java 的泛型在编译后会被类型擦除,变成原始类型,因此 ​ 和 ​ 在运行时其实是同一个类型,JVM 无法区分。为了防止类型安全问题(比如把 ​ 错误地放进 ​),编译器不允许 ​ 继承自 ​,即;但你放进泛型里的对象(如 ​)依然是 ​ 的子类,

举例:

public class GenericsDemo5 {
    public static void main(String[] args) {
        // 创建集合对象
        ArrayList<Ye> list1 = new ArrayList<>();
        ArrayList<Fu> list2 = new ArrayList<>();
        ArrayList<Zi> list3 = new ArrayList<>();

        // 调用 method 方法
        method(list1);
        method(list2); // 报错
        method(list3); // 报错

        list1.add(new Ye());
        list1.add(new Fu());
        list1.add(new Zi());
    }

    public static void method(ArrayList<Ye> list) {
        // ...
    }
}

泛型的通配符:虽然不确定类型,但是希望只接收特定的类型,比如必须继承于某个父类

extends E:表示可以传递E或者E所有的子类类型
super E:表示可以传递E或者E所有的父类类型

public static void method(ArrayList<? extends E> list){

}

java 禁止创建泛型数组(如 ​),是因为泛型在运行时会被擦除,而数组依赖运行时类型检查,若允许泛型数组,结合数组的协变性,会导致堆污染和隐式的 ​,破坏类型安全。

// 假设这行能编译(实际上不能):
List<String>[] lists = new List<String>[2];

// 利用数组协变(List<String>[] 是 Object[] 的子类型)
Object[] objs = lists;

// 放一个 List<Integer> 进去 —— 编译器可能警告,但擦除后 JVM 无法阻止
objs[0] = new ArrayList<Integer>();

// 现在:lists[0] 实际上是一个 List<Integer>!
String s = lists[0].get(0); // ClassCastException!但代码看起来完全合法

协变

Cat 是 Animal 的子类(Cat ≤ Animal)
那么:
如果 List<Cat> 也是 List<Animal> 的子类型 → 协变
如果 List<Cat> 和 List<Animal> 没有子类型关系 → 不变
如果 List<Animal> 反而是 List<Cat> 的子类型 → 逆变(很少见)

处理异常

Java 中的所有异常类型都是Throwable类的子类。异常体系分为两大重要分支:

Exception 又进一步分为:

try catch很消耗性能,

try catch 注意点:

public class TryCatchExample {
    public static void main(String[] args) {
        try {
            // 可能会抛出异常的代码
            int result = 10 / 0; // 这里会抛出 ArithmeticException
            System.out.println("结果是: " + result);
        } catch (ArithmeticException e) {
            // 捕获并处理特定异常
            System.out.println("捕获到算术异常: " + e.getMessage());
        } finally {
            // 无论是否发生异常,都会执行
            System.out.println("finally 块总是会被执行。");
        }
    }
}

如果try进行了return,那么在返回前会执行finally,如果finally还有return, 那么进行覆盖返回

是一种用于调试和测试的机制,用来验证程序中的“假设”是否成立。如果断言失败(即条件为 ​),程序会抛出 ​,通常表示代码中存在逻辑错误。

assert condition;
// 或
assert condition : message;
public class AssertionExample {
    public static void main(String[] args) {
        int age = -5;

        // 简单断言
        assert age >= 0 : "年龄不能为负数!";

        System.out.println("年龄: " + age);
    }
}

默认情况下,,所以即使 ​,程序也不会报错。

要启用断言,必须在运行时加上 ​(enable assertions)参数:

javac AssertionExample.java
java -ea AssertionExample

输出(启用断言后):

Exception in thread "main" java.lang.AssertionError: 年龄不能为负数!
    at AssertionExample.main(AssertionExample.java:6)
特性断言(assert)异常(Exception)
检查开发阶段的逻辑错误(内部假设)处理运行时可能出现的异常情况(如用户输入错误、IO 错误等)
不应被程序捕获或恢复(属于 bug)可被捕获并处理
否(需 ​ 开启)
通常关闭必须保留

File

三种对象创建方式:

​ // 根据文件路径创建文件对象

​ // 根据父路径名字符串和子路径名字符串创建文件对象

​ // 根据父路径对应文件对象和子路径名字符串创建文件对象

方法名称说明
判断此路径名表示的File是否为文件夹
判断此路径名表示的File是否为文件
判断此路径名表示的File是否存在
返回文件的大小(字节数量)
返回文件的绝对路径
返回定义文件时使用的路径
返回文件的名称,带后缀
返回文件的最后修改时间(时间毫秒值)

方法名称说明
创建一个新的空的文件
创建单级文件夹
创建多级文件夹
删除文件、空文件夹

方法名称说明返回值类型
获取当前路径下所有文件和子目录的 ​ 对象数组
情况返回结果说明
路径不存在若文件夹路径无效或找不到,则返回
路径是普通文件(非目录)只有目录才能调用 ​ 列出内容
路径是空目录长度为 0 的数组​,不为
路径是有内容的目录包含所有文件和子目录的 ​ 数组每个元素都是一个 ​ 对象,代表子项
路径包含隐藏文件返回数组中也包含隐藏文件​, ​ 等
路径需要权限访问没有读取权限时无法列出内容
方法名称说明
列出当前系统中所有可用的文件系统根目录(如 Windows 下的 C:、D:;Linux 下的 ​)
获取当前路径下所有文件和子目录的(不包含路径)
使用 ​ 过滤器,获取符合条件的文件或目录名称数组
使用 ​ 过滤器,获取符合条件的文件或目录的 ​ 对象数组
使用 ​ 过滤器,获取符合条件的文件或目录的 ​ 对象数组

IO流

存取数据的方案,文件数据,网络数据等

💡 小贴士:

  • 字节流适用于,包括非文本文件;
  • 字符流仅适用于,效率更高且避免乱码,用记事本能打开

字节流

创建字节流输出对象

参数一:路径或File对象

参数二:续写开关,true表示追加

如何换行写:

​ win: \r\n ,编译器也会自动补全\n为\r\n

​ linux: \n

​ mac: \r

如果文件不存在会创建一个新的文件,但是要保证父级路径是存在的

String url = "src/main/java/mylearn/Multithreading/IoStream/test.txt";
try{
    FileOutputStream fos = new FileOutputStream(url);
    fos.write(97); // 这里写的是ASCII码a
    fos.close();
}catch(Exception e){
    e.printStackTrace();
}

创建字节流输入对象

参数:路径或File对象(必须指向一个已存在的文件,否则会抛出 FileNotFoundException)

如何读取数据:

String url = "src/main/java/mylearn/Multithreading/IoStream/test.txt";
try {
    FileInputStream fis = new FileInputStream(url);
    int data;
    while ((data = fis.read()) != -1) {
        System.out.print((char) data); // 将读取的字节转为字符输出
    }
    fis.close();
} catch (Exception e) {
    e.printStackTrace();
}

String url = "src/main/java/mylearn/Multithreading/IoStream/test.txt";
String url2 = "src/main/java/mylearn/Multithreading/IoStream/1.txt";

try (FileInputStream fis = new FileInputStream(url);
     FileOutputStream fos = new FileOutputStream(url2)) {

    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = fis.read(buffer)) != -1) { // -1表示read结束了,而read会返回本次读取的长度
        fos.write(buffer, 0, bytesRead); // 只写入实际读取的字节数
    }
} catch (IOException e) {
    e.printStackTrace();
}

字节缓冲流

将基本流包装成高级流,提高数据读写性能。以下两种方法底层自带了8192字节的缓冲区

方法名称说明
把基本流包装成高级流,提高读取数据的性能
数据可能暂存于缓冲区中,需调用 ​ 确保数据写出,或在关闭流时自动 flush。

传入的基本流对象会在缓冲流关闭时自动关闭

释放资源的方式

读取写入文件要求处理错误,jdk提供了方便的处理方式会自动释放资源

在 Java 中,,适用于所有实现了 ​ 接口的 I/O 流对象(如 ​、​ 等)。

try (FileInputStream fis = new FileInputStream("input.txt");
     FileOutputStream fos = new FileOutputStream("output.txt")) {
    // 读写操作
    int data;
    while ((data = fis.read()) != -1) {
        fos.write(data);
    }
} catch (IOException e) {
    e.printStackTrace();
}
// 流会自动关闭,无需手动调用 close()

💡 :只要使用 JDK 7 或更高版本, ​ 来管理 I/O 资源。

字符集

ASCII

ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)是一套基于拉丁字母的字符编码,共收录了 128 个字符,用一个字节就可以存储,它等同于国际标准 ISO/IEC 646。

ASCII 编码中第 0~31 个字符(开头的 32 个字符)以及第 127 个字符(最后一个字符)都是不可见的(无法显示),但是它们都具有一些特殊功能,所以称为控制字符( Control Character)或者功能码(Function Code)。无法表示中文字符

GBK

GBK(uo iao uozhan,国家标准扩展)是一种中文字符编码标准,主要用于简体中文的字符表示。它是对GB2312-80标准的扩展,包含了更多的汉字和符号,广泛应用于Windows系统、网页、数据库等中文信息处理环境中,使用两个字节(高位和低位)编码,高位最高位为1表示是双字节字符(比如中文)

例:1xxxxxxx xxxxxxxx表示一个双字节字符,若单字节就是00000000 xxxxxxxx

Unicode

统一码(Unicode),又称万国码、国际码,是制定的国际标准,涵盖字符集及、、等编码方案。该标准通过为全球各语言字符分配唯一编码,解决传统编码的兼容性问题,支持跨语言、跨平台文本处理,其中UTF-8兼容并采用可变字节设计。

UTF-8 -16这种不是字符集,是unicode字符集的一种编码方式,UTF-8下中文占3字节

image-20260212085818240

编码和解码

为何会有乱码?

如何避免编码?

字符流

字符流以 (char)为单位进行读写,底层自动处理编码转换,适合处理 。 核心类:​(输入)、​(输出)

创建字符输出流对象:​ 或

✅ 文件不存在时会自动创建(但父目录必须存在) ✅ 自动使用平台默认字符编码(如 UTF-8、GBK),(若需指定编码,应使用 ​)

String url = "src/main/java/mylearn/Multithreading/IoStream/test.txt";
try {
    FileWriter fw = new FileWriter(url);
    fw.write('a');           // 写一个字符
    fw.write("你好\n");      // 写字符串并换行
    fw.close();
} catch (Exception e) {
    e.printStackTrace();
}

​ 是 ​ 的子类,默认使用系统编码。

image-20260212085835214

因此字符流写入会存在刷新问题


创建字符输入流对象:

String url = "src/main/java/mylearn/Multithreading/IoStream/test.txt";
try {
    FileReader fr = new FileReader(url);
    int ch;
    while ((ch = fr.read()) != -1) {
        System.out.print((char) ch); // 转为 char 输出
    }
    fr.close();
} catch (Exception e) {
    e.printStackTrace();
}

String src = "src/main/java/mylearn/Multithreading/IoStream/test.txt";
String dest = "src/main/java/mylearn/Multithreading/IoStream/1.txt";

try (FileReader fr = new FileReader(src);
     FileWriter fw = new FileWriter(dest)) {

    char[] buffer = new char[1024];
    int charsRead;
    while ((charsRead = fr.read(buffer)) != -1) {
        fw.write(buffer, 0, charsRead);
    }
} catch (IOException e) {
    e.printStackTrace();
}

⚠️注意:​ / ,且。 若需指定编码(如 UTF-8),应使用:

 InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");

image-20260212085851023

字节流没有缓冲区

读取:

缓冲区刷新的方式是覆盖而不是重置0,

Java 的字符流(如 ​)在读取文本时,如果遇到多字节字符(比如 UTF-8 中文)被缓冲区分割的情况——例如缓冲区末尾只有前两个字节,第三个字节还在文件中——它会自动把不完整的字节暂存起来,等下一次读取新数据时,先拼接再解码,从而确保始终返回完整的正确字符,。这个过程对用户完全透明,无需手动处理。

字符缓冲流

方法名称说明特有方法说明
把基本流包装成高级流,提高读取数据的性能readLine()读取一行数据,没有返回null
数据可能暂存于缓冲区中,需调用 ​ 确保数据写出,或在关闭流时自动 flush。newLine()跨平台的换行

缓冲区是16K ! 因为底层会创建一个8192大小的字符数组char

Randomse随机读取

创建随机访问对象 new RandomAccessFile(路径 或 File对象, 访问模式)

参数:

如何读取/写入数据:

String url = "src/main/java/mylearn/Multithreading/IoStream/test.txt";
// 假设 text.txt 里面的内容是 "HelloWorld"
try {
    // 使用只读模式 "r"
    RandomAccessFile raf = new RandomAccessFile(url, "r");
    
    // 把指针直接移动到第5个字节(跳过前面的"Hello")
    raf.seek(5); 
    
    int data;
    while ((data = raf.read()) != -1) {
        System.out.print((char) data); // 会输出 "World"
    }
    raf.close();
} catch (Exception e) {
    e.printStackTrace();
}
核心方法读写方式与功能描述
将文件指针(光标)绝对定位到文件中的指定位置 ​(从 0 开始计数)。
获取位置:返回当前文件指针(光标)所在的字节偏移量。
​ / 读取数据:与普通字节流一致,一次读取一个字节或填充字节数组;读取完成后,指针自动向后移动。
写入数据:写入字节数组。配合 ​ 方法使用,可以覆盖/替换文件中特定位置的内容(而非总是追加)。
String url = "src/main/java/mylearn/Multithreading/IoStream/raf_demo.txt";

// 使用读写模式 "rw" (推荐配合 try-with-resources 自动关流)
try (RandomAccessFile raf = new RandomAccessFile(url, "rw")) {
    
    // 1. 先写入一段基础数据
    raf.write("Hello Java".getBytes()); 
    // 此时光标在末尾(位置10)

    // 2. 需求:我们想单独读取前面的 "Hello"
    raf.seek(0); // 把光标挪回文件开头
    byte[] buffer = new byte[5];
    raf.read(buffer); // 读取5个字节
    System.out.println("读取的内容: " + new String(buffer)); // 输出: Hello

    // 3. 需求:把 "Java" 改成 "Code"
    raf.seek(6); // 把光标挪到 'J' 的位置 (索引为6)
    raf.write("Code".getBytes()); // 写入后,文件内容变成了 "Hello Code"

    // 4. 查看当前光标位置
    System.out.println("当前光标位置: " + raf.getFilePointer()); // 输出: 10

} catch (IOException e) {
    e.printStackTrace();
}

实际应用中通常使用,它打破传统字节流的性能瓶颈,直接对接操作系统底层机制,专攻海量数据与高并发场景。

  • 传统流是一个字节或一个数组地搬运,而 FileChannel 必须配合 ByteBuffer(缓冲区)使用。它直接在内存中分配一块连续空间批量读写,减少了程序与磁盘交互的次数。
  • 传统文件上传:磁盘 -> 操作系统内核 -> JVM内存 -> 操作系统网络层 -> 网卡(来回拷贝4次)。 FileChannel (transferTo / transferFrom 方法):可以直接让操作系统把数据从磁盘丢给网卡,,极大节省 CPU 性能和内存开销。
  • 支持将一个几十GB的大文件,直接“映射”到操作系统的物理内存中。,操作系统会自动在后台帮你同步到硬盘,是处理超大文件的“终极杀器”。

转换流

转换流(​ 和 ​)的作用是在​ 将字节输入流按指定字符编码解码为字符,供字符流读取;​ 将字符按指定编码编码为字节,写入字节输出流。它们使得程序能以字符(文本)方式安全、正确地处理文本数据,同时兼容底层只接受字节的 I/O 设备,并通过显式指定编码(如 UTF-8)避免乱码问题。

流类型类名
// 参数1: 字节输入流(如 FileInputStream)
// 参数2: 字符编码(推荐显式指定,避免乱码)
InputStreamReader isr = new InputStreamReader(
    new FileInputStream("input.txt"), 
    "UTF-8"
);
// 参数1: 字节输出流(如 FileOutputStream)
// 参数2: 字符编码(必须与读取时一致)
OutputStreamWriter osw = new OutputStreamWriter(
    new FileOutputStream("output.txt"), 
    "UTF-8"
);

序列化流

序列化流(如 ​ 和 ​)的作用是将 Java 对象转换为字节序列并保存到本地文件(序列化),之后可以从文件中读取字节并重新在内存中还原出原对象(反序列化)序列化的主要目的是

实现:Serializable 接口: 要使一个类可序列化,需要让该类实现 java.io.Serializable 接口,这告诉 Java 编译器这个类可以被序列化,例如:

import java.io.Serializable;

public class MyClass implements Serializable {
// 类的成员和方法
}

使用 ObjectOutputStream 类来将对象序列化为字节流,以下是一个简单的实例:

MyClass obj = new MyClass();
try {
    // 创建文件输出流,指向名为 "object.ser" 的文件,用于将字节写入磁盘
    FileOutputStream fileOut = new FileOutputStream("object.ser");
    // 将 FileOutputStream 包装为 ObjectOutputStream,使其具备写入 Java 对象的能力
    ObjectOutputStream out = new ObjectOutputStream(fileOut);
    // 将 obj 对象序列化(转换为字节流)并写入到 "object.ser" 文件中
    out.writeObject(obj);
    out.close();
    fileOut.close();
} catch (IOException e) {
    e.printStackTrace();
}

使用 ObjectInputStream 类来从字节流中反序列化对象,以下是一个简单的实例:

MyClass obj = null;
try {
    // 创建文件输入流,从名为 "object.ser" 的文件中读取字节数据
    FileInputStream fileIn = new FileInputStream("object.ser");
    // 将 FileInputStream 包装为 ObjectInputStream,使其具备从字节流中还原 Java 对象的能力
    ObjectInputStream in = new ObjectInputStream(fileIn);
    // 从输入流中读取对象(反序列化),并强制转换为 MyClass 类型,赋值给 obj
    // 注意:readObject() 返回的是 Object 类型,必须显式向下转型
    obj = (MyClass) in.readObject();
    in.close();
    fileIn.close();
} catch (IOException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

比如新增了属性,若没有指定版本号,那么会报错读取的序列化对象版本号和指定类型版本号不匹配,指定版本号就不会在新增内容时重新计算版本号,可以在Idea设置自动生成

image-20260212085910040

若想让某个成员变量的值不参与序列化过程,那么可以加​关键字修饰,标记的成员变量不参与序列化过程

读写多个对象

比如向文件追加写入多个对象,那么读取时如果循环读取,文件读空后会报 EOFException异常,但我们不能用异常来判断是否读空。所以一般规定,这样直接读一次集合就行了。

一定先写再读

打印流

只能写(输出),不能读

打印流是Java IO体系中用于,它提供了一系列重载的​和​方法,可以方便地输出各种数据类型。Java提供了两种打印流:

PrintStream
构造方法说明
关联字节输出流/文件/文件路径
指定字符编码
自动刷新
指定字符编码且自动刷新

字节流底层没有缓冲区,因此自动刷新开不开都一样

成员方法说明
常规方法:规则跟之前一样,将指定的字节写出
特有方法:打印任意数据,自动刷新,自动换行
特有方法:打印任意数据,不换行
特有方法:带有占位符的打印语句,不换行
PrintWriter
构造方法签名参数说明功能描述
​: 字符输出流(如 ​、​)不自动刷新、使用默认字符编码
​: 字符输出流 ​: 是否自动刷新(​/​)​,调用 ​ 等方法会自动刷新缓冲区。
​: 字符输出流 ​: 是否自动刷新 ​: 指定字符编码(如 ​)
​: 目标文件对象 ​: 指定字符编码创建一个写入文件的 ​,使用指定字符编码(Java 11+ 支持)。
​: 文件路径字符串 ​: 指定字符编码通过文件名和指定编码(Java 11+ 支持)。
成员方法说明
常规方法:规则跟之前一样,将指定的字符写出
特有方法:打印任意数据,自动刷新,自动换行
特有方法:打印任意数据,不换行
特有方法:带有占位符的打印语句,不换行

解压缩流

java默认只识别zip格式

位于InputStream下的拓展类,用于解压缩

创建解压缩流并遍历ZipEntry对象:

ZipInputStream zip = new ZipInputStream(new FileInputStream(src));
    private static boolean unzip(File zipFile, String destDir) throws IOException {
        // 判断destDir是否存在
        File destFile = new File(destDir);
        if (!destFile.exists()) {
            destFile.mkdirs();
        }
        // 创建解压缩流
        ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
        ZipEntry entry;
        while((entry = zis.getNextEntry()) != null){
            System.out.println("扫描到:" + entry.getName());
            if(entry.isDirectory()){
                System.out.println("创建目录:" + destDir + entry.getName());
                File file = new File(destDir + entry.getName());
                file.mkdirs();
            }else{
                // 创建输出流写出
                FileOutputStream fos = new FileOutputStream(new File(destDir, entry.toString()));
                byte[] b = new byte[8192]; // 增大缓冲区提高效率
                int bytesRead;
                while((bytesRead = zis.read(b)) != -1){
                    fos.write(b, 0, bytesRead); // 只写入实际读取的字节数
                }
                System.out.println("解压缩文件:" + destDir + entry.getName() + " 成功");
                fos.close();
                // 表处理完一个文件条目后,需要调用closeEntry()方法来结束当前条目的读取操作,释放相关资源,然后才能继续读取下一个条目
                zis.closeEntry();
            }
        }
        zis.close();
        return true;
    }

解压缩流的方法命名都很形象,见名知意,另外,可能会解压缩出如desktop.ini(文件夹个性化信息), thumbs.db(缩略图信息),因为这些是win自带的压缩工具悄悄创建的,用户不可见但是java解压缩流会看到并处理,可以跳过

压缩流

位于OutputStream下的拓展类,用于压缩, 压缩本质就是把每一个文件/文件夹看成ZipEntry对象放到压缩包中

import java.io.*;
import java.nio.file.Files;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 将指定源文件或目录压缩为 ZIP 文件
 *
 * @param srcFile  要压缩的源文件或目录
 * @param zipFile  目标 ZIP 文件
 * @return 是否成功
 * @throws IOException
 */
private static boolean zip(File srcFile, File zipFile) throws IOException {
    // 确保目标 ZIP 文件的父目录存在
    File parentDir = zipFile.getParentFile();
    if (parentDir != null && !parentDir.exists()) {
        parentDir.mkdirs();
    }

    try (FileOutputStream fos = new FileOutputStream(zipFile);
         ZipOutputStream zos = new ZipOutputStream(fos)) {

        if (srcFile.isDirectory()) {
            // 压缩整个目录
            addFolderToZip("", srcFile, zos);
        } else {
            // 压缩单个文件
            addFileToZip("", srcFile, zos);
        }
        System.out.println("压缩完成: " + zipFile.getAbsolutePath());
        return true;
    }
}

/**
 * 将单个文件添加到 ZIP 流中
 */
private static void addFileToZip(String basePath, File file, ZipOutputStream zos) throws IOException {
    String entryName = basePath + file.getName();
    System.out.println("添加文件: " + entryName);
    
    // 创建 ZIP 条目
    ZipEntry entry = new ZipEntry(entryName);
    zos.putNextEntry(entry);

    try (FileInputStream fis = new FileInputStream(file)) {
        byte[] buffer = new byte[8192];
        int bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            zos.write(buffer, 0, bytesRead);
        }
    }
    zos.closeEntry(); // 结束当前条目
}

/**
 * 递归将整个文件夹添加到 ZIP 流中
 */
private static void addFolderToZip(String basePath, File folder, ZipOutputStream zos) throws IOException {
    File[] files = folder.listFiles();
    if (files == null) return;

    for (File file : files) {
        String entryPath = basePath + folder.getName() + "/";
        if (file.isDirectory()) {
            // 递归处理子目录
            addFolderToZip(entryPath, file, zos);
        } else {
            // 添加文件
            addFileToZip(entryPath, file, zos);
        }
    }
}

工具类

这里着重使用国产新工具类工具包

一个Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类,同时提供以下组件:

模块介绍
hutool-aopJDK动态代理封装,提供非IOC下的切面支持
hutool-bloomFilter布隆过滤,提供一些Hash算法的布隆过滤
hutool-cache简单缓存实现
hutool-core核心,包括Bean操作、日期、各种Util等
hutool-cron定时任务模块,提供类Crontab表达式的定时任务
hutool-crypto加密解密模块,提供对称、非对称和摘要算法封装
hutool-dbJDBC封装后的数据操作,基于ActiveRecord思想
hutool-dfa基于DFA模型的多关键字查找
hutool-extra扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等)
hutool-http基于HttpUrlConnection的Http客户端封装
hutool-log自动识别日志实现的日志门面
hutool-script脚本执行封装,例如Javascript
hutool-setting功能更强大的Setting配置文件和Properties封装
hutool-system系统参数调用封装(JVM信息等)
hutool-jsonJSON实现
hutool-captcha图片验证码实现
hutool-poi针对POI中Excel和Word的封装
hutool-socket基于Java的NIO和AIO的Socket封装
hutool-jwtJSON Web Token (JWT)封装实现
hutool-aiAI大模型封装

多线程

是操作系统进行资源分配和调度的基本单位,是一个正在运行的程序的实例。 是 CPU 调度和执行的最小单位,被包含在进程中,是进程中的实际执行单元。 同一进程内的多个线程,但每个线程拥有

通俗说法操作系统术语Java含义说明典型触发方式是否可恢复
新建New线程对象已创建,但尚未启动✅(调用 ​)
就绪Ready线程已启动,等待 CPU 调度(具备运行条件)调用 ​ 后,未获得 CPU 时间片—(属于运行态)
运行Running线程正在 CPU 上执行代码获得 CPU 时间片后自动进入
阻塞Blocked线程等待获取 (该锁被其他线程持有)进入 ​ 块但锁不可用✅(锁释放后)
等待Waiting线程,直到被其他线程显式唤醒​, ​, ✅(需被唤醒)
睡眠 /计时等待Timed Waiting线程,超时后自动恢复​, ​, ✅(超时或中断)
死亡Terminated / Dead线程执行完毕(正常结束或抛出未捕获异常)​ 方法执行完成 或 抛出异常❌(不可重启)

操作系统分配任务的时间是毫秒级,用户交互高的线程有高优先级

为什么 Java 要把 Ready 和 Running 合并为 RUNNABLE? 因为现代操作系统(时间片轮转)切换线程的速度太快了(毫秒甚至微秒级),在 JVM 层面去细分这两个状态没有实际指导意义,干脆统称为“可以运行的状态”。

实现方式

1.将类声明为Thread的子类,重写run方法,接下来可以分配并启动该子类的实例

// 1. 定义线程类
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello from thread!");
    }
}

// 2. 启动线程(通常放在 main 方法中)
public class Main {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.setName("线程1");
        MyThread t2 = new MyThread();
        t2.setName("线程2");
        t1.start(); // 进程已经就绪,等待操作系统调用
        t2.start();
    }
}
线程1Hello from thread!
线程2Hello from thread!

2.实现Runnable接口

// 区别在于真正运行的是线程对象,需要创建线程对象
MyRun mr = new MyRun(); // 实现了接口并重写了run方法的对象
// 创建线程对象
Thread t1 = new Thread(mr);
t1.start(); // 开启线程

以上创建线程的前两种传统方式(​ 和 ​)默认不支持直接返回结果。

3.

import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum; // 返回计算结果
    }
}
import java.util.concurrent.FutureTask;
public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        // 1. 创建一个实现了 Callable 接口的对象(任务)
        MyCallable mc = new MyCallable();
        // 2. 创建 FutureTask 对象(用于管理线程执行结果)
        FutureTask<Integer> ft = new FutureTask<>(mc);
        // 3. 创建 Thread 对象,并将 FutureTask 传入
        Thread t1 = new Thread(ft);
        // 4. 启动线程
        t1.start();
        // 5. 获取线程执行的结果(阻塞等待)
        Integer result = ft.get(); // 等待任务完成并返回结果
        System.out.println("1~100 的和是:" + result); // 输出:5050
    }
}

Thread常见成员方法

方法名称说明
返回当前线程的名称。
设置线程的名字。也可以在构造方法中设置(如 ​)。
获取当前正在执行的线程对象(静态方法)。常用于获取调用线程信息。也就是线程本身
让当前线程休眠指定时间(单位:),释放CPU资源。抛出 ​。
设置线程的优先级(1~10,默认为5)。数值越大优先级越高,但不保证调度顺序。
获取线程的优先级。
将线程设置为守护线程(​)或用户线程(​)。守护线程随程序主进程结束而自动终止。
提示调度器让出CPU给其他线程(非阻塞),但不保证会切换(因为下次调度还会参与竞争)
等待当前线程执行完毕后再继续执行后续代码(插入/插队作用)。可重载为 ​。

:当 JVM 中所有的 非守护线程\都结束时,JVM 会立即退出,此时所有守护线程会被强制终止,无论它们是否执行完毕。比如t1线程从1打印到10,守护线程t2从1打印到100,那么t1结束后,t2可能只打印到30就结束了

例子:比如开一个窗口传输文件,那么就可以把传输文件设置成守护线程,窗口关闭了自动不传输了

public static void main(String[] args) {
    int flag = 0;
    Thread t1 = new Thread(() -> {
        flag = 1; // 编译错误,flag是main线程内变量,其他线程可见但是不可修改(final)
    });

    Thread t2 = new Thread(() -> {
        System.out.println(flag); 
    });

    t1.start();
    t2.start(); // 进入就绪态
    t1.join();
}
join() 的作用是:让当前线程(通常是主线程)等待指定的线程执行完毕后再继续执行。
也就是main这个线程会在t1.join()这里阻塞等待,但是t2其执行是不受影响的

比如一个进程中的变量i被两个线程同时修改,可能会被覆盖而丢失数据

变量存储在主内存中,CPU 通过高速缓存(Cache)加速访问;当多个线程并发修改同一变量时,各自可能将变量加载到自己核心的缓存中进行计算。由于缓存与主存之间存在,若没有同步机制(如 ​ 或 ​),一个线程对变量的修改可能尚未写回主存,另一个线程就从主存或自己的缓存中读取了旧值,导致。当它们先后将结果写回时,就会发生——例如两个线程同时执行 ​,都读到 ​,各自加 1 后写回 ​,最终结果丢失了一次更新。这种问题并非因为缓存“销毁”数据,而是,使得多线程在无协调的情况下操作共享状态,造成竞态条件(Race Condition)。因此,必须通过 Java 的内存模型同步机制,强制刷新缓存、保证操作的原子性和可见性,才能避免数据被错误覆盖。

同步代码块

同步代码块是多线程编程中用于解决数据安全问题的重要机制。它通过​关键字限制多个线程对共享资源的同时访问,确保在同一时刻只有一个线程可以执行同步代码块,从而避免数据不一致或损坏的情况。

synchronized(锁){
	操作共享数据的代码
}

其实还有public synchronized void method(),非静态锁this, 静态锁当前类的.class,一般不推荐使用因为粒度不可调节(锁对象不能自己指定),会锁住方法里的所有代码

对象锁和类锁

我们需要通过一个标识来表示这个锁有没有被人持有,被哪个持有了

在java中是借助于对象头表示的

[ 栈(Stack) ]
  └─ 局部变量 a ────┐
                    │ (引用/指针)
                    ▼
[ 堆(Heap) ]
  └─ Student 对象实例
        ├─ **对象头(Object Header)**
        │     ├─ **Mark Word** ← 存储锁状态(无锁、偏向锁、轻量级锁、重量级锁)、GC 分代年龄、hashCode 等
        │     └─ **Klass Pointer** → 指向方法区中的 Student.class
        │
        └─ 实例数据(Instance Data)
              ├─ name: String
              └─ age: int

[ 方法区(Method Area / Metaspace) ]
  └─ Student.class
        ├─ 方法字节码(如 getName(), setName())
        ├─ 字段描述(name: String, age: int)
        ├─ 静态变量(static fields) ← 若有 synchronized(static method),锁的是该类的 Class 对象
        └─ 运行时常量池
        
---------------------------
[ 堆中的 Student 对象 ]
┌───────────────────────┐
│  Object Header        │
│  ├─ Mark Word         │ ← 存储锁状态、GC 信息、hashCode 等
│  └─ Klass Pointer     │ ← 指向方法区中的 Student.class 元数据的指针
├───────────────────────┤
│  Instance Data        │
│  ├─ name: "Alice"     │
│  └─ age: 20           │
└───────────────────────┘
如何加锁

上面讲了同步代码块​里需要加个锁,然后锁又分为对象锁和类锁以及锁在哪,以下是如何加锁

死锁

死锁的必要条件:

等待唤醒机制*

多线程协作,下面以生产者消费者为列

方法名称说明注意事项
当前线程释放锁,并进入等待状态,直到被其他线程调用 ​ 或 ​ 唤醒。必须在 ​ 块或方法中调用,否则抛出 ​。
随机唤醒一个正在等待的线程(处于 ​ 状态)。只能唤醒一个线程,可能不是期望的线程, 只有当所有等待线程时,才能安全使用
唤醒所有正在等待的线程(处于 ​ 状态)。更安全,推荐用于复杂共享资源场景。存在‘惊群’风险。

​ 的是:

在 Java 中,每个对象都有一个与之关联的

当你在一个对象上调用:

synchronized (obj) {
    obj.wait();
}

该线程会:

而在同一个对象 ​ 上调用:

synchronized (obj) {
    obj.notifyAll();
}

那么:

,并尝试重新竞争 ​ 的锁。

例:

class Buffer {
    private final int MAX_SIZE = 10;
    private Object[] items = new Object[MAX_SIZE];
    private int count = 0;
    private int in = 0;
    private int out = 0;

    public synchronized void produce(Object item) throws InterruptedException {
        while (count == MAX_SIZE) {
            wait(); // 缓冲区满,生产者等待
        }
        items[in] = item;
        in = (in + 1) % MAX_SIZE;
        count++;
        notifyAll(); // 唤醒所有等待的消费者
    }

    public synchronized Object consume() throws InterruptedException {
        while (count == 0) {
            wait(); // 缓冲区空,消费者等待
        }
        Object item = items[out];
        out = (out + 1) % MAX_SIZE;
        count--;
        notifyAll(); // 唤醒所有等待的生产者
        return item;
    }
}
阻塞队列
对比项ArrayBlockingQueueLinkedBlockingQueue
有界数组(循环队列)链表(节点)
必须指定,默认无界(最大 ​),也可设为有界
是(构造时可选)
单锁(入队/出队共用)双锁(入队、出队分离)
一般通常更高(尤其高并发)
小(固定数组)较大(每个元素额外 Node 对象)
固定缓冲区、内存敏感高吞吐、不确定数据量

两者均线程安全、不支持 ​ 元素,且实现 ​ 接口。

方法功能阻塞行为
入队,队满时等待是(永久阻塞)
出队,队空时等待是(永久阻塞)
尝试入队,失败返回
尝试出队,失败返回
带超时入队是(最多等 timeout)
带超时出队是(最多等 timeout)
入队,失败抛异常
查看队首(不移除)

线程池

提交任务时,线程池会创建新的线程对象,任务完毕后线程回归线程池不会死亡,线程池满后新提交的任务会阻塞等待。

方法名称说明
创建一个没有上限的线程池
创建有上限的线程池

常用方法

方法签名说明
提交一个无返回值的任务(​)到线程池执行。
提交一个有返回值的任务(​),返回一个 ​ 对象用于获取结果或检查状态。
提交一个 ​ 任务,并指定一个默认返回值,通过 ​ 获取该结果。
立即尝试停止所有正在执行的任务,暂停等待任务,并返回等待执行的任务列表。
平滑关闭线程池:不再接受新任务,但会继续执行已提交的任务。
判断线程池是否已调用 ​ 方法(即是否正在关闭或已关闭)。
判断线程池是否已完全终止(所有任务都已完成)。
阻塞当前线程,直到线程池终止或超时,常配合 ​ 使用。

💡

  • ​​ 只能用于 ​,无返回值;
  • ​​ 支持 ​ 和 ​,可获取执行结果或异常;
  • 关闭线程池时,推荐先调用 ​,再用 ​ 等待结束,以实现优雅关闭。

自定义线程池

这里引入核心线程,临时线程,阻塞队伍长度三个概念:

举个例子,核心线程池最大三个,阻塞队伍长三,临时线程两个,那么如果来了八个线程,前三个占用核心线程,后三个进入阻塞队伍等待核心线程,最后两个才开启临时线程处理。

​​.

 ThreadPoolExecutor pool = new ThreadPoolExecutor(
            3,                    // 参数一:核心线程数量(corePoolSize)—— 不能小于0
            6,                    // 参数二:最大线程数量(maximumPoolSize)—— 必须 >= 核心线程数
            60L,                  // 参数三:空闲线程最大存活时间(keepAliveTime)—— 单位由 timeUnit 决定
            TimeUnit.SECONDS,     // 参数四:时间单位(timeUnit)—— 使用 TimeUnit 指定
            new LinkedBlockingQueue<>(10), // 参数五:任务队列(workQueue)—— 不能为 null,此处使用有界队列
            Executors.defaultThreadFactory(), // 参数六:创建线程工厂(threadFactory)—— 不能为 null
            new ThreadPoolExecutor.CallerRunsPolicy() // 参数七:任务拒绝策略(handler)—— 不能为 null
        );

        // 提交多个任务测试线程池行为
        for (int i = 0; i < 20; i++) {
            final int taskId = i;
            pool.submit(() -> {
                System.out.println("任务 " + taskId + " 正在执行,线程:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 关闭线程池
        pool.shutdown();
        try {
            if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
                pool.shutdownNow(); // 强制关闭
            }
        } catch (InterruptedException e) {
            pool.shutdownNow();
            Thread.currentThread().interrupt();
        }
最大并行数

事宜的线程池大小需要计算得出。

首先,操作系统可能并不会将所有的逻辑处理器分给java虚拟机,可通过以下代码查看:

int count = Runtime.getRuntime().availableProcessors();
System.out.println(count);

cpu计算时间,等待时间需要工具类测试

网络编程

区别:B/S架构无需下载,但是资源不能过大,依赖网络传输资源

C/S架构需要下载本地资源,网络仅传输必要内容,可以将质量做的很好,但开发安装部署更新麻烦

学习计算机网络时我们一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚。

InetAddress

此类表示互联网协议(IP)地址对象

获取类:

InetAddress address = InetAddress.getByName(机器名称/IP地址)

获取IP地址的主机名

address.getHostName();

返回文本显示中的IP地址字符串

address.getHostAddress(); // 比如192.168.1.100 

端口号

应用程序在设备中的唯一标识

由两个字节表示的整数,取值范围0 ~ 65535

其中0~1023之间的端口用于一些知名的网络服务或应用

1024以上可以自己使用,一个端口号只能被一个应用程序使用

协议

计算机网络中,连接和通信的规则被称为网络通信协议

image-20260124143144698

UDP通信程序

DatagramSocket ds = new DatagramSocket();

// 会绑定一个端口,空参即随机选一个可用的,也可以指定端口如7788

示例

// 创建发送端,端口为7788
DatagramSocket ds = new DatagramSocket(7788);
// 打包数据
String data = "hello UDP";
byte[] bys = data.getBytes();
// 目标主机是本地localhost
InetAddress address = InetAddress.getLocalHost();
// 目标可用端口
int port = 9999;
DatagramPacket dp = new DatagramPacket(bys, bys.length, address, port);
ds.send(dp);
// 释放资源
ds.close();

绑定的端口和上文目标主机端口号一致

// 创建接收端
DatagramSocket ds = new DatagramSocket(9999);
// 创建接收端数据包
byte[] bys = new byte[1024];
DatagramPacket dp = new DatagramPacket(bys, bys.length);
// 接收
ds.receive(dp);
// 解析数据包
byte[] data = dp.getData();
int len = dp.getLength();
InetAddress address = dp.getAddress();
int port = dp.getPort();
System.out.println("接收到数据:" + new String(data,0,len));
System.out.println("来自:" + address.getHostAddress() + ":" + port);
// 释放资源
ds.close();

UDP三种通信方式

在 TCP/IP 网络中,。 比如:

所以,,告诉网络:“请按某种特殊方式处理这个包”。

(Unicast):一对一,上文的就是单播

(Multicast):创建一个组,只有加入该组的主机接收,发送端不用显式加入组

IP 地址分为 A、B、C、D、E 类:

  • A/B/C:用于单播
  • E 类:实验用
  • 这是 IANA(互联网号码分配机构)规定的。。

发送MulticastSocket:指定组播地址,如224.0.0.1这一组然后加入

// 创建发送组
MulticastSocket mu = new MulticastSocket(7788);
// 目标主机是本地组
InetAddress address = InetAddress.getByName("224.0.0.1");
// 加入组
mu.joinGroup(address);

其他不变

接收同理监听的地址就是224.0.0.1这一组,即可加入组

(Broadcast):发送的数据会被接收。它。这是为了防止“广播风暴”——如果全世界都能收到你的广播,网络就瘫痪了。

为啥用255.255.255.255就能广播?

  • 在二进制中,​,全 1。
  • 当网络设备(如交换机、网卡)看到目标 IP 是 ​,就会,而是。
  • 同时,交换机会把这个帧 到所有端口(除了接收端口),确保局域网内每台机器都收到。

TCP通信程序

发送端

// 创建Socket对象并连接服务端
Socket socket = new Socket("127.0.0.1", 7878);

// 写出数据
String str = "hello,tcp";
socket.getOutputStream().write(str.getBytes());
// 写出结束标记
socket.shutdownOutput();

// 接收回写数据
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
int b;
while((b = isr.read()) != -1){
    System.out.print((char)b);
}
isr.close();
socket.close();

接收端

// 创建接收端对象
ServerSocket ss = new ServerSocket(7878);
// 等待客户端连接
Socket socket = ss.accept();
// 获取输入流
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
int b;
while ((b = isr.read()) != -1) {
    System.out.print((char) b);
}

// 回写数据
String str = "ok TCP";
socket.getOutputStream().write(str.getBytes());
socket.close();
ss.close();

TCP发送文件线程池版

package mylearn.Multithreading.webcoding;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

public class TCPPoolRead {

    public static void main(String[] args) throws IOException {
        // 创建线程池:core=3, max=5, queue=2
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,
                5,
                60L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2),
                Executors.defaultThreadFactory(),
                new CustomRejectedHandler() // ← 使用自定义拒绝策略
        );

        ServerSocket serverSocket = new ServerSocket(7878);
        System.out.println("服务器启动,监听端口 7878...");

        try {
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("新连接来自: " + clientSocket.getRemoteSocketAddress());
                pool.execute(new ClientHandler(clientSocket));
            }
        } finally {
            serverSocket.close();
            pool.shutdown();
        }
    }

    // 每个客户端连接的处理任务
    static class ClientHandler implements Runnable {
        private final Socket socket;

        public ClientHandler(Socket socket) {
            this.socket = socket;
        }

        // 提供 getter 供拒绝策略访问 socket
        public Socket getSocket() {
            return socket;
        }

        @Override
        public void run() {
            try {
                // 假设客户端发送的是文件内容(这里简化为文本)
                InputStream input = socket.getInputStream();
                FileOutputStream fileOut = new FileOutputStream("received_file_" + System.currentTimeMillis());

                byte[] buffer = new byte[1024];
                int len;
                while ((len = input.read(buffer)) != -1) {
                    fileOut.write(buffer, 0, len);
                }
                fileOut.close();

                // 回写成功消息
                Thread.sleep(50000);
                OutputStream output = socket.getOutputStream();
                output.write("文件上传成功!\n".getBytes());
                output.flush(); // 刷新缓冲区

            } catch (IOException e) {
                System.err.println("处理客户端时出错: " + e.getMessage());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
    }

    // 自定义拒绝策略:关闭被拒绝的连接
    static class CustomRejectedHandler implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            System.out.println("线程池已满,正在处理被拒绝的连接...");
            if (r instanceof ClientHandler) {
                Socket socket = ((ClientHandler) r).getSocket();
                System.out.println("线程池已满,拒绝连接: " + socket.getRemoteSocketAddress());
                try {
                    // 发送错误信息给客户端
                    OutputStream out = socket.getOutputStream();
                    out.write("服务器繁忙,请稍后再试。\n".getBytes());
                    out.flush();
                } catch (IOException e) {
                    // 写入失败
                    System.err.println("发送错误信息给客户端时出错: " + e.getMessage());
                } finally {
                    try {
                        socket.close(); // 关闭 socket,释放内核缓冲区和连接资源
                    } catch (IOException e) {
                        // 捕获错误
                        System.err.println("关闭 socket 时出错: " + e.getMessage());
                    }
                }
            } else {
                // 如果不是 ClientHandler,按默认方式处理

            }
        }
    }
}

package mylearn.Multithreading.webcoding;

import java.io.*;
import java.net.Socket;

public class TestClient {

    public static void main(String[] args) {
        int totalClients = 20; // 创建 20 个客户端连接

        for (int i = 0; i < totalClients; i++) {
            final int clientId = i + 1;
            new Thread(() -> {
                Socket socket = null;
                try {
                    System.out.println("客户端 " + clientId + " 尝试连接...");
                    socket = new Socket("127.0.0.1", 7878);

                    // 发送少量数据(模拟文件上传)
                    OutputStream out = socket.getOutputStream();
                    out.write(("Client-" + clientId + ": Hello from client!\n").getBytes());
                    socket.shutdownOutput(); // 告诉服务端“我发完了”

                    // 读取服务端响应
                    InputStream in = socket.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                    String response = reader.readLine(); // 因为服务端只写一行
                    if (response != null) {
                        System.out.println("客户端 " + clientId + " 收到: " + response);
                    } else {
                        System.out.println("客户端 " + clientId + " 未收到响应(连接可能被关闭)");
                    }

                } catch (Exception e) {
                    System.err.println("客户端 " + clientId + " 出错: " + e.getMessage());
                } finally {
                    if (socket != null) {
                        try {
                            socket.close();
                        } catch (IOException ignored) {}
                    }
                }
            }).start();

            // 可选:稍微错开连接时间,避免瞬间冲击(也可以去掉)
            try {
                Thread.sleep(10);
            } catch (InterruptedException ignored) {}
        }

        // 主线程等待一段时间,让所有客户端完成
        try {
            Thread.sleep(10000);
        } catch (InterruptedException ignored) {}
    }
}

日志

一般不用java自带的,使用框架提供的,比如springboot的SLF4J + Logback