Pourquoi le type de retour lambda n'est-il pas vérifié au moment de la compilation?

38

La référence de méthode utilisée a un type de retour Integer. Mais un incompatible Stringest autorisé dans l'exemple suivant.

Comment corriger la withdéclaration de méthode pour sécuriser le type de référence de méthode sans transtyper manuellement?

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

}

USE CASE: un générateur de type sûr mais générique.

J'ai essayé d'implémenter un générateur générique sans traitement d'annotation (autovalue) ou plugin de compilation (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());
  }

}
jukzi
la source
1
comportement surprenant. Par intérêt: est-ce la même chose lorsque vous utilisez un classau lieu d'un interfacepour le constructeur?
GameDroids
Pourquoi est-ce inacceptable? Dans le premier cas, vous ne donnez pas le type du getLength, il peut donc être ajusté pour retourner Object(ou Serializable) pour correspondre au paramètre String.
Thilo
1
Je me trompe peut-être, mais je pense que votre méthode withfait partie du problème à son retour null. Lors de l'implémentation de la méthode with()en utilisant réellement le Rtype de la fonction comme le même à Rpartir du paramètre, vous obtenez l'erreur. Par exemple<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroids
2
jukzi, vous devriez peut-être fournir du code ou une explication sur ce que votre méthode with devrait réellement faire et pourquoi vous devez l' Rêtre Integer. Pour cela, vous devez nous montrer comment vous souhaitez utiliser la valeur de retour. Il semble que vous souhaitiez implémenter une sorte de modèle de générateur, mais je ne peux pas reconnaître un modèle commun ou votre intention.
sfiss
1
Merci. J'ai également pensé à vérifier l'initialisation complète. Mais comme je ne vois aucun moyen de le faire au moment de la compilation, je préfère m'en tenir aux valeurs par défaut null / 0. Je n'ai également aucune idée de la façon de vérifier les méthodes sans interface au moment de la compilation. Lors de l'exécution, l'utilisation d'une non interface telle que ".with (m -> 1) .returning (1)" entraîne déjà une première java.lang.NoSuchMethodException
jukzi

Réponses:

27

Dans le premier exemple, MyInterface::getLengthet a "I am NOT an Integer"aidé à résoudre les paramètres génériques Tet Rà MyInterfaceet Serializable & Comparable<? extends Serializable & Comparable<?>>respectivement.

// 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::getLengthn'est pas toujours un Function<MyInterface, Integer>sauf si vous le dites explicitement, ce qui entraînerait une erreur de compilation comme le montre le deuxième exemple.

// 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");
Andrew Tobilko
la source
Cette réponse répond pleinement à la question de savoir pourquoi elle est interprétée autre que l'intention. Intéressant. On dirait que R est inutile. Connaissez-vous une solution au problème?
jukzi
@jukzi (1) définit explicitement les paramètres de type de méthode (ici, R): Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");pour qu'il ne soit pas compilé, ou (2) qu'il soit résolu implicitement et, espérons-le, sans erreur de compilation
Andrew Tobilko
11

C'est l'inférence de type qui joue son rôle ici. Considérez le générique Rdans la signature de la méthode:

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

Dans le cas indiqué:

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

le type de Rest inféré avec succès comme

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

et a Stringimplique par ce type, donc la compilation réussit.


Pour spécifier explicitement le type de Ret découvrir l'incompatibilité, on peut simplement changer la ligne de code comme:

Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "not valid");
Naman
la source
Déclarer explicitement R comme <Entier> est intéressant et répond pleinement à la question de savoir pourquoi il se passe mal. Cependant je cherche toujours une solution sans déclarer le type explicite. Une idée?
jukzi
@jukzi Quel type de solution recherchez-vous? Le code compile déjà, si vous souhaitez l'utiliser comme tel. Un exemple de ce que vous recherchez serait bon pour clarifier les choses.
Naman
11

C'est parce que votre paramètre de type générique Rpeut être inféré comme étant Object, c'est-à-dire que les compilations suivantes:

Builder.of(MyInterface.class).with((Function<MyInterface, Object>) MyInterface::getLength, "I am NOT an Integer");
sfiss
la source
1
Exactement, si OP affectait le résultat de la méthode à une variable de type Integer, ce serait là que l'erreur de compilation se produit.
sepp2k
@ sepp2k Sauf que le Builderest uniquement générique dans T, mais pas dans R. Ceci Integerest simplement ignoré en ce qui concerne la vérification de type du constructeur.
Thilo
2
Rest supposé êtreObject ... pas vraiment
Naman
@Thilo Vous avez raison, bien sûr. J'ai supposé que le type de retour withserait utilisé R. Bien sûr, cela signifie qu'il n'y a aucun moyen significatif d'implémenter cette méthode d'une manière qui utilise réellement les arguments.
sepp2k
1
Naman, vous avez raison, vous et Andrew y avez répondu plus en détail avec le type inféré correct. Je voulais juste donner une explication plus simple (bien que quiconque regarde cette question connaisse probablement l'inférence de type et d'autres types que juste Object).
sfiss
0

Cette réponse est basée sur les autres réponses qui expliquent pourquoi cela ne fonctionne pas comme prévu.

SOLUTION

Le code suivant résout le problème en divisant la bifonction "avec" en deux fonctions fluides "avec" et "renvoyant":

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

(est peu familier)

jukzi
la source
voir aussi stackoverflow.com/questions/58376589 pour une solution directe
jukzi