[Java ☕️] String Class

ConewLog·2024년 8월 1일
0

Java ☕️

목록 보기
3/7

☕️ 김영한의 실전 자바 - 중급 1편 을 수강하며 학습한 내용을 저만의 언어로 정리하고 있습니다.


1. String 기본

String 선언 - 리터럴 vs. 객체 선언

String은 클래스이다.
따라서, 문자열 객체를 생성할 때 new 연산자를 사용해 객체를 선언하는 것이 당연하나,
"..." 쌍따옴표로도 문자열 객체를 생성할 수 있다.

이는 자주 사용되는 문자열을 편리하게 쓸 수 있도록 하기 위해서이다.

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

리터럴 방법과 객체 선언 방법의 동일성(==), 동등성(equals) 비교

public class Example {
    public static void main(String[] args) {
        // 리터럴의 동일성, 동등성 비교
        String str1 = "Conew";
        String str2 = "Conew";
        System.out.println("str1 == str2 :" + (str1 == str2));
        System.out.println("str1.equals(str2): " + str1.equals(str2));

        // 객체 선언의 동일성, 동등성 비교
        String str3 = new String("Conew");
        String str4 = new String("Conew");
        System.out.println("str3 == str4: " + (str3 == str4));
        System.out.println("str3.equals(str4): " + str3.equals(str4));
    }
}
결과
str1 == str2 :true
str1.equals(str2): true
str3 == str4: false
str3.equals(str4): true
  • str1, str2는 리터럴로 String을 선언했다.

    • 동일성 (==) 비교에서 true를 반환한다.

      • 문자열 리터럴을 사용하는 경우, ⭐️문자열 풀에 같은 문자를 가진 인스턴스가 있다면 그 인스턴스의 참조를 반환한다.
    • 동등성 (equals) 비교에서 true를 반환한다.

  • str3, str4는 객체 생성 방식으로 String을 선언했다.

    • 동일성 (==) 비교에서 false를 반환한다.

      • str3str4가 서로 다른 String 인스턴스를 가리키고 있기 떄문이다.
    • 동등성 (equals) 비교에서 true를 반환한다.

문자열 풀


코드를 그림으로 나타내면 위와 같다.

문자열 풀

  • 문자열 풀은 자바가 실행되는 시점에 쓰이는 문자열 리터럴들을 저장하는 공간이다.
  • 문자열 풀을 이용하면 매번 같은 문자열 객체를 생성하지 않아도 되기 때문에 메모리를 효율적으로 이용하고, 성능을 최적화 할 수 있다.
  • 문자열 리터럴을 사용할 때, 우리는 문자열 풀에서 해당 문자열을 가지는 인스턴스를 찾아 참조값을 변수에 저장한다.

str1str2를 동일성 비교했을 때 결과가 true로 나온 이유는
str1, str2 모두 문자열 풀에서 "Conew" 라는 문자를 가진 인스턴스를 찾고, 그 참조값을 변수에 저장하기 때문이다.

반면 str3, str4는 각각 새로운 문자열 객체를 명시적으로 생성하고 각각의 참조값을 변수에 저장하므로 동일성 비교 시 결과가 false로 나온다.

그래서 문자열은 언제나 동등성(equals)비교를 해야한다.

  • String의 동일성(==) 비교는 비교 대상인 문자열들이 어떻게 생성되었는지에 따라 결과가 다르다.

  • 내가 비교하려는 문자열이 리터럴로 생성되었는지, 객체 생성 방식으로 생성되었는지 언제나 알고 있을 수는 없는 법이다.
    특히 내가 만든 메서드를 다른 개발자가 사용한다면? 😲

  • 따라서 문자열은 항상 equals() 메서드를 통해 비교해야 한다.


2. String은 불변 객체

String은 불변객체이다.

  • value에 실제 값이 보관된다.

String은 불변객체이므로 중간에 값을 수정할 수 없다.

  • 기존 값의 수정이 필요한 경우, 새로운 문자열을 생성해야 한다.
  • 예를 들어, String hello = "Hello" 뒤에 문자를 추가해 "Hello Conew"로 만들고 싶은 경우,
    String helloConew = hello + " Conew"와 같은 방식으로 새로 결과를 만들어 반환한다.

String을 불변으로 설계한 이유

문자열 풀의 작동 방식을 생각해본다면, String을 불변으로 설계한 이유를 알 수 있다.

문자열의 값이 중간에 변화될 수 있다면
str1이 가리키고 있는 문자의 값이 "Coding" 으로 변했을 때,
str2가 가리키는 문자도 "Coding"으로 변하게 된다.
이러한 사이드 이펙트를 방지하고자 String은 불변으로 설계되었다.


3. String Builder는 가변 객체

