Study/JAVA

[JAVA] 12장 - 추상 클래스와 인터페이스

오구민 2022. 10. 6. 22:00

1. 추상 클래스

1.1 추상 클래스의 정의

추상 메서드는 '메서드의 본체가 완성되지 않은 미완성 메서드'를 말한다. 미완성 메서드라고도 부른다.

메서드의 기능을 정의하는 중괄호 안이 비어 있다는 것이 아니라 중괄호 자체가 없으며, 중괄호가 없기 때문에 명령어 끝을 알리는 세미콜론으로 끝나야 한다.

abstract 리턴 타입 메서드명(입력매개변수);

추상 메서드 표현 방법

 

추상 메서드를 1개 이상 포함하고 있는 클래스는 반드시 추상 클래소르 정의돼야 한다.

즉, 일반적으로 추상 클래스는 메서드의 기능이 정의돼 있지 않은 미완성 메서드(중괄호가 없는 메서드)가 1개 이상 있다는 의미다. 추상 클래스의 형식은 추상 메서드 구문과 비슷하게 class 키워드 앞에 abstract를 붙여 표현한다.

abstract class 클래스명 {
}

추상 클래스 표현 방법

 

 

  1.1.1 메서드의 완성 기준은?

  • 메서드의 완성과 미완성의 구분 기준은 메서드의 기능을 정의하는 중괄호의 존재 여부다.
  • 기능에 초점을 두다 보니 간혹 중괄호 안에 아무런 코드가 작성되지 않으면 미완성 메서드라고 생각하는 때가 있지만, 그것은 '아무 것도 하지 말라.'는 기능이 명확히 정의된 완성 메서드다.

 

1.2 추상 클래스의 특징

추상 클래스는 내부의 미완성 메서드 때문에 객체를 직접 생성할 수 없다. 힙 메모리에 생성되는 객체는 내부 요소가 미완성된 상태로 들어갈 수 없기 때문이다.

추상 클래스로는 직접 객체를 생성할 수 없지만, 이 추상 클래스를 상속한 자식 클래스를 생성하면 그 자식 클래스로는 객체를 생성할 수 있는 것이다. 그리고 생성된 객체 내부에는 부모 클래스의 추상 메서드가 구현돼 있을 것이다.

추상 클래스를 상속하는 자식 클래스는 부모에게 상속받은 미완성 메서드(추상 메서드)를 반드시 완성(오버라이딩)해야 한다.

 

1.2.1 용어 정리

  • 완성된 메서드이든, 미완성 메서드이든 부모에게 상속받은 메서드를 자식 클래스에서 재정의하는 것을 '오버라이딩'
  • 이 중 부모에게 물려받은 미완성 메서드를 자식 클래스에서 완성하는 것을 '구현한다'
💡 추상 클래스 안에는 반드시 추상 메서드가 포함돼야 할까?
추상 클래스는 반드시 추상 메서드를 포함할 필요는 없다. 내부에 모두 완성된 메서드, 즉 일반 메서드만 존재해도 추상 클래스로 정의할 수 있다.
하지만 추상 클래스로 정의하면 객체를 직접 생성하지 못하는 제약 조건이 있으므로 멀쩡한 클래슬르 추상 클래스로 정의할 필요가 없다.

 

1.3 추상 클래스 타입의 객체 생성 방법

  1.3.1 추상 클래스를 생성하는 두 가지 방법

  • 추상 클래스를 상속하는 일반 클래스 생성
  • 익명 이너 클래스를 사용

 

  1.3.2 추상 클래스를 상속하는 일반 클래스

  • 객체를 여러 개 만들어야 하는 상황에서는 자식 클래스를 직접 정의하는 방법이 적절하다.
  • 한 번 정의한 이후에는 자식 클래스 생성자의 호출만으로도 객체를 몇 개든 생성할 수 있기 때문이다.
package java_001;

