《Java核心技术 I》容易忽视和重要的知识点汇总

本文对《Java核心技术 I》中开发者容易忽视重要 的知识点进行总结,不包含一般开发者都知道内容。大标题后括号的数字代表书中对应的章节。

一、Java的基本程序设计结构(3)

1. 整数表示

可以为数字字面量加上下划线,这些下划线只是为了让人更易读。Java编译器会去除这些下划线。

```java
int n = 1_000_000_000;
System.out.println(n);
// 输出
1000000000
```

2. 无符号类型

Java 中没有原生的无符号整数类型。所有的整数类型(byteshortintlong)都是带符号的。不过,Java 提供了某些方法来模拟无符号行为,特别是对于 intlong 类型。

Java 8 引入的 Integer 类中的 toUnsignedLong(int x)divideUnsigned(int x, int y) 方法主要用于支持无符号整数的操作,尽管 int 类型本身是带符号的。

2.1 toUnsignedLong(int x)

该方法将一个带符号的 int 转换为无符号的 long 类型。由于 int 类型是 32 位,且在 Java 中是有符号的,它的范围是 -2^312^31-1,而无符号 int 的范围是 02^32-1。通过该方法,可以将带符号的 int 值转换为无符号的 long 值来处理。

```java
int signedInt = -1;                                     // 带符号的 int 值
long unsignedLong = Integer.toUnsignedLong(signedInt);  // 转换为无符号 long
System.out.println(unsignedLong);                       // 输出:4294967295
// -1 二进制是 11111111 11111111 11111111 11111111,转成无符号就是把前面二进制直接当成原码,求值为2^32-1即4294967295
// 正常负数的值应该是 上面二进制的各个位取反最后+1
```

2.2 divideUnsigned(int x, int y)

该方法执行两个无符号 int 值的除法操作,并返回无符号的结果。它的作用类似于普通的 x / y,但是它是以无符号的方式来处理运算,即忽略符号位。

```java
int x = 10;
int y = 3;
int result = Integer.divideUnsigned(x, y);  // 无符号除法
System.out.println(result);  // 输出:3
```

2.3 remainderUnsigned(int x, int y)

这个方法与 divideUnsigned 类似,但是它返回的是无符号的余数。

```java
int x = 10;
int y = 3;
int remainder = Integer.remainderUnsigned(x, y);  // 无符号余数
System.out.println(remainder);  // 输出:1
```

无符号数的加减乘法 在二进制层面与有符号数一致,因为它们使用相同的二进制表示,只是解释方式不同。所以加减乘法可以直接使用。

3. double 类型

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

所有浮点数计算都遵循 IEEE 754 规范,具体来说,有3个特殊的浮点数值表示溢出和出错的情况。

  1. 正无穷大(POSITIVE_INFINITY)
  2. 负无穷(NEGATIVE_INFINITY)
  3. NaN(不是一个数)

例如,一个正整数除以0的结果为正无穷大,计算0/0或负数的平方根结果为NaN。

```java
Double x = 1.0;
if (x != Double.POSITIVE_INFINITY) {
    System.out.println("x is not POSITIVE_INFINITY");
}
```

4. 右移操作

>>>>> 都是按照位模式右移的运算符,但有略微区别。

  1. >> 是带符号的右移操作符,叫做 算数右移 。会保持符号位不变。

  2. >>> 是无符号右移操作符,叫做 逻辑右移 。不管正负数,都用 0 填充最高位。

    java
    int a = -8; // 二进制:11111111 11111111 11111111 11111000 (补码形式)
    int b = a >> 2; // 右移 2 位
    System.out.println(b); // 输出:-2

-8 右移两位后是 11111111 11111111 11111111 11111110,负数在计算机中是以补码的形式存储,其原码是各个位取反,再+1。00000000 00000000 00000000 00000001 + 1 = 00000000 00000000 00000000 00000010(原码是2)

```java
int a = -8; // 二进制:11111111 11111111 11111111 11111000 (补码形式)
int b = a >>> 2; // 右移 2 位,高位补 0
System.out.println(b); // 输出:1073741822
```

-8 右移两位后是 00111111 11111111 11111111 11111110,(1073741822)

5. char类型

UTF-8 和 UTF-16 是两种常用的字符编码方式。

  1. UTF-8 是一种可变长度的字符编码,它将 Unicode 字符集中的字符编码成 1 到 4 个字节(8 位),使用 1 个字节到 4 个字节表示一个字符。其中前128个字符(ASCII)采用1字节

  2. UTF-16 也是一种可变长度的字符编码方式,但它将 Unicode 字符集中的字符编码成 2 个字节或 4 个字节,并且对于大多数常用字符(前128个字节),使用 2 个字节来表示。

Java中的 char类型 使用的就是 UTF-16,其中 UTF-16 有一个代码单元 的概念,即表示一个字符的最小存储单位,UTF-16的代码单元是2字节。

观察下面代码可以发现,charAt 返回的是一个代码单元,那么对于占用两个代码单元的字符串就会出现奇怪的问题。所以尽量避免使用 Char 类型。

```java
String s = "\uD83D\uDE00hello"; // 这个字符串实际是 😀hello
System.out.println(s);  
System.out.println(s.charAt(0));
System.out.println(s.charAt(1));    // 这里输出的并不是 h
System.out.println(s.charAt(2));
//  输出如下
😀hello
?
?
h
```

6. trim 和 strip

  1. trim() 只会去除 ASCII 空白字符 (即 \u0020 空格、\u0009 制表符、\u000A 换行符、\u000D 回车符)以及一些常见的控制字符。

  2. strip() 在 Java 11 中引入,除了去除 ASCII 空白字符 外,它还会去除一些其他的 Unicode 空白字符(如 \u2000\u200B 等)。

strip 去除的范围更广,一般采用这个。

```java
String s = "Hello World!\u3000   ";  // 包含零宽空格(\u200B)和全角空格(\u3000)
String trimmed = s.trim();
String stripped = s.strip();

System.out.println("Original: '" + s + "'");
System.out.println("Trimmed: '" + trimmed + "'");
System.out.println("Stripped: '" + stripped + "'");
```

7. switch

传统的switch存在 直通行为 。(即当 switch 语句中的某个 case 匹配到时,它会继续执行下一个 case 的代码,直到遇到 break 语句或 switch 语句结束为止。这种行为叫做 fall-through ,即“穿透”到下一个 case。)

