함수형 프로그래밍과 람다

Dev·2022년 3월 19일
0

1. 함수형 프로그래밍

[1] 비유

비유를 들어 이해한다. 함수형 프로그래밍이 아닌건, 각각의 파트는 시계, 특정 기계와 같이 공유 기기를 두고 자신의 할일을 다한다. 반면 함수형 프로그래밍은 컨베이어 벨트처럼, 특정 인풋이 있으면 동일한 아웃풋을 내는 구조를 가진다. 이때, 각각의 파트는 서로가 공유하는 기기 없이 진행한다.

  • Input -> Output으로 구성한다.
  • 오로지 자신에게 주어진것만 철저히 처리하며, 외부 환경으로부터 철저히 독립적이다. 즉, 외부 요인에 접근도, 영향도 받지 않는다.
  • 외부 요인에 영향을 받지 않아, 같은 입력은 같은 출력을 지킨다. 이는 pure function의 특징이기도 하다.

[2] 특징

  • Input -> Output으로 구성한다. 오로지 자신에게 주어진것만 철저히 처리하며, 외부 환경으로부터 철저히 독립적이다. 즉, 외부 요인에 접근도, 영향도 받지 않는다.
  • 물론 비함수형 프로그래밍도 실수만 없다면 함수형 프로그램처럼 동일 인풋에 동일 아웃풋을 낼 수 있다. 예를 들어, 멀티 스레딩 환경에서 sync를 맞추는것도 있다. 하지만 함수형 프로그래밍은 애초에 이런 오해의 소지를 철저히 방지한다. 여기서 말한 오해의 소지는 특정 함수 효과로 프로그램 내 특정 상태를 변경할 수 있음을 말하며, 예를 들어 변경된 상태가 다른 함수에 영향을 끼침을 의미한다. 따라서 pure function은 외부 변수를 사용하더라도 그 본체에 접근해서 변경하는 것이 아닌, 인자로 넣어서 복제된 사본을 사용한다.
  • 외부 요인에 영향을 받지 않아, 같은 입력은 같은 출력을 지킨다. 이는 pure function의 특징이기도 하다.
  • "너는 이렇게, 재는 이렇게, 이렇게 이런식으로" '명령형'으로 개발하는 것이 아니라, "이거는 이거다" 형식의 선언형으로 개발한다. 이는 동일 인풋에 동일 아웃풋을 내기 때문에 가능하다.
  • 함수를 값으로 바라보기 때문에 함수를 인자로 넘길 수 있다. 이 역시 동일 인풋을 동일 아웃풋으로 내기 때문에 가능하다. 즉, 프로그램이 동작하는 중 함수가 만들어질 수 있다와 유사하다.
  • 커링 : 여러 인자를 받는 함수에 일부 인자만 넣어서, 나머지 인자를 받는 다른 함수를 만들어낼 수 있다. 충분한 인자를 준비하지 않았을 때, 부분 적용된 한수를 제공할 수 있다.
add(num1: Int, num2: Int) -> add(2, 3)
add(num1: Int)(num2: Int) -> add(2)(3)
  • 함수 컴비네이션
    filter, take, map ... 컬렉션 내 요소들을 다양하게, 연속적으로 처리할 수 있는 많은 도구들이 탑재되있다. for문 돌면서 다양하게 처리했던걸 간단하게 코드를 작성할 수 있다.
  • 전에는 이것들을 어떻게 구현해낼까 하고 하나하나 설계했지만, 이제는 어떤것들을 가져다 이어붙이면 될까를 고민하게된다!

2. 람다

[1] 람다란?

람다는 한 마디로 코드 블록이다. 기존 코드 블록은 반드시 메서드 내에 존재했어야 했고, 이르 위해선 익명 객체가 필요했다. 하지만 자바 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();
// 람다 도입

Lamda 변경

  • new Runnable()이 사라짐
    • Runnable이 사라진 이유는 Runnable 타입으로 참조 변수 r을 만들고 있으니 new Runnable()은 컴파일러가 알아낼 수 있기 때문이다.
  • public void run()메서드가 단순하게 ()로 변경된다.
    • Runnble 인터페이스가 가진 추상 메서드가 run() 메서드 단 하나이기 때문이다.
  • -> 추가
    • 이는 람다의 구조가 (인자 목록) -> { 로직}으로 구성되기 때문이다. 참고로, 람다에서는 로직이 단 한줄로 표기되는 경우 블록 기호 {} 마저 생략할 수 있다.
