[포스코x코딩온] 웹개발자 풀스택 과정 17주차 | 추상 클래스, 추상 메소드, 인터페이스, 다형성

구준희·2023년 10월 25일
0

[포스코x코딩온]교육

목록 보기
38/40
post-thumbnail
post-custom-banner

추상 클래스 & 추상 메서드

자바에서는 abstract 키워드를 클래스명과 메서드 명 앞에 붙임으로서 컴파일러에게 추상 클래스와 추상 메서드임을 알려주게 된다.

추상 메서드는 작동 로직은 없고 이름만 껍데기만 있는 메서드라고 보면 된다. 즉, 메서드의 선언부만 작성하고 구현부는 미완성인 채로 남겨둔 메소드 인 것이다. (메소드의 구현부인 중괄호가 없는 형태)

보통 문법적인 측면으로 하나 이상의 추상 메소드를 포함하는 클래스를 가리켜 추상 클래스라고 정의 하기도 한다.

추상 클래스 안에 메서드를 미완성으로 남겨놓는 이유는 추상 클래스를 상속받는 자식 클래스의 주제에 따라서 상속 받는 메서드의 내용이 달라질 수 있기 때문이다.

부모(추상) 클래스에서 메서드를 선언부만을 작성하고, 실제 내용은 상속받는 클래스에서 구현하도록 하기 위해 일부러 비워두는 개념이라고 보면 된다.

따라서 추상 클래스를 상속받는 자식 클래스는 부모의 추상 메서드를 상황에 맞게 적절히 재정의 하여 구현해주어야 비로소 사용이 가능해진다.

즉, 클래스의 선언부에 abstract 키워드가 있다는 말은 안에 추상메서드(abstract method)가 있으니 상속을 통해서 구현해주라는 지침이기도 하다.

// 추상 클래스
abstract class Pet {
    abstract public void walk(); // 추상 메소드
    abstract public void eat(); // 추상 메소드
    
    public int health; // 인스턴스 필드
    public void run() {  // 인스턴스 메소드
    	System.out.println("run run");
    }
}

class Dog extends Pet {
	// 상속 받은 부모(추상) 메소드를 직접 구현
    public void walk() {
        System.out.println("Dog walk");
    }
    public void eat() {
    	System.out.println("Dog eat");
    }
}

public class main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.eat(); // 부모(추상) 클래스로 부터 상속받은 추상 메소드를 직접 구현한 메소드를 실행
        d.walk();
        d.run(); // 부모(추상) 클래스의 인스턴스 메소드 실행
    }
}

추상 클래스 생성자

추상 클래스는 클래스의 일종이라고 하지만 new 생성자를 통해 인스턴스 객체로 직접 만들 수 없다. 왜냐하면 추상클래스는 상속 구조에서 부모 클래스를 나타내는 역할로만 이용되기 때문이다.

//틀린 코드
abstract class Animal{
}

Animal a = new Animal();
//에러!! -> 추상 클래스는 인스턴스를 직접 바로 생성할 수 없음

//옳은 코드
abstract class Animal { 
}

class Cat extends Animal { // 추상 클래스 상속
}

class Dog extends Animal { 
}

public class Main {
    public static void main(String[] args) {
		// 추상 클래스를 상속한 자식 클래스를 객체로 초기화
        Cat c = new Cat(); 
        Dog d = new Dog();
    }
}

그렇다고 추상 클래스의 생성자를 전혀 이용 못하는 것은 아니다. 직접적인 인스턴스화가 불가능 하다 뿐이지, super() 메소드를 이용해 추상 클래스 생성자 호출이 가능하다.

// 추상 클래스
abstract class Shape {
	public String type;
    
    // 추상 클래스 생성자
    public Shape(String type) {
    	this.type = type;
    }
    
    // 추상 메서드
    public abstract void draw();
}

class Figure extends Shape {
	public String name;
    
    public Figure(String type1, String type2) {
    	super(type1); // 부모 추상 클래스 생성자 호출
        name = type2;
    }
    
    @Override
    public void draw() { ... } // 추상 메서드 구현
}

