[TIL] 20250103 String

Drumj·2025년 1월 3일
0

2025 TIL

목록 보기
4/11

String

String str1 = new String("hello");
String str2 = "hello";

String 클래스를 통해 문자열을 생성하는 방법은 위와 같이 2가지가 있다.
new String() 을 사용하는 방법과 "hello" 리터럴을 사용하는 방법.

Stringint,boolean등 과 같은 기본형과 달리 참조형이다. 근데 String str ="hello" 와 같이 생성하는 건 약간 어색해 보이긴 한다..

하지만 문자열은 매우 자주 사용되기 때문에 자바 언어에서 "hello"와 같이 작성해도 알아서 new String("hello")와 같이 변경해준다고 한다.


String class

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 비교

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를 반환하는 것이다.


String 불변 객체

앞에서 본 것 처럼 리터럴을 사용할 경우 참조값이 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

str2hello java 로 변경하고 싶었으나 같은 참조값을 가지고 있는 str1도 변경이 된 걸 확인 할 수 있다.

이런 사이드 이펙트가 발생하기 때문에 String은 불변 객체로 설계되었다고 한다.


StringBuilder

이처럼 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

0개의 댓글