09-1 중첩 클래스와 중첩 인터페이스 소개

중첩 클래스(nested class)란 클래스 내부에 선언한 클래스를 말한다.
중첩 인터페이스(nested interface)란 "클래스" 내부에 선언한 인터페이스를 말한다.

 

중첩 클래스를 사용하면 두 클래스의 멤버들을 서로 쉽게 접근할 수 있고, 외부에는 불필요한 관계 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점이 있다.

 

인터페이스를 클래스 안에 선언할 수도 있는데 이를 중첩 인터페이스라고 한다. 이유는 해당 클래스와 긴밀한 관계를 맺는 구현 클래스를 만들기 위해서 이다.


중첩 클래스

선언되는 위치에 따라 두가지로 분류된다.

  • 멤버클래스 : 클래스의 멤버로 선언되는 중첩클래스, 객체가 사용 중이라면 언제든 사용 가능
  • 로컬클래스 : 생성자 또는 메소드 내부에서 선언되는 중첩클래스, 메소드를 실행할 때만 사용되고 종료되면 없어짐
선언 위치에 따른 분류 선언위치 설명
멤버클래스 인스턴스
멤버 클래스
class A {
   class B {...}
}
A객체를 생성해야만 사용할 수 있는 B클래스
정적
멤버 클래스
class A {
   static class B
}
A클래스로 바로 접근할 수 있는 B클래스
로컬클래스 class A {
   void method() {
      class B {...}
   }
}
method() 실행때만 사용할 수 있는 B 클래스

 

중첩클래스도 하나의 클래스라 컴파일하면 바이트 코드 파일(.class)로 생성되지만 이름이 일반적인 클래스와 다르다

A $ B .class  // 멤버클래스 경우 A: 바깥 클래스, B: 멤버클래스
A $1 B .class // 로컬클래스 경우 A: 바깥 클래스, B: 로컬클래스

 

 

  • 인스턴스 멤버 클래스

static 키워드 없이 중첩 선언된 클래스.

인스턴스 클래스는 인스턴스 필드와 인스턴스 메소드만 선언이 가능하고 정적 필드와 정적 메소드는 선언할 수 없다.

 

- 생성 방법 

▶ A안에 있는 B클래스의 객체를 바깥 클래스 A 밖에서 생성하려면, A객체를 생성하고 B객체를 생성해야 한다.

▶ A안에 있는 B클래스의 객체를 A안에서 생성하는 것은 일반 객체 생성과 동일하다. 

 

A a = new A();
A.B b = a.new B();
b.field = 3;
b.method1();

 

 

  • 정적 멤버 클래스

static 키워드로 중첩 선언된 클래스.

정적 멤버 클래스는 인스턴스와 정적 상관없이 모든 종류의 필드와 메소드를 선언할 수 있다.

 

- 생성 방법

▶외부에서 정적 멤버 클래스의 객체를 생성할 때는 바깥 객체를 생성할 필요없이 객체를 생성하면 된다.

A.C c = new A.C();
c.field1 = 3; 	// 인스턴스 필드 사용
c.method1();	// 인스턴스 메소드 호출
A.C.field2 = 3;	// 정적 필드 사용
A.C.method2();	// 정적 메소드 호출

 

 

  • 로컬 클래스

메소드 내에서 선언하는 중첩 클래스.

로컬 클래스는 접근 제한자(public, private) 및 static을 붙일 수 없다. 또한 로컬 클래스 내부에는 인스턴스 필드와 인스턴스 메소드만 선언할 수 있다.

로컬 클래스는 메소드가 실행 될 때 메소드 내에서 객체를 생성하고 사용해야 한다. 

 

 

 

 


중첩 클래스의 접근 제한

 

멤버 클래스 내부에서 바깥 클래스의 필드와 메소드에 접근할 때는 제한이 따른다.

또 메소드의 매개 변수나 로컬 변수를 로컬 클래스에서 사용할 때도 제한이 따른다.

 

  • 바깥 필드와 메소드에서 인스턴스 멤버 클래스를 사용할 때 제한
  인스턴스 정적
필드 초기값  O X
메소드 내 객체 O X

 

  • 멤버 클래스에서 바깥 필드와 메소드를 사용할 때 사용제한

① 인스턴스 멤버 클래스 

  인스턴스 정적
필드 접근 O O
메소드 접근 O O

 

② 정적 멤버 클래스

  인스턴스 정적
필드 접근 X O
메소드 접근 X O

 

 

  • 로컬 클래스에서 메소드의 매개 변수나 로컬 변수를 사용할 때 제한