public class main {
    public static void main(String[] args) {
		Figure f = new Figure("polygon", "square");
        f.name; // "square"
        f.type; // "polygon" - 부모(추상) 클래스의 멤버를 추상 클래스 생성자를 호출하는 super()을 통해 초기화
    }
}

추상 클래스를 상속한 자식 클래스를 new 생성자로 객체를 초기화 할 때, 자식 클래스 생성자 메소드 내에서 가장 먼저 부모 클래스인 추상 클래스의 생성자가 실행되게 된다.
그래서 만일 위와 같이 부모 추상 클래스 생성자 실행에 있어 인자를 주어 제어를 하고 싶다면, 자식 클래스 생성자 메서드 내에서 super() 부모 생성자 호출 메서드를 통해 가능하다.

인터페이스

인터페이스 정의

  • 인터페이스를 작성하는 것은 추상 클래스를 작성하는 것과 같다고 보면된다.(추상 메서드 집합)
  • 인터페이스도 필드를 선언할 수 있지만 변수가 아닌 상수(final)로서만 정의할 수 있다.
  • public static finalpublic abstract 제어자는 생략이 가능하다.
    인터페이스에 정의된 모든 멤버에 적용되는 사항이기 때문에 편의상 생략 가능하게 지원하는 것이다. 생략된 제어자는 컴파일 시에 컴파일러가 자동으로 추가해준다.
interface 인터페이스이름{
	public static final 타입 상수이름 =;
    public abstract 타입 메서드이름(매개변수목록);
}

====

interface TV{
	int maxVolume = 10; //public static final 생략 가능
    int minVolume = 10;
    
    void turnOn(); // public abstract 생략 가능
    void turnOff();
    void changeVolume(int volume);
    void changeChannel(int channel);
}

인터페이스 구현

  • 인터페이스도 추상 클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상 클래스가 상속을 통해 완성되는 것처럼 인터페이스도 구현부를 만들어주는 클래스에 구현(상속) 되어야 한다.

  • 해당 클래스에 인터페이스를 구현하고 싶다면, implements 키워드를 쓴 후에 인터페이스를 나열하면 된다.

  • 인터페이스를 상속 받았으면, 자식 클래스에서 인터페이스가 포함하고 있는 추상 메소드를 구체적으로 구현해준다.

  • 인터페이스의 가장 큰 특징은 여러개를 다중 구현(다중 상속)이 가능하다는 것이다.

  • 자식 클래스에 클래스 상속(extends)와 인터페이스 구현(implements)는 동시에 가능하다.

interface Animal { 
	public abstract void cry(); 
}
interface Pet { 
	public abstract void play(); 
}
class Tail {
	// ...
}

class Cat extends Tail implements Animal, Pet { // 클래스와 인터페이스를 동시에 상속

    public void cry() {
        System.out.println("냐옹냐옹!");
    }
    public void play() {
        System.out.println("쥐 잡기 놀이하자~!");
    }
}

인터페이스도 따지고 보면 상속이지만 extends 키워드 대신 implements라는 '구현'이라는 키워드를 사용하는 이유는, 상속은 클래스간의 부모 - 자식 관계를 연관 시키는데 의미가 중점이 된다면, 구현은 클래스를 확장시켜 다양히 이용하는데 중점이 되기 때문이다.

인터페이스를 구현받고 추상 메서드를 구체적으로 구현할 때 접근제어자 설정에 주의해야 한다.
기본적으로 메서드를 오버라이딩(overriding) 할 때는 부모의 메서드보다 넓은 범위의 접근제어자를 지정해야 한다는 규칙이 존재한다. 따라서 인터페이스의 추상 메소드는 기본적으로 public abstract가 생략된 상태이기 때문에 반드시 자식 클래스의 메서드 구현부에서는 제어자를 public으로 설정해 주어야 한다.

인터페이스 자체 상속

  • 클래스끼리 상속을 통해 확장을 하듯이, 인터페이스 자체를 확장시키고 싶다면 extends를 통해 인터페이스를 상속하면 된다.
  • 클래스와 달리 인터페이스 끼리의 상속은 다중 상속이 가능하다.(메소드 구현부가 없으니 충돌 가능성이 없음)
  • 클래스의 상속과 마찬가지로 자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다.
    그러나 필드의 경우 기본적으로 static이기 때문에 구현체를 따라가지 않게 된다.(독립상수)

