자바 8에서는 함수형 인터페이스, 람다, 메서드 참조라는 개념이 추가되면서 함수 객체를 더 쉽게 만들 수 있게 됨. 이와 함께 스트림 api까지 추가됨!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
자바 8에 와서 추상 메서드 하나짜리 인터페이스는 특별한 의미를 인정받아 특별한 대우를 받게 되었다. 지금은 함수형 인터페이스라 부르는 이 인터페이스들의 인스턴스를 람다식 을 사용해 만들 수 있게 된 것이다. 람다는 함수나 익명 클래스와 개념은 비슷하지만 코드는 훨씬 간결하다.
Collections.sort(words,
(s1, s2) -> Integer.compare(s1.length(), s2.length()));
람다 익숙해지면 아래처럼 더 줄일 수 있음
Collections.sort(words, comparingInt(String::length));
List 자체의 sort() 사용하면 더 짧아짐
words.sort(comparingInt(String::length));
1) 람다는 이름 없음, 문서화도 못함
→ 코드가 길거나 동작이 복잡하면 오히려 읽기 어려워짐
람다는 한 줄이 가장 좋고, 길어도 3줄 안이 적당
2) 람다는 함수형 인터페이스에서만 사용 가능
람다로 대체할 수 없는 곳도 있다. 람다는 함수형 인터페이스로만 쓰인다.
추상 클래스나 추상 메서드 여러 개 있는 인터페이스는
→ 익명 클래스 필요함
3) 람다는 자신을 참조할 수 없다.
람다 내부의 this는 바깥 클래스를 가리킴
익명 클래스는 자기 자신을 가리킴
public LambdaTest anonymous = new LambdaTest() {
final int value = 200;
public String getValue() { return "" + this.value; } // 200
};
public LambdaTest lambda = () -> "" + this.value; // 바깥의 100
4) 람다를 직렬화하는 일은 극히 삼가야한다.
람다도 익명 클래스처럼 직렬화 형태가 구현별로 다를 수 있기 때문이다. 직렬화해야만 하는 함수 객체가 있다면 private 정적 중첩 클래스의 인스턴스를 사용하자.
map.merge(key, 1, (count, incr) -> count + incr);
key 등장한 횟수 저장하는 멀티셋 같은 기능 구현하는 코드임
merge()는
{key, value} 그대로 저장Integer.sum() 등장하면서 이런 코드 가능해짐
map.merge(key, 1, Integer::sum);
→ 람다보다 짧고 가독성 좋음
1) 매개변수 이름이 의미 전달할 때
count, incr 같은 이름이 설명을 대신할 때는 람다가 더 읽힘2) 메서드 참조가 더 길어지는 경우
service.execute(GoshThisClassNameIsHumongous::action);
보다
service.execute(() -> action());
이게 더 짧고 더 명확함
3) identity 함수
굳이 Function.identity()보다
x -> x
이게 훨씬 직관적임
정리: 메서드 참조는 람다의 간단명료한 대안이 될 수 있다. 메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하라.
자바 8부터 제공하는 함수형 인터페이스 세트가 이미 충분함
(Consumer, Function, Predicate, Supplier 등)
이걸 쓰면
-> 이미 있는 걸 굳이 만들 필요 없음.
✔ Consumer 계열 (반환값 없음)
| 인터페이스 | 설명 |
|---|---|
| Consumer | T 받고 아무것도 반환 안 함 |
| BiConsumer<T,U> | 두 값 받음 |
| IntConsumer / LongConsumer | 기본형 특화 |
✔ Supplier 계열 (입력 없음 → 반환만)
| 인터페이스 | 설명 |
|---|---|
| Supplier | T 반환 |
| BooleanSupplier / IntSupplier | 기본형 특화 |
✔ Function 계열 (입력 → 출력)
| 인터페이스 | 설명 |
|---|---|
| Function<T,R> | T 입력 → R 출력 |
| UnaryOperator | T → T |
| BinaryOperator | T,T → T |
| BiFunction<T,U,R> | 두 값 입력 |
✔ Predicate 계열 (조건 판단)
| 인터페이스 | 설명 |
|---|---|
| Predicate | boolean 반환 |
| BiPredicate<T,U> | 두 값 판단 |
1) 표준 인터페이스가 동작을 정확히 표현하지 못할 때
예:
2) checked exception을 던져야 할 때
표준 함수형 인터페이스는 checked exception 허용 안 함
→ 특별히 throw 해야 하면 직접 만들어야 함
@FunctionalInterface 붙이기@FunctionalInterface
public interface MyHandler {
void handle(String message);
}
이 애너테이션 쓰면 얻는 이점:
❗ 이름이 너무 기술적임
Function<T,R> 같은 건 의미 전달이 약함
도메인 의미 드러내고 싶으면 직접 정의하는 게 더 읽기 좋음
❗ boolean만 반환해야 하는 제한
Predicate는 boolean만 반환 가능 → 다른 타입이면 애매해짐
예외 던지는 로직 필요하면 표준 인터페이스로는 처리 불가능
.parallel()로 병렬 처리 가능스트림을 쓰면 다음 작업은 깔끔하고 직관적으로 처리됨:
➡️ 이런 작업은 스트림이 반복문보다 더 명확하고 짧아지고, 오류 가능성도 감소함
데이터가 스트림의 여러 단계를 거칠 때,
각 단계에서의 중간 값에 동시에 접근하거나 디버깅이 필요한 경우
예:
➡️ 이런 경우는 일반 for문이 더 자연스럽고 이해하기 쉽다.
스트림 패러다임은 계산 과정을 순수한 변환(Transformation)의 연속으로 재구성하는 방식이다.
따라서 각 단계의 함수는 이전 단계의 결과만을 입력으로 받아 처리하는 순수 함수(Pure Function)여야 한다.
즉, 스트림에 전달하는 모든 함수 객체는 부작용(사이드 이펙트)이 없어야 한다.
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
freq)를 직접 수정 → 부작용 발생forEach는 계산이 아니라 결과를 보고(reporting)할 때만 사용해야 한다.
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
java.util.stream.Collectors는 스트림의 원소를 특정 형태로 모으는 다양한 메서드를 제공한다.
대표 예:
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
1. toMap(keyMapper, valueMapper)
가장 기본적인 Map 수집기.
private static final Map<String, Operation> stringToEnum =
Stream.of(values())
.collect(toMap(Object::toString, e -> e));
특징
IllegalStateException 발생
2. groupingBy
카테고리로 묶어 Map<K, List> 형태로 모음.
words.collect(groupingBy(w -> alphabetSize(w)));
3. partitioningBy
groupingBy의 Boolean 버전Predicate로 분류Map<Boolean, List<T>>partitioningBy(str -> str.length() > 5)
4. joining
문자열 스트림 연결.
joining() // 구분자 없이 그대로
joining(", ") // 구분자 제공
joining(", ", "[", "]") // prefix, suffix 가능
핵심 정리
- 스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다. 스트림뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야 한다. 종단 연산 중
forEach는 스트림이 수행한 계산 결과를 보고할 때만 이용해야 한다. 계산 자체에는 이용하지 말자. 스트림을 올바로 사용하려면 수집기를 잘 알아둬야 한다. 가장 중요한 수집기 팩터리는toList,toSet,toMap,groupingBy,joining이다.
자바 7까지는 이런 메서드의 반환 타입으로 Collection, Set, List 같은 컬렉션 인터페이스, 혹은 Iterable이나 배열을 썼다. 그런데 자바 8이 스트림이라는 개념을 들고 오면서 이 선택이 아주 복잡한 일이 되어버렸다.
원소 시퀀스를 반환할 때는 당연히 스트림을 사용해야 한다는 이야기를 들어봤을지 모르겠지만, 스트림은 반복(iteration)을 지원하지 않는다.
핵심 정리
- 원소 시퀀스를 반환하는 메서드를 작성할 때는, 이를 스트림으로 처리하기를 원하는 사용자와 반복으로 처리하길 원하는 사용자가 모두 있을 수 있음을 떠올리고, 양쪽을 다 만족시키려 노력하자.
- 1) 컬렉션을 반환할 수 있다면 그렇게 하라.
- 2) 반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나 컬렉션을 하나 더 만들어도 될 정도로 원소 개수가 적다면 ArrayList 같은 표준 컬렉션에 담아 반환하라.
- 3) 그렇지 않으면 앞서의 멱집합 예처럼 전용 컬렉션을 구현할지 고민하라.
- 4) 컬렉션을 반환하는 게 불가능하면 스트림과 Iterable 중 더 자연스러운 것을 반환하라.
- 5) 만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 자바가 수정된다면 그때는 안심하고 스트림을 반환하면 될 것이다.
❌ 병렬화할 수 없는 스트림 연산
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
primes().map(...)
.filter(...)
.limit(20)
.forEach(System.out::println);
여기서 parallel()을 추가하면 아무것도 출력하지 못하고 무한히 CPU만 사용한다.
Stream.iterate는 병렬 처리에 적합하지 않음limit()과 조합되면 병렬화 전략을 찾기 어려움예: limit(20) 이 붙은 상태에서 병렬 실행
✔ 가장 병렬 효과가 좋은 소스
ArrayListHashMapHashSetConcurrentHashMapint[], long[])IntStream.range, LongStream.range)✔ 이유
핵심정리
- 병렬화 룰을지키고 성능이 빨라질거라는 확신이 있을때만(꼭 테스트해봐야한다) 스트림 파이프라인 병렬화를 시도해라
- 잘못 병렬화하면 프로그램이 오동작, 성능저하가 일어난다.
- 병렬화했을때 명확하게 성능이 좋아지고 계산이 정확할때만 사용해라