java算法语法速查手册

java速查手册

Java简介

Java的性质

  • 简单性
  • 可移植性
  • 面向对象
  • 解释型
  • 分布式
  • 高性能
  • 健壮性
  • 多线程
  • 安全性
  • 动态性:程序运行过程中可以对自己进行修改
  • 体系结构中立:容易的在各种机器上解释执行并翻译成本地机器代码

image-20260212085414855

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

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

idea快捷键

快捷键功能说明示例
Ctrl + Alt + VExtract Variable(提取变量) 将选中的表达式或右侧值提取为新变量,自动生成左侧声明list.get(0)String item = list.get(0);
Ctrl + D复制当前行或选中块
Ctrl + W逐步扩大选区(单词 → 行 → 块)
Alt + Enter万能灯泡(Show Intention Actions) 自动修复、生成 getter/setter、添加 try-catch 等
Alt + 6打开 TODO这个在项目检查还有哪些代办很有用
Ctrl + B跳转定义

java运行环境

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

image-20260212085429219

image-20260212085440266

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

EE:web开发

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

SDK:样例代码+使用说明

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

image-20260212085518775

使用命令行工具

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

打开记事本:

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

编码改为CRLF,windows环境编码

然后先运行javac Test.java生成class文件

然后运行java Test执行代码输出。

java中(以及其他语言)main函数可以传递来自操作系统的参数,比如代码:

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

操作系统是如何打开程序的

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

  • 安装阶段:很多安装程序(尤其是单文件 .exe 安装包)会解压内容,但不是所有软件都如此。
  • 启动阶段:大多数原生编译程序不会解压;但用高级语言打包成单文件的应用可能会在首次或每次启动时解压资源。

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

java基础

面试部分

变量和数据类型*

java变量类型及其空间占用详解

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

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

Boolean:32位*

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

byte:8位,-128 - 127

short:16位 -32768 - 32767

int:32位

Long:64位

float:32位

double:64位,java的默认浮点类型

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

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

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

类型转换

  • 低位转高位高位接收低位,不会丢失精度
  • 高位转低位会舍弃部分高位,因此会丢失精度
  • 比如double转float会丢失精度,但是int转double不会,因为double能完全容纳int
  • 和c不一样boolean不能转为int

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

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

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

int a = 10;

byte c = a;

但是! 如果满足以下条件,允许将 int 类型的编译期常量表达式赋值给 byteshortchar

  1. 表达式是 编译期常量(compile-time constant expression)
  2. 在目标类型的取值范围内
final int a = 10;

byte c  = a;

类型运算

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

数学函数

计算机很多数学库函数底层是用的泰勒展开泰勒级数思想

java.lang.Math 中的大多数方法(如 sin, cos, log, exp)是 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 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 次同一个引用(或者少量几个引用),没有创建新对象。

特性基本类型(非包装类)包装类
类型本质原生数据类型(非对象)引用类型(java.lang 中的类,是对象)
内存存储栈上(局部变量)或直接嵌入对象中堆上(对象),引用在栈上
默认值局部变量无默认值;成员变量有(如 int 默认为 0对象引用默认为 null
能否为 null❌ 不能✅ 可以为 null
能否调用方法❌ 不能✅ 可以(如 Integer.parseInt()
用于泛型❌ 不行(如 List<int> 编译错误)✅ 可以(如 List<Integer>
性能⚡ 高效(无对象开销)🐢 有对象创建/垃圾回收开销
自动转换——支持自动装箱(boxing)和拆箱(unboxing)

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

自动装箱

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

运算符和逻辑

初始:x5 = 5

执行 x5 = x5++ 后:x5 仍然是 5。

这个行为在 Java 中是确定的、可预测的,因为 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);

逻辑运算符&& 和 & 以及 || 和 |的区别是前者是短路逻辑运算符,比如A && B只要A不符合条件立即退出判断,而&会看全部。| 和 ||效果一样

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

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

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

数组

算法语法查看速查手册

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

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

为了高效完成这个检查,必须能快速获取数组长度。因此,JVM 将长度直接存放在数组对象的头部,通过 arr.length 可以 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里的类型为 包名+类名, 也就是全限定名

数组不是原生类

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

字符串*

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

  • 当你对字符串进行“修改”操作(比如拼接、替换、切片等),实际上不是在原内存区域上改动,而是创建了一个新的字符串对象。
  • 原来的字符串对象仍然存在于内存中(直到被垃圾回收),而变量现在指向新的字符串对象。

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

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

字符串常量池

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

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

只有「编译期可知的字符串字面量」(string literals)会被自动放入字符串常量池。*

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

方法

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

重载与重写

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

方法的重写:子类提供一个与父类中已有方法具有相同签名的方法实现

比较:重载改变函数签名,重写不改变函数签名

Java函数签名是方法的唯一标识符,由方法名和参数列表组成,定义了方法的接口和调用方式。

特性重写(Override)重载(Overload)
是否支持多态✅ 是!运行时根据实际对象类型决定调用哪个实现❌ 否!编译时根据引用类型和参数类型决定调用哪个方法
方法签名必须相同(方法名 + 参数列表一致)必须不同(参数列表不同)
发生时机运行时(动态绑定)编译时(静态绑定)

向上转型

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

传递

Java 中只有值传递(pass-by-value),没有引用传递。具体来说:

  • 当参数是基本类型(如 intboolean)时,传递的是该值的副本,方法内修改不会影响原变量;
  • 当参数是引用类型(如对象、数组)时,传递的是对象引用的副本(即内存地址的拷贝),方法内可通过该引用修改对象的内部状态(因此外部可见),但若在方法内让引用指向新对象,则不会影响原引用。 本质上,Java 传递的始终是“值”——基本类型的值,或引用类型的地址值,从未传递过变量本身或句柄的地址,因此不存在“引用传递”。

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

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

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

对象*

成员变量访问的就近原则: System.out.println(age);

System.out.println(this.age);

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

标准JavaBean类

用于开发

  1. 类名见名知意
  2. 成员变量使用private修饰
  3. 提供至少两个构造方法(必须重写无参构造),一般使用lombok

打印对象默认调用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(); // 静态部分不再执行
    }
}

访问和修改

访问器(Accessor)更改器(Mutator)

就是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 中,所有数组(包括 int[]String[]、自定义对象数组等)都隐式继承自 Object,并重写了 clone() 方法

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

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

关键字详解

static
  1. 修饰变量(静态变量)

    静态变量属于类本身,而非类的某个实例。所有对象共享同一份静态变量,内存中只存在一份副本。可通过 类名.变量名 直接访问,无需创建对象(推荐方式),常用于存储所有对象共享的数据,如常量、计数器等。

  2. 修饰方法(静态方法)

    静态方法属于类,不属于任何实例。不能直接访问类中的非静态成员(变量或方法),因为非静态成员依赖于对象实例。可以访问静态成员。通过 类名.方法名 调用,无需创建对象。常用于工具类方法(如 Math.random())、工厂方法等,不需要依赖对象状态即可完成操作。

  3. 修饰代码块(静态代码块)

    静态代码块在类加载时执行,且只执行一次。执行顺序优先于构造方法和非静态代码块。多个静态代码块按定义顺序依次执行。主要用于初始化静态变量或执行类级别的预处理操作。

  4. 修饰内部类(静态内部类)

    静态内部类不依赖于外部类的实例,可以独立存在。不能直接访问外部类的非静态成员(需通过外部类实例访问)。可以访问外部类的静态成员。适用于内部类与外部类实例无关的场景,避免持有外部类引用导致的内存泄漏问题。

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

