Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 아이템 40. @Override 애너테이션을 일관되게 사용하라

- 자바가 기본으로 제공하는 애너테이션 중 @Override은 메서드 선언에만 달 수 있다.
- @Override이 달렸다는 것은 상위 타입의 메서드를 재정의했음을 의미한다.
- @Override을 일관되게 사용하면 여러 가지 악명 높은 버그들을 예방해준다.

```java
import java.util.HashSet;
import java.util.Set;

// 코드 40-1 영어 알파벳 2개로 구성된 문자열(바이그램)을 표현하는 클래스 - 버그를 찾아보자. (246쪽)
public class Bigram {
private final char first;
private final char second;

public Bigram(char first, char second) {
this.first = first;
this.second = second;
}

public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}

public int hashCode() {
return 31 * first + second;
}

public static void main(String[] args) {
Set<Bigram> s = new HashSet<>();
for (int i = 0; i < 10; i++) {
for (char ch = 'a'; ch <= 'z'; ch++) {
s.add(new Bigram(ch, ch));
}
}
System.out.println(s.size());
}
}
```

- equals 메소드를 재정의(overriding)한 것이 아니라 다중정의(overloading, 아이템 52)했다.
- Object의 equals를 재정의하려면 매개변수 타입을 Object로 해야하는데 Bigram 타입으로 정의했다.

```java
@Override
public boolean equals(Object o) {
if (!(o instanceof Bigram2)) {
return false;
}
Bigram2 b = (Bigram2) o;
return b.first == first && b.second == second;
}
```

- 해당 오류는 컴파일러가 찾아낼 수 있지만, Object.equals를 재정의한다는 의도를 명시해야 한다.
- 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자.
- 구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때는 @Override를 선언하지 않아도 된다.
- 구체 클래스인데 아직 구현하지 않은 추상 메서드가 남아 있다면 컴파일러가 알려주기 때문
- IDE에 관련 설정을 통해 재정의할 의도였으나 실수로 새로운 메서드를 추가했을 때 알려주는 컴파일 오류의 보완재 역할을 대신할 수 있다.
- @Override는 클래스뿐 아니라 인터페이스의 메서드를 재정의할 때도 사용하는 것이 좋다.
- 인터페이스가 디폴트 메서드를 지원하면서 구현 메서드에 @Override를 선언하는 습관을 들이면 메서드 시그니처가 올바른지 재차 확신할 수 있다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라

```java
public interface Serializable {
}
```

아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 **마커 인터페이스(marker interface)**라 한다. Serializable 인터페이스(12장)가 좋은 예시이다.

마커 애너테이션(아이템 39)이 등장하면서 마커 인터페이스는 구식이 되었다는 이야기가 있지만, 사실이 아니다. 마커 인터페이스는 두 가지 면에서 마커 애너테이션보다 낫다.

- 마커 인터페이스는 이를 구현한 클래스의 인스턴스를 구분하는 타입으로 사용할 수 있으나, 마커 애너테이션은 그렇지 않다.
- 적용 대상을 더 정밀하게 지정할 수 있다.
- 마커 애너테이션은 적용 대상(@Target)을 ElementType.TYPE으로 선언한 경우 클래스, 인터페이스, 열거 타입, 애너테이션에 달 수 있는데 이는 다시말해 타입을 더 세밀하게 제한하지 못한다는 뜻이다.
- Set 인터페이스도 일종의(제약이 있는) 마커 인터페이스로 볼 수 있다.
- Set을 마커 인터페이스로 생각하지 않는 의견 중 하나가 add, equals, hashCode 등 Collection의 메서드 몇 개의 규약을 살짝 수정했기 때문이다.

```java
final class InvariantHuman {
private int age;
private String name;

public InvariantHuman(String name) {
this(0, name);
}

public InvariantHuman(int age, String name) {
if (age < 0) {
throw new IllegalArgumentException("나이는 0보다 작을 수 없다는 것은 불변한 사실이다.");
}
this.age = age;
this.name = name;
}

public int getAge() {
return age;
}

public String getName() {
return name;
}
}
```

- 마커 인터페이스는 객체의 특정 부분을 불변식(invariant)로 규정하거나, 그 타입의 인스턴스는 다른 클래스의 특정 메서드가 처리할 수 있다는 사실을 명시하는 용도로 사용할 수 있을 것이다.(Serializable 인터페이스가 ObjectOutputStream이 처리할 수 있는 인스턴스임을 명시하듯이)
- 불변식(invariant): 어떤 객체가 정상적으로 작동하기 위해 허무러지지 않아야 하는 값, 식, 상태의 일관성을 보장하기 위해 항상 참이 되는 조건(condition)을 말한다.
- InvariantHuman 클래스에서 age는 항상 0보다 크거나 같다는 조건을 항상 만족하면 불변식을 만족한다고 할 수 있다.
- 반대로 마커 애너테이션이 인터페이스보다 나은점은 거대한 애너테이션 시스템의 지원을 받는다는 것이다.

