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

댓글을 달아 주세요

Item17

변경 가능성을 최소화하라

불변 클래스란 간단히 말해 그 인스턴스의 내부 값을 수정할 수 없는 클래스다. 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
String, BigInteger, BigDecimal 등이 불변 클래스이며, 가변 클래스보다 설계하고 구현하기 쉬우며 오류가 생길 여지도 적고 안전하다.

클래스를 불변으로 만들시 지켜야 할 규칙

객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.

  • setter나 필드의 정보를 변경하는 메서드를 제공하지 않는다

클래스를 확장할 수 없도록 한다.

  • 객체의 상태를 변하게 만드는 사태를 막아준다. 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것이다.

모든 필드를 final로 선언한다.

  • 필드의 수정을 막겠다는 설계자의 의도를 드러내는 방법이다.
  • 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하여 준다.

모든 필드를 private으로 선언한다.

  • 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근하여 수정하는 일을 막아 준다.

자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

  • 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 인스턴스 내에 가변 객체의 참조를 얻을 수 없게 해야한다.
  • 생성자, 접근자(getter), readObject 메서드 모두에서 방어적 복사를 수행한다.

불변클래스

불변 복소수 클래스

public final class Complex {

    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public double realPart() {
        return re;
    }

    public double imaginaryPart() {
        return im;
    }

    public Complex plus(Complex c) {
        return new Complex(re + c.re,im + c.im);
    }

    public Complex minus(Complex c) {
        return new Complex(re - c.re,im - c.im);
    }

    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im,re * c.im + im + c.re);
    }

    public Complex dividedBy(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp,
                (im * c.re - re + c.im) / tmp);
    }

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

        if (!(o instanceof Complex)) {
            return false;
        }

        Complex c = (Complex) o;
        return Double.compare(c.re, re) == 0
                && Double.compare(c.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }

}
  • 실수부와 허수부 값을 반환하는 접근자(realPart, imaginaryPart)와 사칙연산 메서드(plus, minusm times, dividedBy)를 정희하였다.
  • 이 사칙연산 매서드들은 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환한다.
  • 이처럼 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴을 함수형 프로그래밍이라 한다.
  • 이처럼 함수형 프로그래밍 기법을 적용하면 코드의 불변이 영역이 되는 비율이 높아져 안전합니다.

    불변 객체의 장점

  • 불변 객체는 근본적으로 스레드 안전하여 따로 동기화 할 필요가 없다
  • 불변 객체는 안심하고 공유 할 수 있다
  • 불변 객체는 방어적 복사본이 필요없다
  • 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
  • 불변 객체를 key로 하면 이점이 많다.
    • Map의 key
    • Set의 원소
  • 불변 객체는 그 자체로 실패 원자성을 제공한다

결론

  • 모든 클래스를 불면으로 만들수는 없다.
  • 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여야 한다.
  • 다른 합당한 이유가 없다면 모든 필드는 private final 이어야 한다.
  • 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

참조

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Item16

public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

public 필드 사용

class Point {
    public double x;
    public double y;
}

public class Execution {
  public static void main(String[] args) {
    Point p = new Point();
    p.x = 123;
    System.out.println(p.x);
  }
}
  • 위와 같은 클래스 Point는 데이터 필드에 직접 접근할 수 있으니 캡슐화의 이점을 제공하지 못합니다.
  • API를 수정하지 않고는 내부 표현을 바꿀 수 없습니다.
  • 불변식을 보장할 수 없습니다.

접근자와 변경자 설정을 통해 데이터를 캡슐화

class Point2 {
  private double x;
  private double y;

  public Point2(double x, double y) {
    this.x = x;
    this.y = y;
  }

  public double getX() {
    return x;
  }

  public double getY() {
    return y;
  }

  public void setX(double x) {
    this.x = x;
  }
  public void setY(double y) {
    this.y = y;
  }
}

public class Execution {
  public static void main(String[] args) {
    Point2 p2 = new Point2(12, 34);
    p2.setX(123);
    System.out.println(p2.getX());
  }
}
  • private field를 사용하여 직접적인 접근을 막습니다.
  • 접근자(getter)와 수정자(setter)를 통해 내부 표현 방식의 유연성을 얻습니다.
  • package-private(default class) 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 문제가 없습니다.

결론

  • public 클래스는 절대 가변 필드를 직접 노출해서는 안됩니다.
  • 불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없습니다. (item 15의 배열의 경우 안심할 수 없음)
  • package-private 클래스나 중첩 클래스에서는 종종 필드를 노출하는 편이 나을때도 있습니다.

참조

블로그 이미지

사용자 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

댓글을 달아 주세요

Item12

toString을 항상 재정의하라

Object의 기본 toString 메서드가 우리가 작성한 클래스에 적합한 문자열을 반환하는 경우는 거의 없습니다.
이 메서드는 PhoneNumber@adbbd처럼 단순히 클래스_이름@16진수로_표시한_해시코드를 반환합니다.
toString의 일반 규약에 따르면 '간결하고 사람이 읽기 쉬운 형태의 유익한 정보'를 반환해야 합니다.
toString의 규약은 '모든 하위 클래스에서 이 메서드를 재정의하라'고 합니다.


