인터페이스
git/eomcs-java-basic/src/main/java com.eomcs.oop.ex09
git/eomcs-java-basic/src/main/java com.eomcs.oop.ex10.b
다중 상속
● 일반 클래스의 단일상속
클래스 사이의 다중 상속이 안되는 것은 상속을 받은 상위 클래스들이 같은 메서드를 갖고 있다고 가정하면, 이 메서드를 호출할 때 둘 중 어떤 것을 호출한 것인지 구별할 수 없기 때문이다.
안녕하세요ㅕㅛ 야 너 왜 혼자 쿠크다스 다 쳐먹야 뒤지고 시펑?>
● 인터페이스의 다중상속
그러나 인터페이스는 다중 상속이 가능하다. 어차피 메서드가 구현이 되지 않았으므로, 두 인터페이스의 메서드를 하나의 메서드로 상속받아도 문제가 없기 떄문이다. 그러나 다중 상속을 하되, 리턴 타입이 다른 경우에는 다중 상속이 불가능하다. 둘 중에 어떤것을 상속받은 것인지에 따라 결과가 달라지면 안되기 때문이다.
public interface A {
void m1();
}
public interface B2 {
int m1();
void m2();
}
public interface C2 extends A, B2 { // 컴파일 에러
void m3();
}
디폴트 메서드
인터페이스에 새로운 기능을 추가하고 싶을 때 해당 인터페이스를 수정하면 그 인터페이스를 구현한 모든 클래스가 영향을 받는다.
예를 들어, 다음 Computer 인터페이스에 새로운 기능을 추가하고 싶다고 해서 Computer 인터페이스에 메서드를 새로 추가하게 되면 이를 구현한 FirstComputer, SecondComputer, ThirdComputer 클래스에서 그 기능을 따로 추가하지 않는 한, 오류가 뜬다.
public interface Computer {
void compute();
// void touch();
}
public class FirstComputer implements Computer {
public void compute() {
System.out.println("단순히 계산을 수행한다!");
}
}
public class SecondComputer implements Computer {
public void compute() {
System.out.println("멀티태스킹 기능도 수행한다!");
}
}
public class ThirdComputer implements Computer {
public void compute() {
System.out.println("게이밍 컴퓨터!!!");
}
}
따라서 다음과 같이 Computer 인터페이스를 상속받은 새로운 인터페이스 Computer2를 만들고 여기에 추가한다.
public interface Computer2 extends Computer {
void touch();
}
public class NewComputer1 implements Computer2 {
public void compute() {
System.out.println("새 컴퓨터..");
}
public void touch() {
System.out.println("오호라.. 터치가 되네. 이거 서피스 프로인가?");
}
}
public class NewComputer2 implements Computer {
public void compute() {
System.out.println("새 컴퓨터..");
}
public void touch() {
System.out.println("오호라.. 터치가 되네. 이거 서피스 프로인가?");
}
}
그러나 기존의 인터페이스를 사용하는 코드들과 호환이 되지 않는다는 단점이 있다. 즉, 메서드에서 사용되고 있는 Computer 타입의 레퍼런스 변수에 Computer2를 구현한 클래스가 들어갈 수는 있으나 이 변수로 추가된 메서드를 호출하지 못하므로 같은 기능이라도 구현한 인터페이스에 따라 메서드를 따로 만들어줘야한다.
public class User {
public static void main(String[] args) {
play(new FirstComputer());
play(new SecondComputer());
play(new ThirdComputer());
play2(new NewComputer());
}
static void play(Computer computer) {
computer.compute();
System.out.println("----------");
}
static void play2(Computer2 computer2) {
computer2.compute();
computer2.touch();
System.out.println("----------");
}
}
기존 규칙을 변경하되, 기존 구현체에는 영향을 끼치지 않으면서 기존 인터페이스와의 호환을 유지하고 싶을 때, 디폴트 메서드를 사용할 수 있다. 이것은 인터페이스에서 이미 구현된 메서드로, 새로 추가되어도 기존 구현체들이 이를 추가적으로 구현할 필요가 없다. 디폴트 메서드는 이처럼 일반 클래스나 추상클래스의 상속 기능처럼 하위 클래스에 상속해주기 위한 목적이 아니라 호환을 유지하면서 새로운 기능을 추가하는 목적으로 사용된다.
public interface Computer {
void compute();
default void touch(){
}
}
그러나 디폴트 메서드는 구현체에게 해당 메서드를 구현하도록 강제할 수 없어, 개발자가 새 클래스로 구현할 때 추가된 메서드의 구현을 잊을 수도 있다는 단점이 있다. 따라서 default 메서드는 꼭 필요한 경우가 아니면, 사용을 최대한 줄여야한다.
package com.eomcs.oop.ex09.e2;
public class NewComputer3 implements Computer {
@Override
public void compute() {
System.out.println("새 컴퓨터...");
}
// public void touch() {}
// 구현하지 않아도 컴파일 오류 X
}
슈퍼 클래스의 인터페이스 구현
슈퍼 클래스가 한 인터페이스를 구현하면 서브 클래스도 그것을 구현하는 클래스가 된다. 따라서 서브 클래스의 선언부에서 implements가 없다해도 인터페이스를 구현하지 않았다고 착각해선 안된다.
public interface A {
void m1();
}
public class Exam01 implements A {
@Override
public void m1() {}
}
public class Exam02 extends Exam01 {
}
public class Exam03 {
public static void main(String[] args) {
A r1 = new Exam01();
A r2 = new Exam02();
}
}
인터페이스와 스태틱 메서드
인터페이스에는 스태틱 메서드, 디폴트 메서드, 추상 메서드가 있을 수 있다. 추상 메서드를 제외한 다른 메서드들은 모두 인터페이스 안에서 이미 구현된 메서드이다.
public class Exam0120 implements A {
@Override
static String m1() {
return "ok";
}
@Override
default void m2() {
// default 메서드는 구현해도 되고 안해도 된다.
}
@Override
void m3() {
// 추상 메서드는 반드시 구현해야 한다.
}
}
인터페이스를 일반 클래스에서 구현할 때, 인터페이스의 메서드의 종류에 따라 메서드 구현의 필요 여부가 다르다.
- 스태틱 메서드 : 다른 클래스에서 구현되어서는 안된다. 애초에 스태틱 메서드는 오버라이딩이 불가능하므로, 인터페이스안에 구현된 스태틱 메서드를 다른 클래스에서 오버라이딩할 방법이 없기 때문이다.
- 디폴트 메서드 : 디폴트 메서드는 이미 인터페이스 안에서 구현이 되어있으므로, 이를 구현하는 클래스에서 메서드를 구현하라고 강제되지 않는다. 따라서 구현해도 되고, 안해도 된다.
- 추상 메서드 : 추상 메서드를 구현하지 않으면 일반 클래스가 될 수 없다.
public class Exam0120 implements A {
// @Override
// public static void m1() {
// }
// 컴파일 에러!
@Override
public void m2() {
}
@Override
public void m3() {
}
}
스태틱 메서드는 클래스를 갖고 호출이 가능하다. 인터페이스는 인스턴스를 생성할 수 없으므로 비록 직접 인스턴스 메서드를 호출 할 수 없으나, 인스턴스를 생성할 필요가 없는 스태틱 메서드는 호출이 가능하다. 또한 구현체의 인스턴스로는 스태틱 메서드를 호출할 수 없다. 구현은 상속과는 엄연히 다르고, default는 상속이 아니라 구현해야하는 것을 미리 구현해준 개념일 뿐이다.
package com.eomcs.oop.ex09.g;
public class Exam0130 {
public static void main(String[] args) {
System.out.println(A.m1());
Exam0120 e1 = new Exam0120();
// System.out.println(e1.m1()); // 컴파일 오류
}
}
그렇다면 스태틱 메서드는 언제 쓰이는가?
인터페이스를 구현한 객체를 다룰 때에 종종 쓰인다.
public interface CarCheckInfo {
int getGas();
int getBrakeOil();
int getEngineOil();
// 인터페이스에서 스태틱 메서드는 보통
// 그 인터페이스를 구현한 객체를 다루는 일을 한다.
static boolean validate(CarCheckInfo carInfo) {
if (carInfo.getBrakeOil() == 0 ||
carInfo.getEngineOil() == 0||
carInfo.getGas() == 0) {
return false;
}
return true;
}
}
package com.eomcs.oop.ex09.h;
public abstract class Car implements CarCheckInfo {
int gas;
int brakeOil;
int engineOil;
String maker;
String model;
int cc;
@Override
public int getGas() {
return gas;
}
@Override
public int getBrakeOil() {
return brakeOil;
}
@Override
public int getEngineOil() {
return engineOil;
}
public void start() {
System.out.println("시동 건다!");
}
public void shutdown() {
System.out.println("시동 끈다!");
}
public abstract void run();
}
public class Tico extends Car {
@Override
public void run() {
System.out.println("붕붕~ 잘 달린다.");
}
}
public class Exam0110 {
public static void main(String[] args) {
Car c1 = new Tico();
if (CarCheckInfo.validate(c1)) {
c1.start();
c1.run();
c1.shutdown();
} else {
System.out.println("자동차 점검하시기 바랍니다.");
}
}
}
추상 클래스를 활용한 인터페이스의 구현
서블릿(Servlet - Server Application 의 작은 조각) 인터페이스 안에 init(), service(), detroy(), getServletInfo(), getServletConifg() 추상 메서드가 있다. 이 인터페이스를 일반 클래스가 구현하려면 인터페이스에 선언된 모든 메서드를 구현해야한다. 그러나 인터페이스를 구현하는 모든 클래스가 인터페이스에 있는 모든 메서드를 직접 구현할 필요는 없다. 코드를 객체마다 다르게 할 필요가 없는, 이미 몸체가 결정된 메서드들은 한 추상클래스 안에 미리 구현해서 넣고, 다른 클래스는 이 클래스를 상속 받도록 하여 간접적인 구현이 가능하다.
public interface Servlet {
void init();
void service();
void destroy();
String getServletInfo();
String getServletConfig();
}
public abstract class AbstractServlet {
public void init() {}
public void destroy() {}
public String getServletInfo() {
return null;
}
public String getServletConfig() {
return null;
}
}
public class Exam03 extends AbstractServlet {
public void servie() {}
}
추상 클래스는 이런 방식으로 인터페이스를 쉽게 구현하기 위한 도구가 될 수 있다. 또, 추상클래스를 여러개 둠으로써 단계적으로 이를 간접적으로 구현할 수도 있다.
실습 - 인터페이스와 추상 클래스
git/eomcs-java-project/mini-pms-24
저번에는 AbstractList라는 추상클래스를 만들었고 이를 상속받는 ArrayList와 LinkedList의 객체를 사용했다. 그런데 문제는 ArrayList나 LinkedList 이외에 HashSet과 같이 Abstract을 상속받지 않은 객체는 사용이 불가능하다. 현재 핸들러 클래스의 의존객체 타입이 AbstractList 혹은 그 하위 객체로 한정되어있기 때문이다.
이 문제를 해결하기 위해 AbstractList가 아닌 List라는 인터페이스를 만들고, 이를 핸들러 클래스에서 레퍼런스 변수 타입으로 사용할 것이다. 이를 통해 꼭 Abstract의 하위 객체가 아니더라도 인터페이스를 구현한 객체라면 뭐든 들어갈 수 있기 때문에 더 유연성이 커지고 Handler 클래스를 유지보수하기도 쉬워진다.
훈련 목표
-
List라는 인터페이스를 만들고 AbstractList가 이를 구현하도록 한다.
-
Handler 클래스의 의존객체를 다룰 레퍼런스 변수로 AbstractList 대신 List를 사용한다.
1단계 : List<E> 인터페이스를 만들고 AbstractList<E>에 있는 모든 메서드를 복사해서 가져다 놓는다. 구현된 메서드는 추상 메서드로 변경한다. AbstractList<E>의 선언부에는 implements List<E>를 추가한다.
public interface List<E> {
int size();
boolean add(E e);
void add(int index, E element);
E get(int index);
E set(int index, E element);
E remove(int index);
Object[] toArray();
E[] toArray(E[] arr);
}
public abstract class AbstractList<E> implements List<E> {
2단계 : Handler클래스에서 필드로 선언된 memberList, boardList, ProjectList, TaskList 변수 타입을 List<member>, List<Board>, List<Project>, List<Task>로 변경한다. 마찬가지로 생성자에 이 필드의 인스턴스를 받는 파라미터의 타입을 List<>로 바꾼다.
List<Board> boardList;
public BoardHandler(List<Board> list) {
this.boardList = list;
}
이렇게 하면 Handler클래스의 의존 객체를 App클래스에서 주입할때 굳이 AbstractList의 하위 객체를 주지 않아도 List라는 인터페이스를 구현한 객체라면 뭐든 들어갈 수 있다.
Singleton
git/eomcs-java-basic/src/main/java com.eomcs.design_pattern.singleton
Singleton
클래스의 객체를 두 개 이상 생성하지 못하도록 생성자의 접근을 막는 대신 객체를 생성할 수 있는 다른 스태틱 메서드를 제공한다. 이 스태틱 메서드를 여러번 호출하더라도 객체는 하나밖에 생성되지 않을 것이다.
객체를 하나만 생성하는 메서드의 원리는 다음과 같다.
- 생성할 객체를 스태틱 필드로 선언한다.
- 생성자를 protected 접근 제어자로 막는다.
- 생성자를 대신 생성할 스태틱 메서드를 만든다. 필드가 null이라면 객체 하나를 생성해서 필드에 저장하고 이 필드를 리턴하며, 이미 필드에 객체가 들어있다면 생성하지 않고 그대로 필드를 리턴한다.
class Car2 {
String model;
int cc;
// 인스턴스 주소를 받을 클래스 필드를 선언한다.
private static Car2 instance;
// 1) 생성자를 정의하고 private으로 선언하여 비공개로 만들어라.
// => 비공개 생성자를 외부에서 호출할 수 없다.
// => 오직 내부에서만 호출할 수 있다.
private Car2() {}
// 2) 인스턴스를 생성해주는 메서드를 정의한다.
public static Car2 getInstance() {
if (Car2.instance == null) {
// 아직 인스턴스를 생성한 적이 없다면 즉시 인스턴스를 생성한다.
Car2.instance = new Car2();
}
// 기존에 변수에 저장된 인스턴스 주소를 리턴한다.
return Car2.instance;
}
}
이 Car2를 테스트해보면 getInstance() 메서드를 여러번 호출해도 리턴되는 객체의 주소는 항상 똑같다.
public class Test02 {
public static void main(String[] args) {
// 생성자가 존재하지만 private으로 비공개 되어 있기 때문에 직접 호출할 수 없다.
// 생성자를 호출할 수 없으면 인스턴스를 생성할 수 없다.
// => 다른 메서드를 호출하여 인스턴스를 생성하라는 의미다.
//Car2 c1 = new Car2(); // 컴파일 오류!
// 인스턴스를 생성해주는 메서드를 통해 인스턴스를 얻는다.
Car2 c2 = Car2.getInstance();
Car2 c3 = Car2.getInstance();
if (c2 != c3)
System.out.println("다르다!");
else
System.out.println("같다!");
}
}
// 같다!
'국비 교육' 카테고리의 다른 글
2020.9.7일 자 수업 : Iterator, 중첩 클래스 (0) | 2020.09.07 |
---|---|
2020.9.3일자 수업 : 추상클래스, 인터페이스 (0) | 2020.09.03 |
2020.8.31일자 수업 : 첫 비대면 수업 (0) | 2020.09.02 |