자바

16.1 스트림 소개

스트림(Stream) 은 자바 8부터 추가된 컬렉션(배열 포함) 의 저장 요소를 하나씩 참조해서 람다식(함수적-스타일(functional-style) 으로 처리할 수 있도록 해주는 반복자이다.

16.1.1 반복자 스트림

자바 7 이전까지는 List 컬렉션에서 요소를 순차적으로 처리하기 위해 Iterator 반복자를 다음과 같이 사용해왔다.

List list =Arrays.asList("홍길동", "신용권", "감자바");

Iterator iterator=list.iterator();

while(iterator.hasNext()){

  String name =iterator.next();

 System.out.println(name);

}

이 코드를 Stream 을 사용해서 변경하면 다음과 같다.

public class Liste {

    public static void main(String[] args) {
        
        
        List list=Arrays.asList("홍길동", "신용권", "감자바");
        Stream stream =list.stream();
        stream.forEach(name ->System.out.println(name));
        
        
    }
    
}
홍길동
신용권
감자바

컬렉션(java.util.Collection)의 stream() 메소드로 스트림 객체를 얻고 나서 stream.forEach(name ->System.out.println(name)); 메소드를 통해 컬렉션의 요소를 하나씩 콘솔에 출력한다. forEach() 메소드는 다음과 같이 Consumer 함수적 인터페이스 타입의 매개값을 가지므로 컬렉션의 요소를 소비할 코드를 람다식으로 기술할 수 있다.

void forEach(Consumer action)

Iterator 를 사용한 코드와 Stream 을 사용한 코드를 비교해보면 Stream 을 사용하는 것이 훨씬 단순해 보인다. 다음 예제는 List 컬렉션의 String 요소를 Iterator 와 Stream 을 이용해서 순차적으로 콘솔에 출력한다.

public class IteratorVsStreamExample {

    public static void main(String[] args) {
        
        List list =Arrays.asList("홍길동", "최준호", "감자바");
        
        //Iterator 이용
        Iterator iterator =list.iterator();
        while(iterator.hasNext()){
            String name =iterator.next();
            System.out.println(name);
        }
        
        System.out.println();
        
        //Stream 이용
        Stream stream=list.stream();
        stream.forEach(name ->System.out.println(name));
        
    }
    
}

홍길동
최준호
감자바

홍길동
최준호
감자바
 

16.1.2 스트림의 특징

Stream은 Iterator 와 비슷한 역할을 하는 반복자이지만, 람다식으로 요소 처리 코드를 제공하는 점과 내부 반복자를 사용하므로 병렬 처리가 쉽다는 점 그리고 중간 처리와 최종 처리 작업을 수행하는 점에서 많은 차이를 가지고 있다. 이 특징에 대해서 자세히 살펴보기로 하자.

 

람다식으로 요소 처리 코드를 제공한다.

Stream이 제공하는 대부분의 요소 처리 메소드는 함수적 인터페이스 매개 타입을 가지기 때문에 람다식 또는 메소드 참조를 이용해서 요소 처리 내용을 매개값으로 전달할 수 있다. 다음 예제는 컬렉션에 저장된 Student 를 하나씩 가져와 학생 이름과 성적을 콘솔에 출력하도록 forEach() 메소드이 매개값으로 람식을 주었다.

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

 

public class LambdaExpressionsExample {

    public static void main(String[] args) {
        
        List list =Arrays.asList(
                new Student("홍길동", 90),
                new Student("최준호", 92)
        );
        
        Stream stream=list.stream();
        stream.forEach(a ->{
            
            String name =a.getName();
            int score =a.getScore();
            System.out.println(name +  " - "  +score);
        });
        
    }
}
 

 

내부 반복자를 사용하므로 병렬 처리가 쉽다.

외부 반복자(external iterator)란 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 말한다. index 를 이용하는 for문 그리고 Iterator를 이용하는 while 문은 모두 외부 반복자를 이용하는 것이다. 반면에 내부 반복자(internal iterator)는 컬렉션 내부에서 요소들을 반복시키고, 개발자는 요소당 처리해야 할 코드만 제공하는 코드 패턴을 말한다. 

내부 반복자를 사용해서 얻는 이점은 컬렉션 내부에서 어떻게 요소를 반복시킬 것인가는 컬렉션에게 맡겨두고, 개발자는 요소 처리 코드에만 집중할 수 있다는 것이다. 내부 반복자는 요소들의 반복 순서를 변경하거나, 멀티 코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있게 도와주기 때문에 하나씩 처리하는 순차적 외부 반복자보다는 효율적으로 요소를 반복시킬 수 있다.

Iterator는 컬렉션의 요소를 가져오는 것에서부터 처리하는 것까지 모두 개발자가 작성해야 하지만, 스트림은 람다식으로 요소 처리 내용만 전달할 뿐, 반복은 컬렉션 내부에서 일어난다. 스트림을 이용하면 코드도 간결해지지만, 무엇보다도 요소의 병렬 처리가 컬렉션 내부에서 처리되므로 일석이조의 효과를 가져온다.

병렬(parallel) 처리한 한 가지 작업을 서브 작업으로 나누고, 서브 작업들을 분리된 스레드에서 병렬적으로 처리하는 것을 말한다. 병렬 처리 스트림을 이용하면 런타임 시 하나의 작업을 서브 작업으로 자동으로 나누고, 서브 작업의 결과를 자동으로 결합해서 최종 결과물을 생성한다. 예를 들어 컬렉션의 요소 총합을 구할 때 순차 처리  스트림은 하나의 스레드가 요소들을 순차적으로 읽어 합을 구하지만, 병렬 처리 스트림을 이용하면 여러 개의 스레드가 요소들을 부분적으로 합하고 이 부분합을 최종 결합해서 전체 합을 생성한다. 다음 예제는 순차 처리 스트림과 병렬 처리 스트림을 이용할 경우, 사용된 스레드의 이름이 무엇인지 콘솔에 출력한다. 실행 결과를 보면 병렬 처리 스트림은 main 스레드를 포함해서 ForkJoinPool(스레드풀)의 작업 스레드들이 병렬적으로 요소를 처리하는 것을 볼 수 있다.

public class ParallelExample {

    public static void main(String[] args) {
        
        List list =Arrays.asList(
                "홍길동", "최준호", "감자바","람다식", "박병렬"
                );
        
        //순차 처리
        Stream stream =list.stream();
        stream.forEach(ParallelExample :: print);
        
        System.out.println();
        
        //병렬 처리
        Stream parallelStream =list.parallelStream();
        parallelStream.forEach(ParallelExample :: print);
        
    }
    
    public static void print(String str){
        System.out.println(str + " : " + Thread.currentThread().getName());
    }
    

}

홍길동 : main
최준호 : main
감자바 : main
람다식 : main
박병렬 : main

감자바 : main
박병렬 : ForkJoinPool.commonPool-worker-2
홍길동 : main
최준호 : ForkJoinPool.commonPool-worker-1
람다식 : ForkJoinPool.commonPool-worker-2

 

스트림은 중간 처리와 최종 처리를 할 수 있다.

스트림은 컬렉션의 요소에 대해 중간 처리와 최종 처리를 수행할 수 있는데, 중간 처리에서는 매핑, 필터링, 정렬을 수행하고 최종 처리에서는 반복, 카운팅, 평균, 총합 등의 집계 처리를 수행한다.

예를 들어 학생 객체를 요소로 가지는 컬렉션이 있다고 가정해보자. 중간 처리에서는 학생의 점수를 뽑아내고, 최종 처리에서는 점수의 평균값을 산출한다.

다음 예제는 List에 저장되어 있는 Student 객체를 중간 처리에서 score 필드값으로 매핑하고, 최종 처리에서 score 의 평균값을 산출한다.

 

public class MapAndReduceExample {

    public static void main(String[] args) {
        
        List studentsList =Arrays.asList(
                    new Student("홍길동", 10),
                    new Student("최준호", 20),
                    new Student("유미선", 30)
                );
        
        double avg =studentsList.stream()
                .mapToInt(Student :: getScore)
                .average()
                .getAsDouble();
        
        System.out.println("평균 점수 : " + avg);
    }
    
}
 

평균 점수 : 20.0

 

16.2 스트림의 종류

자바 8부터 새로 추가된 java.util.stream 패키지에는 스트림(stream) API 들이 포진하고 있다.

패키지 내용을 보면 BaseStream 인터페이스를 부모로 해서 자식 인터페이스들이 다음과 같은 상속 관계를 이루고 있다.

BaseStream 인터페이스에는 모든 스트림에서 사용할 수 있는 공통 메소드들이 정의되어 있을 뿐 코드에서 직접적으로 사용되지는 않는다. 하위 스트림인 Stream, IntStream, LongStream, DoubleStream 이 직접적으로 이용되는 스트림인데, Stream은 객체 요소를 처리하는 스트림이고, IntStream, LongStream, DoubleStream 은 각각 기본 타입인 int, long, double 요소를 처리하는 스트림이다. 이 스트림 인터페이스의 구현 객체는 다양한 소스로부터 얻을 수 있다. 주로 컬렉션과 배열에서 얻지만, 다음과 같은 소스로부터 스트림 구현 객체를 얻을 수도 있다.

리턴타입 메소드(매개 변수) 소스
Stream

java.util.Collection.stream()

java.util.Collection.parallelSteam()

컬렉션

Stream

IntStream

LongStream

DoubleStream

Arrays.stream(T[]), Stream.of(T[])

Arrays.stream(int[]), IntStream.of(int[])

Arrays.stream(long[]), LongStream.of(long[])

Arrays.stream(double[]), DoubleStream.of(double[])

배열
IntStream

IntStream.range(int, int)

IntStream.rangeClosed(int , int)

int 범위
LongStream

LongStream.range(int, int)

LongStream.rangeClosed(long, long)

long 범위
Stream

Files.find(Path, int, BiPredicate, FileVisitOpiton)

Files.list(Path)

디렉토리
Stream

Files.lines(Path, Charset)

BufferedReader.lines()

파일

DoubleStream

IntStream

LongStream

Random.doubles()

Random.ints()

Random.longs()

fo

 

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

다음 예제는 List 컬렉션에서 Stream 를 얻어내고 요소를 콘솔에 출력한다.

public class FromCollectionExample {
    
    public static void main(String[] args) {
        
        List studentList =Arrays.asList(
                new Student("홍길동", 10),
                new Student("최준호", 20),
                new Student("유미선", 30)
                );
        
        Stream stream =studentList.stream();
        stream.forEach(s->System.out.println(s.getName()));
    }
}
 

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

홍길동
최준호
유미선
 

16.2.2 배열로부터 스트림 얻기

다음 예제는 String[] 과 int[] 배열로부터 스트림을 얻어내고 콘솔에 출력한다.

public class FromArrayExample {

    public static void main(String[] args) {
        
        String[] strArray ={"홍길동", "최준호", "김미나"};
        Stream stream =Arrays.stream(strArray);
        stream.forEach(a->System.out.println(a + ","));
        System.out.println();
        
        int[] intArray ={1,2, 3, 4, 5};
        IntStream intStream =Arrays.stream(intArray);
        intStream.forEach(a->System.out.println(a + ","));
        System.out.println();
        
    }
    
}
 

홍길동,
최준호,
김미나,

1,
2,
3,
4,
5,
 

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

다음은 1부터 100까지의 합을 구하기 위해 IntStream 의 rangeClosed() 메소드를 이용하였다. rangeClosed() 는 첫 번째 매개값에서부터 두 번째 매개값까지 순차적으로 제공하는 InstStream을 리턴한다. InstStream 의 또 다른 range() 메소드도 동일한 IntStream 을 리턴하는데, 두 번째 매개값은 포함하지 않는다.

 

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

총합 :5050
 

16.2.4 파일로부터 스트림 얻기

다음 예제는 Files의 정적 메소드인 lines() 와 BufferedReader의 lines() 메소드를 이용하여 문자 파일의 내용을 스트림을 통해 행 단위로 읽고 콘솔에 출력한다.

public class FromFileContentExample {
    public static void main(String[] args) throws IOException{
        Path path =Paths.get("linedat.txt");
        Stream stream;
        
        //Files.lines()메소드 이용
        stream =Files.lines(path, Charset.defaultCharset());
        stream.forEach(System.out :: println);
        System.out.println();
        
        //BufferedReader 의 lines() 메소드 이용
        File file =path.toFile();
        FileReader fileReader =new FileReader(file);
        BufferedReader br =new BufferedReader(fileReader);
        stream =br.lines();
        stream.forEach(System.out :: println);
        
        
    }
}
 

16.2.5 디렉토리로부터 스트림 얻기

다음 예제는 Files 의 정적 메소드인 list() 를 이용해서 디렉토리의 내용(서브 디렉토리 또는 파일 목록)을 스트림을 통해 읽고 콘솔에 출력한다.

 

public class FromDirectoryExample {
    public static void main(String[] args) throws IOException{
        Path path =Paths.get("C:/javawork/java의정석/ThisIsJava/Chap06/src");
        Stream steam =Files.list(path);
        steam.forEach(p ->System.out.println(p.getFileName()));
        
    }
}
 

ch06
ch07
ch08
ch09
ch10
ch11
ch12
ch13
ch14
ch15
ch16
ch17
ch18
ch19
ch20
ch21
ch22
database.properties
 

16.3 스트림 파이프라인

대량의 데이터를 가공해서 축소하는 것을 일반적으로 리덕션(Reduction) 이라고 하는데, 데이터의 합계, 평균값, 카운팅, 최대값, 최소값 등이 대표적인 리덕션의 결과물이라고 볼 수 있다. 그러나 컬렉션의 요소를 리덕션의 결과물로 바로 집계할  수 없을 경우에는 집계하기 좋도록 필터링, 매핑, 정렬, 그룹핑 등의 중간 처리가 필요하다.

 

16.3.1 중간 처리와 최종 처리

스트림은 데이터의 필터링, 매핑, 정렬, 그룹핑 등의 중간 처리와 합계, 평균, 카운팅, 최대값, 최소값 등의 최종 처리를 파이프라인(pipelines)으로 해결한다. 파이브파인은 여러 개의 스트림이 연결되어 있는 구조를 말한다. 파이프라인에서 최종 처리를 제외하고는 모두 중간 처리 스트림이다.

중간 스트림이 생성될 때 요소들이 바로 중간 처리(필터링, 매핑,정렬) 되는 것이 아니라 최종 처리가 시작되기 전까지 중간 처리는 지연(lazy) 된다. 

최종 처리가 시작되면 비로소 컬렉션의 요소가 하나씩 중간 스트림에서 처리되고 최종 처리까지 오게 된다.

Stream 인터페이스에는 필터링, 매핑, 정렬 등의 많은 중간 처리 메소드가 있는데, 이 메소드들은 중간 처리된 스트림을 리턴한다. 그리고 이 스트림에서 다시 중간 처리 메소드를 호출해서 파이프라인을 형성하게 된다. 예를 들어 회원 컬렉션에서 남자만 필터링하는 중간 스트림을 연결하고, 다시 남자의 나이로 매핑하는 스트림을 연결한 후, 최종 남자 평균 나이를 집계한다면 다음 그림처럼 파이프라인이 형성된다.

파이프라인을 자바 코드로 표현하면 다음과 같다.

Stream maleFemaleStream =list.stream();

Stream maleStream =maleFeamleStream.filter(m ->m.getSex() == Member.MALE);

IntStream ageStream  =maleStream.mapToInt(Member :: getAge);

OptionalDouble optionalDouble =ageStream.average();

double ageAvg =optionalDouble.getAsDouble();

 

로컬 변수를 생략하고 연결하면 다음과 같은 형태의 파이프라인 코드만 남는다.

double ageAvg =list.stream()

.filter(m ->m.getSex() == Member.MALE)

.mapToInt(Member :: getAge)

.average()

.getAsDouble();

 

filter(m -> m.getSex() == Member.MALE) 는 남자 Member 객체를 요소로 하는 새로운 스트림을 생성한다. mapToInt(Member :: getAge()) 는 Member 객체를 age 값으로 매핑해서 age 를 요소로 하는 새로운 스트림을 생성한다.  average() 메소드는 age 요소들의 평균을 OptionalDouble에 저장한다. OpitonalDouble 에서 저장된 평균값을 읽으려면 getAsDouble() 메소드를 호출하면 된다.

 

public class MaleStream {
    
    public static void main(String[] args) {
        
        List list=Arrays.asList(
                    new Member("홍길동", Member.MALE, 30),
                    new Member("김나리", Member.FEMALE, 20),
                    new Member("최준호", Member.MALE, 45),
                    new Member("박수미", Member.FEMALE, 27)
                );
        double ageAvg =list.stream()
                .filter(m->m.getSex() ==Member.MALE)
                .mapToInt(Member :: getAge)
                .average()
                .getAsDouble();
        System.out.println("남자 평균 나이 : " + ageAvg);
    }
    
}

class Member{
    public static int MALE =0;
    public static int FEMALE =1;
    
    private String name;
    private int sex;
    private int age;
    
    public Member(String name, int sex, int age){
        this.name=name;
        this.sex=sex;
        this.age=age;
    }
    
    public int getSex() {
        return sex;
    }
    
    public int getAge() {
        return age;
    }
    
}
 

 

16.4 필터링(distinct(), filter())

필터링은 중간 처리 기능으로 요소를 걸러내는 역할을 한다. 필터링 메소드인 distinct() 와 filter() 메소드는 모든 스트림이 가지고 있는 공통 메소드이다.

리턴타입 메소드(매개변수) 설명
Stream distinct() 중복제거
IntStream filter(Predicate) 조건 필터링
LongStream filter(IntPredicate)  
DoubleStream filter(LongPredicate)  
  filter(DoublePredicate)  

 

 

distinct() 메소드는 중복을 제거하는데, Stream 의 경우 Object.equals(Object)가 true이면 동일한 객체로 판단하고 중복을 제거한다. 

IntStream, LongStream, DoubleStream 은 동일값일 경우 중복을 제거한다.

 

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

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

 

public class FilteringExample {
    public static void main(String[] args) {
        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));
    
    }
}

