[Java] Java의 정석 | Chapter 07 객체지향 프로그래밍 Ⅱ

숙취엔 꿀물.·2023년 12월 5일

Java

목록 보기
7/13
post-thumbnail

👉 1. 상속(inheritance)

1.1 상속의 정의와 장점

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

  • 상속을 통해서 클래스를 작성하면 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있고,
  • 코드를 공통적으로 관리할 수 있기 때문에 코드의 추가 및 변경이 매우 용이하다.

-> 코드의 재사용성을 높이고 코드의 중복을 제거하여 프로그램의 상산성과 유지보수에 크게 기여한다 !

class Parent { }

class Child extends Parent {
	// ...
}

새로 작성하는 클래스의 이름 뒤에 상속받고자 하는 클래스의 이름을 키워드 extends 와 함께 쓰면됨.

조상 클래스 - 부모(parent)클래스, 상위(super)클래스, 기반(base)클래스
자손 클래스 - 자식(child)클래스, 하위(sub)클래스, 파생된(derived)클래스

자손 클래스는 조상 클래스의 모든 멤버를 상속 받으므로 항상 조상 클래스보다 같거나 많은 멤버를 갖는다. 즉, 상속에 상속을 거듭할수록 상속받는 클래스의 멤버 개수는 점점 늘어나게 된다.

- 생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
- 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.
class Parent {
	int age;
}
class Child extends Parent { }
class Child2 extends Parent { }
class GrandChild extends Child { }
  • 조상 클래스만 변경해도 모든 자손 클래스에 영향을 미침
  • 따라서, 전체 프로그램을 구성하는 클래스들을 면밀히 설계 분석하여, 클래스간의 상속관계를 적절히 맺어주는 것이 객체지향 프로그래밍에서 가장 중요한 부분이다.
class Tv{
    boolean power;  // 저원상태(on/off)
    int channel;    // 채널
    
    void power() { power = !power; }
    void channelUp() { ++channel; }
    void channelDown() { --channel; }
}

class CaptionTv extends Tv{
    boolean caption;    // 캡션상태(on/off)
    void displayCaption(String text){
        if(caption){    // 캡션 상태가 on(true)일 때만 text를 보여 준다.
            System.out.println(text);
        }
    }
}

public class CaptionTvTest {
    public static void main(String[] args) {
        CaptionTv ctv = new CaptionTv();
        ctv.channel = 10;                   // 조상 클래스로부터 상속받은 멤버
        ctv.channelUp();                    // 조상 클래스로부터 상속받은 멤버
        
        System.out.println(ctv.channel);
        
        ctv.displayCaption("Hello, World");
        ctv.caption = true;     // 캡션(자막) 기능을 켠다.
        ctv.displayCaption("Hello, World");
    }
}
자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.

1.2 클래스간의 관계 - 포함관계

  • 상속이외에도 클래스를 재사용하는 또 다른 방법이 있는데, 그것은 클래스간에 포함(Composite) 관계를 맺어 주는 것이다.
  • 클래스 간의 포함관계를 맺어 주는 것은 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것을 뜻한다.
class Circle {
	int x, y, r;	// 원점의 x, y 좌표 및 반지름(radius)
}

class Point {
	int x, y;	// x, y 좌표
}

// 위와 같이 있을 때 Circle 클래스를 다음과 같이 할 수 있다는 뜻
class Circle {
	Point c = new Point();	// 원점
    int r;
}

1.3 클래스간의 관계 결정하기

~은 ~이다 (is-a)~은 ~을 가지고 있다(has-a) 를 넣어서 문장을 만들어보면 클래스 간의 관계가 보다 명확해 진다.

원(Circle)은 점(Point)이다. - Circle is a Point.
원(Circle)은 점(Point)을 가지고 있다. - Circle has a Point.

두 번째 문장이 더 옳다는 것을 알 수 있다.
  • ~은 ~이다. 라는 문장이 성립하면 상속관계
  • ~은 ~을 가지고 있다. 는 문장이 성립한다면 포함관계

1.4 단일 상속(single inheritance)

C++에서는 여러 조상 클래스로부터 상속받는 것이 가능한 '다중상속(multiple inheritance)'을 허용하지만 자바에서는 오직 단일 상속 만을 허용한다.

class TVCR extends TV, VCR {	// 에러. 조상은 하나만 허용된다.
	// ...
}

static메서드라면 메서드 이름 앞에 클래스의 이름을 붙여서 구별할 수 있지만, 인스턴스 메서드의 경우 선어부가 같은 두 메서드를 구별할 수 있는 방법이 없기 때문.


1.5 Object 클래스 - 모든 클래스의 조상

  • Object 클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스이다.
  • 다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object 클래스로부터 상속받게 함으로써 이것을 가능하게 한다.
