객체지향 프로그래밍2

우수민·2021년 10월 26일
0
post-thumbnail

1. 상속(inheritance)

1.1 상속의 장점

  • 상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.
  • 상속을 통해서 클래스를 작성하면 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 있기 때문에 코드의 추가 및 변경이 매우 용이하다.
  • 이러한 특징은 코드의 재사용성을 높이고 코드의 중복을 제거하여 프로그램의 생산성과 유지보수에 크게 기여한다.
class child extends Parent{
	...
}
  • 만일 Parent클래스에 age라는 정수형 변수를 멤버 변수로 추가하면, 자손 클래스는 조상의 멤버를 모두 상속받기 때문에, Child클래스는 자동적으로 age라는 멤버 변수가 추가된 효과를 얻는다.

생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.

  • 전체 프로그램을 구성하는 클래스들을 면밀히 설계 분석하여, 클래스간의 상속관계를 적절히 맺어 주는 것이 객체지향 프로그래밍에서 가장 중요한 부분이다.

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

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

  • 상속이외에도 클래스를 재사용하는 또 다른 방법이 있는데, 그것은 클래스간의 '포함(Composite)'관계를 맺어 주는 것이다. 클래스 간의 포함관계를 맺어 주느 ㄴ것은 한 클래스의 멤버변수로 다른 클래스의 타입의 참조변수를 선언하느 ㄴ것으 ㄹ뜻한다.
class  Circle{
	int x; // 원점의 x좌표
    int y; // 원점의 y좌표
    int r; // 반지름
}

class Point{
	int x; // x좌표
    int y; // y좌표
}

// 재사용 할 경우
class Circle{
	Point c = new Point();
    int r;
}
  • 이와 같이 한 클래스를 작성하는 데 다른 클래스를 멤버변수로 선언하여 포함시키는 것은 좋은 생각이다. 하나의 거대한 클래스를 작성하는 것보다 단위별로 여러 개의 클래스를 작성한 다음, 이 단위 클래스들을 포함관계로 재사용하면 보다 간결하고 손쉽게 클래스르 작성할 수 있다.

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

원(Circle)은 점(Point)이다. -> 포함
원(Circle)은 점(Point)을 가지고 있다. -> 상속

1.4 단일 상속(single inheritance)

  • 둘 이상의 클래스로부터 상속을 받을 수 없다.
class TVCR extends TV, VCR{ // 에러. 조상은 하나만 허용된다.
	...
}
  • 다중상속을 허용하면 여러 클래스로부터 상속받을 수 있기 때문에 복합적인 기능을 가진 클래스를 쉽게 작성할 수 있다는 장점이 있지만, 클래스 간의 관계가 매우 복잡해진다는 것과 다른 클래스로부터 상속받은 멤버간의 이름이 같은 경우 구별할 수 있는 방법이 없다는 단점을 가지고 있다.

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

  • Object클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스이다. 다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object클래스로부터 상속받게 함으로써 이것을 가능하게 한다.
class Tv (extends Object){ // 실제로 compile을 하면 괄호 안이 생성된채 실행된다.
	...
}
  • toString()이나 equals(Object o)와 같은 메서드를 따로 정의하지 않고도 사용할 수 있었던 이유는 이 메서드들이 Object클래스에 정의된 것들이기 때문이다.

오버라이딩(overriding)

2.1 오버라이딩이란?

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

2.2 오버라이딩의 조건

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

  • 한마디로 요약하면 선언부가 서로 일치해야 한다. 다만 접근 제어자(access modifier)와 예외(exception)는 제한된 조건 하에서만 다르게 변경할 수 있다.
  1. 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경 할 수 없다.
    • 만일 조상 클래스에 정의된 메서드의 접근 제어자가 protected라면, 이를 오버라이딩하는 자손 클래스의 메서드는 접근 제어자가 protected나 public이어야 한다. 대부분의 경우 같은 범위의 접근 제어자를 사용한다.
    • 접근 제어자의 접근범위를 넓은 것에서 좁은 것 순으로 나열하면 public, protected, (default), private이다.
  2. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.

2.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) // 오버로딩
}

