Background
백준에서 1001번 문제를 풀면서 문자열을 나누기 위해 StringTokenizer 클래스를 사용했는데 StringTokenizer에 대해 생소하다보니 StringTokenizer의 특징과 Split 메서드와의 차이점에 대해 찾아봤다.
백준 1001번 문제풀이
2023.10.24 - [Baekjoon(JAVA)/Algorithm] - <Baekjoon(JAVA) - Algorithm> 1001번: A-B
StringTokenizer 클래스
문자열을 사용자가 지정한 구분자(delim)로 나눠주는 클래스이다. (나눠진 문자열은 token이라 한다.)
StringTokenizer 특징
- 긴 문자열을 지정된 구분자(delimiter)를 기준으로 토큰(token)이라는 여러 개의 문자열로 자르는데 사용된다.
예) "100, 200, 300, 400" 이라는 문자열이 있을 때 ','를 구분자로 자르면 "100", "200", "300", "400" 이라는 4개의 문자열(token)을 얻을 수 있다.
- StringTokenizer의 구분자로 단 하나의 문자 밖에 사용하지 못하기 때문에 보다 복잡한 형태의 구분자로 문자열을 나눠야 할 때는 어쩔 수 없이 정규식을 사용하는 메서드를 사용해야 한다.
StringTokenizer의 생성자와 메서드
생성자
생성자 | 설명 |
StringTokenizer(Stirng str) | 문자열(str)을 기본 구분자(띄어쓰기)를 기준으로 나누는 StringTokenizer를 생성한다. |
StringTokenizer(String str, String delim) | 문자열(str)을 지정된 구분자(delim)로 나누는 StringTokenizer를 생성한다. (구분자는 token으로 간주되지 않는다.) |
StringTokenizer(Stirng str, String delim, boolean returnDelims) | 문자열(str)을 지정된 구분자(delim)로 나누는 StringTokenizer를 생성한다. returnDelims의 값을 true로 하면 구분자도 token으로 간주된다. |
메서드
리턴값 | 메서드 | 설명 |
String | nextToken() | 객체에서 다음 token을 반환한다. |
String | nextToken(String delim) | delim 기준으로 다음 token을지 반환한다. |
int | countTokens() | 전체 token의 수를 반환한다. |
boolean | hasMoreTokens() | token이 남아있는지 알려준다. |
boolean | hasMoreElements() | hasMoreToken()과 동일한데 element보다 token으로 된 메서드를 주로 사용한다. |
Object | nextElement() | nextToken 메서드와 동일하지만 문자열이 아닌 객체를 리턴 |
참고 1
구분자를 지정하지 않으면 default로 '스페이스( ), 탭(\t), 줄바꿈(\n), 캐리지 리턴(\r) 등 기본 구분자가 적용된다.
public StringTokenizer(String str) {
this(str, " \t\n\r\f", false);
}
참고 2
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) {
String ex = "x=100*(200*300)/2";
StringTokenzier st = new StringTokenizer(ex, "+-*/=()", true);
while(st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
}
}
// 출력 결과
x
=
100
*
(
200
+
300
)
/
2
StringTokenizer는 단 한 문자의 구분자만 사용할 수 있기 때문에 “+-*/=()” 전체가 하나의 구분자가 아니라, 각각의 문자가 모두 구분자가 된다. (한 문자씩 잘라져서 +, -, *, /, =, (, )가 각각 구분자가 된다.)
만일 구분자가 "()" 처럼 두 문자 이상이라면 Scanner나 String 클래스의 split을 사용해야 한다.
StringTokenizer vs Split (특징)
StringTokenizer | Split() |
java.util에 포함된 클래스이다. | String 클래스에 속해있는 메서드이다. |
문자로 문자열을 구분한다. | 정규표현식으로 구분한다. |
한 문자의 구분자만 사용 가능하다. | 정규표현식을 이용하면 두 문자 이상의 구분자도 사용 가능하다. |
결과값이 문자열 String이다. | 결과값이 문자열 배열 String[]이다. |
빈 문자열을 token으로 인식하지 않는다. | 빈 문자열을 token으로 인식한다. |
데이터 양이 적을 때 배열에 담아 반환하는 split은 데이터를 바로 잘라서 반환해주는 StringTokenizer보다 느릴 수 있다.
StringTokenizer는 legacy class
공식 홈페이지에 아래와 같이 나와있다.
StringTokenizer는 호환성을 위해 유지되는 legacy class이지만 새 코드에서는 사용하지 않는 것이 좋다. 이 기능을 원하는 사람은 String 또는 java.util.regex 패키지의 split 메서드를 사용하는 것이 좋다.
즉, StringTokenizer는 legacy class라며, split 메서드로 대체할 것을 권장하고 있다.
하지만 가끔 StringTokenizer가 split 메서드보다 더 빠른 속도를 보이기 때문에 StringTokenizer를 쓰는 풀이가 보이는데 어떤 경우에 쓰여야 하는지 속도 비교한 사례를 찾아봤다.
StringTokenizer vs Split (속도)
StringTokenizer 내부 메서드들의 효율이 매우 나쁜 반면에, split은 제법 일정한 성능을 보인다고 한다.
아래는 StringTokenizer가 split보다 더 나은 성능을 보인 조건이다.
- 구분자가 1개이다.
- 구분자가 유니코드가 아니다.
- 분리된 첫 번째 token 값만 필요했기에 hasMoreToken을 사용하지 않고, nextToken을 1회 호출했다.
StringTokenizer 내부 메서드 효율이 왜 나쁘고, 위와 같은 조건에서는 왜 성능이 좋은지는 아래와 같다.
- 구분자 중에 유니코드가 있을 경우, 수행시간이 기하급수적으로 늘어난다.
- 구분자 중에 유니코드가 있는 경우, StringTokenizer는 내부적으로 delimiterCodePoints라는 int[] 배열을 만들어 그 안에 유니코드 구분자를 담는다.
- 문제는 'StringTokenizer가 구분자와 문자열을 하나씩 비교하는 식으로 token을 끊어낸다는 것'과 '유니코드가 아스키 코드로 표현되는 문자보다 훨씬 많다는 것'이다.
- 그렇기 때문에 유니코드 구분자가 하나라도 포함될 경우, 저 배열 전체를 순회하면 작업을 수행하게 되고, 구분자가 길어질수록 수행시간은 기하급수적으로 늘어날 것이다.
- 구분자를 비워두면 기본값으로 '스페이스'가 구분자가 된다고 하지만, 실제 내부적으로는 스페이스( ), 탭(\t), 줄바꿈(\n), 캐리지 리턴(\r), \f 이렇게 총 5개의 구분자가 기본으로 적용된다.
즉, 구분자의 길이가 문자 5개인 것으로, 만약 이 5개가 아닌 문자인 경우 구분자인지를 검사할 때 6번 가량 순회해야 한다.
- 따라서 구분자 길이를 m, 타겟 문자열을 n이라고 할 때, 시간 복잡도는 O(nm)이다. 그렇기 때문에 구분자의 길이가 길어질수록 상당히 느려질 가능성이 있다.
결론
StringTokenizer를 쓰기 적합한 경우
- 구분자에 유니코드 문자가 없고, 구분자의 길이가 길지 않을 때 (StringTokenizer가 속도 측면에서 더 빠를 가능성이 높다.)
- 구분자가 복잡하지 않은 '한 문자'일 때 (두 문자 이상이 아니라 정규표현식이 필요하지 않을 때)
- 반환 타입이 배열이 아니라 문자열인 경우가 적합할 때
알고리즘 문제를 풀 경우 split 보단 StringTokenizer를 사용하는게 괜찮다고 생각한다.
문자열을 나눌 때, 이 문자열들을 보관할 필요없이 일시적으로 쓰는 경우라면 split() 메서드는 무조건 배열로 반환하기 때문에 배열에 한 번 담았다가 꺼내는 불필요한 코드(작업)을 추가적으로 거쳐야 해서 부적합하다.
그러나 프로젝트에서는 StringTokenizer가 legacy 코드로 분류되기 때문에 jdk 버전에 따라 Stringtokenizer가 사라질 수도 있으니 split을 사용해야할 것 같다.
Reference