Programing/JAVA

Java String 에 대해 깊게 파고들어 보자~!

리커니 2019. 8. 1.
반응형

Java String 에 대해 깊게 파고들어 보자~!

 

갑자기 훅! String에 대해 파고들어볼까 합니다.

String이 파고들 것이 뭣이 있나? 하시겠지만, 생각보다 깊고 깊습니다..

그럼! 삽을 들고 파고들어보도록 하죠! (^^;;;)

 

String의 생성

 

우선 String은 우리가 알고 있듯이 문자열을 저장 하는 변수 입니다. 보다 정확하게 설명을 하면

문자열 객체의 인스턴스 주소를 담고 있는 참조형 변수이죠.

그렇습니다. 아래 그림과 같이 값이 아니라 메모리 주소를 담고 있죠.

 

하지만 String은 생성 방식에 따라 생성되는 메모리 영역은 달라지게 됩니다.

 

String str = "Hello";

 

위와같이 리터럴로 생성하게 되면 str 변수는 stack 메모리에,

"Hello" 라는 값은 Heap 메모리 내에 String pool이라는 곳에 저장되고 그 주소가 str 변수에 저장됩니다.

String pool 에 저장이 될 때는 intern() 메소드가 실행이 되게 되는데

같은 값이 있을 경우 기존값의 메모리 주소를, 다른 값일 경우 새롭게 객체를 생성해 값을 저장하고 그 메모리 주소를 리턴합니다. 그래서 아래와 같이 리터럴로 생성한 String을 동등연산자로 비교하게 되면 true가 리턴되게 되죠.

 

String a = "Hello";
String b = "Hello";
boolean c = a == b;
System.out.println(c);   //true, a와 b의 주소 값이 같음.

 

이 String pool은 HashMap 자료구조 형태이기 때문에 중복된 데이터가 저장되지 않는 특징이 있습니다. 그래서 동일한 문자열의 경우 같은 메모리 주소를 갖게 되는 것 입니다.

이렇게되면 메모리의 낭비를 좀 줄일 수 있겠죠? 그래서 String 객체 생성시에는 리터럴 방식을 권장합니다.

 

이와달리 new 연산자를 사용하여 생성한 경우

str이라는 변수는 stack 에 생성되는 건 같지만 "Hello" 라는 값은 일반 Heap 메모리 내에 생성되게 됩니다.

그래서 new 연산자를 사용하여 String 객체를 생성한 경우, 계속 새로운 인스턴스가 생성되게 됩니다.

 

String a = new String("Hello");
String b = new String("Hello");
boolean c = a==b;
System.out.println(c);  //false, a와비의 주소가 다름.

 

그리고 String 클래스의 생성자를 보면, char형의 배열 형태로 저장되는 것을 알 수 있습니다.

 

public final class String implements java.io.Serializable, Comparable, CharSequence {
    private final char value[];

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    .
    .
    .
}

 

컴퓨터가 인식하는 0과 1(bit단위)로 변환하기 위해 유니코드(2byte 문자체계)를 사용하는 char 형으로 변환해서 저장하는 구나~ 라고 생각할 수 있습니다.

 

String의 연산

 

문자열을 결합할때는 + 연산자를 사용합니다.

 

String a = "Hello";
String b = " World";
String c = a+b;
System.out.println(c);   // Hello World

 

concat() 메소드를 사용해서도 문자열을 결합할 수 있습니다.

 

String a = "Hello";
String b = " World";
String c = a.concat(b);
System.out.println(c);   // Hello World

 

하지만 이 둘은 엄청난 차이가 있습니다.

자바 1.5 버전 이전에서 + 연산자는 내부적으로 concat과 같은 방식으로 동작했습니다.

 

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

 

위와 같이 결합 후 new 연산자를 사용하여 새로운 메모리 주소를 리턴했죠.

그러니 2개의 문자열을 결합할때는 총 3개의 인스턴스가 생성되었습니다.

 

주소 값

1000 Hello

2000 World

3000 Hello World -> 3000을 리턴

 

