[자바의 정석] 7장 객체지향 프로그래밍2

1. 상속

1.1 상속의 정의와 장점

  • 정의 : 상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것
  • 장점 : 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드의 추가 및 변경이 매우 용이 ex) 새로 작성하려는 클래스의 이름이 Child이고 상속받고자 하는 기존 클래스의 이름이 Parent
    class Child extends Parent{}
  • 이 두 클래스는 서로 상속 관계에 있다고 하며, 상속해주는 클래스를 ‘조상 클래스’라 하고 상속 받는 클래스를 ‘자손 클래스’라 한다.
  • 자손 클래스는 조상 클래스의 모든 멤버를 상속받기 때문에, Child클래스는 Parent클래스의 멤버들을 포함한다고 할 수 있다.
  • Child클래스에 새로운 코드가 추가되어도 조상인 Parent클래스는 아무런 영향도 받지 않는다.
    -> 조상 클래스가 변경되면 자손 클래스는 영향을 받게 되지만, 자손 클래스가 변경되는 것은 조상 클래스에 아무런 영향을 주지 못한다.
    _ 생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
    _ 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.
  • Parent클래스를 상속 받는 Child와 Child2클래스 간에는 서로 아무런 관계도 성립되지 않는다.
    • 클래스 간의 관계에서 형제 관계와 같은 것은 없다.
    • 만일 두 클래스에 공통적으로 추가되어야 하는 멤버가 있다면, 이 두 클래스에 각각 따로 추가해주는 것보다는 공통 조상인 Parent클래스에 추가하는 것이 좋다.

자손 클래스의 인스턴스를 생성하면 조상클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.

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

상속이외에도 클래스를 재사용하는 또 다른 방법이 있는데, 그것은 클래스간에 ‘포함관계’를 맺어 주는 것
포함관계 : 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것

class Circle {				        //원을 표현하기 위한 클래스
	int x;					//원점 x 좌표
	int y;					//원점 y 좌표
	int r;					//반지름
}

class Point {					//좌표상의 한 점을 다루기 위한 클래스
	int x;
	int y;
}
// Circle은 Point를 가지고 있다. -> 포함 관계

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

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

포함관계? VS 상속관계?
상속관계 : ‘ ~은 ~이다.(is-a) ‘
포함관계 : ‘ ~은 ~을 가지고 있다.(has-a) ‘

  • 클래스를 작성하는데 있어서 포함시키거나 상속받도록 하는 것은 결과적으로 별 차이가 없어 보일 수 있다.
  • 그럴 때는 ‘ ~은 ~이다.(is-a) ‘와 ‘ ~은 ~을 가지고 있다.(has-a) ‘를 넣어서 문장을 만들어 보면 클래스 간의 관계가 보다 명확해 진다.
    ex) Car클래스와 SportsCar클래스는 ‘SportsCar는 Car이다.’와 같이 문장을 만드는 것이 더 옳기 때문에 Car을 조상으로 하는 상속관계를 맺어 주어야 한다.
    ex) Card클래스와 Deck클래스는 ‘Deck은 Card를 가지고 있다.’와 같이 문장을 만드는 것이 더 옳기 때문에 Deck클래스에 Card클래스를 포함시켜야 한다.

1.4 단일 상속

C++에서는 여러 조상 클래스로부터 상속받는 ‘다중상속’을 허용하지만, 자바에서는 오직 단일 상속만을 허용한다.
이유 : 클래스간의 관계가 매우 복잡해지고, 다른 클래스로부터 상속받은 멤버간의 이름이 같은 경우 구별할 수 있는 방법이 없다.

class Tv {		    //Tv 클래스
    boolean power;
    int channel;

    void power() {power = !power;}
    void channelUp() {++channel;}
    void channelDown() {--channel;}
}
class VCR{		    //VCR 클래스
    boolean power;
    int counter = 0;
    void power() { power = !power;}
    void play() {}
    void stop() {}
}

public class TVCR extends Tv {
    VCR vcr = new VCR();

    void play() {
        vcr.play();
    }
    void stop() {
        vcr.stop();
    }
}
  • TVCR클래스에 VCR클래스의 메서드와 일치하는 선언부를 가진 메서드를 선언하고 내용은 VCR클래스의 것을 호출해서 사용하도록 했다.
  • 외부적으로는 TVCR클래스의 인스턴스를 사용하는 것처럼 보이지만 내부적으로는 VCR클래스의 인스턴스를 생성해서 사용하는 것
  • VCR클래스의 메서드의 내용이 변경되더라도 TVCR클래스의 메서드들 역시 변경된 내용이 적용되는 결과를 얻을 수 있다.

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

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

  • 다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object클래스로부터 상속받게 함으로써 이것을 가능하게 함
  • 컴파일러가 자동적으로 ‘extends Object’를 추가하여 상속받도록 한다.
  • 그동안 toString()이나 equals(Object o)와 같은 메서드를 따로 정의하지 않고도 사용할 수 있었던 이유는 이 메서드들이 Object클래스에 정의된 것들이기 때문

