책읽기

[Java의 정석][Chapter-7] 객체지향 프로그래밍 2 (2/2)

pythaac 2021. 8. 23. 02:32
이 글은 "Java의 정석 (남궁 성 지음)"을 읽고 주관적으로 요약한 글입니다. 

출처 : http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788994492032#N

 

1. 다형성

1) 다형성

  • 다형성
    • 상속과 함께 객체지향개념의 중요한 특징 중 하나
    • 상속과 깊은 관계
  • 정의
    • 여러 가지 형태를 가질 수 있는 능력
    • 한 타입의 참조 변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현
      - 조상클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수 있음
  • 조건
    • 참조변수의 타입에 따라 사용할 수 있는 멤버가 달라짐
    • 참조변수가 사용할 수 있는 멤버는 인스턴스의 멤버 수보다 같거나 적어야 함
      - 즉, super 클래스만 sub 클래스를 참조 가능

2) 참조변수와 인스턴스의 타입 불일치

  • 참조변수의 형변환
    • 서로 상속관계에 있는 클래스만 사용 가능
    • 자료형은 작은 타입이 큰 타입으로 변경될 경우 생략 가능
      - 자손 타입이 조상 타입으로 변경될 경우 생략 가능 (큰 타입 -> 작은 타입, Up-casting)
      - Car car = new FireEngine() -> 업캐스팅 생략 가능
      - FireEngine fe = (FireEngine) car -> 다운캐스팅 생략 불가
    • 참조변수가 다룰 멤버 수가 인스턴스 멤버 수보다 적으므로 문제가 되지 않음 (생략 가능)
    • 이 때 형변환은 참조변수의 타입만 변하고 인스턴스는 변하지 않음
      - 인스턴스에서 사용할 멤버의 범위(개수)를 조절하는 것
super -> sub sub -> super
sub가 super를 참조 super가 sub을 참조
(FireEngine) Car (Car) FireEngine
멤버수 증가 멤버수 감수
다운캐스팅 생략 불가 업캐스팅 생략 가능
반드시 인스턴스가 FireEngine으로 참조할 수 있는 인스턴스이여야함
(컴파일은 통과하지만 실행시 에러 가능)
 
  • instanceof 연산자
    • 참조변수 타입과 인스턴스 멤버에 따라 에러가 발생할 수 있으므로, 인스턴스의 타입이 중요함
    • instanceof 연산자는 참조변수가 참조하는 인스턴스의 실제 타입을 알아보기 위해 사용
      - 조상 타입 클래스는 모두 True로 반환 (Car, Object)
    • 주로 조건문에 사용
    • 참조변수 instanceof 타입(클래스명) -> boolean 반환
    • True를 반환하면, 검사한 타입으로 형변환해도 문제가 없다는 의미(사용 가능한 멤버 수만 줄어든다)
// 메서드 내에서는 어떤 인스턴스인지 알 수 없음
void doWork(Car c){
	if ( c instanceof FireEngine){
    	...
    } else if ( c instanceof Ambulance ) {
    	...
    }
}
  • 멤버의 중복
    • 조상 클래스와 자손 클래스가 중복된 이름의 멤버가 있을 경우
    • 참조 변수의 타입에 따라 참조하는 대상이 달라짐
    • 단, 참조 변수가 조상 클래스 타입이더라도 인스턴스 메서드의 경우 오버라이딩된 메서드 호출
      - 인스턴스 메서드만 영향을 받지 않음
    • 멤버변수들은 주로 private으로 접근을 제한하고, 외부에서는 메서드를 통해서만 멤버변수에 접근

3) 다형성의 응용

  • 매개변수의 다형성
    • 조상 타입을 클래스로 둘 경우, 자손타입의 참조변수는 모두 매개 변수로 받을 수 있음
    • 매개 변수 타입을 상속받은 자손 타입 클래스를 추가하면, 해당 메서드의 매개 변수로 받아들여짐
