It's going to be one day 🍀

안녕하세요! 매일 매일 공부하려고 노력하는 백엔드 개발자 지망생의 공부 흔적입니다.

Back-End/Java # 교육

[Java 교육] 오버라이딩/인터페이스/다형성

2jin2 2024. 2. 5. 18:18

[0202 공부 내용 정리]

메소드 오버라이딩

메소드 오버로딩(method overloading) : 메소드의 이름은 동일하고 입력 항목이 다른 경우

  • 동일한 리턴 타입과 메소드명, 다른 매개변수
void sleep()
void sleep(int hour)

 

메소드 오버라이딩(@Override) : 상속된 부모의 메소드 내용이 자식 클래스에 맞지 않을 경우, 자식 클래스에서 동일한 메소드를 재정의할 수 있음.

public class Parent {
	void method1() {
		System.out.println("Parent의 method1 호출");
	}

	void method2() {
		System.out.println("Parent의 method2 호출");
	}
}
public class Child extends Parent {
	void method2() {
		System.out.println("Child의 method2 호출");
	}

	void method3() {
		System.out.println("Child의 method3 호출");
	}
}
public class ChildExample {
	public static void main(String[] args) {
		Child child = new Child();

		child.method1();
		child.method2();    // 재정의된 메소드 호출
		child.method3();
	}
}
  • 메소드를 오버라이딩 할 때 지켜야 할 규칙 :
    • 부모의 메소드와 동일한 시그니처(리턴타입, 메소드이름, 매개변수 리스트)를 가져야함.
    • 부모 클래스의 메소드보다 접근 제한을 강하게 오버라이딩 할 수 없음.
    • 새로운 예외를 throws 할 수 없음.

ex) Calculator의 자식 클래스인 Computer에서 원의 넓이를 좀 더 정확하게 구하기 위해 오버라이딩함.

public class Calculator {
	private static final double PI = 3.14159;
	
	double areaCircle(double r) {
		System.out.println("Calculator 객체의 areaCircle() 실행");
		return PI * r * r;
	}
}
public class Computer extends Calculator {
	@Override
	double areaCircle(double r) {
		System.out.println("Computer 객체의 areaCircle() 실행");
		return Math.PI * r * r;
	}
}

→ 자식 클래스 Computer의 areaCircle() 메소드에서는 Math.PI 상수를 이용함.

  • @Override 어노테이션은 areaCircle() 메소드가 정확하게 오버라이딩된 것인지 컴파일러가 체크할 수 있음.

추상클래스

추상 메서드 : 자식 클래스에서 반드시 오버라이드 해야만 사용할 수 있는 메소드.

abstract 리턴타입 메소드명();

 

추상 클래스 : 추상 메서드를 하나 이상 포함하는 클래스

추상(abstract) : 실체 간의 공통되는 특성을 추출한 것.

추상 클래스는 실체 클래스의 공통되는 필드와 메소드를 추출해서 만들었기 때문에 일반 클래스와는 달리 객체를 직접 생성해서 사용할 수 없음. → new 연산자를 사용해 인스턴스 생성 불가.

Animal animal = new Animal(); (X)

 

추상 클래스는 새로운 실체 클래스를 만들기 위해 부모 클래스로만 사용됨

class Ant **extends** Animal { ... }

 

추상 클래스의 용도

  1. 실체 클래스들의 공통된 필드와 메소드의 이름을 통일할 목적

→ 실체 클래스를 설계하는 사람이 여러 사람일 경우, 추상클래스를 정의함으로써 어느정도 필드나 메소드이름을 통일시킬 수 있음.

  1. 실체 클래스를 작성할 때 시간을 절약하려는 목적

→ 공통적인 필드와 메소드는 추상 클래스에 모두 선언해두고, 실체 클래스마다 다른 점만 실체 클래스에 선언하게 되면 실체 클래스 작성시 시간을 절약할 수 있음.

 

추상 클래스의 선언

추상 클래스를 선언할 때는 클래스 선언에 abstract 키워드를 붙여야 함.

→ abstract를 붙이게 되면 new 연산자를 이용해서 객체를 만들지 못하고 상속을 통해 자식 클래스만 만들 수 있게됨.

public abstract class 클래스명 {
	// 필드
	// 생성자
	// 메소드
}

 

추상 클래스의 오버라이딩

간혹 메소드의 선언만 통일하고, 실행 내용은 실체 클래스마다 달라야하는 경우

public abstract class Animal {
	public abstract void sound();  // 추상메소드 - 메소드의 타입과 이름만 정의
}

public abstract class Animal {    // 추상 클래스
	protected String kind;
	
	public void breathe() {
		System.out.println("숨을 쉽니다.");
	}
	
	public abstract void sound();   // 추상 메소드
}
public class Dog extends Animal {
	public Dog() {
		this.kind = "포유류";
	}

	@Override
	public void sound() {           // 추상 메소드 재정의
		System.out.println("멍멍");
	}
}
public class Cat extends Animal {
	public Cat() {
		this.kind = "포유류";
	}