2. 오버라이딩

2.1 오버라이딩이란?

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

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;
	}
}
  • Point클래스를 사용하던 사람들은 새로 작성된 Point3D클래스가 getLocation()을 호출하면 Point클래스의 getLocation()이 그랬듯이 점의 좌표를 문자열로 얻을 수 있을 것이라고 기대할 것이다.
  • 그렇기 때문에 새로운 메서드를 제공하는 것보다 오버라이딩을 하는 것이 바른 선택이다.

2.2 오버라이딩의 조건

  • 자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와
    1. 이름이 같아야 한다.
    2. 매개 변수가 같아야 한다.
    3. 반환타입이 같아야 한다.
  • 조상 클래스의 메서드를 자손 클래스에서 오버라이딩 할 때
    1. 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
    2. 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
    3. 인스턴스메서드를 static메서드로 또는 그 반대로 변경할 수 없다

2.3 오버로딩 vs. 오버라이딩

오버로딩 - 기존에 없는 새로운 메서드를 정의하는 것(new)
오버라이딩 - 상속받은 메서드의 내용을 변경하는 것(change, modify)

2.4 super

ex) super.x

  • 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수
  • 멤버변수와 지역변수의 이름이 같을 때 this를 붙여서 구별했듯이 상속받은 멤버와 자신의 멤버 이름이 같을 때는 super을 붙여서 구별
  • 조상 클래스로부터 상속받은 멤버도 자손 클래스 자신의 멤버이므로 super대신 this를 사용할 수 있다.
  • 조상 클래스의 멤버와 자손 클래스의 멤버가 중복 정의되어 서로 구별해야하는 경우에만 사용하는 것이 좋다.
  • 모든 인스턴스메서드에는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데, 이것이 참조변수인 this와 super값이 된다.
    -> this와 마찬가지로 super 역시 static메서드에서는 사용할 수 없고, 인스턴스메서드에서만 사용 가능
  • 변수만이 아니라 메서드 역시 super를 써서 호출 가능
    -> 조상클래스의 메서드의 내용에 추가적으로 작업을 덧붙이는 경우라면 super를 사용해서 조상클래스의 메서드를 포함시키는 것이 좋다.
    ex) super.getLocation();

2.5 super() - 조상 클래스의 생성자

조상 클래스의 생성자를 호출하는데 사용하는 생성자

  • 조상 클래스 멤버의 초기화 작업이 수행되어야 하기 때문에 자손 클래스의 생성자에서 조상 클래스의 생성자가 호출되어야 한다.
  • 생성자의 첫 줄에서 조상클래스의 생성자를 호출해야하는 이유
    • 자손 클래스의 멤버가 조상 클래스의 멤버를 사용할 수도 있으므로 조상의 멤버들이 먼저 초기화 되어 있어야 하기 때문
  • Object클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자, this() 또는 super()를 호출해야 한다. 그렇지 않으면 컴파일러가 자동적으로 ‘super();’를 생성자의 첫 줄에 삽입한다
  • 조상클래스의 멤버변수는 조상의 생성자에 의해 초기화되도록 해야 한다

3. Package와 import

3.1 패키지(package)

패키지란, 클래스들의 묶음

  • 클래스의 실제 이름(full name)은 패키지명을 포함한 것
    ex) String클래스의 실제 이름은 java.lang.String
  • 클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리
  • 하나의 소스파일에는 첫 번째 문장으로 단 한 번의 패키지 선언만을 허용
  • 모든 클래스는 반드시 하나의 패키지로 속해야 한다.
  • 패키지는 점(.)을 구분자로 하여 계층구조로 구성할 수 있다.
  • 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리

3.2 패키지의 선언

package 패키지명;

  • 해당 소스파일에 포함된 모든 클래스나 인터페이스는 선언된 패키지에 속하게 된다.
  • 소스파일에 자신이 속할 패키지를 지정하지 않은 클래스는 자동적으로 ‘이름 없는 패키지’에 속하게 된다.

3.3 import문

다른 패키지의 클래스를 사용하려면 패키지명이 포함된 클래스 이름을 사용해야 한다

  • import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스코드에 사용되는 클래스이름에서 패키지명은 생략할 수 있다.
  • import문의 역할은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하는 것

import문의 선언

일반적인 소스파일 (.java)의 구성은 다음의 순서로 되어 있다.

  1. package문
  2. import문
  3. 클래스 선언

Import문의 선언 방법

  • import 패키지명.클래스명;
    -> 키워드 import와 패키지명을 생략하고자 하는 클래스의 이름을 패키지명과 함께 써주면 된다.
  • import 패키지명.*;
    -> 같은 패키지에서 여러 개의 클래스가 사용될 때, 지정된 패키지에 속한 모든 클래스를 패키지명 없이 사용할 수 있다. 실행 시 성능상의 차이는 전혀 없다
  • import문에서 클래스의 이름 대신 ‘*‘을 사용하는 것이 하위 패키지의 클래스까지 포함하는 것은 아니다.