2.4 super

  • super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수이다.
  • 멤버변수와 지역변수의 이름이 같을 때 this를 붙여서 구별했듯이 상속받은 멤버와 자신의 멤버와 이름이 같을 때는 super을 붙여서 구별할 수 있다.
  • 조상 클래스로부터 상속받은 멤버도 자손 클래스 자신의 멤버이므로 super대신 this를 사용할 수 있다. 그래도 조상 클래스의 멤버와 자손클래스의 멤버가 중복 정의되어 서로 구별해야하는 경우에만 super를 사용하는 것이 좋다.
  • 조상의 멤버와 자신의 멤버를 구별하는데 사용된다는 점을 제외하고는 super와 this는 근본적으로 같다. 모든 인스턴스메서드에는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데, 이것이 참조 변수인 this와 super의 값이 된다.
  • static메서드(클래스 메서드)는 인스턴스와 관련이 없다. 그래서 this와 마찬가지로 super 역시 static메서드에서는 사용할 수 없고 인스턴스메서드에서만 사용할 수 있다.
class SuperTest{
	public static void main(String args[]){
    	Child c = enw Child();
        c.method(); 
    }
}

class Parent{
	int x=10;
}

class Child extends Parent{
	int x=20;

	void method(){
    	System.out.println("x="+x); // x=20
    	System.out.println("this.x="+this.x); // this.x = 20
    	System.out.println("super.x="+super.x); // super.x = 10
    }
}
  • 변수만이 아니라 메서드 역시 super를 써서 호출할 수 있다. 특히 조상 클래스와 메서드를 자손 클래스에서 오버라이딩한 경우에 super를 사용한다.

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

  • this()와 마찬가지로 super() 역시 생성자이다.
  • this()는 같은 클래스의 다른 생성자를 호출하는데 사용하지만, super()는 조상 클래스의 생성자를 호출하는데 사용한다.
class PointTest{
	public static void main(String args[]){
    	Point3D p3 = new Point3D(1,2,3);
    }
}

class Point{
	int x,y;
    
    Point(int x, int y){
    	this.x = x;
        this.y = y;
    }
    
    String getLocation(){
    	return "x : "+x+", y :"+y;
    }
}

class Point3D extends Point{
	int z;
    
    Point3D(int x, int y, int z){
    	super(); // 중요!!!!!!!!!! -> 조상 클래스의 생성자 Point(int x, int y)를 호출한다.
        this.z = z;
    }
    
    String getLocation(){
    	return "x : "+x+", y :"+y+",z:"+z; // 오버라이딩
    }

}
  • 조상 클래스의 멤버변수는 이처럼 조상의 생성자에 의해 초기화되도록 해야 한다.
class PointTest2{
	public static void main(String args[]){
    	Point 3D p3 = new Point3D();
        System.out.println("p3.x="+p3.x);
        System.out.println("p3.y="+p3.y);
        System.out.println("p3.z="+p3.z);
    }
}

class Point {
	int x=10;
    int y=20;
    
    Point{
    	super(); // 조상인 Object클래스의 생성자 Object()를 호출한다.
        this.x = x;
        this.y = y;
    }
}

class Point3D extends Point {
	int z = 30;
    
    Point3D(){
    	this(100, 200, 300); // Point3D(int x, int y, int z)를 호출한다.
    }
    
    Point3D(int x, int y, int z){
    	super(x,y); // Point(int x, int y)를 호출한다.
        this.z = z;
    }
}

package와 import

3.1 패키지(package)

  • 패키지란, 클래스의 묶음이다.
  • 패키지에는 클래스 또는 인터페이스를 포함시킬 수 있으며, 서로 관련된 클래스들끼리 그룹 단위로 묶어 놓음으로써 클래스를 효율적으로 관리할 수 있다.
  • 같은 이름의 클래스 일지라도 서로 다른 패키지에 존재하는 것이 가능하므로, 자신만의 패키지 체계를 유지함으로써 다른 개발자가 개바한 클래스 라이브러리의 클래스와 이름이 충돌하는 것을 피할 수 있다.
  • 클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리이다.
    • 하나의 소스파일에는 첫 번째 문장으로 단 한번의 패키지 선언만을 허용한다.
    • 모든 클래스는 반드시 하나의 패키지에 속해야 한다.
    • 패키지는 점(.)을 구분자로 하여 계층구조로 구성할 수 있다.
    • 패키지는 물리적으로 클래스 파일(.class)를 포함하는 하나의 디렉토리이다.

3.2 패키지의 선언

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

3.3 import문

  • 소스코드를 작성할 때 다른 패키지의 클래스를 사용하려면 패키지명이 포함된 클래스 이름을 사용해야 한다.

    import문은 프로그램의 성능에 전혀 영향을 미치지 않는다. import문을 많이 사용하면 컴파일 시간이 아주 조금 더 걸리 뿐이다.

