Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When does Java require explicit type parameters?

Tags:

java

java-8

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?
like image 856
Gili Avatar asked Jul 12 '15 16:07

Gili


People also ask

What are the formal type parameters what are the actual type parameters?

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.

What are parameter types in Java?

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.

How does Java type inference work?

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.

What is the correct name for empty type parameter?

It is called generics.


2 Answers

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.

like image 112
Holger Avatar answered Oct 04 '22 10:10

Holger


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.

like image 29
John Bollinger Avatar answered Oct 04 '22 12:10

John Bollinger