공부하고 기록하는, 경제학과 출신 개발자의 노트

학습일지/Language

Java 함수형 인터페이스 - Lambda 표현식 - 메소드 레퍼런스 정리

inspirit941 2020. 10. 20. 12:54
반응형

함수형 인터페이스

  • 추상 메소드가 하나만 있는 인터페이스
  • 자바8에서부터 @FunctionalInterface Annotation을 지원.
  • static과 default로 선언하고 구현한 함수는 '추상 메소드'가 아니므로 괜찮다.

함수형 프로그래밍을 하기 위해서는

  • 해당 언어에서 함수(메소드)가 First Class Object여야 한다.
  • 해당 함수는 Pure Function이어야 한다.
    는 전제조건이 필요하고, 자바는 함수를 First Class Object로 취급하므로 프로그램 언어의 제약은 없다.

cf. 함수형 인터페이스와 lambda는 자바에서 지원하는 문법일 뿐, 함수형 프로그래밍을 강제하는 게 아니다.

First Class Object?

아래의 조건을 만족하는 객체를 First Class Object라고 한다.

  • 함수의 매개변수 값, 반환 값으로 사용할 수 있다.
  • 변수나 자료구조에 저장할 수 있다.

정수형, 문자열, 여러 자료구조들이 이 조건에 충족한다.

Java, Python 등의 언어에서는 함수도 이 조건에 부합한다.

함수가 함수를 매개변수로 받고, 함수를 리턴할 수 있는 경우 고차원 함수 (High-Order Function) 라고 한다.

참고자료

Pure Function?

수학적 함수라고도 생각할 수 있다. 아래 조건을 만족하는 함수를 말한다.

  • 함수 외부에 정의된 값을 변경하지 않는다
  • 함수 외부에 정의된 값을 참조하지 않는다.

즉, 같은 입력값을 넣으면 같은 결과값이 나오는 것을 보장하는 함수.

자바의 함수형 인터페이스

java.util.function 패키지 안에 정의되어 있음.

  1. Function< T,R >
    인풋 타입 T, 리턴 타입 R을 리턴하도록 하는 인터페이스
    • R apply(T) : T를 input으로, R을 output으로 하는 추상메소드
      함수 조합용 디폴트 메소드
    • andThen
    • compose
package com.inspirit941.java8to11;

import java.util.function.Function;

public class Plus10 implements Function<Integer, Integer> {
    @Override
    public Integer apply(Integer integer) {
        return integer + 10;
    }
}
public class RunSomethingImpl {
    public static void main(String[] args) {
        Plus10 p10 = new Plus10();
        p10.apply(1);

        // 아래처럼 한 번에 생성할 수도 있다.
        Function<Integer, Integer> p11 = (number) -> number + 11;
        p11.apply(1);
        // 조합 함수 사용법
        Function<Integer, Integer> multiply2 = (number) -> number * 2;

        // 1. compose.
        // multiply의 결과값을 받아서 p11에 입력값으로 넘긴다. 
        Function<Integer, Integer> multiply2AndPlus11 = p11.compose(multiply2);
        multiply2AndPlus11(2);
        // return 15 (2*2 + 11)

        // 2. andThen
        Function<Integer, Integer> Plus11Andmultiply2 = p11.andThen(multiply2);
        // return 26 ((2 + 11) * 2)
    }
}

 

  1. BiFunction< T, U, R >
    T, U 두 개의 입력을 받아 R을 리턴하는 인터페이스
    • R apply(T t, U u);

 

  1. Consumer< T >
    받기만 하고 리턴하지 않는 함수.

    Consumer<Integer> printT = (number) -> System.out.println(number)

    • void accept(T t)
  2. Supplier< T >
    어떤 값을 가져오는 인터페이스. 받아올 값의 타입 T를 정의한다.

    • T get();

      Supplier<Integer> get10 = () -> return 10;

      get10.get() // return 10;

 

  1. Predicate< T >
    인자값을 받아서 True / False 리턴.

    Predicate<String> startsWithEbay = (string) -> string.startswith("ebay");

    조합용 함수

    • negate() : 결과값에 not 붙이는 함수
    • and(), or()
  2. UnaryOperator< T >
    Function에서 입력값과 결과값의 타입이 같을 경우 사용가능.

    • Function을 상속받았기 때문에 Function에서 제공하는 메소드 기능은 그대로다.
      UnaryOperator<Integer> plus10 = (number) -> number + 10;
  3. BinaryOperator< T >
    BiFunction의 특수 형태. BiFunction은 input 타입 세 개가 전부 다를 거라는 전제로 만들어진 인터페이스.

    • Input 인자값 세 개가 전부 같은 타입일 경우 사용할 수 있다.