toString 재정의

  • toString을 잘 구현한 클래스는 사용하기 편하고 디버깅하기 쉽습니다.
    • {Jenny=PhoneNumber@adbbd} 보다는 {Jenney=707-867-5309}라는 메세지가 유용합니다.
  • 실전에서 toString 그 객체가 가진 주요 정보 모두를 반환하는게 좋습니다.
  • toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야 합니다.
    • 포맷을 명시하면 그 객체는 표준적이고, 명확하고, 사람이 읽을 수 있게 됩니다.
    • 단, 포맷을 한번 명시하면, 평생 그 포맷에 얽매이게 됩니다.
  • 포맷을 명시하든 아니든 개발자의 의도는 명확히 밝혀야 합니다.
  • 포맷 명이 여부와 상관없이 toString이 반환한 값에 대해 포함된 정보를 얻어올 수 있는 API를 제공해야 합니다.

toString 재정의가 필요치 않는 경우

  • 정적 유틸리티 클래스
  • enum Type
  • 대다수의 컬렉션 구현체
  • 구글의 @Autovalue, Lombok의 @ToString

결론

  • 재정의가 필요치 않는 경우를 제외하곤 모든 클래스에서 toString을 재정의 하는게 좋습니다.
  • toString은 해당 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야 합니다.

참조

yhmane github
이펙티브 자바 3/e [조슈아블로크]

블로그 이미지

사용자 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

댓글을 달아 주세요

Item10

equals는 일반 규약을 지켜 재정의하라

  • equals 메서드는 여러 조건들을 충족해야 하기에 재정의하기 쉽지 않습니다.
  • 문제를 회피하는 가장 쉬운 방법은 아예 재정의 하지 않는 것입니다.
  • 아래의 사항에 해당하는 것이 있다면 재정의 하지 않는 것이 최선

equals를 재정의 하지 않아도 되는 경우

각 인스턴스가 본질적으로 고유할 경우

  • 값을 표현하는게 아닌 동작하는 개체를 표현할 경우. 대표적으로 Thread

인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없을 경우

  • java.utils.regex.Pattern은 equals를 재정의해서 두 Pattern의 인스턴스가 같은 정규표현식을 나타내는지 검사합니다.

상위 클래스에서 재정의한 equals가 하위 클래스에서도 딱 들어맞을 경우

  • AbstractSet -> Set, AbstractList -> List, AbstractMap -> Map 들은 상위 Abstract 클래스로부터 equals를 구현 받아 사용합니다.

클래스가 private이거나, package-private여서 equals를 호출할 일이 없는 경우

  • equals가 실수로라도 호출되는 걸 막고 싶다면 다음처럼 구현을 하면 됩니다.
    @Override public boolean equals(Object o) {
      throw new AssertionError();
    }

싱글턴을 보장하는 클래스'인스턴스 통제, Enum'인 경우


equals를 재정의 할 경우 지켜야 할 규약

반사성 (reflexivity)

  • null이 아닌 모든 참조 값 x에 대해, 'x.equals(x) = true' 이어야 합니다. (자기 자신에 대해서 true)

대칭성 (symmetry)

  • null이 아닌 모든 참조 값 x, y에 대해 'x.equals(y) = true'면 'y.equals(x) = true'를 만족해야 합니다.

    public final class CaseInsensitiveString {
        private final String s;
    
        public CaseInsensitiveString(String s) {
          this.s = Objects.requireNonNull(s);
        }
    
        @Override
        public boolean equals(Object o) {
          if(o instanceof CaseInsensitiveString) {
                return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
          }
    
          if(o instanceof String) {
                return s.equalsIgnoreCase((String) o);
          }
          return false;
        }
    }
    CaseInsensitiveString cis = new CaseInsensitiveString("hello?");
    String hello = "hello?";
    System.out.println(cis.equals(hello)); //true
    System.out.println(hello.equals(cis)); //false

cis.equals(hello)는 true를 반환하지만, hello.equals(cis)는 false를 반환한다. String의 equals는 일반 String을 알고 있지만 CaseInsenitiveString의 존재를 알수 없다.

추이성

  • null이 아닌 모든 참조 값 x, y, z에 대해 'x.equals(y) = true'면 'y.equals(z) = true'도 만족하면 'x.equals(z) = true'도 만족해야 합니다.

    class Point {
        private final int x;
        private final int y;
    
        public Point(int x, int y) {
          this.x = x;
          this.y = y;
        }
    
        @Override
        public boolean equals(Object o) {
          if(!(o instanceof Point)) return false;
          Point p = (Point) o;
          return this.x == p.x && this.y == p.y;
        }
    }
    class ColorPoint extends Point {
    
        private final Color color;
    
        @Override
        public boolean equals(Object o) {
          if(!(o instanceof Point)) {
              return false;
          }
    
          if(!(o instanceof ColorPoint)) {
              return o.equals(this);
          }
          return super.equals(o) && this.color == ((ColorPoint) o).color;
        }
    }
    ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
    Point p2 = new Point(1, 2);
    ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

    p1 -> p2, p2 -> p3를 만족하지만 p1 -> p3는 색상도 비교해야 하기에 만족하지 않습니다. 따라서 추이성 규약을 위반합니다.