로컬 클래스의 객체는 메소드 실행이 종료되면 없어지는 게 일반적이지만, 메소드가 종료되어도 실행 상태로 존재 하는 경우도 있다. (ex. 로컬 스레드 객체)

자바는 이런 문제를 해결하기 위해 컴파일 시 로컬 클래스에서 사용하는 매개 변수나 로컬 번수의 값을 로컬 클래스 내부에 복사해두고 사용한다. 그리고 매개 변수나 로컬 변수가 수정되면 복사해둔 값과 달라지는 문제가 생겨 매개 변수나 로컬 변수에 final 속성을 부여하게 된다. 

 

 

  • 중첩 클래스에서 바깥 클래스 참조 얻기

클래스 내부에서 this는 객체 자신의 참조한다.

중첩 클래스에서 this 키워드를 사용하면 바깥 클래스가 아닌 중첩 클래스 자신을 객체 잠조한다.

따라서 this.필드, this.메소드()로 호출하면 중첩 클래스의 필드와 메소드를 사용할 수 있다.

 

바깥 클래스의 객체 참조를 얻으려면 아래 처럼 사용하면 된다.

바깥 클래스.this.필드;
바깥 클래스.this.method();

 

 

 


중첩 인터페이스

클래스의 멤버로 선언된 인터페이스를 말한다. 

해당 클래스와 긴밀한 관계를 맺는 구현 클래스를 만들기 위해서 선언하는 것으로 인스턴스 멤버 인터페이스와 정적 멤버 인터페이스 모두 가능하다.

 

인스턴스 멤버 인터페이스는 바깥 클래스의 객체가 있어야 사용 가능하고, 정적 멤버 인터페이스는 바깥 클래스 객체 없이 바로 접근 할 수 있다. 

 

주로 정적 멤버 인터페이스를 많이 사용하는데 UI프로그래밍에서 이벤트 처리 목적으로 많이 활용된다.  

 

 

08-2 타입 변환과 다형성

 

인터페이스에도 메소드 재정의와 타입 변환 기능을 제공해 다형성을 구현하는데에 상속과 더불어 많이 사용된다.

- 상속은 같은 종류의 하위 클래스 생성

- 인터페이스는 사용 방법이 동일한 클래스를 만드는 기술

이라는 개념상 차이가 있으나 구현 방법은 비슷하다 

(공부할때 상속과 헷갈릴 수 있다.)

 

프로그램 소스 코드는 변함이 없는데, 구현 객체를 교체함으로써 프로그램의 실행 결과가 다양해진다.
이것이 인터페이스의 다향성이다. 

자동 타입 변환

자동 타입 변환은 프로그램 실행 중에 자동적으로 타입이 변환 되는 것을 말하며

구현 객체가 인터페이스 타입으로 변환 되는 것은 자동 타입 변환(Promotion)에 해당한다. 

인터페이스 변수 = 구현 객체;

 

인터페이스 구현 클래스를 상속하여 자식 클래스를 만들었다면 자식 객체 또한 인터페이스 타입으로 변환이 가능하다.

이런 다양한 객체를 넣을 수 있는 인터페이스 변수를 필드와 매개체로 사용 한다면 다양한 실행결과를 만들 수 있다.

 

 


필드의 다형성

필드 값을 대입할 때 자동 타입 변환이 일어나며 다형성을 보여준다.

 

상속에서의 예시에서는 타이어 클래스 타입에 한국 타이어와 금호 타이어라는 자식 객체로 대입해 교체할 수 있음을 보여줬다.

하지만 인터페이스에서의 타이어는 인터페이스 타입으로 한국 타이어와 금호 타이어는 구현 클래스에 해당하여, 타이어 인터페이스를 구현했다. 따라서 타이어  인터페이스에 있는 메소드를 가지고 있어 타이어 인터페이스로 동일하게 사용할 수 있는 교체 가능한 객체가 된다는 것이 다르다.

 

  • 예시 (클래스때 예시가 유사한데 다른 부분을 찾으며 공부하면 좋다)
인터페이스 Tire roll() 추상메소드
구현 클래스 1 HankookTire roll() 
구현 클래스 2 KumhoTire roll() 

 

