Programming Language/Java

[Java] 19. String 클래스

lumana 2024. 10. 15. 20:19

 

 

String 클래스

#Java


String 클래스 - 기본

char형 배열로 문자열을 관리하기 어려움 String 클래스를 사용한다.


문자열을 생성하는 방법은 2가지가 있다.

  • 쌍따옴표 사용: ”hello"
  • 객체 생성: new String(“hello”)

String은 기본형이 아니라 참조형이다.


어? 참조형이면 인스턴스의 참조값만 들어갈 수 있는거 아닌가요?

String str1 = "hello"; 

이게 어떻게 가능하죠?
실제로는 “hello” 대신 new String(“hello”)와 같이 변경되어 동작한다.


근데 문자열끼리 덧셈도 가능하지 않나요? 참조형이면 덧셈이 어떻게 가능하죠?
원래는 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);
    }
}

String 클래스 구조

public final class String {
	//문자열 보관
	private final char[] value;// 자바 9 이전 
	private final byte[] value;// 자바 9 이후 
	// 여러 메서드
	// 생략
}

참고: 자바 9 이후 String 클래스 변경 사항
자바 9부터 String 클래스에서 char[] 대신에 byte[] 을 사용한다.

자바에서 문자 하나를 표현하는 char2byte 를 차지한다. 그런데 영어, 숫자는 보통 1byte 로 표현이 가능하다. 그래서 단순 영어, 숫자로만 표현된 경우 1byte 를 사용하고(정확히는 Latin-1 인코딩의 경우 1byte 사용) , 그렇지 않은 나머지의 경우 2byte 인 UTF-16 인코딩을 사용한다. 따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었다.



String 클래스 - 비교

String 클래스 비교할 때는 == 비교(동일성 비교)가 아니라 항상 equals() 비교(동등성 비교)를 해야 한다. (문자열은 참조형이니까…)


예시 코드

package lang.string.equals;

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

어? str3 == str4의 결과가 왜 true이죠? new로 각각 생성하면 참조값은 다른거 아닌가요?


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



  • 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어둔다. 이때 같은 문자열이 있으면 만들지 않는다.
  • String str3 = "hello" 와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello" 라는 문자를 가진 String 인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조( x003 )를 반환한다.
  • String str4 = "hello" 의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3 과 같은 x003 참조를 사용한다.
  • 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있다.

따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공한다.


참고: 풀(Pool)은 자원이 모여있는 곳을 의미한다. 프로그래밍에서 풀(Pool)은 공용 자원을 모아둔 곳을 뜻한다. 여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고, 제거하는 것은 비효율적이다. 대신에 이렇게 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 더 최적화 할 수 있다.

참고로 문자열 풀은 힙 영역을 사용한다. 그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문 에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있다. 해시 알고리즘은 뒤에서 설명한다.


어? 그러면 리터럴 쓸 경우에는 ==로 동일성 비교하고, 리터럴 아니면 equals()로 동등성 비교하면 되는 거 아닌가요?
리터럴인지 아닌지 어떻게 확인할건데요? 그냥 문자열 비교 할 때는 무조건 equals()로 비교하면 됩니다!


String 클래스 - 불변 객체

이전 챕터에서 말했듯 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);
    }
}

따라서 str의 메서드 concat()을 호출해도 문자열이 합쳐지지 않는거다.
concat()은 호출한 객체에 파라미터로 넘어온 문자열을 합쳐서 새로운 인스턴스를 만들고 반환해준다.


String이 불변으로 설계된 이유


문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경되기 때문이다.
String 클래스는 불변으로 설계되어서 이런 사이드 이펙트 문제가 발생하지 않는다.


String 클래스 - 주요 메서드

외우지 말고 필요할 때 docs를 읽어보자.

어차피 아래 있는 메서드들은 자주 써서 코딩하다 보면 자연스럽게 외워지긴 합니다.


문자열 정보 조회

  • length() : 문자열의 길이를 반환한다.
  • isEmpty() : 문자열이 비어 있는지 확인한다. (길이가 0)
  • isBlank() : 문자열이 비어 있는지 확인한다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 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("문자열이 비어 있거나 공백인지1: " + str.isBlank()); //Java11
        System.out.println("문자열이 비어 있거나 공백인지2: " + "  ".isBlank());
        char c = str.charAt(7);
        System.out.println("7번 인덱스의 문자: " + c);
    }
}

문자열 비교

  • equals(Object anObject) : 두 문자열이 동일한지 비교한다.
  • equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교한다.
  • compareTo(String anotherString) : 두 문자열을 사전 순으로 비교한다.
  • 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 equalsIgnoreCase 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("'a' compareTo 'b': " + "a".compareTo("b")); // 사전적으로 한 칸 차이
        System.out.println("str1 compareTo str3: " + str1.compareTo(str3)); // J와 W의 차이 : 13
        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 equalsIgnoreCase str2: true
'b' compareTo 'a': 1
'c' compareTo 'a': 2
'a' compareTo 'b': -1
str1 compareTo str3: -13
str1 compareToIgnoreCase str2: 0
str1 starts with 'Hello': true
str1 ends with 'Java!': true

