본문 바로가기

Java/자바 기초

7. 객체지향 프로그래밍 II

상속

상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.

상속을 통해서 클래스를 작성하면 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 있기 때문에 코드의 추가 및 변경이 매우 용이하다. 이러한 특징은 코드의 재사용성을 높이고 코드의 중복을 제거하여 프로그램의 생산성과 유지보수에 크게 기여한다.

 

상속을 구현하는 방법은 새로 작성하고자 하는 클래스의 이름 뒤에 상속받고자 하는 클래스 이름을 'extends'와 함께 써주면 된다.

class Parent {}
class Child extends Parent{
	...
}

 

위의 Parent 클래스와 Child 클래스는 서로 상속 관계에 있다고 하며, Parent 클래스는 '조상 클래스', Child 클래스는 '자손 클래스'라고 한다.

 

자손 클래스는 조상 클래스의 모든 멤버를 상속받기 때문에, Child 클래스는 Parent 클래스의 멤버들을 포함한다. (단, 생성자와 초기화 블럭은 상속되지 않는다.)

조상 클래스가 변경되면 자손 클래스는 자동적으로 영향을 받지만, 자손 클래스가 변경되는 것은 조상 클래스에 아무런 영향을 주지 못한다.

자손 클래스는 조상 클래스보다 같거나 많은 멤버를 갖으며, 상속을 거듭할수록 상속받는 클래스의 멤버 개수는 점점 증가하게 된다. 이는 상속에 사용되는 키워드가 'extends(확장)'인 이유라고 할 수 있다.

상속 예제

class Tv {
    boolean power; // 전원상태(on/off)
    int channel;    // 채널

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

class SmartTv extends Tv {  // CaptionTv는 Tv에 캡션(자막)을 보여주는 기능을 추가
    boolean caption;     // 캡션상태(on/off)
    void displayCaption(String text) {
        if (caption) {   // 캡션 상태가 on(true)일 때만 text를 보여 준다.
            System.out.println(text);
        }
    }
}

class Ex7_1 {
    public static void main(String args[]) {
        SmartTv stv = new SmartTv();
        stv.channel = 10;            // 조상 클래스로부터 상속받은 멤버
        stv.channelUp();            // 조상 클래스로부터 상속받은 멤버
        System.out.println(stv.channel);
        stv.displayCaption("Hello, World");
        stv.caption = true;    // 캡션(자막) 기능을 켠다.           
        stv.displayCaption("Hello, World");
    }
}

자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버도 함께 생성되기 때문에 따로 조상클래스의 인스턴스를 생성하지 않고도 조상 클래스의 멤버들을 사용할 수 있다.

클래스 간의 관계 - 포함관계

포함관계는 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것을 말한다.

클래스 간의 관계 결정하기

클래스를 작성하는데 있어서 상속관계와 포함관계를 결정하는 것에 대한 기준은 아래와 같다.

 

- 상속관계: ~은 ~이다. (is-a).

  ex) Sports Car는 Car이다.

 

- 포함관계: ~은 ~을 가지고 있다. (has-a)

  ex) Car는 Engine을 가지고 있다.

단일 상속(single inheritance)

자바에서는 단일 상속만 허용하므로, 둘 이상의 클래스로부터 상속받을 수 없다.

단일 상속은 하나의 조상 클래스만을 가지므로, 클래스 간의 관계가 보다 명확해지고 코드를 더욱 신뢰할 수 있게 만들어 준다.

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

Object클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스이며, 다른 클래스로부터 상속 받지 않는 모든 클래스들은 컴파일러에 의해 자동적으로 Object클래스로부터 상속받게 된다.

자바의 모든 클래스들은 Object클래스의 멤버들을 상속 받기 때문에 Object클래스에 정의된 멤버들을 사용할 수 있다. 주요 메서드로는 toString()과 equals(Object o)등이 있다.

오버라이딩(overriding)

오버라이딩은 조상클래스로부터 상속받은 메서드의 내용을 변경하는 것을 말한다.

