String 클래스 - 기본
자바에서 문자를 다루는 대표적인 타입은 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);
}
}
기본형인 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 클래스는 대략 다음과 같이 생겼다.
public final class String {
//문자열 보관
private final char[] value;// 자바 9 이전
private final byte[] value;// 자바 9 이후
//여러 메서드
public String concat(String str) {...} public int length() {...}
...
}
클래스이므로 속성과 기능을 가진다.
속성(필드)
private final char[] value
여기에는 String의 실제 문자열 값이 보관된다. 문자 데이터 자체는 char[]에 보관된다.
String 클래스는 개발자가 직접 다루기 불편한 char[] 을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있는 수 있도록 다양한 기능을 제공한다. 그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공한다.
참고 : 자바 9 이후 String 클래스 변경 사항
자바 9부터 String 클래스에서 char[] 대신에 byte[]을 사용한다.
private final byte[] value;
자바에서 문자 하나를 표현하는 char는 2byte를 차지한다. 그런데 영어, 숫자는 보통 1byte 로 표현이 가능하다. 그래서 단순 영어, 숫자로만 표현된 경우 1byte를 사용하고(정확히는 Latin-1 인코딩의 경우 1byte 사용) , 그렇지 않은 나머지의 경우 2byte인 UTF-16 인코딩을 사용한다. 따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었다.
String 클래스와 참조형
String은 클래스이다. 따라서 기본형이 아니라 참조형이다. 참조형은 변수에 계산 할 수 있는 값이 들어 있는 것이 아니라 x001과 같이 계산할 수 없는 참조값이 들어있다. 따라서 원칙적으로 + 같은 연산을 사용할 수 없다.
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이 제공하는 concat()과 같은 메서드를 사용해야 한다.
하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공한다.
String 클래스 - 비교
String 클래스 비교할 때는 == 비교가 아니라 항상 equals() 비교를 해야한다.
동일성(Identity): == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
동등성(Equality): 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
그림 - new String() 비교
str1과 str2는 new String()을 사용해서 각각 인스턴스를 생성했다.
서로 다른 인스턴스이므로 동일성 ( == ) 비교에 실패한다.
둘은 내부에 같은 "hello" 값을 가지고 있기 때문에 논리적으로 같다.
따라서 동등성( equals() ) 비교에 성공한다.
참고로 String 클래스는 내부 문자열 값을 비교하도록 equals() 메서드를 재정의 해두었다.
String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다. 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어둔다. 이때 같은 문자열이 있으면 만들지 않는다. String str3 = "hello"와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조( x003 )를 반환한다. String str4 = "hello"의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3과 같은 x003 참조를 사용한다. 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있다. 따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공한다.
참고
풀(Pool)은 자원이 모여있는 곳을 의미한다. 프로그래밍에서 풀(Pool)은 공용 자원을 모아둔 곳을 뜻한다. 여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고, 제거하는 것은 비효율적이다. 대신에 이렇게 문자열 풀에 필요한 `String` 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 더 최적화 할 수 있다.
참고로 문자열 풀은 힙 영역을 사용한다. 그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문 에 매우 빠른 속도로 원하는 `String` 인스턴스를 찾을 수 있다. 해시 알고리즘은 뒤에서 설명한다.
String 클래스 - 불변 객체
String은 불변 객체이다. 따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없다.
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);
}
}
String은 불변 객체이다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환한다.
String.concat()은 내부에서 새로운 String 객체를 만들어서 반환한다.
따라서 불변과 기존 객체의 값을 유지한다.
String이 불변으로 설계된 이유
String이 불변으로 설계된 이유는 앞서 불변 객체에서 배운 내용에 추가로 다음과 같은 이유도 있다.
문자열 풀에 있는 String 인스턴스의 값이 중간에 변경되면 같은 문자열을 참고하는 다른 변수의 값도 함께 변경된다.
예를 들어보자.
String은 자바 내부에서 문자열 풀을 통해 최적화를 한다.
만약 String 내부의 값을 변경할 수 있다면, 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제가 발생한다. 다음의 경우 str3이 참조하는 문자를 변경하면 str4의 문자도 함께 변경되는 사이드 이펙트 문제가 발생한다.
String str3 = "hello"
String str4 = "hello"
String 클래스는 불변으로 설계되어서 이런 사이드 이펙트 문제가 발생하지 않는다.
StringBuilder - 가변 String
불변인 String 클래스의 단점
불변인 String 클래스에도 단점이 있다.
다음 예를 보자. 참고로 실제로 작동하는 코드는 아니다.
두 문자를 더하는 경우 다음과 같이 작동한다.
"A" + "B"
String("A") + String("B") //문자는 String 타입이다.
String("A").concat(String("B"))//문자의 더하기는 concat을 사용한다.
new String("AB") //String은 불변이다. 따라서 새로운 객체가 생성된다.
불변인 String 의 내부 값은 변경할 수 없다.
따라서 변경된 값을 기반으로 새로운 String 객체를 생성한다.
더 많은 문자를 더하는 경우를 살펴보자.
String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D");
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");
- 이 경우 총 3개의 `String` 클래스가 추가로 생성된다.
- 그런데 문제는 중간에 만들어진 `new String("AB")` , `new String("ABC")` 는 사용되지 않는다.
- 최종적으로 만들어진 `new String("ABCD")` 만 사용된다.
- 결과적으로 중간에 만들어진 `new String("AB")` , `new String("ABC")` 는 제대로 사용되지도 않고, 이후 GC의 대상이 된다.
불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점이다.
문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 `String` 객체를 만들고, GC해야 한다.
결과적으로 컴퓨터의 CPU, 메모리를 자원을 더 많이 사용하게 된다.
그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모한다.
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() {...}
...
}
가변(Mutable) vs 불변(Immutable)
- `String` 은 불변하다. 즉, 한 번 생성되면 그 내용을 변경할 수 없다. 따라서 문자열에 변화를 주려고 할 때마다 새로운 `String` 객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다.
- 반면에, `StringBuilder` 는 가변적이다. 하나의 `StringBuilder` 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있다. 단 사이드 이펙트를 주의해야 한다.
StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) `String` 으로 변환하는 것이 좋다.
메서드 체인닝 - Method Chaining
간단한 예제 코드로 메서드 체이닝(Method Chaining)에 대해 알아보자.
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
- 단순히 값을 누적해서 더하는 기능을 제공하는 클래스다.
- add() 메서드를 호출할 때 마다 내부의 value에 값을 누적한다.
- add() 메서드를 보면 자기 자신( this )의 참조값을 반환한다. 이 부분을 유의해서 보자.
public class MethodChainingMain1 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
adder.add(1);
adder.add(2);
adder.add(3);
int result = adder.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
add() 메서드를 여러번 호출해서 값을 누적해서 더하고 출력한다.
여기서는 `add()` 메서드의 반환값은 사용하지 않았다.
이번에는 `add()` 메서드의 반환값을 사용해보자.
public class MethodChainingMain2 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder adder1 = adder.add(1);
ValueAdder adder2 = adder1.add(2);
ValueAdder adder3 = adder2.add(3);
int result = adder3.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 결과는 기존과 같다.
- adder.add(1)` 을 호출한다.
- `add()` 메서드는 결과를 누적하고 자기 자신의 참조값인 `this` ( `x001` )를 반환한다.
- `adder1` 변수는 `adder` 와 같은 `x001` 인스턴스를 참조한다.
- `add()` 메서드는 자기 자신( `this` )의 참조값을 반환한다. 이 반환값을 `adder1` , `adder2` , `adder3` 에 보관 했다.
- 따라서 `adder` , `adder1` , `adder2` , `adder3` 은 모두 같은 참조값을 사용한다.
- 왜냐하면 `add()` 메서드가 자 기 자신( `this` )의 참조값을 반환했기 때문이다.
그런데 이 방식은 처음 방식보다 더 불편하고, 코드도 잘 읽히지 않는다. 이런 방식을 왜 사용하는 것일까?
이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드 호출에 사용해보자.
public class MethodChainingMain3 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
int result = adder.add(1).add(2).add(3).getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 순서
`add()` 메서드를 호출하면 `ValueAdder` 인스턴스 자신의 참조값( `x001` )이 반환된다.
이 반환된 참조값을 변수에 담아두지 않아도 된다. 대신에 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있다.
다음과 같은 순서로 실행된다.
adder.add(1).add(2).add(3).getValue() //value=0
x001.add(1).add(2).add(3).getValue() //value=0, x001.add(1)을 호출하면 그 결과로 x001을 반환한다.
x001.add(2).add(3).getValue() //value=1, x001.add(2)을 호출하면 그 결과로 x001을 반환한다.
x001.add(3).getValue() //value=3, x001.add(3)을 호출하면 그 결과로 x001을 반환한다.
x001.getValue() //value=6
6
메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있다. 코드를보면 `.` 을 찍고 메서드를 계속 연결해서 사용한다. 마치 메서드가 체인으로 연결된 것처럼 보인다.
이러한 기법을 메서드 체이닝이라 한다. 물론 실행 결과도 기존과 동일하다.
기존에는 메서드를 호출할 때 마다 계속 변수명에 ' . ' 을 찍어야 했다. 예) adder.add(1), adder.add(2)
메서드 체이닝 방식은 메서드가 끝나는 시점에 바로 ' . ' 을 찍어서 변수명을 생략할 수 있다.
메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문이다.
이 참조값에 ' . ' 을 찍어서 바로 자신의 메서드 를 호출할 수 있다.
메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어준다.
StringBuilder와 메서드 체인(Chain)
StringBuilder는 메서드 체이닝 기법을 제공한다.
StringBuilder의 append() 메서드를 보면 자기 자신의 참조값을 반환한다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder` 에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환 한다. 예) insert(), delete(), reverse()
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);
}
}
참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용한다.
'JAVA > 중급1' 카테고리의 다른 글
7. 중첩 클래스, 내부 클래스1 (0) | 2024.08.31 |
---|---|
5. 열거형 - ENUM (0) | 2024.08.28 |
4. 래퍼, Class 클래스 (0) | 2024.08.27 |
2. 불변 객체 (0) | 2024.07.14 |
1. Object 클래스 (0) | 2024.07.14 |