본문 바로가기

국비 교육

2020.8.13일자 수업 : Object 클래스

추상화

Actor : 시스템이 동작하게 만드는 촉발점.

UML 업무를 컴퓨터를 통해서 다룬다면 컴퓨터에게 명령을 내리는 어플리케이션이 필요하다. 어플리케이션을 사용하는 주체를 Actor라고 하는데 이 주체에 사람만 있는 것은 아니다. 시스템도 시스템을 사용할 수 있고, 타이머가 특정 시간에만 동작을 촉발시킬 수도 있다.

Actor의 종류에는 사람과 시스템, 타이머가 있다. 이 세가지가 원하는 것에 맞춰 시스템을 구현해야한다. 

이 시스템을 통해 이뤄져야할 업무는 사람과 사물, 개념을 다룬다. 개념은 추상적인 것으로 쇼핑몰 사이트의 장바구니와 같은 것이다. 프로그램을 구현하려면 사람, 사물, 개념에 관련된 데이터(Member)내부 업무 행위(MemberHandler)를 클래스로 정의해야한다. 클래스로 정의하는 이 행위를 추상화라고 부른다. 회원은 현재 관리자와 고객으로 나눠진다. 회원 클래스를 상속하여 관리자와 고객이라는 sub클래스를 만들어 관리가 가능하다. 또 member[]을 만들면 관리자와 고객을 섞은 배열을 만들어 관리도 가능하다.. 이것을 다형성이라고 부르며 더 구체적으로는 다형적 변수라고 부른다. 멤버 클래스와 필드를 다루는데 캡슐화도 사용될 수 있다. 추상화 과정에서 상속과 다형성, 캡슐화가 사용될 수 있는 것이다.

 

Object 클래스

git/ bitcamp-20200713/ src/ main / java com.eomcs.corelib.ex01.Exam0110~0174.java

어떤 것도 상속받지 않는 클래스는 자동으로 object 를 상속받는다. 

instanceof 연산자(레퍼런스가 가리키는 인스턴스가 지정한 클래스를 인스턴스 이거나 또는 조상으로 갖는지 검사한다.)를 통해서 혹은 레퍼런스 변수에 인스턴스 주소를 할당할 수 있는지 여부를 통해서 이 상속 관계를 확인할 수 있다.

public class Exam0110 /*extends Object*/ {

  static class My /*extends Object*/ {
  }

