코틀린 베이직 KB 11 동등성 비교 — == 와 === 가 자바와 뒤바뀌었다
코틀린 베이직 · 11/26

KB 11 동등성 비교 — == 와 === 가 자바와 뒤바뀌었다

gabury1고친 사람 github-actions[bot]

0. 첫 예제 — 코틀린의 a == b 는 자바의 a.equals(b) 다

자바 개발자라면 「객체는 == 로 비교하지 마라」를 한 번쯤 버그로 배웠습니다. 코틀린에서는 거꾸로 객체를 == 로 비교하는 것이 기본입니다.

같은 숫자를 담은 BigInteger 두 개를 만들어 비교했습니다.

Kotlin
import java.math.BigInteger

fun main() {
    val a = BigInteger("12345678901234567890")
    val b = BigInteger("12345678901234567890")
    println(a == b)     // true
    println(a === b)    // false
}
  • fun main() — 프로그램 시작 함수입니다. fun 은 함수를 선언하는 키워드입니다. 자바와 달리 클래스로 감싸지 않습니다
  • val — 다시 대입할 수 없는 변수입니다. 타입을 안 적으면 값에서 알아냅니다
  • BigInteger("...") — 생성자 호출입니다. 코틀린에는 new 가 없습니다. 두 줄이 객체를 하나씩 만듭니다
  • === — 등호가 셋인 연산자입니다. 자바에는 없습니다. 무엇을 하는지는 출력으로 봅니다

Big.kt 로 저장해 코틀린 컴파일러 kotlinc 로 컴파일하고 돌렸습니다. -d out 은 결과물을 out 폴더에 쓰라는 옵션입니다. 실행에 쓴 kotlin 은 코틀린 표준 라이브러리를 classpath 에 스스로 넣어 주는 명령이고, 뒤에 적은 BigKt 는 컴파일러가 클래스 밖에 쓴 함수를 담으려고 만드는 클래스입니다. JVM(자바 가상 머신)에서는 모든 메서드가 어떤 클래스에 속해야 해서 파일 이름 뒤에 Kt 를 붙인 클래스가 생깁니다.

터미널
$ kotlinc Big.kt -d out
$ kotlin -cp out BigKt

a == b 는 true 입니다. 객체가 둘인데도 그렇습니다. 등호 셋인 a === b 는 false 입니다.

같은 일을 자바로 옮겼습니다. 자바 쪽 결과물은 섞이지 않게 jout 폴더에 둡니다.

Java
import java.math.BigInteger;

public class Big {
    public static void main(String[] args) {
        BigInteger a = new BigInteger("12345678901234567890");
        BigInteger b = new BigInteger("12345678901234567890");
        System.out.println(a == b);
        System.out.println(a.equals(b));
    }
}
터미널
$ javac -d jout Big.java
$ java -cp jout Big
false
true

자바의 a == b 는 false 입니다. true 를 얻으려면 a.equals(b) 를 불러야 합니다. 두 출력을 줄 맞춰 놓으면 연산자가 뒤바뀐 것이 보입니다.

묻는 것 자바 코틀린
내용이 같은가 a.equals(b) a == b
같은 객체인가 a == b a === b

코틀린 공식 문서는 == 를 구조적 동등성(structural equality)이라 부릅니다(코틀린 문서 「Equality」). equals() 로 내용을 견주는 비교입니다.

=== 는 참조 동등성(referential equality)입니다. 두 변수가 같은 객체를 가리키는지 보는 비교, 곧 자바의 == 입니다. 이 편에서는 줄여서 참조 비교라고 부릅니다.

이 편은 이 표 한 장이 JVM 위에서 무엇이 되는지 풉니다. 널이 끼면 어떻게 되는지, 그리고 조심할 두 곳(Int? 의 === 와 배열의 ==)도 봅니다. 출력은 전부 Kotlin 2.4.10, JDK 21 에서 직접 돌린 결과입니다.

1. == 는 equals 를 부르고 === 는 부르지 않는다

