본문 바로가기

JAVA

JAVA STUDY - CH7 : 객체지향 프로그래밍 - 2

 ✅ 1. 상속 (inheritance)

상속은 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것.

코드의 재사용성 높히고, 코드의 중복을 제거함.

새로 작성하고자하는 클래스의 이름 뒤에 상속받고자하는 클래스의 이름을 'extends'와 함께 써주면 됨.

💡생성자와 초기화 블럭은 상속되지 않음. 멤버만 상속됨.

 

상속 관계 : '~은 ~이다. (is-a)'

포함 관계 : '~은 ~을 가지고 있다. (has-a)'

class Circle extends Shape{	// Circle과 Shape는 상속관계
	Point center;		// Circle과 Point는 포함관계
    int r;
    ...
}

 

오버라이딩 : 조상 클래스에 정의된 메서드와 같은 메서드를 자손 클래스에 정의하는 것.

 

✔️ 단일 상속 (single inheritance)

C++과 다르게 java는 단일 상속만을 허용함. 클래스 간의 관계가 보다 명확해지고 코드를 더욱 신뢰할 수 있게 해줌.

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

class VCR{
	boolean power;
    int counter = 0;
    void power()	{power =! power;}
    void play(){}
    void stop(){}
    void rew(){}
    void ff(){}
}

class TVCR extends Tv{
	VCR vcr = new VCR();		//VCR클래스를 포함시켜서 사용함.
   	void play(){
    	vcr.play();
    }
    void stop(){
    	vcr.stop();
   	}
    void rew(){
    	vcr.rew();
   	}
    void ff(){
    	vcr.ff();
    }
}

VCR 클래스의 메서드 내용이 변경되더라도 TVCR 클래스의 메서드들이 변경된 내용이 적용되는 결과를 얻을 수 있음.

 

✔️ Object 클래스 - 모든 클래스의 조상

모든 상속계층도의 최상위에는 Object 클래스가 위치함. toString()이나 equals()과 같은 메서드가 정의되어있음.

 

✅ 2. 오버라이딩 (overriding)

조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것.

💡자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와
- 이름이 같아야함
- 매개변수가 같아야함
- 반환타입이 같아야함
= 선언부가 일치해야함

 

다만, 접근 제어자와 예외는 제한된 조건 하에서만 다르게 변경 가능함.

1. 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경 불가함.

2. 예외는 조상 클래스의 메서드보다 많이 선언할 수 없음

3. 인스턴스메서드를 static 메서드로 또는 그 반대로 변경 불가함.

 

✔️ 오버로딩 vs 오버라이딩

오버로딩 : 기존에 없는 새로운 메서드를 정의하는 것

오버라이딩 : 상속받은 메서드의 내용을 변경하는 것

 

✔️ super

super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수.

멤버변수와 지역변수의 이름이 같을 때 : this

상속받은 멤버와 자신의 멤버와 이름이 같을 때 : super

class SuperTest2{
	public static void main(String args[]){
    	Child c = new Child();
        c.method();
   	}
}

class Parent{
	int x = 10;
}

class Child extends Parent{
	int x = 20;
    
    void method(){
    	System.out.println("x=" + x);
        System.out.println("this.x=" + this.x);
        System.out.println("super.x=" + super.x);
   	}
}

// 실행 결과
// x = 20
// this.x = 20
// super.x = 10

 

 

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

super()는 조상 클래스의 생성자를 호출하는데 사용됨. 

Object클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자, this() 또는 super()를 호출해야함. 

 

인스턴스를 생성할 때는 클래스를 선택하는 것 만큼 생성자 선택도 중요함.

 

✅ 3. package와 import

패키지 : 클래스의 묶음. 물리적으로 하나의 디렉토리

import : 소스코드를 작성할 때, 다른 패키지의 클래스를 사용하기 위해.

 

✅ 4. 제어자 (modifier)

접근 제어자 : public, protected, default, private

그 외: static, final, abstract, native, transient, synchronized, volatile, strictfp

 

