본문 바로가기
자바/김영한의 실전 자바 - 중급 1편

김영한의 실전 자바 - 자바 중급 1편: 3. String 클래스

by limdae94 2024. 12. 3.

1. String 클래스 - 기본

자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있다. 기본형인 char는 문자 하나를 다룰 때 사용되며, char를 사용해 여러 문자를 나열하려면 char[] 사용해야 한다. 하지만 char[]를 직접 다루는 것은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는 String 클래스를 제공한다.

 

String 클래스 문자열 생성 방법

package lang.string;

public class StringBasicMain {
    public static void main(String[] args) {
        String str1 = "hello"; // 문자열 생성 방법 1
        String str2 = new String("hello"); // 문자열 생성 방법 2

        System.out.println("str1 = " + str1);
        System.out.println("str2 = " + str2);
    }
}
  • 쌍따옴표 사용: "hello"
  • 객체 생성: new String("hello")

문자열은 매우 자주 사용되서 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello")와 같이 변경해 준다.

2. String 클래스 구조

String 클래스는 대략 다음과 같이 생겼다.

public final class String {
    //문자열 보관
    private final char[] value;// 자바 9 이전
    private final byte[] value;// 자바 9 이후
    //여러 메서드
    public String concat(String str) {...}
    public int length() {...}
    ...
}

클래스이므로 속성과 기능을 가진다.

 

속성(필드)

private final char[] value

String의 실제 문자열 값이 보관된다(문자 데이터 자체는 char[]에 보관). String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공한다.

 

기능(메서드)

String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공한다.(기능이 방대하므로 필요한 기능을 검색하거나 API 문서를 찾아보는 것이 좋음).

 

주요 메서드

  • length(): 문자열의 길이를 반환한다.
  • charAt(int index): 특정 인덱스의 문자를 반환한다.
  • substring(int beginIndex, int endIndex): 문자열의 부분 문자열을 반환한다.
  • indexOf(String str): 특정 문자열이 시작되는 인덱스를 반환한다.
  • toLowerCase(), toUpperCase(): 문자열을 소문자 또는 대문자로 변환한다.
  • trim(): 문자열 양 끝의 공백을 제거한다.
  • concat(String str): 문자열을 더한다.

 

3. String 클래스와 참조형

String은 클래스로 기본형이 아니라 참조형이다. 참조형은 변수에 값이 아니라 참조값이 들어있어 원칙적으로 + 같은 연산을 사용할 수 없다.

package lang.string;

public class StringConcatMain {

    public static void main(String[] args) {
        String a = "hello";
        String b = " java";

        String result1 = a.concat(b);
        String result2 = a + b;

        System.out.println("result1 = " + result1);
        System.out.println("result2 = " + result2);
    }
}

자바에서 문자열을 더할 때는 String이 제공하는 concat()과 같은 메서드를 사용해야 하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공한다.

 

4. String 클래스 - 비교

String 클래스 비교는 == 비교 대신 equals() 비교를 해야 한다.

  • 동일성(Identity): == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
  • 동등성(Equality): equals() 메서드를 사용하여 두 객체가 논리적으로 같은지 확인
package lang.string.equals;

public class StringEqualsMain2 {

    public static void main(String[] args) {
        String str1 = new String("hello");
        String str2 = new String("hello");
        System.out.println("메서드 호출 비교1: " + isSame(str1, str2));

        String str3 = "hello";
        String str4 = "hello";
        System.out.println("메서드 호출 비교2: " + isSame(str3, str4));
    }

    private static boolean isSame(String x, String y) {
        //return x == y;
        return x.equals(y);
    }
}

 

String 비교

  • str1str2는 서로 다른 인스턴스이므로 동일성(==) 비교에 실패한다.
  • 둘은 내부에 같은 "hello" 값을 가지고 있기 때문에 논리적으로 같다. 따라서 동등성(equals()) 비교에 성공한다.

  • String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.
  • 자바 실행 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어두는데, 같은 문자열이 있으면 만들지 않는다.
  • 문자열 풀 덕에 같은 문자열을 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있다.

