Perché il tipo restituito lambda non viene verificato al momento della compilazione?


38

Il riferimento al metodo utilizzato ha il tipo restituito Integer. Ma un incompatibile Stringè consentito nel seguente esempio.

Come risolvere la withdichiarazione del metodo per rendere sicuro il tipo di riferimento del metodo senza eseguire il cast manualmente?

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

}

CASO DI UTILIZZO: un generatore sicuro ma generico.

Ho provato a implementare un generatore generico senza elaborazione delle annotazioni (autovalue) o plugin del compilatore (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
comportamento sorprendente. Per interesse: è lo stesso quando usi un classinvece di un interfaceper il costruttore?
GameDroids l'

Perché è inaccettabile? Nel primo caso, non si specifica il tipo di getLength, quindi può essere regolato per restituire Object(o Serializable) in modo che corrisponda al parametro String.
Thilo,

1
Potrei sbagliarmi, ma penso che il tuo metodo withsia parte del problema quando ritorna null. Quando si implementa il metodo with()utilizzando effettivamente il Rtipo di funzione come lo stesso Rdal parametro si ottiene l'errore. Ad esempio<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroids l'

2
jukzi, forse dovresti fornire il codice o una spiegazione su cosa dovrebbe effettivamente fare il tuo metodo with e perché devi Resserlo Integer. Per questo, è necessario mostrarci come si desidera utilizzare il valore restituito. Sembra che tu voglia implementare una sorta di modello di costruttore, ma non riesco a riconoscere un modello comune o la tua intenzione.
sfiss

1
Grazie. Ho anche pensato di verificare l'inizializzazione completa. Ma poiché non vedo alcun modo per farlo in fase di compilazione, preferisco attenermi ai valori predefiniti null / 0. Inoltre non ho idea di come verificare la presenza di metodi non di interfaccia al momento della compilazione. In fase di runtime utilizzando una non interfaccia come ".with (m -> 1) .returning (1)" risulta già un inizio java.lang.NoSuchMethodException
jukzi,

Risposte:


27

Nel primo esempio, MyInterface::getLengthe "I am NOT an Integer"contribuito a risolvere i parametri generici Te Rper MyInterfacee Serializable & Comparable<? extends Serializable & Comparable<?>>rispettivamente.

// 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::getLengthnon è sempre un a Function<MyInterface, Integer>meno che tu non lo dica esplicitamente, il che porterebbe a un errore di compilazione come mostrato nel secondo esempio.

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

Questa risposta risponde pienamente alla domanda sul perché viene interpretata se non intenzionale. Interessante. Sembra che R sia inutile. Conosci qualche soluzione al problema?
jukzi,

@jukzi (1) definisce esplicitamente i parametri del tipo di metodo (qui, R): Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");per non compilarlo, o (2) lascia che venga risolto implicitamente e si spera di procedere senza errori di compilazione
Andrew Tobilko

11

È l'inferenza del tipo che sta giocando il suo ruolo qui. Considera il generico Rnella firma del metodo:

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

Nel caso elencato:

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

il tipo di Rviene dedotto correttamente come

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

e a Stringimplica questo tipo, quindi la compilazione ha esito positivo.


Per specificare esplicitamente il tipo di Re scoprire l'incompatibilità, si può semplicemente cambiare la riga di codice come:

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

Dichiarare esplicitamente R come <Intero> è interessante e risponde pienamente alla domanda sul perché vada storto. Tuttavia sto ancora cercando una soluzione senza dichiarare esplicito il Tipo. Qualche idea?
jukzi,

@jukzi Che tipo di soluzione stai cercando? Il codice viene già compilato, se si desidera utilizzarlo così. Un esempio di ciò che stai cercando sarebbe utile per chiarire ulteriormente le cose.
Naman,

11

È perché il tuo parametro di tipo generico Rpuò essere dedotto come Object, ovvero le seguenti compilazioni:

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

1
Esattamente, se OP assegnasse il risultato del metodo a una variabile di tipo Integer, questo sarebbe dove si verifica l'errore di compilazione.
sepp2k,

@ sepp2k Solo che Builderè solo generico in T, ma non in R. Questo Integerviene semplicemente ignorato per quanto riguarda la verifica del tipo del costruttore.
Thilo,

2
Rsi deduce che siaObject ... non proprio
Naman l'

@Thilo, hai ragione, ovviamente. Ho supposto che il tipo restituito di withavrebbe usato R. Ovviamente ciò significa che non esiste un modo significativo per implementare effettivamente quel metodo in un modo che utilizza effettivamente gli argomenti.
sepp2k,

1
Naman, hai ragione, tu e Andrew avete risposto in modo più dettagliato con il tipo dedotto corretto. Volevo solo dare una spiegazione più semplice (sebbene chiunque stia guardando questa domanda probabilmente conosce l'inferenza di tipo e altri tipi oltre al semplice Object).
sfiss

0

Questa risposta si basa sulle altre risposte che spiegano perché non funziona come previsto.

SOLUZIONE

Il codice seguente risolve il problema suddividendo la bifunzione "con" in due funzioni fluenti "con" e "ritorno":

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

(è in qualche modo sconosciuto)


vedi anche stackoverflow.com/questions/58376589 per una soluzione diretta
jukzi
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.