Class Car{

// 일반 클래스에서는 이런식으로 사용했으나 인터페이스는 그 자체로 객체 생성이 불가능하다
//    Tire frontLeftTire = new Tire();
//    Tire frontRightTire = new Tire();
//    Tire backLeftTire = new Tire();
//    Tire backRightTire = new Tire();

// 인터페이스는 구현 객체를 대입함으로써 사용이 가능하다
   Tire frontLeftTire = new HankookTire();
   Tire frontRightTire = new HankookTire();
   Tire backLeftTire = new KumhoTire();
   Tire backRightTire = new KumhoTire();

    void run(){
        frontLeftTire.roll();
        frontRightTire.roll();
        backLeftTire.roll();
        backRightTire.roll();
    }
}

 

이중 앞왼쪽 바퀴와 뒤오른쪽 바퀴를 각각 다른 타이어로 교체를 하면

인터페이스 타입인 타이어에 구현 객체인 한국타이어와 금호타이어가 들어가게 된다.

Car myCar = new Car();
myCar.frontLeftTire = new HankookTire();
myCar.backRightTire = new KumhoTire();
myCar.run();

객체 생성후 초기값으로 대입한 구현 객체 대신 다른 구현 객체도 대입이 가능하다. 한국타이어와 금호 타이어 모두 인터페이스의 추상메소드를 구현하고 있기 때문이다. 따라서 Car객체는 타이어 인터페이스에 선언된 메소드만 사용하면 되므로  어떤 객체가 저장되더라도 수정할 필요가 없다.

 

Car객체가 호출한 인터페이스 메소드에 각 저장된 구현 객체의 메소드를 호출하므로 인터페이스 변수로 선언된 필드는 저장된 객체에 따른 다형성을 보여줄 수 있게 된다. 

 

 


매개 변수의 다형성

메소드 호출할 때에도 자동타입 변환이 일어난다. 

상속에서는 매개변수를 부모 타입으로 선언하고 호출할 때 자식 객체를 대입했다면 

이번에는 매개변수를 인터페이스 타입으로 선언하고 호출할 때는 구현 객체를 대입한다.

 

매개변수의 타입이 인터페이스라면 이를 구현한 객체라면 어떠한 것도 매개값으로 사용할 수 있고, 어떤 객체냐에 따라 메소드의 실행 결과가 다양해 질 수 있다. 이를 매개 변수의 다형성이라고 한다.

 

예시

더보기

인터페이스

public interface Vehicle{
	public void run();
}

매개 변수를 인터페이스 타입으로 준 메소드

public class Driver{
	public void drive(Vehicle vehicle){
    	vehicle.run();
    }
}

구현 클래스 1

public class Bus implements Vehicle{
	@Override
    public void run(){
    	System.out.println("버스가 달립니다.");
    }
}

구현 클래스 2

public class Taxi implements Vehicle{
	@Override
    public void run(){
    	System.out.println("택시가 달립니다.");
    }
}

매개변수 다형성 테스트 해볼 실행 클래스

public class DriverExample{
	public static void main(String[] args){
		Driver driver = new Driver();
        
        Bus bus = new Bus();
        Taxi taxi = new Taxi();
        
        driver.drive(bus);	// 자동 타입 변환 Vehicle vehicle = bus;
        driver.drive(taxi);	// 자동 타입 변환 Vehicle vehicle = taxi;
    }
}
// 버스가 달립니다.
// 택시가 달립니다.

 

 

 


강제 타입 변환

구현객체가 인터페이스 타입으로 자동 변환되면 인터페이스에 선언된 메소드만 사용 가능하다는 제약이 생긴다.

구현 클래스에 메소드가 A,B,C,D,E 5개 있고 인터페이스에 선언된 메소드가 A,B,C 3개라면 자동 타입 변환시 호출 가능한 건 A,B,C 3개 뿐인 것이다. 

 

하지만 때에 따라서는 인터페이스에 없는 구현 클래스의 나머지 메소드, 필드도 사용해야할 경우가 있다. 이때 강제 타입 변환(Casting)을 해서 다시 구현 클래스 타입으로 변환 후 구현 클래스의 필드와 메소드를 사용할 수 있다.

구현클래스 변수 = (구현클래스) 인터페이스변수;

 

+상속과 비슷

 

 


객체 타입 확인

강제 타입 변환은 구현 객체가 인터페이스 타입으로 변환되어 있는 상태에서 가능하다. 

그렇지 않은 경우를 강제 타입 변환하면 ClassCastException이 발생한다. 

 

그렇다면 어떤 구현 객체가 인터페이스 타입으로 변환되었는지 어떻게 확인할 수 있을까

상속에서와 마찬가지로 instanceof 연산자를 이용하면 된다.

 

좌항에 인터페이스 타입, 우항에 구현 클래스를 두어 좌항이 우항에서 변한 것인 지를 확인 할 수 있다.

