문자열을 위한 클래스
참조형(Reference Type)으로 객체와 같이 스택 영역에 저장되는 것이 아니라 힙(heap)에 저장된다.
String 클래스 내부에서는 char형의 배열 객체를 다룬다.
문자열을 저장하고자 문자형 배열 변수(char[]) value를 인스턴스 변수로 정의해놓고 있다.
자바에서는 객체들을 재사용하기 위해서 Constant Pool에 객체들을 저장하는데, String의 경우 동일한 값을 갖는 객체가 이미 Constant Pool에 있으면 이미 만든 String 객체를 재사용한다.
// 둘은 똑같은 객체로 똑같은 객체의 주솟값을 가진다.
String text = "hi";
String text2 = "hi"; // text를 재사용한다.
동등 연산자(==) : 기본형(primitive type) 변수 간에 값을 비교하거나 참조형 변수간에 참조하는 메모리의 주솟값을 비교할 때 사용한다.
String str1 = new String("Hello");
String str2 = new String("Hello");
boolean result = (str1 == str2); // false, 서로 다른 객체를 가리키므로
equals() 메서드 : 매개 변수를 비교하고자 하는 객체의 참조 변수를 받아 비교한다
String str1 = new String("Hello");
String str2 = new String("Hello");
boolean result = str1.equals(str2); // true, 내용이 동일하므로
위의 코드에서처럼 String, Integer, Double 등은 equals() 메서드를 오버라이딩해서 내용을 비교한다.
하지만 일반적인 경우, 서로 다른 객체인 경우 equals() 메서드는 늘 false를 반환한다.
equals 메서드에서는 기본적으로 동등 연산자(==)를 사용해서 객체를 비교한다. 즉, 두 참조 변수가 힙에 있는 같은 객체를 참조하는지를 검사한다.
객체가 가지고 있는 고유한 값
객체를 구별할 때 사용한다.
해시코드는 객체의 주소와 관련된 정숫값이다.
원시(native) 메서드로 JVM에서 원시 코드로 직접 구현된 메서드
해시코드가 같고 자료형도 같아야 같은 객체라고 본다.
String hello = new String("hello");
new 연산자와 String 생성자를 사용하면, 생성할 때마다 힙에 새로운 문자열 객체가 생성된다. 그러나 이러한 구조는 문자열을 다루는데 비효율적일 수 있다.
String 클래스는 문자열 객체를 생성하면서 상수로 인식한다. 그래서 한번 생성되면 문자열을 수정할 수 없다.
즉, 기존의 문자열을 수정하는 것이 안라 새로운 문자열을 생성해야 한다. 이를 불변성이라고 한다.
한 번 생성된 String은 변경 할 수 없다.
하나의 문자열과 다른 문자열을 결합하면 기존 문자열에 추가되는 것이 아니라 새로운 문자열이 생성된다.
public static void main(String[] args) {
String a = "a";
String b = "b";
a = a + b;
System.out.println(a.hashcode() == b.hashcode()); // false
}

