[Java] 자바의 정석 7장 (4) - 캡슐화, 다형성

토닉·2021년 9월 24일
0

Java

목록 보기
13/13
post-thumbnail

📌 캡슐화

캡슐화의 가장 중요한 개념은 데이터의 보호입니다.
자바에서는 접근 제어자를 통해 외부로부터의 데이터 접근을 제어하는 방법을 사용하고 있습니다.

접근 제어자를 사용하는 이유

  • 외부로부터 데이터를 보호하기 위해서
  • 외부에는 불필요한, 내부적으로만 사용되는 부분을 감추기 위해서
// time.java
package time;

public class Time{
    // 외부 접근 허용
    public int hour;
    public int minute;
    public int second;
}

// main.java
package main;

import time.Time;

public class Main{
    public static void main(String[] args){
        Time t = new Time();
        t.hour = 3; // 멤버변수에 직접 접근 가능(접근 제한 x)
    }

}

Main 클래스의 main메서드에서 time 클래스의 멤버변수에 접근하고 있습니다. 하지만 이렇게 작성하면 문제가 있습니다.

  • hour는 0~23까지만 넣을 수 있는데 타입이 int이기 때문에 0~23이 아닌 어떤 숫자를 넣어도 초기화가 가능해집니다.
// time.java
package time;

public class Time{
    // 외부 접근을 막는다.
    private int hour;
    private int minute;
    private int second;

    public void setHour(int hour){
        if (hour < 0 || hour > 23) { return; }
        this.hour = hour;
    }

    public int getHour(){
        return hour;
    }
}

// main.java
package main;

import time.Time;

public class Main{
    public static void main(String[] args){
        Time t = new Time();
        t.setHour(3); // public 메서드를 통해 private 멤버변수 초기화
        System.out.println(t.getHour());
    }

}

위 코드는 Time 클래스의 멤버변수에 직접 접근할 수 없고 메서드를 통해 클래스의 멤버변수를 초기화해야 합니다.
이 때 메서드에서는 초기화를 할 때 조건을 넣을 수 있기 때문에 잘못된 데이터를 넣지 못하게 할 수 있습니다.
이처럼 외부의 접근에 대해 데이터를 보호하는 것을 캡슐화 라고 합니다.
접근 제어자의 범위는 최대한 좁히는게 좋습니다!

📌 다형성

  • 다형성 매우매우매우 중요!!
  • 여러 가지 형태를 가질 수 있는 능력
  • 이해하지 못하면 추상클래스, 인터페이스를 배울 수 없다.

정의
사전적: 여러 가지 형태를 가질 수 있는 능력

  • 조상 타입 참조 변수로 자손 타입 객체를 다루는 것

클래스 예제

class Tv {
    boolean power;
    int channel;
    
    void power() { power = !power; }
    void channelUp() { ++channel; }
    void channelDown() { --channel; }
}

class SmartTv extends Tv {
    String text;
    void caption() {}
}

이 클래스로 인스턴스를 만들기 위해 다음과 같이 작성했습니다.

  • Tv tv1 = new Tv();
  • SmartTv smartTv1 = new SmartTv();

참조 변수의 클래스와 인스턴스 생성의 클래스는 똑같이 하는 것이 일반적이었지만 아래와 같은 코드도 가능합니다.

  • Tv t = new SmartTv();

위같은 상황을 조상 타입의 참조 변수로 자손 타입 객체를 다루는 것입니다.

객체와 참조변수의 타입이 일치할 때와 일치하지 않을 때의 차이는?

SmartTv s = new SmartTv();
Tv t = new SmartTv();

// 자손 타입의 참조 변수로 조상 타입의 객체를 가리킬 수 없다!!(단 이미 생성된 객체를 자손 타입의 참조 변수로 형변환은 가능)
SmartTv s = new Tv(); // 에러..
  • s 는 SmartTv라는 참조 변수이기 때문에 SmartTv의 모든 멤버에 접근이 가능합니다.
  • 반면 t는 Tv라는 참조 변수이기 때문에 Tv 멤버만 접근이 가능하고 그 외에 SmartTv의 멤버(text, caption())은 접근이 불가합니다.
public static void main(String[] args){
        SmartTv s = new SmartTv();
        Tv t = new SmartTv();
				// SmartTv, Tv 공통 멤버
        s.channel = 10;
        t.channel = 20;

        // text는 SmartTv만의 멤버
        s.text = "SmartTv";
        t.text = "Tv"; // 에러.. 참조변수의 타입은 Tv이기 때문에 text 멤버가 없다.
    }

