参考书籍:

Java核心技术卷1、卷2 第12版

一. 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类型

  • 2字节。

  • 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类型

  • 1字节。

  • 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类对象称为是不可变的。

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

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

④检测字符串是否相等

  • 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 关键字,依赖于外部类的实例。

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

  • 每个非静态嵌套类的实例都与外部类的实例相关联,非静态内部类的实例必须通过外部类的实例创建,因此它隐式关联一个Outer.this引用。外部类的引用在构造器中设置口 编译器会修改所有的内部类构造器,添加一个对应外部 类引用的参数。

  • 非静态内部类不能声明非final的静态字段。但允许声明静态的final常量​(即static final修饰且初始值为编译时常量)。这是因为常量值在编译时确定,无需依赖外部类实例的初始化。

  • 非静态内部类不能有静态方法。静态方法属于类本身,而非静态内部类的存在依赖于外部类实例,导致二者语义冲突。

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常量)。不允许静态方法。

  • 局部内部类的生命周期与所在方法的执行绑定,其类加载依赖于外部方法的调用。静态成员需要在类加载时初始化,但局部内部类无法保证类加载的时机(可能多次加载),导致逻辑矛盾。

  • 在实例方法中定义的局部内部类,会含外部类引用;在静态方法中不会。

  • 不能定义为 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();
    }
}

④匿名内部类

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

  • 完全不能声明静态字段​(包括static final常量)。不允许静态方法。

  • 匿名内部类没有显式类名,无法通过类名访问静态成员,且其生命周期与外部方法的执行紧密关联。静态成员的初始化要求独立于实例存在,与匿名内部类的临时性本质冲突。

  • 在实例方法中定义的局部内部类,会含外部类引用;在静态方法中不会。

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

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 。适用于引用构造方法,用于创建对象。

方法引用步骤
生成一个函数式接口实例,并在其方法上调用引用的方法。
int[] array = list.stream().mapToInt(Integer::intValue).toArray();
public int intValue()
IntStream mapToInt(ToIntFunction<? super T> mapper);
@FunctionalInterface
public interface ToIntFunction<T> {

    int applyAsInt(T value);
}

相当于
int[] array = list.stream()
                  .mapToInt(new ToIntFunction<Integer>() {
                      @Override
                      public int applyAsInt(Integer value) {
                          return value.intValue();//任意实例的调用
                      }
                  }).toArray();

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

⑤为什么不能修改局部变量

image-jxlp.png

image-clvd.png

3. 内部接口

4. 注解

①介绍

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

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

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

②用途

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

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

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

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

③注解接口

  • 注解是由注解接口来定义的,其本质是一个接口,其具体实现类是Java运行时生成的动态代理类。

  • 我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方出对应的值。而memberValues的来源是Java常量池。

  • 所有的注解接口都隐式地扩展自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); // 编译错误

④总结

泛型递归绑定

  • 泛型递归绑定(Recursive Generic Bounds)是指在 Java 的泛型声明中,将类型参数绑定到其自身的一个约束条件上。通常出现在以下形式中:<T extends SomeType<T>>

  • 自引用类型: 泛型递归绑定让我们能够创建一种类型,既能处理泛型类型本身,又能在继承时维持一致性。例如,当我们希望通过继承 SomeType 类并在该类中返回 T 类型的对象时,泛型递归绑定使得返回类型总是与当前类一致,而不是返回父类的类型。

  • 流式接口和构建者模式: 在流式接口或构建者模式中,常常希望每次调用方法时返回的是当前对象类型,而不是父类型。递归类型绑定保证了方法链的类型安全和一致性。

假设我们有一个通用类 SomeType,并希望让它的子类能够返回自身的实例
class SomeType<T extends SomeType<T>> {
    // 返回 T 类型
    T doSomething() {
        // 这里可以安全地返回当前类型的实例
        return (T) this;
    }
}

class SubType extends SomeType<SubType> {
    // SubType 继承自 SomeType<SubType>
    // 在此类中,你可以调用 doSomething(),返回的是 SubType 类型
}
在这个例子中,SomeType 类的泛型参数 T 限定为 SomeType<T>,这意味着任何继承 SomeType 的子类都必须声明它自己作为泛型类型。这样,doSomething 方法返回的类型是 T,确保它返回的是子类的实例。
一个常见的应用场景是流式接口。比如你在实现一个建造者模式时,
可以通过递归绑定让每个方法返回当前对象类型,从而允许方法链调用:
class FluentBuilder<T extends FluentBuilder<T>> {
    // 假设这是一个简单的建造者,返回当前对象类型
    public T setName(String name) {
        // 设置名称的逻辑
        return (T) this;
    }

    public T setAge(int age) {
        // 设置年龄的逻辑
        return (T) this;
    }

    public T build() {
        // 构建对象的逻辑
        return (T) this;
    }
}

class PersonBuilder extends FluentBuilder<PersonBuilder> {
    // 继承 FluentBuilder 并返回 PersonBuilder 类型
    public PersonBuilder setName(String name) {
        super.setName(name);
        return this;
    }

    public PersonBuilder setAge(int age) {
        super.setAge(age);
        return this;
    }

    public Person build() {
        // 构建最终的 Person 对象
        return new Person();
    }
}

class Person {
    // Person 类
}
在这个例子中,FluentBuilder 是一个递归泛型类,它的子类 PersonBuilder 
通过继承 FluentBuilder 并返回 PersonBuilder 类型,能够实现流式接口。
这样,每个 set 方法都能返回相同类型的对象 PersonBuilder,确保方法链调用。

5. 泛型和虚拟机

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

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

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

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

  • -

  • 规则

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

  • 2. 泛型类型被其第一个上界替换,如果没有指定上界,默认为 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. 副本与视图

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

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

①小集合

  • java9引入一些静态方法,可以生成给定元素的集或列表,以及给定键值对的映射,简化代码。元素、键或值不能为空,集合映射键不能重复。

  • List、Set接口有11个of方法,分别有0~10个参数,另外还有一个参数可变的of方法。对于Map接口无可变参数版本,因为参数类型会交替为键类型和值类型。不过其有一个ofEntries,能够接受任意多个Map.Entry<K, V>对象(可用静态方法创建entry对象)。

  • 这些集合对象是不可修改的。

List<String> names = List.of("a", "b");//对元素顺序没有保障
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> scores = Map.of("a", 1, "b", 2, "c", 3);
Map<String, Integer> scores2 = Map.ofEntries(Map.entry("p", 1), Map.entry("q", 2));

②不可修改的副本和视图

  • 建立一个集合的不可修改的副本,可以使用集合类型的copyOf方法:其会建立一个副本,如果修改了原集合,这个副本不受影响,若原集合是不可修改的,且类型正确,copyOf会直接返回原集合。

  • CollKtions 类还有一些方法可以生成集合的不可修改的视图。这些视图对现有集合增加了一个运行时检查。如果检测到试图修改不可修改的集合,就抛出一个异常。如果原集合改变,视图会反映这些变化。这正是视图与副本的区别。

③子范围

  • 可以为很多集合建立子范围(subrange) 视图。可以使用subList方法来获得这个列表子范围的视图。

  • 可以对子范围应用任何操作,而且这些操作会自动反映到整个列表。

④检查型视图

⑤同步视图

⑥可选操作

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

八. 下一章