자바 String

황희윤·2023년 11월 21일

String

문자열을 위한 클래스

  • 참조형(Reference Type)으로 객체와 같이 스택 영역에 저장되는 것이 아니라 (heap)에 저장된다.

  • String 클래스 내부에서는 char형의 배열 객체를 다룬다.

  • 문자열을 저장하고자 문자형 배열 변수(char[]) value를 인스턴스 변수로 정의해놓고 있다.

  • 자바에서는 객체들을 재사용하기 위해서 Constant Pool에 객체들을 저장하는데, String의 경우 동일한 값을 갖는 객체가 이미 Constant Pool에 있으면 이미 만든 String 객체를 재사용한다.

// 둘은 똑같은 객체로 똑같은 객체의 주솟값을 가진다.
String text = "hi";
String text2 = "hi"; // text를 재사용한다.
  • 하지만 new 연산자로 String 객체를 만든 경우는 Constant Pool의 값을 재사용하지 않고 별도의 새로운 객체를 생성해서 사용한다. 따라서 내용은 같으나 다른 객체다.

equals() 메서드

  • 값이 동일한지를 비교한다.

동등 연산자(==) vs equals()

동등 연산자(==) : 기본형(primitive type) 변수 간에 값을 비교하거나 참조형 변수간에 참조하는 메모리의 주솟값을 비교할 때 사용한다.

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

boolean result = (str1 == str2); // false, 서로 다른 객체를 가리키므로

equals() 메서드 : 매개 변수를 비교하고자 하는 객체의 참조 변수를 받아 비교한다

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

boolean result = str1.equals(str2); // true, 내용이 동일하므로

위의 코드에서처럼 String, Integer, Double 등은 equals() 메서드를 오버라이딩해서 내용을 비교한다.

하지만 일반적인 경우, 서로 다른 객체인 경우 equals() 메서드는 늘 false를 반환한다.

equals 메서드에서는 기본적으로 동등 연산자(==)를 사용해서 객체를 비교한다. 즉, 두 참조 변수가 힙에 있는 같은 객체를 참조하는지를 검사한다.


hashCode() 메서드

  • 객체가 가지고 있는 고유한 값

  • 객체를 구별할 때 사용한다.

  • 해시코드는 객체의 주소와 관련된 정숫값이다.

  • 원시(native) 메서드로 JVM에서 원시 코드로 직접 구현된 메서드

  • 해시코드가 같고 자료형도 같아야 같은 객체라고 본다.


Immutable (변경 불가능)

String hello = new String("hello");
  • new 연산자와 String 생성자를 사용하면, 생성할 때마다 새로운 문자열 객체가 생성된다. 그러나 이러한 구조는 문자열을 다루는데 비효율적일 수 있다.

  • String 클래스는 문자열 객체를 생성하면서 상수로 인식한다. 그래서 한번 생성되면 문자열을 수정할 수 없다.

  • 즉, 기존의 문자열을 수정하는 것이 안라 새로운 문자열을 생성해야 한다. 이를 불변성이라고 한다.

  • 한 번 생성된 String은 변경 할 수 없다.

  • 하나의 문자열과 다른 문자열을 결합하면 기존 문자열에 추가되는 것이 아니라 새로운 문자열이 생성된다.

 public static void main(String[] args) {
 	String a = "a";
    String b = "b";
    a = a + b;
    System.out.println(a.hashcode() == b.hashcode()); // false
 }

이 때문에 + 연산자 사용은 지양해야 한다.

문자열 리터럴을 지정해서 String 생성

String hello = "hello"
  • 위의 코드처럼 String을 만들면 실행 데이터 영역(runtime data area)의 상수 풀(constant pool)에 리터럴이 저장된다.

    • Runtime Data Area : 프로그램 실행을 위해 운영체제로부터 할당받은 메모리 공간
    • Constant Pool : 상숫값이 중복되는 경우 재사용하기 위한 영역으로 클래스에서 사용된 상수를 저장한다.
  • 상수 풀은 클래스와 같은 Runtime Data Area의 메서드 영역에 위치해서 자바 프로세스가 종료될 때까지 생명 주기를 함께 한다.

  • String 객체를 new 연산자로 생성하지 않고 문자열 리터럴을 지정해서 생성할 경우, 내부적으로 new String() 메서드를 호출한 이후에 String intern() 메서드가 호출되어 고유의 객체공유하도록 강제로 소속(interned)된다.

  • 이렇게 상수 풀을 사용해서 문자열을 관리하면 메모리를 절약하는데 효과적이다. 또한 상수 풀은 GC의 대상이 되지 않기 때문에 프로세스가 종료될 때까지 삭제되지 않는다.