abstract class A {
	abstract void abc();
}
class B extends A {
	void abc() {
		System.out.println("방법 1. 자식 클래스 생성 및 추상 메서드 구현");
	}
}
public class Study_001 {
	public static void main(String[] args){ 
		//객체 생성
		A b1 = new B();
		A b2 = new B();
		
		//메서드 호출
		b1.abc();
		b2.abc();
	}
}

 

  1.3.3 익명 이너 클래스

  • 컴파일러가 내부적으로 추상 클래스를 상속해 메서드 오버라이딩을 수행한 클래스를 생성하고, 그 클래스로 객체를 만드는 방법이다. 이때 내부적으로 생성된 클래스명을 알 수 없어서 개발자 입장에선 익명이 되는 것.
  • 추가로 자식 클래스를 정의하지 않아도 되고, 코드도 간결해진다는 장점이 있다.
  • 한 번만 만들어 사용할 객체에선 익명 이너 클래스 활용이 좋다.
package java_001;

abstract class A {
	abstract void abc();
}
public class Study_001 {
	public static void main(String[] args){ 
		//객체 생성
		A a1 = new A() {
			void abc() {
				System.out.println("방법 2. 익명 이너 클래스 방법으로 객체 생성");
			}
		};
		A a2 = new A() {
			void abc() {
				System.out.println("방법 2. 익명 이너 클래스 방법으로 객체 생성");
			}
		}; //객체 생성 시 마다 오버라이딩을 해줘야 한다.
		//메서드 호출
		a1.abc();
		a2.abc();
	}
}

 

 

 

2. 인터페이스

2.1 인터페이스의 정의와 특징

인터페이스는 내부의 모든 필드가 public static final로 정의되고, static과 default 메서드 이외의 모든 메서드는 public abstract로 정의된 객체 지향 프로그래밍 요소다. class 대신 inerface 키워드를 사용해 선언한다.

interface 인터페이스명 {
     public static final 자료형 필드명 = 값;
     public abstract 리턴 타입 메서드명();
}
//예시
interface A {
     public static final int a = 3;
     public abstract void abc();
}

인터페이스 내에서 필드와 메서드에 사용할 수 있는 제어자는 확정돼 있으므로 필드와 메서드 앞에 제어자를 생략해도 컴파일러가 자동으로 각각의 제어자를 삽입해준다.

 

  2.1.1 예제

package java_001;

interface A {
	public static final int a= 3;
	public abstract void abc();
}
interface B {
	int b = 3; //생략했을 때 자동으로 public static final 삽입
	void bcd(); //생략했을 때 자동으로 public abstract 삽입
	//중괄호가 없지만 오류가 나지 않는 점에서 자동 삽입을 확인할 수 있다.
}
public class Study_001 {
	public static void main(String[] args){ 
		//static 자동 추가 확인
		System.out.println(A.a);
		System.out.println(B.b);
		
		//final 자동 추가 확인
		//A.a = 5;
		//B.b = 5;
		//둘 다 불가능하다.
	}
}

 

2.2 인터페이스의 상속

클래스가 클래스를 상속할 때 extends 키워드를 사용한 반면, 클래스가 인터페이스를 상속할 때는 implements 키워드를 사용한다.

상속에 있어 가장 큰 특징은 다중 상속이 가능하다는 점이다.

클래스명 implements 인터페이스명, ..., 인터페이스명 {
     //내용
}

 

  2.2.1 클래스 상속과 차이점

  • 클래스 다중 상속이 불가능한 이유는 두 부모 클래스에 동일한 이름의 필드 또는 메서드가 존재할 때, 이를 내려받으면 충돌이 발생하기 때문이다.
  • 그러나 인터페이스에서는 모든 필드가 public static final로 정의돼 있어 실제 데이터값은 각각의 인터페이스 내부에 존재해있다. 즉, 저장 공간이 분리돼 공간상 겹치지 않는다.
  • 또한 메서드도 모두 미완성이어서 어차피 자식 클래스 내부에서 완성해 사용하므로 문제될 것이 없다.

 

  2.2.2 클래스와 인터페이스를 동시에 상속