final

1、final修饰的基本类型不可以第二次赋值

2、final修饰的引用类型不可以第二次改变指向(意味着可以改变属性值)

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

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

3、final修饰的类不可以被继承

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

  • 安全性:防止恶意子类篡改行为;
  • 设计意图:该类不是为扩展而设计的;
  • 性能优化:JVM 可以对 final 类的方法调用进行内联(inline)优化,因为知道不会有子类重写。

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

4、final修饰的方法不可以被重写

5、final修饰的变量支持线程可见性,可以阻止一定程度上的指令重排序*

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

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

这意味着:

  • 如果一个对象的 final 字段在构造函数中被赋值,
  • 并且该对象的引用没有在构造完成前“逸出”(即没有在构造器中把 this 赋给全局变量或启动线程),
  • 那么其他线程一旦看到这个对象,就能立即看到其 final 字段的正确值,不会看到“未初始化”或“部分初始化”的状态。

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

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

JMM 禁止编译器和 CPU 对 final 字段的写入与其构造器结束之间的指令进行重排序。这保证了 final 字段的初始化一定在对象构造完成前完成。

举个例子:

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

线程 A 创建对象:obj = new FinalExample(); 线程 B 读取:if (obj != null) { System.out.println(obj.x); }

  • 对于 x(final):B 线程一定看到 3;
  • 对于 y(非 final):B 线程可能看到 0(默认值)或 4,取决于是否发生重排序或缓存未刷新。

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

this

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

equals和HashCode*

  • == 对于 引用类型(如 String)比较的是两个对象的引用(即内存地址)是否相同。
  • 要比较内容是否相等,应该使用 .equals() 方法。

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

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

永久代和元空间

特性永久代(PermGen)元空间(Metaspace)
JDK 版本JDK 7 及之前JDK 8 起取代 PermGen
内存位置Java 堆的一部分(受 -Xmx 限制)本地内存(Native Memory,操作系统内存)
存储内容类元数据、常量池、静态变量、JIT 代码等仅类元数据(静态变量、常量池移到堆中)
是否受 GC 管理是(但回收效率低)是(更高效,与类加载器生命周期绑定)
溢出错误java.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace
默认大小较小(如 64MB~82MB)动态扩展(受限于系统可用内存)
调优参数-XX:PermSize, -XX:MaxPermSize-XX:MetaspaceSize, -XX:MaxMetaspaceSize

为什么从永久代替换为元空间?

❌ 永久代的问题:

  1. 容易 OOM: 在大量动态生成类的场景(如 Spring、Hibernate、Groovy、反射、Lambda),PermGen 很快耗尽。
  2. GC 效率低: Full GC 才能回收 PermGen,导致停顿时间长。
  3. 与堆耦合: PermGen 是堆的一部分,受 -Xmx 限制,难以独立调优。
  4. HotSpot 与 JRockit 合并需求: Oracle 收购 BEA 后希望统一 JVM 架构(JRockit 原本就没有 PermGen)。

✅ 元空间的优势:

  1. 使用本地内存: 不再占用 Java 堆空间,避免与应用对象争抢堆内存。
  2. 按需分配、自动扩展: 默认无上限(除非系统内存耗尽),减少 OOM 风险。
  3. 更高效的类元数据管理: 类卸载更及时(当类加载器不可达时,其加载的类可被回收)。
  4. 职责更清晰
    • 元空间:只存类的元数据(Class metadata)
    • :存对象实例 + 静态变量 + 字符串常量池(JDK 7+ 已移入堆)

📌 注意:从 JDK 7 开始,字符串常量池静态变量 就已经从 PermGen 移到了堆中;JDK 8 彻底移除 PermGen,用 Metaspace 替代剩余部分。

多线程*

wait和sleep的区别

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

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

并发和并行

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

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

这里看<java并发编程的艺术>

语法部分

输入

日常开发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();

String.format("%.6f", l)

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 / StringBuilder / StringBuffer

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[])。这个数组就是所谓的“缓冲区”。

  • StringBuffer 内部维护 char[] (或 byte[])。
  • BufferedInputStream 内部维护一个默认大小为 8192 字节(8KB)的 byte[] buf。

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

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

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

字符串创建

方法说明所属类
"文本"字面量(存入字符串常量池)String
new String("文本")堆中新建对象(绕过常量池)String
new String(char[]) / String.copyValueOf(char[])从字符数组构造String
new StringBuilder() / new StringBuilder(int capacity)可指定初始容量StringBuilder
new StringBuffer() / new StringBuffer(int capacity)同上,线程安全版本StringBuffer

基础信息获取

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

字符串拼接与追加

方法说明所属类
+ 运算符编译期优化为 StringBuilder(仅限常量表达式)String (底层转换)
str.concat(other)拼接返回新 StringString
append(...)追加任意类型(Object/int/char/String 等),原地修改StringBuilder / StringBuffer
insert(int offset, ...)在指定位置插入内容StringBuilder / StringBuffer

建议:循环内拼接务必用 StringBuilder.append(),避免 String + 导致 O(n²) 性能问题。

查找与匹配:

方法说明
int indexOf(String str) / indexOf(char ch)正向查找首次出现位置
int lastIndexOf(...)反向查找最后一次出现位置
boolean contains(CharSequence s)是否包含子串
boolean startsWith(prefix)是否以某前缀开头
boolean endsWith(suffix)是否以某后缀结尾
boolean matches(String regex)是否匹配正则表达式

StringBuilder/StringBuffer 不直接支持上述方法,需 .toString() 后调用

截取与拆分

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

StringBuilder/StringBuffer 需转为 String 后操作。

替换

方法说明
replace(char old, char new)替换所有指定字符
replace(CharSequence target, CharSequence replacement)替换子串(JDK 1.5+)
replaceAll(String regex, String replacement)正则全局替换
replaceFirst(String regex, String replacement)正则替换第一个匹配项
StringBuilder/buffer.replace(start, end, str)按索引区间替换(非内容匹配)

大小写转换

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

去空格与清理

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

类型转换

方法说明
char[] toCharArray()转为字符数组
byte[] getBytes()使用默认编码转字节数组
byte[] getBytes(String charset)指定编码(如 "UTF-8")转字节数组
static String valueOf(...)将基本类型(int, double 等)转为字符串

比较

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

实现真正的“中文拼音字典序”?

需要借助 Collator(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);
    }
}

StringBuilder/StringBuffer

方法说明
reverse()反转字符序列(高效)
delete(start, end)删除指定区间
deleteCharAt(index)删除单个字符
setCharAt(index, ch)修改指定位置字符

StringJoiner是 Java 8 为“带格式的字符串拼接”提供的专用、高效、安全的解决方案

    // 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库

方法说明
Math.sqrt()返回算数平方根
Math.abs()绝对值
Math.ceil()向上取整
Math.floor()向下取整
Math.round()四舍五入
Math.max()/min()获取较大值/较小值
Math.pow()返回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库

操作数组的工具类

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

大数处理

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

BigInteger

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

BigInteger 是不可变类

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

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

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

BigDecimal

方法基本同BigInteger

集合

image-20260212085606728

集合框架

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

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

  • **接口:**是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象
  • **实现(类):**是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
  • **算法:**是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序,这些算法实现了多态,那是因为相同的方法可以在相似的接口上有着不同的实现。

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

集合框架体系如图所示

image-20260212085616146

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

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

Collections集合工具类

常用API:

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

集合进阶练习

