一. Java的基本程序设计结构

1. Hello World

/**
 * 1. Java区分大小写
 * 2. 类名必须以字母开头,后面可以跟字母和数字的任意组合.
 *    长度基本上没有限制。但是不能使用Java保留字
 * 3. 源代码的文件名必须与公共类的类名相同,并用Java作为扩展名。
 * 4. 运行一个已编译的程序时,Java虚拟机总是从指定类中main方法的代码开始执行(这里
 *    的 "方法“ 就是Java中对“函数”的叫法),因此为了能够执行代码,类的源代码中必须包
 *    含一个Main方法。根据Java语言规范,main方法必须声明为public
 */
public class Test
{
    public static void main(String[] args)
    {
        System.out.println("Hello World!");
    }
}

2. 注释

1. //:从//开始到本行结尾都是注释。
2. /* */:将一段长注释括起来
3. /** */:用来自动生成文档。这种注释以/**开始,以*/号结束

3. 数据类型

  • Java是一种强类型语言。这就意味着必须为每一个变量声明一个类型。在Java中,一共有8种基本类型, 其中有4种整型、2种浮点类型、1 种字符类型char和1种用于表示真值的boolean类型。

①整型

  • 在Java中,各种数据类型的取值范围是固定的,与运行Java的机器无关(JVM的原因:Java源码执行过程)。

  • 整型默认为int型。

  • 长整型数值有一个后缀L或l(如4000L)。十六进制数值有一个前缀0x或0X(如0xCAFE)。八进制有一个前缀0(例如010对应十进制中的8)。加上前缀的0b或0B还可以写二进制数。例如, 0b1001就是9。另外,可以为数字字面量加 下画线,如用1_000_000(0b1111_0100_0010_0100_0000)表示100万。这些下画线只是为了让人更易读。Java编译器会去除这些下画线。

②浮点型

  • float类型的数值有一个后缀F或f(例如,3.14F). 没有后缀F的浮点数值(如3.14)总是默认为double类型。也可以在double数值后面添加后缀D或d(例如,3.14D)。

③char类型

  • char类型原本用于表示单个字符。如今,有些Unicode 字符可以用一个char值描述,另外一些Unicode字符则需要两个char值。

  • char类型的字面量值要用单引号括起来。例如:'A'是编码值为65的字符常量。它与"A"不同,"A"是包含一个字符的字符串。

  • char类型的值可以表示为十六进制值,其范围从\u0000~\uFFFF。例如,\u2122表示商标符号(™)。

  • \u是用来表示Unicode字符转义的特殊符号,它后面跟随四个十六进制数字(即 0-9 和 A-F)来表示一个特定的 Unicode 字符。

  • 可以在加引号的字符字面量或字符串中使用这些转义序列。例如,'\u2122'或"hello\n"。\u还可以在加引号字符常量或字符串之外使用(而其他所有转义序列不可以)。 public static void main(String\u005B\u005D args), \u005B和\u005D分别是[和]的编码。

④Unicode和char类型

  • 在Java中,char类型描述了采用UTF-16编码的一个代码单元。强烈建议不要在程序中使用char类型,除非确实需要处理UTF-16代码单元。最好将字符串作为抽象数据类型来处理。

⑤boolean类型

  • boolean (布尔) 类型有两个值:false 和true, 用来判定逻辑条件。整型值和布尔值之间不能进行相互转换。

4. 变量与常量

①声明变量

  • 每个变量都有一个类型(type)。声明一个变量时,先指定变量的类型, 然后是变量名。每个声明都以分号结束。可以在一行中声明多个变量。int i, j;

②初始化变量

  • 声明一个变量之后,必须用赋值语句显式地初始化变量。,Java编译器会认为下面的语句序列有错误:int vacatlonDays; Systera.out.println(vacationOays);"ERROR-variable not initialized"

  • 要想对一个已声明的变量进行赋值,需要将变量名放在等号(=)左侧,再把一个有适当值的Java表达式放在等号的右侧。int a; a=1;

  • 也可以将变量的声明和初始化放在同一行中。int a=1;

  • Java中可以将声明放在代码中的任何地方。从Java 10开始,对于局部变量,如果可以从变量的初始值推断出它的类型,就不再需要声明类型只需要使用关键字var而无须指定类型:var greeting = "Hello" greeting is a String

③常量

  • 可以用关键字final指示常量。final int A=1;

  • 关键字final表示这个变量只能被赋值一次。一旦赋值,就不能再更改了。习惯上,常量名使用全大写。

关键字

用途

final

  • 对于变量,表示常量;

  • 对于方法,表示不能被重写;

  • 对于类,表示不能被继承。final类中的方法自动称为final方法,字段不会。

  • 当 final 用于变量时,意味着该变量的值一旦初始化后就不能再修改。这个变量被视为常量。可以将实例字段定义为final、这样的字段必须在构造对象时初始化。也就是说,必须确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。

  • 对于对象引用,final 保证的是引用不可变,而不是对象的内容不可变。你仍然可以修改 final 变量所引用对象的内部状态,但不能让它指向其他对象。

  • 枚举和记录总是final, 它们不允许扩展。

static final

  • 用于定义常量,确保常量是类级别的,并且不可修改。

  • static:常量是类级别的,而不是实例级别的,因此使用static来确保常量对所有实例共享。

  • 枚举类:枚举类是一种特殊的类,用于表示一组固定的常量。它提供了比普通常量更强大的功能,比如可以为每个枚举值添加字段、方法以及实现接口。

5. 运算符

①算术运算符

  • 通常的算术运算符+,-,* /分别表示加、减、乘、除运算。当参与/运算的两个操作数都是整数时,/表示整数除法;否则,这表示浮点除法。整数的求余操作(有时称为取模) 用%表示。

  • 整数被0除将产生一个异常,而浮点数被0除将会得到一个无穷大或NaN结果。

②数值类型之间的转换

  • 在图3-1中有6个实线箭头,表示无信息丢失的转换;另外有3个虚线箭头,表示可能有精度损失的转换。例如,123456789是一个大整数, 它包含的位数多于float类型所能表示的位数。将这个整数转换为float类型时,数量级是正确的,但是会损失一些精度。

③强制类型转换

  • 在必要的时候,int类型的值将会自动地转换为double类型。但有时也需要将double类型转换成int类型。在Java中,允许进行这种数值转换,不过当然会丢失一些信息。这种可能损失信息的转换要通过强制类型转换(cast)来完成。强制类型转换的语法格式是在圆括号中指定想要转换的目标类型,后面紧跟待转换的变量名。double Pi=3.14; int p=(int)Pi;//3

  • 如果试图将一个数从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值。

④赋值

  • x+=4; x=x+4 一般来说,要把运算符放在=号左边,如*=或%=

⑤自增与自减运算符

  • ++n; n++ 后缀和前缀形式都会使变量值加1或减1。但用在表达式中时,二者就有区别了。前缀形式会先完成加1; 而后缀形式会使用变量原来的值,然后自增。

⑥关系和boolean运算符

  • ==、!=、<、<=、>、>=、&&、||。

  • &&和||运算符是按照“短路”方式来求值的:如果第一个操作数已经能够确定表达式的值,第二个操作数就不必计算了。

⑦条件运算符

⑧位运算符

  • &、|、^、~、>>、<<

  • >>>运算符会用0填充高位,这与>>不同,>>会用符号位填充高位, 不存在<<<运算符。

⑨括号与运算符级别

6. 字符串

  • Java字符串就是Unico加字符序列。例如,"a\u2122"由两个unicode字符a和TM组成。Java没有内置的字符串类型,而是标准Java类库中提供了一个预定义类,很自然地叫作String。每个用双引号括起来的字符串都是Stung类的一个实例。

①子串

  • 子串(Substring)指的是字符串中的一部分。下标从0~尾置下标。

②拼接

  • Java语言允许使用+号拼接两个字符串。

  • 当将一个字符串与一个非字符串的值进行拼接时,后者会转换成字符串(任何一个Java对象都可以转换成字符串)。

③字符串不可变

  • String类没有提供任何方法来修改字符串中的某个字符。若希望修改某个字符串,可以提取想要保留的子串,再与希望替换的字符拼接。

  • 由于不能修改Java字符串中的单个字符.所以在Java文档中将String类对象称为是不可变的。

  • 不可变字符串有一个很大的优点:编译器可以让字符串共享。各个字符串存放一个在公共存储池中,字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串和复制的字符串共享相同的字符。

④检测字符串是否相等

  • s.equals(t) s与t可以是字符串变量,也可以是字符串字面量。

  • 运算符用于比较两个对象的引用是否相同,而不是比较对象的内容。这一点对于 String 类型尤为重要,因为字符串是对象,并且 用于比较字符串引用而不是字符串的实际内容。

  • 只有字符串字面量会共享,而+或substring等操作得到的字符串并不共享。

⑤空串与Null串

  • 空串""是长度为0的字符串。str.length()==0或str.equals("")

  • 空串是一个java对象,有自己的串长度(0)和内容(空)。不过,String变量还可以存放一个特殊的值,名为null, 表示目前没有任何对象与该变量关联。str==null

⑥码点与代码单元

⑦String API

⑧构建字符串

  • 有时由较短的字符串构建字符串,如果采用字符串拼接的方式来达到这个目的,效率会比较低,每次拼接字符串时,都会构建一个新的String对象,既耗时,又浪费空间,使用StringBuilde类就可以避免这个问题。

⑨文本块

  • Java 15 新增的文本块(textblock) 特性,可以提供跨多行的字符串字面量。文本块以""开头(这是开始""), 后面是一个换行符,并以另一个""结尾(这是结束""):

7. 输入与输出

①读取输入

import java.util.*;

Scanner in=new Scanner(System.in);//标准输入

②格式化输出

  • 格式化规则是特定于本地化环境的。例如,在德国,分组分隔符是点号而不是逗号。

        double x=10.0/3;
        System.out.print(x);//以x类型所允许的最大非0位数打印

        //沿用了C语言函数库中的古老约定
        //字段宽度为 8个字符,精度为2个字符
        System.out.printf("%8.2f", x);
        /*
        可以为printf 提供多个参数
        每一个以%字符开头的格式说明符都替换为相应的参数。格式说明
        符末尾的转换字符指示要格式化的数值的类型:f 表示浮点数,s表示
           字符串,d表示十进制整数.
           大写形式会生成大写字母.例如%8.2E" 将3333.33格式化为3.33E+03,
           这里有一个大写的E
         */
        System.out.printf("Hello, %s. You are %d", "wcc", 10);



        /*
            指定控制格式化输出外观的各种标志
            例如,逗号标志会增加分组分隔符
可以使用多个标志.例如,由「2严会使用分组分隔符,并将负数包围在括号内。
         */
        System.out.printf("%,.2f", 10000.0/3.0);//3,333.33



        /*
            可以使用静态的String.format方法创建一个格式化
            字符串而不打印输出
         */
        String message=String.format("Hell, %s. Next year, you'll be %d", "wc", 10);
        //在Java15 中,可以使用 formatted方法、这样可以少敲5个字符
        message="Hello, %s. Next year, you'll be %d".formatted("w", 2);

③文件输入与输出

        //如果文件名中包含反斜线符号,记住要在每个反斜线之前再加一个额外的反斜线转义:C:\\mydirectory
        //现在就可以使用之前见过的任何Scanner方法读取这个文件了口
        Scanner in=new Scanner(Path.of("test.txt", String.valueOf(StandardCharsets.UTF_8)));
        String line=in.nextLine();

        /*
            要想写入文件,需要构造一个PrintWriter对象:
            在构造器中,需要提供文件名和字符编码:
            如果文件不存在,则创建该文件。可以像输出到5ystemmt一样使用print、println以及printf命令
         */
        PrintWriter out=new PrintWriter("test.txt", StandardCharsets.UTF_8);
        out.println(line);

8. 控制流程

①块作用域

  • 块(即复合语句)由若干条Java语句组成,并用一对大括号括起来。块确定了变量的作用域。变量声明在 {} 内部时,作用域仅限于该代码块。一个块可以嵌套在另一个块中。

  • 不能在嵌套的两个块中声明同名的变量。

  • 使用块(有时称为复合语句)可以在Java程序结构中原本只能放置一条(简单)语句的地方放置多条语句。

public class Test
{
    public static void main(String[] args) throws IOException {

        int n;
        {
            {
                int k;
            }

            {
                int k;//✔
                int n;//✖
            }
        }
    }
}

②条件语句

  • 条件语句的形式if (condition) statement if (condition) statement1 else statement2 其中else部分总是可选的。else子句与最邻近的if构成一组。

③循环

  • 循环形式while (condition) statement

  • 希望循环体至少执行一次:do statement while (condition);

④确定性循环

  • 在for语句的第1部分中声明一个变量之后,这个变量的作用域会扩展到这个for循环体的末尾。如果在for语句内部定义一个变量, 这个变量就不能在循环体之外使用。

⑤多重选择:switch语句(Java14引入)

//Case类类型
//case Circle c 是 模式匹配(Pattern Matching) 的一种用法,
//用于 switch 表达式或 switch 语句中。这个语法允许你在 switch 中直接对对象的类型进行检查,并将其解构到指定的变量中(如 c)。
//Circle c 表示:如果 shape 是 Circle 类型的实例,那么匹配成功,并将这个实例赋值给变量 c。
        String description = switch (shape)
        {
            case Circle c -> "Circle with area: " + c.area();
            case Rectangle r -> "Rectangle with area: " + r.area();
            case Square s -> "Square with area: " + s.area();
            //不需要default子句,因为所有直接子类分支都出现在了case中
        };

⑥中断控制流程的语句

public class Test
{
    public static void main(String[] args) throws IOException {

        //不带标签break
        while(true)
        {
            break;
        }

        //带标签break, 允许跳出多重嵌套的循环
        /*
        0 0
        0 1
        0 2
        2 0
        2 1
        2 2
         */
        for(int i=0;i<3;++i)
        {
            extern:
            for(int j=0;j<3;++j)
            {
                if(i==1) break extern;//执行带标签的break会跳转到带标签的语句块【末尾】
                System.out.println(i+" "+j);
            }
        }
        
        //continue
        while(true)
        {
            continue;
        }
    }
}

9. 大数

  • 如果基本的整数和浮点数精度不足以满足需求, 那么可以使用java.math包中两个很有用的类:Biglnteger和 BigDecimal。这两个类可以处理包含任意长度数字序列的数值。Biginteger类实现任意精度的整数运算,BigDecinal实现任意精度的浮点数运算。

10. 数组

①声明数组

  • 数组是一种数据结构,用来存储同一类型值的集合。通过一个整型索引可以访问数组中的每一个值。

  • 在声明数组变量时,需要指出数组类型(元素类型后面紧跟[])和数组变量名int[] a; 。这条语句只声明了变量a, 井没有将a初始化为一个真正的数组。应该使用new 操作符创建数组int[] a=new int[100] , 声明并初始化了一个可以存储100个整数的数组。

  • 数组长度不要求是常量:new int[n] 会创建一个长度为n的数组。

  • 一旦创建了数组,就不能再改变它的长度(不过,当然可以改变单个数组元素)。

public class Test
{
    public static void main(String[] args) throws IOException {
        int[] a={1, 2, 3, 4};//创建数组并提供初始值的简写形式
        a=new int[] {1};//=右边声明了一个匿名数组,可以重新初始化一个数组而无需创建新的变量
        //等价于下面
        int[] niMing={1};
        a=niMing;
        
        
        //java允许长度为0的数组,其与null并不一样
        new Type[0], new Type[]{}
    }
}

②访问数组元素

  • 数组元素从0开始编号。最后一个合法的索引为数组长度减1。

  • 创建一个数字数组时,所有元素都初始化为0, boolean数组的元素会初始化为片false。对象数组的元素则初始化为一个特殊值null, 表示这些元素(还)未存放任何对象。String[] names=new String[3];//会创建一个包含3个字符串的数组,所有字符串为null。a.length获取数组元素个数

③for each循环

  • 增强的for循环形式:for (variable:collection) statement 它将给定变量(variable)设置为集合中的每一个元素,然后执行语句(statement)(当然,也可以是语句块)。collection 表达式必须是一个数组或者是一个实现了Iterable接口的类对象。

④数组拷贝

⑤命令行参数

  • 每一个Java程序都有一个带String arg[]参数的main方法。 这个参数表明main方法将接收一个字符串数组.也就是命令行上指定的参数。

  • java ClassName -g cruel world 命令,args数组将包含以下内容:args[0]:"-g" args[1]:"cruel" args[2]:"world"

⑥数组排序

⑦多维数组

public class Test
{
    public static void main(String[] args) throws IOException {

        int[][] a=new int[3][3];
        int[][] b=//直接初始化, 多维数组本质是一维数组,是数组的数组
                {
                        {1,2,3},
                        {4,5,6},
                };
        if(a[0][0]==b[0][0]) System.out.println(-1);//访问
        
        //for each
        for(int[] row:a)//遍历行
            for(int v:row)//遍历列
                System.out.print(v+" ");
    }
}

⑧不规则数组

  • Java实际上没有多维数组,只有一维数组。多维数组被解释为“数组的数组”。

11. 引用类型

  • 引用是程序访问对象的一种方式。引用的强弱程度直接影响对象是否会被垃圾回收器(GC)回收。Java 提供了以下几种引用类型,从强到弱依次是:

①强引用

  • 默认情况下,任何通过变量直接引用的对象都是强引用

  • 只要强引用存在,垃圾回收器永远不会回收该对象。

  • 对象生命周期受强引用的控制,只有当强引用被置为 null 或超出作用域时,对象才会被回收。

Object obj = new Object(); // obj 是对对象的强引用
只要 obj 存在,对应的 Object 对象就不会被回收,即使内存不足,GC 也不会回收它。

②软引用

  • 对象具有软引用时,当内存不足时,垃圾回收器会考虑回收该对象。

  • 常用于缓存场景,避免内存溢出。

  • 如果内存充足,软引用对象不会被回收。

Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj);

obj = null; // 去掉强引用
// softRef 指向的对象现在可以在内存不足时被回收

③弱引用

  • 对象具有弱引用时,只要垃圾回收器运行,不管内存是否充足,该对象都会被回收。

  • 通常用于弱键关联数据或防止内存泄漏。

Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);

obj = null; // 去掉强引用
// weakRef 指向的对象现在会在下一次 GC 时被回收

④虚引用

  • 虚引用比弱引用更弱,无法通过虚引用获取对象实例

  • 虚引用的唯一作用是跟踪对象被垃圾回收的时间点。

  • 用于监控对象的回收,常见于资源释放和内存管理的场景。需要配合 ReferenceQueue 使用。

Object obj = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);

obj = null; // 去掉强引用
// phantomRef 的对象会在回收后加入队列 queue

二. 对象与类

1. 面向对象程序设计概述

①类

  • 类指定了如何构造对象。由一个类构造对象的过程称为创建这个类的一个实例。

  • 封装是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现细节。对象中的数据称为实例字段, 操作数据的过程称为方法。Java提供了一个超类,名为Object, 所有其他类都扩展(继承)自这个Object类。

②对象

  • 类的实例。

③类之间的关系

2. 使用预定义类

  • 使用构造器(或称构造函数)构造新实例。构造器是一种特殊的方法,其作用是构造并初始化对象。

  • 构造器总是与类同名。因此,Date类的构造器就名为Date。要想构造一个Date对象,需要在构造器前面加上new操作符,如下所示:new Date()

3. 自定义类

①形式

public class ClassName {
//默认:仅限当前包内的类访问(包级私有)
    // 成员变量(属性)
    type variableName;

    // 构造方法
    public ClassName() {
        // 构造方法的主体
    }

    // 成员方法(行为)
    public void methodName() {
        // 方法的主体
    }

    // 主方法(程序入口)
    public static void main(String[] args) {
        // 创建对象并调用类中的方法
        ClassName obj = new ClassName();
        obj.methodName();
    }
}

②构造器

③隐式参数与显示参数

  • 显式参数:通过方法定义中的参数列表显式声明的参数。调用方法时,需要传递这些参数的实际值。

  • 隐式参数:隐式传递给方法,通常指 this 引用,指向当前对象。

④访问权限

⑤同文件多个类

  • 可以有多个类:一个 Java 文件中可以有多个类,public 类和非 public 类都可以共存。

  • 文件名规则:如果文件中包含 public 类,文件名必须与该 public 类的类名相同。其他非 public 类的文件名没有限制。

  • 编译与访问:每个类会被单独编译成 .class 文件,并且可以在同一包内互相访问。

4. 静态字段与静态方法(static)

  • 可以调用静态方而不需要任何对象,也可以对象调用,ClassName|对象.属性 ClassName|对象.方法。非static则需要对象调用对象.属性 对象.方法

  • static 是类级别的,无局部静态变量,常量。

①静态字段

  • 如果将一个字段定义为static, 那么这个字段并不出现在每个类的对象中。每个静态字段只有一个副本。可以认为静态字段属于类,而不属于单个对象。

②静态常量

  • public static final double PI=3.14; 类名|对象名.属性名

  • 如果省略关键字static, 那么PI就变成了Math类的一个实例字段。也就是说.需要通过Math类的一个对象来访问PI, 并且每一个Math对象都有它白己的一个PI副本。

③静态方法

  • public static 方法名()

5. 方法参数

①值调用|引用调用

  • 按值调用:表示方法接收的是调用者提供的值。

  • 按引用调用:表示方法接收的是调用者提供的变量位置。

  • 方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。

  • Java程序设计语言总是采用按值调用。也就是说,方法会得到所有参数值的一个副本。方法不能修改传递给它的任何参数变量的内容。

  • 方法得到的是对象引用的副本,原来的对象引用和这个副本都引用同一个对象。

②可变参数

  • 可变参数允许方法接受可变数量的参数,方法的参数个数可以不固定。其本质是一个数组,方法内部会将传入的参数作为数组处理。

  • 可变参数必须是方法的最后一个参数:public void methodName(DataType... args)

  • 可变参数在重载方法中使用时,需要注意避免歧义。如果某个方法同时存在普通参数版本可变参数版本,编译器会优先选择匹配度更高的方法(普通参数版本)。

  • 允许将数组作为最后一个参数传递给有可变参数的方法。

③重载

  • 重载是Java中的一种多态性实现方式,指在同一个类中定义多个方法,这些方法具有相同的名称,但参数列表(参数的类型、个数、顺序)不同。

  • 重载仅与方法的参数列表有关,与返回值类型方法修饰符无关。

6. 对象构造

①默认字段初始化

  • 类的字段(也叫成员变量)有默认的初始化值,这些值根据字段的数据类型而定。当你没有显式地对字段进行初始化时,Java 会自动为其赋予一个默认值。默认初始化主要适用于实例变量静态变量。然而,局部变量没有默认值,必须在使用之前显式初始化,否则会导致编译错误,其是一个未知值

②无参数的构造器

  • 如果你写的类没有构造器,就会为你提供一个公有的无参数构造器。 这个构造器将所有的实例 字段设置为相应的默认值。

  • 如果类中提供了至少一个构造器,但是没有提供无参数构造器,那么构造对象时就必须 提供参数,否则就是不合法的。

③显式字段初始化

  • 通过重载类的构造器方法,可以采用多种形式设置类实例字段的初始状态c 不论调用哪 个构造器,每个实例字段都要设置为一个有意义的初始值。

  • 可以在类定义中直接为任何字段赋值。class Peo{String name=""; } 在执行构造器之前完成这个赋值。

④初始化块

  • 初始化实例字段的方法:在构造器中设置值、在声明中赋值、初始化块。

  • 初始化块:在一个类的声明中,可以包含任意的代码块:。

  • 三种初始化方式的的执行顺序声明初始化(声明顺序)、实例初始化块、构造器

  • 类型:实例初始化块、静态初始化块

public class Test
{
    int a;

    /*
        实例初始化块
        定义:使用 {} 包围的代码块,没有任何修饰符。
        执行时机:每次创建对象时,先于构造器执行。
        作用:用于初始化实例变量,或执行一些构造器中需要共享的初始化逻辑。
    */
    {
        a=1;
    }
}
public class Test
{
    static int a;

    /*
        静态初始化块
        定义:使用 static {} 包围的代码块。
        执行时机:类加载时(仅在类加载时执行一次)。
        作用:用于初始化静态变量,或者在类加载时执行一次性操作。
    */
    static
    {
        a=1;
    }
}

⑤参数名

  • 参数变量会遮蔽同名的实例字段。例如,如果将参数命名为slary, 那么slary将指示这个参数,而不是实例字段。 但是,还是可以用this.salary访问实例字段。this.slary=slary

⑥调用另一个构造器

public class Test
{
    Test()
    {
        this(0);//调用另一个构造器
    }
    
    Test(int x)
    {
        
    }
    
    public static void main(String[] args) throws IOException {
        
    }
}

