Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Java generics type inference break in chained method calls?

Looking at the type inference for generic types in the following example, I cannot say why methodAutoTypeInference works fine, but methodNotCompilable (which is almost the same) fails to compile and to manage it the compiler requires additional tricks like methodWorkaroundTypeHint or methodWorkaroundTypeCast.

What is the problem with methodNotCompilable causing the compiler to be unsure that the expression type and the method result type are compatible?

Stream<CharSequence> methodAutoTypeInference() {
  return Stream.of("a");
}

Stream<CharSequence> methodNotCompilable() {
  return Stream.of("a").distinct(); 
  //    incompatible types:  java.util.stream.Stream<java.lang.String>
  // cannot be converted to  java.util.stream.Stream<java.lang.CharSequence>
}

Stream<CharSequence> methodWorkaroundTypeHint() {
  return Stream.<CharSequence>of("a").distinct();
}

Stream<CharSequence> methodWorkaroundTypeCast() {
  return Stream.of((CharSequence) "a").distinct();
}
like image 495
diziaq Avatar asked Feb 07 '21 15:02

diziaq


2 Answers

This answer from JDK Developers themselves sort of covers the same area. Just notice that Stuart Marks says : "It might be possible for the compiler to be enhanced to cover this case in a future release". Though the case there is around lambdas, this is not very different than you have. It's just the way compiler (at the moment) works. And we are "stuck" with this.

You can sort of look under the resolution of how a compiler thinks about return Stream.of("a").distinct(); and decides what type to use, via:

javac --debug=verboseResolution=all

which in an un-documented flag. If you compile with that flag, you will see some big output:

  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: Object()
DeleteMe.java:60: Note: resolving method of in type Stream to candidate 1
        return Stream.of("a").distinct();
                     ^
  phase: BASIC
  with actuals: String
  with type-args: no arguments
  candidates:
      #0 not applicable method found: <T#1>of(T#1...)
        (cannot infer type-variable(s) T#1
          (argument mismatch; String cannot be converted to T#1[]))
      #1 applicable method found: <T#2>of(T#2)
        (partially instantiated to: (String)Stream<String>)
  where T#1,T#2 are type-variables:
    T#1 extends Object declared in method <T#1>of(T#1...)
    T#2 extends Object declared in method <T#2>of(T#2)
DeleteMe.java:60: Note: Deferred instantiation of method <T>of(T)
        return Stream.of("a").distinct();
                        ^
  instantiated signature: (String)Stream<String>
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>of(T)
DeleteMe.java:60: Note: resolving method distinct in type Stream to candidate 0
        return Stream.of("a").distinct();
                             ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: distinct()
  where T is a type-variable:
    T extends Object declared in interface Stream
DeleteMe.java:60: error: incompatible types: Stream<String> cannot be converted to Stream<CharSequence>
        return Stream.of("a").distinct();
                                      ^
1 error

I guess the most important part is this : (partially instantiated to: (String)Stream<String>)

You can see that the resolution of what type T is, is done a method call basis; not of the whole chain of calls. If it would would, btw, this would complicate compilers works quite a lot. For a simple chain like this, things might look trivial, but it gets far, far more tricky and involved when there are many. Especially, when you find out about non-denotable types, which will even further complicate this.

like image 152
Eugene Avatar answered Oct 26 '22 06:10

Eugene


It's a compiler's feature. When you write Stream.of("a"); without return statement and generic type for method of() compiler gets the type from methods parameter "a" (Stream<String>).

When you write return Stream.of("a"); compiler gets the type from the return type (Stream<CharSequence>) because you don't set generic type. And when you write return Stream.of("a").distinct(); compiler get type from method's parameter (Stream<String> too), for calling .distinct() or .sorted()

In other words, you don't set generic type for a method of(), and the compiler got it from the method's return type when you don't have another stream's method, and got type from variable's type if you have other stream's methods.

like image 20
Dmitrii B Avatar answered Oct 26 '22 04:10

Dmitrii B