your programing

JDK8 및 JDK10에서 삼항 연산자의 동작 차이

lovepro 2020. 12. 25. 23:36
반응형

JDK8 및 JDK10에서 삼항 연산자의 동작 차이


다음 코드를 고려하십시오.

public class JDK10Test {
    public static void main(String[] args) {
        Double d = false ? 1.0 : new HashMap<String, Double>().get("1");
        System.out.println(d);
    }
}

JDK8에서 실행할 때이 코드는 인쇄 null되지만 JDK10에서는이 코드 NullPointerException

Exception in thread "main" java.lang.NullPointerException
    at JDK10Test.main(JDK10Test.java:5)

컴파일러에 의해 생성 된 바이트 코드는 오토 박싱과 관련되고 NPE를 담당하는 것처럼 보이는 JDK10 컴파일러에 의해 생성 된 두 개의 추가 명령을 제외하고 거의 동일합니다.

15: invokevirtual #7                  // Method java/lang/Double.doubleValue:()D
18: invokestatic  #8                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;

이 동작이 JDK10의 버그입니까 아니면 동작을 더 엄격하게 만들기위한 의도적 인 변경입니까?

JDK8:  java version "1.8.0_172"
JDK10: java version "10.0.1" 2018-04-17

나는 이것이 수정 된 것처럼 보이는 버그라고 생각합니다. NullPointerExceptionJLS에 따르면 던지는 것이 올바른 행동 인 것 같습니다.

여기서 진행되는 것은 버전 8에서 어떤 이유로 컴파일러가 실제 형식 인수가 아닌 메서드의 반환 형식에서 언급 한 형식 변수의 경계를 고려했기 때문이라고 생각합니다. 즉, ...get("1")return 이라고 생각 합니다 Object. 이것은 방법의 삭제 또는 다른 이유를 고려하고 있기 때문일 수 있습니다.

동작은 §15.26get 에서 발췌 한 아래 발췌 한대로 메서드 의 반환 유형에 따라 달라져야합니다 .

  • 두 번째 및 세 번째 피연산자식이 모두 숫자 식이면 조건식은 숫자 조건식입니다.

    조건부 분류를 위해 다음 표현식은 숫자 표현식입니다.

    • […]

    • 선택한 가장 구체적인 메서드 (§15.12.2.5)에 숫자 형식으로 변환 할 수있는 반환 형식이있는 메서드 호출 식 (§15.12)입니다.

      일반 메서드의 경우 메서드의 형식 인수를 인스턴스화하기 전의 형식입니다.

    • […]

  • 그렇지 않으면 조건식은 참조 조건식입니다.

[…]

숫자 조건식의 유형은 다음과 같이 결정됩니다.

  • […]

  • 두 번째 및 세 번째 피연산자 중 하나가 기본 유형 T이고 다른 유형이 복싱 변환 (§5.1.7)을에 적용한 결과 T인 경우 조건식의 유형은입니다 T.

즉, 두 표현식이 모두 숫자 유형으로 변환 가능하고 하나는 기본이고 다른 하나는 박스형이면 삼항 조건의 결과 유형은 기본 유형입니다.

(표 15.25-C는 또한 삼항 표현의 유형 boolean ? double : Double이 실제로 double, 즉 unboxing 및 throwing이 정확하다는 것을 의미합니다.)

메소드의 반환 유형이 get숫자 유형으로 변환되지 않는 경우 삼항 조건은 "참조 조건식"으로 간주되어 unboxing이 발생하지 않습니다.

또한 "제네릭 메서드의 경우 메서드의 형식 인수를 인스턴스화하기 전의 형식입니다" 라는 메모 가 우리의 경우에 적용되지 않아야한다고 생각합니다. Map.get유형 변수를 선언하지 않으므로 JLS의 정의에 의한 제네릭 메서드가 아닙니다 . 그러나이 메모 Java 9 추가 되었으므로 ( 유일한 변경 사항 은 JLS8 참조 ) 오늘날 우리가보고있는 동작과 관련이있을 수 있습니다.

의 경우 HashMap<String, Double>의 반환 유형은 get 이어야 합니다 Double.

다음은 컴파일러가 실제 유형 인수가 아닌 유형 변수 경계를 고려하고 있다는 내 이론을 지원하는 MCVE입니다.

class Example<N extends Number, D extends Double> {
    N nullAsNumber() { return null; }
    D nullAsDouble() { return null; }

    public static void main(String[] args) {
        Example<Double, Double> e = new Example<>();

        try {
            Double a = false ? 0.0 : e.nullAsNumber();
            System.out.printf("a == %f%n", a);
            Double b = false ? 0.0 : e.nullAsDouble();
            System.out.printf("b == %f%n", b);

        } catch (NullPointerException x) {
            System.out.println(x);
        }
    }
}

Java 8에서 해당 프로그램의 출력 은 다음과 같습니다.

a == null
java.lang.NullPointerException

In other words, despite e.nullAsNumber() and e.nullAsDouble() having the same actual return type, only e.nullAsDouble() is considered as a "numeric expression". The only difference between the methods is the type variable bound.

There's probably more investigation that could be done, but I wanted to post my findings. I tried quite a few things and found that the bug (i.e. no unboxing/NPE) seems to only happen when the expression is a method with a type variable in the return type.


Interestingly, I've found that the following program also throws in Java 8:

import java.util.*;

class Example {
    static void accept(Double d) {}

    public static void main(String[] args) {
        accept(false ? 1.0 : new HashMap<String, Double>().get("1"));
    }
}

That shows that the compiler's behavior is actually different, depending on whether the ternary expression is assigned to a local variable or a method parameter.

(Originally I wanted to use overloads to prove the actual type that the compiler is giving to the ternary expression, but it doesn't look like that's possible given the above difference. It's possible there's still another way that I haven't thought of, though.)


JLS 10 doesn't seem to specify any changes to the conditional operator, but I have a theory.

According to JLS 8 and JLS 10, if the second expression (1.0) is of type double and the third (new HashMap<String, Double>().get("1")) is of type Double, then the result of the conditional expression is of type double. The JVM in Java 8 seems to be smart enough to know that, because you're returning a Double, there's no reason to first unbox the result of HashMap#get to a double and then box it back to a Double (because you specified Double).

To prove this, change Double to double in your example, and a NullPointerException is thrown (in JDK 8); this is because the unboxing is now occuring, and null.doubleValue() obviously throws a NullPointerException.

double d = false ? 1.0 : new HashMap<String, Double>().get("1");
System.out.println(d); // Throws a NullPointerException

It seems that this was changed in 10, but I can't tell you why.

ReferenceURL : https://stackoverflow.com/questions/50769880/difference-in-behaviour-of-the-ternary-operator-on-jdk8-and-jdk10

반응형