스트림 API

JUNG GYUWON·2023년 10월 14일
0

Java

목록 보기
17/18

스트림 API

자바에서는 많은 양의 데이터를 저장하기 위해서 배열이나 컬렉션을 사용하는데 그러면 여기 접근하기 위해서는 반복문과 iterator를 사용해야한다

코드는 길이가 너무 길고 가독성도 떨어지며, 코드의 재사용이 거의 불가능 하며 정형화된 처리 패턴이 없어서 불편함

이러한 문제점을 극복하기 위해서 Java SE 8부터 스트림(stream) API를 도입!

스트림 API

스트림 API는 데이터를 추상화하여 다루므로, 다양한 방식으로 저장된 데이터를 읽고 쓰기 위한 공통된 방법을 제공

따라서 배열이나 컬렉션, 파일에 저장된 데이터를 모두 같은 방법으로 다룰 수 있음

스트림 API의 특징:

1. 스트림은 외부 반복을 통해 작업하는 컬렉션과는 달리 내부 반복(internal iteration)을 통해 작업을 수행한다

2. 스트림은 재사용이 가능한 컬렉션과는 달리 단 한 번만 사용할 수 있다.

3. 스트림은 원본 데이터를 변경하지 않는다

4. 스트림의 연산은 필터-맵(filter-map) 기반의 API를 사용하여 지연(lazy) 연산을 통해 성능을 최적화한다

5. 스트림은 parallelStream() 메소드를 통한 손쉬운 병렬 처리를 지원한다

+) 4번 특징 부연 설명
스트림의 연산은 대부분 filter-map 연산인데 아래 코드와 같이 중간연사(filter, map)에서 실제 연산을 하는게 아니라 연산의 결과가 필요한 마지막에(.collect) 연산을 수행한다

List<String> names = Arrays.asList("John", "Anna", "Mike", "Kate");
List<String> filteredNames = names.stream()
    .filter(name -> name.length() <= 4)
    .map(name -> name.toUpperCase())
    .collect(Collectors.toList());

+) 5번 특징 부연 설명

List<String> names = Arrays.asList("John", "Anna", "Mike", "Kate");
List<String> filteredNames = names.parallelStream()
    .filter(name -> name.length() <= 4)
    .map(name -> name.toUpperCase())
    .collect(Collectors.toList());

위와 같이 멀티 코어 프로세서의 특징을 살려서 filter와 map 연산을 동시에 수행할 수도 있다


스트림 API의 동작 흐름

  1. 스트림의 생성

  2. 스트림의 중개 연산 (스트림의 변환)

  3. 스트림의 최종 연산 (스트림의 사용)


스트림의 생성

스트림 API는 아래와 같은 다양한 데이터 소스에서 사용할 수 있다

  1. 컬렉션
  2. 배열
  3. 가변 매개변수
  4. 지정된 범위의 연속된 정수
  5. 특정 타입의 난수들
  6. 람다 표현식
  7. 파일
  8. 빈 스트림

컬렉션

자바에서 제공하는 모든 컬렉션의 최고 상위 조상인 Collection 인터페이스에는 stream() 메소드가 정의되어 있기 때문에 Collection 인터페이스를 구현한 모든 List와 Set 컬렉션 클래스에서도 stream() 메소드로 스트림을 생성할 수 있다

ArrayList<Integer> list = new ArrayList<Integer>(); 

list.add(4);
list.add(2);
list.add(3);
list.add(1);

Stream<Integer> stream = list.stream();

stream.forEach(System.out::println);
4
2
3
1

같은 스트림으로는 forEach() 메소드를 한 번밖에 호출할 수 없다
따라서 또 다른 스트림을 생성하여 forEach() 메소드를 호출해야 한다

Stream 클래스의 forEach() 메소드는 해당 스트림의 요소를 하나씩 소모해가며 순차적으로 요소에 접근하는 메소드이다


배열

배열에 관한 스트림을 생성하기 위해 Arrays 클래스에는 다양한 형태의 stream() 메소드가 클래스 메소드로 정의되어 있다

또한, 기본 타입인 int, long, double 형을 저장할 수 있는 배열에 관한 스트림이 별도로 정의되어 있다

이러한 스트림은 java.util.stream 패키지의 IntStream, LongStream, DoubleStream 인터페이스로 각각 제공된다

String[] arr = new String[]{"넷", "둘", "셋", "하나"};

Stream<String> stream1 = Arrays.stream(arr);

stream1.forEach(e -> System.out.print(e + " "));
System.out.println();

