자바에서는 new를 사용하여 새로운 객체를 생성한다.
동일한 클래스를 여러 개의 객체를 만들어 사용하는 경우가 많겠지만, 클래스의 용도, 특징에 따라 굳이 객체를 여러 개 만들 필요가 없는 클래스들이 있다.
가장 먼저 우리가 자주 사용하는 String이다.
String : new vs literal
String 클래스는 두 가지 생성 방식이 있다.
1. new
String str = new String("camel");
2. literal
String str = "camel";
이 두 가지 방식으로 생성된 String 객체는 기능상으로 다른 점이 있을까? 당연하게도 없다.
하지만 생성된 String 객체가 존재하는 JVM의 위치는 차이가 있다.
new를 사용하여 객체를 만들게 되면 어떤 클래스라도 Heap 영역에 객체가 할당된다. 하지만 literal를 사용할 경우 Heap 영역 안에 다른 영역인 String Constant Pool 영역에 String 객체가 할당된다.
따라서 아래와 같은 결과가 나타난다.
String newStr1 = new String("camel");
String literalStr1 = "camel";
System.out.println(newStr1 == literalStr1); // false
System.out.println(newStr1.equals(literalStr1)); // true
기능상으로는 동일하기 때문에 equals는 true를 반환하지만, 객체의 주소 값을 비교하는 ==은 false를 반환한다.
그럼 아래의 결과는 어떻게 나올까?
String newStr1 = new String("camel");
String newStr2 = new String("camel");
String literalStr1 = "camel";
String literalStr2 = "camel";
System.out.println(newStr1 == newStr2); // false
System.out.println(literalStr1 == literalStr2); // true
new를 사용하면 항상 객체를 생성하므로 newStr1과 newStr2는 서로 다른 객체이다. 따라서 false가 반환된다.
하지만 literal로 선언한 두 값의 비교는 true를 반환하였다. 이유는 무엇일까?
String을 literal로 선언하면 내부적으로 intern() 메서드를 호출하게 된다. intern() 메서드는 주어진 문자열이 String Constant Pool 영역에 존재하는지 먼저 확인하고, 존재한다면 해당 값을 반환하고 없다면 새롭게 생성하기 때문이다.
이처럼 String은 특별한 경우가 아니라면 new를 사용하여 객체를 생성하지 말자.
정적 팩토리 메서드를 제공하는 변경 불가능 클래스(Immutable Class)
잘 짜인 정적 팩토리 메서드(규칙 1)를 제공하는 변경 불가능 클래스는 정적 팩토리 메서드를 호출하면 이미 만들어진 객체를 반환한다.
// Boolean.java
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b){
return b ? TRUE : FALSE;
}
따라서 이런 클래스들은 문서를 잘 읽어보고, new를 사용하여 객체를 생성하지 않아도 되는지 확인하는 것이 좋다.
Boolean newBool = new Boolean(true); // do not
Boolean bool = Boolean.valueOf(true); // do it
변경 가능한 클래스라도 동일한 값을 가진 객체를 계속 사용할 경우
변경 불가능 클래스라면 동일한 값을 가진 객체를 생성할 필요 없이 재사용하면 된다. 하지만 변경 가능한 클래스라면 해당 객체를 다른 영역에서 변경할 수 있으므로, 필요할 때 객체를 자주 생성해서 사용한다.
하지만, 변경되지 않고 해당 영역에서만 사용하고 사라지는 변경 가능한 객체가 있을 수 있다.
아래 예를 보자.
public boolean isBabyBoomer() {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
}
위 코드는 1946년부터 1964년 사이에 태어난 사람인지 확인하는 메서드이다.
위 메서드는 어떠한 파라미터도 받지 않고, 항상 동일한 작업을 한다. 하지만 Date 클래스인 boomStart와 boomEnd에 Date 객체는 메서드가 호출될 때마다 새롭게 생성된다.
또한 Calendar 클래스는 객체를 생성하는데 비용이 꽤 소요되는 클래스인데, 마찬가지로 항상 새롭게 객체를 생성한다.
위 코드는 아래와 같이 변경하면 약 250배 개선된다.
private static final Date BOOM_START;
private static final Date BOOM_END;
/**
* The static initializer block is called only once.
*/
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomerDoThis() {
return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
}
API가 객체를 새롭게 생성하는지, 생성하지 않는지 잘 확인하자
위에서 String의 literal 방식, Boolean의 정적 팩토리 메서드 방식, Calendar의 getInstance(), getTime() 등의 예를 들었다. 이렇게 각각의 API들이 객체를 재사용하는지, 아니면 새롭게 만드는지 분명하지 않을 수 있다. 그렇기 때문에 API의 객체를 생성할 때는 항상 문서를 잘 읽고 개발자가 원하는 방식에 맞게 잘 사용해야 한다.
아래 예를 들어보자.
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("key1", "val1");
Set<String> keyset1 = map.keySet();
map.put("key2", "val2");
Set<String> keyset2 = map.keySet();
System.out.println(keyset1 == keyset2); // true
}
Map의 keySet 메서드는 key값의 목록을 Set 클래스로 반환한다. 위처럼 keyset1을 구하고, 새로운 값을 넣은 후 keyset2를 구했을 때, 일반적으로 생각하기에는 두 값이 서로 다를 것으로 생각한다. 그러나 두 keySet의 값은 동일하다.
그러므로 keySet을 여러 번 호출하여 변수를 여러 개 만들어도 큰일 날 것은 없지만, 쓸데없는 짓이다.
다음으로는 많이 알려진 AutoBoxing(자동 객체화)의 문제(?)이다.
AutoBoxing은 기본 자료형(Premitive Type)과 그에 대응하는 객체 클래스와 섞어 사용할 수 있도록 해주는 기능이다.
아래 예를 들어보자
public static void main(String[] args) {
long start = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
}
Long 클래스인 sum에 기본 자료형인 i의 값을 더하고 있다. 이때 기본 자료형 i는 AutoBoxing이 되어 자동 객체화된다. 즉 loop를 돌 때마다 객체가 계속 생성된다. 그래서 위의 수행 결과는 글쓴이는 9.988초 소요되었다.
위와 같이 짠 개발자가 의도한 게 있는지는 모르겠지만(없을 것이다.) 루프는 기본자료형을 사용하고, 최종적인 결과에서만 Long으로 반환하는 것이 맞을 것이다.
public static void main(String[] args) {
long start = System.currentTimeMillis();
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
}
'Java > [책] 이펙티브 자바' 카테고리의 다른 글
[이펙티브 자바] 규칙7. GC가 호출하는 Object.finalize() (0) | 2021.11.14 |
---|---|
[이펙티브 자바] 규칙6. 메모리 누수 (leak) (0) | 2021.11.14 |
[이펙티브 자바] 규칙4. Util 클래스와 같이 객체 생성이 필요없는 클래스는 private 생성자를 사용 (0) | 2021.10.24 |
[이펙티브 자바] 규칙3. 싱글턴 패턴 (0) | 2021.10.20 |
[이펙티브 자바] 규칙2. 생성자 인자가 많을 때는 Builder 패턴 (0) | 2021.10.10 |