[자바의 정석 기초편] 스트림 1

JEREGIM·2023년 3월 24일
0

자바의 정석 기초편

목록 보기
21/23

📌스트림(Stream)

: 다양한 데이터 소스(컬렉션, 배열, 파일 등)를 표준화된 방법으로 다루기 위한 것

List<Integer> list = Arrays.asList(1,2,3,4,5); // 컬렉션
Stream<Integer> intStream = list.stream(); // 컬렉션 -> 스트림


Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"}); // 배열 -> 스트림
String[] strArr = new String[] {"a", "b", "c", "d"}; // 배열
Stream<String> strStream = Arrays.stream(strArr); // 배열 -> 스트림

Stream<Integer> evenStream = Stream.iterate(0, n->n+2); // 람다식 -> 스트림 
Stream<Double> randomStream = Stream.generate(Math::random); // 메서드 참조 -> 스트림

// Int형 최적화 Stream : IntStream
IntStream intStream = new Random().ints(5); // 난수 스트림

스트림으로 작업하는 과정

  1. 데이터 소스를 스트림으로 만들기

  2. 중간 연산(0 ~ n번 가능) : 연산 결과가 스트림인 연산. 반복적으로 적용 가능

  3. 최종 연산(0 ~ 1번만 가능) : 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모

List<Integer> list = Arrays.asList(3, 4, 1, 2, 1, 1, 2, 5, 2);
Stream<Integer> intStream = list.stream(); // 스트림 만들기
intStream.distinct() // 중간 연산 - 중복 제거
        .sorted() // 중간 연산 - 정렬
        .forEach(s-> System.out.print(s+" ")); // 최종 연산 - 출력

intStream.forEach(System.out::println); // 에러
  • 최종 연산을 마친 스트림은 다시 사용할 수 없다.

스트림의 특징

스트림은 데이터 소스로부터 데이터를 읽기만할 뿐 변경하지 않는다.(Read only)

List<Integer> list = Arrays.asList(3,1,5,4,2);
List<Integer> sortedList = list.stream()
					.sorted()
					.collect(Collections.toList()); // 정렬해서 sortedList에 저장

System.out.println(list); // [3,1,5,4,2] 
System.out.println(sortedList) ; // [1,2,3,4,5]
  • 원본 데이터인 list는 변경되지 않았다.

스트림은 Iterator처럼 일회용이다. 최종 연산 후 필요하면 다시 스트림을 생성해야한다.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> intStream = list.stream(); // 스트림 만들기

intStream.forEach(System.out::println); // 최종 연산 - 출력
intStream.forEach(System.out::println); // 에러
  • 최종 연산하게 되면 스트림은 소모가 되고 다시 그 스트림으로 작업을 수행 할 수 없다.

스트림은 최종 연산 전까지 중간 연산이 수행되지 않는다. -> 지연된 연산

IntStream intStream = new Random().ints(1, 46); // 1~46 범위의 난수를 생성하는 무한 스트림
intStream.distinct().limit(6).sorted()
	.forEach(i -> System.out.println(i + " ")); // 로또 번호 6개 출력하는 연산
  • 1~46 범위의 난수를 무한으로 생성하는 스트림을 중복 제거하고 6개 자르는 연산이 가능한 이유는 중간 연산이 그때그때 수행되지 않고 최종 연산 때 한꺼번에 수행되기 때문이다.(지연된 연산)

스트림은 작업을 내부 반복으로 처리한다.

  • 성능은 비효율적이지만 코드가 간결해진다.

스트림의 작업을 병렬로 처리할 수 있다. - 병렬 스트림

Stream<String> strStream = Stream.of("dd", "aaa", "c", "bb");
int sum = strStream.parallel()
        .mapToInt(String::length)
        .sum(); // 병렬 스트림
  • 멀티쓰레드로 데이터를 동시에 처리하고 싶을 때 사용한다. 빅데이터를 병렬로 처리해서 작업속도를 높일 수 있다.

  • 기본이 직렬 스트림이다.

  • 병렬로 스트림을 수행하고 싶을 때 parallel() 추가

  • 다시 직렬로 바꾸고 싶을때 sequential() 추가

