Jasontreks Blog

DM 보내기


Send

상속

상속은 상위 클래스의 필드와 메소드를 하위 클래스에게 물려주는 것이다. 상위 클래스는 하위 클래스보다 더 추상적이며 공통적인 성질을 가지고있으며, 이를 하위 클래스가 물려받아 구체화하는 식으로 코드를 작성하기 위해 상속을 이용한다.

상속은 주는 장점은 다음 세 가지가 있다.

  • 클래스의 간결화: 멤버의 중복 작성 불필요
  • 클래스 관리 용이: 클래스의 계층적 분류
  • 소프트웨어 생산성 향상: 클래스 재사용과 확장 용이

I. 상속 선언과 객체 생성

extends 키워드를 이용해 상속받는 자식 클레스를 선언할 수 있다.

class Person {
	String name;
}
class Student extends Person {
	int stdnum;
}

그리고 서브 클래스인 Student로 만든 객체에서는 자신의 멤버인 stdnum은 물론 불려받은 name 멤버까지 사용할 수 있다.

public static void main(String[] args) {
	Student s = new Student();
	s.name = "Jason"; // 슈퍼 클래스 Person의 필드
	s.stdnum = 101;   // 서브 클래스 Student의 필드
}

또한 서브 클래스 안에서도 슈퍼 클래스의 멤버를 마치 자신의 멤버처럼 this 키워드로 접근할 수 있다.

class Person {
	String name;
}
class Student extends Person {
	int stdnum;
	public Student(String name) {
		this.name = name; // 슈퍼 클래스 Person의 멤버 접근
		this.stdnum = this.hashCode();
	}
}

자바 상속의 특징

자바에서 상속은 다음과 같은 특징을 가진다.

  • 다중 상속을 지원하지 않음. extends 다음에는 클래스 이름이 하나만 올 수 있음.
  • 상속의 횟수에 제한이 없음.
  • 자바에서는 계층 구조의 최상위에 java.lang.Object 클래스가 있음.

상속과 생성자

슈퍼 클래스도, 서브 클래스도 모두 각각의 생성자를 가지고 있다. 서브 클래스 객체가 생성될 때는 이 모든 생성자들이 순서대로 호출된다.

class A {
	public A() { System.out.println("A 생성자 호출"); }
}

class B extends A {
	public B() { System.out.println("B 생성자 호출"); }
}

class C extends B {
	public C() { System.out.println("C 생성자 호출"); }
}
public class Main {
	public static void main(String[] args) {
		C c = new C();
	}
}
A 생성자 호출
B 생성자 호출
C 생성자 호출

graph TD
C -->|B 생성자 호출| B
B -->|A 생성자 호출| A
A --> B
B --> C

서브 클래스 객체 생성 시 슈퍼 클래스에 여러 생성자가 있는 경우, 원칙적으로는 슈퍼 클래스의 어떤 생성자를 함께 호출할지 명시적으로 지정해야 한다. 이 때 슈퍼클래스 생성자인 super()를 이용한다.

class Circle {
	int radius;
	public Circle() { }
	public Circle(int r) {
		this.radius = r;
	}
}

class Wheel extends Circle {
	String material;
	public Wheel(int r, String mat) {
		super(r); // 슈퍼 클래스의 생성자 호출
		this.material = mat;
	}
}
public class Main {
	public static void main(String[] args) {
		Wheel w = new Wheel(10, "wood");
		System.out.println(w.radius);
		System.out.println(w.material);
	}
}
10
wood

super()로 슈퍼 클래스의 생성자를 지정하지 않으면 기본 생성자가 호출된다.

class Wheel extends Circle {
	String material;
	public Wheel(int r, String mat) {
		// super(r); 슈퍼 클래스 생성자 호출 안함
		this.material = mat;
	}
}
0
wood

II. 캐스팅

캐스팅은 타입 변환을 말한다. 자바에서 클래스에 대한 캐스팅은 업캐스팅과 다운캐스팅으로 나뉜다.

업캐스팅

서브 클래스 객체에 대한 레퍼런스를 슈퍼 클래스 타입으로 변환하는 것. 위로 거슬러 올라가는 형태다.

Person p;
Student s = new Student();
p = s; // 업캐스팅

압케스팅한 레퍼런스로는 서브 클래스의 멤버에 접근하지 못하고 슈퍼 클래스의 멤버에만 접근할 수 있다. 또한 업캐스팅은 p = (Person)s와 같이 명시적인 캐스팅이 필요하지 않다.

다운캐스팅

다운캐스팅은 업캐스팅의 반대로, 슈퍼 클래스 객체에 대한 레퍼런스를 서브 클래스 타입으로 바꾸는 것이다.

Person p = new Person();
Student s;
s = (Student)p; // 다운캐스팅

다운캐스팅은 명시적 캐스팅이 필요하다.

instanceof

