Stream 이 왜 사용되는가 ?
자바8 에서 추가됬으며 람다를 활용할 수 있는 기술 중 하나입니다.
자바8 이전에는 배열 또는 컬렉션을 다룰려면 for/foreach 문을 사용하는 방법이 었습니다.
이 방법은 단점이 있습니다.
1) 복잡해질 수록 로직이 섞입니다.
2) 코드의 양이 많아집니다.
3) 루프가 여러번 도는 현상이 발생할 수 있습니다.
그래서 이를 보완하고자 등장한 것이 스트림입니다.
Stream
- 스트림은 '데이터의 흐름' 입니다.
1) 배열 또는 인스턴스에 함수 여러 개를 조합
2) 원하는 결과에 대해 필터링, 매핑해 가공된 결과를 얻음
전체 -> 매핑 -> 필터링1 -> 필터링2 -> 결과 생성 -> 결과물
- 스트림은 람다를 이용할 수 있습니다.
1) 코드의 양을 줄일 수 있음
2) 배열과 컬렉션을 함수형으로 처리 가능
- 병럴처리 가능
1) 하나의 작업을 둘 이상 잘게 나눠 동시에 진행 가능
2) 즉, 쓰레드를 활용해 많은 요소를 빠르게 처리 가능
Stream 의 생성
스트림은 배열과 컬렉션 인스턴스를 이용해서 생성할 수 있습니다.
// 1. 배열로 스트림 생성
String[] arr = new Stringp[]{"a","b","c"};
Arrays.stream(arr);
// 2. 컬렉션으로 스트림 생성
List<String> list = Arrays.asList("a","b","c");
Stream<String> stream = list.stream();
// 3. StreamBuilder 로 생성
Stream<String> streamBuilder = Stream.<String>builder().add("sungjun").add("jun").build(); //[ sungjun, jun ]
// 4. Stream.generate() 로 생성
Stream<String> generatedStream = Stream.generate(()->"gen").limit(5) // [ "gen","gen","gen","gen","gen" ]
- Stream.generate()
제너레이트 메서드를 사용하면 람다로 값을 넣을 수 있습니다.
Stream.iterate()
iterate 메서드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해 스트림에 들어갈 요소를 만듭니다.
예제에서는 30 이라는 초기값을 기준으로 2씩 증가하는 값들이 들어갑니다.
Stream<Integer> iteratedStream = Stream.iterate(30,n->n+2).limit(5);
[ 30,32,34,36,38 ]
기본 타입형 스트림
제네릭 ( Stream<String> ) 을 사용하지 않고 기본 (int, double, long) 스트림을 생성할 수 있습니다.
IntStream intStream = IntStream.range(1,5) [1,2,3,4]
// 두번째 인자는 종료지점에 포함 안됩니다.
이를 활용한 랜덤 난수 생성
DoubleStream doubles = new Random().doubles(3); // 랜덤 난수 3개 생성
Stream 의 다양한 예제
- 문자열 스트링을 숫자로 변환
IntStream charStream = "Stream".chars(); // [ 83,116,114,101,97,109 ]
- 파일 스트림
Files 클래스의 lines 메소드는 해당 파일의 각 라인을 스트링 타입의 스트림으로 만들어줍니다.
Stream<String> lineStream = Files.lines(Paths.get("file.txt"), Charset.forName("UTF-8"));
- Stream 연결하기
Stream<String> stream1 = Stream.of("a","b","c");
Stream<String> stream2 = Stream.of("d","e","f");
stream<String> concat = Stream.concat(stream1,stream2);
가공하기 ( 중간 작업 )
전체 요소 중에서 API 를 활용해 내가 원하는 것만 뽑아내는 과정입니다.
가공 단계를 중간 작업이라고 하는데, 이러한 작업은 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서 작성할 수 있습니다.
List<String> names = Arrays.asList("sun","mon","jun");
//Filtering
Stream<String> FilteringName = names.stream().filter(name->name.contains("s"); // [sun]
//Mapping
String<String> MappingName = names.strea().map(String::toUpperCase);
- Filtering
스트림 내에 요소들을 하나씩 평가해서 걸러내는 작업
- Mapping
스트림 내에 요소들을 특정 값으로 변환해줍니다. 이때 값을 변환하기 위한 람다를 인자로 받습니다.
IntStream.of(10,20,50,40,30).sorted().collect(Collectors.toList());
// [10,20,30,40,50]
IntStream.of(10,20,50,40,30).sorted(Comparator.reverseOrder()).collect(Collectors.toList());
// [50,40,30,20,10]
List<String> lan = Arrays.asList("seoulpower","geoje","busanwon","bew")
lan.stream().sorted(Comparator.comparingInt(String::length)).collect(Collectors.toList());
// [ bew, geoje,busanwon ... ]
lan.stream().sorted(s1,s2->s2.length()-s1.length()).collect(Collectors.toList());
// [ seoulpower, busanwon, ... ]
- sorted : 스트림 내에 요소를 오름차순으로 정렬
- collect(Collectors.변환될 타입) : 스트림으로 반환하는 요소를 해당 컬렉션으로 바꿔서 리턴해줍니다.
- sorted(Comparator.reverseOrder()) : 스트림 내에 요소를 내림차순으로 정렬
- sorted(Comparator.comparingInt(String::length)) : 스트림 내에 요소를 문자열 길이 기준으로 정렬
결과 만들기
가공한 스트림을 가지고 내가 사용할 결과 값을 만들어내는 단계입니다. 따라서 최종 작업입니다.
long count = IntStream.of(1,2,3,4,5).count();
long count = IntStream.of(1,2,3,4,5).sum();
long count = IntStream.of(1,2,3,4,5).min();
long count = IntStream.of(1,2,3,4,5).max();
최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어 낼 수 있습니다.
* 이때, [ 평균/최소/최대 ] 의 경우에는 스트림이 비어있는 경우를 표현할 수 없기때문에 Optional 을 이용해 리턴합니다.
이 외에,
Reduction, Collecting, Matching, foreach
에 대한 개념이 있지만 현재 쓸 일이 없어서 기억을 못할 것 같으므로 다음에 사용할 일 있을 때
다시 따로 블로깅 하도록 하겠습니다.
조건검사
- findAny, findFirst()
filter() 연산과 함께 사용됩니다. 조건식에 맞는 요소가 있는지 확인할 때 사용됩니다.
반환 타입이 Optional<T> 이며, 스트림의 요소가 없을 때는 비어 있는 Optional 객체를 반환합니다.
- allMatch() : 조건식에 모든 요소가 일치하면 true
- anyMath() : 어떤 요소 하나라도 조건에 일치하면 true
- noneMatch() : 조건식에 모든 요소들이 일치하지많으면 true
[ 소스코드 예제 ]
현재 제가 진행중인 프로젝트에서 사용된 스트림에 대해서 알아보겠습니다.
private static Map<Long, Member> store = new HashMap<>();
public Optional<Member> findByname(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
1. store.values () 의 반환 타입
: Collection<Member>
즉, values 는 값을 컬렉션 타입으로 반환 해줍니다.
2. .stream()
: 컬렉션으로 반환된 values 를 stream 으로 만들어줍니다.
3. .filter( member -> member.getName().equals(name))
: store 내부의 member 인자에서 getName() 을 통해서 name 값을 받아옵니다.
받아온 name 과 findByname 의 인자로 주어진 name 을 비교해줍니다.
4. .findAny()
: filter 한 조건식에 맞는 요소가 있으면 해당 요소를 모두 반환해줍니다. ( 첫번째 하나만 하려면 findFirst )
만약 findAny() 를 붙치지 않는다면 Stream() 을 그대로 반환하는 모습을 볼 수 있습니다.