본문 바로가기
Java/객체 지향 개념 ~

자바의 정석 (Chapter 7_2. 객체지향개념 2)

by kevinntech 2020. 8. 2.

해당 게시물은 자바의 정석을 정리한 내용 입니다.

 

4.1. 제어자(modifier)란?

 

제어자

 

- 제어자(modifier)는 클래스와 클래스의 멤버(멤버 변수, 메서드)에 부가적인 의미를 부여한다.

 

- 제어자의 종류는 접근 제어자와 외의 제어자로 나눌 수 있다.

 

- 하나의 대상에 여러 개의 제어자를 조합해서 사용할 있으나접근제어자는 하나만 사용할 있다.

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

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

 

- final이 붙은 인스턴스 변수의 경우, 생성자에서 단 한번만 초기화 할 수 있다.

 

- 만약 이것이 불가능하다면 클래스에 선언된 final이 붙은 인스턴스 변수는 모든 인스턴스에서 같은 값을 가져야만 할 것이다.

 

4.4. abstract - 추상의, 미완성의

 

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

 

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

 

추상 클래스는 아직 완성되지 않은 메서드가 존재하는 '미완성 설계도'이므로 인스턴스를 생성할 수 없다.

 

4.5. 접근 제어자(access modifier)

 

접근 제어자

 

- 접근 제어자(access modifier)는 멤버 또는 클래스에 사용되어, 외부로 부터의 접근을 제한한다.

 

 

접근 제어자가 사용될 수 있는 곳 - 클래스, 멤버변수, 메서드, 생성자

 

private : 같은 클래스 내에서만 접근이 가능하다.

② (default) : 같은 패키지 내에서만 접근이 가능하다.

protected : 같은 패키지 내에서, 그리고 다른 패키지의 자손 클래스에서 접근이 가능하다.

public : 접근 제한이 전혀 없다.

 

* 클래스에는 public과 (default)만 사용 가능

   멤버변수와 메서드에는 접근 제어자 4개를 모두 사용 할 수 있다.

 

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

 

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

   

  ① 외부로 부터 데이터를 보호하기 위해서 사용한다.

  ② 외부에는 불 필요한, 내부적으로만 사용되는 부분을 감추기 위해서 사용한다.

 

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

 

  → 접근 제어자를 이용하여 멤버 변수들은 private으로 내부에 감추고 메서드들은 public으로 해서 외부에 노출 시킴으로써

      같은 클래스에 정의된 멤버들끼리는 서로 자유롭게 접근하고 외부에서는 메서드를 통해서만 멤버 변수에 접근 할 수 있는

      구조로 만드는 것

 

public class TimeTest { 
      public static void main(String[] args) 
      { 
            Time t = new Time(12, 35, 30); 
            System.out.println(t); 
//          t.hour = 13;
            t.setHour(t.getHour()+1);   // 현재 시간보다 1 시간 후로 변경한다.
            System.out.println(t);      // System.out.println(t.toString());과 같다.
      } 
}

class Time { 
      private int hour; 
      private int minute; 
      private int second; 

      Time(int hour, int minute, int second) { 
            setHour(hour); 
            setMinute(minute); 
            setSecond(second); 
      } 

      public int getHour() { return hour; } 
      public void setHour(int hour) { 
            if (hour < 0 || hour > 23) return; 
            this.hour = hour; 
      } 
      public int getMinute() { return minute; } 
      public void setMinute(int minute) { 
            if (minute < 0 || minute > 59) return; 
            this.minute = minute; 
      } 
      public int getSecond() { return second; } 
      public void setSecond(int second) { 
            if (second < 0 || second > 59) return; 
            this.second = second; 
      } 
      public String toString() { 
            return hour + ":" + minute + ":" + second; 
      } 
} 

 

* println()로 참조 변수를 출력하는 것은 참조 변수가 가리키는 인스턴스의 toString() 메서드를 호출하는 것과 같다.

 

생성자의 접근 제어자

 

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

 

