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의 차이' 에 대해서는 언급을 하지 않았는데, 이는 다음장에서 언급하도록 하겠다.

모 회사 면접 준비중에 예전에 당했던 문제가 생각나서 정리하려다, 문득 제대로 조사해본적이 없는거같아 기록용으로 작성한다.

사실, 검색해보다가 내가 시원하게 이해할만한 자료가 없었던것도 있고...

 

C에서 Char 형 배열 / 포인터로 겨우겨우 만들던 String에서 벗어나, 관리 함수등을 추가한 C++ String Class로 되면서, '어느정도는' 타 언어의 String 사용법과 비슷하게 이루어지기 시작했다. 물론, 문자열 다루는거는 Java 보다는 불편하긴한데, 이전 면접에서 '왜 불편한가요?' 라는 질문에 좀 어영부영 대답한 감이 없잖아 있는것같다.

 

따라서 포인터 개념이 살아있는 OOP인 C++과, 전부 GC로 관리되는 Java와 비교를 해보고자 한다.

 

 

1. C++ String의 경우 (wstring은 포함시키지 않음)

기본적으로 C++ String은 'Basic String' 이라는 자체 클래스를 내부에 구현한 상태로 되어있으며, Basic String의 생성자는 크게 'const Char *' 를 input으로 받는 경우, 자체 Allocator를 input으로 받는 경우(= 타 value_Type 배열포인터를 input으로 받는 경우), basic_string 참조값을 input으로 받는 경우로 나뉘어진다.

즉, 실제 구현에서는 '일단 모든 데이터형을 받을수 있도록' 구현이 되어있긴 하나, 문자열 자체를 받는경우는 여전히 Char 형 배열 포인터를 값으로 받는다... 고 설명할 수 있다.

그리고, String Class가 되면서, 각종 접근법 / 용량 체크 / 복사 / equal 등의 함수가 가능해졌다.

 

 

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

1) 접근법

str.at(N) = N번 자리의 문자열을 리턴해준다.

str.at(1) 을 사용시, 1번째 값인 e를 리턴해주게 된다.

 

str[N] = N번 자리의 문자열을 리턴해준다.

str.at(N)과 다른게 뭐냐고 한다면, 둘다 결국엔 str[N]으로 귀결되는건 동일하나,

basic_string<_CharT, _Traits, _Allocator>::at(size_type __n) const
{
    if (__n >= size())
        this->__throw_out_of_range();
    return (*this)[__n];
}

at()의 경우에는 잘못된 값 삽입 시 out of range() 경고를 리턴해주는 차이가 있다.

속도 자체는 자체 함수로 이동하지 않는 str[N]이 빠르긴 하나, 내가 짠 코드를 100% 믿을 수 있는게 아니라면 at()을 쓰는것을 추천한다...

 

 

2) 길이 파악

size() 함수와 length() 함수가 존재하는데...

 

    _LIBCPP_INLINE_VISIBILITY size_type size() const _NOEXCEPT
        {return __is_long() ? __get_long_size() : __get_short_size();}
    
    _LIBCPP_INLINE_VISIBILITY size_type length() const _NOEXCEPT {return size();}
    

그냥 length 함수 호출해도, size를 호출해도 결국엔 size() 함수가 호출되는거니까 아무거나 써도 된다 (...)

 

 

3) 비교

str.compare() 이라는 함수가 있다.

str.compare("ABC") 와 같은 방식으로 사용이 가능한데...

 

일반적으로, '같으면 0', '함수를 호출한 String이 사전순으로 빠를때 -1', '사전순으로 느릴때 1' 의 값을 리턴하는것으로 알고 있을것이다.

 

이는 또 재밌게도, 

basic_string<_CharT, _Traits, _Allocator>::compare(const _Tp& __t) const
{
    __self_view __sv = __t;
    size_t __lhs_sz = size();
    size_t __rhs_sz = __sv.size();
    int __result = traits_type::compare(data(), __sv.data(),
                                        _VSTD::min(__lhs_sz, __rhs_sz));
    if (__result != 0)
        return __result;
    if (__lhs_sz < __rhs_sz)
        return -1;
    if (__lhs_sz > __rhs_sz)
        return 1;
    return 0;
}

'일단' 두 String을 가져와 min 비교를 걸친 후 (String 별 호출순서 비교), 만일 min() 비교가 동일한것으로 나오면, 길이 비교를 진행, 이후 해당 값을 리턴해주는 식으로 진행된다.

즉, 만일 str.compare("Hello"); 를 넣어줬다면

Hello World / Hello 두 문자열의 비교 자체는 4번째 값인 o까지 비교를 하고, 그 이상은 비교가 불가능하니 if문으로 넘어가게 된다.

근데 여기서 좌측값 (= 함수를 호출한 String)의 길이가 더 길기때문에, 1을 리턴해주는것으로 결과가 종료되는것이다.

 

Min의 내부 구현은 (아스키코드 값의 경우) A값과 B값의 코드 순서로 비교를 수행하게된다.

자세한건 검색을 통해 찾아보시길...

 

 

 

아무튼, 요런 식으로 구현이 되어있는데, 위에 언급했다시피, '배열 포인터' 로 구현이 되어있기에, 문자열을 수정하면 해당 문자열 메모리 자체를 수정하는식으로 값을 수정하게 된다.

이것이 메모리 관리  관점에서 Java String과의 가장 큰 차이점이라고 볼 수 있기도 하다.

 

나머지는 다음장에... 생각보다 길어질거같아서 쪼개야할것같다.

 

 

 

더보기

https://code.woboq.org/llvm/libcxx/include/string.html

 

string source code [libcxx/include/string] - Woboq Code Browser

 

code.woboq.org

참고자료.

C++ 내부 구현을 기록해 둔 레퍼런스 사이트.

해당 글을 작성할때 참고한 페이지를 링크해두었다.

 

초보자가 볼 만한 사이트는 아니므로, 레퍼런스 자료가 필요하면 https://en.cppreference.com/를 참고하거나, 검색을 통해 한글로 설명된 다른 페이지를 참고하는걸 추천드린다..

 

+ Recent posts