| 구분 | 추상 클래스 (abstract class) | 인터페이스 (interface) |
|---|---|---|
| 사용 키워드 | abstract | interface |
| 변수(필드) 사용 | 일반 변수(인스턴스 변수), static 변수, final 변수 모두 가능 | public static final(상수)만 가능 (자동 적용) |
| 메서드 사용 가능 여부 | 일반 메서드 (구현 O), abstract 메서드 (구현 X) | abstract 메서드 (자동 public abstract), default 메서드 (자바 8부터), static 메서드 (자바 8부터), private 메서드 (자바 9부터) |
| 사용 가능한 접근 제어자 | public, protected, default, private (제한 없음) | public (모든 메서드와 변수에 자동 적용) |
| 상속 키워드 | extends (클래스를 상속) | implements (클래스가 인터페이스 구현), extends (인터페이스 간 다중 상속) |
| 다중 상속 가능 여부 | 불가능 (extends로 하나의 부모 클래스만 상속 가능) | 가능 (인터페이스끼리 다중 상속 가능, 클래스에서 다중 구현 가능) |
| 생성자 사용 가능 여부 | 생성자 사용 가능 | 생성자 사용 불가능 |
| 객체 생성 가능 여부 | 단독으로 객체 생성 불가능 (추상 클래스이므로) | 단독으로 객체 생성 불가능 (규약만 정의) |
| 사용 목적 | 공통된 속성과 동작을 가진 클래스의 “부모 역할” → 코드 재사용성을 높이기 위해 사용됨 | 클래스 간의 “규약(프로토콜)“을 정의 → 클래스 간 결합도를 낮추고 확장성을 높이기 위해 사용됨 |
abstract class)super()를 통해 부모 생성자를 호출할 수 있습니다.static 메서드를 선언할 수 있습니다.extends 키워드를 사용하여 상속할 수 있습니다.abstract class Animal {
String name; // 인스턴스 변수 가능
// 생성자 사용 가능
public Animal(String name) {
this.name = name;
}
// 일반 메서드 (구현 O)
public void eat() {
System.out.println(name + "가 음식을 먹습니다.");
}
// 추상 메서드 (구현 X, 반드시 자식 클래스에서 오버라이딩 필요)
public abstract void makeSound();
}
// 자식 클래스에서 추상 메서드 구현 필수
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println("멍멍!");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("바둑이");
dog.eat(); // "바둑이가 음식을 먹습니다."
dog.makeSound(); // "멍멍!"
}
}
interface)public static final 상수입니다.abstract 메서드를 포함합니다.default, static, private 메서드도 포함할 수 있습니다.implements하여 “다중 구현”이 가능합니다.extends를 사용하여 “다중 상속”이 가능합니다.implements 키워드를 사용하여 해당 규약을 따라야 합니다.static 메서드는 인터페이스 자체에서 호출할 수 있으며, 구현 클래스에서 오버라이딩할 수 없습니다.private 메서드는 인터페이스 내부에서만 사용할 수 있으며, default 또는 static메서드에서 호출 가능합니다.interface Animal {
// 추상 메서드 (자동 public abstract 적용)
void makeSound();
// 자바 8 이후 `default` 메서드 (구현 가능)
default void eat() {
System.out.println("음식을 먹습니다.");
}
// 자바 8 이후 `static` 메서드 (구현 가능)
static void info() {
System.out.println("이것은 Animal 인터페이스입니다.");
}
}
// 인터페이스를 구현하는 클래스
class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("멍멍!");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.makeSound(); // "멍멍!"
dog.eat(); // "음식을 먹습니다."
Animal.info(); // "이것은 Animal 인터페이스입니다."
}
}
| 사용 조건 | 추상 클래스 (abstract class) | 인터페이스 (interface) |
|---|---|---|
| 클래스의 공통적인 속성과 동작을 정의해야 하는 경우 | 적합 (변수 + 메서드 포함 가능) | 부적합 (상태 저장 불가) |
| 공통적인 행동(메서드)을 강제하고 싶을 경우 | 가능 | 가능 |
| 여러 개의 부모를 가져야 하는 경우 | 불가능 (단일 상속) | 가능 (다중 구현 가능) |
| 객체의 상태(변수)를 저장해야 하는 경우 | 가능 | 불가능 (static final 상수만 가능) |
| 코드 재사용성을 높이고 싶은 경우 | 가능 (일반 메서드 제공) | 구현 클래스에서 직접 구현해야 함 |
인터페이스는 클래스가 따라야 할 규약(프로토콜)을 정의합니다.
즉, 구현 방식은 신경쓰지 않고, "어떤 기능을 제공해야 하는지"를 명확하게 지정하는 것이 목적입니다.
interface DataLoader {
void loadData(); // 데이터를 로드하는 규약 정의
}
class FileDataLoader implements DataLoader {
@Override
public void loadData() {
System.out.println("파일에서 데이터를 로드합니다.");
}
}
class DatabaseDataLoader implements DataLoader {
@Override
public void loadData() {
System.out.println("데이터베이스에서 데이터를 로드합니다.");
}
}
public class Main {
public static void main(String[] args) {
DataLoader fileLoader = new FileDataLoader();
DataLoader dbLoader = new DatabaseDataLoader();
fileLoader.loadData(); // "파일에서 데이터를 로드합니다."
dbLoader.loadData(); // "데이터베이스에서 데이터를 로드합니다."
}
}
interface Movable {
void move();
}
class Car implements Movable {
@Override
public void move() {
System.out.println("자동차가 도로를 달립니다.");
}
}
class Robot implements Movable {
@Override
public void move() {
System.out.println("로봇이 이동합니다.");
}
}
public class Main {
public static void main(String[] args) {
Movable m1 = new Car();
Movable m2 = new Robot();
m1.move(); // "자동차가 도로를 달립니다."
m2.move(); // "로봇이 이동합니다."
}
}
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Duck implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("오리가 납니다.");
}
@Override
public void swim() {
System.out.println("오리가 헤엄칩니다.");
}
}
public class Main {
public static void main(String[] args) {
Duck duck = new Duck();
duck.fly(); // "오리가 납니다."
duck.swim(); // "오리가 헤엄칩니다."
}
}
Comparable<T> 인터페이스를 구현하면, 어떤 클래스든 비교 기능을 가질 수 있습니다.Comparable<T> 인터페이스를 사용하면, 특정한 비교 기준을 정의할 수 있습니다.class Person implements Comparable<Person> {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age); // 나이 기준 정렬
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Bob", 25);
System.out.println(p1.compareTo(p2)); // 1 (p1이 p2보다 나이가 많음)
}
}
Comparable을 구현함으로써, 객체 간 비교가 가능해졌습니다.Comparable<Person>을 구현했기 때문에, compareTo() 메서드를 반드시 포함해야 합니다.compareTo()가 “나이(age)를 기준으로 비교할지, 이름(name)을 기준으로 비교할지는” 각 클래스에서 결정할 수 있습니다.run() 메서드를 가지게 됩니다.class MyTask implements Runnable {
@Override
public void run() {
System.out.println("작업이 실행됩니다.");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyTask());
thread.start(); // "작업이 실행됩니다."
}
}
Thread 객체를 통해 실행될 수 있습니다.인터페이스의 가장 큰 특징은 상속 구조와 상관없이 다중 구현이 가능하다는 점입니다.
extends는 논리적인 부모-자식 관계를 의미하지만, implements는 단순히 "이 기능을 지원한다"는 의미입니다.
따라서, 논리적으로 관련이 없는 클래스들도 필요에 따라 같은 인터페이스를 구현할 수 있습니다.
interface Movable {
void move();
}
class Car implements Movable {
@Override
public void move() {
System.out.println("자동차가 움직입니다.");
}
}
Class Animal implements Movable {
@Override
public void move() {
System.out.println("동물이 이동합니다.");
}
}
public class Main {
public static void main(String[] args) {
Movable m1 = new Car();
Movable m2 = new Animal();
m1.move(); // 자동차가 움직입니다.
m2.move(); // 동물이 이동합니다.
}
}
추상 클래스는 클래스 간 공통된 속성과 동작을 정의하고, 일부 메서드는 하위 클래스에서 구현하도록 강제하는 클래스입니다.
즉, 어떤 기능을 수행해야 하는지뿐만 아니라, 어떤 속성과 기본 동작을 제공할지도 결정할 수 있습니다.
abstract class Animal {
String name;
public Animal(String name) {
this.name = name;
}
// 모든 동물이 공통적으로 먹는 기능 제공 (구현 완료된 메서드)
public void eat() {
System.out.println(name + "가 음식을 먹습니다.");
}
// 동물마다 소리가 다르므로 추상 메서드로 선언 (하위 클래스에서 구현 필수)
public abstract void makeSound();
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println("멍멍!");
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println("야옹~");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("바둑이");
Cat cat = new Cat("나비");
dog.eat(); // "바둑이가 음식을 먹습니다."
dog.makeSound(); // "멍멍!"
cat.eat(); // "나비가 음식을 먹습니다."
cat.makeSound(); // "야옹~"
}
}
eat() 메서드와 같은 공통적인 동작을 미리 구현할 수 있습니다.makeSound()는 동물마다 다르므로 추상 메서드로 선언하여 하위 클래스에서 구현하도록 강제합니다.public 이외의 접근 제어자(protected, private)를 선언해야 할 때public static final(상수)로 선언되지만, 추상 클래스에서는 private, protected 접근 제어자를 사용할 수 있습니다.protected를 활용할 수 있습니다.abstract class Vehicle {
protected int speed; // 하위 클래스(자식 클래스)에서 접근 가능
public Vehicle(int speed) {
this.speed = speed;
}
// 현재 속도 출력 (공통 기능 제공)
public void showSpeed() {
System.out.println("현재 속도: " + speed + "km/h");
}
// 가속 기능 (하위 클래스에서 다르게 구현)
public abstract void accelerate();
}
class Car extends Vehicle {
public Car(int speed) {
// 자식 클래스의 생성자가 실행될 때, 부모 클래스의 생성자가 먼저 실행됨
super(speed); // 부모 클래스에서 정의된 필드(name)를 자식 클래스가 초기화
}
@Override
public void accelerate() {
speed += 10;
System.out.println("자동차가 가속합니다. 새로운 속도: " + speed + "km/h");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car(50);
car.showSpeed(); // 현재 속도: 50km/h
car.accelerate(); // 자동차가 가속합니다. 새로운 속도: 60km/h
}
}
protected 필드를 사용하면 하위 클래스에서는 접근 가능하지만, 외부에서는 직접 변경할 수 없습니다.static final(상수) 필드만 가능하지만, 추상 클래스에서는 일반적인 필드를 선언하고 값을 변경할 수 있습니다.즉, 객체의 상태를 유지하면서 동작을 정의할 때 추상 클래스가 적합합니다.
abstract class Car {
int speed = 0; // 인스턴스 변수 선언 가능
// 공통 기능 제공
public void showSpeed() {
System.out.println("현재 속도: " + speed + "km/h");
}
// 가속 기능 (하위 클래스에서 다르게 구현)
public abstract void accelerate();
}
class SportsCar extends Car {
@Override
public void accelerate() {
speed += 20;
System.out.println("스포츠카가 가속합니다. 새로운 속도: " + speed + "km/h");
}
}
public class Main {
public static void main(String[] args) {
SportsCar ferrari = new SportsCar();
ferrari.showSpeed(); // 현재 속도: 0km/h
ferrari.accelerate(); // 스포츠카가 가속합니다. 새로운 속도: 20km/h
}
}
공통적인 로직을 제공하면서도, 특정 동작을 하위 클래스에서 다르게 구현해야 할 때 추상 클래스를 사용합니다.
abstract class Game {
// 공통적인 게임 시작 로직 제공 (구현 완료)
public void start() {
System.out.println("게임을 시작합니다.");
play(); // 하위 클래스에서 구현해야 하는 메서드 호출
end(); //
}
public abstract void play();
public void end() {
System.out.println("게임이 종료되었습니다.");
}
}
class Chess extends Game {
@Override
public void play() {
System.out.println("체스를 둡니다.");
}
}
public class Main {
public static void main(String[] args) {
Chess chess = new CHess();
chess.start();
// 게임을 시작합니다.
// 체스를 둡니다.
// 게임이 종료되었습니다.
}
}
play() 메서드는 게임마다 다르게 동작하므로, 추상 메서드로 선언하여 하위 클래스에서 반드시 구현하도록 강제합니다.