[Java] 람다와 스트림

·2022년 9월 17일
0

Java

목록 보기
10/14
post-thumbnail

람다

  • 함수형 프로그래밍 기법을 지원하는 자바의 문법요소
    메서드를 하나의 으로 표현한 것
  • 익명 함수(이름이 없는 함수)
  • 메서드를 표현한 것이지만 람다식은 객체이며 더 정확히는 익명 클래스이다.

💡 코드를 간결하면서 명확하게 표현할 수 있다는 장점을 가지고 있다.

람다식의 문법

//기존 메서드 표현 방식
void sayhello() {
	System.out.println("HELLO!")
}

//위의 코드를 람다식으로 표현한 식
() -> System.out.println("HELLO!")
  • 반환타입과 메서드의 이름을 생략할 수 있다.
int sum (int x, int y) {
	return x + y ;
    }
    
 ==람다식으로 변환=>
 
 (x, y) -> x + y
  • 매개변수의 타입 역시 유추가 충분히 가능한 상황이라면 생략이 가능하다.
  • 매개변수가 하나일 경우 매개변수를 감싸는 소괄호()도 생략이 가능하다.
  • 리턴문세미콜론 생략 가능
  • 메서드 바디에 실행문이 하나일 경우 중괄호({})생략이 가능하나, 두개 이상일 때에는 생략이 불가능하다.

함수형 인터페이스

  • 람다식은 익명 클래스 이다.

    💡 익명 클래스 : 객체의 선언과 생성을 동시에 하며 오직 하나의 객체를 생성하고 단 한번만 사용되어지는 일회용 클래스

  • 클래스이기 때문에 최상위 클래스인 Object 클래스를 상속 받고 있지만 익명 클래스 내에 있는 메서드가 Object 클래스에 존재하지 않기 때문에 익명 클래스에서 람다식으로 구현한 메서드를 사용할 수 없다.
    이를 해결하기 위해 함수형 인터페이스를 사용한다.
  • 람다는 하나의 함수형 인터페이스를 구현하여 생성된 익명 객체라고 볼 수 있다.

    💡 주의 사항
    함수형 인터페이스는 단 하나의 추상 메서드만 선언될 수 있다.
    -> 람다식과 인터페이스의 메서드가 1:1로 매칭되어야 하기 때문에!

  • 예시 코드
public interface MyFunctionalInterface {
    public void accept(매개변수);
}

// 익명객체 선언과 동시에 생성
 MyFunctionalInterface example = (매개변수) -> {실행할 코드};
 
// 메서드 초훌
example.accept(매개변수 대입값);

기본적으로 제공하는 함수형 메서드

자바에서는 빈벅하게 사용되는 함수형 인터페이스를 기본적으로 제공하고 있다.

https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

메서드 레퍼런스(참조)

람다식에서 불필요한 매개변수를 제거할 때 사용된다.
즉, 람다식을 더욱 간단하게 사용하고 싶을 때 사용.

클래스명 :: 메서드명

//예시 Math클래스의 max()메서드를 참조할 때
Math::max;
  • 입출력값의 반환타입이 쉽게 유추될 경우 사용.
  • 메서드 참조 = 익명객체
    인터페이스의 추상 메서드가 어떤 매개 변수를 가지고 리턴 타입이 무언인가에 따라 달라짐.

정적(클래스) 메서드 참조

클래스명::메서드명

인스턴스 메서드 참조

참조변수명::메서드명

생성자 참조

클래스명::new
  • 함수형 인터페이스의 추상메서드와 동일한 매개 변수 타입과 개수를 가지고 있는 생성자를 찾아 실행

스트림

: 배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자

스트림의 장점

  1. 다량의 데이터에 복잡한 연산을 수행할 수 있다.
  2. 가독성과 재사용성이 높은 코드를 작성할 수 있다.

스트림의 특징

1. 선언형으로 데이터 소스를 처리한다.

  • 선언형 프로그래밍이란 어떻게 수행하는지 보다는 무엇을 수행하는 지에 관심을 두는 프로그래밍
    • for문과 같은 반복문을 사용하는 것보다 stream을 사용하는 것이 더욱 단순하고 가독서이 높음

2. 람다식으로 요소 처리 코드를 제공한다.

  • stream 연산자를 사용할 때, 람다식을 사용한다.
  int sum =
                numbers.stream()
                        .filter(number -> number > 4 && (number % 2 == 0))
                        .mapToInt(number -> number)
                        .sum();

3. 내부 반복자를 사용하므로 병렬 처리가 쉽다.

  • 외부 반복자 : 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴
    • for문, iterator를 이용한 while문
  • 내부 반복자 : 컬렉션 내부에서 요소들을 반복시키고 개발자는 요소당 처리해야 할 코드만 제공하는 코드 패턴
    • Stream

💡
외부 반복자 : 요소를 가져 오는 것부터 처리하는 것까지 개발자가 작성
내부 반복자 : 요소 처리 내용만 개발자가 전달하고 반복은 컬렉션 내부에서 자동으로 처리

