[Live Study 8주차] 인터페이스

이호석·2022년 8월 10일
0

목표

  • 자바의 인터페이스에 대해 학습하세요.

학습할 것

  • 인터페이스 정의하는 방법
  • 인터페이스 구현하는 방법
  • 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
  • 인터페이스 상속
  • 인터페이스의 기본 메소드 (Default Method), 자바 8
  • 인터페이스의 static 메소드, 자바 8
  • 인터페이스의 private 메소드, 자바 9



인터페이스 정의하는 방법

인터페이스란?

일종의 추상 클래스, 추상클래스처럼 추상 메서드를 갖지만 추상화 정도가 더 높아서 추상클래스와 달리 몸통을 가진 일반 메서드를 가질 수 없다

인터페이스 정의

interface 인터페이스이름 {
	public static final 이름 =;
    public abstract 메서드이름(매개변수);
}

인터페이스는 다음과 같이 정의할 수 있다.

인터페이스를 정의할때는 다음과 같은 제약사항이 존재하는데

  • 모든 멤버변수는 public static final이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
    (JDK 1.8의 static, default 메소드는 예외)

따라서 다음과 같이 인터페이스를 정의할 수 있다.

// java.lang.Runnable 인터페이스와 다름
 interface Runnable {

    // public static final int status = 1;
    int status = 1;

    // public abstract void run();
    void run();
}



인터페이스 구현하는 방법

인터페이스는 추상 클래스와 비슷하다 구현한다는 의미로 implements라는 키워드를 이용한다. 인터페이스는 추상클래스처럼 인터페이스 자체로 인스턴스를 생성할 수 없고 반드시 구현체에서 인터페이스를 상속해 추상메서드를 구현해야 한다.

interface Runnable {
    // public static final int status = 1;
    int status = 1;

    // public abstract void run();
    void run();
    void jump();
}

// 두 추상 메서드중 하나만 구현하여 추상클래스 선언
abstract class Runner implements Runnable {

    @Override
    public void run() {
        System.out.println("시속 20km로 달립니다.");
    }
}

// 두 메서드 모두 구현
class HighJumper implements Runnable {
	@Override
    public void run() {
        System.out.println("시속 20km로 달립니다.");
    }
    
    @Override
    public void jump() {
    	System.out.println("점프합니다.");
    }
}

만약 구현하는 인터페이스의 메서드 중 일부만 구현하고자 한다면 해당 클래스는 추상클래스가 되어야 한다.
위의 Runner 클래스는 run()메소드만 구현했으므로 추상클래스가 되어야 한다.

interface Runnable {
    void run();
}

interface Jumpable {
    void jump();
}

abstract class NationalTeam {
    int age;
    String country;
    String event;


    public abstract void setStatus(int age, String country, String event);

    public void printStatus() {
        System.out.println("age = " + age);
        System.out.println("country = " + country);
        System.out.println("event = " + event);
    }
}

// 다중 구현 및 상속과 구현을 동시에 할 수 있음
class HighJumper extends NationalTeam implements Runnable, Jumpable {

    @Override
    public void setStatus(int age, String country, String event) {
        this.age = age;
        this.country = country;
        this.event = event;
    }

    @Override
    public void run() {
        System.out.println("시속 20km로 달립니다.");
    }

    @Override
    public void jump() {
        System.out.println("점프합니다.");
    }

    public void play() {
        System.out.println("선수 입장");
        printStatus();
        System.out.println();
        run();
        jump();
    }
}

class Main {
    public static void main(String[] args) {
        HighJumper highJumper = new HighJumper();
        highJumper.setStatus(25, "대한민국", "높이뛰기");
        highJumper.play();
    }
}
[실행결과]
선수 입장
age = 25
country = 대한민국
event = 높이뛰기

시속 20km로 달립니다.
점프합니다.

구현(implements)과 상속(extends)을 동시에 할 수 있고, 한 개만 상속 받아 구현할 수 있는 클래스와 달리 인터페이스는 다중 구현을 할 수 있다. 또한 구현체에서 상속과 구현을 동시에 할 수 있다.

위의 예시는 국가대표 높이뛰기 선수가 가진 능력을 인터페이스와 추상 클래스를 이용해 구현한 예시입니다.

높이뛰기 선수는 국가대표 선수이므로 국가대표라는 추상클래스를 상속받고(NationalTeam), 동시에 뛰거나 점프할 수 있으므로 해당 능력에 해당되는 인터페이스를 다중 구현 합니다.(Runnable, Jumpable)


