Java/Java

객체지향 프로그래밍 심화(다형성, 추상화)

마손리 2023. 3. 1. 23:13

다형성

다형성이란 하나의 객체가 여러가지 형태를 가질수 있는 성질을 의미하며 상위 클래스 타입의 참조 변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 한 것 이라 할 수 있다.

Person programmer = new Programmer();

(상위클래스타입) (참조변수명) = new (하위클래스생성자);

위와 같이 상위클래스 타입으로 하위클래스 객체를 만들수 있다.

 

또  다른 예제를 보자면

public class Main {
    public static void main(String[] args) {
        Person programmer = new Programmer();
        programmer.test();
        //programmer.test2(); 사용불가
        System.out.println(programmer.pro); // Person 클래스의 전역변수로 접근
    }

}

class Person {
    String pro = "Person Variable";
    void test(){
        System.out.println("Person");
    }
};
class Programmer extends Person{
    String pro = "Programmer Variable";
    void test(){
        System.out.println("Programmer Method");
    }
    void test2(){
    }
};

//출력
Programmer Method
Person Variable

위와 같이 상위 클래스인 Person타입으로 하위 클래스인 Programmer객체를 만들었을때,

생성된 객체는 상위 클래스가 가진 멤버들에게만 접근이 가능하였다. 하지만 상위 클래스인 Person의 필드에 접근한것과는 달리 메서드는 메서드 오버라이딩으로 인해 하위 클래스인 Programmer의 메서드를 호출한 결과가 나왔다.

 

이로 인해 알수 있었던 것은 생성된 객체가 접근 가능한 멤버의 개수는 상위 클래스의 멤버의 수가 된다. 좀더 설명하자면 사용할 메서드의 개수를 조절하기 위해 다형성을 사용 할수 있다.

 

 

참조 변수의 타입 변환

형변환 연산자 괄호 '()' 를 이용하여 기본 자료형의 형변환이 가능 했던 것처럼 참조변수 또한 타입 변환이 가능하다.

 

타입 변환을 위해서는 세가지 조건을 충족해야만 한다.

  1. 서로 상속관계에 있는 상위 클래스와 하위 클래스 사이에만 타입 변환이 가능하다.
  2. 하위 클래스 타입에서 상위 클래스 타입으로의 타입변환(업캐스팅)은 형변환 연산자 (괄호)를 생략할 수 있다.
  3. 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형변환 연산자(괄호)가 반드시 필요하다.
    • 또한 다운캐스팅은 업캐스팅이 이루어진 참조변수에 한해서만 가능하다.
public class Main {
    public static void main(String[] args) {
        Programmer programmer = new Programmer();
        Person person = (Person) programmer; //업캐스팅, 상위 클래스로의 타입 변환(괄호 생략가능)
        Programmer programmer1 = (Programmer) person;//다운캐스팅,(괄호 필수)(업캐스팅이 이전에 시행되어야 가능)
        //Chef chef = (Chef) programmer1; 서로 상속관계가 아니므로 타입 변환 불가

        Person person1 = new Person();
        //Programmer programmer2 = (Programmer) person1; //업캐스팅이 이루어 진적이 없으므로 다운캐스팅 불가능
    }
}

class Person {
};

class Programmer extends Person{
};

class Chef extends Person{
}

 

 

instanceof 연산자

instanceof 연산자를 사용하면 참조변수의 타입변환, 즉 캐스팅이 가능한지의 여부를 boolean 타입으로 확인 가능하다.

public class Main {
    public static void main(String[] args) {
        Programmer programmer = new Programmer();
        System.out.println(programmer instanceof Object);//true
        System.out.println(programmer instanceof Person);//true
        System.out.println(programmer instanceof Programmer);//true

        Person person = new Person();
        System.out.println(person instanceof Object);//true
        System.out.println(person instanceof Person);//true
        System.out.println(person instanceof Programmer);//false 
        
        Person person1 = (Person) programmer;
        System.out.println(person1 instanceof Programmer);//true (업캐스팅이후 이므로 다운캐스팅가능)
    }

}

class Person {

};
class Programmer extends Person{

};

 

 

다형성의 사용 예제

public class Main {
    public static void main(String[] args) {
        Coffee americano = new Americano(3000);
        Coffee caffelatte = new Caffelatte(3500);

        Customer customer = new Customer(50000);

        customer.order(americano);
        customer.order(caffelatte);
    }
}
class Coffee{
    int price;
    Coffee(int price){
        this.price = price;
    }
}

class Americano extends Coffee{
     Americano(int price) {
        super(price);
    }
    public String toString() {return "아메리카노";}
    // Object 클래스의 toString()메서드 오버라이딩
    // 메서드 오버라이딩할때 접근제어자는 상위 클래스의 접근제어자와 같아야한다.
}

class Caffelatte extends  Coffee{
     Caffelatte(int price) {
        super(price);
    }
    public String toString() {return "카페라테";}
}

class Customer{
    private int money;
    Customer(int money){
        this.money = money;
    }
    void order(Coffee coffee){
        money -= coffee.price;
        System.out.printf("%s을 구매합니다.\n",coffee.toString());
        System.out.printf("가격은 %d이며 남은 잔액은 %d입니다.\n",coffee.price,this.money);
    }
}

// 출력
아메리카노을 구매합니다.
가격은 3000이며 남은 잔액은 47000입니다.
카페라테을 구매합니다.
가격은 3500이며 남은 잔액은 43500입니다.

AmericanoCaffelatte 두 클래스에게 Caffee 클래스를 상속 시켜준뒤 다형성을 이용하여 Coffee타입의 인스턴스 생성,

