스트림(Stream)

Nak Jun Choi·2022년 11월 14일

스트림이란?

  • 배열, 컬렉션의 저장 요소를 하나씩 참조하여 람다식으로 처리할 수 있도록 해주는 반복자이다.
  • 스트림을 사용하면 List, Set, Map 배열 등 다양한 데이터 소스로부터 스트림을 만들 수 있고, 이를 표준화된 방법으로 다룰 수 있다.
  • 스트림은 데이터 소스를 다루는 풍부한 메서드를 제공한다.

스트림의 특징

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

  • 스트림으로 이용하면, 선언형으로 데이터 소스를 처리할 수 있다.
  • 선언형 프로그램이란?, 어떻게 수행하는지보다 무엇을 수행하는 지에 관심을 두는 프로그래밍 패러다임입니다.
import java.util.List;

public class ImperativeProgrammingExample {
    public static void main(String[] args){
        // List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
        List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
        int sum = 0;

        for(int number : numbers){
            if(number > 4 && (number % 2 == 0)){
                sum += number;
            }
        }

        System.out.println("# 명령형 프로그래밍 : " + sum);
    }
}
import java.util.List;

public class DeclarativeProgramingExample {
    public static void main(String[] args){
        // List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
        List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);

        int sum =
                numbers.stream()
                        .filter(number -> number > 4 && (number % 2 == 0))
                        .mapToInt(number -> number)
                        .sum();

        System.out.println("# 선언형 프로그래밍: " + sum);
    }
}
  • 비교해보면 Stream을 사용한 코드가 훨씬 단순하고 가독성이 높음을 알 수 있다.

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

//Student.java
public class Student {
    private String name;
    private int score;

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

    public String getName(){
        return name;
    }

    public int getScore(){
        return score;
    }
}
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamLambdaExample {
    public static void main(String[] args) throws Exception {
        List<Student> list = Arrays.asList(
            new Student("김코딩", 95),
            new Student("이자바", 92)
        );

        Stream<Student> stream = list.stream();
        stream.forEach( s -> {
            String name = s.getName();
            int score = s.getScore();
            System.out.println(name+ " - " +score);
        });
    }
}
/*
김코딩 - 95
이자바 - 92
*/

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

  • 외부 반복자(external iterator)란 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 말한다. Ex). for문, Iterator를 이용하는 while문
  • 내부 반복자(internal iterator)는 컬렉션 내부에서 요소들을 반복시키고 개발자는 요소당 처리해야할 코드만 제공하는 코드 패턴을 말한다.
  • 내부 반복자를 사용해서 얻는 이점은 컬렉션 내부에서 어떻게 요소를 반복시킬 것인가는 컬렉션에게 맡겨두고, 개발자는 요소 처리 코드에만 집중할 수 있다.
  • 내부 반복자는 요소들의 순서를 변경하거나 멀티 코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있게 도와주기 때문에 하나씩 처리하는 순차적 외부 반복자보다 효율적으로 요소를 반복시킬 수 있다.
  • 병렬 스트림을 사용하기 위해서는 스트림의 parallel()메서드를 사용하면 된다.

중간 연산과 최종 연산을 할 수 있다.

  • 중간 연산에서는 매핑, 필터링, 정렬 등을 수행하고 최종 연산에서는 반복, 카운팅, 평균, 종합 등의 집계를 수행할 수 있다.