인터페이스? 추상클래스? (is-A, has-A)

인터페이스와 추상클래스의 역할이 비슷해 보인다. 그렇다면 인터페이스는 왜 사용할까?

위에서 예시를 본 인터페이스와 추상클래스를 살펴보자

추상클래스: 높이뛰기 선수는 국가대표이다.(is-A)
인터페이스: 높이뛰기 선수는 달릴 수 있다.(has-A)
인터페이스: 높이뛰기 선수는 뛸 수 있다.(has-A)

달리는 능력은 사람뿐 아니라 동물도 가질 수 있다. 따라서 강아지라는 Dog 클래스가 Animal이라는 추상클래스를 상속받아 동물의 특성 + 강아지의 특성을 정의하게 되면

HighJumberDog클래스는 서로 다른 추상 클래스를 상속받는다.
하지만 그럼에도 사람과 강아지 모두 달릴 수 있으므로 Runnable 인터페이스를 구현할 수 있다. 이렇게 인터페이스는 공통적인 기능을 상속관계가 다른 클래스에 기능을 구현하도록 강제 할 수 있다. 따라서 ~~할 수 있다라고 정의할 수 있는 has-A 관계가 성립한다.

반면에 추상클래스는 특정 클래스가 추상클래스를 상속받으면 특정 클래스는 추상 클래스이다로 완전히 포함되므로 is-A 관계가 성립한다.



인터페이스 레퍼런스를 통해 구현체를 사용하는 방법

인터페이스도 추상 클래스나 상위 클래스와 같이 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있고, 인터페이스 타입으로의 형변환도 가능하다.

위에서 생성한 HighJumper클래스의 인스턴스를 Runnable, Jumpable 타입의 참조변수로 참조해 보자

class Main {
    public static void main(String[] args) {
        Runnable runnable = new HighJumper();
        Jumpable jumpable = new HighJumper();

        runnable.run();
        // runnable.setStatus(20, "미국", "높이뛰기");    // 오류 발생
        
        // 캐스팅을 이용하면 사용할 수 있다.
        ((HighJumper) runnable).play();

        jumpable.jump();
        // jumpable.setStatus(20, "일본", "높이뛰기");    // 오류 발생
        
        // 캐스팅을 이용하면 사용할 수 있다.
        ((HighJumper) jumpable).play();
    }
}

코드를 보면 해당 타입의 참조변수는 해당 인터페이스에 정의된 멤버들만 호출할 수 있다. 만약 더 하위 타입으로 사용하고 싶다면 캐스팅의 작업이 필요하다.



인터페이스 상속

인터페이스의 상속은 클래스 상속과 동일하게 extends라는 키워드를 이용하고 interface로부터만 상속받을 수 있으며, 다중상속을 지원한다.

예를들어 Runnable 인터페이스와 Jumpable 인터페이스를 상속받는 새로운 인터페이스가 있다면 다음과 같이 정의할 수 있다.

interface Upgradable extends Runnable, Jumpable {
	void newMethodIndUpgradable();
}

이렇게 정의하면 기존 Runnable, Jumpable의 멤버들을 모두 상속받으며 추가로 자신의 멤버를 정의할 수 있다.

따라서 Upgradable 인터페이스는 새로 정의한 newMethodInUpgradable()뿐 아니라 run(), jump() 메소드도 멤버로 갖게 된다.



인터페이스의 기본 메소드 (Default Method), 자바 8

Java8이 등장하며 인터페이스에 default method라는 것을 추가할 수 있게 되었다.

default method는 추상 메서드의 기본적인 구현을 제공하는 메서드로 추상 메서드가 아니기 깨문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.

디폴트 메서드는 앞에 default 키워드를 붙이며, 추상 메서드와 달리 {, }이 있어야 한다. 접근제어자 역시 public이며 생략할 수 있다.

interface MyInterface {
	// 추상 메서드
    void method();
    
    // 기본 메서드
    default void method2() {
    	...
    }
}

또한 기본 메서드가 포함된 인터페이스를 확장할 때 다음과 같이 이용할 수 있다.

  • 확장 인터페이스는 default method를 상속할 수 있다.
  • default method를 다시 추상 메서드로 선언 할 수 있다.
  • default method를 Override할 수 있다.
interface MyInterface {
    default void method() {
        System.out.println("This is default Method");
    }
}

interface ChildMyInterface extends MyInterface {

