40 @Override 애너테이션을 일관되게 사용하라

@Override는 메서드 선언에만 달 수 있으며, 이 애너테이션이 달렸다는 것은 상위 타입의 메서드를 재정의했음을 뜻합니다.
애너테이션을 일관되게 사용하면 여러 가지 악명 높은 버그들을 예방해줍니다

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 bigram) {
        return bigram.first == this.first &&
                bigram.second == this.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());
    }
}
  • Main () 메서드를 살펴보면 바이그램 26개를 10번 반복해 집합에 추가한 다음 그 집합의 크기를 출력합니다
  • Set은 중복을 허용하지 않으니 26이 출력될거 같지만 실제로는 260이 출력됩니다
    • -> 여기서 equals를 재정의한게 아니라 다중정의를 하였습니다
  • HashSet은 내부적으로 equals 메서드를 기반으로 객체의 논리적 동치적(equals) 검사를 실시합니다
  • 하지만 equals메서드의 파라미터 타입이 Bigram입니다
    • 즉, equals 메서드를 재정의 한게 아니라 Overloading 하였습니다
    • equals를 재정의 하려면 파라미터 타입이 Object이어야 합니다
@Override
public boolean equals(Object bigram) {
    if(!(o instanceof Bigram)) {
        return false;
    }
    Bigram b = (Bigram) bigram;
    return b.first == this.first &&
        b.second == this.second;
}

정리

  • 상위 클래스의 메서드를 재정의 하는 모든 메서드에 @Override 애너테이션을 다는게 좋습니다
  • 인터페이스를 상속한 구체 클래스인데 아직 구현하지 않은 추상 메서드가 남아있다면 컴파일러가 알려줍니다
  • Java 8 부터 Default 메서드의 사용이 가능해 지면서, 인터페이스의 메서드를 재정의 할 때도 사용할 수 있습니다
  • 추상클래스나 인터페이스에서는 상위 클래스나 상위 인터페이스를 재정의하는 모든 메서드에 @Override를 다는것이 좋습니다
블로그 이미지

yhmane

댓글을 달아 주세요

38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

타입 안전 열거 패턴은 확장이 가능하나, 열거 타입은 확장을 할 수 없습니다.
하지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있습니다

타입 안전 열거 패턴(typesafe enum pattern)

enum이 나오기전 jdk 1.5 밑에 버전에서는 아래와 같이 사용하였습니다

public final class Shape {

    private String polygon;

    private Shape() {
    }

    private Shape(String polygon) {
        this.polygon = polygon;
    }

    public static final Shape TRIANGLE = new Shape("triangle");
    public static final Shape RECTANGLE = new Shape("rectangle");
    public static final Shape PENTAGON = new Shape("pentagon");
}

열거타입 (enum)

Enum class cannot inherit from classes

public enum Shape {
    TRIANGLE, RECTANGLE, PENTAGON
}
  • 열거 타입은 타입 안전 열거 패턴보다 우수합니다
  • 단, 타입 안전 열거 패턴은 확장이 가능하나 열거 타입은 확장할 수 없습니다
  • 열거 타입을 확장하려면 열거 타입이 임의의 인터페이스를 구현할 수 있다는 사실을 이용하면 됩니다

인터페이스를 이용한 확장 가능 열거 타입

public interface Operation {
    double apply(double x, double y);
}
public enum BasicOperation implements 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;

    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
}

열거 타입인 BasicOperation은 확장할 수 없습니다.
다만, 인터페이스인 Operation을 이용해 확장할 수 있고
이 인터페이스를 연산의 타입으로 이용할 수 있습니다

다른 열거 타입을 추가

public enum ExtendedOperation implements Operation {

    EXP("^") {
        @Override
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        @Override
        public double apply(double x, double y) {
            return x % y;
        }
    };

    private final String symbol;

    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
}

Operation 인터페이스를 구현하면 다른 열거 타입에서도 확장할 수 있습니다


enum 타입 확인

  1. Enum타입을 넘겨 순회하기 (class literal 넘기기)
    public static void main(String[] args) {
     double x = 10;
     double y = 2;
     test(ExtendedOperation.class, x, y);
    }
    

