주제 : 김영한님의 자바 중급 1편 총 정리
내용 : String 클래스에 대해 공부
초간단 정리 : StringBuilder 는 불변 객체인 String 을 가변으로 다룰 수 있게 해줍니다. append , insert 등의 메서드를 통해 문자열을 효율적으로 추가, 삽입, 삭제, 변경할 수 있습니다.
자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있다.
public class CharArrayMain { public static void main(String[] args) { char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'}; System.out.println(charArr); String str = "hello"; System.out.println("str=" + str); } }실행결과
hello str=hello
- 기본형인
char는 문자 하나를 다룰 때 사용한다.char를 사용해서 여러 문자를 나열하려면char[]를 사용해야 한다.- 하지만, 이렇게
char[]를 직접 다루는 방법은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는String클래스를 제공한다.
String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있다.
public class StringBasicMain { public static void main(String[] args) { // 쌍타옴표를 사용하던, 객체를 사용하던 똑같다. String str1 = "hello"; String str2 = new String("hello"); System.out.println("str1 = " + str1); System.out.println("str2 = " + str2); } }
- 쌍따옴표 사용 :
"hello"
- 객체 생성 :
new String("hello");
String은 클래스다. int, boolean같은 기본형이 아니라 참조형이다. str1 변수에는 String 인스턴스의 참조값만 들어갈 수 있다.String str1 = "hello";문자열은 매우 자주 사용된다. 그래서 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서
new String("hello")와 같이 변경해준다.String str1 = "hello" ; //기존 String str1 = new String("hello") //자바 언어에서 변경해줌
String 클래스와 참조형
- 자바에서 문자열을 더할 때는
String이 제공하는concat()과 같은 메서드를 사용해야 한다.- 하지만, 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공한다.
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 클래스 비교할 때는 == 동일성 비교가 아니라 항상 equals() 동등성 비교를 해야한다. (매우 중요!!)
public class StringEqualsMain1 { 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))); } }실행 결과
new String() == 비교: false new String() equals 비교: true 리터럴 == 비교: true 리터럴 equals 비교: true
의문점 : str3 3번째 결과값은 동일성 비교인데 왜 true가 나왔지?
-> 문자열 리터럴, 문자열 풀을 사용하기 때문
String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.- 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에
String인스턴스를 미리 만들어둔다. 이때 같은 문자열이 있으면 만들지 않는다.String str3 = "hello"와 같이 문자열 리터럴을 사용하면 문자열 풀에서"hello"라는 문자를 가진String인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조(x003)를 반환한다.String str4 = "hello"의 경우"hello"문자열 리터럴을 사용하므로 문자열 풀에서str3과 같은x003참조를 사용한다.- 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있다.
따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공한다.
그렇다면 문자열 리터럴(긴 문자)을 사용하면 == 비교를 하고,new String()을 직접 사용하는 경우에만 equals()비교를 사용하면 되지 않을까?(안된다)
public class StringEqualsMain2 { public static void main(String[] args) { String str1 = new String("hello"); String str2 = new String("hello"); System.out.println("메서드 호출 비교1: " + isSame(str1, str2)); String str3 = "hello"; String str4 = "hello"; System.out.println("메서드 호출 비교2: " + isSame(str3, str4)); } private static boolean isSame(String x, String y) { return x == y; //return x.equals(y); } }실행 결과
- 메서드 호출 비교1: false
- 메서드 호출 비교2: true
isSame() 의 경우 매개변수로 넘어오는 String 인스턴스가 new String() 으로 만들어진 것인지, 문자열 리터럴로 만들어 진 것인지 확인할 수 있는 방법이 없다.equals() 를 사용해서 동등성 비교를 해야 한다.(그래야 항상 실행 결과가 true가 된다. 만약, 동일성(==)으로 비교를 하게 된다면 100% 동일해야 하기 때문에 틀린 경우가 발생하기 때문이다.)String은 불변 객체이다. 따라서, 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없다.
public class StringImmutable1 { public static void main(String[] args) { String str = "hello"; str.concat(" java"); System.out.println("str=" + str); } }실행 결과
str = hello
실행 결과를 보면 뭔가 이상하다. 문자가 전혀 합쳐지지 않았다.
public class StringImmutable2 { public static void main(String[] args) { String str1 = "hello"; String str2 = str1.concat(" java"); System.out.println("str1 = " + str1); System.out.println("str2 = " + str2); } }실행 결과
str1 = hello
str2 = hello java
String.concat()은 내부에서 새로은String객체를 만들어서 반환한다.- 따라서 불변과 기존 객체의 값을 유지한다.
String이 불변으로 설계된 이유
String 이 불변으로 설계된 이유는 앞서 불변 객체에서 배운 내용에 추가로 다음과 같은 이유도 있다. 문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경된다. 예를 들어보자.
String은 자바 내부에서 문자열 풀을 통해 최적화를 한다.- 만약
String내부의 값을 변경할 수 있다면, 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제가 발생한다. 다음의 경우str3이 참조하는 문자를 변경하면str4의 문자도 함께 변경되는 사이드 이펙트 문제가 발생한다.
String str3 = "hello"String str4 = "hello"String클래스는 불변으로 설계되어서 이런 사이드 이펙트 문제가 발생하지 않는다.
String 클래스의 단점불변인 String 클래스에도 단점이 있다.
두 문자를 더하는 경우 다음과 같이 작동한다.
"A" + "B" String("A") + String("B") //문자는 String 타입이다. String("A").concat(String("B"))//문자의 더하기는 concat을 사용한다. new String("AB") //String은 불변이다. 따라서 새로운 객체가 생성된다.
- 불변인
String의 내부 값은 변경할 수 없다.- 따라서, 변경된 값을 기반으로 새로운
String객체를 생성한다.
- 불변인
String클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점이다.- 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은
String객체를 만들고, GC해야 한다.- 결과적으로 컴퓨터의 CPU, 메모리를 자원을 더 많이 사용하게 된다.
그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모한다.
- 이 문제를 해결하는 방법은 단순하다.
- 바로 불변이 아닌 가변
String이 존재하면 된다.- 가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없다. 따라서, 성능과 메모리 사용면에서 불변보다 더 효율적이다.
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 사용 예시
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을 생성해서 반환한다실행결과
sb = ABCD insert = ABCDJava delete = ABCD reverse = DCBA string = DCBA
자바의 String 최적화
- 자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐준다.
문자열 리터럴 최적화
컴파일 전
String helloWorld = "Hello, " + "World!";컴파일 후
String helloWorld = "Hello, World!";
- 이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는
StringBuilder를 사용하지 않아도 된다.- 대신에 문자열 더하기 연산(+)을 사용하면 충분하다.
public class LoopStringMain { public static void main(String[] args) { long startTime = System.currentTimeMillis(); String result = ""; for (int i = 0; i < 100000; i++) { result += "Hello Java "; } long endTime = System.currentTimeMillis(); System.out.println("result = " + result); System.out.println("time = " + (endTime - startTime) + "ms"); } }
-> 이럴 때는 직접 StringBilder를 사용한다.
public class LoopStringBuilderMain { public static void main(String[] args) { long startTime = System.currentTimeMillis(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 100000; i++) { sb.append("Hello Java "); } String result = sb.toString(); long endTime = System.currentTimeMillis(); System.out.println("result = " + result); System.out.println("time = " + (endTime - startTime) + "ms"); } }
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); } }