やり直しJava

制御構文

分岐

if文

if(条件式1) {
    処理1;
} else if(条件式2) {
    処理2;
} else {
    処理3;
}

条件式にはboolean型の変数や値・boolean値を返す式やメソッドを指定します。条件式は booleanである必要があり、C言語などのような柔軟性がありません。

  • 条件式にint型の変数や値・int値を返す式やメソッドを使用することはできません。
  • 条件式にオブジェクト参照を渡してnull判定に使うようなこともできません。
// if文の条件式にint値等を渡すことはできない。
// 以下はコンパイルエラー
//int i = 0;
//if(i) {
//    System.out.println("i != true");
//}

// if文の条件式にObjectを渡してnull判定は行えない。
// 以下はコンパイルエラー
//String str;
//if(str) {
//    System.out.println("str != true");
//}

条件演算子

条件演算子を使うと、if-else文を1行で記述することができます。条件演算子は三項演算子とも呼ばれます。

戻り値 = (条件式)? trueの場合の式 : falseの場合の式;
int value = (param != undefined) ? param : defaultValue;

条件演算子はネストさせることもできます。しかし、trueの時の式やfalseの時の式が長い場合や、ネストが深い場合は かえってコードが読みづらくなるので注意が必要です。

switch文

switch(値や式) {
    case  値1:
        処理1;
        break;
    case  値2:
        処理2;
        break;
    case  値3:
        処理3;
        break;
    default:
        処理4;
}

switchで判定する値や式に指定できるのは、char型・byte型・short型・int型・String・enumに限られます。(Stringを扱えるのはJavaSE 7以降。)

また、caseブロックでは 明示的にbreak;を指定しないと、次のcase文に進みます(フォールスルー)。これは バグを生み易い仕様なので break漏れには注意が必要です。

繰り返し

for文

for(初期化式; 条件式; 増減式) {
    処理;
}

for文は繰り返し回数が決まっている場合や 繰り返し処理の中でループインデックスを参照したい場合に向いています。配列やCollectionの各要素に対して処理を行う(配列やCollectionの要素を走査する)場合は、後述の拡張for文の方が便利です。

ループインデックス変数や走査のために必要なイテレータ変数はループ変数と呼ばれます。

ループ変数をループカウンタとして使用する場合、浮動少数点数型にすると 思わぬ動作になる場合があるため 注意が必要です。

for(float f = 0.1f; f <= 1.0; f += 0.1) {
    System.out.println(f);
}

floatの0.1は 2進数で表すと0.00110011…と無限循環小数となり 0.1より少し大きい近似値に丸められます。そのため、上のコードを実行すると 繰り返し回数は10回ではなく 9回になります。

0.1
0.2
0.3
0.4
0.5
0.6
0.70000005
0.8000001
0.9000001

イテレータを用いた繰り返し

配列やCollectionの要素を走査しながら 途中で要素を削除する場合にはイテレータを用います。

List<String> arrayList = ....
for(Iterator<String> i = arrayList.iterator(); i.hasNext(); ) {
    if(i.next().equals("optional")) {  // 削除したい要素
        i.remove();
    }
}

イテレータを用いず、例えばList.remove()のような削除メソッドで要素の削除を行うとConcurrentModificationExceptionが発生します。

マルチスレッドで実行しているわけでもないのに ConcurrentModificationExceptionが発生するため 少し違和感があるかも知れません。ConcurrentModificationExceptionが発生するメカニズムについては teratailに分かり易い Q&Aがありますので、そちらを参照してください。

CopyOnWriteArrayList

前述の通り 配列やCollectionの要素を走査しながら 途中で要素を削除するような場合にはイテレータを用います。しかし、イテレーションの中で サブクラスでオーバーライド可能なメソッドを呼び出したり、関数オブジェクトのメソッドを呼び出すような場合は、それらのメソッドの中で元の配列やCollectionの構造を変更するような処理が行われてしまう可能性があり、それを防ぐことができません。このように 制御が及ばず どんな処理が行われるかわからないようなメソッドは エイリアンメソッドと呼ばれます

