본문으로 건너뛰기

9.2 String 클래스 완전 정복 (String Class)

노트

이 문서는 Java 21 버전을 기준으로 작성되었습니다.

자바 프로그래밍에서 가장 많이 다루는 데이터 타입 중 하나가 바로 텍스트, 즉 문자열(String) 입니다. java.lang 패키지에 포함된 String 클래스는 텍스트를 담고 제어하는 강력한 기능들을 무수히 제공합니다.

1. String의 불변성(Immutability)

자바에서 자주 쓰이는 String은 단순한 자료형이 아니라 "문자열을 담고 있는 클래스(객체)"입니다. String 클래스를 쓸 때 반드시 기억해야 할 가장 결정적인 성질이 있습니다. 바로 한 번 생성된 String의 내용물은 메모리 상에서 절대로 변경할 수 없다(불변성) 는 점입니다.

public class StringImmutability {
public static void main(String[] args) {
String a = "Hello";
String b = a; // a와 b는 같은 객체를 가리킴

a = a + " World"; // 새로운 String 객체 "Hello World"가 생성됨!

System.out.println(a); // Hello World (새 객체)
System.out.println(b); // Hello (원래 객체, 변하지 않음!)

// String Pool: 같은 리터럴은 같은 객체를 공유
String s1 = "Java";
String s2 = "Java";
String s3 = new String("Java"); // 새 객체 강제 생성

System.out.println(s1 == s2); // true (같은 풀의 객체)
System.out.println(s1 == s3); // false (다른 객체)
System.out.println(s1.equals(s3)); // true (내용은 같음)
}
}
문자열 비교

==주소(참조) 를 비교하므로 문자열 내용 비교에는 절대 사용하지 마세요. 문자열 내용 비교는 항상 equals() 를 사용합니다.

String Pool과 intern()

public class StringPoolDemo {
public static void main(String[] args) {
String s1 = new String("Java"); // 힙에 새 객체 생성
String s2 = s1.intern(); // String Pool에 등록(이미 있으면 재사용)
String s3 = "Java"; // String Pool에서 가져옴

System.out.println(s2 == s3); // true (둘 다 Pool의 같은 객체)
System.out.println(s1 == s3); // false (s1은 힙의 별도 객체)
}
}

2. 문자열 생성과 기본 비교

public class StringCreation {
public static void main(String[] args) {
// 다양한 생성 방법
String s1 = "Hello, Java!";
String s2 = String.valueOf(42); // 숫자 → 문자열
String s3 = String.valueOf(true); // boolean → 문자열
String s4 = String.valueOf(3.14); // double → 문자열
char[] chars = {'J', 'a', 'v', 'a'};
String s5 = new String(chars); // char 배열 → 문자열

System.out.println(s1); // Hello, Java!
System.out.println(s2); // 42
System.out.println(s3); // true
System.out.println(s4); // 3.14
System.out.println(s5); // Java

// 비교 메서드
String a = "Hello";
String b = "hello";
System.out.println(a.equals(b)); // false (대소문자 구분)
System.out.println(a.equalsIgnoreCase(b)); // true (대소문자 무시)
System.out.println(a.compareTo(b)); // 음수 (H가 h보다 앞)
System.out.println(a.compareToIgnoreCase(b)); // 0 (같음)
}
}

3. String 메서드 완전 정리

길이와 문자 접근

public class StringLengthChar {
public static void main(String[] args) {
String s = "Hello, Java!";

// length(): 문자열 길이
System.out.println(s.length()); // 12

// charAt(index): 특정 위치의 문자
System.out.println(s.charAt(0)); // H
System.out.println(s.charAt(7)); // J

// isEmpty(): 빈 문자열 여부 (length() == 0)
System.out.println("".isEmpty()); // true
System.out.println(" ".isEmpty()); // false (공백은 비어있지 않음)

// isBlank(): 공백만 있는지 여부 (Java 11+)
System.out.println(" ".isBlank()); // true
System.out.println(" a".isBlank()); // false

// toCharArray(): 문자 배열로 변환
char[] arr = s.toCharArray();
System.out.println(arr[0]); // H
}
}

검색과 확인

