[이펙티브 자바] 모든 객체의 공통 메서드
by rowing0328※ 책 내용을 바탕으로 제 관점에서 풀어 쓴 글입니다. 일부 내용이 다를 수 있습니다.
아이템 10 - equals는 일반 규약을 지켜 재정의하라
equals() 메서드는 두 참조 변수가 동일한 값을 가지는지 여부를 비교하기 위해 사용된다. 객체의 논리적 동치성을 판단할 때 반드시 일반 규약을 지키며 재정의해야 한다.
equals 메서드, 언제 재정의해야 할까?
재정의 하지 않아도 되는 경우
- 고유 ID를 가지는 경우
각 인스턴스가 고유한 ID를 가지며, 논리적 동치성 검사가 필요 없는 경우 - 상위 클래스의 구현이 적절한 경우
상위 클래스의 equals가 이미 논리적 동치성을 정확히 구현한 경우 - 외부 호출이 없는 경우
클래스가 private 또는 package-private 범위로 제한되어 외부에서 동치성 비교가 필요하지 않은 경우
재정의가 필요한 경우
- 객체의 내부 상태(필드 값)가 같을 때 동일한 객체로 간주해야 하는 경우
이 경우, 논리적 동치성 비교하기 위해 반드시 equals 메서드를 재정의해야 한다.
equals의 일반 규약
- 반사성 (Reflexivity)
모든 null이 아닌 참조 값 x에 대해 x.equals(x)는 항상 true를 반환해야 한다.
객체는 자기 자신과 같아야 한다는 기본적인 원칙이다. 이를 위반하면, 컬렉션의 contains 메서드와 같은 동작에서 예기치 못한 결과가 발생할 수 있다. - 대칭성 (Symmetry)
모든 null이 아닌 참조 값 x, y에 대해 x.equals(y)가 true라면, y.equals(x)도 반드시 true여야 한다.
이를 위반하면, 예를 들어 대소문자를 무시하는 문자열 비교와 같은 상황에서 일관성 없는 동작이 발생할 수 있다. - 추이성 (Transitivity)
모든 null이 아닌 참조 값 x, y, z에 대해 x.equals(y)가 true이고, y.equals(z)도 true라면, x.equals(z)도 반드시 true여야 한다.
동치 관계는 일관성을 유지해야 하며, 이를 위반하면 컬렉션이나 정렬 알고리즘에서 예상치 못한 동작이 발생할 수 있다. 특히, 상속 구조에서 추가된 필드가 규약 위반의 원인이 될 수 있으므로 주의해야 한다. - 일관성 (Consistency)
모든 null이 아닌 참조 값 x, y에 대해, 여러 번 호출된 x.equals(y)는 객체의 상태가 변경되지 않는 한 항상 동일한 값을 반환해야 한다. - null-아님 (Non-nullity)
모든 null이 아닌 참조 값 x에 대해 x.equals(null)은 항상 false를 반환해야 한다.
어떤 객체도 null과 같다고 판단되어서는 안 된다. 이는 기본적으로 안전한 동작을 보장하기 위한 규칙이다.
equals의 전형적인 검사 패턴
- 참조 비교
== 연산자를 사용해 입력 값이 현재 객체와 동일한 참조인지 확인한다. 동일한 참조라면 추가적인 비교를 생략한다. - 타입 확인
instanceof 연산자를 통해 입력 값이 기대하는 타입인지 명확히 검사한다. - 타입 캐스팅
instanceof 검사를 통과한 경우, 입력 값을 올바른 타입으로 캐스팅한다. - 핵심 필드 비교
두 객체의 핵심 필드 값들이 모두 일치하는지 확인한다. - null 비교
x.equals(null)은 항상 false를 반환해야 한다.
아이템 11 - equals를 재정의하려거든 hasecode도 재정의하라
hashCode 메서드는 객체의 주소 값 또는 상태를 기반으로 해시 코드를 생성해 반환한다. 이를 통해 객체를 효율적으로 검색하거나 관리할 수 있다.
하지만 서로 다른 두 객체가 같은 해시코드를 가질 수 있는 경우(해시 충돌)가 있으므로, 해시코드는 객체의 "지문"으로 비유될 수 있다.
비교 방법의 차이
- == 연산자
Primitive 타입은 값을 비교한다.
Reference 타입은 메모리 주소를 비교한다. - equals() 메서드
기본값은 ==과 동일하지만, 필요에 따라 Override하여 논리적으로 같은 객체를 정의할 수 있다. - hashCode() 메서드
논리적으로 같은 객체라면 항상 같은 hashCode를 반환해야 한다.
이를 재정의하지 않으면 equals와 hashCode의 불일치로 인해 해시 기반 컬렉션(HashMap, HashSet 등)에서 예상치 못한 동작이 발생할 수 있다.
잘못된 예
// 안내방송을 위한 TTS (text to speak) 예약 Map
Speaker speaker1 = new Speaker("수업 시작 시간입니다.");
Map<Speaker, LocalTime> localTimeMap = new HashMap<>();
localTimeMap.put(speaker1, LocalTime.of(9, 0);
// 수업 시작 시간을 10분 당기기로 하였다.
Speaker speaker2 = new Speaker("수업 시작 시간입니다.");
localTimeMap.put(speaker2, LocalTime.of(8, 50);
실제로 안내 방송은 8시 50분과 9시에 두번 울리게 된다.
이와 같은 동작이 발생하는 이유는 Hash 값을 사용하는 컬렉션(HashMap, HashSet, HashTable)이 객체를 논리적으로 비교할 때 다음과 같은 과정을 따르기 때문이다.
- hashCode 검증
컬렉션은 먼저 객체의 hashCode를 사용해 해당 객체가 저장된 위치를 찾는다. - equals 검증
동일한 해시코드의 객체가 여러 개 있을 경우, equals 메서드를 사용해 논리적으로 같은 객체인지 추가로 검증한다.
hashCode와 equals 메서드가 일관성 있게 동작하지 않으면 논리적으로 같은 객체라도 다른 객체로 취급되며, 위와 같은 문제가 발생할 수 있다.
간단한 방식의 hsah
// 가장 Simple하게 적용해야 한다면!
@Override
public int hashCode() {
int result = message.hashCode();
return result;
}
// 속도를 고려해야 한다.
@Override
public int hashCode() {
return Objects.hash(modelName, company); // Objects의 경우 속도가 아쉽다.
}
Lombok을 사용한 Equals와 HashCode 구현
@EqualsAndHashCode
public class EqualsAndHashCodeExample {
private transient int transientVar = 10;
private String name;
private double score;
private String[] tags;
@EqualsAndHashCode.Exclude private int id;
}
아이템 12 - toString을 항상 override 하라
기본적으로 toString() 메서드는 객체의 클래스 이름과 @ 뒤에 16진수 해시코드를 결합한 문자열 형식(className@hashcode)을 반환한다.
toString의 일반 규약
toString() 메서드는 간결하고 사람이 읽기 쉬운 형태로, 객체의 유익한 정보를 제공해야 한다.
객체의 상태를 표현하는 데 도움이 되는 필드 값을 포함시키는 것이 좋다.
Lombok을 사용한 toString 구현
@AllArgsConstructor
@ToString
public class Laptop {
private String modelName;
private String company;
}
// 출력 결과
Laptop(modelName=그램 16인치, company=삼성)
불필요한 변수가 있을 경우
// 특정 변수를 제외하는 방법
@AllArgsConstructor
@ToString(exclude = {"modelName"})
public class Laptop {
private String modelName;
private String company;
}
// 더 나은 방법으로는 @ToString.Exclude 어노테이션 사용
@ToString
public class Laptop {
@ToString.Exclude private String modelName;
private String company;
}
아이템 13 - clone 재정의는 주의해서 사용하라
clone() 메서드는 객체의 복사본을 반환하는 기능을 한다.
하지만 복사의 정확한 의미는 클래스마다 다를 수 있으며, 이를 잘못 구현하거나 사용할 경우 예상치 못한 문제를 초래할 위험이 있다.
배열 복사의 두가지 방법
// Shallow Copy (얕은 복사)
int[] a = {1,2,3,4};
int[] b = a;
b는 a와 동일한 참조값을 가지므로, 같은 배열을 가리킨다.
따라서 배열의 내용이 변경되면 a와 b 모두 영향을 받는다.
// Deep copy (깊은 복사)
b = a.clone();
clone() 메서드를 사용하면 새로운 배결 객체가 생성된다.
두 배열의 요소는 동일하지만, a와 b는 서로 다른 참조값을 가진다.
즉, 한 배열의 내용이 변경되어도 다른 배열에는 영향을 미치지 않는다.
clone() 사용 시 주의사항
Laptop[] a = {new Laptop("그램 16인치", "삼성")};
Laptop[] b = a.clone();
b[0].setCompany("LG");
System.out.println(a[0] == b[0]); // true
clone() 메서드는 얕은 복사(Shallow Copy)를 수행한다.
이로 인해 b는 a와 별도의 배열 객체이지만, 배열 내부 요소들은 같은 객체를 참조한다.
따라서, a[0]과 b[0]는 동일한 객체를 가리키며, 한쪽에서 객체의 내용을 변경하면 다른 쪽에도 영향을 미친다.
왜 이런 현상이 발생하는가?
a.clone()은 배열의 객체 참조만 복사하기 때문이다.
이로 인해 객체의 내용이 아닌 참조값이 복사되며, 복사된 배열과 원본 배열은 동일한 객체를 가리키게 된다.
Why not use clone() ?
class A implements Cloneable {
public A clone() throws CloneNotSupportedException {
return (A) super.clone();
}
}
class B extends A {
public B clone() throws CloneNotSupportedException {
return (B) super.clone(); // A의 clone이 실행됨
}
}
clone() 메서드가 객체의 복사본을 생성하지만, 클래스에 따라 동작이 다르다.
상속 구족에서 clone()을 사용할 경우, 하위 클래스 객체가 제대로 복사되지 않는 문제가 발생할 수 있다.
이는 상위 클래스의 clone() 메서드가 호출되기 때문에, 하위 클래스의 추가 필드가 누락되거나 불완전한 복사로 이어질 수 있다.
clone() 대신 명확하고 안전한 복사 방법
// Copy Constructor (명확한 복사)
public Yum(Yum yum) {
this.field1 = yum.field1;
this.field2 = yum.field2;
}
// Copy Factory Method (유연하고 직관적인 복사)
public static Yum newInstance(Yum yum) {
Yum copy = new Yum();
copy.field1 = yum.field1;
copy.field2 = yum.field2;
return copy;
}
Primitive 타입 배열이 아닌 경우, clone()은 얕은 복사로 인해 참조 공유 문제가 발생할 수 있으므로 사용하지 말자.
Cloneable 인터페이스는 구현 방식에 따라 불완전한 복사가 이루어질 수 있어 확장에 주의해야 한다.
아이템 14 - Comparable을 구현할지 고려하라
public class Person {
private int age;
private String name;
private double height;
}
위 코드는 나이, 키, 이름 순으로 정렬하는 예제를 위한 간단한 Person 클래스이다.
compareTo 메서드 구현
public int compareTo(Person p) {
int result = Integer.compare(age, p.age);
if (result == 0) {
result = Double.compare(height, p.height);
}
if (result == 0) {
result = name.compareTo(p.name);
}
return result;
}
위 코드는 Comparable 인터페이스를 구현하여 Person 객체의 정렬 기준을 정의한다.
Comparator를 사용한 또 다른 구현
private static final Comparator<Person> COMPARATOR = Comparator.comparingInt(Person::getAge)
.thenComparingDouble(Person::getHeight)
.thenComparing(person -> person.getName()
);
public int compareTo(Person p) {
return COMPARATOR.compare(this, p);
}
위 코드는 Comparator를 활용하여 Person 객체의 정렬 기준을 정의하고, Comparable 인터페이스의 compareTo 메서드에서 이를 간단히 호출하는 방식이다.
compareTo의 규약
- x.compareTo(y) > 0
x가 y보다 크다는 의미이다. - x.compareTo(y) == 0
x와 y가 같음을 의미한다. - x.compareTo(y) < 0
x가 y보다 작다는 의미이다. - 비교 할 수 없는 경우
ClassCastException이 발생한다.
마무리
Lombok과 같은 라이브러리를 활용하면,
반복적인 코드를 줄이고, 명확하고 읽기 쉬운 코드를 작성할 수 있다는 점에서 큰 도움이 된다.
하지만, 편의 기능을 제대로 활용하려면
그 뒤에 숨겨진 기본 원리와 동작 방식을 이해하는 것이 중요하다는 점을 느꼈다.
이러한 이해가 뒷받침될 때,
상황에 맞는 적절한 선택과 활용이 가능하다는 생각이 들었다.
'📚Book Archive > Effective Java' 카테고리의 다른 글
[이펙티브 자바] Generic 으로 만들어 사용하기 (0) | 2025.01.03 |
---|---|
[이펙티브 자바] Interface와 Class 설계 원칙 (0) | 2024.12.30 |
[이펙티브 자바] Class와 상속 (4) | 2024.12.20 |
[이펙티브 자바] 객체 파괴 (0) | 2024.12.07 |
[이펙티브 자바] 객체 생성 (0) | 2024.12.06 |
블로그의 정보
코드의 여백
rowing0328