JAVA

태태개발일지 - 김영한 java 고급 (람다) 람다 총정리

태태코 2025. 9. 5. 09:58
반응형

람다 총정리

 

람다 VS 익명클래스

 

람다

  • 오직 함수형 인터페이스에만 사용되며 문법이 간결하고 외곽 클래스의 this를 캡처한다. 
  • 람다는 메서드 본문 형태로만 동작하고 별도 클래스 파일을 만들지 않는 컴파일 타임 변환이 이뤄져 경량이다.

 

익명 클래스

  • 인터페이스/추상클래스 구현이나 확장이 가능하고 자체 this를 가진다.
  •  필드, 보조 메서드, 초기화 블록 등을 정의할 수 있어 “작은 클래스”처럼 동작한다. 
  •  this는 내부 클래스의 this를 캡처한다.

 

 

 

사용시기

한 메서드짜리 콜백(Runnable/Callable/Comparator/Listener 등)은 람다,

상태·여러 메서드·명시적 타입 확장·주석/애노테이션이 필요한 경우는 익명 클래스가 적합하다.

 

// Comparator - 람다
var list = List.of("ccc","a","bb");
var sorted = list.stream()
    .sorted((x, y) -> Integer.compare(x.length(), y.length()))
    .toList(); // [a, bb, ccc]

// Comparator - 익명 클래스
var sorted2 = list.stream()
    .sorted(new Comparator<String>() {
        @Override public int compare(String x, String y) {
            return Integer.compare(x.length(), y.length());
        }
    })
    .toList();

 

 

람다의 FunctionalInterface

사실상 람다의 가장 중요한 Point

 Object의 메서드를 제외하고 추상 메서드가 정확히 1개인 인터페이스(SAM). @FunctionalInterface로 의도를 명시할 수 있다.  

 

대표 타입: Predicate<T>, Function<T,R>, Consumer<T>, Supplier<T> 및 Runnable, Callable, Comparator 등 단일 추상 메서드 인터페이스.

 

@FunctionalInterface
interface Op { int apply(int x, int y); } // SAM
Op add = (x, y) -> x + y;
System.out.println(add.apply(2, 3)); // 5

 

 

Stream API란?

 

컬렉션/배열 등 데이터 소스를 선언형으로 처리하는 파이프라인 모델로, 중간 연산을 연결해 최종 연산으로 결과를 만든다.  

불변·무상태·파이프라인 구성·내부 반복을 특징으로 하며, 병렬 처리 용이성과 지연 평가를 제공한다.

 

 

 스트림 메서드(중간/최종) 

 

  • 중간 연산: map, filter, distinct, sorted, limit, skip 등 “Stream을 다시 반환”하여 체이닝 가능. 
  •  최종 연산: forEach, toArray, reduce, collect, min, max, count, anyMatch, allMatch, noneMatch, findFirst, findAny 등 결과를 생성하며 스트림을 소모한다.

 

var names = List.of("Ann","Bob","Andrew","Bill");
long count = names.stream()                // 소스
    .filter(s -> s.startsWith("A"))        // 중간
    .map(String::length)                   // 중간
    .distinct()                            // 중간
    .count();                              // 최종 ->  스트림 소비
System.out.println(count);

 

 

지연연산 vs 즉시연산 

람다의 장점이라고도 볼 수 있는 개념이다.

중간 연산은 “지연(lazy)”: 체인만 쌓고 실행하지 않는다. 최종 연산이 호출될 때 실제로 평가된다. 

최종 연산은 “즉시(eager)”: 스트림을 터뜨리며 결과를 만든다. 한 번 소모되면 재사용 불가.

 

var cnt = List.of(1,2,3,4).stream()
    .peek(x -> System.out.println("peek " + x)) // 지연: count 전까지 출력 안 됨
    .filter(x -> x % 2 == 0)
    .count(); // 여기서 한 번에 실행

 

Optional의 orElse vs orElseGet의 차이로 볼 수 있다.

 

즉시평가

orElse(T) 는 “값을 만들기 위해 전달식이 먼저 평가”될 수 있어 무거운 생성 비용이 낭비될 수 있다.  

 

지연평가

