본문 바로가기

기술 서적/자바의 정석

[자바의 정석] ch7. 객체지향 프로그래밍 II

1. 상속

1-1. 상속의 정의와 장점

 

상속 : 기존 클래스를 재사용하여 새로운 클래스를 작성

=> 재사용성 : 적은 양의 코드로 새로운 클래스 작성

=> 공통규율 : 코드를 공통적으로 관리

 

사용방법

class Child extends Parent{
	//
}

 

>>상속 유의사항

- 멤버만 상속된다.

- 생성자와 초기화블록은 상속되지 않는다. 

- 자손 클래스의 멤버 개수는 조상클래스보다 항상 같거나 많다.

- 클래스간의 관계에서 형제관계는 없다. 오직 부모-자식의 관계만 존재한다.

- 자손 클래스의 객체 => 조상 클래스 멤버 + 자손 클래스 멤버

 

ex)

public class TV{
    String color;
    boolean power;
    int channel;

    void power(){power=!power;}
    void channelUp(){++channel;}
    void channelDown(){--channel;}
}

public class CaptionTv extends TV {
    boolean caption;
    void displayCaption (String text){
        if(caption){
            System.out.println(text);
        }
    }
}

public class CaptionTvTest {
    public static void main(String[] args) {
        CaptionTv ctv = new CaptionTv();
        // 부모 클래스의 멤버를 지님
        ctv.channel =10;
        ctv.channelUp();
        System.out.println(ctv.channel);
        ctv.displayCaption("Hello World");
        ctv.caption =true;
        ctv.displayCaption("Hello World");
    }
}

 

실행결과

11
Hello World


1-2. 클래스 간의 관계 - 포함관계

 

포함관계 : 한 클래스의 멤버변수로 다른 클래스 타입을 선언

 

<이전 코드>

class Circle {
	
    int x;
    int y;
    int r;
}
class Point {
	
    int x;
    int y;
}

class Circle {
	
    Point c= new Point();
    int r;
}

 

 

>>클래스 간의 관계 결정하기

IS-A (상속) : 일종의 ~이다.
HAS-A (포함) : ~를 가지고 있다.

 

--자바는 다중상속을 허용하지 않음

=> 오직 하나의 클래스에서만 상속 허용

=> 만약 다중상속이 필요하다면 하나는 상속 + 하나는 포함관계로 인스턴스 생성후 사용

 

ex)

public class TV{
    String color;
    boolean power;
    int channel;

    void power(){power=!power;}
    void channelUp(){++channel;}
    void channelDown(){--channel;}
}

public class VCR {
    boolean power;
    int counter=0;
    void power(){power=!power;}
    void play(){};
    void stop(){};
    void rew(){};
    void ff(){};

}

public class TVCR extends TV{
    VCR vcr =new VCR(); //내부 포함관계로 인스턴스를 만들어 사용
    
    void play(){vcr.play();}
    
    void stop(){vcr.stop();}
    void rew(){vcr.rew();}
    void ff(){vcr.ff();}
}

 

>>Object Class - 모든 클래스의 조상

: 모든 클래스 상속 계층도의 최상위 계층에 있는 조상 클래스

 


 

2. 오버라이딩

: 조상클래스로부터 상속받은 메서드의 내용을 변경하는 것

 

- 오버라이딩의 조건(시그니처가 같아야함)

1. 메서드 이름이 같아야 한다.
2. 매개변수가 같아야 한다.
3. 반환 타입이 같아야 한다.

 

주의사항

- 접근 제어자는 조상클래스 메서드보다 좁은 범위로 변경할 수 없다.
- 예외는 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
- 인스턴스 메서드를 static 메서드로 또는 그 반대로 변경할 수 없다.

=> 즉 상속을 받은 자손 클래스는 부모 클래스가 기본 옵션이다.

=> 그 옵션 내에서 변형이 가능한 것이지 본질적인 부분(제어자 범위, 던지는 예외 , static 등)을 변경할 수 있는 건 x

 

>>오버로딩 vs 오버라이딩

오버로딩 : 기존에 없는 새로운 메서드를 정의하는 것
오버라이딩  : 상속받은 메서드의 내용을 변경하는 것

 

 

>> super

- 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조

- this와 본질적으로 같음

- 조상클래스에서 선언된 멤버변수와 같은 이름의 멤버변수를 자손 클래스에서 반복정의할 수 있으며 super를 이용해 서로 구별

 

ex)

public class Parent {
    int x=10;
}

public class Child extends Parent{
    int x=20; // Parent 클래스와 같은 이름의 x가 정의되어 있음
    void method(){
        System.out.println("x="+x);
        System.out.println("this.x="+this.x);
        System.out.println("super.x="+super.x);
    }
}

 

>>실행결과

x=20

this.x=20

