<aside> 💡 참조 블로그: https://mangkyu.tistory.com/112
</aside>
Stream API 특징
Stream API 연산 종류
Stream 연산은 크게 생성 → 가공 → 결과 만들기로 구성
생성
가공
결과
예시 코드
List<String> myList = Arrays.asList({"a1", "a2", "b1", "c2", "c1"});
myList
.stream() // 생성
.filter(s -> s.startsWith("C")) // 가공
.map(String::toUpperCase) // 가공
.sorted() // 가공
.count(); // 결과
람다식이란
함수를 람다식으로 표현하면 함수 이름이 필요없어지기 때문에 익명 함수의 한 종류
익명함수들은 모두 일급 객체
일반 함수 익명 함수 코드
// 기존의 일반 함수
public String hello() {
return "Hello World";
}
// 람다 방식(익명 함수)
() -> System.out.println("Hello World!");
람다식의 특징
함수형 인터페이스
함수형 인터페이스는 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션
인터페이스를 선언하여 단 하나의 추상 메서드를 갖도록 제한
함수형 인터페이스를 사용하는 이유 → 람다식이 함수형 인터페이스를 반환
코드 예제
@FuntionalInterface
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));
}
}
Java에서 제공하는 함수형 인터페이스
Supplier<T>
Consumer<T>
Function<T, R>
Predicate<T>
Supplier<T>
매개변수 없이 반환값만 가지는 함수형 인터페이스
Supplier<T>
는 T get()
를 추상 메서드로 가짐
@FunctionalInterface
public interface Supplier<T> {
T get();
}
// 사용 예시
Supplier<String> supplier = () -> "Hello World";
System.out.println(supplier.get());
Consumer<T>
객체 T를 매개변수로 받아서 사용하며, 반환값은 함수형 인터페이스
void accept(T t)
를 추상 메서드로 가짐
andThen()
이라는 함수를 추가로 제공하는데 이를 통해 함수가 끝난 후 다음 Consumer를 연쇄적으로 이용 가능
@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); }
}
}
// 사용 예시
Consumer<String> consumer = (str) -> System.out.println(str.split(" ")[0]);
consumer.andThen(System.out::println).accept("Hello World");
// 출력
Hello
Hello World
Function<T, R>
Function은 객체 T를 매개변수로 받아서 처리한 후 R로 반환하는 함수형 인터페이스
Function은 R apply(T t)
를 추상 메서드로 가짐
Consumer와 마찬가지로 andThen()
메서드 제공, 추가적으로 compose()
제공
andThen()
은 연쇄 실행을 위해 사용, cmopose()
는 첫 번째 함수 실행 이전에 먼저 함수를 실행하여 연쇄적으로 연결
또한 identity()
메서드는 자기 자신을 반환하는 static 함수
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
// 예시, 메소드 참조로 간소화 가능(String::length;)
Function<String, Integer> function = str -> str.length();
function.apply("Hello World");
Predicate <T>
객체 T를 받아 처리한 후 Boolean 반환
Boolean test(T t)
추상 메서드로 가짐
// 정의
@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);
}
}
// 예시
Predicate<String> predicate = (str) -> str.equals("Hello World");
predicate.test("Hello World");
메서드 참조
함수형 인터페이스를 람다식이 아닌 일반 메서드를 참조시켜 선언하는 방법
일반 메서드를 참조하기 위한 조건 3가지
참조 가능한 메서드는 일반, static 메서드, 생성자가 있으며 클래스이름::메서드이름 으로 참조
일반 메서드 참조
예를 들어 Function에 메서드 참조 적용
사용 예시
// 기존의 람다식
Function<String,Integer> function = (str) -> str.length();
function.apply("Hello World");
// 메서드 참조로 변경
Function<String, Integer> function = String::length;
function.apply("Hello World");
// 일반 메소드를 참조하여 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로 참조 가능
Supplier는 매개변수가 없이 반환값만을 갖는 인터페이스이기 때문에 매개변수 없이 String객체를 생성하는 코드 가능
사용 예시
Supplier<String> supplier = String::new;
Stream 생성하기
Collection의 Stream 생성
Collection인터페이스에는 stream()메서드가 정의되어 있어 Collection 인터페이스를 구현한 객체(List, Set 등)는 모두 stream()메서드를 통해 생성 가능
stream()메서드를 사용하면 해당 Collection의 객체를 소스로 하는 Stream 반환
예시 코드
// ArrayList의 stream()을 이용한 Stream 객체 생성
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> listStream = list.stream();
// 배열 원소를 이용해 Stream 객체 생성
Stream<String> stream = Stream.of("a", "b", "c");
Stream<String> stream = Arrays.from(new String[] {"a", "b", "c"});
// 위 예제와 달리 ing/long double과 같은 원시 자료형들을 사용하기 위한 Stream생성 가능
IntStream stream = Instream.range(4, 10); // 기존의 for문 대체 가능
Stream 가공(중간 연산)
Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션 만드는 연산
filter()함수는 인자로 함수형 인터페이스 Predicate를 받아 boolean을 반환하는 람다식을 작성하여 사용
예시 코드
// filter를 이용해 String 타입 Stream에서 "a"를 포함한 원소로 Stream 생성
Stream<String> stream = lit.stream().filter(name -> name.contains("a"));
Map(데이터 변환)
기존의 Stream 요소들을 반환하여 새로운 Stream을 형성하는 연산
map() 함수의 인자로 함수형 인터페이스 function을 받음
예시 코드
// names 리스트의 원소를 대문자로 만들어 Stream 생성
Stream<String> stream = names.stream().map(s -> s.toUpperCase());
// map 함수 메서드 참조 방식으로 사용
Stream<String> fileNameStream = fileStream.map(File::getName);
Sorted(정렬)
Stream의 요소들을 정렬하기 위해 sorted를 사용, 파라미터로 Comparator를 넘길 수 있음
인자 없이 호출한 경우 오름차순으로 정렬, 내림차순으로 하기 위해서는 Comparator의 reverseOrder를 이용
예시 코드
List<String> list = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");
// 기본적인 오름차순 정렬
Stream<String> stream = list.stream().sorted();
// Comparator를 이용한 내림차순 정렬
Stream<String> stream = list.stream().sorted(Comparator::reverseOrder);
Distinct(중복 제거)
Peek(특정 연산 수행)
Stream → 원시 Stream
Stream 결과 만들기(최종 연산)
Max/Min/Sum/Average/Count
Collect(데이터 수집)
FlatMap을 통한 중첩 제거