기본형 스트림 - IntStream, LongStream, DoubleStream

  • 오토 박싱&언박싱의 비효율 제거된다.
    : Stream<T>은 참조형 타입만 올 수 있다. 만약 int[] 배열을 스트림으로 만든다면 int 기본형 타입이 Integer 참조형 타입으로 오토 박싱된다.
    따라서 빅데이터를 다룰 때 효율과 성능을 높이기 위해서 기본형 스트림을 사용한다.

  • 숫자와 관련된 유용한 메서드를 Stream<T>보다 많이 제공한다
    : 기본형 스트림의 요소들은 기본형 타입을 보장하기 때문에 max(), min(), average(), sum() 과 같은 숫자와 관련된 유용한 메서드들을 더 제공한다.


📌스트림 만들기

컬렉션

Collection 인터페이스의 stream()으로 컬렉션을 스트림으로 변환

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> intStream = list.stream(); // 스트림 만들기

배열

객체 배열로부터 스트림 생성

Stream<T> Stream.of(T...values) // 가변 인자
Stream<T> Stream.of(T[])

Stream<T> Arrays.stream(T[])
// 배열의 인덱스 from ~ to(to는 포함x)까지 스트림 생성
Stream<T> Arrays.stream(T[] array, int from, int to) 

Integer[] intArr = {1,2,3,4,5}; //int[] 는 불가능
Stream<Integer> intStream = Arrays.stream(intArr) ; // intArr가 참조형(Integer)이여야 가능

Stream<String> strStream = Stream.of("a", "b", "c")
Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3);

기본형 배열로부터 스트림 생성

IntStream IntStream.of(int...values) // 가변 인자
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
// 배열의 인덱스 from ~ to(to는 포함x)까지 스트림 생성
IntStream Arrays.stream(int[] array, int from, int to)

IntStream intStream2 = IntStream.of(1, 2, 3);
IntStream intStream3 = IntStream.of(new int[]{1, 2, 3});
IntStream intStream4 = Arrays.stream(new int[]{1, 2, 3});
IntStream intStream5 = Arrays.stream(new int[]{1, 2, 3, 4, 5}, 1, 4);
intStream5.forEach(s -> System.out.print(s + " "));

intStream5 : 2 3 4

  • LongStream, DoubleStream도 위 방법과 똑같이 사용가능하다.

난수

임의의 수를 요소로 갖는 스트림 생성

IntStream intStream = new Random().ints(); // 무한 스트림
intStream.limit(5).forEach(System.out::println); // 무한 스트림에서 5개의 요소만 출력

IntStream intStream = new Random().ints(5); // 유한 스트림
  • Random 클래스의 ints(), longs(), doubles() 메서드로 난수 스트림을 만들 수 있다.
  • limit()를 통해 무한 스트림에서 n개의 요소만 출력 할 수 있다.
  • new Random().ints(5); : 난수 스트림을 만들 때 크기를 지정해줄 수도 있다.

ints(), longs(), doubles()의 기본 범위

  • Integer.MIN_VALUE <= ints() <= Integer.MAX_VALUE

  • Long.MIN_VALUE <= longs() <= Long.MAX_VALUE

  • 0.0 <= doubles < 1.0

지정된 범위의 난수를 요소로 갖는 스트림 생성

// 무한 스트림
IntStream ints(int begin, int end)
LongStream longs(long begin, long end)
DoubleStream doubles(double begin, double end)

// 유한 스트림
IntStream ints(long streamSize, int begin, int end)
LongStream longs(long streamSize, long begin, long end)
DoubleStream doubles(long streamSize, double begin, double end)
  • ints(int begin, int end) : begin ~ end 범위(end 포함x)의 난수를 생성하는 스트림
  • ints(long streamSize, int begin, int end) : begin ~ end 범위(end 포함x)의 난수를 streamSize만큼 생성하는 스트림

특정 범위의 정수

특정 범위의 정수를 요소로 갖는 스트림 생성

IntStream IntStream.range(int begin, int end)
IntStream IntStream.rangeClosed(int begin, int end)

