Java21手册(三):变量、常量与类型

avatar
@比心

本篇将会介绍Java 8后续版本中,变量声明、常量类型、以及新class类型等方面的扩充。文章中代码较多,适合在PC上查看,源码可查看原文链接。

3.1 var - 声明局部变量关键字

var 的基本用法

var是声明局部变量的新关键字,通过类型推断来简化繁琐的类型声明,同时保持对象类型是静态类型不变。例如,可以用var进行以下变量声明:

var list = new ArrayList<String>(); // list的类型推断为 ArrayList<String>
var stream = list.stream();        // stream的类型推断为 Stream<String>

局部变量声明采用类型推断的方法,在其他热门语言中已经很常见,例如C++ (auto), C# (var), Scala (var/val), Go ( := ),java几乎是仅有的还不具备这个能力的热门语言了。实际上类型推断已经在lambda中得到了广泛应用,例如:

int maxWeight = blocks.stream()
        .filter(b -> b.getColor() == BLUE)
        .mapToInt(Block::getWeight)
        .max();

熟悉Java8的开发者不会对lambda中变量 b的类型有任何困惑,同样,局部变量类型推断也不会对开发者造成理解困难。许多局部变量的使用实际上是链式的,这种代码用类型推断来声明会更加直观,例如:

var path = Paths.get(fileName);
var bytes = Files.readAllBytes(path);

关于var的语义有几点要注意:

  1. var声明的变量依然是静态类型,并非像javascript那样声明动态类型变量;
  2. var声明的变量不是final类型,但也可以通过 final var 这种形式声明;
  3. var只能用来声明本地变量,不能声明fields、方法参数类型以及方法返回类型;
  4. 除此之外还有一些情况下的本地变量声明不适用于var 类型推断,例如:
var x;  // var不能声明未初始化的变量
var f = () -> { };  // 不能声明lambda表达式
var g = null;  // 不能声明null
var m = this::l;  // 不能声明方法引用
var k = { 1 , 2 };  // 不能声明数组
var f = 1, k = 1;  // 不能声明多个对象

为了对齐局部变量的使用,lambda的参数也支持了var声明:

(var x, var y) -> x.process(y);
//等同于
(x, y) -> x.process(y);

统一的好处是,修饰符尤其是注解,可以统一应用于局部变量和Lambda的形式参数,而不会失去简洁性:

@Nonnull var x = new Foo();
(@Nonnull var x, @Nullable var y) -> x.process(y)

var还可以让我们能很方便地声明和使用匿名内部类变量,例如:

var ref = new Object() {
    int a;
    int b;
};
ref.a = 10;
System.out.println(ref.a);

这个例子中ref的类型是代码所在类的匿名内部类,继承自Object,通过var的类型推断,我们可以在上下文中方便地使用ref类型扩展的两个字段。在之前版本中要实现相同的功能,只能通过新定义class声明来实现,对于开发非常不方便。

var声明匿名内部类另一个作用是在lambda中临时存放变量,例如:

var ref = new Object() {
    int a;
    int b;
};
ref.a = 10;
System.out.println(ref.a);

这个函数的作用是,过滤数组中的第一个负数元素,因此在stream中需要更新过滤状态 ref.filtered 字段。在之前类似的功能只能通过新定义class,或包装为集合类型来实现,使用var明显简化了代码开发。

var 的使用原则