public static void main(String args[]) {
       // 힙
       String s1 = new String("hello");
       String s2 = new String("hello");
       
       // 상수 풀
       String s3 = "hello";
       String s4 = "hello";
       String s5 = s1;
       
       System.out.println(s1 == s2); // false
       System.out.println(s1.equals(s2)); // true
       System.out.println(s1.hashCode() == s2.hashCode()); // true
       
       System.out.println(s2 == s3); // false
       System.out.println(s2.equals(s3)); // true
       
       System.out.println(s3 == s4); // true
       
       System.out.println(s4 == s5); // false
       System.out.println(s4.equals(s5)); // true
       
       System.out.println(s1 == s5); // true
       
       System.out.println(s2.intern() == s3); // true - intern 메서드를 사용하면 상수 풀을 먼저 검사하기 때문에 true
    }

intern() 메서드 사용을 지양해야 하는 이유

  • intern() 메서드는 주로 정확도가 높지만 느린 equals() 메서드가 아닌 빠른 속도의 동등 연산자(==)를 사용하기 위해서 사용한다.

  • intern() 메서드는 C로 만들어진 native 메서드로, Constant Pool에 값을 직접 할당한다.

  • 직접 할당하게 되면 객체를 중복 생성해서 불필요하게 Constant Pool의 메모리를 채우게 된다.

  • 가비지 컬렉팅의 어려움도 겪게 된다.

String이 Immutable(불변)한 이유

1. 문자열 상수 풀(String Constant Pool)을 이용해서 효율적인 메모리 활용

  • 문자열은 참조형(Reference) 데이터이기 때문에 JVM(heap) 영역에 들어간다.

  • 힙 영역에는 문자열만을 특별히 보관하기 위해 문자열 상수 풀(String Constant Pool)이 있는데, 이 곳에서는 문자열을 인스턴스화 시켜서 문자열들이 중복되지 않게 관리한다.

  • 따라서, 문자열 리터럴이 생성될 때마다 JVM은 해당 문자열이 문자열 상수 풀에 존재하는지 확인한다. 문자열 상수 풀에 해당 문자열이 존재하지 않으면, 해당 문자열을 문자열 상수 풀에 저장하고 존재하면 저장하지 않는다.

  • 문자열 상수 풀에 동일한 문자열이 이미 존재하는 경우 새로운 문자열을 생성하지 않으므로 메모리 공간을 절약한다.

  • 문자열 상수 풀은 문자열 캐싱을 사용하므로 JVM은 문자열이 문자열 상수 풀에 존재하는지 빠르게 확인할 수 있습니다.

  • 하지만, new 연산자를 이용해서 new String 객체를 생성하면 그것은 상수 풀에 들어가지 않고 힙 영역에 들어가기 때문에 동일한 내용을 가진 문자열이라도 여러 개 생성이 가능하다.

  • intern() 메서드를 통해 해당 문자열이 문자열 상수 풀에 있는지 확인할 수 있고, 있다면 해당 문자열의 내용을 반환한다.

2. 스레드 안정성

  • Multi-thread 환경에서 여러 스레드가 동시에 접근해도 String은 불변하기 때문에 안전하다.

  • String이 불변해서 여러 스레드가 하나의 String에 접근해도 동기화 문제가 발생하지 않는다.

  • 여러 스레드가 하나의 문자열을 사용하더라도 문자열의 내용은 변하지 않는다.

3. 보안성

  • 문자열은 변하지 않기 때문에 해싱(hash)된 값으로 사용될 수 있다.

  • 사용자 이름, 암호는 데이터베이스 연결을 수신하기 위해 문자열로 전달되는데, 만일 번지수의 문자열 값이 변경이 가능하다면 해커가 참조 값을 변경하여 애플리케이션에 보안 문제를 일으킬 수 있다.