  public static void main(String[] args) {

    Object obj = new My();

    System.out.println(obj instanceof My);
    System.out.println(obj instanceof Object);
    
    // 결과 ! true
    //       true

 

Object 클래스의 메서드

모든 클래스는 Object 클래스를 상속받았으므로 Object에 선언된 메서드를 사용할 수 있다.

1. toString()

클래스 정보를 간단히 출력한다. 

패키지명.클래스명@16진수해시값

더보기

* 해시값?

  - 인스턴스 마다 부여된 고유의 식별자이며 인스턴스가 같은지 검사할 때 사용할 수 있다. hashCode()을 재정의하지 않고 원래 메서드를 그대로 사용하면 무조건 인스턴스마다 새 해시값이 부여된다.

 

중요한 것! 해시값은  메모리의 주소가 아니다

  - 자바는 절대로 메모리 주소를 알려주지 않는다! 해시값은 단지 인스턴스를 식별할 때 사용하라고 JVM이 임의로 붙인 식별자이다. 이것을일종의 디지털 지문이라고 부른다.

toString() 메서드를 호출하면 인스턴스 안에 든 값을 간단히 리턴할 수 있도록 toString() 을 오버라이딩 할 수 있다.

public class Exam0121 {
  
  static class My {
    String name;
    int age;

    @Override
    public String toString() {
      return "My3 [name=" + name + ", age=" + age + "]";
    }
  }
  
  public static void main(String[] args) {
    
    My obj1 = new My();
    
    obj1.name = "홍길동";
    obj1.age = 20;
    
    System.out.println(obj1.toString());
    
    // println()의 파라미터 값으로 문자열을 넘겨주지 않으면,
    // println() 내부에서 파라미터로 넘어온 객체에 대해 toString() 호출한 후 
    // 그 리턴 값을 출력한다.
    // 따라서 그냥 객체(주소)를 넘겨줘도 된다.
    System.out.println(obj1);

println()에 String이 아닌 객체를 넘기면, println()에서 내부적으로 그 객체에 대해 toString()을 호출하여 그 리턴 값을 출력한다.

 

2. equals()

==와 같게 기능하며 같은 인스턴스의 주소인지 검사한다. 

내용물이 같아도 별도의 두개의 인스턴스라면 false를 리턴하는데 내용물을 비교하게 하고 싶다면 equals()를 그에 맞게 오버라이딩할 수 있다. 데이터 타입을 정의한 클래스라면 보통은 equals() 를 오버라이딩한다.

 

오버라이딩을 직접 할 수도 있지만 이클립스과 같은 통합개발환경에서는 자동으로 오버라이딩해주는 기능이 있다.

만들어주고 싶은 클래스 명에 컨테스트 메뉴 -> source -> generate hashCode() and toString()

@Override
public boolean equals(Object obj) {
  if (this == obj)
    return true;
  if (obj == null)
    return false;
  if (getClass() != obj.getClass())
    return false;
  My other = (My) obj;
  if (age != other.age)
    return false;
  if (email == null) {
    if (other.email != null)
      return false;
  } else if (!email.equals(other.email))
    return false;
  if (gender != other.gender)
    return false;
  if (name == null) {
    if (other.name != null)
      return false;
  } else if (!name.equals(other.name))
    return false;
  if (tel == null) {
    if (other.tel != null)
      return false;
  } else if (!tel.equals(other.tel))
    return false;
  if (working != other.working)
    return false;
  return true;
  }


String 클래스와 Wrapper 클래스는 equals()를 오버라이딩 했기 때문에 같은 값을 가지면 true를 리턴한다.

그러나 그 외의 equals 오버라이딩하지 않은 클래스들은 내용만 같다고 true가 나오지 않는다.

StringBuffer sb1 = new StringBuffer("Hello");

StringBuffer sb2 = new StringBuffer("Hello");

    

System.out.println(sb1 == sb2); // false

System.out.println(sb1.equals(sb2)); // false

StringBuffer sb1 = new StringBuffer("Hello"); 
StringBuffer sb2 = new StringBuffer("Hello");    
System.out.println(sb1 == sb2); // false
System.out.println(sb1.equals(sb2)); // false

 

3. hashCode()

인스턴스마다 각각 다른 해시코드 값을 리턴한다.

 

해시코드

데이터를 다른 데이터와 구분하기 위한 특벽한 정수 값이며 특정 수학 공식에 따라 값을 계산한다. 모든 데이터를 일일히 비교하는 대신 해시코드를 비교함으로써 빠르게 두 값이 같은 지 알아낼 수 있다. 매우 낮은 확률로 다른 데이터에 같은 정수 값이 나올 수는 있다. 큰 데이터를 계산 공식을 통해서 4바이트 정수 값으로 표현하기 때문이다. 그러나 확률이 매우 낮기 때문에 그럴 가능성을 걱정할 필요는 없다. 해시코드를 다른 말로는 디지털 지문이라고도 한다.

 

* 이외에 해시코드가 사용되는 곳

  • 본인 여부를 확인하는 인증서.
  • 파일의 위변조를 검사하는 용도.
    예1) git 에서 커밋할 때 고유번호를 붙이는데 바로 해시 값이다.
    예2) 파일 다운로드 사이트에서 제공하는 해시 값.
    예3) 파일 공유사이트에서 파일을 구분할 때 해시 값 사용 : 같은 해시코드인 파일을 같은 파일로 취급하여 같은 파일들을 조합하여 하나의 파일을 만드는 것이 파일 공유 사이트에서 사용하는 원리이다. 그러나 같은 해시코드라 할지라도 다른 파일일 수 있는 가능성이 있는데 이럴 경우에는 파일이 깨지게 된다.
     -> 사용자가 파일 이름을 변경하더라도 데이터만 바꾸지 않는다면 파일의 해시 값은 같다. 
  • 해시 알고리즘 : SHA, MD, PGP 등 

equals()는 인스턴스마다 각각 다른 해시코드를 리턴하므로 같은 값이라도 다른 인스턴스면 다른 해시코드를 리턴한다. 그러나 String 의 hashCode() 메서드는 내용이 같으면 같은 해시코드를 만들도록 오버라이딩됐으므로 이를 이용해서 다른 클래스의 hashCode()도 유사하게 오버라이딩이 가능하다.

  static class My {
    String name;
    int age;

    @Override
    public int hashCode() {
      String str = String.format("%s,%d", this.name, this.age);
      return str.hashCode();
    }
  }

