String은 클래스이다. 즉 참조형이다
문자열은 자주 사용되니 편의상 쌍따옴표로 문자열을 감싸면 자바에서 아래와 같이 자동으로 변경해준다.
package lang.string;
public class StringBasicMain {
public static void main(String[] args) {
String str1 = "hello";
//String str1 = new String("hello");
}
}
(실제로는 성능 최적화를 위해 문자열 풀을 사용한다)
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
@Stable
private final byte[] value;
private final byte coder;
...
public String concat(String str) {...}
public int length() {...}
자바 클래스의 구조이다. java 9 이전버전은 byte가 아닌 char[] value;로 구성되어있다.
char는 2byte를 사용하는데, latin_1 인코딩의 경우 영어와 숫자는 1바이트로 사용가능하기 때문에 메모리를 효율적으로
사용하기 위해 char -> byte로 바뀌었다고 함. (UTF-16 인코딩은 2바이트)
String의 메서드는 다양한 기능을 제공한다. 필요한게 있을 땐 API 문서를 찾아보자.
length : 문자열 길이반환
charAt : 특정 인덱스 반환
substring : 문자열 부분 반환
indexOf : 특정 문자열 시작되는 인덱스 반환
toLowerCase, toUpperCase : 문자열을 소문자 또는 대문자 변환
trim : 문자열 양 끝 공백 제거
concat : 문자열 더하기
*String은 참조형이기 때문에 원칙적으로는 +로 문자열을 더할 수 없다. 하지만 자바에선 문자열은 자주 다루기 때문에
편의상 +로 문자열을 더하는것을 허용한다. (concat)
String 클래스 - 비교
동일성(Identity) : == 연산자를 이용해 참조가 동일한 객체를 가리키고 있는지 확인
동등성(Equality) : .equals() 를 사용해 두 객체가 논리적으로 같은지 확인
package lang.string.equals;
public class StringEqualsMain1 {
public static void main(String[] args) {
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2);
System.out.println(str1.equals(str2));
// false, true
String str3 = "hello";
String str4 = "hello";
System.out.println(str3 == str4);
System.out.println(str3.equals(str4));
// true, true
}
}
str1 == str2의 결과값은 false다. 이유는 str1과 str2는 각각의 인스턴스를 생성했기 때문에 동일성 비교에서 실패한 것
str1.equals(str2)는 내부에 같은 값을 가지고 있기 때문에 동등성 비교에 성공했다.
문자열 풀
아래 str3 == str4 결과는 true이다. 그 이유는 문자열 풀 때문인데, 같은 문자열 리터럴을 사용하는 경우에 자바는
메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.
자바가 실행되는 시점에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어두는데, 이 때
같은 문자열이 있다면 추가로 만들지 않는다.
풀(Pool)은 공용자원을 모아두는 곳을 뜻한다. 여러곳에서 사용할 수 있는 자원을 필요할때마다 생성하고 제거하는것은
비효율적이기에 재사용하기 위해 풀이 사용된다. (문자열 풀은 힙 영역을 사용한다)
어차피 문자열 풀을 사용하니까 문자열 비교를 할 땐 ==를 사용하면 되지 않을까 라고 생각할 수 있지만
문자열은 equals 메서드를 사용해서 비교해야한다. 그 이유는 문자열을 비교하는 메서드가 있다고 가정했을 때,
파라미터로 넘어오는 인스턴스가 문자열 리터럴을 비교할 지, new String으로 생성된 문자열을 비교할 지 모르기 때문이다.
String 클래스 - 불변 객체
String은 불변 객체이다. 따라서 내부의 문자열 값을 변경할 수 없다.
package lang.string.immutable;
public class StringImmutable1 {
public static void main(String[] args) {
String str = "hello";
str.concat("java");
System.out.println(str);
}
}
concat 메서드를 사용해 문자열을 연결하려고 했다. 그러나 출력 결과는 hello만 나오게 되는데, 그 이유는 String은 불변객체이기
때문이다. 따라서 hello java 를 출력하려면 기존 값을 변경하지 않고 새로운 결과를 만들어서 반환해야한다.
package lang.string.immutable;
public class StringImmutable2 {
public static void main(String[] args) {
String str = "hello";
String str2 = str.concat("java");
System.out.println(str2);
}
}
String.concat은 내부에서 새로운 String 객체를 만들어서 반환한다. 따라서 기존 객체 값은 유지하는 것이다
String은 자바 내부에서 문자열 풀을 통해 최적화하는데, 만약 String 내부의 값을 변경할 수 있게 된다면 문자열 풀에서
같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제가 발생하니 String은 불변 객체가 되야하는 것이다.
String 클래스의 메서드는 아주 많은 메서드가 있으니 필요할 때 검색해서 사용하면 된다. (일일이 다 외우기 힘들다)
StringBuilder - 가변 String
불변인 String도 단점이 있다. String은 불변이므로 내부의 값을 변경할 수 없기 때문에, 변경 된 값을 기반으로 새로운 String 객체를
생성한다.
tring 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 ("A"), new String ("AB"), new String ("ABC"), new String ("ABCD") 총 4개의 객체가 생성된다.
우리는 최종적으로 "ABCD"의 값을 가진 객체가 필요한 것인데, 그 과정에서 필요 없는 3개의 객체가 생성되고, GC를 통해 삭제된다.
결과적으로 CPU, 메모리 자원을 더 많이 사용하게 되기 때문에 굉장히 비효율적인 코드이다.
(실제로는 자바 내부에서 최적화를 하긴 한다)
이때 사용되는것이 StringBuilder 이다.
StringBuilder는 가변객체이다. 따라서 객체의 값을 변경할 때, 객체를 생성하지 않고 내부에서 바로 변경할 수 있게 된다.
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
byte[] value;
StringBuilder는 final이 아닌 변경할 수 있는 byte[]를 가지고 있는 것을 확인할 수 있다. StringBuilder는 보통 문자열을 변경하는
동안에만 사용하다가, 문자열 변경이 끝나게 되면 안전한(불변) String으로 변환하는 것이 좋다. (사이트이펙트가 발생할 가능성이 있기때문이다)
String string = sb.toString();
System.out.println("String" + string);
String 최적화
자바 컴파일러는 문자열 리터럴을 더하는 부분을 자동적으로 합쳐준다.
String helloWorld = "Hello, " + "World!";
//컴파일 후
String helloWorld = "Hello, World!";
문자열 변수의 경우는 컴파일 시점에 변수에 어떤값이 들어가있는지 알 수 없기때문에 단순하게 합칠 수 없다.
String result = str1 + str2;
//최적화 (자바의 버전에 따라 최적화 방법은 다를 수 있음)
String result = new StringBuilder().append(str1).append(str2).toString();
지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 충분하다. 다만 최적화가 어려운 경우가 있는데,
루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않는다.
package lang.string.builder;
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");
}
}
위 반목문의 경우 어떤 방식으로 최적화가 되냐면
String result = "";
for (int i = 0; i < 100000; i++) {
result = new StringBuilder().append(result).append("Hello Java").toString();
}
최적화가 되있는 것 처럼 보이지만, 반복 횟수만큼 객체가 생성되어야 한다. 반복문 내에서의 문자열 연결은 런타임에 연결할 문자열의 개수
와 내용이 결정되기 때문에, 컴파일러는 얼마나 많은 반복이 일어날지 어떻게 변할 지 예측할 수 없기 때문에 최적화가 어려운 것이다.
위 코드는 아마 10만번의 String객체가 생성되고, GC로 객체가 삭제되는 행위가 일어났을 것이다.
이럴 경우엔 StringBuilder를 사용하면 된다.
package lang.string.builder;
public class LoopStringMain {
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 ");
}
Long endTime = System.currentTimeMillis();
String result = sb.toString();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
StringBuilder를 사용하고 안하고의 차이는 약 800배정도 였다.
StringBuilder를 직접 사용하면 좋은 경우
1. 반복문에서 반복해서 문자를 연결할 때
2. 조건문을 통해 동적으로 문자열을 조합할 때
3. 복잡한 문자열의 특정 부분을 변경해야 할 때
4. 매우 긴 대용량 문자열을 다룰 때
StringBuilder와 StringBuffer는 똑같은 기능을 수행하는데, 이 차이는 나중에 따로 검색을 해볼 것
메서드 체이닝 - Method Chaining
package lang.string.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를 반환한다.
package lang.string.chaining;
import com.sun.jdi.Value;
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);
}
}
위 코드는 add메서드의 반환값을 사용하지 않은 코드이다. 반환된 값을 사용하지 않고 넘어간 것
package lang.string.chaining;
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);
}
}
이 코드는 add메서드의 반환값을 사용한 코드이다.
adder.add(1)을 호출하고, add() 는 결과를 누적하고 자신의 참조값인 this (ValueAdd 인스턴스)를 반환
adder1은 adder와 같은 인스턴스를 참조한다.
add() 메서드는 자기 자신의 참조값을 반환한다. 이 반환값을 adder1, adder2, adder3에 보관했다.
따라서 adder1,2,3은 모두 같은값을 참조한다. add()메서드가 자기 자신의 참조값을 반환하기 때문이다.
그런데 이 코드는 사용하기도 불편하고, 잘 읽히지도 않는다. 이 문제를 해결하기 위해 메서드체이닝을 사용해보면
package lang.string.chaining;
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);
}
}
add() 메서드를 호출하면 자신의 참조값이 반환된다. 이 참조값을 변수에 담지 않고 연속적으로 참조해서 바로 호출할 수 있다.
위 방식처럼 . 을 찍고 계속 연결해서 사용한다. 메서드가 체인으로 연결된 것 처럼 보인다. 이러한 기법을 메서드 체이닝이라고 한다.
위 방식이 사용 가능한 이유는 자기 자신의 참조값을 반환하기 때문에 변수값을 생략하고 호출할 수 있는것이다.
메서드 체이닝은 코드를 간결하고 읽기 쉽게 만들어준다.
'Java' 카테고리의 다른 글
열거형 - ENUM (2) | 2024.11.05 |
---|---|
래퍼 클래스, Class 클래스 (3) | 2024.10.17 |
불변 객체 (0) | 2024.10.10 |
Object (1) | 2024.10.09 |
제네릭 - 제한된 타입 파라미터/ 와일드카드 (0) | 2024.07.11 |