虽然 var 通过消除冗余信息可以使代码更易读,但也可能因为省略了类型信息而使代码更难读,过度使用也会导致我们编写出糟糕的Java代码。如何用好var,语法的开发者们给了我们一些使用原则:

  1. 读代码比写代码更重要,代码的阅读频率比编写频率高得多。在编写代码时,我们通常在头脑中有整个上下文,并且写的过程会很专注;而阅读代码时,我们经常需要进行上下文切换,而且可能比起编写会更加匆忙。短程序可能比长程序更可取,但过于缩短程序可能会省略对程序理解有用的信息。我们需要找到适当的代码块大小,以最大化可理解性。
  2. 代码应该从本地推理中清晰明了。读者应该能够立刻看懂var声明和已声明变量的含义和作用。
  3. 代码可读性不依赖IDE,代码的表意应该自解释。代码应该在表面上就能被理解,不需要工具的帮助。
  4. 显式类型是一种权衡,省略显式类型可以减少混乱,但仅当省略不会影响可读性时才可行。类型不是向读者传递信息的唯一途径,我们还可以通过用变量名称和初始化表达式等方式来实现目的,写代码时要考虑所有可用的方式。

var 的最佳实践

1. 通过变量名传递有用信息

给变量起一个好名字是很常见的良好实践,在var的上下文中这一点会更加重要。将显式类型替换为var通常应该伴随着改进变量名称。例如:

// 原始代码
List<Customer> x = dbconn.executeQuery(query);
// 良好实践
var custList = dbconn.executeQuery(query);

// 原始代码
try (Stream<Customer> result = dbconn.executeQuery(query)) {    
    return result.map(...).filter(...).findAny();
}
// 良好实践
try (var customers = dbconn.executeQuery(query)) {    
    return customers.map(...).filter(...).findAny();
}

上面的两个例子,无用的变量名称(x、result)被替换为能体现变量类型的名称(cusList、customers),让代码变得简洁的同时也没有让可读性变差,甚至原来的变量通过有效命名,在上下文中可读性会更好。

2. 最小化局部变量的作用域

限制局部变量的作用域通常是良好的实践,这个实践在《Effective Java》中有更详细的描述。而使用var时,这个实践就更为重要。

在下面的例子中,add方法清楚地将特殊项添加为最后一个列表元素,因此后面的循环会按照预期最后处理这个特殊项:

var items = new ArrayList<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...

假设我们为了删除重复项,修改此代码,使用HashSet而不是ArrayList:

var items = new HashSet<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...

此时这段代码就会有一个错误,因为Set没有定义的迭代顺序,但开发者很可能会立即修复此错误,因为使用items变量的地方与其声明相邻。

现在假设此代码是一个大方法的一部分,变量的使用跨了一个很大的作用域:

var items = new HashSet<Item>(...);
// …100行代码...

items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...

items的使用距离其声明语句如此之远,于是从ArrayList改为HashSet所带来的影响,看起来也不再明显了,这段代码出问题的概率也就会大大增加。

3. 在初始化语句已经提供足够信息时使用

本地变量通常使用构造函数进行初始化,构造函数的类名通常作为左侧的显式类型重复使用。如果类型名称很长,则使用 var 可以在不损失信息的情况下提供简洁性:

// 原始代码
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

// 好的代码
var outputStream = new ByteArrayOutputStream();

在初始化程序是方法调用(如静态工厂方法)而不是构造函数的情况下,并且方法名称包含足够的类型信息时,使用 var 也是合理的:

// 原始代码
BufferedReader reader = Files.newBufferedReader(...);
List<String> stringList = List.of("a", "b", "c");

// 好的代码
var reader = Files.newBufferedReader(...);
var stringList = List.of("a", "b", "c");

在这些情况下,方法的名称已经表明了特定的返回类型。这里引入了一种新的List初始化方式List.of(),这个特性将会在第4篇中继续介绍。

4. 使用 var 拆分链式或嵌套表达式

下面是一段处理字符串集合,并找出出现最多次数的字符串的代码:

return strings.stream()
      .collect(groupingBy(s -> s, counting()))
      .entrySet()
      .stream()
      .max(Map.Entry.comparingByValue())
      .map(Map.Entry::getKey);

这段代码虽然是正确的,但是表意不明确,因为看起来像是一个单独的流管道。实际上,它包含两个stream,第二个stream对第一个stream的结果进行操作,最后再对第二个stream的结果进行映射。表达这段代码的最可读方式应该是两到三个语句;首先将集合分组到一个映射中,然后在该映射上进行缩减,最后从结果中提取键,如下所示:

Map<String, Long> freqMap = strings.stream()      
    .collect(groupingBy(s -> s, counting()));
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet()      
    .stream()      
    .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

但开发者可能不愿意这样做,因为写出中间变量的类型过于繁琐。使用 var 可以让我们更自然地实现这个过程,不必显式声明中间变量类型,代码十分简洁:

var freqMap = strings.stream()
      .collect(groupingBy(s -> s, counting()));

var maxEntryOpt = freqMap.entrySet()
      .stream()
      .max(Map.Entry.comparingByValue());

return maxEntryOpt.map(Map.Entry::getKey);

在某些情况下,你可能会更喜欢第一段代码,因为它只有一长串的方法调用;但是在某些情况下,拆分长的方法链可能更好。在第二段代码中,完整声明中间变量会让拆分长链变得很繁琐,可读性没有多大提高,在这些情况下使用 var 就是一个好的实践。与许多其他情况一样,正确使用 var 可能既会让代码丢失一些信息(显式类型),也会要求我们通过其他方式补充一些信息(采用更好的变量名,更好的代码结构)。

5. 谨慎处理泛型和字面值

涉及到泛型的变量声明,除非在初始化时指定了类型,否则泛型类型会回退到Object,例如:

// 原始代码
PriorityQueue<Item> itemQueue = new PriorityQueue<>();

// OK:都声明了类型为PriorityQueue<Item>的变量
var itemQueue = new PriorityQueue<Item>();

// 危险:推断出类型为PriorityQueue<Object>
var itemQueue = new PriorityQueue<>();

使用字面值初始化时也要注意,对于boolan、char、long、String这种类型的字面值没有任何问题,因为类型推断是明确的,对于整数字面值var只会推断为int,这可能导致不符合预期:

// 原始代码
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";

// OK:可以正常声明
var ready = true;
var ch = '\ufffd';
var sum = 0L;
var label = "wombat";

// 原始代码
byte flags = 0;
short mask = 0x7fff;
long base = 17;

// 危险: 变量都会被声明为int
var flags = 0;
var mask = 0x7fff;
var base = 17;

使用 var 来声明变量可以通过减少杂乱的代码,使更重要的信息更加突出,从而改进代码。但另一方面,盲目地应用 var 可能会让代码变得更糟,希望以上的代码原则和实践建议,可以帮你构建更加良好的java代码。

3.2 Records - java的元组类型

Record 基本用法

很多语言都可以方便地定义一个不可变的复合类型值,例如定义一个二维平面的点,不同语言的代码如下:

// Python tuple
point = (3, 4)
x = point[0]  # x = 3
y = point[1]  # y = 4

// Swift Tuple
let point = (x: 3, y: 4)
print(point.x) // 输出 3
print(point.y) // 输出 4

// C# ValueTuple
(int x, int y) point = (3, 4);
Console.WriteLine($"Point.X: {point.x}, Point.Y: {point.y}");

反观java,定义这样的类型只能通过class,而编写这样的类涉及大量低价值、重复、易出错的代码:构造函数、访问器、equals、hashCode、toString等等,代码可能如下:

class Point {
    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    int x() { return x; }
    int y() { return y; }

    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point other = (Point) o;
        return other.x == x && other.y == y;
    }

    public int hashCode() {
        return Objects.hash(x, y);
    }

    public String toString() {
        return String.format("Point[x=%d, y=%d]", x, y);
    }
}

虽然IDE可以帮助我们编写大多数代码,但没法帮助我们提高这个类的可读性。

Record 就是基于这个背景引入到java的“元组”类型,它是Java语言中的一种新的class类型,本质上还是一个class,但在对数据聚合进行建模时比一般的class更简单,如上面的例子,Point的定义和用法如下:

record Point(int x, int y) { }
var point = new Point(3, 4);
System.out.println("Point.x = " + point.x);
System.out.println("Point.y = " + point.y);
System.out.println(point.equals(new Point(3, 4)));
System.out.println(point);

程序输出如下:
Point.x = 3
Point.y = 4
true
Point[x=3, y=4]

一个Record类型的声明,内部会自动包含许多class的标准成员:

  1. 对声明record类的每个参数,都添加了对应的字段和public访问方法;

  2. 一个规范化构造函数;

  3. equals 和 hashCode 方法,确保在组成值都相等时两个record是相等的;

  4. 一个 toString 方法,它返回record的标准。

使用record定义局部数据类型来辅助完成代码中的数据和逻辑处理,可以显著提高代码效率。例如下面这个例子,用局部record类MerchantSales模拟了商家和月销售额的聚合,代码实现起来非常简单:

List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
    // Local record
    record MerchantSales(Merchant merchant, double sales) {}

    return merchants.stream()
        .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
        .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
        .map(MerchantSales::merchant)
        .collect(toList());
}

Record 构造函数

如果要重载record类的构造函数,可以用跟class一样的方式来显示声明,例如:

Point(int x, int y) {    
    this.x = x;    
    this.y = y;
}

也可以使用紧凑声明,即省略形式参数列表。在这种紧凑声明的构造函数中,参数会隐式声明,并且与record对应的私有字段不必在函数体内分配,构造函数末尾会自动添加分配给相应的形式参数的代码(this.x = x;)。这种紧凑声明有助于开发者集中于验证和规范化参数,而不必写赋值的重复代码。

例如,在构造函数中做参数校验的代码:

record Range(int lo, int hi) {    
    Range {        
        if (lo > hi)              
            throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));    
        }
}

例如,在构造函数中规范参数值:

record Rational(int num, int denom) {    
    Rational {        
        int gcd = gcd(num, denom);        
        num /= gcd;        
        denom /= gcd;    
    }
}

这段代码等同于

record Rational(int num, int denom) {    
    Rational(int num, int demon) {        
        // 规范参数        
        int gcd = gcd(num, denom);        
        num /= gcd;        
        denom /= gcd;        
        // 赋值        
        this.num = num;        
        this.denom = denom;    
    }
}

Record 和一般 Class的差异

相较于普通类,record类的声明有许多限制:

  • record类不能继承,并且超类始终为 java.lang.Record,类似于枚举类的超类始终为 java.lang.Enum。

  • record类是 final 的,不能是抽象类,这些限制保证了record类的 API 跟类型定义保持一致,不能通过另一个类修改。

  • 从record声明中派生的字段是 final 的,此限制体现了一个默认情况下record的值应该是不可变的。

  • record类中不能声明新的实例字段,也不能包含实例字段初始化方法。这些限制确保record的声明跟record值的一致性。

  • 如果一个成员本来应该被自动派生,但是被显式声明了,那么这个显式声明必须与自动派生的成员的类型完全匹配。如果需要实现访问器(accessors)、equals方法或hashCode方法,那么需要注意不要影响record默认的语义。

  • record类不能声明本地方法(native methods)。

除了以上限制,record类的行为类似于普通类:

  • 使用 new 表达式创建record类的实例。

  • record类可以被声明为顶层类(独立的java文件)或嵌套类,并且可以是泛型的。

  • record类可以声明静态方法、字段和初始化器。

  • record类可以声明实例方法。

  • record类可以实现接口。

  • record类可以声明嵌套类型,包括嵌套record类。

  • record类以及其声明字段可以用注释进行修饰。任何作用于record上的注释都会根据注释适用的目标集合传播到自动派生的字段、方法和构造函数参数中,在record声明字段的类型上的类型注释也会传播到自动派生成员的相应类型使用中。

  • record类的实例可以进行序列化和反序列化。但是,无法通过提供writeObject、readObject、readObjectNoData、writeExternal或readExternal方法来自定义此过程。

