[자바의 정석 기초편] 객체지향개념 3

JEREGIM·2023년 1월 20일
0

📌다형성

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

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

class Tv {
	boolean power;
    int channel;
    
    void power() { power = !power; }
    void channelUp() { ++channel; }
    void channelDown() { --channel; }
}

class SmartTv extends Tv{
	String text;
    void caption() { /*내용 생략*/ }
}

Tv t = new SmartTv();
참조변수의 타입(조상 - Tv)과 객체의 타입(자손 - SmartTv)이 불일치

객체와 참조변수의 타입이 일치할 때와 일치하지 않을 때의 차이
1. SmartTv s = new SmartTv(); 타입 일치

  • SmartTv 인스턴스의 멤버 모두 사용 가능
  1. Tv t = new SmartTv(); 타입 불일치
  • SmartTv 인스턴스의 멤버 중 Tv로부터 상속 받은 5개의 멤버만 사용 가능
  • SmartTV 인스턴스의 자손 멤버는 사용 불가

자손 타입의 참조변수로 조상 타입의 객체를 가리킬 수 없다.
SmartTv s = new Tv(); 에러, 사용 불가

Q&A

Q. 참조변수의 타입은 인스턴스의 타입과 반드시 일치해야 하는지?
A. 아니요. 일치하는 것이 보통이지만 일치하지 않을 수도 있습니다.

Q. 참조변수가 조상타입일 때와 자손타입일 때의 차이는?
A. 참조변수로 사용할 수 있는 멤버의 갯수가 달라집니다.

Q. 자손 타입의 참조변수로 조상 타입의 객체를 가리킬 수 있나요?
A. 아니요. 허용되지 않습니다.


📌참조변수의 형변환

사용할 수 있는 멤버의 개수를 조절하는 것

  • 조상, 자손 관계의 참조변수는 서로 형변환 가능
class Car {
    String color;
    int door;

    void drive() {
        System.out.println("drive!!!");
    }
    void stop() {
        System.out.println("stop!!!");
    }
}

class FireEngine extends Car {
    void water() {
        System.out.println("water!!!");
    }
}

class Ex7_7 {
    public static void main(String[] args) {
        FireEngine fe = new FireEngine();
        Car car = null;
        FireEngine fe2 = null;

        fe.water();
        car = (Car) fe;
        car.water(); // 에러
        fe2 = (FireEngine) car;
        fe2.water();
    }
}

  • 참조변수 car도 FireEngine 인스턴스를 가리킬 수 있지만 water() 메서드는 사용할 수 없다.
class Ex7_7 {
    public static void main(String[] args) {
        Car car = null;
        FireEngine fe = null;
        
        FireEngine fe2 = (FireEngine) car;
        Car car2 = (Car) fe2;
        car2.drive(); // NullPointerException 발생
    }
}
  • 컴파일은 되지만 NullPointerException 발생
  • 참조변수가 가리키는 실제 인스턴스(null)가 없기 때문이다.
    -> 실제 인스턴스가 무엇인지 중요
class Ex7_7 {
    public static void main(String[] args) {
        Car c = new Car();
        FireEngine fe3 = (FireEngine) c; // 형변환 실행 에러 java.lang.ClassCastException 발생
        fe3.water(); // 컴파일ok
    }
}
  • 컴파일은 되지만 형변환 실행 에러 java.lang.ClassCastException 발생

  • 참조변수 fe3가 가리키는 실제 인스턴스는 Car 인스턴스이다.
    Car 인스턴스에는 water() 메서드가 없기 때문에 에러가 발생

  • 참조변수가 가리키는 실제 인스턴스가 무엇인지 꼭 확인하고 그 인스턴스의 멤버 개수를 넘어서면 안된다.


📌instanceof 연산자

참조변수의 형변환 가능여부 확인에 사용, 가능하면 true 반환

FireEngine fe = new FireEngine();
System.out.println(fe instanceof Object); // true
System.out.println(fe instanceof Car); // true
System.out.println(fe instanceof FireEngine); // true
  • 3개 모두 true를 반환한다.
  • 자손 타입의 참조변수는 조상들로 형변환하는 것이 가능

Q&A

Q. 참조변수의 형변환은 왜 하나요?
A. 참조변수를 변경함으로써 사용할 수 있는 멤버의 개수를 조절하기 위해서

Q. instanceof 연산자는 언제 사용하나요?
A. 참조변수를 형변환하기 전에 형변환 가능 여부를 확인할 때


📌다형성의 장점

1. 매개변수의 다형성

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

class Product {
    int price;
    int bonusPoint;

