[Java] 20. iterator(for-each)과 Stream API


Study/Java  2020. 5. 20. 18:22

안녕하세요. 명월입니다.


이 글은 Java에서의 iterator(for-each)과 Stream API와 에 대한 글입니다.


객체 지향에서 데이터를 객체화하게 되면, 가장 많이 쓰이는 알고리즘은 아마 List와 Map일 것입니다.

List나 Map을 사용하게 되면 데이터를 탐색하기 쉽고 정렬이나 데이터 분류등이 수월하기 때문입니다.


그럴 때마다 for문을 사용해서 i=0에서 size()까지 사용하는 형식의 문법을 많이 사용할 것입니다.

import java.util.ArrayList;
import java.util.List;
// Node 클래스
class Node {
  // 멤버 변수
  private int data;
  // 생성자
  public Node(int data) {
    this.data = data;
  }
  // 멤버 변수 출력
  public int getData() {
    return this.data;
  }
}
public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 0부터 9까지의 데이터가 있는 Node 클래스의 리스트
    List<Node> list = new ArrayList<>();
    // 0부터 9까지 반복
    for (int i = 0; i < 10; i++) {
      // Node 인스턴스 생성
      list.add(new Node(i));
    }
    // 홀수의 값만 list에서 탐색해서 분류할 리스트
    List<Node> odds = new ArrayList<>();
    // list의 크기만큼
    for (int i = 0; i < list.size(); i++) {
      // Node 인스턴스를 취득
      Node node = list.get(i);
      // Node의 data가 홀수인 것만
      if (node.getData() % 2 == 1) {
        // 홀수 리스트에 넣는다.
        odds.add(node);
      }
    }
    // 홀수 리스트의 크기만큼
    for (int i = 0; i < odds.size(); i++) {
      // 콘솔 출력
      System.out.println(odds.get(i).getData());
    }
  }
}

전혀 아무런 문제없이 잘 실행되는 소스입니다.


이런 리스트 형식의 데이터는 디자인 패턴의 Iterator 패턴으로 next와 has를 사용하여 데이터를 가져올 수 있습니다.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
// Node 클래스
class Node {
  // 멤버 변수
  private int data;
  // 생성자
  public Node(int data) {
    this.data = data;
  }
  // 멤버 변수 출력
  public int getData() {
    return this.data;
  }
}
public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 0부터 9까지의 데이터가 있는 Node 클래스의 리스트
      List<Node> list = new ArrayList<>();
      // 0부터 9까지 반복
      for (int i = 0; i < 10; i++) {
        // Node 인스턴스 생성
        list.add(new Node(i));
      }
      // 홀수의 값만 list에서 탐색해서 분류할 리스트
    List<Node> odds = new ArrayList<>();
    // list의 Iterator를 취득
    Iterator<Node> iter= list.iterator();
    // 다음 포인터에 값이 있는가?
    while(iter.hasNext()) {
      // 다음 포인터의 값을 가져온다.
      Node node = iter.next();
      // Node의 data가 홀수인 것만
      if (node.getData() % 2 == 1) {
        // 홀수 리스트에 넣는다.
        odds.add(node);
      }
    }
    // odds의 Iterator를 취득
    iter = odds.iterator();
    // 다음 포인터에 값이 있는가?
    while(iter.hasNext()) {
      // 다음 포인터의 값을 가져온다.
      Node node = iter.next();
      // 콘솔 출력
      System.out.println(node.getData());
    }
  }
}

for문이 아닌 while을 써서 반복의 형태가 바뀐 것 같습니다만, 스텝이 줄어든 건 아닙니다. 오히려 list.iterator()의 구문이 추가되고 왠지 더 어려워진 느낌입니다.

그래서 Java에서는 이 Iterator패턴을 for문으로 정말 간단하게 식을 줄일 수 있습니다.

import java.util.ArrayList;
import java.util.List;
// Node 클래스
class Node {
  // 멤버 변수
  private int data;
  // 생성자
  public Node(int data) {
    this.data = data;
  }
  // 멤버 변수 출력
  public int getData() {
    return this.data;
  }
}

