다형적 변수와 오버라이딩
// 다형적 변수와 오버라이딩 - 레퍼런스와 메서드 호출
A obj = new A3() 가능 !
A obj = new A2() 가능 !
A obj = new A() 가능 !
A2 obj = new A() 불가능 !
★ 하위 레퍼런스로 상위 레퍼런스를 가리킬 수 없다 !
package com.eomcs.oop.ex06.d;
class A {
public void m() {
System.out.println("A의 m() 호출!");
}
}
class A2 extends A {
@Override // 컴파일러에게 오버라이딩을 제대로 했는지 검사하라고 명령한다.
public void m() {
System.out.println("A2의 m() 호출!");
}
public void x() {
System.out.println("A2에서 추가한 메서드 x()");
}
}
class A3 extends A2 {
public void y() {
System.out.println("A3에서 추가한 메서드 y()");
}
}
public class Exam0110 {
public static void main(String[] args) {
A a = new A();
a.m(); // A의 멤버 호출. OK!
// ((A2)a).x(); // A 객체를 A2 객체라 우기면, 컴파일러는 통과! 실행은 오류!
System.out.println("--------------------");
A2 a2 = new A2();
a2.m(); // A2가 오버라이딩 한 메서드 호출! 즉 A2의 m() 호출! OK!
a2.x(); // A2의 메서드 호출! OK!
System.out.println("----------------------");
A obj = new A2();
obj.m(); // A2의 m() 호출.
// 레퍼런스가 하위 클래스의 인스턴스를 가리킬 때,
// => 레퍼런스를 통해 호출하는 메서드는
// 레퍼런스가 실제 가리키는 하위 클래스에서 찾아 올라 간다.
//
// 그렇다고 해서 A2에서 추가한 메서드를 호출할 수는 없다.
// => 즉 레퍼런스의 클래스를 벗어나서 사용할 수는 없다.
// 컴파일러가 허락하지 않는다.
// obj.x(); // 컴파일 오류!
// 물론 a3가 실제 A2 객체를 가리키기 때문에
// A2로 형변환을 수행한 후에는 A2의 멤버를 사용할 수 있다.
((A2)obj).x(); // OK!
System.out.println("----------------------");
A obj2 = new A3();
obj2.m(); // A2의 m() 호출
// a4가 실제 가리키는 A3 클래스부터 상위 클래스로 따라 올라가면서
// 첫 번째로 만난 m()을 호출한다.
System.out.println("--------------------");
}
}
// 다형적 변수와 오버라이딩 - 레퍼런스와 메서드 호출
추상 클래스가 가리키는 클래스를 통해 추상 메서드가 출력된다.
Car c = nw Sedan();
c.run();
★ 즉 추상 클래스로 인스턴스를 생성하는 것은 불가능하다.
Car c = new car(); X
package com.eomcs.oop.ex06.d;
abstract class Car {
public abstract void run();
public void m() {}
}
class Sedan extends Car {
@Override
public void run() {
System.out.println("Sedan.run() 호출됨!");
}
}
public class Exam0210 {
public static void main(String[] args) {
// 1) 다형적 변수의 사용법에 따라,
// - super 클래스 레퍼런스로 하위 클래스의 인스턴스를 가리킨다.
Car car = new Sedan();
// 2) 오버라이딩 메서드 호출 규칙에 따라,
// - 레퍼런스가 실제 가리키는 객체의 클래스부터 메서드를 찾아 올라간다.
car.run();
}
}
// final 사용법: 상속 불가!
수퍼클래스를 상속받은 서브클래스가 존재한다면 수퍼클래스의 역할을 대체하여
해킹의 위험성이 존재할 수 있다.
① 그래서 수퍼클래스에 final을 붙여서 상속 불가를 만들어 주어야한다.
수퍼클래스 자체를 교체하지못하게 막는 것보다는,
② 일부 메서드만 교체하지 못하게 막는 문법이다. final 을 메서드 앞에 붙이는 것이다.
③ 또는 변수의 값을 변경하지 못하게 final을 붙여서 상수를 만드는 것이다.
* Hashcode
// 해시코드?
// => 데이터를 식별할 때 사용하는 고유 아이디이다.
// => 보통 데이터를 특별한 공식(ex: MD4, MD5, SHA-1, SHA-256 등)으로
// 계산해서 나온 정수 값을 해시코드로 사용한다.
// => 그래서 해시코드를 데이터를 구분하는 지문과 같다고 해서
// '디지털 지문'이라고 부른다.
// hashCode()를 오버라이딩 할 때?
// => 인스턴스(메모리)가 다르더라도 같은 데이터를 갖는 경우
// 같은 것으로 취급하기 위해 이 메서드를 재정의한다.
// => 따라서 위의 예처럼 데이터가 같은지 따지지도 않고
// 모든 인스턴스에 대해 같은 해시코드를 리턴하는 것은
// 아무 의미없다!
// 이런 식으로 오버라이딩하는 것은 부질없는 짓이다!
public class Exam0144 {
static class Score {
String name;
int kor;
int eng;
int math;
int sum;
float aver;
public Score(String name, int kor, int eng, int math) {
this.name = name;
this.kor = kor;
this.eng = eng;
this.math = math;
this.sum = kor + eng + math;
this.aver = this.sum / 3f;
}
// hashCode()를 오버라이딩하면 원하는 값을 리턴할 수 있다.
@Override
public int hashCode() {
// 무조건 모든 Score 인스턴스가 같은 해시코드를 갖게 하자!
return 1000;
}
}
public static void main(String[] args) {
Score s1 = new Score("홍길동", 100, 100, 100);
Score s2 = new Score("홍길동", 100, 100, 100);
Score s3 = new Score("임꺽정", 90, 80, 70);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
* Hash set
★ HashSet ?
순서를 유지하지 않고, 객체를 해시 값으로 저장하는 자료구조이다.
중복된 요소를 허용하지 않고, 객체의 해시 코드를 기반으로 저장하고 조회한다.
List와 달리 저장된 순서와 반복 순회 순서는 일정하지 않을 수 있다..
따라서, 순서가 중요하지 않고 중복을 제거해야 하는 경우에 사용하기 적합하다.
★ 문제점 ?
모든 인스턴스 변수는 각각의 해시코드를 가진다.
해쉬셋을 만들게 되면 같은 값이지만 해시코드가 달라서 중복 출력된다.
해쉬셋은 순서는 상관없이 중복값을 제거할 때 사용하는 것이기 때문에
이러한 문제를 해결하기 위해서는 오버라이딩을 통해서 해시코드를 맞춰주어야 한다.
★ 즉, 오버라이딩을 통해서 hashcode 와 equals를 둘다(and) 맞춰주어야 한다는 것이다.
// hash code 응용 - HashSet 과 hashCode(), equals()의 관계
package main.java.ex01.copy;
import java.util.HashSet;
public class Exam0150 {
static class Student {
String name;
int age;
boolean working;
public Student(String name, int age, boolean working) {
this.name = name;
this.age = age;
this.working = working;
}
// @Override
// public int hashCode() {
// return 100;
// }
// @Override
// public boolean equals(Object obj) {
// return true;
// }
}
public static void main(String[] args) {
Student s1 = new Student("홍길동", 20, false);
Student s2 = new Student("홍길동", 20, false);
Student s3 = new Student("임꺽정", 21, true);
Student s4 = new Student("유관순", 22, true);
System.out.println(s1 == s2);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
System.out.println(s4.hashCode());
System.out.println("--------------------");
// 해시셋(집합)에 객체를 보관한다.
HashSet<Student> set = new HashSet<Student>();
set.add(s1);
set.add(s2);
set.add(s3);
set.add(s4);
// 해시셋에 보관된 객체를 꺼낸다.
Object[] list = set.toArray();
for (Object obj : list) {
Student student = (Student) obj;
System.out.printf("%s, %d, %s\n",
student.name, student.age, student.working ? "재직중" : "실업중");
}
}
}
* HashCode와 equals를 Override 한다면 ?
중복되지 않게 출력된다 !
* HashMap
인스턴스 Key가 달라도 equals와 hashcode가 같다면 같은 것으로 간주한다.
때문에 key로 사용하기 위해서는 hashcode와 equals를 재정의해야한다.
// 결론!
// => k2와 k6는 다른 객체지만,
// hashCode()의 리턴 값이 같고, equals()의 리턴 값이 true이기 때문에
// 두 객체는 같은 key로 간주된다.
// hash code 응용 II - MyKey의 hashCode()와 equals() 오버라이딩 하기
package main.java.ex01.copy;
import java.util.HashMap;
import java.util.Objects;
public class Exam0153 {
static class MyKey2 {
String contents;
public MyKey2(String contents) {
this.contents = contents;
}
@Override
public String toString() {
return "MyKey2 [contents=" + contents + "]";
}
@Override
public int hashCode() {
return Objects.hash(contents);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MyKey2 other = (MyKey2) obj;
return Objects.equals(contents, other.contents);
}
}
public static void main(String[] args) {
HashMap<MyKey2, Student> map = new HashMap<>();
MyKey2 k1 = new MyKey2("ok");
MyKey2 k2 = new MyKey2("no");
MyKey2 k3 = new MyKey2("haha");
MyKey2 k4 = new MyKey2("ohora");
MyKey2 k5 = new MyKey2("hul");
map.put(k1, new Student("홍길동", 20, false));
map.put(k2, new Student("임꺽정", 30, true));
map.put(k3, new Student("유관순", 17, true));
map.put(k4, new Student("안중근", 24, true));
map.put(k5, new Student("윤봉길", 22, false));
System.out.println(map.get(k3));
// 다른 key 객체를 사용하여 값을 꺼내보자.
MyKey2 k6 = new MyKey2("haha");
System.out.println(map.get(k6)); // OK! 값을 정상적으로 꺼낼 수 있다.
// k3와 k6는
// hashCode()의 리턴 값이 같다
// equals() 비교 결과도 true 이기 때문에
// HashMap 클래스에서는 서로 같은 key라고 간주한다.
System.out.println(k3 == k6); // 인스턴스는 다르다.
System.out.printf("k3(%s), k6(%s)\n", k3, k6);
System.out.println(k3.hashCode()); // hash code는 같다.
System.out.println(k6.hashCode()); // hash code는 같다.
System.out.println(k3.equals(k6)); // equals()의 비교 결과도 같다.
}
}
* shallow copy vs deep copy
Shallow copy는 원본 객체의 필드를 복사하지만 참조 타입 필드는 참조만 복사하여 공유될 수 있다.
참조 타입 필드는 원본 객체와 동일한 주소를 가리킬 수 있으며,
원본 객체와 복사된 객체는 같은 객체를 참조할 수 있다.
Deep copy는 원본 객체와 그에 속한 모든 객체를 재귀적으로 복사하여 독립적인 복사본을 생성한다.
이로 인해 원본 객체와 복사된 객체는 독립적으로 존재하며,
한 객체의 필드 값 변경이 다른 객체에 영향을 주지 않는다.