为什么这段代码在Java 1.6中编译通过而在Java 1.7中却不行?

16
以下代码在Java 1.6中编译良好,但在Java 1.7中无法编译。为什么?
代码的相关部分是对私有“data”字段的引用。该引用来自于同一类中定义该字段的位置,因此似乎是合法的。但是它是通过一个泛型类型的变量发生的。这段代码是基于内部库中的一个类的简化示例,在Java 1.6中可以工作,但在Java 1.7中不行。
我不是在问如何解决这个问题。我已经做到了。我试图找到为什么这不再起作用的解释。有三种可能性:
1. 根据JLS,这段代码是不合法的,不应该编译(1.6编译器存在错误,在1.7中得到修复);
2. 根据JLS,这段代码是合法的,应该编译(1.7编译器引入了向后兼容性问题);
3. 这段代码属于JLS的灰色地带。
Foo.java:
import java.util.TreeMap;
import java.util.Map;

public abstract class Foo<V extends Foo<V>> {

    private final Map<String,Object> data = new TreeMap<String,Object>();

    protected Foo() { ; }

    // Subclasses should implement this as 'return this;'
    public abstract V getThis();

    // Subclasses should implement this as 'return new SubclassOfFoo();'
    public abstract V getEmpty();

    // ... more methods here ...

    public V copy() {
        V x = getEmpty();
        x.data.clear();      // Won't compile in Java 1.7
        x.data.putAll(data); // "
        return x;
    }

}

编译器输出:

> c:\tools\jdk1.6.0_11\bin\javac -version
javac 1.6.0_11

> c:\tools\jdk1.6.0_11\bin\javac c:\temp\Foo.java

> c:\tools\jdk1.7.0_10\bin\javac -version
javac 1.7.0_10

> c:\tools\jdk1.7.0_10\bin\javac c:\temp\Foo.java
Foo.java:18: error: data has private access in Foo
        x.data.clear();
         ^
Foo.java:19: error: data has private access in Foo
        x.data.putAll(data);
         ^
2 errors

补充说明:如果引用的是私有方法而不是私有成员变量,就会出现同样的问题。这在Java 1.6中有效,但在1.7中无效。

Foo2.java:

import java.util.TreeMap;
import java.util.Map;

public abstract class Foo2<V extends Foo2<V>> {

    private final Map<String,Object> data = new TreeMap<String,Object>();

    protected Foo2() { ; }

    // Subclasses should implement this as 'return this;'
    public abstract V getThis();

    // Subclasses should implement this as 'return new SubclassOfFoo();'
    public abstract V getEmpty();

    // ... more methods here ...

    public V copy() {
        V x = getEmpty();
        x.theData().clear();      // Won't compile in Java 1.7
        x.theData().putAll(data); // "
        return x;
    }

    private Map<String,Object> theData() {
        return data;
    }

}

编译器输出:

> c:\tools\jdk1.6.0_11\bin\javac c:\temp\Foo2.java

> c:\tools\jdk1.7.0_10\bin\javac c:\temp\Foo2.java
Foo2.java:18: error: theData() has private access in Foo2
        x.theData().clear();
         ^
Foo2.java:19: error: theData() has private access in Foo2
        x.theData().putAll(data);
         ^

我建议反编译生成的两个类文件,然后差异应该很明显。 - Landei
@Landei 在1.7的情况下没有生成的类文件,因为编译器拒绝编译它。 - Paŭlo Ebermann
1个回答

18
所展示的问题似乎与Oracle bug 6904536中报告的行为相符。该错误已被关闭,原因是“不是问题”,并附有以下解释:

javac的行为符合JLS的规定。还请参阅65585516711619以及相关的JLS问题6644562

相应的JLS问题尚未解决,以下是相关评论:

A simplified explanation for the membership of type variables is welcome. There is a general difficulty with private members of a type variable's bounds. Formally such members do not become members of the type variable itself, though javac and Eclipse traditionally made them members and code has come to rely on that:

class Test {
  private int count = 0;
  <Z extends Test> void m(Z z) {
    count = z.count;  // Legal in javac 1.6, illegal in javac 1.7 due to fix for 6711619
  }
}

Peter submitted a similar test:

class A {
  static class B { private String f; }

  abstract static class Builder<T extends B> {
    abstract T getB();

    {
      ((B)getB()).f.hashCode();
      getB().f.hashCode(); // error: f has private access in A.B
    }

  }
}

Since intersection types are constructed by inheritance, and private members are never inherited, it is tricky to re-specify intersection types to have private members. Nonetheless, it would be the compatible thing to do.

供参考,JLS的相关部分是§4.4

编辑:

实际上,我倾向于同意JLS的观点,因为当我们从泛型中移除时,它与自身相匹配。考虑以下示例:

static class Parent {
    
    private int i;
    
    void m(Child child) {
        i = child.i; //compile error
    }
}

static class Child extends Parent { }
child.i不可见,因为私有成员的访问权限不会被继承。这一点通过以下事实得到了证明:Child可以拥有自己的i,而不会有任何遮蔽的影响。
static class Child extends Parent {
    private int i; //totally fine
}

这将是一个罕见的需要进行向上转型的例子:
void m(Child child) {
    i = ((Parent)child).i;
}

所以,如果继承的可访问性不考虑的话,根据JLS,这里是正确的,因为在Foo<V extends Foo<V>>中的V不一定是Foo<V>,而可以是扩展了Foo<V>的某种类型。

谢谢Paul。是的,答案似乎是JDK 6中的javac编译了某些不符合JLS严格规范的形式,上面的例子就是一个例子。JDK 7中的javac已经修复了这个问题。特别是,通过通用引用访问私有字段/方法,即使在同一类中也不应该被允许。(这很遗憾。我不是JLS专家,但我立刻看不出为什么不应该这样做。)简而言之,JDK 1.6编译器存在一个错误。 - Paul
1
@Paul 没问题,感谢您的有趣帖子。如果您感兴趣,请查看我的编辑。 - Paul Bellora
4
非常好的回答!感谢您进行研究并解释。 - Jesse
1
我很惊讶JDK 1.6版本45中这个问题还没有被修复!难道不应该吗? - Giovanni Botta

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