[Effective Java] item 47 : 반환 타입으로는 스트림보다 컬렉션이 낫다

DEINGVELOP·2023년 3월 12일
0

Effective Java

목록 보기
16/19

Java 8 이전과 이후

  • ~ Java 7 : 여러 원소, 즉 일련의 원소를 반환해야 할 때 주로 Collection, Set, List 같은 컬렉션 인터페이스 / Iterable / 배열 을 사용했다.

    • Collection : 기본
    • Iterable : for-each문에서만 쓰이거나, 반환되는 원소 시퀀스가 Collection 메서드를 구현할 수 없을 때
    • 배열 : 반환 원소들이 기본 타입이거나, 성능에 민감한 상황일 때
  • Java 8 : 스트림 등장

    Stream은 Iterable을 extend 하지 않는다.
    즉, 스트림은 반복(iteration)을 지원하지 않는다. 따라서 스트림과 반복을 알맞게 조합해야 좋은 코드가 나온다.
    다만 재밌는 것은, 사실 Stream 인터페이스는 Iterable 인테페이스가 정의한 방식대로 동작함
    다만, Iterable을 extend하지 않기에 for-each로 스트림을 반복할 수는 없다.


스트림을 반복하기 위해서는?

방법 1 - Stream의 iterator 메서드에 메서드 참조를 건네기

for (ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator)
 {
 	// 프로세스를 처리한다.
 }
  • 작동은 하지만, 실전에서 쓰기에는 너무 난잡하고 직관성이 떨어짐

방법 2 - 어댑터 메서드 사용하기

public static <E> Iterable<E> iterableOf(Stream<E> stream) {
	return stream::iterator;
}
  • 위는 Stream<E>Iterable<E>로 중개해주는 어댑터
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
	// 프로세스를 처리한다.
}
  • 자바의 타입 추론이 문맥을 잘 파악하여, 어댑터 메서드 안에서 따로 형변환하지 않아도 된다.

어댑터(Adapter)

  • 구조 디자인 패턴이며, 호환되지 않는 객체들이 협업할 수 있도록 함
  • 어댑터는 두 객체 사이의 래퍼 역할을 함. 즉, 하나의 객체에 대한 호출을 캐치하고 두 번째 객체가 인식할 수 있는 형식과 인터페이스로 변환함.

Iterable<E>Stream<E>에서 처리하려면?

방법 : 어댑터를 구현하여 해결하기

public static <E> Stream<E> streamOf(Iterable<E> iterable) {
	return StreamSupport.stream(iterable.spliterator(), false);
}
  • 위는 Iterable<E>Stream<E>로 중개해주는 어댑터 코드이다.
  • 객체 시퀀스를 반환하는 메서드를 작성하는데, 이 메서드가 오직 스트림 파이프라인에서만 쓰인다면 마음 놓고 스트림을 반환하게 해도 된다.

단, 오픈 API처럼 사용자 대부분이 한 방식만 사용할 거라는 근거가 없다면, 스트림 파이프라인을 사용하는 사람과 반복문에서 사용하려는 사람 모두를 배려해야 한다.


Collection 프레임워크 이용

Collection 인터페이스는 Iterable의 하위 타입이고, stream 메서드도 제공한다. 즉, 반복과 스트림을 동시에 지원한다.

👉🏻 따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는 게 일반적으로 최선이다.

ex) Arrays 역시 Arrays.asList와 Stream.of 메서드로 손쉽게 반복과 스트림을 지원할 수 있다.

반환하는 시퀀스의 크기가 메모리에 올려도 안전할만큼 작다면, ArrayList나 HashSet같은 표준 컬렉션 구현체를 반환하는 게 최선일 수 있다.

단, 컬렉션 반환만을 목표로 덩치 큰 시퀀스를 메모리에 올려서는 안된다.

그렇다면 반환할 시퀀스가 크다면?

방법 1 - 전용 컬렉션을 구현