    Product(int price) {
        this.price = price;
        bonusPoint = (int) (price / 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 = 1000;
    int bonusPoint = 0;

    void buy(Product p) {
        if (money < p.price) {
            System.out.println("잔액이 부족합니다.");
            return;
        }

        money -= p.price;
        bonusPoint += p.bonusPoint;
        System.out.println(p + "을/를 구입하셨습니다.");
    }
}

class Ex7_8 {
    public static void main(String[] args) {
        Buyer b = new Buyer();

        b.buy(new Tv1());
        b.buy(new Computer());

        System.out.println("남은 잔액은 " + b.money + "만원 입니다.");
        System.out.println("현재 보너스포인트는 " + b.bonusPoint + "점 입니다.");
    }
}
class Tv1 extends Product {
    Tv1() {
        super(100);
    }

    public String toString() {
        return "Tv";
    }
}
  • super(100); : 조상의 생성자 Product(int price) 호출, 100으로 초기화
  • toString() "Tv"로 오버라이딩
Product p = new Tv1();
b.buy(p);
  • 위 두줄을 한 줄로 바꾸면 b.buy(new Tv1()); 가 된다.
  • 다만, 직접 넣으면 참조변수가 없기 때문에 main 메서드에서는 사용할 수 없다.(buy() 메서드 에서는 사용 가능)

2. 여러 종류의 객체로 배열 만들기

조상타입의 배열에 자손들의 객체를 담을 수 있다.

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

class Buyer {
    int money = 1000;
    int bonusPoint = 0;
    int i = 0;
    
    Product[] cart = new Product[10];

    void buy(Product p) {
        if (money < p.price) {
            System.out.println("잔액이 부족합니다.");
            return;
        }

        money -= p.price;
        bonusPoint += p.bonusPoint;
        cart[i++] = p;
        System.out.println(p + "을/를 구입하셨습니다.");
    }
}
  • Product[] cart = new Product[10]; : 구입한 물건을 담을 배열 생성
  • cart[i++] = p; : 구입한 물건 p를 cart[0] 부터 차례대로 저장
	void summary() {
		int sum = 0;
		String itemList ="";


		for(int i=0; i<cart.length;i++) {
			if(cart[i]==null) break;
			sum += cart[i].price;
			itemList += cart[i] + ", ";
		}
		System.out.println("현재까지 구입하신 제품의 총 금액은 " + sum + "만원 입니다.");
		System.out.println("현재까지 구입하신 제품은 " + itemList + "입니다.");
	}
}
  • if(cart[i]==null) break; cart[] 배열에 아무것도 없다면 break 문으로 빠져나온다.
  • sum += cart[i].price; 구입한 제품들의 가격을 누적해 sum에 저장
  • itemList += cart[i] + ", "; cart[i] = cart[i].toString 구입한 제품들을 출력

📌추상 클래스(Abstract class)

미완성 설계도, 미완성 메서드를 갖고 있는 클래스

abstract class Player {
	abstract void play(int pos);
    abstract void stop();
}
  • 추상 메서드를 가지고 있는 클래스가 추상 클래스
  • 추상 메서드는 구현부(몸통 {})가 없는 미완성 메서드

다른 클래스 작성에 도움을 주기 위한 것, 인스턴스 생성 불가

  • Player p = new Player(); 에러, 추상 클래스는 인스턴스 생성 불가

상속을 통해 추상 메서드를 완성해야 인스턴스 생성 가능

class AudioPlayer extends Player {
	void play(int pos) { /*내용 생략*/ }
    void stop() { /*내용 생략*/ }
}

AudioPlayer ap = new AudioPlayer(); 
Player p = new AudioPlayer();
  • 상속받은 추상 메서드를 모두 구현해야 인스턴스 생성 가능, 앞에 abstract 안붙여도 됨
  • Player p = new AudioPlayer(); : 다형성 때문에 조상 타입인 Player 참조변수 p로도 인스턴스 생성 가능
abstract class AudioPlayer extends Player {
	void play(int pos) { /*내용 생략*/ }
}
  • 상속받은 추상 메서드를 모두 구현하지 않아도 되지만, 앞에 abstract를 붙여줘서 아직 구현되지 않은 추상 메서드가 있음을 알려줘야 한다.

추상 메서드(Abstract method)

미완성 메서드, 구현부(몸통 {})가 없는 메서드

  • abstract 리턴타입 메서드이름();

꼭 필요하지만 자손마다 다르게 구현될 것으로 예상되는 경우에 작성

추상 메서드 호출가능(호출할 때는 선언부만 필요)

abstract class Player {
	boolean pause; // iv
    int currentPos;
    
    Player() { // 생성자
    	pause = false;
        currentPos = 0;
    }
    
    abstract void play(int pos);
    abstract void stop();
    