파이프라인 구성(.)

  • 대량의 데이터를 가공해서 축소하는 것으 일반적으로 리덕션(Reduction이라고 한다.
  • 컬렌션의 요소를 리덕션의 결과물로 바로 집계할 수 없을때에는 필터, 매핑, 정렬, 그룹화 등의 중간 연산이 필요하다.

파이프 라인

  • 파이프 라인은 여러개의 스트림이 연결되어 있는 구조를 의미한다. 파이프 라인에서는 최종 연산을 제외하고는 모두 중간 연산 스트림이다.

스트림 생성, 중간 연산, 최종 연산

  • 스트림은 데이터를 연속적으로 전달하는 통로로 이해할 수 있다.

스트림 생성

  • Collection 인터페이스에는 stream()이 정의되어 있기 때문에, Collection 인터페이스를 구현한 객체들(List, Set 등)은 모두 이 메서드를 이용해 스트림을 생성할 수 있다.
// List로부터 스트림을 생성
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> listStream = list.stream();
listStream.forEach(System.out::prinln); //스트림의 모든 요소를 출력.
  • 배열의 원소들을 소스로하는 Stream을 생성하기 위해서는 Stream의 of 메서드 또는 Arrays의 stream 메서드를 사용한다.
// 배열로부터 스트림을 생성
Stream<String> stream = Stream.of("a", "b", "c"); //가변인자
Stream<String> stream = Stream.of(new String[] {"a", "b", "c"});
Stream<String> stream = Arrays.stream(new String[] {"a", "b", "c"});
Stream<String> stream = Arrays.stream(new String[] {"a", "b", "c"}, 0, 3); //end 범위 미포함


// 4이상 10미만의 숫자를 갖는 IntStream
IntStream stream = IntStream.range(4, 10);
  • 스트림은 주로 컬렉션과 배열에서 얻지만, 다음과 같은 소스로부터 스트림 구현 객체를 얻을 수도 있다.
  • 스트림을 사용할 때 주의할 점!
    • 스트림의 데이터 소스로부터 데이터를 읽기만 할 뿐 변경하지 않는다.(Read-Only)
    • 스트림은 일회용이다. 한 번 사용하면 닫히므로, 필요하다면 새로운 스트림을 만들어야 한다.

중간 연산

  • 중간 연산은 연산 결과를 스트림으로 반환하기 때문에, 연속해서 여러 번 수행할 수 있다.
  • 필터링(filter(), distinct())
    • filter() : Stream에서 조건에 맞는 데이터만을 정제해서 더 작은 컬렉션을 만든다. filter() 메서드에는 매개값으로 조건이 주어지고, 조건(Predicate)이 참이되는 요소만 필터링한다.
    • distinct() : Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 사용한다.
public class FilteringExample {
    public static void main(String[] args) throws Exception {
        List<String> names = Arrays.asList("김코딩", "이자바", "김인기", "김코딩");

        names.stream()
						 .distinct() //중복제거
						 .forEach(n -> System.out.println(n));
        System.out.println();

        names.stream()
						 .filter(n -> n.startsWith("김")) //필터링
						 .forEach(n -> System.out.println(n));
        System.out.println();

        names.stream()
						 .distinct() //중복제거
						 .filter(n -> n.startsWith("김")) //필터링
						 .forEach(n -> System.out.println(n));
    }
}
  • 매핑(map())
    • map()은 기존의 Stream 요소들을 대체하는 요소로 구성된 새로운 Stream을 형성하는 연산이다.
    • 저장된 값을 특정한 형태로 반환하는데 주로 사용 된다. Java에서는 map 함수의 인자로 함수형 인터페이스 function을 받고 있다.
    • map()이외에도 mapToInt(), mapToLong(), mapToDouble() 등의 메서드가 있다.
    • flatmap()은 요소를 대체하는 복수 개의 요소들로 구성된 새로운 스트림을 리턴한다.
List<String> names = Arrays.asList("kimcoding", "javalee", "ingikim", "kimcoding");
    names.stream()
				 .map(s -> s.toUpperCase())
				 .forEach(n->System.out.println(n));
  • 정렬(sorted())
    • Stream의 요소들을 정렬하기 위해서는 sorted를 사용해야 하며, 파리미터로 Comparator를 넘길 수도 있다.(Comparator 인자 없이 호출하면 오름차순 정렬되며, 내림차순으로 정렬하기 위해서는 reverseOrder를 이용해야한다.)
List<String> list = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");

        list.stream()
            .sorted()
            .forEach(n -> System.out.println(n));
        System.out.println();

/*
Go, Groovy, Java, Python, Scala, Swift
*/
        list.stream()
            .sorted(Comparator.reverseOrder())
            .forEach(n -> System.out.println(n));

/*
Swift, Scala, Python, Java, Groovy, Go
*/
  • 연산 결과 확인(peak())
    • peek()은 중간 연산 메서드이다. 중간 연산이므로 하나의 스트림에 여러번 사용가능하다. 주로 연산 중간에 결과를 확인하여 디버깅 하고자 할 때 사용한다.
    • forEach()는 최종 연산 메서드이다. 스트림의 요소를 소모하므로 한 번만 호출할 수 있다.
      intStream
      	.filter(a -> a%2 ==0)
      	.peek(n-> System.out.println(n))
      	.sum();

최종 연산

  • 최종 연산은 연산 결과가 스트림이 아니므로, 한 번만 연산이 가능하다.
  • 연산 결과 확인(forEach())
    • 파이프라인 마지막에서 요소를 하나씩 연산합니다. forEach값을 출력할 때도 사용하지만, 이메일 발송, 스케줄링 등 리턴 값이 없는 작업에서도 많이 사용한다.
intStream
	.filter(a -> a%2 ==0)
	.forEach(n -> System.out.println(n));
  • 매칭(match())
    • 특정한 조건을 충족하는지 검사하고 싶은 경우에 사용한다. 검사 결과를 boolean으로 반환한다.
      • allMatch() : 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하는지 조사한다.
      • anyMatch() : 최소한 한 개의 요소가 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
      • noneMatch() : 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하지 않는지 조사
import java.util.Arrays;

public class MatchesExample {
    public static void main(String[] args) throws Exception {
        int[] intArr = {2,4,6};
        boolean result = Arrays.stream(intArr).allMatch(a->a%2==0);
        System.out.println("모두 2의 배수인가? " + result);

        result = Arrays.stream(intArr).anyMatch(a->a%3==0);
        System.out.println("하나라도 3의 배수가 있는가? " + result);

        result = Arrays.stream(intArr).noneMatch(a->a%3==0);
        System.out.println("3의 배수가 없는가? " + result);
    }
}

/*
모두 2의 배수인가? true
하나라도 3의 배수가 있는가? true
3의 배수가 없는가? false
*/

  • 기본 집계(sum(), count(), average(), max(), min())
    • 집계는 최종 연산 기능으로 요소들을 연산하여 하나의 값으로 산출하는 것을 의미한다.
  import java.util.Arrays;

public class AggregateExample {
    public static void main(String[] args) throws Exception {
        int[] intArr = {1,2,3,4,5};

        long count = Arrays.stream(intArr).count();
        System.out.println("intArr의 전체 요소 개수 " + count);

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

        double avg = Arrays.stream(intArr).average().getAsDouble();
        System.out.println("전체 요소의 평균값 " + avg);

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

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

        int first = Arrays.stream(intArr).findFirst().getAsInt();
        System.out.println("배열의 첫번째 요소 " + first);

    }
}

/*
intArr의 전체 요소 개수 5
intArr의 전체 요소 합 15
전체 요소의 평균값 3.0
최대값 5
최소값 1
배열의 첫번째 요소 1
*/
  • reduce()
    • 누적하여 하나로 응축하는 방식으로 동작한다. 앞의 두 요소의 연산 결과를 바탕으로 다음 요소와 연산한다. reduce 메서드는 최대 3개의 매개변수를 받을 수 있다.
    1. Accumulate : 각 요소를 계산한 중간 결과를 생성하기 위해 사용
    2. Identity : 계산을 수행하기 위한 초기값
    3. Combiner : 병렬 스트림(Parlallel Stream)에 나누어 계산된 결과를 하나로 합치기 위한 로직
import java.util.Arrays;

public class ReduceExample {
    public static void main(String[] args) throws Exception {
        int[] intArr = {1,2,3,4,5};

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

        int sum1 = Arrays.stream(intArr)
                        .map(el -> el*2)
                        .reduce((a,b) -> a+b)
                        .getAsInt();
        System.out.println("초기값 없는 reduce " + sum1);

        int sum2= Arrays.stream(intArr)
                        .map(el -> el*2)
                        .reduce(0, (a,b) -> a+b);
        System.out.println("초기값 존재하는 reduce " + sum2)
    }
}

/*
intArr의 전체 요소 합 15
초기값 없는 reduce 30
초기값 존재하는 reduce 30
*/
  • Collect()
    • Stream의 요소들을 List나 Set, Map 등 다른 종류의 결과로 수집하고 싶은 경우에 사용한다.
    • Collector 인터페이스를 구현한 클래스이다.
//Student.java
public 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 Gender getGender(){
        return gender;
    }

    public String getName(){
        return name;
    }

    public int getScore(){
        return score;
    }
}
//CollectExample.java
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class CollectExample {
    public static void main(String[] args) throws Exception {
        List<Student> totalList = Arrays.asList(
            new Student("김코딩", 10, Student.Gender.Male),
            new Student("김인기", 8, Student.Gender.Male),
            new Student("이자바", 9, Student.Gender.Female),
            new Student("최민선", 10, Student.Gender.Female)
        );

        List<Student> maleList = totalList.stream()
        .filter(s -> s.getGender() == Student.Gender.Male)
        .collect(Collectors.toList());

        maleList.stream().forEach(n->System.out.println(n.getName()));

        Set<Student> femaleSet = totalList.stream()
        .filter(s -> s.getGender() == Student.Gender.Female)
        .collect(Collectors.toCollection(HashSet :: new));

        femaleSet.stream().forEach(n->System.out.println(n.getName()));
    }
}

