앞에서 간단히 C++ String과 Java String을 알아봤다.

 

이제 각설하고, 두 String의 구현법 및 내부 구현 함수들을 보며 뭐가 다른지 파악해보자.

(Java에서는 메소드로 읽는게 정석이나, 편의를 위해 전부 함수로 칭하도록 하겠다)

 

(빠지거나 잘못된것이 있다면 덧글로 지적바랍니다.)

(C++의 경우 : C++17까지 / Java의 경우 : Java 8+ 까지 를 기준으로 작성하였습니다.)


1. 절대값

일단 이 방식이 매우 안좋은 방식인건 알지만, 단순히 각 String의 함수 개수를 세어보기로 했다.

1) C++

https://en.cppreference.com/w/cpp/string/basic_string

기준, Operator 연산을 제외하고 31개 (+a)의 함수가 존재한다.

 

2) Java

https://docs.oracle.com/javase/8/docs/api/

기준, 중복을 제외하고 40개 (+a) 메소드가 존재한다.

 

2. 문자열 접근

특정 위치의 문자열에 접근할 수 있는 함수들이다.

1) C++

(1) front / back : 각 String의 맨 앞 / 맨 뒤 값을 리턴해준다.

(2) begin / end : Iterator로, 각각 String의 맨 앞 / 맨 뒤 '위치'에 접근가능하게 해준다. 

(3) data : 맨 앞의 '포인터 값'을 리턴해준다.

(4) at / '[ ]' : 특정 위치의 값을 리턴해준다.

 

2) Java

(1) charAt : 특정 위치의 값을 리턴해준다.

의외로 이거 하나밖에 안보이는것같지만....

 

3. 문자열 삽입

문자열에 값을 삽입하는 함수들이다. 생성 및 수정 제외.

 

1) C++

(1) push_back / pop_back : 문자열의 맨 뒤/맨 앞에 문자를 추가한다.

push_back와 append, '+=' 는 모두 같은 결과를 보여준다.

 

2) Java

(1) concat(string) : 호출한 String의 뒤에 concat에 인자로 삽입된 문자열을 덧붙인다.

 

 

4. 문자열 관리

문자열의 특정 값을 수정하거나, Case를 변경해주는 등의 함수들이다.

1) C++

(1) replace() : 특정 위치의 값을 변경해준다. (C++11부터 가능)

(2) swap(string) : 두 String의 값을 변환해준다. 

 

2) Java

(1) replace(A, B) : A 문자열을 B 문자열로 일괄 변환한다.

(2) toLower/UpperCase() : 각각 전체 문자열을 소문자/대문자로 변환한다.

(3) trim() : 문자열 앞 뒤의 '공백'을 제거한다.

(4) valueOf(Type) : 인자로 주어진 값을 문자열로 변환한다. 모든 기본형(PrimitiveType)에 대응.

 

 

5. 문자열 추출

특정 문자열을 자르는 함수들이다.

1) C++

(1) copy(char* arr, size_t length, size_t index) : arr에 index부터 length까지의 문자열을 복사해준다.

(2) substr(size_t index, size_t length(기본값 : Underflow -> INT_MAX)) : 문자열을 index부터 length만큼 리턴한다.

(3) find_last_of() : 인자로 받은 문자열이 '마지막으로 나타난 위치' 이후를 리턴해준다.

 

2) Java

(1) split(string) : 인자로 들어온 정규 표현식에 따라 문자열을 나누어 리턴

(2) substring(int(, int)) : 인자로 들어온 인덱스부터 새로운 문자열로 리턴

 

 

6. 문자열 비교

문자열을 찾거나, 비교에 사용되는 함수들이다

1) C++

(1) find(string) : 인자로 받은 문자열이 '어디에 있는지'를 찾아준다.

 

2) Java

+ 가 붙은 함수는 'Case'를 구분하지 않는 함수가 존재하는 경우이다.

(1) indexOF(string) : 인자로 받은 문자열이 '어디에 있는지'를 찾아준다.

(2) contains(char) : 인자로 받은 문자열이 존재하는지 아닌지를 찾아준다.

(3) compareTo(string)+ : 인자로 받은 문자열과의 사전식 비교를 실시한다.

