상속이란 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다.
생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속이 이뤄진다.
따라서 자손클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래승의 멤버가 합쳐진 하나의 인스턴스가 생성된다.
class Parent {
int age;
}
class Child extends Parent {
void play(){
...
};
굳이 상속이 아니더라도 클래스를 재사용할 수 있다. 그 방법은 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것이다.(클래스 안에서 다른 클래스를 멤버변수로 쓰는 것이다.)
클래스의 관계를 포함으로 할 지, 상속으로 할 지 고민해야한다. 만약 두 클래스의 관계가 ~is 라면 상속, ~include 라면 포함으로 관계를 설정하는 것이 좋다.
잠깐 C++를 살펴보자. C++는 여러 클래스로부터의 상속이 가능하다.(다중상속) 하지만 자바에서는 오직 단일 상속만이 가능하다. 만약 여러 클래스를 재활용하고 싶다면 '포함'을 활용하는 것이 좋다.
사실 extends를 쓰지 않아도 클래스는 상속을 받는다. 만약, 우리가 상속할 클래스를 작성하지 않는 경우, 컴파일러는 자바의 Object 클래스를 자동으로 상속한다.
따라서 우리가 어떤 관계로든 클래스간의 관계를 설정하더라도, 최상위 조상 클래스는 Object클래스다. 이것이 toString(), equals()와 같은 메서드를 따로 정의하지 않고도 쓸 수 있는 이유다.
조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩이라고 한다.
- 이름이 같고
- 매개변수가 같고
- 반환타입이 같아야한다.
다만 접근 제어자와 예외는 제한된 조건 하에서만 다르게 변경할 수 있다. 그리고 추가적으로 주의해야 할 사항은 다음과 같다.
- 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
- 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
- 인스턴스메서드를 static메서드로 또는 그 반대로 변경할 수 없다.
오버로딩(overloading)은 기존에 없는 새로운 메서드를 정의하는 것이고. 오버라이딩(overriding)은 상속받은 메서드의 내용을 변경하는 것이다.
this를 멤버변수와 지역변수를 구분할 때 사용한 것처럼, super는 조상클래스의 멤버와 자손클래스의 멤버를 구분할 때 사용한다.
super()는 조상 클래스 생성자다. 이 생성자가 필요한 이유는 자손 클래스의 멤버가 조상 클래스의 멤버를 사용할 수도 있기 때문이다. 따라서 Object 클래스를 제외한 모든 클래스의 생성자는 첫 줄에 다른 클래스 혹은 조상 클래스의 생성자를 호출해야한다. 그렇지 않으면 컴파일러는 자동으로 super();를 추가할 것이다.
- 하나의 소스파일에는 첫 번째 문장으로 단 한 번의 패키지 선언만을 허용한다.
- 모든 클래스는 반드시 하나의 패키지에 속해야한다.
- 패키지는
.을 구분자로 하여 계층구조로 구성할 수 있다.- 패키지는 물리적으로 클래스 파일을 포함하는 하나의 디렉토리다.
package dir1.dir2.pack;
class PackageTest {
public static void main(String[] args){
System.out.println("Hello, my package")
}
}
다음과 같이 옵션을 추가하여 컴파일 하면 지정된 경로를 참조하거나, 해당 디렉토리가 존재하지 않는다면 자동적으로 생성한다.
javac -d . PackageTest.java
경우에 따라 루트 디렉토리를 클래스 패스에 포함해야할 수 있다. 아니면 JDK에 기본적으로 설정되어 있는 클래스패스를 사용할 수 있다.
Python과 같은 요령으로 사용하면 된다.
참고로 System과 String과 같은 java.lang 패키지 내의 클래스들을 import없이 사용할 수 있었던 것은 모든 소스파일에서 해당 패키지가 암묵적으로 import되어 있기 때문이다.
static import를 하면 static 멤버를 호출할 때 클래스의 이름을 생략할 수 있다.
static을 통해 멤버 변수와 메서드는 인스턴스간의 구분없이 공통적인 변수와 메서드가 된다. 초기화 블럭에도 사용가능하다.
final을 사용하면 클래스는 더이상 조상 클래스가 될 수 없고, 메서드는 오버라이딩을 할 수 없으며, 변수들은 변경이 불가능하다.
하지만 인스턴스 변수는 생성자를 통해서 초기화가 가능하다. 선언만 하고, 값은 생성자의 매개변수로 전달받으면 된다. 이러면 인스턴스 별로 서로 다른 값을 가지게 할 수 있다.
지금은 아직 완성되지 않은 메서드, 클래스 앞에 붙여서 쓰는걸로 이해하자.
public: 전체에서 접근 가능protected: 같은 패키지, 다른 패키지의 자손클래스에서 접근 가능default: 같은 패키지 내 접근만 가능private: 같은 클래스 내에서만 접근이 가능하다.
다음은 제어자를 조합할 떄 주의해야 할 사항이다.
- 메서드에
static과abstract를 함께 사용할 수 없다.- 클래스에
abstract와final을 함께 사용할 수 없다.abstract메서드의 접근 제어자가private일 수 없다.- 메서드에
private과final을 같이 사용할 필요는 없다.
자바에서 다형성이란 여러 가지 형태를 가질 수 있는 능력을 의미한다.
구체적인 사례로 이야기하자면, 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수 있다. 하지만 반대는 불가능하다. 왜냐하면 조상 클래스에 정의되어있지 않는 멤버가 사용되는 것을 방지하기 위함이다.
참조변수도 형 변환이 가능하다. 다시 말하자면 자손 타입에서 조상 타입으로 형 변환이 가능하다는 것이다. 이 경우, 형변환은 생략될 수 있다. 하지만 반대는 형 변환이 불가능하다.
class Parent{
...
}
class Child extends Parent{
...
}
Child c;
Child c2 = null;
Parent p;
p = c; // p = (Parent) c;에서 형변환 생략, 업캐스팅
c2 = (Child) p; //생략 불가, 다운캐스팅
이렇게 형변환을 하더라도 참조변수에 대해 형변환이 이뤄진 것 뿐이라, 인스턴스에는 아무런 영향이 없다. 단지 참조변수의 형변환을 통해서 참조하는 인스턴스에서 접근 할 수 있는 멤버의 수를 조절할 뿐이다.
여기서는 조상 클래스와 자손 클래스 간의 양방향 참조변수 형변환이 가능하다고 말했다. 그런데 5.1에서는 일방향 밖에 안된다고 하지 않았는가?
그 이유는 5.2에서는 자손 클래스의 인스턴스에 대한 참조변수의 형변환만을 이야기하는 것이기 때문이다. 만약 조상 클래스 인스턴스에 대한 참조변수를 형변환 하는 경우에는 컴파일은 되지만 실행에서 에러가 발생한다.
instanceof 연산자는 왼쪽의 참조변수가 오른쪽의 타입의 인스턴스인지 판단해준다.
자손 클래스에서 조상 클래스의 메서드를 오버라이드 할 수 있음을 앞서 살펴보았다. 그렇다면 조상 클래스의 멤버변수와 자손 클래스의 인스턴스변수가 같은 이름으로 정의 되어 있을 때 어떤 일이 일어날까?
이 경우, 참조 변수가 조상 클래스인 경우에는 조상 클래스의 멤버 변수에 접근하게 되고, 자손 클래스 타입의 참조 변수인 경우에는 인스턴스변수에 접근하게 된다.
임의의 클래스 타입의 참조변수가 메서드의 매개변수로 주어지는 경우, 해당 클래스를 상속받는 또다른 임의의 클래스 타입의 인스턴스는 해당 메서드의 매개변수로 전달 될 수 있다.
다시 말하자면, 매개변수에 주어진 클래스 타입과 일치하는 참조변수 뿐만 아니라 해당 클래스를 상속받은 클래스 타입의 참조변수도 매개변수에 전달될 수 있다는 뜻이다.
println(Object obj) 메서드가 모든 타입을 다룰 수 있는 이유가 바로 여기에 있다. 모든 클래스의 최상위 조상 클래스는 Object 이기 때문이다.
Parent p[] = new Parent[3];
p[0] = new Child1();
p[1] = new Child2();
p[2] = new Child3();
이처럼 조상타입의 참조변수 배열을 통해, 공통 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다. 하지만 배열의 크기를 사전에 설정해야하는 것이 문제가 된다. 그렇다고 무작정 크기를 크게 설정 할 수도 없는 노릇이다. 이때 사용하는 것이 Vector 클래스다. 이 클래스에는 내부적으로 Object타입의 배열을 가지고 있어서, 이 배열에 객체를 추가하거나 제거할 수 있게 작성되어 있다.그리고 배열의 크기를 알아서 관리해준다.
Vector 클래스는 add, remove, isEmpty, get, size 메서드를 가진다.
추상클래스란 해당 클래스에 미완성 된 메서드, 추상 메서드가 있다는 의미다. 추상클래스로는 인스턴스 생성이 불가능하며, 상속을 통해 자손클래스에서 완성되어야 한다. 만약 자손 클래스가 하나라도 미완성인 메서드를 가진다면 자손 클래스도 abstract를 붙여야 한다.
이를 이용해서 구현부는 다르지만 공통적인 선언부를 갖는 클래스들에 대해서 조상-자손 클래스 관계를 형성할 수 있다. 이때, 선언부만 같다면 abstract를 통해 추상화를 수행할 수 있다. 그리고 조상 클래스 참조변수 배열을 통해 자손 클래스 인스턴스들을 다룰 수 있다.
인터페이스는 일종의 추상클래스다. 하지만, 추상메서드와 상수만을 멤버로 가질 수 있다.
interface 인터페이스 이름 {
public static final 타입 상수이름 = 값;
public abstract 메서드 이름(메개변수 목록);
}
- 모든 멤버변수는
public static final이어야 하며, 생략 가능하다.- 모든 메서드는
public abstract이어야 하며, 이를 생략할 수 있다. 단static메서드와default메서드는 제외한다.(JDK1.8~)
인터페이스는 클래스와 다르게 다중상속이 가능하다.
인터페이스는 그 자체로 인스턴스를 생성할 수 없다.(추상클래스와 마찬가지로) 따라서 인터페이스는 자신을 완성해 줄 클래스가 필요하다. 그 과정은 추상클래스를 구체화 하는 과정과 다를 것이 없다. 다만 extends 대신 implements 키워드를 사용할 뿐이다.(물론 둘 다 동시에 쓸 수 있다. 구현과 상속을 동시에!!) 물론 인터페이스의 일부만 구현한 경우에는 abstract를 붙여 추상클래스로 선언해야한다.
자바에서는 다중 상속이 불가능하다. 하지만 인터페이스를 통한 다중 상속은 가능하다. 물론 인터페이스의 목적이 다중 상속에 있는 것은 아니라는 점에 유의하자.
인터페이스는 클래스에서 구현된다. 즉 인터페이스는 자기 자신을 구현해주는 클래스의 조상이라고 할 수 있다. 따라서 인터페이스 타입의 참조변수로 클래스의 인스턴스를 참조할 수 있으며 형변환도 가능하다.
이에 더해, 인터페이스는 메서드의 매개변수의 타입으로 사용될 수 있다. 인터페이스 타입의 매개변수는 인터페이스 자체가 아닌, 해당 인터페이스를 구현한 클래스의 인스턴스가 전달되어야 함을 의미한다.
메서드의 리턴 타입으로 인터페이스 타입을 지정할 수도 있다. 리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다. 자꾸 인터페이스 자체가 매개변수로 전달되거나 반환되는게 아니라 이를 구현한 클래스의 인스턴스를 반환하는지 의문일 수도 있다. 하지만 잘 생각해보자 인터페이스는 고도의 추상화가 이뤄진 미완성품이다. 미완성품을 전달하거나 반환할 수 없다.
- 개발시간 단축
- 표준화
- 클래스 간 관계 설정
- 독립적인 프로그래밍
A클래스가 B클래스의 인스턴스를 생성하여 이용한다고 가정하자. B클래스의 메서드 등이 수정되면 A클래스도 수정되어야 한다. 이 때, A와 B는 강하게 결합되어 있다. 고 볼 수 있다.
이 때, 인터페이스를 이해하면 이 결합을 약하게 할 수 있다. I인터페이스가 B클래스에서 구현된다고 하자. 그리고 A클래스는 I인터페이스를 매개변수 타입으로 설정한 메서드를 가지고 있다. 이 경우, A클래스 선언부에는 B클래스에 관한 내용이 전혀 들어가지 않지만, 인터페이스를 통해 B클래스 메서드에 접근할 수 있다.
즉, A클래스는 B클래스에 관한 구체적인 정보를 알지 않아도 되고, 알 필요도 없다.
사실 제3의 클래스를 통해 간접적인 관계를 설정할 수도 있긴하다.
JDK 1.8 이후로 static 메서드를 인터페이스에서 사용할 수 있게 되었다. 그 이전에는? 불가능했다. 사실 안 될 이유가 없는데 규칙의 단순화를 위해서 막아왔었다고 한다.
만약 인터페이스에 새롭게 메서드를 추가한다면? 해당 인터페이스를 구현하는 클래스에서 해당 메서드를 하나씩 구현하도록 해야한다. 여간 귀찮은 일이아니다. 따라서 JDK 1.8이후로 디폴트 메서드는 구현되지 않아도 되도록 바뀌었다.
- 여러 인터페이스의 디폴트 메서드 간의 충돌은 클래스에서 디폴트 메서드를 오버라이딩 해야한다.
- 디폴트 메서드와 조상 클래스의 메서드간 충돌의 경우, 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.