인터페이스는 이를 구현하는 구현체 클래스가 어떤 역할을 하는지, 어떤 기능들을 구현해야 하는지 지정해 준다.
인터페이스는 게임 속 플레이어와 같이 능력에 관한 것들이다. 인터페이스를 구현하는 클래스는 반드시 인터페이스 안에 정의되어 있는 메소드를 구현해야 한다. 마치 우리가 게임 속 캐릭터를 방향 키를 누르면 캐릭터가 움직이는 것처럼 인터페이스에 정의되어 있는 메소드는 이를 구현하는 클래스에서 반드시 구현되어야 한다. 그래서 인터페이스는 클래스가 구현할 방법을 지정하는 역할을 한다.
인터페이스를 구현할 때 모든 메소드를 구현하지 않으면 그 클래스는 추상 클래스로 만들어야 한다.
추상 클래스에선 abstract
키워드를 메소드에 붙힐 수 있어 상속받는 클래스에서 해당 부분만 구현하지만, 인터페이스에 정의된 메소드들은 구현체 클래스에서 필수로 구현하기 때문이다.
abstract class AbsTest {
public void method1() {
System.out.println("implementation");
}
public abstract void method2(); // 상속받는 클래스에선 method2만 구현 해주면 된다.
}
class Test extends AbsTest{
@Override
public void method2() {
System.out.println("method2 implementation");
}
}
interface In1 {
void method1();
void method2();
}
class Test implements In1{ // abstract 클래스와는 달리 인터페이스에 정의된 메소드들은 구현체 클래스에서 다 구현해야한다.
@Override
public void method1() {
System.out.println("method1() implementation");
}
@Override
public void method2() {
System.out.println("method2() implementation");
}
}
interface interface_name {
// 멤버변수는 public static final 이여야 하며, 이를 생략할 수 있다.
// 메소드는 public abstract로 선언되어야 하며, 이를 생략할 수 있다.
// 기본적으로
}
interface Vehicle {
void changeGear(int a);
void speedUp(int a);
void applyBrakes(int a);
}
class Bicycle implements Vehicle {
int gear;
int speed;
@Override
public void changeGear(int newGear) {
gear = newGear;
}
@Override
public void speedUp(int increment) {
speed += increment;
}
@Override
public void applyBrakes(int decrement) {
speed -= decrement;
}
public void printStates() {
System.out.println("speed : " + speed + " gear : " + gear);
}
}
class Bike implements Vehicle {
int speed;
int gear;
@Override
public void changeGear(int newGear) {
gear = newGear;
}
@Override
public void speedUp(int increment) {
speed += increment;
}
@Override
public void applyBrakes(int decrement) {
speed -= decrement;
}
public void printStates() {
System.out.println("speed : " + speed + " gear : " + gear);
}
}
public class GFG {
public static void main(String[] args) {
Bicycle bicycle = new Bicycle();
bicycle.changeGear(2);
bicycle.speedUp(3);
bicycle.applyBrakes(1);
System.out.println("Bicycle present state :");
bicycle.printStates();
Bike bike = new Bike();
bike.changeGear(1);
bike.speedUp(4);
bike.applyBrakes(3);
System.out.println("Bike present state :");
bike.printStates();
}
}
interface Man {
void who();
}
class Father implements Man{
@Override
public void who() {
System.out.println("나는 아빠");
}
}
class Employee implements Man{
@Override
public void who() {
System.out.println("나는 회사원");
}
}
class PolymorphismTest {
public static void main(String[] args) {
Man father = new Father();
father.who();
Man employee = new Employee();
employee.who();
}
}
output
나는 아빠
나는 회사원
인터페이스를 구현하는 클래스들 중에서 어떤 클래스의 인스턴스가 who() 메서드를 실행시키는지에 따라서 실행되는 메서드가 달라진다.
Father
클래스의 인스턴스로 attack()
을 호출한다면, Father의 who()
이 실행Employee클래스
의 인스턴스로 who
()을 호출한다면 Employee의 who()
이 실행GameCharacter
인터페이스는 attack()
, move()
메소드만 정의되어 있다. 이를 구현하는 Wizard
, Warrior
클래스에서 각자 특성에 맞게 구현된 상태이다.Wizard
, Warrior
객체를 생성한 후 attack()
, move()
각각 호출할 것이다. 물론 이렇게 해도 문제가 되진 않는다. 하지만 코드의 중복이 존재한다. 우리가 예제로 공부할 때는 코드 라인 수가 적기 때문에 실질적으로 와닿진 않지만 만약 복잡한 프로그램을 고치고 있다고 가정하면서 공부를 한다면 조금 더 이해가 잘 될 것이다.huntingMonster
라는 메소드를 만들고 그 안에 attack()
, move()
메소드를 호출시켜놓은 상태에서 타입만 GameCharacter
인터페이스 타입이 오게끔 정의해두면 이 인터페이스를 구현하고 있는 하위 구현체 클래스 타입에 구현되어 있는 attack()
, move()
가 실행이 된다.Wizard
, Warrior
객체를 생성하고 메소드를 호출하지 않아도 공통적인 기능들이지만 서로 다른 결과를 내야 할 때는 인터페이스 타입으로 다형성을 구현하는 방법이 좋은 방법이다.interface GameCharacter {
void attack();
void move();
}
class Wizard implements GameCharacter {
@Override
public void attack() {
System.out.println("마법봉으로 공격");
}
@Override
public void move() {
System.out.println("텔레포트를 이용하여 이동");
}
}
class Warrior implements GameCharacter {
@Override
public void attack() {
System.out.println("검을 이용한 공격");
}
@Override
public void move() {
System.out.println("점프를 이용하여 이동");
}
}
public class Game {
public static void main(String[] args) {
Game game = new Game();
game.huntingMonster(new Wizard());
}
public void huntingMonster(GameCharacter gameCharacter) {
gameCharacter.move();
gameCharacter.attack();
}
}
Comparator Interface
는 우선 사용자가 정의한 클래스에 대해 정렬하는 메소드들을 제공하는 인터페이스이다.Comparator
을 통해 서로 다른 두 클래스의 객체를 비교할 수 있다.Compare
메소드는 Comparator
인터페이스를 구현해야만 사용할 수 있는 메소드이다.
public int compare(T o1, T o2)
만약 우리가 지정한 타입의 클래스가 있다고 가정해보자,
import java.util.*;
class Wizard {
int id;
String nickname;
String skill;
public Wizard(int id, String nickname, String skill) {
this.id = id;
this.nickname = nickname;
this.skill = skill;
}
@Override
public String toString() {
return "Wizard{" +
"id=" + id +
", nickname='" + nickname + '\'' +
", skill='" + skill + '\'' +
'}';
}
}
class SortById implements Comparator<Wizard> {
@Override
public int compare(Wizard o1, Wizard o2) {
return o1.id - o2.id;
}
}
class SortByName implements Comparator<Wizard> {
@Override
public int compare(Wizard o1, Wizard o2) {
return o1.nickname.compareTo(o2.nickname);
}
}
public class Game {
public static void main(String[] args) {
ArrayList<Wizard> list = new ArrayList<Wizard>();
list.add(new Wizard(333, "tedd", "fire"));
list.add(new Wizard(222, "elsa", "frozen"));
list.add(new Wizard(111, "anna", "water"));
System.out.println("Unsorted");
for (int i=0; i<list.size(); i++)
System.out.println(list.get(i));
Collections.sort(list, new SortById());
System.out.println("\nSorted by rollno");
for (int i=0; i<list.size(); i++)
System.out.println(list.get(i));
Collections.sort(list, new SortByName());
System.out.println("\nSorted by name");
for (int i=0; i<list.size(); i++)
System.out.println(list.get(i));
}
}
여기서 주목할 점은 SortById
, SortByName
클래스에 Comparator
인터페이스를 구현하고 있다는 점과 Collection.sort
를 이용하여 list에 있는 데이터를 정리하는 과정이다.
Collection.sort
의 내용을 분석해보면
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
첫 번째 매개변수로 List
타입이 들어오고, 두 번째 매개변수로는 Comparator
타입이 들어올 수 있다. 다시 Comparator
을 분석해보면 위에서 언급했지만 인터페이스로 선언되어 있다. 그 말은 우리가 이 Comprarator
인터페이스를 구현하는 구현체 클래스를 만들어주면 이 sort
메소드를 원하는 대로 사용할 수 있다는 뜻이다.
class SortById implements Comparator<Wizard> { // Comparator 인터페이스를 구현하여 compare 메소드를 재구현하는 과정이다.
@Override
public int compare(Wizard o1, Wizard o2) {
return o1.id - o2.id;
}
}
class SortByName implements Comparator<Wizard> {
@Override
public int compare(Wizard o1, Wizard o2) {
return o1.nickname.compareTo(o2.nickname);
}
}
자바의 다형성으로 인해서 Collection.sort()
에 두 번째 매개변수로 우리가 지정한 SortById
, SortByName
클래스 타입이 올 수 있는 것이다.
class A {
}
class B extends A {
// 단일 상속 가능
}
class C extends B, A {
// 클래스와 클래스사이에선 다중상속 불가능
}
interface A {
void methodA();
}
interface B {
void methodB();
}
interface C extends B, A {
void methodC();
}
public class MultipleInheritance implements C{ // A,B,C에 정의되어 있는 메소드들을 전부다 구현해야한다!
@Override
public void methodA() {
System.out.println("A 인터페이스의 메소드입니다.");
}
@Override
public void methodB() {
System.out.println("B 인터페이스의 메소드입니다.");
}
@Override
public void methodC() {
System.out.println("C 인터페이스의 메소드입니다.");
}
}
자바 8버전 이전에는 인터페이스에서 메소드의 정의만 할 수 있었고, 이에 따른 구현은 할 수 없었다. 하지만 default
메소드를 이용하면 인터페이스에서 메소드를 구현할 수 있다. default
메소드는 특별한 경우에 사용되는데, 그렇다고 이러한 기능이 인터페이스를 구현한 클래스들에게 악영향을 미치진 않는다.
이미 잘 사용하고 있는 인터페이스에 새로운 기능을 추가한다고 생각해보자.. 이미 인터페이스를 구현하고 있는 클래스에서 새로운 메소드를 정의해야 한다는 내용에 에러를 뱉어낼 것이다.. 근데 구현체 클래스가 1 ~ 2개면 귀찮아도 별문제 없이 해결이 가능하지만.. 10개 아니 100개 된다면..?? 호..쒯…만약 해결하려면 최소한 10개 혹은 100개의 클래스 파일에 가서 재정의를 해야할 것이다.. 이를 방지하기 위해서 default
메소드가 나온 것이다.
default
메소드는 인터페이스 안에서 메소드를 정의하고 구현할 수 있으며 구현체 클래스에선 동일하게 메소드를 호출하면 아무런 문제 없이 사용할 수 있다.
package com.lee.company;
// 위 다이어그램처럼 method4()를 추가한다고 가정해보자
// In1를 구현하고 있는 TestClass1, TestClass2, TestClass3 전부다 method4를 정의하여 사용해야한다.
// 현재 예시는 단순히 출력기능만 가지고 있지만 만약 100줄 혹은 1000줄 짜리 코드를 추가해야한다고 가정한다면?? 매우매우 끔찍할 것 같다..
interface In1 {
void method1();
void method2();
void method3();
void method4(); // 위 다이어그램처럼 메소드를 추가한다고 가정해보자 이를 구현하는 클래스에선 method4()를 구현해야하는 불상사가 발생한다.
}
class TestClass1 implements In1{
@Override
public void method1() {
System.out.println("In1.method1()");
}
@Override
public void method2() {
System.out.println("In1.method2()");
}
@Override
public void method3() {
System.out.println("In1.method3()");
}
}
class TestClass2 implements In1{
@Override
public void method1() {
System.out.println("In1.method1()");
}
@Override
public void method2() {
System.out.println("In1.method2()");
}
@Override
public void method3() {
System.out.println("In1.method3()");
}
}
class TestClass3 implements In1{
@Override
public void method1() {
System.out.println("In1.method1()");
}
@Override
public void method2() {
System.out.println("In1.method2()");
}
@Override
public void method3() {
System.out.println("In1.method3()");
}
}
class DefaultTest {
public static void main(String[] args) {
TestClass1 testClass1 = new TestClass1();
testClass1.method1();
testClass1.method2();
testClass1.method3();
TestClass2 testClass2 = new TestClass2();
testClass2.method1();
testClass2.method2();
testClass2.method3();
TestClass3 testClass3 = new TestClass3();
testClass3.method1();
testClass3.method2();
testClass3.method3();
}
}
In1
인터페이스에 method4
를 추가할 경우이다.package com.lee.company;
// default 메소드를 사용함으로써, In1를 구현하고 있는 구현체 클래스에 재정의할 필요없다.
// 인터페이스에서 바로 정의한 후 구현체 클래스에서 바로 사용 가능하다.
interface In1 {
void method1();
void method2();
void method3();
default void method4() {
System.out.println("In1.method4()");
}
}
class TestClass1 implements In1{
@Override
public void method1() {
System.out.println("In1.method1()");
}
@Override
public void method2() {
System.out.println("In1.method2()");
}
@Override
public void method3() {
System.out.println("In1.method3()");
}
}
class TestClass2 implements In1{
@Override
public void method1() {
System.out.println("In1.method1()");
}
@Override
public void method2() {
System.out.println("In1.method2()");
}
@Override
public void method3() {
System.out.println("In1.method3()");
}
}
class TestClass3 implements In1{
@Override
public void method1() {
System.out.println("In1.method1()");
}
@Override
public void method2() {
System.out.println("In1.method2()");
}
@Override
public void method3() {
System.out.println("In1.method3()");
}
}
class DefaultTest {
public static void main(String[] args) {
TestClass1 testClass1 = new TestClass1();
testClass1.method1();
testClass1.method2();
testClass1.method3();
testClass1.method4(); // 구현체 클래스타입의 변수로 접근하여 메소드를 호출할 수 있다.
TestClass2 testClass2 = new TestClass2();
testClass2.method1();
testClass2.method2();
testClass2.method3();
testClass2.method4();
TestClass3 testClass3 = new TestClass3();
testClass3.method1();
testClass3.method2();
testClass3.method3();
testClass3.method4();
}
}
default
메소드와 같이 자바 8버전 이후에 나왔고, static
클래스 메소드에 접근할 때 객체 생성 없이 클래스 이름으로 메소드를 호출하는 것처럼 static
메소드를 가진 인터페이스를 구현하는 구현체 클래스에서 인터페이스명.메소드명
으로 접근하는 메소드이다default
메소드와 동일하게 메소드의 구현체를 가지고 있고, 다른 점은 상속이 불가능하다는 점이다.interface In1 {
static void test() {
System.out.println("static method");
}
}
public class TestClass implements In1{
public static void main(String[] args) {
In1.test(); // 인터페이스명.메소드명으로 접근한다.
}
}
public interface FuntionalTest { // 구현해야할 메소드가 하나만 있기 때문에 Functional Interface에 속한다.
public void test();
}
public interface FunctionalTest2 { // 구현해야할 메소드가 하나 이상이기 때문에 Functional Interface에 속하지 않는다.
public void test2();
public void test3();
}
public interface FunctionalTest3 { // Object 객체의 메소드만 인터페이스에 선언된 경우 Functional Interface에 속하지 않는다.
public boolean equals(Object obj);
}
public interface FunctionalTest4 { // Object 객체를 제외하고 하나의 추상 메소드가 존재하므로 Functional Interface에 속한다.
public boolean equals(Object obj);
public void execute();
}
즉, FunctionalTest2, FunctionalTest3은 Functional Interface가 될 수 없고 나머지 두 개의 인터페이스는 Functional Interface가 될 수 있다.
abstract
로 선언할 수 없다.private static
으로 시작하는 메소드는 다른 메소드가 non-static
이든, static
이든 접근 가능하다.private
만 적용된 메소드는 static
메소드 안에선 사용할 수 없다.public interface CustomInterface {
public abstract void method1();
public default void method2() {
method4(); //private method inside default method
method5(); //static method inside other non-static method
System.out.println("default method");
}
public static void method3() {
method5(); //static method inside other static method
System.out.println("static method");
}
private void method4(){
System.out.println("private method");
}
private static void method5(){
System.out.println("private static method");
}
}
public class CustomClass implements CustomInterface {
@Override
public void method1() {
System.out.println("abstract method");
}
public static void main(String[] args){
CustomInterface instance = new CustomClass();
instance.method1();
instance.method2();
CustomInterface.method3();
}
}
Output:
abstract method
private method
private static method
default method
private static method
static method
그렇다면 인터페이스에 이러한 기능들을 제공하는 이유가 뭘까? 위에서도 언급했지만 수정하기 쉽고, 재사용을 위한 기능들이라는 건 이해했다. 하지만 왜? 수정하기 쉽고, 재사용하기 쉽게 만들었냐는 것이다. 이번에도 역시 나의 개인적인 생각이지만, 코드의 중복을 제거하기 위함이라고 생각한다.
조금 더 찾아보니 템플릿 메소드 패턴(template method pattern)라는 것이 있었다. 소프트웨어 공학에서 동작 상의 알고리즘의 프로그램 뼈대를 정의하는 행위 디자인 패턴이다. 알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해준다.
템플릿(template)은 하나의 '틀'을 의미한다. 하나의 틀에서 만들어진 것들은 형태가 다 같다. 이런 틀 기능을 구현할 때는 template method 패턴을 이용할 수 있다. 이는 상속의 개념이 있는 상위 클래스와 하위 클래스의 구조에서 표현할 수 있다. 일반적으로 상위 클래스(현재 공부하고 있는 인터페이스)에는 추상 메서드를 통해 기능의 골격을 제공하고, 하위 클래스(구체 클래스)의 메서드에서는 세부 처리를 구체화한다.
이처럼 상위 클래스에서는 추상적으로 표현하고 그 구체적인 내용은 하위 클래스에서 결정되는 디자인 패턴을 template method 패턴이라고 한다. 상속의 개념처럼 template method 패턴도 코드 양을 줄이고 유지보수를 용이하게 만드는 역할을 한다. 따라서 유사한 서브 클래스가 존재할 때 template method 패턴을 사용하면 매우 유용하다.
예를 한번 들어보자. 음료를 만들기 위한 클래스가 하나 있으면 음료수를 만들기 위해서는 1)컵을 준비한다. 2)물을 붓는다 3)첨가물을 넣는다. 4)음료를 내어드린다. 이렇게 4가지의 음료 만드는 과정이 있다. 1,2,4번 과정은 모든 음료를 만드는 데 공통적인 과정이라면 3번은 어떤 음료인가에 따라 첨가물이 달라질 것이다. 예를 들면 커피면 커피 가루, 홍차면 홍차 가루를 넣을 것이다.
이렇게 변경되는 로직부분을 추상 메소드로 만들어 놓고 상위 클래스에서는 알고리즘의 전체적인 틀을 만들고 하위 클래스에서는 구체적인 알고리즘의 일부를 구현하는 것이다.
ps) static, default, private 메소드를 조사하다보니 템플릿 메소드 패턴이라는 것을 알게 되었고, 아직까진 완벽하게 나의 지식으로 습득하지 못했기에 정리가 잘 된 글을 퍼왔다.
/**
* 공통 기능을 구현하고 세부 기능은 추상화한 추상클래스(음료제작)
* @author yun-yeoseong
*
*/
public abstract class TemplateMethodPattern {
public final void makeBeverage() {
prepareCup();
prepareWater();
additive();
finish();
}
/**
* 공통 메소드
*/
private void prepareCup() {
System.out.println("컵을 준비한다.");
}
/**
* 공통 메소드
*/
private void prepareWater() {
System.out.println("물을 붓는다.");
}
/**
* 실제 구현이 필요한 부분
*/
abstract void additive();
/**
* Hook 메소드, 서브클래스에서 구현이 필요하다면 오버라이드 해도된다.
* 하지만 꼭 오버라이드가 강제는 아니다.
*/
private void hookMethod() {
System.out.println("hook method");
}
/**
* 공통 메소드
*/
private void finish() {
System.out.println("음료를 내어드린다.");
}
}
/**
* 템플릿 추상 클래스를 상속하는 서브 클래스
* 세부내용을 구현한다.
* @author yun-yeoseong
*
*/
public class SubClassA extends TemplateMethodPattern{
@Override
void additive() {
System.out.println("커피가루를 넣는다.");
}
}
/**
* 템플릿 추상 클래스를 상속하는 서브 클래스
* 세부내용을 구현한다.
* @author yun-yeoseong
*
*/
public class SubClassB extends TemplateMethodPattern{
@Override
void additive() {
System.out.println("홍차가루를 넣는다.");
}
}
람다식은 매개변수 -> 함수몸체의 형태로 이용할 수 있다.
매개변수가 하나일 경우 매개변수를 생략할 수 있다.
함수몸체가 단일 실행문이면 괄호{}를 생략 할 수 있다.
함수몸체가 return
문으로만 구성되어 있는 경우 괄호{}를 생략 할 수 없다.
1. (매개변수) -> {함수몸체}
2. () -> {함수몸체}
3. (매개변수) -> 함수몸체
4. (매개변수) -> {return 0;}
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
public class TestClass {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("no Lamda");
}
}).start();
}
}
public class TestClass {
public static void main(String[] args) {
new Thread(()-> {
System.out.println("Lamda");
}).start();
}
}