orElseGet(Supplier<? extends T>) 는 “필요할 때만” 공급자를 호출하므로 지연 생성이 가능해 효율적이다.

 

String heavy() { System.out.println("heavy"); return "H"; }

var v1 = Optional.<String>empty().orElse(heavy());      // heavy 호출됨
var v2 = Optional.<String>empty().orElseGet(() -> heavy()); // heavy 필요시 호출

 

Stream API downstream

람다에서 집합단위로 통계를 내거나 연산을 할 경우 사용한다.

 

collect는 결과를 컬렉션(List/Set/Map 등)으로 수집하거나 통계·그룹핑을 수행한다.  

 

다운스트림(downstream) 컬렉터는 groupingBy/partitioningBy의 “하위 수집 로직”으로, 예: 평균, 합계, 리스트 수집 등 조합 가능.  

대표 수집 컬랙션

toList(), toSet(), toMap(k,v), groupingBy(keyFn), partitioningBy(pred), joining, summingInt/averagingInt 등.

 

 

record User(String city, int age) {}
var users = List.of(new User("Seoul",30), new User("Busan",20), new User("Seoul",25));

// 도시별 평균 나이 (downstream: averagingInt)
var avgByCity = users.stream()
    .collect(Collectors.groupingBy(
        User::city,
        Collectors.averagingInt(User::age)
    ));

// 성인/미성년 파티션 후 이름 목록 (downstream: mapping + toList)
var namesByAdult = users.stream()
    .collect(Collectors.partitioningBy(
        u -> u.age() >= 20,
        Collectors.mapping(u -> u.city() + ":" + u.age(), Collectors.toList())
    ));

 

언제 Stream 컬렉션 수집을 쓸까 결과를 재사용·순회·캐싱해야 하면 toList()/toSet() 등으로 컬렉션 수집을 한다.

 즉시 결과(합계, 카운트, 매칭 여부)만 필요하면 reduce/count/anyMatch 같은 최종 연산으로 끝낸다. 

 

실전 팁

선택 기준 “한 메서드짜리 콜백”은 람다, “여러 메서드·상태·this 구분·애노테이션 필요”는 익명 클래스.  

스트림은 가독성이 높을 때만 사용하고, 복잡해지면 단계 분해/메서드 추출로 의도를 드러낸다.

 

 

Optinal의 개념

Optional은 값이 있을 수도 없을 수도 있는 상황을 표현해 NPE를 방지하고, orElse/orElseGet/orElseThrow 등으로 부재 시 정책을 명시한다. 

스트림의 findFirst, max, average 같은 종단연산이 Optional 계열을 반환한다. 

 

 

import java.util.*;

public class OptionalBasics {
    public static void main(String[] args) {
        String raw = null;

        // ofNullable: null 허용
        String name = Optional.ofNullable(raw)
                .orElse("anonymous"); // 즉시 기본값 반환
        System.out.println(name); // anonymous

        // orElseGet: 기본값 계산을 지연
        String heavy = Optional.ofNullable(raw)
                .orElseGet(() -> expensiveCompute());
        System.out.println(heavy);

        // orElseThrow: 반드시 존재해야 할 때
        Optional<String> must = Optional.of("ok");
        String v = must.orElseThrow(() -> new IllegalStateException("missing"));
        System.out.println(v);
    }

    static String expensiveCompute() {
        System.out.println("compute...");
        return "computed";
    }
}

 

 

메서드 레퍼런스

 

메서드 레퍼런스는 람다를 간결하게 치환하는 문법으로, 종류는 정적 메서드 참조(ClassName::staticMethod), 특정 객체 인스턴스 메서드 참조(instance::method), 생성자 참조(ClassName::new), 임의 객체 인스턴스 메서드 참조(ClassName::instanceMethod)가 있다.  

예로 Comparator에서 String::compareToIgnoreCase를 쓰면 (a, b) -> a.compareToIgnoreCase(b)와 같다.

 

정적 메서드 참조 

람다 (x) -> Integer.parseInt(x) 를 Integer::parseInt로 축약해 사용합니다. 

 예: 리스트 문자열을 정수로 매핑할 때 Function<String,Integer> f = Integer::parseInt 처럼 사용한다.

 

