Java에서 String은 불변(Immutable) 객체라고 불립니다. 불변 객체란 객체가 생성된 후 상태가 변하지 않고 계속 유지되는 객체를 말합니다. String 인스턴스는 한 번 생성되면 그 값을 읽기만 할 수 있고, 변경할 수는 없습니다. 이제 Java에서 String 왜 불변 객체인지 그리고 그로 인한 장점과 특징이 무엇이 있는지 알아보도록 하겠습니다.
1. String Pool
모든 언어에서 String은 활발하게 사용되는 자료형입니다. 이러한 자료형을 생성하고 삭제하는 것은 많은 자원을 소비하게 됩니다. Java에서는 이러한 현상을 해결하고자 Heap의 String Pool을 활용하여 해결하고 있습니다.
String은 참조 타입의 자료형이지만 특이하게도 new 키워드와 리터럴로 모두 생성이 가능합니다. String을 new 키워드로 생성할 경우 Heap에 개별적인 메모리를 가지게 되고, 리터럴로 생성할 경우에는 Heap의 String Pool에 생성하게 되는데 동일한 값이 있다면 String Pool에 있는 주소 값을 반환하게 되고 동일한 값이 없다면 새로운 주소를 할당하고 해당 주소를 반환합니다.
만약 A라는 String 리터럴을 저장하고 있는 변수에 다른 String 리터럴 B를 대입하게 된다면 Java는 B라는 리터럴이 String Pool에 존재하지 않는다면 String Pool에 생성 후 참조 값을 반환합니다. 만약 B라는 리터럴이 존재할 경우 B의 참조 값을 반환하여 중복 생성되지 않도록 합니다. 또 덧셈(+) 연산자를 이용하여 문자열 결합을 수행하면, 기존 문자열의 내용이 변경되는 것이 아니라 내용이 합쳐진 새로운 String 인스턴스가 생성됩니다. 결론적으로 Value가 같지 않은 String 서로 다른 객체라고 할 수 있고, 이 객체는 변하지 않는 객체입니다.
public static void main(String[] args) {
String strA = "donghwan";
System.out.println(System.identityHashCode(strA));
String strB = "donghwan";
System.out.println(System.identityHashCode(strB));
strA = strA + " developer";
System.out.println(System.identityHashCode(strA));
}
//result
1829164700
1829164700
2018699554
2. HashCode Caching
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
문자열은 데이터 구조에 많이 사용됩니다. 해시맵, 해시테이블, 해시셋 등과 같은 자료구조에서 해시 구현에 따라 작동할 때 hashCode() 메서드가 꽤 자주 호출됩니다. 앞서 설명한 자료구조들은 key의 hash를 통해 value를 저장하는 구조를 가지고 있습니다. 따라서 매번 hashCode() 메서드를 호출하게 됩니다. 이 때, 매번 HashCode를 계산하게 된다면 성능에 영향을 줄 수 있습니다. 이런 부분을 고려하여 Java에서 String은 hash 값을 계산한 적이 없을 때 최초 1번만 실제 계산을 실행하고, 이 후 해당 값을 그냥 반환만 하도록 하여 재활용 할 수 있도록 설계되어 있습니다. 이런 설계가 가능한 이유는 String이 독립적이고 불변이기 때문이라고 할 수 있습니다.
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("donghwan", "1");
map.put("donghwan", "2");
map.put("donghwan", "3");
map.values().forEach(str -> System.out.println(str));
}
//result
3
위 예제를 보면 HashMap에 put을 통해 3개의 데이터를 넣었지만 3만 출력이 되었습니다. HashMap의 put 메서드를 보면 이유를 알 수 있습니다. 아래 메서드에서 putValu는 입력 받은 hash를 기준으로 조건을 검색하고 동일한 Hash와 key이면 덮어쓰기를 실행하고 있습니다. 따라서 위 예제 코드에서 순차적으로 값이 덮어씌여 "3"만 존재하게 됩니다.
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
3. Thread-Safe
String이 불변이기 때문에 Thread-Safe를 가져갈 수 있습니다. 불변 객체는 값이 바뀔 일이 없기 때문에 멀티스레드 환경에서 Thread-safe 하다는 장점이 있습니다. 따라서 일반적으로 불변의 개체는 동시에 실행되는 여러 스레드에서 공유할 수 있습니다. 스레드가 값을 변경하면 동일한 문자열을 수정하는 대신 String Pool에 새 문자열이 생성되기 때문에 Thread-Safe합니다.
참고자료
'Language > Java' 카테고리의 다른 글
[Java] Arrays 클래스 (0) | 2021.11.02 |
---|---|
[Java] String과 String Pool (0) | 2021.11.01 |
[Java] 동일성과 동등성 ( == vs equals ) (0) | 2021.11.01 |
[Java] orElse vs orElseGet (0) | 2021.10.29 |
[Java] Optional (0) | 2021.10.29 |
댓글