Когда Java требует явных параметров типа?

Дано:

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));
    }
}

Операторы с map1 и map3 компилируются отлично, но map2 не выполняется:

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

Ошибка компилятора может быть решена путем предоставления явных параметров типа <String, String, Integer>.

  • Когда Java 8 требует явных параметров типа? Смысл, есть ли известный шаблон, который нарушает тип вывода?
  • Можно ли изменить toImmutableMap() и MapCollectorBuilder, чтобы избежать явных параметров типа без потери использования Builder для настройки коллектора?

UPDATE

  1. Почему работает оператор map3? Чем он отличается от утверждения, содержащего map2?

Ответ 1

Чтобы ответить на ваш вопрос "Смысл, есть ли известный шаблон, который прерывает вывод типа?" в ближайшее время: конечно, есть шаблон, более того, существует огромная спецификация для всего поведения языка программирования Java.

Но главы, касающиеся типов вывода и типов вызова методов, действительно являются исчерпывающими и трудно понять. Это лучше всего иллюстрируется тем фактом, что в случае неожиданного поведения часто возникают большие дискуссии о ожидаемом поведении в соответствии со спецификацией.

Но есть некоторые моменты, объясняемые и запоминающиеся для программиста.

Существует два способа определить параметры типа, аргументы, переданные методу или частям, из которых составлено выражение, или целевым типом выражения, то есть ожидаемым типом для параметра вызова, переменную, которая назначена, или возвращаемый тип метода в случае оператора return.

Тип цели может распространяться через вложенные вызовы метода, такие как

TargetType x=foo(bar(/*target type can be used*/));

или в условном выражении

TargetType x=condition? foo(/*target type can be used*/): foo(/*target type can be used*/);

но не в случае цепного вызова, как в

TargetType x=foo(/*target type can NOT be used*/).foo();

Теперь к вашим примерам:

ImmutableMap<String, Integer> map1 = Stream.of("1", "2", "3").collect( expression );

Здесь Stream.of(…) и .collect(…) привязаны, поэтому целевой тип не может использоваться для определения типа потока вызова of, но аргументы, предоставленные этому методу, достаточны для вывода типа Stream<String>, Метод collect предоставляет результат, который присваивается map1, поэтому оба типа потока Stream<String> и целевой тип ImmutableMap<String, Integer> известны и могут использоваться для вывода типа для выражения. На выражения:

  • Testcase.toImmutableMap(keyMapper, valueMapper).build() Это цепной вызов, поэтому целевой тип известен build(), но не для toImmutableMap. Однако аргументы toImmutableMap являются локальными переменными, имеющими известный точный тип, поэтому вывод типа может использовать их для вывода типа результата toImmutableMap и проверить, соответствует ли он ожиданиям для .build()

  • Testcase.toImmutableMap(i -> i, Integer::valueOf).build() это снова цепной вызов, но теперь аргумент i - > i имеет неполный тип и страдает отсутствием целевого типа. Попытка угадать тип для i -> i без знания о типе цели не работает.

  • Testcase.toImmutableMap2(i -> i, Integer::valueOf) это не цепной вызов, поэтому целевой тип доступен для вызова toImmutableMap2 (по отношению к вызову collect, его вложенному вызову). Поэтому целевой тип toImmutableMap2 позволяет выводить типы целей для параметров, следовательно, для выражения i -> i лямбда. При правильном типе цели можно определить правильную функциональную подпись.

Ответ 2

Целевой тип выражения лямбда полностью определяется контекстом как описано в учебнике Java. Поэтому lambdas не вносят вклад в вывод параметра типа; вместо этого они полагаются на него. Ссылки на методы "- это компактные, легко читаемые лямбда-выражения для методов, которые уже имеют имя" (Oracle Java Tutorial, добавлено выделение), поэтому нет никакого различия там, где анализ цвета будет по-разному, когда они будут задействованы.

Когда вы назначаете ссылку на lambda/method на переменную, этот тип переменной предоставляет контекст для вывода параметров типа. Однако, когда вы передаете их непосредственно на общий метод, вам нужен другой механизм для вывода их типов. В некоторых случаях другие аргументы метода могут служить этой цели. В вашем конкретном случае похоже, что вам, вероятно, нужны явные аргументы типа:

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

Обновление

Что касается обновленного вопроса, похоже, что Java может правильно определять типы в случае map3 отчасти потому, что определение не осложняется вызовом метода MapCollectorBuilder.build(). Без build() тип map3 предоставляет контекст для определения аргумента первого типа Stream.collect(), который дает как K, так и V. Параметр типа T может быть выведен из (предполагаемого) типа Stream.

Тем не менее, при участии build() я думаю, что Java разделяет вопрос о выводе параметров типа для универсального метода toImmutableMap() из вопроса о типе возвращаемого значения вызова build() по его возвращаемому значению. Другими словами, он хочет определить тип объекта, возвращаемого toImmutableMap(), прежде чем он рассмотрит тип значения, полученного путем вызова метода для этого значения.

Ответ 3

Есть еще один способ исправить это. Вы можете дать подсказку компилятору, явно указывая тип аргумента для идентификатора lambda:

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

Компилируется в Javac 1.8.0_25 и ECJ 3.10.2.