Java 프로그래밍기초 230411 #17 스트림

김춘복·2023년 4월 12일
0

Java 공부

목록 보기
20/20

스트림

컬렉션이나 배열에 데이터를 담고 for문이나 Iterator를 쓰면 코드의 가독성과 재사용성이 떨어진다. 스트림을 통해 컬렉션이나 배열에 함수 여러개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있다. 또한 람다를 이용해 코드의 양을 줄이고 간결하게 표현할 수 있다.
즉, 컬렉션과 배열을 함수형으로 처리할 수 있다.

스트림의 특징

  • 스트림은 데이터 소스를 변경하지 않는다. 데이터 소스로부터 데이터를 읽기만 할 뿐 데이터 소스는 변경하지 않는다. 필요하다면 정렬된 결과를 배열이나 컬렉션에 담아서 반환할 수 있다.

  • 스트림은 일회용이다. 한번 사용하면 닫혀서 다시 쓸 수 없다. 필요하면 다시 생성해야 한다.

  • 스트림은 작업을 내부 반복으로 처리한다. 내부반복은 반복문을 메서드 내부에 숨겼다는 것을 의미한다.

  • 지연된 연산. 최종 연산이 수행되기 전 까지 중간연산이 수행되지 않는다. 중간에 sort()같은 연산이 있어도 즉각적으로 수행되지 않는다. 최종연산이 수행되어야 스트림의 요소들이 중간연산을 거쳐 최종연산에서 소모된다.

  • 병렬처리가 가능하다. 병렬처리는 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것을 말한다. 쓰레드를 이용해 많은 요소를 빠르게 처리할 수 있다. 스트림에 parallel()이라는 메서드를 호출해 병렬로 연산을 수행하게 하면 병렬연산이 된다.
    sequential()을 호출하면 병렬처리 기능을 끌 수 있다.

  • 타입을 지정할때 기본적으로 Stream<T> 지만 IntStream, LongStream, DoubleStream이 기본적으로 제공되고 이를 활용하는 것이 효율적이다.

스트림 생성

컬렉션, 배열, 임의의 수 등을 소스로 스트림을 생성할 수 있다.

  • 컬렉션
    컬렉션의 최고 조상인 Collection에 stream()이 정의되어있어 List와 Set을 구현한 컬렉션 클래스는 모두 이 메서드로 스트림을 생성할 수 있다.
List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> stream1 = list.stream();	// 스트림 생성
stream1.forEach(System.out::println);
  • 배열
    Stream과 Arrays에 static 메서드로 정의되어 있다. Stream.of() / Arrays.stream()
Stream<String> stream2 = Stream.of("a", "b","c");
Stream<String> stream3 = Arrays.stream(new String[]{"a", "b","c"});
IntStream IntStream.of(1,2,3,4,5);
  • 임의의 수
    난수를 생성하는데 쓰이는 Random클래스에는 난수로 이루어진 스트림을 반환하는 메서드가 있다.
    이 메서드들은 크기가 정해지지 않은 무한 스트림이므로 limit()로 스트림의 크기(개수)를 제한해주어야 한다. limit()를 쓰지 않고 ints(5) 처럼 파라미터로 안에 넣어도 된다.
    IntStream : ints()
    LongStream : longs()
    DoubleStream : doubles()
    IntStream.range(int begin, int end) 로 쓰면 begin<= x < end 범위의 정수가 스트림 안으로 들어간다.

  • generate(), iterate() 람다식
    static <T> Stream <T> generate(Supplier<T> s)
    람다식에 의해 계산되는 값들을 요소로 하는 무한스트림 생성
    static <T> Stream <T> iterate(T seed, UnaryOperator<T> f)
    seed부터 시작해서 람다식 f에 의해 계산된 결과를 반복. 이전결과를 이용해 다음 요소 계산
    ex) Stream<Integer> stream4 = Stream.iterate(20, n -> n+2).limit(3); 20,22,24

  • 빈 스트림. 스트림 연산 수행 결과가 없을때 null보다 빈스트림을 반환하는 것이 좋다.
    Stream emptyStream = Stream.empty();

  • builder() 빌더를 사용하면 원하는 값을 직접 넣을 수 있다.

Stream<String> builderStream = Stream.<String>builder().add("java").add("hi").build();

스트림의 중간 연산

스트림의 연산을 이용하면 복잡한 작업을 간단히 처리할 수 있다.
중간연산은 연산 결과가 스트림이라 연속해서 사용할 수 있다.

Skip(), limit()

skip(long n)은 처음 n개를 건너 뛰고 limit(long maxSize)는 요소를 maxSize로 제한

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

filter(), distinct()

distinct()는 중복요소 제거. filter()는 주어진 조건(Predicate)에 맞지 않는 요소 제거

IntStream intStream = IntStream.of(1,2,2,3,3,4,4,5,6,7,8);
intStream.distinct().filter(i -> i%2==0).forEach(System.out.print); // 2468

sorted()

지정된 Comparator로 스트림을 정렬. Coparator 대신 int 반환하는 람다식 사용 가능.
Coparator 미지정시 기본 정렬 기준으로 정렬. 문자열은 사전순(대문자우선알파벳순). 정수는 오름차순
.sorted(Comparator.reverseOrder()) : 기본정렬의 역순 정렬

  • Comparator의 메서드인 comparing() 사용 가능. 정렬 조건 추가시 thenComparing() 사용
