[김영한의 실전 자바 중급편1] - String

jkky98·2024년 5월 9일
0

Java

목록 보기
1/51

Type for string

자바에서 문자열을 다루는 대표적 타입 두 가지는 char, String이다. String 클래스는 맨 앞 글자가 대문자인 것 처럼 참조형이지만 자바에서 자주 사용하기에 편의상 리터럴 방식을 제공한다.

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

리터럴 방식이란?

리터럴 방식이란, 객체를 생성할 때 new 키워드를 사용하지 않고
직접 값을 써서 생성하는 방식을 말한다.

String s1 = "hello";        // 리터럴 방식
String s2 = new String("hello"); // 명시적 생성자 방식

String Class

실제로 String Class의 문자열은 char로 보관된다.
그리고 불변객체의 성질을 가진다.

그리고 편의를 위한 많은 메서드들을 지원한다.

  • length() : 길이 반환
  • charAt(int index) : 특정 인덱스의 문자 반환
  • substring(begin, end) : 문자열의 부분 문자열을 반환
  • indexOf(str) : 문자열 str이 시작되는 인덱스를 반환
  • toLowerCase(), toUpperCase() : 대,소문자 변환
  • trim() : 양 끝 공백 제거
  • concat(str) : 문자열 더하기

문자열 더하기

String은 참조형이므로 참조값을 담을 것이다. 일반적으로 참조값끼리의 덧셈은 성립이 불가능하다. 하지만 String은 덧셈을 지원한다. 예로 "hello", "java"끼리의 덧셈이 가능하다.

equals()

String 클래스는 equals() 메서드를 오버라이딩하여 문자열이 동등한지 검사하는 역할을 수행한다. 자바 생태계에서는 equals()==무조건적으로 equals() 사용을 권장한다.

이유

==는 참조형 변수에서 참조값(주소)을 비교한다. 따라서 문자열을 리터럴로 생성하지 않고 new String()으로 생성했을 경우, 같은 문자열이라도 참조값이 달라 동일성이 충족되지 않아 false를 반환한다.

반면, 리터럴로 생성된 동일한 문자열을 ==로 비교하면 true를 반환한다. 이는 리터럴 생성 시 자바가 String Pool을 사용하기 때문이다.

헷갈릴 여지가 있기에 안전한 동등성 비교, equals의 사용을 권장한다.

String Pool

  • 자바는 메모리 효율성과 성능 최적화를 위해 리터럴로 생성된 문자열String Pool이라는 특수한 메모리 영역에 저장한다.
  • 동일한 리터럴 문자열이 생성되면, String Pool에서 기존 문자열을 참조하도록 처리하여 중복된 문자열 객체 생성을 방지한다.

따라서, 문자열의 동등성을 비교할 때 equals()를 사용하면, 리터럴 생성 여부와 관계없이 문자열 값 자체를 비교할 수 있어 더 안정적이고 일관성 있는 결과를 얻을 수 있다.

String - 불변 객체

String은 불변 객체로 설계되어 있다.

이는 이전에 배운 것처럼, String클래스의 대부분의 메서드가 새로운 String 객체를 생성하여 반환한다는 것을 의미한다.

String str1 = "hello";
String strUpper = str1.toUpperCase();

예를 들어, 문자열을 대문자로 변환하는 toUpperCase() 메서드를 호출하면 기존 문자열이 변경되지 않고, 새로운 문자열 객체가 생성되어 반환된다. 따라서, 다른 변수에 반환된 값을 할당해야 할 상황이 많다.

불변 객체로 설계된 이유

String이 불변 객체로 설계된 이유는 String Pool과 관련이 깊다.

만약 불변 객체가 아니라면, 동일한 리터럴을 참조하는 사이드 이펙트가 발생할 수 있다.

예를 들어, 동일한 "Apple" 문자열을 참조하는 두 인스턴스가 있다고 가정해보자.

이 상황에서 한 인스턴스가 toUpperCase()를 호출하여 "Apple""APPLE"로 변경된다면, 다른 인스턴스도 예상치 못하게 영향을 받을 것이다.(즉 다른 인스턴스는 APPLE이 되는걸 원하지 않았지만 APPLE로 변경된 것을 발견하게 된다.)

이러한 문제를 방지하기 위해 String은 불변 객체로 설계되었으며, 이를 통해 문자열 공유로 인한 부작용을 원천적으로 차단할 수 있다. 불변 객체는 특히 멀티스레드 환경에서도 안전성을 보장하는 데 큰 이점을 제공한다.

StringBuilder

불변의 특성을 가지는 String의 단점은 객체 생성 비용이다. 특정 String 변수가 자주 변경된다면, 변경될 때마다 새로운 객체를 생성해야 하므로 성능이 저하될 수 있다. 이러한 단점을 보완하기 위해 도입된 것이 가변 객체StringBuilder 클래스이다.

