결론
꼭 필요한 경우가 아니면 equals를 재정의하지 말자.
많은 경우에 Object의 equals가 여러분이 원하는 비교를 정확히 수행해준다.
정의해야할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 규약을 확실히 지켜가며 비교해야 한다.
AutoValue 프레임워크를 사용하거나, IDE를 이용하는 것도 방법이다.
AutoValue > IDE > 직접 작성
전형적인 equals 메서드의 예 (64쪽)
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
// 나머지 코드는 생략 - hashCode 메서드는 꼭 필요하다(아이템 11)!
}
설명
재정의하지 않아도 되는 상황
equals 메서드는 재정의하기 쉬워 보이지만, 곳곳에 함정이 도사리고 있어서 자칫하면 끔찍한 결과를 초래하기 때문에 아예 재정의하지 않는 것이 가장 좋습니다.
그냥 두면 그 클래스의 인스턴스는 오직 자기 자신과만 같게 됩니다.
다음과 같은 상황 중 하나에 해당한다면 재정의하지 않는 것이 최선입니다.
- 각 인스턴스가 본질적으로 고유하다. 즉, 값을 표현하는게 아니라 동작하는 객체를 표현하는 클래스이다.
- ex) Thread
- Object의 equals 메서드는 이러한 클래스에 딱 맞게 구현되었습니다.
- 인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없다.
- ex) java.util.regex.Patten은 equals를 재정의해서 두 Pattern의 인스턴스가 같은 정규표현식을 나타내는지 검사하는, 즉 논리적 동치성을 검사하는 방법이 있습니다.
- 하지만, 설계자가 애초에 이런 검사가 필요하지 않다고 판단되면 Object의 기본 equals만으로 해결이 됩니다.
- 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
- 예를 들어, 대부분의 Set 구현체는 AbstractSet이 구현한 equals를 상속받아 쓰고, List 구현체들은 AbstractList로부터, Map 구현체들은 AbstractMap으로부터 상속받아 그대로 씁니다.
- 클래스가 private이거나 package-private 이고 equals 메서드를 호출할 일이 없다.
- equals가 실수라도 호출이 되는 걸 막고 싶다면 다음처럼 구현해둡시다.
@Override public boolean equals(Obejct o) {
throw new AssertionError(); // 호출금지!
}
재정의해야 하는 경우: 논리적 동치성을 확인하는 경우
- 객체 식별성(Object identity; 두 객체가 물리적으로 같은가)이 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때입니다.
- 주로 값 클래스들이 여기에 해당합니다. (ex) Integer, String 처럼 값을 표현하는 클래스)
- 두 값 객체를 equals로 비교하는 프로그래머는 객체가 같은지가 아니라 값이 같은지를 알고 싶어하는 것입니다.
- 값 클래스라 해도, 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스라면 equals를 재정의하지 않아도 됩니다. ex) Enum
equals 메서드를 재정의할 때는 반드시 일반 규약을 따라야 합니다.
일반 규약
equals 메서드는 동치관계(equivalence relation)을 구현하며, 다음을 만족한다.
- 반사성(reflexivity)
- Null이 아닌 모든 참조 값 x에 대해, x.equals(x) 는 true다.
- 대칭성(symmetry)
- Null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
- 추이성(transitivity)
- Null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고, y.equals(z)도 true면 x.equals(z)도 true다.
- 일관성(consistency)
- Null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
- null-아님
- Null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
참고 일반 규약을 어긴다면?
프로그램이 이상하게 동작하거나 종료됩니다.
원인이 되는 코드를 찾기도 굉장히 어렵습니다.
존 던(John Done)의 말처럼 세상에 홀로 존재하는 클래스는 없습니다. 한 클래스의 인스턴스는 다른 곳으로 빈번히 전달됩니다. 그리고 컬렉션 클래스들을 포함해 수 많은 클래스는 전달받은 객체가 equals 규약을 지킨다고 가정하고 동작합니다.
동치 관계
쉽게 말해, 집합을 서로 같은 원소들로 이뤄진 부분집합으로 나누는 연산입니다.
이 부분집합을 동치류(equivalence class; 동치 클래스)라 합니다.
equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 합니다.
이런 동치관계를 만족시키기 위한 다섯 요건이 있습니다.
1. 반사성
객체는 자기 자신과 같아야 한다. x.equals(x) = true
이 조건은 어기기가 더 어려워 보입니다.
이 요건을 어긴 클래스의 인스턴스를 컬렉션에 넣은 다음 contains 메서드를 호출하면 방금 넣은 인스턴스가 없다고 답할 것입니다.
2. 대칭성
두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다.
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 this.s.equalsIgnoreCase(((CaseInsensitiveString) o).s); //대소문자를 무시
if (o instanceof String) // 문자열과의 비교 시도
return this.s.equalsIgnoreCase((String) o);
return false;
}
// 문제 시연 (55쪽)
public static void main(String[] args) {
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s); //true
s.equals(cis); //false (대치성 위배), String의 equals는 CaseInsensitiveString을 모른다.
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
list.contains(s); //false (JDK 버전에 따라 다르다.)
//이처럼 equals 규약을 어기면 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.
}
}
해결책
CaseInsensitiveString의 equals를 String과도 연동하겠다는 허황된 꿈을 버려야 합니다.
equals 메서드 (56쪽)
@Override public boolean equals(Object o) {
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(this.s);
}
3. 추이성
첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체가 같아야 한다.
x.equals(y) = true, y.equals(z) = true ⇒ x.equals(z) = true
이 요건도 자칫하면 어기기 쉽습니다.
추이성 위반 : 상위 클래스에 없는 새로운 필드를 하위 클래스에 추가
equals 비교에 영향을 주는 정보를 추가한 것입니다.
Point 클래스
단순한 불변 2차원 정수 점을 표현하는 클래스 (p. 56)
public 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 p.x == x && p.y == y;
}
// 아이템 11 참조
@Override public int hashCode() {
return 31 * x + y;
}
}
Point에 값 컴포넌트(color)를 추가 (56쪽) - 대칭성 위배
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
// 코드 10-2 잘못된 코드 - 대칭성 위배 (57쪽)
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
// 대칭성 위배
p.equals(cp); //true (색상을 무시)
cp.equals(p); //false (매개변수의 클래스 종류가 다름)
}
}
public enum Color { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET }
Point에 값 컴포넌트(color)를 추가 (57쪽) - 대칭성은 지키지만, 추이성 위배
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
// o가 일반 Point면 색상을 무시하고 비교한다.
if (!(o instanceof ColorPoint))
return o.equals(this);
// o가 ColorPoint면 색상까지 비교한다.
return super.equals(o) && ((ColorPoint) o).color == color;
}
public static void main(String[] args) {
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
Point p = new Point(1, 2);
ColorPoint cp2 = new ColorPoint(1, 2, Color.BLUE);
//추이성 위반
cp1.equals(p); //true (색상 무시)
p.equals(cp2); //true (색상 무시)
cp1.equals(cp2); //false; (색상 포함)
}
}
참고 이 방식은 무한 재귀에 빠질 위험도 있습니다.
1. Point를 상속받는 SmellPoint를 만든다.
public class SmellPoint extends Point { private final char smell; public SmellPoint(int x, int y, char smell) { super(x, y); this.smell = smell; } // 코드 10-3 잘못된 코드 - 대칭성은 지키지만, 추이성 위배! (57쪽) @Override public boolean equals(Object o) { if (!(o instanceof Point)) return false; // o가 일반 Point면 smell 속성을 무시하고 비교한다. if (!(o instanceof SmellPoint)) return o.equals(this); // o가 SmellPoint smell까지 비교한다. return super.equals(o) && ((SmellPoint) o).smell == smell; } }
2. colorPoint.equals(smellPoint) 를 호출하면
가 호출되고, 다시 smellPoint의 equals가 호출된다.// o가 일반 Point면 색상을 무시하고 비교한다. if (!(o instanceof ColorPoint)) return o.equals(this);
// o가 일반 Point면 smell 속성을 무시하고 비교한다. if (!(o instanceof SmellPoint)) return o.equals(this);
그럼 다시, colorPoint의 equals가 호출된다.
3. 따라서 StackOverflowError를 일으킨다.
해결책: 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
객체 지향적 추상화의 이점을 포기하지 않는 한 해결책은 존재하지 않습니다.
추상화 이점을 포기. but)리스코프 치환 법칙 위배
@Override public boolean equals(Object o) {
if(o==null || o.getClass() != getClass())
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
- instanceof 대신 getClass()를 사용해 같은 구현 클래스의 객체일 때만 true를 반환합니다.
- 상위 하위 클래스와 상관없이 같은 클래스의 객체가 아니면 false를 반환하므로, 대칭성과 추이성을 모두 지킬 수 있습니다.
- 괜찮아 보이지만 실제로 활용할 수는 없습니다.
- 리스코프 치환 법칙 : "Point의 하위 클래스는 정의상 Point이므로 어디서든 Point로써 활용될 수 있어야 한다."
- 이 방법은 위의 리스코프 치환 법칙을 위반합니다.
리스코프 치환 법칙 위배 증명
Point의 하위 클래스 CounterPoint - 값 컴포넌트를 추가하지 않았다. (p. 59)
public class CounterPoint extends Point {
private static final AtomicInteger counter = new AtomicInteger();
public CounterPoint(int x, int y) {
super(x, y);
counter.incrementAndGet(); //만들어진 인스턴스의 갯수를 생성자에서 센다.
}
public static int numberCreated() { return counter.get(); }
}
CounterPoint를 Point로 사용하는 테스트 프로그램
public class CounterPointTest {
// 단위 원 안의 모든 점을 포함하도록 unitCircle을 초기화한다. (58쪽)
private static final Set<Point> unitCircle = Set.of(
new Point( 1, 0), new Point( 0, 1),
new Point(-1, 0), new Point( 0, -1));
//Point가 단위 원안에 포함되는지 판별하는 메소드
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
public static void main(String[] args) {
Point p1 = new Point(1, 0);
Point p2 = new CounterPoint(1, 0);
onUnitCircle(p1); //true
onUnitCircle(p2); //false (true를 출력해야 하지만, Point의 equals가 getClass를 사용해 작성되어 false를 출력한다.)
}
}
- CounterPoint의 x, y값과는 무관하게, Point클래스의 equals를 getClass를 이용해 작성했다면 onUnitCircle은 false를 리턴합니다.
- 이유는 onUnitCircle에서 사용한 Set을 포함하여 대부분의 컬렉션은 이 작업에 equals 메서드를 사용하는데, CounterPoint 의 인스턴스는 어떠한 Point와도 같을 수 없기 때문입니다.
- 반면에, Point의 equals를 instanceof 기반으로 올바르게 구현했다면, CounterPoint 인스턴스를 건네줘도 onUnitCircle 메서드가 제대로 동작할 것입니다.
- 따라서 해당 코드는 리스코프 치환 법칙인 "Point의 하위 클래스는 정의상 Point이므로 어디서든 Point로써 활용될 수 있어야 한다."에서 CounterPoint는 Point의 하위 클래스이므로 Point로써 활용할 수 있어야 하는데, 활용되지 못하고 있으므로 해당 법칙을 위배합니다.
괜찮은 우회 방법 : 상속 대신 컴포지션을 사용하라
// 코드 10-5 equals 규약을 지키면서 값 추가하기 (60쪽)
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
/**
* 이 ColorPoint의 Point 뷰를 반환한다.
*/
public Point asPoint() {
return point;
}
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
@Override public int hashCode() {
return 31 * point.hashCode() + color.hashCode();
}
}
public static void main(String[] args) throws Exception {
// 대칭성
Point p = new Point(0, 1);
ColorPoint cp = new ColorPoint(0, 1, Color.BLUE);
p.equals(cp); //false
cp.equals(p); //false
//추이성
ColorPoint cp1 = new ColorPoint(1, 2, Color.RED);
Point p = new Point(1, 2);
ColorPoint cp2 = new ColorPoint(1, 2, Color.BLUE);
cp1.equals(p); //false
p.equals(cp2); //false
cp1.equals(cp2); //false
}
자바의 대칭성 위반 예제: Date, Timestamp
자바라이브러리에도 구체 클래스를 확장해 값을 추가한 클래스가 종종 있습니다.
java.sql.Timestamp는 java.util.Date를 확장한 후 nanoseconds 필드를 추가했습니다.
그 결과, Timestamp의 equals는 대칭성을 위반하며, Date 객체와 한 컬렉션에 넣거나 서로 섞어 사용하면 엉뚱하게 동작할 수 있습니다.
그래서, TimeStamp의 API 사용 설명에는 Date와 섞어 쓸 때의 주의사항을 언급하고 있습니다.
둘을 명확히 분리해 사용하는 한 문제될 것은 없지만, 섞이지 않도록 보장해줄 수단은 없습니다.
자칫 실수하면 디버깅하기 어려운 이상한 오류를 경험할 수 있으므로 주의해야 합니다.
참고 추상 클래스와 equals
추상 클래스의 하위 클래서에서라면 equals 규약을 지키면서 값을 추가할 수 있습니다.
아무런 값을 갖지 않는 추상 클래스인 Shape를 위에 두고, 이를 확장하여 radius 필드를 추가한 Circle 클래스와, length와 width 필드를 추가한 Rectangle 클래스를 만들 수 있습니다.
상위 클래스를 직접 인스턴스로 만드는 게 불가능하다면 지금까지 이야기한 문제들은 일어나지 않습니다.
4. 일관성
x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
예) java.net.URL의 equals
- java.net.URL의 equals는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해 비교합니다.
- 호스트 이름을 IP 주소로 바꾸려면 네트워크를 통해야 하는데, 그 결과가 항상 같다고 보장할 수 없습니다.
- 이는 URL이 일반 규약을 어기게 하고, 실무에서도 종종 문제를 일으킵니다.
- 따라서, 이런 문제를 피하려면 equals는 항시 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 합니다.
5. null-아님
x.equals(null)=false
- 실수로 NullPointerException을 던지는 코드를 작성하면 안됩니다.
- 따라서, 다음과 같이 코드를 작성하는 일이 빈번하지만 필요 없는 코드입니다.
명시적 null 검사 - 필요 없다!
@Override public boolean equals(Object o) {
if(o == null)
return false;
...
}
묵시적 null 검사 - 이쪽이 낫다.
@Override public boolean equals(Object o) {
if(!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
...
}
- equals는 건네받은 객체를 적절히 형변환 후 필수 필드들의 값을 알아냅니다.
- equals가 타입을 확인하지 않아 잘못된 타입이 인수로 주어지면, classCastException 을 던져 일반 규약을 위배하게 됩니다.
- 또한, instanceof는 (두 번째 피연산자와 무관하게) 첫 번째 피연산자가 null이면 false를 반환합니다.
- 따라서 타입 확인 단계에서 입력이 null이면 false를 반환하기 때문에 null 검사를 명시적으로 하지 않아도 됩니다.
equals를 재정의하는 단계
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
자기 자신이면 true를 반환해야 한다. - instanceof 연산자로 입력이 올바른 타입인지 확인한다.
그렇지 않으면 false를 반환한다. - 입력을 올바른 타입으로 형변환한다.
앞서 2번에서 instanceof 검사를 했기 때문에 이 단계는 100% 성공합니다. - 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
모든 필드가 일치하면 true를, 하나라도 다르면 false를 반환합니다.
타입 | 비교 연산 |
기본 타입 (float와 Double 제외) | == 연산자 |
참조 타입 | equals 메서드 |
float와 double | Float.compare(float, float) Double.compare(double, double) |
배열 | 원소 각각을 위의 지침대로 비교한다. 배열의 모든 원소가 핵심 필드라면, Arrays.equals 메서드들 중 하나를 사용하자. |
참고 float와 double
float와 dobule을 특별 취급하는 이유는 Float.NaN, -0.0f, 특수한 부동 소수 값 등을 다뤄야 하기 때문입니다.
Float.equals와 Double.equals 메서드를 대신 사용할 수도 있지만, 이 메서드들은 오토박싱을 수반할 수 있으므로 성능상 좋지 않습니다.
테스트
equals를 다 구현했다면, 세 가지만 자문해봅시다.
대칭적인가? 추이성이 있는가? 일관적인가?
물론, 나머지 요건인 반사성과 null-아님도 만족해야 하지만, 이 둘이 문제되는 경우는 별로 없습니다.
자문에서 끝내지 말고 단위 테스트를 작성해봅시다.
(단, equals 메서드를 AutoValue를 이용해 작성했다면, 테스트를 생략해도 안심할 수 있습니다.)
전형적인 equals 메서드의 예 (64쪽)
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
// 나머지 코드는 생략 - hashCode 메서드는 꼭 필요하다(아이템 11)!
}
주의사항
- equals를 재정의할 땐 hashcode도 반드시 재정의하자
- 너무 복잡하게 해결하려 들지 말자
- 필드들의 동치성만 검사해도 equals 규약을 어렵지 않게 지킬 수 있습니다.
- 일반적으로 별칭(alias)은 비교하지 않는게 좋습니다.
- 예를 들어, File 클래스라면 심볼릭 링크를 비교해 같은 파일을 가리키는지를 확인하려 들면 안 됩니다.
(다행히 File 클래스는 이런 시도를 하지 않습니다.)
- Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자.
// 잘못된 예 - 입력 타입은 반드시 Object 여야 한다!
public boolean equals(MyClass o) {
...
}
- 이 메서드는 재정의한 것이 아니라 다중정의한 것입니다.
- 이처럼 '타입을 구체적으로 명시한' equals는 오히려 해가 됩니다.
- 이 메서드는 하위 클래스에서의 @Override 애너테이션이 긍정 오류(false positive; 거짓 양성)을 내게 하고 보안 측면에서도 잘못된 정보를 줍니다.
- 이 문제는 @Override 애너테이션을 일관되게 사용함으로써 실수를 예방할 수 있습니다.
AutoValue 프레임워크
equals(hashCode도 마찬가지)를 작성하고 테스트하는 일은 지루하고 이를 테스트하는 코드도 항상 뻔합니다.
다행히 이 작업을 대신 해줄 오픈 소스가 있으니, 그 친구는 구글이 만든 AutoValue 프레임워크입니다.
클래스에 애너테이션 하나만 추가하면 AutoValue가 이 메서드들을 알아서 작성해주며, 여러분이 작성한 것과 근본적으로 똑같은 코드를 만들어줄 것입니다.
IDE도 같은 기능을 제공하지만, AutoValue만큼 깔끔하거나 읽기 좋지 않습니다.
또한, IDE는 클래스가 수정된 걸 자동으로 알아채지 못해 테스트 코드를 작성해 둬야 합니다.
하지만, 이런 단점을 감안하더라도 사람이 작성하는 것보단 IDE에게 맡기는 게 더 좋습니다.
AutoValue 프레임워크 > IDE > 직접 작성
출처
https://www.yes24.com/Product/Goods/65551284
'책 > effective java' 카테고리의 다른 글
[effective java] item 12. toString을 항상 재정의하라 (0) | 2023.08.01 |
---|---|
[effective java] item 11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2023.07.31 |
[effective java] item 9. try-finally보다는 try-with-resources를 사용하라 (0) | 2023.07.30 |
[effective java] item 8. finalizer와 cleaner 사용을 피하라 (0) | 2023.07.25 |
[effective java] item 7. 다 쓴 객체 참조를 해제하라 (0) | 2023.07.25 |