객체 지향 프로그래밍의 기본 아이디어 중 하나로, 객체들 간의 관계를 구축하는 방법이다
고전적으론 기존에 있던 클래스로부터 속성과 동작을 상속받아 새로운 클래스를 만드므로서, 객체 간 관계를 설정하고, 코드의 중복을 최소화 할 수 있다.
아래는 클래스 간 관계를 표현할 때 자주 사용하는 클래스 다이어그램이다. SUV와 SEDAN 클래스는 Car 클래스의 모든 속성과 동작을 가지고, 추가로 새로운 속성과 동작을 가진다.
class Car {
var mSpeed;
void accelerate(float value) {...}
void break(float value) {...}
}
class SUV extends Car {
boolean misOffRoadMode;
void setOffRoadMode(boolean mode) {...}
boolean isOffRoadMode() {...}
}
class SEDAN extends Car {
int mDrivingMode;
void setDrivingMode(int mode) {...}
}
extends 예약어를 통해 상속을 한다
다중 클래스의 상속을 지원하지 않는다
부모 클래스의 생성자는 상속되지 않는다
private 접근 지정자를 가진 변수나 메소드가 있다면, 변수는 상속 받으나 바로 접근이 불가능하고, 메소드는 상속되지 않는다
정적 변수 및 메소드도 상속이 된다
Object 클래스가 최상위 클래스이며, 유일하게 Object 클래스만 부모 클래스를 가질 수 없다
super : 부모 클래스를 가리키는 참조 변수
super.variable 또는 super.method()와 같이 부모 클래스의 멤버에 접근 가능하다
super()는 부모 클래스의 생성자를 호출한다. 생성자 내에서 사용하려면, this()와 마찬가지로 생성자의 맨 첫줄에 작성해야한다
자식 클래스의 인스턴스는 부모 클래스의 멤버까지 포함하고 있기 때문에, 부모 클래스의 생성자까지 호출해야한다. 모든 클래스는 Object 클래스의 자식 클래스이기 때문에, Object 클래스의 생성자까지 계속 거슬러 올라가 호출한다. 생성자에 super()가 호출되지 않으면, 컴파일러가 자동으로 생성자 첫줄에 추가한다. 인자를 받지 않는 기본 생성자도 신경써야하는 이유가 여기에 있다. 만약 기본 생성자가 없다면, 해당 클래스를 상속하는 클래스에서 super()를 호출할 경우 오류가 발생하기 때문이다.
// 오류 발생
class Parent {
int a;
Parent(int n) { a = n; }
}
class Child extends Parent {
int b;
Child() {
super();
b = 20;
}
// 정상 작동
class Parent {
int a;
Parent() { a = 10; }
Parent(int n) { a = n; }
}
class Child extends Parent {
int b;
Child() {
super();
b = 20;
}
OOP의 다형성을 구현하기 위한 방법 중 하나로 자식 클래스에서 동일한 시그니쳐를 같는 메소드를 재정의 해서 다르게 작동하게 하는 것
메소드의 선언부는 기존 메소드와 완전히 동일해야한다. 단 메소드의 반환 타입이 부모 클래스의 반환 타입으로 변환될 수 있다면, 변경할 수 있다
부모 클래스의 메소드보다 좁은 범위의 접근 제어자로 변경할 수 없다
부모 클래스의 메소드보다 더 큰 범위의 예외를 선언할 수 없다
class Parent {
void smth() {...}
}
class Child {
@Override
void smth() {...}
}
@Override
애너테이션은 컴파일러에게 해당 메소드가 부모 클래스의 메소드를 오버라이딩한다는 것을 알려준다. 애너테이션을 달지 않아도 오버라이딩은 가능하지만, 가독성 측면에서나 컴파일 타임 검증 및 오류 확인한다는 점에서 쓰는게 좋다.
오버라이딩 + 업캐스팅으로 실행시간 다형성 구현
class Car {
void printType() {
System.out.println("자동차");
}
}
class SEDAN extends Car {
@Override
void printType() {
System.out.println("세단");
}
}
class SUV extedns Car {
@Override
void printType() {
System.out.println("SUV");
}
}
public class Main {
public static void main(String[] args) {
Car ref = new Car();
ref.printType // 자동차
// 업캐스팅
ref = new SEDAN();
ref.printType // 세단
// 업캐스팅
ref = new SUV();
ref.printType // SUV
}
}
다이나믹 메소드 디스패치가 중요한 이유는 클라이언트 --> 서플라이어 의존관계의 클라이언트 클래스의 재사용성을 높이기 위해서이다. 서플라이어를 클래스가 아닌 인터페이스로 하므로써 재사용성을 높일 수 있는데 List<Integer> list = new ArrayList<Integer>();
처럼 흔히 인터페이스 참조변수로 사용하는 이유이기도 하다.
추상 메소드를 하나 이상 가진 클래스
abstract class Calculator {
public abstract int add(int a, int b);
public abstract int subtract(int a, int b);
public abstract double average(int[] a);
}
public class GoodCalc extends Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int subtract(int a, int b) {
return a - b;
}
@Override
public double average(int[] a) {
double sum = 0;
for (int i = 0; i <a.length; i++)
sum += a[i];
return sum/a.length;
}
}
abstract class Animal {
abstract void sound();
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("멍멍");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("야옹");
}
}
final class FinalClass {
...
}
class SubClass extends FinalClass { // 컴파일 오류
...
}
class SuperClass {
final int finalMethod() {...}
}
class SubClass extends Superclass {
@Override // 컴파일 오류
int finalMethod() {...}
}
class SharedClass {
final double PI = 3.14;
}
부모를 가지지 않는 최상위 클래스
class Dog {
@Override
public String toString() {
return "Dog 객체" + '@' + Integer.toHexString(hashCode());
}
}
==
로 객체를 비교하면 동일한 객체인지를 확인하기 때문에 값을 비교하기 위해선 equals 메소드를 사용해야한다class Student {
String name;
Student (String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
Student _obj = (Student)obj;
return name == _obj.name;
}
}