공부/Java

자바 String 클래스 복습하기

Stair 2024. 9. 19. 11:08
반응형

String 클래스-기본

자바에서 문자를 다루는 대표적인 타입은 char, String 두가지가 있다.

public class CharArrayMain {

    public static void main(String[] args) {
        char a = '가';
        System.out.println("a = " + a);

        char[] charArr = {'h','e','l','l','o'};

        System.out.println(charArr);

        String str = "hello";
        System.out.println(str);
    }
}

 

기본형인 char을 다루는 방법은  매우 불편하기 때문에 자바는 문자열을 편리하게 다룰 수 있는 String클래스를 제공한다.

 

String 클래스를 통해 문자열을 생성하는 방법은 두가지가 있다.

public class StringBasicMain {

    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = new String("hello");

        System.out.println("str1 = " + str1);
        System.out.println("str2 = " + str2);
    }
}

String은 클래스다. int, boolean같은 기본형이 아니라 참조형이다 .따라서 str1 변수에는 String 인스턴스의 참조값만 들어갈 수 있다.

따라서 다음 코드는 뭔가 어색하다.

    String str1 = "hello";

 

하지만 문자열은 매우 자주 사용되기 때문에 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello") 와 같이 변경해준다. ( 이 경우 실제로는 성능 최적화를 위해 문자열 풀을 사용하는데, 이 부분은 뒤에서 설명한다.)

 

 

 

제공되어지는 String 클래스는 대략 다음과 같이 생겼다.

@Stable
private final byte[] value;

 

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

 

private final char[] value;에는 String의 실제 문자열 값이 보관된다. 문자 데이터 자체는 char[]에 보관 된다.

String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공한다.

 

참고 : 자바9 이후 String 클래스에선 char[]대신 byte[]를 사용한다.

자바에서 문자 하나를 표현하는 char는 2byte를 차지한다. 단순이 영어 숫자는 보통 1byte로 표현이 가능하기 때문에 단순 영어, 숫자로만 표현된 경우 1byte를 사용하고, 그렇지 않은 나머지의 경우 2byte를 사용하게 구현되어있다.(메모리를 효율적으로 사용하기 위해 변경되었다.)

 

String 클래스는 참조형이라 원칙적으로 + 와 같은 연산은 사용할 수 없다.

x001 + x002 와 같이 참조값은 연산 불가능

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);
    }
}

result1 = hello java
result2 = hello java

근데 정상적으로 더해지는것을 볼 수 있다.

자바에서 문자열을 더할때는 원래 concat()과 같은 메서드를 사용해야 하나

문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공한다.

 

 

String 클래스 - 비교

String 클래스 비교할 때는 == 비교가 아닌 equals()비교를 해야 한다.

public static void main(String[] args) {
    String str1 = new String("hello");
    String str2 = new String("hello");

    System.out.println("new String()==비교: " + (str1 == str2));
    System.out.println("new String() equals 비교: " + str1.equals(str2));

}

new String()==비교: false
new String() equals 비교: true

 

str1,str2는 new String()을 사용해서 각각의 인스턴스를 생성했다. 이건 당연하다 객체끼리의 참조값이 다르기에 == 비교는 false가 나온다. 그리고 둘은 내부에 같은 "hello"라는 값을 가지고 있기 때문에 equals는

 

근데 아래 코드는 둘 다 true가 나오는것을 볼 수 있다.

String str3 = "hello";
String str4 = "hello";
System.out.println("리터럴 == 비교: " + (str3 == str4));
System.out.println("리터럴 equals 비교: " + str3.equals(str4));

리터럴 == 비교: true
리터럴 equals 비교: true

 

str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.

자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 String 인스턴스를 미리 만들어둔다. 이때 같은 문자열이 있으면 만들지 않는다.

str3, str4 모두 "hello"라는 문자열 리터럴을 사용하므로 같은 참조를 사용하게 된다 .문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있다.

 

 

 

String 클래스 - 불변객체

String 클래스는 불변객체이다. 따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없다.

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

str = hello

 

결과값은 hello Java가 아닌 그저 hello이다. 

String은 불변객체이기 때문에 변경이 필요한 경우 기존 값을 변경하지 않고, 새로운 결과를 만들어서 반환하여야 한다.

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);
    }
}

str1 = hello
str2 = hellojava

 

String이 불변으로 설계된 이유 : String이 불변으로 설계된 이유는 앞서 불변 객체에서 배운 내용에 추가로 다음과도 같은 이유가 있다.

문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경되게 된다.

이러한 문제를 해결하기 위해 String 클래스는 불변으로 설계되어서 이런 사이드 이펙트를 예방한다.

 

 

 

 

StringBuilder - 가변 String