需要集合,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
方法说明
add(E e)将元素添加到列表末尾(高效)
add(int index, E element)在指定位置插入元素,原位置及之后元素后移
addAll(Collection<? extends E> c)将指定集合的所有元素追加到列表末尾
addAll(int index, Collection<? extends E> c)从指定位置开始插入集合中的所有元素
get(int index)获取指定位置的元素
set(int index, E element)替换指定位置的元素,返回被替换的旧值
remove(int index)删除指定位置的元素,返回被删除的元素
remove(Object o)删除列表中第一个匹配的元素(按 .equals()
removeAll(Collection<?> c)删除列表中所有包含在指定集合中的元素
retainAll(Collection<?> c)仅保留列表中也包含在指定集合中的元素
clear()清空列表,移除所有元素
size()返回列表中元素的个数
isEmpty()判断列表是否为空(无元素)
contains(Object o)判断列表是否包含指定元素(使用 .equals()
indexOf(Object o)返回指定元素首次出现的索引,未找到返回 -1
lastIndexOf(Object o)返回指定元素最后一次出现的索引
toArray()转换为 Object 数组
toArray(T[] a)转换为指定类型的数组(推荐传入 new T[0]
subList(int fromIndex, int toIndex)返回 [fromIndex, toIndex)视图子列表(原列表修改会反映到子列表)
forEach(Consumer<? super E> action)对每个元素执行指定操作(Java 8+)
removeIf(Predicate<? super E> filter)删除满足条件的所有元素(Java 8+)
replaceAll(UnaryOperator<E> operator)使用函数替换每个元素(Java 8+)
sort(Comparator<? super E> c)根据指定比较器对列表排序(Java 8+)
clone()返回 ArrayList 的浅拷贝(注意:非泛型安全)
  • toArray() 总是返回 Object[],不能直接转为 String[] 等具体类型(会抛 ClassCastException)。
  • toArray(new String[0]) 可以安全返回 String[]。底层使用优化的反射

💡 提示

  • ArrayList非线程安全的,适用于单线程环境。
  • 底层基于动态数组,随机访问(get/set)时间复杂度为 O(1),中间插入/删除为 O(n)
  • 所有索引均从 0 开始,toIndexsubList 等方法中表示不包含(左闭右开)。
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接口类似:

方法名称说明
public boolean add(E e)把给定的对象添加到当前集合中(已存在返回false)
public void clear()清空集合中所有的元素
public boolean remove(E e)把给定的对象从当前集合中删除
public boolean contains(Object obj)判断当前集合中是否包含指定对象
public boolean isEmpty()判断当前集合是否为空
public int size()返回集合中元素的个数 / 集合的长度

这里介绍三种遍历方法:

  1. 迭代器
  2. 增强for循环
  3. lambda表达式
       lambda:
       ts.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });
HashSet

无序,不重复,无索引

  • 底层采用哈希表存储数据
  • 哈希表是一种对于增删改查性能都比较好的结构
  • jdk8开始采用数组+链表+红黑树(新增)组成

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

  • 使用hashCode方法计算出来的int类型整数
  • 这个方法定义在Object类中,所有对象都可调用,默认使用地址值计算
  • 一般情况下会重写hashCode方法,使用对象内部属性值进行计算

在小部分情况下,不同的属性值或地址值计算出来的hash值也有可能一样(哈希碰撞)

如何重写?
按下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方法

三个问题:

  1. 为什么HashSet遍历顺序和存入顺序不一样? :因为遍历是按照index顺序遍历的,存入位置是按照hash值计算出来的
  2. 为啥没索引:因为有链表存在,一个索引可能有多个数据,没必要
  3. 去重机制:相同属性的对象计算得到的index是相同的,再加上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叫Entry对象

其实现类是HashMap<>,因此Map<> m = new HashMap<>

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
  • toArray() 总是返回 Object[],不能直接转为 String[] 等具体类型(会抛 ClassCastException)。
  • toArray(new String[0]) 可以安全返回 String[]。底层使用优化的反射

遍历方式

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

  • 键值对遍历 entrySet();

先来介绍Entry:

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

判断 key 是否重复的依据是:

  • 先比较 hashCode()
  • 再调用 equals()
HashMap

允许 1个 null 键和多个 null 值

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

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

底层原理和HashSet一样,都是哈希表结构,数组+链表+红黑树

LinkedHashMap

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

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

TreeMap

底层同TreeSet,红黑树

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

  • 实现Comparable接口
  • 创建集合时传递Comparator比较器对象,指定比较规则
HashTable
  • 线程安全:Hashtable 是线程安全的。它的几乎所有公共方法(如 put, get, remove)都使用了 synchronized 关键字修饰。这意味着多个线程可以同时访问一个 Hashtable 实例而不会导致数据不一致。
  • 不允许 Null:Hashtable 的 Key 和 Value 都不允许为 null。如果插入 null,会抛出 NullPointerException。
  • 继承关系:它继承自 Dictionary 类(这是一个过时的抽象类),并实现了 Map 接口。
  • 无序性:它不保证键值对的遍历顺序,顺序可能会随时间变化。

为什么 Hashtable 被弃用了?

  1. 性能瓶颈:由于它在整个方法上加了 synchronized 锁(即“对象锁”),在高并发环境下,所有线程必须竞争同一把锁,导致并发效率极低。
  2. 设计陈旧:它是 JDK 1.0 的产物,其父类 Dictionary 已经过时。
  3. 功能限制:它不支持红黑树优化,在极端哈希冲突下性能退化严重。

迭代器

在 Java 中,只要一个类实现了 Iterable<T> 接口,它就可以使用 增强 for 循环(for-each),也就可以提供 Iterator<T>

所有标准集合类(如 List, Set, Queue 等)都实现了 Iterable,所以它们天然支持迭代器。

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表示coll.forEach(s->System.out.println(s))

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

能得到的流:

获取方式方法名说明
单列集合default Stream<E> stream()Collection 中的默认方法
双列集合无法直接使用 stream 流
数组public static <T> Stream<T> stream(T[] array)Arrays 工具类中的静态方法
一堆零散数据public static <T> Stream<T> of(T... values)Stream 接口中的静态方法

中间操作

方法名称描述返回类型
.filter(Predicate p)过滤流中的元素,保留满足条件的元素Stream<T>
.map(Function f)对流中的每个元素执行给定的操作,并将结果映射到一个新的值Stream<R>
.flatMap(Function f)类似于 map,但每个元素被映射到一个流,然后这些流会被合并成一个流Stream<R>
.distinct()去重操作,基于元素的 equals 方法Stream<T>
.sorted()自然排序(根据元素的 compareTo 方法)Stream<T>
.sorted(Comparator c)根据提供的比较器进行排序Stream<T>
.peek(Consumer c)对每个元素执行给定的操作,主要用于调试Stream<T>
.limit(long n)返回由该流的前 n 个元素组成的流Stream<T>
.skip(long n)跳过前 n 个元素,返回剩余元素组成的流Stream<T>
.concat(Stream a, Stream b)合并两个流为一个流Stream<T>

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

终结操作

方法名称描述返回类型
.forEach(Consumer c)对流中的每个元素执行给定的动作void
.collect(Collector c)将流中的元素收集到集合、列表、Map等数据结构中R
.toArray()将流中的元素收集到一个数组中Object[]
.reduce(BinaryOperator b)使用指定的归约操作对流中的元素进行累积计算Optional<T>T
.min(Comparator c)根据提供的比较器找到最小元素Optional<T>
.max(Comparator c)根据提供的比较器找到最大元素Optional<T>
.count()计算流中元素的数量long
.anyMatch(Predicate p)测试流中是否有至少一个元素匹配给定的谓词boolean
.allMatch(Predicate p)测试流中的所有元素是否都匹配给定的谓词boolean
.noneMatch(Predicate p)测试流中是否没有元素匹配给定的谓词boolean
.findFirst()返回流的第一个元素,如果流为空则返回空的 OptionalOptional<T>
.findAny()返回当前流中的任意元素,如果流为空则返回空的 Optional。在并行流中,可能提高效率Optional<T>

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

所以,Java 团队在工具类 Arrays 中提供了静态方法 stream() 来“桥接”数组和 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;
        }
    }