override의 사전적 의미는 ‘~위에 덮어쓰다(overwirte)’이다.

class Point {
    int x;
    int y;

    String getLocation() {
        return "x :"+x+", y :"+y;
    }
}

class Point3D extends Point{
    int z;

    String getLocation() {  //오버라이딩
        return "x :"+x+", y :"+y+", z :"+z;
    }
}

오버라이딩의 조건

오버라이딩은 메서드의 내용만을 새로 작성하는 것이므로 메서드의 선언부, 즉 메서드이름, 매개변수, 반환타입은 조상의 것과 완전히 일치해야 한다. 단, 접근 제어자(access modifier)와 예외(exception)는 제한된 조건하에서만 다르게 변경할 수 있다.

 

위의 오버라이딩의 조건을 정리하면 아래와 같다.

- 선언부가 조상 클래스의 메서드와 일치해야 한다.

- 접근 제어자를 조상 클래스의 메서드보다 좁은 법위로 변경할 수 없다.

- 접근 제어자의 접근 범위를 넓은 것에서 좁은 것 순으로 나열하면 public, protected, (default), private이다.

- 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.

class Parent{
	void parentMethod() throws IOException, SQLException{
		...
	}
}


class Child extends Parent{
	//조상인 Parent클래스의 parentMethod()에 선언된 예외의 개수보다 적으므로
  //바르게 오버라이딩 되었다.
	void parentMethod() throws IOException{
		...
	}
}

오버로딩 vs. 오버라이딩

오버로딩(overloading) : 기존에 없는 새로운 메서드를 정의하는 것(new)

오버라이딩(overriding) : 상속받은 메서드의 내용을 변경하는 것(change, modify)

참조변수 super

super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수이며, 상속받은 멤버와 자신의 멤버를 구별하기 위해 사용한다.

class Ex7_2 {
	public static void main(String args[]) {
		Child c = new Child();
		c.method();
	}
}

class Parent { int x=10; //super.x }

class Child extends Parent {
	int x=20; // this.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( ) - 조상의 생성자

this( )처럼 super( )도 생성자이며, 조상의 생성자를 호출하는데 사용된다.

참고로 생성자는 상속되지 않는다.

public class Ex7_4 {
    public static void main(String[] args) {
        Point3D p = new Point3D(1, 2, 3);
        System.out.println("x=" + p.x + ",y=" + p.y + ",z=" + p.z);
    }
}

class Point {
    int x, y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

class Point3D extends Point {
    int z;

    Point3D(int x, int y, int z) {
        super(x, y); // Point(int x, int y)를 호출
        this.z = z;
    }
}

패키지(package)

패키지는 클래스의 묶음이다.

패키지에는 클래스 또는 인터페이스를 포함시킬 수 있으며, 서로 관련된 클래스들끼리 그룹 단위로 묶어 놓음으로써 클래스를 효율적으로 관리할 수 있다.

클래스의 실제 이름은 패키지명을 포함한 것이므로 같은 이름의 클래스일 지라도 서로 다른 패키지에 속하면 구별이 가능하다.

클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리이다.

패키지의 선언

패키지를 선언하는 방법은 클래스나 인터페이스의 소스파일(.java)의 맨 위에 아래와 같이 적어주면 된다.

 

package 패키지명;

 

패키지명은 대소문자를 모두 허용하지만, 클래스명과 쉽게 구분하기 위해서 소문자로 하는 것을 원칙으로 한다.

package com.codechobo.book; //PackageTest클래스가 속할 패키지의 선언

public class PackageTest {

