Object는 객체를 만들 수 있는 구체 클래스이지만 기본적으로 상속해서 사용하도록 설계되어 있다. Object에서 final이 아닌 메서드(equals, hashCode, toString, clone, finalize)는 모두 재정의(overriding)을 염두에 두고 설계된 것이라 재정의 시 지켜야할 일반 규약이 명확히 정의되어 있다.
Item10. equals는 일반 규약을 지켜 재정의하라
equals를 재정의 하지 않는 것이 최선이다. 아래의 경우에 해당한다면 equals를 재정의 할 필요가 없다.
각 인스턴스가 본질적으로 고유하다
ex) 싱글톤은 그 자체로 고유할 수 밖에 없다. enum도 기본적으로 단 1개만 생성된다.
인스턴스의 ‘논리적 동치성’을 검사할 필요가 없다
ex 1) 50달러 짜리 2개가 존재한다고 했을 때, 이 둘은 같을까? 기본적으로 정의되어 있는 equals에서는 이 둘을 다르게 정의한다. 어떤 가치를 볼 것인지에 따라 이 둘을 같게 또는 다르게 볼 수 있다.
ex 2) 동치성을 검사할 필요가 없는 경우
문자열 “Hello” == “Hello”
상위 클래스에서 재정의한 equals가 하위 클래스에도 적절하다
ex) List, Set을 상속해서 구현할 때
List → AbstractList에서 equals가 구현
Set → AbstractSet에서 equals가 구현
클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다
equals를 재정의 해야 한다면 반드시 Object 명세에 적힌 일반 규약을 따라야 한다.
// 대칭성 위배 54-55 페이지
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
//대칭성 위배
@Override
public boolean equals(Object obj) {
if (obj instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
}
if (obj instanceof String) {
return s.equalsIgnoreCase((String) obj);
}
return false;
}
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String polish = "polish";
**System.out.println(cis.equals(polish));**
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
**System.out.println(list.contains(polish));**
}
}
- 이렇게 쓰겠다는 것은 CaseInsensitiveString을 String과 동급으로 쓰겠다는 의도가 담겨 있다.(잘못된 생각)
- CaseInsensitivieString은 String을 알고 있기 때문에 처리가 가능하지만 String은 CaseInsensitiveString을 알지 못하기 때문에 처리가 불가능하다.
ii) **수정된 equals 메서드(56페이지)**
```java
// 대칭성 위배 54-55 페이지
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
//수정된 equals 메서드(56페이지)
@Override
public boolean equals(Object obj) {
**return (obj instanceof CaseInsensitiveString) &&
(((CaseInsensitiveString) obj).s.equalsIgnoreCase(s));**
}
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String polish = "polish";
**System.out.println(cis.equals(polish));**
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
**System.out.println(list.contains(polish));**
}
}
```
- 본인이 CaseInsensitiveString이라면, CaseInsensitivieString만을 지원해야 한다. 다른 타입을 지원해서는 안된다.
- 다른 타입을 지원하기 시작하면 대칭성이 깨 지기 쉽다.
public enum Color {
RED, ORANGE, YELLOW, GREEN, BLUE
}
// 단순한 불변 2차원 정수 점(Point) 클래스 (56페이지)
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
Point p = (Point) obj;
return p.x == x && p.y == y;
}
}
//Point에 값 컴포넌트(Color)를 추가 (56페이지)
public class ColorPoint extends Point{
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
//코드 10-2 잘못된 코드 - 대칭성 위배 (57페이지)
//Point는 부모 속성으로 비교하고 Color는 따로 속성 비교를 하면 되지 않을까?
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint)) {
return false;
}
return super.equals(obj) && ((ColorPoint) obj).color == color;
}
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(**p.equals(cp) + " " + cp.equals(p)**);
}
}
- p.equals(cp)를 할때 cp는 Point을 상속 받았기 때문에 Point의 (obj instanceof Point)의 true에 해당한다.
- ColorPoint 입장에서 Point를 바라보면 ColorPoint가 아니다.(ColorPoint가 더 구체적인 클래스) 그래서 false가 나오게 된다.
ii) 추이성 위배
```java
public enum Color {
RED, ORANGE, YELLOW, GREEN, BLUE
}
// 단순한 불변 2차원 정수 점(Point) 클래스 (56페이지)
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
Point p = (Point) obj;
return p.x == x && p.y == y;
}
}
//Point에 값 컴포넌트(Color)를 추가 (56페이지)
public class ColorPoint extends Point{
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
// 코드 10-3 잘못된 코드 - 추이성 위배! (57페이지)
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point)) {
return false;
}
// obj가 일반 Point면 색상을 무시하고 비교한다.
if (!(obj instanceof ColorPoint)) {
return obj.equals(this); //Point
}
// obj가 ColorPoint면 색상까지 비교한다.
return super.equals(obj) && ((ColorPoint) obj).color == color;
}
public static void main(String[] args) {
// 두 번째 equals 메서드(코드 10-3)는 추이성을 위배한다.
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
System.out.printf("%s %s %s%n", p1.equals(p2), p2.equals(p3), p1.equals(p3));
}
}
```
- 굉장히 위험한 코드, ColorPoint와 동일한 레벨에 있는 서브 클래스를 만든 다음 서로 equals를 구현하면 서로 equals를 호출하면서 method가 호출될 때마다 stack이 쌓이면서 stackoverflow가 발생하게 된다.
- p1과 p2는 같다고 나올 것이다. (Color가 무시되기 때문에) p2와 p3도 같다고 생각을 하게 될 것이다. p1과 p3를 비교할 때에는 ColorPoint이기 때문에 Color까지 비교하게 되고 서로 다르다고 생각하게 된다.
iii) 리스코프 치환 원칙 위배
```java
public enum Color {
RED, ORANGE, YELLOW, GREEN, BLUE
}
// 단순한 불변 2차원 정수 점(Point) 클래스 (56페이지)
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// 잘못된 코드 - 리스코프 치환 원칙 위배 (59페이지)
// ColorPoint는 ColorPoint 끼리 같고, Point는 Point 끼리 같아야 하지 않을까?
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
Point p = (Point) obj;
return p.x == x && p.y == y;
}
}
public class CounterPointTest {
private static final Set<Point> unitCircle = Set.of(
new Point(1, 0), new Point(0, 1),
new Point(-1, 0), new Point(0, -1)
);
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
public static void main(String[] args) {
Point p1 = new Point(1, 0);
Point p2 = new Point( 1, 0);
//true를 출력한다.
System.out.println(onUnitCircle(p1));
//true를 출력해야 하지만, Point의 equals가 getClass를 사용해 작성되었다면 그렇지 않다.
System.out.println(onUnitCircle(p2));
}
}
```
리스코프 치환 원칙 : 상위 타입에서 동작 하는 코드는 하위 타입의 인스턴스를 주더라도 그대로 동작해야 한다.
ex) 대칭성 위배 예제. Timestamp, date
```java
public static void main(String[] args) throws MalformedURLException {
long time = System.currentTimeMillis();
Timestamp timestamp = new Timestamp(time);
Date date = new Date(time);
//대칭성 위배! (60페이지)
System.out.println(date.equals(timestamp));
System.out.println(timestamp.equals(date));
}
```
iiii) Composition 사용(상속이 아닌 필드로 선언)
```java
// 코드 10-5 equals 규약을 지키면서 값 추가하기 (60페이지)
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
//이 ColorPoint의 Point 뷰를 반환한다.
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ColorPoint)) {
return false;
}
ColorPoint cp = (ColorPoint) obj;
return cp.point.equals(point) && cp.color.equals(color);
}
@Override
public int hashCode() {
return 31 * point.hashCode() + color.hashCode();
}
}
```
implementation 'com.google.auto.value:auto-value:1.3'
public class AutoValueTest {
public static void main(String[] args) {
Point point = Point.create(1,2);
System.out.println(point.equals(Point.create(1,2)));
}
}
@AutoValue
abstract public class Point {
static Point create(int x, int y) {
return new AutoValue_Point(x, y);
}
abstract int x();
abstract int y();
}
- 빨간색으로 표시되지만 정석적인 방법이 맞다. 자바 애노테이션 프로세서는 자신을 뜯고 고치는 코드를 생성하지 않는다. 추가로 코드를 생성하는 용도로만 만들어 졌다.
- 빨간색으로 없는 데 있는 것처럼 사용해야 한다. 컴파일할 때 만들어 주기 때문에 실행 시 정상 작동한다.
- 많이 쓰지 않는다. 많이 침투적이다. AutoValue를 사용하려면 너무 규약이 많다.
Lombok
@EqualsAndHashCode
@ToString
마찬가지로, 애노테이션 프로세서를 사용한다. 컴파일 타임에 애노테이션 정보를 읽어서 클래스에 코드를 추가해 준다. 이러한 기존 코드를 뜯어 고치는 것은 자바가 기대하지 않았던 동작이다. 일종의 롬복이 해킹해서 자바가 의도하지 않았지만 손쉽게 쓸 수 있도록 만들어 주는 것이다.
이렇게 의도하지 않은 방법으로 접근 하는 것은 JDK 버전이 올라감에 따라 막힐 수 있다. 권장하진 않지만 막힐 일은 없을 것 같다.
Record(14버전 부터 들어왔고 15버전부터 사용가능함)
VO를 만드는 데 있어, 이렇게 애노테이션을 붙이지 않아도 된다.
Intellij에서 equals와 hashCode를 Override해서 사용한다.
필드가 늘어나거나 줄어들면 다시 작성해야 한다.
자바 11버전인 경우 → Lombok
자바 17버전인 경우 → Record, Record가 적절하지 않다면 Lombok
주의 사항
equals를 재정의 할 때 hashCode도 반드시 재정의하자(아이템 11)
너무 복잡하게 해결하지 말자(심볼릭 링크나 URI까지 따지지 말자)
Object가 아닌 타입의 매개변수를 받는 equals 메서드는 선언하지 말자. (오버라이딩하지 않고 Object가 아닌 타입의 매개변수를 받을 경우 논리적 동치성 검사를 수행할 수 없다)
완벽 공략 24. Value 기반의 클래스(VO)
클래스 처럼 생겼지만 Int 처럼 동작하는 클래스
식별자가 없고 불변이다.(Entity의 라이프사이클의 기반한다.)
식별자가 아니라 인스턴스가 가지고 있는 상태를 기반으로 equals, hashCode, toString 을 구현한다.
== 오퍼레이션이 아니라 equals를 사용해서 동등성을 비교한다.
동일한(equals) 객체는 상호교환 가능한다.
Record
public record Point(int x, int y) {
}
public class PointTest {
public static void main(String[] args) {
Point p1 = new Point(1, 0);
Point p2 = new Point(1, 0);
System.out.println(p1.equals(p2));
System.out.println(p1);
System.out.println(p1.x());
}
}
equals와 hashCode를 구현해 주고, toString도 구현되어 있음을 알 수 있다.
한 번 쓰게 되면 고칠 수 없고 변수명으로 가져온다.
불변하고 식별자가 없고 딱 안에 들어있는 값으로 equals가 구현되어 있다.
완벽공략 25. StackOverflowError
로컬 변수와 객체가 저장되는 공간의 이름은?
스택(stack)과 힙(heap)
스택은 한 쓰레드마다 쓸 수 있는 공간, 스택이라는 공간에는 Stack Frame이 쌓인다. 그리고 위에서부터 처리한다. LIFO(Last In First Out).
힙은 객체들이 있는 공간이고 가비지 컬랙터가 일을 해서 정리해 주는 공간이다. 실제 인스턴스가 존재하는 공간. 이 인스턴스를 가리키는 공간은 Stack에 존재한다.
메소드 호출시, 스택에 스택 프레임이 쌓인다.
스택 프레임에 들어있는 정보 : 메소드에 전달하는 매개변수, 메소드 실행 끝내고 돌아갈 곳, 힙에 들어있는 객체에 대한 레퍼런스 …
그런데 더이상 스택 프레임을 쌓을 수 없다면? StackOverflowError!
재귀적인 알고리즘을 작성할 때, 무한 루프를 가질 때 Stack Frame을 많이 쓰게 된다.
스택의 사이즈를 조정하고 싶다면 -Xss1M
완벽공략 26. 리스코프 치환 원칙
객체 지향 5대 원칙 SOLID 중 하나
‘하위 클래스의 객체’가 ‘상위 클래스 객체’를 대체하더라도 소프트웨어의 기능을 깨트리지 않아야 한다.
아이템 11. equals를 재정의하려거든 hashCode도 재정의하라
hashCode 규약
equals 비교에 사용하는 정보가 변경되지 않았다면 hashCode는 매번 같은 값을 리턴 해야 한다. (변경되거나, 애플리케이션을 다시 실행했다면 달라질 수 있다.)
두 객체에 대한 equals가 같다면, hashCode의 값도 같아야 한다.
두 객체에 대한 equals가 다르더라도, hashCode의 값은 같을 수 있지만 해시 테이블 성능을 고려해 다른 값을 리턴하는 것이 좋다.
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max) {
throw new IllegalArgumentException(arg + " : " + val);
}
return (short) val;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof PhoneNumber)) {
return false;
}
PhoneNumber phoneNumber = (PhoneNumber) obj;
return phoneNumber.lineNum == lineNum && phoneNumber.prefix == prefix && phoneNumber.areaCode == areaCode;
}
public static void main(String[] args) {
Map<PhoneNumber, String> map = new HashMap<>();
map.put(new PhoneNumber(707, 867, 5309),"A");
System.out.println(map.get(new PhoneNumber(707, 867, 5307)));
}
}
public class HashMapTest {
public static void main(String[] args) {
Map<PhoneNumber, String> map = new HashMap<>();
PhoneNumber number1 = new PhoneNumber(123, 456, 7890);
PhoneNumber number2 = new PhoneNumber(123, 456, 7890);
System.out.println(number1.equals(number2));
System.out.println(number1.hashCode());
System.out.println(number2.hashCode());
map.put(number1, "kim");
map.put(number2, "seo");
String s1 = map.get(number2);
System.out.println(s1);
String s2 = map.get(new PhoneNumber(123, 456, 7890)); //null
System.out.println(s2);
}
}
위 코드에서 마지막 출력 값이 null이 나오는 이유는 새로 생성된 객체에 대한 해시값에 해당하는 값이 없기 때문이다. Key에 해당하는 Hash값을 가져온다. 그리고 Hash에 해당하는 버킷의 Object를 꺼내온다.
그렇다면 다른 Key에 대해 같은 Hash를 반환한다면 어떨까? 정상동작하지만 해쉬 충돌이 발생한다. 해쉬 버켓에 들어있는 Object를 Object가 아닌 LinkedList로 바꿔준다. 집어 넣을 때에도 두 개가 같은 버켓의 LinkedList에 들어간다. 그래서 같은 해쉬값을 가지게 되면, 같은 버켓의 LinkedList를 가져와서 equals로 비교를 하게 된다. 거기서 일치하는 값을 가져온다. 이렇게 되면 HashMap을 쓰는 이유가 없어진다. 그냥 LinkedList를 쓰는 것과 동일하다.
hashCode 구현 방법( @EqualsAndHashCode Lombok 애노테이션이 훨씬 낫다)
핵심 필드 하나의 값의 해쉬값을 계산해서 result 값을 초기화 한다.
기본 타입은 Type.hashCode
참조 타입은 해당 필드의 hashCode
배열은 모든 원소를 재귀적으로 위의 로직을 적용하거나, Arrays.hashCode result = 31*result + 해당 필드의 hashCode 계산 값
result를 리턴한다.(외부에 hashCode를 노출할 필요가 없다.)
완벽 공략 27. 해쉬맵 내부의 연결 리스트
내부 구현은 언제든지 바뀔 수도 있다.
자바 8에서 해쉬 충돌 시 성능 개선을 위해 내부적으로 동일한 버켓에 일정 개수 이상의 엔트리가 추가되면, 연결 리스트 대신 이진 트리를 사용하도록 바뀌었다.
연결 리스트에서 어떤 값을 찾는데 걸리는 시간은?
이진 트리에서 어떤 값을 찾는데 걸리는 시간은?
자바에서 제공하는 LinkedList는 Double Ened LinkedList(이중 말단 연결 리스트), LinkedList의 맨끝이나 맨처음에 추가할 때 성능은 O(1), 조회시 성능은 O(n).
자바 8부터 해쉬 충돌이 자주 발생하는 경우에 대해 성능 최적화가 이뤄졌다. 해쉬 충돌이 자주 발생하는 경우에 대해 넣는 건 O(1)이 걸리기 때문에 문제가 없지만 가져올 땐 O(n)이 걸린다. 여기서 n은 충돌한 엔트리의 갯수이다. 버켓에 쌓여있는 갯수가 8개면, 이진 트리(레드-블랙 트리)로 바꿔준다. 이때 조회할 때 걸리는 O(logn)이 걸린다.
왜 ArrayList가 아니었을까? ArrayList는 배열이기 때문에 그만큼의 사이즈가 필요하다. LinkedList는 레퍼런스 하나만 필요하다. 공간의 효율성을 가져가고자 LinkedList를 사용하지 않았을까?
이중 말단 연결 리스트(Double Ended LinkedList)
헤더에 처음 노드의 참조와 함께 마지막 노드에 대한 참조도 같이 저장되기 때문에 마지막 노드에 대한 접근을 빠르게 할 수 있다는 장점을 가진다.(이중 말단 연결 리스트는 FIFO(first in first out) 구조인 큐(queue)를 구현하기 좋은 방법)
완벽 공략 28. 스레드 안전
멀티 스레드 환경에서 안전한 코드, Thread-safefy
가장 안전한 방법은 여러 스레드 간에 공유하는 데이터가 없는 것!
공유하는 데이터가 있다면:
Synchronization
ThreadLocal
불변 객체 사용
Synchronized 데이터 사용
메서드에 Synchronized를 걸면 성능 상의 문제가 발생할 수 있으니, 더블 체크드 락킹을 사용한다. 이때, volatile을 같이 사용한다.
다른 스레드가 업데이트 했지만 캐시 해 둔 데이터를 가져올 수 있다. 유효하지 않은 데이터를 가져갈 수 있는 경우가 생긴다. volatile를 사용하면 메인 메모리에서 가져온다. 가장 최근에 업데이트된 데이터를 참조하게 된다.
Concurrent 데이터 사용
…
아이템 12. toString을 항상 재정의하라
toString은 간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환해야 한다.
Object의 toString은 클래스이름@16진수로 표시한 해쉬 코드
객체가 가진 모든 정보를 보여주는 것이 좋다.(절대 밖으로 노출 해서는 안되는 정보가 있다. 로깅 정보가 탈취당할 수 있다는 가정하에 로그 조차 남기면 안되는 정보가 있다.)
값 클래스라면 포맷을 문서에 명시하는 것이 좋으며 해당 포맷으로 객체를 생성할 수 있는 정적 팩터리나 생성자를 제공하는 것이 좋다.
toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하는 것이 좋다.
경우에 따라 AutoValue, Lombok 또는 IDE를 사용하지 않는 게 적절할 수 있다.
원하는 포맷이 필요한 경우 → 전화번호, 주소, 좌표(위도, 경도)
아이템 13. clone 재정의는 주의해서 진행하라
애매모호한 clone 규약
clone 규약
x.clone() != x 반드시 true (클론은 반드시 원본과 다른 인스턴스여야 한다.)
x.clone().getClass() == x.getClass() 반드시 true
x.clone().equals(x) true가 아닐 수도 있다 (클론이라는 메서드 안에서 id를 바꾸어줄 수 있다.)
불변 객체라면 다음으로 충분하다.
Cloneable 인터페이스를 구현하고
clone 메서드를 재정의한다. 이때 super.clone()을 사용해야 한다.
public final class PhoneNumber implements Cloneable {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max) {
throw new IllegalArgumentException(arg + " : " + val);
}
return (short) val;
}
**//코드 13-1 가변 상태를 참조하지 않는 클래스용 Clone 메서드(79쪽)
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); //일어날 수 없는 일
}
}**
public static void main(String[] args) {
PhoneNumber phoneNumber = new PhoneNumber(707, 867, 5309);
Map<PhoneNumber, String> m = new HashMap<>();
m.put(phoneNumber, "제니");
**PhoneNumber clone = phoneNumber.clone();
System.out.println(clone != phoneNumber); //반드시 true
System.out.println(clone.getClass() == phoneNumber.getClass()); // 반드시 true
System.out.println(clone.equals(phoneNumber)); //true가 아닐 수 있다.**
}
}
가변 객체의 clone 구현하는 방법
접근 제한자는 public, 반환 타입은 자신의 클래스로 변경한다.
super.clone을 호출한 뒤 필요한 필드를 적절히 수정한다.
배열을 복제할 때는 배열의 clone 메서드를 사용하라.
경우에 따라 final을 사용할 수 없을지도 모른다.
필요한 경우 deep copy를 해야 한다.
super.clone으로 객체를 만든 뒤, 고수준 메서드를 호출하는 방법도 있다.
오버라이딩 할 수 있는 메서드는 참조하지 않도록 조심해야 한다.
상속용 클래스는 Cloneable을 구현하지 않는 것이 좋다.
Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 동기화를 해야 한다.
Shallow Copy
public class HashTable implements Cloneable {
private Entry[] buckets = new Entry[10];
//LinkedList의 Head에 해당하는 노드
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
public void add(Object key, Object value) {
this.next = new Entry(key, value, null);
}
public Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
**/**
* Shallow Copy(얕은 복사)
* 새로 만든 인스턴스 배열의 인스턴스들이 동일한 인스턴스라는 것.
* 배열 내부에 참조하고 있는 인스턴스들이 COPY한 것과 원본이 동일하다.
* TODO hashTable -> entryH[],
* TODO copy -> entryC[]
* TODO entryH[0] == entryC[0]
*/
@Override
public HashTable clone() {
HashTable result = null;
try {
result = (HashTable) super.clone();
result.buckets = this.buckets.clone(); //shallow copy 라서 위험하다.
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}**
public static void main(String[] args) {
HashTable hashTable = new HashTable();
Entry entry = new Entry(new Object(), new Object(), null);
hashTable.buckets[0] = entry;
HashTable clone = hashTable.clone();
**System.out.println(hashTable.buckets[0] == entry);
System.out.println(hashTable.buckets[0] == clone.buckets[0]);**
}
}
Deep Copy
public class HashTable implements Cloneable {
private Entry[] buckets = new Entry[10];
//LinkedList의 Head에 해당하는 노드
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
public void add(Object key, Object value) {
this.next = new Entry(key, value, null);
}
public Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
//deepCopy, 까다롭다. 기존에 있던 키를 그대로 사용했기 때문에 엄청 Deep하진 않다.
@Override
public HashTable clone() {
HashTable result = null;
try {
result = (HashTable) super.clone();
result.buckets = new Entry[this.buckets.length];
result.buckets = createNewBuckets(); //
for (int i = 0; i < this.buckets.length; i++) {
if (buckets[i] != null) {
result.buckets[i] = this.buckets[i].deepCopy(); //83페이지 deep copy
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
protected Entry[] createNewBuckets(){
throw new AssertionError();
}
public static void main(String[] args) {
HashTable hashTable = new HashTable();
Entry entry = new Entry(new Object(), new Object(), null);
hashTable.buckets[0] = entry;
HashTable clone = hashTable.clone();
System.out.println(hashTable.buckets[0] == entry);
System.out.println(hashTable.buckets[0] == clone.buckets[0]);
}
}
Clone 대안
clone 안에서 메소드가 재정의 가능하도록 해서는 안된다.
@Override
public HashTable clone() {
HashTable result = null;
try {
result = (HashTable) super.clone();
result.buckets = new Entry[this.buckets.length];
**result.buckets = createNewBuckets();**
for (int i = 0; i < this.buckets.length; i++) {
if (buckets[i] != null) {
result.buckets[i] = this.buckets[i].deepCopy(); //83페이지 deep copy
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
**// 이렇게 쓰면 안된다..!!
protected Entry[] createNewBuckets(){
throw new AssertionError();
}**
하위 클래스에서 오버라이딩하면 동작이 바뀔 수 있다. 생성자도 마찬가지이다. 객체를 생성하는 과정에 끼어드는 행위를 하려면 굉장히 Straight한 룰을 적용하거나 아니면 아에 오버라이딩하지 못하게 막는 게 좋다.
추상 클래스에서 상속을 허용한 계층 구조로 만든다면, Cloneable을 implements 하지 않는 게 좋다. implements하는 순간 해당 클래스의 하위 클래스를 구현하는 개발자에게 많은 짐을 떠안기는 꼴이 된다. 하위 클래스에서 Clonable을 올바르게 구현하려면 어떻게 구현해야 하는지 많은 고민을 하게 된다. 만약 상위 클래스에서 Cloneable을 구현해야 하는 상황이라면 하위 클래스에서 구현하지 못하도록 막는 방법도 있다.
/**
* 84페이지, 126페이지 일반적으로 상속용 클래스에 Cloneable 인터페이스 사용을 권장하지 않는다.
* 해당 클래스를 확장하려는 프로그래머에게 많은 부담을 주기 때문이다.
*/
public abstract class Shape implements Cloneable{
private int area;
public abstract int getArea();
/**
* 84페이지 부담을 덜기 위해서는 기본 clone() 구현체를 제공하여,
* Cloneable 구현 여부를 서브 클래스가 선택할 수 있다.
* @return
* @throws CloneNotSupportedException
*/
// @Override
// protected Object clone() throws CloneNotSupportedException {
// return super.clone();
// }
/**
* 85페이지 Cloneable 구현을 막을 수도 있다.
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
이 클래스가 멀티 스레드 환경에 안전한 클래스로 만들어져야 한다면, clone 메서드에 sychronized를 붙여야 한다.
현실적으로는.. 생성자를 쓴다. Cloneable은 사용을 지양해야 한다.
ex. TreeSet
public static void main(String[] args) {
Set<String> hashSet = new HashSet<>();
hashSet.add("kim");
hashSet.add("seo");
Set<String> treeSet = new TreeSet<>(hashSet);
}
//카피 생성자
//TreeSet<>() 메소드
//TreeSet을 만들고
//Copy한다.
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
계층 구조가 있을 때 생성자를 쓰면 명확하다. 이전에는 final을 사용하지 못했지만 생성자를 쓰게 되면 final이여도 상관이 없다.(생성자도 처음에 선언해 주기 때문에)
Clone 대신 권장하는 방법
“복사 생성자” 또는 변환 생성자, “복사 팩토리” 또는 변환 팩토리
생성자를 쓰지 않으며, 모호한 규약, 불필요한 검사 예외, final 용법 방해 등에서 벗어날 수 있다.
또 다른 큰 장점 중 하나로 인터페이스 타입의 인스턴스를 리턴할 수 있다.
클라이언트가 복제본의 타입을 결정할 수 있다.
완벽 공략
80페이지, 비검사 예외(UnChecked Exception)였어야 했다는 신호다.
RuntimeException이나 Error를 상속 받은 Exception을 UnChecked Exception이라고 한다.
왜 우리는 UnCheckedException을 선호할까? 작성이 쉽다. 호출을 한다고 하더라도 사용하는 쪽에서 해줄 게 없다. CheckedException이라면 컴파일 에러가 발생하기 때문에 try~catch로 잡거나 throw로 던지거나 해야 한다. 이런 번거로움 때문에 UnCheckedException을 채택해서는 안된다. UnCheckedException 자체가 API이기 때문이다. 클라이언트에게 알려주어야 하기 때문이다.
UnCheckedException은 왜 잡거나 메소드에 선언하라고 강제하지 않을까? RuntimeException은 발생하면 클라이언트가 할 수 있는 게 없다.
정리하면, 단순히 처리하기 쉽고 편하다는 이유만으로 RuntimeException을 선택하지는 말자.
가이드라인 : 클라이언트가 해당 예외 상황을 복구할 수 있다면 검사 예외를 사용하고, 해당 예외가 발생했을 때 아무것도 할 수 없다면, 비검사 예외로 만든다.
81페이지, HashTable과 LinkedList
83페이지, 깊은 복사(deep copy)
83페이지, 리스트가 길면 스택 오버플로를 일으킬 위험이 있기 때문이다.
85페이지, clone 메서드 역시 적절히 동기화해줘야 한다.
86페이지, TreeSet
AbstractSet을 확장된 정렬된 컬렉션
엘리먼트를 추가한 순서는 중요하지 않다.
엘리먼트가 지닌 자연적인 순서(natural order)에 따라 정렬한다.
자연적인 순서가 없는 것을 넣게 되면? 못 넣는 다.. Comparable을 구현해야 한다. 또는 Comparator.comparingInt(Example::hashCode);를 넣어주어야 한다.
오름차순으로 정렬한다.
스레드 안전하지 않다.
동기화 블록이 적용되어 있는 트리셋이 필요하다면, Set examples = Collection.synchronizedSet(new TreeSet())으로 감싸서 사용하면 된다. Set에 있는 모든 오퍼레이션이 전부 싱크로나이즈드가 걸리게 되고 성능은 당연히 느려진다. 한 번에 하나의 쓰레드만 접근 가능하니까 대신 안정적이다.
이진 검색 트리, 레드 블랙 트리 학습 필요
아이템 14. Comparable을 구현할지 고민하라
compareTo 규약
Object.equals에 더해서 순서까지 비교할 수 있으며 Generic을 지원한다.
자기 자신이 (this)이 compareTo에 전달된 객체보다 작으면 음수, 같으면 0, 크다면 양수를 리턴한다.(-1이나 1을 기대하고 프로그래밍 해서는 절대 안된다.)
반사성, 대칭성, 추이성을 만족해야 한다.
반드시 따라야 하는 것은 아니지만 x.compareTo(y) == 0이라면 x.equals(y)가 true이여야 한다.
compareTo 구현 방법1
자연적인 순서를 제공할 클래스에 Implements Comparatable을 선언한다.
compareTo 메서드를 재정의한다.
compareTo 메서드 안에서 기본 타입은 박싱된 기본 타입의 compare을 사용해 비교한다.
핵심 필드가 여러 개라면 비교 순서가 중요하다. 순서를 결정하는데 있어서 가장 중요한 필드를 비교하고 그 값이 0이라면 다음 필드를 비교한다.
기존 클래스를 확장하고 필드를 추가하는 경우 compareTo 규약을 지킬 수 없다.
Composition을 활용할 것.
compareTo 구현 방법2
자바 8부터 함수형 인터페이스, 람다, 메서드 래퍼런스와 Comparator가 제공하는 기본 메서드와 static 메서드를 사용해서 Comparator를 구현할 수 있다.
Comparator가 제공하는 메서드 사용하는 방법
Comparator의 static 메서드를 사용해서 Comparator 인스턴스 만들기
인스턴스를 만들었다면 default 메서드를 사용해서 메서드 호출 이어가기 (체이닝)
static 메서드와 default 메서드의 매개변수로는 람다 표현식 또는 메서드 래퍼런스를 사용할 수 있다.
완벽공략
90페이지, 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일 타임에 정해진다.
3장 모든 객체의 공통 메서드
Object는 객체를 만들 수 있는 구체 클래스이지만 기본적으로 상속해서 사용하도록 설계되어 있다. Object에서 final이 아닌 메서드(equals, hashCode, toString, clone, finalize)는 모두 재정의(overriding)을 염두에 두고 설계된 것이라 재정의 시 지켜야할 일반 규약이 명확히 정의되어 있다.
Item10. equals는 일반 규약을 지켜 재정의하라
equals를 재정의 하지 않는 것이 최선이다. 아래의 경우에 해당한다면 equals를 재정의 할 필요가 없다.
ex 2) 동치성을 검사할 필요가 없는 경우 문자열 “Hello” == “Hello”
equals를 재정의 해야 한다면 반드시 Object 명세에 적힌 일반 규약을 따라야 한다.
대칭성 : A.equals(B) == B.equals(A) ex) CaseInsensitiveString i) 대칭성 위배
추이성 : A.equals(B) && B.equals(C), A.equals(C) ex) ColorPoint i) 대칭성 위배
null-아님 : A.equals(null) == false → 명시적으로 null을 검사하지 말고 묵시적으로 null을 검사하자.
equals 구현 방법
equals 구현 툴
AutoValue
Lombok
마찬가지로, 애노테이션 프로세서를 사용한다. 컴파일 타임에 애노테이션 정보를 읽어서 클래스에 코드를 추가해 준다. 이러한 기존 코드를 뜯어 고치는 것은 자바가 기대하지 않았던 동작이다. 일종의 롬복이 해킹해서 자바가 의도하지 않았지만 손쉽게 쓸 수 있도록 만들어 주는 것이다. 이렇게 의도하지 않은 방법으로 접근 하는 것은 JDK 버전이 올라감에 따라 막힐 수 있다. 권장하진 않지만 막힐 일은 없을 것 같다.
Record(14버전 부터 들어왔고 15버전부터 사용가능함) VO를 만드는 데 있어, 이렇게 애노테이션을 붙이지 않아도 된다.
Intellij에서 equals와 hashCode를 Override해서 사용한다.
자바 11버전인 경우 → Lombok
자바 17버전인 경우 → Record, Record가 적절하지 않다면 Lombok
주의 사항
완벽 공략 24. Value 기반의 클래스(VO)
클래스 처럼 생겼지만 Int 처럼 동작하는 클래스
Record
equals와 hashCode를 구현해 주고, toString도 구현되어 있음을 알 수 있다.
한 번 쓰게 되면 고칠 수 없고 변수명으로 가져온다.
불변하고 식별자가 없고 딱 안에 들어있는 값으로 equals가 구현되어 있다.
완벽공략 25. StackOverflowError
로컬 변수와 객체가 저장되는 공간의 이름은?
완벽공략 26. 리스코프 치환 원칙
객체 지향 5대 원칙 SOLID 중 하나
아이템 11. equals를 재정의하려거든 hashCode도 재정의하라
hashCode 규약
위 코드에서 마지막 출력 값이 null이 나오는 이유는 새로 생성된 객체에 대한 해시값에 해당하는 값이 없기 때문이다. Key에 해당하는 Hash값을 가져온다. 그리고 Hash에 해당하는 버킷의 Object를 꺼내온다.
그렇다면 다른 Key에 대해 같은 Hash를 반환한다면 어떨까? 정상동작하지만 해쉬 충돌이 발생한다. 해쉬 버켓에 들어있는 Object를 Object가 아닌 LinkedList로 바꿔준다. 집어 넣을 때에도 두 개가 같은 버켓의 LinkedList에 들어간다. 그래서 같은 해쉬값을 가지게 되면, 같은 버켓의 LinkedList를 가져와서 equals로 비교를 하게 된다. 거기서 일치하는 값을 가져온다. 이렇게 되면 HashMap을 쓰는 이유가 없어진다. 그냥 LinkedList를 쓰는 것과 동일하다.
hashCode 구현 방법( @EqualsAndHashCode Lombok 애노테이션이 훨씬 낫다)
완벽 공략 27. 해쉬맵 내부의 연결 리스트
내부 구현은 언제든지 바뀔 수도 있다.
자바에서 제공하는 LinkedList는 Double Ened LinkedList(이중 말단 연결 리스트), LinkedList의 맨끝이나 맨처음에 추가할 때 성능은 O(1), 조회시 성능은 O(n).
자바 8부터 해쉬 충돌이 자주 발생하는 경우에 대해 성능 최적화가 이뤄졌다. 해쉬 충돌이 자주 발생하는 경우에 대해 넣는 건 O(1)이 걸리기 때문에 문제가 없지만 가져올 땐 O(n)이 걸린다. 여기서 n은 충돌한 엔트리의 갯수이다. 버켓에 쌓여있는 갯수가 8개면, 이진 트리(레드-블랙 트리)로 바꿔준다. 이때 조회할 때 걸리는 O(logn)이 걸린다.
왜 ArrayList가 아니었을까? ArrayList는 배열이기 때문에 그만큼의 사이즈가 필요하다. LinkedList는 레퍼런스 하나만 필요하다. 공간의 효율성을 가져가고자 LinkedList를 사용하지 않았을까?
이중 말단 연결 리스트(Double Ended LinkedList)
헤더에 처음 노드의 참조와 함께 마지막 노드에 대한 참조도 같이 저장되기 때문에 마지막 노드에 대한 접근을 빠르게 할 수 있다는 장점을 가진다.(이중 말단 연결 리스트는 FIFO(first in first out) 구조인 큐(queue)를 구현하기 좋은 방법)
완벽 공략 28. 스레드 안전
멀티 스레드 환경에서 안전한 코드, Thread-safefy
아이템 12. toString을 항상 재정의하라
아이템 13. clone 재정의는 주의해서 진행하라
애매모호한 clone 규약
가변 객체의 clone 구현하는 방법
Shallow Copy
Deep Copy
Clone 대안
clone 안에서 메소드가 재정의 가능하도록 해서는 안된다.
하위 클래스에서 오버라이딩하면 동작이 바뀔 수 있다. 생성자도 마찬가지이다. 객체를 생성하는 과정에 끼어드는 행위를 하려면 굉장히 Straight한 룰을 적용하거나 아니면 아에 오버라이딩하지 못하게 막는 게 좋다.
추상 클래스에서 상속을 허용한 계층 구조로 만든다면, Cloneable을 implements 하지 않는 게 좋다. implements하는 순간 해당 클래스의 하위 클래스를 구현하는 개발자에게 많은 짐을 떠안기는 꼴이 된다. 하위 클래스에서 Clonable을 올바르게 구현하려면 어떻게 구현해야 하는지 많은 고민을 하게 된다. 만약 상위 클래스에서 Cloneable을 구현해야 하는 상황이라면 하위 클래스에서 구현하지 못하도록 막는 방법도 있다.
이 클래스가 멀티 스레드 환경에 안전한 클래스로 만들어져야 한다면, clone 메서드에 sychronized를 붙여야 한다.
현실적으로는.. 생성자를 쓴다. Cloneable은 사용을 지양해야 한다.
ex. TreeSet
계층 구조가 있을 때 생성자를 쓰면 명확하다. 이전에는 final을 사용하지 못했지만 생성자를 쓰게 되면 final이여도 상관이 없다.(생성자도 처음에 선언해 주기 때문에)
Clone 대신 권장하는 방법
완벽 공략
아이템 14. Comparable을 구현할지 고민하라
compareTo 규약
compareTo 구현 방법1
compareTo 구현 방법2
완벽공략