⑦对象析构与finalize方法

  • 在析构器中,最常见的操作是回收分配 给对象的存储空间)由于Java会完成自动的垃圾回收,不需要人工回收内存,所以Java不 支持析构器。

  • 某些对象使用了内存之外的其他资源,在这种情况下,当资源不再需要时,将其回收和再利用就十分重要。如果一个资源一旦使用完就需要立即关闭,那么应当提供一个Hose方法来完成必要的 清理工作:可以在对象使用完时调用这个close方法。

  • 如果可以等到虚拟机退出,那么可以用方法Runtime.addShutdownHook增加一个“关闭钩”。

  • 在 Java 9中,可以使用Cleaner类注册一个动作,当对象不再可达时(除了清洁器还能访问,其他对象都无法访问这个对象),就会完成这个动作。

  • 不要使用finalize方法来完成清理。这个方法原本要在垃圾回收器清理对象之 前调用。不过,你并不能知道这个方法到底什么时候调用,而且该方法已经被废弃

7. 包

  • Java允许使用包将类组织在一个集合中。默认导入java.lang包。

①包名

  • 使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地提供了Employee类,只要他们将自已的类放置在不同的包中,就不会产生冲突。事实上,为了保证包名的绝对唯一性,可以使用一个因特网域名(这显然是唯一的)以逆序的形式作为包名.然后对于不同的项目使用不同的子包。

②类的导入

  • 一个类可以使用所属包(这个类所在的包)中的所有类,以及其他包中的公共类。

  • 可以采用两种方式访问另一个包中的公共类。第一种方式是使用完全限定名, 也就是包名后面跟着类名。java.time.LocalDateTime now = java.time.LocalDateTime.now();

  • 更常用的方式是使用import语句。一旦增加了import语句,在使用类时,就不必写出类的全名了。一旦增加了import语句,在使用类时,就不必写出类的全名了。import java.time.*;导入time包所有类 import java.time.LocalDate;导入特定类

  • 只能使用星号(*)导入一个包,不能使用import java.*导入多个包,因为不同包可能有相同类名。若导入的两个包有冲突类名,例如java.util和java.sql都有Date类,只需增加一个import导入使用的类即可:import java.util.*; import java.sql.*; import java.util.Date; 。若两个类都需使用,则使用时在每个类名前加完整包名。

③静态导入

  • 有一种import语句允许导入静态方法和静态字段,而不只是类。import static java.lang.System.*;可以使用System类的静态方法和静态字段.而不必加类名前缀。

④在包中增加类

  • 要想将类放入包中,就必须将包名放在源文件的开头,即放在定义这个包中各个类的代码之前,若无package语句,则属于无包类。package com.wc; public class ClassName {}

⑤包访问

  • 标记为public的部分可以由任意类使用;标记为private的部分只能由定义它们的类使用。如果没有指定public或private, 这个部分(类、方法或变量)可以由同一个包中的所有方法访问。

⑥类路径

  • 类存储在文件系统的子目录中。类的路径必须与包名匹配。

  • 类文件也可以存储在JAR(Java归档)文件中。在一个JAR文件中,可以包含多个压缩格式的类文件和子目录,这样既可以节省空间又可以改善性能。在程序中用到第三方的库时, 你通常会得到一个或多个需要包含的JAR文件。

⑦设置类路径

8. JAR文件

  • 在将应用程序打包时,你希望只向用户提供一个单独的文件,而不是一个包含大量类文件的目录结构,JaVa归档(JAR)文件就是为此目的而设计的。JAR文件既可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。此外,JAR文件是压缩的,它使用了我们熟悉的ZIP压缩格式。

①创建JAR文件

  • jar工具制作JAR文件(在默认的JDK安装中,这个工具位于jdk/bin目录下)。

  • 命令:jar options file1 file2...

②清单文件

  • 每个JAR文件还包含一个清单文件, 用于描述归档文件的特殊特性中。清单文件被命名为MANIFEST.MF, 它位于JAR 文件的一个特殊的META-INF子目录中。

③可执行JAR文件

image-mcsg.png

④多版本JAR文件

9. 文档注释

  • JDK包含一个工具,叫作javadoc, 它可以由源文件生成一个HTML文档。

  • 如果在源代码中添加以特殊定界符/**开始的注释,可以很容易地生成一个具有专业水准的文档。这是一种很好的方法,因为这样可以将代码与注释放在一个地方(分开放可能出现不一致,修改注释只需重新运行javadoc)。

①注释的插入

②类注释

  • 类注释必须放在import语句之后, class定义之前。

/**
 * 我是 类注释
 */
public class Test
{
    public static void main(String[] args)
    {
        

    }
}

③方法注释

④字段注释

  • 只需要对公共字段(通常指的是静态常量》增加文档注释。

    /**
     * 
     */
    public static final int HERTS=1;

⑤通用注释

⑥包注释

⑦注释提取

10. 继承

  • 继承的基本思想是,可以基于已有的类创建新的类。继承已存在的类就是复用(继承)这些类的方法,而且可以增加一些新的方法和字段,使新类能够适应新的情况。

①定义子类

  • 关键字extends表示继承。

  • 关键字extends指示正在构造的新类派生于一个已存在的类。这个已存在的类称为超类、基类或父类;新类称为子类或派生类。

  • Java语言规范指出:"声明为私有的类成员不会被这个类的子类继承", 实际上是子类无法直接访问父类的私有字段/方法"。public、protect、默认的字段/方法都可以被继承。

  • 构造方法不会被继承。子类如果没有显式定义构造方法,则会默认调用父类的无参构造方法。如果父类没有无参构造方法,子类必须显式调用父类的其他构造方法。

  • 子类可以继承父类的方法,但是子类可以改变这些方法的访问权限,不能使方法的访问权限更严格。

  • 子类覆盖超类方法的方法,而这个超类方法没有抛出异常,就必须捕获你的方法代码中出现的每一个检查型异常。 子类的throws列表中不允许出现超类方法中未列出的异常类。

class Parent
{
    private double bonus;
    
    public void setBonus(double bonus)
    {
        this.bonus = bonus;
    }

    public double getBonus()
    {
        return bonus;
    }
}

class Child extends Parent
{
  
}

②覆盖方法

  • 超类中的有些方法对子类并不一定适用。需要提供一个新的方法来覆盖 (override) 超类中的这个方法。

  • super只是一个指示编译器谓用超类方法的特殊关健字。与this不同, this指示自身对象。

class Parent
{
    private int slary;
    public double bonus;
 
    public double getSlary()
    {
        return slary;
    }
}

class Child extends Parent
{
    @Override
    public double getSlary()
    {
        //double slary=getSlary();
        // 会导致自身无限调用,slary是private必须使用父类公共方法调用
        double slary = super.getSlary();
        return slary+bonus;
    }
}

③子类构造器

  • 使用super调用构造器的语句必须是子类构造器的第一条语句。如果构造子类对象时没有显式地调用超类的构造器,那么超类必须有一个无参数构造器。这个构造器要在子类构造之前调用。

  • this两个含义:一是指示隐式参数的引用,二是调用该类的其他构造器。

  • super关键字两个含义:一是调用超类的方法,二是调用越类的构造器。

  • 调用构造器的语句只能作为另一个构造器的第一条语句出现。

  • 由于子类的构造器不能直接访问父类私有字段,所以必须使用super调用父类构造器,去初始化父类私有字段。

class Parent
{
    private int slary;
    public double bonus;

    public Parent(int slary, double bonus)
    {
        this.slary = slary;
        this.bonus = bonus;
    }
}

class Child extends Parent
{
    public Child(int slary, double bonus)
    {
        super(slary, bonus);
    }
}

④多态

  • 多态:允许我们通过统一的接口操作不同类型的对象,而实际运行时调用的方法由对象的实际类型决定。

⑤理解方法调用

⑥阻止继承:final类和方法

  • :一4③

final class Parent//该类不允许被继承
{
    private final int slary;//常量,不允许被修改
    public double bonus;

    public Parent(int slary, double bonus)
    {
        this.slary = slary;
        this.bonus = bonus;
    }

    public final int getSlary()//子类不能覆盖这个方法
    {
        return slary;
    }
}

⑦强制类型转换

  • 对象引用的强制类型转换,转换语法与数值表达式的强制类型转换类似。用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前。

  • 只能在继承层次结构内进行强制类型转换。

  • 将超类强制转换成子类之前,应该使用instanceof进行检查(只看实际超类里面引用的类型)。

  • 将一个超类对象强制转换为子类对象称为向下转型。这种操作需要特别小心,只有当超类引用实际指向的是子类对象时,才能进行安全的向下转型,否则会抛出运行时异常 ClassCastException。

  • 将子类对象强制转换为其超类对象称为向上转型。这种操作是安全的,也是常用的,因为子类是超类的扩展,超类可以表示其所有子类。向上转型是隐式的,通常不需要显式强制转换。

if(object instanceof ClassName)
{
  ...
}
如果 object 是 ClassName或其子类的实例,返回 true,否则返回 false。
如果 object 为 null,instanceof 会直接返回 false。
//////

public class Test
{
    public static void main(String[] args)
    {
        Parent p = new Parent();
        Child c = new Child();
        if(c instanceof Parent)
        {
            Parent p2=(Parent)c;
            p2.getName();
        }
        
        //Java16中,可以直接在instanceof测试中声明子类变量
        if(c instanceof Parent p2)//若c是Parent的一个实例,将p2设置为c, 并作为Parent
        {//若不是instanceof就返回false
            p2.getName();
        }
    }
}

class Parent
{
    public String getName()
    {
        return "Parent";
    }
}

class Child extends Parent
{
    
}

⑧Object:所有类的超类

  • Object类是Java中所有类的始祖,Java中的每一个类都扩展了Object。如果没有明确地指出超类,那么理所当然Object就是这个类的超类。


  • Object类型的变量

  • 可以使用Object类型的变量引用任何类型的对象。

  • 只有基本类型不是对象, 所有的数组类型(不管是对象数组还是基本类型的数组)都扩展了Object类的类类型。

        Object obj;
        obj=new Parent();
        obj=new int[10];
        obj=new Parent[10];

  • equals方法

  • Object类中的equals方法用于检测一个对象是否等于另外一个对象。Object类中实现的equals方法将确定两个对象引用是否相同

  • 基于状态检测对象的相等性:两个对象有相同的状态.则认为这两个对象是相等的。

  • 在子类中定义equals方法时,首先调用超类的equals。如果检测失败,那么对象就不可能相等。如果超类中的字段都相等,则可以继续比较子类中的实例字段。

  • 如果隐式和显式的参数不属于同一个类,equals方法将如何处理

  • image-zvpr.pngimage-vnbe.png


  • hashCode方法hashCode方法

  • 散列码是由对象导出的一个整型值。散列码是没有规律的。如果x和y是两个不同的对象,那么x.hashCode()与 y.hashCode()基本上不会相同。

  • 由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值由对象的存储地址得出。

  • image-xviv.png

  • image-kbvp.png


  • toString方法toString方法

  • toString方法,它会返回一个字符串,表示这个对象的值。

  • image-ufci.png

11. 对象包装器与自动装/拆箱

①对象包装器

②自动装/拆箱

  • 自动装箱:将基本数据类型自动转换为其对应的包装类对象的过程。

  • 自动拆箱:将包装类对象自动转换为其对应的基本数据类型的过程。

  • 常见操作:赋值、表达式(先将封装类型拆箱操作后(+-...), 再装箱)...

  • 比较包装类对象时,使用 equals 而非 ==。

  • 如果在一个条件表达式中混合使用Integer和Double类型,则Integer值就会拆箱,提升为double, 再装箱为Double。

  • 装箱和拆箱是编译器要做的工作,而不是虚拟机。编译器生成类的字节码时会插入必要的方法调用。虚拟机只是执行这些字节码。

  • 可以将某些基本方法放在包装器中,这会很方便,例如将一个数字字符申转换成数值。

  • -

12. 抽象类

  • 抽象类是通过关键字abstract声明的类,表示它不能直接实例化。抽象类通常作为父类,用于定义通用的行为或接口,而具体的子类负责实现其细节。

  • 抽象类无法创建对象,必须由子类继承并实现其抽象方法后才能实例化。仍然可以创建一个抽象类的对象变量,但是这样一个变量只能引用非抽象子类的对象。

  • 包含一个或多个抽象方法的类本身必须被声明为抽象的。

  • 除了抽象方法之外,抽象类还可以包含字段和具体方法。

  • 抽象方法相当于子类中实现的具体方法的占位符,没有方法体(实现),由子类实现。扩展一个抽象类时,可以有两种选择。一种是在子类中保留抽象类中的部分或所有抽象方法仍未定义,这样就必须将子类也标记为抽象类;另一种做法是定义全部方法,这样一来,子类就不再是抽象的。

  • 抽象类可以包含成员变量、静态方法、静态变量等。

  • 抽象类也可以定义构造方法,但只能在子类中通过 super 调用。

abstract class Person
{
    private String name;
    public static final int AGE=18;
    
    public Person(String name)
    {
        this.name=name;
    }

    public abstract int getAge();
    
    public String getName()
    {
        return name;
    }
}

13. 内部类

  • Java中的嵌套类是指定义在另一个类内部的类。

  • 在 Java 中,外部类(即顶层类)不能直接声明为 staticstatic 只能用在内部类(即定义在类内部的类)上。一个类如果是顶层类,它的实例是与类的实例相关联的,所以顶层类不能是静态的。

①静态内部类

  • 是定义在外部类内部的静态类,使用 static 关键字修饰。

  • 可以直接访问外部类的静态成员,但不能直接访问非静态成员。

  • 静态嵌套类的实例化与外部类的实例无关。

  • 与常规内部类不同,静态内部类可以有静态字段和方法。

  • 只要内部类不需要访问外部类对象,就应该使用静态内部类。

  • 在接口中声明的内部类自动是static和public。

  • 类中声明的接口、记录和枚举都自动为static。

  • 使用静态内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外部类对象的一个引用。

class Outer {
    static int x = 10;

    static class Nested {
        void display() {
            System.out.println("x = " + x);  // 访问外部类的静态成员
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Outer.Nested nested = new Outer.Nested();
        nested.display();
    }
}

②非静态内部类

  • 没有使用 static 关键字,依赖于外部类的实例。

  • 可以访问外部类的所有成员(包括私有成员)。

  • 每个非静态嵌套类的实例都与外部类的实例相关联。

class Outer {
    private int x = 10;

    class Inner {
        void display() {
            System.out.println("x = " + x);  // 访问外部类的实例成员
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.display();
    }
}

③局部内部类

  • 定义在方法或构造函数内部的类,作用范围仅限于该方法或构造函数。

  • 声明局部类时不能有访问说明符。

  • 不能定义为 static,并且不能访问外部类的实例变量,除非它们是 final 或等效于 final

class Outer {
    void display() {
        class Local {
            void show() {
                System.out.println("Inside Local Class");
            }
        }
        Local local = new Local();
        local.show();
    }
}

public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.display();
    }
}

④匿名内部类

  • 没有类名的类,是对某个类的简短实现,通常用于事件监听器等地方。

  • 匿名类可以实现接口或继承类。

interface Greeting {
    void greet();
}

public class Main {
    public static void main(String[] args) {
        Greeting greeting = new Greeting() {
            @Override
            public void greet() {
                System.out.println("Hello, World!");
            }
        };
        greeting.greet();
    }
}

⑤内部类访问权限

  • (无论什么类型)内部类可以直接访问外部类的私有字段,即使这些字段是 private 修饰的。这是因为内部类本质上是外部类的一个成员,它能够访问外部类的所有成员(包括私有成员)。

  • 内部类的对象总有一个隐式引用,指向创建它的外部类对象(所以可以访问外部)。外部类的引用在构造器中设置口 编译器会修改所有的内部类构造器,添加一个对应外部 类引用的参数。

  • 静态内部类:只能访问外部类的 static 成员,不能直接访问外部类的非静态成员(包括 private)。如果需要访问外部类的非静态成员,必须通过外部类的实例。Java 外部类可以访问静态内部类。静态内部类本质上是外部类的一个静态成员,通过类名直接访问(推荐),通过外部类的实例访问(不常用,静态内部类不依赖外部实例)。

  • 非静态内部类:可以访问外部类的所有成员,包括 private 字段和方法。内部类需要通过实例才能被外部类的其他部分访问

  • 外部类不能直接访问局部内部类和匿名内部类因为它们的作用域受到限制,只能在它们被定义的代码块中访问。

  • 局部内部类:只能在方法或构造函数内部使用,可以访问外部类的所有成员(包括 private)。可以访问定义它的作用域中的局部变量,但这些变量必须是 有效 final 的。局部内部类是定义在方法或代码块中的类,其作用域仅限于该方法或代码块。局部内部类只能在定义它的方法或代码块内访问。不能被外部类直接访问:因为局部内部类不是外部类的成员,而是局部变量的形式存在。

  • 匿名内部类匿名内部类是没有名字的内部类,通常用于简化接口或抽象类的实现。可以访问外部类的所有成员(包括 private)。可以访问定义它的作用域中的局部变量,但这些变量必须是 有效 final 的。匿名内部类必须定义在代码块中,并且在定义时创建实例。不能被外部类直接访问:因为匿名内部类没有名字,无法在定义之外引用它。

14. 枚举类

  • 枚举类是一种特殊的类,用于表示一组固定的常量。它提供了比普通常量更强大的功能,比如可以为每个枚举值添加字段、方法以及实现接口。

  • 线程安全:枚举类型在 Java 中是 自动线程安全 的。

  • 防止反序列化:自带序列化机制,还未防止多次实例化问题提供了坚实保证,再复杂的序列化或反射攻击不用担心。枚举类型在反序列化时不会创建新的实例。

  • 每个枚举值都是枚举类型的实例。

  • 枚举类隐式继承 java.lang.Enum,无法再继承其他类,但可以实现接口。

  • 可以像类一样定义构造方法、字段和方法。

  • 枚举的构造器总是(默认)私有的。可以省略private修饰符。如果声明一个enum构造器为public或protected, 则会出现语法错误。不可能构造新的对象。在比较枚举类型的值时.并不需要使用equals, 可以直接使用==来比较。

  • Java 默认会为所有枚举类型提供一个 toString() 方法。枚举类型的 toString() 方法默认返回枚举常量的名称,即 enum 常量的标识符字符串。

public enum Day {
    MONDAY("工作日"), 
    SATURDAY("周末"), 
    SUNDAY("周末");

    private final String description; // 字段

    // 构造方法
    Day(String description) {
        this.description = description;
    }

    // Getter 方法
    public String getDescription() {
        return description;
    }
}


public class EnumTest {
    public static void main(String[] args) {
        Day today = Day.MONDAY;
        System.out.println(today); // 输出: MONDAY
        System.out.println(today.getDescription()); // 输出: 工作日
    }
}

15. 记录类

  • 记录:记录是Java14引入的一种特殊类,用于简化不可变数据类的定义。旨在减少样板代码,并专注于数据的存储和访问。

  • 定义:使用关键字 record 定义

  • 简化数据类:

  • 自动生成构造器(构造一个对象,字段按定义顺序初始化)

  • getter 方法(方法名与字段同名)

  • equals(按字段值比较两个记录实例

  • hashCode (基于字段值生成哈希值)

  • toString (返回类名及字段名-值对)方法。对于这些自动提供的方法,也可以定义你且已的版本,只要它们有相同的参数和返回类型。

  • 记录默认 final,不能被继承。记录类默认继承 java.lang.Record

  • 字段默认 private final,不可变,但是可以是可变对象的引用。不能为记录增加实例字段

  • 记录可以有静态字段和方法。可以为记录增加自己的方法。不支持自定义 setter 方法。

  • 标准构造器:自动定义地设置所有实例字段的构造器称为标准构造器。

  • 记录支持自定义构造器,但必须调用已有的标准构造器。这种构造器的第一个语句必须调用另一个构造器,所以最终会调用标准构造器(全字段)。

  • 简洁标准构造器:不用指定参数列表

public class Test
{
    public static void main(String[] args)
    {
        Point p = new Point(10, 20);
        System.out.println(p.x()); // 访问字段: 输出 10
        System.out.println(p.y()); // 访问字段: 输出 20
        System.out.println(p);// 自动生成的 toString 方法: 输出 Point[x=10, y=20]
        //p.x=10;✖,默认private final不可改变
    }
}

record Point(int x, int y)//x和y是通过记录参数隐式定义的字段
{
//    private int z;✖,不能为其增加实例字段
//    private final int g;✖,不能为其增加实例字段

    public Point//自定义简洁标准构造器,等同Point(int x, int y)
    {
        System.out.println("Point");
    }

    public Point(int x)
    {
        this(x, 0);//必须最终调用标准构造器,否则报错
    }

    //静态字段和静态方法
    public static final int z=0;
    public static int getZ()
    {
        return z;
    }

    //自定义方法
    public int getSum()
    {
        return x+y+z;
    }
}

//等价于
//public final class Point {
//    private final int x;
//    private final int y;
//
//    public Point(int x, int y) {
//        this.x = x;
//        this.y = y;
//    }
//
//    public int x() {
//        return x;
//    }
//
//    public int y() {
//        return y;
//    }
//
//    @Override
//    public boolean equals(Object o) {
//        // 自动生成
//    }
//
//    @Override
//    public int hashCode() {
//        // 自动生成
//    }
//
//    @Override
//    public String toString() {
//        return "Point[x=" + x + ", y=" + y + "]";
//    }
//}

16. 密封类

  • 密封类是Java15引入的一种特性(正式版在 Java 17 中推出)。密封类允许我们显式控制哪些类可以继承或实现它,用于限制继承层次,增强代码的可读性和安全性。

  • 密封类通过sealed关键字声明,同时需要用permits指定可以继承它的子类。它的子类必须是以下三种之一:

  • 密封类sealed):继续限制继承范围。非密封类non-sealed):解除限制,允许自由继承。最终类final):不能再被继承。

  • 一个密封类允许的子类必须是可访问的。它们不能是嵌套在另一个类中的私有类,也不能是位于另一个包中的包可见的类。

//示例
// 定义密封类
public sealed class Shape permits Circle, Rectangle {
    public abstract double area();
}

// 定义最终类 Circle
public final class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

// 定义密封类 Rectangle
public sealed class Rectangle extends Shape permits Square {
    private final double length, width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    public double area() {
        return length * width;
    }
}

// 定义非密封类 Square
public non-sealed class Square extends Rectangle {
    public Square(double side) {
        super(side, side);
    }
}
//使用密封类与 switch 结合
//使用密封类的一个重要原因是编译时检查。
//编译器会检查是否覆盖所有可能的子类(密封类提供了完整性保障)。
//如果新增子类未处理,编译器会报错。
public class Main
{
    public static void main(String[] args) 
    {
        Shape shape = new Circle(5);

//case Circle c 是 模式匹配(Pattern Matching) 的一种用法,
//用于 switch 表达式或 switch 语句中。这个语法允许你在 switch 中直接对对象的类型进行检查,并将其解构到指定的变量中(如 c)。
//Circle c 表示:如果 shape 是 Circle 类型的实例,那么匹配成功,并将这个实例赋值给变量 c。
        String description = switch (shape)
        {
            case Circle c -> "Circle with area: " + c.area();
            case Rectangle r -> "Rectangle with area: " + r.area();
            case Square s -> "Square with area: " + s.area();
            //不需要default子句,因为所有直接子类分支都出现在了case中
        };

        System.out.println(description);
    }
}

三. 接口和lambda表达式

1. 接口

①接口的概念

  • 接口是一种抽象类型,用于定义一组方法的规范,而无需提供具体的实现。接口为类提供了一种实现多重继承的方式。

  • 接口中的方法默认abstract 和 public 的(Java 8 之前),在实现接口时,必须把方法声明为public(否则会改变默认pub权限)。Java 9 引入,可以在接口中定义 private方法(仅供接口内部使用)。Java 8 允许接口定义静态方法和可以在接口中提供方法的默认实现

  • 接口绝不会有实例字段,接口中的字段默认是 public static final

  • 类通过 implements 关键字实现接口中的方法。

  • 使用接口的主要原因在于

  • 定义行为规范:Java程序设计语言是一种强类型语言,在编译器可以检查。接口是一种契约,规定了实现类必须具备的行为,而不关心具体实现。这种抽象化的设计使得代码更具一致性和可维护性。

  • 多继承:Java 不支持类的多继承,但允许类实现多个接口。这种设计避免了传统多继承带来的菱形继承问题,同时保留了多继承的灵活性。

  • 面向接口编程:接口支持面向接口编程,这是一种解耦的设计思想。调用方不关心对象的具体实现,只关注接口提供的功能。

  • 提高代码的扩展性:接口允许开发者通过新增实现类的方式扩展功能,而不需要修改现有代码。这种设计使得系统更加灵活,易于扩展。

  • 支持多态:接口的引用类型可以指向任何实现了该接口的对象,从而实现多态。这种特性使得代码更加灵活和通用。

  • 解耦和团队协作:接口可以帮助团队在开发初期就定义好模块的行为契约,避免因为实现细节不明确而导致的耦合。

  • 支持函数式编程:Java 8 中引入了函数式接口(Functional Interface),这种接口中只有一个抽象方法。结合 Lambda 表达式,可以大大简化代码。

  • -

  • 接口不是类,不能new操作符实例化一个接口。但仍能声明接口变量,其必须引用实现了这个接口的一个类对象。

  • 可以使用instanceof检查一个对象是否实现了某个特定的接口。

  • 可以扩展接口,允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口。

  • 记录和枚举类不能扩展其他类(因为它们隐式地扩展了Record和Enum类)不过,它们可以实现接口。

  • 接口可以是密封的(sealed,)。与密封类一样,直接子类型(可以是类或接口)必须在permits子句中声明,或者要放在同一个源文件中。

  • -

