반응형

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

hashCode 일반 규약을 어기에 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제

논리적으로 같은 객체는 같은 해시코드를 반환해야함

class example() {

    public static void main(String[] args) {
        Map<PhoneNumber, String> m = new HashMap<>();
        m.put(new PhoneNumber(707, 867, 5309), "제니");    
    }

}

위 코드에서 m.get(new PhoneNumber(707, 867, 5309))를 실행하면 "제니"가 나와야함
하지만, null을 반환함

PhoneNumber 클래스는 hashCode를 재정의하지 않았기 때문에 논리적 동치인 두 객체가
서로 다른 해시코드를 반환하여 두번째 규약을 지키지 못함

백기선님 강의 example

https://github.com/jshag90/effective-java-study/blob/main/src/main/java/com/ji/effective/java/chapter2/item11/hashtable/HashMapTest.java

최악의 (하지만 적법한) hashCode 구헌 - 사용 금지!

class sample(){

    @Override public int hashCode(){return 42;}
}

위와 같이 정의하면 모든 객체에서 똑같은 해시코드를 반환
해시테이블의 버킷 하나에 담겨 마치 연결 리스트 처럼 동작한다.

그 결과 평균 수행 시간이 O(1)인 해시테이블이 O(n)으로 느려져서 객체가 많아지면 도저기 쓸수 없게 된다.

곱한 숫자를 31로 정한 이유는 31이 홀수이면서 소수(prime)이기 때문임
소수를 곱하는 이유는 명확하지 않지만 정통적으로 그리해왔다.
결과적으로 31을 이용하면, 이 곱셈을 시프트 연산과 뺄셈으로 대체해 최적화할 수 있음
요즘 VM들은 이런 최적화를 자동으로 해줌

31이라는 숫자를 사용했을 때 가장 해시 충돌이 적었다고함

전형적인 hashCode 메서드

class sample(){
    @Override public int hashCode(){
        int result = Short.hashCode(areaCode);
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(lineNum);
        return result;
    }
}

한 줄짜리 hashCode 메서드 - 성능이 살짝 아쉽다.

class sample(){
    @Override public int hashCode(){
        return Object.hash(lineNum, prefix, areaCode);
    }
}

hash 메서드는 성능에 민감하지 않은 상황에서만 사용하자.

클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기보다는 캐싱하는 방식으로 고려해야함

해시의 키로 사용되지 않는 경우라면 hashCode가 처음 불릴 때 계산하는 지연 초기화(lazy initialization) 전략을 고려
(생성자에서 미리 계산하는게 아니라 hashCode()가 호출 되었을 때 값을 초기화)

해시코드를 지연 초기화하는 hashCode 메서드 - 스레드 안정성까지 고려해야 함

class Sample(){
    private int hashCode; // 자동으로 0으로 초기화됨

    @Override public int hashCode(){
        int result = hashCode;
        if(result == 0){
            int result = Short.hashCode(areaCode);
            result = 31 * result + Short.hashCode(prefix);
            result = 31 * result + Short.hashCode(lineNum);
            hashCode = result;
        }

        return result;
    }

}

성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안됨
특히 어떤 필드는 특정 영역에 몰른 인스턴스들의 해시코드를 넓은 범위로 고르게 퍼트려주는 효과가 있을 수 있음
핵심 필드가 없다면 수많은 인스턴스가 단 몇 개의 해시코드로 집중되어 해시테이블의 속도가 선형으로 느려짐

정리

equals를 재정의할 때는 hashCode도 반드시 재정의 해야함

hashCode는 Object의 API 문서에 기술된 일반 규약을 따라야 하며,
서로 다른 인스턴스라면 되도록 해시코드도 서로 다르게 규현해야함

AutoValue 프레임워크를 사용하면 멋진 equals와 hashCode를 자동으로 만들어줌

추가 정리

스레드 안전

https://github.com/jshag90/effective-java-study/blob/main/src/main/java/com/ji/effective/java/chapter2/item11/hashcode/PhoneNumber.java

 

728x90
반응형

try-finally - 더 이상 자원을 회수하는 최선의 방책이 아니다.!

class sampleCode() {

    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            br.close();
        }

    }
}

자원이 둘 이상이면 try-finally 방식은 너무 지저분하다!

static void copy(String src, String dst) 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();
        }

}

물리적인 문제가 생긴다면 firstLineOfFile 메서드 안의 readLine 메서드가 예외를 던지고,
같은 이유로 close 메서드도 실패할 것이다.
이러한 상황이라면 두번째 예외가 첫 번째 예외를 완전히 삼겨 버림

try-with-resources - 자원을 회수하는 최선책

static String firstLineOfFile(String path) throws IOException {
    try(BufferedReader br = new BufferedReader( new FileReader(path))){
        return br.readLine();
    }
        }

복수의 자원을 처리하는 try-with-resources 짧고 매혹적이다.

static void copy(String src, String dst) 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);
        }
}

readLine과 close 호출 약쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고
readLine에서 발생한 예외가 기록됨

try-with-resouces를 catch 절과 함께 쓰는 모습

static String firstLineOfFile(String path,String defaultVal){
        try(BufferedReader br=new BufferedReader(
        new FileReader(path))){
            return br.readLine();
        }catch(IOException e){
            return defaultVal;
        }
}

정리

꼭 회수해야 하는 자원을 다룰 때는 try~finll 말고, try-with-resources를 사용하자
코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용함

728x90
반응형

finalizer와 cleaner 사용을 피하라

finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.

cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

finalizer와 cleanser는 즉시 수행된다는 보장이 없다. 객체에 접근 할 수 없게 된 후 finalizer나 cleaner가 실행되기까지
얼마나 걸릴지 알 수 없다.
finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다.

finalizer와 cleaner는 심각한 성능 문제도 동반한다.

finalize가 가비지 컬렉터의 효율을 떨어뜨리기 때문이다.

객체 생성을 막으려면 생성자에서 예외를 던지는 것만드로 충분하지만, finalize가 있다면 그렇지 않다.

final 클래스들은 그 누구도 하위 클래스를 만들 수 없으니 이 공격에서 안전함

final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.

AutoCloaseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 됨

cleaner와 finalizer를 적절히 활용하는 두 번째 예는 네이티브 피어(native peer)와 연결된 객체에서다.

cleaner를 안정망으로 활용하는 AutoCloseable 클래스

public class Room implements AutoCloseable {
    private static final Cleaner cleaner= Cleaner.create();

    private static class State implements Runnable {
        int numJunkPiles;

        State(int numJunkPiles){
            this.numJunkPiles = numJunkPiles;
        }

        @Override public void run(){
            System.out.println("방 청소");
            numJunkPiles = 0;
        }
    }

    private final State state;

    private final Cleaner.Cleanable cleanable;

    public Room(int numJunkPiles){
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }

    @Override public void close(){
        cleanable.clean();
    }

}

모든 Room 생성을 try-with-resources 블록으로 감싼다면 자동 청소는 전혀 필요하지 않음

public class Adult{
    public static void main(String[] args) {
        try(Room myRoom = new Room(7)){
            System.out.println("안녕~");
        }
    }
}

방 청소를 하지 않는 프로그램

public class Teenager {
    public static void main(String[] args) {
        new Room(99);
        System.out.println("아무렴");
    }
}

정리

cleaner는 안정망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자,
물론 이런 경우라도 불확실성과 성능 저하에 주의해야 한다.

728x90

+ Recent posts