인생을 코딩하다.

[Java] String,String, String +, StringBuffer, StringBuilder, Wrapper Class, boxing & unboxing 본문

Java

[Java] String,String, String +, StringBuffer, StringBuilder, Wrapper Class, boxing & unboxing

Hyung1 2020. 11. 19. 15:59
728x90
반응형

String class클래스의 내부

jdk 11

Object 클래스를 확장하고, 직렬화 Serializable, 매개변수로 넘어가는 객체와 현재 객체가 같은지를 비교하는데 사용하는 Comparable, 문자열을 다루는CharSequence 를 구현하고 있다.

또 위에 보면 final로 선언이 되어있다. 그래서 상속해서 사용할 수 없다.

String 문자열을 byte로 변환하기

  • 같은 프로그램 내에서 문자열을 byte 배열로 만들 때에는 getBytes() 메서드 사용
  • 다른 시스템에서 전달 받은 문자열을 byte 배열로 변환할 때에는 두번째나 세번째에 있는 메소드를 사용하는 것이 좋다 -> 문자열이 다른 캐릭터 셋으로 되어 있을 수도 있기 때문에
byte[] array1 = korean.getBytes(StandardCharsets.UTF_8);
String korean2 = new String(array1,StandardCharsets.UTF_16);
예시
  • 한글 한 글자의 바이트는 EUC-KR -  2바이트, UTF-8, 16 - 3 바이트
  • getBytes() 메서드를 이용할 때, 존재하지 않는 캐릭터 셋의 이름을 지정할 경우 에외가 발생하게 되므로 반드시 try~catch을 추가해준다.

String의 메소드나 그외 모든 객체를 사용하기 전에 항상 널 체크를 해준다.

널 체크를 하지 않아서 애플리케이션이 비정상으로 작동하여 장애로 이어질 수도 있기 때문에,

메소드의 매개 변수로 넘어오는 객체가 널이 될 확률이 조금이라도 있다면 반드시 한 번씩 확인하는 습관을 갖고 있어야 한다.

상수 풀(Constant pool)

  • 힙 영역의 Permanent area(고정 영역)에 생성되어 Java 프로세스의 종료까지 계속 유지되는 메모리 영역이다. 기본적으로 JVM에서 관리하며 프로그래머가 작성한 상수에 대해서 최우선적으로 찾아보고 없으면 상수풀에 추가한 이후 그 주소값을 리턴한다.
  • 객체들을 재사용하기 위해서 상수풀이란 것이 만들어졌다. String의 경우 동일한 값을 갖는 객체가 있으면, 이미 만든 객체를 재사용한다.  예)
String text = "Check value"; 
String text2 = "Check value"; // 문자열 리터럴을 변수에 넣는 방법
String text3 = new String("Chect value"); // 객체로 생성하는 방법

text와 text2는 동일한 값을 갖는 객체이므로 text2는 이미 만들어진 text를 재사용한다. 따라서 text와 text 객체는 실제로 같은 객체이다. 하지만 text3은 String 객체를 생성했으므로 값이 같은 String 객체를 생성한다고 하더라도 상수 풀의 값을 재활용하지 않고 별도의 객체를 생성한다. 

 

아무래도 String 리터럴은 같은 내용의 문자열을 공유하기 때문에,

힙 영역에 저장이 되고 같은 내용의 문자열이라도 공유하지 않는 객체형보다 더 좋은 방법 인 것 같다.

풀에 해당 값이 있으면, 풀에 있는 값을 참조하는 객체를 리턴하는 intent메서드를 사용하는 것은 좋지 않다.

equlas와 ==로 비교하는 것중 ==로 비교하는 것이 훨씬 빠르다. 하지만,

만약 새로운 문자열을 쉴새 없이 만드는 프로그램에서 interrn() 메소드를 사용하여 억지로 문자열 풀에 값을 할당하도록 만들면, 저장되는 영역은 한게가 있기 때문에 그 영역에 대해서 별도로 메로리를 청소하는 단게를 거치게 된다. 따라서, 작은 연산 하나를 빠르게 하기 위해서 전체 자바 시스템의 성능의 악영향을 주게 된다.

String은 왜 불변객체로 만들어졌을까?