class Tv {
	// ...
}

// 위와 같이 정의하면 자동으로 아래와 같이 됨
class Tv extends Object {
	// ...
}

만일 다른 클래스로부터 상속을 받더라도 상속계층도를 따라 조상클래스를 찾아 올라가다 보면 결국 마지막 최상위 조상은 Object 클래스 일 것이다.

  • 보통 toString() 이나 equals(Object o) 와 같은 메서드를 사용할 수 있었던 이유이기도 하다.


👉 2. 오버라이딩(overriding)

2.1 오버라이딩이란?

조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩 이라고 한다.

class Point {
	int x, y;
    
    String getLocation() {
    	return "x :" + x + ", y :" + y;
    }
}

class Point3D extends Point {
	int z;
    
    String getLocation() {		// 오버라이딩
    	return "x :" + x + ", y :" + y + ", z :" + z;
    }
}

2.2 오버라이딩의 조건

자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와

  • 이름이 같아야 한다.
  • 매개변수가 같아야 한다.
  • 반환타입이 같아야 한다.

한마디로 선언부가 서로 일치해야 한다는 것이다. 다만 접근 제어자(access modifier)와 예외(exception)는 제한된 조건 하에서만 변경할 수 있다.

조상 클래스의 메서드를 자손 클래스에서 오버라이딩 할 때
1. 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
2. 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
3. 인스턴스메서드르르 static메서드로 또는 그 반대로 변경할 수 없다.

2.3 오버로딩 vs. 오버라이딩

오버로딩(overloading) - 기존에 없는 새로운 메서드를 정의하는 것(new)
오버라이딩(overriding) - 상속받은 메서드의 내용을 변경하는 것(change, modify)

class Parent {
	void parentMethod() {}
}

class Child extends Parent {
	void parentMethod() {}		// 오버라이딩
    void parentMethod(int i) {}	// 오버로딩
    
    void childMethod() {}
    void childMethod(int i) {} // 오버로딩
}

2.4 super

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

  • 멤버변수와 지역변수의 이름이 같을 때 this를 붙여서 구별했듯이 상속받은 멤버와 자신의 멤버의 이름이 같을 때는 super를 붙여서 구별할 수 있다.

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

class Parent {
	int x = 10;
}

class Child extends Parent {
	void method() {
    	System.out.println("x=" + x);
        System.out.println("this.x=" + this.x);
        System.out.println("super.x=" + super.x);
    }	// 이 경우 x, this.x, super.x 모두 같은 변수를 의미하므로 모두 같은 값 출력
}
class SuperTest {
	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);
    }
}
// 이 경우는 20, 20, 10을 출력한다.

이처럼 조상 클래스에 선언된 멤버변수와 같은 이름의 멤버변수를 자손 클래스에서 중복해서 정의하는 것이 가능하며, 참조변수 super를 이용해서 서로 구별할 수 있다.


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

  • this() 는 같은 클래스의 다른 생성자를 호출하는 데 사용되지만, super() 는 조상 클래스의 생성자를 호출하는데 사용된다.

  • 생성자의 첫 줄에서 조상클래스의 생성자를 호출해야 한다.

  • 그 이유는 자손 클래스의 멤버가 조상 클래스의 멤버를 사용할 수도 있으므로 조상의 멤버들이 먼저 초기화되어 있어야 하기 때문이다.

    	Object 클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자, this() 또는 super()를 호출해야 한다. 그렇지 않으면 컴파일러가 자동적으로 'super();'를 생성자의 첫줄에 삽입한다.
class PointTest {
	public static void maint(String args[]) {
    	Point3D p3 = new Point3D(1,2,3);
    }
}

class Point {
	int x, y;
    
    Point(int x, int y) {
    	this.x = x;
        this.y = y;
    }
    
    String getLocation() {
    	return "x :" + x + ", y :" + y;
    }
}

class Point3D extends Point {
	int z;
    
    Point3D(int x, int y, int z) {
    	// 생성자 첫 줄에서 다른 생성자를 호출하지 않기 때문에 컴파일러가 'super();'를 여기에 삽입한다.
        // super()는 Point3D의 조상인 Point 클래스의 기본 생성자인 Point()를 의미한다.
        // Point()가 정의되어 있지 않으므로 Point(int x, int y)를 호출하도록 super(x,y);를 삽입 해야함
        this.x = x;
        this.y = y;
        this.z = z;
    }
    
    String getLocation() {		// 오버라이딩
    	return "x :" + x + ", y :" + y + ", z :" + z;
    }
}

조상 클래스의 멤버변수는 이처럼 조상의 생성자에 의해 초기화되도록 해야 하는 것이다.



👉 3. package 와 import

