Почему лямбда-тип возврата не проверяется во время компиляции?

38

Ссылка на используемый метод имеет тип возвращаемого значения Integer. Но несовместимое Stringдопускается в следующем примере.

Как исправить withобъявление метода, чтобы получить безопасный тип ссылки на метод без приведения вручную?

import java.util.function.Function;

public class MinimalExample {
  static public class Builder<T> {
    final Class<T> clazz;

    Builder(Class<T> clazz) {
      this.clazz = clazz;
    }

    static <T> Builder<T> of(Class<T> clazz) {
      return new Builder<T>(clazz);
    }

    <R> Builder<T> with(Function<T, R> getter, R returnValue) {
      return null; //TODO
    }

  }

  static public interface MyInterface {
    Integer getLength();
  }

  public static void main(String[] args) {
// missing compiletimecheck is inaceptable:
    Builder.of(MyInterface.class).with(MyInterface::getLength, "I am NOT an Integer");

// compile time error OK: 
    Builder.of(MyInterface.class).with((Function<MyInterface, Integer> )MyInterface::getLength, "I am NOT an Integer");
// The method with(Function<MinimalExample.MyInterface,R>, R) in the type MinimalExample.Builder<MinimalExample.MyInterface> is not applicable for the arguments (Function<MinimalExample.MyInterface,Integer>, String)
  }

}

ИСПОЛЬЗОВАНИЕ СЛУЧАЯ: тип безопасный, но общий Builder.

Я пытался реализовать универсальный компоновщик без обработки аннотаций (автозначение) или плагина компилятора (lombok)

import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

public class BuilderExample {
  static public class Builder<T> implements InvocationHandler {
    final Class<T> clazz;
    HashMap<Method, Object> methodReturnValues = new HashMap<>();

    Builder(Class<T> clazz) {
      this.clazz = clazz;
    }

    static <T> Builder<T> of(Class<T> clazz) {
      return new Builder<T>(clazz);
    }

    Builder<T> withMethod(Method method, Object returnValue) {
      Class<?> returnType = method.getReturnType();
      if (returnType.isPrimitive()) {
        if (returnValue == null) {
          throw new IllegalArgumentException("Primitive value cannot be null:" + method);
        } else {
          try {
            boolean isConvertable = getDefaultValue(returnType).getClass().isAssignableFrom(returnValue.getClass());
            if (!isConvertable) {
              throw new ClassCastException(returnValue.getClass() + " cannot be cast to " + returnType + " for " + method);
            }
          } catch (IllegalArgumentException | SecurityException e) {
            throw new RuntimeException(e);
          }
        }
      } else if (returnValue != null && !returnType.isAssignableFrom(returnValue.getClass())) {
        throw new ClassCastException(returnValue.getClass() + " cannot be cast to " + returnType + " for " + method);
      }
      Object previuos = methodReturnValues.put(method, returnValue);
      if (previuos != null) {
        throw new IllegalArgumentException("Value alread set for " + method);
      }
      return this;
    }

    static HashMap<Class, Object> defaultValues = new HashMap<>();

    private static <T> T getDefaultValue(Class<T> clazz) {
      if (clazz == null || !clazz.isPrimitive()) {
        return null;
      }
      @SuppressWarnings("unchecked")
      T cachedDefaultValue = (T) defaultValues.get(clazz);
      if (cachedDefaultValue != null) {
        return cachedDefaultValue;
      }
      @SuppressWarnings("unchecked")
      T defaultValue = (T) Array.get(Array.newInstance(clazz, 1), 0);
      defaultValues.put(clazz, defaultValue);
      return defaultValue;
    }

    public synchronized static <T> Method getMethod(Class<T> clazz, java.util.function.Function<T, ?> resolve) {
      AtomicReference<Method> methodReference = new AtomicReference<>();
      @SuppressWarnings("unchecked")
      T proxy = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, new InvocationHandler() {

        @Override
        public Object invoke(Object p, Method method, Object[] args) {

          Method oldMethod = methodReference.getAndSet(method);
          if (oldMethod != null) {
            throw new IllegalArgumentException("Method was already called " + oldMethod);
          }
          Class<?> returnType = method.getReturnType();
          return getDefaultValue(returnType);
        }
      });

