[JAVA] 람다
by 핫도구람다는 Java 8에서 도입된 핵심 기능으로, 함수형 프로그래밍을 가능하게 하는 익명함수이다. 메서드를 하나의 식으로 표현할 수 있게 해주며, 코드를 더 간결하고 읽기 쉽게 만들어준다.
람다를 필요한 이유
람다를 사용하면 바로 앞에서 이야기 했지만, 코드를 훨씬 간결하고 읽기 쉽게 만들어 준다. 이렇게 이야기하기 보다 아래처럼 코드로 보는 것이 훨씬 보기 좋다.
// Java 8 이전 - 익명 클래스 사용
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("실행");
}
});
// Java 8 이후 - 람다 사용
Thread thread = new Thread(() -> System.out.println("실행"));
이처럼 람다를 사용하면 훨씬 간결하게 사용할 수 있는 것을 보여준다.
람다식의 기본 문법
람다식은 (매개변수) -> {실행문} 형태로 작성되며 매개변수와 실행코드를 화살표로 구분하는 것이 특징이다.
// 기본 형태
(int a, int b) -> { return a + b; }
// 타입 추론 가능
(a, b) -> { return a + b; }
// 한 줄일 경우 중괄호와 return 생략 가능
(a, b) -> a + b
// 매개변수가 하나면 괄호 생략 가능
a -> a * 2
// 매개변수가 없을 경우
() -> System.out.println("Hello")
함수형 인터페이스
람다식을 사용하려면 함수형 인터페이스가 필요하다. 함수형 인터페이스는 단 하나의 추상 메서드만 가진 인터페이스를 말하며, @FunctionalInterface 애노테이션을 사용하면 컴파일러가 검증해준다.
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b); // 추상메서드는 1개만 가능
default void print() {
System.out.println("계산기"); // default 메서드는 여러개 가능
}
static void info() {
System.out.println("정보"); // static 메서드는 여러개 가능
}
}
public class LamdaMain {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
Calculator subtract = (a, b) -> a - b;
Calculator divide = (a, b) -> a / b;
System.out.println(add.calculate(5, 3)); // 8
System.out.println(multiply.calculate(4, 6)); // 24
}
}
이런식으로 2개의 파일로 나누어 애노테이션을 사용해 처리할 수 있다. 물론, 애노테이션 없이도 람다식 사용이 가능하지만, 컴파일 시점에 실수를 방지해주는 굉장히 큰 영향을 미치기 때문에 써주는 것을 추천한다.
Java 표준 함수형 인터페이스
java.util.function 패키지에 자주 사용되는 함수형 인터페이스들을 미리 정의해 두어 매번 인터페이스를 만들 필요 없이 활용할 수 있다. 또한, 코드의 일관성과 가독성 때문이며(개발자 마다 인터페이스, 메서드 이름이 다름), 강력한 기본 메서드 제공하기 문에 아래의 인터페이스들을 알고 활용할 수 있어야 한다.
1. Predicate<T> - 조건 검사
T를 받아서 boolean을 반환하며 주로 필터링에 사용된다.
public class PredicateExample {
public static void main(String[] args) {
// 기본 사용
Predicate<Integer> isPositive = num -> num > 0;
System.out.println(isPositive.test(5)); // true
System.out.println(isPositive.test(-3)); // false
// 문자열 검사
Predicate<String> isEmpty = str -> str.isEmpty();
System.out.println(isEmpty.test("hello")); // false
Predicate<String> isLongString = str -> str.length() > 10;
System.out.println(isLongString.test("hello")); // false
// 메서드 체이닝
Predicate<Integer> isEven = num -> num % 2 == 0;
Predicate<Integer> isPositiveAndEven = isPositive.and(isEven);
System.out.println(isPositiveAndEven.test(4)); // true
System.out.println(isPositiveAndEven.test(-4)); // false
}
}
Predicate<Integer> isPositiveAndEven = isPositive.and(isEven)에서 and()를 포함해서 or(), negate() 메서드가 존재한다. and()는 두 조건을 만족 해야 true를 반환하고 or()은 하나만 만족해도 true를 반환하며 negate()는 조건을 반대로 뒤집는 다는 뜻이다. 이를 제외한다면 코드를 이해하기에 어렵지 않다.
2. Function<T, R> - 변환 / 매핑
T를 받아서 R로 변환한다. 데이터 변환에 사용된다.
public class FunctionExample {
public static void main(String[] args) {
// 기본 사용
Function<String, Integer> strLength = str -> str.length();
System.out.println(strLength.apply("Hello")); // 5
// 객체 변환
Function<String, User> createUser = name -> new User(name);
User user = createUser.apply("홍길동");
// 함수 합성 - andThen
Function<Integer, Integer> multiplyBy2 = num -> num * 2;
Function<Integer, String> toString = num -> "결과: " + num;
Function<Integer, String> combined = multiplyBy2.andThen(toString);
System.out.println(combined.apply(5)); // "결과: 10"
// 함수 합성 - compose (역순)
Function<String, Integer> parseAndDouble = multiplyBy2.compose(Integer::parseInt);
System.out.println(parseAndDouble.apply("10")); // 20
}
}
Function<Integer, String> combined = multiplyBy2.andThen(toString)와 Function<String, Integer> parseAndDouble = multiplyBy2.compose(Integer::parseInt)가 존재한다. 여기에서 andThen()과 compose()이 존재하는데 andThen()은 A → B → C 순서로 실행되며 compose()는 C → B → A처럼 역순으로 실행되는 메서드이다.
3. Consumer<T> - 변환값 없음
T를 받아서 처리하지만 반환값이 없으며 출력, 저장 등에서 사용된다.
public class ConsumerExample {
public static void main(String[] args) {
// 기본 사용
Consumer<String> printer = str -> System.out.println(str);
printer.accept("Hello Consumer!"); // Hello Consumer!
// 객체 수정
Consumer<List<String>> addElement = list -> list.add("새 요소");
List<String> myList = new ArrayList<>();
addElement.accept(myList);
System.out.println(myList); // ["새 요소"]
// 체이닝 - andThen
Consumer<String> printUpperCase =
str -> System.out.println(str.toUpperCase());
Consumer<String> printLength =
str -> System.out.println("길이: " + str.length());
Consumer<String> combined = printUpperCase.andThen(printLength);
combined.accept("hello"); // HELLO, 길이: 5
// forEach와 함께 사용
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(num -> System.out.print(num + " ")); // 1 2 3 4 5
}
}
4. Supplier<T> - 매개변수 없음
매개변수 없이 T를 반환하며 값 생성, 팩토리 패턴에 사용된다.
public class SupplierExample {
public static void main(String[] args) {
// 기본 사용
Supplier<Double> randomNumber = () -> Math.random();
System.out.println(randomNumber.get());
// 현재 시간 제공
Supplier<LocalDateTime> now = () -> LocalDateTime.now();
System.out.println(now.get()); // 2025-09-19T21:53:19.534352800
// 객체 생성 팩토리
Supplier<User> userFactory = () -> new User("기본 사용자");
User user1 = userFactory.get();
User user2 = userFactory.get(); // 새로운 객체
// Lazy Evaluation (지연 평가)
Supplier<String> expensiveOperation = () -> {
System.out.println("비용이 큰 작업 실행");
return "결과값";
};
// 필요할 때만 실행
boolean someCondition = true;
if (someCondition) {
String result = expensiveOperation.get(); // 비용이 큰 작업 실행
}
}
}
여기에서 Lazy Evaluation은 개인적으로 stream()에서 봤었던 것인데 값이 실제로 필요한 시점까지 계산을 미루는 방법으로 코드를 정의할 때가 아니라 실제로 사용될 때 실행된다. 그래서 get()메서드가 호출되는 시점에 실행되기 때문에 조건에 따라서 실행 자체가 안될 수 있으며 이를 통해 당장 필요 없는 객체를 미리 생성하지 않기 때문에 메모리 효율성이 증대된다.
그래서 이들을 정리하면 아래의 표와 같이 된다. Predicate는 조건 판단이 필요한 경우(true, false), Function은 A를 B로 변환할 경우, Consumer는 반환값 없이 처리만 할 경우, Supplier는 값을 생성 및 제공할 경우에 사용된다고 생각하면 된다. 이를 간단하게 정리한 표가 아래와 같다.
| 인터페이스 | 입력 | 출력 | 메서드 | 사용 예 |
| Predicate<T> | T | boolean | test() | 필터링 |
| Function<T,R> | T | R | apply() | 변환 |
| Consumer<T> | T | void | accept() | 출력, 저장 |
| Supplier<T> | 없음 | T | get() | 생성 |
| 람다식의 장단점 | |||
| 장점 | 단점 | ||
| 코드 간결성 | 불필요한 코드를 제거하여 핵심 로직만 표현 | 디버깅 어려움 | 스택 트레이스가 복잡하게 중단점 설정이 까다로움 |
| 가독성 향상 | 의도가 명확하게 드러나 코드 이해가 쉬움 | 재사용 불가 | 같은 로직이 여러 곳에 필요하면 중복 작성 |
| 함수형 프로그래밍 | 함수를 일급 객체로 다룰 수 있어 유연한 프로그래밍 가능 | 복잡한 로직 부적합 | 여러 줄의 복잡한 로직은 오히려 가독성 저하 |
| stream API 활용 | Collection 처리를 함수형으로 처리 | 성능 오버헤드 | 런타임에 익명 클래스 새엇ㅇ 가능성 |
| 병렬 처리 용이 | parallelStream()으로 쉽게 병렬 처리 전환 | 메모리 사용 | 클로저로 인한 외부 변수 캡처시 메모리 누수 가능성 |
| 지연 실행 | 필요한 시점에만 실행되어 성능 최적화 가능 | 타입 추론 한계 | 컴파일러가 타입을 추론 못하는 경우 발생 |
| 유지보수성 | 비즈니스 로직에 집중할 수 있어 유지보수 용이 | 과도한 사용 | 모든 곳에 람다를 쓰면 오히려 복잡도 증가 |
| 테스트 용이성 | 동작을 매개변수로 전달하여 테스트 코드 작성이 쉬움 | ||
그래서 람다는 적절히 사용하면 코드 품질을 크게 향상시키지만, 무분별한 사용은 오히려 독이 될 수 있기 때문에, 상황에 맞게 사용해야 한다.
메서드 참조(::)
메서드 참조는 람다식을 더 간결하게 표현하는 방법으로, 이미 존재하는 메서드를 참조할 때 사용한다.
1. 기본 개념
// 람다식과 메서드 참조 비교
Function<String, Integer> lambda = s -> Integer.parseInt(s);
Function<String, Integer> methodRef = Integer::parseInt; // 동일한 기능!
람다도 기존의 기능에서는 굉장히 간결하게 표현할 수 있었지만 메서드참조를 사용하면 람다보다도 더 간단하게 표현할 수 있다.
또한, 정적메서드 참조, 인스턴스 메서드 참조, 특정 타입의 임의 객체 메서드 참조, 생성자 참조 4가지 유형을 나누어 실행할 수 있다.
public class MethodReferenceTypes {
public static void main(String[] args) {
// 1. 정적 메서드 참조 (ClassName::staticMethod)
Function<String, Integer> parser = Integer::parseInt;
// 2. 인스턴스 메서드 참조 (instance::instanceMethod)
String str = "Hello";
Supplier<Integer> lengthSupplier = str::length;
// 3. 특정 타입의 임의 객체 메서드 참조 (ClassName::instanceMethod)
Function<String, String> toUpper = String::toUpperCase;
// 4. 생성자 참조 (ClassName::new)
Supplier<List<String>> listFactory = ArrayList::new;
Function<Integer, List<String>> sizedListFactory = ArrayList::new;
}
}
| 람다가 더 좋은 경우 | 예시 | 메서드 참조가 더 좋은 경우 | 예 |
| 단순한 메서드 호출 | list.removeIf(s -> s.length() > 5) | 추가 로직 필요 | list.forEach(System.out::println) |
| 단순한 변환 | list.forEach(s -> logger.info("처리: " + s) | 문자열 조합 | list.stream().map(String::length) |
| 단순한 비교 | map.forEach((k, v) -> process(k, v, 10)) | 추가 매개변수 | list.sort(Integer::compareTo) |
여기까지 람다에 대해 알아봤고 다음에는 stream에 대해 알아볼 예정이다.
'JAVA > MID' 카테고리의 다른 글
| [JAVA] Stream(2) (1) | 2025.09.26 |
|---|---|
| [JAVA] Stream(1) (0) | 2025.09.24 |
| [JAVA] Optional (0) | 2025.09.17 |
| [JAVA] 제네릭(Generic) (0) | 2025.09.12 |
| [JAVA] 예외처리(2) (0) | 2025.09.11 |
블로그의 정보
AquaMan
핫도구