方法引用

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

  1. 引用处必须是函数式接口

  2. 被引用方法必须已经存在

  3. 被引用方法的形参和返回值需要和抽象方法保持一致

  4. 被引用方法的功能需满足要求

    Arrays.sort(arr, 函数式接口)
    
    
    // 声明的函数式接口
    public int subTraction(int n1, int n2){
    	return n2 - n1;
    }
    

    ::作为方法引用符

    Arrays.sort(arr, Sub::subTraction)
    

引用静态方法

类名::静态方法名

例: Integer::parseInt // 字符串转int

实例引用成员方法

对象实例::成员方法

  • 其他类: 其他类对象::方法名
  • 本类: this::方法名
  • 父类: super::方法名

引用构造方法

为了创建对象,比如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);

面向对象

  1. 封装(Encapsulation):封装是将数据和方法组合在一起,对外部隐藏实现细节,只公开对外提供的接口。这样可以提高安全性、可靠性和灵活性。
  2. 继承(Inheritance):继承是从已有类中派生出新类,新类具有已有类的属性和方法,并且可以扩展或修改这些属性和方法。这样可以提高代码的复用性和可扩展性。
  3. 多态(Polymorphism):多态是指同一种操作作用于不同的对象,可以有不同的解释和实现。它可以通过接口或继承实现,可以提高代码的灵活性和可读性。
  4. 抽象(Abstraction):抽象是从具体的实例中提取共同的特征,形成抽象类或接口,以便于代码的复用和扩展。抽象类和接口可以让程序员专注于高层次的设计和业务逻辑,而不必关注底层的实现细节。
  • 成员变量
  • 成员方法
  • 构造器

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

  • 代码块
  • 内部类

修饰符讲解

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

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

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

静态方法能被重写吗? → 不能!子类定义同名静态方法是方法隐藏(Method Hiding),不是多态。

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

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

protecteddefault修饰符的主要区别在于它们的访问权限范围:protected允许子类和同一包中的类访问,而default(包级私有)仅允许同一包中的类访问。

继承

image-20260212085658452

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

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

父子类转换

  1. 向上转型 (Up):Parent p = new Child(); —— 自动。用于统一处理不同子类(多态)。
  2. 向下转型 (Down):Child c = (Child) p; —— 手动。用于恢复子类特有功能。
  3. 转型的死理
    • 能转的前提:对象本身必须是你转的那个类型(或者是其子类)。
    • 兄弟类之间不能互转(Dog 不能转成 Cat)。
  4. 安全口诀:先用 instanceof 判断,再进行强制类型转换。

多态

多态是指:同一个方法调用,在运行时根据对象的实际类型执行不同的实现。只能调用父类中已有的方法(重写的会走子类版本),不能直接调用子类特有方法;它依赖继承、重写和向上转型,核心价值是解耦与扩展——新增子类无需改动原有代码,是开闭原则的体现。

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

“软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。”

多态调用成员的特点

  • 变量调用:编译看左边,运行也看左边。
  • 方法调用:编译看左边,运行看右边。

多态的弊端是不能调用子类特有方法,需要强转

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

接口

image-20260212085709654

接口和抽象类的区别*

接口是对方法的抽象

  1. 定义的关键字不同。

  2. 子类继承或实现关键字不同。

  3. 类型扩展不同:抽象类是单继承,而接口是多继承。

  4. 方法访问控制符:抽象类中的抽象方法不能被 private 修饰;接口默认的是 public 控制符。

  5. 属性方法控制符:抽象类无限制,而接口有限制,接口默认的是 public 控制符。

    类型抽象类中的字段接口中的字段
    访问控制符✅ 无限制:可用 privateprotectedpublic、包私有只能是 public static final(即使不写)
    是否可变可以是普通变量(可变)所有字段自动是 public static final(即常量)
  6. 方法实现不同:抽象类中的普通方法必须有实现,抽象方法必须没有实现;而接口中普通方法不能有实现,但在 JDK 8 中的 static 和 defualt 方法必须有实现。

  7. 静态代码块的使用不同:抽象类可以有静态代码块,而接口不能有。

内部类

  • 成员内部类
  • 静态内部类
  • 局部内部类
  • 匿名内部类:掌握

成员内部类(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();
    }
}

面试要点

  • 持有外部类引用,能访问其所有成员;
  • 编译后生成 Outer$Inner.class
  • 不能定义 static 成员(除非是常量)。

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


  1. 静态内部类(Static Nested Class)

特点:用 static 修饰,不依赖外部类实例,不能访问外部类非 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();
    }
}

面试要点

  • 更像“顶级类”,只是命名空间在外部类内;
  • 常用于 Builder 模式(如 AlertDialog.Builder);
  • 无外部类引用,更轻量、安全。

  1. 局部内部类(Local Inner Class)

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

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

面试要点

  • 很少用,多用于临时逻辑封装;
  • 必须在定义它的方法内实例化;
  • 访问局部变量需“事实 final”(Java 8+ 放宽了语法,但语义不变)。

  1. 匿名内部类(Anonymous Inner Class) ✅【重点掌握】

特点:没有名字,通常用于继承类或实现接口的一次性使用场景本质:继承/实现 + 实例化一步完成。

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

  1. 生成一个新类:它偷偷创建一个类,名字通常叫 外部类名$1。
  2. 实现/继承:让这个 $1 类去 implements Runnable。
  3. 编译成文件:你会发现文件夹里多了一个 Outer$1.class。
  4. 实例化:在代码运行到那一行时,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 的匿名类");
    }
};

面试要点

  • 广泛用于事件监听、回调、线程、Lambda 表达式前身;
  • 编译后生成 Outer$1.classOuter$2.class…;
  • 同样只能访问 effectively final 的局部变量;
  • Java 8+ 中,很多匿名内部类可用 Lambda 替代(仅限函数式接口)。

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提供了一个标记接口Cloneable,继承了Cloneable并重写clone方法表示该类可被克隆,这是标准写法:

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

**这里提一下浅拷贝和深拷贝:**浅拷贝对引用类型只是复制指针,还是指向同一对象。深拷贝会创建新的引用对象。

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

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

protected void finalize() throws Throwable { }

finalize 方法的调用时机

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

反射

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

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

image-20260212085727464

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

image-20260212085735486

获取class

三种方式

  1. Class.forName("全类名");

    最常用

    该方法会触发指定类的加载、链接和初始化(包括执行 static 块),前提是该类尚未被加载或初始化。若类已存在,则直接返回其 Class 对象。适用于需要动态加载并立即初始化类的场景(如 JDBC 驱动注册)。

  2. 类名.class

    更多当参数传递

    类名在编译期已知。如果该类尚未加载,JVM 会触发加载和链接,但不会执行初始化(static 块不运行)。类信息会被放入方法区(或元空间),Class 对象可被获取,但实例尚未创建。适用于仅需类型引用而不触发初始化的场合(如泛型、注解处理)。

  3. 对象.getClass();

    有对象才能使用

    对象实例必须已经创建。这意味着该类早已完成加载、链接和初始化全过程。此方法返回的是对象实际运行时类型的 Class 对象(支持多态),常用于反射中判断真实类型或动态调用。