IntStream intS = IntStream.range(1, 5) ; // 1,2,3,4
IntStream intS = IntStream.rangeClosed(1, 5) ; // 1,2,3,4,5
  • 두 메서드의 차이는 end 값 포함 여부
    range : end 값 포함 X
    rangeClosed : end 값 포함 O

람다식

람다식을 소스로 하는 스트림 생성

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f) // 이전 요소에 종속적
static <T> Stream<T> generate(Supplier<T> s) // 이전 요소에 독립적
  • 이 두 메서드는 매개변수로 받은 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성

  • T seed 는 초기값

iterate()

: 이전 요소를 seed로 해서 다음 요소를 계산한다.

Stream<Integer> evenStream = Stream.iterate(0, n -> n +2);
evenStream.limit(10).forEach(s -> System.out.print(s + " "));

0 2 4 6 8 10 12 14 16 18

  • 초기값부터 시작해서 람다식을 계산하고 이 결과를 seed로 다시 람다식을 계산한다.
    즉, 이전 요소에 영향을 미친다.

generate()

초기값이 없으며 이전 요소에 독립적이다.

Stream<Integer> oneStream = Stream.generate(() -> 1);
oneStream.limit(10).forEach(s -> System.out.print(s + " "));

1 1 1 1 1 1 1 1 1 1

  • 이전 요소와 상관없이 람다식이 계산된다.

파일과 빈 스트림

파일

파일을 소스로 하는 스트림 생성
Stream<Path> Files.list(Path dir)

  • 지정 디렉토리(dir)에 있는 파일 목록을 소스로 하는 스트림을 생성해 반환한다.
Stream<String> Files.lines(Path path)
Stream<String> Files.lines(Path path, Charset cs)
Stream<String> lines() // BufferedReader 클래스의 메서드
  • 한 파일의 한 행(line)을 요소로 하는 스트림을 생성(보통 로그(Log)파일을 읽을 때 사용)

빈 스트림

요소가 하나도 없는 비어있는 스트림

Stream emptyStream = Stream.empty(); // 비어있는 스트림 생성 및 반환

long cnt = emptyStream.count(); // 비어있기 때문에 당연히 0
  • 연산 수행 결과가 없을 때, null보단 빈 스트림을 반환하는 것이 낫다.

📌중간 연산

: 연산 결과가 스트림인 연산. 0 ~ n번 사용 가능

중간 연산 메서드

중간 연산설명
Stream<T> distinct()중복 제거
Stream<T> filter(Predicate<T> predicate조건에 안맞는 요소 제거
Stream<T> limit(long maxSize)스트림의 일부를 잘라낸다.
Stream<T> skip(long n)스트림의 일부를 건너뛴다.
Stream<T> peek(Consumer<T> action)스트림의 요소에 작업 수행
Stream<T> sorted()
Stream<T> sorted(Comparator<T> comparator)
스트림의 요소를 정렬

map(), flatMap()

반환타입중간 연산설명
Stream<R>map(Function<T, R> mapper)
IntStreammapToInt(ToIntFunction<T> mapper)
LongStreammapToLong(ToLongFunction<T> mapper)
DoubleStreammapToDouble(ToDoubleFunction<T> mapper)
Stream<R>flatMap(Function<T, Stream<R>> mapper)스트림의 요소를 변환한다.
IntStreamflatMapToInt(Function<T, IntStream> mapper)
LongStreamflatMapToLong(Function<T, LongStream> mapper)
DoubleStreamflatMapToDouble(Function<T, DoubleStream> mapper)

skip(), limit()

Stream<T> skip(long n) : 앞에서부터 n개 건너뛴다.

Stream<T> limit(long maxSize) : maxSize 이후의 요소는 잘라낸다.

IntStream intStream = IntStream.rangeClosed(1, 10); // 12345678910
intStream.skip(3).limit(5).forEach(System.out::print);

45678

  • skip(3) : 123 건너뛴다.
  • limit(5) : 4부터 5개 요소만 잘라낸다.

filter(), distinct()

Stream<T> filter(Predicate<T> predicate) : 조건에 맞지 않는 요소를 제거한다.

IntStream intStream = IntStream.rangeClosed(1, 10); // 12345678910
intStream.filter(i -> i % 2 == 0).forEach(System.out::print);

246810

IntStream intStream = IntStream.rangeClosed(1, 10); // 12345678910
intStream.filter(i -> i % 2 == 0 && i % 3 == 0).forEach(System.out::print);

6

  • intStream.filter(i -> i % 2 == 0).filter(i -> i % 3 == 0).forEach(System.out::print); ); : filter를 2번 써서 && 와 같은 동작을 수행할 수 있다.

