Language/Java

[Java] 객체지향 프로그래밍2 (다형성, 추상클래스, 인터페이스)

Ella_K 2022. 12. 28. 00:30

다형성

  • 다형성: 여러가지 형태를 가질 수 있는 능력
  • 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현
  • 조상 클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있다.
    • 참조 변수가 가리킬 수 있는 멤버변수만 사용이 가능하다.
    • 참조 변수의 타입이 참조변수가 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 결정한다.
조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다.
반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수 없다.

 

참조변수의 형변환

  • 자손타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입의 참조변수로의 형변환만 가능하다.
  • 형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다.
  • 참조변수의 형변환을 통해서 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절한다.
  • 서로 상속관계에 있는 타입간의 형변환은 양방향으로 가능하다. 단, 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수 없기 때문에  참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다.

 

instanceof 연산자

  • 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 사용
  • 참조변수 instance of 타입 → boolean 값인 true와 false 중 반환
  • true: 같은 타입의 연산 이외에 조상타입의 연산에도 true를 얻는다. → 참조변수가 가리키는 인스턴스와 같은 타입 또는 조상타입으로 형변환 → 참조변수가 검사한 타입으로 형변환이 가능하다.
void doWork(Car c){
    if (c instanceof FireEngine){
        FireEngine fe = (FireEngine) c;
        fe.water();
    }
}

 

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

  • 인스턴스 메서드는 항상 실제 인스턴스의 메서드가 실행된다.
  • 멤버변수는 참조변수의 타입에 따라 달라진다.

 

매개변수의 다형성

  • 매개변수에 조상타입을 이용하여 자손 인스턴스들을 동적으로 제공받을 수 있다.
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(Product p){
        money -= p.price;
        bonusPoint += p.bonusPoint;
    }
}

 

 

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

  • 조상타입의 참조변수 배열을 사용해 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.
Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();

 


추상클래스

  • 미완성 설계도
  • 미완성 메서드(추상 메서드)를 포함하고 있는 클래스 (추상클래스에도 생성자가 있으며, 멤버변수와 메서드도 가질 수 있다.)
  • 추상클래스는 인스턴스를 생성할 수 없으며 상속을 통해서 자손클래스에 의해서만 완성될 수 있다.
  • 새로운 클래스를 작성하는데 도움을 주는 역할(아무 것도 없는 상태에서 시작하는 것보다는 완전하지는 못하더라도 어느정도 틀을 갖춘 상태에서 시작하는 것이 낫다. )
abstract class 클래스이름 {
...
}

 

추상메서드

  • 선언부만 작성하고 구현부는 작성하지 않은 메서드
  • 추상클래스로부터 상속받는 자손클래스는 오버라이딩을 통해 조상인 추상클래스의 추상 메서드를 모두 구현해주어야 한다. 조상으로부터 상속받은 추상메서드 중 하나라도 구현하지 않는다면, 자손클래스 역시 추상클래스로 지정해주어야 한다.
/*주석을 통해 어떤 기능을 수행할 목적으로 작성하였는지 설명*/
abstract 리턴타입 메서드이름();

 

추상클래스 작성

  • 추상화: 클래스간의 공통점을 찾아내서 공통의 조상을 만드는 작업
  • 구체화: 상속을 통해 클래스를 구현, 확장하는 작업
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) {
        // 지정된 위치로 이동
    }
    void stimPack(){
        // 스팀팩을 사용한다.
    }
}

class Tank extends Unit { // 탱크 
    void move (int x, int y) {
        // 지정된 위치로 이동
    }
    void changeMode(){
        // 공격모드로 변환한다.
    }
}
Unit[] group = new Unit[4];
group[0] = new Marine();
group[1] = new Tank();
group[2] = new Marine();
group[3] = new Dropship();

for(int i = 0; i < group.length; i++){
	group[i].move(100, 200); // 인스턴스 메서드는 각 실제 인스턴스의 메서드 실행
}

 


인터페이스

  • 오직 추상메서드와 상수만을 멤버로 가질수 있다.
  • 구현된 것이 아무 것도 없고 밑그림만 그려져 있는 기본 설계도
  • 자체만으로 사용되기 보다는 다른 클래스를 작성하는데 도움을 줄 목적으로 작성된다.

 

