본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
5장 : 배열과 문자열

String의 불변성과 비교

문자열은 Java 프로그램에서 가장 자주 만나는 객체 가운데 하나입니다.

String은 한 번 만들어진 뒤 내부 내용이 바뀌지 않으며 변환 메서드는 새 문자열을 반환합니다.

문자열 리터럴은 풀에 공유될 수 있어 ==가 우연히 참이 되는 경우가 있지만 내용 비교 계약은 equals입니다.

null 가능성과 빈 문자열도 서로 다른 상태입니다.

이 절에서는 불변성, 비교, 검색과 분해의 기준을 정리합니다.


문자열 메서드는 새 값을 반환한다

toUpperCase, replace, trim을 호출해도 원본 변수는 바뀌지 않습니다.

결과가 필요하면 반환값을 저장해야 합니다.


내용 비교에는 equals를 사용한다

==는 같은 String 객체인지 확인할 뿐입니다.

외부 입력과 리터럴을 비교할 때는 "yes".equals(input)처럼 null에도 안전한 방향을 선택할 수 있습니다.

문자열 비교 예제는 글자가 같은 두 객체와 같은 객체를 각각 준비합니다. 이 구분을 두면 equals가 내용을, ==가 참조의 동일성을 비교한다는 차이를 출력 한 번으로 확인할 수 있습니다.

StringSemantics.java
import java.util.regex.Pattern;

public class StringSemantics {
    public static void main(String[] args) {
        String input = new String("java.lang");
        String literal = "java.lang";
        System.out.println("reference=" + (input == literal));
        System.out.println("content=" + input.equals(literal));
        System.out.println("blank=" + "  ".isBlank());

        String[] parts = input.split(Pattern.quote("."));
        System.out.println(parts[0] + "/" + parts[1]);

        String changed = input.toUpperCase();
        System.out.println(input + " -> " + changed);
    }
}

new String("java")를 사용해도 equals는 참이고 ==는 거짓일 수 있습니다. 문자열 풀이 우연히 같은 참조를 돌려주는 경우에 기대지 말고, 업무 값의 비교는 항상 내용 계약으로 표현합니다.

reference=false
content=true
blank=true
java/lang
java.lang -> JAVA.LANG

변환 메서드는 새 문자열을 반환하고 원본 input은 그대로 유지됩니다.

문자열 API를 고를 때는 먼저 묻는 질문을 정확히 적습니다.

  • 같은 객체인가를 확인하는 내부 최적화 문제라면 ==를 볼 수 있습니다.
  • 같은 글자 시퀀스인가를 확인하는 업무 비교라면 equals를 사용합니다.
  • 입력이 없거나 공백뿐인 상태를 거르려면 null 가능성을 정한 뒤 isEmptyisBlank 중 계약에 맞는 것을 고릅니다.

변환 메서드의 결과는 새 문자열이므로, 이후에 쓸 값이라면 반환값을 반드시 변수에 받습니다.


빈 값과 공백과 null을 나눈다

isEmpty는 길이 0, isBlank는 공백 문자만 있는 경우까지 포함합니다.

null은 문자열 객체 자체가 없으므로 메서드를 호출할 수 없습니다.

null, "", " "은 각각 값 없음, 길이 0, 공백 문자만 있는 서로 다른 상태입니다. 입력 정책에 맞춰 value == null, isEmpty, isBlank를 고르고, null 가능성이 있다면 메서드 호출보다 먼저 처리합니다.

비교 1
String value = null;
boolean isYes = "yes".equals(value); // false, null 안전
// boolean broken = value.equals("yes"); // 실행 시 NullPointerException
비교 2
String emoji = "😀";
System.out.println(emoji.length());          // 2 char 단위
System.out.println(emoji.codePointCount(0, emoji.length())); // 1

분할 규칙은 정규식이라는 점을 기억한다

split의 인자는 단순 문자열이 아니라 정규식입니다.

점처럼 특별한 문자를 구분자로 쓰면 이스케이프하거나 Pattern.quote를 사용합니다.


대소문자 비교에는 언어 규칙이 들어간다

단순 식별자 비교는 equalsIgnoreCase로 충분할 수 있지만 사용자 언어에 따른 변환에는 Locale을 명시해야 합니다.

저장용 정규화와 표시용 변환을 구분합니다.


문자열 풀은 최적화 세부 사항이다

리터럴이 공유되는 현상을 이용해 ==로 내용을 비교하면 입력 경로에 따라 결과가 달라집니다.

프로그램 의미를 풀 구현에 의존시키지 않습니다.

문자열 풀과 intern()은 메모리 최적화의 구현 세부에 가깝습니다. ==가 어떤 리터럴에서 참이 되는지 실험할 수는 있지만, 그 결과를 내용 비교 규칙으로 가져오지 않는 것이 이 절의 실전 결론입니다.


String을 안전하게 다루려면 객체 동일성, 내용 동일성, 비어 있음, 값 없음이라는 네 상태를 섞지 않아야 합니다.