JAVA
- 형변환(Casting)
- 변환(Conversion)
- 연산자
- String(문자열)
- Java: String이란?
- Java: String의 주요 메서드
- Java: String의 불변성 (Immutability)
- Java: StringBuilder 클래스
- Java: StringBuilder의 주요 메서드
- Java: StringBuffer 클래스
- Java: StringBuffer 주요 메서드 정리
- Java 코딩 테스트: StringBuilder 활용 사례 모음
- Java: String.split() 정규식 예제 모음
- Java: String vs StringBuilder 비교
- Java: String vs StringBuilder vs StringBuffer 비교
- Java: 기본형(int) vs String(불변 객체)의 메모리 구조 비교
- Java: 문자열 리터럴 vs new String() 객체 생성 차이
- Java: BufferedReader
- Java: BufferedWriter
- Stream
형변환(Casting)
변환(Conversion)
List <-> Array 간 형변환
1. List → Array 형변환
List 타입 | 변환 방법 |
---|---|
|
|
|
|
| 반복문으로 수동 변환 필요 |
1) List → Array (참조형, 예: Integer, String 등)
- toArray() 메서드로 변환이 가능하다.
List<String> list = Arrays.asList("a", "b", "c");
String[] arr = list.toArray(new String[0]);
- list.toArray(new String[0]): 타입을 명시한 배열로 변환
- new String[0]은 사이즈가 0이어도 자동으로 맞춰준다.
List<Integer> list = Arrays.asList(1, 2, 3);
Integer[] arr = list.toArray(new Integer[0]);
2) List → Array (기본형, 예: int)
- Java의 List<Integer>는 int[]로 직접 변환 불가능
- 직접 반복문으로 넣어줘야 한다.
List<Integer> list = Arrays.asList(1, 2, 3);
int[] arr = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
arr[i] = list.get(i); // 언박싱 자동 수행
}
2. Array → List 형변환
변환 방법 | |
---|---|
|
|
|
|
|
|
1) 참조형 배열 → List (예: String[], Integer[] 등)
String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr);
- Arrays.asList()는 배열을 고정 크기 리스트로 바꿔준다.
- 추가/삭제는 불가능 → 변경 가능한 리스트로 바꾸려면
List<String> modifiableList = new ArrayList<>(Arrays.asList(arr));
2) 기본형 배열 → List (예: int[], double[] 등)
- 자바의 int[]는 바로 List<Integer>로 바꿀 수 없다.
- Stream + boxed()를 활용한다.
int[] arr = {1, 2, 3, 4};
List<Integer> list = Arrays.stream(arr) // IntStream 생성
.boxed() // int → Integer (박싱)
.collect(Collectors.toList());
int -> String, char 변환
1. int → String 변환
아래와 같은 세 가지 방법이 있다.
✅ 방법 1. String.valueOf(int)
가장 범용적으로 쓰이고, null 처리가 가능하다.
int num = 3;
String str = String.valueOf(num);
System.out.println(str); // 출력: "3"
✅ 방법 2. Integer.toString(int)
오직 int → String 변환에만 사용된다.
int num = 3;
String str = Integer.toString(num);
System.out.println(str); // 출력: "3"
✅ 방법 3. 문자열 더하기 (+ 연산자)
가장 짧고 직관적이지만 권장 방식은 아니다.
int num = 3;
String str = num + ""; // 문자열과 더하면 문자열로 변환됨
System.out.println(str); // 출력: "3"
2. int → char 변환
✅ 방법 1. '0'을 더한다.
int num = 3;
char ch = (char) (num + '0'); // '0'의 아스키 값은 48
System.out.println(ch); // 출력: '3'
📌 왜 이렇게 될까?
- '0'은 아스키 코드 48이다
- 3 + 48 = 51,
- 아스키 51은 문자 '3' 이기 때문에 이렇게 출력된다.
✅ 방법 2. Character.forDigit()
int num = 3;
char ch = Character.forDigit(num, 10);
System.out.println(ch); // 출력: '3'
🔎 Character.forDigit(value, radix)는 정수 값을 해당 진법(radix)의 문자로 바꿔준다.
❌ int → char 변환시 주의사항
아래과 같이 단순 캐스팅은 잘못된 것이다.
int num = 3;
char ch = (char) num; // ❌ 결과: 이상한 제어문자 (아스키 3 → ETX)
String, char > int 변환
1. int → String 변환
자바에서 문자열(String) '123'을 정수(int) 로 바꾸는 방법이다.
✅ 방법 1. Integer.parseInt()
가장 많이 쓰이는 방법이다.
String str = "123";
int num = Integer.parseInt(str);
System.out.println(num); // 출력: 123
- 문자열이 숫자 형태여야만 정상 작동한다.
- "12a3"처럼 숫자가 아닌 문자가 포함되면 예외가 발생한다.
✅ 방법 2. Integer.valueOf()
Integer.valueOf()는 Integer 객체를 반환하지만, 필요하면 int로 자동 변환된다.
String str = "123";
int num = Integer.valueOf(str); // 반환 타입은 Integer (객체)
int primitive = Integer.valueOf(str); // 자동 언박싱됨
✅ 방법 3. 예외 처리 (안전하게 변환하기)
사용자가 입력한 문자열이 숫자가 아닐 수도 있으니 예외 처리를 하는 것이 권장된다.
String str = "123";
try {
int num = Integer.parseInt(str);
System.out.println(num);
} catch (NumberFormatException e) {
System.out.println("유효한 숫자가 아닙니다.");
}
❌ 이런 건 에러가 난다.
String str = "12a3";
int num = Integer.parseInt(str); // ❌ NumberFormatException
2. char → int 변환
자바에서 '3'과 같은 문자(char) 를 정수(int) 로 바꾸는 방법이다.
✅ 방법 1. '0'을 빼기 (가장 일반적이고 직관적)
char ch = '3';
int num = ch - '0'; // 결과: 3
📌 왜 이렇게 될까?
- 문자 '3'의 아스키 코드: 51
- 문자 '0'의 아스키 코드: 48
- 51 - 48 = 3
✅ 방법 2. Character.getNumericValue()
char ch = '3';
int num = Character.getNumericValue(ch); // 결과: 3
이 방식은 'A' → 10, 'B' → 11 같은 16진 문자도 지원한다.
단, '0'~'9'만 처리할 거라면 첫 번째 방법이 더 간단하고 빠르다.
✅ 방법 3. String으로 바꿔서 Integer.parseInt()
문자열로 바꿔서 정수화하는 방법인데 이건 다소 무거운 방법이고 잘 쓰지 않는다.
char ch = '3';
int num = Integer.parseInt(String.valueOf(ch)); // 결과: 3
연산자
삼항 연산자 (Ternary Operator)
1. 개요
1.1 삼항 연산자란?
삼항 연산자(Ternary Operator)는 조건에 따라 값을 선택하는 Java의 축약 표현식이다. 일반적인 if-else
구문보다 간결하게 작성할 수 있으며, 값을 반환하는 표현식으로 활용된다.
int max = (a > b) ? a : b;
- 첫 번째 피연산자: 조건 (boolean 결과)
- 두 번째 피연산자: 조건이 참일 때 반환 값
- 세 번째 피연산자: 조건이 거짓일 때 반환 값
1.2 사용 목적
- UI 메시지 처리: 상황에 따라 메시지 문구 설정
- 값 분기 처리: 조건에 따라 상수/계산식 결정
- 간단한 로직 처리에서 코드 길이 절감
2. 문법
조건식 ? 참일 때의 값 : 거짓일 때의 값;
2.1 예시
int age = 20;
String type = (age >= 18) ? "성인" : "미성년자"; // 출력: 성인
조건식
:age >= 18
→ true- 참일 때: "성인"
- 거짓일 때: "미성년자"
3. 삼항 연산자 vs if-else
3.1 차이점
항목 | 삼항 연산자 | if-else 구문 |
---|---|---|
반환 가능 여부 | ✅ 값 반환 (표현식) | ❌ 문(statement) |
간결함 | ✅ 매우 간결 | ❌ 구조적으로 길어질 수 있음 |
가독성 | ✅ 단순 조건일 때만 | ✅ 복잡 조건/로직에 적합 |
3.2 비교 예시
// 삼항 연산자
int max = (a > b) ? a : b;
// 동일한 if-else
int max;
if (a > b) {
max = a;
} else {
max = b;
}
4. 중첩 삼항 연산자
4.1 기본 중첩 예제
int score = 85;
String grade = (score >= 90) ? "A" :
(score >= 80) ? "B" :
(score >= 70) ? "C" : "F";
System.out.println(grade); // 출력: B
4.2 주의사항
- 중첩이 많아질수록 가독성이 급격히 저하됨
- 중첩보다는
if-else
또는switch
를 고려할 것
5. 실전 예제 모음
5.1 나이 판별
int age = 16;
String result = (age >= 18) ? "성인" : "미성년자";
System.out.println(result); // 출력: 미성년자
5.2 최대값 구하기
int a = 25, b = 17;
int max = (a > b) ? a : b;
System.out.println("최대값: " + max); // 출력: 25
5.3 짝수/홀수 판별
int num = 9;
String parity = (num % 2 == 0) ? "짝수" : "홀수";
System.out.println(parity); // 출력: 홀수
5.4 로그인 상태 메시지
boolean isLoggedIn = false;
String message = isLoggedIn ? "환영합니다!" : "로그인이 필요합니다.";
System.out.println(message); // 출력: 로그인이 필요합니다.
5.5 다중 조건 - 세금율 계산 예시
double income = 45000;
double taxRate = (income > 80000) ? 0.35 :
(income > 40000) ? 0.25 : 0.15;
System.out.println("세율: " + taxRate);
// 출력: 세율: 0.25
✅ 요약
특징 | 설명 |
장점 | 간결한 조건 처리, 변수 할당 가능 |
단점 | 복잡한 로직에는 가독성 저하 |
사용 추천 | UI 메시지, 단순 조건 분기, 코드 압축 |
사용 주의 | 중첩 사용은 피하고 if-else 대체로 제한 |
삼항 연산자는 작고 간단한 판단 로직에 매우 유용하지만, 가독성을 해치는 복잡한 분기에는 적합하지 않다는 점을 기억하자.
String(문자열)
Java: String이란?
1. 개요
String
은 Java에서 문자들의 연속(문자열) 을 표현하는 대표적인 클래스이며, java.lang
패키지에 포함되어 있어 별도의 import
없이 바로 사용할 수 있다.
- 단일 문자는
char
타입으로, 복수 문자의 집합은String
으로 다룬다. String
은 객체이며, 내부적으로char[]
배열을 기반으로 구성된다.- Java의
String
은 불변(immutable) 구조를 갖고 있어, 생성 이후 변경이 불가능하다.
2. 주요 특징
2.1 문자열은 객체다
String str = "hello"; // str은 String 클래스의 인스턴스
- 문자열은 객체이므로, 메서드와 연산을 통해 다양한 처리가 가능하다.
- 내부적으로
char[]
배열을 가지며, 다양한 유틸리티 메서드 (length()
,substring()
,toUpperCase()
등)를 제공한다.
2.2 불변성 (Immutable)
String
은 한 번 생성되면 그 내용을 변경할 수 없다.- 문자열을 수정하는 모든 작업은 새로운 String 객체를 생성한다.
String a = "hi";
a += "!";
System.out.println(a); // hi!
→ "hi"
는 그대로 있고 "hi!"
는 새 객체로 Heap에 생성됨.
2.3 문자열 리터럴은 상수 풀(String Constant Pool)에 저장
String s1 = "java";
String s2 = "java";
System.out.println(s1 == s2); // true → 같은 주소 참조
→ 동일한 리터럴은 JVM 내의 String Constant Pool에서 공유됨
3. 문자열 생성 방법
3.1 리터럴 방식 (권장)
String s = "Hello";
- JVM이 문자열 상수 풀(Constant Pool)에서 공유 → 메모리 효율적
- 가장 일반적이고 직관적인 방식
3.2 생성자 방식 (비권장)
String s2 = new String("Hello");
- 항상 새로운 객체를 Heap에 생성
- 리터럴과는 다른 주소를 갖게 됨 → 메모리 낭비 가능
📌 사용 예시
String a = "abc";
String b = new String("abc");
System.out.println(a == b); // false (주소 비교)
System.out.println(a.equals(b)); // true (내용 비교)
4. 문자열 비교
4.1 ==
vs .equals()
String a = "test";
String b = "test";
String c = new String("test");
System.out.println(a == b); // true (상수 풀에서 공유)
System.out.println(a == c); // false (Heap에 새 객체 생성)
System.out.println(a.equals(c)); // true (내용 비교)
비교 방식 | 설명 |
---|---|
| 객체의 주소값 비교 (레퍼런스) |
| 객체가 가진 내용값 비교 |
5. 문자열 길이와 메서드
String s = "hello world";
System.out.println(s.length()); // 11
System.out.println(s.charAt(0)); // h
System.out.println(s.substring(0, 5)); // hello
System.out.println(s.toUpperCase()); // HELLO WORLD
System.out.println(s.replace(" ", "_")); // hello_world
6. 기타 특징 및 내부 구조
6.1 String 내부 구조
- Java 9 이전:
char[]
로 저장 - Java 9 이후:
byte[]
+coder
(압축을 위해 Compact Strings 적용)
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private final int offset;
private final int count;
...
}
6.2 문자열 연결 (Concatenation)
String a = "Hello";
String b = "World";
String c = a + " " + b; // Hello World
+
연산자는 내부적으로StringBuilder
로 변환되어 처리됨 (컴파일러 최적화)- 반복적인 연결에는 직접
StringBuilder
사용 권장
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
System.out.println(sb.toString()); // Hello World
7. 정리
항목 | String |
---|---|
불변성 | ✅ 한 번 생성된 값은 변경 불가 |
생성 방식 | 리터럴(권장), 생성자(비권장) |
비교 방식 |
|
내부 구조 | Java 9 이전: |
연결 방식 |
|
사용 시기 | 고정된 문자열, 메시지, 상수 등 |
Java: String의 주요 메서드
1. String의 인스턴스 메서드
1.1 개요
String 클래스의 인스턴스 메서드는 문자열 객체를 통해 호출하는 메서드입니다. 즉, "hello".length()처럼 문자열 인스턴스를 기준으로 작동합니다.
1.2 주요 인스턴스 메서드 + 메모리 작동
Java의 String 클래스는 다양한 문자열 조작 기능을 제공하는 인스턴스 메서드를 포함하고 있다. 아래는 자주 사용되는 메서드들의 설명과 예시이다. 모든 String 인스턴스 메서드는 원본 문자열을 절대 수정하지 않으며, 필요한 경우 항상 새로운 문자열 객체를 Heap에 생성한다. (*불변성(immutability)*)
length()
문자열의 길이를 반환한다.
예:"hello".length()
→5
📌 설명: 내부char[]
배열의 길이를 반환하며, 연산은 매우 빠름 (O(1)).
charAt(int index)
지정한 인덱스의 문자를 반환한다.
예:"java".charAt(1)
→'a'
📌 설명: 내부 배열에서 해당 인덱스의 문자에 직접 접근.IndexOutOfBoundsException
주의.
substring(int begin, int end)
지정한 범위의 부분 문자열을 반환한다.
예:"hello".substring(1, 3)
→"el"
📌 설명: 새로운String
객체가 생성됨. 원본 문자열은 변하지 않음.
indexOf(String s)
주어진 문자열이 처음 등장하는 인덱스를 반환한다.
예:"hello".indexOf("l")
→2
📌 설명: 내부적으로 왼쪽부터 한 문자씩 비교. 시간복잡도는 O(n).
lastIndexOf(String s)
주어진 문자열이 마지막으로 등장하는 인덱스를 반환한다.
예:"hello".lastIndexOf("l")
→3
📌 설명: 내부적으로 오른쪽부터 역방향 검색. 불변 객체지만 탐색은 가변적으로 수행됨.
contains(String s)
특정 문자열이 포함되어 있는지를 boolean으로 반환.
예:"hello".contains("he")
→true
📌 설명: 내부적으로indexOf(...) >= 0
과 동일한 방식으로 작동.
startsWith(String prefix)
주어진 문자열로 시작하는지 확인.
예:"hello".startsWith("he")
→true
📌 설명: 접두어 길이만큼의 앞쪽 문자들을 순차 비교.
endsWith(String suffix)
주어진 문자열로 끝나는지 확인.
예:"hello".endsWith("lo")
→true
📌 설명: 끝부분에서 길이만큼 잘라 비교. 새로운 객체는 생성되지 않음.
equals(String s)
문자열의 내용을 비교한다.
예:"hi".equals("hi")
→true
📌 설명: 참조값이 다르더라도, 내부 문자 배열이 동일하면 true. 대소문자 구분.
equalsIgnoreCase(String s)
대소문자 무시하고 내용을 비교.
예:"Hi".equalsIgnoreCase("hi")
→true
📌 설명: 내부적으로toLowerCase()
후equals()
수행. 추가 연산 포함됨.
toUpperCase()
문자열을 모두 대문자로 변환.
예:"java".toUpperCase()
→"JAVA"
📌 설명: 원본 문자열은 유지되고, 대문자로 된 새 문자열 객체가 생성됨.
toLowerCase()
문자열을 모두 소문자로 변환.
예:"JAVA".toLowerCase()
→"java"
📌 설명:toUpperCase()
와 동일하게 새 객체 생성됨. 원본 유지.
trim()
문자열의 앞뒤 공백을 제거한다.
예:" hello ".trim()
→"hello"
📌 설명: 공백이 없는 경우에도 새로운 문자열 객체가 생성될 수 있음 (JDK 버전에 따라 다름).
replace(CharSequence old, CharSequence new)
문자열 내에서 지정한 문자열을 다른 문자열로 치환.
예:"apple".replace("p", "b")
→"abble"
📌 설명: 모든 치환 결과를 담은 새 문자열 객체 생성. 원본 변경 없음.
split(String regex)
지정한 정규 표현식을 기준으로 문자열을 나누고 배열로 반환.
예:"a,b,c".split(",")
→["a", "b", "c"]
📌 설명: 내부적으로 정규식 패턴을 컴파일하여 배열을 생성. 반환 타입은String[]
.
1.3 특징
- 모두 String 객체를 통해 호출한다.
- String은 불변(immutable)이라, 원본 문자열은 변경되지 않고 새 문자열을 반환한다.
2. String의 클래스 메서드
2.1 개요
String 클래스의 클래스 메서드는 static 메서드이며, 객체 생성 없이 String.으로 호출할 수 있다.
주로 타입 변환이나 문자열 생성에 사용된다.
2.2 주요 클래스 메서드 목록
Java의 String 클래스는 객체 없이도 호출 가능한 클래스 메서드(static method)들을 제공한다. 자주 쓰이는 타입 변환, 문자열 생성, 서식 처리 작업을 편리하게 처리하기 위해 고안되었다.
String.valueOf(...)
다양한 타입의 값을 문자열로 변환한다.
예:String.valueOf(123)
→"123"
📌 설명: 내부적으로 해당 타입을 문자열로 바꿔 새로운String
객체를 Heap에 생성한다.null
이 들어가면"null"
문자열이 반환됨.
String.copyValueOf(char[])
주어진char[]
배열을 문자열로 변환한다.
예:String.copyValueOf(new char[]{'h','i'})
→"hi"
📌 설명:valueOf(char[])
과 동일하며, 내부적으로 새String
객체가 생성됨.
String.format(String format, Object... args)
형식 지정 문자열을 생성한다.
예:String.format("%04d", 5)
→"0005"
📌 설명:printf
스타일 형식을 기반으로 포맷 처리. 내부에서Formatter
클래스를 사용해 문자열을 구성하고, Heap에 새 객체로 반환.
String.join(String delimiter, CharSequence...)
여러 문자열을 하나로 연결하고, 사이에 지정된 구분자를 넣는다.
예:String.join("-", "a", "b", "c")
→"a-b-c"
📌 설명: 내부적으로StringBuilder
를 이용하여 문자열을 효율적으로 이어붙인 후, 최종적으로String
객체로 반환됨.
String.valueOf(char[])
문자 배열 전체를 문자열로 변환한다.
예:String.valueOf(new char[]{'j','a','v','a'})
→"java"
📌 설명: 배열 전체를 읽어서 새로운String
객체를 생성한다. 부분 변환도valueOf(char[], offset, count)
로 가능.
2.3 특징
- 클래스 이름으로 직접 호출 (String.format(...))한다.
- 보통 타입 변환, 포맷팅, 조합 등에 사용된다.
- 객체 없이도 사용할 수 있다.
- static 키워드로 정의되며, String 클래스 이름으로 직접 호출
- 내부적으로는 대부분 새로운 문자열을 생성하여 Heap에 저장
Java: String의 불변성 (Immutability)
1. 개요
Java의 String
클래스는 불변 객체(Immutable Object) 로 설계되어 있다.
- 즉, 한 번 생성된 문자열은 절대로 수정되지 않으며, 문자열을 변경하려고 시도하면 항상 새로운 객체가 생성된다.
- 이러한 불변성은 보안, 성능, 스레드 안정성 등 다양한 이유로 매우 중요한 특징이다.
2. 왜 불변(Immutable)한가?
불변성은 단순히 설계 철학이 아니라, 실제 Java 플랫폼의 안정성과 효율성을 위한 필수적인 요소이다.
2.1 보안(Security)
- 문자열은 종종 파일 경로, 데이터베이스 연결 문자열, 네트워크 주소 등에 사용됨.
- 가변 문자열이라면 악의적인 코드가 문자열을 수정해 보안 구멍을 만들 수 있음.
String dbUrl = "jdbc:mysql://localhost";
dbUrl.replace("localhost", "hacker-site.com"); // String은 수정되지 않음
2.2 성능(Performance)
- String Constant Pool을 통해 동일한 문자열을 재사용할 수 있음.
- 이 기능이 제대로 작동하려면 문자열이 절대 바뀌지 않아야 함.
String a = "hello";
String b = "hello";
System.out.println(a == b); // true → 동일 객체 공유
2.3 스레드 안전성(Thread Safety)
- 불변 객체는 내부 상태가 변경되지 않기 때문에, 동기화 없이도 안전하게 공유 가능하다.
- 멀티스레드 환경에서 추가 비용 없이 안정적인 동작 보장.
3. 작동 방식 – 변경은 실제로 “새 객체 생성”
3.1 코드 예시
String a = "hello";
String b = a;
a = a + " world";
System.out.println(a); // hello world
System.out.println(b); // hello
"hello"
는 리터럴 풀에 존재"hello world"
는 Heap에 새로운 객체로 생성- 변수
b
는 여전히"hello"
를 참조
3.2 메모리 구조 흐름
[초기 상태]
a ───▶ "hello"
b ───┘
[수정 후]
a ───▶ "hello world"
b ───▶ "hello"
📌 변경이 아닌 “대체”다. 기존 객체는 절대 바뀌지 않음!
4. 불변 객체의 특징 요약
항목 | 설명 |
---|---|
변경 가능 여부 | ❌ 불가능 – 항상 새로운 객체 생성됨 |
스레드 안전성 | ✅ 동기화 없이 공유 가능 |
공유 가능 여부 | ✅ 문자열 리터럴 풀에서 객체 공유 가능 |
메모리 구조 | Heap 영역 + String Constant Pool 최적화 |
관련 API 특징 |
,
등 모두 새 객체 반환 |
5. 관련 클래스 구분
5.1 불변 객체 (Immutable)
String
Integer
Boolean
LocalDate
,LocalTime
,BigDecimal
5.2 가변 객체 (Mutable)
StringBuilder
StringBuffer
ArrayList
HashMap
📝 불변 클래스는 equals(), hashCode() 재정의에 유리하며, 객체 캐싱 및 보안 측면에서도 장점을 가짐.
6. 성능상의 주의점
6.1 반복 연결 시 성능 저하
String s = "";
for (int i = 0; i < 1000; i++) {
s += "a"; // 매 반복마다 새 객체 생성
}
→ 이 방식은 1000개의 String 객체를 생성함
→ 해결 방법: StringBuilder
사용
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("a");
}
String result = sb.toString();
7. 결론
String
의 불변성은 Java 언어의 안정성과 성능 최적화를 위한 중요한 설계 원칙이다.- 문자열이 자주 변경된다면
StringBuilder
나StringBuffer
를 적극적으로 활용해야 한다. - 불변 객체는 보안성, 공유성, 스레드 안전성 측면에서 뛰어난 장점을 가진다.
Java: StringBuilder 클래스
1. 개요
StringBuilder
는 Java에서 가변(mutable) 문자열을 처리하기 위한 클래스이다.
java.lang
패키지에 포함되어 있으며 import 없이 사용 가능- 문자열을 반복적으로 추가/삭제/수정할 때 매우 유용
- 내부적으로 char[] 버퍼를 사용하여 객체 생성 없이 문자열을 조작함
StringBuffer
와 기능은 거의 동일하지만, 동기화를 지원하지 않아 더 빠름
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");
System.out.println(sb); // 출력: Hello World
2. 주요 특징
항목 | 설명 |
---|---|
가변성 | ✅ 내부 문자열을 직접 수정 (객체 생성 없음) |
동기화 | ❌ 지원하지 않음 (→ 단일 스레드에서만 안전) |
성능 |
|
내부 구조 |
|
기본 크기 | 초기 용량 16 → 필요 시 자동 확장됨 |
3. 생성자 종류
StringBuilder sb1 = new StringBuilder(); // 초기 버퍼 크기 16
StringBuilder sb2 = new StringBuilder(50); // 초기 버퍼 크기 지정
StringBuilder sb3 = new StringBuilder("Hello"); // 초기 문자열 지정
4. 주요 메서드 정리
메서드 | 설명 |
---|---|
| 문자열 끝에 추가 |
| 특정 위치에 문자열 삽입 |
| 지정 구간 문자 삭제 |
| 지정 인덱스 문자 삭제 |
| 지정 구간을 문자열로 대체 |
| 문자열을 뒤집음 |
| 최종 문자열로 변환 ( |
| 현재 문자열 길이 반환 |
| 내부 버퍼의 총 용량 반환 |
| 최소 버퍼 크기 확보 |
| 특정 위치 문자 변경 |
| 특정 인덱스 문자 반환 |
5. 사용 예제
public class StringBuilderExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // Hello World
sb.insert(5, ","); // Hello, World
sb.replace(0, 5, "Hi"); // Hi, World
sb.delete(3, 4); // Hi World
sb.reverse(); // dlroW iH
System.out.println(sb.toString()); // 출력: dlroW iH
}
}
6. 성능 비교: String vs StringBuilder
// 느린 방식: String
String str = "";
for (int i = 0; i < 10000; i++) {
str += "a"; // 매 반복마다 새 객체 생성
}
// 빠른 방식: StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("a"); // 동일 객체 안에서 처리
}
⏱ StringBuilder
는 수천 배 빠를 수 있으며 GC 부담도 낮다.
7. 주의 사항
- ❗ 멀티스레드 환경에서는
StringBuilder
는 스레드 안전하지 않음- → 이 경우
StringBuffer
사용
- → 이 경우
- 반복 수정이 없고 단순한 문자열만 다룬다면
String
사용이 더 직관적일 수 있음
8. 요약
항목 | StringBuilder |
---|---|
불변성 | ❌ 가변 |
스레드 안전성 | ❌ 비동기 (단일 스레드만 안전) |
성능 | ✅ 매우 빠름 |
용도 | 반복적인 문자열 연결/수정에 최적 |
권장 사용 환경 | 루프 내 문자열 처리, 대용량 로그 조립 등 |
Java: StringBuilder의 주요 메서드
1. StringBuilder
의 인스턴스 메서드
1.1 개요
StringBuilder
클래스는 가변 문자열 처리를 위해 설계된 클래스이다. 모든 메서드는 StringBuilder
객체 자체를 수정하며, 새로운 객체를 생성하지 않는다. 이는 String
과의 가장 큰 차이점이다.
StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); // 같은 객체 내에서 문자열을 수정
1.2 주요 메서드 + 내부 동작
모든 메서드는 시간복잡도가 평균적으로 O(1) 또는 **O(n)**이며, 대부분 String보다 빠름.
대부분 메서드가return this
를 통해 메서드 체이닝을 지원한다.
🔹 append(String str)
- 문자열을 뒤에 이어붙임
- 예:
new StringBuilder("hi").append(" there") → "hi there"
- 📌 내부 작동:
char[]
배열 뒤쪽에 복사 → 공간 부족 시 자동 확장 - 시간복잡도: 평균 O(1), 확장 시 O(n)
🔹 insert(int offset, String str)
- 지정 위치에 문자열 삽입
- 예:
"abc".insert(1, "X") → "aXbc"
- 📌 내부 작동: offset 뒤쪽 문자들을 밀고
str
삽입 - 시간복잡도: O(n)
🔹 delete(int start, int end)
- 주어진 범위의 문자를 삭제 (
start <= i < end
) - 예:
"abcdef".delete(2, 4) → "abef"
- 📌 내부 작동: end 이후 문자들을 앞당김
- 시간복잡도: O(n)
🔹 deleteCharAt(int index)
- 특정 인덱스의 문자 1개 삭제
- 예:
"abc".deleteCharAt(1) → "ac"
- 📌 내부 작동:
delete(index, index+1)
호출과 동일
🔹 replace(int start, int end, String str)
- 주어진 범위를
str
로 대체 - 예:
"abcde".replace(1, 4, "X") → "aXe"
- 📌 내부 작동:
delete
후insert
조합 - 시간복잡도: O(n)
🔹 reverse()
- 문자열을 역순으로 뒤집음
- 예:
"hello".reverse() → "olleh"
- 📌 내부 작동:
char[]
를 양 끝에서 swap - 시간복잡도: O(n)
🔹 toString()
StringBuilder
내용을String
객체로 반환- 예:
sb.toString()
- 📌 내부 작동: 새로운
String
객체 생성 (불변 객체) - 시간복잡도: O(n)
🔹 setCharAt(int index, char c)
- 지정한 인덱스의 문자를 변경
- 예:
"java".setCharAt(1, 'o') → "jova"
- 📌 내부 작동: 배열 원소 직접 교체
- 시간복잡도: O(1)
🔹 charAt(int index)
- 특정 위치의 문자 반환
- 예:
"java".charAt(2) → 'v'
- 📌 설명: 내부
char[]
배열의 직접 접근 - 시간복잡도: O(1)
🔹 length()
- 현재 문자열 길이 반환
- 예:
"abc".length() → 3
- 📌 설명: 버퍼 내 유효 문자 수 (
count
필드) - 시간복잡도: O(1)
🔹 capacity()
- 내부 버퍼의 현재 용량 반환 (할당된 char 배열 길이)
- 예:
new StringBuilder(10).capacity() → 10
- 📌 동작: 초기 용량 16, 필요 시 자동 확장 (공식:
newCapacity = (old * 2) + 2
) - 시간복잡도: O(1)
🔹 ensureCapacity(int minimumCapacity)
- 최소 버퍼 크기를 보장 (수동 확장)
- 예:
sb.ensureCapacity(100)
- 📌 설명: 대량 조작 전 성능 최적화에 사용
2. 메서드 체이닝 예시
StringBuilder sb = new StringBuilder();
String result = sb.append("Hi")
.append(" ")
.append("there")
.replace(0, 2, "Bye")
.reverse()
.toString();
System.out.println(result); // "ereht eyB"
3. 주의 사항
주의 포인트 | 설명 |
---|---|
❌
값 전달 |
은 문자열
로 처리됨 |
✅ 내부 수정 | 객체 내부 수정 → 참조가 유지됨 |
❌ 멀티스레드 비안전 | 멀티스레드 환경에서는
사용 |
✅ 마무리 요약
메서드 | 설명 |
---|---|
| 문자열 끝에 추가 |
| 중간에 삽입 |
| 구간 삭제 |
| 문자열 교체 |
| 문자열 뒤집기 |
| 문자 추출 |
| 문자 수정 |
| 최종 문자열 반환 |
| 내부 버퍼 용량 확인 |
| 버퍼 미리 확장 |
Java: StringBuffer 클래스
1. 왜 필요한가? (등장 배경 및 필요성)
1.1 StringBuffer
📌 문제 상황
- 기존
String
은 불변(immutable) → 문자열을 수정할 수 없음 "abc" + "def"
처럼 문자열을 반복적으로 연결하면 새로운 객체가 계속 생성됨- 루프 내 문자열 누적, 로그 생성, 텍스트 조립 시 성능 저하 & 메모리 낭비 발생
✅ 등장 이유
- 가변(mutable) 문자열 필요
- 기존 객체 안에서 문자열을 효율적으로 수정/추가/삭제 가능해야 함
- 그리고 멀티스레드 환경에서도 안전하게 사용 가능하도록 설계 필요
→ 그래서 등장한 것이 StringBuffer
(JDK 1.0부터 존재)
(후속으로 성능 개선 목적의 StringBuilder
는 JDK 1.5에 등장)
1.2 StringTokenizer
📌 문제 상황
- 문자열을 구분자(delimiter) 로 나누고 싶을 때마다
indexOf()
,substring()
을 반복하는 건 번거롭고 비효율적 - 문자열을 공백, 콤마, 탭 등으로 분리하는 간단한 기능이 자주 필요
✅ 등장 이유
split()
등장 이전부터 빠르게 문자열을 분리할 수 있는 도구 필요- 성능이 중요했던 옛 시절, 반복문에서 빠르게 동작하는 문자열 분리기
→ 그래서 등장한 것이 StringTokenizer
(JDK 1.0)
→ 이후 등장한 split()
에 비해 속도는 빠르지만 정규표현식은 불가
1.3 split()
(String 클래스의 메서드)
📌 문제 상황
StringTokenizer
는 정규표현식을 못 씀- 복잡한 구분자(예: 공백 여러 개, 특수문자 등)는 처리 어렵다
✅ 등장 이유
- 문자열을 자유로운 규칙(정규표현식)으로 분리할 수 있는 유연한 기능 필요
→ 그래서 등장한 것이 split()
메서드 (JDK 1.4부터 안정화)
- 정규식 기반, 가독성 높고 활용도 넓음
2. StringBuffer – 선언 방법 및 주요 사용법
2.1 선언 방법
StringBuffer sb1 = new StringBuffer(); // 빈 버퍼 (기본 용량 16)
StringBuffer sb2 = new StringBuffer(50); // 초기 버퍼 크기 지정
StringBuffer sb3 = new StringBuffer("Hello"); // 초기 문자열 지정
2.2 주요 메서드 예제
StringBuffer sb = new StringBuffer("Java");
sb.append(" is awesome"); // "Java is awesome"
sb.insert(4, " 8"); // "Java 8 is awesome"
sb.delete(0, 5); // "8 is awesome"
sb.reverse(); // "emosewa si 8"
System.out.println(sb.toString());
3. StringTokenizer – 선언 방법 및 사용법
3.1 선언 방법
StringTokenizer st = new StringTokenizer("a,b,c", ",");
3.2 반복 사용법
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
출력
a
b
c
3.3 기타 특징
특징 | 설명 |
---|---|
기본 구분자 | 공백 (
) |
성능 |
보다 빠름 |
정규식 지원 여부 | ❌ 불가능 |
4. split() – 선언 방법 및 사용법
4.1 기본 사용법
String str = "one,two,three";
String[] result = str.split(",");
4.2 정규표현식 사용 예
String s = "apple banana melon";
String[] arr = s.split("\\s+"); // 공백 하나 이상 기준
출력
apple
banana
melon
4.3 특수 문자 분리 (예: |, . 등)
String s = "a|b|c";
String[] arr = s.split("\\|"); // |는 정규식 특수문자라 \\로 이스케이프 필요
5. 코딩 테스트 실전 예제
5.1 공백 구분 숫자 입력
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] input = br.readLine().split(" ");
int a = Integer.parseInt(input[0]);
int b = Integer.parseInt(input[1]);
System.out.println(a + b);
5.2 빠른 입력 + 반복 분리 (StringTokenizer
)
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int sum = 0;
while (st.hasMoreTokens()) {
sum += Integer.parseInt(st.nextToken());
}
System.out.println(sum);
6. 요약 비교
항목 | StringBuffer | StringTokenizer | split() |
---|---|---|---|
용도 | 가변 문자열 조작 | 문자열 구분자 분할 | 정규식 기반 문자열 분할 |
동기화 | ✅ 있음 | ❌ 필요 없음 | ❌ 필요 없음 |
성능 | 중간 | 빠름 | 약간 느림 |
정규표현식 | ✖ | ✖ | ✅ 지원 |
등장 시기 | 오래됨 (JDK 1.0) | 오래됨 (JDK 1.0) | 나중에 도입됨 (JDK 1.4 이후) |
사용 추천 환경 | 멀티스레드 조작 | 빠른 단순 입력 분할 | 복잡한 기준 분할 or 가독성 우선 |
7. 결론
StringBuffer
: 멀티스레드 환경에서 가변 문자열 조작이 필요할 때 사용StringTokenizer
: 빠르고 간단한 문자열 분할이 필요할 때 유용 (특히 코테)split()
: 정규표현식으로 복잡한 기준 분할이 필요할 때 적합- 코딩 테스트에서는 상황에 따라 도구를 구분해서 사용하는 것이 중요
Java: StringBuffer 주요 메서드 정리
1. StringBuffer
란?
- 가변(mutable) 문자열 클래스
StringBuilder
와 API는 동일하지만, 멀티스레드 환경에서 안전하도록 모든 메서드에synchronized
처리- 성능은
StringBuilder
보다 약간 느리지만, 스레드 안정성 보장
2. 주요 메서드 + 내부 동작
아래 메서드들은
StringBuilder
와 메서드명이 동일하며, 내부적으로 synchronized 블록을 포함한다는 점만 다름.
🔹 append(String str)
- 문자열을 끝에 추가
- 예:
new StringBuffer("hi").append(" there") → "hi there"
- 시간복잡도: O(1) (평균), O(n) (버퍼 확장 시)
🔹 insert(int offset, String str)
- 지정 위치에 문자열 삽입
- 예:
"abc".insert(1, "X") → "aXbc"
- 시간복잡도: O(n)
🔹 delete(int start, int end)
- 주어진 범위 삭제
- 예:
"abcdef".delete(2, 4) → "abef"
- 시간복잡도: O(n)
🔹 deleteCharAt(int index)
- 인덱스에 해당하는 문자 하나 삭제
- 예:
"abc".deleteCharAt(1) → "ac"
- 시간복잡도: O(n)
🔹 replace(int start, int end, String str)
- 지정 범위 문자열을 새 문자열로 교체
- 예:
"abcde".replace(1, 4, "X") → "aXe"
- 시간복잡도: O(n)
🔹 reverse()
- 문자열을 뒤집음
- 예:
"hello".reverse() → "olleh"
- 시간복잡도: O(n)
🔹 toString()
- 최종 문자열을
String
객체로 반환 - 예:
sb.toString()
- 시간복잡도: O(n)
🔹 setCharAt(int index, char c)
- 인덱스 위치의 문자를 변경
- 예:
"java".setCharAt(1, 'o') → "jova"
- 시간복잡도: O(1)
🔹 charAt(int index)
- 지정 위치 문자 반환
- 예:
"java".charAt(2) → 'v'
- 시간복잡도: O(1)
🔹 length()
- 문자열 길이 반환
- 예:
"abc".length() → 3
- 시간복잡도: O(1)
🔹 capacity()
- 내부 버퍼 용량 반환
- 예:
new StringBuffer(10).capacity() → 10
- 시간복잡도: O(1)
🔹 ensureCapacity(int minimumCapacity)
- 버퍼 크기 미리 확보 (확장용)
- 예:
sb.ensureCapacity(100)
- 시간복잡도: O(n) (버퍼 확장 시)
3. 체이닝 예시
StringBuffer sb = new StringBuffer("Hi");
String result = sb.append(" there")
.replace(0, 2, "Bye")
.reverse()
.toString();
System.out.println(result); // "ereht eyB"
4. 주의사항
항목 | 설명 |
---|---|
스레드 안전성 | ✅ |
성능 | ❌ |
내부 수정 | ✅ 같은 객체 내에서 수정 |
메서드 반환값 | 대부분 |
✅ 요약 테이블
메서드 | 설명 | 시간복잡도 | 리턴값 |
---|---|---|---|
| 문자열 끝에 추가 | O(1~n) |
|
| 중간에 삽입 | O(n) |
|
| 구간 삭제 | O(n) |
|
| 문자 하나 삭제 | O(n) |
|
| 구간 대체 | O(n) |
|
| 문자열 뒤집기 | O(n) |
|
| 문자 조회 | O(1) |
|
| 문자 수정 | O(1) | void |
| 문자열 길이 | O(1) |
|
| 버퍼 크기 조회 | O(1) |
|
| 최소 용량 확보 | O(n) | void |
| 문자열 객체 반환 | O(n) |
|
Java 코딩 테스트: StringBuilder 활용 사례 모음
1. 왜 StringBuilder
를 쓸까?
코딩 테스트에서 문자열을 다룰 때는 String
보다 StringBuilder
나 StringBuffer
를 활용하는 것이 성능 면에서 매우 유리하다.
1.1 String
은 불변 (immutable)
String str = "a"; str += "b";
는 사실상 매번 새로운 객체를 생성합니다.- 반복 연결 시 매우 비효율적 →
O(n^2)
시간복잡도 발생 가능
1.2 StringBuilder
는 가변 (mutable)
- 내부
char[]
버퍼를 직접 수정 - 반복적으로 append해도 객체 1개로 해결 → 훨씬 빠름
📌 참고: StringBuilder
는 synchronized
가 아닌 대신 더 빠르다. 멀티스레드가 아니면 StringBuffer
보다 성능상 유리하다.
2. 자주 나오는 활용 패턴
2.1 문자열 뒤집기 (문제 유형: 팰린드롬 판단, 좌우 비교)
String str = "hello";
String reversed = new StringBuilder(str).reverse().toString();
System.out.println(reversed); // 출력: "olleh"
- 빠르게 문자열을 뒤집는 가장 간단한 방법
- 팰린드롬 문제에서
str.equals(reversed)
로 비교
2.2 대량 문자열 이어붙이기 (문제 유형: 문자열 압축, 변환 등)
StringBuilder sb = new StringBuilder();
for (char c : arr) {
sb.append(Character.toUpperCase(c)).append(", ");
}
String result = sb.toString();
- 루프 안에서 문자열을 누적할 때
+
대신append()
사용 String
으로 누적하면 매번 새로운 객체가 생김 → 성능 저하
2.3 조건에 따라 문자열 조립 (문제 유형: 포맷팅, 규칙 변환)
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
sb.append("[").append(i).append("]");
if (i < 5) sb.append("-");
}
System.out.println(sb.toString()); // 출력: [1]-[2]-[3]-[4]-[5]
- 특정 규칙을 가진 문자열을 만들어야 할 때
- 공백, 구분자, 특수문자 등을 유동적으로 넣기 좋음
2.4 문자열 일부 제거 (문제 유형: 특정 문자 제거, 구간 삭제)
StringBuilder sb = new StringBuilder("abcdef");
sb.delete(2, 4); // 인덱스 2~3 삭제 → "abef"
System.out.println(sb);
- 특정 구간 제거, 문자 삭제 등 깔끔하게 가능
- 문자열 슬라이싱처럼 사용할 수 있음
3. 코딩 테스트 팁
상황 | 추천 도구 | 이유 |
---|---|---|
문자열을 자주 수정/누적할 때 |
| 가변 객체, 빠른 속도 |
문자열을 뒤집을 때 |
| 메서드 하나로 간단 처리 |
멀티스레드 고려 필요 없음 |
| 동기화 없음 → 더 빠름 |
🧠 참고: 코테에서 StringBuilder
를 쓰는 것이 대부분 OK, StringBuffer
는 멀티스레드 환경에서만 고려
✅ 마무리 요약
- 문자열 반복 조작 문제에는 절대
String +="..."
쓰지 말기! - 대신
StringBuilder
로 누적하자 .append()
,.delete()
,.insert()
,.reverse()
등 다양한 메서드를 적재적소에 활용
코테에서 성능이 중요한 대형 문자열 문제에서는
StringBuilder
는 최고의 무기
Java: String.split() 정규식 예제 모음
✅ 기본 문법
String[] result = 문자열.split("정규표현식");
split()
은 인자로 정규표현식(String regex) 을 받기 때문에, 메타 문자(예: .
|
*
)는 반드시 이스케이프 해야 한다.
1. 공백 기준 분할
1.1 공백 하나
String s = "hello world";
String[] arr = s.split(" ");
1.2 공백 여러 개 (1개 이상)
String s = "hello world java";
String[] arr = s.split("\\s+"); // \\s = 공백 문자, +는 1개 이상
1.3 탭 또는 공백
String s = "a\tb c";
String[] arr = s.split("[ \t]+");
2. 콤마(,) 기준 분할
String s = "apple,banana,grape";
String[] arr = s.split(",");
2.1 콤마 + 공백 제거 (CSV 정리)
String s = "apple, banana, grape";
String[] arr = s.split(",\\s*"); // , 뒤 공백 무시
3. 특수 문자 기준 분할
정규표현식에서 특수 문자는 반드시 이스케이프(\) 필요!
문자 | 정규표현식에서 의미 | split에 쓰는 방식 |
---|---|---|
| 모든 문자 |
|
` | ` | OR (또는) |
| 반복자 |
|
| 그룹 지정 |
|
3.1 마침표(.
) 기준
String s = "www.example.com";
String[] arr = s.split("\\.");
3.2 파이프(|
) 기준
String s = "red|green|blue";
String[] arr = s.split("\\|");
4. 숫자/문자 기준 분할
4.1 숫자만 기준으로 나누기
String s = "abc123def456ghi";
String[] arr = s.split("\\d+"); // 숫자(1개 이상) 기준 나눔
4.2 문자 기준 나누기
String s = "123abc456def";
String[] arr = s.split("[a-zA-Z]+"); // 알파벳 기준 나눔
5. 복수 구분자 분리
5.1 콤마(,) 또는 세미콜론(;) 또는 공백
String s = "apple,banana;grape orange";
String[] arr = s.split("[,; ]");
5.2 AND/OR 조건 키워드 분리
String s = "red and blue or green";
String[] arr = s.split("\\s+(and|or)\\s+"); // 공백 포함한 and/or 기준
6. 문장 끝 단위 분리 (구두점)
String s = "Hi. I am John! Nice to meet you?";
String[] arr = s.split("[.!?]\\s*"); // . ! ? 뒤 공백까지 포함
7. 빈 문자열 처리
String s = "a,,b,c";
String[] arr = s.split(",", -1); // -1 옵션으로 빈 항목도 유지
System.out.println(Arrays.toString(arr));
// 출력: [a, , b, c]
🧾 보너스: 정규표현식 요약표
패턴 | 의미 | 예시 |
---|---|---|
| 공백 문자 (스페이스, 탭 등) |
→ 연속된 공백 |
| 숫자 (0~9) |
→ 숫자 1개 이상 |
| 문자/숫자/밑줄 |
|
| 아무 문자 한 개 |
→ "acb", "a1b" 가능 |
| a 또는 b 또는 c |
→ "abd", "acd" |
| a,b,c 제외한 문자 |
→ 숫자 제외 |
| 앞의 패턴 1번 이상 반복 |
|
| 0번 이상 반복 |
|
` | ` | OR |
✅ 결론
split()
은 정규표현식 기반의 유연한 문자열 분리가 가능하다.- 단순 입력에는
StringTokenizer
, 복잡한 기준 분리에는split()
이 적합. - 특수문자 분리 시 반드시 이스케이프(
\\
) 를 주의할 것!
Java: String vs StringBuilder 비교
1. 개요
자바에서 문자열을 다룰 수 있는 대표 클래스는 String
과 StringBuilder
이다. 두 클래스 모두 문자열을 표현하는 데 사용되지만, **불변성(immutability)**과 메모리 구조, 성능 면에서 근본적인 차이가 존재한다.
String
: 불변(immutable) 객체. 문자열을 수정하는 것처럼 보이지만 실제로는 새로운 객체가 생성됨.StringBuilder
: 가변(mutable) 객체. 동일 인스턴스 내의 버퍼를 직접 수정하여 효율적인 메모리 사용과 빠른 성능을 제공함.
이러한 차이로 인해, 문자열 처리 방식에 따라 두 클래스의 선택은 전혀 달라져야 한다.
2. 메모리 관점에서의 차이
2.1 String – 불변 객체의 메모리 동작
String
클래스는 한 번 생성된 이후로 내용을 절대 변경할 수 없는 불변 객체이다. 문자열을 수정할 때마다 새로운 객체가 생성되어 Heap 메모리에 누적된다.
예시
String s = "hi";
s = s + "!"; // "hi!"라는 새로운 String 객체가 생성됨
메모리 흐름
[Heap Memory]
"hi" ← 초기 객체
"hi!" ← 새로운 객체 (변수 s가 참조)
→ 기존 객체는 GC 대상이 되기 전까지 Heap에 남아 있음
→ 반복적인 수정 시 객체 수 증가 → GC 부하 및 메모리 낭비
주요 특징
- 내부적으로
char[]
배열을final
로 선언하여 불변성 보장 - 멀티스레드 환경에서 안전하게 공유 가능
- 문자열 상수 풀(String Constant Pool)을 활용한 최적화 가능
2.2 StringBuilder – 가변 객체의 효율적 구조
StringBuilder
는 내부에 동적으로 크기 조절이 가능한 char[] 버퍼를 가지고 있으며, 문자열 조작 시 이 버퍼를 직접 수정한다. 불필요한 객체 생성이 없고, 성능과 메모리 측면에서 뛰어나다.
예시
StringBuilder sb = new StringBuilder("hi");
sb.append("!");
System.out.println(sb); // 출력: hi!
메모리 흐름
[Heap Memory]
StringBuilder 객체
└─ char[] buffer = ['h', 'i', '!'] ← 내부 버퍼가 직접 수정됨
→ 동일 객체 내에서 작업 → 메모리 재사용
→ 객체 수 증가 없음 → GC 부담 없음
내부 구조 예시
public final class StringBuilder {
char[] value;
int count;
public StringBuilder append(String str) {
ensureCapacityInternal(count + str.length());
str.getChars(0, str.length(), value, count);
count += str.length();
return this;
}
}
3. 성능 차이 비교
3.1 코드 성능 테스트
// String (비효율적 방식)
String s = "";
for (int i = 0; i < 1000; i++) {
s += "a"; // 매번 새로운 객체 생성됨
}
// StringBuilder (효율적 방식)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("a"); // 내부 버퍼만 수정됨
}
3.2 성능 분석
항목 | String | StringBuilder |
---|---|---|
메모리 구조 | 불변 객체, 매번 새 객체 생성 | 가변 객체, 하나의 버퍼 사용 |
연결 방식 |
또는
호출마다 새 객체 |
는 동일 객체에 누적 |
GC 부하 | 심함 | 적음 |
속도 | 느림 | 빠름 |
💡 결론: 루프나 재귀에서 문자열을 누적해야 할 경우, StringBuilder
가 수십 배 빠를 수 있음.
3.3 스레드 안전성 관점
String
: 불변 → 스레드 안전StringBuilder
: 가변 + 동기화 없음 → 스레드 안전하지 않음- 다중 스레드에서 문자열 조작이 필요하다면 →
StringBuffer
사용 고려
// 멀티스레드 환경에서 StringBuilder는 위험
StringBuilder sb = new StringBuilder();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
sb.append("x"); // 동기화 안 되어 경합 발생 가능
}
};
→ 해결책: StringBuffer
또는 synchronized
블록 사용
4. 결론 및 실무 가이드라인
4.1 어떤 클래스가 적절한가?
사용 상황 | 적절한 클래스 | 이유 |
---|---|---|
고정된 문자열 / 변경 없음 |
| 불변성으로 안전하고, 메모리 캐싱 가능 |
루프 내 문자열 누적 / 대량 조작 |
| 메모리 효율 및 성능 우수 |
멀티스레드 환경에서 동기화가 필요한 경우 |
| 메서드 동기화로 스레드 안전성 확보 |
4.2 실전 팁
- ⚠
String
으로 반복 연결하지 말 것 (예:s = s + "a"
X) - ✅ 대량 처리 시에는 반드시
StringBuilder
사용 - ✅ 멀티스레드 환경에서는
StringBuffer
또는 동기화 처리 필수
Java: String vs StringBuilder vs StringBuffer 비교
1. 개요
자바에서 문자열을 다룰 때 흔히 사용하는 세 가지 클래스인 String, StringBuilder, StringBuffer는 모두 CharSequence 인터페이스를 구현하지만, 불변성, 동기화, 성능 측면에서 명확한 차이를 보인다.
- String은 문자열 변경 시마다 새로운 객체를 생성 → 여러 객체가 Heap에 누적
- StringBuilder와 StringBuffer는 동일 객체 내에서 내부 버퍼를 수정 → Heap 메모리를 효율적으로 사용
- StringBuffer는 동기화를 위한 추가 비용으로 인해 메모리뿐 아니라 CPU 리소스도 더 소모됨
이 문서에서는 다음과 같은 관점에서 세 클래스의 차이점을 심층 분석한다.
- 변경 가능 여부 (immutable vs mutable)
- 성능 및 메모리 구조
- 스레드 안전성 및 동기화 여부
- 사용 예시 및 실제 적용 시 고려사항
2. String
2.1 불변 객체(Immutable)의 특징
String
객체는 **불변(immutable)**이다.- 문자열이 변경되는 것처럼 보이지만 실제로는 새로운 객체가 생성된다.
String str = "hello";
str += " world";
// 실제로는 "hello", "hello world" 두 개의 객체가 Heap에 존재
이유
- 내부적으로
char[]
배열을 final로 가지고 있으며, 변경이 불가능 final class String
→ 상속 불가- 안전성, 보안, 캐싱, 문자열 상수 풀(String Pool) 최적화에 유리
2.2 성능 및 메모리 측면
- 연결이 많을수록 비효율적: 반복적인 문자열 조작 시 매번 새로운 객체 생성
- Garbage Collector 부하 증가
2.3 사용 예시
- 단순 문자열 선언 및 불변 문자열 상수
- 파라미터 값, 로그인 ID, 파일 경로 등 변경되지 않는 문자열
3. StringBuilder
3.1 가변 객체(Mutable) + 비동기 환경에 적합
- 내부에
char[]
버퍼를 가지고 있으며, 동기화를 고려하지 않음 - 성능과 메모리 측면에서 매우 효율적
StringBuilder sb = new StringBuilder("hello");
sb.append(" world");
System.out.println(sb.toString()); // hello world
3.2 주요 메서드
sb.append("a");
sb.insert(2, "x");
sb.replace(1, 3, "yz");
sb.delete(0, 2);
sb.reverse();
3.3 성능 테스트 예시
public class BuilderTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) {
sb.append("abc");
}
long end = System.currentTimeMillis();
System.out.println("StringBuilder 소요 시간: " + (end - start));
}
}
3.4 사용 시점
- 단일 스레드 환경에서 반복적인 문자열 조작
- 파일 생성, 로깅 등 실시간 데이터 처리
4. StringBuffer
4.1 가변 객체(Mutable) + 스레드 안전(Thread-safe)
StringBuilder
와 거의 동일하나, 모든 메서드가 synchronized 처리- 멀티스레드 환경에서 안정성 보장
StringBuffer sb = new StringBuffer("safe");
sb.append(" thread");
System.out.println(sb); // safe thread
4.2 내부 구조
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
4.3 성능 비교
- 동기화 오버헤드로 인해 성능은
StringBuilder
보다 낮음 - 병렬 처리나 스레드풀 환경에서 안정성을 확보해야 할 때 적합
4.4 멀티스레드 예시
public class BufferThreadTest {
static StringBuffer sb = new StringBuffer();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
sb.append("a");
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("최종 길이: " + sb.length()); // 2000 보장됨
}
}
5. 성능 및 메모리 비교 요약
항목 | String | StringBuilder | StringBuffer |
---|---|---|---|
변경 가능 여부 | ❌ 불변 | ✅ 가변 | ✅ 가변 |
동기화 지원 | ❌ 없음 | ❌ 없음 | ✅ |
성능 | 느림 (매번 객체 생성) | 빠름 | 중간 (동기화 오버헤드 존재) |
메모리 효율성 | 낮음 | 높음 | 중간 |
적합한 환경 | 상수, 설정값, 로그 ID 등 | 단일 스레드 문자열 조작 | 멀티스레드 문자열 조작 |
6. 결론 및 실무 팁
- ✅ String: 변경이 없는 정적 문자열 (상수, config 값 등)
- ✅ StringBuilder: 반복문 내에서 문자열 누적, 대량 처리 시
- ✅ StringBuffer: 동기화가 필요한 멀티스레드 환경에서만 사용
✅ StringBuilder
vs StringBuffer
– 언제 뭐 써야 해?
항목 | StringBuilder | StringBuffer |
---|---|---|
스레드 안전성 | ❌ 비동기(스레드 안전 아님) | ✅ 동기화(synchronized, 스레드 안전) |
속도 | 빠름 | 상대적으로 느림 |
코딩 테스트/일반 개발 | ✅ 단일 스레드 환경에서 최적 | ❌ 보통 코테에선 불필요한 오버헤드 발생 |
멀티스레드 환경 | ❌ 사용 시 동기화 문제 생길 수 있음 | ✅ 멀티스레드 환경에서 안전하게 사용 가능 |
사용 예 | 코딩테스트, 파일 파싱, 로그 누적, 텍스트 생성 | 서버에서 공유 자원 처리, 멀티스레드 로그 등 |
- 코딩 테스트, 단일 스레드 로직:
StringBuilder
사용 (기본값) - 멀티스레드 환경 (예: 서버 개발):
StringBuffer
사용
💡 JDK 1.5 이상에서는 StringBuilder
를 기본으로 고려하고, 동기화가 꼭 필요한 경우에만 StringBuffer
를 선택하자.
Java: 기본형(int) vs String(불변 객체)의 메모리 구조 비교
1. Java의 메모리 구조
Java 프로그램은 크게 세 가지 메모리 공간을 사용한다:
1.1 Stack 영역
- 지역 변수 및 기본형(primitive type) 값 저장
- 메서드 호출 시마다 프레임이 생성되고, 종료되면 자동으로 해제됨
- 매우 빠르며, GC 대상이 아님
1.2 Heap 영역
new
연산자 등을 통해 생성된 객체가 저장됨ArrayList
,String
,Integer
, 사용자 정의 클래스 등- GC(Garbage Collector)에 의해 관리됨
1.3 String Constant Pool (리터럴 풀)
- Heap 내부의 특별한 공간
- 동일한 문자열 리터럴을 공유하여 메모리 절약
"hello"
와 같은 리터럴은 Pool에서 재사용됨
2. 기본형 변수 (예: int
)
기본형은 값 그 자체가 Stack 메모리에 저장된다. 변수 간의 대입은 값 복사이며, 변수 간 영향이 없다.
2.1 코드 예시
int a = 10;
int b = a;
a = 20;
2.2 메모리 흐름
초기 상태:
[Stack]
a → 10
b → 10 (값 복사됨)
a = 20; 이후:
a → 20
b → 10 (영향 없음)
📌 int
는 값 자체(value) 를 저장하며, 변수 간 대입은 값 복사다.
값을 변경해도 기존 변수나 참조에 영향을 주지 않는다.
3. String (불변 객체)
String
은 참조형(reference type) 객체이며, Stack에는 참조만 저장되고 실제 문자열은 Heap 또는 Constant Pool에 존재한다.
3.1 코드 예시
String s1 = "hello";
String s2 = s1;
s1 = s1 + " world";
3.2 메모리 흐름
[초기 상태]
[Stack] [Heap (String Pool)]
s1 ─┐ "hello"
└────────▶
s2 ─┘ (s1, s2 둘 다 "hello" 참조)
[변경 후]
[Stack] [Heap]
s1 ──────────▶ "hello world" ← 새 객체 생성
s2 ──────────▶ "hello" ← 원본 유지
→ s1 + " world" 는 기존 "hello"를 수정하는 것이 아니라
**"hello world"라는 새로운 객체를 생성**
📌 String
은 불변(immutable) 이기 때문에 수정이 아닌 새 객체 생성 방식으로 동작한다.
따라서 반복 수정이 많으면 Heap에 객체가 누적 → GC 부담 증가 가능.
4. 예제 비교 요약
// 기본형
int a = 10;
int b = a;
a = 20; // b는 여전히 10
// 참조형 (String)
String s1 = "hi";
String s2 = s1;
s1 = s1 + "!"; // s2는 여전히 "hi"
항목 | 기본형 (int 등) | 참조형 (String) |
---|---|---|
저장 위치 | Stack | Stack(참조), Heap(객체) |
대입 방식 | 값 복사 | 참조값 복사 |
수정 시 영향 | 원본과 무관 | 불변 → 새 객체 생성 |
메모리 부담 | 없음 | 반복 수정 시 Heap 객체 증가 가능 |
5. 요약 및 실무 팁
- 🔹 기본형은 값 자체를 Stack에 저장 → 변경 간섭 없음, 매우 빠름
- 🔹 String은 참조형이며 불변 → 수정 시 마다 새 객체 생성
- 🔸 반복적인 문자열 연결/수정이 필요한 경우 →
StringBuilder
사용이 권장됨
// 비효율 (String)
String result = "";
for (int i = 0; i < 1000; i++) {
result += "a"; // 1000개 객체 생성
}
// 효율적 (StringBuilder)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("a");
}
String result = sb.toString(); // 단 1개 객체 사용
6. 관련 참고
- 불변 객체:
String
,Integer
,Boolean
,LocalDate
- 가변 객체:
StringBuilder
,ArrayList
,HashMap
📌 불변성 덕분에 String은 안정적이고 안전하지만, 반복 수정 시에는 가변 객체 사용이 메모리 효율에 유리하다.
1. Java 의 메모리 구조
세 가지 메모리 영역
- Stack : 지역 변수, 기본형 변수 저장 (`int`, `boolean`, etc)
- Heap : `new` 키워드나 객체 생성 시 사용 (`String`, `ArrayList`, etc)
- String Constant Pool : 같은 문자열 리터럴을 재사용하는 공간 (Heap 내부 특수 영역)
2. 기본형 변수(int 등)
값 자체가 Stack에 저장됨
int a = 10;
int b = a;
a = 20;
📌 메모리 흐름:
[Stack]
a → 10
b → 10 (복사됨)
a = 20; 이후
a → 20
b → 10 (영향 없음)
- int는 값(value) 자체가 저장되므로 대입 시 값만 복사됨
- 변경해도 다른 변수에 영향 없음
3 . String (불변 객체)
참조값이 Stack에, 실제 값은 Heap/String Pool에 저장
String s1 = "hello";
String s2 = s1;
s1 = s1 + " world";
📌 메모리 흐름:
초기 상태:
[Stack] [Heap (String Pool)]
s1 ─┐ "hello"
└────────────▶
s2 ─┘
변경 후:
[Stack] [Heap]
s1 ──────────────▶ "hello world" ← 새 객체 (불변성)
s2 ──────────────▶ "hello" ← 원본은 그대로 유지
- s1 + " world"는 기존 문자열을 수정하지 않고, 새로운 문자열 객체 생성
- 기존 "hello"는 s2가 여전히 참조 중
- 따라서 메모리 활용방식이 더 복잡 (GC 대상 증가 가능)
4. 예제 비교
// 기본형
int a = 10;
int b = a;
a = 20; // b는 여전히 10
// String
String s1 = "hi";
String s2 = s1;
s1 = s1 + "!"; // s2는 여전히 "hi"
- 기본형은 값 그 자체를 다루고, 수정 시 그냥 덮어쓰기
- String은 객체의 참조를 다루며, 수정 시 새 객체 생성 = 불변성 유지
이 불변성 덕분에 String은 안정적이고 안전하지만, 반복 수정이 많을 땐 StringBuilder를 쓰는 게 더 효율적이다.
Java: 문자열 리터럴 vs new String() 객체 생성 차이
1. 개요
Java에서 문자열은 두 가지 방식으로 생성할 수 있다:
String s1 = "hello"; // 리터럴 방식
String s2 = new String("hello"); // new 키워드 방식
두 방식은 겉보기에는 동일한 문자열 값을 가지지만, 내부 메모리 구조, 생성 방식, 비교 결과, 성능 등에서 중요한 차이를 가진다.
2. 메모리 구조 차이
2.1 리터럴 방식 (String s = "hello"
)
- 문자열 리터럴은 String Constant Pool이라는 특별한 메모리 영역에 저장된다.
- 동일한 리터럴이 여러 번 사용되더라도 중복 없이 하나의 객체로 공유됨.
- JVM이 자동 최적화해 메모리 사용이 매우 효율적이다.
예시
String a = "hello";
String b = "hello";
System.out.println(a == b); // true (같은 객체 참조)
System.out.println(a.equals(b)); // true (내용도 동일)
→ 같은 상수 풀 객체를 참조하고 있기 때문에 a == b
는 true
.
2.2 new 연산자 방식 (String s = new String("hello")
)
new String()
은 무조건 Heap 메모리에 새로운 객체를 생성한다.- 내부적으로 리터럴
"hello"
를 참조하더라도, 그 값을 복제한 별도 인스턴스가 생성됨.
예시
String a = "hello";
String b = new String("hello");
System.out.println(a == b); // false (다른 참조)
System.out.println(a.equals(b)); // true (내용은 같음)
→ ==
결과는 false
, .equals()
는 true
3. 객체 비교 방식
3.1 ==
연산자 (참조 비교)
- 두 객체의 참조 주소값을 비교함
- 리터럴 방식끼리는 상수 풀 공유로
true
일 수 있음 new
로 생성한 객체는 항상false
3.2 .equals()
메서드 (값 비교)
- 두 문자열의 내용이 같은지 비교함
- 방식과 무관하게, 내용이 같으면 항상
true
String s1 = "test";
String s2 = new String("test");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
4. 성능 및 메모리 효율
항목 | 리터럴 방식 ( "hello" ) | new 방식 ( new String("hello") ) |
---|---|---|
메모리 위치 | Constant Pool (Method Area) | Heap 영역 |
객체 중복 여부 | 없음 (공유됨) | 항상 새 객체 생성 |
비교 결과 | true (동일 참조) | false (서로 다른 참조) |
결과 | true | true |
메모리 효율 | 높음 | 낮음 |
성능 | 빠름 | 느림 (GC 대상 증가) |
GC 부담 | 낮음 | 높음 |
사용 권장 여부 | ✅ 일반적으로 권장 | ❌ 특별한 경우에만 사용 |
5. 실제 사용 시 차이
5.1 리터럴 방식 – 일반적으로 권장
- 정적 값, 설정 상수, 조건 분기 등에 적합
- 성능과 메모리 효율이 우수함
if (userRole.equals("ADMIN")) {
// 권한 체크
}
5.2 new 방식 – 특수 목적용
- 동일한 문자열이더라도 서로 다른 객체를 명시적으로 생성할 때
- 예: 문자열 캐시 우회, 디버깅용 비교, 참조를 강제로 분리해야 할 경우
- 필요하다면
.intern()
메서드를 사용해 수동으로 상수 풀 등록 가능
String s = new String("hello").intern(); // Constant Pool 등록
6. 정리: 리터럴 vs new String()
비교 항목 | 리터럴 방식 ( "hello" ) | new 방식 ( new String("hello") ) |
---|---|---|
저장 위치 | String Constant Pool | Heap 메모리 |
객체 공유 여부 | ✅ 공유됨 (중복 방지) | ❌ 매번 새 객체 생성 |
비교 결과 | true | false |
결과 | true | true |
성능 | 빠름 | 느림 |
메모리 효율 | 높음 | 낮음 |
GC 부담 | 낮음 | 높음 |
사용 추천 여부 | ✅ 일반적 상황에서 적극 권장 | ⚠️ 특별한 목적이 없으면 지양 |
7. 결론
- 문자열은 리터럴 방식으로 생성하는 것이 가장 효율적이다.
new String()
방식은 불필요한 Heap 객체 생성으로 인해 성능과 메모리 측면에서 불리하다.- 문자열 비교 시에는 항상
==
이 아닌.equals()
를 사용할 것. - 고급 최적화가 필요한 경우에는
.intern()
으로 Constant Pool을 수동 활용할 수 있다.
이 정리는 Java
메모리 구조, 성능 최적화, 참조와 객체 비교에 대해 정확한 이해를 돕기 위한 위키 스타일
자바에서 문자열은 두 가지 방법으로 생성할 수 있다.
String str = "hello";와 String str = new String("hello"); 두 가지 방식은 같아 보이지만, 작동 방식, 메모리 구조, 성능, 객체 비교 등 여러 측면에서 차이가 난다.
String s1 = "hello"; // 리터럴 방식
String s2 = new String("hello"); // new 연산자 사용
두 방식은 겉보기에는 같은 값을 가지지만, 메모리 처리, 비교 방법, 성능 등의 측면에서 분명한 차이가 존재한다.
1. 메모리 구조 차이
1.1 리터럴 방식: String s1 = "hello";
문자열 리터럴은 String Constant Pool에 저장된다.
JVM은 동일한 문자열 리터럴이 여러 번 등장하더라도 중복을 제거하여 하나의 객체만 저장한다.
메모리 효율이 높고, 비교 연산 시 빠르다.
📌 예시
String a = "hello";
String b = "hello";
System.out.println(a == b); // true (같은 상수 풀 객체)
1.2 new 연산자 방식: String s2 = new String("hello");
new 키워드를 사용하면 Heap에 새로운 객체가 강제로 생성된다.
비록 내부적으로 "hello"라는 리터럴이 사용되더라도, Constant Pool의 객체를 복제한 별도 인스턴스가 Heap에 만들어진다.
따라서 리터럴과는 다른 참조값을 가진다.
📌 예시
String a = "hello";
String b = new String("hello");
System.out.println(a == b); // false (주소 다름)
System.out.println(a.equals(b)); // true (값 같음)
2. 객체 비교 (== vs equals())
2.1 == (참조 비교)
리터럴 방식은 JVM이 Constant Pool을 공유하므로 == 결과가 true일 수 있다.
new 키워드는 항상 새로운 객체를 생성하므로 == 비교 시 false가 된다.
2.2 .equals() (내용 비교)
두 방식 모두 문자열 내용을 같게 만들면 .equals()는 항상 true를 반환한다.
문자열 비교에서는 항상 .equals()를 사용하는 것이 안전하다.
3. 성능 및 메모리 효율
3.1 리터럴 방식
- 런타임 시 상수 풀을 이용하므로 메모리 사용이 효율적이다.
- JVM이 자동으로 중복 문자열을 제거해주므로 불필요한 객체 생성을 방지할 수 있다.
- 가장 선호되는 방식.
3.2 new 방식
- new String()을 사용할 경우 매번 객체가 새로 만들어지므로 메모리 낭비 가능성이 있다.
- GC(Garbage Collector) 부담 증가 → 성능 저하로 이어질 수 있음.
- 특별한 이유(예: 강제로 다른 참조를 만들어야 할 필요)가 없다면 지양하는 것이 좋다
4. 실제 사용 시 차이점
4.1 리터럴 방식의 사용 예
- 상수 정의, 비교가 필요한 경우
- 조건 분기, 설정 값 체크 등에서 성능 이점 있음.
if (userRole.equals("ADMIN")) { ... }
4.2 new 방식의 사용 예
- 객체 생성을 명시적으로 컨트롤해야 하는 특수한 경우
- 외부 API나 라이브러리에서 intern() 메서드를 사용하여 상수 풀로 다시 보낼 수도 있음
String s = new String("hello").intern(); // 풀로 보냄
5. 정리
리터럴 방식 ( | new 방식 ( | |
---|---|---|
메모리 위치 | Constant Pool (Method Area) | Heap 영역 |
객체 중복 여부 | 없음 (공유됨) | 항상 새 객체 생성 |
| true (같은 참조) | false (다른 객체) |
| true | true |
메모리 효율 | 높음 | 낮음 |
성능 | 빠름 | 느림 |
GC 부담 | 낮음 | 높음 |
사용 권장 여부 | 일반적으로 권장 | 특별한 경우에만 사용 |
6. 결론
- 문자열을 선언할 때는 특별한 이유가 없다면 "hello"와 같은 리터럴 방식을 사용하는 것이 가장 효율적이다.
- new String()은 불필요한 객체 생성을 유발하며, 메모리와 성능 측면에서 손해를 볼 수 있다.
- 문자열 비교 시에는 항상 ==가 아닌 .equals()를 사용할 것.
- 최적화를 원할 경우 intern()을 활용해 상수 풀을 직접 사용할 수도 있다.