String str1 = new String("hello");
String str2 = "hello";
String
클래스를 통해 문자열을 생성하는 방법은 위와 같이 2가지가 있다.
new String()
을 사용하는 방법과 "hello"
리터럴을 사용하는 방법.
String
은 int
,boolean
등 과 같은 기본형과 달리 참조형이다. 근데 String str ="hello"
와 같이 생성하는 건 약간 어색해 보이긴 한다..
하지만 문자열은 매우 자주 사용되기 때문에 자바 언어에서 "hello"
와 같이 작성해도 알아서 new String("hello")
와 같이 변경해준다고 한다.
public final class String {
//문자열 보관
private final char[] value; //java9 이전
private final byte[] value; //java9 이후
//여러 메서드
public String concat(String str) {...}
public int length() {...}
...
}
value
필드를 보면 자바9 이전엔 char[]
를 사용했지만 자바9 이후엔 최적화를 시켜서 byte[]
를 사용한다고 한다. 그리고 final
로 선언되어 있는걸 기억하자!
String 클래스의 여러 메서드
- length() : 문자열의 길이를 반환
- charAt(int index) : 특정 인덱스의 문자를 반환
- subString(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환
- indexOf(String str) : 특정 문자열이 시작되는 인덱스를 반환
- toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환
- trim() : 문자열 양 끝의 공백을 제거
- concat(String str) : 문자열을 더한다.
이 외에도 여러 메서드들이 있으니 필요할 때 검색해서 사용하도록 하자!
String
클래스를 비교할 때는 ==
이 아닌 항상 equals()
를 사용해서 비교해야 한다.
String str1 = new String("hello");
String str2 = new String("hello");
str1 == str2; //false
str1.equals(str2); //true
String str3 = "hello";
String str4 = "hello";
str3 == str4; //true
str3.equals(str4); //true
위 코드를 보면 new String()
으로 객체를 생성해서 사용할 경우 ==
비교는 false
를 반환하는 걸 볼 수 있다. equals()
메서드를 사용해도 내부적으로 ==
비교를 사용한다는 걸 Object
를 공부하면서 알고 있는데 String
클래스는 equals()
를 재정의 해놓았다고 한다.
또 리터럴("hello"
)를 사용한 경우에는 ==
,equals()
모두 true
를 반환하는데 이는 자바에서 문자열을 사용할 때 문자열 풀
이라는걸 만들어서 메모리 효율성과 성능 최적화를 하기 때문이라고 한다.
자세히 설명하자면
String str1 = new String("hello"); // 참조값 x001
String str2 = new String("hello"); // 참조값 x002
String str3 = "hello"; //참조값 x003
String str4 = "hello"; //참조값 x003
new String()
을 우리가 직접 적어서 사용할 경우 자바가 "아? 완전 다른 객체를 만들어서 사용하려고 하는거구나?" 하고 각각 새로운 인스턴스를 만드는 반면 리터럴hello
를 사용할 경우 "어 이건 내가 알아서 문자열 풀로 관리할게!" 해버린다는 것.
그렇기 때문에 리터럴을 사용할 경우 참조값이 같아서 ==
비교도 true
를 반환하는 것이다.
앞에서 본 것 처럼 리터럴을 사용할 경우 참조값이 x003
으로 같다는 걸 알 수있다.
그럼 str3
을 수정해버리면 어떻게 될까?
str3.concat(" java");
System.out.println(str3); // hello
어..? concat()
메서드를 사용해서 문자열을 추가했는데 왜 아직도 hello
가 반환되는 거지?
그 이유는 앞서 설명한 private final byte[] value;
에 있다. value
필드는 final
로 선언 되어있다. 또한 public String concat(String str)
메서드를 잘 보면 이 메서드는 String
을 반환값으로 갖고 있다!
String concat = str3.concat(" java");
System.out.println(concat); // hello java
이렇게 concat()
메서드를 사용하면 새로운 인스턴스를 반환하고 그 값을 사용해야 내가 원하는 결과인 hello java
를 얻을 수 있다!
그럼 왜 String
은 불변 객체로 만들어졌을까?
앞서 리터럴로 String
을 생성했을 때 같은 값을 가지면 x003
으로 참조값이 같다는 걸 확인했었다. 이렇게 같은 참조값을 가진 상태로 해당 값을 변경하려고 할 때 어떤 사이드 이펙트가 발생했었는지 이전에도 봤었다.
//String이 불변객체가 아니라면?
String str1 = "hello"; // x001
String str2 = "hello"; // x001
//여기선 임의로 concat 메서드도 String을 반환하지 않고 바로 문자열을 합친다고 가정하자
str2.concat(" java");
System.out.println(str1); // hello java
System.out.println(str2); // hello java
난 str2
만 hello java
로 변경하고 싶었으나 같은 참조값을 가지고 있는 str1
도 변경이 된 걸 확인 할 수 있다.
이런 사이드 이펙트가 발생하기 때문에 String
은 불변 객체로 설계되었다고 한다.
이처럼 String
은 불변 객체이기 때문에 단점도 존재한다.
문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점이다. 코드를 보자
"A" + "B";
String("A") + String("B");
String("A").concat(String("B"));
new String("AB");
String
을 더하는 과정을 간단하게 알아봤다. 만약 더 많은 문자를 추가한다면 어떻게 될까?
"A" + "B" + "C" + "D";
//...생략
new String("AB") + "C" + "D";
//...생략
new String("ABC") + "D";
//...생략
new String("ABCD");
생략을 하긴 했지만 여러 과정을 거쳐 최종적으로 생성된 new String("ABCD")
만을 사용하고 중간에 생성된 new String("AB")
,new String("ABC")
는 사용되지 않고 GC의 대성이 된다.
이러면 결과적으로 컴퓨터의 CPU,메모리 자원을 더 많이 사용하게 된다고 한다.
이러한 단점을 해결하기 위해 나온 녀석이 바로 StringBuilder
public final class StringBuilder {
char[] value; //Java9 이전
byte[] value; //Java9 이후
//여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
여기서 value
필드가 String
과는 달리 final
로 선언되어 있지 않다는 점을 봐야한다!
그럼 어떻게 사용할 수 있는지 알아보자.
StringBuilder sb = new StringBuilder();
sb.append("A").append("B").append("C").append("D");
System.out.println(sb); //ABCD
상당히 간단하게 끝난다!
어??? 위에 보니까 append()
메서드도 StringBuilder
를 반환하고 있는데?? 이러면 new StringBuilder()
로 객체를 생성하는 거 아니야?라는 생각이 들어서
String.concat()
과 StringBuilder.append()
메서드를 더 깊게 들어가 봤다.
결과적으로 concat()
메서드는 new String()
을 반환하고 append()
메서드는 this
를 반환하는 걸 확인했다. 내부코드를 정확히 파악은 못했지만 append()
는 arrayCopy
를 이용해서 값을 추가하는 것 같았다...(이 부분은 추가 확인 필요)
ChatGPT 찬스
- String.concat():
- 항상 새로운 String 객체를 생성.
- 두 문자열을 복사하여 새 배열에 저장.
- 기존 객체는 변경되지 않음.
- StringBuilder.append():
- 같은 객체 내부에서 배열 크기를 조정하며 문자열을 추가.
- 기존 객체를 재사용하므로 성능이 효율적.
- 크기 초과 시에만 새로운 배열 생성.
결론
- String.concat():
- 새로운 객체를 생성하므로 성능이 비교적 느립니다.
- 문자열 조합 작업이 많을 경우 성능 이슈가 발생할 수 있습니다.
- StringBuilder.append():
- 가변 배열을 사용하므로 문자열 추가 작업에 더 효율적입니다.
- 다량의 문자열 처리가 필요할 때 적합합니다.
- 추가 팁
- 문자열을 반복적으로 연결해야 한다면 StringBuilder 또는 StringBuffer(멀티스레드 환경)를 사용하는 것이 좋습니다.
- String.concat()은 단순한 문자열 결합 작업에 적합하지만, 반복 작업에서는 피하는 것이 좋습니다.
물론! 자바에서도 최적화를 꾸준히 하고 있기 때문에 간단한 +
연산의 경우에는 그대로 사용해도 된다!
- 김영한의 실전 자바 - 중급 1편
- ChatGPT