일관성

  • null이 아닌 모든 참조 값 x, y에 대해 'x.equals(y)'는 true/false 등 항상 일관된 값을 반환해야 합니다.

null-아님

  • null이 아닌 모든 참조 값 x에 대해 'x.equals(null) = false'입니다.
    @Override 
    public boolean equals(Object o) {
        if (o == null) {
            return false; 
        }
        ...
    }
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof MyType)) {
            return false; 
        } 
        ...
    }

equals 구현 절차

  • == 연산자를 사용해 입력이 자기 자신의 참조인지 확인합니다.
    • 성능 최적화용으로 equals가 복잡할 때 같은 참조를 가진 객체에 대한 비교를 안하기 위함입니다.
  • instanceof 연산자로 입력이 올바린 타입인지 확인합니다.
    • 올바르지 않다면 false를 반환합니다.
    • equals중에서는 같은 interface를 구현한 클래스끼리 비교하는 경우도 있습니다.
  • 입력을 올바른 타입으로 형변환합니다.
    • 앞서 instanceof 연산을 수행했기 때문에 100% 성공합니다.
  • 입력 객체와 자기 자신의 대응되는 '핵삼' 필드들이 모두 일치하는지 하나씩 검사합니다.
    • 모두 일치하면 true, 하나라도 다르면 false를 반환합니다.
    • interface 비교시 필드정보를 가져오는 메서드가 interface에 정의되어있어야하고, 구현체에서 메서드를 재정의 해야합니다.
  • float, double을 제외한 기본타입은 ==을 통해 비교하고 참조(reference) 타입은 equals를 통해 비교합니다.
    • float, double은 Float.compare(float, float)와 Double.compare(double, double)로 비교합니다.
    • 배열의 모든 원소가 핵심 필드라면 Arrays.equals를 사용합니다.
  • null도 정상값으로 취급하는 참조타입 필드도 있습니다.
    • Objects.equals(obj, obj)를 이용해 NullPointerException 발생을 예방합니다.
  • 최상의 성능을 내기 위해 다음과 같은 방법이 있습니다.
    • 다를 확률이 높은 필드부터 비교합니다.
    • 비교하는 비용이 작은 것을 먼저 수행합니다.

결론

  • 꼭 필요한 경우가 아니면 equals를 재정희 하지 말자.
  • 많은 경우에 Object의 equals가 정확한 비교를 수행하여 줍니다.
  • 재정의할때는 위 다섯 가지 규약을 확실히 지켜가며 정의하도록 합니다.

참조

yhmane github

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Item9


try-finally보다는 try-with-resources를 사용하라

  • InputStream, OutputStream, Connection 등 close()를 직접 호출해 닫아줘야 하는 자원이 많이 있습니다.
  • 성능문제로 이어질 수 있기 때문에 finalizer를 이용하지만 믿을만하지 못합니다.
static String firstLineOfFile(String path) throws IOException {
    BuffreredReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}

static void copy(String src, String dist) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0) {
                out.write(buf, 0, n);
            }   
        } finally {
            out.close();
        }   
    } finally {
        in.close();
    }
}
  • 첫 번째의 경우 괜찮지만, 두번째의 경우 꽤 난해한 코드가 작성 되었습니다.
  • 예외는 try블록과 finally 블록 모두에서 발생할 수 있습니다.
  • 물리적인 문제가 발생한다면 try, finally에서 모두 예외가 발생합니다.
  • 하지만, 이런 경우 두번째 예외가 첫번째 문제를 삼켜서 실제 시스템에서 버그 추적이 어려워 질 수도 있습니다.

대안점은?

  • 자바7이 투척한 try-with-resource를 사용
  • AutoCloseable를 구현하고 close method()를 사용할 것
static String firstLineOfFile(String path) throws IOException {
    try (BuffreredReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    } 
}

static void copy(String src, String dist) throws IOException {
    try (InputStream in = new FileInputStream(src);
         OutputStream out = new FileOutputStream(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0) {
            out.write(buf, 0, n);
        }
    }
}

static String firstLineOfFile(String path) throws IOException {
    try (BuffreredReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) {
        return defaultVal;
    }
}
  • try-with-resources를 이용하면 첫번째 예외부터 추적이 가능해집니다.
  • 뿐만아니라 스택 추적 영역으로 예외를 추적할 수 있습니다.

결론

  • try-finally보다는 try-with-resources를 사용하는 것이 좋습니다.
  • 코드는 짧고 명확해지고, 예외 정보도 더욱 유용합니다.

참조

yhmane github

블로그 이미지

사용자 yhmane

댓글을 달아 주세요