List<String> xs = List.of("1","2","3");
List<Integer> ys = xs.stream().map(Integer::parseInt).toList(); // (s) -> Integer.parseInt(s) [6][18]

 

특정 객체의 인스턴스

 메서드 참조 이미 가진 객체의 메서드를 참조한다.: greeter::greet 는 name -> greeter.greet(name)과 동일하다.

class Greeter { void greet(String n){ System.out.println("Hi " + n); } }
Greeter g = new Greeter();
List<String> names = List.of("A","B");
names.forEach(g::greet); // s -> g.greet(s) [9][6]

 

생성자 참조

 new를 가리킨다: Supplier<List<String>> s = ArrayList::new 처럼 무인자 생성자를 공급하거나, 매개변수 시그니처가 맞는 팩토리 함수형 인터페이스에 연결한다.

Supplier<ArrayList<String>> makeList = ArrayList::new; // () -> new ArrayList<>() [5]
interface EmpFactory { Employee create(String n, Integer a); } // SAM
EmpFactory f = Employee::new; // (n,a) -> new Employee(n,a) [7]

 

특정 타입의 임의 객체 인스턴스 메서드 참조 

컬렉션 원소(해당 타입)의 메서드를 호출하는 람다를 축약한다: (a,b) -> a.compareToIgnoreCase(b) 를 String::compareToIgnoreCase로.

 

String[] arr = {"b","A","c"};
Arrays.sort(arr, String::compareToIgnoreCase); // (a,b) -> a.compareToIgnoreCase(b) [5][6]

 

 

import java.util.*;
import java.util.function.*;

class Greeter {
    private final String name;
    Greeter() { this("World"); }
    Greeter(String name) { this.name = name; }
    static String helloStatic() { return "Hello, static!"; }
    String greet(String who) { return "Hello, " + who; }
    String greetSelf() { return "Hello, " + name; }
}

public class MethodRefDemo {
    public static void main(String[] args) {
        // 정적 메서드 참조
        Supplier<String> s = Greeter::helloStatic;
        System.out.println(s.get());

        // 특정 객체의 인스턴스 메서드 참조
        Greeter g = new Greeter();
        Function<String, String> f = g::greet;
        System.out.println(f.apply("Java"));

        // 생성자 참조
        Function<String, Greeter> ctor = Greeter::new;
        System.out.println(ctor.apply("Kotlin").greetSelf());

        // 임의 객체의 인스턴스 메서드 참조
        String[] arr = {"c", "A", "b"};
        Arrays.sort(arr, String::compareToIgnoreCase);
        System.out.println(Arrays.toString(arr));
    }
}

 

 

인터페이스의 default 메서드

 

default 메서드는 인터페이스에 메서드의 기본 구현을 제공해, 기존 구현체를 깨뜨리지 않고 새 기능을 추가할 수 있게 한다(바이너리/소스 호환에 유리). 

Object의 메서드(equals/hashCode 등)는 default로 제공 불가하다.  

구현 클래스는 default를 재정의할 수 있고, 상속받는 인터페이스는 이를 다시 추상화하거나 재정의할 수 있다. 문서화 시 @implSpec로 계약을 명확히 하는 것이 권장된다.

 

interface PagingSupport {
    int pageSize();

    // @implSpec 기본 구현은 페이지 크기를 100으로 제한해 반환한다.
    default int cappedPageSize() {
        int p = pageSize();
        return p > 100 ? 100 : p;
    }

    // 정적 메서드도 함께 제공 가능
    static boolean isValid(int size) {
        return size > 0;
    }
}

class SearchService implements PagingSupport {
    private final int sz;
    SearchService(int sz) { this.sz = sz; }
    @Override public int pageSize() { return sz; }

    // 필요하면 재정의
    @Override public int cappedPageSize() {
        return Math.min(pageSize(), 200);
    }
}

public class DefaultMethodDemo {
    public static void main(String[] args) {
        PagingSupport svc = new SearchService(300);
        System.out.println(svc.cappedPageSize()); // 200 (재정의 동작)

        System.out.println(PagingSupport.isValid(10)); // true
    }
}
반응형