Backend

모던 자바 인 액션 - 람다표현식, 함수형인터페이스

연_우리 2022. 7. 10. 16:21
반응형

목차

     

     

     

     

     

    기억하자. 람다는 기술적으로 자바8 이전의 자바로 할 수 없었던 일을 제공하는 것이 아니다.

     

     

     

    람다 표현식

     

    람다 표현식은 메서드로 전달할 수 있는 익명함수를 단순화한 것이다.

    람다 표현식은 이름은 없지만, 파라미터, 바디, return값, 예외를 가질 수 있다.

    //기존의 익명클래스를 이용한 정렬
    Comparator<Apple> byWeight = new Comparator<Apple>() {
    	public int compare(Apple a1, Apple a2) {
        		return a1.getWeight().compareTo(a2.getWeight());   //⭐
    	}
    };
    
    //람다를 이용한 정렬
    Comparator<Apple> byWeight = 
    	(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());  //⭐

    익명클래스를 사용할 때, compare 메서드의 body부분은 ⭐표시된

    a1.getWeight().compareTo(a2.getWeight()); 이다. (사실상 핵심로직)

     

    람다 표현식을 이용하면 compare메서드의 body를 직접 전달하는 것처럼 코드를 전달할 수 있다.

    다시말해, 람다 표현식을 이용하면 동작파라미터 형식의 코드를 쉽게 구현할 수 있다.

     

     

     

    람다 표현식 구성요소

    파라미터는 없어도 ( )를 작성해야한다. ( ) -> System.out.println("hello");
    람다 바디의 코드가 1줄이 넘어가면 { }로 묶어준다.  (int x, int y) -> { 
            System.out.println("result : ");
            System.out.println(x + y);
    }
    return문이 있다면 { }로 묶어준다. ( ) -> { return "Mario"; }

     

     

     

     

     

    람다는 어디에 쓸 수 있나?

    람다는 "함수형 인터페이스" 에서 사용할 수 있다.

     

     

    @FunctionalInterface 함수형 인터페이스

    함수형 인터페이스는 하나의 추상 메서드를 지정하는 인터페이스이다.

    public interface Comparator<T>{
    	int compare(T o1, T o2);
    }
    
    public interface Runnable{
    	void run();
    }

     

    람다 표현식은 함수형 인터페이스의 추상 메서드 구현을 직접 전달 할 수 있으므로,

    람다 표현식 자체를 함수형 인터페이스의 인스턴스로(하나의 객체처럼) 취급할 수 있게된다.

    //⭐Runnable은 함수형 인터페이스이다.
    public interface Runnable{
    	void run();
    }
    ----------------------------------------------------------------
    //람다를 Runnable의 변수에 할당
    Runnable r1 = () -> System.out.println("Hello World 1");	
    
    //익명클래스 사용
    Runnable r2 = new Runnable() {	
        public void run(){
            System.out.println("Hello World 2");
        }
    }
    
    public static void process(Runnable r){ //⭐
    	r.run();
    }
    
    process(r1);    //Hello World 1 
    process(r2);    //Hello World 2
    ------------------------------------------------------------
    //r1은 아래와 동일하게 표현할 수 있다.
    //⭐람다식 "() -> System.out.println(~)" 자체를 Runnable 인스턴스로 취급되었다.
    process( () -> System.out.println("Hello World 3") );

     

    ➡ 람다는 함수형 인터페이스의 변수로 할당할 수 있다.

    ➡ 람다는 함수형 인터페이스를 파라미터로 받는 메서드에 전달할 수 있다.

     

     

     

     

    함수형 인터페이스의 추상메서드 시그니처 = 람다식의 시그니처

    //함수형 인터페이스 Runnable
    public interface Runnable{
    	void run();
    }
    
    public void process(Runnable r){
    	r.run();
    }
    
    process( () -> System.out.println("hello!") );

    람다식 () -> System.out.println("hello!") 는 파라미터가 없으며 void를 반환한다. 

    Runnable 인터페이스의 run메서드 시그니처도 파라미터가 없으며 void를 반환한다.

    Runnable 인터페이스의 run메서드 시그니처와, 람다식의 시그니처가 같다.

     

     

    ➡ 함수형 인터페이스의 추상메서드 시그니처와, 람다식의 시그니처가 같아야만 람다를 사용할 수 있다.

     

     

     

    ✨람다, 함수형 인터페이스 활용 예제

    파일 읽고쓰기는 보통 파일을 열고(초기화/셋팅), 내용을 읽거나 쓰고(핵심로직), 버퍼 및 스트림을 종료하고(정리/마무리)하는 순서로 이루어진다.

    이렇게 실제 자원을 처리하는 코드(핵심로직)을 초기화/마무리 코드가 둘러싼 형태를 실행 어라운드 패턴이라고 부른다.

     

     

    아래의 코드는 파일에서 한번에 한줄만 읽을 수 있게되어있다.

    한번에 두줄을 읽게하려면 processFile을 두번 호출해야할까?? 

    기존의 초기화/마무리 코드는 재사용하고, processFile메서드만 다른 동작으로 수행하도록 하면 된다. (=동작을 파라미터화해라)

    public String processFile() throws IOException {
        try(
            BufferedReader br = new BufferedReader(new FileReader("data.txt"))
        ){
        	return br.readLine();
        }
    }

     

     

    람다는 함수형 인터페이스 자리에 사용할 수 있으니,

    BufferedReader를 파라미터로 받아 String을 반환하고, IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 생성해야한다.

    @FunctionalInterface
    public interface BufferedReaderProcessor {
    	String process(BufferedReader br) throws IOException;
    }

     

     

    processFile메서드에 함수형 인터페이스 BufferedReaderProcessor를 파라미터로 전달한다.

    public String processFile(BufferedReaderProcessor brp) throws IOException {
        try(
            BufferedReader br = new BufferedReader(new FileReader("data.txt"))
        ){
            return brp.process(br);
        }
    }

     

     

    이제 BufferedReader -> String 시그니처와 일치하는 람다를 전달할 수 있게되었다!

    String oneLine = processFile((BufferedReader br) -> br.readLine());
    String twoLine = processFile((BifferedReader br) -> br.readLine() + br.readLine());

     

     

     

    자바8에서 제공하는 함수형 인터페이스

    그렇다면, 위 예제처럼 람다를 사용하려면 매번 함수형 인터페이스를 생성해주어야할까??

    자바 8에는 이미 여러가지 함수형 인터페이스를 제공한다. 

     

    (함수형 인터페이스의 추상메서드 시그니처 함수 디스크립터라고 한다.)

    함수형 인터페이스 디스크립터 메서드
    Predicate T -> boolean boolean test(T t)
    Consumer T -> void void accept(T t)
    Supplier () -> t T get()
    Function<T, R> T -> R R apply(T t)
    ..... 생략    

     

     

     

    반응형

     

     

    컴파일러의 함수형 인터페이스 형식추론

    람다는 함수형 인터페이스의 인스턴스로 취급될 수 있다했다.

    하지만 람다식에는 어떤 함수형 인터페이스를 구현하는지 정보가 포함되어있지 않다.

     

    컴파일러는 람다가 사용되는 문맥을 파악해서 람다의 형식을 추론한다.

    List<Apple> heavierThan150g = 
    	filter(inventory, (Apple apple) -> apple.getWeight() > 150);

    1. 컴파일러는 filter메서드의 선언을 확인한다

    2. filter메서드는 두번째 파라미터로 Predicate<Apple> 대상형식을 기대한다.

    3. Predicate<Apple>은 test라는 한개의 추상 메서드를 정의하는 함수형 인터페이스다.

    4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.

    5. filter메서드로 전달된 인수는 이와 같은 요구사항을 만족해야한다.

     

     

     

    함수형 인터페이스 제약조건

    람다 표현식에서는 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 활용할 수 있다.

    단, 람다에서 참조하는 지역변수는 final로 선언되거나 한번만 할당되어야한다.

     

    왜??

    람다가 지역변수에 바로 접근할 수 있다는 가정하에

    람다가 스레드에서 실행되고 있을 때,

    변수를 할당한 스레드가 사라져 변수할당이 해제되었는데도

    람다에서는 해당 변수를 참조할 수 있기 때문이다.

     

    따라서 람다에서 지역변수를 참조하려면, 값이 바뀌지 않아야한다!!

     

     

     

     

     

     

     

     

     

     

    #람다 #자바 #자바8람다 #람다표현식 #함수형인터페이스 #functionalinterface #lambda #Predicate #Consumer #Function #Supplier 

    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기