[2주차] 7-14
Joshua Bloch 저 이복연 역 「Effective Java 3rd Edition, 2018」를 읽고 정리하였습니다.
아이템 7: finalized cleaner 사용을 피하라.
자바는 두 가지 객체 소멸자를 제공한다.
finalizer는 기본적으로 쓰지 말아야 한다. 자바 9에서 deprecated API로 지정되고 cleaner를 그 대안으로 소개 했다.
cleaner는 finalizer보다는 덜 위험하지만 여전히 예측할 수 없고 느리고 일반적으로 불 필요하다.
자바에서는 접근할 수 없게 된 객체를 회수하는 역할을 가비지 컬렉터가 담당한다.
자바에서는 try-withresources와 try-finally를 사용해 해결한다.
finalizer와 cleaner는 즉시 수행된다는 보장이 없다. 따라서 제때 실행되어야 하는 작업에 절대 사용면 안된다.
finalizer나 cleaner를 얼마나 신속히 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달렸으며, 이는 가비지 컬렉터 구현마다 천차만별이다. 테스트한 JVM에서는 완벽하게 동작하던 프로그램이 가장 중요한 고객의 시스템에서는 엄청난 재앙을 일으킬지도 모른다.
저자 친구 GUI프로그램이 OutOfMemory 에러가 낫고, 원인은 객체 수천개가 finalizer에서 기다리고 있었는데... finalizer 스레드가 다른 애플리케이션 스레드보다 우선 순위가 낮아서 실행될 기회를 제대로 얻지 못한 것이다.
사례를 들어 데이터베이스 같은 공유 자원의 영구 락 해제를 finalizer나 cleaner에 맡겨 놓으면 분산 시스템 전체가 서서 히 멈출 것이다.
System.gc나System.runFinalization메서드는 finalizer와 cleaner가 실행될 가능성을 높여줄 수는 있으나 보장해주진 않는다. 문제를 보장하기 위해 System.runFinalizersOnExit, Runtime.runFinalizersOnExit 메서드가 있어왔으나 심각한 결함이 있다.finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.
공격원리 -> 생성자나 직렬화 과정(ReadObject와 readResolve 메서드, 12장)에서 예외가 발생하면, 이 생성되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 된다.
AutoClosable은 12ns가 걸린 반면 finalizer를 사용하면 550ns 걸렸다. 안정망을 설치할때는 66ns가 걸렸다.
안전망: finalize() 안에서 close()를 호출해준다. FileInputStream 참고.
객체 생성을 막으려면 생성자에서 예외를 던지는 것만으로 충분하지만 finalizer가 있다면 그렇지도 않다.
아이템 8: finalized cleaner 사용을 피하라.
더 현실적으로 만들려면 numJunkPile 필드는 네이티브 피어를 가리키는 포인터를 담은 final long 변수여야 한다.
run 메서드는 cleanable에 의해 딱 한 번만 호출된다. (등록시 호출)
State 인스턴스는 절대로 Room 인스턴스를 참조해서는 안 된다. 순환참조가 생겨 가비지 컬렉터가 Room 인스턴스를 회수 해갈 기회가 오지 않는다.
Room의 cleaner는 안전망으로만 쓰였다. 클라이언트가 모든 Room 생성을 try-with-resources 블록으로 감왔다면 자동 청소는 전혀 필요하지 않다.
이템 9: try-finally보다는 try-with-resources를 사용하라
자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. Inputstream, Outputstream, java.sql.Connection 등좋은 예다. 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다. 이런 자원 중 상당수가 안전망으로 finalizer를 활용하고는 있지만 finalized 그리 믿을만하지 못하다(아이템 8).
만약 사용하는 자원이 두개였다면.. try-finally는 아래 같이 복잡해진다.
그리고 try-finally 문을 제대로 사용한 앞의 두 예제 조차 미묘한 결점이 있다. 예외는 try 블록과 finally 블록 모두에서 발생할 수 있는데, 예컨대 기기에 물리적인 문제가 생긴다면 firstLineOfFile01 메서드 안의 readLine 메서드가 예외를 던지고 같은 이유로 close 메서드도 실패할 것이다. 이런 상황이라면 두 번째 예외가 첫 번째 예외를 완전히 집어삼켜 버린다. 그러면 스택 추적 내역에 첫 번째 예외에 관한 정보는 남지 않게 되어 실제 시스템에서의 디버깅을 몹시 어렵게 한다(일반적으로 문제를 진단하려면 처음 발생한 예외를 보고 싶을 것이다).
대비되는 try-with-resources 장점
close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록된다. 이렇게 숨겨진 예외들도 그냥 버려지지는 않고, 스택 추적 내역에 "숨겨졌다"는 (suppressed) 꼬리표를 달고 출력된다. 또한, 자바 7에서 Throwable에 추가 된 getSuppressed()를 이용하면 프로그램 코드에서 가져올 수도 있다. try-with-resource suppressed 예제
총코드
요약, 꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resources를 사용하자.
3장 모든 객체의 공통 메서드 (10-14)
Object는 객체를 만들 수 있는 구체 클래스지만 기본적으로는 상속해서 사용하도록 설계되었다. Object에서 final이 아닌 메서드(equals, hashCode, toString, clone, finalize)는 모두 재정의(overriding)를 염두에 두고 설계된 것이라 재정의 시 지켜야 하는 일반 규약이 (general contracts) 명확히 정의되어 있다. 그래서 Object를 상속하는 클래스, 즉 모든 클래스는 이 메서드들을 일반 규약에 맞게 재정의해야 한다. 메서드를 잘못 구현하면 대상 클래스가 이 규약을 준수한다고 가정하는 클래스(HashMap과 HashSet 등)를 오동작하게 만들 수 있다.
이번 장에서는 final이 아닌 Object 메서드들을 언제 어떻게 재정의해야 하는지를 다룬다.
아이템 10: equals는 일반 규약을 지켜 재정의하라.
다음 중 하나에 해당한다면 equals를 재정의하지 않는 것이 최선이다.
각 인스턴스가 본질적으로 고유하다. 예를들어, Thread
인스턴스의 '논리적 동치성(logical equality)’을 검사할 일이 없다. 예를들어, Pattern.
상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다. List, Set, Map - AbstractList, AbstractSet, AbstractMap의 equals 상속받아 사용한다.
클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.
재정의를 해야 될때는 언제일까?
객체 식별성이 아니라 (object identity 두 객체가 물리적으로 같은가) 논리적 동치성을 확인해야 하는데, 상위 클래스의equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때다. 주로 값클래스들이 (Integer, String) 해당된다. 객체가 같은 지가 아니라 값이 같은지가 궁금할 것이다. 논리적 동치성을 판단하도록 재정의하면, 인스턴스 값을 비교도 할 뿐만 아니라 Map의 키와 Set의 원소로 쓸 수 있다.
equals 메서드 재정의시 지켜야할 규약
반사성 reflexibility
대칭성 symmetry
추이성 transitivity
일관성 consistency
null-아님
세상에 홀로 존해하는 클래스는 없다. 한 클래스의 인스턴스는 다른 곳으로 빈번히 전달된다. 그리고 컬렉션 클래스들을 포함해 수많은 클래스는 전달받은 객체가 equals 규약을 지킨다고 가정하고 동작한다.
동치관계를 만족시키기 위한 다섯 요건을 하나씩 살펴보자.
동치관계 = 집합을 서로같은원소들로 이뤄진 부분집합(동치류, equivalence class)으로 나누는 연산. equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 한다.
반사성 reflexibility
이 요건은 일부러 어기는 경우가 아니라면 만족시키지 못하기 더 어렵다.
대칭성 symmetry
위배 사례를 보자.
추이성 transitivity
Color enum 생성 -> Point 부모 -> ColorPoint 자식
또다른 하위클래스 있을시, 재귀 위험도 있을 수 있다.
해결 방법은 무엇일까?
구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다. 객체 지향적 추상화의 이점을 포기하지 않는 한은 말이다. 이 말은 얼핏, equals 안의 instanceof 검사를 getClass 검사로 바꾸면 규약도 지키고 값도 추가하면서 구체 클래스를 상속할 수 있다는 뜻으로 들린다. (아래)
하지만 실제 활용할 수는 없다. 예를 들기 위해, 주어진 점이 (반지름이 1인) 단위 원 안에 있는지를 판별하는 메서드가 필요하다고 해보자.
리스코프 치환 원칙에 따르면, Point의 하위 클래스는 정의상 여전히 Point이므로 어디서든 Point로써 활용될 수 있어야 한다. 그러나 CounterPoint의 인스턴스를 onUnitCircle 메서드에 넘기면 어떻게 될까? false반환.
Set을 포함하여 대부분의 컬렉션은 contains 작업에 equals 메서드를 이용하는데, CounterPoint의 인스턴스는 어떤 Point와도 같을 수 없기 때문이다.
구체 클래스의 하위 클래스에서 값을 추가할 방법은 없지만 괜찮은 우회 방법이 하나 있다. 합성.
자바 라이브러리에도 구체 클래스를 확장해 값을 추가한 클래스가 종종 있다. Timestamp는 Date를 확장한 후 nanoseconds 필드를 추가했다. 그 결과로 Timestamp의 equals는 대칭성을 위배하며, Date 객체와 한 컬렉션에 넣거나 서로 섞어 사용하면 엉뚱하게 동작할 수 있다. Timestamp를 이렇게 설계한 것은 실수니 절대 따라 해서는 안된다.
일관성 consistency
두 객체가 같다면 (어느 하나 혹은 두 객체 모두가 수정되지 않는 한) 앞으로도 영원히 같아야 한다는 뜻이다.
클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 된다. 이 제약을 어기면 일관성 조건을 만족시키기가 아주 어렵다. 예컨대 java.net.URL의 equlas는 URL과 IP 주소를 이용해 비교한다
URL을 저렇게 만든건 실수였으니 절대 따라 해서는 안 된다. 이런 문제를 피하려면 equals는 항시 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 한다.
null-아님
모든 객체가 null과 같지 않아야 한다는 뜻이다.
지금까지의 내용을 종합해서 equals 메서드 구현
== 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
instanceof 연산자로 입력이 올바른 타입인지 확인한다.
입력을 올바른 타입으로 형변환한다.
입력 객체와 자기 자신의 대응되는 '핵심 '필드들이 모두 일치하는지 하나씩 검사한다.
3장 모든 객체의 공통 메서드 (10-14)
Object는 객체를 만들 수 있는 구체 클래스지만 기본적으로는 상속해서 사용하도록 설계되었다. Object에서 final이 아닌 메서드(equals, hashCode, toString, clone, finalize)는 모두 재정의(overriding)를 염두에 두고 설계된 것이라 재정의 시 지켜야 하는 일반 규약이 (general contracts) 명확히 정의되어 있다. 그래서 Object를 상속하는 클래스, 즉 모든 클래스는 이 메서드들을 일반 규약에 맞게 재정의해야 한다. 메서드를 잘못 구현하면 대상 클래스가 이 규약을 준수한다고 가정하는 클래스(HashMap과 HashSet 등)를 오동작하게 만들 수 있다.
이번 장에서는 final이 아닌 Object 메서드들을 언제 어떻게 재정의해야 하는지를 다룬다.
아이템 10: equals는 일반 규약을 지켜 재정의하라.
다음 중 하나에 해당한다면 equals를 재정의하지 않는 것이 최선이다.
각 인스턴스가 본질적으로 고유하다. 예를들어, Thread.
인스턴스의 '논리적 동치성(logical equality)’을 검사할 일이 없다. 예를들어, Pattern.
상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다. List, Set, Map - AbstractList, AbstractSet, AbstractMap의 equals 상속받아 사용한다.
클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다. 무슨말?
재정의를 해야 될때는 언제일까?
객체 식별성이 아니라 (object identity 두 객체가 물리적으로 같은가) 논리적 동치성을 확인해야 하는데, 상위 클래스의equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때다. 주로 값클래스들이 (Integer, String) 해당된다. 객체가 같은 지가 아니라 값이 같은지가 궁금할 것이다. 논리적 동치성을 판단하도록 재정의하면, 인스턴스 값을 비교도 할 뿐만 아니라 Map의 키와 Set의 원소로 쓸 수 있다.
무슨말?@@ -> 값을 비교하지 않고 물리적인 위치를 비교할때 재정의 해야된다는거지?
equals 메서드 재정의시 지켜야할 규약 - 그림 그리며 설명
반사성 reflexibility
대칭성 symmetry
추이성 transitivity
일관성 consistency
null-아님
세상에 홀로 존해하는 클래스는 없다. 한 클래스의 인스턴스는 다른 곳으로 빈번히 전달된다. 그리고 컬렉션 클래스들을 포함해 수많은 클래스는 전달받은 객체가 equals 규약을 지킨다고 가정하고 동작한다.
동치관계 = 집합을 서로같은원소들로 이뤄진 부분집합(동치류, equivalence class)으로 나누는 연산. equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 한다. 동치관계를 만족시키기 위한 다섯 요건을 하나씩 살펴보자.
반사성 reflexibility
대칭성 symmetry
위배 사례를 보자.
추이성 transitivity
Color enum 생성 -> Point 부모 -> ColorPoint 자식
또다른 하위클래스 있을시, 재귀 위험도 있을 수 있다.
해결 방법은 무엇일까?
d아래 이해 안되@@@
구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다. 객체 지향적 추상화의 이점을 포기하지 않는 한은 말이다. 이 말은 얼핏, equals 안의 instanceof 검사를 getClass 검사로 바꾸면 규약도 지키고 값도 추가하면서 구체 클래스를 상속할 수 있다는 뜻으로 들린다. (아래)
하지만 실제 활용할 수는 없다. 예를 들기 위해, 주어진 점이 (반지름이 1인) 단위 원 안에 있는지를 판별하는 메서드가 필요하다고 해보자.
리스코프 치환 원칙에 따르면, Point의 하위 클래스는 정의상 여전히 Point이므로 어디서든 Point로써 활용될 수 있어야 한다. 그러나 CounterPoint의 인스턴스를 onUnitCircle 메서드에 넘기면 어떻게 될까? false반환.
Set을 포함하여 대부분의 컬렉션은 contains 작업에 equals 메서드를 이용하는데, CounterPoint의 인스턴스는 어떤 Point와도 같을 수 없기 때문이다.
구체 클래스의 하위 클래스에서 값을 추가할 방법은 없지만 괜찮은 우회 방법이 하나 있다. 합성.
아래가 위 문제를 해결해주는지 테스트 해보기@@@@
자바 라이브러리에도 구체 클래스를 확장해 값을 추가한 클래스가 종종 있다. Timestamp는 Date를 확장한 후 nanoseconds 필드를 추가했다. 그 결과로 Timestamp의 equals는 대칭성을 위배하며, Date 객체와 한 컬렉션에 넣거나 서로 섞어 사용하면 엉뚱하게 동작할 수 있다. Timestamp를 이렇게 설계한 것은 실수니 절대 따라 해서는 안된다.
직접 보여주면 조겠음@@ api에 주의사항 위배해서 만든경우와 잘 작성한경우...@@
일관성 consistency
클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 된다. 이 제약을 어기면 일관성 조건을 만족시키기가 아주 어렵다. 예컨대 java.net.URL의 equlas는 URL과 IP 주소를 비교한다
~~ 사이 이해안됨..
실수였으니 절대 따라 해서는 안 된다. 이런 문제를 피하려면 equals는 항시 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 한다.
null-아님
모든 객체가 null과 같지 않아야 한다는 뜻이다.
위에 instanceof 널이었으면 어떤 결과가 나올까!! @@@????
지금까지의 내용을 종합해서 equals 메서드 구현
== 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
instanceof 연산자로 입력이 올바른 타입인지 확인한다.
입력을 올바른 타입으로 형변환한다.
입력 객체와 자기 자신의 대응되는 '핵심 '필드들이 모두 일치하는지 하나씩 검사한다.
생략@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
아이템 11: equals를 재정의하려거든 hashCode도 재정의하라.
equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다. 그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킨다.
Object 명세에서 발췌한 규약
equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다. 단, 애플리케이션을 다시 실행한다면 이 값이 달라져도 상관없다.
equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다. -> 재정의 잘못했을때 가장 문제가 되는 조항.
equals가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
Hash function을 수행하여 관리하는 HashMap 예제를 확인해보자.
왜 다르 값이 나올까? - 두번째 Contract 위배
좋은 hashCode를 작성하는 요령 무슨말
int 변수 result를 선언한 후 값 c로 초기화한다.
해당객체의 나머지 핵심필드 f 각각에 대해 다음작업을 수행한다.
해당 필드의 해시코드 c를 계산한다.
단계 2.1에서 계산한 해시코드 c로 result를 갱신한다.
result를반환한다.
주의할 점은 성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안 된다. 그리고 hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자. 자바 라이브러리의 많은 클래스에서 hashCode 메서드가 반환하는 정확한 값을 알려준다. 바람직하지 않은 실수지만 바로잡기에는 이미 늦었다.
아이템12: toString을 항상 재정의하라
toString의 규약은 "모든 하위 클래스에서 이 메서드를 재정의하라"고 한다.
아이템13: clone 재정의는 주의해서 진행하라
이번 아이템 에서는 clone 메서드를 잘 동작하게끔 해주는 구현 방법과 언제 그렇게 해야 하는지를 알려주고, 가능한 다른 선택지에 관해 논의한다.
클래스가 가변 객체를 참조하는 순간 재앙으로 돌변한다.
아래 클래스를 복제할 수 있도록 하고 (Cloneable 설정), super.clone의 결과를 그대로 반환한다면 어떻게 될까? size 필드는 올바른 값을 갖겠지만 elements 필드는 (리스트) 원본 Stack 인스턴스와 똑같은 배열을 참조할 것이다.
clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다.
참고, 복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다. final 특징 알아보기.
HashTable을 구현해서 확인해보자.
Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다. 이 메서드는 가장 먼저 supen.clone을 호출한 후 필요한 필드를 전부 적절히 수정한다. 이 말은 그 객체의 내부 ‘깊은 구조’에 숨어 있는 모든 가변 객체를 복사하고, 복제본이 가진 객체 참조 모두가 복사된 객체들을 가리키게 함을 뜻한다. 이러한 내부 복사는 주로 clone을 재귀적으로 호출해 구현하지만 이 방식이 항상 최선인 것은 아니다. 기본 타입 필드와 불변 객체 참조만 갖는 클래스라면 아무 필드도 수정할 필요가 없다. 단, 일련번호나 고유 ID는 비록 기본타입이나 불변일지라도 수정해줘야 한다.
Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다. 그렇지 않은 상황에서는 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다.
아이템 14: Comparable을 구현할지 고려하라.
equals와 다르게 compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다. Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서(natural order)가 있음을 뜻한다. 그래서 Comparable을 구현한 객체 들의 배열은 Arrays.sort(a) 손쉽게 정렬할 수 있다.
검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 역시 쉽게할 수 있다. 예컨대 다음 프로그램은 명령줄 인수들을 (중복은 제거하고) 알파벳순으로 출력한다.
compareTo와 equals는 항상 일관되지 않는다. 예, BigDicimal을 각각 HashSet과 TreeSet에 넣을 경우 차이가 있다.
CaseInsensitiveString에 compareTo 구현. 자바 7 부터는 정적메서드 compare사용
PhoneNumber에 compareTo 구현.
자바 8부터 비교자 생성 메서드 Comparator construction method를 사용하면 연쇄 방식으로 비교자를 생성할 수 있게 되었다. 이렇게 사용하자..
핵심정리
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다. compareTo 메서드에서 필드의 값을 비교할 때 <와 > 연산자는 쓰지 말아야 한다. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
Last updated