String과 System같은 java.lang패키지의 클래스들을 패키지명 없이 사용할 수 있던 이유는 모든 소스파일에 묵시적으로 ‘import java.lang.*;’이 선언되어 있기 때문이다.
-> 매우 빈번히 사용하기 때문에

3.4 static import문

  • static import 문을 사용하면 static멤버를 호출할 때 클래스 이름을 생략할 수 있다.
  • 특정 클래스의 static멤버를 자주 사용할 때 편리하다.
import static java.lang.Integer.*;	//Integer클래스의 모든 static메서드
import static java.lang.Math.random;	//Math.random()만. 괄호 안붙임.
import static java.lang.System.out;	//System.out을 out만으로 참조가능
* 위와 같이 static import문을 선언하였다면,

System.out.println(Math.random());
out.println(random());으로 간략히 할 수 있다.

4. 제어자(modifier)

4.1 제어자란?

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

  • 제어자의 종류는 크게 접근 제어자와 그 외의 제어자로 나눌 수 있다.
  • 접근 제어자 : public, protected, default, private
  • 그 외 : static, final, abstract 등
    • 하나의 대상에 대해서 여러 제어자를 조합하여 사용하는 것이 가능
    • 단, 접근 제어자는 한 번에 네 가지 중 하나만 선택해서 사용할 수 있다.

4.2 static - 클래스의, 공통적인

  • 클래스변수는 인스턴스에 관계없이 같은 값을 가진다.
    -> 하나의 변수를 모든 인스턴스가 공유하기 때문에
  • static이 사용될 수 있는 곳 - 멤버변수, 메서드, 초기화 블럭

멤버변수

  • 모든 인스턴스에 공통적으로 사용되는 클래스변수가 된다.
  • 클래스변수는 인스턴스를 생성하지 않고도 사용 가능하다.
  • 클래스가 메모리에 로드될 때 생성된다.

메서드

  • 인스턴스를 생성하지 않고도 호출이 가능한 static 메서드가 된다.
  • static메서드 내에서는 인스턴스멤버들을 직접 사용할 수 없다.

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

  • final이 사용될 수 있는 곳 - 클래스, 멤버변수, 메서드, 지역변수

클래스

  • 변경될 수 없는 클래스, 다른 클래스의 조상이 될 수 없다.
  • ex) 대표적인 final클래스로는 String과 Math가 있다.

메서드

  • 변경될 수 없는 메서드, 오버라이딩을 통해 재정의 될 수 없다.

멤버변수, 지역변수

  • 값을 변경할 수 없는 상수가 된다.

생성자를 이용한 final멤버 변수의 초기화

  • 매개변수를 갖는 생성자를 선언하여, 인스턴스를 생성할 때 final이 붙은 멤버변수를 초기화하는데 필요한 값을 생성자의 매개변수로부터 제공받음으로 각 인스턴스마다 final이 붙은 멤버변수가 다른 값을 갖도록 하는 것이 가능

4.4 abstract - 추상의, 미완성의

  • abstract이 사용될 수 있는 곳 - 클래스, 메서드

클래스

  • 클래스 내에 추상 메서드가 선언되어 있음을 의미

메서드

  • 선언부만 작성하고 구현부는 작성하지 않은 추상 메서드

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

4.5 접근 제어자(access modifier)

  • 접근 제어자가 사용될 수 있는 곳 - 클래스, 멤버변수, 생성자, 메서드
    • private : 같은 클래스 내에서만 접근 가능
    • (default) : 같은 패키지 내에서만 접근 가능
    • protected : 같은 패키지 내에서, 그리고 다른 패키지의 자손 클래스에서 접근 가능
    • public : 접근 제한이 전혀 없다.

대상에 따라 사용할 수 있는 접근 제어자

  • 클래스 - public, (default)
  • 메서드, 멤버변수 - 모두 사용 가능
  • 지역변수 - 없 음

접근 제어자를 이용한 캡슐화

주로 멤버에 접근 제어자를 사용하는 이유?

  1. 외부로부터 데이터를 보호하기 위해서
    ex) 데이터가 유효한 값 유지, 비밀번호 같은 데이터를 접근하거나 변경하지 못하도록 제한
  2. 외부에는 불필요한, 내부적으로만 사용되는, 부분을 감추기 위해서
    ex) 외부에서 접근이 필요 없는 멤버들을 외부에 노출시키지 않음으로써 복잡성을 줄임
    -> 접근 범위를 최소화하도록 노력
