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


163

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

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에서 스트림을 비이 디오 틱으로 반환합니까? 스트림은 생성 된 것과 동일한 표현 내에서 항상 "종료"되도록 설계 되었습니까?


12
관용구로서 이것에는 아무런 문제가 없습니다. 결국 players.stream(), 호출자에게 스트림을 리턴하는 메소드 일뿐입니다. 실제 질문은 호출자를 단일 순회로 제한하고 CollectionAPI 를 통해 컬렉션에 대한 액세스를 거부하고 싶습니까? 발신자 addAll가 다른 컬렉션으로 전달하고 싶습니까?
Marko Topolnik

2
모든 것이 다릅니다. Stream.collect ()뿐만 아니라 collection.stream ()도 항상 수행 할 수 있습니다. 따라서 사용자와 해당 기능을 사용하는 발신자에게 달려 있습니다.
Raja Anbazhagan

답변:


222

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

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

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

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

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

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

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

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

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

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

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

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


6
내가 말했듯이, 움직이는 대상의 시간에 스냅 샷을 반환하려는 경우, 특히 강한 일관성 요구 사항이있는 경우와 같이 비행하지 않는 경우가 있습니다. 그러나 대부분의 경우 스트림 사용 방법에 대한 특정 정보가 없으면 Stream이 더 일반적인 선택 인 것 같습니다.
Brian Goetz

8
@Marko 당신이 당신의 질문을 너무 좁게 제한하더라도, 나는 여전히 당신의 결론에 동의하지 않습니다. 아마도 스트림을 만드는 것이 불변 래퍼로 컬렉션을 포장하는 것보다 훨씬 비싸다고 가정하고 있습니까? (그렇지 않으면 래퍼에서 얻는 스트림 뷰가 원본에서 가져온 것보다 나쁩니다. UnmodifiableList는 spliterator ()를 재정의하지 않기 때문에 모든 병렬 처리를 효과적으로 잃게됩니다.) 결론 :주의하십시오. 친숙한 편견; 당신은 수년간 Collection을 알고 있었고, 그로 인해 새로 온 사람을 불신하게 만들 수 있습니다.
Brian Goetz

5
@MarkoTopolnik은 확실합니다. 내 목표는 일반적인 API 디자인 문제를 해결하는 것이 었으며 FAQ가되었습니다. 비용과 관련하여 이미 구체화 된 컬렉션 이없는 경우 반환하거나 줄 바꿈 할 수 있지만 (OP는 있지만 종종없는 경우) getter 메서드에서 컬렉션을 구체화하는 것은 스트림을 반환하고 보내는 것보다 저렴하지 않습니다. 호출자는 하나를 구체화합니다 (물론 호출자가 필요하지 않거나 ArrayList를 반환하지만 호출자가 TreeSet을 원하면 초기 구체화는 훨씬 비쌉니다.) 그러나 Stream은 새롭고 사람들은 종종 $ $ 이상을 가정합니다 그것은.
Brian Goetz

4
@MarkoTopolnik 인 메모리는 매우 중요한 사용 사례이지만 비 순차적으로 생성 된 스트림 (예 : Stream.generate)과 같이 병렬 처리를 지원하는 다른 경우도 있습니다. 그러나 스트림이 적합하지 않은 경우 데이터가 임의 지연 시간으로 도착하는 사후 사용 사례입니다. 이를 위해 RxJava를 제안합니다.
Brian Goetz

4
@MarkoTopolnik 나는 우리가 우리의 노력을 약간 다르게 초점을 맞추기를 좋아했을 수도 있다는 점을 제외하고는 동의하지 않는다고 생각합니다. (우리는 이에 익숙합니다. 모든 사람을 행복하게 만들 수는 없습니다.) Streams의 디자인 센터는 인 메모리 데이터 구조에 중점을 둡니다. RxJava의 디자인 센터는 외부에서 생성 된 이벤트에 중점을 둡니다. 둘 다 좋은 라이브러리입니다. 또한 디자인 센터를 벗어난 케이스에 적용하려고 할 때 둘 다 잘 지내지 않습니다. 그러나 망치가 바늘 끝에 끔찍한 도구이기 때문에 망치에 문제가 있음을 암시하지는 않습니다.
Brian Goetz

63

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();

3
좋은 지적. 명명 관용어가 어디에서 발생하는지, 그리고 얼마나 많은 견인력 (증기?)이 발생하는지에 대해 더 말할 수 있습니까? 나는 네이밍 컨벤션의 아이디어를 좋아한다. 당신이 스트림과 컬렉션을 얻는다는 것을 분명히한다. 그러나 나는 종종 "get"에서 IDE 완성이 내가 얻을 수있는 것을 말해 줄 것을 기대한다.
Joshua Goldberg

