무작정 개발.Vlog

[Java8] 인터페이스의 변화 - default method, static method

by 무작정 개발
반응형

이번에는 인터페이스의 변화, default method, static method에 대해 정리할 것이다.

 

[목차]

  1. 인터페이스 기본 메서드와 스태틱 메서드
  2. Java 8 API의 기본 메서드와 스태틱 메서드

 

1. 인터페이스 기본 메서드와 스태틱 메서드

 

지금까지 사용하던 Java의 인터페이스 기능은 추상 메서드와 달리 메서드 선언만 할 뿐 그 내부에 로직이 들어가는 일이 없었습니다.

하지만 Java 8부터 인터페이스에 메서드 선언뿐만 아니라 구현이 가능해졌는데 이 구현 방법으로는

default method(기본 메서드), static method(스태틱 메서드)가 있습니다.

 

(1) 기본 메서드(Default Method)

// Foo 인터페이스 생성
public interface Foo {
	// 추상 메서드(abstract method)
	void printName(); // void 앞에 abstract 생략 가능
}

// Foo 인터페이스를 상속받은 DefaultFoo 클래스 생성 - 구현체
public class DefaultFoo implements Foo {
	@Override
    public void printName() {
    System.out.println("DefaultFoo!!");
}

 

위에 Foo 인터페이스와 DefaultFoo 클래스(구현체)를 생성하였습니다.

 

public interface Foo {
	// 추상 메서드(abstract method)
	void printName(); // void 앞에 abstract 생략 가능
    
	// 기능 추가
	void printNameUpperCase();
}

여기서 Foo를 구현한 클래스에 공통적으로 사용할 기능을 추가해야 하는 상황이 생겨 Foo 인터페이스가 추상 메서드 1개를 추가한다면

Foo를 구현한 모든 구현체에서 Error 메세지가 출력됩니다.

 

Why?

-> 구현체(DefaultFoo 클래스)에 따로 구현을 해주지 않았기 때문입니다. 기본 메서드(default method)는 이러한 상황 때문에

생기게 되었고 Error가 발생하지 않고 공통적으로 사용할 기능을 추가할 수 있는 방법이  바로 default method를 활용하는 방법입니다.

 

public interface Foo {

	void printName();
    
	// 기본 메서드(Default Method)
	default void printNameUpperCase() {
		System.out.println("FOO");
    }
}
// =============================================
public static void main(String[] args) {

	Foo foo = new DefaultFoo();
	foo.printName();
	foo.printNameUpperCase();
}

 

이처럼 default method를 사용하면 구현체(인터페이스를 상속받는 클래스)에서 따로 해당 메서드를 구현하지 않아도 사용 가능합니다.

하지만 default method는 구현체가 모르게 추가된 기능이기 때문에 구현체에서는 알 수 있는 방법이 없습니다.

그렇기에 구현체에서는 default method의 기능, 반환 값 등을 모르기 때문에 문제가 발생할 수 있습니다.

[ default로 제공되는 기능이 모든 구현체의 인스턴스에게 항상 제대로 동작한다는 보장이 없다.]

 

public interface Foo {
    void printName();

    default void printNameUpperCase(){
        System.out.println(getName().toUpperCase());
    }
    String getName();
}

 

예시를 들어 위의 코드에서 default method인 printNameUpperCase()에서 구현체가 구현한 getName() 메서드를 호출해

toUpperCase()를 호출하지만, 실제 구현체의 getName()에서 무조건 문자열이 반환될지에 대해서는 확실하지 않습니다.

 

만약에 null값이 반환된다면 Error가 발생하기에 최소한으로 반드시 문서화를 해야 합니다. -> Java8에서 추가된 @ImplSpec 어노테이션

 

/**
 * @ImplSpec
 * 이 구현체는 getName()으로 가져온 문자열을 대문자로 변환 후 출력한다.
 */
default void printNameUpperCase(){
    System.out.println(getName().toUpperCase());
}

 

위의 방법처럼 @ImplSpec 어노테이션을 사용해 문서화를 하는 방법이 있습니다.

이렇게 해도 문제가 생긴다면 구현체에서 재정의(Override)하는 방법이 있고, 일반적으로 선언된 메서드와 동일하게 오버라이드 해서 기능을 정의하면 됩니다.

 

추가적으로 기본 메서드(default method)는 Object가 제공하는 기능(equals, hasCode)은 default method로 제공할 수 없고, 

재정의는 구현체에서 해야 합니다. 또한 default method는 Foo 클래스처럼 사용자가 직접 정의한 인터페이스에서만 사용이 가능하며

외부 라이브러리 등에 추가하여 사용할 수 없습니다.

 

(2) 기본 메서드(Default Method)의 상속

기본 메서드를 가진 인터페이스를 상속받는 인터페이스에서 기본 메서드가 필요 없다면 추상 메서드로 변경해서 사용 가능합니다.

 

public interface Foo {
    void printName();
    /**
     * @ImplSpec
     * 이 구현체는 "안녕하세요"를 출력한다.
     */
    default void hello(){
        System.out.println("안녕하세요!!");
    }
}
// ====================================================
public interface Hello extends Foo {
	