자바는 클래스 상속에 대해서 다중상속을 허용하지 않지만, 인터페이스에 한해서는 다중상속을 허용한다.

interface Changeable{
	//채널을 바꾸는 기능의 메서드
	void change();
}
interface Powerable{
	//전원을 껐다 켰다 하는 메서드
    void power(boolean b);
}

//채널 기능과 전원 기능을 가진 인터페이스들을 하나의 인터페이스로 통합 상속
interface Controable extends Changeable, Powerable {
	//인터페이스끼리 다중 상속하면 그대로 추상 멤버들을 물려받음
}

//클래스에 통합된 인터페이스를 그대로 상속
class MyObject implements Controlable{
	public void change(){
    	System.out.println("채널의 기능을 바꾸는 메서드");
    }
    public void power(boolean b){
    	System.out.println("전원을 껐다켰다 하는 메서드");
    }
}

public class Main {
	public static void main(String[] args) {
        // 인터페이스 다형성 (인터페이스를 타입으로 취급해서 업캐스팅 가능)
        Controlable[] o = { new MyObject(), new MyObject() };
        o[0].change();
        o[0].power(true);
        
        // 각각 단일 인터페이스로도 타입으로 사용이 가능하다. (그러나 지니고 있는 추상 메서드만 사용이 가능하다)
        Changeable inter1 = new Changeable();
        inter1.change(); 

        Powerable inter2 = new Powerable();
        inter2.power(true);
    }
}

인터페이스 상수 필드 상속 관계

클래스 상속일 경우 클래스 필드 멤버끼리 상속되어 덮어 씌워지지만, 인터페이스 필드들은 모두 public static final이기에, 서로 상속을 해도 독립적으로 운용된다.

인터페이스 vs 추상클래스 비교

인터페이스 정리

  • 내부의 모든 메서드는 public abstract로 정의 (default 메소드 제외)
  • 내부의 모든 필드는 public static final 상수
  • 클래스에 다중 구현 지원
  • 인터페이스 끼리는 다중 상속지원
  • 인터페이스에도 static, default, private 제어자를 붙여 클래스 같이 구체적인 메서드를 가질 수 있음
    따라서 하위 멤버의 중복 메서드 통합을 어느정도 할 수는 있겠지만, 필드는 상수이기 때문에 중복 필드 통합은 불가능
  • 인터페이스는 부모자식 관계인 상속에 얽매이지 않고, 공통 기능이 필요할 때마다 추상 메서드를 정의해놓고 구현(implement)하는 식으로 추상클래스보다 자유롭게 붙였다 땟다 사용
  • 인터페이스는 클래스와 별도로 구현 객체가 같은 동작은 한다는 것을 보장하기 위해 사용하는 것에 초점
  • 다중 구현이 된다는 점을 이용해, 내부 멤버가 없는 빈 껍데기 인터페이스를 선언하여 마커 인터페이스로서 이용 가능

추상 클래스 정리

  • 추상 클래스는 하위 클래스들의 공통점을 모아 추상화하여 만든 클래스
  • 추상 클래스는 다중 상속이 불가능하여 단일상속만 허용한다.
  • 추상 클래스는 추상 메소드 외에 일반클래스와 같이 일반적인 필드, 메서드, 생성자를 가질 수 있다.
  • 이러한 특징으로 추상클래스는 추상화(추상메서드)를 하면서 중복되는 클래스 멤버들을 통합 및 확장할수 있다.
  • 같은 추상화인 인터페이스와 다른 점은, 추상클래스는 클래스간의 연관 관계를 구축하는 것에 초점을 둔다.

다형성(polymorphism)

다형성이란 하나의 객체가 여러가지 타입을 가질 수 있는 것을 의미한다.
자바에서는 이러한 다형성을 부모 클래스 타입의 참조 변수로서 자식 클래스 타입의 인스턴스를 참조할수 있도록 하여 구현하고 있다.\

