16. [Java] Annotation, 람다식, 스트림 / 20230503

2023. 5. 9. 01:20

🧑🏻‍💻 TIL(Today I Learned)


🧑🏻‍💻 Annotation, 람다식, 스트림

 

3. 스트림(Stream)

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

➡️ 다양의 데이터에 복잡한 연산을 수행하면서도 가독성과 재사용성이 높은 코드 작성 가능 

➡️ 스트림을 사용하면 '선언형 프로그래밍(Declarative Programming)' 방식으로 데이터 처리 가능 
: 인간 친화적이고 직관적인 코드 작성 가능 → "무엇"을 작성할지 

명령형 프로그래밍(Imprerative Programming)
→ 코드 한 줄 한 줄의 동작 원리를 이해하고 순차적이고 세세하게 규정하는 방식 
→ "어떻게" 코드를 작성할지

➡️ 데이터 소스가 무엇이냐에 관계없이 같은 방식으로 데이터를 가공/처리를 할 수 있음 
즉, 하나의 통합된 방식으로 데이터를 다룰 수 있게 됨

 

🔎 스트림의 특징

1. 스트림의 처리 과정은 생성, 중간 연산, 최종 연산 세 단계의 파이프라인으로 구성될 수 있다. 

2. 스트림은 원본 데이터 소스를 변경하지 않는다.(Read-Only)

: 오직 데이터를 읽어올 수만 있고 데이터에 대한 변경과 처리는 생성된 스트림 안에서 수행 

3. 스트림은 일회용이다(Onetime-only)
: 마지막 연산이 수행되고 난 후에 스트림은 닫히고 사용 불가, 추가적인 작업은 새로 생성

4. 스트림은 내부 반복자이다.

: 외부 반복자는 개발자가 코드로 직접 컬렉션 요소를 반복해서 가져오는 코드 패턴

(인덱스를 사용하는 for, Iterator를 사용하는 while 대표적)
반대로 내부 반복자는 데이터 처리 코드(람다식)만 주입해서 그 안에서 모든 데이터 처리가 이루어지도록 함

 

 

🔎 스트링의 생성

✍🏻 배열 스트림의 생성 

➡️ 배열을 데이터 소스로 하는 스트림 생성은 Arrays 클래스의 stream() 메서드 또는 Stream 클래스의 of() 메서드 사용 가능

 

 

✍🏻 컬렉션 스트림 생성

➡️ 컬렉션 타입(List, Set 등) 경우 컬렉션의 최상위 클래스인 Collection에 정의된 stream() 메서드를 사용하여 생성 가능

 

 

✍🏻 임의의 수 스트림 생성

➡️ 난수를 생성하는 자바의 기본 내장 클래스 Random 클래스 안에는 해당 타입의 난수들을 반환하는 스트림을 생성하는 메서드가 정의되어 있음 

➡️ ints() 메서드의 경우 int형의 범위 안에 있는 난수들을 무한대로 생성하여 IntStream 타입의 스트림으로 반환 

IntStream vs Stream<Integer>
: IntStream 쓰는 것이 좋음 후자는 박싱 언박싱 과정을 거쳐야함, 수학에서 사용하는 메서드들을 다 사용할 수 있음  

  • int 형 범위 안에서 출력값이 무한대로 생성됨 
    → 무한 스트림(infinite stream) : 스트림의 크기가 정해지지 않은 것 

  • limit() 사용해서 크기를 제한할 수도 있고 ints() 통해서 제한할 수도 있음 
    → 출력값은 그때그때 달라지지만 5개로 제한되어서 나오는 것을 확인할 수 있음 

 

🔎 중간 연산자(Intermediate Operation)

➡️ 중간 연산자의 결과는 스트림을 반환하기 때문에 여러 연산자를 연결하여 원하는 데이터 처리 수행 가능함

스트림 코드 구조

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