3.4 import문의 선언

일반적인 소스파일(*.java)의 구성은 1. package문 2. import문 3. 클래스 선언의 순서로 되어 있다.

import 패키지명.클래스명;
//or
import 패키지명.*; // 실행시 성능차이 X 

3.5 static import문

  • import문을 사용하면 클래스의 패키지명을 생략할 수 있는 것과 같이 static import문을 사용하면 static멤버를 호출할 때 이름을 생략할 수 있다. 특정 클래스의 static멤버를 자주 사용할 때 편리하다. 그리고 코드도 간결해진다.
import static java.lang.Integer.*; // Integer클래스의 모든 static메서드

4. 제어자(modifier)

4.1 제어자란?

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

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

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

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

  • static은 '클래스의' 또는 '공통적인'의 의미를 가지고 있다.
  • 인스턴스변수는 하나의 클래스로부터 생성되었더라도 각기 다른 값을 유지하지만, 클래스변수(static멤버변수)는 인스턴스에 관계없이 같은 값을 갖는다. 그 이유는 하나의 변수를 모든 인스턴스가 공유하기 때문이다.
  • static이 붙은 멤버변수와 메서드, 그리고 초기화 블럭은 인스턴스가 아닌 클래스에 관계된 것이기 때문에 인스턴스를 생성하지 않고도 사용할 수 있다.

    static이 사용될 수 있는 곳 - 멤버변수, 메서드, 초기화 블럭

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

	static { // 클래스 초기화 블럭
    	// static변수의 복잡한 초기화 수행
    }
    
    static int max(int a, int b){ // 클래스 메서드(static메서드)
    	return a>b ? a: b;
    }
}

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

  • final은 '마지막의' 또는 '변경될 수 없는'의 의미를 가지고 있으며 거의 모든 대상에 사용될 수 있다.
  • 변수에 사용되면 값을 변경할 수 없는 상수가 되며, 메서드에 사용되면 오버라이딩을 할 수 없게 되고 클래스에 사용되면 자신을 확장하는 자손클래스를 정의하지 못하게 된다.

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

final class FinalTest{ // 조상이 될 수 없는 클래스
	final int MAX_SIZE=10; // 값을 변경할 수 없는 멤버변수(상수)
	
    final void getMaxSize(){ // 오버라이딩할 수 없는 메서드(변경 불가)
    	final int LV = MAX_SIZE; // 값을 변경할 수 없는 지역변수(상수)
        return MAX_SIZE;
    }
}
  • 생성자를 이용한 final멤버 변수의 초기화
    • final이 붙은 변수는 상수이므로 일반적으로 선언과 초기화를 동시에 하지만, 인스턴스변수의 경우 생성자에서 초기화되도록 할 수 있다.
    • 클래스 내에 매개변수를 갖는 생성자를 선언하여, 인스턴스를 생성할 때 final이 붙은 멤버변수를 초기화하는데 필요한 값을 생성자의 매개변수로부터 제공받은 것이다.
    • 이 기능을 활용하면 각 인스턴스마다 final이 붙은 멤버변수가 다른 값을 갖도록 하는 것이 가능하다.
class Card{
	final int NUMBER;
    final STRING KIND;
    static int width = 100;
    static int height = 250;
    
    Card(String kind, int num){
    	this.kind = kind;
        this.num = num;
    }
    
    Card(){
    	this("HEART", 1);
    }
    
    public String toString(){
    	return KIND + " " + NUMBER;
    }
}

class FinalCardTest{
	public static void main(String args[]){
    	Card c = new Card("HEART", 10);
//        c.NUMBER = 5; // 에러 
		System.out.println(c.KIND);
		System.out.println(c.NUMBER);
		System.out.println(c);
    	
    }
}

4.4 abstract - 추상의, 미완성의

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

    abstract가 사용될 수 잇는 곳 - 클래스, 메서드

  • 추상 클래스는 아직 완성되지 않은 메서드가 존재하는 '미완성 설계도'이므로 인스턴스를 생성할 수 없다.
abstract class AbstractTest{ // 추상 클래스
	abstract void move(); // 추상 메서드
}

4.5 접근 제어자(access modifier)

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

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