Runnable r = () -> System.out.println("Hello Lamda");

[2] 함수형 인터페이스

추상 메서드를 하나만 갖는 인터페이스를 자바 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;
}

[3] 메서드에서 람다 사용

// 메서드 호출인자로 람다 사용

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;
}

[4] 자바 8 API에서 제공하는 대표적인 함수형 인터페이스

함수형 인터페이스추상메서드용도
Runnablevoid run()실행할 수 있는 인터페이스
SupplierT get()제공할 수 있는 인터페이스
Consumervoid accept(T t)소비할 수 있는 인터페이스
Function<T, R>R apply(T t)입력을 받아서 출력할 수 있는 인터페이스
PredicateBoolean test(T t)입력을 받아 참/거짓을 단정할 수 있는 인터페이스
UnaryOperatorT 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)이항 연산 인터페이스

[5] 컬렉션 스트림에서 람다 사용

람다는 다양한 용도가 있지만, 그 중에서도 컬렉션 스트림을 위한 기능에 크게 조첨이 맞춰져 있다. 이 경우 개발자가 <를 써야할 곳에 <= 초기값 설정 등 실수를 방지하고 무엇보다 코드를 깔끔하게 작성할 수 있다.
즉, 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);

[6] 메서드 레퍼런스와 생성자 레퍼런스

메서드 레퍼런스

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 함수형 인터페이스를 사용해 생성자 자체에 대한 참조가 만들어진다.
*/

3. Stream

[1] 정의

  • 스트림은 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 값 요소
  • 스트림은 컬렉션의 요소를 하나씩 참조해 람다식으로 처리할 수 있는 반복자
  • 데이터 컬렉션 반복을 멋지게 처리하는 기능이다.

[2] 왜 쓰는거야?

WHAT 중심의 직관적인 코드

// 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);

쇼트 서킷

  • 자료구조가 포함하는 모든 값을 메소드에 포함하는 컬렉션과 다르게, 스트림은 요청할 때만 요소를 계산하는 고정된 자료구조를 가진다.
  • 스트림은 특정 연산자를 사용할 때 여러 개의 조건이 중첩된 상황에서 값이 결정나면 불필요한 연산을 진행하지 않고 조건문을 빠져나와 실행 속도를 높인다.

[3] 사용법

  1. STREAM 생성 (STREAM 등)
  2. 중간 연산 (FILTER, MAP 등 중간 연산)
  3. 최종 연산 (COLLECT 등 연산을 정리하고 결과를 도출)
  4. 각 단계는 점으로 연결되며 이를 파이프라이닝이라 부른다.

4. Stream 사용법

[0] 스트림이란?

  • 스트림은 자바8부터 지원된 기능인데, 컬렉션에 저장되어 있는 엘리먼트들을 하나씩 순회하면서 처리할 수 있는 코드패턴이다. 자바 8 이전에는 배열, 컬렉션 객체를 다루는게 for, foreach문을 돌면서 처리했다. 로직이 복잡해질수록 여러 로직이 섞이고, 메소드를 나눌 경우 루프를 여러번 도는 문제가 발생한다.
  • 스트림은 '데이터의 흐름'이다. 배열, 컬렉션 인스턴스에 여러 함수를 조합해서 원하는 결과를 필터링하고 가공된 결과를 어등ㄹ 수 있다. 또한, 람다를 이용해서 코드의 양을 줄이고 가독성있게 간결하게 표현할 수 있다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있다.
  • 또 하나의 장점은 간단하게 '병렬처리'가 가능하다는 점이다. 하나의 작업을 둘 이상의 작업으로 나눠서 동시에 진행하는 병렬처리가 쓰레드를 이용해 많은 요소를 빠르게 처리할 수 있다.
  1. Stream 인스턴스 생성
  2. 가공하기 : 필터링 및 맵핑 등 원하는 결과를 만들어가는 중간 작업
  3. 결과 만들기 : 최종적으로 결과를 만들어내는 작업

