Java로 백준 문제를 풀던 중 error가 발생했다.

직역하자면, StringBuilder의 append() method를 사용하는데, 여기에 사용되는 argument가 모호하다는 뜻이다. 위 코드를 보면, append() method의 인자로 삼항연산자를 활용하였는데, 이 부분이 문제가 되는 것이다.
Java의 경우 method overloading이 허용되기에, 동일한 이름을 가진 method 중 적절한 method를 찾아야 하는데, 이 과정에서 문제가 발생하는 것으로 보인다.
우선 위와 같이 코드를 작성하고 답안 제출을 했던 이유는 당시 JDK11을 사용하고 있었고, 성공적으로 동작했기에 제출을 했던 것이다. (필자는 백준 기본 언어 옵션을 Java 8으로 설정해 두었다.)
그래서 백준 Java version을 바꾸어 다시 제출했고, 문제는 성공적으로 해결할 수 있었다.
문제는 해결했으니, 문제가 발생한 이유에 대해서 알아보자
우선 필자는 위와 같은 sb.append( ) 에서 삼항연산자를 많이 활용한다.
boolean ans = solve();
System.out.println(ans? 1 : 0);
위와 같이 가능한 경우에 1, 불가능한 경우에 0을 출력하는 문제가 꽤 있는 것을 문제를 풀어본 사람은 알 것이다. 이런 경우에 solve( ) method는 가능/불가능을 판별하기 위함이기에 return 값을 1/0으로 바꾸고 싶지 않았고, 삼항연산자를 활용하면 쉽게 원하는 정답을 출력할 수 있기에 자주 사용해 왔다.
그래서 JDK version 문제도 있었지만, 첫 사진과 같이 별 생각 없이 문제를 제출한 것이다.
그렇다면 두 코드의 차이점은 무엇일까.
문제가 되었던 코드는 사실 아래와 같다.
import java.util.*;
public class TestFile {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
Stack<Integer> stack = new Stack<>();
...
sb.append(stack.size() > 0 ? stack.pop() : 0);
}
}
자세히 보면 두 코드의 차이점은 삼항연산자의 T/F 표현식의 type이 다르다. 첫 번째 코드는 int / int인 반면 위 예시는 `Integer / int` 임을 알 수 있다.
Java Compiler가 Integer와 int 사이 형변환을 대부분의 경우에 알아서 잘 수행해 주지만, 위 상황에서는 그렇지 않은가 싶어 우선 StringBuilder의 append( ) method를 확인해 보았다.

StringBuilder.java
StringBuilder의 append( ) method는 총 13개 이며, argument는 String, StringBuffer, CharSequence 등 문자열 관련 타입과, Object, 그리고 int, char 등 primitive type이 있다. 그 중 위 예시 코드에서 문제가 되는 상황은 Integer 타입은 append(Object o), int 타입은 append(int i) method에 해당한다고 추론하는 것으로 예상된다.
알아서 변환해 줄 것이라 생각하고 멍청한 짓을 했다.
append( ) method의 parameter를 동일하게 해주면 정상적으로 동작한다.
// Integer / Integer
sb.append(stack.size() > 0 ? stack.pop() : new Integer(0));
// int / int
sb.append(stack.size() > 0 ? (int)stack.pop() : 0);
멍청한 짓을 한 것은 맞지만, Java 11에서는 문제 없이 동작했기에 조금 더 알아보기로 했다.
여러 시도를 해 보았고, 어떤 경우에서는 에러가 발생하고 그렇지 않은 경우도 있었다. 아래는 시도해 보았던 예시들 이다.
// 1 - fine
sb.append(stack.size() > 0 ? 1 : 0);
// 2- ERROR
sb.append(stack.size() > 0 ? stack.pop() : 0);
// 3 - fine if exists
sb.append(stack.size() > 1 ? stack.pop() : stack.pop());
// 4 - fine ?????
sb.append(stack.size() > 0 ? 0 : stack.pop());
4 번 예시를 보면, 2번 예시와 달리 순서만 바꾸었을 뿐인데 에러가 발생하지 않았다.
테스트를 위해 코드를 간략하게 작성하다 보니, 코드 상 stack.size() > 0은 항상 true이기 때문에 IDE 또는 Java Compiler에 의해 true에 해당하는 표현식에 영향을 받는가 싶어 반대로 시도해 보았다.
// 5 - fine
sb.append(stack.size() < 0 ? 0 : stack.pop());
// 6 - ERROR
sb.append(stack.size() < 0 ? stack.pop() : 0);
결과는 같았다.
T/F 표현식에 해당하는 타입에 따라 Error가 발생 하기도 하며 그렇지 않기도 한다. True에 해당하는 표현식이 int 타입인 경우 정상적으로 동작하며, Integer 타입인 경우 에러가 발생하는 것 같아 조금 더 극단적으로 테스트 해 보았다.
// 7 - ERROR
sb.append(true ? stack.pop() : 0);
// 8 - fine
sb.append(true ? 0 : stack.pop());
// 9 - ERROR
sb.append(false ? stack.pop() : 0);
// 10 - fine
sb.append(false ? 0 : stack.pop());
상당히 혼란스럽다
조건의 여부에 관계없이 True에 해당하는 표현식이 int 타입인 경우 정상적으로 동작하며, Integer 타입인 경우 에러가 발생하는 것을 확인할 수 있다.
여러 overloading된 method 중 적절한 method를 선정하는 과정에서 삼항연산자의 True / False 표현식에 해당하는 부분이 영향을 미치는 것 같다는 예상을 할 수 있게 되었다.
근데 Java11에서는 왜 정상적으로 작동하는지 확인하기 위해 우선 Java11의 StringBuilder 코드를 확인해 보았으나, @HotSpotIntrinsicCandidate annotation 외 다른 점을 찾지 못했다.
삼항연산자의 결과가 어떻게 나오는지 알아보았다.
JLS(Java Language Specification)에 따르면 Java8과 Java11 모두 삼항연산자의 둘, 셋째 expression의 타입에 대해 (Integer, int) 또는 (int, Integer)인 경우 모두 결과 타입은 int가 된다고 한다.