super.x=10

 

>>super() : 조상 클래스의 생성자

- Object클래스를 제외한 모든 클래스 생성자 첫줄에 생성자, this() 혹은 super()를 호출해야 한다. 그렇지 않으면 컴파일러가 자동적으로 super(); 를 생성자의 첫줄에 삽입한다.

=> 조상 클래스부터 순차적으로 생성자가 호출됨

=> 조상 클래스의 멤버변수는 조상의 생성자에 의해 초기화되어야 함

 


3. pacakge와 import 

 

- 패키지란?

: 서로 관련 있는 클래스의 묶음

: 클래스의 실제 이름에는 pacakge 명도 포함됨 ex) String 클래스 => java.lang.String

: 클래스가 물리적으로 하나의 클래스 파일 -> 패키지는 물리적으로 하나의 디렉토리

 

주의사항

- 패키지 선언 :  첫번째 문장 + 단 한번만

- 모든 클래스는 반드시 하나의 패키지에 속한다.

- 패키지는 점을 구분자로 하여 계층구조를 구성한다.

- 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.

 

 

>>패키지의 선언 : 첫번째 문장 + 단 한번만

package 패키지명

 

>>이름 없는 패키지(unnamed pacakge)

- 자바가 기본적으로 제공함

- 패키지를 지정하지 않으면 이 패키지에 속함

 

 

>>import문

: 타 패키지의 클래스를 사용할 때, 패키지명을 생략 가능

 

ex)

import 패키지명.클래스 명;
import 패키지명.*

 

=> 실행시 성능상의 차이는 전혀 없음

=> import 하는 패키지가 많을 땐, 어느 클래스가 어느 패키지에 속하는지 구별이 힘들 수 있음

=> import문이 하위 패키지의 클래스까지 포함하는 것은 아님

 

import java util.*

import java. text.*

≠ import java.*

 

우리가 System이나 String 클래스를 아무런 import문 없이 사용할 수 있었던 이유는

import java.lang.* 이 자동삽입되어 있기 때문

 

>>static import문

: static 멤버 호출 시 클래스 이름을 생략 가능

 

ex) 

package com.example;

import static java.lang.System.out;
import static java.lang.Math.*;

public class _07_staticImportEx1 {
    public static void main(String[] args) {
        //System.out.println(Math.random());
        out.println(random());

        //System.out.println("Math.PI : "+Math.PI);
        out.println("Math.PI : "+PI);
    }
}

 

Sytem.out.println(Math.random())   => out.println(random())

=> static import문을 통해 클래스 이름 System / Math 생략

 

 


4. 제어자

>>제어자란?

: 클래스, 변수, 메소드 선언부에 함께 사용되어 부가적인 의미를 부여

접근 제어자 : public, protected, default, private
그 외 : static, final, abstract, native, transient, synchronized, volatile, stirctfp

 

 

>> static : 클래스의 공통적인

제어자 대상 의미
static 멤버변수 - 클래스 변수 : 모든 인스턴스에 공통적으로 사용
- 인스턴스를 생성하지 않고도 사용 가능
- 메모리에 로드 될 때 생성
메서드 - static 메서드 : 인스턴스 생성하지 않고도 사용가능
- 인스턴스 변수를 사용할 수 없음

=> 인스턴스 멤버를 사용하지 않은 메서드는 static을 붙여 static 메서드로 선언

 

 

>>final : 마지막의 변경될 수 없는(상수화)

 

- 클래스=> 상속 불가

- 메서드 => 재정의 불가

제어자 대상 의미
final 클래스 상속 불가 : 변경될 수 없는 클래스 
메서드 재정의 불가 : 변경할 수 없는 메서드
멤버변수 상수 : 값을 변경할 수 없는 상수
지역변수

 

 

>>생성자를 이용한 final 변수 초기화

: 생성자를 통해 final 멤버 변수를 초기화 가능

: 만약 이게 불가능하면 final이 붙은 인스턴스 변수는 모든 객체에서 공통값을 가져야 함.

 

package ch7;

public class Card {
    final int NUMBER; //상수지만 선언과 함께 초기화하지 않음
    final String KIND;
    static int width=100;
    static int height= 250;

    Card(String kind, int num){
        KIND= kind; //생성자에서 final 변수 초기화 가능
        NUMBER=num; // 생성자에서 final 변수 초기화 가능
    }

    Card(){
        this("HEART", 1);
    }

    //문자열로 호출될 때 호출됨
    public String toString(){
        return KIND+" "+NUMBER;
    }
}



public class FinalCardTest {
    public static void main(String[] args) {
        Card c= new Card("HEART", 10);
        System.out.println(c.KIND);
        System.out.println(c.NUMBER);
        System.out.println(c);
    }
}

 

>>접근 제어자