➡️ 조건에 맞는 데이터들만 정제하는 역할을 하는 중간 연산자

  • filter() : Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어냄 
                  매개값으로 조건(Predicate) 주고, 조건이 참이 되는 요소만 필터링 → 조건은 람다식 사용하여 정의 가능
  • distinct() : Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 사용

 

✍🏻 매핑(map())

➡️ 원하는 필드만 추출하거나 특정 형태로 변환할 때 사용하는 중간 연산자

➡️ 값은 변환하기 위한 조건을 람다식으로 정의 

➡️ 이중 배열처럼 뎁스가 깊어지는 경우 : flatMap()

 

✍🏻 정렬(sorted())

➡️ 정렬할 때 사용하는 중간 연산자 

➡️ 괄호 안에 Comparator 라는 인터페이스에 정의된 static 메서드와 디폴트 메서드를 사용하여 간편하게 정렬 작업 수행 가능 
(만약 괄호 안에 아무것도 넣지 않으면 기본 정렬[오름차순]으로 정렬)

 

✍🏻 기타 

➡️ skip() : 스트림의 일부 요소 건너뜀

➡️ limit() : 스트림의 일부 자름

➡️  peek() : forEach() 와 마찬가지로 요소들을 순회하며 특정 작업 수행, 하지만 중간 연산자이기 때문에 여러 번 연결해서 사용 가능 

 

 

🔎 스트림의 최종 연산(Terminal Operation)

➡️ 최종 연산자가 스트림 파이프라인에서 최종적으로 사용되고 나면 해당 스트림은 닫히고 모든 연산 종료

➡️ 중간 연산은 최종 연산자가 수행될 때야 스트림의 요소들이 중간 연산을 거쳐 가공된 후에 최종 연산에서 소모 됨 
    : 지연된 연산(lazy evaluation)

 

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

➡️ 숫자와 관련된 최종 연산자

import java.util.Arrays;

public class TerminalOperationExample1 {
    public static void main(String[] args) {

        // int 형 배열 생성
        int[] intArray = {1, 2, 3, 4, 5};

        // 카운팅
        long count = Arrays.stream(intArray).count();
        System.out.println("intArr의 전체 요소 개수 : " + count);

        // 합계
        long sum = Arrays.stream(intArray).sum();
        System.out.println("intArr의 전체 요소 합 : " + sum);

        // 평균
        double avg = Arrays.stream(intArray).average().getAsDouble(); // average() 는 OptionalDouble 객체를 반환하는 메서드
        System.out.println("전체 요소 평균값 : " + avg);

        // 최대값
        int max = Arrays.stream(intArray).max().getAsInt();
        System.out.println("최대값 : " + max);

        // 최소값
        int min = Arrays.stream(intArray).min().getAsInt();
        System.out.println("최소값 : " + min);

        // 배열의 첫 번째 요소 --> 스트림에서 가장 처음으로 만나는 요소
        int first = Arrays.stream(intArray).findFirst().getAsInt();
        System.out.println("배열의 첫 번째 요소 : " + first);
    }
}
// OptionalDouble, OptionalInt 등은 일종의 래퍼클래스
// null 값으로 인해서 에러가 발생하는 현상을 효율적으로 방지하기 위한 목적으로 도입된 것
// 즉, 래퍼클래스로 객체로 되어있기 때문에 기본형으로 바꿔줘야 함 ! --> getAsDouble(), getAsInt() 사용
// 그저 기본형으로 돌아가기 위한 메서드!

 

 

✍🏻 매칭(allMatch(), anyMatch(), noneMatch())

➡️ match()  메서드를 사용하면 조건식 람다 Predicate 를 매개변수로 넘겨 스트림의 각 데이터 요소가 특정한 조건을 충족하지 않는지 검사하여, 그 결과를 boolean 값으로 반환

  • allMatch() : 모든 요소가 조건을 만족하는지 여부를 판단
  • nonMatch() : 모든 요소가 조건을 만족하지 않는지 여부 판단
  • anyMatch() : 하나라도 조건을 만족하는 요소가 있는지 여부 판단
