이 포스팅은 공부를 위해 망나니개발자 를 참고 하였습니다.
JDK8부터 Stream API와 람다식, 함수형 인터페이스 등을 지원하면서 Java를 이용해 함수형으로 프로그래밍할 수 있는 API 들을 제공해주고 있다. 그 중에서 Stream API는 데이터를 추상화하고, 처리하는데 자주 사용되는 함수들을 정의해두었다. 여기서 데이터를 추상화하였다는 것은 데이터의 종류에 상관 없이 같은 방식으로 데이터를 처리할 수 있다는 것을 의미하며, 그에 따라 재사용성을 높일 수 있다.
public static void main(String[] args) {
String[] nameArr = {"IronMan", "Coptain", "Hulk", "Thor"};
List<String> nameList = Arrays.asList(nameArr);
Arrays.sort(nameArr);
Collections.sort(nameList);
for (String str:nameArr) {
System.out.println(str);
}
for(String str:nameList){
System.out.println(str);
}
}
🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻
코드 가독성은 높이고, 데이터의 원본 손실 없이 함수형으로 리팩토링~~
// Stream 사용 후
String[] nameArr = {"IronMan", "Captain", "Hulk", "Thor"}
List<String> nameList = Arrays.asList(nameArr);
// 원본의 데이터가 아닌 별도의 Stream을 생성함
Stream<String> nameStream = nameList.stream();
Stream<String> arrayStream = Arrays.stream(nameArr);
// 복사된 데이터를 정렬하여 출력함
nameStream.sorted().forEach(System.out::println);
arrayStream.sorted().forEach(System.out::println);
Stream은 데이터를 처리하기 위해 다양한 연산들을 지원한다. Stream이 제공하는 연산을 이용하면 복잡한 작업들을 간단히 처리 할 수 있는데, 스트림에 대한 연산은 크게 생성하기, 가공하기, 결과만들기 3가지 단계로 나눌 수 있다.
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList
.stream() // 생성하기
.filter(s -> s.startsWith("c")) // 가공하기
.map(String::toUpperCase) // 가공하기
.sorted() // 가공하기
.count(); // 결과만들기
중간 연산이 세미콜론 없이 여러 번 연결되는 것은 해당 중간 연산이 Stream을 반환하기 때문이다. 이렇게 Stream 연산이 연결된 것을 연산 파이프라인이라고 하기도 한다. 최종 연산에서는 필요한 결과를 만들 수 있다. 위의 예제에서는 count()를 통해 남아 있는 요소의 갯수를 최종적으로 반환하도록 되어 있다. 물론 forEach와 같이 값을 반환하지 않는 최종 연산도 존재한다.
위의 예시 코드에서 확인 가능하듯 Stream 연산들은 매개변수로 함수형 인터페이스(Functional Interface)를 받도록 되어있다.
Stream 연산들은 매개변수로 함수형 인터페이스(Functional Interface)를 받도록 되어있다. 그리고 람다식은 반환값으로 함수형 인터페이스를 반환하고 있다
람다식(Lambda Expression)이란 함수를 하나의 식(expression)으로 표현한 것이다. 함수를 람다식으로 표현하면 메소드의 이름이 필요 없기 때문에, 람다식은 익명 함수(Anonymous Function)의 한 종류라고 볼 수 있다.
익명함수(Anonymous Function)란 함수의 이름이 없는 함수로, 익명함수들은 모두 일급 객체이다.
// 람다 방식
(매개변수, ... ) -> { 실행문 ... }
// 예시
() -> "Hello World!";
메소드의 이름이 불필요하다고 여겨져서 이를 사용하지 않는다. 대신 컴파일러가 문맥을 살펴 타입을 추론한다.
람다식 내에서 사용되는 지역변수는 final이 붙지 않아도 상수로 간주된다.
람다식으로 선언된 변수명은 다른 변수명과 중복될 수 없다.
결국 무조건 람다가 좋다는 보장은 없다. 상황에 따라 필요에 맞는 방법을 사용하는 것이 중요하다.
[ 함수형 인터페이스(Functional Interface) 란? ]
함수형 인터페이스란 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션으로, 인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 한다. 함수형 인터페이스를 사용하는 이유는 Java의 람다식이 함수형 인터페이스를 반환하기 때문이다.
함수형 인터페이스의 등장으로 우리는 함수를 변수처럼 선언할 수 있게 되었고, 코드 역시 간결하게 작성할 수 있게 되었다. 함수형 인터페이스를 구현하기 위해서는 인터페이스를 개발하여 그 내부에는 1개 뿐인 abstract 함수를 선언하고, 위에는 @FunctionalInterface 어노테이션을 붙여주면 된다.
@FunctionalInterface
interface MyLambdaFunction {
int max(int a, int b);
}
public class Lambda {
public static void main(String[] args) {
// 람다식을 이용한 익명함수
MyLambdaFunction lambdaFunction = (int a, int b) -> a > b ? a : b;
System.out.println(lambdaFunction.max(3, 5));
}
}
람다식으로 생성된 순수 함수는 함수형 인터페이스로만 선언이 가능하다는 점이다. 또한 @FunctionalInterface는 해당 인터페이스가 1개의 함수만을 갖도록 제한하기 때문에, 여러 개의 함수를 선언하면 컴파일 에러가 발생할 것이라는 점이다.
메소드 참조란 함수형 인터페이스를 람다식이 아닌 일반 메소드를 참조시켜 선언하는 방법이다. 일반 메소드를 참조하기 위해서는 다음의 3가지 조건을 만족해야 한다.
참조가능한 메소드는 일반 메소드, Static 메소드, 생성자가 있으며 클래스이름::메소드이름 으로 참조할 수 있다. 이렇게 참조를 하면 함수형 엔터페이스로 반환이 된다. 3가지의 메소드에 대해 메소드 참조 예시를 자세히 살펴보도록 하자.
예를 들어 위에서 보여준 Function에 메소드 참조를 적용한다고 하자. 우선 해당 메소드(length)가 위의 3가지 조건을 만족하는지 살펴보아야 한다.
String의 length 함수는 매개변수가 없으며, 반환형이 int로 동일하기 때문에 String::length로 다음과 같이 메소드 참조를 적용할 수 있다
// 기존의 람다식
Function<String, Integer> function = (str) -> str.length();
function.apply("Hello World");
// 메소드 참조로 변경
Function<String, Integer> function = String::length;
function.apply("Hello World");
또한 추가로 예시를 살펴보자. System.out.println() 메소드는 반환형이 void이며, 파라미터로 String을 받는 메소드이다. 그렇기 때문에 우리는 Consumer에 System.out.println() 메소드를 참조시킬 수 있다.
// 일반 메소드를 참조하여 Consumer를 선언한다.
Consumer<String> consumer = System.out::println;
consumer.accept("Hello World!!");
// 메소드 참조를 통해 Consumer를 매개변수로 받는 forEach를 쉽게 사용할 수 있다.
List<String> list = Arrays.asList("red", "orange", "yellow", "green", "blue");
list.forEach(System.out::println);
//interface Iterable<T>
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Static 메소드 역시 메소드 참조가 가능하다. 예를 들어 Objects의 isNull은 반환값이 Boolean이며, 매개변수 값은 1개이고, 매개 변수가 Object이므로 Predicate로 다음과 같이 메소드 참조가 가능하다.
Predicate<Boolean> predicate = Objects::isNull;
// isNull 함수
public static boolean isNull(Object obj) {
return obj == null;
}
생성자도 메소드 참조를 할 수 있다. 생성자는 new로 생성해주므로 클래스이름::new로 참조할 수 있다. Supplier는 매개변수가 없이 반환값만을 갖는 인터페이스이기 때문에, 매개변수 없이 String 객체를 새롭게 생성하는 String의 생성자를 참조하여 Supplier로 선언할 수 있다.
Supplier<String> supplier = String::new;