[Java] 람다(Lambda)

곰민·2022년 12월 24일
1
post-thumbnail

Lambda Expression


저번 functional interface에 이어서
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에서 참조가 가능합니다.
    Effective Final인 경우 세 가지 모두 참조가 가능합니다.

  • 이후에 변수의 상태가 변경되면 컴파일 에러 발생합니다.

메서드 레퍼런스


메서드 레퍼런스란?
람다 표현식을 통해서 직접 구현하는 것이 아니라 람다를 구현할 때 기존에 같은 행위를 하는 메서드가 존재한다면 그 메서드를 참조하는 것, 그 메서드 자체를 함수형 인터페이스의 구현체로 사용하는 것입니다.

  • 이전 함수형 인터페이스 포스팅에서 사용한 예제를 다시 사용하겠습니다.
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<Instant> timeSupplier = Instant::now;
    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)에서는 권장합니다.

참조


일급 객체 - 위키백과, 우리 모두의 백과사전

함수형 프로그래밍 이해하기

함수형 프로그래밍 언어에 대한 고찰

Chapter 15. Expressions

자바의 정석

Effective Java

profile
더 나은 개발자로 성장하기 위해 퀘스트 🧙‍♂🧙‍♂ 깨는 중입니다. 관심사는 back-end와 클라우드입니다.

0개의 댓글