스트림 (Stream)

박대운·2022년 11월 14일
0

Java

목록 보기
9/9

스트림의 특징

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

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

for문을 사용한 코드와 Stream을 사용한 코드를 비교해보면 Stream을 사용하는 것이 훨씬 단순하고 가독성이 높음을 알 수 있습니다.

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

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
*/

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

외부반복자(external iterator)란 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 말합니다. index를 사용하는 for문, Iterator를 이용하는 while문은 모두 외부 반복자를 이용하는 형태입니다.

반면에 내부 반복자(internal iterator)는 컬렉션 내부에서 요소들을 반복시키고 개발자는 요소당 처리해야할 코드만 제공하는 코드 패턴을 이야기합니다.

Iterator는 컬렉션의 요소를 가져오는 것에서부터 처리하는 것까지 모두 개발자가 작성해야 하지만 스트림은 람다식으로 요소 처리 내용만 전달할 뿐, 반복은 컬렉션 내부에서 일어납니다.

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

스트림은 컬렉션의 요소에 대해 중간 연산과 최종 연산을 수행할 수 있는데, 중간 연산에서는 매핑, 필터링, 정렬 등을 수행하고 최종 연산에서는 반복, 카운팅, 평균, 총합 등의 집계를 수행할 수 있습니다.

파이프라인 구성(.)

대량의 데이터를 가공해서 축소하는 것을 일반적으로 리덕션(Reduction)이라고 합니다.

데이터의 합계, 평균값, 카운팅, 최대값, 최소값 등이 대표적인 리덕션의 결과물이라고 볼 수 있습니다. 그러나 컬렉션의 요소를 리덕션의 결과물로 바로 집계할 수 없을 때에는 필터, 매핑, 정렬, 그룹화 등의 중간 연산이 필요합니다.

파이프라인

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

회원중에 남자회원의 평균나이 집계 예시)

Stream<Member> maleFemaleStream = list.stream();
Stream<Member> maleStream = maleFemaleSTream.filter(m -> m.getGender() == Member.MALE);
IntStream ageStream = maleStream.mapToInt(Member::getAge);
OptionalDouble opd = ageStream.average();
double ageAve = opd.getAsDouble();

위 코드에서 로컬 변수를 생략하고 연결하면 다음과 같은 형태의 파이프라인 코드만 남습니다.

double ageAve = list.stream() //오리지널 스트림
.filter(m-> m.getGender() == Member.MALE) //중간 연산 스트림
.mapToInt(Member::getAge) //중간 연산 스트림
.average() //최종 연산
.getAsDouble();

스트림 생성

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<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 범위 미포함

스트림을 사용할 때 주의할 점

1. 스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐 변경하지 않습니다(Read-only).
2. 스트림은 일회용입니다. 한 번 사용하면 닫히므로, 필요하다면 새로운 스트림을 만들어야 합니다.

중간 연산

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

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

distinct() : Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 사용합니다.

filter() : Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어냅니다. filter() 메서드에는 매개값으로 조건(Predicate)이 주어지고, 조건이 참이 되는 요소만 필터링합니다.

예시)

import java.util.Arrays;
import java.util.List;

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()
map은 기존의 Stream 요소들을 대체하는 요소로 구성된 새로운 Stream을 형성하는 연산입니다.

String을 요소로 갖는 stream을 모두 대문자로 변환하는 예시)

List<String> names = Arrays.asList("kimcoding", "javalee", "ingikim", "kimcoding");
    names.stream()
				 .map(s -> s.toUpperCase())
				 .forEach(n->System.out.println(n));
/*
KIMCODING
JAVALEE
INGIKIM
KIMCODING
*/

map() 메서드는 작업을 하다 보면 일반적인 Stream 객체를 원시 Stream으로 바꾸거나 그 반대로 하는 작업이 필요한 경우에 쓰입니다. 반대로 원시 객체는 mapToObject를 통해 일반적인 Stream 객체로 바꿀 수 있습니다.

flatMap(): flatMap은 요소를 대체하는 복수 개의 요소들로 구성된 새로운 스트림을 리턴합니다.
flatMap()과 map()의 차이점은, map()은 스트림의 스트림을 반환하는 반면, flatMap()은 스트림을 반환한다는 것입니다.

예시)

public class ComparatorExample {
	public static void main(String[] args) {
	        Stream<String[]> stringArraysStream = Stream.of(
									new String[]{"hello", "world", "java"},
									new String[]{"code", "states"});
	
	        stringArraysStream.flatMap(Arrays::stream).forEach(System.out::println);
	    }
}

