현재 공부하고 있는 도서의 Stream API 에 대한 자세한 설명이 나와 있어서, 같은 주제로 계속 블로깅을 하고 있습니다. 


하지만 대략적인 부분이 끝이 난 것 같습니다. Stream API 소개는 이번 포스팅이 마지막입니다.

지난 Stream API 가 궁금하다면, 하단 글들을 참고해주세요. :-)



오늘 포스팅은 보통 전공 과목에서 흔히 말하는 기타 기능에 대해 알아보려고 합니다. 그러나 유용할 수 있죠? :-)


1. 숫자형 스트림으로 변환


람다를 포스팅할 때도 있었지만, 스트림 역시 원시타입의 스트림을 지원합니다. 보다 더 정확히 말하면, 숫자들에 대한 스트림을 지원하며 이 기본형 특화 스트림들은 sum, max, min 등 자주 사용하는 리듀싱 메소드를 제공해줍니다.


숫자 스트림을 사용하기 위해서는 map 의 파생 메소드인 mapToInt, mapToDouble, mapToLong 등의 메소드를 사용해야 합니다.


예제부터 한번 봐볼까요?


1
2
3
4
5
6
7
8
9
List<Integer> numberList = Arrays.asList(1,2,3,4,5,6);
    
System.out.println(
        numberList.
            stream().
            mapToInt(Integer::intValue).
            sum());
 
// 출력 21
cs


간단한 Integer Collection 객체의 stream 에서 int value 의 stream 얻기 위해 mapToInt 메소드를 사용하였으며(언박싱), 리듀스 메소드 중 하나인 sum 을 사용하였습니다.


추가적으로 또다른 리듀싱 메소드인 max나 min 은 Optional 상태로 데이터가 제공됩니다. 컬렉션이 비어있는 상태에서 max 나 min 의 값이 무조건 존재한다는 것을 보장할 수 없기 때문이죠.



2. 숫자 범위 스트림


JAVA8 에서는 숫자형스트림에서 특정 범위안의 숫자 집합을 출력해주는 정적메소드인 rangerangeClosed 를 제공해줍니다. 두 메소드 모두 범위에 대한 인수를 받으며, 차이는 시작값과 종료값이 결과에 포함하는 여부입니다. (range 의 경우 결과에 포함되지 않습니다.)


사용예제는 다음과 같습니다. 아래 예제는 숫자 Stream 을 이용하여, 피타고라스의 수를 구하는 쿼리입니다.