「== 가 equals 에 해당한다」가 비유인지, equals 를 부르는 호출인지 확인합니다. equals 를 직접 정의하고 그 안에 출력 한 줄을 심었습니다.

Kotlin
class Money(val amount: Int) {
    override fun equals(other: Any?): Boolean {
        println("equals 호출")
        return other is Money && other.amount == amount
    }
}

fun main() {
    val x = Money(100)
    val y = Money(100)
    println(x == y)
    println(x != y)
    println(x === y)
}

Money.kt 로 저장해 돌렸습니다.

터미널
$ kotlinc Money.kt -d out
$ kotlin -cp out MoneyKt
equals 호출
true
equals 호출
false
false
  • class Money(val amount: Int) — 클래스 이름 뒤 괄호가 생성자입니다. 괄호 안에 val 을 붙인 인자는 필드와 게터를 함께 갖는 프로퍼티가 됩니다
  • override fun equals(other: Any?): Boolean — 부모의 메서드를 다시 정의할 때는 override 를 반드시 적습니다. Any 는 자바의 Object 에 해당합니다. 타입 뒤 ? 는 null 도 받는다는 표시입니다
  • other is Money — 자바의 instanceof 입니다. 이 검사를 통과한 뒤에는 캐스트 없이 other.amount 를 읽습니다
  • != — == 의 부정입니다

equals 호출 이 두 번 찍혔습니다. x == y 와 x != y 가 한 번씩 우리가 쓴 equals 를 불렀습니다. 마지막 x === y 는 equals 를 부르지 않고 false 를 냈습니다.

그러니 코틀린의 == 는 비유가 아니라 equals 를 부르는 호출입니다. 부정인 != 도 같은 equals 를 부르고 결과만 뒤집습니다. === 는 equals 를 거치지 않고 같은 객체인지만 봅니다.

우리가 쓴 equals 가 자바의 Object.equals 를 재정의한 것도 확인했습니다. JDK 에 딸린 javap 는 클래스 파일에 적힌 선언의 모양을 보여 주는 도구이고, -p 는 private 멤버까지 보여 달라는 옵션입니다. javap -p out/Money.class 로 보면 우리가 쓴 equals 는 public boolean equals(java.lang.Object); 입니다. Any? 인자가 Object 가 되어 Object.equals 를 재정의합니다.

타입이 안 맞으면 컴파일이 멈춘다

자바의 equals 는 Object 를 받으니 무엇과도 비교가 컴파일됩니다. 코틀린의 == 는 여기서 한 번 더 따집니다. Int 1 과 Long 1 을 비교했습니다.

Kotlin
fun main() {
    val i = 1
    val l = 1L
    println(i == l)  // 컴파일 오류
}
터미널
$ kotlinc Mixed.kt -d out
Mixed.kt:4:13: error: operator '==' cannot be applied to 'Int' and 'Long'.
    println(i == l)
            ^^^^^^

같은 비교를 자바로 쓰면 컴파일이 되고, 실행하면 false 가 나옵니다.

Java
public class Mixed {
    public static void main(String[] args) {
        Integer i = 1;
        Long l = 1L;
        System.out.println(i.equals(l));
    }
}
터미널
$ javac -d jout Mixed.java
$ java -cp jout Mixed
false

자바에서는 조용히 false 로 끝나는 비교를 코틀린은 컴파일 단계에서 막았습니다.

막는 범위는 좁습니다. 네 경우를 돌려 보면 두 갈래로 갈립니다.

비교한 두 쪽 결과
서로 관계없는 두 클래스 — Int 와 Long · Money 와 String 컴파일이 막힌다
한쪽이 인터페이스나 Any — Money 와 Shape? · val a: Any = 1 과 1L 통과하고 false

Shape 는 Money 가 구현하지 않은 인터페이스입니다. 한쪽이 인터페이스나 Any 이면 컴파일러가 못 막습니다.

2. 널에 안전한 ==

