헷갈리는 자바 문자열 구석구석 이해하기

Soeun Park·2025년 1월 20일
3

자바 공부

목록 보기
1/2
post-thumbnail

🐣 시작하며

안녕하세요! 오늘은 자바 문자열을 주제로 포스팅해보려고 합니다. 먼저 아래 문제들을 살펴보도록 하겠습니다. ✈️🌝

문제 1

String str1 = "abc";
String str2 = "abc";

// 출력 결과는? 
System.out.println(str1==str2);
System.out.println(str1.equals(str2));
System.out.println(str1.hashCode()==str2.hashCode());

문제 2

String str3 = new String("abc");
String str4 = new String("abc");

// 출력 결과는?
System.out.println(str3==str4);
System.out.println(str3.equals(str4));
System.out.println(str3.hashCode()==str4.hashCode());

정답은, 차례대로

## 문제 1
true
true
true

## 문제 2
false
true
true

입니다.


🐰 자바에서 문자열을 만드는 방법

자바에서는 문자열을 크게 두 가지 방식으로 만들 수 있습니다.

1️⃣ String Literal

String usingLiteral = "velog";

두 개의 큰따옴표(" ")로 묶인 텍스트를 String Literal이라고 합니다.

2️⃣ new 연산자

String usingNew = new String("velog");

new 연산자를 사용해 새로운 String 객체를 생성하는 방법입니다.


🏊 String Literal 방식과 Java String Pool

먼저, String Literal 방식부터 살펴보겠습니다.

자바의 힙 메모리 영역에는 Java String Pool(또는 String Constant Pool)이라는 공간이 존재합니다. JVM이 문자열을 효율적으로 관리하기 위해 사용하는 장소인데요!

JVM은 String 변수를 선언하고 String Literal 방식으로 값을 할당할 때, 동일한 값을 가진 문자열이 이미 스트링 풀에 존재하는지 탐색합니다.

String str1 = "abc";
String str2 = "abc";

만약 스트링 풀에 "abc"라는 문자열이 이미 존재한다면, 추가적인 메모리 할당 없이 기존 문자열에 대한 참조값을 반환합니다.

반대로 "abc"라는 문자열이 스트링 풀에 없다면, 이를 새로 추가한 뒤 참조값을 반환합니다.

위의 예시에서 str2에 값을 할당할 때에는 스트링 풀에 이미 "abc"가 존재합니다. 따라서 str2는 str1과 스트링 풀 속 동일한 공간을 가리키게 됩니다.

System.out.println(str1==str2); // true
System.out.println(str1.equals(str2)); // true
System.out.println(str1.hashCode()==str2.hashCode()); // true
}
  1. str1==str2

== 연산자의 경우 두 개의 인스턴스가 메모리 상에서 같은 공간을 참조하고 있는지를 비교합니다. 위의 그림을 통해 확인했듯이, str1과 str2는 스트링 풀 속 동일한 공간을 가리키고 있습니다. 따라서 true가 출력됩니다.

  1. str1.equals(str2)

String은 equals() 메서드를 재정의해 동등 비교가 가능합니다. str1과 str2는 모두 "abc"라는 논리적으로 동일한 문자열을 지칭합니다. 즉, str1과 str2는 동등하다고 판단되기에 true가 출력됩니다.

  1. str1.hashCode()==str2.hashCode()

String은 equals()와 함께 hashCode()도 재정의하고 있습니다. 논리적으로 동일한 인스턴스는 같은 해시 코드를 반환합니다. 따라서 true가 출력됩니다.

참고) String Interning

스트링 풀에 이미 문자열이 존재하는지 확인하고, 재사용하는 작업을 String Interning이라고 합니다. 이를 통해 논리적으로 내용이 같은 문자열은 동일한 메모리 공간에서 공유될 수 있도록 합니다.

String Literal 방식을 사용하면, 내부적으로 Interning을 수행합니다. 이밖에 String.intern() 메서드를 직접 사용할 수도 있습니다.