1
2
3
4
5
6
7
IntStream.rangeClosed(1,100).
        boxed().
        flatMap(a -> IntStream.
                        rangeClosed(a, 100).
                        mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a *+ b * b)})).
        filter(v -> v[2]%1 == 0).
        forEach(v -> System.out.println(v[0+ " " + v[1+ " " + v[2]));
cs


flatMap 을 통해, 두 개의 숫자 스트림을 엮었습니다. 첫번째 범위에서는 Stream 연산의 결과를 객체로 받기 위해 boxed 메소드를 사용하였습니다. boxed 객체를 사용하지 않으면, 오직 중간연산 결과로 숫자타입의 스트림밖에 생산할 수 없습니다. (왜냐하면 IntStream 은 숫자 스트림이기 때문이죠. ㅡㅡ^)



3. 스트림 만들기


컬렉션이나 배열은 다음과 같이 임의의 값으로 데이터를 만들 수 있습니다.


1
2
String[] dataArray = new String[]{"남두현""윤석진""성지윤""오진명"}; 
List<String> dataList = Arrays.asList(dataArray);
cs


스트림 역시 임의의 값으로 스트림 만들기가 가능합니다. 꼭 컬렉션 클래스의 stream 키워드를 쓸 필요는 없는 것이죠. ㅡㅡ^


1
2
3
4
String[] dataArray = new String[]{"남두현""윤석진""성지윤""오진명"}; 
        
Stream<String> ofStream = Stream.of("남두현""윤석진""성지윤""오진명");
Stream<String> arrayStream = Arrays.stream(dataArray);
cs



4. 고정되지 않은 크기의 스트림 만들기


여태까지 Stream은 고정된 크기의 컬렉션을 통해서 만들곤 했습니다. 그러나 특정 Function, Supplier 타입의 메소드 레퍼런스 (람다 포함) 를 통해 무한한 Stream 을 만들 수 있습니다.


1
2
3
4
5
// iterate 는 Function type 람다를 사용 
Stream.iterate(0, n -> n + 2).forEach(System.out::println);
        
// generate 는 Supplier type 메소드 레퍼런스 사용
Stream.generate(Math::random).forEach(System.out::println);    
cs


만들 수 있는 key-word 메소드는 iterate 와 generate 로 보통 limit 로 제한을 두어 사용하곤 합니다. 제한을 두지 않은 스트림을 언바운드 스트림(Unbounded Stream) 이라 하며, 특정 로직에서 무한한 결과 때문에 제대로된 결과를 얻을 수 없을 지도 모릅니다. 

(그러나 위 예제 에서는 무한한 숫자가 출력이 될 것입니다. 컬렉션과 달리 Stream 은 즉시 생산을 하는 것이 특징이기 때문이죠. ㅡㅡ^)


것으로 Stream API 활용과 관련된 주제는 끝났습니다.


다음 포스팅 부터는 Stream 을 통한 데이터 수집과 관련된 주제로 진행을 하게 될 것 같습니다. 

(스포일러를 하자면, 언제나 최종 연산을 구할 때 사용했던 Collector 와 관련된 이야기 입니다. ^^)



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기


반응형
Posted by N'

 지금까지는 Stream API 의 최종 연산에서 여러 종류의 값 (Boolean, filter 된 리스트, void 연산) 을 반환을 함을 알 수 있었습니다. 여러 중간 연산을 pipe 처럼 묶어서 사용하고, 그 결과를 취함으로 여러 로직의 이점을 많이 챙길 수 있었습니다.


오늘의 포스팅 주제는 조금 더 복잡한 질의를 표현할 수 있는 리듀싱(reducing)에 대해 알아보려고 합니다. 예를 들어, 컬렉션 내부에 대해 총합을 구한다거나 최대값, 최소값을 구하는 연산이 대표적이라고 할 수 있습니다. ㅡㅡ^


 간단한 예제 부터 한번 봐보도록 할까요? 아래 코드는 컬렉션 내부의 값 중, 짝수 만을 덧셈하는 연산입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
List<Integer> numberList = Arrays.asList(5103692);
        
int sum = 0;
        
for (Integer number : numberList) {
    if (number % 2 == 0) {
        sum += number;
    }
}
 
System.out.println("총 합 : " + sum);
 
// 총 합 : 108
cs


 이 간단한 코드는 Stream 연산을 통해 더욱 간단해질 수 있습니다. 약간의 중간연산과 리듀싱을 통해 처리할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
List<Integer> numberList = Arrays.asList(5103692);
 
System.out.println("총 합 : " + 
            numberList.
                stream().
                filter(n -> n % 2 == 0).
                reduce(0, (a, b) -> a + b)
            );
 
//총 합 : 108
cs


 filter 를 통해 짝수만 추출했으며, reduce 를 이용해 덧셈의 결과를 구했습니다.


위의 예제에서 사용한 reduce 는 초기 값을 0 을 했음을 알 수 있습니다. 물론, 초기값이 없는 연산을 수행할 수 있습니다. 이 때 결과는 Optional 객체로 나타나게 되며 해당 객체를 이용해 값이 있을 때만 처리하는 기능 등을 수행할 수 있습니다.


리듀싱의 응용은 다양하게 할 수 있습니다. 예를들어 특정 객체에 대한 컬렉션 중, 한 필드에 대한 합은 다음과 같이 구할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * Test 를 위한  VO
 * 
 * @author namduhyeon
 *
 */
class TestVo {
    private String name;
    private Integer value;
    
    public TestVo (String name, Integer value) {
        this.name = name;
        this.value = value;
    }
 
    public String getName() {
        return name;
    }
 
    public Integer getValue() {
        return value;
    }
}
 
List<TestVo> numberList = Arrays.asList(
                new TestVo("Doohyun Nam",7)
                , new TestVo("Ford Mill",9)
                , new TestVo("Hwang",2));
        
numberList.stream().
    map(v -> v.getValue()).
    reduce((v1, v2) -> v1 + v2).
    ifPresent(System.out::println);
 
// 18
cs


특정 필드를 추출하기 위해 map 을 사용하였으며, 이 Stream 을 통해 합을 구할 수 있었습니다. 


이러한 map 과 reduce 를 이용하여, 데이터를 가공하는 패턴을 map-reduce 패턴이라 하며 웹 검색 엔진 등에서 많이 사용하는 것을 볼 수 있습니다.


그 이유는 언제나 Stream API 에서 쉽게 볼 수 있는 병렬성이라는 특징 덕분입니다. 예를 들어 특정 컬렉션의 합을 병렬적으로 구하기 위해서는 임계영역간 스레드의 경쟁 비용이 발생하기 마련이지만, 리듀스 내부의 내부반복은 개발자에게는 추상화되어 제공됩니다. (분할하여 결과를 구하고 머징하는 과정이 아닐지...) 


하지만, 모든 기능이 만병통치약일 수는 없습니다. ㅡㅡ^ 

병렬스트림으로 리듀싱 결과를 일반 스트림과 동일하게 얻기 위해서는 연산이 어떤 순서로 연산되어도 결과가 보장되는 구조이며, 람다 내부 인스턴스는 변경되어서는 안되는 구조이어야 합니다. 즉 각 트랜잭션 중 병렬구조로 처리가 가능한 부분만 사용이 가능함을 알 수 있습니다.


복잡한 데이터의 질의를 쉽게 표현할 수 있는 리듀싱에 대해 알아보았습니다. 생각보다 자주 접해보면, 금방 눈에 들어오지 않을까 생각이 듭니다. 


점점 함수형 프로그래밍의 장점이 눈에 들어오지 않으시나요? :-) 


자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기




반응형
Posted by N'

지난 포스트에 이어서, 계속 Stream API에 대한 포스팅을 하고 있습니다.


지난 포스팅에서는 각 컨테이너를 매핑하여, 원하는 형태의 객체로 출력을 할 수 있는 방법을 소개했었습니다. 

마치 SQL 처럼 특정 필드만을 추출한다던가, 두 개 이상의 컨테이너를 JOIN 할 수도 있었죠.


해당 포스팅은 이 곳에서 확인!! 



오늘은 특정 속성이 데이터 집합에 있는지, 있다면 해당 객체를 추출할 수 있는 다양한 유틸 메소드를 살펴볼까 합니다.


예를 들어, 우리는 이러한 작업을 많이 하곤 합니다.


아래 로은 특정 숫자 컬렉션에서 5의 배수가 있는지 확인하는 로직입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<Integer> sampleList = Arrays.asList(5,10,12,38);
        
Boolean isHave = false;
for (Integer number : sampleList) {
    if (number % 5 == 0) {        
        // 5의 배수인 데이터가 있는지 확인
        isHave = true;
        break;
    }
}
        
System.out.println("5의 배수가 존재  : " + isHave);
 
// 출력 : 5의 배수가 존재  : true
cs


루프를 돌면서, 출력할 컬렉션 객체에 if 조건을 사용하여 여부를 업데이트하려 하고 있습니다.


해당 로직은 다음과 같이 수정될 수 있습니다.



1. Predicate 가 적어도 한 요소와 일치하는지 확인 (anyMatch)


anyMatch 유틸 메소드로 다음과 같은 로직을 구할 수 있습니다.


1
2
3
4
5
List<Integer> sampleList = Arrays.asList(5,10,12,38);
        
System.out.println("5의 배수가 존재  : " + sampleList.stream().anyMatch(n -> n % 5 == 0));
 
// 출력 : 5의 배수가 존재  : true
cs


단순히 predicate 형태로 컬렉션 제네릭 객체의 조건만 명시해주면 조금 더 간소화된 코드를 구현할 수 있습니다.


2. Predicate 를 이용한 기타 확인 (allMatch, noneMatch)


Predicate 를 이용하여 컬렉션의 모든 요소가 일치하는지를 확인하려면, allMatch 메소드를 사용할 수 있습니다. 또한 반대로 일치하는 요소가 없는지를 확인하는 기능 역시도 noneMatch 메소드를 사용함으로써 간단하게 구현할 수 있습니다.


1
2
3
4
5
6
7
8
9
List<Integer> sampleList = Arrays.asList(5,10,15,40);
        
System.out.println("모든 요소가 5의 배수인가? : " + sampleList.stream().allMatch(n -> n%5 == 0));
System.out.println("5의 배수는 없는가? : " + sampleList.stream().noneMatch(n -> n % 5 == 0));
 
// 출력
// 모든 요소가 5의 배수인가? : true
// 5의 배수는 없는가? : false
 
cs


3. 요소 검색


Stream API 를 사용하면 쉽게 한 요소에 접근할 수 있습니다. 일련의 중간 연산(filter, distinct 등)을 수행한 후 findAny, findFirst 등의 최종연산 메소드로 조건에 맞는 한 요소를 선택할 수 있습니다. 예제를 한번 볼까요? 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Integer> sampleList = Arrays.asList(17,19,28,37,16);
        
sampleList.
    stream().
    filter(n -> n % 2 == 0).
    findAny().
    ifPresent(n -> System.out.println("2의 배소의 요소 찾기 : " + n));
        
sampleList.
    stream().
    filter(n -> n % 2 == 0).
    findFirst().
    ifPresent(n -> System.out.println("2의 배소의 첫번째 요소 찾기 : " + n));
 
// 출력
// 2의 배소의 요소 찾기 : 28
// 2의 배소의 첫번째 요소 찾기 : 28
cs


두 메소드의 내용을 살펴보면, 동일함을 알 수 있습니다. 왜냐하면, 쇼트서킷 검사(보통 Boolean 연산 특정 조건에 의해 검사결과가 정해지면 추 후 연산은 안하는 행위)를 통해 결과를 찾은 뒤 즉시 실행을 하기 때문이죠.


그렇다면 왜 굳이 메소드를 두 개로 둔 이유가 무엇일까요?


그 이유는 공짜로 얻을 수 있는 병렬성 이 후 후처리 때문입니다. Collection 클래스에서 Stream 추출 시, parallelStream 을 통해 병렬 처리를 쉽게 수행할 수 있습니다. 그러나 병렬처리를 수행 시 컬렉션의 순서가 보장이 되지 않습니다! ㅡㅡ^


즉 병렬처리를 통해 데이터 연산을 수행하였다면, 명시적으로 첫 번째 요소를 찾기 위해 findFirst 를 사용하여야만 합니다! 


아래 예제를 통해 결과를 확인해볼까요? 아래 예제는 병렬처리를 했음에도 findFirst 사용 시, 첫 번째 요소찾기를 보장하고 있음을 보여주고 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Integer> sampleList = Arrays.asList(17,19,28,37,16);
        
sampleList.
    parallelStream().
    filter(n -> n % 2 == 0).
    findAny().
    ifPresent(n -> System.out.println("2의 배소의 요소 찾기 : " + n));
        
sampleList.
    parallelStream().
    filter(n -> n % 2 == 0).
    findFirst().
    ifPresent(n -> System.out.println("2의 배소의 첫번째 요소 찾기 : " + n));
 
// 출력
// 2의 배소의 요소 찾기 : 16
// 2의 배소의 첫번째 요소 찾기 : 28

cs



마지막으로 생각해봐야할 부분은 요소 검사 메소드 활용 시, 결과가 항상 있다는 것을 보장하지 않는다는 점입니다. 보통 이럴 때 기존 자바에서는 null 로써, 데이터를 처리했지만 JAVA8 부터 많은 함수형 프로그래밍에서 볼 수 있는 Optional 의 개념이 생겼습니다. 


Optional값의 존재나 부재 여부를 표현하는 컨테이너 클래스로, 예제에서 사용한 ifPresent 는 값이 존재할 때만 실행을 명령하는 메소드 입니다. (Optional 은 추 후 따로 포스팅을 한 후 연결을 시켜두겠습니다. ㅡㅡ^)



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기



반응형
Posted by N'