import java.util.Arrays;

public class TerminalOperationExample {
    public static void main(String[] args) throws Exception {
        // int형 배열 생성
        int[] intArray = {2,4,6};

        // allMatch()
        boolean result = Arrays.stream(intArray).allMatch(element-> element % 2 == 0);
        System.out.println("요소 모두 2의 배수인가요? " + result);

        // anyMatch()
        result = Arrays.stream(intArray).anyMatch(element-> element % 3 == 0);
        System.out.println("요소 중 하나라도 3의 배수가 있나요? " + result);

        // noneMatch()
        result = Arrays.stream(intArray).noneMatch(element -> element % 3 == 0);
        System.out.println("요소 중 3의 배수가 하나도 없나요? " + result);
    }

}

// 출력값
요소 모두 2의 배수인가요? true
요소 중 하나라도 3의 배수가 있나요? true
요소 중 3의 배수가 하나도 없나요? false

 

 

✍🏻 요소 소모( reduce())

➡️  스트림의 요소를 줄여나가면서 연산을 수행하고 최종적인 결과 반환

➡️ 첫 번째와 두 번째 요소를 가지고 연산 수행하고, 그 결과와 다음 세 번째 요소 가지고 또다시 연산을 수행하는 식으로 연산이 끝낼 때까지 반복

➡️ reduce() 메서드의 매개변수 타입은 함수형 인터페이스 BinaryOperator<T> 로 정의되어 있음 

Optional<T> reduce(BinaryOperator<T> accumulator)

T reduce(T identity, BinaryOperator<T> accumulator) : 매개 변수 2개까지 받는 경우
→ identity : 특정 연산 시작할 때 초기값
→ accumulator : 각 요소를 연산하여 나온 누적된 결과값을 생성하는 데 사용되는 조건식

➡️ count()와 sum() 과 같은 집계 메서드 또한 내부적으로 reduce() 사용하여 연산 수행 

 

 

✍🏻 요소 수집(collect())

➡️ 중간 연산을 통한 요소들의 데이터 가공 후 요소들을 수집하는 최종 처리 메서드 

➡️ 스트림의 요소들을 List, Set, Map 등 다른 타입의 결과로 수집하고 싶은 경우에 사용 

➡️ Collector 인터페이스 타입의 인자를 받아서 처리 → 직접 구현하거나 미리 제공한 것들 사용 가능 

➡️ 요소 그룹핑 및 분할 등 다른 기능들도 제공함 

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TerminalOperationExample {

    public static void main(String[] args) {
        // Student 객체로 구성된 배열 리스트 생성 
        List<Student> totalList = Arrays.asList(
                new Student("김코딩", 100, Student.Gender.Male),
                new Student("박해커", 80, Student.Gender.Male),
                new Student("이자바", 90, Student.Gender.Female),
                new Student("나미녀", 60, Student.Gender.Female)
        );
        
        // 스트림 연산 결과를 Map으로 반환
        Map<String, Integer> maleMap = totalList.stream()
                .filter(s -> s.getGender() == Student.Gender.Male)
                .collect(Collectors.toMap(
                        student -> student.getName(), // Key
                        student -> student.getScore() // Value
                ));

        // 출력
        System.out.println(maleMap);
    }
}

class Student {
    public enum Gender {Male, Female};
    private String name;
    private int score;
    private Gender gender;

    public Student(String name, int score, Gender gender) {
        this.name = name;
        this.score = score;
        this.gender = gender;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    public Gender getGender() {
        return gender;
    }
}

// 출력값
{김코딩=100, 박해커=80}

스트림까지 정리 완료! 다시 보니까 새록새록 생각나고 머릿속에서 한 번 더 정리되는 느낌!

BELATED ARTICLES

more