プログラミング応用b 第9回『コレクション』 |
【Vectorクラスの問題点】
Vectorクラスは,高機能配列として非常に便利ですが,2点ほど問題があります。
・Vectorの問題点その1:データはすべてObject型オブジェクトとして格納するために,int型やdouble型などのプリミティブ型は格納できない。
・Vectorの問題点その2:データはすべてObject型オブジェクトとして格納するために,格納したオブジェクトを取り出して利用する際に元の型に変換しなくてはならない。
このことについて,解決策を示します。
【プリミティブ・ラッパークラス】
まず,最初の問題点である
・Vectorの問題点その1:データはすべてObject型オブジェクトとして格納するために,int型やdouble型などのプリミティブ型は格納できない。
について見てみましょう。実際に以下の様にint型定数をベクタに登録しようとすると(Java SE 5.0より前のバージョンでは)エラーになります。
VectorTest2.java
import java.util.Vector; class VectorTest2 { public static void main( String args[ ] ) { Vector v = new Vector( ); v.addElement( 10 ); // ベクタに登録できるのはオブジェクト(の参照値)のみなのでエラーになる } }解決方法として,プリミティブ型をオブジェクト型としてベクタに登録するという方法があります。各プリミティブ型には下表のように,
プリミティブ型 | プリミティブラッパークラス |
---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
char |
Character |
boolean |
Boolean |
int型に対応するプリミティブラッパークラスのオブジェクトのイメージは下図のようなものです。つまり,プリミティブラッパークラスの
オブジェクトは,プリミティブ型のデータを内部に持っていて包んで(ラップ)いるのです。プリミティブ型の値をプリミティブラッパークラス型
オブジェクトに包むことをボクシング(boxing)と言います。まさに「箱に入れる」という感じです。逆にプリミティブラッパークラス型オブジェ
クトに包まれたプリミティブ型の値を取り出すことをアンボクシング(unboxing)と呼びます。
プリミティブラッパークラスのオブジェクトに内包させたい値の設定はコンストラクタで行います(後述しますがこの方法は後に非推奨になりました)。
値の取り出しは,プリミティブ型の型名を x とすると xValue( ) というメソッドで行うことが出来ます。int型のプリミティブラッパークラス
Integer型の場合,値の取り出しは intValue( ) というメソッドで行えるわけです。
実際に,Integer型オブジェクトとして,整数値10をベクタに格納した例を以下に示します。8行目で, 型キャスト演算子(プログラミング応用a
第12回で学習)を使って,ベクタvから取り出したオブジェクト(への参照値)の型を Object からInteger に変換している事に注意して下さい。
VectorTest3.java
import java.util.Vector; class VectorTest3 { public static void main( String args[ ] ) { Vector v = new Vector( ); v.addElement( new Integer( 10 ) ); // Integer型オブジェクトとしてベクタに登録する。 Integer integer = (Integer) v.elementAt( 0 ); // ベクタから取り出したオブジェクトを Object型から Integer型に戻して,変数 integerに格納する。 System.out.println( integer.intValue( ) ); } }
【コラム】Java SE 5.0 で導入されたオートボクシング,オートアンボクシング
Java SE 5.0 からボクシング(boxing)とアンボクシング(unboxing)を自動で行うオートボクシングとオートアンボクシングという機能が
使用可能になりました。
オートボクシングは,プリミティブ型からプリミティブラッパークラス型への自動的な変換として動作するように見えます。例えば
int型→Integer型へ自動的に変換されるように見えるわけである。プリミティブラッパーオブジェクトを『プリミティブ型データを入れる箱』
と見立てれば,オートボクシングは『自動箱詰め』と言える。ここで,プリミティブラッパーオブジェクトは自動的に生成・初期化されます。
Integer a = 10; // int型の10という値がInteger型に変換されているように見える。実際の動作は,Integer a = new Integer( 10 ); という処理が行われている。
void f( Integer v ) { }
f( 10 ); // int型の10という値がInteger型に変換されているように見える。実際の動作は,f( new Integer( 10 ) ); という処理が行われている。
逆に,オートアンボクシングは,プリミティブラッパークラス型からプリミティブ型への自動的な変換として動作するように見えます。例えば
Integer型→int型へ自動的に変換されるように見えるわけです。プリミティブラッパーオブジェクトを『プリミティブ型データを入れる箱』
と見立てれば,オートアンボクシングは『自動開封』と言えます。
// 下の例では,Integer a = 10; の部分で int型→Integer型の自動変換が行われているように見えるが,実際にはオートボクシングが実行されている。
// 下の例では,int b = a; の部分で Integer型→int型の自動変換が行われているように見えるが,実際にはオートアンボクシングが実行されている(aから取り出された10がbの初期値に用いられる)。
Integer a = 10; int b = a;
前述したように,プリミティブラッパーオブジェクトに値を設定する方法は,当初はコンストラクタで特定の値に初期化する形をとっていましたが,
オートボクシング・オートアンボクシングの導入等により,Java 9 (2017)からは,プリミティブラッパーオブジェクトに値を設定するのにコンスト
ラクタを使用する方法は非推奨となり,オートボクシングによって値を設定するか,staticメソッド valueOf を使うように推奨されています。
import java.util.Vector; class IntegerTest2 { public static void main( String args[ ] ) { // 注:Java J2SE 5.0 からオートボクシング(auto boxing)機能・アンボクシング(auto-unboxing)が追加され, // 元のプリミティブ型←→プリミティブラッパークラス型 の相互自動変換が可能になった。 // つまり,Integer i = 10; int j = i; というコードを書ける様になった(前者は int→Integerの自動変換,後者はInteger→intの自動変換)。 Integer i = 10; // オートボクシング int j = i + 90; // i からint型の値10がオートアンボクシングされ,90と加算されて j にint型の値100がオートボクシングされる。 System.out.println( i ); // 10と表示。 System.out.println( j ); // 100と表示。 System.out.println( j + 1 ); // 101と表示。j からint型の値100がオートアンボクシングされ,1と加算されて 100 と表示される。 // また,Object型が望まれる場所にプリミティブ型の値を書くと,プリミティブ型→プリミティブラッパークラス型→Object型 // という一連の変換が自動で行われる。これは,Object型がくるべき場所にプリミティブ型の値を書くと,この自動変換が起こることを意味する。 // たとえば,Object o = 10; というコードが書け,これは Object o = new Integer( 10 ); と同じ意味となる。 // そして,int s = (Integer)o + 1; というコードも可能になる。 // // そのため,Java J2SE 5.0 以降では,22行目の v.addElement( 10 ); は,addElementメソッドの仮引数型が Object であるため, // v.addElement( new Integer( 10 ) ); と同じ意味となり,エラーにならない。 Vector v = new Vector( ); v.addElement( 10 ); // J2SE 5.0 以降ではエラーにならない。プリミティブ(int)型→プリミティブラッパークラス(Integer)型→Object型 の変換。 Object o = 10; // J2SE 5.0 以降ではエラーにならない。プリミティブ型→プリミティブラッパークラス型→Object型 の変換。 System.out.println( (Integer)o + 1); // Object型からInteger型へは自分で戻してやる。 } }このオートボクシング・オートアンボクシングにより,Vectorでプリミティブラッパークラスのオブジェクトを使わなくてはならない面倒くささは
【ジェネリック(総称)クラス】
ベクタの第2の問題点は,
・Vectorの問題点その2:データはすべてObject型オブジェクトとして格納するために,格納したオブジェクトを取り出して利用する際に元の型に変換しなくてはならない。
ということです。前掲の IntegerTest.java でも,ベクタに一度登録したオブジェクトは Object型なので,いちいち Integer型に
戻してから利用しています。
この問題を解決できる便利な方法が,ジェネリック(総称)クラスと呼ばれる特殊なクラスです(下図)。
端的に言うと,ジェネリッククラスは仮のデータ型名(これを総称型と呼ぶ)を使ってクラスを定義しておき,必要に従って,実際の型
を指定するというものです。使用例を以下に示します。
GenericTest.java
class MyClass< T > { // ジェネリッククラスの定義 T field; MyClass( T t ) { field = t; } T getField( ) { return field; } } class GenericTest { public static void main( String args[ ] ) { // Integer型用の MyClass を使用する。 MyClass<Integer> mi = new MyClass<Integer>( new Integer( 10 ) ); System.out.println( mi.getField( ).intValue( ) ); // Double型用の MyClass を使用する。 MyClass<Double> md = new MyClass<Double>( new Double( 1.23 ) ); System.out.println( md.getField( ).doubleValue( ) ); } }
なお, interface についても同様に,
ジェネリック(総称)なinterfaceも作成することができる。定義方法と使用例を以下に示す。
// 「E型の値に関するゲッタとセッタを持っている」ことを表すインタフェイスContainable<E> interface Containable<E> { E get( ); void set( E e ); } // インタフェイスContainable<E>をInteger型用にしたContainable<Integer>を実装したクラスの例。 class IntegerContainer implements Containable<Integer> { Integer e; public Integer get( ) { return e; } public void set( Integer e ) { this.e = e; } public IntegerContainer( Integer e ) { this.e = e; } }
【ジェネリッククラス Vector<E>】
実は,Vectorクラスも,ジェネリッククラスのバージョン Vector<E> が用意されています。Vector<E>の E はベクタに格納する
要素の型です。ジェネリッククラスバージョンの Vector<E> を使えば,オブジェクトを元の型のまま(Object型にせずに)格納して,
元の型まま取り出すことができます。
前回のベクタの使用例を Vector<E> を使って書き換えた例を以下に示します。
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.Vector; // for Vector<E> public class Reverse4 { public static void main( String args[ ] ) throws IOException { FileReader fr = null; BufferedReader br = null; try { final int arrayN = 50; // ベクタオブジェクトbuffers(初期容量50) Vector<String> buffers = new Vector<String>( arrayN ); // String型用のベクタ Vector<String> を使用する。 String str; fr = new FileReader( "weapon.csv" ); br = new BufferedReader( fr ); while( ( str = br.readLine() ) != null ) { // ベクタにデータを挿入 buffers.addElement( str ); // addElement と同じ機能を持つ add メソッドを使って,buffers.add( str ); としても良い。 } for( int i = buffers.size() - 1; i >= 0 ; i-- ) System.out.println( buffers.elementAt( i ) ); } catch( IOException ioe ) { System.out.println( ioe.toString() ); } catch( ArrayIndexOutOfBoundsException aiobe ) { // 添字範囲逸脱の例外を処理 System.out.println( aiobe.toString() ); } finally { if( br != null ) br.close(); if( fr != null ) fr.close(); } } }
Vector以外にも便利なコレクションクラスはジェネリッククラスとして定義されていますので,
ジェネリッククラスバージョンを使用するようにしましょう。
【ジェネリッククラス定義上の制限】
この様に,ジェネリッククラスは,データ型に対して汎用的なクラス定義が出来る便利なものですが,定義するときにいくつか細かい
制限があります。今の皆さんの段階で気をつけなければならない代表的な制限は
(a)総称型に対する実際の型はクラスまたはinterfaceでなくてはならない(つまり実際の型にプリミティブ型は指定できない)。
(b)総称型を要素とする配列を生成できない。
です。
制限(a)(下図(1))に関しては,実際の型としてプリミティブ型を使いたいような場合に問題となります。対策として,プリミティブ型の代わりに
前述したプリミティブ・ラッパークラスを使えば解決します(下図(2))。なお,この制限は,前述したジェネリックインタフェイスにも存在します。
制限(b)(下図(1))の対策としては,下図(2)のように,ジェネリッククラスでは総称型の要素を格納したい配列はObject型の配列
とすることです。
なお,そうのような対策をした場合,下図(2)の例にあるように, Object型から総称型(下図の例ではE)への変換を行いますが,
その様な処理を行うメソッドの定義の前にアノテーションとして @SuppressWarnings("unchecked") と書かないと警告が出
てしまいます。
【ジェネリッククラス ArrayList<E>】
前述で軽く触れましたが,Vector型オブジェクトは,複数のスレッド(スレッドに関しては次回詳しく説明)から同時に利用されても安全
である(スレッドセーフと言う)ように定義されています。しかし,その分,処理は低速になってしまっています。
Java には,Vectorと同じ様に使える高機能配列として java.util.ArrayList クラスが用意されています。ArrayList はスレッド
セーフではない分,高速で,現在では Vector より ArrayList を使う方が主流になっています。
もちろん,ArrayListには Object型を格納する ArrayList と,ジェネリッククラスの java.util.ArrayList<E> が用意されています。
高機能な「配列」,つまり,要素へのランダムアクセス可能なコレクションです。ArrayList はスレッドセーフではない分,高速で,現在
では Vector より ArrayList を使う方が主流になっています。 ArrayList, ArrayList<E> の仕様については,こちらを参照して下さい。
以下は,ArrayList <E> の使用例です。
import java.util.ArrayList; class ArrayListSample { public static void main( String[ ] args ) { ArrayList<Integer> ai = new ArrayList<Integer>(); // ArrayList<Integer>型を使用している。 for( int i = 0; i < 10; i++ ) { ai.add( new Integer( i ) ); // Integer型要素を追加 } for( int i = 0; i < ai.size(); i++ ) { // size()メソッドで有効要素数を得ている点に注意 System.out.println( ai.get( i ) ); // get()メソッドで指定された要素を取得している。 } } }