관리 메뉴

나구리의 개발공부기록

String 클래스(기본, 비교, 불변 객체, 주요 메서드), StringBuilder - 가변 String, String 최적화, 메서드 체이닝(Method Chaining) 본문

인프런 - 실전 자바 로드맵/실전 자바 - 중급 1편

String 클래스(기본, 비교, 불변 객체, 주요 메서드), StringBuilder - 가변 String, String 최적화, 메서드 체이닝(Method Chaining)

소소한나구리 2025. 1. 18. 17:01

출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. String 클래스

1) 기본

(1) CharArrayMain

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

public class CharArrayMain {
    public static void main(String[] args) {
        char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'};
        System.out.println(charArr);

        String str = "hello";
        System.out.println("str = " + str);
    }
}
/* 실행 결과
hello
str = hello
*/

 

(2) StringBasicMain

  • String클래스를 통해 문자열을 생성하는 방법은 2가지가 있음
  • 쌍따옴표 사용: "hello"
  • 객체 생성: new String("hello")
  • String은 클래스이기 때문에 참조형이므로 str1, str2 변수에는 String 인스턴스의 참조값만 들어갈 수 있지만 기본형처럼도 입력할 수 있음
  • 문자열은 매우 자주 사용되기 때문에 편의상 기본형처럼 입력할 수 있도록 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello")와 같이 변경해줌
  • 그리고 성능 최적화를 위해 문자열 풀을 사용하는데 이부분은 뒤에서 설명함
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);
    }
}

 

(3-1) String 클래스 구조

  • String 클래스는 간략하게만 보면 아래처럼 생겼으며 클래스이므로 속성(필드)와 기능(메서드)를 가짐
  • 자바9 이전에는 char[]을 가지고 있었으나 자바9 이후에는 byte[]을 가지고 있음
public final class String {

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

 

(3-2) String 클래스 속성(필드)

  • private final char[] value
  • String의 실제 문자열값이 보관되며 문자 데이터 자체는 char[]에 보관됨
  • String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 편리하게 사용할 수 있도록 다양한 기능을 제공하며 자바 언어 차원에서도 여러 편의 문법을 제공함

** 참고

  • 자바 9 이후부터 String 클래스에서 char[] 대신에 byte[]를 사용함
  • 자바에서 문자 하나를 표현하는 char는 2byte를 차지하는데 영어, 숫자는 1byte로 표현이 가능하기 때문에 1byte를 사용하고(Latin-1 인코딩의 경우에만), 나머지의 경우는 2byte인 UTF-16 인코딩을 사용하여 메모리를 더 효율적으로 사용할 수 있도록 변경되었음

(3-3) String 클래스 기능(메서드)

  • String 클래스는 문자열로 처리할 수 있는 매우 다양한 기능을 제공하므로 필요한 기능이 있으면 검색하거나 API 문서를 찾아서 사용하면 됨
  • 메서드의 자세한 내용은 뒤에서 다시 배우므로 주요 메서드 몇개만 작성
  • length(): 문자열의 길이 반환
  • charAt(int index): 특정 인덱스의 문자를 반환
  • substring(int beginIndex, int endIndex): 문자열의 부분 문자열을 반환
  • indexOf(String str): 특정 문자열이 시작되는 인덱스를 반환
  • toLowerCase(), toUpperCase(): 문자열을 소문자 혹은 대문자로 변환
  • trim(): 문자열 양 끝의 공백을 제거
  • concat(String str): 문자열을 더함

(4) StringConcatMain - String 클래스와 참조형

  • String은 클래스이므로 기본형이 아니라 참조형이므로 원칙적으로 + 같은 연산을 사용할 수 없으므로 String 클래스에서 제공하는 concat()과 같은 메서드를 사용해야함
  • 그러나 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공함
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);
    }
}
/* 실행 결과
result1 = Hello Java
result2 = Hello Java
*/

2) 비교

(1) StringEqualsMain1

  • String 클래스를 비교할 때는 동일성 비교인 == 비교가 아니라 항상 equals() 메서드로 동등성 비교를 해야함
  • new String()으로 동일한 문자열을 값으로 생성한 객체들을 == 비교를 하면 당연히 false가 나오지만 String 클래스의 equals()는 오버라이딩이 되어 있기 때문에 equals() 비교를 하면 true가 나옴
  • 그러나 더 재밌는것은 String은 클래스이므로 참조형이지만 == 비교를 해도 true가 됨
package lang.String.equals;

public class StringEqualsMain1 {

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