참조변수의 다형성

자바에서는 다형성을 위해 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조할 수 있도록 하고있다.

이때 참조변수가 사용할 수 있는 멤버의 개수가 실제 인스턴스의 멤버 개수보다 같거나 적어야 참조가 가능하다.

//예제
class Parent { ... }
class Child extends Parent { ... }

...

Parent pa = new Parent(); // 허용
Child ch = new Child();   // 허용
Parent pc = new Child();  // 허용
Child cp = new Parent();  // 오류 발생.
  1. 특정타입의 참조변수로는 같은 타입의 인스턴스 참조 가능
    -> 참조변수가 사용할 수 있는 멤버의 개수 = 실제 인스턴스 멤버 개수

  2. 부모 클래스 타입의 참조 변수로도 자식 클래스 타입의 인스턴스 참조 가능
    -> 참조변수 사용할 수 있는 멤버의 개수 < 실제 인스턴스 멤버 개수

  3. 자식 클래스 타입의 참조 변수로는 부모 클래스 타입의 인스턴스를 참조 X
    -> 참조변수가 사용할 수 있는 멤버의 개수 > 실제 인스턴스 멤버 개수

클래스는 상속을 통해 확장될 수는 있어도 축소될 수는 없으므로, 항상
자식 클래스 멤버개수 >= 부모 클래스 멤버 개수 이다.

참조변수의 타입 변환

참조변수는 다음과 같은 조건에 따라 타입변환이 가능하다.

  1. 서로 상속관계에 있는 클래스 사이에서만 타입 변환
  2. 자식 클래스 타입에서 부모 클래스 타입변환 생략 가능
  3. 하지만 부모 클래스 타입에서 자식 클래스 타입으로의 타입 변환은 반드시 명시해야 한다.
(변환할 타입의 클래스 이름) 변환할참조변수

//예제
class Parent { ... }
class Child extends Parent { ... }
class Brother extends Parent { ... }

...

Parent pa01 = null;
Child ch = new Child();
Parent pa02 = new Parent();
Brother br = null;

pa01 = ch;          // pa01 = (Parent)ch; 와 같으며, 타입 변환을 생략할 수 있음.
br = (Brother)pa02; // 타입 변환을 생략할 수 없음.
br = (Brother)ch;   // 직접적인 상속 관계가 아니므로, 오류 발생.

instanceof 연산자

이러한 다형성으로 인해 런타임에 참조 변수가 실제로 참조하고 있는 인스턴스의 타입을 확인할 필요성이 생긴다. 자바에서는 instanceof라는 연산자를 제공하여, 참조변수가 참조하고 있는 인스턴스의 실제 타입을 확인할 수 있도록 해준다.

참조변수 instanceof 클래스이름 

왼쪽에 전달된 참조 변수가 실제로 참조하고 있는 인스턴스의 타입이 오른쪽에 전달된 클래스 타입이면 true를 반환하고, 아니면 false를 반환한다.
만약에 참조 변수가 null을 가리키고 있으면 false를 반환한다.

// 참조변수가 실제로 가지키고 있는 인스턴스 타입을
// instanceof 연산자로 확인하는 예제

class Parent { }
class Child extends Parent { }
class Brother extends Parent { }
 
public class Polymorphism01 {
    public static void main(String[] args) {
        Parent p = new Parent();
        System.out.println(p instanceof Object); // true
        System.out.println(p instanceof Parent); // true
        System.out.println(p instanceof Child);  // false
        System.out.println();
        
        Parent c = new Child();
        System.out.println(c instanceof Object); // true
        System.out.println(c instanceof Parent); // true
        System.out.println(c instanceof Child);  // true
    }
}

//실행결과
true
true
false

true
true
true

출처
https://www.tcpschool.com/java/java_polymorphism_concept
https://www.tcpschool.com/java/java_inheritance_overriding
https://inpa.tistory.com/객체지향 클래스문법
https://inpa.tistory.com/추상클래스
https://inpa.tistory.com/인터페이스vs 추상클래스

profile
꾸준히합니다.
post-custom-banner

0개의 댓글