개발자입니다
[비트캠프] 56일차(12주차1일) - Java: myapp-22, 인터페이스(인터페이스 필드, default, static 메서드, 다중 상속) 본문
[비트캠프] 56일차(12주차1일) - Java: myapp-22, 인터페이스(인터페이스 필드, default, static 메서드, 다중 상속)
끈기JK 2023. 1. 25. 11:51
21. 인터페이스를 이용하여 loose coupling 구현
### 21. 인터페이스를 이용하여 객체 목록 관리를 규격화 하기
- List 인터페이스를 구현
- ArrayList와 LinkedList는 List 규칙에 따라 재구현
- XxxDao 클래스에 인터페이스 적용
- 인터페이스를 이용하여 느슨하게 결합(loose coupling)하는 방법
loose coupling 구현 : 커플링(종속성)을 느슨하게 만들기
App 에서 main() 실행한다. BoardHandler에서 service() 실행한다. BoardDao 에서 List 를 사용한다. insert() call 한다. BoardDao 에서 List의 insert() call 한다. List 는 LinkedList, ArrayList 의 규칙을 정하는 interface 이다. StudentDao, TeacherDao 에서도 List 사용한다.
22. 데이터 조회 기능을 규격화 → 구현 방식에 상관없이 일관된 조회 가능
### 22. 데이터 조회 로직을 객체화 하기: Iterator 패턴 적용
- 데이터 조회 규격을 Iterator 인터페이스로 정의한다.
- List 구현체에서 Iterator 규격을 따르도록 변경한다.
- 객체 목록을 다룰 때 Iterator 구현체를 사용한다.
ArrayList : 배열을 이용하여 목록을 다룬다.
LinkedList : 노드 간의 연결을 이용해 목록을 다룬다.
→ 둘 다 index를 이용하여 데이터를 조회. add(인덱스, 값) get(인덱스)
Stack : Last In First Out (LIFO), First In Last Out (FILO). push(값), pop()
Queue : First In First Out (FIFO). offer(값), poll()
→ 데이터 저장 방식(mechanism)에 따라 데이터를 꺼내는 방법이 다르다! → 데이터 조회에 대해 일관성이 없다. → 일관성 해결 설계 기법 → Iterator 패턴
App 에서 데이터 조회 객체를 call 해서 get() 을 이용해 ArrayList, LinkedList 에서 데이터 꺼낸다.
pop() 을 이용해 Stack 에서 데이터 꺼낸다.
poll() 을 이용해 Queue 에서 데이터 꺼낸다.
↑ call 로 일관된 호출을 한다. 그러려면 메서드 signature 가 같아야 한다. = 인터페이스
《interface》 Iterator 로 호출 규칙을 정하고 데이터 조회 객체에서 구현한다.
hasNext(), next() 를 사용해 값을 꺼낸다.
↑ 규칙에 따라 호출하여 일관성 확보
App 이 ArrayList 에 ① iterator() 를 호출한다. ② return 으로 ListIterator (Iterator 구현체)를 반환한다. App 에서 ListIterator 를 call 하면 ListIterator 에서 ④ get() 사용해서 값을 요청하면 ArrayList는 ⑤ 값 리턴한다. ListIterator 는 App 에 ⑥ 값 리턴 한다.
App 의 main() 에서 BoardHandler 의 service() 사용한다. BoardHandler 에서 BoardDao 를 call 한다. BoardDao 에서 List 구현체를 call 한다. List 는 Iterable 을 상속받고 구현체로 ArrayList, LinkedList 가 있다. List 구현체는 Iterator 구현체를 사용한다. 구현체로 ListIterator 가 있다. 또한 BoardDao 에서 Iterator 구현체를 call 한다.
List 와 Iterator 인터페이스
BoardHandler 에서 BoardDao 를 사용해 ① findAll() 를 실행한다. findAll() 에서 ② toArray 에서 변경된 iterator() 를 실행한다. iterator() 는 List 구현체에 구현되어 있다. List 구현체인 LinkedList 에서 ③ return 으로 Iterator 구현체인 ListIterator 를 반환한다. BoardDao 에서 ListIterator 를 사용해 ④ hasNext(), next() 를 실행한다. Board[ ] 를 받아 BoardHandler 로 리턴한다.
《interface》List 에 iterator() 를 추가하지 않고 공통 규칙을 수퍼 interface 인 Iterable 에 정의한다. 《interface》List 와 《interface》Queue 가 《interface》Iterable 을 상속한다.
여러 인터페이스(예. List 와 Queue)에 공통으로 적용되는 규칙(메서드)이 있다면, 수퍼인터페이스를 만들어 공통규칙을 정의한 후, 상속 받는 것이 인터페이스 유지보수에 좋다.
예제 코드
com/eomcs/oop/ex09/a~d
인터페이스 사용 전
BlueWorker는 doFight() 실행하고, WhiteWorker는 doZingZing() 실행하고, JubuWorker는 doSsingSsing() 실행한다.
↑ 객체마다 사용법이 다르다. 프로그래밍에 일관성이 없다.
Worker 구현체 사용
package com.eomcs.oop.ex09.a1.before;
public class Exam01 {
public static void main(String[] args) {
BlueWorker w1 = new BlueWorker();
WhiteWorker w2 = new WhiteWorker();
JubuWorker w3 = new JubuWorker();
// 각 노동자에게 일을 시키는 방법이 다르다.
// 왜?
// => 메서드 호출 방법이 다르기 때문에
w1.doFight();
w2.doZingZing();
w3.doSsingSsing();
// 객체에 일을 시키는 방식은 엇비슷한데,
// 메서드 시그너처가 다르기 때문에
// 호출할 때 일관성이 없다.
// 즉 worker 객체를 사용하는 입장에서는
// 각 worker에 어떤 메서드가 있는지 확인해서
// 그 형식에 맞춰야 하기 때문에
// 객체를 사용하기가 불편한다.
// 또한,
// 앞으로 worker를 추가 제작할 경우
// 개발자의 입맛에 맞춰 메서드 이름과 파라미터 형식이 결정될 것이기 때문에
// 점점 더 worker 객체를 사용하기 힘들어진다.
//
// 결론!
// => 유사한 일을 하는데 객체 사용법이 다르다면 쓰기가 매우 불편하다.
//
// 해결책!
// => 유사한 일을 하는 객체에 대해 사용법을 통일하자.
//
// 방법!
// => 객체 사용 규칙을 정의한다.
// => 그리고 클래스를 정의할 때 그 규칙에 따라 만든다.
// => 그러면 규칙에 따라 만든 클래스를 사용할 때는
// 일관된 방법으로 메서드를 호출할 수 있어
// 코딩하기가 훨씬 편해지고,
// 유지보수가 쉬워진다.
//
// 이렇게 객체의 사용 규칙(호출 규칙)을 정의하는 문법이 "인터페이스(interface)"이다.
//
//
}
}
package com.eomcs.oop.ex09.a1.before;
public class BlueWorker {
public void doFight() {
System.out.println("육체 노동자가 일을 합니다!");
}
}
package com.eomcs.oop.ex09.a1.before;
public class WhiteWorker {
public void doZingZing() {
System.out.println("사무직 노동자가 일을 합니다.");
}
}
package com.eomcs.oop.ex09.a1.before;
public class JubuWorker {
public void doSsingSsing() {
System.out.println("주부로 일합니다.");
}
}
인터페이스 사용 후
《interface》Worker 에 execute() 규칙을 정의한다.
Client 에서 BlueWorker, WhiteWorker, JubuWorker의 execute() 를 실행한다.
호출 규칙 중요 → 프로그래밍이 쉽다.
Worker 구현체 사용
package com.eomcs.oop.ex09.a1.after;
// 작업:
// 1) worker 객체의 사용 규칙을 정의한다.
// => Worker 인터페이스 정의
// 2) 클래스를 정의할 때 Worker 규칙에 따라 만든다.
// => BlueWorker, WhiteWorker, JubuWorker 클래스 변경
// 3) worker를 사용하는 측에서는 Worker 인터페이스에 정의된 대로 메서드를 호출한다.
// => Exam01 클래스 변경
//
public class Exam01 {
public static void main(String[] args) {
// 같은 사용규칙에 따라 만든 클래스는
// 한 인터페이스 레퍼런스에 그 객체 주소를 저장할 수 있어 편리하다.
//
// 인터페이스 레퍼런스
// => 인터페이스를 구현한 클래스의 객체 주소를 저장하는 변수
Worker w1 = new BlueWorker();
Worker w2 = new WhiteWorker();
Worker w3 = new JubuWorker();
// Worker 인터페이스에 선언된 메서드를 구현한다고 해서 되는 게 아니다.
// 반드시 implements 키워드를 사용해서
// Worker 인터페이스를 구현한다고 선언해야 한다.
// 다음 HulWorker 클래스처럼 Worker 인터페이스 구현을 선언하지 않는다면,
// 공식적으로 Worker 구현체가 아니기 때문에
// 레퍼런스에 담을 수 없다.
// Worker w4 = new HulWorker(); // 컴파일 오류!
// => 인터페이스를 구현하지 않은 클래스의 인스턴스 주소는 저장할 수 없다.
// Worker w5 = new String();// 컴파일 오류!
// 사용하려는 객체가 같은 인터페이스를 구현(사용규칙에 따라 작성)하였다면,
// 다음과 같이 동일한 사용규칙에 따라 메서드를 호출할 수 있어 편리하다.
// 호출에 일관성이 있어 유지보수에 좋다.
w1.execute();
w2.execute();
w3.execute();
// 역할?
// => 사용 규칙: Worker
// => caller : Exam01
// => callee : BlueWorker, WhiteWorker, JubuWorker
// 참고!
Worker w;
// 인터페이스 레퍼런스?
// => 해당 인터페이스에 따라 작성된(사용규칙을 준수하는) 클래스의 인스턴스 주소를 저장한다.
//
// 위 예제에서 w 레퍼런스를말로 표현하는 방법:
// => Worker 사용규칙에 따라 작성된 클래스의 인스턴스 주소를 저장하는 변수 w.
// => Worker 인터페이스를 구현한 클래스의 인스턴스 주소를 저장하는 변수 w.
// => Worker 구현체의 인스턴스 주소를 저장하는 변수 w.
// => Worker 구현체의 객체 주소를 저장하는 변수 w.
// => Worker 구현 객체를 저장하는 변수 w.
// => Worker 객체를 저장하는 (변수) w.
// => Worker 객체를 가리키는 (변수) w.
// => Worker 타입 객체 w.
// 프로그램을 짜다가 인터페이스를 만나게 되면,
// - 누가 호출자이고 누가 피호출자인지 확인하라.
// - 본인이 맡은 개발 일이 호출자를 만드는 것인지
// 아니면 피호출자를 만드는 것인지 확인하라.
}
}
// 인터페이스 - caller와 callee 사이의 호출 규칙을 정의하는 문법
package com.eomcs.oop.ex09.a1.after;
// caller(호출자;사용자) : Exam01
// callee(피호출자;도구) : BlueWorker, JubuWorker, WhiteWorker
// 문법:
// interface 사용규칙명 {...}
//
public interface Worker {
// 호출 규칙?
// => 메서드 형식을 의미한다.
// => 메서드의 몸체는 정의하지 않는다.
// => 메서드의 몸체는 이 규칙에 따라 만드는 클래스에서 정의하는 것이다.
//
// 문법
// => public abstract 리턴타입 메서드명(파라미터,...);
// => public과 abstract는 모두 생략 가능.
// => public 외에 다른 접근 범위는 사용할 수 없다.
// (규칙은 공개되어야 하니까!)
//
void execute();
//=> public abstract void execute();
}
// 구현체(implementor) - 인터페이스(사용규칙)에 따라 만든 클래스
package com.eomcs.oop.ex09.a1.after;
// 문법
// => class 클래스명 implements 인터페이스명, 인터페이스명, ... {...}
public class BlueWorker implements Worker {
// 인터페이스(약속,규칙,규격,법률)를 이행하는 클래스는
// 인터페이스에 선언된 모든 메서드를 반드시 구현해야만 일반 클래스(concrete class)가 된다.
// 만약에 구현하지 않으면, 추상 메서드인채로 존재한다.
// 따라서 졸지에 추상 메서드를 가진 클래스가 된다.
// 추상 메서드는 추상 클래스만이 가질 수 있기 때문에
// 추상 클래스로 바꿔야 한다.
//
@Override
public void execute() {
System.out.println("육체 노동자가 일을 합니다!");
}
}
// Worker 인터페이스를 이행하는 클래스 - 줄여서 "Worker 구현체"라 부른다.
package com.eomcs.oop.ex09.a1.after;
public class WhiteWorker implements Worker {
// 인터페이스 구현체(인터페이스를 구현하는 클래스)는
// 반드시 인터페이스에 선언된 모든 메서드를 구현해야 한다.
@Override
public void execute() {
System.out.println("사무직 노동자가 일을 합니다.");
}
}
// Worker 구현체
package com.eomcs.oop.ex09.a1.after;
public class JubuWorker implements Worker {
// 인터페이스 구현체는 인터페이스에 선언된 모든 메서드를 구현해야 한다.
@Override
public void execute() {
System.out.println("주부로 일합니다.");
}
}
package com.eomcs.oop.ex09.a1.after;
// 클래스를 정의할 때 Worker 인터페이스를 구현하겠다고 선언하지는 않았지만,
// Worker 인터페이스에 존재하는 메서드를 모두 구현했을 때,
// 이 클래스는 Worker 규칙을 따른다고 할 수 있는가?
// 답: 아니오!
//
// 운전 면허가 없더라도 운전을 할 수 있다고 해서
// 도로로 차를 몰 수는 없다.
public class HulWorker {
public void execute() {
System.out.println("헐....^^");
}
}
인터페이스와 caller/callee
Caller 가 인터페이스 Worker를 사용한다. Callee 는 Worker 규칙에 맞게 작성해 BlueWorker 를 만든다.
인터페이스 사용 전
package com.eomcs.oop.ex09.a2.before;
public class Exam01 {
public static void main(String[] args) {
// 객체의 기능을 사용해보자!
// => 각 도구(객체)의 사용법이 다르기 때문에 각 도구에 맞춰서 기능을 사용해야 한다.
// => 그래서 각 도구에 맞는 use() 메서드를 각각 따로 준비했다.
// 1) ToolA 객체 사용하기
use(new ToolA());
// 2) ToolB 객체 사용하기
use(new ToolB());
}
static void use(ToolA tool) {
// ToolA 객체를 사용하려면 m1() 메서드를 호출해야 한다.
tool.m1();
}
static void use(ToolB tool) {
// ToolB 객체를 사용하려면 m2() 메서드를 호출해야 한다.
tool.m2();
}
}
package com.eomcs.oop.ex09.a2.before;
public class ToolA {
// 각각의 클래스는 자신만의 이름으로 메서드를 제공한다.
public void m1() {
System.out.println("ToolA.m1()");
}
}
package com.eomcs.oop.ex09.a2.before;
public class ToolB {
//각각의 클래스는 자신만의 이름으로 메서드를 제공한다.
public void m2() {
System.out.println("ToolB.m2()");
}
}
인터페이스 레퍼런스와 인스턴스의 관계
package com.eomcs.oop.ex09.a2.after;
public class Exam01 {
public static void main(String[] args) {
// 인터페이스 레퍼런스 선언
// => Spec 인터페이스를 구현한 클래스의 인스턴스 주소를 저장하겠다는 의미다.
// => Spec 규칙에 따라 작성한 클래스의 객체를 담겠다는 의미다.
// => Spec 객체를 담겠다는 의미다.
Spec tool;
// ToolA 클래스는 Spec 인터페이스의 규칙에 따라 만든 클래스이기 때문에
// 이 클래스의 인스턴스 주소를 tool 레퍼런스 변수에 저장할 수 있다.
tool = new ToolA();
// ToolB 클래스도 ToolA와 마찬가지로
// Spec 구현체(implementer; 인터페이스 규칙에 따라 만든 클래스)이기 때문에
// 해당 객체를 저장할 수 있다.
tool = new ToolB();
tool = new ToolC();
// String 클래스는 Spec 구현체가 아니기 때문에 해당 객체를 레퍼런스에 저장할 수 없다.
// tool = new String("Hello"); // 컴파일 오류!
}
}
// 인터페이스 - 호출 규칙(사용 규칙)을 정의하는 문법
package com.eomcs.oop.ex09.a2.after;
// 인터페이스
// => 사용자(클래스)와 도구(클래스) 사이의 호출 규칙을 정의할 때 사용한다.
// => 도구를 사용하기 위해 어떤 메서드를 호출해야 하는지 정의하는 것이다.
//
public interface Spec {
// 호출 규칙을 정의할 때,
// => 규칙은 공개되어야 하기 때문에 무조건 public 이다.
// => 규칙이기 때문에 당장 구현할 필요는 없다. 그래서 추상 메서드이다.
public abstract void m1();
// 물론 다음과 같이 public 과 abstract를 모두 생략할 수 있다.
// void m1();
}
// 인터페이스 구현 - 규칙에 따라 클래스를 만든다.
package com.eomcs.oop.ex09.a2.after;
// 규칙에 따라 클래스를 만들면 도구를 사용하는 입장에서
// 일관된 코딩을 할 수 있어 유지보수에 좋다.
public class ToolA implements Spec {
// 인터페이스의 규칙에 따라 클래스를 작성할 때는
// 인터페이스에 선언된 모든 추상 메서드를 반드시 구현해야 한다.
// 구현하지 않으면 추상 메서드를 그대로 갖고 있는 것이 된다.
// 추상 메서드를 갖고 있는 클래스는 추상 클래스만이 가능하다.
// => 인터페이스에 선언된 메서드가 public 이기 때문에 이 메서드를 구현하는 클래스에서는
// public 보다 낮은 접근을 지정할 수 없다.
// 즉 무조건 public 메서드이다.
//
@Override
public void m1() {
System.out.println("ToolA.m1()");
}
}
// 인터페이스 구현 - 규칙에 따라 클래스를 만든다.
package com.eomcs.oop.ex09.a2.after;
// 규칙에 따라 클래스를 만들면 도구를 사용하는 입장에서
// 일관된 코딩을 할 수 있어 유지보수에 좋다.
public class ToolB implements Spec {
// 인터페이스의 규칙에 따라 클래스를 작성할 때는
// 인터페이스에 선언된 모든 추상 메서드를 반드시 구현해야 한다.
// 구현하지 않으면 추상 메서드를 그대로 갖고 있는 것이 된다.
// 추상 메서드를 갖고 있는 클래스는 추상 클래스만이 가능하다.
// => 인터페이스에 정의된 규칙을 따르기로 했으면
// 원래 클래스에 있던 메서드의 이름을 포기하고 인터페이스에 있는 메서드 이름을 따르라!
// m2() ---> m1() 으로 변경한다.
@Override
public void m1() {
System.out.println("ToolB.m1()");
}
}
// 인터페이스 구현 - 규칙에 따라 클래스를 만든다.
package com.eomcs.oop.ex09.a2.after;
// 누가 도구를 만들든지 간에 항상 Spec 에 따라 만들기 때문에,
// - 이 도구를 사용하는 입장에서는 Spec에 맞춰 사용함으로써
// 프로그래밍에 일관성을 확보할 수 있다.
//
public class ToolC implements Spec {
@Override
public void m1() {
System.out.println("ToolC.m1()");
}
}
// 인터페이스 미구현
package com.eomcs.oop.ex09.a2.after;
// ToolD 클래스는 Spec 인터페이스에 선언된 메서드를 구현했지만,
// 문법적으로는 Spec 인터페이스와 상관이 없다.
//
public class ToolD {
public void m1() {
System.out.println("ToolC.m1()");
}
}
인터페이스 규칙에 따라 만든 클래스를 사용하기
package com.eomcs.oop.ex09.a2.after;
public class Exam02 {
public static void main(String[] args) {
// 도구 사용하기
// Spec 규칙(interface)에 따라 만든 도구(클래스)를 use() 메서드에 넘긴다.
use(new ToolA());
use(new ToolB());
// 새로 추가한 도구(클래스 ToolC)도 Spec 규칙에 따라 만들었기 때문에
// use() 메서드에 그대로 넘길 수 있다.
use(new ToolC());
// use(new ToolD()); // 컴파일 오류!
// 비록 ToolD 클래스에 Spec에 정의된 m1() 메서드가 있다 하더라도
// 문법적으로 ToolD 클래스는 Spec 인터페이스를 구현한 것이 아니기 때문에
// use 파라미터 값으로 넘길 수 없다.
}
static void use(Spec tool) {
// 파라미터 tool에 넘어오는 객체는 Spec 규칙에 따라 만든 객체일 것이다.
// Spec 규칙에 따라 만든 도구를 사용할 때는
// Spec 규칙에 따라 일관된 방식으로 사용(메서드를 호출)하면 된다.
// 그러면 해당 인스턴스의 클래스를 찾아 그 클래스에서 구현한 메서드를 호출할 것이다.
tool.m1();
}
}
인터페이스의 구현
《interface》MyInterface 에 m1(), m2(), m3() 규칙을 정한다.
《concrete》MyObject 에서 MyInterface 규칙에 따라 m1() {-}, m2() {-}, m3() {-} 를 구현한다.
《abstract》MyObject2 에서 MyInterface 규칙에 따라 m1() {-}, m2() {-} 를 구현했지만 m3() 를 구현하지 않아 abstract 이다.
MyInterface ref;
ref = new MyObject(); 하면 인터페이스를 구현한 클래스의 인스턴스 주소가 들어간다.
인터페이스 - 정의와 구현
package com.eomcs.oop.ex09.b;
// 1) 인터페이스 정의
interface MyInterface {
// 규칙1) 인터페이스에 선언되는 모든 메서드는 public 이다.
// - 인터페이스에 정의하는 메서드는 호출 규칙이다.
// - 규칙은 공개되어야 한다.
// 규칙2) 인터페이스에 선언되는 모든 메서드는 추상 메서드로 선언한다.
// - 인터페이스에 선언하는 메서드는 호출 규칙을 정의한 것이다.
// - 규칙은 클래스가 따라야 한다.
// - 그래서 인터페이스에 선언되는 모든 메서드는 몸체를 구현하지 않는다.
public abstract void m1();
// public 을 생략할 수 있다.
abstract void m2(); // public 이 생략된 것이다. (default) 아니다!
// abstract 를 생략할 수 있다.
public void m3();
// public, abstract 모두 생략할 수 있다.
void m4();
// => private, protected, (default)는 없다.
// private void m5(); // 컴파일 오류!
// protected void m6(); // 컴파일 오류!
void m7(); // 이건 (default) 아니라, public 이 생략된 것이다.
}
// 2) 인터페이스 구현
abstract class MyInterfaceImpl implements MyInterface {
@Override
public void m1() {}
// public 보다 접근 범위를 좁힐 수는 없다.
@Override
// private void m2() {} // 컴파일 오류!
// protected void m2() {} // 컴파일 오류!
// void m2() {} // 컴파일 오류!
public void m2() {} // OK!
// 인터페이스의 모든 메서드를 구현해야 한다.
// 한 개라도 빠뜨린다면 concrete 클래스가 될 수 없다.
// 추상 클래스로 선언해야 한다.
}
class MyInterfaceImpl2 implements MyInterface {
@Override
public void m1() {}
@Override
public void m2() {} // OK!
@Override
public void m3() {}
@Override
public void m4() {}
@Override
public void m7() {}
}
// 3) 인터페이스 사용
public class Exam01 {
public static void main(String[] args) {
// 인터페이스 레퍼런스 선언
MyInterface obj = null;
// 인터페이스의 구현체 생성
// - 인터페이스를 구현한 클래스의 객체라면
// 언제든 해당 인터페이스의 레퍼런스에 담을 수 있다.
obj = new MyInterfaceImpl2();
// 인터페이스는 규칙이기 때문에
// 구체적인 구현 내용이 없다.
// 그래서 인스턴스를 생성할 수 없다.
//
// obj = new MyInterface(); // 컴파일 오류!
}
}
인터페이스 필드
package com.eomcs.oop.ex09.b;
// 인터페이스 필드 선언
interface MyInterface2 {
// 인터페이스 필드는 public static final 이다.
// - 인스턴스를 생성할 수 없기 때문에 인스턴스 필드를 선언할 수 없다.
// - 규칙이기 때문에 무조건 public 이다.
// - 인스턴스 필드가 아니기 때문에 값을 변경할 수 없다.
public static final int v1 = 100;
// public, static, final 을 생략할 수 있다.
static final int v2 = 200;
public final int v3 = 300;
public static int v4 = 400;
int v5 = 500; // 모두 생략된 상태!
}
// 인터페이스 필드 사용
public class Exam02 {
public static void main(String[] args) {
// 인터페이스에 선언한 필드는 public static final 이기 때문에 바로 사용할 수 있다.
System.out.println(MyInterface2.v1);
System.out.println(MyInterface2.v2);
System.out.println(MyInterface2.v3);
System.out.println(MyInterface2.v4);
System.out.println(MyInterface2.v5);
// 인터페이스 필드는 상수 필드이기 때문에 값을 변경할 수 없다.
// MyInterface2.v1 = 111; // 컴파일 오류!
// MyInterface2.v2 = 222; // 컴파일 오류!
// MyInterface2.v3 = 333; // 컴파일 오류!
// MyInterface2.v4 = 444; // 컴파일 오류!
// MyInterface2.v5 = 555; // 컴파일 오류!
}
}
default 메서드 사용 전
interface Car 에 start(), stop(), run() 메서드가 있고 구현체 Tico, Sonata 를 작성했다.
interface Car에 자율주행 규칙이 필요하여 autoPilot(); 규칙을 추가한다. 그러면 기존의 구현체는 오류 발생한다. Car 인터페이스에 추가된 새 규칙을 구현(정의)하지 않았기 때문이다.
default 메서드 사용 후
interface Car에 default autoPilot() { } 규칙 추가한다. Car에 추가된 새 규칙은 구현된 메서드이기 때문에 기존 클래스에는 아무런 영향을 끼치지 않는다.
인터페이스 - 기본 메서드(default method)
package com.eomcs.oop.ex09.b;
interface MyInterface3 {
/*public static final*/ int a = 100;
/*public abstract*/ void m1();
// default method:
// - 기존 프로젝트에 영향을 끼치지 않으면서 기존 규칙에
// 새 메서드를 추가할 때 유용한다.
// - 인터페이스에서 미리 구현한 메서드이기 때문에
// 클래스에서 구현을 생략할 수 있다.
// - 반대로 구현을 강제할 수 없다는 것이 단점이다.
default void m2() {
System.out.println("default 출력!");
}
// 어차피 새 메서드는 새 프로젝트의 구현체가 오버라이딩 할 것이니
// 여기에서는 자세하게 정의하지 않는다.
// 다만 이 인터페이스를 구현한 예전 프로젝트에 영향을 끼치지 않으면서
// 새 메서드를 정의할 때 사용하는 문법이다.
default void m3() {
System.out.println("MyInterface3.m3()");
};
}
// 2) 인터페이스 구현
class MyInterface3Impl implements MyInterface3 {
// 추상 메서드는 반드시 구현해야 한다.
@Override
public void m1() {
System.out.println("MyInterfaceImpl.m1()");
}
// default 메서드는 오버라이딩 해도 되고 안해도 된다.
@Override
public void m2() {
System.out.println("MyInterfaceImpl.m2()");
}
// default 메서드는 오버라이딩 해도 되고 안해도 된다.
// => m3() 는 이 클래스에서 오버라이딩을 하지 않았다.
}
public class Exam03 {
public static void main(String[] args) {
MyInterface3 obj = new MyInterface3Impl();
obj.m1();
obj.m2();
obj.m3();
}
}
인터페이스 - private 메서드
package com.eomcs.oop.ex09.b;
interface MyInterface4 {
void m1();
default void m2() {
System.out.println("MyInterface4.m2()");
x();
};
default void m3() {
System.out.println("MyInterface4.m3()");
x();
};
// 인터페이스 내부에서 사용할 메서드라면
// private 접근 범위를 갖는
// 구현 메서드를 정의할 수 있다.
// 언제?
// - 해당 기능을 m2()와 m3() 처럼 여러 메서드에서 사용해야 할 경우
// 그 공통 코드를 다음과 같이 private 구현 메서드로 정의하면 될 것이다.
//
private void x() {
System.out.println("MyInterface4.x()");
}
}
// 2) 인터페이스 구현
class MyInterface4Impl implements MyInterface4 {
@Override
public void m1() {
// m2(); // 현재 클래스의 m2() 메서드
// 인터페이스에 선언된 오버라이딩 전의 default 메서드를 호출하고 싶다면,
// super.m2(); // 수퍼 클래스(Object)에서 m2()를 찾아 올라간다. Object에는 m2()가 없기 때문에 컴파일 오류!
// MyInterface4.m2(); // m2()는 인스턴스(non-static) 메서드이기 때문에 인터페이스 이름으로 직접 호출 불가!
MyInterface4.super.m2();
System.out.println("MyInterface4Impl.m1()");
}
@Override
public void m2() {
System.out.println("MyInterface4Impl.m2()");
}
}
public class Exam04 {
public static void main(String[] args) {
MyInterface4 obj = new MyInterface4Impl();
obj.m1();
System.out.println("-----------------------------");
obj.m2();
System.out.println("-----------------------------");
obj.m3();
System.out.println("-----------------------------");
// 인터페이스에 정의된 private 메서드는 호출할 수 없다.
// obj.x(); // 컴파일 오류!
}
}
static 메서드
MyInterface5Impl 클래스에서 Parent 를 상속하고 MyInterface5 를 구현한다.
인터페이스 - static 메서드
package com.eomcs.oop.ex09.b;
interface MyInterface5 {
// 인터페이스도 클래스처럼 static 메서드를 정의할 수 있다.
// => 접근 범위는 기본이 public 이다. 다른 접근 범위를 가질 수 없다.
// => public 을 생략할 수 있다.
//
// 다음 메서드는 public 이 생략된거지, (package-private) 접근 범위가 아니다.
static void m1() {
System.out.println("MyInterface5.m1()");
}
}
class Parent {
static void m2() {
System.out.println("Parent.m2()");
}
}
class MyInterface5Impl extends Parent implements MyInterface5 {
}
public class Exam05 {
public static void main(String[] args) {
// 인터페이스의 스태틱 메서드 호출하기
MyInterface5.m1();
// 클래스의 스태틱 메서드 호출하기
Parent.m2();
MyInterface5Impl obj = new MyInterface5Impl();
// 수퍼 클래스에 정의된 static 메서드는,
// => 다음과 같이 서브 클래스를 통해 호출할 수 있다.
MyInterface5Impl.m2();
// => 또는 다음과 같이 인스턴스 레퍼런스를 통해 호출할 수 있다.
// => 물론 스태틱 메서드는 가능한 그 메서드가 선언된 클래스 이름으로 호출하는 것이 좋다.
obj.m2();
// 그러나, 인터페이스에 정의된 static 메서드는,
// => 인터페이스를 구현한 클래스를 통해 호출할 수 없다.
// MyInterface5Impl.m1(); // 컴파일 오류!
// => 또한 구현체의 레퍼런스를 통해서도 호출할 수 없다.
// obj.m1(); // 컴파일 오류!
}
}
인터페이스 상속과 구현
레퍼런스를 통한 메서드 호출 범위
ProtocolA 를 ProtocolB 가 상속하고 ProtocolB 를 ProtocolImpl 가 구현한다.
ProtocolImpl obj = new ProtocolImpl(); 한다.
obj.m1(), obj.rule1(), obj.rule2() 가능하다.
ProtocolB obj2 = obj; 한다.
obj2.m1(), obj2.rule2(), obj2.rule1() 가능하다. obj 에 담긴게 ProtocolImpl 인스턴스라 하더라도 레퍼런스가 ProtocolB 주소이기 때문이다.
ProtocolA obj3 = obj; 한다.
obj3.m1(), obj3.rule2(), obj3.rule1() 가능하다. 레퍼런스가 ProtocolA 주소이기 때문이다.
인터페이스 상속과 구현
package com.eomcs.oop.ex09.c;
public class Exam0110 {
interface ProtocolA {
void rule1();
}
// 인터페이스도 다른 인터페이스를 상속 받을 수 있다.
// - ProtocolB 는 ProtocolA의 입장에서 서브 인터페이스이다.
// - ProtocolA 는 ProtocolB의 입장에서 수퍼 인터페이스이다.
interface ProtocolB extends ProtocolA {
void rule2();
}
// 인터페이스를 구현할 때는
// 수퍼 인터페이스의 메서드까지 모두 구현해야 한다.
class ProtocolImpl implements ProtocolB {
// ProtocolA 규칙 준수!
@Override
public void rule1() {System.out.println("rule1()");}
// ProtocolB 규칙 준수!
@Override
public void rule2() {System.out.println("rule2()");}
// 인터페이스와 상관 없이 메서드 추가
public void m1() {System.out.println("m1()");}
}
void test() {
ProtocolImpl obj = new ProtocolImpl();
// 1) 클래스의 레퍼런스 사용,
// - 그 클래스에 정의된 메서드 호출 가능
// - 그 클래스의 상위 클래스에 정의된 메서드 호출 가능
obj.rule1(); // OK
obj.rule2(); // OK
obj.m1(); // OK
System.out.println("------------------------------------");
// 2) 인터페이스의 레퍼런스 사용
// - 인터페이스에 정의된 메서드 호출 가능
// - 상위 인터페이스에 정의된 메서드 호출 가능
ProtocolB b = obj;
b.rule2(); // OK --> ProtocolB.rule2()
b.rule1(); // OK --> ProtocolA.rule1()
// b.m1(); // 컴파일 오류 --> ProtocolImpl.m1()
System.out.println("-------------------------------");
// 3) 수퍼 인터페이스의 레퍼런스 사용
// - 인터페이스에 정의된 메서드 호출 가능
// - 상위 인터페이스에 정의된 메서드 호출 가능
// - ProtocolImpl 클래스가 ProtocolB 의 규칙에 따라 제작되었다면
// 결국 그 수퍼 인터페이스의 규칙도 준수하는 것이 된다.
// - 따라서 수퍼 인터페이스 레퍼런스에 객체를 담을 수 있다.
// 즉 수퍼 인터페이스 레퍼런스로 객체를 가리킬 수 있다.
//
ProtocolA a = obj;
a.rule1(); // OK --> ProtocolA.rule1()
// a.rule2(); // 컴파일 오류 --> ProtocolB.rule2()
// a.m1(); // 컴파일 오류 --> ProtocolImpl.m1()
}
public static void main(String[] args) {
new Exam0110().test();
}
}
인터페이스 다중 상속
인터페이스 ProtocolC 가 ProtocolA, ProtocolB 를 둘 다 상속한다. 이를 구현한 ProtocolImpl 에서 rule3() {-}, rule1() {-}, rule2() {-}, m1() {-} 를 작성한다.
인터페이스의 메서드는 추상메서드이기 때문에 다중상속을 해도 문제가 될게 없다 → 다중 상속 가능!
인터페이스 다중 상속과 구현
package com.eomcs.oop.ex09.c;
public class Exam0120 {
interface ProtocolA {
void rule1();
}
interface ProtocolB {
void rule2();
}
interface ProtocolC extends ProtocolA, ProtocolB {
void rule3();
}
// 인터페이스를 구현할 때는
// 수퍼 인터페이스의 메서드까지 모두 구현해야 한다.
class ProtocolImpl implements ProtocolC {
// ProtocolA 규칙 준수!
@Override
public void rule1() {System.out.println("rule1()");}
// ProtocolB 규칙 준수!
@Override
public void rule2() {System.out.println("rule2()");}
// ProtocolC 규칙 준수!
@Override
public void rule3() {System.out.println("rule3()");}
public void m1() {System.out.println("m1()");}
}
void test() {
ProtocolImpl obj = new ProtocolImpl();
obj.rule1(); // OK
obj.rule2(); // OK
obj.rule3(); // OK
obj.m1(); // OK
System.out.println("-------------------------------");
// 1) 인터페이스 레퍼런스로 구현체의 주소 받기
ProtocolC c = obj;
ProtocolB b = obj;
ProtocolA a = obj;
// 2) 메서드 호출
// - 해당 인터페이스의 규칙에 따라서만 호출할 수 있다.
c.rule1(); // OK
c.rule2(); // OK
c.rule3(); // OK
// c.m1(); // 컴파일 오류!
System.out.println("-------------------------------");
// b.rule1(); // 컴파일 오류!
b.rule2(); // OK
// b.rule3(); // 컴파일 오류!
System.out.println("-------------------------------");
a.rule1(); // OK
// a.rule2(); // 컴파일 오류!
// a.rule3(); // 컴파일 오류!
System.out.println("-------------------------------");
}
public static void main(String[] args) {
new Exam0120().test();
}
}
조언
*generalization으로 추상클래스 뽑아내는 기술을 나에게 배워야 한다. 문법은 책이나 예제로 충분히 배울 수 있다. 이 강의를 듣는 목적이 이것이다.
*프로그램을 잘 다루려면 추상화, 상징화, 가정을 잘 해야 한다.
과제
/