백엔드/JAVA

[JAVA의 정석] Ch 7. 객체 지향 프로그래밍 2

-minari- 2025. 4. 5. 12:05

1. 상속


1) 상속의 정의와 장점

상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 상속을 통해서 클래스를 작성하면 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 있기 때문에 코드의 추가 및 변경이 매우 용이하다.

 

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

class Child extends Parent {

}
조상 클래스 = 부모 클래스 = 상위 클래스 = 기반 클래스
자손 클래스 = 자식 클래스 = 하위 클래스 = 파생된 클래스 

 

조상 클래스가 변경되면 자손 클래스는 자동적으로 영향을 받게 되지만, 자손 클래스가 변경되는 것은 조상 클래스에 아무런 영향을 주지 못한다. 자손 클래스는 조상 클래스의 모든 멤버를 상속받으므로 항상 조상 클래스보다 같거나 많은 멤버를 갖는다. 즉, 상속에 상속을 거듭할수록 상속받는 클래스의 멤버 개수는 점점 늘어나게 된다. 그래서 상속을 받는다는 것은 조상 클래스를 확장한다는 의미로 해석할 수도 있으며 이것이 상속에 사용되는 키워드가 extends인 이유기도 하다.

 

 

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

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

class Circle{
    Point p = new Point(); // 원점
    int z;
}

class Point{
    int x;
    int y;
}

 

이와 같이 한 클래스를 작성하는 데 다른 클래스를 멤버 변수로 선언하여 포함시키는 것은 좋은 생각이다. 하나의 거대한 클래스를 작성하는 것보다 단위별로 여러 개의 클래스를 작성한 다음, 이 단위 클래스들을 포함 관계로 재사용하면 보다 간결하고 손쉽게 클래스를 작성할 수 있다. 또한 작성된 단위 클래스들은 다른 클래스를 작성하는데 재사용될 수 있을 것이다.

 

 

3) 클래스 간의 관계 결정하기

클래스를 작성할 때 상속관계를 맺어줄지, 포함관계를 맺어줄지 고민이 될 때가 있을 것이다.

아래 두 경우를 비교해보면 Circle 클래스를 작성하는 데 있어 Point 클래스를 포함시키거나 상속받도록 하는 것은 결과적으로 별 차이가 없어 보인다.

// 포함 관계
class Circle {
	Point p = new Point();
    int z;
}

// 상속 관계
class Circle extends Point{
	int z;
}

 

그럴 때는 '~은 ~이다(is - a)'와 '~은 ~를 가지고 있다(has - a)'를 넣어서 문장을 만들어 보면 클래스 간의 관계가 보다 명확해진다.

원(Circle)은 점(Point)이다.
원(Circle)은 점(Point)을 가지고 있다

 

두 문장을 비교해보면 후자가 더 옳다는 것을 알 수 있다. 이처럼 클래스를 가지고 문장을 만들었을 때 '~은 ~이다'라는 문장이 성립한다면, 서로 상속 관계를 맺어주고, '~은 ~을 가지고 있다'는 문장이 성립되면 포함 관계를 맺어주면 된다.

 

 

4) 단일 상속 (single inheritance)

다른 객체 지향 언어인 C++에서는 여러 조상 클래스로부터 상속받는 것이 가능한 '다중 상속'을 허용하지만 자바에서느 오직 단일 상속만을 허용한다. 그래서 둘 이상의 클래스로부터 상속을 받을 수 없다.

class TVCR extends TV, VCR {   // error. 조상은 하나만 허용된다

}

 

 다중 상속을 이용하게 되면 TV나 VCR에 power()라는 동일한 메서드가 있는 경우 TVCR 입장에서는 어떤 메서드를 상속받아야 하는지 알 수가 없다. 자바에서는 이러한 문제점을 해결하기 위해서 다중 상속의 장점을 포기하고 단일 상속만을 허용한다. 

 단일 상속이 하나의 조상 클래스만을 가질 수 있기 때문에 다중 상속에 비해 불편한 점도 있지만, 클래스 간의 관계가 보다 명확해지고 코드를 더욱 신뢰할 수 있게 만들어 준다는 점에서 다중 상속보다 유리하다.

 

 

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

Object 클래스는 모든 클래스 상속 계층도의 최상위에 있는 조상 클래스이다. 다른 클래스로부터 상속받지 않는 모든 클래스들은 자동적으로 Object 클래스로부터 상속받게 함으로써 이것을 가능하게 한다. 

 

