Java初心者が失敗しがち/間違えがちなこと5選

Java初心者が失敗しがち/間違えがちなこと

静的型付け言語の中では、長年多くの支持を集めているJava。堅実で扱いやすく、初心者にとっても学習のしやすいプログラミング言語です。

しかし、そんなJavaにもいくつかの間違いやすいポイントがあります。

今回はJava初心者が間違えそうな箇所を5つピックアップしました。

1. Arrays.asList()でつくったリストにaddしようとしてしまう

配列からリストを作りたい時に便利なArrays.asList()ですが、このメソッドで作成したリストは、新しく要素を追加したり、要素を削除したりすることができません。

List<String> list = Arrays.asList("foo", "hoo");
list.add("hoge");
// ▲エラー出力:java.lang.UnsupportedOperationException

このように、Arrays.asList()で作成したリストの構造を変化させようとすると、UnsupportedOperationExceptionが出力されてしまうのです。Javaのリストは実行時に長さが決まる「可変長配列(要素の追加や削除を自由に行える配列)」のはずなのに、なぜadd()でエラーが出るのでしょうか?

答えは、Arrays.asList()で作成したリストは、リストでありながら固定長配列として扱われるためです。

Arrays.asList()は配列をリストに変換するメソッドですが、この時、配列自身の固定長の性質は変換されずにそのまま残ります。そのためArrays.asList()で作成したリストには、要素の追加や削除を行えないのです。

2. List.subList()が親リストの部分参照を返すことを知らずにaddしてしまう

Arrays.subList()メソッドは、メソッド実行もとのリストの一部分を「参照として」抜き出します。

参照としてなので、subList()が返却したリストに要素を追加すると、元となったリストにも同じく要素が追加されてしまいます。

List<String> l = new ArrayList<>(Arrays.asList("1", "2", "3", "4", "5"));
List<String> sl = l.subList(2, 4);

System.out.println("初期リスト:" + l);
System.out.println("subListで抜き出したリスト:" + sl);

// subListで抜き出したリストに要素追加
sl.add("hoge");

System.out.println("hoge追加後の「subListで抜き出したリスト」:" + sl);
System.out.println("hoge追加後の初期リスト:" + l);
/**
* ### 出力結果
* 初期リスト:[1, 2, 3, 4, 5]
* subListで抜き出したリスト:[3, 4]
* hoge追加後の「subListで抜き出したリスト」:[3, 4, hoge]
* hoge追加後の初期リスト:[1, 2, 3, 4, hoge, 5]
*/

このように、subList()で作成したリストに追加した”hoge”は、元のリストの同じ場所に挿入されています。subList()は部分的に抜き出した新しいリストを作成するのではなく、元のリストの部分的なビューを返すのです。

ビューを返す仕様は、元の配列の一部分のみをソートしたいときに便利ですが、別のリストとして扱いたい場合には不都合です。

元のリストと異なるリストを作成したい場合には、以下のように新たなListオブジェクトを作成すると良いでしょう。

List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3", "4", "5"));
List<String> sub = l.subList(2, 4);
List<String> otherList = new ArrayList(sub);
otherList.add("hoge");
System.out.println("subListから切り離した別オブジェクト:" + otherList);
System.out.println("subListを作成した大元のリスト:" + list);
// subListから切り離した別オブジェクト:[3, 4, hoge]
// subListを作成した大元のリスト:[1, 2, 3, 4, 5]

3. private変数を継承先から参照しようとしてしまう

Javaには「public」「protected」「private」の3つのアクセス修飾子があります。

このうちもっとも範囲を限定するprivateがついたメンバを、クラス外から呼び出すことが基本不可能であることは、ほとんどの方がご存知かと思います。

ですが、継承したときのアクセス可否はどうでしょう? 子クラスは親クラスのprivateメンバにアクセスできるでしょうか?

実際にやってみましょう。

