[Java] Stream API

programmeaow·2022년 6월 13일
0

Java

목록 보기
11/13

✔️ Stream이란?

컬렉션이나 배열 등에 저장되어 있는 데이터들을 하나씩 참조하여 반복적인 처리가 가능하게 도와주는 기능

Stream은 수도관에 물이 흐르는 것과 같이 데이터의 흐름을 뜻한다.
Java 8 버전 부터 지원하는 Stream의 구조는 Stream 생성 > 중간 연산 > 최종 연산 의 세가지로 나눌 수 있다.

아래의 코드를 통해 간단하게 구조를 파악해보자.

//예시 코드
List<String> name = studentList.stream().filter(x -> !x.isEmpty()).collect(Collectors.toList());

//구조 파악하기
List<String> name = studentList.stream(). 		 //Stream 생성
					filter(x -> x.length() > 3). //중간 연산
                    collect(Collectors.toList()); //최종 연산
  1. stream을 생성하고
  2. 필터를 이용해 조건에 따른 연산을 처리한 후
  3. 중간 연산으로 가공된 데이터를 모아준다.

❓ 공부하면서 생각난 의문점
: 왜 List 객체 에서 .stream 사용이 가능한가? 일반 String 객체 등은 불가능한가?

❗ 해답
: Collection 인터페이스에는 stream()이 정의되어 있기 때문에, Collection 인터페이스를 구현한 객체들(List, Set 등)은 모두 이 메소드를 이용해 Stream을 생성할 수 있다.

출처: [Java] Stream API의 활용 및 사용법 - 기초 (3/5) [MangKyu's Diary:티스토리]


Stream 은 다양한 filter 를 이용할 수 있다. filter 에 관해 알아보기 전에, Stream에서 사용할 수 있는 람다식과 이중 콜론에 관해 먼저 살펴보자.



- 람다(Lamda), 이중 콜론 ( :: )

람다식은 -> 으로, 이중 콜론은 :: 으로 사용할 수 있다.

- 람다식 기본 문법
변수 -> { 구현코드 };

- 이중 콜론 기본 문법
[인스턴스]::[메소드명 or (new)]

람다식과 이중 콜론의 활용법을 간단한 출력 예제를 통해 알아보자.
List 를 사용하여, [ 사과, 딸기, 바나나 ] 의 데이터를 넣은 뒤 순서대로 출력해보자.

public static void main(String[] args) {
	List<String> fruit = Arrays.asList("사과", "딸기", "바나나");
    
    for(String s : fruit) {
			System.out.println(s);
	}
}

//출력 결과
사과
딸기
바나나

위와 같이 for 반복문을 통해 List의 데이터를 출력할 수 있다.
이번에는 람다식을 이용해 코드를 작성해보았다.

public static void main(String[] args) {
	List<String> fruit = Arrays.asList("사과", "딸기", "바나나");
    
    fruit.forEach(s -> System.out.println(s));
}

for 반복문 대신 람다식을 사용하면 위와 같이 코드가 간결해진다.

같은 출력문을 이중 콜론을 활용하여 작성해보자.

public static void main(String[] args) {
	List<String> fruit = Arrays.asList("사과", "딸기", "바나나");
    
    fruit.forEach(System.out::println);
}

동일한 기능의 코드를 더욱 간결하게 작성이 가능하다.




📊 중간 연산(중개 연산)

parameter로 받은 데이터를 int 변환 : mapToInt
특정 구간까지 제외하고 출력 : skip
Stream에서 뽑아낸 데이터에 대해 특정 작업을 하겠다는 의미 : forEach
~ 값을 반환 : getAs~
ex ) int 값을 반환 : getAsInt

그 외에도 sum ,count 등의 다양한 기능 이용이 가능하다.



- Filter

filter()는 parameter 안에 조건을 입력하면 해당하는 stream의 데이터를 추출하는 기능을 가지고 있다.

아래의 숫자 데이터를 가지고 있는 List에 filter를 적용하여, 조건에 맞는 데이터를 출력해보자.

List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5);

홀수인 숫자의 합 출력

System.out.println(numberList.stream().filter(v -> v % 2 != 0).mapToInt(i -> i).sum());

//출력 : 9

짝수인 숫자의 합 출력

System.out.println(numberList.stream().filter(v -> v % 2 == 0).mapToInt(i -> i).sum());

//출력 : 6

홀수의 개수 출력

System.out.println(numberList.stream().filter(v -> v % 2 != 0).mapToInt(i -> i).count());

//출력 : 3

짝수 중 최대값 출력

System.out.println(numberList.stream().filter(v -> v % 2 == 0).mapToInt(i -> i).max().getAsInt());

//출력 : 4


- Skip

skip()은 parameter로 숫자를 받고, 해당 순서까지의 데이터를 건너뛰고 출력한다.

숫자 리스트를 2번째까지의 데이터를 제외하고 출력해보자.

List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5);
numberList.stream().skip(2).forEach(System.out::println);

//출력 : 3, 4, 5
//System.out.println(numberList.stream().skip(2)) -> 주소 값이 출력된다.


- Map

map()은 스트림의 데이터를 parameter에 입력한 조건에 맞게 변경해준다.