	// default method를 다시 추상메서드로 정의 일종의 Override이다.
    @Override
    void method();
}

class DefaultMethod implements MyInterface {
}

class OverriddenDefaultMethod implements ChildMyInterface {

	// default method 재정의
    @Override
    public void method() {
        System.out.println("This is Overridden default method");
    }
}

class Test {
    public static void main(String[] args) {
        DefaultMethod defaultMethod = new DefaultMethod();
        defaultMethod.method();

        OverriddenDefaultMethod overriddenDefaultMethod = new OverriddenDefaultMethod();
        overriddenDefaultMethod.method();
    }
}
[결과]
This is default Method
This is Overridden default method

위와같이 기본 메서드는 재정의할 수 있다.
따라서 재정의 하면서 발생할 수 있는 문제도 존재하는데 만약 서로 다른 인터페이스에서 동일한 이름의 기본 메서드가 존재하고, 하나의 클래스에서 두 인터페이스를 구현하게 되면 문제가 될 수 있다. 이 충돌을 해결하는 규칙은 다음과 같다.

  1. 여러 인터페이스의 기본 메서드 간의 충돌
    • 인터페이스를 구현한 클래스에서 기본 메서드를 오버라이딩 해야함
  2. 기본 메서드와 조상 클래스의 메서드 간의 충돌
    • 조상 클래스의 메서드가 상속되고, 기본 메서드는 무시됨



인터페이스의 static 메소드, 자바 8

Java8은 인터페이스에 기본 메서드를 선언하는 것 외에도 인터페이스에 정적 메서드를 정의하고 구현할 수 있도록 한다.

정적 메서드는 특정 개체에 속하지 않고 인터페이스를 구현하는 클래스에도 속하지 않는다. 따라서, 메서드 이름 앞에 인터페이스의 이름을 사용하여 호출해야 한다.

인터페이스의 정적 메서드도 접근 제어자는 public이며 생략할 수 있다.

interface Name {
	static void getName() {
		return "Name";
    }
}

class Main {
	public static void main(String[] args) {
    	String name = Name.getName();
        System.out.println(name);
    }
}
[결과]
Name

인터페이스 내에서 정적 메서드를 정의 하는 것은 클래스에서 정의하는 것과 동일하다. 또한 정적 메서드는 다른 정적 및 기본 메서드 내에서 호출할 수 있다.

interface Name {
	static String getName() {
		return "Name";
	}
    
    // 정적 메서드 내에서 정적 메서드 호출
    static String getRealName() {
    	return getName() + ": Hoseok";
    }
    
    // 기본 메서드 내에서 정적 메서드 호출
    default String getProfile() {
    	return getName() + ", age";
    }
}

class Main {
	public static void main(String[] args) {
    	String name = Name.getName();
        System.out.println(name);
    }
}
[결과]
Name



인터페이스의 private 메소드, 자바 9

Java9에서 인터페이스에 private 메서드 및 private static 메서드를 추가할 수 있게 되었습니다.

private method는 인터페이스 내부의 코드 재사용성을 개선하고 의도한 메서드 구현만 사용자에게 노출할 수 있는 선택권을 제공합니다.

private method는 해당 인터페이스 내에서만 액세스할 수 있으며 인터페이스에서 다른 인터페이스 또는 클래스로 액세스하거나 상속할 수 없습니다.

interface MyInterface {
	// private method
    private void method2() {
        System.out.println("This is private method");
    }
    
    //  default method에서 private method 호출
    default void method() {
        method2();
        System.out.println("This is default Method");
    }
    
    // private static method
    private static String getType() {
    	return "person";
    }
    
    // private static method는 정적 메소드에서만 사용 가능
    static String getName() {
        return getType() + " name";
    }
}

class Test {
    public static void main(String[] args) {
        DefaultMethod defaultMethod = new DefaultMethod();
        defaultMethod.method();
        System.out.println(MyInterface.getName());
    }
}
결과
This is private method
This is default Method
person name
  • 인터페이스에서 private method를 사용하기 위한 규칙
    • private method는 abstract 메소드가 될 수 없으며 private와 abstract 키워드가 함께 있을 수 없습니다.
    • private method는 인터페이스 내부와 다른 정적 및 비정적 인터페이스 메서드에서만 사용할 수 있습니다.
    • private method는 private static method내부에서 사용될 수 없다.



References

profile
꾸준함이 주는 변화를 믿습니다.

0개의 댓글