프로젝트를 진행하면서 기능 구현에서 스트림을 사용할 때마다 구글링해서 사용했다. (왜 그렇게 사용하는지 잘 모르지만 모두가 사용하는 방법대로 그냥 사용해옴) 마침 스터디 주제가 람다와 스트림이어서 이번에 정리하고자 한다.
스트림 연산은 람다식(함수형 인터페이스)을 인자로 사용하기 때문에 람다식과 함수형 인터페이스를 먼저 살펴본 후 스트림에 대 살펴볼 것이다.
람다식 (Lambda Expression)
자바 8 버전부터 지원했고 이것으로 자바는 객체지향언어와 함수형언어의 장점을 모두 가지게 되었다.
람다식은 메서드를 하나의 식으로 표현한 것인데 메서드의 이름과 반환값이 없기 때문에 익명 함수라고 부른다.
람다식은 일급 객체로 변수나 데이터에 담을 수 있고, 함수의 매개 변수로 전달될 수 있으며 함수의 반환값으로 사용 가능하다.
람다식을 사용하면 코드가 간결해지며 가독성이 좋아진다. 또, 병렬 처리에 유리해진다. (+내장 메서드 사용으로 구현이 쉬워짐)
람다식 표현 방법
람다식은 메서드 이름과 반환값 없이 사용하며 이를 적절히 생략할 수 있다.
//기본
PlusNumber plus = (int a, int b) -> { return a+b; }
//타입 생략
PlusNumber plus = (a,b) -> { return a+b; }
//매개변수 없을 경우
PlusNumber plus = () -> { return 0; }
//괄호 생략
//1. 인자 하나일 경우 중괄호 생략 가능 (return도 생략)
PlusNumber plus = (a,b) -> a+b;
//2. 매개변수 하나일 경우 소괄호 생략 가능
PlusNumber plus = a -> a;
메서드 참조
람다식에서 하나의 메서드만 호출하는 경우에 메서드 참조를 사용할 수 있다.
일반 메서드, static 메서드, 생성자를 참조할 수 있다.
//기존
str -> str.toString()
//메서드 참조
String::toString
람다식 주의사항
자바에서 모든 메서드는 클래스 안에 포함되어야 한다. 아까 말했듯이 람다식은 일급 객체다. 이름이 없는 익명 객체와 같다.
//람다식
(int a, int b) -> a+b
//익명 클래스
new Object(){
int plus(int a, int b){
return a+b;
}
}
람다식을 사용하기 위해서는 참조 변수에서 호출해야 한다. (익명 클래스의 메서드를 사용하려면 참조 변수에서 호출해야 함과 동일) 이때 참조 변수의 타입을 람다식과 동일한 메서드가 있는 인터페이스로 지정한다면 참조 변수에서 메서드를 구현하고 호출해 람다식을 사용할 수 있다.
interface CalFunction{
public abstract int plus(int a, int b);
}
CalFunction f = new CalFunction(){
int plus(int a, int b){
return a+b;
}
}
CalFunction f = (int a, int b) -> a+b; //위와 동일
int sum = f.plus(1,2);
함수형 인터페이스 (Functional Interface)
이처럼 람다식을 다루기 위한 인터페이스를 함수형 인터페이스라고 부른다. 람다식과 인터페이스의 메서드를 1:1로 연결하기 위해 함수형 인터페이스는 추상 메서드 한 개로 이뤄져야만 한다.
@FunctionallInterface를 사용하면 함수형 인터페이스를 제대로 정의했는지 컴파일러가 확인해준다.
람다식, 무명클래스 사용 비교
위의 CalFunction 인터페이스로 살펴본 람다식 사용 예시와 다른 예시로 살펴보겠다.
람다식을 사용하지 않고 무명클래스만 사용할 경우 함수형 인터페이스를 무명클래스로 구현해 넣어줘야 한다.
robot.work(new Something() {
@Override
public int doSome(int a, int b) {
return a+b;
}}
, 3, 4);
하지만 람다식을 사용할 경우에는 더 간단하게 같은 내용을 구현할 수 있다.
robot.work((a,b) -> a+b
, 3, 4);
자바에서 제공하는 함수형 인터페이스
자바에서는 자주 사용하는 함수형 인터페이스를 제공한다.
이름 | 사용 | 메서드 | 설명 |
Predicate | T → boolean | boolean test(T t) | 결과 예측 (조건식 표현) |
Consumer | T → void | void accept(T t) | 데이터 소비 |
Supplier | () → T | T get() | 데이터 공급 |
Function | T → R | R apply(T t) | 함수 (타입 변환에 주로 사용) |
Comparator | (T, T) → int | int compare(T t1,T t2) | 비교 |
Runnable | () → void | void run() | 정의된 코드만 실행 |
Callable | () → T | V call() | Supplier와 동일함, 병렬처리 위해 Runnable과 함께 등장 |
매개변수가 두 개면 인터페이스 이름 앞에 Bi가 붙는다.
기본적으로 제공되는 인터페이스를 이용해 편하게 람다식을 사용할 수 있다.
스트림 (Stream)
stream을 이용해 컬렉션 데이터를 쉽게 처리할 수 있게 되었다. stream은 일회성으로 사용 가능하며 데이터 소스를 변경하지 않고 내부 반복으로 작업을 처리한다.
stream은 컬렉션으로부터 생성된 후 중간 연산들로 데이터를 가공하고 최종 연산으로 결과를 반환하는 순서로 작동한다. (forEach처럼 결과가 없을 수도 있음)
stream 연산
stream 연산은 중간 연산과 최종 연산으로 나뉘어진다. 최종 연산이 수행되기 전까지 중간 연산이 수행되지 않는다.
한 요소씩 수직적으로 실행되기 때문에 요소의 범위를 줄이는 연산을 먼저 하면 성능을 향상시킬 수 있다.
중간 연산 | 최종 연산 |
데이터 가공 | 결과 반환 |
연산 결과가 stream이어서 중간 연산 연속으로 사용할 수 있음 | stream 요소들을 소모하며 연산 |
중간 연산 API
필터링, 정렬, 데이터 변환, 데이터 조회, 자르기 등의 연산이 있다.
list.stream().
.skip(2)
.limit(10)
.filter(i -> i>10)
.distint()
.sorted()
.peek(System.out::println) //stream에 아무 영향 안줌
.map(s -> s.toString())
최종 연산 API
조건 검사, 통계 내기, 데이터 수집 등의 연산이 있다.
list.stream()
.min();
//.max() , .count() , .sum() ...
list.stream()
.collect(Collectors.toList());
//toSet() , toMap() , groupingBy() , counting() , joining()...
list.stream()
.findFirst();
//allMatch() , anyMatch() ...
list.stream()
.forEach();