interface Animal
{
    // 常量
    String TYPE = "Living Being"; // 等同于 public static final String TYPE = "Living Being";

    // 抽象方法(默认是 public abstract)
    void eat();

    // 默认方法,  实现类可以选择重写或直接使用默认方法
    default void sleep()
    {
        System.out.println("Animal is sleeping");
    }

    //可以在接口中定义静态方法,并通过接口名调用
    static void showType()
    {
        System.out.println("This is an animal");
    }

    //私有方法,用于接口内部复用逻辑,不能在实现类中访问。
    private void helperMethod()
    {
        System.out.println("Helper method");
    }

    default void useHelper()
    {
        helperMethod();
        System.out.println("Using helper");
    }
}

②解决默认方法冲突

  • 如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,若一个类实现这些类或接口会产生同名冲突。

  • 解决规则

  • 超类优先:如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。

  • 接口冲突:如果一个接口提供了一个默认方法.另一个接口提供了一个同名而且参数类型相同的方法(不论是否是默认方法),必须覆盖(不一定非调用某个默认方法)这个方法来解决冲突。若两个都没有提供默认方法,则不存在冲突。

public class Test
{
    public static void main(String[] args)
    {
        MyClass myClass=new MyClass();
        myClass.doSomething();//A's method, 类优先
    }
}


//类和接口同名方法
class A
{
    public void doSomething()
    {
        System.out.println("A's method");
    }
}

interface B
{
    default void doSomething()
    {
        System.out.println("B's method");
    }
}

class MyClass extends A implements B
{

}
//接口冲突
interface A
{
    default void doSomething()
    {
        System.out.println("A's method");
    }
}

interface B
{
    void doSomething();
}

class MyClass implements A, B
{
    @Override
    public void doSomething()//只要覆盖即可
    {
        // 明确调用某个接口的方法,或者不选
        // A.super.doSomething();
    }
}

③Comparable和Compamtor接口

//Comparable
public class Person implements Comparable<Person> {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

  
     /**
     * 当前对象小于o时返回负数
     * 等于o时返回0
     * 大于o时返回正数
     */
    // 定义自然顺序:按年龄升序排序
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }

    @Override
    public String toString() {
        return name + ": " + age;
    }
}

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        // 自然顺序排序
        Collections.sort(people);
        System.out.println(people);//[Bob: 25, Alice: 30, Charlie: 35]
    }
}
//Comparator
class Person implements Comparable<Person>
{
    private String name;
    private int age;

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

    public String getName()
    {
        return name;
    }

    public int getAge()
    {
        return age;
    }

    // 定义自然顺序:按年龄升序排序
    @Override
    public int compareTo(Person other)
    {
        return Integer.compare(this.age, other.age);
    }

    @Override
    public String toString()
    {
        return name + ": " + age;
    }
}

//按名字长度排序
class LengthComparator implements Comparator<Person>
{   //o1<o2返回正数,o1==o2返回0, o1>o2返回正数
    public int compare(Person o1, Person o2)
    {
        return Integer.compare(o2.getName().length(), o1.getName().length());
    }
}

public class Test
{
    public static void main(String[] args)
    {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        // 自然顺序(按年龄)
        Collections.sort(people);//Comparable
        System.out.println("By age: " + people);//By age: [Bob: 25, Alice: 30, Charlie: 35]

        // 自定义排序规则(按姓名), 匿名内部类
        Collections.sort(people, new Comparator<Person>()
        {
            @Override
            public int compare(Person p1, Person p2)
            {
                return p1.getName().compareTo(p2.getName());
            }
        });
        System.out.println("By name: " + people);//By name: [Alice: 30, Bob: 25, Charlie: 35]

        //Lambda
        Collections.sort(people, (p1, p2) -> p1.getName().compareTo(p2.getName()));
        people.sort(Comparator.comparing(Person::getName));//List自带排序方法

        //外部Comparator
        Collections.sort(people, new LengthComparator());
        System.out.println("By length: " + people);//By length: [Charlie: 35, Alice: 30, Bob: 25]
    }
}

④对象克隆:Cloneable接口

  • image-vdet.png

  • 浅拷贝:默认的 clone() 是浅拷贝。对象内部的引用字段不会被递归克隆,原对象和克隆对象共享引用字段。

  • 深拷贝:深拷贝需要手动实现,将引用类型字段也克隆。

  • Object 类提供了 clone() 方法,用于创建一个对象的浅拷贝clone() 方法是 native 方法(底层由 JVM 实现),直接复制对象的字段值。clone() 方法受 Cloneable 保护,未实现接口时不可调用。clone() 方法在执行时会检查对象是否实现了 Cloneable 接口。

  • image-iseo.png

  • Cloneable 的局限性:

  • 设计不优雅:clone() 方法直接依赖 Object,与具体类无关;强制所有字段手动处理深拷贝。

  • 共享引用字段可能引发线程安全问题。

  • Java 中更推荐使用拷贝构造器或序列化方式来实现深拷贝。

//浅拷贝
class Address
{
    String city;

    public Address(String city)
    {
        this.city = city;
    }
}

class Person implements Cloneable
{
    String name;
    Address address;

    public Person(String name, Address address)
    {
        this.name = name;
        this.address = address;
    }

    @Override
    public Object clone() throws CloneNotSupportedException
    {
        return super.clone();
    }
}

public class Test
{
    public static void main(String[] args) throws CloneNotSupportedException
    {
        Address address = new Address("New York");
        Person p1 = new Person("Alice", address);
        Person p2 = (Person) p1.clone();
        System.out.println(p1.address == p2.address); // true,共享引用
    }
}
//深拷贝
class Person implements Cloneable
{
    String name;
    Address address;

    public Person(String name, Address address)
    {
        this.name = name;
        this.address = address;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException
    {
        Person cloned = (Person) super.clone();
        cloned.address = (Address) address.clone(); // 深拷贝
        return cloned;
    }
}

class Address implements Cloneable
{
    /**
     *String 是一个特殊的引用类型,它的行为不同于普通的引用类型。
     *虽然 String 是引用类型,但它是 不可变的。在克隆的语境中,这种不可变性使得 String 的行为类似于深拷贝。
     *
     *不可变性定义: 一旦创建,String 对象的内容就不能被修改。任何修改都会生成一个新的String对象。
     * 因此,无论浅拷贝还是深拷贝,String 都是安全的,因为即使多个对象共享同一个 String 实例,也不会影响彼此。
     *
     * 在浅拷贝中,字段直接复制引用值String
     * 由于 String 是不可变的,即使共享引用,p1 和 p2 的 name 内容也不会受到影响,因此无需深拷贝。
     *
     * 深拷贝通常用于 引用类型可变对象 的字段(如自定义类或集合)。
     * 但对 String 来说,其不可变性使得无论是浅拷贝还是深拷贝,行为上都是一样的。
     */
    String city;

    public Address(String city)
    {
        this.city = city;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException
    {
        return super.clone();
    }
}

public class Test
{
    public static void main(String[] args) throws CloneNotSupportedException
    {
        Address address = new Address("New York");
        Person p1 = new Person("Alice", address);
        Person p2 = (Person) p1.clone();
        System.out.println(p1.address == p2.address); // false
    }
}
//拷贝构造器方式实现深拷贝
//拷贝构造器是一种手动实现深拷贝的方式。
//在拷贝构造器中,递归创建对象的所有字段,确保每个引用类型字段都分配了新的内存地址。
class Address
{
    String city;

    public Address(String city)
    {
        this.city = city;
    }

    // 拷贝构造器
    public Address(Address other)
    {
        this.city = other.city; // 对于 String,直接赋值即可,因为 String 是不可变的
    }

    @Override
    public String toString()
    {
        return "Address{city='" + city + "'}";
    }
}

class Person
{
    String name;
    Address address;

    public Person(String name, Address address)
    {
        this.name = name;
        this.address = address;
    }

    // 拷贝构造器
    public Person(Person other)
    {
        this.name = other.name; // String 直接赋值
        this.address = new Address(other.address); // 深拷贝 Address
    }

    @Override
    public String toString()
    {
        return "Person{name='" + name + "', address=" + address + '}';
    }
}

public class Test
{
    public static void main(String[] args) throws CloneNotSupportedException
    {
        Address address = new Address("New York");
        Person person1 = new Person("Alice", address);

        // 使用拷贝构造器实现深拷贝
        Person person2 = new Person(person1);

        System.out.println("Original: " + person1);
        System.out.println("Clone: " + person2);

        // 修改原对象的地址
        person1.address.city = "Los Angeles";

        // 验证拷贝是否独立
        System.out.println("After modification:");
        System.out.println("Original: " + person1);
        System.out.println("Clone: " + person2);
        /*
        Original: Person{name='Alice', address=Address{city='New York'}}
        Clone: Person{name='Alice', address=Address{city='New York'}}
        After modification:
        Original: Person{name='Alice', address=Address{city='Los Angeles'}}
        Clone: Person{name='Alice', address=Address{city='New York'}}
         */
    }
}
//序列化方式实现深拷贝
//将对象序列化为字节流,再反序列化为一个新的对象。
//这种方法可以自动处理对象中的所有字段,包括复杂的嵌套对象,但需要类实现 Serializable 接口。
class Address implements Serializable
{
    String city;

    public Address(String city)
    {
        this.city = city;
    }

    @Override
    public String toString()
    {
        return "Address{city='" + city + "'}";
    }
}

class Person implements Serializable
{
    String name;
    Address address;

    public Person(String name, Address address)
    {
        this.name = name;
        this.address = address;
    }

    @Override
    public String toString()
    {
        return "Person{name='" + name + "', address=" + address + '}';
    }

    // 深拷贝方法:通过序列化实现
    public Person deepCopy()
    {
        try
        {
            // 序列化
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            oos.flush();

            // 反序列化
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (Person) ois.readObject();
        } catch (IOException | ClassNotFoundException e)
        {
            throw new RuntimeException("Deep copy failed", e);
        }
    }
}

public class Test
{
    public static void main(String[] args) throws CloneNotSupportedException
    {
        Address address = new Address("New York");
        Person person1 = new Person("Alice", address);

        // 使用序列化实现深拷贝
        Person person2 = person1.deepCopy();

        System.out.println("Original: " + person1);
        System.out.println("Clone: " + person2);

        // 修改原对象的地址
        person1.address.city = "Los Angeles";

        // 验证拷贝是否独立
        System.out.println("After modification:");
        System.out.println("Original: " + person1);
        System.out.println("Clone: " + person2);
        /*
        Original: Person{name='Alice', address=Address{city='New York'}}
        Clone: Person{name='Alice', address=Address{city='New York'}}
        After modification:
        Original: Person{name='Alice', address=Address{city='Los Angeles'}}
        Clone: Person{name='Alice', address=Address{city='New York'}}
         */
    }
}

2. lambda表达式

①基本语法

  • Lambda表达式:Java8引入的一种简洁表示功能式接口的语法。它允许将功能(代码块)作为参数传递,大大简化了代码的编写,特别是对于匿名内部类的场景。

  • 本质:是一个匿名实现类对象。

  • 如果一个lambda表达式只在某些分支返回一个值,而另外一些分支不返回值,这是不合法的。例如,(int x)->{if(x>=0) return 1}就不合法。

//无参数
@FunctionalInterface
interface Greeting
{
    void sayHello();
}

//带参数
@FunctionalInterface
interface MathOperation
{
    int operate(int a, int b);
}

//单参数无括号
interface Printer
{
    void print(String message);
}

//带参数
public class Test
{
    public static void main(String[] args) throws CloneNotSupportedException
    {
        //使用匿名内部类
        Greeting greeting = new Greeting()
        {
            @Override
            public void sayHello()
            {
                System.out.println("Hello, World!");
            }
        };
        greeting.sayHello();//Hello, World!

        //使用lambda
        greeting = () -> System.out.println("Hello, World2!");
        greeting.sayHello();//Hello, World2!

        ////////////////////////

        //两个参数加法
        MathOperation add = (a, b) -> a + b;
        System.out.println(add.operate(1, 1));//2
        //两个参数减法
        MathOperation sub = (a, b) -> a - b;
        System.out.println(sub.operate(2, 1));//1

        /////////////////////////

        //单参数无括号
        Printer printer = message -> System.out.println("Message is " + message);
        printer.print("Hello, World!");//Message is Hello, World!

        //多行代码需要{}
        printer = message ->
        {
            message+="wcc";
            System.out.println("Message is " + message);
        };
        printer.print("Hello, World2!");//Message is Hello, World2!wcc
    }
}

②函数式接口

  • 函数式接口是指仅包含一个抽象方法的接口。它们可以用于 Lambda 表达式、方法引用以及构造方法引用,是 Java 8 引入的一项核心特性。


  • 定义函数式接口

  • 函数式接口的定义很简单,只需要确保接口中只有一个抽象方法。为了明确接口是函数式接口,可以加上 @FunctionalInterface 注解。虽然这个注解不是必须的,但推荐使用,以便编译器在接口不符合函数式接口的定义时抛出错误。

//无参数和返回值
@FunctionalInterface
interface Greeting {
    void sayHello();
}

public class Main {
    public static void main(String[] args) {
        Greeting greeting = () -> System.out.println("Hello, Lambda!");
        greeting.sayHello();
    }
}
有参数和返回值
@FunctionalInterface
interface MathOperation {
    int operate(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        MathOperation add = (a, b) -> a + b;
        System.out.println("Result: " + add.operate(5, 3)); // 输出 8
    }
}

  • 函数式接口的特性

  • 只能有一个抽象方法

  • 接口中允许有默认方法和静态方法(它们不影响抽象方法的数量)。

  • 允许重写 Object 类中的方法(toString()equals() 等),但这些方法不算作抽象方法。

  • 推荐使用 @FunctionalInterface 注解:防止无意中增加额外的抽象方法破坏函数式接口。


  • 内置函数式接口

  • Java 8 提供了几个常用的函数式接口,位于 java.util.function 包中:

Function 示例:
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> lengthFunction = str -> str.length();
        System.out.println(lengthFunction.apply("Hello")); // 输出 5
    }
}
Consumer 示例:
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        Consumer<String> printer = str -> System.out.println("Hello, " + str);
        printer.accept("World"); // 输出 Hello, World
    }
}
Supplier 示例:
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Supplier<Double> randomSupplier = () -> Math.random();
        System.out.println(randomSupplier.get()); // 输出随机数
    }
}
Predicate 示例:
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isEven = num -> num % 2 == 0;
        System.out.println(isEven.test(4)); // 输出 true
        System.out.println(isEven.test(5)); // 输出 false
    }
}
******
ArrayList<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        //有一个方法removeIf, 参数为Predicate
        //public boolean removeIf(Predicate<? super E> filter)
        list.removeIf(x->x%2==0);//移除偶数
BiFunction 示例:
import java.util.function.BiFunction;

public class Main {
    public static void main(String[] args) {
        BiFunction<String, String, String> concat = (str1, str2) -> str1 + str2;

        String result = concat.apply("Hello, ", "World!");
        System.out.println(result); // 输出: Hello, World!
    }
}
*********
Arrays.sort的一个
public static <T> void sort(T[] a, Comparator<? super T> c)
Comparator是一个BiFunction函数式接口,同当前
Integer[] a = new Integer[10];
Arrays.sort(a, (x, y) -> x - y);

  • 函数式接口的应用场景

  • 配合Lambda表达式: 函数式接口是 Lambda 表达式的载体,任何地方需要传递行为(代码块)时,可以使用函数式接口(某个方法的参数是一个函数式接口,这时可以传一个lambda表达式)。

  • Stream API: 函数式接口广泛用于 Java 的 Stream API,用于过滤、映射、收集等操作。

  • 事件处理: 函数式接口可以用作回调,比如处理按钮点击事件。

③方法引用

  • 方法引用:是 Lambda 表达式的一种简化形式,用于直接引用类或对象的方法(相当于用引用的方法,作为函数接口实例的重写方法)。它可以让代码更简洁和可读。

  • -

  • 隐式参数:隐式参数是指方法需要的参数,由上下文(如流中的元素)自动传递给方法,而不需要显式地在 Lambda 表达式中声明。

  • 在方法引用中,流中的每个元素作为隐式参数传递给实例方法(根据方法的需要)。

  • map() 等流操作中,流中的每个元素自动成为方法的隐式参数。

  • -

  • 四种方法引用类型

  • 静态方法引用:格式:ClassName::staticMethod 。适用于引用类的静态方法。所有参数都传递到静态方法。

  • 实例方法引用(特定对象):格式:instance::instanceMethod 。适用于引用一个特定对象的实例方法。方法引用等价于一个lambda表达式,其参数要传递到方法。

  • 实例方法引用(任意对象):格式:ClassName::instanceMethod 。适用于引用类的实例方法,实例由上下文决定(比如集合中的每个元素)。第1个参数会成为方法的隐式参数。

  • 构造器引用:格式:ClassName::new 。适用于引用构造方法,用于创建对象。

④有效final(事实最终变量)

  • 有效final含义

  • 1. 捕获的(局部)变量在 Lambda 表达式中不能修改(线程安全)。

  • 2. 捕获的(局部)变量在定义之后,外部作用域中不能再次修改(线程安全)。

  • -

  • 闭包特性:Lambda 表达式可以访问外部作用域中的变量(类似于闭包)。在 Java 中,为了确保 Lambda 在不同线程或生命周期中能正常使用这些变量,捕获的变量值需要在 Lambda 内部存储,而不是依赖外部的变量。

  • 对于局部变量(原始值类型):Lambda 表达式会将变量值复制到自己的内部实现中。

  • 对于引用类型:Lambda 表达式捕获引用类型变量时,会存储该引用(而非引用的内容)。如果引用指向的对象的内容发生变化,Lambda 表达式运行时会看到这些变化。

  • -

  • 为什么局部变量需要有效 final?

  • 1. 线程安全:Lambda 表达式可能被延迟执行,例如传递给另一个线程。如果局部变量可以修改,就可能导致线程安全问题。

  • 2. Java 的闭包机制:Lambda 表达式运行时可能超出定义它的方法的作用域,但局部变量是定义在栈上的,方法执行结束后局部变量会被销毁。为避免引用悬空的问题,Java 强制要求局部变量是有效 final,这样可以安全地在 Lambda 中捕获其值。

  • -

  • 为什么类成员变量和静态变量可以修改:

  • 1. 它们的生命周期与对象或类绑定,Lambda 执行时始终有效。

  • 2. 修改它们的线程安全问题由开发者自行处理。

  • -

  • lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

  • 在一个方法中,不能有两个同名的局部变量,因此,lambda表达式中同样也不能有同名的局部变量。

  • 在 Lambda 中,this 指向的是外部类,而匿名类中,this 指向的是匿名类自身

//有效final:如果一个局部变量(包括方法参数)在初始化后没有被修改,就被称为“有效 final”。
//即使没有显式声明为 final,只要变量的值在作用域内没有被重新赋值,编译器会将其视为有效 final。

public class Main {
    public static void main(String[] args) {
        int num = 10; // 有效 final
//这里 num 的值在定义后没有被修改,因此它是“有效 final”。
        Runnable r = () -> System.out.println("Value: " + num);
        r.run();
    }
}
****************************

public class Main {
    public static void main(String[] args) {
        int num = 10;

        Runnable r = () -> System.out.println("Value: " + num);

        num = 20; // 修改变量,导致编译失败Variable used in lambda expression should be final or effectively final
        r.run();
    }
}
***************************
public class Test
{
    public static void main(String[] args, int start) throws CloneNotSupportedException
    {
        ActionListener listener=event->
        {
            start--;//✖,不能修改捕获变量
        };
    }
}
******************
public class Main {
    public static void main(String[] args) {
        Runnable r;
        {
            int num = 10; // 局部变量
            r = () -> System.out.println(num); // Lambda 捕获局部变量
        }

        // num 超出了作用域,被销毁
//Variable used in lambda expression should be final or effectively final
        r.run(); // 如果允许,可能导致不安全的行为
    }
}
*******************
Lambda 表达式可以引用类的成员变量或静态变量,它们不受有效 final 的限制,
可以在 Lambda 表达式中自由使用和修改。:
public class Main {
    private int count = 0;

    public void increment() {
        Runnable r = () -> {
            count++; // 成员变量可以被修改
            System.out.println("Count: " + count);
        };
        r.run();// 在方法之外调用,count 仍然有效
    }

    public static void main(String[] args) {
        new Main().increment();
    }
}
********
public class Main {
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(this); // 输出匿名类实例
            }
        };
        r1.run();

        Runnable r2 = () -> System.out.println(this); // 输出外部类实例
        r2.run();
    }
}

3. 内部接口

4. 注解

①介绍

  • Java注解是Java5引入的一种用于提供元数据的机制注解不会直接影响程序的逻辑(Java编译器对于包含注解和不包含注解的代码会生成相同的虚拟机指令),它们提供了一种声明性的方式来附加信息,通常用于代码的自动生成、编译时检查和运行时反射。

  • 注解是那些插入到源代码中使用其他工具可以对其进行处理的标签。这些工具可以在源码层次上进行操作,或者可以处理编译器在其中放置了注解的类文件。

  • 注解是当作修饰符来使用的,它被置于被注解项之前,中间没有分号,注解是代码的一部分。

②用途

  • 编译时检查:例如,使用 @Override 注解可以确保重写的方法确实是父类的方法。

  • 文档生成:通过 @Documented 注解,注解的信息可以被包括在 Javadoc 中。

  • 框架配置:如 Spring、Hibernate 等框架,广泛使用注解来定义配置和元数据。

  • 代码生成:如 Lombok 插件,使用注解自动生成 getter、setter 方法等代码。

③注解接口

  • 注解是由注解接口来定义的,其本质是一个接口。

  • 所有的注解接口都隐式地扩展自java.lang.annotation.Annotation接口。这个接口是一个常规接口,不是一个注解接口。

  • 你无法扩展注解接口。所有的注解接口都直接扩展自java.lang.annotation.Annotation。你也从来不用为注解接口提供实现类。

  • 注解接口的方法没有参数也没有throws子句,它们不能是default或static方法,也不能有类型参数。

修饰符 @interface AnnotationName
{
    //每个元素声明都有下面形式
    type elementName();
    type elementName(); default value;
}

public @interface Test
{
    enum Status
    {UNCONFIRMED, CONFIRMED, CANCELLED}

    ;

    boolean showStopper() default false;
    Class<?> testCase() default Void.class;
    Reference ref() default @Reference();//一个注解类型
    String[] result();
    String assingedTo() default "[none]";
}

④注解使用规则

  • 因为注解是由编译器计算而来的,所有元素值必须是编译期常量。

  • 一个注解元素永远不能设置为null, 甚至不允许其默认值为null。

//注解形式
@AnnotationName(elementName1=v1, elementName2=v2,...)


@Test(assingedTo="1", showStopper=true)
//与顺序无关
@Test(elementName2=v2, elementName1=v1,...)
//若某个元素值未指定,则使用默认值
//默认值并不是和注解存储在一起的,它们是动态计算而来的。


//标记注解
//如果没有指定元素,要么是因为注解中没有任何元素,
//要么是因为所有元素都使用默认值,那么你就不需要使用圆括号了。
@Test


//单值注解
//如果一个元素具有特殊的名字value, 并且在注解中没
//有指定其他元素,那么你就可以忽略掉这个元素名以及等号
public @interface Test2
{
    String value();
}

@Test2("wcc")

一项可以有多个注解
@Test
@Test2("s")

//如果注解的作者将其声明为可重复的
//那么你就可以多次重复使用同一个注解
@Test2("w")
@Test2("s")


如果元素值是一个数组,那么要将它的值用括号括起来
@Test(..., result={"a", "b"})

如果该元素具有单值,那么可以忽略这些括号
@Test2(..., result="a")

一个注解元素可以是另一个注解,
那么就可以创建出任意复杂的注解。
在注解中引入循环依赖是一种错误, 因为Test具有一个类型注解类型
为Reference的元素,所以Reference就不能再拥有一个类型为Test的元素
@Test(ref=@Reference(id="123"),...)

