[Java 8] Stream API 면접 질문과 답변

Java 면접을 준비하고 계신다면 이 글이 가장 유용할 것입니다. Java 버전의 지속적인 업그레이드 이후 면접 질문도 증가했기 때문입니다. 여기에서는 가장 자주 묻는 Java 8 Stream API 면접 질문과 답변을 정리했습니다.

Stream API

Stream API는 면접에서 면접관이 가장 많이 선택하는 주제 중 하나입니다. 가장 많이 사용되는 Stream API 질문의 개념을 알아보세요. Java 8의 Stream API에 대한 몇 가지 질문과 답변을 살펴보겠습니다.

1. Stream API란 무엇인가요?

  • Java 8은 java.util.stream이라는 새로운 추가 패키지를 제공합니다. 이 패키지는 클래스, 인터페이스, 열거형 등으로 구성되어 요소에 대한 함수형 연산을 허용합니다.
  • java.util.stream 패키지를 사용하여 Stream API를 사용할 수 있습니다.
  • Stream을 사용하여 한 데이터 구조에서 다른 데이터 구조로 filter, collect, print 및 변환할 수 있습니다.
  • Stream API는 요소를 저장하지 않습니다. Stream을 사용하여 수행한 작업은 소스를 수정하지 않습니다.

2. Collection과 Stream의 차이점은 무엇인가요?

  • Collection과 Stream의 주요 차이점은 Collection에는 요소가 포함되어 있지만 Stream에는 요소가 포함되어 있지 않다는 것입니다.
  • Stream은 요소가 Collection 또는 배열로 저장되는 뷰에서 작동하지만 다른 뷰와 달리 Stream에 대한 변경 사항은 원래 Collection에 반영되지 않습니다.

3. Stream API의 중간 연산(Intermediate operation)이란 무엇인가요?

  • 중간 연산은 Stream을 출력으로 반환하고 Stream에서 터미널 연산이 호출될 때까지 중간 연산이 실행되지 않습니다. 이를 지연 평가라고 합니다.
  • Stream API의 중간 연산은 현재 데이터를 처리한 다음 새 Stream을 반환합니다.
  • 중간 연산이 실행되면 새 Stream만 생성됩니다.

예: map(), limit(), filter(), skip(), flatMap(), sorted(), distinct(), peek()

4. Stream API에서 터미널 연산(Terminal operation)이란 무엇인가요?

  • 이름에서 알 수 있듯이 터미널은 Stream 파이프라인의 마지막 작업을 의미합니다. 터미널 연산은 Stream을 통과하여 결과 또는 Collection을 생성하지만 새 Stream은 생성하지 않습니다.
  • 터미널 연산은 모든 중간 연산이 적용된 후 Stream의 결과를 생성하며, 터미널 연산이 수행되면 더 이상 Stream을 사용할 수 없습니다.
  • Stream 파이프라인은 소스(Collection, array, function 또는 I/O channel)로 구성되며, 파이프라인에서 중간 작업을 호출하고 마지막으로 터미널 작업이 수행되어 Stream 파이프라인이 소비되고 닫힌 것으로 표시됩니다.
  • 파이프라인의 끝에는 하나의 터미널 작업만 있을 수 있습니다. 닫힌 Stream에서 작업을 수행하면 Stream이 이미 작업되었거나 닫혔기 때문에 java.lang.IllegalStateException이 발생합니다.

예:

  • collect()
  • forEach()
  • forEachOrdered()
  • findAny()
  • findFirst()
  • toArray()
  • reduce()
  • count()
  • min()
  • max()
  • anyMatch()
  • allMatch()
  • noneMatch()

5. map() 함수는 어떤 기능을 하나요? 왜 사용하나요?

  • map() 함수는 자바에서 map 함수 연산을 수행합니다. 즉, 한 유형의 객체를 다른 유형으로 변환할 수 있습니다.
  • 문자열 목록이 있고 정수 목록을 변환하고 싶다면, map() 함수를 사용하여 문자열을 정수로 변환합니다. 예를 들어, parseInt()map()에 적용하면 목록의 모든 요소에 해당 변환을 적용하여 정수 목록을 반환합니다.
1
2
3
List<Integer> list = pattern.splitAsStream(ints)
.map(Integer::valueOf)
.collect(Collectors.toList());