정리하면

- 클래스와 인터페이스 외의 프로그램 요소(모듈, 패키지, 필드, 지역변수 등)에 마킹이 필요할 때 애너테이션을 사용
- 클래스와 인터페이스만이 인터페이스를 구현하거나 확장할 수 있기 때문
- 클래스나 인터페이스에 마커를 사용해야 할 때, “이 마킹된 객체를 매개변수로 받는 메서드를 작성해야 하는가?”의 질문에 “그렇다”라면 인터페이스를 사용
- 마커 인터페이스는 컴파일 타임에 오류를 검출해낼 수 있다.
- 타입을 정의가 목적이라면 인터페이스를 사용

## Serializable의 오류

```java
public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants {
...(중략)

/**
* Write the specified object to the ObjectOutputStream. The class of the
* object, the signature of the class, and the values of the non-transient
* and non-static fields of the class and all of its supertypes are
* written. Default serialization for a class can be overridden using the
* writeObject and the readObject methods. Objects referenced by this
* object are written transitively so that a complete equivalent graph of
* objects can be reconstructed by an ObjectInputStream.
*
* <p>Exceptions are thrown for problems with the OutputStream and for
* classes that should not be serialized. All exceptions are fatal to the
* OutputStream, which is left in an indeterminate state, and it is up to
* the caller to ignore or recover the stream state.
*
* @throws InvalidClassException Something is wrong with a class used by
* serialization.
* @throws NotSerializableException Some object to be serialized does not
* implement the java.io.Serializable interface.
* @throws IOException Any exception thrown by the underlying
* OutputStream.
*/
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
...
}
```

Serializable 마커 인터페이스를 보고 그 대상이 직렬화할 수 있는 타입인지 확인한다. 위 메소드의 인수는 당연히 Serializable를 구현했을 거라고 가정하지만 해당 메서드는 Serializable이 아닌 Object를 받도록 설계되었다. 즉, 직렬화할 수 없는 객체를 넘겨도 런타임에야 문제를 확인할 수 있다. 마커 인터페이스를 사용하는 주요 이유가 컴파일타임 오류 검출인데, 그 이점을 살리지 못한 것이다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 아이템 42. 익명 클래스보다는 람다를 사용하라

```java
List<String> words = Arrays.asList(args);

// 코드 42-1 익명 클래스의 인스턴스를 함수 객체로 사용 - 낡은 기법이다! (254쪽)
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
```

- 1997년 JDK1.1이 등장하면서 함수 객체를 만드는 수단은 익명 클래스(아이템 24)가 되었다.
- 익명 클래스 방식은 코드가 너무 길어 자바는 함수형 프로그래밍에 적합하지 않았다.
- 자바 8에서 추상 메서드가 하나인 인터페이스는 특별한 의미를 인정받게됐다.
- 지금의 함수형 인터페이스, 람다식(lambda expression)
- 람다는 함수나 익명 클래스와 개념은 비슷하지만 코드는 훨씬 간결하다.

```java
// 코드 42-2 람다식을 함수 객체로 사용 - 익명 클래스 대체 (255쪽)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
```

- 해당 람다식의 매개변수, 반환 타입은 컴파일러가 문맥을 살펴 타입을 추론해준다.
- 상황에 따라 컴파일러가 결정하지 못할 수 있는데, 이 땐 프로그래머가 직접 명시해줘야한다.
- 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자

