Why does this code compile in Java 1.6 but not in

2019-02-21 07:54发布

问题:

The following code compiles fine in Java 1.6 but fails to compile in Java 1.7. Why?

The relevant part of the code is the reference to the private 'data' field. The reference is from within the same class in which the field is defined, and so seems legal. But it is happening via a generically-typed variable. This code - a stripped down example based on a class from an in-house library - worked in Java 1.6 but doesn't now in Java 1.7.

I'm not asking how to work around this. I've already done that. I'm trying to find an explanation of why this doesn't work any more. Three possibilities come to mind:

  • This code is NOT LEGAL according to the JLS and should never have compiled (there was a bug in the 1.6 compiler, fixed in 1.7)
  • This code is LEGAL according to the JLS and should compile (a backward compatibility bug has been introduced into the 1.7 compiler)
  • This code falls into a GREY AREA in the 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;
    }

}

Compiler output:

> 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

Addendum. The same problem occurs if the reference is to a private method instead of a private member variable. This works in Java 1.6 but not in 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;
    }

}

Compiler output:

> 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);
         ^

回答1:

The demonstrated issue seems to match the behavior reported in Oracle bug 6904536. The bug was closed as "Not an Issue" with the following explanation:

javac is behaving according to the JLS. See also 6558551, 6711619 and related JLS issue 6644562.

The corresponding JLS issue is unresolved, with the following comment:

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.

For reference, the relevant section of the JLS is §4.4.

EDIT:

I tend to agree with the JLS here actually, because it matches up with itself when we remove generics from the picture. Consider this example:

static class Parent {

    private int i;

    void m(Child child) {
        i = child.i; //compile error
    }
}

static class Child extends Parent { }

child.i isn't visible because access to private members isn't inherited. This point is driven home by the fact that Child can have its own i without any shadowing:

static class Child extends Parent {
    private int i; //totally fine
}

So this would be a rare example of upcasting being necessary:

void m(Child child) {
    i = ((Parent)child).i;
}

So with inherited accessibility out of the picture, the JLS seems correct here, given that the V in Foo<V extends Foo<V>> isn't necessarily Foo<V> but could be some type that extends Foo<V>.