홍길동
최준호
감자바
최준민
신민철

신민철

최준호
최준민

 

16.5 매핑 (flatMapxxx(), mapXXX(), asXXXStream(),boxed())

매핑(mapping) 은 중간 처리 기능으로 스트림의 요소를 다른 요소로 대체하는 작업을 말한다. 스트림에서 제공하는 매핑 메소드는 flatXXX()와 

mapXXX(), 그리고 asDoubleStream(), asLongStream(), boxed() 가 있다.

 

16.5.1 flatMapXXX() 메소드

flatMapXXX() 메소드는 요소를 대체하는 복수 개의 요소들로 구성된 새로운 스트림을 리턴한다.

다음 그림을 보면서 이해해보자. 스트림에서 A라는 요소는 A1, A2 요소는 대체되고, B라는 요소는 B1, B2로 대체된다고 가정했을 경우, A1, A2, B1,B2요소를 가지는 새로운 스트림이 생성된다.

 

리턴타입 메소드(매개변수) 요소->대체요소
Stream<R> flatMap(Function<T, Stream<R>>) T->Stream<R>
DoubleStream flatMap(DoubleFunction<DoubleStream>) double->DoubleStream
IntStream flatMap(IntFunction<IntStream>) int->IntStream
LongStream flatMap(LongFunction<LongStream>) long->LongStream
DoubleStream flatMapToDouble(Function<T, DoubleStream>) T->DoubleStream
IntStream flatMapToInt(Function<T, IntStream>) T->InputStream
LongStream flatMapToLong(Function<T, LongStream>) T->LongStream

 

