자바의 정석 3rd , 354p~402p

이민균·2021년 5월 15일
0

Java

목록 보기
2/4

5.1 다형성이란?

객체지향의개념에서 다형성이란 -> "여러 가지 형태를 가질 수 있는 능력"

1.빵
2.자동차
3.로미오 줄리엣

class Tv{
	boolean power; //전원 상태 (on,off)
    int channel;  // 채널
    
    void power(){ power = !power;}
    void channelUp(){ ++channel;}
    void channelDown(){ --channel; }
}
class CaptionTv extends Tv{
	String text;
    void caption(){/*생략*/}
  
}

CaptionTv c = new CaptionTv();
Tv	  t = new CaptionTv();

참조변수의 타입이 일치하는 것이 보통이지만, Tv와 CaptionTv클래스가 서로 상속관계에 있을 경우 가능!

CaptionTv c = new Tv(); -->> 컴파일 오류 발생
Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문!

5.2 참조변수의 형변환

기본형 변수의 형변환에서 작은 자료형에서 큰 자료형의 형변환은 생략이 가능하듯이 참조형 변수에선 자손타입의 참조변수를 조상타입으로 형변환하는 경우 생략 가능

여기서 FireEngine 클래스와 Ambulance클래스는 형제관계는 아니다.
자바에서는 조상과 자식관계만 존재하므로 아무 관계가 없다. 따라서 위 코드는 에러발생

<형변환 예시>

car -> fe; 자손타입의 참조변수를 조상타입의 참조변수로 할당하는 경우
fe2 -> car; 조상타입의 참조변수를 자식타입의 참조변수에 저장할 경우

위에서 말한 것 처럼 멤버개수의 한계로 인해 생략 가능 or 생략 불가

class CastingTest2 {
	public static void main(String args[]) {
		Car car = new Car();
		Car car2 = null;
		FireEngine fe = null;
  
		car.drive();
		fe = (FireEngine)car;		// 8번째 줄. 컴파일은 OK.실행 시 에러가 발생
		fe.drive();
		car2 = fe;
		car2.drive();
	}
}

위 코드는 캐스트 연산자를 이용해서 형변환을 잘 수행한 것 같지만
참조변수 car이 참조하고 있는 인스턴스가 Car타입의 인스턴스이다.
조상타입의 인스턴스를 자손타입의 참조변수로 참조하는 것은 허용하지 않으므로 오류 발생!

그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다!

5.3 instanceof 연산자

instanceof를 이용한 연산결과로 true를 얻었다는 것은
참조변수가 검사한 타입으로 형변환이 가능하다는 의미

class InstanceofTest {
	public static void main(String args[]) {
		FireEngine fe = new FireEngine();

		if(fe instanceof FireEngine) {
			System.out.println("This is a FireEngine instance.");
		} 

		if(fe instanceof Car) {
			System.out.println("This is a Car instance.");
		} 

		if(fe instanceof Object) {
			System.out.println("This is an Object instance.");
		} 

		System.out.println(fe.getClass().getName()); // Ŭ·¡½ºÀÇ À̸§À» Ãâ·Â
	}
} // class
class Car {}
class FireEngine extends Car {}

위 예제에선 참조변수 fe가 생성된 인스턴스는 FireEngine 타입인데도 ,
Object타입과 Car타입의 instanceof연산에서 true결과를 얻는다.
그 이유는 FireEngine 클래스는 Object클래스와 Car클래스의 자손 클래스이므로 조상의 멤버를 상속받았기 때문이다.

어떤 타입에 대한 instanceof연산 결과가 true라는 것은 검사한 타입으로 형변환이 가능하다는 뜻!

5.4 참조변수와 인스턴스의 연결

조상클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때,

  1. 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우
  2. 자손타입의 참조변수로 자손 인스턴스를 참조하는 경우

위 두가지 경우는 서로 다른 결과를 얻는다.

1의 경우 >> 조상타입의 참조변수를 사용할 때 조상 클래스에 선언된 멤버변수가 사용됨'

2의 경우 >> 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버 변수가 사용된다.

<중복 정의 되지 않은 경우, 둘의 차이는 없다. (조상의 멤버사용)>

class BindingTest{
	public static void main(String[] args) {
		Parent p = new Child();
		Child c = new Child();

		System.out.println("p.x = " + p.x);
		p.method();

		System.out.println("c.x = " + c.x);
		c.method();
	}
}