3.1 패키지(package)

  • 패키지란, 클래스의 묶음이다.

  • 패키지에는 클래스 또는 인터페이스를 포함시킬 수 있으며, 서로 관련된 클래스들끼리 그룹 단위로 묶어 놓음으로써 클래스를 효율적으로 관리할 수 있다.

    클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리이다.
    ex) String 클래스 -> java.lang.String

    - 하나의 소스파일에는 첫 번째 문장으로 단 한 번의 패키지 선언만을 허용한다.
    - 모든 클래스는 반드시 하나의 패키지에 속해야 한다.
    - 패키지는 점(.)을 구분자로 하여 계층구조로 구성할 수 있다.
    - 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.

3.2 패키지의 선언

package 패키지명;
  • 클래스패스 부분은 패스

3.3 import 문

import 문의 역할은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하는 것


3.4 import 문의 선언

일반적인 소스파일(*.java)의 구성은 다음의 순서로 되어 있다.
1. package문
2. import문
3. 클래스 선언
  • 클래스 이름을 지정해주는 대신 * 을 사용하면, 컴파일러는 해당 패키지에서 일치하는 이름을 찾아야 하는 수고를 더 해야 한다. 단지 그 뿐이다. 실행 시 성능상의 차이는 전혀 없다.

3.5 static import 문

  • static import 문을 사용하면 static 멤버를 호출할 때 클래스 이름을 생략할 수 있다.
  • 특정 클래스의 static 멤버를 자주 사용할 때 편리하다.
import static java.lang.Integer.*;		// Integer 클래스의 모든 static 메서드
import static java.lang.Math.random;	// Math.random()만. 괄호 안붙임.
import static java.lang.System.out;		// System.out을 out만으로 참조가능

다음과 같이 쓸 수 있다는 말이다.

System.out.println(Math.random()); <-> out.println(random());
  • 이런건 처음 알았는데 나름 유용한 기능이지 않나 싶다.. 자바 공부 다시 하길 잘한 것 같다.


👉 4. 제어자(modifier)

4.1 제어자란?

제어자(modifier)는 클래스, 변수 또는 메서드의 선언부와 함께 사용되어 부가적인 의미를 부여한다.

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

제어자는 클래스나 멤버변수와 메서드에 주로 사용되며, 하나의 대상에 대해서 여러 제어자를 조합하여 사용하는 것이 가능하다. 단, 접근 제어자는 한 번에 하나만.


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

static이 사용될 수 있는 곳 - 멤버변수, 메서드, 초기화 블럭
제어자대상의미
- 모든 인스턴스에 공통적으로 사용되는 클래스변수가 된다.
멤버변수- 클래스변수는 인스턴스를 생성하지 않고도 사용 가능하다.
static- 클래스가 메모리에 로드될 때 생성된다.
메서드- 인스턴스를 생성하지 않고도 호출이 가능한 static 메서드가 된다.
- static 메서드 내에서는 인스턴스멤버들을 직접 사용할 수 없다.
class StaticTest {
	static int width = 200;			// 클래스 변수(static변수)
    static int height = 120;		// 클래스 변수(static변수)
    
    static {						// 클래스 초기화 블럭
    	// static 변수의 복잡한 초기화 수행
    }
    
    static int max(int a, int b) {	// 클래스 메서드(static 메서드)
    	return a > b ? a : b;
    }
}

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

final이 사용될 수 있는 곳 - 클래스, 메서드, 멤버변수, 지역변수
제어자대상의미
클래스변경될 수 없는 클래스, 확장될 수 없는 클래스가 된다.
그래서 final로 지정된 클래스는 다른 클래스의 조상이 될 수 없다.
final메서드변경될 수 없는 메서드, final로 지정된 메서드는 오버라이딩을 통해 재정의 될 수 없다.
멤버변수변수 앞에 final이 붙으면, 값을 변경할 수 없는 상수가 된다.
지역변수(멤버변수와 동일)
final class FinalTest {				// 조상이 될 수 없는 클래스
	final int MAX_SIZE = 10;		// 값을 변경할 수 없는 멤버변수(상수)
    
    final void getMaxSize() {		// 오버라이딩을 할 수 없는 메서드(변경불가)
    	final int LV = MAX_SIZE;	// 값을 변경할 수 없는 지역변수(상수)
        return MAX_SIZE;
    }
}

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

final이 붙은 변수는 상수이므로 일반적으로 선언과 초기화를 동시에 하지만, 인스턴스변수의 경우 생성자에서 초기화 되도록 할 수 있다.

class Card{
    final int NUMBER;       // 상수지만 선언과 함께 초기화 하지 않고
    final String KIND;      // 생성자에서 단 한번만 초기화할 수 있다.
    static int width = 100;
    static int height = 250;
    
    Card(String kind, int num){
        KIND = kind;
        NUMBER = num;
    }
    
