[Java] equals()와 == 사이, JVM 오토캐싱이 던지는 진짜 질문
들어가며
자바의 문법을 잘 알고있다고 생각했지만, 예상과는 다른 실행 결과가 나온적이 있다. JVM의 오토캐싱을 잘 몰라서 알고있던 문법과 실제 실행 결과가 다르게 나왔던 것이다. 이에 대해 알아본 내용을 정리해보자
==와 .equals()의 차이를 알고있는가?
자바에서 문자열을 비교할때는 == 가 아닌 .equals()를 사용하라는 소리는 다들 많이 들어봤을 것이다. ==는 객체를 비교하기 때문에 문자열내용이 같아도 false가 나올 수 있다. 반면 .equals()는 값을 비교하기 때문에, 문자열은 .equals()로 비교한다.
그렇다면, 아래 코드의 실행결과는 true인가 false인가?
Integer a = 1;
Integer b = 1;
System.out.println(a == b);
정답
정답 확인
정답은 true 이다.
JVM의 오토캐싱을 알고있다면, 이 코드의 실행결과는 true라고 답했을 것이다.
만약 오토캐싱을 몰랐지만 true라고 했다면, 객체 참조에 대해 오해하고 있을 가능성이 있다.
false라 생각한다면, 코드를 직접 실행해보자. 흥미로운 결과가 기다리고 있을 것이다.
기본 개념 정리
확인 개념
==와.equals()- Wrapper Class
- 오토캐싱
== 와 .equals()
먼저 ==와 .equals()의 차이를 자세히 알아보자.
==
==연산자는 두객체의 참조를 비교한다. 메모리 상에서 같은 객체를 가리키고 있으면 true를 반환한다. 즉 객체가 같아야지 true인 동일성비교이다.
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false (다른 객체니까)
위의 코드는 hello 문자열을 비교하는 코드이다.
위 코드의 실행 결과는 false이다. a와 b는 같은 String 문자열인 hello를 나타내지만, 메모리 상에서는 다른 객체이고, == 비교를 통해 객체 비교를 하기때문에 false가 반환된다.
물론 기본 자료형은 ==이 값을 비교한다.
equals()
그래서 내용(값)이 같은지를 비교하고 싶다면 equals() 메소드를 사용해서 비교해야한다.
equals() 메소드를 오버라이드한 클래스들은 내부 로직에 따라 객체 비교가아닌 내부 값을 비교를 하여 결과를 반환한다. 많은 클래스 (String, Integer)등이 값을 비교하도록 equals() 메소드가 오버라이딩 되어있다.
직접 오버라이드 하여 사용할 수도 있다.
String a = new String("hello");
String b = new String("hello");
System.out.println(a.equals(b)); // true (내용을 비교한다)
Wrapper Class
래퍼 클래스는 자바의 기본 자료형을 객체로 사용 가능하게 하는 클래스이다.
기본형자료에 대한 래퍼 클래스는 아래와 같다.
| 기본형 (primitive) | 래퍼 클래스 (wrapper) |
|---|---|
| int | Integer |
| double | Double |
| char | Character |
| boolean | Boolean |
| long | Long |
| float | Float |
| short | Short |
| byte | Byte |
래퍼 클래스는 아래와 같이 기본 자료형이 못하는 기능을 가능하게 한다.
- 컬렉션은 객체만 저장 가능하다 -> 기본 자료형 대신 래퍼 클래스를 사용
null을 표현할때 -> 기본 자료형은null표현을 못한다.- 제네릭 사용시 -> 제네릭 사용시에도 래퍼 클래스를 사용한다.
Wrapper Class의 오토박싱, 언박싱
자바는 자동적으로 Wrapper Class의 오토박싱과 언박싱을 지원한다.
오토박싱은 기본 자료형의 값을 래퍼 클래스 객체로 자동 변환하는것을 의미하고,
언박싱은 래퍼 클래스 객체를 기본 자료형으로 자동 변환하는것을 의미한다.
자세한 예시를 살펴보자
오토박싱
int num = 10
Integer number = new Integer(10);
Integer, Double 같은 래퍼 클래스도 결국 클래스 이므로, new 키워드를 사용하여 위와같이 객체를 생성해야 한다.
그러나 래퍼 클래스는 오토박싱을 지원하기 때문에 new 키워드 없이도 아래와 같이 기본형 값을 래퍼 객체로 자동 변환한다. 내부적으로 Integer.valueOf(10)를 통하여 객체를 생성한다.
int num = 10
Integer number = num; // 오토박싱: int -> Integer
언박싱
오토박싱과 마찬가지로, 자동적으로 래퍼 클래스 객체의 값을 기본 자료형으로 자동 변환한다.
언박싱이 없다면 아래와 같이 개발자가 값을 직접 꺼내는 코드를 작성해야한다.
Integer a = 10; //오토 박싱
int b = a.intValue(); //메소드를 통해 값을 직접 꺼냄
언박싱은 아래 코드와 같이 자동으로 값을 변환한다. 내부적으로 a.intValue()를 통하여 값을 가져온다.
Integer a = 10; //오토박싱
int b = a; //언박싱
JVM의 오토캐싱
오토 캐싱은 JVM이 성능 최적화를 위해 사용하는 기법이다. 내부적으로는 자주 사용되는 값을 미리 만들어두고, 재사용하며 래퍼 클래스에서 사용된다.
대표적으로 Integer클래스에서는, -128 ~ 127 범위의 정수 값을, JVM이 미리 객체를 생성하여 놓고, 동일한 값은 재사용한다.
이 범위 안의 값은 오토 박싱이 일어날때, 항상 미리 생성된 같은 객체를 사용하게 된다.
예를들어, 위에서 살펴보았던 코드는
Integer a = 1;
Integer b = 1;
System.out.println(a == b); //true
1이라는 값은 -128 ~ 127범위 내부의 값이므로, a, b둘다 같은 객체를 참조하게 되어 true를 반환한다.
아래의 코드는 300이라는 값이 -128 ~ 127범위 외부이므로 false를 반환한다.
Integer a = 300;
Integer b = 300;
System.out.println(a == b); //false
래퍼클래스의 오토박싱은 위에서 설명한 valueOf() 메소드를 통해 작동한다.
Integer클래스의 공식 래퍼런스를 보면 valueOf()메소드는 아래와 같이 정의되어 있다.
public static Integer valueOf(int i) {
if (i >= -128 && i <= 127) { // 캐시 범위
return IntegerCache.cache[i + 128];
}
return new Integer(i);
}
IntegerChache라는 내부 클래스는 -128 ~ 127범위의 Integer 객체 배열을 미리 생성(캐싱)한다.
오토박싱이 이루어질 때, valueOf()메소드의 동작은 매개변수 i가 캐시 범위 내부라면 캐싱된 값을 반환하고, 캐시 범위 외부의 값이라면 new 키워드를 통해 새로운 Integer 객체를 반환한다.
즉, 오토 캐싱의 내부 동작을 통해 범위 내부의 값은 매번 같은 객체를, 범위 내부의 값은 매번 다른 객체를 반환하는것이다.
이로 인해, 동일한 문법의 코드이지만, 값만 바뀌어도 다른 결과가 나올 수 있는것이다.
결론
값을 비교할때는 equals(), 객체를 비교할때는 ==를 사용하라고 많이들 말한다.
==비교는 다른 객체를 비교하면 값이 같아도 false를 반환하기 때문이다.
String에서 문자열을 비교할때 적용하는 내용이라고 생각했다.
그런데, 래퍼클래스에서 다른 객체를 ==으로 비교하였을때 어떤것은 true, 어떤것은 false가 반환되는것을 보고 더 깊이 공부하게 되었다.
그 결과, 래퍼 클래스의 오토박싱 과정을 더 자세히 알 수 있었고, 그 내부에서는 오토 캐싱이 일어난다는것을 알게 된것이다.
==비교가 오토캐싱으로 인해서 실행 결과가 달라질 수 있기 때문에, String뿐만 아니라, 다른 래퍼클래스에서도 값 비교는 반드시 equals()를 사용해야한다는 점을 다시 확인하게 되었다.
추가로 아래의 코드는 Integer의 == 비교가 오토캐싱으로 인해 달라지는 예시다.
아래 결과를 참고하여, ==비교는 래퍼클래스에서도 주의해야한다는 점을 명심하자.
// == 으로 키를 비교하는 잘못된 메소드
public static boolean findValue(HashMap<Integer, String> map, Integer key) {
for (Integer k : map.keySet()) {
if (k == key) { // ❌ (잘못됨)
return true;
}
}
return false;
}
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
Integer key1 = 129; // 오토캐싱 범위 밖
Integer key2 = 129;
//Integer key1 = 125; // 오토캐싱 범위 내
//Integer key2 = 125;
map.put(key1, "Value");
if (findValue(map, key2)) {
System.out.println("키를 찾음");
} else {
System.out.println("키를 못 찾음");
}
}
키 값을 캐싱범위 내부의 125와, 캐싱범위 외부의 129로 바꾸어서 실행결과를 확인해보자.
두개가 다른 실행결과가 나올 것이다.
댓글남기기