三种方式获取的是一个对象

获取构造方法

方法名说明
public Constructor<?>[] getConstructors()返回该类所有公共(public)构造方法的数组。仅包含可被当前类访问的 public 构造函数。
public Constructor<?>[] getDeclaredConstructors()返回该类所有声明的构造方法的数组,包括 private、protected、default 和 public 的,不考虑访问权限。
public Constructor<T> getConstructor(Class<?>... parameterTypes)根据指定的参数类型,返回一个公共构造方法对象。如果找不到匹配的 public 构造方法,则抛出异常。
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)根据指定的参数类型,返回一个任意访问权限的构造方法对象(包括 private)。若找不到则抛出异常。
public T newInstance(Object... initArgs)使用此 Constructor 对象创建一个新的实例。传入构造方法所需的参数。若构造方法是私有的,需先调用 setAccessible(true) 才能成功创建对象。
public void setAccessible(boolean flag)设置是否取消 Java 访问检查。当 flag = true 时,允许访问私有(private)构造方法或字段,绕过访问控制限制。

如何得到权限修饰符?

object.getModifiers();

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

获取成员变量

将Constructor改成Field

方法名说明
void set(Object, Object vavlue)为对象赋值
Object get(Object obj)获取值
String getName()获取成员变量名字
Class<> getType()获取成员变量类型
public void setAccessible(boolean flag)不仅对于private访问,用在static上还可以强制修改变量

对内部类的处理

  1. 查找内部类:Class.getDeclaredClasses() —— 获取类中定义的所有内部类(包括私有的)。
  2. 查找宿主类:Class.getDeclaringClass() —— 如果当前类是内部类,返回它的外部类。
  3. 判断身份
    • isMemberClass():是否是成员内部类。
    • isLocalClass():是否是局部内部类。
    • isAnonymousClass():是否是匿名内部类。

获取成员方法

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

方法名说明
public Method[] getMethods()返回该类及其所有父类中所有公共(public)成员方法的数组,包括继承的方法。不包含 private、protected 或 default 方法。
public Method[] getDeclaredMethods()返回该类自己声明的所有成员方法的数组,包括 private、protected、default 和 public,但不包括继承的方法
public Method getMethod(String name, Class<?>... parameterTypes)根据方法名和参数类型,返回一个公共方法对象。如果找不到匹配的 public 方法,则抛出 NoSuchMethodException
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)根据方法名和参数类型,返回一个任意访问权限的方法对象(包括 private),但不包括继承的方法。若找不到则抛出异常。
public Object invoke(Object obj, Object... args)调用该 Method 对象所代表的方法。 - 参数一:调用该方法的对象实例(如果是静态方法,传 null) - 参数二:传递给方法的实际参数(可变参数) - 返回值:方法执行后的返回值(无返回值时为 null

反射的作用

  1. 获取一个类里的所有信息,然后执行业务逻辑
  2. 结合配置文件,动态的创建对象并调用方法
    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(); // 调用真实业务

    }
  • 冗余:如果接口增加了方法,代理类也必须跟着改。
  • 不灵活:一个代理类只能服务于一个特定的接口,如果项目中有 100 个 Service,就要写 100 个代理类。

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

如何创建代理

java.lang.reflect.Proxy类提供了为对象产生代理的核心方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • 参数一 (ClassLoader loader):类加载器,用于加载动态生成的代理类。通常传入目标对象的类加载器。
  • 参数二 (Class<?>[] interfaces):接口数组,定义了代理对象应该实现哪些接口,也就是代理对象“长什么样”。
  • 参数三 (InvocationHandler h):调用处理器,这是最核心的部分。当代理对象的方法被调用时,所有的调用都会被转发到这个处理器的invoke方法中。

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

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 代理要求目标类必须实现接口

  1. 单继承限制:JVM 生成的代理类名为 $Proxy0,它默认已经继承了 java.lang.reflect.Proxy。由于 Java 不支持多继承,它无法再继承你的目标类。
  2. 面向接口模仿:既然不能继承类,它只能通过实现相同的接口来“模仿”目标类的行为。

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

CGLIB

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

工作原理: CGLIB直接在内存中构建目标类的子类。

  • 它是继承关系:代理对象是目标类的“子类”。
  • 能力更强:天然拥有父类的所有的 public 方法(包括特有方法)。

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

  • 如果类实现了接口,Spring 默认使用 JDK 代理
  • 如果类没实现接口,Spring 强制切换为 CGLIB

关键配置:proxy-target-class=true 在 Spring Boot 中,这个配置默认为 true。它的字面意思是:“是否针对目标类(Target Class)本身进行代理?”

  • 开启后,Spring 即使发现你有接口,也会暴力使用 CGLIB 生成子类。
  • 好处:这样不仅能统计到接口方法,连类里的特有方法也能被 AOP 拦截并统计到(只要不是 final 方法)。

lambda

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

  • 语法(参数) -> { 代码体 }
  • 前提:只能用在函数式接口上(如 RunnableComparatorConsumer 等)
  • 作用:避免写冗长的匿名内部类
Runnable r = () -> System.out.println("Hello, Lambda!");
r.run();

泛型

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

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

泛型的使用

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

泛型的继承和通配符

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

List<String>List<Object> 没有继承关系,不能互相赋值

如何理解?

Java 的泛型在编译后会被类型擦除,变成原始类型,因此 List<String>List<Object> 在运行时其实是同一个类型,JVM 无法区分。为了防止类型安全问题(比如把 Integer 错误地放进 List<String>),编译器不允许 List<String> 继承自 List<Object>,即泛型类型本身不具备继承性;但你放进泛型里的对象(如 String)依然是 Object 的子类,数据本身的继承关系依然有效

举例:

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 禁止创建泛型数组(如 new List<String>[10]),是因为泛型在运行时会被擦除,而数组依赖运行时类型检查,若允许泛型数组,结合数组的协变性,会导致堆污染和隐式的 ClassCastException,破坏类型安全。

// 假设这行能编译(实际上不能):
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类的子类。异常体系分为两大重要分支:

  • Error:表示程序无法处理的严重问题

  • Exception:表示程序许可处理的异常情况

Exception 又进一步分为:

  • CheckedException(受检异常):编译器强制要求处理

  • RuntimeException(运行时异常):编译器不强制处理

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, 那么进行覆盖返回

断言

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

assert condition;
// 或
assert condition : message;
  • condition:一个布尔表达式。如果为 false,则触发断言失败。
  • message(可选):当断言失败时,作为错误信息的一部分输出。