제어자는 클래스나 멤버변수와 메서드에 주로 사용됨.

 

✔️ static - 클래스의, 공통적인

static이 붙은 멤버변수와 메서드, 그리고 초기화 블럭은 모두 인스턴스를 생성하지 않고도 사용 가능.

인스턴스 멤버를 사용하지 않는다면, static메서드로 선언하는 것을 고려하기. (더 효율적, 속도 빠름)

 

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

final + 클래스 : 변경될 수 없는 클래스. 다른 클래스의 조상이 될 수 없음.

final + 메서드 : 변경될 수 없는 메서드. 오버라이딩을 통해 재정의 될 수 없음.

final + 변수 : 값을 변경할 수 없는 상수.

 

✔️ abstract - 추상의, 미완성의

메서드의 선언부만 작성하고 실제 수행내용은 구현하지않은 추상 메서드를 선언할 때 사용함.

abstract 클래스 자체로는 쓸모가 없을지라도, 다른 클래스가 이 클래스를 상속받아서 일부의 원하는 메서드만 오버라이딩해도 됨.

abstract class AbstractTest{		//추상 클래스 (추상 메서드를 포함한 클래스)
	abstract void move();		//추상 메서드 (구현부가 없는 메서드)
}

 

✔️ 접근 제어자 (access modifier)

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

default : 같은 패키지 내에서만 접근이 가능.

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

public : 접근 제한이 없음.

 

접근 제어자는 클래스의 내부에 선언된 데이터를 보호하기 위해 사용됨. (객체 지향의 캡슐화)

메서드를 변경할 때에도, 오류를 테스트해야 할 범위가 줄어듦.

 

상속을 통해 확장이 될 것으로 예상되는 클래스라면 멤버에 접근 제한을 두되 private 대신 protected를 사용함.

 

✅ 5. 다형성 (polymorphism)

여러 가지 형태를 가질 수 있는 능력. 

조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있음.

클래스 Tv와 CaptionTv는 서로 상속관계에 있음.

CaptionTv c = new CaptionTv();
Tv t = new CaptionTv();

 

이 경우 Tv타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv 클래스의 멤버들만 사용할 수 있음. 따라서, displayCation()과 caption은 참조변수 t로 사용이 불가능함.

 

✔️ 참조변수의 형변환

형변환은 상속관계에 있어야지만 가능함.

 

자손타입 → 조상타입 (Up-casting) : 형변환 생략가능 (= 자손타입의 참조변수를 조상타입의 참조변수에 할당)

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

 

형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기에 인스턴스에 아무런 영향 미치지 않음.

단지, 참조변수의 형변환을 통해서 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위를 조절하는 것임.

 

class CastingTest1{
	public static void main(String args[]){
    	Car car = null;
        FireEngine fe = new FireEngine();
        FireEngine fe2 = null;
        
        fe.water();
        car = fe;
//		car.water();		// 컴파일 에러! Car타입의 참조변수로는 water()를 호출할 수 없음
		fe2 = (FireEngine)car;
        fe2.water();
    }
}

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!!!");
    }
}

 

1. Car car = null;

Car타입의 참조변수 car을 선언하고 null로 초기화함.

 

2. FireEngine fe = new FireEngine();

FireEngine인스턴스를 생성하고 FireEngine 타입의 참조변수(fe)가 참조하도록 함.

 

3. car = fe; // 조상 타입 ← 자손 타입

참조변수 fe가 참조하고 있는 인스턴스를 참조변수 car가 참조하도록 함. fe의 값이 car에 저장됨. 이 때 두 참조변수의 타입이 다르지만, 업캐스팅이므로 생략가능함. 이제는 참조변수 car를 통해서도 FireEngine인스턴스를 사용할 수 있지만, Car타입이므로 Car클래스의 멤버가 아닌 water은 사용이 불가능함.

 

서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유로움. 하지만 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용 안됨.

Fruit fruit = new Apple(); // 업캐스팅
Apple apple = (Apple) fruit; // 다운캐스팅 - OK, 안전!

 