접근 범위가 넓은 쪽에서 좁은 쪽의 순으로 왼쪽부터 나열하면 다음과 같다.
public > protected > (default) > private

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

  • 클래스나 멤버, 주로 멤버에 접근 제어자를 사용하는 이유는 클래스의 내부에 선언된 데이터를 보호하기 위해서이다. 데이터가 유효한 값을 유지하도록, 또는 비밀번호와 같은 데이터를 외부에서 함수로 변경하지 못하도록 하기 위해서는 외부로부터의 접근을 제한하는 것이 필요하다.
  • 데이터가 유효한 값을 유지하도록, 또는 비밀번호와 같은 데이터를 외부에서 함부로 변경하지 못하도록 하기 위해서는 외부로부터의 접근을 제한가는 것이 필요하다.
  • 이것을 데이터 감추기(data hiding)라고 하며, 객체지향개념의 캡슐화(encapsulation)에 해당하는 내용이다.
  • 또 다른 이유는 클래스 내에서만 사용되는, 내부 작업을 위해 임시로 사용되는 멤버변수나 부분작업을 처리하기 위한 메서드 등의 멤버들을 클래스 내부에 감추기 위해서이다.
  • 외부에서 접근할 필요가 없는 멤버들을 private으로 지정하여 외부에 노출시키지 않음으로써 복잡성을 줄일 수 있다.
  • 만일 상속을 통해 확장될 것이 예상되는 클래스라면 멤버에 접근 제한을 주되 자손 클래스에만 접근하는 것이 가능하도록 하기 위해 private대신 protected를 사용한다. private이 붙은 멤버는 자손 클래스에서도 접근이 불가능하기 때문이다.
public class Time{
    private int hour; // 접근 제어자를 private으로 하여 외부에서 직접 접근하지 못하도록 한다.
    private int minute; // 접근 제어자를 private으로 하여 외부에서 직접 접근하지 못하도록 한다.
    private int second; // 접근 제어자를 private으로 하여 외부에서 직접 접근하지 못하도록 한다.
    
    public int getHour() { return hour; }
    public void setHour(int hour){
        if (hour <0 || hour>23) return;
        this.hour = hour
    }
    ...
}

생성자의 접근 제어자

  • 생성자에 접근 제어자를 사용함으로써 인스턴스의 생성을 제한할 수 있다. 보통 생성자의 접근 제어자는 클래스의 접근 제어자와 같지만, 다르게 지정할 수도 있다.
  • 생성자의 접근 제어자를 private으로 지정하면, 외부에서 생성자에 접근할 수 없으므로 인스턴스를 생성할 수 없게 된다. 그래도 클래스 내부에서는 인스턴스를 생성할 수 있다.
class Singleton{
	...
    private static  Singleton s = new Singleton(); // getInstance()에서 사용될 수 있도록 인스턴스가 미리 생성되어야 하므로  static이어야 한다.
    private Singleton(){
    	...
    }
    // 인스턴스를 생성하지 않고도 호출할 수 있어야 하므로 static이어야 한다.
    public static Singleton getInstance(){
    	return s;
    }
    
    	...
}
  • 이처럼 생성자를 통해 직접 인스턴스를 생성하지 못하게 하고 public메서드를 통해 인스턴스에 접근함으로써 사용할 수 있는 인스턴스의 개수를 제한할 수 있다.

  • 또 한가지, 생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다. 왜냐하면, 자손클래스의 인스턴스를 생성할 때 조상클래스의 생성자를 호출해야만 하는데, 생성자의 접근 제어자가 private이므로 자손 클래스에서 호출하는 것이 불가능하기 때문이다. 그래서 클래스 앞에 final을 추가하여 상속할 수 없는 클래스라는 것을 알리는 것이 좋다.

4.6 제어자(modifier)의 조합

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

5. 다형성(polymorphism)

5.1 다형성이란?

  • 객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.
  • 이를 좀 더 구체적으로 말하자면, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.
class Tv{
    boolean power;
    int channel;
    
    void power(){...};
    void channlUp(){...};
    void chnnelDown(){...};   
}
class CaptionTv extends Tv{
    String text;
    void caption(){...}
}

CaptionTv c = new CaptionTv();
Tv t = new CaptionTv();
// Caption Tv cc = new Tv; // 에러 발생 -> 허용X
  • 위의 c와 t는 같은 타입의 인스턴스이지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
  • 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.

    클래스는 상속을 통해서 확장될 수는 있어도 축소될 수는 없어서, 조상 인스턴스의 멤버 개수는 자손 인스턴스의 멤버 개수보다 항상 적거나 같다.

5.2 참조변수의 형변환

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

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

class Car{...}
class FireEngine extends Car{...}
class Ambulance extends Car{...}

// 예시1
FireEngine f;
Ambulance a;
a = (Ambulance) f; // 에러
f = (FireEngine) a; // 에러

