Java 8 Double curly bracket initialization and name collision

2.3k views Asked by At

The following class has an inner class called Entry. This code will not compile in Java 8 as the compiler assumes the Entry referenced within the double curly brace initializer is of type Map.Entry and not Scope.Entry. This code compiles in previous versions (at least 6 and 7) of the JDK but is broken in JDK 8. My question is "why?" Map.Entry is not imported in this class, so there is no reason for the compiler to assume that the value is of type Map.Entry. Is there some implicit scope being brought in or something for anonymous classes?

Error:

scope/Scope.java:23: error: incompatible types: scope.Scope.Entry cannot be converted to java.util.Map.Entry for (final Entry entry : entries) {
scope/Scope.java:22: error: cannot find symbol put(entry.getName(), entry);

Example Code:

package scope;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

public class Scope {

    public static class Entry<T> {
        public String getName() {
            return "Scope";
        }
    }

    public static void main(String[] args) {
        final Set<Entry> entries = new HashSet<>();

        new HashMap<String, Entry>() {{
            // Why does the Java 8 compiler assume this is a Map.Entry
            // as it is not imported? 
            for (final Entry entry : entries) {
                put(entry.getName(), entry);
            }
        }};
    }
}
2

There are 2 answers

2
Floegipoky On BEST ANSWER

It's definitely not a bug, it's a side-effect of using double-brace initialization.

new HashMap<String, Entry>() {{
    for (final Entry entry : entries) {
        put(entry.getName(), entry);
    }
}};

This type of initialization is basically a clever way to abuse instance initialization blocks. It creates an anonymous subclass of HashMap with an initialization block, and then copies that block into the beginning of its default constructor before calling it. This subclass gives priority to the Entry in the scope of its parent, rather than the scope that it's nested in. This is explained by shadowing.

From 8.1.6. Class Body and Member Declarations

If C itself is a nested class, there may be definitions of the same kind (variable, method, or type) and name as m in enclosing scopes. (The scopes may be blocks, classes, or packages.) In all such cases, the member m declared in or inherited by C shadows (ยง6.4.1) the other definitions of the same kind and name. [emphasis mine]

Here, C is the anonymous inner class declared. Since it inherits from HashMap, java.util.Map.Entry shadows scope.Scope.Entry.

As for why it compiled as you wanted it to with previous versions, I have no idea. This behavior was present in those versions (the docs I referenced are from 7), so it shouldn't have worked. So maybe those versions are bugged.

0
Stanislav Lukyanov On

Scopes of type members and shadowing is a hard place for a compiler. There was/are number of bugs related to this - mostly about nested/inner/anonymous types. I can't find the one that is exactly about that issue but I know some that can be related to it. Here is the one with a case very similiar to this (type variable instead of enclosing type).

Concerning what specification says about shadowing there is also an issue. It has references to JLS and describes what is not ideal.