diff --git "a/\352\271\200\354\210\230\353\271\210/6\354\236\245/item34.md" "b/\352\271\200\354\210\230\353\271\210/6\354\236\245/item34.md" new file mode 100644 index 0000000..474df1d --- /dev/null +++ "b/\352\271\200\354\210\230\353\271\210/6\354\236\245/item34.md" @@ -0,0 +1,215 @@ +## item34 예제, 열거 타입의 제약 + +ex 1. 데이터와 메서드를 갖는 열거 타입 + +public enum Planet { MERCURY(3.302e+23, 2.439e6), VENUS (4.869e+24, 6.052e6), EARTH (5.975e+24, 6.378e6), MARS ( +6.419e+23, 3.393e6), JUPITER(1.899e+27, 7.149e7), SATURN (5.685e+26, 6.027e7), URANUS (8.683e+25, 2.556e7), NEPTUNE( +1.024e+26, 2.477e7); + +```java + private final double mass; // 질량(단위: 킬로그램) +private final double radius; // 반지름(단위: 미터) +private final double surfaceGravity; // 표면중력(단위: m / s^2) + +// 중력상수(단위: m^3 / kg s^2) +private static final double G=6.67300E-11; + + // 생성자 + Planet(double mass,double radius){ + this.mass=mass; + this.radius=radius; + surfaceGravity=G*mass/(radius*radius); + } + +public double mass(){return mass;} +public double radius(){return radius;} +public double surfaceGravity(){return surfaceGravity;} + +public double surfaceWeight(double mass){ + return mass*surfaceGravity; // F = ma + } + } +``` + +```java +public class WeightTable { + public static void main(String[] args) { + double earthWeight = Double.parseDouble(args[0]); + double mass = earthWeight / Planet.EARTH.surfaceGravity(); + for (Planet p : Planet.values()) + System.out.printf("%s에서의 무게는 %f이다.%n", + p, p.surfaceWeight(mass)); + } +} +``` + +열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드인 values를 제공. + +값들은 선언된 순서로 저장된다. (그렇다고 이 순서에 의존하는 코드를 작성하는 것은 매우 위험) + +ex 2. 값에 따라 분기하는 열거 타입 + +```java +public enum Operation { + PLUS, MINUS, TIMES, DIVDE; + + public double apply(double x, double y) { + switch (this) { + case PLUS: + return x + y; + case MINUS: + return x - y; + case TIMES: + return x * y; + case DIVDE: + return x / y; + } + throw new AssertionError("알 수 없는 연산:" + this); + } +} +``` + +-> 깨지기 쉬운 코드 : 새로운 상수 추가시 해당 case문도 추가해야한다. 혹시라도 깜빡하게 되면 오류가 발생한다. + +ex 3. 상수별 클래스 몸체와 데이터를 사용한 열거 타입 + +- 열거 타입에 apply라는 추상 메서드를 선언하고 각 상수별 클래스 몸체, 즉 각 상수에서 자신에 맞게 재정의하는 방법. + +-> apply가 추상 메서드이므로 재정의하지 않았다면 컴파일 오류로 알려준다. (깜빡할 수가 없다.) + +```java +public enum Operation { + PLUS("+") { + public double apply(double x, double y) { + return x + y; + } + }, + MINUS("-") { + public double apply(double x, double y) { + return x - y; + } + }, + TIMES("*") { + public double apply(double x, double y) { + return x * y; + } + }, + DIVDE("/") { + public double apply(double x, double y) { + return x / y; + } + } +} +``` + +```java + private final String symbol; + + Operation(String symbol){ + this.symbol=symbol; + } + +@Override +public String toString(){ + return symbol; + } + +public abstract double apply(double x,double y); + } +``` + +ex 4. 열거 타입용 fromString 메서드 구현하기 + +- toString 재정의시 toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 fromString 메서드 구현도 고려. + +```java +private static final Map stringToEnum= + Stream.of(Operation.values()) + .collect(Collectors.toMap(Operation::toString,operation->operation)); +// {문자열, 열거 타입 상수} + +//Optional로 반환하여 값이 존재하지않을 상황을 클라이언트에게 알린다. +public static Optional fromString(String symbol){ + return Optional.ofNullable(stringToEnum.get(symbol)); + } + +``` + +열거 타입의 제약 + +1. 생성자에서 접근할 수 있는 것은 상수 변수뿐이다. + +- 열거 타입의 생성자가 실행되는 시점에는 정적 필드가 초기화되기 전이기 때문에 생성자에서 정적 필드를 참조하려고 시도하면 컴파일 에러가 발생한다. + +따라서 자신의 인스턴스를 추가하지 못하게 하는 제약이 존재. + +(It is illegal to access static member 'ENUM' from enum constructor or instance initializer) + +열거 타입 상수 생성 순서 + +2. 열거 타입 상수끼리 코드를 공유하기 어렵다. + +-> 전략 열거 타입 패턴 (내부에 또 다른 enum을 정의해서 선택하도) + +```java +public enum PayrollDay { + MONDAY(PayType.WEEKDAY), + TUESDAY(PayType.WEEKDAY), + WEDNESDAY(PayType.WEEKDAY), + THURSDAY(PayType.WEEKDAY), + FRIDAY(PayType.WEEKDAY), + SATURDAY(PayType.WEEKEND), + SUNDAY(PayType.WEEKEND); + + private final PayType payType; + + PayrollDay(PayType payType) { + this.payType = payType; + } + + int pay(int minutesWorked, int payRate) { + return payType.pay(minutesWorked, payRate); + } + + private enum PayType { + WEEKDAY { + int overtimePay(int minutesWorked, int payRate) { + return minutesWorked <= MINS_PER_SHIFT ? + 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2; + } + }, + WEEKEND { + int overtimePay(int minutesWorked, int payRate) { + return minutesWorked * payRate / 2; + } + }; + + abstract int overtimePay(int minutesWorked, int payRate); + + private static final int MINS_PER_SHIFT = 8 * 60; + + int pay(int minutesWorked, int payRate) { + int basePay = minutesWorked * payRate; + return basePay + overtimePay(minutesWorked, payRate); + } + } +} +``` + +3. 코드를 수정 할 수 없는 기존 열거 타입에 상수별 동작을 넣을 때는 switch문이 좋은 선택이 될 수 있다. + +```java +public static Operation inverse(Operation operation){ + switch(operation){ + case PLUS: + return Operation.MINUS; + case MINUS: + return Operation.PLUS; + case TIMES: + return Operation.DIVDE; + case DIVDE: + return Operation.TIMES; + } + throw new AssertionError("알 수 없는 연산 : "+operation); + } +``` diff --git "a/\352\271\200\354\210\230\353\271\210/6\354\236\245/item37.md" "b/\352\271\200\354\210\230\353\271\210/6\354\236\245/item37.md" new file mode 100644 index 0000000..d40ab18 --- /dev/null +++ "b/\352\271\200\354\210\230\353\271\210/6\354\236\245/item37.md" @@ -0,0 +1,91 @@ +## ordinal 인덱싱 대신 EnumMap을 사용하라 + +```java +class Plant { + enum LifeCycIe {AMMUAL, PERENNIAL, BIEMMIAL} + + final String name; + final LifeCycIe lifeCycle; + + Plant(String name, LifeCycIe lifeCycle) { + this.name = name; + this.lifeCycle = lifeCycle; + } + + @Override + public String toString() { + return name; + } +} +``` + +### ordinal() 을 배열 인덱스로 사용한 예 - 따라하면 안 됨 + +```java +public static void usingOrdinalArray(List garden){ +// 1. 배열과 제네릭의 호환성 문제 + Set[]plantsByLifeCycle=(Set[])new Set[LifeCycle.values().length]; + for(int i=0;i(); + } + + for(Plant plant:garden){ + // 2. 열거 타입의 ordinal 사용 + plantsByLifeCycle[plant.lifeCycle.ordinal()].add(plant); + } + + for(int i=0;i garden){Map>plantsByLifeCycle=new EnumMap<>( + LifeCycle.class); + + for(LifeCycle lifeCycle:LifeCycle.values()){ + plantsByLifeCycle.put(lifeCycle,new HashSet<>()); + } + + for(Plant plant:garden){ + plantsByLifeCycle.get(plant.lifeCycle).add(plant); + } + System.out.println(plantsByLifeCycle); + + } +``` + +- 더 짧고 명료하고 안전하고 성능도 원래 버전과 비등 + +: 내부적으로 배열을 사용하므로 배열의 성능을 유지하면서도, 열거 타입의 타입 안정성을 활용 + +- 안전하지 않은 형변환은 쓰지 않고, 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공 + +### 스트림 사용 + +```java +public static void streamEx1(List garden){Map plantsByLifeCycle=garden.stream().collect( + Collectors.groupingBy(plant->plant.lifeCycle));System.out.println(plantsByLifeCycle);} + +// 성능 개선을 위해 EnumMap과 Set 사용 public static void streamEx2(List garden) { Map plantsByLifeCycle = garden.stream() + .collect(Collectors.groupingBy(plant->plant.lifeCycle, + ()->new EnumMap<>(LifeCycle.class),Collectors.toSet()));System.out.println(plantsByLifeCycle);} + +``` + +- EnumMap 버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만, 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다. + +### 정리 + +배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라. + +다차원 관계는 EnumMap<..., EnumMap<...>> 으로 표현하라 \ No newline at end of file diff --git "a/\352\271\200\354\210\230\353\271\210/6\354\236\245/item40.md" "b/\352\271\200\354\210\230\353\271\210/6\354\236\245/item40.md" new file mode 100644 index 0000000..2d301e6 --- /dev/null +++ "b/\352\271\200\354\210\230\353\271\210/6\354\236\245/item40.md" @@ -0,0 +1,66 @@ +## @Override 애너테이션을 일관되게 사용하라 + +### @Override가 없을 때 하기 쉬운 실수 + +```java +public class Item40Test { + static 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; + } + } + + @Test + public void bigramTest() { + Set s = new HashSet<>(); + for (int i = 0; i < 10; i++) { + for (char ch = 'a'; ch <= 'z'; ch++) { + s.add(new Bigram(ch, ch)); + } + } + + Assertions.assertEquals(26, s.size()); // 실제 값 260 + } +} +``` + +- bigramTest() 가 원한 결과는 s.size()가 26인 것이지만 실제로는 260이 나왔다. +- equals()에 @Override 애너테이션을 붙이지 않아서 생긴 실수가 있다. + - Object에서 상속받는 equals()는 원래 Object 타입의 파라미터를 받는데, Bigram의 파라미터를 받고 있다. + - 그래서 Set에서 비교에 사용되는 equals()가 제대로 정의되지 않고, 오직 객체 주소의 동치만 비교하는 기본 Object의 equals()가 쓰이고 있던 것이다. + - 따라서 같은 소문자를 소유한 바이그램 10개 각각이 서로 다른 객체로 인식되고, 결국 260을 출력한 것이다. + - 실제 오버라이딩(재정의)된 것이 아니라 오버로딩을 하고 있는 것이다. + +### 실수 고치기 + +```java +@Override +public boolean equals(Object o){ + if(!(o instanceof Bigram)){ + return false; + } + + Bigram b=(Bigram)o; + return b.first==first&&b.second==second; + } +``` + +- 위와 같이 @Override 애너테이션을 달면, 실제로 상속받은 메서드가 아니면 에러를 내주기 때문에 실수할 확률이 적어진다. +- 잘못한 부분을 명확히 알려주므로 곧장 올바르게 수정할 수 있다. + +### 핵심 정리 + +- 재정의한 모든 메서드에 @Override 애너테이션을 달아 실수를 방지하자. +- 단, 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 이 애너테이션을 달지 않아도 된다. \ No newline at end of file diff --git "a/\352\271\200\354\210\230\353\271\210/7\354\236\245/item43.md" "b/\352\271\200\354\210\230\353\271\210/7\354\236\245/item43.md" new file mode 100644 index 0000000..4b3c56c --- /dev/null +++ "b/\352\271\200\354\210\230\353\271\210/7\354\236\245/item43.md" @@ -0,0 +1,139 @@ +## 익명 클래스보다는 람다를 사용하라 + +### 익명 클래스 : 클래스의 선언과 객체 생성과 동시에 단 한번 사용할 수 있게 만든 클래스 + +```java +Collections.sort(words,new Comparator(){ +@Override +public int compare(String o1,String o2){ + return Integer.compare(o1.length(),o2.length()); + } + }); +``` + +- 자바8부터는 추상 메소드가 하나만 존재는 인터페이스는 람다식을 사용해 만들 수 있게 되었다. + +Comparator 타입은 추상메서드 하나만 구현하면 되기 때문에 람다로 대체 가능. + +```java +Collections.sort(words,(o1,o2)->Integer.compare(o1.length(),o2.length())); + +``` + +- 비교자 메서드와 sort 메서드를 이용하면 더욱 짧아짐 + +```java +words.sort(Comparator.comparingInt(String::length)); +``` + +타입을 명시해야 코드가 더 명확할 때만 제외하고는 람다의 모든 매개변수 타입을 생략하자 + +컴파일러가 타입을 알 수 없다는 오류를 내면 그 때 타입을 적어도 된다. + +### 열거 타입에서 활용 + +```java +enum Operation { + PLUS("+") { + @Override + public double apply(double x, double y) { + return x + y; + } + }, + MINUS("-") { + @Override + public double apply(double x, double y) { + return x - y; + } + }, + TIMES("*") { + @Override + public double apply(double x, double y) { + return x * y; + } + }, + DIVIDE("/") { + @Override + public double apply(double x, double y) { + return x / y; + } + }; + + private final String symbol; + + Operation(String symbol) { + this.symbol = symbol; + } + + @Override + public String toString() { + return symbol; + } + + public abstract double apply(double x, double y); +} +``` + +단순히 각 열거 타입 상수의 동작을 람다로 구현해 생성자에 넘기고, 생성자는 이 람다를 인스턴스 필드로 저장하여 사용 + +```java +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 BinaryOperator op; + + Operation(String symbol, BinaryOperator op) { + this.symbol = symbol; + this.op = op; + } + + @Override + public String toString() { + return symbol; + } + + public double apply(double x, double y) { + return op.apply(x, y); + } +} +``` + +### 주의할 점 + +- 람다는 이름도 없고 문서화도 못하기 때문에 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 않는 것이 좋다. + +- 람다의 this 키워드는 바깥을 가리키므로 주의해야 한다. + + 반면 익명 클래스의 this 키워드는 인스턴스 자신을 가리키므로 함수 객체가 자신을 참조해야 한다면 익명 클래스를 사용하자. + +```java +class SelfReferenceExample { + public void print() { + System.out.println("Hello from " + this); + } + + public Supplier getSelf() { + // return () -> this; // 람다 사용 불가 (컴파일 오류 발생) + return new Supplier<>() { + @Override + public SelfReferenceExample get() { + return this; // 익명 클래스 사용 가능 + } + }; + } + + public static void main(String[] args) { + SelfReferenceExample example = new SelfReferenceExample(); + Supplier supplier = example.getSelf(); + System.out.println(supplier.get()); // 익명 클래스의 toString() 출력됨 + } +} +``` + +- 람다도 익명 클래스처럼 직렬화 형태가 구현별로(가령 가상머신별로) 다를 수 있다. 따라서 람다를 직렬화하는 일은 극히 삼가야 한다 + +