Given:
import com.google.common.collect.ImmutableMap;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Stream;
public class Testcase
{
public static <T, K, V> MapCollectorBuilder<T, K, V>
toImmutableMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper)
{
return null;
}
public static final class MapCollectorBuilder<T, K, V>
{
public Collector<T, ?, ImmutableMap<K, V>> build()
{
return null;
}
}
public static <T, K, V> Collector<T, ?, ImmutableMap<K, V>> toImmutableMap2(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper)
{
return null;
}
public void main(String[] args)
{
Function<String, String> keyMapper = i -> i;
Function<String, Integer> valueMapper = Integer::valueOf;
ImmutableMap<String, Integer> map1 = Stream.of("1", "2", "3")
.collect(Testcase.toImmutableMap(keyMapper, valueMapper).build());
ImmutableMap<String, Integer> map2 = Stream.of("1", "2", "3")
.collect(Testcase.toImmutableMap(i -> i, Integer::valueOf).build());
ImmutableMap<String, Integer> map3 = Stream.of("1", "2", "3")
.collect(Testcase.toImmutableMap2(i -> i, Integer::valueOf));
}
}
The statements involving map1
and map3
compile fine, but map2
fails with:
Testcase.java:[41,57] incompatible types: cannot infer type-variable(s) T,K,V
(argument mismatch; invalid method reference
no suitable method found for valueOf(java.lang.Object)
method java.lang.Integer.valueOf(java.lang.String) is not applicable
(argument mismatch; java.lang.Object cannot be converted to java.lang.String)
method java.lang.Integer.valueOf(int) is not applicable
(argument mismatch; java.lang.Object cannot be converted to int))
The compiler error can be solved by providing explicit type parameters <String, String, Integer>
.
toImmutableMap()
and MapCollectorBuilder
be changed to avoid explicit type parameters without losing the use of a Builder for configuring the Collector?UPDATE:
map3
work? How does it differ from the statement involving map2
?Formal and Actual Parameters The following definitions are useful: formal parameter — the identifier used in a method to stand for the value that is passed into the method by a caller. actual parameter — the actual value that is passed into the method by a caller.
A type parameter, also known as a type variable, is an identifier that specifies a generic type name. The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.
Type inference represents the Java compiler's ability to look at a method invocation and its corresponding declaration to check and determine the type argument(s). The inference algorithm checks the types of the arguments and, if available, assigned type is returned.
It is called generics.
To answer your question “Meaning, is there a known pattern that breaks type inference?” shortly: of course, there is a pattern, moreover there is a huge specification for the entire behavior of the Java programming language.
But the chapters regarding type inference and method invocation types are really exhaustive and hard to understand. This is best illustrated by the fact that in the case of unexpected behavior, often large discussions about the expected behavior according to the specification occur.
But there are some points explainable and rememberable for a programmer.
There are two ways how to infer the type parameters, by the arguments passed to a method or parts from which an expression is composed or by the target type of an expression, that is, the expected type for a parameter of an invocation, the variable which is assigned or the return type of a method in case of a return statement.
The target type can get propagated through nested method invocations like in
TargetType x=foo(bar(/*target type can be used*/));
or in a conditional like
TargetType x=condition? foo(/*target type can be used*/): foo(/*target type can be used*/);
but not in case of a chained invocation as in
TargetType x=foo(/*target type can NOT be used*/).foo();
Now to your examples:
ImmutableMap<String, Integer> map1 = Stream.of("1", "2", "3").collect( expression );
Here, the Stream.of(…)
and .collect(…)
are chained, therefore the target type cannot be used to determine the stream type of the of
invocation but the arguments provided to that method are sufficient to infer the type Stream<String>
. The collect
method provides the result that gets assigned to map1
, so both, the stream type Stream<String>
and the target type ImmutableMap<String, Integer>
are known and can be used for the type inference for the expression. Onto the expressions:
Testcase.toImmutableMap(keyMapper, valueMapper).build()
this is a chained invocation, so the target type is known for build()
but not for toImmutableMap
. However, the arguments to toImmutableMap
are local variables which have a known exact type, therefore the type inference can use them to infer the result type of toImmutableMap
and check whether it matches the expectations for .build()
Testcase.toImmutableMap(i -> i, Integer::valueOf).build()
this is again a chained invocation but now the argument i - > i
has an incomplete type and suffers from the absence of the target type. The attempt to guess a type for i -> i
without knowledge about the target type fails.
Testcase.toImmutableMap2(i -> i, Integer::valueOf)
this is not a chained invocation, therefore the target type is available for the toImmutableMap2
call (in respect to the collect
call, it’s a nested invocation). Therefore, the target type of toImmutableMap2
allows to infer target types for the parameters, hence for the i -> i
lambda expression. With a proper target type, the correct functional signature can be inferred.
The target type of a lambda expression is determined entirely from context, as discussed in the Java tutorial. Therefore, lambdas do not contribute to type parameter inference; instead, they rely on it. Method references "are compact, easy-to-read lambda expressions for methods that already have a name" (Oracle Java Tutorial; emphasis added), so the there is no distinction there that would color type analysis differently when they are involved.
When you assign your lambda / method reference to a variable, that variable's type provides the context for inferring type parameters. When you pass them directly to a generic method, however, you need some other mechanism to infer their types. In some cases, other arguments to the method might serve that purpose. In your particular case, it looks like you probably need explicit type arguments:
ImmutableMap<String, Integer> map2 = Stream.of("1", "2", "3").collect(
Testcase.<String, String, Integer>toImmutableMap(i -> i, Integer::valueOf).build());
Update
With respect to the updated question, it looks like Java can infer types correctly in the map3
case in part because the determination is not complicated by invocation of the MapCollectorBuilder.build()
method. Without build()
, the type of map3
provides the context to determine the first type argument of Stream.collect()
, which gives both K
and V
. Type parameter T
can be inferred from the (inferred) type of the Stream
.
With build()
involved, however, I think Java is separating the question of inferring the type parameters for generic method toImmutableMap()
from the question of the type of the return value of invoking build()
on its return value. In other words, it wants determine the type of the object returned by toImmutableMap()
before it considers the type of the value obtained by invoking a method on that value.
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