개발 공부/Java

함수형 인터페이스와 람다식

gmelon 2022. 4. 26. 16:27

스프링 및 자바 공부 중에 람다식을 통한 익명 내부 클래스를 종종 사용하게 되었는데 원리를 모르고 따라 사용하기만 하고 있는 듯해서 관련 내용을 공부하고 정리했다.

1. 함수형 인터페이스?

  • 추상 메서드가 한 개만 선언된 인터페이스를 말함.
    • 추상 메서드 외에 다른 static, default 메서드 존재 여부 및 개수는 관계 없음
  • @FuncationalInterface 어노테이션을 붙여 해당 인터페이스가 함수형 인터페이스인지 검증할 수 있다.
    • (필수는 아니나 함수형 인터페이스가 아닐 경우 오류 발생)
  • 자바에서는 기본적으로 여러 종류의 유용한 함수형 인터페이스를 제공한다.
  • ex) 함수형 인터페이스 Function
    • Generic으로 T 타입 인자를 받아 R 타입을 반환한다.
    • 수학의 ‘function’과 같은 역할. 즉, 입력이 있을 때 어떤 연산을 수행하고 출력을 반환한다.
@FunctionalInterface public interface Function<T, R> {
    R apply(T t);          
    // 기타 static, default 메서드 (생략)  
}

2. 익명 내부 클래스

  • 함수형 인터페이스와 람다를 사용하면 익명 내부 클래스를 쉽게 생성할 수 있다.
  • 기존 익명 내부 클래스 생성 방법
Function<Integer, Integer> f1 = new Function<>() {
    @Override     
    public Integer apply(Integer i) {         
        return i + 1;     
    } 
};
  • 람다식을 사용하는 방법
Function<Integer, Integer> f2 = (i) -> i + 1;

3. 람다식 문법

  • 기본적으로 (인자 리스트) → { 바디 } 형식으로 사용
    • 인자가 하나거나 바디가 한 구문일 경우 ()와 {}는 생략가능
    • 바디가 한 구문일 경우 return 문도 생략 가능
    • 인자가 여러 개일 경우 (a, b, c, ...) 와 같이 작성 가능
    • 인자 타입은 선택, 작성하지 않아도 컴파일러가 추론해줌
  • 변수 캡처
    • 람다 표현식에서 외부 변수를 참조하는 것을 말함
    • 람다 표현식에서는 final 변수만 참조할 수 있다.
      • 따라서 당연히 외부의 변수 값을 변경하지도 못한다.
      • 그렇지 않을 경우 동시성 문제가 발생할 수 있어 컴파일러에서 오류를 발생시킨다.
      • 명시적으로 final 선언이 이루어진 변수와 실질적으로 값이 변하지 않는 effective final 변수도 참조할 수 있다.

4. 자바에서 기본적으로 제공하는 함수형 인터페이스 종류

  • 자바에서 기본적으로 제공하는 함수형 인터페이스는 java.util.function 패키지에 정의되어 있다.
  • Untitled
  • 이들은 함수형 인터페이스이므로 람다식을 통해 앞서 첨부한 코드와 같은 방식으로 내부 익명 클래스를 생성하여 바로 사용할 수 있다.
  • Function<T, R>
    • 입력을 받아 출력을 반환하는 수학적인 함수와 가장 가까운 인터페이스
    • T 타입을 받아 R 타입을 반환한다.
    • R apply(T t)
  • BiFunctional<T, U, R>
    • T, U 두 타입 + 두 개의 input을 받아 R 타입을 반환한다.
    • R apply(T t, U u)
  • Consumer
    • T 타입을 받아서 아무 값도 리턴하지 않는 함수형 인터페이스
    • void Accept(T t)
  • Supplier
    • 인자 없이 T 타입의 값을 제공하는 함수형 인터페이스
    • T get()
  • Predicate
    • T 타입을 받아 boolean을 반환하는 함수형 인터페이스
    • boolean test(T t)
  • UnaryOperator
    • Function<T, R>의 특수한 형태 (T == R 인 경우)
    • 아래와 같이 Function<t, t>을 상속받아 만들어진다.
    @FunctionalInterface public interface UnaryOperator<T> extends Function<T,T>
    • T apply(T t)
  • BinaryOperator
    • BiFunction<T, U, R>의 특수한 형태 (T == U == R 인 경우)
    • T apply (T t, T u)
  • 이외에도 위의 인터페이스들을 조합, 응용해 만들어진 다양한 함수형 인터페이스가 존재한다.
  • 상황에 맞게 활용!

