본문 바로가기
자바/이것이 자바다

17. 스트림 요소 처리

by 989898 2023. 11. 21.

17.1 스트림이란?

지금까지 컬렉션 및 배열에 저장된 요소를 반복 처리하기 위해서는 for 문을 이용하거나 Iterator(반복자)를 이용했다. 다음은 List 컬렉션에서 요소를 하나씩 처리하는 for문이다.

그리고 Set에서 요소를 하나씩 처리하기 위해 Iterator를 다음과 같이 사용했다.

Java 8부터는 또 다른 방법으로 컬렉션 및 배열의 요소를 반복 처리하기 위해 스트림을 사용할 수 있다.

 

스트림은 요소들이 하나씩 흘러가면서 처리된다는 의미를 가지고 있다. List 컬렉션에서 요소를 반복 처리하기 위해 스트림을 사용하면 다음과 같다.

List 컬렉션의 stream() 메소드로 Stream 객체를 얻고, forEach() 메소드로 요소를 어떻게 처리할지를 람다식으로 제공한다. 다음 예제는 Set 컬렉션의 요소를 하나씩 읽고 출력하기 위해 스트림을 사용한다.

package ch17.sec01.exam01;

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

public class StreamExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		// Set 컬렉션 생성
		Set<String> set = new HashSet<>();
		set.add("홍길동");
		set.add("신용권");
		set.add("감자바");

		// Stream을 이용한 요소 반복 처리
		Stream<String> stream = set.stream(); // set을 Stream으로 변환
		stream.forEach(name -> System.out.println(name));
	}

}

Stream은 Iterator와 비슷한 반복자이지만, 다음과 같은 차이점을 가지고 있다.

자세한 내용은 다음 절부터 하나씩 알아보기로 하자.


17.2 내부 반복자

for 문과 Iterator는 컬렉션의 요소를 컬렉션 바깥쪽으로 반복해서 가져와 처리하는데, 이것을 외부 반복자라고 한다. 반면 스트림은 요소 처리 방법을 컬렉션 내부로 주입시켜서 요소를 반복 처리하는데, 이것을 내부 반복자라고 한다. 다음 그림을 보면서 외부 반복자와 내부 반복자를 이해해보자.

외부 반복자일 경우는 컬렉션의 요소를 외부로 가져오는 코드와 처리하는 코드를 모두 개발자 코드가 가지고 있어야 한다.

반면 내부 반복자일 경우는 개발자 코드에서 제공한 데이터 처리 코드(람다식)를 가지고 컬렉션 내부에서 요소를 반복 처리한다.

 

내부 반복자는 멀티 코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있다.
하나씩 처리하는 순차적 외부 반복자보다는 효율적으로 요소를 반복시킬 수 있는 장점이 있다.

다음 예제는 List 컬렉션의 내부 반복자를 이용해서 병렬 처리하는 방법을 보여준다. parallelStream() 메소드로 병렬 처리 스트림을 얻고, forEach() 메소드를 호출할 때 요소 처리 방법인 람다식을 제공한다. 람다식은 처리되는 요소가 무엇이고, 어떤 스레드가 처리하는지를 출력한다.

package ch17.sec02;

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

public class ParallelStreamExample {

	public static void main(String[] args) {
		// List 컬렉션 생성
		List<String> list = new ArrayList<>();
		list.add("홍길동");
		list.add("신용권");
		list.add("감자바");
		list.add("람다식");
		list.add("박병렬");

		// 병렬 처리
		Stream<String> parallelStream = list.parallelStream();
		// 람다식 -> 요소 처리 방법
		parallelStream.forEach(name -> {
			System.out.println(name + ": " + Thread.currentThread().getName());
		});
	}

}

package ch17.sec02;

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

public class ParallelStreamExample {

