[Java] 다형성과 추상화

Nakjoo·2022년 12월 30일
0

[SEB_BE_43]

목록 보기
12/29

1. 다형성

다형성(polymorphism)이란 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미한다.

구체적으로, 상위 클래스 타입의 참조변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것이라 할 수 있다.

class Friend {
	public void friendInfo() {
    	System.out.println("나는 당신의 친구입니다.");
    }
}

class BoyFriend extends Friend {
	public void friendInfo() {
    	System.out.println("나는 당신의 남자친구입니다.");
    }
}

class GirlFriend extends Friend {
	public void friendInfo() {
    	System.out.println("나는 당신의 여자친구입니다.");
    }
}

public class FriendTest {

	public static void main(String[] args) {
    	Friend friend = new Friend(); //	객체 타입과 참조변수 타입의 일치
        BoyFriend boyfriend = new BoyFriend();
        Friend girlFriend = new GirlFriend(); // 객체 타입과 참조변수 타입의 불일치
        
        friend.friendInfo();
        boyfriend.friendInfo();
        girlfriend.friendInfo();
    }
}

// 출력값
나는 당신의 친구입니다.
나는 당신의 남자친구입니다.
나는 당신의 여자친구입니다.

위의 예제처럼 상위 클래스의 타입인 Friend의 참조변수 girlFriend에 하위 클래스의 타입인 GirlFriend 클래스의 인스턴스를 생성해 할당하였다.

이 경우, 상위 클래스를 참조변수 타입으로 지정했기 때문에 자연스럽게 참조변수가 사용할 수 있는 멤버의 개수는 상위 클래스의 멤버의 수가 된다.

하지만 반대로 하위 클래스 타입으로 상위 클래스 타입의 객체를 참조하는 것은 불가능하다.

그 이유는, 실제 참조하고 있는 인스턴스의 멤버를 기준으로 참조 변수의 타입 멤버가 실제 인스턴스의 멤버 수보다 작은 것은 실제 사용할 수 있는 기능을 줄이는 것이기에 허용되지만, 그 반대의 경우는 참조하고 있는 인스턴스에 실제로 구현된 기능이 없어 사용이 불가능하기 때문이다.

2. 참조변수의 타입 변환

기본 자료형의 형변환처럼 참조변수도 타입변환이 가능하다.

타입변환을 위해서는 세 가지 조건이 필요하다.

1. 서로 상속관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능하다.
2. 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형변환 연산자(괄호)를 생략할 수 있다.
3. 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형변환 연산자(괄호)를 반드시 명시해야한다.

public class VehicleTest {
	public static void main(String[] args) {
    	Car car = new Car(); 
        Vehicle vehicle = (Vehicle) car; // 상위 클래스 Vehicle 타입으로 변환(생략 가능)
        Car car2 = (car) vehicle; // 하위 클래스 Car 타입으로 변환(생략 불가능)
        MotorBike motorBike = (MotorBike) car; // 상속관계가 아니므로 타입 변환 불가 -> 에러 발생
    }
}

class Vehicle {
	...
}

class Car extends Vehicle {
	...
}

class MotorBike extends Vehicle {
	...
}

2.1. instanceof 연산자

intanceof 연산자캐스팅이 가능한 지 여부를 boolean 타입으로 확인할 수 있는 자바의 문법요소이다.

참조_변수 instanceof 타입

만약 참조_변수 instaceof 타입을 입력했을 때 리턴 값이 true가 나오면 참조 변수가 검사한 타입으로 타입 변환이 가능하며, 반대로 false가 나오는 경우에는 타입 변환이 불가능하다.

public class InstanceOfExample {
    public static void main(String[] args) {
        Animal animal = new Animal();
        System.out.println(animal instanceof Object); //true
        System.out.println(animal instanceof Animal); //true
        System.out.println(animal instanceof Bat); //false

        Animal cat = new Cat();
        System.out.println(cat instanceof Object); //true
        System.out.println(cat instanceof Animal); //true
        System.out.println(cat instanceof Cat); //true
        System.out.println(cat instanceof Bat); //false
    }
}

class Animal {};
class Bat extends Animal{};
class Cat extends Animal{};

3. 추상화

추상화객체의 공통적인 속성과 기능을 추출하여 정의하는 것을 의미한다.

상속이 하위 클래스를 정의하는데 상위 클래스를 사용하는 것이라고 한다면, 추상화는 반대로 기존 클래스들의 공통적인 요소들을 뽑아서 상위 클래스를 만들어 내는 것이라고 할 수 있다.

3.1. abstract 제어자

abstract는 주로 클래스와 메서드를 형용하는 키워드로 사용디는데, 메서드 앞에 붙은 경우를 '추상 메서드(abstract method)', 클래스 앞에 붙은 경우를 '추상 클래스(abstract class)'라 부른다.

어떤 클래스에 추상 메서드가 포함되어있는 경우 해당 클래스는 자동으로 추상 클래스가 된다.

abstract class AbstractExample { // 추상 메서드가 최소 하나 이상 포함돼있는 추상 클래스
	abstract void start(); // 메서드 바디가 없는 추상 메서드
}

참고로 추상 클래스는 객체 생성이 불가능하다.

3.2. 추상 클래스

추상 클래스를 만드는 이유는

첫 번째로 상속 관계에 있어 새로운 클래스를 작성하는데 매우 유용하다.

