그릭요거트 전문점에 손님이 와서 과일요거트를 주문했다. 아르바이트생인 당신은 주문을 받고 그릭요거트를 만들기 위해 우유를 사러 가는게 편한가 아니면 미리 만들어둔 그릭요거트에 과일을 올려 나가는게 편할까? 당연히 후자가 편하다. 그릭요거트를 만들어 두면 주문을 받을 때마다 요거트를 만들 필요 없이 기존의 요거트에 손님이 주문한 토핑만 얹으면 된다. 상속을 이용하는 이유도 이와 비슷하다.
상속이란 기존의 클래스를 재사용하여 새로운 클래스를 정의하는 것이다. 상속받아 클래스를 작성하면 적은 양의 코드로 새로운 클래스를 작성할 수 있고, 클래스를 공통적으로 관리하여 추가와 변경이 쉽다. 따라서 코드의 재사용성을 높이고 중복을 제거할 수 있다는 장점이 있다. 클래스를 상속받는 방법은 다음과 같다.
class Chlid extends Parent{
// ...
}
그릭요거트 알바생인 당신은 요거트의 질을 높이기 위해 레몬즙을 몇 방울 추가하기로 했다. 그렇다면 앞으로 손님이 주문할 과일 요거트, 초코 요거트, 바나나요거트 등 모든 요거트에 레몬즙이 추가된다. 토핑이 올라간 요거트메뉴는 모두 기본 요거트를 상속받기 때문이다. 반대로 초코 요거트에 요즘 유행하는 몰티져스 초코볼을 넣어보기로 했다. 초코볼이 추가되었을 때, 기본 요거트는 영향을 받을까? 아무런 영향도 받지 않는다. 과일 요거트에 제철 과일을 바꿔 넣든, 초코볼을 추가하든, 바나나 요거트에서 바나나 양을 줄이든 기본 그릭요거트는 아무런 영향을 받지 않는다. 이처럼 자식 클래스는 조상 클래스의 모든 멤버를 상속받기 때문에 항상 조상 클래스보다 같거나 많은 멤버를 갖는다. 상속에 상속을 거듭할 수록 클래스의 멤버 개수는 늘어난다.
과일 요거트에 그래놀라를 추가하기로 했다. 바나나 요거트에도 그래놀라를 추가하기로 했다. 둘 다 공통적으로 그래놀라를 추가하기로 했다면 그릭 요거트에 그래놀라를 추가하는게 나을 수 있다. 중복을 최소화할 수 있고 오동작을 줄일 수 있다.
자식 클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.
클래스간의 관계 - 포함관계
클래스끼리 관계를 맺고 재사용하는 방법은 상속 외에 포함(Composite)관계를 맺어주는 것이 있다. 포함관계를 맺어주는 것은 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것이다. 하나의 거대한 클래스를 작성하는 것보다 단위별로 여러 개의 클래스를 작성한 다음, 이 단위 클래스를 포함관계로 재사용하면 간결하고 손쉽게 클래스를 사용할 수 있다.
예를 들어 과일 요거트 클래스를 정의할 때, 멤버변수로 요거트에 들어가는 우유, 레몬즙과 그래놀라에 들어가는 오트밀, 아몬트와 각종 견과류, 꿀, 그리고 과일을 종류별로 다 선언하면 정말 거대한 클래스가 생긴다. 하지만 과일 요거트 클래스이 멤버변수로 요거트 클래스, 그래놀라 클래스, 모듬 과일 클래스로 선언하면 클래스도 간결하고 요거트 클래스와 그래놀라 클래스, 과일 클래스를 변경할 때, 각 클래스를 변경하면 되므로 관리하기 쉽다.
클래스간의 관계 결정하기
단일 상속(single inheritance)
C++은 여러 조상 클래스로부터 상속받는 다중상속(multiple inheirtance)을 허용하지만 자바는 단일상속(single inheritance)을 허용한다. 다중상속을 허용하면 여러 클래스로부터 상속받을 수 있기 때문에 여러 클래스로부터 상속받은 기능을 모두 사용할 수 있다는 장점이 있지만 관계가 복잡해지고 상속받은 멤버 사이에 이름을 구별할 수 없다는 단점이 있다.
단일 상속은 하나의 조상 클래스에게만 상속받을 수 있어 불편하지만 클래스와 클래스 사이의 관계가 명확해지고 신뢰를 얻을 수 있다는 장점이 있다.
Object클래스 - 모든 클래스의 조상
Object클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스이다. 다른 클래스로부터 상속받지 않은 모든 클래스는 자동으로 Object클래스로부터 상속받게 한다. 상속받지 않은 클래스를 컴파일하면 자동으로 extends Object를 추가하여 Object클래스로부터 상속받게 해준다. toString()이나 equasl()와 같은 메소드를 클래스를 정의하지 않고 사용할 수 있던 이유도 두 메소드가 Object클래스에 정의된 멤버 메서드이기 때문이다.
오버라이딩(overriding)
오버라이딩이란?
자식 클래스는 부모클래스로부터 상속받은 메소드를 그대로 사용할 수 있지만 자식 클래스가 필요한 대로 변경할 수 있다. 이를 오버라이딩이라 한다.
요거트 가게를 다시 예로 들어보자. 대부분의 인간은 음식을 먹고 맛에 대해 생각할 수 있다. 이를 getTaste()라고 해보자.그릭 요거트를 먹은 사람은 getTaste()의 반환값으로 "새콤하다"를 반환할 것이다. 그릭 요거트를 물려받은 초코요거트를 먹은 사람도 어떠한 맛을 느낄 것이고 getTaste()라는 메소드를 반환하겠지만 그 값은 새콤하다가 아니라 "달콤하다"일 것이다.
오버라이딩의 조건
자식클래스가 부모 클래스를 오버라이딩하기 위해서는 일정한 조건을 갖춰야한다. 첫번째, 메소드의 이름이 같아야한다. 두번째, 매개변수가 같아야 한다. 세번째, 반환타입이 같아야 한다. 즉 선언부가 일치해야한다.
접근제한자와 예외는 제한된 조건 하에서 변경할 수 있다. 접근 제어자를 부모 클래스의 메서드보다 좁은 범위로 변경할 수 없다. 예외는 부모 클래스의 메서드보다 많이 선언할 수 없다. 인스턴스 메서드를 static메서드로 또는 그 반대로 변경할 수 없다.
super
super는 자식 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용하는 참조변수이다. 생성자를 만들 때, 지역변수와 멤버 변수를 구분하기 위해 this를 사용했던 것처럼 부모 클래스로부터 물려받은 멤버도 자식 클래스의 멤버이므로 this도 사용할 수 있다. 하지만 부모 클래스의 멤버와 자식 클래스의 멤버를 구별하기 위해 super를 사용하는 것이 좋다.
아까 요거트의 맛을 느끼는 getTaste()메서드에서 부모 클래스인 그릭 요거트는 새콤하게, 초코 요거트는 달콤하게 반환하는 것으로 설명했다. 하지만 당연히 그릭 요거트가 들어갔으므로 새콤한 맛과 달콤한 맛을 동시에 느낄 것이다. 이 때, super.getTaste() + getTaste라고 선언하면 새콤하면서 달콤하다는 맛을 느낄 수 있다.
super() - 조상 클래스의 생성자
this()는 같은 클래스의 다른 생성자들을 호출하는데 사용했다면 super()는 자식 클래스의 생성자를 호출하는데 사용한다. 자식 클래스의 인스턴스를 생성하면 자식 클래스의 멤버와 부모 클래스의 멤버를 합쳐진 하나의 인스턴스가 생기기 때문에 자식 클래스가 부모 클래스의 멤버를 사용할 수 있게 된다. 따라서 부모 클래스의 멤버를 초기화해줘야하는데 그렇기 때문에 자식 클래스의 생성자에서 부모 클래스의 생성자를 호출해야한다.
Object클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자.this()나 생성자.super()를 호출해야 한다. 그렇지 않으면 컴파일러가 자동으로 super();를 생성자의 첫 줄에 삽입한다. 하지만 만약 자식 클래스가 super()를 이용하여 부모 클래스의 생성자를 초기화하지 않고 자식 클래스가 자기 클래스의 생성자를 초기화한다면 에러가 발생한다. 생성자를 공부할 때 컴파일러가 생성자를 선언하지 않은 경우 기본 생성자를 선언해줬지만, 생성자를 선언한 경우에는 컴파일러가 기본 생성자를 선언해주지 않는 것처럼 자식클래스가 생성자로 초기화한다면 컴파일러가 super();를 선언하지 않기 때문이다. 조상클래스의 멤버변수는 조상클래스의 생성자에 의해 초기화되도록 해줘야 한다.
package와 import문
패키지(package)
패키지는 서로 관련된 클래스와 인터페이스를 효과적으로 관리하기 위하여 그룹단위로 묶어 놓은 것이다. 클래스의 이름이 같더라도 서로 다른 패키지에 존재할 수 있다.
하나의 소스파일에는 첫 번째 문장으로 단 한 번의 패키지 선언만을 허용한다.
모든 클래스는 반드시 하나의 패키지에 속해야 한다.
패키지는 점으로 구분자하여 계층구조를 구성할 수 있다.
패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.
패키지의 선언
패키지를 선언하는 것은 클래스나 인터페이스 소스파일(.java)의 맨 위에 'package 패키지명;'과 같이 적어주면 된다. 조상 클래스의 생성자인 super()가 자식 클래스의 메소드에서 가장 첫번째 줄에 나와야 하는 것처럼 패키지 선언문도 주석과 공백을 제외하고서 가장 첫번째 문장이어야 한다.
여태까지 생성자를 선언하지 않아도 컴파일시 기본생성자가 만들어진 것처럼 패키지도 패키지를 지정하지 않으면 컴파일 시 이름없는 패키지(unnamed package)를 제공하여 자동으로 속하게 된다.
import문
클래스 코드를 작성하기 전 import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스코드에 사용되는 클래스이름에서 패키지명은 생략할 수 있다.
코딩 테스트를 연습할 때를 예로 들어보면 백준에서 입력받을 때 가장많이 사용하는 BufferedReader 클래스를 사용할 때, package java.io;를 선언한 뒤 사용하는 것이 아니라 import java.io.BufferedReader;로 적어 간단하게 사용할 수 있다.
import문의 선언
import문은 package문과 다르게 여러 번 선언할 수 있다.
다시 한번 코딩 문제를 풀 때를 예로 들어보면 BufferedReader 클래스 외에도 java.io패키지 내 BufferedWriter, InputStreamReader, OutputStreamWriter, IOException 등 여러가지 클래스를 사용해야할 때, 일일이 선언해줄 수 있지만 import java.io.*로 선언할 수 있다. 컴파일러가 해당 패키지에서 일치하는 클래스 이름을 찾느라 수고해야하겠지만 성능상의 차이는 없다.
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으로 호출 가능
class StaticImport{
public static void main(String[] args){
out.println(random());
}
}