Skip to content

概述 ​

Java 语言特点 ​

  1. 简单易学、丰富的类库
  2. 面向对象的抽象(封装,继承,多态)让程序耦合度更低,内聚性更高,扩展性更高
  3. Java 虚拟机
    • 编译与解释并存
      • 编译:源代码编译为字节码,热点代码字节码由 JIT 编译器编译为机器码
      • 解释:机器码由解释器逐行解释执行
    • 平台无关性
    • 安全性(沙箱安全模式)
  4. 可靠性
  5. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持)
  6. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语 言 不仅支持网络编程而且很方 便)

面向对象编程的思想 ​

面向对象的编程 ​

程序员会把数据和过程分别作为独立的部分来考虑,数据代表问题空间中的客体,程序代码则用于处理这些数据,这种思维方式直接站在 计算机的角度 去抽象问题和解决问题,被称为 面向过程 的编程思想。

面向对象 的编程思想则站在 现实世界的角度 去抽象和解决问题,它把数据和行为都看作对象的一部分,这样可以让程序员能以符合现实世界的思维方式来编写和组织程序。

对于面向对象编程来说,“万物皆可归类”,类即世界上某种事物的抽象,不同事物之间有不同的关系。对象是具体的事物个体。

面向对象的四大特征 ​

抽象

就是将一类对象的共同特征总结出来构造类的过程, 包括数据抽象和行为抽象两方面。抽象只关注事物有哪些属性和行为, 并不关注事物的细节是什么。

体现:抽象类 Abstract Class 、接口 Interface

封装

通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过必要的接口。封装就是隐藏一切可隐藏的东西, 只向外界提供最简单的编程接口。

体现:public / private 关键字

好处:提高代码的安全性、可维护性。

继承

体现:extends、implement

好处:有利于代码的复用。

多态

多态性是指允许不同子类型的对象对同一消息作出不同的响应。

体现:

  • 编译时的多态性(前绑定)体现:依赖于方法重载 Overload。
  • 运行时的多态性(后绑定):依赖于 继承 机制和方法 重写 @Override。父类引用指向子类对象 并调用方法,具体执行哪个子类的方法则取决于对象的实际类型。Java 虚拟机(JVM)会根据对象的实际类型来调用相应的方法。

好处:增加了代码的可移植性、健壮性。

JavaEE ​

JavaEE 是一套使用 Java 进行 企业级应用开发 的大家一致遵循的 13 个 规范工业标准。JavaEE 平台提供了一个基于组件的方法来加快设计、开发装配及部署企业应用程序

  1. JDBC(Java Database)数据库连接
  2. JNDI(Java Naming and Directory Interfaces)Java 的命名和目录接口
  3. EJB(Enterprise JavaBean)
  4. RMI(Remote Method Invoke)远程方法调用
  5. Java IDL(Interface Description Language)/CORBA(Common Objeer Architecture) 接口定义语 ; 言/公用对象请求代理程序体系结构
  6. JSP(Java ServerPages)
  7. Servlet
  8. XML(Extensible Markup Language)可扩展白标记语言
  9. JMS(Java Message Service)Java 消息服务
  10. JTA(Java Transaction API)Java 事务 API
  11. JTS(Java Transaction Service)Java 事务服务
  12. JavaMail
  13. JAF(JavaBean Activation Framework) JavaBean 激活框架

JavaSE 可以看作 JavaEE 的子集。Java EE 是在 Java SE 的基础上扩展的企业版,专为开发大型、复杂的企业级应用而设计。它提供了更多的功能和服务,以支持分布式计算、事务管理、安全性等方面的需求。Java EE 包含了用于开发 Web 服务、组件模型、管理和通信 API 的类库。

JDK JRE JVM ​

JDK ( Java Development Kit),它是功能齐全的 Java SDK,是 Java 开发工具包,包含 JRE 以及开发工具和基础类库,用于开发和编译 Java 程序。

  • Java 语言

  • 开发工具

    • javac 编译器,用于将 .java 文件编译为 .class 字节码文件。

    • java 启动器,用于启动 JVM 并执行编译后的 .class 文件。

    • javap 反编译器,用于反编译 .class 文件,查看其字节码内容。

    • jar 打包工具,用于创建、解压和管理 JAR 归档文件。

    • jdb 调试器,用于在命令行中查找和修复程序中的错误

    • javadoc 文档生成器,用于根据源代码中的注释生成 HTML 格式的 API 文档。

    • 虚拟机性能监控、故障处理工具

    • 其他实用工具(jarsigner、jdeps、jlink、jpackage)

  • 基础类库

  • JRE

JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机 (JVM),Java 类库,java 命令和其他的一些基础构件。但不包含开发工具。

  • 运行所必需的 java 命令
  • 核心类库
  • JVM

JVM(Java Virtual Machine)是 Java 虚拟机,负责将字节码(.class 文件)解释或编译为机器指令,实现跨平台运行。 其核心功能包括内存管理、垃圾回收等。

图解

关系图

总结:JDK 包含完整的 JRE 和开发工具。JRE 包含 JVM 和运行所需的类库。开发阶段,通过 JDK 的 javac 编译源码为字节码或构建 JAR 归档。运行阶段,用户只需 JRE 即可执行.jar 或.class 文件。跨平台,通过 JVM 屏蔽操作系统差异,确保“一次编译,到处运行”。

Java 和 C++的区别 ​

相同点:面向对象,支持封装、继承、多态

不同点:

  • Java 不通过指针直接访问内存,程序安全
  • C++ 支持多重继承,Java 只支持单继承,但可以实现多个接口
  • Java 有自动内存管理

数据类型 ​

  • 基本数据类型
    • 数值型
      • 整数类型(byte short int long)分别占 1 个、2 个、4 个、8 个字节
      • 浮点类型(float double)分别占 4 个、8 个字节
    • 字符型(char)占两个字节
    • 布尔型(boolean)理论上占用 1bit,实际上占用一个字节
  • 引用数据类型 (存储对象在内存中的位置)
    • 类(class)包含字符串(String)
    • 接口(interface)
    • 数组([])

浮点型 ​

浮点型为 Java 中的数据类型,主要有:单精度 float、双精度 double

浮点数的表现形式 ​

浮点型常量 Java 的实常数有两种表示形式:

  • 十进制数形式:由数字和小数点组成, 且必须有小数点, 如 0.123 , 123.0

  • 科学计数法形式:如: 123e3 或 123E3, 其中 e 或 E 之前必须有数字, 且 e 或 E 后面的指数必须为 整数(当然也包括负整数)。

E 是指数的意思,E 代表的英文是 exponent,E 表示 10 的多少次方的意思。

浮点数的占用字节数 ​

