본문 바로가기
JAVA

6주차 : 상속

by 앙헬디마리아 2020. 12. 26.
728x90

6주차

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버라이딩
  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
  • 더블 메소드 디스패치(Double Method Dispatch)
  • 추상 클래스
  • final 키워드
  • Object 클래스

 

1. 자바 상속의 특징

 

현실에서 상속은 부모가 자식에게 물려주는 행위를 말한다.

 

자식은 상속을 통해서 부모가 물려준 것을 자연스럽게 이용할 수 있다. 객체 지향 프로그램에서도 부모 클래스의 멤버를 자식 클래스에게 물려줄 수 있다.

 

(출처: https://kephilab.tistory.com/56)

 

 

상속은 이미 잘개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 코드의 중복을 줄여준다.

 

field1, field2, method1(), method2()를 가지는 클래스를 작성한다고 생각해보자. 4개를 모두 처음부터 작성하는 것보다는 field1과 method1()을 가지고 있는 클래스가 있다면 이것을 상속하고, field2와 method2()만 추가 작성하는 것이 보다 효율적이고 개발 시간을 절약시켜준다.

 

상속을 해도 부모 클래스의 모든 필드와 메소드들을 물려받는 것은 아니다. 부모 클래스에서 private 접근 제한을 갖는 필드와 메소드는 상속 대상에서 제외된다.

 

그리고 부모 클래스와 자식 클래스가 다른 패키지에 존재한다면 default 접근 제한을 갖는 필드와 메소드도 상속 대상에서 제외된다.

 

자식 클래스를 선언할 때 어떤 부모 클래스를 상속받을 것인지를 결정하고 선택된 부모 클래스는 다음과 같이 extends 뒤에 기술한다.

 

class 자식클래스 extends 부모클래스 {
	//필드
	//생성자
	//메소드
}

 

예를 들어 Car 클래스를 상속해서 SportsCar 클래스를 설계하고 싶다면 다음과 같이 작성하면 된다.

class SportsCar extends Car{ }

 

다른 언어와는 달리 자바는 다중 상속을 허용하지 않는다.

class SportsCar extends Car, 다른부모클래스{
 	//이건 안돼!!!
}

 

자바에서는 자식 객체를 생성하면, 부모 객체가 먼저 생성되고 자식 객체가 그 다음에 생성된다.

SportsCar sportsCar = new SportsCar();

위의 코드는 SportCar 객체만 생성하는 것 처럼 보이지만, 사실은 내부적으로 부모인 Car 객체가 먼저 생성되고, SportsCar 객체가 생성된다.

 

dmbCellPhone이 SportsCar로 생각하면되고 CellPhone이 Car라고 생각하면 된다.

 

(출처: https://kephilab.tistory.com/56)

 

 

2. super 키워드

 

모든 객체는 클래스의 생성자를 호출해야만 생성된다. 부모 객체도 예외는 아니다. 그렇다면 부모 객체를 생성하기 위해 부모 생성자를 어디서 호출한 것인가?

 

이것에 대한 비밀은 자식 생성자에 숨어 있다. 부모 생성자는 자식 생성자의 맨 첫 줄에서 호출된다. 예를 들어 SportCar의 생성자가 명시적으로 선언되지 않았다면 컴파일러는 다음과 같은 기본 생성자를 생성해 낸다.

 

public SportsCar(){
	super();
}

 

첫줄에 super(); 가 추가된 것을 볼 수 있다. super()는 부모의 기본 생성자를 호출한다. 즉 Car 클래스의 다음 생성자를 호출한다.

public Car(){
	
}

 

만약 직접 자식 생성자를 선언하고 명시적으로 부모 생성자를 호출하고 싶다면 다음과 같이 작성하면 된다.

자식클래스(매개변수선언, ...){
	super(매개값, ...);
	...
}

 

다음의 예를 한번 보자

//부모클래스
public class People{
	public String name;
	public String ssn;
	
	public People(String name, String ssn){
		this.name = name;
		this.ssn = ssn;
	}
}

 

People 클래스는 기본 생성자가 없고 name과 ssn을 매개값으로 받아 객체를 생성시키는 생성자만 있다.

 

그렇기 때문에 People을 상속하는 Student 클래스는 생성자에서 super(name, ssn) 으로 People 클래스의 생성자를 호출해야 한다.

//자식클래스
public class Student extends People{
	public int studentNo;
	
	public Student(String name, String ssn, int studentNo){
		super(name, ssn);
		this.studentNo = studentNo;
	}
}

 

Student 클래스의 생성자는 name, ssn, studentNo를 매개값으로 받아서 name과 ssn은 다시 부모 생성자를 호출하기 위해 매개값으로 넘겨준다.

 

super(name, ssn); 을 주석처리하면 컴파일 오류가 발생한다. 이것은 부모의 기본 생성자가 없으니 다른 생성자를 명시적으로 호출하라는 것이다.

 

 

 

3. 메소드 오버라이딩

 

부모 클래스의 모든 메소드가 자식 클래스에게 맞게 설계되어 있다면 가장 이상적인 상속이지만, 어떤 메소드는 자식 클래스가 사용하기에 적합하지 않을 수도 있다.

 

이 경우 상속된 일부 메소드는 자식 클래스에서 다시 수정해서 사용해야 한다.

 

메소드 오버라이딩은 상속된 메소드의 내용이 자식 클래스에 맞지 않을 경우, 자식 클래스에서 동일한 메소드를 재정의하는 것을 말한다. 메소드가 오버라이딩되었다면 부모 객체의 메소드는 숨겨지기 때문에, 자식 객체에서 메소드를 호출하면 오버라이딩된 자식 메소드가 호출된다.

 

(출처: https://kephilab.tistory.com/56)

 

메소드를 오버라이딩할 때는 다음과 같은 규칙에 주의해서 작성해야 한다.

  • 부모의 메소드와 동일한 시그니처(리턴타입, 메소드이름, 매개변수리스트)를 가져야 한다.
  • 접근 제한을 더 강하게 오버라이딩할 수 없다.
  • 새로운 예외(Exception)를 throws할 수 없다

 

접근 제한을 더 강하게 오버라이딩할 수 없다는 것은 부모 메소드가 public 접근 제한을 가지고 있을 경우 오버라이딩하는 자식 메소드는 default나 private 접근 제한으로 수정할 수 없다는 뜻이다.

 

반대는 가능하다. 부모 메소드가 default 접근 제한을 가지면 재정의되는 자식 메소드는 default 또는 public 접근 제한을 가질 수 있다.

//부모클래스
public class Animal{
	public String eat(){
		return "냠냠";
	}
}

//자식클래스
public class Cow extends Animal{
	@Overrid
	public String eat(){
		retrun "쩝쩝";
	}
}

//메인
public class example{
	public static void main(String[] args){
		Animal animal = new Animal();
        System.out.println(animal.eat()); //냠냠
        
        Cow cow = new Cow();
        System.out.println(cow.eat()); //쩝쩝
        
	}
} 

 

부모 메소드 호출(super)

 

자식 클래스에서 부모 클래스의 메소드를 오버라이딩하게 되면, 부모 클래스의 메소드는 숨겨지고 오버라이딩된 자식 메소드만 사용된다.

 

그러나 자식 클래스 내부에서 오버라이딩된 부모 클래스의 메소드를 호출해야 하는 상황이 발생한다면 명시적으로 super 키워드를 붙여서 부모 메소드를호출할 수 있다.

 

super는 부모 객체를 참조하고 있기 때문에 부모 메소드에 직접 접근할 수 있다.

super.부모메소드();

 

(출처: https://kephilab.tistory.com/56)

 

Airplane 클래스를 상속해서 SuperSonicAirplane 클래스를 만들어보자.

 

Airplane의 fly() 메소드는 일반 비행이지만 SuperSonicAirplane의 fly() 는 초음속 비행 모드와 일반 비행 모드 두가지로 동작하도록 설계해보자.

//Airplane.java

public class Airplane{
	
	public void land(){
		System.out.println("착륙합니다.");
	}
	
	public void fly(){
		System.out.println("일반 비행합니다.");
	}
	
	public void takeOff(){
		System.out.println("이륙합니다.");
	}
}
//SuperSonicAirplane.java

public class SuperSonicAirplane extends Airplane{
	public static final int NORMAL = 1;
	public static finla int SUPERSONIC = 2;
	
	public int flyMode = NORMAL;
	
	@Override
	public void fly(){
		if(flyMode == SUPERSONIC){
			System.out.println("초음속 비행합니다.");
		}else{
			//Airplane 객체의 fly() 메소드 호출
			super.fly();
		}
	}
}
//SuperSonicAirplaneExample.java

public class SuperSonicAirplaneExample {
	public static void main(String[] args){
		SuperSonicAirplane sa = new SuperSonicAirplane();
		sa.takeOff();
		sa.fly();
		sa.flyMode = SuperSonicAirplane.SUPERSONIC;
		sa.fly();
		sa.flyMode = SuperSonicAirplane.NORMAL;
		sa.fly();
		sa.land();
	}
}
//결과
이륙합니다.
일반 비행합니다.
초음속 비행합니다.
일반 비행합니다.
착륙합니다.

 

4. 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)

 

Method Dispatch란 어떤 메소드를 호출할지 결정하여 실제로 실행시키는 과정을 말한다.

 

dispatch는 static dispatch와 dynamic dispatch가 있는데 static은 구현클래스를 이용해 컴파일타임에서부터 어떤 메서드가 호출될지 정해져있는것이고, dynamic은 인터페이스를 이용해 참조함으로서 호출되는 메서드가 동적으로 정해지는걸 말한다.

 

  • Static Dispatch

자바에서 객체 생성은 런타임시에 호출된다. 즉 컴파일타임에 알수있는건 타입에 대한 정보이다. 타입자체가 Dispatch라는 구현클래스이기때문에 해당 메서드를 호출하면 어떤 메서드가 호출될지 정적으로 정해진다. 이에대한 정보는 컴파일이 종료된 후 바이트코드에도 드러나게된다.

 

public class Test {
    public static void main(String[] arg) {
        Dispatch dispatch = new Dispatch();
        System.out.println(dispatch.method());
    }
}

class Dispatch{
    public String method(){
        return "hello dispatch";
    }
}

 

 

  • Dynamic Dispatch

인터페이스를 타입으로 메서드를 호출한다. 컴파일러는 타입에 대한 정보를 알고있으므로 런타임시에 호출 객체를 확인해 해당 객체의 메서드를 호출한다. 런타임시에 호출 객체를 알 수 있으므로 바이트코드에도 어떤 객체의 메서드를 호출해야하는지 드러나지 않는다.

 

예제코드에서 method() 메서드는 인자가 없는 메서드이지만 자바는 묵시적으로 항상 호출 객체를 인자로 보내게된다. 호출 객체를 인자로 보내기때문에 this를 이용해 메서드 내부에서 호출객체를 참조할 수 있는 것이다.

 

public class Test {
    public static void main(String[] arg) {
        Dispatchable dispatch = new Dispatch();
        System.out.println(dispatch.method());
    }
}

class Dispatch implements Dispatchable {
    public String method(){
        return "hello dispatch";
    }
}

interface Dispatchable{
    String method();
}

 

5. 더블 메소드 디스패치 (Double Method Dispatch)

 

 

Double Dispatch는 Dynamic Dispatch를 두번하는 것이다.

 

public class Test {
    public static void main(String[] arg) {
        List<SmartPhone> phoneList = Arrays.asList(new Iphone(), new Gallaxy());
        Game game = new Game();
        phoneList.forEach(game::play);
    }
}

interface SmartPhone{
}

class Iphone implements SmartPhone{

}

class Gallaxy implements SmartPhone{

}

class Game {
    public void play(SmartPhone phone) {
        System.out.println("game play [" +phone.getClass().getSimpleName()+ "]");
    }
}

 

스마트폰 리스트를 순회하면서 각각 게임을 하고있다. 스마트폰 리스트 인터페이스를 타입파라미터로 전달했기때문에 동적디스패치로 인해 출력내용은 모두 다르다. 앞에서 말했듯이 이는 인터페이스로 참조하고있는 객체 레퍼런스를 동적으로 추적하기때문이다.

 

만약 Game play가 스마트폰 구현체별로 다르게 구현되어야한다면 어떻게될까?

class Game {
    public void play(SmartPhone phone) {
    
        if(phone instanceof Iphone) {
            System.out.println("iphone play [" + phone.getClass().getSimpleName() + "]");
        }

        if(phone instanceof Gallaxy) {
            System.out.println("gallaxy play [" + phone.getClass().getSimpleName() + "]");
        }
    }
}

 

이런 방법이 있긴 하지만 만약 SmartPhone의 구현체로 Optimus가 추가된다면 Game 클래스까지 변경이 발생하게되어서 그렇게 효율적인 느낌은 아니다.

 

자바에서 지원을 하고말고는 논외로하고 잠깐 생각해본다면, 어차피 런타임시에 어떤 객체가 들어오는지를 확인해서 서로 다른 메서드를 호출해주는 동적 디스패치가 존재한다면 이를 인자에도 적용할 수 있지 않을까? 그렇게되면 알맞은 SmartPhone 구현체를 확인해 알아서 각각 메서드를 호출시켜주면 참 고마울것이다. 하지만 자바에서는 그런걸 지원하지않는다. 이런 이유로 자바를 싱글 디스패치(Single Dispatch) 언어라고 한다.

 

수정을 해보자

interface SmartPhone{
    void game(Game game);
}

class Iphone implements SmartPhone{
    @Override
    public void game(Game game) {
        System.out.println("iphone play [" + this.getClass().getSimpleName() + "]");
    }
}

class Gallaxy implements SmartPhone{
    @Override
    public void game(Game game) {
        System.out.println("gallaxy play [" + this.getClass().getSimpleName() + "]");
    }
}

class Game {
    public void play(SmartPhone phone) {
        phone.game(this);
    }
}

 

기존 Game클래스에 존재하던 비즈니스 로직을 각각 자기자신이 직접 처리하게끔 수정했다.

 

이때는 디스패치가 2번 일어나게되는데 play() 메서드를 찾기위한 정적 디스패치가 발생하고, game()메서드를 호출하는 객체를 찾기위한 동적 디스패치가 발생하게된다.

 

나는 지금 Game 클래스를 구현 클래스로 만들었기때문에 정적1번 동적1번이 발생하지만 Game 클래스역시 인터페이스를 기반으로 구현하여 인터페이스로 참조를 하게된다면 동적 디스패치가 2번 발생할것이다. 처음이 정적이든 동적이든 play() 메서드를 찾기위한 디스패치만 발생하던 기존 코드에서 play() 메서드 내부에 비즈니스로직을 호출하는 실제 객체를 찾기위한 동적 디스패치가 1번 더 발생하면서 더블 디스패치(Double Dispatch)가 되는 것이다.

 

 

6. 추상 클래스

 

객체를 직접 생성할 수 있는 클래스를 실체 클래스라고한다면 이 클래스들의 공통적인 특성을 추출해서 선언한 클래스를 추상 클래스라고 한다.

 

추상 클래스와 실체 클래스는 상속의 관계를 가지고 있다. 추상 클래스가 부모이고 실체 클래스가 자식으로 구현되어 실체 클래스는 추상 클래스의 모든 특성을 물려 받고, 추가적인 특성을 가질 수 있다. 여기서 특성이란 필드와 메소드들을 말한다.

 

예를 들어 Bird.class, Insect.class, Fish.class 등의 실체 클래스에서 공통되는 필드와 메소드를 따로 선언한 Animal.class 클래스를 만들 수 있는데, 이것이 바로 추상 클래스라고 볼 수 있다.

 

추상 클래스는 실체 클래스의 공통되는 필드와 메소드를 추출해서 만들었기 때문에 객체를 직접 생성해서 사용할 수 없다.

 

다시 말해서 추상 클래스는 new 연산자를 사용해서 인스턴스를 생성시키지 못한다

.

추상 클래스는 새로운 실체 클래스를 만들기 위해 부모 클래스로만 사용된다. 예를들어 Ant 클래스를 만들기 위한 Animal클래스는 다음과 같다.

class Ant extends Animal {...}

 

추상 클래스를 사용하는 용도는 두 가지가 있다.

 

  1. 실체 클래스들의 공통된 필드와 메소드의 이름을 통일할 목적
  2. 실체 클래스를 작성할 때 시간을 절약

 

예를 들어 자동차를 설계할 때에는 일반적인 타이어 규격에 맞추어서 작성해야 한다. 특정한 타이어만 사용할 수 있도록 자동차를 설계하지는 않는다.

 

일반적인 타티어 규격에 준수하는 어떠한 타이어든 부착할 수 있도록 하기 위해서이다. 여기서 타이어 규격은 타이어 추상 클래스라고 볼 수 있고, 타이어 규격에 준하는 한국 타이어나 금호 타이어는 추상 클래스를 상속하는 실체 타이어 클래스라고 볼 수 있다.

 

 

추상 클래스 선언

 

추상 클래스를 선언할 때에는 클래스 선언에 abstract 키워드를 붙여야 한다. abstract를 붙이게 되면 new 연산자를 이용해서 객체를 만들지 못하고 상속을 통해 자식 클래스만 만들 수 있다.

 

public abstract class 클래스{
	//필드
	//생성자
	//메소드
}

 

추상 클래스도 필드 , 생성자, 메소드를 선언할 수 있다. new 연산자로 직접 생성자를 호출할 수는 없지만 자식 객체가 생성될 때 super(..)를 호출해서 추상 클래스 객체를 생성하므로 추상 클래스도 생성자가 반드시 있어야 한다.

 

 

다음은 Phone 클래스를 추상 클래스로 선언한 것이다.

 

public abstract class Phone {
	//필드
	public String owner;
	
	//생성자
	public Phone(String owner){
		this.owner = owner;
	}
	
	//메소드
	public void turnOn(){
		System.out.println("폰 ON");
	}
	public void turnOff(){
		System.out.println("폰 OFF");
	}
}

 

다음은 Phone 추상 클래스를 상속해서 SmartPhone 자식 클래스를 정의한 것이다.

public class SmartPhone extends Phone{
	//생성자
	public SmartPhone(String owner){
		super(owner); //Phone의 생성자를 호출
	}
	//메소드
	public void internetSearch(){
		System.out.println("인터넷 검색");
	}
}

 

두개의 클래스를 이용하여 객체를 생성해보고 메소드도 호출해보자

public class PhoneExample{
	public static void main(String[] args){
		//Phone phone = new Phone(); 생성안됌
		
		SmartPhone smartPhone = new SmartPhone("홍길동");
		
		smartPhone.turnOn();
		smartPhone.internetSearch();
		smartPhone.turnOff();
	}
}

//결과
폰 ON
폰 OFF
인터넷 검색

 

추상 메소드와 오버라이딩

추상 클래스는 실체 클래스가 공통적으로 가져야 할 필드와 메소드들을 정의해 놓은 추상적인 클래스이므로 실체 클래스의 멤버(필드, 메소드) 를 통일화하는데 목적이 있다.

모든 실체들이 가지고 있는 메소드의 실행 내용이 동일하다면 추상 클래스에 메소드를 작성하는 것이 좋을 것이다. 하지만 메소드의 선언만 통일화하고, 실행 내용은 실체 클래스마다 달라야 하는 경우가 있다. 예를들어 모든 동물은 소리를 내기 때문에 Animal 추상 클래스에서 sound() 라는 메소드를 정의했다고 하자. 그렇다면 어떤 소리를 내도록 해야 하나느데, 이것은 실체에서 직접 작성해야될 부분임을 알게 된다.

 

추상 메소드는 추상 클래스에서만 선언할 수 있는데, 메소드의 선언부만 있고 메소드 실행 내용인 중괄호 {}가 없는 메소드를 한다.

 

추상 클래스를 설계할 때, 하위 클래스가 반드시 실행 내용을 채우도록 강요하고 싶은 메소드가 있을 경우, 해당 메소드를 추상 메소드로 선언하면 된다.

 

 

추상메소드를 선언하는 방법

[public | protected] abstract 리턴타입 메소드명(매개변수, ...);

일반 메소드와의 차이점은 abstract 키워드가 붙어있고 메소드 중괄호{}가 없다.

 

Animal 클래스를 추상 클래스로 선언하고 sound() 메소드를 추상 메소드로 선언한 것이다.

public abstract class Animal {
	public abstract void sound();
}

어떤 소리를 내는지는 결정할 수 없지만 동물은 소리를 낸다는 공통적인 특징이 있으므로 sound()메소드를 추상 메소드로 선언했다.

 

Animal 클래스를 상속하는 하위 클래스는 고유한 소리를 내도록 sound() 메소드를 재정의해야 한다. 예를들어 Dog는 "멍멍", Cat은 "야옹" 소리를 내도록 Dog, Cat 클래스에서 sound() 메소드를 재정의해야 한다.

public abstract class Animal { //추상클래스
	public String kind;
	
	public void breathe(){
		System.out.println("숨을 쉽니다.");
	}
	
	public abstract void sound(); // 추상메소드
}

public class Dog extends Animal {
	public Dog(){
		this.kind = "포유류";
	}
	
	@Override
	public void sound(){
		System.out.println("멍멍"); 
	}
}

 

Dog클래스의 sound()메소드를 재정의했다. 주석을 처리한다면 컴파일 에러가 발생한다.

 

 

7. final 키워드

 

final 키워드는 클래스, 필드, 메소드 선언 시에 사용할 수 있다.

 

final 키워드는 해당 선언이 최종 상태이고, 결코 수정될 수 없음을 뜻한다.

 

final 필드는 다음과 같이 선언한다

final 타입 필드 [=초기값]

final 필드의 초기값을 줄 수 있는 방법은 두가지 밖에 없다.

 

  1. 필드선언시 주는방법
  2. 생성자에서 주는방법

 

생성자는 final 필드의 최종 초기화를 마쳐야 하는데, 만약 초기화되지 않은 final 필드를 그대로 남겨두면 컴파일 에러가 발생한다.

 

public class Person {
	final String nation="korea";
	final String ssn;
	String name;
	
	public Person(String ssn, String name){
		this.ssn = ssn;
		this.name = name;
	}
}

 

주민등록번호 필드는 한 번 값이 저장되면 변경할 수 없도록 final 필드로 선언했다. 하지만 주민등록번호는 Person 객체가 생성될 때 부여되므로 Person 클래스 설계 시 초기값을 미리 줄 수 없다. 그래서 생성자 매개값으로 주민등록번호를 받아서 초기값으로 지정해주었다. 반면 nation은 항상 고정된 값을 갖기 때문에 필드 선언 시 초기 값으로 "korea"를 주었다.

 

public class PersonExample{
	public static void main(String[] args){
		Person p1 = new Person("123456-789102", "홍길동");
		
		System.out.println(p1.nation);
		System.out.println(p1.ssn);
		System.out.println(p1.name);
		
		//final 필드는 값 수정 불가
		//p1.nation = "USA";
		//p1.ssn = "456789-1234567"; 
		p1.name= "둘리";
	}
}

//결과
korea
123456-789102
홍길동

 

클래스를 선언할 때 final 키워드를 class 앞에 붙이게 되면 이 클래스는 최종적인 클래스이므로 상속할 수 없는 클래스가 된다. 즉, final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없다는 것이다.

public final class 클래스 {...}

 

final 클래스의 대표적인 예는 자바 표준 API에서 제공하는 String 클래스이다. String 클래스는 다음과 같이 선언되어 있다.

public final class String {...}

 

그래서 다음과 같이 자식 클래스를 만들 수 없다.

public class NewString extends String { ...} (X)

 

메소드를 선언할 때 final 키워드를 붙이게 되면 이 메소드는 최종적인 메소드이므로 오버라이딩 할 수 없는 메소드가 된다. 즉 부모 클래스를 상속해서 자식 클래스를 선언할 때 부모 클래스에 선언된 final 메소드는 자식 클래스에서 재정의할 수 없다는 것이다.

 

public final 리턴타입 메소드 ([매개변수, ...]) { ... }

 

다음 예제는 Car 클래스의 stop() 메소드를 final로 선언해서 Car를 상속한 SportsCar 클래스에서 stop() 메소드를 오버라이딩할 수 없음을 보여준다.

public class Car{
	//필드
	public int speed;
	
	//메소드
	public void speedUp(){ speed += 1; }
	
	//final메소드
	public final void stop(){
		System.out.println("차를 멈춤");
		speed = 0;
	}
}

public class SportsCar extends Car{
	@Override
	public void speedUp(){ speed += 10;}
	
	//stop메소드는 오버라이드 할 수 없음.
}

 

8. Object 클래스

 

클래스를 선언할 때 extends 키워드로 다른 클래스를 상속하지 않으면 암시적으로 java.lang.Object 클래스를 상속하게 된다. 따라서 자바의 모든 클래스는 Object 클래스의 자식이거나 자손 클래스이다.

 

Object 클래스는 필드가 없고, 메소드들로 구성되어 있다. 이 메소드들은 모든 클래스가 Object를 상속하기 때문에 모든 클래스에서 사용이 가능하다.

 

https://docs.oracle.com/javase/7/docs/api/ <<< 자바 API

 

Object 클래스에는 다양한 메소드들이있는데 그중에 clone() 메소드는 잘 몰라서 이 부분을 공부해 보았다.

 

 

객체 복제(Clone())

객체 복제는 원본 객체의 필드값과 동일한 값을 가지는 새로운 객체를 생성하는 것을 말한다. 객체를 복제하는 이유는 원본 객체를 안전하게 보호하기 위해서이다.

 

신뢰하지 않는 영역으로 원본 객체를 넘겨 작업할 경우 원본 객체의 데이터가 훼손될 수 있기 때문에 복제된 객체를 만들어 신뢰하지 않는 영역으로 넘기는 것이 좋다. 복제된 객체의 데이터가 훼손되더라도 원본 객체는 아무런 영향을 받지 않기 때문에 안전하게 데이터를 보호할 수 있게 된다. 객체를 복제하는 방법에는 얕은 복제와 깊은 복제가 있다.

 

  • 얕은 복제

얕은 복제(thin clone)란 단순히 필드값으로 복사해서 객체를 복제하는 것을 말한다. 필드값만 복제하기 때문에 필드가 기본 타입일 경우 값 복사가 일어나고, 필드가 참조 타입일 경우에는 객체의 번지가 복사된다.

 

예를 들어 원본 객체에 int 타입의 필드와 배열 타입의 필드가 있을 경우, 얕은 복제된 객체의 필드값은 다음과 같다.

 

 

Object의 clone() 메소드는 자신과 동일한 필드값을 가진 얕은 복제된 객체를 리턴한다. 이 메소드로 객체를 복제하려면 원본 객체는 반드시 java.lang.Cloneable 인터페이스를 구현하고 있어야 한다.

 

다음 예제를 보면 Member 클래스가 Cloneable 인터페이스를 구현했기 때문에 getMemeber() 메소드에서 clone() 메소드로 자신을 복제한 후, 복제한 객체를 외부로 리턴할 수 있다.

 

public class Memeber implements Cloneable{
	public String id;
	public String name;
	public String password;
	public int age;
	public boolean adult;
	
	public Member(String id, String name, String password, int age, boolean adult){
		this.id=id;
		this.name=name;
		this.password=password;
		this.age=age;
		this.adult=adult;
	}
	
	public Member getMember(){
		Member cloned = null;
		
		try{
			Cloned = (Member) clone(); //clone() 메소드의 리턴 타입은 Object이므로 Member 타입으로 캐스팅해야 함
		}catch(CloneNotSupportedException e){
			return cloned;
		}
	}
}

 

다음 예제를 보면 원본 Member를 복제한 후, 복제 Member의 password 필드값을 변경하더라도 원본 Member의 password 필드값은 변경되지 않음을 보여준다.

 

public class MemberExample {
	public static void main(String[] args) {
		//원본 객체 생성
		Member original = new Member("blue","홍길동","12345",25,true);
		
		//복제 객체를 얻은 후에 패스워드 변경
		Member cloned = original.getMember();
		cloned.password = "67890" //복제 객체에서 패스워드 변경
		
		System.out.println("[복제 객체의 필드값]");
		System.out.println("id : " + cloned.id);
		System.out.println("name : " + cloned.name);
		System.out.println("password : " + cloned.password);
		System.out.println("age : " + cloned.age);
		System.out.println("adult : " + cloned.adult);
		
		System.out.println();
		
		System.out.println("[원본 객체의 필드값]");
		System.out.println("id : " + original.id);
		System.out.println("name : " + original.name);
		System.out.println("password : " + original.password);
		System.out.println("age : " + original.age);
		System.out.println("adult : " + original.adult);
	}
}

//결과
[복제 객체의 필드값]
id : blue
name : 홍길동
password : 67890
age : 25
adult : true

[원본 객체의 필드값]
id : blue
name : 홍길동
password : 12345
age : 25
adult : true

 

  • 깊은 복제

얕은 복제의 경우 참조 타입 필드는 번지만 복제되기 때문에 원본 객체의 필드와 복제 객체의 필드는 같은 객체를 참조하게 된다. 만약 복제 객체에서 참조 객체를 변경 하면 원본 객체도 변경된 객체를 가지게 된다. 이것이 얕은 복제의 단점이다.

 

얕은 복제의 반대 개념은 깊은 복제이다. 깊은 복제란 참조하고 있는 객체도 복제하는 것을 말한다. 다음 그림은 원본 객체를 깊은 복제했을 경우 참조하는 배열 객체도 복제된다는 것을 보여준다.

 

 

깊은 복제를 하려면 Object의 clone() 메소드를 재정의해서 참조 객체를 복제하는 코드를 직접 작성해야한다. 다음 예제를 보면 Member 클래스에 int[] 배열과 Car 타입의 필드가 있다. 이 필드들은 모두 참조 타입이므로 깊은 복제 대상이 된다. Member 클래스는 Object의 clone() 메소드를 재정의해서 int[] 배열과 Car 객체를 복제한다.

 

public class Member implements Cloneable {
	public String name;
	public int age;
	public int[] scores; //깊은복제 대상
	public Car car; //깊은복제 대상
	
	public Member(String name, int age, int[] scores, Car car){
		this.name = name;
		this.age = age;
		this.scores = scores;
		this.car = car;
	}
	
	@Override
	protected Object clone() throws CloneNotSupportedException {
		//먼저 얕은 복사를 해서 name, age를 복제한다.
		Member cloned = (Member) super.clone(); //Object의 clone() 호출
		
		//scores를 깊은 복제한다.
		cloned.scores = Arrays.copyOf(this.scores, this.scores.length); //clone()메소드 재정의
		
		//car를 깊은 복제한다.
		cloned.car = new Car(this.car.model);
		//깊은 복제된 Member 객체를 리턴
		return cloned;
	}
	
	public Member getMember(){
		Member cloned = null;
		try {
			cloned = (Member) clone();
		}catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return cloned;
	}
}

public class Car{
	public String model;
	
	public Car(String model){
		this.model = model;
	}
}

 

 

다음 예제는 Member 클래스의 getMember() 메소드를 호출해서 복제된 Member 객체를 얻은 후에 scores 배열 항목값과 car 객체의 모델을 변경한다. 하지만 원본 Member 객체의 scores 배열 항목값과 car 객체의 모델은 변함이 없는 것을 볼 수 있다.

 

원본과 복제본이 각각 참조하는 scores 배열과 car 객체는 서로 다르기 때문이다.

 

깊은 복제 후 복제본 변경은 원본에 영향을 미치지 않는다.

 

public class MemberExample {
	public static void main(String[] args){
		//원본 객체 생성
		Member original = new Member("홍길동",25,new int[]{90,90}, new Car("소나타"));
		
		
		//복제 객체를 얻은 후에 참조 객체의 값을 변경
		Member cloned = original.getMember();
		cloned.scores[0] = 100;
		cloned.car.model = "그랜저";  //깊은 복제 후 참조 객체의 데이터를 변경
		
		System.out.println("[복제 객체의 필드값]");
		System.out.println("name : " + cloned.name);
		System.out.println("age : " + cloned.age);
		System.out.print("scores:{"); 
		for(int i=0; i<cloned.scores.length; i++){
			System.out.print(cloned.scores[i]);
			System.out.print((i==(cloned.scores.length-1))? "":",");
		}
		System.out.println("}");
		System.out.println("car: "+ cloned.car.model);
		
		
		System.out.println();
		
		
		
		System.out.println("[원본 객체의 필드값]");
		System.out.println("name : " + cloned.name);
		System.out.println("age : " + cloned.age);
		System.out.print("scores:{"); 
		for(int i=0; i<cloned.scores.length; i++){
			System.out.print(cloned.scores[i]);
			System.out.print((i==(cloned.scores.length-1))? "":",");
		}
		System.out.println("}");
		System.out.println("car: "+ cloned.car.model);
	
	}
}

 

 

 

 

참고 문헌


이것이 자바다

출처: https://multifrontgarden.tistory.com/133 [우리집앞마당]

728x90

'JAVA' 카테고리의 다른 글

8주차 : 인터페이스  (0) 2021.01.31
7주차 : 패키지  (0) 2021.01.02
5주차 : 클래스  (0) 2020.12.26
4주차 : 제어문  (0) 2020.12.26
3주차 : 연산자  (0) 2020.12.26