김영한 선생님의 자바 중급을 듣고 정리한 내용입니다.
1. String 클래스
자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있다.
package stringClass;
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[]을 직접 다루는 방법은 매우 불편하기 때문에 자바에서는 문자열을 편리하게 다룰 수 있도록 String 클래스를 제공한다.
String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있다.
[1] 쌍따옴표 사용하기 “hello”
→ String은 참조형 클래스이기 때문에 String str = “hello” 처럼 사용하는 것은 조금 어색할 수 있다. 하지만 매우 자주 사용되기 때문에 편의상 쌍따옴표로 감싸서 사용할 수 있도록 자바에서 허용해준다.
[2] 객체 생성 new String(”hello”)
1.1. String 클래스 구조
String 클래스는 대략 다음과 같이 생겼다.
public final class String {
// java 9 이전
private final char[] value;
// java 9 이후
private final byte[] value;
}
- String 실제 문자열 값이 위 Array 안에 보관된다. 즉, 문자 데이터 자체는 char[] 혹은 byte[]에 보관된다는 것이다.
- 문자 하나에 char는 2byte를 차지한다. 보통 영어, 숫자는 1 byte로 표현이 가능하기 때문에 단순히 영어, 숫자로 표현된 경우 1 byte를 사용하고 그렇지 않은 나머지의 경우 2byte인 UTF-16 인코딩을 사용한다. 따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었다.
1.2. String 클래스와 참조형
String 클래스는 참조형이다. 따라서 참조형은 객체를 생성하면 x001과 같이 참조값이 들어있다.
→ 따라서 원칙적으로 +와 같은 연산을 사용할 수 없다.
→ 하지만 문자열을 너무 자주 다루어지기 때문에 자바 언어에서 특별히 + 연산을 제공한다.
package lang.string;
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);
}
}
// 결과
result1 = hello java
result2 = hello java
1.3. String 클래스 - 비교
String 클래스 비교할 때는 항상 == 연산자가 아니라 equals() 메서드를 활용해야 한다.
package stringClass;
public class StringEqualsMain {
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)));
System.out.println("==========================================");
String str3 = "hello";
String str4 = "hello";
System.out.println("new String() == 연산자 : " + (str3 == str4));
System.out.println("new String() equals 연산자 : " + (str3.equals(str4)));
}
}
// 결과
new String() == 연산자 : false
new String() equals 연산자 : true
==========================================
new String() == 연산자 : true
new String() equals 연산자 : true
Process finished with exit code 0
1.3.1. new String()으로 만든 인스턴스
- str1, str2 == 연산자 : new String()을 사용해서 각각 인스턴스를 생성했다. 서로 다른 인스턴스이므로 동일성(==) 비교에 실패한다.
- str1, str2 equals 메서드 : “hello” 값을 가지고 있기 때문에 논리적으로 같다. 따라서 동등성(equals()) 비교에 성공한다. 참고로 String 클래스는 내부 문자열 값을 비교하도록 equals() 메서드를 재정의 해두었다.
1.3.2. String xx = “yy” 로 만든 인스턴스
문자열 리터럴을 통해 문자열 객체를 생성하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.
- 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어둔다. → 이때 같은 문자열이 있으면 만들지 않는다.
- String str3 = “hello” 와 같이 문자열 리터럴을 사용하면 문자열 풀에서 “hello”라는 문자를 가진 String 인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조(x003)을 반환한다.
- String str4 = “hello”의 경우 문자열 리터럴을 동일하게 사용하는데, 이 때 hello라는 문자를 가진 String 인스턴스가 이미 있기 때문에 str3와 같은 x003 참조를 같이 사용한다.
따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공하게 된다.
2. String 클래스 - 불변 객체
String은 불변 객체이다. 따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없다.
package stringClass;
public class StringImmutable {
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 객체를 만들어서 반환한다.
→ 따라서 불변과 기존 객체의 값을 유지한다.
3. String 클래스의 주요 메서드
- 문자열 비교
- equals(Object anObject)
- equalsIgnoreCase(String anotherString)
- compareTo(String anotherString)
- compareToIgnoreCase(String str)
- startsWith(String prefix)
- endsWith(String suffix)
- 문자열 검색
- contains(CharSequence s)
- indexOf(String ch)
- indexOf(String ch, int fromIndex)
- lastIndexOf(String ch)
- 문자열 조작 및 변환
- substring(int beginIndex)
- substring(int beginIndex, int endIndex)
- concat(String str)
- replaceAll(String regex, String replacement)
- replaceFirst(String regex, String replacement)
- toLowerCase()
- toUpperCase()
- trim()
- strip()
- 문자열 분할 및 조합
- split(String regex)
- join(CharSequence delimiter, CharSequence… elements)
- 기타 유틸리티
- valueOf(Object obj)
- toCharArray()
- format(String format, Object… args)
- matches(String regex)
- 문자열 정보 조회
- length()
- isEmpty()
- isBlank()
- charAt(int index)
4. StringBuilder - 가변 String
4.1. 불변인 String 클래스 단점
불변인 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");
중간에 만들어진 new String(”AB”), new String(”ABC”)는 사용되지 않으며 new String(”ABCD”)만 사용된다.
→ 중간에 만들어진 AB, ABC는 제대로 사용되지도 않고 추후 GC 대상이 될 뿐이다.
즉, 불변인 String 클래스의 단점은 문자열을 더하거나 변경할 때마다 계속해서 새로운 객체를 생성해야 한다는 점이다. 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고 GC 해야만 한다.
결과적으로 컴퓨터의 CPU, Memory 자원을 더 많이 소모하게 된다.
4.2. StringBuilder
이런 단점을 해결하기 위해 StringBuilder를 사용할 수 있다. 자바에서는 StringBuilder이라는 가변 String을 제공한다. (물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 한다.)
// StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]를 가지고 있다.
public final class StringBuilder {
char[] value;// 자바 9 이전
byte[] value;// 자바 9 이후
//여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
package stringClass;
public class StringBuilderMain {
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);
}
}
// 결과
sb = ABCD
insert = ABCDJava
delete = ABCD
reverse = DCBA
string = DCBA
- StringBuilder 객체를 생성한다.
- append() 메서드를 사용해 여러 문자열을 추가한다.
- insert() 메서드로 특정 위치에 문자열을 삽입한다.
- delete () 메서드로 특정 범위의 문자열을 삭제한다.
- reverse() 메서드로 문자열을 뒤집는다.
- 마지막으로 toString 메소드를 사용해 StringBuilder 의 결과를 기반으로 String 을 생성해서 반환한다.
4.3. String 최적화
4.3.1. String 최적화가 어려운 케이스
문자열을 루프 안에서 문자열을 더하는 경우 최적화가 잘 이루어지지 않는다.
package stringClass;
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");
}
}
// 결과
... Java Hello Java Hello Java Hello Java ...
time = 3920ms
- 반복문 내에서의 문자열 연결은 런타임에 연결할 문자열의 개수와 내용이 결정된다.
- 이런 경우 컴파일러는 얼마나 많은 반복이 일어날지 예측할 수 없다. 따라서 이런 상황에서는 최적화가 잘 이루어지지 않는다.
4.3.2. 이럴 때, StringBuilder를 사용하면 된다.
package stringClass;
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");
}
}
// 결과
Java Hello ... Hello Java
time = 6ms
StringBuilder를 직접 사용하는 것이 더 좋은 이유
- 반복문에서 반복해서 문자를 연결할 때
- 조건문을 통해 동적으로 문자열을 조합할 때
- 복잡한 문자열의 특정 부분을 변경해야 할 때
- 매우 긴 대용량 문자열을 다룰 때
4.4. StringBuilder vs StringBuffer
StringBuilder : 내부에 동기화가 되어 있어서 멀티 스레드 상황에서 안전하지만 동기화 오버헤드로 인해 성능이 느리다. (StringBuffer와 동기화에 관한 내용은 이후 멀티스레드를 학습해야 이해할 수 있음)
StringBuilder : 멀티 쓰레드 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠르다.
5. 메서드 체이닝
간단한 예제 코드로 메서드 체이닝에 대해 알아본다.
ValueAdder 클래스는 단순히 값을 누적해서 더하는 기능을 제공하는 클래스이다.
- add() 메서드를 호출할 때 마다 내부의 value에 값을 누적한다.
- add() 메서드를 보면 자기 자신(this)의 참조값을 반환한다.
package stringClass;
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
아래는 ValueAdder 클래스의 add 메서드에서 반환된 참조값(this)를 바로 메서드 호출에 사용하는 예시이다.
package stringClass;
public class MethodChainingMain {
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
Process finished with exit code 0
→ add() 메서드를 호출하면 ValueAdder 인스턴스의 참조값(x001)이 반환된다. 이 반환된 참조값을 변수에 담아두지 않아도 되며 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있게 된다.
→ .을 찍고 메서드를 계속 연결해서 사용한다. 마치 메서드가 체인으로 연결된 것처럼 보인다. 이렇나 기법을 메서드 체이닝이라고 한다.
→ 메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어준다.
5.1. 메서드 체이닝와 StringBuilder
StringBuilder는 메서드 체이닝 기법을 제공한다.
// StringBuilder의 append() 메서드는 return this에서 볼 수 있듯이
// 자기 자신의 참조값을 반환한다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환한다. (ex. insert(), delete(), reverse() )
package stringClass;
public class StringBuilderMethodChainingMain {
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);
}
}
// 결과
string = DCBA
'Programming > Java' 카테고리의 다른 글
[Java] 김영한의 자바 중급 1편 #5 - ENUM (0) | 2024.09.30 |
---|---|
[Java] 김영한의 자바 중급 1편 #4 - 래퍼 클래스 (0) | 2024.09.30 |
[Java] 김영한의 자바 중급 1편 #2 - 불변 객체 (0) | 2024.09.06 |
[Java] 김영한의 자바 중급 1편 #1 - Object 클래스 (0) | 2024.09.03 |
[Java] Local 개발 환경 구축 : Spring MVC (0) | 2021.06.27 |