String 클래스의 compreTo() 메서드는 두 문자열을 character 단위로 비교한 후 다른 문자가 있으면 문자끼리 뺀 값을 리턴합니다. 결과값은 아주 간단합니다. 하지만 개인적으로 두 개의 문자열의 위치가 바뀌었을 때 어떤 값이 나올 지 유추가 안 됐습니다.
String str1 = "a"; // 97
String str2 = "b"; // 98
str1.compareTo(str2); // -1
str2.compareTo(str1); // 1
compareTo 메서드의 내부 동작 방식을 더 살펴보고 이해하여 평소에 해당 메서드를 사용할 때 조금이나마 편안하게 사용하게 되길 희망하며 글을 작성했습니다.
"aa".compareTo("ab")
를 호출한다고 가정하고 내부적으로 어떠한 동작들이 일어나는지 살펴보겠습니다.
"aa".compareTo("ab")
compareTo 의 구현부
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}
우리가 "aa".compareTo("ab")를 호출했으므로 compareTo() 메서드는 "aa" 가 가진 메서드를 호출합니다.
String 클래스는 내부적으로 value 라는 private 필드에 값을 저장합니다. "a" 는 ascii 코드로 97로 변환되고 "b"는 97로 변환이 되네요.
v1 변수는 97, 97를 나타냅니다. v2 변수는 우리가 compareTo()의 인수로 넘겨준 값, 즉 97, 98 가 담겨있습니다.
그 다음은 if 조건문에서 coder() == anotherString.coder() 을 평가합니다. coder() 메서드는 String 클래스가 가지고 있는 메서드입니다.
아래는 coder() 메서드의 내부 구현입니다. COMPACT_STRINGS 라는 정적 필드가 true이면 coder를 리턴하고 아니면 UTF16이라는 상수를 리턴합니다.
byte coder() {
return COMPACT_STRINGS ? coder : UTF16;
}
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
private final byte coder;
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
... 생략
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16 = 1;
}
COMPACT_STRINGS는 String 클래스 내부에 선언되어 있는 상수 입니다. 기본적으로 true로 초기화 되어 있습니다. coder() 메서드에서 사용하고 있는 coder도 String 클래스에 선언되어 있는 필드 입니다. 지금은 0이라는 값이 있다고 생각해주세요. UTF16 역시 마찬가지입니다.
정리해보면 coder() 메서드 COMPACT_STRINGS가 true이면 0, false 라면 1을 리턴하는 메서드 입니다. 디버거로 확인해보니 "aa"와 "ab" 모두 COMPACT_STRINGS가 true이고 0을 리턴합니다. 그러므로 3. coder() == anotherString.coder()의 분기문을 타게 됩니다.
if 분기문을 타고 들어오면 isLatin1() 의 결과값이 true 이면 StringLatin1.compareTo() 메서드의 결과값을 리턴하고 아니라면 StringUTF16.compareTo() 메서드의 결과값을 리턴합니다.
"가".compareTo("나")를 실행하면 StringUTF16.compareTo(v1, v2)가 실행됩니다.
isLatin1() 의 구현입니다. COMPACT_STRINGS가 true 이고 coder의 값과 LATIN1의 값이 같으면 true를 리턴합니다.
private boolean isLatin1() {
return COMPACT_STRINGS && coder == LATIN1;
}
앞에서도 봤듯이 COMPACT_STRINGS의 값은 true 입니다. coder 필드의 값은 0이고 LATIN1 필드의 값도 0입니다. 그러므로 true && true 가 평가되고 메서드 전체는 true를 리턴합니다. 따라서 StringLatin1.compareTo(v1, v2) 가 실행되게 됩니다.
v1, v2 매개변수는 앞에서 보았듯이 "aa", "ab"의 byte 배열 입니다.
@HotSpotIntrinsicCandidate
public static int compareTo(byte[] value, byte[] other) {
int len1 = value.length;
int len2 = other.length;
return compareTo(value, other, len1, len2);
}
매개변수로 받은 두 개의 배열의 길이를 구하고 StringLatin1 클래스에 선언된 compareTo() 메서드를 호출합니다.
StringLatin1 클래스의 compareTo 메서드 구현부 입니다. 이 메서드가 바로 "aa"와 "ab"의 결과값을 실제적으로 만들어주는 메서드입니다.
public static int compareTo(byte[] value, byte[] other, int len1, int len2) {
int lim = Math.min(len1, len2);
for (int k = 0; k < lim; k++) {
if (value[k] != other[k]) {
// 'a' - 'b'
return getChar(value, k) - getChar(other, k);
}
}
return len1 - len2;
}
우선 주어진 두 개의 배열의 길이 중 작은 값을 lim 변수에 담습니다. 우리는 두 개의 문자열의 길이가 모두 2이므로 lim 변수가 2가 되겠네요.
0 부터 lim(2) 만큼 반복문을 돌립니다.
두 개의 배열을 k 포인터로 순회하면서 값이 다를 경우 getChar() 함수를 이용해서 그 차이를 구합니다. 다른 값이 없을 경우 메서드의 끝까지 실행이되고 len1 - len2가 리턴됩니다. 이래서 같은 문자를 "aa".compareTo("aa") 했을 경우 0을 리턴하는 거네요.
전체적인 실행흐름은 살펴본 것 같습니다. 추가적으로 getChar() 메서드를 살펴볼까합니다.
0xff 는 16 진수로 10진수로 나타낼 시 255, 이진수로 나타내면 1111 1111 입니다. 이제 val[index] 가 'a', 즉 97 이라고 가정하고 계산해보겠습니다. 97은 이진수로 01100001 입니다. & 연산은 두 개의 비트가 둘 다 1일 때 1을 반환합니다.
255 : 1111 1111
97 : 0110 0001
------------------- & 연산
0110 0001 ---> 97
위의 계산 과정을 보면 결국 97의 값을 반환합니다.
public static char getChar(byte[] val, int index) {
return (char)(val[index] & 0xff);
}
String 클래스의 compareTo 메서드의 동작 방식을 한 번 살펴봤습니다. 여러가지 과정이 있었습니다. 하지만 핵심만 정리해보면 아래와 같습니다.
"a".compareTo("b")
97 - 98 = -1
"b".compareTo("a")
98 - 97 = 1
Compact Strings in Java9
Understanding the & 0xff Value in Java