Chapter 1. 다형성
💡 학습 목표
- 자바 객체지향 프로그래밍에서 다형성이 가지는 의미와 장점을 이해할 수 있다.
- 참조변수의 타입 변환에 대한 내용을 이해하고, 업캐스팅과 다운캐스팅의 차이를 설명할 수 있다.
- instanceof 연산자를 언제 어떻게 활용할 수 있는 지 이해하고 설명할 수 있다.
- 코딩 예제를 실제로 입력해보면서 다형성이 실제로 어떻게 활용되는 지 이해할 수 있다.
다형성이란 “여러 개”를 의미하는 poly와 어떤 ‘형태’ 또는 ‘실체’를 의미하는 morphism의 결합어로 하나의 객체가 여러 가지 형태를 가질 수 있는 성질을 의미한다.
자바에서 다형성이란 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미한다.
class Friend {
public void friendInfo() {
System.out.println("나는 당신의 친구입니다.");
}
}
class BoyFriend extends Friend {
public void friendInfo() {
System.out.println("나는 당신의 남자친구입니다.");
}
}
class GirlFriend extends Friend {
public void friendInfo() {
System.out.println("나는 당신의 여자친구입니다.");
}
}
public class FriendTest {
public static void main(String[] args) {
Friend friend = new Friend(); // 객체 타입과 참조변수 타입의 일치
BoyFriend boyfriend = new BoyFriend();
Friend girlfriend = new GirlFriend(); // 객체 타입과 참조변수 타입의 불일치
friend.friendInfo();
boyfriend.friendInfo();
girlfriend.friendInfo();
}
}
// 출력값
나는 당신의 친구입니다.
나는 당신의 남자친구입니다.
나는 당신의 여자친구입니다.
Friend girlfriend = new GirlFriend(); 처럼 상위 클래스를 참조변수의 타입으로 지정하면 참조변수가 사용할 수 있는 멤버의 개수는 상위 클래스의 멤버의 수가 된다.
상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조하는 것은 가능하지만 그 반대는 성립하지 않는다.
그 이유는 상위 클래스의 멤버 개수는 항상 하위 클래스의 것과 같거나 적기 때문이다.
좀 더 구체적으로 설명하면, 실제 참조하고 있는 인스턴스의 멤버를 기준으로 참조 변수의 타입의 멤버가 실제 인스턴스의 멤버 수보다 작은 것은 실제 사용할 수 있는 기능을 줄이는 것이기에 허용되는 것이다.
참조변수의 타입 변환
참조 변수의 타입 변환은 사용할 수 있는 멤버의 개수를 조절하는 것을 의미한다.
타입 변환을 위해서는 아래 세 가지 조건을 충족해야 한다.
- 상속 관계에 있어야 한다.
- 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형변환 연산자 생략이 가능하다.
- 상위 클래스 타입에서 하위 클래스 타입으로의 타입 변환(다운캐스팅)은 형변환 연산자를 반드시 명시해야 한다. 또한 업캐스팅이 되어있다는 전제 하에 다운캐스팅이 가능하다.
instanceof 연산자
instanceof 연산자를 사용하면 참조변수의 타입 변환, 즉 캐스팅이 가능한 지 여부를 boolean 타입으로 확인할 수 있다.
다형성의 활용 예제
class Coffee {
int price;
public Coffee(int price) {
this.price = price;
}
}
class Americano extends Coffee {};
class CaffeLatte extends Coffee {};
class Customer {
int money = 50000;
void buyCoffee(Americano americano) { // 아메리카노 구입
money = money - americano.price;
}
void buyCoffee(CaffeLatte caffeLatte) { // 카페라테 구입
money = money - caffeLatte.price;
}
}
위 코드에서 보면 커피의 종류마다 buyCoffee메서드를 추가해야 한다.
이를 다형성을 활용하여 상위 클래스인 Coffee의 타입을 매개변수로 전달 받으면, 매번 다른 타입의 참조변수를 매개변수로 전달해주어야 하는 번거로움을 훨씬 줄일 수 있다.
public class PolymorphismEx {
public static void main(String[] args) {
Customer customer = new Customer();
customer.buyCoffee(new Americano());
customer.buyCoffee(new CaffeLatte());
System.out.println("현재 잔액은 " + customer.money + "원 입니다.");
}
}
class Coffee {
int price;
public Coffee(int price) {
this.price = price;
}
}
class Americano extends Coffee {
public Americano() {
super(4000); // 상위 클래스 Coffee의 생성자를 호출
}
public String toString() {return "아메리카노";}; //Object클래스 toString()메서드 오버라이딩
};
class CaffeLatte extends Coffee {
public CaffeLatte() {
super(5000);
}
public String toString() {return "카페라떼";};
};
class Customer {
int money = 50000;
void buyCoffee(Coffee coffee) {
if (money < coffee.price) { // 물건 가격보다 돈이 없는 경우
System.out.println("잔액이 부족합니다.");
return;
}
money = money - coffee.price; // 가진 돈 - 커피 가격
System.out.println(coffee + "를 구입했습니다.");
}
}
// 출력값
아메리카노를 구입했습니다.
카페라떼를 구입했습니다.
현재 잔액은 41000원 입니다.
Chapter 2. 추상화
💡 학습 목표
- 추상화의 핵심 개념과 목적을 이해하고 설명할 수 있다.
- abstract 제어자가 내포하고 있는 의미를 이해하고, 어떻게 사용되는 지 설명할 수 있다.
- 추상 클래스의 핵심 개념과 기본 문법을 이해할 수 있다.
- final 키워드를 이해하고 설명할 수 있다.
- 자바 추상화에서 핵심적인 역할을 수행하는 인터페이스의 핵심 내용과 그 활용을 이해할 수 있다.
- 추상 클래스와 인터페이스의 차이를 설명할 수 있다.
추상화란 객체의 공통적인 속성과 기능을 추출하여 정의하는 것을 의미한다.
즉 기존 클래스들의 공통적인 요소들을 뽑아서 상위 클래스를 만들어 내는 것이라고 할 수 있다.
abstract 제어자
주로 클래스와 메서드를 형용하는 키워드로 사용되는데 메서드 앞에 붙은 경우를 ‘추상 메서드’ 클래스 앞에 붙은 경우를 ‘추상 클래스’라고 부른다.
추상 메서드가 최소 하나 이상 포함되어 있는 클래스를 추상 클래스라고 한다.
메서드의 시그니처만 있고 바디가 없는 메서드를 추상 메서드라고 하는데 abstract 키워드를 메서드 이름 앞에 붙여 표현한다.
즉, 충분히 구체화되지 않은 미완성 메서드이다.
이에 따라 추상 클래스는 메서드 바디가 완성이 되기 전까지 이를 기반으로 객체 생성이 불가능하다.
추상 클래스
객체를 생성하지 못하는 미완성 클래스를 만드는 이유는 크게 두 가지가 있다.
먼저 상속 관계에 있어 새로운 클래스를 작성하는데 유용하다는 것이다.
상위 클래스에서는 선언부만 작성하고 하위 클래스에서 실제 구체적인 내용을 구현한다면 설계하는 상황이 변해도 보다 유연하게 대응이 가능하다.
이 때 사용하는 것이 오버라이딩인데 오버라이딩을 통해 각각 상황에 맞는 메서드 구현이 가능하다.
두 번째는 추상화를 구현하는데 핵심적인 역할을 수행한다는 것이다.
상속계층도의 상층부에 위치할수록 추상화의 정도가 높고 그 아래로 내려갈수록 구체화된다.
final 키워드
위치 의미
클래스 | 변경 또는 확장 불가능한 클래스, 상속 불가 |
메서드 | 오버라이딩 불가 |
변수 | 값 변경이 불가한 상수 |
공통적으로 final 제어자가 추가되면 해당 대상은 더 이상 변경이 불가하거나 확장되지 않는 성질을 지닌다.
인터페이스
추상 클래스를 설계가 모두 끝나지 않은 “미완성 설계도’에 비유한다면 인터페이스는 그보다 더 높은 추상성을 가지는 가장 기초적인 “밑그림”에 빗대어 표현할 수 있다.
인터페이스는 추상 메서드와 상수만을 멤버로 가질 수 있다는 점에서 추상 클래스에 비해 추상화 정도가 더 높다고 할 수 있다.
자바 8 이후에 default/static 메서드를 인터페이스에 포함시키도록 업데이트가 되었다.
인터페이스의 기본 구조
class 키워드 대신 interface 키워드를 사용하면 된다.
일반 클래스와 다르게 내부의 모든 필드가 public static final로 정의되고 static과 default 메서드 이외의 모든 메서드가 public abstract로 정의된다.
인터페이스 안에서 상수를 정의하는 경우 반드시 public static final로, 메서드를 정의하는 경우 public abstract로 정의되어야 하지만 일부 또는 전부 생략이 가능하다.
인터페이스의 구현
인터페이스도 그 자체로 인스턴스를 생성할 수 없고 메서드 바디를 정의하는 클래스를 따로 작성해야 한다.
class 클래스명 implements 인터페이스명 {
... // 인터페이스에 정의된 모든 추상메서드 구현
}
특정 인터페이스를 구현한 클래스는 해당 인터페이스에 정의된 모든 추상메서드를 구현해야 한다.
인터페이스는 다중 구현이 가능하다.
하나의 클래스가 여러 개의 인터페이스를 구현할 수 있다.
다만 인터페이스는 인터페이스로부터만 상속이 가능하고, object 클래스와 같은 최고 조상이 존재하지 않는다.
클래스에서 다중 상속이 불가능했던 핵심적인 이유는 부모 클래스에 동일한 이름의 필드 또는 메서드가 존재하는 경우 충돌이 발생하기 때문인데, 인터페이스는 애초에 미완성된 멤버를 가지고 있기 때문에 안전하게 다중 구현이 가능하다.
인터페이스의 장점
public class InterfaceExample {
public static void main(String[] args) {
User user = new User(); // User 클래스 객체 생성
user.callProvider(new Provider()); // Provider 객체 생성 후에 매개변수로 전달
}
}
class User { // User 클래스
public void callProvider(Provider provider) { // Provider 객체를 매개변수로 받는 callProvider 메서드
provider.call();
}
}
class Provider { //Provider 클래스
public void call() {
System.out.println("무야호~");
}
}
// 출력값
무야호~
위 코드에서 Provider 클래스가 아닌 Provider2 클래스로 교체햐아 하는 상황이 발생한다고 가정해보자.
public class InterfaceExample {
public static void main(String[] args) {
User user = new User(); // User 클래스 객체 생성
user.callProvider(new Provider2()); // Provider객체 생성 후에 매개변수로 전달
}
}
class User { // User 클래스
public void callProvider(Provider2 provider) { // Provider 객체를 매개변수로 받는 callProvider 메서드
provider.call();
}
}
class Provider2 { //Provider 클래스
public void call() {
System.out.println("야호~");
}
}
// 출력값
야호~
이처럼 Provider2 객체를 생성하고, user 클래스의 callProvider 메서드가 동일한 타입의 매개변수를 받을 수 있도록 매개변수 타입을 Provider2 로 변경해야 한다.
즉, Provider 클래스에 의존하고 있는 User 클래스의 코드 변경이 불가피하다.
이런 상황을 인터페이스를 사용해 코드를 다시 작성한다고 하면
interface Cover { // 인터페이스 정의
public abstract void call();
}
public class Interface4 {
public static void main(String[] args) {
User user = new User();
// Provider provider = new Provider();
// user.callProvider(new Provider());
user.callProvider(new Provider2());
}
}
class User {
public void callProvider(Cover cover) { // 매개변수의 다형성 활용
cover.call();
}
}
class Provider implements Cover {
public void call() {
System.out.println("무야호~");
}
}
class Provider2 implements Cover {
public void call() {
System.out.println("야호~");
}
}
//출력값
야호~
Provider 클래스의 내용 변경 또는 교체가 발생하더라도 User 클래스는 코드를 변경하지 않아도 사용이 가능하다.
결론적으로 정리하면, 인터페이스는 기능이 가지는 역할과 구현을 분리시켜 사용자로 복잡한 기능의 구현이나 교체/변경을 신경쓰지 않고도 코드 변경의 번거로움을 최소화하고 손쉽게 해당 기능을 사용할 수 있도록 한다.
개발자의 입장에서도 선언과 구현을 분리시켜 개발시간을 단축할 수 있고, 한 클래스의 변경이 다른 클래스에 미치는 영향을 최소화할 수 있다.
'CodeStates_BE_44 > TIL' 카테고리의 다른 글
Day 16. [Java] 애너테이션, 람다, 스트림 (0) | 2023.03.08 |
---|---|
Day 15. [Java] 컬렉션 (1) | 2023.03.07 |
Day 11. [Java] 객체지향 프로그래밍 심화_상속, 캡슐화 (0) | 2023.02.27 |
Day 10. [Java] 객체지향 프로그래밍 기초_생성자, 내부 클래스 (0) | 2023.02.24 |
Day 9. [Java] 객체지향 프로그래밍 기초_클래스, 객체, 필드, 메서드 (0) | 2023.02.23 |