: 해당 멤버 혹은 클래스의 접근 범위를 설정

 

-- 접근 제어자의 용도

: 외부로부터 데이터를 보호

: 외부에는 불필요한 내부적으로만 사용되는 부분을 감추기 위해(private)

 

  같은 클래스 같은 패키지 상속 클래스 전체
public O O O O
protected O O O  
(default) O O    
private O      

 

=> protected는 다른 패키지에 있는 상속 클래스에서도 접근 가능

=> 접근 제어자를 통해 접근 범위를 최소화하는 노력이 필요

 

>> getter 와 setter

- 변수의 유효성 검사를 할 수 있음

- getter : 멤버 변수 접근 제한 + 유일한 접근 경로 제공

- setter : 멤버 변수 유일한 초기화 경로 제공 + 변수의 제한 조건 검사

package ch7;

public class Time {
    private int hour, minute, second;
    
    public int getHour(){return hour;}
    public int getMinute(){return minute;}
    public int getSecond(){return second;}
    
    public void setHour(int hour){
        if(hour<0 ||hour>23){
            return;
        }
        this.hour= hour;
    }
    public void setMinute(int minute){
        if(minute<0 ||minute>59){
            return;
        }
        this.minute= minute;
    }
    public void setSecond(int second){
        if(second<0 ||second>59){
            return;
        }
        this.second= second;
    }
}

 

 

>>private 생성자

- 생성자를 private으로 하게 되면 외부에서 호출이 불가

- 상속불가 : 자손 클래스에서 생성자를 호출x

- 객체를 반환하는 public static 메소드가 필요(객체가 생성되기 전에 호출되어야 하므로)

 

package ch7;

public class Singleton {
    private static Singleton s= new Singleton(); //멤버 변수

    private Singleton(){
        //private 생성자이므로 외부에서 호출 불가
    }

    //객체를 반환하는 함수
    public static Singleton getInstance(){
        return s;
    }
}

public class SingletonTest {
    public static void main(String[] args) {
        //Singleton s = new Singleton(); //외부에서 호출이 불가
        Singleton s= Singleton.getInstance();
    }
}

 

 

>> 제어자 조합시 주의 사항

1) abstract 메서드에는 static 조합x
=>몸통이 있는 메서드만 메모리에 올릴 수 있음

2) abstract 클래스에는 final 조합x
=> abstract는 재정의를 기대하고 만든 클래스이다.
=> final 처리를 해버리면 수정이 불가함

3) abstract 메서드에는 private 조합x
=> abstract는 재정의를 기대하고 만든 클래스이다.
=> private으로 접근제어자를 지정하면 상속이 불가

4) private +final (무의미)
=> private: 클래스에서 밖에 접근 불가
=> final : 수정이 불가
=> 같은 목적을 지녔기에 하나만 사용해도 된다.

5. 다형성

 

>>다형성이란?

 

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

= 조상클래스 타입의 참조변수로 자손클래스의 객체를 참조 가능

= 반대로 자손타입의 참조변수로 조상타입의 인스턴스는 참조 불가 (추가 기능 수행이 불가하기 때문에)

=> 같은 타입의 인스턴스도 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라짐

 

ex)

package com.example;

public class TV{
    String color;
    boolean power;
    int channel;

    void power(){power=!power;}
    void channelUp(){++channel;}
    void channelDown(){--channel;}
}

public class CaptionTv extends TV {
    boolean caption;
    void displayCaption (String text){
        if(caption){
            System.out.println(text);
        }
    }
}

 

다음 상속관계에서 

TV t= new CaptionTV();   => 가능 : 자손클래스를 조상 클래스 참조변수가 가르키는 것
CaptionTV c = new TV(); => 불가능 : 조상 클래스를 자손 클래스 참조변수가 가르키는 것
                                        => c.displayCaption(text)를 수행하지 못하기 때문에

 

>>참조 변수의 형변환

- 형변환은 참조변수의 타입을 변환하는 것

== 인스턴스를 변환하지는 않음

: 참조변수의 형변환은 상속관계에서만 가능하다

자손 타입 >> 조상타입(UP-casting) : 형변환 생략 가능
조상타입 >> 자손타입(Down-casting) : 형변환 생략 불가

 

다운 캐스팅에서 형변환 생략이 불가능한 이유

: 실제 객체 멤버보다 참조변수가 사용할 수 있는 멤버의 개수가 더 많아지므로 문제가 발생 가능

 

ex)

package ch7;

public class CastingTest1 {
    public static void main(String[] args) {
        Car car = null; // 조상 클래스 참조변수
        FireEngine fe = new FireEngine(); // 자손클래스 참조변수
        FireEngine fe2= null;

        fe.water();
        car= fe; //car도 FireEngine 객체를 가리킴
        //car.water() ; => 에러!! car타입의 참조변수에는 water를 호출 못함
        fe2 = (FireEngine) car; // 자손 클래스의 변경은 형변환 생략 불가
        fe2.water();
        
    }
}