        String str3 = "hello";
        String str4 = "hello";
        System.out.println("리터럴 == 비교: " + (str3 == str4));
        System.out.println("리터럴 equals 비교: " + (str3.equals(str4)));
    }
}
/* 실행 결과
new String() == 비교: false
new String() equals 비교: true
리터럴 == 비교: true
리터럴 equals 비교: true
*/

 

(1-2) new String() 비교

  • str1과 str2는 new String()을 사용하여 각각 인스턴스를 생성했기 때문에 서로 다른 인스턴스이므로 동일성 실패에 비교함
  • 둘은 내부에 같은 "hello" 값을 가지고 있기 때문에 논리적으로 같고 String 클래스가 내부 문자열 값을 비교하도록 equals()메서드를 재정의 해두었기 때문에 동등성(equals()) 비교에 성공함

(1-3) 문자열 리터럴, 문자열 풀

  • String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용함
  • 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어두는데 이때 같은 문자열이 있으면 만들지 않음
  • String str3 = "hello" 처럼 문자열을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾은 뒤 인스턴스의 참조값을 반환함
  • String str4 = "hello" 로 한번더 입력해도 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 찾은 인스턴스의 참조값을 반환하게 되어 str3와 str4는 같은 참조값을 사용함
  • 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있음
  • 문자열 리터럴을 사용하면 같은 참조값을 가지기 때문에 == 비교에 성공함

** 참고

  • 풀(Pool)은 자원이 모여있는 곳을 의미하며 프로그래밍에서 풀은 공용 자원을 모아둔 곳을 뜻함
  • 여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고 제거하는 것은 비효율적이므로 이렇게 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 최적화 할 수 있음
  • 문자열 풀은 힙 영역을 사용하며 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있음
  • 해시 알고리즘은 컬렉션에서 설명함

(2) StringEqualsMain2

    • 위의 내용들을 근거로 문자열 리터럴을 사용하면 == 비교를 하고 new String()을 직접 사용하는 경우에는 equals() 비교를 사용하면 될거라고 생각할 수 있는데 그러면 안됨
    • main() 메서드를 만드는 개발자와 isSame() 메서드를 만드는 개발자가 서로 다르다고 가정해보면 isSame()의 매개변수로 넘어오는 String 인스턴스가 new String()으로 만들어졌는지, 문자열 리터럴로 만들어진 것인지 확인할 수 있는 방법이 없음
    • 즉, 문자열 비교는 항상 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 str1, String str2) {
        return str1 == str2;
//        return str1.equals(str2);
    }
}
/* 실행 결과
메서드 호출 비교1: false
메서드 호출 비교2: true
*/

 

3) 불변 객체

(1) StringImmutable1

  • String 클래스 내부에 들어가보면 필드가 final로 되어있는 불변 객체이므로 절대로 내부의 문자열 값을 변경할 수 없음
  • String이 제공하는 concat() 메서드를 사용하면 기존 문자열에 새로운 문자열을 연결해서 합칠 수 있는데, 실행 결과를 보면 문자가 전혀 합쳐지지 않았음
package lang.String.immutable;

public class StringImmutable1 {
    public static void main(String[] args) {
        String str = "hello";
        str.concat(" java");
        System.out.println("str = " + str);
    }
}
/* 실행 결과
str = hello
*/

 

(2) StringImmutable2

  • String은 불변 객체이기 때문에 변경이 필요한 경우 기존값을 변경하지 않고 새로운 결과를 만들어서 반환함(이 내용은 바로 이전의 강의의 불변객체에서 배움)
  • String이 제공하는 concat()메서드의 내부를 보면 새로운 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);
    }
}
/* 실행 결과
str1 = hello
str2 = hello java
*/

 

(3) String이 불변으로 설계된 이유

  • String 클래스가 만약 가변이라면 문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경되게 됨
  • String은 자바 내부에서 문자열 풀을 통해 최적화를 하는데 만약 String 내부의 값을 변경할 수 있다면 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 사이드 이펙트 문제가 발생하게 됨
  • String 클래스가 불변으로 설계되었기 때문에 이런 사이드 이펙트 문제가 발생하지 않음

4) 주요 메서드

** 참고

  • String 클래스는 문자열을 편리하게 다루기 위한 다양한 메서드를 제공하는데 여기서는 자주 사용하는 기능 위주로 나열
  • 기능이 너무 많기 때문에 외우기보다는 주로 사용하는 메서드가 어떤것이 있는지 대략 알아두고 필요할 때 검색을 통해 원하는 기능을 찾는것이 좋음

