객체지향 프로그래밍 II

이 글은 남궁성님의 자바의 정석 3/e을 기반으로 공부한 내용을 정리한 글입니다.

다형성 polymorphism

5.1 다형성이란?

객체지향개념에서 다형성이란 ‘여라 가지 형태를 가질 수 있는 능력’을 의미한다. 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 했다. 즉, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다.

인스턴스를 같은 타입의 참조변수로 참조하는 것과 조상타입의 참조변수로 참조하는 것의 차이는 사용할 수 있는 멤버의 개수 차이이다.

반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 허용하지 않는다. 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.

| 참고 | 클래스는 상속을 통해 확장은 되지만 축소는 할 수 없기때문에, 조상 인스턴스의 멤버 개수는 자손 인스턴스의 멤버 개수보다 항상 적거나 같다.

참조변수의 타입이 참조변수가 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 결정한다.

조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다.
반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.

5.2 참조변수의 형변환

참조변수도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스사이에서만 가능하다.

| 참고 | 바로 윗 조상이나 자손이 아닌, 조상의 조상으로도 형변환이 가능하다. 따라서 모든 참조변수는 Object클래스 타입으로 형변환이 가능하다.

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

참조변수간의 형변환 역시 캐스트연산자를 사용하며, 괄호()안에 변환하고자 하는 타입의 이름(클래스명)을 적어주면 된다.

형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다. 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것뿐이다.

서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다.
그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.

5.3 instanceof연산자

참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다. 주로 조건문에 사용되며 왼쪽에는 참조변수, 오른쪽에는 타입(클래스명)이 피연산자로 위치한다. 그리고 연산의 결과로 boolean값인 truefalse 중 하나를 반환한다.

true를 연산결과로 얻었다면 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.

| 참고 | 값이 null인 참조변수에 대해 instanceof연산을 수행하면 false를 결과로 얻는다.

조상타입의 참조변수로는 실제 인스턴스의 멤버들을 모두 사용할 수 없기 때문에, 실제 인스턴스와 같은 타입의 참조변수로 형변환을 해야만 인스턴스의 모든 멤버들을 사용할 수 있다.

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

조상클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손클래스에 중복으로 정의했을 때, 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우와 자손타입의 참조변수로 자손 인스턴스를 참조하는 경우 서로 다른 결과를 얻는다.

메서드의 경우 오버라이딩한 경우에도 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.

| 참고 | static메서드는 static변수처럼 참조변수의 타입에 영향을 받는다. 참조변수의 타입에 영향을 받지 않는 것은 인스턴스메서드 뿐이다. 따라서 static메서드는 반드시 ‘클래스이름.메서드()’로 호출해야 한다.

멤버변수가 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다.

5.5 매개변수의 다형성

참조변수의 다형성은 메서드의 매개변수에도 적용된다.

PrintStream클래스에 정의되어있는 print(Object o)는 매개변수로 Object타입의 변수가 선언되어 있다. Object클래스는 모든 클래스의 조상이므로 이 메서드의 매개변수로 어떤 타입의 인스턴스도 가능하므로, 이 메서드 하나로 모든 타입의 인스턴스를 처리할 수 있다. 매개변수로 toString()을 호출하여 문자열을 얻어서 출력한다.

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

  public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
  }

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

조상타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다. 또는 묶어서 다루고싶은 객체들의 상속관계를 따져서 가장 가까운 공통조상 클래스 타입의 참조변수 배열을 생성해서 객체들을 저장하면 된다.

Vector클래스는 내부적으로 Object타입의 배열을 가지고 있어서, 이 배열에 객체를 추가하거나 제거할 수 있게 작성되어 있다. 그리고 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스의 개수에 신경 쓰지 않아도 된다.

  public class Vector extends AbstractList
                implements List, Cloneable, java.io.Serializable {
    protected Object elementData[];
      ...
  }
메서드 / 생성자
설 명
Vector() 10개의 객체를 저장할 수 있는 Vector인스턴스를 생성한다.
10개 이상의 인스턴스가 저장되면, 자동적으로 크기가 증가된다.
boolean add(Object o) Vector에 객체를 추가한다. 추가에 성공하면 결과값으로 true, 실패하면 false를 반환한다.
boolean remove(Object o) Vector에 저장되어 있는 객체를 제거한다. 제거에 성공하면 true, 실패하면 false를 반환한다.
boolean isEmpty() Vector가 비어있는지 검사한다. 비어있으면 true, 비어있지 않으면 false를 반환한다.
Object get(int index) 지정된 위치(index)의 객체를 반환한다. 반환타입이 Object타입이므로 적절한 타입으로의 형변환이 필요하다.
int size() Vector에 저장된 객체의 개수를 반환한다.


위로

추상클래스 abstract class

6.1 추상클래스란?