	public static void main(String[] args) {
		System.out.println("Hello World!");
	}
}

import문

import문을 통해 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스코드에 사용되는 클래스이름에서 패키지명 생략이 가능하다.

 

java.util.Date today = new java.util.Date();
import java.util.Date;

...

Date today = new Date();

 

import문의 역할은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하는 것이다. 컴파일 시에 컴파일러는 import문을 통해 소스파일에 사용된 클래스들의 패키지를 알아 낸 다음, 모든 클래스이름 앞에 패키지명을 붙여 준다.

 

import문은 클래스 선언문 이전에 위치하며, 한 소스파일에 여러 번 선언할 수 있다.

 

import문을 선언하는 방법은 아래와 같다.

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

- import 패키지명.*;

 

'패키지명.*'을 사용하면 지정된 패키지에 속하는 모든 클래스를 패키지명 없이 사용가능하며, 실행 시 성능상의 차이는 전혀 없다.

static import문

static import문을 사용하면 static멤버를 호출할 때, 클래스 이름을 생략할 수 있다.

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

class Ex7_6 {
    public static void main(String[] args) {    
        // System.out.println(Math.random());
        out.println(random()); //System과 Math를 생략했다.

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

제어자(modifier)

제어자(modifier)는 클래스, 변수 또는 메서드의 선언부와 함께 사용되어 부가적인 의미를 부여한다.

제어자는 클래스나 멤버변수와 메서드에 주로 사용되며, 하나의 대상에 대해서 여러 제어자를 조합하여 사용가능하다.

단, 접근 제어자는 한 번에 네가지 중 하나만 선택해서 사용가능하다.

 

- 접근 제어자: public, protected, (default), private

- 그 외: static, final, abstract, native, transient, synchronized, volatile, strictfp

          static, final, abstract을 제외하고는 자주 사용되지 않는다.

static - 클래스의, 공통적인

static은 '클래스의' 또는 '공통적인'의 의미를 가지고 있으며, 멤버변수, 메서드, 초기화 블럭에 사용될 수 있다.

class StaticTest{
    static int width = 200; //클래스 변수 (static 변수)

    static { //클래스 초기화 블럭
        //static 변수의 복잡한 초기화 수행
    }

    static int max(int a, int b) { //클래스 메서드 (static 메서드)
        return a > b ? a : b;
    }
}

final - 마지막의, 변경될 수 없는

final은 '마지막의' 또는 '변경될 수 없는'의 의미를 가지고 있으며, 클래스, 메서드, 멤벼변수, 지역변수에 사용될 수 있다.

final class FinalTest{  //조상이 될 수 없는 클래스.
    final int MAX_SIZE = 10; //값을 변경할 수 없는 멤버변수 (상수)

    final void getMaxSize() { //오버라이딩할 수 없는 메서드(변경 불가)
        final int LV = MAX_SIZE; //값을 변경할 수 없는 지역변수(상수)
        return MAX_SIZE;
    }
}

abstract - 추상의, 미완성의

abstract는 '미완성'의 의미를 가지고 있으며, 클래스, 매서드에 사용될 수 있다.

추상 클래스는 아직 완성되지 않은 메서드가 존재하는 ‘미완성 설계도’이므로 인스턴스를 생성할 수 없다.

AbstractTest a = new AbstractTest(); //에러. 추상 클래스의 인스턴스 생성 불가

접근 제어자(access modifier)

접근 제어자는 멤버 또는 클래스에 사용되어, 해당하는 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 한다. 접근 제어자가 지정되지 않다면, 접근 제어자가 default임을 뜻한다.

캡슐화와 접근 제어자

클래스나 멤버에 접근 제어자를 사용하는 이유는 클래스의 내부에 선언된 데이터를 보호하기 위함이며, 이를 객체지향개념의 캡슐화(encapsulation)에 해당하는 데이터 감추기(data hiding)라고 한다.

 

일반적으로 private나 protected로 멤버변수를 제한하고 public메서드를 제공함으로써 간접적으로 멤벼변수 값을 다룰 수 있도록 하는 것이 바람직하다.

 

접근 제어자를 사용하는 이유

- 외부로부터 데이터를 보호하기 위해서

- 외부에는 불필요한, 내부적으로만 사용되는, 부분을 감추기 위해서

final class Time{ 
    private int hour;  //접근 제어자를 private로 하여 외부에서 직접 접근하지 못하도록 한다.

    public int getHour() { return Hhour; }
    public void setHour(int hour){
        if(hour < 0 || hour > 23) return;
        this.hour = hour;
    }

}

만일 상속을 통해 확장될 것이 예상되는 클래스라면 멤버에 접근 제한을 주되 자손클래스에서 접근하는 것이 가능하도록 하기 위해 private 대신 protected를 사용한다. private이 붙은 멤버는 자손 클래스에서도 접근이 불가능하기 때문이다.

다형성(polymorphism)

다형성은 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.

 

일반적으로 인스턴스의 타입과 참조변수의 타입이 일치하지만, 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하는 것도 가능하다.

class Tv{
    boolean power;  // 전원상태(on/off)
    int channel;    //채널

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

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

//참조 변수와 인스턴스 타입 일치
SmartTv s = new SmartTv();

//조상 타입 참조변수로 자손 타입 인스턴스 참조
Tv t = new SmartTv();

위의 그림과 같이, 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.

 

조상타입의 참조변수로 자손타입의 인스턴스를 참조하는 경우, 조상 클래스의 멤버만 사용 가능하며, 자손 클래스에 생성된 멤버는 사용이 불가능하다. 이와 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 허용하지 않는데, 이는 자손타입의 참조변수가 사용할 수 있는 멤버의 개수가 조상의 멤버의 개수보다 많기 때문이다.

참조변수의 형변환

참조변수의 형변환은 사용할 수 있는 멤버의 개수를 조절하기 위한 것이며, 서로 상속관계에 있는 클래스 사이에서만 가능하다.

 

자손타입에서 조상타입으로 형변환하는 경우, 형변환은 생략이 가능(Up-casting)하지만 조상타입에서 자손타입으로 형변환하는 경우, 형변환은 생략이 불가능(Down-casting)하다.

 

기본형의 형변환과 달리 참조형의 형변환은 변수에 저장된 값(주소값)이 변환되는 것이 아니다.

 

참조변수의 형변환은 그저 리모컨(참조변수)을 다른 종류의 것으로 바꾸는 것 뿐이다. 리모컨의 타입을 바꾸는 이유는 사용할 수 있는 멤버 개수를 조절하기 위한 것이다.

참조변수의 형변환 예제

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

        fe.water();
        car = fe;    // car = (Car)fe;에서 형변환이 생략됨
//        car.water();  //컴파일 에러! Car타입의 참조변수로는 water()를 호출할 수 없다.
        fe2 = (FireEngine)car; // 자손타입 ← 조상타입. 형변환 생략 불가
        fe2.water();
    }
}

class Car {
    String color;
    int door;

