Tại sao loại trả về lambda không được kiểm tra tại thời điểm biên dịch?


38

Tham chiếu phương thức được sử dụng có kiểu trả về Integer. Nhưng một không tương thích Stringđược cho phép trong ví dụ sau.

Làm cách nào để sửa withkhai báo phương thức để có được kiểu tham chiếu phương thức an toàn mà không cần truyền thủ công?

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

}

SỬ DỤNG CASE: một loại Builder an toàn nhưng chung chung.

Tôi đã cố gắng thực hiện một trình xây dựng chung mà không cần xử lý chú thích (autovalue) hoặc trình biên dịch plugin (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
hành vi đáng ngạc nhiên. Không quan tâm: nó có giống nhau khi bạn sử dụng classthay vì một interfacecho người xây dựng không?
GameDroids

Tại sao điều đó không được chấp nhận? Trong trường hợp đầu tiên, bạn không đưa ra loại getLength, vì vậy nó có thể được điều chỉnh để trả về Object(hoặc Serializable) để khớp với tham số Chuỗi.
Thilo

1
Tôi có thể nhầm, nhưng tôi nghĩ phương pháp của bạn withlà một phần của vấn đề khi nó trở lại null. Khi thực hiện phương thức with()bằng cách thực sự sử dụng Rkiểu của hàm giống Rvới tham số bạn gặp lỗi. Ví dụ<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroids

2
jukzi, có lẽ bạn nên cung cấp mã hoặc giải thích về những gì phương thức của bạn thực sự nên làm và lý do tại sao bạn cần phải Rnhư vậy Integer. Đối với điều này, bạn cần cho chúng tôi thấy bạn muốn sử dụng giá trị trả về như thế nào. Có vẻ như bạn muốn triển khai một số kiểu mẫu xây dựng, nhưng tôi không thể nhận ra một mẫu chung hoặc ý định của bạn.
sfiss

1
Cảm ơn. Tôi cũng nghĩ về việc kiểm tra khởi tạo hoàn chỉnh. Nhưng vì tôi thấy không có cách nào để làm điều đó vào thời gian biên dịch nên tôi thích gắn bó với các giá trị mặc định null / 0. Tôi cũng không có ý tưởng làm thế nào để kiểm tra các phương thức giao diện không tại thời gian biên dịch. Trong thời gian chạy bằng cách sử dụng một giao diện không như ".with (m -> 1) .return (1)" đã dẫn đến một java.lang.NoSuchMethodException sớm
jukzi

Câu trả lời:


27

Trong ví dụ đầu tiên, MyInterface::getLength"I am NOT an Integer"đã góp phần giải quyết các thông số chung chung TRđể MyInterfaceSerializable & Comparable<? extends Serializable & Comparable<?>>tương ứng.

// 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::getLengthkhông phải luôn luôn là một Function<MyInterface, Integer>trừ khi bạn nói rõ ràng như vậy, điều này sẽ dẫn đến lỗi thời gian biên dịch như ví dụ thứ hai cho thấy.

// 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");

Câu trả lời này trả lời đầy đủ câu hỏi tại sao nó được giải thích khác sau đó có ý định. Hấp dẫn. Âm thanh như R là vô dụng. Bạn có biết giải pháp nào cho vấn đề này không?
jukzi

@jukzi (1) xác định rõ ràng các tham số loại phương thức (ở đây, R): Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");để làm cho nó không được biên dịch hoặc (2) để nó được giải quyết ngầm và hy vọng tiến hành không có lỗi thời gian biên dịch
Andrew Tobilko

11

Đây là loại suy luận đang đóng vai trò của nó ở đây. Xem xét chung Rtrong chữ ký phương thức:

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

Trong trường hợp như được liệt kê:

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

loại Rđược suy ra thành công là

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

và a Stringkhông ngụ ý bởi loại này, do đó quá trình biên dịch thành công.


Để chỉ định rõ ràng loại Rvà tìm ra sự không tương thích, người ta có thể chỉ cần thay đổi dòng mã là:

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

Khai báo rõ ràng R là <Integer> rất thú vị và trả lời đầy đủ câu hỏi tại sao nó sai. Tuy nhiên tôi vẫn đang tìm kiếm một giải pháp mà không khai báo Kiểu rõ ràng. Bất kỳ ý tưởng?
jukzi

@jukzi Bạn đang tìm loại giải pháp nào? Mã đã biên dịch, nếu bạn muốn sử dụng nó như nó. Một ví dụ về những gì bạn đang tìm kiếm sẽ tốt để làm cho mọi thứ rõ ràng hơn.
Naman

11

Đó là bởi vì tham số loại chung của bạn Rcó thể được suy ra là Object, tức là các biên dịch sau:

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

1
Chính xác, nếu OP gán kết quả của phương thức cho một biến kiểu Integer, đó sẽ là nơi xảy ra lỗi biên dịch.
sepp2k

@ sepp2k Ngoại trừ việc Builderchỉ chung chung trong T, nhưng không phải trong R. Điều này Integerchỉ bị bỏ qua khi có liên quan đến việc kiểm tra kiểu trình xây dựng.
Thilo

2
Rđược suy luận làObject ... không thực sự
Naman

@Thilo Bạn nói đúng, tất nhiên. Tôi giả sử kiểu trả về withsẽ sử dụng R. Tất nhiên điều đó có nghĩa là không có cách có ý nghĩa để thực sự thực hiện phương pháp đó theo cách thực sự sử dụng các đối số.
sepp2k

1
Naman, bạn nói đúng, bạn và Andrew đã trả lời chi tiết hơn với loại suy luận chính xác. Tôi chỉ muốn đưa ra một lời giải thích đơn giản hơn (mặc dù bất cứ ai nhìn vào câu hỏi này có thể biết suy luận kiểu và các loại khác hơn là chỉ Object).
sfiss

0

Câu trả lời này dựa trên các câu trả lời khác giải thích tại sao nó không hoạt động như mong đợi.

GIẢI PHÁP

Đoạn mã sau giải quyết vấn đề bằng cách chia b nhị phân "với" thành hai hàm lưu loát "với" và "trả về":

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

(có phần lạ lẫm)


xem thêm stackoverflow.com/questions/58376589 để biết giải pháp trực tiếp
jukzi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.