Java/JAVA에 대하여

startsWith, endsWith 무조건 좋은가

lms0806 2024. 8. 17. 20:46
728x90
반응형

java에는 A라는 문자열의 맨앞에 B라는 문자열이 prefix(접두사)로 있는지 체크하는 함수인 startsWith가 있습니다.

또한 A라는 문자열의 맨 뒤에 B라는 문자열이 suffix(접미사)로 있는지 체크하는 함수인 endsWith가 있습니다.

여기서 하나의 궁금증이 생깁니다.

"문자열 : 문자열" 비교가 아닌 "문자열 : 문자" 비교인 경우에 startsWith나 endsWith보다 charAt()가 더 좋지 않을까?

라는 의문을 가지고 테스트에 들어갔습니다.

테스트 소스

public class Main {
    static String s = "";
    static int size = 1000000000;
    public static void main(String[] args) {
        System.out.println("JAVA Version : " + System.getProperty("java.version"));
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < 10000; i++) {
            sb.append(1);
        }
        s = sb.toString();

        startWith();
        startChar();
        endWith();
        endChar();
    }

    public static void startWith() {
        long before = System.currentTimeMillis();
        for(int i = 0; i < size; i++) {
            if(s.startsWith("1"));
        }
        long after = System.currentTimeMillis();
        System.out.println("start With : " + ((after - before) / 1000.0));
    }

    public static void endWith() {
        long before = System.currentTimeMillis();
        for(int i = 0; i < size; i++) {
            if(s.endsWith("1"));
        }
        long after = System.currentTimeMillis();
        System.out.println("end With : " + ((after - before) / 1000.0));
    }

    public static void startChar() {
        long before = System.currentTimeMillis();
        for(int i = 0; i < size; i++) {
            if(s.charAt(0) == '1');
        }
        long after = System.currentTimeMillis();
        System.out.println("start char : " + ((after - before) / 1000.0));
    }

    public static void endChar() {
        long before = System.currentTimeMillis();
        for(int i = 0; i < size; i++) {
            if(s.charAt(s.length() - 1) == '1');
        }
        long after = System.currentTimeMillis();
        System.out.println("end char : " + ((after - before) / 1000.0));
    }
}

결과

start With : 0.004
start char : 0.841
end With : 0.004
end char : 0.884

charAt 코드가 엄청 느리다는 것을 확인할 수 있었습니다.

'근데 저정도로 심하게 차이가 나는 이유는 머지?'라는 의구심이 들어 다른 환경에서도 테스트를 해보았습니다.

tio.run에서 테스트해본 결과

start With : 0.017
start char : 0.013
end With : 0.009
end char : 0.012

차이가 미비했습니다.

둘의 차이가 무엇일까? 라고 찾아보던 와중 제 로컬하고 tio.run의 jdk버전 차이가 발생했었습니다,.

System.out.println("JAVA Version : " + System.getProperty("java.version"));

다음과 같은 코드로 jdk버전을 확인해본 결과

  • tio.run : jdk12
  • 로컬 : jdk16

이였습니다.

jdk12 기준 String.java의 startsWith

public boolean startsWith(String prefix, int toffset) {
        // Note: toffset might be near -1>>>1.
        if (toffset < 0 || toffset > length() - prefix.length()) {
            return false;
        }
        byte ta[] = value;
        byte pa[] = prefix.value;
        int po = 0;
        int pc = pa.length;
        if (coder() == prefix.coder()) {
            int to = isLatin1() ? toffset : toffset << 1;
            while (po < pc) {
                if (ta[to++] != pa[po++]) {
                    return false;
                }
            }
        } else {
            if (isLatin1()) {  // && pcoder == UTF16
                return false;
            }
            // coder == UTF16 && pcoder == LATIN1)
            while (po < pc) {
                if (StringUTF16.getChar(ta, toffset++) != (pa[po++] & 0xff)) {
                    return false;
               }
            }
        }
        return true;
    }

jdk16 기준 String.java의 startsWith

public boolean startsWith(String prefix, int toffset) {
        // Note: toffset might be near -1>>>1.
        if (toffset < 0 || toffset > length() - prefix.length()) {
            return false;
        }
        byte ta[] = value;
        byte pa[] = prefix.value;
        int po = 0;
        int pc = pa.length;
        byte coder = coder();
        if (coder == prefix.coder()) {
            int to = (coder == LATIN1) ? toffset : toffset << 1;
            while (po < pc) {
                if (ta[to++] != pa[po++]) {
                    return false;
                }
            }
        } else {
            if (coder == LATIN1) {  // && pcoder == UTF16
                return false;
            }
            // coder == UTF16 && pcoder == LATIN1)
            while (po < pc) {
                if (StringUTF16.getChar(ta, toffset++) != (pa[po++] & 0xff)) {
                    return false;
               }
            }
        }
        return true;
    }
        byte coder = coder();
        if (coder == prefix.coder()) {
            int to = (coder == LATIN1) ? toffset : toffset << 1;

해당 부분에 차이점이 있다는 것을 발견하였습니다.

이 코드로 인하여 시간차이가 발생하지 않을까? 라는 추측을 하였습니다.

추가로 jdk8버전에서도 테스트해본 결과

start With : 0.004
start char : 0.005
end With : 0.004
end char : 0.007

다음과 같았습니다.

jdk8의 startsWith 코드는

public boolean startsWith(String prefix, int toffset) {
        char ta[] = value;
        int to = toffset;
        char pa[] = prefix.value;
        int po = 0;
        int pc = prefix.value.length;
        // Note: toffset might be near -1>>>1.
        if ((toffset < 0) || (toffset > value.length - pc)) {
            return false;
        }
        while (--pc >= 0) {
            if (ta[to++] != pa[po++]) {
                return false;
            }
        }
        return true;
    }

다음과 같이 매우 간단하게 동작하였습니다.

추가로 최근에 나온 jdk21의 startsWith 코드는

public boolean startsWith(String prefix, int toffset) {
        // Note: toffset might be near -1>>>1.
        if (toffset < 0 || toffset > length() - prefix.length()) {
            return false;
        }
        byte[] ta = value;
        byte[] pa = prefix.value;
        int po = 0;
        int pc = pa.length;
        byte coder = coder();
        if (coder == prefix.coder()) {
            if (coder == UTF16) {
                toffset <<= UTF16;
            }
            return ArraysSupport.mismatch(ta, toffset,
                    pa, 0, pc) < 0;
        } else {
            if (coder == LATIN1) {  // && pcoder == UTF16
                return false;
            }
            // coder == UTF16 && pcoder == LATIN1)
            while (po < pc) {
                if (StringUTF16.getChar(ta, toffset++) != (pa[po++] & 0xff)) {
                    return false;
               }
            }
        }
        return true;
    }

다음과 같이 변경되었습니다.

jdk21에서의 테스트

start With : 0.026
start char : 0.006
end With : 0.023
end char : 0.008

결론

  • startsWith와 endsWith vs charAt의 시간 차이는 버전별로 발생하는 것으로 추측된다.
  • jdk12와 jdk16의 차이점인 처음에 체크하는 if문에서 시간이 오래걸리는 것으로 확인된다.
728x90
반응형