    void drive() {     // 운전하는 기능
        System.out.println("drive, Brrrr~");
    }

    void stop() {      // 멈추는 기능    
        System.out.println("stop!!!");    
    }
}

class FireEngine extends Car {    // 소방차
    void water() {    // 물을 뿌리는 기능
        System.out.println("water!!!");
    }
}

instanceof 연산자

instanceof 연산자는 참조변수가 참조하는 인스턴스의 실제 타입을 확인하는데 사용하며, 연산의 결과는 boolean타입으로 참조변수가 검사한 타입으로 형변환이 가능하면 true, 형변환이 불가능하면 false를 반환한다.

void doWork(Car c){
    if(c instanceof FireEngine) {
        FireEngine fe = (FireEngine)c;  //1. 형변환이 가능한지 확인
        fe.water();                     //2. 형변환
        ...

매개변수의 다형성

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

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

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

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

    // Object클래스의 toString()을 오버라이딩한다.
    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) {  //Product와 같은 타입 혹은 자손 타입을 매개변수로 받음.
        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 + "점입니다.");
    }
}

/*
    출력 결과

    Tv을/를 구입하셨습니다.
    Computer을/를 구입하셨습니다.
    현재 남은 돈은 700만원입니다.
  현재 보너스점수는 30점입니다.

*/

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

조상 타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.

//Buyer2 클래스의 cart에 조상타입(Product2)의 참조변수 배열을 사용했다.

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

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