public class AssertionExample {
    public static void main(String[] args) {
        int age = -5;

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

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

默认情况下,Java 虚拟机是禁用断言的,所以即使 age = -5,程序也不会报错。

要启用断言,必须在运行时加上 -ea(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)可被捕获并处理
默认启用?否(需 -ea 开启)
生产环境通常关闭必须保留

File

三种对象创建方式:

public File(String pathname) // 根据文件路径创建文件对象

public File(String parent, String child) // 根据父路径名字符串和子路径名字符串创建文件对象

public File(File parent, String child) // 根据父路径对应文件对象和子路径名字符串创建文件对象

常见成员方法

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

创建和删除

方法名称说明
public boolean createNewFile()创建一个新的空的文件
public boolean mkdir()创建单级文件夹
public boolean mkdirs()创建多级文件夹
public boolean delete()删除文件、空文件夹

获取和遍历

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

IO流

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

按流的方向分类

  • 输入流
    • 作用:从外部源(如文件、网络)读取数据到程序中。
    • 关键词:读取(Read)
  • 输出流
    • 作用:将程序中的数据写入到外部目标(如文件、控制台)。
    • 关键词:写出(Write)

按操作文件类型分类

  • 字节流
    • 用于处理所有类型的文件(二进制文件),以字节(8位)为单位进行读写。
    • 适用文件类型:图片、音频、视频、可执行文件等。
    • 示例类:FileInputStream, FileOutputStream
  • 字符流
    • 专门用于处理纯文本文件(如 .txt, .java),以字符(16位 Unicode)为单位进行读写。
    • 优点:自动处理编码转换(如 UTF-8、GBK)。
    • 示例类:FileReader, FileWriter

💡 小贴士:

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

字节流

FileOutputStream

创建字节流输出对象new FileOutputStream(路径 或 File对象)

参数一:路径或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();
}

写数据的三种方式

  1. void write(int b) 一次写一个字节数据
  2. void write(byte[] b) 一次写一个字节数组数据
  3. void write(byte[] b, int off, int len) 写一个字节数组的部分数据,off是起始索引,len是写入长度
FileInputStream

创建字节流输入对象 new FileInputStream(路径 或 File对象)

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

如何读取数据:

  • 每次读取一个字节,返回该字节的 int 值(0~255),若返回 -1 表示已读到文件末尾
  • 可配合循环逐字节读取,或使用字节数组批量读取以提高效率
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();
}

读数据的三种方式

  1. int read() 一次读一个字节,返回读取的字节值(int 类型),到达末尾返回 -1
  2. int read(byte[] b) 一次读取多个字节,存入字节数组 b 中,返回实际读取的字节数,到达末尾返回 -1
  3. int read(byte[] b, int off, int len) 从输入流中最多读取 len 个字节,存入数组 b 的从 off 开始的位置,返回实际读取的字节数,到达末尾返回 -1
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字节的缓冲区

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

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

释放资源的方式

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

在 Java 中,最常用且推荐的资源释放方式是 try-with-resources(JDK 7 引入),适用于所有实现了 AutoCloseable 接口的 I/O 流对象(如 FileInputStreamFileOutputStream 等)。

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()
  • 自动释放:无论是否发生异常,fisfos 都会在 try 结束时自动调用 close()
  • 支持多个资源:用分号分隔即可。
  • 代码简洁安全:避免了传统 finally 块中手动关闭的繁琐和潜在错误。

💡 建议:只要使用 JDK 7 或更高版本,一律优先使用 try-with-resources 来管理 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(Guo Biao Kuozhan,国家标准扩展)是一种中文字符编码标准,主要用于简体中文的字符表示。它是对GB2312-80标准的扩展,包含了更多的汉字和符号,广泛应用于Windows系统、网页、数据库等中文信息处理环境中,使用两个字节(高位和低位)编码,高位最高位为1表示是双字节字符(比如中文)win的ANSI(本地默认编码)就是默认使用的GBK

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

Unicode

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

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

image-20260212085818240

编码和解码

为何会有乱码?

  1. 编码和解码不统一
  2. 读取不完全,字节流单字节读取但编码是多字节

如何避免编码?

  1. 不要用字节流读取文本文件

  2. 编码解码时使用同一个码表,同一个编码方式

  3. 编码(字符串 → 字节数组)

    • str.getBytes():使用默认编码(不推荐)
    • str.getBytes("UTF-8"):指定编码(推荐)
    • 推荐使用 str.getBytes(StandardCharsets.UTF_8)
  4. 解码(字节数组 → 字符串)

    • new String(bytes):使用默认编码(易乱码)
    • new String(bytes, "UTF-8"):指定编码(必须与编码时一致)
    • 推荐使用 new String(bytes, StandardCharsets.UTF_8)

字符流

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

FileWriter

创建字符输出流对象:new FileWriter(路径 或 File对象)new FileWriter(路径, boolean append)

  • 参数一:路径字符串或 File 对象
  • 参数二(可选):续写开关
    • true:追加写入(文件末尾)
    • false 或省略:覆盖写入(默认)

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

如何换行写?

  • Java 中统一使用 \n,运行时会根据操作系统自动转换:
    • Windows → \r\n
    • Linux/macOS → \n
  • 也可手动写 System.lineSeparator() 获取当前系统换行符
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();
}

写数据的三种方式

  1. void write(int c)
    • 写一个字符(实际传入 char 的 int 值,如 'a'97
  2. void write(char[] cbuf)
    • 一次写入整个字符数组
  3. void write(char[] cbuf, int off, int len)
    • 写入字符数组的一部分,从 off 开始,写 len 个字符
  4. void write(String str)
    • 写入整个字符串(常用!)
  5. void write(String str, int off, int len)
    • 写入字符串的一部分

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

底层原理

image-20260212085835214

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


FileReader

创建字符输入流对象:new FileReader(路径 或 File对象)

  • 参数:路径或 File 对象
  • 要求:文件必须存在,否则抛出 FileNotFoundException
  • 编码:使用平台默认字符编码(无法指定,若需控制编码请用 InputStreamReader

如何读取数据?

  • 每次读一个字符,返回其 int 值(0~65535),返回 -1 表示读到末尾
  • 可逐字符读取,或使用字符数组批量读取(推荐,效率高)
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();
}

读数据的三种方式

  1. int read()
    • 读一个字符,返回其 int 值,末尾返回 -1
  2. int read(char[] cbuf)
    • 将字符读入 cbuf 数组,返回实际读取的字符数,末尾返回 -1
  3. int read(char[] cbuf, int off, int len)
    • 最多读 len 个字符,存入 cbufoff 开始的位置,返回实际读取数

高效复制示例(字符流)

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

⚠️注意:FileReader / FileWriter 仅适用于文本文件,且无法指定编码。 若需指定编码(如 UTF-8),应使用:

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

底层原理

image-20260212085851023

字节流没有缓冲区

读取:

  1. 判断缓冲区中是否有数据可以读取

    • 如果有 → 直接从缓冲区读取字符
    • 如果没有 → 调用底层字节流从文件中读取更多字节,填充缓冲区
  2. 缓冲区没有数据时

    • 通过底层 FileInputStream 从文件中读取字节
    • 尽可能多地装满缓冲区(最多 8192 字节)
    • 如果文件已到末尾,则返回 -1
  3. 缓冲区有数据时

    • 从缓冲区中取出字节,并进行 解码操作(根据默认或指定编码)
    • 解码后得到字符,返回给上层程序

    空参的read方法:一次读取一个字节,遇到中文读多个,解码并转成十进制返回

    有参的read方法:把读取字节,解码,强转三步合并后放到数组中。

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

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

字符缓冲流

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

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

Randomse随机读取

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

参数:

  1. 路径或File对象:指向目标文件。
  2. 访问模式(必须填),常用的有两种:"r":只读模式(若文件不存在会抛出 FileNotFoundException)。"rw":读写模式(若文件不存在,会自动帮你创建一个新文件)。

如何读取/写入数据:

  • 它的内部自带一个文件指针(就相当于我们在Word里打字时的闪烁光标),默认在文件开头(索引为0)。
  • 每次读或写数据后,这个指针会自动往后移动。
  • 特色:可以通过代码随意挪动这个“光标”的位置,实现“指哪读哪”、“指哪改哪”。
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();
}
核心方法读写方式与功能描述
void seek(long pos)将文件指针(光标)绝对定位到文件中的指定位置 pos(从 0 开始计数)。
long getFilePointer()获取位置:返回当前文件指针(光标)所在的字节偏移量。
int read() / int read(byte[] b)读取数据:与普通字节流一致,一次读取一个字节或填充字节数组;读取完成后,指针自动向后移动。
void write(byte[] b)写入数据:写入字节数组。配合 seek 方法使用,可以覆盖/替换文件中特定位置的内容(而非总是追加)。
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,它打破传统字节流的性能瓶颈,直接对接操作系统底层机制,专攻海量数据与高并发场景。

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