다음 예제는 입력된 데이터(요소)들이 List<String> 에 저장되어 있다고 가정하고, 요소별로 단어를 뽑아 단어 스트림으로 재생성한다. 만약 입력되 데이터들이 숫자라면 숫자를 뽑아 숫자 스트림으로 재생성한다.

public class FlatMapExample {

    public static void main(String[] args) {
        
        List<String> inputList1 =Arrays.asList("java8 lambda", "stream mapping");
        inputList1.stream()
        .flatMap(data->Arrays.stream(data.split(" ")))
        .forEach(word->System.out.println(word));
        
        System.out.println();
        
        List<String> inputList2 =Arrays.asList("10, 20, 30", "40, 50, 60");
        inputList2.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));
    }
}

 

java8
lambda
stream
mapping

10
20
30
40
50
60

public class MapExample {
    public static void main(String[] args) {
        List<Student> studentList =Arrays.asList(
                new Student("홍길동", 10),
                new Student("최준호", 20),
                new Student("유미선", 30)
        );
        studentList.stream()
        .mapToInt(Student :: getScore)
        .forEach(score ->System.out.println(score));
    }
}

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

10
20
30
 

16.5.3 asDoubleStream(), asLongStream(), boxed() 메소드

asDoubleStream() 메소드는 IntStream 의 int 요소 또는 LongStream 의 long 요소를 double 요소로 타입 변환해서 DoubleStream 을 생성한다.

