학습 자료

String 은 고칠 수 없다 — == 와 .equals()


Article

글자가 똑같은 문자열 두 개를 비교해보자.

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

System.out.println(a == b);
콘솔
  • true

true 다. 당연해 보인다. 그럼 만드는 방법만 바꿔보자.

String a = "hello";
String c = new String("hello");

System.out.println(a == c);
콘솔
  • false

글자는 똑같은데 false 다.

이게 자바 입문자가 가장 많이 틀리는 문제다. 그리고 진짜 무서운 건 false 가 나온다는 게 아니라, 아까는 true 가 나왔다는 것이다. 틀린 코드가 가끔 맞게 동작한다.

== 는 상자 안을 본다

4강에서 심어둔 문장을 꺼낼 때가 됐다. 기본형은 값을 담고, 참조형은 주소를 담는다.

String 은 참조형이다. 그러니 ac 라는 상자에는 글자가 아니라 주소가 들어 있다.

== 는 상자 안에 든 것을 비교한다. 참조형이라면 주소를 비교하는 셈이다.

new String 은 새 자리를 잡는다

String a = "hello"; · String c = new String("hello");

a
0x1A4F
→ "hello"
c
0x7B22
→ "hello" (다른 자리)
글자는 같지만 자리가 다르다. 주소가 다르니 ==false 다.

그럼 맨 처음 a == b 는 왜 true 였을까.

문자열 풀 — 같은 글자는 한 번만 만든다

문자열은 워낙 자주 쓰인다. "hello" 를 쓸 때마다 새로 만들면 낭비다.

그래서 자바는 큰따옴표로 적은 글자를 문자열 풀이라는 창고에 모아둔다. 이미 있는 글자면 새로 만들지 않고 있던 것의 주소를 준다.

리터럴은 창고에서 꺼내 쓴다

String a = "hello"; · String b = "hello";

a
0x1A4F
→ 문자열 풀의 "hello"
b
0x1A4F
→ 같은 것을 가리킨다
주소가 같으니 ==true 다. 우연히 맞은 것이다.

new String("hello") 는 창고를 무시하고 굳이 새 자리를 잡으라는 명령이다. 그래서 주소가 달라진다.

여기까지만 보면 "리터럴로만 만들면 == 를 써도 되겠네" 싶다. 그렇지 않다.

String a = "hello";
String e = "hel";
String f = e + "lo";

System.out.println(f == a);
System.out.println(f.equals(a));
콘솔
  • false
  • true

f 도 리터럴만 써서 만들었는데 false 다. 실행 중에 이어붙인 문자열은 창고에 들어가지 않는다.

.equals() 는 내용을 본다

.equals() 는 주소를 따라가서 글자를 하나씩 맞춰본다.

String a = "hello";
String c = new String("hello");

System.out.println(a == c);        // false — 주소가 다르다
System.out.println(a.equals(c));   // true  — 글자가 같다
무엇을 비교하는가

==

연산자

  • 상자 안에 든 것을 비교한다
  • 참조형이면 주소를 비교한다
  • 기본형(int, char …)에는 이게 맞다
  • 문자열에 쓰면 가끔 맞고 가끔 틀린다

.equals()

메서드

  • 주소를 따라가 내용을 비교한다
  • 글자가 같으면 true
  • 기본형에는 쓸 수 없다 (메서드가 없다)
  • 참조형 비교의 기본값이다

대소문자를 무시하고 싶으면 equalsIgnoreCase() 를 쓴다.

그런데 .equals() 는 대체 어디서 온 메서드일까. 우리는 String 클래스를 만든 적이 없다. 그리고 나중에 우리가 직접 만든 클래스로 .equals() 를 부르면 어떻게 될까?

이 질문은 14강에서 답한다. 답을 미리 말하면, 그때 .equals() 의 정체가 드러나면서 이 강의가 완전히 다시 읽힌다.

String 은 고칠 수 없다

toUpperCase() 로 대문자로 바꿔보자.

String s = "hello";
s.toUpperCase();

System.out.println(s);
콘솔
  • hello

아무 일도 일어나지 않았다.

String 은 한 번 만들어지면 절대 바뀌지 않는다. 이걸 불변이라고 한다. toUpperCase() 는 원본을 고치는 게 아니라 대문자로 바꾼 새 문자열을 만들어서 돌려준다.

돌려준 것을 받아야 한다.

String s = "hello";
String upper = s.toUpperCase();