리스트에 있는 숫자들에 모두 1씩 더한 뒤 출력해보자.

List<Integer> numberList = Arrays.asList(1, 2, 3, 4, 5);
numberList.stream().map(v -> v + 1).forEach(System.out::print);

//출력 : 2, 3, 4, 5, 6

이번에는 리스트에 있는 문자들을 모두 대문자로 바꿔 출력해보자.

List<Integer> numberList = Arrays.asList("apple", "banana", "coconut");
numberList.stream().map(v -> v.toUpperCase()).forEach(System.out::print);

//출력 : APPLE, BANANA, COCONUT


- flatMap

jdk 1.8 버전 부터 사용이 가능한 코드로 스트림 평면화의 기능을 가지고 있다.
각 스트림이 아닌, 스트림의 콘텐츠로 매핑하는 것이다.

예제를 통해 flatMap 을 알아보자.

List<String> fruit1 = Arrays.asList("사과", "딸기", "바나나");
List<String> fruit2 = Arrays.asList("멜론", "수박", "키위");
List<String> fruit3 = Arrays.asList("복숭아", "레몬", "파인애플");

과일가게에 들어온 과일의 목록들이 있다. 각각의 리스트를 전부 합쳐서 하나의 리스트로 만들어야 한다고 가정해보자.

우리가 원하는 리스트의 형태는 이렇다.

출력 : ["사과", "딸기", "바나나", "멜론", "수박", "키위", "복숭아", "레몬", "파인애플"]

새로운 List인 fruitname 를 생성하여 .add 로 데이터를 리스트를 합친 뒤에 출력해보자.

List<List<String>> fruitname = new ArrayList<>();
		
fruitname.add(fruit1);
fruitname.add(fruit2);
fruitname.add(fruit3);

//출력 : ["사과", "딸기", "바나나"], ["멜론", "수박", "키위"], ["복숭아", "레몬", "파인애플"]

하지만 리스트 내부에서 각 요소 별로 중복된 리스트를 생성했기 때문에 우리가 원하는 형태로 출력되지 않는다.

원하는 형태로 리스트를 출력하기 위해서는 이렇게 코드를 작성해야 한다.

List<String> AllPfruits = new ArrayList<>();

	for (List<String> nameList : fruitname) {
		for (String name : nameList) {
			AllPfruits.add(name);
		}
	}
    
System.out.println(AllPfruits); 
//출력 : ["사과", "딸기", "바나나", "멜론", "수박", "키위", "복숭아", "레몬", "파인애플"]

객체를 새로 생성하여 이중 for문을 사용해 작성해야 하는 번거로움이 발생하는데, 이런 경우에 사용하는 것이 바로 flatMap 이다.

flatMap 을 사용해서 코드를 다시 작성해보자.

List<String> AllPfruits = fruitname.stream()
						  .flatMap(l -> l.stream())
                          .collect(Collectors.toList());
                                
System.out.println(AllPfruits);

//출력 : ["사과", "딸기", "바나나", "멜론", "수박", "키위", "복숭아", "레몬", "파인애플"]

[Java] Stream API - map과 flatMap의 차이 및 활용법



- peek

peek은 중간 연산의 결과를 디버깅 할 수 있는 메서드이다.
한개 이상의 필터와 peek 을 이용하여 다양한 결과를 확인해보자.

List<String> fruitList = new ArrayList<>();
fruitList.add("apple");
fruitList.add("banana");
fruitList.add("coconut");

글자 수가 6 이상인 과일과 "c"로 시작하는 과일이라는 조건을 filter 로 뽑아낸 뒤, peek 으로 출력해보자.

fruitList.stream().filter(s -> s.length() > 6)
				  .peek(e -> System.out.println("글자 수가 6 이상인 과일: " + e))
                  .filter(s -> s.startsWith("c"))
                  .peek(e -> System.out.println("c로 시작하는 과일: " + e))
                  .collect(Collectors.toSet()); 
                  
/*출력
글자 수가 6 이상인 과일: banana
글자 수가 6 이상인 과일: coconut
c로 시작하는 과일 : cocnut
*/

이와 같이 peek 을 사용하여 중간 연산의 값을 출력할 수 있다.


✨peek과 forEach의 차이

기능은 동일, 그러나 동작 방식은 다르다!

forEach 메서드는 최종 연산을 하기 때문에 이것만 사용해도 결과 확인이 가능하다.
그러나 peek 메서드는 중간 처리 단계에서 그냥 시도해보겠다는 뜻을 가지고 있기 때문에, 최종 연산이 와야만 결과를 확인할 수 있다!

조금 더 쉽게 풀어보자.

  • forEach() : 스트림의 마지막에 와도 결과 확인 가능
  • peek() : 스트림의 마지막에 오면 결과 확인 불가능

forEach()
forEach().sum()
peek()
peek().sum()

이런 차이점이 존재하므로 peekforEach 를 헷갈려서는 안된다.



복습하면서 같이 읽어볼 글 목록 :

[Java] 람다식(Lambda Expression)과 함수형 인터페이스(Functional Interface)
, [Java] 람다식(Lambda) 익히기
Java 스트림 Stream (1) 총정리 , 자바의 정석 - 스트림

profile
개발이란 뭘까

0개의 댓글