Чому тип повернення лямбда не перевіряється під час компіляції?


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.

Я намагався реалізувати загальний конструктор без обробки анотацій (autovalue) або плагіна компілятора (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());
  }

}

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, може бути , ви повинні надати код або пояснення про те, що ваш з методом фактично повинні робити і чому ви повинні 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це не завжди, 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

@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<?>>

а а Stringмає на увазі під цим типом, отже, компіляція вдається.


Щоб чітко вказати тип Rта виявити несумісність, можна просто змінити рядок коду як:

Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "not valid");

Явне оголошення R як <Integer> є цікавим і повністю відповідає на питання, чому воно піде не так. Однак я все ще шукаю рішення, не оголошуючи Тип явним. Будь-яка ідея?
jukzi

@jukzi Яке рішення ви шукаєте? Код вже компілюється, якщо ви хочете використовувати його як його. Приклад того, що ви шукаєте, було б добре, щоб далі було зрозуміло.
Наман

11

Це тому, що для вашого загального параметра типу Rможна зробити об'єкт, тобто такі компіляції:

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

1
Точно, якби ОП призначив результат методу змінній типу 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());

(дещо незнайоме)


дивіться також stackoverflow.com/questions/58376589 про пряме рішення
jukzi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.