	public static void main(String[] args) {
		// List 컬렉션 생성
		List<String> list = new ArrayList<>();
		list.add("홍길동");
		list.add("신용권");
		list.add("감자바");
		list.add("람다식");
		list.add("박병렬");

		// 병렬 처리가 아닐 때
		Stream<String> parallelStream = list.stream();
		// 람다식 -> 요소 처리 방법
		parallelStream.forEach(name -> {
			System.out.println(name + ": " + Thread.currentThread().getName());
		});
	}

}

 

처리할 요소가 적을 때는 병렬 처리 시스템은 비효율적이다.


17.3 중간 처리와 최종 처리

스트림은 하나 이상 연결될 수 있다. 다음 그림을 보면 컬렉션의 오리지널 스트림 뒤에 필터링 중간 스트림이 연결될 수 있고, 그 뒤에 중간 스트림이 연결될 수 있다. 이와 같이 스트림이 연결되어 있는 것을 스트림 파이프라인 이라고 한다.

오리지널 스트림과 집계 처리 사이의 중간 스트림들은 최종 처리를 위해

 

요소를 걸러내거나(필터링), 요소를 변환시키거나(매핑), 정렬하는 작업을 수행한다.

 

최종 처리는 중간 처리에서 정제된 요소들을 반복하거나, 집계(카운팅, 총합, 평균) 작업을 수행한다.

 

다음 그림은 Student 객체를 요소로 가지는 컬렉션에서 Student 스트림을 얻고, 중간 처리를 통해 score 스트림으로 변환한 후 최종 집계 처리로  score 평균을 구하는 과정을 나타낸 것이다.

이것을 코드로 표현하면 다음과 같다.

map이 나오면 이렇게 생각하자 - > 아~ 어떤 객체의 타입을 바꾸는구나!!

 

mapToInt() 메소드는 객체를 int 값으로 매핑해서 IntStream으로 변환시킨다. 어떤 객체를 어떤 int 값으로 매핑할 것인지는 람다식으로 제공해야 한다. student -> student.getScore() 는 Student 객체를 getScore()의 리턴값으로 매핑한다. IntStream은 최종 처리를 위해 다양한 메소드를 제공하는데, average() 메소드는 요소들의 평균 값을 계산한다.

 

메소드 체이닝 패턴을 이용하면 앞의 코드를 다음과 같이 더 간결하게 작성할 수 있다.

스트림 파이프 라인으로 구성할 때 주의할 점은 파이프라인의 맨 끝에는 반드시 최종 처리 부분이 있어야 한다는 것이다.

최종 처리가 없다면 오리지널 및 중간처리 스트림은 동작하지 않는다. 즉, 위 코드에서 average() 이하를 생략하면 stream(), mapToInt()는 동작하지 않는다.

 

package ch17.sec03;

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

 

package ch17.sec03;

import java.util.*;

public class StreamPipeLineExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		List<Student> list = Arrays.asList(
				new Student("홍길동", 10), 
				new Student("신용권", 20), 
				new Student("유미선", 30)
			);

		double avg = list.stream()
				.mapToInt(student -> student.getScore())
				.average()
				.getAsDouble();
		
		System.out.println("평균 점수 : " + avg);
	}

}


17.4 리소스로부터 스트림 얻기

java.utll.stream 패키지에는 스트림 인터페이스들이 있다. BaseStream 인터페이스를 부모로 한자식 인터페이스들은 다음과 같은 상속 관계를 이루고 있다.

BaseStream에는 모든 스트림에서 사용할 수 있는 공통 메소드들이 정의되어 있다. Stream은 객체 요소를 처리하는 스트림이고, IntStream, LongStream, DoubleStream은 각각 기본 타입인 int, long, double 요소를 처리하는 스트림이다.

 

이 스트림 인터페이스들의 구현 객체는 다양한 리소스로부터 얻을 수 있다. 주로 컬렉션과 배열에서 얻지만, 다음과 같은 리소스로부터 스트림 구현 객체를 얻을 수도 있다.

 

컬렉션으로부터 스트림 얻기

 