아무 것도 상속받지 않는 클래스가 있다고 정의하자. 이 코드를 컴파일하면 컴파일러는 자동적으로 'extends Object'를 추가하여 해당 클래스가 Object 클래스로부터 상속받도록 한다. 이렇게 함으로써 Object 클래스가 모든 클래스의 조상이 되도록 한다. 그래서 자바의 모든 클래스들은 Object 클래스의 멤버들을 상속받기 때문에 Object 클래스에 정의된 멤버들을 사용할 수 있다.

 

 

2. 오버라이딩 (Overriding)


1) 오버라이딩이란?

조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩이라고 한다. 상속받은 메서드를 그대로 사용하기도 하지만, 자손 클래스 자신에 맞게 변경해야 하는 경우가 많다. 이럴 때 조상의 메서드를 오버라이딩한다.

 

 

2) 오버라이딩의 조건

자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와
- 이름이 같아야 한다
- 매개 변수가 같아야 한다
- 반환 타입이 같아야 한다

 

한마디로 요약하면 선언부가 서로 일치해야 한다는 것이다. 다만 접근 제어자와 예외는 제한된 조건 하에서만 다르게 변경할 수 있다.

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

만일 조상 클래스에 정의된 메서드의 접근 제어자가 protected라면, 이를 오버라이딩하는 자손 클래스의 메서드는 접근 제어자가 protected나 public이어야 한다. 접근 제어자의 접근 범위를 넓은 것에서 좁은 것 순으로 나열하면 public, protected, (default), private이다

 

2. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다

여기서 주의해야 할 점은 단순히 선언된 예외의 개수의 문제가 아니라는 것이다. Exception은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 던질 수 있도록 선언한 것이다

 

3. 인스턴스 메서드를 static 메서드로 또는 그 반대로 변경할 수 없다.

 

 

3) 오버로딩 vs 오버라이딩

오버로딩과 오버라이딩은 서로 혼동하기 쉽지만 사실 그 차이는 명백하다

오버로딩(overloading) : 기존에 없는 새로운 메서드를 정의하는 것 (new)
오버라이딩(overriding) : 상속받은 메서드의 내용을 변경하는 것 (change, modify)
class Parent{
    void parentMethod();
}

class Child extends Parent{
    void parentMethod();         // 오버라이딩
    void parentMethod(int i);    // 오버 로딩
    
    void childMethod();         
    void childMethod(int i);    // 오버 로딩
}

 

4) super

super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조 변수이다. 

 

조상 클래스에 선언된 멤버 변수와 같은 이름의 멤버 변수를 자손 클래스에서 중복해서 정의하는 것이 가능하며 참조 변수 super를 통해 서로 구별할 수 있다.

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

class Parent{
    int x = 10;
}

class Child{
    int x = 20;
    
    void method(){
    	System.out.println("x = " + x);                 // 20
        System.out.println("this.x = " + this.x);       // 20
        System.out.println("super.x = " + super.x);     // 10
   	}
}

 

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

this()는 같은 클래스의 다른 생성자를 호출하는 데 사용되지만, super()는 조상 클래스의 생성자를 호출하는데 사용된다.

 

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

 

 

3. package와 import


1) 패키지 (package)

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

 

- 하나의 소스파일에는 첫 번째 문장으로 단 한 번의 패키지 선언만을 허용한다
- 모든 클래스는 반드시 하나의 패키지에 속해야 한다.
- 패키지는 점(.)을 구분자로 하여 계층 구조로 구성할 수 있다. 
- 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.

 

2) 패키지의 선언

패키지를 선언하려면 클래스나 인터페이스의 소스파일의 맨 위에 다음과 같이 한 줄만 적어주면 된다.

package 패키지명;

위와 같은 패키지 선언문은 반드시 소스파일에서 주석과 공백을 제외한 첫 번째 문장이어야 하며, 하나의 소스파일에 단 한번만 선언될 수 있다. 해당 소스파일에 포함된 모든 클래스나 인터페이스는 선언된 패키지에 속하게 된다.

 

소스파일을 작성할 때 패키지를 선언하지 않아도 아무런 문제가 발생하지 않는데, 이는 자바에서 기본적으로 제공하는 '이름 없는 패키지' 때문이다. 소스파일에 자신이 속할 패키지를 지정하지 않은 모든 클래스를 자동적으로 '이름 없는 패키지'에 속하게 된다. 결국 패키지를 지정하지 않는 모든 클래스들은 같은 패키지에 속하는 셈이다.

 

 

3) import 문

