Warum wird der Lambda-Rückgabetyp zur Kompilierungszeit nicht überprüft?


38

Die verwendete Methodenreferenz hat den Rückgabetyp Integer. Im Stringfolgenden Beispiel ist jedoch eine Inkompatibilität zulässig.

Wie kann die Methodendeklaration withkorrigiert werden, um den Methodenreferenztyp ohne manuelles Casting sicher zu machen?

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: Ein typsicherer, aber generischer Builder.

Ich habe versucht, einen generischen Builder ohne Annotation Processing (Autovalue) oder Compiler Plugin (Lombok) zu implementieren.

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
überraschendes Verhalten. Aus Interesse: Ist es dasselbe, wenn Sie für den Builder ein classanstelle eines verwenden interface?
GameDroids

Warum ist das nicht akzeptabel? Im ersten Fall geben Sie den Typ des nicht an getLength, daher kann er so angepasst werden, dass er Object(oder Serializable) dem String-Parameter entspricht.
Thilo

1
Ich könnte mich irren, aber ich denke, Ihre Methode withist Teil des Problems, wenn es zurückkehrt null. Bei Durchführung des Verfahrens with()nach tatsächlich die Funktion der Verwendung von RTyp als die gleiche Raus dem Parameter erhalten Sie den Fehler. Zum Beispiel<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroids

2
jukzi, vielleicht sollten Sie Code bereitstellen oder eine Erklärung über das, was Ihr mit Methode sollte eigentlich tun und warum müssen Sie Rsein Integer. Dazu müssen Sie uns zeigen, wie Sie den Rückgabewert verwenden möchten. Es scheint, dass Sie eine Art Builder-Muster implementieren möchten, aber ich kann ein gemeinsames Muster oder Ihre Absicht nicht erkennen.
sfiss

1
Vielen Dank. Ich dachte auch darüber nach, die vollständige Initialisierung zu überprüfen. Aber da ich keine Möglichkeit sehe, dies zur Kompilierungszeit zu tun, halte ich mich lieber an die Standardwerte null / 0. Ich habe auch keine Ahnung, wie ich beim Kompilieren nach Nicht-Schnittstellenmethoden suchen soll. Zur Laufzeit führt die Verwendung einer Nicht-Schnittstelle wie ".with (m -> 1) .returning (1)" bereits zu einer frühen java.lang.NoSuchMethodException
jukzi

Antworten:


27

Im ersten Beispiel, MyInterface::getLengthund "I am NOT an Integer"half , die generischen Parameter zu lösen Tund Rzu MyInterfaceund Serializable & Comparable<? extends Serializable & Comparable<?>>ist.

// 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::getLengthist nicht immer ein, es Function<MyInterface, Integer>sei denn, Sie sagen dies ausdrücklich, was zu einem Fehler bei der Kompilierung führen würde, wie das zweite Beispiel gezeigt hat.

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

Diese Antwort beantwortet vollständig die Frage, warum sie anders als beabsichtigt interpretiert wird. Interessant. Klingt so, als wäre R nutzlos. Kennen Sie eine Lösung für das Problem?
Jukzi

@jukzi (1) definieren explizit Methodentyp-Parameter (hier R): Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");damit es nicht kompiliert wird, oder (2) lassen Sie es implizit auflösen und hoffentlich ohne Fehler bei der Kompilierung fortfahren
Andrew Tobilko

11

Es ist die Typinferenz, die hier ihre Rolle spielt. Betrachten Sie das Generikum Rin der Methodensignatur:

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

In dem aufgeführten Fall:

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

Der Typ von Rwird erfolgreich abgeleitet als

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

und a Stringimpliziert dies, daher ist die Kompilierung erfolgreich.


Um den Typ von explizit anzugeben Rund die Inkompatibilität herauszufinden, kann man einfach die Codezeile wie folgt ändern:

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

Die explizite Deklaration von R als <Integer> ist interessant und beantwortet die Frage, warum es schief geht, vollständig. Ich bin jedoch immer noch auf der Suche nach einer Lösung, ohne den Typ explizit zu deklarieren. Irgendeine Idee?
Jukzi

@jukzi Was für eine Lösung suchst du? Der Code wird bereits kompiliert, wenn Sie ihn so verwenden möchten. Ein Beispiel für das, wonach Sie suchen, wäre gut, um die Dinge weiter klar zu machen.
Naman

11

Dies liegt daran, dass Ihr generischer Typparameter Rals Objekt abgeleitet werden kann, dh die folgenden Kompilierungen:

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

1
Wenn OP das Ergebnis der Methode einer Variablen vom Typ Integerzuweist, tritt genau dort der Kompilierungsfehler auf.
sepp2k

@ sepp2k Nur dass das Buildernur generisch ist T, aber nicht in R. Dies Integerwird bei der Typprüfung des Builders einfach ignoriert.
Thilo

2
Rwird vermutet, dassObject ... nicht wirklich
Naman

@Thilo Du hast natürlich Recht. Ich nahm an, dass der Rückgabetyp von verwenden withwürde R. Das bedeutet natürlich, dass es keinen sinnvollen Weg gibt, diese Methode tatsächlich so zu implementieren, dass die Argumente tatsächlich verwendet werden.
sepp2k

1
Naman, Sie haben Recht, Sie und Andrew haben es mit dem richtigen abgeleiteten Typ ausführlicher beantwortet. Ich wollte nur eine einfachere Erklärung geben (obwohl jeder, der sich diese Frage ansieht, wahrscheinlich Typinferenz und andere Typen kennt als nur Object).
sfiss

0

Diese Antwort basiert auf den anderen Antworten, die erklären, warum sie nicht wie erwartet funktioniert.

LÖSUNG

Der folgende Code löst das Problem, indem die Bifunktion "mit" in zwei fließende Funktionen "mit" und "zurück" aufgeteilt wird:

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

(ist etwas unbekannt)


Siehe auch stackoverflow.com/questions/58376589 für eine direkte Lösung
jukzi
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.