6. filter() 메서드는 어떤 기능을 하나요? 언제 사용하나요?

  • filter()는 조건부 함수를 사용하여 지정한 특정 조건을 만족하는 요소를 필터링하는 데 사용됩니다.
  • predicate 함수는 객체를 받아 boolean을 반환하는 함수에 불과합니다.
  • 예를 들어, 정수 목록이 있고 짝수 정수 목록을 원한다고 가정해 보겠습니다.
1
2
3
4
List<Integer> list = Arrays.asList(10, 15, 8, 49, 25, 98, 32);
list.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);

7. flatMap() 메서드는 어떤 기능을 하나요?

  • flatMap()는 map 함수의 확장입니다. 한 객체를 다른 객체로 전송하는 것 외에도 객체를 평평하게 만듭니다.
  • 예: 목록 데이터의 목록이 있고 목록의 모든 요소를 하나의 목록으로 결합하고 싶다고 가정해 보겠습니다. 이 경우 flatMap()을 사용할 수 있습니다.
1
2
3
4
5
6
7
8
9
List<Integer> evens = Arrays.asList(2, 4, 6);
List<Integer> odds = Arrays.asList(3, 5, 7);
List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11);
List<Integer> numbers = Stream.of(evens, odds, primes)
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println("flattend list: " + numbers) ;

// Output: flattend list: [2, 4, 6, 3, 5, 7, 2, 3, 5, 7, 11]

8. Java Stream에서 predicate Interface란 무엇인가요?

  • Predicate는 객체를 받아서 boolean 값을 반환하는 함수를 나타내는 함수형 인터페이스입니다.
  • 이는 원하지 않는 요소를 필터링하기 위해 Predicate를 사용하는 filter()와 같은 여러 Stream 함수에서 사용됩니다.

9. Stream API에서 peek() 메서드는 어떤 기능을 하나요?

  • Stream 클래스의 peek() 메서드를 사용하면 Stream 파이프라인을 들여다볼 수 있습니다.
  • peek()는 각 요소에 대해 지정된 작업을 수행한 후 Stream의 요소로 구성된 Stream을 반환합니다. 이 함수는 각 중간 작업 후에 값을 인쇄할 때 유용합니다.
  • 각 단계를 들여다보고 콘솔에 의미 있는 메시지를 인쇄할 수 있습니다. 일반적으로 람다 표현식 및 Stream 처리와 관련된 문제를 디버깅하는 데 사용됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

List<Integer> ans = list.stream()
.filter(value -> value % 2 == 0)
.peek(value -> System.out.println("Filtered " + value))
.map(value -> value * 10)
.collect(Collectors.toList());

System.out.println(Arrays.toString(ans.toArray()));

// Output:
Filtered 2
Filtered 4
[20, 40]

10. Stream API의 map()과 flatMap()의 차이점은 무엇인가요?

Java 8에서 map()flatMap()의 주요 차이점은 다음과 같습니다.

  • map() 연산에 전달하는 함수는 단일 값을 반환합니다.
  • flatMap() 연산에 전달하는 함수는 값의 Stream을 반환합니다.
  • flatMap()은 map과 flat 연산의 조합입니다.
  • map()은 변환에만 사용되지만 flatMap()은 변환과 평탄화 모두에 사용됩니다.

map() 예시

1
2
3
4
5
List<String> names = Arrays.asList("Saket", "Trevor", "Franklin", "Michael");
List<String> upperCase = names.stream()
.map(String::toUpperCase)
.collet(Collectors.toList());
upperCase.forEach(System.out::println);

flatMap() 예시

1
2
3
4
5
6
7
8
List<List<String>> names = Arrays.asList(Arrays.asList("Saket", "Trevor"),
Arrays.asList("Shawn", "Franklin"),
Arrays.asList("johnty", "Sean"));
List<String> start = names.stream()
.flatMap(firstName -> firstName.stream())
.filter(s -> s.startsWith("s"))
.collet(Collectors.toList());
start.forEach(System.out::println);

11. 배열을 Stream으로 변환할 수 있나요?

  • 예, Java를 사용하여 배열을 Stream으로 변환할 수 있습니다.
  • Stream 클래스는 가변 매개변수를 허용하는 Stream.of(T...)와 같이 배열에서 Stream을 만드는 팩토리 함수를 제공하며, 배열을 제공할 수도 있습니다.
1
2
3
String[] languages = {"Java", "Python", "JavaScript"};
Stream stream = Stream.of(languages);
stream.forEach(System.out::println);