(1) 문자열 정보 조회 - StringInfoMain

  • length(): 문자열의 길이를 반환
  • isEmpty(): 문자열이 비어있는지 확인, 길이가 0
  • isBlank(): 문자열이 비어있는지 확인, 길이가 0이거나 공백만 있는경우, 자바 11부터 지원
  • charAt(int index): 지정된 인덱스에 있는 문자를 반환
package lang.String.method;

public class StringInfoMain {
    public static void main(String[] args) {
        String str = "Hello, Java!";
        System.out.println("문자열의 길이: " + str.length());
        System.out.println("문자열이 비어 있는지: " + str.isEmpty());
        System.out.println("문자열이 비어 있거나 공백인지 : " + str.isBlank());
        System.out.println("문자열이 비어 있거나 공백인지 : " + "            ".isBlank());

        char c = str.charAt(7);
        System.out.println("7번 인덱스의 문자 = " + c);
    }
}
/* 실행 결과
문자열의 길이: 12
문자열이 비어 있는지: false
문자열이 비어 있거나 공백인지 : false
문자열이 비어 있거나 공백인지 : true
7번 인덱스의 문자 = J
*/

 

(2) 문자열 비교 - StringComparisonMain

  • equals(Object anObject): 두 문자열이 동일한지 비교
  • equalsIgnoreCase(String anotherString): 두 문자열을 대소문자 구분없이 비교
  • compareTo(String anotherString): 두 문자열을 사전 순으로 비교한 후 다른 단어를 만나면 그 차이를 int로 반환, 
  • compareToIgnoreCase(String str): 위와 동일한 기능을 대소문자 구분없이 비교
  • startsWith(String prefix): 문자열이 특정 접두사로 시작하는지 확인
  • endsWith(String suffix): 문자열이 특정 접미사로 끝나는지 확인
package lang.String.method;

public class StringComparisonMain {
    public static void main(String[] args) {
        String str1 = "Hello, Java!";
        String str2 = "hello, java!";
        String str3 = "Hello, World!";

        System.out.println("str1 equals str2: " + str1.equals(str2));
        System.out.println("str1 equals str2: " + str1.equalsIgnoreCase(str2));

        System.out.println("'b' compareTo 'a': " + "b".compareTo("a"));
        System.out.println("'c' compareTo 'a': " + "c".compareTo("a"));
        System.out.println("str1 compareTo str3: " + str1.compareTo(str3));
        System.out.println("str1 compareToIgnoreCase str2: " + str1.compareToIgnoreCase(str2));

        System.out.println("str1 starts with 'Hello': " + str1.startsWith("Hello"));
        System.out.println("str1 ends with 'Java!': " + str1.endsWith("Java!"));
    }
}
/* 실행 결과
str1 equals str2: false
str1 equals str2: true
'b' compareTo 'a': 1
'c' compareTo 'a': 2
str1 compareTo str3: -13
str1 compareToIgnoreCase str2: 0
str1 starts with 'Hello': true
str1 ends with 'Java!': true
*/

 

(3) 문자열 검색 - StringSearchMain

  • contains(CharSequence s): 문자열이 특정 문자열을 포함하고 있는지 확인
  • indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환
  • lastIndexOf(String ch): 문자열이 마지막으로 등장하는 위치를 반환
package lang.String.method;

public class StringSearchMain {
    public static void main(String[] args) {
        String str = "Hello, Java! Welcome to Java world.";

        System.out.println("문자열에 'Java'가 포함되어 있는지: " + str.contains("Java"));
        System.out.println("'Java'의 첫 번째 인덱스: " + str.indexOf("Java"));
        System.out.println("인덱스 10부터 'Java'의 인덱스: " + str.indexOf("Java", 10));
        System.out.println("'Java'의 마지막 인덱스: " + str.lastIndexOf("Java"));
    }
}
/* 실행 결과
문자열에 'Java'가 포함되어 있는지: true
'Java'의 첫 번째 인덱스: 7
인덱스 10부터 'Java'의 인덱스: 24
'Java'의 마지막 인덱스: 24
*/

 

(4) 문자열 조작 및 변환 - StringChangeMain

  • 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부터 지원
package lang.string.method;