登録されているイベントリスナにイベント発生を知らせるような場合に この問題に直面することがあります。次のようなイベントリスナとイベント通知元の枠組みを例に挙げます。

// イベントリスナ
interface SomeListener {
    void handleData(int data);
}

// イベントの通知元
class ListenerRegistry {
    static ListenerRegistry instance = new ListenerRegistry();
    public static ListenerRegistry getInstance() {
        return instance;
    }
    // イベントリスナリスト
    List<SomeListener> list = new ArrayList<>();
    // イベントリスナ登録
    public void addListener(SomeListener l) {
        list.add(l);
    }
    // イベントリスナ削除
    public void removeListener(SomeListener l) {
        list.remove(l);
    }
    // イベントリスナにイベントを通知する。
    public void fireEvent(int data) {
        for(Iterator<SomeListener> it = list.iterator(); it.hasNext(); ) {
            it.next().handleData(data);
        }
    }
}

イベントリスナのhandleDate()メソッド内で、イベントリスナリストの構造を変更するようなメソッドを呼び出すとConcurrentModificationExceptionが発生します。そのような例を次に示します。

ListenerRegistry registry = ListenerRegistry.getInstance();
registry.addListener(new SomeListener() {
    @Override
    public void handleData(int data) {
        // なんらかのデータ処理
        System.out.println("handleDate:" + data);
        
        // 最後に自身を削除
        ListenerRegistry.getInstance().removeListener(this);
    }
});

registry.fireEvent(123);    // ConcurrentModificationException

イテレーションの中でエイリアンメソッドを呼び出す場合は、メソッドの一般契約として イベントリスナリストの構造を変更するメソッドの呼び出しを禁止するよう文書化するという方法も考えられますが、それでは強制力がありません。

このような場合は 次のような方法で対策することが可能です。ListenerRegistryのfireEvent()メソッドで イベントリスナリストの要素を走査する前に リストの複製を作成して、複製の方を走査するようにします。これによって、エイリアンメソッドでイベントリスナリストの構造を変更しても 問題なく動作することができます。

class ListenerRegistry {

    public void fireEvent(int data) {
        // イベントリスナリストの複製を作成。
        List tmp = new ArrayList<>(list)
        // 複製したリストを走査する。
        for(Iterator it = tmp.iterator(); it.hasNext(); ) {
            it.next().handleData(data)
        }
    }

    // ・・・ 他は同じ。
}

これと同様の効果をもっと簡単に実現することもできます。ListenerRegistryのlistフィールドの実装クラスをArrayListからjava.util.concurrent.CopyOnWriteArrayListに置き換えることによって同じ効果を実現できます。CopyOnWriteArrayListは、リストの構造を変更する際には、内部配列の複製を作成して 複製に対して操作を行います。そのため、走査中にadd()やremove()等の構造変更メソッドが呼ばれた場合、走査は引き続き古い内部配列に対して行い、構造変更メソッドは新しい内部配列に対して行うため、干渉せずに動作することができます。

CopyOnWriteArrayListは構造変更のたびに内部配列全体の複製を作成するため、変更が頻繁に行われるような場合には適していません。しかし、イベントリスナリストのように 構造変更の頻度が低く 走査がほとんどで、且つ 走査中に構造変更が行われるような場合にピッタリのCollectionです

拡張for文(for-eachループ)

for(型 変数 : 配列かCollectionなどのIterable) {
    変数を用いた処理:
}

拡張for文for-eachループとも呼ばれます)は 配列やCollectionの各要素を走査する場合に向いています。従来のfor文のようにループインデックス変数やイテレータ変数が不要なため、コードがすっきりとして 変数の記述間違い等によるバグを防ぐことができます。拡張for文のターゲットには 配列の他に CollectionなどのIterableインタフェースを実装したクラスのオブジェクトを指定することができます

2つのコンテナのマトリクスを作成するような場合に イテレータを使うと間違いが起こりやすくなりますが、拡張for文を使うと簡潔に書くことができます。例として {“1”, “2”, “3”}と{“A”, “B”, “C”}の2つのリストの全ての組み合わせを抽出したリストを作成してみます。

2つのイテレータを使う場合、次のように間違えてしまう可能性があります。