인터페이스의 작성

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

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

모든 메서드는 public abstrac 이어야 하며, 생략할 수 있다.

(디폴트, static 메서드는 예외 JDK 1.8부터)

 

인터페이스의 상속

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

 

인터페이스의 구현

class 클래스이름 implements 인터페이스이름 {
	// 인터페이스에 정의된 추상메서드를 구현해야 한다.
}

 

  • 구현하는 인터페이스의 메서드 중 일부만 구현한다면, abstract를 붙여서 추상클래스로 선언해야한다.
abstract class Fighter implements Fightable {
    public void move (int x, int y) {
     ...
    }
}

 

  • 상속과 구현을 동시에 할 수 있다.
class Fighter extends Unit implements Fightable {
    public void move(int x, int y) {...}
    public void attack(Unit u) {...}
}

 

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

  • 인터페이스를 이용한 다중상속이 가능하지만 자바에서 인터페이스로 다중상속을 구현하는 경우는 거의 없다.
public class Tv {
    protected boolean power;
    protected int channel;
    protected int volume;

    public void power() {power = ! power;}
    public void channelUp() {channel++;}
    public void channelDown() {channel--;}
    public void volumeUp() {volume++;}
    public void volumeDown() {volume--;}
}

public class VCR {
    protected int counter;
    public void play(){}
    public void stop(){}
    public void reset(){counter = 0;}
    public int getCounter(){
        return counter;
    }
    public void setCounter(int c){
        counter = c;
    }
}

public interface IVCR{
    public void play();
    public void stop();
    public void reset();
    public int getCounter();
    public void setCounter(int c);
}

public class TVCR extends Tv implements IVCR{
    VCR vcr = new 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);
    }
}

VCR 클래스의 내용이 변경되어도 변경된 내용이 TVCR클래스에도 자동적으로 반영된다.

 

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

  • 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있다.
  • 인터페이스 타입으로의 형변환도 가능하다.
Fightable f = (Fightable)new Fighter();
Fightable f = new Fighter();

 

  • 인터페이스는 매개변수 타입으로 사용될 수 있다. (인터페이스를 구현한 클래스의 인스턴스들을 동적으로 제공받을 수 있다.)
void attack (Fightable f){...}

 

  • 리턴타입으로 인터페이스의 타입을 지정하는 것이 가능 → 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다.
Fightable method() {
    ...
    Fighter f = new Fighter();
    return f;
}
interface Parseable {
    // 구문 분석작업을 수행한다
    public abstract void parse(String fileName);
}
 
class ParserManager {
    // 리턴타입이 Parseaable 인터페이스.
    public static Parseable getParser(String type) {
        if(type.equals("XML")) {
            return new XMLParser(); // 새로 개정된 NewXMLParser가 생기면 여기만 변경하면 됨 
        } else {
            Parseable p = new HTMLParser(); 
            return p;
            // return new HTMLParser();
        }
    }
}
class XMLParser implements Parseable {
    public void parse(String fileName) {
        System.out.println(fileName + "- XML parsing completed.");
    }
}
class HTMLParser implements Parseable {
    public void parse(String fileName) {
        System.out.println(fileName + "- HTML parsing completed.");
    }
}
public class ParserTest {
    public static void main(String[] args) {
        Parseable parser = ParserManager.getParser("XML");
        parser.parse("document.xml");
 
        parser = ParserManager.getParser("HTML");
        parser.parse("document2.html");
    }
}
나중에 새로운 종류의 XML구문분석기 NewXMLPaser클래스가 나와도 ParserTest클래스는 변경할 필요 없이 ParserManager클래스의 getParser 메서드에서 'return new XMLParser();' 대신 'return new NewXMLParser();' 로 변경하기만 하면 된다. → 사용자 컴퓨터에 설치된 프로그램을 변경하지 않고 서버측의 변경만으로도 사용자가 새로 개정된 프로그램을 사용하는 것이 가능하다.

 

