"Comparing Objects in Java" 글을 번역한 글입니다

자바의 객체 비교 방법에 대해 알아봅시다

 

1. ==, != 연산자

가장 기본적인 비교 연산자인 ==, !=에 대해 알아봅시다

1.1. primitive(원시) 타입

원시타입에 대해, '같다'는 것은 '같은 값을 갖는다'는 것을 의미합니다

assertThat(1 == 1).isTrue();

자바의 오토언박싱 덕분에, wrapper type(래퍼 타입)에도 이 룰이 똑같이 적용됩니다

Integer a = new Integer(1);
assertThat(1 == a).isTrue();

int, Integer가 다른 값을 가지면, == 연산자는 false를 반환하고 != 연산자는 true를 반환합니다

 

1.2. Object(객체)

같은 값을 가진 두 Integer 래퍼 타입을 비교한다고 해 봅시다

Integer a = new Integer(1);
Integer b = new Integer(1);

assertThat(a == b).isFalse();

== 연산자로 비교하면, 두 객체의 값(1)을 비교하는 것이 아니고, 스택에 할당된 메모리 주소를 비교하게 됩니다.

두 객체는 각각 new 연산자로 만들어졌기 때문에 메모리 주소가 다릅니다.

만약 a를 b에 대입한다면 아래처럼 다른 결과가 나오게 됩니다

Integer a = new Integer(1);
Integer b = a;

assertThat(a == b).isTrue();

 

만약 객체를 Integer.valueOf 팩토리 메소드로 생성한다면 어떻게 될까요?

Integer a = Integer.valueOf(1);
Integer b = Integer.valueOf(1);

assertThat(a == b).isTrue();

이때 a, b는 같은 객체입니다. 이는 valueOf 메소드가 너무 많은 래퍼 객체 생성을 방지하기 위해 자주 사용되는 일정 범위의 값을 내부적으로 캐싱하기 때문입니다. (참고: Integer.valueOf(127) == Integer.valueOf(127) 는 참일까요?, NHN Cloud Meetup)

이는 String에도 동일하게 적용됩니다

assertThat("Hello!" == "Hello!").isTrue();

하지만 new 연산자로 String 객체를 만들었다면, ==로 비교했을 때 false가 반환됩니다 (물론 이렇게 쓰는 경우는 별로 없습니다)

assertThat(new String("abc") != new String("abc")).isTrue();

 

마지막으로, null 레퍼런스는 항상 같고 null과 non-null 객체를 비교하면 항상 다릅니다.

assertThat(null == null).isTrue();

assertThat("Hello!" == null).isFalse();

 

두 객체가 다른 주소에 할당되어 있더라도 다른 기준으로 같은지 비교하려면? 아래 내용을 확인하세요

 

2. Object.equals 메소드

equals 메소드는 모든 자바 객체의 Base class인 Object 클래스에 정의되어 있습니다. equals 메소드의 기본 동작은 객체의 메모리 주소를 비교하는 방식이고, 이는 == 연산자와 동일한 방식입니다.

하지만 equals 메소드를 오버라이드해 우리가 원하는 방식으로 equality(동등성)를 정의할 수 있습니다.

예를 들어 Person 클래스를 아래처럼 만들었다고 해보겠습니다

public class Person {
    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

멤버변수(firstName, lastName)을 이용해 Person 객체를 비교하기 위해 아래처럼 equals 메소드를 오버라이드할 수 있습니다

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person that = (Person) o;
    return firstName.equals(that.firstName) &&
           lastName.equals(that.lastName);
}

 

3. Objects.equals static 메소드

Object의 equals 메소드를 사용할 때의 문제는 a.equals(b)를 호출한다고 했을 때, a가 null인 경우 equals 메소드를 호출할 수 없어 NullPointerException이 발생한다는 것입니다. 따라서 a가 null인지 여부를 추가로 확인해줘야 하는 귀찮음이 있습니다.

java.util.Objects::equals 메소드는 이 문제를 편리하게 해결해줍니다. 이 메소드는 두 객체를 받아 동일하게 비교하고, null값도 문제 없이 비교합니다.

Person joe = new Person("Joe", "Portman");
Person joeAgain = new Person("Joe", "Portman");
Person natalie = new Person("Natalie", "Portman");

assertThat(Objects.equals(joe, joeAgain)).isTrue();
assertThat(Objects.equals(joe, natalie)).isFalse();

 

4. Comparable 인터페이스

클래스 객체마다 특정 기준으로 비교 로직을 만들 수 있습니다. 문자열도 사전순, 길이순 등 여러 기준으로 순서를 부여할 수 있습니다.

Comparable 인터페이스를 이용해 객체간 크고 작거나 같음을 비교할 수 있습니다.