public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 0부터 9까지의 데이터가 있는 Node 클래스의 리스트
      List<Node> list = new ArrayList<>();
      // 0부터 9까지 반복
      for (int i = 0; i < 10; i++) {
        // Node 인스턴스 생성
        list.add(new Node(i));
      }
      // 홀수의 값만 list에서 탐색해서 분류할 리스트
    List<Node> odds = new ArrayList<>();
    // while의 iterator 패턴을 한줄로 줄였다.
    for(Node node : list) {
      // Node의 data가 홀수인 것만
      if (node.getData() % 2 == 1) {
        // 홀수 리스트에 넣는다.
        odds.add(node);
      }
    }
    // while의 iterator 패턴을 한줄로 줄였다.
    for(Node node : odds) {
      // 콘솔 출력
      System.out.println(node.getData());
    }
  }
}

가독성이 올라가고 스탭도 줄어든 모습입니다. 이것이 for-each입니다.


그리고 Stream API를 알아보겠습니다.

먼저 Stream이라는 것은 일련의 연속적인 값의 배열를 뜻합니다. 예를 들면 하나의 동영상을 볼때, 하나의 byte값을 보면 전혀 의미없는 데이터이지만 이것이 여러 byte 데이터가 합쳐져서 하나의 동영상이 되는 것입니다.

Java에서는 이런 일련의 연속적인 값의 배열, 즉 List나 Map을 좀 더 수월하게 다룰 수 있는 API(Application provider interface)를 제공하고 있습니다.


먼저 위의 예제에서 List에서 data가 홀수인 값만 가져오고 싶을 때 다른 반복문을 사용해서 데이터를 분리했습니다.

이걸 Stream의 filter를 사용하면 간단하게 해결됩니다.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

// Node 클래스
class Node {
  // 멤버 변수
  private int data;
  // 생성자
  public Node(int data) {
    this.data = data;
  }
  // 멤버 변수 출력
  public int getData() {
    return this.data;
  }
}

public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 0부터 9까지의 데이터가 있는 Node 클래스의 리스트
    List<Node> list = new ArrayList<>();
    // 0부터 9까지 반복
    for (int i = 0; i < 10; i++) {
      // Node 인스턴스 생성
      list.add(new Node(i));
    }
    // 스트림 식으로 변환
    Stream<Node> stream = list.stream();
    // list의 값을 필터한다.
    stream = stream.filter(x -> x.getData() % 2 == 1);
    // 결과는 list로 내보낸다.
    List<Node> odds = stream.collect(Collectors.toList());
    // odds를 stream으로 변환해서 for-each한다.
    odds.stream().forEach(x -> {
      // 콘솔 출력
      System.out.println(x.getData());
    });
  }
}

확실히 스탭이 줄어 들었습니다. 그리고 stream으로 변환해서 filter로 리스트를 분류해서 새로운 리스트로 생성했습니다.

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

// Node 클래스
class Node {
  // 멤버 변수
  private int data;
  // 생성자
  public Node(int data) {
    this.data = data;
  }
  // 멤버 변수 출력
  public int getData() {
    return this.data;
  }
}

public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 0부터 9까지의 데이터가 있는 Node 클래스의 리스트
    List<Node> list = new ArrayList<>();
    // 0부터 9까지 반복
    for (int i = 0; i < 10; i++) {
      // Node 인스턴스 생성
      list.add(new Node(i));
    }
    // 체인 메소드 패턴 형식으로 줄줄이 엮을 수도 있고 하나의 데이터를 취득할 수도 있습니다.
    Optional<Node> op = list.stream()
                            .filter(x -> x.getData() % 2 == 1)
                            .filter(x -> x.getData() % 3 == 0)
                            .filter(x -> x.getData() > 5)
                            .findFirst();
    // 하나의 데이터를 취득할 때는 Optional으로 반환됩니다.
    // isPresent 값이 있나, isEmpty 값이 없나. 체크를 하고 get으로 데이터를 가져옵니다.
    if(op.isPresent()) {
      System.out.println(op.get().getData());
    }
  }
}