Java 12 引入了增强的 switch 表达式,支持 非直通行为 。可以返回值并消除掉传统 switch 语句中的直通行为,避免不小心出现的错误。引入了 ->yield,其中用 -> 代替传统的 :,并且还可以返回一个值,yield 用来指定返回什么,相当于return。

```java
int day = 3;

String result = switch (day) {
    case 1 -> "Monday"; // 对于没有花括号包围的,相当于是 yield "monday";
    case 2 -> "Tuesday";
    case 3 -> {
        // 使用 yield 返回值
        System.out.println("Processing Wednesday...");
        yield "Wednesday"; // 必须使用 yield 来返回值
    }
    case 4 -> "Thursday";
    case 5 -> "Friday";
    default -> "Invalid day";
};

System.out.println(result);  // 输出返回的字符串
// 输出
Processing Wednesday...
Wednesday
```

对于 switch cast -> 这种语句只能使用 yield 返回结果,不能用 breakcontinuereturn

二、对象与类(4)

1. final 的用途

  1. final 修饰一个字段时,意味着该字段的值 一旦被赋值后就不能再改变

  2. final 修饰方法时,意味着该方法不能被子类重写(覆写)。

  3. final 修饰类时,意味着该类不能被继承。

final 修饰符对于基本类型 或者不可变类型 的字段尤其有用。如果修饰的是可变类型 的字段,那么表示存储该字段指向的对象引用不用再指向其他的,但是引用本身是可以改变 的。

2. 静态方法构造对象

使用静态工厂方式来构造对象 是指通过类的静态方法 来创建并返回对象,而不是直接通过类的构造器 (new) 来实例化对象。这种设计模式可以提供更多的灵活性和可扩展性。

2.1 与构造器的对比

构造器 静态工厂方法
名称固定,必须与类名相同。 方法名可以自由命名 ,能更清晰地表达意图。
每次调用都会返回一个新对象。 可以返回缓存对象或共享对象 ,提升性能。
直接调用类的构造器,不能对返回的对象做额外处理。 可以自定义逻辑,如参数校验、对象池管理等。
无法隐藏实现类。 可以返回接口类型或不同的实现类 ,隐藏具体实现。

2.2 静态工厂的优势

  1. 更具可读性和语义性
    方法名可以描述创建对象的意图,而构造器只能使用类名。

    ```java
    

    // 使用构造器
    LocalDate today = new LocalDate();

    // 使用静态工厂
    LocalDate today = LocalDate.now(); // 可读性更强
    ```

  2. 可以缓存和复用实例
    静态工厂方法可以返回已有实例,而不总是创建新对象。

    ```java
    

    public class Boolean {
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    public static Boolean valueOf(boolean b) {
        return b ? TRUE : FALSE;
    }
    

    }
    ```

  3. 隐藏实现类,返回接口或父类类型
    静态工厂方法可以返回接口类型或其父类,隐藏具体实现,从而提高灵活性和扩展性。

    ```java
    

    public interface Animal {}
    public class Dog implements Animal {}
    public class Cat implements Animal {}

    public class AnimalFactory {
    public static Animal createAnimal(String type) {
    if ("dog".equals(type)) {
    return new Dog();
    } else if ("cat".equals(type)) {
    return new Cat();
    }
    throw new IllegalArgumentException("Unknown type");
    }
    }
    ```

3. 方法参数传递方式

  1. 基本数据类型的传递 (传递值)

  2. 引用类型的传递 (传递引用的副本)

对于对象的传递,方法得到的是对象引用的副本 ,而不是像C++那样按引用调用,原来的对象引用和这个副本都是引用同一个对象。下面两个示例都可以证明这一点。

  1. String 类型

对于String类型,参数传递是按照引用类型传递,但是String是不可变对象,所以在 testString 方法中重新赋值a,b,实际上是对方法中的a和b重新创建String对象,并不会改变main中的a,b。

```java
public static void main(String[] args) throws InterruptedException {
    String a = "hello";
    String b = "world";
    System.out.println(a + " " + b);
    testString(a, b);
    System.out.println(a + " " + b);
}
public static void testString(String a, String b) {
    a = "testa";
    b = "testb";
};
// 输出
hello world
hello world
```
  1. 对象类型

我们打算让两个对象 person1 和 person2 互换一下对象的引用,可以发现并没有成功。实际是方法中的 p1 指向了 person2 引用的对象,p2指向了person1引用的对象。

```java
public class Test {
    private static final Logger log = LoggerFactory.getLogger(Main.class);
    public static void main(String[] args) throws InterruptedException {
        Person person1 = new Person("name1", 18);
        Person person2 = new Person("name2", 18);
        System.out.println(person1);
        System.out.println(person2);
        testObject(person1, person2);
        System.out.println(person1);
        System.out.println(person2);
    }

    public static void testObject(Person p1, Person p2) {
        Person person = p1;
        p1 = p2;
        p2 = person;
    }
}

class Person {
    String name;
    Integer age;
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}
// 输出
other.Person@27c6e487
other.Person@49070868
other.Person@27c6e487
other.Person@49070868
```

4. this

this 是一个隐式参数,也就是所构造的对象,可以通过它访问当前对象的字段,也可以用来调用同一个类的另一个构造方法。

```java
// 访问字段
public Person(String name, Integer age) {
    this.name = name;
    this.age = age;
}

// 调用另一个构造方法
public Person(String name, Integer age) {
    this();
}
public Person() {
    this.name = "xiao wang";
    this.age = 18;
}
```

5. 初始化实例字段

常见的初始化实例字段的方法是

  1. 构造方法 中初始化
  2. 声明 中直接赋值

实际上,Java还有第三种机制,成为初始化块 。在一个类的声明中,可以包含任意的代码块。构造这个类的对象时,这些块就会执行。

```java
class Person {
    String name;
    Integer age;

    {
        name = "xiao wang";
        age = 18;
    }
// get ... set ... 方法
}
Person person = new Person();
System.out.println(person.getName());
System.out.println(person.getAge());
// 输出
xiao wang
18
```

当然,对于静态字段,可以使用静态初始化代码块,只需要在代码块前加一个 static 关键字即可

```java
static {

}
```

6. 特殊类: 记录

记录是一种特殊形式的类,其状态不可变,而且公共可读。记录会自动添加:toString、equals、hashCode、getter方法。 (JDK16正式发布)

```java
public record Point(int x, int y) {
}

Point point = new Point(1, 1);
System.out.println(point.x());  // getter方法
System.out.println(point.y());  // getter方法
```