public class StringSearch {
public static void main(String[] args) {
String s = "Hello, Java Programming!";

// contains(): 포함 여부
System.out.println(s.contains("Java")); // true
System.out.println(s.contains("Python")); // false

// startsWith() / endsWith(): 시작/끝 확인
System.out.println(s.startsWith("Hello")); // true
System.out.println(s.endsWith("!")); // true

// indexOf(): 처음 등장하는 위치 (없으면 -1)
System.out.println(s.indexOf("a")); // 8 (Java의 a)
System.out.println(s.indexOf("z")); // -1 (없음)

// lastIndexOf(): 마지막으로 등장하는 위치
System.out.println(s.lastIndexOf("a")); // 20 (Programming의 a)

// indexOf(str, fromIndex): 특정 위치부터 검색
System.out.println(s.indexOf("a", 10)); // 14 (Programming의 첫 a)
}
}

추출과 분리

public class StringExtract {
public static void main(String[] args) {
String s = "Hello, Java Programming!";

// substring(start): start 인덱스부터 끝까지
System.out.println(s.substring(7)); // Java Programming!

// substring(start, end): start부터 end-1까지
System.out.println(s.substring(7, 11)); // Java

// split(regex): 구분자로 분리
String csv = "apple,banana,cherry,date";
String[] fruits = csv.split(",");
for (String f : fruits) {
System.out.print(f + " "); // apple banana cherry date
}
System.out.println();

// split(regex, limit): 최대 limit개로 분리
String[] parts = csv.split(",", 2);
System.out.println(parts[0]); // apple
System.out.println(parts[1]); // banana,cherry,date

// 정규식으로 분리
String sentence = "one two three"; // 여러 공백
String[] words = sentence.split("\\s+"); // 하나 이상의 공백
System.out.println(words.length); // 3
}
}

변환과 수정

public class StringTransform {
public static void main(String[] args) {
String s = " Hello, Java! ";

// trim(): 앞뒤 공백 제거 (ASCII 공백만)
System.out.println(s.trim()); // "Hello, Java!"

// strip(): 앞뒤 공백 제거 (유니코드 공백 포함, Java 11+)
System.out.println(s.strip()); // "Hello, Java!"
System.out.println(s.stripLeading()); // "Hello, Java! " (앞만)
System.out.println(s.stripTrailing()); // " Hello, Java!" (뒤만)

// toUpperCase() / toLowerCase()
System.out.println("hello".toUpperCase()); // HELLO
System.out.println("JAVA".toLowerCase()); // java

// replace(old, new): 모든 해당 문자열 교체
String str = "banana";
System.out.println(str.replace("a", "o")); // bonono

// replaceAll(regex, replacement): 정규식으로 교체
String phone = "010-1234-5678";
System.out.println(phone.replaceAll("-", "")); // 01012345678
System.out.println(phone.replaceAll("[0-9]", "*")); // ***-****-****

// replaceFirst(regex, replacement): 첫 번째만 교체
System.out.println("aaa".replaceFirst("a", "b")); // baa

// repeat(n): 문자열 반복 (Java 11+)
System.out.println("ha".repeat(3)); // hahaha
}
}

연결과 포맷

public class StringFormat {
public static void main(String[] args) {
// concat(): 문자열 연결 (+ 연산자와 동일하지만 null 허용 안 함)
String s = "Hello".concat(", World!");
System.out.println(s); // Hello, World!

// String.join(): 구분자로 연결
String joined = String.join("-", "2026", "03", "23");
System.out.println(joined); // 2026-03-23

String.join(", ", "apple", "banana", "cherry"); // apple, banana, cherry

// String.format(): C의 printf 스타일 포맷 (Java 1.5+)
String name = "홍길동";
int age = 30;
double score = 95.5;
String formatted = String.format("이름: %s, 나이: %d세, 점수: %.1f점", name, age, score);
System.out.println(formatted); // 이름: 홍길동, 나이: 30세, 점수: 95.5점

// formatted(): 인스턴스 메서드 버전 (Java 15+)
String result = "이름: %s, 나이: %d세".formatted(name, age);
System.out.println(result); // 이름: 홍길동, 나이: 30세

// 주요 포맷 지정자
System.out.printf("%d%n", 42); // 정수
System.out.printf("%05d%n", 42); // 005자리, 0으로 패딩: 00042
System.out.printf("%.2f%n", 3.14159); // 소수점 2자리: 3.14
System.out.printf("%-10s|%n", "left"); // 왼쪽 정렬, 10칸: left |
System.out.printf("%10s|%n", "right"); // 오른쪽 정렬, 10칸: right|
}
}

