1. 스트림이란
스트림(Stream)은 Java 8에 도입된 기능으로, 컬렉션(List, Set 등)의 요소를 함수형 스타일로 처리할 수 있게 해주는 기능
즉, 데이터를 흐름처럼 처리하면서 필터링, 변환, 정렬, 집계 등을 간결하고 선언적으로 작성가능
List<String> list = List.of("apple", "banana", "cherry");
// 1. 스트림 생성
// 2. 중간 연산 (filter, map 등)
// 3. 종단 연산 (collect, forEach 등)
List<String> result = list.stream()
.filter(s -> s.startsWith("b")) // 중간 연산: 필터링
.map(String::toUpperCase) // 중간 연산: 대문자 변환
.sorted() // 중간 연산: 정렬
.toList(); // 종단 연산: 리스트 수집
📍 자주 사용하는 중간 연산
| 메서드 | 설명 | 예시 |
| `filter(Predicate)` | 조건에 맞는 요소만 걸러냄 | list.stream().filter(s -> s.length() > 3)→ 길이 3초과인 문자열만 통과 |
| `map(Function)` | 요소를 변환 | list.stream().map(String::toUpperCase)→ 모두 대문자로 변환 |
| `flatMap(Function)` | 중첩된 구조를 펼침 | list.stream().flatMap(s -> Arrays.stream(s.split("")))→ 문자열을 문자 하나씩 스트림으로 |
| `distinct()` | 중복 제거 | list.stream().distinct() |
| `sorted()` | 기본 정렬 (Comparable 기준) | list.stream().sorted() |
| `sorted(Comparator)` | 사용자 정의 정렬 | list.stream().sorted(Comparator.reverseOrder()) |
| `limit(n)` | 처음 n개만 선택 | list.stream().limit(2) |
| `skip(n)` | 처음 n개 건너뜀 | list.stream().skip(2) |
| `peek(Consumer)` | 디버깅용: 중간에 값 출력 등 | list.stream().peek(System.out::println) |
📍 자주 사용하는 종단 연산
| 메서드 | 설명 | 예시 |
| `collect(Collectors.toList())` | 결과 수집 | list.stream().collect(Collectors.toList()) |
| `forEach(Consumer)` | 각 요소에 작업 수행 | list.stream().forEach(System.out::println) |
| `count()` | 요소 수 반환 | list.stream().count() |
| `anyMatch(Predicate)` | 하나라도 조건 만족 여부 | list.stream().anyMatch(s -> s.startsWith("a")) |
| `allMatch(Predicate)` | 모두 조건 만족 여부 | list.stream().allMatch(s -> s.length() > 1) |
| `noneMatch(Predicate)` | 모두 조건 불만족 여부 | list.stream().noneMatch(String::isEmpty) |
| `findFirst()` | 첫 번째 요소 반환 | list.stream().findFirst() → Optional 반환 |
| `findAny()` | 아무 요소 하나 반환 | list.stream().findAny() |
| `reduce()` | 누적 계산 (합, 곱 등) | list.stream().reduce("", (a, b) -> a + b) |
| `max(Comparator)` | 최대값 찾기 | list.stream().max(Comparator.naturalOrder()) |
| `min(Comparator)` | 최소값 찾기 | list.stream().min(Comparator.naturalOrder()) |
1-1. 스트림의 특징 요약
- 스트림은 데이터를 변형·처리하는데 초점 (원본 데이터 변경 X)
- 지연 평가(Lazy Evaluation): 중간 연산은 최종 연산 전까지 수행되지 않음
- 병렬 스트림(parallelStream())을 사용하면 멀티코어에서 병렬 처리 가능
2. Optional이란
Java의 Optional<T>은 null을 안전하게 다루기 위한 컨테이너 클래스
T 타입의 값을 가질 수도 있고 없을 수도 있다.
직접 null 체크하는 대신 Optional이 제공하는 메서드들을 활용하면 널포인트 예외(NPE)를 방지하고 코드 가독성도 좋아진다.
2-1. Optional 주요 메서드
| 메서드 | 설명 | 예제 |
| of(value) | 절대 null이 아닌 값을 감쌈 | Optional.of("hello") |
| ofNullable(value) | null일 수도 있는 값을 감쌈 | Optional.ofNullable(null) |
| empty() | 비어있는 Optional 생성 | Optional.empty() |
2-2. 값 조회 및 조건 분기
| 메서드 | 설명 | 예제 |
| isPresent() | 값이 존재하는지 여부 (true/false) | if (opt.isPresent()) {...} |
| isEmpty() | Java 11 이상. 값이 비었는지 확인 | if (opt.isEmpty()) {...} |
| get() | 값을 꺼냄 (비었으면 예외 발생) ❗ | opt.get() (❌지양) |
2-3. 기본값 처리
| 메서드 | 설명 | 예제 |
| orElse(defaultValue) | 값이 없으면 기본값 반환 | opt.orElse("기본값") |
| orElseGet(Supplier) | 값이 없으면 함수 실행해서 기본값 제공 | opt.orElseGet(() -> "생성") |
| orElseThrow() | 없으면 예외 발생시킴 | opt.orElseThrow() |
| orElseThrow(Supplier) | 없으면 직접 지정한 예외 발생 | opt.orElseThrow(() -> new IllegalArgumentException()) |
2-4. 값 변형/필터링
| 메서드 | 설명 | 예제 |
| map(Function) | 값이 있으면 변환 후 Optional로 감쌈 | opt.map(String::toUpperCase) |
| flatMap(Function) | Optional 안에 Optional이 있을 때 펼침 | opt.flatMap(...) |
| filter(Predicate) | 조건에 맞으면 그대로, 아니면 empty 반환 | opt.filter(s -> s.length() > 3) |
2-5. 조건이 있을 때만 실행
| 메서드 | 설명 | 예제 |
| ifPresent(Consumer) | 값이 있으면 작업 수행 | opt.ifPresent(System.out::println) |
| ifPresentOrElse(Consumer, Runnable) | Java 9+값이 있으면 A, 없으면 B 수행 | opt.ifPresentOrElse(..., ...) |
Optional<String> opt = Optional.ofNullable("hello");
// 1. 값이 있으면 출력
opt.ifPresent(System.out::println); // hello
// 2. 값이 없으면 기본값
String result = opt.orElse("default"); // "hello"
// 3. 길이 5 이상인 경우만 유지
opt = opt.filter(s -> s.length() >= 5); // 그대로 유지됨
// 4. 값이 있으면 대문자로 변환
opt = opt.map(String::toUpperCase); // Optional["HELLO"]
// 5. 값이 없으면 예외 발생
String val = opt.orElseThrow(() -> new RuntimeException("값이 없어요"));
📍 주의사항~
- `Optional<T>을 필드, 파라미터, 리턴 외에 쓴다면 불필요하게 복잡해짐
- `Optional.get()`을 남용하면 값이 없으면 예외 발생한다 . (null이랑 다를 바가 없음 )
- `Optional<T>`를 컬렉션 타입과 함께 사용 할때 불필요하다
예를들어 `List<Optional<T>>` 또는 `Optional<List<T>>`는 불필요함
누가 이렇게 쓸까싶지만..하다보면 또 이렇게 될수도 ..ㅋ
- Optional은 값이 "있을 수도, 없을 수도" 있는 단일 객체에 적합
- 컬렉션(List, Set 등)은 자체적으로 비어있음을 표현할 수 있기 때문에 Optional로 감쌀 필요가 없다
- 그래서 Optional<List<T>>, List<Optional<T>>는 대부분의 경우 과도한 추상화이며 코드를 복잡하게 만들기만 함.
- List<Optional<T>> 불필요한 이유
더보기
List<Optional<String>> list = List.of(
Optional.of("a"),
Optional.empty(),
Optional.of("b")
);
- 리스트 내부의 각 요소가 Optional인데, Optional을 꺼낼 때마다 isPresent(), get() 등을 확인해야 함.
- 반복문 돌릴 때 매번 Optional 체크:
for (Optional<String> opt : list) {
opt.ifPresent(System.out::println);
}
- 실제로는 null 또는 값이 존재하는 일반 리스트 요소가 더 간단하게 처리됨:
List<String> list = List.of("a", null, "b");
Optional 대신 필터링이 가능한 일반 리스트를 쓰고, null 값 제거:
List<String> list = List.of("a", null, "b");
list.stream()
.filter(Objects::nonNull)
.forEach(System.out::println); // a, b
- Optional<List<T>>가 불필요한 이유
더보기
Optional<List<String>> maybeList = findSomeList(); // List가 있을 수도 없을 수도
if (maybeList.isPresent()) {
maybeList.get().forEach(System.out::println);
}
- Optional의 존재 여부를 먼저 확인하고, 그 안의 리스트를 다시 순회해야 함. 코드 중첩.
- 대부분의 경우, 빈 리스트 (List.of())를 반환하면 Optional이 필요 없음.
애초에 Optional 대신 빈 리스트 반환:
public List<String> findSomeList() {
if (조건) return List.of("a", "b");
else return Collections.emptyList(); // Optional 쓰지 않음
}
// 사용처
findSomeList().forEach(System.out::println); // 안전하게 처리됨
3. 람다란?
- 익명메서드~
이름 없는 메서드를 간단한 식으로 표현해서 코드의 간결함과 가독성 - Stream API나 콜백 함수, 정렬, 이벤트 처리에 자주 사용.
3-1. 기본 문법
(매개변수) -> { 실행문 }
3-2. 함수형 인터페이스와 람다
람다는 함수형 인터페이스(Functional Interface) 가 있을 때만 사용할 수 있다.
함수형 인터페이스: 추상 메서드가 하나만 존재하는 인터페이스
예시: `Runnable`, `Comparator`, `Consumer`, `Function`, `Predicate`
Runnable r = () -> System.out.println("Thread 실행");
new Thread(r).start();
| 인터페이스 | 람다 형식 | 의미 (설명) |
| Consumer<T> | T → void | 입력만 받고 반환 없음→ System.out.println(x) |
| Function<T, R> | T → R | 입력 T를 받아서 결과 R 반환→ x -> x.length() |
| Predicate<T> | T → boolean | 입력 T를 받아 true/false 반환→ x -> x.startsWith("a") |
| Supplier<T> | () → T | 아무것도 안 받고 T를 반환→ () -> "hello" |
3-3. 메서드 참조의 종류 4가지
| 유형 | 예시 | 설명 |
| 1. Static 메서드 참조 | `ClassName::staticMethod | `x -> ClassName.staticMethod(x)` |
| 2. 인스턴스 메서드 참조 (특정 객체) | `instance::method` | `x -> instance.method(x)` |
| 3. 인스턴스 메서드 참조 (타입 기준) | `ClassName::method` | `x -> x.method()` |
| 4. 생성자 참조 | `ClassName::new` | `() -> new ClassName()` 또는 `args -> new ClassName(args)` |
3-4. 비교: 람다식 vs 메서드 참조
//1.System.out::println
list.forEach(x -> System.out.println(x));
// ↓ 동일한 동작
list.forEach(System.out::println);
//2.String::toUpperCase
list.stream()
.map(s -> s.toUpperCase())
// ↓
.map(String::toUpperCase);
//3. 생성자 참조
Supplier<List<String>> listSupplier = () -> new ArrayList<>();
// ↓
Supplier<List<String>> listSupplier = ArrayList::new;
'공부일기.. > Java' 카테고리의 다른 글
| [TDD](TDD기반 서비스 개발 후) 회고 및 객체지향 설계 고민 (0) | 2025.07.12 |
|---|---|
| [TDD] JUnit 테스트 입문 – Mockito로 실제 객체 vs Mock 비교 (0) | 2025.07.11 |
| [TDD] TDD 테스트 방법론 (0) | 2025.07.11 |
| 객제지향 속성 (캡슐화,상속,추상화,다형성) (0) | 2025.06.20 |
| JVM 메모리영역 (6) | 2025.06.19 |