String a = "abc"; 와 String b = new String("abc"); 의 메모리상 차이를 String Constant Pool과 연관 지어 설명해주세요.
먼저 String 클래스는 final 로 선언되며 불변(immutable) 하다는 특징이 있습니다. String Constant Pool과 같은 공유 메커니즘을 안전하고 효율적으로 활용하기 위해서입니다.
한번 생성된 String 객체의 내용은 절대 변하지 않습니다. 이 불변성 덕분에 JVM은 문자열들을 안전하게 공유하고 재사용할 수 있습니다. SCP는 자바 7부터 PermGen 영역이 아닌 Heap 영역으로 이동했습니다.
String a = "abc"; 리터럴 할당 동작 과정
JVM은 Heap 내의 String Constant Pool(SCP)에 "abc"라는 값을 가진 String 인스턴스가 이미 존재하는지 확인합니다.
존재하는 경우 - SCP에 있는 기존 인스턴스의 주소를 변수 a에 할당합니다. 새로운 객체를 만들지 않습니다.
존재하지 않는 경우 - SCP에 "abc" 값을 가진 인스턴스를 새로 생성하고, 그 주소를 변수 a에 할당합니다.
String b = new String("abc"); 생성자 호출의 상세 동작 과정
이 방식은 new 키워드의 특성상 항상 새로운 객체를 생성합니다.
new 키워드는 무조건 Heap 영역에 새로운 String 객체를 생성합니다. 이 객체는 SCP와는 관련이 없는, 완전히 별개의 객체입니다.
이와 별개로 생성자의 인자로 사용된 리터럴 "abc"는 1번 과정(리터럴 할당)과 동일한 규칙에 따라 SCP에서 확인 후 없으면 생성된다.
결과적으로 변수 b는 SCP에 있는 객체가 아닌, Heap 에 새로 생성된 객체의 주소를 가리키게 된다.
a와 c는 리터럴 방식으로 생성되었으므로, SCP에 있는 동일한 0x100 주소의 객체를 가리킵니다. 따라서 a == c는 true입니다.
b는 new 키워드로 생성되었으므로, Heap에 별도의 객체(0x200)가 생성됩니다. 따라서 a == b는 false입니다.
하지만 a.equals(b)는 두 객체의 내용(값)이 "abc"로 동일하므로 true를 반환합니다.
"두 방식의 가장 큰 차이는 메모리 관리 효율성에 있습니다.
String a = "abc";와 같이 리터럴을 사용하는 방식은 JVM의 String Constant Pool을 통해 이미
존재하는 문자열이 있다면 그 객체를 재사용합니다. 따라서 같은 내용의 문자열을 여러 번
선언해도 단 하나의 객체만 존재하게 되어 메모리를 매우 효율적으로 사용할 수 있습니다.
반면, String b = new String("abc");는 new 키워드로 인해 호출될 때마다 Heap 영역에 항상
새로운 객체를 만듭니다. 이 때문에 같은 내용의 문자열이라도 == 비교 시 false가 나오며,
불필요한 객체 생성으로 메모리 낭비와 GC의 부담을 줄 수 있습니다.
따라서 특별한 이유가 없다면, 항상 리터럴 방식을 사용하여 String을 생성하는 것이
바람직합니다."
JVM이 "abc"라는 문자열 리터럴을 만났을 때, SCP에 이 문자열이 이미 존재하는지 확인하는 과정은 다음과 같은 2단계로 이루어진다.
해시 코드로 버킷(Bucket) 위치를 빠르게 찾기
JVM은 "abc"라는 문자열의 내용(각각의 char 값)을 기반으로 내부적인 해시 함수를 사용해 해시 코드(hash code)를 계산합니다.
계산된 해시 코드를 사용하여 SCP라는 내부 해시 테이블에서 해당 문자열이 저장된 버킷 위치를 즉시 찾아낸다. O(1)
그런데 이로는 충분하지 않다. 버킷 안의 실제 문자열 내용이 다를 수 있기 때문이다. 다른 데 해시코드는 같을 수 있다. 따라서 일치하는 문자열인지 검사한다.
일치하는 문자열을 찾으면: "아, 이미 있구나!" 라고 판단하고, 그 문자열 객체의 주소를 반환합니다.
버킷을 다 찾아봤는데 일치하는 문자열이 없으면: "아, 이건 처음 보는 문자열이네!" 라고 판단하고, 새로운 String 객체를 SCP의 해당 버킷에 저장한 뒤 그 주소를 반환합니다.
이를 String.intern() 이 한다 -> SCP에 문자열이 있는지 확인하고, 없으면 넣고, 주소를 반환하는 메서드
String 클래스가 왜 final로 선언되었을까요? 만약 String을 상속받아 내용을 변경할 수 있다면 어떤 보안 문제가 발생할 수 있을까요? equals()와 hashCode()의 관계에 대한 Java의 규약(Contract)은 무엇이며, 이 규약이 깨졌을 때 HashMap은 어떻게 오작동하는지 설명해주세요.
HashMap은 Java에서 가장 널리 쓰이는 자료구조 중 하나이며, String은 HashMap의 키로 가장 많이 사용되는 클래스입니다.
HashMap이 정상적으로 동작하기 위한 절대적인 전제 조건이 있습니다.
-> "Key"로 사용되는 객체는, Map에 저장된 이후에 상태가 변해서는 안 된다."
만약 Key의 상태가 변하면 , hashCode() 값이 달라질 수 있고, 그렇게 되면 Map 안에서 해당 Key-Value 쌍을 찾지 못하게 된다.
"네, 두 질문은 매우 밀접하게 연관되어 있습니다. HashMap과 같은 해시 기반 컬렉션이
안정적으로 동작하려면, 키로 사용되는 객체가 두 가지 특성을 만족해야 합니다. 첫째,
equals()와 hashCode()의 규약을 올바르게 지켜야 하고,
논리적으로 동등한 객체(equals()가 true)는 반드시 동일한 hashCode()를 반환해야 한다는 규약을 지켜야, 해시 기반 컬렉션이 정상 동작한다는 것입니다.
연결 지점: 이 규약은 HashMap의 키가 올바른 위치를 찾아가고, 그 위치에서 정확한 값을 찾아내기 위한 '검색 알고리즘의 규칙' 입니다.
둘째, Map에 저장된 이후에 상태가 변하지 않는 불변성을 가져야 합니다. String 클래스는 equals()와 hashCode()를 완벽하게 구현하고 있으며, 동시에 클래스 자체가 final로 선언되어 상속을 통한 가변성을 원천 차단함으로써 불변성을 보장합니다. 이 두 가지 특성 덕분에 String은 HashMap의 키로서 가장 이상적이고 신뢰할 수 있는 선택지가 됩니다."
hashCode(), identityHashCode()?
모든 Java 클래스는 Object 클래스를 암묵적으로 상속 받는다. 그리고 Object 클래스에 정의된 hashCode() 메서드는 오버라이딩 되지 않은 기본 상태이다.
Object.hashCode() 의 동작 : 이 기본 메서드는 각 객체의 내부 메모리 주소를 어떤 정수 값으로 변환하여 반환하도록 구현되어 있다. (정확히 주소 값 그 자체는 아지만, 주소를 기반으로 한 고유한 값)
결과 : 따라서 , 어떤 클래스를 만들고 hashCode()를 오버라이딩하지 않으면, 그 클래스의 인스턴스들은 new로 생성될 때마다 서로 다른 메모리 주소를 가지므로, hashCode()를 호출하면 모두 다른 값이 나온다.
이것이 바로 System.identityHashCode()가 하는 일과 사실상 동일합니다. identityHashCode()는 hashCode()가 오버라이딩되었든 아니든 무시하고, 항상 이 '기본' 방식의 해시 코드를 반환해주는 특별한 메서드인 셈입니다.
class MyObject {
// hashCode()를 오버라이딩하지 않음
}
MyObject obj1 = new MyObject();
MyObject obj2 = new MyObject();
// obj1.hashCode()와 obj2.hashCode()는 서로 다른 값을 가짐
// 이는 System.identityHashCode(obj1)과 System.identityHashCode(obj2)가 다른 것과
같은 원리
변화의 시작: hashCode() 오버라이딩
우리가 String, Integer 또는 직접 만든 Person 클래스에서 hashCode()를 오버라이딩하는 순간, 이 기본 동작 방식은 완전히 새로운 로직으로 대체됩니다.
오버라이딩된 hashCode()의 동작: 이제 hashCode()는 더 이상 메모리 주소를 쳐다보지 않습니다. 대신, 우리가 정의한 로직에 따라 객체의 필드(내용, 상태)를 조합하여 해시 코드를 계산합니다.
String의 예: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 와 같은 공식을 사용하여 문자열의 내용으로부터 해시 코드를 생성합니다.
Person의 예: Objects.hash(id, name) 과 같이 id와 name 필드를 조합하여 해시 코드를 생성합니다.
class Person {
private int id;
private String name;
// 생성자...
@Override
public boolean equals(Object o) { /* id와 name으로 비교 */ }
@Override
public int hashCode() {
// 이제 메모리 주소가 아닌, id와 name 필드를 기반으로 해시코드를 생성!
return Objects.hash(id, name);
}
}
Person p1 = new Person(1, "홍길동");
Person p2 = new Person(1, "홍길동");
// p1.hashCode()와 p2.hashCode()는 이제 같은 값을 가짐!
// 하지만 System.identityHashCode(p1)과 System.identityHashCode(p2)는 여전히
다름
10 public int hashCode() { 11 // 이제 메모리 주소가 아닌, id와 name 필드를 기반으로 해시코드를 생성! 12 return Objects.hash(id, name); 13 } 14 } 15 16 Person p1 = new Person(1, "홍길동"); 17 Person p2 = new Person(1, "홍길동"); 18 19 // p1.hashCode()와 p2.hashCode()는 이제 같은 값을 가짐! 20 // 하지만 System.identityHashCode(p1)과 System.identityHashCode(p2)는 여전히 다름
==와 equals()의 차이점은 무엇인가요? Integer 타입을 비교할 때 주의해야 할 점은 무엇인가요?
자바에서 동일하다가 말하는 것은 같은 인스턴스를 참조하고 있을 때이다. == 비교는 이러한 인스턴스 참조 비교를 할 수 있다. equals() 비교는 동등성을 비교할 때 사용한다. 다른 참조여도 equals()를 통해 동등할 수 있다.
다만 동일한 인스턴스는 항상 동등하다. 같은 인스턴스를 가리키니까
Integer 비교시 JAVA에서는 내부적으로 값을 캐싱한다.
-128~127 범위의 숫자는 같은 객체를 재사용한다.
📌 예제 3: 오토박싱 & 캐싱
Integer x =100;Integer y =100;System.out.println(x == y); // ✅ true (캐싱 범위 내, 같은 객체)System.out.println(x.equals(y)); // ✅ true
📌 예제 4: 캐싱 범위 밖
Integer x =128;Integer y =128;System.out.println(x == y); // ❌ false (다른 객체)System.out.println(x.equals(y)); // ✅ true
Object 클래스에 wait(), notify()가 있는 이유는 무엇이라고 생각하시나요? Thread 클래스가 아닌 Object에 있는 이유는 무엇일까요?
volatile 키워드의 두 가지 주요 기능(가시성, 순서성)에 대해 설명하고, synchronized와의 차이점을 설명해주세요.
ThreadLocal은 어떤 원리로 동작하며, 어떤 경우에 사용해야 할까요? 잘못 사용했을 때 발생할 수 있는 메모리 누수 문제에 대해 설명해주세요.
GC의 기본 원리를 설명하고, 'Stop-the-World'가 왜 필수적인지 설명해주세요.
G1 GC는 기존의 CMS GC와 비교하여 어떤 점이 개선되었으며, 'Region'이라는 개념은 왜 도입되었을까요?
InterruptedException이 왜 체크 예외(Checked Exception)라고 생각하시나요? 이 예외를 catch했을 때 단순히 e.printStackTrace()만 호출하면 왜 안 되는지 설명해주세요.
Java의 타입 소거(Type Erasure)란 무엇이며, 이로 인해 제네릭(Generics)을 사용할 때 어떤 한계가 발생하나요?
new 키워드를 사용하지 않고 객체를 생성하는 방법들을 설명하고, 각각의 장단점을 비교해주세요.
try-with-resources 구문이 기존의 try-catch-finally와 비교하여 가지는 장점은 무엇이며, 어떤 원리로 동작하나요? (AutoCloseable 인터페이스)
interface에 default 메서드가 도입된 이유는 무엇이며, 이로 인해 발생할 수 있는 다이아몬드 문제를 Java는 어떻게 해결하나요?
WeakReference, SoftReference, PhantomReference의 차이점은 무엇이며, 각각 어떤 용도로 사용될 수 있을까요?
JIT 컴파일러란 무엇이며, JVM이 왜 처음부터 모든 코드를 컴파일하지 않고 인터프리터 방식을 혼용하는지 설명해주세요.
클래스 로더의 위임 모델(Delegation Model)에 대해 설명하고, 이 모델이 가지는 이점은 무엇인가요?
StackOverflowError와 OutOfMemoryError의 차이점은 무엇이며, 각각 어떤 상황에서 발생하나요?
Java의 메모리 모델(JMM)에서 happens-before 관계란 무엇이며, 왜 중요한가요?
Reflection API는 무엇이며, 어떤 경우에 사용될 수 있을까요? Spring 프레임워크는 Reflection을 어떻게 활용하고 있을까요?
직렬화(Serialization)와 역직렬화는 무엇이며, serialVersionUID의 역할은 무엇인가요?
CompletableFuture는 기존의 Future와 비교해서 어떤 장점이 있나요?
Stream의 lazy evaluation(지연 평가) 특성에 대해 설명하고, 이로 인해 얻을 수 있는 성능상의 이점은 무엇인가요?
Optional를 사용하는 주된 이유는 무엇이며, Optional을 필드로 선언하는 것을 지양해야 하는 이유는 무엇일까요?
어노테이션(Annotation)과 어노테이션 프로세서(Annotation Processor)의 동작 원리에 대해 설명해주세요.
invokedynamic 바이트코드 명령어는 왜 도입되었으며, 람다 표현식이 이와 어떻게 관련되어 있는지 설명해주세요.
final, finally, finalize의 차이점을 각각 설명해주세요.
Java의 기본 타입(Primitive Type)이 있는데도 래퍼 클래스(Wrapper Class)가 필요한 이유는 무엇인가요? (오토박싱/언박싱 포함)
public 클래스에 public static void main(String[] args) 메서드가 있어야 프로그램이 실행되는 이유를 JVM의 관점에서 설명해주세요.
abstract class와 interface의 차이점은 무엇이며, 각각 어떤 상황에서 사용하는 것이 적합한가요?
checked exception과 unchecked exception의 차이점은 무엇이며, Spring의 @Transactional은 기본적으로 어떤 예외에 대해 롤백을 수행하나요?
StringBuilder와 StringBuffer의 차이점은 무엇이며, String과의 성능 차이는 왜 발생하는지 설명해주세요.
제네릭에서 <? extends T>와 <? super T> (와일드카드)의 차이점은 무엇이며, 각각 어떤 경우에 사용해야 하나요? (PECS 원칙)
static 키워드의 의미를 설명하고, static 변수와 static 메서드가 메모리에 어떻게 로드되고 사용되는지 설명해주세요.
Java 9부터 도입된 모듈 시스템(Project Jigsaw)의 목적은 무엇인가요? 기존의 JAR 파일 방식과 비교하여 어떤 점이 개선되었나요?
레코드(Record) 타입이 Java 14에 도입된 이유는 무엇이며, 기존의 클래스(DTO)와 비교하여 어떤 장점이 있나요?
instanceof 연산자의 동작 원리에 대해 설명해주세요.
clone() 메서드의 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)의 차이점을 설명해주세요.