¿Por qué no se verifica el tipo de retorno lambda en tiempo de compilación?


38

La referencia del método utilizado tiene tipo de retorno Integer. Pero Stringse permite una incompatibilidad en el siguiente ejemplo.

¿Cómo arreglar la withdeclaración del método para que el tipo de referencia del método sea seguro sin la conversión 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)
  }

}

CASO DE USO: un generador de tipo seguro pero genérico.

Traté de implementar un generador genérico sin procesamiento de anotaciones (autovalor) o complemento del compilador (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
comportamiento sorprendente Fuera de interés: ¿es lo mismo cuando usas un en classlugar de un interfacepara el constructor?
GameDroids

¿Por qué es eso inaceptable? En el primer caso, no proporciona el tipo de getLength, por lo que se puede ajustar para devolver Object(o Serializable) para que coincida con el parámetro String.
Thilo

1
Podría estar equivocado, pero creo que su método withes parte del problema a medida que regresa null. Al implementar el método with()utilizando el Rtipo de función como el mismo Rdel parámetro, se obtiene el error. Por ejemplo<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroids el

2
jukzi, tal vez deberías proporcionar un código o una explicación sobre lo que tu método debería hacer realmente y por qué debes Rserlo Integer. Para esto, debe mostrarnos cómo desea utilizar el valor de retorno. Parece que desea implementar algún tipo de patrón de construcción, pero no puedo reconocer un patrón común o su intención.
sfiss

1
Gracias. También pensé en verificar la inicialización completa. Pero como no veo forma de hacerlo en tiempo de compilación, prefiero mantener los valores predeterminados nulo / 0. Tampoco tengo idea de cómo buscar métodos que no sean de interfaz en tiempo de compilación. En tiempo de ejecución, el uso de una interfaz que no sea ".with (m -> 1) .returning (1)" ya da como resultado un java.lang.NoSuchMethodException temprano
jukzi

Respuestas:


27

En el primer ejemplo, MyInterface::getLengthy "I am NOT an Integer"ayudó a resolver los parámetros genéricos Ty Ra MyInterfacey Serializable & Comparable<? extends Serializable & Comparable<?>>respectivamente.

// 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::getLengthno siempre es a Function<MyInterface, Integer>menos que usted lo diga explícitamente, lo que llevaría a un error en tiempo de compilación como lo mostró el segundo ejemplo.

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

Esta respuesta responde por completo a la pregunta de por qué se interpreta de forma distinta a la del asistente. Interesante. Parece que R es inútil. ¿Conoces alguna solución al problema?
jukzi

@jukzi (1) define explícitamente los parámetros de tipo de método (aquí, R): Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");para que no se compile, o (2) permita que se resuelva implícitamente y, con suerte, proceda sin errores en tiempo de compilación
Andrew Tobilko

11

Es la inferencia de tipo que está jugando su papel aquí. Considere el genérico Ren la firma del método:

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

En el caso de la lista:

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

el tipo de Rse infiere con éxito como

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

y a Stringimplica por este tipo, por lo tanto la compilación tiene éxito.


Para especificar explícitamente el tipo de Ry descubrir la incompatibilidad, uno simplemente puede cambiar la línea de código como:

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

Declarar explícitamente R como <Integer> es interesante y responde completamente a la pregunta de por qué sale mal. Sin embargo, todavía estoy buscando una solución sin declarar el tipo explícito. ¿Alguna idea?
jukzi

@jukzi ¿Qué tipo de solución estás buscando? El código ya se compila, si quieres usarlo como tal. Un ejemplo de lo que está buscando sería bueno para aclarar aún más las cosas.
Naman el

11

Esto se debe a que Rse puede inferir que su parámetro de tipo genérico es Object, es decir, las siguientes compilaciones:

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

1
Exactamente, si OP asignó el resultado del método a una variable de tipo Integer, allí sería donde se produce el error de compilación.
sepp2k

@ sepp2k Excepto que Buildersolo es genérico en T, pero no en R. Esto Integersolo se ignora en lo que respecta a la verificación de tipo del constructor.
Thilo

2
Rse infiere que esObject ... no realmente
Naman

@Thilo Tienes razón, por supuesto. Supuse que el tipo de retorno de withusaría R. Por supuesto, eso significa que no hay una manera significativa de implementar ese método de una manera que realmente use los argumentos.
sepp2k

1
Naman, tienes razón, tú y Andrew respondieron con más detalle con el tipo inferido correcto. Solo quería dar una explicación más simple (aunque cualquiera que lea esta pregunta probablemente conozca la inferencia de tipos y otros tipos además de Object).
sfiss

0

Esta respuesta se basa en las otras respuestas que explican por qué no funciona como se esperaba.

SOLUCIÓN

El siguiente código resuelve el problema al dividir la bifunción "con" en dos funciones fluidas "con" y "regresar":

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

(es algo desconocido)


ver también stackoverflow.com/questions/58376589 para una solución directa
jukzi
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.