public class Time {
	public int hour;
	public int minute;
 	public int second;
}
// 이 클래스의 인스턴스를 생성한 다음, 멤버변수에 직접 접근하여 값을 변경할 수 있을 것이다.
Time t = new Time();
t.hour = 25;
  • hour은 0보다는 같거나 크고 24보다는 작은 범위의 값을 가져야 하지만 위의 코드처럼 잘못된 값을 지정한다고 해도 이것을 막을 방법은 없다.
  • 멤버변수를 private나 protected로 제한하고 멤버변수의 값을 읽고 변경할 수 있는 public 메서드를 제공하여 간접적으로 멤버변수의 값을 다룰 수 있도록 하는 것이 바람직하다.
  • 보통 멤버변수의 값을 읽는 메서드의 이름을 ‘get멤버변수이름’으로 하고 겟터(getter)라 부른다.
  • 멤버변수 값을 변경하는 메서드의 이름을 ‘set멤버변수이름’으로 하고 셋터(setter)라 부른다.
  • t.hour = 13; 같은 멤버변수로의 직접적인 접근은 허가되지 않는다. 메서드를 통한 접근만이 허용된다.

생성자의 접근 제어자

Singleton

  • 생성자에 접근 제어자를 사용함으로써 인스턴스의 생성을 제한할 수 있다.
  • 생성자의 접근 제어자를 private으로 지정하면, 외부에서 생성자에 접근할 수 없으므로 인스턴스를 생성할 수 없게 된다.
  • 대신 인스턴스를 생성해서 반환해주는 public 메서드를 제공함으로써 외부에서 이 클래스의 인스턴스를 사용할수 있도록 할 수 있다.
  • 이 메서드는 public인 동시에 static이어야 한다.
class Singleton {
	private static Singleton s = new Singleton();	  //getInstance()에서 사용될 수 있도록 인스턴스가 미리 생성되야 하므로 static이어야 함
	private Singleton() {

	}

	public static Singleton getInstance() {         //인스턴스를 생성하지 않고도 호출할 수 있어야 하므로 static이어야 함
		return s;
	}
}
  • 이처럼 생성자를 통해 직접 인스턴스를 생성하지 못하게 하고 public메서드를 통해 인스턴스에 접근하게 함으로써 사용할 수 있는 인스턴스의 개수를 제한할 수 있다.
  • 생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다.
  • 자손 클래스가 인스턴스를 생성할 때, 조상클래스의 생성자를 호출해야만 하는데, 생성자의 접근 제어자가 private이므로 자손클래스에서 호출하는 것이 불가능 하기 때문
  • 클래스 앞에 final을 더 추가하여 상속할 수 없는 클래스라는 것을 알리는 것이 좋다.

4.6 제어자(modifier)의 조합

대상에 따라 사용할 수 있는 제어자

  • 클래스 - public, (default), final, abstract
  • 메서드 - 모든 접근 제어자, final, abstract, static
  • 멤버변수 - 모든 접근 제어자, final, static
  • 지역변수 - final

제어자를 조합해서 사용할 때 주의해야 할 사항

  1. 메서드에 static과 abstract를 함께 사용할 수 없다.
    -> static메서드는 몸통이 있는 메서드에만 사용할 수 있기 때문
  2. 클래스에는 abstract와 final을 동시에 사용할 수 없다.
    -> final은 클래스를 확장할 수 없다는 의미이고 abstract는 상속을 통해 완성되어야 한다는 의미이므로 서로 모순
  3. abstract메서드의 접근 제어자가 private일 수 없다.
    -> abstract메서드는 자손클래스에서 구현해주어야 하는데 private이면, 자손클래스에서 접근할 수 없기 때문
  4. 메서드에 private과 final을 같이 사용할 필요는 없다.
    -> 접근 제어자가 private인 메서드는 오버라이딩될 수 없기 때문이다. 둘 중 하나만 사용해도 의미가 충분

5. 다형성

5.1 다형성이란?

객체지향에서는 ‘여러 가지 형태를 가질 수 있는 능력’
자바에서는 조상클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수 있도록 함
ex) Tv와 CaptionTv클래스가 서로 상속 관계일 때,
Tv t = new CationTv();

  • 실제 인스턴스가 CaptionTv 타입이라 할지라도, 참조 변수 t로는 CaptionTv 인스턴스의 모든 멤버를 사용할 수 없다.
  • Tv타입의 참조변수로는 CaptionTv 인스턴스 중에서 Tv 클래스의 멤버들만 사용할 수 있다.
  • 같은 타입의 인스턴스라도 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.

반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 가능할까?
CaptionTv c = new Tv(); //컴파일 에러

  • 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문
  • 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 존재하지 않는 멤버를 사용하고자 할 가능성이 있으므로 허용하지 않음
  • 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.

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

5.2 참조변수의 형변환

  • 서로 상속관계에 있는 클래스사이에서는 참조변수도 형변환이 가능하다.
  • 자손타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입의 참조변수로의 형변환만 가능
    자손타입 -> 조상타입(Up-casting) : 형변환 생략가능
    조상타입 -> 자손타입(Down-casting) : 형변환 생략불가 _ 자손타입의 참조변수를 조상타입의 참조변수로 형변환 하는 것은 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명하므로 문제가 되지 않는다. 그래서 형변환을 생략할 수 있다. _ 형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것 뿐이다.