      resolve.apply(proxy);
      Method method = methodReference.get();
      if (method == null) {
        throw new RuntimeException(new NoSuchMethodException());
      }
      return method;
    }

    // R will accep common type Object :-( // see /programming/58337639
    <R, V extends R> Builder<T> with(Function<T, R> getter, V returnValue) {
      Method method = getMethod(clazz, getter);
      return withMethod(method, returnValue);
    }

    //typesafe :-) but i dont want to avoid implementing all types
    Builder<T> withValue(Function<T, Long> getter, long returnValue) {
      return with(getter, returnValue);
    }

    Builder<T> withValue(Function<T, String> getter, String returnValue) {
      return with(getter, returnValue);
    }

    T build() {
      @SuppressWarnings("unchecked")
      T proxy = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, this);
      return proxy;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
      Object returnValue = methodReturnValues.get(method);
      if (returnValue == null) {
        Class<?> returnType = method.getReturnType();
        return getDefaultValue(returnType);
      }
      return returnValue;
    }
  }

  static public interface MyInterface {
    String getName();

    long getLength();

    Long getNullLength();

    Long getFullLength();

    Number getNumber();
  }

  public static void main(String[] args) {
    MyInterface x = Builder.of(MyInterface.class).with(MyInterface::getName, "1").with(MyInterface::getLength, 1L).with(MyInterface::getNullLength, null).with(MyInterface::getFullLength, new Long(2)).with(MyInterface::getNumber, 3L).build();
    System.out.println("name:" + x.getName());
    System.out.println("length:" + x.getLength());
    System.out.println("nullLength:" + x.getNullLength());
    System.out.println("fullLength:" + x.getFullLength());
    System.out.println("number:" + x.getNumber());

    // java.lang.ClassCastException: class java.lang.String cannot be cast to long:
    // RuntimeException only :-(
    MyInterface y = Builder.of(MyInterface.class).with(MyInterface::getLength, "NOT A NUMBER").build();

    // java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long
    // RuntimeException only :-(
    System.out.println("length:" + y.getLength());
  }

}
jukzi
источник
1
удивительное поведение. Из интереса: это то же самое, когда вы используете classвместо interfaceстроителя?
GameDroids
Почему это недопустимо? В первом случае вы не указываете тип getLength, поэтому его можно настроить на возврат Object(или Serializable) в соответствии с параметром String.
Тило
1
Я могу ошибаться, но я думаю, что ваш метод withявляется частью проблемы, когда он возвращается null. При реализации метода with(), фактически используя Rтип функции, такой же, как Rв параметре, вы получаете ошибку. Например<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroids
2
jukzi, может быть, вы должны предоставить код или объяснение того, что на самом деле должен делать ваш метод with и почему вы должны Rбыть Integer. Для этого вам нужно показать нам, как вы хотите использовать возвращаемое значение. Кажется, вы хотите реализовать какой-то шаблон-конструктор, но я не могу распознать общий шаблон или ваше намерение.
sfiss
1
Спасибо. Я также думал о проверке на полную инициализацию. Но так как я не вижу способа сделать это во время компиляции, я предпочитаю придерживаться значений по умолчанию null / 0. Я также не знаю, как проверить неинтерфейсные методы во время компиляции. Во время выполнения использование неинтерфейса, такого как ".with (m -> 1) .returning (1)", уже приводит к раннему java.lang.NoSuchMethodException
jukzi

Ответы:

27

В первом примере MyInterface::getLengthи "I am NOT an Integer"помогли разрешить общие параметры Tи Rк MyInterfaceи Serializable & Comparable<? extends Serializable & Comparable<?>>соответственно.

// it compiles since String is a Serializable
Function<MyInterface, Serializable> function = MyInterface::getLength;
Builder.of(MyInterface.class).with(function, "I am NOT an Integer");

MyInterface::getLengthэто не всегда a, Function<MyInterface, Integer>если вы явно не скажете это, что приведет к ошибке во время компиляции, как показано во втором примере.