⑤注解各类声明

  • 声明注解可以出现在下列声明处

  • 包、类(包含enum)、接口(包括注解接口)、方法、构造器、实例域(包含enum常量)、局部变量、参数变量、类型参数。

  • 对于类和接口,需要将注解放置在class和interface关键字前面:@Entity public class User{...}

  • 对于变量,需要将他们放置在类型的前面:@SuppressWarnings("unchecked") List<User> users=...; public User getUser(@Param("id") String userId);

  • 泛化类或方法中的类型参数public class Cache<@Immutable V> {...}

  • 是在文件package-info,java 中注解的,该文件只包含以注解先导的包语句://Package-level Javadoc

    @GPL(version="3")

    package com.horstmann.corejava;

    import org.gnu.GPL;

⑥注解类型用法

  • 声明注解提供了正在被声明的项的相关信息。public User getUser(@NonNull String userId)//断言userId不为空

  • 类型用法注解可以出现在下面的位置

  • 泛化类型参数一起使用:List<@NonNull String>, Comparator<@NonNull String> reverseOrder();

  • 数组中的任何位置:@NonNull String[][] words;//words[i][j]不为空 String @NonNull [][] words;//words不为null String[] @NonNull [] words;//words[i]不为空

  • 超类和实现接口一起使用:class Warning extends @Localized Message;

  • 构造器调用一起使用:new @Localized String(...)

  • 强制转型和instanceof检查一起使用:(@Localized String) text if(text instanceof @Localized String) 这些注解只供外部工具使用,它们对强制转型和instanceof检查不会产生任何影响

  • 异常规约一起使用:public String read() throws @Localized IOException

  • 通配符和类型边界一起使用:List<@Localized ?extends Message> List<? extends @Localized Message>

  • 方法和构造器引用一起使用:@Loaclized Test::main;

  • -

  • 放置习惯:可以将注解放置到诸如private和static这样的其他修饰符的前面或后面。习惯将类型用法注解放置到其他修饰符的后面和将声明注解放置到其他修饰符的前面。

⑦注解this

⑧标准注解

  • java.lang、java.lang.annotation 和 javax.annotation包中定义了大量的注解接口。其中四个是元注解,用于描述注解接口的行为属性,其他的是规则接口,可以用它们来注解你的源代码中的项。

  • ©Documented元注解为像Javadoc这样的归档工具提供了一些提示。应该像处理其他修饰符(例如protected 和static)一样来处理归档注解,以实现其归档目的。其他注解的使用并不会纳入归档的范畴。

  • @Inherited元注解只能应用于对类的注解。如果一个类具有继承注解,那么它的所有子类都自动具有同样的注解。这使得创建一个与Serializable这样的标记接有相同运行方式的注解变得很容易。

  • @Repeatable标记某个注解可以重复使用。


四. 异常、断言和日志

1. 异常

①异常分类

  • Error类:描述了Java运行时系统的内部错误和资源耗尽问题。一般不做抛出

  • Exception类:分为RuntimeException和其他。

  • 非检查型异常:派生于Error类或RuntineException不需要显式处理(编译器不强制要求)。

  • 检查型异常Exception中的非RuntineException部分。必须在编译时处理(通过 try-catchthrows 声明)。

②声明检查型异常

  • 使用 throws 声明异常

  • 一个方法必须声明所有可能抛出的检查型异常。

  • 不需要声明Java的内部错误,即从Error继承的异常。任何代码都有可能抛出那些异常,而我们对此完全无法控制。类似地,也不应该声明从RuntimeException继承的那些非检查型异常。

  • 如果类中的一个方法声明它会抛出一个异常,而这个异常是某个特定类的实例,那么这个 方法抛出的异常可能属于这个类,也可能属于这个类的任意一个子类。

public void readFile(String filePath) throws IOException, EOFException {
    FileReader file = new FileReader(filePath);
}

③抛出异常

  • 使用 throw 抛出异常

  • 找到一个合适的异常类->创建这个类的一个对象->将对象抛出。

public void checkAge(int age) {
    if (age < 18) {
        throw new IllegalArgumentException("Age must be 18 or above.");
    }
}

④创建异常类

  • 标准异常类无法描述清楚问题,需要自己创建异常类。

  • 开发者可以通过继承 ExceptionRuntimeException 或某个子类创建自己的异常。

  • 自定义的这个类应该包 含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器(超类Ttrowabh的 touring 方法会返回一个字符串,其中包含这个详细信息,这在调试中非常有用)。

class CustomException extends Exception
{
    public CustomException()
    {
    }

    public CustomException(String message)
    {
        super(message);//传递给父类详细描述信息
    }
}

public class Test
{
    public static void main(String[] args)
    {
        try
        {
            throw new CustomException("This is a custom exception");
        } catch (CustomException e)
        {
            System.out.println(e.getMessage());
        }
    }
}

⑤捕获异常

  • 如果发生了某个异常,但没有在任何地方捕获这个异常.程序就会终止,并在控制台上 打印一个消息.其中包括这个异常的类型和一个栈轨迹。

  • finally子句的体要用于清理资源。不要把改变控制流的语句 (return,throw, break,continue) 放在 finally 子句中。否则会出错。

//try catch finally
/**
  如果try语句块中的任何代码抛出了catch子句中指定的一个异常类:
     程序将跳过try语句块的其余代码。
     程序将执行catch子句中的处理器代码。
  如果try语句块中的代码没有抛出任何异常,那么程序将跳过catch子句。
  如果方法中的任何代码抛出了一个异常,但不是m生子句中指定的异常类型,那么这
个方法会立即退出
*/
try {
    // 可能会引发异常的代码
} catch (ExceptionType1 e1) {
    // 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
    // 处理 ExceptionType2 类型的异常
} finally {
    // 可选的 finally 块,用于清理资源(总会执行)
//不管是否捕获到异常,finally子句中的代码都会执行。即使异常捕获后又抛出。
}
******
try{
}
catch(ExceptionType1 | ExceptionType2 e) {//当捕获的异常彼此之间不存在子类关系时,可以合并catch句子
捕获多个异常时,异常变量隐含为final变量。
}
******
        try
        {
            // 可能会引发异常的代码
        } catch (ExceptionType1 e1)
        {
            throw new ExceptionType2;//再次抛出一个别的异常
            //或将原始异常设为新异常的‘原因’
            var e2 = new ExceptionType2();
            e2.initCause(e1);
            throw e;
        }
*******
try
{
}
finally
{
}
*******
最好是传递给异常调用者通过:throws声明异常
*******
子类覆盖超类方法的方法,而这个超类方法没有抛出异常,就必须捕获你的方法代码中出现的每一个检查型异常。 子类的throws列表中不允许出现超类方法中未列出的异常类。

  • try-with-resources 语句(java7)是一种自动管理资源(如文件流、数据库连接等)的方式。它确保在使用完资源后自动关闭资源,无需显式在 finally 块中释放资源,从而简化代码并减少内存泄漏的风险。

  • ResourceType:实现了 AutoCloseable Closeable(AutoCloseable的子接口)接口的类。

  • AutoCloseable 有一个方法:void close() throws Exception;Closeable 抛出IOException

  • try-with-resources 语句中,所有在 try 语句中声明的资源都必须实现 AutoCloseable 接口,或者是其子接口。这是因为 try-with-resources 语句会在 try 语句块结束时自动调用每个资源的 close() 方法,因此需要确保该资源具有适当的 close() 方法。

  • 资源声明:在 try 中声明的资源,会在代码块执行结束后自动调用其 resource.close() 方法。

  • Java9中,可以在try首部提供之前声明的事实最终变量。

  • try-with-resources 语句自身也可以有catch子句和finally子句。这些子句会在关闭资源后执行

try (ResourceType resource = new ResourceType()) {
    // 使用资源的代码
}
*******
try (FileReader reader = new FileReader("example.txt");//可以指定多个资源
             BufferedReader br = new BufferedReader(reader)) {

            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }

        } catch (IOException e) {
            System.out.println("文件操作异常:" + e.getMessage());
        }
*******
Java9中,可以在try首部提供之前声明的事实最终变量。
如果try块抛出一个异常,而且close()方法也抛出一个异常,这就会带来一个难题。
try-with-resource可以处理这种情况, 原来异常会重新抛出(无捕获下),close方法抛出的异常会被抑制。
这些异常将被自动捕获,并由addSuppressed方法添加到原来的异常当中。
若想获得这些异常,可用getSuppressed方法,会生成一个数组,其中包含从close方法抛出的被抑制的异常。
否则也可以用原来的手动处理这些问题。

PrintWriter out;
try(out)
{
  out.println();
}// out.closet) called here

⑥分析栈轨迹

  • -

2. 断言

①断言概念

  • 断言(Assertion) 是一种用于测试程序假设的调试工具。断言可以在开发和测试阶段验证程序的逻辑是否正确,帮助发现潜在的错误或不一致之处。断言通过 assert 语句实现,当条件不满足时会抛出错误。

  • 断言机制允许你在测试期间在代码中捕人一些检查,而在生产代码中自动删除这些检查。若使用异常来测试,会保留在程序里,使得程序运行变慢。

  • 关键字assert ,有两种形式:assert conditionassert condition:expression 这两个语句都会计算condition,若结果为false,则会抛出一个AssertionErrror异常。在第二个语句中,expression将传入AssertionError对象的构造器,并转换成一个消息字符串。

//基本断言
public class AssertionExample {
    public static void main(String[] args) {
        int x = 5;
        assert x > 0; // 条件满足,程序继续执行
        System.out.println("断言通过");

        assert x < 0; // 条件不满足,抛出 AssertionError
        System.out.println("不会执行这行");
    }
}
//带消息的断言
public class AssertionWithMessage {
    public static void main(String[] args) {
        int age = -1;
        assert age >= 0 : "年龄不能为负数!"; // 条件不满足,抛出 AssertionError,附带消息
        System.out.println("年龄是:" + age);
    }
}//Exception in thread "main" java.lang.AssertionError: 年龄不能为负数!

②启用和禁用断言

  • 默认断言是禁用的,需要通过 JVM 参数显式启用。

  • 启用断言:使用 -ea-enableassertions 参数。java -ea AssertionExample

  • 针对某个类启用java -ea:com.example.MyClass

  • 针对某个包启用java -ea:com.example...

  • 禁用断言:使用 -da-disableassertions 参数(默认行为)。java -da AssertionExample

3. 日志

①概念和框架

  • 日志(Logging) 是一种记录应用程序运行过程中信息的机制,通常用于调试、监控和审计。相比于使用 System.out.println 输出,日志框架更灵活、高效,支持日志级别、输出格式、日志存储等功能。

  • 默认的日志配置会记录IN印或更高级别的所有日志。

②Java 自带日志

import java.util.logging.Logger;

public class Test
{
    //全局日志记录器,一般不使用
    private static final Logger globalLog=Logger.getGlobal();
    //定义自己的日志记录器
    private static final Logger logger = Logger.getLogger(Test.class.getName());//参数是记录器名称,这里用类名

    public static void main(String[] args)
    {
        globalLog.info("我是全局记录器");
//        11月 23, 2024 3:25:46 下午 Test main
//        信息: 我是全局记录器

        logger.severe("这是 SEVERE 日志");
        logger.warning("这是 WARNING 日志");
        logger.info("这是 INFO 日志");
        logger.fine("这是 FINE 日志");  // 默认不输出低级别日志
        /*
            11月 23, 2024 3:22:38 下午 Test main
            严重: 这是 SEVERE 日志
            11月 23, 2024 3:22:38 下午 Test main
            警告: 这是 WARNING 日志
            11月 23, 2024 3:22:38 下午 Test main
            信息: 这是 INFO 日志
         */
    }
}

③修改日志管理器配置

  • 可以通过编辑配置文件来修改日志系统的各个属性。默认的配置文件位于:jdk/conf/logging.properties(或者在Java9 之前,位于jre/lib/logging.properties)

④本地化

⑤处理器

  • 默认情况, 日志记录器将记录发送到ConsoleHandler它会将记录输出到System.err流。

⑥过滤器

  • 在默认情况下,会根据日志记录的级别进行过滤。每个日志记录器和处理器都可以有一个可选的过滤器来完成额外的过滤。

⑦格式化

  • ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。

⑧API


五. 泛型

  • 泛型是 Java 中的一种特性,用于参数化类型。它允许你在定义类、接口和方法时使用类型参数,从而使代码更加通用、灵活和类型安全。

1. 泛型类

//子类保留父类泛型
public class GenericParent<T>
{
    private T value;

    public GenericParent(T value)
    {
        this.value = value;
    }

    public T getValue()
    {
        return value;
    }
}

public class GenericChild<T> extends GenericParent<T>
{
    public GenericChild(T value)
    {
        super(value);
    }

    public void printValue()
    {
        System.out.println("Value: " + getValue());
    }
}

public class Test
{
    public static void main(String[] args)
    {
        GenericChild<String> child = new GenericChild<>("Hello");
        child.printValue(); // 输出:Value: Hello
    }
}

//子类指定父类泛型
public class StringChild extends GenericParent<String>
{
    public StringChild(String value)
    {
        super(value);
    }

    public void printValue()
    {
        System.out.println("String Value: " + getValue());
    }
}

public class Test
{
    public static void main(String[] args)
    {
        StringChild child = new StringChild("Hello");
        child.printValue(); // 输出:String Value: Hello
    }
}

2. 泛型方法

  • 泛型方法的类型参数与类的类型参数无关: 即使类本身有泛型参数,泛型方法仍可以定义自己的类型参数。

  • 类型变量放在修饰符后,返回类型前面。

  • 可以在普通类中定义泛型方法,也可以在泛型类中定义。

//定义泛型方法
public class GenericMethodExample
{
    public static <T> void printArray(T[] array)
    {
        for (T element : array)
        {
            System.out.println(element);
        }
    }
}

//使用泛型方法
public class Test
{
    public static void main(String[] args)
    {
        String[] stringArray = {"A", "B", "C"};
        Integer[] intArray = {1, 2, 3};

        GenericMethodExample.<String>printArray(stringArray);//可以指明,一般省略,能推断出来
        GenericMethodExample.printArray(stringArray); // 输出 A, B, C
        GenericMethodExample.printArray(intArray);    // 输出 1, 2, 3
    }
}

3. 泛型接口

// 泛型接口
public interface Processor<T extends Comparable<T>>
{
    void process(T input);
}

// 实现类, 指定泛型
public class StringProcessor implements Processor<String>
{
    @Override
    public void process(String input)
    {
        System.out.println("String Length: " + input.length());
    }
}

//保留泛型
public class NumberProcessor<T extends Number> implements Processor<T>
{
    @Override
    public void process(T input)
    {
        System.out.println("Number Value: " + input.doubleValue());
    }
}

public class Test
{
    public static void main(String[] args)
    {
        Processor<String> stringProcessor = new StringProcessor();
        stringProcessor.process("Hello"); // 输出:String Length: 5

        Processor<Integer> intProcessor = new NumberProcessor<>();
        intProcessor.process(42); // 输出:Number Value: 42.0
    }
}

4. 类型变量的限定

  • Function<? super T, ? extends Stream<? extends R>> 中的 <? super T, ? extends Stream<? extends R>> 对原始的 public interface Function<T, R> 中的 <T, R> 做了限定,但这种限定并不是直接替换 TR,而是通过通配符类型上下限来对泛型类型进行约束,从而实现更大的灵活性。

①具体类型

//接口
public interface Processor<T>
{
    void process(T input);
}

//StringProcessor 只能处理 String 类型的输入
public class StringProcessor implements Processor<String>
{
    @Override
    public void process(String input)
    {
        System.out.println("Processing String: " + input);
    }
}

public class Test
{
    public static void main(String[] args)
    {
        Processor<String> stringProcessor = new StringProcessor();
        stringProcessor.process("Hello, World!"); // 输出:Processing String: Hello, World!
    }
}

②有界类型

E extends T 表示泛型类型参数 E 必须是类型 T 或其子类。
// 定义一个泛型类,限制 E 必须是 Number 的子类
public class Box<E extends Number>
{
    private E value;

    public Box(E value)
    {
        this.value = value;
    }

    public double getDoubleValue()
    {
        return value.doubleValue(); // 使用 Number 中的方法
    }

    public E getValue()
    {
        return value;
    }
}

public class Test
{
    public static void main(String[] args)
    {
        Box<Integer> intBox = new Box<>(42);
        Box<Double> doubleBox = new Box<>(3.14);

        System.out.println(intBox.getDoubleValue()); // 输出:42.0
        System.out.println(doubleBox.getDoubleValue()); // 输出:3.14
    }
}
E super T 表示泛型类型参数 E 必须是类型 T 或其父类。
// 定义一个泛型方法,限制 E 必须是 Integer 的父类
public class Utils
{
    public static <E super Integer> void addIntegers(E value, List<E> list)
    {
        list.add(value); // 确保类型安全
    }

    public static void main(String[] args)
    {
        List<Number> numberList = new ArrayList<>();
        List<Object> objectList = new ArrayList<>();

        addIntegers(42, numberList); // Number 是 Integer 的父类
        addIntegers(42, objectList); // Object 是 Integer 的父类
    }
}
可以使用多个有界类型为泛型参数设置复杂的上下界。
多个有界类型通过 "&" 符号连接,这些边界可以包括 类 和 接口,
上下界限制可以在泛型中提供更多的约束。

上界(extends):限定泛型参数必须是多个类/接口的子类型。
下界(super):下界不能直接与多个类型一起使用,但在泛型中常通过接口或继承实现类似效果

public static <T extends Comparable<T>> void addToList(List<? super T> list, T value)
{
    list.add(value);
}

③通配符

  • 通配符(Wildcard) 是用 ? 表示的占位符,用于表示任意类型。通配符能够让泛型更加灵活,尤其是在处理泛型类或泛型方法时,可以通过通配符定义类型的范围或限制其操作行为。

  • ? 表示元素可以是任何类型,但是只能是相同的类型。

//无限制通配符<?>
List<?> list = new ArrayList<String>();
list.add(null); // 允许添加 null
// list.add("Hello"); // 编译错误

Object obj = list.get(0); // 只能作为 Object 读取
上界通配符 ? extends T:表示类型参数是 T 本身或其子类型。
List<? extends Number> list = new ArrayList<Integer>();

// 读取
Number num = list.get(0); // 允许读取为 Number
// Integer integer = list.get(0); // 具体类型信息无法确定,编译错误

// 写入
// list.add(123); // 编译错误
list.add(null); // 允许添加 null
下界通配符 ? super T:表示类型参数是 T 本身或其父类型。
List<? super Integer> list = new ArrayList<Number>();

// 写入
list.add(42); // 允许添加 Integer 或其子类
// list.add(3.14); // 编译错误:Double 不是 Integer 或其子类

// 读取
Object obj = list.get(0); // 读取只能作为 Object
// Integer integer = list.get(0); // 编译错误

④总结

5. 泛型和虚拟机

  • 虚拟机没有泛型类型对象——所有对象都属于普通类。

  • 会合成桥方法来保持多态。

  • 所有的类型参数(泛型T和普通类型)都会替换为它们的限定类型。

  • 为保持类型安全性,必耍时会插入强制类型转换。

  • -

  • 规则

  • 1. 一个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。

  • 2. 泛型类型被其第一个上界替换,如果没有指定上界,默认为 Object

  • -

  • 无泛型的类:对于没有使用泛型的普通类,类中的所有类型信息在编译后的字节码中是保留的。这意味着,如果你有一个普通的类,其中定义了某个特定类型的字段或方法参数,这些类型信息在运行时是可用的。

  • 泛型类:类型擦除机制确保了所有的泛型类型信息在编译后的字节码中被移除。这意味着,泛型类中的泛型类型参数在运行时是不可见的。在泛型类中,非泛型的参数类型,比如int、String等,也会受到类型擦除的影响。尽管这些类型不是泛型参数,但它们出现在泛型类中时,其类型信息在运行时也会被擦除。这是因为泛型类的所有信息在编译时被转换为它们对应的边界类型,通常是Object,或者是泛型声明时指定的边界。这种转换是为了确保运行时的类型安全和向后兼容性。

①类类型擦除

//Pair<T>的原始类型如下
/*
因为T是一个无限定的类型变量,所以直接替换为Object
其结果是一个普通类,就好像Java语言中引人泛型之前实现的类一样。
Pair<String>, Pair<int>, 类型擦除后都会变成原始的Pair类型
 */
class Pair
{
    private Object first;
    private Object second;

    public Pair(Object first, Object second)
    {
        this.first = first;
        this.second = second;
    }

    public Object getFirst()
    {
        return first;
    }

    public void setFirst(Object first)
    {
        this.first = first;
    }
}
//泛型
class Interval<T extends Comparable & Serializable> implements Serializable
{
    private T lower;
    private T upper;

    public Interval(T first, T second)
    {

    }
}
//原始类型
class InInterval implements Serializable
{
    private Comparable lower;
    private Comparable upper;
    
    public Interval(Comparable first, Comparable second)
    {
        
    }
}

②转换泛型方法

  • 泛型方法的类型参数同样会被替换为擦除类型。

// 泛型方法定义
public <T extends Comparable<T>> T findMax(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

// 擦除后
public Comparable findMax(Comparable a, Comparable b) {
    return a.compareTo(b) > 0 ? a : b;
}

③通配符的擦除

  • 无界通配符:List<?> 在类型擦除后变为 List,实际元素类型为 Object。

  • 通配符被擦除为原始类型 List

  • 上界通配符(? extends T)会被擦除为 T

  • 下界通配符(? super T)会被擦除为 Object

    // 方法接收任意类型的 List
    public static void printList(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
        // list.add("New Element"); // 编译错误
        list.add(null); // 允许添加 null
    }
****
擦除后
public static void printList(List list) { // 擦除为原始类型
    for (Object obj : list) {
        System.out.println(obj); // 所有元素都作为 Object 处理
    }
    list.add(null); // 允许添加 null
}
public static void processNumbers(List<? extends Number> list) {
        for (Number number : list) { // 读取为 Number
            System.out.println(number.doubleValue());
        }
        // list.add(42); // 编译错误:不能写入具体类型
        list.add(null); // 允许添加 null
    }
****
擦除后
public static void processNumbers(List list) { // 擦除为原始类型
    for (Object number : list) {
        System.out.println(((Number) number).doubleValue()); // 强制类型转换
    }
    list.add(null); // 允许添加 null
}
public static void addNumbers(List<? super Integer> list) {
        list.add(42); // 可以写入 Integer
        list.add(99); // 可以写入 Integer
        // list.add(3.14); // 编译错误:不能写入 Double

        Object obj = list.get(0); // 读取只能作为 Object 处理
        System.out.println(obj);
    }
***
擦除后
public static void addNumbers(List list) { // 擦除为原始类型
    list.add(42); // 写入 Integer
    list.add(99); // 写入 Integer
    Object obj = list.get(0); // 读取时只能作为 Object
    System.out.println(obj);
}

④转换泛型表达式

  • 如果泛型方法在编译后被 类型擦除,返回类型变为擦除类型(通常是 Object 或上界类型),在调用时,编译器会自动插入 强制类型转换 以恢复实际的泛型类型。这是由于 Java 泛型的设计决定的,它在编译期间检查类型安全性,而运行时通过类型擦除保持与旧版本代码的兼容性。

//返回值类型是泛型参数
public static <T> T getGenericValue(T value) {
    return value;
}

public static void main(String[] args) {
    Integer result = getGenericValue(42); // 返回值类型擦除为 Object
    // 编译器会插入:Integer result = (Integer) getGenericValue(42);
    System.out.println(result);
}
//返回值类型是通配符
public static List<?> getWildcardList() {
    return List.of(1, 2, 3);
}

public static void main(String[] args) {
    List<?> wildcardList = getWildcardList(); // 类型擦除为 List
    List<Integer> integerList = (List<Integer>) wildcardList; // 需要强制类型转换
    System.out.println(integerList);
}

6. 限制与局限性

①不能用基本类型实例化类型参数

  • 不能用基本类型代替类型参数。因此,没有Pair<double>只有Pair<Double>。因为类型擦除后Pair类含有Object字段,而Object不能存储基本数据类型值。

②运行时类型查询只适用于原始类型

③不能创建参数化类型的数组


④不能构造泛型数组

⑤Varargs警告(带有泛型类型的可变参数)

⑥不能实例化类型变量

⑦泛型类的静态上下文中类型变量无效

⑧不能抛出或捕获泛型类的实例

⑨可以取消对检查型异常的检查

⑩注意擦除后的冲突

7. 泛型类型的继承规则


六. 集合

1. Java集合框架

①集合接口与实现分离

  • Java集合类库将接口和实现分离,有助于多态和扩展。

②集合框架中的接口

  • 集合有两个基本接口:Collection和Map。

  • List 接口定义了多个用于随机访问的方法。

  • RandomAccess 是一个标记接口,用来测试一个集合是否支持高效随机访问。c instanceof RandomAccess

  • Listlterator接口是Iterator的子接口,定义了一个方法用于在迭代器前面增加一个元素。

  • Set接口等同于Collection接口,但里面的add方法不允许增加重复元素。equals只要两个集包含相同元素就相等,不要求顺序。hashCode只要包含相同元素就返回相同码。

  • SortedSet和SortedMap 接口提供用于排序的比较器对象。

  • NavigableSet、NavigableMap 包含额外一些用于搜索和遍历有序集合映射的方法。

③迭代器(Iterator、Iterable)

  • 用于访问集合元素。

  • for each循环可以处理任何实现了Iterable接口的对象。编译器会把for each循环转换为一个带迭代器的循环。

  • Collection接口扩展了Iterable,对于标准库任何集合都可以使用for each

  • 访问元素的顺序取决于集合类型:如果迭代处理一个ArrayList,迭代器将从索引0开始每迭代一次加1.若访问HashSet则随机获得元素。

  • Java迭代器位于两个元素之间口。当调用next时,迭代器就越过下一个元 素,并返回刚刚越过的那个元索的引用。

public interface Iterator<E>

boolean hasNext();//是否有下一个元素
E next();//返回下一个元素
default void remove()//移除迭代器最后一次返回的元素
default void forEachRemaining(Consumer<? super E> action)//对剩余元素执行给定lambda操作

  • Java 多个迭代器的修改或遍历问题,以及并发下的问题

④Collection接口

  • 集合类的基本接口是Collection接口。

public interface Collection<E> extends Iterable<E>

boolean add(E e);//添加元素,成功返回true
boolean addAll(Collection<? extends E> c);//添加c中全部元素
boolean remove(Object o);//移除元素
boolean removeAll(Collection<?> c);//移除c中全部元素
boolean retainAll(Collection<?> c);//从集合中删除不是c中元素的元素
Iterator<E> iterator();//返回一个迭代器
boolean isEmpty();//集合是否为空
boolean contains(Object o);//是否包含某个元素
boolean containsAll(Collection<?> c);//是否包含c中的全部元素
int size();//返回集合元素数量
default boolean removeIf(Predicate<? super E> filter)//带条件移除
void clear();//清空
boolean equals(Object o);//等于
Object[] toArray();//返回集合中对象的数组
<T> T[] toArray(T[] a);
default <T> T[] toArray(IntFunction<T[]> generator)//返回集合中的对象的数组。这个数组用generator构造,通常是一个构造器表达式T[]::new
default Stream<E> stream()//返回流
default Stream<E> parallelStream()//返回并行流

⑤Map接口

  • 映射类的基本接口是Map。key唯一

Map中的视图
interface Entry<K, V> {
    K getKey();//返回此键值对的键
    V getValue();//返回此键值对的值
    V setValue(V value);//设置此键值对的值
    boolean equals(Object o);//判断是否相等
    int hashCode();//返回hash值
    //返回一个比较器,按key的自然顺序比较
    public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K, V>> comparingByKey()
    ////返回一个比较器,按value的自然顺序比较
    public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K, V>> comparingByValue()
    //返回一个比较器,该比较器用给定的比较器比较key
    public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp)
    //返回一个比较器,该比较器用给定的比较器比较value
    public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp)
    //返回e的副本
    public static <K, V> Map.Entry<K, V> copyOf(Map.Entry<? extends K, ? extends V> e) 
}
public interface Map<K, V>

