Java와 Kotlin의 동등성과 동일성

사명기·2023년 2월 23일
0
post-custom-banner

회사 업무 중에 Spring 내부를 파보다가 Java의 ==equals를 만났습니다.
몇년간 Kotlin만 사용해오다가, 문득 자바 코드를 보니 기억이 잘 나지 않았습니다.
그래서 이번 기회에 자바와 코틀린의 동등성과 동일성에 대한 것에 hashCode를 한 스푼 추가해 정리를 해두려고 합니다.



동등성과 동일성 간단 개념

동등성이란?

두 객체가 내부에 동일한 데이터를 가지고 있는지 의미합니다.

동일성이란?

두 객체가 참조하고 있는 주소값이 같은지를 의미합니다.




Java의 동등성과 동일성

Java 동등성

자바에서 참조 타입인 경우에 동등성은 equals를 사용해서 확인합니다.

간단한 예시를 하나 보겠습니다.

public class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }

Student라는 클래스는 이름(name)과 나이(age) 필드를 가지고 있습니다.
여기서 두개의 인스턴스를 생성해보고, 동등성을 확인해보겠습니다.

Student ekko1 = new Student("ekko", 33);
Student ekko2  = new Student("ekko", 33);

System.out.println(ekko1.equals(ekko2));

결과는 어떻게 나올까요?
두 인스턴스가 모두 같은 필드값을 가지고 있으니까 true일까요?
결과는 false입니다.

그 이유는 아래와 같이, Object의 equals이 기본적으로 동일성(==)을 검사하기 때문입니다. (참조하고 있는 주소가 같은지 검사)
Object의 equals

여기서 우리는 Student의 동등성을 확인할 수 있도록 보장하려면 equals를 재정의해야겠구나 라는 생각이 듭니다. equals & hashCode를 override하는 코드를 Student class에 추가해줬습니다.

// Student class 내부
		@Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student student = (Student) o;
            return age == student.age && name.equals(student.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }

그리고 다시 ekko1과 ekko2의 동등성을 확인하는 코드를 실행해보면, true가 출력됩니다.
여기서 hashCode는 왜 override하는 것인지 궁금하실 겁니다.

자바에서는 equals를 override할 때 반드시 hashCode도 override해야합니다.
기본적으로 Object의 hashCode는 객체의 주소값을 int 형으로 반환합니다.
hashCode를 override하지 않으면 hash를 사용하는 Collection 등을 사용할 때 문제가 발생할 수 있습니다.
예를 들어 hash를 사용하는 Collection을 사용할 때, hashCode를 override하지 않으면 동등성을 가진 두 객체가 중복되어 들어갈 수 있습니다. 또한 contains로 확인해도 마찬가지다.

// hashCode를 override하지 않은 경우
        Student ekko1 = new Student("ekko", 33);

        HashSet<Student> students = new HashSet<>();
        students.add(ekko1);
        System.out.println(students.contains(new Student("ekko", 33))); // false

Student class에서 hashCode를 오버라이드하지 않아서 false가 출력된다.
JVM 언어에서는 hashCode가 지켜야 하는 "equals가 true를 반환하는 두 객체는 반드시 같은 hashCode를 반환해야 한다"라는 제약이 있는데, Student class는 이를 어기고 있다.
따라서 equals를 오버라이드 했으므로 hashCode도 오버라이드 해야한다.


Java 동일성

자바의 참조 변수에 대한 동일성은 == 으로 확인합니다.
두 객체가 참조하고 있는 주소가 같다면 true, 아니라면 false로 나올 것입니다.

자바의 동일성은 간단히 예시만 보고 넘어가겠습니다.

        Student ekko1 = new Student("ekko", 33);
        Student ekko2 = new Student("ekko", 33);
        Student ekko3 = ekko1;

        System.out.println(ekko1 == ekko2); // false
        System.out.println(ekko1 == ekko3); // true

ekko1과 ekko2는 메모리의 서로 다른 위치에 존재하므로, 주소값이 달라 false이며.
ekko1과 ekko3는 참조하는 주소값이 같으므로 true가 나옵니다.




Kotlin의 동등성과 동일성

Kotlin 동등성

코틀린에서는 동등성을 비교하기 위해 ==을 사용합니다.
코틀린의 ==은 내부적으로 equals를 호출해서 객체를 비교
합니다.
따라서 클래스가 equals를 오버라이드하면 ==을 통해 안전하게 그 클래스의 인스턴스를 비교할 수 있습니다. (override하지 않으면 동일성 비교함 주의)

예시를 보겠습니다.

    class Student(
        val name: String,
        val age: Int
    )

    val ekko1 = Student("ekko", 33)
    val ekko2 = Student("ekko", 33)
    val ekko3 = ekko1

    println(ekko1 == ekko2) // false
    println(ekko1 == ekko3) // true

Student class를 생성하고, 동일한 필드값을 가지는 ekko1, ekko2 두 객체를 생성했습니다.

첫번째로 ekko1 == ekko2를 보겠습니다.
Kotlin의 ==은 내부적으로 equals를 호출하는데, equals를 오버라이딩하지 않아서 동일성을 확인하므로 false가 출력됩니다.
두번째로 ekko1 == ekko3는 같은 메모리 주소를 참조하므로 true가 출력됩니다.

이제 equals와 hashCode를 override한 코드를 보겠습니다.

    class Student(
        val name: String,
        val age: Int
    ) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false

            other as Student

            if (name != other.name) return false
            if (age != other.age) return false

            return true
        }

        override fun hashCode(): Int {
            var result = name.hashCode()
            result = 31 * result + age
            return result
        }
    }

    val ekko1 = Student("ekko", 33)
    val ekko2 = Student("ekko", 33)
    val ekko3 = ekko1
    val ekko4 = Student("jungle", 31)

    println(ekko1 == ekko2) // true
    println(ekko1 == ekko3) // true
    println(ekko1 == ekko4) // false

equals를 오버라이드했으니, 코드와 주석으로 된 결과를 보시면 당연한 결과로 보입니다.
참고로 여기서는 일반 class를 사용했으나, data class를 사용하면 equals, hashCode, copy, toString가 자동으로 생성됩니다.

Kotlin 동일성

동일성은 참조하는 주소값이 같은지 확인하는 것이라고 했습니다.
코틀린에서는 동일성을 확인하기 위해 === 연산자를 사용합니다.
이 연산자는 자바에서 객체의 참조를 비교할 때 사용하는 == 연산자와 같습니다.

    class Student(
        val name: String,
        val age: Int
    )

    val ekko1 = Student("ekko", 33)
    val ekko2 = Student("ekko", 33)
    val ekko3 = ekko1

    println(ekko1 === ekko2) // false
    println(ekko1 === ekko3) // true


결론

  • 동등성은 두 객체의 데이터(필드값)이 같은지 확인하는 것
  • 동일성은 두 객체가 참조하는 주소값이 같은지 확인하는 것
  • 자바에서는
    • 동등성을 확인하기 위해 equals 사용. Object의 equals는 == 비교를 하므로 오버라이드가 필요
    • 동일성을 확인하기 위해 == 연산자 사용
  • 코틀린에서는
    • 동등성을 확인하기 위해 == 연산자 사용, == 연산자는 내부적으로 equals를 호출.
    • 동일성을 확인하기 위해 === 연산자 사용
post-custom-banner

0개의 댓글