Lambda 표현식

  • 함수형 인터페이스의 인스턴스를 생성하는 방법.
  • 코드를 간결하게 생성하며
  • 메소드 매개변수, 리턴 타입, 변수로 만들어 사용할 수 있다.

표현식은 아래와 같다.

@FunctionalInterface
public interface RunSomething {
    // 함수형 인터페이스 : 인터페이스에 추상 메소드가 "한 개"만 주어져 있는 인터페이스
    int doit(int number);
    // 자바8에 추가된 기능
    // 추상메소드만 한 개인 것이므로, static이나 default로 구현되어 있는 메소드는 해당사항이 없음.
    static void printName(){
        System.out.println("inspirit941");
    }
    default void printAge(){
        System.out.println("99");
    }
}

public class RunSomethingImpl {
    public static void main(String[] args) {
        // 기존까지 추상메소드에 적용하던 익명 내부클래스 형태.
        RunSomething runSomething1 = new RunSomething() {
            @Override
            public int doit(int number) {
            }
        };


        // 아래 코드처럼로 간결하게 만들 수 있다.
        RunSomething runSomething2 = (number) -> {
            return number + 10;
        };
        // 메소드 호출방법은 동일함.
        runSomething.doit(1);

lambda에서 외부 변수를 참조하는 것도 가능하다.

단, 참조할 외부 변수의 상태가 final이라는 전제를 하기 때문에,

참조한 외부 변수 값을 변화시키는 코드를 작성할 경우 컴파일 에러가 발생.

int baseNumber = 10;
RunSomething runSomething2 = (number) -> {
    return number + baseNumber;
};
baseNumber ++; // 컴파일 에러 발생.
runSomething.doit(1);

lambda 표현식에서 외부 변수를 참조할 경우, 엄밀한 의미의 함수형 프로그래밍은 아니게 된다.

lambda 표현식 상세

핵심 키워드 : 변수 캡처와 Shadowing

lambda를 사용할 경우, 동일한 역할을 하는 내부클래스 / 내부익명클래스와 공통점 & 차이점이 있다.

자바8 이전까지는, 외부변수를 참조하려면 final로 선언해야 했다.

자바8에서 effective final이라는 개념이 도입됨.

  • final을 생략하더라도, 정의한 변수가 코드 내에서 값이 변하지 않을 경우 참조할 수 있음.
  • 값이 변할 경우 concurrency 문제가 생길 수 있기 때문에 컴파일 에러 발생.

아래 코드의 예시에서, baseNumber라는 값을 참조해도 컴파일 에러가 발생하지 않는다.

package com.inspirit941.java8to11;

import java.util.function.Consumer;
import java.util.function.IntConsumer;

public class testVariableCapture {
    public static void main(String[] args) {
        testVariableCapture test = new testVariableCapture();
        test.run();
    }
    private void run() {
        // 공통점: 여기서 정의한 baseNumber를 아래 세 개에서 전부 참조할 수 있다.
        // = 변수 캡쳐 (Variable Capture)
        int baseNumber = 10;


        // 차이점: Shadowing 여부.
        // 1. 로컬 클래스
        class LocalClass {
            void printBaseNumber(){
                int baseNumber = 11;
                System.out.println(baseNumber);
                // return 11;
            }
        }
        // 2. 익명 클래스
        Consumer<Integer> integerConsumer = new Consumer<Integer>(){
            @Override
            public void accept(Integer baseNumber) {
                // 메소드 내의 baseNumber는 더 이상 run 메소드에 정의한 baseNumber가 아니다.
                System.out.println(baseNumber);
                // 10이 아니라, accept 메소드가 입력받은 값을 리턴한다. 
            }
        };
        // 즉, 1과 2는 내부클래스에 별도의 scope이 생성되고,
        // 생성된 scope에 정의된 변수가 상위 scope 변수를 가리는 Shadowing이 발생한다.

        // 3. lambda
        // IntConsumer = Consumer 인터페이스 중 정수형을 받는 인터페이스.
        IntConsumer printInt = (number) -> {
            // int baseNumber = 11; 
            // 컴파일 에러 발생. 동일한 scope에서 두 번 정의했기 때문.
            System.out.println(number + baseNumber);
        };
        printInt.accept(10);
        // lambda를 사용할 경우, 별도의 scope이 생성되지 않는다.
        // 따라서 외부 변수를 내부에서 재정의할 경우 컴파일 에러가 발생하고, shadowing이 발생하지 않는다.
    }
}

메소드 레퍼런스

lambda가 하는 일이 기존 메소드 / 생성자를 호출하는 거라면,

메소드 레퍼런스를 사용해서 간결하게 표현할 수 있다.

  • 생성자 참조하기
  • 특정 객체의 인스턴스 메소드 참조하기
  • static 메소드 참조하기
  • 임의의 객체에 인스턴스 메소드 참조하기

코드 예시에 주석으로 설명을 달았다.

package com.inspirit941.java8to11;

import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

public class testMethodReference {
    public static void main(String[] args) {
        // 생성자 참조하기 - 1. 기본생성자.
        // 입력값은 없으나 리턴값은 존재하는 인터페이스: Supplier
        Supplier<Greeting> newGreeting = Greeting::new;
        // Supplier만으로 객체를 생성하는 것은 아님. get 메소드까지 적용해야 함.
        Greeting greeting0 = newGreeting.get();

        // 생성자 참조하기 - 2. 인자값을 받는 생성자.
        // 문자열을 받아서 객체를 리턴하므로 Function 사용
        Function<String, Greeting> newGreeting2 = Greeting::new;
        Greeting funcGreeting = newGreeting2.apply("createdByFunction");
        funcGreeting.getName();
        // return "createdByFunction"

        // 인스턴스 메소드 참조하기
        Greeting greeting1 = new Greeting();
        UnaryOperator<String> hello = greeting1::hello;
        System.out.println(hello.apply("this is Name"));
        // return "hello this is Name";

        // static 메소드 참조하기
        UnaryOperator<String> hi = Greeting::hi;
    }
}
package com.inspirit941.java8to11;

import java.util.Arrays;
import java.util.Comparator;

public class testInstanceMethodReference {
    public static void main(String[] args) {
        String[] names = {"name1", "name2", "name3"};
        // 정렬 기준을 지정하는 영역에 Comparator 인터페이스를 적용하던 기존 방식.
        Arrays.sort(names, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return 0;
            }
        });

        // lambda 형식으로 사용할 수 있다.
        Arrays.sort(names, (Comparator<String>) (o1, o2) -> 0);
        // 다시말해, 메소드 참조 방법을 사용하는 것도 가능하다.
        // name1 문자열에 name2 문자열을 넣고 비교해서 결과를 리턴하고, 다시 name2에 name3읗 넣고 비교하는 식으로 작동
        // 즉, 임의의 인스턴스에 해당 메소드를 참조하는 형태.
        Arrays.sort(names, String::compareToIgnoreCase);
    }
}
반응형