11

I have the following class with the following method.

public class MyClass {

    public static <T> T getSomeThing(final int id, final java.lang.reflect.Type type, final Map<Integer, String> someThings) {
        final String aThing = someThings.get(id);
        if (aThing == null || aThing.isEmpty()) {
            return null;
        }
        return GsonHelper.GSON.fromJson(aThing, type);
    }

}

The GsonHelper provides me with some com.google.gson.GsonBuilder

public class GsonHelper {

    public static final com.google.gson.Gson GSON = getGsonBuilder().create();

    public static GsonBuilder getGsonBuilder() {
        return new GsonBuilder().setPrettyPrinting()
                .enableComplexMapKeySerialization()
                .registerTypeAdapter(new com.google.gson.reflect.TypeToken.TypeToken<byte[]>() {
                    // no body
                }.getType(), new Base64TypeAdapter())
                .registerTypeHierarchyAdapter(Date.class, new DateTypeAdapter())
                .registerTypeHierarchyAdapter(Pattern.class, new PatternTypeAdapter())
                .registerTypeAdapterFactory(new ListTypeAdapterFactory())
                .registerTypeAdapterFactory(new MapTypeAdapterFactory())
                .registerTypeAdapterFactory(new SetTypeAdapterFactory());
    }
}

Until Java 7 I was using this method like:

Map<Integer, String> allThings = new HashMap<>();
//FILL allThings with values

if(MyClass.getSomeThing(7, java.lang.Boolean.class, allThings)){
    //do something
}

This worked fine. cause the method will return a Boolean and I can use this inside "if". But when I change to Java 8 this is not possible anymore. The compiler complains about:

Type mismatch: cannot convert from Object to boolean

 //while this is working but would throw a JsonSyntaxException
final String myString = "myInvalidJsonString";
if(GsonHelper.GSON.fromJson(myString, java.lang.Boolean.class)){
    //do something
}

I know that java.lang.Boolean can be null. And I could solve this issue with:

final Boolean b = MyClass.getSomeThing(7, java.lang.Boolean.class, allThings);
if(b){
    //do something
}

But I am curious about why this is working with Java 7 and NOT in Java 8. (not answered)
What did they change? (not answered)
What is the reason for this compiler error when changing to Java 8? (answered)

