[java] 컬렉션이나 스트림을 반환해야하나요?

읽기 전용 뷰를 멤버 목록으로 리턴하는 메소드가 있다고 가정하십시오.

class Team {
    private List < Player > players = new ArrayList < > ();

    // ...

    public List < Player > getPlayers() {
        return Collections.unmodifiableList(players);
    }
}

또한 모든 클라이언트가 즉시 목록을 한 번 반복한다고 가정하십시오. 플레이어를 JList 또는 다른 것에 넣을 수도 있습니다. 클라이언트는 나중에 검사하기 위해 목록에 대한 참조를 저장 하지 않습니다 !

이 일반적인 시나리오에서 스트림을 대신 반환해야합니까?

public Stream < Player > getPlayers() {
    return players.stream();
}

아니면 Java에서 스트림을 비이 디오 틱으로 반환합니까? 스트림은 생성 된 것과 동일한 표현 내에서 항상 “종료”되도록 설계 되었습니까?



답변

대답은 언제나 그렇듯이 “의존적”입니다. 반환 된 컬렉션의 크기에 따라 다릅니다. 시간이 지남에 따라 결과가 변경되는지 여부와 반환 된 결과의 일관성이 얼마나 중요한지에 따라 다릅니다. 그리고 사용자가 어떻게 대답을 사용할 지에 달려 있습니다.

먼저 스트림에서 컬렉션을 항상 가져올 수 있으며 그 반대도 가능합니다.

// If API returns Collection, convert with stream()
getFoo().stream()...

// If API returns Stream, use collect()
Collection<T> c = getFooStream().collect(toList());

따라서 질문은 발신자에게 더 유용합니다.

a

결과가 무한 할 수있는 경우 스트림 중 하나만 선택할 수 있습니다.

결과가 매우 클 경우 스트림을 한 번에 구체화하는 데 아무런 가치가 없기 때문에 스트림을 선호 할 수 있으며, 그렇게하면 상당한 힙 압력이 발생할 수 있습니다.

모든 발신자가 처리 (검색, 필터링, 집계)를 반복하는 경우 Stream에 이미 내장되어 있고 컬렉션을 구체화 할 필요가 없으므로 (특히 사용자가 전체 결과입니다.) 이것은 매우 일반적인 경우입니다.

사용자가 여러 번 반복하거나 주변에 유지한다는 것을 알고 있더라도 스트림을 반환하기로 선택한 컬렉션 (예 : ArrayList)이 그렇지 않을 수도 있다는 사실 때문에 스트림을 대신 반환 할 수 있습니다 원하는 형식으로 발신 한 다음 발신자는 어쨌든 복사해야합니다. 스트림을 반환하면 collect(toCollection(factory))원하는 형식으로 할 수 있습니다.

위의 “스트림 선호”사례는 대부분 스트림이 더 유연하다는 사실에서 비롯됩니다. 컬렉션에 구체화하는 데 드는 비용과 제약을받지 않으면 서 사용 방법에 늦게 묶을 수 있습니다.

컬렉션을 반환해야하는 경우는 일관성 요구 사항이 강력하고 움직이는 대상의 일관된 스냅 샷을 만들어야하는 경우입니다. 그런 다음 변경되지 않는 컬렉션에 요소를 넣기를 원할 것입니다.

따라서 대부분의 경우 Stream이 정답입니다. 더 유연하고 일반적으로 불필요한 materialization 비용을 부과하지 않으며 필요한 경우 원하는 컬렉션으로 쉽게 전환 할 수 있습니다. 그러나 때로는 일관성이 높은 요구 사항으로 인해 컬렉션을 반환해야 할 수도 있고, 사용자가 컬렉션을 사용하는 방법을 알고 이것이 가장 편리한 방법임을 알고 컬렉션을 반환해야 할 수도 있습니다.


답변

Brian Goetz의 훌륭한 답변 에 추가해야 할 몇 가지 사항이 있습니다 .

“getter”스타일 메소드 호출에서 Stream을 반환하는 것이 일반적입니다. Java 8 javadoc 의 Stream 사용법 페이지 를 참조하고 이외의 패키지에 대해 “Streams를 반환하는 메소드 …”를 찾으십시오 java.util.Stream. 이러한 메소드는 일반적으로 여러 값 또는 무언가의 집합을 나타내거나 포함 할 수있는 클래스에 있습니다. 이러한 경우 API는 일반적으로 컬렉션이나 그 배열을 반환했습니다. Brian이 자신의 답변에서 언급 한 모든 이유 때문에 여기에 스트림 반환 방법을 추가하는 것이 매우 유연합니다. 클래스는 Streams API보다 먼저 사용되기 때문에 이러한 클래스 중 다수에는 이미 컬렉션 또는 배열 반환 메소드가 있습니다. 새로운 API를 디자인 할 때 스트림 리턴 메소드를 제공하는 것이 합리적이라면 콜렉션 리턴 메소드도 추가하지 않아도됩니다.

Brian은 값을 컬렉션으로 “구체화”하는 비용을 언급했습니다. 이 점을 증폭시키기 위해 실제로 두 가지 비용이 있습니다 : 콜렉션에 값을 저장하는 비용 (메모리 할당 및 복사)과 처음에 값을 생성하는 비용. 후자의 비용은 종종 스트림의 게으름을 찾는 행동을 이용하여 줄이거 나 피할 수 있습니다. 이에 대한 좋은 예는 다음과 같은 API입니다 java.nio.file.Files.

static Stream<String>  lines(path)
static List<String>    readAllLines(path)

