-
아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계해라.Study/Effective Java 2023. 8. 10. 08:33
기존 인터페이스에 디폴트 메서드 구현을 추가하는 것은 위험한 일이다.
해당 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다. 자바 7까지의 세상에서는 모든 클래스가 “현재의 인터페이스에 새로운 메서드가 추가될 일은 영원히 없다”고 가정하고 작성되었다.
자바 8에서는 람다를 활용하기 위해 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었다. 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.
▼ java.util.Collection.removeIf 메서드 (java 8에서 추가)
default boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); boolean removed = false; final Iterator<E> each = iterator(); while (each.hasNext()) { if (filter.test(each.next())) { each.remove(); removed = true; } } return removed; }
동기화와 관련된 코드가 없어서 멀티스레드 환경에서 안전하지 않은 컬렉션이 된다.
apache.commons.collections4.collection.SynchronizedCollection
이 대표적인 예다. 모든 메서드에서 주어진 락 객체로 동기화한 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스이다.removeIf
메서드를 재정의를 하지 않는다면, 모든 메서드 호출을 알아서 동기화해주지 못하기 때문에 자신이 한 약속을 더 이상 지키지 못하게 된다.removeIf
의 구현은 동기화에 관해 아무것도 모르므로 락 객체를 사용할 수 없다. 따라서SynchronizedCollection
인스턴스를 여러 스레드가 공유하는 환경에서 한 스레드가removeIf
를 호출하면ConcurrentModificationException
이 발생하거나 다른 예기치 못한 결과로 이어질 수 있다.💡 책을 쓰는 시점에는 재정의되어있지 않았지만, 4.4 버전부터 재정의 되었다.
/** * @since 4.4 */ @Override public boolean removeIf(final Predicate<? super E> filter) { synchronized (lock) { return decorated().removeIf(filter); } }
이처럼 인터페이스를 구현한 클래스 관점에서 보았을 때 기능이 해가될 수 있다.
디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.
public interface MarkerInterface { default void hello() { System.out.println("hello interface"); } }
public class SuperClass { private void hello() { System.out.println("hello class"); } }
public class SubClass extends SuperClass implements MarkerInterface { public static void main(String[] args) { SubClass subClass = new SubClass(); subClass.hello(); } }
위와 같은 예제 코드를 구현하였을 때,
MarkerInterface.hello()
가 있어 컴파일 오류가 발생하지 않지만, 실행할 때는 private 메서드인SuperClass.hello()
에 접근하여 런타임 오류가 발생한다.Exception in thread "main" java.lang.IllegalAccessError: class me.whiteship.chapter04.item21.SubClass tried to access private method 'void me.whiteship.chapter04.item21.SuperClass.hello()' (me.whiteship.chapter04.item21.SubClass and me.whiteship.chapter04.item21.SuperClass are in unnamed module of loader 'app') at me.whiteship.chapter04.item21.SubClass.main(SubClass.java:7)
클래스가 인터페이스를 이긴다는 규칙이 적용되었고, 추후 고쳐질 수 있지만 현재 최신 버전인 자바17까지 발생중이다.
ConcurrentModificationException
현재 바뀌면 안되는 것을 수정할 때 발생하는 예외이다. 멀티 스레드가 아니라 싱글 스레드 상황에서도 발생할 수 있다.
https://docs.oracle.com/javase/8/docs/api/java/util/ConcurrentModificationException.html
Iterators that do this are known as fail-fast iterators, as they fail quickly and cleanly, rather that risking arbitrary, non-deterministic behavior at an undetermined time in the future.
💡 fail-fast iterator : 바뀌면 안되는데 바뀌는 상황이 발생하면 빨리 실패한다. 미래에 문제가 생길 수 있는 동작을 미리 실패하는 것으로, 순회하는 도중에 예외를 발생시킨다.
List<Integer> numbers1 = List.of(1, 2, 3, 4, 5); List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); numbers.add(4); numbers.add(5);
예제를 보기 전, numbers1, numbers는 서로 다름을 참고하고 넘어가자.
List.of
는 수정이 불가능한 Collection이다.static <E> List<E> of(E e1, E e2, E e3, E e4, E e5) { return ImmutableCollections.listFromTrustedArray(e1, e2, e3, e4, e5); }
ImmutableCollections로 구현되어 있으며, numbers1을 수정하려고 하면
UnsupportedOperationException
이 발생한다.Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.remove(ImmutableCollections.java:150)List를 수정하려면 수정가능한 numbers를 사용하여야 한다.
▼ ConcurrentModificationException 에러가 발생하는 예제코드
public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); numbers.add(4); numbers.add(5); // 이터레이터로 콜렉션을 순회하는 중에 Collection의 remove를 사용한다면... for (Integer number : numbers) { if (number == 3) { numbers.remove(number); } } // 출력 numbers.forEach(System.out::println); }
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)Iterator로 Collection을 순회하며 Collection의 remove를 사용한다면
ConcurrentModificationException
이 발생한다.방법1) Iterator의 remove 사용
public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); numbers.add(4); numbers.add(5); // 이터레이터의 remove 사용하기 for (Iterator<Integer> iterator = numbers.iterator(); iterator.hasNext();) { Integer integer = iterator.next(); if(integer == 3) { iterator.remove(); } } // 출력 numbers.forEach(System.out::println); }
에러가 발생하는 예제코드와 아주 비슷한 코드임에도 컬렉션이라는 외부의 remove가 아니라, iterator를 사용해서 remove를 하면 안전하다.
방법2) Index 사용
public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); numbers.add(4); numbers.add(5); // 인덱스 사용하기 for (int i = 0; i < numbers.size() ; i++) { if (numbers.get(i) == 3) { numbers.remove(numbers.get(i)); } } // 출력 numbers.forEach(System.out::println); }
Iterator를 사용하지 않고 Index를 사용하는 방법이다.
방법3) removeIf 사용
public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); numbers.add(4); numbers.add(5); // removeIf 사용하기 numbers.removeIf(number -> number == 3); // 출력 numbers.forEach(System.out::println); }
위에서 계속 살펴보던 default method removeIf를 사용하는 방법으로, 내부적으로 방법1)과 똑같이 Iterator의 remove를 사용하고 있다.
'Study > Effective Java' 카테고리의 다른 글
아이템 23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라 (0) 2023.08.10 아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) 2023.08.10 아이템 20. 추상 클래스보다는 인터페이스를 우선하라 (0) 2023.08.03 아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라. (0) 2023.08.02 아이템 18. 상속보다는 컴포지션을 사용하라 (0) 2023.07.29