public static <T extends Enum & Operation> void test(Class opEnumType, double x, double y) {

for (Operation op : opEnumType.getEnumConstants()) {
    System.out.printf(“%f %s %f = %f%n”, x, op, y, op.apply(x, y));
}

}


ExtendedOperation의 class 리터럴

<T extends Enum<T> & Operation> 
 타입이 Enum타입이면서, Operation을 구현하는 클래스

10 ^ 2 = 100
10 % 2 =0

2. 열거 타입의 리스트 넘기기
```java
public static void main(String[] args) {
    double x = 10;
    double y = 2;
    test(Arrays.asList(ExtendedOperation.values()), x, y);
}

public static void test(Collection<? extends Operation> opSet, double x, double y) {
    for (Operation op : opSet) {
        System.out.printf(“%f %s %f = %f%n”, x, op, y, op.apply(x, y));
    }
}

열거 타입의 리스트를 넘겨 <? extends Operation>인 한정적 와일드 카드 타입으로 지정

10 ^ 2 = 100
10 % 2 =0

정리

  • 열거 타입끼리 구현을 상속 할수는 없습니다
  • 확장할 수 있는 열거 타입이 필요한 경우 인터페이스 정의를 구현합니다
블로그 이미지

yhmane

댓글을 달아 주세요

28. 배열보다는 리스트를 사용하라

배열과 제네릭 타입에는 중요한 차이가 두가지 있습니다

1. 배열은 공변, 제네릭은 불공변입니다

// 배열, ArrayStoreException, RunTime 오류
Object[] objectArray = new Long[1];
objectArray[0] = “타입이 달라 넣을 수 없다”; 

// 리스트, 컴파일 되지 않는다
// List<Object> objectList = new ArrayList<Long>(); 

배열은 sub가 super의 하위타입이라면 sub[]는 배열 super[]의 하위타윕이 됩니다. 따라서, 위에 Long에 String을 입력하여도 컴파일 오류가 나지 않았습니다.

반면, 리스트 List은 List의 하위타입도 상위 타입도 아닙니다. 따라서, 위의 코드에서 컴파일 오류가 났습니다

2. 배열은 실체화가 되고, 리스트는 그렇지 않습니다

배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인합니다.
반면 제네릭은 타입정보가 런타임에는 소거됩니다. 이 두 차이로 인해 배열과 제네릭은 어울러질 수 없습니다

List<String>[] stringLists = new List<String>[1];

정리

타입 세이프하고, 런타임이 아닌 컴파일 단계에서 에러를 잡아줍니다. 다만, 성능으로는 배열이 유리하지만 웬만하면 리스트를 사용하도록 합니다

참조

effective java 3/e

블로그 이미지

yhmane

댓글을 달아 주세요

27. 비검사 경고를 제거하라

제네릭을 사용하기 시작하면 수많은 컴파일러 경고들을 마주치게 됩니다
이러한 경고들을 가능한 많이 제거하는 것이 습니다
경고들을 모두 제거한다면, 그 코드는 타입 안정성이 보장되기 때문입니다

대부분의 비검사 경고는 쉽게 제거할 수 있습니다

Set<Car> cars = new HashSet();
Venery.java:4: warning: [unchecked] unchecked conversion
                Set<Car> cars = new HashSet();
                                ^
  required: Set<Car>
  found:    HashSet

컴파일러가 알려준 대로 수정하면 경고가 사라집니다

자바7 이후 제공하는 다이아몬드 연산자<>로 해결이 가능합니다

Set<Car> cars = new HashSet<>();

다만, 제거하기 훨씬 어려운 경고들도 있습니다.

곧바로 해결되지 않는 경고가 나타나더라도 할 수 있는한 모든 비검사 경고를 제거하도록 합니다

경고를 제거할 수 없지만 타입이 안전하다고 확신할 수 있으면,  @SuppressWarnings(“unchecked”)를 달아 경고를 숨기도록 합니다

@SuppressWarnings

  • 지역변수 ~ 클래스 전체까지 모두 선언 할 수 있습니다
    • 가능한 가장 좁은 범위에 적용하도록 합니다
    • 또한, 클래스 전체에 적용하지 않도록 합니다
  • 한줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings은 지역변수 선언 쪽으로 옮기도록 합니다
    • 이를 위해 지역변수를 새로 선언하는 수고를 해야할 수 있지만, 그만한 값어치가 있습니다
  • 또한 @ SuppressWarnings 사용시 안전한 이유를 항상 주석으로 남겨두도록 합니다 
  • public <T> T[] toArray(T[] a) { if (a.length < size) { // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[]로 같으므로 올바른 형변환입니다 @SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass()); return result; } System.arraycopy(elements, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }

참조

effective java 3/e

블로그 이미지

yhmane

댓글을 달아 주세요

25. 톱레벨 클래스는 한 파일에 하나만 담아라

우리가 사용하는 파일 내에 하나만 존재하는 클래스를 톱레벨 클래스라고 합니다.
(중첩클래스는 top 레벨 클래스가 아닙니다)

하지만, 소스파일 하나에 여러개의 톱레벨 클래스가 있다면 심각한 위험을 감수해야 합니다

문제

Utensil.java

class UtenSil {
    static final String NAME = "pan";
}

class Dessert {
    static final String NAME = "cake";
}

Dessert.java

class UtenSil {
    static final String NAME = "pot";
}

class Dessert {
    static final String NAME = "pie";
}

여기서 아래의 main 함수를 실행해보자

class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }
}

일반적으론 중복 클래스가 존재한다고 컴파일 에러가 나지만,
javac Main.java Utensil.java의 명령으로 컴파일 한다면 ‘pancake’가
Javac Main.java Dessert.java의 명령으로 컴파일 한다면 ‘potpie’가 출력될 것입니다

해결방안

정적멤버 클래스 또는 톱레벨 클래스를 서로 다른 파일로 분리

정적멤버클래스

class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }

    static class Utensil {
        static final String NAME = "pan";
    }

    static class Dessert {
        static final String NAME = "cake";
    }
}

톱레벨 클래스로 분리

class Utensil {
    static final String NAME = "pan";
}
class Dessert {
    static final String NAME = "cake";
}

참조

effective java 3/e

블로그 이미지

yhmane

댓글을 달아 주세요

24. 멤버 클래스는 되도록 static으로 만들라

중첩클래스란

중첩 클래스(nested class) 다른 클래스 안에 정의된 클래스를 말합니다.
중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱 레벨 클래스로 만들어야 합니다

  • 정적 멤버 클래스
  • (비정적) 멤버 클래스
  • 익명 클래스
  • 지역클래스

이 중에서 정적 멤버 클래스를 제외한 나머지를 내부 클래스 (inner class)라고 합니다

정적 멤버 클래스

정적 멤버 클래스는 바깥 클래스의 private 멤버에도 접근할 수 있다는 점을 제외하고
일반 클래스와 쓰임새는 동일합니다

public class OuterWithStaticClass {

    private String name;

    static class StaticClass {
        void hello() {
            OuterWithStaticClass outerWithStaticClass = new OuterWithStaticClass();
            outerWithStaticClass.name = "홍길동";
            System.out.println(outerWithStaticClass.name);
        }
    }
}
public class Item24App {

    public static void main(String[] args) {

        OuterWithStaticClass.StaticClass staticClass = new StaticClass();
        staticClass.hello();
    }
}

비정적 멤버 클래스

구문상으로는 정적 클래스와 static이 붙어있고 없고의 차이입니다.
하지만 의미상으로 비정적 멤버 클래스는 바깥 클래스의 인스턴스와 암묵적으로 연결됩니다. 따라서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 참조를 가져올 수 있습니다

public class OuterWithNoneStaticClass {

    private final String name;

    public OuterWithNoneStaticClass(String name) {
        this.name = name;
    }

    public String getName() {
        NonStaticClass nonStaticClass = new NonStaticClass("noneStatic-class : ");
        return nonStaticClass.getNameWithOuter();
    }

    private class NonStaticClass {
        private final String noneStaticName;

        public NonStaticClass(String noneStaticName) {
            this.noneStaticName = noneStaticName;
        }

        public String getNameWithOuter() {
            return noneStaticName + OuterWithNoneStaticClass.this.name;
        }
    }
}
public class Item24App {

    public static void main(String[] args) {
        OuterWithNoneStaticClass noneStaticClass = new OuterWithNoneStaticClass("고길동");
        System.out.println(noneStaticClass.getName());
    }
}

드물게 직접 바깥 인스턴스의 클래스.new Member Class(args)를 호출해 수동으로
만들기도 합니다. 하지만 이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성시간도 더 걸립니다.

따라서, 중첩 클래스의 인스턴스가 바깥 클래스의 인스턴스와 독립적으로 존재해야 한다면 정적 멤버 클래스로 만드는게 좋습니다.

익명 클래스

  • 이름이 없고 바깥 클래스의 멤버도 아닙니다
  • 쓰이는 시점에 선언과 동시에 인스턴스가 만들어집니다
  • 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스 참조가 가능하다
  • 상수 정적변수 (static final) 외에는 정적 변수를 가질 수 없다.
public class Anonymouss {

    private String name;

    public void hello() {
        HelloBot helloBot = new HelloBot() {
            @Override
            public void hello() {
                System.out.println("안녕하세요. 헬로우 봇입니다");
            }
        };
        helloBot.hello();
    }
}

interface HelloBot {
    void hello();
}
public class Item24App {

    public static void main(String[] args) {
        Anonymouss anonymouss = new Anonymouss();
        anonymouss.hello();
    }
}

익명 클래스의 제약사항

  • 선언한 지점에서만 인스턴스를 만들수 있습니다
  • 여러 인터페이스를 구현할 수 없고, 구현과 동시에 다른클래스 상속도 불가능합니다
  • 익명 클래스 사용 클라이언트는 사용하는 익명 클래스가 상위타입에서 상속한 멤버외에는 호출이 불가능합니다
  • 람다(자바7) 등장 이전에는 작은 함수 객체나 처리 객체 구현에 사용되었지만 java8 부터는 람다를 제공합니다

지역 클래스

네 가지 중첩 클래스 중 가장 드물게 사용됩니다.
지역 클래스는 지역 변수를 선언할 수 있는 곳이면 어디서든 선언할 수 있고, 유효 범위도 지역 변수와 같습니다.

public class LocalClass {

    public void hello() {
        class LocalExample {
            private String name;

            public LocalExample(String name) {
                this.name = name;
            }

            public String getName() {
                return name;
            }
        }

        LocalExample localExample = new LocalExample("윤호호");
        System.out.println(localExample.getName());
    }
}
public class Item24App {

    public static void main(String[] args) {
        LocalClass localClass = new LocalClass();
        localClass.hello();
    }
}

지역 클래스는 다른 중첩 클래스들의 공통점을 하나씩 가지고 있다.

  • 멤버 클래스처럼 이름을 가질수 있고 반복해서 사용할 수 있습니다
  • 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있습니다
  • 정적 멤버는 가질 수 없으며, 가독성을 위해 짧게 작성되어야 합니다
블로그 이미지

yhmane

댓글을 달아 주세요

23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라

태그 달린 클래스

태그 클래스란, 두가지 이상의 의미를 표현할 때 그 중 현재 표현하는 의미를 태그값으로 알려주는 클래스 입니다.

책에 나온 예제로 살펴보도록 하겠습니다.

class Figure {

    enum Shape {
        RECTANGLE, CIRCLE
    }

    // 태그 필드
    final Shape shape;

    // RECTANGLE 용 필드
    double length;
    double width;

    // CIRCLE 용 필드
    double radius;

    // RECTANGLE
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    // CIRCLE
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}

태그 클래스의 단점

  1. Enum switch, field 등 여러 구현이 혼합 되어 있습니다
  2. 이로 인해 가독성이 떨어집니다
  3. 사용하지 않는 필드로 인해 메모리도 추가적으로 사용합니다
  4. 또 다른 타입을 추가하게 되면 switch 문의 수정이 일어납니다
  5. 인스턴스 타입만으로 객체의 의미를 알 수 없습니다

태그가 달린 클래스는 코드가 길어지고 확장성에 취약합니다.
또한, 태그가 추가 될 때마다 사용하지 않는 필드들이 추가 될 수도 있습니다.


계층 클래스

계층 구조의 클래스를 만드는 방법

  1. 계층구조의 최상위(root)가 될 추상클래스를 정의합니다
  2. 태그값에 따라 달라지는 동작(메서드)들을 최상위 클래스의 추상 메서드로 선언합니다
  3. 태그값에 상관없이 동작이 일정한 메서드는 최상위 클래스에 일반 메서드로 정의합니다.
  4. 모든 하위 클래스에 공통으로 사용하는 상태값(필드)들은 루크 클래스에 정의합니다.

계층구조로 변환

abstract class Figure {
    abstract double area();
}

class Rectangle extends Figure {

    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }

    @Override
    double area() {
        return length * width;
    }
}


class Circle extends Figure {

    final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

클래스 계층구조의 장점

  1. 독립된 의미를 가지는 상태값(필드)들이 제거 되어 각 구현 클래스는 간결해집니다
  2. 살아남은 field는 모두 final이므로 정의할 수 있습니다
  3. 실수로 빼먹은 case 문으로 인해 runTime 에러를 방지할 수 있습니다
  4. 최상위 클래스를 수정하지 않고 타입을 확장 할 수 있습니다
  5. 타입사이의 자연스러운 계층 관계를 반영할 수 있습니다
블로그 이미지

yhmane

댓글을 달아 주세요

22. 인터페이스는 타입을 재정의하는 용도로만 사용하라

인터페이스는 타입을 정의하는 용도로만 사용해야 합니다.
상수 공개용 수단으로 사용해서는 안됩니다

안티패턴 - 상수 인터페이스

메서드 없이, 상수를 뜻하는 static final 필드로만 가득찬 인터페이스

public interface PhysicalConstants {

    // 아보가드로 수
    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    // 볼츠만 상수
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    // 전자 질량
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}
  • 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아닌 내부 구현입니다
    • 내부 구현을 클래스의 API로 노출하는 행위가 됩니다
    • 또한, 사용자에게 혼란과 오해의 소지를 줄 뿐입니다

대안

클래스의 상수로 선언하여 사용합니다

public class PhysicalConstants {

    // 인스턴스화 방지
    private PhysicalConstants(){}

    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}

또는 enum의 값으로 사용합니다

public enum PhysicalConstants {

    AVOGADROS_NUMBER(6.002_140_857e23),
    BOLTZMANN_CONSTANT(1.380_648_52e-23),
    ELECTRON_MASS(9.109_383_56e-31);

    private double value;

    PhysicalConstants(double value) {
        this.value = value;
    }

    public double getValue(){
        return value;
    }
}

참조

effective java 3/e - 조슈아 블로크

블로그 이미지

yhmane

댓글을 달아 주세요

Item20

추상 클래스보다는 인터페이스를 우선하라

자바가 제공하는 다중 구현 메커니즘으로 인터페이스와 추상클래스가 존재한다.

자바8부터 인터페이스에 default method를 지원하여 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있다.

둘의 가장 큰 차이는 추상클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다.반면, 인터페이스는 어떤 클래스를 상속했든 같은 타입으로 취급 받는다.

  • 인터페이스 - 다중 상속이 가능하고 구현한 클래스와 같은 타입으로 취급음. Java8 부터 default 메서드 제공
  • 추상클래스 - 다중 상속이 불가하고, 구현체와 상하관계에 있

인터페이스 장점

기존 클래스에도 손쉽게 새로운 인터페이스를 구현할 수 있다

  • 인터페이스 - 인터페이스의 추상 메서드를 추가하고, 클래스에 implements 구문을 추가하여 구현체임을 알린다.
  • 추상클래스 - 계층 구조상 두 클래스의 공통 조상이어야 하며, 새로 추가된 추상 클래스의 모든 자손이 상속하게 된다.

믹스인 정의에 적합하다.

  • 추상 클래스는 단일 상속만 가능하기 때문에 기존 클래스에 덧씌울 수 없다.

인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.

먼저 인터페이스로 살펴보자.

public interface Singer {
    AudioClip sing(Song s);
}

public interface SongWriter {
    Song compose(int charPosition);
}

public interface SingSongWriter extends Singer, SongWriter {
    AudioClip strum();
    void actSensitive();
}

다음으로 추상클래스로 살펴보자.

public abstract class Singer {
    abstract AudioClip sing(Song s);
}

public abstract class SongWriter {
    abstract Song compose(int charPosition);
}

public abstract class SingerSongWriter {
    abstract AudioClip sing(Song s);
    abstract Song compose(int charPosition);
    abstract AudioClip strum();
    abstract void actSensitive();
}

추상 클래스로 만들면 다중상속이 불가하여 새로운 추상클래스를 만들어서 클래스 계층을 표현할 수 밖에 없다.

따라서 이 계층구조를 만들기 위해서는 많은 조합이 필요하게 된다.


디폴트 메서드 제약

자바8부터 인터페이스에서도 메서드를 구현할 수 있게 되었다. default 메서드. 다만, 아래와 같은 규칙을 지켜줘야 한다.

  • @implSpec 자바독 태그를 붙여 사용하려는 default 메서드를 문서화한다.
  • equals와 hashCode는 default 메서드로 제공해서는 안된다.
  • 인스턴스 필드를 가질 수 없다.
  • public이 아닌 정적 멤버를 가질 수 없다.
  • 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없다.

추상 골격 클래스(Skeletal Implementation)

인터페이스로는 타입을 정의하고 디폴드 메서드도 제공한다. 골격 구현 클래스는 나머지 메서드들까지 구현한다.

이렇게 해두면 단순히 골격 구현을 확장하는 것만으로 인터페이스를 구현하는데 필요한 일이 대부분 완료된다.(템플릿 메서드 패턴
)

시뮬레이트한 다중 상속(Simulated Multiple Inheritance)

골격 구현 클래스를 우회적으로 이용하는 방식이다.
인터페이스를 구현한 클래스에서 골격구현을 확장한 private 내부 클래스를 정의하고 각 메서드 호출을 내부 클래스의 인스턴스에 전달하는 것이다.

아이템 18에서 다룬 내용과 비슷한 방식이다.

public class ForwardingSet<E> implements Set<E> {
  private final Set<E> s;

  public ForwardingSet(Set<E> s) {
    this.s = s;
  }

  public void clear() {
    s.clear();
  }
  ... 중략 ...
}

골격 구현 작성방법

  • 먼저 인터페이스를 잘 살펴 다른 메서드들의 구현에 사용되는 기반 메서드를 선정한다
  • 이 기반 메서드들을 사용해 직접 구현할 수 있는 메서드들을 모두 디폴트 메서드로 제공한다.
  • 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 남아있다면, 이 인터페이스를 구현하는 골격 구현 클래스를 하나 만들어 남은 메서드들은 작성해 넣는다.

다음과 같이 구현한다.

public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> {
    @Override public V setValue(V value) {
        throw new UnsupportedOperationException();
    }

    @Override public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Map.Entry)) {
            return false;
        }

        Map.Entry<?,?> e = (Map.Entry) o;
        return Objects.equals(e.getKey(),   getKey()) && Objects.equals(e.getValue(), getValue());
    }

    @Override public int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    @Override public String toString() {
        return getKey() + "=" + getValue();
    }
}

결론

  • 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다.
  • 복잡한 인터페이스라면 구현하는 수고드를 덜어주는 골격 구현을 고려해보자.

참조

  • effective java 3/e chapter20
블로그 이미지

yhmane

댓글을 달아 주세요

Item18

상속보다는 컴포지션을 사용하라

상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다.

잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다.


메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.

  • 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
    • 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한줄 건드리지 않은 하위 클래스가 오작동할 수 있다.
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    public InstrumentedHashSet(){}

    public InstrumentedHashSet(int initCap, float loadFactor){
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

public class Item18 {
    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("틱", "탁탁", "펑"));

        System.out.println(s.getAddCount());
    }
}
  • getAddCount()의 결과가 3을 반환하리라 생각하겠지만 6을 반환한다. HashSet의 addAll 메서드가 add 메서드를 사용해 구현된 데 있다.
  • 하위 클래스에서 addAll 메서드를 재정의하지 않으면 문제를 고칠 수 있다.
    • 하지만 이처럼 자신의 다른 부분을 사용하는 '자기사용' 여부는 해당 클래스의 내부 구현방식에 해당하며, 자바 플랫폼 전반적인 정책인지, 그 다음 릴리즈에도 유지될지 알 수 없다.
  • 그렇다면 재정의 대신 새로운 메서드를 추가하면 괜찮을까?
    • 괜찮은 방법이라 생각할수도 있지만, 위험이 전혀 없는 것은 아니다.
    • 다음 릴리스에 상위 클래스에 새 메서드가 추가 됐는데, 운 없게도 하필 추가된 메서드와 시그니처가 같고 반환 타입이 다를 수도 있다. 컴파일 문제가 바로 발생한다.

상속대신 컴포지시션을 이용

기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 한다.
컴포지션을 통해 새 클래스의 인스턴스 메서드들은 기존 클래스에 대응하는 메서드를 호출해 그 결과를 반환하게 한다.
새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향을 받지 않는다.

public class ForwardingSet<E> implements Set<E> {
  private final Set<E> s;

  public ForwardingSet(Set<E> s) {
    this.s = s;
  }

  public void clear() {
    s.clear();
  }

  public boolean contains(Object o) {
    return s.contains(o);
  }

  public boolean isEmpty() {
    return s.isEmpty();
  }

  public int size() {
    return s.size();
  }

  public Iterator<E> iterator() {
    return s.iterator();
  }

  public boolean add(E e) {
    return s.add(e);
  }

  public boolean remove(Object o) {
    return s.remove(o);
  }

  public boolean containsAll(Collection<?> c) {
    return s.containsAll(c);
  }

  public boolean addAll(Collection<? extends E> c) {
    return s.addAll(c);
  }

  public boolean removeAll(Collection<?> c) {
    return s.removeAll(c);
  }

  public boolean retainAll(Collection<?> c) {
    return s.retainAll(c);
  }

  public Object[] toArray() {
    return s.toArray();
  }

  public <T> T[] toArray(T[] a) {
    return s.toArray(a);
  }

  @Override
  public boolean equals(Object o) {
    return s.equals(o);
  }

  @Override
  public int hashCode() {
    return s.hashCode();
  }

  @Override
  public String toString() {
    return s.toString();
  }
}

public class InstrumentedSet<E> extends ForwardingSet<E> {
  private int addCount = 0;

  public InstrumentedSet(Set<E> s) {
    super(s);
  }

  @Override
  public boolean add(E e) {
    addCount++;
    return super.add(e);
  }

  @Override
  public boolean addAll(Collection<? extends E> c) {
    addCount += c.size();
    return super.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }
}

public class Item18 {
  public static void main(String[] args) {
    InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
    s.addAll(List.of("틱", "탁탁", "펑"));
    System.out.println(s.getAddCount());

    InstrumentedSet<String> s2 = new InstrumentedSet<>(new HashSet<String>());
    s2.addAll(List.of("틱", "탁탁", "펑"));
    System.out.println(s2.getAddCount());
  }
}

결론

  • 상속은 강렬하지만 캡술화를 해친다는 문제가 있다.
  • 상속은 상위 클래스와 하위 클래스가 is-a 관계일 때만 써야 한다.
  • 상위 클래스와 하위 클래스의 패키지가 다를 경우에는 is-a 관계라도 문제가 발생할 수 있다.
  • 상속의 취약점을 피하려면 상속 대신 컴포지션 전달을 사용하자.

참조

블로그 이미지

yhmane

댓글을 달아 주세요