class Parent {
	int x = 100;

	void method() {
		System.out.println("Parent Method");
	}
}

class Child extends Parent {
	int x = 200;

	void method() {
		System.out.println("Child Method");
	}
}

위 코드에선 조상타입 참조변수 p, 자손타입 참조변수 c를 각각 자손 인스턴스를 참조하고 있고 Parent, Child클래스 서로 같은 멤버들을 정의하고있는 경우이다. 메서드인 method()의 경우 참조변수의 타입에 관계없이 실제 인스턴스의 타입인 Child클래스에 정의된 메서드가 호출되지만, 인스턴스변수인 x는 참조변수의 타입에 따라 달라진다.

<결과>

5.5 매개변수의 다형성

class Product{
	int price;
        int bonusPoint;
    }
class Tv extends Product {}
class Computer extends Product {}
class Audio extends Product {}

class Buyer {
	int money = 1000;
   	int bonusPoint = 0;
}

/*			구입하는 기능
각 제품마다 매개변수가 각각 다르므로 구입 제품마다 메소드를 추가로 필요로함
*/

void buy(Tv t) {
 
  }
void buy(Computer c) {
 
  }
void buy(Audio a) {
 
  }

메서드의 매개변수에 다형성을 적용하면 다음과 같이 하나의 메서드로 표현할 수 있다!
Product 타입의 참조변수는 매개변수로 Product 클래스의 자손타입의 참조변수라면 어느 것이나 매개변수로 받아들일 수 있다.

class Product {
	int price;			// 제품의 가격
	int bonusPoint;		// 제품구매 시 제공하는 보너스점수

	Product(int price) {
		this.price = price;
		bonusPoint =(int)(price/10.0);	// 보너스점수는 제품가격의 10%
	}
}

class Tv extends Product {
	Tv() {
		// 조상클래스의 생성자 Product(int price)를 호출한다.
		super(100);			// Tv의 가격을 100만원으로 한다.
	}

	public String toString() {	// Object클래스의 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 PolyArgumentTest {
	public static void main(String args[]) {
		Buyer b = new Buyer();

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

		System.out.println("현재 남은 돈은 " + b.money + "만원입니다.");
		System.out.println("현재 보너스점수는 " + b.bonusPoint + "점입니다.");
	}
}

5.6 여러 종류의 객체를 배열로 다루기

cart의 배열크기가 10이므로 11개 이상의 제품을 담을 수 없다
이런 경우 Vector 클래스를 사용한다. Vector 클래스는 내부적으로 Object타입의 배열을 가지고 있다. 배열의 크기를 알아서 관리해주기 때문에 인스턴스의 개수를 신경 쓰지 않아도 된다.

6. 추상 클래스 (abstract class)

클래스를 설계도라고 한다면 추상클래스는 미완성 설계도라고 할 수 있다. 단어 그대로 완성되지 못한 채로 남겨진 설계도를 말한다.
미완성이라는 것은 멤버의 개수에 관계된 것이 아니라, 미완성 메서드를 포함하고 있다는 의미이다.

추상클래스는 상속을 통해 자손클래스에 의해서만 완성될 수 있다.

추상클래스 자체로는 역할을 다 못하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 조상클래스로서 중요한 의미를 갖는다.

실생활 Tv의 각각의 기능을 하는 여러가지 모델이 있다. Tv의 모델은 다 각각 다르더라도 이 들의 설계도는 아마 90%정도는 동일할 것이다. 그러므로 새로운 Tv를 만들 때 새하얀 백지에서 시작하는 것 보단 미리 공통부분만을 그린 미완성 설계도에서 시작하는 것이 효율적이다.

추상클래스는 키워드 'abstract'를 붙이기만 하면 된다.
추상메서드를 포함하고 있다는 것을 제외하고는 일반 클래스와 다르지 않다.

메서드를 미완성 상태로 남겨 놓는 이유

메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만 작성하고(주석을 통해 작성할 내용 설명), 실제 내용은 상속받는 클래스에서 구현하도록 비워 두는 것이다. 추상메서드 역시 키워드 'abstract'를 앞에 붙여주고 구현부가 없으므로 괄호{}대신 ';'을 적어준다.

추상클래스로부터 상속받는 자손클래스는 오버라이딩을 통해 조상인 추상클래스의 추상메서드 부분을 모두 구현해주어야 한다. 하나라도 구현하지 않으면, 자손클래스 역시 추상클래스로 지정해야한다.

메서드의 이름과 작업에 필요한 매개변수, 반환 타입 등등 결정하는 일은 쉽지 않다. 따라서 선언부분이 구현부분보다 중요하다고 볼 수 있다.

7.인터페이스(interface)

인터페이스는 일종의 추상클래스이다. 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상메서드와 변수가 아닌 상수만을 멤버로 가질 수 있다. 인터페이스 역시 추상클래스처럼 완성되지 않은 불완전상태이기 때문에 자체로 사용(인스턴스 생성)되기보다는 다른 클래스를 작성하는데 도움을 주는 목적이다.

class 대신 interface를 사용한다. 그리고 접근 제어자로 public 또는 default를 사용할 수 있다.

인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에 제어자를 생략할 수 있다. 편의목적으로 생략을 많이 하며 생략된 제어자는 컴파일 시에 컴파일러가 자동으로 추가해준다.

  • 인터페이스는 인터페이스로부터만 상속받을 수 있다.
  • 클래스와 달리 다중상속이 가능하다.

조상 인터페이스에 정의된 멤버를 모두 상속받는다. Fightable 자체에는 정의된 멤버가 하나도 없지만 조상 인터페이스로부터 상속받은 두 개의 추상메서드 move, attack을 멤버로 갖게 된다.

오버라이딩 할 때 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야한다. void move(int x , int y)로 정의되어 있지만 public abstract가 생략된 것이기 때문에 접근 제어자를 반드시 public로 해야 하는 것이다.

인터페이스를 이용한 다중 상속

자바에서는 다중상속을 허용하지 않는다.(두 조상으로부터 받은 멤버변수의 이름이 같거나 메서드의 선언부가 일치하거나 등등..)인터페이스를 이용하면 다중상속을 구현이 가능하지만 인터페이스로 다중상속을 구현하는 경우는 거의 없다.

인터페이스는 static상수만 정의하므로 조상클래스의 멤버변수와 충돌하는 경우는 거의 없고 메서드는 전혀 구현 내용이 없으므로 조상의 메서드를 따르면 되기에 문제가 없다. 하지만 이렇게 하면 멤버의 충돌을 피할 수 있지만 다중상속의 장점을 잃게된다. 만일 두 개의 클래스로부터 상속을 받아야 할 상황이라면 ,

1. 두 조상클래스 중에서 비중이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부에 멤버로 포함시키는 방식
2. 어느 한쪽의 필요한 부분을 봅아서 인터페이스로 만든 다음 구현

/추가 내용/

인터페이스의 장점

디폴트 메소드

class DefaultMethodTest {
	public static void main(String[] args) {
		Child c = new Child();
		c.method1();
		c.method2();
		MyInterface.staticMethod(); 
		MyInterface2.staticMethod();
	}
}

class Child extends Parent implements MyInterface, MyInterface2 {
	public void method1() {	
		System.out.println("method1() in Child"); // żŔšöśóŔĚľů
	}			
}

class Parent {
	public void method2() {	
		System.out.println("method2() in Parent");
	}
}

interface MyInterface {
	default void method1() { 
		System.out.println("method1() in MyInterface");
	}
	
	default void method2() { 
		System.out.println("method2() in MyInterface");
	}

	static  void staticMethod() { 
		System.out.println("staticMethod() in MyInterface");
	}
}

interface MyInterface2 {
	default void method1() { 
		System.out.println("method1() in MyInterface2");
	}

	static  void staticMethod() { 
		System.out.println("staticMethod() in MyInterface2");
	}

여러분들이 만약 오픈 소스코드를 만들었다고 가정하자. 그 오픈소스가 엄청 유명해져서 전 세계 사람들이 다 사용하고 있는데, 인터페이스에 새로운 메소드를 만들어야 하는 상황이 발생했다. 인터페이스가 변경되지 않으면 좋겠지만, 아무리 설계를 잘해도 언젠가 변경은 발생하기 마련이다. 이럴 때 사용하는 것이 바로 default 메소드다.

profile
바보민균

0개의 댓글