[Java] StringTokenizer와 Split() 메서드 언제 써야할까?

effiRin·2022년 7월 21일
11

Java

목록 보기
2/2
post-thumbnail

우선, 비교적 생소한 StringTokenizer에 대해 알아보자.

java.util.StringTokenizer 클래스란?


  • 긴 문자열을 지정된 구분자(delimiter)를 기준으로 토큰(token)이라는 여러 개의 문자열로 잘라내는 데 사용된다.
  • 예) “100,200,300,400”이란 문자열이 있을 때 ‘,’를 구분자로 잘라내면 “100”, “200”, “300”, “400”이라는 4개의 문자열(토큰)을 얻을 수 있다.

  • StringTokenizer를 이용하는 방법 이외에도 아래와 같이 String의 split(String regex)이나 Scanner의 useDelimiter(String pattern)를 사용할 수 있지만…
    String[] result = "100,200,300,400".split(",");
    Scanner sc = new Scanner("100,200,300,400").useDelimiter(",");
    이 두 방법은 ‘정규식 표현’을 사용해야하므로 정규식 표현에 익숙하지 않은 경우 StringTokenizer를 사용하는 것이 간단하면서 명확한 결과를 얻을 수 있다.
  • 그러나 StringTokenizer의 구분자로 단 하나의 문자 밖에 사용하지 못하기 때문에, 보다 복잡한 형태의 구분자로 문자열을 나누어야 할 때는 어쩔 수 없이 정규식을 사용하는 메서드를 사용해야 한다.



StringTokenizer의 생성자와 메서드


  • 생성자

StringTokenizer를 생성하는 방식 3가지

생성자설명
StringTokenizer(String str)문자열(str)을 기본 구분자(띄어쓰기)를 기준으로 나누는 StringTokenizer를 생성한다.
StringTokenizer(String str, String delim)문자열(str)을 지정된 구분자(delim)로 나누는 StringTokenizer를 생성한다.
(구분자는 토큰으로 간주되지 않음)
StringTokenizer(String str, String delim, boolean returnDelims)문자열(str)을 지정된 구분자(delim)로 나누는 StringTokenizer를 생성한다.
returnDelims의 값을 true로 하면 구분자도 토큰으로 간주된다.
  • 메서드
리턴값메서드설명
StringnextToken()객체에서 다음 토큰을 반환
StringnextToken(String delim)delim 기준으로 다음 토큰을 반환한다.
intcountTokens()전체 토큰의 수를 반환한다.
booleanhasMoreTokens()토큰이 남아있는지 알려준다.
booleanhasMoreElements()hasMoreToken()과 동일한데 엘리먼트보다 토큰으로 된 메서드를 주로 사용
ObjectnextElement()nextToken 메서드와 동일하지만 문자열이 아닌 객체를 리턴
  • 참고 1
    구분자를 지정하지 않으면 디폴트로 ‘스페이스( ), 탭(\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";
            StringTokenizer st = new StringTokenizer(ex, "+-*/=()", true);
    
            while(st.hasMoreTokens()){
                System.out.println(st.nextToken());
            }
        }
    }
    // 출력 결과
    x
    =
    100
    *
    (
    200
    +
    300
    )
    /
    2
  • StringTokenizer는 단 한 문자의 구분자만 사용할 수 있기 때문에, “+-*/=()” 전체가 하나의 구분자가 아니라 각각의 문자가 모두 구분자가 된다.
    (
    한 문자씩 잘라져서 + - * / = ( )가 각각 구분자가 된다.)
  • 만일 구분자가 두 문자 이상이라면, Scanner나 String 클래스의 split을 사용해야 한다.
    (예 : () → 이렇게 문자 두 개 이상이 붙어있는 것을 찾고 싶다면)



StringTokenizer와 Split의 차이


StringTokenizerSplit()
java.util에 포함된 클래스다.String 클래스에 속해있는 메소드
문자로 문자열을 구분정규표현식으로 구분
오직 단 한 문자의 구분자만 사용 가능정규표현식을 이용하면 두 문자 이상의 구분자도 사용 가능
결과값이 문자열 String결과값이 문자열 배열 String[]
빈 문자열을 토큰으로 인식하지 않음빈 문자열을 토큰으로 인식함

→ 데이터 양이 적을 때 배열에 담아 반환하는 split는 데이터를 바로 잘라서 반환해주는 StringTokenizer보다 느릴 수 있다.




[심층 분석]

StringTokenizer는 legacy class이다?

StringTokenizer is a legacy class that is retained for compatibility reasons although its use is discouraged in new code. It is recommended that anyone seeking this functionality use the split method of String or the java.util.regex package instead.


StringTokenizer는 호환성을 위해 유지되는 레거시 클래스(legacy class)이지만 새 코드에서는 사용하지 않는 것이 좋습니다. 이 기능을 원하는 사람은 대신 String 또는 java.util.regex 패키지의 split 메서드를 사용하는 것이 좋습니다.


출처 :
StringTokenizer (Java Platform SE 7 )

놀랍게도 공식홈페이지에서는 StringTokenizer는 레거시 클래스라며, split 메서드로 대체할 것을 권장하고 있다. 하지만 알고리즘을 풀다보면 때때로 StringTokenizer가 split 메소드보다 더 빠른 속도를 보이기 때문에 StringTokenizer를 쓰는 풀이가 심심치 않게 보인다.

