컴파일 타임에 람다 리턴 유형이 검사되지 않는 이유는 무엇입니까?


38

사용 된 메소드 참조에는 리턴 유형이 Integer있습니다. 그러나 String다음 예 에서는 호환되지 않습니다 .

with수동으로 캐스팅하지 않고 메소드 참조 유형을 안전하게 얻기 위해 메소드 선언 을 수정하는 방법 은 무엇입니까?

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

}

사용 사례 : 형식이 안전하지만 일반 빌더입니다.

주석 처리 (자동 값) 또는 컴파일러 플러그인 (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
놀라운 행동. 관심의 부족 : 당신이 사용하는 경우는 동일 class대신의를 interface빌더에 대한?
GameDroids

왜 받아 들일 수 없습니까? 첫 번째 경우의 유형을 지정하지 않으므로 String 매개 변수와 일치 getLength하도록 리턴 Object(또는 Serializable)하도록 조정할 수 있습니다 .
Thilo

1
나는 잘못 생각할 수도 있지만 with반환하는 메소드 가 문제의 일부 라고 생각 합니다 null. with()실제로 매개 변수 R와 동일한 함수 유형을 사용하여 메서드 를 구현 R하면 오류가 발생합니다. 예를 들어<R> R with(Function<T, R> getter, T input, R returnValue) { return getter.apply(input); }
GameDroids

2
jukzi, 아마도 당신은 with 메소드가 실제로 무엇을해야하고 왜 필요한지 R에 대한 코드 또는 설명을 제공해야 할 것 Integer입니다. 이를 위해 반환 값을 어떻게 활용하고 싶은지 알려주세요. 일종의 빌더 패턴을 구현하고 싶지만 일반적인 패턴이나 의도를 인식 할 수 없습니다.
sfiss

1
감사. 또한 완전한 초기화 확인에 대해 생각했습니다. 그러나 컴파일 타임에 할 수있는 방법이 없기 때문에 기본값 null / 0을 선호합니다. 컴파일 타임에 인터페이스가 아닌 메소드를 확인하는 방법도 모릅니다. 런타임시와 같은 인터페이스를 사용하여 비 ".with (m -> 1) .returning (1)"초기 java.lang.NoSuchMethodException 이미 결과
jukzi

답변:


27

첫 번째 예에서 MyInterface::getLength"I am NOT an Integer"일반 매개 변수를 해결하는 데 도움 TRMyInterfaceSerializable & Comparable<? extends Serializable & Comparable<?>>각각.

// 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::getLengthFunction<MyInterface, Integer>명시 적으로 말하지 않는 한 항상 그렇지는 않습니다 . 두 번째 예에서 볼 수 있듯이 컴파일 타임 오류가 발생합니다.

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

이 답변은 왜 다른 의도와 다른 의미로 해석되는지에 대한 질문에 대한 답입니다. 흥미 롭군 R 같은 소리는 쓸모가 없습니다. 문제에 대한 해결책을 알고 있습니까?
jukzi

@jukzi (1) 메소드 타입 파라미터를 명시 적으로 정의한다 (여기서, R) : Builder.of(MyInterface.class).<Integer>with(MyInterface::getLength, "I am NOT an Integer");컴파일하지 않도록하거나, (2) 암시 적으로 해결되고 컴파일 타임 오류없이 희망적으로 진행되도록
하라

11

여기서 역할을 수행하는 형식 유추입니다. R메소드 서명 의 일반 을 고려하십시오 .

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

나열된 경우 :

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

의 유형은 다음 R과 같이 성공적으로 추론됩니다.

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

a String는이 유형을 의미하므로 컴파일이 성공합니다.


유형을 명시 적으로 지정하고 R비 호환성을 찾으 려면 코드 행을 다음과 같이 간단히 변경하면됩니다.

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

R을 <정수>로 명시 적으로 선언하는 것은 흥미롭고 왜 그것이 잘못되었는지에 대한 질문에 완전히 대답합니다. 그러나 나는 Type 명시 적을 선언하지 않고 여전히 솔루션을 찾고 있습니다. 어떤 아이디어?
jukzi

@jukzi 어떤 종류의 솔루션을 찾고 있습니까? 코드를 사용하려는 경우 이미 컴파일됩니다. 당신이 찾고있는 것의 예는 일을 더 명확하게하는 것이 좋습니다.
Naman

11

제네릭 형식 매개 변수 R는 Object 일 수 있습니다. 즉, 다음과 같은 컴파일입니다.

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

1
정확하게 말해서 OP가 메소드의 결과를 유형의 변수에 지정 Integer하면 컴파일 오류가 발생합니다.
sepp2k

@ (가) 점을 제외하고 sepp2k Builder에만 일반에 T에 있지만 R. 이것은 Integer단지까지 유형 검사 빌더에 관한 한 무시되고있다.
Thilo

2
R로 추정되어Object 정말 ...
나만

@Thilo 물론입니다. 의 반환 유형을 with사용 한다고 가정했습니다 R. 물론 이는 실제로 인수를 사용하는 방식으로 해당 메소드를 실제로 구현할 의미있는 방법이 없음을 의미합니다.
sepp2k

1
나만, 네 말이 맞아. 앤드류가 올바른 유추 유형으로 더 자세히 대답했다. 나는 단지 더 간단한 설명을하고 싶었습니다 (이 질문을 보는 사람은 아마도 형식 유추와 다른 유형을 알고있을 것입니다 Object).
sfiss

0

이 답변은 예상대로 작동하지 않는 이유를 설명하는 다른 답변을 기반으로합니다.

해결책

다음 코드는 "with"기능을 "with"및 "returning"이라는 두 가지 유창한 기능으로 분할하여 문제를 해결합니다.

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

(약간 익숙하지 않습니다)


또한 참조 stackoverflow.com/questions/58376589을 직접 솔루션
jukzi
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.