본문 바로가기

코딩/JAVA 공부

[JAVA] 자바의 정석 _ 다형성, 추상 클래스, 인터페이스

 * 다형성(polymorphism)

- 여러 가지 형태를 가질 수 있는 능력

 = 조상 타입 참조 변수로 자손 타입 객체를 다루는 것.

조상 타입 참조변수 = new 자식타입();  타입 불일치. 즉 참조 변수의 타입과 인스턴스(객체) 타입이 불일치.

// 부모인 Tv엔 5개의 멤버, 자식인 SmartTv엔 상속받은 5개 + 2개의 멤버 가 있다고 가정.
SmartTv s = new SmartTv(); // s 를 통해 7개 멤버 모두 접근 가능
Tv 	t = new SmartTv(); // t 를 통해 부모의 멤버인 5개만 접근 가능

반대의 경우 SmartTv s = new Tv(); 는 안 됨.

 

 * 참조변수의 형 변환

- 사용할 수 있는 멤버의 개수를 조절하는 것 (기본형 형 변환처럼 값이 변경되는 게 아님)

- 조상, 자손 관계의 참조변수만 서로 형 변환 가능

// 부모 class Car --> 멤버 4개
// 자식 class FireCar extends Car --> 멤버 1개 (총 5개)
// 자식 class Ambulance extends Car --> 멤버 1개 (총 5개)

FireCar f = new FireCar();
Car c = (Car) f;
FireCar f2 = (FireCar) c;
Ambulance a = (Ambulance) f2; // 부모 자식 관계가 아니므로 에러

 * instanceof 연산자

- 참조변수의 형 변환 가능여부 확인에 사용. 가능하면 true 반환. 형 변환 전 반드시 확인 필요

- 자신의 모든 조상으론 형변환 가능함.

void doWork(Car c){
    if(c instanceof FireEngine){	//형변환 가능 확인
    	fireEngine fe = (fireEngine)c; // 형변환
        fe.water();
    }
}

 

 * 다형성의 장점

1. 다형적 매개변수

- 참조형 매개변수는 메서드 호출 시, 자신과 같은 타입 또는 자손타입의 인스턴스를 넘겨줄 수 있다.

class Procudts{
    int price;
}

class Tv extends Products{}
class Dvd extends Products{}
class Computer extends Produts{}

class Customer{
	int money = 1000;
    
    void buy(Products p){
    	money -= p.price;
    }
    
Products p1 = new Tv();
products p2 = new Dvd();
products p3 = new Computer();
Customer c = new Customer();
c.buy(p1);
c.buy(p2);
c.buy(p3);

위 코딩을 보면, Customer 클래스의 buy 메서드에서 매개변수를 Products 형으로 받았는데, 이는 다형성이 가능하기 때문이다.

Tv, Dvd, Computer를 전부 Products형 참조변수로 생성했기 때문에 buy 메서드에 넣을 수 있다.

만약 다형성이 없었다면 buy 메서드를 오버로딩을 통해 모든 제품 타입을 매개변수로 갖도록 만들어야 했을 것이다.

class Product{
	int price;
	int bonuspoint;
	Product(){}
	Product(int i){
		this.price = i;
		this.bonuspoint = (int)(i/10.0);
	}
}

class Tv1 extends Product{
	Tv1(){super(100);}
	public String toString() {return "Tv";} // 오버라이딩
}

class Computer extends Product{
	Computer(){super(200);}
	public String toString() {return "Computer";}
}

class Buyer {
	int Money;
	int Bonuspoint;
	Buyer(int a){
		this.Money = a;
		this.Bonuspoint = 0;
	}
	void buy(Product p) {
		if(this.Money < p.price) {
			System.out.println("잔액이 부족합니다");
			return;
		}
		this.Money -= p.price;
		this.Bonuspoint += p.bonuspoint;
		System.out.println(p + "을 구매하셨습니다.");
	}
}

public class Ex7 {
	public static void main(String args[]) {
		Buyer b = new Buyer(10000);
		Tv1 Tv = new Tv1();
		Computer C = new Computer();
		b.buy(Tv);
		b.buy(C);
		System.out.println(b.Money);
	}
}

 강의 듣다 조금 이해가 안 돼서 예제를 직접 코딩해봤다.

마지막 buy 메서드를 사용할 때 하나 이해 안 가는 게 있는데,

위 코딩처럼 Tv, C 를 각각 Tv1, COmputer 형으로 작성해도 buy메서드를 사용하는데 문제없다.

buy 메서드는 매개변수로 Product형을 받도록 돼있는데... 자동으로 형 변환이 되는 걸까?

 

2. 하나의 배열로 여러 종류 객체 다루기

Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();

 

 * 추상 클래스(abstract class)

- 미완성 메서드를 갖고 있는 클래스

abstract class Player{
    abstract void play(int pos);
    abstract void stop();
}

상속을 통해 추상 메서드를 완성 후 인스턴스시(객체) 생성 가능.

언제 쓰는가? 꼭 필요하지만 자손마다 다르게 구현될 것으로 예산되는 경우.

객체 생성은 안되지만, 클래스 내에서 호출은 가능함.

abstract class Player{
    boolean pause;
    int currentPos;
    
    Player(){
    	pause = false;
        currentPos = 0;
    }
	abstract void play(int pos);
    abstract void stop();
    void play(){
    	play(currentPos);
    }
}

im play에서 play 추상 메서드 호출 가능.

기존 클래스의 공통부분을 뽑아서 추상 클래스를 만들 수 있음.

 

