티스토리 뷰
[Java]람다(Lambda), 함수형 인터페이스(Functional Interface), 메서드 레퍼런스(Method Reference)
Vagabund.Gni 2022. 7. 20. 19:46람다 표현식(Lambda Expression) 혹은 줄여서 람다식은 함수형 프로그래밍 기법을 지원하는 자바의 문법요소이다.
간단하게 말해서 메서드를 하나의 식으로 표현한 것이라고 할 수도 있는데,
메서드를 람다식으로 표현하면 클래스와 객체를 생성하지 않고도 메서드를 사용할 수 있다.
따라서 람다 함수는 익명 함수와 동의어이며, 더 정확하게는 익명 객체라 볼 수 있다.
또한 이 익명 객체를 사용하기 위해 뒤에 살펴볼 함수형 인터페이스(Functional Interface)가 만들어지게 되었다.
람다식의 기본 문법
람다식의 가장 큰 장점은 코드의 직관성과 간결성이다.
//기존 메서드 표현 방식
void sayhello() {
System.out.println("HELLO!")
}
//위의 코드를 람다식으로 표현한 식
() -> System.out.println("HELLO!")
기존 메서드 표현 방식에서 반환 타입과 이름을 생략한 것을 볼 수 있다.
이것이 람다 함수를 익명 함수라 부르는 이유이다.
먼저 몇 가지 예를 보자.
//기존 방식
int sum(int num1, int num2) {
return num1 + num2;
}
//람다식
(int num1, int num2) -> {num1 + num2}
// 기존 방식
void example1() {
System.out.println(5);
}
// 람다식
() -> {System.out.println(5);}
void example3(String str) {
System.out.println(str);
}
// 람다식
(String str) -> {System.out.println(str);}
먼저 반환 타입과 메서드 이름을 제거하고 코드 블록 사이에 화살표를 넣는다.
추가로 반환 값이 있는 경우 return문과 세미콜론(;)을 생략할 수 있다.
이어서 특정한 조건을 만족하는 경우엔 람다식을 더욱 축약할 수 있는데,
위에 쓰인 sum 메서드를 보자.
먼저, 메서드 바디에 실행문이 하나만 존재할 때 중괄호를 생략할 수 있다.
(int num1, int num2) -> num1 + num2
다음으로, 매개변수의 타입을 쉽게 유추할 수 있는 경우, 타입을 생략할 수 있다.
(num1, num2) -> num1 + num2
처음과 끝을 이어서 보면, 람다식이 얼마나 간결하고 직관적인지 알 수 있다.
//기존 방식
int sum(int num1, int num2) {
return num1 + num2;
}
//람다식
(num1, num2) -> num1 + num2
추가로, 람다식을 이용한 스레드 생성을 보자.
new Thread(new Runnable() {
public void run() {
System.out.println("전통적인 방식의 일회용 스레드 생성");
}
}).start();
new Thread(()->{
System.out.println("람다 표현식을 사용한 일회용 스레드 생성");
}).start();
함수형 인터페이스(Functional Interface)
위에 언급한 대로, 람다식은 정확하게 말하면 익명 객체이다.
자바에서는 메서드(함수)는 반드시 클래스 안에 선언되어야 하며, 독립적으로 존재할 수 없기 때문이다.
따라서 아래의 두 표현식은 같다.
// sum 메서드 람다식
(num1, num2) -> num1 + num2
// 람다식을 객체로 표현
new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
}
아래의 객체는 선언과 생성을 동시에 하며 일회용으로 쓰이는 익명 객체이다.
람다식이 객체라 한다면, 람다식을 다루기 위한 참조 변수가 필요해진다.
그렇다고 아래와 같이 사용하려고 하면 에러가 난다.
Object obj = new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
}
obj.sum(2,4); // 에러. Object 클래스에 sum()이 없음
이 한계를 극복하기 위해 등장한 것이 바로 함수형 인터페이스(Functional Interface)이다.
기존의 인터페이스 문법을 사용하여 람다식을 다루는 것이라고 할 수도 있다.
이것이 가능한 이유는 위에 적었듯이 람다식 자체가 하나의 객체이기 때문에
인터페이스에 정의된 추상 메서드를 구현할 수 있기 때문이다.
함수형 인터페이스가 단 하나의 추상 메서드만 가질 수 있는 이유도 여기에서 나오는데,
람다식과 인터페이스가 1 : 1로 매칭이 되어야 하기 때문이다.
위의 예제에 함수형 인터페이스를 적용하면 아래와 같이 된다.
public class LamdaExample1 {
public static void main(String[] args) {
ExampleFunction exampleFunction = (num1, num2) -> num1 + num2;
System.out.println(exampleFunction.sum(10,15));
}
}
@FunctionalInterface // 함수형 인터페이스를 올바로 작성했는지 컴파일러가 체크
interface ExampleFunction {
public abstract int sum(int num1, int num2);
}
// 출력 결과
25
함수형 인터페이스의 추상 메서드를 람다식과 엮어서 구현하고 있는 것을 볼 수 있다.
이어서 매개변수와 리턴 값의 유무에 따른 예를 보자.
매개변수와 리턴 값이 없는 람다식
@FunctionalInterface
public interface MyFunctionalInterface {
public void accept();
}
public class MyFunctionalInterfaceExample {
public static void main(String[] args) throws Exception {
MyFunctionalInterface example;
example = () -> {
String str = "첫 번째 메서드 호출!";
System.out.println(str);
};
example.accept();
example = () -> System.out.println("두 번째 메서드 호출!");
//실행문이 하나라면 중괄호 { }는 생략 가능
example.accept();
}
}
// 출력 결과
첫 번째 메서드 호출!
두 번째 메서드 호출!
함수형 인터페이스의 추상 메서드 accept()가 매개변수와 리턴 값을 가지지 않기 때문에
example()에 대한 람다식도 위와 같이 적혀야 한다.
매개변수가 있는 람다식
@FunctionalInterface
public interface MyFunctionalInterface {
public void accept(int x);
}
public class MyFunctionalInterfaceExample {
public static void main(String[] args) throws Exception {
MyFunctionalInterface example;
example = (x) -> {
int result = x * 5;
System.out.println(result);
};
example.accept(2);
example = (x) -> System.out.println(x * 5);
example.accept(2);
}
}
// 출력 결과
10
10
매개변수가 하나 존재하는 accept() 메서드를 타깃으로 갖는 람다식 작성에 관한 예이다.
이번엔 람다식에도 매개변수가 하나 존재함을 알 수 있다.
매개변수와 리턴 값이 있는 람다식
@FunctionalInterface
public interface MyFunctionalInterface {
public int accept(int x, int y);
}
public class MyFunctionalInterfaceExample {
public static void main(String[] args) throws Exception {
MyFunctionalInterface example;
example = (x, y) -> {
int result = x + y;
return result;
};
int result1 = example.accept(2, 5);
System.out.println(result1);
example = (x, y) -> { return x + y; };
int result2 = example.accept(2, 5);
System.out.println(result2);
example = (x, y) -> x + y;
// 실행문이 한 줄인 경우, 중괄호 {}와 return문 생략가능
int result3 = example.accept(2, 5);
System.out.println(result3);
example = (x, y) -> sum(x, y);
// 실행문이 한 줄인 경우, 중괄호 {}와 return문 생략가능
int result4 = example.accept(2, 5);
System.out.println(result4);
}
public static int sum(int x, int y){
return x + y;
}
}
//출력 결과
7
7
7
7
두 개의 매개변수와 리턴 값을 가진 accept()에 대한 람다식과 호출 방법이다.
메서드 레퍼런스
메서드 레퍼런스, 또는 메서드 참조는 람다 표현식을 더욱 간단하게 표현하기 위해 사용되는 방법이다.
예를 들어, 다음 메서드는 두 개의 값 중 큰 수를 리턴하는 람다식이다.
(left, right) -> Math.max(left, right);
이 람다식처럼 람다 표현식이 단 하나의 메서드만을 호출하는 경우, 아래와 같이 간략화할 수 있다.
// 클래스이름::메서드이름
Math :: max; // 메서드 레퍼런스
불필요한 매개변수를 제거하고 '::' 기호를 이용해 표현한 것을 볼 수 있다.
메서드 레퍼런스도 함수형 인터페이스의 익명 구현 객체로 생성되므로 람다식과 동일하게 인터페이스의 추상 메서드가
어떤 매개변수를 가지는지, 리턴 값이 존재하는지에 따라 사용법이 달라진다.
예를 들어 아래의 IntBinaryOperator 인터페이스는 두 개의 int 값을 받아 int 값을 리턴한다.
IntBinaryOperator operator = Math :: max; //메서드 레퍼런스
메서드 레퍼런스는 사용하는 패턴에 따라 다음의 세 가지로 분류할 수 있는데,
- 정적(static) 메서드 레퍼런스
- 인스턴스 메서드 레퍼런스
- 생성자 레퍼런스
아래에서 간단히 예를 보자.
정적(static) 메서드 레퍼런스와 인스턴스 메서드 레퍼런스
정적 메서드를 참조할 경우는 아래와 같고,
클래스명::메서드명
인스턴스 메서드를 참조하는 경우는 다음과 같다.
참조변수명::메서드명
정적 메서드와 달리 인스턴스 메서드의 경우는 당연하게도 객체 생성이 선행되어야 한다.
이어지는 예제는 Calculator 클래스의 정적, 인스턴스 메서드를 참조한다.
import java.util.function.IntBinaryOperator;
class Calculator {
public static int staticMethod(int x, int y) {
return x + y;
}
public int instanceMethod(int x, int y) {
return x * y;
}
}
public class MethodReferences {
public static void main(String[] args) throws Exception {
IntBinaryOperator operator;
/*정적 메서드
클래스명::메서드명
*/
operator = Calculator::staticMethod;
System.out.println("정적메서드 결과 : " + operator.applyAsInt(3, 5));
/*인스턴스 메서드
인스턴스명::메서드명
*/
Calculator calculator = new Calculator();
operator = calculator::instanceMethod;
System.out.println("인스턴스 메서드 결과 : "+ operator.applyAsInt(3, 5));
}
}
//출력 결과
정적메서드 결과 : 8
인스턴스 메서드 결과 : 15
위에 설명한 바와 같이 정적 메서드는 클래스명::메서드명으로, 인스턴스 메서드는 인스턴스명::메서드명으로 참조한 것을 볼 수 있다.
생성자 레퍼런스
생성자 레퍼런스는 말 그대로 생성자를 참조하는 코드이다.
람다 표현식이 단 하나의 메서드만을 호출하는 경우와 비슷하게,
단순히 객체를 생성하고 반환하는 람다 표현식은 생성자 레퍼런스로 변환할 수 있다.
(a,b) -> {return new Company(a,b);};
예를 들면 Company 클래스의 인스턴스를 생성하는 위와 같은 람다식은
Company::new
와 같이 변환하여 사용할 수 있다.
생성자가 오버로딩 되어있는 경우, 컴파일러가 함수형 인터페이스의 추상 메서드와
동일한 매개변수 타입과 개수를 가지고 있는 생성자를 찾아 실행하게 된다.
다음 예를 보자.
//Member.java
public class Member {
private String name;
private String id;
public Member() {
System.out.println("Member() 실행");
}
public Member(String id) {
System.out.println("Member(String id) 실행");
this.id = id;
}
public Member(String name, String id) {
System.out.println("Member(String name, String id) 실행");
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
public String getId() {
return id;
}
}
import java.util.function.BiFunction;
import java.util.function.Function;
public class ConstructorRef {
public static void main(String[] args) throws Exception {
Function<String, Member> function1 = Member::new;
Member member1 = function1.apply("kimcoding");
BiFunction<String, String, Member> function2 = Member::new;
Member member2 = function2.apply("kimcoding", "김코딩");
}
}
//출력 결과
Member(String id) 실행
Member(String name, String id) 실행
위에 사용된 함수형 인터페이스 Function <String, Member>는 String 타입을 받아 Member 타입을 반환하는 역할을 한다.
apply는 Function 함수형 인터페이스의 추상 메서드 이름이다.
비슷하게 BiFunction <String, String, Member>의 경우는 두 개의 String 타입을 받아 Member 타입을 반환한다.
여기서도 동일하게 apply가 BiFunction 함수형 인터페이스의 추상 메서드 이름이다.
매개변수의 타입과 개수에 따라 다른 생성자가 호출되었음을 확인할 수 있다.
아래는 자주 사용하는 함수형 인터페이스의 예이다.
함수형 인터페이스 | 파라미터 타입 | 반환 타입 | 추상 메서드 이름 | 설명 | 다른 메서드 |
Runnable | 없음 | void | run | 인자나 반환 값 없이 액션을 수행한다. | 없음 |
Supplier | 없음 | T | get | T 타입 값을 공급한다. | 없음 |
Consumer | T | void | accept | T 타입 값을 소비한다. | andThen |
BiConsumer<T, U> | T, U | void | accept | T와 U타입 값을 소비한다. | andThen |
Function<T, R> | T | R | apply | T 타입 인자를 받는 함수다. | compose andThen identity |
BiFunction<T, U, R> | T, U | R | apply | T와 U타입 인자를 받는 함수다. | andThen |
UnaryOperator | T | T | apply | T 타입에 적용하는 단항 연산자다. | compose andThen identity |
BinaryOperator | T, T | T | apply | T 타입에 적용하는 이항 연산자다. | andThen maxBy minBy |
Predicate | T | boolean | test | Boolean 값을 반환하는 함수다. | and or negate isEqual |
BiPredicate<T, U> | T, U | boolean | test | 두 가지 인자를 받고 boolean 값을 반환하는 함수다. | and or negate |
출처: https://imcts.github.io/java-method-reference/
'Java+Spring > Java' 카테고리의 다른 글
[Java]스레드(Thread) (0) | 2022.07.20 |
---|---|
[Java]스트림 생성, 중간 연산, 최종 연산 (0) | 2022.07.20 |
[Java]스트림(Stream) (0) | 2022.07.20 |
[Java]열거형(enum), 애너테이션(Annotation) (0) | 2022.07.20 |
[Java]Map<K, V>, HashMap<K, V>, HashTable<K, V> (2) | 2022.07.15 |
[Java]Set<E>, HashSet<E>, TreeSet<E> (0) | 2022.07.15 |
- Total
- Today
- Yesterday
- Algorithm
- Backjoon
- 기술면접
- 지지
- Python
- 파이썬
- spring
- 면접 준비
- 스프링
- a6000
- 여행
- 남미
- 야경
- 리스트
- 유럽여행
- BOJ
- 자바
- RX100M5
- java
- 알고리즘
- 칼이사
- 세계여행
- 동적계획법
- 세모
- 유럽
- 스트림
- 맛집
- 세계일주
- 중남미
- 백준
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |