☕️ 김영한의 실전 자바 - 중급 1편 을 수강하며 학습한 내용을 저만의 언어로 정리하고 있습니다.
String
은 클래스이다.
따라서, 문자열 객체를 생성할 때 new
연산자를 사용해 객체를 선언하는 것이 당연하나,
"..."
쌍따옴표로도 문자열 객체를 생성할 수 있다.
이는 자주 사용되는 문자열을 편리하게 쓸 수 있도록 하기 위해서이다.
String str1 = new String("Hello String");
String str2 = "Hello String";
public class Example {
public static void main(String[] args) {
// 리터럴의 동일성, 동등성 비교
String str1 = "Conew";
String str2 = "Conew";
System.out.println("str1 == str2 :" + (str1 == str2));
System.out.println("str1.equals(str2): " + str1.equals(str2));
// 객체 선언의 동일성, 동등성 비교
String str3 = new String("Conew");
String str4 = new String("Conew");
System.out.println("str3 == str4: " + (str3 == str4));
System.out.println("str3.equals(str4): " + str3.equals(str4));
}
}
결과
str1 == str2 :true
str1.equals(str2): true
str3 == str4: false
str3.equals(str4): true
str1, str2
는 리터럴로 String을 선언했다.
동일성 (==) 비교에서 true를 반환한다.
동등성 (equals) 비교에서 true를 반환한다.
str3, str4
는 객체 생성 방식으로 String을 선언했다.
동일성 (==) 비교에서 false를 반환한다.
str3
과 str4
가 서로 다른 String 인스턴스를 가리키고 있기 떄문이다.동등성 (equals) 비교에서 true를 반환한다.
코드를 그림으로 나타내면 위와 같다.
문자열 풀
str1
과 str2
를 동일성 비교했을 때 결과가 true로 나온 이유는
str1, str2
모두 문자열 풀에서 "Conew"
라는 문자를 가진 인스턴스를 찾고, 그 참조값을 변수에 저장하기 때문이다.
반면 str3, str4
는 각각 새로운 문자열 객체를 명시적으로 생성하고 각각의 참조값을 변수에 저장하므로 동일성 비교 시 결과가 false로 나온다.
String의 동일성(==) 비교는 비교 대상인 문자열들이 어떻게 생성되었는지에 따라 결과가 다르다.
내가 비교하려는 문자열이 리터럴로 생성되었는지, 객체 생성 방식으로 생성되었는지 언제나 알고 있을 수는 없는 법이다.
특히 내가 만든 메서드를 다른 개발자가 사용한다면? 😲
따라서 문자열은 항상 equals() 메서드를 통해 비교해야 한다.
String은 불변객체이다.
value
에 실제 값이 보관된다.String은 불변객체이므로 중간에 값을 수정할 수 없다.
String hello = "Hello"
뒤에 문자를 추가해 "Hello Conew"로 만들고 싶은 경우,String helloConew = hello + " Conew"
와 같은 방식으로 새로 결과를 만들어 반환한다.문자열 풀의 작동 방식을 생각해본다면, String을 불변으로 설계한 이유를 알 수 있다.
문자열의 값이 중간에 변화될 수 있다면
str1
이 가리키고 있는 문자의 값이 "Coding" 으로 변했을 때,
str2
가 가리키는 문자도 "Coding"으로 변하게 된다.
이러한 사이드 이펙트를 방지하고자 String은 불변으로 설계되었다.
StringBuilder는 ⭐️가변 String이다.
StringBuilder 클래스의 장점
메서드 체이닝(Method Chaining)은 메서드 호출을 연속으로 할 수 있게 하는 프로그래밍 기법이다.
메서드 체이닝이 가능한 메서드들은 호출된 객체의 참조값을 반환하기 때문에, 연속적으로 다음 메서드를 호출할 수 있다.
메서드 체이닝의 예시
// ChainingTimer.class
public class ChainingTimer {
private int seconds = 0;
public ChainingTimer tick() {
seconds += 1;
return this;
}
public ChainingTimer tock() {
seconds += 1;
return this;
}
public int getSeconds() {
return seconds;
}
}
tick()
, tock()
메서드는 호출한 객체를 리턴한다.// ChainingTimerMain.class
public class ChainingTimerMain {
public static void main(String[] args) {
ChainingTimer chainingTimer = new ChainingTimer();
chainingTimer.tick().tock().tick().tock().tick().tock();
System.out.println(chainingTimer.getSeconds());
}
}
결과
6
StringBuilder는 메서드 체이닝이 가능하다.
public class StringBuilderMain {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
String string = sb.append("Hello ")
.append("I am ")
.append("Conew")
.reverse()
.toString();
System.out.println("string = " + string);
}
}
결과
string = wenoC ma I olleH
toString()
을 통해 String
객체로 변환시켜 사이드 이펙트를 방지한다!Java는 문자열 결합 연산을 컴파일 타임에 처리한다. 따라서, 런타임에 실행될 연산을 줄여 성능을 높일 수 있다.
(간단한 결합 연산의 경우 신경쓰지 않아도 자바가 최적화를 잘 해줌)
컴파일 타임: 소스코드가 기계어로 변환되어 실행 가능한 프로그램 형태가 되는 과정 (컴파일 오류가 나면 IDE에서 바로 알아낼 수 있다)
런타임: 사용자가 프로그램을 실행하는 시점 (런타임 오류는 프로그램을 실행하기 전까지는 오류를 알아낼 수 없다)
문자열 리터럴인 경우
String str1 = "Hello " + "Conew";
위 코드는 컴파일 타임에 아래와 같이 처리된다
String str1 = "Hello Conew";
변수인 경우
String str3 = str1 + str2;
변수에 어떤 값이 들었는지 모르기에 아래와 같은 방식으로 처리된다.
(JDK9 이후부터는 StringConcatFactory
를 이용한다.)
String str3 = new StringBuilder().append(str1).append(str2).toString();
반복문 내에 문자열 결합 연산이 있는 경우에는 최적화가 일어나지 않는다.
런타임이 되어서야 어떤 문자열들을 결합할지 알 수 있기 때문이다.
String result = "";
for (int i = 0; i < 1000; i++) {
result += "Conew is the best ";
}
위 코드에서 for loop 내부의 문자열 결합 연산은 최적화가 일어나지 않는다.
따라서 루프를 1000번 돌면서 계속 새로운 문자열을 생성하게 된다.
반복문 내의 문자열 결합 연산을 효율적으로 처리하고 싶다면 StringBuilder
를 사용하면 된다.
StringBuilder tmp = new StringBuilder();
for (int i = 0; i < 1000; i++) {
tmp.append("Conew is the best ");
}
String result = tmp.toString();
StringBuilder
를 알아보던 중, StringBuffer
의 존재도 알게 되었다.
StringBuffer
는 우리가 지금까지 알아본 StringBuilder
와 유사한 역할을 수행하나, 멀티 쓰레드 환경에서 안전하게 사용할 수 있도록 동기화 관련 오버헤드를 갖고 있다.
즉, 멀티스레딩 환경에서는 값을 보장하는 안전한 StringBuffer
를 써야 하나, 그 외의 경우에는 StringBuilder
가 훨씬 효율적이다.