# 인터페이스의 변화
# 1. 인터페이스 기본 메소드와 스태틱 메소드
Java 8 부터는 인터페이스에도 메소드 선언뿐만 아니라
default method
와static method
를 통해 구현이 가능해졌다.
# 1-1. Default method (기본 메소드)
public interface Foo {
void printName();
}
public class DefaultFoo implements Foo{
@Override
public void printName() {
System.out.println("DefaultFoo");
}
}
Foo 인터페이스를 구현하는 클래스가 많아지면, 새로운 기능의 메소드를 선언했이 추가됐을 때 구현체 모두 컴파일 에러가 발생.
이를 해결하기 위해, 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();
}
장점
해당 인터페이스를 구현한 클래스를 깨트리지 않고 새 기능을 추가할 수 있음.주의할 점
기본 메소드는 구현체가 모르게 추가된 기능이기 때문에, 문제가 생길 수 있음.
public interface Foo {
void printName();
default void printNameUpperCase() {
System.out.println(getName().toUpperCase());
}
String getName(); // Name 값이 항상 있다고 보장할 수 없다.
// name = null -> NullPointException
}
- 컴파일 에러는 아니지만 구현체에 따라 런타임 에러 발생 가능
- 반드시 문서화 (
@ImplSpec
자바독 태그 사용)
/**
* @ImplSpec
* 이 구현체는 getName()으로 가져온 문자열을 대문자로 변환 후 출력한다.
*/
default void printNameUpperCase() {
System.out.println(getName().toUpperCase());
}
이래도 문제가 발생하는 경우는 구현 클래스에서 Overriding
을 이용해 재정의 할 수 있다.
주의할 점
Object가 제공하는 기능은 default 메소드로 제공할 수 없다.equals
,hashCode
등- 구현체가 재정의 해야한다!
- 인터페이스에서 추상 메서드로 선언하는 것은 괜찮다.
- 모두 공통으로 제공하기 때문에 추상 메서드로 분류하지 않는다.
- 본인이 수정할 수 있는 인터페이스만 수정 가능하다.
- 인터페이스를 상속받는 인터페이스에서 다시 추상 메소드로 변경할 수 있다.
구현체가 상속받는 두 인터페이스 모두 동일한 기본 메소드가 있는 경우?
public interface Foo {
default void getNameUpperCase() {
...
}
}
public interface Bar extends Foo {
void getNameUpperCase(); // 추상 메서드로 선언한 경우 구현 클래스에서 재정의 필수
// 추상 메서드로 선언 하지 않은 경우 상위 인터페이스 그대로 사용 가능
}
public interface Foo {
default void getNameUpperCase() {
System.out.println("Foo");
}
}
public interface Bar {
default void getNameUpperCase() {
System.out.println("Bar");
}
}
public class Test implements Foo, Bar {
...
// @Override
// public void printNameUpperCase() {
// ...
// }
}
Error
: DefaultFoo inherits unrelated defaults for printNameUpperCase() from types Foo and Bar
- Bar, Foo 중 무엇을 사용할지 모르기 때문에 컴파일 에러 발생
- 충돌하는 default method 직접 override 해야한다.
# 1-2. 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();
Foo.helloAll(); // static 메소드 사용
}
}
# 2. 자바8 API의 기본 메소드와 스태틱 메소드
JAVA8 에서 추가된 기본 메소드로 인해 API 에 변화에 대해 알아보자
# 2-1. Iterable
순회를 편하게 할 수 있다!
forEach()
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("catsbi");
names.add("hansol");
names.add("toby");
names.add("mijeong");
names.forEach(System.out::println);
// for (String name : names) {
// System.out.println(name);
// }
}
names.forEach(System.out::println)
- forEach 는 내부 엘리먼트를 순회하며 각각의 요소들을 파라미터로 전달된 일급함수에 Functional Interface인 Consumer가 들어오고 처리.
- 단순 출력만 하기 때문에 메소드 레퍼런스 기능을 이용해 간결하게 작성!
for-of{...}
- 기존에는 for-of 문을 사용해 출력이 가능하다고 함.
- 하지만 조금이나마 더 간결한 forEach를 사용하는 것이 좋아 보임.
# 2-2. Collection
Iterable
을 상속받는 인터페이스다!
spliterator
iterator 와 비슷한 개념이지만 Collection 을 분할한다는 점에서 차이가 있음.
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("yj");
names.add("youngjun");
names.add("spring");
names.add("java8");
Spliterator<String> spliterator = names.spliterator();
while(spliterator.tryAdvance(System.out::println));
}
- Spliterator
spliterator = names.spliterator(); - iterator와 비슷하지만 분할할 수 있는 기능을 가진 Iterator 를 만들어 반환
- spliterator.tryAdvance(System.out::println)
- iterator의 hasNext()메소드와 유사
- 다만 내부 메소드에 파라미터로 forEach()와 동일하게 Functional Interface인 Consumer가 들어오고 더 이상 들어올게 없을 경우 false를 반환
- 이 예제에서는 들어온 값을 단순 출력하는 메소드 레퍼런스를 사용
여기까지는 iterator와 차이가 없다. trySplit을 사용해보자.
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("yj");
names.add("youngjun");
names.add("spring");
names.add("java8");
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));
}
/*
[실행 결과]
spring
java8
=========================
yj
youngjun
*/
Spliterator<String> trySplit = spliterator.trySplit();
- spliterator에서 trySplit()메소드를 호출하게되면 해당 spliterator에서 앞에서부터 절반의 요소를 꺼내 새로운 spliterator를 만들어 반환
🔼 trySplit() 실행 결과
while(spliterator.tryAdvance(System.out::println));
- trySplit()을 통해 앞의 두 String(yj, youngjun)이 분할되어 빠져나갔기 때문에 뒤의 두 값만 출력
while(trySplit.tryAdvance(System.out::println));
- spliterator 에서 가져온 앞 두 값(yj, youngjun)을 출력
removeIf
Collection 요소를 순회하며 인자로 넘겨주는 함수를 Functional Interface인 Predicate에 넘겨줘서 true 이면 삭제
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("yj");
names.add("youngjun");
names.add("spring");
names.add("java8");
names.removeIf(s -> s.startsWith("y"));
names.forEach(System.out::println);
}
names.removeIf(s -> s.startsWith("y"));
- names를 순회하며 각 요소들 중 단어가 'y'로 시작하는 단어를 찾아 삭제
# 2-3. Comparator
정렬에 사용되는 인터페이스
reversed
정렬을 역순으로!
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("yj");
names.add("youngjun");
names.add("spring");
names.add("java8");
Comparator<String> compareToIgnoreCase = String::compareToIgnoreCase;
names.sort(compareToIgnoreCase.reversed());
names.forEach(System.out::println);
}
Comparator<String> compareToIgnoreCase = String::compareToIgnoreCase
- 메소드 레퍼런스를 메소드 체이닝 방식으로 사용할수는 없기에 분리하여 Comparator 타입의 Functional Interface인 compareToIgnoreCase 를 만들어준다.
names.sort(compareToIgnoreCase.reversed());
- 미리 선언해놓은 String 의 정렬 기준 메소드 레퍼런스에
reversed()
메소드를 호출해 역순으로 정렬
- 미리 선언해놓은 String 의 정렬 기준 메소드 레퍼런스에
❓ 만약, 여기서 다음 정렬조건을 사용하여 정렬을 이어가고싶다면 thenComparing() 메소드를 이용하여 추가적인 정렬을 할 수 있다.
# Reference
← 함수형 인터페이스와 람다 Stream →