3. String 클래스
Last updated
Last updated
자바에서 문자를 다루는 대표적인 타입은 char
, String
2가지가 있다.
기본형인 char
는 문자 하나를 다룰 때 사용한다. char
를 사용해서 여러 문자를 나열하려면 char[]
을 사용해야 한다. 하지만 이렇게 char[]
을 직접 다루는 방법은 매우 불편하기 때문에, 자바는 문자열을 매우 편리하게 다룰 수 있는 String
클래스를 제공한다.
String
클래스를 통해 문자열을 생성하는 방법은 2가지가 있다.
리터럴 사용 : "hello"
객체 생성 : new String("hello")
기본적으로 String
은 클래스이다. 기본형 타입(Primitive type) 이 아니다.
따라서 String
을 리터럴로 사용하는 다음의 코드는 뭔가 어색하다.
문자열은 매우 자주 사용된다. 때문에 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello")
와 같이 변경해준다. (이 경우 실제로는 성능 최적화를 위해서 문자열 풀을 사용한다)
📌 문자열 리터럴의 시점 별 구체적인 동작 흐름1. 컴파일 결과 (
.class
)
Constant Pool에
"hello"
라는CONSTANT_String_info
항목 추가됨바이트코드엔
ldc #2
같은 식으로"hello"
의 인덱스를 참조함
2. 클래스 로딩 시점
JVM은
.class
에서 Constant Pool을 읽어 Runtime Constant Pool 생성 (이때도"hello"
는String
객체가 아닌, UTF-8 바이트만 있는 심볼 상태)
3. 실행 시점 (ldc #2
)
JVM이 RCP의 2번 인덱스를 참조
"hello"
라는 심볼릭 문자열 리터럴을 intern pool에 조회
없으면
new String("hello")
수행 후 intern pool에 등록있으면 intern pool에 등록된 Heap 객체를 사용
📌 String Constant Pool (Intern Pool)
intern pool은 Java의 Heap 영역에 존재하며, 동일한 문자열 리터럴이 여러 번 생성되는 것을 방지합니다.
"hello"
처럼 문자열 리터럴은 자동으로 intern 처리되지만,new String("hello")
와 같이 명시적으로 객체를 생성한 경우에는 intern pool에 등록되지 않습니다.
Java 6까지는 intern pool이 PermGen에 존재했지만, Java 7부터는 Heap으로 이동하여 메모리 유연성이 향상되었습니다.
참고 :
"hello" == "hello"
는true
인데,new String("hello") == "hello"
는 왜false
일까?
실제로 Java 7 부터 문자열 리터럴과 문제열 객체는 힙 공간에서 관리된다. (때문에, 같은 공간에 저장되는 것으로 생각할 수 있다)
하지만, 문자열 리터럴의 경우 JVM 이 Intern Pool 이라는 논리적인(!) 공간에서 관리한다.
때문에, 문자열 리터럴의 경우
==
(동일성 비교) 를 해도 Intern Pool 에서 캐시된 내용을 비교하기 떄문에,true
가 나온다.
String
클래스 구조는 대략 다음과 같이 생겼다.
여기에는 String
의 실제 문자열 값이 보관된다. 문자 데이터 자체는 char[]
에 보관된다.
String
클래스는 개발자가 직접 다루기 불편한 char[]
을 내부에 감추고 String
클래스를 사용하는 개발자가
편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공한다. 그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공한다.
참고 - 자바 9 이후String
클래스 변경 사항자바 9 부터
String
클래스에서char[]
대신에byte[]
를 사용한다.자바에서 문자 하나를 표현하는
char
는 2byte 를 사용한다. 그런데 영어, 숫자는 보통 1byte 로 표현이 가능하다. 그래서 단순 영어, 숫자로만 표현된 경우 1byte 인 Latin-1 인코딩을 사용하고 그렇지 않은 경우 2byte 인 UTF-16 인코딩을 사용한다. 따라서 메모리를 더욱더 효율적으로 사용할 수 있도록 변경되었다. (객체 생성 시점에 해당 인코딩 타입을 체크한다)예를 들어서
"가나다라"
문자열을String
타입으로 저장하면 크기가 8인byte
배열에 저장된다.
너무 많아서 패스..
필요할 때 알아서 찾아쓰자;;
String
은 클래스이다. 따라서 기본형이 아니라 참조형이다.
참조형은 변수에 계산할 수 있는 값이 들어있는 것이 아니라, 레퍼런스 값이 들어있다. 따라서 원칙적으로는 +
같은 연산자를 사용할 수 없다.
자바에서 문자열을 더할 때는 String
이 제공하는 concat()
과 같은 메서드를 사용해야 한다.
하지만, 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 +
연산을 제공한다.
C++ 은 연산자 오버로딩이 존재하기 때문에, + 연산이 가능하도록 할 수 있지만 자바는 아니다..
String
클래스 비교할 때는 ==
비교가 아니라 항상 equals()
비교를 해야한다.
동일성(Identity) : ==
연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
동등성(Equality) : equals()
메서드를 사용하여 두 객체가 논리적으로 같은지 확인
str1
, str2
는 기본적으로 레퍼런스 값이 다르기 때문에, 동일성 비교를 했을 때 false
를 반환한다.
String
클래스는 내부 문자열 값을 비교하도록 equals()
메서드를 재정의 해두었다.
String str3 = "hello"
와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해서 문자열 풀을 사용한다.
자바가 실행되는 시점(런타임) 에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String
인스턴스를 만들어둔다. 이때 같은 문자열이 있으면 만들지 않는다.
String str3 = "hello"
와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello"
라는 문자를 가진 String
인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조 (x003) 을 반환한다.
String str4 = "hello"
의 경우 "hello"
문자열 리터럴을 사용하므로 문자열 풀에서 str3
과 같은 x003 참조를 사용한다.
문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에, 성능도 최적화 할 수 있다.
참고로 문자열 풀은 힙 영역을 사용한다. 그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에, 매우 빠른 속도로 원하는 String
인스턴스를 찾을 수 있다.
따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로, ==
(동일성) 비교에 성공한다.
그렇다면 문자열 리터럴을 사용하면 ==
비교를 하고, new String()
을 직접 사용하는 경우에만 equals()
비교를 사용하면 되지 않을까?
정답은 아니다..
개발을 하다보면 협업을 하게 되는데, 다른 개발자가 내 메서드를 사용할 때 메서드에서 문자열 리터럴을 사용하는지 new String()
을 사용하는지는 구분할 수 없다..
때문에! 안전하게 equals()
를 사용하는 것이 좋다!
String
은 불변 객체이다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고 새로운 결과를 만들어서 반환한다.
String.concat()
은 내부에서 새로운 String
객체를 만들어서 반환한다.
따라서 불변과 기존 객체의 값을 유지한다.
String
이 불변으로 설계된 이유는 앞서 불변 객체에서 배운 내용에 추가로 다음과 같은 이유도 있다.
String
은 리터럴의 겨우 자바 내부에서 문자열 풀을 통해서 최적화한다.
만약 String
내부의 값을 변경할 수 있다면(불변이 아니라면), 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제가 발생한다.
위 그림의 경우 str3
이 참조하는 문자를 변경하면 str4
의 문자도 함께 변경되는 사이트 이펙트 문제가 발생한다.
하지만, String
클래스는 불변으로 이러한 사이드 이펙트 문제를 고려하지 않아도 된다.
불변인 String
클래스에도 단점이 있다. 아래 예를 보면 두 문자를 더하는 경우 다음과 같이 작동한다.
(실제 동작하는 코드는아니다)
더 많은 문자를 더하는 경우를 생각해보자.
이 경우 총 3개의 String
클래스가 추가로 생성된다.
하지만, 중간에 더하기 위해서 만들어진 String 클래스는 생성 되었다가 바로 GC 에 의해서 버려지고 최종적으로 new String("ABCD")
만 사용된다.
불변인 String
클래스의 단점은 문자를 더하거나 변경할 때 마다 새로운 객체를 생성해야 한다는 점이다. 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String
객체를 만들고, GC 를 해야 한다. 결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 된다. 그리고 문자열 크기가 클수록 문자열을 더 자주 변경할수록 시스템 자원을 많이 소모하게 된다.
이 문제를 해결하는 방법은 단순하다. 바로 불변이 아닌 가변 String
이 존재하면 된다. 가변은 내부의 값을 바로 변경하면 되기 때문에, 새로운 객체를 생성할 필요가 없다. 따라서 성능과 메모리 사용면에서 불변보다 효율적이다.
이런 문제를 해결하기 위해 자바는 StringBuilder
라는 가변 String
을 제공한다. 물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 한다.
StringBuilder
는 내부에 final
이 아닌 변경할 수 있는 byte[]
을 가지고 있다.
String
은 불변하다. 즉, 한번 생성되면 그 내용을 변경할 수 없다. 따라서 문자열에 변화를 주려고 할 때마다 새로운 String
객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다.
반면에 StringBuilder
는 가변적이다. 하나의 StringBuilder
객체 안에서 문자열을 추가, 삭제 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있다.
단, 사이드 이펙트를 주의해야 한다.
StringBuilder
는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String
으로 변환하는 것이 좋다.
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐준다.
컴파일 전
컴파일 후
따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에, 성능이 향상된다.
문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에, 단순하게 합칠 수 없다.
이런 경우 다음과 같이 최적화를 수행한다.(최적화 방식은 자바 버전에 따라서 달라진다)
이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder
를 사용하지 않아도 된다. 대신 문자열 더하기 연산(+) 을 사용하면 충분하다.
다음과 같이 문자열을 루프 안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않는다.
왜냐하면 대략 다음과 같이 최적화가 되기 때문이다.(최적화 방식은 자바 버전에 따라서 다르다)
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 한다. 반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정된다. 이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없다. 따라서, 이런 상황에서는 최적화가 어렵다.
StringBuilder
는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String
객체를 생성했을 것이다.
이럴 때는 직접 StringBuilder
를 사용해주면 된다.
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때