자바에서 a.equals(b) 는 a 가 null 이면 NullPointerException 을 냅니다. 코틀린의 == 는 null 을 넣어도 멈추지 않습니다. Money? 인자 둘에 null 을 번갈아 넣어 봅니다.

1절과 같은 Money 클래스를 두고, 함수 하나를 더 썼습니다.

Kotlin
fun check(m: Money?, n: Money?) {
    println(m == n)
    println(m == null)
}

fun main() {
    check(null, Money(100))
    check(Money(100), null)
}

Money 와 check 를 함께 담은 Nulls.kt 를 돌렸습니다.

터미널
$ kotlinc Nulls.kt -d out
$ kotlin -cp out NullsKt
false
true
equals 호출
false
false
  • m: Money? — 인자 이름 뒤에 콜론, 그 뒤에 타입을 씁니다. ? 가 붙어서 null 을 넘길 수 있습니다
  • fun check(...) — { } 로 본문을 쓴 함수에서 반환 타입을 안 적으면 돌려주는 값이 없는 함수, 곧 자바의 void 입니다

두 번의 호출이 다르게 끝났습니다.

  • 왼쪽이 null — 첫 호출은 equals 호출 없이 false 와 true 를 찍었습니다. null 에는 equals 를 부를 수 없으니 부르지 않고 답을 냈습니다
  • 오른쪽이 null — 두 번째 호출은 equals 호출 을 찍었습니다. 왼쪽 객체의 equals 에 null 이 인자로 넘어갔습니다. other is Money 가 false 라서 false 가 나왔습니다

그러니 equals 를 직접 쓸 때는 인자로 null 이 들어오는 경우를 자바와 똑같이 처리해야 합니다. other is Money 는 null 에 대해 false 이므로 그 처리를 겸합니다.

출력 다섯 줄이 말하는 규칙은 하나입니다. 왼쪽이 null 이면 오른쪽도 null 인지만 보고, 아니면 왼쪽의 equals 를 부릅니다. 둘 다 null 인 갈래도 첫 호출에 들어 있습니다. m 이 null 인 채로 m == null 을 물은 줄이 equals 없이 true 를 냈습니다.

이 규칙을 갈래로 그리면 이렇습니다.

flowchart TD
    A["m == n"] --> B{"m 이 null 인가"}
    B -->|예| C{"n 이 null 인가"}
    C -->|예| T["true"]
    C -->|아니오| F["false"]
    B -->|아니오| E["m.equals(n) 의 결과"]

자바에서 같은 비교

자바는 같은 일에 java.util.Objects.equals 를 따로 씁니다. 자바 쪽은 짧게 문자열로 보입니다. null 을 담은 static 필드 m 으로 두 방법을 차례로 불렀습니다.

Java
import java.util.Objects;

public class Nulls {
    static String m = null;

    public static void main(String[] args) {
        System.out.println(Objects.equals(m, "kotlin"));
        System.out.println(m.equals("kotlin"));
    }
}
터미널
$ javac -d jout Nulls.java
$ java -cp jout Nulls
false
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "Nulls.m" is null
	at Nulls.main(Nulls.java:8)

Objects.equals 는 false 를 냈습니다. m.equals(...) 는 실행 중에 NullPointerException 으로 멈췄습니다. 코틀린의 == 는 자바의 Objects.equals 와 거의 같은 일을 합니다.

다른 곳이 하나 있습니다. 두 인자가 같은 객체이면 Objects.equals 는 equals 를 부르지 않고 true 를 냅니다. 코틀린에서 1절의 Money 로 x == x 를 비교하면 위의 규칙대로 equals 를 불러 equals 호출 을 찍고 true 가 나왔습니다.

코틀린에서 자바 버릇대로 m.equals(...) 를 쓰면 어떨까요. 타입이 Any? 나 Money? 이면 실행까지 가지 못합니다. val m: Any? = null 에 m.equals("kotlin") 을 부른 NullCall.kt 는 error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'Any?'. 로 컴파일이 막혔습니다.

