Mengapa tipe pengembalian lambda tidak dicentang pada waktu kompilasi?


38

Referensi metode yang digunakan memiliki tipe kembali Integer. Tapi tidak cocokString diizinkan dalam contoh berikut.

Bagaimana cara memperbaiki withdeklarasi metode untuk mendapatkan tipe referensi metode yang aman tanpa casting secara manual?

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

}

KASUS PENGGUNAAN: jenis Builder yang aman tetapi generik.

Saya mencoba menerapkan pembangun generik tanpa pemrosesan anotasi (nilai otomatis) atau plugin kompiler (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
perilaku yang mengejutkan. Tidak menarik: apakah sama ketika Anda menggunakan classbukan interfaceuntuk pembangun?
GameDroids

Mengapa itu tidak bisa diterima? Dalam kasus pertama, Anda tidak memberikan tipe getLength, sehingga dapat disesuaikan untuk mengembalikan Object(atau Serializable) agar sesuai dengan parameter String.
Thilo

1
Saya mungkin salah, tapi saya pikir metode Anda withadalah bagian dari masalah saat kembali null. Saat mengimplementasikan metode with()dengan benar-benar menggunakan tipe fungsi Rsebagai sama Rdari parameter Anda mendapatkan kesalahan. Misalnya<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroid

2
jukzi, mungkin Anda harus memberikan kode atau penjelasan tentang apa yang Anda dengan metode benar-benar harus dilakukan dan mengapa Anda perlu Rmenjadi Integer. Untuk ini, Anda perlu menunjukkan kepada kami bagaimana Anda ingin memanfaatkan nilai pengembalian. Tampaknya Anda ingin menerapkan semacam pola pembangun, tetapi saya tidak dapat mengenali pola umum atau niat Anda.
sfiss

1
Terima kasih. Saya juga berpikir untuk memeriksa inisialisasi lengkap. Tetapi karena saya tidak melihat cara untuk melakukannya pada waktu kompilasi saya lebih suka tetap dengan nilai default null / 0. Saya juga tidak tahu cara memeriksa metode non antarmuka pada waktu kompilasi. Saat runtime menggunakan non interface seperti ".with (m -> 1). Returning (1)" sudah menghasilkan java.lang
awal.NoSuchMethodException

Jawaban:


27

Dalam contoh pertama, MyInterface::getLengthdan "I am NOT an Integer"membantu menyelesaikan parameter generik Tdan Rke masing MyInterface- Serializable & Comparable<? extends Serializable & Comparable<?>>masing.

// 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::getLengthtidak selalu a Function<MyInterface, Integer>kecuali jika Anda secara eksplisit mengatakannya, yang akan menyebabkan kesalahan waktu kompilasi seperti yang ditunjukkan contoh kedua.

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

Jawaban ini sepenuhnya menjawab pertanyaan mengapa itu ditafsirkan selain intendet. Menarik. Kedengarannya seperti R tidak berguna. Apakah Anda tahu solusi untuk Masalah ini?
jukzi

@ jukzi (1) secara eksplisit mendefinisikan parameter tipe metode (di sini, R): Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");untuk membuatnya tidak dikompilasi, atau (2) biarkan diselesaikan secara implisit dan mudah-mudahan dilanjutkan tanpa kesalahan waktu kompilasi
Andrew Tobilko

11

Ini jenis inferensi yang memainkan perannya di sini. Pertimbangkan generik Rdalam tanda tangan metode:

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

Dalam hal ini seperti yang tercantum:

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

jenis Rberhasil disimpulkan sebagai

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

dan Stringtidak menyiratkan dengan tipe ini, maka kompilasi berhasil.


Untuk secara eksplisit menentukan jenis Rdan menemukan ketidakcocokan, seseorang dapat dengan mudah mengubah baris kode sebagai:

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

Secara eksplisit menyatakan R sebagai <Integer> menarik dan sepenuhnya menjawab pertanyaan mengapa itu salah. Namun saya masih mencari solusi tanpa menyatakan Tipe eksplisit. Ada ide?
jukzi

@ jukzi Solusi apa yang Anda cari? Kode sudah dikompilasi, jika Anda ingin menggunakannya seperti itu. Contoh dari apa yang Anda cari akan baik untuk memperjelas hal-hal.
Naman

11

Itu karena parameter tipe generik Anda Rdapat disimpulkan sebagai Objek, yaitu kompilasi berikut:

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

1
Tepatnya, jika OP menetapkan hasil metode ke variabel tipe Integer, itu akan menjadi tempat kesalahan kompilasi terjadi.
sepp2k

@ sepp2k Kecuali bahwa Builderhanya generik dalam T, tetapi tidak dalam R. Ini Integerhanya diabaikan sejauh memeriksa jenis pembangun yang bersangkutan.
Thilo

2
Rdisimpulkan sebagaiObject ... tidak juga
Naman

@Thilo Kamu benar, tentu saja. Saya berasumsi tipe pengembalian withakan digunakan R. Tentu saja itu berarti tidak ada cara yang berarti untuk benar-benar mengimplementasikan metode itu dengan cara yang benar-benar menggunakan argumen.
sepp2k

1
Naman, Anda benar, Anda dan Andrew menjawabnya lebih terinci dengan tipe yang disimpulkan benar. Saya hanya ingin memberikan penjelasan yang lebih sederhana (walaupun siapa pun yang melihat pertanyaan ini mungkin tahu tipe inferensi dan tipe lain selain adil Object).
sfiss

0

Jawaban ini didasarkan pada jawaban lain yang menjelaskan mengapa itu tidak berfungsi seperti yang diharapkan.

LARUTAN

Kode berikut memecahkan masalah dengan memisahkan bifungsi "dengan" menjadi dua fungsi yang lancar "dengan" dan "mengembalikan":

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

(agak asing)


lihat juga stackoverflow.com/questions/58376589 untuk solusi langsung
jukzi
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.