System.out.println(s);      // hello — 원본은 그대로
System.out.println(upper);  // HELLO

s 를 아예 갈아치우고 싶으면 이렇게 쓴다.

s = s.toUpperCase();

s 라는 상자에 든 주소를 바꿔 끼운 것이다. 원래 있던 "hello" 를 고친 게 아니다.

그래서 + 는 비싸다

+ 로 문자열을 이어붙이면 매번 새 문자열이 만들어진다. 짧은 코드에서는 티가 안 난다. 반복문 안에 들어가면 얘기가 다르다.

String s = "";
for (int i = 0; i < 50000; i++) {
    s += "a";
}

50,000번 도는 동안 문자열이 50,000개 만들어지고 버려진다. 그때마다 앞에 쌓인 글자를 통째로 복사한다.

고칠 수 있는 문자열이 필요하다. 그게 StringBuilder 다.

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 50000; i++) {
    sb.append("a");
}
String s = sb.toString();

같은 일을 시키고 시간을 재봤다.

"a" 를 50,000번 이어붙이기
s += "a"200 ms
sb.append("a")2 ms
이 컴퓨터에서 잰 값이다. 절대값은 기계마다 다르지만, 자릿수 차이는 어디서나 난다.

백 배다. 반복문 안에서 문자열을 이어붙이고 있다면 StringBuilder 로 바꾼다.

불변이 손해처럼 보이지만 얻는 것도 있다. 문자열을 어디에 넘기든 누가 몰래 고칠 걱정이 없다. 그래서 문자열 풀 같은 창고도 안심하고 공유할 수 있다. 방금 본 a == btrue 였던 것도 불변이라서 가능한 일이다.

자주 쓰는 것들

메서드하는 일
length()글자 수"hello".length()5
charAt(i)i번째 글자 (char)"hello".charAt(1)'e'
substring(a, b)a부터 b 직전까지 잘라낸다"hello".substring(1, 3)"el"
indexOf(s)s 가 시작하는 위치. 없으면 -1"hello".indexOf("ll")2
contains(s)s 가 들어 있나boolean 을 준다
isBlank()비었거나 공백뿐인가" ".isBlank()true
strip()앞뒤 공백을 떼어낸 새 문자열원본은 그대로다
split(s)s 를 기준으로 쪼갠다결과는 배열 — 9강

위치는 0 부터 센다. charAt(1) 은 두 번째 글자다. 이 규칙은 9강 배열에서도 똑같다.

length() 는 괄호가 붙는다. 9강에서 배열의 length 는 괄호가 없다. 헷갈리기 딱 좋은데, 배열은 클래스가 아니라서 그렇다.

정리하면

String 은 참조형이고 불변이다. 그래서 == 가 아니라 .equals() 로 비교하고, 반복해서 이어붙일 때는 StringBuilder 를 쓴다.

다시 짚어보기
  1. 01

    == 는 주소를 비교한다

    문자열 풀 덕분에 가끔 true 가 나오지만, 믿으면 안 된다.

  2. 02

    문자열 비교는 언제나 .equals()

    예외를 두지 않는다. 대소문자를 무시하려면 equalsIgnoreCase.

    if (name.equals("김자바")) { ... }
  3. 03

    String 은 절대 바뀌지 않는다

    메서드는 원본을 고치지 않고 새 문자열을 돌려준다. 반드시 받아야 한다.

    s = s.toUpperCase();
  4. 04

    반복문 안의 + 는 StringBuilder 로

    밖에서 한두 번 쓰는 + 는 컴파일러가 알아서 처리한다.

  5. 05

    .equals() 의 정체는 아직 모른다

    누가 만든 메서드인지, 내 클래스에 쓰면 어떻게 되는지는 14강에서 밝힌다.

Part 2 가 끝났다. 변수에 타입을 적는 것부터 시작해서, 여덟 개의 기본형과 나머지 전부인 참조형을 갈랐고, 그 차이가 == 에서 어떻게 드러나는지까지 봤다.

지금까지 코드는 위에서 아래로 한 번 흐르고 끝났다. 다음 Part 에서는 흐름을 꺾는다. 조건에 따라 갈라지고, 같은 일을 반복하고, 값을 여러 개 묶어 다루고, 코드에 이름을 붙여 다시 부른다.

7강은 조건문이다. score >= 90 같은 boolean 값이 마침내 쓸모를 얻는다.

String 은 고칠 수 없다 — == 와 .equals() — 디코드랩(DCODELAB)