소스 코드를 작성할 때 다른 패키지의 클래스를 사용하려면 패키지명이 포함된 클래스 이름을 사용해야 한다. 하지만, 매번 패키지명을 붙여서 작성하는 것은 불편하다.

 

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

 

 

4) import 문의 선언

모든 소스파일에서 import 문은 package 문 다음에, 그리고 클래스 선언문 이전에 위치해야 한다. import 문은 package 문과 달리 한 소스파일에 여러 번 선언할 수 있다.

 

import 문을 선언하는 방법은 다음과 같다.

한 패키지에서 여러 클래스를 사용하는 경우 클래스의 이름을 일일이 지정해주는 것보다 '패키지명.*'와 같이 하는 것이 편리하다.

import 패키지명.클래스명;

import 패키지명.*;

 

 

단 주의해야 할 점은 import 문에서 클래스의 이름 대신 '*'을 사용하는 것이 하위 패키지의 클래스까지 포함하는 것은 아니라는 것이다.

import java.util.*;
import java.text.*;

// 위의 두 문장을 아래와 같이 대신할 수 없다
import.java.*;

 

 

5) static import 문

import 문을 사용하면 클래스의 패키지명을 생략할 수 있는 것과 같이 static import 문을 사용하면 static 멤버를 호출할 때 클래스 이름을 생략할 수 있다. 특정 클래스의 static 멤버를 자주 사용할 떄 편리하다

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

System.out.println(Math.random());

out.println(random());

 

 

4. 제어자 (modifier)


1) 제어자란?

제어자란 클래스, 변수 또는 메서드의 선언부에 함께 사용되어 부가적인 의미를 부여한다. 제어자의 종류는 크게 접근 제어자와 그 외의 제어자로 나눌 수 있다.

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

 

제어자는 클래스나 멤버 변수와 메서드에 주로 사용되며, 하나의 대상에 대해서 여러 제어자를 조합하여 사용하는 것이 가능하다. 다만, 접근 제어자는 한 번에 네 가지 중 하나만 선택해서 사용할 수 있다.

 

 

2) static - 클래스의, 공통적인

static은 '클래스의' 또는 '공통적인'의 의미를 가지고 있다. 인스턴스 변수는 하나의 클래스로부터 생성되었더라도 각기 다른 값을 유지하지만, 클래스 변수는 인스턴스에 관계없이 같은 값을 갖는다. 그 이유는 하나의 변수를 모든 인스턴스가 공유하기 때문이다. 

 

static이 붙은 멤버 변수와 메서드, 그리고 초기화 블럭은 인스턴스가 아닌 클래스에 관계된 것이기 때문에 인스턴스를 생성하지 않고도 사용할 수 있다.

제어자 대상 의미
static 멤버변수 - 모든 인스턴스에 공통적으로 사용되는 클래스 변수가 된다
- 클래스 변수는 인스턴스를 생성하지 않고도 사용 가능하다
- 클래스가 메모리에 로드될 때 생성된다
메서드 - 인스턴스를 생성하지 않고도 호출이가능한 static 메서드가 된다
- static 메서드 내에서는 인스턴스 멤버들을 직접 사용할 수 없다. 

 

 

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

final은 '마지막의' 또는 '변경될 수없는'의 의미를 가지고 있으며, 거의 모든 대상에 사용될 수 있다.

 변수에 사용되면 값을 변경할 수 없는 상수가 되며, 메서드에 사용되면 오버라이딩을 할 수 없게 되고, 클래스에 사용되면 자신을 확장하는 자손 클래스를 정의하지 못하게 된다.

제어자 대상 의미
final 클래스 변경될 수 없는 클래스, 확장될 수 없는 클래스가 된다
그래서 final로 지정된 클래스는 다른 클래스의 조상이 될 수 없다
메서드 변경될 수 없는 메서드 final로 지정된 메서드는 오버라이딩을 통해 재정의 될 수 없다
멤버 변수 변수 앞에 final이 붙으면, 값을 변경할 수 없는 상수가 된다.
지역 변수

 

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

final이 붙은 변수는 상수이므로 일반적으로 선언과 초기화를 동시에 하지만, 인스턴스 변수의 경우 생성자에서 초기화 되도록 할 수 있다. 

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

 

 

4) abstract - 추상의, 미완성의

abstract는 '미완성'의 의미를 가지고 있다. 메서드의 선언부만 작성하고 실제 수행 내용은 구현하지 않은 추상 메서드를 선언하는데 사용된다. 