에러가 권하는 둘은 이렇습니다.

  • ?. — 안전 호출입니다. 받는 쪽이 null 이면 메서드를 부르지 않고 null 을 냅니다
  • !!. — 널 아님 단언입니다. null 이 아니라고 못박고 부르며, 그래도 null 이면 예외가 납니다

둘 다 널 안전성을 다루는 뒤에 나올 편에서 봅니다.

String? 은 예외입니다. 표준 라이브러리가 널을 받는 equals 를 따로 두었습니다. 그래서 String? 인자 m 에 null 을 넣고 m.equals("kotlin") 을 부르면 컴파일되고 false 가 나옵니다.

3. Integer 캐시 함정 — Int? 둘을 === 로 비교하면

=== 는 같은 객체인지 보는 비교라고 했습니다. 그런데 Int? 에 담은 숫자끼리 === 로 견주면 값이 같아도 결과가 갈립니다. 자주 쓰는 작은 Integer 객체를 미리 만들어 두고 다시 내주는 장치, 곧 Integer 캐시 때문입니다. 어떻게 걸리는지 차례로 봅니다.

이 함정을 이해하려면 먼저 코틀린의 Int 가 JVM 에서 무엇이 되는지 알아야 합니다. 코틀린에는 자바의 int 와 Integer 가 따로 없고 Int 하나뿐입니다. 컴파일러는 이 Int 를 대개 int 로 컴파일합니다.

Int? 는 다릅니다. null 을 받는 Int? 는 박싱된 java.lang.Integer 가 됩니다. int 같은 값을 Integer 객체로 감싸는 것을 박싱(boxing)이라고 부릅니다.

Kotlin
fun main() {
    val a: Int? = 127
    val b: Int? = 127
    val c: Int? = 128
    val d: Int? = 128
    println(a === b)    // true
    println(c === d)    // false
    println(c == d)     // true
}
터미널
$ kotlinc Cache.kt -d out
Cache.kt:6:13: warning: identity equality for arguments of types 'Int?' and 'Int?' is prohibited.
    println(a === b)
            ^^^^^^^
Cache.kt:7:13: warning: identity equality for arguments of types 'Int?' and 'Int?' is prohibited.
    println(c === d)
            ^^^^^^^
$ kotlin -cp out CacheKt
  • val a: Int? = 127 — 변수 이름 뒤에 콜론과 타입을 적었습니다. 타입을 Int? 로 못박아 Integer 가 되게 했습니다

볼 줄은 둘입니다. 첫째, 컴파일러가 === 두 줄에 경고를 냈습니다. 경고 문구는 Int? 끼리의 참조 비교(identity equality)를 금지(prohibited)한다고 적습니다. 그래도 에러가 아니라 경고라서 컴파일은 끝났습니다.

둘째, 실행 결과입니다. 127 끼리는 true, 128 끼리는 false 입니다. 내용 비교인 c == d 는 true 입니다.

127 과 128 을 가르는 것

값을 감싸는 일은 소스에 안 적혀 있으니 컴파일된 클래스 파일을 열어 봅니다. CFR 은 클래스 파일을 거꾸로 읽어 자바 코드로 다시 적는 디컴파일러이고, jar 하나로 된 도구라 java -jar cfr.jar 클래스파일 로 돌립니다. --sugarboxing false 는 박싱 호출을 감추지 말라는 옵션입니다. 이 옵션이 없으면 CFR 은 Integer a = 127; 처럼 호출을 감춘 모양으로 보여 줍니다. 결과 맨 위의 머리 주석과 import, 한 줄이 아주 긴 @Metadata(...)(코틀린 컴파일러가 남기는 부가 정보)는 ... 로 줄여 싣습니다.

터미널
$ java -jar cfr.jar --sugarboxing false out/CacheKt.class
...
        Integer a = Integer.valueOf((int)127);
        Integer b = Integer.valueOf((int)127);
        Integer c = Integer.valueOf((int)128);
        Integer d = Integer.valueOf((int)128);
        boolean bl = a == b;
...
        bl = c == d;