// ▼Human.java
public class Human {
    private String name;
    private int age;
    public Human(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

// ▼Sig.java (Humanを継承した子クラス)
public class Sig extends Human {
    private String address;
    public Sig(String name, int age, String address) {
        super(name, age);
        this.address = address;
    }
    public void talk() {
        System.out.println("'name' is " + this.name + ", 'address' is " + this.address);
        // ▲コンパイルエラー:The field Human.name is not visible
    }
    public void plusOneCurrentAge() {
        super.age += 1;
        System.out.println(super.age);
        // ▲コンパイルエラー:The field Human.age is not visible
    }
}

このように親クラスHumanのprivate変数”name”, “age”は、子クラスSigからは不可視であり、コンパイルエラーが発生してしまいます。子クラスは親クラスのprivateメンバには、基本的にアクセスできないのです。

privateは外部からメンバを隠蔽し、作成したクラスを安全に使えるようにしてくれる場合もあります。しかし、なんでもかんでもprivateにすると、拡張性のないコードになってしまいます。

アクセス修飾子は「なぜここでprivateにするのか」「なぜここはprotectedにするのか」を意識して、適切に選択しましょう。

4. コンストラクタの後の波括弧の正体を読み違える

Javaでオブジェクトを作成する時のもっとも基本的な文法は「”new” + クラス名 + “;”」です。

この構文は他の多くの言語でも類似する書き方なので、特に混乱することはないと思います。しかし中には、こんな書き方があるのをご存知でしょうか。

List<String> l = new ArrayList<String>(){
    private static final long serialVersionUID = 1L;
    {
        this.add("foo");
        this.add("hoo");
    }
};

コンストラクタのあとに波括弧(”{…}”)がありますね。そして、波括弧の中にはさらに波括弧があります。

Java初心者の方はこの文法自体を知らず、何かの書き間違えだと思い込みがちです。しかし実際には、これらはれっきとしたJavaの文法であり、それぞれ以下のように呼ばれています。

  • ひとつ目の波括弧:匿名クラス
  • ふたつ目の波括弧:インスタンスイニシャライザ

匿名クラスとは、文字通り「名前の存在しないクラス」です。無名クラスとも呼ばれます。「ベースとなるクラス + 波括弧 + 実装 」という文法で「ベースとなるクラスの子クラス」を匿名で実装できます。上記コードではコンストラクタ後のひとつ目の波括弧で、ArrayListを継承する匿名クラスをその場で実装しているのですね。

そして、ひとつ目の波括弧内にあるもうひとつの波括弧では、インスタンスイニシャライザを定義しています。インスタンスイニシャライザとは「そのクラスのコンストラクタの実行直前」に実行される処理です。継承関係を含めると、以下のような実行順序になります。

  1. 親クラスのインスタンスイニシャライザ実行
  2. 親クラスのコンストラクタ実行
  3. 子クラスのインスタンスイニシャライザ実行
  4. 子クラスのコンストラクタ実行

インスタンスイニシャライザの実行順について、実際にコードでみてみましょう。

public class Foo {
    {
        System.out.println("親イニシャライザ実行");
    }
    public Foo() {
        super();
        System.out.println("親コンストラクタ実行");
    }
}

public class Hoo extends Foo {
    {
        System.out.println("子イニシャライザ実行");
    }
    public Hoo() {
        super();
        System.out.println("子コンストラクタ実行");
    }
}

Hoo h = new Hoo();
// 出力:
// 親イニシャライザ実行
// 親コンストラクタ実行
// 子イニシャライザ実行
// 子コンストラクタ実行

このように、インスタンスイニシャライザは自身のコンストラクタ実行直前に実行されます。

ここまでの解説を踏まえて、もう一度最初の「コンストラクタ + 波括弧 + 波括弧」のコードを振り返ってみましょう。

List<String> l = new ArrayList<String>(){ // 匿名クラス(ここではArrayList<String>を継承する匿名クラスが作られている)
    private static final long serialVersionUID = 1L;
    { // インスタンスイニシャライザ(ここでは初期化処理として自身のリストに”foo”と”hoo”を追加している)
        this.add("foo");
        this.add("hoo");
    }
};
System.out.println(l);
// 出力:[foo, hoo]
// ▲ 匿名クラス + インスタンスイニシャライザのおかげで、インスタンス作成時点ですぐに、要素の追加されたリストを取得できる

通常はリストを作成した後に要素を追加するところを、上記コードでは「匿名クラス + インスタンスイニシャライザ」を利用して、リスト作成時に要素の追加まで同時に実行しているのですね。

このように、匿名クラスとインスタンスイニシャライザを利用すると、要所要所で処理を簡略化できるのです。

5. 反復処理中のリストに要素を追加しようとしてしまう

Javaでは、「拡張for文やforEach()によって反復処理中のリスト」に要素を追加しようとすると、エラーが出ます。

List<String> list = new ArrayList<>(Arrays.asList("foo", "hoo", "hoge"));
for (String str : list) {
    list.add(str);
}
// 例外発生:ConcurrentModificationException

ここで発生するConcurrentModificationExceptionは、主にコレクションの同時変更が検出されたときに投げられる例外です。Javaのコレクションは、反復処理中の他スレッドからの構造変更を保証しないようになっており、一部のクラスを除いて、構造変更が検出された時点で例外を投げるようになっているのです。

同時変更の検出と聞くと、一見マルチスレッドでのみ発生する例外のようにも思えますが、実際には上記コードのようにシングルスレッドでも発生します。

より安全な挙動を保証するために、シングルスレッド / マルチスレッドを問わず、反復処理中のコレクションのイテレータは、自身に構造変更があるかどうかを繰り返しの度にチェックしているのです。

なお、イテレータの外部から変更を加えた場合には、上記コードのようにConcurrentModificationExceptionが発生します。しかし以下のように、イテレータの内部から構造変更を行った場合には例外は発生しません。

List<String> list = new ArrayList<>(Arrays.asList("foo", "hoo", "hoge"));
for (ListIterator<String> itr = list.listIterator(); itr.hasNext();) {
    String str = itr.next();
    itr.add(str + "_itr");
}
System.out.println(list);
// [foo, foo_itr, hoo, hoo_itr, hoge, hoge_itr]

このときListIterator.add()は「次要素の直前」に要素を挿入するため、反復処理の順番には影響を及ぼしません。ListIterator.add()で追加された要素は、反復処理の次のターゲットになることはないのです。

まとめ

静的型付け言語の代表格ともいえるJavaは、堅実な言語と評されるだけあって、いわゆる「罠」と呼ばれるような分かりづらい仕様は少ないです。文法的に間違っているコードを書いたとしても、コンパイル時にエラーが出力されるので、どこが間違っていたのかがすぐに分かるようになっています。

しかしコンパイルエラーの出ない「挙動の読み違い」は、あらかじめ仕様を知らなければ、実際に実行するまで何が間違いなのかが分かりません。今回ピックアップしたもののうち、リストまわりの操作はこれに当たります。

各メソッドの仕様を正確に理解して、的確なコードの実装を心がけましょう。

Java初心者が失敗しがち/間違えがちなこと

▲ 余談ですが、Javaの名前は”ジャワコーヒー(java coffee)”が由来です

こちらもおすすめ!▼

SHARE

  • 広告主募集
  • ライター・編集者募集
  • WorkshipSPACE
エンジニア副業案件
Workship