    Product2() {} // 기본 생성자
}

class Tv2 extends Product2 {
    Tv2() {  super(100);     }

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

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

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

class Buyer2 {              // 고객, 물건을 사는 사람
    int money = 1000;      // 소유금액
    int bonusPoint = 0; // 보너스점수
    Product2[] cart = new Product2[10];   // 구입한 제품을 저장하기 위한 배열
    int i =0;              // Product배열에 사용될 카운터

    void buy(Product2 p) {
        if(money < p.price) {
            System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
            return;
        }

        money -= p.price;             // 가진 돈에서 구입한 제품의 가격을 뺀다.
        bonusPoint += p.bonusPoint;   // 제품의 보너스 점수를 추가한다.
        cart[i++] = p;                // 제품을 Product[] cart에 저장한다.
        System.out.println(p + "을/를 구입하셨습니다.");
    }
// 뒷 페이지에 계속됩니다.
    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 + "입니다.");
    }
}

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

        b.buy(new Tv2());
        b.buy(new Computer2());
        b.buy(new Audio2());
        b.summary();
    }
}

/*
    출력 결과

    Tv을/를 구입하셨습니다.
    Computer을/를 구입하셨습니다.
    Audio을/를 구입하셨습니다.
    구입하신 물품의 총금액은 350만원입니다.
  구입하신 제품은 Tv, Computer, Audio, 입니다.

*/

추상 클래스(abstract class)

클래스를 설계도에 비유한다면, 추상 클래스는 미완성 설계도라고 할 수 있다.

클래스가 미완성이라는 것은 미완성 메서드(추상 메서드)를 포함하고 있다는 의미이며, 완성된 설계도가 아니므로 인스턴스를 생성할 수 없다.

 

추상 클래스 자체로는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는데 조상 클래스로서 도움을 줄 목적으로 작성되며, 키워드 'abstract'만 붙이기만 하면 된다.

abstract class 클래스이름 { }

 

추상 클래스에서 생성자가 있으며, 멤버변수와 메서드도 가질 수 있다.

추상 메서드(abstract method)

추상 메서드는 선언부만 있고 구현부(몸통, body)가 없는 메서드이다.

추상 메서드는 상속받는 클래스에 따라 다르게 구현될 것으로 예상되는 경우에 사용하며, 추상클래스를 상속받는 자손 클래스는 추상메서드의 구현부를 상황에 맞게 적절히 구현해주어야 한다.

 

추상 메서드도 키워드 'abstract'를 앞에 붙이고 구현부가 없으므로 괄호{ } 대신 문장의 끝을 알리는 ';'을 붙이면 된다.

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

추상 클래스의 작성

추상의 사전적 정의는 '낱낱의 구체적 표상이나 개념에서 공통된 성질을 뽑아 이를 일반적인 개념으로 파악하는 정신 작용' 이다. 추상의 사전적 정의처럼 여러 클래스에 공통적으로 사용될 수 있는 추상클래스를 바로 작성하기도 하고, 기존의 클래스의 공통적인 부분을 뽑아서 추상클래스로 만들어 상속하는 경우도 있다.

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;
 //Marine, Tank, DropShip에서 공통으로 쓰이는 메서드.
    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() { /* 선택된 대상을 내린다. */ }
}

/*
    출력 결과

    Marine[x=100,y=200]
    Tank[x=100,y=200]
    Dropship[x=100,y=200]
*/

인터페이스(interface)

인터페이스는 일종의 추상클래스이며, 추상클래스와 달리 구현부를 갖춘 일반 메서드와 멤버변수를 구성원으로 가질 수 없으며, 오직 추상메서드와 상수만을 멤버로 가질 수 있기 때문에, 추상클래스보다 추상화 정도가 높다고 할 수 있다.

 

추상클래스를 부분적으로만 미완성된 '미완성 설계도'라고 한다면, 인터페이스는 밑그림만 그려져 있는 '기본 설계도'라고 할 수 있으며, 미리 정해진 규칙에 맞게 구현하도록 표준을 제시하는 역할을 한다.

 

