关于Java编译器愚蠢:Java编译器愚蠢 – 构造函数

Java compiler stupidity - Constructors

本问题已经有最佳答案,请猛点这里访问。

为什么Java编译器(javac)有时是愚蠢的,当然我知道Java编译器只是一个程序,但有时被设计成愚蠢的(有时不是),我也是Java的粉丝。

1
2
3
4
5
6
7
8
9
10
11
class Child extends Base {
    Child() {
        int i = 5; // any statement here.
        super(i);
    }
}

class Base{
    Base(int i) {
    }
}

在这里,编译器声称第一条语句应该是对继承类的构造函数的调用,但是如果我们将第一条语句包装在静态方法中,它就可以正常工作!!!!!!!!

1
2
3
4
5
6
7
8
9
class Child extends Base {
    Child() {
        super(foo()); // works fine!!!!
    }

    static int foo(){
        return 5;
    }
}

这个很好用!!!!!!!!!!,另一个杀手例子:

1
2
3
4
5
    Child() {
        try{
            super(5);
        }catch(Exception e){}
    }

尝试捕捉是语言功能!!!!

我知道编译器强制我们调用超类型构造函数,因为它应该在继承对象之前初始化继承对象(一般通过对象链接释放Java继承),但是我认为编译器应该有点聪明,它应该让我们在调用对象之前不处理对象时操纵代码。每个构造函数,因此必须工作:

1
2
3
4
5
6
    Child() {
        int i = 5; // this MUST BE acceptable since we didn't touch
                   // any current object or inherited field or we didn't
                   // call any method on it.
        super(i);
    }

但这不应该奏效:

1
2
3
4
5
6
7
class Child extends Base {
    private int i;
    Child() {
        i = 6; // this shouldn't work (its clear why).
        super();
    }
}

我只是想知道为什么没有实现这一点,尤其是当我看到Java可以捕获不可访问的代码(智能特征)时????因此,20多年来,Java没有提供这样一个基本特征,因为通常这个代码有时会使代码变得更丑陋,有时我们不得不使用愚蠢的静态方法来避免这种情况,其他时候,我们只调用超级构造函数(JavaCup),然后重新初始化继承的字段!!!!!!!!!!!

尽管我不认为这是JVM和字节码的问题,但我认为这只能在javac中实现:)

我真的很喜欢Java,但是这个让我很生气,我忘了给下一个版本(Java 9)提这个建议,我希望它会被包含在Java 10中,我们等待了3年,比没有它更好。


我认为这主要是基于意见。

在我看来,保持简单有时是更明智的设计方式。

要让编译器接受一些编译器当前不接受的代码,这将(至少)要求更复杂。编译器将有额外的负担来决定允许哪些构造,哪些构造不允许。编译器足够复杂。为什么要增加不必要的复杂性。特别是如果额外的复杂性不能解决或帮助我们解决某个特定的问题。

超类中的构造函数在子类的构造函数中的任何代码之前运行的要求使事情变得简单。

如操作示例所示,声明静态方法不会违反在超类中运行构造函数的规则。静态方法从未实例化。它是类定义的一部分,不是类实例的一部分。

我认为编译器更聪明(处理由OP提出的Java代码)根本不是明智的决定。

问:建议的编译器变更解决了什么实际问题?

问:它可能会产生什么问题?

有时,避免额外复杂性(并可能产生更多问题)的选择是更明智的选择。


两个答案都是完全正确的。恐怕手术室想知道这种行为背后的原因。

  • 每个对象都有一个构造函数
  • 每个对象都继承自对象-所以所有对象都有一个父对象
  • 在每个构造函数中,继承的父对象都被隐式调用(如果不调用super)或显式调用(如果确实调用super)
  • 在对对象执行任何操作之前,必须确保继承的对象已正确初始化。

这就是为什么对super()的调用必须是构造函数中第一个调用的原因,并且在调用super()之前不能执行任何操作。

1
int i = 5;

不起作用,因为您可以进行更复杂的初始化:

1
int i=someMethodCall();

如果允许的话,它显然可以使用任何未初始化的继承字段。

1
2
3
static int foo(){
        return 5;
    }

工作,因为它是一个静态方法——不依赖于对象实例的字段(也看不到字段)——所以可以在super()之前调用它;

1
2
3
4
5
 Child() {
        try{
            super(5);
        }catch(Exception e){}
    }

不起作用,因为不能保证将再调用super()。Try..Catch将允许super(5)引发异常(而不是初始化继承的字段),而捕获和忽略此异常将生成一个子对象,继承的字段根本不初始化。

虽然这种行为听起来很愚蠢-实际上完全有道理,不是吗?


If a constructor body does not begin with an explicit constructor invocation and the constructor being declared is not part of the primordial class Object, then the constructor body implicitly begins with a superclass constructor invocation"super();", an invocation of the constructor of its direct superclass that takes no arguments.

从Java规范,在这里。

他们正在积极执行这一规定。

以下是解析规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
ConstructorBody:
  { [ExplicitConstructorInvocation] [BlockStatements] }

ExplicitConstructorInvocation:
  [TypeArguments] this ( [ArgumentList] ) ;
  [TypeArguments] super ( [ArgumentList] ) ;
  ExpressionName . [TypeArguments] super ( [ArgumentList] ) ;
  Primary . [TypeArguments] super ( [ArgumentList] ) ;

TypeArguments:
  < TypeArgumentList >
ArgumentList:
  Expression {, Expression}


也许这会给你一点启发。不管您做什么,超级类的构造函数总是首先被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Parent {
    Parent(int x) {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    Child () {
        super(foo());
        System.out.println("Child");
    }

    public static void main(String[] args) {
        Child a = new Child();     // prints Parent
Child
        Parent b = new Child();    // prints Parent
Child
    }

    static int foo() {
        return 5;
    }
}

正如您所看到的,无论您做什么,Parent构造函数总是首先被调用。这样可以确保,如果在构造函数中调用Parent类上的任何方法,则已经正确设置了Parent类。如果我试图在调用super()之前移动System.out.println("Child")行,编译器错误就会显示这个属性是严格执行的事实。

来自[JLS 8.8.7.1],

Let C be the class being instantiated, and let S be the direct
superclass of C.

It is a compile-time error if S is not accessible (§6.6).

If a superclass constructor invocation statement is qualified, then:

1
2
3
4
5
If S is not an inner class, or if the declaration of S occurs in a static context, then a compile-time error occurs.

Otherwise, let p be the Primary expression immediately preceding".super". Let O be the innermost lexically enclosing class of S.

It is a compile-time error if the type of p is not O or a subclass of O, or if the type of p is not accessible (§6.6).

If a superclass constructor invocation statement is unqualified, and
if S is an inner member class, then it is a compile-time error if S is
not a member of a lexically enclosing class of C by declaration or
inheritance.