instanceof 연산자는 업캐스팅된 객체가 실제로 어느 서브 클래스이 객체인지 알기 위해 사용하는 연산자이다.

다음과 같은 클래스들이 있다고 해보자.

class Person {	
}
class Student extends Person {
}
class Teacher extends Person {
}

public class Main {
	public static void main(String[] args) {
		Person p1 = new Student();
		Person p2 = new Teacher();
	}
}

p1과 p2는 실제로 Person 클래스의 객체인지, 아니면 이를 상속받은 StudentTeacher 클래스의 객체인지, 코드만 보고는 구분이 어렵다. 그래서 다음과 같이 instanceof 연산자를 사용하면 이렇게 어떤 서브 클래스로부터 업캐스팅된것인지 알 수 있다.

System.out.println(p1 instanceof Person);
System.out.println(p1 instanceof Student);
System.out.println(p1 instanceof Teacher);
true
true
false

III. 오버라이딩

오버라이딩은 슈퍼 클래스에 이미 작성된 메소드를, 그를 상속한 서브 클래스에서 재작성하는 것이다. 슈퍼 클래스에 존재하는 기능과 같은 맥락의 기능임을 명시하되, 작업 내용은 달리 해야할 때 오버라이딩을 한다.

class Shape {
	public void draw() {
		System.out.println("Shape");
	}
}
class Circle extends Shape {
	// 오버라이딩
	@Override
	public void draw() {
		System.out.println("Circle");
	}
}
class Rect extends Shape {
	// 오버라이딩
	@Override
	public void draw() {
		System.out.println("Rect");
	}
}
Rect r = new Rect();
r.draw();
Circle c = new Circle();
c.draw();
Rect
Circle

즉 오버라이딩은 슈퍼 클래스의 원본 함수를 서브 클래스에서 덮어쓰는 것으로 볼 수 있다.

@Override 어노테이션의 필요성

오버라이딩 함수 위에 붙이는 @Override 어노테이션은 필수는 아니지만, 있으면 컴파일러가 오버라이딩 함수임을 알 수 있어 오타와 같은 문법 오류를 미리 잡아주기도 하며, 코드에 대한 직관성도 높이므로 실무에서는 항상 사용된다.

동적 바인딩

위 코드에서, Rect나 Circle로 레퍼런스 변수를 생성하지 않고 Shape로 생성한 뒤 업캐스팅하면 어떻게 될까?

Shape r = new Rect();
r.draw();
Shape c = new Circle();
c.draw();
Rect
Circle

결과는 같다. RectCircle 객체를 Shape 타입의 레퍼런스로 가리키더라고 draw() 메소드 호출 시 오버라이딩 된 내용이 존재하면 그 서브 클래스의 draw()를 호출한다. 이것을 동적 바인딩이라고 부른다. 동적 바인딩은 어떤 메소드를 실행할 지 컴파일시에 정해지징 않고 런타임 시에 정해진다.

하지만 서브 클래스에서 오버라이딩된 메소드를 실행하지 않고 꼭 슈퍼 클래스의 원본 메소드를 실행해야 하는 경우가 있다면 super를 이용할 수 있다.

class Shape {
	public void draw() {
		System.out.println("Shape");
	}
}
class Circle extends Shape {
	@Override
	public void draw() {
		super.draw(); // 슈퍼 클래스의 draw() 호출 => 정적 바인딩
	}
}
this와 super의 차이
  • this.객체내멤버
  • super.객체내슈퍼클래스멤버

오버리이딩 특징

  • 오버라이딩은 다형성 실현에 목적을 둔다.
  • 슈퍼 클래스와 동일한 형태로 선언한다. 내용만 날라야 한다.
  • 슈퍼 클래스의 접근 지정자보다 접근 범위를 좁일 수 없다.
  • static, private, final로 선언된 메소드는 오버라이딩이 불가능하다.

IV. 추상화

추상 메소드는 코드가 구현되어있지 않고 선언만 있는 껍데기에 불과한 메소드를 말하며, 이런 추상 메소드를 포함하는 클래스를 추상 클래스라고 한다.

추상 메소드를 선언할 땐 abstract키워드와 함께 쓴다.

public abstract String getName();
public abstract void setName(String s);

추상 클래스

추상 메서드를 포함하는 클래스는 추상 클래스여야 한다. 이때 클래스 역시 abstract 키워드와 함께 쓰인다.

abstract class Shape {
	public void paint() { draw(); }
	abstract public void draw(); // 추상 메소드 선언
}

추상 클래스의 특징

  • 추상 클래스는 객체를 생성할 수 없다. 추상 클래스는 그 자체로 객체를 생성하려고 만드는 클래스가 아니라, 그것을 상속한 서브 클래스에서 추상 메소드를 오버라이딩하여 사용하기 위해 만든다.
  • 추상 클래스는 서브 클래스가 어떤 기능을 구현해야 하는지 명료하게 알려주는 설계도와 같은 역할을 한다. 이는 다형성 실현에 기여한다.
  • 설계와 구현을 분리할 수 있다. 추상 클래스를 통해 개발의 방향을 미리 잡아놓고 서브 클래스를 만들어나는 체계적인 구현 작업을 할 수 있다.

