[Java] 추상 클래스, 인터페이스, 중첩 클래스
1. 추상 클래스
- Abstract Class(추상 클래스): 완전하게 구현되지 않은 메소드를 가진 클래스
- abstract 키워드를 사용한다.
- 추상 메소드: 몸체가 없는 메소드
- 추상 메소드는 세미콜론(;)로 종료되어야 한다.
- 추상 클래스는 하나 이상의 추상 메소드를 가져야 하고, 추상 클래스를 상속받는 자식 클래스에서는 모든 추상 메소드를 재정의해야 한다.
예제 7-1
abstract class Shape {
int x, y;
public void translate(int x, int y) {
this.x = x;
this.y = y;
}
public abstract void draw();
}
class Rectangle extends Shape{
int width, height;
public void draw() {
System.out.println("직사각형 그리기 메소드");
}
}
class Circle extends Shape {
int radius;
public void draw() {
System.out.println("원 그리기 메소드");
}
}
public class AbstractTest {
public static void main(String [] args) {
// Shape shape = new shape(); // 컴파일 오류
Shape circle = new Circle();
circle.draw();
}
}
추상 클래스의 용도
- 부모 클래스에서 void draw() {}와 같이 내용이 없는 일반 메소드로 정의하는 것은 자식 클래스에서 오버라이드하지 않아도 컴파일 에러가 발생하지 않지만, 추상 클래스로 정의하면 서브 클래스에서 반드시 구현해야 하므로 구현을 강요하는 면에서 장점이 있다.
2. 인터페이스
인터페이스의 용도
- 추상화: 인터페이스는 메소드 몸체(세부 구현)가 없는 메소드 시그니처만 저장한다. 메소드 시그니처만 공개하는 것은 사용자에게 메소드 구현을 숨김으로써 추상화를 달성한다. 추상화는 객체 지향 프로그래밍 기술에서 중요한 개념이다.
- 다중 상속: 기존 방식은 심각한 모호성 문제(다이아몬드 문제) 때문에 다중 클래스 상속이 불가능하다. 인터페이스는 필드의 정의가 금지되므로 이러한 문제를 해결할 수 있다.
- 느슨한 결합: 결합(커플링)이란 하나의 클래스가 다른 클래스에 종속되는 정도를 말한다. 인터페이스를 사영하면 메소드와 메소드 시그니처를 따로 정의할 수 있어서 클래스들이 완전히 독립적인 상태에서 느슨한 결합을 이룰 수 있다.
인터페이스의 필요성과 예
인터페이스 정의
- 키워드 interface를 이용하여 인터페이스를 정의한다.
- 인터페이스는 추상 메소드들과 디폴트 메소드로 이루어진다.
- 인터페이스 안에서 필드(변수)는 선언될 수 없고 상수는 정의될 수 있다.
- 인터페이스 안에서 선언되는 메소드는 묵시적으로 public abstract이기 때문에 별도의 수식어를 사용하지 않아도 된다.
인터페이스 구현
- 인터페이스 안에는 구현되지 않은 메소드가 존재하기 때문에 인터페이스만으로 객체를 생성할 수 없고, 다른 클래스에 의하여 구현될 수 있다.
- 인터페이스를 구현한다는 것은 인터페이스에 정의된 추상 메소드의 몸체를 정의한다는 것이다.
- 클래스가 인터페이스를 구현하기 위해서는 키워드 implement를 사용한다.
인터페이스 vs 추상 클래스
- 인터페이스와 추상 클래스는 객체화될 수 없다.
- 주로 구현이 안 된 메소드들로 이루어진다.
- 추상 클래스에서는 인터페이스와 달리 일반적인 필드도 선언할 수 있으며 일반적인 메소드도 정의할 수 있다.
- 인터페이스에서 추상 클래스와 달리 모든 메소드는 public, abstract으로 선언되며 여러 개의 인터페이스갸 동시에 구현될 수 있다.
추상 클래스를 사용하는 경우
- 관련된 클래스 사이에서 코드를 공유하고 싶은 경우
- 공통적인 필드나 메소드의 수가 많은 경우, 또는 public 이외의 접근 지정자를 사용해야 하는 경우
- 정적이 아닌 필드나 상수가 아닌 필드를 선언하는 경우
인터페이스를 사용하는 경우
- 관련 없는 클래스에서 동일한 동작을 하는 경우
- 누가 구현하는지 신경쓸 필요가 없는 경우
- 다중 상속을 하는 경우
인터페이스와 타입
- 인터페이스 자료형처럼 사용하여 이 인터페이스를 구현한 참조 변수를 정의할 수 있다.
예제 7-2
interface RemoteControl {
public abstract void turnOn();
public abstract void turnOff();
public default void printBrand() {
System.out.println("Remote Control TV");
}
}
class Television implements RemoteControl {
boolean on;
public void turnOn() {
on = true;
System.out.println("TV turn on");
}
public void turnOff() {
on = false;
System.out.println("TV turn off");
}
@Override
public void printBrand() {
System.out.println("PowerJava TV");
}
}
public class TestInterface {
public static void main(String[] args) {
RemoteControl rc = new Television();
rc.turnOff();
rc.turnOff();
rc.printBrand();
}
}
3. 인터페이스를 이용한 다중 상속
인터페이스끼리도 상속이 가능하다
- 다른 프로그래머가 사용하고 있던 인터페이스를 변경하면 이 인터페이스를 구현하는 모든 클래스가 동작하지 않게 된다.
- 이 경우를 대비하여 인터페이스도 상속을 받아서 확장할 수 있다.
- we can inherit interface using keyword extends.
인터페이스를 이용한 다중 상속
- Multiple inheritance(다중 상속): 하나의 클래스가 여러 개의 부모 클래스를 가지는 것
- 다이아몬드 문제
- 하나의 클래스를 상속하는 동시에 여러 인터페이스를 구현하면 다중 상속과 비슷한 효과를 가질 수 있다.
방법1
interface Drivable {
public abstract void drive();
}
interface Flyable {
public abstract void fly();
}
public class FlyingCar1 implements Drivable, Flyable {
public void drive() {
System.out.println("I'm driving");
}
public void fly() {
System.out.println("I'm flying");
}
public static void main(String[] args) {
FlyingCar1 obj = new FlyingCar1();
obj.drive();
obj.fly();
}
}
방법2
interface Flyalbe {
public abstract void fly();
}
class Car {
int speed;
void setSpeed(int speed) {
this.speed = speed;
}
}
public class FlyingCar2 extends Car implements Flyable{
public void fly() {
System.out.println("I'm flying");
}
public static void main(String[] args) {
FlyingCar2 obj = new FlyingCar2();
obj.setSpeed(100);
obj.fly();
}
}
상수 정의
- 인터페이스에서 정의된 변수는 자동으로 public, static, final이 되어 상수가 된다.
예제 7-3
class Shape {
protected int x, y;
}
interface Drawable {
public abstract void draw(int x, int y);
}
class Circle1 extends Shape implements Drawable {
int radius;
public void draw(int x, int y) {
System.out.println("Circle draw at (" + x + ", " + y + ")");
}
}
public class TestInterface2 {
public static void main(String[] args) {
Circle1 circle = new Circle1();
circle.draw(0, 0);
}
}
4. 디폴트 메소드와 정적 메소드
디폴트 메소드
- Default method: interface를 구현하는 class가 method의 몸체를 구현하지 않아도 되도록 하기 위해 Interface 개발자가 method의 default 구현하는 기능
interface MyInterface {
public abstract void myMethod1();
public default void myMethod2() {
System.out.println("myMethod2");
}
}
public class MyClass implements MyInterface {
public void myMethod1() {
System.out.println("myMethod1");
}
public static void main(String[] args) {
MyClass myclass = new MyClass();
myclass.myMethod1();
myclass.myMethod2();
}
}
예제 7-4
interface Drawable {
public abstract void draw();
public default void getSize() {
System.out.println("1024x768 resolution");
}
}
class Circle implements Drawable {
public void draw() {
System.out.println("Circle draw");
}
@Override
public void getSize() {
System.out.println("3000x2000 resolution");
}
}
public class TestClass {
public static void main(String[] args) {
Circle circle = new Circle();
circle.draw();
circle.getSize();
}
}
정적 메소드
- JDK8부터 Interface에 static method를 추가할 수 있다.
- Factory method(팩토리 메소드): new를 호출하여 객체를 생성하는 코드를 부모 클래스에 위임하는 것. 팩토리 메소드를 사용하는 이유는 하나의 클래스가 변경되었을 경우에 다른 클래스의 변경을 최소화하기 위해서다.
예제 7-5
import javax.management.RuntimeErrorException;
interface Employable {
public abstract String getName();
static boolean isEmpty(String str) {
if (str == null || str.trim().length() == 0) {
return true;
} else {
return false;
}
}
}
class Employee implements Employable {
private String name;
public Employee(String name) {
if (Employable.isEmpty(name) == true)
throw new RuntimeException("이름을 반드시 입력하여야 함!");
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
public class StaticMethodTest2 {
public static void main(String [] args) {
Employee employee = new Employee("홍길동");
}
}
5. 중첩 클래스
- 자바에서는 클래스 안에 클래스를 정의할 수 있다.
- 외부 클래스(outer class): 내부에 클래스를 가지고 있는 클래스
- 중첩 클래스(nested class): 클래스 내부에 포함되는 클래스
중첩 클래스의 종류
중첩 클래스 | Description | |
---|---|---|
정적 중첩 클래스 | static이 붙어서 내장되는 클래스 | |
비정적 중첩 클래스 | 내부 클래스 | 클래스의 멤버처럼 선언되는 중첩 클래스 |
지역 클래스 | 메소드의 몸체 안에서 선언되는 중첩 클래스 | |
익명 클래스 | 수식의 중간에서 선언되고 바로 객체화되는 클래스 |
내부 클래스
- 클래스 안에 클래스를 선언하는 경우
- 내부 클래스의 접근 지정자는 public, private, protected, package(default)일 수 있다.
- 내부 클래스는 외부 클래스의 인스턴스 변수와 메소드를 전부 사용할 수 있다. 이것이 내부 클래스의 최대 장점이다.
- 내부 클래스도 외부 클래스의 인스턴스 멤버므로 외부 클래스 객체가 만들어져야 내부 클래스도 만들 수 있다.
class OuterClass {
private int value = 10;
class InnerClass {
public void myMethod() {
System.out.println("외부 클래스의 private 변수 값: " + value);
}
}
OuterClass() {
InnerClass obj = new InnerClass();
obj.myMethod();
}
}
public class InnerClassTest {
public static void main(String [] args) {
OuterClass outer = new OuterClass();
}
}
지역 클래스
- Local Class(지역 클래스): 메소드 안에 정의되는 클래스
- 지역 클래스는 접근 제어 지정자를 가질 수 없으며 abstract 또는 final로만 지정할 수 있다.
- 지역 클래스는 외부 클래스의 인스턴스 변수뿐만 아니라 final로 선언된 메소드의 지역 변수에도 접근할 수 있다.
class LocalInner {
private int data = 30;
void display() {
final String msg = "현재의 데이터 값은 ";
class Local {
void printMsg() {
System.out.println(msg + data);
}
}
Local obj = new Local();
obj.printMsg();
}
}
public class localClassTest {
public static void main(String [] args) {
LocalInner obj = new LocalInner();
obj.display();
}
}
중첩 클래스를 사용하는 이유
- 중첩 클래스는 외부 클래스의 멤버가 previate로 선언되어 있더라도 접근할 수 있다.
- 중첩 클래스는 외부에서 보이지 않는다.
- 익명 클래스는 콜백 메소드를 작성할 때 편리하다.
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.Timer;
public class CallBackTest {
public static void main(String [] args) {
class MyClass implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("beep");
}
}
ActionListener listener = new MyClass();
Timer t = new Timer(1000, listener);
t.start();
for (int i = 0; i < 1000; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) { }
}
}
}
6. 익명 클래스
- Anonymous clas(익명 클래스): 클래스 몸체는 정의되지만 이름이 없는 클래스
- 익명 클래스는 클래스를 정의하면서 동시에 객체를 생성한다.
- 이름이 없기 때문에 한 번만 사용이 가능하다.
- 익명 클래스는 하나의 객체만 생성하면 되는 경우에 사용한다.
- 익명 클래스는 부모 클래스의 상속을 받아서 작성하거나 인터페이스를 구현하여서 작성할 수 있다.
- new 키워드 다음에 부모 크래스 이름이나 인터페이스 이름을 적어주면 된다.
- 익명 클래스도 내부 클래스와 같이 필드와 다른 메소드들을 정의할 수 있지만 메소드 안에 정의되는 지역 변수 중에서 final로 선언된 변수만 사용이 가능하다.
- GUI에서 이벤트 처리 객체는 하나만 생성되면 되기 때문에 익명 클래스는 주로 GUI의 이벤트 처리기를 구현하는 경우에 많이 사용된다.
// 이름이 있는 클래스의 경우
class Car extends Vehicle {
...
}
Car obj = new Car();
// 익명 클래스의 경우
Vehicle obj = new Vehicle() { ... };
interface RemoteControl {
void turnOn();
void turnOff();
}
public class AnonymousClassTest1 {
public static void main(String args []) {
RemoteControl ac = new RemoteControl() {
public void turnOn() {
System.out.println("TV turn on");
}
public void turnOff() {
System.out.println("TV turn off");
}
};
ac.turnOn();
ac.turnOff();
}
}
예제 7-7