❗️도대체 어떠한 경우에 객체 생성이 불필요 할까❓
먼저 아래 예제를 봅시다.
public class Strings {
public static void main(String[] args) {
String hi = "hi";
String hi2 = new String("hi2");
String hi3 = "hi3";
System.out.println(hi == hi2); // false
System.out.println(hi.equals(hi2)); // hi
System.out.println(hi == hi3); // true
System.out.println(hi.equals(hi3)); // true
}
}
먼저 hi는 문자열 리터럴을 통해서 string을 만들고 있고 hi2는 new 생성자를 통해 string을 만들고 있습니다.
이 둘의 차이는 무엇일까요???
문자열 리터럴로 생성하면 해당 String 값은 Heap 영역 내 String Constant Poll에 저장되어 재사용되지만, New 연산자로 생성하면 같은 내용이라도 여러 개의 객체가 각각 Heap 영열을 차지합니다.
조금 더 이해하기 쉽게 그림으로 보겠습니다.
먼저 문자열 리터럴을 사용하는 방법입니다.
String str1 = "Hello";
위 그림처럼 str1은 문자열 리터럴로 생성해서 Heap 영역 내 String Constant poll에 저장됩니다.
❗️ 여기서 똑같이 "Hello"를 한 개 더 만들면 어떻게 될까요?
String str1 = "Hello";
String str2 = "Hello";
두 개의 문자열 객체를 만들었지마 Hello 값의 다른 인스턴스는 힙에 생성되지 않습니다. 대신 Hello의 이전 인스턴스가 재사용됩니다. String constant pool은 힙 내에 있는 작은 캐시입니다. Java는 직접 할당 시 String constant poll 내부에 모든 값을 저장합니다. 이렇게 하면 유사한 값에 다시 접근해야 하는 경우 스택에 생성된 새 문자열 객체가 포인터의 도움을 받아 이를 직접 참조할 수 있습니다. 즉, 문자열 상수 풀은 주로 메모리 사용량을 줄이고 메모리 내 기존 인스턴스의 재사용을 향상시키기 위해 존재합니다.
❗️ 만약 여기서 문자열 객체에 "Hello"가 아닌 값이 할당되면 어떻게 될까요?
String str1 = "Hello";
String str2 = "Hello";
String str3 = "Class";
만약 이렇게 메모리 할당을 하고 싶지 않다면 방법이 있습니다.
바로 new키워드를 사용하는 것 입니다.
new를 사용하면 문자열 상수 풀 외부의 힙에 인스턴스가 강제로 생성됩니다. 여기서는 인스턴스 캐싱 및 재사용이 허용되지 않기 때문입니다.
String str1 = new String("John");
String str2 = new String("Doe");
이와 같이 new키워드를 사용하면 힙에 중복 상관하지 않고 계속 생성되기 때문에 메모리가 낭비될 수 있습니다.
만드는데 메모리나 시간이 오래 걸리는 객체 즉 "비싼 객체"를 반복적으로 만들어야 한다면 캐싱을 고려해보면 좋습니다.
아래 예제는 String.matches() 사용합니다.
matches() 메서드는 정규표현식 패턴을 인자로 받습니다. 그리고 내부적으로 받은 패턴을 컴파일 한 후 패턴 인스턴스를 만듭니다.
문제는 이 과정이 비용도 높고 오래걸리는 과정입니다.
public class RomanNumerals {
// 방법 1
static boolean isRomanNumeralSlow(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
public static void main(String[] args) {
boolean result = false;
long start = System.nanoTime();
for (int j = 0; j < 100; j++) {
result = isRomanNumeralSlow("MCMLXXVI");
}
long end = System.nanoTime();
System.out.println(end - start);
System.out.println(result);
}
}
for 문을 순회할 때마다 isRomanNumeralSlow()를 호출하기 때문에 앞전에 설명한 패턴 인스턴스가 지속적으로 생깁니다.
이에 따라 메모리 낭비 뿐만아니라 처리시간도 길어집니다.
이때 불편인 패턴 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드가 호출될 때마다 이 인스턴스를 재사용한다면 메모리 낭비도 없고 처리 속도도 훨씬 빠릅니다. 예제는 아래와 같습니다.
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeralFast(String s) {
return ROMAN.matcher(s).matches();
}
public static void main(String[] args) {
boolean result = false;
long start = System.nanoTime();
for (int j = 0; j < 100; j++) {
result = isRomanNumeralFast("MCMLXXVI");
}
long end = System.nanoTime();
System.out.println(end - start);
System.out.println(result);
}
}
'정보' 카테고리의 다른 글
[Effective java] Item 11 equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2024.03.24 |
---|---|
🎯 Item 7 - 다 쓴 객체 참조를 해제하라 (0) | 2024.03.17 |
[Effective java] Item 5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2024.03.03 |
[Effective java] Item 4 인스턴스화를 막을려거든 private 생성자를 사용하라 (0) | 2024.02.25 |
[Effective java] Item 1 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2024.02.18 |