// 예시2
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;

car = fe; // car = (Car)fe;에서 형변환됨. 업캐스팅
fe2 = (FireEngine)car//형변환을 생략 불가능. 다운 캐스팅
  • 형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조변수의 형변화을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것뿐이다.

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

5.3 instanceof연산자

  • 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다. 주로 조건문에 사용되며, instanceof의 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다.
void doWork(Car c){
    if (c instanceof FireEngine){
        FireEngine fe = (FireEngine)c;
        fe.water;
        ...
    } else if (c instanceof Ambulance){
        Ambulance a = (Ambulance)c;
        a.siren();
        ...
    }
    ...
}
  • 실제 인스턴스와 같은 타입의 instanceof연산 이외에 조상타입의 instanceof 연산에도 true를 얻으며, instanceof연산의 결과가 true라는 것은 검사한 타입으로 형변환을 해도 아무런 문제가 없다는 뜻이다.

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

  • 조상 클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때, 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우와 자손타입의 참조변수로 자손 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다.
  • 메서드의 경우 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우에도 참조 변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
  • 멤버 변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다.

5.5 매개변수의 다형성

  • 참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다.

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

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

    문자열과 참조변수의 덧셈(결합연산)은 참조변수에 toString()을 호출해서 문자열을 얻어 결합한다.

6. 추상클래스(abstract class)

6.1 추상클래스란?

  • 클래스를 설계도에 비유한다면, 추상클래스는 미완성 설계도에 비유할 수 있다. 미완성 설계도란, 단어의 뜻 그대로 완성되지 못한 채로 남겨진 설계도를 말한다.

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

  • 추상클래스는 상속을 통해서 자손클래스에 의해서만 완성될 수 있다.

  • 추상클래스 자체로는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 조상클래스로서 중요한 의미를 갖는다.

  • 추상클래스는 키워드 'abstract'를 붙이기만 하면 된다.

abstract class 클래스이름{
    ...
}

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

6.2 추상메서드(abstract method)

  • 메서드는 선언부와 구현부(몸통)로 구성되어 있다. 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨 둔것이 추상메서드이다. 즉, 설계만 해 놓고 실제 수행될 내용은 작성하지 않았기 때문에 미완성 메서드인 것이다.
  • 이와 같이 미완성 상태로 남겨 놓는 이유는 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만을 작성하고, 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려주고, 실제 내용은 상속받은 클래스에서 구현하도록 비워두는 것이다.
/* 주석을 통해 어떤 기능을 수행할 목적으로 작성하였는지 설명한다. */
abstract 리턴타입 메서드이름();

6.3 추상클래스의 작성

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

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

7. 인터페이스

7.1 인터페이스란?

  • 인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.
  • 오직 추상메서드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용하지 않는다.
  • 추상클래스를 부분적으로만 완성된 '미완성 설계도'라고 한다면, 인터페이스는 구현된것은 아무 것도 없고 밑그림만 그려져 있는 '기본 설계도'라 할 수 있다.

7.2 인터페이스의 작성

  • 인터페이스를 작성하는 것은 클래스를 작성하는 것과 같다. 다만 키워드를 class 대신 interface를 사용한다는 것만 다르다. 그리고 interface에도 클래스와 같이 접근제어자로 public 또는 default를 사용할 수 있다.
interface 인터페이스이름{
	public static final 타입 상수이름 =;
    public abstract 메서드이름(매개변수 목록);
}
  • 모든 멤버변수는 public static final이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
  • 인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에 제어자를 생략할 수 있는 것이며, 편의상 생략하는 경우가 많다. 생략된 제어자는 컴파일 시에 컴파일러가 자동적으로 추가해준다.

7.3 인터페이스의 상속

  • 인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와 달리 다중상속, 즉 여러개의 인터페이스로부터 상속을 받는 것이 가능하다.
interface Movable{
	void move(int x, int y);
}
interface Attackable{
	void attack(Unit u);
}

interface Fightable extends Movable, Attackable{};

7.4 인터페이스의 구현

  • 인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상클래스가 상속을 통해 추상메서드를 완성하는 것처럼, 인터페이스도 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야 하는데, 그 방법은 추상클래스가 자신을 상속받는 클래스를 정의하는 것과 다르지 않다.
  • 다만 클래스는 확장한다는 의미의 키워드 'extends'를 사용하지만 인터페이스는 구현한다는 의미의 키워드 'implements'를 사용할 뿐이다.
  • 만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, abstract를 붙여서 추상클래스로 선언해야 한다.