// 타입마다 오버로딩해야함
void buy(Computer c){
	money = money - c.price;
    bonusPoint = bonusPoint + c.bonusPoint
}

void buy(Audio a){
	money = money - a.price;
    bonusPoint = bonusPoint + a.bonusPoint
}

// 조상타입 매개변수로 통일
void buy(Product p){
	money = money - p.price;
    bonusPoint = bonusPoint + p.bonusPoint
}
  • 여러 종류의 객체를 배열로 다루기
    • 조상 클래스의 타입 배열로 여러 인스턴스를 생성하여 사용 가능
    • 모든 클래스의 조상인 Object로도 사용이 가능하지만, 구현한 메서드를 사용할 수 없음
Product p[] = new Product[3];
p[0] = new TV();
p[1] = new Computer();
p[2] = new Audio();

 

2. 추상클래스(abstract class)

  • 추상클래스
    • 미완성된 추상 메서드를 포함하는 클래스
    • 인스턴스를 생성할 수 없음 -> 상속을 통해 자손클래스에서 완성시켜야함
    • 새로운 클래스를 작성할 때 어느 정도 틀을 갖춘 상태에서 시작할 수 있음
    • abstract 키워드 사용
      - 추상메서드를 포함하지 않아도 사용 가능하며, 인스턴스를 생성할 수 없게 만들 수 있음
  • 추상메서드
    • 선언부만 작성하고 구현부는 작성하지 않은 메서드
    • 상속받는 클래스에 따라 메서드 내용이 다를 수 있음
    • 자손클래스가 오버라이딩을 통해 추상클래스를 모두 구현해주어야함
      - 구현하지 않은 추상메서드가 있는 경우, 추상클래스로 지정해줘야함
    • 추상메서드의 기능
      - 메서드의 이름, 매개변수, 결과 타입과 같은 선언부만 설계해도 많은 도움이 됨
/* 주석을 통해 어떤 기능을 수행할 목적으로 작성하는지 설명 */
// abstract 리턴타입 메서드이름();

abstract class Player{
	abstract void play(int pos);
    abstract void stop();
}
  • 추상클래스의 작성
    • 추상의 의미
      - 공통된 성질을 뽑아 이를 일반적인 개념으로 파악하는 정신 작용
    • 상속
      - 자손 클래스를 만드는데 조상 클래스를 사용
    • 추상화
      - 기존 클래스의 공통 부분을 뽑아 조상 클래스를 만드는 것
    • 자손 클래스들의 메서드 중 공통된 부분이 있으나 구현내용이 다를 경우 추상메서드로 정의
    • 오버라이딩이 있어도 abstract로 추상메서드를 선언하는 이유는 자손 클래스에서 반드시 구현하도록 강요
  • 추상클래스 작성시 주의사항
    • 추상클래스도 생성자가 있어야함
    • 추상클래스 내에서 추상메서드를 사용할 수 있음

 

3. 인터페이스

  • 인터페이스
    • 일종의 추상클래스
    • 오직 추상메서드와 상수만 멤버로 가질 수 있음
    • 추상클래스처럼 자체로 사용되기보다 다른 클래스 작성에 도움 목적으로 사용
  • 인터페이스 작성
    • class 대신 interface 사용
    • 클래스와 같이 접근제어자로 public 또는 default 사용 가능
    • 모든 멤버변수는 public static final이어야 하며, 생략 가능
    • 모든 메서드는 public abstract이어야 하며, 생략 가능
    • 편의상 생략하는 경우가 많으며, 컴파일러가 자동 추가
    • JDK 1.8에서 static 메서드와 default 메서드 추가를 허용
  • 인터페이스의 상속
    • 인터페이스는 인터페이스로부터만 상속 가능
    • 다중 상속도 가능
    • Object 클래스 같은 최고 조상이 없음
interface Movalbe {
	void move(int x, int y);
}

