솜이의 데브로그

Chapter 07 ) 객체지향프로그래밍(2) 본문

책을 읽자/Java의 정석

Chapter 07 ) 객체지향프로그래밍(2)

somsoming 2021. 9. 8. 01:29

참고 : Java의 정석

 

1. 상속 (inheritance)

 

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

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

 

다음과 같이 상속받을 수 있다.

class Child extends Parent{
	//...
}

 

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

자손 클래스는 조상 클래스의 모든 멤버를 상속받으므로, Child 클래스는 Parent 클래스의 멤버들을 포함한다고 할 수 있다.

extends 의 의미 -> 조상 클래스를 '확장' 한다는 의미로 해석할 수 있음.

 

  • 생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
  • 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.

같은 내용의 코드를 하나 이상의 클래스에 중복적으로 추가해야하는 경우 상속관계를 이용해서 코드의 중복을 최소화해야한다.

자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.

 

 

포함(Composite) : 클래스 간의 포함 관계를 맺어주는 것은 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것을 뜻한다.

 

ex) 아래 예시처럼 Circle2 에 멤버변수로 Point 클래스의 참조변수를 선언한다.

class Circle{
	int x;
    int y;
    int z;
}

class Point{
	int x;
    int y;
}

class Circle2{
	Point c = new Point();
    int r;
}

 

상속처럼 클래스를 재사용 가능하다.

 

※상속과 포함 구분하는 법!!!

'~은 ~이다(is- a)' 인 경우 → 상속.      ex) SportsCar 는 Car 이다.

'~은 ~을 가지고 있다 ('has- a)' 인 경우 → 포함      ex) 원(Circle)은 점(Point)을 가지고 있다.

 

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

toString() 의 경우, 모든 클래스의 조상인 Object 클래스에 정의 된 것으로, 어떤 종류의 객체에 대해서도 toString()을 호출하는 것이 가능하다. 참조변수와 문자열의 결합에는 toString()이 자동적으로 호출되어 참조변수를 문자열로 대치한 후 처리한다.

 

자바에서는 단일 상속만을 허용한다. (C++와 다른점!) → 클래스 간의 관계가 명확해지고 코드를 더 신뢰할 수 있게 한다.

따라서 여러개의 클래스를 사용하고 싶을 땐 하나의 클래스를 상속받고, 클래스 내에 포함하여 사용 가능하다.

 

 

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

Object 클래스는 모든 클래스 상속계층도 최상위에 있는 조상클래스이다. 다른 클래스로부터 상속받지 않는 모든클래스들은 자동적으로 Object 클래스로부터 상속받는다. 즉, 모든 클래스의 조상이 된다.

 

 

2. 오버라이딩 (overriding)

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

오버라이딩은 선언부가 서로 일치해야한다.

 

오버라이딩의 조건

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

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

→ 조상클래스의 메서드를 자손 클래스에서 오버라이딩 할 때

  1. 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.  (ex. protected 였으면 오버라이딩은 protected, public만 가능)
  2. 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다. (범위 역시 넓어질 수 없다)
  3. 인스턴스메서드를 static 메서드로 또는 그 반대로 변경 할 수 없다.

 

오버로딩 vs 오버라이딩

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

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

 

super

상속받은 멤버와 자신의 멤버와 이름이 같을 때, 상속받은 멤버에 super을 붙여 구별할 수 있다. (this와 유사)

super() 역시 생성자이며, 자손 클래스의 인스턴스가 생성될 때 조상클래스의 멤버도 초기화해야하므로 조상클래스의 생성자가 호출되어야한다.

 

Object클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자, this() 또는 super() 를 호출해야한다. 그렇지 않으면 컴파일러가 자동적으로 'super();' 를 생성자의 첫 줄에 삽입한다.

 

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;
    
    Point 3D(int x, int y, int z){
    	super(x,y);
        this.z = z;
    }
    
    String getLocation(){
    	return "x :" + x + ", y :" + y + ", z :" + z;
    }
}

 

3. 패키지 (Package)

 

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

 

클래스가 물리적으로 하나의 클래스파일(.class) 파일인 것과 같이 패키지는 물리적으로 하나의 디렉토리이다. 따라서 어떤 패키지에 속한 클래스는 해당 디렉토리에 존재하는 클래스파일이어야 한다.

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

 

패키지는 다음과 같이 선언한다.

package 패키지명;

패키지선언문 반드시 소스파일에서 첫번째 문장이어야하며, 하나의 소스파일에 단 한번만 선언될 수 있다.

패키지의 이름은 클래스명과 구분하기 위해 소문자로하는 것을 원칙으로 한다.