String str1 = new String("abc");
String str2 = str1.intern();
System.out.println(str1.equals(str2)); // true
System.out.println(str1.hashCode()==str2.hashCode()); // true

🆕 new 연산자 방식

new String() 방식은 언제나 새로운 String 객체를 생성합니다.

new 연산자를 사용해 String 객체를 생성 시, 자바 컴파일러는 새로운 객체를 생성해 힙 메모리에 저장합니다. 이렇게 생성되는 객체는 항상 서로 다른 메모리 영역에 위치하게 됩니다.

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

객체의 주소값을 비교하는 == 연산을 통해 str1과 str2의 메모리 주소가 같은지, 다른지 확인해보도록 하겠습니다!

System.out.println(str3 == str4); // false

false가 출력되었고, 같은 값을 갖더라도 new 연산자로 String 객체를 만들면 다른 메모리 주소를 참조한다는 것을 확인할 수 있습니다.


🙅 String은 불변 객체

Oracle 공식 문서에 따르면,

Strings are constant; their values cannot be changed after they are created.

String 객체는 불변합니다.

String 클래스는 final로 선언되어 있으며, value 및 coder, hash 필드 등으로 구성되어 있습니다.

그 중 value 필드는 byte[] 타입의 배열로, 문자열의 실제 문자 데이터를 저장합니다. 이 필드는 final로 선언되어 있기 때문에, String 객체가 한 번 생성된 이후에는 변경할 수 없습니다.

객체의 실제 메모리 위치(참조)에 따라 해시코드를 반환하는 System.identityHashCode를 사용해 String의 불변성을 직접 확인해보도록 하겠습니다.

String hello = "hello";
System.out.println(System.identityHashCode(hello)); //2060468723
hello = hello + " velog";
System.out.println(System.identityHashCode(hello)); //1933863327
  1. String hello = "hello";

"hello"라는 문자열 리터럴은 스트링 풀에 저장됩니다. hello는 문자열 풀에 있는 "hello" 객체를 참조합니다.

  1. " velog"

" velog"도 문자열 리터럴이므로 스트링 풀에 저장됩니다.

  1. hello + " velog"

문자열 연결 연산자(+)를 통해 새로운 문자열 객체가 힙 메모리에 생성됩니다. 이때는 new 연산자를 사용한 것과 같이 스트링 풀에 저장되지 않습니다.

따라서 위의 코드는 아래와 같이 세 개의 문자열 객체를 생성합니다. 즉, 기존 문자열이 변경되는 것이 아니라 새로운 객체를 만들어 참조하게 됩니다.


🟰 Equals And HashCode

기본적으로, Object 클래스는 equals() 및 hashCode() 메서드를 정의하고 있습니다. 따라서 자바의 모든 클래스는 이 두 메서드를 사용할 수 있습니다.

Object 클래스가 디폴트로 정의한 equals() 메서드는 객체의 동일성을 비교합니다. 즉, 같은 메모리 공간을 참조하고 있는지를 확인합니다. 이를 물리적 동일성(인스턴스의 메모리 주소가 같음)이라고 합니다.

그런데 서로 다른 주소 값을 가지더라도, 내용물이 같아 같은 인스턴스라고 정의하고 싶은 경우가 있습니다. 이를 논리적 동일성(논리적으로 두 인스턴스가 같음)이라고 하며, 동등성이라고도 합니다.

만약 equals() 메서드를 통해 객체의 동등성을 비교하고 싶다면, 해당 메서드를 오버라이드 해주어야 합니다. (동등성 vs 동일성)

String 클래스는 아래처럼 equals() 메서드를 재정의하여 동등성 비교가 가능합니다.

먼저, 비교하려는 객체가 자기 자신과 동일하다면 true를 반환합니다. 두 번째로, 객체가 String 타입인지 확인해 아니라면 false를 반환합니다. 이후 두 문자열의 실제 문자 내용을 비교하여 내용이 같다면 true를 반환합니다.