문자열 검색

  • 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 

문자열 조작 및 변환

  • 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 StringChangeMain1 {
    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"));
    }
}

인덱스 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 

package lang.string.method;

public class StringChangeMain2 {
    public static void main(String[] args) {
        String strWithSpaces = " Java Programming ";
        System.out.println("소문자로 변환: " + strWithSpaces.toLowerCase());
        System.out.println("대문자로 변환: " + strWithSpaces.toUpperCase());
        System.out.println("공백 제거(trim): '" + strWithSpaces.trim() + "'");
        System.out.println("공백 제거(strip): '" + strWithSpaces.strip() + "'");
        System.out.println("앞 공백 제거(strip): '" + strWithSpaces.stripLeading() + "'");
        System.out.println("뒤 공백 제거(strip): '" + strWithSpaces.stripTrailing() + "'");
    }
}

소문자로 변환: java programming 
대문자로 변환: JAVA PROGRAMMING 
공백 제거(trim): 'Java Programming' 
공백 제거(strip): 'Java Programming' 
앞 공백 제거(strip): 'Java Programming ' 
뒤 공백 제거(strip): ' Java Programming' 

문자열 분할 및 조합

  • 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("-", "A", "B", "C");
        System.out.println("연결된 문자열: " + joinedStr);
        // 문자열 배열 연결
        String result = String.join("-", splitStr);
        System.out.println("result = " + result);
    }
}

Apple
Banana
Orange
연결된 문자열: A-B-C
result = Apple-Banana-Orange 

기타 유틸리티

  • valueOf(Object obj) : 다양한 타입을 문자열로 변환한다.
  • toCharArray(): 문자열을 문자 배열로 변환한다.
  • format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성한다.
  • matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인한다.

package lang.string.method;

public class StringUtilsMain1 {
    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 -> 문자x)
        String numString2 = "" + num;
        System.out.println("빈문자열 + num:" + numString2);
        // toCharArray 메서드
        char[] strCharArray = str.toCharArray();
        System.out.println("문자열을 문자 배열로 변환: " + strCharArray); // 다른 문자열과 더해서는 문자열이 출력되지 않음. 참조값이 출력됨
        // 문자열과 다른 타입의 객체를 + 연산자로 결합할 때: Java는 해당 객체의 toString() 메서드를 호출하여 문자열로 변환한 후 결합하기 때문
        System.out.print("바로출력: ");
        System.out.println(strCharArray); // 요렇게 하면 문자열이 출력됨
        for (char c : strCharArray) {
            System.out.print(c);
        }
        System.out.println();
    }
}