补充:

  1. 记录可以添加自定义的方法,可以添加静态字段和方法,但是不能增加实例字段。比如上面如果在代码块中添加 int z; 就会报错。

  2. 记录不可以被继承

  3. 记录生成的equals是比较字段的值是否相同的。(在6.1 中会讲到)

三、继承(5)

继承是指允许一个类(子类)从另一个类(超类)获取属性和方法。其中超类的私有变量和方法 不能直接被子类访问 但是它们是 可以被子类继承

1. super

super 是一个指示编译器调用超类方法的特殊关键字,可以通过super调用超类的构造方法普通方法 、字段。使用super调用超类构造方法的语句必须放在子类构造方法的第一条语句。

在创建子类 对象时,需要先调用超类的构造器 来初始化父类部分的成员变量和状态,然后再初始化子类的成员变量。因此super必须是子类构造方法的第一条语句,确保可以按正确顺序初始化超类

```java
class Person {
    private String name;
    private Integer age;

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

public class Man extends Person{

    private String sex;

    public Man(String name, Integer age, String sex) {
        super(name, age);
        this.sex = sex;
    }
}
```

2. 多态

一个对象变量可以指示多种实际类型可以把子类对象赋值给超类变量 ),这一点称为多态。在运行时能够自动地选择适当的方法,这称为动态绑定(运行时多态)。

在下面示例中,Animal 类指示了两种实际类型DogCat ,并且在调用 sound 方法时,即使是 Animal 对象,也可以选择出正确的方法。

```java
package other;

class Animal {
    public void sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("Dog barks");
    }
}

class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("Cat meows");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal1 = new Dog();  // 父类引用指向子类对象
        Animal animal2 = new Cat();     // new Cat()可以指向多个实际类型,比如 Animal、Cat类型

        animal1.sound();  // 输出: Dog barks
        animal2.sound();  // 输出: Cat meows
    }
}
```

3. 方法重载

方法的名字和参数列表称为方法的 签名 ,一个类中不能存在相同的方法签名。

比如下面这个就会报错,因为他们的方法签名相同,返回类型 并不是签名的一部分

```java
public class Test {
    public Integer test() { // Integer 返回类型
        return 1;
    }

    public String test() {  // String 返回类型
        return "1";
    }
}
```

如果子类中定义了一个与超类签名相同的方法,那么子类中的这个方法就会覆盖超类中有相同签名的方法。为了保证返回类型的兼容性,允许子类讲覆盖方法的返回类型改成原返回类型的子类型

```java
class Animal {
    public Animal getAnimal() {
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    public Dog getAnimal() {    // 重载时允许讲返回类型从Animal改成Dog
        return new Dog();
    }
}
```

4. instanceof

instanceof 是 Java 中的一个 运算符 ,用于测试一个对象是否是某个 的实例,或者是否是该类的子类 实例。如果对象是某个类的实例,或者是该类的子类的实例,instanceof 会返回 true。同时,如果对象实现了某个接口,instanceof 也会返回 true,前提是该对象的超类 实现了该接口。

```java
class Animal {}
class Dog extends Animal {}
interface Pet {}

class Cat extends Animal implements Pet {}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        System.out.println(animal instanceof Animal);  // true
        System.out.println(animal instanceof Dog);     // true
        System.out.println(animal instanceof Pet);     // false

        Pet pet = new Cat();
        System.out.println(pet instanceof Pet);        // true
        System.out.println(pet instanceof Animal);     // true
    }
}
```

一般我们在进行强制类型转换时,会先查看是否能够成功地转换,为此需要使用 instanceof 判断,然后再强制类型转换。如下:

```java
Animal animal = new Dog();  
if (animal instanceof Dog) {    // true animal实际是Dog的实例,所以他是Dog类
    Dog tmp = (Dog) animal;
}

if (animal instanceof Animal) { // true Dog是Animal的子类
    Animal tmp = (Animal) animal;
}
```

在Java16中,引入了新的简化的写法。如果为真,会把 animal强制类型转换成Dog类型,并赋值给 tmp。

```java
if (animal instanceof Dog tmp) {};
```

5. 四种访问控制修饰符

  1. 可由外部访问——public
  2. 本包和所有子类可以访问——protected
  3. 本包中可以访问——不带上面三个的修饰符时
  4. 仅本类可以访问——private

权限大小按照上面顺序,buplic最大,其次是protected、默认的、private

6. Object

Object类是Java中所有类的始祖,每个类都继承了Object。我们可以用Object类型的变量引用任意类型的对象,Object animal = new Animal()

6.1 equals

