이것이 자바다 - Part 07

mj·2023년 1월 13일
0
post-thumbnail

Part 07 상속

상속 개념

상속은 부모가 자식에게 물려주는 행위를 말한다
객체 지향 프로그램에서도 부모 클래스의 필드와 메소드를 자식 클래스에게 물려줄 수 있다.

상속의 이점
1. 상속은 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여 개발 시간을 단축시킨다.
2. 부모 클래스를 수정하면 모든 자식 클래스에 수정 효과를 가져오므로 수정을 최소화할 수 있다.

클래스 상속

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

public class 자식클래스 extends 부모클래스 {
}

다중 상속
다른 언어와는 달리 자바는 다중 상속을 허용하지 않는다. 따라서 extends 뒤에는 단 하나의 부모 클래스만이 와야 한다.

  • 부모클래스
public class Phone {
    public String model;
    public String color;

    public void bell() {
        System.out.println("벨이 울립니다.");
    }

    public void sendVoice(String message) {
        System.out.println("자기: " + message);
    }

    public void receiveVoice(String message) {
        System.out.println("상대방: " + message);
    }

    public void hangUp() {
        System.out.println("전화를 끊습니다.");
    }
}
  • 자식클래스
public class SmartPhone extends Phone {
    public boolean wifi;

    public SmartPhone(String model, String color) {
        this.model = model;
        this.color = color;
    }
    public void setWifi(boolean wifi) {
        this.wifi = wifi;
        System.out.println("와이파이 상태를 변경했습니다.");
    }
    public void internet() {
        System.out.println("인터넷에 연결합니다.");
    }
}

부모 생성자 호출

자바에서는 자식 객체를 생성하면 부모 객체가 먼저 생성된 다음에 자식 객체가 생성된다.
즉 자식클래스의 객체를 생성할 때는 한개 객체만 색성되는 것처럼 보이지만 사실은 부모 클래스 객체가 먼저 생성되고 그 다음에 자식클래스 객체가 생성되는 것이다.

모든 객체는 생성자를 호출해야된다. 부모 객체의 생성자는 자식 객체 생성자의 맨 첫 줄에 숨겨져 있는 super() 에 의해 호출된다.

public 자식클래스(...) {
	super();
}

super() 는 컴파일 과정에서 자동 추가되는데, 이것은 부모의 기본 생성자를 호출한다. 만약 부모 클래스에 기본 생성자가 없다면 자식 생성자 선언에서 컴파일 에러가 발생한다.

부모 클래스에 기본 생성자가 없고 매개변수를 갖는 생성자만 있다면 개발자는 다음과 같이 super(매개값, ...) 코드를 직접 넣어야 한다.

public 자식클래스(...) {
	super(매개값, ...);
}

메소드 재정의

부모 클래스에서 정의된 메소드를 자식 클래스에서 알맞게 사용할려면 자식 클래스에서 메소드를 재정의해서 사용해야 한다. 이것을 메소드 오버라이딩 이라고 한다.

메소드 오버라이딩

메소드 오버라이딩은 상속된 메소드를 자식 클래스에서 재정의하는 것을 말한다.
메소드가 오버라이딩되었다면 해당 부모 메소드는 숨겨지고, 자식 메소드가 우선적으로 사용된다.

메소드 오버라이딩 주의사항

  • 부모 메소드의 선언부(리턴 타입, 메소드 이름, 매개변수)와 동일해야 한다.
  • 접근 제한을 더 강하게 오버라이딩 할 수 없다.(public -> private 으로 변경 불가)
  • 새로운 예외를 throws 하게 할 수 없다.
  • 부모 클래스
public class Calculator {
    public double areaCircle(double r) {
        System.out.println("Calculator 객체의 areaCircle() 실행");
        return 3.14159 * r * r;
    }
}
  • 자식 클래스
public class Computer extends Calculator {
    @Override	// 컴파일 시 정확히 오버라이딩이 되었는지 체크해 줌(생략 가능)
    public double areaCircle(double r) {
        System.out.println("Computer 객체의 areaCircle() 실행");
        return Math.PI * r * r;
    }
}

자바는 개발자의 실수를 줄여주기 위해 정확히 오버라이딩이 되었는지 체크해주는 @Override 어노레이션을 제공한다.

해당 어노테이션을 붙이면 컴파일 단계에서 정확히 오버라이딩이 되었는지 체크하고 문제가 있다면 컴파일 에러를 출력한다.

부모 메호드 호출

메소드를 재정의하면, 부모 메소드는 숨겨지고 자식 메소드만 사용되기 떄문에 비록 부모 메소드의 일부만 변경된다 하더라도 중복된 내용을 자식 메소드도 가지고 있어야 한다.

이러한 문제는 자식 메소드와 부모 메소드의 공동 작업 처리기법 을 이용하면 매우 쉽게 해결된다.