java.utll.Collection 인터페이스는 스트림과 parallelStream() 메소드를 가지고 있기 때문에 자식 인터페이스인 List와 Set 인터페이스를 구현한 모든 컬렉션에서 객체 스트림을 얻을 수 있다. 다음 예제는 List<Product> 컬렉션에서 Product 스트림을 얻는 방법을 보여준다.

 

"toString" 메서드는 객체가 가지고 있는 정보나 값들을 문자열로 만들어 리턴하는 메소드 이다.

하지만 "String" 클래스나 "File"클래스에서는 "toString"에 메소드를 재정의 하여 의미있는 값을 리턴해줄 수 있다.

그리고 toString() 메소드는 자동으로 호출된다.

 

package ch17.sec04.exam01;

public class Product {
	private int pno;
	private String name;
	private String company;
	private int price;

	public Product(int pno, String name, String company, int price) {
		this.pno = pno;
		this.name = name;
		this.company = company;
		this.price = price;
	}

	public int getPno() {
		return pno;
	}

	public String getName() {
		return name;
	}

	public String getCompany() {
		return company;
	}

	public int getPrice() {
		return price;
	}

	@Override
	public String toString() {
		return new StringBuilder().append("{").append("pno:" + pno + ", ").append("name:" + name + ", ")
				.append("company:" + company + ", ").append("price:" + price).append("}").toString();
	}
}

 

package ch17.sec04.exam01;

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

public class StreamExample {

	public static void main(String[] args) {
		List<Product> list = new ArrayList<>();
		for (int i = 1; i <= 5; i++) {
			Product product = new Product(i, "상품" + i, "멋진 회사", (int) (10000 * Math.random()));
			list.add(product);
		}

		// 객체 스트림 얻기
		Stream<Product> stream = list.stream();
		stream.forEach(p -> System.out.println(p));
	}

}

 

 

배열로부터 스트림 얻기

 

java.utll.Arrays 클래스를 이용하면 다양한 종류의 배열로부터 스트림을 얻을 수 있다. 다음은 문자열 배열과 정수 배열로부터 스트림을 얻는 방법을 보여준다.

package ch17.sec04.exam02;

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

public class StreamExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String[] strArray = { "홍길동", "신용권", "김미나" };
		Stream<String> strStream = Arrays.stream(strArray);
		strStream.forEach(item -> System.out.print(item + ","));
		System.out.println();

		int[] intArray = { 1, 2, 3, 4, 5 };
		IntStream intStream = Arrays.stream(intArray);
		intStream.forEach(item -> System.out.print(item + ","));
		System.out.println();
	}

}

 

숫자 범위로부터 스트림 얻기

 

IntStream 또는 LongStream의 정적 메소드인 range()와 rangeClosed() 메소드를 이용하면 특정 범위의 정수 스트림을 얻을 수 있다. 첫 번째 매개값은 시작 수이고 두 번째 매개값은 끝 수인데, 끝 수를 포함하지 않으려면 range(), 포함하려면 rangeClosed()를 사용한다.

package ch17.sec04.exam03;

import java.util.stream.*;

public class StreamExample {
	public static int sum;

	public static void main(String[] args) {
		IntStream stream = IntStream.rangeClosed(1, 100);
		stream.forEach(a -> sum += a);
		System.out.println("총합 : " + sum);
	}
}

파일로부터 스트림 얻기

 

java.nio.Files의 lines() 메소드를 이용하면 텍스트 파일의 행 단위 스트림을 얻을 수 있다. 이는 텍스트 파일에서 한 행씩 읽고 처리할 때 유용하게 사용할 수 있다. 다음 data.txt 파일은 한 행에 하나의 상품에 대한 정보를 담고 있다.

{"pno":1, "name":"상품1", "company":"멋진 회사", "price":1558};
{"pno":2, "name":"상품2", "company":"멋진 회사", "price":4671};
{"pno":3, "name":"상품3", "company":"멋진 회사", "price":470};
{"pno":4, "name":"상품4", "company":"멋진 회사", "price":9584};
{"pno":5, "name":"상품5", "company":"멋진 회사", "price":6868};

 