맞다면 true를 반환하고, 틀리다면 false를 반환한다.  

if(vehicle instanceof Bus){
	Bus bus = (Bus) vehicle;
}

이렇게 어떤 객체인지 확인을 하고 안전하게 강제 타입 변환을 해야 한다. 

 

 


인터페이스 상속

 

인터페이스도 다른 인터페이스를 상속할 수 있다. 그리고 일반 클래스와 달리 다중 상속이 가능하다. 

public interface 하위인터페이스 extends 상위인터페이스1, 상위인터페이스2, 상위인터페이스3...{...}

 

하위 인터페이스를 구현하는 클래스는 하위는 물론 상위의 모든 추상 메소드에 대한 실체 메소드를 가지고 있어야 한다. 그리고 이렇게 다 가지고 있기 때문에 아래와 같이 자동 변환이 가능하다. 

하위인터페이스 변수 = new 구현클래스();
상위인터페이스1 변수 = new 구현클래스();
상위인터페이스2 변수 = new 구현클래스();
상위인터페이스3 변수 = new 구현클래스();
..

 

하지만 하위인터페이스 변수로 변환되면 하위는 하위를 포함해 상위에 선언된 모든 메소드들을 사용할 수 있으나

만약 상위인터페이스1 변수로 변환되면 하위의 메소드들은 물론 상위2, 상위3에 선언된 메소드들을 사용할 수 없고 오로지 상위1의 메소드만 사용할 수 있다. 

08-1 인터페이스

 

인터페이스는 개발 코드와 객체가 서로 통신하는 접점 역할을 한다. 
개발코드가 인터페이스의 메소드를 호출하면 인터페이스는 객체의 메소드를 호출한다. 

 

왜 개발코드가 직접 객체의 메소드를 호출하지 않고 중간에 인터페이스를 둘까?

이유는 개발코드를 수정하지 않고 사용하는 객체를 변경할 수 있도록 하기 위해서 이다. 인터페이스는 여러 객체들과 사용이 가능하므로 어떤 객체를 사용하느냐에 따라 실행 내용과 리턴값이 달라질 수 있는데 개발코드 입장에서는 수정없이 실행 내용과 리턴값을 다양화할 수 있다는 장점이 있다. 

 

 


인터페이스 선언

인터페이스는 ~.java로 작성되고 컴파일러를 통해 ~.class 로 컴파일되어 물리적 형태는 클래스와 동일하나, 소스를 작성할 때 선언하는 방법이 다르다.

선언부에 interface 키워드를 추가하여 선언한다. 

[public] interface 인터페이스이름 {...}

인터페이스 이름은 영어 대소문자를 구분하며, 첫글자 대문자에 나머지는 소문자로 작성하는 것이 관례이다. 

+ 인터페이스의 public 접근 제한자는 다른 패키지에서도 인터페이스를 사용할 수 있도록 한다.

 

 

 

  • 인터페이스 구성

클래스와 달리 인터페이스는 상수필드와 추상메소드만을 구성 멤버로 가지며, 객체를 생성할 수 없기 때문에 생성자를 가질 수 없다.

interface 인터페이스이름 {
    //상수
    타입 상수이름 = 값;
    
    //추상메소드
    타입 메소드이름(매개변서,...);
}

 

 

 

  • 상수 필드 선언

인터페이스는 객체 사용 방법을 정의한 것으로 데이터를 저장할 수 있는 인스턴스 또는 정적 필드를 선언할 수 없다.

단, 상수 필드는 선언이 가능하다.

[public static final] 타입 상수이름 = 값;

상수를 선언하는 public static final을 앞에 붙이지 않아도 인터페이스에 선언딘 필드는 모두 public static final 특성이 붙어 생략하더라도 컴파일 과정에서 자동으로 붙는다. 그리고 인터페이스 상수는 선언과 동시에 초기값을 지정해야한다. (생성자로 넣는 방법이 불가능하기 때문)

 

상수 이름은 대문자로 작성, 서로 다른 단어로 구성되어 있으면 _로 연결하는 것이 관례이다.

 

 

 

  • 추상 메소드 선언
[public abstract] 리턴타입 메소드이름(매개변수, ...);

인터페이스를 통해 호출된 메소드는 최종적으로 객체에서 실행된다. 그래서 인터페이스 메소드는 실행 블록이 없는 추상 메소드로 선언한다. 인터페이스의 메소드는 모두 public abstract 특성을 붙어 이를 생략해도 컴파일 과정에서 자동으로 붙는다.

 

 

 

 


인터페이스 구현 (implement)