환경변수를 잘 설정해주기!

 

import 문

다른 패키지의 클래스를 사용할 때 import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해 줄 수 있다.

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

 

모든 소스파일(.java)에서 import문은 package문 다음에, 그리고 클래스 선언문 이전에 위치해야한다.

import는 package문과 다르게 한 소스파일에서 여러번 선언 할 수 있다.

 

import 패키지명.클래스명;
or
import 패키지명.*;

위의 예시에서 .* 를 사용하는 것이 하위 패키지의 클래스까지 포함하는 것은 아니다.

 

지금까지 String, System과 같은 java.lang 패키지의 클래스들을 패키지명 없이 사용할 수 있었던 이유는 모든 소스파일에 묵시적으로 다음과 같은 import문이 선언되어 있기 때문이다.

 

import java.lang.*;

 

static import 문을 사용하면 static 멤버를 호출할 때 클래스 이름을 생략할 수 있다.

 

 

4. 제어자(modifier)

 

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

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

하나의 대상에 대해서 여러 제어자를 조합하여 사용하는 것이 가능하다.

 

 

(1) static

static은 '클래스의' 또는 '공통적인' 의 의미를 가지고 있다.

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

  • 멤버변수 : 모든 인스턴스에 공통적으로 사용, 인스턴스 생성 안하고도 사용 가능, 클래스가 메모리에 로드될 때 생성.
  • 메서드 : 인스턴스 생성 없이 호출 가능, 메서드 내에서 인스턴스 멤버들을 직접 사용할 수 없음.

 

(2) final

final은 '마지막의' 또는 '변경될 수 없는' 의 의미를 가지고 있으며 거의 모든 대상에 사용 가능하다.

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

  • 클래스 : 변경될 수 없는 클래스, 확장 불가능. 따라서 final로 지정된 클래스는 다른 클래스의 조상이 될 수 없다.
  • 메서드 : 변경될 수 없는 메서드이므로 오버라이딩을 통해 재정의 될 수 없다.
  • 멤버변수, 지역변수 : 값을 변경할 수 없는 상수가 된다.

 

(3) abstract

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

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

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

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

 

 

(4) 접근 제어자 (access modifier)

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

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

  • pirvate : 같은 클래스 내에서만 접근이 가능하다.
  • default : 같은 패키지 내에서만 접근이 가능하다. (접근제어자가 지정되어있지 않다면 default)
  • protected : 같은 패키지 내에서, 그리고 다른 패키지의 자손클래스에서 접근 가능
  • public : 접근 제한이 전혀 없다.

public > protected > (default) > private

 

캡슐화

클래스나 멤버에 접근제어자를 사용하는 이유는 클래스의 내부에 선언된 데이터를 보호하기 위해서이다. 데이터를 외부에서 함부로 변경하지 못하도록 외부로부터의 접근을 제한하는 것이 필요한데, 이것을 객체지향개념의 '캡슐화'에 해당한다.

또 내부작업을 위해 임시로 사용되는 멤버변수나 부분작업 처리를 위한 메서드등의 멤버들을 클래스에 감추기 위해 사용한다.

 

일반적으로 멤버변수는 private 이나 protected (확장 가능성 있을 경우)로 설정하고, 멤버변수의 값을 읽고 변경할 수 있도록 'get멤버변수이름' , 'set멤버변수이름' 으로 설정하는 메서드를 선언한다.

 

생성자에 접근제어자를 사용하여 (private 등으로 지정) 인스턴스의 생성을 제한 할 수 있다.

 

class Singleton{
	...
    private static Singleton s = new Singleton();
    private Singleton(){
    	...
    }
    
    public static Singleton getInstance(){
    	return s;
    }
}

→ 생성자를 통해 직접 인스턴스를 생성하지 못하게 하고 public 메서드를 통해 인스턴스에 접근하게 함으로써 사용할 수 있는 인스턴스의 개수를 제한 할 수 있다.

 

또 생성자가 prviate인 클래스는 다른 클래스의 조상이 될 수 없다. 따라서 클래스 앞에 final을 추가하여 상속할 수 없는 클래스임을 표시하는 것이 좋다.

 

 

5. 다형성 (polymorphism)

 객체지향의 중요한 특징 중 하나!!!

 

객체지향개념에서 다형성이란 '여러가지 형태를 가질 수 있는 능력'을 의미하며, 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있는 것이다.

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

 

ex)

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

