비유를 들어 이해한다. 함수형 프로그래밍이 아닌건, 각각의 파트는 시계, 특정 기계와 같이 공유 기기를 두고 자신의 할일을 다한다. 반면 함수형 프로그래밍은 컨베이어 벨트처럼, 특정 인풋이 있으면 동일한 아웃풋을 내는 구조를 가진다. 이때, 각각의 파트는 서로가 공유하는 기기 없이 진행한다.
add(num1: Int, num2: Int) -> add(2, 3)
add(num1: Int)(num2: Int) -> add(2)(3)
람다는 한 마디로 코드 블록이다. 기존 코드 블록은 반드시 메서드 내에 존재했어야 했고, 이르 위해선 익명 객체가 필요했다. 하지만 자바 8부터는 코드 블록인 람다를 메서드의 인자나 반환 값으로 사용할 수 있게 됐다. 이 의미는 코드 블록을 변수처러 사용할 수 있다는 것이다.
MyTest mt = new MyTest();
Runnable r = mt;
r.run();
...
class MyTest implements Runnable {
public void run() {
System.out.println("Hello Lamda");
}
}
// 인터페이스 구현체를 활용한 방식
->
Runnable r = new Runnable() {
public void run() {
System.out.println("Hello Lamda");
}
};
r.run();
// 별도의 클래스 정의 없이 코드 블록인 메서드를 사용하고자 할 때 맣이 사용되던 익명 객체를 활용한 방식 (자바 8 이전이라면 이 코드가 최선)
->
Runnable r = () -> {
System.out.println("Hello Lamda");
}
r.run();
// 람다 도입
Runnable r = () -> System.out.println("Hello Lamda");
추상 메서드를 하나만 갖는 인터페이스를 자바 8부터는 함수형 인터페이스라고 하고, 이런 함수형 인터페이스만을 람다식으로 변경할 수 있다.
@FunctionalInterface
interface MyFunctionalInterface {
public abstract int runSomthing(int count);
}
// 다른 패키지에서 인터페이스 선언 후,
MyFunctionalInterface mfi = (int a) -> { return a + a};
// 메인에서 이렇게 사용
->
MyFunctionalInterface mfi = (a) -> {return a + a};
// a가 int형일 수밖에 없음을 runSomething interface 메서드 정의에서 알 수 있다. 따라서 int형은 생략하 수 있다. 이를 타입 추정 기능이라고한다.
->
MyFunctionalInterface = a -> {return a + a}
MyFunctionalInterface = a -> a+a
// 이렇게 인자가 하나이고 자료형을 표기하지 않을 경우 소괄호를 생략할 수 있다. 또한, 코드가 한줄일 경우 줄괄호 또한 생략할 수 있다. 다만 이때 return 구문과 세미콜론을 생략 해야한다.
// 최종본
public void test() {
MyFunctionalInterface mfi = todo();
int result = mfi.runSomthing(3);
System.out.println(result);
}
public MyFunctionalInterface todo() {
return num -> num + num;
}
// 메서드 호출인자로 람다 사용
public static void main(String[] args) {
MyFunctionalInterface mfi = a -> a+a;
todo(mfi);
//todo(a -> a+a);
// 람다식을 한번만 사용할 경우 굳이 변수에 할당할 필요 없이, 바로 넘길 수도 있다.
}
public static void todo(MyFunctionalInterface mfi) {
int b = mfi.runSomthing(5);
System.out.println(b);
}
---
// 메서드 반환값으로 람다 사용
public static void main(String [] args) {
MyFunctionalInterface mfi = todo();
int result = mfi.runSomthing(3);
System.out.println(result);
}
public static MyFunctionalInterface todo() {
return num -> num + num;
}
함수형 인터페이스 | 추상메서드 | 용도 |
---|---|---|
Runnable | void run() | 실행할 수 있는 인터페이스 |
Supplier | T get() | 제공할 수 있는 인터페이스 |
Consumer | void accept(T t) | 소비할 수 있는 인터페이스 |
Function<T, R> | R apply(T t) | 입력을 받아서 출력할 수 있는 인터페이스 |
Predicate | Boolean test(T t) | 입력을 받아 참/거짓을 단정할 수 있는 인터페이스 |
UnaryOperator | T apply (T t) | 단항(Unary) 연산할 수 있는 인터페이스 |
함수형 인터페이스 | 추상메서드 | 용도 |
---|---|---|
BiConsumer<T, U> | void accpet(T t, U u) | 이항 소비자 인터페이스 |
BiFunction<T, U> | R apply(T t, U u) | 이항 함수 인터페이스 |
BiPredicate<T, U> | Boolean test(T t, U u) | 이항 단정 인터페이스 |
BinaryOperator<T, T> | T apply(T t, T t) | 이항 연산 인터페이스 |
람다는 다양한 용도가 있지만, 그 중에서도 컬렉션 스트림을 위한 기능에 크게 조첨이 맞춰져 있다. 이 경우 개발자가 <를 써야할 곳에 <= 초기값 설정 등 실수를 방지하고 무엇보다 코드를 깔끔하게 작성할 수 있다.
즉, HOW가 아닌, What을 지정하여, 선언적 프로그래밍을 중시한다. SQL문을 작성할 때 '어떻게 하라!'가 아닌, '무엇을 원한다'라고 선언하는것과 유사하다. 또한, 스트림은 메서드 체인 패턴을 이용해 최종 연산이 아닌 모든 중간 연산을 다시 스트림에 반환해 코드를 좀더 간략히 작성할 수 있게 지원한다.
for(int i =0; i< ages.length; i++) {
if (ages < 20) {
System.out.println(ages[i]);
}
}
->
for(int age : ages) {
if(age < 20) {
System.out.println(age);
}
}
Arrays.stream(ages) // 기본 배열을 이용해 스트림을 얻기 위해 Arrays 클래스의 stream() 정적 메서드 이용
.filter(age -> age < 20) // SQL 구문에서 where 절과 같은 역할인데 이전에 얘기했던 Predicate 함수형 인터페이스를 filter 메서드의 인자로 제공하면 된다.
.forEach(age -> System.out.println(age)); // 스트림 내부 반복을 실행하는 forEach aptjemdlsep, 전달된 인자를 소비하는 함수형 인터페이스 즉, Consumer를 요구한다.
Arrays.stream(ages).mapToInt(age -> age).sum();
Arrays.stream(ages).mapToInt(age -> age).average();
Arrays.stream(ages).mapToInt(age -> age).min();
Arrays.stream(ages).mapToInt(age -> age).max();
Arrays.stream(ages).allMatch(age -> age > 20);
Arrays.stream(ages).findFirst();
Arrays.stream(ages).findAny();
Arrays.stream(ages).sorted().forEach(System.out::println);
Arrays.stream(ages).sorted().forEach(System.out::println);
=
Arrays.stream(ages).sorted().forEach(age -> System.out.println(age));
람다식이 들어갈 자리에 이상하게 표시된것처럼 보이는데, 위 람다식은 인자를 아무런 가공 없이 그대로 출력한다. 이런 코드를 사용할 때 메서드 레퍼런스라고하는 간략한 형식을 쓸 수 있다.
Arrays.stream(nums)
.map(num -> Math.sqrt(num))
.forEach(sqrtNum -> System.out.println(sqrtNum));
Arrays.stream(nums)
.map(Math::sqrt)
.forEach(System.out::println);
// 클래스::정적메서드 형태로 바뀐것으로, 람다식의 인자가 정적 메서드의 인자가 된다.
// 인스턴스::인스턴스메서드 형태로 바뀐것으로, 인스턴스 메서드의 인자가 된다.
BiFunction<Integer, Integer, Integer> bip_lamda = (a, b) -> a.compareTo(b)
BiFunction<Integer, Integer, Integer> bip_method_reference = Integer::compareTo
// 클래스::인스턴스메서드 형태로 바뀐것으로, 첫번째 인자는 인스턴스가 되고 그 다음 인자(들)는 인스턴스 메서드의 인자(들)가 된다.
클래스::new
TEST test1 = TEST::new; // ERROR!
Supplier<TEST> factory = TEST::new;
// Supplier<TEST> factory = () -> new TEST(); 와 동일
TEST test2 = factory.get(); // SUCCESS
/*
생성자 레퍼런스로 생성한 것은 TEST 클래스의 객체가 아니라 함수형 인터페이스 구현 그 자체이기 때문이다. 위에서 사용한 생성자는 인자가 없는 기본 생성자 이기 때문에 이를 만족하는 Supplier 함수형 인터페이스를 사용해 생성자 자체에 대한 참조가 만들어진다.
*/
// BEFORE JAVA8 -> HOW 중심의 외부 반복
int sum =0;
int count = 0;
for(Employee emp : emps) {
if(emp.getSalary() > 100_100_000) {
sum += emp.getSalary();
count++;
}
}
double average = (double) sum / count;
// AFTER JAVA8 -> 연산에 필요한 매개변수 없이 WHAT 중심의 내부 반복을 사용하여 직관적인 코드 작성 가능. 무엇보다 위에서 사용한 sum, count 등을 고려하지 않고 한눈에 파악 가능
double average = emps.stream()
.filter(emp -> emp.getSalary() > 100_100_000)
.mapToInt(Employee::getSalary)
.average() // 다른 메서드도 많음
.orElse(0);
스트림 생성 -> 필터링1 -> 필터링2 - - - > 결과 만들기
보통 배열과 컬렉션을 통해 만들지만 이외에도 다양한 방법이 있다.
List<String> list = Arrays.asList("A", "B", "C");
Stream<String> stream = list.stream();
String[] array = new String[] {"A", "B", "C"};
Stream<String> stream = Arrays.stream(array);
Stream<String> stream = Stream<String>.builder()
.add("Apple")
.add("Banana")
.add("Melon")
.build();
Stream.generate( () -> "Hello").limit(5)
// generate() 메소드 인자로 "Hello"를 넘길 수 있는데, limit 메소드를 통해 5개만 생성한다.
Stream.iterate(100, n -> n+ 10).limit(5)
// iterate() 메소드로 초기 값 100부터 110, 120 --- 140까지 생성하는 스트림을 생성한다.
Stream.empty();
// 빈 스트림을 생성할 수 있다.
Stream<Product> parallelStream = list.parallelStream(); // 병렬 스트림 생성
boolean isParallel = parallelStream.isParallel(); // 병렬 여부 확인
boolean isMany = parallelStream
.map(product -> product.getAmount() * 10)
.anyMatch(amount -> amount > 200);
이 외에도 primitive, wrapping type, 문자 스트림, 파일스트림이 있다.
이 단계에선 특정 데이터만 걸러내거나 데이터에 대해서 가공하는 작업 후 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(Chaining) 작성할 수 있다.
스트림 내 요소들을 하나씩 평가해서 걸러내는 작업이다. 인자로 받는 Predicate는 boolean을 리턴하는 함수형 인터페이스로 람다형이다.
Stream<T> filter(Predicate<? super T> predicate);
Stream<String> stream = names.stream()
.filter(name -> name.contains("A"));
// [Elenam, Java ---]
스트림 내 요소들을 하나씩 특정 값으로 변환해준다. 이때 값을 변환하기 위해 람다로 인자를 받는다. 즉, 스트림에 들어가 있는 값이 input이 되어서 특정 로직을 거친 후 output이 되어(리턴되는) 새로운 스트림에 담기게 된다.
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Stream<String> stream = names.stream()
.map(String::toUpperCase)
// names에 담긴 문자열 리스트를 모두 대문자로 만들기[ELENAM, JAVA]
Stream<Integer> stream = productList.stream()
.map(Product::getAmount);
// Product 타입의 리스트에서 Product 개체의 수량을 가져온다. [23, 14, 12]
인자로 mapper를 받고 있는데, 리턴 타입은 Stream이다. 즉, 새로운 스트림을 생성해서 리턴하는 람다를 넘겨야한다. flatMap은 중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어주는 역할을 수행한다.
List<List<String>> list = Arrays.asList(Arrays.asList("A"), Arrays.asList("B"));
// [[A], [B]]
List<String> flatList =
list.stream()
.flatMap(Colection::stream)
//flatMap(list -> list.stream()) 약간 스트림 이어 붙이는 느낌
.collect(Collectors.toList());
// [A, B]
students.stream()
.flatMapToInt(student ->
IntStream,of(student.getKor(), student.getEng(), student.getMap()))
//80, 90, 100 이런식의 스트림이 나온거지 현재
.average()
.ifPresent(avg -> System.out.println(avg));
map은 어떤 타입을 반환하는 반면 flatMap은 중첩 구조에서 한단계를 제거한(타입변환) 스트림을 생성해준다. 이렇게 나온 스트림을 가지고 다시 연산을 처리할 수 있다.
정렬은 Comparator를 이용한다.
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
IntStream.of(14, 11, ---)
.sorted()
.boxed()
.collect(Collectors.toList());
// [11, 14 --- ]
list.stream()
.sorted((s1, s2) -> s2.length() - s1.length())
.collect(Collectors.toList());
스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드이다. 'peek'은 그냥 확인해본다는 뜻으로 특정 결과를 반환하지 않는 함수형 인터페이스 Consumer를 인자로 받는다.
따라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과(스트림 반환)에 영향을 미치지 않는다. 작업을 처리하는 중간에 겨롸를 확인해볼 때 사용할 수 있다.
Stream<T> peek(Consumer<? super T> action);
int sum = IntStream.of(1, 3, 5, 7, 9)
.peek(System.out::println)
.sum();
가공된 스트림을 가지고 내가 사용할 결과값을 만들어내는 단계이다.
스트림 API는 다양한 종료 작업을 제공한다. 최대, 최소, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있다.
만약 스트림이 비어있는 경우 count와 sum은 0을 출력하면 되지만 평균, 최소, 최대의 경우에는 표현할 수 없어 Optional을 이용해 리턴한다.
long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
스트림은 reduce라는 메소드를 이용해서 결과를 만드들어낸다. 총 세가지의 파라미터를 받을 수 있는데,
// 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);
// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);
// 3개 (combiner)
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
같은 타입의 인자 두개를 받아 같은 타입의 결과를 반환해주는 함수형 인터페이스로, 아래 예제에서는 6(1+2+3)이다.
OptionalInt reduced =
IntStream.range(1, 4) // [1, 2, 3]
.reduce((a, b) -> {
return Integer.sum(a, b);
});
이번엔 두개의 인자를 받는데, 10은 초기값이고, 스트림 내 값을 더하는 결과는 16(10+1+2+3)이며, 람다는 메소드 참조를 이용해서 넘길 수 있다.
int reducedTwoParams =
IntStream.range(1, 4) // [1, 2, 3]
.reduce(10, Integer::sum); // method reference
Collector 타입의 인자를 받아서 처리하며, 자주 사용하는 작업은 Collectors 객체에서 제공하고 있다.
Collectors.toList()
스트림에서 작업한 결과를 담은 리스트를 반환한다.
List<String> collectorCollection =
productList.stream()
.map(Product::getName)
.collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]
Collectors.joining()
스트림에서 작업한 결과를 스트링으로 이러 붙일 수도 있다.
Collectors.joining 은 세 개의 인자를 받을 수 있다.
String listToString =
productList.stream()
.map(Product::getName)
.collect(Collectors.joining());
// potatoesorangelemonbreadsugar
String listToString =
productList.stream()
.map(Product::getName)
.collect(Collectors.joining(", ", "<", ">"));
// <potatoes, orange, lemon, bread, sugar>
Collectors.averageingInt
이외에도 summingInt, summarizingInt, groupingBy, partitioningBy(조건에 따라 분류해서 데이터 모아줌)가 있으며, intStream으로 바꿔주는 mapToInt 메소드를 사용해서 좀 더 간단하게 표현할 수도 있다.
Double averageAmount =
productList.stream()
.collect(Collectors.averagingInt(Product::getAmount));
// 17.2
Collector.of()
필요한 로직이 있다면 직접 collector를 만들 수도 있다.
public static<T, R> Collector<T, R, R> of(
Supplier<R> supplier, // new collector 생성
BiConsumer<R, T> accumulator, // 두 값을 가지고 계산
BinaryOperator<R> combiner, // 계산한 결과를 수집하는 함수.
Characteristics... characteristics) { ... }
matching
조건식 람다 Predicate를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴한다.
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
// 모두 TRUE를 반환한다.
foreach
foreach 는 요소를 돌면서 실행되는 최종 작업이다. 보통 System.out.println 메소드를 넘겨서 결과를 출력할 때 사용하곤 한다. 앞서 살펴본 peek 과는 중간 작업과 최종 작업의 차이가 있다.
names.stream().forEach(System.out::println);