Ch2. 配列とArrayList

配列

      int[] id; //配列の宣言と
      id = new  int[50]; //領域確保は分けて書ける
     System.out.println("10番目は "+ id[10]); //初期化時に0が設定されている

      int[] id2 =  {50, 60, 70, 80, 90};
      System.out.println(id2.length);

Arrayでは初期化時に長さを指定し、後からの変更は出来ない。 長さはLengthで確認する。

ArrayListではsize()を使って 値がセットされている要素の数 を求めるので、この差に注意する。

ArrayListのsize()がint型を返却するメソッドであるのに対して、Arrayのlengthはfinal宣言されたプロパティのようだ。 ArrayListにもsizeプロパティはあるが、privateとなっている。

ただし、Arrayのlengthプロパティを自分で見つけることは出来なった。

Since arrays are objects in Java, we can find their length using the object property length. This is different from C/C++ where we find length using sizeof. (Arrays in Java - GeeksforGeeks)

jdk7u-jdk/ArrayList.java at master · openjdk-mirror/jdk7u-jdk · GitHub

なお、Javaの配列はプリミティブ型ではなくれっきりとしたオブジェクトである。 下記のようなコードで、どのクラスから生成されているかがわかる。

      //ArrayListでなくても、配列はそもそも配列クラスのオブジェクトである。
      int[] arrayIsObject = new int[10];
     String clazz = arrayIsObject.getClass().getCanonicalName();
      System.out.println(clazz);//'int[]'

オブジェクトにはto_string()メソッドがあり、System.out.printlnではこれが呼び出される。 intの配列型であれば、そのオブジェクトのハッシュ値が返却される。(これは配列クラスとして実装されている訳ではなく、継承元のObjectクラス。)

jdk/Object.java at eaf4db6b8b883b1ab5731ac62f590e39a7b67210 · openjdk/jdk · GitHub

      int[] arrayIsObject2 = new int[1];
      System.out.println(arrayIsObject2);//'[I@7d6f77cc' arrayオブジェクトのto_string()メソッドより
      arrayIsObject2[0] = 1000;
      System.out.println(arrayIsObject2);//'[I@7d6f77cc' 

ArrayListクラスが、配列の中身をわかりやすく返却してくれるのに比べるとかなり挙動が異なる。

配列宣言時の書き方は2パターン認められている。

      int nameBefore[]; //気持ち悪く感じるが、配列を後置で宣言出来る。
      int[] nameAfter; 
      int[] arrayInArray[]; //これで2次元配列も宣言出来る。
      int[][] arrayInArrayInArray[]; //これもOK 3次元配列を宣言できる

配列型の変数を宣言するときは、領域確保と異なり長さは指定しない。

      int[] a2;
      a2 = new int[3]; //配列のインスタンス化(領域確保)
      //int[3] a3; //これは認められない

配列型の変数は配列インスタンスに対する参照を保持するだけ。 配列型の変数はスタックに、配列インスタンスはヒープにメモリを確保するので配列型の変数にとって配列インスタンスの大きさは関係がない。

配列は方によっては初期値がセットされるものもある。

      Something[] something = new Something[10];
      System.out.println("Object型の配列では中身の初期化は行われない。" + something[0]);//'Object型の配列では中身の初期化は行われない。null'

配列を拡張forで利用する場合、たとえ中身がnullでも拡張forの型と同じとみなして処理を進めることが出来る。

      String[] stringA = new String[10];
      stringA[0] = "helloWorld";
      for(String a1 :stringA){
        System.out.println(a1);
        //'helloWorld'
        //'null'
        //'null'
        //....
        //'null'
      }

Rubyと異なりnullはnullクラスのオブジェクトではないようだ。 なのにちゃんと、System.out.printlnでnullという文字列が表示できるのは何故なんだろう。

同じような疑問を抱く人はいるようです。(そして大体Ruby出身者らしい) java - Is null an Object? - Stack Overflow

調べてみたところ、どうやらSystem.out.printlnの方で、うまくやってくれているらしい。 詳細は分からないけど、うまいことnullPointerExceptionを出さないようにしているようだ。

jdk/PrintStream.java at master · openjdk/jdk · GitHub

配列の初期化には{}(初期化演算子という名前がついている)が利用できる。 new int[]を代わりに実施して、値をセットしてくれる。

      //{}は配列のインスタンス化を一発でやってくれる。
      int[] intArrayA = {};
      int[] intArrayB = new int[3];
      System.out.println(
        "intArrayA is : " + intArrayA.getClass().getCanonicalName() + '\n' +
        "intArrayB is : " + intArrayB.getClass().getCanonicalName()
        //'intArrayA is : int[]'
        //'intArrayB is : int[]'
      ); 