StringBuilder는 ⭐️가변 String이다.

  • StringBuilder 클래스의 장점

    • String 클래스는 문자열을 수정할 수 없다. 따라서, 문자열이 변경될 때마다 새로운 String 객체를 생성해야 한다.
      따라서 성능과 메모리 사용면에서 비효율적이다.
    • 반면, StringBuilder 클래스로 만들어진 객체는 내부에서 값 수정이 가능하다. 따라서 성능과 메모리면에서 효율적이다.
    • 단, StringBuilder를 사용할 때는 사이드 이펙트를 주의해야 한다!

🔗 메서드 체이닝

메서드 체이닝(Method Chaining)은 메서드 호출을 연속으로 할 수 있게 하는 프로그래밍 기법이다.
메서드 체이닝이 가능한 메서드들은 호출된 객체의 참조값을 반환하기 때문에, 연속적으로 다음 메서드를 호출할 수 있다.

메서드 체이닝의 예시

// ChainingTimer.class
public class ChainingTimer {
    private int seconds = 0;

    public ChainingTimer tick() {
        seconds += 1;
        return this;
    }

    public ChainingTimer tock() {
        seconds += 1;
        return this;
    }

    public int getSeconds() {
        return seconds;
    }
}
  • 1초를 증가시키는 tick(), tock() 메서드는 호출한 객체를 리턴한다.
// ChainingTimerMain.class
public class ChainingTimerMain {
    public static void main(String[] args) {
        ChainingTimer chainingTimer = new ChainingTimer();
        chainingTimer.tick().tock().tick().tock().tick().tock();
        System.out.println(chainingTimer.getSeconds());
    }
}
결과
6

StringBuilder는 메서드 체이닝이 가능하다.

public class StringBuilderMain {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        String string = sb.append("Hello ")
                .append("I am ")
                .append("Conew")
                .reverse()
                .toString();
        System.out.println("string = " + string);
    }
}
결과
string = wenoC ma I olleH
  • StringBuilder는 메서드체이닝을 통해 편리하게 값을 수정하는 코드를 작성할 수 있다.
  • 수정이 완료되었고 더이상 변경사항이 없다면 toString()을 통해 String 객체로 변환시켜 사이드 이펙트를 방지한다!

4. Java의 String 최적화

Java는 문자열 결합 연산을 컴파일 타임에 처리한다. 따라서, 런타임에 실행될 연산을 줄여 성능을 높일 수 있다.
(간단한 결합 연산의 경우 신경쓰지 않아도 자바가 최적화를 잘 해줌)

컴파일 타임: 소스코드가 기계어로 변환되어 실행 가능한 프로그램 형태가 되는 과정 (컴파일 오류가 나면 IDE에서 바로 알아낼 수 있다)
런타임: 사용자가 프로그램을 실행하는 시점 (런타임 오류는 프로그램을 실행하기 전까지는 오류를 알아낼 수 없다)

String 최적화 예시

  1. 문자열 리터럴인 경우

    String str1 = "Hello " + "Conew";

    위 코드는 컴파일 타임에 아래와 같이 처리된다

    String str1 = "Hello Conew";
  2. 변수인 경우

String str3 = str1 + str2;

변수에 어떤 값이 들었는지 모르기에 아래와 같은 방식으로 처리된다.
(JDK9 이후부터는 StringConcatFactory를 이용한다.)

String str3 = new StringBuilder().append(str1).append(str2).toString();

String 최적화가 일어나지 않는 경우

반복문 내에 문자열 결합 연산이 있는 경우에는 최적화가 일어나지 않는다.
런타임이 되어서야 어떤 문자열들을 결합할지 알 수 있기 때문이다.

String result = "";
for (int i = 0; i < 1000; i++) {
	result += "Conew is the best ";
}

위 코드에서 for loop 내부의 문자열 결합 연산은 최적화가 일어나지 않는다.
따라서 루프를 1000번 돌면서 계속 새로운 문자열을 생성하게 된다.

반복문 내의 문자열 결합 연산을 효율적으로 처리하고 싶다면 StringBuilder를 사용하면 된다.

StringBuilder tmp = new StringBuilder();
for (int i = 0; i < 1000; i++) {
	tmp.append("Conew is the best ");
}
String result = tmp.toString();

StringBuilder 사용이 권장되는 경우

  • 위 예시처럼, 반복문 내부에서 문자열 결합 연산을 행할 때
  • 조건문을 통해 동적으로 문자열을 결합할 때
  • 복잡한 문자의 특정 부분을 수정할 때
  • 아주 긴 문자열을 다룰 때

+) StringBuffer?

StringBuilder를 알아보던 중, StringBuffer의 존재도 알게 되었다.

StringBuffer는 우리가 지금까지 알아본 StringBuilder와 유사한 역할을 수행하나, 멀티 쓰레드 환경에서 안전하게 사용할 수 있도록 동기화 관련 오버헤드를 갖고 있다.

즉, 멀티스레딩 환경에서는 값을 보장하는 안전한 StringBuffer를 써야 하나, 그 외의 경우에는 StringBuilder가 훨씬 효율적이다.


참고 사이트

profile
코뉴로그

0개의 댓글