    void play() { // 인스턴스 메서드
    	play(currentPos);
    }
}    
  • abstract void play(int pos); : "지정된 위치(pos)에서 재생을 시작하는 기능이 수행하도록 작성되어야 한다." 라고 자손 클래스들에게 필수적으로 구현을 강제하기 위해서 추상 메서드로 작성

  • void play() { play(currentPos); } 나중에 상속을 통해 자손이 play(int pos) 메서드를 구현하면 호출 가능

추상 클래스의 작성

여러 클래스에 공통적으로 사용될 수 있는 추상 클래스를 바로 작성하거나 기존 클래스의 공통 부분을 뽑아서 추상 클래스로 만든다.

public class Ex7_10 {
	public static void main(String[] args) {
		Unit[] group = { new Marine(), new Tank(), new Dropship() };

		for (int i = 0; i < group.length; i++)
			group[i].move(100, 200);
	}
}

abstract class Unit {
	int x, y;
	abstract void move(int x, int y);
	void stop() { /* 현재 위치에 정지 */ }
}

class Marine extends Unit { // 보병
	void move(int x, int y) {
		System.out.println("Marine[x=" + x + ",y=" + y + "]");
	}
	void stimPack() { /* 스팀팩을 사용한다. */ }
}

class Tank extends Unit { // 탱크
	void move(int x, int y) {
		System.out.println("Tank[x=" + x + ",y=" + y + "]");
	}
	void changeMode() { /* 시즈모드로 변환한다. */ }
}

class Dropship extends Unit { // 수송선
	void move(int x, int y) {
		System.out.println("Dropship[x=" + x + ",y=" + y + "]");
	}
	void load()   { /* 유닛들을 태운다. */ }
	void unload() { /* 유닛들을 내린다. */ }
}
abstract class Unit {
	int x, y;
	abstract void move(int x, int y);
	void stop() { /* 현재 위치에 정지 */ }
}
  • 마린, 탱크, 드랍쉽이 이동하는 방식(구현)은 모두 다르다. 그러나 이동한다는 메서드는 필수적으로 들어가야 되기 때문에 Unit 조상 클래스에 abstract void move(int x, int y) 라는 추상 메서드를 만들어주는 것이다.
public class Ex7_10 {
	public static void main(String[] args) {
		Unit[] group = { new Marine(), new Tank(), new Dropship() };
		for (int i = 0; i < group.length; i++)
			group[i].move(100, 200);
	}
}
  • Unit[] group = { new Marine(), new Tank(), new Dropship() }; 다형성의 장점 2번째인 조상타입(Unit)의 배열에 자손 객체들을 담을 수 있다.
  • 			for (int i = 0; i < group.length; i++)
    				group[i].move(100, 200);
    		}
    마린, 탱크, 드랍쉽을 모두 (100, 200) 좌표로 이동시키는 for 문

단계별 추상 클래스의 작성

  • 단계별로 추상 클래스를 작성하게 되면 중간 단계를 선택해서 구현할 수 있는 장점이 있다.

추상화된 코드는 구체화된 코드보다 유연하다. 변경에 유리


📌인터페이스(Interface)

추상 메서드의 집합

구현된 것이 전혀 없는 설계도.껍데기(모든 멤버가 public)

interface 인터페이스이름 {
	public static final 타입 상수이름 =;
    public abstract 메서드이름(매개변수목록);
}
  • 상수는 항상 public static final 이 붙기 때문에 생략 가능
  • 메서드는 항상 추상 메서드이기 때문에 public abstract 생략 가능

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

다중 상속이 가능(추상 메서드는 구현부가 없어서 충돌해도 문제가 없기 때문에)

인터페이스의 구현

인터페이스에 정의된 추상 메서드를 완성하는 것

class 클래스이름 implements 인터페이스이름 {
	// 인터페이스에 정의된 추상 메서드를 구현
}
  • 일부만 구현하는 경우, 클래스 앞에 abstract 를 붙여야 한다.

Q&A

Q. 인터페이스란?
A. 추상 메서드의 집합( + 상수, static 메서드, 디폴트 메서드)

Q. 인터페이스의 구현이란?
A. 인터페이스의 추상 메서드 몸통 {} 만드는 것

Q. 추상 클래스와 인터페이스의 공통점은?
A. 추상 메서드를 가지고 있다.

Q. 추상 클래스와 인터페이스의 차이점은?
A. 인터페이스는 iv, im, 생성자를 가질 수 없다.

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

인터페이스 타입 매개변수는 인터페이스를 구현한 클래스의 인스턴스만 가능

인터페이스를 메서드의 반환타입으로 지정할수 있다.

Fightable method() {
	Fighter f = new Fighter();
    return (Fightable) f;
}