data.txt 파일을 한 행씩 읽고 상품 정보를 출력하기 위해 Files의 lines() 메소드를 이용하는 방법은 다음과 같다.

 

URI -> 식별정보를 갖고 있는 객체

URL -> 어디 있는지 위치정보를 가지고 있는 객체

package ch17.sec04.exam04;

import java.nio.charset.*;
import java.nio.file.*;
import java.util.stream.*;

public class StreamExample {

	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub
		Path path = Paths.get(StreamExample.class.getResource("data.txt").toURI());
		Stream<String> stream = Files.lines(path, Charset.defaultCharset());
		stream.forEach(line -> System.out.println(line));
		stream.close();
	}

}

 


17.5 요소 걸러내기 (필터링)

필터링은 요소를 걸러내는 중간 처리 기능이다. 필터링 메소드에는 다음과 같이 distinct()와 filter()가 있다.

distinct()  메소드는 요소의 중복을 제거한다. 객체 스트림(Stream)일 경우, equals() 메소드의 리턴값이 true이면 동일한 요소로 판단한다. IntStream, LongStream, DoubleStream은 같은 값일 경우 중복을 제거한다.

filter() 메소드는 매개값으로 주어진 Predicate가 true를 리턴하는 요소만 필터링한다.

Predicate는 함수형 인터페이스로, 다음과 같은 종류가 있다.

모든 Predicate는 매개값을 조사한 후 boolean을 리턴하는 test() 메소드를 가지고 있다.

Predicate<T>을 람다식으로 표현하면 다음과 같다.

다음 예제는 이름 List에서 중복된 이름을 제거하고 출력한다. 이어서 성이 '신'인 이름만 필터링해서 출력한다.

package ch17.sec05;

import java.util.*;

public class FilteringExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		List<String> list = new ArrayList<>();
		list.add("홍길동");
		list.add("신용권");
		list.add("감자바");
		list.add("신용권");
		list.add("신민철");

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

		// 신으로 시작하는 요소만 필터링
		list.stream().filter(n -> n.startsWith("신")).forEach(n -> System.out.println(n));
		System.out.println();

		// 중복 요소를 먼저 제거하고, 신으로 시작하는 요소만 필터링
		list.stream().distinct().filter(n -> n.startsWith("신")).forEach(n -> System.out.println(n));
	}

}

 


17.6 요소 변환 (매핑)

매핑은 스트림의 요소를 다른 요소로 변환하는 중간 처리 기능이다.

 

매핑 메소드는 mapXxx(), asDoubleStream(), asLongStream(), boxed(), flatMapXxx() 등이 있다.

 

요소를 다른 요소로 변환

 

mapXxx() 메소드는 요소를 다른 요소로 변환한 새로운 스트림을 리턴한다. 다음 그림처럼 원래 스트림의 A 요소는  C요소로, B 요소는 D 요소로 변환해서 C,D 요소를 가지는 새로운 스트림이 생성된다.

mapXxx() 메소드의 종류는 다음과 같다.

매개타입인 Function은 함수형 인터페이스로, 다음과 같은 종류가 있다.

모든 Function은 매개값을 리턴값으로 매핑(변환)하는 applyXxx() 메소드를 가지고 있다.

Function <T,R>을 람다식으로 표현하면 다음과 같다.

다음은 Student 스트림을 score 스트림으로 변환하고 점수를 콘솔에 출력하는 예제이다.

package ch17.sec06.exam01;

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

 

package ch17.sec06.exam01;

import java.util.*;

public class MapExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		List<Student> studentList = new ArrayList<>();
		studentList.add(new Student("홍길동", 85));
		studentList.add(new Student("홍길동", 92));
		studentList.add(new Student("홍길동", 87));

		// Student를 score 스트림으로 변환
		studentList.stream().mapToInt(s -> s.getScore()).forEach(score -> System.out.println(score));
	}

}

 