1.5 버전에서는 이전에서는 String pool 개념 조차도 없었고, 유동적이지 못한 Permgen 영역에 저장되어

String 연산이 많이 발생한 경우 Out of memory를 빵빵 날렸습니다.

이러한 문제 때문에 1.5버전 부터는 새로운 개념이 나오게 됩니다.

 

StrinBuilder, StringBuffer

 

보다 효율적인 문자열 결합을 위해 StringBuilder와 StringBuffer 가 등장하게 됩니다.

이들의 append 메소드를 사용해 문자열을 결합한 경우 하나의 메모리 주소만을 갖게 됩니다.

 

StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.sppend(" World");
System.out.println(sb);   // Hello World

 

주소 값

1000 Hello

1000 World

1000 Hello World  ->1000을 리턴

 

이 놈들의 등장으로 String 객체의 + 연산방식도 바뀌게 됩니다.

 

String a = "Hello";
String b = " World";
String c = a+b;  // new StringBuilder(String.valueOf(a)).append(b).toString();
System.out.println(c); // Hello World

 

내부적으로는 StringBuilder를 생성해 append 메소드로 문자열을 결합하도록 바뀐 것이죠!

여러개의 문자열을 결합할 수록 메모리의 효율이 증가되겠죠.

 

참고로 StringBuffer와 StringBuilder는 동등연산자나 equals 메소드로 값을 비교할 수 없습니다.

그래서 toString 메소드로 String 인스턴스를 얻은 후 equals로 메소드로 비교하여야 합니다.

 

StringBuffer sb = new StringBuffer("gg");
StringBuffer sb2 = new StringBuffer("gg");
System.out.println(sb==sb2);  //false
System.out.println(sb.equals(sb2));  //false
System.out.println(sb.toString().equals(sb2.toString()));  //true

 

StringBuilder, StringBuffer의 차이

 

이 둘의 차이는 동기화에 있습니다.

StringBuffer는 동기화를 제공하는 대신 속도가 느리고

StringBuilder는 동기화를 제공하지 않아 속도가 보다 빠릅니다.

멀티쓰레드 환경에서 동시에 객체에 접근하는 경우 StringBuffer를,

아닌 경우에는 StringBuilder를 사용합니다.

단순성능 : StringBuilder > StringBuffer >>> String

 

Link : Java List<VO> to JsonArray, String, StringBuilder, StringBuffer

 

Java List to JsonArray, String, StringBuilder, StringBuffer

Java List to JsonArray, String, StringBuilder, StringBuffer 타 업체 API를 걷어내고 새로 구축한 DB 베이스의 API를 개발하는 업무를 맡았을 때의 코드를 리펙토링 하는 도중 이상한 코드가 발견되어 수정..

aljjabaegi.tistory.com

 

String 의 값 비교

 

String 변수의 값이 같은지 다른지 비교는 동등연산자(==)가 아닌 equals() 메소드를 사용합니다.

앞에서 말씀드렸듯이 String 변수는 인스턴스 주소를 담고 있기 때문에 동등연산자로는 값을 비교할 수 없습니다.

 

String a = "Hello";
String b = "Hello";
if(a.equals(b)) System.out.println("같아요!");

 

[equals Method]

 

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                    i++;
            }
            return true;
        }
    }
    return false;
}

 

String은 객체인데 System.out.println() 출력 시 값이 찍히는 이유

 

갑자기 든 궁금증.

String은 객체인데 출력을 하면 왜 메모리 주소가 아닌 값이 찍히는 것일까?

일단 System.out.println(); 이 동작하는 과정을 따라가 보았습니다.

System.class -> PrintStream.class -> BufferedWriter.class -> Writer.class -> StringWriter.class

 

StringWriter.class 를 보면 StringBuffer에 값을 append해서 write를 하게 되는데,

StringWriter.class에는 toString 메소드가 오버라이드 되어 있어 문자열로 출력을 하게 됩니다.

 

 

별거 아닌줄 알았던 String인데 파고들다 보니 별거 아닌게 아니죠?

역시 고슬링은 천재인 것 같습니다.

 

반응형

댓글

💲 추천 글