Record 使用建议

Records 特性是我非常喜爱的特性,由于我的编程习惯遵循了一些函数式编程的思想,为了提高代码的质量和可维护性,我习惯在代码中尽量避免使用变量。在Java中Class的语义是有状态的对象类型,很多时候我只想把Class定义为常量类型,类似于Java 8引入的LocalDate、Duration等时间相关的类。然而将Class定义为常量类型常常需要额外考虑很多逻辑,诸如如何处理set方法、字段类型是否本身可变、如何初始化、hashCode和equals方法处理等等。

Record 类型无论从语义到使用,都完美符合了我对值类型的需求,将变量声明为Record 类型,语义上也相当于这个变量应该被理解为常量。希望你也能从 Record 类型使用中受益,让代码变得更加简洁和优雅。

3.3 Sealed Classes - 定义类的继承权限

Sealed Classes 基本用法

如果你想限制一个基类的继承权限,除了final class这种严格禁止继承的方式,还可以用sealed class来指定这个基类只能被哪些类所继承。举例来说,Shape这个类只允许三个子类有继承权限,可以用sealed class来定义类,再用permits来指定有权限继承的类:

public abstract sealed class Shape 
    permits Circle, Rectangle, Square { ... }

在sealed class文件内定义的类,默认也是有继承权限的,例如:

abstract sealed class Root { ... 
    final class A extends Root { ... }
    final class B extends Root { ... }
    final class C extends Root { ... }
}

Sealed Classes的使用遵循以下几个规则:

  1. 匿名类、局部类(local classes)以及不符合驼峰式命名的类,都不能作为permits的对象;
  2. sealed class和被允许子类都必须在同一模块;被允许的子类必须直接继承sealed class;
  3. 被允许的子类必须用final、sealed、non-sealed来声明(non-sealed是java的第一个连接符关键字),例如:
abstract sealed class Root { ... 
    final class A extends Root { ... }
    final class B extends Root { ... }
    final class C extends Root { ... }
}

Sealed Interfaces

        除了class外,关键字sealed 也可以用于interface,例如:

public sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { ... }

public final class ConstantExpr implements Expr { ... }
public final class PlusExpr     implements Expr { ... }
public final class TimesExpr    implements Expr { ... }
public final class NegExpr      implements Expr { ... }

sealed interface 也可以permits record类型,例如:

public sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { ... }

public record ConstantExpr(int i)       implements Expr { ... }
public record PlusExpr(Expr a, Expr b)  implements Expr { ... }
public record TimesExpr(Expr a, Expr b) implements Expr { ... }
public record NegExpr(Expr e)           implements Expr { ... }

对我们服务端程序的日常开发来说,sealed interfaces是很有意义的。特别是开发者在编写API时,经常会定义一个interface,然后再用一个唯一的impl class实现这个interface,使用sealed来定义可以精准地表达这类设计模式,例如:

public sealed interface UserService permits UserServiceImpl { ... }
public final class UserServiceImpl implements UserService { ... }

Sealed Classes 内部细节和反射

我们再来看一下JVM是如何支持sealed classes的,JVM会在运行时识别sealed classes和 interfaces,并动态地防止未经授权的子类和子接口扩展。

尽管sealed是一个类修饰符,但在ClassFile结构中并没有ACC_SEALED标志。sealed classes的类文件具有PermittedSubclasses属性,它隐式表示sealed修饰符并明确指定允许的子类:

PermittedSubclasses_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_classes;
    u2 classes[number_of_classes];
}

当JVM试图定义一个其超类或超接口具有PermittedSubclasses属性的类时,正在定义的类必须由该属性命名。否则将抛出IncompatibleClassChangeError异常。