Optinal<T>

  • Optional은 NullPointerException(NPE), 즉 null 값으로 인해 에러가 발생하는 현상을 객체 차원에서 효율적으로 방지하고자 도입되었다.
  • 연산 결과를 Optional에 담아서 반환하면, 따로 조건문을 작성해주지 않아도 NPE가 발생하지 않도록 코드를 작성할 수 있다.

Optional

  • Optional 클래스는 모든 타입의 객체를 담을 수 있는 래퍼(Wrapper) 클래스이다.
public final class Optional<T> {
	private final T value; // T타입의 참조변수
}

// Optional 객체를 생성하려면 of() 또는 ofNullable()을 사용한다. 참조 변수의 값이 null일 가능성이 있다면, ofNullable()을 사용한다.
  
Optional<String> opt1 = Optional.ofNullable(null);
Optional<String> opt2 = Optional.ofNullable("123");
System.out.println(opt1.isPresent()); //Optional 객체의 값이 null인지 여부를 리턴합니다.
System.out.println(opt2.isPresent());  

// Optional 타입의 참조변수를 기본값으로 초기화하려면 empty() 메서드를 사용한다.  
Optional<String> opt3 = Optional.<String>empty();
  
  
// Optional 객체에 저장된 값을 가져오려면 get()을 사용한다.
// 참조변수의 값이 null일 가능성이 있다면 orElse()메서드를 사용해 디폴트 값을 지정할 수 있다.
  
Optional<String> optString = Optional.of("codestates");
System.out.println(optString);
System.out.println(optString.get());

String nullName = null;
String name = Optional.ofNullable(nullName).orElse("kimcoding");
System.out.println(name);

  
// Optional 객체는 스트림과 유사하게 여러 메서드를 연결해서 작성할 수 있다.(이를 메서드 체이닝이라 한다.)  
  
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class OptionalExample {
    public static void main(String[] args) {
        List<String> languages = Arrays.asList(
                "Ruby", "Python", "Java", "Go", "Kotlin");
        Optional<List<String>> listOptional = Optional.of(languages);

        int size = listOptional
                .map(List::size)
                .orElse(0);
        System.out.println(size);
    }
}
  • 다음에 볼떄는 꼭 심화학습 연습 문제 풀어보기!
profile
HelloWorld

0개의 댓글