//부모 클래스(사람)
class Human{
String name; // 2개의 필드
int age;
void eat() {...} //2개의 메소드
void sleep() {...}
}
//자식 클래스(대학생)
class Student extends Human{
int studentID; //1개 필드
void goToSchool() {...} //1개 메소드
}
//자식 클래스(직장인)
class Worker extends Human{
int workerID; //1개 필드
void goToWork() {...} //1개 메소드
}
총 대학생 필드 3개, 메서드 3개
총 직장인 필드 3개, 메서드 3개
기본적으로 부모의 모든 필드, 메서드를 내 클래스에서 사용할 수 있는 것
하나의 객체를 여러가지 이름으로 부를 수 있는 성질로, 상속관계(부모-자식 관계)에서 예제에서 볼 수 있듯이 A 타입을 선언했지만 A 뿐만 아니라 B, C, D로 부터 생성 가능
B 타입을 선언했지만 B 뿐만 아니라 A라고도 부를 수 있음.
Human을 부모클래스로 상속해 student, worker를 자식클래스로 작성하면 1개의 객체를 여러가지 모양으로 표현할 수 있는 특징인 다형성을 이용할 수 있다.
상속을 표현할 때 화살표가 부모클래스로 향할 수 있는 이유가 다형성이기에 상속 구조도에서 화살표 표현을 할 수 있다. 이때, 자식 클래스의 공통적인 성분을 뽑아 부모 클래스에서 정의했음으로 반대 방향은 성립이 안된다.
상속 방향이 a<-b<-c/ b<-d이다.
이때, b type 다형성 정의에서 화살표 방향이 반대인 a는 b이다가 안되고, 마찬가지로 a는 c이다, b는 c이다, d는 c이다가 안되는 이유이다.
기본 자료형에서 int 형(값의 범위가 좁음) → double 형(값의 범위가 넓음)
상속관계(부모-자식 관계)에서 자식 → 부모
업캐스팅의 경우 명시하지 않을 시, 컴파일러에 자동으로 업캐스팅해준다.
기본 자료형에서 double 형(값의 범위가 넓음) → int 형(값의 범위가 좁음)
상속관계(부모-자식 관계)에서 부모 → 자식
// 상속관계
class A{}
class B extends A{}
class C extends B{}
// 캐스팅
A a = new B()
자바에서 캐스팅은 등호(=)의 오른쪽을 대상으로!
왜?
왼쪽의 a는 A 타입으로 명시해놨으니 형변환이 불가, 따라서 오른쪽을 캐스팅해야!
이때 B는 업캐스팅임(자식 → 부모)
업캐스팅의 경우 컴파일러가 자동으로~
다운캐스팅의 경우 바꾸려는 형을 반드시 변수 앞에 써주어야 하는데, 위의 경우 b type으로 다운캐스팅하려고 type b를 명시적으로 써주었음에도 에러가 난다.(ClassCastException)
이유는 변수 aa는 A()생성자로 만든 A 타입이기 때문에 B 타입으로 다운캐스팅이 이뤄질 수 없기 때문이다.
그러나 B() 생성자로 만든 A 타입은 B 타입으로 다운캐스팅이 가능하다. 객체 자체가 B 타입으로 만들어져 있기 때문이다.
따라서,
캐스팅의 가능 여부는 무슨 타입으로 선언돼 있는지 보다 어떤 생성자로 생성됐는지에 있는 것이다.
b 타입 및 B() 생성자로 만든 B bb = new B()
의 경우,
B() 생성자로 생성했으므로 힙 메모리에는 A 객체를 감싸고 있는 B 객체가 만들어질 것이다!
A 객체 내부에는 m와 abc()가 있고, B 객체에는 추가로 n와 bcd()가 있다. 결국 B 객체 내에 m, n, abc(), bcd()가 있는 형태이다. 참조 변수가 B 타입으로 선언돼 있으므로 참조 변수 b는 B 객체를 가르키게 되고, 이때 참조변수를 통해 2개의 필드&2개의 메서드를 사용할 수 있다.
A 타입 및 B() 생성자로 만든 A ab = new B()
의 경우,
B() 생성자로 객체를 생성한 것은 동일하되 참조 변수가 A 타입이므로 실제로 힙 메모리에 B 객체가 있더라도 참조변수 ab는 A 객체만을 가리킬 것이다.
따라서 m과 abc()만 사용가능
선언 타입에 따른 차이점 존재, 선언한 타입의 멤버일 때 사용 가능.
상속구조가 복잡할 때 참조변수에 instanceof
연산자 사용했을 때 true일때
참조변수 instanceof 클래스
실제 객체를 어떤 생성자로 만들었는지 클래스 사이의 상속관계를 파악하기가 어려울 때가 있다. 이때, 캐스팅 가능여부를 확인할 수 있는 instanceof
를 이용해 true라면 캐스팅 가능, false라면 캐스팅이 불가능한 걸 알아낼 수 있다.
왼쪽 피연산자인 참조변수가 오른쪽 피연산자인 클래스으로부터 생성되었다면 true를 반환 아니면 false를 반환.
즉, 반환결과는 참조변수가 클래스 type으로의 cast availability(가능 여부) 이기도 함.
상속했을 때 나타나는 특징으로, 부모클래스에서 상속받은 메서드를 재정의(이름을 바꾸거나, 기능을 추가 등)하여 사용하는 걸 메서드 오버라이딩이라고 한다.
즉, over(위)에다 riding(올라탄다)라는 뜻으로 덮어쓰기 같은 개념이다.
예를 들어, 디렉토리 내 a.jpg 파일이 있는데, 이를 수정한 a.jpg 파일으로 덮어쓰려고 한다. 이때 파일 이름과 확장명이 똑같이 동일해야 파일을 덮어쓸 수 있듯이 시그니처(메서드이름, 매개변수)와 시그니처에는 포함되지 않지만 시그니처가 동일한데 다른 리턴타입이 있을 수 없기 때문에 리턴타입까지 동일해야 덮어쓸 수 있다.
시그니처
void abc(int a, double b){ ... }
이때 abc(메서드이름) & int a, double b(매개변수)를 메서드를 구분 짓는 요소로 시그니처라고 한다.
둘다 부모 클래스로부터 상속받은 후 일어나는 결과이지만 메서드 오버라이딩은 시그니처(메소드 이름&매개변수)와 리턴타입이 모두 동일해 덮어쓰기가 되는 것이고 메서드 오버로딩은 시그니처와 리턴타입이 부모 메서드와 (하나 이상) 다른 것이다.
예를 들어, 폴더 내 a.jpg 파일이 있는데 수정하고 저장하려고 보니 a.jpg 파일이 있어 덮어쓰기가 되는 것이(메서드 오버라이딩), a.jpg가 있는 폴더에 a.bmp처럼 확장명이 다른 것을 저장하는 것(메서드 오버로딩)
오버라이딩 시 자식클래스의 메서드 접근지정자는 부모의 접근지정자보다 같거나 커야 한다.(즉, 좁혀질 수 없음)
접근지정자
범위: public > protected > default(아무것도 작성안함) > private
만약 부모클래스의 메서드 접근지정자가 protected라면 자식클래스의 메서드 접근지정자는 public(접근지정자보다 큼), protected(접근지정자와 같음)이 가능하다.
1. A aa = new A()
와 같이 객체를 생성했을 때,
힙 메모리에 A() 생성자로 객체가 생성되고 이를 A타입으로 선언한 참조변수 aa는 이 객체를 가르키고 있다.
A 객체 내에는 print() 메서드가 있지만, 객체 내의 메서드는 실제 메서드의 위치 정보만 저장돼 있고, 실제 print() 메서드는 메서드 영역에 정의돼있다.
aa.print()를 실행하면 A가 나올 것이다.
B bb = new B()
와 같이 객체를 생성했을 때,
B() 생성자가 호출되면 우선 부모 클래스인 객체가 힙 메모리에 먼저 생성된다.
A 객체 내의 print()가 메서드 영역에 생성되고 B 객체가 생성되는데, 여기에 똑같은 메서드인 print()를 추가했다. 그러면 A 객체를 생성하는 과정에서 print() 메서드가 이미 존재하고 있는 상황이다. 이때 B 객체의 print() 메서드가 이미 있는 A 객체의 print() 메서드를 덮어쓰기, 즉 오버라이딩 하게 된다.
이제 bb.print();
를 실행하면 bb 객체 내부의 print() 메서드가 가르키는 곳에 가서 print() 메서드가 실행된다. 메서드 영역의 print() 메서드는 이미 B의 메서드로 오버라이딩 된 이후이므로 B가 출력된다.
A ab = new B()
와 같이 객체를 생성했을 때,
B() 생성자로 생성했지만, A 타입으로 참조변수를 선언했음에 주의하자.
우선 B() 생성자로 객체를 생성하므로 부모 클래스인 A 객체가 만들어지고 이후 B객체가 만들어진다. 이 과정에서 메서드 영역에는 print() 메서드의 오버라이딩이 발생.
그러나 A 타입의 참조변수를 사용하고 있으므로 참조 변수는 A 객체를 가르키고 있으니 ab.print()는 실제 A 객체의 print()메서드를 호출한다. 하지만 A 객체의 print() 메서드가 가리키는 곳은 이미 B의 print() 메서드로 오버라이딩된 상태 이므로 A 객체의 내부 print()가 실행됨에도 B클래스의 B가 출력된다.
메서드 오버라이딩을 사용하는 이유를 알아보자.
부모 클래스로 Animal클래스를 선언하고, 이 클래스를 상속받는 자식 클래스로 Bird, Cat, Dog를 생성하자.
4개의 클래스 모두 cry()메서드를 포함하고 있다. Animal 클래스 내부의 cry클래스에는 아무런 내용이 없으며 나머지 자식 클래스에 각각 자신의 울음소리를 출력하는 내용이 담겨있다.
이제 부모 클래스를 포함해 모든 클래스의 객체를 선언하고 각각의 타입을 객체의 타입과 일치시키고 각각의 객체의 cry()메서드를 실행하면 각각의 울음소리가 출력된다.
다형적 표현을 이용해 각각의 자식 클래스 타입으로 객체를 생성한 후 부모 클래스 타입으로 선언하자.
이떼 참조 변수 ab, ac, ad는 모두 Animal타입이자만 각각 서로 다른 메서드로 오버라이딩 되었으므로 각각의 cry() 메서드는 서로 다른 출력 결과를 보인다.
여기서 꼭 알아야 할 것은 Animal 클래스 내부에 아무런 기능을 수행하지 않는 cry() 메서드가 있는 이유다.
다형적 표현으로 자식 클래스의 객체를 부모 클래스인 Animal 타입으로 선언할 수 있지만, 이렇게 되면 Animal 내부의 메서드만 사용할 수 있다.
즉, 만일 Animal 클래스 내부에 cry() 메서드가 없었다면 어떤 참조변수도 cry()를 호출할 수 없을 것이다. 이것이 아무런 기능을 수행하지 않는 cry()메서드를 Animal내부에 넣어둔 이유이다.
이렇게 모든 객체를 부모 타입 하나로 선언하면 배열로 한번에 관리할 수 있다는 장점이 있다.
오버로딩은 시그니처가 다른 여러개의 메서드를 같은 공간에 정의하는 것이다.
오버라이딩과 오버로딩을 폴더 내 파일과 비교해보면, 오버라이딩은 파일명&확장명이 동일한 파일을 같은 공간에 넣으려고 할 때 덮어쓰기가 일어나는 현상에 비유할 수 있고 오버로딩은 파일명은 동일하지만 확장명이 다른 파일을 같은 폴더에 넣을때다. 각각의 파일이 모두 같은 공간에 존재할 수 있다.
클래스 A에는 print1()과 print2() 메서드가 있다. 클래스 A를 상속받은 클래스 B에서는 print1()과 print2(int a)를 추가정의했다. 이때 클래스 B에서는 총 몇개의 메서드를 이용할 수 있을까?
답은 3개이다. print1()은 상속받은 메서드와 시그니처가 완벽하게 동일하기에 오버라이딩된다.
반면 클래스 A에서 상속받은 print2()메서드는 입력 매개변수가 없는 print2()메서드이며 클래스 B에서 추가로 정의한 입력매개변수로 정숫값을 1개 받는 print2(int a)이므로 메서드 시그니처가 다르기에 print2()메서드는 오버로딩된다.
결과적으로 B 내부에서는 print1(),print2(),print(int a)를 사용할 수 있다.
자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때는 반드시 상속받은 메서드의 접근 지정자와 범위가 같거나 넓은 접근 지정자를 사용해야 한다. 즉, 접근 지정자의 범위를 좁힐 수 없다는 말이다.
예를 들어 부모 클래스가 protected 접근 지정자를 포함했을 때, 자식 클래스는 protected 접근 지정자와 같거나 큰 범위의 접근 지정자인 protected, public만 사용가능.
메서드 오버라이딩에서 생략되어있는 말이 있는 데, 맨 앞에 객체를 뜻하는 인스턴스가 빠져있다. 정확한 이름은 인스턴스 메서드 오버라이딩이다.
인스턴스 멤버 중 메서드는 중복이 가능한 것이다. 그럼 나머지 멤버인 필드는 중복이 가능할까?
인스턴스 필드란 객체 내 static을 포함하고 있지 않는 인스턴스의 필드이다.
인스턴스 필드는 메모리 구조 공간 상 분리 저장되어 있어 같은 공간에 있는 메소드처럼 덮어쓰지 않는다.
따라서, 인스턴스 필드는 오버라이딩이 안된다.
즉, 무슨 생성자로 만들었는지 보다 어떤 type으로 선언되었는지가 중요하다.
인스턴스 필드는 상속받은 필드와 동일한 이름으로 자식클래스에서 정의해도 각각의 저장공간에 저장되므로 오버라이딩은 발생하지 않는다.
Static은 객체를 생성하지않고 바로 사용가능.
예시로 폴더에 a.jpg 파일이 있는데 같은 폴더 안에 새 폴더를 만들어 거기다 새로운 a.jpg를 넣으면 아무런 상관이 없다.
예시처럼 static 필드 또한 메모리 구조 상 분리 저장되어 같은 공간에 있는 메소드처처럼 덮어쓰지 않는다.
따라서, static 필드는 오버라이딩이 안된다.
즉, 무슨 생성자로 만들었는지 보다 어떤 type으로 선언되었는지가 중요하다.
상속할 때, 정적 필드명을 중복해 정의해도 저장 공간이 분리돼 있으므로 오버라이딩은 발생 x
static 메소드 또한 메모리 구조 상 분리 저장되어 같은 공간에 있는 메소드처처럼 덮어쓰지 않는다.
따라서, static 메소드는 오버라이딩이 안된다.
즉, 무슨 생성자로 만들었는지 보다 어떤 type으로 선언되었는지가 중요하다.
this 라는 키워드는 자신이 속한 클래스의 객체(자신의 객체)이면
super 는 부모클래스의 객체(부모의 객체)이다.
class A{
void abc(){
System.out.println("A class abc()");
}
}
class B{
void abc(){ //method overriding
System.out.println("B class abc()");
}
void bcd(){
abc(); //(this.)abc(); 객체 내부에만 존재할 수 있기 때문에
//주인인 객체(this)가 있어야 한다.
// 자신의 메소드를 호출하니, B class abc()가 호출됨.
// 부모의 클래스의 abc()를 호출하고 싶다면? super사용!
}
}
모든 필드와 메소드는 클래스내부에서 주인이 있어야 한다.
즉, 객체가 있으면 객체의 참조변수.필드/메소드를 사용해야 한다.
class A{
void init() {
메모리할당 / 화면세팅 / 변수초기화 등 100줄 코드
}
}
class B extends A{
void init() {
메모리할당 / 화면세팅 / 변수초기화 등 100줄 코드
화면출력: 1줄 코드(추가하고자 하는 기능)
}
}
클래스 A의 내용에다가 1줄 코드를 추가하기 위해 101줄의 코드를 작성하는 건 바람직하지 못하다.(java API를 이용할 경우 코드의 줄 수는 훨씬 더 길어짐)
따라서, super 키워드를 사용해 2줄 코드로 작성하는 것이 훨씬 바람직하다.
class A{
void init() {
메모리할당 / 화면세팅 / 변수초기화 등 100줄 코드
}
}
class B extends A{
void init() {
super.init();
화면출력: 1줄 코드(추가하고자 하는 기능)
}
}
우리가 this를 써주지 않으면 컴파일러가 자동으로 this 키워드를 추가해준다.
그렇다면 부모 클래스의 abc()를 자식클래스에서 호출할 수 있을까?
이때 사용하는게 super 키워드다.
super.abc()로 부모의 abc()를 호출했다.
this()메소드는 자신의 생성자를 호출해라 라는 뜻이다.
super() 메소드는 부모의 생성자를 호출해라 라는 뜻이다.
super() 메소드 특징
1. super() 메서드는 생성자 내부에서만 사용 가능
2. 반드시 중괄호 이후 첫 줄에 위치해야
3. 자식클래스 생성자의 첫 줄에는 반드시 this() 또는 super()가 포함되어야 함(생략시 컴파일러가 자동으로 super() 추가)
=> 모든 자식클래스에는 생성자의 첫 줄에 this() 또는 super()가 반드시 와야!!class A{ A(){ System.out.println("A 생성자"); } } class B extends A{ B(){ super(); //생략시 컴파일러가 자동 삽입 System.out.println("B 생성자"); } } B b = new B(); //A 생성자 //B 생성자
클래스 D가 C를 상속받고 내부에 아무런 코드를 작성하지 않아도 오류가 발생한다. 왜일까? 자식 클래스의 첫 줄에 super/this 메서드가 없으면 컴파일러는 자동으로 기본 생성자의 첫줄에 super 메서드를 추가한다. super는 부모의 기본 생성자 즉 C()를 호출하라는 의미이다. 그러나 클래스 C는 기본 생성자를 포함하고 있지 않으므로 오류가 난다.
해결하기 위해 클래스 D에 직접 생성자를 작성하고 첫줄에 super(3)과 같이 정수를 입력받는 부모의 생성자를 명시적으로 호출해야 한다.
this()와 super()가 여러개 섞어 있는데, B bb 2 = new B(3);
을 보면 클래스 B의 두번째 생성자를 호출해 객체를 생성한다.
두번 째 생성자 첫 줄에 this/super 메서드가 없으므로 컴파일러가 자동으로 super()를 추가해준다.
따라서 부모의 기본생성자인 A()가 먼저 호출된다.
A() 첫 줄에는 다시 this(3)와 같이 자신의 생성자를 호출하고 있으므로 클래스 A의 두번째 생성자가 호출된다.
자바의 모든 클래스는 Object 클래스를 상속받는다. 즉, Object 클래스는 자바의 최상위 클래스이다. 컴파일러는 아무런 클래스를 상속하지 않으면 자동으로 extends Object를 삽입해 Object 클래스를 상속한다.
System.out.println()의 println() 메서드는 다양한 타입을 출력하기 위해 여러개의 입력매개변수 타입으로 오버로딩 되어 있다.
10개의 타입을 출력하는 기능을 부여하려면 10개의 메서드로 오버로딩해놓아야 한다. 그러나 System.out.println(new A())와 같이 사용자가 만든 클래스 타입도 출력할 수 있다는 것이다.
이것이 가능한 이유는 무엇일까? 자바가 사용자가 만들 타입을 미리 생각해 오버로딩해놓을 수는 없다.
여기 Object 클래스의 비밀이 있다.
System.out.println(Object x)로 자바 API에 포함되어 있기 때문이다.
기본 자료형 외에 Object를 매개변수로 하는 println() 메서드를 오버로딩해 놓았기 때문에, 사용자가 어떤 클래스 타입의 객체를 생성하더라도 다형성에 따라 Object 타입으로 불릴 수 있기 때문에 입력매개변수로 모든 타입의 객체를 받아들일 수 있다.
Object 클래스는 자바의 최상위 부모 클래스다. 이는 자바의 모든 클래스가 Object 클래스의 메서드를 포함하고 있다는 뜻이다.
Object 클래스의 대표적인 메서드는 toString(), equals(Object obj), hashCode()가 있다.
Object 클래스의 toString() 메서드는 객체 정보를 문자열로 리턴하는 메서드다. 여기서 객체정보는 패키지명.클래스명@해시코드로 나타난다. 이때 해시코드는 객체가 저장된 위치와 관련된 값이다. 실제 객체의 정보를 표현하고자 할 때는 대부분 클래스명이나 숫자로 나열된 해시코드보다는 객체에 포함돼 있는 필드값을 출력한다.
따라서 이때 자식 클래스에서는 toString() 메서드를 오버라이딩해 사용한다.
클래스 A는 아무것도 상속하지 않았기에 컴파일러가 자동으로 extends Object를 삽입한다. 따라서 내부에는 Object 메서드가 포함돼 있다. 다음과 같이 A 객체를 생성한 후 hashCode() 메서드의 리턴값을 16진수로 출력하면 a 객체의 위칫값과 관련된 고유값이 출력된다.
또한 println() 메서드는 객체를 출력하면 자동으로 객체 내 toString() 메서드를 호출한다. 따라서 System.out.println(a)는 System.out.println(aa.toString())과 같다.
일반적으로, toString()의 출력 결과인 패키지명.클래스명@해시코드는 객체의 직관적인 정보를 제공하지 못하기에 클래스 B처럼 자식 클래스에서 toString() 메서드를 오버라이딩해 사용하는 것이 일반적이다.
equals(Object obj)는 스택메모리 값을 비교한다.
입력매개변수로 넘어온 객체와 자기 객체의 스택 메모리 변수값을 비교해 그 결과를 true 또는 false로 리턴하는 메서드.
기본 자료형이 아닌 객체의 스택 메모리값을 비교하기에 실제 데이터 값이 아닌 실제 데이터의 위치(번지)를 비교하는 것이다.
즉, 등가 비교 연산(==)과 완벽하게 동일한 기능을 수행.
클래스 A는 name 필드 1개를 포함하고 생성자를 이용해 이 필드값을 초기화 한다.
이 후 A a1 = new A("안녕"), A a2 = new A("안녕")과 같이 동일한 필드값을 포함하고 있는 2개의 객체를 생성했다.
객체 내부의 값은 동일하지만, 실제 객체는 다른 곳에 위치하므로 위치값을 나타내는 스택메모리 값은 서로 다르다.
따라서 등가 비교 연산과 equals()메서드는 모두 false가 나온다.
만일 실제 내용을 비교하고 싶다면 equals() 메서드를 오버라이딩해 사용해야 한다.
클래스 B에서는 equals() 메서드를 오버라이딩했다.
메서드 내부에서 자신의 name 값과 입력받은 객체의 name 값을 비교해 동일하면 true, 동일하지 않으면 false를 리턴했다.
이 과정에서 자신의 객체 타입을 일치시키기 위해 캐스팅을 할 수 있는지 확인하는 instanceof 키워드와 다운캐스팅을 사용했다.
이렇게 되면 클래스 B의 equals는 위칫값이 아닌 내용을 비교하게 된다. 다음과 같이 b1==b2는 여전히 false이지만 bb.equals(b2)는 true가 나오게 된다.
hashCode()는 객체의 위치와 연관된 값이다.
객체의 위치값을 기준으로 생성된 고윳값 정도인데, 일반적으로 두 객체의 내용을 비교하기위해서는 equals 메서드를 오버라이딩하는 것만으로도 충분하다. 그러나 Hashtable, HashMap 등에서 동등 비교를 하고자 할때는 hashCode()까지 오버라이딩해야 한다.
HashMap 자료구조는 데이터를 (Key, Value)쌍으로 저장하며, Key 값은 중복되지 않는다. 따라서 Key값이 서로 같은 지 확인해야 하는데 2단계로 이뤄진다.
1단계에서는 두 객체의 hashCode() 값을 비교
두 객체의 hashCode() 값이 동일할 때
2단계에서 equals() 메서드를 호출하며, 이 값이 true이면 같은 객체로 인식한다. 이를 정리하면 hashMap 관점에서 두 객체가 동일하기 위해서는 hashCode() 값이 동일해야 하고, equals() 메서드가 true를 리턴해야!
HashMap 객체는 <Key, Value>의 자료형이 <Integer, String>타입이다. Integer는 기본 자료형 int를 클래스로 만들어 놓은 것으로, 일단 여기서는 int로 생각하자. 이때 HashMap을 쓰기 위해 관련 라이브러리를 import해주어야 한다.
Mac 단축어) cmd+Shift+O
첫번째에서 3쌍의 데이터를 넣었는데, 동일한 key값이 중복돼 들어갔다.
따라서 최종적으로 저장된 데이터셋은 2개이고, key=1 일때의 Value 값은 나중에 들어간 "데이터2"라는 것을 알 수 있다.
두번째 생성된 객체를 보면, Key 값으로 A 객체가 들어갔다. 여기서 관건은 처음 2개의 입력으로 사용된 Key 값인 new A("첫 번째")와 new A("첫 번째)가 동일한지다!
이는 2단계의 과정을 통해 결정된다. 첫번째는 두 객체의 hashCode() 값 비교이다. 그런데 클래스 A는 hashCode()를 오버라이딩 하지 않았다. 따라서 클래스 A 내부에서 사용할 수 있는 hashCode()는 Object의 hashCode()이다. Object의 hashCode()는 객체의 위치에 따라 생성된 고윳값을 리턴한다!
그런데 두 객체가 서로 다른 위치에 생성될 것이므로 두 객체의 hashCode() 값도 서로 다를 것이다. 따라서 첫번째 입력 Key(new A("첫 번째"))와 두 번째 입력 Key(new A("첫 번째"))는 서로 다른 Key로 인식한다.
따라서 이 HashMap 객체의 저장결과를 보면 3쌍의 데이터가 다 들어갔다.
마지막 역시 hashCode() 값과 equals() 메서드의 결과를 순차적으로 확인해 두 객체가 동일한지 결정한다.
클래스 B에서는 hashCode()를 오버라이딩했고 여기서는 name.hashCode()를 사용해 문자열에 따라 해시코드값을 리턴하게 했다.
그런데 두 객체의 name값이 같으므로 두 값의 hashCode()값도 같을 것이다. 두번째인 equals() 메서드의 리턴값을 확인하자.
클래스 B는 equals() 메서드도 오버라이딩했고, 여기서 name 값이 동일할 때 true를 리턴하도록 했다. 따라서 equals() 역시 true를 리턴한다.
두 단계 모두 통과했으므로 첫번째 입력 키와 두번째 입력 키는 동일한 Key 값이 된 것이다.
따라서 최종적으로 저장된 HashMap 데이터를 보면 2쌍의 데이터만 들어가 있고, 중복 저장된 Key 값의 위치에는 나중에 들어간 Value 값이 저장돼있는 것을 확인할 수 있다.