为了支持通过反射获取sealed信息,java.lang.Class添加了两个方法:

  • Class<?>[] getPermittedSubclasses() 返回所有授权的子类,如果类不是sealed则返回空

  • boolean isSealed() 返回类是否是sealed classes或 interfaces

通过sealed classes,我们可以对类或接口的扩展权限进行更精细化的控制。如果结合新的switch表达式和模式匹配一起使用,sealed classes还可以让普通类具备类似枚举类的条件匹配能力,这一特性将会在下一篇继续介绍。

3.4 Value-based Classes

Value-based Classes是诸如java.lang.Integer和java.time.LocalDate这样一些class,具体来说,Value-based Classes有以下特点:

  • 该类仅声明了final实例字段(尽管这些字段可能包含对可变对象的引用);

  • 该类的equals、hashCode和toString方法仅根据类的实例字段的值(以及它们引用的对象的成员)计算它们的结果,而不是根据实例的身份;

  • 该类的方法在实例相等时可自由互换,也就是说,如果根据equals()方法判断相等的任意两个实例x和y互换,不会对该类的方法行为产生可见的变化;

  • 该类不使用实例的monitor进行同步;

  • 该类没有声明(或已弃用)任何可访问的构造函数;

  • 该类不提供任何在每次方法调用时都保证唯一身份的实例创建机制,特别是任何工厂方法的约定必须允许这样一种可能性:如果根据equals()方法判断相等的两个独立生成的实例,它们用==来比较也可能相等;

  • 该类是final的,并且继承自Object或仅声明了实例字段或实例初始化程序的抽象类层级结构,其构造函数为空。

Value-based Classes会以ValueBased annotation来标记,主要包括如Integer、Character等基础类型class、日期类型class、Optional和KeyValueHolder等class。一些构造方法如new Integer、new Double,已经被标记为Deprecated,现有代码中使用了值对象类构造方法的地方需要进行重构;使用值对象类的对象锁进行同步的代码也会在编译期和执行时进行警告,例如:

Double d = 20.0;
synchronized (d) { ... } // javac warning & HotSpot warning
Object o = d;
synchronized (o) { ... } // HotSpot warning

值对象一般是基础类型对象和不可变对象,在程序开发中使用场景非常广泛,将这些对象归为值对象类处理,实际上是对Java编程模型的一次重大强化,这些类声明它们的实例是无身份的,并且能够进行内联或扁平化表示,实例可以在内存位置之间自由复制,并且仅使用实例字段的值进行编码。这些优势带来了更好的性能、可靠的相等语义以及原始类型和包装类的统一,因此对比由此带来的少量的重构成本,这是一个很值得推进的特性。

对于我们开发者来说,我们需要注意编译器的警告,即时重构值对象的过期用法。

3.5 Hidden Classes

Hidden Classes是一种新的class类型,这种类有几个特点:1、动态生成,它是在运行期才会被实例化的类;2、不可见,Hidden Classes对classloader(Class::forName、ClassLoader::loadClass)不可见;3、生命周期,隐藏类可以在不再可达时被卸载,或者可以与类加载器的生命周期共享,以便仅在类加载器被垃圾回收时卸载。

Hidden Classes通过MethodHandles.Lookup::defineHiddenClass方法创建,传入的字节码byte数组必须是class文件结构,实例代码可以参考一个git项目,使用Hidden Class创建动态代理:github.com/forax/hidde…

Hidden Classes非常适合一些框架,用于动态生成仅对内部可见并希望及时卸载的内部类,例如java.lang.reflect.Proxy定义隐藏类作为实现代理接口的代理类、java.lang.invoke.StringConcatFactory生成隐藏类来保存常量拼接方法、java.lang.invoke.LambdaMetaFactory 生成隐藏的嵌套类来保存lambda。

JDK中的最佳案例就是lambda的生成,在过去的版本中,lambda的动态生成使用的是Unsafe::defineAnonymousClass,Unsafe是一个不推荐的API,如今lambda实现已经替换为了隐藏类。