  public static void main(String[] args) {

    My obj1 = new My();
    obj1.name = "홍길동";
    obj1.age = 20;

    My obj2 = new My();
    obj2.name = "홍길동";
    obj2.age = 20;

    System.out.println(obj1 == obj2);
    System.out.println(obj1.equals(obj2));
      }
    }

hashCode() 와 equals()를 함께 오버라이딩하는 이유:

객체 두개를 비교할 때 보통 해시코드 값의 동일여부와 equals() 의 리턴 값을 모두 확인한다. 따라서 같은 값에서 모두 같다는 결과가 나오도록 hashCode() 와 equals() 를 함께 오버라이딩해주는 것이 좋다. 이것은 위에서 언급한 이클립스의 기능을 사용하는 것이 훨씬 간편하다.

 

HashSet

java,util.class 클래스에 해시셋을 관리하는 기능이 있다. ArrayList 이나 HashSet과 클래스를 컬렉션 API 라고 칭한다.

set은 집합이며 같은 값을 중복하여 추가하지 않는다는 특징을 갖는다.

 

중복하지 않게 값을 넣으려면 같은 값은 넣지 말아야하므로 같은 값인지 구별하기 위해서 해시코드와 equals() 결과를 통해서 비교하게 된다. 따라서 넣고 나면 각각 다른 해시코드를 갖고 서로에 대한 equals()결과가 false인 객체들만 들어가게 된다.

 

인스턴스가 다르더라도 필드 값이 같을 때 HashSet에 이를 중복저장하지 않게 하려면, hashCode()와 equals()를 모두 오버라이딩해야한다.

hashCode()는 같은 필드 값을 갖는 경우 같은 해시코드를 리턴하도록 변경하고, equals()는 필드 값이 같을 경우 true를 리턴하도록 변경한다.

import java.util.HashSet;

public class Exam0151 {

  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() {
      final int prime = 31;
      int result = 1;
      result = prime * result + age;
      result = prime * result + ((name == null) ? 0 : name.hashCode());
      result = prime * result + (working ? 1231 : 1237);
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      Student other = (Student) obj;
      if (age != other.age)
        return false;
      if (name == null) {
        if (other.name != null)
          return false;
      } else if (!name.equals(other.name))
        return false;
      if (working != other.working)
        return false;
      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); // false
    
    System.out.println(s1.hashCode());
    System.out.println(s2.hashCode());
    System.out.println(s3.hashCode());
    System.out.println(s4.hashCode());
    System.out.println("--------------------");
    
    // 1678702170
    // 1678702170
    // 1566232583
    // 1562779400
    
    // 해시셋(집합)에 객체를 보관한다.
    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 ? "재직중" : "실업중");
          
    // 홍길동, 20, 실업중
    // 임꺽정, 21, 재직중
    // 유관순, 22, 재직중

    }
  }
}

 

HashMap 

HashMap  key를 사용하여 값을 더 빨리 검색하는 데 최적화된 클래스이다. 값을 저장할 때 key 객체의 해시코드를 이용하여 저장할 위치(인덱스)를 계산한다. 따라서 key의 해시코드가 같다면 같은 key로 간주한다.

HashMap<MyKey,Student> map = new HashMap<>();