    Card(){
        this("HEART", 1);
    }
    
    public String toString(){
        return KIND + " " + NUMBER;
    }
}

public class FinalCardTest {
    public static void main(String[] args) {
        Card c = new Card("HEART", 10);
        // c.NUMBER = 5; // 에러! cannot assign a value to final variable NUMBER
        System.out.println(c.KIND);
        System.out.println(c.NUMBER);
        System.out.println(c);  // System.out.println(c.toString());
    }
}

4.4 abstract - 추상의, 미완성의

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

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

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

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

4.5 접근 제어자(access modifier)

접근 제어자는 멤버 또는 클래스에 사용되어, 해당하는 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 한다.

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

private : 같은 클래스 내에서만 접근이 가능하다.
default : 같은 패키지 내에서만 접근이 가능하다.(말그대로 default 이기도 하다.)
protected : 같은 패키지 내에서, 그리고 다른 패키지의 자손클래스에서 접근이 가능하다.
public : 접근 제한이 전혀 없다.
제어자같은 클래스같은 패키지자손클래스전체
publicOOOO
protectedOOOX
(default)OOXX
privateOXXX

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

클래스나 멤버, 주로 멤버에 접근 제어자를 사용하는 이유는 클래스의 내부에 선언된 데이터를 보호하기 위해서이다.

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

  • 외부로부터 데이터를 보호하기 위해서
  • 외부에는 불필요한, 내부적으로만 사용되는, 부분을 감추기 위해서
  • 보통 멤버변수를 private나 protected로 제한하고,
  • 멤버변수의 값을 읽고 변경할 수 있는 public 메서드를 제공함으로써 간접적으로 멤버변수의 값을 다룰 수 있도록 하는 것이 바람직하다.
class Time {
    private int hour, minute, 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;
    }
}

public class TimeTest {
    public static void main(String[] args) {
        Time t = new Time(12, 35, 30);
        System.out.println(t);
        // t.hour = 13;                 // 에러. 변수 hour의 접근 제어자가 private이므로 접근할 수 없다.
        t.setHour(t.getHour() + 1);     // 현재시간보다 1시간 후로 변경한다.
        System.out.println(t);          // System.out.println(t.toString());
    }
}

생성자의 접근 제어자

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

  • 생성자의 접근 제어자를 private로 지정하면, 외부에서 생성자에 접근할 수 없으므로 인스턴스를 생성할 수 없게 된다. 그래도 클래스 내부에서는 인스턴스를 생성할 수 있다.

-> 대신 인스턴스를 생성해서 반환해주는 public 메서드를 제공함으로써 외부에서 이 클래스의 인스턴스를 사용하도록 할 수 있다. 이 메서드는 public인 동시에 static 이어야 한다.

class Singleton {
	...
    private static Singleton s = new Singleton();
    private Singleton() {
    	...
    }
    // 인스턴스를 생성하지 않고도 호출할 수 있어야 하므로 static이어야 한다.
    public static Singleton getInstance() {
    	return s;
    }
    ...
}

4.6 제어자(modifier)의 조합

<대상에 따라 사용할 수 있는 제어자>

대상사용가능한 제어자
클래스public, (default), final, abstract
메서드모든 접근 제어자, final, abstract, static
멤버변수모든 접근 제어자, fianl, static
지역변수final
1. 메서드에 static과 abstract를 함께 사용할 수 없다.
	- static메서드는 몸통이 있는 메서드에만 사용할 수 있기 때문이다.

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

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

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


👉 5. 다형성(polymorphism)

5.1 다형성이란?

  • 다형성이란 어려 가지 형태를 가질 수 있는 능력 을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.

  • 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 했다는 것이다.

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

// ex)
CationTv c = new CationTv();
Tv t = new CationTv();	// 조상 타입의 참조변수로 자손 인스턴스를 참조

Tv타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv클래스의 멤버들(상속받은 멤버포함)만 사용할 수 있다. 따라서, 생성된 CaptionTv인스턴스의 멤버 중에서 Tv클래스에 정의 되지 않은 멤버, text와 caption()은 참조변수 t로 사용이 불가능하다. 즉, t.text 또는 t.caption()과 같이 할 수 없다는 것이다.

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

조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다.
반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다 !

5.2 참조변수의 형변환

기본형 변수와 같이 참조변수도 형변환이 가능하다.

자손타임 -> 조상타입(Up-casting) : 형변환 생략가능
자손타입 <- 조상타입(Down-casting) : 형변환 생략불가
class Car {
	String color;
    int door;
    
    void drive() {}
    void stop() {}
}

class FireEngine extends Car  {
	void water(){}
}

class Ambulance extends Car {
	void siren(){}
}