Naxos84
  • 1,184
  • 1
  • 13
  • 32
  • 3
    Not answering your question, but another workaround is `if(MyClass.getSomeThing(...))`. – shmosel Mar 27 '17 at 05:54
  • 3
    Worked for me. Can you provide more details about the GsonHelper class? – Saket Mehta Mar 27 '17 at 06:02
  • 1
    What minor version of Java are you using? I had similar problems in 1.8.0_45, which was down to a bug in the compiler. This was fixed in later versions (1.8.0_121 being the latest). – Joe C Mar 27 '17 at 06:03
  • @SaketMehta I added some details for GsonHelper but I am not sure wether the problem is there. cause it worked with Java 7 – Naxos84 Mar 27 '17 at 06:14
  • @JoeC I am using jdk1.8.0_121 – Naxos84 Mar 27 '17 at 06:14
  • I added an additional check that I am using to the used method. – Naxos84 Mar 27 '17 at 06:18
  • 1
    Your code contains several trivial syntax errors that makes it impossible to compile under any Java version, like a forgotten parameter name, using different names for the same variable, or no `()` after `isEmpty`, which is supposed to be a method invocation. If you fix them, this code compiles under all Java 8 jdks from Oracle. If you are using Eclipse, you should learn that it has its own compiler that is independent from the jdk’s. – Holger Mar 27 '17 at 11:01
  • @Holger thx for the hint with the missing "()" fixed it. where am I forgetting a parameter name?. I just wrote this inside Stack overflow and not in any IDE. so sorry for those errors. – Naxos84 Mar 27 '17 at 11:05
  • 1
    Well, as said, [compiles fine under Java 8](http://ideone.com/zwXYvz), and I tested almost every JDK version. So this is merely an Eclipse issue. – Holger Mar 27 '17 at 11:30
  • It really seems to be an Eclipse issue. Cause maven can compile this and IDEA can compile it too. – Naxos84 Mar 27 '17 at 13:19
  • @Holger, can you help me? In my understanding, T could be easily inferred to Boolean iff the expression would appear in an assignment context or an invocation context targeting Boolean. However, I see no indication in JLS that the condition of an if expression forms either of these contexts. Shouldn't the expression thus be treated as a standalone expression, causing T to be inferred as j.l.Object? – Stephan Herrmann Mar 27 '17 at 22:25
  • 1
    @Stephan Herrmann: well, I was surprised that this works, too. I just thought, since it works for all JDKs of Java 7 and Java 8 and even Eclipse with Java 7 compliance (at least the version I tried), there must be something. On the other hand, everyone agrees that the `return` statement provides an appropriate target type context, but I couldn’t find the related definition in the specification either. It could be so much simpler, if just every context provided a target type… – Holger Mar 28 '17 at 08:07
  • Thanks, @Holger. Looks like we should ping Oracle on https://bugs.openjdk.java.net/browse/JDK-8030361 - but "every context" would be very tricky: what's the target type for the receiver of an invocation? – Stephan Herrmann Mar 28 '17 at 11:26
  • @Stephan Herrmann: well, if the upper bound or erasure of the receiver type allows to find an unambiguous candidate method, it could allow inferring the actual receiver type in turn, though, admittedly, the chained invocation issue is the most complex one, which I hadn’t in mind when saying “every context”. – Holger Mar 28 '17 at 13:04
  • @Holger, I was ready to believe there was an accidental omission in JLS, but Dan Smith (via private email) calls this a compiler bug -- he is *not* going to change JLS as to admit target typing in `if` conditions and related positions. – Stephan Herrmann Apr 20 '17 at 21:00

2 Answers2

6

Your method public static <T> T getSomeThing(final int id, final java.lang.reflect.Type t, final Map<Integer, String> someThings) does not guarantee to return a Boolean. It returns T which is defined by the caller and could be anything, which means Object.

The if statement can't know which type T will have and hence can't guarantee to convert it to a boolean.

Why not change the signature to boolean?

public static boolean getSomeThing(final int id,
                                   final java.lang.reflect.Type t,
                                   final Map<Integer, String> someThings)

Or are you in search of this?

public static <T> T getSomeThing(final int id,
                                 final Class<T> clazz,
                                 final Map<Integer, String> someThings)

Than this code will compile and work:

public static void main(String[] args) {
    if (getSomeThing(7, Boolean.class, emptyMap())) {
        System.out.println("It works!");
    }
}

public static <T> T getSomeThing(final int id,
                                 final Class<T> clazz,
                                 final Map<Integer, String> someThings) {
    ...
}
Harmlezz
  • 7,524
  • 23
  • 34
  • Correct. the method "getSomeThing" can also return something else. i.e. String --> MyClass.getSomeThing(7, String.class, allThings); So changing the signature is not an option. The second advice is an option i will look into. But why did my code work in java 7 and when i change to java 8 it is complaining? – Naxos84 Mar 27 '17 at 08:45
  • 1
    That's the reason why `if (MyClass.getSomeThing(...))` does not compile. The compiler can't convert anything (which means `Object`) into a `boolean`. If you change the signature as mentioned in my post at the bottom, it will work as long as the compiler has the context of `T` being a `Boolean`. – Harmlezz Mar 27 '17 at 08:47
  • 1
    I am not sure why. Perhaps (it is a vague guess of mine) the *if-statement* provided the type context (type inference) for `T` to be `Boolean`. I am not sure and too lazy to install *Java 7* to verify it myself :-) – Harmlezz Mar 27 '17 at 09:11
  • I think I will go for the "Class" option. Thank you. – Naxos84 Mar 27 '17 at 13:20
2

The last version where the result of getSomeThing() is assigned to a Boolean variable can be inferred according to JLS 8, because in an assignment context the target type Boolean lets T to be inferred as Boolean, indeed. Here all compilers agree.

Regarding the original case, JLS 8 does not classify the condition of an if-statement as an assignment context. Outside assignment or invocation contexts, the invocation is not treated as a poly expression, but as a standalone expression (JLS 15.12 1st bullet). Standalone expressions have no target type. Without a target type inference in Java 8 falls back to inferring T to Object.

The Eclipse team has requested clarification in this regard even before Java 8 GA. Unfortunately, the resulting issue remains unresolved until today.

Ergo: JLS and the observed behavior of javac don't seem to agree. Likely, JLS is the entity that should be fixed.

Update: JLS is not going to change (confirmed via private email), hence accepting the program is a bug in javac.

Edit: Javac version 12 will propagate this bug even through a switch expression:

public class X {
    @SuppressWarnings("preview")
    public void foo(int i) {
        if (switch(i) { default -> magic(); })
            System.out.println("true");
    }
    <T> T magic() { return null; }
}

javac accepts this due to type inference with target type Boolean, but performing type inference in this location is illegal per JLS.

Stephan Herrmann
  • 7,168
  • 1
  • 22
  • 33