Table 15.25-A

Table 15.25-C
출처 (https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.25)
Java는 Overloading 된 여러 method에 대해 실제 사용할 method를 선정하는 방식이 있다. 타입 변환이나 class의 다형성 등 여러 사항을 고려해 선정한다.
우선 java가 여러 method 중 적합한 method를 선정하는 방식은 아래와 같다.
Boxing/Unboxing, 또는 varags를 포함한 method를 고려하지 않고 호출할 method를 찾는다.Boxing/Unboxing을 허용하지만, varags를 포함한 method는 고려하지 않고 method를 찾는다.Boxing/Unboxing 및 varargs를 모두 허용해 method를 찾는다.polymorphism이 관여하는 것 같다. 위 과정을 대략 요약하자면,
Primitive type widening → Boxing/Unboxing → Varargs → Polymorphism
순서로 이루어 진다고 볼 수 있다.
그렇다면, 문제가 되었던 상황에서 Integer, int 모두 append(int i) method를 호출해야 하는데, ambiguity가 발생한다는 것이 조금 이상하다. 삼항연산자의 결과는 항상 int가 되어야 하며, 어떠한 이유로 Integer가 된다고 하더라도, method 선정 방식에 따라 append(Object o) 가 아닌 append(int i)를 항상 호출해야 한다.
알아본 것들에 대해 간략히 정리하자면,
StringBuilder sb = new StringBuilder();
Stack<integer> stack = new Stack<Integer>();
// 1
sb.append(stack.size() > 0 ? stack.pop() : 32);
// 2
sb.append(stack.size() <= 0 ? 32 : stack.pop());
위 두 코드 모두 StringBuilder의 append(int i) method를 호출하도록 되어야 한다. 하지만, 어떠한 이유에 의해서 첫 번째 코드는 append(Object o) 와 append(int i) 를 결정하지 못해 ambiguity error가 발생하게 되었다.
정확한 이유를 찾지는 못하였지만, Java8에서 타입 추론과 관련하여 버그가 있었음을 찾았고, 삼항연산자, Overloading 등 문제 상황과 유사한 내용은 아니지만 버그와 연관이 있지 않을까 싶다. Java8 에서 Target-Type Inference 등 타입 관련 업데이트가 있었던 만큼 어떠한 버그가 있었지 않을까
언젠가 원인을 정확히 알게 된다면 다시 돌아와 글을 작성하겠다.
글 작성 후 태어나 처음으로 stackoverflow에 글을 작성해 보았다.
생각보다 빠르게 답변이 달렸는데, 결국 버그가 맞다.
JDK-8064464 버그 리포트에 나와 동일한 버그가 있었다.

[JDK-8064464] regression with type inference of conditional expression
결론은 Java8에서 Compiler의 버그가 맞으며, Java 9 부터 수정이 된, Java 8 에서 만 만날 수 있는 버그다.
버그를 찾은 것과 별개로, 위 버그 리포트는
“regression with type inference of conditional expression”
이라는 제목인데, 내가 검색한 내용은 보통, ternary operator과 method overloading 이었다. 완전히 틀렸다.
“regression” 이라는 표현은 그렇다고 치자. 검색을 위한 키워드마저 틀렸었다. 영어 공부를 해야겠다.
Java Ternary Operator seems to be inconsistantly casting Integers to ints