위처럼 체인 메소드 패턴식으로 Stream식을 재사용할 수 있고 filter를 여러번 붙일 수도 있습니다.

import java.util.ArrayList;
import java.util.List;
// Node 클래스
class Node {
  // 멤버 변수
  private int data;
  // 생성자
  public Node(int data) {
    this.data = data;
  }
  // 멤버 변수 출력
  public int getData() {
    return this.data;
  }
  // 멤버 변수 값 저장
  public void setDate(int data) {
    this.data = data;
  }
}

public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 0부터 9까지의 데이터가 있는 Node 클래스의 리스트
    List<Node> list = new ArrayList<>();
    // 0부터 9까지 반복
    for (int i = 0; i < 10; i++) {
      // Node 인스턴스 생성
      list.add(new Node(i));
    }
    // 병렬 처리가 가능합니다.
    list.parallelStream().forEach(x -> {
      // 데이터에 10을 곱해서 저장
      x.setDate(x.getData() * 10);
      // 콘솔 출력
      System.out.println(x.getData());
    });
    // 개행 출력
    System.out.println();
    // 소트.. 내림차순..
    list.stream().sorted((x, y) -> {
      // sort의 return 값은 int형입니다. -1이 되면 내림차순, 1이 되면 올림차순입니다.
      return Integer.compare(y.getData(), x.getData());
    }).forEach(x -> {
      // 콘솔 출력
      System.out.println(x.getData());
    });
  }
}

여기서의 병렬 처리는 쓰레드와 관계가 있는 부분입니다.

간단하게 설명하면 for문에서 리스트를 처리할 때 순서대로 get(0)부터 get(list.size()-1)까지 처리가 되는 것을 알고 있습니다.

그러나 사양에 따라서 순서대로 처리할 필요없이 동시에 처리해서 for문의 성능을 높힐 때 사용합니다. PC에 성능에 따라 쓰레드가 자동 생성됩니다. 문제는 순서대로 처리되는 것이 아니라서 순서를 요하는 데이터를 넣을 때는 문제가 발생할 수 있습니다.


그리고 List에서 정렬을 한다고 한다면 각 값의 비교를 통한 소트 알고리즘을 작성해야하지만 Stream API에서 sorted를 사용하면 손쉽게 정렬도 가능합니다.

sorted의 람다식 반환값은 int형으로 compare값으로 오름차순 내림차순이 정렬됩니다.

참조 - [Java] Compare 함수 사용법

import java.util.ArrayList;
import java.util.List;
// Node 클래스
class Node {
  // 멤버 변수
  private int data;
  // 생성자
  public Node(int data) {
    this.data = data;
  }
  // 멤버 변수 출력
  public int getData() {
    return this.data;
  }
  // 멤버 변수 값 저장
  public void setDate(int data) {
    this.data = data;
  }
}
public class Example {
  // 실행 함수
  public static void main(String... args) {
    // 0부터 9까지의 데이터가 있는 Node 클래스의 리스트
    List<Node> list = new ArrayList<>();
    // 0부터 9까지 반복
    for (int i = 0; i < 10; i++) {
      // Node 인스턴스 생성
      list.add(new Node(i));
    }
    // Node의 data가 10이 넘는 값이 존재하는가?
    if (!list.stream().anyMatch(x -> x.getData() > 10)) {
      // 콘솔 출력
      System.out.println("The data is not have it.");
    }
  }
}

list에 값이 존재하는 지에 대한 판단도 가능합니다. list.contains와 비슷합니다. contains는 보통 클래스 타입은 사용하기 힘들기 때문에 Stream API의 anyMatch로 많이 사용됩니다.


Stream API의 종류는 사실 엄청 많습니다.

그걸 다 설명하기에는 내용이 너무 많아서 실제 프로그래밍할 때 자주 사용하는 함수 filter, sorted, findFirst, anyMatch등을 소개했습니다.

그 이상이 필요하면 API를 참조하세요.

링크 - https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html


여기까지 Java에서의 iterator(for-each)과 Stream API에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.