✅네이버클라우드 캠프/개발일기

[네이버클라우드캠프] 42일차.자바 다형성과 오버라이딩, 객체 복사와 Immutable, 그리고 해시코드와 컬렉션 활용하기

우동한그릇 2023. 6. 23. 16:30
반응형

 

 

다형적 변수와 오버라이딩

// 다형적 변수와 오버라이딩 - 레퍼런스와 메서드 호출


com.eomcs.oop.ex06.d;

 

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("--------------------");
  }
}

 

// 다형적 변수와 오버라이딩 - 레퍼런스와 메서드 호출

 

com.eomcs.oop.ex06.d;

추상 클래스가 가리키는 클래스를 통해 추상 메서드가 출력된다.

 

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 사용법: 상속 불가!

 

com.eomcs.oop.ex06.e;

 

수퍼클래스를 상속받은 서브클래스가 존재한다면 수퍼클래스의 역할을 대체하여

해킹의 위험성이 존재할 수 있다.

 

① 그래서 수퍼클래스에 final을 붙여서 상속 불가를 만들어 주어야한다.

 

수퍼클래스 자체를 교체하지못하게 막는 것보다는,

② 일부 메서드만 교체하지 못하게 막는 문법이다.  final 을 메서드 앞에 붙이는 것이다.

 

③ 또는 변수의 값을 변경하지 못하게 final을 붙여서 상수를 만드는 것이다.

 


com.eomcs.basic.ex01

* 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);

 


com.eomcs.basic.ex01

* 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 한다면 ?

 

 

중복되지 않게 출력된다 !

 


com.eomcs.basic.ex01

* 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는 원본 객체와 그에 속한 모든 객체를 재귀적으로 복사하여 독립적인 복사본을 생성한다.

이로 인해 원본 객체와 복사된 객체는 독립적으로 존재하며, 

한 객체의 필드 값 변경이 다른 객체에 영향을 주지 않는다.

 

 

반응형