(4) equals(string)+ : 인자로 받은 문자열과 동일한 문자열인지 비교한다.

(5) LastIndexOf(string) : 인자로 받은 문자열이 '마지막으로 나타난 위치'를 찾아준다.

(6) matches(string) : 정규 표현식인지 아닌지 체크.

 

 

7. 기타 기본사항

1) C++

(1) C++ string은 char* 문자열 배열로 이루어져 있어, 각 항목이 1바이트로 처리된다.

ASCII가 아닌 타 문자 (CP949 등)에 대해서는, 내부적으로 항목 2개를 1개로 치환해서 비교하게 되어, 단순 순차접근을 실시하게 될 시 "깨진 값"을 얻게 된다.

 

2) Java

(1) Java에서는 char 형 또한 2바이트로 이루어져 있어, UTF-16 데이터를 한 묶음으로 처리가 가능하다.

C++ 에서처럼 순차접근 했다고 깨지는 불상사는 안 일어난다 (...)

 

 


뭔가 단순 비교를 했을때, Java의 경우 변환, 비교 함수 등이 많이 갖춰져 있었고, C++의 경우엔 의외로 삽입쪽이 Java보다 풍성하게 되었음을 알 수 있었다.

 

실제로는 7번 항목의 문자열 처리방식때문에 문자열을 많이 다룰시 C++보다는 Java를 선택하게 되고, 요즘은 단순 문자열 비교 등에 경우엔 Python 등 다른 생산성 높은 언어로 옮겨가는 추세로 보인다.

 

간단히 레퍼런스 자료만 보며 작성한것이므로, 다른 점이나 문제가 있다면 알려주시면 반영하겠습니다. 

C++ String에 대해서는 이전 글 참고

 

이번에는 Java다.

 

Java String은 익히 알려진것처럼 UniCode 사용을 예상하고 작업되어 각 문자 1개가 2바이트씩 차지하고, 문자열을 수정시에는 문자열 자체가 수정되는것이 아니라 수정된 문자열이 새로 생성되는 등의 특징이 있다.

하지만 '체감상' Java String이 C++ String보다 다루기 편했던것으로 기억하는데, 오랫만에 한번 뜯어보도록 하자.

 

이번 레퍼런스 자료는 Open JDK의 소스코드입니다.

검색하면 금방 찾으실 수 있으니 링크는 딱히 달지 않겠습니다.

 

2. Java String의 경우

 

    public String(char value[]) {
        this(value, 0, value.length, null);
    }

 

Java String 또한 Char형 배열로 이루어져 있으며 (* C++과는 다르게 포인터 개념을 사용할 수 없기에 일반 배열이다), Java에서는 Char형의 바이트 값이 2바이트 이므로, String의 내부 구현또한 2바이트 문자열을 저장하는것으로 이루어진다.

어찌보면 당연한 소리인데... 배열의 각 값이 2바이트면 전체가 2바이트 데이터의 배열 구조인것은 당연한 소리 아니겠는가..

 

여기서 바로 차이점이 나타난다.

Java String의 경우 처음 값이 정해져버리기 때문에, 사이즈 변환에 좀 심한 에로사항이 생기게 된다. C++의 경우에는 (아무튼) 배열 포인터로 구현이 되어있으니, 값을 늘리는것 만큼은 (메모리 침범이 일어나지 않는다는 전제하에) 쉽게 늘릴 수 있으나...

 

그리고 또한, Java String은 기본적으로 'UTF-16' 인코딩을 사용하고 있다.

 

참고중인 Open JDK의 String.java 파일에 보면...

 

A {@code String} represents a string in the UTF-16 format in which supplementary characters are represented by surrogate pairs. 

(see the section Character.html#unicode">Unicode haracter Representations in the {@code Character} class for
more information).

 

(Surrogate pairs 에 대해서는 나중에 따로 다루도록 하겠습니다. 저도 조금 이해할 필요가 있어보이네요)

 

아무튼 UTF-16 포멧으로 표현된다고 나타나있다.

UTF-8과 다른점은, 뭐 당연히 표현 비트 수가 8비트 / 16비트라는 차이점이라는것... 정도겠다.

 

 