class Parent {
	public void method() {
    	//작업 처리1
    }
}
class Child extends Parent {
	@Override
    void method {
    	super.method();
        //작업 처리2;
    }
}

final 클래스와 final 메소드

필드 선언 시에 final을 붙이면 초기값 설정 후 값을 변경할 수 없다.
final 클래스와 final 메소드는 상속과 관련이 있다.

final 클래스

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

대표적인 예로 String 클래스가 있다.

public final class String{ ... }

final 메소드

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

final 메소드

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

protected 접근 제한자

protected 는 상속과 관련이 있고, public 과 default 이ㅡ 중간쯤에 해당하는 접근 제한을 한다.

접근제한자제한 대상제한 범위
protected필드, 생성자, 메소드같은 패키지이거나, 자식 객체만 사용 가능

protected는 같은 패키지에서는 default처럼 접근이 가능하나, 다른 패키지에서는 자식 클래스만 접근을 허용한다.

타입 변환

클래스에도 기본 타입과 마찬가지로 타입 변환이 있는데, 클래스의 타입 변환은 상속 관계에 있는 클래스 사이에서 발생한다.

자동 타입 변환

자동 타입 변환은 의미 그대로 자동적으로 타입 변환이 일어나는 것을 말한다.

자동 타입 변환 조건

부모타입 변수 = 자식타입객체 	//자식타입객체가 부모타입으로 자동 타입 변환
Cat cat = new Cat();
Animal animal = cat;
//Animal animal = new Cat()

부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다.
비록 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로 한정된다.

그러나 자식 클래스에서 오버라이딩된 메소드가 있다면 부모 메소드 대신 오버라이딩된 메소드가 호출된다.

public class Parent {
    public void method1() {
        System.out.println("Parent-method1()");
    }

    public void method2() {
        System.out.println("Parent-method2()");
    }
}
public class Child extends Parent {
    @Override
    public void method2() {
        System.out.println("Child-method2()");
    }

    public void method3() {
        System.out.println("Child-method3()");
    }
}
public class ChildExample {
    public static void main(String[] args) {
        Child child = new Child();

        Parent parent = child;

        parent.method1();
        parent.method2();
        // parent.method3(); (호출 불가능)
    }
}

알아둘 것

Parent obj = new Child;
  • 일단 변수의 타입 은 부모이므로 부모클래스 필드와 메소드에 한정해서 접근 가능
  • 변수에 저장된 객체 자체는 자식이므로 오버라이딩된 메소드나 재정의한 필드에 접근할 때는 자식의 멤버에 접근

다형성

다형성이란 사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질을 말한다.

객체 사용 방법이 동일하다는 것은 동일한 메소드를 가지고 있다는 뜻이다.

다형성을 구현하기 위해서는 자동 타입 변환과 메소드 재정의가 필요하다.

자동 타입변환

+

메소드 오버로딩

->

다형성

필드 다형성

필드 사형성은 필드 타입은 동일하지만, 대입되는 객체가 달라져서 실행 결과가 다양하게 나올 수 있는 것을 말한다.

public class Car {
	//필드 선언
    public Tire tire;
    
    //메소드 선언
    public void run() {
    	tire.roll();
    }
}
//Car 객체 생성
Car myCar = new Car();

//HankookTire 장착
myCar.tire = new HankookTire();

//KumhoTire 장착
myCar.tire = new KumhoTire();

매개변수 다형성

다형성은 필드보다는 메소드를 호출할 때 많이 발생한다.
메소드가 클래스 타입의 매개변수를 가지고 있을 경우, 호출할 때 동일한 타입의 객체를 제공하는 것이 정석이지만 자식 객체를 제공할 수도 있다.
여기서 다형성이 발생한다.

public class Vehicle {
    public void run() {
        System.out.println("차량이 달립니다.");
    }
}
public class Bus extends Vehicle {
    @Override
    public void run() {
        System.out.println("택시가 달립니다.");
    }
}
public class DriverExample {
    public static void main(String[] args) {
        Driver driver = new Driver();

        Bus bus = new Bus();
        driver.drive(bus);
    }
}

객체 타입 확인

매개변수의 다형성에서 실제로 어떤 객체가 매개값으로 제공되었는지 확인하는 방법이 있다.
꼭 매개변수가 아니더라도 변수가 참조하는 객체의 타입을 확인하고자 할 때, instanceof 연산자를 사용할 수 있다.

instanceof 연산자

  • instanceof 연산자의 좌항에는 객체가 오고 우항에는 타입이 오는데, 좌항의 객체가 우항의 타입이면 true를 산출하고, 그렇지 않으면 false를 산출한다.