    // 추상 메서드 - abstract 생략가능
    void hello(); // 안녕하세요!!
}

 

위를 보면  Foo 인터페이스에 있는 hello() 메서드를 default 메서드로 두고 싶지 않을 때 Foo 인터페이스를 상속받는 Hello 인터페이스에서

이 메서드를 다시 추상 메서드로 변경할 수 있습니다.

 

(3) 다이아몬드 문제

 

여기서 구현체가 상속받는 두 인터페이스 모두 동일한 기본 메서드(default method)가 있을 경우가 있습니다.

 

public interface Bar {
    default void hello(){
        System.out.println("Bar");
    }
}
// ===============================================
public interface Foo {
    void printName();
    
    default void hello(){
        System.out.println("Foo");
    }
}
// ===============================================
public class DefaultFoo implements Foo, Bar{
    private String name;
	// 다이아몬드 문제 해결을 위해 직접 Override(재정의) 해준다.
    @Override
    public void printName() {
        System.out.println("DefaultFoo");
    }
}

 

Foo, Bar 인터페이스가 hello()라는 동일한 이름을 가진 기본 메서드가 있고, DefaultFoo 클래스는 Foo, Bar 인터페이스를 상속받는

상황일 때 하단의 Error가 발생합니다.

Error: DefaultFoo inherits unrelated defaults for hello() from types Foo and Bar

구현체가 동시에 두 인터페이스를 구현하려고 하기 때문에 컴파일 에러가 발생하는데 이 문제를 다이아몬드 문제라고 합니다.

Why?

-> Foo와 Bar 인터페이스 둘 중 어느 기본 메서드인지 모호하기 때문에 발생하고, 이 경우 직접 Override(재정의)를 해서

hello() 메서드를 재정의 해줘야 합니다.

 

(4) Static Method

해당 인터페이스를 구현한 모든 인스턴스나 해당 타입에 관련된 유틸리티나 헬퍼 메서드를 제공할 때 인터페이스에 static method를 제공할 수 있습니다.

 

public interface Foo {
    void printName();

    default void hello(){
        System.out.println("Foo");
    }
    static void helloAll(){
        System.out.println("인삿말");
    }
}

 

public class App {
    public static void main(String[] args) {
        Foo foo = new DefaultFoo();
        foo.hello();
        //static 메서드 사용
        Foo.helloAll();
    }
}

2. Java 8 API의 기본 메서드와 스태틱 메서드

Java 8에 추가된 API에 대해 정리할 것이다.

 

(1) Java 8에서 추가된 기본 메서드로 인한 API 변화

 

Iterable의 기본 메서드

  • forEach()
  • spliterator()

Collection의 기본 메서드

  • stream() / parallelStream()
  • removeIf(Predicate)
  • spliterator()

Comparator의 기본 메서드 및 스태틱 메서드

  • reversed()
  • thenComparing()
  • static reverseOrder() / naturalOrder()
  • static nullsFirst() / nullsLast()
  • static comparing()

 

(2)  Iterable

 

들어가기 전에 앞서 Iterable 인터페이스는 Collection 인터페이스의 상위 인터페이스입니다.

즉, Collection은 Iterable을 상속받는 인터페이스입니다.

인터페이스 계층 구조
인터페이스 계층 구조

 

  • forEach
    • 조금 더 쉽게 Loop를 가능하게 해주는 메서드
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("hyunwoo");
    names.add("Blog");
    names.add("25age");
    names.add("무작정개발");

    names.forEach(System.out::println);
		
	/*for (String name : names) {
        System.out.println(name);
    }*/
}

 

1. names.forEach(System.out::println);

-> forEach는 내부 엘리먼트를 순회하며 각각의 요소들을 파라미터로 전달된 일급 함수에 Functional Interface인 Consumer가

들어오게 되는데 이를 받아서 처리할 수 있습니다. 출력만 해줄 것이기에 메서드 레퍼런스 기능을 이용해 간결하게 작성합니다.

 