// ex1)
FireEngine f;
Ambulance a;
a = (Ambulance)f;	// 에러. 상속관계가 아닌 클래스간의 형변환 불가
f = (FireEngine)a;	// 에러. 상속관계가 아닌 클래스간의 형변환 불가

// ex2)
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;

car = fe;					// car = (Car)fe;에서 형변환 생략됨. 업캐스팅
fe2 = (FireEngine) car;		// 형변환을 생략불가. 다운 캐스팅

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

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


5.3 instanceof 연산자

  • 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 사용하는 연산자

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

-> instanceof를 이용한 연산결과로 true를 얻엇다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.

void doWork(Car c) {
	if(c instanceof FireEngine) {
    	FireEngine fe = (FireEngine)c;
        fe.water();
        ...
    } else if(c instanceof Ambulance) {
    	Ambulance a = (Ambulance)c;
        a.siren();
        ...
    }
}

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

  • 조상 클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때, 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우자손타입의 참조변수로 자손 인스턴스를 참조하는 경우 는 서로 다른 결과를 얻는다.

  • 메서드의 경우 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우 에도 변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.

결론부터 말하자면,

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

하지만, 중복 정의되지 않은 경우, 조상타입의 참조변수를 사용했을 때와 자손타입의 참조변수를 사용했을 때의 차이는 없다.

class Parent {
    int x = 100;
    
    void method(){
        System.out.println("Parent Method");
    }
}

class Child extends Parent {
    int x = 200;
    
    void method(){
        System.out.println("x=" + x);   // this.x 와 같다.
        System.out.println("super.x=" + super.x);
        System.out.println("this.x=" + this.x);
    }
}

public class BindingTest3 {
    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();
        System.out.println("c.x = " + c.x);
        c.method();
    }
}

/* 실행결과
p.x = 100
x=200
super.x=100
this.x=200

c.x = 200
x=200
super.x=100
this.x=200
*/

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


5.5 매개변수의 다형성

참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다. 아래와 같이 Product, Tv, Computer, Audio, Buyer 클래스가 정의되어 있을 때,

class Product {
	int price;
    int bonusPoint;
}
class Tv extends Product{}
class Computer extends Product{}
class Audio extends Product{}

class Buyer {
	int money = 1000;
    int bonusPoint = 0;
}

원래라면 제품들을 구입할 때마다 메서드가 추가로 필요하다. 그러나 메서드의 매개변수에 다형성을 적용하면 아래와 같이 하나의 메서드로 간단히 처리할 수 있다.

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;
}

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

import java.util.Vector;    // Vector클래스를 사용하기 위해서 추가해 주었다.

class Product {
    int price;          // 제품의 가격
    int bonusPoint;     // 제품구매 시 제공하는 보너스점수
    
    Product(int price){
        this.price = price;
        bonusPoint = (int)(price/10.0);
    }
    
    Product(){
        price = 0;
        bonusPoint = 0;
    }
}

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; // 보너스점수
    Vector item = new Vector();     // 구입한 제품을 저장하는데 사용될 Vector객체
    
    void buy(Product p){
        if(money < p.price){
            System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
            return;
        }
        
        money -= p.price;           // 가진 돈에서 구입한 제품의 가격을 뺀다.
        bonusPoint += p.bonusPoint; // 제품의 보너스 점수를 추가한다.
        item.add(p);                // 구입한 제품을 Vector에 저장한다.
        System.out.println(p + "을/를 구입하셨습니다.");
    }
    
    void refund(Product p){         // 구입한 제품을 환불한다.
        if(item.remove(p)){         // 제품을 Vector에서 제거한다.
            money += p.price;
            bonusPoint -= p.bonusPoint;
            System.out.println(p + "을/를 반품하셨습니다.");
        } else {                    // 제거에 실패한 경우
            System.out.println("구입하신 제품 중 해당 제품이 없습니다.");
        }
    }
    
    void summary(){                 // 구매한 물품에 대한 정보를 요약해서 보여준다.
        int sum = 0;                // 구입한 물품의 가격합계
        String itemList = "";       // 구입한 물품목록
        
        if(item.isEmpty()){         // Vector가 비어있는지 확인한다.
            System.out.println("구입하신 제품이 없습니다.");
            return;
        }
        
        // 반복문을 이용해서 구입한 물품의 총 가격과 목록을 만든다.
        for(int i=0; i<item.size(); i++){
            Product p = (Product)item.get(i);   // Vector의 i번째에 있는 객체를 얻어온다.
            sum += p.price;
            itemList += (i==0) ? "" + p : ", " + p;
        }
        
        System.out.println("구입하신 물품의 총금액은 " + sum + "만원입니다.");
        System.out.println("구입하신 제품은 " + itemList + "입니다.");
    }
}