V. 인터페이스

인터페이스는 추상 클래스보다 더 상위 단계로, 클래스가 구현해야 할 기능 명세서와 같다. 인터페이스는 다음 요소들로 구성할 수 았다.

  • 상수: public static final로 고정
  • 추상 메서드: public abstract로 고정
  • default 메서드: public default로 고정
  • private 메서드: 인터페이스 내에서만 호출 가능
  • static 메서드: 접근 지정자 생략 시 public, 또는 private으로 지정 가능

인터페이스의 필드는 오로지 상수만 올 수 있으며, default, private, static 메서드는 반드시 코드가 구현되어 있어야 한다. 또한 인터페이스에 선언된 추상 메서드는 이를 구현받는 클래스에서 반드시 오버라이딩을 해야 한다.

인터페이스 특징

  • 인터페이스의 필드는 오로지 상수만 올 수 있다.
  • default, private, static 메서드는 반드시 코드가 구현되어 있어야 한다.
  • 인터페이스에 선언된 추상 메서드는 이를 구현받는 클래스에서 반드시 오버라이딩을 해야 한다.
  • 인터페이스는 객체를 생성할 수 없다. 하지만 레퍼런스 변수는 선언이 가능하다.(추상 클래스와 동일)
  • 인터페이스끼리 상속된다. 추상 클래스와 달리 다중 상속이 가능하다.

인터페이스 구현

인터페이스를 클래스가 이어받아 사용하는것을 인터페이스 구현이라고 부른다. 아래는 인터페이스 구현 예제이다.

interface Computer {
	// 인터페이스의 추상 메소드
	public static final int powerwatt = 1000;
	public abstract void playMusic();
	public abstract void runBrowser();
	public default void powerOff() {
		System.out.println("bye");
	}
}

class MacComputer implements Computer {
	@Override // 필수 구현
	public void playMusic() {
		System.out.println("apple music");
	}
	@Override // 필수 구현
	public void runBrowser() {
		System.out.println("safari");
	}
}
public class Main {
	public static void main(String[] args) {
		MacComputer mac = new MacComputer();
		System.out.println(mac.powerwatt);
		mac.playMusic();
		mac.runBrowser();
		mac.powerOff();
	}
}
1000
apple music
safari
bye

인터페이스 상속

인터페이스끼리도 상속이 가능하며, 추상 클래스와 다르게 다중 상속도 가능하다.

interface Computer {
	...
}

interface Internet {
	...
}
// 여러 인터페이스를 상속
interface SmartPhone extends Computer, Internet {
	...
}

다중 인터페이스

클래스에는 인터페이스를 여러 개 구현할 수 있다.

interface Computer {
	int powerwatt = 1000;
    void playMusic();
    void runBrowser();
    default void powerOff() {
        System.out.println("bye");
    }
}

// 카메라 인터페이스 (새로운 기능)
interface Camera {
    void takePhoto();
    void recordVideo();
}

// 통화 인터페이스 (새로운 기능)
interface Phone {
    void makeCall(String number);
}

// [다중 구현] MacComputer는 컴퓨터이자, 카메라며, 전화기이다.
class MacComputer implements Computer, Camera, Phone {
	...
}
인터페이스에서 생략 가능한 것들
  • 추상 메소드: public abstract 생략 가능 -> void playMusic();
  • 상수 필드: public static final 생략 가능 -> int powerwatt;
  • default 메소드: public 생략 가능 -> default void powerOff();

이미지에 있는 추상 클래스인터페이스의 비교 표를 마크다운 형식으로 정리해 드립니다.

추상 클래스 vs 인터페이스 비교

비교목적구성
추상 클래스추상 클래스는 서브 클래스에서 필요로 하는 대부분의 기능을 구현하여 두고 서브 클래스가 상속받아 활용할 수 있도록 하되, 서브 클래스에서 구현할 수밖에 없는 기능만을 추상 메소드로 선언하여, 서브 클래스에서 구현하도록 하는 목적(다형성)추상 메소드와 일반 메소드 모두 포함. 상수, 변수 필드 모두 포함
인터페이스인터페이스는 객체의 기능을 모두 공개한 표준화 문서와 같은 것으로, 개발자에게 인터페이스를 상속받는 클래스의 목적에 따라 인터페이스의 모든 추상 메소드를 만들도록 하는 목적(다형성)변수 필드(멤버 변수)는 포함하지 않음. 상수, 추상 메소드, 일반 메소드, default 메소드, static 메소드 모두 포함. protected 접근 지정 선언 불가. 다중 상속 지원