처음부터 차근차근

[Java] String 클래스 본문

Language/Java

[Java] String 클래스

HangJu_95 2024. 7. 31. 15:39
728x90

String 클래스란??

자바에서 문자를 대표적인 타입은 char, String 2가지가 존재한다.

기본형인 char 는 문자 하나를 다룰 때 사용한다. char 를 사용해서 여러 문자를 나열하려면 char[] 을 사용해야 한다. 하지만 이렇게 char[] 을 직접 다루는 방법은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는String 클래스를 제공한다.

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

package lang.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은 클래스이다. 하지만 문자열을 매우 편하게 다루기 위해 자바 언어에서는 1번을 2번과 같이 자동으로 변경해준다.

String 클래스는 참조형이다.

참조형은 변수에 계산할 수 있는 값이 들어있는 것이 아니라, x001과 같이 계산할 수 없는 참조값이 들어있다. 따라서 원칙적으로 + 와 같은 연산을 사용할 수 없다.

String a = "hello";
String b = " java";

String result1 = a.concat(b);
String result2 = a + b; // 문자열은 너무 자주 다루어지기 때문에 편의상 특별히 + 연산 제공

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

String 클래스 비교하기

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

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

public class StringEqualsMain1 {
    public static void main(String[] args) {
        String str1 = new String("hello"); // x001
        String str2 = new String("hello"); // x002

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

        String str3 = "hello"; // x003
        String str4 = "hello"; // x004

        // 왜 리터럴로 하면 True가 나오는가?
        // 리터럴로 선언하면 같은 문자열이 있는지 먼저 확인하고 있으면 같은 주소를 참조하게 한다.
        // 문자열 리터럴, 문자열 풀을 사용한다.
        System.out.println("리터럴 == 비교: " + (str3 == str4));
        System.out.println("리터럴 equals 비교: " + (str3.equals(str4)));
    }
}

실행 결과

new String() == 비교: false
new String() equals 비교: true
리터럴 == 비교: true
리터럴 equals 비교: true

왜 객체로 생성했을 때는 다르다고 나오는데, 리터럴로 비교하면 같다고 할까??
그 이유는 문자열 풀 떄문에 그렇다.

참조: 김영한 자바 중급 1

str3와 같이 문자열 리터럴을 사용하는 경우, 자바는 메모리 효율과 성능 최적화를 위해 문자열 풀을 사용한다.
자바는 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어두며, 같은 문자열이 있으면 만들지 않는다. str3과 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조(x003)을 반환한다.
str4의 경우 "hello" 문자열 리터럴을 사용하므로, 문자열 풀에서 str3과 같은 x003 참조를 사용한다.

따라서, 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용도 줄이고, 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있다.

그렇다면 문자열 리터럴만 사용하면 되지 않는가??

다음 예시 코드를 보자.

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

main 메서드를 만드는 개발자와 isSame() 메서드를 만드는 개발자가 서로 다르다고 할 경우.
isSame에는 객체로 String이 넘어올지, 문자열 리터럴로 넘어올 지 알 수 없다.

따라서 문자열 비교는 항상 equals()를 사용해서 동등성 비교를 해야 한다.

String 클래스는 불변 객체이다.

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

package lang.string.immutable;

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

concat으로 합쳐봤지만, 출력시에는 hello만 나온다. 이는 concat이 기존 문자열과 신규 문자열을 합친 이후에 새로운 문자열을 반환하기 때문이다.

그렇다면 왜 불변으로 만든 것인가??
문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경되기 때문이다.

만약 String 내부의 값을 변경할 수 있다면, 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제가 발생한다. 다음의 경우 str3 이 참조하는 문자를 변경하면 str4 의 문자도 함께 변경되는 사이드 이펙트 문제가 발생한다.