인터페이스에는 실행 블록이 없는 추상 메소드만 가지고 있기 때문에 객체는 인터페이스가 정의한 추상 메소드와 동일한 선언부를 가진 실체 메소드를 가지고 있어야 한다. 이러한 객체를 인터페이스의 구현(implement) 객체라 하고, 구현 객체를 생성하는 클래스를 구현 클래스라 한다. 

 

  • 구현 클래스

보통의 클래스와 동일한데, 인터페이스 타입으로 사용할 수 있음을 알리기 위해 클래스 선언부에 implements 키워드를 추가한다. 

public class 구현클래스이름 implements 인터페이스이름 {
	// 인터페이스에 선언된 추상 메소드의 실체 메소드 선언
}

+ 인터페이스 모든 메소드들은 기본적으로 public 접근 제한을 갖기에 구현 클래스 역시 public 보다 낮은 접근 제한을 작성할 수 없다.

 

구현 클래스가 작성되면 new 연산자로 객체를 생성할 수 있지만 일반적인 클래스처럼 클래스 변수에 대입하는 것으로는 인터페이스를 사용할 수 없다. 클래스 변수가 아닌 인터페이스 변수를 선언하고 대입해야 한다. 

인터페이스 변수;
변수 = 구현 객체;

// or

인터페이스 변수 = 구현 객체;

 

 

  • 다중 인터페이스 구현 클래스

객체는 다수의 인터페이스를 implements 선언하여 다수의 인터페이스의 타입으로 사용할 수 있다. 다만, 인터페이스 각각의 추상 메소드를 한 객체 안에 모두 구현해야 한다. 

public class 구현클래스이름 implements A인터페이스이름, B인터페이스이름 {
    // 인터페이스 A에 선언된 추상 메소드의 실체 메소드 선언
    // 인터페이스 B에 선언된 추상 메소드의 실체 메소드 선언
}

그리고 둘 모두 구현하고 있어 각각 인터페이스 변수에 대입이 가능하다.

A인터페이스 변수 = 구현객체;
B인터페이스 변수 = 구현객체;

 

 

  • 인터페이스 사용

인터페이스는 클래스를 선언할 때 다음과 같은 용도들로 선언될 수 있다. 

- 필드

- 생성자의 매개변수

- 메소드의 매개변수

- 생성자의 로컬 변수

- 메소드의 로컬 변수

 

  • 인터페이스 사용 예시

인터페이스 RemoteControl.java

더보기
public interface RemoteControl {
    // public static final 키워드 자동 붙음
    public int MAX_VOLUME = 10;
    public int MIN_VOLUME = 0;
    
    // public abstract 키워드 자동 붙음
    public void turnOn();
    public void turnOff();
    public void setVolume(int volume);
}

 

구현클래스1 Television.java

더보기
public class Television implements RemoteControl {
	private int volume;
    
    public void turnOn(){
    	System.out.println("TV를 켭니다.");
    }
    
    public void turnOff(){
    	System.out.println("TV를 끕니다.");
    }
    
    public void setVolume(int volume){
		if(volume > RemoteControl.MAX_VOLUME){
        	this.volume = RemoteControl.MAX_VOLUME;
        } else if (volume < RemoteControl.MIN_VOLUME){
        	this.volume = RemoteControl.MIN_VOLUME;
        } else {
        	this.volume = volume;
        }
        System.out.println("현재 TV 볼륨 : " + this.volume);
	}
}

 

구현클래스2 Audio.java

더보기
public class Audio implements RemoteControl {
	private int volume;
    
    public void turnOn(){
    	System.out.println("Audio를 켭니다.");
    }
    
    public void turnOff(){
    	System.out.println("Audio를 끕니다.");
    }
    
    public void setVolume(int volume){
		if(volume > RemoteControl.MAX_VOLUME){
        	this.volume = RemoteControl.MAX_VOLUME;
        } else if (volume < RemoteControl.MIN_VOLUME){
        	this.volume = RemoteControl.MIN_VOLUME;
        } else {
        	this.volume = volume;
        }
        System.out.println("현재 Audio 볼륨 : " + this.volume);
	}
}

 

인터페이스 사용 MyClass.java

더보기
public class MyClass {
    //필드로 사용
    RemoteControl rc = new Television();

    MyClass(){
    }
    
    //생성자 매개변수로 사용
    MyClass(RemoteControl rc){
    	this.rc = rc
        rc.turnOn();
        rc.setVolume(5);
    }
    