public class Car {
    String color;
    int door;

    void drive(){
        System.out.println("drive, Brrr~");
    }

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

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

 

>>실행결과

Water!!
Water!!

 

=> car는 water 메소드를 사용하지 못함(Car 클래스의 멤버함수가 아니기에)

=> fe/fe2는 FireEngine 참조변수이므로 water 메소드를 사용가능함

 

>>instanceof 연산자

: 참조변수가 참조하고 있는 인스턴스의 실제타입을 알아내는 연산자

: 참조변수 instanceof 클래스명

=> true라는 것은 참조변수가 검사한 타입으로 변환이 가능하다는 것을 의미

 

public 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());
    }

}

 

>>실행결과 

This is a FireEngine instance.
This is a Car instance
This is an Object instance
ch7.FireEngine

 

FireEngine 인스턴스는 Object 인스턴스와 Car 인스턴스를 포함하고 있기에

즉, fe는 일종의 Car로 볼 수 있으며 / 일종의 Object 라고 생각할 수 있고

각 조상 클래스로 형변환될 수 있기에 모든 if문에서 true가 반환된다.

 

 

>> 참조변수에 따른 인스턴스 변수

: cpp과 달리 java에서는 메서드는 항상 실제 인스턴스의 메서드(오버라이딩 된 메서드)가 호출(virtual이 옵션)

: 그러나, 멤버변수의 경우 이름이 중복될 경우, 참조변수의 타입에 따라 달라짐

 

조상 타입의 참조변수를 사용하면 => 조상 클래스에 선언된 멤버변수가 사용

자손 타입의 참조변수를 사용하면 => 자손 클래스에 선언된 멤버변수가 사용

 

 

ex)

package ch7;

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

		//이름이 중복되면 각 클래스에서 참고
        System.out.println("p.x = "+p.x); // 100
        System.out.println("c.x = "+c.x); // 200
        p.method(); //method의 경우 오버라이딩 된 것으로
        c.method();
    }
}

public class Parent {
    int x=100;
    void method(){
        System.out.println("Parent method");
    }
}

public class Child extends Parent{
    int x=200;
    void method(){
        System.out.println("Child Method");
    }
}

 

 

>> 실행결과(메소드는 오버라이드 된 걸로 / 변수는 각 클래스에서)

p.x = 100
c.x = 200
Child Method
Child Method

 

참조변수 타입에 따라 결과가 달라지는 경우는

조상 클래스의 멤버변수와 같은 이름의 멤버변수를 자손 클래스에 중복해서 정의한 경우

 

 

>>매개변수의 다형성

: 조상 클래스를 매개변수로 설정함으로써 각 클래스별 메소드를 만들지 않아도 됨

 

ex)

public class Buyer {
    int money =1000;
    int bonusPoint =0;
    
    void buy(Tv t){
        money-=t.price;
        bonusPoint = bonusPoint +t.bonusPoint;
    }

    void buy(Computer c){
        money-=c.price;
        bonusPoint = bonusPoint +c.bonusPoint;
    }

    void buy(Audio a){
        money-=a.price;
        bonusPoint = bonusPoint +a.bonusPoint;
    }
    
}

 

=> 위 3개의 메소드를 다음과 같이 요약가능

    void buy (Product p){
        money -=p.price;
        bonusPoint+= p.bonusPoint;
    }

 

 

ex2)

public void print(Object obj){
		write(String.valueOf(obj));
}

public static void String valueOf(Object obj){

		return (obj==null) ? "null" : obj.toString(); //어떤 객체던 toString을 호출
}

 

 

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

: 공통된 조상클래스 배열을 통해 각 요소가 다른 객체인 배열 생성 가능

 

--Vector : 내부 Object 배열로 배열의 크기를 알아서 관리해주는 클래스

메서드/생성자 설명
Vector() 10개의 객체를 저장할 수 있는 vector 인스턴스를 생성
boolean add(Object o) vector에 객체를 추가 => 성공하면 true / 실패하면 false
boolean remove(Object o) vector에서 객체 o를 찾아 삭제 => 성공하면 true / 실패하면 false
boolean isEmpty() vector가 비었는지 확인
Object get(int idx) idx의 객체를 반환 => 반환타입이 Object이므로 적절한 형변환 필요
int size() vector에 저장된 객체의 개수 반환

 

public class PolyArgumentTest {
    public static void main(String[] args) {
        Buyer b = new Buyer();
        Tv tv= new Tv();
        Computer com= new Computer();
        Audio audio= new Audio();

        b.buy(tv);
        b.buy(com);
        b.buy(audio);
        b.summary();
        System.out.println();
        b.refund(com);
        b.summary();
    }
}