인터페이스를 작성하는 방법은 키워드 'class' 대신 'interface'를 사용하는 것 외에는 클래스 작성과 동일하며, 접근제어자로 public이나 (default)만 사용합니다.

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

 

일반적인 클래스와 달리 인터페이스의 멤버들은 다음과 같은 제약사항이 있다.

- 모든 멤버변수public static final 이어야 하며, 이를 생략할 수 있다.

- 모든 메서드public abstract 이어야 하며, 이를 생략할 수 있다.

- 단, static 메서드와 디폴트 메서드는 예외(JDK1.8부터)

 

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

interface PlayingCard {
    public static final in SPADE = 4;
    final int DIAMOND = 3; // public static final int DIAMOND = 3;
    static int HEART = 2; // public static final int HEART = 2;
    int CLOVER = 1; // public static final int CLOVER = 1;

    public abstract String getCardNum();
    String getCardKind(); // public abstract String getCardKind();
}

인터페이스의 상속

인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중상속, 즉 여러 개의 인터페이스로 부터 상속 받는 것이 가능하다.

인터페이스는 Object와 같은 최고 조상이 없다.

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

interface Attackable {
    void attack(int x, int y);
}

interface Fightable extends Movable, Attackable {}

인터페이스의 구현

인터페이스를 구현하는 것은 클래스를 상속받는 것과 같으며, 키워드 'expends' 대신 키워드 'implements'를 사용한다.

인터페이스를 구현하면 인터페이스에 정의된 추상메서드를 완성해야 하며, 인터페이스의 메서드 중 일부만 구현한다면 'abstract'를 붙여서 추상클래스로 선언해야 한다.

또한 상속과 구현을 동시에 할 수 있다.

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

인터페이스는 인터페이스를 구현한 클래스의 조상이라고 할 수 있으므로 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로의 형변환도 가능하다.

 

// 인터페이스 Fightable을 클래스 Fighter가 구현했을 때
Fightable f = (Fightable)new Fighter();
Fightable f = new Fighter();

 

또한 인터페이스는 메서드의 매개변수의 타입으로 사용될 수 있으며, 메서드의 리턴타입으로 인터페이스를 지정하는 것도 가능하다.

public void attack(Fightable f){}

 

리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

Fitable method() {    
      Fither f = new Fighter();
      return f;    
    }

인터페이스의 장점

  • 개발시간을 단축시킬 수 있다.
  • 표준화가 가능하다.
  • 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
  • 독립적인 프로그래밍이 가능하다.

디폴트 메서드와 static메서드 예제

인터페이스에 추상 메서드만 선언할 수 있는데, JDK1.8부터 디폴트 메서드와 static 메서드도 인터페이스에 추가할 수 있게 되었다. static 메서드는 인스턴스와 관계 없는 독립적인 메서드이므로, 인터페이스에 추가할 자격은 충분하다.

 

디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로서, 일반 메서드처럼 구현부{ }를 가지고 있으며, 앞에 키워드 'default'를 붙이고 접근 제어자는 항상 public이며, 생략 가능하다.

interface MyInterface{
	void method();
	default void newMethod(){} //public 생략 가능
}

 

만약, 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우, 이 충돌을 해결하는 규칙은 다음과 같다.

 

- 여러 인터페이스의 디폴트 메서드 간의 충돌

  인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.

 

- 디폴트 메서드와 조상클래스의 메서드 간의 충돌

  조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

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

class Child3 extends Parent3 implements MyInterface, MyInterface2 {
// 여러 인터페이스의 디폴트 메서드 간의 충돌
// 디폴트 메서드를 오버라이딩
    public void method1() {    
        System.out.println("method1() in Child3"); 
    }            
}

class Parent3 {
// 디폴트 메서드와 조상클래스의 메서드 간의 충돌
// 조상 클래스(Parent3)의 메서드가 상속되고, 디폴트 메서드는 무시된다.
    public void method2() {    
        System.out.println("method2() in Parent3"); 
    }
}

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");
    }
}

/*
    출력 결과

    method1() in Child3
    method2() in Parent3
    staticMethod() in MyInterface
    staticMethod() in MyInterface2
*/

