[교재 EffectiveJava] 아이템 47. 반환 타입으로는 스트림보다 컬렉션이...

[교재 EffectiveJava] 아이템 47. 반환 타입으로는 스트림보다 컬렉션이...

728x90

스트림 반환타입

스트림은 for-each 반복을 제공하지 않는다. API를 스트림만 반환하도록 짜놓으면 반환된 스트림을 for-each로 반복하길 원하는 사용자는 불편하다. Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함하고, Iterable 인터페이스가 정의한 방식대로 동작한다. 그럼에도 for-each로 스트림을 반복할 수 없는 까닭은 Stream이 Iterable을 확장하지 않아서다.

Iterator

List list = List.of(1,2,3); Iterator iterator = list.iterator(); while(iterator.hasNext()) { system.out.println(iterator.next()); }

스트림의 경우에는 내부에 Iterator 을 response 하는 메서드를 가지고있다.

컴파일 오류 발생 코드

for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) { }

스트림 반복을 위한 끔찍한 우회 방법

for (ProcessHandle ph : (Iterable) ProcessHandle.allProcesses()::iterator) { }

좀더 나은 방법인 어댑터 메서드 사용

public static Iterable iterableOf(Stream stream) { return stream::iterator; }

어댑터 메서드 사용하여 반복처리

for (ProcessHandle ph : iterfableOf(ProcessHandle.allProcesses())) { }

이전 예제 코드로 알아보기

반복버전 - 스캐너 사용

File dictionary = new File(args[0]); try (Scanner s = new Scanner(dictionary)) { .... }

스트림 - Files.lines 메서드 사용

File dictionary = new File(args[0]); try (Stream words = Files.lines(dictionary)) { .... }

파일을 읽는동안 발생하는 모든 예외를 알아서 처리해준다는 점에서 Files.lines 쪽이 더 우수하다. 이는 스트림만 반환하는 API가 반환한 값을 for-each로 반복하길 원하는 프로그래머가 감수해야할 부분이다.

스트림 어댑터 메서드

API가 Iterable만 반환하면 이를 스트림 파이프라인에서 처리하려는 프로그래머가 불만을 가질 수 있다. 이를 어댑터로 구현해보자.

Iterable 은 스트림 변환 메서드를 제공하지 않기 때문에 StreamSupport.stream 메서드를 사용하여 구현해야한다.

public static Stream streamOf(Iterable iterable) { return StreamSupport.stream(iterable.spliterator(), false); }

StreamSupport.stream 메서드

public static Stream stream(Spliterator spliterator, boolean parallel) { Objects.requireNonNull(spliterator); return new ReferencePipeline.Head<>(spliterator, StreamOpFlag.fromCharacteristics(spliterator), parallel); }

객체 시퀀스를 반환하는 메서드를 작성하는데, 이 메서드가 오직 스트림 파이프라인에서만 쓰일 걸 안다면 마음 놓고 스트림을 반환하게 해주자. 반대로 반환된 객체들이 반복문에서만 쓰일걸 안다면 Iterable을 반환하다.

하지만 공개 API를 작성할때는 스트림 파이프라인을 사용하는 사람과 반복문에서 쓰려는 사람 모두를 배려해야한다. Collection 인터페이스는 Iterable 의 하위타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 제공한다. 따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection 이나 그 하위 타입으로 쓰는게 최선이다. Arrays 역시 Arrays.asList, Stream.of 메서드로 손쉽게 반복과 스트림을 지원할 수 있다. 반환하는 시퀀스의 크기가 메모리에 올려도 안전할만큼 작다면 ArrayList, HashSet과 같은 표준 컬렉션 구현체를 반환하는게 최선일 수 있다. 하지만 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안된다.

반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하는 방안을 검토해보자.

전용 컬렉션 구현

멱집합을 구성하는 각 원소의 인덱스를 비트 벡터로 사용하고, 인덱스의 n 번째 비트 값은 멱집합의 해당 원소가 원래 집합의 n번째 원소를 포함하는지 여부를 알려주어 훌륭한 전용 컬렉션을 구현해보자.

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

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

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

1) contains

2) size

만약 여기서 반복이 시작되기 전에는 시퀀스의 내용을 확정할 수 없는 등의 사유로 contains, size를 구현하는게 불가능할때는 컬렉션보다는 스트림이나 Iterable 을 반환하는 편이 낫다.

리스트의 부분 리스트를 반환하는 메서드 구현

1) 첫번째 원소를 포함하는 부분리스트 : 그 리스트의 prefix

(a, b, c)의 프리픽스 : (a), (a,b) (a,b,c)

2) 마지막 원소를 포함하는 부분 리스트 : 그 리스트의 suffix

(a, b, c)의 서픽스 : (a,b,c) (b,c) (c)

package com.java.effective.item47; import java.util.*; import java.util.stream.IntStream; import java.util.stream.Stream; public class SubLists { public static Stream> of(List list) { // Stream.concat : 반환되는 스트림에 빈 리스트를 추가 return Stream.concat(Stream.of(Collections.emptyList()), // flatMap : 모든 프리픽스의 모든 서픽스로 구성된 하나의 스트림을 만든다. prefixes(list).flatMap(SubLists::suffixes)); } /** for (int start = 0; start < src.size(); start++) { for (int end = start + 1; end <= src.size(); end++ { System.out.println(src.subList(start, end)); } } * @param list * @param * @return */ private static Stream> prefixes(List list) { // rangeClosed 가 반환하는 연속된 정수값들을 매핑 return IntStream.rangeClosed(1, list.size()) .mapToObj(end -> list.subList(0, end)); } private static Stream> suffixes(List list) { // range 가 반환하는 연속된 정수값들을 매핑 return IntStream.range(0, list.size()) .mapToObj(start -> list.subList(start, list.size())); } }

prefixes, suffixes 메서드는 아래 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)); } }

이를 스트림으로 변환해보자.

public static Stream> of(List list) { return IntStream.range(0, list.size()) .mapToObj(start -> IntStream.rangeClosed(start + 1, list.size()) .mapToObj(end -> list.subList(start, end))). flatMap(x -> x); }

정리

원소 시퀀스를 반환하는 메서드를 반환할때는, 이를 스트림으로 처리하기를 원하는 사용자와 반복을 처리하길 원하는 사용자 모두를 배려해야한다. 컬렉션을 반환할 수 있다면, 컬렉션을 반환하자. 반환 전부터 이미 원소들을 컬렉션에 담아 관리하고있거나, 컬렉션을 하나 더 만들어도 될 정도로 원소 개수가 적다면 ArrayList 와 같은 표준 컬렉션에 담아 반환하자. 그렇지 않으면 위 '전용 컬렉션 구현' 단원의 멱집합 예제코드처럼 전용 컬렉션을 구현할지 고민하자. 컬렉션을 반환하는게 불가능하다면 스트림과 Iterable 중 더 자연스러운 것을 반환하자. 추후 Stream 인터페이스가 Iterable 을 지원하도록 자바가 업데이트된다면 그때 안심하고 스트림을 반환하자.

from http://devfunny.tistory.com/604 by ccl(A) rewrite - 2021-11-13 10:28:05