StringBuilder의 특징

  • 문자열을 변경 가능한 객체로 관리하여, 문자열 조작 시 새로운 객체를 생성하지 않는다.
  • 문자열 추가, 삭제, 수정 등의 작업에서 성능이 뛰어나다.

주의점

  • StringBuilder는 가변 객체이므로, 사이드 이펙트가 발생할 가능성이 있다. 따라서, 멀티스레드 환경에서는 신중하게 사용하거나 StringBuffer처럼 동기화를 지원하는 클래스를 고려해야 한다.

StringBuilder의 효과

StringBuilder의 효용성을 제대로 이해하려면 먼저 불변 객체의 개념을 학습하는 것이 중요하다. 불변 객체의 단점을 알고 나면, StringBuilder가 객체 생성 비용을 절감하고 성능을 향상시키는 데 얼마나 유용한지 공감할 수 있다.

String 최적화

String의 덧셈을 생각해보자.

String a = "Hello" + " World!"
// 실제 덧셈은 아래 처럼 이루어진다.
String a = new StringBuilder().append("Hello").append( "World!!").toString();

String의 덧셈은 자바가 위와 같이 처리한다. 왜 .concat()으로 처리하지 않을까?

문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기에 위와 같이 최적화하여 처리한다.

그럼 자바가 결국 알아서 최적화해준다는 것인데...

우리가 추가적으로 어떠한 최적화 작업을 해줄 수 있다는 걸까?

밑의 코드를 보며 그것을 알아보자.

String 덧셈이 이루어진다.

public class LoopMain1 {
    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) + "ms");
    }
}

result라는 빈 문자열에 약 10만번의 "Hello Java"를 for문을 통해 추가한다.

위에서 보듯 아마 자바는 StringBuilder를 10만번 만들고 뒤에 따르는 계산들도 동일한 반복을 가질 것이다.

실제로 이를 돌렸을 때
매우 느린 속도를 보여준다.

우리는 이러한 문제를 직접 StringBuilder을 이용함으로 해결가능하다.

package lang.string.builder;

public class LoopMain2 {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        StringBuilder result = new StringBuilder();
        for (int i=0; i < 100000; i++) {
            result.append("Hello Java");
        }
        String resultStr = result.toString();
        long endTime = System.currentTimeMillis();
        System.out.println("result = " + result);
        System.out.println("time = " + (endTime - startTime) + "ms");
    }
}

StringBuilder를 직접 사용한다. 한번의 StringBuilder 생성만 시도하며 10만 번의 append를 수행한다.

10만 번의 append 작업 후 toString()을 호출하여 최종 문자열을 생성할 수 있다. 이를 통해, 객체를 생성하는 횟수를 극히 줄여 불변 String의 단점을 효과적으로 상쇄할 수 있다.

즉, 가변 String의 장점을 활용하여 불변 String의 단점을 보완하는 것이다. 이러한 방식으로 리팩토링하면, 문자열 조작이 많은 상황에서 다음과 같은 속도 향상을 기대할 수 있다:

  • 객체 생성 비용 절감
  • 메모리 사용 효율 증가
  • 실행 시간 단축

이처럼 StringBuilder는 성능 최적화가 중요한 문자열 조작 작업에서 매우 유용한 도구이다.

StringBuilder 직접 사용이 더 좋을 경우

  • 반복문에서 반복해서 문자를 연결할 때
  • 조건문을 통해 동적으로 문자열을 조합할 때
  • 복잡한 문자열의 특정 부분을 변경해야할 때
  • 매우 긴 대용량 문자열을 다룰 때

Method Channing

package lang.string.builder;

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

        System.out.println(str);

    }
}

StringBuilder의 메서드는 호출 후 자기 자신을 반환한다. 즉, 새로운 객체를 생성하지 않고, 변경된 자신의 참조를 반환하기 때문에 메서드 호출을 연속적으로 연결할 수 있다.

이러한 특징 덕분에 StringBuilder의 여러 메서드를 체인처럼 엮어 사용할 수 있다. 이를 메서드 체이닝(Method Chaining)이라고 하며, 코드를 더욱 간결하고 읽기 쉽게 만들어준다.

메서드 체이닝의 장점

  • 가독성 향상: 불필요한 변수 선언 없이 연속적인 작업을 하나의 표현식으로 처리 가능.
  • 코드 간소화: 중복된 코드를 줄이고, 작업 흐름을 직관적으로 표현.

StringBuilder의 이러한 설계는 성능뿐만 아니라 코드의 유지보수성에도 긍정적인 영향을 준다.

profile
자바집사의 거북이 수련법

0개의 댓글