// 학생을 반별, 성적순, 이름순으로 정렬해 출력하는 스트림
studentStream.sorted(Comparator.comparing(Student::getBan)
								.thenComparing(Student::getTotalScore)
                                .thenCompating(Student::getName)
                                .forEach(System.out::println));

map()

원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때 사용

// 파일이 요소로 들어있는 스트림에서 확장자만 뽑은 다음 중복 제거후 출력
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) // TXTJPGJAVA

peek()

연산 사이에 올바르게 처리되었는지 확인 가능.
최종 연산이 아니므로 연산 사이에 여러번 끼워도 문제가 문제는 안된다.
.peek(s -> System.out.printf("파일이름=%s%n", s)) 식으로 사용할 수 있다.

flatMap()

스트림의 타입이 Stream<T[]> 처럼 배열인 경우 Stream<T>로 변환할 수 있다.
Stream<String[]> 을 .flatMap(Arrays::stream)으로 Stream<String>으로 변환 가능.


스트림의 최종 연산

최종연산은 스트림의 요소를 소모해서 결과를 만들어 낸다. 따라서 최종 연산은 한번만 가능하고 그 뒤에 스트림의 재사용은 불가능하다.

forEach()

peek()과 달리 스트림의 요소를 소모하는 최종연산이다.
반환타입이 void라 주로 스트림의 요소를 출력하는 용도로 사용된다.

~~Match()

스트림의 요소에 대해 지정된 조건에 요소들이 일치하는지 확인하는데 사용한다.
매개변수로 Predicate, 연산 결과로 boolean을 반환한다.

boolean allMatch (Predicate) // 모든 요소가 일치하면 true
boolean anyMatch (Predicate) // 하나라도 일치하면 true
boolean noneMatch (Predicate) // 모든 요소가 불일치면 true

// 모든 학생이 80점 넘었는지 확인
boolean success = stuStream.allMatch(s->s.getScore()>=80) 

reduce()

스트림의 요소를 하나씩 줄여나가면서 연산을 수행하고 최종 결과를 반환
매개변수의 타입이 BinaryOperator<T>다.
처음 두 요소로 연산한 결과를 다음 요소와 연산해 하나씩 줄여나간다.

int count = intStream.reduce(0, (a,b) -> a+1); // count()
int sum = intStream.reduce(0, (a,b) -> a+b); // sum()
int max = intStream.reduce(Integer.MIN_VALUE, (a,b) -> a>b ? a : b) // max()

collect()

컬렉터(Collector)로 정의 된 방법으로 스트림의 요소를 수집해 최종연산을 수행한다.
최종 연산중 가장 복잡하지만 유용하게 활용될 방법이 많다.

Collector : 인터페이스. 컬렉터는 이 인터페이스를 구현해야 한다.
Collectors : 클래스. static 메서드로 미리 작성된 컬렉터
collect() : 스트림의 최종연산. 매개변수로 컬렉터를 필요로 한다.

스트림을 컬렉션, 배열로 반환

~~.collect(Collectors.toList()); 처럼 사용해서 컬렉션으로 반환하게 할 수 있다.

List<String> names = stuStream.map(Student::getName())	// toList()로 리스트 변환
								.collect(Collectors.toList());
TreeSet<Integer> ids = stuStream.map(Student::getId()) // toCollection(생성자)로 treeset으로 변환
							.collect(Collectors.toCollection(TreeSet::new));
Map<String,Person> map1 = personStream	// map은 key-value 각각 할당해서 변환
				.collect(Collectors.toMap(p->p.getId(), p->p));

스트림의 그룹화와 분할

  • 분할 : 스트림의 요소를 지정된 조건(Predicate)에 일치하는 그룹과 일치하지 않는 그룹으로 분할 .partitioningBy() 두개로 나눌때는 분할이 빠르다.
    Collector partitioningBy(Predicate predicate)
// 학생들을 성별로 분할한 뒤 통계정보를 추가
Map<Boolean, Long> stuNumBySex = stuStream
					.collect(partitioningBy(Student::isMale, counting())); 
System.out.println(stuNumBySex.get(true)); // 남학생 수
System.out.println(stuNumBySex.get(false)); // 여학생 수
  • 그룹화 : 스트림의 요소를 특정 기준(Function)으로 그룹화
    .groupingBy() 여러개로 나눌 때 쓴다.
    Collector groupingBy(Function classifier)
// 학생들을 성적 상중하 그룹으로 분류후 통계 정보 추가
Map<Student.Level, Long> stuByLevel = stuStream
					.collect(groupingBy(s -> {
                    if(s.getScore>=80) return Student.Level.HIGH;
                    else if(s.getScore >= 60) return Student.Level.MID;
                    else return Student.Level.Low;
                    }, counting()));
System.out.println(stuByLevel.get(Student.Level.HIGH)); // 상반 학생 수
                    

그 외 사용 방법

  • Collectors.counting()으로 collect()안에서 count()처럼 카운트를 셀 수 있다.

  • Collectors.reducing()으로 collect()안에서 reduce()처럼 요소를 줄여나가며 연산을 할 수 있다.

  • Collectors.joining()으로 문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환할 수 있다.

profile
Backend Dev / Data Engineer

0개의 댓글