여기서 fruit이 실제로 Apple 이기 때문에 다운캐스팅이 성공함.

Fruit fruit = new Fruit(); // 조상 타입 인스턴스
Apple apple = (Apple) fruit; // 🚫 런타임 에러 (ClassCastException)!

 

여기서는 fruit가 실제로 Fruit이기 때문에 실패함. 조상 Fruit 클래스는 Apple 클래스의 color() 메서드가 없기 때문에 참조하려 하면 ClassCastException이 발생함. 

 

(너무 헷갈림,,,,ㅠ)

✔️ instanceof 연산자

위의 상황을 안전하게 처리하기 위해, 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 사용함.

주로 조건문에 사용되면 boolean 값을 가짐. (true = 검사한 타입으로 형변환이 가능함)

Fruit fruit = new Fruit(); // 조상 타입 인스턴스

if (fruit instanceof Apple) {
    Apple apple = (Apple) fruit; // 안전한 다운캐스팅
    apple.color();
} else {
    System.out.println("fruit는 Apple 타입이 아닙니다!");
}

 

✔️ 참조변수와 인스턴스의 연결

멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우 (= 오버라이딩), 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용됨.

class BindingTest{
	public static void main(String args[]){
    	Parent p = new Child();		// Parent타입 참조변수 p가 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

 

인스턴스 변수에 직접 접근하게 되면, 참조변수의 타입에 따라 사용되는 인스턴스 변수가 달라질 수 있으므로 주의해야함.

따라서, 멤버변수들은 private로 접근을 제한하고, 외부에서는 메서드를 통해서만 멤버변수에 접근할 수 있도록함.

 

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

조상타입의 참조변수로 자손타입의 객체를 참조하는 것이 가능하므로, 매개변수가 조상타입이면 자손타입의 참조변수 어느 것이나 매개변수로 받아들일 수 있음. ( = 공통의 조상을 가진 서로 다른 종류의 객체를 묶을 수 있음)

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);}		//조상클래스의 생성자 Product(int price)를 호출
    
    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;
    
    void buy(Product p) {
    	if(money < p.price){
        	System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
            return;
        }
        
        money -= p.price;
        bonusPoint += p.bonusPoint;
        item[i++] = p;
        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();
     }
}

//Tv을/를 구입하셨습니다.
//Computer을/를 구입하셨습니다.
//Audio을/를 구입하셨습니다.
//구입하신 물품의 총금액은 350만원입니다.
//구입하신 제품은 Tv,Computer,Audio, 입니다.

 

배열의 크기를 10으로 지정했기 때문에 11개 이상의 제품을 구입할 수 없는 문제점 발생.

Vector 클래스를 사용하면 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스의 개수에 신경쓰지 않아도 됨.

메서드 / 생성자 설명
Vector() 10개의 객체를 저장할 수 있는 Vector 인스턴스를 생성함.
10개 이상의 인스턴스가 저장되면, 자동적으로 크기가 증가됨.
boolean add(Object o) Vector에 객체를 추가함. 추가에 성공하면 결과값으로 true, 실패하면 false가 반환됨.
boolean remove(Object o) Vector에 저장되어 있는 객체를 제거함. 제거에 성공하면 결과값으로 true, 실패하면 false가 반환됨.
boolean isEmpty() Vector가 비어있는지 검사함. 비어있으면 true, 비어있지 않으면 false를 반환함.
Object get(int index) 지정된 위치(index)의 객체를 반환함. 반환타입이 Object타입이므로 적절한 타입으로의 형변환 필요.
int size() Vector에 저장된 객체의 개수를 반환함.

 

✅ 추상클래스 (abstract class)

추상 클래스 = 미완성 설계도

추상클래스로는 인스턴스 생성 불가. 상속을 통해서 자손클래스에 의해서만 완성이 가능.

 

추상메서드를 사용하는 이유는 상속받는 자손 클래스에서는 이 메서드들을 무조건 구현해주어야하기 때문.(빼먹지 않기 위함)

부모 클래스를 추상 클래스로 선언한 뒤, 자손 클래스에서 추상메서드를 구현하는 형식으로 코드를 작성함.

 