初期化演算子には2つの制限がある。

  • newを利用した初期化と併用できるが、その場合[]には長さを指定できない。
      //newを使った初期化と{}(初期化演算子)は併用できる。ただし、{}がある場合は長さの指定はできない。
      int[] intArrayC = new int[]{};
      int[] intArrayD = new int[]{1,2,3,4};
      //int[] intArrayD = new int[0]{}; //これはNG
  • 初期化演算子の利用時は、配列の次元が明示的に示されていなければならない。
      int[] intArrayF = {1,2,3,4,5};//OK
      int[] intArrayG = new int[]{1,2,3,4,5};//OK
      int[] intArrayE;
      //intArrayE = new int{1,2,3,5}; //NG

ArrayList

実装がここにあるので、読みながら進めると勉強になる。

jdk/ArrayList.java at master · openjdk/jdk · GitHub

ArraListはJavaのコレクションAPIのうちの一つでJavaAPIとして提供されている数多くのコレクションの周囲のうちの一つである。 Javaはこのほかにも様々なコレクションAPIを提供しており、HashMapとかLinkedListとかArrayDeque(両端から出し入れするキューとして利用)などが提供されている。

ArrayListはコレクションAPIの中で、次のような特徴を備えている。

  • 必要に応じて要素数は自由に増える
  • 追加した順に並ぶ
  • nullも追加できる
  • 重複した値も追加できる。
  • 任意の場所に要素を追加できる。

更にスレッドセーフではない、という特徴も備える。そのため、2つのスレッド同士で一つのArrayListを共有することは危険である。スレッドセーフなArrayListとしてCopyOnWriteArrayListが存在する。

      ArrayList<String> stringArray;//配列の宣言と
      stringArray = new ArrayList<String>(3); //領域確保は分けて書ける

ArrayListの【長さ】の概念はよくわからない。 長さの指定なしで10が初期値として設定されているらしい。

      ArrayList<String> stringArray2;//配列の宣言
      stringArray2 = new ArrayList<String>();  //長さ指定なしで自動的に10が確保される(らしい)
      System.out.println("8番目は: " + stringArray2.get(8)); //これは出来そうで出来ない。
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 8 out of bounds for length 0
        at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
        at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
        at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:248)
        at java.base/java.util.Objects.checkIndex(Objects.java:372)
        at java.base/java.util.ArrayList.get(ArrayList.java:459)
        at Ch2_1.main(Ch2_1.java:33)

(Java SE8の教科書を使っているが、自分のPCのランタイムはSE 11なのでそれが影響しているのかな...)

JavaArrayListのダイアモンドの中身はジェネリクスジェネリクスコンパイル時に型不正を見つけ出してくれるもので、無くても実行は出来る。 その場合、型指定のない配列というRubyのようなこともできる。

      ArrayList noGenericsArray = new ArrayList();
      noGenericsArray.add(100);
      noGenericsArray.add("HelloWorld");
      System.out.println(noGenericsArray.get(0));//'100'
      System.out.println(noGenericsArray.get(1));//'HelloWorld'

 //但し、取り出して変数代入する際はキャストが必要になる
      int firstElement    = (int)noGenericsArray.get(0);
      String secondElemnt = (String)noGenericsArray.get(1);

ジェネリクスで指定する型は【参照型】である必要がある。 配列が保持しているものは『値への参照(ポインタ)』だからか。

プリミティブ型の配列を使いたいときは、プリミティブ型の値への参照型である『ラッパークラス』というのを使う。

      ArrayList<Integer> intArray;//ArrayListでプリミティブ型を使う場合は値を参照型に変換する[ラッパークラス]を利用
      ArrayList<int> intArray; //<>の中は参照型(その値へのポインタ)を入れる。プリミティブ型は不可。
      ArrayList<string> stringArray// String型もラッパークラス。プリミティブなstring型は存在しない。

ジェネリクスのダイアモンドの中身は左辺に書かれている限り空白でもよい。

      ArrayList<String>  emptyDiamond = new ArrayList<> ();
      System.out.println("Size is " + emptyDiamond.size());

      ArrayList<Integer> emptydiamond2;
      emptydiamond2 = new ArrayList<>();
      //分かち書きをしていても、ダイアモンド演算子は利用できる。

ArrayList#add

addはオーバーロードされていて、コレクションの後ろにオブジェクトを追加するだけでなく、差込場所を指定して要素を追加することもできる。 Array#setとの違いは、差込場所に配列の範囲外を指定しても実行時エラーになってしまう、ということである。

要素の差込なので、当然既存の値が上書きされることはない。

       ArrayList<String> list = new ArrayList<>();
        list.add(2, "Hello");
        ///Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 2, Size: 0
       //at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:665)

差込場所の数え方は、String型のインスタンスメソッドと似ていて以下のような指定の仕方をする。

(0) 要素1 (1) 要素2 (2) 要素3

そのため、空っぽのArrayListに対しても以下のような指定が出来る。

       ArrayList<String> list = new ArrayList<>();
        list.add(0, "Hello");
        System.out.println(list);//'[Hello]'

        ArrayList<String> list2 = new ArrayList<>();
        list2.add(1, "Hello");
        System.out.println(list2); //NG

        ArrayList<String> list3 = new ArrayList<>();
        list3.add(2, "Hello");
        System.out.println(list3); //NG