5. String 클래스 - 불변 객체

String은 불변 객체이므로 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없다.

package lang.string.immutable;

public class StringImmutable2 {

    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = str1.concat(" java");
        System.out.println("str1 = " + str1);
        System.out.println("str2 = " + str2);
    }
}

  • String.concat()은 내부에서 새로운 String 객체를 만들어서 반환하기 때문에 불변과 기존 객체의 값을 유지한다.

String이 불변으로 설계된 이유

문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경된다. String 내부의 값을 변경할 수 있다면 기존 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자까지 함께 변경되어 버리는 문제가 발생한다(사이드 이펙트 문제발생). 이러한 문제를 방지하기 위해 String 클래스는 불변으로 설계되었다.

6. String 클래스 - 주요 메서드

 

7. StringBuilder - 가변 String

 

불변인 String 클래스의 단점

불변인 String의 내부 값은 변경할 수 없음, 따라서 변경된 값을 기반으로 새로운 String 객체를 생성한다. 문자를 더하거나 변경할 때마다 계속해서 새로운 객체를 생성해야 하기 때문에 문자를 자주 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC 해야 한다. 결과적으로 CPU, 메모리 자원을 더 많이 사용하게 된다.

 

StringBuilder

String의 불변으로 인한 문제를 해결하기 위한 가변 String으로 내부의 값을 바로 변경하면 되기 때문에 객체를 생성할 필요가 없어 성능과 메모리 사용면에서 일반 String 보다 효율적이다(사이드 이펙트에 주의해서 사용해야 한다).

package lang.string.builder;

public class StringBuilderMain1 {

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append("A");
        sb.append("B");
        sb.append("C");
        sb.append("D");
        System.out.println("sb = " + sb);

        sb.insert(4, "Java");
        System.out.println("insert = " + sb);

        sb.delete(4, 8);
        System.out.println("delete = " + sb);

        sb.reverse();
        System.out.println("reverse = " + sb);

        // StringBuilder -> String
        String string = sb.toString();
        System.out.println("string = " + string);
    }
}
  • StringBuilder 객체를 생성한다.
  • append() 메서드로 여러 문자열을 추가한다.
  • insert() 메서드로 특정 위치에 문자열을 삽입한다.
  • delete() 메서드로 특정 범위의 문자열을 삭제한다.
  • reverse() 메서드로 문자열을 뒤집는다.
  • toString() 메서드를 사용해 StringBuilderString으로 변환한다.

 

 

8. String 최적화

 

9. 메서드 체이닝 - Method Chaining

package lang.string.chaining;

public class ValueAdder {

    private int value;

    public ValueAdder add(int addValue) {
        value += addValue;
        return this;
    }

    public int getValue() {
        return value;
    }
}
package lang.string.chaining;

public class MethodChainingMain3 {

    public static void main(String[] args) {
        ValueAdder adder = new ValueAdder();
        int result = adder.add(1).add(2).add(3).getValue();
        System.out.println("result = " + result);
    }
}

메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있다. 위 코드처럼 .을 찍고 메서드를 계속 연결해서 사용한다(마치 메서드가 체인으로 연결된 것처럼 보이고 이러한 기법을 메서드 체이닝이라 한다).

StringBuilder와 메서드 체인(Chain)

StringBuilder는 메서드 체이닝 기법을 제공한다. StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환한다.

앞서 StringBuilder를 사용한 코드는 다음과 같이 개선할 수 있다.

package lang.string.builder;

public class StringBuilderMain1_2 {

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        String string = sb.append("A").append("B").append("C").append("D")
                .insert(4, "Java")
                .delete(4, 8)
                .reverse()
                .toString();

        System.out.println("string = " + string);
    }
}