Object 类中的equals方法用于检测一个对象是否等于另一个对象。Object类中实现equals方法将确定两个对象引用 是否相同。这是一个合理的默认是行为,但如果经常要基于状态 检测对象的相等性,可以重载这个方法。(java提供的大部分类都重载了这个方法,确保是每个字段都相同的时候才返回 true

默认情况下我们的类如果没有重载equals方法,那么它的实例调用equals时,比较的就是引用是否相同,但是我们一般更希望它们比较的是字段是否相同。

```java
public class Point {
    int x;
    int y;
}

Point p1 = new Point();
Point p2 = new Point();
if (p1.equals(p2)) {    // false, 因为它两是不同的引用
    System.out.println("true");
}
```

重载这个方法的要点:

  1. 重写 equals 方法时,通常还需要重写 hashCode 方法。根据 Java 的规定,如果两个对象通过 equals 方法比较相等,那么它们的 hashCode 也必须相等。这是为了确保对象在哈希集合(如 HashMap, HashSet)中正确工作。

  2. 重写 equals 方法时,应遵循以下规则:

    • 自反性:对于任何非 null 引用 x,x.equals(x) 应该返回 true

    • 对称性:x.equals(y) 如果返回 true,那么 y.equals(x) 也应该返回 true

    • 传递性:如果 x.equals(y)y.equals(z) 都为 true,那么 x.equals(z) 也应该为 true

    • 一致性:在对象状态没有变化的情况下,多次调用 x.equals(y) 应该返回相同的结果。

    • 非空性:x.equals(null) 应该返回 false

下面是一个例子,这个实现方式,对于同一超类,不同子类的实例,equals会返回false

```java
abstract class Animal {
    private String name;

    // 构造、get、set...

    @Override
    public boolean equals(Object obj) {
        // 自反性检查
        if (this == obj) {
            return true;
        }

        // 类型检查,确保比较的是同一类或同一类的子类对象
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        // 比较字段
        Animal animal = (Animal) obj;
        return Objects.equals(name, animal.name);
    }

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

class Dog extends Animal {
    private String breed;

    // 构造、get、set...

    @Override
    public boolean equals(Object obj) {
        // 确保是相同类型的 Dog 对象
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        Dog dog = (Dog) obj;
        // 调用超类的,以及判断自己特有的字段是否相同
        return super.equals(obj) && Objects.equals(breed, dog.breed);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), breed);
    }
}
```

6.2 hashCode

Object 类中的hashCode方法用于返回对象的哈希值。哈希值是一个整数,通常用于哈希表(如 HashMapHashSet)中快速查找对象。

如果重新定义了equals方法,还必须为用户可能插入散列表的对象重新定义hashCode方法。

```
// Objects.hash 会对各个参数调用Objects.hashCode,并组合这些散列值
@Override
public int hashCode() {
    return Objects.hash(super.hashCode(), breed);
}
```

6.3 toString

Object 类中的toString方法用于返回对象的字符串表示形式。默认情况下,Object 类的 toString 方法返回对象的类名哈希值(十六进制)(类似 ClassName@hashCode)。

```java
// 默认的toString方法
public String toString() {
    return getClass().getName() + '@' + Integer.toHexString(hashCode());
}
```

一般我们会用 类名[字段名1=字段值1,字段名2=字段值2] 的形式重载 toString 方法

注意 :在重载方法时,不要将类名字符串硬编码到方法中,最好通过调用 getClass().getName 获取类名的字符串。这样我们后续在编写这个类的子类时,就可以调用 super.toString() + 子类中新的字段。

7. 包装器、装箱和拆箱

Integer 类对应基本类型 int ,像这些类被称为包装器 。比如 IntegerLongFloatDoubleCharacterBoolean

包装器类是不可变 的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,包装器类还是final,因此不能派生 出它们的子类。

  1. 装箱

    java
    // 第一行这样写在编译成的jvm指令,等价于第二行这种写法。这种转换成为自动装箱
    Integer n = 1;
    Integer n = Integer.valueOf(1); // 构造一个对象,值为1

  2. 拆箱

    java
    // 第二行这样写在编译成jvm指令,等价于第三行这种写法。这种转换成为自动拆箱
    Integer n = 1;
    int m = n;
    int m = n.intValue();

补充:

  1. Integer本质上还是对象,所以 n 和 m通过 == 比较的是对象的引用是否相同,但是观察可以发现,正常来说 n 和 m 的引用应该是不同的,但是他们输出的却是 true。这是由于 Integer 对 -128 到 127 之间的数字进行了缓存,当时中这些数字时都引用缓存对象;这也是为什么 x == y 输出的确实 false的原因。

    ```java
    Integer n = 127;
    Integer m = 127;
    System.out.println(n == m); // true

    Integer x = 128;
    Integer y = 128;
    System.out.println(x == y); // false
    ```

  2. 不要将包装器对象作为锁

8. 抽象类

使用 abstract 关键字修饰的方法叫做抽象方法,可以不需要写具体的实现。同时,为了提高程序的清晰性,包含一个或多个抽象方法的类本身必须带上 abstract 关键字,声明为抽象类

  1. 抽象类不可以创建实例,可以创建一个它的对象变量,但是这个变量只能引用其非抽象子类的对象。

    java
    // 假设这里的 Animal 是抽象类,Dog是其非抽象子类
    Animal animal = new Animal(); // 不可以
    Animal animal = new Dog(); // 可以

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

  3. 抽象类的子类如果存在超类中的抽象方法,并且未对其进行具体的实现,那么这个子类必须也声明为抽象类。如果子类将超类中的所有的抽象方法都具体实现了,那么就不再是抽象的了

9. 密封类

Java 的 密封类 (Sealed Class)是在 Java 15 中引入的特性,它允许开发者控制哪些类可以继承某个类或者实现某个接口。用关键字 sealed 指定密封类,用 permits 指定哪些类可以继承这个类或结构。

对子类的约束

  1. 子类不能是嵌套在另一个类中的私有类,也不能是位于另一个包中的可见类,必须是与封装类在同一个包中的。(如果是模块的话,必须是在同一个模块中的)
  2. 密封类的子类可以是 finalsealednon-sealed。如果子类是 sealed,它仍然可以进一步指定允许的子类。(no-sealed 标记该类不再密封)

10. 反射

反射是 Java 的一种机制,使得程序能够在不知道具体类的信息 的情况下操作这些类。比如访问、修改 类的结构和成员(方法、字段、构造函数 等)。

其中Java 反射是通过 Class对象 获取类的元数据的,当 JVM 加载一个类时,Java 会根据 .class 文件中的字节码生成一个 Class 对象。

10.1 Class对象

获取 Class 对象:

```java
// 最常用
Class clazz1 = Class.forName("other.Person");

// 一般当做参数传递
Class clazz2 = Person.class;

// 有这个类对象时,调用获取
Person p = new Person();
Class clazz3 = p.getClass();

System.out.println(clazz1 == clazz2);   // true
System.out.println(clazz2 == clazz3);   // true
```

10.2 Constructor

通过 Constructor 类,我们可以在运行时获取类的构造函数信息,并动态地调用构造函数来创建对象。

  1. 获取 Constructor 类

    ```java
    Class clazz = Class.forName("other.Person");

    Constructor[] constructors1 = clazz.getConstructors(); // 获取所有的 public 构造方法
    Constructor constructor2 = clazz.getConstructor(String.class, Integer.class);// 获取指定的 public 构造方法

    Constructor[] declaredConstructor1 = clazz.getDeclaredConstructors(); // 获取所有构造方法(public, private,protect都可以)
    Constructor declaredConstructor2 clazz.getDeclaredConstructor(String.class, Integer.class); // 获取指定的构造方法(public, private,protect都可以)

    public class Person {
    private String name;
    private Integer age;

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

    }
    ```

  2. 获取信息和创建实例

    ```java
    Class clazz = Class.forName("other.Person");

    Constructor constructor = clazz.getConstructor(String.class, Integer.class);
    System.out.println(constructor.getName()); // other.Person
    System.out.println(constructor.getModifiers()); // 1 (访问权限,1是public)
    System.out.println(Arrays.deepToString(constructor.getParameters())); // [java.lang.String arg0, java.lang.Integer arg1]
    // constructor.setAccessible(true); 如果获取到的constructor的构造方法是私有的,需要先调用这个才可以通过 newInstance 创建实例。
    // 这里我们获取到的是 Public 的,所以不需要调用
    Person person = (Person) constructor.newInstance("zhang san", 18);
    ```

10.3 Field

通过 Field 类我们可以在在运行时获取或修改类的字段。

  1. 获取 Field

    ```java
    Class clazz = Class.forName("other.Person");

    Field[] fields1 = clazz.getFields(); // 获取所有 public 的字段
    Field name1 = clazz.getField("name"); // 获取指定的 public 字段
    Field[] fields2 = clazz.getDeclaredFields(); // 获取所有字段(包含public,protect,private)
    Field name2 = clazz.getDeclaredField("name"); // 获取指定字段(包含public,protect,private)
    ```

  2. 获取、修改信息

    ```java
    Class clazz = Class.forName("other.Person");

    Field name = clazz.getDeclaredField("name"); // 获取指定字段
    System.out.println(name.getName()); // name
    System.out.println(name.getModifiers()); // 2 (访问权限,2是private)
    System.out.println(name.getType()); // class java.lang.String (字段类型)

    name.setAccessible(true); // 因为我们的name是private,所有设置权限为true,允许访问私有字段。
    Person person = new Person("zhang san", 18); // 一般这里通过constructor来创建
    System.out.println(name.get(person)); // zhang san (获取指定实例的name字段值)
    name.set(person, "lao wang"); // 修改指定实例的name字段值
    System.out.println(person.getName()); // lao wang
    ```

10.4 Method

通过 Method 类可以用来访问、调用类中的方法

  1. 获取 Method

    ```java
    Class clazz = Class.forName("other.Person");

    Method[] methods = clazz.getMethods();
    Method method = clazz.getMethod("setName", String.class);

    Method[] declaredMethods = clazz.getDeclaredMethods();
    Method declaredMethod = clazz.getDeclaredMethod("setName", String.class);
    ```

  2. Method 的使用

    ```java
    Class clazz = Class.forName("other.Person");

    Method method = clazz.getMethod("setName", String.class);
    System.out.println(method.getName()); // setName
    System.out.println(method.getModifiers()); // 1
    System.out.println(Arrays.deepToString(method.getParameters())); // 获取形参
    System.out.println(Arrays.deepToString(method.getExceptionTypes()));// 获取方法抛出的异常
    Person person = new Person("zhang san", 18);
    method.invoke(person, "wang wu"); // 调用这个方法,第一个参数是要调用的实例,第二个参数是方法的实参(可接受多个)
    System.out.println(person.getName()); // wang wu
    ```

四、接口、lambda表达式与内部类(6)

1. 接口

接口中的所有方法都自动是 public 抽象方法。因此,在接口中声明方法时,不必提供关键字 public。

  1. 接口概念

接口中绝不会有实例字段、方法的实现

```java
public interface Test {
    // 接口中的字段(常量),默认是 public static final
    String test = "hello world";

    // 接口中的方法,默认是 public abstract
    void test();

    // 默认方法,提供实现
    default void sleep() {
        System.out.println("Animal is sleeping");
    }

    // 静态方法
    static void breathe() {
        System.out.println("Animal is breathing");
    }
}
```
  1. 接口属性

Comparable 接口的主要作用是定义对象的自然顺序 (natural ordering),让对象能够按照一定规则进行排序。实现了 Comparable 接口的类的对象可以直接使用 Java 提供的排序机制(如 Arrays.sort()Collections.sort())进行排序。

```java
public class Person implements Comparable {

    @Override
    public int compareTo(Object o) {
        return 0;
    }
}
```

虽然接口不能构造对象,但是可以声明接口变量。而且可以用 instanceof 来判断某个对象的类是否或超类是否是实现了 Comparable 接口的类。

```java
Comparable person = new Person();
System.out.println(person instanceof Comparable);   // true
```

补充:记录枚举类不能扩展其他类,因为他们隐式地扩展了Record和Enum类。(一个类只能有一个超类,但可以有多个接口,所以记录和枚举类可以实现多个接口)

  1. 默认方法冲突规则

上面讲到,接口中可以定义默认方法 final,但是如果一个类的超类中和实现的一个接口中定义了同一个方法,会发生什么。

  • 超类优先:如果超类提供了一个具体方法,同名而且有相同参数类型,那么超类中的方法会覆盖接口中的默认方法
  • 接口冲突:如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法(不论是否是默认方法),必须覆盖这个方法来解决冲突

2. lambda 表达式

对于只有一个抽象方法 的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口成为函数式接口。(函数式接口通常含有 @FunctionalInterface 注解)

特点:延迟执行 :Lambda 表达式往往用于支持延迟执行的 API 中,如 Stream API、Runnable 等。

2.1 方法引用

方法引用本质上是 Lambda 表达式的一种特殊形式,可以用来替代 Lambda 表达式中调用一个已经定义的函数。

  1. 静态方法引用

静态方法引用用于调用类的静态方法。基本语法是:ClassName::staticMethodName

```java
public class MathUtils {
    public static int add(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        // 使用 Lambda 表达式
        BinaryOperator addLambda = (a, b) -> MathUtils.add(a, b);
        System.out.println(addLambda.apply(3, 5));  // 输出 8

        // 使用方法引用
        BinaryOperator addMethodReference = MathUtils::add;
        System.out.println(addMethodReference.apply(3, 5));  // 输出 8
    }
}
```
  1. 具体对象引用实例方

实例方法引用用于调用对象的实例方法。语法是:instanceName::instanceMethodName

```java
class Person {
    private String name;

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

    public void greet() {
        System.out.println("Hello, " + name);
    }

    public static void main(String[] args) {
        Person person = new Person("John");

        // 使用 Lambda 表达式
        Consumer greetLambda = v -> person.greet();
        greetLambda.accept(null);  // 输出 "Hello, John"

        // 使用方法引用
        Consumer greetMethodReference = person::greet;
        greetMethodReference.accept(null);  // 输出 "Hello, John"
    }
}
```
  1. 类引用了实例方法

基本语法是:ClassName::MethodName

```java
public class Main {
    public static void main(String[] args) {
        String[] words = {"Banana", "apple", "Orange", "grape"};

        // 使用 Lambda 表达式
        Arrays.sort(words, (x, y) -> x.compareToIgnoreCase(y));
        System.out.println(Arrays.toString(words));  // 输出: [apple, Banana, grape, Orange]

        // 使用方法引用
        // String::compareToIgnoreCase 是一个可以接受两个 String 类型的参数的比较器方法,它会被用在需要比较两个 String 的上下文中。
        Arrays.sort(words, String::compareToIgnoreCase);
        System.out.println(Arrays.toString(words));  // 输出: [apple, Banana, grape, Orange]
    }
}
```
  1. 构造器引用

构造器引用通常用于通过 构造器 创建对象。语法是:ClassName::new

```java
class Car {
    private String model;

    public Car(String model) {
        this.model = model;
    }

    public void display() {
        System.out.println("Car model: " + model);
    }

    public static void main(String[] args) {
        // 使用 Lambda 表达式
        Supplier carLambda = () -> new Car("Toyota");
        Car car1 = carLambda.get();
        car1.display();  // 输出 "Car model: Toyota"

        // 使用构造器引用
        Supplier carConstructorReference = Car::new;  // 引用 Car 的构造器
        Car car2 = carConstructorReference.get();
        car2.display();  // 输出 "Car model: null"(因为没有提供参数)
    }
}
```

2.2 变量作用域

  1. 局部变量或常量

Lambda 表达式可以访问 局部变量常量 ,但要求这些变量必须是 final等效于final(即不可修改的)。并且它们的值在lambda内部不能被改变。

```java
public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                int j = i;
                new Thread(() -> {
//                    System.out.println(i);// 会报错,因为 i 是可变的
                    System.out.println(j);  // 虽然是变量,但是等效于 final,因为他没有发生变化
//                    j = 2;                // 会报错,不可以修改
                });
            }
        }
```

上面讲到lambda有一个特点就是 延迟执行,而执行的时候局部变量可能已经不存在了,为什么对于局部变量或常量可以访问,并且只能访问?

闭包:函数可以捕获并“记住”它被创建时的环境(外部变量),即使该函数在执行时,外部环境中的变量已经失效或不再存在

Lambda 表达式通过创建一个内部类(或匿名类)来实现闭包。在编译时,Lambda 表达式会被转换成一个匿名的 函数对象 ,它实现了某个接口(通常是 RunnableCallableFunctionConsumer 等)。这个匿名类会捕获的外部变量并变成匿名类的 实例变量 ,保持对原始外部变量的引用。确保即使在外部作用域中这些变量已经结束,Lambda 表达式仍然能访问这些变量。

补充:上面讲到会创建内部类,但是lambda表达式中的 this 指定还是正常类中的this,而不是内部类的this,其含义并没有发生变化。

  1. 成员变量

Lambda 表达式可以访问外部类的 成员变量 (即实例变量或类变量),不需要 final等效于 final 的限制。也可以在lambda中修改。

```java
public class Main {
    int a = 5;
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                Main test = new Main();
                new Thread(() -> {
                    System.out.println(test.a);
                    test.a = 10;
                });
            }
        }
}
```

3. 内部类

内部类是定义在另一个类内部的类。可以让代码更加紧凑,增强可读性,并提供一些特殊的功能(如访问外部类的实例变量和方法)。

在 Java 中,内部类主要有以下几种类型:

  1. 成员内部类(Non-static Inner Class)

  2. 静态内部类(Static Inner Class)

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

  4. 匿名内部类(Anonymous Inner Class)

  5. 成员内部类

成员内部类是定义在外部类的成员位置(方法外部)且没有被声明为静态的类。它可以访问外部类的所有成员,包括私有成员。成员内部类必须通过外部类的实例来创建。

```java
class Outer {
    private int outera = 10;

    private static int outerb = 10;

    private String outerc() {
        return "outerc";
    }

    private static String outerd() {
        return "outerd";
    }

    class Inner {
        public void display() {
            System.out.println("display " + Outer.this.outera);
            System.out.println("display " + Outer.outerb);
            System.out.println("display " + Outer.this.outerc());
            System.out.println("display " + Outer.outerd());
        }

        public static void display2() { // 同样,静态方法只能访问静态变量
            System.out.println("static display2 " + Outer.outerb);
            System.out.println("static display2 " + Outer.outerd());
        }
    }

    public static void main(String[] args) {
        // 创建外部类的实例
        Outer outer = new Outer();

        // 通过外部类的实例创建内部类的实例
        Outer.Inner inner = outer.new Inner();
        inner.display();
        Outer.Inner.display2();
    }
}
```
  1. 静态内部类

静态内部类是使用 static 修饰的内部类。与成员内部类不同,静态内部类不依赖于外部类的实例 ,只能访问外部类中的静态变量和静态方法。

```java
class Outer {
    private static int staticVar = 20;

    static class Inner {
        public void display() {
            // 只能访问外部类的静态成员
            System.out.println("display " + staticVar);
        }

        public static void display2() {
            System.out.println("display2 " + staticVar);
        }
    }

    public static void main(String[] args) {
        // 通过类名直接创建静态内部类的实例
        Outer.Inner inner = new Outer.Inner();
        inner.display();
        Outer.Inner.display2();
    }
}
```
  1. 局部内部类

局部内部类是定义在方法中的内部类。它只在方法内部可见,生命周期仅限于方法的执行。可以访问方法的局部变量,但这些局部变量必须是 final 或者是隐式 final(在 Java 8 及以后)

```java
class Outer {
    public static int outerA = 1;

    public int outerB;

    public void outerMethod() {
        final int localVar = 30; // 局部变量必须是 final

        class Inner {
            public void display() {
                System.out.println("display: " + localVar);
                System.out.println("display: " + Outer.outerA);
                System.out.println("display: " + Outer.this.outerB);
            }
        }

        // 创建并调用局部内部类的实例
        Inner inner = new Inner();
        inner.display();
    }

    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.outerMethod();
    }
}
```
  1. 匿名内部类

匿名内部类没有类名,通常用于实现接口继承类 ,并且仅在单个方法中使用一次。(匿名内部类是一个没有类名的类,不能有构造方法 ,构造参数会传给超类的构造方法

```java
class Outer {
    public void display() {
        // 使用匿名内部类实现接口  
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymous class running");
            }
        };

        r.run(); // 调用匿名类的方法
    }

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

补充:上面所有例子中,我们访问外部类的变量或方法时,通过 OuterClass.this.变量名OuterClass.变量名,其实可以不用写前缀 OuterClass.thisOuterClass.,当内部类中和外部类命名冲突时,直接写变量名或方法名默认是内部类的。

4. 代理

Java 中的代理通过在方法调用前后加入额外的行为,增加了灵活性,但也有性能开销。通常有两种方式:

  1. 静态代理 :手动编写代理类。

  2. 动态代理 :通过 Java 提供的 API 动态生成代理类。

4.1 静态代理

静态代理 是指代理类和被代理类必须在编译时 就确定,代理类通常会显式实现被代理类的接口并转发方法调用。

在下面的例子中,AnimalProxy 是静态代理类,它实现了 Animal 接口,并且在 speak 方法前后增加了自己的逻辑。

```java
interface Animal {
    void speak();
}

class Dog implements Animal {
    public void speak() {
        System.out.println("Woof!");
    }
}

// 代理 Animal 类,编译阶段就确定了要代理的类
class AnimalProxy implements Animal {   
    private Animal animal;

    public AnimalProxy(Animal animal) {
        this.animal = animal;
    }

    public void speak() {
        System.out.println("Proxy: Before method call");    // 前后都加上了额外的行为
        animal.speak();  // 委托给实际的 Animal 实现
        System.out.println("Proxy: After method call");
    }
}

public class StaticProxyExample {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal proxy = new AnimalProxy(dog);    // 把dog交给代理类
        proxy.speak();
    }
}
```

4.2 动态代理

动态代理是通过 Java 的 Proxy 类和 InvocationHandler 接口在运行时创建代理类。它不需要显式创建代理类,代码更加灵活。(Java 动态代理只能为实现了接口的类 创建代理。如果类没有接口,需要使用 CGLIB 等库来实现代理。)

```java
interface Animal {
    void speak();
}

class Dog implements Animal {
    public void speak() {
        System.out.println("Woof!");
    }
}

class AnimalInvocationHandler implements InvocationHandler {
    private Object target;

    public AnimalInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Proxy: Before method call");
        Object result = method.invoke(target, args);  // 调用实际对象的方法
        System.out.println("Proxy: After method call");
        return result;
    }
}