클래스명 extends 클래스명 implements 인터페이스명, ..., 인터페이스명 {
     //extends와 implements의 순서는 바꿀 수 없다
     //내용
}
package java_001;

interface A {}
interface B {}

//단일 인터페이스 상속
class C implements A {}

//다중 인터페이스 상속
class D implements A, B {}

//클래스와 인터페이스를 한 번에 상속
class E extends C implements A, B {}

public class Study_001 {
	public static void main(String[] args){ 
	}
}

 

  2.2.3 extends와 implements 키워드

클래스 extends 클래스 {
     // ...
}
인터페이스 extends 인터페이스 {
      // ...
}
클래스 implements 인터페이스 {
     // ...
}
인터페이스 extends/implements 클래스 {
     // 안 됨!
}
  • 클래스가 클래스를 상속할 때는 extends
  • 클래스가 인터페이스를 상속할 때는 implements
  • 인터페이스가 인터페이스를 상속할 때는 extends
  • 인터페이스는 클래스를 상속할 수 없다.

 

  2.2.4 인터페이스 상속 시 주의할 점

  • 인터페이스 내부에는 미완성 메서드가 있다. 자식 클래스에서 상속할 때는 반드시 미완성 메서드를 완성시켜 줘야 문법적 오류를 피한다.
  • 자식 클래스에서 오버라이딩을 수행할 때, 접근 지정자는 반드시 부모 메서드의 접근 지정자보다 접근 범위가 같거나 커야 한다. 인터페이스의 모든 필드와 메서드는 public으로 강제되므로 사실상 모든 자식 클래스의 구현 메서드는 public만 가능할 것이다.
package java_001;

interface A {
	public abstract void abc();
}
interface B {
	void bcd(); //public abstract 자동 추가
}
class C implements A {
	public void abc() {
		// ...
	}
}
/* public -> default 불가능
class D implements B {
	void bcd() {
	}
} */
public class Study_001 {
	public static void main(String[] args){ 
	}
}

 

2.3 인터페이스 타입의 객체 생성 방법

인터페이스도 추상 메서드를 포함하고 있으므로 객체를 직접 생성할 순 없다.

일반 추상 클래스와 마찬가지로 자식 클래스를 정의하고, 자식 클래스의 생성자로 객체를 생성하거나 익명 이너 클래스를 이용해 바로 객체를 생성하는 방법을 사용할 수 있다.

 

  2.3.1 예제 : 자식 클래스를 직접 정의해 인터페이스 객체 생성

package java_001;

interface A {
	int a = 3;
	void abc();
}
class B implements A {
	public void abc() {
		System.out.println("방법 1. 자식 클래스 생성자로 객체 생성");
	}
}
public class Study_001 {
	public static void main(String[] args){ 
		//객체 생성
		A b1 = new B();
		A b2 = new B();
		
		//메서드 호출
		b1.abc();
		b2.abc();
	}
}

 

  2.3.2 예제 : 익명 이너 클래스를 활용해 인터페이스 객체 생성

package java_001;

interface A {
	int a = 3;
	void abc();
}
public class Study_001 {
	public static void main(String[] args){ 
		//객체 생성
		A a1 = new A() {
			public void abc() {
				System.out.println("방법 2. 익명 이너 클래스를 이용한 객체 생성");
			}
		};
		A a2 = new A() {
			public void abc() {
				System.out.println("방법 2. 익명 이너 클래스를 이용한 객체 생성");
			}
		};
		
		//메서드 호출
		a1.abc();
		a2.abc();
	}
}

 

2.4 인터페이스의 필요성

인터페이스는 입출력의 호환성을 의미한다.

일종의 규격이 되는 것이다. 어떤 회사든 동일한 인터페이스를 상속해 드라이버를 제작했다면, 동일한 메서드가 존재할 것이다. 메서드 내부 구현 내용은 서로 다르겠지만 이를 이용하는 또다른 개발자 입장에선 내부 메서드가 어떻게 생겼는지는 고려할 필요가 없을 것이다. 즉, 단말이나 장비를 바꾸어도 어플리케이션을 수정할 필요가 없다는 것이다.

