람다식(Lambda expression) : 메서드를 하나의 식으로 표현한 것. 익명 함수(anonymous function) 이라고도 한다
| 구분 | 원래 식 (익명 클래스) | 람다식 | 설명 |
|---|---|---|---|
| 매개변수 없음 | new Runnable() { public void run() { System.out.println("Hi"); } } | () -> System.out.println("Hi") | 매개변수 없음 |
| 매개변수 1개, 한 문장 | new Consumer<Integer>() { public void accept(Integer x) { System.out.println(x); } } | x -> System.out.println(x) | 매개변수 1개, 괄호 생략 가능 |
| 매개변수 1개, 괄호 포함 | 동일 | (x) -> System.out.println(x) | 괄호 명시 |
| 매개변수 2개 | new BiFunction<Integer, Integer, Integer>() { public Integer apply(Integer a, Integer b) { return a + b; } } | (a, b) -> a + b | 괄호 필수 |
| 타입 명시 | 동일 | (int a, int b) -> a + b | 타입 명시 가능 |
| 구현부 여러 문장 | new BiFunction<Integer, Integer, Integer>() { public Integer apply(Integer a, Integer b) { int sum = a + b; return sum; } } | (a, b) -> { int sum = a + b; return sum; } | 여러 문장, 중괄호+return 필요 |
| 중괄호 사용, 한 문장 | new Function<Integer, Integer>() { public Integer apply(Integer x) { return x * 2; } } | (x) -> { return x * 2; } | 중괄호 있으면 return 필요 |
| 반환값 없는 경우 | new Consumer<String>() { public void accept(String s) { System.out.println(s); } } | s -> System.out.println(s) | void 메서드 |
| 메서드 참조 | 동일 | System.out::println | 람다식 안의 로직이 여러 줄일 때는 사용하지 못한다. 단순히 메서드 하나만 호출하는 한 줄의 코드일 경우 가능하다 |
* 생성자를 호출하는 람다식도 메서드 참조로 변경 가능
Supplier<MyClass> s = () -> new MyClass(); // MyClass 객체를 생성하여 반환하는 람다식
// →
Supplier<MyClass> s = MyClass::new;
| 사용 예제 | 익명 클래스 방식 | 람다식 변환 후 |
|---|---|---|
| 스레드 실행 | new Thread(new Runnable() { public void run() { System.out.println("Hello"); } }).start(); | new Thread(() -> System.out.println("Hello")).start(); |
| Comparator 정렬 | Collections.sort(list, new Comparator<String>() { public int compare(String a, String b) { return a.compareTo(b); } }); | Collections.sort(list, (a, b) -> a.compareTo(b)); |
| 버튼 이벤트 처리 | button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Click"); } }); | button.addActionListener(e -> System.out.println("Click")); |
| Callable 정의 | Callable<Integer> c = new Callable<Integer>() { public Integer call() { return 42; } }; | Callable<Integer> c = () -> 42; |
| 리스트 반복 출력 | list.forEach(new Consumer<String>() { public void accept(String s) { System.out.println(s); } }); | list.forEach(s -> System.out.println(s)); |
| map의 entry 출력 | map.forEach(new BiConsumer<String, Integer>() { public void accept(String k, Integer v) { System.out.println(k + "=" + v); } }); | map.forEach((k, v) -> System.out.println(k + "=" + v)); |
함수형 인터페이스(Functional Interface): 오직 하나의 추상 메서드만을 가지는 인터페이스
자바에서 모든 메서드는 클래스에 포함되어야 하는데 람다식은 익명 클래스의 객체이다 → 함수형 인터페이스를 구현한 익명 클래스
함수형 인터페이스는 Object 클래스를 상속 받기 때문에 Object 클래스의 메서드들을 사용할 수 있다
함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다. 람다식은 익명 객체이고 익명 객체는 타입이 없다. 정확히는 타입이 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다.
출처 : Java의 정석 3판 (저자 : 남궁성)
// ❌
Object o1 = (int a, int b) -> a > b ? a : b // 불가능. 람다식은 항상 인터페이스 타입으로 받아야한다
// ❌
Object o2 = new Obejct() {
int max(int a, int b) {
return a > b ? a : b;
}
}; // 컴파일 에러는 나지 않지만 Object 클래스에 max 라는 메서드가 없기 때문에 max 메서드를 사용하지 못한다
// ✅
interface MyFunction {
int max(int a, int b);
}
Myfunction f = (int a, int b) -> a > b ? a : b; // 이렇게 함수형 인터페이스를 정의하고 람다식을 받아야한다
일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓은 패키지
| 인터페이스 | 추상 메서드 시그니처 | 역할 / 설명 | 예시 람다식 |
|---|---|---|---|
| Function<T, R> | R apply(T t) | 입력 T → 반환 R | x -> x + 1 |
| BiFunction<T, U, R> | R apply(T t, U u) | 입력 (T, U) → 반환 R | (x, y) -> x + y |
| UnaryOperator | T apply(T t) | Function의 특수형 (T → T) | x -> x * 2 |
| BinaryOperator | T apply(T t1, T t2) | BiFunction 특수형 (T, T → T) | (x, y) -> x + y |
| Predicate | boolean test(T t) | 조건 검사 (true/false 반환) | x -> x > 10 |
| BiPredicate<T, U> | boolean test(T t, U u) | 두 인자 조건 검사 | (a, b) -> a.equals(b) |
| Consumer | void accept(T t) | 값을 소비 (출력 등, 반환 없음) | x -> System.out.println(x) |
| BiConsumer<T, U> | void accept(T t, U u) | 두 인자 소비 | (a, b) -> System.out.println(a + b) |
| Supplier | T get() | 값 제공 (입력 없음) | () -> "hello" |
| 인터페이스 | 추상 메서드 시그니처 | 설명 | 예시 람다식 |
|---|---|---|---|
| IntFunction | R apply(int value) | int → R | i -> i * 2.0 |
| ToIntFunction | int applyAsInt(T t) | T → int | str -> str.length() |
| IntPredicate | boolean test(int value) | int 값에 대한 조건 검사 | i -> i > 0 |
| LongSupplier | long getAsLong() | long 값 제공 | () -> 123L |
| BooleanSupplier | boolean getAsBoolean() | boolean 제공 | () -> true |
| IntConsumer | void accept(int value) | int 값 소비 | i -> System.out.println(i) |
| ToIntBiFunction<T, U> | int applyAsInt(T t, U u) | (T, U) → int 반환 | (a, b) -> a.length() + b |
| 분류 | 목적 | 대표 인터페이스 |
|---|---|---|
| 함수형 | 입력 → 출력 | Function, BiFunction, Unary/BinaryOperator |
| 조건 검사 | true/false 반환 | Predicate, BiPredicate, IntPredicate |
| 소비형 | 값을 받아 소비 | Consumer, BiConsumer, IntConsumer |
| 생산형 | 값을 제공 | Supplier, LongSupplier, BooleanSupplier |
Function 인터페이스
static 메서드
| 메서드 | 설명 |
|---|---|
identity() | 입력값을 그대로 반환하는 Function 반환 |
예시: Function.identity() | x -> x |
default 메서드
| 메서드 | 설명 |
|---|---|
andThen(Function<? super R, ? extends V> after) | 먼저 실행된 Function 이후에 after를 실행하는 새로운 Function 반환 |
compose(Function<? super T, ? extends V> before) | 먼저 실행된 before 후에 Function 실행하는 새로운 Function 반환 |
예시: x -> x + 1 andThen(y -> y * 2) | (x -> x + 1) 후 (y -> y * 2) 실행 |
BiFunction 인터페이스
static 메서드
| 메서드 | 설명 |
|---|---|
identity() | 입력값 두 개를 그대로 반환하는 BiFunction 반환 |
예시: BiFunction.identity() | (x, y) -> (x, y) |
default 메서드
| 메서드 | 설명 |
|---|---|
andThen(Function<? super R, ? extends V> after) | 먼저 실행된 BiFunction 이후 after를 실행하는 새로운 BiFunction 반환 |
예시: (x, y) -> x + y andThen(z -> z * 2) | (x, y) -> x + y 후 (z -> z * 2) 실행 |
Predicate 인터페이스
static 메서드
| 메서드 | 설명 |
|---|---|
isEqual(Object targetRef) | 주어진 객체와 비교하여 true/false를 반환하는 Predicate 반환 |
예시: Predicate.isEqual("test") | "test".equals(x) |
default 메서드
| 메서드 | 설명 |
|---|---|
and(Predicate<? super T> other) | 먼저 실행된 Predicate 이후 other를 실행하는 새로운 Predicate 반환 |
or(Predicate<? super T> other) | 먼저 실행된 Predicate 이후 other를 실행하는 새로운 Predicate 반환 |
negate() | 조건을 반대로 하는 새로운 Predicate 반환 |
예시: x -> x > 10 and(x -> x < 20) | x -> x > 10 후 x -> x < 20 조건 |
Consumer 인터페이스
default 메서드
| 메서드 | 설명 |
|---|---|
andThen(Consumer<? super T> after) | 먼저 실행된 Consumer 이후 after를 실행하는 새로운 Consumer 반환 |
예시: x -> System.out.println(x) andThen(y -> System.out.println(y)) | x -> System.out.println(x) 후 y -> System.out.println(y) 실행 |
Supplier 인터페이스
default 메서드
| 메서드 | 설명 |
|---|---|
andThen(Supplier<? extends T> after) | 먼저 실행된 Supplier 이후 after를 실행하는 새로운 Supplier 반환 |
예시: () -> "Hello" andThen(() -> " World") | () -> "Hello" 후 () -> " World" 실행 |
기타 주요 함수형 인터페이스들
IntFunction, ToIntFunction, LongFunction 등 이러한 함수형 인터페이스들도 default 및 static 메서드를 지원하지만 그 내용은 대부분 위의 Function 인터페이스의 패턴을 따른다
java.util.function 패키지의 static 메서드는 주로 기본적인 기능을 제공하거나 특정 객체를 쉽게 생성할 수 있도록 도와준다
default 메서드는 기존의 메서드 체이닝을 확장할 수 있도록 도와주며 이를 통해 함수형 인터페이스의 재사용성을 높여준다
스트림(Stream) : 데이터의 흐름(Flow) 을 추상화한 것으로, 데이터 소스를 기반으로 파이프라인 형태의 처리를 지원하는 API이다
→ 데이터 소스를 추상화했기 때문에 데이터 소스가 무엇이든 간에 같은 방식으로 다룰 수 있다
→ 코드의 재사용성이 높아진다
parallel() 을 호출해서 병렬 스트림으로 전환할 수 있다 (내부적으로 fork & join framework 사용)Collection 인터페이스에 default 메서드로 정의된 stream() 메서드로 생성할 수 있다
List<Integer> list = new ArrayList<>();
// →
Stream<Integer> intStream = list.stream();
Arrays 클래스의 static 메서드인 stream() 이나 Stream 인터페이스의 static 메서드인 of() 로 생성할 수 있다
// Stream.of() 사용
Stream<String> s1 = Stream.of("a", "b", "c");
Stream<String> s2 = Stream.of(new String[]{"a", "b", "c"});
// Arrays.stream() 사용
Stream<String> s3 = Arrays.stream(new String[]{"a", "b", "c"});
Stream<String> s4 = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3); // 시작, 끝 지정
IntStream 인터페이스의 static 메서드인 range(), rangeClosed() 로 생성할 수 있다 (Long도 동일)
IntStream intStream = IntStream.range(1, 5); // 1에서 4까지의 정수 스트림 생성
Random 클래스의 ints(), longs(), doubles()로 난수 무한 스트림을 생성할 수 있다
IntStream intStream = new Random().ints();
intStream.limit(5).forEach(System.out::println); // 무한 스트림이므로 잘라서 유한 스트림으로 사용해야한다
Stream 클래스의 iterate(T seed, UnaryOperator<T> f), generate(Supplier<T> s) 로 계산값을 무한 스트림으로 생성할 수 있다
Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
Stream<Double> randomStream = Stream.generate(Math::random);
IntStream과 같은 기본형 스트림 타입의 참조변수로는 다룰 수 없다
java.nio.file.Files 클래스의 static 메서드인 list(Path dir) 메서드로 dir의 파일 목록을 스트림으로 생성할 수 있다
Stream 클래스의 empty() 메서드로 빈 스트림을 만들 수 있다 (null 값 대용)
Stream 클래스의 concat() 메서드로 요소가 같은 타입의 두 스트림을 연결할 수 있다
| 메서드 | 설명 | 예시 |
|---|---|---|
filter(Predicate<T>) | 조건에 맞는 요소만 통과 | stream.filter(x -> x > 10) |
map(Function<T, R>) | 요소를 변환 | stream.map(String::toUpperCase) |
flatMap(Function<T, Stream<R>>) | 중첩된 구조를 평탄화 | list.stream().flatMap(List::stream) |
distinct() | 중복 제거 | stream.distinct() |
sorted() | 자연 정렬 | stream.sorted() |
sorted(Comparator<T>) | 지정된 기준으로 정렬 | stream.sorted(Comparator.reverseOrder()) |
limit(long n) | n개 요소만 남김 | stream.limit(5) |
skip(long n) | 처음 n개 요소 건너뜀 | stream.skip(2) |
peek(Consumer<T>) | 중간 결과를 디버깅 용도로 확인 | stream.peek(System.out::println) |
mapToInt(ToIntFunction<T>) | IntStream으로 변환 | stream.mapToInt(String::length) |
mapToLong(ToLongFunction<T>) | LongStream으로 변환 | stream.mapToLong(x -> x) |
mapToDouble(ToDoubleFunction<T>) | DoubleStream으로 변환 | stream.mapToDouble(Math::sqrt) |
boxed() | 기본형 스트림을 참조형으로 변환 | IntStream.range(1, 5).boxed() |
| 메서드 | 설명 | 예시 |
|---|---|---|
forEach(Consumer<T>) | 각 요소에 대해 작업 수행 | stream.forEach(System.out::println) |
toArray() | 배열로 변환 | stream.toArray() |
reduce(BinaryOperator<T>) | 누적해서 하나의 결과 도출 | stream.reduce((a, b) -> a + b) |
collect(Collector) | 컬렉션이나 문자열 등으로 수집 | stream.collect(Collectors.toList()) |
min(Comparator<T>) | 최소값 요소 반환 | stream.min(Comparator.naturalOrder()) |
max(Comparator<T>) | 최대값 요소 반환 | stream.max(Comparator.naturalOrder()) |
count() | 요소 수 세기 | stream.count() |
anyMatch(Predicate<T>) | 하나라도 조건을 만족하는지 검사 | stream.anyMatch(x -> x > 0) |
allMatch(Predicate<T>) | 모두 조건을 만족하는지 검사 | stream.allMatch(x -> x != null) |
noneMatch(Predicate<T>) | 아무것도 조건을 만족하지 않는지 검사 | stream.noneMatch(x -> x < 0) |
findFirst() | 첫 번째 요소 반환 (Optional) | stream.findFirst() |
findAny() | 병렬 스트림에서 아무 요소 반환 (Optional) | stream.findAny() |
* 최종 연산을 호출하면 스트림 파이프라인은 소비(consume) 되며, 더 이상 사용할 수 없다
collect() 에서 groupingBy() 와 partitioningBy()의 사용Collectors.partitioningBy()
| 항목 | 설명 |
|---|---|
| 목적 | boolean 기준으로 2개의 그룹으로 분할 |
| 반환 타입 | Map<Boolean, List<T>> |
| 조건 | Predicate 조건에 따라 true, false로 나눔 |
List<Integer> list = List.of(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> result =
list.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
// { true=[2, 4, 6], false=[1, 3, 5] }
→ 조건이 true인 그룹과 false인 그룹으로 나눠진다
Collectors.groupingBy()
| 항목 | 설명 |
|---|---|
| 목적 | 주어진 기준으로 여러 그룹으로 분류 |
| 반환 타입 | Map<K, List<T>> |
| 조건 | Function<T, K> 으로 그룹핑 기준을 제공 |
List<String> names = List.of("Tom", "Jane", "John", "Max", "Alice");
Map<Integer, List<String>> grouped =
names.stream()
.collect(Collectors.groupingBy(String::length));
// {3=[Tom, Max], 4=[John, Jane], 5=[Alice]}
→ 각 문자열의 길이로 그룹화한다
collect() 메서드에 사용되는 수집 전략을 정의하는 인터페이스| 타입 매개변수 | 설명 |
|---|---|
T | 입력 요소의 타입 (Stream으로 처리되는 요소의 타입) |
A | 누적할 자료 구조의 타입 (중간 컨테이너) |
R | 최종 반환 결과의 타입 |
| 메서드 | 반환 타입 | 설명 |
|---|---|---|
supplier() | Supplier<A> | 결과를 저장할 새로운 컨테이너를 생성 |
accumulator() | BiConsumer<A, T> | 스트림의 요소를 컨테이너에 수집 |
combiner() | BinaryOperator<A> | 병렬 처리 시 두 컨테이너를 병합 |
finisher() | Function<A, R> | 컨테이너를 최종 결과로 변환 |
characteristics() | Set<Collector.Characteristics> | Collector의 특성 집합 반환 |
| 상수 | 설명 |
|---|---|
CONCURRENT | 병렬 처리 가능한 수집기. 이 특성이 있으면 Collector는 병렬 스트림에서 안전하게 사용할 수 있습니다. |
UNORDERED | 요소의 순서가 중요하지 않음을 나타냅니다. 이 특성이 있으면, 수집 결과의 순서가 스트림의 순서와 다를 수 있습니다. |
IDENTITY_FINISH | finisher() 함수가 identity이면, 그 결과가 동일하기 때문에 finisher()가 생략될 수 있음을 나타냅니다. (즉, A == R) |
Collector<T, List<T>, List<T>> toListCollector = new Collector<>() {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
@Override
public BinaryOperator<List<T>> combiner() {
return (left, right) -> {
left.addAll(right);
return left;
};
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity(); // 그대로 반환
}
@Override
public Set<Characteristics> characteristics() {
return Set.of(Collector.Characteristics.IDENTITY_FINISH);
}
};
Optional<T> 는 T 타입의 객체를 감싸는 래퍼 클래스이다
| 메서드 | 설명 | 예시 |
|---|---|---|
Optional.of(value) | null이 아닌 값으로 Optional 생성 | Optional.of("hello") |
Optional.ofNullable(value) | null 가능 값으로 Optional 생성 | Optional.ofNullable(name) |
Optional.empty() | 비어 있는 Optional 생성 | Optional.empty() |
| 메서드 | 설명 | 예시 |
|---|---|---|
isPresent() | 값이 존재하는지 확인 (true/false) | opt.isPresent() |
isEmpty() (Java 11+) | 값이 비어 있는지 확인 | opt.isEmpty() |
get() | Optional 내부 값 반환 (null일 경우 예외) | opt.get() ❗조심 |
ifPresent(Consumer) | 값이 있을 경우 작업 수행 | opt.ifPresent(System.out::println) |
ifPresentOrElse(Consumer, Runnable) (Java 9+) | 있으면 Consumer 실행, 없으면 Runnable 실행 | opt.ifPresentOrElse(..., ...) |
| 메서드 | 설명 | 예시 |
|---|---|---|
orElse(T other) | 값이 없으면 기본값 반환 | opt.orElse("default") |
orElseGet(Supplier) | 값이 없으면 함수 실행 결과 반환 | opt.orElseGet(() -> createDefault()) |
orElseThrow() | 값이 없으면 예외 발생 | opt.orElseThrow() |
orElseThrow(Supplier) | 값이 없으면 지정 예외 발생 | opt.orElseThrow(() -> new RuntimeException()) |
| 메서드 | 설명 | 예시 |
|---|---|---|
map(Function) | Optional의 값을 변환 | opt.map(String::length) |
flatMap(Function) | 중첩 Optional 평탄화 | opt.flatMap(x -> Optional.of(x.length())) |
filter(Predicate) | 조건 만족하는 경우만 유지 | opt.filter(s -> s.startsWith("a")) |
| 메서드 | 설명 | 예시 |
|---|---|---|
Optional.equals(Object) | Optional 값 비교 | opt1.equals(opt2) |
stream() (Java 9+) | Optional을 Stream으로 변환 | opt.stream() |