✅ 인터페이스 (interface)

인터페이스는 일종의 추상 클래스. 다만 추상클래스보다 추상화 정도가 높음.

몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없음.

 

인터페이스 = 기본 설계도 (구현된 것은 아무 것도 없음)

 

class 대신 interface를 작성함.

interface 인터페이스 이름{
	public static final 타입 상수이름 = 값;
    public abstract 메서드이름 (매개변수 목록);
}

 

1. 모든 멤버 변수는 public static final이어야 하며, 생략 가능

2. 모든 메서드는 public abstract이어야 하며, 생략 가능

 

인터페이스는 인터페이스로부터만 상속 가능, 다중 상속 가능.

class FighterTest{
	public static void main(String args[]){
    	Fighter f = new Fighter();
        
        if(f instance of Unit)
        	System.out.println("f는 Unit클래스의 자손입니다.");
      	if(f instance of Fightable)
        	System.out.println("f는 Fightable인터페이스를 구현했습니다.");
        if(f instance of Movable)
        	System.out.println("f는 Movable인터페이스를 구현했습니다.");
        if(f instance of Attackable)
        	System.out.println("f는 Attackble인터페이스를 구현했습니다.");
        if(f instance of Object)
        	System.out.println("f는 Unit클래스의 자손입니다.");
        }
}

class Fighter extends Unit implements Fightable{
	public void move(int x, int y) {}
    public void attack(Unit u){}
}

class Unit{
	int currentHP;
    int x;
    int y;
}

interface Fightable extends Movable,Attackable {}
interface Movable {void move(int x, int y);}
interface Attackable{ void attack(Unit u);}

오버라이딩 할 때는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야함.

따라서 Fighter 클래스에서는 void move의 접근 제어자를 반드시 public으로 해야함.

인터페이스는 다음과 같이 다른 조상 클래스를 상속하는 클래스들이 Eatable하는 기능을 구현하도록 할 수 있음.

여기서, Talkable 기능은 같은 조상 클래스를 상속하고 있으므로

Human 추상 클래스에 말하는 기능을 강제함으로써 공통 기능의 구현을 강제할 수 있음.

 

✔️ 인터페이스의 이해

인터페이스를 이해하기 위해 두 가지 사항을 반드시 염두해두어야함.

💡 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있음.
💡 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 됨.

 

인터페이스를 사용하기 전, 클래스 A와 B는 직접적인 관계임.

class A{
	public void methodA(B b){
    	b.methodB();
    }
}

class B{
	public void methodB(){
    	System.out.println("methodB()");
   	}
}

class InterfaceTest{
	public static void main(String args[]){
    	A a = new A();
        a.methodA(new B());
    }
}

 

클래스 A를 작성하려면 클래스 B가 필요함. 클래스 B의 methodB의 선언부가 변경되면 다른 한쪽도 변경된다는 단점.

인터페이스 사용 후. 'A-I-B'의 간접적인 관계로 바뀜. 클래스 B의 변경에 A가 영향을 받지 않음. 클래스 A는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 됨.

class InterfaceTest3{
	public static void main(String args[]{
    	A a = new A();
        a.methodA();
   	}
}

class A{
	void methodA(){
    	I i = InstanceManager.getInstance();
        i.methodB();
        System.out.println(i.toString());
    }
}

interface I {
	public abstract void methodB();
}

class B implements I{
	public void methodB(){
    	System.out.println("methodB in B class");
    }
    
    public String toString(){ return "class B";}
}

class InstanceManager { 	// 제 3의 클래스의 메서드를 통해서 인터페이스 I를 구현한 클래스
	public static I getInstance(){
    	return new B();		// 다른 인스턴스로 바꾸려면 여기만 변경하면 됨.
    }
}

 

인스턴스를 직접 생성하지 않고, getInstance()라는 메서드를 통해 제공받음. 이렇게 하면 다른 클래스의 인스턴스로 변경되어도 A 클래스를 변경하지 않아도 됨.