5. 메서드 래퍼런스

  • 람다가 하는 일이 기존 메소드의 호출이라면 메서드 레퍼런스를 사용해 간결하게 표현할 수 있다.
// 일반적인 람다식
Consumer<String> c1 = (s) -> System.out.println(s);
// 메서드 레퍼런스 사용
Consumer<String> c2 = System.out::println;

c1.accept("hi"); // "hi"
c2.accept("hi"); // "hi"
  • 메서드 레퍼런스는 static 메서드나 특정 객체의 인스턴스 메서드, 심지어 임의 객체의 인스턴스 메소드 참조가 가능하다.
    • 아래 코드는 임의 객체의 인스턴스 메서드 참조 예시로,
    • Arrays.sort가 두 번째 인자로 Comparator 를 받기 때문에 String의 인스턴스 메서드 compareToIgnoreCase를 넣을 수 있고 이는 static 메서드가 아니므로 names의 각각의 원소에 대해 임의의 인스턴스가 각각 생성되고 그에 대한 메서드가 참조된다.
      String[] names = {"hyun", "sang", "hyeok"};
      Arrays.sort(names, String::compareToIgnoreCase);
      System.out.println(Arrays.toString(names));

6. 함수형 인터페이스와 람다의 사용 이유?

  • 이를 위해서는 먼저 함수형 프로그래밍에 대해 이해해야 함.

6-1. 함수형 프로그래밍이란?

  • 함수는 특정 동작을 수행하는 코드의 뭉치
    • 특정 동작을 수행하는 코드를 하나로 묶어 필요할 때 호출해서 사용함
  • 이는 수학의 ‘함수'에서 기원한다.
    • 즉, 함수에 어떤 값을 전달하면, 전달된 값에 해당하는 결과를 반환한다.
    • 단, 수학에서의 함수와 다르게 프로그램에는 부수효과 (side-effect) 가 존재하여 함수에 같은 값을 전달하더라도 상황에 따라 다른 값을 반환할 수 있다.
  • 함수형 프로그래밍이란 First Class Object (1급 객체) 로 사용할 수 있는 것
    • 즉, 함수를 변수에 저장하거나 다른 함수의 인자, 반환 값 등으로 사용 가능
    • JAVA에서는 람다식을 통해 함수형 프로그래밍을 지원함

6-2. 함수형 프로그래밍의 이점

  • 함수형 프로그래밍에서의 함수는 순수 함수로, 부수 효과가 없다.
  • 순수 함수?
    • 함수 밖의 값을 변경하지 않아야 하고
    • 함수 밖의 값을 사용하지 않아야 한다.
    • 즉, 동일한 입력에 대해서는 항상 동일한 출력을 반환한다. (수학의 함수와 같이)
  • 부수효과가 없으면 좋은점?
    • 숨겨진 입력과 출력에 의해 함수가 의도하지 않은 출력을 반환할 수 있다.
    • 테스트 시에 해당 함수가 다른 함수와 엮여있으면 해당 함수만 단위 테스트하기 어렵다.
    • 정리하면, 복잡성이 낮아지고 테스트하기 쉬워지며 함수의 동작을 유추하기 더 쉬워진다.

6-3. 자바에서의 함수형 프로그래밍

  • 자바에서는 람다식을 사용하여 함수형 프로그래밍을 쉽게 사용할 수 있다.
    • 자바에서 람다 사용 시 함수는 특수한 형태의 Object 가 되기 때문에 이러한 특성을 활용해 자바에서 함수형 프로그래밍을 사용할 수 있다.
    • 람다식을 사용한 익명 객체는 final / effective final 변수만 참조할 수 있기 때문에 부수 효과가 없는 함수를 생성할 수 있다.
  • Stream 사용 시에 멀티 쓰레드 환경에서 병렬 처리가 가능하다는 장점때문에 사용, Stream은 인자로 함수형 인터페이스의 객체를 받는다.
    • → Stream의 메서드와 병렬 처리 관련해서는 추후에 포스팅해서 다시 정리할 예정.

참고

  1. 인프런 강의 - 더 자바, Java 8 (https://www.inflearn.com/course/the-java-java8)
  2. https://jsqna.com/fjs-4-pure-functions-and-curry/