[Java] 20. iterator(for-each)とStream APIを使う方法


Study / Java    作成日付 : 2019/09/04 20:11:28   修正日付 : 2021/01/26 21:01:27

こんにちは。明月です。


この投稿はJavaでiterator(for-each)とStream APIを使う方法に関する説明です。


オブジェクト指向(OOP)でデータをオブジェクト化すると、よく使うアルゴリズムはListとMapだと思います。

ListとMapをよく使う理由はデータの探索がしやすいし整列、データ分類などがしやすいからだと思います。


Listを探索する時に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 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<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());
    }
  }
}


リストにデータを格納して奇数のデータはfor文とif文を組み合わせて抽出しました。


このリスト形式のデータはデザインパターンによる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 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<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()の処理項目が増えてもっと難しいプログラムみたいに見えます。

でも、for(for-each)文を使って簡単に作成することができます。

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<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文で探索すると初期値や増加値がいらないです。つまり、もっと簡単に作成することができるという意味です。


Stream APIに関して説明します。

Streamという意味はプログラムでは連続的な値の配列の意味です。

例えば、一つの動画のファイルを見るときに一つのbyteの値を見れば全然意味がないですね。でもそのbyteのデータが集まって一つの意味があるファイルになって動画がプレイーすることができます。

Javaにはこの連続なデータつまり、配列やList、Mapをもっとしやすく使えるAPI(Application provider interface)がありますが、それがStream APIです。


上の例でListからDataが奇数値だけ分類する時にfor-each文を使ってifで比較して格納しました。

これを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 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));
    }
    // Stream式に変換
    Stream<Node> stream = list.stream();
    // listの値をフィルターする。
    stream = stream.filter(x -> x.getData() % 2 == 1);
    // 結果はlistタイプに返却する。
    List<Node> odds = stream.collect(Collectors.toList());
    // oddsリストをStream式に変換して繰り返し呼び出す。
    odds.stream().forEach(x -> {
      // コンソールに出力
      System.out.println(x.getData());
    });
  }
}


ソースのステップが最初より確実に減らしました。可読性もよくなりました。返却値も新しいリストでリターンするので、元のデータにも影響がありません。

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 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));
    }
    // チェインメソッドパタンタイプで次々に連結することができて、一つのデータを取得することができます。
    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式はチェインメソッドパターン式で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));
    }
    // 並列処理が可能なStream式
    list.parallelStream().forEach(x -> {
      // データに10を掛けて格納
      x.setDate(x.getData() * 10);
      // コンソールに出力
      System.out.println(x.getData());
    });
    // 改行
    System.out.println();
    // ソート、降順
    list.stream().sorted((x, y) -> {
      // sortのリターンタイプはintタイプです。-1になると降順、1になると昇順
      return Integer.compare(y.getData(), x.getData());
    }).forEach(x -> {
      // コンソールに出力
      System.out.println(x.getData());
    });
  }
}


並列処理はスレッドと関係がある部分です。

簡単に説明するとfor文でリストを処理する時に順番とおりにget(0)からget(list.size()-1)まで取得することができます。

でも、仕様によって順番通りに処理する必要がなくて、並列に処理してパフォーマンス改善する時があります。StreamのパラレルスレッドはPCの性能によって自動に生成します。でも、順番通りに処理することではなく、同時に処理することなので、順番が必要な処理では問題がなる可能性があります。


Stream式はsort関数もあります。ただ、二つの値をCompare関数を利用して昇順、降順で並べることもできます。

link - [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)を説明しました。

もしかして、その以上の関数の使い方を知りたいなら下記のJavadocを確認してください。

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


ここまでJavaでiterator(for-each)とStream APIを使う方法に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

最新投稿