- 생성자의 접근 제어자를 private으로 지정하면 외부에서 생성자에 접근 할 수 없으므로 인스턴스를 생성할 수 없다.

 

- 대신 인스턴스를 생성해서 반환해주는 public static 메서드를 제공함으로써 외부에서 이 클래스의 인스턴스를 사용하도록

  할 수 있다.

 

- 그리고 생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다.

   왜냐하면, 자손 클래스의 인스턴스를 생성할 때 조상의 생성자를 호출 하는 것이 불가능하기 때문이다.

   그래서 클래스 앞에 final을 더 추가하여 상속 할 수 없는 클래스라는 것을 알리는 것이 좋다.

 

final class Singleton{
    private static Singleton s = new Singleton();

    private Singleton(){
        // ...
    }

    public static Singleton getInstance(){
        if(s == null)
            s = new Singleton();

        return s;
    }

}

public class SingletonTest {
    public static void main(String[] args) {
        //Singleton s = new Singleton(); // 에러 발생
        Singleton s = Singleton.getInstance();
    }
}

 

4.6. 제어자(modifier)의 조합

 

제어자를 조합해서 사용할 때 주의해야 할 사항

 

① 메서드에 static과 abstract를 함께 사용 할 수 없다.

     - static 메서드는 몸통이 있는 메서드에만 사용할 수 있기 때문이다.

 

② 클래스에 abstract와 final을 동시에 사용 할 수 없다.
     - 클래스에 사용되는 final은 클래스를 확장할 수 없다는 의미이고 abstract는 상속을 통해서 
        완성되어야 한다는
의미이므로 서로 모순되기 때문이다.

③ abstract 메서드의 접근 제어자가 private 일 수 없다.
     - abstract 메서드는 자손 클래스에서 구현해주어야 하는데 접근 제어자가 private이면 자손 클래스에서
       접근할 수 없기 때문이다.

④ 메서드에 private과 final을 같이 사용할 필요는 없다.
     - 접근 제어자가 private인 메서드는 오버라이딩 될 수 없기 때문이다.
        이 둘 중 하나만 사용해도 의미가 충분하다.

 

5.1. 다형성이란?

 

다형성(polymorphism)

 

- 객체 지향 개념에서 다형성이란 "여러 가지 형태를 가질 수 있는 능력"을 의미한다.

 

- Java에서 다형성은 조상 클래스 타입의 참조변수로 자손 클래스 타입의 객체를 참조할 수 있는 것이다.

 

   class Tv { 
        boolean power; // 전원 상태 (on/off) 
        int channel; // 채널

        void power() { power = !power; } 
        void channelUp() { ++channel; } 
        void channelDown() { --channel; }
  }

  class CaptionTv extends Tv{
       String text; // 캡션을 보여 주기 위한 문자열
       void caption() { /* 내용 생략 */ }
   }

 

Tv 클래스와 CaptionTv 클래스가 위와 같이 정의되어 있을 , 인스턴스를 같은 타입의 참조 변수로 참조하는 것과

조상 타입의 참조 변수로 참조하는 것의 차이점을 알아 보도록 하자.

 

CaptionTv c = new CaptionTv();

Tv t = new CaptionTv();

 

Tv 타입의 참조변수로는 CaptionTv 인스턴스 중에서 Tv 클래스의 멤버들만 사용할 수 있다.

(Tv 클래스에 정의 되지 않은 멤버는 참조변수 t로는 사용 할 수 없다.)

 

둘다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.

 

- 자손 타입의 참조 변수로 조상 타입의 객체를 가리킬 수 없다.

 

   Tv t = new CaptionTv();        // OK. 허용

   CaptionTV s = new Tv();          // 에러. 허용 안됨

  

5.2. 참조변수의 형변환

참조변수의 형변환

 

- 참조변수의 형 변환은 사용 할 수 있는 멤버의 개수를 조절하는 것이다.

 

