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

초코칩·2023년 12월 8일
0

Java

목록 보기
3/14
post-thumbnail

함수형 프로그래밍

함수형 프로그래밍의 핵심 개념은 다음과 같다.

  • 일급 객체 함수
  • 순수 함수
  • 고차 함수

순수 함수

순수 함수는 실행할 때 부수효과가 일어나지 않고 매번 같은 결과를 반환하는 함수이다. 즉, 외부의 상태를 변경하지 않으면서 동일한 인자에 대해 항상 똑같은 값을 리턴하는 함수인 것이다.

public class Calculator {
	public int sum(int x, int y) {
    	return x + y;
    }
}

위 메서드에서는 sum()메서드는 오직 매개변수의 영향 받고 외부의 요인에 영향을 받지 않기 때문에 순수함수이다.

만약 메서드가 멤버 변수를 사용하거나 멤버 변수의 상태를 변경한다면 순수 함수가 아닌것이다.

일급 객체

함수가 일급 객체라는 것은 함수의 인스턴스를 생성하여 해당 함수의 인스턴스를 참조하는 변수를 할당할 수 있음을 의미한다. String, List 또는 기타 객체를 참조하는 방법과 동일하게 함수를 다룰 수 있다.

자바의 메서드는 일급 객체가 아니기 때문에 메서드를 함수처럼 사용하는 최선의 방법은 람다식을 사용하는 것이다.

  1. 모든 일급 객체는 변수나 데이터에 담을 수 있어야 한다.
import java.util.function.Consumer;

// 람다식을 인터페이스 타입 변수에 할당
Consumer<String> c = (t) -> System.out.println(t); 
  1. 모든 일급 객체는 함수의 파라미터로 전달할 수 있어야 한다.
import java.util.function.Consumer;

// 메소드 매개변수로 람다 함수를 전달
public static void print(Consumer<String> c, String str) {
    c.accept(str);
}
  1. 모든 일급 객체는 함수의 리턴값으로 사용할 수 있어야 한다.
import java.util.function.Consumer;

// 람다 함수 자체를 리턴함
public static Consumer<String> hello() {

        return (t) -> {
            System.out.println(t);
        };
    }

고차 함수

고차 함수는 하나 이상의 함수를 매개변수로 갖거나 다른 함수를 결과를 반환하는 함수이다. 자바는 람다식으로 고차 함수를 구현한다. 즉, 자바에서의 고차 함수는 하나 이상의 람다식을 인수로 받아오거나 다른 람다식을 반환하는 메서드이다.

대표적으로 Collections.sort 메서드가 존재한다.

Collections.sort(list, (String x, String y) -> {
	return x.compareTo(y);
})

Collections.sort의 첫 번째 매개변수는 List이고 두 번째 매개변수는 람다식이다. 이 람다식 매개변수는 Collections.sort를 고차 함수로 만든다.

람다식

람다식이란 메서드를 이름과 반환 값을 없애고 하나의 식인 익명함수로 표현하기 위한 한 방식이다.

람다식은 람다 매개변수, 화살표, 람다 실행문으로 총 세 가지 주요 요소로 구성된다.

int[] arr = new int[5];

// Before
int method() {
	return (int)(Math.random() * 5) + 1)
}

// After
Arrays.setAll(arr, (i) -> (int)(Math.random() * 5) + 1);

위 예제에서는 (i)가 매개변수, (int)(Math.random() * 5) + 1이 실행문이 된다.

람다식 내에서 사용되는 지역변수는 final이 붙지 않아도 상수로 간주된다.

장점

  1. 간결해서 이해하기 쉽다. 간결해지기 때문에 가독성에 도움을 준다.
  2. 객체 생성 없이 메서드 호출이 가능하다.
  3. 람다식이 메서드의 매개변수로 전달될 수 있고, 메서드의 결과로 반환될 수 있다. 그렇기 때문에 메서드를 변수처럼 다루는 것이 가능하다.
  4. Stream API의 매개변수로 전달이 가능하다.