기본 타입 간의 변환이거나 기본 타입 요소를 래퍼 객체 요소로 변환하려면 다음과 같은 간편화 메소드를 사용할 수도 있다.

다음은 정수 스트림을 실수 스트림으로 변환하고, 기본 타입 스트림을 래퍼 스트림으로 변환하는 방법을 보여준다.

요소를 복수 개의 요소로 변환

 

flatMapXxx() 메소드는 하나의 요소를 복수 개의 요소들로 변환한 새로운 스트림을 리턴한다. 다음 그림처럼 원래 스트림의 A요소를 A1, A2 요소로 변환하고 B 요소를 B1, B2로 변환하면 A1, A2, B1, B2 요소를 가지는 새로운 스트림이 생성됨.

flatMap() 메소드의 종류는 다음과 같다.

다음 예제는 문장 스트림을 단어 스트림으로 변환하고, 문자열 숫자 목록 스트림을 숫자 스트림으로 변환한다.

package ch17.sec06;

import java.util.*;

public class FlatMappingExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		List<String> list1 = new ArrayList<>();
		list1.add("this is java");
		list1.add("i am a best developer");
		list1.stream().flatMap(data -> Arrays.stream(data.split(" "))).forEach(word -> System.out.println(word));

		System.out.println();

		List<String> list2 = Arrays.asList("10, 20, 30, 40 ,50");
		list2.stream().flatMapToInt(data -> {
			String[] strArr = data.split(",");
			int[] intArr = new int[strArr.length];
			for (int i = 0; i < strArr.length; i++) {
				intArr[i] = Integer.parseInt(strArr[i].trim());
			}
			return Arrays.stream(intArr);
		}).forEach(number -> System.out.println(number));
	}

}

 


17.7 요소 정렬

정렬은 요소를 오름차순 또는 내림차순으로 정렬하는 중간 처리 기능이다. 요소를 정렬하는 메소드는 다음과 같다.

 

Comparable 구현 객체의 정렬

 

스트림의 요소가 객체일 경우 객체가 Comparable을 구현하고 있어야만 sorted() 메소드를 사용하여 정렬할 수 있다. 그렇지 않다면 ClassCastException이 발생한다. Comparable을 구현하는 자세한 방법은 15.5절을 참고.

만약 내림차순으로 정렬하고 싶다면 다음과 같이 Comparator.reverseOrder() 메소드가 리턴하는 Comparator를 매개값으로 제공하면 된다.

다음은 Student 스트림을 score 기준으로 올림차순 또는 내림차순으로 정렬한 새로운 Student 스트림을 생성하는 방법을 보여준다. 정렬을 하기 위해 Student 클래스가 Comparable을 구현하고 있는 것을 볼 수 있다.

package ch17.sec07.exam01;

public class Student implements Comparable<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;
	}

	@Override
	public int compareTo(Student o) {
		// TODO Auto-generated method stub
		// score와 o.score가 같을 경우 0을 리턴, 작을 경우 음수를 리턴, 클 경우 양수를 리턴
		return Integer.compare(score, o.score);
	}

}

 

package ch17.sec07.exam01;

import java.util.*;

public class SortExample {

	public static void main(String[] args) {
		// List 컬렉션 생성
		List<Student> studentList = new ArrayList<>();
		studentList.add(new Student("홍길동", 30));
		studentList.add(new Student("신용권", 10));
		studentList.add(new Student("유미선", 20));

		// 정수를 기준으로 오름차순으로 정렬한 새 스트림 얻기
		studentList.stream().sorted().forEach(s -> System.out.println(s.getName() + ": " + s.getScore()));
		System.out.println();

		// 점수를 기준으로 내림차순으로 정렬한 새 스트림 얻기
		studentList.stream().sorted(Comparator.reverseOrder())
				.forEach(s -> System.out.println(s.getName() + ": " + s.getScore()));
		System.out.println();
	}

}

 