public class CastingTest {
    public static void main(String[] args) {
        Car car = new Car();
        Car car2 = null;
        FireEngine fe = null;

        car.drive();
        fe = (FireEngine) car;		//컴파일은 OK. 실행 시 에러가 발생
        fe.drive();
		  car2 = fe;
		  car2.drive();
    }
}
  • 참조변수 car가 참조하고 있는 인스턴스가 Car타입의 인스턴스이기 때문에 조상 타입의 인스턴스를 자손타입의 참조변수로 참조하는 것은 허용되지 않는다.
  • 컴파일 시에는 참조변수간의 타입만 체크하기 때문에 실행 시 생성될 인스턴스의 타입에 대해서는 전혀 알지 못한다.
  • 서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다.
  • 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것은 중요하다.

5.3 instance 연산자

  • 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof 연산자를 사용한다.
  • 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다.
  • 실제 인스턴스와 같은 타입의 instanceof연산 이외에 조상타입의 instanceof연산에도 true를 결과로 얻는다.
  • 어떤 타입에 대한 instanceof연산의 결과가 true라는 것은 검사한 타입으로 형변환이 가능하다는 것을 뜻한다

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

  • 조상클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때, 조상타입 참조변수로 자손 인스턴스를 참조하는 경우와 자손타입 참조변수로 자손 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다.
  • 메서드의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
  • 중복 정의되지 않은 경우, 조상타입의 참조변수를 사용했을 때와 자손타입의 참조변수를 사용했을 때의 차이는 없다.
public class BindingTest {				//Parent는 Child의 조상 클래스
    public static void main(String[] args) {
        Parent p = new Child();
        Child c = new Child();

        System.out.println("p.x = " + p.x);		//Parent에 선언된 멤버변수 사용
        p.method();
        System.out.println("c.x = " + c.x);		//Child에 선언된 멤버변수 사용
        c.method();		//메서드는 참조변수 타입과 관계없이 실제 인스턴스의 메서드 호출
    }
}
  • 멤버변수들은 주로 private으로 접근을 제한하고, 외부에서는 메서드를 통해서만 멤버변수에 접근할 수 있도록 한다.
  • 인스턴스변수에 직접 접근하면, 참조변수의 타입에 따라 사용되는 인스턴스변수가 달라질 수 있다.

5.5 매개변수의 다형성

  • 참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다.
  • 예를 들어 조상 클래스인 Product 클래스와 자손 클래스인 Tv, Audio 클래스가 있을 때, Tv를 구입하는 기능의 메서드를 추가하면
	void buy(Tv t) {
    money -= t.price;
    bonusPoint += t.bonusPoint;
}

와 같이 작성될 수 있다.

  • 그런데 buy(Tv t)로는 Tv밖에 살 수 없기 때문에 다른 제품들도 구입하려면 메서드를 계속해서 추가해 주어야 한다.
  • 메서드의 매개변수에 다형성을 적용하면 하나의 메서드로 간단히 처리할 수 있다.
	void buy(Product p) {
    money -= p.price;
    bonusPoint += p.bonusPoint;
}
* 매개변수가 Product타입의 참조변수라는 것은, 메서드의 매개변수로 Product클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다는 뜻이다.
* 앞으로 다른 제품 클래스를 추가할 때 Product클래스를 상속받기만 하면, buy(Product)메서드의 매개변수로 받아들여질 수 있다.

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

  • 조상타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.
class Buyer {
    int money = 1000;
    int bonusPoint = 0;
	  Product[] item = new Product[10];
	  int i = 0;

    void buy(Product p) {
        if (money < p.price) {
            System.out.println("잔액이 부족합니다.");
            return;
        }
        money -= p.price;
        bonusPoint += p.bonusPoint;
        item.[i++] = p;
        System.out.println(p + "을/를 구매하셨습니다.");
    }
}
* 'item[i++] = p;' 문자를 추가함으로써 물건을 구입하면, 배열 item에 저장되도록 했다.
* 이렇게 함으로써, 모든 제품클래스의 조상인 Product클래스 타입의 배열을 사용함으로써 구입한 제품을 하나의 배열로 간단하게 다룰 수 있게 된다.

6. 추상 클래스(abstract class)

6.1 추상클래스란?

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

  • 상속을 통해서 자손클래스에 의해서만 완성될 수 있다.
  • 자체로는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 조상클래스로서 중요한 의미를 갖는다.
  • 키워드 ‘abstract’를 붙이기만 하면 된다.
    • 이렇게 함으로써 클래스 선언부를 보고 이 클래스에는 추상메서드가 있으니 상속을 통해서 구현해주어야 한다는 것을 알 수 있다.
abstract class 클래스이름 {
	...
}
  • 추상클래스는 추상메서드를 포함하고 있다는 것을 제외하고는 일반클래스와 전혀 다르지 않다.

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

6.2 추상메서드(abstract method)

