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

댓글을 달아 주세요

Item15

클래스와 멤버의 접근 권한을 최소화하라

잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깔끔히 분리합니다. 정보 은닉, 혹은 캡슐화라고 하는 이 개념은 소프트웨어의 근간이 되는 원리입니다.

정보 은닉의 장점

  • 시스템 개발 속도를 높여줍니다. 여러 컴포넌트를 병렬로 개발할 수 있습니다.
  • 시스템 관리 비용을 낮춰줍니다.
  • 소프트웨어의 재사용성을 높여줍니다. 외부에 거의 의존하지 않고 독자적으로 동작하는 컴포넌트라면 낯선 환경에서도 유용하게 쓰일 가능성이 높습니다.
  • 큰 시스템을 제작하는 난이도를 낮춰줍니다.

Java의 접근제한자

  • private
    • 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있습니다.
  • package-private
    • 멤버가 소속된 패키지 안의 모든 클래스에서 접근 할 수 있습니다.
  • protected
    • package-private의 접근 범위를 포함하며 선언한 클래스의 하위 클래스에서도 접근할 수 있습니다.
  • public
    • 어디에서나 접근 가능합니다.

클래스 레벨

  • 톱레벨 수준이 같은 수준에서의 접근제한자는 public과 package-private만 사용 할 수 있습니다.
    • public으로 선언하는 경우 - 공개 API로 사용하고 하위호환을 평생 신경써야 합니다.
    • package-private로 사용하는 경우 - 해당 패키지 안에서만 사용 가능하여 다음 릴리즈에서도 변경이 가능합니다.

public 클래스의 인스턴스 필드는 되도록 public이 아니어야 합니다.

필드가 가변객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 불변식을 보장할 수 없습니다. public 가변 필드를 갖는 클래스는 일반적으로 thread safe 하지 않습니다.

  • 상수라면 관례대로 public static final 필드로 공개해도 좋습니다.
  • 하지만 클래스에서 public static final 배열 필드를 두면 안됩니다.
    • 배열을 private로 만들고 public 불변 리스트를 추가하거나 public 메서드를 추가하여 줍니다.

결론

  • 프로그램 요소의 접근성은 가능한 최소한으로 설계합니다.
  • public API는 필요한 것만 골라 최소한으로 설계합니다.
  • 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안됩니다. static final field 필드가 참조하는 객체가 불변인지도 확인해야 합니다.

참조

[effective java 3/e]
yhmane github

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Item14

Comparable을 구현할지 고려하라

  • 유일무이한 메서드인 compareTo
  • compareTo()는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며 제네릭 합니다.
    public interface Comparable<T> {
      int compareTo(T t);
    }


compareTo() 일반규약

  • 이 객체와 주어진 객체의 순서를 비교합니다.
  • 이 객체가 주어진 객체보다 작으면 음수를, 같으면 0을, 크면 양수를 반환합니다.
  • 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던집니다.
  • x.compareTo(y) = y.compareTo(x) * -1 입니다. 또한 예외가 발생한다면 양쪽 표현식 모두 발생합니다.
  • 추이성을 보장합니다.
    • x.compareTo(y) > 0이고 y.compareTo(z)이면 x.compareTo(z) > 0도 성립합니다.
  • (x.compareTo(y) == 0) == (x.equals(y)) 이어야 합니다.
    • 이 권고를 지키지 않는 모든 클래스는 아래의 사실을 명시해야 합니다
    • "이 클래스의 순서는 equals 메서드와 일관되지 않다"
    • Collection, Set, Map 인터페이스들은 동치성을 비교할 때 equals 대신 compareTo를 사용하기 때문에 마지막 규약을 꼭 지키는 것이 좋습니다.

사용예제

  • 기본 타입 필드가 여럿일 때의 비교자

    public int compareTo(PhoneNumber pn) {
      int result = Short.compare(this.areaCode, pn.areaCode);
      if(result == 0) {
          result = Short.compare(this.prefix, pn.prefix);
          if(result == 0) {
              result = Short.compare(line Num, pn.lineNum);
          }
      }
      return result;
    }
  • 정적 compare 메서드를 활용한 비교자

    static Comparator<Object> hashCodeOrder = new Comparator<>() {
      public int compare(Object o1, Object o2) {
          return Integer.compare(o1.hashCode(), o2.hashCode());
      } 
    }
  • 비교자 생성 메서드를 활용한 비교자

    static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());


결론

  • 순서를 고려해야 하는 값 클래스를 작성할 경우 꼭 Comparable 인터페이스를 구현합니다.
  • 그 인스턴스들은 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러져야 합니다.
  • compareTo()는 필드의 값을 비교할 때 '<'와 '>' 연산자는 쓰지 말도록 합니다.
  • 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용합니다.

참조

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Item11

