Link Search Menu Expand Document

섹션 1. 함수형 인터페이스와 람다

함수형 인터페이스와 람다 표현식 소개

함수형 인터페이스 (Fuctional Interface)

  • 추상 메소드를 딱 하나만 가지고 있는 인터페이스로 SAM (Single Abstract Method) 라고도 함
  • @FunctionalInterface-> 추상 메소드 2개 이상일 경우 컴파일 에러 발생. 인터페이스를 견고하게 관리할 수 있음.

람다 표현식 (Lambda Expressions)

  • 함수형 인터페이스의 인스턴스를 만드는 방법으로 쓰일 수 있음
  • 코드를 줄일 수 있음
  • 메소드 매개변수, 리턴 타입, 변수로 만들어 사용할 수도 있음

자바에서의 함수형 프로그래밍

  • 함수를 First class object로 사용할 수 있다.
  • 순수 함수 (Pure function)
    • 사이드 이펙트가 없다. -> 함수 밖에 있는 값을 변경하지 않는다.
    • 상태가 없다. -> 함수 밖에 있는 값을 사용하지 않는다.
  • 고차 함수 (Higher-Order Function)
    • 함수가 함수를 매개변수로 받을 수도 있고 함수를 리턴할 수도 있다.
  • 불변성
    • 입력받은 값이 동일한 경우 결과값도 같아야 한다.

예제

RunSomething.java

@FunctionalInterface
public interface RunSomething {

    public int doit(int number);
}

Foo.java

public class Foo {

    public static void main(String[] args) {
        //anonymous inner class
		RunSomething runSomething = new runSomething() {
        	@Override
            public int doit(int number) {
            	return number + 1;
            }
        };
        
        //lambda expression
        RunSomething runSomething = number -> number + 1; 
        System.out.println(runSomething.doit(10));
    }
}
// 11

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

Java는 함수형 인터페이스를 java.lang.function 패키지에 기본으로 제공한다.

  • Function<T, R> : T 타입을 받아서 R 타입을 리턴하는 함수 인터페이스 -> R apply(T t)
  • UnaryOperator<T> : Function<T, R>의 특수한 형태로 T 타입을 받아서 T 타입을 리턴하는 함수 인터페이스
  • BiFunction<T, U, R> : 두 개의 값(T, U)를 받아서 R 타입을 리턴하는 함수 인터페이스 -> R apply(T t, U u)
  • BinaryOperator<T> : BiFunction<T, U, R>의 특수한 형태로 동일한 타입 T의 입력값 2개를 받아서 T 타입을 리턴하는 함수 인터페이스
  • Consumer<T> : T 타입을 받아서 아무 값도 리턴하지 않는 함수 인터페이스 -> void accept(T t)
  • Supplier<T> : 아무 값도 받지 않고 T 타입의 값을 제공하는 함수 인터페이스 -> T get()
  • Predicate<T> : T 타입을 받아서 boolean을 리턴하는 함수 인터페이스 -> boolean test(T t)

함수 조합용 메소드

  • andThen: A.andThen(B)에서 A를 먼저 처리함
  • compose: A.compose(B)에서 B를 먼저 처리함
  • and, or, negate: Predicate와 함께 사용. 조건식.

예제

import java.util.function.*;

public class Practice2 {
    public static void main(String[] args) {
        Function<Integer, Integer> plus10 = (i) -> i + 10;
        Function<Integer, Integer> multiply2 = (i) -> i * 2;
        System.out.println(plus10.compose(multiply2).apply(2));
        System.out.println(plus10.andThen(multiply2).apply(2));

        BiFunction<Integer, Integer, Integer> multiplyTwoValues = (a, b) -> a * b;
        System.out.println(multiplyTwoValues.apply(3, 5));

        Consumer<Integer> printT = (i) -> System.out.println(i);
        printT.accept(9);

        Supplier<Integer> get10 = () -> 10;
        System.out.println(get10.get());

        Predicate<String> startsWithKeesun = (s) -> s.startsWith("keesun");
        Predicate<String> endsWithEgg = (s) -> s.endsWith("Egg");
        Predicate<Integer> isEven = (i) -> i % 2 == 0;
        System.out.println(startsWithKeesun.and(endsWithEgg).test("keesunEgg"));

        UnaryOperator<Integer> plus10Unary = (i) -> i + 10;
        UnaryOperator<Integer> multiply2Unary = (i) -> i * 2;
        System.out.println(plus10Unary.compose(multiply2Unary).apply(2));

        BinaryOperator<Integer> multiplyTwoValuesBinary = (a, b) -> a * b;
        System.out.println(multiplyTwoValuesBinary.apply(3, 5));
    }
}

