스터디를 진행하다가 내가알고있는 String 문자열 비교하는 부분에서 정확히 알고 넘어가지 못했다는 것을 알게 되었다.
메모리 구조상으로 이렇게 그림이 그려지는 것은 대충 알고 있으나, 실제 코드상에서는 주소값이 어떻게 반환될 지도 궁금하게 되었다.
그래서 먼저 String에 대해 파헤쳐보겠다.
또한, String에서 동등성, 동일성 비교를 하며 차이점을 명확히 알아보겠다.
그리고 equals()를 할 때, 꼭 필요한 hashCode() 메서드는 어떠한 값을 반환하고 있는지 알아보고, 다른 객체일 때에 참조값으로 hashCode()메서드를 사용하면 어떻게 될 지 알아보자!
Java에서 String은 문자열을 위한 클래스이다.
일단, String의 특징에 대해 알아보자.
String은 객체이다.String의 특징은 int와 boolean과 같은 기본자료형(primitive type)이 아니고 참조자료형(reference type)으로 분류 된다.
즉, 스택(stack)영역이 아닌 힙(heap)영역에서 문자열 데이터가 생성되고 다뤄진다. 한 마디로 String은 객체이다.
아래 코드와 메모리 영역을 그림을 참고하자.
int age = 20;
String name = "보라보라";

그러므로 String같은 참조자료형은 GC의 대상이 되어진다.
String은 불변(Immutable)하다.또, String의 특징에는 불변 객체(immutable object)이다.
즉, 인스턴스가 한 번 생성되면 그 값을 읽기만 할 수 있고, 그 값은 변경할 수 없는 객체이다.
아래의 코드를 보면, String형인 str 변수에 "보라보라" 를 담았다. 그리고 +연산자를 이용하여 " World" 도 담으려고 한다. 메모리 구조의 그림을 참고하자.
String str = "보라보라";
str = str + " World";
System.out.println(str); // 보라보라 World

기존에 str을 참조하고 있던 메모리 주소 영역의 참조를 끊고, 새로운 메모리에 "보라보라 World" 가 담고, str에는 새로운 참조값이 할당 되었다.
즉, 기존 메모리 공간에 값을 덮어씌운 것이 아닌 새로운 영역에 값을 담게 되는 그림을 볼 수 있다.
아래와 같은 세가지 특징으로 인해 취할 수 있는 이득이 있다고 한다.
String Constant Pool 이라는 독립적인 영역을 만들고 문자열들을 Constant화 하여 다른 변수 혹은 객체들과 공휴하게 됨.// 1. 리터럴을 이용한 방식
String str1 = "보라보라";
String str2 = "보라보라";
// 2. new 연산자를 이용한 방식
String str3 = new String("보라보라");
String str4 = new String("보라보라");
위의 코드 모두 "보라보라" 라는 문자열 값을 저장한다는 점에서는 차이가 없지만, JVM 메모리 내부적인 측면에서는 큰 차이 존재한다. 위의 코드를 그림으로 표현해 보았다.

Heap영역 안에 String Constant pool이라는 영역에 존재하게 된다.String Constant pool에서 재사용하여 메모리를 절약할 수 있다는 특징이 있다.str3은 new를 이용하여 생성하였다.Heap영역 안에 다른 메모리 주소를 가리키고 있다.str3같은 경우에는 String Constant pool안에 위치하지 않고 새로운 객체로 생성되었다.new를 이용하여 생성하였다.Heap영역 안에 다른 메모리 주소를 가리키고 있다.String Constant pool안에 위치하지 않고 새로운 객체로 생성되었다. 전형적인 참조자료형의 모습이다.new를 이용할 때에는 메모리 낭비가 심할 것으로 보인다.메모리 구조의 그림으로는 이러한 위와 같은 차이가 있다는 것을 알았다. 하지만, 코드로써 값을 비교하고 주소 값을 비교하고 싶다는 생각이 들었다.
== 비교와 equals()비교 의 차이점 위의 str1, str2, str3, str4을 선언한 변수들을 다시 아래에 코드로 보여준다. 그리고 이 아래의 변수들을 비교해 보겠다.
// 1. 리터럴을 이용한 방식
String str1 = "보라보라";
String str2 = "보라보라";
// 2. new 연산자를 이용한 방식
String str3 = new String("보라보라");
String str4 = new String("보라보라");
==System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str3 == str4)); // false
str1 == str2 리터럴 문자열 비교 같은 경우에는

String Constant Pool에 있는 객체값을 바라보고 있기 때문에 참조하고 있는 주소값이 같아 true가 됨을 알 수 있다.
반면에, str1 == str3, str3 == str4 같은 경우에는