public class DynamicProxyExample {
    public static void main(String[] args) {
        Animal dog = new Dog();

        Animal proxy = (Animal) Proxy.newProxyInstance(
                dog.getClass().getClassLoader(),
                new Class[] { Animal.class },
                new AnimalInvocationHandler(dog));

        proxy.speak();
    }
}
```

在上面的例子中:

  • Proxy.newProxyInstance 创建了一个动态代理对象。
  • InvocationHandler 接口的 invoke 方法实现了方法的拦截和增强逻辑。

补充:

  1. 代理类时在程序运行过程中动态创建的,一旦创建,它们就是常规的类,与虚拟机中的任何其它类没有区别。

  2. 对于一个特定的类加载器和一组接口,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次 newProxyInstance 方法,将得到同一个类的两个对象。也可以用 getProxyClass 方法获取这个类。

newProxyInstance参数说明

  1. ClassLoader loader
    这个参数指定了代理对象的类加载器。它用于加载生成的代理类。

    • 代理类会由 loader 加载到 JVM 中。通常,可以使用目标类的类加载器

示例:

    ```java
ClassLoader loader = MyClass.class.getClassLoader();
```
  1. Class[] interfaces
    这是一个接口数组,指定了代理对象需要实现的接口。代理对象将实现这些接口,并且通过代理来处理接口中声明的方法。

    • 代理对象将 实现 这些接口,所以你可以在代理对象上调用接口中的方法。
    • 可以为代理对象指定多个接口。

示例:

    ```java