List<String> numList = List.of("1", "2", "3");
List<String> alphaList = List.of("A", "B", "C");

List<String> resultList = new ArrayList<>();
// イテレータを使うと誤り易い。
for(Iterator<String> i = numList.iterator(); i.hasNext();) {
    for(Iterator<String> j = alphaList.iterator(); j.hasNext();) {
        resultList.add(i.next() + "-" + j.next());
    }
}
System.out.println(resultList); // [1-A, 2-B, 3-C]

3×3で9個の組み合わせが抽出されることが期待されますが、実際には3個だけになっています。resultListに要素を追加する際に 内側のイテレータを進めると同時に外側のイテレータも進めてしまっているためです。イテレータを使う場合は正しくは次のようにする必要があります。

// イテレータを使う場合の正しい方法。
for(Iterator<String> i = numList.iterator(); i.hasNext();) {
    String num = i.next();
    for(Iterator<String> j = alphaList.iterator(); j.hasNext();) {
        resultList.add(num + "-" + j.next());
    }
}
System.out.println(resultList); // [1-A, 1-B, 1-C, 2-A, 2-B, 2-C, 3-A, 3-B, 3-C]

このような場合は 拡張for文を使うと簡潔に書くことができます。

// 拡張for文を使うと簡潔に書ける。
for(String num : numList) {
    for(String alpha : alphaList) {
        resultList.add(num + "-" + alpha);
    }
}
System.out.println(resultList); // [1-A, 1-B, 1-C, 2-A, 2-B, 2-C, 3-A, 3-B, 3-C]

拡張for文が使えない場合

拡張for文は便利ではありますが、次のような場合は拡張for文が使えません

  • 拡張for文の繰り返し処理の中で 元のCollectionの要素を削除する場合
    拡張for文の中で元のCollectionの要素を削除するとConcurrentModificationExceptionを発生することがあります。基本的にはループ処理内で元のCollectionの要素を削除する場合にはイテレータを用います。また、Collectionの要素を削除する場合には イテレータ使って走査をしなくても removeIf()メソッドで条件に合った要素を削除することもできます。
  • 拡張for文の繰り返し処理の中で 元の配列やListの要素を別の要素に置き換える場合
    要素の置換をしたい場合は 配列・リストのインデックスやListIteratorを使う必要があります。
  • 複数の配列やCollectionを並列に走査する場合
    そのような場合はイテレータやループインデックスを使って制御する必要があります。
  • その他 ループインデックスが必要な場合
    同じfor-eachループでも、PythonやRubyなどと違って Javaでは ループインデックスを取得することができません。繰り返し処理の中でループインデックスが必要な場合は、従来のfor文を使う必要があります。(Pythonではenumerateを使って、Rubyではeach_with_indexで for-eachループでも ループインデックスを参照することができます。)

拡張for文のループ変数を変更しても ループの流れを制御することはできない

拡張for文のループ変数を変更しても、ループの流れを制御することはできないことに注意が必要です。例えば 次の要素に対するポインタを持つLinkクラスを Listに格納して、拡張for文で Listの要素を走査する場合を考えます。ある特定の要素に対するループをスキップさせたいような場合に、ループ変数の内容を変更しても 該当の要素に対するループをスキップすることはできません。

public class Link {
    String name;
    Link next;
    
    Link(String name) {
        this.name = name;
    }

    // nextを設定した後、
    // メソッドチェーンでnextを設定できるように 設定したnextを返す。
    public Link setNext(Link next) {
        this.next = next;
        return next;
    }

    public static void main(String[] args) {
        Link one = new Link("One");
        Link two = new Link("Two");
        Link three = new Link("Three");
        one.setNext(two).setNext(three);
        
        List linkList = List.of(one, two, three);
        for(Link link : linkList) {
            if(link.name.equals("One")) {
                link = link.next;  // ループ変数の参照先を変更しても、ループの制御は変わらない。
            }
            System.out.println(link.name);
        }
    }
}

上のコードを実行すると、次のようになります。

Two
Two
Three

