Java Category/Java

[Java] 스트림(스트림 개념, 스트림 얻기, 필터링, 매핑)

ReBugs 2023. 8. 6.

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 


스트림이란

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

스트림은 요소들이 하나씩 흘러가면서 처리된다는 의미를 가지고 있다.

 

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

- 내부 반복자이므로 처리 속도가 빠르고 병렬 처리에 효율적이다. (내부에서 멀티 스레딩으로 처리)
- 람다식으로 다양한 요소 처리를 정의할 수 있다.
- 중간 처리와 최종 처리를 수행하도록 파이프 라인을 형성할 수 있다.(필터링 후 원하는 데이터만 추출 및 가공)

 

List 컬렉션의 stream() 메소드로 Stream 객체를 얻고, forEach() 메소드로 요소를 어떻게 처리할지를 람다식으로 제공한다.

import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;

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

		//Stream을 이용한 요소 반복 처리
		Stream<String> stream = set.stream();
		stream.forEach( name -> System.out.println(name) ); //forEach()에 람다식 제공
	}
}
/*
홍길동
신용권
감자바
*/

 


 

내부 반복자

  • 외부 반복자 : for문과 Iterator는 컬렉션의 요소를 컬렉션 바깥쪽으로 반복해서 가져와 처리한다.
  • 내부 반복자 : 스트림은 요소 처리 방법을 컬렉션 내부로 주입 시켜서 요소를 반복 처리한다.

출처 : 이것이 자바다 유튜브 동영상 강의

내부 반복자는 멀티 코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있다.

하나씩 처리하는 순차적 외부 반복자보다는 효율적으로 요소를 반복시킬 수 있는 장점이 있다.

 

내부 반복자는 멀티 스레딩 환경에서도 공유 자원을 안전하게 처리한다.
예를 들어, 공유 객체가 1000개이고 내부 반복자가 스레드를 2개 생성했다면, 각각 500개씩 분담 해서 처리한다. 

 

아래의 예제는 List 컬렉션의 내부 반복자를 이용해서 병렬 처리하는 방법을 보여준다.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.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()); //처리하는 스레드 이름 출력
		} );
	}
}
/*
감자바: main
박병렬: main
람다식: main
홍길동: ForkJoinPool.commonPool-worker-2
신용권: ForkJoinPool.commonPool-worker-1
*/

 

 


 

중간 처리와 최종 처리

 

스트림은 하나 이상 연결될 수 있다.

아래의 그림을 보면 컬렉션의 오리지널 스트림 뒤에 필터링 중간 스트림이 연결 되고, 그 뒤에 매핑 중간 스트림이 연결된다.

이와 같이 스트림이 연결되어 있는 것을 스트림 파이프 라인이라고 한다.

출처 : 이것이 자바다 유튜브 동영상 강의

  • 중간 처리 과정 : 최종 처리를 위해 요소를 필터링 하거나 요소를 변환(매핑), 정렬 하는 작업을 수행
  • 최종 처리 과정 : 중간 처리에서 정제된 요소들을 반복하거나, 집계(카운팅, 총합, 평균 등) 작업을 한다.

 

아래의 그림은 이름과 점수를 가지는 Studet 객체 스트림에서 중간 처리를 통해 score 스트림으로 변환한 후 최종 집계 처리로 score 평균을 구하는 과정을 나타낸다.

출처 : 이것이 자바다 유튜브 동영상 강의

//방법 1
Stream<Student> studentStream = list.stream();

//중간 처리(학생 객체를 점수로 매핑)
IntStream scoreStream = studentStream.mapToInt(student -> student.getScore());

//최종 처리(평균 점수)
double avg = scoreStream.average().getAsDouble();

 

//방법 2 (메소드 체이닝)
double avg = list.stream()
            .mapToInt(student -> student.getScore()) //매핑
            .average() //평균(최종 처리)
            .getAsDouble(); //더블 타입으로 변환 (최종 처리)

 

파이프라인의 맨 끝에는 반드시 최종 처리 부분이 있어야 한다.
최종 처리가 없다면 오리지널 및 중간 처리 스트림은 동작하지 않는다.
최종 처리가 된 스트림은 재사용이 안된다. 따라서 해당 스트림에 추가 작업이 필요하면 스트림을 새로 생성해야 한다.

 

자세한 예제는 아래의 더보기를 클릭하면 볼 수 있다.

더보기
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.IntStream;
import java.util.stream.Stream;

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

		//방법1
 		Stream<Student> studentStream = list.stream();
 		//중간 처리(학생 객체를 점수로 매핑)
 		IntStream scoreStream = studentStream.mapToInt(student -> student.getScore());
		//최종 처리(평균 점수)
 		double avg = scoreStream.average().getAsDouble();
 		System.out.println("평균 점수: " + avg);

		//방법2
		avg = list.stream()
				.mapToInt(student -> student.getScore()) //중간 처리(학생 객체를 점수로 매핑)
				.average() //최종 처리(평균 점수)
				.getAsDouble();
		System.out.println("평균 점수: " + avg);
		
	}
}
/*
평균 점수: 20.0
평균 점수: 20.0
*/

 

 


 

리소스로부터 스트림 얻기

java.util.stream 패키지에는 스트림 인터페이스들이 있다.

BaseStream 인터페이스를 부모로 한 자식 인터페이스들은 아래와 같은 상속 관계를 이루고 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

이 스트림 인터페이스들의 구현 객체는 다양한 리소스로부터 얻을 수 있다.

주로 컬렉션과 배열에서 얻지만, 아래와 같은 리소스로부터 스트림 구현 객체를 얻을 수도 있다.

 

출처 : 이것이 자바다 유튜브 동영상 강의

 

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