转换流

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

流类型类名
转换输入流InputStreamReader
转换输出流OutputStreamWriter
// 参数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"
);

序列化流

序列化流(如 ObjectOutputStreamObjectInputStream)的作用是将 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

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

读写多个对象

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

一定先写再读

打印流

只能写(输出),不能读

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

  • PrintStream:字节打印流,处理字节数据
  • PrintWriter:字符打印流,处理字符数据

打印流的特点

  • 自动刷新:可以在输出换行符或字节数组后自动刷新缓冲区
  • 不抛出IOException:打印流的方法不会抛出IOException,而是设置一个内部错误标志
  • 格式化输出:提供了格式化输出的方法,如printf()
  • 支持多种数据类型:可以直接打印各种基本数据类型和对象,实现数据的原样写出
  • 特有的写出方法可以实现自动刷新和换行

打印流的应用场景

  • 控制台输出:System.out和System.err就是PrintStream实例
  • 日志记录:将程序运行信息输出到日志文件
  • 数据导出:将数据以格式化的方式输出到文件或网络
  • 调试信息输出:在开发过程中输出调试信息
PrintStream
构造方法说明
public PrintStream(OutputStream/File/String)关联字节输出流/文件/文件路径
public PrintStream(String fileName, Charset charset)指定字符编码
public PrintStream(OutputStream out, boolean autoFlush)自动刷新
public PrintStream(OutputStream out, boolean autoFlush, String encoding)指定字符编码且自动刷新

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

常用方法

成员方法说明
public void write(int b)常规方法:规则跟之前一样,将指定的字节写出
public void println(Xxx xx)特有方法:打印任意数据,自动刷新,自动换行
public void print(Xxx xx)特有方法:打印任意数据,不换行
public void printf(String format, Object... args)特有方法:带有占位符的打印语句,不换行
PrintWriter
构造方法签名参数说明功能描述
PrintWriter(OutputStream out)out: 字符输出流(如 System.outFileOutputStream不自动刷新、使用默认字符编码
PrintWriter(OutputStream out, boolean autoFlush)out: 字符输出流 autoFlush: 是否自动刷新(true/falseautoFlush=true,调用 println() 等方法会自动刷新缓冲区。
PrintWriter(OutputStream out, boolean autoFlush, Charset charset)out: 字符输出流 autoFlush: 是否自动刷新 charset: 指定字符编码(如 UTF-8
PrintWriter(File file, Charset charset)file: 目标文件对象 charset: 指定字符编码创建一个写入文件的 PrintWriter,使用指定字符编码(Java 11+ 支持)。
PrintWriter(String fileName, Charset charset)fileName: 文件路径字符串 charset: 指定字符编码通过文件名和指定编码(Java 11+ 支持)。
成员方法说明
public void write(int b)常规方法:规则跟之前一样,将指定的字符写出
public void println(Xxx xx)特有方法:打印任意数据,自动刷新,自动换行
public void print(Xxx xx)特有方法:打印任意数据,不换行
public void printf(String format, Object... args)特有方法:带有占位符的打印语句,不换行

解压缩流

java默认只识别zip格式

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

java将压缩包里的每一个文件视为ZipEntry对象,解压就是将每一个ZipEntry按照层级拷贝到本地另一个文件夹中

创建解压缩流并遍历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);
        }
    }
}

工具类

老牌Commons工具类,强大

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

一个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大模型封装

简介 | Hutool

多线程

进程是操作系统进行资源分配和调度的基本单位,是一个正在运行的程序的实例。 线程是 CPU 调度和执行的最小单位,被包含在进程中,是进程中的实际执行单元。 同一进程内的多个线程共享该进程的内存空间(如堆、方法区)和系统资源(如文件句柄),但每个线程拥有独立的栈(如虚拟机栈 Stack、程序计数器 PC,用于记录局部变量和执行进度)

  • 并行 (Parallel):CPU 逻辑核心数决定了能真正同时运行的线程上限(如 8 核 CPU 真正并行跑 8 个线程)。
  • 并发 (Concurrent):依靠 CPU 毫秒级的“时间片轮转”,让单核也能制造出多线程“同时运行”的假象。

线程的状态