"보라보라" 로 같은 값이지만 이 값들은 Heap메모리에서 서로 다른 메모리 영역에 만들어져 있기 때문에 주소값이 달라 false를 반환하게 된다.
equals()System.out.println(str1.equals(str2)); // true
System.out.println(str1.equals(str3)); // true
System.out.println(str3.equals(str4)); // true
모두 "보라보라" 라는 같은 내용을 변수에 담고있으니 equals()메소드를 이용하여 true를 반환하게 된다.
"자 근데 그림상으로 이렇게 다른 것 알겠어요. 근데 정말로 보여줄 수 없어요? 정말 그림그대로인지... 알고싶어요."
라고 스터디원의 질문이 들어왔다!!!!!!!!!!!!!!
"우리 주소값을 찍어볼까요? 주소를 어떻게 알죠?"
이렇게 오합지졸의 여행이 시작되었다.
모든 클래스의 부모인 Object에서는 toString()메소드를 사용하면
{className}@{(hashCode}가 반환된 것이 생각이 났다.
System.out.println(str1.toString()); // 보라보라
System.out.println(str2.toString()); // 보라보라
System.out.println(str3.toString()); // 보라보라
System.out.println(str4.toString()); // 보라보라
결론은.... 오잉? "보라보라" 천국이 되었다. 그냥 String변수를 찍는것과 다를바 없었다...
뭔가 주소값이라 하니 hashCode()메소드를 사용하면 될것이야!!! 라고 착각을 하며 결과물을 확인해 보았다.
System.out.println(str1.hashCode()); // 2080
System.out.println(str2.hashCode()); // 2080
System.out.println(str3.hashCode()); // 2080
System.out.println(str4.hashCode()); // 2080
아니... hashCode가 객체의 내부 주소를 해시 값으로 변환하여 반환하는 것이 아니었나? 그럼 str3, str4는 다른 값이 나와야 하는데... 왓왓?
아니 근데?
equals()는 재정의 하지 않으면 동일성 비교가 되지 않을 텐데...
hashCode()는 동일성 비교 할때 같이 오버라이딩 해주라 했는데?
toString()은 또 왜 그대로 문자열을 출력하는건데?
난 따로 구현하지 않았는데 왜 되는거야? 😅😅😅😅😅😅😅😅😅
String 클래스를 뜯어봐야 겠는걸??!
toString() : 자기 자신을 그대로 반환한다.public String toString() {
return this;
}
equals() : String VS ObjectObject : 단순 동일성을 비교하고 있다. 결국 == 비교하고 있다.
public boolean equals(Object obj) {
return (this == obj);
}
String : 오버라이딩 하여 동등성 비교를 재정의 하여 구현되었다.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value);
}
hashCode() : 문자열에서 한 글자씩 가져와서 정수값으로 변경hashCode() 메소드에 대한 내용은 대략 같은 문자열의 값을 유일한 조합이 나오게 계산을 한다. 하지만 String에서는 중복 되는 값이 나올 수 있었던 것이다. public int hashCode() {
int h = hash;
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true;
} else {
hash = h;
}
}
return h;
}
아니 그럼.... 뭘로 객체가 다른 주소값인걸 확인해야하는고닝?
객체의 고유한 hashCode를 리턴하는 메소드가 있다고 한다.
public static native int identityHashCode(Object x)
이 메소드 같은 경우에 재정의 할 수 없고, 객체의 내부 주소를 기반으로 해시 값을 반환 한다고 한다.
그럼 위의 str1, str2, str3, str4변수들의 해시 값을 비교한다.
System.out.println(System.identityHashCode(str1)); // 168423058
System.out.println(System.identityHashCode(str2)); // 168423058
System.out.println(System.identityHashCode(str3)); // 821270929
System.out.println(System.identityHashCode(str4)); // 1160460865
내가 생각했던 그림과 같이 str1 == str2 처럼 두 변수는 같은 참조 값을 가지고 있다.
하지만 str3과 str4는 다른 참조 값을 가지고 있음을 확인할 수 있게 되었다.
스터디를 하면서 서로 꼬리 질문을 하며
당연히 인지하고 있다고 생각했던 String의 세계에 빠졌다.
일단 String의 특징을 알아보았다. 그 다음에 이어서 당연히 우리가 알고있다고 생각한 질문들을 이어갔다.
==과 equals의 차이는?
그럼 왜 이런 차이가 나는지 참조하고 있는 주소를 알아볼까?
그렇게 해서 System에서 identityHashCode()메서드로 어느 주소를 가르키고 있는지 그림과 함께 비교하며 이해할 수 있게 되었다.
[참조]
블로그 - 자바 String 타입 특징 이해하기 (String Pool & 문자열 비교)
블로그 - [Java] 동일성(identity)과 동등성(equality)
블로그 - Equals()와 Hashcode()를 override 해야하는 이유
너무 정리가 잘 되어있어서 잘 보고 갑니다. 감사합니다