# 함수형 프로그래밍
## 자바 함수형 프로그래밍의 개요
함수형 프로그래밍은 프로그램의 로직을 수학적 함수의 계산으로 간주하고 상태 및 변경 가능한 데이터를 피하는 프로그래밍 패러다임입니다. 이는 코드를 더 간결하게 만들고, 병렬 처리와 테스트에 유리합니다.
- 함수형 자바 코드 작성에 필요한 지식 : 람다, 스트림 API, Optional, 함수형 인터페이스 등
### 함수형 프로그래밍의 핵심 개념
함수형 프로그래밍의 핵심 개념은 일급 객체 함수, 순수 함수, 고차 함수가 있습니다.
1. **일급 객체 함수**: 함수를 다른 함수의 인자로 전달하거나, 함수의 결과로서 반환하거나, 변수에 저장하거나 데이터 구조안에 저장할 수 있음을 의미합니다. 일급 함수를 지원하는 언어에서는 함수를 값처럼 자유롭게 사용할 수 있습니다.
2. **순수 함수**: 순수 함수는 같은 인자에 대해 항상 같은 결과를 반환하며, 그 외 어떤 상태도 변경하지 않는 함수를 의미합니다. 즉, 순수 함수는 어떤 외부 상태에도 의존하지 않고, 외부 상태를 변경하지 않습니다. 이로 인해 순수 함수는 입력만을 기반으로 동작하기 때문에 테스트와 디버깅이 용이하며, 병렬 처리에 안전합니다.
3. **고차 함수**: 고차 함수는 다른 함수를 인자로 받거나, 함수를 결과로 반환하는 함수를 말합니다. 이는 함수의 재사용성과 로직의 추상화 레벨을 높여 코드의 간결성을 높이고, 코드의 가독성을 향상시킵니다.
### 순수 함수형 프로그래밍 규칙
1. 상태없음
- 순수 함수는 어떠한 상태도 변경하지 않습니다. 이는 전역 변수나 인스턴스 변수, 정적 변수 등 어떠한 외부 상태도 변경하지 않음을 의미합니다. 이를 통해 함수의 결과가 외부 상태에 의존하지 않으므로, 동일한 입력값에 대해 항상 동일한 결과를 보장할 수 있습니다.
2. 부수 효과 없음
- 순수 함수는 부수 효과가 없습니다. 즉, 함수가 시스템 상태를 변경하거나 함수 외부에 영향을 주는 작업을 수행하지 않습니다. 예를 들어, 파일에 쓰는 작업, 화면에 출력하는 작업, 네트워크 요청 등은 모두 부수 효과에 해당합니다.
3. 불변변수
- 순수 함수의 결과는 입력값에 의해서만 결정됩니다. 동일한 입력값에 대해 항상 동일한 결과가 반환되어야 합니다. 이는 함수의 결과가 어떠한 외부 상태에도 의존하지 않음을 의미합니다.
4. 반복보다 재귀 선호
- 이 원칙은 순수 함수형 프로그래밍에서 상태를 변경하지 않는 계산을 선호하는 데 기인합니다. 반복문은 일반적으로 반복을 수행하면서 상태를 변경하고, 이 상태를 통해 반복을 제어합니다. 예를 들어, for문이나 while문에서는 보통 카운터 변수를 사용하여 반복 횟수를 제어하며, 이는 상태 변경을 의미합니다. 반면에 재귀는 상태 변경 없이 연산을 수행합니다. 재귀 함수는 자기 자신을 호출하여 문제를 해결하며, 이때 각각의 함수 호출은 독립적인 스택 프레임에 저장됩니다. 이는 각 함수 호출이 독립적인 상태를 가지며, 다른 함수 호출의 상태에 영향을 주지 않음을 의미합니다. 따라서 순수 함수형 프로그래밍에서는 상태 변경을 최소화하고, 함수의 순수성을 유지하기 위해 반복보다 재귀를 선호합니다. 하지만 실제 프로그래밍에서는 재귀의 과도한 사용이 스택 오버플로우를 일으키는 등의 문제를 야기할 수 있으므로, 적절한 균형이 필요합니다. 이런 문제를 해결하기 위해 꼬리 재귀 최적화(tail recursion optimization)와 같은 기법이 사용되기도 합니다.
## 기술인터뷰
### 스트림이란?
자바에서 스트림(Stream)은 '데이터의 흐름'을 의미합니다. 여러 데이터를 하나씩 차례대로 가져와서 연산을 수행하는 기능을 가진 것이 바로 스트림입니다.
스트림은 데이터 소스를 변경하지 않는다는 특징이 있습니다. 예를 들어, 리스트에서 스트림을 생성하면 원본 리스트는 그대로 유지되고, 스트림에서 연산을 수행한 결과는 새로운 리스트에 저장됩니다. 즉, 스트림은 '원본 데이터에 영향을 주지 않는 상태에서 효율적으로 연산을 수행'하도록 도와줍니다.
스트림은 '중간 연산'과 '최종 연산'으로 구분됩니다. 중간 연산은 스트림을 반환하므로 연산을 연결하여 사용할 수 있습니다. 이를 '스트림 파이프라인'이라고 합니다. 중간 연산의 예로는 `filter`, `map`, `flatMap` 등이 있습니다. 최종 연산은 스트림을 소비하여 결과를 만들어냅니다. 최종 연산의 예로는 `collect`, `count`, `sum` 등이 있습니다.
**이해가 되지 않는다면**, 스트림을 상상하실 때는 마치 작은 시냇물이 졸졸 흐르는 것을 떠올려보세요. 이 시냇물에는 여러 가지 작은 돌이나 나뭇잎 등이 떠있는데, 이것들이 바로 데이터라고 생각하시면 됩니다.
자바에서 스트림은 이런 데이터들을 하나씩 가져와서 처리하는 역할을 합니다. 이 때, 중요한 점은 스트림이 데이터를 가져와 처리할 때 원본 데이터(시냇물에 떠있는 돌이나 나뭇잎)를 손상시키지 않는다는 것입니다. 즉, 데이터를 가져와 처리하더라도 원본 데이터는 그대로 유지되는 거죠.
그리고 스트림은 여러 가지 연산을 할 수 있습니다. 예를 들어, 모든 돌을 2배 크기로 만들거나, 나뭇잎만 골라낼 수 있습니다. 이런 연산들을 스트림에서는 '중간 연산'이라고 부릅니다.
마지막으로, 스트림에서는 '최종 연산'을 통해 최종 결과를 만들어냅니다. 예를 들어, 시냇물에서 가장 큰 돌을 골라내는 것이 최종 연산이 될 수 있겠죠.
아래에 코드 예제를 준비해보았습니다.
우선, 리스트에 있는 숫자들의 스트림을 만들어 보겠습니다.
```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> numberStream = numbers.stream();
```
여기서 `numbers.stream()`이 바로 숫자 리스트에서 스트림을 만드는 부분입니다.
이제 이 스트림에서 중간 연산을 수행해 볼까요? 중간 연산으로는 각 숫자를 2배로 만드는 `map` 연산을 수행해보겠습니다.
```java
Stream<Integer> doubledStream = numberStream.map(n -> n * 2);
```
이제 `doubledStream`에는 원래 숫자들이 2배로 된 값들이 들어있습니다. 이 때, 원래의 `numbers` 리스트는 변하지 않습니다.
마지막으로, 최종 연산을 수행해보겠습니다. 최종 연산으로는 `sum`을 사용해 모든 숫자를 더해보겠습니다.
```java
int sum = doubledStream.reduce(0, (a, b) -> a + b);
```
이제 `sum`에는 `doubledStream`에 있는 모든 숫자의 합이 저장됩니다.
이렇게 스트림은 데이터의 흐름을 표현하며, 중간 연산과 최종 연산을 통해 데이터를 처리합니다.
### 람다식구성
람다식은 함수형 프로그래밍의 핵심 요소로, '이름이 없는 함수'라고 생각할 수 있습니다. 람다식은 매개변수 리스트, 화살표(`->`), 그리고 함수 본문으로 구성되며, 이를 통해 간결하면서도 명확한 코드를 작성할 수 있습니다. 람다식은 이름이 없기 때문에 한번 사용하고 버리거나, 메소드의 인자로 전달할 수 있습니다.
### 함수형 인터페이스
함수형 인터페이스는 자바 8부터 도입된 개념으로, 하나의 추상 메서드를 가진 인터페이스를 의미합니다. `Runnable`, `Callable`, `Comparator` 등이 대표적인 예시입니다. 이러한 인터페이스는 람다 표현식이나 메서드 참조를 통해 간편하게 구현체를 제공할 수 있습니다. 일반적으로 함수형 인터페이스는 @FunctionalInterface 이노테이션으로 표시합니다. 함수형 인터페이스가 아닌 인터페이스는 자바 람다 표현식으로 구현할 수 없습니다.
### 컬렉션과 스트림
컬렉션은 데이터를 저장하는 자료구조이며, 스트림은 이러한 데이터를 처리하는 연속된 연산을 표현하는 인터페이스입니다. 스트림은 컬렉션에서 데이터를 가져와 연산을 수행하고, 그 결과를 다시 컬렉션에 저장하거나 출력할 수 있습니다.
### map 함수
`map` 함수는 스트림의 각 요소에 주어진 함수를 적용하고, 그 결과로 구성된 새로운 스트림을 반환하는 함수입니다. 이 함수는 주로 스트림의 요소를 변환하거나 가공하는 데 사용됩니다.
예를 들어, 아래의 코드는 리스트의 각 요소를 제곱하여 새로운 리스트를 만드는 예제입니다.
```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
```
위의 코드에서 `map` 함수는 각 요소(여기서는 숫자)에 제곱 연산(`n -> n * n`)을 적용합니다. 그리고 그 결과로 구성된 새로운 스트림을 만들어 `collect`를 통해 리스트로 변환합니다.
따라서 `map` 함수의 주요 사용 목적은 스트림의 각 요소를 주어진 함수를 통해 변환하고, 그 결과로 새로운 스트림을 만드는 것입니다. 이를 통해 데이터를 원하는 형태로 가공하거나 변환하는 작업을 수행할 수 있습니다.
### flatMap 함수
`flatMap` 함수는 리스트 안에 있는 리스트를 하나로 합칠 때 주로 사용됩니다.
예를 들어, 여러분이 "각 사람이 가진 책 리스트"라는 리스트를 가지고 있다고 가정해봅시다. 이 경우 각 사람마다 책들의 리스트가 있기 때문에, 전체 구조는 '리스트의 리스트'가 됩니다.
```java
[
["해리포터", "지식의 씨앗"],
["해밀턴", "토지"],
["프로그래밍의 기술", "데이터 사이언스"]
]
```
이때, "모든 사람의 책을 하나의 리스트로 모으고 싶다"는 요구가 있다면 어떻게 해야할까요? 이럴 때 `flatMap` 함수를 사용하면 간단하게 해결할 수 있습니다. `flatMap` 함수는 이런 '리스트의 리스트'를 '하나의 리스트'로 만들어주는 역할을 합니다.
```java
[
"해리포터", "지식의 씨앗", "해밀턴", "토지", "프로그래밍의 기술", "데이터 사이언스"
]
```
이처럼 `flatMap` 함수는 중첩된 리스트를 하나의 리스트로 풀어주는 역할을 합니다.
위의 예시를 사용하여 `flatMap`을 사용하는 방법에 대해 설명드리겠습니다. 여기서는 자바의 Stream API를 사용하겠습니다.
먼저, 각 사람이 가진 책 리스트를 리스트의 리스트로 표현해 봅시다.
```java
List<List<String>> bookLists = Arrays.asList(
Arrays.asList("해리포터", "지식의 씨앗"),
Arrays.asList("해밀턴", "토지"),
Arrays.asList("프로그래밍의 기술", "데이터 사이언스")
);
```
이제 `flatMap`을 사용하여 이 리스트의 리스트를 하나의 리스트로 만들어 봅시다.
```java
List<String> allBooks = bookLists.stream()
.flatMap(books -> books.stream())
.collect(Collectors.toList());
```
위의 코드에서 `flatMap` 함수는 각 요소(여기서는 책 리스트)에 `stream()` 함수를 적용합니다. `stream()` 함수는 리스트를 스트림으로 변환하는 함수입니다. 그리고 `flatMap`은 이렇게 변환된 스트림들을 결합하여 하나의 스트림으로 만듭니다.
이렇게 만들어진 스트림은 `collect(Collectors.toList())`를 통해 다시 리스트로 변환됩니다. 이 결과, `allBooks` 리스트에는 모든 사람의 책이 하나의 리스트로 모아집니다.
이처럼 `flatMap` 함수를 사용하면 중첩된 리스트를 풀어서 하나의 리스트로 만들 수 있습니다. 이는 데이터를 가공하거나 변환할 때 매우 유용하게 사용됩니다.
### map과 flatMap 함수의 차이점
`map` 함수는 각 요소에 함수를 적용하고 그 결과를 새로운 스트림으로 만드는 반면, `flatMap` 함수는 각 요소에 함수를 적용한 후 그 결과를 평면화하여 하나의 스트림으로 만듭니다. 즉, `flatMap`함수는 스트림의 각 요소에 함수를 적용한 후 그 결과를 다시 스트림으로 변환하고, 이렇게 만들어진 여러 개의 스트림을 하나의 스트림으로 합치는 역할을 합니다.
즉, `flatMap` 함수는 함수를 적용한 결과가 리스트라면 그 리스트의 요소를 모두 하나의 스트림으로 합치는 것입니다.
예를 들어, 아래 코드는 `map`과 `flatMap`의 차이를 보여줍니다.
```java
List<String> words = Arrays.asList("Hello", "World");
List<String[]> mapped = words.stream()
.map(word -> word.split(""))
.collect(Collectors.toList());
List<String> flatMapped = words.stream()
.flatMap(word -> Arrays.stream(word.split("")))
.collect(Collectors.toList());
```
`map`을 사용한 경우, 결과는 "단어를 글자로 분리한 배열의 리스트"입니다. 반면에 `flatMap`을 사용한 경우, 결과는 "모든 단어의 글자를 모은 리스트"입니다.
즉, `map`은 변환 함수를 적용한 결과를 그대로 유지하지만, `flatMap`은 변환 함수를 적용한 후 그 결과를 평면화하여 하나의 스트림으로 만듭니다. 이런 차이 때문에 `map`과 `flatMap`은 서로 다른 상황에서 사용됩니다.
[**Easy Version**]
`map`과 `flatMap`의 차이는 '박스를 어떻게 다루느냐'에 있습니다. 여기서 '박스'는 리스트나 배열 같은 컨테이너를 의미합니다.
1. `map`은 각 박스에 있는 아이템을 변경하거나 업데이트하지만, 그 박스의 구조는 그대로 유지합니다. 예를 들어, 리스트의 각 요소를 2배로 만드는 것은 `map`을 사용할 수 있는 상황입니다.
```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubled = numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
```
위의 코드를 실행하면, `doubled`는 `[2, 4, 6, 8, 10]`이라는 새로운 리스트가 됩니다.
2. 반면, `flatMap`은 각 박스에 있는 아이템을 다른 박스로 바꿀 수 있습니다. 그리고 이렇게 만들어진 모든 박스를 하나로 합칩니다. 예를 들어, 리스트의 각 문자열을 글자 리스트로 바꾸는 것은 `flatMap`을 사용할 수 있는 상황입니다.
```java
List<String> words = Arrays.asList("Hello", "World");
List<String> letters = words.stream()
.flatMap(word -> word.chars().mapToObj(c -> String.valueOf((char)c)))
.collect(Collectors.toList());
```
위의 코드를 실행하면, `letters`는 `['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd']`라는 새로운 리스트가 됩니다.
즉, `map`은 '박스의 아이템을 바꾸는 역할'을 하고, `flatMap`은 '박스의 아이템을 다른 박스로 바꾸고, 그 박스를 모두 합치는 역할'을 합니다. 이런 차이 때문에 `map`과 `flatMap`은 각각 다른 상황에서 사용됩니다.
### filter 함수
`filter` 함수는 주어진 조건에 맞는 요소만을 선택하여 새로운 스트림을 생성합니다. 이를 통해 특정 조건을 만족하는 요소만을 골라내는 필터링 작업을 수행할 수 있습니다.
즉, 스트림의 요소 중에서 원하는 조건에 맞는 것만 골라내는 역할을 합니다.
예를 들어, 숫자 리스트에서 홀수만을 골라내고 싶다면 `filter` 함수를 사용하여 다음과 같이 작성할 수 있습니다.
```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> oddNumbers = numbers.stream().filter(n -> n % 2 != 0);
```
이 코드에서 `n -> n % 2 != 0`는 홀수를 의미하는 람다 표현식입니다. 이 람다 표현식의 조건에 맞는 요소만을 골라서 새로운 스트림 `oddNumbers`를 만듭니다.
따라서 `filter` 함수의 주요 사용 목적은 스트림에서 원하는 조건에 맞는 요소만을 골라내는 것입니다. 이를 통해 원하는 데이터만을 추려내어 처리할 수 있습니다.
### 중간 연산과 종료 연산
스트림에서는 연산을 중간 연산과 종료 연산으로 구분합니다. 중간 연산은 스트림을 반환하여 연산의 체이닝(chain)을 가능하게 합니다. 반면, 종료 연산은 스트림을 소비하고 최종 결과를 반환하거나 출력합니다. 이 두 연산의 주요 차이점은 다음과 같습니다.
1. 중간 연산: 이 연산은 스트림의 요소에 어떤 처리를 하는 역할을 합니다. 중간 연산의 특징은 그 결과가 또 다른 스트림이라는 점입니다. 따라서 중간 연산 후에 추가적인 연산을 계속해서 체이닝할 수 있습니다. 예를 들어, `filter`, `map`, `sorted` 등의 연산이 여기에 속합니다.
2. 최종 연산: 이 연산은 스트림의 처리를 마무리하고 최종 결과를 도출하는 역할을 합니다. 최종 연산 후에는 더 이상 연산을 체이닝할 수 없습니다. 예를 들어, `reduce`, `collect`, `forEach`, `sum`, `count` 등의 연산이 여기에 속합니다.
한 가지 더 중요한 차이점은 '중간 연산'은 '최종 연산'이 호출될 때까지 실제로 연산이 수행되지 않는다는 점입니다. 이를 '지연 연산'이라고 합니다. 즉, 중간 연산은 최종 연산을 만나기 전까지는 실제로 수행되지 않고, 최종 연산이 수행될 때 한꺼번에 처리됩니다.
### peek 함수
`peek` 함수는 자바의 스트림에서 사용할 수 있는 중간 연산 중 하나입니다.
`peek` 함수는 주로 디버깅 목적으로 사용되며, 스트림의 요소를 특정 방식으로 소비하면서도 원본 스트림을 그대로 유지할 수 있습니다.
`peek` 함수는 주어진 함수를 스트림의 각 요소에 적용하지만, 그 결과를 새로운 스트림으로 만들지 않습니다.
대신, 원본 스트림의 요소를 그대로 유지하면서 각 요소에 대해 주어진 함수를 실행합니다.
예를 들어, 아래의 코드는 각 요소를 출력하면서 원본 리스트를 그대로 유지합니다.
```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.peek(System.out::println)
.collect(Collectors.toList());
```
위 코드에서 `System.out::println`은 각 요소를 출력하는 함수입니다. `peek` 함수를 통해 이 함수를 스트림의 각 요소에 적용하면서도 원본 스트림을 그대로 유지합니다.
따라서 `peek` 함수의 주요 사용 목적은 스트림의 각 요소에 대해 어떤 처리를 하면서도 원본 스트림을 유지하고 싶을 때입니다. 이는 주로 디버깅이나 로깅 등의 용도로 사용됩니다.
### 지연 스트림
"스트림 지연" 또는 "지연 연산"이란 표현은 자바 스트림에서 중간 연산이 즉시 실행되지 않고, 최종 연산이 호출될 때까지 연산이 '지연'되는 것을 의미합니다.
이는 스트림의 모든 데이터가 메모리에 한 번에 올라가지 않아도 되므로, 매우 큰 데이터 세트를 처리할 때 메모리 효율성을 높일 수 있습니다.
또한 필요한 데이터만 처리하므로 연산의 효율성 또한 높일 수 있습니다.
예를 들어, 아래와 같은 코드가 있다고 가정해봅시다.
```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.sum();
```
위 코드에서 `filter`와 `map`은 중간 연산이며, `sum`은 최종 연산입니다. 이 경우 `filter`와 `map`은 `sum`이 호출될 때까지 실제로 실행되지 않고 지연됩니다.
그리고 `sum`이 호출될 때 한꺼번에 `filter`와 `map` 연산이 수행됩니다.
즉, 스트림 지연은 스트림의 연산을 최적화하고 메모리 사용량을 줄이는 데 도움이 됩니다.
### 함수형 인터페이스와 일반 인터페이스
함수형 인터페이스는 하나의 추상 메서드를 가지는 인터페이스입니다. 이는 람다 표현식이나 메서드 참조를 통해 간편하게 구현체를 제공할 수 있습니다.
반면, 일반 인터페이스는 하나 이상의 메서드를 가질 수 있습니다.
### Supplier와 Consumer 인터페이스
`Supplier`와 `Consumer`는 자바의 함수형 인터페이스로, 주요 차이점은 다음과 같습니다:
1. `Supplier<T>`: 이 인터페이스는 어떤 입력 없이 값을 제공합니다. `Supplier` 인터페이스의 `get` 메서드는 매개변수 없이 결과를 반환합니다. 즉, `Supplier`는 입력을 받지 않지만 출력을 합니다.
예시:
```java
Supplier<String> stringSupplier = () -> "Hello, World!";
System.out.println(stringSupplier.get()); // "Hello, World!"
```
2. `Consumer<T>`: 이 인터페이스는 입력을 받아서 소비하며, 결과를 반환하지 않습니다. `Consumer` 인터페이스의 `accept` 메서드는 매개변수를 받아서 수행하지만 결과를 반환하지 않습니다. 즉, `Consumer`는 입력을 받지만 출력을 하지 않습니다.
예시:
```java
Consumer<String> stringConsumer = (s) -> System.out.println(s);
stringConsumer.accept("Hello, World!"); // "Hello, World!"
```
따라서 `Supplier`와 `Consumer`의 주요 차이점은 `Supplier`가 출력만 제공하고 `Consumer`가 입력만 소비한다는 점입니다.
### Predicate 인터페이스
`Predicate` 인터페이스는 자바의 함수형 인터페이스 중 하나로, 조건을 테스트하는 데 사용됩니다.
`Predicate` 인터페이스는 `test`라는 메서드를 가지며, 이 메서드는 어떤 조건을 만족하는지 (true) 만족하지 않는지 (false)를 판단하는 데 사용됩니다.
`Predicate` 인터페이스의 `test` 메서드는 하나의 입력 인자를 받아서 boolean 값을 반환합니다.
즉, `Predicate`는 입력을 받아서 조건에 따라 true 또는 false를 출력합니다.
예를 들어, 아래의 코드는 `Predicate` 인터페이스를 사용하여 숫자가 10보다 큰지를 테스트합니다.
```java
Predicate<Integer> isGreaterThanTen = (i) -> i > 10;
System.out.println(isGreaterThanTen.test(15)); // true
System.out.println(isGreaterThanTen.test(5)); // false
```
위 코드에서 `i > 10`은 `Predicate`의 `test` 메서드에 들어갈 조건입니다. 이 조건에 따라 `test` 메서드는 입력된 숫자가 10보다 큰지를 판단하여 그 결과를 반환합니다.
따라서 `Predicate` 인터페이스는 조건에 따라 boolean 값을 결정하는 데 사용됩니다.
`Predicate` 인터페이스는 다양한 상황에서 사용될 수 있습니다. 아래에 몇 가지 일반적인 사용 예시를 제공하겠습니다.
1. 컬렉션 필터링: `Predicate`는 컬렉션의 요소를 필터링하는 데 사용될 수 있습니다. 아래의 코드는 리스트에서 짝수만을 필터링하는 예시입니다.
```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Predicate<Integer> isEven = n -> n % 2 == 0;
List<Integer> evenNumbers = numbers.stream()
.filter(isEven)
.collect(Collectors.toList());
```
2. 문자열 검증: `Predicate`는 입력된 문자열이 특정 조건을 만족하는지 검증하는 데 사용될 수 있습니다. 아래의 코드는 문자열이 비어있는지를 확인하는 예시입니다.
```java
Predicate<String> isNotEmpty = s -> !s.isEmpty();
System.out.println(isNotEmpty.test("Hello")); // true
System.out.println(isNotEmpty.test("")); // false
```
3. 복합 조건: `Predicate` 인터페이스의 `and`, `or`, `negate` 메서드를 사용하여 여러 조건을 복합적으로 적용할 수 있습니다. 아래의 코드는 숫자가 10보다 크고 20보다 작은지를 판별하는 예시입니다.
```java
Predicate<Integer> greaterThanTen = n -> n > 10;
Predicate<Integer> lessThanTwenty = n -> n < 20;
Predicate<Integer> betweenTenAndTwenty = greaterThanTen.and(lessThanTwenty);
System.out.println(betweenTenAndTwenty.test(15)); // true
System.out.println(betweenTenAndTwenty.test(25)); // false
```
이러한 방식으로 `Predicate` 인터페이스는 다양한 상황에서 유용하게 사용될 수 있습니다.
### findFirst와 findAny 메서드
`findFirst`와 `findAny`는 모두 스트림에서 요소를 찾는 메서드입니다. 두 메서드의 주요 차이점은 병렬 스트림에서의 동작 방식입니다.
- `findFirst` 메서드는 스트림의 첫 번째 요소를 찾습니다. 이 메서드는 순차 스트림과 병렬 스트림에서 동일하게 동작하며, 항상 스트림의 첫 번째 요소를 반환합니다.
- `findAny` 메서드는 스트림의 어떤 요소라도 찾습니다. 이 메서드는 순차 스트림에서는 첫 번째 요소를 반환하지만, 병렬 스트림에서는 어떤 요소를 반환할지는 정해져 있지 않습니다. 즉, 병렬 스트림에서는 가장 먼저 처리가 완료된 요소를 반환합니다.
따라서, 순차 스트림에서는 `findFirst`와 `findAny`가 동일하게 동작하지만, 병렬 스트림에서는 `findAny`가 더 효율적일 수 있습니다. 왜냐하면 `findAny`는 스트림의 어떤 요소라도 반환할 수 있기 때문에, 첫 번째 요소의 처리를 기다릴 필요가 없기 때문입니다.
`findFirst`와 `findAny` 메서드를 사용하는 일반적인 예시를 들어보겠습니다.
1. `findFirst` 메서드 사용 예시: 리스트에서 첫 번째 짝수를 찾는 경우입니다.
```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Optional<Integer> firstEvenNumber = numbers.stream()
.filter(n -> n % 2 == 0)
.findFirst();
System.out.println(firstEvenNumber.get()); // 2
```
2. `findAny` 메서드 사용 예시: 리스트에서 임의의 짝수를 찾는 경우입니다.
```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
Optional<Integer> anyEvenNumber = numbers.stream()
.filter(n -> n % 2 == 0)
.findAny();
System.out.println(anyEvenNumber.get()); // 결과는 실행마다 달라질 수 있습니다.
```
위의 예시에서, `findFirst`는 리스트의 첫 번째 짝수인 2를 반환하며, `findAny`는 리스트의 임의의 짝수를 반환합니다.
### 배열을 스트림으로 변환
객체 배열을 스트림으로 변환하는 방법에는 여러 가지가 있지만, 가장 일반적이고 쉬운 방법 세 가지는 다음과 같습니다:
1. `Arrays.stream()` 메서드 사용하기:
`Arrays.stream()` 메서드를 사용하면 배열을 스트림으로 쉽게 변환할 수 있습니다.
```java
String[] array = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(array);
```
2. `Stream.of()` 메서드 사용하기:
`Stream.of()` 메서드는 가변 인자를 받아 스트림을 생성합니다. 따라서 배열을 이 메서드에 전달하면 스트림으로 변환할 수 있습니다.
```java
String[] array = {"a", "b", "c"};
Stream<String> stream = Stream.of(array);
```
3. 컬렉션의 `stream()` 메서드 사용하기:
배열을 리스트나 다른 컬렉션으로 변환한 후, 컬렉션의 `stream()` 메서드를 사용하여 스트림을 생성할 수 있습니다.
```java
String[] array = {"a", "b", "c"};
List<String> list = Arrays.asList(array);
Stream<String> stream = list.stream();
```
### 병렬 스트림
`parallelStream` 메서드를 사용하여 병렬 스트림을 생성할 수 있습니다. 이는 멀티 코어 환경에서 효율적인 연산을 수행할 수 있게 해줍니다.
### 메서드 참조
메서드 참조는 람다 식의 축약 형태로, 특정 메서드를 직접 참조하는 표현식입니다. 이를 통해 코드를 간결하게 만들 수 있습니다.
### default 메서드
`default` 메서드는 인터페이스에 메서드의 구현을 제공하는 기능입니다. 이를 통해 기존의 코드를 변경하지 않고도 인터페이스에 새로운 기능을 추가할 수 있습니다.
### Iterator와 Spliterator 인터페이스
`Iterator` 인터페이스는 요소의 순차적인 접근을 가능하게 합니다. `Spliterator` 인터페이스는 병렬 처리를 위해 요소를 분할하는 기능을 가집니다.
### Optional
`Optional` 클래스는 null 값을 안전하게 처리하기 위한 컨테이너입니다. 이를 통해 NullPointerException을 방지할 수 있습니다.
### String::valueOf
`String::valueOf` 메서드는 객체의 문자열 표현을 반환합니다. 이는 메서드 참조에서 자주 사용되며, 객체를 문자열로 변환하는 데 사용됩니다.