// 배열의 특정 부분만을 이용한 스트림 생성
Stream<String> stream2 = Arrays.stream(arr, 1, 3);
stream2.forEach(e -> System.out.print(e + " "));
넷 둘 셋 하나 
둘 셋 

가변 매개변수

Stream 클래스의 of() 메소드를 사용하면 가변 매개변수(임의의 값)를 전달받아 스트림을 생성할 수 있다

그러면 무조건 이것만 쓰면 되는거 아닌가?
맞긴 하지만 다른 방식이 더 효율적이기 때문에 타입에 따라 나누는 게 좋다

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class VarArgsAndStreamExample {

    public static void main(String[] args) {
        List<String> result = filterStrings("A", "BB", "CCC", "DDDD");
        System.out.println(result); // [CCC, DDDD]
    }

    public static List<String> filterStrings(String... strings) {
        return Stream.of(strings)
                     .filter(s -> s.length() >= 3)
                     .collect(Collectors.toList());
    }
}
4.2
2.5
3.1
1.9

지정된 범위의 연속된 정수

지정된 범위의 연속된 정수를 스트림으로 생성하기 위해 IntStream나 LongStream 인터페이스에는 range()와 rangeClosed() 메소드가 정의되어 있다

range() 메소드는 명시된 시작 정수를 포함하지만, 명시된 마지막 정수는 포함하지 않는다
rangeClosed() 메소드는 명시된 시작 정수뿐만 아니라 명시된 마지막 정수까지도 포함하는 스트림을 생성한다

// 지정된 범위의 연속된 정수에서 스트림 생성
IntStream stream1 = IntStream.range(1, 4);

stream1.forEach(e -> System.out.print(e + " "));

System.out.println(); 

IntStream stream2 = IntStream.rangeClosed(1, 4);

stream2.forEach(e -> System.out.print(e + " "));
1 2 3 
1 2 3 4

특정 타입의 난수들

특정 타입의 난수로 이루어진 스트림을 생성하기 위해 Random 클래스에는 ints(), longs(), doubles()와 같은 메소드가 정의되어 있다

이 메소드들은 매개변수로 스트림의 크기를 long 타입으로 전달받을 수 있다

이 메소드들은 만약 매개변수를 전달받지 않으면 크기가 정해지지 않은 무한 스트림(infinite stream)을 반환한다

이때에는 limit() 메소드를 사용하여 따로 스트림의 크기를 제한해야 한다

// 특정 타입의 난수로 이루어진 스트림 생성
IntStream stream = new Random().ints(4);

stream.forEach(System.out::println);
1072176871
-649065206
133298431
-616174137

람다 표현식

람다 표현식을 매개변수로 전달받아 해당 람다 표현식에 의해 반환되는 값을 요소로 하는 무한 스트림을 생성하기 위해 Stream 클래스에는 iterate()와 generate() 메소드가 정의되어 있다

iterate() 메소드는 시드(seed)로 명시된 값을 람다 표현식에 사용하여 반환된 값을 다시 시드로 사용하는 방식으로 무한 스트림을 생성한다

IntStream stream = Stream.iterate(2, n -> n + 2); 
// 2, 4, 6, 8, 10, ...

반면에 generate() 메소드는 매개변수가 없는 람다 표현식을 사용하여 반환된 값으로 무한 스트림을 생성한다

Stream<Double> generateStream = Stream.generate(Math::random); 
// 랜덤한 숫자들을 무한히 생성

파일

파일의 한 줄(line)을 요소로 하는 스트림을 생성하기 위해 java.nio.file.Files 클래스에는 lines() 메소드가 정의되어 있다

String<String> stream = Files.lines(Path path);

빈 스트림

아무 요소도 가지지 않는 빈 스트림은 Stream 클래스의 empty() 메소드를 사용하여 생성할 수 있다

Stream<Object> stream = Stream.empty();

System.out.println(stream.count()); // 스트림의 요소의 총 개수를 출력함.
0

스트림의 중개 연산

스트림 API에 의해 생성된 초기 스트림은 중개 연산을 통해 또 다른 스트림으로 변환된다

결국 중개 연산은 스트림을 전달받아 스트림을 반환하는 것 이기 때문에, 중개 연산은 연속으로 연결해서 사용할 수 있다

또한, 스트림의 중개 연산은 필터-맵(filter-map) 기반의 API를 사용함으로 지연(lazy) 연산을 통해 성능을 최적화할 수 있습니다.

대표적인 stream 중개 연산들

