-
Java 8 - 1. 함수형 인터페이스와 람다 표현식공부일기/Java 8 2020. 12. 6. 20:13
1. 함수형 인터페이스란?
- 추상 메소드를 단 하나만 가지고 있는 인터페이스
- static 메소드나, default 메소드는 몇 개를 갖고 있던 상관없으나 추상메소드는 단 하나여야 한다.
- @FunctionalInterface 어노테이션을 지닌 인터페이스
2. 람다 표현식이란?
-
함수형 인터페이스의 인스턴스를 만드는 방법으로 쓰일 수 있다.
- 함수형 인터페이스를 구현할 때 쓸 수 있는 표현 방법으로 코드를 간결하게 할 수 있다.
-
메소드의 매개변수, 리턴 타입, 변수로 만들어 사용할 수 있다.
3. 자바에서 함수형 프로그래밍
-
순수 함수 (Pure function)
- 함수 밖에 있는 값을 참조하거나 변경하려 하면 안 된다. 오직 함수 내부에서 쓰는 값(매개변수)만 사용하는 것을 지향해야 한다.
-
사이드 이팩트가 없다. (함수 밖에 있는 값을 변경하지 않는다.)
-
상태가 없다. (함수 밖에 있는 값을 사용하지 않는다.)
-
고차 함수 (Higher-Order Function)
-
함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있다.
-
불변성
4. 자바에서 기본으로 제공하는 함수형 인터페이스
-
자바에서 미리 정의해둔 자주 사용할만한 함수 인터페이스
-
Function<T, R>
-
BiFunction<T, U, R>
-
Consumer<T>
-
Supplier<T>
-
Predicate<T>
-
UnaryOperator<T>
-
BinaryOperator<T>
- 그 외 : java.util.function 패키지
-
Function<T, R>
T 타입을 받아서 R 타입을 리턴하는 함수 인터페이스
- 함수 조합용 메소드
-
andThen
-
compose
import java.util.function.Function; public class Practice { public static void main(String[] args) { Function<Integer, Integer> plus10 = (number) -> number + 10; System.out.println(plus10.apply(10)); // 결과 20 Function<Integer, Integer> multiply3 = (number) -> number * 3; System.out.println(multiply3.apply(3)); // 결과 9 Function<Integer, Integer> multiply3AndPlus10 = plus10.compose(multiply3); System.out.println(multiply3AndPlus10.apply(4)); // 결과 22 Function<Integer, Integer> plus10AndMultiply3 = plus10.andThen(multiply3); System.out.println(plus10AndMultiply3.apply(4)); // 결과 42 } }
실행결과
20 9 22 42
위 예제에서 Function<T, R> 의 형태로 함수형 인터페이스를 불러와 람다 표현식으로 정의했다. ( ) 안에 매개변수를 지정하고 -> 반환값; 을 입력해준다. 들어오는 값과 반환되는 값이 모두 Integer타입으로 정했기 때문에 plus10, multiply3 과 같은 함수들을 정의할 수 있다.
plus10.compose(multiply3)과 같은 조합으로도 사용할 수 있는데, 이때는 mutiply3을 먼저 실행하고 plus10을 실행한다.
반대로 plus10.andThen(multiply3) 같은 경우 plus10을 먼저 실행하고, multiply3을 실행한다.
BiFunction<T, U, R>
import java.util.Arrays; import java.util.List; import java.util.function.BiFunction; public class Practice { public static void main(String[] args) { BiFunction<Integer, Integer, Integer> func = (x1, x2) -> x1 + x2; Integer result = func.apply(2, 3); System.out.println(result); // 5 BiFunction<Integer, Integer, Double> func2 = (x1, x2) -> Math.pow(x1, x2); Double result2 = func2.apply(2, 4); System.out.println(result2); // 16.0 BiFunction<Integer, Integer, List<Integer>> func3 = (x1, x2) -> Arrays.asList(x1 + x2); List<Integer> result3 = func3.apply(2, 3); System.out.println(result3); // [5] } }
실행결과
5 16.0 [5]
Consumer<T>
T 타입을 입력받아 아무 값도 리턴하지 않음
- 함수 조합용 메소드
-
andThen
import java.util.function.Consumer; public class Practice { public static void main(String[] args) { Consumer<String> printCity = (city) -> System.out.println(city); printCity.accept("Seoul"); } }
실행결과
Seoul
Supplier <T>
T 타입의 값을 제공하는 함수 인터페이스
import java.util.function.Supplier; public class Practice { public static void main(String[] args) { Supplier<Integer> get10 = () -> 10; System.out.println(get10.get()); } }
실행결과
10
Predicate<T>
T 타입을 받아서 boolean을 리턴하는 함수 인터페이스
- 함수 조합용 메소드
-
And
-
Or
-
Negate
import java.util.function.Predicate; public class Practice { public static void main(String[] args) { Predicate<String> startsWithSeoul = (str) -> str.startsWith("Seoul"); Predicate<Integer> isOdd = (number) -> number % 2 == 1; System.out.println(startsWithSeoul.test("서울")); System.out.println(isOdd.test(30)); } }
실행결과
false false
.test( )로 매개변수를 받아 boolean 타입을 리턴한다. 조건식을 논리 연산자인 And, Or, Not으로 연결해 하나의 식으로 구성할 수 있는 것처럼, 여러 Predicate를 and(), or(), negate()로 연결해 하나의 새로운 Predicate를 만들 수도 있다.
import java.util.function.Predicate; public class Practice { public static void main(String[] args) { Predicate<Integer> under100 = (number) -> number < 100; Predicate<Integer> under200 = (number) -> number < 200; Predicate<Integer> isEven = (number) -> number%2 == 0; Predicate<Integer> notUnder100 = under100.negate(); Predicate<Integer> all = notUnder100.and(under200.or(isEven)); System.out.println(all.test(151)); String str1 = "abc"; String str2 = "abc"; Predicate<String> isSame = Predicate.isEqual(str1); boolean result = isSame.test(str2); System.out.println(result); } }
실행결과
true true
우측 끝의 isEven 부분부터 실행하게 되고, under200.or(true) > notUnder100.and(true) 의 과정을 거쳐 true가 출력된다.
String 비교시 Predicate.isEqual()을 사용할 수 있다.
UnaryOperator<T>
-
Function<T, R>의 특수한 형태로, 입력값 하나를 받아서 동일한 타입을 리턴하는 함수 인터페이스
import java.util.function.UnaryOperator; public class Practice { public static void main(String[] args) { UnaryOperator<Integer> plus10 = (number) -> number + 10; System.out.println(plus10.apply(10)); // 결과 20 UnaryOperator<Integer> multiply3 = (number) -> number * 3; System.out.println(multiply3.apply(3)); // 결과 9 } }
Fuction<T, R>에서 사용했던 예제를 UnaryOperator<T>의 형태로 바꿔보았다.
BinaryOperator<T>
-
BiFunction<T, U, R>의 특수한 형태로, 동일한 타입의 입렵값 두개를 받아 리턴하는 함수 인터페이스
import java.util.function.BiFunction; public class Practice { public static void main(String[] args) { BinaryOperator<Integer> func = (x1, x2) -> x1 + x2; Integer result = func.apply(2, 3); System.out.println(result); // 5 } }
BiFunction<T, U, R>에서 사용했던 예제를 UnaryOperator<T>의 형태로 바꿔보았다.
아래는 BinaryOperator를 이용한 참고할만한 예제
import java.util.Arrays; import java.util.List; import java.util.function.BinaryOperator; public class Practice { public static void main(String[] args) { Integer[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; Integer result = math(Arrays.asList(numbers), 0, (a, b) -> a + b); System.out.println(result); // 55 } public static <T> T math(List<T> list, T sum, BinaryOperator<T> accumulator) { T result = sum; for (T t : list) { result = accumulator.apply(result, t); } return result; } }
5. 람다 표현식
- 람다
- (인자 리스트) -> {바디}
- 바디가 한 줄일 경우 { } 생략 가능하다.
- 인자 리스트
- 인자가 없을 때: 0
- 인자가 한개일 대: (one) 또는 one
- 인자가 여러개 일 때: (one, two)
- 인자의 타입은 생략 가능, 컴파일러가 추론(infer)하지만 명시할 수도 있다. (Integer one, Integer two)
- 바디
-
화상표 오른쪽에 함수 본문을 정의한다.
-
여러 줄인 경우에 { }를 사용해서 묶는다.
-
한 줄인 경우에 생략 가능, return도 생략 가능.
-
- 변수 캡처 (Variable Capture)
-
로컬 변수 캡처
-
final이거나 effective final 인 경우에만 참조할 수 있다.
-
그렇지 않을 경우 concurrency 문제가 생길 수 있어서 컴파일가 방지한다.
-
-
effective final
- 자바 8부터 지원하는 기능으로 "사실상" final인 변수. 변수가 선언되고 변수를 어디서도 변경하지 않는 경우.
- final 키워드 사용하지 않은 변수를 익명 클래스 구현체 또는 람다에서 참조할 수 있다.
- 익명클래스 구현체와 달리 '쉐도윙'하지 않는다.
- 익명 클래스는 새로 스콥을 만들지만, 람다는 람다를 감 싸고 있는 스콥과 같다.
6. 메소드 레퍼런스
람다 표현식을 구현할 때 쓸 수 있는 방법으로 기존의 다른 메소드를 참조하는 것.
메소드 참조하는 방법
static 메소드 참조 타입::static 메소드 특정 객체의 인스턴스 메소드 참조 객체 레퍼런스::인스턴스 메소드 임의 객체의 인스턴스 메소드 참조 타입::인스턴스 메소드 생성자 참조 타입::new 예를들어 Greeting이라는 클래스가 이미 있고, 아래와 같은 함수를 실행하려한다고 보자.
블럭지정된 부분은 Greeting 클래스의 static함수 hi와 기능이 완전히 같다.
이런 경우 위의 메소드 참조하는 방법에 따라
import java.util.function.UnaryOperator; public class Practice { public static void main(String[] args) { UnaryOperator<String> hi = Greeting::hi; System.out.println(hi.apply("man")); } }
으로 표현하는 것이 메소드 레퍼런스이다.
실행결과
hi man
import java.util.function.UnaryOperator; public class Practice { public static void main(String[] args) { Greeting greeting = new Greeting(); UnaryOperator<String> hello = greeting::hello; System.out.println(hello.apply("youngbin")); } }
특정 객체의 인스턴스 메소드 레퍼런스 표현.
실행결과
hello youngbin
import java.util.Arrays; public class Practice { public static void main(String[] args) { String[] names = {"kim", "lee", "park", "jang"}; Arrays.sort(names, String::compareToIgnoreCase); System.out.println(Arrays.toString(names)); } }
임의 객체의 인스턴스 메소드 레퍼런스 표현 예시
실행결과
[jang, kim, lee, park]
import java.util.function.Supplier; public class Practice { public static void main(String[] args) { Supplier<Greeting> newGreeting = Greeting::new; System.out.println(newGreeting.get().hello("bye")); } }
생성자 참조 표현. newGreeting.get()을 해야 인스턴스를 실제로 만들게 된다.
실행결과
hello bye
import java.util.function.Function; public class Practice { public static void main(String[] args) { Function<String, Greeting> helloGreeting = Greeting::new; Greeting youngbin = helloGreeting.apply("youngbin"); System.out.println(youngbin.getName()); } }
문자열을 받는 생성자를 활용했다. 주의할 점은 Supplier<T>와 헷갈릴 수 있으나 getter 생성 후 확인해보면 다르다는 것을 알 수 있다.
'공부일기 > Java 8' 카테고리의 다른 글
Java 8 - 6. Concurrent 프로그래밍과 Executors (0) 2020.12.24 Java 8 - 5. Date & Time (0) 2020.12.23 Java 8 - 4. Optional (0) 2020.12.22 Java 8 - 3. Stream (0) 2020.12.16 Java 8 - 2. 인터페이스의 변화 (0) 2020.12.07