	@Override
	public void sound() {           // 추상 메소드 재정의
		System.out.println("야옹");
	}
}

+) 추상 메소드를 abstract 메소드로 정의했는데도 실체(자식)클래스에서 해당 메소드를 구현하지 않는다면 컴파일 오류가 발생함.


인터페이스

자바에서 인터페이스란? 다른 클래스를 작성할 때 기본이 되는 틀을 제공하면서, 다른 클래스 사이의 중간 매개 역할을 하는 일종의 추상 클래스

→ 어떤 객체가 있고 그 객체가 특정한 인터페이스를 사용한다면 그 객체는 반드시 인터페이스의 메소드들을 구현해야함.

ex) 요구사항 클래스로 작성해보기

다음은 어떤 동물원의 사육사가 하는 일이다.

난 동물원(zoo)의 사육사(zookeeper)이다.
육식동물(predator)이 들어오면 난 먹이를 던져준다(feed).
 - 호랑이(tiger)가 오면 고기(meat)를 던져준다.
 - 사자(lion)가 오면 생선(fish)를 던져준다.
public class Sample {
	public static void main(String[] args) {
		ZooKeeper zooKeeper = new ZooKeeper();
		
		Tiger tiger = new Tiger();
		zooKeeper.feed(tiger);
		
		Lion lion = new Lion();
		zooKeeper.feed(lion);
	}
}

class Animal {
	String name;

	void setName(String name) {
		this.name = name;
	}
}

class Tiger extends Animal {
}

class Lion extends Animal {
}

class ZooKeeper {
	void feed(Tiger tiger) {  // 호랑이가 오면 고기를 던져 준다.
		System.out.println("feed meat");
	}

	void feed(Lion lion) {  // 사자가 오면 생선을 던져준다.
		System.out.println("feed fish");
	}
}

→ 이렇게 메소드 오버로딩 방식으로 구현하면, 각각의 동물 클래스를 생성할 때마다 feed메소드를 추가해줘야함. 이렇게 클래스가 추가될 때마다 메소드를 추가해야한다면 Zookeeper 클래스가 복잡해짐. 따라서 이런 어려움을 극복하기 위해 인터페이스를 사용해야함.

 

인터페이스의 선언

인터페이스의 선언은 class 키워드 대신에 interface 키워드를 사용함

interface Predator {   // 육식동물 인터페이스 생성

}

→ 첫 문자를 대문자로 하는것이 관례임

그리고 Tiger, Lion 클래스가 Predator 인터페이스를 구현하도록 implements 키워드를 사용해 수정해줌.

class Tiger extends Animal implements Predator {
	...
}

class Lion extends Animal implements Predator {
	...
}

→ 이렇게 Tiger, Lion 클래스가 Predator 인터페이스를 구현하게 되면 Zookeeper 클래스의 feed 메소드를 다음과 같이 변경할 수 있음.

// 변경 전
class ZooKeeper {
	void feed(Tiger tiger) {  // 호랑이가 오면 고기를 던져 준다.
		System.out.println("feed meat");
	}

	void feed(Lion lion) {    // 사자가 오면 생선을 던져준다.
		System.out.println("feed fish");
	}
}
// 변경 후
class ZooKeeper {
	void feed(**Predator predator**) {
		System.out.println("feed meat");
	}
}
...

→ 이전에는 feed 메소드의 입력으로 Triger, Lion을 각각 필요로 했지만, 이제 이것을 Predator라는 인터페이스로 대체할 수 있게 되었음.

  • tiger: Tiger 클래스의 객체이자 Predator 인터페이스의 객체
  • lion: Lion 클래스의 객체이자 Predator 인터페이스의 객체

인터페이스 구현

class ZooKeeper {
	void feed(Predator predator) {
		System.out.println("feed meat");   // 어떤 predator객체가 오더라도 feed meat 출력
	}
}

우리가 원하는 출력을 하기 위해서 이 부분을 어떻게 구현할 수 있을까?

→ Predator 인터페이스에 다음과 같은 getFood 메소드 추가하면됨!

interface Predator {
    String getFood();
}
  • 인터페이스의 메소드는 메소드의 이름과 입출력에 대한 정의만 있고 내용은 없음.
  • → 그 이유는 인터페이스는 ‘규칙’이기 때문.
  • getFood() 메소드는 인터페이스를 implements한 클래스들이 강제적으로 구현해야하는 규칙이 됨.
...

class Tiger extends Animal implements Predator {
	@Override
	public String getFood() {
		return "meat";
	}
}

class Lion extends Animal implements Predator {
	@Override
	public String getFood() {
		return "fish";
	}
}

→ Tiger, Lion 구현 클래스에 위와같은 getFood() 메소드를 구현해줘야함.

인터페이스 구현체로 변경 후 Zookeeper의 feed 메소드만 Predator.gerFood 구현체를 호출하도록 변경하면됨.

class ZooKeeper {
	void feed(Predator predator) {
		System.out.println("feed " + predator.getFood());
	}
}

 

인터페이스 사용

