스트림(stream)은 배열, 컬렉션 같은 여러 자료를 처리하는 일을 더 효율적으로 할 수 있도록 도와주는 라이브러리이다. 처리해야 하는 자료에 상관없이 동일한 방법으로 처리(메소드를 사용)할 수 있도록 기능이 구현되어있다.

package stream;
import java.util.Arrays;

public class IntArrayTest {
   public static void main(String[] args) {

      int arr[] = {1,2,3,4,5};

      //배열의 모든 요소 출력 1
      for(int i=0; i<arr.length; i++) {
         System.out.println(arr[i]);
      }

      System.out.println();

      //배열의 모든 요소 출력 2 : stream 사용
      Arrays.stream(arr).forEach(x -> System.out.println(x));
   }
}

기본적으로 for문을 사용하여 배열의 모든 요소를 출력할 수 있는데, 스트림을 이용해서 같은 기능을 수행하는 코드를 예시로 보았다. 뭔가 더 간결해보인다.

위에서 예시로 든 것과 같이 스트림은 자료를 효율적으로 처리하도록 도와준다. 자료형의 관계에 상관없이 말이다.

 

1. 스트림 연산


스트림 연산에는 중간연산과 최종연산이 있다. 중간연산은 자료를 변경하여 내부적으로 새로운 자료를 생성한다. 최종 연산은 생성된 내부자료에 대해 연산을 수행하여 연산결과를 만들어내며, 마지막에 단 한 번만 호출된다.

모든 중간연산, 최종연산 메소드를 알 필요는 없다. 자주 쓰이는 메소드 위주로 보면서 사용법을 익히면 충분하다.

package stream;
import java.util.Arrays;
import java.util.ArrayList;

public class IntArrayTest {
   public static void main(String[] args) {
      ArrayList<String> myStrings = new ArrayList<String>();
      myStrings.add("hello");
      myStrings.add("java");
      myStrings.add("digimon");
      myStrings.add("ha");
      myStrings.add("say");

      //        (스트림생성) (길이가 4 인 문자열만 추출)         (추출된 각각의 문자열을 출력)

      myStrings.stream().filter(s -> s.length() >=4).forEach(s -> System.out.println(s));

   }
}

위 예제에서 filter() 중간연산으로 길이가 4이상인 문자열만 추출하여 새로운 자료를 내부적으로 생성하였다. 그리고 마지막에는 forEach() 최종연산으로 중간연산으로 생성된 자료를 하나하나 훑으며 출력하는 최종 연산을 수행한다.

하나의 예시를 더 보자.

package stream;

public class Student {
   int age;
   String name;

   public Student(int age, String name) {
      this.age = age;
      this.name = name;
   }

   public int getAge() {
      return age;
   }

   public String getName() {
      return name;
   }

}

 

package stream;
import java.util.ArrayList;

public class StudentTest {

   public static void main(String[] args) {
      ArrayList<Student> stus = new ArrayList<>();
      stus.add(new Student(20, "Lee"));
      stus.add(new Student(17, "Kim"));
      stus.add(new Student(22, "Park"));
      stus.add(new Student(15, "Choi"));

      int sum=0;
      stus.stream().map(p -> p.getName()).forEach(p -> System.out.println(p));
   }

}

 

package stream;
import java.util.Arrays;
import java.util.ArrayList;

public class IntArrayTest {
   public static void main(String[] args) {
      int arr[] = {1,2,3,4,5};

      int sum = Arrays.stream(arr).sum();
      int count = (int)Arrays.stream(arr).count(); //count()의 반환값은 long형 정수이므로 int로 형변환

      System.out.println("sum = "+sum+", count = "+count);

   }
}

 

 

2. 스트림 사용


package stream;
import java.util.ArrayList;
import java.util.stream.Stream;

public class StudentTest {

   public static void main(String[] args) {

      //ArrayList 생성하여 4명의 학생 객체 추가
      ArrayList<Student> stus = new ArrayList<>();
      stus.add(new Student(20, "Lee"));
      stus.add(new Student(17, "Kim"));
      stus.add(new Student(22, "Park"));
      stus.add(new Student(15, "Choi"));


      Stream<Student> stream = stus.stream(); //스트림 생성
      stream.map(s -> s.getAge()).forEach(s -> System.out.println(s)); //스트림 소모

      // stream.map(s -> s.getName()).forEach(s -> System.out.println(s)); // 오류 발생

      Stream<Student> stream2 = stus.stream(); //스트림 재생성
      stream2.map(s -> s.getName()).forEach(s -> System.out.println(s)); //스트림 소모
   }

}

위에서 처음으로 스트림을 생성하고 나이만 추출한 후 출력을 하였다. 이후에는 스트림으로 이름만 추출하여 출력하려하는데 오류가 발생한다. 오류발생 이유는 스트림이 소모되었기 때문이다. 스트림은 최종 출력결과를 내는 최종연산 메소드를 호출하여 최종결과를 내면서 스트림을 소모한다. 따라서 스트림을 사용하려면 다시 스트림을 생성해 주어야한다.

 

 

3. 스트림의 성질


1) 자료형에 상관없이 동일한 방법 적용

자료형에 관계없이 같은 방법으로 메소드를 호출해서 사용하면 된다. 이는 아주 편리한 특징이다. 이는 스트림이 자료구조에 의존하지 않는 일관성있는 메소드를 제공하기 때문이다.

 

2) 스트림은 소모된다

한 번 생성된 스트림으로 일단 연산을 하면 스트림이 소모된다. 스트림으로 또 다른 작업을 하기 위해서는 새로 생성해주어야한다.

 

3) 스트림은 별도의 메모리 공간을 사용한다