단점

  • 익명 함수의 재상용이 불가능하다.
  • 재귀에 부적합하다.
  • 람다는 기본적으로 익명함수 기반이기 때문에, 익명 함수의 특성상 함수 콜 스택 추적이 매우 어렵다.

함수형 인터페이스

함수형 인터페이스란 인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 한다. 함수형 인터페이스를 사용하는 이유는 Java의 람다식이 함수형 인터페이스를 반환하기 때문이다.

@FunctionalInterface 어노테이션은 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션으로, 컴파일 타임에 에러를 잡을 수 있게 한다.

함수형 인터페이스에는 추상 메서드 외에 default 또는 static 메서드도 포함될 수 있다.

종류

Supplier<T>

Supplier는 매개변수 없이 값을 제공하는 역할을 하는 함수형 인터페이스다. Supplier는 주로 어떤 계산을 한 후에 값을 제공하는 경우에 사용된다.

  • get 메서드는 매개변수 없이 값을 반환한다.
// 정의
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

// 사용 예시
Supplier<String> supplier = () -> "Hello World!";
System.out.println(supplier.get());

// 출력
Hello World!

Consumer<T>

Consumer 인터페이스는 입력값을 받아서 어떤 동작을 수행하지만 리턴값은 없는 함수를 표현하는 데 사용된다. Consumer 인터페이스는 제네릭 타입 T를 받아서 accept 메서드를 통해 어떠한 동작을 수행한다.

  • accept 메서드는 매개변수로 전달된 값을 소비(consume)한다. 즉, 주어진 입력값을 가지고 어떠한 동작을 수행하는 역할을 한다.

andThen 메서드

  • Consumer 인터페이스의 andThen 메서드는 두 개의 Consumer를 순차적으로 연결하여 하나의 Consumer를 생성한다. 첫 번째 Consumer가 처리한 후에 두 번째 Consumer가 처리된다.
  • after 매개변수로 전달된 Consumer를 현재 Consumer 다음에 실행할 Consumer로 반환한다.
  • 새로운 Consumer는 두 Consumer의 동작을 연결한다.
// 정의
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

// 예시
import java.util.function.Consumer;

public class ConsumerExample {
    public static void main(String[] args) {
        // 첫 번째 Consumer: 문자열을 출력하는 동작
        Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());

        // 두 번째 Consumer: 문자열 길이를 출력하는 동작
        Consumer<String> printLength = str -> System.out.println("Length: " + str.length());

        // 두 개의 Consumer를 andThen을 사용하여 연결
        Consumer<String> combinedConsumer = printUpperCase.andThen(printLength);

        // 연결된 Consumer를 실행
        combinedConsumer.accept("hello");
    }
}


// 출력
Hello
Length: 5

Function<T, R>

Function은 입력을 받아 출력을 생성하는 함수를 표현하는 함수형 인터페이스이다. Function 인터페이스는 제네릭 타입 T를 입력으로 받아 제네릭 타입 R을 출력으로 생성하는 apply 메서드를 정의하고 있다.

  • apply 메서드는 입력값 t를 받아서 결과값 R을 생성한다.

compose 메서드

  • Function 인터페이스의 compose 메서드는 현재 Function을 다른 Function과 조합하여 새로운 Function을 생성한다. 새로운 Function은 다른 Function을 먼저 실행하고 그 결과를 현재 Function에 전달하여 실행한다.
  • before 매개변수로 전달된 Function을 현재 Function 이전에 실행할 Function으로 사용하여 새로운 Function을 반환한다.

andThen 메서드

  • Function 인터페이스의 andThen 메서드는 현재 Function을 다른 Function과 조합하여 새로운 Function을 생성한다. 새로운 Function은 현재 Function을 먼저 실행하고 그 결과를 다른 Function에 전달하여 실행한다.
  • after 매개변수로 전달된 Function을 현재 Function 이후에 실행할 Function으로 사용하여 새로운 Function을 반환한다.

identity 메서드

  • Function 인터페이스의 identity 메서드는 입력값을 그대로 반환하는 Function을 반환한다. 이는 두 개의 Function을 조합할 때 기본 동작으로 사용할 수 있다.
  • 항등 함수(identity function)를 반환한다.