通俗说法操作系统术语JavaThread.State含义说明典型触发方式是否可恢复
新建NewNEW线程对象已创建,但尚未启动new Thread()✅(调用 start()
就绪ReadyRUNNABLE线程已启动,等待 CPU 调度(具备运行条件)调用 start() 后,未获得 CPU 时间片—(属于运行态)
运行RunningRUNNABLE线程正在 CPU 上执行代码获得 CPU 时间片后自动进入
阻塞BlockedBLOCKED线程等待获取 synchronized 锁(该锁被其他线程持有)进入 synchronized 块但锁不可用✅(锁释放后)
等待WaitingWAITING线程无限期等待,直到被其他线程显式唤醒Object.wait(), Thread.join(), LockSupport.park()✅(需被唤醒)
睡眠 /计时等待Timed WaitingTIMED_WAITING线程等待指定时间,超时后自动恢复Thread.sleep(1000), Object.wait(1000), Thread.join(1000)✅(超时或中断)
死亡Terminated / DeadTERMINATED线程执行完毕(正常结束或抛出未捕获异常)run() 方法执行完成 或 抛出异常❌(不可重启)

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

为什么 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(); // 开启线程

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

3.实现Callable接口重写call并创建FutureTask对象管理多线程运行的结果

好处是call有返回值表示多线程执行的结果,FutureTask对象来管理运行的结果

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常见成员方法

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

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

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

一个线程看到的另一个线程的变量是final类型,可读不可修改

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其执行是不受影响的

锁(Lock)是为了在多线程并发访问共享数据时,确保操作的原子性、可见性和有序性,从而防止数据不一致、丢失更新或竞态条件(Race Condition)而引入的同步机制。

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

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

同步代码块

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

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

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

对象锁和类锁

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

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

对象的组成
  • 对象头

  • 实例数据

  • 对齐填充

    Java 要求对象大小是 8 字节的整数倍,并通过“对齐填充”实现这一要求,主要目的是提升 CPU 内存访问效率、简化垃圾回收,并保证跨平台一致性。

[ 栈(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           │
└───────────────────────┘
如何加锁

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

  1. 对象锁

    • synchronized(this)synchronized(任意实例对象)
    • 用于保护实例变量,粒度是每个线程实例
  2. 类锁

    • synchronized(ClassName.class)
    • 用于保护静态变量,粒度是类(每个类可以有多个线程)
  3. lock锁

    • JDK5以后提供Lock锁对象,void lock() 获得锁,void unlock() 释放锁
    • 实现类ReentrantLock实例化
     private static final ReentrantLock lock = new ReentrantLock();
     定义在类上的锁,所有类实例共享一把锁
    

锁的对象 = 引用指向的目标,不是引用变量本身,更不是当前类实例。

死锁

死锁的必要条件:互斥条件、请求与保持条件、不可剥夺条件和循环等待条件。

  1. 互斥条件:资源在同一时刻只能被一个进程占用。如果资源可以被多个进程共享,则不会发生死锁。例如,打印机在被一个进程使用时,其他进程无法同时使用该打印机。
  2. 请求与保持条件:一个进程在持有至少一个资源的同时,又请求其他被占用的资源。如果进程在请求资源时不持有任何资源,或者可以释放已持有的资源,则不会发生死锁。
  3. 不可剥夺条件:已经获得的资源在未使用完之前,不能被其他进程强行剥夺。只有占用资源的进程主动释放资源,其他进程才能使用。如果系统允许强制剥夺资源,则不会发生死锁。
  4. 循环等待条件:多个进程之间形成一种头尾相接的循环等待资源关系。如果线程之间没有形成循环等待,则不会发生死锁。例如,进程A等待进程B的资源,进程B等待进程C的资源,而进程C又等待进程A的资源,这样就形成了一个循环等待的局面。
等待唤醒机制*

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

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

notifyAll()和notify()唤醒范围是:当前对象监视器(monitor)上所有因调用 wait() 而处于等待状态的线程

在 Java 中,每个对象都有一个与之关联的 内置锁(Intrinsic Lock)等待集合(Wait Set)

当你在一个对象上调用:

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

该线程会:

  1. 释放 obj 的锁;
  2. 进入 obj等待集合(Wait Set),进入阻塞状态。

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

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

那么:

所有在 obj 的等待集合中的线程都会被唤醒,并尝试重新竞争 obj 的锁。

例:

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
底层结构有界数组(循环队列)链表(节点)
容量必须指定,有界默认无界(最大 Integer.MAX_VALUE),也可设为有界
是否支持公平锁是(构造时可选)
锁机制单锁(入队/出队共用)双锁(入队、出队分离)
吞吐性能一般通常更高(尤其高并发)
内存开销小(固定数组)较大(每个元素额外 Node 对象)
常用场景固定缓冲区、内存敏感高吞吐、不确定数据量

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

方法功能阻塞行为
put(E e)入队,队满时等待是(永久阻塞)
take()出队,队空时等待是(永久阻塞)
offer(E e)尝试入队,失败返回 false
poll()尝试出队,失败返回 null
offer(E e, timeout, unit)带超时入队是(最多等 timeout)
poll(timeout, unit)带超时出队是(最多等 timeout)
add(E e)入队,失败抛异常
peek()查看队首(不移除)

线程池

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

方法名称说明
public static ExecutorService newCachedThreadPool()创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads)创建有上限的线程池

常用方法

方法签名说明
void execute(Runnable command)提交一个无返回值的任务(Runnable)到线程池执行。
<T> Future<T> submit(Callable<T> task)提交一个有返回值的任务(Callable),返回一个 Future 对象用于获取结果或检查状态。
<T> Future<T> submit(Runnable task, T result)提交一个 Runnable 任务,并指定一个默认返回值,通过 Future.get() 获取该结果。
List<Runnable> shutdownNow()立即尝试停止所有正在执行的任务,暂停等待任务,并返回等待执行的任务列表。
void shutdown()平滑关闭线程池:不再接受新任务,但会继续执行已提交的任务。
boolean isShutdown()判断线程池是否已调用 shutdown() 方法(即是否正在关闭或已关闭)。
boolean isTerminated()判断线程池是否已完全终止(所有任务都已完成)。
boolean awaitTermination(long timeout, TimeUnit unit)阻塞当前线程,直到线程池终止或超时,常配合 shutdown() 使用。

💡 提示

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

自定义线程池

ThreadPoolExecutor pool = new ThreadPoolExecutor();

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

  • **核心线程:**线程池创建新的线程对象,任务完毕后线程回归线程池不会死亡
  • **阻塞队伍长度:**线程池满后(核心线程占满)新来的任务进入阻塞队伍
  • **临时线程:**核心线程占满且阻塞队伍长度也达到最大值时,创建临时线程用于处理后续到来的任务

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

ThreadPoolExecutor.

  1. AbortPolicy(默认)
    • 线程池和队列都满时,直接抛出 RejectedExecutionException
    • 适合不能容忍任务丢失、需立即感知失败的场景。
  2. DiscardPolicy
    • 静默丢弃新提交的任务,不抛异常。
    • 不推荐使用,因为无法知道任务被丢弃。
  3. DiscardOldestPolicy
    • 丢弃队列中等待时间最长的任务,然后尝试重新提交当前任务。
    • 适合新任务比旧任务更重要的场景(如实时数据处理)。
  4. CallerRunsPolicy
    • 由提交任务的线程(调用者)自己执行该任务。
    • 能避免任务丢失,但会阻塞调用线程,降低系统吞吐量。
    • 适合希望“降级但不丢任务”的场景。

实例

 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密集型: 最大并行数+1,比如count = 8,那么设置为9 (防止偶尔一个线程出问题)
  • IO密集型最大并行数 * 期望cpu利用率 * 总时间/cpu时间

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

网络编程

在网络通信协议下,不同计算机上运行的程序,进行的数据传输

两种架构:

  • C/S:Client/Server 客户端/服务器架构
  • B/S:Browser/Server 浏览器/服务器架构

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

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

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

网络编程三要素

  • IP:设备在网络中的地址,是唯一标识
  • 端口号:应用程序在设备中唯一的标识
  • 协议:数据在网络中传输的规则,常见的协议有UDP,TCP,http,https,ftp

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

  • 用户数据报协议
  • 面向无连接的协议,不管是否连接成功没收到就算了
  • 速度快,有大小限制一次最多64K,数据不安全

TCP协议

  • 传输控制协议
  • 面向连接的,三次握手,四次挥手,丢包检查等
  • 速度慢,没有大小限制,数据安全

UDP通信程序

创建发送端的DatagramSocket对象

DatagramSocket ds = new DatagramSocket();

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

数据打包发送(DatagramPacket)

DatagramPacket(byte[] buf, int length, InetAddress address, int port)

  • 用途:构造一个用于发送的数据报包。
  • 参数说明
    • buf:要发送的数据字节数组(内容)。
    • length:要发送的数据长度(≤ buf.length)。
    • address:目标主机的 IP 地址(InetAddress 对象)。
    • port:目标主机的端口号(0~65535)。

示例

// 创建发送端,端口为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对象

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

// 创建接收端
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 网络中,IP 地址不仅用于标识一台主机,还决定了网络设备如何处理这个数据包。 比如:

  • 普通单播地址(如 192.168.1.10) → 路由器会尝试把它送到那台特定主机。
  • 广播地址(255.255.255.255) → 路由器/交换机会把它发给本地所有主机
  • 组播地址(224.x.x.x) → 路由器会根据“谁加入了这个组”来决定是否转发和复制。

所以,地址本身是一种“指令”,告诉网络:“请按某种特殊方式处理这个包”。

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

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

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

  • A/B/C:用于单播
  • D 类(224.0.0.0 ~ 239.255.255.255):专门保留给组播
  • E 类:实验用
  • 这是 IANA(互联网号码分配机构)规定的。任何设备看到目标 IP 在这个范围,就知道这是组播包

发送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就能广播?

  • 在二进制中,255 = 11111111,全 1。
  • 当网络设备(如交换机、网卡)看到目标 IP 是 255.255.255.255,就会不检查 MAC 地址匹配,而是直接交给本机的所有应用程序监听该端口的套接字
  • 同时,交换机会把这个帧泛洪(flood) 到所有端口(除了接收端口),确保局域网内每台机器都收到。

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

Administrator
更新于 2026-06-01
上一篇 没有了
下一篇 MybatisPlus
评论交流

文档目录