자바의 정석을 보며 새로 알게되거나 잊었던 사실들을 포스팅한다
추상 클래스
클래스가 설계도라면 추상 클래스는 미완성 설계도이다. 추상 클래스는 미완성 메서드를 포함하고 있다.
추상 클래스는 인스턴스를 생성하지 못하며, 상속을 통해 자손 클래스를 통해서만 완성될 수 있다.
추상클래스는 클래스 선언 앞에 abstract 키워드를 붙이면 된다.
참고로, 추상 메서드를 포함하고 있지 않은 클래스에도 abstract를 붙여 추상 클래스로 만들 수 있다. 추상 클래스로 지정되면 추상 메서드가 없어도 인스턴스 생성이 불가능해진다.
추상 메서드
설계만 해놓고 실제 수행될 내용을 작성하지 않은 것을 추상 메서드라고 한다. 메서드를 미완성으로 남겨 놓는 이유는, 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만을 작성하고, 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려 주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워두는 것이다.
추상 클래스와 추상 메서드는 다음과 같이 구현한다.
abstract class Player {
int name;
abstract void play(int pos);
abstract void stop();
void replay() {};
}
class AudioPlayer extends Player {
void play(int pos) { ... }
void stop() { ... }
}
abstract class AbstractPlayer extends Player {
void play(int pos) { ... }
}
추상 클래스 Player를 선언하고 AudioPlayer에서 이를 상속해 구현한다. 추상 클래스의 일부만 구현한 새로운 클래스 AbstractPlayer도 만들 수 있다. 하지만 추상 메서드가 전부 구현되지 않았으므로 abstract를 붙여 추상 클래스임을 나타내야한다.
추가적으로 추상 클래스에서는 멤버 변수나 일반 메서드의 선언이 가능하다.
사실 추상 클래스 Player를 만드는 대신에, play()와 stop()의 구현부를 비워두어도 된다. 이렇게 하면 추상 메서드가 아니라 일반 메서드로 간주되기 때문이다.
class Player {
...
void play(int pos) {}
void stop() {}
...
}
하지만 abstract를 붙여 추상 메서드로 선언하는 이유는 자손 클래스에서 추상 메서드를 구현하도록 강요하기 위해서이다.
만약 추상 메서드로 구현되어 있지 않다면 상속받는 자손 클래스에서는 이 메서드들이 온전히 구현된 것으로 인식하고 오버라이딩을 통해 자신의 클래스에 맞도록 구현하지 않을 수도 있기 때문이다.
인터페이스
인터페이스는 일종의 추상 클래스이다. 인터페이스는 추상 클래스처럼 추상 메서드를 갖지만 추상화 정도가 높아서 추상 클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버 변수를 구성원으로 가질 수 없다. 오직 추상 메서드와 상수만을 멤버로 가질 수 있다.
추상 클래스가 미완성 설계도라고 한다면, 인터페이스는 구현된 것은 아무 것도 없고 밑그림만 그려져 있는 기본 설계도라고 할 수 있다. 이러한 특징때문에 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.
인터페이스는 interface 키워드를 이용하여 작성하고 다음과 같은 제약이 있다.
- 모든 멤버 변수는 public static final 이어야 하며, 이를 생략할 수 있다.
- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다. (JDK1.8부터 static 메서드와 default 메서드는 예외)
인터페이스의 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에 편의상 생략하는 경우가 많다. 생략된 제어자는 컴파일러가 알아서 추가해준다.
또, 인터페이스의 모든 메서드는 추상 메서드여야 하는데, JDK1.8부터 static 메서드와 디폴트 메서드의 추가를 허용하는 방향으로 변경되었다.
interface PlayingCard {
public static final int SPADE = 4;
// 아래 변수는 알아서 컴파일러가 public static final로 만들어준다.
int DIAMOND = 3;
int HEART = 2;
int CLOVER = 1;
public abstract String getCardNumber();
// 아래 메서드는 알아서 컴파일러가 public abstract로 만들어준다.
String getCardKind();
}
인터페이스의 상속
인터페이스는 인터페이스로부터만 상속받을 수 있으며 클래스와는 달리 다중 상속, 즉 여러개의 인터페이스로부터 상속을 받는 것이 가능하다.
interface Movable {
}
interface Attackable {
}
interface Fightable extends Movable, Attackable { }
인터페이스의 구현
인터페이스의 구현은 implements 키워드를 사용해서 구현한다.
interface Movable {
}
class Fightable implements Movable {
...
}
또, 구현하는 인터페이스의 메서드 중 일부만 구현한다면 abstract를 붙여서 추상 클래스로 선언해야 한다.
class Fighter extends Unit implements Fightable {
...
}
위와 같이 상속과 구현을 동시에 할 수도 있다.
인터페이스를 이용한 다형성
자손 클래스의 인스턴스는 조상타입의 참조변수로 참조하는 것이 가능하다. 마찬가지로 인터페이스도 이를 구현한 클래스의 조상이라고 할 수 있으므로, 해당 인터페이스의 참조 변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있다.
인터페이스 Fightable을 구현한 클래스를 Fighter라고 할 때 다음과 같이 참조한다.
Fightable f = (Fightable) new Fighter();
// 또는
Fightable f = new Fighter();
따라서 다음과 같이 메서드의 매개변수 타입으로도 활용할 수 있다.
void attack(Fightable f) { ... }
매개변수에는 Fightable 인터페이스를 구현한 클래스가 제공되어야 한다.
Fightable method() {
...
Fightable f = new Fighter();
return f;
}
또한, 리턴 타입으로도 사용할 수 있는데 이때도 마찬가지로 해당 인터페이스를 구현한 클래스의 인스턴스를 반환해야 한다.
인터페이스의 장점
인터페이스를 사용하는 이유와 그 장점을 정리해보면 다음과 같다.
1. 개발시간을 단축시킬 수 있다.
일다 인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는 것이 가능하다. 메서드를 호출하는 쪽에서는 메서드의 내용과 관계없이 선언부만 알면 되기 떄문이다.
그리고 동시에 다른 한쪽에서는 인터페이스를 구현하는 클래스를 작성하게 하면, 인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않고도 양쪽에서 동시에 개발을 진행할 수 있다.
2. 표준화가 가능하다.
프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 함으로써 보다 일관되고 정형화된 프로그램의 개발이 가능하다.
3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
서로 상속관계에 있지도 않고, 같은 조상 클래스를 가지고 있지 않은 서로 아무런 관계도 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있다.
4. 독립적인 프로그래밍이 가능하다.
인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제구현에 독립적인 프로그램을 작성하는 것이 가능하다. 클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.
인터페이스의 이해
다음과 같은 코드가 있다고 가정하자.
class A {
public void methodA(B b) {
b.methodB();
}
}
class B {
public void methodB() {
System.out.println("methodB()");
}
}
class InterfaceTest {
public static void main(String args[]) {
A a = new A();
a.methodA(new B());
}
}
여기서 클래스 A는 클래스 B의 인스턴스를 호출하고 메서드를 호출한다.
이 경우에, 클래스 A를 작성하려면 클래스 B가 작성되어 있어야 한다. 그리고 클래스 B의 methodB()의 선언부가 변경되면, 이를 사용하는 클래스 A도 변경되어야 한다. 이와 같이 직접적인 두 관계의 두 클래스는 한 쪽이 변경되면, 다른 한 쪽도 변경되어야 한다는 단점이 있다.
그러나 클래스 A가 클래스 B를 직접적으로 호출하지않고, 인터페이스를 매개체로 해서 클래스 A가 클래스 B의 메서드에 접근하도록 하면, 클래스 B에 변경사항이 생기거나 클래스 B와 같은 기능의 다른 클래스로 대체 되어도 클래스 A는 전혀 영향을 받지 않도록 하는 것이 가능하다.
class A {
void autoPlay(I i) {
i.play();
}
}
interface I {
public abstract void play();
}
class B implements I {
public void play() {
System.out.println("play in B class");
}
}
class C implements I {
public void play() {
System.out.println("play in C class");
}
}
class InterfaceTest2 {
public static void main(String[] args) {
A a = new A();
a.autoPlay(new B());
a.autoPlay(new C());
}
}
위와 같이, play() 메서드가 있는 인터페이스 I를 선언한 뒤, 클래스 B와 C에서 그것을 구현했다. 그리고 클래스 A의 메서드 autoPlay에서는 인터페이스 I를 매개변수로 받아 메서드를 실행하고 있다.
이렇게 작성하면 클래스 A와 클래스 B는 직접적인 관계에서 간접적인 관계로 바뀌게 되고, 클래스 A에서 클래스 B의 메서드를 직접 실행하는게 아니라, 중간 매개체인 인터페이스를 통해서 간접적으로 클래스 B의 메서드를 실행하게 된다.
결론적으로 클래스 A는 여전히 클래스 B의 메서드를 호출하지만, 클래스 A는 인터페이스 I하고만 직접적인 관계에 있기 때문에 클래스 B의 변경에 영향을 받지 않는다.
클래스 A는 인터페이스를 통해 실제 사용하는 클래스의 이름을 몰라도 되고, 심지어는 실제로 구현된 클래스가 존재하지 않아도 된다. 클래스 A는 오직 직접적인 관계이 있는 인터페이스 I의 영향만 받는다.
인터페이스 I는 실제 구현 내용(클래스 B)를 감싸고 있는 껍데기이며, 클래스 A는 껍데기안에 어떤 알맹이(클래스)가 들어 있는지 몰라도 된다.
추가로 모든 객체는 Object 클래스에 정의된 메서드를 가지고 있기 때문에, 인터페이스 타입의 참조 변수로도 Object 클래스에 정의된 메서드를 호출할 수 있다.
디폴트 메서드와 static 메서드
원래는 인터페이스에 추상 메서드만 선언할 수 있었는데, JDK1.8부터 디폴트 메서드와 static 메서드도 추가할 수 있게 되었다.
인터페이스는 변경되지 않으면 가장 좋지만, 아무리 설계를 잘해도 언젠가 변경이 필요하다. 디폴트 메서드는 추상 메서드의 구현을 제공하는 메서드로 추상 메서드가 아니기 때문에 추가해도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다. 따라서 변경이 필요할 때 사용한다.
interface Myinterface {
void method();
default void newMethod() {};
}
그런데, 새로 추가되 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌할 수 있다. 그럴땐 다음 규칙으로 해결한다.
1. 여러 인터페이스에서의 디폴트 메서드 간의 충돌
-> 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.
2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌
-> 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.
위의 규칙이 외우기 귀찮다면, 그냥 필요한 쪽의 메서드로 오버라이딩하면 된다.
static 메서드는 클래스처럼 그냥 선언한다음 '인터페이스명.메서드이름' 으로 사용하면 된다.
class DefaultMethodTest {
public static void main(String[] args) {
Child c = new Child();
c.method1();
c.method2();
MyInterface.staticMethod();
MyInterface2.staticMethod();
}
}
class Child extends Parent implements MyInterface, MyInterface2 {
public void method1() {
System.out.println("method1() in Child");
}
}
class Parent {
public void method2() {
System.out.println("method2() in Parent");
}
}
interface MyInterface {
default void method1() {
System.out.println("method1() in MyInterface");
}
default void method2() {
System.out.println("method2() in MyInterface");
}
static void staticMethod() {
System.out.println("staticMethod() in MyInterface");
}
}
interface MyInterface2 {
default void method1() {
System.out.println("method1() in MyInterface2");
}
static void staticMethod() {
System.out.println("staticMethod() in MyInterface2");
}
}
method1() in child
method2() in parent
staticMethod() in MyInterFace
staticMethod() in MyInterFace2
내부 클래스
내부 클래스는 클래스 내에 선언되는 클래스다. 그것 외에는 전부 일반적인 클래스와 같다. 내부 클래스를 사용하면 내부 클래스에서 외부 클래스의 멤버드을 쉽게 접근할 수 있고, 코드의 복잡성을 줄일 수 있다는(캡슐화) 장점이 있다.
class A {
...
class B {
...
}
...
}
위 코드와 같이 선언하면 클래스 A는 외부 클래스가 되고, 클래스 B는 내부 클래스가 된다. 이 때 내부 클래스 B는 외부 클래스 A를 제외하고는 다른 클래스에서 잘 사용되지 않는 것이어야 한다.
내부 클래스의 종류는 인스턴스, 스태틱, 지역, 익명 클래스가 있으며 이는 변수의 선언위치에 따른 종류와 같다.
class Outer {
// 인스턴스 클래스
class InstanceInner {}
// 스태틱 클래스
static class StaticInner {}
void method() {
// 지역 클래스
class LocalInner {}
}
}
익명 클래스는 특이하게 다른 내부 클래스들과 달리 이름이 존재하지 않는다. 단 한번만 사용되고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스다.
class InnerEx6 {
Object iv = new Object(){ void method(){} }; // 익명클래스
static Object cv = new Object(){ void method(){} }; // 익명클래스
void myMethod() {
Object lv = new Object(){ void method(){} }; // 익명클래스
}
}
익명 클래스의 특징은 이름이 없기 때문에 생성자도 없으며 조상 클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 없다.
오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.
import java.awt.*;
import java.awt.event.*;
class InnerEx7{
public static void main(String[] args) {
Button b = new Button("Start");
b.addActionListener(new EventHandler());
}
}
class EventHandler implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println("ActionEvent occurred!!!");
}
}
import java.awt.*;
import java.awt.event.*;
class InnerEx8 {
public static void main(String[] args) {
Button b = new Button("Start");
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("ActionEvent occurred!!!");
}
} // 익명 클래스의 끝
);
} // main메서드의 끝
} // InnerEx8클래스의 끝
위 코드를 아래 코드에서 익명 클래스로 변환한 예제다.
'언어 > Java' 카테고리의 다른 글
java [7] Collections-1 (0) | 2022.04.18 |
---|---|
java [6] 예외처리 (0) | 2022.04.14 |
java [5] 객체지향 프로그래밍-2 (0) | 2022.04.10 |
java [5] 객체지향 프로그래밍-1 (0) | 2022.04.09 |
java [4] 배열 (0) | 2022.04.08 |