Java Stream이란? 4탄 API를 활용하고 사용해보자

Java Stream이란? 4탄 API를 활용하고 사용해보자

2021, Mar 19    

정리를 하게 된 이유.

앞선 정리에서 람다와 함수형 인터페이스에 대해 알아보았다면, 이젠 Stream API의 사용법을 익혀보면서, 프로그래머스에 간단한 알고리즘 문제들을 풀어보기로 한다.

Stream 생성하기

Stream API를 사용하기 위해서는 먼저 Stream을 생성해주어야 한다. 사용하려는 객체들마다 Collection을 생성하는 방법이 다른데, 여기서는 Collection과 Array에 대해서 Stream을 생성하는 방법에 대해 알아보도록 하겠다.

Collection의 Stream 생성

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

//List로부터 스트림을 생성 
List<String> list = Arrays.asList("a","b","c");
Stream<String> listStream = list.stream();

배열의 Stream 생성

배열의 원소들을 소스로하는 Stream을 생성하기 위해서는 Stream의 of 메소드 또는 Arrays의 Stream 메소드를 사용하면 된다.

//배열로부터 스트림을 생성
Stream<String> stream = Stream.of("a","b","c"); //가변인자 
Stream<String> stream = Stream.of(new String[]{"a","b","c"});
Stream<String> stream = Arrays.stream(new String[]{"a","b","c"});
Stream<String> stream = Arrays.stream(new String[]{"a","b","c"},0,3);

원시 Stream 생성

위와 같이 객체를 위한 Stream 외에도 int와 long 그리고 double과 같은 원시 자료형들을 사용하기 위한 특수한 종류의 Stream(IntStream, LongStream, DoubleStream)들도 사용할 수 있으며, IntStream과 같은 경우 range()함수를 사용하여 기존의 for문을 대채할 수 있다.

IntStream stream = IntStream.range(4,10);

Stream 가공하기(중간연산)

생성한 Stream 객체에서 요소들을 가공하기 위해서는 중간연산이 필요하다. 가공하기 단계의 파라미터로는 앞서 설명하였던 함수형 인터페이스들이 사용되며, 여러 개의 중간연산이 연결되도록 반환값으로 Stream을 반환한다.

[필터링]

Filter는 Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어내는 연산이다. Java에서는 filter 함수의 인자로 함수형 인터페이스 predicate를 받고 있기 때문에, boolean을 반환하는 람다식을 작성하여 filter 함수를 구현할 수 있다.

  • 어떤 String의 stream에서 a가 들어간 문자열만 포함하도록 필터링 해보자
Stream<String> stream = list.stream()
                            .filter(name -> name.contains("a"));

데이터 변환 Map

Map은 기존의 Stream 요소들을 변환하여 새로운 Stream을 형성하는 연산이다. 저장된 값을 특정한 형태로 변환하는데 주로 사용되며, Java에서는 Map 함수의 인자로 함수형 인터페이스 function을 받고 있다. 예를 들어 String을 요소들로 갖는 Stream을 모두 대문자 String의 요소들로 변환하고자 할 때 map을 이용 할 수 있다.

Stream<String> stream = names.stream()
                             .map(s -> s.toUpperCase());

메소드 참조를 이용하여 파일의 Stream을 파일 이름의 Stream으로 변경해보자.

Stream<File> fileStream = Stream.of(new File("Test1.java), new File("Test2.java), new File("Test3.java));

Stream<String fileNameStream = fileStream.map(File:getName);

중복 제거 Distinct

Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 distinct를 사용할 수 있다. distinct는 중복된 데이터를 검사하기 위해 Object의 equals() 메소드를 사용한다. 예를 들어 중복된 Stream의 요소들을 제거하기 위해서는 아래와 같이 사용 가능 하다.

List<String> list = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift", "Java");

Stream<String> stream = list.stream()
                        .distinct()

만약 우리가 생성한 클래스를 Stream으로 사용하고자 하면, equals와 hashCode를 오버라이드 해야만 distinct()를 제대로 적용할 수 있다.

public class Employee {

    private String name;

    public Employee(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}

위의 Employee 클래스는 equals와 hashCode를 오버라이드하지 않았기 때문에, 아래 코드를 실행해도 중복된 데이터가 제거되지 않고, size값으로 2를 출력하게 된다.

import java.util.*;

public class Main {

    public static void main(String[] args){
        
        Employee e1 = new Employee("Drk");
        Employee e1 = new Employee("Alam");
        List<Employee> employees = new ArrayList<>();
        employees.add(e1);
        employees.add(e2);
        
        int size = employees.stream().distinct().collect(Collectors.toList()).size();
        System.out.println(size);        
     
    }
    
}

그래서 equals 와 hashCode를 오버라이드해서 해결해야 된다.

public class Employee {

    private String name;

    public Employee(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
    @Override
    public boolean equals(Object o){
        if(this == o) return true;
        if(o==null||getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return Objects.equals(name,employee.name);
    }
    
    @Override
    public int hashCode(){
        return objects.hash(name);
    }
}

특정 연산 수행 Peek

Stream의 요소들을 대상으로 Stream에 영향을 주지 않고 특정 연산을 수행하기 위한 peek 함수가 존재한다. peek 함수는 Stream의 각각의 요소들에 대해 특정 작업을 수행할 뿐 결과에 영향을 주지 않는다. peek 함수는 파라미터로 함수형 인터페이스 Consumer를 인자로 받는다.

int sum = IntStream.of(1,3,5,7,9)
                   .peek(System.out::println)
                   .sum();

원시 Stream <-> Stream

작업을 하다 보면 일반적인 Stream 객체를 원시 Stream으로 바꾸거나 그 반대로 하는 작업이 필요한 경우가 있다. 이러한 경우를 위해서, 일반적인 Stream 객체는 mapToInt(),mapToLong(),mapToDouble()이라는 특수한 Mapiing 연산을 지원하고 있으며, 그 반대로 원시객체는 mapToObject를 통해 일반적인 Stream 객체로 바꿀 수 있다.

IntStream.range(1,4)
         .mapToObj(i -> "a" + i)

Stream.of(1.0, 2.0, 3.0)
      .mapToInt(Double::intValue)
      .mapToObj(i - > "a" + i)