Q. 참조변수가 조상타입일 때와 자손타입일 때의 차이?

참조변수로 접근할 수 있는 멤버의 개수가 달라집니다.

Q. 자손 타입의 참조변수로 조상 타입의 객체를 가리킬 수 있나요?

아니요 자손 타입의 멤버가 많거나 같기 때문에 조상 타입 객체에 없는 멤버를 가리킬 수 없어 에러가 발생합니다. 단 선언할 때가 아닌 아미 생성된 객체를 가리키는 것은 가능합니다. 이를 참조변수의 형변환이라고 부릅니다.

📍 참조변수의 형변환

결론

사용할 수 있는 멤버의 개수를 조절하는 것

  • 조상, 자손 관계의 참조변수는 서로 형변환 가능
  • 조상인 타입으로 형변환할 때는 생략이 가능하지만 무조건 작성해주면 다 된다.
  • 이 때 형제관계는 형변환을 할 수 없다.
package main;

public class Main{
    public static void main(String[] args){
        Child c = new Child(10, 20); // Child 객체 생성
        Parent p = (Parent) c; // 자손 객체의 참조변수 -> 조상 참조변수 (형변환)
        Child c2 = (Child) p; // 조상 객체의 참조변수 -> 자손 참조변수 (형변환)
        System.out.println("p = " + p);
        System.out.println("c = " + c);
        System.out.println("c2 = " + c2);
        System.out.println("p.parentInt = " + p.parentInt);
        System.out.println("c.parentInt = " + c.parentInt);
        System.out.println("c2.parentInt = " + c2.parentInt);
        // p 참조변수는 Parent이기 때문에 Child로 형변환 해주어야 한다.
        System.out.println("p.childInt = " + ((Child) p).childInt);
        System.out.println("c.childInt = " + c.childInt);
        System.out.println("c2.childInt = " + c2.childInt);
    }
}

class Parent {
    int parentInt;

    Parent(int a){
        parentInt = a;
    }
}

class Child extends Parent{
    int childInt;

    Child(int a, int b){
        super(a);
        childInt = b;
    }
}

// 출력
p = main.Child@28d93b30
c = main.Child@28d93b30
c2 = main.Child@28d93b30
p.parentInt = 10
c.parentInt = 10
c2.parentInt = 10
p.childInt = 20
c.childInt = 20
c2.childInt = 20

인스턴스가 없을 때도 형변환이 가능한가요?

package main;

public class Main{
    public static void main(String[] args){
        Parent p1 = null;
        Child c1 = null;

        Child c2 = (Child) p1; // 조상 -> 자손 으로 형변환
        Parent p2 = (Parent) c1; // 자손 -> 조상 으로 형변환

        System.out.println(c2.parentInt); // NullPointerException 발생
        System.out.println(p2.parentInt); // NullPointerException 발생

    }
}

인스턴스(객체)가 없기 때문에 참조변수의 값은 null이기 때문에 NullPointerException이 발생합니다.

형변환은 조상과 자손관계에선 마음껏 할 수 있지만 만약 참조변수가 접근할 수 있는 멤버의 수가 실제 객체(인스턴스)보다 많다면 안됩니다!

조상 인스턴스를 자손 참조변수로 형변환에서 사용했을 때 조상에는 없고 자손에 있는 멤버에 접근하려고 하면 어떻게 되나요?

public class Main{
    public static void main(String[] args){
        Parent p1 = new Parent(10);
        Child c1 = (Child) p1;
        System.out.println(c1.childInt); // 컴파일은 가능 .. 하지만 실행하면 ClassCastException 에러 발생

    }
}

컴파일 시에는 가능하지만 실행하면 ClassCastException 에러가 발생합니다. 이렇기 때문에 형변환을 할 때는 해당 인스턴스의 멤버의 개수보다 큰 참조변수를 사용하는 것을 자제해야 합니다.

⭐️ instanceof 연산자

  • 참조변수의 형변환 가능여부 확인에 사용, 가능하면 true 반환
  • 형변환 전에 반드시 instanceof 로 확인해야 한다!
public class Main{
    public static void main(String[] args){
        Child c1 = new Child(10,20);
        System.out.println(c1 instanceof Parent); // true
        System.out.println(c1 instanceof Object); // true
        System.out.println(c1 instanceof Child); // true
    }
}