4. 중간 연산과 최종 연산을 할 수 잇따.

  • 중간 연산 : 매핑, 필터링, 정렬 등
  • 최종 연산 : 반복, 카운팅, 평균, 총합 등

    💡 주의 사항
    스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐 변경하지 않는다.
    스트림은 일회용이므로 한번 사용하면 닫힌다! 필요하다면 새로운 스트림을 만들어야 함.

파이프라인 구성

리덕션 : 대량의 데이터를 가공해서 축소하는 것
리덕션의 대표 결과물 : 데이터의 합계, 평균값, 최대값 등

  • 컬렉션의 요소를 리덕션의 결과물로 바로 집계할 수 없을 때에는 중간 연산이 필요!

파이프라인

  • 스트림은 중간연산과 최종연산을 파이프라인으로 해결
    파이프라인 : 여러개의 스트림이 연결되어 있는 구조

  • 최종 연산이 시작되기 전까지는 중간 연산은 지연된다.
  • 최종 연산이 시작 되면 컬렉션의 요소가 하나씩 중간 연산에서 최종 연산으로 흘러간다.

스트림 생성

스트림 : 데이터를 연속적으로 전달하는 통로

  • 스트림 생성 -> 중간 연산 -> 최종 연산
// 1. 컬렉션의 스트림 생성 
// : Collection 인터페이스에는 stream()가 정의되어 있음

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> listStream = list.stream(); // 스트림 생성 완료!

// 2. 배열의 스트림 생성
// : Stream의 of()메서드나 Arrays의 stream()메서드를 사용

Stream<String> stream = Stream.of(new String[] {"a", "b", "c"});
Stream<String> stream = Arrays.stream(new String[] {"a", "b", "c"});
  • 숫자와 관련된 컬렉션이나 배열의 스트림을 생성할 경우, IntStream을 사용하는 것을 권장

스트림 중간 연산

: 연산 결과를 스트림으로 반환하기 때문에 연속해서 여러번 수행할 수 있다.

필터링(distinct(), filter())

  • distinct() : 스트림의 요소 중 중복된 데이터가 있는 경우, 중복을 제거
  • filter() : 조건이 맞는 데이터를 필터링 해주며, 매개값으로 조건이 주어지고 참이 되는 요소만 걸러준다.

매핑(map(), flatMap())

  • map() : 기존의 스트림 요소들을 대체하는 요소로 구성된 새로운 스트림을 형성하는 요소
  • flatMap() : 요소를 대체하는 복수 개의 요소들로 구성된 새로운 스트림을 리턴
  • 💡 map()과 flatMap()의 차이점
map()flatMap()
반환타입스트림의 스트림스트림
Stream<Stream>Stream

정렬(sorted())

  • sorted() : 요소들을 정렬하기 위한 요소
    기본적으로 오름차순으로 정렬되며 파라미터로 Comparator의 reverseOrder를 이용하면 내림차순으로 정렬이 가능하다.

연산 결과 확인(peek())

  • 최종 연산 전, 중간 연산에서 결과를 확인하여 디버깅하고자 할 때 사용
  • 요소를 하나 씩 돌면서 출력(최종연산자인 forEach()와 같은 기능을 한다.)

스트림 최종 연산

: 연산 결과가 스트림이 아니므로 한번만 연산이 가능

연산 결과 확인(forEach())

  • forEach() : 요소를 하나씩 연산, 리턴 값이 없는 작업에서 많이 사용

매칭(match())

  • 특정한 조건을 충족하는지 검사, 결과를 boolean으로 반환
    • allMatch() : 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
    • anyMatch() : 최소한 한 개의 요소가 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
    • noneMatch() : 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하지 않는지 조사

기본 집계(sum(), count(), average(), max(), min())

응축 (reduce())

  • 연산 결과를 누적하여 하나로 응축한다.
  • reduce()는 최대 3개의 매개변수를 받을 수 있다.
    • Accumulator: 각 요소를 계산한 중간 결과를 생성하기 위해 사용
    • Identity: 계산을 수행하기 위한 초기값
    • Combiner: 병렬 스트림(Parlallel Stream)에서 나누어 계산된 결과를 하나로 합치기 위한 로직

결과를 다른 종류로 반환(collect())

  • 스트림의 요소들을 List, Set, Map등 다른 종류의 결과로 수집하고 싶은 경우에 사용
    • collect(Collectors.toList()); : 요소들을 list로 반환

Optional<T>

: optional은 null값으로 인해 에러(NullPointerException)가 발생하는 현상을 객체 차원에서 효율적으로 방지하고자 도입되었다.

  • Optional 클래스 : 모든 타입의 객체를 담을 수 있는 래퍼(Wrapper) 클래스
  • 객체 생성 방법 :
    of() 또는 ofNullable()(참조변수의 값이 null일 가능성이 있는 경우 사용) 메서드를 사용한다.
  • empty() : Optional 타입의 참조변수를 기본값으로 초기화하는 메서드
  • get() : Optional 객체의 저장된 값을 가져오는 메서드
  • orElse() : 참조변수의 값이 null일 가능성이 있다면, 이 메서드를 사용해 디폴트 값을 지정할 수 있다.

    💡 Optional 객체는 스트림과 유사하게 여러 메서드를 연결해서 작성할 수 있다.

profile
🧑‍💻백엔드 개발자, 조금씩 꾸준하게

0개의 댓글