public class PolyArgumentTest3 {
    public static void main(String[] args) {
        Buyer b = new Buyer();
        Tv tv = new Tv();
        Computer com = new Computer();
        Audio audio = new Audio();
        
        b.buy(tv);
        b.buy(com);
        b.buy(audio);
        b.summary();
        System.out.println();
        b.refund(com);
        b.summary();
    }
}

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

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

Vector 클래스는 단지 동적으로 크기가 관리되는 객체배열일 뿐이다.



👉 6. 추상클래스(abstract class)

6.1 추상클래스란?

  • 클래스를 설계도에 비유한다면, 추상클래스는 미완성 설계도에 비유할 수 있다.

  • 추상클래스 자체로는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 조상클래스 로서 중요한 의미를 갖는다.

키워드 abstract 를 붙이기만 하면 된다.

abstract class 클래스이름 {
	...
}

6.2 추상메서드(abstract method)

  • 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨둔 메서드

  • 미완성 상태로 남겨 놓는 이유는 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만을 작성하고, 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려 주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워 두는 것이다.

/* 주석을 통해 어떤 기능을 수행할 목적으로 작성하였는지 설명한다. */ 
abstract 리턴타입 메서드이름();

추상클래스로부터 상속받는 자손클래스는 오버라이딩을 통해 조상인 추상클래스의 추상 메서드를 모두 구현해주어야 한다.

만일 조상으로부터 상속받은 추상메서드 중 하나라도 구현하지 않는다면, 자손클래스 역시 추상클래스로 지정해 주어야 한다.


6.3 추상클래스의 작성

추상 - 낱낱의 구체적 표상이나 개념에서 공통되 ㄴ성질을 뽑아 이를 일반적인 개념으로 파악하는 정신 작용


추상화 : 클래스간의 공통점을 찾아내서 공통의 조상을 만드는 작업
구체화 : 상속을 통해 클래스를 구현, 확장하는 작업

abstract class Player {
	boolean pause;			// 일시정지 상태를 저장하기 위한 변수
    int currentPos;			// 현재 Play되고 있는 위치를 저장하기 위한 변수
    
    Player() {				// 추상클래스도 생성자가 있어야 한다.
    	puase = false;
        currentPos = 0;
    }
    /** 지정된 위치(pos)에서 재생을 시작하는 기능이 수행하도록 작성되어야 한다. */
    abstract void play(int pos);	// 추상메서드
    /** 재생을 즉시 멈추는 기능을 수행하도록 작성되어야 한다. */
    abstract void stop();			// 추상메서드
    
    void play() {
    	play(currentPos);			// 추상메서드를 사용할 수 있다.
    }
    
    void pause() {
    	if(pause) {	// pause가 true일 때(정지상태) 에서 pause가 호출되면,
        	pause = false;			// pause의 상태를 false로 바꾸고,
            play(currentPos);		// 현재의 위치에서 play를 한다.
        } else {	// puase가 false일 때(paly상태)에서 pause가 호출되면,
        	pause = true;			// pause의 상태를 true로 바꾸고
            stop();					// play를 멈춘다.
        }
    }
}

// Player클래스를 조상으로 하는 CDPlayer
class CDPlayer extends Player {
	void play(int currentPos) {
    	/* 조상의 추상메서드를 구현, 내용 생략 */
    }
    
    void stop() {
    	/* 조상의 추상메서드를 구현, 내용 생략 */
    }
    
    // CDPlayer클래스에 추가로 정의된 멤버
    int currentTrack;	// 현재 재생 중인 트랙
    
    void nextTrack(){
    	currentTrack++;
        ...
    }
    
    void preTrack(){
    	if(currentTrack > 1){
        	currentTrack--;
            ...
        }
    }
}


👉 7. 인터페이스(interface)

7.1 인터페이스란?

  • 인터페이스는 일종의 추상클래스

  • 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.

  • only 추상메서드 + 상수


7.2 인터페이스의 작성

키워드를 class 대신 interface, 그리고 접근제어자로 public 또는 default 사용 가능

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

<제약사항>

  • 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
    단, static 메서드와 디폴트 메서드는 예외(JDK1.8부터)
interface PlayingCard {
	public static final int SPADE = 4;
    final int DIAMOND = 3;	// public static final int DIAMOND = 3;
    static int HEART = 2;	// public static final int HEART = 2;
    int CLOVER = 1;			// public static final int CLOVER = 1;
    
    public abstract String getCardNumber();
    String getCardKind();	// public abstract String getCardKind();
}

7.3 인터페이스의 상속

  • 인터페이스는 인터페이스로부터만 상속받을 수 있다.

  • 클래스와는 달리 다중상속, 즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능하다.

interface Movable {
	/** 지정된 위치(x, y)로 이동하는 기능의 메서드 */
    void move(int x, int y);
}
interface Attackable {
	/** 지정된 대상(u)을 공격하는 기능의 메서드 */
    void attack(Unit u);
}