내부 클래스(inner class)

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

내부 클래스를 선언하면 두 클래스의 멤버들 간에 서로 쉽게 접근할 수 있으며, 외부에는 불필요한 클래스를 내부로 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점을 가지고 있다.

내부 클래스의 종류와 특징

내부 클래스의 종류는 변수의 선언위치에 따른 종류와 동일하며, 유효범위와 성질도 변수와 유사하다.

내부 클래스의 선언

내부 클래스의 선언위치는 변수의 선언위치와 동일하며, 각 내부 클래스의 선언 위치에 따라 변수와 동일한 유효범위(scope)와 접근성(accessibility)을 갖는다.

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

내부 클래스는 외부 클래스의 멤버와 같이 간주되고, 인스턴스멤버와 static멤버 간의 규칙이 내부클래스에도 똑같이 적용된다.

 

내부 클래스도 클래스이므로 'abstract'나 'final'과 같은 제어자를 사용할 수 있을 뿐만 아니라, 멤버변수들처럼 'private'나 'protected'와 같은 접근제어자도 사용 가능하다.

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

내부 클래스 중에서 스태틱 클래스만 static멤버를 가질 수 있다.

final과 static이 동시에 붙은 변수는 상수(constant)이므로 모든 내부 클래스에서 정의가 가능하다.

class Ex7_12 { 
    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;    // static클래스만 static멤버를 정의할 수 있다. 
    } 

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

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

내부 클래스도 외부 클래스의 멤버로 간주되며, 동일한 접근성을 갖는다.

인스턴스 클래스는 외부 클래스의 인스턴스 멤버를 객체생성 없이 바로 사용할 수 있지만, 스태틱 클래스는 외부 클래스의 인스턴스 멤버를 객체생성 없이 사용할 수 없다.

class Ex7_13 {
    class InstanceInner {}
    static class StaticInner {}

    // 인스턴스멤버 간에는 서로 직접 접근이 가능하다.
    InstanceInner iv = new InstanceInner();
    // static 멤버 간에는 서로 직접 접근이 가능하다.
    static StaticInner cv = new StaticInner();

    static void staticMethod() {
      // static멤버는 인스턴스멤버에 직접 접근할 수 없다.
//        InstanceInner obj1 = new InstanceInner();    
        StaticInner obj2 = new StaticInner();

      // 굳이 접근하려면 아래와 같이 객체를 생성해야 한다.
      // 인스턴스클래스는 외부 클래스를 먼저 생성해야만 생성할 수 있다.
        Ex7_13 outer = new Ex7_13();
        InstanceInner obj1 = outer.new InstanceInner();
    }

    void instanceMethod() {
      // 인스턴스메서드에서는 인스턴스멤버와 static멤버 모두 접근 가능하다.
        InstanceInner obj1 = new InstanceInner();
        StaticInner obj2 = new StaticInner();
        // 메서드 내에 지역적으로 선언된 내부 클래스는 외부에서 접근할 수 없다.
//        LocalInner lv = new LocalInner();
    }

    void myMethod() {
        class LocalInner {}
        LocalInner lv = new LocalInner();
    }
}

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

내부 클래스에서 외부 클래스의 변수들에 대한 접근성을 보여주는 예제이다.

class Outer {
    private int outerIv = 0;
    static  int outerCv = 0;

    class InstanceInner {
        int iiv  = outerIv;  // 외부 클래스의 private멤버도 접근가능하다.
        int iiv2 = outerCv;
    }

    static class StaticInner {
// 스태틱 클래스는 외부 클래스의 인스턴스멤버에 접근할 수 없다.
//        int siv = outerIv;
        static int scv = outerCv;
    }

    void myMethod() {
        int lv = 0;
        final int LV = 0;  // JDK1.8부터 final 생략 가능

        class LocalInner {
            int liv  = outerIv;
            int liv2 = outerCv;
//    외부 클래스의 지역변수는 final이 붙은 변수(상수)만 접근가능하다.
//            int liv3 = lv;    // 에러!!!(JDK1.8부터 에러 아님. 컴파일러가 lv에 final 자동 추가)
            int liv4 = LV;    // OK
        }
    }
}