12. Stream API의 Stateful 및 Stateless 중간 작업이란 무엇인가요?

  • Stateful 연산은 skip(), distinct(), limit(), sorted()입니다. 나머지 모든 Stream 연산은 stateless입니다.
  • 연산이 현재 요소를 처리하기 위해 지금까지 처리한 요소의 정보를 유지해야 하는 경우, 이는 stateful 연산입니다.
  • 예시: Distinct 연산은 지금까지 처리한 모든 값을 추적해야 하며, 이 정보를 기반으로 현재 값이 고유한 값인지 또는 이전에 처리된 적이 있는 값인지를 판단하여 현재 값을 새 Stream(Distinct 연산 출력)에 추가하거나 값을 무시하고 새 Stream에 추가하지 않을 수 있습니다.

13. Stream의 findFirst()와 findAny()의 차이점은 무엇인가요?

  • 이름에서 알 수 있듯이 findFirst() 함수는 Stream에서 첫 번째 요소를 찾는 데 사용되는 반면, findAny() 함수는 Stream에서 모든 요소를 찾는 데 사용됩니다.
  • findFirst()pre-deterministic이지만 findAny()non-deterministic입니다. 프로그래밍에서 결정적(deterministic)이란 출력이 시스템의 입력 또는 초기 상태에 기반한다는 것을 의미합니다.

14. Parallel Stream이란 무엇인가요? 목록을 Parallel Stream으로 변환하는 방법은 무엇인가요?

  • Java 8의 눈에 띄는 기능 중 하나는 Java Parallel Stream입니다. 이는 프로세서의 다양한 코어를 활용하기 위한 것입니다.
  • 기본적으로 모든 Stream 작업은 명시적으로 병렬로 지정되지 않는 한 Java에서 순차적으로 이루어집니다. Java에서 Parallel Stream은 두 가지 방법으로 생성됩니다.
  1. 순차 Stream에서 parallel() 메서드를 호출합니다.
  2. Collection에서 parallelStream() 메서드를 호출합니다.
  • Parallel Stream은 프로그램 실행 시간을 최소화하기 위해 동시에 처리할 수 있는 독립적인 작업이 많을 때 유용합니다.
  • 모든 Java 코드에는 일반적으로 순차적으로 실행되는 하나의 처리 Stream만 있습니다. 하지만 Parallel Stream을 사용하면 Java 코드를 두 개 이상의 스트림으로 분리하여 각각의 코어에서 병렬로 실행할 수 있으며, 그 결과는 개별 결과의 조합으로 나타납니다.
  • 이러한 Stream이 실행되는 순서는 당사가 통제할 수 없습니다. 따라서 개별 항목의 실행 순서가 최종 결과에 영향을 미치지 않는 경우에는 Parallel Stream을 사용하는 것이 좋습니다.

parallel(): 기존 순차 Stream에서 parallel() 메서드를 호출하여 병렬 스트림으로 만듭니다.

1
2
3
4
5
6
7
8
9
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
stream.parallel().forEach(System.out::println);

Output:
3
5
4
1
2

parallelStream(): List, Set 등과 같은 Java Collection에서 parallelStream()을 호출하여 병렬 스트림으로 만듭니다.

1
2
3
4
5
6
7
8
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
list.parallelStream().forEach(System.out::println);

Output:
3
1
5
4

Java Stream에 대해 기억해야 할 중요한 사항

  1. Stream은 데이터 구조가 아니라 연산 파이프라인에서 처리할 수 있는 요소의 시퀀스입니다.
  2. Stream은 중간 연산과 터미널 연산이라는 두 가지 유형의 연산을 지원합니다. 중간 연산은 Stream을 변환하거나 필터링하는 반면, 터미널 연산은 결과 또는 부작용을 생성합니다.
  3. Stream은 지연 처리되도록 설계되었기 때문에 터미널 연산이 실행될 때 요소들이 on-demand 방식으로 처리됩니다.
  4. 적절한 리소스 관리를 위해 close() 메서드를 사용하거나 try-with-resources를 활용하여 I/O 채널 또는 리소스에서 열린 Stream을 닫는 것이 중요합니다.
  5. Java Stream이 모든 시나리오에 적합한 것은 아닙니다. 경우에 따라서는 루프를 사용하는 전통적인 반복이 더 적절하고 효율적일 수 있습니다.
Share