...
        bl = Intrinsics.areEqual((Object)c, (Object)d);
...

Int? 변수에 넣은 숫자는 전부 Integer.valueOf 로 감싸졌습니다. 그리고 연산자 둘이 서로 다른 모양으로 내려갔습니다. === 는 자바의 == 이고, == 는 Intrinsics.areEqual 이라는 메서드 호출입니다. Intrinsics 는 컴파일러가 만든 코드가 부르는 도우미를 모아 둔 코틀린 표준 라이브러리 클래스입니다. 1절에서 본 equals 호출 은 이 도우미를 거쳐 나온 것입니다.

(int) 캐스트와 변수 이름 bl 은 소스에 없는, CFR 이 타입과 이름을 드러내려고 붙인 표시입니다.

답은 Integer.valueOf 에 있습니다. 이 메서드는 -128~127 을 늘 캐시합니다. Javadoc 원문은 「This method will always cache values in the range -128 to 127, inclusive」입니다(자바 21 Javadoc 의 Integer.valueOf 항목).

-128 부터 127 까지는 valueOf 가 미리 만들어 둔 Integer 객체를 돌려줍니다. 그래서 a 와 b 는 같은 객체이고 === 가 true 입니다. 기본 설정에서 128 은 부를 때마다 새 객체라서 c 와 d 는 다른 객체입니다.

자바의 Integer 도 똑같이 걸립니다. 같은 네 변수를 Integer 로 선언해 == 와 equals 로 비교했습니다.

Java
public class Cache {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        Integer c = 128;
        Integer d = 128;
        System.out.println(a == b);
        System.out.println(c == d);
        System.out.println(c.equals(d));
    }
}
터미널
$ javac -d jout Cache.java
$ java -cp jout Cache
true
false
true

같은 결과입니다. -XX:AutoBoxCacheMax 는 캐시의 위 끝을 바꾸는 JVM 옵션입니다. 같은 클래스를 java -XX:AutoBoxCacheMax=1000 -cp jout Cache 로 돌리자 세 줄 모두 true 가 나왔습니다. 캐시의 위 끝인 127 은 이렇게 실행 옵션에 따라 움직입니다.

그러니 박싱된 숫자의 === 결과에 기대면 안 됩니다.

? 없는 Int 둘은 128 이어도 === 가 true 입니다. int 로 컴파일돼 감쌀 객체가 없기 때문입니다(코틀린 문서 「Equality」). val a: Int = 128 둘을 비교한 Plain.kt 가 true 를 찍었습니다.

이때 나온 경고 문구는 identity equality for arguments of types 'Int' and 'Int' is deprecated. 입니다. deprecated 는 아직 허용하지만 쓰지 말라는 표시로, Int? 의 prohibited 보다 한 단계 약합니다.

숫자는 == 로 비교합니다. Int? 의 === 는 자바 Integer 의 == 와 같은 함정이고, 코틀린 컴파일러는 그 줄에 경고를 냅니다.

4. 문자열은 == 로 비교해도 된다 — 자바의 흔한 버그가 사라졌다

자바에서 if (answer == "yes") 는 흔한 버그입니다. 고약한 점은 테스트에서는 통과하는 때가 많다는 것입니다. 소스에 직접 쓴 문자열과 실행 중에 이어 붙인 문자열을 나란히 비교해 봅니다.

먼저 자바입니다. lit 는 문자열 리터럴입니다. built 는 변수 head 에 "lin" 을 실행 중에 이어 붙인 문자열입니다.

Java
public class Str {
    public static void main(String[] args) {
        String lit = "kotlin";
        String head = "kot";
        String built = head + "lin";
        System.out.println(lit == "kotlin");
        System.out.println(built == "kotlin");
        System.out.println(built.equals("kotlin"));
    }
}
터미널
$ javac -d jout Str.java
$ java -cp jout Str
true
false
true

내용이 같은 "kotlin" 인데 lit == "kotlin" 은 true, built == "kotlin" 은 false 입니다.

