I have the following piece of code
public class TeeingCollector {
public static void main(String[] args) {
// var strs = List.of("abc");
var dividedStrings = Stream.of("foo", "hello", "bar", "world")
.collect(Collectors.teeing(
Collectors.filtering((String s) -> s.length() <= 3, Collectors.toList()),
Collectors.filtering((String s) -> s.length() > 3, Collectors.toList()),
List::of
));
System.out.println(dividedStrings);
}
private static class Employee {
boolean isActive;
public Employee(boolean isActive) {
this.isActive = isActive;
}
public boolean isActive() {
return isActive;
}
@Override
public String toString() {
return "Employee{" +
"isActive=" + isActive +
'}';
}
}
private static class MaxMin {
int max;
int min;
MaxMin(int max, int min) {
this.max = max;
this.min = min;
}
@Override
public String toString() {
return "MaxMin{" +
"max=" + max +
", min=" + min +
'}';
}
}
}
If I execute that class from terminal with java src/TeeingCollector.java
I get the following error:
src/TeeingCollector.java:14: error: incompatible types: inferred type does not conform to equality constraint(s)
.collect(Collectors.teeing(
^
inferred: List<String>
equality constraints(s): List<Object>,R
where R,T,A are type-variables:
R extends Object declared in method <T,A,R>filtering(Predicate<? super T>,Collector<? super T,A,R>)
T extends Object declared in method <T,A,R>filtering(Predicate<? super T>,Collector<? super T,A,R>)
A extends Object declared in method <T,A,R>filtering(Predicate<? super T>,Collector<? super T,A,R>)
1 error
error: compilation failed
If I uncomment the line var strs = List.of("abc");
then the code is executed without any problem.
Java version (for macOS):
OpenJDK Runtime Environment (build 12+33)
OpenJDK 64-Bit Server VM (build 12+33, mixed mode, sharing)
running the same code with the following version (older) yields no errors
OpenJDK Runtime Environment (build 12-ea+23)
OpenJDK 64-Bit Server VM (build 12-ea+23, mixed mode, sharing)
Note: if I compile it then run, I don't have any errors with both builds for macOS, so it seems that only java TeeingCollector.java
isn't working properly
TL;DR this is obviously a bug, as the compiler’s behavior depends on entirely unrelated things, including environmental aspects outside the Java language.
I simplified your example and integrated implementations of Collectors.teeing
and Predicate.not
into the example, to be able to test your code with Java versions from JDK 9 to JDK 12.
Note that while I first thought that this was some kind of interaction of var
’s type inference with the generic construct on the right-hand-side, more tests revealed that the problem even exists when using explicit types for the variables, which allowed to include JDK 9 in the tests.
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import java.util.stream.Collector;
public class Temp5 {
public static void main(String[] args) {
// List<Character> strs = List.of("abc");
List<List<Character>> lettersAndNumbers = Stream.of('5', 't', 'o', '9', 'p', '1', 'h')
.collect(teeing(
Collectors.filtering(Character::isLetter, Collectors.toList()),
Collectors.filtering(not(Character::isLetter), Collectors.toList()),
List::of
));
}
public static <T, R1, R2, R>
Collector<T, ?, R> teeing(Collector<? super T, ?, R1> downstream1,
Collector<? super T, ?, R2> downstream2,
BiFunction<? super R1, ? super R2, R> merger) {
return teeing0(downstream1, downstream2, merger);
}
private static <T, A1, A2, R1, R2, R>
Collector<T, ?, R> teeing0(Collector<? super T, A1, R1> downstream1,
Collector<? super T, A2, R2> downstream2,
BiFunction<? super R1, ? super R2, R> merger) {
Objects.requireNonNull(downstream1, "downstream1");
Objects.requireNonNull(downstream2, "downstream2");
Objects.requireNonNull(merger, "merger");
Supplier<A1> c1Supplier = Objects.requireNonNull(downstream1.supplier(), "downstream1 supplier");
Supplier<A2> c2Supplier = Objects.requireNonNull(downstream2.supplier(), "downstream2 supplier");
BiConsumer<A1, ? super T> c1Accumulator =
Objects.requireNonNull(downstream1.accumulator(), "downstream1 accumulator");
BiConsumer<A2, ? super T> c2Accumulator =
Objects.requireNonNull(downstream2.accumulator(), "downstream2 accumulator");
BinaryOperator<A1> c1Combiner = Objects.requireNonNull(downstream1.combiner(), "downstream1 combiner");
BinaryOperator<A2> c2Combiner = Objects.requireNonNull(downstream2.combiner(), "downstream2 combiner");
Function<A1, R1> c1Finisher = Objects.requireNonNull(downstream1.finisher(), "downstream1 finisher");
Function<A2, R2> c2Finisher = Objects.requireNonNull(downstream2.finisher(), "downstream2 finisher");
Collector.Characteristics[] characteristics;
Set<Collector.Characteristics> c1Characteristics = downstream1.characteristics();
Set<Collector.Characteristics> c2Characteristics = downstream2.characteristics();
EnumSet<Collector.Characteristics> c = EnumSet.noneOf(Collector.Characteristics.class);
c.addAll(c1Characteristics);
c.retainAll(c2Characteristics);
c.remove(Collector.Characteristics.IDENTITY_FINISH);
characteristics = c.toArray(new Collector.Characteristics[0]);
class PairBox {
A1 left = c1Supplier.get();
A2 right = c2Supplier.get();
void add(T t) {
c1Accumulator.accept(left, t);
c2Accumulator.accept(right, t);
}
PairBox combine(PairBox other) {
left = c1Combiner.apply(left, other.left);
right = c2Combiner.apply(right, other.right);
return this;
}
R get() {
R1 r1 = c1Finisher.apply(left);
R2 r2 = c2Finisher.apply(right);
return merger.apply(r1, r2);
}
}
return Collector.of(PairBox::new, PairBox::add, PairBox::combine, PairBox::get, characteristics);
}
@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}
The results are disastrous. Every version can have a different opinion about the correctness of the code, even when just changing the minor version. Well, even using OpenJDK instead of Oracle’s distribution can have a different outcome. Additionally, even the smallest changes to the code can affect it. As you’ve noted, changing the comment into an actual declaration may change the outcome, but even removing the comment changes the outcome for some JDK versions. Just changing the values, like using Stream.of('5', 't', 'o', '9', 'p', '1')
instead of Stream.of('5', 't', 'o', '9', 'p', '1', 'h')
changes the outcome for some versions.
My conclusion is that something in the compiler implementation depends on something stable for a particular setup, but actually unpredictable, like the iteration order of a HashMap
. And the JDK version itself seems to be part of it. This would also explain why the outcome may change when using MacOS instead of Linux or Windows. Even compiling this source file together with another, unrelated source file can change the result.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With