참고
String 클래스
NOTE
불변 객체에 대해 설명하다가 갑자기 String으로 넘어온 이유는 String이 불변 객체이며, 불변 객체를 이해하는 데 가장 적합한 클래스이기 때문입니다.
public static void main(String[] args) {
char[] chars = {'h', 'e', 'l', 'l', 'o'}; // char 배열
System.out.println(chars);
String hello = "hello"; // String 방식(문자열 리터럴 방식)
System.out.println("hello = " + hello);
}
Java
복사
문자열 타입 char, String
public final class String {
//문자열 보관
private final char[] value;// 자바 9 이전
private final byte[] value;// 자바 9 이후
//여러 메서드
public String concat(String str) {...}
public int length() {...}
...
}
Java
복사
String 클래스 구조
•
String은 char와 달리 클래스이며, 기본형이 아닌 참조형입니다. 그럼 어떻게 문자열 리터럴 방식처럼 할당하는 걸까요?
•
자바에서 문자열은 자주 사용되므로 “”로 감싸면 자바 언어가 자동으로 변환해주기 때문입니다.
String 클래스 비교
NOTE
String을 생성할 때 new String() 방식과 문자열 리터럴을 할당하는 방식은 내부 동작에서 큰 차이가 있습니다. 이에 대한 개념은 각 방식으로 생성한 String을 비교하면서 이해할 수 있습니다.
public static void main(String[] args) {
// new String 비교, ==, equals
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println("new String() == 비교: " + (str1 == str2)); // false
System.out.println("new String() equals 비교: " + (str1.equals(str2))); // true
// 문자열 리터럴 비교, == , equals
String str3 = "hello";
String str4 = "hello";
System.out.println("리터럴 == 비교: " + (str3 == str4)); // true
System.out.println("리터럴 equals 비교: " + (str3.equals(str4))); // true
}
Java
복사
new String() vs 문자열 리터럴
new String()
new String() 비교
•
str1과 str2는 각각 생성자를 통해 만들어진 서로 다른 인스턴스입니다. 따라서 동일성(==) 비교에 실패합니다.
•
str1과 str2는 논리적으로 동일한 값인 “hello”를 가지고 있습니다. 따라서 동등성(equals()) 비교에 성공합니다.
문자열 리터럴
문자열 리터럴 비교
•
str3과 str4는 문자열 리터럴을 통해 생성되었습니다. 이 경우, 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용하여 미리 할당된 String 객체를 사용합니다. 따라서 동일성(==) 비교에서 같다고 판단됩니다.
•
str3과 str4는 동일한 값인 “hello”를 가지므로 논리적으로 같습니다. 그렇기 때문에 동등성(equals()) 비교에서도 같다고 판단됩니다.
문자열 풀은 필요한 String 인스턴스를 미리 생성하여 여러 곳에서 재사용할 수 있게 해줍니다. 이는 동일한 문자를 사용하는 경우 메모리 사용량을 줄이고, 문자를 생성하는 시간을 단축시켜 성능을 최적화할 수 있게 합니다.
String 불변객체
NOTE
String은 기본적으로 불변객체이므로 값을 변경할 수 없습니다. 따라서 값을 수정하려면 새로운 객체를 반환해주어야 합니다.
public static void main(String[] args) {
String str1 = "hello";
str1.concat(" java");
System.out.println("str1 = " + str1); // str1 = hello
String str2 = str1.concat(" java");
System.out.println("str2 = " + str2); // str2 = hello java
}
Java
복사
str1이 값이 그대로인 이유는 새로운 객체를 받지 않았기 때문.
•
불변객체는 언제나 값을 변경할때 새로운 객체를 만들어서 반환합니다.
String 주요메서드
NOTE
String에서는 매우 많은 기본 메서드를 제공해주고 있습니다. 외우지는 말고 어떠한 메서드가 있는지 알아보는정도로 넘어가시면 됩니다.
public static void main(String[] args) {
String str = "Hello World";
String anotherStr = "hello world";
String emptyStr = "";
String whitespaceStr = " ";
// 문자열 정보 조회
System.out.println("Length " + str.length());
System.out.println("isEmpty " + emptyStr.isEmpty());
System.out.println("isBlank " + whitespaceStr.isBlank()); // java 11
System.out.println("charAt " + str.charAt(1));
// 문자열 비교
System.out.println("equals " + str.equals(anotherStr));
System.out.println("equalsIgnoreCase " + str.equalsIgnoreCase(anotherStr));
System.out.println("compareTo " + str.compareTo(anotherStr));
System.out.println("compareToIgnoreCase " + str.compareToIgnoreCase(anotherStr));
System.out.println("startWith " + str.startsWith("Hell"));
System.out.println("endsWith " + str.endsWith("orld"));
// 문자열 검색
System.out.println("contains " + str.contains("World"));
System.out.println("indexOf " + str.indexOf("o"));
System.out.println("lastIndexOf " + str.lastIndexOf("o"));
// 문자열 조작 및 변환
System.out.println("substring " + str.substring(6));
System.out.println("concat " + str.concat(" Again"));
System.out.println("replace " + str.replace("World", "Java"));
System.out.println("replaceAll " + str.replaceAll("o", "O"));
System.out.println("replaceFirst" + str.replaceAll("o", "O"));
System.out.println("toLowerCase " + str.toLowerCase());
System.out.println("toUpperCase " + str.toUpperCase());
System.out.println("trim " + whitespaceStr.trim());
System.out.println("strip " + whitespaceStr.strip()); // java 11
// 문자열 분할 및 조합
String[] parts = str.split(" ");
for (String part : parts) {
System.out.println("split " + part);
}
System.out.println("join " + String.join("-", "2024", "03", "31"));
System.out.println("join " + String.join(" ", parts));
// 기타 유틸리티
System.out.println("valueOf " + String.valueOf(123));
char[] charArray = str.toCharArray();
System.out.println("toCharArray " + Arrays.toString(charArray));
System.out.println("format: " + String.format("Name: %s, Age: %d", "Alice", 30));
System.out.println("matcher " + "12345".matches("\\d+")); // 정규표현식 일치확인
}
Java
복사
StringBuilder - 가변 String
NOTE
불변인 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"); // 1
String str = new String("ABC") + String("D"); // 2
String str = new String("ABCD"); // 3
Java
복사
•
+연산이 수행될 때마다 객체가 생성되므로, 총 3개의 String 클래스가 추가로 생성됩니다.
•
하지만 최종적으로는 마지막의 new String("ABCD")만 사용됩니다.
•
결과적으로 중간에 만들어진 객체들은 제대로 사용되지 않고 GC에 의해 처리됩니다.
이 문제를 해결하는 방법은 가변 String을 사용하는 것입니다. 가변의 경우 단순히 값을 변경하면 되므로, 새로운 객체를 생성할 필요가 없습니다.
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);
String string = sb.toString(); //StringBuilder -> String
System.out.println("string = " + string);
}
Java
복사
•
String은 불변합니다. 즉, 한 번 생성되면 그 내용을 변경할 수 없습니다. 따라서 문자열에 변화를 주려면 매번 새로운 String 객체가 생성되고, 기존 객체는 버려집니다. 이 과정에서 메모리와 처리 시간을 더 많이 소모합니다.
•
StringBuilder는 가변적입니다. 하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며 이때마다 새로운 객체를 생성하지 않습니다. 이로 인해 메모리 사용을 줄이고 성능을 개선할 수 있습니다.
String 최적화
NOTE
자바는 컴파일러에서 문자열과 관련되어서 최적화 자동을 자동으로 수행줍니다.
String helloWorld = "Hello, " + "World!"; // 컴파일 전
String helloWorld = "Hello, World!"; // 컴파일 후
Java
복사
문자열 리터럴의 경우 자동으로 합친다.
String result = str1 + str2; // 컴파일 전
String result = new StringBuilder()
.append(str1)
.append(str2)
.toString(); // 컴파일 후
Java
복사
문자열 변수의 경우 버전마다 최적화가 달라질 수 있습니다.
자바에서 이미 최적화를 다 해결해주는데, 그럼에도 불구하고 가변 String을 별도로 사용해야 하는 상황이 있을까요? 이런 생각이 들 수 있지만, 최적화가 제대로 이루어지지 않는 상황도 발생합니다.
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
// 원본 코드
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
// 자동 최적화 코드(버전마다 다름) => 결국 i 범위만큼 StringBuilder를 생성한다.
for (int i = 0; i < 100000; i++) {
result = new StringBuilder().append(result).append("Hello Java ").toString();
}
// StringBuilder를 직접 사용하여 최적화하는 경우
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
Java
복사
•
반복문에서 문자를 반복해서 연결하는 경우
•
조건문을 통해 문자열을 동적으로 조합하는 경우
•
복잡한 문자열의 특정 부분을 변경해야 하는 경우
•
매우 긴 대용량 문자열을 다루는 경우
StringBuilder vs StringBuffer
StringBuilder와 동일한 기능을 수행하는 StringBuffer 클래스도 있습니다.
•
StringBuffer: 내부 동기화가 가능하여 멀티 스레드 상황에서 안전하지만, 동기화 오버헤드로 인해 성능이 느립니다.
•
StringBuilder: 멀티 스레드에서는 안전하지 않지만, 동기화 오버헤드가 없기 때문에 속도가 빠릅니다.
메서드 체이닝
NOTE
메서드 체이닝은 메서드 반환값으로 자기 자신을 반환해 체이닝을 할 수 있도록 해주는 기법입니다.
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this; // 자기 자신을 반환한다.
}
public int getValue(){
return value;
}
}
Java
복사
메서드 체이닝 기법은 자기 자신을 반환한다.
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);
// 체이닝 방법
int result2 = adder.add(1).add(2).add(3).getValue();
System.out.println("result2 = " + result2);
}
Java
복사
체이닝 방식으로 더 보기 좋은 코드가 만들어진다.
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
복사
StringBuilder 역시 메서드 체이닝을 지원한다.