Class[] interfaces = new Class[] {Runnable.class, AutoCloseable.class};
```
  1. InvocationHandler h
    这是一个 InvocationHandler 的实例,它用于处理代理对象的方法调用。当代理对象上的方法被调用时,InvocationHandlerinvoke 方法将被执行。

    • InvocationHandlerinvoke 方法会被传入代理对象、方法、方法参数等信息,允许你自定义方法调用的行为。

示例:

    ```java
InvocationHandler handler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Invoked method: " + method.getName());
        return null;
    }
};
```

五、异常、断言和日志(7)

所有异常都是派生于 Throwable 类的一个类的实例

在这里插入图片描述

检查型异常 (继承自 Exception)在编译前,编译器就会检测,如果存在检查型异常,要么通过 try-catch 捕获异常,要么通过 throws 声明抛出异常。(如果一个方法有可能抛出多个检查型异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。)

非检查型异常 (继承自 RuntimeException)编译前不会检测,只有在运行时才会检测。

如果异常一直被抛出并且没有被捕获 ,最终会导致程序终止 。这适用于检查型异常非检查型异常 。异常会沿着调用栈传播,直到找到一个合适的 catch 块进行处理,或者异常到达最顶层(例如 main 方法)并且没有被捕获,程序会在这个点终止。

1. 声明异常

如果在子类中覆盖超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更通用

```java
class Parent {
    public void doSomething() throws IOException {
        // ...
    }
}