스태틱 클래스(Static Inner)는 외부 클래스(Outer)의 static 멤버이기 때문에 외부 클래스의 인스턴스멤버인 outerIv와 InstanceInner를 사용할 수 없다. 단지 static 멤버인 outerCv만을 사용할 수 있다.

 

지역 클래스(LocalInner)는 외부 클래스의 인스턴스멤버와 static멤버를 모두 사용할 수 있으며, 지역 클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다.

 

단, final이 붙은 지역변수만 접근가능한데 그 이유는 메서드가 수행을 마쳐서 지역변수가 소멸한 시점에도, 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다.

 

JDK1.8부터 지역 클래스에서 접근하는 지역 변수 앞에 final을 생략할 수 있게 바뀌었다. 대신 컴파일러가 자동으로 붙여준다. 즉, 편의상 final을 생략할 수 있게 한 것일 뿐 해당 변수의 값이 바뀌는 문장이 있으면 컴파일 에러가 발생한다.

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

외부 클래스가 아닌 다른 클래스에서 내부 클래스를 생성하고 내부 클래스의 멤버에 접근하는 예제이다.

class Outer2 {
    class InstanceInner {
        int iv = 100;
    }

    static class StaticInner {
        int iv = 200;
        static int cv = 300;
    }

    void myMethod() {
        class LocalInner {
            int iv = 400;
        }
    }
}

class Ex7_15 {
    public static void main(String[] args) {
        // 인스턴스클래스의 인스턴스를 생성하려면
        // 외부 클래스의 인스턴스를 먼저 생성해야 한다.
        Outer2 oc = new Outer2();
        Outer2.InstanceInner ii = oc.new InstanceInner();

        System.out.println("ii.iv : "+ ii.iv);
        System.out.println("Outer2.StaticInner.cv : "+Outer2.StaticInner.cv);

       // 스태틱 내부 클래스의 인스턴스는 외부 클래스를 먼저 생성하지 않아도 된다.
        Outer2.StaticInner si = new Outer2.StaticInner();
        System.out.println("si.iv : "+ si.iv);
    }
}

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

내부 클래스와 외부 클래스에 선언된 변수의 이름이 같을 때, 변수 앞에 'this' 또는 '외부클래스명.this'를 붙여서 서로 구별할 수 있다.

class Outer3 {
    int value = 10;    // Outer3.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("Outer3.this.value :" + Outer3.this.value);
        }
    } // Inner클래스의 끝
} // Outer클래스의 끝

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

/*
    출력 결과

                value :30
           this.value :20
    Outer3.this.value :10
*/

익명 클래스(anonymous class)

클래스는 이름이 없는 클래스로 클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용될 수 있고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다.

class Ex7_17 {
    Object iv = new Object(){ void method(){} };         // 익명 클래스
    static Object cv = new Object(){ void method(){} };  // 익명 클래스

    void myMethod() {
        Object lv = new Object(){ void method(){} };      // 익명 클래스
    }
}

익명 클래스(anonymous class) 예제

두 개의 독립된 클래스를 익명클래스로 변환하여 보다 쉽게 코드를 작성할 수 있다.

EventHandler 클래스를 익명클래스로 만든다.

class Ex7_18 {
    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!!!");
    }
}

 

class Ex7_19 {
    public static void main(String[] args) {
        Button b = new Button("Start");
        b.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    System.out.println("ActionEvent occurred!!!");
                }
            } // 익명 클래스의 끝
        );
    } // main의 끝
}

'Java > 자바 기초' 카테고리의 다른 글

9. java.lang 패키지와 유용한 클래스  (0) 2022.03.14
8. 예외처리  (0) 2022.03.14
6. 객체지향 프로그래밍 I  (0) 2022.03.11
5. 배열  (0) 2022.03.11
4. 조건문과 반복문  (0) 2022.03.11