스트림을 호출하여 리스트, 집합 등의 자료를 가지고 여러가지 연산을 하여도 대상이 되는 자료는 아무런 영향을 받지 않는다. 이는 스트림이 연산을 할 때 사용하는 별도의 메모리공간이 존재하기 때문이다.

 

4) 중간 연산은 여러번 가능하다

예를들어 학생 객체를 요소로 가지는 ArrayList가 있다고 하자. 이 때 15세 이상의 학생만 추출하기 위해 중간연산 메소드인 filter()를 한 번 호출하고, 이를 오름차순으로 정렬하기위해 sorted()메소드를 한 번 호출할 수 있다.

하지만 마지막으로 최종연산 메소드를 호출하여야만 결과가 만들어진다.

 

 

4. 프로그래머가 정의하는 연산기능 : reduce()


reduce()는 스트림의 자료를 하나하나 소모해가며 프로그래머가 직접 정의한 함수 기능을 수행한다.

reduce() 메소드는 다음의 형태로 정의되어있다.

T reduce(T identify, BinaryOperator<T> accumulator)
//T reduce(초기값, 함수 기능)

identify는 초기값을 의미하고 BinaryOperator는 인터페이스로, 람다식을 써주어도 되고 인터페이스를 구현한 클래스를 만들어 추상메소드를 재정의할 수도 있다.

BinaryOperator는 apply() 추상메소드를 가지고 있으며 우리가 람다식으로 구현한 기능, 혹은 클래스를 통해 구현한 기능이 apply()메소드의 기능으로 재정의 되는 원리이다. apply() 메소드는 매개변수 2개, 반환값 1개를 가지며 세 가지의 자료형은 동일하다.

겉보기에 복잡해 보이지만 사용방법은 엄청 단순하다.

package stream;

import java.util.function.BinaryOperator;
import java.util.Arrays;

public class CompareString implements BinaryOperator{  

   @Override //apply() 메소드는 두 개씩 비교하며 큰 것을 추려내도록 재정의
   public Object apply(Object s1, Object s2) {

      if(((String) s1).getBytes().length >= ((String) s2).getBytes().length) {
         return s1;
      }
      else {
         return s2;
      }

   }

   public static void main(String[] args) {
      String[] says = {"hello man~!", "yo man hallo~ Yeah!", "halo~"};

      //1. 인터페이스를 구현한 클래스 객체를 전달하여 사용하는 방법
      String str = (String) Arrays.stream(says).reduce(new CompareString()).get();
      System.out.println(str);

      //2.  초기값과 람다식을 직접 전달하여 사용하는 방법
      System.out.println(
         Arrays.stream(says).reduce("", (s1, s2) -> {
         if(s1.getBytes().length >= s2.getBytes().length) {return s1;}
         else {return s2;}
         })
      );
   }

}

인터페이스를 구현한 클래스로 객체를 만들어 reduce() 메소드의 매개변수로 전달하면 클래스에 재정의된 apply()메소드가 자동으로 호출된다. 마지막에 남은 최종 결과를 get()메소드를 이용하여 얻어서 str에 저장하고 출력해보면 가장 긴 문자열이 출력된다.

초기값과 람다식을 직접 전달한 경우도 같다. 사실 이 경우도 익명클래스 객체가 생성되는 경우이므로 내부적인 구조와 로직이 인터페이스를 구현한 클래스로 호출하는 경우와 동일하다.

 

 

5. 예제


여행사의 고객 리스트 클래스를 정의한다. 멤버변수로는 이름, 나이, 가격이 있는데 20세 이상 고객은 100원, 밑으로는 50원의 가격을 적용한다.

package stream;

public class TravelCustomer {
   private String name;
   private int age;
   private int price;

   public TravelCustomer(String name, int age, int price) {
      this.name = name;
      this.age = age;
      this.price = price;
   }

   public String getName() {
      return name;
   }

   public int getAge() {
      return age;
   }

   public int getPrice() {
      return price;
   }

   @Override
   public String toString() {
      return "name: "+name +" age: "+age+ " price: "+price;
   }
}

 

package stream;
import java.util.ArrayList;
import java.util.List;
public class TravelTest {

   public static void main(String[] args) {
      List<TravelCustomer> list = new ArrayList<>();
      list.add(new TravelCustomer("Lee", 25, 100));
      list.add(new TravelCustomer("Kim", 45, 100));
      list.add(new TravelCustomer("Park", 14, 50));

      System.out.println("======show all customers=======");
      list.stream().forEach(p -> System.out.println(p));

      System.out.println("=======total cost==========");
      int total = list.stream().mapToInt(p -> p.getPrice()).sum();
      System.out.println(total);

      System.out.println("========all adult customer========"); //20세 이상 고객만 출력
      //filter()를 사용하여 20세 이상만 추출 -> map 메소드를 사용하여 이름만 추출 -> 오름차순 정렬 -> 각각의 요소를 출력
      list.stream().filter(p->p.getAge()>=20).map(p->p.getName()).sorted().forEach(p -> System.out.println(p));

   }

}

성인 이상의 고객 명단만 출력할 때에는 중간 연산 메소드를 총 3개 사용하였다. 다시 한 번 말하지만 중간연산은 몇번이 되어도 상관없으며 항상 최종연산을 하여야 결과를 얻을 수 있다.

'프로그래밍 언어 > Java' 카테고리의 다른 글

(Java) 51 - 예외 throws  (0) 2020.05.18
(Java) 50 - 예외처리  (0) 2020.05.18
(Java) 48 - 람다식  (0) 2020.05.17
(Java) 47 - 내부 클래스  (0) 2020.05.17
(Java) 46 - Map 인터페이스  (0) 2020.05.17

+ Recent posts