String VS StringBuffer VS StringBuilder

자바에서는 대표적으로 문자열을 다루는 자료형 클래스로 String, StringBuffer, StringBuilder 라는 3가지 자료형을 지원한다.

String (Immutable)

+ 연산자

  • 기존의 문자열에 다른 문자열을 더하면 기존의 문자열에 추가가 되는 것이 아니라 메모리의 주소가 다른 새로운 문자열이 생긴다.

  • JDK 5 이전에는 concat 방식과 동일했다가 JDK 9 부터는 StringBuilder를 사용하도록 바뀌었다.

  • 컴파일 전 내부적으로 StringBuilder 클래스를 만든 후 다시 문자열로 돌려준다.

String a = "hello" + "world";
// 는 아래와 같다.
String b = new StringBuilder("hello").append("world").toString();

/// 반복적인 + 연산자 사용 예시

String a = "";

for(int i = 0; i < 10000; i++) {
    a = a + i;
}

// 이런 짓거리 하면 이렇게 구현하는 것과 같다.

String b = "";

for(int i = 0; i < 10000; i++) {
    b = new StringBuilder(b).append(i).toString();
}

concat

  • 메소드 호출시마다 String 객체를 새로 만들어서 합친 뒤 반환
public static void main(String args[]) {
  String strValue = "ABC";

  // concat() 메서드를 3번 호출했으므로 힙 영역에는 3개의 데이터가 생성된다.
  // 힙 영역에 생성되는 3개의 데이터: "ABCD", "ABCDE", "ABCDEF"
  String strConcatResult = strValue.concat("D").concat("E").concat("F");
}

+ 연산자 VS concat

  • +연산자를 여러 번 호출하더라도 최종 결과만 힙 영역에 생성된다.

  • concat 메소드를 호출할 때마다 힙 영역에 데이터가 생성되기 때문에 메모리를 가장 많이 소모한다.

  • 대부분의 경우에는 concat을 지양하고 +를 사용하면 충분하다.

StringBuffer (Mutable)

  • StringBuffer는 수정이 가능하다.

  • 내부적으로 버퍼(buffer)라고 하는 독립적인 공간을 가지게 되어, 문자열을 바로 추가할 수 있어 공간의 낭비도 없으며 문자열 연산 속도도 매우 빠르다는 특징이 있다.

  • 멀티 스레드 환경에서 동기화를 지원해서 여러 스레드가 동시에 접근해도 안전하다.

  • 동기화 때문에 성능이 조금 떨어진다.

  • Web이나 소켓환경과 같이 비동기로 동작하는 경우가 많을 때는 StringBuffer를 사용하는 것이 안전하다.

  • String에서는 +연산자를 이용해 새로운 객체를 계속 생성하지만, StringBuffer는 append() 메서드를 통해 하나의 String 객체만을 사용한다.

  • StringBuffer에서는 toString() 메서드를 재정의해 StringBuffer 객체의 문자 배열을 String 객체로 생성해 반환한다.

  • String 클래스와 달리 equals 메서드를 재정의하지 않아서 동등 연산자(==)과 같은 결과를 가진다.

    • 따라서 String 클래스의 equals() 메서드를 사용하려면 StringBuffer 객체를 toString() 메서드를 통해 String으로 변환 후 사용해서 비교해야 한다.

StringBuilder (Mutable)

  • StringBuilder는 동기화가 없어서 String 객체를 빠르게 수정할 수 있다.

  • 스레드 안정성을 보장하지 않는다.

/// + 연산자 대신 올바른 사용 예시
final StringBuilder a = new StringBuilder();

for(int i = 0; i < 10000; i++) {
    a.append(i);
}

final String b = a.toString();
  • StringBuffer와 StringBuilder 모두 사용할 때 초기에 미리 메모리의 크기를 할당하는 작업 시간이 소요되기 때문에 String 수정이 빈번히 일어나는 경우가 아니라면 + 연산자를 쓰는게 더 좋다.

빈 문자열

  • 크기가 0인 char형 배열
profile
HeeYun's programming study

0개의 댓글