String은 자바에서 가장 많이 사용되는 객체이다. 그렇다는 말은 String타입의 객체들이 가장 많은 메모리를 차지한다는 뜻으로 해석할 수 있다. 그래서 Stirng 객체는 재사용 될 가능성이 높기 때문에 같은 값일 경우, 어플리케이션 당 하나의 String만을 생성해두어 JVM의 힙을 정약하기 위함이다.

 

1.String 객체의 캐싱

  • 예를들어, '축구'와 관련된 웹사이트가 Java 언어로 만들어졌다고 가정해보자. 여기서 "축구" 라는 문자열이 아마도 굉장히 많이 쓰일것이다. 만약에 이 사이트가 세계적으로 유명한 사이트라면, 1초에 수백, 수천만의 HTTP request 요청을 받게 될 것이다.
  • "축구"라는 값을 가지는 문자열 객체를 사용자의 요청을 처리할때마다 계속해서 한개씩 생성한다면, 힙(heap)에는 "축구"라는 값을 가진 문자열 객체가 사용자의 요청 만큼 생성되어 있을 것이다.
  • 조금 더 작은 예로, 우리가 "Hello" 라는 문자열을1000번 출력한다고 가정해보자.
for(int i=0; i<1000; i++){ 
	String str = "Hello"; 
    System.out.println(str); 
}
  • 위 코드는 "Hello" 라는 값을 갖는 1000개의 String 객체를 생성하게 된다. 그러나 String의 immutable(불변)한 성질 덕분에 실제로는 "Hello"라는 문자열 객체는 단 하나만 생성된다.
  • 다만, "Hello"라는 참조값을 갖는 참조변수인 str변수 자체는 스택상에서 1000번 생성되었다가 사라질 뿐이다.
  • 이러한 원리는 앞에서 설명한 String Constant Pool 이라는 특별한 공간에 의해 성립한다.
  • 동일한 문자열이 String Constant Pool 에 존재한다면, 새로 객체를 생성하지 않고 String Constant Pool 에 있는 객체를 사용하는 것이다.
  • 결론적으로, 객체의 캐싱으로 인해 [메모리 절약]과 자주 쓰이는 값을 CPU와 가까운 곳에 위치시킴으로써 [속도 향상] 의 효과를 얻을 수 있다.

2. 보안 기능

  • String이 불변이 아니라면 보안상의 문제를 야기할 수 있다.
    • 예를 들어, DB의 username과 password 라던가, 소켓 통신에서 host와 port에 대한 정보가 String으로 다루어지기 때문에 String이 불변이 아니라면, 해커의 공격으로부터 값이 변경될 수 있다.
    • 네트워크 연결시 포트,파일 경로, db 연결에 필요한 URL도 모두 String으로 이루어져 있다. 그런데 이러한 String이 가변적이라면 누군가가 고의로든 실수로든 a를 b로 바꿔버리면 심각한 문제를 초래할 수 있다.

3. 안전성(Thread-safe)

  • String 객체가 변경될 수 없다는것은 여러 쓰레드에서 동시에 특정 String 객체를 참조하더라도 안전하다는 뜻이다.
    • 만약, 여러 어플리케이션에서 특정 String 객체를 참조하고 있을때, 그 값은 절대 변하지 않으므로 안전하다고 할 수 있다.
    • 앞에서 설명했듯이, 만약 String str= "KH"에서 str = "KI HYUK"로 새로운 값을 할당한다고 해도, String 객체의 값이 변경된 것이 아니다.

 

immutable한 String을 보완하기 위한 클래스, StringBuffer와 StringBuilder의 차이점

 

String은 불변의 객체라고 했다. 분명 String이 불변으로 설계된대는 충분한 이유가 있고, 여러가지 장점이 있다. 하지만 String이 불변으로 설계되어서 생기는 단점도 있다.

바로 GC(Garbage Collection) 이다. 기존 문자열을 수정하게 되면 그 전에 사용하던 String 객체는 GC의 대상이 된다.

이러한 단점을 보완하기 위한 StringBuffer와 StringBuilder가 있다.

1. StringBuffer

