다형성이란 하나의 인스턴스가 여러가지 타입을 가질 수 있는 것을 의미한다.
즉, 하나의 타입으로 여러 타입의 인스턴스를 처리할 수도 있고, 하나의 메소드를 객체마다 다른 방법으로 동작하게 할 수도 있다.
이러한 다형성을 쓰면 좋은 점은 아래와 같다.
여러 타입의 객체를 하나의 타입으로 관리할 수 있게 된다.
→ 유지보수성과 생산성 증가
상속을 기반으로 한 기술이기 때문에 상속 관계에 있는 모든 객체는 동일한 메세지를 수신하면서도 각 객체가 자신만의 고유한 방식으로 처리할 수 있다.
확장성이 좋은 코드를 작성할 수 있다.
결합도를 낮춰 유지보수성을 증가 시킬 수 있다.
Animal.java - 부모 클래스
public class Animal {
public void eat() {
System.out.println("동물이 먹이를 먹습니다.");
}
public void run() {
System.out.println("동물이 달려갑니다.");
}
public void cry() {
System.out.println("동물이 울음소리를 냅니다.");
}
}
Rabbit.java - 자식 클래스
public class Rabbit extends Animal { //상속 받음
@Override
public void eat() {
System.out.println("토끼가 풀을 뜯어 먹고 있습니다.");
}
@Override
public void run() {
System.out.println("토끼가 달려갑니다. 깡총~ 깡총~");
}
@Override
public void cry() {
System.out.println("토끼가 울음소리를 냅니다. 끼익~ 끼익~");
}
public void jump() {
System.out.println("토끼가 점프합니다. 점프!!!!");
}
}
Tiger.java - 자식 클래스
public class Tiger extends Animal { //상속 받음
@Override
public void eat() {
System.out.println("호랑이가 고기를 뜯어 먹습니다.");
}
@Override
public void run() {
System.out.println("호랑이는 왠만해서 달리지 않습니다. 어슬렁~ 어슬렁~ 걸어갑니다.");
}
@Override
public void cry() {
System.out.println("호랑이가 울부짖습니다. 어흐으으으응~!!!");
}
public void bite() {
System.out.println("호랑이가 물어뜯습니다. 앙~");
}
}
위 예제 코드와 같이 Animal 클래스를 상속 받은 Rabbit과 Tiger 클래스는 부모 클래스의 메소드를 오버라이딩 하여 객체마다 다르게 처리해 줄 수 있다.
또한, 다형성에서 인스턴스를 생성 시 부모 타입으로 자식 인스턴스의 주소 값을 저장해 줄 수도 있다.
public class Application {
public static void main(String[] args){
//부모는 자식을 담을 수 있다.
Animal a1 = new Rabbit();
Animal a2 = new Tiger();
//자식은 부모를 담을 수 없다.
// Rabbit r = new Animal(); //에러남
// Tiger t = new Animal();
}
}
++ 동적 바인딩
동적 바인딩은 컴파일 당시에는 해당 타입의 메소드와 연결되어 있다가, 런타임 시에는 당시 실제 객체가 가진 오버라이딩 된 메소드로 바인딩이 바뀌어 동작하는 것을 의미한다.
public class Application {
public static void main(String[] args){
Animal a1 = new Rabbit();
Animal a2 = new Tiger();
a1.cry(); //Rabbit 메소드로 오버라이딩
a2.cry(); //Tiger 메소드로 오버라이딩
// a1.jump(); //불가능
// a2.bite();
}
}
Animal 타입으로 자식 인스턴스인 Rabbit과 Tiger를 담았기 때문에 자식 고유의 메소드인 jump()와 bite()는 호출할 수 없다.
그럼 객체 별로 고유한 기능을 동작시키기 위해서는 어떻게 해야할까?
바로 해당 레퍼런스 변수를 객체에 맞게 형변환 하면 메소드 호출이 가능해진다.
public class Application {
public static void main(String[] args){
Animal a1 = new Rabbit();
Animal a2 = new Tiger();
a1.cry(); //Rabbit 메소드로 오버라이딩
a2.cry(); //Tiger 메소드로 오버라이딩
((Rabbit) a1).jump(); //형변환을 해준다면 수행 가능
((Tiger) a2).bite();
}
}
코드를 작성 하다보면 그 객체의 타입이 무엇인지 궁금할 수도 있고, 비교해보고 싶을 때도 있을 것이다. 그럴때는 instanceof 연산자를 사용하여 해당 레퍼런스 변수가 참조하는 실제 인스턴스가 원하는 타입과 맞는지 비교할 수 있다.
instanceof 연산자를 이용하면 결과 값은 true, false 로 나오게 된다.
public class Application {
public static void main(String[] args){
Animal a1 = new Rabbit();
Animal a2 = new Tiger();
Rabbit r1 = new Rabbit();
System.out.println("a1이 Tiger 타입인지 확인 : " + (a1 instanceof Tiger)); //false
System.out.println("a1이 Rabbit 타입인지 확인 : " + (a1 instanceof Rabbit)); //true
/* 상속 받은 타입도 함께 가지고 있다(다형성) */
System.out.println("a1이 Animal 타입인지 확인 : " + (a1 instanceof Animal)); //true
System.out.println("r1이 Animal 타입인지 확인 : " + (r1 instanceof Animal)); //true
}
}
클래스의 형변환은 업 캐스팅과 다운 캐스팅으로 구분할 수 있다.
public class Application {
public static void main(String[] args){
Animal a1 = (Animal) new Rabbit(); //up-casting. 명시적 형변환
Animal a2 = new Rabbit(); //up-casting. 묵시적 형변환
Rabbit r1 = (Rabbit) a1; //down-casting. 명시적 형변환
// Rabbit r2 = a2; //down-casting. 묵시적 형변환 불가능
}
}
방금 전 부모 타입 변수에 자식 인스턴스를 담을 수 있다고 했지만 자식 타입 변수에는 부모 인스턴스를 담을 수 없다고 하였다.
위 코드에서도 볼 수 있듯이 자식 타입 변수인 r2에 부모 인스턴스인 a2는 명시적 형변환 없이는 담을 수가 없다.
즉, 담아주고 싶다면 명시적으로 형변환 해주어야 한다.
추상 클래스는 추상 메소드를 0개 이상 포함하는 클래스이다.
추상 클래스를 사용하기 위해서는 클래스 선언부에 abstract 키워드를 명시해주어야 한다.
이러한 추상 클래스로는 인스턴스를 직접 생성해줄 수는 없다.
즉, 추상 클래스를 사용하기 위해서는 추상 클래스를 상속 받은 하위 클래스를 이용하여 인스턴스를 생성해야 한다.
public abstract class Product { //추상 클래스
// ...
}
추상 메소드는 메소드의 선언부만 있고 구현부가 없는 메소드이다.
추상 클래스와 같이 메소드의 선언부에 abstract 키워드를 명시해주어야 한다.
추상 클래스에 작성한 추상 메소드는 반드시 자식이 오버라이딩 하여 작성해야 하며, 자식 클래스들의 메소드들의 공통 인터페이스의 역할을 수행할 수 있다.
Product.java - 추상 클래스
public abstract class Product { //추상 클래스
// ...
// 추상클래스는 필드를 가질 수 있다.
private int nonStatidField;
private static int staticFiled;
// 추상 클래스는 생성자를 가질 수 있지만 직접 인스턴스를 생성할 수는 없다.
public Product() {}
// 추상클래스는 일반적인 메소드를 가질 수 있다.
public void nonStaticMethod() {
//...
}
public static void staticMethod() {
//...
}
//추상 메소드가 한 개라도 있다면 클래스를 추상 클래스로 만들어 주어야 한다.
public abstract void abstMethod(); //추상 메소드. 구현부 x
}
위 코드의 내용을 정리하자면 아래와 같다.
SmartPhone.java - 추상 클래스를 상속 받은 자식 클래스
public class SmartPhone extends Product {
public SmartPhone() {} //생성자
//추상 클래스가 가지는 추상 메소드를 반드시 오버라이딩 해야한다.
@Override
public void abstMethod() {
System.out.println("SmartPhone에서 추상 메소드 오버라이딩");
}
// 추가적인 메소드도 작성이 가능하다.
public void printSmartPhone() {
//...
}
}
위 코드에서 가장 중요한 점은 상속 받은 추상 클래스 내에 추상 메소드가 있다면, 자식 클래스에서 반드시 오버라이딩 해야한다는 점이다.
Main.java - 실행 클래스
public class Main {
public static void main(String[] args){
/* 추상클래스는 직접적으로 인스턴스 생성 불가하다. */
// Product product = new Product();
/* 추상클래스를 상속받은 객체를 이용해야 한다. */
SmartPhone smartPhone = new SmartPhone();
/* SmartPhone은 SmartPhone 타입이기도 하지만 Product 타입이기도 하다. */
System.out.println(smartPhone instanceof SmartPhone); //true
System.out.println(smartPhone instanceof Product); //true
/* 따라서 다형성을 적용해서 추상클래스를 레퍼런스 타입으로 활용할 수 있다. */
Product product = new SmartPhone();
/* 동적 바인딩에 의해 SmartPhone의 메소드가 호출된다. */
product.abstMethod(); //오버라이딩
/* 추상클래스가 가진 메소드도 호출할 수 있다. */
product.nonStaticMethod();
/* static 메소드는 그냥 사용이 가능하다. (인스턴스 생성 불필요) */
Product.staticMethod();
}
}
인터페이스(Interface)는 추상 메소드와 상수 필드만 가질 수 있는 클래스의 변형체이다.
상속과 달리 implements 키워드를 사용하며 자식 클래스 입장에서 ‘상속 받았다’고 하지 않고 ‘구현한다’ 라는 표현을 쓴다.
이러한 인터페이스를 사용하면 필요한 기능을 공통화 하여 강제성을 부여할 목적으로 사용할 수 있으며, 자바의 단일 상속의 단점을 극복 할 수 있다.
public interface InterProduct extends InterProduct1, InterProduct2{
//...
}
위 코드와 같이 인터페이스를 사용하기 위해서는 선언부에 interface 키워드를 사용해주면 되며, 다른 인터페이스를 상속 받을 때는 extends 키워드를 사용한다.
이때 InterProduct 인터페이스는 여러 다른 인터페이스를 상속 받을 수 있다. (다중 상속)
public interface InterProduct extends InterProduct1, InterProduct2{
// 인터페이스는 상수 필드만 작성 가능하다.
// 상수 필드는 final를 붙여 사용할 수 있으며, 일반적으로 static과 같이 쓰인다.
// 반드시 선언과 동시에 초기화 해주어야 한다.
public static final int MAX_NUM = 100;
//묵시적으로 인터페이스 내의 모든 필드는 public static final이다.
int MIN_NUM = 10;
void nonStaticMethod(); //묵시적으로 public abstract의 의미를 가진다.
}
인터페이스 내에는 상수 필드만 작성이 가능하며, public static final 키워드를 붙혀 상수 필드를 선언해주면 된다.
또한 인터페이스는 생성자를 가질 수 없고, 추상 메소드만 작성이 가능하다.
++ JDK 1.8부터 추가된 기능으로 인터페이스 내에 static 메소드와 default 키워드를 사용하여 메소드를 선언하면 추상 메소드가 아니더라도 구현 해줄 수 있다.
public interface InterProduct {
//인터페이스 내에 static 메소드 작성 가능
public static void staticMethod(){
//...
}
//인터페이스 내에서 default 키워드를 메소드에 사용하면 static이 아닌 메소드도 작성이 가능해진다.
public default void defaultMethod(){
//...
}
}
Product.java - 인터페이스 구현 클래스
// 인터페이스를 상속 받기 위해서는 implements 키워드 사용하며 여러개를 상속 받을 수 있다.
// 상속도 동시에 받을 수 있으며 extends 키워드를 implements 키워드 앞에 사용해야한다.
public class Product extends Object implements InterProduct, InterProduct1{
@Override
public void nonStaticMethod() {
System.out.println("InterProduct의 nonStaticMethod 메소드 오버라이딩");
}
/* static 메소드는 오버라이딩 할 수 없다. */
// @Override
// public static void staticMethod() {}
/* default메소드는 인터페이스에서만 작성 가능하다. */
// @Override
// public default void defaultMethod() {}
/* default 키워드를 제외하면 오버라이딩 가능하다. */
@Override
public void defaultMethod() {
System.out.println("InterProduct의 defaultMethod에 default 키워드 제외 후 호출 가능");
}
}
인터페이스를 구현하고 싶다면 구현할 클래스에 implements 키워드를 사용하면 된다. 인터페이스는 다중 상속이 가능하기 때문에 여러 개의 인터페이스를 상속 받을 수 있다.
Main.java - 실행 클래스
public class Main {
public static void main(String[] args) {
// 인터페이스는 직접 생성 불가능
// InterProduct interProduct = new InterProduct();
InterProduct interProduct = new Product();
/* 인터페이스의 추상메소드가 오버라이딩한 메소드로 동적바인딩에 의해 호출됨 */
interProduct.nonStaticMethod();
/* static 메소드는 인터페이스명.메소드명() 으로 호출함 */
InterProduct.staticMethod();
/* 상수 필드 접근도 인터페이스명.필드명 으로 접근함 */
System.out.println(InterProduct.MAX_NUM);
System.out.println(InterProduct.MIN_NUM);
}
}
인터페이스 내에 있는 static 메소드는 인터페이스명.메소드명() 으로 호출할 수 있다.
또한, 인터페이스 내에 있는 상수 필드를 호출하고 싶다면, 인터페이스명.필드명 으로 접근할 수 있다.
추상 클래스와 인터페이스를 정리하자면 다음과 같다.
| 추상 클래스 | 인터페이스 | |
|---|---|---|
| 메소드 | 추상 메소드와 일반 메소드 O | 기본적으로 모든 메소드가 추상 메소드 |
| 필드 | 일반 필드, 상수 필드 O | 상수 필드만 가능 |
| 다중 상속 | X | O |
| 접근 제어자 | O | 모든 메소드는 public |
| 인스턴스 직접 생성 | X | X |
추상 클래스 → 기본적인 구현을 제공하고, 클래스 간의 공통된 기능을 공유할 때 사용
인터페이스 → 필요한 기능을 공통화 하여 강제성을 부여