갈린 이유는 객체가 몇 개인가입니다. 같은 내용의 문자열 리터럴은 JVM 안에서 한 객체를 함께 씁니다(자바 언어 명세 §3.10.5). 그래서 lit 와 "kotlin" 은 같은 객체입니다. built 는 실행 중에 새로 만든 객체라 다른 객체입니다.

리터럴로 짠 테스트에서는 == 가 true 로 통과합니다. 사용자 입력처럼 실행 중에 만들어진 문자열이 들어오면 false 가 됩니다. 이것이 자바의 문자열 == 버그가 늦게 발견되는 이유입니다.

같은 코드를 코틀린으로 옮겼습니다.

Kotlin
fun main() {
    val lit = "kotlin"
    val head = "kot"
    val built = head + "lin"
    println(lit == "kotlin")       // true
    println(built == "kotlin")     // true
    println(built === "kotlin")    // false
}
터미널
$ kotlinc Str.kt -d out
$ kotlin -cp out StrKt

코틀린의 built == "kotlin" 은 true 입니다. 코틀린에서는 == 가 곧 내용 비교이니, 자바 버릇대로 == 를 써도 버그가 되지 않습니다. 객체가 다르다는 사실은 built === "kotlin" 의 false 에만 남았습니다.

문자열이라고 규칙이 따로 있는 것은 아닙니다. built == "kotlin" 은 1절의 Money 와 똑같이 equals 를 부르는 내용 비교이고, built === "kotlin" 은 같은 객체인지만 보는 참조 비교입니다. 문자열끼리의 === 에는 3절 같은 경고가 없었습니다.

코틀린에서 문자열은 == 로 비교하면 됩니다.

5. 배열의 == 는 참조 비교다 — contentEquals

지금까지는 == 가 내용을 비교했습니다. 그런데 배열에 쓰면 내용이 같아도 false 가 나옵니다. 같은 원소를 담은 배열 둘과 리스트 둘을 == 로 비교해 봅니다.

Kotlin
fun main() {
    val a = arrayOf(1, 2, 3)
    val b = arrayOf(1, 2, 3)
    println(a == b)               // false
    println(a.contentEquals(b))   // true
    // true
    println(listOf(1, 2, 3) == listOf(1, 2, 3))
}
터미널
$ kotlinc Arr.kt -d out
Arr.kt:4:15: warning: '==' on arrays compares only references. Replace '==' with 'contentEquals' to compare the arrays' contents or use `===` to remove the warning.
    println(a == b)
              ^^
$ kotlin -cp out ArrKt
  • arrayOf(1, 2, 3) — 원소를 받아 배열을 만드는 표준 라이브러리 함수입니다. 자바의 Integer[] 가 됩니다
  • a.contentEquals(b) — 두 배열의 원소를 순서대로 견주는 함수입니다
  • listOf(1, 2, 3) — 원소를 받아 읽기 전용 리스트를 만드는 함수입니다. JVM 에서는 java.util.List 입니다

컴파일러가 먼저 경고했습니다. 배열의 == 는 참조만 비교하니 contentEquals 로 바꾸라는 문구입니다. 출력도 그대로 a == b 는 false, contentEquals 는 true 입니다. 리스트끼리의 == 는 true 입니다.

배열에만 다른 규칙이 붙은 것은 아닙니다. == 는 배열에서도 equals 를 부릅니다. 달라진 것은 어느 equals 가 불렸는가입니다. 자바 배열은 equals 를 재정의하지 않아 Object.equals, 곧 참조 비교가 불립니다. java.util.List 의 equals 는 원소를 순서대로 견주도록 정의돼 있어 리스트는 true 가 나옵니다.

자바에서도 배열의 equals 는 같은 함정입니다.

Java
import java.util.Arrays;

public class Arr {
    public static void main(String[] args) {
        Integer[] a = {1, 2, 3};
        Integer[] b = {1, 2, 3};
        System.out.println(a.equals(b));
        System.out.println(Arrays.equals(a, b));
    }
}
터미널
$ javac -d jout Arr.java
$ java -cp jout Arr
false
true

