스트림이란?
- 배열, 컬렉션의 저장 요소를 하나씩 참조하여 람다식으로 처리할 수 있도록 해주는 반복자이다.
- 스트림을 사용하면 List, Set, Map 배열 등 다양한 데이터 소스로부터 스트림을 만들 수 있고, 이를 표준화된 방법으로 다룰 수 있다.
- 스트림은 데이터 소스를 다루는 풍부한 메서드를 제공한다.
스트림의 특징
선언형으로 데이터 소스를 처리한다.
- 스트림으로 이용하면, 선언형으로 데이터 소스를 처리할 수 있다.
- 선언형 프로그램이란?, 어떻게 수행하는지보다 무엇을 수행하는 지에 관심을 두는 프로그래밍 패러다임입니다.
import java.util.List;
public class ImperativeProgrammingExample {
public static void main(String[] args){
// List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
int sum = 0;
for(int number : numbers){
if(number > 4 && (number % 2 == 0)){
sum += number;
}
}
System.out.println("# 명령형 프로그래밍 : " + sum);
}
}
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);
}
}
- 비교해보면 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
*/
내부 반복자를 사용하므로 병렬 처리가 쉽다.

- 외부 반복자(external iterator)란 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 말한다. Ex). for문, Iterator를 이용하는 while문
- 내부 반복자(internal iterator)는 컬렉션 내부에서 요소들을 반복시키고 개발자는 요소당 처리해야할 코드만 제공하는 코드 패턴을 말한다.
- 내부 반복자를 사용해서 얻는 이점은 컬렉션 내부에서 어떻게 요소를 반복시킬 것인가는 컬렉션에게 맡겨두고, 개발자는 요소 처리 코드에만 집중할 수 있다.
- 내부 반복자는 요소들의 순서를 변경하거나 멀티 코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있게 도와주기 때문에 하나씩 처리하는 순차적 외부 반복자보다 효율적으로 요소를 반복시킬 수 있다.
- 병렬 스트림을 사용하기 위해서는 스트림의
parallel()메서드를 사용하면 된다.
중간 연산과 최종 연산을 할 수 있다.
- 중간 연산에서는 매핑, 필터링, 정렬 등을 수행하고 최종 연산에서는 반복, 카운팅, 평균, 종합 등의 집계를 수행할 수 있다.
파이프라인 구성(.)
- 대량의 데이터를 가공해서 축소하는 것으 일반적으로 리덕션(Reduction이라고 한다.
- 컬렌션의 요소를 리덕션의 결과물로 바로 집계할 수 없을때에는 필터, 매핑, 정렬, 그룹화 등의 중간 연산이 필요하다.
파이프 라인
- 파이프 라인은 여러개의 스트림이 연결되어 있는 구조를 의미한다. 파이프 라인에서는 최종 연산을 제외하고는 모두 중간 연산 스트림이다.

스트림 생성, 중간 연산, 최종 연산
- 스트림은 데이터를 연속적으로 전달하는 통로로 이해할 수 있다.
스트림 생성
- 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을 생성하기 위해서는 Stream의 of 메서드 또는 Arrays의 stream 메서드를 사용한다.
// 배열로부터 스트림을 생성
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 범위 미포함
// 4이상 10미만의 숫자를 갖는 IntStream
IntStream stream = IntStream.range(4, 10);
- 스트림은 주로 컬렉션과 배열에서 얻지만, 다음과 같은 소스로부터 스트림 구현 객체를 얻을 수도 있다.

- 스트림을 사용할 때 주의할 점!
- 스트림의 데이터 소스로부터 데이터를 읽기만 할 뿐 변경하지 않는다.(Read-Only)
- 스트림은 일회용이다. 한 번 사용하면 닫히므로, 필요하다면 새로운 스트림을 만들어야 한다.
중간 연산
- 중간 연산은 연산 결과를 스트림으로 반환하기 때문에, 연속해서 여러 번 수행할 수 있다.
- 필터링(filter(), distinct())
filter() : Stream에서 조건에 맞는 데이터만을 정제해서 더 작은 컬렉션을 만든다. filter() 메서드에는 매개값으로 조건이 주어지고, 조건(Predicate)이 참이되는 요소만 필터링한다.
distinct() : Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 사용한다.
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()은 기존의 Stream 요소들을 대체하는 요소로 구성된 새로운 Stream을 형성하는 연산이다.
- 저장된 값을 특정한 형태로 반환하는데 주로 사용 된다. Java에서는 map 함수의 인자로 함수형 인터페이스 function을 받고 있다.
- map()이외에도 mapToInt(), mapToLong(), mapToDouble() 등의 메서드가 있다.
flatmap()은 요소를 대체하는 복수 개의 요소들로 구성된 새로운 스트림을 리턴한다.
List<String> names = Arrays.asList("kimcoding", "javalee", "ingikim", "kimcoding");
names.stream()
.map(s -> s.toUpperCase())
.forEach(n->System.out.println(n));
- 정렬(sorted())
- Stream의 요소들을 정렬하기 위해서는 sorted를 사용해야 하며, 파리미터로 Comparator를 넘길 수도 있다.(Comparator 인자 없이 호출하면 오름차순 정렬되며, 내림차순으로 정렬하기 위해서는 reverseOrder를 이용해야한다.)
List<String> list = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");
list.stream()
.sorted()
.forEach(n -> System.out.println(n));
System.out.println();
/*
Go, Groovy, Java, Python, Scala, Swift
*/
list.stream()
.sorted(Comparator.reverseOrder())
.forEach(n -> System.out.println(n));
/*
Swift, Scala, Python, Java, Groovy, Go
*/
최종 연산
- 최종 연산은 연산 결과가 스트림이 아니므로, 한 번만 연산이 가능하다.
- 연산 결과 확인(forEach())
- 파이프라인 마지막에서 요소를 하나씩 연산합니다. forEach값을 출력할 때도 사용하지만, 이메일 발송, 스케줄링 등 리턴 값이 없는 작업에서도 많이 사용한다.
intStream
.filter(a -> a%2 ==0)
.forEach(n -> System.out.println(n));
- 매칭(match())
- 특정한 조건을 충족하는지 검사하고 싶은 경우에 사용한다. 검사 결과를 boolean으로 반환한다.
- allMatch() : 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하는지 조사한다.
- anyMatch() : 최소한 한 개의 요소가 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
- 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 메서드는 최대 3개의 매개변수를 받을 수 있다.
- Accumulate : 각 요소를 계산한 중간 결과를 생성하기 위해 사용
- Identity : 계산을 수행하기 위한 초기값
- 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 등 다른 종류의 결과로 수집하고 싶은 경우에 사용한다.
- 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()));
}
}
Optinal<T>
- Optional은 NullPointerException(NPE), 즉 null 값으로 인해 에러가 발생하는 현상을 객체 차원에서 효율적으로 방지하고자 도입되었다.
- 연산 결과를 Optional에 담아서 반환하면, 따로 조건문을 작성해주지 않아도 NPE가 발생하지 않도록 코드를 작성할 수 있다.
Optional
- Optional 클래스는 모든 타입의 객체를 담을 수 있는 래퍼(Wrapper) 클래스이다.
public final class Optional<T> {
private final T value; // T타입의 참조변수
}
// Optional 객체를 생성하려면 of() 또는 ofNullable()을 사용한다. 참조 변수의 값이 null일 가능성이 있다면, ofNullable()을 사용한다.
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);
// Optional 객체는 스트림과 유사하게 여러 메서드를 연결해서 작성할 수 있다.(이를 메서드 체이닝이라 한다.)
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class OptionalExample {
public static void main(String[] args) {
List<String> languages = Arrays.asList(
"Ruby", "Python", "Java", "Go", "Kotlin");
Optional<List<String>> listOptional = Optional.of(languages);
int size = listOptional
.map(List::size)
.orElse(0);
System.out.println(size);
}
}
- 다음에 볼떄는 꼭 심화학습 연습 문제 풀어보기!