Stream<T> distinct() : 중복을 제거한다.

IntStream intStream = IntStream.of(1,1,2,2,3,4,4,5);
intStream.distinct().forEach(System.out::print);

12345

sorted()

Stream<T> sorted() : 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬

Stream<T> sorted(Comparator<T> comparator) : 지정된 Comparator으로 정렬

Stream<String> strStream = Stream.of("dd", "aaa", "CC", "cc", "b");

// 기본 정렬 -> 출력 결과 : CCaaabccdd
strStream.sorted(); // String 클래스의 Comparable
strStream.sorted(Comparator.naturalOrder()); // Comparator 클래스에 구현된 기본 정렬 기준 
strStream.sorted((s1, s2) -> s1.compareTo(s2)); // 람다식
strStream.sorted(String::compareTo); // 메서드 참조

// 역순 정렬 -> 출력 결과 : ddccbaaaCC
strStream.sorted(Comparator.reverseOrder()) // 기본 정렬의 역순
strStream.sorted(Comparator.<String>naturalOrder().reversed()) // 잘안씀

// 기본 정렬(대소문자 구분 X) -> 출력 결과 : aaabCCccdd
strStream.sorted(String.CASE_INSENSITIVE_ORDER)

// 역순 정렬(대소문자 구분 X) -> 출력 결과 : ddCCccbaa 오타 아님(대문자가 앞에 온다.)
strStream.sorted(String.CASE_INSENSITIVE_ORDER.reversed())

// 별도 기준 정렬 -> 출력 결과 : bddCCccaaa
strStream.sorted(Comparator.comparing(String::length)) // 문자열 길이 순 정렬
strStream.sorted(Comparator.comparingInt(String::length))

// 별도 기준 역순 정렬 -> 출력 결과 : aaaddCCccb
strStream.sorted(Comparator.comparing(String::length).reversed())

comparing(), thenComparing()

Comparator의 comparing()으로 정렬 기준을 제공

Comparator<T> comparing(Function<T, U> keyExtractor)
Comparator<T> comparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)

thenComparing() : 추가 정렬 기준을 제공할 때 사용

Comparator<T> thenComparing(Comparator<T> other)
Comparator<T> thenComparing(Function<T, U> keyExtractor)
Comparator<T> thenComparing(Function<T, U> keyExtractor, Comparator<U> keyComparator)

실습 예제

import java.util.*;
import java.util.stream.*;

class Student implements Comparable<Student> {
    private String name;
    private int ban;
    private int totalScore;

    Student(String name, int ban, int totalScore) {
        this.name = name;
        this.ban = ban;
        this.totalScore = totalScore;
    }
	
	// toString() 오버라이딩
    public String toString() {
        return String.format("[%s, %d, %d]", name, ban, totalScore);
    }

    public String getName() { return name; }
    public int getBan() { return ban; }
    public int getTotalScore() { return totalScore; }

    // 총점 내림차순으로 기본 정렬
    public int compareTo(Student s) {
        return s.totalScore - this.totalScore;
    }
}

class Ex14_5 {
    public static void main(String[] args) {
        Stream<Student> studentStream = Stream.of(
                new Student("이빛나", 3, 300),
                new Student("김자바", 1, 200),
                new Student("안정호", 2, 100),
                new Student("박코딩", 2, 150),
                new Student("최자연", 1, 200),
                new Student("연보라", 3, 290),
                new Student("정빛나", 3, 180)
        );

        studentStream.sorted(Comparator.comparing(Student::getBan)
                        .thenComparing(Comparator.naturalOrder()))
                .forEach(System.out::println);
    }
}