public class StringChangeMain {
    public static void main(String[] args) {
        String str = "Hello, Java! Welcome to Java";

        System.out.println("인덱스 7부터 부분 문자열: " + str.substring(7));
        System.out.println("인덱스 7부터 12까지의 부분 문자열: " + str.substring(7, 12));

        System.out.println("문자열 결합: " + str.concat("!!!"));

        System.out.println("'Java'를 'World'로 대체: " + str.replace("Java", "World"));
        System.out.println("첫 번째 'Java'를 'World'로 대체: " + str.replaceFirst("Java", "World"));

        String strWithSpace = "      Java Programming      ";
        System.out.println("소문자로 변환: " + strWithSpace.toLowerCase());
        System.out.println("대문자로 변환: " + strWithSpace.toUpperCase());

        System.out.println("공백 제거(trim): '" + strWithSpace.trim() + "'");
        System.out.println("공백 제거(strip): '" + strWithSpace.strip() + "'");
        System.out.println("앞 공백 제거(strip): '" + strWithSpace.stripLeading() + "'");
        System.out.println("뒤 공백 제거(strip): '" + strWithSpace.stripTrailing() + "'");

    }
}

/* 실행 결과
인덱스 7부터 부분 문자열: Java! Welcome to Java
인덱스 7부터 12까지의 부분 문자열: Java!
문자열 결합: Hello, Java! Welcome to Java!!!
'Java'를 'World'로 대체: Hello, World! Welcome to World
첫 번째 'Java'를 'World'로 대체: Hello, World! Welcome to Java
소문자로 변환:       java programming      
대문자로 변환:       JAVA PROGRAMMING      
공백 제거(trim): 'Java Programming'
공백 제거(strip): 'Java Programming'
앞 공백 제거(strip): 'Java Programming      '
뒤 공백 제거(strip): '      Java Programming'
*/

 

(5) 문자열 분할 및 조합

  • split(String regex): 문자열을 정규 표현식을 기준으로 분할
  • join(CharSequence delimiter, CharSequence... elements): 주어진 구분자로 여러 문자열을 결합
package lang.String.method;

public class StringSplitJoinMain {
    public static void main(String[] args) {
        String str = "Apple,Banana,Orange";

        // split()
        String[] splitStr = str.split(",");
        for (String s : splitStr) {
            System.out.println(s);
        }

        // join()
        String joinedStr = String.join("-", "Apple", "Banana", "Orange");
        System.out.println("연결된 문자열 = " + joinedStr);

        // 문자열 배열 연결
        String result = String.join("-", splitStr);
        System.out.println("result = " + result);
    }
}
/* 실행 결과
Apple
Banana
Orange
연결된 문자열 = Apple-Banana-Orange
result = Apple-Banana-Orange
*/

 

(6) 기타

  • valueOf(Object obj): 다양한 타입을 문자열로 변환, 빈 문자열과 + 연산을 하여 간단히 문자열로 변환할 수 있음
  • toCharArray(): 문자열을 문자 배열로 변환
  • format(String format, Object... args): 형식 문자열과 인자를 사용하여 새로운 문자열을 생성
  • matches(String regex): 문자열이 주어진 정규 표현식과 일치하는지 확인
package lang.String.method;

public class StringUtilsMain {
    public static void main(String[] args) {
        int num = 100;
        boolean bool = true;
        Object obj = new Object();
        String str = "Hello, Java!";

        // valueOf 메서드
        String numString = String.valueOf(num);
        System.out.println("숫자의 문자열 값" + numString);
        String boolString = String.valueOf(bool);
        System.out.println("불리언의 문자열 값 " + boolString);
        String objString = String.valueOf(obj);
        System.out.println("객체의 문자열 값 " + objString);

        // 문자 + x -> 문자
        String numString2 = "" + num;
        System.out.println("빈 문자열 + num: " + numString2);

        char[]  strCharArray = str.toCharArray();
        System.out.println("문자열을 문자 배열로 전환: " + strCharArray);
        for (char c : strCharArray) {
            System.out.print(c);
        }
        System.out.println();

        // format 메서드
        String format1 = String.format("num: %d, bool: %b, str: %s", num, bool, str);
        System.out.println(format1);

        String format2 = String.format("숫자: %.2f", 10.1234);
        System.out.println(format2);

        // printf
        System.out.printf("숫자: %.2f\n", 10.1234);

        // matches 메서드
        String regex = "Hello, (Java!|World!)";
        System.out.println("'str'이 패턴과 일치하는가? " + str.matches(regex));
    }
}
/* 실행 결과
숫자의 문자열 값100
불리언의 문자열 값 true
객체의 문자열 값 java.lang.Object@23fc625e
빈 문자열 + num: 100
문자열을 문자 배열로 전환: [C@3f99bd52
Hello, Java!
num: 100, bool: true, str: Hello, Java!
숫자: 10.12
숫자: 10.12
'str'이 패턴과 일치하는가? true
*/

 