인터페이스의 장점

  1. 개발시간을 단축시킬 수 있다.
    • 동시에 다른 한 쪽에서는 인터페이스를 구현하는 클래스를 작성하게 하면, 인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않고도 양쪽에서 동시에 개발을 진행할 수 있다.
  2. 표준화가 가능하다
    • 프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 함으로써 일관되고 정형화된 프로그램의 개발이 가능하다.
  3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
    • 서로 상속관계에 있지도 않고, 같은 조상클래스를 가지고 있지도 않은 서로 아무런 관계도 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있다. p391
  4. 독립적인 프로그래밍이 가능하다.
    • 클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다. 

 

인터페이스의 이해

  • 클래스를 사용하는 쪽 (User)와 클래스를 제공하는 쪽 (Provider)가 있다.
  • 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다. (내용은 몰라도 된다.) 
// A-B의 관계 (직접적인 관계)
 
class A {
    public void methodA(B b) {
        b.methodB();
    }
}
class B {
    public void methodB() {
        System.out.println("methodB()");
    }
}
class InterfaceTest {
    public static void main(String[] args) {
        A a = new A();
        a.methodA(new B()); // 클래스 A는 클래스 B의 인스턴스를 생성하고 메서드를 호출한다.
    }
}
한쪽(provider B)이 변경되면 다른한쪽(User A)도 변경되어야 한다.

 

// A-I(인터페이스)-B 의 관계 (간접적인 관계)
 
class A {
    void autoPlay(I i) {
        i.play();
    }
}
 
interface I {
    public abstract void play();
}
 
class B implements I {
    public void play() {
        System.out.println("play in B class");
    }
}
 
class C implements I {
    public void play() {
        System.out.println("play in C class");
    }
}
 
public class InterfaceTest2 {
    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)호출
    }
}
인터페이스를 매개체로 해서 클래스 B(provider)가 변경사항이 생기거나 클래스 B(provider)와 같은 기능의 다른 클래스로 대체되어도 클래스 A(User)는 전혀 영향을 받지 않는다.

인터페이스 I는 실제구현 내용(클래스 B)를 감싸고 있는 껍데기이며,
A는 매개변수를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받기 때문에
클래스 A는 껍데기안에 어떤 알맹이(클래스)가 들어있는지 몰라도 된다.

 

// 인터페이스를 통한 캡슐화
 
public class InterfaceTest3 {
    public static void main(String[] args) {
        A a = new A();
        a.methodA();
    }
}
 
class A {
    void methodA() {
        I i = InstanceManager.getInstance();  // getInstance()메서드를 통해 인스턴스를 제공받는다.
        i.methodB();
        System.out.println(i.toString()); // i로 Object의 메서드 호출가능
    }
}

interface I {
    public abstract void methodB();
}
 
class B implements I {
    public void methodB() {
        System.out.println("methodB in B class");
    }
    public String toString() { return "class B"; }
}
 
class InstanceManager {
    public static I getInstance() { //반환타입이 인스턴스
        return new B(); // 다른 인스턴스로 바꾸려면 여기만 변경하면 됨.
    }
}
Interface2는 매개변수를 통해 동적으로 인스턴스를 제공받았지만 제 3의 클래스를 통해 제공받을 수도 있다.
인스턴스를 직접 생성하지 않고, getInstance() 메서드를 통해 제공받는다.
이렇게 하면, 나중에 다른 클래스의 인스턴스로 변경되어도 A클래스의 변경없이 getInstance()만 변경하면 된다는 장점이 있다. 

 

디폴트 메서드와 static 메서드

  • 원래는 인터페이스에 추상 메서드만 선언할 수 있는데, JDK1.8부터 디폴트 메서드와 static메서드도 추가할 수 있게 되었다.
  • static메서드는 인스턴스와 관계가 없는 독립적인 메서드이므로 인터페이스에 추가해도 문제 없다.
  • 인터페이스에 메서드를 새로 추가한다면, 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야한다. 따라서 디폴트 메서드를 고안했다.
  • 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.
  • 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우
1. 여러 인터페이스의 디폴트 메서드 간의 충돌
: 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.

2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
: 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.
public 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");
    }
}

source

자바의 정석