Link(“One”)の場合に ループ変数linkを次の要素に変更すると、linkが指すオブジェクトはLink(“Two”)に変更されますが、ループの流れには影響しません。そのため、次のループでは linkにLink(“Two”)が代入され、結果的にLink(“Two”)に対する処理が二度行われてしまいます。拡張for文のループ変数にfinalを付けることで、このような誤りを防ぐことができます

ストリーム

配列やCollectionの要素を走査する場合には イテレータの他にストリームを利用することもできます。

Collectionからストリームを生成するにはstream()メソッドを用います。Collectionのstream()でストリームを生成してから 変換・ソート・重複排除といった操作を行うことができます。(尚、CollectionインタフェースはIterableを拡張しているので、forEach()メソッドで要素を走査することもできます)

配列の場合は Arraysクラスのstream()クラスメソッドで 配列からストリームを生成することができます。

ただし、ストリームでは基本的に副作用のない関数を呼び出すのが好ましいとされていますので、元の配列やCollectionの要素を削除したり変更・置換するような処理には向いていません。

while文

while(条件式) {
    処理;
}

while文は 繰り返し回数が分からないけれども一定の条件が成立する間処理を繰り返す場合に向いています。次の do … while との違いは、条件が成立しない場合は1回も処理を行なわないことです。

while文は for文や拡張for文と違って 冒頭でループ変数を宣言することができません。そのため、ループ変数が必要な場合は while文の外で宣言する必要があり、while文が終了した後もループ変数は残ります。同じスコープ内に別のwhile文があるような場合に、誤って前のwhile文のループ変数を参照してしまうようなバグが混入する可能性があります。そのため、ループ変数が必要で ループが終了した時点でループ変数が不要となる場合は while文よりfor文・拡張for文を使う方が好ましいです。

int i = 10;
while(i > 0) {
    // 何らかの処理...
    i --;
}

int j = 10;
while(i > 0) {  // jにするところを コピペでiにしてしまったため、1回も実行されない。
    // 何らかの処理...
}

do … while文

do {
    処理;
} while(条件);

do … while文もwhile文と同様に 繰り返し回数が分からないけれども 一定の条件が成立する間処理を繰り返す場合に向いています。上のwhile文との違いは、条件がfalseの場合でも最初の1回は必ず処理を行うことです。

尚、perl等の言語ではdo … while文とは逆に 条件が成立するようになるまで繰り返すdo … until文があり、do … while文を置き換えたりできますが、Javaにはdo … until文はありません。

break

ループを途中で抜けるにはbreakを使います。

for(int i = 0; i < 10; i ++) {
    if(i > 5) {
        break;
    }
}

ラベル付きbreak

ラベル付きbreakを使うとネストされたループを一気に抜けることができて便利です

OUT_LOOP:
    for(int i = 0; i < 10; i ++) {
        for(int j = 0; j < 10; j ++) {
            if((i * j) > 20) {
                break OUT_LOOP;
            }
        }
    }

ラベル付きbreakは ループ以外でもブロックを途中で抜けたい場合にも使うことができます(switchのbreakもループを抜けるのではなくブロックを抜けるために使われます)。これは ブロック部分を別のメソッドとして抜き出して、条件を満たさない場合は途中でメソッドを抜けるのと同じ制御フローを実現することができます。

BLOCK:
    {
        処理1;
        処理2;
        if(後続の処理3, 4を実行したくない場合) {
            break BLOCK;
        }
        処理3;
        処理4;
    }

continue

繰り返し処理の中で 後続の処理を行わず次のループカウンタに進むにはcontinueを使います。

for(int i = 0; i < 10; i ++) {
    if(i == 5) {
        continue;
    }
}

ラベル付きcontinue

breakと同様にラベル付きcontinueも使えます。ただしbreakの場合と異なりループの中でしか使えません

LOOP:
    for(int i = 0; i < 10; i ++) {
        for(int j = 0; j < 10; j ++) {
            if((i * j) % 5 == 0) {
                continue LOOP;
            }
        }
    }

// ラベル付き continue はループの中でしか使えない。
// 以下はコンパイルエラーになる。
//BLOCK:
//{
//    if(true) {
//        continue BLOCK;
//    }
//}