- 단, 서로 상속 관계에 있는 타입 간의 형변환만 가능하다.

 

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

 

  자손 타입의 참조변수를 조상 타입으로 형 변환하는 경우에는 형변환을 생략할 수 있다.

  조상 타입의 참조변수를 자손 타입으로 형 변환하는 경우에는 형변환을 생략할 수 없다.

 

class Car {
	String color;
	int door;

	void drive() { 		// 운전하는 기능
		System.out.println("drive, Brrrr~");
	}

	void stop() {		// 멈추는 기능	
		System.out.println("stop!!!");	
	}
}

class FireEngine extends Car {	// 소방차
	void water() {		// 물 뿌리는 기능
		System.out.println("water!!!");
	}
}

class Ambulance extends Car {	// 앰뷸런스
	void siren() {		// 사이렌을 울리는 기능
		System.out.println("water!!!");
	}
}

 

FireEngine 타입의 참조 변수와 Ambulance 타입의 참조 변수 간에는 상속 관계가 아니므로 서로 형 변환이 가능하지 않다.

 

FireEngine f = new FireEngine(); // FireEngine 인스턴스 생성

Car c = (Car) f;                // OK. 조상인 Car 타입으로 형 변환(생략 가능)
FireEngine f2 = (FireEngine) c;	// OK. 자손인 FireEngine 타입으로 형 변환(생략 불가)
Ambulance a = (Ambulance)f; 	// 에러. 상속 관계가 아닌 클래스 간의 형 변환 불가 

 

5.3. instanceof 연산자

instanceof

 

- instanceof 연산자는 참조변수의 형 변환이 가능한지 확인 하는데 사용된다. 가능하면 true를 반환.

 

  주로 조건문에 사용되며, instanceof의 왼쪽에는 참조변수가 위치하며 오른쪽에는 타입(클래스명)이 피연산자로 위치한다.

 

- 형 변환 전에 반드시 instanceof로 확인 해야 한다.

 

void doWork(Car c){
    // 참조변수 c가 가리키는 객체가 FireEngine 타입으로 형 변환이 가능한지 확인
    if(c instanceof FireEngine){          // 1. 형변환이 가능한지 확인                                        
        FireEngine fe = (FireEngine) c;   // 2. 형변환
        fe.water();
    }else if(c instanceof Ambulance){
        Ambulance fe = (Ambulance) c;   // 2. 형변환
        ...   	
    }
}

 

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

 

멤버변수가 중복 정의된 경우, 참조 변수의 타입에 따라 연결되는 멤버 변수가 달라진다. (참조 변수 타입에 영향 받음)

 

메서드가 중복 정의된 경우, 참조 변수의 타입에 관계 없이 항상 실제 인스턴스의 메서드가 호출된다.

 

class BindingTest{
	public static void main(String[] args) {
		Parent p = new Child();
		Child c = new Child();

		System.out.println("p.x = " + p.x);
		p.method();

		System.out.println("c.x = " + c.x);
		c.method();
	}
}

class Parent {
	int x = 100;

	void method() {
		System.out.println("Parent Method");
	}
}

class Child extends Parent {
	int x = 200;

	void method() {
		System.out.println("Child Method");
	}
}

 

 

[실행결과]

 

p.x = 100

Child Method

c.x = 200

Child Method

 

 

아래 내용 부터는 "다형성의 장점"에 관한 설명이다.

① 다형적 매개변수

② 하나의 배열로 여러 종류의 객체를 다루기

 

5.5. 매개변수의 다형성

 

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

 

   즉, 매개 변수의 타입을 공통 조상 타입으로 정의하면 여러 종류의 자손 인스턴스를 넘겨 받을 수 있다.

 

예를 들어, 매개변수의 타입이 Product 클래스 타입이라는 것은 해당 메서드를 호출할 때 Product 인스턴스 또는

 

Product 클래스의 자손 인스턴스를 넘겨받을 수 있다는 것을 의미한다.

 

void buy(Product p) {
    // ...
}

 