class Fighter extends Unit implements Fightable {
	public void move(int x, int y) { /*내용 생략*/ }
    public void attack(Fightable f) { /*내용 생략*/ }
}
...
Fightable f = method(); // 호출
  • public void move(int x, int y) { /*내용 생략*/ }
    오버라이딩 규칙 : 조상(public)보다 접근 제어자의 범위가 좁으면 안된다.

  • 반환타입이 인터페이스인 메서드는 인터페이스를 구현한 클래스의 인스턴스를 반환한다.

  • 메서드를 호출해서 저장할 값의 타입은 인터페이스 타입이 일치 혹은 형변환이 가능한 타입이어야 한다.

인터페이스의 장점

두 객체 간의 연결을 돕는 중간 역할

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

class B {
	public void method() {
    	System.out.println("클래스 B의 메서드");
    }
}
  • 선언 + 구현 : 유연하지 않고 변경에 불리
interface I {
	public void method();
}

class B implements I {
	public void method() {
    	System.out.println("클래스 B의 메서드");
    }
}
  • 선언과 구현을 분리해서 구현부를 변경하는데 유연하다.

    직접적인 관계의 두 클래스 A-B

class A {
    public void method(B b) {
        b.method();
    }
}

class B {
    public void method() {
        System.out.println("클래스 B의 메서드");
    }
}

public class InterfaceTest {
    public static void main(String[] args) {
        A a = new A();
        a.method(new B()); // A가 B를 사용(A가 B에 의존)
    }
}
  • a.method(new B()); A가 B를 사용(A가 B에 의존)

  • B를 C로 변경하면 클래스 A의 코드도 변경해주어야 한다.
    -> public void method(C c) { c.method(); }

    간접적인 관계의 두 클래스 A-I-B
    -> 변경에 유리한 유연한 설계가 가능

class A {
    public void method(I i) {
        i.method();
    }
}

interface I { // 인터페이스로 선언부를 분리
    public void method();
}

class B implements I {
    public void method() {
        System.out.println("클래스 B의 메서드");
    }
}

class C implements I {
    public void method() {
        System.out.println("클래스 C의 메서드");
    }
}

public class InterfaceTest {
    public static void main(String[] args) {
        A a = new A();
        a.method(new B());
    }
}
  • class A { public void method(I i) { i.method(); } } 인터페이스 I를 구현한 클래스의 인스턴스만 매개변수로 가능

  • B를 C로 변경해도 클래스 A를 수정해줄 필요가 없다.
    -> main 메서드에서 a.method(new C()); 한번 변경해주면 끝이기 때문에 코드가 유연하고 변경이 유리하다.

개발 시간을 단축할 수 있다.

  • B가 완성이 안되어있어도 A는 인터페이스(I)를 이용해서 코드를 작성할 수 있다.

표준화가 가능하다.

서로 관계가 없는 클래스들을 관계 맺어줄 수 있다.

SCV, 탱크, 드랍쉽을 수리하는 메서드를 작성하려고 한다.
그런데 저 셋을 묶을 마땅한 관계가 보이지 않을때 인터페이스를 사용한다.

interface Repairable() {}

class SCV extends GroundUnit implements Repairable { //.. }
class Tank extends GroundUnit implements Repairable { //.. }
class Dropship extends AirUnit implements Repairable { //.. }

void repair(Repairable r) {
	if(r instanceof Unit) {
    	Unit u = (Unit) r;
        while(u.hitPoint != u.MAX_HP) {
        	u.hitPoint++; // Unit의 HP를 증가
        }
    }
}
  • Repairable 인터페이스를 구현한 클래스의 인스턴스만 매개변수로 올 수 있는 repair 메서드를 만들어서 서로 관계가 없는 클래스들을 관계 맺어준다.

디폴트 메서드와 static 메서드

인터페이스에 디폴트 메서드, static 메서드를 추가 가능(JDK 1.8부터)

인터페이스에 새로운 메서드(추상 메서드)를 추가하기 어렵다.
-> 추상 메서드를 추가하게 되면 기존에 인터페이스를 구현했던 클래스들 전부가 새로운 추상 메서드를 추가로 구현해야 한다.

해결책 : 디폴트 메서드(default mathod)

interface MyInterface {
	void method();
    default void newMethod() {} // 인스턴스 메서드
}
  • 디폴트 메서드는 인스턴스 메서드(인터페이스 규칙 위반)

디폴트 메서드가 기존의 메서드와 충돌할 때 해결책

  1. 여러 인터페이스와 디폴트 메서드 간의 충돌
    • 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 한다.
  2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
    • 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

-> 충돌했을때, 그냥 직접 오버라이딩 하면 해결된다.

0개의 댓글