Die verwendete Methodenreferenz hat den Rückgabetyp Integer
. Im String
folgenden Beispiel ist jedoch eine Inkompatibilität zulässig.
Wie kann die Methodendeklaration with
korrigiert 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());
}
}
class
anstelle eines verwendeninterface
?getLength
, daher kann er so angepasst werden, dass erObject
(oderSerializable
) dem String-Parameter entspricht.with
ist Teil des Problems, wenn es zurückkehrtnull
. Bei Durchführung des Verfahrenswith()
nach tatsächlich die Funktion der Verwendung vonR
Typ als die gleicheR
aus dem Parameter erhalten Sie den Fehler. Zum Beispiel<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
R
seinInteger
. 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.Antworten:
Im ersten Beispiel,
MyInterface::getLength
und"I am NOT an Integer"
half , die generischen Parameter zu lösenT
undR
zuMyInterface
undSerializable & Comparable<? extends Serializable & Comparable<?>>
ist.MyInterface::getLength
ist nicht immer ein, esFunction<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.quelle
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 fortfahrenEs ist die Typinferenz, die hier ihre Rolle spielt. Betrachten Sie das Generikum
R
in der Methodensignatur:In dem aufgeführten Fall:
Der Typ von
R
wird erfolgreich abgeleitet alsund a
String
impliziert dies, daher ist die Kompilierung erfolgreich.Um den Typ von explizit anzugeben
R
und die Inkompatibilität herauszufinden, kann man einfach die Codezeile wie folgt ändern:quelle
Dies liegt daran, dass Ihr generischer Typparameter
R
als Objekt abgeleitet werden kann, dh die folgenden Kompilierungen:quelle
Integer
zuweist, tritt genau dort der Kompilierungsfehler auf.Builder
nur generisch istT
, aber nicht inR
. DiesInteger
wird bei der Typprüfung des Builders einfach ignoriert.R
wird vermutet, dassObject
... nicht wirklichwith
würdeR
. Das bedeutet natürlich, dass es keinen sinnvollen Weg gibt, diese Methode tatsächlich so zu implementieren, dass die Argumente tatsächlich verwendet werden.Object
).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:
(ist etwas unbekannt)
quelle