방금 살펴본 내용과 비슷한 예시를 보자.

 

class Product {
	int price;			// 제품의 가격
	int bonusPoint;		// 제품구매 시 제공하는 보너스점수

	Product(int price) {
		this.price = price;
		bonusPoint =(int)(price/10.0);	// 보너스점수는 제품가격의 10%
	}
}

class Tv extends Product {
	Tv() {
		// 조상클래스의 생성자 Product(int price)를 호출한다.
		super(100);			// Tv의 가격을 100만원으로 한다.
	}

	public String toString() {	// Object클래스의 toString()을 오버라이딩한다.
		return "Tv";
	}
}

class Computer extends Product {
	Computer() {
		super(200);
	}

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

class Buyer {			// 고객, 물건을 사는 사람
	int money = 1000;	// 소유금액
	int bonusPoint = 0;	// 보너스점수

	void buy(Product p) {
		if(money < p.price) {
			System.out.println("잔액이 부족하여 물건을 살수 없습니다.");
			return;
		}

		money -= p.price;			// 가진 돈에서 구입한 제품의 가격을 뺀다.
		bonusPoint += p.bonusPoint;	// 제품의 보너스 점수를 추가한다.
		System.out.println(p + "을/를 구입하셨습니다.");
	}
}

class PolyArgumentTest {
	public static void main(String args[]) {
		Buyer b = new Buyer();

		b.buy(new Tv());
		b.buy(new Computer());

		System.out.println("현재 남은 돈은 " + b.money + "만원입니다.");
		System.out.println("현재 보너스점수는 " + b.bonusPoint + "점입니다.");
	}
}

 

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

 

조상 타입의 객체 배열에 여러 종류의 자손 인스턴스를 저장할 수 있다.

 

Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();

 

방금 살펴본 내용과 관련된 예시를 보자.

 

class Product {
	int price;          // 제품의 가격
	int bonusPoint;     // 제품구매 시 제공하는 보너스점수

	Product(int price) {
		this.price = price;
		bonusPoint =(int)(price/10.0);
	}

	Product() {} // 기본 생성자
}
 
class Tv extends Product {
	Tv() {
		super(100);	
	}

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

class Computer extends Product {
	Computer() { super(200); }

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

class Audio extends Product {
	Audio() { super(50); }

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

class Buyer {			// 고객, 물건을 사는 사람
	int money = 1000;	// 소유금액
	int bonusPoint = 0;	// 보너스점수
	Product[] item = new Product[10];  // 구입한 제품을 저장하기 위한 배열
	int i =0;                          // Product배열에 사용될 카운터

	void buy(Product p) {
		if(money < p.price) {
			System.out.println("잔액이 부족하여 물건을 살수 없습니다.");
			return;
		}

		money -= p.price;           // 가진 돈에서 구입한 제품의 가격을 뺀다.
		bonusPoint += p.bonusPoint;	// 제품의 보너스 점수를 추가한다.
		item[i++] = p;		        // 제품을 Product[] item에 저장한다.
		System.out.println(p + "을/를 구입하셨습니다.");
	}
            
	void summary() {            // 구매한 물품에 대한 정보를 요약해서 보여 준다.
		int sum = 0;            // 구입한 물품의 가격합계
		String itemList ="";    // 구입한 물품목록

		// 반복문을 이용해서 구입한 물품의 총 가격과 목록을 만든다.
		for(int i=0; i<item.length;i++) {
			if(item[i]==null) break;
			sum += item[i].price;
			itemList += item[i] + ", ";
		}
		System.out.println("구입하신 물품의 총금액은 " + sum + "만원입니다.");
		System.out.println("구입하신 제품은 " + itemList + "입니다.");
	}
}

class PolyArgumentTest2 {
	public static void main(String args[]) {
		Buyer b = new Buyer();

		b.buy(new Tv());
		b.buy(new Computer());
		b.buy(new Audio());
		b.summary();
	}
}

 

 

댓글