본문 바로가기
Java

[Effective Java] 반환 타입으로는 Stream 보다 Collection이 낫다. (item 47)

by 곰민 2023. 1. 13.
728x90

Stream에서는 반복을 지원하지 않기 때문에 API에서 Stream만 반환하도록 한다면 반복과 stream을 잘 시기적절하게 사용하기를 원하는 사용자는 불만을 토로할 수 있다. Stream을 사용할 수도 반복을 사용할 수도 있게 지원해야 한다.

Stream과 iterator


Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함하고 정의한 방식대로 동작하나

for-each로 stream을 반복할 수 없는 이유는 Stream이 Iterable을 extend 하지 않아서 이다.

 

실제로 BaseStream을 열어보면 iterator메서드가 들어있다.

 

하지만 반복문에서 활용하면 컴파일 에러가 발생한다.

 

메서드 참조를 매개변수화된 Iterable로 적절히 형변환 해줘야 한다.

Stream<String> stars = Stream.of("승천한 메시", "우승한 지우", "개발자 곰민");
for (String starts : (Iterable<String>)stars::iterator) {
    System.out.println("starts = " + starts);
}

 

어댑터 메서드를 활용하여 직관적인 코드로 작성도 가능하다.

public static <E> Iterable<E> iterableOf(Stream<E> stream) {
	return stream::iterator; 
}
for (String starts : iterableOf(stars)) {
    System.out.println("starts = " + starts);
}

 

어댑터 메서드를 작성하여 이를 활용하자.

 

공개 API에서의 Collection 반환


해당 메서드가 오직 stream pipe line에서만 쓰이는 걸 안다면 stream을 반환해 주고
반환된 객체들이 반복문에서만 쓰일 걸 알면 Iterable을 반환하면 된다.

하지만 공개 API의 경우 stream pipe line을 사용하려는 사용자와 반복문을 사용하려는 사용자 둘 다 배려해야 한다.
Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 stream을 동시 지원한다.
즉 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는 게 일반적으로 최선이다.

 

전용 커스텀 컬렉션을 구현하는 방안을 검토하자


단지 collection을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.

 

입력 집합의 멱집합(한 집합의 모든 부분집합을 원소로 하는 집합)을 전용 컬렉션에 담아 반환

{a, b, c}의 멱집합 : {{a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c}}

public class PowerSet {
    public static final <E> Collection<Set<E>> of(Set<E> s) {
       List<E> src = new ArrayList<>(s);
       if(src.size() > 30) {
           throw new IllegalArgumentException("집합에 원소가 너무 많습니다(최대 30개).: " + s);
       }

       return new AbstractList<Set<E>>() {
           @Override
           public int size() {
               return 1 << src.size();
           }

           @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;
           }
       };
    }
}

size() 메서드의 리턴 타입은 int이기에 최대 길이는 2^31 - 1 또는 Integer.Max_Value로 제한되기에

입력 집합의 원소 수가 30을 넘으면 Power.of가 예외를 던집니다.

Collection을 반환할 때의 단점을 보여주는데 Collection의 size 메서드가 int값을 반환하므로 생깁니다.

 

Collection 보다 Stream이 나을 때도 있다


위의 예제처럼 AbstractCollection을 활용해서 Collection 구현체를 리턴할 때는 Iterator용 메서드 외에 2개만 더 구현하면 된다.
바로 contains과 size이다.

하지만 반복이 시작되기 전에는 (시퀀스의 내용을 확정할 수 없는 등의 사유로) contains와 size를 구현할 수 없는 경우에는 Collection보다는 Stream이나 Iterable을 반환하는 편이 낫다.

 

때로는 단순히 구현하기 쉬운 쪽을 선택하기도 한다


입력 리스트의 (연속적인) 부분리스트를 모두 반환하는 메서드를 작성한다면? 
필요한 부분리스트를 만들어서 표준 컬렉션에 담는 코드는 3줄이면 충분할 수 있다.
하지만 입력 리스트의 크기가 거듭제곱만큼 메모리를 차지하기에 좋은 방식이 아니다.

하지만 입력 리스트의 모든 부분 리스트를 스트림으로 구현하기엔 구현이 쉽다.

 

첫 번째 원소를 포함하는 부분리스트를 해당 리스트의 prefix라고 하자.

  • (a,b,c) 의 prefix는 (a), (ab), (a,b,c)가 된다.

마지막 원소를 포함하는 부분리스트를 해당 리스트의 suffix라고 하자.

  • (a,b,c)의  suffix는 (a,b,c), (b,c), (c)
public class SubLists {
	public static <E> Stream<List<E>> of(List<E> list) {
		return Stream.concat(Stream.of(Collections.emptyList()), 
       			 prefixes(list).flatMap(SubLists::suffixes));
	}
    
	private static <E> Stream<List<E>> prefixes(List<E> list) { 
    	return IntStream.rangeClosed(1, 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 메서드는 반환되는 Stream에 빈 리스트를 추가.

flatMap 메서드는 모든 prefix의 모든 suffix로 구성된 하나의 Stream으로 만들어 주는 과정을 합니다.

 

for문을 활용한 예시

for (int start = 0; start < src.size(); start++)
	for (int end = start + 1; end <= src.size(); end++)
		System.out.println(src.subList(start, end));

 

 

핵심정리


  1. 원소 시퀀스를 반환하는 메서드를 작성할 때는, 이를 스트림으로 처리하기를 원하는 사용자와 반복으로 처리하길 원하는 사용자가 모두 있을 수 있음을 떠올리고, 양쪽을 다 만족시키려 노력하자.
  2. 컬렉션을 반환할 수 있다면 그렇게 하라.
  3. 반환 전부터 이미 원소 들을 컬렉션에 담아 관리하고 있거나 컬렉션을 하나 더 만들어도 될 정도로 원소 개수 가 적다면 ArrayList 같은 표준 컬렉션에 담아 반환하라.
  4. 그렇지 않으면 앞서의 멱집 합 예처럼 전용 컬렉션을 구현할지 고민하라.
  5. 렉션을 반환하는 게 불가능하면 스트 림과 Iterable 중 더 자연스러운 것을 반환하라.
  6. 만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 자바가 수정된다면, 그때는 안심하고 스트림을 반환하면 될 것이다(스트림 처리와 반복 모두에 사용할 수 있으니).

 

참조


Effective java

Carrey`s 기술블로그

728x90

댓글