함수형 프로그래밍의 핵심 개념은 다음과 같다.
순수 함수는 실행할 때 부수효과가 일어나지 않고 매번 같은 결과를 반환하는 함수이다. 즉, 외부의 상태를 변경하지 않으면서 동일한 인자에 대해 항상 똑같은 값을 리턴하는 함수인 것이다.
public class Calculator {
public int sum(int x, int y) {
return x + y;
}
}
위 메서드에서는 sum()
메서드는 오직 매개변수의 영향 받고 외부의 요인에 영향을 받지 않기 때문에 순수함수이다.
만약 메서드가 멤버 변수를 사용하거나 멤버 변수의 상태를 변경한다면 순수 함수가 아닌것이다.
함수가 일급 객체라는 것은 함수의 인스턴스를 생성하여 해당 함수의 인스턴스를 참조하는 변수를 할당할 수 있음을 의미한다. String, List 또는 기타 객체를 참조하는 방법과 동일하게 함수를 다룰 수 있다.
자바의 메서드는 일급 객체가 아니기 때문에 메서드를 함수처럼 사용하는 최선의 방법은 람다식을 사용하는 것이다.
import java.util.function.Consumer;
// 람다식을 인터페이스 타입 변수에 할당
Consumer<String> c = (t) -> System.out.println(t);
import java.util.function.Consumer;
// 메소드 매개변수로 람다 함수를 전달
public static void print(Consumer<String> c, String str) {
c.accept(str);
}
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이 붙지 않아도 상수로 간주된다.
함수형 인터페이스란 인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 한다. 함수형 인터페이스를 사용하는 이유는 Java의 람다식이 함수형 인터페이스를 반환하기 때문이다.
@FunctionalInterface
어노테이션은 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션으로, 컴파일 타임에 에러를 잡을 수 있게 한다.
함수형 인터페이스에는 추상 메서드 외에 default 또는 static 메서드도 포함될 수 있다.
Supplier<T>
Supplier는 매개변수 없이 값을 제공하는 역할을 하는 함수형 인터페이스다. Supplier는 주로 어떤 계산을 한 후에 값을 제공하는 경우에 사용된다.
// 정의
@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 메서드를 통해 어떠한 동작을 수행한다.
// 정의
@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 메서드를 정의하고 있다.
// 정의
@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는 주로 조건을 검사하거나 필터링하는 데 활용된다.
and 메서드는 현재 Predicate와 다른 Predicate를 조합하여 새로운 Predicate를 생성한다. 새로운 Predicate는 두 조건이 모두 참일 때 참을 반환한다.
negate 메서드는 현재 Predicate의 결과를 반전시킨 새로운 Predicate를 생성한다. 현재 Predicate가 참이면 거짓을 반환하고, 거짓이면 참을 반환한다.
or 메서드는 현재 Predicate와 다른 Predicate를 조합하여 새로운 Predicate를 생성한다. 새로운 Predicate는 두 조건 중 하나 이상이 참일 때 참을 반환한다.
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;
https://mangkyu.tistory.com/113
https://developer.mozilla.org/ko/docs/Glossary/First-class_Function