마찬가지로 asLongStream() 메소드는 IntStream 의 int 요소를 long 요소로 타입 변환해서 LongStream 을 생성한다. boxed() 메소드는 int, long, double 요소를 Integer, Long, Double 요소로 박싱해서 Stream 을 생성한다.

리턴타입 메소드(매개변수) 설명
DoubleStream asDoubleStream()

int ->double

long->double

LongStream asLongStream() int->long

Stream<Integer>

Stream<Long>

Stream<Double>

boxed()

int ->Integer

long->Long

double->Double

 

다음 예제는 int[] 배열로부터 IntStream 을 얻고 난 다음 int 요소를 double 요소로 타입 변환해서 DoubleStream 을 생성한다. 또는 int 요소를 Integer 객체로 박싱해서 Stream<Integer> 를 생성한다.

 

public class AsDoubleStreamAndBoxedExample {
    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
 

16.6 정렬(sorted())

스트림은 요소가 최종 처리되기 전에 중간 단계에서 요소를 정렬해서 최종 처리 순서를 변경할 수 있다.

요소를 정렬하는 메소드는 다음과 같다.

리턴타입 메소드(매개 변수) 설명
Stream<T> sorted() 객체를 Comparable 구현 방법에 따라 정렬
Stream<T> soreted(Comparator<T>) 객체를 주어진 Comparator에 따라 정렬
DoubleStream sorted() double 요소를 오름차순으로 정렬
IntStream  sorted() int 요소를 오름차순으로 정렬
LongStream sorted() long 요소를 오름차순으로 정렬

 

객체 요소일 경우에는 클래스가 Comparable 을 구현하지 않으면 sorted() 메소드를 호출했을 때 ClassCastException 이 발생하기 때문에 Comparable을 구현한 요소에서만 sorted() 메소드를 호출해야 한다. 다음은 점수를 기준으로 Student 요소를 오름차순으로 정렬하기 위해 Comparable을 구현했다.

 

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

객체 요소가 Comparable을 구현한 상태에서 기본 비교(Comparable) 방법으로 정렬하고 싶다면 다음 세 가지 방법 중 하나를 선택해서 sorted()  를 호출하면 된다.

sorted();

sorted((a,b) ->a.compareTo(b));

sorted(Comparator.naturalOrder());

만약 객체 요소가 Comparable을 구현하고 있지만, 기본 비교 방법과 정반대 방법으로 정렬하고 싶다면 다음과 같이 sorted()를 호출하면 된다.

sorted( (a, b) ->b.compareTo(a) );

sorted( Comparator.reverseOrder() );

 

객체 요소가 Comparable 를 구현하지 않았다면 Comparator를 매개값으로 갖는 sorted() 메소드를 사용하면 된다. Comparator 는 함수적 인터페이스이므로 다음과 같이 람다식으로 매개값을 작성할 수 있다.

 

soreted( (a,b) -> { ...})

중괄호 {} 안에는 a 와 b를 비교해서  a가 작으면 음수, 같으면 0, a 가 크면 양수를 리턴하는 코드를 작성하면 된다.

다음 예제를 보면 숫자 요소일 경우에는 오름차순으로 정렬한 후 출력했다. Student 요소일 경우는 Student 의 기본 비교(comparable) 방법을 잉요해서 점수를 기준으로 오름차순으로 정렬한 후 출력했다. 그리고 Comparator 를 제공해서 점수를 기준으로 내림차순으로 정렬한 후 출력했다.

 

public class SortingExample {
    public static void main(String[] args) {
        //숫자 요소일 경우
        IntStream intStream =Arrays.stream(new int[] {5, 3, 2, 1, 4});
        intStream
        .sorted()
        .forEach(n ->System.out.print(n + ","));
        System.out.println();
        
        //객체 요소일 경우
        List<Student> studentList =Arrays.asList(
                new Student("홍길동", 30),
                new Student("최준호", 10),
                new Student("유미선", 20)
        );

        studentList.stream()
        .sorted()
        .forEach(s ->System.out.print(s.getScore() + ","));
        System.out.println();
        
        studentList.stream()
        .sorted(Comparator.reverseOrder())
        .forEach(s ->System.out.print(s.getScore() + ","));
        
    }
}

 

1,2,3,4,5,
10,20,30,
30,20,10,

 

16.7 루핑(peek(), forEach() )

루핑(looping)은 요소 전체를 반복하는 것을 말한다. 루핑하는 메소드에는 peek(), forEach() 가 있다. 이 두 메소드는 루핑한다는 기능에서 동일하지만, 동작 방식은 다르다. peek() 는 중간 처리 메소드이고, forEach() 는 최종 처리 메소드이다.

peek()는 중간 처리 단계에서 전체 요소를 루핑하면서 추가적인 작업을 하기 위해 사용한다. 최종 처리 메소드가 실행되지 않으면 지연되기 때문에 반드시 최종 처리 메소드가 호출되어야 동작한다. 예를 들어 필터링 후 어떤 요소만 남았는지 확인하기 위해 다음과 같이 peek() 를 마지막에서 호출할 경우, 스트림은 전혀 동작하지 않는다.

intStream.filter(a ->a%2 ==0).peek(a -> System.out.println(a))

요소 처리의 최종 단계가 합을 구하는 것이라면, peek() 메소드 호출 후 sum() 을 호출해야만 peek() 가 정상적으로 동작한다.

 

intStream.filter(a -> a%2 ==0).peek(a ->System.out.println(a) ).sum()

 

하지만 forEach() 는 최종 처리 메소드이기 때문에 파이프라인 마지막에 루핑하면서 요소를 하나씩 처리한다. forEach() 는 요소를 소비하는 최종 처리 메소드이므로 이후에 sum() 과  같은 다른 최종 메소드를 호출하면 안 된다.

public class MatchExample {

