为什么`Class`类是final的?

5
在Stack Overflow回答一个问题时,我想到了一种解决方案,如果可以扩展Class类的话,这种解决方法会很好用。
这个解决方案的核心是尝试装饰Class类,以便只允许包含某些特定值,即继承具体类C的类。
public class CextenderClass extends Class
{
    public CextenderClass (Class c) throws Exception
    {
        if(!C.class.isAssignableFrom(c)) //Check whether is `C` sub-class
            throw new Exception("The given class is not extending C");
        value = c;
    }

    private Class value;

    ... Here, methods delegation ...
}

我知道这段代码不起作用,因为 Class 是 final 的,我想知道为什么 Class 被设为了 final。我知道这可能与安全有关,但是我无法想象出扩展 Class 可能会有危险的情况。您可以提供一些例子吗?

顺便说一下,我能够达到期望行为的最接近解决方案是:

public class CextenderClass
{
    public CextenderClass(Class c) throws Exception
    {
        if(!C.class.isAssignableFrom(c)) //Check whether is `C` sub-class
            throw new Exception("The given class is not extending C");
        value = c;
    }

    public Class getValue() {
        return value;
    }

    private Class value;

}

但是它缺乏透明装饰的美感。

不可更改的可能,:/ - Afzaal Ahmad Zeeshan
虽然我认为被接受的答案是完全正确的,但我也可能提一下最终类可以更好地由JVM进行优化,因此许多常见的类都是final的。我想这就是String被定义为final的原因之一(同时可变字符串还会允许更多的错误和Java始终致力于避免错误)。 - Bill K
2个回答

5
根据Class类中的注释只有Java虚拟机才能创建Class对象。

如果允许扩展类,则会出现问题,您是否应该被允许创建自定义Class对象? 如果是,则会违反上述仅由JVM创建Class对象的规则。因此,您不具备灵活性。

PS:getClass()仅返回已创建的class object。它不会返回一个new class object。


听起来很合理,但如果您字面上遵循这个规则,那么当您能够创建一个扩展Class的类的对象时,该规则仍然是正确的。 - Pablo Francisco Pérez Hidalgo
@PabloFranciscoPérezHidalgo - 没错,那么你会如何处理自定义类呢?你会尝试实例化它们,对吧?(去掉final关键字就可以这样做了..)想象一下如果这样做会发生什么..同样的情况也适用于String类(它也是final)..如果我们被允许扩展这些类,那么我们肯定会尝试改变JVM内部的工作方式。 - TheLostMind

0

这是一个非常好的问题,但只是更一般问题的一个实例。虽然@TheLostMind在4年前已经回答了这个问题(从实现的角度考虑),但从JVM规则/限制的角度来看,问题仍然存在:为什么JVM提出这些规则?必须有一个合理的原因。我们必须从更抽象的层面(观点)来考虑它。

简短回答:
可能所有这些都与类型安全有关,正如我们所知,Java是一种强类型语言,不允许任何人改变这个事实。

详细回答(带一些上下文以便于大家理解): 整个故事始于静态和动态绑定子类型多态性所赋予的灵活性使得对象的声明(静态)类型通常与其运行时(动态)类型不同。 运行时类型通常是静态类型的子类型。 例如:

AClass a = new AClass();
AsubClass b = new AsubClass(); //  AsubClass is derived from AClass
a=b;

a的静态类型是AClass,在赋值a=b; 后它的运行时类型变成了AsubClass。这对于执行消息时选择最合适的方法有影响。

现在考虑下面给出的Vehicle类。

public class Vehicle {
  private int VIN; // Vehicle Identification Number
  private String make;

  public boolean equals(Object x) {
    return (VIN == (Vehicle)x.VIN);
  }
  // other methods
}

根类java.lang.Object中的equals方法被定义为对象标识测试。

这是一般情况下定义对象相等的唯一有意义的方式。也就是说,如果两个对象具有相同的标识,则它们是相等的。

在特定的类中,更适合的相等含义可能更为恰当。在上述类中,如果两辆车的VIN(车辆识别号码)相等,则认为它们是相等的。

因此,在类Vehicle中,方法equals相应地被重新定义。这个继承方法的重定义称为覆盖。 请注意,子类中继承的方法参数的签名需要保持不变,根据function subtyping rule。 这会创建一个尴尬的情况,因为在类Vehicle中,我们想引用参数的VIN字段,而Object没有这样的字段。这就是为什么类型转换(Vehicle)x指定意图将x视为Vehicle。没有办法静态验证这个转换,因此编译器会生成一个动态检查。这是dynamic type checking.的一个例子。

为了使覆盖正常工作,要调用的方法是由接收对象的动态类型确定的(也称为dynamic dispatch (selection) of methods,是面向对象语言中最重要的动态绑定案例)。 例如:

Object a = new Object();
Object b = new Object();
Vehicle aV = new Vehicle();
Vehicle bV = new Vehicle();
a=aV; 
b=bV;
. . . 
a.equals(b)
. . .

响应消息a.equals(b)的调用方法将是在类Vehicle中重写的equals方法,因为a的运行时类型是Vehicle。

有些情况下,重写方法可能会有问题,不应该允许。一个很好的例子是Java.lang.Object的getClass()方法。这个方法在底层虚拟平台中有一个特定的实现,保证调用此方法将确实返回方法接收者的类对象。 允许重写将对该方法的预期语义产生严重影响,在动态类型检查中创建非常规问题。这可能就是为什么getClass()被声明为final的原因。

public class Object {
  public final Class getClass();
  ....
}

最后,Java中的类Class是final的,即不能被扩展,因此它的所有方法都不能被覆盖。由于类Class只有内省方法,这保证了运行时类型系统的安全性,即类型信息不能在运行时改变。

再深入一点...

基于接收对象类型的动态调度(选择)方法是面向对象语言中的基本技术。它带来了使整个面向对象范例工作的灵活性。

通过继承将新类型添加到已编译和运行的应用程序中,只需要编译和链接新引入的类型,而无需重新编译现有应用程序。然而,这种灵活性会带来一些效率上的惩罚,因为方法选择的决定被推迟到运行时。现代语言具有有效的动态方法调度技术,但某些语言如C++和C#则通过提供静态绑定(方法选择)选项来避免相关成本。在C#中,除非明确声明为虚拟的,否则方法是静态绑定的。

public class Object {
  public virtual boolean equals(Object x);
  // other methods
}

在C#中覆盖此方法将使用显式关键字override指示。 例如。
public class Vehicle {
  private int VIN;
  private String make;

  public override boolean equals(Object x) {
    return (VIN == (Vehicle)x.VIN);
  }
  // other methods
}

接收器为类对象的方法始终静态绑定。原因是该类的所有对象只有一个类对象。由于在编译时已知接收器,因此无需将方法选择推迟到运行时。这些方法因此被声明为静态,以表示它们属于类本身。 一个例子是Vehicle类的numberOfVehicles方法。车辆数量不是单个车辆对象的属性。它是Vehicle类的所有对象的属性,因此它属于类本身。 例如:

public class Vehicle {
  // fields;
  public static int numberOfVehicles();
  // other methods
}

我们可以将以上讨论总结如下:
- 面向对象语言中选择执行消息的方法(方法调度)的基本机制是动态的。它基于接收器对象的运行时类型。 - 静态(即类)方法的接收器是类对象。由于给定类型只有一个类对象,因此选择静态方法是静态的。 - 一些语言(如C++和C#)允许选择静态或动态方法调度。虽然这样做是出于效率的考虑,但已经证明,当程序中同时使用两种调度机制时,可能会使程序的含义变得模糊。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接