
이전 포스팅에서 이어집니다.
[java 기초] 람다와 스트림, 스트림의 이해와 생성(1 / 3)
Table of Contents 주의 :: 주인장 학습용 포스팅입니다. Stream API : 이론 학습에 어질어질한 파트일 수 밖에 없습니다.. Stream API : JDK 8때 도입되어 객체지향적 언어인 Java를 함수형으로 프로그래밍 할
doinitright.tistory.com
람다식

람다식을 알기 전 학습해야 할 선수 지식
익명 클래스 : 인터페이스에 대한 익명 클래스 구현 방법
[java 기초] 중첩 클래스(Nested Class)의 이해와 구현
Table of Contents 같이 보면 좋은 글 (static 개념의 이해) [java 기초] 기타 제어자(static, final, abstract)의 이해와 구현 Table of Contents 주인장 학습용 포스팅입니다. 열람에 참고해주세요 [java 기초] java final
doinitright.tistory.com
함수형 인터페이스 : 함수형 인터페이스의 조건과 어노테이션을 활용한 명시 방법
[java 기초] 어노테이션(Annotation)의 이해와 사용방법
Table of Contents java 어노테이션이란? Annotation : 주석이라는 사전적 의미를 가지고 있습니다. java에서 어노테이션은 @를 붙여 코드에서 주석처럼 쓰이며, 동시에 컴파일 및 실행 시 여러 기능을 제공
doinitright.tistory.com
람다식의 이해
람다식은 함수를 하나의 식으로 표현하는 것입니다.
함수란 값의 입력과 출력 사이의 값의 중간 처리를 위한 일정한 연산 과정입니다.
ex ) 함수 `f(x) = 2x + 3`이라고 가정 시 `입력값 2 -> 함수 -> 출력 값 2 * 2 + 3 = 7`
람다식은 객체지향 프로그래밍 언어인 Java에서 이런 함수형 프로그래밍과 연산을 지원하는 기능입니다.
람다식의 특징과 장단점
- 람다식의 특징
- 람다식은 함수로써 메서드의 이름을 정의하지 않고 메서드와 같은 연산 기능을 합니다.
- 람다식은 익명 클래스의 한 종류로써 함수형 인터페이스를 참조하는 형태로 사용합니다.
- 람다식은 함수를 값으로 취급할 수 있어 함수를 다른 함수로 전달하거나 다른 함수에서 반환할 수 있습니다.
- java 컴파일러는 람다식에서 매개변수의 타입을 추론할 수 있습니다. (특수한 경우 제외 명시적 선언 불필요)
- 람다식에서 사용된 지역변수는 final이거나 final처럼 작동해야 합니다. (익명 클래스의 특성)
- 람다식의 장점
- 간결하고 가독성 있는 코드를 만들 수 있습니다.
- 병렬 프로그래밍에 용이합니다.
- 함수를 선언하고 상속하는 기타 과정 없이 한 줄로 처리할 수 있습니다.
- 선언한 람다식은 변수 형태로써 저장되어 재사용이 가능합니다.
- 람다식의 단점
- 람다식의 난발은 코드를 어지럽게 합니다.
- 디버깅이 어려우며, 재귀에는 부적합합니다.
뭐든지 상황에 따라 필요에 맞는 방법을 사용하는 것이 좋다고 하지만,
요즘 실무는 다 람다를 쓸 줄 안다고 하니 반드시 학습하고 읽을 줄 알아야겠습니다.
람다식과 익명 클래스 비교
람다식은 익명 클래스와 동일한 기능을 하나, 이를 간결하게 표현한 식입니다.
익명 클래스와 람다식의 코드 비교를 통해 어떻게 차이가 나는지 확인하겠습니다.
익명 클래스
// 인터페이스의 메소드를 일회적으로 구현한 익명 클래스 // 십의 자리 수 x, 일의 자리 수 y로 이루어진 두자리수를 만든다. Par2 ex1 = new Par2() { @Override public int exec(int x, int y) { return x * 10 + y; } };
익명 클래스는 해당 인터페이스 변수를 선언하고,
이를 오버라이딩해 새로운 메소드를 정의하고 return 문을 작성하는 형태입니다.
람다식
// 익명 클래스 내용을 동일하게 람다식으로 구현한 것 // 함수형 인터페이스 변수 = 람다식; 형태로 구현한다. Par2 ex2 = (x, y) -> x * 10 + y;
반면, 람다는 구현 내용이 동일하지만 한 줄로 종결됩니다. (강력하다!)
익명 클래스와 람다식 실행 결과
// 호출 및 실행 결과 int a = 5, b = 3; System.out.println(ex1.exec(5, 3)); // 익명 클래스 // out : 53 System.out.println(ex2.exec(5, 3)); // 람다식 // out : 53
그러면 본격적으로 람다식을 사용하는 방법에 대해 알아보겠습니다.
람다식의 사용 방법
람다식의 사용전제
- JAVA 버전 8 이상에서 지원합니다.
- 람다식은 함수형 인터페이스를 참조해 메서드를 재정의하는 것이므로 함수형 인터페이스가 정의되어 있어야 합니다.
// int 입력 인자 값이 1개이며, int를 리턴하는 함수형 인터페이스 @FunctionalInterface public interface Par1 { int exec(int x); }
람다식의 기본 문법
람다식은 매개변수 입력인자 `( )` , 연산이 진행됨을 명시하는 애로우 토큰 `->` , 결괏값인 바디 `{ }`로 구성됩니다.
1. 람다식의 기본 형태
/** * 입력 인자 값이 int 1개인 함수형 인터페이스 * 결과 값을 int로 리턴한다 */ @FunctionalInterface public interface Par1 { int exec(int x); }
// 람다식은 매개변수 리스트 (), 애로우 토큰 ->, 바디 { } 3요소로 구성 Par1 ex3 = (int x) -> { return x + 10; };
- ( ) : 입력할 매개변수를 입력하는 공간입니다.
- -> : 해당 매개변수가 람다식으로 처리됨을 컴파일러에게 알립니다.
- { } : return을 포함한 매개변수를 어떻게 처리할 지 결괏값을 작성합니다.
2. 람다식의 중괄호 { } 생략이 가능한 경우
/** * 입력 인자 값이 int 1개인 함수형 인터페이스 * 결과 값을 int로 리턴한다 */ @FunctionalInterface public interface Par1 { int exec(int x); }
// 바디가 한 문장인 경우는 중괄호 { } 생략 가능 // 바디가 두 문장 이상인 경우는 return 문과 중괄호 모두 사용해야 한다 Par1 ex4 = (int x) -> x + 10; // 단, 람다식 내에 return 문이 있으면 반드시 중괄호로 감싸야 한다 Par1 ex5 = (int x) -> return x + 10; // error
- 람다식의 중괄호 { } 는 바디가 한 문장으로만 구성되어 있는 경우 생략이 가능합니다.
- 그러나 return 문으로 그 결과 값을 표기할 시에는 반드시 중괄호로 { } 감싸주어야 에러가 발생하지 않습니다.
3. 람다식의 입력 인자 ( ) 내에 타입 생략이 가능한 경우
/** * 입력 인자 값이 int 1개인 함수형 인터페이스 * 결과 값을 int로 리턴한다 */ @FunctionalInterface public interface Par1 { int exec(int x); }
Par1 ex6 = (x) -> x + 10;
// 단, 두 개 이상의 인자값을 받는 람다식의 경우 특정 매개변수 타입만 생략은 불가능하다 Par2 ex7 = (int x, y) -> x + y; // error : y도 int로 선언하던지 둘다 생략하던지 해야 함
- 위 예제는 입력 변수 타입인 int를 명시하지 않았지만 오류가 발생하지 않습니다.
- 이처럼 java 람다식은 입력 매개변수의 타입을 추론하는 기능을 가지고 있습니다.
- 이는 함수형 인터페이스를 통해 입력인자의 타입을 추론하기 때문입니다.
- 단, 2개 이상의 입력 인자를 입력하는 람다식은 입력 인자 중 한 개 인자의 타입만 생략하는 것은 불가능합니다.
4. ( ) 자체의 생략이 가능한 경우
/** * 입력 인자 값이 int 1개인 함수형 인터페이스 * 결과 값을 int로 리턴한다 */ @FunctionalInterface public interface Par1 { int exec(int x); }
// 인자값이 한개일 때 괄호의 생략이 가능하다 Par1 ex8 = x -> x + 10;
- 인자값이 한개일 때 괄호의 생략이 가능합니다.
5. 매개변수가 없는 람다식
/** * 인자 값이 없는 함수형 인터페이스 * 결과 값은 void로 출력하는 메소드를 구현한다 */ @FunctionalInterface public interface Par0 { void print(); }
// 매개변수가 없는 람다식 Par0 ex9 = () -> { System.out.println("LambdaTest.main"); System.out.println("LambdaLambdaLambda"); }; ex9.print(); // out : // "LambdaTest.main" // "LambdaLambdaLambda"
- 매개변수가 없는 람다식을 작성할 시에는 ()을 비워놓고 작성하면 됩니다.
6. 제네릭 타입의 람다식
/** * 입력 인자 값이 1개, 제네릭 타입 T를 가진 함수형 인터페이스 * 결과 값은 void로 출력하는 메소드를 구현한다 */ @FunctionalInterface public interface Gen1<T> { void print(T x); }
// 제네릭 타입의 람다식 Gen1<String> ex10 = x -> System.out.println(x); ex10.print("hello"); // out : hello Gen1<Integer> ex11 = x -> System.out.println(x); ex11.print(12345); // out : 12345
위 예제는 제네릭 타입의 함수형 인터페이스를 선언함으로써 매개변수 타입을 유동적으로 정의할 수 있음을 보여줍니다.
7. 람다식의 형변환
/** * 인자 값이 없는 함수형 인터페이스 * 결과 값은 void로 출력하는 메소드를 구현한다 */ @FunctionalInterface public interface Par0 { void print(); }
// 람다식의 형변환 // 람다식은 익명 객체이므로 타입이 없다 (함수형 인터페이스로 람다식을 참조할 뿐) Par1 ex12 = (abc) -> abc + 1; // 이런 특징을 활용해 최상위 클래스인 Object로 업캐스팅이 가능하다 // 그 과정에서 함수형 인터페이스로 반드시 변환해야 한다 Object ex14 = (Object)(Par1) abc -> abc + 1;
- 함수형 인터페이스로 람다식을 참조할 수 있지만, 람다식 타입이 함수형 인터페이스 타입과 일치하는 것은 아닙니다.
- 람다식은 익명 객체이고 익명 객체는 타입이 없습니다.
- 정확히는 타입이 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없습니다.
// 람다식은 인터페이스를 직접 구현하지 않았지만, // 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 아래와 같은 형변환을 허용한다 // 그리고 이 형변환은 생략 가능하다 // 아래는 명시적 형변환을 표현한 것 // (모든 인터페이스 타입 변수에 람다식 할당 행위는 캐스팅 연산자가 생략되어 있음) Par1 ex13 = (Par1)(abc) -> abc + 1;
위는 람다식이 형변환 과정을 거친다는 것을 보여주기 위해, 명시적 형변환으로 표시해 놓은 것입니다.
- 람다식은 인터페이스를 직접 구현하지는 않지만 인터페이스를 구현한 익명 클래스의 객체와 동일합니다.
- 형변환은 생략이 가능합니다.
// 이런 특징을 활용해 최상위 클래스인 Object로 업캐스팅이 가능하다 // 그 과정에서 먼저 함수형 인터페이스로 반드시 변환해야 한다 Object ex14 = (Object)(Par1) abc -> abc + 1;
위는 1. 함수형 인터페이스로 명시적 형변환 → 2. Object 클래스로 형변환 하는 과정을 설명합니다.
8. 람다식의 메소드 참조(method reference)
// 람다의 메소드 참조 Par1 ex15 = Math::abs; // Math.abs(int a) 를 간략화 System.out.println(ex15.exec(-13)); // out : -13 Par2 ex16 = Integer::max; // Integer.max(int a, int b)를 간략화 System.out.println(ex16.exec(13,15)); // out : 15 Gen1<String> ex17 = System.out::println; // Sysout(String)을 간략화 ex17.print("Hello!"); // out : "Hello!"
람다식은 함수형 인터페이스의 추상 메서드를 구현하는 기능이므로, 기존의 구현된 메서드를 참조할 수 있습니다.
위의 예제는 클래스의 static 메소드를 람다식이 참조하는 과정을 나타냅니다.
- 람다식이 메소드를 참조해 사용하기 위한 조건
- 참조할 메소드와 람다식으로 구현하는 추상 메서드 시그니처가 일치해야 합니다.
- 시그니처 : 메소드의 이름, 매개변수 유형, 개수 및 반환 유형
- 참조할 메소드와 람다식으로 구현하는 추상 메서드 시그니처가 일치해야 합니다.
- 메소드메서드 참조 문법 : `클래스명(혹은 객체명) :: 호출 메서드명` 의 형태로 사용됩니다.
java.util.function 패키지
java.util.function 패키지는 자바에서 제공되는 표준 함수형 인터페이스 패키지로, 스트림 연산에 빈번하게 사용됩니다.

