ทำไมแลมบ์ดาส่งคืนชนิดไม่ตรวจสอบในเวลารวบรวม?


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

}

ใช้กรณี: ตัวสร้างชนิดที่ปลอดภัย แต่ตัวสร้างทั่วไป

ฉันพยายามใช้เครื่องมือสร้างทั่วไปโดยไม่ต้องใช้การประมวลผลคำอธิบายประกอบ (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
Thilo

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
ขอบคุณ ฉันยังคิดเกี่ยวกับการตรวจสอบการเริ่มต้นที่สมบูรณ์ แต่เนื่องจากฉันไม่เห็นวิธีที่จะทำในเวลารวบรวมฉันชอบที่จะยึดติดกับค่าเริ่มต้นเป็นโมฆะ / 0 ฉันยังไม่รู้ว่าจะตรวจสอบวิธีการที่ไม่ใช่อินเตอร์เฟสในเวลารวบรวมได้อย่างไร ที่รันไทม์โดยใช้ส่วนต่อประสานที่ไม่ใช่ ".with (m -> 1). การคืนค่า (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) ปล่อยให้มันได้รับการแก้ไขโดยปริยายและหวังว่าจะดำเนินการต่อโดยไม่มีข้อผิดพลาดในการคอมไพล์
Andrew Tobilko

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

การประกาศ R อย่างชัดเจนในฐานะ <Integer> นั้นน่าสนใจและตอบคำถามอย่างเต็มที่ว่าทำไมมันถึงผิดพลาด อย่างไรก็ตามฉันยังคงค้นหาวิธีแก้ปัญหาโดยไม่ต้องประกาศประเภทชัดเจน ความคิดใด ๆ
jukzi

@jukzi คุณกำลังมองหาวิธีแก้ปัญหาแบบไหน? รหัสนี้ได้รวบรวมไว้แล้วหากคุณต้องการที่จะใช้มัน ตัวอย่างของสิ่งที่คุณกำลังมองหาจะเป็นการดีที่จะทำให้สิ่งต่าง ๆ ชัดเจนยิ่งขึ้น
Naman

11

เป็นเพราะพารามิเตอร์ประเภททั่วไปของคุณRสามารถอนุมานได้ว่าเป็นวัตถุเช่นคอมไพล์ดังต่อไปนี้:

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

1
ถ้า OP กำหนดผลลัพธ์ของวิธีการให้กับตัวแปรชนิดIntegerนั้นจะเกิดข้อผิดพลาดในการรวบรวม
sepp2k

@ sepp2k ยกเว้นว่าBuilderเป็นเพียงในทั่วไปแต่ไม่ได้อยู่ในT Rนี่Integerเป็นเพียงการเพิกเฉยต่อการตรวจสอบชนิดของเครื่องมือสร้างเท่านั้น
Thilo

2
Rอนุมานได้ว่าเป็นObject ... ไม่ได้จริงๆ
Naman

@Thilo คุณถูกต้องแน่นอน ผมถือว่าผลตอบแทนประเภทของใช้งานซึ่งwith Rแน่นอนซึ่งหมายความว่าไม่มีวิธีที่มีความหมายจริง ๆ ในการนำวิธีการนั้นไปใช้ในทางที่ใช้ข้อโต้แย้งจริง
sepp2k

1
ใช่คุณพูดถูกแล้วคุณกับแอนดรูว์ตอบแบบละเอียดด้วยประเภทที่ถูกต้อง ฉันแค่อยากจะให้คำอธิบายที่ง่ายกว่า (แม้ว่าใครก็ตามที่ดูคำถามนี้อาจรู้ถึงการอนุมานประเภทและประเภทอื่นที่ไม่ใช่แค่Object)
sfiss

0

คำตอบนี้ขึ้นอยู่กับคำตอบอื่น ๆ ซึ่งอธิบายว่าทำไมมันไม่ทำงานตามที่คาดไว้

สารละลาย

รหัสต่อไปนี้แก้ปัญหาโดยการแยก bifunction "กับ" เป็นสองฟังก์ชั่นคล่อง "กับ" และ "กลับ"

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.