[Java] Lambda & Stream

허경두·2025년 4월 14일

Java

목록 보기
12/12

람다식

람다식(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; // 이렇게 함수형 인터페이스를 정의하고 람다식을 받아야한다

java.util.function 패키지

일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓은 패키지

기본 함수형 인터페이스

인터페이스추상 메서드 시그니처역할 / 설명예시 람다식
Function<T, R>R apply(T t)입력 T → 반환 Rx -> x + 1
BiFunction<T, U, R>R apply(T t, U u)입력 (T, U) → 반환 R(x, y) -> x + y
UnaryOperatorT apply(T t)Function의 특수형 (T → T)x -> x * 2
BinaryOperatorT apply(T t1, T t2)BiFunction 특수형 (T, T → T)(x, y) -> x + y
Predicateboolean test(T t)조건 검사 (true/false 반환)x -> x > 10
BiPredicate<T, U>boolean test(T t, U u)두 인자 조건 검사(a, b) -> a.equals(b)
Consumervoid accept(T t)값을 소비 (출력 등, 반환 없음)x -> System.out.println(x)
BiConsumer<T, U>void accept(T t, U u)두 인자 소비(a, b) -> System.out.println(a + b)
SupplierT get()값 제공 (입력 없음)() -> "hello"

기본형을 사용하는 함수형 인터페이스

인터페이스추상 메서드 시그니처설명예시 람다식
IntFunctionR apply(int value)int → Ri -> i * 2.0
ToIntFunctionint applyAsInt(T t)T → intstr -> str.length()
IntPredicateboolean test(int value)int 값에 대한 조건 검사i -> i > 0
LongSupplierlong getAsLong()long 값 제공() -> 123L
BooleanSupplierboolean getAsBoolean()boolean 제공() -> true
IntConsumervoid 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

java.util.function 함수형 인터페이스의 static 메서드와 default 메서드

  • 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 > 10x -> 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 등 이러한 함수형 인터페이스들도 defaultstatic 메서드를 지원하지만 그 내용은 대부분 위의 Function 인터페이스의 패턴을 따른다

java.util.function 패키지의 static 메서드는 주로 기본적인 기능을 제공하거나 특정 객체를 쉽게 생성할 수 있도록 도와준다
default 메서드는 기존의 메서드 체이닝을 확장할 수 있도록 도와주며 이를 통해 함수형 인터페이스의 재사용성을 높여준다

스트림

스트림(Stream) : 데이터의 흐름(Flow) 을 추상화한 것으로, 데이터 소스를 기반으로 파이프라인 형태의 처리를 지원하는 API이다
→ 데이터 소스를 추상화했기 때문에 데이터 소스가 무엇이든 간에 같은 방식으로 다룰 수 있다
→ 코드의 재사용성이 높아진다

특징

  1. 데이터 소스를 변경하지 않는다
  2. 일회용이다
    → 한번 사용하면 닫혀서 다시 사용할 수 없다 (재사용 시 다시 생성)
  3. 작업을 내부 반복으로 처리한다
  4. 지연된 연산을 수행한다
    → 최종 연산이 수행되기 전까지 중간 연산이 수행되지 않는다
  5. 병렬 연산이 가능하다
    parallel() 을 호출해서 병렬 스트림으로 전환할 수 있다 (내부적으로 fork & join framework 사용)

스트림 생성

Collections

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()의 사용

  1. 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인 그룹으로 나눠진다

  2. 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]}

    → 각 문자열의 길이로 그룹화한다

Collector 인터페이스

  • Java Stream API에서 collect() 메서드에 사용되는 수집 전략을 정의하는 인터페이스
  • 스트림의 요소를 가공하여 리스트, 집합, 맵 등으로 모으거나 요약할 때 사용

제네릭 정의

타입 매개변수설명
T입력 요소의 타입 (Stream으로 처리되는 요소의 타입)
A누적할 자료 구조의 타입 (중간 컨테이너)
R최종 반환 결과의 타입

주요 메서드

메서드반환 타입설명
supplier()Supplier<A>결과를 저장할 새로운 컨테이너를 생성
accumulator()BiConsumer<A, T>스트림의 요소를 컨테이너에 수집
combiner()BinaryOperator<A>병렬 처리 시 두 컨테이너를 병합
finisher()Function<A, R>컨테이너를 최종 결과로 변환
characteristics()Set<Collector.Characteristics>Collector의 특성 집합 반환

Collectors.Characteristics

상수설명
CONCURRENT병렬 처리 가능한 수집기. 이 특성이 있으면 Collector는 병렬 스트림에서 안전하게 사용할 수 있습니다.
UNORDERED요소의 순서가 중요하지 않음을 나타냅니다. 이 특성이 있으면, 수집 결과의 순서가 스트림의 순서와 다를 수 있습니다.
IDENTITY_FINISHfinisher() 함수가 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

Optional<T> 는 T 타입의 객체를 감싸는 래퍼 클래스이다

Optional 클래스의 메서드

생성 관련

메서드설명예시
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"))

비교/기타 (Java 10+ 일부)

메서드설명예시
Optional.equals(Object)Optional 값 비교opt1.equals(opt2)
stream() (Java 9+)Optional을 Stream으로 변환opt.stream()

0개의 댓글