abstract class Fighter implements Fightable{
	public void move(int x, int y){...}	
}

인터페이스의 이름에는 주로 Fightable과 같이 '~을 할 수 있는'의 의미인 'albe'로 끝나는 것들이 많은데, 그 이유는 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공하는 의미를 강조하기 위해서이다. 이름이 'able'로 끝나는 것은 인터페이스라고 추측할 수 있지만, 모든 인터페이스의 이름이 반드시 'able'로 끝나야 하는 것은 아니다.

interface Movable{
	void move(int x, int y);
}
class Fighter extends Unit implements Fightable{
	public void move(int x, int y){...};
    public void attack(Unit u);
}
  • 오버라이딩을 할 때에는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야 한다. Movable인터페이스에 'void move(int x, int y)'와 같이 정의되어 있지만 사실 'public abstract'가 생략된 것이기 때문에 실제로 'public abstract void move(int x, int x)'이다. 그래서, 이를 구현하는 Fighter클래스에서는 'void move(int x, int y)'의 접근 제어자를 반드시 public으로 해야 하는 것이다.

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

  • 자바에서는 다중 상속을 허용하지 않는다.
  • 인터페이스는 static상수만 정의할 수 있으므로 조상클래스의 멤버변수와 충돌하는 경우는 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능하다.
  • 그리고 추상메서드는 구현내용이 전혀 없으므로 조상클래스의 메서드와 선언부가 일치하는 경우에는 당연히 조상클래스 쪽의 메서드를 상속받으면 되므로 문제되지 않는다.
  • 다중 상속은 허용되지 않으므로, 한 쪽만 선택하여 상속받고 나머지 한 쪽은 클래스 내에 포함시켜서 내부적으로 인스턴스를 생성해서 사용한다.

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

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

Fightable타입의 참조변수로는 인터페이스 Fightable에 정의된 멤버들만 호출이 가능하다.

  • 따라서 인터페이스는 아래와 같이 메서드의 매개변수의 타입으로 사용될 수 있다.
class Fighter extends Unit implements Fightable{
    public void move(int x, int y) {...};
    public void attack(Fightable f) {...}; 
}
  • 그리고 다음과 같이 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.
Fightable method(){
    ...
    Fighter f = new Fighter();
    return f; // 위두문장은 return new Fighter(); 과 동일
}
  • 리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

7.7 인터페이스의 장점

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

7.8 인터페이스의 이해

  • 인터페이스를 이해하기 위해서는 다음의 두 가지 사항을 반드시 염두에 두고 있어야 한다.
  1. 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
  2. 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다.

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

디폴트 메서드

  • 조상 클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만, 인터페이스의 경우에는 보통 큰 일이 아니다. 인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야하기 때문이다.
  • 인터페이스가 변경되지 않으면 제일 좋겠지만, 아무리 설계를 잘하도 언젠가 변경은 발생하기 마련이다. JDK의 설계자들은 고심 끝에 디폴트 메서드(default method)라는 것을 고안해 내었다.
  • 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 되었다.
// 추상 메서드일 경우
interface MyInterface{
    void method();
    void newMethod(); // 추상 메서드
// 디폴트 메서드
interface MyInterface{
    void method();
    default void newMethod(){};
}

새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우를 해결하기 위한 규칙
1. 여러 인터페이스의 디폴트 메서드 간의 충돌 : 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 해야한다.
2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌 : 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

8. 내부 클래스(inner class)

  • 내부 클래스는 사용빈도가 높지 않으므로 내부 클래스의 기본 원리와 특징을 이해하는 정도까지만 학습해도 충분하다.

8.1 내부 클래스란?

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

    내부 클래스의 장점

    1. 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
    2. 코드의 복잡성을 줄일 수 있다.

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

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

8.5 익명 클래스(anonymous class)

  • 익명 클래스는 특이하게도 다른 내부 클래스들과는 달리 이름이 없다. 클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용될 수 있고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다.
class InnerEx6{
   Object lv = new Object(){void method() {} }; // 익명 클래스
   static Object cv = new Object(){void method() {} }; // 익명 클래스
   
   void myMethod(){
       Object lv = new Object(){ void method(){} }; // 익명 클래스
   }
}
  • 익명 클래스는 이름이 없기 때문에 '외부 클래스명$숫자.class'의 형식으로 클래스파일명이 결정된다.

  • 참고 : 자바의 정석 3판
profile
데이터 분석하고 있습니다

0개의 댓글