선언부만 작성하고 구현부는 작성하지 않은 채로 남겨 둔 메서드
-> 설계만 해 놓고 실제 수행될 내용은 작성하지 않았기 때문에 미완성 메서드

  • 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만 작성하고, 실제 내용은 상속받는 클래스에서 구현하도록 비워 두는 것
  • 추상메서드 역시 키워드 ‘abstract’를 앞에 붙여 주고, 구현부가 없으므로 괄호{} 대신 문장의 끝을 알리는 ‘;’을 적어준다.
/* 주석을 통해 어떤 기능을 수행할 목적으로 작성하였는지 설명한다. */
abstract 리턴타입 메서드이름();
  • 추상클래스를 상속받는 자손클래스는 오버라이딩을 통해 추상메서드를 모두 구현해주어야 한다.
  • 하나라도 구현하지 않는다면, 자손클래스 역시 추상클래스로 지정해 주어야 한다.
  • 추상메서드 대신, 아무 내용도 없는 메서드로 작성할 수도 있다.
  • 그래도 굳이 추상메서드로 선언하는 이유는 자손 클래스에서 추상메서드를 반드시 구현하도록 강요하기 위해서이다.

6.3 추상클래스의 작성

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

  • 상속계층도를 따라 내려갈수록 클래스는 점점 기능이 추가되어 구체화의 정도가 심해지며, 올라갈수록 추상화의 정도는 심해진다고 할 수 있다.
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클래스를 정의하고 이로부터 상속받도록 하였다.
  • move메서드의 선언부는 같지만 이동하는 방법이 서로 달라서 추상메서드로 정의한다.
  • 최대한의 공통부분을 뽑아내기 위한 것이기도 하지만, 모든 유닛은 이동할 수 있어야 하므로 Unit클래스에는 move메서드가 반드시 필요한 것이기 때문
  • stop메서드는 모두 공통적이지만, move메서드가 추상메서드로 선언된 것에는, Unit클래스를 상속받아서 작성되는 클래스는 move메서드를 자신의 클래스에 알맞게 반드시 구현해야 한다는 의미가 담겨있는 것이기도 하다.
Unit[] group = new Unit[2];
group[0] = new Marine();
group[1] = new Tank();

for(int i=0; i<group.length; i++)
	group[i].move(100, 200);
  • 공통조상인 Unit클래스 타입의 참조변수 배열을 통해서 서로 다른 종류의 인스턴스를 하나의 묶음으로 다룰 수 있다.
  • 메서드는 참조변수의 타입에 관계없이 실제 인스턴스에 구현된 것이 호출되기 때문에 Unit클래스 타입의 참조변수로 move메서드 호출이 가능하다.
  • 모든 클래스의 조상인 Object클래스 타입의 배열로도 서로 다른 종류의 인스턴스를 묶어서 다룰 수 있지만, move메서드가 정의되어 있지 않기 때문에 호출하는 부분에서 에러가 발생한다.
    -> 참조변수는 리모콘 역할

7. 인터페이스(interface)

7.1 인터페이스란?

추상 메서드의 집합

  • 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.
  • 오직 추상메서드와 상수만을 멤버로 가질수 있다.
    -> 다른 클래스를 작성하는데 도움 줄 목적으로 작성

추상클래스 vs 인터페이스

추상클래스를 부분적으로 완성된 ‘미완성 설계도’라고 한다면, 인터페이스는 구현된 것은 아무 것도 없고 밑그림만 그려져 있는 ‘기본 설계도’라 할 수 있다.

7.2 인터페이스의 작성

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

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

  • 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
  • 단, static메서드와 디폴트 메서드는 예외(JDK 1.8부터)
    -> 모든 멤버에 예외없이 적용되는 사항이기 때문에 생략할 수 있는 것이며, 편의상 생략하는 경우가 많다.

7.3 인터페이스의 상속

  • 인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 다르게 다중상속이 가능하다.
  • 클래스의 상속과 마찬가지로 자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다.

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

7.4 인터페이스의 구현

  • 인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 자신에 정의된 초상메서드의 몸통을 만들어주는 클래스를 작성해야 한다.
  • 단, 인터페이스는 구현한다는 의미의 키워드 ‘implements’를 사용한다.
  • 구현하는 인터페이스의 메서드 중 일부만 구현한다면, abstract를 붙여서 추상클래스로 선언해야 한다.
  • 상속과 구현을 동시에 할 수도 있다.