public class Product {
    int price;
    int bonusPoint;

    Product(int price){
        this.price = price;
        this.bonusPoint = (int)(price/10.0);
    }

    Product(){
        this(0);
    }
}

public class Tv extends Product{
    Tv(){super(100);}

    public String toString() {
        return "TV";
    }
}

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

public class Audio extends Product{
    Audio(){
        super(50);
    }
    public String toString(){
        return "Audio";
    }
}

//vector 메소드에 주목
public class Buyer {
    int money =1000;
    int bonusPoint =0;
    Vector item = new Vector();


    void buy (Product p){
        if(p.price>money){
            System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
            return;
        }
        money -=p.price;
        bonusPoint+= p.bonusPoint;
        item.add(p);
        System.out.println(p+"을/를 구입하였습니다.");
    }

    //환불 기능
    void refund(Product p){
        if(item.remove(p)){
            money+=p.price;
            bonusPoint -=p.bonusPoint;
        }
        else{
            System.out.println("구입하신 제품 중 해당 제품이 없습니다.");
        }
    }

    void summary(){
        int sum=0;
        String itemList ="";

        if(item.isEmpty()){
            System.out.println("구입하신 제품이 없습니다.");
            return;
        }

        for(int i=0; i<item.size();i++){
            Product p= (Product)item.get(i); // 꺼낸 타입이 Object이므로 down-casting 필요
            sum+=p.price;
            itemList+=(i==0)? ""+p:". "+p;

        }
        System.out.println("구입하신 물품의 총 금액은 "+sum+"만원입니다.");
        System.out.println("구입하신 제품은 "+itemList +"입니다.");
    }
}

6. 추상클래스

 

추상클래스 : 미완성 메서드가 포함된 클래스

abstrcat class 클래스 이름{

}

- 추상클래스로는 인스턴스를 생성할 수 없음

- 조상으로 받은 추상 메서드 중 하나라도 구현하지 않으면 자손클래스 역시 추상클래스로 지정

=> 미완성이라는 메시지를 강하게 던짐

=> 공통된 부분을 찾아 추상화하는데 사용

 

주의사항

- 미완성 메서드는 구현부가 있으면 안됨

== void play(int pos) ≠ void play(int pos) {}

==> 전자는 미완성 메소드이고 후자는 아무런 구현부에 내용이 없어도 일반메서드임

- 추상 클래스를 공통조상 클래스로 만들고, 이를 통한 객체배열 생성이 가능함.

==> 공통 요소가 있으면 추상화해보자

 


7. 인터페이스

 

>>인터페이스란?

: 인터페이스는 추상클래스와 달리 몸통을 갖춘 일반 메서드를 가질 수 없다.

 

 

-- 작성방법

interface 인터페이스 이름{
    pubic static final 타입 상수이름 = 값
    public abstract 메서드 이름(매개변수 목록);
}

- 모든 멤버변수는 public static final 이어야하며 이를 생략 가능하다

- 모든 메소드는 public abstract이며 이를 생략할 수 있다.

- jdk1.8부터는 static 메서드와 default 메소드를 추가할 수 있음

 

 

>>인터페이스의 상속

-> 클래스와 달리 다중상속이 가능하다

interface Fightable extends Movable, Attackable {}

 

>>인터페이스의 구현

class Figther implements Fightable{
    public void move(int x, int y) {}
    public void attack(Unit u) {}
}

- 인터페이스 그 자체로는 인스턴스를 생성x

- implemets를 통해 구현된 클래스라는 의미를 사용

- 메서드 중 일부만 구현하면 abstract를 붙여서 추상 클래스로 선언

 

public class FighterTest {
    public static void main(String[] args) {
        Fighter f = new Fighter();
        
        if( f instanceof Unit){
            System.out.println("f는 Unit 클래스의 자손입니다.");
        }
        if( f instanceof Fightable){
            System.out.println("f는 Figthable인터페이스를 구현하였습니다.");
        }
        if(f instanceof Movable) {
            System.out.println("f는 Movable인터페이스를 구현하였습니다.");
        }
        if(f instanceof Attackable){
            System.out.println("f는 Attackable인터페이스를 구현하였습니다.");
        }
        if(f instanceof Object){
            System.out.println("f는 Object 클래스의 자손입니다.");
        }
    }
}

//1 . 상속 + 구현을 함께 할 수 있음
public class Fighter extends Unit implements Fightable{
    public void move(int x, int y){}; // 2. 인터페이스 메소드는 public abstract이기에 public 
    public void attack(Unit u){};
}

// 3. 인터페이스는 다중상속이 가능함
public interface Fightable extends Movable, Attackable{ }
public interface Attackable {    void attack(Unit u); } // 실제로는 public abstract void attack
public interface Movable { void move(int x, int y); } // 실제로는 public abstract void move

