JDK1.8부터 람다식이 도입된 후 자바는 객체지향언어인 동시에 함수형 언어가 되었다.
람다식은 메소드의 이름과 반환값을 생략한 것으로 익명함수라고도 한다.
굳이 메소드 대신 람다식을 쓰는 이유는 뭘까?
메소드는 반드시 클래스 내에 포함되어 있어 객체 생성 후 메소드를 호출할 수 있다. 그러나 람다식은 이러한 과정없이 메소드의 역할을 대신할 수 있기 때문이다.
메소드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통 {} 사이에 ->를 추가한다.
반환타입 메소드이름 (매개변수 선언) -> { 문장들 }
int max (int a, int b) {
return a > b ? a : b;
}
(a, b) -> a > b ? a : b;
선언된 매개변수의 타입이 추론 가능한 경우 생략 가능하여 이처럼 간단하게 표현할 수 있다.
함수형 인터페이스@FunctionalInterface
는 람다식을 다루기 위한 인터페이스로 단 하나의 추상 메소드만 정의되어야 한다.
java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메소드를 함수형 인터페이스로 미리 정의해 놓았다.
함수형 인터페이스 | 메소드 | 설명 |
---|---|---|
java.lang.Runnable | void run() | 매개변수도 없고, 반환값도 없음 |
Supplier<T> | T get() | 매개변수는 없고, 반환값만 있음 |
Consumer<T> | void accept(T t) | Supplier와 반대로 매개변수만 있고, 반환값이 없음 |
Function<T, R> | R apply(T t) | 하나의 매개변수를 받아서 결과 반환 |
Predicate<T> | boolean test(T t) | 조건식을 표현하는데 사용 |
람다식이 하나의 메소드만 호출하는 경우 클래스 이름::메소드
또는 참조변수::메소드이름
으로 바꿀 수 있다.
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
Function<String, Integer> f = Integer::parseInt;
스트림은 데이터 소스를 추상화하고, 자주 사용되는 메소드들을 정의해 놓았다. 그 결과 데이터 소스에 상관없이 같은 방식으로 다룰 수 있게 되었으며 코드의 재사용성이 높아졌다. 스트림을 이용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.
이전에 배웠던 배열이나 컬렉션 프레임워크의 경우 List 정렬 시 Collections.sort()를 사용하고 배열 정렬 시 Arrays.sort()를 사용하는 번거로움이 있었는데 스트림은 이러한 문제점을 해결하였다.
String[] strArr = {"aaa", "bbb", "ccc"};
List<String> strList = Arrays.asList(strArr);
Arrays.sort(strArr);
Collections.sort(strList);
for (String str : strArr) {
System.out.println(str);
}
for (String str : strList) {
System.out.println(str);
}
String[] strArr = {"aaa", "bbb", "ccc"};
List<String> strList = Arrays.asList(strArr);
// 스트림 생성
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr);
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);
둘 다 데이터를 정렬하고 출력하는 코드인데 스트림을 이용할 경우 훨씬 코드가 간결한 것을 확인할 수 있다.
스트림은 데이터 소스를 변경하지 않는다. (only read)
스트림은 일회용이다. (Iterator와 비슷)
스트림은 작업을 내부 반복으로 처리한다.
스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있는데 중간 연산과 최종 연산으로 분류할 수 있다.
stream.distinct().limit(5).sorted().forEach(System.out::println)
스트림은 최종 연산이 수행되기 전까지 중간 연산이 수행되지 않는다.
병렬 스트림은 자동적으로 연산을 병렬로 수행한다.