String 타입의 객체를 동등 비교해보도록 하겠습니다. new 연산자를 사용해 같은 값을 갖는 두 개의 객체를 만들었습니다.

String str3 = new String("abc");
String str4 = new String("abc");

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

str3과 str4는 서로 다른 메모리 공간을 참조하고 있지만, 문자열 내용이 같기 때문에 equals() 비교 결과 동등하다고 판단합니다. 즉, 값으로 비교가 가능해집니다.

자바에서 equals()를 재정의하면, hashCode()도 함께 재정의해주는 것이 관례입니다. 논리적으로 동일한 두 인스턴스는 같은 해시 코드 값을 반환하도록 해야 합니다. 또한 HashMap, HashSet과 같은 Hash 기반의 자료 구조를 사용할 때 hashCode()를 재정의하지 않으면 문제가 발생할 수 있습니다. 따라서 동등성 비교를 위해서는 equals()를 재정의해야 하며, 이때 hashCode()도 함께 재정의하는 것이 권장됩니다.


🏡 StringBuilder와 + 연산자의 차이점은?

StringBuilder 클래스는 내부에 변경 가능한 byte 배열을 가집니다. StringBuilder 클래스는 AbstractStringBuilder 추상클래스를 상속받습니다.

이때 AbstractStringBuilder 추상 클래스는 String 클래스와 달리 byte 배열을 final로 선언하지 않습니다.

따라서 StringBuilder 클래스는 String 클래스와 달리 변경이 가능한 데이터 구조를 가집니다. 문자열을 추가 및 삭제 또는 삽입 시 새로운 객체를 만들지 않고 기존 객체를 활용합니다. 따라서 문자열에 대한 작업을 훨씬 효율적으로 처리합니다.

문자열을 두 가지 방법으로 합쳐보며 테스트를 진행해보도록 하겠습니다.

먼저, StringBuilder를 활용한 예제입니다.

StringBuilder sb = new StringBuilder("hello");
System.out.println(System.identityHashCode(sb));
sb.append(" velog");
System.out.println(System.identityHashCode(sb));

append 연산 전과 후의 StringBuilder 객체는 같은 메모리 공간을 참조하고 있는 것을 확인할 수 있습니다. 즉, 새로운 StringBuilder 객체를 생성하지 않고 내부의 값을 변경했습니다.

실행 결과

이와 반면, + 연산자를 사용해 두 개의 문자열을 합치게 되면 새로운 String 객체가 생성됩니다. 연산 전과 후 참조하고 있는 메모리 주소가 다른 것을 확인할 수 있습니다.

String str = new String("hello");
System.out.println(System.identityHashCode(str));
str += " velog";
System.out.println(System.identityHashCode(str));

문자열에 대한 연산을 할 때마다 새로운 객체를 생성하게 되면 커다란 GC 오버헤드 문제가 발생할 수 있습니다. 이럴 때는 StringBuilder를 사용하는 것을 고려해볼 수 있습니다.

🍊 마치며

자바 공부를 다시 하면서 놓치고 있었던 String과 관련한 개념들을 정리해봤습니다. 자바에서 제공해주는 클래스들을 사용할 때 한 번씩 타고 들어가 살펴보는 습관을 기르려고 합니다 🐰🥕

참고 자료

String - Oracle
Java String Pool - Baeldung
Interning of String in Java - GeeksForGeeks
String Interning - StackOverFlow
String is Immutable - GeeksForGeeks
Equals and HashCode - Baeldung

profile
Backend Developer

2개의 댓글

comment-user-thumbnail
2025년 1월 22일

String에 대해서 대충 알고 넘어갔었는데 자세하게 설명해 주셔서 감사합니다!
동등성과 동일성 개념이 매번 헷갈렸는데 이제는 안까먹을 것 같네요 ㅎㅎ
StringBuffer vs StringBuilder에 대한 내용이 있으면 좋을 것 같아요!!

1개의 답글