diff --git "a/docs/\354\225\204\354\235\264\355\205\234 40. @Override \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\235\274\352\264\200\353\220\230\352\262\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/docs/\354\225\204\354\235\264\355\205\234 40. @Override \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\235\274\352\264\200\353\220\230\352\262\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..57fd0fa --- /dev/null +++ "b/docs/\354\225\204\354\235\264\355\205\234 40. @Override \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\235\274\352\264\200\353\220\230\352\262\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -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 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를 선언하는 습관을 들이면 메서드 시그니처가 올바른지 재차 확신할 수 있다. \ No newline at end of file diff --git "a/docs/\354\225\204\354\235\264\355\205\234 41. \354\240\225\354\235\230\355\225\230\353\240\244\353\212\224 \352\262\203\354\235\264 \355\203\200\354\236\205\354\235\264\353\235\274\353\251\264 \353\247\210\354\273\244 \354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/docs/\354\225\204\354\235\264\355\205\234 41. \354\240\225\354\235\230\355\225\230\353\240\244\353\212\224 \352\262\203\354\235\264 \355\203\200\354\236\205\354\235\264\353\235\274\353\251\264 \353\247\210\354\273\244 \354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..1704110 --- /dev/null +++ "b/docs/\354\225\204\354\235\264\355\205\234 41. \354\240\225\354\235\230\355\225\230\353\240\244\353\212\224 \352\262\203\354\235\264 \355\203\200\354\236\205\354\235\264\353\235\274\353\251\264 \353\247\210\354\273\244 \354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -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. + * + *

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를 받도록 설계되었다. 즉, 직렬화할 수 없는 객체를 넘겨도 런타임에야 문제를 확인할 수 있다. 마커 인터페이스를 사용하는 주요 이유가 컴파일타임 오류 검출인데, 그 이점을 살리지 못한 것이다. \ No newline at end of file diff --git "a/docs/\354\225\204\354\235\264\355\205\234 42. \354\235\265\353\252\205 \355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224 \353\236\214\353\213\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/docs/\354\225\204\354\235\264\355\205\234 42. \354\235\265\353\252\205 \355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224 \353\236\214\353\213\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..460a23d --- /dev/null +++ "b/docs/\354\225\204\354\235\264\355\205\234 42. \354\235\265\353\252\205 \355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224 \353\236\214\353\213\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,98 @@ +# 아이템 42. 익명 클래스보다는 람다를 사용하라 + +```java +List words = Arrays.asList(args); + +// 코드 42-1 익명 클래스의 인스턴스를 함수 객체로 사용 - 낡은 기법이다! (254쪽) +Collections.sort(words, new Comparator() { + 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)의 인스턴스를 사용하자. \ No newline at end of file diff --git a/src/main/java/item40/Bigram.java b/src/main/java/item40/Bigram.java new file mode 100644 index 0000000..501ecca --- /dev/null +++ b/src/main/java/item40/Bigram.java @@ -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 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()); + } +} diff --git a/src/main/java/item40/Bigram2.java b/src/main/java/item40/Bigram2.java new file mode 100644 index 0000000..b654236 --- /dev/null +++ b/src/main/java/item40/Bigram2.java @@ -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 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()); + } +} diff --git a/src/main/java/item41/InvariantHuman.java b/src/main/java/item41/InvariantHuman.java new file mode 100644 index 0000000..11062f0 --- /dev/null +++ b/src/main/java/item41/InvariantHuman.java @@ -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; + } +} diff --git a/src/main/java/item42/Operation.java b/src/main/java/item42/Operation.java new file mode 100644 index 0000000..59d0e81 --- /dev/null +++ b/src/main/java/item42/Operation.java @@ -0,0 +1,37 @@ +package item42; + +import java.util.function.DoubleBinaryOperator; + +// 코드 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)); + } + } +} diff --git a/src/main/java/item42/SortFourWays.java b/src/main/java/item42/SortFourWays.java new file mode 100644 index 0000000..8b8042c --- /dev/null +++ b/src/main/java/item42/SortFourWays.java @@ -0,0 +1,38 @@ +package item42; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static java.util.Comparator.comparingInt; + +// 함수 객체로 정렬하기 (254-255쪽) +public class SortFourWays { + public static void main(String[] args) { + List words = Arrays.asList(args); + + // 코드 42-1 익명 클래스의 인스턴스를 함수 객체로 사용 - 낡은 기법이다! (254쪽) + Collections.sort(words, new Comparator() { + public int compare(String s1, String s2) { + return Integer.compare(s1.length(), s2.length()); + } + }); + System.out.println(words); + Collections.shuffle(words); + + // 코드 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); + } +}