    //메소드의 로컬변수로 사용
    void methodA (){
    	RemoteControl rc = new Audio();
    	rc.turnOn();
        rc.setVolume(5);
    }
    
    //메소드의 매개변수로 사용
    void methodB (RemoteControl rc){
    	rc.turnOn();
        rc.setVolume(5);
    }
}

 

인터페이스 사용 MyClassExample.java

더보기
public class MyClassExample{
    public static void main(String[] args){
    
    	// 기본 생성자로 생성되어 클래스 선언때 선언한 tv객체를 참조한 필드를 사용
        MyClass myClass1 = new MyClass(); 
        myClass1.rc.turnOn();		// TV를 켭니다.
        myClass1.rc.setVolume(4);	// 현재 TV 볼륨 : 4
        
        // 매개값있는 생성자로 생성되어 매개값으로 던진 Audio객체를 사용, 생성자 안의 메소드까지 실행
        MyClass myClass2 = new MyClass(new Audio()); 
       	// Audio를 켭니다.
        // 현재 Audio 볼륨 : 5
        
        // 클래스 안의 methodA 메소드를 실행
        MyClass myClass3 = new MyClass();
        myClass3.methodA();
       	// Audio를 켭니다.
        // 현재 Audio 볼륨 : 5
        
        // 클래스 안의 methodB 메소드를 실행
        MyClass myClass4 = new MyClass();
        myClass4.methodB(new Television);
       	// TV를 켭니다.
        // 현재 TV 볼륨 : 5
    }
}

 

 


추상 클래스랑 인터페이스랑 뭐가 다르지?

 

추상클래스는 extend의 단어 뜻대로 자신의 기능을 하위로 확장 시키는 개념에 가깝다고 한다면
인터페이스는 implement 단어 뜻대로 인터페이스에 정의된 메서드를 각 클래스의 목적 혹은 성능에 맞게 동일한 기능으로 구현하는 것으로 볼 수 있다. 

 

추상 클래스는 extends 키워드를 사용하며 다중 상속이 불가능
인터페이스는 implements 키워드를 사용하며 다중 상속이 가능하다.

 

정리하자면 자바의 특성상 한 개의 클래스만 상속이 가능하기 때문에 해당 클래스의 구분을 추상 클래스 상속을 통해서 해결하고, 할 수 있는 공통된 기능들을 인터페이스의 다중 상속을 통해 구현하는 식으로 사용

 

+ 추상화 정도는 일반 메서드 일반 멤버 변수, 생성자를 가질수 없는 인터페이스가 더 높다

 

 

 

 

 

 

자바 추상 클래스와 인터페이스의 차이점 이해하기

'is a kind of(~의 한 종류) 추상 클래스'와 'be able to(~할 수 있는) 인터페이스' - 추상 클래스 public abstract class 클래스이름 { ... public abstract void 메서드이름(); } class 앞에 'abstract' 키워드를 사용하여 정

wildeveloperetrain.tistory.com

 

 

 

07-3 추상클래스

 

사전적 의미로 추상(abstract)는 실체 간에 공통되는 특성을 추출한 것을 말한다. 

객체를 직접 생성할 수 있는 클래스를 실체 클래스라고 한다면 이 클래스의 공통적인 특성을 추출해서 선언한 클래스를 추상 클래스라고 한다. 

 

추상 클래스와 실체 클래스는 상속의 관계를 가지고 있기에 실체 클래스는 추상 클래스의 모든 특성을 물려받고, 추가적인 특성(필드, 메소드)을 가질 수 있다.

 

 


추상 클래스의 용도

  • 공통된 필드와 메소드의 이름을 통일할 목적

여러 사람이 설계하는 경우 실체 클래스마다 필드와 메소드가 제각기 다른 이름을 가질 수 있는데 이를 통일할 목적으로 사용된다.

 

  • 실체 클래스를 작성할 때 시간 절약

공통적인 필드와 메소드는 추상클래스에 선언하고 다른 점만 실체 클래스에 선언하므로 실체 클래스를 작성하는데 시간을 절약할 수 있다.

 

이런 이유들 때문에 설계자는 어플리케이션 내에서 공통으로 사용하는 필드와 미소드를 추려내어 추상 클래스로 설계 규격을 만드는 것이 좋다. 

 

 


추상 클래스 선언

추상 클래스를 선언할 때에는 클래스 선언에 abstract 키워드가 붙어야한다. 

abstract가 붙으면 new연산자로 객체를 생성하지 못하고 오로지 상속을 통해서 자식 클래스만 만들 수 있다.

public abstract class 클래스이름 {
    //필드
    //생성자
    //메소드
}

 