2. for (String name : names) {  System.out.println(name) };

-> 이 방법 또한 결과는 동일하지만 forEach를 사용하면 좀 더 간결하게 표현이 가능합니다.

 

 

  • spliterator
    • iterator와 비슷한 개념으로 Collection을 분할한다는 점이 다르다.
    • split + iterator
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("hyunwoo");
    names.add("blog");
    names.add("26age");
    names.add("무작정개발");

    Spliterator<String> spliterator = names.spliterator();
    Spliterator<String> trySplit = spliterator.trySplit();
    while(spliterator.tryAdvance(System.out::println));
    System.out.println("=========================");
    while(trySplit.tryAdvance(System.out::println));
}
/*
[실행 결과]
26age
무작정개발
=======
hyunwoo
blog
*/

 

1. Spliterator<String> spliterator = names.spliterator();

-> iterator와 비슷하지만 분할하는 기능을 가진 iterator를 만들어서 반환합니다.

 

2. spliterator.tryAdvance(System.out::println);

-> iterator의 hasNext() 메서드와 유사합니다. 다만 메서드 파라미터로 forEach()와 동일하게 Functional Interface인 Consumer가

들어오며 더 이상 들어올 게 없을 경우 false를 반환합니다.

 

3. Spliterator<String> trySplit = spliterator.trySplit();

-> spliterator에서 trySplit() 메서드를 호출하게 되면 해당 spliterator에서 앞에서부터 절반의 요소를 꺼나 새로운 spliterator를 만들어서 반환합니다.

trySplit() 실행 결과
trySplit() 실행 결과

 

4. while(spliterator.tryAdvance(System.out::println));

-> trySplit()를 통해 앞의 두 요소(Catsbi, Hansol)이 분할되어 빠져나갔기 때문에 뒤의 두 요소만 출력합니다.

 

5. while(trySplit.tryAdvace(System.out::println));

-> spliterator에서 가져온 앞 두 요소(Catsbi, Hansol)을 출력합니다.

 

  • removeIf
    • Collection 요소를 순회하며 인자로 넘겨주는 함수를 Functional Interface인 Predicate에 넘겨줘서 합당한(true를 반환) 값을 찾아 삭제(Delete)합니다.
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("hyunwoo");
    names.add("blog");
    names.add("26age");
    names.add("무작정개발");

    names.removeIf(s -> s.startsWith("c"));
    names.forEach(System.out::println);
}

 

1. names.removeIf(s -> s.startsWith("h"));

-> names을 순회하며 각 요소들 중 'c'로 시작하는 단어를 찾아서 삭제(Delete)합니다.

 

(2) Comparator

정렬(Sort)에 사용되는 인터페이스

 

  • reversed
    • 메서드명 그대로 뒤집는 것인데, 숫자를 오름차순으로 정렬한 뒤 reversed() 메서드를 호출하면 반대로 내림차순으로 정렬이 됩니다.
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("hyunwoo");
    names.add("blog");
    names.add("26age");
    names.add("무작정개발");

    Comparator<String> compareToIgnoreCase = String::compareToIgnoreCase;
    names.sort(compareToIgnoreCase.reversed());
    names.forEach(System.out::println);
}

 

1. Comparator<String> compareToIgnoreCase = String::compareToIgnoreCase

-> 메서드 레퍼런스를 메서드 체이닝 방식으로 사용할 수는 없기 때문에 분리하여 Comparator 타입의 Functional Interface인 compareToIgnoreCase를 만들어줍니다.

 

2. names.sort(compareToIgnoreCase.reversed());

-> 미리 선언해놓은 String의 정렬 기준 메서드 레퍼런스에 reversed() 메서드를 호출해 정렬 순서를 역순으로 바꿔줍니다.

 

만약에 여기서 다음 정렬 조건을 사용하여 이어가고 싶다면 thenComparing() 메서드를 사용하여 추가적인 정렬이 가능합니다.

 


Reference

Kangworld 티스토리 블로그

 

[Java8] Chapter 2-1. 인터페이스 default, static 메서드

✍️인터페이스 default 메서드 default 메서드 예제 자바 8부터 인터페이스에 default 메서드와 static 메서드가 추가됐다. public interface Foo { void printName(); } public class DefaultFoo implements Foo { String name; @Over

kangworld.tistory.com

인프런-더 자바, Java8_백기선

 

더 자바, Java 8 - 인프런 | 강의

자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합니다. 이

www.inflearn.com

 

반응형

블로그의 정보

무작정 개발

무작정 개발

활동하기