Comparator를 이용한 정렬

 

요소 객체가 Comparable을 구현하고 있지 않다면, 비교자를 제공하면 요소를 정렬시킬 수 있다.

비교자는 Comparator 인터페이스를 구현한 객체를 말하는데, 15.5절에서는 명시적인 클래스로 구현하는 방법을 설명했지만, 다음과 같이 간단하게 람다식으로 작성할 수도 있다.

중괄호 안에는 o1이 o2보다 작으면 음수, 같으면 0, 크면 양수를 리턴하도록 작성하면 된다. o1과 o2가 정수일 경우에는 Integer.compare(o1,o2)를, 실수일 경우에는 Double.compare(o1,o2)를 호출해서 리턴값을 리턴해도 좋다.

 

다음 예제는 Student 클래스가 Comparable을 구현하고 있지 않기 때문에 비교자를 람다식으로 제공하고 있다.

package ch17.sec07.exam02;

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

}

 

package ch17.sec07.exam02;

import java.util.*;

public class sortingExample {

	public static void main(String[] args) {
		// List 컬렉션 생성
		List<Student> studentList = new ArrayList<>();
		studentList.add(new Student("홍길동", 30));
		studentList.add(new Student("신용권", 10));
		studentList.add(new Student("유미선", 20));

		// 정수를 기준으로 오름차순으로 정렬한 새 스트림 얻기
		studentList.stream().sorted((s1, s2) -> Integer.compare(s1.getScore(), s2.getScore()))
				.forEach(s -> System.out.println(s.getName() + ": " + s.getScore()));
		System.out.println();

		// 점수를 기준으로 내림차순으로 정렬한 새 스트림 얻기
		studentList.stream().sorted((s1, s2) -> Integer.compare(s2.getScore(), s1.getScore()))
				.forEach(s -> System.out.println(s.getName() + ": " + s.getScore()));
		System.out.println();
	}

}

 


17.8 요소를 하나씩 처리 (루핑)

루핑은 스트림에서 요소를 하나씩 반복해서 가져와 처리하는 것을 말한다. 루핑 메소드에는 peek()와 forEach()가 있다.

peek()과 forEach()는 동일하게 요소를 루핑하지만 peek()은 중간 처리 메소드이고, forEach()는 최종 처리 메소드이다.

따라서 peek()은 최종 처리가 뒤에 붙지 않으면 동작하지 않는다.

매개타입인 Consumer는 함수형 인터페이스로, 다음과 같은 종류가 있다.

모든 Consumer는 매개값을 처리(소비)하는 accept() 메소드를 가지고 있다.

Consumer<? super T>를 람다식으로 표현하면 다음과 같다.

다음은 정수 짝수 스트림에서 요소를 하나씩 반복해서 출력시키는 예제이다.

package ch17.sec08;

import java.util.*;

public class LoopingExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] intArr = { 1, 2, 3, 4, 5 };

		// 최종 처리가 없으므로 동작하지 않는다.
		Arrays.stream(intArr).filter(a -> a % 2 == 0).peek(n -> System.out.println(n));

		// 중간 처림 메소드 peek()를 이용해서 반복 처리 sum()이 최종처리이기 때문에 동작한다.
		int total = Arrays.stream(intArr).filter(a -> a % 2 == 0).peek(n -> System.out.println(n)).sum();
		System.out.println("총합 :" + total);

		System.out.println();

		// 최종 처리 메소드 forEach()를 이용해서 반복 처리
		Arrays.stream(intArr).filter(a -> a % 2 == 0).forEach(n -> System.out.println(n));

	}

}

 


17.9 요소 조건 만족 여부(매칭)

매칭은 요소들이 특정 조건에 만족하는지 여부를 조사하는 최종 처리 기능이다. 매칭과 관련된 메소드는 다음과 같다.

