[Java] 함수형 인터페이스와 람다

Sony·2020년 4월 18일
1

자바(Java)

목록 보기
3/3

함수형 인터페이스(Functional Interface)란?

함수형 인터페이스단 하나의 추상 메소드만이 선언된 인터페이스이다.

함수형 인터페이스를 사용하면 입력에 의해서만 출력이 결정된다. 이러한 함수형 인터페이스라는 개념과 람다식 표현을 통해 Java8에서는 Side-effect가 없는 ‘순수한 함수’를 표현할 수 있게 되었다. 하지만, 단순히 구조만으로 순수성이 보장되지는 않는다. 입력으로 참조값이 오는 경우 Side-effect가 생긴다.

@Test
public void 함수를_반환_값으로_활용하는_예제() {
    Function<String, Integer> toInt = value -> Integer.parseInt(value);
    final Integer number = toInt.apply("100");

    assertThat(number).isEqualTo(100);
}
@FunctionalInterface
public interface Function<T, R> {

/**
 * Applies this function to the given argument.
 *
 * @param t the function argument
 * @return the function result
 */
R apply(T t);

함수형 인터페이스와 람다를 이용하면 구조적으로 유연하고 간결한 메서드를 만들 수 있다.

예제

techcourse.fp.mission.CalculatorList에 있는 sumAll, sumAllEven, sumAllOverThree 메서드 소스 코드를 확인하고, 람다를 활용해 중복 제거한다.

힌트

  • 변경되는 부분과 변경되지 않는 부분의 코드를 분리한다.
  • 변경되는 부분을 인터페이스로 추출한다.
  • 인터페이스에 대한 구현체를 익명 클래스(anonymous class)로 구현해 메소드의 인자로 전달한다.

인터페이스는 다음과 같은 형태로 추출할 수 있다.

public interface Conditional {
    boolean test(Integer number);
}

Conditional을 활용한 공통 메소드의 구조는 다음과 같다.

public int sumAll(List<Integer> numbers, Conditional conditional) {
        // conditional.test(number)를 활용해 구현할 수 있다.
    }

Calculator 클래스

package techcourse.fp.mission;

import java.util.List;

public class Calculator {
    public static int sumAll(List<Integer> numbers) {
        int total = 0;
        for (int number : numbers) {
            total += number;
        }
        return total;
    }

    public static int sumAllEven(List<Integer> numbers) {
        int total = 0;
        for (int number : numbers) {
            if (number % 2 == 0) {
                total += number;
            }
        }
        return total;
    }

    public static int sumAllOverThree(List<Integer> numbers) {
        int total = 0;

        //TODO: List에 담긴 값 중 3보다 큰 수만을 더해야 한다.

        return total;
    }
}

sumAll은 결국 모든 숫자를 더하는 메서드이므로 if 문true를 넣은 것과 같다.

package techcourse.fp.mission;

public interface Conditional {
    boolean test (Integer number);
}

Conditional 인터페이스를 만들고 각 메서드의 두 번째 인자로 인터페이스를 넣어준다.

public static int sumAll(List<Integer> numbers, Conditional conditional) {
    int total = 0;
    for (int number : numbers) {
        if (conditional.test(number)) {
            total += number;
        }
    }
    return total;
}

public static int sumAllEven(List<Integer> numbers, Conditional conditional) {
    int total = 0;
    for (int number : numbers) {
        if (conditional.test(number)) {
            total += number;
        }
    }
    return total;
}

public static int sumAllOverThree(List<Integer> numbers, Conditional conditional) {
    int total = 0;
    for (int number : numbers) {
        if (conditional.test(number)) {
            total += number;
        }
    }
    return total;
}

테스트 코드에서 두번째 인자로 각 메서드 조건에 맞는 Conditional 인터페이스 객체를 넣어준 후 결과를 확인한다.

Calculator 클래스 테스트 코드

package techcourse.fp.mission;

import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class CalculatorTest {
    private List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

    @Test
    public void sumAll() {
        int sum = Calculator.sumAllByCondition(numbers, new Conditional() {
            @Override
            public boolean test(Integer number) {
                return true;
            }
        });
        assertThat(sum).isEqualTo(21);
    }

    @Test
    public void sumAllEven() {
        int sum = Calculator.sumAllByCondition(numbers, new Conditional() {
            @Override
            public boolean test(Integer number) {
                return (number % 2) == 0;
            }
        });
        assertThat(sum).isEqualTo(12);
    }

    @Test
    public void sumAllOverThree() {
        int sum = Calculator.sumAllByCondition(numbers, new Conditional() {
            @Override
            public boolean test(Integer number) {
                return number > 3;
            }
        });

        assertThat(sum).

            isEqualTo(15);
    }
}

익명 클래스를 람다식으로 바꾸면 식이 더 간결해진다.

package techcourse.fp.mission;

import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class CalculatorTest {
    private List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

    @Test
    public void sumAll() {
        int sum = Calculator.sumAllByCondition(numbers, (number) -> true);
        assertThat(sum).isEqualTo(21);
    }

    @Test
    public void sumAllEven() {
        int sum = Calculator.sumAllByCondition(numbers, (number -> number % 2 == 0));
        assertThat(sum).isEqualTo(12);
    }

    @Test
    public void sumAllOverThree() {
        int sum = Calculator.sumAllByCondition(numbers, number -> number > 3);

        assertThat(sum).isEqualTo(15);
    }
}

이렇게 인터페이스를 이용해서 바꿔놓고 보니 결국 세 함수모두 공통된 부분을 갖고 있다.

package techcourse.fp.mission;

import java.util.List;

public class Calculator {
    public static int sumAll(List<Integer> numbers, Conditional conditional) {
        int total = 0;
        for (int number : numbers) {
            if (conditional.test(number)) {
                total += number;
            }
        }
        return total;
    }

    public static int sumAllEven(List<Integer> numbers, Conditional conditional) {
        int total = 0;
        for (int number : numbers) {
            if (conditional.test(number)) {
                total += number;
            }
        }
        return total;
    }

    public static int sumAllOverThree(List<Integer> numbers, Conditional conditional) {
        int total = 0;
        for (int number : numbers) {
            if (conditional.test(number)) {
                total += number;
            }
        }
        return total;
    }
}

따라서 다음과 같이 하나의 메서드로 표현 가능하다.

package techcourse.fp.mission;

import java.util.List;

public class Calculator {
    public static int sumAllByCondition(List<Integer> numbers, Conditional conditional) {
        return numbers.stream()
            .filter(conditional::test)
            .reduce(0, Integer::sum);
    }
}

인터페이스람다를 이용해서 공통된 부분을 추출했다. 그 결과 기존보다 더 유연하고 간결한 식을 만들 수 있게 되었다.

다음과 같이 하나의 메소드로 원하는 조건에 맞는 계산 결과를 얻을 수 있다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

Calculator.sumAllByCondition(numbers, (number) -> true)
Calculator.sumAllByCondition(numbers, (number) -> number % 2 == 0)
Calculator.sumAllByCondition(numbers, (number) -> number > 3)

Reference

profile
개발하며 배운점과 회고를 남기는 공간입니다.

0개의 댓글