public class Unit {
    int currentHP;
    int x;
    int y;
}

 

1. 상속과 구현을 함께 할 수 있음 (extends + implements)

2. 인터페이스 메소드는 public abstract이기에 구현할 때도 public method로

3. 인터페이스는 다중상속이 가능함

 

>>실행결과

f는 Unit 클래스의 자손입니다.
f는 Figthable인터페이스를 구현하였습니다.
f는 Movable인터페이스를 구현하였습니다.
f는 Attackable인터페이스를 구현하였습니다.
f는 Object 클래스의 자손입니다.

 

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

다중상속의 문제점

: 상속받는 멤버 중 멤버변수 이름이 같거나 메서드 선언부가 일치하면 어느 조상의 것인지 판단 불가

=> 단점이 더 크기 때문에 자바에서는 클래스의 다중상속을 금지

 

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

자손클래스의 인스턴스를 조상타입의 참조변수로 참조하는 것이 가능함

== 인터페이스를 구현한 클래스의 객체를 인터페이스 타입 참조변수로 참조가 가능함

 

ex)

Fightable f= (Fightable) new Fighter();

 

즉, 매개변수나 리턴값에 인터페이스 타입을 준다는 것은

그 인터페이스를 구현한 클래스의 객체를 인수로 넣거나 반환한다는 것을 의미한다.

public interface Parseable {
    void parse(String fileName);
}

public class ParserManager {
    public static Parseable getParser(String type){
        if (type.equals("XML")){
            return new XMLParser();
        }
        else{
            return new HTMLParser();
        }
    }
}

public class HTMLParser implements Parseable{
    public void parse(String fileName){
        System.out.println(fileName+"-HTML parsing completed.");
    }
}

public class XMLParser implements Parseable{
    public void parse(String fileName){
        System.out.println(fileName+"-XML parsing completed.");
    }
}

public class ParserTest {
    public static void main(String[] args) {
        Parseable parser = ParserManager.getParser("XML");
        parser.parse("document.xml"); // XMLParser 반환
        parser= ParserManager.getParser("HTML"); // HTMLParser 반환
        parser.parse("document.html");
    }
}

 

- ParserManager 클래스의 getParser는 매개변수로 넘겨받은 type에 따라 XMLParser나 HTMLParser를 반환

=> Parserable이라는 인터페이스 참조변수로 포인팅하여 parse함

 

>> 인터페이스의 장점

1) 개발시간 단축
- 인터페이스 호출 쪽에서는 메서드 선언부만 가지고 동시에 프로그래밍 가능

2) 표준화 가능
- 공통된 기본 틀을 제공

3) 관계없는 클래스들에게 관계성 제공

4) 독립적인 프로그래밍 가능
: 클래스의 선언과 구현을 분리 => 구현에 있어 독립적인 프로그램 가능
: 한 클래스의 변경이 다른 클래스에 영향을 미치지 않도록 설계 가능

 

예시)

Unit 
GroundUnit AirUnit
Marine SCV Tank   DropShip

 

다음처럼 상속관계를 가진 클래스들이 있다고 가정하자

 

여기서 SCV가 Tank와 DropShip에게만 Repair라는 기능을 제공하려면 어떻게할까?

 

void repair(Tank t){
	//Tank를 수리한다.
}

void repair(DropShip d){
	//Dropship을 수리한다
}

 

이런 식으로 유닛마다 수리하는 repair메소드를 각각 따로 만들어주어야 할 것이다.

그러나, repair 그 내용을 살펴보면 사실 코드가 중복된다고 판단할 만큼 로직이 같다.

 

따라서, 우리는 Tank와 DropShip사이의 어떠한 관계성을 만들어서

같은 로직을 처리하는 프로세스를 고민해보아야 한다.

 

그리고 그러한 관계성을 부여하는 것이 인터페이스의 역할이다.

Repairable (interface)
SCV(class) Tank(class) DropShip(class)

즉, Repairable이라는 인터페이스를 Tank, Dropship, Scv가 구현하게 함으로써

공통성을 부여할 수 있다.

 

그리고 repair메소드의 매개변수 타입을 Repairable로 바꾸면 된다.

이는 확장성 측면에서도 앞으로 SCV에 의해 수리가 가능하도록 하려면

Repairable를 구현하도록만 하면 될 것이다.

 

public class RepairTest {
    public static void main(String[] args) {
        Tank tank = new Tank();
        DropShip dropship = new DropShip();
        SCV scv= new SCV();
        Marine marine = new Marine();
        
        scv.repair(tank);
        scv.repair(dropship);
        // scv.repair(marine); 에러! => Marine은 Repairable 인터페이스를 구현x
    }
}

public interface Repairable {
}