ArrayList#set

値の置き換えを行うメソッドsetでは差込場所ではなく、要素の配置場所を添え字で指定できる。 ここでも、配列の要素外の値を指定した場合は例外が発生する。 勝手に配列の長さが拡張するわけではない。

       list.set(40, "Hello");
        System.out.println(list);//'Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 40, Size: at java.util.ArrayList.rangeCheck(ArrayList.java:657)'

Rubyでは下記のようなことが出来てしまうので、配列が勝手に長さを伸ばしてくれると思ってしまうが、Javaにはそのような機能はない。

array = []
array[10] = "hello"
puts array # [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "hello"]

 ArrayList#remove

removeメソッドもオーバーライドされている。 removeメソッドに配列の添え字ではなく、オブジェクト型を渡したばあい、配列の中でそのオブジェクトをequalsの関係にあるオブジェクトを削除してくれる。

equalsはオーバーライドされていなければ同一比較であるが、オーバーライドされていれば同値比較も可能なのでこれを利用してオブジェクトの削除が行える。 ただし、removeAllメソッドではないので削除されるのは最初に該当する一つだけである。

配列の要素を削除したときのループ文の挙動に関しても注意しなくてはならない。

       ArrayList<String> list = new ArrayList<>();
        list.add( "Sydney");
        list.add( "Cairns");
        list.add( "Melbourne");

        for(String a: list) {
            System.out.println(a);
            if(a.equals("Cairns")) {
                list.remove(a);
            }
        }

        System.out.println(list);//'[Sydney, Melbourne]'

        ArrayList<String> list2 = new ArrayList<>();
        list2.add( "Sydney");
        list2.add( "Cairns");
        list2.add( "Melbourne");

        for(String a: list) {
            System.out.println(a);
            if(a.equals("Melbourne")) {
                list.remove(a);//'Exception in thread "main" java.util.ConcurrentModificationException'
            }
        }

        System.out.println(list2);

先述したようにArrayListはスレッドセーフではない。マルチスレッドではなくても、ループ処理内で要素の削除を実施しようとした場合原則、ConcurrentModificationExceptionが発生する。 このエラーが発生しない場合が一つだけあり、それは【最後から2番目の要素を削除する場合】である。

なぜ、最後から2番目の要素を削除するときだけ特別扱いされるのかについては以下の記事が詳しい。

シングルスレッドでConcurrentModificationExceptionがスローされる、されないのはどのような場合か? - Qiita

全てのクラスはObjectクラスを継承しているので、オブジェクトクラスの配列を宣言しておけば、あらゆる型の値がその配列を利用できる。

      Object[] objectArray = {"HelloWorld", 2, 3, new Something()};

配列のコピーは * 配列の全ての(1次元目の)値ごと複製するclone * 配列の一部の(1次元目の)値ごと複製するarraycopy(Streamクラスのクラスメソッド)

が用意されている。

特殊な型指定

また、ジェネリクスの指定は片方だけでも文法上の誤りとはならない。

ただし右側だけ指定した場合、ジェネリクスを全く指定していないのと同じ挙動になる。

右側だけジェネリクスを指定した場合。

      ArrayList listForSomething = new ArrayList<Integer>();
      listForSomething.add(100); listForSomething.add(101); listForSomething.add(102);
      listForSomething.add("文字列");
      int a = (int)listForSomething.get(2); //ただしキャストは必要になる。
      System.out.println("int a is ...: " + a);//'int a is ...: 102'
      System.out.println(listForSomething);//'[100, 101, 102, 文字列]'

一方、左側だけジェネリクスを指定した場合は

      ArrayList<String> listForSomethig2 = new ArrayList();
      listForSomethig2.add("ユメミル"); listForSomethig2.add("チカラ"); listForSomethig2.add("HTB");
      // listForSomethig2.add(100); //コンパイルエラー
      String st = (String)listForSomethig2.get(1);
      System.out.println("string st is...: " + st);//'string st is...: チカラ'
      System.out.println(listForSomethig2);//'[ユメミル, チカラ, HTB]'

つまり左側でジェネリクスを指定しないと...

  • 型に関係なく要素を追加できる。
  • 要素の代入時にはキャストが必要。

また、ArrayList<Listの気象関係があるため、左側に継承元の型を指定しておけば右側は継承先のオブジェクトを指定できる。

    List<Integer> integerList = new ArrayList();//OK
   ArrayList<String> stringList = new List<String>();//NG

一方、ジェネリクスは親子関係関係なく、左右を必ず同じクラスとする必要がある。

      class Parent{
      }
      class Childe extends Parent{
      }
      //ジェネリクスは型の継承関係とは関係がない。必ず左右で併せる必要がある。
      ArrayList<Parent> childObject = new ArrayList<Childe>();//NG

コマンドライン実行引数は必ずString型として渡される。

      System.out.println("Length of args is " + args.length);
      System.out.println("実行引数だよ " + args[0] + args[1]); //'1020'