뿐만 아니라 않는 readAllLines결과리스트에 저장하기 위해 메모리에 전체 파일 내용을 유지해야한다, 그것은 또한 목록을 반환하기 전에 끝까지 파일을 읽을 수 있습니다. 이 lines메소드는 설정을 수행 한 후 거의 즉시 리턴하여 필요할 때까지 파일 읽기 및 줄 바꿈을 남겨 둘 수 있습니다. 예를 들어, 발신자가 처음 10 줄에만 관심이있는 경우 이는 큰 이점입니다.

try (Stream<String> lines = Files.lines(path)) {
    List<String> firstTen = lines.limit(10).collect(toList());
}

물론 호출자가 패턴과 일치하는 행만 반환하도록 스트림을 필터링하면 상당한 메모리 공간을 절약 할 수 있습니다.

출현하고있는 관용구는 get접두사 없이 표현하거나 포함하는 것들의 이름을 여러 번 사용한 후 스트림 반환 방법의 이름을 지정하는 것입니다 . 또한 stream()반환 할 수있는 값 집합이 하나 뿐인 경우 스트림 반환 방법의 적절한 이름이지만 여러 유형의 값이 집계 된 클래스가있는 경우도 있습니다. 예를 들어, 속성과 요소가 모두 포함 된 개체가 있다고 가정합니다. 두 개의 스트림 리턴 API를 제공 할 수 있습니다.

Stream<Attribute>  attributes();
Stream<Element>    elements();


답변

스트림은 항상 같은 식 안에서 “종료”되도록 설계 되었습니까?

이것이 대부분의 예에서 사용되는 방식입니다.

참고 : 스트림 반환은 반복자 반환과 크게 다르지 않습니다 (표현 능력이 훨씬 뛰어남).

IMHO 최고의 솔루션은 왜 이런 일을하는지 캡슐화하고 컬렉션을 반환하지 않는 것입니다.

예 :

public int playerCount();
public Player player(int n);

또는 당신이 그들을 계산하려는 경우

public int countPlayersWho(Predicate<? super Player> test);


답변

스트림이 유한하고 반환 된 객체에 예상 예외가 발생하여 확인 된 예외가 발생하면 항상 Collection을 반환합니다. 점검 예외를 발생시킬 수있는 각 개체에 대해 무언가를 수행하려는 경우 스트림을 싫어하게됩니다. 스트림에 대한 하나의 실제 부족으로 확인 된 예외를 우아하게 처리 할 수 ​​없습니다.

아마도 그것은 아마도 당신이 확인 된 예외가 필요하지 않다는 신호 일 것입니다.


답변

컬렉션과 달리 스트림에는 추가 특성이 있습니다. 어떤 방법 으로든 반환 된 스트림은 다음과 같습니다.

  • 유한 또는 무한
  • 병렬 또는 순차 (애플리케이션의 다른 부분에 영향을 줄 수있는 기본 전역 공유 스레드 풀 사용)
  • 주문 또는 비 주문

이러한 차이점은 컬렉션에도 존재하지만 명백한 계약의 일부입니다.

  • 모든 컬렉션에는 크기가 있으며 Iterator / Iterable은 무한 할 수 있습니다.
  • 컬렉션은 명시 적으로 주문되었거나 순서가 없습니다
  • 고맙게도 병렬 처리는 스레드 안전성을 넘어서는 컬렉션에 신경 쓰지 않습니다.

스트림의 소비자로서 (메소드 리턴 또는 메소드 매개 변수로) 이것은 위험하고 혼란스러운 상황입니다. 알고리즘이 올바르게 작동하도록하려면 스트림 소비자는 알고리즘이 스트림 특성에 대해 잘못된 가정을하지 않도록해야합니다. 그리고 그것은 매우 어려운 일입니다. 단위 테스트에서 이는 동일한 스트림 내용으로 스트림을 반복하기 위해 모든 테스트를 곱해야한다는 의미입니다.

  • (유한, 순서, 순차)
  • (유한, 순서, 병렬)
  • (정확하고 비 순차적이며 순차적 임) …

입력 스트림에 알고리즘을 깨뜨리는 특성이있는 경우 속성이 숨겨져있어 IllegalArgumentException을 발생시키는 스트림대한 메소드 가드 쓰기

따라서 위의 문제가 전혀 발생하지 않는 경우 메소드 서명에서 Stream 만 올바른 선택으로 남습니다.

명시 적 계약 (암시 적 스레드 풀 처리없이)을 사용하여 메소드 시그니처에 다른 데이터 유형을 사용하는 것이 훨씬 안전하므로 순서, 크기 또는 병렬성 (및 스레드 풀 사용)에 대한 잘못된 가정으로 데이터를 실수로 처리 할 수 ​​없습니다.


답변

나는 그것이 당신의 시나리오에 달려 있다고 생각합니다. Team구현할 경우 Iterable<Player>충분할 수 있습니다.

for (Player player : team) {
    System.out.println(player);
}

또는 기능적 스타일 :

team.forEach(System.out::println);

그러나보다 완벽하고 유창한 API를 원한다면 스트림이 좋은 솔루션이 될 수 있습니다.


답변

더 유명한 응답자들 중 일부는 훌륭한 조언을했지만, 아무도 언급하지 않은 것에 놀랐습니다.

이미 “구체화 된” Collection핸드가있는 경우 (예 : 멤버 필드 인 주어진 예제의 경우와 같이 호출 전에 이미 작성된 경우)이를로 변환 할 필요가 없습니다 Stream. 발신자는 스스로 쉽게 할 수 있습니다. 발신자가 데이터를 원래 형태로 사용하려면 데이터를 Stream원래 구조의 사본을 다시 구체화하기 위해 중복 작업을 수행 하도록 강제 변환합니다 .