4. 문자열 타입 변환

문자열 → 다른 타입 변환

public class StringParsing {
public static void main(String[] args) {
String intStr = "123";
String doubleStr = "3.14";
String boolStr = "true";
String longStr = "9876543210";

// 문자열 → 기본형
int i = Integer.parseInt(intStr); // 123
double d = Double.parseDouble(doubleStr); // 3.14
boolean b = Boolean.parseBoolean(boolStr); // true
long l = Long.parseLong(longStr); // 9876543210

// 문자열 → 래퍼 객체 (Integer.valueOf는 캐시를 활용)
Integer iObj = Integer.valueOf(intStr); // Integer 객체
Double dObj = Double.valueOf(doubleStr); // Double 객체

System.out.printf("int: %d, double: %.2f, boolean: %b, long: %d%n", i, d, b, l);

// 진수 변환
int binary = Integer.parseInt("1010", 2); // 이진수 "1010" → 10
int hex = Integer.parseInt("FF", 16); // 16진수 "FF" → 255
System.out.println("1010(2) = " + binary); // 10
System.out.println("FF(16) = " + hex); // 255
}
}

다른 타입 → 문자열 변환

public class ToStringConversion {
public static void main(String[] args) {
int i = 42;
double d = 3.14;
boolean b = true;
char c = 'A';

// String.valueOf(): 가장 안전한 방법 (null → "null")
String s1 = String.valueOf(i); // "42"
String s2 = String.valueOf(d); // "3.14"
String s3 = String.valueOf(b); // "true"
String s4 = String.valueOf(c); // "A"

// toString() 메서드 (null이면 NullPointerException!)
String s5 = Integer.toString(i); // "42"
String s6 = Integer.toString(255, 16); // "ff" (16진수로 변환)
String s7 = Integer.toBinaryString(10); // "1010" (이진수)
String s8 = Integer.toHexString(255); // "ff"
String s9 = Integer.toOctalString(8); // "10" (8진수)

// + 연산자 (빈 문자열과 더하기)
String s10 = "" + i; // "42"

System.out.println(s1 + ", " + s6 + ", " + s7 + ", " + s9);
// 42, ff, 1010, 10
}
}

5. 정규표현식(Regular Expression)

public class StringRegex {
public static void main(String[] args) {
// matches(): 전체 문자열이 패턴과 일치하는지
String email = "user@example.com";
String emailPattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
System.out.println(email.matches(emailPattern)); // true

String badEmail = "not-an-email";
System.out.println(badEmail.matches(emailPattern)); // false

// replaceAll(): 정규식으로 일괄 교체
String dirty = " Hello World ";
String clean = dirty.trim().replaceAll("\\s+", " ");
System.out.println(clean); // Hello World

// 전화번호 형식 통일
String phone1 = "010-1234-5678";
String phone2 = "01012345678";
String phone3 = "010.1234.5678";

String normalized = phone1.replaceAll("[^0-9]", ""); // 숫자만 남김
System.out.println(normalized); // 01012345678

// split(): 정규식으로 분리
String data = "one1two2three3four";
String[] parts = data.split("[0-9]"); // 숫자로 분리
for (String p : parts) System.out.print(p + " ");
// one two three four
System.out.println();

// 숫자만 있는지 검사
System.out.println("12345".matches("[0-9]+")); // true
System.out.println("123a5".matches("[0-9]+")); // false
}
}

6. 실전 예제 1: 이메일 유효성 검사