instaceof는 조상, 자기 자신을 대상으로 true값을 반환합니다. 형제 관계는 컴파일에서 에러가 발생합니다.

Q. 참조변수의 형변환은 왜 하나요?
참조변수를 변경함으로써 사용할 수 있는 멤버의 개수를 조절하기 위해서

Q. 참조변수를 형변환하면 객체(인스턴스)도 바뀌나요?
바뀌지 않습니다. 객체의 주소, 멤버들도 그대로 유지됩니다.

Q. instanceof 연산자는 언제 사용하나요?
참조변수를 형변환하기 전에 형변환이 가능한지 확인할 때 사용합니다.

⭐️ 다형성의 장점

매개 변수의 다형성

  • 참조형 매개변수는 메서드 호출시, 자신과 같은 타입 또는 자손타입의 인스턴스를 넘겨줄 수 있다!

예제

package main;

public class Main{
    public static void main(String[] args){
        Tv t = new Tv(1500000); // Tv 인스턴스 생성
        Computer c = new Computer(1000000); // Computer 인스턴스 생성
        Buyer b = new Buyer(1400000); // 구매자 인스턴스 생성
        b.buy(t); // 구매자 메서드 호출(Tv, Computer 둘 다 매개변수로 받을 수 있다)
        b.buy(c);
    }
}
// 상품
class Product {
    int price;
    int bonusPoint;

    Product(int price){
        this.price = price;
        this.bonusPoint = (int) (price / 10.0);
    }
}
// Tv, 상품 상속
class Tv extends Product {
    int channel;

    Tv(int price) {
        super(price);
    }

    public String toString(){
        return "Tv";
    }

}
// Computer, 상품 상속
class Computer extends Product {
    Computer(int price) { super(price); }

    public String toString(){
        return "Computer";
    }
}
// 구매자
class Buyer {
    int money;
    int bonusPoint = 0;

    Buyer(int money) { this.money = money; }

    void buy(Product p){
        System.out.println(p);
        if (money < p.price){
            System.out.println("돈이 부족합니다.");
        }else {
            money -= p.price;
            this.bonusPoint += p.bonusPoint;
            System.out.println("구매 금액: " + p.price);
            System.out.println("남은 금액: " + money);
            System.out.println("누적 포인트: " + this.bonusPoint);
        }
    }
}

⭐️ 하나의 배열로 여러 종류 객체 다루기

  • 조상타입의 배열에 자손들의 객체를 담을 수 있다.
public class Main{
    public static void main(String[] args){
        Buyer b = new Buyer(3000000);
        b.buy(new Computer(500000));
        b.buy(new Tv(1000000));
        b.cartList();

    }
}

class Product {
    // 생략
}

class Tv extends Product {
    // 생략

}

class Computer extends Product {
    // 생략
}

class Buyer {
    int money;
    int bonusPoint = 0;
// cart의 최대 길이 상수값으로 선언
    final int CART_MAX = 10;
// cart 리스트와 담기 위한 변수 i 선언
    int i = 0;
    Product cart[] = new Product[CART_MAX];

    Buyer(int money) { this.money = money; }

    void buy(Product p){
        System.out.println(p);
        if (money < p.price){
            System.out.println("돈이 부족합니다.");
        }else if (i > CART_MAX - 1){
            System.out.println("카트가 가득 찼습니다.");
        }
        else {
	// 카트 리스트에 참조변수를 담고 리스트 인덱스 +1
            cart[i++] = p;
            money -= p.price;
            this.bonusPoint += p.bonusPoint;
            System.out.println("구매 금액: " + p.price);
            System.out.println("남은 금액: " + money);
            System.out.println("누적 포인트: " + this.bonusPoint);
        }
    }

    void cartList(){
        for (int i = 0; i < CART_MAX; i++){
	// 만약 카트에 참조변수값이 없다면 반복문 탈출!
            if (cart[i] == null){
                break;
            }
            System.out.print(cart[i] + " ");
        }
    }
}

Buyer 클래스 안에 cart라는 Product 참조변수 배열이 있습니다.
이 배열에는 Product 참조변수가 들어갈 수 있기 때문에 Product의 자손 참조변수가 들어갈 수 있습니다.
이를 활용하여 cart라는 배열에는 Computer, Tv 참조변수가 들어가 여러 종류의 객체를 담을 수 있게 됩니다.
(단, 자손들의 객체를 담아야 합니다.)

profile
우아한테크코스 4기 교육생

0개의 댓글