Java 객체지향 심화 - 상속, 다형성, 추상화
1. 상속 (Inheritance)
상속은 부모 클래스의 필드와 메서드를 자식 클래스가 물려받는 것이다. extends 키워드를 사용한다.
// 부모 클래스 (슈퍼클래스)
class Animal {
String name;
int age;
void eat() {
System.out.println(name + "이(가) 먹이를 먹습니다.");
}
void sleep() {
System.out.println(name + "이(가) 잠을 잡니다.");
}
}
// 자식 클래스 (서브클래스)
class Dog extends Animal {
String breed;
void bark() {
System.out.println(name + "이(가) 멍멍 짖습니다!");
}
}
class Cat extends Animal {
void meow() {
System.out.println(name + "이(가) 야옹~ 합니다.");
}
}
public class InheritanceExample {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "바둑이"; // 부모로부터 상속
dog.age = 3; // 부모로부터 상속
dog.breed = "진돗개"; // 자신의 필드
dog.eat(); // 부모 메서드
dog.sleep(); // 부모 메서드
dog.bark(); // 자신의 메서드
}
}
상속의 특징
- 자식 클래스는 부모 클래스의 메서드를 선언 없이 사용할 수 있다
- 자식 클래스의 메서드는 부모 클래스가 사용할 수 없다
- 자식 클래스끼리는 메서드 공유가 불가능하다
상속의 장점
- 코드 재사용성 향상
- 유지보수 용이
- 계층적 구조 표현
2. 메서드 오버라이딩 (Overriding)
부모 클래스의 메서드를 자식 클래스에서 재정의하는 것이다.
class Animal {
String name;
void sound() {
System.out.println("동물이 소리를 냅니다.");
}
}
class Dog extends Animal {
@Override // 오버라이딩 어노테이션 (선택이지만 권장이라고 함..)
void sound() {
System.out.println(name + ": 멍멍!");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println(name + ": 야옹~");
}
}
public class OverridingExample {
public static void main(String[] args) {
Animal animal = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
dog.name = "바둑이";
cat.name = "나비";
animal.sound(); // 동물이 소리를 냅니다.
dog.sound(); // 바둑이: 멍멍!
cat.sound(); // 나비: 야옹~
}
}
오버로딩 vs 오버라이딩
| 구분 | 오버로딩 (Overloading) | 오버라이딩 (Overriding) |
|---|---|---|
| 위치 | 같은 클래스 | 상속 관계 |
| 메서드명 | 동일 | 동일 |
| 매개변수 | 다름 | 동일 |
| 반환타입 | 상관없음 | 동일 |
3. super 키워드
super는 자식 클래스에서 부모 클래스를 참조할 때 사용하는 키워드다.
super로 부모 필드/메서드 접근
class Parent {
String name = "부모";
void show() {
System.out.println("Parent의 show()");
}
}
class Child extends Parent {
String name = "자식"; // 부모와 같은 이름의 필드
@Override
void show() {
System.out.println("Child의 show()");
}
void display() {
System.out.println("this.name: " + this.name); // 자식
System.out.println("super.name: " + super.name); // 부모
this.show(); // Child의 show()
super.show(); // Parent의 show()
}
}
super() 생성자
부모 클래스의 생성자를 호출할 때 사용한다. 왜 가능한지는 모르지만 private이 아닌 경우에는 자식클래스에서 부모 클래스의 필드에 대해 조회/변경이 모두 가능하다.
class Animal {
String name;
Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
String breed;
Dog(String name, String breed) {
super(name); // 부모 생성자 호출
this.breed = breed;
}
}
중요:
super()는 반드시 생성자의 첫 줄에 위치해야 한다!
4. 다형성 (Polymorphism)
부모 타입으로 자식 객체를 참조할 수 있다. 이를 통해 같은 타입으로 다양한 객체를 다룰 수 있다.
public class PolymorphismExample {
public static void main(String[] args) {
// 다형성: 부모 타입으로 자식 객체 참조
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.sound(); // 멍멍! (Dog의 오버라이딩된 메서드)
animal2.sound(); // 야옹~ (Cat의 오버라이딩된 메서드)
// 배열로 여러 타입 관리
Animal[] animals = {new Dog(), new Cat(), new Cow()};
for (Animal a : animals) {
a.sound(); // 각 객체의 오버라이딩된 메서드 호출
}
}
}
5. 추상 클래스와 인터페이스
추상 클래스 (Abstract Class)
미완성 메서드를 포함한 클래스로, 인스턴스를 생성할 수 없다.
abstract class Shape {
String color;
// 추상 메서드 (구현부 없음)
abstract double getArea();
// 일반 메서드 (구현부 있음)
void setColor(String color) {
this.color = color;
}
}
class Circle extends Shape {
double radius;
@Override
double getArea() {
return Math.PI * radius * radius;
}
}
인터페이스 (Interface)
추상 메서드의 집합으로, 클래스가 반드시 구현해야 할 기능을 정의한다.
interface Flyable {
void fly(); // 추상 메서드 (public abstract 생략)
default void land() { // default 메서드
System.out.println("착륙합니다.");
}
static void info() { // static 메서드
System.out.println("Flyable 인터페이스");
}
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("새가 날아갑니다.");
}
}
추상 클래스 vs 인터페이스
| 구분 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 다중 상속 | 불가능 | 가능 (implements) |
| 변수 | 모든 종류 가능 | public static final만 |
| 메서드 | 추상/일반 모두 가능 | 추상 메서드 (default, static 가능) |
| 목적 | “~이다” (is-a) | “~할 수 있다” (can-do) |
추상 클래스
- 추상 클래스를 상속 받는 자식 클래스마다 반드시 동작이 달라지는 경우에 추상 클래스를 사용한다.
- 추상 클래스에는 일반 메서드와 추상 메서드가 포함될 수 있다.
- 추상 메서드는 시그니처만 있고 구현은 되어있지 않다.
- 일반 메서드는 구현해야하고, 추상 메서드만 구현하지 않는다.
인터페이스
- 어떤 대상들 사이에서 소통하는 역할을 하는 클래스
- 객체가 행동할 내용은 구현 클래스에 맡긴다.
- 동작의 명세만을 작성해서 구현하는 쪽이 어떤 동작을 구현해야하는지 알 수 있다.
- 추상 메서드만 작성할 수 있다.
- 여러 인터페이스를 상속해서 다중 구현이 가능하다.
왜 두개를 나누어 두었을까?
추상화, 캡슐화의 관점에서 봤을때 인터페이스가 추상 클래스보다 유연하다. 상속은 사실 이해하려면 부모 클래스와 자식 클래스를 모두 이해해야한다. 근데 추상 클래스는 구현체만 이해하면 됨. 그리고 부모 클래스의 변경이 자식 클래스에게 영향을 주기때문에 독립성의 부분에서 봤을때도 추상 클래스가 더 객체 지향적이라고 할 수 잇다. 상속은 상속을 받는 순간 명세가 바뀌지 않더라도 영향을 주는 것임. 동작에 대한 명세를 하고 싶으면 인터페이스를 사용하는 걸 추천한다.
default와 static의 차이
인터페이스에서:
- default: 객체가 있어야 호출 가능, 오버라이딩 가능
- static: 객체 없이 호출 가능, 오버라이딩 불가능
interface MyInterface {
default void defaultMethod() {
System.out.println("default 메서드");
}
static void staticMethod() {
System.out.println("static 메서드");
}
}
class MyClass implements MyInterface {
@Override
public void defaultMethod() { // 오버라이딩 가능
System.out.println("오버라이딩된 default 메서드");
}
// static 메서드는 오버라이딩 불가!
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.defaultMethod(); // 객체 필요
MyInterface.staticMethod(); // 객체 없이 호출
}
}