boolean result = 객체 instanceof 타입;
  • instanceof 연산자를 사용하는 이유는 객체의 원래 타입을 확인해서 강제 타입 변환을 하기 위해서이다.
  • 강제 타입 변환을 하는 이유는 자식 객체의 모든 멤버에 접근하기 위해서이다.
if(parent instanceof Child child) {
	Child child = (child) parent;
    //child 변수 사용
}

Java 12 부터는 instanceof 연산의 결과가 true 일 경우, 우측 타입 변수를 사용할 수 있기 때문에 강제 타입 변환이 필요 없다.

if(parent instanceof Child child) {
	//child 변수 사용
}

추상 클래스

사전적 의미로 추상은 실체 간에 공통되는 특성을 추출한 것을 말한다.

추상 클래스란?

객체를 생성할 수 있는 클래스를 실체 클래스라고 한다면, 이 클래스들의 공통적인 필드나 메소드를 추출해서 선언한 클래스를 추상 클래스라고 한다.

추상 클래스는 실체 클래스의 부모 역할을 한다. 따라서 실체 클래스는 추상 클래스를 상속해서 공통적인 필드나 메소드를 물려받을 수 있다.

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

Animal animal = new Animal()	//x

추상 클래스 선언

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

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

추상 클래스도 필드, 메소드를 선언할 수 있다. 그리고 자식 객체가 생성될 때 super() 로 추상 클래스의 생성자가 호출되기 때문에 생성자도 반드시 있어야 한다.

추상 메소드와 재정의

자식 클래스들이 가지고 있는 공통 메소드를 뽑아내어 추상 클래스로 작성할 때, 메소드 선언부(리턴타입, 메소드명, 매개변수)만 동일하고 실행 내용은 자식 클래스마다 달라야 하는 경우가 많다.

이런 경우에 추상 클래스는 다음과 같은 추상 메소드를 선언할 수 있다.
일반 메소드 선언과의 차이점은 abstract 키워드가 붙고, 메소드 실행 내용인 중괄호{} 가 없다.

abstract 리턴타입 메소드명(매개변수, ...);

추상 메소드는 자식 클래스의 공통 메소드라는 것만 정의할 뿐, 실행 내용을 가지지 않는다.

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

봉인된 클래스

기본적으로 final 클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있다. 그러나 Java15 부터는 무분별한 자식 클래스 생성을 방지하기 위해 봉인된(sealed) 클래스가 도입되었다.

다음과 같이 Person의 자식 클래스는 Employee와 Manager만 가능하고, 그 외에는 자식 클래스가 될 수 없도록 Person 을 봉인된 클래스로 선언할 수 있다.

public sealed class Person permits Employee, Manager { ... }

sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 클래스를 지정해야 한다.

봉인된 Person 클래스를 상속하는 Employee와 Manager는 final 또는 non-sealed 키워드로 다음과 같이 선언하거나, sealed 키워드를 사용해서 또 다른 봉인 클래스로 선언해야 한다.

public final class Employee extends Person { ... }
public non-sealed class Manager extends Person { ... }

봉인된 클래스의 자식 클래스

  • final : sealed 된 클래스를 상속한 자식클래스를 더 이상 상속할 수 없다는 뜻이다.
  • non-sealed : 봉인을 해제해서 자식 클래스를 상속받는 또 다른 클래스를 만들 수 있다는 뜻이다.

문제

  1. 자바의 상속에 대한 설명 중 틀린 것은 무엇입니까?
    ➊ 자바는 다중 상속을 허용한다.
    ➋ 부모의 메소드를 자식 클래스에서 재정의(오버라이딩)할 수 있다.
    ➌ 부모의 private 접근 제한을 갖는 필드와 메소드는 상속의 대상이 아니다.
    ➍ final 클래스는 상속할 수 없고, final 메소드는 오버라이딩할 수 없다.
  • 답: ➊
  1. 클래스 타입 변환에 대한 설명 중 틀린 것은 무엇입니까?
    ➊ 자식 객체는 부모 타입으로 자동 타입 변환된다.
    ➋ 부모 객체는 어떤 자식 타입으로도 강제 타입 변환된다.
    ➌ 자동 타입 변환을 이용해서 필드와 매개변수의 다형성을 구현한다.
    ➍ 강제 타입 변환 전에 instanceof 연산자로 변환 가능한지 검사하는 것이 좋다.
  • 답 : ➋
  1. final 키워드에 대한 설명으로 틀린 것은 무엇입니까?
    ➊ final 클래스는 부모 클래스로 사용할 수 있다.
    ➋ final 필드는 초기화된 후에는 변경할 수 없다.
    ➌ final 메소드는 재정의(오버라이딩)할 수 없다.
    ➍ static final 필드는 상수를 말한다.
  • 답 : ➊
  1. 오버라이딩(Overriding)에 대한 설명으로 틀린 것은 무엇입니까?
    ➊ 부모 메소드의 시그너처(리턴 타입, 메소드명, 매개변수)와 동일해야 한다.
    ➋ 부모 메소드보다 좁은 접근 제한자를 붙일 수 없다.(예: public (부모)  private (자식) ).
    ➌ @Override 어노테이션을 사용하면 재정의가 확실한지 컴파일러가 검증한다.
    ➍ protected 접근 제한을 갖는 메소드는 다른 패키지의 자식 클래스에서 재정의할 수 없다.
  • 답 : ➍
  1. 추상 클래스에 대한 설명으로 틀린 것은 무엇입니까?
    ➊ 직접 객체를 생성할 수 없고, 상속만 할 수 있다.
    ➋ 추상 메소드를 반드시 가져야 한다.
    ➌ 추상 메소드는 자식 클래스에서 재정의(오버라이딩)할 수 있다.
    ➍ 추상 메소드를 재정의하지 않으면 자식 클래스도 추상 클래스가 되어야 한다.
  • 답 : ➋
  1. Parent 클래스를 상속해서 Child 클래스를 다음과 같이 작성했는데, Child 생성자에서 컴파일
    에러가 발생했습니다. 그 이유와 해결 방법을 설명해보세요.
