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 은 참조형이다. 그러니 a 와 c 라는 상자에는 글자가 아니라 주소가 들어 있다.
== 는 상자 안에 든 것을 비교한다. 참조형이라면 주소를 비교하는 셈이다.
String a = "hello"; · String c = new String("hello");
- a
- 0x1A4F
- → "hello"
- c
- 0x7B22
- → "hello" (다른 자리)
그럼 맨 처음 a == b 는 왜 true 였을까.
문자열 풀 — 같은 글자는 한 번만 만든다
문자열은 워낙 자주 쓰인다. "hello" 를 쓸 때마다 새로 만들면 낭비다.
그래서 자바는 큰따옴표로 적은 글자를 문자열 풀이라는 창고에 모아둔다. 이미 있는 글자면 새로 만들지 않고 있던 것의 주소를 준다.
String a = "hello"; · String b = "hello";
- a
- 0x1A4F
- → 문자열 풀의 "hello"
- b
- 0x1A4F
- → 같은 것을 가리킨다
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();
같은 일을 시키고 시간을 재봤다.
백 배다. 반복문 안에서 문자열을 이어붙이고 있다면 StringBuilder 로 바꾼다.
불변이 손해처럼 보이지만 얻는 것도 있다. 문자열을 어디에 넘기든 누가 몰래 고칠 걱정이 없다. 그래서 문자열 풀 같은 창고도 안심하고 공유할 수 있다. 방금 본 a == b 가 true 였던 것도 불변이라서 가능한 일이다.
자주 쓰는 것들
| 메서드 | 하는 일 | 예 |
|---|---|---|
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 를 쓴다.
- 01
==는 주소를 비교한다문자열 풀 덕분에 가끔 true 가 나오지만, 믿으면 안 된다.
- 02
문자열 비교는 언제나
.equals()예외를 두지 않는다. 대소문자를 무시하려면 equalsIgnoreCase.
if (name.equals("김자바")) { ... } - 03
String 은 절대 바뀌지 않는다
메서드는 원본을 고치지 않고 새 문자열을 돌려준다. 반드시 받아야 한다.
s = s.toUpperCase(); - 04
반복문 안의
+는 StringBuilder 로밖에서 한두 번 쓰는 + 는 컴파일러가 알아서 처리한다.
- 05
.equals()의 정체는 아직 모른다누가 만든 메서드인지, 내 클래스에 쓰면 어떻게 되는지는 14강에서 밝힌다.
Part 2 가 끝났다. 변수에 타입을 적는 것부터 시작해서, 여덟 개의 기본형과 나머지 전부인 참조형을 갈랐고, 그 차이가 == 에서 어떻게 드러나는지까지 봤다.
지금까지 코드는 위에서 아래로 한 번 흐르고 끝났다. 다음 Part 에서는 흐름을 꺾는다. 조건에 따라 갈라지고, 같은 일을 반복하고, 값을 여러 개 묶어 다루고, 코드에 이름을 붙여 다시 부른다.
7강은 조건문이다. score >= 90 같은 boolean 값이 마침내 쓸모를 얻는다.