interface Fightable extends Movable, Attackable { }

7.4 인터페이스의 구현

구현한다는 의미의 키워드 implements 를 사용하여 인터페이스에 정의된 추상메서드를 구현해야 한다.

class 클래스이름 implements 인터페이스이름 {
	// 인터페이스에 정의된 추상메서드를 구현해야 한다.
}

class Fighter implements Fightable {
	public void move(int x, int y) { /* 내용 생략 */ }
    public void attack(Unit u)  { /* 내용 생략 */ }
}
  • 만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, abstract를 붙여서 추상클래스로 선언해야 한다.

  • 상속과 구현을 동시에도 가능하다.

    	class Fighter extends Unit implements Fightable { }

7.5 인터페이스를 이용한 다중상속

public class Tv {
	protected boolean power;
    protected int channel;
    protected int volume;
    
    public void power() { power = !power; }
    public void channelUp() { channel++; }
    public void channelDown() { channel--; }
    public void volumeUp() { volume++; }
    public void volumeDown() { volume--; }
}

public class VCR {
	protected int counter;
    
    public void play() { /* Tape 재생 */ }
    public void stop() { /* 재생 멈춤 */ }
    public void reset() { counter = 0; }
    public int getCounter() { return counter; }
    public void setCounter(int c) { counter = c; }
}

public interface IVCR {
	public void play();
    public void stop();
    public void reset();
    public int getCounter();
    public void setCounter(int c);
}

// IVCR 인터페이스를 구현하고 Tv클래스로부터 상속받는 TVCR클래스
public class TVCR extends Tv implements IVCR {
	VCR vcr = new VCR();
    
    public void play() { vcr.play(); }	// 코드를 작성하는 대신 VCR인스턴스의 메서드를 호출한다.
    public void stop() { vcr.stop(); }
    public void reset() { vcr.reset(); }
    public int getCounter() { return vcr.getCounter(); }
    public void setCounter(int c) { vcr.setCounter(c); }
}

IVCR 인터페이스를 구현하기 위해서는 새로 메서드를 작성해야하는 부담이 있지만 이처럼 VCR 클래스의 인스턴스를 사용하면 손쉽게 다중상속을 구현할 수 있다.


7.6 인터페이스를 이용한 다형성

인터페이스 역시 자손클래스의 인스턴스조상타입의 참조변수 로 참조하는 것이 가능하며, 인터페이스 타입으로의 형변환도 가능하다.

인터페이스 Fightable을 클래스 Figher가 구현했을 때, 다음과 같이 Fighter 인스턴스를 Fightable 타입의 참조변수로 참조하는 것이 가능하다.

Fightable f = (Fightable) new Fighter();
	또는
Fightable f = new Fighter();

따라서 인터페이스는 다음과 같이 메서드의 매개변수의 타입으로 사용될 수 있다.

void attack(Fightable f) { ... }

그리고 다음과 같이 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.

Fightable method() {
	...
    Fighter f = new Fighter();
    return f;
}
  • 리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

7.7 인터페이스의 장점

  • 개발시간을 단축시킬 수 있다.
  • 표준화가 가능하다.
  • 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
  • 독립적인 프로그래밍이 가능하다.

7.8 인터페이스의 이해

먼저 인터페이스를 이해하기 위해서는 다음 두 가지 사항을 반드시 염두에 두고 있어야 한다.

  • 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
  • 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다.(내용은 몰라도 된다.)

7.9 디폴트 메서드와 static 메서드

  • 원래는 인터페이스에 추상 메서드만 선언할 수 있는데,
  • JDK1.8부터 디폴트 메서드와 static 메서드도 추가할 수 있게 되었다.

디폴트 메서드

  • 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드 로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.

  • 앞에 키워드 default 를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통{}이 있어야 한다. 디폴트 메서드 역시 접근 제어자가 public이며, 생략가능하다.

interface MyInterface {
	void method();
    void newMethod();	// 추상 메서드
}
    
// ->
interface MyInterface {
	void method();
    default void newMethod();
}
  • 위쪽과 같이 newMethod()라는 추상 메서드를 추가하는 대신, 아래쪽과 같이 디폴트 메서드를 추가하면,
  • 기존의 MyInterface를 구현한 클래스를 변경하지 않아도 된다.
  • 즉, 조상 클래스에 새로운 메서드를 추가한 것과 동일해 지는 것이다.

대신, 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생한다. 이 충돌을 해결하는 규칙은 다음과 같다.

1. 여러 인터페이스의 디폴트 메서드 간의 충돌
인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.


2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

외우기 귀찮으면, 그냥 필요한 쪽의 메서드와 같은 내용으로 오버라이딩 해버리자



