기존의 다른 언어에서는 문자열을 char형의 배열로 다루었으나 자바에서는 문자열을 위한 클래스를 제공한다. char문자열을 저장하고 해당 문자열을 활용 할 메서드를 같이 제공하는 클래스이다.
변경 불가능한(immutable)클래스
String 클래스에는 문자열을 저장하기 위해서 문자열 배열 참조변수(car[]) value를 인스턴스 변수로 정의해 놓고 있다. 인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 인스턴스변수(value)에 문자형 배열(cahr[])로 저장되는 것이다.
따라서 한번 생성된 String인스턴스가 갖고 있는 문자열은 읽어올수만 있으며 변경 불가능 하다.
그럼 아래의 코드는 불가능한 코드인 것일까?
String a = "a";
String b = "b";
a = a + b;
실행하는데 문제가 없을것이다. 하지만 이는 우리가 모른 사이에 새로운 문자열"ab"를 담은 String a 인스턴스가 생성되는 것이다. 이를 알아보려면 System.identityHashcode()를 사용 하면 되는데 아래와 같은 결과를 보여준다.
public class Test {
public static void main(String[] args) {
String a = "a";
String b = "b";
System.out.println(System.identityHashCode(a));
a = a + b;
System.out.println(System.identityHashCode(a));
}
}
결과
1435804085
1121172875
분명 identityHashCode()는 object의 hashCode()와 비슷하게 주소를 이용하여 해시를 생성하는데 같은 String a더라도 결합 후의 a주소가 바뀐것을 볼 수 있다.
이처럼 덧셈연산자'+'를 사용해서 문자열을 결합하는 것은 매 연산 시 마다 새로운 문자열을 가진 String인스턴스가 생성되어 메모리 공간을 차지하게 되므로 가능한 결합횟수를 줄이는 것이 좋다.
문자열 간의 결합이나 추출 등 문자열을 다루는 작업이 많이 필요한 경우 StringBuffer클래스를 사용하는 것이 최적화에 좋다. StringBuffer인스턴스에 저장된 문자열은 변경이 가능하므로 하나의 StringBuffer인스턴스만으로도 문자열을 다루는 것이 가능하다.
문자열을 만들 때는 두 가지 방법, 문자열 리터럴을 지정하는 방법과 String클래스의 생성자를 사용해서 만드는 방법이 있다.
String str1 = "abc"; // 문자열 리터럴 "abc"의 주소가 str1에 저장됨
String str2 = "abc"; // 문자열 리터럴 "abc"의 주소가 str2에 저장됨
String str3 = new String("abc"); // 새로운 String 인스턴스를 생성
String str4 = new String("abc"); // 새로운 String 인스턴스를 생성
자바에서는 문자열 리터럴을 문자열 풀(String Pool)이라는 특별한 메모리 영역에 저장합니다. 문자열 리터럴이 생성될 때, 문자열 풀은 이미 동일한 값을 가진 문자열 리터럴이 있는지 확인합니다. 만약 동일한 문자열 리터럴이 이미 존재한다면, 새로운 문자열 객체를 생성하지 않고 기존의 문자열 객체를 재사용합니다.
예를 들어, 아래와 같은 코드가 있다고 가정해봅시다:
String str1 = "abc";
String str2 = "abc";
위 코드에서 str1
과 str2
는 모두 "abc"
라는 동일한 문자열 리터럴을 참조합니다. 이 경우, 자바는 문자열 풀에서 "abc"
라는 문자열 리터럴을 찾고, 이미 존재한다면 새로운 문자열 객체를 생성하지 않고 기존의 문자열 객체를 재사용합니다¹. 따라서 str1
과 str2
는 메모리에서 동일한 문자열 객체를 참조하게 됩니다.
그러나 이것은 문자열 리터럴에만 해당되며, new
키워드를 사용하여 명시적으로 새로운 문자열 객체를 생성하는 경우에는 이 규칙이 적용되지 않습니다. 예를 들어, 아래와 같은 코드를 보겠습니다:
String str3 = new String("abc");
위 코드에서 str3
는 new
키워드를 사용하여 새로운 문자열 객체를 생성하므로, str1
또는 str2
와는 다른 메모리 주소를 가지게 됩니다.
따라서, 자바는 모든 경우의 수의 문자열 리터럴을 저장하는 것이 아니라, 프로그램에서 실제로 사용되는 문자열 리터럴만을 저장하고 있습니다. 이는 메모리 사용량을 최적화하는 데 도움이 됩니다. 이러한 방식 덕분에 자바는 동일한 문자열 리터럴을 여러 번 재사용할 수 있으므로, 메모리 사용량을 줄일 수 있습니다.
자바 소스파일에 포함된 모든 문자열 리터럴은 컴파일 시에 클래스 파일에 저장된다. 이 때 같은 내용의 문자열 리터럴은 한번만 저장된다. 문자열 리터럴도 String인스턴스이고, 한번 생성하면 내요을 변경할 수 없으니 하나의 인스턴스만 공유하면 되기 때문이다.
상세내용 Java의 정석 기초편 1 332페이지 참조
class Example {
public static void main(String[] args) {
String s1 = "AAA";
String s2 = "AAA";
String s3 = "AAA";
String s4 = "BBB";
}
}
위와 같은 소스파일을 컴파일 하면 해당하는 클래스파일이 생성되고, 해당 클래스파일 내에 "AAA"인스턴스 한 개 "BBB"인스턴스 한 개 생성 되고 s1,s2,s3는 모두 "AAA"인스턴스의 주소를 참조하고, s4만 "BBB"인스턴스의 주소를 참조하게 된다.
클래스 파일이 클래스 로더에 의해 메모리에 올라갈 때, 클래스 파일의 리터럴들이 JVM내에 있는 '상수 저장소(constant pool)'에 저장되고, 이 때, "AAA"와 같은 문자열 리터럴은 자동적으로 생성되어 저장되는 것이다.
join()은 여러 문자열 사이에 구분자를 넣어서 결합한다. split()과 반대의 작업을 한다고 생각하면 된다.
String animals = "dog,cat,bear";
String[] arr = animals.split(","); // 문자열을 ','을 기준으로 나눠서 배열에 저장
String str = String.join("-", arr); // 배열의 문자열을 '-'로 구분해서 결합
System.out.println(str); //dog-cat-bear
java.util.StringJoiner클래스를 사용해서도 문자열을 결합할 수도 있다. 사용하는 방법은 아래와 같다.
StringJoiner sj = new StringJoiner(",", "[", "]");
String[] strArr = { "aaa", "bbb", "ccc" };
for(String s : strArr)
sj.add(s.toUpperCase());
System.out.println(sj.toString()); // [AAA,BBB,CCC]
숫자로 이루어진 문자열을 숫자로, 또는 그 반대로 변환하는 경우가 자주 있다.
int i = 1000;
String str1 = i + "";
String str2 = String.valueOf(i);
첫번째 방법은 숫자에 빈 문자열""을 더해주는 것이다.
두번째 방법은 String클래스의 valueOf메서드를 사용하는 것이다.
첫번째 방법이 간단하고 많이 사용하지만, 두번째 방법은 성능이 좋기 때문에 성능 향상이 필요할때만 사용한다.
int i = Integer.parseInt("100");
int i2 = Integer.valueOf("100");
첫번째나 두번째나 Integer클래스에 있는 방법이나, 원래 valueOf()의 반환타입은 Integer인데, 오토박싱(auto-boxing)에 의해 자동으로 int로 변환된다.
모든 기본형 타입 클래스에는 parse타입() 메서드가 존재하기에 해당 메서드의 인자로 String값을 주면 자동으로 해당 타입의 데이터로 변환해서 반환해 준다. 문자열에 해당 타입에 필요없는 문자열 예를 들면 Integer에 "33294f" 처럼 문자가 들어있으면 NumberFormatException이 발생한다. 만약 Float.parseFloat("33294f"); 처럼 해당 데이터타입에 필요한 문자라면 상관없다.
또한 접두사로 +,-가 들어있는것은 괜찮으며, 공백은 항상 없어야 하기 때문에 .trim()과 같이 사용하는것도 방법이다.