제어자 대상 의미
abstract 클래스 클래스 내의 추상 메서드가 선언되어 있음을 의미한다
메서드 선언부만 작성하고 구현부는 작성하지 않은 추상 메서드임을 알린다

 

5) 접근 제어자 (access modifier)

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

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

접근 범위
public > protected > default > private
대상 사용 가능한 접근 제어자 
클래스 public, (default)
메서드 public, protected, (default), private
멤버 변수
지역 변수 없음

 

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

 클래스나 멤버에 접근 제어자를 사용하는 이유는 클래스의 내부에 선언된 데이터를 보호하기 위해서이다. 데이터가 유효한 값을 유지하도록, 또는 비밀번호와 같은 데이터를 외부에서 함부로 변경하지 못하도록 하기 위해서는 외부로부터의 접근을 제한하는 것이 필요하다.

 또 다른 이유는 클래스 내에서만 사용되는 내부 작업을 위해 임시로 사용되는 멤버 변수나 부분 작업을 처리하기 위한 메서드 등의 멤버들을 클래스 내부에 감추기 위해서다.

 

 만일 메서드 하나를 변경해야 한다고 가정했을 때, 이 메서드의 접근제어자가 public이라면, 메서드를 변경한 후에 오류가 없는지 테스트해야 한느 범위가 넓다. 그러나 접근 제어자가 default라면 패키지 내부만 확인해보면 되고, private면 클래스 하나만 살펴보면 된다. 이처럼 접근 제어자 하나가 때로는 상당한 차이를 만들어 낼 수 있다.

 

 

생성자의 접근 제어자

생성자에 접근 제어자를 사용함으로써 인스턴스의 생성을 제한할 수 있다 

 

생성자의 접근 제어자를 private로 지정하면, 외부에서 생성자에 접근할 수 없으므로 인스턴스를 생성할 수 없게 된다. 그래도 클래스 내부에서는 인스턴스를 생성할 수 있다. 대신 인스턴스를 생성해서 반환해주는 public 메서드를 제공함으로써 외부에서 이 클래스를 사용하도록 할 수 있다. 이 메서드는 public인 동시에 static이어야 한다.

class Singleton{
    private static Singleton s = new Singleton();
    
    private Singleton(){
    ...
    }
    
    // 인스턴스를 생성하지 않고도호출할 수 있어야 하므로 static이어야 한다. 
    public static Singleton getInstance(){
    	return s;
    }
}

 

6) 제어자의 조합

대상 사용 가능한 제어자
클래스 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. 다형성


1) 다형성이란?

객체 지향 개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조 변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다. 즉, 조상 클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.

 

TV와 CaptionTV는 서로 상속관계에 있다.

 보통은 인스턴스 타입과 참조 변수의 타입이 일치하는 것이 보통이지만, 서로 상속관계에 있는 경우, 아래와 같이 조상 클래스 타입의 참조 변수로 자손 클래스의 인스턴스를 참조하도록 하는 것도 가능하다

class TV {
}

class CaptionTv extends TV{
}

TV t = new CaptionTv();

 

둘 다 같은 타입의 인스턴스를 생성하였지만, 참조 변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다. 자식 클래스의 범위가 부모 클래스의 범위보다 넓기 때문이다.

CaptionTv c = new CaptionTv();
TV t = new CaptionTv();

 

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

 

 

2) 참조 변수의 형변환

기본형 변수와 같이, 참조 변수도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에 자손 타입의 참조 변수를 조상 타입의 참조 변수로, 조상 타입의 참조 변수를 자손 타입의 참조변수로의 형변환만 가능하다

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

 

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

 

 

3) instanceof 연산자

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

참조변수 instanceof 타입(클래스)

 

아래 코드의 if 문의 결과는 전부 true이다. 그 이유는 FireEngine클래스는 Object 클래스와 Car 클래스의 자손 클래스이므로 조상 멤버들을 상속받았기 때문에, FireEngine 인스턴스는 Object 클래스와 Car 클래스를 포함하고 있는 셈이기 때문이다.

public static void main(String args[]){
    FireEngine fe = new FireEngine();
    
    if(fe instanceof FireEngine){
    }
    
    if(fe instanceof Car){
    }
    
    if(fe instanceof Object){
    }
}

class Car{ }
class FireEngine extends Car{ }

 

 

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

조상 클래스와 자손 클래스에 중복된 메서드와 멤버 변수가 있는 경우에 대해서 알아보자

 

 메서드의 경우 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우에도 참조 변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드=자손 클래스의 메서드)가 호출된다.

 

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

 

 