Predator 인터페이스로 구현 객체인 Lion과 Tiger을 사용하려면 다음과 같이 Animal타입 변수를 선언하고, 구현 객체를 대입해야함.

Predator anything = new Lion();
or 
anything = new Tiger();

 

[0205 공부 내용 정리]

인터페이스 보충 설명

package chapter08;

public class PrintMain {
    public static void main(String[] args) {
        Run child = new Child();
        Run adult = new Adult();
        print(child);
        print(adult);
    }

    static void print(Run run){
        run.run();
    }
}

→ Child(), Adult()는 Run 인터페이스를 구현한 클래스들. 얘네는 Run 타입의 변수에 할당됨.

→ 이러한 할당은 다형성의 한 예로서, 인터페이스를 통해 여러 구현체를 사용할 수 있게 함.

→ 따라서 위 코드에서는 Child와 Adult 클래스가 Run 인터페이스를 구현한 구현체임.

 

인터페이스 상속

인터페이스도 다른 인터페이스를 상속할 수 있음. 다중 상속 허용.

public interface 하위인터페이스 **extends 상위인터페이스1, 상위인터페이스2, ...** { 
		...
}

하위 인터페이스를 구현하는 클래스는 하위 메소드 뿐만 아니라 상위 인터페이스의 모든 추상 메소드에 대한 실체 메소드를 갖고있어야 함.

ex)

→ InterfaceC는 InterfaceA, InterfaceB의 methodA(), methodB() 메소드를 모두 호출할 수 있지만, InterfaceA 또는 InterfaceB의 변수는 각각 methodA(), methodB() 메소드만 호출할 수 있음.

 

인터페이스의 다형성

프로그램을 개발할 때 인터페이스를 사용하여 메소드 호출을 하도록 코딩했다면, 구현 객체를 교체하는 것은 매우 쉬움.

프로그램 소스코드는 크게 변함이 없으면서, 구현 객체를 교체하면서 프로그램 실행 결과가 다양해짐.

이것이 바로 인터페이스의 다형성

 

자동타입변환

프로그램 실행 도중에 자동으로 타입 변환이 일어나는 것.

ex) 구현 객체가 인터페이스 타입으로 변환되는 것.

public interface Vehicle {
    void run();
}
public class Bus implements Vehicle {
    @Override
    public void run() {
        System.out.println("버스가 달립니다!");
    }
}
public class Taxi implements Vehicle {
    @Override
    public void run() {
        System.out.println("택시가 달립니다!");
    }
}

→ Bus와 Taxi에서 각각 run() 재정의. (Java의 규약)

public class Driver {
    public void drive(**Vehicle vehicle**) {  // 구현 객체 vehicle 
        vehicle.run();    // 구현 객체의 run() 메소드가 실행됨
    }
}
public class DriverExample {
    public static void main(String[] args) {
        // 구현체를 선언할 때 인터페이스도 인터페이스의 타입으로 구현체를 들고있을 수 있음.
        Vehicle bus = new Bus(); // Vehicle을 Bus로 자동 타입변환
        Vehicle taxi = new Taxi(); // Vehicle을 Taxi로 자동 타입변환

        // 이렇게 Vehicle의 구현체 클래스를 생성해서 Driver.drive(매개변수)로 넘겨줄 수 있음.

        Driver driver = new Driver();
        driver.drive(bus);
        driver.drive(taxi);
    }
}

→ Vehicle을 Bus와 Taxi로 자동 타입 변환 가능.

→ 한마디로 Vehicle 타입인 변수를 Bus, Taxi 타입으로 변환하는 것임.

 

객체 타입 확인 (instanceof)

어떤 구현 객체가 인터페이스 타입으로 변환되었는지 확인하는 방법

→ instanceof 연산자를 사용

public class Driver {
    public void drive(Vehicle vehicle) {
        if (vehicle instanceof Bus) {
            System.out.println("버스!");
        } else if (vehicle instanceof Taxi) {
            System.out.println("택시!");
        }

        vehicle.run();
    }
}
버스!
버스가 달립니다!
택시!
택시가 달립니다!

이렇게 instanceof 연산자로 어떤 구현 객체인지 확인할 수 있음.

 

디폴트 메소드

자바8 버전 이후부터 디폴트 메소드 사용가능.

인터페이스의 메소드는 구현체를 가질 수 없지만 디폴트 메소드를 사용하면 실제 구현된 형태의 메소드를 가질 수 있음.

ex) Predator 인터페이스에 다음과 같은 디폴트 메소드 추가 가능

interface Predator {
	String getFood();

	// 디폴트 메소드
	default void printFood() {   
		System.out.printf("my food is %s\\n", getFood());
	}
}

→ 이렇게 Predator 인터페이스에 디폴트 메소드를 구현하면 Predator 인터페이스를 구현한 Tiger, Lion 등의 실제 클래스는 printFood() 메소드를 구현하지 않아도 공통으로 사용할 수 있음.

 

https://github.com/drinkgalaxy/Ormi-Task/tree/main/DailyQuiz/0205