인터페이스의 이름에는 주로 Fightable과 같이 ‘~을 할 수 있는’의 의미인 ‘able’로 끝나는 것들이 많은데, 그 이유는 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 각종하기 위해서이다.

  • 인터페이스를 구현한 클래스는 인터페이스로부터 상속받은 추상 메서드를 구현하기 때문에 인터페이스도 조금은 다른 의미의 조상이라고 할 수 있다.
  • 인터페이스에서 ‘public abstract’가 생략되어 있는 경우가 많은데, 오버라이딩 할 때는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야 하므로 이를 구현하는 클래스에서는 구현하는 메서드의 접근 제어자를 반드시 public으로 해야 한다.

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

  • 두 조상으로부터 상속받는 멤버 중에서 멤버변수의 이름이 같거나 메서드의 선언부가 일치하고 구현 내용이 다르다면 자손 클래스는 어느 조상의 것을 상속받게 되는 것인지 알 수 없다.
  • 자바에서는 단점이 더 크다고 판단하였기에 다중상속을 허용하지 않는다.
  • 인터페이스는 static상수만 정의할 수 있어서 조상클래스의 멤버변수와 충돌하는 경우가 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능하다.
  • 그리고 추상메서드는 구현내용이 전혀 없으므로 조상클래스의 메서드와 선언부가 일치하는 경우에는 조상클래스 쪽의 메서드를 상속받으면 된다.
  • 그러나 이렇게 하면 상속받는 멤버의 충돌은 피할 수 있지만, 다중상속의 장점을 잃게 된다.
  • 만일 두 개의 클래스로부터 상속을 받아야 할 상황이라면, 두 조상클래스중 비중이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부에 멤버로 포함시키는 방식을 사용하거나 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다

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

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

  • 따라 인터페이스는 다음과 같이 메서드의 매개변수 타입으로 사용 될 수 있다.
void attack(Fightable f){
	//...
}
  • 인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야한다는 것
  • 예를 들어, attack메서드를 호출할 때는 매개변수로 Fightable 인터페이스를 구현한 클래스의 인스턴스를 넘겨주어야 한다.
  • 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.
Fightable method() {
	Fighter f = new Fighter();
	return f;
}
  • 리턴 타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

7.7 인터페이스의 장점

  1. 개발시간을 단축시킬 수 있다.
    -> 메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문에 이를 사용해서 프로그램을 작성하는 것이 가능
  2. 표준화가 가능하다.
    -> 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성함으로써 일관된 개발이 가능
  3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
    -> 서로 상속관계에 있지도 않고, 같은 조상클래스를 가지고 있지 않은 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어준다.
  4. 독립적인 프로그래밍이 가능하다.
    -> 인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 프로그래밍이 가능

예를 들어 건물을 표현하는 클래스 Academy, Bunker, Barrack, Factory가 있고 이들의 조상인 Building클래스가 있다고 하자. 이 때 Barrack클래스와 Factory클래스에 건물을 이동시킬 수 있는 메서드를 추가하고자 하면 어떻게 해야할까?

  • Barrack클래스와 Factory클래스 모두 같은 코드를 적어두면 되지만, 코드가 중복된다는 단점이 있다.
  • 그렇다고 조상클래스인 building클래스에 코드를 추가해주면 다른 자손인 Academy클래스와 Bunker클래스도 추가된 코드를 상속받으므로 안 된다.

  • 우선 새로 추가하고자하는 메서드를 정의하는 인터페이스를 정의하고 이를 구현하는 클래스를 작성한다.
interface Liftable {
	void liftOff();
	void move(int x, int y);
	void stop();
	void land();
}

class liftableImpl implements Liftable {
	public void liftOff()		{}
	public void move(int x, int y)	{}
	public void stop()		{}
	public void land() 		{}
}
  • 새로 작성된 인터페이스와 이를 구현한 클래스를 Barrack과 Factory클래스에 적용하면 된다.
  • Barrack클래스가 Liftable인터페이스를 구현하도록 하고, 인터페이스를 구현한 LiftableImpl클래스를 Barrack클래스에 포함시켜서 내부적으로 호출해서 사용하도록 한다.
  • 이렇게 함으로써 같은 내용의 코드를 LiftableImpl클래스 한 곳에서 관리할 수 있고, 후에 다시 재사용될 수 있을 것이다.

7.8 인터페이스의 이해

인터페이스를 이해하기 위해서는 다음의 두 가지 사항을 반드시 염두에 두고 있어야 한다.

  1. 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
  2. 메서드를 사용하는 쪽에서는 사용하려는 메서드의 선언부만 알면 된다.(내용은 몰라도 된다)
  • 직접적인 관계의 두 클래스는 한 쪽이 변경되면 다른 한 쪽도 변경되어야 한다는 단점이 있다.
  • 클래스 A가 클래스 B를 직접 호출하지 않고 인터페이스를 매개체로 해서 접근하도록하면, B에 변경사항이 생겨도 A는 전혀 영향을 받지 않도록 하는 것이 가능하다.
  • 두 클래스간의 관계를 간접적으로 변경하기 위해서는 먼저 인터페이스를 이용해서 B의 선언과 구현을 분리해야한다.
//직접적인 관계
class A {
	public void methodA(B b)	//여기서 B는 인터페이스 I를 구현했다고 가정한다.
		b.methodB();
	}
}