비록 new연산자로 객체를 생성할 수 없지만 자식 객체에서 super(), super.메소드명() 등으로 호출해 객체를 생성 필드와 메소드를 사용할 수 있게 되므로 추상 클래스 역시 생성자를 반드시 작성해줘야 한다.

 

 


추상 메소드와 재정의

추상 클래스를 사용하는 여러 케이스 중에 메소드의 선언만 통일하고, 실행 내용은 실체 클래스마다 달라야 하는 경우가 존재한다. 이런 경우를 위해 추상 클래스는 추상 메소드를 선언할 수 있다. 

추상 메소드는 abstract 키워드와 함께 메소드의 선언부만 있고 메소드 실행 블록인 {}가 없는 메소드를 말한다.

[public | protected] abstract 리턴타입 메소드이름(매개변수, ... );

 

추상 클래스 설계시 실체 클래스가 반드시 실행 내용을 채우도록 강제하고 싶은 메소드가 있다면 추상 메소드를 선언한다.

하위인 실체 클래스가 이를 재정의 하지 않으면 컴파일 에러가 발생한다.

 

 

07-1 / 타입 변환과 다형성

이전에 기본 타입의 변환에 대해 공부했다면 클래스에서도 자동 타입 변환이 있고 강제변환이 있다. 

재정의(오버라이딩)과 타입변환을 이용하면 객체 지향 프로그래밍의 매우 중요한 특징인 다형성을 구현할 수 있다. 

 

  • 다형성 정의 

사용 방법은 동일하나 다양한 객체를 이용해서 다양한 실행 결과가 나오도록 하는 성질

ex) 자동차가 타이어 사용하는 방법은 동일 - 어떤 타이어를 사용하느냐에 따라 변화

 


자동 타입 변환

클래스의 변환은 상속 관계에 있는 클래스 사이에서 발생하는데 그 중 자동 타입변환은 프로그램 실행 도중에 자동적으로 타입변환이 일어나는 것을 말한다. 

  • 자식에서 부모로? 
부모타입 변수 = 자식타입;

자식은 부모의 특징과 기능을 상속 받기 때문에 부모와 동일하게 취급 될 수 있다. 그러면 자식변수를 대입한 부모타입 변수는 부모 객체를 참조하고 있을까?

메모리 적으로 봤을 때 자식 타입을 대입한 부모 변수를 부모 객체를 참조하는 것이 아니라 자식 객체를 참조하고 있다

Cat cat = new Cat();
Animal animal = cat;

위에 두 변수 cat과 animal은 모두 동일하게 Cat객체를 참조하고 있는 것이다.

또한 바로 위의 부모가 아니더라도 상속 계층 상의 상위 타입이라면 하위 타입들이 자동 타입 변환이 일어날 수 있다. 

 

  • 부모 타입으로 자동 타입 변환 후 

부모 타입으로 자동 변환 되었다면 그때 부터는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다. 위에서 자식 객체를 참조하고 있다고 했지만 접근은 부모 클래스 멤버로만 한정되어 버리는 것이다.

하지만 이것에 대한 예외가 있는데, 메소드가 자식 클래스에서 재정의 되었다면 부모 클래스가 아닌 자식 클래스의 메소드가 호출된다. 

 

 

 


필드의 다형성

자동 타입 변환이 왜 필요할까? 다형성을 구현하기 위해서다.

 

필드 타입을 부모 타입으로 선언하면 이 필드 안에 다양한 자식 객체들을 수용할 수 있다. 그럼 이 부모타입 필드 하나로 다양한 사용결과가 나올 수 있게 된다. 이게 바로 필드의 다향성이다.

 

  • 다향성 구현의 기술적 조건 3가지 

- 자식클래스는 부모가 가지고 있는 필드와 메소드를 가지고 있어 사용 방법이 동일하다.

- 자식클래스는 부모의 메소드를 재정의해 더 우수한 실행결과가 나올수 있다.

- 자식타입은 부모타입으로 변환할 수 있다. 

 

  • 예시
부모 클래스 Tire roll()
자식 클래스 1 HankookTire roll() 오버라이딩
자식 클래스 2 KumhoTire roll() 오버라이딩

차 한대에 기본으로 Tire가 4개를 사용한다. 

Class Car{
    Tire frontLeftTire = new Tire();
    Tire frontRightTire = new Tire();
    Tire backLeftTire = new Tire();
    Tire backRightTire = new Tire();
    
    void run(){
        frontLeftTire.roll();
        frontRightTire.roll();
        backLeftTire.roll();
        backRightTire.roll();
    }
}