int size();//返回映射数量
boolean isEmpty();//是否为空

boolean containsKey(Object key);//是否包含key
boolean containsValue(Object value);//是否包含值

V get(Object key);//得到key对应的值
default V getOrDefault(Object key, V defaultValue)//若key存在返回对应value, 否则返回defaultValue

V put(K key, V value);//添加键值对
void putAll(Map<? extends K, ? extends V> m);//复制m中的元素到本集合,相当于调put
default V putIfAbsent(K key, V value)//若Key未与值关联则关联value, 否则返回原来关联的值
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)//若该键未关联,则用计算出的值与其关联
default V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)//旧值存在才关联新计算的值
//根据旧值和lambda计算新值,新值不空则建立映射,若新值为空则移除就映射。
default V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)
default V merge(K key, V value,
            BiFunction<? super V, ? super V, ? extends V> remappingFunction)

V remove(Object key);//移除key对应的键值对
default boolean remove(Object key, Object value)//仅移除key-value这种键值对
void clear();//清空

Set<K> keySet();//得到key集视图,可以从这个集中删除元素,这些键和相关联的值也将从映射中删除,但是不能添加任何元素
Collection<V> values();//得到值集视图。 可以从这个集合中删除元素,所删除的值及相应的键也将从映射中删除,不过不能添加任何元素
//返回集合的Entry集合,代表所有键值对。对Entry的修改会影响原集合(反之亦然),导致迭代器失效(除了迭代器自身提供的修改操作外)。
Set<Map.Entry<K, V>> entrySet();//(键值视图)可以从这个集中删除元素.它们也将从映射中删除,但是不能添加任何元素。
boolean equals(Object o);//相等
int hashCode();//hash值

default void forEach(BiConsumer<? super K, ? super V> action)//消费操作
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)//每一项的值替换为lambda结果
default boolean replace(K key, V oldValue, V newValue)//仅当key映射为oldValue时才替换为新值
default V replace(K key, V value)//将键为key的值替换为value, 若ke不存在不进行任何操作

static <K, V> Map<K, V> of()//返回一个不可修改的map, 其中包含0个元素
static <K, V> Map<K, V> of(K k1, V v1)//返回一个不可修改的map, 其中包含1个{k1,v1}
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2)//同上
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3....)至多十个
static <K, V> Map<K, V> ofEntries(Entry<? extends K, ? extends V>... entries)//返回不可修改的map,内容来自参数
static <K, V> Entry<K, V> entry(K k, V v)//返回不可修改的Entey, 内容来自参数
static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map)//返回不可修改的map,内容来自参数

⑥集合实例

2. Collection

②双向链表(LinkedList|有序链表)

  • image-toin.pngimage-pith.pngimage-mngz.pngimage-luaa.pngimage-zcrh.pngimage-ofco.png

  • 如果某个迭代器修改集合,另一个在遍历集合,会异常

public class LinkedListExample
{
    public static void main(String[] args)
    {
        // 创建一个LinkedList
        LinkedList<String> linkedList = new LinkedList<>();

        // 添加元素
        linkedList.add("A");
        linkedList.add("B");
        linkedList.add("C");
        linkedList.add("D");

        // 正向遍历
        System.out.println("正向遍历:");
        ListIterator<String> forwardIterator = linkedList.listIterator();
        while (forwardIterator.hasNext())
        {
            System.out.println(forwardIterator.next());
        }

        // 反向遍历
        System.out.println("\n反向遍历:");
        ListIterator<String> backwardIterator = linkedList.listIterator(linkedList.size());
        while (backwardIterator.hasPrevious())
        {
            System.out.println(backwardIterator.previous());
        }
    }
}

③数组列表(ArrayList|有序数组)

  • List接口描述一个有序集合,其中每个元素的位置很重要。

  • 有两种访问元素的协议:一种是通过迭代器,另一种是通过get 和set方法随机访问。后者不适用于链表,但当然get和set方法对数组很有用。AirayList封装了一个动态再分配的对象数组。

④散列集(散列表实现|HashSet|无序集合)

  • 散列表为每个对象计算一个整数称为散列码。快速地计算出散列码,并且这个计算只与要计算散列的那个对象的状态有关.与散列表中的其他对象无关。

  • 散列表实现为链表数组。每个列表被称为桶。要想查找一个对象在表中的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的数就是保存这个元素的那个桶的索引。

  • 有时候会遇到桶已经填充了元素的情况。这种现象被称为散列冲突。需要将新对象与那个桶中的所有对象进行比较,查看这个对象是否已经存在。

  • 在Java 8中,桶满时会从链表变为平衡二叉树

  • HashSet 就是基于散列表的。

④树集(TreeSet|有序集合|红黑树实现)

  • 树集是一个有序集合:可以以任意顺序将元素插入集合中。在对集合进行遍历时,值将自动地按照排序后的顺序出现。

  • 添加到树中比散列表慢,但检查数组或链表中的重复元素,树快。

  • 要使用树集,必须能够比较元素。 这些元素必须实观Comparable接口,或者构造集时必须提供一个Comparator。

  • 从Java6起,TreeSet 类实现了NavigableSet接口。这个接口增加了几个便利方法,用于查找元素以及反向遍历。

⑤队列与双端队列(ArrayDeque数组实现|LinkedList双向链表实现)

  • image-oefa.png

⑥优先队列(PriorityQueue|堆实现(默认小根堆是一个数组))

  • 任意顺序插入元素,有序弹出(默认从小到大)。这些元素必须实观Comparable接口,或者构造集时必须提供一个Comparator。

3. Map

①散列映射(HashMap)

  • 散列映射对键进行散列,树映射根据键的顺序将它们组织为一个搜索树。散列或比较函数只应用于键,与键关联的值不进行散列或比较。

②TreeMap(基于红黑树实现)

  • TreeMap 是 Java 中的一个基于红黑树实现的 Map,它的键值对是 有序 的,默认按键的自然顺序排序,或者按照用户提供的 Comparator 定制排序。

③弱散列映射(WeakHashMap)

  • 解决问题:假设对某个键的最后一个引用已经消失,那么没有任何途径可以引用这个值对象了。因此,无法直接从映射中删除这个键值对。但是GC不会回收它。GC会跟踪活动对象,只要映射对象(Map)是活动的,其中的所有桶就是活动的,他们就不能被回收。要么程序自己删除,要么使用WeakHashMap

  • WeakHashMap 是 Java 中的一个特殊实现,它的特点是使用弱引用WeakReference)存储键。其主要设计目标是允许键被垃圾回收,特别适用于缓存场景

public class WeakHashMapExample
{
    public static void main(String[] args)
    {
        Map<Object, String> map = new WeakHashMap<>();

        Object key1 = new Object();
        Object key2 = new Object();

        map.put(key1, "Value1");
        map.put(key2, "Value2");

        System.out.println("Before GC: " + map); // 输出完整的键值对

        // 去掉 key1 的强引用
        key1 = null;

        // 强制触发垃圾回收
        System.gc();

        // 稍作等待以确保 GC 运行
        try
        {
            Thread.sleep(1000);
        } catch (InterruptedException ignored)
        {
        }

        System.out.println("After GC: " + map); // 可能只剩下 key2 的键值对
    }
}
Before GC: {java.lang.Object@1b6d3586=Value1, java.lang.Object@4554617c=Value2}
After GC: {java.lang.Object@4554617c=Value2}

④链接散列集(LinkedHashSet)和链接散列映射(LinkedHashMap)(双链表)

  • 两者会记录插入元素项的顺序。

⑤枚举集(EnumSet)与映射(EnumMap)

  • EnumSet是一个高效的集实现, 其元素属于一个枚举类型。因为枚举类型只有有限个实例,所以EnumSet在内部实现为一个位序列。如果对应的值在集中出现,相应的位则置为1。

⑥标识散列映射(IdentityHashMap)

  • 键的散列值不是用hashCode计算的,而是由函数计算的,而是用Systm.identityHashCode方法计算的(根据对象的内存地址得出)。Object.hashcode根据对象的内存地址计算散列码时就使用了这个方法。另外,对两个对象进行比较时.IdentityHashMap使用了==,而不是用equals。即:不同的键对象即使内容相同,也被视为不同的对象。

  • 在实现对象遍历算法(如对象串行化)时,如果你想跟踪哪些对象已经遍历过,这个类就很有用.

4. 副本与视图

  • 视图:视图是对原数据的一种引用或投影,本质上与原数据共享底层数据。对视图的修改会直接影响原数据,反之亦然。

  • 副本:副本是原数据结构的一个拷贝,通常是深拷贝或浅拷贝。修改副本不会影响原始数据,反之亦然。

①小集合

②不可修改的副本和视图

③子范围

④检查型视图

⑤同步视图

⑥可选操作

5. 算法

  • Java 集合框架提供了许多内置的算法,主要通过 Collections 工具类实现,支持排序、查找、填充、同步化等操作。这些算法作用于Collection接口和Map接口中的实例。

①泛型算法

  • 泛型算法 是指可以在不限定具体数据类型的情况下处理集合或对象的算法。

②常用算法

//public class Collections
//排序:对List集合进行排序
//将List所有元素都放在一个数组,对这个数组进行排序,然后再将排序后的序列复制回列表
//稳定的,不会改变相等元素的顺序
public static <T extends Comparable<? super T>> void sort(List<T> list) //T必须实现Comparable接口或其子接口
public static <T> void sort(List<T> list, Comparator<? super T> c)//传入一个比较器
public static void reverse(List<?> list)//翻转列表
//public class Collections
//混排
//如果提供的列表没有实现RandomAccess(支持高效随机访问)接口,
// shuffle方法会将元素复制到数组中, 然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。
public static void shuffle(List<?> list)
//public class Collections
/*
  二分查找
集合必须是有序的。
类型必须是实现比较接口或者提供排序器。
支持高效随机访问。
返回非负值表示匹配对象的索引,负值表示没有匹配到。
可以利用这个返回值来计算应该将element插人集合的哪个位置,以保持集合的有序性。插人的位置是-i-1(不是-i,0有二义性)。if(i<0) c.add(-i-1, element)
*/
public static <T>//key是要找的值
    int binarySearch(List<? extends Comparable<? super T>> list, T key)
public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c)//提供一个比较器c

//集合数组的转换
String[] names;
List<String> staff=List.of(names)
*****
Object[] names=staff.toArray()//得到一个Obj数组(不能改变类型),但是不能强转,
//Java 中无法直接将 Object[] 转换为 String[],因为两者在内存布局上不兼容。
String[] names2=(String[]) staff.toArray()//✖
default <T> T[] toArray(IntFunction<T[]> generator)//返回一个使用生成器生成的数组,包含集合所有元素
names=staff.toArray(String[]::new)//✔

List<Integer> list=new ArrayList<>();
        Integer[] a=new Integer[10];
        list.toArray(a);//存入a中若其足够大,否则返回新的数组

6. 遗留的集合

  • 集合框架出来前的容器类,目前已经集成到集合框架中。

①Hashtable类

  • 作用同HashMap, 接口也一样,方法是同步的。

②枚举Enumeration

  • 遗留的集合使用Enumeration接口变量元素:hasMoreElements和nextElement方法

③属性映射Properties

  • 键与值都是字符串。

  • 这个映射可以很容易地保存到文件以及从文件加载。

  • 有一个二级表存放默认值。

  • Properties类有两种提供默认值的机制。第一种方法是,只要查找一个字符串的值,可以指定一个默认值,当查找的键不存在时就会自动使用这个默认值。

  • 可以把所有默认值都放在一个二 级属性映射中,井在主属性映射的构造器中提供这个二级映射.

④栈Stack

⑤位集(BitSet)


七. 反射

1. 反射介绍

  • 反射是Java提供的一种强大机制,允许在运行时检查和操作类、方法、字段、构造函数等信息。通过反射,程序可以动态地加载类、调用方法、访问和修改字段值,这为开发提供了高度的灵活性。反射机制在Java框架中被广泛应用,尤其在依赖注入和对象的动态管理中。

  • 反射用途

  • 在运行时分析类的能力。

  • 在运行时检查对象。

  • 实现泛型数组操作代码。

  • 利用Method对象。

  • 反射主要通过 java.lang.reflect 包中的类和接口来实现。常用的类和接口包括:

  • Class:表示类或接口的对象,可以用来获取类的各种信息。

  • Field:表示类的成员变量。

  • Method:表示类的方法。

  • Constructor:表示类的构造函数。

  • Modifier:用于分析类、方法或字段的修饰符(如 publicprivatestatic 等)。

  • Annotation:用于获得上面各种的注解。

2. 涉及包

①java.lang包

  • Class:用于表示类的元数据,它是反射的核心类。通过 Class 对象,我们可以获取类的构造方法、字段、方法等信息,并对其进行操作。Class<?> cls = MyClass.class;

  • Object:是所有 Java 类的超类,也是反射中不可避免的一个类。通过反射,我们可以通过 Object 类来动态操作对象的字段和方法。Object obj = cls.newInstance();

  • Modifier:用于分析类、字段、方法等的修饰符(如 publicprivatestatic 等)。可以通过反射获取成员的修饰符信息。int modifiers = field.getModifiers();

  • Package:表示类或接口所属的包。在反射中,可以通过 Package 对象获取类的包名信息。Package pkg = cls.getPackage(); String packageName = pkg.getName();

  • ClassLoader:用于动态加载类。在反射中,ClassLoader 用于加载类,以便可以通过反射访问它们。ClassLoader classLoader = cls.getClassLoader(); Class<?> loadedClass = classLoader.loadClass("java.lang.String");

②java.lang.reflect包

  • Field:表示类的成员变量。通过 Field 对象可以访问和修改类的字段值。Field field = cls.getDeclaredField("fieldName"); field.setAccessible(true); field.set(obj, value);

  • Method:表示类的方法。通过 Method 对象可以获取方法的签名、参数、返回类型等信息,并动态调用方法。Method method = cls.getMethod("methodName", String.class); method.invoke(obj, "parameter");

  • Constructor:表示类的构造函数。通过 Constructor 对象可以动态地创建类的实例。Constructor<?> constructor = cls.getConstructor(String.class); Object obj = constructor.newInstance("parameter");

  • Array:提供用于动态创建和访问数组的方法。可以使用反射动态创建数组,并对数组进行操作。Object array = Array.newInstance(String.class, 10); // 创建一个长度为 10 的字符串数组 Array.set(array, 0, "Hello");

  • Proxy:用于创建接口的动态代理对象。通过反射机制,Proxy 类可以在运行时为接口创建代理实例,通常用于 AOP、事件监听等场景。MyInterface proxy = (MyInterface) Proxy.newProxyInstance(

    MyInterface.class.getClassLoader(),

    new Class[] { MyInterface.class },

    new InvocationHandler() {

    @Override

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    return method.invoke(new MyClass(), args);

    }

    }

    );

  • Constructor:表示类的构造函数。通过 Constructor 类,可以获取类的构造方法并用它来动态创建对象。Constructor<?> constructor = cls.getConstructor(String.class); Object obj = constructor.newInstance("Hello");

  • Type接口:Type 是一个接口,表示 Java 程序中的类型。在反射中,Type 是获取泛型类型信息的基础接口。ParameterizedTypeType 接口的一个实现。

  • ParameterizedType接口:ParameterizedType 是一个接口,表示具有实际类型参数的类型(即泛型类型)。通过该接口,可以获取泛型的实际类型参数。

  • GenericArrayType接口:GenericArrayTypeType 接口的一个实现,表示泛型数组的类型。例如,List<String>[] 就是一个泛型数组。在运行时,我们可以使用 GenericArrayType 来获取数组元素的泛型类型。

  • WildcardType接口:WildcardType 表示泛型中的通配符类型(如 ?)。在泛型类型中使用通配符时,WildcardType 可以用来获取具体的上下界信息。

  • TypeVariable :是 Java 反射中的一个接口,表示类、方法或构造器的类型参数。它通常用于获取泛型类、泛型方法、泛型接口等的类型参数信息。

  • Class:虽然 Class 主要用于获取类的元数据,但它也提供了 getTypeParameters() 等方法,帮助在反射中获取类型参数信息。

③java.lang.annotation包

  • Annotation:所有注解的基类。通过反射,您可以访问类或方法上的注解。MyAnnotation annotation = cls.getAnnotation(MyAnnotation.class);

3. Class

①Class介绍

  • Class 是 Java 反射机制中的一个核心类,它代表了一个类或接口的运行时信息。每个 Java 类在 JVM 中都有一个与之对应的 Class 对象,它提供了关于类的信息,比如类的名字、构造方法、字段、方法等。通过 Class 对象,我们可以在运行时动态地获取或修改类的成员、创建类的实例,甚至动态加载类。

  • Class对象实际上描述的是一个类型.这可能是类,也可能不是类。例如,int不是类,但int.class确实是一个Class类型的对象。

  • 虚拟机为每个类型管理一个唯一的Class对象。因此,可以使用==运算符比较两个类对象。a.getClass()==E.class //a是E的一个实例,则相等

  • Class类实际上是一个泛型类,例如Employee.class的类型是Class<Employee>。

②API

③获取Class对象

通过类名获取
Class<?> cls = String.class;  // 获取 String 类的 Class 对象
通过实例对象获取
String str = "Hello, world!";
Class<?> cls = str.getClass();  // 获取 str 实例的 Class 对象//Object的getClass()
通过 Class.forName() 获取
Class<?> cls = Class.forName("java.lang.String");  // 通过类的完全限定名获取 Class 对象//java.lang.Class④

④Class常用方法实例

String className = cls.getName();  // 获取类的完全限定名
System.out.println(className);  // 输出 "java.lang.String"



String simpleName = cls.getSimpleName();  // 获取类的简单名称(不包含包名)
System.out.println(simpleName);  // 输出 "String"



Package pkg = cls.getPackage();  // 获取类的包信息
System.out.println(pkg.getName());  // 输出 "java.lang"



int modifiers = cls.getModifiers();
System.out.println(Modifier.toString(modifiers));  // 输出类的修饰符(如 public、abstract、final 等)



Class<?> superClass = cls.getSuperclass();  // 获取父类的 Class 对象
System.out.println(superClass.getName());  // 输出类的父类名



Class<?>[] interfaces = cls.getInterfaces();  // 获取所有实现的接口
for (Class<?> iface : interfaces) {
    System.out.println(iface.getName());  // 输出每个接口的名称
}




//构造实例
Object obj = cls.newInstance();  // 创建类的一个实例,已经弃用

Constructor<?> constructor = cls.getConstructor(String.class);  // 获取带一个 String 参数的构造方法
Object obj = constructor.newInstance("Hello");
Java 反射支持操作数组,可以动态创建数组并访问数组元素。
Class<?> componentType = String.class;
Object array = Array.newInstance(componentType, 5);  // 创建一个包含 5 个元素的 String 数组

Array.set(array, 0, "Hello");  // 设置数组元素
System.out.println(Array.get(array, 0));  // 获取数组元素,输出 "Hello"
Class 类还与 ClassLoader 密切相关。ClassLoader 负责加载类,Class 对象则代表了类的实际信息。
ClassLoader classLoader = cls.getClassLoader();//获取当前类加载器
Class<?> loadedClass = classLoader.loadClass("java.lang.String");  // 动态加载类

4. java.lang.reflect

①介绍

image-ifkz.pngimage-goik.png

②API

③常用实例

Class<?> cls = Class.forName("java.lang.String"); 

Constructor<?>[] constructors = cls.getConstructors();  // 获取所有公共构造方法
for (Constructor<?> constructor : constructors) {
    System.out.println(constructor.getName());  // 输出构造方法的名称
}
Constructor<?> constructor = cls.getConstructor(String.class);  // 获取特定参数的构造方法



Method[] methods = cls.getMethods();  // 获取所有公共方法(包括继承的方法)
for (Method method : methods) {
    System.out.println(method.getName());  // 输出方法名
}
Method method = cls.getMethod("substring", int.class, int.class);  // 获取特定方法



Field[] fields = cls.getFields();  // 获取所有公共字段
for (Field field : fields) {
    System.out.println(field.getName());  // 输出字段名
}
Field field = cls.getDeclaredField("value");  // 获取特定字段(包括私有字段)
field.setAccessible(true);  // 设置访问权限

5. 反射和泛型

  • 类型擦除:如前所述,Java 泛型的类型参数会在编译时被擦除,因此在运行时无法直接访问泛型类型参数。

  • 无法获取具体泛型类型:如果我们只是获取一个泛型类的 Class 对象(如 List.class),我们将无法得知其具体的类型参数。只能知道它是一个泛型类(List),但无法知道是 List<String> 还是 List<Integer>

  • 复杂的泛型嵌套:对于复杂的嵌套泛型类型,反射需要更加复杂的代码来解析所有的类型参数。

  • 反射和泛型的结合:尽管反射不能直接获取泛型的实际类型参数,但通过 ParameterizedType 等反射 API,可以在某些情况下获取泛型类型的信息,尤其是在类的字段和方法参数中。

①泛型Class类

  • Class类是泛型类,String.class实际上是一个Class<String>类的对象(也是唯一对象)。

②使用Class<T>参数进行类型匹配

③API

④实例

  • 我们有一个泛型类 Box<T>, 以及一个泛型方法 printList(List<T> list),我们将使用反射来获取类和方法的泛型类型参数、类型名称和边界信息。

public class GenericExample<T extends Number>
{

    private T value;

    // 泛型方法
    public <T> void printList(List<T> list)
    {
        for (T item : list)
        {
            System.out.println(item);
        }
    }

    public T getValue()
    {
        return value;
    }