// it doesn't compile since String isn't an Integer
Function<MyInterface, Integer> function = MyInterface::getLength;
Builder.of(MyInterface.class).with(function, "I am NOT an Integer");
Андрей Тобилко
источник
Этот ответ полностью отвечает на вопрос, почему он интерпретируется иначе, чем намерение. Интересно. Похоже, R бесполезно. Знаете ли вы какое-либо решение проблемы?
Джукзи
@jukzi (1) явно определяет параметры типа метода (здесь R): Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");чтобы он не компилировался, или (2) пусть он решается неявно и, как мы надеемся, продолжаем без ошибок во время компиляции
Эндрю Тобилко
11

Это вывод типа, который играет здесь свою роль. Рассмотрим общее Rв сигнатуре метода:

<R> Builder<T> with(Function<T, R> getter, R returnValue)

В случае, как указано в списке:

Builder.of(MyInterface.class).with(MyInterface::getLength, "I am NOT an Integer");

тип Rуспешно выведен как

Serializable, Comparable<? extends Serializable & Comparable<?>>

и a Stringподразумевает этот тип, следовательно, компиляция завершается успешно.


Чтобы явно указать тип Rи выяснить несовместимость, можно просто изменить строку кода следующим образом:

Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "not valid");
Naman
источник
Явное объявление R как <Integer> интересно и полностью отвечает на вопрос, почему это не так. Однако я все еще ищу решение без объявления явного типа. Есть идеи?
Джукзи
@jukzi Какое решение вы ищете? Код уже компилируется, если вы хотите использовать его как есть. Пример того, что вы ищете, будет хорошим, чтобы прояснить ситуацию дальше.
Наман
11

Это потому, что ваш параметр общего типа Rможет быть выведен как Object, то есть следующие компиляции:

Builder.of(MyInterface.class).with((Function<MyInterface, Object>) MyInterface::getLength, "I am NOT an Integer");
sfiss
источник
1
Точно, если OP присваивает результат метода переменной типа Integer, это будет причиной ошибки компиляции.
sepp2k
@ sepp2k За исключением того, что Builderэто только общий T, но не в R. Это Integerпросто игнорируется, если речь идет о проверке типов в сборщике.
Тило
2
Rпредполагается, чтоObject ... не совсем
Наман
@Thilo Ты прав, конечно. Я предположил, что возвращаемый тип withбудет использовать R. Конечно, это означает, что нет никакого реального способа реализовать этот метод так, чтобы он действительно использовал аргументы.
sepp2k
1
Наман, ты прав, ты и Андрей ответили на это более подробно с правильным выведенным типом. Я просто хотел дать более простое объяснение (хотя любой, кто рассматривает этот вопрос, вероятно, знает вывод типов и другие типы, кроме просто Object).
sfiss
0

Этот ответ основан на других ответах, которые объясняют, почему он не работает должным образом.

РЕШЕНИЕ

Следующий код решает проблему путем разбиения бифункции «с» на две беглые функции «с» и «возврат»:

class Builder<T> {
...
class BuilderMethod<R> {
  final Function<T, R> getter;

  BuilderMethod(Function<T, R> getter) {
    this.getter = getter;
  }

  Builder<T> returning(R returnValue) {
    return Builder.this.with(getter, returnValue);
  }
}

<R> BuilderMethod<R> with(Function<T, R> getter) {
  return new BuilderMethod<>(getter);
}
...
}

MyInterface z = Builder.of(MyInterface.class).with(MyInterface::getLength).returning(1L).with(MyInterface::getNullLength).returning(null).build();
System.out.println("length:" + z.getLength());

// YIPPIE COMPILATION ERRROR:
// The method returning(Long) in the type BuilderExample.Builder<BuilderExample.MyInterface>.BuilderMethod<Long> is not applicable for the arguments (String)
MyInterface zz = Builder.of(MyInterface.class).with(MyInterface::getLength).returning("NOT A NUMBER").build();
System.out.println("length:" + zz.getLength());

(немного незнакомо)

jukzi
источник
см. также stackoverflow.com/questions/58376589 для прямого решения
jukzi