Ch07. 객체지향 프로그래밍Ⅳ

ho_c·2023년 5월 15일
0

자바의 정석 3판

목록 보기
8/8
post-thumbnail

들어가는 말

  • 지난 시간 '상속'에 이어서 이번에는 '다형성'을 공부해보자.

  • 다형성은 상속과 깊은 관계를 가진다. 이 관계 안에서 OOP의 효율을 극대화할 수 있다.



1. 다형성(polymorphism)

여러 가지 형태를 가지는 능력

  • 다형성은 말 그대로 여러 형태를 가지는 걸 의미한다.

  • 이를 자바에선 '한 가지 자료형'으로 '여러 자료형의 객체''참조'할 수 있도록 구현한다.

  • 그리고 연결된 객체에 따라 서로 다른 메서드들을 호출한다.


정의

조상클래스 타입의 참조변수에 자손 클래스의 인스턴스의 주소를 저장할 수 있다.

  • 참조변수는 주소를 저장한다. ( null or 4byte )

  • 이전까지 인스턴스를 다룰 땐, 인스턴스의 타입과 일치하는 참조변수를 사용해 주소를 저장하고 접근했다.

  • 다형성은 상속관계에서 조상클래스의 참조변수에 자손클래스의 구현된 '인스턴스'의 주소를 저장하는 것이다.

  • 조상 클래스의 참조변수는 자신에게 정의되지 않은 자손의 멤버에는 접근할 수 없다.

  • 반면 오버라이딩된 멤버에는 접근이 가능하다.

//조상 클래스
public class Parent {
    int age = 60;

    void happy(){ System.out.println("parent happy"); }
}

//자손 클래스
public class Child extends Parent{
    int age = 20;
	
    // 오버라이딩 메서드
    void happy(){ System.out.println("Child happy"); }
	
    // 자손 메서드
    void angry(){ System.out.println("Child angry"); }
}

//다형성 구현
public static void main(String[] args){
		// 다형성
        Parent p = new Child();
        
        p.happy(); // 실행결과 : "Child happy"
		// p.angry(); - 호출 불가능
        
        // Down-Casting → 사용할 수 있는 멤버의 수가 늘어남.
        Child c = (Child) p;

        System.out.println(p.age); // 실행결과 : 60
        System.out.println(c.age); // 실행결과 : 20
}

자손 멤버에 대한 접근 문제

이전 예제에서 p.angry();는 호출이 불가능했다.
바로 다음의 참조변수의 형변환을 쓰면 호출할 수 있다.

p.angry(); // 불가능
c.angry(); // 가능
  • 상속 관계 안에선 자손 인스턴스가 생성되면 내부적으로 부모 인스턴스도 함께 생성된다.
    그래서 내부적으로 부모의 생성자도 호출된다.

  • 즉, 다형성은 자손 인스턴스 생성이 부모 인스턴스의 생성을 보장하기 때문에 가능하다.

  • 하지만 p에 자손 인스턴스를 연결해도, 상속받은 조상의 멤버에만 접근할 수 있다.
    자손에만 정의된 멤버에는 접근이 불가능하다.

  • 결과적으로 참조변수의 타입은 사용할 수 있는 멤버의 수를 결정한다.

그럼 자손의 참조변수로 부모 인스턴스에 접근하면 되는거 아님?

  • 가당치 않다. 참조변수는 말그대로 주소를 저장한 공간이고, 그 타입은 사용할 수 있는 멤버의 수를 말한다.

  • 또한 상속 관계에서 자손의 멤버는 조상의 멤버보다 최소 같거나, 많다.

  • 이 말은 조상에 없는 멤버가 자손에는 존재할 수 있단 것이고, 자바는 이런 변수를 허용하지 않는다.

  • 결과적으로 참조변수가 사용할 수 있는 멤버는 인스턴스보다 같거나, 적어야 한다.


그럼 그냥 참조변수랑 인스턴스 타입 일치시키는게 더 낫지 않나?

  • 기능이 변경 또는 확장될 일이 전혀 없다면 그게 편리할 것이다.
    하지만, 프로그램의 변경은 늘상 있는 일이다. 오죽하면 유지보수가 그리 중요할까.

  • 바로 여기서 다형성이 중요한 역할을 한다.

  • 다형성에선 같은 메서드라도 연결된 인스턴스가 다르면 그 인스턴스에 설정된 동작으로 실행한다.

  • 즉, 프로그램이 변경되도 참조변수에 꽂는 인스턴스만 교체하면 모든 일이 해결되기 때문이다.

  • 그래서 다형성은 '인터페이스'를 만나야 그 진가를 발휘한다.



참조변수의 형변환

상속관계 안에서 참조변수도 기본형처럼 형변환이 가능하다.

Up-Casting : 자손타입 → 조상타입 (생략가능)
Down-Casting : 조상타입 → 자손타입 (생략불가)


참조변수의 형변환도 ()로 캐스팅 연산을 해주면 된다.

//조상 클래스
class TV {
	boolean power;
    int channel;
    
    void power(){ power != power; }
    void channelUp(){ ++channel; }
    void channelDown(){ --channel; }

}

//자손 클래스
class CaptionTV extends TV{
	String text;
    boolean caption;
    
    void captionActivate(){ caption != caption; }

}