//간접적인 관계
class A {
	public void methodA(I i)
		i.methodB();
	}
}
  • 클래스 A를 작성하는데 있어서 B를 사용하지 않도록 작성하면, A는 여전히 클래스 B를 호출하지만 인터페이스 I와 직접적인 관계에 있기 때문에 클래스 B의 변경에 영향을 받지 않는다.
  • 인터페이스 I는 실제구현 내용을 감싸고 있는 껍데기이며, 클래스 A는 껍데기안에 어떤 알맹이가 들어있는지 몰라도 된다.
  • 인터페이스 타입의 참조변수에는 toString()이 정의되어 있지 않지만, 모든 객체는 Object클래스에 정의된 메서드를 가지고 있을 것이기 때문에 허용한다.

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

JDK1.8부터 디폴트 메서드와 static메서드도 추가할 수 있게 되었다.

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

디폴트 메서드

  • 인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 모든 클래스들이 새로 추가된 메서드를 구현해야 한다.
  • 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.
  • 앞에 키워드 default를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통{}이 있어야 한다. 접근 제어지가 public이며 생략 가능하다.
  • 대신, 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생한다.

충돌을 해결하는 규칙

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

8. 내부클래스

8.1 내부 클래스란?

클래스 내에 선언된 클래스

내부클래스의 장점

  1. 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
  2. 코드의 복잡성을 줄일 수 있다(캡슐화).
class A {		//외부클래스
	class B {	//내부클래스

	}
}
  • 이 때 내부클래스인 B는 A를 제외하고는 잘 사용되지 않는 것이어야 한다.

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

  • 내부 클래스의 종류는 변수의 선언위치에 따른 종류와 같다.
  • 변수를 선언하는 것과 같은 위치에 선언할 수 있으며, 선언 위치에 따라 인스턴스 내부클래스, 스태틱 내부클래스, 지역 내부클래스로 나뉜다.
  • 각 내부 클래스의 선언위치에 따라 같은 선언위치의 변수와 동일한 유효범위와 접근성을 갖는다.

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

  • 내부 클래스가 외부 클래스의 멤버와 같이 간주되고, 인스턴스멤버와 static멤버 간의 규칙이 내부 클래스에도 똑같이 적용
  • 내부클래스도 클래스이기 때문에 abstract나 final과 같은 제어자를 사용할 수 있을 뿐만 아니라, 멤버변수들처럼 private, protected과 접근제어자도 사용이 가능하다.
  • 내부 클래스 중에서 스태틱 클래스만 static멤버를 가질 수 있다.
  • 다만 final과 static이 동시에 붙은 변수는 상수이므로 모든 내부 클래스에서 정의가 가능
  • 내부 클래스와 외부 클래스에 선언된 변수의 이름이 같을 때 변수 앞에 ‘this’또는 ‘외부 클래스명.this’를 붙여서 서로 구별할 수 있다.
  • 인스턴스클래스는 외부 클래스의 인스턴스멤버를 객체생성 없이 바로 사용할 수 있지만, 스태틱 클래스는 외부 클래스의 인스턴스 멤버를 객체생성 없이 사용할 수 없다.
  • 인스턴스클래스는 스태틱 클래스의 멤버들을 객체생성 없이 바로 사용할 수 있지만, 스태틱 클래스는 인스턴스 클래스의 멤버를 객체생성 없이 사용할 수 없다.
  • 인스턴스 클래스는 외부 클래스의 private멤버도 접근 가능하다.
  • 지역 클래스는 외부 클래스의 인스턴스 멤버와 static멤버를 모두 사용할 수 있으며, 메서드에 정의된 지역변수도 사용할 수 있다.
  • 단, final이 붙은 지역변수만 접근 가능한데, 메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도, 지역클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다.

    JDK 1.8부터 지역 클래스에서 접근하는 지역 변수 앞에 final을 생략할 수 있게 바뀌었다. 컴파일러가 자동으로 붙여주는 것이라 해당 변수의 값이 바뀌는 문장이 있으면 컴파일 에러가 발생한다.

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

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

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

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

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

        //스태틱 내부클래스의 인스턴스는 외부 클래스를 먼저 생성하지 않아도 된다.
        Outer.StaticInner si = new Outer.StaticInner();
        System.out.println("si.iv : " + si.iv);
    }
}
  • 컴파일 했을 때 생성되는 파일명은 ‘외부 클래스명$ 내부 클래스명.class’형식
  • 지역내부 클래스는 다른 메서드에 같은 이름의 내부 클래스가 존재할 수 있기 때문에 내부 클래스명 앞에 숫자가 붙는다.

8.4 익명 클래스(anonymous class)

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

new 조상클래스이름() {
	//멤버 선언
}
또는
new 구현인터페이스이름() {
	//멤버 선언
}
  • 이름이 없기 때문에 생성자도 가질 수 없으며, 조상클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.
  • 익명 클래스는 이름이 없기 때문에 ‘외부 클래스명$숫자.class’의 형식으로 클래스파일명이 결정

태그:

카테고리:

업데이트: