같은 기능을 하는 객체는 하나를 재사용하는 방법을 고려해야 한다.
Wrapper Class
자바의 wrapper class 인 Byte, Short, Integer 등 이있다.
primitive type 을 wrapper class로 변환할때, new와 valueOf를 통한 인스턴스 생성이 있는데,
팩터리 메서드인 valueOf를 이용하게 되면 캐싱을 이용하여 불필요한 객체 생성을 막고 있다.
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
한가지 유의할 점은 불변 객체가 아닌 가변 객체라면 변경되지 않을 것임을 보장해야 한다.
정규표현식
정규표현식을 이용하여 주로 이메일 등 정해진 패턴에 일치하는지 유효성 검증에 많이 이용된다.
compileOnCall 메서드는 호출시 matches를 이용하여 Pattern.matches를 호출하는 방식이다.
public class Regexp {
public void compileOnCall(String email) {
long startTime = System.nanoTime();
for(int i = 0; i < 100; i++) {
email.matches("^([\\w\\.\\_\\-])*[a-zA-Z0-9]+([\\w\\.\\_\\-])*([a-zA-Z0-9])+([\\w\\.\\_\\-])+@([a-zA-Z0-9]+\\.)+[a-zA-Z0-9]{2,8}$");
}
System.out.println("Compile On Call >> " + (System.nanoTime() - startTime));
}
// String.matches 호출시 Pattern.matches 호출
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
}
compileBeforeCall은 String.mathes를 호출시 호출되는 Pattern 인스턴스 초기화 과정을
미리 캐시를 해두어 확인하는 방식이다.
public class Regexp {
private final Pattern emailPattern = Pattern.compile("^([\\w\\.\\_\\-])*[a-zA-Z0-9]+([\\w\\.\\_\\-])*([a-zA-Z0-9])+([\\w\\.\\_\\-])+@([a-zA-Z0-9]+\\.)+[a-zA-Z0-9]{2,8}$");
public void compileBeforeCall(String email) {
long startTime = System.nanoTime();
for(int i = 0; i < 100; i++) {
emailPattern.matcher(email).matches();
}
System.out.println("Compile Before Call >> " + (System.nanoTime() - startTime));
}
}
아래는 두가지 경우를 100회 호출하였을때 성능 비교 지표이다.
public class Main {
public static void main(String[] args) {
Regexp regexp = new Regexp();
// 매번 정규식 컴파일
regexp.compileOnCall("yim3370@gmail.com");
// 정규식 초기화 공유
regexp.compileBeforeCall("yim3370@gmail.com");
}
}
지연초기화
불필요한 초기화를 방지하기 위해 필요한 순간에 초기화를 진행하는 지연초기화 방식이 존재한다.
간단한 지연초기화 방식을 구현한 예제이다.
public class ClassA {
private ClassB classB;
public ClassA() {
this.classB = new ClassB();
}
}
public class LazyInitializationA {
private ClassB classB;
public ClassB getLazyInitializationB() {
if(classB == null) {
this.classB = new ClassB();
}
return classB;
}
}
ClassA는 생성자 호출시 ClassB를 생성자 호출한다.
LazyInitializationA는 getLazyInitializationB 메서드 호출시 ClassB의 초기화 여부를 판단하여 동작한다.
아래는 두가지 방식의 메모리 사용량 비교이다.
public class Main {
public static void main(String[] args) {
// 초기화 당시 생성
Runtime.getRuntime().gc();
for (int i = 0; i < 100000; i++) {
new ClassA();
}
long used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("초기화 당시 생성할 경우 메모리 사용량 >> " + used);
// 지연 초기화
Runtime.getRuntime().gc();
for (int i = 0; i < 100000; i++) {
new LazyInitializationA();
}
used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("지연 초기화를 이용하여 호출시 생성할 경우 메모리 사용량 >> " + used);
}
}
Adpter Pattern
마지막으로 어뎁터 패턴을 이용한 불필요한 객체 생성을 방지하는 방법이다.
Map interface의 keyset 메서드는 새로운 객체를 생성하는 것이 아닌 매번 같은 객체를 반환한다.
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
아래는 예제는 keyset 메서드를 통해 반환된 Set<K>이 동일한 객체인지 비교하는 예제이다.
public class Main {
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
Set<String> setAdapterA = map.keySet();
Set<String> setAdapterB = map.keySet();
System.out.println(setAdapterA == setAdapterB);
System.out.println(setAdapterA.hashCode() == setAdapterB.hashCode());
System.out.println(setAdapterA.equals(setAdapterB));
}
}
Auto Boxing
명시적인 형변환이 아닌 묵시적으로 큰 타입으로 자동 형변환이 되는 등의 경우 성능에 영향을 미친다.
Wrapper, AutoBoxing, 동일 타입 연산에 대한 성능 비교 예제이다.
public class Primitive {
private long longValue = 0L;
public void sumLongValue() {
long startTime = System.nanoTime();
for(int i = 0; i < 100000; i++) {
longValue++;
}
System.out.println("Primitive type add int >> " + (System.nanoTime() - startTime));
startTime = System.nanoTime();
for(long i = 0; i < 100000; i++) {
longValue += i;
}
System.out.println("Primitive type add long >> " + (System.nanoTime() - startTime));
}
}
public class Wrapper {
private Long longValue = 0L;
public void sumLongValue() {
Long startTime = System.nanoTime();
for(int i = 0; i < 100000; i++) {
longValue += i;
}
System.out.println("Wrapper Class Long >> " + (System.nanoTime() - startTime));
}
}
항상 같은 객체를 재사용하는 것이 장점 만을 가지는 것이 아니다.
같은 객체를 재사용할 때에는 참조를 유의하여야하며, 변화가 되어야 할 경우 새로운 객체를 생성하여 사용해자.
'Effective Java > 정리' 카테고리의 다른 글
[Item 8] finalizer와 cleaner 사용을 피하라 (0) | 2023.01.17 |
---|---|
[Item 7] 다 쓴 객체 참조를 해제하라 (0) | 2023.01.17 |
[Item 3] private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2023.01.07 |
[Item 2] 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2023.01.06 |
[Item 1] 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2023.01.04 |