interface Attackable {
	void attack(Unit u);
}

interface Fightable extends Movable, Attacable { }
  • 인터페이스의 구현
    • 추상클래스와 같이 인터페이스 자체로 인스턴스 생성 불가능
    • 추상메서드의 구현부를 만들어주는 클래스를 작성해야함
    • implements 키워드 사용
    • 인터페이스 메서드의 일부만 구현한 클래스는 abstract를 붙여 추상클래스로 선언해야함
    • 인터페이스의 이름
      - 'able'로 끝나는 이름이 많음
      - 어떠한 기능 / 행위를 하는데 필요한 메서드를 제공한다는 의미
      - '~를 할 수 있는' 능력을 갖추었다는 의미
    • 상속+구현 가능 (extends ... implements ...)
    • 오버라이딩시 조상 메서드보다 넓은 범위의 접근 제어자를 지정해야하므로, 구현 클래스에서 추상메서드의 접근 제어자는 public으로 해야함
// class 클래스이름 implements 인터페이스이름

class fighter implements Fightable {
    public void move(int x, int y) { }
    public void attack(Unit u) { }
}
  • 인터페이스를 이용한 다중상속
    • 두 조상으로부터 상속받는 멤버 중에 멤버변수 이름이 같거나 메서드의 선언부가 일치할 경우
      - 어느 한쪽으로부터 상속을 포기하거나 조상 클래스에서 충돌하는 이름을 변경
    • 다중상속을 허용하지 않는다는 자바의 단점이 부각되어 인터페이스의 다중상속을 허용
    • 인터페이스로 다중상속을 구현하는 경우는 거의 없음
    • 인터페이스는 static 상수만 정의하므로 조상클래스와 충돌이 거의 없고, 충돌할 경우 클래스 이름을 붙여 구분
  • 인터페이스를 이용한 다형성
    • 인터페이스 역시 조상클래스로써 인터페이스 타입 참조변수로 구현한 클래스의 인스턴스 참조 가능
    • 매개변수
      - 인터페이스 타입의 매개변수는 구현 클래스의 인스턴스를 매개변수로 제공함
    • 리턴타입
      - 리턴 타입이 인터페이스라면 구현 클래스의 인스턴스를 반환
    • 클래스의 추가/변경
      - 인터페이스를 사용하면 구현 클래스의 추가나 변경시 수정할 내용이 적음
    • 분산환경 프로그래밍에서 서버측만 변경해도 사용자가 새로 개정된 프로그램을 사용하는 것이 가능
  • 인터페이스의 장점
    1. 개발시간 단축
      - 메서드 호출하는 쪽에서 메서드 내용에 관계없이 선언부만 알면됨
      - 인터페이스를 구현하는 쪽에서 클래스를 작성할 때까지 기다리지 않고 양쪽에서 동시 개발 진행이 가능
    2. 표준화
      - 기본 틀을 인터페이스로 작성하여 일관되고 정형화된 프로그램 개발이 가능
    3. 관계없는 클래스의 관계를 맺어줌
      - 상속관계도 아니고 같은 조상클래스도 갖지 않는 관계없는 클래스들의 관계를 맺어줌
    4. 독립적인 프로그래밍
      - 실제구현에 독립적인 프로그램 작성이 가능
      - 간접적인 관계로 한 클래스의 변경이 다른 클래스에 영향을 미치지 않음
  • 인터페이스 사용 예시
    • 조상 클래스 : Building
    • 자손 클래스 : Academy, Bunker, Barrack, Factory
    • Barrack, Factory 객체만 건물을 이동시킬 수 있는 새로운 메서드를 추가하고 싶을 때
    • Litable 인터페이스를 정의, Barrack, Factory 클래스에서 구현
  • 인터페이스의 이해
    • 클래스를 사용하는 User와 제공하는 Provider가 있음
    • 메서드를 사용(호출)하는 User는 사용하려는 메서드(Provider)의 선언부만 알면 됨
    • 예를 들어, Provider 클래스를 사용하는 User 클래스가 있음
      - Provider 클래스가 작성되어 있어야 User 클래스가 사용이 가능
      - Provider 클래스와 직접적인 관계로, 메서드 선언부가 변경되면 User 클래스에서도 변경해야함
    • 그러나 만약 Provider 클래스를 직접 호출하지 않고 인터페이스를 매개로 한다면
      - Provider 클래스와 간접적인 관계가 되어, 클래스 이름을 몰라도, 실제로 구현되지 않아도 됨
      - Provider 클래스가 아닌 같은 기능의 다른 클래스로 대체되어도 됨
    • InstanceManager의 사용
      - getInstance 메서드로 인터페이스 타입을 반환하면, 반환하는 클래스만 바꿔 인스턴스 타입을 바꿀 수 있음
  • 인터페이스의 static 메서드
    • JDK1.8부터 디폴트 메서드와 static 메서드도 추가 가능
    • 그러나 인터페이스의 모든 메서드는 추상 메서드여야한다는 규칙이 그대로 적용
    • 인터페이스와 관련된 static 메서드는 별도의 클래스에 따로 두어야 함
      - 대표적으로 java.util.Collection 인터페이스의 static 메서드는 Collections 클래스에 들어감
  • 인터페이스의 디폴드 메서드
    • 조상클래스에 새로운 메서드 추가는 별 일 아님
    • 그러나 인터페이스의 경우 추상 메서드이므로, 모든 구현 클래스들이 구현해줘야함
    • 이러한 문제로 디폴드 메서드 추가가 가능하며, 구현 클래스들을 변경하지 않아도 됨
    • 디폴드 메서드명 충돌시
      - 여러 인터페이스를 구현한 경우, 구현 클래서에서 해당 메서드를 구현해야함
      - 디폴드 메서드와 조상 클래스의 메서드 충돌시, 조상 클래스의 메서드가 상속

 