1. 스트림 필터링 : filter(), distinct()
2. 스트림 변환 : map(), flatMap()
3. 스트림 제한 : limit(), skip()
4. 스트림 정렬 : sorted()
5. 스트림 연산 결과 확인 : peek()


스트림 필터링

filter()
해당 스트림에서 주어진 조건에 맞는 요소만으로 구성된 새로운 스트림을 반환한다

distinct()
메소드는 해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 반환한다

distinct() 메소드는 내부적으로 Object 클래스의 equals() 메소드를 사용하여 요소의 중복을 비교합니다.

IntStream stream1 = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
IntStream stream2 = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);

stream1.distinct().forEach(e -> System.out.print(e + " "));
System.out.println();

stream2.filter(n -> n % 2 != 0).forEach(e -> System.out.print(e + " "));
7 5 2 1 3 4 6 
7 5 5 1 3 5 

스트림 변환

map()
해당 스트림의 요소들을 주어진 함수에 인수로 전달하여, 그 반환값들로 이루어진 새로운 스트림을 반환

flatMap()
만약 해당 스트림의 요소가 배열일때 각 배열의 각 요소의 반환값을 하나로 합친 새로운 스트림을 얻을 수 있다

map 예시

Stream<String> stream = Stream.of("HTML", "CSS", "JAVA", "JAVASCRIPT");

stream.map(s -> s.length()).forEach(System.out::println);
4
3
4
10

flatMap 예시

String[] arr = {"I study hard", "You study JAVA", "I am hungry"};

Stream<String> stream = Arrays.stream(arr);

stream.flatMap(s -> Stream.of(s.split(" +"))).forEach(System.out::println);
I
study
hard
You
study
JAVA
I
am
hungry

위 예시와 같이 flatMap은 스트림의 요소가 stream일 때 이를 하나의 stream으로 합쳐주는 역할을 하는 것이다

각 요소가 stream이어야 하기 때문에 s.split()이 리턴하는 문자열 배열을 Stream.of()를 통해서 stream으로 바꾸어 준다
(Stream.of 가 아니라 Arrays.stream()이어도 상관없음)


스트림 제한

limit():
해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 반환한다

skip():
해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 반환한다

IntStream stream1 = IntStream.range(0, 10);
IntStream stream2 = IntStream.range(0, 10);
IntStream stream3 = IntStream.range(0, 10);

stream2.limit(5).forEach(n -> System.out.print(n + " "));
System.out.println();

stream1.skip(4).forEach(n -> System.out.print(n + " "));
System.out.println();

stream3.skip(3).limit(5).forEach(n -> System.out.print(n + " "));
4 5 6 7 8 9 
0 1 2 3 4 
3 4 5 6 7 

스트림 정렬

sorted() 메소드는 해당 스트림을 주어진 비교자를 이용하여 정렬한다

이때 비교자를 전달하지 않으면 기본적으로 사전 편찬 순으로 정렬한다

Stream<String> stream1 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");
Stream<String> stream2 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");

stream1.sorted().forEach(s -> System.out.print(s + " "));

System.out.println();

stream2.sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s + " "));
CSS HTML JAVA JAVASCRIPT 
JAVASCRIPT JAVA HTML CSS 

근데 앞서 배운 내용에서 Comparator은 인터페이스인데 구현체가 없이 저렇게 줘도 되나?

자바 8부터 인터페이스에도 구현이 포함될 수 있게 변경되었다

그러면 추상클래스와 차이는 뭐야?

  • 추상 클래스는 상태 (멤버 변수)를 가질 수 있지만, 인터페이스는 상수만 가질 수 있다
  • 추상 클래스는 단일 상속만 가능하지만, 인터페이스는 다중 상속이 가능하다.
  • 추상 클래스는 일반 메서드, 생성자, 필드도 포함할 수 있지만, 인터페이스는 디폴트 메서드와 정적 메서드 (및 private 메서드)만 구현을 가질 수 있다.

아하 그러면 위에서 Comparator는 인터페이스이고 reverseOrder()는 정적 메소드이구나


스트림 연산 결과 확인

peek() 메소드는 결과 스트림으로부터 요소를 소모하여 추가로 명시된 동작을 수행한다

이 메소드는 원본 스트림에서 요소를 소모하지 않으므로, 주로 연산과 연산 사이에 결과를 확인하고 싶은 디버깅 용도로 많이 활용한다

IntStream stream = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);