    public static void main(String[] args) {
        
        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

 

16.9 기본 집계(sum(), count(), average(), max(), min() )

집계 (Aggregate) 는 최종 처리 기능으로 요소들을 처리해서 카우팅, 합계, 평균값, 최대값, 최소값 등과 같이 하나의 값으로 산출하는 것을 말한다.

집계는 대량의 데이터를 가공해서 축소하는 리덕션(Reduction) 이라고 볼 수 있다.

 

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

스트림은 다음과 같은 기본 집계 메소드를 제공하고 있다.

리턴타입 메소드(매개 변수) 설명
long count() 요소개수
OptionalXXX findFirst() 첫 번째 요소

Optional<T>

OptionalXXX

max(Comparator<T>)

max()

최대 요소

Optional<T>

OptionalXXX

 

min(Comparator<T>)

min()

최소요소
OptionalDouble average() 요소평균
int, long, double sum() 요소총합

이 집계 메소드에서 리턴하는 OptionalXXX 는 자바 8 에서 추가한 java.util 패키지의 Optional, OptionalDouble, OptionalInt, OptionalLong 클래스 타입을 말한다. 이들은 값을 저장 하는 값 기반 클래스(value -based class) 들이다. 이 객체에서 값을 얻기 위해서는 get(), getAsDouble(), getAsInt(), getAsLong() 을 호출하면 된다.

public class AggregateExample {

    public static void main(String[] args) {
        
        long count =Arrays.stream(new int[] {1,2, 3, 4, 5})
                .filter(n ->n%2 ==0)
                .count();
        System.out.println("2의 배수 개수 :" + count);
        
        long sum =Arrays.stream(new int[] {1, 2, 3, 4, 5})
                .filter(n ->n%2 ==0)
                .sum();
        System.out.println("2의 배수의 합 :" + sum);
        
        double avg =Arrays.stream(new int[] {1, 2, 3, 4, 5})
                .filter(n ->n%2 ==0)
                .average()
                .getAsDouble();
        System.out.println("2의 배수의 평균 :" + avg);
        
        int max =Arrays.stream(new int[] {1, 2, 3, 4, 5})
                .filter(n ->n%2 ==0)
                .max()
                .getAsInt();
        System.out.println("최대값 :" + max);
                
        
        int min =Arrays.stream(new int[] {1, 2, 3, 4, 5})
                .filter(n ->n%2 ==0)
                .min()
                .getAsInt();
        
        System.out.println("최소값: " + min );
        
        
        int first =Arrays.stream(new int[] {1, 2, 3,4,5 })
                .filter(n ->n%3 ==0)
                .findFirst()
                .getAsInt();
        System.out.println("첫번째 3의 배수 : " + first);
    }
}

 

2의 배수 개수 :2
2의 배수의 합 :6
2의 배수의 평균 :3.0
최대값 :4
최소값: 2
첫번째 3의 배수 : 3
 

List<Integer> list =new ArrayList<>();

double avg =list.stream()

.mapToInt(Integer :: intValue)

.average()

.getAsDouble();

System.out.println("평균 : " + avg);

요소가 없기 때문에 평균값도 있을 수 없다. 그래서 NoSuchElementException 예외가 발생한다. 요소가 없을 경우 예외를 피하는 세 가지 방법이 있는데, 첫 번째는 Optional 객체를 얻어 isPresent() 메소드로 평균값 여부를 확인하는 것이다. isPresent() 메소드가 true를 리턴할 때만 getAsDouble()메소드로 평균값을 얻으면 된다.

OptionalDouble optional  =list.stream()

.mapToInt(Integer :: intValue)

.average();

if(optional.isPresent()){

 System.out.println("평균 : " + optional.getAsDouble());

}else{

 System.out.println("평균 : 0.0 ");

}

두 번째 방법은 orElse() 메소드로 디폴트 값을 정해 놓는다. 평균값을 구할 수 없는 경우에 orElse() 의 개개값이 디폴트 값이 된다.

double avg =list.sream()

.mapToInt(Integer :: intValue)

.average()

.orElse(0.0);

System.out.println("평균 : " +avg );

세 번째 방법은 ifPresent() 메소드로 평균값이 있을 경우에만 값을 이용하는 람다식을 실행한다.

list.stream()

.mapToInt(Integer :: intValue)

.average()

.ifPresent(a ->System.out.println("평균 : "+ a));

public class OptionalExample {

    public static void main(String[] args) {
        
        List<Integer> list =new ArrayList<Integer>();
        
        //예외 발생(java.util.NoSuchElementException)
        /*double avg =list.stream()
                .mapToInt(Integer :: intValue)
                .average()
                .getAsDouble();
        
        */
        
        OptionalDouble optional =list.stream()
                .mapToInt(Integer :: intValue)
                .average();
        if(optional.isPresent()){
            System.out.println("방법1_평균 : "+ optional.getAsDouble());
        }else{
            System.out.println("방법1_평균 : 0.0");
        }

        //방법2
        double avg1 =list.stream()
                .mapToInt(Integer :: intValue)
                .average()
                .orElse(0.0);
        System.out.println("방법2_평균 : " + avg1);
        
        //방법3
        list.stream()
        .mapToInt(Integer :: intValue)
        .average()
        .ifPresent(a->System.out.println("방법3_평균 :" + a));
        
    }
    
}
방법1_평균 : 0.0
방법2_평균 : 0.0

 

 

 

 

 

 

 

about author

PHRASE

Level 60  머나먼나라

선인은 살기 위해서 먹고 마시는 반면, 악인은 먹고 마시기 위해서 산다. -소크라테스

댓글 ( 4)

댓글 남기기

작성

자바 목록    more