class CaptionTv extends Tv{
	String text;
    void caption() { //내용 생략 }
}


Tv t = new CaptionTv();

위의 예시에서 CaptionTv가 Tv를 상속받는다.

이 때 조상클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하는 것이 가능하다.

그러나 이 때 Tv 타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv 클래스의 멤버들 (상속받은 멤버 포함)만 사용할 수 있다.

 

반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 불가능하다.

참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버의 개수보다 같거나 적어야하기 때문.

 

 

참조변수의 형변환

상속관계에 있는 클래스 사이에서 자손타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입의 참조변수로의 형변환이 가능하다.

 

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

 

참조변수간의 형변환 역시 캐스트 연산자를 사용하며 괄호() 안에 변환하고자 하는 타입의 이름 (클래스명) 을 적어주면 된다.

 

ex)

Car car - null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;

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

 

Car 클래스가 FireEngine클래스의 조상클래스인 경우이다. 이처럼 자손클래스를 조상클래스로 형변환할 땐 생략 가능하지만 조상클래스를 자손클래스로 형변환할 땐 생략 불가능하다.

 

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

 

※서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다.따라서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.

 

instanceof 연산자

참조변수가 참조하고 있는 인스턴스의 실제 타입을 체크할 수 있다.

실제 인스턴스와 같은 타입의 instanceof 연산 이외에 조상타입의 instanceof 연산에도 true를 결과로 얻으며, instanceof 연산의 결과가 true라는 것은 검사한 타입으로의 형변환을 해도 문제가 없다.

 

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

 

(메서드의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입의 메서드를 호출.)

 

 

조상타입의 참조변수 배열을 사용해 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어 다룰 수 있다.

ex)

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

 

Vector 클래스 : 동적으로 크기가 관리되는 객체배열. 10개의 객체를 저장할 수 있는 Vector 인스턴스를 생성하고, 10개 이상의 인스턴스가 저장되면 자동적으로 크기가 증가된다.

 

 

6. 추상클래스(abstract class)

미완성 클래스는 미완성 메서드를 포함하고 있다는 것이다.

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

abstract를 앞에 붙임으로써 상속을 통해 추상메서드를 구현해주어야함을 알 수 있다.

 

 

추상메서드

선언부만 작성하고 구현부는 작성하지 않은채로 남겨둔 것이 추상메서드이다.

 

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

구현부가 없으므로 {} 대신 문장의 끝을 알리는 ';' 를 적어준다.

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

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

 

상속계층도를 따라 내려갈수록 클래스는 기능이 점점 추가되어 구체화 정도가 심해지며, 상속계층도를 따라 올라갈수록 클래스의 추상화 정도가 심해진다고 할 수 있다.

 

 

7. 인터페이스(interface)

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

오직 추상메서드와 상수만을 멤버로 가질 수 있다. 따라서 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.

 

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

위와 같이 class 대신 interface를 사용하며, interface에도 클래스와 같이 접근제어자로 public 또는 default를 사용가능하다.

 

인터페이스의 멤버들은 다음과 같은 제약사항이 있다.

  • 모든 멤버변수는 public static final 이어야하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야하며, 이를 생략 할 수 있다.

 

인터페이스는 인터페이스로부터는 상속받을 수 있으며, 클래스와는 달리 다중상속이 가능하다.

 

인터페이스의 구현

추상메서드의 몸통을 만들어주는 클래스는 추상클래스가 자신을 상속받는 클래스를 정의하는 것과 같다. 다만 확장한다는 의미의 'extends' 대신 구현한다는 의미의 'implements'를 사용한다.

 

ex)

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

 

인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야한다는 것이다.

또한 리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

 

인터페이스의 장점

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

 

인터페이스의 이해

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

 

JDK1.8부터 인터페이스에 디폴트 메서드와 static 메서드도 추가 할 수 있게 되었다.

 

 

8. 내부 클래스(inner class)

내부 클래스는 클래스 내에 선언된 클래스이다.

 

내부 클래스의 장점

  1. 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
  2. 코드의 복잡성을 줄일 수 있다.(캡슐화)

 

내부 클래스의 종류는 변수의 선언위치에 따른 종류와 같다.

  • 인스턴스 클래스 : 외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 인스턴스 멤버처럼 다루어진다.
  • 스태틱 클래스 : 외부 클래스의 멤버변수 선언위치에 선언, 외부 클래스의 static 멤버처럼 다루어짐.
  • 지역 클래스 : 외부 클래스의 메서드나 초기화 블럭 안에 선언. 선언된 영역 내부에서만 사용 가능.
  • 익명 클래스 : 클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스 (일회용)

 

익명 클래스

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

또는

new 구현인터페이스이름(){
	//멤버 선언
}