stream.peek(s -> System.out.println("원본 스트림 : " + s))
    .skip(2)
    .peek(s -> System.out.println("skip(2) 실행 후 : " + s))
    .limit(5)
    .peek(s -> System.out.println("limit(5) 실행 후 : " + s))
    .sorted()
    .peek(s -> System.out.println("sorted() 실행 후 : " + s))
    .forEach(n -> System.out.println(n));
원본 스트림 : 7
원본 스트림 : 5
원본 스트림 : 5
skip(2) 실행 후 : 5
limit(5) 실행 후 : 5
원본 스트림 : 2
skip(2) 실행 후 : 2
limit(5) 실행 후 : 2
원본 스트림 : 1
skip(2) 실행 후 : 1
limit(5) 실행 후 : 1
원본 스트림 : 2
skip(2) 실행 후 : 2
limit(5) 실행 후 : 2
원본 스트림 : 3
skip(2) 실행 후 : 3
limit(5) 실행 후 : 3
sorted() 실행 후 : 1
1
sorted() 실행 후 : 2
2
sorted() 실행 후 : 2
2
sorted() 실행 후 : 3
3
sorted() 실행 후 : 5
5

위에서 7, 5, 5, 2, 1, 2, 3, 5, 4, 6 이 순서대로 각각의 중간 연산을 거치기 때문에 하나씩 출력된 결과가 위와 같이 나온다

이렇게 peek() 메소드는 스트림의 각 요소가 해당 중개 연산 후에 어떻게 변화하는지를 보여준다

peek()가 없으면 그냥 아래와 같이 최종 결과만 나올 것이다

1
2
2
3
5

스트림의 최종 연산

스트림의 최종 연산

스트림 API에서 중개 연산을 통해 변환된 스트림은 마지막으로 최종 연산을 통해 각 요소를 소모하여 결과를 표시한다

즉, 지연(lazy)되었던 모든 중개 연산들이 최종 연산 시에 모두 수행되는 것임

이렇게 최종 연산 시에 모든 요소를 소모한 해당 스트림은 더는 사용할 수 없게 된다

Stream의 대표적인 최종 연산들의 메소드들
1. 요소의 출력 : forEach()
2. 요소의 소모 : reduce()
3. 요소의 검색 : findFirst(), findAny()
4. 요소의 검사 : anyMatch(), allMatch(), noneMatch()
5. 요소의 통계 : count(), min(), max()
6. 요소의 연산 : sum(), average()
7. 요소의 수집 : collect()


요소의 출력

스트림의 각 요소를 소모하여 명시된 동작을 수행한다

반환 타입이 void이므로 보통 스트림의 모든 요소를 출력하는 용도로 많이 사용함

Stream<String> stream = Stream.of("넷", "둘", "셋", "하나");

stream.forEach(System.out::println);
넷
둘
셋
하나

요소의 소모

reduce() 메소드는 첫 번째와 두 번째 요소를 가지고 연산을 수행한 뒤, 그 결과와 세 번째 요소를 가지고 또다시 연산을 수행하는 식으로 해당 스트림의 모든 요소를 소모하여 연산을 수행하고, 그 결과를 반환한다

또한, 인수로 초깃값을 전달하면 초깃값과 해당 스트림의 첫 번째 요소와 연산을 시작하며, 그 결과와 두 번째 요소를 가지고 계속해서 연산을 수행한다

Stream<String> stream1 = Stream.of("넷", "둘", "셋", "하나");
Stream<String> stream2 = Stream.of("넷", "둘", "셋", "하나");

Optional<String> result1 = stream1.reduce((s1, s2) -> s1 + "++" + s2);

result1.ifPresent(System.out::println);

String result2 = stream2.reduce("시작", (s1, s2) -> s1 + "++" + s2);

System.out.println(result2);
넷++둘++셋++하나
시작++넷++둘++셋++하나

초기값을 전달하지 않는 reduce() 메소드는 반환타입이 Optional T 이지만,
초깃값을 전달하는 reduce() 메소드의 반환 타입은 T 타입입니다.


요소의 검색

findFirst()와 findAny() 메소드는 해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환한다

두 메소드 모두 비어 있는 스트림에서는 비어있는 Optional T 객체를 반환합니다.

IntStream stream1 = IntStream.of(4, 2, 7, 3, 5, 1, 6);
IntStream stream2 = IntStream.of(4, 2, 7, 3, 5, 1, 6);

OptionalInt result1 = stream1.sorted().findFirst();
System.out.println(result1.getAsInt());

OptionalInt result2 = stream2.sorted().findAny();
System.out.println(result2.getAsInt());
1
1

