07. 추상 클래스 & 인터페이스
1. 인터페이스
인터페이스가 가진 모든 것들을 재정의하여 사용할 것을 강력하게 강제하고자 할 때, 사용한다.
1) 인터페이스는 동적 바인딩이 자동으로 이루어진다.
- 왜냐하면, 인터페이스의 모든 메서드는 abstract이므로, 이를 구현하는 클래스에서 반드시 오버라이딩해야 한다.
- 그로 인해, 인터페이스 타입의 객체에서 메서드를 호출해도, 결국 구현 클래스의 오버라이딩된 메서드가 실행된다. (덮어쓰기 개념)
2) "인터페이스 참조변수 = new 구현 클래스();"
- 위 방식으로 객체를 생성하면, 별도의 캐스팅 없이도 구현 클래스의 오버라이딩된 메서드를 호출할 수 있다.
- 단, 구현 클래스의 인터페이스 메서드만 호출 가능하며, 구현 클래스의 독립적인 메서드는 호출할 수 없다.
interface Animal {
void makeSound();
}
class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("멍멍!");
}
}
public class Main {
public static void main(String[] args) {
Animal myPet = new Dog(); // 인터페이스 타입으로 객체 생성
myPet.makeSound(); // "멍멍!" 출력 (동적 바인딩)
}
}
✅ 언제 인터페이스에서 상수를 사용할까?
1️⃣ 여러 클래스에서 공통적으로 사용되는 값이 있을 때
- 예를 들어, 상태 코드, 설정 값, 단위 같은 변하지 않는 값들을 인터페이스에 정의하면 좋음.
2️⃣ 상수를 일관성 있게 사용하고 싶을 때
- 하드코딩을 방지하고, if문이나 switch문에서 가독성을 높일 수 있음.
3️⃣ 인터페이스를 구현하는 모든 클래스에서 해당 상수를 참조할 때
- 따로 implements를 하지 않아도, 인터페이스명.상수명으로 접근 가능!
2. 추상 클래스
부모 클래스가 가진 일부의 기능들을 특정하여 재정의 해 사용하게끔 할 때, 사용한다.
1) 추상 클래스는 업캐스팅으로 동적바인딩을 구현한다.( "부모클래스 참조변수 = new 구현 자식 클래스();")
2) "부모클래스 참조변수 = new 구현 자식 클래스();"
위와 같이 생성하면 스택 영역에 부모 객체의 주소가 올라오고 힙 영역에 자식 객체가 생성이 되는데,
이 때, 부모 클래스의 주소를 활용하여 자식 클래스에서 필요한 것만을 호출하기 위한 것이다.
이것을 동적 바인딩이라고 한다.
a. "(자식클래스)참조변수.(자식메서드)"와 같이 다운캐스트하여 사용할 때
-> 자식 클래스에서 독립적인 메서드를 생성한 경우이다.
-> 다른 방식으로, foreach-instanceof 구문을 활용하여 확인 후 호출하기도 한다.
public class Main {
public static void main(String[] args) {
Parent[] arr = { new ChildA(), new ChildB(), new Parent() }; // 부모 타입 배열
for (Parent p : arr) {
if (p instanceof ChildA) {
((ChildA) p).specialMethodA(); // ChildA 타입일 경우 실행
} else if (p instanceof ChildB) {
((ChildB) p).specialMethodB(); // ChildB 타입일 경우 실행
} else {
p.show(); // 부모 클래스 메서드 실행
}
}
}
}
b. 그 외, 자식 객체에서 오버라이딩한 메서드들은 부모 객체에서 덮어쓰기 되어버려 캐스트하지 않아도
자식 메서드가 바로 호출된다.
🔹 초간단 요약 정리
✔ 인터페이스(스케치) → 기능(메서드)만 선언하고, 내부 로직은 없음
✔ 추상 클래스(정의) → 일부 구현이 포함될 수 있으며, 공통된 로직 제공 가능
✔ 메인 클래스(구현) → 추상 클래스를 상속받거나 인터페이스를 구현해서 최종 기능 완성
가급적이면 루트 클래스에 implements를 하는 것이 코드 재사용을 방지하고 유지보수하기 편하다.