class Child extends Parent {
    @Override
    public void doSomething() throws Exception { // 这是不允许的
        // ...
    }
}
```

在上面的代码中,如果 Child 类抛出了 Exception,那么调用者如果捕获 IOException 时,就无法捕获 Child 类可能抛出的 Exception。这就破坏了父类和子类方法在异常处理上的一致性。

2. try catch

抛出异常:

  1. 如果 try 语句块中的任何代码抛出的是 catch 字句中指定的一个异常类,那么程序将跳过 try 语句块中的其余代码,然后执行catch子句中的处理器代码
  2. 如果抛出的不是 catch子句中指定的异常类型,那么这个方法会立即退出

无论是否抛出异常,最后都会执行 finally 子句中的代码

3. try-with-resources

try-with-resources 用于简化资源管理,特别是用于自动关闭实现了 AutoCloseable 接口的资源。资源通常是指一些需要手动关闭的对象,例如文件流、数据库连接、网络套接字等。

try-with-resources 要求资源必须实现 AutoCloseable 接口(AutoCloseable 接口有一个 close() 方法),可以声明一个或多个资源,只要它们都实现了 AutoCloseable 接口。多个资源之间用分号 ; 隔开。

```java
try (BufferedReader reader1 = new BufferedReader(new FileReader("file1.txt"));
     BufferedReader reader2 = new BufferedReader(new FileReader("file2.txt"))) {
    String line1;
    while ((line1 = reader1.readLine()) != null) {
        System.out.println(line1);
    }

    String line2;
    while ((line2 = reader2.readLine()) != null) {
        System.out.println(line2);
    }
} catch (IOException e) {
    e.printStackTrace();
}
```

4. 断言

断言(assertion )是 Java 中的一种调试工具,用于在程序运行时验证某个条件是否为真。如果条件为假,断言会抛出一个 AssertionError 异常,从而提示开发者在代码中存在逻辑错误。断言主要用于在开发和测试过程中捕捉潜在的错误,而不是用于生产环境中的错误处理。

在默认情况下,断言是禁用的,这意味着即使代码中存在 assert 语句,它们也不会执行。要启用断言,可以在运行时通过 JVM 参数来启用:

  • 启用断言: 使用 -ea-enableassertions 参数启用断言。
  • 禁用断言: 使用 -da-disableassertions 参数禁用断言。

  • 基本语法

    java
    assert expression;
    assert expression : message;

  • 示例

    ```java
    public class AssertionExample {
    public static void main(String[] args) {
    int x = 5;

        // 断言 x 必须大于 0
        assert x > 0 : "x should be greater than 0";  // 如果 x <= 0,会抛出 AssertionError
    
        System.out.println("x is positive.");
    }
    

    }
    ```

六、泛型程序设计(8)

泛型(Generic)使得类、接口和方法能够处理各种类型的数据 。在没有泛型的情况下,所有的数据类型通常都作为 Object 来处理,这可能导致类型不安全的错误。而泛型允许你在定义类、接口或方法时,使用类型参数来指定数据类型

在Java中增加泛型类之前,泛型程序设计都是用继承实现的。ArrayList类只维护一个Object引用的数组。这种方法存在两个问题:

  1. 获取一个值时必须进行强制类型转换
  2. 可以向数组列表中添加任何类型的值(没有错误检查

类型变量 是泛型编程的核心概念之一。它是一个占位符,代表了一个类型,可以在编译时被指定为具体类型。类型变量通常用单个字母来表示,最常见的字母是 T,但你也可以使用其他字母(如 E, K, V, N 等)。

1. 泛型类

泛型类是指在类的定义中使用类型参数 的类。这使得类能够在实例化时指定具体的数据类型。

```java
// 定义一个泛型类 Box
public class Box {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}
```

T 是类型参数,它代表了泛型类 Box 能处理的类型。T 可以是任何合法的 Java 类型(例如 IntegerStringCustomClass 等)。

```java
public class Main {
    public static void main(String[] args) {
        // 使用 Integer 类型创建 Box 对象
        Box intBox = new Box<>();
        intBox.setValue(123);
        System.out.println(intBox.getValue());

        // 使用 String 类型创建 Box 对象
        Box strBox = new Box<>();
        strBox.setValue("Hello Generics");
        System.out.println(strBox.getValue());
    }
}
// 输出
123
Hello Generics
```

2. 泛型方法

泛型方法是指在方法的声明中使用类型参数 ,而不是在类中使用。这使得方法可以操作不同类型的参数并返回相应的类型。(可以在普通类中定义泛型方法,也可以在泛型类中定义)

```java
public class GenericMethodExample {