 * 인터페이스(interface)

- 추상 메서드의 집합 / 구현된 것이 없는 설계도. 껍데기(메서드 : 전부 public abstract / 상수 : 전부 public static final)

인터페이스의 상속

- Object가 최고 조상 아님, 인터페이스의 조상은 인터페이스만 가능

- 다중 상속이 가능함. (구현부가 없어 충돌 발생할 수 없음)

인터페이스의 구현

- 인터페이스에 정의된 추상 메서드를 완성하는 것. extends 가 아니라 implements 사용

- 일부만 구현하는 경우, 클래스 앞에 abstract를 붙여야 함.

interface Fightable{
    void move(int x, int y);
    void attack(Unit u);
}

class Fighter implements Fightable{
    public void move(int x, int y){/* 내용 생략*/}
    public void attach(Unit u){/*내용 생략*/}
}

  추상 클래스 VS 인터페이스

  추상클래스 인터페이스
추상메서드 O O
iv O X
im O X
생성자 O X

 * 인터페이스를 이용한 다형성

클래스의 다형성과 마찬가지로, 조상형 참조변수 = new 자식 객체 가능하다.

Fightable f = new Fighter();

매개변수를 interface형으로 갖는 경우. 해당 인터페이스를 구현한 클래스의 인스턴스가 들어갈 수 있다!

interface Fightable{
    void move(int x, int y);
    void attack(Fightable f); // Fightable 인터페이스 구현한 클래스의 인스턴스만 들어갈 수 있음
}

메서드의 리턴 타입으로 인터페이스 지정 가능.

Fightable testmethod(){ 	// Fighterable인터페이스 구현한 클래스의 인스턴스(객체) 반환
    Fighter f = new Fighter();
    return f;				
}
//위 매서드 호출 시 형일치 해야함
Fightable f = testmethod();

 

/* 내가 이해한 다형성 */l

다양한 클래스들 중 동일한 성질?(메서드)를 가진걸 하나의 조상으로 묶는 느낌?

그런데 클래스의 경우 조상을 1개만 가질 수 있어 1개의 분류만 가능하지만,

인터페이스는 다중 상속이 가능하므로 다양한 분류로 묶을 수 있다.

abstract class Unitt{
    abstract void move(int x, int y);
	void stop() {System.out.println("멈춥니다.");}
}
	
interface Fightable{
    void move(int x, int y);
	void fignt(Fightable f);
}

class Fighter extends Unitt implements Fightable{
    String i;
    Fighter(){}
    Fighter(String S){this.i = S;}
    public void move(int x, int y) {
        System.out.println("["+x+","+y+"] 로 이동");
    }
    public void fignt(Fightable f) {
        System.out.println(f+"를 공격합니다.");
    }
    public String toString() {
        return i;
    }
}

public class FighterTest {

    public static void main(String[] args) {
        Fighter f = new Fighter();
        f.move(100, 200);
        Fighter k = new Fighter("이름");
        f.fignt(k);
    }
}

예시를 구현해봤다. Fighter 생성 시 이름을 부여했다.

 

 * 인터페이스의 장점

1. 두 대상(객체) 간의 연결을 돕는 '중간 역할'을 한다.

 

2. 선언(설계)과 구현을 분리시킬 수 있게 한다.

Class B{      // 선언(설계) + 구현
    public void method(){
    	System.out.printlf("methodInB");
    }
}
----------------------------------------------------------------------------------------------
interface I{   		// 선언(설계)
    void method();
}

class B implements I{	// 구현
    public void method(){
    	System.out.printlf("methodInB");
    }
}

위 코딩처럼 선언과 구현을 분리할 수 있음. 수정이 필요할 때 구현 부위를 수정하면 돼서 훨씬 유연함.

 

3.  A의 구현부를 B에서 C로 변경할 때, 인터페이스가 있다면 A 변경 없이 B --> C 변경만 진행하면 됨. (느슨한 결합)

// A 와 B 직접적인 연결
class A{
    public void metgodA(B b){   // B 사용 --> B 를 C로 수정할 때 변경 필요.
    	b.metbodB();
    }
}

class B{
    public void methodB(){
    	System.out.println("methodB");
    }
}

class InterfaceTest{
    public static	void main(String args[]){
    	A a = new A();
        a.methodA(new B());
    }
}
------------------------------------------------------------------------------------------------
// interface를 통해 A 와 B 간접적인 연결 (느슨한 연결)
class A{
    public void methodA(I i){   // interface b 사용 --> B를 C로 수정해도 변경 필요 없음.
    	i.methodB();
    }
}

interface I{
    void methodB();
}

class B implements I(
    public void methodB(){
    	System.out.println("methodB");
    }
}

class InterfaceTest{
    public static	void main(String args[]){
    	A a = new A();
        a.methodA(new B());
    }
}

조금만 복잡해져도 느슨한 연결이 훨씬 유연하게 수정 가능할 것 같다.

 

4. 개발 시간 단축 가능

5. 변경에 유리한 유연한 설계

6. 표준화가 가능!

7. 서로 관계없는 클래스들을 관계를 맺어줄 수 있다. 새로운 인터페이스를 만들어 하나로 묶어줌!  

 

 * 인스턴스에서 사용되는 디폴트 메서드와 static 메서드

- 인터페이스에 새로운 메서드를 추가하기 어려움. (추가 시 자식들 모두 변경 필요) 

      --> 해결책 : 디폴트 메서드

interface MyInterface{
    void method();
    void newMethod();
}

interface MyInterface{
    void method();
    default void newMethod(){} // default 적어줘야함
}

but 이렇게 새로 추가가 생기면서 충돌 문제가 발생

 --> 충돌 발생 시 오버라이딩을 통해 구현 필요한 것으로 수정