
문자열을 다루다 보면 자연스럽게 이런 코드를 시도하게 됩니다.
String s = "hello";
for (char c : s) { // 어쩐지 될 것 같은데...
System.out.println(c);
}
하지만 이 코드는 컴파일조차 되지 않습니다.
error: incompatible types: String cannot be converted to Iterable<...>
배열이나 List는 for-each(for (타입 변수 : 컬렉션))로 순회가 되는데, 왜 String은 안 되는 걸까요?
자바의 for-each문은 사실 마법이 아닙니다. Iterable 인터페이스를 구현한 객체에 대해서만 동작하는 문법입니다.
List<Integer> list = List.of(1, 2, 3);
for (int x : list) { ... } // List는 Iterable을 구현하고 있어서 가능
int[], char[] 같은 배열도 (정확히는 Iterable을 구현한 게 아니라 언어 차원에서 별도로 지원하지만) for-each가 가능합니다. 그런데 String 클래스는 Iterable을 구현하고 있지 않습니다. 그래서 String 자체를 for-each 대상으로 넣을 수가 없는 거예요.
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// Iterable이 안 보임
}
실제 String 클래스 선언을 봐도 Iterable은 구현 목록에 없습니다. 그래서 우리는 String을 "순회 가능한 형태"로 먼저 바꿔줘야 합니다.
toCharArray()는 문자열을 char[] 배열로 변환해주는 메서드입니다.
String s = "hello";
char[] arr = s.toCharArray(); // ['h', 'e', 'l', 'l', 'o']
for (char c : arr) {
System.out.println(c);
}
배열은 for-each가 가능하니, 이렇게 변환만 해주면 깔끔하게 순회할 수 있습니다. 실무에서는 변수에 따로 담지 않고 한 줄로 줄여서 많이 씁니다.
for (char c : s.toCharArray()) {
System.out.println(c);
}
장점: 코드가 간결하고 직관적입니다. "이 문자열의 글자 하나하나에 대해 뭔가 처리하고 싶다"는 의도가 바로 드러납니다.
단점: 매번 호출할 때마다 새로운 배열을 만드는 비용이 발생합니다. 문자열이 아주 길고 반복 호출이 많은 상황이라면 약간의 성능 차이가 생길 수 있습니다. (코딩테스트 수준에서는 거의 신경 쓸 필요 없습니다.)
인덱스(위치 정보)가 같이 필요한 경우에는 일반적인 for문과 charAt()을 조합합니다.
String s = "hello";
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
System.out.println(i + "번째 글자: " + c);
}
charAt(i)는 i번째 위치의 문자 하나를 꺼내오는 메서드입니다. for-each와 달리 인덱스를 직접 다루기 때문에, "앞뒤 글자와 비교해야 한다", "특정 위치부터 거꾸로 봐야 한다" 같은 요구사항에 유리합니다.
// 예: 바로 다음 글자와 비교해야 하는 경우
for (int i = 0; i < s.length() - 1; i++) {
if (s.charAt(i) == s.charAt(i + 1)) {
System.out.println("연속된 같은 글자 발견: " + s.charAt(i));
}
}
이런 코드는 toCharArray() + for-each로는 쓰기 까다롭습니다. 인덱스가 없으니까요.
자바 8 이후로는 스트림을 활용한 방식도 있습니다.
String s = "hello";
s.chars().forEach(c -> System.out.println((char) c));
chars()는 IntStream을 반환하기 때문에, 각 요소가 char가 아니라 int(유니코드 코드값)로 다뤄집니다. 그래서 출력할 때 (char)로 캐스팅이 필요합니다. 스트림 문법에 익숙하다면 필터링이나 변환 작업을 체이닝하기에 편리하지만, 코딩테스트에서는 자주 쓰이는 방식은 아닙니다.
| 방식 | 인덱스 필요? | 코드 스타일 | 추천 상황 |
|---|---|---|---|
toCharArray() + for-each | ❌ 불필요 | 가장 간결 | 그냥 글자 하나씩 순서대로 처리할 때 |
charAt(i) + for문 | ✅ 필요 | 약간 더 길어짐 | 인덱스, 이전/다음 글자 비교가 필요할 때 |
chars() 스트림 | ❌ 불필요 | 함수형 스타일 | 스트림 체이닝이 필요한 경우 |
스택을 활용한 괄호 검사 문제로 toCharArray() 방식을 적용해보면 다음과 같습니다.
boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(c);
} else if (c == ')') {
if (stack.isEmpty()) {
return false; // 짝이 안 맞음
}
stack.pop();
}
}
return stack.isEmpty(); // 다 짝지어졌으면 true
}
이 문제는 인덱스가 필요 없고 "글자 하나하나를 순서대로 보면서 여는 괄호면 쌓고, 닫는 괄호면 빼는" 단순한 흐름이기 때문에 toCharArray() + for-each가 가장 깔끔합니다.
반면 "i번째 글자와 i+2번째 글자를 비교해야 한다" 같은 요구사항이 생기면 인덱스 기반 for문으로 바꿔야 합니다.
String은 Iterable을 구현하지 않아서 for (char c : s)처럼 직접 순회할 수 없습니다.s.toCharArray()로 배열로 바꾼 뒤 for-each를 쓰는 것입니다.charAt(i)와 일반 for문을 씁니다.문자열 순회는 코딩테스트에서 정말 자주 등장하는 패턴이라, toCharArray()와 charAt() 두 가지만 손에 익혀두면 대부분의 문제에서 막힘없이 풀어나갈 수 있습니다.