불변인 String 클래스에도 단점이 있다.

문자열을 계속해서 더할 떄 변경된 값을 기반으로 새로운 String 객체를 생성하기 때문에 변경할 때마다 계속해서 새로운 객체를 생성해야 한다는 점이다. 문자를 자주 더하거나 변경해야하는 상황이라면 많은 String 객체를 만들고, GC해야 한다. 결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 된다. 그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모한다.

 

이 문제를 해결하는 방법은 단순하다. 바로 불변이 아닌 가변 String이 존재하면 된다. 가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없다.

 

자바는 StringBuilder라는 가변 String을 제공한다. 물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 한다.

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("sb = " + sb);

    sb.reverse();
    System.out.println("sb = " + sb);
    
    //StringBuilder -> String
    String string = sb.toString();
    System.out.println("string = " + string);
}

sb = Abcd
sb = AbcdJava
sb = avaJdcbA
string = avaJdcbA

 

가변이기때문에 객체를 새로 생성하지 않고 sb가 계속 변한다.

 

 

 

String 최적화

자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐준다.

 

String helloWorld = "Hello, " + "World!";

-->

String helloWorld="Hello, World!:";

 

문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없다.

따라서 StringBuilder가 사용된다.

 

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

 

가변인 상태로 문자열을 더한 후 다시 불변상태로 변환시키는 것이다.

 

하지만 String 최적화가 어려운 경우도 있다. 다음과 같은 경우이다.

public class LoopStringMain {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        String result ="";
        for (int i = 0; i < 100000; i++){
            result += "hello java";
        }
        long endTime = System.currentTimeMillis();

        System.out.println("result = " + result);
        System.out.println("time = " + (endTime - startTime));
    }
}

 

이 코드를 실행해보면 (내 컴퓨터 기준) time = 6479ms 가 걸렸다. 상당히 오래 걸렸다.

반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 한다.

반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결저오딘다. 이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없다. 따라서 이런 상황에서는 최적화가 어렵다.

 

StringBuilder를 사용하여 최적화를 진행했을땐

public class LoopStringBuilderMain {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100000; i++){
            sb.append("hello java");
        }
        long endTime = System.currentTimeMillis();

        String result = sb.toString();
        System.out.println("result = " + result);
        System.out.println("time = " + (endTime - startTime));
    }
}

time = 5 가 걸렸다.

 

StringBuilder를 직접 사용하는 것이 더 좋은 경우

1. 반복문을 반복해서 문자를 연결할때

2. 조건문을 통해 동적으로 문자열을 조합할 때

3. 복잡한 문자열의 특정 부분을 바꿔야할 때

4. 큰 문자열을 다루며 특정 부분을 수정하거나 바꿀때

 

 

메서드 체이닝

public class ValueAdder {

    private int value;

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

    public int getValue() {
        return value;
    }
}

단순히 값을 누적해서 더하는 기능을 제공하는 클래스이다.

add()메서드를 호출할 때마다 내부의 value 값을 누적한다.

add()메서드를 보면 자기 자신(this)의 참조값을 반환한다. 이 부분을 유의해서 보자. 

public class MethodChainingMain1 {

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

adder.add(1)을 호출하면 add()메서드는 결과를 누적하고 자기 자신의 참조값인 this(x001)을 반환한다.

adder.add(2)도 마찬가지가 되고, adder.add(3)도 마찬가지가 된다.

 

이 방식은 결국 자신을 반환값으로 던지기 때문에

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);
}

 

위와 같이 메서트 체이닝이 가능하게 된다.

add()메서드를 호출하면 인스턴스 자신의 참조값(x001)이 반환되기 때문에 이 반환된 참조값을 변수에 담아두지 않아도 되며, 반환된 참조값을 즉시 사용해서 메서드를 호출할 수 있다.

메서드 체이닝 방식은 메서드가 끝나는 시점에 바로 . 을 찍어 변수명을 생랷할 수 있다.

메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문에, 이 참조값을 가지고 .을 찍어 자신의 메서드를 호출할 수 있다.

 

StringBuilder는 메서드 체이닝 기법을 제공한다.

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();
    String string = sb.append("A").append("b")
            .append("c").append("d")
            .insert(4, "Java")
            .reverse().toString();
    System.out.println("string = " + string);
}

 

메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리해진다.

참고로 자바 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용한다.

 

 

반응형

'공부 > Java' 카테고리의 다른 글

자바 중첩 클래스, 내부 클래스 복습하기  (0) 2024.09.20
자바 열거형 ENUM 복습하기  (0) 2024.09.20
자바 불변 객체 복습하기  (1) 2024.09.18
자바 Object 클래스 복습하기  (1) 2024.09.18
자바 예외처리 2  (5) 2024.09.16