서론
저번 함수형인터페이스(functional interface)에 이어서
Lambda Expression(람다식)에 대해서 학습한 것에 대해 포스팅을 하려고 합니다.
람다식, 함수형 프로그래밍, 람다 이전 익명클래스, 변수 캡처에 따른 쉐도잉, 메서드 래퍼런스 순으로 알아보도록 하겠습니다.
Lambda Expression
람다식(Lambda Expression)이란?
메서드를 하나의 식(expression)으로 표현한 것.
메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로
람다식을 익명 함수(anonymous function)라고도 한다.
람다 형식.
(인자리스트) -> 바디
단 단일 실행문일 경우 {} 함수 몸체 괄호를 생략할 수 있습니다.
예시
Supplier<Integer> get10 = () -> 10; //body가 한줄이면 생략 가능
Supplier<Integer> get10 = () -> {
return 10; //body가 두줄이면 이렇게 블록으로 만들 수도 있음.
};
() -> {} // No parameters; result is void
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body
() -> { // Complex block body with returns
if (true) return 12;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type parameter
(int x) -> { return x+1; } // Single declared-type parameter
(x) -> x+1 // Single inferred-type parameter
x -> x+1 // Parentheses optional for
// single inferred-type parameter
(String s) -> s.length() // Single declared-type parameter
(Thread t) -> { t.start(); } // Single declared-type parameter
s -> s.length() // Single inferred-type parameter
t -> { t.start(); } // Single inferred-type parameter
(int x, int y) -> x+y // Multiple declared-type parameters
(x, y) -> x+y // Multiple inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
(x, final y) -> x+y // Illegal: no modifiers with inferred types
컴파일러가 매개변수 타입을 통해 타입 추론이 가능하기 때문에 타입 생략이 가능합니다.
BinaryOperator<Integer, Integer> get10 = (a, b) -> a + b;
//(Integer a, Integer b) -> a+b 와 같이 타입을 적어줘도 되지만
//BinaryOperator<Integer, Integer> 변수 선언부에 정의를 할 수 있고 해당 변수 선언부의
// 타입을 토대로 추론이 가능하기 때문에 타입을 적어두지 않아도 된다.
Effective java(익명 클래스보다는 람다를 사용하라 item42)에서는
타입을 명시해야 코드가 더 명확할 때,
매개변수 타입을 컴파일러가 추론하지 못해서 직접 프로그래머가 명시해 줘야 할 때,
두 가지를 제외하고는 생략하는 것을 권장합니다.
함수형 프로그래밍
Java8에서 람다는 함수형 프로그래밍을 지원하기 위해서 추가되었습니다.
그렇다면 함수형 프로그래밍이란 무엇일까요?
함수형 프로그래밍이란?
함수형 프로그래밍은 자료 처리를 수학적 함수의 계산으로 취급하고
상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다. -wiki-
함수형 프로그래밍의 특징
불변성과 순수 함수.
함수형 프로그래밍은 가변 데이터를 멀리하고 변경 가능한 상태를 최대한 제거하려고 노력하는 프로그래밍언어입니다.
순수 함수를 지향하는 프로그래밍 언어라고 설명하기도 합니다.
순수 함수란?
내부에 상태를 갖지 않아 같은 입력에 대해서는 항상 같은 출력이 보장되는 함수입니다.
상태 값에 따른 side effect 즉 예상하지 못한 값이 리턴되는 것이 없는 함수이기도 합니다.
함수의 출력값은 항상 함수의 입력값에만 영향을 받습니다.
- 오직 입력값의 영향만 받기 때문에 내부 상태 값이 예측하지 못하는 상황에 변경되거나 하지 않기 때문에 테스트 코드 작성이 쉽고 프로그램이 예측 가능해집니다.
- 여러 스레드에서 프로그램 상태 값을 공유하기 때문에 발생하는 동시성 이슈, Lock과 Synchronize와 같은 스레드 관련 문제에서 벗어나 핵심 로직에 집중할 수 있습니다.
일급 시민, 객체(First Class Citizen) , 고차 함수(higher-order functions)
함수형 프로그래밍에서 함수는 일급 시민입니다(type, object, entity, 또는 value 라고도 합니다.)
🤔 일급 시민(First Class Citizen)이란?
다른 객체들에 일반적으로 적용 가능한 연산 모두를 지원하는 객체를 가킨다.
1. 모든 요소는 함수의 실제 매개변수가 될 수 있다.
2. 모든 요소는 함수의 반환 값이 될 수 있다.
3. 모든 요소는 할당 명령문의 대상이 될 수 있다.
4. 모든 요소는 동일 비교의 대상이 될 수 있다.
🤔 고차 함수란?
적어도 아래 두 가지 중 한 가지를 수행하는 함수이다.
1. 하나 이상의 함수를 인수로 취한다.
2. 함수를 결과로 반환한다.
public static void main(String[] args){
//익명 내부 클래스
DoSomething dosomething = () -> System.out.println("Hello");
}
위 와 같이 람다를 사용하여 변수에 할당하고, 메서드에 파라미터로 정리하고, 리턴 타입으로 리턴을 할 수도 있습니다.
즉 람다 표현식을 일급 시민으로서 고차 함수의 표현이 가능해집니다.
익명 클래스와 람다.
예전에는 자바에서 함수 타입을 표현할 때 추상 메서드를 하나만 담은 인터페이스(드물게는 추상 클래스)를 사용했습니다.
이런 인터페이스를 함수 객체(function object)라고 하여, 특정 함수나 동작을 나타내는 데 썼고
이후 JDK1.1에 등장과 함께 주요 수단은 익명 클래스가 되었습니다.
- effective java 익명 클래스보다는 람다를 사용하라 item 42-
익명 클래스에 대한 예시를 확인해 보도록 하겠습니다.
예시 A
문자열을 길이 순으로 정렬하는데, 정렬을 위한 비교 함수로 익명 클래스를 사용하는 익명 클래스
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length()); }
});
예시 B
간단한 익명 클래스 예시
public interface DoSomething(){
void doItNow();
}
public static void main(String[] args){
DoSomething dosomething = new DoSomething(){
@Override
public void doItNow(){
System.out.println("Hello");
}
}
}
익명 클래스를 활용하는 자바 코드가 너무 길기 때문에 자바는 함수형 프로그래밍에 적합하지 않았었습니다.
하지만 함수형 인터페이스를 람다식을 사용해 만들 수 있게 되면서 훨씬 간결하게 사용이 가능해졌습니다.
예시 A
Collections.sort(words,
(s1, s2) -> Integer.compare(s1.length(), s2.length()));
예시 B
public static void main(String[] args){
//익명 내부 클래스
DoSomething dosomething = () -> System.out.println("Hello");
}
Java8 이후부터는 위와 같이 람다 형식으로 변경이 가능합니다.
람다는 함수형 인터페이스의 인스턴스를 생성하며 함수형 인터페이스를 인라인으로 구현한 object로 볼 수 있습니다.
🤔 그렇다면 무조건 람다를 사용하는 것이 좋을까요?
Effective Java(익명 클래스보다는 람다를 사용하라 item42)에서는 람다를 아래 사항에 맞춰 권장합니다.
1. 람다는 이름이 없고 문서화도 못하기 때문에 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다.
2. 람다는 한 줄 일 때 가장 좋고 세 줄 안에 끝내는 것이 좋다
3. 람다가 길어질 경우에는 람다를 쓰지 않는 쪽으로 리팩터링 하는 것을 권장한다.
변수 캡처
그렇다면 익명 클래스와 로컬 클래스 그리고 람다는 어떤 부분이 다를까요?
public static void main (String[] args){
Foo foo = new Foo();
foo.run();
}
private void run() {
final int baseNumber = 10;
class LocalClass(){
void printBaseNumber(){
System.out.println(baseNumber);
}
}// 로컬 클래스
Consumer<Integer> integerConsumer = new Consumer<Integer> () {
@Override
public void accept(Integer integer){
System.out.println(baseNumber);
}
}// 익명 클래스
IntConsumer printInt = (i) -> {
System.out.println(i + baseNumber);
}// 람다
}
- run 메서드에 내부 블록에서 int baseNumber라는 변수를 참조하게 되는 경우에
익명 클래스와 로컬 클래스(이너클래스)와는 람다가 다른 점이 존재합니다.
🚀 섀도잉
로컬 클래스와 익명 클래스는 각각의 scope가 다르기 때문에
해당 scope 내에서 선언한 변수들이 run() 메서드에서 선언한 로컬 변수인 baseNumber를
섀도잉 즉 가리고 있게 됩니다.
하지만 Lambda는 Lambda를 감싸는 scope와 scope가 동일하기 때문에
섀도잉 하지 않으며 body에서 값을 출력 시 동일한 값이 출력이 되게 됩니다.
물론 변수 정의 시 동일한 scope에서 동일한 이름의 변수 생성이 불가능합니다.
람다에서의 this 역시 바깥 인스턴스를 가리키게 됩니다.
Effective Final
lambda 내부에서 사용되는 변수는 final 이어야 사용이 가능합니다.
하지만 java8부터는 final를 생략할 수 있는 경우가 존재합니다.
사실상 해당 변수가 final인 경우 즉 선언되고 값이 변경된 적이 없는 경우에는
Effective Final이라고 표현하며 lambda body에서 참조가 가능합니다.
예시에서의 baseNumber와 같이 이후에 변수의 상태가 변경되면 컴파일 에러 발생합니다.
메서드 레퍼런스
메서드 레퍼런스란?
람다 표현식을 통해서 직접 구현하는 것이 아니라 람다를 구현할 때 기존에 같은 행위를 하는 메서드가 존재한다면 그 메서드를 참조하는 것, 그 메서드 자체를 함수형 인터페이스의 구현체로 사용하는 것입니다.
이전 함수형 인터페이스 포스팅에서 사용한 예제를 다시 사용하겠습니다.
public class FItest {
public static void main(String[] args) {
Supplier<String> stringSupplier = CItest::getString;
Supplier<Instant> timeSupplier = Instant::now;
System.out.println("stringSupplier = " + stringSupplier.get());
System.out.println("timeSupplier = " + timeSupplier.get());
}
}
..///
public class CItest{
private static String getString() {
return "stringTest";
}
}
Supplier<String> stringSupplier = CItest::getString;
CItest라는 클래스의 getString라는 메서드를 함수형 인터페이스의 구현체로 사용하겠다는 의미입니다.
public class FItest {
public static void main(String[] args) {
CItest citest = new CItest();
Supplier<String> stringSupplier = CItest::getString;
System.out.println("timeSupplier = " + timeSupplier.get());
}
}
- 인스턴스 메서드인 경우 객체 생성을 한 뒤 참조 가능합니다.
- 위의 익명 클래스 예시에 나왔던 예시 A와 예시 B에 적용한다면 보다 간결하게 사용 가능합니다.
예시 A
Collections.sort(words, comparingInt(String::length));
//...
words.sort(comparingInt(String::length));
//list에 추가된 sort 메서드를 이용하면 더욱 간결하게 사용가능.
예시 B
DoSomething dosomething = () -> System.out::println("Hello");
하지만 그렇지 않은 경우도 존재합니다.
메서드 참조 표현식이 짧지도 명확하지도 않은 예시
service.execute(GoshThisClassNameIsHumongous::action);
..///
service.execute(() -> action());
람다를 활용하여 구현하는 게 훨씬 간단하고 명확합니다.
람다가 익명 클래스보다 나은 점 중에서 가장 큰 특징은 간결함입니다.
메서드 레퍼런스는 함수 객체를 람다보다도 더 간결하게 해 주니
메서드 참조 쪽이 더 짧고 명확한 경우에는 메서드 참조를 활용하고
그렇지 않을 경우에는 람다를 사용하는 것을 effective java(람다보다는 메서드 참조를 사용하라 item 43)에서는 권장합니다.
참조
'Programming Language > Java' 카테고리의 다른 글
[Effective Java] Stream은 주의해서 사용해라(Item 45) (1) | 2023.01.12 |
---|---|
[Java] Stream (Stream이 밀려온다) (0) | 2023.01.09 |
[Java] Functional Interface(함수형 인터페이스) (0) | 2023.01.08 |
[Java] Equals vs Hashcode 그리고 재정의 (0) | 2023.01.08 |
[EffectiveJava] 정적 팩터리 메서드(Static Factory Method) 장단점 (1) | 2022.11.19 |
댓글