표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하면 된다.

  • ex) 멱집합 구현

    public class PowerSet {
        public static final <E> Collection<Set<E» of(Set<E> s) {
          List<E> src = new ArrayListo(s);
          if (src.size() > 30)
            throw new Illega1ArgumentException("집합에 원소가 너무 많습니다(최대 30개).: " + s);
    
          return new AbstractList<Set<E>>() {
          @Override
          public int size() {
          // 역집합의 크기는 2롤 원래 집합의 원소 수만큼 거듭제곱 것과 같다.
              return 1 src.sizeO;
          }
    
          @Override
          public boolean contains(Object o) {
              return o instanceof Set && src.containsAll((Set)o);
          }
    
          @Override 
          public Set<E> get(int index) {
              Set<E> result = new HashSet<>();
              for (int i = 0; index != 0; i++, index >>= 1)
                  if ((index & 1) = 1)
                      result.add(src.get(i));
              return result;
      	  }
        };
      }
    }

    입력 집합의 원소 수가 30을 넘으면 PowerSet.of가 예외를 던진다.
    이는 Stream이나 Iterable이 아닌 COllection을 반환 타입으로 쓸 때의 단점을 잘 보여준다.

  • 위처럼 AbstractCollection을 활용해서 Collection 구현체를 작성할 때는 Iterable용 메서드 외에 contains, size 2개만 더 구현하면 된다.

  • contains와 size를 구현하는 게 불가능할 경우(ex: 반복이 시작되기 전에 시퀀스 내용이 확정되지 않는 경우 등) - Stream이나 Iterable이 나음

방법 2 - 통찰을 통해 자신의 로직을 짜서 구현하기
  • ex) 입력 리스트의 (연속적인) 부분 리스트를 모두 반환하는 메서드

  • 방법 2-1 : 직접 클래스 만들기

public class SubLists {
    public static <E> Stream<List<E>> of(List<E> list) {
      return Stream.concat(St ream.of(Collections.emptyList()),
      	prefixes(list).flatMap(SubLists::suffixes));
    }
    
    private static <E> Stream<List<E>> prefixes(List<E> list) {
      return IntStream, rangeClosedd, list.size())
      	.mapToObj(end -> list.subList(0, end));
    }
    
    private static <E> Stream<List<E>> suffixes(List<E> list) {
      return IntStream. range(0, List.size())
      	.mapToObj(start -> list.subList(start, list.size()));
    }
}
  • Stream.concat : 반환되는 스트림에 빈 리스트를 추가함

  • flatMap : 모든 프리픽스의 모든 서픽스로 구성된 하나의 스트림을 만듦

  • prefixes(), suffixes() streams : IntStream. range와 IntStream.rangeClosed가 반환하는 연속된 정숫값들을 매핑해 만듦

    쉽게 말해 이 관용구는 정수 인텍스를 사용한 표준 for 반복문의 스트림 버전이라 할 수 있다. 따라서 이 구현은 for 반복문을 중첩해 만든 것과 취지가 비슷하다.

    for (int start = 0; start < src.sizeO; start++)
        for (int end = start + 1; end <= src.sizeO; end++)
        	System.out.printIn(sre.subList(start, end));
  • 방법 2-2) (위의) 반복문을 그대로 스트림으로 변환하기

    public static <E> Stream<List<E» of(List<E> list) {
       	return IntStream. range(0, list.sizeO)
           .mapToObj(start ->
           		IntStream. rangeClosed (start + 1, list, size())
           			.mapToObj(end -> list.subList(start, end)))
           .flatMap(x -> x);
     }
    • 방법2-1의 구현보다 간결해지지만,아마도 읽기에는 더 안 좋을 것이다.

요약

  • 원소 시퀀스를 반환하는 메서드를 작성할 때에는, 이를 스트림으로 처리하기를 원하는 사용자와 반복으로 처리하길 원하는 사용자가 모두 있다면 양쪽을 다 만족시켜야 한다.

  • 컬렉션을 반환할 수 있다면 그렇게 하라.

    • 특히, 원래도 컬렉션을 썼었거단 원소 개수가 적다면 ArrayList 등 표준 컬렉션에 담아 반환하라.

    • 그렇지 못하다면, 전용 컬렉션을 구현할지 고민하라.

  • 컬렉션 반환이 불가능하면, Stream과 Iterable 중 더 자연스러운 것을 반환하라.

💡 만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 Java가 수정된다면, 그 때는 안심하고 Stream을 반환하면 되겠다.

0개의 댓글