사실 java.util.function 패키지에 대한 이야기를 하고 싶어 위에서부터 장황하게 람다식에 대해 설명했습니다.
스트림의 연산 과정에서 사용되는 것은 람다이며, 람다를 위해서는 함수형 인터페이스가 정의되어야 합니다.
java가 가장 빈번하게 사용되는 함수형 인터페이스를 사전에 모아놓은 패키지가 function 패키지입니다.
따라서 람다와 스트림에서 가장 빈번하게 사용되는 자바 function 패키지에 대한 이해가 필요합니다.
function 패키지 사용을 위해 import 해줍시다.
import java.util.function.*;
java.util.function 패키지의 대표적인 인터페이스 종류
java.util.function의 인터페이스로는 대표적으로 아래 5가지가 있습니다.
구분 기준은 인터페이스의 선언된 추상 메소드의 매개값과 리턴값의 유형입니다.
인터페이스 종류
종 류 | 추상메소드 특징 | 비 고 |
Consumer | 매개변수 O, 리턴 값 X | |
Supplier | 매개변수 X, 리턴 값 O | |
Function | 매개변수 O, 리턴 값 O | 주로 매개값을 리턴값으로 매핑(타입 변환) 하는 역할 |
Operator | 매개변수 O, 리턴 값 O | 주로 매개값을 연산하고 결과를 리턴하는 역할 |
Predicate | 매개변수 O, 리턴 값 O | boolean 타입으로 리턴값이 한정, 조건식 역할 |
Consumer
Consumer는 리턴 값이 없는 accept() 메서드를 구현하는 함수형 인터페이스입니다.
입력값이 있으며, 리턴 값이 없으므로 입력한 매개변수를 '소비' 하는 메서드를 구현해야 합니다.
인터페이스 구성
종 류 | 추상 메소드 | 비 고 |
Consumer<T> | void accept(T t) | 타입 T의 객체 t를 받아 소비 |
BiConsumer<T, U> | void accept(T t, U u) | 타입 T의 객체 t, 타입 U의 객체 u를 받아 소 |
DoubleConsumer | void accept(double value) | double value 값을 받아 소비 |
intConsumer | void accept(int value) | Int value 값을 받아 소비 |
LongConsumer | void accept(long value) | long value 값을 받아 소비 |
ObjDoubleConsumer<T> | void accept(T t, double value) | 타입 T의 객체 t, double value를 받아 소비 |
ObjIntConsumer<T> | void accept(t t, int value) | 타입 T의 객체 t, int value를 받아 소비 |
ObjLongConsumer<T> | void accept(T t, long value) | 타입 T의 객체 t, long value를 받아 소비 |
구현 예시
import java.util.function.*; public class FunctionTest { public static void main(String[] args) { // Consumer<T> : 타입 T의 객체 t를 받아 소비 Consumer<String> cs1 = t -> System.out.println(t + " World!"); cs1.accept("Hello"); // out : "Hello World!" // BiConsumer<T, U) : 타입 T의 객체 t,타입 U의 객체 u를 받아 소비 BiConsumer<String, String> cs2 = (t, u) -> System.out.println(t + " " + u); cs2.accept("Hello", "World!"); // out : "Hello World!" // intConsumer : int value 값을 받아 소비 IntConsumer cs3 = i -> System.out.println(i + i + i); cs3.accept(3); // out : 9 // ObjIntConsumer : 타입 T의 객체 t와 int i를 받아 소비 // accept 연산 과정에서 입력 매개변수 타입이 분간이 됩니다 ObjIntConsumer<String> cs4 = (t, i) -> System.out.println(t +" "+ i); cs4.accept("Java", 8); // out : Java 8 } }
Supplier
Supplier는 매개 값이 없고 지정한 타입의 리턴값을 공급하는 get() 메서드를 구현하는 함수형 인터페이스입니다.
입력 매개 값이 없고, 리턴 값이 있으므로 일정 데이터를 '공급' 하는 메소드를 구현해야 합니다.
인터페이스 구성
종 류 | 추상 메소드 | 비 고 |
Supplier<T> | T get() | 타입 T의 객체 t를 리턴 |
BooleanSupplier | boolean getAsBoolean() | boolean 값을 리턴 |
DoubleSupplier | double getAsDouble() | double 값을 리턴 |
IntSupplier | Int getAsInt() | int 값을 리턴 |
LongSupplier | long getAsLong() | long 값을 리턴 |
구현 예시
/* 2. Supplier : 매개 값이 없고 리턴 값이 있는 추상 메소드 get 구현 */ // Supplier<T>, get : 타입 T의 객체 t를 리턴 Supplier<String> sp1 = () -> "Hello World!"; System.out.println(sp1.get()); // out : "Hello World!" // IntSupplier getAsInt : int 값을 리턴 IntSupplier sp2 = () -> 12345; System.out.println(sp2.getAsInt()); // out : 12345
Function
Function은 매개 값을 리턴 값으로 매핑(타입 변환) 하는 메소드를 구현하는 함수형 인터페이스입니다.
입력 매개 값을 리턴 값으로 '매핑, 즉 변환' 하는 메소드를 구현해야 합니다.
인터페이스 구성
종 류 | 추상 메소드 | 비 고 |
Function<T, R> | R apply(T t) | 타입 T의 객체 t를 객체 R로 매핑 |
BiFunction<T, U, R> | R apply(T t, U u) | 타입 T의 객체 t와 타입 U의 객체 u를 객체 R로 매핑 |
DoubleFunction<R> | R apply(double value) | double을 객체 R로 매핑 |
IntFunction<R> | R apply(int value) | int를 객체 r로 매핑 |
IntToDoubleFunction | double applyAsDouble(int value) | int를 double로 매핑 |
IntToLongFunction<R> | long applayasLong(int value) | int를 long으로 매핑 |
LongToDoubleFunction<R> | double apply(T t) | long을 double로 매핑 |
LongToIntFunction<R> | int applyAsInt(long value) | long을 int로 매핑 |
ToDoubleBiFunction<T, U> | double applyAsDouble(T t, U u) | 타입 T의 객체 t와 타입 U의 객체 u를 double로 매핑 |
ToDoubleFunction<T> | double applyAsDouble(T value) | 타입 T의 객체 t double로 매핑 |
ToIntBiFunction<T,U> | int applyAsInt(T t, U u) | 타입 T의 객체 t와 타입 U의 객체 u를 int로 매핑 |
ToIntFunction<T> | int applyAsInt(T value) | 타입 T의 객체 t 를 int로 매핑 |
ToLongBiFunction<T, U> | long applyAsLong(T t, U u) | 타입 T의 객체 t와 타입 U의 객체 u를 long으로 매핑 |
ToLongFunction<T> | long applyAsLong(T value) | 타입 T의 객체 t를 long으로 매핑 |
매핑 메소드는 각 형별 유형이 비슷하므로 주요 메서드 3개만 예시로 다루어보았습니다.
구현 예시
/* 3. Function : 매개 값을 특정 리턴값으로 매핑하는 apply 메소드 구현 */ // Function<T, R> : 타입 T의 객체 t를 R로 매핑 Function<Integer, String> fu1 = i -> i.toString().repeat(i); System.out.println(fu1.apply(10)); // out : 10101010101010101010 // Function<T, U, R> : 타입 T의 객체 t, 타입 U의 객체 u를 R로 매핑 BiFunction<Integer, Integer, String> fu2 = (i1, i2) -> i1.toString().concat(i2.toString()); System.out.println(fu2.apply(12, 34)); // out : 1234 // ToIntFunction<T> : 타입 T의 객체 t를 int로 매핑 ToIntFunction<String> fu3 = String::length; System.out.println(fu3.applyAsInt("Hello World!")); // out : 12
Operator
Operator는 타입 변환 없이 ,입력 매개 변수 간의 연산 후 매개 변수와 동일한 타입으로
리턴하는 추상 메서드 apply를 구현하는 메서드입니다.
인터페이스 구성
종 류 | 추상 메소드 | 비 고 |
BinaryOperator<T> | T apply(T t, T u) | 동일 타입인 T t와 T u를 연산한 후 T 리턴 |
UnaryOperator<T> | T apply(T t) | T를 연산한 후 T 리턴 |
DoubleBInaryOperator | double applyAsDouble(double) | 두 개의 double 연산 |
DoubleUnaryOperator | double applyAsDouble(double) | 한 개의 double 연산 |
IntBinaryOperator | int applyAsInt(int, int) | 두 개의 int 연산 |
IntUnaryOperator | int applyAsInt(int) | 한 개의 int 연산 |
LongBinaryOperator | long applyAsLong(long, long) | 두 개의 long 연산 |
LongUnaryOperator | long applyAsLong(long) | 한 개의 long 연산 |
구현 예시
/* 4. Operator : 매개 변수 간의 연산 후 매개변수와 동일한 리턴 값을 제공하는 추상메소드 apply 구현 */ // BinaryOperator : 동일 타입인 T 매개변수 두개 연산 후 T로 리턴 BinaryOperator<Integer> op1 = (i1, i2) -> i1 * 10 + i2; System.out.println(op1.apply(1,4)); // out : 14 // UnaryOperator : 동일 타입인 T 매개변수 한개 연산 후 T로 리턴 UnaryOperator<String> op2 = s -> "wa! " + s; System.out.println(op2.apply("Sans!")); // out : "wa! Sans!" // IntBinaryOperator : int 매개변수 두개 연산 후 int로 리턴 // op1 예제와 동일한 기능임 IntBinaryOperator op3 = op1::apply; System.out.println(op3.applyAsInt(1,4)); // out : 14
Predicate
Predicate는 매개 변수에 따른 boolean 리턴 값을 반환하는 test 메서드를 구현하는 함수형 인터페이스입니다.
조건식을 구현할 때 사용되며 true / false를 리턴합니다.
인터페이스 구성
종 류 | 추상 메소드 | 비 고 |
Predicate<T> | boolean test(T t) | 객체 t를 조사 |
BiPredicate<T, U> | boolean test(T t, U u) | 타입 T의 객체 t와 타입 U의 객체 u를 조사 |
DoublePredicate | boolean test(double value) | double 값을 조사 |
IntPredicate | boolean test(int value) | int 값을 조사 |
LongPredicate | boolean test(long value) | long 값을 조사 |
구현 예시
/* 5. Predicate : 매개 변수 간의 연산 후 boolean 리턴 값을 제공하는 추상메소드 test 구현 */ // Predicate<T> : 타입 T 객체 t를 조사해 boolean 리턴 Predicate<String> pr1 = s -> s.length() < 4; System.out.println(pr1.test("String")); // out : false // BiPredicate<T, U> 타입 T 객체 t와 타입 U 객체 u를 조사해 boolean 리턴 BiPredicate<String, Character> pr2 = (s, c) -> s.charAt(0) == c; System.out.println(pr2.test("java", 'j')); // out : true
다음 시간에는 람다식을 포함한 스트림의 중간 연산, 최종 연산까지 알아보겠습니다.
#구현 코드
java.util.function 활용 예제 : 클릭 시 깃허브로 이동합니다.
'DEV > Java' 카테고리의 다른 글
[java] NoSuchElementException 예외의 이해와 발생사례 (1) | 2023.10.25 |
---|---|
[java] JVM 메모리 구조와 변수별 메모리 할당 (0) | 2023.10.23 |
[java] 어노테이션(Annotation)의 이해와 사용방법 (2) | 2023.10.17 |
[java] Comparable 활용 Collections.sort() 정렬 방식 정의 (0) | 2023.10.12 |
[java] 중첩 클래스(Nested Class)의 이해와 구현 (1) | 2023.10.09 |