[김자바, 1, 200]ㅤ
[최자연, 1, 200] ㅤ
[박코딩, 2, 150]ㅤ
[안정호, 2, 100]ㅤ
[이빛나, 3, 300]ㅤ
[연보라, 3, 290]ㅤ
[정빛나, 3, 180]

  • 반 순서대로 정렬하고 기본 정렬(성적 내림차순)
studentStream.sorted(Comparator.comparing(Student::getBan).reversed()
                .thenComparing(Student::getName))
        .forEach(System.out::println);

[연보라, 3, 290]ㅤ
[이빛나, 3, 300]ㅤ
[정빛나, 3, 180]ㅤ
[박코딩, 2, 150]ㅤ
[안정호, 2, 100]ㅤ
[김자바, 1, 200]ㅤ
[최자연, 1, 200]ㅤ

  • 반 역순으로 정렬하고 이름 순서대로 정렬

map()

스트림의 요소 변환

import java.io.*;
import java.util.stream.*;

class Ex14_6 {
    public static void main(String[] args) {
        File[] fileArr = {
                new File("Ex1.java"),
                new File("Ex1.bak"),
                new File("Ex2.java"),
                new File("Ex1"),
                new File("Ex1.txt")
        };

        Stream<File> fileStream = Stream.of(fileArr);

        fileStream.map(File::getName) // Stream<File> -> Stream<String>
                .filter(s -> s.indexOf('.') != -1) // 확장자 없는 파일 삭제
                .map(s -> s.substring(s.indexOf('.') + 1)) // 확장자명만 추출
                .map(String::toUpperCase) // 대문자로 변경
                .distinct() // 중복 제거
                .forEach(System.out::print); // JAVABAKTXT
    }
}

JAVABAKTXT

peek()

스트림의 요소를 소비하지 않고 엿보기

Stream<T> peek(Consumer<T> action)
void forEach(Consumer<T> action)
  • peek()은 중간 연산. 스트림의 요소를 소비 X
  • forEach()는 최종 연산. 스트림의 요소를 소비 O
fileStream.map(File::getName) // Stream<File> -> Stream<String>
        .filter(s -> s.indexOf('.') != -1) // 확장자 없는 파일 삭제
        .peek(s -> System.out.printf("파일명 : %s%n", s))
        .map(s -> s.substring(s.indexOf('.') + 1)) // 확장자명만 추출
        .peek(s -> System.out.printf("확장자 : %s%n", s))
        .map(String::toUpperCase) // 대문자로 변경
        .distinct() // 중복 제거
        .forEach(System.out::print); // JAVABAKTXT

System.out.println();

파일명 : Ex1.java
확장자 : JAVA
JAVA파일명 : Ex1.bak
확장자 : BAK
BAK파일명 : Ex2.java
확장자 : JAVA
파일명 : Ex1.txt
확장자 : TXT
TXT

  • 메서드 중간 중간마다 수행이 잘되고 있는지 확인하는 디버깅 용도로 쓰인다.

flatMap()

스트림의 스트림을 스트림으로 변환

Stream<String[]> strArrStrm = Stream.of(
        new String[]{"abc", "def", "jkl"},
        new String[]{"ABC", "GHI", "JKL"}

Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);

String[] strings = strStrm.map(String::toLowerCase)
        .distinct()
        .sorted()
        .toArray(String[]::new);
System.out.println(Arrays.toString(strings));

[abc, def, ghi, jkl]

  • 문자열 배열을 가지고 있는 스트림을 flatMap을 통해 각 문자열 배열의 요소들을 합쳐서 하나의 스트림으로 만든다.
String[] lineArr = {
        "Believe or not It is true",
        "Do or do not There is no try",
};

Stream<String> lineStream = Arrays.stream(lineArr);
List<String> list = lineStream.flatMap(line -> Stream.of(line.split(" +")))
        .map(String::toLowerCase)
        .distinct()
        .sorted()
        .collect(Collectors.toList());
System.out.println(list);

[believe, do, is, it, no, not, or, there, true, try]

  • 문장들을 요소로 하는 String[] 배열을 flatMap을 통해 각 문장의 단어들을 하나로 합친 스트림으로 만든다.

0개의 댓글