Comparable 인터페이스는 제네릭이며 compareTo 메소드 하나만 갖고 있습니다. compareTo는 제네릭 타입의 인자를 받고 int를 반환하는 메소드인데, 반환값이 음수이면 객체가 인자보다 작고 0이면 같으며 양수이면 객체가 인자보다 크다는 것을 나타냅니다.

public class Person implements Comparable<Person> {
    //...

    @Override
    public int compareTo(Person o) {
        return this.lastName.compareTo(o.lastName);
    }
}

예를 들어 Person 객체를 lastName 기준 사전순으로 비교할 수 있도록 만들려면 위와 같이 Comparable 인터페이스를 implement(구현)합니다

 

5. Comparator 인터페이스

Comparator 인터페이스는 제네릭이며, compare 메소드를 갖고 있습니다. 이 메소드는 두 객체를 입력받아 대소관계를 비교해 int 값을 반환합니다.

Comparator는 클래스와 분리되어 있습니다. 따라서, 클래스 하나에 여러개의 Comparator를 만들어 골라 사용할 수 있습니다.

Comparator<Person> compareByFirstNames = Comparator.comparing(Person::getFirstName);

Person joe = new Person("Joe", "Portman");
Person allan = new Person("Allan", "Dale");

List<Person> people = new ArrayList<>();
people.add(joe);
people.add(allan);

people.sort(compareByFirstNames);

assertThat(people).containsExactly(allan, joe);

예를 들어 이름 전체가 아닌 firstName만 기준으로 정렬하고 싶다면 위처럼 Comparator를 만들어 sort 메소드 호출 시 comparator를 넘겨 정렬할 수 있습니다.

@Override
public int compareTo(Person o) {
    return Comparator.comparing(Person::getLastName)
      .thenComparing(Person::getFirstName)
      .thenComparing(Person::getBirthDate, Comparator.nullsLast(Comparator.naturalOrder()))
      .compare(this, o);
}

Comparator 인터페이스에는 compareTo 메소드를 구현할 때 사용할 수 있는 다른 메소드도 있습니다.

정렬시 null 값이 있는 경우를 위한 nullsFirst, nullsLast 메소드도 있습니다.

 

6. 라이브러리

6.1. Apache Commons(아파치 커먼스) - commons-lang3

Apache Commons의 commons-lang3는 인기있는 유틸리티 라이브러리입니다

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

 

6.1.1. ObjectUtils.notEqual

ObjectUtils.notEquals 메소드는 두 객체를 인자로 받아 객체의 equals() 메소드를 이용해 다른지 여부를 반환합니다. 또한 null 값도 인자로 받을 수 있습니다.

String a = new String("Hello!");
String b = new String("Hello World!");

assertThat(ObjectUtils.notEqual(a, b)).isTrue();

주의할 점은 ObjectUtils엔 equals() 메소드가 있지만, Java 7부터 Objects.equals() 메소드가 도입되었기 때문에 ObjectUtils.equals는 deprecated 되었습니다.

 

6.1.2. ObjectUtils.compare 

ObjectUtils.compare는 Comparable 인터페이스를 구현하는 두 객체를 인자로 받아 int 값을 반환하는 메소드입니다.

String first = new String("Hello!");
String second = new String("How are you?");

assertThat(ObjectUtils.compare(first, second)).isNegative();

기본적으로 null값이 non-null값보다 작은 것으로 계산됩니다.

public static <T extends Comparable<? super T>> int compare(T c1,
                                                            T c2,
                                                            boolean nullGreater)

null 값이 non-null보다 큰지 작은지는 compare의 3번째 파라미터로 설정할 수 있습니다.

 

6.2. Google Guava (구글 구아바)

Guava는 구글에서 만든 유틸리티 라이브러리입니다

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

 

6.2.1. Objects.equal

Apache Commons 라이브러리와 비슷하게, Guava도 Objects.equal 메소드를 갖고 있습니다. 구현 방식은 다르지만, 결과는 같습니다.

Java 7부터는 쓸 이유가 없습니다

 

6.2.2. 비교 메소드

Guava는 두 객체를 비교하는 메소드는 없지만 primitive 값을 비교하는 메소드를 제공합니다. 예를 들면 Ints 헬퍼 클래스의 compare 메소드를 아래처럼 사용할 수 있습니다

assertThat(Ints.compare(1, 2)).isNegative();

 

6.2.3. ComparisonChain 클래스

Guava에서는 비교 체인을 통해 두 객체를 비교할 수 있는 ComparisonChain 클래스를 제공합니다.

Person natalie = new Person("Natalie", "Portman");
Person joe = new Person("Joe", "Portman");

int comparisonResult = ComparisonChain.start()
  .compare(natalie.getLastName(), joe.getLastName())
  .compare(natalie.getFirstName(), joe.getFirstName())
  .result();

assertThat(comparisonResult).isPositive();

이때 compare 메소드에 들어가는 인자는 primitive 또는 Comparable이어야 합니다.

반응형