클래스가 설계도라면, 추상클래스는 미완성 설계도라고 볼 수 있다. 클래스가 미완성이라는 것은 멤버의 개수에 관계된 것이 아니라, 미완성 메서드(추상메서드)를 포함하고 있다는 의미이다.

추상클래스로는 인스턴스를 생성할 수 없다. 상속을 통해서 자손클래스에 의해서만 완성될 수 있다.

추상클래스는 키워드 abstract를 붙이면 된다. 이렇게 함으로써 이 클래스를 사용할 때, 추상메서드가 있으니 상속을 통해 구현해주어야 한다는 것을 알 수 있다.

| 참고 | 추상메서드를 포함하고 있지 않은 클래스에도 키워드 abstract를 붙여서 추상클래스로 지정할 수도 있다. 추상메서드가 없는 완성된 클래스도 추상클래스로 지정되면 인스턴스를 생성할 수 없다.

6.2 추상메서드(abstract method)

메서드는 선언부와 구현부(몸통)로 구성되어 있다. 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨 둔 것이 추상메서드이다.

메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만 작성한 후 주석으로 목적을 알려주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워 두는 것이다.

추상메서드 역시 키워드 abstract을 앞에 붙여 주고, 추상메서드는 구현부가 없으므로 괄호 { } 대신 문장의 끝을 알리는 ;을 적어준다.

  /* 주석을 통해 어떤 기능을 수행할 목적으로 작성하였는지 설명한다. */
  abstract 리턴타입 메서드이름();

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

메서드를 사용하는 쪽에서는 메서드가 실제로 어떻게 구현되어있는지 몰라도 메서드의 이름과 매개변수, 리턴타입, 즉 선언부만 알고 있으면 내용이 없을지라도 추상메서드를 사용하는 코드를 작성하는 것이 가능하며, 자손클래스에 구현된 완성된 메서드가 호출되도록 할 수 있다.

6.3 추상클래스의 작성

추상      낱낱의 구체적 표상이나 개념에서 공통된 성질을 뽑아 이를 일반적인 개념으로 파악하는 정신 작용

상속이 자손 클래스를 만드는데 조상 클래스를 사용하는 것이라면, 추상화는 기존 클래스의 공통부분을 뽑아내서 조상 클래스를 만드는 것이다.

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

위로

인터페이스 interface

7.1 인터페이스란?

인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상메서드와 상수만을 멤버로 가질 수 있다.

인터페이스는 밑그림만 그려져 있는 ‘기본 설계도’라 할 수 있다. 그 자체만으로 사용되기 보다는 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.

7.2 인터페이스의 작성

클래스 작성 방법과 동일하지만 키워드로 class 대신 interface를 사용한다는 것만 다르다. interface에도 클래스와 같이 접근제어자로 public 또는 default를 사용할 수 있다.

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

인터페이스의 멤버들은 다음과 같은 제약사항이 있다.

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

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

7.3 인터페이스의 상속

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

| 참고 | 인터페이스는 클래스와 달리 Object클래스와 같은 최고 조상이 없다.

클래스의 상속과 마찬가지로 자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다.

7.4 인터페이스의 구현

인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없다. 따라서 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야 하는데, 확장한다는 의미의 extends 대신 구현한다는 의미의 implements를 사용한다.

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

구현하는 인터페이스의 메서드 중 일부만 구현한다면, abstract를 붙여서 추상클래스로 선언해야 한다. 그리고 상속과 구현을 동시에 할 수 있다.

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

두 조상으로부터 상속받는 멤버 중에서 멤버변수 이름이 같거나 메서드의 선언부가 일치하고 구현 내용이 다르다면 어느 조상으로부터 상속받는 자손클래스인지 알 수 없다.

인터페이스는 static상수만 정의할 수 있으므로 조상클래스의 멤버변수와 충돌하는 경우는 거의 없다. 충돌하더라도 클래스 이름을 붙여서 구분이 가능하고 추상메서드는 조상 클래스 쪽의 메서드를 상속받으면 된다. 그러나 이렇게 할 경우 다중상속의 장점을 잃는다.

따라서 조상클래스 중에서 비중이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부 멤버로 포함시키거나 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현한다.

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

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

인터페이스는 메서드의 매개변수의 타입으로 사용될 수 있다. 인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야한다는 것이다.

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

7.7 인터페이스의 장점

인터페이스를 사용하는 이유와 장점은 다음과 같다.

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

1. 개발시간을 단축시킬 수 있다.
인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는 것이 가능하다. 메서드를 호출하는 쪽에서는 선언부만 알면 되기 때문이다.

2. 표준화가 가능하다.
프로젝트에 사용되는 기본 틀을 인터페이스로 작성하여 인터페이스 구현으로 프로그램을 작성하도록 함으로써 보다 일관되고 정형화된 프로그램의 개발이 가능하다.