// 정의
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

import java.util.function.Function;

public class FunctionExample {
    public static void main(String[] args) {
        // 첫 번째 Function: 문자열을 대문자로 변환
        Function<String, String> toUpperCase = String::toUpperCase;

        // 두 번째 Function: 문자열 길이를 반환
        Function<String, Integer> lengthFunction = String::length;

        // compose: toUpperCase를 먼저 적용하고 lengthFunction을 실행
        Function<String, Integer> composedFunction = lengthFunction.compose(toUpperCase);
        System.out.println("Compose Result: " + composedFunction.apply("hello")); // 5

        // andThen: toUpperCase를 먼저 실행하고 lengthFunction을 적용
        Function<String, Integer> andThenFunction = toUpperCase.andThen(lengthFunction);
        System.out.println("AndThen Result: " + andThenFunction.apply("hello")); // 5

        // identity: 항등 함수(identity function)
        Function<String, String> identityFunction = Function.identity();
        System.out.println("Identity Result: " + identityFunction.apply("hello")); // hello
    }
}

// 출력
Compose Result: 5
AndThen Result: 5
Identity Result: hello

Predicate<T>

Predicate는 주어진 조건에 따라 true 또는 false를 반환하는 함수를 표현하는 데 사용된다. Predicate는 주로 조건을 검사하거나 필터링하는 데 활용된다.

  • test 메서드는 주어진 조건에 따라 true 또는 false를 반환한다.

and 메서드

and 메서드는 현재 Predicate와 다른 Predicate를 조합하여 새로운 Predicate를 생성한다. 새로운 Predicate는 두 조건이 모두 참일 때 참을 반환한다.

negate 메서드

negate 메서드는 현재 Predicate의 결과를 반전시킨 새로운 Predicate를 생성한다. 현재 Predicate가 참이면 거짓을 반환하고, 거짓이면 참을 반환한다.

or 메서드

or 메서드는 현재 Predicate와 다른 Predicate를 조합하여 새로운 Predicate를 생성한다. 새로운 Predicate는 두 조건 중 하나 이상이 참일 때 참을 반환한다.

isEqual 정적 메서드

isEqual은 주어진 객체와 동일한지 여부를 검사하는 Predicate를 생성한다.

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
    
}

// 예시
import java.util.function.Predicate;

public class PredicateExample {
    public static void main(String[] args) {
        // 첫 번째 Predicate: 문자열 길이가 5 이하인지 확인
        Predicate<String> lengthCheck = str -> str.length() <= 5;

        // 두 번째 Predicate: 문자열이 "hello"인지 확인
        Predicate<String> isEqualToHello = Predicate.isEqual("hello");

        // and: 두 조건이 모두 참일 때 참
        Predicate<String> andPredicate = lengthCheck.and(isEqualToHello);

        // negate: 결과를 반전
        Predicate<String> negatePredicate = lengthCheck.negate();

        // or: 두 조건 중 하나 이상이 참일 때 참
        Predicate<String> orPredicate = lengthCheck.or(isEqualToHello);

        // 테스트
        System.out.println("andPredicate: " + andPredicate.test("hello")); // false
        System.out.println("negatePredicate: " + negatePredicate.test("hello")); // false
        System.out.println("orPredicate: " + orPredicate.test("hello")); // true
    }
}

// 출력
andPredicate: false
negatePredicate: false
orPredicate: true

메서드 참조

람다식이 하나의 메서드만 호출하는 경우에 더욱 간결하게 표현할 수 있다.

// Before
Function<String, Integer> f = (String s) -> Integer.parseInt(s);

// After
Function<String, Integer> f = Integer::parseInt;

생성자에도 메서드 참조를 적용할 수 있다.

// Before
Supplier<MyClass> s = () -> new MyClass();

// After
Supplier<MyClass> s = MyClass::new;

Ref

https://mangkyu.tistory.com/113
https://developer.mozilla.org/ko/docs/Glossary/First-class_Function

profile
초코칩처럼 달콤한 코드를 짜자

0개의 댓글