allMatch(), anyMatch(), noneMatch() 메소드는 매개값으로 주어진 Predicate가 리턴하는 값에 따라 true 또는 false를 리턴한다. (Predicate에 대한 설명은 17.5절 참고) 예를 들어 allMatch()는 모든 요소의 Predicate가 true를 리턴해야만 true를 리턴한다. 

 

다음 예제는 정수 스트림에서 모든 요소가 2의 배수인지, 하나라도 3의 배수가 존재하는지, 또는 모든 요소가 3의 배수가 아닌지를 조사한다.

package ch17.sec09;

import java.util.*;

public class MatchingExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		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);
	}

}


17.10 요소 기본 집계

집계는 최종 처리 기능으로 요소들을 처리해서 카운팅, 합계, 평균값, 최대값, 최소값등과 같이 하나의 값으로 산출하는 것을 말한다. 즉, 대량의 데이터를 가공해서 하나의 값으로 축소하는 리덕션이라고 볼 수 있다.

 

스트림이 제공하는 기본 집계

 

스트림은 카운팅, 최대, 최소, 평균, 합계 등을 처리하는 다음과 같은 최종 처리 메소드를 제공한다.

집계 메소드가 리턴하는 OptionalXXX는 Optional, OptionalDouble, OptionalInt, OptionalLong 클래스를 말한다.

이들은 최종값을 저장하는 객체로 get(), getAsDouble(), getAsInt(), getAsLong()을 호출하면 최종값을 얻을 수 있다.

package ch17.sec10;

import java.util.*;

public class AggregateExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] arr = { 1, 2, 3, 4, 5 };

		// 카운팅
		long count = Arrays.stream(arr).filter(n -> n % 2 == 0).count();
		System.out.println("2의 배수 개수: " + count);

		// 총합
		long sum = Arrays.stream(arr).filter(n -> n % 2 == 0).sum();
		System.out.println("2의 배수의 합 " + sum);

		// 평균
		double avg = Arrays.stream(arr).filter(n -> n % 2 == 0).average().getAsDouble();
		System.out.println("2의 배수의 평균 " + avg);

		// 최대값
		int max = Arrays.stream(arr).filter(n -> n % 2 == 0).max().getAsInt();
		System.out.println("최대값 " + max);

		// 최대값
		int min = Arrays.stream(arr).filter(n -> n % 2 == 0).min().getAsInt();
		System.out.println("최소값 " + min);

		// 첫 번째 요소
		int first = Arrays.stream(arr).filter(n -> n % 3 == 0).findFirst().getAsInt();
		System.out.println("첫 번째 3의 배수 " + first);
	}

}

 

Optional 클래스

 

Optional, OptionalDouble, OptionalInt, OptionalLong 클래스는 단순히 집계값만 저장하는 것이 아니라, 집계값이 존재하지 않을 경우 디폴트 값을 설정하거나 집계값을 처리하는 Consumer를 등록할 수도 있다. 다음은 Optional 클래스가 제공하는 메소드이다.

컬렉션의 요소는 동적으로 추가되는 경우가 많다. 만약 컬렉션에 요소가 존재하지 않으면 집계 값을 산출할 수 없으므로 NoSuchElementException 예외가 발생한다. 하지만 앞의 표에 언급되어 있는 메소드를 이용하면 예외 발생을 막을 수 있다.

 

예를 들어 평균을 구하는 average를 최종 처리에서 사용할 경우, 다음과 같이 3가지 방법으로 요소(집계값)가 없는 경우를 대비할 수 있다.

 

1) isPresent() 메소드가 true를 리턴할 때만 집계값을 얻는다.

2) orElse() 메소드로 집계값이 없을 경우를 대비해서 디폴트 값을 정해놓는다.

3) ifPresent() 메소드로 집계값이 있을 경우에만 동작하는 Consumer 람다식을 제공한다.

package ch17.sec10;

import java.util.*;

public class OptionalExample {

	public static void main(String[] args) {
		List<Integer> list = new ArrayList<>();
		// 방법1
		OptionalDouble optional = list.stream().mapToInt(a -> a.getInteger(null)).average();
		if (optional.isPresent()) {
			System.out.println("방법1_평균: " + optional.getAsDouble());
		} else {
			System.out.println("방법1_평균: 0.0");
		}

		// 방법2
		double avg = list.stream().mapToInt(a -> a.getInteger(null)).average().orElse(0.0);
		System.out.println("방법2_평균: " + avg);

		// 방법3
		list.stream().mapToInt(a -> a.getInteger(null)).average().ifPresent(a -> System.out.println("방법3_평균: " + a));

	}

}

 



17.12 요소 수집

스트림은 요소들을 필터링 또는 매핑한 후 요소들을 수집하는 최종 처리 메소드인 collect()를 제공한다. 이 메소드를 이용하면 필요한 요소만 컬렉션에 담을 수 있고, 요소들을 그룹핑한 후에 집계도 할 수 있다.

 

필터링한 요소 수집

 

Stream의 collect (Collector<T,A,R> collector) 메소드는 필터링 또는 매핑된 요소들을 새로운 컬렉션에 수집하고, 이 컬렉션을 리턴한다. 매개값인 Collector는 어떤 요소를 어떤 컬렉션에 수집할 것인지를 결정한다.

타입 파라미터의 T는 요소, A는 누적기(accumulator), 그리고 R은 요소가 저장될 컬렉션이다. 풀어서 해석하면 T 요소를 A 누적기가 R에 저장한다는 의미이다.

 

Collector의 구현 객체는 다음과 같이 Collectors 클래스의 정적 메소드로 얻을 수 있다.

toList() 메서드는 요소들을 List에 저장한다는 메서드다.

 

리턴값인 Collector를 보면 A(누적기)가 ? 로 되어 있는데, 이것은 Collector가 List, Set, Map 컬렉션에 요소를 저장하는 방법을 알고 있어 별도의 누적기가 필요 없기 때문이다.

 

다음은 Student 스트림에서 남학생만 필터링해서 별도의 List로 생성하는 코드이다.

collect 메서드로 스트림으로 변환한 List를 다시 List로 반환

 

다음은 Student 스트림에서 이름을 키로, 점수를 값으로 갖는 Map 컬렉션을 생성하는 코드이다.

Java 16부터는 좀 더 편리하게 요소 스트림에서 List 컬렉션을 얻을 수 있다. 스트림에서 바로 toList() 메소드를 다음과 같이 사용하면 된다.

package ch17.sec12.exam01;

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

public class CollectExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		List<Student> totalList = new ArrayList<>();
		totalList.add(new Student("홍길동", "남", 92));
		totalList.add(new Student("김수영", "여", 87));
		totalList.add(new Student("김자바", "남", 95));
		totalList.add(new Student("오해영", "여", 93));

		// 남학생만 묶어 List 생성
		List<Student> maleList1 = totalList.stream().filter(s -> s.getSex().equals("남")).collect(Collectors.toList());

		List<Student> maleList2 = totalList.stream().filter(s -> s.getSex().equals("남")).toList();

		maleList1.stream().forEach(s -> System.out.println(s.getName()));

		System.out.println();

		maleList2.stream().forEach(s -> System.out.println(s.getName()));

		System.out.println();

		Map<String, Integer> map = totalList.stream().collect(Collectors.toMap(s -> s.getName(), s -> s.getScore()));

		System.out.println(map);
	}

}

 

 

'자바 > 이것이 자바다' 카테고리의 다른 글

19. 네트워크 입출력  (1) 2024.11.26
18  (0) 2024.11.25
16. 람다식  (1) 2023.11.20
15. 컬렉션 자료구조  (0) 2023.11.18
14. 멀티 스레드  (1) 2023.11.17