> [아이템 26](https://www.notion.so/26-ecd3e8c883e84b9bb1ff5462501dfe84?pvs=21), [29](https://www.notion.so/29-cb1c5e345dbe464195c4fb68f6b46b80?pvs=21), [30](https://www.notion.so/30-c788b257745348a79bd4d24ad69be572?pvs=21)의 내용은 람다를 사용할 때 더 중요해진다.
컴파일러가 타입을 추론하는 데 필요한 정보 대부분 제네릭에서 얻기 때문이다. 만약 해당 정보를 제공하지 않으면 컴파일러가 타입을 추론할 수 없고 개발자가 일일이 명시해야 한다.
>

```java
// 코드 42-2 람다식을 함수 객체로 사용 - 익명 클래스 대체 (255쪽)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
System.out.println(words);
Collections.shuffle(words);

// 람다 자리에 비교자 생성 메서드(메서드 참조와 함께)를 사용 (255쪽)
Collections.sort(words, comparingInt(String::length));
System.out.println(words);
Collections.shuffle(words);

// 비교자 생성 메서드와 List.sort를 사용 (255쪽)
words.sort(comparingInt(String::length));
System.out.println(words);
```

- 자바 8의 API를 사용하면 더 간결하게 만들 수 있다.

```java
// 코드 42-4 함수 객체(람다)를 인스턴스 필드에 저장해 상수별 동작을 구현한 열거 타입 (256-257쪽)
public enum Operation {
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y),
TIMES("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);

private final String symbol;
private final DoubleBinaryOperator op;

Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}

@Override
public String toString() {
return symbol;
}

public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}

// 아이템 34의 메인 메서드 (215쪽)
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
}
```

- 상수별로 다르게 동작해야하는 코드에서도 람다를 사용하면 쉽게 구현할 수 있다.
- DoubleBinaryOperator 인터페이스는 java.util.function 패키지가 제공하는 다양한 함수 인터페이스(아이템 44) 중 하나이다.
- 메서드나 클래스와 달리, 람다는 이름이 없고 문서화를 못 한다.
- 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다 사용을 지양하는 것이 좋다.
- 람다는 한 줄일 때 가장 좋고 길어야 세 줄 안에 끝내는 것이 좋다.
- 열거 타입 생성자에 넘겨지는 인수들의 타입도 컴파일타임에 추론된다. 따라서, 열거 타입 생성자 안의 람다는 열거 타입의 인스턴스 멤버에 접근할 수 없다.(인스턴스는 런타임에 만들어지기 때문)
- 람다는 자신을 참조할 수 없다. 람다에서의 this 키워드는 바깥 인스턴스를 가리킨다.
- 익명 클래스에서의 this는 익명 클래스의 인스턴스 자신을 가리킨다.
- 함수 객체가 자신을 참조해야 한다면 반드시 익명 클래스를 사용해야한다.
- 람다도 익명 클래스처럼 직렬화 형태가 구현별로(ex, 가상머신별로) 다를 수 있다.
- 람다를 직렬화하는 일은 극히 삼가한다.
- 직렬화가 필요한 함수 객체(Comparator)는 private 정적 중첩 클래스(아이템 24)의 인스턴스를 사용하자.
33 changes: 33 additions & 0 deletions src/main/java/item40/Bigram.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package item40;

import java.util.HashSet;
import java.util.Set;

// 코드 40-1 영어 알파벳 2개로 구성된 문자열(바이그램)을 표현하는 클래스 - 버그를 찾아보자. (246쪽)
public class Bigram {
private final char first;
private final char second;

public Bigram(char first, char second) {
this.first = first;
this.second = second;
}

public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}

public int hashCode() {
return 31 * first + second;
}

public static void main(String[] args) {
Set<Bigram> s = new HashSet<>();
for (int i = 0; i < 10; i++) {
for (char ch = 'a'; ch <= 'z'; ch++) {
s.add(new Bigram(ch, ch));
}
}
System.out.println(s.size());
}
}
38 changes: 38 additions & 0 deletions src/main/java/item40/Bigram2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package item40;

import java.util.HashSet;
import java.util.Set;

// 버그를 고친 바이그램 클래스 (247쪽)
public class Bigram2 {
private final char first;
private final char second;

public Bigram2(char first, char second) {
this.first = first;
this.second = second;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof Bigram2)) {
return false;
}
Bigram2 b = (Bigram2) o;
return b.first == first && b.second == second;
}

public int hashCode() {
return 31 * first + second;
}

public static void main(String[] args) {
Set<Bigram2> s = new HashSet<>();
for (int i = 0; i < 10; i++) {
for (char ch = 'a'; ch <= 'z'; ch++) {
s.add(new Bigram2(ch, ch));
}
}
System.out.println(s.size());
}
}
26 changes: 26 additions & 0 deletions src/main/java/item41/InvariantHuman.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package item41;

final class InvariantHuman {
private int age;
private String name;

public InvariantHuman(String name) {
this(0, name);
}

public InvariantHuman(int age, String name) {
if (age < 0) {
throw new IllegalArgumentException("나이는 0보다 작을 수 없다는 것은 불변한 사실이다.");
}
this.age = age;
this.name = name;
}

public int getAge() {
return age;
}

public String getName() {
return name;
}
}
Loading