MyKey k1 = new MyKey("ok");
MyKey k2 = new MyKey("no");
MyKey k3 = new MyKey("haha");
MyKey k4 = new MyKey("ohora");
MyKey k5 = new MyKey("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를 함께 저장하면 나중에 key를 이용하여 값을 꺼낼 수 있다. 

 

key 객체의 해시코드 값이 같고 equals() 결과 값이 true여야 두 key를 같은 key로 간주한다. 따라서 같은 값을 가진 다른 key 인스턴스만으로는 원하는 값을 꺼낼 수 없다.

package com.eomcs.corelib.ex01;

import java.util.HashMap;

public class Exam0152 {

  static class MyKey {
    String contents;

    public MyKey(String contents) {
      this.contents = contents;
    }

    @Override
    public String toString() {
      return "MyKey [contents=" + contents + "]";
    }
  }

  public static void main(String[] args) {
    HashMap<MyKey,Student> map = new HashMap<>();

    MyKey k1 = new MyKey("ok");
    MyKey k2 = new MyKey("no");
    MyKey k3 = new MyKey("haha");
    MyKey k4 = new MyKey("ohora");
    MyKey k5 = new MyKey("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));
    // Student [name=유관순, age=17, working=true]

    MyKey k6 = new MyKey("haha");
    System.out.println(map.get(k6)); // null
    // 두 키 객체 k3와 k6가 내용물이 같다 하더라도, (둘다 "haha"이다.)
    // hashCode()의 리턴 값이 다르고, equals() 비교 결과도 false 라면
    // HashMap 클래스에서는 서로 다른 key로 간주한다.

    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()의 비교 결과도 다르다.

  }
}







 따라서 해시맵을 쓰려면 같은 내용이 있는 객체에 같은 해시코드를 줄 수 있도록 hashCode() 을 오버라이딩하고, 내용이 같으면 equals() 도 true가 나오도록 오버라이딩 해야한다.

// hash code 응용 II - MyKey의 hashCode()와 equals() 오버라이딩 하기
package com.eomcs.corelib.ex01;

import java.util.HashMap;


public class Exam0153 {

  static class MyKey2 {
    String contents;

    public MyKey2(String contents) {
      this.contents = contents;
    }

    @Override
    public String toString() {
      return "MyKey2 [contents=" + contents + "]";
    }
    // hashCode() 오버라이딩
    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((contents == null) ? 0 : contents.hashCode());
      return result;
    }
    // equals() 오버라이딩
    @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;
      if (contents == null) {
        if (other.contents != null)
          return false;
      } else if (!contents.equals(other.contents))
        return false;
      return true;
    }
  }
  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)); 
    // Student [name=유관순, age=17, working=false]
    
    MyKey2 k6 = new MyKey2("haha");

    System.out.println(map.get(k6)); 
    // Student [name=유관순, age=17, working=false]
    
    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()의 비교 결과도 같다.

  }
}

실무에서는 key를 따로 클래스로 생성하여 만들지 않는다. 굳이 오버라이딩하면서 클래스를 만들 필요 없이 String 클래스나 Wrapper 클래스를 사용하면 이미 오버라이딩 된 메서드들을 이용할 수 있다. 다음은 Wrapper 타입의 인스턴스를 key로 사용한 사례이다.

map.put(101, new Student("홍길동", 20, false));
map.put(102, new Student("임꺽정", 30, true));
map.put(103, new Student("유관순", 17, true));

// => map.put(new Integer(101), new Student("홍길동", 20, false));
// => map.put(new Integer(102), new Student("임꺽정", 30, true));
// => map.put(new Integer(103), new Student("유관순", 17, true));

// 이렇게 primitive data 타입값을 줘도 그 값을 갖는 새 Wrapper 클래스를 자동으로 생성해주는 것을
// auto-wrapping 이라고 부른다.

System.out.println(map.get(102)); // 오토 랩핑!
// 결과 ! Student [name=임꺽정, age=30, working=false]
Integer k6 = new Integer(102);
System.out.println(map.get(k6));
// 결과 ! Student [name=임꺽정, age=30, working=false]


map.put("ok", new Student("홍길동", 20, false));
map.put("no", new Student("임꺽정", 30, true));
map.put("haha", new Student("유관순", 17, true));
map.put("ohora", new Student("안중근", 24, true));
map.put("hul", new Student("윤봉길", 22, false));

System.out.println(map.get("haha"));
// 결과 ! Student [name=유관순, age=17, working=true]
String k6 = new String("haha");
System.out.println(map.get(k6));
// 결과 ! Student [name=유관순, age=17, working=true]

4. getClass()

인스턴스의 클래스 정보를 ClassInfo 클래스 타입 형태로 리턴한다.

reflection API (Aplication Program Interface)- ClassInfo와 같이 객체를 통해 클래스의 정보를 분석해 내는 프로그램 기법을 말한다. 

 

우리는 getClass()를 통해서 Classinfo 형태의 인스턴스를 얻고, 또 ClassInfo 클래스에 getName()와 getSimpleName()을 통해 알고 싶은 인스턴스의 클래스 이름을 출력할 수 있다.

getName() : 클래스의 풀네임, 즉 패키지+클래스명을 리턴한다.

getSimpleName() : 클래스의 간단한 이름, 즉 패키지를 제외한 클래스명만을 리턴한다.