문자열 주요 메서드

  • 문자열 정보 조회
    length() : 문자열의 길이를 반환한다.
    isEmpty() : 문자열이 비어 있는지 확인한다. (길이가 0)
    isBlank() : 문자열이 비어 있는지 확인한다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11
    charAt(int index) : 지정된 인덱스에 있는 문자를 반환한다.
  • 문자열 비교
    equals(Object anObject) : 두 문자열이 동일한지 비교한다.
    equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교한다.
    compareTo(String anotherString) : 두 문자열을 사전 순으로 비교한다.
    compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교한다.
    startsWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인한다.
    endsWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인한다.
  • 문자열 검색
    contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인한다.
    indexOf(String ch) / indexOf(String ch, int fromIndex) : 문자열이 처음 등장하는 위치를 반환한다.
    lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환한다.
  • 문자열 조작 및 변환
    substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환한다.
    concat(String str) : 문자열의 끝에 다른 문자열을 붙인다.
    replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체한다.
    replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체한다.
    replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체한다.
    toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환한다.
    trim() : 문자열 양쪽 끝의 공백을 제거한다. 단순 Whitespace 만 제거할 수 있다.
    strip() : Whitespace 와 유니코드 공백을 포함해서 제거한다. 자바 11
  • 문자열 분할 및 조합
    split(String regex) : 문자열을 정규 표현식을 기준으로 분할한다.
    join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합한다.
  • 기타 유틸리티
    valueOf(Object obj) : 다양한 타입을 문자열로 변환한다.
    toCharArray(): 문자열을 문자 배열로 변환한다.
    format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성한다.
    matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인한다.

StringBuilder - 가변 String

불변인 String클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점이다.
문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 한다. 따라서 시스템 자원을 더 많이 소모한다.

이러한 문제를 해결하기 위해 StringBuilder라는 가변 String이 존재한다.
이를 통해 성능과 메모리 사용면에서 불변보다 더욱 효율적이다. (단, 가변이기 때문에 사이드 이펙트에 주의해서 사용해야 한다.)

StringBuilder 는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String 으로 변환하는 것이 좋다.

StringBuilder 사용 예시

package lang.string.builder;

public class StringBuilderMain1_1 {
    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);
    }
}

실행 결과

sb = ABCD
insert = ABCDJava
delete = ABCD
reverse = DCBA
string = DCBA

String 최적화

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

String result = str1 + str2;
// 다음과 같이 최적화를 시켜준다.
String result = new StringBuilder().append(str1).append(str2).toString();

참고로 자바 9부터는 StringConcatFactory를 사용해서 최적화를 수행한다.

이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 된다. 대신에 문자열 더하기 연산을 사용하면 충분하다.

String 최적화가 어려운 경우

다음과 같이 루프 안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않는다.

package lang.string.builder;

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) + "ms");
    }
}

반복문의 루프 내부에서는 최적화되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 한다.
이런 경우는 StringBuilder를 사용하면 된다.

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

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

참고: StringBuilder vs StringBuffer
StringBuilder 와 똑같은 기능을 수행하는 StringBuffer 클래스도 있다.
StringBuffer 는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느리다.
StringBuilder 는 멀티 쓰레드에 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠르다.

Method Chaining

단순한 코드로 메서드 체이닝에 대해 알아보자.

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

실행 결과

adder.add(1).add(2).add(3).getValue() //value=0
x001.add(1).add(2).add(3).getValue() //value=0, x001.add(1)을 호출하면 그 결과로 x001을 반환한다.
x001.add(2).add(3).getValue() //value=1, x001.add(2)을 호출하면 그 결과로 x001을 반환한다.
x001.add(3).getValue() //value=3, x001.add(3)을 호출하면 그 결과로 x001을 반환한다.
x001.getValue() //value=6
6

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

 

참조

 

김영한의 실전 자바 - 중급 1편 강의 | 김영한 - 인프런

김영한 | 실무에 필요한 자바의 다양한 중급 기능을 예제 코드로 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을

www.inflearn.com

'Language > Java' 카테고리의 다른 글

[Java] Enum  (0) 2024.08.09
[Java] Wrapper Class  (0) 2024.07.31
[Java] 불변 객체  (0) 2024.07.26
[Java] Object 클래스  (0) 2024.07.24
[Java] Interface  (1) 2024.01.02