이 때문에 + 연산자 사용은 지양해야 한다.
String hello = "hello"
위의 코드처럼 String을 만들면 실행 데이터 영역(runtime data area)의 상수 풀(constant pool)에 리터럴이 저장된다.
상수 풀은 클래스와 같은 Runtime Data Area의 메서드 영역에 위치해서 자바 프로세스가 종료될 때까지 생명 주기를 함께 한다.
String 객체를 new 연산자로 생성하지 않고 문자열 리터럴을 지정해서 생성할 경우, 내부적으로 new String() 메서드를 호출한 이후에 String intern() 메서드가 호출되어 고유의 객체를 공유하도록 강제로 소속(interned)된다.
이렇게 상수 풀을 사용해서 문자열을 관리하면 메모리를 절약하는데 효과적이다. 또한 상수 풀은 GC의 대상이 되지 않기 때문에 프로세스가 종료될 때까지 삭제되지 않는다.
public static void main(String args[]) {
// 힙
String s1 = new String("hello");
String s2 = new String("hello");
// 상수 풀
String s3 = "hello";
String s4 = "hello";
String s5 = s1;
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
System.out.println(s1.hashCode() == s2.hashCode()); // true
System.out.println(s2 == s3); // false
System.out.println(s2.equals(s3)); // true
System.out.println(s3 == s4); // true
System.out.println(s4 == s5); // false
System.out.println(s4.equals(s5)); // true
System.out.println(s1 == s5); // true
System.out.println(s2.intern() == s3); // true - intern 메서드를 사용하면 상수 풀을 먼저 검사하기 때문에 true
}
intern() 메서드는 주로 정확도가 높지만 느린 equals() 메서드가 아닌 빠른 속도의 동등 연산자(==)를 사용하기 위해서 사용한다.
intern() 메서드는 C로 만들어진 native 메서드로, Constant Pool에 값을 직접 할당한다.
직접 할당하게 되면 객체를 중복 생성해서 불필요하게 Constant Pool의 메모리를 채우게 된다.
가비지 컬렉팅의 어려움도 겪게 된다.
문자열은 참조형(Reference) 데이터이기 때문에 JVM의 힙(heap) 영역에 들어간다.
힙 영역에는 문자열만을 특별히 보관하기 위해 문자열 상수 풀(String Constant Pool)이 있는데, 이 곳에서는 문자열을 인스턴스화 시켜서 문자열들이 중복되지 않게 관리한다.
따라서, 문자열 리터럴이 생성될 때마다 JVM은 해당 문자열이 문자열 상수 풀에 존재하는지 확인한다. 문자열 상수 풀에 해당 문자열이 존재하지 않으면, 해당 문자열을 문자열 상수 풀에 저장하고 존재하면 저장하지 않는다.
문자열 상수 풀에 동일한 문자열이 이미 존재하는 경우 새로운 문자열을 생성하지 않으므로 메모리 공간을 절약한다.
문자열 상수 풀은 문자열 캐싱을 사용하므로 JVM은 문자열이 문자열 상수 풀에 존재하는지 빠르게 확인할 수 있습니다.
하지만, new 연산자를 이용해서 new String 객체를 생성하면 그것은 상수 풀에 들어가지 않고 힙 영역에 들어가기 때문에 동일한 내용을 가진 문자열이라도 여러 개 생성이 가능하다.
intern() 메서드를 통해 해당 문자열이 문자열 상수 풀에 있는지 확인할 수 있고, 있다면 해당 문자열의 내용을 반환한다.
Multi-thread 환경에서 여러 스레드가 동시에 접근해도 String은 불변하기 때문에 안전하다.
String이 불변해서 여러 스레드가 하나의 String에 접근해도 동기화 문제가 발생하지 않는다.
여러 스레드가 하나의 문자열을 사용하더라도 문자열의 내용은 변하지 않는다.
문자열은 변하지 않기 때문에 해싱(hash)된 값으로 사용될 수 있다.
사용자 이름, 암호는 데이터베이스 연결을 수신하기 위해 문자열로 전달되는데, 만일 번지수의 문자열 값이 변경이 가능하다면 해커가 참조 값을 변경하여 애플리케이션에 보안 문제를 일으킬 수 있다.
자바에서는 대표적으로 문자열을 다루는 자료형 클래스로 String, StringBuffer, StringBuilder 라는 3가지 자료형을 지원한다.
기존의 문자열에 다른 문자열을 더하면 기존의 문자열에 추가가 되는 것이 아니라 메모리의 주소가 다른 새로운 문자열이 생긴다.
JDK 5 이전에는 concat 방식과 동일했다가 JDK 9 부터는 StringBuilder를 사용하도록 바뀌었다.
컴파일 전 내부적으로 StringBuilder 클래스를 만든 후 다시 문자열로 돌려준다.
String a = "hello" + "world";
// 는 아래와 같다.
String b = new StringBuilder("hello").append("world").toString();
/// 반복적인 + 연산자 사용 예시
String a = "";
for(int i = 0; i < 10000; i++) {
a = a + i;
}
// 이런 짓거리 하면 이렇게 구현하는 것과 같다.
String b = "";
for(int i = 0; i < 10000; i++) {
b = new StringBuilder(b).append(i).toString();
}
public static void main(String args[]) {
String strValue = "ABC";
// concat() 메서드를 3번 호출했으므로 힙 영역에는 3개의 데이터가 생성된다.
// 힙 영역에 생성되는 3개의 데이터: "ABCD", "ABCDE", "ABCDEF"
String strConcatResult = strValue.concat("D").concat("E").concat("F");
}
+연산자를 여러 번 호출하더라도 최종 결과만 힙 영역에 생성된다.
concat 메소드를 호출할 때마다 힙 영역에 데이터가 생성되기 때문에 메모리를 가장 많이 소모한다.
대부분의 경우에는 concat을 지양하고 +를 사용하면 충분하다.
StringBuffer는 수정이 가능하다.
내부적으로 버퍼(buffer)라고 하는 독립적인 공간을 가지게 되어, 문자열을 바로 추가할 수 있어 공간의 낭비도 없으며 문자열 연산 속도도 매우 빠르다는 특징이 있다.
멀티 스레드 환경에서 동기화를 지원해서 여러 스레드가 동시에 접근해도 안전하다.
동기화 때문에 성능이 조금 떨어진다.
Web이나 소켓환경과 같이 비동기로 동작하는 경우가 많을 때는 StringBuffer를 사용하는 것이 안전하다.
String에서는 +연산자를 이용해 새로운 객체를 계속 생성하지만, StringBuffer는 append() 메서드를 통해 하나의 String 객체만을 사용한다.
StringBuffer에서는 toString() 메서드를 재정의해 StringBuffer 객체의 문자 배열을 String 객체로 생성해 반환한다.
String 클래스와 달리 equals 메서드를 재정의하지 않아서 동등 연산자(==)과 같은 결과를 가진다.
StringBuilder는 동기화가 없어서 String 객체를 빠르게 수정할 수 있다.
스레드 안정성을 보장하지 않는다.
/// + 연산자 대신 올바른 사용 예시
final StringBuilder a = new StringBuilder();
for(int i = 0; i < 10000; i++) {
a.append(i);
}
final String b = a.toString();