아래부터는 String str = "Hello World";  라는 String을 선언해둔것으로 가정하고 시작한다.

* 또한, 기본적으로 세부 자료형은 StringUTF16임을 명시해둔다.

1) 접근법

(1) charAt(N);

C++ String의 at과 기능상 동일하다. N의 위치에 존재하는 값을 리턴해주는 메소드로, 내부적으로는 String Data Array(= byte 배열) 과 int N 값을 인자로 받고, 이를 getChar() 메소드에서 탐색하고 리턴해주는 방식이다.

 

그래서 getChar()는 어떻게 되어있느냐...

 

    static char getChar(byte[] val, int index) {
        assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
        index <<= 1;
        return (char)(((val[index++] & 0xff) << HI_BYTE_SHIFT) |
                      ((val[index]   & 0xff) << LO_BYTE_SHIFT));
    }

이런식으로, byte배열에서 index에 맞는 위치의 값을 bit shift를 통해 리턴해주는 모습을 보여준다.

Java에서는 Char형도 2바이트로 표현하니, 바이트 값이 2개가 올라가는것을 볼 수 있다.

 

 

(2)str.indexof(temp);

temp 라는 문자가 어디에 존재하는지 탐색해주는 메소드이다.

내부 구현은, indexof가 호출시, 호출된 String의 구조가 latin1 (잠깐 찾아보니 그냥 아스키코드값하고 동일하다.) 인가, 아닌가로 비교를 실시한다.

만일 latin1 형식이 아닌경우, StringUTF16라는 클래스의 indexof 메소드로 넘어가게되는데, 일반 구조의 경우에는 'getChar()' 를 호출하여, 문자열의 위치를 탐색하고 값을 리턴해주는 방식으로 이루어진다.

그런데 재밌는것이...

 

해당 메소드의 구현 밑바닥까지 가면

    @HotSpotIntrinsicCandidate
    private static int indexOfChar(byte[] value, int ch, int fromIndex, int max) {
        checkBoundsBeginEnd(fromIndex, max, value);
        return indexOfCharUnsafe(value, ch, fromIndex, max);
    }

    private static int indexOfCharUnsafe(byte[] value, int ch, int fromIndex, int max) {
        for (int i = fromIndex; i < max; i++) {
            if (getChar(value, i) == ch) {
                return i;
            }
        }
        return -1;
    }

'Unsafe' 라는 문구가 붙어있는것을 확인 할 수 있다.

범위 침범을 할 수 있어서 그런건지, 아니면 다른 이유가 있는건지는 모르겠지만 구현시에 '사실 좀 위험하긴한데...' 라는 생각을 했던건 아닌가 싶다.

 

2) 길이 파악

length(); 라는 메소드가 존재한다.

 

근데 구현이... 내가 잘못본게 아니라면

    public static int length(byte[] value) {
        return value.length >> 1;
    }

이게 끝이다.

배열값을 input으로 받고, 배열의 길이를 1 shift (뒤의 '\0' 문자 제거용으로 추정)하여 리턴해준다.

 

더 찾아봐도 저거 외에는 제대로 된 메소드를 찾을수가 없었다...

 

 

3) 비교

Java String에서는 '==' 를 쓰면 안된다는 사실을 알것이다.

==를 사용하게 되면, 값 자체를 비교하는것이 아니라 값의 위치, "객체"가 동일한지 아닌지를 비교하기 때문에 무조건 false가 날 수 밖에 없다.

그래서 사용하는것이 equals인데...

    public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            int len = value.length >> 1;
            for (int i = 0; i < len; i++) {
                if (getChar(value, i) != getChar(other, i)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

구조는 다음과 같다.

1)) 길이를 비교한다.

2)) 길이가 같다면 -> 동일한 위치의 문자를 1개씩 비교한다

3)) 이 중 하나라도 틀린다면 -> false

4)) 모든 비교를 통과했다면 true 

 

 

이런 식으로 구현이 되어있다.

 

아직 주제인 'C++ String 과 Java String의 차이' 에 대해서는 언급을 하지 않았는데, 이는 다음장에서 언급하도록 하겠다.

+ Recent posts