public class Parent {
	public String name;

    public Parent(String name) {
 		this.name = name;
	}
}
public class Child extends Parent {
	public int studentNo;
	public Child(String name, int studentNo) {
 		this.name = name;
 		this.studentNo = studentNo;
	}
}
  • 답 : 부모 생성자가 기본 생성자가 아닌데 자식 클래스에서 부모 생성자 호출을 하지 않았다. this.name = name; 줄을 지우고, super(name); 코드를 넣는다.
  1. Parent 클래스를 상속받아 Child 클래스를 다음과 같이 작성했습니다. ChildExample 클래스
    를 실행했을 때 호출되는 각 클래스의 생성자의 순서를 생각하면서 출력 결과를 작성해보세요.
public class Parent {
	public String nation;
	public Parent() {
 		this("대한민국");
 		System.out.println("Parent() call");
	}
	public Parent(String nation) {
 		this.nation = nation;
 		System.out.println("Parent(String nation) call");
	}
}
public class Child extends Parent {
	public String name;
	public Child() {
 		this("홍길동");
 		System.out.println("Child() call");
	}
	public Child(String name) {
 		this.name = name;
 		System.out.println("Child(String name) call");
	}
}
public class ChildExample {
	public static void main(String[] args) {
 		Child child = new Child();
	}
}
  • 답 :
    Parent(String nation) call
    Parent() call
    Child(String name) call
    Child() call
  1. Tire 클래스를 상속받아 SnowTire 클래스를 다음과 같이 작성했습니다. SnowTireExample
    클래스를 실행했을 때 출력 결과를 작성해보세요.
public class Tire {
	public void run() {
 		System.out.println("일반 타이어가 굴러갑니다.");
	}
}
public class SnowTire extends Tire {
	@Override
	public void run() {
 		System.out.println("스노우 타이어가 굴러갑니다.");
	}
}
public class SnowTireExample {
	public static void main(String[] args) {
 		SnowTire snowTire = new SnowTire();
 		Tire tire = snowTire;

 		snowTire.run();
 		tire.run();
	}
}
  • 답 :
    스노우 타이어가 굴러갑니다.
    스노우 타이어가 굴러갑니다.
  1. A, B, C, D, E, F 클래스가 다음과 같이 상속 관계에 있을 때 다음 빈칸에 들어올 수 없는 코드를
    선택하세요.
  • 답 : ➋
  1. 다음과 같이 작성한 Computer 클래스에서 컴파일 에러가 발생했습니다. 그 이유를 설명해보세요.
public abstract class Machine {
	public void powerOn() { }
	public void powerOff() { }
	public abstract void work();
}
	public class Computer extends Machine {
}
  • 답 : 추상 클래스를 상속받는 Computer 클래스에서 추상 메소드인 work() 를 재정의하지 않았다.
  1. MainActivity의 onCreate()를 실행할 때 Activity의 onCreate()도 실행시키고 싶습니다.
    밑줄에 들어갈 코드를 작성해보세요
public class Activity {
	public void onCreate() {
 		System.out.println("기본적인 실행 내용");
	}
}
public class MainActivity extends Activity {
 	@Override
 	public void onCreate() {
 		____________.onCreate();
 		System.out.println("추가적인 실행 내용");
 	}
}
  • 답 : super
  1. 다음과 같은 Example 클래스에서 action() 메소드를 호출할 때 매개값이 C 객체일 경우에만
    method2()가 호출되도록 밑줄에 들어갈 코드를 작성해보세요.

  • 답 : a instanceof C c
profile
사는게 쉽지가 않네요

0개의 댓글