equals를 재정의하려거든 hashCode도 재정의하라

  • equals 재정의한 클래스스 모두에서 hashCode도 재정의해야 합니다.
  • 그렇지 않으면 hashCode 일반 규약을 어기게 됩니다.
  • HashMap, HashSet 같은 컬렉션의 원소로로 사용할때 문제를 일으키게 됩니다.

hashCode 기본 규약

  • equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 hashCode 메서드는 몇번을 호출해도 항상 일관된 값을 반환해야 합니다.
  • 논리적으로 각은 객체는 같은 해시코드를 반환해야 합니다.
  • equals가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없습니다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아집니다.

hashCode를 재정의하지 않은 경우

Map<PhoneNumber, String> map = new HashMap<>();
map.put(new PhoneNumber(707, 867, 5309), "제니");
System.out.println(m.get(new PhoneNumber(707, 867, 5309));
  • '제니'의 결과를 기대하지만 get의 결과는 null입니다.
  • 최악의 hashCode 재정의
@Override
public int hashCode() {
    return 42;    
}
  • 모든 객체의 hashCode값이 42로 반환됩니다. 성능적으로도 떨어지고 논리적으로도 부적합하기에 규약을 따라야합니다.

hashCode 재정의

@Override
public int hashCode() {
    int result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}
  • 클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 캐싱 방식을 고려해보는 것도 좋습니다.
private int hashCode; //0으로 초기화
@Override
public int hashCode() {
    int result = hashCode;
    if (result == 0) {
      result = Short.hashCode(areaCode);
      result = 31 * result + Short.hashCode(prefix);
      result = 31 * result + Short.hashCode(lineNum);
      hashCode = result;
    }
    return result;
}

hashcode를 재정의 할 때 주의

  • 불변 객체에 대해서는 hashcode 생성비용이 많이 든다면 캐싱을 고려해 봅니다.
  • 성능을 높인다고 핵심 field를 계산에서 제외하면 안됩니다.
  • hashcode 생성규칙을 API 사용자에게 open하지 않는게 좋습니다.
  • 그래야 클라이언트가 hashcode값에 의지한 코드를 구현하지 않습니다.

결론

  • equals를 재정의할 때는 반드시 hashCode도 재정의해야 합니다.
  • 서로 다른 인스턴스라면 해시코드도 서로 다르게 구현해야 합니다.
  • AutoValue를 이용한다면 간단히 해시코드를 재정의 할 수 있습니다.

참조

effective java 3/e [조슈아 블로크]
yhmane github

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Item7

다 쓴 객체 참조를 해제하라

  • Java의 경우 C, C++ 처럼 메모리를 직접 관리하지 않음
  • C, C++의 경우 개발자가 메모리를 직접 할당하고 해제하지만 Java는 가비지 컬렉터가 존재한다
  • 하지만, 아예 신경을 안 써도 되는 것은 아니다.
  • 가비지 컬렉션의 소멸 대상이 되지 않는다면 메모리 누수가 일어나고, 대용량 처리의 경우 OOM을 일으킬 수도 있다



가비지 컬렉션의 소멸 대상

직접할당 해제

public class Stack {
    // 문제가 있는 메서드
    public Object pop() {
        if (size == 0) throw new EmptyStackException();
        return elements[--size];
    }

    // 직접할당 해제 하는 메서드
    public Object pop() {
        if (size == 0) throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] == null; // 다 쓴 참조 해제
        return result;
    }
}
  • 다슨 객체의 참조 변수에 null을 할당해준다.
  • heap 메모리에서 존재하는 객체는 어떠한 참조도 가지지 않기에 가비지 컬렉션의 소멸 대상이 된다.
  • 클래스 내에서 메모리 관리하는 객체라면 써야 겠지만(Stack), 일반적으론 아래의 방법(Scope를 통한 자동 할당 해제)가 좋

Scope를 통한 자동 할당 해제

  • 지역 변수의 범위를 최소화 해준다 (item57), 지역변수의 선언된 변수들은 함수 return과 함께 정리된다.
  • 메서드를 작게 유지하고, 한가지 기능에 초점을 맞춘다면 지역 변수 범위 최소화의 도움이 된다.



메모리 누수를 일으키는 주범

  • 첫번째 케이스처럼 클래스내에서 인스턴스에 대한 참조를 관리하는 객체 -> unreachable (null) 상태로 만들어 GC의 대상으로 만들어 준다
  • Map과 같은 캐시 -> WeakHashMap() 사용을 고려해 보자
  • 리스너 또는 콜백 -> 클라이언트가 콜백을 등록하고 명확히 해지하지 않는다면 콜백은 계속 쌓여 간다. 이럴 경우 약한 참조로 설정하여 주면 GC가 즉시 수거해 간다.



참조

effective java 3/e [조슈아 블로크]
yhmane github

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Item6

불필요한 객체 생성을 피하라

자주 사용되는 객체의 재사용