static class My {
  }

  public static void main(String[] args) {
    My obj1 = new My();

    // 레퍼런스를 통해서 인스턴스의 클래스 정보를 알아낼 수 있다.
    Class<?> classInfo = obj1.getClass();

    // 클래스 정보로부터 다양한 값을 꺼낼 수 있다. 
    System.out.println(classInfo.getName());
    System.out.println(classInfo.getSimpleName());
    
    // 결과 ! com.eomcs.corelib.ex01.Exam0160$My
    //       My
  }
}

getName()를 통해 받은 ClassInfo 클래스로 해당 객체가 어떤 데이터를 받는 배열인지도 알 수 있다. 

 

어떤 배열인지에 따라 가장 앞에 오는 내용이 다르다.

  • [L - 클래스 배열
  • [I - int 배열
  • [F - float 배열
  • [D - double 배열
  • [B - byte 배열
  • [S - short 배열
  • [J - long 배열
  • [C - char 배열
  • [Z - boolean 배열

그리고 클래스 타입을 받는 배열이라면 그 뒤에 클래스의 풀네임을 붙인다.

// Object 클래스 - getClass() 와 배열
package com.eomcs.corelib.ex01;

public class Exam0161 {

  public static void main(String[] args) {

    String obj1 = new String();
    Class<?> classInfo = obj1.getClass();
    System.out.println(classInfo.getName()); // java.lang.String

    // 배열의 클래스 정보
    String[] obj2 = new String[10];
    classInfo = obj2.getClass();
    System.out.println(classInfo.getName()); //[Ljava.lang.String;

    int[] obj3 = new int[10];
    classInfo = obj3.getClass();
    System.out.println(classInfo.getName()); //[I

    float[] obj4 = new float[10];
    classInfo = obj4.getClass();
    System.out.println(classInfo.getName()); //[F

    double[] obj5 = new double[10];
    classInfo = obj5.getClass();
    System.out.println(classInfo.getName()); //[D

    System.out.println(new byte[10].getClass().getName()); //[B
    System.out.println(new short[10].getClass().getName()); //[S
    System.out.println(new long[10].getClass().getName()); //[J
    System.out.println(new char[10].getClass().getName()); //[C
    System.out.println(new boolean[10].getClass().getName()); //[Z

    System.out.println(obj3);
  }
}

배열의 클래스 정보 뿐만 아니라 getComponentType()  메서드가 리턴하는 compTypeInfo 클래스를 통해서 배열의 각 항목의 타입의 정보도 알 수 있다. 즉 각 항목이 어떤 클래스 타입의 레퍼런스 주소인지 (인스턴스의 클래스 타입을 의미하는 것이 아니다) 를 의미한다. 

String[] obj2 = new String[10];
Class<?> classInfo = obj2.getClass();
Class<?> compTypeInfo = classInfo.getComponentType();
System.out.println(compTypeInfo.getName()); //java.lang.String

5. clone()

인스턴스를 복제한 후 그 복제 인스턴스를 리턴한다. 이 메서드는 JVM이 직접 메모리를 복제하도록 되어있다. 따라서 해당 메서드는 OS에 종속된 프로그램을 다루기 위해 자바가 아닌 C, C++, 어셈블리와 같은 다른 언어로 작성된 라이브러리를 사용한다. 이를 native라고 부른다.

 

오브젝트의 clone() 은 protected 접근제어자이므로 다른 클래스에서 해당 클래스의 인스턴스를 갖고 직접 clone()을 호출할 수 없다. 

package com.eomcs.corelib.ex01;

public class Exam0170 {

  static class Score {

    String name;
    int kor;
    int eng;
    int math;
    int sum;
    float aver;

    public Score() {}

    public Score(String name, int kor, int eng, int math) {
      this.name = name;
      this.kor = kor;
      this.eng = eng;
      this.math = math;
      this.sum = this.kor + this.eng + this.math;
      this.aver = this.sum / 3f;
    }

    @Override
    public String toString() {
      return "Score [name=" + name + ", kor=" + kor + ", eng=" + eng + ", math=" + math + ", sum="
          + sum + ", aver=" + aver + "]";
    }
  }

  public static void main(String[] args) {

    Score s1 = new Score("홍길동", 100, 100, 100);
    System.out.println(s1);

    // Object에서 상속 받은 clone()을 호출한다.
    Score s3 = s1.clone(); // 컴파일 오류!
  }
}

따라서 해당 클래스 안에서 인스턴스를 clone()을 오버라이딩한 메서드를 만들고 super.clone()을 거기서 호출해야한다.

또한 implements Cloneable 를 클래스 선언부에 붙여서 복제 능력을 활성화시켜야하고, 오버라이딩한 메서드 선언부에는 CloneNotSupportedException 로 예외처리를 해야한다.

package com.eomcs.corelib.ex01;

public class Exam0171 {

  static class Score implements Cloneable {
    String name;
    int kor; 
    int math;
    int sum;
    float aver;

    public Score() {}

    public Score(String name, int kor, int eng, int math) {
      this.name = name;
      this.kor = kor;
      this.eng = eng;
      this.math = math;
      this.sum = this.kor + this.eng + this.math;
      this.aver = this.sum / 3f;
    }

    @Override
    public String toString() {
      return "Score [name=" + name + ", kor=" + kor + ", eng=" + eng + ", math=" + math + ", sum="
          + sum + ", aver=" + aver + "]";
    }

    // => Object에서 상속 받은 clone()을 오버라이딩하여 다른 패키지의 멤버도 사용할 수 있게
    //    public 으로 접근 범위를 넓혀라!
    // => 오버라이딩은 접근 범위를 좁힐 수는 없지만, 넓힐 수는 있다.
    // => 오버라이딩 할 때 리턴 타입을 클래스 타입으로 변경해도 된다.
    @Override
    public Score clone() throws CloneNotSupportedException {
      return (Score) super.clone();
    }
  }

  public static void main(String[] args) throws Exception {

    Score s1 = new Score("홍길동", 100, 100, 100);
    Score s2 = s1.clone();

    System.out.println(s1 == s2);
    System.out.println(s1);
    System.out.println(s2);
    
    // 결과 ! false
    //       Score [name=홍길동, kor=100, eng=100, math=100, sum=300, aver=100.0]
    //       Score [name=홍길동, kor=100, eng=100, math=100, sum=300, aver=100.0]

  }
}

Shallow copy vs deep copy

  • shallow copy : 복제된 인스턴스가 가진 필드 중에 객체를 가리키는 레퍼런스 변수가 가리키는 인스턴스는 복제가 되지 않는다. 따라서 두 개 레퍼런스 변수가 하나의 객체를 가리키고 있는 것이다. 따라서 해당 객체의 클래스에 implements Cloneable과 clone() 메서드를 오버라이딩 안해도 문제가 없다. Object가 갖는 clone()은 shallow copy를 따른다.
  • deep copy : 복제된 클래스의 인스턴스 필드 중에 있는 객체들을 모두 복제하고 그 객체에 든 다른 하위 객체도 복제하는 방식으로 모든 객체를 복제하는 방식이다. 따라서 모든 복제되는 객체 안에서 clone() 메서드를 오버라이딩하고 implements Cloneable을 클래스 선언부에 붙이는 과정이 필요하다.
// Object 클래스 - clone() : deep copy
package com.eomcs.corelib.ex01;

public class Exam0174 {

  static class Engine implements Cloneable {
    int cc;
    int valve;

    public Engine(int cc, int valve) {
      this.cc = cc;
      this.valve = valve;
    }

    @Override
    public String toString() {
      return "Engine [cc=" + cc + ", valve=" + valve + "]";
    }
    
    // Car 클래스 안에 있는 engine 도 함께 clone() 오버라이딩해줘야
    // deep copy를 할 수 있다.
    @Override
    public Engine clone() throws CloneNotSupportedException {
      return (Engine) super.clone();
    }
  }

  static class Car implements Cloneable {
    String maker;
    String name;
    Engine engine;

    public Car(String maker, String name, Engine engine) {
      this.maker = maker;
      this.name = name;
      this.engine = engine;
    }

    @Override
    public String toString() {
      return "Car [maker=" + maker + ", name=" + name + ", engine=" + engine + "]";
    }

    @Override
    public Car clone() throws CloneNotSupportedException {
      Car copy = (Car) super.clone();
      copy.engine = this.engine.clone(); // 안에 있는 엔진도 함께 복제해주는 코드
      return copy;
    }
  }

  public static void main(String[] args) throws Exception {
    Engine engine = new Engine(3000, 16);
    Car car = new Car("비트자동차", "비트비트", engine);

    // 자동차 복제
    Car car2 = car.clone();

    System.out.println(car == car2);
    System.out.println(car);
    System.out.println(car2);
    System.out.println(car.engine == car2.engine);

    // car의 엔진과 car2의 엔진이 다른 엔진인지 확인해보자!
    car.engine.cc = 2000;
    System.out.println(car2.engine.cc);

  }
}