코틀린과 같은 결과입니다. 자바의 Arrays.equals 가 코틀린의 contentEquals 입니다.

배열 안에 배열이 들면 한 번 더 걸립니다. arrayOf(arrayOf(1, 2)) 로 만든 a 와 b 를 비교한 Deep.kt 에서 a.contentEquals(b) 는 false 였습니다. 원소인 안쪽 배열끼리를 다시 equals, 곧 참조로 견주기 때문입니다.

안쪽까지 내려가 견주는 a.contentDeepEquals(b) 는 true 였습니다. 자바에서 Integer[][] 둘을 견준 Deep.java 도 Arrays.equals 는 false, Arrays.deepEquals 는 true 를 찍었습니다.

배열은 == 대신 contentEquals 로 비교합니다. 내용 비교가 필요하면 배열보다 리스트를 쓰는 편이 == 를 그대로 쓸 수 있어 편합니다.

6. 한 장 요약

코틀린의 == 는 null 을 먼저 가리고 equals 를 부르는 내용 비교다. === 가 자바의 == 인 참조 비교다. 그래서 자바의 문자열 == 버그는 사라진다. 대신 equals 가 참조 비교인 배열은 contentEquals 를 써야 한다. Integer 로 박싱되는 Int? 의 === 는 127 과 128 에서 결과가 갈린다.

하는 일 자바 코틀린 확인한 방법
내용이 같은가 a.equals(b) a == b 실행(equals 호출)
내용이 다른가 !a.equals(b) a != b 실행(equals 호출)
같은 객체인가 a == b a === b → 자바 a == b 실행 · CFR
왼쪽이 null 일 수 있을 때 Objects.equals(a, b) (a.equals(b) 는 NPE) a == b 그대로 실행(세 갈래)
관계없는 두 클래스 Integer.equals(Long) 컴파일 ✓, false Int == Long 컴파일 ✗ kotlinc 에러 · 실행
박싱된 정수의 참조 비교 Integer 127 → true · 128 → false Int? 127 → true · 128 → false, 경고 실행 · Integer.valueOf
문자열 내용 s.equals("kotlin") s == "kotlin" 리터럴 vs 이어 붙인 문자열 실행
배열 내용 Arrays.equals(a, b) a.contentEquals(b) 경고 · 실행(코틀린·자바)
중첩 배열 내용 Arrays.deepEquals(a, b) a.contentDeepEquals(b) 실행(코틀린·자바)

관련 항목

코틀린 동등성 비교를 이루는 연산자와 함수

구조적 동등성 · 참조 동등성 · equals · hashCode · contentEquals · contentDeepEquals

동등성 비교가 컴파일되어 바뀌는 JVM 코드와 자바 대응

Intrinsics · Objects.equals · Arrays.equals · Arrays.deepEquals · Object.equals

박싱된 정수의 참조 비교를 흔드는 요인

박싱 · Integer 캐시 · Integer.valueOf · AutoBoxCacheMax · 래퍼 클래스

문자열 == 버그를 만드는 문자열 객체의 성질

문자열 리터럴 · 문자열 인터닝 · 문자열 연결

널을 다루는 코틀린 문법 요소와 자바 예외

널 안전성 · 널 허용 타입 · 안전 호출 연산자 · 널 아님 단언 연산자 · NullPointerException · 스마트 캐스트

이 편의 코드를 이루는 코틀린 문법 요소

주 생성자 · 프로퍼티 · override · Any · is 연산자 · val

내용 비교 방식이 갈리는 배열과 컬렉션

배열 · arrayOf · listOf · 컬렉션 · List

컴파일 결과를 열어 보는 도구

javap · 디컴파일 · 디컴파일러 · CFR · 클래스 파일

이 편에 쓰인 언어와 개발 도구

코틀린 · 자바 · JDK · kotlinc · 컴파일러 · 가상 머신 · 코틀린 표준 라이브러리