public class Item6 {
    public static void main(String[] args) {
        // 1 (not recomended)
        String s1 = new String("hello");
        // 2 recomended
        String s = "hello";

        // 3 (not recomended)
        Boolean trueOne = new Boolean(true);
        Boolean falseOne = new Boolean(false);

        // 4 recomended
        Boolean trueObject = Boolean.valueOf(true);
        Boolean falseObject = Boolean.valueOf(false);
    }
}

1번의 문장은 실행될 때 마다 String 인스턴스를 새로 만든다.

이 문장이 반복문이나 빈번히 호출되는 메서드 안에 있다면 String 인스턴스가 반복적으로 계속 생겨난다.

 

2번의 경우, 하나의 인스턴스를 사용한다.

또한, 같은 가상머신 안에서 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용하는 것이 보장된다.

 

3번의 경우, 매번 Boolean 생성자를 사용하는 방식이다. 추천하지 않는 방식이다.

 

4번의 경우, 정적 팩터리 메서드(아이템1)을 이용해 불피요한 객체 생성을 피할 수 있다.

 

비용이 큰 객체의 재사용

public class RomanNumerals {
    static boolean isRomanNumeral(String s) {
        return s.matches("^(?-.)M*(C[MD]|D?C{0,3})"
                + "(X[CL]|L?X{0,3}) (I[XV]|V?I{0,3|)$");
    }
}

이 방식의 문제점은 String.matches 메서드 이용에 있다.

String.matches는 정규 표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만,

메서드 내부에서 만드는 정규표현식용 Pattern은 한번쓰고 버려져 곧바로 가비지 컬렉션 대상이 된다

또한, Pattern은 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다.

public class RomanNumerals {
    private static final Pattern ROMAN = Pattern.compole(
            "^(?-.)M*(C[MD]|D?C{0,3})"
            + "(X[CL]|L?X{0,3}) (I[XV]|V?I{0,3|)$");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }
}

미리 Pattern을 초기화하여 재사용하는 것이 좋다. 이렇게 사용하면 코드의 가독성도 높아지고 더 빠른 성능을 보인다.

 

의도치 않은 Auto boxing

private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += i;
    }
    return sum;
}

이 코드는 정상적으로 수행된다. 하지만, 자세히 보면 문제점이 보인다.

sum의 경우 Long, i의 경우 long type이다. i가 sum에 더해질 때마다 Wrapper 클래스로 Auto 박시이 일어난다.

단순히, sum을 long으로 선언하기만 해도 성능은 향상된다.

참조 Item #6. 불필요한 객체 생성을 피하라

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Item3

private 생성자나 열거 타입으로 싱글턴임을 보증하라

싱글턴(Singleton)

싱글턴이란?

  • 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말합니다. (Application내에서 단 1개의 인스턴스만 생성할 수 있는 클래스)
  • 한번의 객체 생성으로 재사용이 가능하기 때문에 메모리 낭비를 방지할 수 있습니다.
  • 싱글톤으로 생성된 객체는 전역성을 띄기에 다른 객체와 공유가 용이합니다.

싱글턴의 단점

  • 싱글턴의 역할을 복잡하게 부여할 경우, 객체간의 결합도가 높아지는 문제가 발생살 수 있습니다.
  • 멀티 쓰레드 환경에서 동기화 처리 문제가 있습니다.
  • 인터페이스를 구현한 싱글턴 객체가 아니라면 mock 객체를 만들 수 없기에 테스트가 어렵습니다.

싱글턴의 구현

필드 방식의 싱글턴

public class Item3 {
    public static final Item3 INSTANCE = new Item3();
    private Item3() {}
}

public class Item3Main {
    public static void main(String[] args) {
        Item3 item3 = Item3.INSTANCE;
    }
}

정적 팩터리 메서드 방식의 싱글턴

public class Item3 {
    private static final Item3 INSTANCE = new Item3();
    public static Item3 getInstance() {
        return INSTANCE;
    }
    private Item3() {}
}


public class Item3Main {
    public static void main(String[] args) {
        Item3 item3 = Item3.getInstance();
    }
}

Enum 방식의 싱글턴

public enum Item3Enum {
    INSTANCE("윤호", 10);

    private String name;
    private int age;

    private Item3Enum(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

정리

  • 싱글턴을 만드는 위와 같이 다양히 존재합니다.
  • 리플렉션을 통한 예외가 존재하기에 생성자에 검증작업을 추가하여 새로운 인스턴스를 생성하지 못하도록 막아야 합니다.
if( INSTANCE != null) {
    throw new RuntimeException("Can't create Constructor");
}
  • 싱글 클래스를 직렬화한 후 역직렬화할 때 새로운 인스턴스를 만들어서 반환합니다. 다음과 같이 싱글턴임을 보장해야 합니다.
private Object readResolve(){
    // '진짜' 객체를 반환하고, 가짜 객체는 가비지 컬렉터에 맡깁니다.
    return INSTANCE;
}

참고

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


블로그 이미지

사용자 yhmane

댓글을 달아 주세요