3.6 其他性能优化

基于嵌套的访问权限控制

Java 11中引入了嵌套(nests)的概念,它是与Java编程语言中现有的嵌套类型概念相对应的访问控制上下文。在之前的版本中,一个类去访问它内部定义的子类的private成员时,由于这两个类会被编译为不同的class文件,编译器是通过将子类成员的private级别上升为包级别,再通过插入扩展访问性的桥接方法来实现的。这种实现方式既破坏了语言的封装性,又增加了一点程序包体大小,并且无法支持反射,导致程序执行行为逻辑不一致。

嵌套类型是一种新的关系类型,适用于类和接口,这些嵌套类型之间具有无限制的访问权限,包括对私有字段、方法和构造函数的访问。我们可以将顶层类型以及其内部嵌套的所有类型描述为一个嵌套组,而嵌套组中的两个成员被描述为嵌套成员。私有访问在包含顶层类型的整个声明范围内是不受限的,可以将其视为顶层类型定义了一种类似于“迷你包”内的访问权限。

Java也增加了描述嵌套关系的几个反射接口:java.lang.Class: getNestHost 返回嵌套顶层类型, getNestMembers 返回嵌套成员类型, 和 isNestmateOf 判断是否为嵌套关系。

在Java18中,Method Handles替代了原有的核心反射机制实现方案,成为了java平台的通用底层反射机制。

原本的核心反射在调用方法和构造函数时有两种内部机制:为了实现快速启动,它使用HotSpot虚拟机中的本地方法来调用特定的反射方法或构造函数对象的前几次调用;为了获得更好的性能峰值,在多次调用之后,它会生成字节码用于反射操作,并在随后的调用中使用该字节码。对于字段访问,核心反射使用内部的sun.misc.Unsafe API。

使用Method Handles代替核心反射实现后,带来以下优势:

  1. 更高的性能:方法句柄比反射对象更高效,可以在访问和调用类的结构和方法时提供更好的性能。

  2. 更好的安全性:方法句柄的使用受到更严格的访问控制,可以提供更好的安全性,防止对私有方法和字段的非法访问。

  3. 更灵活的代码优化:方法句柄可以与即时编译器(Just-In-Time Compiler)更好地集成,从而实现更灵活的代码优化和性能提升。

Dynamic Class-File Constants

Dynamic Class-File Constants(动态类文件常量)是Java虚拟机中引入的一种机制,用于在类文件中支持动态生成的常量。

传统的类文件常量,如字符串常量、整数常量等,是在编译时确定的,其值在类加载时就已经确定并存储在常量池中。而动态类文件常量则允许在运行时通过特定的引导方法(bootstrap method)动态地生成常量的值。

动态类文件常量的作用主要体现在以下几个方面:

  1. 提供更灵活的常量定义:传统的类文件常量的值是静态确定的,无法根据运行时的条件动态生成。而动态类文件常量可以根据特定的逻辑和条件,在运行时动态生成常量的值,从而提供更灵活的常量定义方式。

  2. 扩展语言和编译器的表达能力:动态类文件常量的引入使得语言设计者和编译器实现者可以更广泛地选择表达能力和性能方面的选项。通过动态生成常量,可以在类加载过程中执行复杂的逻辑和计算,从而提供更丰富的语言特性和编译器优化。

  3. 支持动态语言特性:动态类文件常量的机制可以用于支持动态语言特性,如动态类型、动态方法调用等。通过动态生成常量,可以在运行时根据实际情况动态调整类型和行为。

  4. 提高程序性能:动态类文件常量的引入可以将一些在运行时需要进行的计算和逻辑转移到类加载时进行,从而减少运行时的计算和判断,提高程序的性能和响应性。

通过使用动态类文件常量,可以在类加载时动态生成常量的值,从而提供更灵活和高度可控的常量定义方式,增强了Java虚拟机和编程语言的表达能力和性能优化能力。

image.png