4

I am getting Ambiguity error while trying to print result of JAVA8 collectors.

I am trying to print result of summation of IDs in Product object, but getting the following error :

"The method println(double) is ambiguous for the type PrintStream"

Here is a small line of code where I am getting the compilation error:

Edited : Adding code snippet for more details :

  1. Product.java domain class.

package com.sample.reproduce.bugs;

public class Product {

    private double id;

    private String productName;

    public double getId() {
        return id;
    }

    public void setId(double id) {
        this.id = id;
    }

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

}
  1. Main.java class where I am getting compilation error :

Following is the line of code where I am getting compilation error :

System.out.println(productList.stream().collect(Collectors.summingDouble(x -> x.getId())));

Class Snapshot :

enter image description here

I don't get any error if I will use Collector in a separate line (out of println method).

Why ins't the compiler able to detect the exact return type of JAVA 8 Collectors if we use it in println() method?

Adding details of another approach with command prompt :

I tried with command prompt with same JDK version and the program was compiled and executed successfully. So the answer by Holger seems correct. This seems issue only with Eclipse compiler :

enter image description here

Gunjan Shah
  • 5,088
  • 16
  • 53
  • 72
  • works fine for me, what's the type of `id` there? anything abstract around it possibly? – Naman Feb 21 '19 at 08:14
  • Can't reproduce on muy environment. Can you share the exact output of your compiler or a screenshot from your IDE? – Mureinik Feb 21 '19 at 08:14
  • I'm getting the same error with this simpler code: `List productsList = new ArrayList<>(); System.out.println(productsList.stream().collect(Collectors.summingDouble(Double::doubleValue)));` I'm using Java 8. – Eran Feb 21 '19 at 08:19
  • If it passes compilation in newer Java versions, perhaps it's a bug. – Eran Feb 21 '19 at 08:19
  • The next question is, which compiler you are using... – Flown Feb 21 '19 at 08:21
  • I think the downvotes are not justified. I reproduced this error. +1 – Eran Feb 21 '19 at 08:22
  • 1
    @Eran not with `javac`. Eclipse, perhaps? – Holger Feb 21 '19 at 09:04
  • @Holger yes. Eclipse – Eran Feb 21 '19 at 09:04
  • 1
    @Eran could reproduce the problem, and analyzing it revealed even worse… – Holger Feb 21 '19 at 10:09
  • I have added image from my eclipse IDE for more details. Also I am using Eclipse oxygen Version: 2018-09 (4.9.0). – Gunjan Shah Feb 22 '19 at 18:46
  • I think, the answer from Holger seems correct. There may be bug in Eclipse. I tried to compile and run the same program with same JDK but on command prompt and it worked perfectly. – Gunjan Shah Feb 22 '19 at 19:33

3 Answers3

9

This is a bug in the Eclipse compiler, and the rabbit hole goes even deeper than the compiler error. I reduced your code example to

public static void main(String[] args)
{
  println(Stream.of(42).collect(Collectors.summingDouble(d -> d)));
}
public static void println(double x) {}
public static void println(char[] x) {}
public static void println(String x) {}
public static void println(Object x) {}

I only kept the the println methods affecting the behavior of the compiler.

There are the methods println(Object x), which is the one which should be invoked, as it is the only one applicable without boxing operations, println(double), which is the one mentioned in the error message and applicable after unboxing, and the two methods println(char[] x) and println(String x), which are not applicable at all.

Removing the println(double x) method makes the error go away, which would be understandable, even if the error is not correct, but weirdly, removing the println(Object x) method does not solve the error.

And even worse, removing either of the inapplicable methods, println(char[] x) or println(String x), also removes the error, but generates code invoking the wrong, inapplicable method:

public static void main(String[] args)
{
  println(Stream.of(42).collect(Collectors.summingDouble(d -> d)));
}
public static void println(double x) { System.out.println("println(double)"); }
public static void println(char[] x) { System.out.println("println(char[])"); }
//public static void println(String x) { System.out.println("println(String)"); }
public static void println(Object x) { System.out.println("println(Object)"); }
Exception in thread "main" java.lang.ClassCastException: java.lang.Double cannot be cast to [C
    at Tmp2.main(Unknown Source)
public static void main(String[] args)
{
  println(Stream.of(42).collect(Collectors.summingDouble(d -> d)));
}
public static void println(double x) { System.out.println("println(double)"); }
//public static void println(char[] x) { System.out.println("println(char[])"); }
public static void println(String x) { System.out.println("println(String)"); }
public static void println(Object x) { System.out.println("println(Object)"); }
Exception in thread "main" java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.String
    at Tmp2.main(Unknown Source)

I think, we don’t need to dig into the depth of the formal Java Language Specification, to recognize this behavior as inappropriate.

Removing both inapplicable methods, println(char[] x) and println(String x), makes the compiler choose the correct method, println(Object x) over println(double x), but that’s not impressive.

For reference, I tested with version Oxygen.3a Release (4.7.3a), build 20180405-1200. There are likely other versions affected as well.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 2
    very interesting. – Eran Feb 21 '19 at 10:12
  • 2
    @Michael `[C` is the internal signature of `char[]`. The error message does not translate it, see [`Class.getName()`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Class.html#getName()). – Holger Feb 21 '19 at 10:12
  • @Holger Oh, fair enough, sorry – Michael Feb 21 '19 at 10:13
  • 1
    @Michael no problem, that’s not an intuitive behavior, but rather, the missing translation likely was a mistake of the early Java versions that couldn’t be changed for compatibility reasons. – Holger Feb 21 '19 at 10:15
  • @Holger, yes, this is a bug. From a quick look I fear that this can only possibly be fixed after https://bugs.openjdk.java.net/browse/JDK-8054721 has been resolved, viz. inference creates a capture bound but JLS does not specify a viable way how to leverage that capture bound. The effect is that `collect()` is bogusly considered as applicable against target type `char[]`. Please support us in getting the JDK/spec bug resolved. – Stephan Herrmann Feb 22 '19 at 00:09
  • @Holger my initial guess regarding JDK-8054721 was a near miss: it's not about how capture bounds are handled during resolution, but how they are propagated from inner inference to outer inference. Obviously, if relevant bounds are dropped during inference, all kinds of havoc can result. See my answer and the linked Eclipse bug. Many thanks for the succinct example, btw. – Stephan Herrmann Mar 05 '19 at 21:15
  • @StephanHerrmann I have difficulties with the term “outer inference”, as `println` is not generic. To my understanding, the only connection of the non-generic method `println` to type inference should be that it provides the target type for the argument expression but since `char[] x = Stream.of(42).collect( Collectors.summingDouble(d -> d));` is correctly rejected by the compiler, the `println(char[])` should not be applicable either. – Holger Mar 06 '19 at 08:24
  • @Holger sure, there's no inference for `println` (only overload resolution). When I said inner inference I was looking at `summingDouble(..)`, and then outer inference is for `collect(..)`, both being generic. `println(char[])` (e.g.) indeed provides the target type, which according to current JLS drives inference into believing that `collect(..summingDouble(..))` were compatible with `char[]`, because the relation to Double (from summingDouble) is not seen. Clearer now? – Stephan Herrmann Mar 06 '19 at 23:19
  • Admittedly, I don't yet have a ready answer regarding the plain assignment, but the difference probably relates to the fact that overload resolution is based only on invocation applicability inference, whereas an assignment can do full inference in one go. I'll add a test for that and report back. (It is possible to have overload resolution select a method that later is found to fail invocation type inference - but we don't see that when ambiguity is reported first). – Stephan Herrmann Mar 06 '19 at 23:25
  • @Holger thanks for insisting on the question, why overload resolution produces different results than a plain assignment. It led me to a better understanding of how repeated inference driven by overload resolution can influence the outcome in unwanted ways. See also the edit in my answer. – Stephan Herrmann Mar 07 '19 at 16:49
2

Yes, this is a compiler bug, but closer investigation suggests that this may be caused by an omission in JLS.

More specifically, the bug would go away if one sentence in JLS §18.5.2.2. were changed like this:

Old:

For a poly class instance creation expression or a poly method invocation expression , C contains all the constraint formulas that would appear in the set C generated by §18.5.2 when inferring the poly expression's invocation type.

Mentioning only "constraint forumulas" seems to be insufficient.

Proposed new:

For a poly class instance creation expression or a poly method invocation expression , C contains all the type bounds and capture bounds that would result from reducing and incorporating the set C generated by §18.5.2 when inferring the poly expression's invocation type.

PS: Javac is known for implementing more / different data flows between inner and outer inference than are captured in JLS, which is probably the reason why javac selects println(Object). In some regards this implementation may be closer to the intended semantics, and in the example in this question common sense agrees with javac. That's why focus should IMHO be on improving JLS (and transitively ecj).

EDIT: While the above analysis is plausable, fixes the issue, and perhaps even matches what javac is actually doing, it cannot explain, why the problem only occurs below overload resolution for println(..) but not in an assignment to a char[] variable.

After more research into this difference, an alternate change was crafted, that will effectively (via several indirections) force the compiler to re-compute the capture bound instead of passing it up like proposed above. This change is in line with the current JLS. The exact chain of causality of this problem is beyond the scope of this forum, but interested parties are invited to read some background in the Eclipse bug linked above.

Stephan Herrmann
  • 7,963
  • 2
  • 27
  • 38
0
System.out.println(productsList.stream().mapToDouble(x -> x.id).sum());

I am not entire sure about the exact code here, but without a required type (println has many overloaded parameters), and generic typing of a stream, ambiguity arises. Especially with a Double id instead of a double. Maybe someone else can do a better explanation.

Assigment to a local variable might have worked.

Better is to use the streams of the primitive type. The above uses a DoubleStream. For an "id" I would rather have expected a LongStream.

Joop Eggen
  • 107,315
  • 7
  • 83
  • 138
  • 2
    `Collectors.summingDouble(…)` will collect to a `Double` result, no matter what shape the function argument has, independently of the target type. And digging deeper into the behavior of the Eclipse compiler revealed really serious problems. – Holger Feb 21 '19 at 10:06