//형변환 구현
public static void main(String[] args){
	Object ob = null;
	TV tv = null;
    CaptionTV cTv1 = new CaptionTV();
    CaptionTV cTv2 = null;
    
    // up-casting
    tv = cTv1; // tv = (tv)cTv1;
    ob = tv;
    
    // down-casting
    cTv2 = (CaptionTv)tv; // 생략 불가.
    
    // 자손 멤버 호출
    ((CaptionTv)tv).captionActivate();
}
  • 업캐스팅에서 형변환의 생략이 가능한 건, 참조변수가 다룰 수 있는 멤버의 개수가 연결된 인스턴스의 멤버보다 당연히 적기 때문이다.

  • 반대로 다운캐스팅에서 불가능한 건, 참조변수가 다룰 수 있는 멤버가 인스턴스보다 많기 때문에 인스턴스에 없는 멤버를 호출해 문제가 생길 수 있기 때문이다.

  • 따라서 참조변수의 형변환은 인스턴스에 전체 멤버에 대해 사용가능한 멤버의 개수를 조절한 것이다.

  • 업캐스팅 : 사용 가능한 멤버의 수 '감소'
    다운캐스팅 : 사용 가능한 멤버의 수 '증가'



instanceof연산자

객체가 특정 클래스의 인스턴스인지 확인하는 연산자

참조변수 instanceof 클래스명

  • 참조변수가 가리키는 인스턴스가 무엇인지 모를 때 또는 확실히 알 수 없을 때, 사용한다.

  • 참조변수가 가리키는 '인스턴스'가 오른쪽 클래스 타입에 해당한다면 true를 아니면 false를 반환한다.

  • 참조변수의 값이 null인 경우, false를 반환한다.


용도

  • 참조변수의 클래스의 타입을 확인해서, 다운 캐스팅이 가능한지 확인하는 용도로 쓰인다.
class Parent { int age = 65; }
class Son extends Parent { 
		int age = 24;
    	void eat(){/* 구현 */}
    	...
        ...
}
class Daughter extends Parent { 
		int age = 15;
    	void goSchool(){/* 구현 */}
    	...
        ...
}

public static void main(String[] args){
	Parent p1 = new Son();
    Parent p2 = new Dauther();
    
	if(p1 instanceof Son){
    	Son son = (Son)p1;
        /* son의 멤버를 처리 */
        System.out.println(son.age) // 24
        son.eat();
    } else if (p2 instatnceof Dauther) {
    	Daughter daughter = (Daughter)p2
        /* Daughter의 멤버를 처리 */
        System.out.println(daughter.age) // 15
        daughter.goSchool();
    }
}

  • 상속관계인지 확인
class GrandParent { /* 구현 */ }
class Parent extends GrandParent { /* 구현 */ }
class Son extends Parent { /* 구현 */ }

public static void main(String[] args){
		Son son = new Son();

		System.out.println(son instanceof Parent); // true
		System.out.println(son instanceof GrandParent); // true
		System.out.println(son instanceof Object); // true
}


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

상속관계에서 까다로운 건, 중복되는 변수 또는 메서드(오버라이딩된)에 대한 처리문제이다.
이전에 메서드 내부에서 this, super로 중복 변수를 구분한 것과 같은 맥락이다.

  • 중복 메서드 : 참조변수에 상관없이 실제 연결된 인스턴스의 메서드를 호출함.

  • 중복 멤버변수 : 참조변수의 타입을 따라 달라짐.

  • static멤버 : 이 역시, 참조변수에 따라 달라진다. 그래서 클래스명.메서드형식을 쓰는게 좋음.

class Parent {
    int age = 60;
    String who = "Father";
    
    void happy(){
        System.out.println("parent happy");
    }
}

class Child extends Parent{
    int age = 20;
    void happy(){
        System.out.println("Child happy");
    }
}

/* 실행 */
public static void main(String[] args) {
     Parent p1 = new Child();
     Parent p2 = new Parent();
     Child c = new Child();
     
     // 중복 메서드
     p1.happy(); // Child happy
     p2.happy(); // parent happy
     
     // 중복 멤버 변수
     System.out.println(p1.age); // 60
     System.out.println(c.age); // 20
     
     // 중복되지 않는 변수
     System.out.println(p1.who); // father
     System.out.println(c.who); // father
}
  • 중복되지 않는 경우에는 선택지가 없기 때문에 참조변수에 영향을 받지 않는다.

  • 중복되는 경우만 참조변수에 영향을 받는다는 것만 인지하면 된다.



응용1. 매개변수의 다형성

메서드의 매개변수에서도 다형성을 사용할 수 있다.

// 조상
class Product {
	int price; // 제품 가격
    int bonusPoint; // 구매 시, 제공 보너스 점수
    
    // 생성자
    Product(int price){
    	this.price = price;
        bonusPoint = (int)(price/10.0);
    }
}

// 자손 : 제품
class Tv extends Product{}
class Computer extends Product{}
class Audio extends Product{}

class Buyer {
	int money = 1000;
    int bonusPoint = 0;
    
    void buy(){
    	money = money - 물건값
        bonuspoint = bonusPoint + 물건의 보너스 포인트
    
    };
}

다형성을 사용하지 않고선 구매자(Buyer)가 물건을 사려면 Tv, Computer, Audio 각각 buy()라는 메서드를 구현해줘야 한다.


다형성 적용

// 조상
class Product {
	int price; // 제품 가격
    int bonusPoint; // 구매 시, 제공 보너스 점수
    
    // 생성자
    Product(int price){
    	this.price = price;
        bonusPoint = (int)(price/10.0);
    }
}

// 자손 : 제품
class Tv extends Product{
		/* 상속받은 멤버
        price, bonusPoint */

		// 상속받은 멤버를 조상 클래스 생성자로 초기화		
        Tv(){ super(100); }
      /*Tv(){
        	super.price = 100;
            super.bonusPoint = (int)(super.price/10.0);
        }*/
}

class Computer extends Product{ /*구현*/ }
class Audio extends Product{ /*구현*/ }

// 구매자 - 다형성을 매개변수에 적용
class Buyer {
	int money = 1000;
    int bonusPoint = 0;
    
    void buy(Product p){
    	money = money - p.price;
        bonuspoint = bonusPoint + p.bonusPoint;
    };
}

/*실행*/
public static void main(String[] args){
	Buyer b = new Buyer();
    
	// 제품 구매
    b.buy(new Tv());
    b.buy(new Computer());
	
   	// 출력
    System.out.println(b.money); // 700
    System.out.println(b.bonusPoint); // 30
}
  • 다형성을 사용하면 Product클래스를 매개변수로 선언해서, 자손 인스턴스들을 전달할 수 있다.

  • 같은 원리로 매개변수 타입이 Object클래스면 한 메서드로 모든 클래스들을 매개변수로 처리할 수 있다.



응용2. 여러 종류의 객체 배열

공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 처리가 가능하다.

class Product { ... }
class Tv extends Product{}
class Computer extends Product{}
class Audio extends Product{}

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


『 핵심 정리 』

  • 다형성 : 참조변수 1개로 여러 인스턴스를 다룬다.

  • 참조변수의 타입은 참조할 수 있는 객체의 종류와 사용할 수 있는 멤버의 수를 결정한다.

  • 상속 관계 안에선 연결된 인스턴스에 따라, 동명의 메서드라도 동작이 달라질 수 있다.

  • 형변환을 쓰면, 자손에만 있는 멤버에도 접근할 수 있다.
    다만 인스턴스에는 영향을 미치지 않는다.

  • instanceof연산 결과가 true면 검사한 타입으로 형변환(up or down)이 가능하다.
    형변환 유형은 검사 대상과의 관계에 따라 다름.

  • 상속관계에서 중복되는 멤버변수는 참조변수의 타입을 따른다.
    멤버메서드는 실제 구현된 인스턴스를 따르며, Static멤버는 참조변수에 영향을 받으니 클래스명으로 다뤄야 한다.

  • 다형성은 매개변수에도 사용 가능하다. 이 경우 조상 타입의 매개변수 하나로, 자손 인스턴스들을 전달받아 처리할 수 있다.

  • 다형성은 배열에도 사용 가능하다. 조상 타입의 배열의 요소에 자손의 주소를 저장할 수 있다.



2. 추상클래스(abstract class)


정의

추상 메서드가 포함된 클래스

abstract class 클래스명 {}
  • 추상 메서드는 구현되지 않은 '미완성'메서드이다.
    이런 메서드를 포함된 클래스 역시 '미완성 설계도'가 된다.
    예외로 완성된 클래스에 abstract를 붙여 추상 클래스로 정의할 수 있다.

  • 추상 클래스는 인스턴스화 할 수 없다.

  • 추상 클래스를 상속받은 자손 클래스는 추상 메서드를 구현해야만 한다.
    그래야 인스턴스를 만들 수 있다.

  • 추상 클래스는 하나의 '틀'이다.
    이 틀을 기준으로 상속 안에서의 '기능 확장'이 추상 클래스의 목적이다.


추상 메서드

선언부만 작성된 메서드

abstract 리턴타입 메서드명(); // 구현부는 존재하지 않는다.
  • 메서드의 구현부(실제 수행 내용)를 작성하지 않은 메서드이다.

  • 상속받는 쪽에서 구현내용이 달라질 수도 있기 때문에 추상 메서드를 사용한다.

  • 따라서 실제 구현내용은 상속받는 쪽에서 오버라이딩으로 구현한다.
    조상 클래스는 선언부만 상속한다.

  • 같은 메서드라도 실제 구현한 클래스에 따라 다르게 동작한다.

  • 상속 받는 쪽에서 하나라도 구현하지 않으면, 해당 클래스도 추상 클래스가 된다.

  • 다형성을 이용하면 사용하는 쪽에선 추상메서드를 호출해도, 실제 연결된 인스턴스의 메서드가 호출된다.


추상 클래스와 메서드

// 추상 클래스
abstarct class Player {
	// 추상메서드
	abstract void play(Object o);
    abstract void stop();
}

// 추상메서드 구현
class AudioPlayer extends Player{
	// 오버라이딩으로 구현
    void play(Object o) {
        System.out.println(o + "을 실행합니다.");
    }

    void stop() {
        System.out.println("재생을 멈춥니다.");
    }
}

// 추상메서드 미구현
abstract class AbstractPlayer extends Player{
	void play(int pos){ /*구현*/ };
}

// 추상 메서드 호출
public static void main(String[] args) {
	Player p = new AudioPlayer();
	p.play("Rollin'"); // Rollin'을 재생합니다.
	p.stop(); // 재생을 멈춥니다.
}

추상화

  • 추상 클래스를 만드는 일은 '추상화'작업에 해당한다. 추상화를 이해하기 위해선, 그 반대개념인 구체화를 함께 봐야 한다.

  • 일단, 추상 클래스든 상속이든 목적은 '상속관계를 통한 기능확장'이다.

  • 다만 추상 클래스는 '추상화'에 상속은 '구체화'작업에 해당한다.

  • 상속 계층도 안에서 보면 추상클래스는 '조상 클래스', 상속은 '자손 클래스'의 방향으로 흐른다.

  • 즉, 추상화는 자손에서 사용할 '조상 클래스'를 만들고,
    상속은 조상 클래스를 사용해 '자손 클래스'를 만든다.

정리하면 다음과 같다.

  • 추상화 : 클래스 간 공통점을 찾아내 공통 조상을 만드는 작업
  • 구체화 : 상속을 통해 클래스를 구현, 확장하는 작업

추상 클래스의 사용 용도

그러나 좀만 생각해보면, 추상화 작업으로 만드는 조상이 굳이 '추상 클래스'일 필요는 없다.

그냥 조상클래스를 정의하고, 자손에서 오버라이딩해서 사용해도 구체화할 수 있다.

class Player {
	void play(Object o){};
    void stop(){};
}

심지어 위처럼 {}만 붙여줘도, 추상메서드가 아니게 된다.

그럼에도 쓰는 이유는 추상 클래스의 '강제성'때문이다.

  • 앞서 봤듯, 추상메서드를 구현하지 않으면 해당 클래스는 인스턴스화할 수 없다.

  • 그래서 상속 받는 쪽에서 의도에 맞게 구현하라는 뜻을 내포한다.
    일반 클래스면 구현부를 그대로 남겨둘 수 있기 때문에 강제성이 떨어진다.

  • 결과적으로 추상 클래스의 용도는 상속받는 쪽에서의 구현을 강요하기 위해서이다.

  • 추가로 다형성과 객체배열을 이용하면, 추상 클래스의 참조변수로 추상메서드를 실제 인스턴스에 따라서 실행할 수 있다.



『 핵심 정리 』

  • 추상메서드가 포함되면 그 클래스는 추상클래스가 된다.
    물론 추상메서드가 없어도 abstract를 붙여서 추상클래스를 만들 수 있다.

  • 추상클래스는 인스턴스화할 수 없다.

  • 추상메서드는 선언부만 작성된 메서드이다. 빈 {}도 쓰지 않는다.

  • 추상메서드는 상속받는 쪽에서 구현해야된다.
    구현하지 않으면 해당 클래스도 추상클래스가 된다.
    따라서 상속받은 클래스들의 인스턴스는 해당 메서드가 구현되어 있다.

  • 추상화는 '공통 조상 클래스'를 만듦. / 구체화는 조상을 확장하는 자손 클래스를 만듦.

  • 추상클래스의 목적 : 상속을 통한 기능 확장

  • 사용 용도
    1) 상속받는 쪽의 상황에 맞게 추상메서드의 구현을 강요하기 위해
    2) 추상클래스 타입 참조변수에 연결된 인스턴스에서 구현된 추상 메서드를 호출하기 위해.(동명의 메서드가 구현되어있기 때문에 캐스팅을 하지 않아도 된다.)



3. 인터페이스(interface)

인터페이스도 일종의 추상클래스로 분류할 수 있다.
다만 추상화정도는 추상클래스보다 높으며, 그 용도가 사뭇 다르다.

정의

상수와 추상메서드로만 이뤄진 일종의 추상클래스

  • 인터페이스의 멤버는 '상수, 추상메서드'만 허용된다.

  • 추상클래스가 '미완성 설계도'라면, 인터페이스는 '밑그림'이다.

  • 추상화↑------------------->구체화↑
    인터페이스추상클래스클래스
  • 인터페이스도 구현을 통해 기능을 확장하지만, 포인트는 '기본 규격'이다.

인터페이스 작성

interface 인터페이스명 {
	// 상수
	public static final 타입 상수명 = 리터럴;
    
    // 추상메서드
    public abstract 메서드명();
}

제약조건

인터페이스 작성 시엔, 제약사항이 있다.

  • 인터페이스의 접근제어자는 pubilc, default만 가능하다.
    (구현하려면 어디서든지 접근이 가능해야된다.)

  • 모든 멤버변수의 제어자는 public static final이다. (생략 가능)

  • 모든 메서드의 제어자는 public abstract이다. (생략 가능)

  • 단, jdk1.8부터는 static, default메서드도 추가할 수 있게 되었다.



인터페이스의 상속

인터페이스끼리는 다중 상속이 가능하다.

interface Movable {
	void move(int x, int y);
}

interface Attackable {
	void attack(Unit u);
}

interface Fightable extends Movable, Attackable{}
/*Fightable는 Movable, Attackable에 정의된 멤버를 모두 상속받는다.*/

인터페이스 구현

인터페이스는 빈 껍데기이다. 그래서 일반 클래스로 인터페이스를 구현해야된다.

// 인터페이스
interface Fightable extends Movable, Attackable{}

// 구현체
class Fighter implements Fightable{
	public void move(int x, int y){ /*구현*/ };
	public void attack(Unit u){ /*구현*/ };
}
  • 인터페이스는 만든 부분이 없어서 인스턴스화 시킬 수 없다. 그래서 구현해야한다.

  • 구현은 추상클래스를 상속받아 미구현된 부분을 구현하는 것과 다르지 않다.

  • 추상클래스는 미구현된 부분을 구현하거나, 해당 상황에 맞게 변경해서 기능을 확장(extends)한다.

  • 반대로 인터페이스는 전체를 구현(implements)해야 한다.

  • 만약 인터페이스의 일부 메서드만 구현하면, 해당 클래스는 추상클래스가 된다.

  • 상속과 구현은 동시에 가능하다.

// 구현체
class Fighter extends Unit implements Fightable{
	public void move(int x, int y){ /*구현*/ };
	public void attack(Unit u){ /*구현*/ };
}

상속과 구현의 구조도

  • 예제
// 구현체
class Fighter extends Unit implements Fightable{
	public void move(int x, int y){ /*구현*/ };
	public void attack(Unit u){ /*구현*/ };
    /* 상속받는 메서드의 접근제어자보다 범위가 작아선 안된다.
    인터페이스의 접근제한자는 public이기 때문에 구현체에서도 public이 된다.
    */
}

// 조상클래스
class Unit {
	int currentHP;
	int x;
    int y;
}
// 인터페이스
interface Fightable extends Movable, Attackable{}
  • 구조도



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

인터페이스를 활용하면, 막힌 다중상속을 구현할 수 있다.


다중 상속의 모호성

  • 다중 상속의 가장 큰 단점은 '모호성'이다.

  • 모호성은 상속받는 조상들의 멤버가 중복(변수명, 메서드 선언부 일치)될 때, 발생한다.
    즉, 어느 조상의 것을 선택할지 애매하다는 것이다.

  • 이를 위해선 한 쪽의 상속을 포기하거나, 충돌하지 않게 조상 클래스를 변경해야 한다.

  • 따라서 자바는 단점이 이점보다 크다고 판단해, 다중 상속을 막았다.


다중 상속 구현

인터페이스 자체는 다중 상속을 구현하는 용도가 아니라는 점을 알아두자.

  • 인터페이스와 조상클래스 간에는 부딪힐 일이 없다.

  • 멤버변수는 static 상수이니 클래스명으로 구분이 가능하고,
    메서드는 조상 클래스의 구현된 걸 상속받으면 된다.

  • 즉, 인터페이스와 클래스는 부딪힐 일이 없다는 것이다.

  • 그래서 다중 상속은 상속, 포함 관계 그리고 인터페이스 구현을 조합해서 가능하다.

// 조상 클래스 1
public class Tv{
	protected boolean power;
    protected int channel;
    protected int volume;
    
    public void power() { /*구현*/ }
    public void channelUp() { /*구현*/ }
    public void channelDown() { /*구현*/ }
    public void volumeUp() { /*구현*/ }
	public void volumeDown() { /*구현*/ }
}

// 인터페이스 : VCR클래스의 인터페이스(규격)
public interface IVCR {
	public void play();
	public void stop();
	public void reset();
	public int getCounter();
	public void setCounter(int c);
}

// 조상 클래스 2
public class VCR implements IVCR {
	protected int counter;

	public void play() { /*구현*/ }
	public void stop() { /*구현*/ }
	public void reset() { /*구현*/ }
	public int getCounter() { return this.counter; }
	public void setCounter(int c) { this.counter = c; }
}


// 다중 상속을 활용한 구현체
public class TVCR extends Tv implements IVCR {
	IVCR vcr = new VCR(); // 포함 관계 및 다형성
    
    // 인터페이스 구현 → 구현된 메서드는 호출 시, VCR 인스턴스의 메서드를 재호출한다.
	public void play() { vcr.play(); };
	public void stop() { vcr.stop(); };
	public void reset() { vcr.reset();};
	public int getCounter() { return vcr.getCounter(); };
	public void setCounter(int c) { vcr.setCounter(c); }
}
  • 책에선 인터페이스 없이도 포함관계로도 충분하며, 단지 다형적 특성을 살리기 위해 인터페이스를 사용한다고 설명한다.
  • 또 책의 예제에선 IVCR은 VCR클래스의 카피본이었지만, 이 경우에는 다형적 특성을 잘 살리지 못한다고 판단했다.
  • 난 반대로 VCR를 IVCR의 구현체로 활용했다. 그래서 IVCR만 구현하면 다중상속에서 포함되는 인스턴스만 변경하여 다형적 특성을 더 살리도록 했다.


인터페이스의 다형성

Fightable f = new Fighter(); // (Fightable)new Fighter();
  • 다형성으로 인해 조상타입의 참조변수에 자손 인스턴스의 주소를 저장할 수 있다. (참조)

  • 인터페이스로도 다형성이 동일하게 가능하다. (인터페이스 → 구현체)

  • 또한, 인터페이스로 형변환(up,down)이 가능하다.

  • 같은 원리로 메서드의 매개변수, 리턴타입으로도 사용될 수 있다.

// 매개변수
void attack(Fightable f){
	...
}

// 리턴타입
Fightable method(){
	...
    Fightable f = new Fighter();
	return f;
}
  • 리턴타입으로 사용 시, 인터페이스를 반환하는 것이 아닌 해당 인터페이스를 구현한 인스턴스의 주소를 반환해야 한다.

규격으로서의 인터페이스

드디어 인터페이스가 '다형성'을 만났다.
이제 예제를 통해 '기본 규격'으로서 인터페이스가 어떻게 활용되는지 살펴보자.

// 인터페이스
interface DBAPI {
	// 검색
    public abstract String search(String word);
    // 쓰기
    public abstract int write(String article);
    // 수정
    public abstract int update(String article);
    // 삭제
	public abstract int delete();
}

// 구현체 1 : 오라클 DB
class OracleDB implements DBAPI {
	// 검색
    public String search(String word){
    	/*구현*/
    	return article; // DB에서 불러온 글을 String article에 저장해서 반환. 
    };
    // 쓰기
    public int write(String article){
    	boolean result;
    	/*구현*/
    	if(result){
        	return 1; // 작업 결과가 성공이면 1을 반환
        }else{
        	return 0; // 실패 시, 0을 반환
        }
    };
    // 수정
    public int update(String article){
       	boolean result;
    	/*구현*/
    	if(result){
        	return 1; // 작업 결과가 성공이면 1을 반환
        }else{
        	return 0; // 실패 시, 0을 반환
        }
    };
    // 삭제
	public int delete(){
        boolean result;
    	/*구현*/
    	if(result){
        	return 1; // 작업 결과가 성공이면 1을 반환
        }else{
        	return 0; // 실패 시, 0을 반환
        }
    };
}

// 구현체 2 : MariaDB
class MariaDB implements DBAPI {
	// 검색
    public String search(String word){ /*위와 동일*/ };
    // 쓰기
    public int write(String article){ /*위와 동일*/ };
    // 수정
    public int update(String article){ /*위와 동일*/ };
    // 삭제
	public int delete(){ /*위와 동일*/ };
}

// main();
public static void main(String[] args){
	DBAPI db = new Oracle();
    db.search("Interface");
    db.write("현재 인터페이스 공부 중입니다.");
    db.update("현자 자바 공부 중입니다.")
    db.delete();
};
  • DB는 기본적으로 CRUD가 가능해야된다. 즉, CRUD는 DB라면 가져야 하는 기본 규격이다.
    그래서 DBAPI 인터페이스는 CRUD를 구현하도록 작성되어 있다.

  • 그리고 각 구현체의 실제 구현된 부분은 DB에 따라 다르다.

  • main에서 만약 DB를 MariaDB로 교체할 경우, DBAPI db = new MariaDB();로 변경해주면 된다. 그러면 참조변수 db를 이용한 모든 작업들이 MariaDB로 변경되서 수행된다.

  • 이렇게 인터페이스와 다형성을 함께 활용하면, 호스트-서버 환경에서 서버 코드를 호스트 측에 영향을 미치지 않고 변경할 수 있다.



인터페이스 장점


1. 개발 시간 단축

인터페이스는 작성만 해도 다른 곳에서 사용할 수 있다.
그래서 메서드 구현 / 호출, 양쪽에서 독립적인 동시 개발이 가능하다.
구현하는 측은 구현만 하면되고, 호출 측은 선언부만 알아서 input, output만 확인하면 된다.


2. 표준화

앞서 본 '규격'이 클래스 단위의 규격이라면 '표준화'는 프로젝트 전체의 규격이다.
즉, 프로젝트의 틀을 인터페이스로 작성하고 개발자는 이를 따라 구현한다.

예를 들어 ERP프로그램을 개발하게 됐다 하자.
고객사마다 세부적인 부분은 다르겠지만, ERP의 기본 기능은 표준화된 인터페이스를 구현하면된다.
그 후, 다른 부분들을 추가 개발하면 될 것이다.

이처럼 인터페이스를 이용하면 일관되고 정형화된 프로그램을 개발할 수 있다.


3. 서로 관계없는 클래스들 간의 관계를 만들 수 있다.

인터페이스가 클래스 간 '다리'를 놔준다고 생각하자.
일단 상속은 '기능 확장'에 초점이 맞춰져 있다. 그래서 조상-자손, 공통조상을 가진 자손들은 서로 공통점이 있어야 한다.
하지만 공통점 없는 클래스도 인터페이스만 있으면, 관계가 성립될 수 있다. 한마디로 하나로 묶어서 써먹을 수 있다는 것이다.

예시

// 조상 클래스
class Animal{ /*내용생략*/ }
class LandAnimal extends Animal{ /*내용생략*/ }
class MarineAnimal extends Animal{ /*내용생략*/ }

// 육지 동물
class Giraffe extends LandAnimal{ /*내용생략*/ }
class Koala extends LandAnimal{ /*내용생략*/ }
class Lion extends LandAnimal{ /*내용생략*/ }

// 해양 동물
class Shark extends MarineAnimal{ /*내용생략*/ }
class Seahorse extends MarineAnimal{ /*내용생략*/ }

LandAnimal, MarineAnimalAnimal를 상속받고, 다시 두 클래스를 상속받는 기린, 코알라, 사자, 상어, 해마가 있다.
여기서 우리는 '육식 행위'를 하는 메서드를 구현하고 싶은데, 사자와 상어만 해당된다.
하지만 이 둘을 연결해줄 접점은 Animal 하나뿐인데 그렇게 되면 채식동물도 육식을 하게 된다.
또, 오버로딩으로 메서드 구현을 하더라도 결국 2개는 만들어야 된다.
이런 경우 '인터페이스'로 연결을 시켜준다.

interface MeetAble {}
class Lion extends LandAnimal implements MeetAble{ /*내용생략*/ }
class Shark extends MarineAnimal implements MeetAble{ /*내용생략*/ }

// 메서드 구현
void eatMeet(MeetAble name){
	/*육식 행위 구현*/
}

따라서 상속관계가 아니지만, 공통점이 있을 때 인터페이스를 사용해 관계를 맺어준다.
그리고 인터페이스를 기준으로 코드가 관리되므로, 코드 중복도가 낮아지고(오버로딩 안해도 됨), 재사용성과 유지보수성이 높아진다.(인터페이스에 코드를 모았기때문에 인터페이스로 구현체들을 관리할 수 있다.)


4. 독립적인 프로그래밍이 가능

상속, 포함은 선언과 구현이 연결되어 있어 구현이 변경될 경우, 그 클래스가 선언되어 사용되는 곳도 변경해야될 수도 있다.
쉽게 말하면 어떤 클래스 내부에서 다른 클래스의 인스턴스를 사용하는 순간, 클래스 간의 의존관계가 만들어진다.

하지만 인터페이스를 사용하면, 앞선 DB예제처럼 구현이 바뀌더라도 선언된 곳에서는 변경할 필요가 없어진다.
따라서 말 그대로 인터페이스로 클래스 간을 연결해서 한쪽이 변경되더라도 영향을 미치지 않도록 만든다.



인터페이스 정리

인터페이스 장점4에 해당하는 내용이다.


먼저 인터페이스를 사용하기 위한 두 가지 사항을 기억해두자.

① 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
② 메서드를 호출하는 쪽(User)는 메서드(Provider)의 선언부만 알면 된다.


클래스의 직접 관계

class A {
	pubilc void methodA(B b){
    	b.methodB(); // 의존 관계 발생
    }
}
class B {
	public void methodB() {
    	System.out.println("methodB()");
    }
}

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

위 예제를 구조로 표현하면 다음과 같다.


인터페이스를 이용한 간접 관계

interface I {
	public abstract void methodB();
}

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

class A {
	pubilc void methodA(I i){
    	i.methodB(); // 의존 관계 발생
    }
}

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

구조로 표현하면 다음과 같다.

  • 코드상 클래스A 내부에는 클래스B가 사용되지 않았고, 그 결과 직접적인 관계는 맺어지지 않았다.

  • I의 구현체는 A에겐 중요하지 않다. 구현되지 않아도 A는 메서드를 호출할 수 있고, 이름을 몰라도 알아서 호출된다.

  • 또한 B가 아니어도 된다. 따라서 A와 B는 독립적인 프로그래밍이 가능하다.



원래 인터페이스에 추상메서드를 제외하고는 사용할 수 없었다.
근데 JDK1.8부터 정적 메서드와 디폴트 메서드를 추가할 수 있게 되었다.

디폴트 메서드

  • 상속 관계에서 메서드 추가는 조상의 멤버추가로 자손에서 오버라이딩할지 말지만 결정하면 된다.

  • 반면 인터페이스는 추상메서드가 추가되는거라, 구현체에서 구현할게 더 생기는 큰 일이다.

  • 그래서 나온게 Default Method이다. 디폴트 메서드는 추상메서드가 아니라 구현체에서 구현할 필요가 없다.

  • default라는 키워드가 붙으며, 접근제어자는 public으로 생략 가능이다.
    구현부의 {}까지 있어야 한다.

// 인터페이스
interface DefaultInterface{
	// 추상메서드
	void method(); 
    // 디폴트메서드
   	default void newMethod(){
        System.out.println("디폴트 메서드");    
    }; 
}

// 구현체
public class DefaultClass implements DefaultInterface{
    public void method() {
        System.out.println("구현 완료");
    }
}

// 실행
public static void main(String[] args) {
	DefaultInterface d = new DefaultClass();
	d.method(); // 구현 완료
	d.newMethod(); // 디폴트 메서드
}

주의사항

디폴트 메서드도 인터페이스(다중 상속 가능), 조상 클래스(상속과 인터페이스 동시 가능)의 메서드와 이름이 겹쳐서 충돌할 수 있다.

  1. 여러 인터페이스의 디폴트 메서드 간의 충돌
    : 뭐를 따를지 모르니까 구현체에서 오버라이딩해야 한다.
  1. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
    : 조상이 먼저다. 그래서 조상 클래스의 메서드 상속, 디폴트 메서드는 무시

static 메서드

  • 정적 메서드 자체는 인스턴스랑 관계가 없기 때문에, 인터페이스에 못 쓸 이유가 없다.

  • 인터페이스에 쓰이는 정적메서드의 접근 제어자는 public이며, 생략할 수 있다.



『 핵심 정리 』

  • 인터페이스의 접근제어자는 pubilc, default만 가능하다.
    멤버의 제어자는 대부분 생략가능하다.

  • 인터페이스의 멤버는 상수, 추상메서드, 정적 메서드와 기본 메서드만 가능하다.

  • 인터페이스 간의 다중 상속이 가능하다.
  • 인터페이스의 일부만 구현한다면, 해당 클래스는 abstract가 붙어 추상클래스가 된다.
  • 인터페이스의 구현체는 상속과 구현을 동시에 할 수 있다.

  • 구현체의 메서드의 접근 제어자는 반드시 public이어야 한다.
    오버라이딩 시, 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야 한다.

  • 인터페이스에서도 다형성을 적용할 수 있다.
    (참조변수, 형변환, 메서드의 리턴타입 and 매개변수)

  • 반환타입인 인터페이스인 건, 해당 인터페이스 구현체의 인스턴스를 반환한다는 것이다.

  • 인터페이스와 다형성을 활용하면, 호스트 측에 상관없이 서버 코드만 변경할 수 있다.

  • 서로 관계 없는 클래스들을 빈 인터페이스로 관계를 맺어줄 수 있다.

  • 인터페이스 사용 시, 메서드를 이용하는 쪽은 '선언부'만 알고 있어도 사용할 수 있다.

  • 인터페이스를 이용해서 클래스 간의 관계를 맺어주면, 독립적인 프로그래밍이 가능하다.
    (호출하는 쪽은 구현체가 아닌 인터페이스를 의존하게 된다.)

  • 디폴트 메서드는 구현체에서 구현하지 않아도 된다.

  • 정적 메서드도 public접근제어자를 붙여 인터페이스에 작성할 수 있다.



4. 내부 클래스(inner class)

정의

클래스 내에 선언되는 클래스.

  • 서로 긴밀한 관계에 있는 클래스 중 한 클래스를 다른 클래스의 내부에 선언하는 것이다.

  • 내부 클래스는 자신이 속해있는 외부 클래스 외에선 잘 사용되지 않아야 한다.

class A {
	/*내용 생략*/
}

class B {
	/*내용 생략*/
}

// 내부 클래스로 만들기
class A { // 외부 클래스
	...
    class B { // 내부 클래스 → A에서만 사용되야 그 목적을 살린다.
    	...
    }
	...
}

장점

  • 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.

  • 내부에 클래스를 숨김으로 코드의 복잡성을 줄일 수 있다.(캡슐화)



종류와 특징

내부 클래스의 종류는 변수 선언 위치에 따른 종류와 같다.

내부 클래스선언위치특징
인스턴스 클래스외부 클래스 멤버변수 선언위치인스턴스 멤버와 관련된 작업에 사용
스태틱 클래스외부 클래스 멤버변수 선언위치static멤버(메서드)와 관련된 작업에 사용
지역 클래스외부 클래스의 메서드, 초기화블럭선언된 영역 내부에서만 사용
익명 클래스선언과 인스턴스화를 동시에 하는 클래스

선언

// 외부 클래스
class Outer{
	class InstanceInner{} // 인스턴스 클래스 - 멤버변수 영역
    static class StaticInner{} // 정적 클래스 - 멤버변수 영역
    
    void method(){
    	class LocalInner{} // 지역 클래스 - 메서드 영역
    }
}

내부 클래스의 제어자와 접근성

  • 내부 클래스도 클래스라 abstract, final 같은 제어자를 사용할 수 있다.

  • 선언 위치에 따라 내부 클래스는 외부 클래스의 멤버로 간주되기도 하고, 메서드 내의 지역 변수처럼 다뤄지기도 한다.

  • 따라서 선언 위치에 따라 멤버 변수처럼 private, protected 같은 접근 제어자도 사용할 수 있다. (일반 클래스는 public, default만 사용가능)

  • 내부 클래스 중, static 클래스만 static멤버를 가질 수 있다.

  • 단, final이 붙은 변수는 상수라서 모든 내부 클래스에서 정의할 수 있다.

class Outer{
	class InstanceInner{
    	// static int a = 1; 에러
    }
    static class StaticInner{
    	static int a = 1;
    } 
    void method(){
    	class LocalInner{
        	final static int b = 10; // final이 붙어 상수라 가능
        }
    }
}
  • 인스턴스 멤버와 static 멤버의 관계처럼 내부 클래스 간의 관계도 같다.
    (생성 시기가 다름)
내부 클래스 종류외부 인스턴스 멤버외부 static 멤버인스턴스 클래스 멤버static 클래스 멤버메서드 영역
인스턴스 클래스객체 생성없이 사용 가능객체 생성없이 사용 가능객체 생성으로 사용가능클래스명.멤버명으로 접근X
static 클래스객체 생성없이 사용 불가객체 생성없이 사용 가능X클래스명.멤버명으로 접근X
지역 클래스OO객체 생성으로 사용가능클래스명.멤버명으로 접근상수 한정 가능
  • 지역 클래스의 경우 선언된 메서드의 지역변수가 상수인 경우에 참조할 수 있다.
    (소멸된 지역변수를 참조하려는 경우도 있어서)

  • 컴파일하면 외부 클래스명$내부 클래스명.class형식으로 파일명이 붙는다.

  • 단, 지역내부 클래스는 이름이 중복될 경우 클래스명 앞에 숫자를 붙인다.

  • 외부와 내부의 중복되는 변수명은 this로 구분된다.

class Outer{
	int value = 10; // Outer.this.value
    class Inner{
    	int value = 20; // this.value
    }
}


익명 클래스

이름 없는 일회용 익명 클래스.

new 조상클래스명(){
	// 멤버 선언
}

new 구현인터페이스명(){
	// 멤버 선언
}
  • 내부 클래스와 달리 이름이 없다. 대신 구별하기 쉽게 클래스명 뒤에 ()이 붙는다.

  • 선언과 생성이 동시에 일어나, 단 한번만 사용이 가능하다.

  • 객체 또한 딱 하나 밖에 생성할 수 있다.

  • 이름이 없어 생성자도 없으며, 조상 클래스명 or 인터페이스명을 사용한다.

  • 그래서 상속과 구현을 동시에 할 수 없고, 하나의 조상만 상속받거나 or 하나의 인터페이스만 구현해야 한다.

  • 클래스 파일명은 외부 클래스명$숫자.class으로 생성된다.


예시 1. 익명 클래스의 유형

class Anonymous(){
	Object iv = new Object(){ void method(){} };
    static Object cv = new Object(){ void method(){} };
    
    void myMethod() {
    	Object lv = new Object(){ void method(){} };
    }
}

예시 2. 익명 클래스로 변환

// 기존
class Inner{
	public static void main(String[] args) {
		Button b = new Button("start");
        b.addActionListener(new EventHandler());
}

class EventHandler implements ActionLisener{
	public void actionPerformed(ActionEvent e) {
    	System.out.println("ActionEvent occurred!")
    }
}

// 익명 클래스로 변환
class Inner{
	public static void main(String[] args) {
		Button b = new Button("start");
        b.addActionListener(new ActionLisener(){ // 익명 클래스로 대체
        		public void actionPerformed(ActionEvent e) {
    				System.out.println("ActionEvent occurred!")
    			}
        	}
        );
}


『 핵심 정리 』

  • 내부 클래스는 어떤 클래스 내부에 선언된 클래스이다.

  • 종류는 선언위치에 따른 변수의 종류와 유사하다.

  • 외부와 내부 클래스는 서로 긴밀한 관계로 연결된 작업을 처리하기 위해 사용한다.

  • 내부 클래스 사용 시, 외부 클래스 멤버에 쉽게 접근할 수 있고 동시에 밖으로 유출되지 않기 때문에 코드의 복잡성이 낮아진다.

  • 상수를 제외한 static멤버는 static 내부 클래스에만 정의할 수 있다.

  • 생성 시점에 따른 인스턴스 멤버와 static멤버의 관계는 내부 클래스끼리 or 내부-외부 클래스 관계에서도 유사하다.

  • 익명 클래스는 일회용 클래스로, 상속과 구현 둘 중 하나만 가능하며 객체도 한 개만 만들 수 있다.



도움이 되셨다면 '좋아요' 부탁드립니다 :)

profile
기록을 쌓아갑니다.

0개의 댓글