이후 Customer클래스의 order()메서드매개변수 타입으로 Coffee타입의 인스턴스를 전달받은뒤 Coffee타입으로 생성된 각 인스턴스의 정보에 접근한다.

 

 

추상화

자바에서의 추상화란 기존 클래스들의 공통적인 요소들을 뽑아서 상위 클래스를 만들어 내는 것이다.

이 추상화에 사용하는것이 abstract 제어자 이다.

 

 

abstract 제어자 

abstract 제어자를 이용하여 추상 클래스(abstract class)추상 메서드(abstract method)를 만들수 있다.

 

추상클래스의 경우 직접적인 인스턴스 생성이 불가능하며 추상메서드의 경우 추상 클래스 내에 정의만 해주며 직접적인 기능(로직)들은 자식 클래스에서 구현하게 된다.

 

즉, 추상 클래스 및 메서드는 완벽하지 않은 '미완성' 적인 것이며 이 '미완성 메서드'와 '미완성 클래스'를 자식 클래스에서 메서드 오버라이딩을 이용하여 완성시켜준다.

 

public class Main {
    public static void main(String[] args) {
        // AbstractClass abstractClass = new AbstractClass(); 추상클래스는 인스턴스화 불가능
        AbstractClass child = new Child(); // 추상클래스 타입의 다형성은 사용가능
    }
}


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

abstract class AbstractClass extends GrandAbstractClass{ 
    //abstract void AbsractMethod(); 자식 클래스가 추상 클래스라면 부모의 추상메서드를 구현할 필요없다.
}

class Child extends AbstractClass{
    void AbsractMethod(){}; 
    // 상위 부모클래스들중 추상 메서드가 있다면 
    //그의 자식 클래스들중 최소 하나의 클래스에는 그 추상메서드를 구체화 해야한다.
}

위의 예제와 같이 추상 클래스가 다른 추상 클래스를 상속 받을수 있으며 부모클래스들중 하나의 클래스에 추상 메서드가 존재한다면 그의 자식 클래스들중 최소 하나의 클래스에는 해당 추상메서드를 구체화해야 한다.

 

또한, 이처럼 상속 계층도가 여러층으로 이루어저 있을경우, 상속 계층도의 상층부에 위치할수록 더욱더 추상적이며 범위가 넓다. 즉, 상층부에 가까울수록 더 공통적인 속성과 기능들이 정의 되있으며 아래로 내려갈수록 구체화된다. 

 

 

final 키워드

final 키워드의 경우 클래스, 멤버(필드, 메서드, 내부클래스), 지역변수 모두에게 적용할수 있으며 그 위치에 따라 의미가 달라진다.

위치 의미
클래스 변경 또는 확장 불가능한 클래스, 상속 불가
메서드 오버라이딩 불가
변수 값 변경이 불가한 상수

위의 표를 보았을때, 위치에따라 조금의 차이점이 있지면 공통적으로 변경이 불가능하고 확장할 수 없다는 점이 같다.

 

또한 abstract와 final의 기능과 의미가 반대되는 개념이므로 절대 같이 사용할 수 없다.

 

 

인터페이스(interface)

인터페이스도 추상 클래스 처럼 추상화를 구현하는데 활용된다. 하지만 인터페이스는 추상 클래스에 비해 더 높은 추상성을 가진다.

 

추상 클래스는 미완성 설계도라 비유한다면 인터페이스는 더 높은 추상성을 가지는 가장 기초적인 밑그림으로 표현할 수 있다.

 

추상클래스는 추상메서드를 하나 이상 포함한다는 점 외에는 기본적으로 일반 클래스와 동일하지만 인터페이스는 기본적으로 추상 메서드와 상수만을 멤버로 가질수 있으며 일반적으로 인터페이스는 추상 메서드의 집합 정도로 생각할 수 있다.

 

 

인터페이스의 기본 구조

인터페이스는 클래스를 작성하는 것과 비슷하지만 class 키워드 대신 interface 키워드를 사용한다.

 

또한, 내부의 모든 필드는 public static final로 자동 정의되며 메서드의 경우 모든 메서드가 public abstract로 정의 됩니다. 

interface Interface{
    boolean variable = true;
    // public static final boolean variable = true; 와 동일
    void method();
    // public abstract void method(); 와 동일
}

(인터페이스안에서 메서드의 경우 메서드 본문을 작성한뒤 static, default, private을 설정 할수 있지만 어디에 사용되는지는 모르겠다....)

 

 

인터페이스의 구현

interface Interface{
     boolean variable = true;
    // public static final boolean variable = true; 와 동일
     void method();
    // public abstract void method(); 와 동일
}
interface Interface2{
    void method2();
}

class InterfaceClass implements Interface, Interface2{ // implements 키워드 사용
    public void method(){};
    // Interface의 해당 메서드는 public 속성을 가지므로 현재 클래스의 메서드 또한 public으로 설정 필수
    public void method2(){};
    // 인터페이스의 다중구현이 가능하며 구현된 모든 인터페이스의 메서드들을 오버라이딩해야함
}

위의 코드와 같이 하나의 클래스에 implements 키워드를 사용하여 인터페이스를 구현하며 모든 추상 메서드들을 해당 클래스 내에서 완성 시켜주어야 한다.

 

또한 하나의 추상 클래스만이 상속이 가능 했던것 과는 달리 인터페이스는 다중 구현이 가능하며 이경우 구현된 모든 인터페이스의 메서드들을 오버라이딩 해주어야 한다.