자바 8에서 부터 지원하는 기능으로, 여러 기능을 하나의 코드에서 연결하여 데이터를 정제하고 변경할 수 있다.(즉 메서드를 체이닝해서 쓴다고 본다)
또한 스트림을 사용하면 멀티스레드 구성 없이 데이터를 병렬로 처리할 수 있다.
먼저 예시를 보면서 코드가 어떻게 바뀌는지 하나씩 알아보자.
내가 원하는 건 이름, 키, 몸무게를 가지는 Human 리스트가 있고, 해당 리스트에서 키 160 이상, 몸무게 내림차순으 Human의 이름만 출력하고 싶다.
Human 클래스
public class Human {
private String name;
private Integer height;
private Integer weight;
public Human(String name, Integer height, Integer weight) {
this.name = name;
this.height = height;
this.weight = weight;
}
Main
public static void main(String[] args) throws IOException {
List<Human> list = new ArrayList<>();
list.add(new Human("박기현", 170, 65));
list.add(new Human("김소연", 163, 50));
list.add(new Human("최규호", 163, 140));
list.add(new Human("이세훈", 200, 40));
list.add(new Human("송소연", 150, 50));
list.add(new Human("유민국", 180, 500));
// 키 160 이상인 사람 필터링
List<Human> height160 = new ArrayList<>();
for(Human human : list){
if(human.getHeight() >= 160) height160.add(human);
}
// 몸무게 내림차순 정렬
Collections.sort(height160, (o1 ,o2) -> o2.getWeight().compareTo(o1.getWeight()));
// 이름만 뽑음
List<String> choiceName = new ArrayList<>();
for(Human human : height160){
choiceName.add(human.getName());
}
System.out.println(choiceName);
}
위 코드를 보면 열심히 키 160 이상 필터링하고 몸무게 내림차순 정렬하고 이름만 출력하는 코드이다.
대충 봐도 뭔가 많이 거쳐야 한다.
이걸 스트림을 사용하면 어떻게 코드가 변할까?
Stream 이용한 코드
List<String> resultName = list.stream()
.filter(human -> human.getHeight() >= 160)
.sorted((h1, h2) -> h2.getWeight().compareTo(h1.getWeight()))
.map(human -> human.getName())
.collect(Collectors.toList());
System.out.println(resultName);
너무 신기신기하 짧고 가독성이 좋게 바뀌는 걸 확인할 수 있다.
이제 저 코드가 뭘 의미하는지 하나씩 알아보자.
list.stream()
리스트를 스트림 형태로 변환해서 사용하겠다고 선언하는 부분이다.
이렇게 하면 스트림 API를 밑에 연결해서 데이터를 정제할 수 있게 된다.
.filter(human -> human.getHeight() >= 160)
filter는 말 그대로 데이터를 정제하기 위해 사용된다. 키가 160 이상인 Human만 골라야 했기에 람다식을 사용해서 160 이상의 Human만 고르게 된 것.
human -> 이 부분은 List<Human> list에 있는 하나의 Human 객체를 의미한다. 즉 하나씩 꺼내서 전부 조건을 판단하는 것.
.sorted((h1, h2) -> h2.getWeight().compareTo(h1.getWeight()))
sorted는 정렬을 하기 위해 사용된다. 몸무게 내림차순 정렬이기에 람다식을 사용해서 정렬을 진행.
.map(human -> human.getName())
map은 한 요소를 다른 요소로 변환하거나 정보를 추출할 때 사용한다.
우리의 코드에서는 Human이라는 객체를 String이라는 이름으로 변경시키는 부분이다.
즉 이 부분에서 기존 Human 리스트의 스트림에서 이름만 가진 스트림으로 바꾸게 된다.
.collect(Collectors.toList());
스트림을 List로 만들어내는 코드이다.
이 코드를 통해 최종 결과물인 List<String>을 뽑아내게 된다.
이 외에도 Stream API는 굉장히 많은 기능을 가지고 있다.
- distinct() = 중복 제거
- limit(n) = n개로 개수 제한
- skip(n) = n개의 원소 건너뛰기
- reduce() = 메서드를 연쇄적으로 계산하기 위해
- ....
등등 수 많은 기능이 존재한다. 해당 내용은 공식 문서를 참고하는 게 좋을 것 같습니다.
toList() vs collect(Collectors.toList())
List<String> resultName = list.stream()
.filter(human -> human.getHeight() >= 160)
.sorted((h1, h2) -> h2.getWeight().compareTo(h1.getWeight()))
.map(human -> human.getName())
.toList();
JDK 16부터 toList()라는 메서드가 추가되었는데, 기존의 collect(Collectors.toList())를 대체하여 사용할 수 있다.
그럼 무조건 이걸 쓰는 게 좋냐? 이건 고민을 해야 하는 부분이 있는데
toList()로 List를 생성하면
위 코드에서 보듯이 unmodifiableList를 반환하게 된다. (즉 수정이 불가능하다)
반면에 collect(Collectors.toList())를 보면
그냥 일반적인 ArrayList를 생성해서 주는 걸 확인할 수 있다.
즉 toList()로 생성하면 변경이 불가능하게 된다.
List<String> resultName = list.stream()
.filter(human -> human.getHeight() >= 160)
.sorted((h1, h2) -> h2.getWeight().compareTo(h1.getWeight()))
.map(human -> human.getName())
.toList();
resultName.add("추가");
toList()로 만든 List에 데이터를 추가하게 된다면?
보란듯이 에러가 발생.
이런 차이가 있다는 것을 알면 된다.
근데 궁금한 게 기존의 코드랑 시간차이는 얼마나 발생할까?
스트림을 쓰면 5000000ns가 걸린다.
리스트는 1000000ns가 걸린다.
즉 for문 > 스트림 (무려 5배 차이로 for문이 빠르다)
다른 블로그의 글들을 보면 Stream vs for의 비교가 되게 많다.
결론은 for문이 웬만한 상황에서는 우세하다는 소리인데...stream이 가독성 하나는 좋다. 만약에 진짜 시간이 엄청엄청 오래 걸리는 거 아니면 stream으로 가독성을 챙기는 게 좋다고 판단이 된다.
참고 자료