이중 앞왼쪽 바퀴와 뒤오른쪽 바퀴를 각각 자식 클래스인 한국타이어와 금호타이어로 교체를 하면

부모인 타이어에 자식 클래스인 한국타이어와 금호타이어가 들어가게 된다.

Car myCar = new Car();
myCar.frontLeftTire = new HankookTire();
myCar.backRightTire = new KumhoTire();
myCar.run();

또한 run메소드를 통해 돌아가는 roll메소드 또한 자식 클래스에 오버라이딩 되어 있는 메소드가 있어 한국타이어와 금호타이어를 낀 부분은 부모와 다른 자식 메소드를 호출해 다른 성능을 내게 된다. 

 

 

 


매개 변수의 다형성

자동 타입 변환은 메소드를 호출할 때도 많이 발생한다.

메소드를 호출할때 매개 변수와 동일한 매개값을 지정하는 것이 정석이지만, 매개값을 다양화하기 위해 매개 변수에 자식 객체를 지정할 수도 있다.

이를 통해 매개 변수가 클래스 타입으로 지정되어 있는 경우, 해당 클래스의 객체 뿐만 아니라 자식 객체까지고 매개값으로 사용할 수 있음을 알게된다. 따라서 부모 클래스 타입으로 지정해놓고 어떤 자식 객체를 사용하느냐에 따라 실행결과가 달라진다. 자식 객체가 부모의 메소드를 오버라이딩 했다면 메소드 내부에서 자식이 오버라이딩한 메소드를 호출함으로써 메소드의 실행결과가 달라지는 것이다. 

부모 클래스 Vehicle run()
자식 클래스 1 Bus run() 오버라이딩
자식 클래스 2 Taxi run() 오버라이딩

부모클래스

public class Vehicle {
	public void run(){
    	System.out.println("차량이 달립니다.");
    }
}

자식클래스 (예시하나만)

public class Bus extends Vehicle {
	@Override
    public void run(){
    	System.out.println("버스가 달립니다");
    }
}

Vehicle을 이용하는 클래스

public class Driver{
    public void drive(Vehicle vehicle){
        vehicle.run();
    }
}

실행 클래스

public class DriverExample{
	public static void main(String[] args){
    	Driver driver  = new Driver();
        
        Vehicle vehicle = new Vehicle();
        Bus bus = new Bus();
        Taxi taxi = new Taxi();
        
        driver.drive(vehicle); 
        driver.drive(bus);	// 자동변환 자식메소드인 bus의 run실행
        driver.drive(taxi);	// 자동변환 자식메소드인 taxi의 run실행
    }
}
// 차량이 달립니다
// 버스가 달립니다
// 택시가 달립니다

 

 

 


강제 타입 변환

자동 타입 변환이 자식이 부모 타입이 되는 것이라면, 강제 타입 변환은 캐스팅 연산자를 사용하여 부모 타입을 자식 타입으로 변환하는 것을 말한다. 

모든 부모가 자식 타입으로 강제 변환 될 수 있는 것은 아니다. 자식 타입에서 부모타입으로 자동 변환이 되어 부모타입이 된 것을 다시 자식 타입으로 돌릴때 강제 타입 변환을 사용할 수 있다

자식타입 변수 = (자식타입) 부모타입;
Parent parent = new Child();	// 자동 타입 변환
Child child = (Child) parent;	// 강제 타입 변환

자식 타입이 부모 타입으로 바뀌면 부모의 메소드와 필드에만 접근할 수 있는데 이를 다시 자식 타입으로 변환하면 자식의 멤버를 사용할 수 있게 된다. 

 

 

 


객체 타입 확인

처음부터 부모타입으로 태어난 객체는 강제 타입 변환이 불가능하다. 그럼 태초에 부모타입의 변수가 부모 객체인지 자식 객체인지 확인 할 수 있는 방법이 있을까?

 

instanceof 연산자를 사용하면 된다. 

boolean result = 좌항(객체) instanceof 우항(타입)

instanceof를 기준으로 좌항의 객체가 우항의 타입의 인스턴스이면 true를 리턴하고 그렇지 않으면 false를 리턴한다.

 

이 연산자는 주로 매개값의 타입을 조사할 때 사용되며, 메소드 내에서 강제 타입 변환이 필요할 때는 이 연산자를 통해 매개값이 어떤 객체인지 확인하고 안전하게 강제 타입 변환을 해야한다. 

if(parent instanceof Child){
	Child child = (child) parent;
}

 

 

 

+ Recent posts