이전 게시글에서 String은 immutable한 객체로 immutable한 객체를 다른 변수에 할당하는 경우에 Heap 메모리에 존재하는 값을 공유하는 것이 아닌 복사하여 복사한 값을 가리킨다고 설명했다. 이런 immutable한 String 객체에 대해서 좀 더 자세히 살펴보자. 그리고 아래는 이전 게시글 링크이다.
String Literal vs new String
public class StringTest {
public static void main(String[] args) {
StringTest stringTest = new StringTest();
stringTest.testStringLiteral();
stringTest.testNewString();
}
public void testStringLiteral() {
String first = "oneny";
String second = "oneny";
System.out.println(first == second);
}
public void testNewString() {
String third = new String("oneny");
String fourth = new String("oneny");
System.out.println(third == fourth);
}
}
위 코드를 실행시키면 어떤 결과가 나올까? 모두 true가 나올 것 같다고 생각했던 것과는 달리 testStringLiteral 메서드를 실행하면 true가 출력되고, testNewString 메서드를 실행하면 false를 출력하는 것을 확인할 수 있다. 같은 String 타입이라도 왜 이렇게 결과가 다르게 나오는지 자세히 살펴보자.
String
String 클래스의 가장 큰 특징은 String 값들은 변경불가능(immutable)하다는 점이다. 모든 문자열 리터럴은 String 클래스의 인스턴스와 같다. 따라서 한 번 생성된 문자열은 변경할 수 없다. 만약 문자열 타입의 변수에 다른 문자열 객체(String Object)와 결합하려는 경우 기존의 문자열 값이 변경되는 것이 아니라, 기존의 문자열 값은 그대로 두고 결합하려는 다른 문자열 객체와 연산을 통해 결합하여 새로 생성하여 할당한다. 즉, 문자열을 결합하는 것은 매 연산 시마다 새로운 문자열을 가진 String 인스턴스를 생성하여 메모리 공간을 차지하는 결과를 낳는다. 이러한 String 객체를 초기화하는 방법에는 두 가지가 존재하며, 이는 Java의 문자열 풀(String Pool)과 관련이 있다.
- String Literal을 통해 초기화
- new String으로 초기화
new String
new String으로 문자열을 초기화하는 경우에는 String Literal과 달리 Heap 메모리에 항상 새로운 String 객체를 생성한다. 따라서 값은 값의 문자열이더라도 메모리 주소를 다르게 가리킨다. 그래서 위에서 살펴본 예제 코드에서 testNewString 메서드가 실행되면 false가 출력되는 것이다.
String Literal
문자열 리터럴을 지정하는 경우 String객체는 Heap 메모리 영역 내 String Pool 영역에 생성된다. 이 객체는 상수이므로 수정할 수 없으며, 즉 불변(immutable)이다. 하지만 new String으로 String 객체를 생성하는 것과 달리 String Literal을 통해 초기화하는 경우에는 기존에 존재하던 값을 재사용한다는 특징이 있다. 즉, Heap Memory 내 String Pool에 캐싱된 객체를 공유하기 때문에 위 예제 코드에서 testStringLiteral 메서드 실행 시 true가 출력되는 것이다.
String Pool
그러면 String Pool이라는 것은 무엇일까? 위에서 살펴본 것처럼 String Pool은 Java의 Heap Memory 내에 문자열 리터럴을 저장하는 공간이다. 이 공간이 존재하는 이유는 한번 생성된 문자열 리터럴은 변경될 수 없기 때문이다. 따라서 효율적인 메모리 사용 및 매번 문자열을 생성하는데 들어갈 시간을 절약하기 위해 String Pool을 만들어두고, 한번이라도 생성된 문자열 리터럴은 따로 저장해두는 것이다.
즉, 리터럴 문자열은 String Pool에 같은 값이 있는지 찾아본 후 있으면 그 참조값이 반환되고 없을 경우 String Pool에 문자열이 등록된 후 해당 참조값이 반환되는 것이다. 이 String Pool 덕분에 String Pool 내에 String 객체를 생성하는 것이 좀 더 걸릴지 몰라도 많은 공간을 절약할 수 있다. 참고로 문자열 리터럴은 클래스가 메모리에 로드될 때 자동적으로 미리 생성된다.
String Literal과 new String 성능 비교
public class ComparePerformance {
public static void main(String[] args) {
long start1 = System.currentTimeMillis();
for (int i = 0; i < 100_000_000; i++) {
String s1 = "oeny";
String s2 = "twony";
}
long end1 = System.currentTimeMillis();
long totalTime1 = end1 - start1;
System.out.println("Time taken to execute" + " string literal = " + totalTime1);
long start2 = System.currentTimeMillis();
for (int i = 0; i < 100_000_000; i++) {
String s3 = new String("oneny");
String s4 = new String("twony");
}
long end2 = System.currentTimeMillis();
long totalTime2 = end2 - start2;
System.out.println("Time taken to execute" + " string object = " + totalTime2);
}
}
매번 새로운 String 객체를 생성하는 new String과 String Pool 내에서 먼저 찾아 해당 값이 있으면 그 참조값을 반환되는 String Literal에서 성능 차이가 있는 것을 확인할 수 있다.
주소값 비교(==) 와 값 비교(equals) 비교 차이
== 연산자와 equals 메서드의 차이점은 == 연산자는 비교하고자 하는 두 개의 대상의 주소값을 비교하는데, String 클래스의 equals 메서드는 비교하고자 하는 두 개의 대상의 값 자체를 비교한다는 점이다.
equals가 생긴 이유가 무엇일까? 프로그래밍을 하다보면 동일한 객체가 메모리 상에 여러 개 띄워져있는 경우가 있다. 해당 객체는 서로 다른 메모리에 띄워져있으므로 동일한(Identity) 객체가 아니다. 하지만 프로그래밍 상으로는 같은 값을 지니므로 같은 객체로 인식되어야 하는데, 이러한 동등성(Equality)를 위해 우리는 값으로 객체를 비교하도록 equals 메서드를 오버라이딩해주는 것이다.
String s1 = new String("oneny");
String s2 = new String("oneny");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
따라서 우리가 살펴본 s1과 s2는 값을 같지만 Heap 메모리 영역에서 서로 다른 참조값을 가리키고 있으므로 == 연산자로 비교하는 경우에는 false가 되고, equals 메서드를 사용하여 비교하는 경우에는 값으로 객체를 비교하기 때문에 true가 된다.
equals를 사용하는 메서드를 들여보면 위 코드처럼 되어있다. equals 메서드는 모든 객체의 부모 클래스인 Object에 정의되어 있는 메서드이다. String 클래스는 위와 같이 equals 메서드를 오버라이딩하여 인자로 전달된 String을 주소값이 아닌 데이터값으로 비교한다. 따라서 String을 어떻게 생성하느냐에 따라 결과가 달라지지 않고 정확한 비교를 할 수 있다.
출처
How to Initialize and Compare Strings in Java?
String Literal Vs String Object in Java
'Java > Java' 카테고리의 다른 글
ArrayList와 LinkedList 차이 (0) | 2023.07.15 |
---|---|
Java의 hashCode, equals와 hashCode 같이 써야하는 이유 (0) | 2023.07.10 |
JVM의 Stack&Heap 이해하기 (0) | 2023.07.08 |
정적 팩토리 메서드, 인스턴스 캐싱 (0) | 2023.04.30 |
함수형 인터페이스와 람다식 + 전략패턴 (0) | 2023.04.19 |