스트림 생성 -> 필터링1 -> 필터링2 - - - > 결과 만들기

[1] 스트림 생성

보통 배열과 컬렉션을 통해 만들지만 이외에도 다양한 방법이 있다.

컬렉션 객체들은 'stream()' 메소드를 지원한다.

List<String> list = Arrays.asList("A", "B", "C");
Stream<String> stream = list.stream();

정적 메소드 'Arrays.stream()'에 인자로 배열을 넘겨서 스트림을 생성한다.

String[] array = new String[] {"A", "B", "C"};
Stream<String> stream = Arrays.stream(array);

Stream.builder로 직접 값을 입력하여 생성할 수도 있다.

Stream<String> stream = Stream<String>.builder()
							.add("Apple")
                            .add("Banana")
                            .add("Melon")
                            .build();

generate, iterate, empty를 이용한 생성

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, 문자 스트림, 파일스트림이 있다.

[2] 중간 연산

이 단계에선 특정 데이터만 걸러내거나 데이터에 대해서 가공하는 작업 후 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(Chaining) 작성할 수 있다.

(1) Filtering

스트림 내 요소들을 하나씩 평가해서 걸러내는 작업이다. 인자로 받는 Predicate는 boolean을 리턴하는 함수형 인터페이스로 람다형이다.

Stream<T> filter(Predicate<? super T> predicate);
Stream<String> stream = names.stream()
							.filter(name -> name.contains("A"));
                            
// [Elenam, Java ---]

(2) Mapping

스트림 내 요소들을 하나씩 특정 값으로 변환해준다. 이때 값을 변환하기 위해 람다로 인자를 받는다. 즉, 스트림에 들어가 있는 값이 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]

(3) flatMap

인자로 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은 중첩 구조에서 한단계를 제거한(타입변환) 스트림을 생성해준다. 이렇게 나온 스트림을 가지고 다시 연산을 처리할 수 있다.

(4) sorted

정렬은 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());

(5) peek

스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드이다. 'peek'은 그냥 확인해본다는 뜻으로 특정 결과를 반환하지 않는 함수형 인터페이스 Consumer를 인자로 받는다.
따라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과(스트림 반환)에 영향을 미치지 않는다. 작업을 처리하는 중간에 겨롸를 확인해볼 때 사용할 수 있다.

Stream<T> peek(Consumer<? super T> action);
int sum = IntStream.of(1, 3, 5, 7, 9)
			.peek(System.out::println)
            .sum();

[3] 최종 연산

가공된 스트림을 가지고 내가 사용할 결과값을 만들어내는 단계이다.

(1) Calculating

스트림 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();

(2) Reduce

스트림은 reduce라는 메소드를 이용해서 결과를 만드들어낸다. 총 세가지의 파라미터를 받을 수 있는데,

  • accumulator : 각 요소를 처리하는 계산 로직으로 각 요소가 올 때마다 중간 결과를 생성하는 로직
  • identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
  • combiner : 병렬 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직
// 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

(3) Collecting

Collector 타입의 인자를 받아서 처리하며, 자주 사용하는 작업은 Collectors 객체에서 제공하고 있다.

Collectors.toList()
스트림에서 작업한 결과를 담은 리스트를 반환한다.

List<String> collectorCollection =
  productList.stream()
    .map(Product::getName)
    .collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]

Collectors.joining()
스트림에서 작업한 결과를 스트링으로 이러 붙일 수도 있다.

Collectors.joining 은 세 개의 인자를 받을 수 있다.

  • delimiter : 각 요소 중간에 들어가 요소를 구분시켜주는 구분자
  • prefix : 결과 맨 앞에 붙는 문자
  • suffix : 결과 맨 뒤에 붙는 문자
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를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴한다.

  • 하나라도 조건을 만족하는 요소가 있는지 체크 (anyMatch)
  • 모두 조건을 만족하는지 (allMatch)
  • 모두 조건을 만족하지 않는지 (noneMatch)
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);
profile
성장하는 개발자가 되고싶어요

0개의 댓글