1
나는 또한 그 명명 관용구에 매우 관심이 있습니다
선출

5
@JoshuaGoldberg JDK는이 명명 관용구를 독점적으로 채택하지는 않았지만 채택한 것 같습니다. 다음을 고려하십시오. CharSequence.chars () 및 .codePoints (), BufferedReader.lines () 및 Files.lines ()는 Java 8에 존재합니다. Java 9에는 다음이 추가되었습니다. Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). 이 관용구를 사용하지 않는 다른 스트림 반환 방법 (Scanner.findAll ()이 떠오름)이 추가되었지만 복수 명사 관용구가 JDK에서 공정하게 사용 된 것 같습니다.
스튜어트 마크

1

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

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

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

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

예 :

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

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

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

2
이 답변의 문제점은 저자가 클라이언트가 원하는 모든 행동을 예상해야하며 클래스의 메소드 수를 크게 늘리는 것입니다.
dkatzel

@dkatzel 그것은 최종 사용자가 저자인지 또는 그들이 작업하는 사람인지에 달려 있습니다. 최종 사용자를 알 수 없으면보다 일반적인 솔루션이 필요합니다. 기본 컬렉션에 대한 액세스를 계속 제한 할 수 있습니다.
Peter Lawrey

1

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

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


1

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

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

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

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

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

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

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

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

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


2
무한한 흐름에 대한 여러분의 관심은 근거가 없습니다. 문제는 "컬렉션이나 스트림을 반환해야한다"입니다. 컬렉션 가능성이 경우, 결과는 정의에 의해 유한. 따라서 컬렉션을 반환 할 수 있다고 가정하면 호출자가 무한 반복을 위험에 빠뜨릴 염려가 없습니다 . 이 답변의 나머지 조언은 단지 나쁩니다. 스트림을 과도하게 사용하는 사람을 만나고 다른 방향으로 과도하게 회전하는 것처럼 들립니다. 이해할 만하지 만 나쁜 조언.
Brian Goetz

0

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

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

또는 기능적 스타일 :

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

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


OP가 게시 한 코드에서 플레이어 수는 추정치 ( '1034 명의 플레이어가 지금 재생하려면 여기를 클릭하십시오!')를 제외하고는 거의 쓸모가 없습니다. 이는 변경 가능한 컬렉션에 대한 불변의 뷰를 반환하기 때문입니다. 이므로 지금 얻는 카운트가 3 마이크로 초와 같지 않을 수 있습니다. 따라서 Collection을 반환하면 카운트에 도달하는 "쉬운"방법을 제공하지만 (실제로 stream.count()는 매우 쉽습니다),이 숫자는 디버깅이나 추정 이외의 다른 용도로는 실제로 의미가 없습니다.
브라이언 괴츠

0

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

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


-1

아마도 스트림 팩토리가 더 나은 선택 일 것입니다. Stream을 통해 컬렉션을 공개하는 것의 가장 큰 장점은 도메인 모델의 데이터 구조를 더 잘 캡슐화한다는 것입니다. 도메인 클래스를 사용하여 단순히 스트림을 노출하여 목록 또는 세트의 내부 작업에 영향을 미치는 것은 불가능합니다.

또한 도메인 클래스 사용자가 최신 Java 8 스타일로 코드를 작성하도록 권장합니다. 기존 게터를 유지하고 새로운 스트림 리턴 게터를 추가하여이 스타일로 점진적으로 리팩터링 할 수 있습니다. 시간이 지남에 따라 목록 또는 집합을 반환하는 모든 게터를 마침내 삭제할 때까지 레거시 코드를 다시 작성할 수 있습니다. 이런 종류의 리팩토링은 모든 레거시 코드를 지우면 정말 기분이 좋습니다!


7
이것이 완전히 인용 된 이유가 있습니까? 소스가 있습니까?
Xerus

-5

아마 두 가지 방법이 있습니다. 하나는 a를 반환 Collection하고 다른 하나는 컬렉션을 a로 반환합니다 Stream.

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

// ...

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

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

}

이것은 두 세계의 최고입니다. 클라이언트는 List 또는 Stream을 원하는지 선택할 수 있으며 Stream을 얻기 위해 불변의 목록 복사본을 만들기 위해 추가 개체를 만들 필요가 없습니다.

또한 API에 메소드를 하나만 추가하므로 메소드가 너무 많지 않습니다.


1
그는이 두 가지 옵션 중 하나를 선택하고 각각의 장단점을 물었습니다. 또한 모든 사람들에게 이러한 개념을 더 잘 이해할 수 있습니다.
Libert Piou Piou

그렇게하지 마십시오. API를 상상해보십시오!
François Gautier
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.