위 예시에서 출력은 hello world ... flatMap -> Map 으로 바꾸면 new String[] 의 주소값이 출력된다.

정렬(sorted())

Stream의 요소들을 정렬하기 위해서는 sorted를 사용해야 하며, 파라미터로 Comparator를 넘길 수도 있습니다.
Comparator 인자 없이 호출할 경우에는 오름차순으로 정렬이 되며, 내림차순으로 정렬하기 위해서는 Comparator의 reverseOrder를 이용합니다.

import java.lang.*;
import java.util.*;
import java.util.stream.Stream;

class Employee implements Comparable<Employee>{
    int id;
    String name,department;

    public Employee(int id, String name, String department) {
        this.id = id;
        this.name = name;
        this.department = department;
    }

    String getName() {
        return name;
    }
    int getId() {
        return id;
    }
    String getDepartment() {
        return department;
    }
    public String toString() {
        return String.format("[%d, %s, %s]", id, name, department);
    }
    public int compareTo(Employee e) {
        return e.id - this.id;
    }

}

class ComparatorExample {
    public static void main(String[] args) {
        Stream<Employee> workersStream =
                Stream.of(new Employee(11, "Kim Coding", "Software Engineering"),
                new Employee(5, "Hello World", "Growth Marketing"),
                new Employee(7, "Park Hacker", "Software Engineering"));
        workersStream.sorted(Comparator.comparing(Employee::getId)).forEach(System.out::println);

    }
}

Comparable을 기본적으로 구현한 클래스가 아니라면, 다음과 같이 comparing() 메서드를 사용해 비교할 수 있습니다.

연산 결과 확인(peek())

peek(), forEach()는 요소를 하나씩 돌면서 출력한다는 기능에서는 동일하지만, 동작 방식은 다릅니다.

peek은 중간 연산 메서드이고, forEach는 최종 연산 메서드입니다. forEach는 스트림의 요소를 소모하므로 한 번만 호출할 수 있지만(재호출하려면 새로운 스트림을 생성해야 합니다), peek은 중간 연산이므로 하나의 스트림에 여러 번 사용할 수 있습니다.

peek()은 주로 연산 중간에 결과를 확인하여 디버깅하고자 할 때 사용합니다.

intStream
	.filter(a -> a%2 ==0)
	.peek(n-> System.out.println(n))
	.sum();

최종 연산

연산 결과 확인(forEach())

forEach는 최종 연산 메서드이기 때문에 파이프라인 마지막에서 요소를 하나씩 연산합니다.

매칭(match())

Stream의 요소들이 특정한 조건을 충족하는지 검사하고 싶은 경우에는 match() 메서드를 이용할 수 있습니다.

  1. allMatch() : 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
  2. anyMatch() : 최소한 한 개의 요소가 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
  3. 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는 누적하여 하나로 응축(reduce)하는 방식으로 동작합니다. 앞의 두 요소의 연산 결과를 바탕으로 다음 요소와 연산합니다.

reduce 메서드는 최대 3개의 매개변수를 받을 수 있습니다.

  1. Accumulator: 각 요소를 계산한 중간 결과를 생성하기 위해 사용
  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, 등 다른 종류의 결과로 수집하고 싶은 경우에는 collect 메서드를 이용할 수 있습니다. collect 메서드는 어떻게 Stream의 요소들을 수집할 것인가를 정의한 Collector 타입을 인자로 받습니다. 이는 Collector 인터페이스를 구현한 클래스입니다.

일반적으로 List로 Stream의 요소들을 수집하는 경우가 많습니다. 자주 사용하는 작업은 Collectors 객체에서 static 메서드로 제공하고 있고, 원하는 것이 없는 경우에는 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()));
    }
}

List와 Set으로 해도 collect로 사용가능하다.
List 메서드 .collect(Collectors.toList())
Set 메서드 .collect(Collectors.toCollection(HashSet::new))

Optional < T >

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

연산 결과를 Optional에 담아서 반환하면, 따로 조건문을 작성해주지 않아도 NPE가 발생하지 않도록 코드를 작성할 수 있습니다.

Optional

Optional 클래스는 모든 타입의 객체를 담을 수 있는 래퍼(Wrapper) 클래스입니다.

public final class Optional<T> {
	private final T value; // T타입의 참조변수
}
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);
profile
성장하는사람이 되자

0개의 댓글