目次

【デザインパターン】JavaのIteratorについて

今日から自分の忘備録も兼ねてデザインパターンシリーズをまとめて行こうと思います。

September 14, 20237 min read
Java
デザインパターン

Iteratorとは

Iteratorは配列やリストなどのデータ構造を順番に処理するためのインターフェースです。 iterator=繰り返し です。 java.lang.Iterable, java.utils.Iteratorからimportすることで使用できます。

java.lang.Iterableから抜粋

java
public interface Iterable<E>{
  public abstruct Iterator<E> iterator();
}

java.utils.Iteratorから抜粋

java
public interface Iterator<E>{
  public abstract boolean hasNext();
  public abstract E next();
}

上記の抽象interfaceをimplementsして使います。

public MenuList implements Iterable<Menu> {}

なんで使うの?

ループの処理を抽象化して実装と切り分けることで保守性を高めることができます。 例えばJavaのFor文では裏でこのIteratorを呼び出して配列を一つずつ回してチェックをしています。Iteratorを実行するMenuListを作れば下記のような実装ができます。

for(Menu menu: MenuList){
	System.out.println(menu.getName());
}

使ってみなきゃ分からんと思うので実装してみましょう。

実装

今回は果物のメニューのリストを作ってみます。

java
public class Menu {
    public String name;
    public String price;
    public Menu(String name, String price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public String getPrice() {
        return price;
    }
}

Iterableを実装することでiteratorの具体的な処理をオーバーライド(実装)します。 具体的といってもここではMenuListIteratorというインスタンスを返すだけで、こちらのインスタンスに実際の処理は後ほど書きます。それを用いるよ、ということです。 for文はこのiteratorを用いて順繰りに値を参照することになります。

java
import java.util.Iterator;

public class MenuList implements Iterable<Menu>{
    private Menu[] menus;
    public int last = 0;
    public MenuList(int max_size) {
        this.menus = new Menu[max_size];
    }
    public void addMenu(Menu menu){
        this.menus[last] = menu;
        last++;
    }
    public Menu getMenuAt(int index){
        return menus[index];
    }
    @Override
    public Iterator<Menu> iterator() {
        return new MenuListIterator(this);
    }
}

ここで配列のスキャンを行うための具体的なメソッドの中身を実装します。 hasNext()は配列の次の値が存在するかどうかを確認、next()は現在参照されているMenuを返してから次へ配列を進める処理をしています。

java
import java.util.Iterator;
public class MenuListIterator implements Iterator<Menu>{
    private MenuList menuList;
    public int index = 0;
    public MenuListIterator(MenuList menuList) {
        this.menuList = menuList;
    }
    @Override
    public boolean hasNext() {
        return index < menuList.last;
    }
    @Override
    public Menu next() {
        if(!hasNext()){
            throw new IndexOutOfBoundsException();
        } else {
            Menu menu = menuList.getMenuAt(index);
            index++;
            return menu;
        }
    }
}

Mainクラス

メインクラスでメニューを追加し、ループ処理を実際に行います。

java
import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        MenuList menuList = new MenuList(5);
        menuList.addMenu(new Menu("lemon", "200"));
        menuList.addMenu(new Menu("strawberry", "300"));
        menuList.addMenu(new Menu("orange", "400"));
        menuList.addMenu(new Menu("apple", "500"));
        menuList.addMenu(new Menu("banana", "600"));
        Iterator<Menu> it = menuList.iterator();
        while (it.hasNext()) {
            Menu menu = it.next();
            System.out.println("name:" + menu.getName() + " price:" + menu.getPrice());
        }
    }
}

こうすることで、menuListを配列として扱うことができます。 ちなみにこれは以下でも出力できます。

Menu[] menus = menuList.menus;
for(Menu menu: menus) {
	...
}

じゃあこれで良くね。てなりますが抽象化は色々いいこともあります。

  1. 汎用性: Iterator は、Collection インターフェースのすべての実装に対して動作します。つまり、List, Set, Queueなどの様々なデータ構造に対して統一的にアクセスする手段を提供します。

  2. 修正の安全性: Iterator を使用すると、コレクションを反復処理している最中にコレクションを安全に変更することができる場合があります。

    while (it.hasNext()) {
        Menu menu = it.next();
        if (menu.getName().equals("banana")) {
            it.remove();
        }
    }
    
    System.out.println(list);  // [apple, cherry]
    

    iteratorにはremoveメソッドがあるのでそちらの処理を実装することでリストから "banana" を安全に削除しています。 このような操作をfor-eachループで行うと ConcurrentModificationException が発生します。

  3. 独自のデータ構造: カスタムのデータ構造や外部のデータソースに対する反復処理を実装する場合、Iterator パターンを使用して、そのデータ構造やデータソースをJavaのfor-eachループで使用できるようにすることができます。

  4. 遅延評価: 一部の Iterator の実装は、次の要素が必要になるまでデータの取得や計算を遅延させることができます。これにより、巨大なデータセットや無限のデータストリームを効率的に処理することが可能になります。 自然数のストリームの例を以下に示します。

    java
    public class NaturalNumbers implements Iterable<Long> {
        @Override
        public Iterator<Long> iterator() {
            return new Iterator<Long>() {
                private long current = 1;
    
                @Override
                public boolean hasNext() {
                    return true;  // 自然数は無限
                }
    
                @Override
                public Long next() {
                    return current++;
                }
            };
        }
    }
    
    // このイテレータを使用して最初の5つの自然数を出力する
    Iterator<Long> numbers = new NaturalNumbers().iterator();
    for (int i = 0; i < 5; i++) {
        System.out.println(numbers.next());  // 1, 2, 3, 4, 5 が順に出力される
    }
    

    こうすることで、自然数をどれだけ表示するかという計算を遅延させることができます。 型と処理を分離することで再利用性、スケーリングが簡単になります。

抽象化は冗長に見えますが、より柔軟なコードになるので必要なタイミングを見極めて使えるようになりましょう!(僕もまだ使いこなせませんが)

See you later👋


0

Conversation