람다 표현식

람다

(인자 리스트) -> {바디}

인자 리스트

  • 인자가 없을 때 : ()
  • 인자가 한 개일 때 : (one) 또는 one
  • 인자가 여러 개일 때 : (one, two)
  • 컴파일러가 인자의 타입을 추론하므로 생략 가능하지만 명시할 수도 있다. : (Integer one, Integer two)

바디

  • 함수 본문을 정의한다.
  • 여러 줄인 경우에 {}를 사용해서 묶는다.
  • 한 줄일 경우 생략 가능, return도 생략 가능

변수 캡처 (Variable Capture)

  • 로컬 변수 캡처
    • final이거나 effective final인 경우에만 참조할 수 있다.
    • 그렇지 않을 경우 concurrency 문제가 생길 수 있어 컴파일러가 컴파일 에러를 낸다.
  • effective final (Java 8부터 지원)
    • final 키워드를 사용하지 않아도 값이 변하지 않는 변수를 익명 클래스 구현체 또는 람다에서 참조할 수 있다.
  • 익명 클래스 구현체와 달리 shadowing하지 않는다.
    • 익명 클래스는 새로운 scope를 만들지만 람다는 새로운 scope를 만들지 않는다.

예제

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

public class Practice3 {

    public static void main(String[] args) {
        Practice3 practice3 = new Practice3();
        practice3.run();
    }

    private void run() {
        int baseNumber = 10;
        // baseNumber++; -> 컴파일 에러 발생

        // 로컬 클래스
        class LocalClass {
            void printBaseNumber() {
                int baseNumber = 11;
                System.out.println(baseNumber); // 11
            }
        }

        // 익명 클래스
        Consumer<Integer> IntegerConsumer = new Consumer<Integer>() {
            Integer baseNumber = 11;
            @Override
            public void accept(Integer integer) {
                System.out.println(baseNumber); // 11
            }
        };

        // 람다
        IntConsumer printInt = (i) -> {
             // int baseNumber = 11; 컴파일 에러
            System.out.println(i + baseNumber);
        };

        printInt.accept(10);
    }

}

메소드 레퍼런스

참조 방법문법
스태틱 메소드 참조타입::스태틱 메소드
특정 객체의 인스턴스 메소드 참조객체 레퍼런스::인스턴스 메소드
임의 객체의 인스턴스 메소드 참조타입::인스턴스 메소드
생성자 참조타입::new
  • 메소드 또는 생성자의 매개변수로 람다의 입력값을 받는다.
  • 리턴값 또는 생성한 객체는 람다의 리턴값이다.

예제

Greeting.java

public class Greeting {

    private String name;

    public Greeting() {
    }

    public Greeting(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public String hello(String name) {
        return "hello " + name;
    }

    public static String hi(String name) {
        return "hi " + name;
    }
}

Practice4.java

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

public class Practice4 {

    public static void main(String[] args) {
        // Static Method Reference
        UnaryOperator<String> hi = Greeting::hi;
        System.out.println(hi.apply("MELT"));
		
        // Instance Method Reference
        Greeting greeting = new Greeting();
        UnaryOperator<String> hello = greeting::hello;
        System.out.println(hello.apply("MELT"));
        
        // Constructor Reference
        Supplier<Greeting> greeting1 = Greeting::new;
        Function<String, Greeting> greeting2 = Greeting::new;
		
        Greeting newGreeting1 = greeting1.get(); // Greeting()
        Greeting newGreeting2 = greeting2.apply("MELT"); // Greeting(String name)

        System.out.println(newGreeting1.hello("MELT"));
        System.out.println(newGreeting2.getName());
		
        // Class Method Reference
        String[] names = {"MELT", "melodist", "melt.ep00"};
        Arrays.sort(names, String::compareToIgnoreCase);
        System.out.println(Arrays.toString(names));
    }
}