숫자의 문자열 값: 100
불리언의 문자열 값: true
객체의 문자열 값: java.lang.Object@a09ee92
빈문자열 + num:100
문자열을 문자 배열로 변환: [C@30f39991
바로출력: Hello, Java!
Hello, Java!

단순히 char형 배열을 출력할 때는 문자열처럼 출력되지만, 다른 문자열과 더하기 연산을 하게 되면 toString()이 호출되어 참조값이 출력된다


package lang.string.method;

public class StringUtilsMain2 {
    public static void main(String[] args) {
        int num = 100;
        boolean bool = true;
        String str = "Hello, Java!";
        // 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 메서드
        // "Hello, Java!"
        // 정규 표현식은 필요할 때 마다 찾아서 공부하자.
        String regex = "Hello, (Java!|World!)";
        System.out.println("'str'이 패턴과 일치하는가? " + str.matches(regex));
    }
}

num: 100, bool: true, str: Hello, Java!
숫자: 10.12
숫자: 10.12
'str'이 패턴과 일치하는가? true

정규 표현식은 필요할 때 마다 찾아서 공부하자.


StringBuilder - 가변 String

불변인 String 클래스의 단점

String이 불변이여서 문자열 풀에서 메모리 최적화가 가능했다. 과연 좋은점만 있을까?


"A" + "B" 
String("A") + String("B") //문자는 String 타입이다. 
String("A").concat(String("B"))//문자의 더하기는 concat을 사용한다. 
new String("AB") //String은 불변이다. 따라서 새로운 객체가 생성된다. 

불변인 String 의 내부 값은 변경할 수 없다. 따라서 변경된 값을 기반으로 새로운 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"); 

  • 이 경우 총 3개의 String 클래스가 추가로 생성된다.
  • 그런데 문제는 중간에 만들어진 new String("AB") , new String("ABC") 는 사용되지 않는다. 최종적으로 만들어진 new String("ABCD") 만 사용된다.
  • 결과적으로 중간에 만들어진 new String("AB") , new String("ABC") 는 제대로 사용되지도 않고, 이후 GC의 대상이 된다.

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


물론 자바가 내부에서 문자열을 다룰 때 최적화를 해주는데, 이 내용은 아래에서 다시 설명한다.


StringBuilder

이 문제를 해결하는 방법은 단순하다. 바로 불변이 아닌 가변 String 이 존재하면 된다. 가변은 내부의 값을 바로 변경 하면 되기 때문에 새로운 객체를 생성할 필요가 없다. 따라서 성능과 메모리 사용면에서 불변보다 더 효율적이다.


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


StringBuilder 는 내부에 final 이 아닌 변경할 수 있는 byte[] 을 가지고 있다.

public final class StringBuilder { 
	char[] value;// 자바 9 이전 byte[] value;// 자바 9 이후 
	//여러 메서드
	public StringBuilder append(String str) {...} public int length() {...}
	... 
} 

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

  • StringBuilder 객체를 생성한다.
  • append() 메서드를 사용해 여러 문자열을 추가한다.
  • insert() 메서드로 특정 위치에 문자열을 삽입한다.
  • delete () 메서드로 특정 범위의 문자열을 삭제한다.
  • reverse() 메서드로 문자열을 뒤집는다.
  • 마지막으로 toString 메소드를 사용해 StringBuilder 의 결과를 기반으로 String 을 생성해서 반환한다.

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


String 최적화

아까 문자 덧셈에서 문자열이 여러 개 생성되는 것을 보았다. 실제로는 컴파일러가 최적화해주는데, 이에 대해 자세히 알아보자


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


문자열 리터럴 최적화


컴파일 전

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

컴파일 후

String helloWorld = "Hello, World!";

따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상된다.


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

String result = str1 + str2; 

이런 경우 예를 들면 다음과 같이 최적화를 수행한다. (최적화 방식은 자바 버전에 따라 달라진다.)

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

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


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


물론 컴파일러 최적화가 만능은 아니다.


String 최적화가 어려운 경우

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


반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 한다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정된다. 이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없다. 따라서, 이런 상황에서는 최적화가 어렵다.


이 때는 직접 StringBuilder를 사용해주자.


정리

문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 된다.


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

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

참고: StringBuilder vs StringBufferStringBuilder 와 똑같은 기능을 수행하는 StringBuffer 클래스도 있다.
StringBuffer 는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느리다.
StringBuilder 는 멀티 쓰레드에 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠르다. StringBuffer 와 동기화에 관한 내용은 이후에 멀티 스레드를 학습해야 이해할 수 있다. 지금은 이런 것이 있구나 정도만 알아두면 된다.


메서드 체이닝 - Method Chaining

StringBuilder를 더 잘 사용하기 위해서는 메서드 체이닝을 알아야 한다.


메서드 체이닝이 뭔지 예를 통해서 바로 확인해보자

package lang.string.chaining;

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

  • add() 메서드를 호출할 때 마다 내부의 value 에 값을 누적한다.
  • add() 메서드를 보면 자기 자신( this )의 참조값을 반환한다

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

add()메서드의 반환값을 사용하지 않고 값을 더했다.


이번에는 add()메서드 반환값을 사용해보자

package lang.string.chaining;

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

결과는 동일하다.
추가적으로 참조 변수가 3개나 필요한데, 왜 이런 방식을 사용할까?


이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드 호출에 사용해보자.


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 또한 insert(), delete(), reverse() 등에서 자기 자신을 반환해준다.


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 클래스를 통해 생성할 수 있지만, 리터럴로도 생성할 수 있다.

리터럴로 생성하면 알아서 String 객체를 만들어 준다.

 

문자열 비교

== 비교(동일성 비교)가 아니라 항상 equals() 비교 (동등성 비교)를 하자. 문자열은 참조형이다.

 

문자열 풀

문자열은 불변 객체이다. 문자열은 기본적으로 String Pool에서 관리된다.String 참조 변수들이 같은 값을 가리킨다면, 사실 이 참조 변수들은 Spring Pool에 있는 하나의 String 객체를 동일하게 가리키고 있다. 해시를 통해 참조 변수들이 문자열 풀에서 빠르게 찾을 수 있다.

애초에 불변 객체이기 때문에 특정 참조 변수가 문자열 값을 바꿀 수 없다. 사이드 이펙트를 신경쓰지 않아도 된다.

 

문자열 메서드

필요할 때 찾아서 보자

 

StringBuilder

 String이 불변이기 때문에 생기는 단점도 존재한다. 문자열 끼리 덧셈을 할 때 "A" + "B" + "C" + "D"에서 실제로 사용되지 않을 임시 문자열을 엄청 많이 만들어야 한다.

 이 문제를 해결하기 위해 가변 String인 StringBuilder를 제공한다. 사이드 이펙트에 주의하자.

문자열을 변경하는 동안에만 StringBuilder로 유지하고, 문자열 변경이 끝나면 String으로 변환하자.

 

String 최적화

자바 8까지는 SB(StringBuilder)를 통해서, 자바 9부터는 StringConcatFactory를 사용해서 최적화를 수행한다.

 

String 최적화를 직접 해줘야 하는 경우

문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않는다. 이 때는 직접 SB를 사용해줘야 한다.

 

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

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

메서드 체이닝

StringBuilder는 메서드 체이닝 기법을 사용한다.

SB의 메서드에서는 SB의 참조값을 반환해준다. 반환된 참조값을 변수에 담아두지 않아도 된다. 대신에 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있다. 메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어준다. 

 

Ref) 김영한의 실전 자바 - 중급 1편