람다 총정리
람다 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
}
}'JAVA' 카테고리의 다른 글
| 태태개발일지 - JAVA 간단한 network programming (1) | 2025.12.13 |
|---|---|
| 태태개발일지 - Java 파일 입출력 완전 정리 (0) | 2025.12.01 |
| 태태개발일지 - 김영한 java 고급 디폴트 메서드 (3) | 2025.08.11 |
| 태태코딩 - 김영한 고급 java 람다 (1) | 2025.07.29 |
| 태태개발일지 - 김영한 고급 JAVA LAMBDA (2) | 2025.07.28 |