메서드의 내용이 상속을 받는 클래스에 따라서 종종 달라지기 때문에 상위 클래스에서는 선언부만을 작성하고, 실제 구체적인 내용은 상속을 받는 하위 클래스에서 구현하도록 비워둔다면 설계하는 상황이 변하더라도 보다 유연하게 대응 가능하다.

이 때 사용하게 되는 것이 '메소드 오버라이딩'이다.

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

class Dog extends Animal { // Animal 클래스로부터 상속
	public Dog() {
		this.kind = "포유류";
	}

	public void sound() { // 메서드 오버라이딩 -> 구현부 완성
		System.out.println("멍멍");
	}
}

class Cat extends Animal { // Animal 클래스로부터 상속
	public Cat() {
		this.kind = "포유류";
	}

	public void sound() { // 메서드 오버라이딩 -> 구현부 완성
		System.out.println("야옹");
	}
}

class DogExample {       
    public static void main(String[] args) throws Exception {
       Animal dog = new Dog();
       dog.sound();

       Cat cat = new Cat();
       cat.sound();
    }
 }

// 출력값
멍멍
야옹

위의 예제에서 먼저 Animal 클래스 안에 abstract 키워드를 사용한 sound() 메서드가 추상 메서드로 선언되었고, 따라서 이를 포함하는 Animal 클래스 또한 abstract 키워드를 사용하여 추상 클래스로 만들어주었다.

그 이후 추상 클래스 Animal을 상속받은 Dog 클래스와 Cat 클래스 안에 추상 메서드 sound()를 각각 오버라이딩하여 각 객체에 맞는 구현부를 완성해주었고, 마지막으로 완성된 클래스를 기반으로 dog 인스턴스와 cat 인스턴스를 생성하여 sound() 메서드를 호출했다.

두 번째로, 추상 클래스는 자바 객체 지향 프로그래밍의 추상화를 구현하는데 핵심적인 역할을 수행한다.

위의 예시를 다시 보면, 동물이 가지는 공통적인 특성을 모아 먼저 추상 클래스로 선언해주었고, 이를 기반으로 각각의 상속된 하위 클래스에서 오버라이딩을 통해 클래스의 구체적인 내용을 결정해주었다.

결론적으로 추상화는, 상속계층도의 상층부에 위치할수록 추상화의 정도가 높고 그 아래로 내려갈수록 구체화된다고 정리해 볼 수 있다.

3.3. final 키워드

final 키워드는 필드, 지역 변수, 클래스 앞에 위치할 수 있으며 그 위치에 따라 그 의미가 조금씩 다르다.

위치의미
클래스변경 또는 확장 불가능한 클래스, 상속 불가
메서드오버라이딩 불가
변수값 변경이 불가한 상수
final class FinalEx { // 확장 / 상속이 불가능한 클래스
	final int x = 1; // 변경되지 않는 상수
    
    final int getNum() { // 오버라이딩 불가한 메서드
    	final int localVar = x; // 상수
        return x;
    }
}

3.4. 인터페이스

인터페이스는 모든 메서드가 추상메서드인 경우를 말한다.

인터페이스의 기본 구조로는, class 대신 interface 키워드를 사용한다.

그리고 일반 클래스와 다르게, 내부의 모든 필드가 public static final로 정의되고, 모든 메서드는 public abstarct 로 정의된다. 두 가지 모두 생략이 가능하다. 생략된 부분은 컴파일러가 자동으로 추가해주게 된다.

public interface InterfaceEx {
	public static final int rock = 1; // 인터페이스 인스턴스 변수 정의
    final int scissors = 2; // public static 생략
    static int paper = 3; // public & final 생략
    
    public abstract String getPlayingNum();
    void call(); // public abstract 생략
}

인터페이스를 구현하려면 구현하고자 하는 class 에서 implements 키워드를 사용해 구현할 수 있다.

class 클래스명 implements 인터페이스명 {
	... // 인터페이스에 정의된 모든 추상 메서드 구현
}

특정 인터페이스를 구현한 클래스는 해당 인터페이스에 정의된 모든 추상 메서드를 구현해야한다.

즉, 어떤 클래스가 특정 인터페이스를 구현한다는 것은 그 클래스에게 인터페이스의 추상 메서드를 반드시 구현하도록 강제하는 것을 의미한다.

다른말로, 어떤 클래스가 어떤 인터페이스를 구현한다는 것은 그 인터페이스가 가진 모든 추상 메서드들을 해당 클래스 내에서 오버라이딩하여 바디를 완성한다라는 의미를 가진다.

그리고 인터페이스는 다중 구현이 가능하다.

class ExampleClass implements ExampleInterface1, ExampleInterface2, ExampleInterface3 {

}

인터페이스의 장점은 기능이 가지는 역할과 구현을 분리시켜 사용자로 복잡한 기능의 구현이나 교체 / 변경을 신경쓰지 않고도 코드의 번거로움을 최소화하고 손쉽게 해당 기능을 사용할 수 있도록 한다는 점이다.

이를 통해 기능을 구현하는 개발자의 입장에서도 선언과 구현을 분리시켜 개발시간을 단축할 수 있고, 독립적인 프로그래밍을 통해 한 클래스의 변경이 다른 클래스에 미치는 영향을 최소화할 수 있다는 큰 장점이 있다.

0개의 댓글