6. 추상 클래스 (abstract class)


1) 추상 클래스란?

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

 

 추상 클래스 자체로는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 조상 클래스로서 중요한 의미를 갖는다. 새로운 클래스를 작성할 때 아무것도 없는 상태에서 시작하는 것보다는 완전하지는 못하더라도 어느 정도 틀을 갖춘 상태에서 시작하는 것이 낫다.

 

추상 클래스는 키워드 'abstract'만 붙이면 된다. 이렇게 함으로써 이 클래스를 사용할 때, 클래스 선언부의 abstract을 보고 이 클래스에는 추상 메서드가 있으미 상속을 통해서 구현해주어야 한다는 것을 쉽게 알 수 있을 것이다.

abstract class 클래스 이름{

}

 

- 추상 클래스는 추상 메서드를 포함하고 있다는 것을 제외하고는 일반 클래스와 전혀 다르지 않다. 추상 클래스에도 생성자가 있으며, 멤버 변수와 메서드도 가질 수 있다.

 

 

2) 추상 메서드

메서드를 미완성 상태로 남겨놓는 이유는 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에, 조상 클래스에서는 선언부만 작성하고, 실제 내용은 상속받는 클래스에서 구현하도록 비워두는 것이다. 

 

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

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

 

 

7. 인터페이스


1) 인터페이스란?

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

 

 인터페이스도 추상 클래스처럼 완성되지 않은 불완전한 것이기 때문에 그 자체만으로 사용되기보다는 다른 클래스를 작성하는데 도움을 줄 목적으로 작성된다.

 

 

2) 인터페이스의 작성

 인터페이스를 작성하는 것은 클래스를 작성하는 것과 같다. 다만 키워드로 class 대신 interface를 사용한다는 것만 다르다.

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

 

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

 

 

3) 인터페이스의 상속

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

interface Movable{
}  

interface Attackable{
}

interface Fightable extends Movable, Attackable{
}

 

 

4) 인터페이스의 구현

인터페이스도 자신에 정의된 추상 메서드의 몸통을 만들어주는 클래스를 작성해야 하는데, 그 방법은 추상 클래스가 자신을 상속받는 클래스를 정의하는 것과 다르지 않다. 다만 클래스는 확장한다는 의미의 키워드인 extends를 사용하지만, 인터페이스는 구현한다는 의미의 키워드인 implements를 사용할 뿐이다.

class 클래스이름 implements 인터페이스이름{
}

 

 

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

인터페이스는 static 상수만을 정의할 수 있으므로 조상 클래스의 멤버 변수와 충돌하는 경우는 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능하다. 그리고 추상 메서드는 구현 내용이 전혀 없으므로 조상 클래스의 메서드와 선언부가 일치하지 않는 경우에는 당연히 조상 클래스 쪽의 메서드를 상속 받으면 되므로 문제되지 않는다.

 

 그러나, 이렇게 하면 상속받는 멤버의 충돌은 피할 수 있지만, 다중 상속의 장점을 잃게 된다. 만일 두 개의 클래스로부터 상속을 받아야 할 상황이라면, 두 조상 클래스 중에서 비중이 높은 쪽을 선택하고 다른 한 쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나 어느 한 쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다.

 

 

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

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

 

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

 

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

 

 

7) 인터페이스의 장점

 1. 개발 시간을 단축할 수 있다

 2. 표준화가 가능하다

 3. 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다

 4. 독립적인 프로그래밍이 가능하다

 

 

8) 인터페이스의 이해

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

- 클래스를 사용하는 쪽(USER)과 클래스를 제공하는 쪽(PROVIDER)가 있다
- 메서드를 사용(호출)하는 쪽에서는 사용하려는 메서드의 선언부만 알고 있으면 된다(내용은 몰라도 됨)

 

 

8. 내부 클래스 (inner class)


1) 내부 클래스란?

내부 클래스란 클래스 내에 선언된 클래스이다. 클래스에 다른 클래스를 선언하는 이유는 두 클래스가 서로 긴밀한 관계에 있기 때문이다.

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

 

 

2) 내부 클래스의 종류와 특징

내부 클래스는 마치 변수를 선언하는 것과 같은 위치에 선언할 수 있으며, 선언 위치에 따라 구분된다.

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

 

 

3) 내부 클래스의 선언

Outer 외부 클래스에 3개의 서로 다른 종류의 내부 클래스를 선언하였다

class Outer{
    class InstanceInner{ }
    static class StaticInner{ }
    void myMethod(){
    	class LocalInner{}
    }
}