package ch7;

public class SCV extends GroundUnit implements Repairable{
    SCV(){
        super(60);
        hitPoint = MAX_HP;
    }

    void repair(Repairable r){
        if (r instanceof Unit){
            Unit u= (Unit)r; // Unit의 MAX_HP와 hitPoint를 쓰기위해서는 형변환 필요
            while(u.hitPoint!=u.MAX_HP){
                u.hitPoint++;
            }
            System.out.println(u.toString() + "의 수리가 끝났습니다.");
        }
    }
}

public class Tank extends GroundUnit implements Repairable{
    Tank(){
        super(150);
        hitPoint = MAX_HP;
    }

    public String toString(){
        return "Tank";
    }
}

public class DropShip extends AirUnit implements Repairable{

    DropShip(){
        super(125);
        hitPoint= MAX_HP;
    }
    public String toString(){
        return "DropShip";
    }
}

public class Marine extends GroundUnit{
    Marine(){
        super(40);
        hitPoint = MAX_HP;
    }
}

public class AirUnit extends Unit{
    AirUnit(int hp){
        super(hp);
    }
}

public class GroundUnit extends Unit{
    GroundUnit(int hp){
        super(hp);
    }
}

public class Unit {
    int hitPoint;
    final int MAX_HP;
    Unit(int hp){
        MAX_HP=hp;
    }
}

예시2) 공통성 부여

 

Building
Academey Bunker Barrack Factory

 

여기서 Barrack과 Factory 클래스에 건물을 이동시킬 수 잇는 다음 메서드를 추가하고자 하면 어떻게 해야할까?

void liftOff();
void move(int x, int y);
void stop();
void land();

 

가장 먼저 Barrack 클래스와 Factory 클래스에 모두 위의 코드를 적으면 되지만

코드가 중복된다는 단점이 분명 생긴다.

 

그렇다고 Building에 추가하면 전혀 상관없는 Academy와 Bunker에도 건물 옮기기 기능이 들어간다.

 

이런 경우에도 interface를 통해 공통성을 부여할 수 있다.

 

step1. 먼저 공통 기능을 추상화한 인터페이스를 생성한다.

public interface Liftable {
    void liftOff();
    void move(int x, int y);
    void stop();
    void land();
}

 

step2. 이 인터페이스를 구현한 클래스를 만든다.

public class LiftableImpl implements Liftable{
    public void liftOff(){/*내용생략*/};
    public void move(int x, int y){/*내용생략*/};
    public void stop(){/*내용생략*/};
    public void land(){/*내용생략*/};
    
}

 

step3. 공통성을 부여하고자 하는 클래스에 구현 클래스 인스턴스를 기반으로 구현한다.

public class Barrack implements Liftable{
    LiftableImpl l = new LiftableImpl();
    public void liftOff(){l.liftOff();}
    public void move(int x, int y){l.move(x,y);}
    public void stop(){l.stop();}
    public void land(){l.land();}

}

public class Factory implements Liftable{
    LiftableImpl l = new LiftableImpl();
    public void liftOff(){l.liftOff();}
    public void move(int x, int y){l.move(x,y);}
    public void stop(){l.stop();}
    public void land(){l.land();}

}

 


>> 인터페이스의 본질

1) 클래스를 사용하는 쪽(User)과 제공하는 쪽(Provider)이 있다.
2) 메서드를 사용하는 쪽에서는 사용하려는 메서드의 선언부만 알면 된다.

 

 

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


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

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

 

다음 interfaceTest에서는 A >>B 의 직접적인 관계가 성립한다.

따라서, B의 methodB() 선언부가 변경되면 A도 변경되어야 한다.

 

그러나, 인터페이스는 이러한 직접적인 관계를 간접적인 관계로 바꾸어 준다.

따라서, B에 변경사항이 생기거나 B가 다른 클래스도 대체되어도

A는 전혀 영향을 받지 않도록 하는 것이 가능하다.

 

다음 코드를 보자

public interface I {
    void methodB();
}

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

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

 

이젠 인터페이스 I가 A와 B를 매개하고 있다.

직접적 관계 : A >>B 

간접적 관계 : A >> I >> B

 

로 변경된 것이다.

 

따라서, 클래스 A는 사용하는 클래스 이름을 몰라도 된다.

public interface I {
    void play();
}

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

public class C implements I{
    public void play(){
        System.out.println("play in C class");
    }
}

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

public class InterfaceTest {
    public static void main(String[] args) {
        A a =new A();
        a.autoPlay(new B()); // void autoPlay(I i )호출
        a.autoPlay(new C()); // void autoPlay(I i )호출
    }
}

 

>>실행결과

play in B class
play in C class

 


 >>디폴트 메서드

- 인터페이스에 메서드를 추가하는 것 => 이 인터페이스를 구현한 모든 클래스에 새로운 메서드 추가해주어야 함