그러면 둘의 차이는 뭐냐?
병렬 스트림인 경우에는 findAny() 메소드를 사용해야만 정확한 연산 결과를 반환할 수 있다

왜?
findFirst()는 병렬 스트림에서도 항상 스트림의 첫 번째 요소를 반환하려고 하기 때문에 첫번째 요소에 도달하기 위해 추가적인 조정과 동기화가 필요하다. 즉 성능저하가 발생할 수 있음

이에 반해 findAny()는 병렬 스트림에서 스트림의 어느 요소든지 빠르게 찾아 반환하는 것을 목적으로 하기 때문에 첫 번째 요소가 아닌 다른 요소를 반환할 수도 있다.


요소의 검사

해당 스트림의 요소 중에서 특정 조건을 만족하는 요소가 있는지, 아니면 모두 만족하거나 모두 만족하지 않는지를 확인한다

1. anyMatch() : 해당 스트림의 일부 요소가 특정 조건을 만족할 경우에 true를 반환

2. allMatch() : 해당 스트림의 모든 요소가 특정 조건을 만족할 경우에 true를 반환

3. noneMatch() : 해당 스트림의 모든 요소가 특정 조건을 만족하지 않을 경우에 true를 반환

세 메소드 모두 인수로 Predicate 객체를 전달받으며, 요소의 검사 결과는 boolean 값으로 반환한다

IntStream stream1 = IntStream.of(30, 90, 70, 10);
IntStream stream2 = IntStream.of(30, 90, 70, 10);

System.out.println(stream1.anyMatch(n -> n > 80));
System.out.println(stream2.allMatch(n -> n > 80));
true
false

+) Predicate
Predicate는 자바에서 제공하는 함수형 인터페이스 중 하나로 Java 8부터 java.util.function 패키지에 소개되었으며, 주로 람다 표현식과 스트림 API에서 사용된다

Predicate 인터페이스는 한 개의 입력 파라미터를 받아 조건에 따라 boolean 값을 반환하는 test라는 메서드를 정의한다

Predicate 인터페이스는 and(), or(), negate()와 같은 다양한 디폴트 메서드들도 제공하고 이를 통해 여러 개의 조건을 조합할 수 있다


요소의 통계 : count(), min(), max()

  • count() 메소드는 해당 스트림의 요소의 총 개수를 long 타입의 값으로 반환한다
  • max()와 min() 메소드를 사용하면 해당 스트림의 요소 중에서 가장 큰 값과 가장 작은 값을 가지는 요소를 참조하는 Optional T 객체를 얻을 수 있다
IntStream stream1 = IntStream.of(30, 90, 70, 10);
IntStream stream2 = IntStream.of(30, 90, 70, 10);

System.out.println(stream1.count());
System.out.println(stream2.max().getAsInt());
4
90

+) OptionalInt
여기서 stream2는 IntStream이고 이로 인해 stream2.max()는 OptionalInt를 반환하기 때문에 값을 얻기 위해서는 get()이 아니라 getAsInt()를 사용해야만 한다
(Optional이면 getAsInt() 가 안되고, OptionalInt면 get()이 안됨)


요소의 연산 : sum(), average()

IntStream이나 DoubleStream과 같은 기본 타입 스트림에 포함된 해당 스트림의 모든 요소에 대해 합과 평균을 구할 수 있는 메소드

이때 sum() 메소드는 해당하는 T 타입을, average() 메소드는 각 기본 타입으로 래핑 된 OptionalDouble객체를 반환한다

IntStream stream1 = IntStream.of(30, 90, 70, 10);
DoubleStream stream2 = DoubleStream.of(30.3, 90.9, 70.7, 10.1);

System.out.println(stream1.sum());
System.out.println(stream2.average().getAsDouble());
200
50.5

요소의 수집

collect() 메소드는 인수로 전달되는 Collectors 객체에 구현된 방법대로 스트림의 요소를 수집한다

Collectors 클래스에는 미리 정의된 다양한 방법이 클래스 메소드로 정의되어 있으며 사용자가 직접 Collector 인터페이스를 구현하여 자신만의 수집 방법을 정의할 수도 있다

Collectors 클래스의 메소드

1. 스트림을 배열이나 컬렉션으로 변환 : toArray(), toCollection(), toList(), toSet(), toMap()

2. 요소의 통계와 연산 메소드와 같은 동작을 수행 : counting(), maxBy(), minBy(), summingInt(), averagingInt() 등

3. 요소의 소모와 같은 동작을 수행 : reducing(), joining()