java.util.Collection 인터페이스는 스트림과 parallelStream() 메소드를 가지고 있기 때문에 자식 인터페이스인 List와 Set 인터페이스를 구현한 모든 컬렉션에서 객체 스트림을 얻을 수 있다.

 

Product.java

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

 

StreamExample.java

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {
	public static void main(String[] args) {
		//List 컬렉션 생성
		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));
	}
}
/*
{pno:1, name:상품1, company:멋진회사, price:3764}
{pno:2, name:상품2, company:멋진회사, price:5358}
{pno:3, name:상품3, company:멋진회사, price:5351}
{pno:4, name:상품4, company:멋진회사, price:8885}
{pno:5, name:상품5, company:멋진회사, price:6178}
*/

 


배열로부터 스트림 얻기

java.util.Arrays 클래스를 이용하면 다양한 종류의 배열로부터 스트림을 얻을 수 있다.

 

StreamExample.java

import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class StreamExample {
	public static void main(String[] args) {
		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();
	}
}
/*
홍길동,신용권,김미나,
1,2,3,4,5,
*/

 


 

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

IntStream 또는 LongStream의 정적 메소드인 range()와 rangeClosed() 메소드를 이용하면 특정 범위의 정수 스트림을 얻을 수 있다.

첫 번째 매개값은 시작 수이고 두 번째 매개값은 끝 수인데, 끝 수를 포함하지 않으면 range(), 포함하면 rangeClosed()를 사용한다.

StreamExample.java

import java.util.stream.IntStream;

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);
	}
}
//총합: 5050

 


 

파일로부터 스트림 얻기

java.nio.file.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}

 

StreamExample.java

import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class StreamExample {
	public static void main(String[] args) throws Exception {
		Path path = Paths.get(StreamExample.class.getResource("data.txt").toURI()); //StreamExample 클래스 기준으로 data.txt 상대경로를 얻고 URI 객체를 리턴
		Stream<String> stream = Files.lines(path, Charset.defaultCharset()); //스트림을 얻음, 문자셋 지정
		stream.forEach(line -> System.out.println(line) );
		stream.close();
	}
}
/*
{"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}
*/
URI와 URL
URI (Uniform Resource Identifier) : 리소스 “자원 자체”를 식별하는 고유한 문자열 시퀀스
URL (Uniform Resource Locator) : 자원(리소스)의 “위치” 를 나타내기 위한 규약

 


 

요소 걸러내기 (필터링)

필터링은 요소를 걸러내는 중간 처리 기능이다.

필터링은 요소를 걸러내는 중간 처리 기능이다.

필터링 메소드에는 disinct()와 filter()가 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

disinct()

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

출처 : 이것이 자바다 유튜브 동영상 강의

 

filter()

  • 매개값으로 주어진 Predicate가 false를 리턴하는 요소는 스트림에서 제거한다.

출처 : 이것이 자바다 유튜브 동영상 강의

  • Predicate는 함수형 인터페이스로, 아래와 같은 종류가 있다.

출처 : 이것이 자바다 유튜브 동영상 강의

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

출처 : 이것이 자바다 유튜브 동영상 강의

Predicate<T>를 람다식으로 표현하면 아래와 같다.

T -> {...return true}
또는
T -> true; // 리턴문만 있을 경우 다른거 다 생략 가능

 

사용 예제

FilteringExample.java

import java.util.ArrayList;
import java.util.List;

public class FilteringExample {
	public static void main(String[] args) {
		//List 컬렉션 생성
		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));		
	}
}
/*
홍길동
신용권
감자바
신민철

신용권
신용권
신민철

신용권
신민철
*/

 

요소 변환 (매핑)

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

 

요소를 다른 요소로 변환

mapXxx() 메소드는 요소를 다른 요소로 변환한 새로운 스트림을 리턴한다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

mapXxx() 메소드의 종류는 아래와 같다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

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

출처 : 이것이 자바다 유튜브 동영상 강의

 

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

출처 : 이것이 자바다 유튜브 동영상 강의

 

Function <T, R>을 람다식으로 표현하면 아래와 같다.

T -> {...return R}
또는
T -> R; // 리턴문만 있을 경우 다른거 다 생략 가능

 

사용 예제

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

 

MapExample.java

import java.util.ArrayList;
import java.util.List;

public class MapExample {
	public static void main(String[] args) {
		//List 컬렉션 생성
		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));
	}
}
/*
85
92
87
*/

 

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

출처 : 이것이 자바다 유튜브 동영상 강의

 

사용 예제

MapExample.java

import java.util.Arrays;
import java.util.stream.IntStream;

public class MapExample {
	public static void main(String[] args) {
		int[] intArray = { 1, 2, 3, 4, 5};
		
		IntStream intStream = Arrays.stream(intArray);
		intStream
			.asDoubleStream()
			.forEach(d -> System.out.println(d));
			
		System.out.println();
		
		intStream = Arrays.stream(intArray);
		intStream
			.boxed()
			.forEach(obj -> System.out.println(obj.intValue()));
	}
}
/*
1.0
2.0
3.0
4.0
5.0

1
2
3
4
5
*/

 


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

flatMapXxx() 메소드는 하나의 요소를 복수 개의 요소들로 변환한 새로운 스트림을 리턴한다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

flatMap() 메소드의 종류는 아래와 같다.

출처 : 이것이 자바다 유튜브 동영상 강의

 

사용예제

FlatMappingExample.java

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

public class FlatMappingExample {
	public static void main(String[] args) {
		//문장 스트림을 단어 스트림으로 변환
		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));
	}
}
/*
this
is
java
i
am
a
best
developer

10
20
30
40
50
*/

댓글