String 클래스도 당연히 클래스이므로 다른 클래스들의 특징을 모두 지니고 있다. 하지만 워낙 자주 사용되는 클래스이다 보니 다른 클래스에는 없는 2개의 특징이 있다. 첫 번째 특징은 한 번 정의된 문자열은 변경할 수 없다는 것이다. 만일 문자열의 내용을 변경하면 자바 가상 머신은 기존의 문자열을 수정하는 것이 아니라 새로운 문자열을 포함하고 있는 객체를 생성해 사용하고 기존의 객체는 버린다. 두 번째 특징은 문자열 리터럴을 바로 입력해 객체를 생성할 때 같은 문자열끼리 객체를 공유한다는 것이다. 이는 메모리의 효율성 때문이다. 그럼 각각의 특징을 자세히 살펴보자.
특징 1. 객체 안의 값을 변경하면 새로운 객체를 생성
String 객체는 내부에 포함된 문자열을 변경할 수 없다. 다음 예를 살펴 보자.
String str1 = new String("안녕");
String str2 = str1;
str1 = "안녕하세요";
System.out.println(str1); // 안녕하세요
System.out.println(str2); // 안녕
먼저 코드를 String str1 = new String("안녕")과 같은 형태로 작성하면 메모리의 클래스 영역에는 String 클래스의 바이트 코드가 로딩되고 스택에는 참조 변수 str1의 공간이 생긴다. 실제 데이터인 "안녕"은 힙 메모리에 생성되며, 생성된 실제 데이터의 위칫값이 스택 메모리의 str1 공간에 저장될 것이다. 이 상황에서 String str2 = str1, 즉 참조 자료형의 값을 복사하면 스택 메모리의 값이 복사되므로 str1과 str2 는 이제 모두 동일한 객체를 가리키고 있을 것이다. 이제 str1 = "안녕하세요"와 같이 수정하면 자바 가상 머신은 기존의 문자열을 수정하는 것이 아닌 "안녕하세요"라는 문자열을 포함하고있는 새로운 String 객체를 생성하고 이 위치를 str1의 공간에 저장한다. 이는 일반적으로 참조 자료형에서 2개의 참조 변수가 1개의 객체를 가리킬 때 하나의 참조 변수에 접근해 객체의 값을 변경하면 다른 참조 변수가 가리키는 값도 함께 변하는 것과느니 구분되는 특징이다. 이상의 과정을 메모리 구조로 살펴보면 다음과 같다.
+) String 클래스의 특징과 배열의 특징 비교
값을 변경할 때 새로운 객체를 생성하는 String 클래스의 특징을 앞에서 배운 배열의 특징과 비교해보자. 배열은 객체의 값 자체가 수정되므로 참조 변수 복사 이후 하나의 변수에서 수정하면 나머지 변수에도 적용된다.
int[] array1 = new int[] {3, 4, 5};
int[] array3 = array1;
array1[0] = 6; array1[1] = 7; array1[2] = 8;
System.out.println(Arrays.toString(array2)); // [6, 7, 8]
System.out.println(Arrays.toString(array1)); // [6, 7, 8]
// 문자열 수정
String str1 = new String("안녕");
String str2 = str1;
str1 = "안녕하세요"; // 새로운 객체 생성
System.out.println(str1);
System.out.println(str2);
// 배열 참조 자료형
int[] array1 = new int[] {3, 4, 5};
int[] array2 = array1;
array1[0] = 6;
array1[1] = 7;
array1[2] = 8;
System.out.println(Arrays.toString(array1));
System.out.println(Arrays.toString(array2));
결과
특징 2. 리터럴을 바로 입력한 데이터는 문자열이 같을 때 하나의 객체를 공유
두 번째 방법인 문자열 리터럴만 입력해 String 객체를 생성하면 하나의 문자열을 여러 객체가 공유할 수 있다. 이는 다른 클래스에 없는 특징으로 특정 문자열의 객체를 여러 개 만들어 사용할 때 메모리 효율성을 증가시키기 위한 것이다. 다음처럼 4개의 String 객체를 생성해 보자.
String str1 = new String("안녕");
String str2 = "안녕";
String str3 = "안녕";
String str4 = new String("안녕");
모두 동일한 문자열을 포함하고 있으며 첫 번째와 네 번째는 new 키워드 나머지 2개는 문자열 리터럴을 사용했다. 이제 메모리를 살펴 보자.
String str1 = new String("안녕")이 실행되면 힙 메모리에는 "안녕"이라는 객체 하나가 생성될 것이다. 두 번째는 문자열 리터럴로 입력했는데, 이때는 힙 메모리에 이미 "안녕"이 있어도 새롭게 안녕이라는 개체를 추가한다. 세 번째도 문자열 리터럴로 생성했는데 이때 이미 앞에서 문자열 리터럴로 생성한 "안녕"이라는 객체가 있으므로 새롭게 객체를 생성하는 것이 아니라 기존에 있는 이 객체를 공유한다. 마지막에는 다시 new로 객체를 생성했으며 이때는 새롭게 객체를 생성한다. 정리하면 new로 생성할 때는 동일한 문자열 객체가 힙 메모리에 있든 없든 무조건 새롭게 객체를 생성한다. 문자열 리터럴로 생성할 때는 힙 메모리에 리터럴로 생성된 동일 문자열을 포함하고 있는 객체가 있으면 그 객체를 공유한다.
String str1 = new String("안녕");
String str2 = "안녕";
String str3 = "안녕"; // str2가 가리키는 객체를 공유
String str4 = new String("안녕"); // 새 객체 생성
// 스택 메모리 값 비교
System.out.println(str1 == str2);
System.out.println(str2 == str3);
System.out.println(str3 == str4);
System.out.println(str4 == str1);
결과