4. 요소의 그룹화와 분할 : groupingBy(), partitioningBy()

toList() 예제

Stream<String> stream = Stream.of("넷", "둘", "하나", "셋");

List<String> list = stream.collect(Collectors.toList());

Iterator<String> iter = list.iterator();

while(iter.hasNext()) {
    System.out.print(iter.next() + " ");
}
넷 둘 하나 셋 

groupingBy() 예제

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

class Student {
    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    @Override
    public String toString() {
        return name + ": " + score;
    }
}

public class GroupingExample {
    public static void main(String[] args) {
        List<Student> students = List.of(
            new Student("John", 90),
            new Student("Jane", 85),
            new Student("Tom", 85),
            new Student("Alice", 92)
        );

        // 학생들을 점수에 따라 그룹화
        Map<Integer, List<Student>> groupedByScore = students.stream()
            .collect(Collectors.groupingBy(Student::getScore));

        System.out.println(groupedByScore);
    }
}
{85=[Jane: 85, Tom: 85], 90=[John: 90], 92=[Alice: 92]}

partitioningBy() 예제
Stream<String> stream = Stream.of("HTML", "CSS", "JAVA", "PHP");

Map<Boolean, List<String>> patition = stream.collect(Collectors.partitioningBy(s -> (s.length() % 2) == 0));

List<String> oddLengthList = patition.get(false);
System.out.println(oddLengthList);

List<String> evenLengthList = patition.get(true);
System.out.println(evenLengthList);
[CSS, PHP]
[HTML, JAVA]

Optional 클래스

Optional T 클래스는 Integer나 Double 클래스처럼 'T'타입의 객체를 포장해 주는 래퍼 클래스이기 때문에 모든 타입의 참조변수를 저장할 수 있다

왜 쓰는가?
Optional 클래스를 통해서 복잡한 구문 없이도 null값으로 인해 발생하는 NullPointerException을 처리할 수 있다


Optional 생성

of() 메소드로 생성: null이 아닌 명시된 값을 가짐
(null이 저장되면 NullPointerException이 발생함)

ofNullable() 메소드로 생성: null이 아니면 명시된 값을 가지는 Optional 객체를 반환하고 null이면 비어있는 Optional 객체를 반환한다

Optional<String> opt = Optional.ofNullable("자바 Optional 객체");

System.out.println(opt.get());
자바 Optional 객체

Optional 객체에 접근

get() 메소드를 사용하여 Optional 객체에 저장된 값에 접근
(Optional 객체에 저장된 값이 null이면, NoSuchElementException 예외가 발생)

따라서 get() 메소드를 호출하기 전에 isPresent() 메소드를 사용하여 Optional 객체에 저장된 값이 null인지 아닌지를 먼저 확인한 후 호출하는 것이 좋다

Optional<String> opt = Optional.ofNullable("자바 Optional 객체");

if(opt.isPresent()) {
    System.out.println(opt.get());
}
자바 Optional 객체

또한, 다음과 같은 메소드를 사용하여 null 대신에 대체할 값을 지정할 수도 있다

  1. orElse() 메소드 : 저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 인수로 전달된 값을 반환함
  2. orElseGet() 메소드 : 저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 인수로 전달된 람다 표현식의 결괏값을 반환함
  3. orElseThrow() 메소드 : 저장된 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 인수로 전달된 예외를 발생시킴.
Optional<String> opt = Optional.empty(); // Optional를 null로 초기화함.

System.out.println(opt.orElse("빈 Optional 객체"));
System.out.println(opt.orElseGet(String::new));
System.out.println(opt.orElseThrow(IllegalArgumentException::new));
빈 Optional 객체

기본 타입의 Optional 클래스

자바에서는 IntStream 클래스와 같이 기본 타입 스트림을 위한 별도의 Optional 클래스를 제공한다

  1. OptionalInt 클래스
  2. OptionalLong 클래스
  3. OptionalDouble 클래스

이러한 클래스는 반환 타입이 Optional T 타입이 아니라 해당 기본 타입이라는 사실만 제외하면 거의 모든 면에서 비슷함

그리고 각각은 다른 get() 메소드를 통해서 값에 접근할 수 있다

Optional T : T get()
OptionalInt : int getAsInt()
OptionalLong : long getAsLong()
OptionalDouble : double getAsDouble()

IntStream stream = IntStream.of(4, 2, 1, 3);

OptionalInt result = stream.findFirst();

System.out.println(result.getAsInt());
4
profile
반가워요😎

0개의 댓글