Gili
Gili

Reputation: 90033

When does Java require explicit type parameters?

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>.

  1. When does Java 8 require explicit type parameters? Meaning, is there a known pattern that breaks type inference?
  2. Can toImmutableMap() and MapCollectorBuilder be changed to avoid explicit type parameters without losing the use of a Builder for configuring the Collector?

UPDATE:

  1. Why does the statement involving map3 work? How does it differ from the statement involving map2?

Upvotes: 13

Views: 1694

Answers (3)

Holger
Holger

Reputation: 298233

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.

Upvotes: 5

Tagir Valeev
Tagir Valeev

Reputation: 100239

There's another way to fix this. You can give a hint to compiler explicitly specifying the argument type for identity lambda:

ImmutableMap<String, Integer> map2 = Stream.of("1", "2", "3")
    .collect(Testcase.toImmutableMap((String i) -> i, Integer::valueOf).build());

Compiles fine in Javac 1.8.0_25 and ECJ 3.10.2.

Upvotes: 1

John Bollinger
John Bollinger

Reputation: 180351

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.

Upvotes: 3

Related Questions