👉 8. 내부 클래스(inner class)

서로 긴밀한 관계에 있어 클래스 내에 선언된 클래스

내부 클래스 장점
- 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 잇ㄷ.
- 코드의 복잡성을 줄일 수 있다(캡슐화).
class A {	// 외부 클래스
	...
    class B {	// 내부 클래스
    	...
    }
    ...
}

8.2 내부 클래스의 종류와 특징

내부 클래스특징
인스턴스 클래스
(instance class)
외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 인스턴스멤버
처럼 다루어진다. 주로 외부 클래스의 인스턴스멤버들과 관련된 작업에 사용
될 목적으로선언된다.
스태틱 클래스
(static class)
외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 static 멤버처럼
다루어진다. 주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목
적으로 선언된다.
지역 클래스
(local class)
외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만
사용될 수 있다.
익명 클래스
(anonymous class)
클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용)

8.3 내부 클래스의 선언

1, 2를 보면 변수가 선언된 위치에 따라 인스턴스변수, 클래스변수(static 변수), 지역변수로 나뉘듯이 내부 클래스도 이와 마찬가지로 나뉜다.

// 1
class Outer {
	int iv = 0;
    static int cv = 0;
    
    void myMethod() {
    	int lv = 0;
    }
}

// 2 (내부 클래스)
class Outer {
	class InstanceInner {}
    static class StaticInner {}
    
    void myMethod() {
    	class LocalInner {}
    }
}

8.4 내부 클래스의 제어자와 접근성

내부 클래스도 클래스이기 때문에 abstract나 final과 같은 제어자 를 사용할 수 있을 뿐만 아니라, 멤버변수들처럼 private, protected 와 접근제어자 도 사용이 가능하다.

ex1)

public class InnerEx1 {
    class InstanceInner {
        int iv = 100;
        // static int cv = 100;         // 에러! static 변수를 선언할 수 없다.
        final static int CONST = 100;   // final static은 상수이므로 허용
    }

    static class StaticInner{
        int iv = 200;
        static int cv = 200;    // static 클래스만 static 멤버를 정의할 수 있다.
    }

    void myMethod(){
        class LocalInner{
            int iv = 300;
            // static int cv = 300;         // 에러! static 변수를 선언할 수 없다.
            final static int CONST = 300;   // final static은 상수이므로 허용
        }
    }
    
    public static void main(String[] args) {
        System.out.println(InstanceInner.CONST);
        System.out.println(StaticInner.cv);
    }
}
  • 인스턴스멤버는 같은 클래스에 있는 인스턴스멤버와 static 멤버 모두 직접 호출 가능하지만, static멤버는 인스턴스멤버를 직접 호출 할 수 없다.

-> 인스턴스 클래스는 외부클래스의 인스턴스멤버를 객체생성 없이 바로 사용할 수 있지만, 스태틱 클래스는 외부 클래스의 인스턴스멤버를 객체생성 없이 사용할 수 없다.


추가 예시)

class Outer {
    int value = 10; // Outer.this.value
    
    class Inner {
        int value = 20; // this.value
        
        void method1(){
            int value = 30;
            System.out.println("           value :" + value);
            System.out.println("      this.value :" + this.value);
            Systme.out.println("Outer.this.value :" + Outer.this.value);
        }
    } // Inner클래스의 끝
} // Outer클래스의 끝

public class InnerEx5 {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.method1();
    }
}

내부 클래스와 외부 클래스에 선언된 변수의 이름이 같을 때 변수 앞에 this 또는 외부 클래스명.this 를 붙여서 서로 구별할 수 있다는 것을 보여준다.


8.5 익명 클래스(anonymous class)

클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용될 수 있고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스 이다.

new 조상클래스이름() {
	// 멤버 선언
}

	또는

new 구현인터페이스이름() {
	// 멤버 선언
}
  • 이름이 없기 때문에 생성자도 가질 수 없으며, 조상클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 없다.

ex)

import java.awt.*;
import java.awt.event.*;

class InnerEx7 {
	public static void main(String[] args) {
    	Button b = new Button("Start");
        b.addActionListener(new EventHandler());
	}
}
class EventHandler implements ActionListener {
	public void actionPerformed(ActionEvent e) {
    	System.out.println("ActionEvent occurred!!!");
	}
}

// 익명클래스로 변환 시
import java.awt.*;
import java.awt.event.*;

class InnerEx7 {
	public static void main(String[] args) {
    	Button b = new Button("Start");
        b.addActionListener(new ActionListener() {
        	public void actionPerformed(ActionEvent e) {
            	Systme.out.println("ActionEvent occurred!!!");
            }
        };
    }
}
profile
단단하게 오래가고자 하는 백엔드 개발자

0개의 댓글