반응형
해당 글에서는 Java에서 사용되는 람다식(Lambda Expression)과 함수형 인터페이스(Functional Interface)에 대해 알아봅니다.
1) 순수 함수, 일급 함수, 고차 함수
💡 순수 함수, 일급 함수, 고차 함수
- 람다, 함수형 인터페이스를 이해하기 이전에 일반 함수, 일급 함수, 고차 함수에 대해서 이해를 하고 이후 상세히 알아봅니다.
용어 | 설명 | 예시 |
순수 함수(Pure function) | - 매개 변수가 존재하거나 존재하지 않을 수 있고 연산을 수행한 후 결과를 반환하는 함수를 의미합니다. | - 일반 함수 |
일급 함수(First-class function) | - 함수를 변수에 할당하거나 다른 함수의 인자로 전달하거나 함수의 반환 값으로 사용할 수 있는 특성의 함수를 의미합니다. | - 람다식, - java.util.function 패키지 |
고차 함수(Higher Order Function) | - 함수를 매개변수로 받거나 함수를 반환하는 함수를 의미합니다. | - Stream API |
// 순수 함수(Pure function)
public int add(int a, int b) {
return a + b;
}
// 일급 함수(First-class function)
public interface MathOperation {
int operate(int a, int b);
}
// 고차 함수(Higher Order Function)
public MathOperation createMultiplier(int factor) {
return (a, b) -> a * b * factor;
}
public static void main(String[] args) {
// 순수 함수(Pure function) 호출
int result1 = add(3, 5);
System.out.println("Result of add function: " + result1);
// 일급 함수(First-class function) 호출
MathOperation addition = (a, b) -> a + b;
int result2 = addition.operate(3, 5);
System.out.println("Result of first-class function: " + result2);
// 고차 함수(Higher Order Function) 사용
MathOperation multiplier = createMultiplier(2);
int result3 = multiplier.operate(3, 5);
System.out.println("Result of higher-order function: " + result3);
}
1. 순수 함수(Pure function)
💡 순수 함수(Pure function)
- 동일한 입력에 대해 항상 동일한 출력을 반환하는 함수를 의미합니다.
- 외부에 있는 다른 상태를 변경하지 않기에 코드의 예측 가능성과 가독성을 높여준다는 장점이 있습니다.
public int add(int a, int b) {
return a + b;
}
[ 더 알아보기 ]
💡 외부 상태가 변경되지 않는다는 말은 무슨 말일까?
- 함수가 실행되는 동안 함수 외부에 있는 변수나 객체 등의 상태를 수정하지 않는 것을 의미합니다.
2. 일급 함수 (First-class function)
💡 일급 함수 (First-class function)
- 함수를 변수에 할당하거나 다른 함수의 인자로 전달할 수 있는 함수를 의미합니다.
- 함수를 값처럼 다룰 수 있으며 자유롭게 조합하고 조작할 수 있습니다.
- Java에서는 일급 함수로는 java.util.function 패키지, 람다식 등이 있습니다.
// java.util.function 패키지를 이용한 일급함수
List<Integer> numbers = Arrays.asList(10, 20, 30, 40, 50);
Predicate<Integer> isEven = number -> number % 2 == 0;
for (int number : numbers) {
if (isEven.test(number)) {
answer += 1;
}
}
// 함수형 인터페이스를 이용한 일급 함수
// 1. 함수형 인터페이스 정의
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
public class LambdaExample {
public static void main(String[] args) {
// 2. 함수형 인터페이스 구현 : 람다식을 사용하여 Calculator 인터페이스를 구현
Calculator addition = (a, b) -> a + b;
Calculator subtraction = (a, b) -> a - b;
// 3. 함수형 인터페이스의 메서드 호출(일급 함수 호출)
int result1 = addition.calculate(5, 3);
int result2 = subtraction.calculate(10, 7);
System.out.println("Addition: " + result1);
System.out.println("Subtraction: " + result2);
}
}
3. 고차 함수 (Higher Order Function)
💡 고차 함수(Higher Order Function)
- 함수를 인자로 받거나 함수를 반환하는 함수를 의미합니다.
- 다른 함수를 조작하거나 추상화하기 위해 사용됩니다. 고차 함수를 사용하면 코드의 재사용성과 유연성을 높일 수 있습니다.
- java.util.function 패키지나 람다식을 이용하여 고차 함수와 유사 패턴을 구현할 수 있습니다
💡 고차 함수 예시
1. BinaryOperator 인터페이스를 사용하여 두 수를 더하는 일급함수 add를 정의합니다.
2. higherOrderFunction 고차함수를 사용하여 add 함수를 인자로 받아서 새로운 함수 addFunction을 반환합니다.
3. 이후 apply 메서드를 사용하여 addFunction 함수를 호출하고 결과를 출력합니다.
// BinaryOperator를 사용하여 두 수를 더하는 일급함수를 정의
BinaryOperator<BinaryOperator<Integer>> higherOrderFunction = (operation) -> (a, b) -> operation.apply(a, b);
// 일급함수를 사용하여 두 수를 더하는 함수를 정의
BinaryOperator<Integer> add = (a, b) -> a + b;
// 고차함수를 사용하여 두 수를 더하는 함수를 반환
BinaryOperator<Integer> addFunction = higherOrderFunction.apply(add);
// apply 메소드를 사용하여 함수를 호출
int result = addFunction.apply(5, 3);
2) 람다식(Lambda Expression)
💡 람다식(Lambda Expression)
- 함수형 인터페이스를 구현하는 익명 함수(Anonymous functions)로 간단하고 간결하게 표현하는 표현식 의미합니다. 이를 사용하면 함수형 프로그래밍 스타일로 코드를 작성할 수 있습니다.
- Java 8 버전부터 지원하며 Stream API와 함께 함수형 프로그래밍의 개념을 적용하고 데이터를 처리하는 방식을 효율적으로 개선하는데 활용이 됩니다.
[ 더 알아보기 ]
💡 익명 함수(Anonymous functions)
- 이름이 없는 함수로 자바에서는 람다식을 통해 익명 함수를 생성할 수 있습니다.
- 함수형 프로그래밍을 지원하기 위한 기능으로, 간결하고 효율적인 코드 작성을 가능하게 합니다.
💡 함수형 프로그래밍
- 프로그램을 함수들의 조합으로 구성하는 프로그래밍 패러다임을 의미합니다.
- 코드의 가독성과 재사용성을 높이는데 초점을 둡니다.
💡 [참고] Stream API에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
설명 | 링크 |
Stream API : 용어 및 Stream 생성 | https://adjh54.tistory.com/107 |
Stream API : Stream 중간 연산 | https://adjh54.tistory.com/109 |
Stream API : Stream 최종 연산 |
https://adjh54.tistory.com/110 |
1. 람다식의 특징
💡 람다식의 주요한 특징에 대해서 알아봅니다.
특징 | 설명 |
간결성 | 람다식은 한 줄로 표현할 수 있기 때문에 코드가 간결해집니다. |
함수형 프로그래밍 지원 | 람다식은 함수형 프로그래밍의 핵심 개념으로, 함수를 값으로 취급할 수 있게 합니다. |
익명 함수 | 람다식은 익명 함수로 정의되기 때문에 함수의 이름을 정의할 필요가 없습니다. |
고차 함수와 함께 사용 | 람다식은 고차 함수와 함께 사용되어 함수를 매개변수로 전달하거나 함수를 반환하는 등의 작업을 수행할 수 있습니다. |
2. 람다식 기본 구조
💡 람다식 기본 구조
- CASE1의 기본구조는 입력 매개변수인 ‘parameters’와 메서드의 실행 결과인 ‘expression’로 구성이 되어 있습니다.
- CASE2의 기본구조는 입력 매개변수인 ‘parameters’와 블록이 있는 경우 메서드의 본문을 나타내는 ‘statements’로 구성이 되어 있습니다.
// CASE1
(parameters) -> expression
// OR
// CASE2
(parameters) -> { statements; }
💡 일반 함수와 람다식의 예시
- 일반 함수에서 2개의 ‘parameters’를 받고 이에 대한 값을 반환하는 ‘statements’로 구성이 되어 있습니다.
- 람다식에서도 동일하게 2개의 ‘parameters’를 받고 이에 대한 값을 반환하는 ‘statements’로 구성이 되어 있습니다.
// 일반 함수 예시
public int calcSum(int a, int b) {
return a + b;
}
// 람다식 예시
(a, b) -> a + b
[ 더 알아보기 ]
💡 익명함수로 구성을 하면 함수의 호출 자체가 불가능한 것 아닌가?
- 익명 함수는 이름이 없지만 람다 표현식으로 사용될 때 호출할 수 있습니다.
- 예를 들어, 람다 표현식을 사용하여 함수형 인터페이스를 구현한 후, 이를 호출할 수 있습니다.
- 익명 함수는 일회성으로 사용되는 경우에 유용하며, 필요한 곳에서 즉석으로 정의하여 사용할 수 있습니다.
3) 함수형 인터페이스(Functional Interface)
💡 함수형 인터페이스(Functional Interface)
- 하나의 추상 메서드만 가지는 인터페이스를 의미합니다. 이러한 인터페이스를 람다 표현식(Lambda Expression)이나 메서드 참조를 활용하여 구현할 수 있습니다.
- Java 8 버전부터 도입이 되었으며 java.util.function 패키지에서 여러 가지 함수형 인터페이스를 제공합니다. 이러한 인터페이스를 사용하여 다양한 기능을 구현할 수 있습니다.
- 함수형 인터페이스를 사용하면 간결하고 유연한 코드를 작성할 수 있으며 함수형 프로그래밍의 개념을 적용할 수 있습니다.
[ 더 알아보기 ]
💡 추상 메서드
- 구현이 없이 선언만 되어 있는 메서드입니다. 인터페이스나 추상 클래스에서 선언되며, 하위 클래스에서 구현해야 합니다.
- 이를 사용하여 상위 클래스나 인터페이스에서 메서드의 동작을 정의하고, 하위 클래스에서 이를 구체화할 수 있습니다. 추상 메서드를 사용하면 다형성을 구현하고, 클래스 간의 계층 구조를 설계할 수 있습니다.
1. 함수형 인터페이스를 직접 구현 방식
💡 함수형 인터페이스의 기본구조
1. 함수형 인터페이스 정의
- @FunctionalInterface 어노테이션을 통해 함수형 인터페이스임을 선언하고 calculate()라는 하나의 추상 메서드를 구성하였습니다.
2. 함수형 인터페이스 구현
- Calculator 인터페이스의 추상 메서드(myMethod)에 대한 구현체를 람다식을 이용하여 구현합니다.
3. 함수형 인터페이스의 메서드 호출
- 함수형 인터페이스 내의 추상 클래스를 호출하여 “함수형 인터페이스의 메서드 실행”이라는 값을 반환받습니다.
// 1. 함수형 인터페이스 정의
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
public class LambdaExample {
public static void main(String[] args) {
// 2. 함수형 인터페이스 구현 : 람다식을 사용하여 Calculator 인터페이스를 구현
Calculator addition = (a, b) -> a + b;
Calculator subtraction = (a, b) -> a - b;
// 3. 함수형 인터페이스의 메서드 호출
int result1 = addition.calculate(5, 3);
int result2 = subtraction.calculate(10, 7);
System.out.println("Addition: " + result1);
System.out.println("Subtraction: " + result2);
}
}
💡 @FunctionalInterface
- 해당 어노테이션을 선언한 함수가 ‘함수형 인터페이스’ 임을 의미합니다. 그렇기에 단 하나의 추상 메서드만 가지고 있는 인터페이스를 의미합니다.
- 컴파일러에게 해당 인터페이스가 함수형 인터페이스임을 명시적으로 알려주고 추상 메서드가 여러 개인 인터페이스를 함수형 인터페이스로 선언하는 것을 방지할 수 있습니다.
- 필수적으로 사용되는 것은 아니지만 주로 문서화 목적이나 실수 방지를 위해 사용합니다.
2. 함수형 인터페이스를 java.util.function 패키지 이용 방식
💡 함수형 인터페이스를 내부적 함수 이용 방식: java.util.function
- @FunctionalInterface 어노테이션의 경우 함수형 인터페이스임을 선언하는 어노테이션이며 java.util.function 패키지를 이용하여 주요한 Predicate <T>, Function <T, R>, Consumer <T>, Supplier <T>, UnaryOperator <T>, BinaryOperator <T>의 경우는 @FunctionalInterface로 선언된 인터페이스에서 가져와서 실제 구현을 합니다.
💡 [참고] 실제 해당 메서드의 인터페이스를 찾아보면 @FunctionalInterface로 선언되어 있습니다.
함수형 인터페이스 | 매개변수 유무 | 리턴 값 | 설명 |
Predicate<T> | O | X | 주어진 '조건을 만족하는지 평가'하는 함수형 인터페이스입니다. |
Function<T, R> | O | O | 입력 값을 받아서 '결과 값을 반환'하는 함수형 인터페이스입니다. |
Consumer<T> | O | X | '입력 값을 받아서 처리'하는 함수형 인터페이스입니다. |
Supplier<T> | X | O | '값을 제공'하는 함수형 인터페이스입니다. |
UnaryOperator<T> | O | O | '입력 값을 연산하고 결과 값을 반환'하는 함수형 인터페이스입니다. |
BinaryOperator<T> | O | O | '두 개의 입력 값을 연산하고 결과값을 반환'하는 함수형 인터페이스입니다. |
💡 [참고] java.util.function 패키지의 API Document입니다.
2.1. Predicate <T>
💡 Predicate <T>
- 주어진 매개변수(parameter)를 평가하여 true 또는 false를 반환하는 메서드를 의미합니다.
- 일반적으로 조건을 검사하고, 필터링 또는 검증 작업에 사용됩니다.
- 컬렉션의 요소를 필터링하거나, 조건에 맞는 값을 검증하기 위해 Predicate <T>를 활용할 수 있습니다.
💡 Predicate <T> 주요 메서드
메서드 | 설명 |
test(T) | - 주어진 매개변수를 이 Predicate에 적용하여 논리적인 평가를 수행합니다. |
and(Predicate) | - 현재 Predicate와 다른 Predicate를 조합한 새로운 Predicate를 반환합니다. - 조합된 Predicate는 두 Predicate가 모두 true인 경우에만 true를 반환합니다. |
or(Predicate) | 현재 Predicate와 다른 Predicate를 조합한 새로운 Predicate를 반환합니다 조합된 Predicate는 두 Predicate 중 하나라도 true인 경우에 true를 반환합니다. |
negate() | - 현재 Predicate의 논리적인 부정을 수행한 새로운 Predicate를 반환합니다. - 부정된 Predicate는 현재 Predicate의 결과값을 반대로 반환합니다. |
💡 함수형 인터페이스 : predicate 예시
- 리스트 중에 짝수인 값을 찾아서 Counting 하는 활용 예시
/**
* 함수형 인터페이스 : predicate 예시
* 리스트 중 짝수인 값을 찾아서 Counting 하는 활용 예시
*
* @return
*/
@GetMapping("/predicate")
public ResponseEntity<ApiResponse<Object>> predicate() {
int answer = 0;
List<Integer> numbers = Arrays.asList(10, 20, 30, 40, 50);
Predicate<Integer> isEven = number -> number % 2 == 0;
for (int number : numbers) {
if (isEven.test(number)) {
answer += 1;
}
}
ApiResponse<Object> ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
2.2. Function <T, R>
💡 Function <T, R>
- T 타입의 입력을 받아서 R타입의 결과를 반환하는 메서드를 의미합니다.
- 입력 값을 변환하거나, 다른 타입으로 매핑하기 위해 Function <T, R>을 활용할 수 있습니다.
💡 Function <T, R> 주요 메서드
메서드 | 설명 |
apply(T) | 주어진 매개변수를 이 Function에 적용하여 결과를 반환합니다. |
andThen(Function) | 현재 Function의 결과를 다른 Function에 적용한 새로운 Function을 반환합니다. |
compose(Function) | 다른 Function의 결과를 현재 Function에 적용한 새로운 Function을 반환합니다. |
💡 함수형 인터페이스 : function 예시
- 정수를 문자열로 변환, 문자열을 정수로 변환하고 이 변환된 값을 다시 함수로 구성합니다.
/**
* 함수형 인터페이스 : function 예시
* 정수를 문자열로 변환, 문자열을 정수로 변환하고 이 변환된 값을 다시 함수로 구성합니다.
*
* @return
*/
@GetMapping("/functions")
public ResponseEntity<ApiResponse<Object>> functions() {
int answer = 0;
// 정수를 문자열로 변환하는 Function
Function<Integer, String> toString = number -> String.valueOf(number);
// 문자열을 정수로 변환하는 Function
Function<String, Integer> toInteger = str -> Integer.parseInt(str);
// 숫자를 문자열로 변환 후, 정수로 변환하는 Function
Function<Integer, Integer> convert = toString.andThen(toInteger);
int number = 10;
String numberAsString = toString.apply(number);
int convertedNumber = convert.apply(number);
System.out.println("Number as String: " + numberAsString);
System.out.println("Converted Number: " + convertedNumber);
ApiResponse<Object> ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
2.3. Consumer <T>
💡 Consumer <T>
- T 타입의 매개변수(parameter)를 받아들여 어떠한 동작을 수행하는 메서드를 의미합니다.
- 컬렉션의 각 요소에 대해 특정 동작을 수행하거나, 매개변수를 소비하여 어떠한 처리를 수행하기 위해 Consumer <T>를 활용할 수 있습니다.
💡 Consumer <T> 주요 메서드
메서드 | 설명 |
accept(T) | 주어진 매개변수를 소비하여 동작을 수행합니다. |
andThen(Consumer) | 현재 Consumer의 동작을 수행한 후, 다른 Consumer의 동작을 수행합니다. |
💡 함수형 인터페이스 : Consumer 예시
- 리스트에 이름들을 넣고 이름을 출력하는 콘솔을 찍는 함수를 구성하는 forEach를 통해 반복을 수행하며 이름을 출력합니다.
/**
* 함수형 인터페이스 : Consumer 예시
* 리스트에 이름들을 넣고 이름을 출력하는 콘솔을 찍는 함수구성하여 forEach를 통해 반복을 수행하며 이름을 출력합니다.
*
* @return
*/
@GetMapping("/consumer")
public ResponseEntity<ApiResponse<Object>> consumer() {
int answer = 0;
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 이름을 출력하는 Consumer
Consumer<String> printName = name -> System.out.println("Name: " + name);
// 각 이름을 출력
names.forEach(printName);
ApiResponse<Object> ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
2.4. Supplier <T>
💡 Supplier <T>
- 어떠한 인수도 받지 않고 T 타입의 결과를 반환하는 메서드를 의미합니다.
- 무작위 한 값을 생성하거나, 초기화 작업을 수행할 때 Supplier <T>를 활용할 수 있습니다.
💡 Supplier <T> 주요 메서드
메서드 | 설명 |
get() | 결과값을 제공하는 동작을 수행하고, 결과값을 반환합니다. |
💡 함수형 인터페이스 : Supplier 예시
- 현재 시간을 제공하는 Supplier를 구성합니다. 이 구성된 함수를 호출하면 결과값을 출력합니다.
/**
* 함수형 인터페이스 : supplier 예시
* 현재 시간을 제공하는 Supplier를 구성합니다. 이 구성된 함수를 호출하면 결과값을 출력합니다.
*
* @return
*/
@GetMapping("/supplier")
public ResponseEntity<ApiResponse<Object>> supplier() {
int answer = 0;
// 현재 시간을 제공하는 Supplier
Supplier<Long> currentTimeSupplier = () -> System.currentTimeMillis();
// 현재 시간을 얻기 위해 Supplier를 호출
long currentTime = currentTimeSupplier.get();
System.out.println("Current Time: " + currentTime);
ApiResponse<Object> ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
2.5. UnaryOperator <T>
💡 UnaryOperator <T>
- 하나의 입력 매개변수를 받아 동일한 타입의 결과를 반환하는 연산을 수행하는 메서드를 의미합니다. 즉, 입력과 출력이 동일한 타입입니다.
- 단항 연산을 수행하는 데 필요한 경우에 활용할 수 있습니다.
💡 UnaryOperator <T> 주요 메서드
메서드 | 설명 |
T apply(T t) | 입력 값을 받아 연산을 수행하고 결과를 반환합니다. |
💡 함수형 인터페이스 : UnaryOperator 예시
- 하나의 x의 매개변수를 받아서 동일한 타입 결과를 반환하여 출력합니다
/**
* 함수형 인터페이스 : unaryOperator 예시
* 하나의 x의 매개변수를 받아서 동일한 타입 결과를 반환하여 출력합니다
*
* @return
*/
@GetMapping("/unaryOperator")
public ResponseEntity<ApiResponse<Object>> unaryOperator() {
int answer = 0;
UnaryOperator<Integer> square = x -> x * x;
int result = square.apply(5); // 결과: 25
answer = result;
ApiResponse<Object> ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
2.6. BinaryOperator <T>
💡 BinaryOperator <T>
- 두 개의 입력 매개변수를 받고 동일한 타입의 값을 반환하는 이항 연산자를 수행하는 메서드를 의미합니다.
- 이항 연산을 수행하는 데 필요한 경우에 활용할 수 있습니다.
💡 BinaryOperator <T> 주요 메서드
메서드 | 설명 |
apply(T t1, T t2) | 두 개의 입력값을 받아 이항 연산을 수행하고 결과를 반환합니다. |
minBy(Comparator<T> c) | 주어진 Comparator에 따라 최소값을 반환합니다. |
maxBy(Comparator<T> c) | 주어진 Comparator에 따라 최대값을 반환합니다. |
💡 함수형 인터페이스 : BinaryOperator 예시
- 두 개의 매개변수를 받아서 합계를 구하는 함수를 반환하여 출력합니다.
/**
* 함수형 인터페이스 : binaryOperator 예시
* 두 개의 매개변수를 받아서 합계를 구하는 함수를 반환하여 출력합니다.
*
* @return
*/
@GetMapping("/binaryOperator")
public ResponseEntity<ApiResponse<Object>> binaryOperator() {
int answer = 0;
BinaryOperator<Integer> sum = (a, b) -> a + b;
int result = sum.apply(10, 5);
answer = result;
ApiResponse<Object> ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
오늘도 감사합니다. 😀
반응형
'Java > 아키텍처 & 디자인 패턴' 카테고리의 다른 글
[Java/디자인 패턴] 메서드 체이닝(Method Chaining) , 플루언트 인터페이스(Fluent Interface), 빌더 패턴(Builder Pattern) (0) | 2024.03.04 |
---|---|
[Java/디자인패턴] 싱글턴 패턴(Signleton Pattern) 이해하기 -1 : 정의 및 종류 (1) | 2023.12.02 |
[Java] Spring Framework 주요 특징 이해하기 : DI, IoC, POJO, AOP (1) | 2023.10.28 |
[Java] MacOS 환경에서 Java JDK 설정 및 변경하기 : homebrew, 다운로드 파일 (2) | 2023.06.28 |
[Java] Gradle 버전 확인 및 변경 방법 (2) | 2023.06.27 |