    public static void main(String[] args) throws NoSuchMethodException
    {
        // 反射获取泛型类的信息
        Class<?> clazz = GenericExample.class;

        // 获取类的类型参数
        System.out.println("Class Type Parameters:");
        TypeVariable<?>[] classTypeVariables = clazz.getTypeParameters();
        for (TypeVariable<?> typeVariable : classTypeVariables)
        {
            System.out.println("Type name: " + typeVariable.getName());
            for (Type bound : typeVariable.getBounds())
            {
                System.out.println("Bound: " + bound);
            }
        }

        // 反射获取泛型方法的信息
        Method method = GenericExample.class.getMethod("printList", List.class);
        System.out.println("\nMethod Type Parameters:");
        TypeVariable<?>[] methodTypeVariables = method.getTypeParameters();
        for (TypeVariable<?> typeVariable : methodTypeVariables)
        {
            System.out.println("Type name: " + typeVariable.getName());
            for (Type bound : typeVariable.getBounds())
            {
                System.out.println("Bound: " + bound);
            }
        }
    }
}

//输出
Class Type Parameters:
Type name: T
Bound: class java.lang.Number

Method Type Parameters:
Type name: T
Bound: class java.lang.Object

八. 并发

1. 介绍

  • Java 并发是指在一个程序中同时执行多个任务的能力。Java 提供了多种并发机制来支持多线程编程、并发控制和任务调度等。在 Java 中,并发主要通过以下几种方式实现:

  • 线程(Thread)

  • 线程池(Executor)

  • 同步(Synchronization)

  • 并发集合类

  • 原子变量

  • 并发工具类(如 CountDownLatchCyclicBarrier 等)

  • 不同实例对象:每个线程访问不同的对象实例时,不会发生并发问题,因为每个对象有自己的实例变量,不会被多个线程共享。

  • 同一实例对象:当多个线程访问 同一实例对象 时,可能会产生 多线程并发问题。主要原因是多个线程同时访问并操作同一个对象的共享资源(通常是实例变量),如果没有适当的同步机制,可能导致数据不一致、竞态条件、丢失更新等问题。

  • 静态变量或共享资源:如果类中有静态变量或资源被多个线程共享,那么多个线程同时访问这些资源时可能会发生线程安全问题。这时需要通过同步机制(如 synchronizedReentrantLock 等)来保证线程安全。

  • 当多个线程访问同一类的 不同静态资源 时,通常不会引发并发问题,前提是 这些静态资源之间没有相互依赖或者共享的状态

2. 线程

①介绍

  • 进程:是操作系统分配资源的基本单位,每个进程都有独立的内存空间和资源,进程间相互隔离,通常需要特殊的机制(如管道、共享内存)进行通信。进程的创建和切换有较大的开销,适用于需要独立运行、互不干扰的任务。

  • 线程:是进程中的最小执行单元,多个线程共享同一个进程的内存和资源,因此线程间通信非常高效。线程切换的开销较小,适用于需要并发执行、共享资源的任务,但需要特别注意线程安全的问题。

  • Java 的基本并发单位是线程。Thread 类和 Runnable 接口是 Java 提供的多线程编程的基本组成部分。

  • Thread 类:表示一个线程,可以通过继承 Thread 类来创建线程。

  • Runnable 接口:比继承 Thread 类更推荐的方式,可以通过实现 Runnable 接口并将其传递给 Thread 构造函数来创建线程。

  • 线程状态

  • 新建(New)

  • 可运行(Runnable)(就绪)

  • 阻塞(Blocked)

  • 等待 (Waiting)

  • 计时等待(Timed waiting)

  • 终止(Terminated)

继承 Thread 类
class MyThread extends Thread
{
    @Override
    public void run()
    {
        System.out.println("Thread is running...");
    }
}

public class ThreadExample
{
    public static void main(String[] args)
    {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}
实现 Runnable 接口
class MyRunnable implements Runnable
{
    @Override
    public void run()
    {
        System.out.println("Runnable is running...");
    }
}

public class RunnableExample
{
    public static void main(String[] args)
    {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start(); // 启动线程
    }
}

②新建线程

  • 当用new操作符创建一个新线程时,如new Thread(r),此时线程还没有开始运行(run也没执行),线程处于新建状态。

③可运行线程(就绪)

  • 一旦调用start方法,线程就处于可运行状态。一个可运行的线程可能正在运行也可能没有运行。

④阻塞和等待线程

  • 当线程处于阻塞或在等待状态时,它暂时是不活动的。

  • Thred.sleep和计时版的Object.wait、Thread.join、Lock.tryLock、Condition.await。

⑤终止线程

  • 原因

  • 由于run方法正常退出,线程自然终止。

  • 因为一个没有捕获的异常终止了run方法,使线程意外终止。

⑥中断线程

  • 中断线程是通过 Thread 类的 interrupt() 方法来实现的。中断并不直接终止线程,而是通过设置线程的中断标志来通知线程进行适当的处理。线程是否响应中断,取决于线程中的代码逻辑,尤其是如何处理中断信号。

class MyThread extends Thread
{
    @Override
    public void run()
    {
        try
        {
            for (int i = 0; i < 10; i++)
            {
                if (Thread.interrupted())
                {
                    System.out.println("Thread was interrupted");
                    break;
                }
                System.out.println("Running " + i);
                Thread.sleep(1000);  // 模拟任务
            }
        } catch (InterruptedException e)
        {
            System.out.println("Thread was interrupted during sleep");
        }
    }
}

public class InterruptExample
{
    public static void main(String[] args) throws InterruptedException
    {
        MyThread thread = new MyThread();
        thread.start();

        // 主线程睡眠2秒后中断子线程
        Thread.sleep(2000);
        thread.interrupt();
    }
}

⑦守护线程

⑧线程名

  • 在 Java 中,每个线程都可以有一个名称,用于标识线程。线程名称可以帮助我们在调试和日志记录时更方便地识别不同的线程。线程的名称可以在创建时指定,也可以在运行时修改。

  • 当一个线程被创建时,如果没有指定线程名称,Java 会为其分配一个默认的名称。默认名称通常是:主线程main其他线程Thread-N(其中 N 是线程的编号,从 0 开始)。

  • 可以在创建线程时通过 Thread 类的构造函数指定线程名,或者在线程运行时通过 setName() 方法来修改线程名称。

创建时指定线程名
class MyThread extends Thread
{
    @Override
    public void run()
    {
        System.out.println("Thread name: " + getName());
    }
}

public class ThreadNameExample
{
    public static void main(String[] args)
    {
        MyThread thread1 = new MyThread();
        thread1.setName("WorkerThread1");  // 设置线程名称
        thread1.start();

        MyThread thread2 = new MyThread();
        thread2.setName("WorkerThread2");
        thread2.start();
    }
}
*****
class MyRunnable implements Runnable
{
    @Override
    public void run()
    {
        System.out.println(Thread.currentThread().getName() + " is executing the task");
    }
}

public class ThreadNameExample
{
    public static void main(String[] args)
    {
        Runnable task = new MyRunnable();

        // 通过构造函数指定线程名
        Thread thread1 = new Thread(task, "WorkerThread-1");
        thread1.start();

        Thread thread2 = new Thread(task, "WorkerThread-2");
        thread2.start();
    }
}
运行时修改线程名
class MyThread extends Thread
{
    @Override
    public void run()
    {
        System.out.println("Thread name: " + getName());
    }
}

public class ThreadNameExample
{
    public static void main(String[] args)
    {
        MyThread thread = new MyThread();
        thread.start();

        // 修改线程名称
        thread.setName("ModifiedThread");
        System.out.println("Thread name after modification: " + thread.getName());
    }
}

⑨未捕获异常的处理器

  • 线程的run方法不能抛出任何检查型异常,但是非检查型异常可能会导致线程终止

  • 对于可以传播的异常(非检查异常),并没有任何catch子句。在线程死亡之前,异常会传递到一个用于处理未捕获异常的处理器。

  • 未捕获异常的处理是通过 Thread.UncaughtExceptionHandler 接口来实现的。UncaughtExceptionHandler 允许开发者定义一个处理未捕获异常的回调方法,从而在异常没有被捕获时进行相应的处理,比如打印日志、发送通知等。处理器必须是一个实现上述接口的类。

  • Thread.UncaughtExceptionHandler 是一个接口,它只有一个方法:public void uncaughtException(Thread t, Throwable e); t:抛出异常的线程。e:抛出的异常。

  • 要为某个线程或所有线程设置未捕获异常处理器,必须实现 UncaughtExceptionHandler 接口,并通过 Thread.setUncaughtExceptionHandler() 方法来设置。

  • 可以通过 Thread.setDefaultUncaughtExceptionHandler() 为所有线程设置一个默认的处理器。当任何线程未捕获异常时,都会使用这个处理器。若没安装默认处理器,则默认处理器为null。若没有为单个线程安装处理器,那么处理器为该线程的ThreadGroup对象。

为单个线程设置未捕获异常处理器
class MyThread extends Thread
{
    @Override
    public void run()
    {
        // 故意抛出一个异常,未捕获
        int result = 10 / 0;  // 这会抛出 ArithmeticException
    }
}

public class UncaughtExceptionHandlerExample
{
    public static void main(String[] args)
    {
        // 创建一个线程
        MyThread thread = new MyThread();

        // 为该线程设置 UncaughtExceptionHandler
        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler()
        {
            @Override
            public void uncaughtException(Thread t, Throwable e)
            {
                System.out.println("Caught an exception in thread: " + t.getName());
                System.out.println("Exception: " + e);
            }
        });

        // 启动线程
        thread.start();
    }
}
*****
为所有线程设置未捕获异常处理器
public class UncaughtExceptionHandlerExample
{
    public static void main(String[] args)
    {
        // 设置全局的未捕获异常处理器
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler()
        {
            @Override
            public void uncaughtException(Thread t, Throwable e)
            {
                System.out.println("Global handler caught an exception in thread: " + t.getName());
                System.out.println("Exception: " + e);
            }
        });

        // 创建并启动多个线程
        new Thread(() ->
        {
            int result = 10 / 0;  // 故意抛出 ArithmeticException
        }).start();

        new Thread(() ->
        {
            String str = null;
            str.length();  // 故意抛出 NullPointerException
        }).start();
    }
}

⑩线程优先级

3. 任务和线程池

①介绍

  • 线程池:是一个用于管理和复用线程的机制,特别适用于需要处理大量短小任务的应用程序。线程池的核心思想是将任务提交给池中的线程,由池中的线程来执行,而不是每次都创建新线程。这样可以减少频繁创建和销毁线程带来的性能开销,提高系统的响应速度和资源利用率。当有任务需要执行时,线程池中的线程就会去执行该任务;如果池中的线程都在工作,那么新的任务会被暂时保存在队列中,等待线程空闲后再执行。

  • 在 Java 中,线程池由 java.util.concurrent 包提供,主要通过 ExecutorExecutorService (是前一个子接口,提供更多服务)接口来管理。最常用的线程池实现是 ThreadPoolExecutor,和ScheduledThreadPoolExecutorExecutors 类提供了线程池的工厂方法来简化线程池的创建。


  • Future 是一个接口,用于表示异步计算的结果。Callable 是一个可以返回结果的任务,它与 Runnable 类似,但与 Runnable 不同的是,它能够返回结果并抛出异常。通过 ExecutorService.submit() 提交的任务会返回一个 Future 对象,程序可以使用该对象来获取任务的执行结果。

  • 在 Java 中,FutureCallable 是用于执行并发任务并获取结果的工具。它们通常与 ExecutorService 配合使用,允许我们以异步方式执行任务并获取执行结果。相比传统的 Thread 类,ExecutorService 提供了更强大、更灵活的线程管理功能,而 FutureCallable 更好地处理任务的结果和异常。

②线程池核心组件

  • 任务队列:用于存放等待执行的任务。

  • 工作线程:池中的线程,负责从队列中获取任务并执行。

  • 线程池大小:线程池中维护的线程数量,分为核心线程数和最大线程数。

③线程池的创建

  • 可以通过 Executors 类提供的静态方法来创建线程池,这些方法封装了常见的线程池创建模式。常见的线程池类型有:

固定大小线程池(newFixedThreadPool())
该线程池维护一个固定大小的线程池,如果线程池中的线程处于空闲状态,
则可以处理提交的任务。任务会被保存在任务队列中,直到有线程可用。
public class FixedThreadPoolExample
{
    public static void main(String[] args)
    {
        ExecutorService executor = Executors.newFixedThreadPool(3); // 创建一个包含 3 个线程的线程池

        Runnable task = () ->
        {
            System.out.println(Thread.currentThread().getName() + " is executing the task.");
        };

        for (int i = 0; i < 5; i++)
        {
            executor.submit(task); // 提交任务
        }

        executor.shutdown(); // 关闭线程池
    }
}
缓存线程池(newCachedThreadPool())
该线程池可根据需要创建新的线程,如果线程池中的线程在一定时间内空闲,
则会被终止。适用于执行很多短期异步任务的场景。
public class CachedThreadPoolExample
{
    public static void main(String[] args)
    {
        ExecutorService executor = Executors.newCachedThreadPool(); // 创建一个缓存线程池

        Runnable task = () ->
        {
            System.out.println(Thread.currentThread().getName() + " is executing the task.");
        };

        for (int i = 0; i < 5; i++)
        {
            executor.submit(task); // 提交任务
        }

        executor.shutdown(); // 关闭线程池
    }
}
单线程池(newSingleThreadExecutor())
该线程池保证所有任务按顺序执行,且只有一个工作线程。
适用于只需要一个线程顺序执行任务的场景。
public class SingleThreadExecutorExample
{
    public static void main(String[] args)
    {
        ExecutorService executor = Executors.newSingleThreadExecutor(); // 创建一个单线程池

        Runnable task = () ->
        {
            System.out.println(Thread.currentThread().getName() + " is executing the task.");
        };

        for (int i = 0; i < 5; i++)
        {
            executor.submit(task); // 提交任务
        }

        executor.shutdown(); // 关闭线程池
    }
}
定时线程池(newScheduledThreadPool())
该线程池用于定时或周期性地执行任务。它适用于调度任务的场景,例如定时任务和周期性任务。
public class ScheduledThreadPoolExample
{
    public static void main(String[] args)
    {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(3); // 创建一个定时线程池

        Runnable task = () ->
        {
            System.out.println(Thread.currentThread().getName() + " is executing the task.");
        };

        executor.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS); // 每 1 秒执行一次任务,并且任务间隔为 2 秒

        // Shutdown the executor after some time
        try
        {
            Thread.sleep(10000); // 让任务执行 10 秒
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        executor.shutdown(); // 关闭线程池
    }
}

④线程池的核心参数

  • ThreadPoolExecutor 是 Java 中最常用的线程池实现类,它提供了更灵活的配置和管理机制。ThreadPoolExecutor 构造函数中有多个重要的参数:

  • corePoolSize:核心线程池的大小,即线程池保持的最小线程数。

  • maximumPoolSize:线程池允许的最大线程数。

  • keepAliveTime:线程池中空闲线程的存活时间。超过这个时间没有任务要处理时,线程会被终止。

  • TimeUnitkeepAliveTime 参数的时间单位。

  • workQueue:用于存储等待执行任务的队列。常见的队列有:LinkedBlockingQueueArrayBlockingQueueSynchronousQueue 等。

  • threadFactory:用于创建新线程的工厂,允许自定义线程的名称、优先级等。

  • rejectedExecutionHandler:用于处理无法执行的任务的策略。常见的拒绝策略有:AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy

public class CustomThreadPoolExample
{
    public static void main(String[] args)
    {
        // 创建一个自定义的线程池
        ExecutorService executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                10, // 空闲线程存活时间
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingQueue<>(10), // 等待队列
                Executors.defaultThreadFactory(), // 线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        Runnable task = () ->
        {
            System.out.println(Thread.currentThread().getName() + " is executing the task.");
        };

        for (int i = 0; i < 15; i++)
        {
            executor.submit(task); // 提交任务
        }

        executor.shutdown(); // 关闭线程池
    }
}

⑤线程池的生命周期管理

  • 线程池通常在不再需要时进行关闭,以释放资源。关闭线程池可以使用 shutdown()shutdownNow() 方法。

  • shutdown():优雅关闭,线程池不再接受新任务,但会执行已经提交的任务。

  • shutdownNow():立即关闭,尝试停止所有正在执行的任务并返回待执行的任务。

⑥API

⑦Future接口

  • Future 接口

  • Future 是一个接口,代表一个异步执行任务的结果。它允许你获取任务执行的结果,检查任务是否完成,或取消任务。你通常通过 ExecutorService.submit() 提交一个任务,并获得一个 Future 对象来操作该任务。

  • 主要方法

  • get():获取任务的结果。如果任务没有完成,get()阻塞直到任务完成。如果任务执行时抛出异常,get() 会抛出异常。

  • get(long timeout, TimeUnit unit):在指定的时间内获取任务的结果,如果超时仍未完成,抛出 TimeoutException

  • cancel(boolean mayInterruptIfRunning):尝试取消任务。如果任务已开始执行并且可以中断,设置 mayInterruptIfRunningtrue 使其被中断。

  • isCancelled():检查任务是否被取消。

  • isDone():检查任务是否已经完成。

public class FutureExample
{
    public static void main(String[] args)
    {
        // 创建一个线程池
        ExecutorService executor = Executors.newSingleThreadExecutor();

        // 提交一个返回结果的 Callable 任务
        Future<Integer> future = executor.submit(() ->
        {
            System.out.println("Task is running...");
            Thread.sleep(2000);  // 模拟耗时操作
            return 42;  // 返回结果
        });

        try
        {
            // 获取任务的执行结果,阻塞直到任务完成
            Integer result = future.get();
            System.out.println("Task result: " + result);
        } catch (InterruptedException | ExecutionException e)
        {
            e.printStackTrace();
        } finally
        {
            executor.shutdown();
        }
    }
}

⑧Callable接口

  • Callable 接口

  • Callable 接口与 Runnable 类似,但与 Runnable 不同的是,Callable 可以返回一个结果,并且可以抛出异常。Callable 是在并发编程中获取任务返回值的核心接口。

  • 主要方法:V call():执行任务的入口方法,返回任务的计算结果,可以抛出异常。

  • Callable 的设计目的主要是为了解决 Runnable 接口不能返回值和抛出异常的问题。Runnable 只能执行任务,并不能返回结果,而 Callable 可以通过 ExecutorService.submit() 提交,并由 Future 获取返回结果。

public class CallableExample
{
    public static void main(String[] args)
    {
        // 创建一个线程池
        ExecutorService executor = Executors.newCachedThreadPool();

        // 提交一个返回结果的 Callable 任务
        Callable<Integer> task = () ->
        {
            System.out.println("Task is running...");
            Thread.sleep(2000);  // 模拟耗时操作
            return 42;  // 返回结果
        };

        // 提交任务并获得 Future 对象
        Future<Integer> future = executor.submit(task);

        try
        {
            // 获取任务的执行结果,阻塞直到任务完成
            Integer result = future.get();
            System.out.println("Task result: " + result);
        } catch (InterruptedException | ExecutionException e)
        {
            e.printStackTrace();
        } finally
        {
            executor.shutdown();
        }
    }
}

⑨FutureTask

  • FutureTask 是 Java 中表示异步执行任务的一个类。它实现了 RunnableFuture 接口,允许你在独立的线程中执行任务并获取计算结果。通常与线程池或执行器一起使用。

  • FutureTask 的关键方法:

  • run():执行任务的方法,通常在任务被提交到执行器时调用。

  • get():获取任务的执行结果。如果任务还未完成,则会阻塞当前线程,直到任务完成。

  • cancel():尝试取消任务的执行。如果任务还没有开始执行或者已经完成,则取消失败。

  • isDone():检查任务是否完成,无论是正常完成还是因为取消而完成。

  • isCancelled():检查任务是否被取消。

public class FutureTaskExample
{
    public static void main(String[] args) throws Exception
    {
        // 创建一个 Callable 任务
        Callable<Integer> callableTask = new Callable<Integer>()
        {
            @Override
            public Integer call() throws Exception
            {
                Thread.sleep(2000);  // 模拟长时间任务
                return 123;          // 返回结果
            }
        };

        // 创建 FutureTask 实例
        FutureTask<Integer> futureTask = new FutureTask<>(callableTask);

        // 启动线程执行任务
        Thread thread = new Thread(futureTask);
        thread.start();

        // 获取任务执行结果
        System.out.println("Task result: " + futureTask.get());  // 这里会阻塞直到任务完成
    }
}

⑩控制任务组

①①Fork/Join框架

4. 同步

①介绍

  • 在 Java 中,同步是指在多线程环境下控制对共享资源的访问,以避免多个线程同时操作共享资源导致数据不一致或程序异常。同步机制通过确保一次只有一个线程能访问某一代码块或资源,从而避免并发问题。Java 提供了多种同步方法来保证线程安全。

②常见问题

  • 竞态条件(Race Condition):多线程竞争共享资源,导致数据的不一致性。通过同步可以避免竞态条件。

  • 死锁(Deadlock):多个线程因相互等待资源而无法继续执行。通过避免嵌套锁或使用 tryLock() 可以避免死锁。

  • 资源饥饿(Starvation):某些线程一直无法获得资源,导致无法执行。通过公平锁或合理调度线程优先级来避免饥饿。

③synchronized 关键字

  • synchronized 是 Java 提供的最常用同步机制,用来保证同一时刻只有一个线程能够访问某个资源或代码块。

  • Java中每个对象都有一个内部锁。

  • 方式:同步方法、同步代码块、锁住类对象

同步方法
将整个方法声明为 synchronized,它会锁住当前对象
(对于实例方法)或类对象(对于静态方法),从而避免其他线程同时访问。

实例方法的同步:锁住当前实例对象。
public synchronized void increment() {
    count++;  // 线程安全的增加操作
}
****
静态方法的同步:锁住类对象,即 Class 对象。
public static synchronized void increment() {
    count++;  // 线程安全的增加操作
}
同步代码块
同步代码块(synchronized 块)不会锁住整个实例或类,而是锁住代码块中指定的对象
synchronized 还可以用来修饰方法中的一部分代码块,锁住指定的对象。
这样可以避免锁住整个方法,提高程序的效率。
public void increment() {
    synchronized (this) {  // 锁住当前实例对象
        count++;
    }
}

④Lock接口和条件对象

  • Lock 是 Java 中更为灵活的同步机制,位于 java.util.concurrent.locks 包中。Lock 提供了比 synchronized 更加细粒度的控制,例如可中断的锁、尝试锁等。

  • ReentrantLockLock 接口的常见实现类,它提供了比 synchronized 更强大的功能,例如显式锁、可中断的锁、定时锁等。它是可重入的,意味着同一个线程可以多次获取锁而不会发生死锁。