=> 기본적인 구현을 제공하는 디폴트 메서드를 추가

 

default void newMethod()

=> 조상 클래스에 새로운 메서드를 추가한 것과 동일한 효과

 


8. 내부 클래스

 

>>내부 클래스란?

클래스 내부에 선언된 클래스

: outer 클래스와 긴밀한 관계 형성

: 캡슐화 - 코드의 복잡성을 줄임

: 내부 클래스에서 외부 클래스 멤버를 쉽게 접근

 

>>내부 클래스의 종류와 특징

내부 클래스 특 징
인스턴스 클래스  외부 클래스의 인스턴스 멤버처럼 다루어짐
스태틱 클래스 - 외부 클래스의 static 멤버처럼 다루어짐
- 외부 클래스의 static 멤버변수만을 사용가능
지역 클래스 - 메서드나 초기화 블록 안에 선언
- 선언된 영역 내에서만 사용 가능
익명 클래스 - 이름없는 클래스(일회용)
- 클래스의 선언 + 객체 생성을 한번에

 

class Outer{
	class InstanceInner {} // 인스턴스 클래스
    static class StaticInner {} //static class
    
    void myMethod() {
    	class LocalInner {} //지역 클래스
    }
}

 

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

: 멤버변수와 같은 성질을 가짐

: 인스턴스 클래스 -> static 이 불가

: Local 클래스 => static이 불가 

-- (메모리에 올라가지 않았는데 올라가야하는 상황이 연출됨)

-- but, 인스턴스 클래스와 Local 클래스에 상수 (final static)은 가능

 

: static 클래스 -> static 변수 +인스턴스변수 둘다 가능

 

public class InnerEx1 {
    class InstanceInner{
        int iv=100;
        //static int cv=100; -> 에러! : static 변수를 선언 불가
        final static int CONST=100; //final static은 가능
    }
    
    static class StaticInner{
        int iv=200;
        static int cv=200; 
    }
    
    void myMethod(){
        class LocalInner{
            int iv=300;
            // static int cv=300; -> 에러! : static 변수를 선언 불가
            final static int CONST=300; //final static은 가능
        }
    }

    public static void main(String[] args) {
        System.out.println(InstanceInner.CONST);
        System.out.println(StaticInner.cv);
    }
}

 

 

>> 내부 클래스의 접근성

public class InnerEx3 {
    private int outerIv=0;
    private static int outerCv=0;
    
    //인스턴스 클래스의 접근성
    class InstanceInner{
        int iiv= outerIv;
        int iiv2= outerCv;
    }
    
    //static 클래스의 접근성 (static 만)
    static class StaticInner{
        //int siv= outerIv; -> 인스턴스 멤버에 접근 불가
        static int scv= outerCv;
    }
    
    void myMethod(){
        int lv=0;
        final int LV=0;
        
        //지역 클래스의 접근성
        class LocalInner{
            int liv  = outerIv;
            int liv2= outerCv;
            
            //외부 클래스의 지역변수는 final이 붙은 변수만 접근가능
            int liv3=lv; // jdk1.8부터 final을 자동으로 붙여줌
            int liv4=LV;
        }
    }

 

-- 인스턴스 클래스 : 외부 클래스의  [인스턴스 변수 + static 변수]

-- static 클래스 : 외부 클래스의  [static 변수]

-- 지역 클래스 : 외부 클래스의 [인스턴스 변수+ static 변수 + final 지역변수]

 

 

>> 내부클래스와 외부 클래스에 선언된 변수 명이 같을 때 구분

package ch7;

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

        void method1(){
            int value=30;
            System.out.println("value = "+value);
            System.out.println("this.value = "+this.value);
            System.out.println("Outer.this.value = "+ Outer.this.value);

        }
    }
}

public class InnerEx5 {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.method1();
    }
}

 

 

>> 실행결과

value = 30
this.value = 20
Outer.this.value = 10

 

 

>> 익명 클래스

: 클래스의 선언과 객체의 생성을 동시에

: 한번만 사용될 수 있고 하나의 객체만 생성할 수 있는 일회용 클래스

: 단 하나의 클래스만 상속

: 단 하나의 인터페이스만 구현

 

>>익명 클래스 활용 전

public class InnerEx8 {
    public static void main(String[] args) {
        Button b= new Button("Start");
        b.addActionListener(new EventHandler());
        
    }
}

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

 

>>익명 클래스 활용 후

package ch7;
import java.awt.*;
import java.awt.event.*;

public class InnerEx8 {
    public static void main(String[] args) {
        Button b= new Button("Start");
        
        //익명 클래스 활용 파트
        b.addActionListener(new ActionListener(){
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("ActionEvent occured!!");
            }
        });

    }
}