单精度浮点数(float)在机器内占 4 个字节、有效数字 8 位、表示范围:`-3.40E+38 ~ +3.40E+38

在 Java 语言当中,所有的浮点型字面值都默认当做 double 类型来处理,要想该字面值当做 float 类型来处理,需要在字面值后面添加 F/f, 或者强制装换为 float。

双精度浮点数在机内占 8 个字节、有效数字 16 位、表示范围:-1.79E+308 ~ +1.79E+308

浮点数的格式 ​

浮点数在计算机中遵循 IEEE 754 标准。它们以二进制形式存储,具体包括:

  • 符号位(sign bit):表示正负号。
  • 指数(exponent):决定数值的规模。
  • 尾数(mantissa 或 significand):表示精确的有效数字。

因此很多十进制的小数无法用有限的二进制精确表示,会产生近似值。

例如,0.1 在浮点数中会被表示为一个近似值:0.10000000000000000555。

在这里插入图片描述

通常不建议使用 浮点数(float 和 double) 来表示金钱或其他需要精确计算的数值,因为浮点数在存储和运算中存在 精度问题 和 舍入误差,这可能导致计算结果不准确。

BigDecimal 是 Java 提供的高精度数字处理类,适用于需要精确计算的场景(如金钱)。

BigDecimal 的构造推荐使用字符串参数而不是 double,否则会带入浮点数的精度问题。

字符型 ​

在 Java 中,char 类型用于表示单个字符,占用 2 个字节(16 位)的内存空间。它使用 Unicode 编码,可以表示世界上几乎所有的字符。

所以虽然 UTF-8 编码的中文字符是占用 3 个或者 4 个字节,但是对应的代码点仍然集中在 [0x4E00, 0x9FBB],所以 char 是能够存下在这个范围内的中文字符的。

但是对于超过 16bit 的 Unicode 字符集,也就是 Unicode 的扩展字符集,一个 char 是放不下的,需要两个 char 才能放下。

参考资料:Java 中 char 只有两个字节,如何储存 UTF-8 的中文呢,中文在 UTF-8 中占 3 个字节或者 4 个字节 - duo&duo - 博客园

运算符 ​

Java 提供了一套丰富的运算符来操纵变量。

算术运算符 ​

关系运算符 ​

  • ==
  • !=
  • >
  • <
  • >=
  • <=

**Java 中 == 和 equals() 有哪些区别 **

==:如果比较的是基本数据类型,则比较数值是否相等。如果比较的是引用数据类型,则比较引用的地址值是否相等。

equals():属于 Object 类的方法,无法比较基本数据类型。用来确定两个对象是否具有逻辑一致性。如果不重写则等同于 ==,比较引用的地址值是否相等。

位运算符 ​

  • & 按位与
  • | 按位或
  • ^ 按位异或
  • 〜 按位取反
  • << 按位左移
  • >> 按位右移
  • >>> 按位右移补零(无符号右移)

逻辑运算符 ​

  • & 逻辑与
  • | 逻辑或
  • ! 逻辑非

赋值运算符 ​

三元运算符 ​

instanceof 运算符 ​

数组 ​

数组本身就是引用类型,数组元素可以说任何数据类型

创建数组对象会再内存中开辟一整块连续空间,数组名引用的即为连续空间的首地址

数组长度一旦确定,就不可修改

通过下标调用指定位置的元素

内存解析

Image

Image

Image

面向对象 ​

类与对象 ​

类:类是对具有相同属性(字段或者成员变量)和行为(法)的一组对象的抽象,对一类 对象 的统称。

类的成员

属性、方法、构造器、代码块、内部类

Image

类的实例化

new 关键字用于创建一个对象的实例

类加载于方法区

实例加载于堆

权限修饰

对于类的权限修饰只可以用 public 和 default(缺省),

  • public 类可以在任意地方被访问。
  • default 类只可以被同一个包内部的类访问。

关键字修饰

  • final 表示该类是无法被任何其他类继承的,意味着此类在一个继承树中是一个叶子类,并且此类的设计已被认为很完美而不需要进行修改或扩展。

  • abstract 修饰类,会使得类变成抽象类,抽象类不能生成实例,但是可以作为对象变量声明的类型,也就是编译时类型。抽象类相当于类的半成品,需要子类继承并覆盖其中的方法。

类的加载 ​

随着类加载 类中各代码的执行顺序(由父及子,静态先行)

在程序入口(main 方法)中第一次调用类的静态结构(非构造器) ↓ 类的信息正式加载至方法区中 ↓ 执行类的初始化方法:<clinit>() 方法,该方法仅能由 Java 编译器生成并由 JVM 调用,由字节码指令所组成。它是由类静态成员的赋值语句以及 static 语句块合并产生的。 ↓ 静态结构(方法或者属性)开始执行 ↓ 结束

在程序入口(main 方法)中第一次调用类的构造器 ↓ 类的信息正式加载至方法区中 ↓ 执行类的初始化方法:<clinit>() 方法,该方法仅能由 Java 编译器生成并由 JVM 调用,由字节码指令所组成。它是由类静态成员的赋值语句以及 static 语句块合并产生的。 ↓ 非静态属性开始默认初始化 ↓ 显式初始化/非静态代码块开始执行 ↓ 构造器开始执行 ↓ 结束

在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的总是在子类之前被调用。也就是说,父类的 static 块优先级高于子类

成员变量 ​

局部变量成员变量
所处位置方法内的变量
形参(方法形参、构造器形参)
构造器或代码块内的变量
类成员之一:类的属性
初始值不具有默认初始化值,必须初始化具有默认初始化值
虚拟机内存加载到栈空间非 static 成员变量加载到堆空间
static 成员变量加载到方法区
权限修饰无private 表 s 属性为该类所有,其它类(包括子类)无法访问到
关键字修饰final 表示变量的值不可以改变,此时该变量可以被称为常量final 表示该属性一旦被初始化便不可改变
static 表示该属性为静态属性,可被多个对象共享,可以被类名调用

全局常量

由 final 和 static 共同修饰的变量

成员方法 ​

方法形参值传递机制

如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值。 如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值。

权限修饰

private 表示方法为该类所有,其它类(包括子类)无法访问到

关键字修饰

  • final 表示这种方法提供的功能已经满足当前要求,不需要进行扩展,并且也不允许任何从此类继承的类来重写这种方法,但是子类仍然可以使用这个方法。在声明类中,一个 final 方法只被实现一次。
  • static 表示静态方法,加载于方法区,可被类名调用。生命周期随着类的消失而消失。静态的方法只能访问静态的成员;
  • abstract 修饰方法会使得这个方法变成抽象方法,也就是只有声明,而没有实现,需要子类重写。

重载 ​

方法的重载指在 同一个类或父子类 中,声明了 方法名相同、参数个数不同或参数类型不同 的新方法。

方法的重载是一种编译时的静态绑定

重载后的方法与原方法可能有语义上的同源性,但程序中无实质关联。其目的是增加方法灵活性,处理不同类型参数

构造器方法可以声明多个,也类似于重载。

与 重写 对比

代码块 ​

静态代码块非静态代码块
内部可以有输出语句内部可以有输出语句
随着类的加载而执行,因此只执行一次随着对象的实例化而执行
作用: 初始化类信息作用:初始化对象的属性
只能调用静态属性和方法可以调用静态结构和非静态结构

构造器 ​

内部类 ​

如果一个事物的内部,还需要一个完整的结构进行描述,而且该完整结构只为外部事务服务,则最好使用内部类。

成员内部类

通过类名.this.方法名调用外部结构

局部内部类

存在于 方法内、代码块内、构造器内

局部内部类的方法调用外部类方法中的变量,要求该变量为 final 类型

原因:考虑到变量的生命周期,num 变量出了 method 方法即消亡。若加上 final 修饰,使其成为常量即可在内部类的独立文件中存活。

抽象性 ​

抽象类 ​

由 abstract 修饰的类

抽象类 无法实例化,但是有构造器,在子类实例化时调用

抽象类一定存在抽象方法(由 abstract 修饰的方法),也可以有具体的实现方法的。

接口 ​

由 interface 修饰

接口中无法定义构造器,无法实例化

实现接口使类得到了类似多重继承的效果,弥补 Java 单继承的局限性,接口与接口之间也可以多继承。

接口在 JDK7 及以前:只能定义全局常量,和抽象方法。在 JDK8 及以后:除了定义全局常量,和抽象方法。还可以定义静态方法、默认方法 (类似于抽象类)

语义:有时候,需要从几个类中抽取出共同的行为特征,可以用到接口

抽象类和接口的异同 ​

都无法实例化;都可以作为引用类型。

对比维度抽象类接口
方法抽象方法、具体方法(静态方法和实例方法)抽象方法,JDK1.8 以后有静态方法和默认方法
属性静态变量、实例变量、常量静态常量 public static final
构造器有构造方法,类实例化时调用。没有构造方法。
继承单继承(一个类只能继承一个抽象类)多实现(一个类可以实现多个接口),且接口与接口之间可以多继承
访问修饰符任意访问修饰符方法默认 public
语义表示 "事物是什么”的抽象表示”事物具什么有能力" 的抽象

封装性 ​

封装性的体现 ​

设计思想:该隐藏的隐藏该暴露的暴露

  • 类的属性私有化
  • 构造器私有化(类似于单例模式)
  • 缺省类,防止类在包外调用
  • 方法就是一种封装

权限修饰符 ​

修饰符当前类同包不同包的子类不同包
public能能能能
protected能能能不能
default能能不能不能
private能不能不能不能

Image

import 关键字 ​

Image

pakage 关键字 ​

Image

UML 类图 ​

Image

继承性 ​

一切类直接或间接继承于 java.lang 包中的 Object 类

只支持单继承和多层继承,不允许多重继承

多态性 ​

多态性定义 ​

所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的 方 法调用到底是哪个类中实现的方法,必须在由程序 运行期间 才能决定

多态性的体现 ​

  • 在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。

  • 一个父类引用指向不同的子类对象,同时可以调用其 重写父类后的方法,从而实现代码的通用性

  • 一个接口引用指向不同的实现类对象,同时可以调用其 重写接口后的方法,从而实现代码的通用性

  • 即 向上转型

重写 ​

重写基于继承,是 子类对父类 的允许访问的实例方法的实现过程进行重新编写。

方法的重写要遵循“两同两小一大”

  • “两同”:方法名相同、形参列表相同
  • “两小”:子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
  • “一大”:子类方法的访问权限应比父类方法的访问权限更大或相等。

重写是多态性的基础。

构造器不能重写,因为构造方法不能被继承,也没法重写,因为子类名和父类名也不相同。

改写静态方法不算重写,因为静态方法可以在没有创建类的实例的情况下被调用。

与 重载 对比

多态性的使用 ​

  • 应用了对象的多态性后,内存(堆)中实际上加载了子类的属性和方法,但父类对象不能调用子类特有的方法和属性。

  • 为了能调用子类特有的属性和方法:使用 向下转型

    java
    Person p = new Man();//向上转型(多态性体现)
    Man m = (Man)p;//向下转型

    Image

instanceof 操作符 ​

Image

枚举类 ​

枚举类 ​

Java 枚举是一个特殊的类,一般表示一组常量

Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割。

java
public enum MyEnum {
    Enum1, Enum2, Enum3;
}

javac 编译后,编译器会自动帮助我们生成一个与枚举相关的类。自动完善静态的 values()方法、静态的 valueOf()方法、私有构造器和各个枚举实例。

枚举类继承一个公共基本类 java.lang.Enum,Enum 是一个抽象类。

java
// 简单反编译MyEnum.class后的结果
Compiled from "MyEnum.java"
public final class com.tintin.basic.MyEnum extends java.lang.Enum<com.tintin.basic.MyEnum> {
  public static final com.tintin.basic.MyEnum Enum1;
  public static final com.tintin.basic.MyEnum Enum2;
  public static final com.tintin.basic.MyEnum Enum3;
  private static final com.tintin.basic.MyEnum[] $VALUES;
  public static com.tintin.basic.MyEnum[] values();
  public static com.tintin.basic.MyEnum valueOf(java.lang.String);
  private com.tintin.basic.MyEnum();
  static {};
}

设计模式 ​

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式免去我们自己再思考和摸索。设计模式就像是经典的棋谱,不同的棋局,我们用不同的棋谱,”套路”

根据完成的工作类型来划分,这种方式可分为 创建型模式、结构型模式和行为型模式 3 种。

Image

详细的设计模式列表

创建型模式 ​

单例模式 Singleton ​

实现方式

  1. 私有化构造器
  2. 声明静态的当前类对象
  3. 声明静态的返回当前类对象的方法

好处

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可 以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

分类

饿汉式:对象加载时间长,但线程安全

Image

懒汉式:延迟对象的创建

Image

结构性模式 ​

代理模式 Proxy ​

代理模式就是为其他对象提供一直代理以控制对这个对象的访问

java
interface Network {
    void browse();
}
// 被代理类
class Server implements Network {
    Server() {
    }

    public void browse() {
        System.out.println("真实服务器访问网络");
    }
}
// 代理类
class ProxyServer implements Network {
    public Network network;

    public ProxyServer(Network network) {
        this.network = network;
    }

    public void check() {
        System.out.println("联网前的检查工作");
    }

    public void browse() {
        this.check();
        this.network.browse();
    }
}
// 测试
    public static void main(String[] args) {
        Server server = new Server();
        ProxyServer proxyServer = new ProxyServer(server);
        proxyServer.browse();
    }

行为型模式 ​

模板方法设计模式 TemplateMethod ​

Image

修饰符 ​

static ​

用来修饰内部类、类方法和类变量

静态内部类:

它不需要外部类的实例就可以被创建和使用。它可以访问外部类的静态成员(静态变量和静态方法)以及它自己的成员。

静态变量:

static 关键字用来声明独立于对象的静态变量,无论一个类实例化多少对象,他的静态变量只有一份拷贝,静态变量也被称为类变量

静态方法:

static 关键字用来声明独立于对象的静态方法。静态方法不能使用类的非静态变量。

访问方式:

对类变量和方法的访问可以直接使用 class/obj.variablename 和 class/obj.methodname 的方式访问。

java
   private static int numInstances = 0;
   protected static int getCount() {
      return numInstances;
  }

final ​

final 变量:

被 final 修饰的变量成为常量,必须显示指定初始值,且不可更改。在编译阶段会存入常量池中。

被 final 修饰的成员变量为成员常量,通过和 static 一起来创建静态(类)常量,方法块中的常量称为局部常量。

如果修饰的是引用,那么表示引用不可变,但引用指向的内容可以改变。

java
  final int value = 10;
  // 下面是声明常量的实例
  public static final int BOXWIDTH = 6;
  static final String TITLE = "Manager";
//final类的声明
  public final class Test {
   // 类体
}

final 方法:

final 方法不可被重写,final 方法的主要目的是防止该方法的内容被修改,JVM 会尝试将其内联, 以提高运行效率。

final 类:

final 类不可被子类继承。

可以发现,假如这个类的所有方法都无需重写,代表无需子类继承,可以用 final 修饰类。例如 String 类。

假如类中不需要成员变量,而类的方法已然确定,那么可以将构造器私有,不允许外界创建对象,直接通过 static 关键字修饰所有方法,通过类来调用静态方法或获取静态属性。例如有关数学计算的 Math 类。

abstract ​

抽象类:

抽象类不能用来实例化对象,声明抽象类的唯一目的是为了将来对该类进行扩充

一个类不能同时被 abstract 和 final 修饰,如果一个类包含抽象方法,该类必须为抽象类

抽象类可以包含抽象方法和非抽象方法

java
abstract class Caravan{
   private double price;
   private String model;
   private String year;
   public abstract void goFast(); //抽象方法
   public abstract void changeColor();
}

抽象方法

java
public abstract class SuperClass{
    abstract void m(); //抽象方法
}
 
class SubClass extends SuperClass{
     //实现抽象方法
      void m(){
          .........
      }
}

synchronized ​

synchronized 关键字声明的方法同一时间只能被一个线程访问,可用于四个访问修饰符

java
public synchronized void showDetails(){
.......
}

transient ​

transient 只能修饰变量,它可以阻止实例中那些用此关键字修饰的变量序列化;

当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复,而是会被置成变量类型的默认值。

序列化对象包含被 transient 修饰的实例变量时,java 虚拟机会跳过该变量

该修饰符包含在定义变量的语句中,用来预处理类和变量的数据类型

java
public transient int limit = 55;   // 不会持久化
public int b; // 持久化

序列化和反序列化 ​

使用场景:通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

  • 序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
  • 反序列化:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程

img

JDK 自带的序列化,只需实现 java.io.Serializable 接口即可。

java
public class SerializableDemo implements Serializable {
    private static final long serialVersionUID = 1905122075436251207L;
}

序列化号 serialVersionUID 属于版本控制的作用。反序列化时,会检查其是否与当前类的 serialVersionUID 一致,不一致则抛出 InvalidClassException 异常。

serialVersionUID 被 static 修饰,属于类而不属于对象的一部分,但被序列化机制本身用作一个特殊的“指纹”或“版本号”,因此也会被序列化

volatile ​

  • volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值,而且当成员变量发生变化时,会强制线程将变化的值回写到共享内存,这样在任何时可,俩个不同的线程总是看到某个成员变量的同一个值

Java 的内存模型

image-20260615230642390

image-20260615230645855

使用 volatile 后

  • 使 用 volatile 可以禁 止 JVM 的指令重排,保证在多线程环境下也能正常运 行。
java
public class MyRunnable implements Runnable
{
    private volatile boolean active;
    public void run()
    {
        active = true;
        while (active) // 第一行
        {
            // 代码
        }
    }
    public void stop()
    {
        active = false; // 第二行
    }
}

通常情况下,在一个线程调用 run()方法(在 runnable 开启的线程),在另一个线程调用 stop()方法。如果第一行缓冲区的 active 值被使用,在第二行的 active 值为 false 时循环停止

异常处理 ​

异常类型 ​

image-20260821175419529

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。 Throwable 类有两个重要的子类 Exception (异常)和 Error (错误)。

  • Exception : 程序本身可以处理的异常,可以通过 catch 来进行捕获。
    • 运行时异常:Exception 中除 RuntimeException 及其子类之外的异常。
    • 非运行时异常:RuntimeException 类及其子类异常。
  • Error : Error 属于 程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。这些异常发生 时,Java 虚拟机(JVM)一 般会选择线程终止。

受检异常与非受检异常 ​

Java 的异常又可分为受检异常(checked exceptions)和非受检异常(unchecked exceptions)。

  • 受检查异常:Java 代码在编译过程中,如果受检查异常没有被 catch / throw 处理的话,就没办法通过编译 。
    • Exception 中的非运行时异常
  • 不受检查异常:Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
    • Exception 中的运行时异常
    • Error

异常处理机制 ​

异常对象的生成与抛出

  • 虚拟机自动生成并抛出

    程序运行过程中,虚拟机检测到程序发生了问题,如果在当前代码中没有找到相应的处理程序,就会在后台自动创建一个对应异常类的实例对象并抛出一一自动抛出

  • 开发人员手动创建并抛出

    java
    throw new ClassCastException();
  • 一旦抛出异常对象后,其后代码不再执行

异常的捕获与处理 try-catch-finally ​

  • try 是必要的,负责监听代码运行时发生的异常。
  • catch 是可选的,可声明多个,负责对指定类型的异常进行处理。
  • finally 是可选的,最多声明一个,属于不管执行情况如何,都会走的逻辑块。

具有 try-catch 形式、try-finally 形式、try-catch-finally 形式。

java
        try {
			//用于捕获异常。
			InputStream inputStream = new FileInputStream("");

        } catch (RuntimeException e) {
            //用于处理 try 捕获到的异常。
            e.getMessage();//返回异常发生时的简要描述
            e.toString();//返回异常发生时的详细信息
            e.printStackTrace();//在控制台上打印 Throwable 对象封装的异常信息
        } catch (IOException e) {

        }
        finally {
			//无论是否捕获或处理异常,finally 块里的语句都会被执行。当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。
        }

异常的声明 throws ​

如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用 throws 子句来声明抛出异常。

关键字 throws 运用于方法声明之上, 用于表示当前方法不处理异常, 而是提醒该方法的调用者来处理异常或继续向上抛出异常。

java
public class ThrowsDemo {

    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.tintin.custom.People");
        System.out.println(aClass);
    }
}

一旦方法体执行时,出现异常,仍会生成一个异常类对象,后续代码不再执行,并将异常传递给方法的调用者

自定义异常 ​

创建自定义异常 ​

  1. 继承于现有异常结构 RuntimeException,Exception
  2. 提供全局常量 serialVersionUid
  3. 提供重载的构造器
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GuliException extends RuntimeException{
    static final long serialVersionUID = -7034897190745766939L;

    @ApiModelProperty(value = "状态码")
    private Integer code;

    private String msg;
}

自定义异常的应用场景 ​

  • 系统中有些错误是符合 Java 的语法,但不符合业务逻辑。
    • 例如业务规则(余额必须大于等于转账金额)被违反了,Java 自带的 ArithmeticException(数学异常)描述不了这种情况,需要自定义异常(比如 InsufficientBalanceException)来表达。
  • 通常在表现层统一对系统的持久层和中间逻辑层的异常进行捕获处理,写一堆 try-catch,代码会变得极其臃肿。
    • 持久层 (DAO/Repository),抛出(抛出原生 SQL 异常,如 DataAccessException)
    • 中间逻辑层 (Service),抛出(抛出上文中说的自定义业务异常,如 BalanceException)
    • 表现层 (Controller) :利用 Spring 的 @ControllerAdvice(全局异常拦截器),统一拦截所有 Service 层抛上来的异常。

多线程 ​

程序、进程、线程 ​

程序

程序是含有指令和数据的 文 件,被存储在磁盘或其他的数据存储设备中,也就是说程序是 静态 的代码。

进程

进程是程序的 一 次执 行 过程,是系统运 行 程序的基本单位,因此进程是 动态 的。系统运 行一 个程序即是 一 个进程从创建,运 行 到消亡的过程。简单来说,一 个进程就是 一 个执 行 中的程序,它在 计算机中 一 个指令接着 一 个指令地执 行 着,同时,每个进程还占有某些系统资源如 CPU 时间, 内存空间,文 件,输 入 输出设备的使 用 权等等。换句话说,当程序在执 行 时,将会被操作系统载 入 内存中。

线程

线程是进程划分成的更 小 的运 行 单位。线程和进程最 大 的不同在于基本上各进程是独 立 的,而 各线程则不 一 定,因为同 一 进程中的线程极有可能会相互影响。从另 一 ⻆ 度来说,进程属于操作系统的范畴,主要是同 一 段时间内,可以同时执 行一 个以上的程序,而 线程则是在同 一 程序内 几 乎同时执 行一 个以上的程序段。

线程与进程相似,但线程是 一 个 比 进程更 小 的执 行 单位。一 个进程在其执 行 的过程中可以产 生 多个线程。与进程不同的是 同进程的多个线程共享同 一 块内存空间和 一 组系统资源,所以系统在产 生一 个线程,或是在各个线程之间作切换 工 作时,负担要 比 进程 小 得多,也正因为如此,线程也被称为轻量级进程。

线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(gc)

由于共享相同的内存单元(堆和方法区),它们从同一个堆中分配对象,访问相同的变量和对象,因此线程间通信更简便高效。但多个线程操作共享的系统资源会带来安全隐患(线程安全问题)

image-20220413142713519

Java 线程基本状态 ​

image-20220412223433015

线程创建之后它将处于 NEW(新建) 状态。

调 用 start() 方 法后开始运 行,线程这时候处于 READY(可运 行) 状态。可运 行 状态的线程获得了 cpu 时间 片(timeslice)后就处于 RUNNING(运 行) 状态。两个状态一起又称 RUNNABLE 状态。

当线程执 行 wait() 方 法之后,线程进 入 WAITING(等待)状态。进 入 等待状态的线程需要依靠 其他线程的通知才能够返回到运 行 状态,

而 TIME_WAITING(超时等待) 状态 相当于在等待状态 的基础上增加了超时限制,比 如通过 sleep(long millis) 方 法或 wait(long millis) 方 法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状 态。

当线程调 用 同步 方 法时,在没有获取到锁的情况下,线程将会进 入 到 BLOCKED(阻塞) 状态。

线程在执 行 Runnable 的 run() 方 法之后将会进 入 到 TERMINATED(终 止) 状态

为什么要使 用 多线程 ​

  • 从计算机底层来说: 线程可以 比 作是轻量级的进程,是程序执 行 的最 小 单位, 线程间的切换 和调 度的成本远远 小 于进程。另外,多核 CPU 时代意味着多个线程可以同时运 行,这减少 了线程上下 文 切换的开销。
  • 从当代互联 网 发展趋势来说: 现在的系统动不动就要求百万级甚 至 千万级的并发量,而 多线 程并发编程正是开发 高 并发系统的基础,利 用 好多线程机制可以 大大 提 高 系统整体的并发能 力 以及性能。

多线程可能带来什么问题 ​

并发编程的 目 的就是为了能提 高 程序的执 行 效率提 高 程序运 行 速度,但是并发编程并不总是能提 高 程序运 行 速度的,而 且并发编程可能会遇到很多问题,比 如:内存泄漏、上下 文 切换、死锁 。

上下 文 切换 ​

当前任务在执 行 完 CPU 时间 片 切换到另 一 个任务之前会先保存 自己 的状态,以 便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是 一 次 上下 文 切换。上下 文 切换对系统来说意味着消耗 大 量的 CPU 时间,事实上,可能是操作系统中时间消耗最 大 的操作。 Linux 相 比 与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有 一 项就是,其上下 文 切换和模式切换的时间消耗 非 常少

线程安全问题 ​

多线程环境中 , 且存在数据共享 , 一个线程访问的共享 数据被其他线程修改了, 那么就发生了线程安全问题 , 整个访问过程中 , 没有共享的数据被其他线程修改了就是线程安全的。

解决线程安全问题

  • 使用同步机制, 使得在同一时间只能有一个线程修改共享数据

  • 消除共享数据, 即多个线程数据不共享或者共享的数据不被做修改 如果使用成员变量, 对成员变量不进行修改

死锁 ​

多个线程同时被阻塞,它们中的 一 个或者全部都在等待某个资 源被释放。由于线程被 无 限期地阻塞,因此程序不可能正常终 止。 例如:线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对 方 的资源,所以这两 个线程就会互相等待 而 进 入 死锁状态。

死锁必须具备以下四个条件

  • 互斥条件:该资源任意 一 个时刻只由 一 个线程占 用
  • 请求与保持条件:进程因请求资源 而 阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已获得的资源在末使 用 完之前不能被其他线程强 行 剥夺,只能自行释放
  • 循环等待条件:若干进程,循环等待所需资源

避免线程死锁

  • 互斥条件:来自线程的同步机制,无法改变
  • 请求与保持条件:一次性获取所有资源,不现实
  • 不剥夺条件:如果申请不到,可以主动释放它占有的资源。
  • 循环等待条件:按序申请资源来预防,必须先申请资源 1 才能申请资源 2

内存泄漏 ​

ThreadLocalMap 中使 用 的 key 为 ThreadLocal 的弱引 用, 而 value 是强引 用。所以,如果 ThreadLocal 没有被外部强引 用 的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会 被清理掉。这样 一 来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措 施的话,value 永远 无 法被 GC 回收,这个时候就可能会产 生 内存泄露。ThreadLocalMap 实现 中已经考虑了这种情况,在调 用 set() 、 get() 、 remove() 方 法的时候,会清理掉 key 为 null 的记录。使 用 完 ThreadLocal 方 法后 最好 手 动调 用 remove() 方 法

线程同步机制 ​

线程同步能够解决线程安全问题

synchronized 关键字 ​

同步代码块

指定加锁对象,对给定对象/类加锁。 synchronized(this.object) 表示进 入 同步代码 库前要获得给定对象的锁。 synchronized(this.class) 表示进 入 同步代码前要获得 当前 class 的锁

同步方法

  • 修饰实例方法 作 用 于当前对象实例加锁,进 入 同步代码前要获得 当前对象实例的锁
  • 修饰静态 方 法: 也就是给当前类加锁,会作 用 于类的所有对象实例 ,进 入 同步代码前要获得 当 前 class 的锁。

双重检验锁方式实现 懒汉式单例模式的原理

java
public class Singleton {
    //使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
	private volatile static Singleton uniqueInstance;
    
	private Singleton() {
	}
    
    public static Singleton getUniqueInstance() {
        //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

synchronized 早期实现

Java 早期版本中, synchronized 属于 重量级锁,效率低下。

Java 的线程是映 射到操作系统的原 生 线程之上的。如果要挂起或者唤醒 一 个线程,都需要操作系统帮忙完成,而 操作系统实现线程之间的切换时需要从 用 户态转换到内核态,这个状态之间的转换需要相对 比 较 ⻓ 的时间,时间成本相对较 高。

synchronized 的虚拟机实现

现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引 入 了 大 量的优化,如 自 旋锁、适 应性 自 旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销

  • synchronized 同步语句块的实现使 用 的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位 置。

    当执 行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

    在执 行 monitorenter 时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取 后将锁计数器设为 1 也就是加 1。 在执 行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前 线程就要阻塞等待,直到锁被另外 一 个线程释放为 止

  • synchronized 修饰的 方 法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该 方 法是 一 个同步 方 法。

不过两者的本质都是对对象监视器 monitor 的获取。

Lock 接口 ​

JDK1.5 以后,Java 提供了 Lock 同步锁,相对与需要 JVM 隐式获取和释放锁 Synchronized 同步锁,Lock 同步锁需要显式获取和释放锁,这就为获取和释放锁提供了更多的灵活性。

由于 Lock 锁也会在阻塞的时候被挂起,因此它依然属于 悲观锁。

好处

只要不涉及到复杂用法,一般采用的是 Java 的 synchronized 机制

不过,Lock 可以提供一些 synchronized 不支持的机制

  • 非阻塞的获取锁:尝试获取锁,如果能获取马上获取,不能获取马上返回,不会阻塞
  • 中断获取锁:当获取锁的线程被中断时,抛出异常,锁被释放
  • 超时获取锁:为尝试获取锁设定超时时间

相应 API

java
void lock()//普通的获取锁
    
void lockInterruptibly() throws InterruptedException//可中断的获取锁,锁的获取中可以中断线程
    //获得锁,可中断。举个例子,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
    
boolean tryLock()//非阻塞获取锁 
    
boolean tryLock(long time, TimeUnit unit)//超时获取锁
    //如果锁定可用,则此方法立即返回值true。如果锁不可用,则当前线程将被禁用以进行线程调度,并且在发生以下三种情况之一之前处于休眠状态:
//当前线程获取锁。
//其他一些线程中断当前线程。
//等待时间过去了,返回false
    
void unlock()//释放锁

ThreadLocal ​

通常情况下,我们创建的变量是可以被任何 一 个线程访问并修改的。如果想实现每 一 个线程都有 自己 的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。 ThreadLocal 类主要解决的就是让每个线程绑定 自己 的值。

java
public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

Thread 类中有 一 个 threadLocals 和 一 个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量

默认情况下这两个变量都 是 null,只有当前线程调 用 ThreadLocal 类的 set 或 get 方 法时才创建它们,实际上调 用 这两 个 方 法的时候,我们调 用 的是 ThreadLocalMap 类对应的 get() 、 set() 方 法。如下:

java
// ThreadLocal.class
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

同时 ThreadLocalMap 是 ThreadLocal 的静态内部类。因此,ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,帮助 Thread 维护了 ThreadLocalMap,并传递了变量值。

比 如我们在同 一 个线程中声明了两个 ThreadLocal 对象的话,会使 用 Thread 内部都是使 用 仅有 那个 ThreadLocalMap 存放数据的, ThreadLocalMap 的 key 就是 ThreadLocal 对象,value 就是 ThreadLocal 对象调 用 set 方 法设置的值。

创建线程的方式 ​

继承 Thread 类 ​

  1. 定义 Thread 类的子类,并重写该类的 run 方法,该 run 方法的方法体就代表了线程要完成的任务。因此把 run()方法称为执行体。
  2. 创建 Thread 子类的实例,即创建了线程对象。
  3. 调用线程对象的 start()方法来启动该线程。

调 用 start() 方 法 方 可启动线程并使线程进 入 就绪状态,直接执 行 run() 方 法的话不会 以多线程的 方 式执 行,而是在 main 线程中执行了 run()方法内的代码

Runnable 接口 ​

  1. 定义 runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体同样是该线程的线程执行体。
  2. 创建 Runnable 实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
  3. 调用线程对象的 start()方法来启动该线程。

Callable 和 Future ​

  1. 创建 Callable 接口的实现类,并实现 call()方法,该 call()方法将作为线程执行体,并且有返回值
  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call()方法的返回值。
  3. 使用 FutureTask 对象 作为 Thread 对象的 target 创建并启动新线程。
  4. 调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值

Callable 方式与 Runnable 方式对比

Callable 功能更强大些

  • 可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助 FutureTask 类,比如获取返回结果

Future 接口与 FutureTask 实现类

  • 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutureTask 是 Futrue 接口的唯一的实现类
  • FutureTask 同时实现了 Runnable,.Future 接口。它既可以作为
  • Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值

线程池 ​

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。

线程池提供了 一 种限制和管理资源(包括执 行一 个任务)。 每个线程池还维护 一 些基本统计信 息,例如已完成任务的数量。

execute()方 法

用 于提交不需要返回值的任务,所以 无 法判断任务是否被线程池执 行 成功与 否

submit()方 法

用 于提交需要返回值的任务。线程池会返回 一 个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执 行 成功

好处

  • 降低资源消耗
  • 提高响应速度
  • 提高线程可管理性

Java 常用类 ​

String 类 ​

字符串的内存结构 ​

字符串常量存储在常量池,实现线程共享

字符串非常量对象存储在堆中

image-20220413110958435

常用方法 ​

Image

Image

Image

String 与 char 型数组的转换:toCharArray()方法

String 与 byte 型数组的转换:getBytes()方法

String、StringBuffer 和 StringBuilder 的异同 ​

String 是只读字符串,它并不是基本数据类型,而是引用类型。底层是使用 final 关键字修饰的 char [] ,不可变,大量操作时效率低下。

每一次 + 操作,都是在堆上 new 了一个跟原字符串相同的 StringBuilder 对象,再调用 append 方法拼接新的字符串。

StringBuffer 与 StringBuilder 都继承了 AbstractStringBulder 类,AbstractStringBulder 继承了 CharSequence 类,字符串操作效率比 String 更高。底层是不使用 final 关键字修饰 char [] 可变,如果容量不足,内部数组会进行扩容。

  • StringBuffer 的方法大多都加了 synchronized 关键字,因此 StringBuffer 是线程安全的。
  • Stringbuilder 在 jdk1.5 中引进,是非线程安全的,因此 Stringbuilder 比 StringBuffer 效率更高。

Image

I/O 流 ​

I/O 流概述 ​

IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。

流(Stream),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。

当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。

I/O 流的分类 ​

按照流的流向划分

  • 输入流:数据从文件/网络/内存/管道流进程序。
  • 输出流:数据从程序流出到文件/网络/内存/管道。

按照操作单元划分

  • 字节流:传输原始字节,什么都能传(图片、视频、音频、压缩包、网络二进制数据)。
  • 字符流:传输字符,专门处理文本(文本、配置文件、日志),底层还是字节+编码。

Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而 且彼此之间存在非常紧密的联系, Java I/0 流的 40 多个类都是从 4 个抽象类基类 中派生出来的。

分类及场景字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
访问字符串StringReaderStringWriter
缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
转换流InputStreamReaderOutputStreamWriter
对象流ObjectInputStreamObjectOutputStream
FilterInputStreamFilterOutputStreamFilterReaderFilterWriter
打印流PrintStreamPrintWriter
推回输入流PushbackInputStreamPushbackReader
基本数据流DataInputStreamDataOutputStream

I/O 流实战 ​

常用类

  • RandomAccessFile:可读可写,支持文件指针 seek(),适合断点续传。
  • File:表示文件路径,不是流。
  • Path / Files:NIO.2 工具类,现代文件操作首选。
  • Scanner:文本解析工具,不是流本身。

字节流复制文件,带缓冲

BufferedInputStream
  └─ FileInputStream // 节点流,读文件字节
java
public static void main(String[] args) throws URISyntaxException {
        Path classPath = Paths.get(FileCopyDemo.class.getResource("/").toURI());
        File srcFile = classPath.resolve("static").resolve("src.png").toFile();
        File destFile = classPath.resolve("static").resolve("dest.png").toFile();
        try (InputStream in = new BufferedInputStream(new FileInputStream(srcFile));
             OutputStream out = new BufferedOutputStream(new FileOutputStream(destFile))) {

            byte[] buf = new byte[8192];
            int len;
            while ((len = in.read(buf)) != -1) {
                out.write(buf, 0, len);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

字符流按 UTF-8 读写文本

BufferedReader
  └─ InputStreamReader  // 字节 -> 字符,指定 UTF-8
       └─ FileInputStream // 节点流,读文件字节
java
public static void main(String[] args) throws IOException, URISyntaxException {
        Path classPath = Paths.get(TextDemo.class.getResource("/").toURI());
        File srcFile = classPath.resolve("static").resolve("in.txt").toFile();
        File destFile = classPath.resolve("static").resolve("out.txt").toFile();
        // 读
        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(new FileInputStream(srcFile), StandardCharsets.UTF_8))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        }

        // 写
        try (BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(destFile), StandardCharsets.UTF_8))) {
            bw.write("你好,世界");
            bw.newLine();
            bw.write("第二行");
        }
    }

对象序列化

java
@Data
@AllArgsConstructor
public class User implements Serializable {
    private String name;
    private int age;
}
java
public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
            oos.writeObject(new User("Tom", 18));
        }

        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
            User u = (User) ois.readObject();
            System.out.println(u.getName() + " " + u.getAge());
        }
    }

基本数据存取

java
public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"))) {
            dos.writeInt(100);
            dos.writeDouble(3.14);
            dos.writeUTF("你好");
        }

        try (DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"))) {
            int i = dis.readInt();
            double d = dis.readDouble();
            String s = dis.readUTF();
            System.out.println(i + " " + d + " " + s);
        }
    }

Java 7+ / Java 11+ 后,简单文件读写可以用 Files:

java
public class FilesDemo {
    public static void main(String[] args) throws IOException {
        String text = Files.readString(Path.of("in.txt"), StandardCharsets.UTF_8);
        Files.writeString(Path.of("out.txt"), text, StandardCharsets.UTF_8);

        Files.copy(Path.of("src.jpg"), Path.of("dest.jpg"),
                StandardCopyOption.REPLACE_EXISTING);
    }
}

BIO, NIO, AIO ​

BIO (Blocking I/O)

同步阻塞 I/O 模式,数据的读取写 入 必须阻塞在 一 个线程内等待其完 成。在活动连接数不是特别 高(小 于单机 1000)的情况下,这种模型是 比 ᫾ 不错的,可以 让每 一 个连接专注于 自己 的 I/O 并且编程模型简单,也不 用 过多考虑系统的过载、限流等问 题。线程池本身就是 一 个天然的漏 斗,可以缓冲 一 些系统处理不了的连接或请求。但是,当 面 对 十 万甚 至 百万级连接的时候,传统的 BIO 模型是 无 能为 力 的。因此,我们需要 一 种更 高 效的 I/O 处理模型来应对更 高 的并发量。

NIO (Non-blocking/New I/O)

NIO 是 一 种 同步非阻塞 的 I/O 模型,在 Java 1.4 中引 入 了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以 理解为 Non-blocking,不单纯是 New。它 支 持 面 向缓冲的,基于通道 的 I/O 操作 方 法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现, 两种通道都支持阻塞和非阻塞两种模式。 阻塞模式使 用 就像传统中的 支 持 一 样,比 ᫾ 简单,但是性能和可靠性都不好;非 阻塞模式正 好与之相反。对于低负载、低并发的应 用 程序,可以使 用 同步阻塞 I/O 来提升开发速率和更 好的维护性;对于 高 负载、高 并发的(网 络)应 用,应使 用 NIO 的 非 阻塞模式来开发

AIO (Asynchronous I/O):

AIO 也就是 NIO 2。在 Java 7 中引 入 了 NIO 的改进版 NIO 2, 它 是 异步非阻塞 的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应 用 操作之后会直 接返回,不会堵塞在那 里,当后台处理完成,操作系统会通知相应的线程进 行 后续的操作。 AIO 是异步 IO 的缩写,虽然 NIO 在 网 络操作中,提供了 非 阻塞的 方 法,但是 NIO 的 IO 行 为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就 由这个线程 自行 进 行 IO 操作,IO 操作本身是同步的。查阅 网 上相关资料,我发现就 目 前来 说 AIO 的应 用 还不是很 广 泛,Netty 之前也尝试使 用 过 AIO,不过 又 放弃了。

Java 集合 ​

分类 ​

  • Collection 接口:单列数据
    • List:元素有序、可重复的集合
    • Set:元素无序,不可重复的集合
  • Map 接口:双列数据,具有 key-value 映射关系的集合

Collection ​

Image

常用方法 ​

java
Arrays.aslist(元素)、new ArrayList() 创建ArrayList类型collection
size() 返回collection元素个数
add()、remove()增删查改
removeAll(Collection) 从collection中移除collection1的元素(差集)
retainAll(Collection) 改变collection为collection1和collection的交集
contains(Object )调用Object对象的equals()方法 与 collection内的对象进行判断
containsAll(Collection) 判断collection1是否为collection的子集
equals(Collection) 判断集合是否相同,需综合考虑是否无序和有序集合
hashCode() 返回collection的哈希值
toArray() 根据collection返回一个数组
Arrays.aslist(包装类[]) 根据包装类数组[]返回一个list

遍历方法

java
    @Test
    public void test() {
        Collection list = new ArrayList();
        //Iterator(迭代器)
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            if (next.equals("")) {
                iterator.remove();
                //执行一次remove()后
                //游标移到被删元素的下标
                //lastRet回到容器的顶部(-1)
				//此时必须再执行一次next(),才能再执行一次remove(),否则回出现异常
            }
        }
        //forEach()
        list.forEach(System.out::println);
        //增强for循环
        for (Object o : list) {
            System.out.println(o);
        }
    }

List ​

ArrayLIstLinkedListVector
线程安全不安全不安全安全
底层数据结构Object []双向链表(元素为 Node 对象)
jdk1.6 循环 jdk1.7 不循环
Object []
i 位置添加元素O(n-i)o(i)
i 位置删除元素O(n-i)O(i)
原因元素都要执 行 向后位/向前移 一 位的操作需要先移动到指定位置再插 入。
快速随机访问通过元素的序号快速获取不 支 持
内存空间占 用数组结尾会预留 一 定的容量空 间每个元素存放前驱和后继,消耗大
扩容机制添加元素时,创建 Obejct [10],扩容 1.5 倍无创建对象时,创建 Obejct [10],扩容 2 倍

List 接口方法

Image

Set ​

无序性(并非随机):不是指添加元素时的顺序而是指存储的数据并非以索引的顺序依次添加,而是根据存储数据的 hash 值添加

不可重复性:是根据 equal() 和 hashCode() 判断, 即在新元素和旧元素 hash 值相同的情况下,当新元素.equal(旧元素)返回值为 false 才可以添加

HashSetLinkedHashSetTreeSet
线程安全不安全不安全
底层结构HashMap <>HashMap <>,维护了两个引用,记录前一个数据和后一个数据红黑树结构(自平衡的排序二叉树)
特点可以存 储 null 值可以存 储 null 值,频繁遍历效率高,能够按照添加的顺序遍历TreeSet 自动进行排序,排序的 方 式有 自 然排序和定制排 序。
定义元素是否相同的标准不再是 equals()的返回值;
而是 comparaTo()或者 compare()是否返回了 0

Image

Set 添加元素的过程

以 HashSet 为例

我们向 HashSet 中添加元素 a 首先调用元素 a 所在类 hashCode() 方法,计算元素 a 的哈希值, 也称为散列码(int 整数)

此哈希值接若通过某种算法计算出在 HashSet 底层数组中的存放位置(即为:素引位置)

该算法可以是扰动函数 hash 方法得到 hash 和取模运算(n-1)&hash

使 用 hash 方 法也就是扰动函数是为了防 止一 些实现 比 较差的 hashCode() 方 法 换句话说使 用 扰动函数之后可以减少碰撞

判断数组此位置上是已经有元素

  • 如果此位置上没有其他元素,则元素和添加成功。
  • 如果此位置上有其他元素 b(或以链表形式存在的多个元素),则比较元素 a 和与元素 b 的 hash 值:
    • 如果 hash 值不相同,则元素 a 添加成功。
    • 如果 hash 值相同,进而需调用元素和所在类的 equlas()方法:
      • equals() 返回 true, 元素和添加失败
      • equals() 返回 false, 则元素 a 添加成功,

这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

为什么重写 equals 时必须重写 hashCode 方 法?

由于 Object 的 hashcode 方法是本地方法,也就是用 c 语 言 或 c++ 实现的,默认行为是对堆上的对象产 生 独特值。如果没有重写 hashCode() ,则两个对象即使内容相同,生成的哈希值也不同,此时都会被添加进 HashSet,导致元素重复。

为什么两个对象有相同的 hashcode 值,它们也不 一 定是相等的?

因为 hashCode() 所使 用 的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算 法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同 的 hashCode)。

但是原则上:两个对象 equals() 比较返回 true,hashcode() 也返回一样的值

Map ​

Image

HashtableHashMapLinkedHashMap
底层结构数组+链表
数组+链表+红 黒 树
数组+链表
数组+链表+红 黒 树
数组+双向链表
数组+双向链表+红 黒 树
线程安全是否
效率低高
对 Null key 和 Null value 的 支 持不允许有 null 键和 null 值null 作为 键只能有 一 个,null 作为值可以有多个
初始容量1116 (既定容量时 2 的幂次 方大小)
扩充容量2n+12n
底层数据结构当链表 ⻓ 度 大 于阈值,链表转换成红 黒 树当链表 ⻓ 度 大 于阈值,链表转换成红 黒 树前会判断,如果当前数组的 ⻓ 度 小 于 64 先进 行 数组扩容

HashMap 和 HashSet 的区别

HashSet 底层就是基于 HashMap 实现的。 (HashSet 的源码非常非常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet 自己 不得不实现之外,其他 方 法都是直接调 用 HashMap 中的 方 法。

HashMap 使用键 (Key)计算 hashcode

HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals() 方 法 用 来判断对象的相等性

hashcode() 和 equals() 的联系

equal() 源于 java.lang.object,用于验证两个对象的逻辑相等性。

  • 若不重写则等同于 ==,比较两个对象的引用是否相同,即比较对象的内存地址。
  • 若重写,则看方法具体的逻辑。但主要遵循以下原则:
    • 自反性:x.equals(x) 返回 true。
    • 对称性:若 x.equals(y) 为 true,则 y.equals(x) 亦为 true。
    • 传递性:若 x.equals(y) 为 true 且 y.equals(z) 也为 true,则 x.equals(z) 亦为 true。
    • 一致性:保存 x 和 y 不变,x.equals(y) 多次调用都返回一致的结果。
    • 非空性:x.equals(null) 永远返回 false。

hashCode() 源于 java.lang.object,用于获取对象的唯一整数(散列码)。Object 中的 hashcode 方法是本地方法,其他类可以重写 hashcode 方法。

  • 若不重写,hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,默认行为是对堆上的对象产生独特值。
  • 若重写,需遵循以下原则:
    • 一致性:如果两个对象通过 equals() 比较相等,那么它们的 hashCode() 必须返回相同的值,否则在不可重复的集合中可能会出现两个相同的对象。
    • 非必要性:如果两个对象的 hashCode() 相同,它们不一定通过 equals() 比较相等(哈希碰撞是允许的)。
    • 降低哈希冲突:不相等的对象尽量返回不同的哈希值以减少冲突。

HashMap 的底层实现

数组和链表结合在 一 起使 用 也就是链表散列。HashMap 通过 key 的 hashCode 经过 扰动函数 处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这 里 的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存 入 的元素的 hash 值是否相同以及 key 是否 equal() 相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突

使 用 扰动函数之后可以减少哈希碰撞。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建 一 个链表数组,数组中每 一 格就是 一 个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

ConcurrentHashMap

多线程下使 用 HashMap 还是会存在其 他问题 比 如数据丢失。并发环境下推荐使 用 ConcurrentHashMap 。

ConcurrentHashMapHashtable
底层数据结构jdk1.7 分段的数组+链表
jdk1.8Node 数组+链表+红 黒 树
数组+链表
数组+链表+红 黒 树
如何实现线程安全jdk1.7 对整个桶数组进 行 了分割分段,每 一 把锁只锁容器其中 一 部分数据
jdk1.8 并发 控制使 用 synchronized 和 CAS 来操作。
全表锁使 用 synchronized 来保证线程安全,效率 非 常低下

jdk 1.7

image-20220413141316155

jdk 1.8

采 用 CAS 和 synchronized 来保证并发安全。数 据结构跟 HashMap1.8 的结构类似,数组+链表/红 黒二 叉树。Java 8 在链表 ⻓ 度超过 一 定阈值 (8)时将链表(寻址时间复杂度为 O(N))转换为红 黒 树(寻址时间复杂度为 O(log(N))) synchronized 只锁定当前链表或红 黒二 叉树的 首 节点,这样只要 hash 不冲突,就不会产 生 并 发,效率 又 提升 N 倍。

选用集合 ​

主要根据集合的特点来选 用,

需要根据键值获取到元素值时就选 用 Map 接 口 下的集 合,需要排序时选择 TreeMap , 不需要排序时就选择 HashMap , 需要保证线程安全就选 用 ConcurrentHashMap 。

需要存放元素值时,就选择实现 Collection 接 口 的集合,需要保证元素唯 一 时选择实现 Set 接 口 的集合 比 如 TreeSet 或 HashSet ,不需要就选择实现 List 接 口 的 比 如 ArrayList 或 LinkedList ,然后再根据实现这些接 口 的集合的特点来选 用。

反射与动态代理 ​

反射 ​

反射是什么 ​

反射允许对成员变量,成员方法和构造方法的信息进行编程访问

image-20260725225839626

获取 class 对象 ​

三种方式,分别对应三个阶段

  • Class.forName("全类名"); ——源码阶段,磁盘
  • 类名.class ——类加载阶段,内存
  • 对象.getClass();——运行阶段,内存
java
public class ReflectDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.tintin.custom.People");
        Class<People> bClass = People.class;
        Class<? extends People> cClass = new People().getClass();
        System.out.println(aClass == bClass);
        System.out.println(aClass == cClass);
    }
}
// true 
// true

利用反射获取构造方法 ​

java
	@Test
    public void testReflectConstructor() throws Exception {
        Class<People> clazz = People.class;
        Constructor<?>[] publicConstructors = clazz.getConstructors();// 获取所有公共(public)构造方法
        Constructor<?>[] constructors = clazz.getDeclaredConstructors(); // 获取所有构造方法
        Constructor<People> publicConstructor = clazz.getConstructor();// 获取指定参数的构造方法
        Constructor<People> constructor = clazz.getDeclaredConstructor();// 获取指定参数的构造方法

        System.out.println(Modifier.toString(nameField.getModifiers())); // 获取构造方法的修饰符
        System.out.println(Arrays.asList(constructor.getParameters())); // 获取构造方法的参数列表

        People people = constructor.newInstance(); // 实例化对象
        System.out.println(people);
    }

利用反射获取成员变量 ​

java
@Test
    public void testReflectField() throws Exception {
        Class<People> clazz = People.class;
        Field[] publicFields = clazz.getFields();// 获取所有公共(public)成员变量的数组
        Field[] fields = clazz.getDeclaredFields();// 获取所有成员变量的数组
//        Field nameField = clazz.getField("name"); // 获取指定名称的公共(public)成员变量
        Field nameField = clazz.getDeclaredField("name"); // 获取指定名称的成员变量

        System.out.println(Modifier.toString(nameField.getModifiers())); // 获取成员变量的修饰符
        System.out.println(nameField.getName()); // 获取成员变量的名称
        System.out.println(nameField.getType()); // 获取成员变量的类型

        People people = new People();
        nameField.setAccessible(true); // 设置成员变量可访问
        System.out.println(nameField.get(people)); // 获取成员变量的值
        nameField.set(people, "张三"); // 设置成员变量的值
        System.out.println(nameField.get(people));
    }

利用反射获取成员方法 ​

java
@Test
    public void testReflectField() throws Exception {
        Class<People> clazz = People.class;
        Field[] publicFields = clazz.getFields();// 获取所有公共(public)成员变量的数组
        Field[] fields = clazz.getDeclaredFields();// 获取所有成员变量的数组
//        Field nameField = clazz.getField("name"); // 获取指定名称的公共(public)成员变量
        Field nameField = clazz.getDeclaredField("name"); // 获取指定名称的成员变量

        System.out.println(Modifier.toString(nameField.getModifiers())); // 获取成员变量的修饰符
        System.out.println(nameField.getName()); // 获取成员变量的名称
        System.out.println(nameField.getType()); // 获取成员变量的类型

        People people = new People();
        nameField.setAccessible(true); // 设置成员变量可访问
        System.out.println(nameField.get(people)); // 获取成员变量的值
        nameField.set(people, "张三"); // 设置成员变量的值
        System.out.println(nameField.get(people));
    }

动态代理 ​

代理是什么 ​

代理(Proxy)是一种设计模式,通过代理对象访问目标对象,同时可以扩展目标对象的功能,实现无侵入式的给代码增加额外功能。

image-20260725220809515

静态代理 ​

通常把自行编写代码(在 编译期之前 确认好细节)实现的代理,称为 Java 的 静态代理。

目标类(xxx)实现某个接口或继承某个类。代理类(xxxProxy)中也 实现相同接口或继承相同父类,同时 持有目标类对象。代理对象(xxxProxyBean)通过调用相同的方法来调用目标对象(xxxBean)的方法

java
public class PersonDao {
    
    public void update() {
        System.out.println("update...");
    }
    
}

public class PersonDaoProxy extends PersonDao {
    
    PersonDao personDao;

    public PersonDaoProxy(PersonDao personDao) {
        this.personDao = personDao;
    }
    
    public void update() {
        personDao.update();
    }
    
    public static void main(String[] args){
        PersonDao p = new PersonDao();
        // p.update();
        PersonDaoProxy pProxy = new PersonDaoProxy(p);
        pProxy.update();// 代理对象的方法已在内部调用了被代理对象的方法
    }

}

好处:在不修改被代理对象的功能前提下,完成对目标功能的扩展

缺点:需要代理的目标对象多,代理类也就写得多。

动态代理 ​

把在程序 运行时 动态创建代理对象实现的代理称为 Java 的 动态代理。

通常依赖于 Java 内建的对代理模式支持的功能,两种主流实现方式:

  • JDK 动态代理
  • CGLIB 动态代理

JDK 动态代理 ​

JDK 动态代理,基于 接口,依靠 Java 的 反射机制 和 动态生成 class 的技术,来动态生成被代理的接口的实现对象。

在 java.lang.reflect.Proxy 类中,提供的一个 Proxy.newProxyInstance() 运行期动态创建创建了一个 "接口对象",底层是使用了 ProxyGenerator

java
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
# 参数一:用于指定用哪个类加载器,去加载生成的代理类
# 参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
# 参数三:用来指定生成的代理对象要干什么事情

由 java.lang.reflect.InvocationHandler 基于反射定义代理的具体逻辑。当代理对象调用任何方法时,都会触发该接口的 invoke 方法,通常通过 if 条件语句实现细粒度控制。

java
// 目标接口
interface UserService {
    void addUser();
}
// 目标接口实现
class UserServiceImpl implements UserService {
    public void addUser() {
        System.out.println("添加用户成功");
    }
}
// 动态代理调用处理器——定义代理逻辑
class MyHandler implements InvocationHandler {
    private Object target; // 真实对象
    public MyHandler(Object target) { this.target = target; }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("--- 开启日志记录 ---"); // 额外逻辑
        Object result = method.invoke(target, args); // 调用真实方法
        System.out.println("--- 结束日志记录 ---");
        return result;
    }
}
// 创建代理对象并调用代理方法
public class DynamicProxyDemo {
    public static void main(String[] args) {
        UserService realService = new UserServiceImpl();
        // 动态生成代理对象
        UserService proxy = (UserService) Proxy.newProxyInstance(
            realService.getClass().getClassLoader(),
            realService.getClass().getInterfaces(),
            new MyHandler(realService)
        );
        proxy.addUser(); // 此时会自动插入日志逻辑
    }
}

CGLIB 动态代理 ​

CGLIB 就是开源的 Code Generation Library,基于类的 继承,通过 操作字节码生成 被代理类的子类。

如果要实现 类的代理,可以使用 CGLIB 。

Java 新特性 ​

Java 版本历史 ​

Java 从 Java 10 开始每半年发布一次 Java 新版本,从 Java21 开始每两年发布一个 Java LTS 版本。

LTS 版本如下表所示,未出现的版本均为 非 LTS 的预览版本。

Java 版本类型说明发布日期
Java 8LTS广泛使用,已进入维护阶段2014-03
Java 11LTS当前最主流的 LTS 之一2018-09
Java 17LTS当前最推荐的 LTS,新项目首选2021-09
Java 21LTS引入了虚拟线程等重磅特性2023-09
Java 25LTS

LTS (Long-Term-Support,长期支持)指的是某个特定的 Java 版本会获得 Oracle 和其他参与厂商(如 Azul,Amazon,Red Hat,IBM 等)长期、稳定的更新和支持。

而非 LTS 版本的生命周期会非常短,当一个新版本发布后(比如 Java 14 发布),上—个非 LTs 版本(Java 13)很快就停止公共更新了。适合开发者学习、体验新功能,用于个人项目或测试环境,最好不要应用于生产。

JDK1.8 的新特性 ​

接口的静态方法 ​

在 Java 8 之前,如果想在接口相关的方法里提供一些工具方法(比如 Collections 类对集合的操作),只能另建一个工具类。现在,这些方法可以直接定义在接口本身中,让代码设计更加内聚。

接口的默认方法 ​

允许使用 default 关键字,在接口中添加具有默认实现的方法。其主要目的是为了接口演化,即向现有的接口添加新方法而不破坏已有的实现。

继承了这种接口的子类,可以直接使用该接口的默认方法无须实现。

java
@FunctionalInterface
public interface Comparator<T> {
	default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }
}

函数式接口 ​

指仅有一个抽象方法的接口。接口的其他方法可以由默认方法提前定义。一般搭配 Lambda 表达式使用。

给接口加上@FunctionalInterface 注解后,多于一个抽象方法时就会自动报错。

java
@FunctionalInterface
public interface Comparator<T> {    
	int compare(T o1, T o2);
}

内建的函数式接口:

输入输出
Predicate 判断接口一个参数返回 boolean
Function 转换数据接口一个参数返回一个结果
Supplier 生产者接口无返回一个结果
Consumer 消费者接口一个参数无

Lambda 表达式 ​

Lambda 表达式是匿名内部类的简写,用于即时创建某个函数式接口(即只有一个抽象方法的接口,如 Runnable、Comparator)的实例,并作为参数传递给方法或构造器。

Lambda 表达式无法像匿名内部类那样,支持多个抽象方法的接口或抽象类。

Lambda 表达式简化了代码量,增加可读性

java
		List<Long> aList = new ArrayList<>();
        Collections.sort(aList, new Comparator<Long>() {

            @Override
            public int compare(Long o1, Long o2) {
                return o2.compareTo(o1);
            }
        });
		//简化后
        Collections.sort(aList, (o1, o2)->o2.compareTo(o1));

Stream ​

Stream API 用于对集合元素进行函数式、声明式的处理(如过滤、映射、排序、归约、聚合等)。它可以透明地并行执行,充分利用多核处理器。

Stream 操作分为中间操作和最终操作。

  • 中间操作返回一个新的流,可进行链式操作。
  • 最终操作返回特定类型的计算结果,不再返回流,只能放在链式操作的最后。
java
public class StreamDemo {
    public static void main(String[] args) {
        /**
         * 创建流 
         * 将 .Stream() 替换为 .parallelStream()即可轻松实现并行处理,提升大规模数据集的处理性能。
         */
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        // 由集合创建流
        Stream<Integer> listStream = list.stream();

        // 特殊流
        IntStream intStream = list.stream().mapToInt(Integer::intValue);
        DoubleStream doubleStream = list.stream().mapToDouble(Integer::doubleValue);
        LongStream longStream = list.stream().mapToLong(Integer::longValue);

        // 由数组创建流
        String[] array = {"a", "b", "c"};
        Stream<String> arrayStream = Stream.of(array);

        // 直接创建流
        Stream<Integer> streamOfInts = Stream.of(1, 2, 3, 4, 5, 6);
        Stream<List<Integer>> streamOfLists = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4), Arrays.asList(5, 6));

        /**
         * 中间操作
         *
         * 基本类型流转换 mapToInt() mapToDouble() mapToLong()
         * 扁平映射 flatMap()
         * 过滤 filter()
         * 去重 distinct()
         * 跳过和截取 skip() limit()
         * 映射 map()
         * 排序 sorted()
         * 无影响遍历 peek(Consumer)
         */
        listStream = listStream.distinct();// 去重
        listStream = listStream.skip(2).limit(3); // // 跳过和截取
        listStream = listStream.filter(i -> i > 1);// 过滤
        listStream = listStream.map(i -> i * 2);// 映射
        listStream = listStream.sorted(Comparator.reverseOrder()); // 排序
        System.out.println("中间操作结果:" + listStream.collect(Collectors.toList()));

        Stream<Integer> integerStream = streamOfLists.flatMap(l -> l.stream());
        System.out.println("扁平映射结果:" + integerStream.collect(Collectors.toList()));

        /**
         * 终止操作
         *
         * 遍历操作 forEach(Consumer) forEachOrdered(Consumer) 牺牲并行性能,保证遍历顺序
         * 收集 collect() toArray()  jdk 16+ toList() toSet()  toMap()
         * 归约与聚合 reduce()  count() max() min() sum() average();
         * 匹配与查找 anyMatch() allMatch() noneMatch() findFirst() findAny()
         * 并行能力 parallel()
         */
        System.out.println("遍历结果:");
        list.stream().forEach(System.out::println); // 遍历

        List<String> collectResult = arrayStream.collect(Collectors.toList()); // 转换为其他数据结构
        System.out.println("收集结果:" + collectResult);

        Integer reduceResult = list.stream().reduce(0, (i, j) -> i + j);// 聚合
        System.out.println("聚合结果:" + reduceResult);
        long count = list.stream().count();
        System.out.println("元素个数:" + count);
        Optional<Integer> max = list.stream().max(Comparator.naturalOrder());
        max.ifPresent(value -> System.out.println("最大值:" + value));
        Optional<Integer> min = list.stream().min(Comparator.naturalOrder());
        min.ifPresent(value -> System.out.println("最小值:" + value));
        int sum = intStream.sum(); // 特殊流的特殊聚合
        System.out.println("元素和:" + sum);
        OptionalDouble average = doubleStream.average(); // 特殊流的特殊聚合
        System.out.println("元素平均值:" + average.getAsDouble());

        boolean anyMatchResult = list.stream().anyMatch(i -> i > 1);
        System.out.println("任意匹配结果:" + anyMatchResult);
        boolean allMatchResult = list.stream().allMatch(i -> i > 1);
        System.out.println("全部匹配结果:" + allMatchResult);
        boolean noneMatchResult = list.stream().noneMatch(i -> i > 1);
        System.out.println("无匹配结果:" + noneMatchResult);
        Optional<Integer> findFirst = list.stream().findFirst();
        System.out.println("第一个元素:" + findFirst.get());
        Optional<Integer> findAny = list.stream().findAny();
        System.out.println("任意元素:" + findAny.get());
    }
}

Optional ​

Optional 是一个容器对象,用于包装可能为 null 的值。它强制开发者显式地处理值不存在的情况,而不是被动地遭遇 NullPointerException。辅助开发者更安全、更直观地处理空指针异常

java
// 传统方式
if (user != null) {
    Address address = user.getAddress();
    if (address != null) {
    // ...
    }
}

// 使用0ptional
Optional<User> optionalUser = Optional.ofNullable(user);
optionalUser.map(User::getAddress).ifPresent(address → { 
    // ... 
});

常用场景:判断请求参数为空

java
int pageNum = Optional.ofNullable(params.getPageNum())
    .orElseThrow(() -> new RuntimeException("pageNum不能为空"));

LocalDate 等时间类型 ​

新的日期时间 API 引入了 LocalDate、LocalTime、LocalDateTime、 ZonedDateTime 等类,彻底解决了旧的 java.util.Date 和 java.util.Calendar 类的诸多问题(如非线程安全、API 设计差、时区处理繁琐)。

Java 9 新特性 ​

集合工厂方法 ​

便捷的工厂方法,能够轻松创建不可变集合。

java
public class ImmutableCollectionDemo {
    public static void main(String[] args) {
        List<String> list = List.of("a", "b", "c");
        Map<String, Integer> map = Map.of("a", 1, "b", 2);
        Set<String> set = Set.of("a", "b", "c");
        list.add("d"); // 报错
    }
}

如果想创建包含大量元素的不可变 Map,可以使用 ofEntries 方法

java
Map<String, Integer> largeMap = Map.ofEntries(
                Map.entry("a", 1),
                Map.entry("b", 2),
                Map.entry("c", 3),
                Map.entry("d", 4));

改进 try-with-resources ​

try-with-resources 是 Java 7(JDK 1.7) 引入的特性

只要资源实现了 AutoCloseable(或 Closeable),就可以在 try 的括号中声明,代码块结束后会自动关闭,无需手写 finally。

Java 9 改进了 try-with-resources 语句,在这之前,我们不能在 try 子句中使用外部定义的变量,必须在 try 括号内重新声明,会让代码变得冗余。

java
public void readFile(String filename) throws IOException {
    BufferedReader bufferedReader = Files.newBufferedReader(Paths.get(filename));
    try(BufferedReader br = bufferedReader) { // JAVA 9 以前
        br.lines().forEach(System.out::println);
    }
}
public void readFile2(String filename) throws IOException {
    BufferedReader bufferedReader = Files.newBufferedReader(Paths.get(filename));
    try(bufferedReader) { // JAVA 9
        bufferedReader.lines().forEach(System.out::println);
    }
}

接口的私有(静态)方法 ​

允许接口定义私有方法(private)和私有静态方法(private static)

模块系统 ​

在模块系统出现之前,传统 Java 应用只能依赖 classpath 来管理依赖,所有的类都在同一个类路径下,任何类都可以访问任何其他类,这种 “全局可见性” 在大型项目中会导致代码耦合严重、依赖关系混乱、运行时才发现 ClassNotFoundException 等问题。

模块系统允许我们将代码组织成模块,每个模块都有明确的依赖关系和导出接口,让大型应用的架构变得更加清晰和可维护。

模块系统允许通过 module-info.java 定义模块,明确声明依赖和导出包,增强了封装性和安全性。解决了 JAR Hell 问题,为打造更小的运行时镜像打下基础。

JShell 交互工具 ​

JShell,使开发者可以像使用 Python 解释器一样使用 Java,对于学习调试有点儿用。

Java 10 新特性 ​

var 变量 ​

弱类型编程语言如 JavaScript 无需声明变量的类型。

对于 Java 这种强类型语言,写一长串的变量声明语句。

java
Map<String, List<Integer>>  complexMap = new HashMap<String, List<Integer>>( );

var 关键字支持局部变量的类型推断,编译器会根据初始化表达式自动推断变量的类型,让代码可以变得更简洁。

java
var complexMap = new HashMap<String, List<Integer>>();

实际开发中,滥用 var 会降低可读性。即使前端开发也趋向于用 TypeScript 提供类型检查支持。

应用程序类数据共享 ​

Java 10 扩展了类数据共享功能,允许应用程序类也参与共享(Application Class-DataSharing)。在此之前,只有 JDK 核心类可以进行类数据共享,应用程序类每次启动都需要重新加载和解析。

Java 11 新特性 ​

HTTP 客户端 API ​

HTTP 请求是后端开发常用的能力,之前我们只能基于内置的 HttpURLConnection 自己封装,或者使用 Apache HttpClient、OkHttp 第三方库。

Java 11 将 HTTP 客户端 API 正式化,新的 HTTP 客户端提供了现代化的、支持 HTTP/2 和 WebSocket 的客户端实现,让网络编程变得简单。

java
public class JavaHttpClientDemo {
    public static void main(String[] args) {
        // 建造者模式
        // 创建 HTTP 客户端
        HttpClient httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .followRedirects(HttpClient.Redirect.NORMAL)
                .build();

        // 构建 GET 请求
        HttpRequest getRequest = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/get"))
                .header("Accept", "application/json")
                .header("User-Agent", "Java-HttpClient")
                .timeout(Duration.ofSeconds(30))
                .GET()
                .build();

        // 构建 POST 请求
        HttpRequest postRequest = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/post"))
                .header("Accept", "application/json")
                .header("User-Agent", "Java-HttpClient")
                .timeout(Duration.ofSeconds(30))
                .POST(HttpRequest.BodyPublishers.ofString("key1=value1&key2=value2"))
                .build();
        
        // 发送 GET 请求
        try {
            HttpResponse<String> response = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString());
            System.out.println("状态码:" + response.statusCode());
            System.out.println("响应体:" + response.body());
            System.out.println("响应头:" + response.headers());
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 异步发送 POST 请求
        httpClient.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::body)
                .thenAccept(System.out::println)
                .join();
    }
}

String 新增方法 ​

Java 11 为 String 类添加了许多实用的方法,让字符串处理变得更加方便。

  • isBlank() 判断空白字符串
  • strip() 比起 trim() 更好的去除空白字符串
  • lines() 按行 stream 处理
  • repeat() 重复字符串

实际开发,可能还是统一使用 Hutool 或者 Apache Commons 提供的字符串工具类。

Files 类 ​

Java 11 为文件操作新增了 更便捷的方法,不需要使用 FileReader/FileWriter 这种复杂的操作了。

​

Java 14 新特性 ​

swtich 表达式 ​

传统的 switch 语句存在不少问题,比如需要手动添加 break 防止穿透、赋值不方便等

Java 14 以后,switch 表达式增加不少特性

java
public class SwitchDemo {

    public static void main(String[] args) {
        System.out.println(new SwitchDemo().traditionSwitch("MONDAY"));
        System.out.println(new SwitchDemo().newSwitch("SATURDAY"));
        System.out.println(new SwitchDemo().newSwitchWithYield("SUNDAY"));
    }

    public String traditionSwitch(String day) {
        String dayType;
        DayOfWeek dayOfWeek = DayOfWeek.valueOf(day);
        switch (dayOfWeek) {
            case MONDAY:
            case TUESDAY:
            case WEDNESDAY:
            case THURSDAY:
            case FRIDAY:
                dayType = "weekday";
                break;
            case SATURDAY:
            case SUNDAY:
                dayType = "weekend";
                break;
            default:
                dayType = "unknown";
                break;
        }
        return dayType;
    }

    public String newSwitch(String day) {
        DayOfWeek dayOfWeek = DayOfWeek.valueOf(day);
        return switch (dayOfWeek) {
            case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "weekday";
            case SATURDAY, SUNDAY -> "weekend";
            default -> "unknown";
        };
    }

    // yield 用于复杂逻辑的结果返回
    public String newSwitchWithYield(String day) {
        DayOfWeek dayOfWeek = DayOfWeek.valueOf(day);
        return switch (dayOfWeek) {
            case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> {
                System.out.println("It's a weekday");
                yield "weekday";
            }
            case SATURDAY, SUNDAY -> {
                System.out.println("It's a weekend");
                yield "weekend";
            }
            default -> {
                System.out.println("It's unknown");
                yield "unknown";
            }
        };
    }



    enum DayOfWeek {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
    }
}

空指针异常提示 ​

以前一行代码中可能包含多个可能为 null 的访问路径,无法确认具体是哪个变量出现空指针异常。

Java 14 改进了 NullPointerException 的错误信息。JVM 会提供更详细的堆栈跟踪信息,指出导致异常的具体位置和原因,让调试变得更加容易。

image-20260918213318936

Java 15 新特性 ​

文本块和占位 ​

文本块,直接用三个引号括起来,就能以字符串本来的格式展示,大大提升多行字符串的可读性和可维护性。

formatted() 新增的占位替换方法

java
public class TextBlockDemo {
    public static void main(String[] args) {
        // 使用三引号定义多行字符串并插入变量
        String name = "Tintin";
        int age = 25;
        String message = """
I am %s,i am %d years old.
""".formatted(name, age);
        System.out.println(message);

        // html
        String html = "<html>\n" +
                "    <body>\n" +
                "        <p>Hello, World!</p>\n" +
                "    </body>\n" +
                "</html>";
        html = """
        <html>
            <body>
                <p>%s</p>
            </body>
        </html>
        """.formatted("Hello, World!");
        System.out.println(html);

        // 多行 sql
        String sql = "SELECT * FROM users\n" +
                "WHERE id = 1";
        sql = """
        SELECT * FROM users
        WHERE id = 1
        """;
        System.out.println(sql);
        
        // JSON
        String json = "{\n" +
                "    \"name\": \"Tintin\",\n" +
                "    \"age\": 25\n" +
                "}";
        json = """
        {
            "name": "Tintin",
            "age": 25
        }
        """;
        System.out.println(json);
    }
}

Hidden 隐藏类 ​

在 Lambda 表达式、AOP 动态代理、ORM 映射等场景中,框架会动态生成代码载体(比如方法句柄、临时代理类),这些载体需要关联类的元数据才能运行。如果生成频繁,传统类的元数据会被类加载器追踪,需要等待类加载器卸载才能回收,导致元空间堆积和 GC 压力。

Java15 引入了 Hidden 隐藏类特性,这是一个专为框架和运行时环境设计的底层机制,主要是为了优化动态生成短期类(比如 Lambda 表达式、动态代理)的性能问题,普通开发者无需关心。

Hidden 类的特点是对其定义类加载器之外的所有代码都不可见,由于不可发现且链接微弱,JVM 垃圾回救器能够更高效地卸载隐藏类及其元数据,从而防止短期类堆积对元空间造成压力,优化了需要动态生成大量类的性能。

Java 16 新特性 ​

Record 类型 ​

Java 语言从诞生之日起就以其严谨和啰嗦著称。创建一个简单的数据类,我们需要编写构造函数、getter 方法、equals()、hashCode()和 toString()方法。这些 "模板代码" 占据了大量的开发时间,却几乎不包含任何业务逻辑。Java 14 引入并在 JDK 16 正式确立的 Record 的初衷正是为了解决这个问题。

Record 是 Java 语言级别的特性,旨在简化数据类的定义,让开发者能够专注于真正重要的业务逻辑,而不是被样板代码所困扰。

传统 POJO(Plain Ordinary Java Object) 类

java
public class TraditionalPerson {
    private String name;
    private int age;

    public TraditionalPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public boolean equals(Object obj) {
        return super.equals(obj);
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    @Override
    public String toString() {
        return super.toString();
    }
}

lombok 简化

java
@Data
@AllArgsConstructor
public class LombokPerson {
    private String name;
    private int age;
}

使用 Record

java
public record RecordPerson(String name, int age) {}

可以看出 Record 做到了极致的简洁性。

Record 类 和 Lombok 插件类似,自动为我们生成:

  • 私有的、final 的字段
  • 公共的构造函数
  • 对应每个组件的公共访问方法(注意不是 getXxx 而是直接以字段名命名)
  • equals()和 hashCode()方法
  • toString()方法
  • 实现了 java.io.Serializable 接口

Lombok 在 Java 生态系统中仍然非常流行,但它终究是一个第三方库,需要在项目中添加依赖,通过修改 AST 工作,需要 IDE 插件支持,过度使用可能导致代码可读性下降。Record 作为 Java 语言的原生特性,不需要任何第三方依赖。这意味着更少的项目依赖、更快的编译速度、更好的工具支持、更少的兼容性问题,尤其适合微服务架构场景。

Record 类默认是不可变的,所有字段都是 final 的。这种设计有很多优点:

  • 线程安全:不可变对象天然线程安全
  • 可缓存:不可变对象可以安全地被缓存
  • 防御性复制:不需要进行防御性复制
  • 函数式编程友好:适合与 Stream API 等函数式特性配合使用

instanceof 模式匹配 ​

传统的 instanceof 使用方式,需要显示转换对象类型

Java 16 正式推出了 instanceof 的模式匹配,让类型检查和转换变得更优雅。

java
public static void main(String[] args) {
        // 传统方式
        Object obj = "Hello, World!";
        if (obj instanceof String) {
            String str = (String) obj;
            System.out.println(str.length());
        }

        // Java 16
        if (obj instanceof String s) {
            System.out.println(s.length());
        }
    }

Stream 新增方法 ​

toList() 方法,可以用更简洁的代码将流转换为不可变列表。

java
list.stream.collect(Collectors.toList()); // 可变列表
list.stream..toList();// 不可变列表

提供了 mapMulti() 方法,跟 flatMap() 的作用一样,将一个元素映射为 0 个或多个元素,但是某些场景下比 flatMap() 更灵活高效。

Java 17 新特性 ​

Sealed 密封类 ​

在以前,一个类要么完全开放继承(任何类都能继承),要么完全禁止继承(final 类)。

Java 17 将 Sealed 密封类转正,让类的继承关系变得更可控和安全,实现更精确的控制继承关系,有利于设计可靠的 API 或领域模型。

java
public sealed class Triangle extends Shape permits RightTriangle {}

但是,被允许继承的子类必须选择一种继承策略

java
public final class RightTriangle extends Triangle {} // 到我为止,不能再继承了
java
public sealed class RightTriangle extends Triangle {} // 我也要控制谁能继承我
java
public non-sealed class RightTriangle extends Triangle {} // 我开放继承,任何人都可以继承我 必须手动标注 non-sealed 确保开发者有意开放权限

image-20260918215946503

新的随机数生成器 ​

java
public static void main(String[] args) {
    //传统的随机数
    Random oldRandom = new Random();
    int oldValue = oldRandom.nextInt(100);
    //新的随机数生成器
    RandomGenerator generator = RandomGenerator.of("L32X64MixRandom");
    int newValue = generator.nextInt(100);
    System.out.println("Old Random: " + oldValue);
    System.out.println("New Random: " + newValue);
}

Java 18 新特性 ​

简易 Web 服务器 ​

引入了一个简单的 Web 服务器,主要用于开发和测试。

java
#启动简单的文件服务器,服务当前目录jwebserver
#指定端口和目录
jwebserver -p 8080 -d /path/to/your/files
#绑定到特定地址
jwebserver -b 127.0.0.1 -p 9000

UTF-8 作为默认字符集 ​

在这之前,Java 使用的是系统默认字符集,会导致同一段代码在不同操作系统上可能产不同的结果。

Java 18 将 UTF-8 设为默认字符集,解决了很多字符编码相关的问题,Java 程序在不同平台上的行为会更加一致。

java
// 这些操作现在默认使用 UTF-8 编码
FileReader reader = new FileReader("file.txt");
FileWriter writer = new FileWriter("file.txt");
PrintStream out = new PrintStream("output.txt");

Java 21 新特性 ​

Virtual Threads 虚拟线程 ​

在传统的 Java 线程模型中也是如此。如果每个线程都对应操作系统的一个真实线程,创建成本很高、内存占用也大。当需要处理大量并发请求时,系统可能很快就会被拖垮。

而协程是用户态的轻量级线程,协程的调度完全由用户控制,一个线程可以拥有多个协程,协程之间的切换不需要操作系统的干预,因此开销更小。

协程并非新概念,它作为一种比线程更轻量的并发编程模型,在后续几十年里被多种语言采纳和发展。Java 的协程功能——虚拟线程(Virtual Threads)是 JAVA 19 的预览特性,在 JAVA 21 正式转正。

Virtual Threads 虚拟线程是 Java 并发编程的革命性突破,也是很多 Java 开发者选择 21 的理由。

假设开 1000 个线程同时处理网络请求,创建 1000 个线程,大多数线程都会阻塞在等待网络请求的响应上,很浪费。

java
public void handleRequests() {
    for (int i = 0; i < 1000; i++) {
        new Thread(() -> {
            //发送网络请求,需要等待响应
            String result = httpClient.get("https://codefather.cn");System.out.println("收到响应:" + result);
        }).start();
    }
}

常见解决方法是使用线程池,还要一个方法就是创建虚拟线程,开一个虚拟线程执行器执行同样的一批任务,执行器会为每个任务生成一个虚拟线程来处理。

java
// 传统创建线程 处理网络请求
    public void handleRequests() {
        for (int i = 0; i < 1000; i++) {
            new Thread(this::sendRequest).start();
        }
    }

    // 线程池优化
    public void handleRequestsWithThreadPool() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            executorService.submit(this::sendRequest);
        }
    }

    // 协程优化
    public void handleRequestsWithCoroutine() {
        ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
        for (int i = 0; i < 1000; i++) {
            executorService.submit(this::sendRequest);
        }
    }

直接创建虚拟线程

java
// 传统
new Thread(() -> {
// 要异步执行的任务
}).start();

// 虚拟线程
Thread.ofVirtual().start(() -> {
//要异步执行的任务
});

Virtual Threads 的核心优势。

  • 超级轻量。一个传统线程可能需要几 MB 的内存,而一个虚拟线程只需要几 KB。你可以轻松创建百万级别的虚拟线程而不用担心系统资源。
  • 编程简单。你不需要学习复杂的异步编程模式,跟创建一个普通线程的代码类似,一行代码就能提交异步任务。当遇到阻塞的 I/O 操作时,虚拟线程会自动让出底层的操作系统线程。

switch 模式匹配 ​

Java 14 版本推出了 Switch 表达式,能够一行处理多个条件;Java 21 版本进一步优化了 Switch 的能力,新增了模式匹配特性,能够更轻松地根据对象的类型做不同的处理。

没有 Switch 模式匹配时,我们需要利用 instanceof 匹配类型

有了 switch 模式匹配,这段代码可以变得很优雅,直接在匹配对象类型的同时声明了变量(相当于 Java 14 swtich 表达式 和 Java 16 instanceof 模式匹配的结合)

java
public static void main(String[] args) {
    // 传统方式
    List<String> list = List.of("a", "b", "c");
    Object message = list;
    if (message instanceof String textMessage) {
        System.out.println("文本消息:" + textMessage);
    } else if (message instanceof Integer numberMessage) {
        System.out.println("数字消息:" + numberMessage);
    } else if (message instanceof List listMessage) {
        System.out.println("列表消息,包含" + listMessage.size() +" 个元素");
    } else {
        System.out.println("未知消息类型");
    }

    // Java 21
    Integer number = 123;
    message = number;
    switch (message) {
        case String textMessage -> System.out.println("文本消息:" + textMessage);
        case Integer numberMessage -> System.out.println("数字消息:" + numberMessage);
        case List listMessage -> System.out.println("列表消息,包含" + listMessage.size() +" 个元素");
        default -> System.out.println("未知消息类型");
    }
}

此外,模式匹配还支持 条件判断,让处理逻辑更加精细,相当于在 case ... when ...中写 if 条件表达式(感觉有点像 SQL 的语法)。

java
public static void main(String[] args) {
    	// case when  进一步细分
        String string = "Hello, World!";
        Object message = string;
        switch (message) {
            case String textMessage when textMessage.length() < 10 -> System.out.println("小文本:" + textMessage);
            case String textMessage when textMessage.length() < 100 -> System.out.println("中等文本:" + textMessage);
            case String textMessage -> System.out.println("大文本:" + textMessage);
            case Integer numberMessage -> System.out.println("数字消息:" + numberMessage);
            case List listMessage -> System.out.println("列表消息,包含" + listMessage.size() +" 个元素");
            default -> System.out.println("未知消息类型");
        }
}

进一步结合 record 模式

image-20260918233506627

有序集合 ​

public interface List<E> extends SequencedCollection<E> {
public interface SequencedCollection<E> extends Collection<E> {

List 、SequencedMap 、SequencedSet 接口 都继承了 SequencedCollection 有序集合接口,新增了类似 addFirst()、 addLast()、 removeFirst()、 removeFirst() 、 reversed() 等方法

分代 ZGC ​

ZGC 从 Java 11 开始就以其超低延迟而闻名,但是它并没有采用分代的设计思路。ZGC 对所有对象一视同仁,无论是刚创建的新对象还是存活了很久的老对象,都使用同样的收集策略。这虽然保证了一致的低延迟,但在内存分配密集的应用中,效率并不是最优的。

Java 21 中的分代 ZGC 可以说是垃圾收集器领域的一个重大突破。

Java 22 新特性 ​

未命名变量和模式 ​

在开发中,我们可能会遇到这样的情况:有些变量我们必须声明,但实际上并不会使用到它们的值。 Java 22 之前,我们只能给这些不使用的变量起一个名字,代码会显得有些多余。

有了未命名变量特性,可以使用下划线_表示不使用的变量代码,意图更清晰

Java 24 新特性 ​

类文件 API、Stream Gatherers 流收集器、