람다식(Lambda Expression)
- 간단히 말하자면 수학에서 사용하는 함수를 보다 단순하게 표현하는 방법
- 람다식이란 함수를 하나의 식으로 표현한 것.
- 함수를 람다식으로 표현하면 메소드의 이름이 필요없기 때문에 익명함수의 한 종류라고 볼 수 있다.
- 람다 함수는 프로그래밍 언어에서 사용되는 개념으로 익명 함수(Anonymous functions)를 지칭하는 용어
- 익명함수는 모두 일급 객체이고 일급 객체인 함수는 변수처럼 사용 가능하며 매개 변수로 전달이 가능한 특징이 있음
예시
예시 1
- 기존 방식
// 기존의 방식
반환티입 메소드명 (매개변수, ...) {
실행문
}
// 예시
public String hello() {
return "Hello World!";
}
- 람다 방식
// 람다 방식
(매개변수, ... ) -> { 실행문 ... }
// 예시
() -> "Hello World!";
예시 2
- 기존 방식
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Welcome Heejin blog");
}
}).start();
- 람다 방식
new Thread(()->{
System.out.println("Welcome Heejin blog");
}).start();
1. 람다식이 등장하게 된 이유
- 불필요한 코드를 줄이고 가독성을 높이기 위함.
- 함수형 인터페이스의 인스턴스를 생성하여 함수를 변수처럼 선언하는 람다식에서는 메소드 이름이 불필요.
- 대신 컴파일러가 문맥을 살펴 타입을 추론
- 람다식으로 선언된 함수는 1급 객체이기 때문에 Stream API의 매개변수로 전달 가능
2. 람다식의 특징
- 람다식 내에서 사용되는 지역변수는 final이 붙지 않아도 상수로 간주
- 람다식으로 선언된 변수명은 다른 변수명과 중복될 수 없다.
1) 람다식의 장점
- 코드의 간결성
람다를 사용하면 불필요한 반복문의 삭제가 가능하고 복잡한 식을 단순하게 표현할 수 있음. - 지연연산 수행
불필요한 연산을 최소화할 수 있음. - 높은 생산성
함수를 만드는 과정없이 한번에 처리할 수 있어 생산성이 높아짐. - 병렬처리 가능
멀티스레드를 활용하여 병렬처리를 사용할 수 있음.
2) 람다식의 단점
- 람다식의 호출이 까다롭다.
- 재사용 불가
람다를 사용하면서 만든 무명함수는 재사용 불가 - 람다 stream 사용 시 단순 for문 혹은 while문 사용시 성능 저하
- 디버깅 어려움
- 가독성 떨어뜨릴 우려
람다를 남발하면 비슷한 함수가 중복 생성 → 코드 지저분해짐 - 재귀로 만들 경우 부적합
3) 람다의 표현식
- 람다는 매개변수 화살표(->) 함수몸체로 이용하여 사용할 수 있음.
- 함수몸체가 단일 실행문이면 괄호{ }를 생략할 수 있음.
- 함수몸체가 return문으로만 구성되어 있는 경우 괄호{ }를 생략할 수 없음.
//정상적인 유형
() -> {}
() -> 1
() -> { return 1; }
(int x) -> x+1
(x) -> x+1
x -> x+1
(int x) -> { return x+1; }
x -> { return x+1; }
(int x, int y) -> x+y
(x, y) -> x+y
(x, y) -> { return x+y; }
(String lam) -> lam.length()
lam -> lam.length()
(Thread lamT) -> { lamT.start(); }
lamT -> { lamT.start(); }
//잘못된 유형 선언된 type과 선언되지 않은 type을 같이 사용 할 수 없다.
(x, int y) -> x+y
(x, final y) -> x+y
함수형 인터페이스(Functional Interface)
@FunctionalInterface
Java는 객체지향 언어이기 때문에 순수 함수와 일반 함수를 다르게 취급하고 있고, Java에서는 이를 구분하기 위해 함수형 인터페이스가 등장하였다.
1. 함수형 인터페이스란?
함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션으로, 구현해야할 메서드가 하나만 정의된 인터페이스를 말한다.
함수형 인터페이스를 사용하는 이유는 Java의 람다식이 함수형 인터페이스를 반환하기 때문이다.
자바 컴파일러는 명시된 함수형 인터페이스에 두 개 이상의 메서드가 선언되면 오류를 발생시킨다.
//구현해야 할 메소드가 한개이므로 Functional Interface이다.
@FunctionalInterface
public interface Math {
public int Calc(int first, int second);
}
//구현해야 할 메소드가 두개이므로 Functional Interface가 아니다. (오류 사항)
@FunctionalInterface
public interface Math {
public int Calc(int first, int second);
public int Calc2(int first, int second);
}
예시
두 값 중 큰 값을 구하는 익명 함수
- 기존 방식
public class Lambda {
public static void main(String[] args) {
// 기존의 익명함수
System.out.println(new MyLambdaFunction() {
public int max(int a, int b) {
return a > b ? a : b;
}
}.max(3, 5));
}
}
- 함수형 인터페이스 적용
@FunctionalInterface
interface MyLambdaFunction {
int max(int a, int b);
}
public class Lambda {
public static void main(String[] args) {
// 람다식을 이용한 익명함수
MyLambdaFunction lambdaFunction = (int a, int b) -> a > b ? a : b;
System.out.println(lambdaFunction.max(3, 5));
}
}
함수형 인터페이스를 사용함으로 함수를 변수처럼 선언할 수 있게 되었고 코드 또한 간결하게 작성할 수 있게 됨.
함수형 인터페이스를 구현하기 위해서는 인터페이스를 개발하여 그 내부에는 1개 뿐인 abstract 함수를 선언
위에는 @FunctionalInterface 어노테이션을 붙여주면 된다.
중요한 점은,
- 람다식으로 생성된 순수 함수는 함수형 인터페이스로만 선언이 가능
- @FunctionalInterface는 해당 인터페이스가 1개의 함수만을 갖도록 제한 → 여러 개의 함수를 선언하면 컴파일 에러 발생
2. Java에서 제공하는 함수형 인터페이스
Supplier<T> / Consumer<T> / Function<T, R> / Predicate<T>
1) Supplier <T>
Supplier는 매개변수 없이 반환값만을 갖는 함수형 인터페이스
Supplier는 T get()을 추상 메서드로 갖고 있다.
// 정의
@FunctionalInterface
public interface Supplier<T> {
T get();
}
// 사용 예시
Supplier<String> supplier = () -> "Hello World!";
System.out.println(supplier.get());
2) Consumer<T>
Consumer는 객체 T를 매개변수로 받아서 사용하며, 반환값은 없는 함수형 인터페이스
Consumer는 void accept(T t)를 추상메서드로 갖는다.
또한 andThen이라는 함수를 제공하는데 이를 통해 하나의 함수가 끝난 후 다음 Consumer를 연쇄적으로 이용 가능
// 정의
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
// 예시
Consumer<String> consumer = (str) -> System.out.println(str.split(" ")[0]);
consumer.andThen(System.out::println).accept("Hello World");
// 출력
Hello
Hello World
accept로 받은 Consumer을 먼저 처리하고 andThen으로 받은 두 번째 Consumer를 처리한다.
함수형 인터페이스에서 함수는 값의 대입이나 변경이 없음
→ 첫 번째 Consumer가 split으로 데이터를 변경하였다고 해도 원본의 데이터는 유지된다.
3) Function<T>
Function은 객체 T를 매개변수로 받아서 처리한 후 R로 반환하는 함수형 인터페이스
Function은 R apply(T t)를 추상메서드로 갖는다.
Consumer처럼 andThen을 제공하고 추가로 compose를 제공
- andThen: 첫 번째 함수 실행 이후에 다음 함수를 연쇄적으로 실행하도록 연결해준다.
- compose: 첫 번째 함수 실행 이전에 먼저 함수를 실행하여 연쇄적으로 연결해준다.
- identity: 자기 자신을 반환하는 static 함수
4) Predicate<T>
객체 T를 매개변수로 받아 처리한 후 Boolean을 반환
Boolean test(T t)를 추상메서드로 가지고 있음.
// 정의
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
// 예시
Predicate<String> predicate = (str) -> str.equals("Hello World");
predicate.test("Hello World");
3. 메서드 참조(Method Reference)
메서드 참조란 함수형 인터페이스를 람다식이 아닌 일반 메서드를 참조시켜 선언하는 방법.
조건
- 함수형 인터페이스의 매개변수 타입 = 메서드의 매개변수 타입
- 함수형 인터페이스의 매개변수 개수 = 메서드의 매개변수 개수
- 함수형 인터페이스의 반황형 = 메서드의 반환
참조가능한 메서드
- 일반 메서드
- static 메서드
- 생성자
참조 방법
클래스이름::메서드이름
1) 일반 메서드
예를 들어 아래의 Function에 메서드 참조를 적용.
// 정의
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
// 예시, 메소드 참조로 간소화 가능(String::length;)
Function<String, Integer> function = str -> str.length();
function.apply("Hello World");
3가지 조건
- 매개변수 없음
- 매개변수 개수 = 0개
- 반환형 = int
String의 length 함수는 매개변수가 없으며, 반환형이 int로 동일하기 때문에 String::length로 다음과 같이 메서드 참조를 적용 가능
// 기존의 람다식
Function<String, Integer> function = (str) -> str.length();
function.apply("Hello World");
// 메소드 참조로 변경
Function<String, Integer> function = String::length;
function.apply("Hello World");
System.out.println() 메서드의 반황형이 void이며, 파라미터로 String을 받는 메서드.
Consumer에 System.out.println() 메서드 참조 가능
// 일반 메소드를 참조하여 Consumer를 선언한다.
Consumer<String> consumer = System.out::println;
consumer.accept("Hello World!!");
// 메소드 참조를 통해 Consumer를 매개변수로 받는 forEach를 쉽게 사용할 수 있다.
List<String> list = Arrays.asList("red", "orange", "yellow", "green", "blue");
list.forEach(System.out::println);
//interface Iterable<T>
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
2) Static 메서드 참조
Objects의 isNUll은 반환값이 Boolean, 매개변수 값은 1개, 매개변수가 Object이므로 Predicate로 다음과 같이 메서드 참조 가능
Predicate<Boolean> predicate = Objects::isNull;
// isNull 함수
public static boolean isNull(Object obj) {
return obj == null;
}
3) 생성자 참조
생성자는 new로 생성해주므로 클래스이름::new로 참조.
Supplier는 매개변수가 없이 반환값만을 갖는 인터페이스 → 매개변수 없이 String 객체를 새롭게 생성하는 String의 생성자를 참조하여 Supplier로 선언 가능하다.
참고자료
https://mangkyu.tistory.com/113
http://www.tcpschool.com/java/java_lambda_concept
'Java' 카테고리의 다른 글
[Java] 컬렉션 프레임워크(List, Set, Map, Iterator) (0) | 2023.10.11 |
---|---|
[Java] 제네릭스(Generics) (0) | 2023.10.11 |
[Java] 함수형 프로그래밍(Functional Programming) (0) | 2023.09.08 |
[Java] 예외 처리 (0) | 2023.08.26 |
[Java] 다형성(polymorphism) (0) | 2023.04.05 |