** 참고

  • 정규 표현식 위키백과
  • 정규 표현식은 별도로 공부가 필요함
  • CharSequence는 String, StringBuilder의 상위 타입이며 문자열을 처리하는 다양한 객체를 받을 수 있음

2. StringBuilder - 가변 String

1) StringBuilder

(1) 불변인 String 클래스의 단점

String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D");
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");
  • String클래스는 불변이기 때문에 문자를 더하거나 변경할 때마다 계속해서 새로운 객체를 생성해야함
  • 문자열 A, B, C, D를 더하게 되면 문자열 ABCD가 생성되는데, 그 과정속에서 만들어지는 AB, ABC는 사용되지 않으므로 가비지컬렉터의 대상이 됨
  • 즉, 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String객체를 만들고 GC해야하기 때문에 결과적으로 컴퓨터의 CPU나 메모리 자원을 더 많이 사용하게 되며 문자열의 크기가 크거나 더 자주 변경할 수록 시스템의 자원은 더 많이 소모하게 됨

** 참고

  • 실제로는 문자열을 다룰 때 자바가 내부에서 최적화를 적용하는데, 이 부분은 뒤에서 다룸

(2) StringBuilder 구조 일주

  • 이 문제를 해결하는 방법은 단순히 불변이 아닌 가변 String이 존재하면 됨
  • 가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없으므로 성능과 메모리 사용면에서 불변보다 더 효율적이지만 가변이기 때문에 사이드 이펙트에 주의해서 사용해야 함
public final class StringBuilder extends AbstractStringBuilder implements ... {
    char[] value;// 자바 9 이전
    byte[] value;// 자바 9 이후

    //여러 메서드
    public StringBuilder append(String str) {...}
    public int length() {...}
    ...
}

 

** 참고

  • 실제로는 상속받고 있는 AbstractStringBuilder에 value속성과 length()메서드가 있음

(3) StringBuilderMain1_1

  • StringBuilder 객체를 생성하면 참조변수를 통해 문자열을 조작할 수 있는 다양한 메서드를 사용할 수 있는데, String과 차이점은 가변이기 때문에 반환값 없이 메서드만 호출해도 인스턴스의 값이 변경이 됨
  • append(): 여러 문자열을 추가
  • insert(): 특정 위치에 문자열을 삽입
  • delete(): 특정 범위의 문자열을 삭제
  • reverse(): 메서드로 문자열을 뒤집음
  • toString(): stringBuilder의 결과를 String을 생성하여 반환
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);

        // 4번째 인덱스에 문자열을 추가
        sb.insert(4, "Java!");
        System.out.println("insert = " + sb);

        // 4번째부터 7번째 인덱스까지 문자열을 삭제
        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
*/

 

(4) 가변 vs 불변

  • String은 불변이기 때문에 한 번 생성되면 그 내용을 변경할 수 없으므로 문자열에 변화를 주려고 할 때마다 새로운 String객체가 생성되고 기존 객체는 버려지는데 이 과정에서 메모리와 처리 시간을 더 많이 소모함
  • 반면 StringBuilder는 가변이기 때문에 문자열에 변화가 있을 때마다 새로운 객체를 생성하지 않으므로 메모리 사용을 줄이고 성능을 향상시킬 수 있으나 사이드 이펙트를 주의해야함
  • StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋음

3. String 최적화

1) 자바의 String 최적화

(1) 문자열 리터럴 최적화

  • 자바 컴파일러는 문자열 리터럴을 더하는 부분을 자동으로 합쳐주기 때문에 런타임시 별도의 문자열 결합 연산을 수행하지 않으므로 성능이 향상됨
String helloWorld = "Hello, " + "World!";    // 컴파일 전
String helloWorld = "Hello, World!";         // 컴파일 후

 

(2) String 변수 최적화

  • 문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없는데 아래의 코드처럼 최적화를 수행함
  • 최적화 방식은 자바 버전에 따라 달라지며 자바9 부터는 StringConcatFactory를 사용하여 최적화를 수행함
  • 즉 자바가 최적화를 처리해주기 때문에 이렇게 간단한 경우에는 StringBuilder를 사용할 필요가 없이 + 연산을 사용하면 충분함
String result = str1 + str2;
String result = new StringBuilder().append(str1).append(str2).toString(); // 자동으로 최적화

2) String 최적화가 어려운 경우

(1) LoopStringMain

  • 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않음
  • 반복문의 루프 내부에서 컴파일러가 자바의 버전에 따라 최적화를 진행하려고 하겠지만 반복문 내부에서 최적화가 진행될 것이라고 예상할 수 있겠지만 결국에는 반복 횟수만큼 객체를 생성해야함
  • 이유는 반복문 내에서의 문자열 연결은 런타임에 연결할 문자열의 개수와 내용이 결정되기 때문에 컴파일러가 얼마나 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없기 때문임
  • 즉 최적화를 위한 객체는 물론이고 반복 횟수인 100,000번의 String 객체를 생성했을 것이므로 이런 상황에서는 최적화가 어려움
  • 맥북 1M pro 기준 약 2.7초가 걸렸음
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");
    }
}

/*
Hello Java가 더해진 결과..
time = 2671ms
*/

 

 

(2) LoopStringBuilderMain

  • 이럴 때는 직접 StringBuilder를 사용하면 됨
  • 반복문 내부에서는 StringBuilder를 사용하여 문자열에 변화를 주고 마지막에 toString()메서드로 String으로 변환하여 출력
  • 실행해보면 0.003초로 매우 빠른 속도로 연산이 된 것을 확인할 수 있음
package lang.String.builder;

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) + "ms");
    }
}
/* 실행 결과
... 출력 내용 생략
time = 3ms
*/

 

(3) 정리

  • 문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됨
  • 그러나 아래의 경우에는 StringBuilder를 직접 사용하는 것이 더 좋음
  • - 반복문에서 반복해서 문자를 연결할 때
  • - 조건문을 통해 동적으로 문자열을 조합할 때
  • - 복잡한 문자열의 특정 부분을 변경해야 할 때
  • - 매우 긴 대용량 문자열을 다룰 때

** 참고 StringBuilder vs StringBuffer

  • StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있는데 StringBuffer는 내부에 동기화가 되어있어서 멀티 쓰레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느림
  • StringBuilder는 멀티 쓰레드 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠름
  • 이에 대한 내용은 멀티 쓰레드를 학습하면 이해할 수 있음

4. 메서드 체이닝(Method Chaining)

1) 메서드 체이닝

(1) ValueAdder

  • 자신의 값과 add()메서드로 넘어온 값을 누적하는 기능을 제공하는 클래스
  • 반환값으로 자기 자신(this)의 참조값을 반환함, 이 부분을 유의해서 봐야함
package lang.String.chaining;

public class ValueAdder {
    private int value;

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

    public int getValue() {
        return value;
    }
}

 

(2) MethodChainingMain1

  • add() 메서드를 여러번 호출해서 값을 누적하고 더해서 출력
  • add() 메서드의 반환값을 사용하지 않았음
package lang.String.chaining;

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);
    }
}
/* 
result = 6
*/

 

(2-1) MethodChainingMain2

  • 반환값을 억지로 사용해보면 이런 코드가 될텐데, 실행결과는 기존과 동일함
package lang.String.chaining;

public class MethodChainingMain2 {
    public static void main(String[] args) {
        ValueAdder adder = new ValueAdder();
        ValueAdder adder1 = adder.add(1);
        ValueAdder adder2 = adder.add(2);
        ValueAdder adder3 = adder.add(3);

        int result = adder3.getValue();
        System.out.println("result = " + result);
    }
}
/* 실행 결과 동일 */

 

(2-2) 그림으로 설명

  • adder.add(1)을 호출하면 add()메서드는 결과를 누적 후 자기 자신의 참조값을 반환하고 adder1 변수는 adder와 같은 인스턴스를 참조하게 됨
  • 나머지 add()메서드들도 add()메서드가 자기 자신(this)의 참조값을 반환했기 때문에 실행 후 참조값을 저장한 변수에는 모두 같은 참조값을 가지게 됨

(3) MethodChainingMain3

  • 2번에서 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않는 대신 바로 메서드 호출에 사용할 수 있음
  • 1번의 코드보다 훨씬 간결하고 가독성이 좋아짐
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);
    }
}
/* 실행 결과 동일 */

 

(4) 실행 순서 및 정리

  • add() 메서드를 호출하면 ValueAdder 인스턴스 자신의 참조값이 반환되는데 이 반환된 참조값을 변수에 담아두지 않는 대신 즉시 사용하여 바로 메서드를 호출할 수 있음
  • 메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용하여 메서드 호출을 계속 이어갈 수 있는데 .(dot)을 찍고 메서드를 계속 연결해서 사용함
  • 마치 메서드가 체인으로 연결된 처럼 보인다고하여 이러한 기법을 메서드 체이닝이라고 함
  • 기존에는 메서드를 호출할 때 마다 계속 변수명에 .(dot)을 찍어야 했지만 메서드 체이닝 방식은 메서드가 끝나는 시점에 바로 .을 찍어서 변수명을 생략할 수 있음
  • 메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문이며 이 참조값에 .을 찍어서 바로 자신의 메서드를 호출할 수 있음
  • 메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어줌

(5-1) StringBuilder와 메서드 체인

  • StringBuilder는 메서드 체이닝 기법을 제공하며 내부 구조를 들어가서 작성된 메서드를 보면 return this;로 자기 자신의 참조값을 반환하고 있음
  • StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신의 참조값을 반환함

(5-2) StringBuilderMain1_2

  • 기존의 StringBuilderMain1_1의 코드를 전부 메서드 체이닝을 활용하면 여러가지의 기능을 가독성 좋게 코드를 작성할 수 있음
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);
    }
}
/* 실행 결과
string = !DCBA
*/

 

(6) 정리

  • "만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다"
  • 메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리해짐
  • 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용함

5. 문제와 풀이

1) startsWith

(1) 문제 설명

  • startsWith()를 사용하여 url이 https://로 시작하는지 확인
더보기
package lang.string.test;

public class TestString1 {
    public static void main(String[] args) {
        String url = "https://www.example.com";
        // 코드 작성
    }
}

 

실행 결과

true

 

(2) 정답

더보기
package lang.string.test;

public class TestString1 {
    public static void main(String[] args) {
        String url = "https://www.example.com";
        System.out.println(url.startsWith("https://"));
    }
}

2) length()

(1) 문제 설명

  • length()를 사용하여 arr 배열에 들어있는 모든 문자열의 길이 합을 구하여 출력
더보기
package lang.string.test;

public class TestString2 {
    public static void main(String[] args) {
        String[] arr = {"hello", "java", "jvm", "spring", "jpa"};
        // 코드 작성
    }
}

 

실행 결과

hello:5

java:4

jvm:3

spring:6

jpa:3

sum = 21

 

(2) 정답

더보기
package lang.string.test;

public class TestString2 {
    public static void main(String[] args) {
        String[] arr = {"hello", "java", "jvm", "spring", "jpa"};

        int sum = 0;
        for (String str : arr) {
            int strSize = str.length();
            System.out.println(str + ":" + strSize);
            sum += strSize;
        }

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

3) indexOf()

(1) 문제 설명

  • str에서 ".txt" 문자열이 언제부터 시작하는지 위치를 찾아서 출력
  • indexOf()를 사용해야 함
더보기
package lang.string.test;

public class TestString3 {
    public static void main(String[] args) {
        String str = "hello.txt";
        // 코드 작성
    }
}

 

실행 결과

index = 5

 

(2) 정답

더보기
package lang.string.test;

public class TestString3 {
    public static void main(String[] args) {
        String str = "hello.txt";
        System.out.println(str.indexOf(".txt"));
    }
}

4) substring()

(1) 문제 설명

  • substring()을 사용하여 hello 부분과 .txt 부분을 분리, 단순히 subString()에 숫자를 입력하여 문제를 풀면 됨
더보기
package lang.string.test;

public class TestString4 {
    public static void main(String[] args) {
        String str = "hello.txt";
        // 코드 작성
    }
}

 

실행결과

filename = hello

extName = .txt

 

(2) 정답

더보기
package lang.string.test;

public class TestString4 {
    public static void main(String[] args) {
        String str = "hello.txt";
        System.out.println("filename = " + str.substring(0, 5));
        System.out.println("extName = " + str.substring(5));
    }
}

5) indexOf, substring 조합

(1) 문제 설명

  • str에는 파일의 이름과 확장자가 주어지고, ext에는 파일의 확장자가 주어질 때 파일명과 확장자를 분리하여 출력
  • indexOf()와, substring()을 사용해야함
더보기
package lang.string.test;

public class TestString5 {
    public static void main(String[] args) {
        String str = "hello.txt";
        String ext = ".txt";
        // 코드 작성
    }
}

 

실행 결과

 

filename = hello

extName = .txt

 

(2) 정답

더보기
package lang.string.test;

public class TestString5 {
    public static void main(String[] args) {
        String str = "hello.txt";
        String ext = ".txt";

        int extIndex = str.indexOf(ext);
        System.out.println("filename = " + str.substring(0, extIndex));
        System.out.println("extName = " + str.substring(extIndex));
    }
}