4. 내부 클래스 (inner class)

  • 내부 클래스
    • 클래스 내에 선언된다는 점을 제외하고 일반 클래스와 같음
    • 클래스와 내부 클래스가 서로 긴밀한 관계가 있기 때문에 사용
    • 다른 클래스에서는 잘 사용되지 않아야 함
    • 클래스 파일이 생성됨
  • 내부 클래스의 장점
    • 클래스 멤버에 쉽게 접근 가능
    • 코드의 복잡성을 줄임(캡슐화)
  • 내부 클래스의 종류
    • 선언 위치에 따라 종류가 나뉨 (변수와 동일)
    • 인스턴스 클래스
      - 외부 클래스의 멤버변수 선언 위치에 선언, 인스턴스 멤버와 관련 작업에 사용
    • static 클래스
      - 외부 클래스의 멤버변수 선언 위치에 선언, static 메서드에서 사용
    • 지역 클래스
      - 외부 클래스의 메서드/초기화블럭 안에 선언, 선언 영역 내부에서만 사용
    • 익명 클래스
      - 클래스 선언과 객체의 생성을 동시에 하는 이름없는 일회용 클래스
  • 내부 클래스의 접근 범위
    • 외부 클래스의 private도 접근 가능
    • static 클래스는 인스턴스 멤버에 접근할 수 없음
    • 지역 클래스는 포함된 메서드의 지역변수까지 접근 가능하지만, final이 붙은 변수만 접근 가능
      - 이유는 메서드 수행이 끝나 지역변수가 소멸되도, 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문
  • 익명 클래스(anonymous class)
    • 단 한번 사용, 단 하나의 객체만 생성하는 일회용 클래스
    • 클래스 이름이 없으며, 선언과 동시에 객체 생성
      - 조상클래스/인터페이스의 이름을 사용하여 정의
    • 생성자가 없음