그렇다면 정말 StringTokenizer는 아예 쓰지 말아야 할까?




StringTokenizer와 Split 속도 비교


궁금증이 계속해서 커져갈 때,
고맙게도 StringTokenizer와 Split에 대해 심층적으로 분석한 블로그 글들을 찾았다.


💡 [StringTokenizer VS String.split] 누가 더 빠른가

위 블로그에선 StringTokenizer와 split()의 성능 차이를 비교하는 테스트를 진행했고, 이 둘에 대해 다음과 같이 서술했다.

StringTokenizer의 내부의 메서드들의 효율이 매우 나쁘다. 그에 반해서, split은 제법 일정한 성능을 보이고 있다.

StringTokenizer가 더 나은 성능을 보였던 것은 아래의 조건인 경우였다.
1. 구분자가 1개였다.
2. 구분자가 유니코드가 아니었다.
3. 분리된 첫번째 토큰 값만 필요했기에, hasMoreToken을 사용하지 않고, nextToken을 1회 호출 하였다.

StringTokenizer의 내부 메소드 효율은 왜 나쁘고, 위와 같은 조건에선 왜 StringTokenizer가 우세한 성능을 보였을까?
이는 다음 블로그를 참고하면 이해할 수 있다.


💡 java StringTokenizer : 생각보다 느릴까? 뜯어보자.


➡️ 위 블로그에서 일부 요약정리

  • 구분자 중에 유니코드가 있을 경우, 수행시간이 어마어마하게 늘어난다.

    • 구분자 중에 유니코드가 있는 경우, StringTokenizer는 내부적으로 delimiterCodePoints라는 int[] 배열을 만들어 그 안에 유니코드 구분자를 담는다.
    • 문제는 ‘StringTokenizer가 구분자와 문자열을 하나씩 비교하는 식으로 token을 끊어낸다는 것’과 ‘유니코드가 아스키 코드로 표현되는 문자보다 훨씬 많다는 것’이다.
    • 그렇기 때문에 유니코드 구분자가 하나라도 포함될 경우, 저 배열 전체를 순회하며 작업을 수행하게 되고, 구분자가 길어질수록 수행시간은 기하급수적으로 늘어날 것이다.
  • 구분자를 비워두면 기본값으로 ‘스페이스’가 구분자가 된다고 하지만, 실제 내부적으로는 스페이스( ), 탭(\t), 줄바꿈(\n), 캐리지 리턴(\r), \f 이렇게 총 5개의 구분자가 기본으로 적용된다.

    public StringTokenizer(String str) {
            this(str, " \t\n\r\f", false);
        }

    즉 구분자의 길이가 문자 5개인 것으로, 만약 이 5개가 아닌 문자인 경우 구분자인지를 검사할 때 6번 가량 순회해야 하는 것.

  • 따라서 구분자 길이를 m, 타켓 문자열을 n이라고 할 때, 시간 복잡도는 O(nm)이다. 이때 구분자의 길이가 길어질수록 상당히 느려질 가능성이 있다는 것



결론

다음과 같은 상황인 경우엔 StringTokenizer를 쓰는 것이 적합할 것 같다.

1. 구분자에 유니코드 문자가 없고, 구분자의 길이가 길지 않을 때
(StringTokenizer가 속도 측면에서 더 빠를 가능성이 높다)

  1. 구분자가 복잡하지 않은 '한 문자'일 때
    (두 문자 이상이 아니라 정규표현식이 필요하지 않을 때)

  2. 반환 타입이 배열이 아니라 문자열인 경우가 적합할 때

➡️ 사실 3번은 개인적으로 알고리즘 문제를 풀 때 StringTokenizer를 써야겠다고 생각한 상황이다.

문자열을 쪼갤 때, 이 문자열들을 보관할 필요없이 일시적으로 쓰는 경우라면 split() 메서드는 부적합하다. split()은 무조건 배열로 반환하기 때문에, 배열에 한번 담았다가 꺼내는 불필요한 코드(작업)을 추가적으로 거쳐야 하기 때문.

+) 자세한 것 다음 주소의 알고리즘 오답풀이를 참고
[TIL_Algorithm] 구간 합 구하기 공식, StringTokenizer

➡️ 그러나 프로젝트에서 StringTokenizer를 적용하는 경우라면 좀더 신중히 생각해야 할 것 같다.

공식적으로 legacy 코드로 분류되었기 때문에 이후 jdk 버전을 올리면서 StringTokenizer가 사라지면, 이와 관련된 코드를 모두 수정해야하는 일이 발생하므로.



  • [출처]

[Java] StringTokenizer 문자열 분리하기 (split과 차이는 뭘까?)
[Java] StringTokenizer 기본 및 사용법
[JAVA 자바] StringTokenizer 클래스로 문자열 분리하기! split 비교.
[StringTokenizer VS String.split] 누가 더 빠른가
java StringTokenizer : 생각보다 느릴까? 뜯어보자.
남궁성, <자바의 정석>, pp.513-517

profile
모종삽에서 포크레인까지

0개의 댓글