Por que o tipo de retorno lambda não é verificado no tempo de compilação?

38

A referência do método usado possui tipo de retorno Integer. Mas um incompatível Stringé permitido no exemplo a seguir.

Como corrigir a withdeclaração do método para obter o tipo de referência do método seguro sem transmitir 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 DE USO: um Construtor seguro, mas genérico, do tipo.

Tentei implementar um construtor genérico sem processamento de anotação (autovalue) ou plug-in do 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());
  }

}
jukzi
fonte
11
comportamento surpreendente. Fora de interesse: é o mesmo quando você usa um em classvez de um interfacepara o construtor?
GameDroids 11/11/19
Por que isso é inaceitável? No primeiro caso, você não fornece o tipo de getLength, para que possa ser ajustado para retornar Object(ou Serializable) para corresponder ao parâmetro String.
Thilo
11
Posso estar enganado, mas acho que seu método withfaz parte do problema à medida que retorna null. Ao implementar o método with(), na verdade, usando o Rtipo da função como o mesmo Rdo parâmetro, você obtém o erro. Por exemplo<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroids 11/10/19
2
jukzi, talvez você deva fornecer código ou uma explicação sobre o que seu método with deve realmente fazer e por que você precisa Rser Integer. Para isso, você precisa nos mostrar como deseja utilizar o valor de retorno. Parece que você deseja implementar algum tipo de padrão de construtor, mas não consigo reconhecer um padrão comum ou sua intenção.
Sfiss # 11/19
11
Obrigado. Também pensei em verificar a inicialização completa. Mas como não vejo como fazê-lo em tempo de compilação, prefiro ficar com os valores padrão null / 0. Também não tenho idéia de como verificar se há métodos de interface em tempo de compilação. Em tempo de execução usando uma interface não como ".com (m -> 1) .returning (1)" já resulta em um java.lang.NoSuchMethodException cedo
jukzi

Respostas:

27

No primeiro exemplo, MyInterface::getLengthe "I am NOT an Integer"ajudou a resolver os parâmetros genéricos Te Rpara MyInterfacee 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::getLengthnem sempre é um, a Function<MyInterface, Integer>menos que você o diga explicitamente, o que levaria a um erro em tempo de compilação, como o segundo exemplo mostrou.

// 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
fonte
Esta resposta responde totalmente à pergunta por que ela é interpretada além da intenção. Interessante. Parece que R é inútil. Você conhece alguma solução para o problema?
Jukzi 11/11/19
@jukzi (1) definir explicitamente os parâmetros de tipo de método (aqui, R): Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");para torná-lo não compilar, ou (2) deixá-lo fica resolvido, implicitamente, e espero continuar com nenhum erro de tempo de compilação
Andrew Tobilko
11

É a inferência de tipo que está desempenhando seu papel aqui. Considere o genérico Rna assinatura do método:

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

No caso conforme listado:

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

o tipo de Ré inferido com sucesso como

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

e a Stringimplica nesse tipo, portanto a compilação é bem-sucedida.


Para especificar explicitamente o tipo de Re descobrir a incompatibilidade, pode-se simplesmente alterar a linha de código como:

Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "not valid");
Naman
fonte
Declarar explicitamente R como <Integer> é interessante e responde totalmente à pergunta por que isso dá errado. No entanto, ainda estou procurando uma solução sem declarar o tipo explícito. Qualquer ideia?
Jukzi 11/11/19
@jukzi Que tipo de solução você está procurando? O código já é compilado, se você quiser usá-lo como ele. Um exemplo do que você está procurando seria bom para esclarecer ainda mais as coisas.
Naman
11

Isso ocorre porque seu parâmetro de tipo genérico Rpode ser deduzido como Object, ou seja, as seguintes compilações:

Builder.of(MyInterface.class).with((Function<MyInterface, Object>) MyInterface::getLength, "I am NOT an Integer");
sfiss
fonte
11
Exatamente, se OP atribuísse o resultado do método a uma variável do tipo Integer, seria onde o erro de compilação ocorre.
sepp2k
@ sepp2k Exceto que o arquivo Builderé apenas genérico T, mas não o é R. Isso Integerestá sendo ignorado no que diz respeito à verificação de tipo do construtor.
Thilo
2
Ré inferida a serObject ... não realmente
Naman
@ Thilo Você está certo, é claro. Presumi que o tipo de retorno withusaria R. É claro que isso significa que não há uma maneira significativa de realmente implementar esse método de uma maneira que realmente use os argumentos.
sepp2k
11
Naman, você está certo, você e Andrew responderam com mais detalhes com o tipo inferido correto. Eu só queria dar uma explicação mais simples (embora alguém que esteja olhando para essa pergunta provavelmente conheça inferência de tipos e outros tipos além de apenas Object).
Sfiss # 11/19
0

Esta resposta é baseada em outras respostas que explicam por que não funciona conforme o esperado.

SOLUÇÃO

O código a seguir resolve o problema dividindo a bifunção "com" em duas funções fluentes "com" e "retornando":

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

(é um pouco estranho)

jukzi
fonte
ver também stackoverflow.com/questions/58376589 para uma solução direta
jukzi