3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
하나의 인터페이스를 공통적으로 구현하도록 해서 관계를 맺어 줄 수 있다.

4. 독립적인 프로그래밍이 가능하다.
인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제구현에 독립적인 프로그램을 작성하는 것이 가능하다. 클래스 간의 직접적인 관계를 간접적인 관계로 변경하면, 독립적인 프로그래밍이 가능하다.

7.8 인터페이스의 이해

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

매개변수를 통해 동적으로 제공받을 수도 있지만 제3의 클래스를 통해서 제공받을 수도 있다.

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

class A {
  void methodA() {
    I i = InstanceManager.getInstance();
    i.methodB();
    System.out.println(i.toString);
  }
}

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

인스턴스를 직접 생성하지않고, getInstance()라는 메서드를 통해 제공받는다. 나중에 다른 클래스의 인스턴스로 변경되어도 A클래스의 변경없이 getInstance()만 변경하면 된다는 장점이 생긴다.

7.9 디폴트 메서드와 static메서드

인터페이스와 관련된 static메서드는 별도의 클래스에 있다. 대표적인 예로 java.util.Collection인터페이스가 있는데, 인터페이스에는 추상 메서드만 선언할 수 있다는 원칙 때문에 관려된 static메서드들이 별도의 클래스, Collections라는 클래스에 들어가 있다.

인터페이스의 static메서드 역시 접근 제어자가 항상 public이며, 생략할 수 있다.

디폴트 메서드

인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야한다.

디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 추가되어도 인터페이스를 구현한 클래스를 변경하지 않아도 된다.

디폴트 메서드는 앞에 키워드 default를 붙이며, 추상 메서드와 달리 몸통이 있어야 한다. 접근 제어자는 public이며, 생략가능하다. 디폴트 메서드를 추가하는 것은 조상 클래스에 새로운 메서드를 추가한 것과 동일하다. 대신 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 있는데, 이를 해결하는 규칙은 다음과 같다.

1. 여러 인터페이스의 디폴트 메서드 간의 충돌
- 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.
2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
- 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

더욱 간단한 방법은 필요한 쪽의 메서드와 같은 내용으로 오버라이딩 해버리면 된다.

위로

내부 클래스 inner class

내부 클래스는 클래스 내에 선언된다는 점을 제외하고 일반적인 클래스와 다르지 않다.

8.1 내부 클래스란?

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

내부 클래스의 장점
- 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
- 코드의 복잡성을 줄일 수 있다(캡슐화).

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

내부 클래스의 종류는 변수의 선언위치에 따른 종류와 같다. 변수를 선언하는 것과 같은 위치에 선언할 수 있으며, 선언위치에 따라 구분되어 진다.

내부 클래스
특 징
인스턴스 클래스
(instance class)
외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 인스턴스멤버처럼 다루어진다. 외부 클래스의 인스턴스멤버들과 관련된 작업에 사용될 목적으로 선언된다.
스태틱 클래스
(static class)
외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 static멤버처럼 다루어진다. 외부 클래스의 static멤버, 특히 static메서드에서 사용될 목적으로 선언된다.
지역 클래스
(local class)
외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다.
익명 클래스
(anonymous class)
클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용)

8.3 내부 클래스의 선언

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

  class Outer {
    class InstanceInner { }
    static class StaticInner { }

    void myMethod() {
      class LocalInner { }
    }
  }

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

인스턴스 클래스(InstanceInner)와 스태틱 클래스(StaticInner)는 외부 클래스(Outer)의 멤버변수(인스턴스변수와 클래스변수)와 같은 위치에 선언되며, 멤버변수와 같은 성질을 가진다. 따라서 내부 클래스가 외부 클래스의 멤버와 같이 간주되고, 인스턴스멤버와 static멤버 간의 규칙이 내부 클래스에도 똑같이 적용된다.

내부 클래스도 클래스이기 때문에 abstractfinal과 같은 제어자를 사용할 수 있으며, 멤버변수들처럼 private, protected과 접근제어자도 사용이 가능하다. 스태틱 클래스(StaticInner)만 static멤버를 가질 수 있는데 finalstatic이 동시에 붙은 변수는 상수(constant)이므로 모든 내부 클래스에서 정의가 가능하다.

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

지역 클래스(LocalInner)는 외부 클래스의 인스턴스멤버와 static멤버를 모두 사용할 수 있으며, 지역 클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다. 단, final이 붙은 지역변수만 가능한데 메서드가 수행을 마쳐 지역변수가 소멸된 시점에도 지역클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다.

8.5 익명 클래스(anonymous class)

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

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

        또는

  new 구현인터페이스이름() {
    // 멤버 선언
  }

이름이 없기 때문에 생성자도 가질 수 없으며, 조상클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해 정의한다. 따라서 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 없다.

위로