인터페이스는 일반적으로 자바 API(Application Programming Interface)에서 제공한다. 자바 API는 자바에서 기능을 제어할 수 있게 만든 인터페이스를 의미한다.

 

2.5 디폴트 메서드와 정적 메서드

자바 8이 등장하면서 인터페이스에 몇 가지 기능이 추가됐다. 그 첫 번째가 인터페이스 내에 완성된 메서드인 디폴트 메서드가 포함될 수 있다는 것이다.

💡 인터페이스 내의 default 메서드는 default 접근 지정자와 구분해야 한다. default 접근 지정자는 접근 지정자 자체를 생략한다.

 

만약 인터페이스 내에 새로운 메서드를 한 개 추가하면 이전에 만들어 사용하던 모든 자식 클래스에 오류가 발생할 것이다. 새롭게 추가된 메서드를 구현하지 않았기 때문이다. 자바 8 이전에는 자식 클래스를 그대로 사용하기 위해 새로운 인터페이스를 정의해서 사용해야 했지만, 이제는 아니다.

해결 방법은 디폴트 메서드로 인터페이스 내부에 완성된 메서드를 삽입하는 것이다. 완성된 메서드이므로 자식 클래스는 이 메서드를 반드시 오버라이딩할 필요가 없다.

물론 인터페이스 자체가 여전히 객체를 생성할 수 없는 상태이므로 디폴트 메서드를 실행하기 위해서는 일단 상속시켜야 한다는 점을 기억하자.

 

  2.5.1 예제 : 인터페이스 구현 및 디폴트 메서드의 오버라이딩

package java_001;

interface A {
	void abc();
	default void bcd() {
		System.out.println("A 인터페이스의 bdc()");
	}
}
class B implements A {
	public void abc() {
		System.out.println("B 클래스의 abc()");
	}
}
class C implements A {
	public void abc() {
		System.out.println("C 클래스의 abc()");
	}
	public void bcd() {
		System.out.println("C 클래스의 bcd()");
	}
}
public class Study_001 {
	public static void main(String[] args){ 
		//객체 생성
		A a1 = new B();
		A a2 = new C();
		
		//메서드 호출
		a1.abc();
		a1.bcd();
		a2.abc();
		a2.bcd();
	}
}
B 클래스의 abc()
A 인터페이스의 bdc()
C 클래스의 abc()
C 클래스의 bcd()

 

  2.5.2 자식 클래스에서 부모 인터페이스의 디폴트 메서드 호출 방법

부모 인터페이스명.super.디폴트 메서드명
  • 부모 클래스에서 메서드를 호출할 때와 달리, 부모 인터페이스의 메서드를 호출할 때는 앞에 부모 인터페이스명을 써줘야 한다. 인터페이스는 다중 상속이 가능하기 때문이다.
  • 참고로 아무런 클래스도 상속하지 않으면 컴파일러는 자동으로 Object 클래스를 상속하는 구문을 추가한다.
package java_001;

interface A {
	default void abc() {
		System.out.println("A 인터페이스의 abc()");
	}
}
class B implements A {
	public void abc() {
		A.super.abc();
		System.out.println("B 클래스의 abc()");
	}
}
public class Study_001 {
	public static void main(String[] args){ 
		//객체 생성
		B b = new B();
		
		//메서드 호출
		b.abc();
	}
}
A 인터페이스의 abc()
B 클래스의 abc()

 

  2.5.3 두 번째 추가 기능

  • 자바 8 이후부터 추가된 인터페이스의 두 번째 기능은 static 메서드를 포함할 수 있다는 것이다.
  • 클래스 내부의 정적 메서드와 동일한 기능으로, 객체를 생성하지 않고 아래와 같은 방식으로 바로 호출할 수 있다.
인터페이스명.정적 메서드명
package java_001;

interface A {
	static void abc() {
		System.out.println("A 인터페이스의 정적 메서드 abc()");
	}
}
public class Study_001 {
	public static void main(String[] args){ 
		//정적 메서드 호출
		A.abc();
	}
}