public class EmailValidator {

private static final String EMAIL_PATTERN =
"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";

public static boolean isValidEmail(String email) {
if (email == null || email.isBlank()) {
return false;
}
return email.matches(EMAIL_PATTERN);
}

public static String maskEmail(String email) {
if (!isValidEmail(email)) return "유효하지 않은 이메일";
int atIndex = email.indexOf('@');
String local = email.substring(0, atIndex);
String domain = email.substring(atIndex);
if (local.length() <= 2) return local.charAt(0) + "**" + domain;
return local.substring(0, 2) + "*".repeat(local.length() - 2) + domain;
}

public static void main(String[] args) {
String[] emails = {
"user@example.com",
"hong.gildong@company.co.kr",
"invalid-email",
"no-at-sign.com",
"@domain.com",
""
};

System.out.println("=== 이메일 유효성 검사 ===");
for (String email : emails) {
System.out.printf("%-30s → 유효: %b, 마스킹: %s%n",
email, isValidEmail(email), maskEmail(email));
}
}
}

실행 결과:

=== 이메일 유효성 검사 ===
user@example.com → 유효: true, 마스킹: us**@example.com
hong.gildong@company.co.kr → 유효: true, 마스킹: ho**********@company.co.kr
invalid-email → 유효: false, 마스킹: 유효하지 않은 이메일
no-at-sign.com → 유효: false, 마스킹: 유효하지 않은 이메일
@domain.com → 유효: false, 마스킹: 유효하지 않은 이메일
→ 유효: false, 마스킹: 유효하지 않은 이메일

7. 실전 예제 2: 문자열 통계 분석

public class StringAnalyzer {

public static void analyze(String text) {
if (text == null || text.isEmpty()) {
System.out.println("분석할 텍스트가 없습니다.");
return;
}

System.out.println("=== 문자열 통계 분석 ===");
System.out.println("원문: " + text);
System.out.println("전체 길이: " + text.length());
System.out.println("공백 제거 길이: " + text.replace(" ", "").length());

// 단어 수 계산
String[] words = text.trim().split("\\s+");
System.out.println("단어 수: " + words.length);

// 대문자/소문자/숫자/공백 개수
int upper = 0, lower = 0, digit = 0, space = 0;
for (char c : text.toCharArray()) {
if (Character.isUpperCase(c)) upper++;
else if (Character.isLowerCase(c)) lower++;
else if (Character.isDigit(c)) digit++;
else if (c == ' ') space++;
}
System.out.printf("대문자: %d, 소문자: %d, 숫자: %d, 공백: %d%n",
upper, lower, digit, space);

// 가장 긴 단어
String longest = "";
for (String word : words) {
if (word.length() > longest.length()) longest = word;
}
System.out.println("가장 긴 단어: " + longest + " (" + longest.length() + "자)");

// 팰린드롬(앞뒤가 같은 단어) 확인
String reversed = new StringBuilder(text).reverse().toString();
System.out.println("뒤집기: " + reversed);
System.out.println("팰린드롬: " + text.equalsIgnoreCase(reversed));
}

public static void main(String[] args) {
analyze("Hello Java Programming 2026");
System.out.println();
analyze("racecar");
}
}

실행 결과:

=== 문자열 통계 분석 ===
원문: Hello Java Programming 2026
전체 길이: 27
공백 제거 길이: 24
단어 수: 4
대문자: 3, 소문자: 18, 숫자: 4, 공백: 3
가장 긴 단어: Programming (11자)
뒤집기: 6202 gnimmargorP avaJ olleH
팰린드롬: false

=== 문자열 통계 분석 ===
원문: racecar
전체 길이: 7
공백 제거 길이: 7
단어 수: 1
대문자: 0, 소문자: 7, 숫자: 0, 공백: 0
가장 긴 단어: racecar (7자)
뒤집기: racecar
팰린드롬: true
고수 팁: 성능

반복문 안에서 String 연결(+)을 반복하면 매번 새 객체가 생성되어 성능이 저하됩니다. 루프 안에서 문자열을 조합할 때는 반드시 StringBuilder 를 사용하세요. 단순 상수 문자열 연결은 컴파일러가 자동으로 최적화해 줍니다.

다음 장에서는 기본형을 객체로 감싸는 래퍼 클래스(Wrapper Class) 에 대해 배워보겠습니다.