    // 泛型方法:返回类型和参数类型都是泛型的
    public static  void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4};
        String[] strArray = {"Java", "Generics", "Example"};

        // 调用泛型方法
        printArray(intArray);  // 输出:1 2 3 4
        printArray(strArray);  // 输出:Java Generics Example
    }
}
```

这里 表示方法中的泛型类型参数。

在调用 printArray 方法时,JVM 会根据传入的数组类型推断出 T 的类型。

3. 类型参数的限定

有时,类或方法需要对类型参数加以约束。比如要求传入的类必须是实现 Comparable 接口的类,那么可以使用 来限定。(用 & 隔开多个限定 ,限定可以是接口或类)

```java
public static  void printArray(T[] array) {
    for (T element : array) {
        System.out.print(element + " ");
    }
    System.out.println();
}
```

补充:按照Java继承机制,限定可以有多个接口 ,但是只能有一个是类 ,并且如果有类时,必须把 放在限定列表中的第一个限定。

4. 泛型擦除

Java 的泛型(Generics)允许我们编写更加灵活、可重用的代码,通常在编译时会通过类型参数来进行类型检查和保证类型安全。但是,Java 中的泛型并不像其他语言那样在运行时保留类型信息。在 Java 中,泛型是通过 类型擦除(Type Erasure) 机制来实现的,这意味着在编译时类型参数会被擦除,转化为原始类型。

4.1 泛型擦除的基本概念

Java 的泛型采用了 类型擦除 (Type Erasure)的方式来实现。类型擦除是指,在编译阶段,泛型类型参数会被擦除成原始类型(通常是 Object 类型,或者是限定类型的类型)。换句话说,泛型信息在字节码中不再存在。

```java
public class GenericExample {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}
```

在编译时,GenericExample 会被擦除成 GenericExample,并且所有的 T 类型参数都会被替换成原始类型。换句话说,T 会被擦除为 Object,并且类型信息不会保留在编译后的字节码中。

4.2 泛型擦除的工作方式

所有的泛型类型参数都会被擦除为它们的 限定类型 ,如果没有显式的限定类型,类型参数会被擦除为 Object。如果有类型限制(例如 T extends Number),那么泛型参数会被擦除为该限制类型。

  • 如果泛型类型参数没有边界(没有 extends 关键字限制),它会被替换为 Object
  • 如果泛型类型参数有边界(例如 T extends Number),它会被替换为这个边界类型。

    ```java
    public class Box {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
    

    }
    ```

在编译后,Box 会变成 Box,而 T 会被擦除为 Object。如果使用了有边界的类型参数,如 Box,则 T 会被擦除为 Number

4.3 泛型擦除后的影响

  1. 无法直接获取泛型类型信息

由于泛型类型在运行时已经被擦除,Java 在运行时无法知道泛型参数的类型。例如:

```java
public class Test {
    public void printClassName() {
        System.out.println(T.class.getName());  // 编译时错误:无法找到类 T
    }
}
```

上面的代码会编译失败,因为 T 在运行时并不保留类型信息。泛型擦除后,T 只会变成 Object

  1. 泛型不能用于实例化对象和数组

由于泛型参数在运行时被擦除,不能使用泛型类型来创建对象。例如:

```java
public class Test {
    public void createObject() {
        T obj = new T();  // 编译时错误:无法实例化泛型类型 T
        T[] mm = new T[2];  // 编译时错误:无法实例化泛型类型 T
    }
}
```

这段代码会在编译时出错,因为 T 是一个类型参数,在运行时并没有实际的类型,因此无法实例化。

  1. 不能创建参数化类型的数组

    java
    public class GenericArrayExample {
    public static void main(String[] args) {
    // 编译错误:无法创建参数化类型的数组
    List[] array = new List[10]; // 编译错误
    }
    }

  2. 泛型方法和重载

由于泛型类型参数会被擦除,两个泛型方法如果只在类型上不同,可能会发生冲突。由于类型擦除后,它们的参数类型是相同的,编译器会报错。

例如,以下代码会在编译时出现错误:

```java
public class Test {
    public void print(String s) {
        System.out.println(

相关文章

暂无评论

暂无评论...