Java의 String은 두 가지 방법으로 생성할 수 있다. 하나는 new를 통한 생성이고 하나는 리터럴을 통한 생성이다.
1
2
|
String str1 = "hello"; // 리터럴을 사용한 생성
String str2 = new String("hello"); // new를 통한 생성
|
cs |
이 두 방식은 겉보기에는 같지만 리터럴은 String 값이 Heap 메모리 내의 Constant Pool에 저장되어 재사용 된다는 차이가 있다. 따라서 두 방식으로 할당한 같은 문자열들을 비교하면 new 연산은 주소값이 다르기 때문에 다음과 같은 결과를 얻는다.
1
2
3
4
5
6
7
8
9
10
11
12
|
// 리터럴을 사용한 선언
String str1 = "hello";
String str2 = "hello";
// String Constant Pool에 있는 같은 "hello"를
// 가르킨다.
System.out.println(str1 == str2); // true
// new를 사용한 선언
String str1 = new String("hello");
String str2 = new String("hello");
// str1과 str2는 서로 다른 객체다.
System.out.println(str1 == str2); // false
|
cs |
new 연산을 사용해 생성한 String 객체는 같은 값이 String Pool에 존재해도 Heap 영역 내 별도의 객체를 가르킨다.
이를 바이트 코드를 통해 확인해 보면 리터럴을 사용한 선언은 다음과 같이 new 연산을 하지 않는 것을 알 수 있다.
1
2
3
4
|
L0
LINENUMBER 9 L0
LDC "hello"
ASTORE 1
|
cs |
이에 반해 new를 통한 할당은 new를 사용하면 새로운 객체를 생성하는 것을 알 수 있다.
1
2
3
4
5
6
7
|
L0
LINENUMBER 17 L0
NEW java/lang/String
DUP
LDC "hello"
INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
ASTORE 1
|
cs |
Constant Pool은 Java Class File의 구성 항목 중 하나이며 리터럴 상수 값을 저장하는 곳이다. 여기에는 String, 모든 종류의 숫자, 문자열, 식별자 이름, Class, method에 대한 참조 같은 값이 포함되 있다.
스트링 덧셈 연산
스트링을 결합하기 위해 사용되는 + 연산은 자바 1.5 이전에는 concat과 같은 방식으로 동작했기 때문에 메모리 낭비가 심했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public String concat(String str) {
if (str.isEmpty()) {
return this;
}
if (coder() == str.coder()) {
byte[] val = this.value;
byte[] oval = str.value;
int len = val.length + oval.length;
byte[] buf = Arrays.copyOf(val, len);
System.arraycopy(oval, 0, buf, val.length, oval.length);
return new String(buf, coder);
}
int len = length();
int olen = str.length();
byte[] buf = StringUTF16.newBytesFor(len + olen);
getBytes(buf, 0, UTF16);
str.getBytes(buf, len, UTF16);
return new String(buf, UTF16);
}
|
cs |
이때문에 두 개의 스트링을 결합하기 위해선 3 개의 인스턴스를 생성했다.
1
2
3
4
|
"hello" + "world"
// 1. "hello" 인스턴스
// 2. "world" 인스턴스
// 3. "hello world" 인스턴스
|
cs |
이런 방식은 메모리 낭비가 심했기 때문이 이를 최적화 하기 위해 StringBuilder와 StringBuffer가 추가 되었다.
StringBuilder의 append를 사용하면 문자열 결합에 대해 하나의 메모리 주소만 갖는다. 그 이유는 append가 위에서 본 concat과는 다르게 새로운 객체를 생성하지 않기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
/**
* Appends the specified string to this character sequence.
* <p>
* The characters of the {@code String} argument are appended, in
* order, increasing the length of this sequence by the length of the
* argument. If {@code str} is {@code null}, then the four
* characters {@code "null"} are appended.
* <p>
* Let <i>n</i> be the length of this character sequence just prior to
* execution of the {@code append} method. Then the character at
* index <i>k</i> in the new character sequence is equal to the character
* at index <i>k</i> in the old character sequence, if <i>k</i> is less
* than <i>n</i>; otherwise, it is equal to the character at index
* <i>k-n</i> in the argument {@code str}.
*
* @param str a string.
* @return a reference to this object.
*/
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);
count += len;
return this;
}
// 이 StringBuilder 내의 append 메서드는 위 메서드의 override 이다.
@Override
@HotSpotIntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this;
}
|
cs |
이에 따라 + 연산도 내부적으로 StringBuilder의 append를 사용하게 되어 메모리 효율이 증가했다. java 8에서 두 스트링을 더하는 연산을 바이트 코드로 보면 다음과 같이 append를 사용함을 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
String str1 = "hello";
String str2 = "world";
String str3 = str1 + str2;
// 위 연산을 바이트 코드로 보면 다음과 같다
L0
LINENUMBER 17 L0
LDC "a"
ASTORE 1
L1
LINENUMBER 18 L1
LDC "b"
ASTORE 2
L2
LINENUMBER 19 L2
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 3
|
cs |
하지만 java 11 부터는 더이상 String의 + 연산에 StringBuilder를 사용하지 않는다. 그 이유를 이해하기 위해 우선 다음과 같은 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
String str4 = "";
for (int i = 0; i < 10; i++) {
str4 += i;
}
// 위 코드에서 for loop를 바이트 코드로 보면 다음과 같다.
L1
LINENUMBER 18 L1
ICONST_0
ISTORE 2
L2
FRAME APPEND [java/lang/String I]
ILOAD 2
BIPUSH 10
IF_ICMPGE L3
L4
LINENUMBER 19 L4
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ILOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 1
L5
LINENUMBER 18 L5
IINC 2 1
GOTO L2
L3
LINENUMBER 21 L3
FRAME CHOP 1
RETURN
|
cs |
위 for loop를 바이트 코드로 변환 했을 때 loop 내부에서 지속적으로 StringBuilder를 생성해주는 것을 볼 수 있다. 따라서 성능 문제가 있었다. 따라서 자바 11 부터는 StringConcatFactory.makeConcatWithConstants를 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
L1
LINENUMBER 18 L1
ICONST_0
ISTORE 2
L2
FRAME APPEND [java/lang/String I]
ILOAD 2
BIPUSH 10
IF_ICMPGE L3
L4
LINENUMBER 19 L4
ALOAD 1
ILOAD 2
INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;I)Ljava/lang/String; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
// arguments:
"\u0001\u0001"
]
ASTORE 1
L5
LINENUMBER 18 L5
IINC 2 1
GOTO L2
L3
LINENUMBER 21 L3
FRAME CHOP 1
RETURN
|
cs |
StringBuilder와 StringBuffer
StringBuffer는 StringBuilder와 달리 thread-safe하다. 하지만 이로 인해 느리다. 게다가 String 덧셈 연산을 thread-safe하게 할 일이 거의 없다. 다음은 StringBuffer의 일부 코드다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// synchronized로 떡칠을 했다...
@Override
public synchronized int compareTo(StringBuffer another) {
return super.compareTo(another);
}
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return super.capacity();
}
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
super.ensureCapacity(minimumCapacity);
}
/**
* @since 1.5
*/
@Override
public synchronized void trimToSize() {
super.trimToSize();
}
|
cs |
따라서 StringBuffer 대신 StringBuilder를 사용하자.
'java' 카테고리의 다른 글
Annotation (0) | 2022.04.23 |
---|---|
enum (0) | 2022.02.21 |
추상 클래스와 인터페이스의 차이 (0) | 2022.01.03 |
Nested class, inner class (0) | 2021.05.05 |
패키지와 클래스 규칙, 특성 (0) | 2021.03.04 |