6) 검색 Count

(1) 문제 설명

  • str에서 key로 주어지는 문자를 찾고, 찾은 문자의 수를 출력
  • indexOf()를 반복문과 함께 사용
더보기
package lang.string.test;

public class TestString6 {
    public static void main(String[] args) {
        String str = "start hello java, hello spring, hello jpa";
        String key = "hello";
        // 코드 작성
    }
}

 

실행 결과

count = 3

 

(2) 정답

더보기
package lang.string.test;

public class TestString6 {
    public static void main(String[] args) {
        String str = "start hello java, hello spring, hello jpa";
        String key = "hello";

        int count = 0;
        int index = str.indexOf(key);
        while (index >= 0) {
            index = str.indexOf(key, index + 1);
            count++;
        }
        System.out.println("count = " + count);
    }
}

 

  • 만약 str.indexOf(key)를 호출하였는데 str에 key가 포함되어있지 않으면 -1을 반환하므로 while 조건문을 0보다 크거나 같을 때 반복하도록 설정
  • str에서 key를 찾은 인덱스의 다음 인덱스부터 key를 찾기 위해 str.indexOf(key, index + 1)로 메서드를 호출

7) 공백 제거

(1) 문제 설명

  • 문자의 양쪽 공백을 제거
더보기
package lang.string.test;

public class TestString7 {
    public static void main(String[] args) {
        String original = "    Hello Java      ";
        // 코드 작성
    }
}

 

실행 결과

Hello Java

 

(2) 정답

더보기
package lang.string.test;

public class TestString7 {
    public static void main(String[] args) {
        String original = "      Hello Java     ";
        System.out.println(original.strip());
    }
}
  • trim()을 사용해도 됨

8) replace

(1) 문제 설명

  • replace()를 사용하여 java라는 단어를 jvm으로 변경
더보기
package lang.string.test;

public class TestString8 {

    public static void main(String[] args) {
        String input = "hello java spring jpa java";
        // 코드 작성
    }
}

 

실행 결과

hello jvm spring jpa jvm

 

(2) 정답

더보기
package lang.string.test;

public class TestString8 {

    public static void main(String[] args) {
        String input = "hello java spring jpa java";
        String replace = input.replace("java", "jvm");
        System.out.println(replace);
    }
}

9) split()

(1) 문제 설명

  • split()를 사용하여 이메일의 ID 부분과 도메인 부분을 분리
더보기
package lang.string.test;

public class TestString9 {
    public static void main(String[] args) {
        String email = "hello@example.com";
        // 코드 작성
    }
}

 

실행 결과

ID: hello

Domain: example.com

 

(2) 정답

더보기
package lang.string.test;

public class TestString9 {
    public static void main(String[] args) {
        String email = "hello@example.com";

        String[] parts = email.split("@");
        String id = parts[0];
        String domain = parts[1];

        System.out.println("ID = " + id);
        System.out.println("Domain = " + domain);
    }
}

10) split(), join()

(1) 문제 설명

  • split()을 사용하여 fruits를 분리하고 join을 사용하여 분리한 문자를 하나로 연결
더보기
package lang.string.test;

public class TestString10 {
    public static void main(String[] args) {
        String fruits = "apple,banana,mango";
        // 코드 작성
    }
}

 

실행 결과

apple

banana

mango

joinedString = apple->banana->mango

 

(2) 정답

더보기
package lang.string.test;

public class TestString10 {
    public static void main(String[] args) {
        String fruits = "apple,banana,mango";

        String[] fruitsList = fruits.split(",");
        for (String fruit : fruitsList) {
            System.out.println(fruit);
        }

        String joinString = String.join("->", fruitsList);
        System.out.println("joinString = " + joinString);
    }
}

11) reverse()

(1) 문제 설명

  • str문자열을 반대로 뒤집어서 출력하되 StringBuilder의 reverse()를 사용
더보기
package lang.string.test;

public class TestString11 {
    public static void main(String[] args) {
        String str = "Hello Java";
        // 코드 작성
    }
}

 

실행 결과

avaJ olleH

 

(2) 정답

더보기
package lang.string.test;

public class TestString11 {
    public static void main(String[] args) {
        String str = "Hello Java";

        StringBuilder sb = new StringBuilder(str);
        System.out.println(sb.reverse().toString());
    }
}
  • StringBuilder로 문자열을 조작한 후 결과는 스트링으로 반환하는 것이 좋음