使用 Lock 来同步
public class Counter
{
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment()
    {
        lock.lock();  // 获取锁
        try
        {
            count++;  // 线程安全的增加操作
        } finally
        {
            lock.unlock();  // 释放锁
        }
    }
}
尝试获取锁(tryLock())
tryLock() 方法允许线程尝试获得锁,如果锁已被其他线程持有,它不会阻塞,而是直接返回。
public class Counter
{
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment()
    {
        if (lock.tryLock())
        {  // 尝试获取锁
            try
            {
                count++;
            } finally
            {
                lock.unlock();
            }
        } else
        {
            System.out.println("Could not acquire lock");
        }
    }
}

  • 条件对象是 Java 中用于线程间通信的机制,主要通过 Condition 接口实现。它是与 Lock 一起使用的,提供了比 synchronizedObject 类的 wait()/notify() 更强大和灵活的线程协调能力。

  • Lock 配合使用Condition 必须由 Lock(如 ReentrantLock)对象创建,不能独立使用。

  • 多条件队列支持:一个 Lock 可以创建多个 Condition 对象,从而支持多个等待队列。

  • await():让线程等待,释放锁,并进入该条件的等待队列。

  • signal():唤醒等待该条件的某个线程。哪个被唤醒的线程会重新进入该对象,若锁可用则得到锁,并从之前暂停的地方继续执行。

  • signalAll():唤醒所有等待该条件的线程。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample
{

    public static void main(String[] args)
    {
        Buffer buffer = new Buffer();

        // 创建生产者线程
        Thread producer = new Thread(() ->
        {
            for (int i = 0; i < 10; i++)
            {
                try
                {
                    buffer.put(i);
                    Thread.sleep(500); // 模拟生产时间
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        });

        // 创建消费者线程
        Thread consumer = new Thread(() ->
        {
            for (int i = 0; i < 10; i++)
            {
                try
                {
                    buffer.get();
                    Thread.sleep(1000); // 模拟消费时间
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

class Buffer
{
    private int data;
    private boolean available = false; // 标记缓冲区是否有数据(是否已满)
    private final Lock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();

    // 生产数据
    public void put(int value) throws InterruptedException
    {
        lock.lock(); // 获取锁
        try
        {
            while (available)
            { // 如果缓冲区已满,等待
                notFull.await();//释放锁
            }
            data = value;
            available = true;
            System.out.println("Produced: " + value);
            notEmpty.signal(); // 通知消费者缓冲区不为空
        } finally
        {
            lock.unlock(); // 释放锁
        }
    }

    // 消费数据
    public void get() throws InterruptedException
    {
        lock.lock(); // 获取锁
        try
        {
            while (!available)
            { // 如果缓冲区为空,等待
                notEmpty.await();
            }
            System.out.println("Consumed: " + data);
            available = false;
            notFull.signal(); // 通知生产者缓冲区不满
        } finally
        {
            lock.unlock(); // 释放锁
        }
    }
}

⑤ReadWriteLock 接口

  • ReadWriteLock 是一种特殊的锁机制,它为读操作和写操作提供了不同的锁。读操作是共享的,即多个线程可以同时读取,但写操作是独占的,只有一个线程可以写入。ReadWriteLock 包含两个锁:

  • readLock():用于读操作。

  • writeLock():用于写操作。

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteExample
{
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private int count = 0;

    public void read()
    {
        lock.readLock().lock();
        try
        {
            System.out.println("Reading: " + count);
        } finally
        {
            lock.readLock().unlock();
        }
    }

    public void write(int value)
    {
        lock.writeLock().lock();
        try
        {
            count = value;
            System.out.println("Writing: " + count);
        } finally
        {
            lock.writeLock().unlock();
        }
    }
}

⑥volatile

  • volatile 是 Java 中的一种轻量级同步机制,用于保证多线程环境下变量的可见性。它被用来修饰实例变量或类变量,确保该变量在多个线程之间的变化是可见的,防止线程之间的缓存不一致问题。

  • volatile 关键字告诉 Java 虚拟机(JVM)和线程:

  • 可见性:当一个线程修改了被 volatile 修饰的变量的值,其他线程可以立即看到这个修改的值。即当一个线程更新该变量时,其他线程会从主内存中重新读取它,而不是从线程自己的工作内存(CPU 缓存或寄存器)中读取。

  • 禁止指令重排序volatile 保证对该变量的写操作不会与其他操作重排序,防止编译器和 CPU 执行指令优化时造成不可预测的结果。

  • 虽然 volatile 可以确保可见性和禁止指令重排序,但它 不保证原子性。换句话说,对于复合操作(如 i++),volatile 并不能保证线程安全,因为这些操作涉及多个步骤(读取、计算、写回),多个线程同时访问时仍然可能出现问题。

  • 工作原理:在 Java 中,每个线程都有自己的工作内存(也叫做 CPU 缓存)。当一个线程修改了变量的值时,这个修改首先会写入到该线程的工作内存中。其他线程如果要读取该变量时,可能会从自己的工作内存中获取一个过时的值。

  • volatile 确保:

  • 线程写入的值会立即刷新到主内存中,而其他线程也能立即看到这个更新。

  • 线程从主内存读取变量时会获取最新的值,而不会使用自己的缓存。

public class VolatileExample
{
    private volatile boolean flag = false; // volatile 修饰变量

    public void toggleFlag()
    {
        flag = !flag; // 改变变量的值
    }

    public void printFlag()
    {
        while (!flag)
        {  // 在其他线程中检查变量
            System.out.println("Flag is still false.");
        }
        System.out.println("Flag is now true.");
    }

    public static void main(String[] args) throws InterruptedException
    {
        VolatileExample example = new VolatileExample();

        Thread t1 = new Thread(example::printFlag);
        Thread t2 = new Thread(example::toggleFlag);

        t1.start();
        Thread.sleep(100);  // 确保 t1 先执行
        t2.start();
    }
}

⑦final变量

  • 当字段声明为final时,多个线程可以安全的读取一个字段。因为 final 关键字保证了字段在构造时一旦初始化完成,其值就不能再被修改,并且这种初始化对所有线程都是可见的。

⑧为什么废弃stop和suspend方法

⑨按需初始化

  • 有时对于某些数据结构,可能希望第一次需要它时才进行初始化,且希望确保这种初始化只发生一次。

  • 虚拟机会在第一次使用类时执行一个静态初始化器,而且只执行一次,虚拟机利用一个锁来确保这一点。

⑩线程局部变量

  • 线程局部变量(Thread-Local Variables)是指每个线程都有自己的变量副本,线程之间不会互相干扰。即使多个线程访问同一个变量,每个线程都会有自己独立的副本,其他线程无法看到该副本。这种变量通常用于避免线程间共享数据,从而避免同步问题(采用同步开销过大)。

  • 在 Java 中,可以通过 ThreadLocal 类来实现线程局部变量。每个线程会有自己独立的存储空间来保存该变量的值,这样即使不同的线程使用同一个 ThreadLocal 实例,它们之间的数据也不会相互干扰。

  • ThreadLocal 常用于以下几种情况:

  • 数据库连接:每个线程可以有自己独立的数据库连接对象,避免线程之间共享同一个连接。

  • Session 管理:每个线程可以保存自己的 HttpSession,避免不同用户的 session 混淆。

  • 线程上下文信息:例如,日志记录中每个线程保存自己独立的用户信息、请求 ID 等上下文数据。

class ThreadLocalExample
{

    // 创建一个ThreadLocal对象
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) throws InterruptedException
    {

        // 创建并启动多个线程
        Thread thread1 = new Thread(() ->
        {
            threadLocal.set(10); // 为线程1设置线程局部变量的值
            System.out.println("Thread1 local value: " + threadLocal.get()); // 获取线程局部变量
        });

        Thread thread2 = new Thread(() ->
        {
            threadLocal.set(20); // 为线程2设置线程局部变量的值
            System.out.println("Thread2 local value: " + threadLocal.get()); // 获取线程局部变量
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        // 主线程访问ThreadLocal
        System.out.println("Main thread local value: " + threadLocal.get());
    }
}

5. 并发集合类

①介绍

  • Java 提供了一组专门用于处理并发环境下的集合类,称为 并发集合类。这些集合类位于 java.util.concurrent 包中,旨在解决多线程环境中对集合类进行操作时可能遇到的线程安全问题。与传统的同步集合类(如 VectorHashtable)相比,并发集合类提供了更高效的并发操作,它们通过内置的同步机制或其他优化策略来保证线程安全。

  • 并发集合类的优势

  • 线程安全:并发集合类采用内部锁机制或无锁设计,以保证多个线程可以安全地并发访问。

  • 高效性:并发集合类通过减少同步开销、使用细粒度锁或无锁设计,提供了比传统集合类更高的并发性能。

  • 增强功能:并发集合类在设计上往往支持更丰富的并发操作,如原子操作、并发读写等。

②ConcurrentHashMap

  • ConcurrentHashMapjava.util.Map 接口的一个实现,支持高效的并发操作。它通过将整个 Map 分割成多个段,每个段有自己的锁,允许多个线程并发访问不同的段。与 HashtablesynchronizedMap 不同,ConcurrentHashMap 允许多个线程同时读取,并且对写操作提供细粒度锁。

  • 线程安全:对 putgetremove 等操作提供并发支持。

  • 无阻塞操作:读取操作几乎不需要锁,可以并发进行。

  • 更高的吞吐量:相比于 Hashtable,它能更好地处理大量并发请求。


  • 原子更新:原子操作方法 computemerge

  • ConcurrentHashMap不允许有null值,很多方法都用null值指示映射中某个给定的键不存在。若传入compute和merge的函数返回null,将从映射中删除现有条目。


  • 并发散列映射的批操作

  • 在 Java 中,ConcurrentHashMap 提供了对并发操作的支持,并且也支持一些批量操作,这些操作可以显著提高性能,尤其是在高并发环境下。批量操作通常涉及多个键值对的操作,在并发场景中使用批量操作可以减少锁的竞争,提高效率。


  • 并发集视图

import java.util.concurrent .*;

public class ConcurrentHashMapExample
{
    public static void main(String[] args)
    {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 多线程并发插入
        for (int i = 0; i < 10; i++)
        {
            final int index = i;
            new Thread(() ->
            {
                map.put("key" + index, index);
            }).start();
        }

        // 输出所有的键值对
        map.forEach((key, value) -> System.out.println(key + ": " + value));
    }
}
不安全的更新
public class Test
{
    static final ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();

    public static void main(String[] args)
    {
        String word="wcc";
        
        Long oldv=map.get(word);
        Long newv=oldv==null?1:oldv+1;
        map.put(word, newv);
    }
}


旧版原子更新方法
public class Test
{
    static final ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();

    public static void main(String[] args)
    {
        String word="wcc";

        Long oldv;
        Long newv;
        do
        {
            oldv=map.get(word);
            newv=oldv==null?1:oldv+1;//只有旧值和map中的一样,即没被别的线程修改,才会更新
        }while (!map.replace(word, oldv, newv));
    }
}
//旧
public class Test
{
    static final ConcurrentHashMap<String, AtomicLong> map = new ConcurrentHashMap<>();

    public static void main(String[] args)
    {
        String word="wcc";
        map.putIfAbsent(word, new AtomicLong());
        map.get(word).incrementAndGet();//几个方法都是确保了原子性
    }
}

新版原子更新方式
ConcurrentHashMap 提供了原子操作方法 compute,
它可以确保在一个原子操作中同时读取、计算和更新值,从而避免上述问题。
public class Test
{
    static final ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();

    public static void main(String[] args)
    {
        String word = "wcc";

        map.compute(word, (key, oldv) -> oldv == null ? 1 : oldv + 1);
    }
}

新
ConcurrentHashMap 还提供了 merge 方法,它也是线程安全的,并且可以避免竞争条件。
public class Test
{
    static final ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();

    public static void main(String[] args)
    {
        String word = "wcc";
        merge 方法会将当前值与 1L 合并,如果 word 对应的值不存在,
        则设置为 1L,否则将原值与 1L 相加。这个操作是原子的,因此不会发生线程安全问题。
        map.merge(word, 1L, Long::sum);
    }
}

③CopyOnWriteArrayList

  • CopyOnWriteArrayList 是线程安全的 ArrayList 实现,每当修改操作(如 addremove)发生时,它会创建一个新的副本来保存数据。这使得它非常适合读多写少的场景。虽然 addremove 操作会有额外的开销,但读操作是无锁的,可以并发执行。

  • 适合场景:读多写少的场景。

  • 线程安全:所有修改操作都会创建副本,保证线程安全。

class CopyOnWriteArrayListExample
{
    public static void main(String[] args)
    {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

        // 启动多个线程进行并发写入
        for (int i = 0; i < 10; i++)
        {
            final int index = i;
            new Thread(() ->
            {
                list.add(index);
            }).start();
        }

        // 读取列表中的元素
        list.forEach(System.out::println);
    }
}

④CopyOnWriteArraySet

  • CopyOnWriteArraySet 是线程安全的 Set 实现,底层使用 CopyOnWriteArrayList 来存储元素。它适用于并发读多写少的场景,提供了与 HashSet 相同的行为,但通过复制集合来保证线程安全。

  • 线程安全:类似 CopyOnWriteArrayList,所有写操作都会复制数据。

  • 适合场景:读多写少的场景。

public class CopyOnWriteArraySetExample
{
    public static void main(String[] args)
    {
        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();

        // 启动多个线程进行并发写入
        for (int i = 0; i < 10; i++)
        {
            final String value = "element" + i;
            new Thread(() ->
            {
                set.add(value);
            }).start();
        }

        // 读取并输出所有元素
        set.forEach(System.out::println);
    }
}

⑤BlockingQueue 接口及其实现

  • BlockingQueue 是一个用于多线程环境下的线程安全队列。它的主要特点是当队列为空时,消费者线程会被阻塞,直到有新的元素加入;而当队列已满时,生产者线程也会被阻塞,直到有空间可用。BlockingQueue 常用于生产者-消费者模式。

  • ArrayBlockingQueue:基于数组的有界阻塞队列, 指定容量。

  • LinkedBlockingQueue:基于链表的阻塞队列,支持可选的容量。

  • PriorityBlockingQueue:具有优先级的阻塞队列,无容量限制。

  • SynchronousQueue:每个 put 操作都必须等待一个 take 操作,适用于传递任务的场景。

public class BlockingQueueExample
{
    public static void main(String[] args) throws InterruptedException
    {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);

        // 生产者线程
        new Thread(() ->
        {
            for (int i = 0; i < 10; i++)
            {
                try
                {
                    queue.put(i);  // 向队列中插入元素
                    System.out.println("Produced: " + i);
                } catch (InterruptedException e)
                {
                    Thread.currentThread().interrupt();
                }
            }
        }).start();

        // 消费者线程
        new Thread(() ->
        {
            for (int i = 0; i < 10; i++)
            {
                try
                {
                    Integer item = queue.take();  // 从队列中获取元素
                    System.out.println("Consumed: " + item);
                } catch (InterruptedException e)
                {
                    Thread.currentThread().interrupt();
                }
            }
        }).start();
    }
}

⑥ConcurrentSkipListMap 和 ConcurrentSkipListSet

  • ConcurrentSkipListMapConcurrentSkipListSet 提供了线程安全的有序映射和集合,底层实现是跳表(Skip List)。这使得它们在多线程环境下也能保持有序性,并且能够高效地支持并发操作。

  • ConcurrentSkipListMap:线程安全的有序映射,支持并发访问。

  • ConcurrentSkipListSet:线程安全的有序集合。

public class ConcurrentSkipListMapExample
{
    public static void main(String[] args)
    {
        ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();

        // 启动多个线程进行并发写入
        for (int i = 0; i < 10; i++)
        {
            final String key = "key" + i;
            final int value = i;
            new Thread(() ->
            {
                map.put(key, value);
            }).start();
        }

        // 输出有序的键值对
        map.forEach((key, value) -> System.out.println(key + ": " + value));
    }
}

⑦ConcurrentLinkedQueue

  • ConcurrentLinkedQueue 是 Java 中的一种无界线程安全的队列实现,它属于 java.util.concurrent 包,专为并发环境下设计,能够有效地处理多个线程并发操作队列的问题。ConcurrentLinkedQueue 采用了非阻塞的 CAS(Compare-And-Swap) 技术,保证线程安全性,而不会产生线程之间的锁竞争,因此它非常适用于高并发的场景。

  • 特性:

  • 无界队列:队列的容量没有上限,随着元素的加入,队列会动态扩展。

  • 线程安全:通过非阻塞算法(CAS)保证了多线程操作时的安全。

  • FIFO 顺序:遵循先进先出(First In, First Out)的队列规则。

  • 适用于高并发场景:由于使用了 CAS 技术,ConcurrentLinkedQueue 不需要传统的锁机制,能有效减少线程之间的竞争和阻塞,提高性能。

  • 常用方法

  • offer(E e):将元素 e 添加到队列的尾部,如果添加成功,则返回 true

  • poll():从队列头部移除并返回一个元素,如果队列为空,则返回 null

  • peek():查看队列头部的元素,但不移除它,如果队列为空,则返回 null

  • isEmpty():检查队列是否为空。

  • size():获取队列中的元素个数。

public class ConcurrentLinkedQueueExample
{
    public static void main(String[] args) throws InterruptedException
    {
        // 创建一个ConcurrentLinkedQueue实例
        ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();

        // 启动生产者线程,将元素放入队列
        Thread producer = new Thread(() ->
        {
            for (int i = 0; i < 5; i++)
            {
                queue.offer(i);
                System.out.println("Produced: " + i);
            }
        });

        // 启动消费者线程,从队列中取出元素
        Thread consumer = new Thread(() ->
        {
            for (int i = 0; i < 5; i++)
            {
                Integer value = queue.poll();
                if (value != null)
                {
                    System.out.println("Consumed: " + value);
                }
            }
        });

        // 启动线程
        producer.start();
        consumer.start();

        // 等待线程结束
        producer.join();
        consumer.join();
    }
}

⑧并行数组算法

  • 在 Java 中,处理大规模数据集时,并行数组算法可以显著提高性能,特别是在多核处理器上。通过并行化操作,我们可以在多个 CPU 核心上同时执行计算,从而加速处理过程。Java 提供了一些内置的机制来帮助我们并行化数组的操作,最常见的是使用 parallelStreamArrays.parallelSort() 等方法。

  • parallelStream() 是 Java 8 引入的流(Stream)API的一部分,它允许你轻松地并行处理集合,包括数组。使用 parallelStream(),Java 会自动将数据拆分成小块,并在多个线程上并行执行操作。

  • Arrays.parallelSort() 是 Java 8 引入的方法,它能够对数组进行并行排序。parallelSort() 会自动将数组拆分为多个子数组,使用多个线程进行排序,然后将结果合并。

  • Java 8 引入了 IntStreamDoubleStream 等原始类型流,它们可以直接进行并行处理,不需要手动装箱。可以通过 parallel() 方法将流转为并行流。

  • parallelSetAll() 是 Java 8 引入的一个方法,属于 Arrays 类的一部分,主要用于并行化对数组的元素赋值操作。它允许你使用并行流的方式修改数组中的元素,从而充分利用多核处理器进行并行计算。这个操作是基于数组的 索引,而不是元素的值。即lambda x是索引。

  • parallelPrefix() 是 Java 8 引入的 Arrays 类中的一个方法,用于执行并行前缀操作。它可以对数组执行类似累加(prefix sum)、累乘、最大值等操作,并将结果存储在原始数组中。

parallelStream()
public class ParallelArrayExample
{
    public static void main(String[] args)
    {
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

        // 使用 parallelStream 并行求和
        int sum = Arrays.stream(arr)
                .parallel()  // 将流转为并行流
                .sum();

        System.out.println("Sum: " + sum);  // 输出:Sum: 55
    }
}
parallelSort
public class ParallelSortExample
{
    public static void main(String[] args)
    {
        int[] arr = {9, 4, 7, 3, 1, 8, 6, 5, 2, 0};

        // 使用 parallelSort 进行并行排序
        Arrays.parallelSort(arr);

        System.out.println("Sorted Array: " + Arrays.toString(arr));
    }
}
IntStream
public class ParallelIntStreamExample
{
    public static void main(String[] args)
    {
        int[] arr = {1, 2, 3, 4, 5};

        // 使用 IntStream 并行计算平方和
        int sumOfSquares = IntStream.of(arr)
                .parallel()  // 并行处理
                .map(x -> x * x)
                .sum();

        System.out.println("Sum of squares: " + sumOfSquares);  // 输出:55
    }
}
parallelSetAll
class ParallelSetAllExample
{
    public static void main(String[] args)
    {
        int[] array = new int[10];

        //
        // 使用 parallelSetAll 将数组的每个元素设置为其索引的平方
        Arrays.parallelSetAll(array, x->x*x);

        // 输出修改后的数组
        System.out.println(Arrays.toString(array));  // 输出:[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    }
}
parallelPrefix
public class ParallelPrefixExample
{
    public static void main(String[] args)
    {
        int[] array = {1, 2, 3, 4, 5};

        // 使用 parallelPrefix 计算前缀和
        Arrays.parallelPrefix(array, (a, b) -> a + b);

        // 输出修改后的数组,每个元素是其前缀和
        System.out.println(Arrays.toString(array));  // 输出:[1, 3, 6, 10, 15]
    }
}

⑨较早的线程安全集合

6. 原子变量

①介绍

  • 在 Java 中,原子变量(Atomic Variables)是指通过原子操作(即不可分割的操作)保证线程安全的变量。这些操作通常通过 java.util.concurrent.atomic 包中的类来实现,能够确保多个线程对同一变量的并发访问不会导致竞态条件。

  • 原子操作是指一系列操作要么完全执行,要么完全不执行,中途不会被其他线程打断。在多线程环境中,原子操作避免了使用锁来保证线程安全,从而减少了性能开销。

  • Java 提供了原子变量类,这些类中的方法是线程安全的,并且不需要使用同步机制(如 synchronizedLock)来保证数据一致性。它们利用底层硬件提供的原子操作实现。


  • 作用

  • 原子变量类的设计目的是简化并发编程,特别是在涉及到简单的数值操作时(如递增、递减、交换等),可以避免使用较重的同步机制(如 synchronized)来保证线程安全。常见的原子操作包括:

  • 获取当前值

  • 设置新值

  • 递增(increment

  • 递减(decrement

  • 原子比较并交换(CAS)

②AtomicInteger

  • AtomicIntegerjava.util.concurrent.atomic 包中最常用的原子变量类之一,用于提供对 int 类型值的原子操作。它支持线程安全的增减、比较和更新操作。

  • 常见方法:

  • get(): 获取当前值

  • set(int newValue): 设置新值

  • getAndSet(int newValue): 获取当前值并设置为新值

  • incrementAndGet(): 递增并返回新值

  • getAndIncrement(): 获取当前值并递增

  • decrementAndGet(): 递减并返回新值

  • compareAndSet(int expect, int update): 如果当前值等于期望值,则原子地将其更新为新值

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample
{
    public static void main(String[] args)
    {
        AtomicInteger atomicInt = new AtomicInteger(0);

        // 启动多个线程进行并发操作
        for (int i = 0; i < 10; i++)
        {
            new Thread(() ->
            {
                int oldValue = atomicInt.getAndIncrement();
                System.out.println("Old Value: " + oldValue + ", New Value: " + atomicInt.get());
            }).start();
        }
    }
}

③AtomicLong

  • AtomicLongAtomicIntegerlong 类型版本,提供对 long 类型值的原子操作。其方法与 AtomicInteger 类似。

import java.util.concurrent.atomic.AtomicLong;

public class AtomicLongExample
{
    public static void main(String[] args)
    {
        AtomicLong atomicLong = new AtomicLong(0);

        // 启动多个线程进行并发操作
        for (int i = 0; i < 10; i++)
        {
            new Thread(() ->
            {
                long oldValue = atomicLong.getAndIncrement();
                System.out.println("Old Value: " + oldValue + ", New Value: " + atomicLong.get());
            }).start();
        }
    }
}

④AtomicBoolean

  • AtomicBooleanjava.util.concurrent.atomic 包中的原子变量类之一,用于提供对布尔值的原子操作。它通常用于表示某个标志位的状态。

  • 常见方法:

  • get(): 获取当前值。

  • set(boolean newValue): 设置新值

  • compareAndSet(boolean expect, boolean update): 如果当前值等于期望值,则将其原子地设置为新值

  • getAndSet(boolean newValue): 获取当前值并设置为新值

public class AtomicBooleanExample
{
    public static void main(String[] args)
    {
        AtomicBoolean atomicBoolean = new AtomicBoolean(false);

        // 启动多个线程进行并发操作
        for (int i = 0; i < 10; i++)
        {
            new Thread(() ->
            {
                boolean oldValue = atomicBoolean.getAndSet(true);
                System.out.println("Old Value: " + oldValue + ", New Value: " + atomicBoolean.get());
            }).start();
        }
    }
}

⑤AtomicReference

  • AtomicReference 是一个用于对对象引用进行原子操作的类。它支持对对象的引用进行安全的更新操作,可以用于需要线程安全操作引用类型的场景。

  • 常见方法:

  • get(): 获取当前引用

  • set(T newValue): 设置新引用

  • compareAndSet(T expect, T update): 如果当前引用等于期望引用,则原子地将其更新为新引用

  • getAndSet(T newValue): 获取当前引用并设置为新引用

public class AtomicReferenceExample
{
    public static void main(String[] args)
    {
        AtomicReference<String> atomicReference = new AtomicReference<>("Initial Value");

        // 启动多个线程进行并发操作
        for (int i = 0; i < 10; i++)
        {
            new Thread(() ->
            {
                String oldValue = atomicReference.getAndSet("New Value");
                System.out.println("Old Value: " + oldValue + ", New Value: " + atomicReference.get());
            }).start();
        }
    }
}

⑥LongAdder

  • 在高并发环境中,多个线程对同一个变量执行加法操作时,使用传统的 AtomicLong 会存在一些性能瓶颈。因为 AtomicLong 使用单一的原子变量来确保线程安全,而这可能导致 线程竞争,在高并发的情况下性能会下降。LongAdder 采用了不同的策略来减轻这种竞争。它通过将累加操作分散到多个底层变量上,从而减少了线程竞争的影响,进而提高了性能。适用于高并发的频繁累加操作。

  • LongAdder 采用了分段锁的思想,内部维护了多个 Cell(一个类似于桶的概念)。每当一个线程执行 add() 操作时,它会尽量选择一个空闲的 Cell 进行累加,减少竞争。这使得在高并发场景下,多个线程的累加操作不会频繁争用同一个原子变量,从而提高了效率。

  • add(long delta):将指定的值添加到当前计数器。

  • sum():返回当前计数器的总和。

  • sumThenReset():返回当前计数器的总和,并将计数器重置为 0。

  • reset():将计数器重置为 0,但不会返回当前的总和。

⑦LongAccumulator

  • LongAccumulator 是一个线程安全的可变累加器,它允许通过指定一个累加操作来进行高效的原子操作。与 LongAdder 不同,LongAccumulator 不仅仅局限于累加操作,还支持自定义操作,比如乘法、最大值、最小值等。

  • LongAccumulator 的构造函数允许你传入一个 二元操作,并指定一个初始值。这个二元操作用于在累加过程中对两个值进行组合,例如:加法、乘法、求最大值等。

  • 构造方法public LongAccumulator(LongBinaryOperator op, long identity) op:用于指定累加操作的二元操作,通常是一个 LongBinaryOperator,可以是加法、乘法等。identity:指定初始值(即身份值),用于操作的初始状态。

  • -

  • LongAccumulator 的方法:

  • accumulate(long x):将 x 累加到当前值,使用在构造时指定的二元操作。

  • get():返回当前的累加值。

  • reset():重置累加器的值为初始值。

  • getThenReset():返回当前值并将累加器重置为初始值。

public class LongAccumulatorExample
{
    public static void main(String[] args) throws InterruptedException
    {
        // 使用加法作为累加操作
        LongAccumulator accumulator = new LongAccumulator(Long::sum, 0);

        // 创建多个线程并进行累加操作
        Runnable task = () ->
        {
            for (int i = 0; i < 1000; i++)
            {
                accumulator.accumulate(1);  // 累加 1
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        Thread t3 = new Thread(task);

        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println("Final sum: " + accumulator.get());  // 输出最终的总和
    }
}

7. 并发工具类

①介绍

  • 在 Java 中,并发工具类java.util.concurrent 包中的一系列类,它们为开发者提供了更加高效、安全的并发编程支持。这些工具类不仅简化了多线程编程,还解决了许多常见的并发问题,如线程协调、资源共享、任务调度等。

②CountDownLatch

  • CountDownLatch 是一个用于线程间协调的工具,它允许一个或多个线程等待其他线程完成某些操作。它通过一个计数器来控制等待的线程数量,计数器从一个初始值开始,每调用一次 countDown() 方法,计数器减 1。当计数器为 0 时,所有调用 await() 的线程会被释放。

  • 主要用途:

  • 等待多个线程执行完成后再继续执行某些操作。

  • 用于并发测试中,控制多个线程并发执行直到某个时刻。

public class CountDownLatchExample
{
    public static void main(String[] args) throws InterruptedException
    {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        // 启动多个线程执行任务
        for (int i = 0; i < threadCount; i++)
        {
            new Thread(() ->
            {
                System.out.println(Thread.currentThread().getName() + " is working...");
                try
                {
                    Thread.sleep(1000);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                latch.countDown();  // 每个线程完成任务后调用 countDown
            }).start();
        }

        latch.await();  // 主线程等待所有线程完成
        System.out.println("All threads have completed.");
    }
}

③CyclicBarrier

  • CyclicBarrier 是一个同步辅助工具,它允许一组线程相互等待,直到所有线程都达到某个屏障点(Barrier)。一旦所有线程都到达这个屏障点,所有线程将继续执行。CyclicBarrier 可以重用,因此可以用于多次任务的同步。

  • CountDownLatch 不同,CyclicBarrier 可以重用,即在屏障点释放所有线程后,它可以重新开始,允许线程在多次任务执行中重复使用。

  • 主要用途:

  • 当多个线程在某些特定步骤上需要同步时。

  • 用于多阶段计算,每个阶段线程都需要等待其他线程。

public class CyclicBarrierExample
{
    public static void main(String[] args)
    {
        // 设定 3 个线程达到屏障后才开始执行
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () ->
        {
            // 当所有线程都到达屏障时,执行这段代码
            System.out.println("All threads reached the barrier. Starting the next phase...");
        });

        // 启动多个线程并让它们等待在 barrier
        for (int i = 0; i < threadCount; i++)
        {
            final int index = i;
            new Thread(() ->
            {
                try
                {
                    System.out.println("Thread " + index + " is working...");
                    Thread.sleep(1000);  // 模拟线程的工作
                    System.out.println("Thread " + index + " has reached the barrier.");
                    barrier.await();  // 等待其他线程到达屏障
                    System.out.println("Thread " + index + " is proceeding after the barrier.");
                } catch (InterruptedException | BrokenBarrierException e)
                {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

④Semaphore

  • Semaphore 是一个计数信号量,它控制对某些资源的访问数量。信号量维护一个许可计数,线程在执行某个操作前需要先获取许可,执行完毕后释放许可。如果没有许可,线程会被阻塞,直到有许可可用。

  • 主要用途:

  • 限制对某些资源(如数据库连接、线程池等)的并发访问。

  • 用于实现资源池的控制。

public class SemaphoreExample
{
    public static void main(String[] args) throws InterruptedException
    {
        Semaphore semaphore = new Semaphore(2);  // 允许2个线程同时访问

        // 启动多个线程模拟对共享资源的访问
        for (int i = 0; i < 5; i++)
        {
            final int index = i;
            new Thread(() ->
            {
                try
                {
                    semaphore.acquire();  // 获取许可
                    System.out.println("Thread " + index + " is accessing the resource.");
                    Thread.sleep(1000);  // 模拟处理任务
                    System.out.println("Thread " + index + " has finished.");
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                } finally
                {
                    semaphore.release();  // 释放许可
                }
            }).start();
        }
    }
}

⑤Exchanger

  • Exchanger 是一种用于线程间交换数据的工具类。两个线程通过 exchange() 方法交换数据,若两个线程都调用 exchange(),它们会阻塞,直到它们都到达交换点并交换数据。

  • 主要用途:

  • 在两个线程之间交换对象,通常用于双向通信。

  • 用于并发算法中,线程之间需要交换状态或数据。

public class ExchangerExample
{
    public static void main(String[] args) throws InterruptedException
    {
        Exchanger<String> exchanger = new Exchanger<>();

        // 线程1
        new Thread(() ->
        {
            try
            {
                String data = "Hello from thread 1!";
                String received = exchanger.exchange(data);
                System.out.println("Thread 1 received: " + received);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }).start();

        // 线程2
        new Thread(() ->
        {
            try
            {
                String data = "Hello from thread 2!";
                String received = exchanger.exchange(data);
                System.out.println("Thread 2 received: " + received);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }).start();
    }
}

⑥ScheduledExecutorService

  • ScheduledExecutorService 是一个扩展 ExecutorService 的接口,它用于执行定时任务或周期性任务(定期执行某些操作,例如心跳检测、日志清理等)。通过 schedule()scheduleAtFixedRate() 等方法,可以在未来某个时刻或定期执行任务。

public class ScheduledExecutorServiceExample
{
    public static void main(String[] args)
    {
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

        // 延迟1秒后执行任务
        scheduledExecutorService.schedule(() -> System.out.println("Task executed after 1 second"), 1, TimeUnit.SECONDS);

        // 每2秒执行一次任务
        scheduledExecutorService.scheduleAtFixedRate(() -> System.out.println("Periodic task executed"), 0, 2, TimeUnit.SECONDS);
    }
}

⑦Phaser

  • Phaser 是一个允许多个线程参与的同步器,允许线程在多个阶段上同步。在每一阶段,所有参与线程都必须在该阶段结束之前到达屏障点,才能开始下一阶段。

  • CyclicBarrierCountDownLatch 不同的是,Phaser 具有以下特点:

  • 动态参与者:可以在运行时添加或移除参与的线程。

  • 多个阶段:支持多个同步阶段,而不仅限于一个阶段。

  • 灵活的控制:允许线程在每个阶段有不同的“同步策略”。


  • Phaser 的关键方法

  • register():注册一个线程参与到同步的过程中。每次注册一个线程,Phaser 的参与者数量就会增加 1。

  • arrive():表示当前线程已到达该阶段的屏障点,参与同步。该线程不等待其他线程到达这个屏障。

  • arriveAndAwaitAdvance():表示当前线程到达了一个阶段的屏障,且等待其他线程到达屏障,直到所有线程都到达该阶段。

  • awaitAdvance(phase):让当前线程等待直到指定的阶段结束。

  • getPhase():返回当前的阶段(Phase)。Phaser 的阶段从 0 开始,逐步增加。

  • onAdvance(phase, registeredParties):一个回调方法,允许你自定义每个阶段完成时的操作。

  • getRegisteredParties():获取当前已注册的线程数。

  • getArrivedParties():获取已经到达当前阶段的线程数。

  • getUnarrivedParties():获取还未到达当前阶段的线程数。


  • Phaser 的工作原理

  • Phaser 的工作流程是基于阶段(Phase)同步的。在每个阶段,所有线程必须在该阶段的屏障点处等待,直到所有线程都到达该阶段。每个线程在某个阶段的完成后,都会调用 arrive()arriveAndAwaitAdvance() 来通知 Phaser,然后在所有线程都到达之后,所有线程才能进入下一阶段。

8. 异步计算

①介绍

  • Future对象调用get获取值时,这个方法会阻塞,直到值可用。

  • CompletableFuture 是 Java 8 引入的一个非常强大的类,它是 Future 接口的实现,提供了异步编程的更丰富的功能。CompletableFuture 不仅支持传统的异步操作(类似于 Futureget()submit()),还提供了链式调用、组合、异常处理等功能,使得异步编程更加灵活和强大。

  • 对于CompletableFuture 类可以注册一个回调,一旦结果可用,就会利用该结果调用这个回调,采用这种方式,一旦结果可用就可以对结果进行处理而无需阻塞。

  • 非阻塞调用通过回调来实现。


  • CompletableFuture 主要特点:

  • 异步执行CompletableFuture 允许在后台线程中执行任务,并提供方法来处理完成后的结果。

  • 链式调用:可以通过 thenApply()thenAccept() 等方法对异步结果进行处理,支持通过链式调用来串联多个异步任务。

  • 回调机制:你可以通过 thenRun()whenComplete() 等方法添加回调,这些回调会在任务完成时执行。

  • 异常处理:提供了内建的异常处理方法,如 exceptionally()handle(),用于处理异步任务中可能发生的异常。

  • 组合多个 CompletableFuture:你可以通过方法如 thenCombine()thenCompose() 等组合多个异步任务。


  • 完成方式:得到一个结果或者有一个未捕获异常

②supplyAsync

  • public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

  • public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

  • 功能:使用 Supplier 提供的值异步执行任务,并返回一个 CompletableFuture 对象。

  • 返回值:一个包装了结果的 CompletableFuture

  • 执行器:若省略Executor 会在默认的执行器(ForkJoinPool.commonPool()返回的执行器)上运行。

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    // 模拟异步操作
    return 42;
});

③runAsync

  • public static CompletableFuture<Void> runAsync(Runnable runnable)

  • public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)

  • 功能:异步执行一个不返回结果的 Runnable 任务。

  • 返回值:一个 CompletableFuture<Void>,表示任务的执行状态。

  • 执行器:若省略Executor 会在默认的执行器(ForkJoinPool.commonPool()返回的执行器)上运行。

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    // 执行不需要返回值的任务
    System.out.println("Running asynchronously");
});

④结果处理:thenApply()、thenAccept()、thenRun()

  • thenApply(Function<? super T, ? extends U> fn)

  • 功能:当 CompletableFuture 完成时,使用 Function 对结果进行转换。返回一个新的 CompletableFuture

  • 返回值:新的 CompletableFuture,其中包装了转换后的结果。

  • -

  • thenAccept(Consumer<? super T> action)

  • 功能:当 CompletableFuture 完成时,对结果进行消费,不返回值。

  • 返回值:一个 CompletableFuture<Void>

  • -

  • thenRun(Runnable action)

  • 功能:当 CompletableFuture 完成时执行一个 Runnable 操作,且不使用结果。

  • 返回值:一个 CompletableFuture<Void>

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<Integer> result = future.thenApply(x -> x * 2);  // 结果为 10
***
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5);
future.thenAccept(x -> System.out.println("Result: " + x));  // 输出 Result: 5
***
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> 5);
future.thenRun(() -> System.out.println("Task complete"));  // 输出 Task complete

⑤whenComplete

  • whenComplete()CompletableFuture 提供的一个重要方法,它允许你在任务完成时执行一个回调函数,并能够同时处理任务的正常结果和异常。与 thenApply()thenAccept() 不同的是,whenComplete() 总是会被调用,无论任务是否成功完成,还是抛出了异常。

  • public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)

  • 参数action 是一个 BiConsumer,接收两个参数:

  • 第一个参数:任务的结果(类型是 T)。

  • 第二个参数:异常(如果任务完成时抛出了异常),类型是 Throwable

  • 返回值:返回一个新的 CompletableFuture,它代表当前的任务。


  • 特点

  • 不管任务是否成功完成,whenComplete() 中的回调都会被执行。

  • 如果任务执行成功,action 的第一个参数会是结果,第二个参数是 null

  • 如果任务执行失败,action 的第一个参数是 null,第二个参数包含异常信息。

  • exceptionally()handle() 不同,whenComplete() 本身不会修改或处理任务的结果或异常,它只是执行一个副作用操作,例如日志记录、清理资源等。

class WhenCompleteExample
{
    public static void main(String[] args)
    {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() ->
        {
            int result = 10 / 2; // 正常计算
            return result;
        });

        future.whenComplete((result, ex) ->
        {
            if (ex == null)
            {
                System.out.println("Task completed successfully with result: " + result);
            } else
            {
                System.out.println("Task failed with exception: " + ex.getMessage());
            }
        });

        // 如果是异常的任务
        CompletableFuture<Integer> futureWithError = CompletableFuture.supplyAsync(() ->
        {
            int result = 10 / 0; // 会抛出异常
            return result;
        });

        futureWithError.whenComplete((result, ex) ->
        {
            if (ex == null)
            {
                System.out.println("Task completed successfully with result: " + result);
            } else
            {
                System.out.println("Task failed with exception: " + ex.getMessage());
            }
        });
    }
}

⑥isDone/join/get

  • isDone()CompletableFuture 中的一个方法,用于检查异步任务是否已完成。它会返回一个布尔值,指示任务是否已经完成,无论是成功完成、被取消还是由于异常失败。

  • join():功能:等待任务执行完成,并获取任务结果。如果任务执行过程中抛出异常,join() 会抛出 CompletionException返回值:任务的结果。

  • get():功能:等待任务执行完成,并返回任务结果。get() 方法会阻塞直到任务完成。如果任务在执行过程中抛出异常,它会将异常包装在 ExecutionException 中抛出。返回值:任务的结果。

⑦组合可完成Future

  • 组合可完成的任务CompletionStage)是指通过不同的 API 方法将多个异步任务结合在一起,从而创建更复杂的任务链。这使得我们能够通过链式操作来组合任务并处理任务间的依赖关系。

thenCombine() 用于将两个 CompletableFuture 完成时的结果组合起来,
执行一个 BiFunction 来处理两个结果,并返回一个新的 CompletableFuture。
public <U, V> CompletableFuture<V> thenCombine(
    CompletableFuture<? extends U> other, 
    BiFunction<? super T, ? super U, ? extends V> fn)

参数:
other:另一个 CompletableFuture,当两个任务都完成时执行。
fn:用于组合两个任务结果的 BiFunction。
返回值:返回一个新的 CompletableFuture,包含组合后的结果。

public class ThenCombineExample
{
    public static void main(String[] args)
    {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);

        CompletableFuture<Integer> result = future1.thenCombine(future2, (x, y) -> x + y);  // 组合两个结果

        result.thenAccept(res -> System.out.println("Combined result: " + res));  // 输出 Combined result: 30
    }
}
thenCompose() 用于将两个异步操作串联起来。它的作用是将前一个 CompletableFuture 的结果作为下一个 CompletableFuture 的输入,
并且返回一个新的 CompletableFuture。
public <U> CompletableFuture<U> thenCompose(
    Function<? super T, ? extends CompletableFuture<U>> fn)

参数:fn:一个 Function,接受当前任务的结果,并返回一个新的 CompletableFuture。
返回值:返回一个新的 CompletableFuture,其结果是由 fn 返回的 CompletableFuture。

public class ThenComposeExample
{
    public static void main(String[] args)
    {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);

        CompletableFuture<Integer> result = future1.thenCompose(x ->
                CompletableFuture.supplyAsync(() -> x * 2));  // 使用前一个任务的结果来执行下一个任务

        result.thenAccept(res -> System.out.println("Final result: " + res));  // 输出 Final result: 20
    }
}
allOf() 用于等待多个 CompletableFuture 同时完成。它返回一个新的 CompletableFuture,
该 CompletableFuture 只有在所有给定的任务都完成时才会完成。
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
参数:多个 CompletableFuture 对象。
返回值:返回一个新的 CompletableFuture<Void>,只有当所有的任务都完成时才会完成。

public class AllOfExample
{
    public static void main(String[] args)
    {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);

        CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2);

        allOf.thenRun(() ->
        {
            try
            {
                // 获取两个任务的结果
                System.out.println("Task 1 result: " + future1.get());
                System.out.println("Task 2 result: " + future2.get());
            } catch (Exception e)
            {
                e.printStackTrace();
            }
        });
    }
}
anyOf() 用于等待多个 CompletableFuture 中的任意一个完成。
它返回一个新的 CompletableFuture,该 CompletableFuture 会在最先完成的任务完成时立即完成。
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
参数:多个 CompletableFuture 对象。
返回值:返回一个新的 CompletableFuture<Object>,包含最先完成的任务的结果。

public class AnyOfExample
{
    public static void main(String[] args)
    {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() ->
        {
            try
            {
                Thread.sleep(2000);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            return 10;
        });

        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() ->
        {
            try
            {
                Thread.sleep(1000);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            return 20;
        });

        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(future1, future2);

        anyOf.thenAccept(res -> System.out.println("First completed result: " + res));  // 输出 First completed result: 20
    }
}
handle() 方法不仅处理成功的结果,还能处理异常,它提供了一个 BiFunction,允许你对任务的结果或异常进行处理。
public <U> CompletableFuture<U> handle(
    BiFunction<? super T, Throwable, ? extends U> fn)
参数:
fn:一个 BiFunction,接收任务的正常结果和异常(如果有的话),并返回一个新的结果。
返回值:返回一个新的 CompletableFuture,其结果是处理后的值。

class HandleExample
{
    public static void main(String[] args)
    {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() ->
        {
            if (true) throw new RuntimeException("Something went wrong");
            return 10;
        });

        CompletableFuture<Integer> result = future.handle((res, ex) ->
        {
            if (ex != null)
            {// 输出 Handled exception: Something went wrong
                System.out.println("Handled exception: " + ex.getMessage());
                return -1;  // 返回默认值
            } else
            {
                return res;
            }
        });
        //Result: -1
        result.thenAccept(res -> System.out.println("Result: " + res));
    }

9. 进程

①介绍

  • 进程是一个程序在执行时的实例。它是计算机中资源管理的基本单位,每个进程都拥有独立的内存空间、代码、数据以及系统资源。进程是操作系统(调度)和执行程序的基础单位。

  • 进程

  • 拥有独立的内存空间,每个进程之间互相隔离。

  • 进程之间的通信比较复杂(需要使用进程间通信机制,如管道、消息队列、共享内存等)。

  • 一个进程可以包含多个线程。

  • 线程

  • 线程是进程中的一个执行单元,多个线程共享同一进程的内存空间。

  • 线程之间的通信较为简单,通常通过共享内存或全局变量来实现。

  • 创建和销毁线程的开销比进程小。

  • 线程:在同一个程序的不同线程中执行Java代码。

  • 进程:执行另一个程序。

②创建和启动进程

  • Runtime 类是 Java 提供的一个类,它允许你与 Java 虚拟机(JVM)及操作系统进行交互,包括启动新进程、运行外部命令等。通过 Runtime.getRuntime().exec() 方法可以启动一个外部进程。

  • ProcessBuilder 类提供了更灵活的方式来启动和管理进程。你可以通过它传递命令参数、设置环境变量、获取进程的输入输出等。

使用 Runtime 类
public class RuntimeExample
{
    public static void main(String[] args)
    {
        try
        {
            // 使用 Runtime 类启动一个外部进程(在这里是 "notepad.exe")
            Process process = Runtime.getRuntime().exec("notepad.exe");

            // 等待进程结束
            process.waitFor();
            System.out.println("Notepad has been closed.");
        } catch (IOException | InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}
使用 ProcessBuilder 类
public class ProcessBuilderExample
{
    public static void main(String[] args)
    {
        try
        {
            // 使用 ProcessBuilder 启动外部进程
            ProcessBuilder builder = new ProcessBuilder("notepad.exe");
            Process process = builder.start();

            // 等待进程结束
            process.waitFor();
            System.out.println("Notepad has been closed.");
        } catch (IOException | InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

③与进程的输入输出交互

获取进程的输出
你可以获取外部进程的标准输出流(stdout),并读取其返回结果
public class ProcessOutputExample
{
    public static void main(String[] args)
    {
        try
        {
            // 使用 ProcessBuilder 启动外部命令(例如:dir 或 ls)
            ProcessBuilder builder = new ProcessBuilder("dir");
            builder.redirectErrorStream(true);  // 合并错误输出流和标准输出流
            Process process = builder.start();

            // 读取进程的输出
            InputStream inputStream = process.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = reader.readLine()) != null)
            {
                System.out.println(line);
            }

            // 等待进程结束
            process.waitFor();
        } catch (IOException | InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}
向进程输入数据
你可以向进程的输入流(stdin)发送数据,通常用于与进程交互
class ProcessInputExample
{
    public static void main(String[] args)
    {
        try
        {
            // 启动一个命令行进程(例如:cmd 或 bash)
            ProcessBuilder builder = new ProcessBuilder("cmd");
            Process process = builder.start();

            // 向进程发送命令(例如:echo Hello)
            OutputStream outputStream = process.getOutputStream();
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
            writer.write("echo Hello\n");
            writer.flush();
            writer.close();

            // 获取进程输出
            InputStream inputStream = process.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = reader.readLine()) != null)
            {
                System.out.println("进程输出"+line);
            }

            // 等待进程结束
            process.waitFor();
        } catch (IOException | InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

④进程管理

获取进程的退出状态
每个进程都有一个退出状态,通常表示进程是否成功执行。如果进程成功退出,
通常退出状态为 0,如果出错则为非零值。
public class ProcessExitStatusExample
{
    public static void main(String[] args)
    {
        try
        {
            // 启动外部命令
            Process process = Runtime.getRuntime().exec("dir");

            // 获取进程的退出状态
            int exitCode = process.waitFor();
            System.out.println("Process exit code: " + exitCode);
        } catch (IOException | InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}
检查进程是否已完成
可以使用 Process.isAlive() 来检查一个进程是否仍在运行。
public class ProcessAliveExample
{
    public static void main(String[] args)
    {
        try
        {
            // 启动一个长期运行的进程
            Process process = Runtime.getRuntime().exec("ping -t 127.0.0.1");

            // 检查进程是否还在运行
            while (process.isAlive())
            {
                System.out.println("Process is still running...");
                Thread.sleep(1000);  // 等待1秒钟
            }

            System.out.println("Process has finished.");
        } catch (IOException | InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

⑤设置进程的工作目录和环境变量

  • 可以通过 ProcessBuilder 设置进程的工作目录和环境变量。这对于需要特定环境配置的进程非常有用。

public class ProcessEnvironmentExample
{
    public static void main(String[] args)
    {
        try
        {
            // 设置进程的工作目录和环境变量
            ProcessBuilder builder = new ProcessBuilder("java", "-version");
            builder.directory(new File("C:\\Program Files\\Java\\jdk-11.0.8\\bin"));
            builder.environment().put("MY_ENV_VAR", "SomeValue");

            Process process = builder.start();

            // 获取进程输出
            InputStream inputStream = process.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = reader.readLine()) != null)
            {
                System.out.println(line);
            }

            process.waitFor();
        } catch (IOException | InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

⑥进程的错误输出

  • 如果进程遇到错误,它会将错误信息输出到标准错误流(stderr)。你可以通过 Process.getErrorStream() 获取该流并进行读取。

public class ProcessErrorExample
{
    public static void main(String[] args)
    {
        try
        {
            // 执行一个错误的命令
            Process process = Runtime.getRuntime().exec("nonexistent_command");

            // 获取错误输出
            InputStream errorStream = process.getErrorStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream));
            String line;
            while ((line = reader.readLine()) != null)
            {
                System.out.println("Error: " + line);
            }

            process.waitFor();
        } catch (IOException | InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

⑦进程句柄

  • 进程句柄(Process Handle) 是操作系统中用于唯一标识和操作进程的一个引用或指针。在许多操作系统中,进程句柄是操作系统内核用来管理进程的标识符。通过进程句柄,程序可以对进程进行操作,如获取进程的状态、终止进程、等待进程结束等。

  • 在 Java 中,进程句柄 并不是一个显式的概念,Java 程序通过 Process 对象来管理和控制外部进程。Process 对象在 Java 中充当了进程的“句柄”,通过它,你可以与外部进程进行交互,如获取进程的输出、输入,等待进程结束,或者终止进程等。