StringBuffer의 내부 구조

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, Comparable<StringBuffer>, CharSequence
{

String 클래스의 인스턴스는 한 번 생성되면 그 값을 읽기만 할 수 잇고, 변경할 수는 없다. 

하지만 StringBuffer 클래스의 인스턴스는 그 값을 변경할 수도 있고, 추가할 수도 있습니다.

 

이를 위해 StringBuffer 클래스는 내부적으로 버퍼(buffer)라고 하는 독립적인 공간을 가진다

버퍼 크기의 기본값은 16개의 문자를 저장할 수 있는 크기이며, 생성자를 통해 그 크기를 별도로 설정할 수도 있다.

하지만 인스턴스 생성 시 사용자가 설정한 크기보다 언제나 16개의 문자를 더 저장할 수 있도록 여유 있는 크기로 생성된다.

 

덧셈(+) 연산자를 이용해 String 인스턴스의 문자열을 결합하면, 내용이 합쳐진 새로운 String 인스턴스를 생성한다.

따라서 문자열을 많이 결합하면 결합할수록 공간의 낭비뿐만 아니라 속도 또한 매우 느려지게 된다.

 

하지만 StringBuffer 인스턴스를 사용하면 문자열을 바로 추가할 수 있으므로, 공간의 낭비도 없으며 속도도 매우 빨라진다. 이러한 StringBuffer 클래스는 java.lang 패키지에 포함되어 제공된다. (아래 StringBuilder도 마찬가지)

  • Synchronized - 동기화를 지원한다.
    • 동기화 : A스레드와 B스레드가 한 객체를 작업중일 때, A가 값을 바꿔버리면 B가 엉뚱한 값으로 작업을 시도할 수 있다. 여러 스레드가 한 자원을 사용하려고 할 때 다른 스레드의 접근을 막는 것을 동기화라 한다. 데이터의 무결성을 보장해준다. 다른 스레드의 접근을 막는 동기화를 지원하니, 여러 스레드(멀티 스레드)가 작업하기에 매우 안전한 환경이 만들어진다. 그래서 동기화를 지원한다는 말은 멀티스레드 환경을 지원한다는 말과 같다고 볼 수 있다.
  • 동기화를 지원한다는 것은 멀티스레드 환경을 지원한다는 것이다. 즉, 멀티스레드 환경에서 안전하다는 뜻이다. (=Thread safe)

2. StringBuilder

StringBuilder의 내부 구조

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
{
  • non-Synchronized - 동기화를 지원하지 않는다.또한 StringBuilder는 동기화를 고려하지 않기때문에 단일 쓰레드 환경에서 사용시 StringBuffer에 비해 속도가 빠르다.
  • 동기화를 지원하지 않기 때문에 단일 스레드 환경에서 사용해야 한다.

StringBuffer와 StringBuilder 정리

  • StringBuffer와 StringBuilder의 차이는 'Thread safe 하다' 와 'Thread safe 하지 않다' 는 것이다.
  • 결론적으로, StringBuffer가 StringBuilder보다 더 안전하다는 것만 기억해두자.
  • 다만 속도는 StringBuilder가 더 빠르다.
  • StringBuffer은 하나의 문자열 객체를 여러 쓰레드에서 공유해야 하는 경우에만 사용하고, StringBuilder은 여러 쓰레드에서 공유할 일이 없을때 사용하면 된다.

결론

  • String 클래스는 자바에서 가장 많이 사용되는 클래스 중 하나다. 따라서 String에 대해 깊게 공부할 필요가 있다.
  • String 클래스를 잘 사용해야만 메모리를 효율적으로 사용할 수 있다
  • StringBuffer와 StringBuilder를 상황에 맞게 잘 사용하자

StringBuffer, StringBuilder에서는 + 연산 대신 append() 라는 함수를 이용한다.

value에 사용되지 않고 남아있는 공간에 새로운 문자열이 들어갈 정도의 크기가 있다면 그대로 삽입한다. 그렇지 않다면 value 배열의 크기를 두배로 증가시키면서 기존의 문자열을 복사하고 새로운 문자열을 삽입한다. 

 

예시

  1. StringBuilder sb = new StringBuilder();
    • StringBuilder 를 생성할 때 capacity를 지정하지 않으면 기본 16으로 설정됩니다.
    • 따라서 value 의 크기는 16, 값은 비어있게 되고,
    • count는 0으로 초기화 됩니다.
  2. sb.append("first string");
    • “first string” 이라는 문자열의 크기는 12이고 value는 비어있으므로 수용공간 16보다 작으므로 아무런 문자열의 복사 없이 바로 추가됩니다.
    • value 의 값은 “first string” 이 되고,
    • count는 12로 갱신됩니다.
  3. sb.append("+second string");
    • “second string” 의 크기는 14. value 에 남아 있는 공간이 4 밖에 되지 않으므로 배열의 크기를 늘려줘야 합니다.
    • value 의 크기를 두배(32)로 늘리고 기존의 문자열을 복사합니다. 새로운 문자열까지 더해주면 value 값은 “first string+second string”이 됩니다.
    • count는 26으로 갱신됩니다.

실제로는 위의 설명보다 조금 더 복잡하게 구현되어있지만 핵심적인 부분은 동일합니다.
이런식으로 StringBuffer와 StringBuilder는 문자열의 더하더라도 매번 문자열을 복사할 필요가 없어서 성능을 높일 수 있습니다.

 

StringBuffer와 StringBuilder 추가내용

 

글을 다 작성하고 StringBuffer와 StringBuilder의 적절한 상황에 대해서 더 알아보았다.

  • StringBuffer와 StringBuilder는 성능으로만 보면 2배의 속도 차이를 보인다고 한다.
  • 하지만 참고한 사이트의 속도 실험 결과, append() 연산이 약 1억6천만번 일어날 때 약 2.6초 정도의 속도 차이를 보인다고 한다.
  • 2.6초라는 수치가 무의미한지 유의미한지는 프로그램의 스펙에 따라 다르다.
  • 따라서, 문자열 연산이 엄청나게 많이 일어나지 않는 환경이라면 StringBuffer를 이용해 thread-safe하게 구현하는 것이 좋다는 의견이 있었다.
  • StringBuffer, StringBuilder 클래스에서도 String 클래스를 이용합니다. toString() 메소드의 경우 StringBuffer, StringBuilder의 toString()가 호출되면 해당 문자열에 대한 String 객체를 생성해서 반환한다.
  • 따라서 연산이 적게 사용되고, 문자열 값의 수정 없이 읽기가 많은 경우에는 String 클래스의 사용이 더 적절하다.

StringBuffer는 Thread-safe하기 때문에 하나의 문자열 객체를 여러 쓰레드에서 공유해야 하는 경우에 사용하자.
StringBuilder는 Thread-safe하지 않다. 그러나 StringBuffer에 비해 속도가 빠르다. 즉, 여러 쓰레드에서 공유할 일이 없을 때 사용하자.
StringBuilder와 StringBuffer중 어떤 클래스가 더 좋다라고 순위를 매길수는 없다. 자기 주관을 가지고 사용하면 된다.

 

출처 :

자바의 신 / 이상민

StringBuffer, StringBuilder가 String보다 성능이 좋은 이유와 원리

TCPschool

 

============================================================================

 

String 클래스 선언하기 

 

String str1 = new String("abc");   // 인스턴스로 생성됨

String str2 = "abc";                  // 상수풀에 있는 문자열을 가리킴

 

                    힙 메모리

str1 ------->     "abc"

                

                    상수 풀

str2 ------->    "abc"

 

 

힙 메모리

사용자가 직접 관리할 수 있고 해야만하는 영역

- 힙 영역은 사용자에 의해 메모리 공간이 동적으로 할당되고 해제된다.

- 메모리의 낮은 주소에서 높은 주소의 방향으로 할당된다.

- malloc() 또는 new 연산자를 통해 할당하고 free() 또는 delete 연산자를 통해서 해제가 가능하다.

- 런타임 시에 크기가 결정된다.

장점

- 프로그램에 필요한 개체의 개수나 크기를 미리 알 수 없는 경우에 사용 가능.

- 개체가 너무 커서 스택 할당자에 맞지 않는 경우 사용 가능.

단점

- 할당 작업으로 인한 속도 저하

- 해제 작업으로 인한 속도 저하

 

상수 풀(Constant pool)

힙 영역의 Permanent area(고정 영역)에 생성되어 Java 프로세스의 종료까지 계속 유지되는 메모리 영역이다. 기본적으로 JVM에서 관리하며 프로그래머가 작성한 상수에 대해서 최우선적으로 찾아보고 없으면 상수풀에 추가한 이후 그 주소값을 리턴한다.

 

장점 -  메모리 절약 효과

 

str1, str2는 메모리 주소가 틀리다.

        String str1 = new String("hyungil");
        String str2 = new String("hyungil");

        System.out.println(str1 == str2);
        // false
        
        String str3 = "hyungil";
        String str4 = "hyungil";

        System.out.println("str3 == str4");
        // true

String은 immutable(불변)

한번 선언되거나 생성된 문자열을 변경할 수 없음

 

String 클래스의 concat()메서드 혹은 "+"를 이용하여 String을 연결하는 경우 문자열은 새로 생성됨

public static void main(String[] args) {
        String str1 = new String("hyungil");
        String str2 = new String("angry");
        System.out.println(System.identityHashCode(str1));

        str1 = str1.concat(str2);

        System.out.println(str1);
        System.out.println(System.identityHashCode(str1));
    }

284720968
hyungilangry
189568618

str1에 str2가 붙어서 "hyungilangry"가 되는 것이 아니라,

str1이 새로 생성된 문자열을 가리키게 되는 것이다.

 

우리가 String를 가지고 작업할 일이 굉장히 많다. 예를 들면, 프로토콜을 만든다거나 서버에서 클라이언트쪽으로 리스펀스를 주기 위해서 내용을 작성한다던가 할때 String 값을 계속 연결을 해서 그 결과를 만든다고 하면 메모리 낭비가 계속 되게 된다. 메모리들이 게속 GC가 생긴다.

 

실제로 메모리 주소가 어떻게 되나 확인해보려고 할때, 해쉬코드 값으로는 확인 할 수 없고 (해쉬코드는 스트링에서 오버라드이 했기때문에) , System.identityHashCode(str1) 를 이용해서 메모리 주소를 비교해보면 주소 값이 틀린 것을 볼 수 있다. 결국 연결이 되면, 새로운 것을 가리킨다는  것을 볼 수 있다.

 

그리하여 String을 계속 연결해서 쓸 일이 있다면, StringBuilder과 SringBuffer을 쓰는 것이 좋다

 

StringBuilder과 SringBuffer

- 가변적인 char[] 배열을 멤버변수라 가지고 있는 클래스

- 문자열을 변경하거나 연결하는 경우 사용하려면 편리한 클래스

- StringBuffer는 멀티 쓰레드 프로그래밍에서 동기화(Synchronization)이 보장됨 (StringBuilder는 동기화 지원이 안됨)

- 단일 쓰레드 프로그래밍에서는 StringBuilder를 사용하는 것이 더 좋음 (멀티 쓰레드에 관한 내용은 Java -> multi-thread에 포스팅 해놨습니다.)

- toString() 메서드로 String 반환

 

StringBuilder (Buffer도 메모리 주소가 똑같이 나온다.)

    public static void main(String[] args) {

        String str1 = new String("hyungil");
        String str2 = new String("cute");

        StringBuilder buffer = new StringBuilder(str1);
        System.out.println(System.identityHashCode(buffer));
        buffer.append("cute");
        System.out.println(System.identityHashCode(buffer));

        str1 = buffer.toString();
    }
284720968
284720968

 

메모리 주소 값은 똑같다. 말씀드린대로 불변이 아니라 append() 하고 value 값이 다 바뀐 다음에 나중에 필요하면, toString()해서 쓰면 된다.

 

StringBuffer

    public static void main(String[] args) {

        String s = "jeonghyungil";
        StringBuffer sb = new StringBuffer(s);

        System.out.println("처음 상태 : " + sb);
        System.out.println("문자열 string 변환 : " + sb.toString());
        System.out.println("문자열 추출 : " + sb.substring(2,4));
        System.out.println("문자열 추가 : " + sb.insert(2,"추가"));
        System.out.println("문자열 삭제 : " + sb.delete(2, 4));
        System.out.println("문자열 연결 : " + sb.append("aaaaaa"));
        System.out.println("문자열의 길이 : " + sb.length());
        System.out.println("용량의 크기 : " + sb.capacity());
        System.out.println("문자열 역순 변경 : " + sb.reverse());
        System.out.println("마지막 상태 : " + sb);
    }
처음 상태 : jeonghyungil
문자열 string 변환 : jeonghyungil
문자열 추출 : on
문자열 추가 : je추가onghyungil
문자열 삭제 : jeonghyungil
문자열 연결 : jeonghyungilaaaaaa
문자열의 길이 : 18
용량의 크기 : 28
문자열 역순 변경 : aaaaaalignuyhgnoej
마지막 상태 : aaaaaalignuyhgnoej

StringBuilder 

    public static void main(String[] args)  {
        String s = "jeonghyungil";
        StringBuilder sb = new StringBuilder(s); // String -> StringBuilder

        System.out.println("처음 상태 : " + sb);
        System.out.println("문자열 String 변환 : " + sb.toString());
        System.out.println("문자열 추출 : " + sb.substring(2,4));
        System.out.println("문자열 추가 : " + sb.insert(2,"추가"));
        System.out.println("문자열 삭제 : " + sb.delete(2,4));
        System.out.println("문자열 연결 : " + sb.append("zzzzzzz"));
        System.out.println("문자열의 길이 : " + sb.length());
        System.out.println("용량의 크기 : " + sb.capacity());
        System.out.println("문자열 역순 변경 : " + sb.reverse());
        System.out.println("마지막 상태 : " + sb);
    }
처음 상태 : jeonghyungil
문자열 String 변환 : jeonghyungil
문자열 추출 : on
문자열 추가 : je추가onghyungil
문자열 삭제 : jeonghyungil
문자열 연결 : jeonghyungilzzzzzzz
문자열의 길이 : 19
용량의 크기 : 28
문자열 역순 변경 : zzzzzzzlignuyhgnoej
마지막 상태 : zzzzzzzlignuyhgnoej

 

Wrapper Class 

 

기본형            Wrapper class

boolean           Boolean

byte                Byte

char                Character

short               Short

int                  Interger

long                Long

float                Float

double             Double

 

Wrapper Class 특징은 ToString, HashCode, Equals 등이 다 오버라이딩이 되있고, new 했을때나 값을 가져올 때 메모리를 사용하는 것이 좀 다르다. Character와 Boolean을 제외한 숫자를 처리하는 클래스를 wrapper 클래스라고 부르며 모두 Number라는 abstract 클래스를 확장(extends)한다. 겉으로보기에는 참조자료형이지만, 자바 컴파일러에서 자동으로 형 변환을 해주기 때문에 기본자료형 처럼 쓸 수 있다.

 

기본형(primitive type) 변수도 때로는 객체로 다루어져야 하는 경우가 있다.

1. 매개변수로 객체가 요구 될때.

2. 기본형 값이 아닌 객체로 저장해야 할 때.

3. 객체간의 비교가 필요할 때. 등등

이 때 사용되는 것이 wrapper class 이다.

 

박싱(boxing), 언박싱(un-boxing)

 

박싱(boxing)은 기본 자료형의 데이터를 래퍼(wrapper) 클래스의 객체로 만드는 과정을 의미한다.

언박싱(un boxing)은 래퍼(wrapper) 클래스의 데이터를 기본 자료형으로 얻어내는 과정을 말한다.

기본형 값과 대응하는 Wrapper 객체 사이의 자동 변환

 

Integer ten = 10; // 자동 박싱 Integer ten = Integer.valueOf(10);로 자동 처리

int n = ten;        // 자동 언박싱 int n = ten.intValue();로 자동 처리

 

    public static void main(String[] args) {
        int n = 10;
        // Integer i = Integer.valueOf(n);  // 이렇게 할 필요 없이
        Integer i = n;                      // 이렇게만 써주면 된다. 자동박싱

        int n2 = i; // i라는 객체가 가지고 있는 정수값을 간편하게 뽑아낼 수 있다. 언박싱
    }
728x90
반응형

'Java' 카테고리의 다른 글

[Java] 입출력 스트림  (0) 2020.11.19
[Java] Generic  (0) 2020.11.19
[Java] Mulit-thread  (0) 2020.11.18
[Java] Thread  (0) 2020.11.17
[Java] 디자인패턴(DesignPattern) - 데코레이터 패턴(Decorator Pattern)  (0) 2020.11.12
Comments