XMLEncoderで保存する
オブジェクトの保存方法
データを保存・読み込みする機能はなくてはならないものです。これができないと入力したデータも水の泡ですから。
Javaにはもともとシリアライズ(srialization)という機能が有りました。これはオブジェクト(データ)をバイトストリームに変換して、それをファイルに書き出すというものです。その逆の手順でオブジェクトをファイルから読み込むこともできます。
これはJava.io.Serializableインタフェースと、シリアライズ・デシリアライズを実行するjava.io.ObjectOutputStreamとjava.io.ObjectInputStreamを使って実現されます。
しかし、この方法には大きな問題点がありました。これまで運用されていたデータに新しいフィールドを追加するなどして、それを読み書きできるアプリケーションもバージョンアップしたとしましょう。なんとこの新バージョンのアプリケーションは古いデータを読み込めなくなってしまうのです。バイナリ形式では、新しい形式のクラスが古い形式のオブジェクトを知る由もないからです。
しかし、古いアプリケーションが新しいデータを読み込めないのは仕方ありませんが、その逆が許されるはずはありません。
この問題を解決するために、JCP(Java Community Process)は、オブジェクトをXML形式で保存するという全く新しい方法を採用し、その機能がJ2SE1.4から搭載されました。
この機能はjava.beans.XMLEncoderとjava.beans.XMLDecoderによって実現されます。これらの使い方は意外に簡単です。XMLEncoderで保存されるクラスは、JavaBeansの仕様に従っていればいいのです。それは次の2つのルールを守ることです。
保存・読み込みしたいプロパティ(フィールド)について、
set<プロパティ名>という形のセッターメソッドと
get<プロパティ名>という形のゲッターメソッドを用意する。
ただし、boolean型のプロパティの場合、ゲッターメソッドは
is<プロパティ名>でもかまいません。
例えば、String型プロパティtextの場合は、
public void setText(String text)というセッターメソッドと
public String getText()というゲッターメソッドになります。
boolean型プロパティenabledの場合は
public void setEnabled(boolean enabled)というセッターメソッドと
public boolean getEnabled()または
public boolean isEnabled()というゲッターメソッドなります。
もう一つのルールはpublicで引数無しのコンストラクタ、いわゆるデフォルトコンストラクタを用意することです。
保存されたオブジェクトを読み込むXMLDecoderは、まずデフォルトコンストラクタを呼び出して、プロパティ未設定のオブジェクトを生成します。それからセッターメソッドを呼び出して、プロパティを設定していきます。こうして元通りのオブジェクトができるわけです。
尚、従来のシリアライズ機能と違って、保存されるクラスはSerializableインタフェースを実装する必要はありません。
では実例を見ていきましょう。まずは今回保存対象となるデータを表すクラスです。
先ほどの2つのルールに従っていることを確認してください。
Goods.java
public class Goods { private String name; private int price; private boolean discounted; public Goods(){//デフォルトコンストラクタ } public Goods(String name, int price, boolean discounted){ this.name=name; this.price=price; this.discounted=discounted; } //アクセッサ public void setName(String name){ this.name=name; } public String getName(){ return name;} public void setPrice(int price){ this.price=price; } public int getPrice(){ return price; } public void setDiscounted(boolean discounted){ this.discounted=discounted; } public boolean isDiscounted(){ return discounted; } }
次は実行用のクラスです。今回は保存するデータに構造性を持たせるため、Goodsオブジェクトを要素とする配列の形にまとめました。
XMLEncoderTest.java
import java.beans.*; import java.io.*; public class XMLEncoderTest { public static void main(String[] args){ Goods[] goodsArray=new Goods[3]; goodsArray[0]=new Goods("book", 2000, false); goodsArray[1]=new Goods("DVD", 3990, true); goodsArray[2]=new Goods("software", 6800, true); try{ XMLEncoder encoder=new XMLEncoder( new BufferedOutputStream( new FileOutputStream("goods.xml"))); encoder.writeObject(goodsArray);//配列をエンコード encoder.close();//エンコーダとストリームを閉じる }catch(FileNotFoundException ex){ System.err.println(ex); } } }
tryブロックの中を見ていただければ、XMLEncoderの使い方が分かると思います。オブジェクトを書き出したら、XMLEncoderオブジェクトを閉じるのを忘れないでください。
しかし、たったこれだけで複雑なデータも保存してくれるのですから有り難いものですね。
では実行してみましょう。次が書き出されたgoods.xmlファイルの中身です。実は一番注目してほしいのはこれなんです。
goods.xml
<?xml version="1.0" encoding="UTF-8"?> <java version="1.5.0_13" class="java.beans.XMLDecoder"> <array class="Goods" length="3"> <void index="0"> <object class="Goods"> <void property="name"> <string>book</string> </void> <void property="price"> <int>2000</int> </void> </object> </void> <void index="1"> <object class="Goods"> <void property="discounted"> <boolean>true</boolean> </void> <void property="name"> <string>DVD</string> </void> <void property="price"> <int>3990</int> </void> </object> </void> <void index="2"> <object class="Goods"> <void property="discounted"> <boolean>true</boolean> </void> <void property="name"> <string>software</string> </void> <void property="price"> <int>6800</int> </void> </object> </void> </array> </java>
配列はarray要素、オブジェクトはobject要素、プロパティはvoid要素として書き出されているのがお分かりいただけるでしょうか。index="1"とindex="2"のオブジェクトには次の記述が有ります。
<void property="discounted"> <boolean>true</boolean> </void>
そうすると、index="0"のオブジェクトには、
<void property="discounted"> <boolean>false</boolean> </void>
という記述が有りそうなものですが、見当たりませんね。これはなぜでしょう。
XMLDecoderがオブジェクトを復元するときのことを考えてみましょう。デフォルトコンストラクタではフィールドの値を明示的に設定していないので、コンストラクタを呼び出した時点ではdiscountedはデフォルト値のfalseに初期化されています。つまり、falseの場合はわざわざ保存しなくても、復元するときに自動的にfalseになるから問題ないのです。trueの場合はこの後セッターでtrueに設定されるわけです。
尚、フィールドの値を明示的に設定しないコンストラクタが呼び出された直後は、String型のnameはデフォルト値のnullに、int型のpriceはデフォルト値の0に初期化されます。
仮にデフォルトコンストラクタを次のように書き換えてGoods.javaをコンパイルしてから、XMLEncoderTestをもう一度実行したらどうなるでしょう。
public Goods(){//デフォルトコンストラクタ discounted=true; }
今度はindex="1"とindex="2"のdiscountedは書き出されず、index="0"のdiscountedだけがfalseとして書き出されるのです。
以上のことから分かるように、デフォルトコンストラクタで復元されるプロパティはわざわざ保存しないようになっているのです。非常に効率的にできていますね。
オブジェクトの復元方法
では書き出したデータを読み込むクラスを見てみましょう。
XMLDecoderTest.java
import java.beans.*; import java.io.*; public class XMLDecoderTest { public static void main(String[] args){ Goods[] goodsArray; try{ XMLDecoder decoder=new XMLDecoder( new BufferedInputStream( new FileInputStream("goods.xml"))); goodsArray=(Goods[])decoder.readObject();//配列をデコード decoder.close();//デコーダとストリームを閉じる }catch(FileNotFoundException ex){ System.err.println(ex); return; } for(int i=0; i < goodsArray.length; i++){ System.out.println("name: "+goodsArray[i].getName() +" price: "+goodsArray[i].getPrice() +" discounted: "+goodsArray[i].isDiscounted()); } } }
XMLEncoderのときと同じようにXMLDecoderオブジェクトを閉じるのを忘れないでください。
実行すると以下のようにデータが復元されているのが確認できます。
XMLDecoderTestの実行結果
name: book price: 2000 discounted: false name: DVD price: 3990 discounted: true name: software price: 6800 discounted: true
バージョンアップへの対応
最後にもう一つ気になる実験をしてみましょう。Goodsにint型のプロパティrateを追加して、引数付きのコンストラクタを書き換え、セッターメソッドとゲッターメソッドも追加してください。次のようになります。
Goods.java新バージョン
public class Goods { private String name; private int price; private boolean discounted; private int rate; public Goods(){//デフォルトコンストラクタ } public Goods(String name, int price, boolean discounted, int rate){ this.name=name; this.price=price; this.discounted=discounted; this.rate=rate; } //アクセッサ public void setName(String name){ this.name=name; } public String getName(){ return name;} public void setPrice(int price){ this.price=price; } public int getPrice(){ return price; } public void setDiscounted(boolean discounted){ this.discounted=discounted; } public boolean isDiscounted(){ return discounted; } public void setRate(int rate){ this.rate=rate; } public int getRate(){ return rate;} }
これに合わせて、XMLDecoderTestもforブロックの中だけを次のように書き換えてください。
XMLDecoderTest.java新バージョン
import java.beans.*; import java.io.*; public class XMLDecoderTest { public static void main(String[] args){ ... for(int i=0; i < goodsArray.length; i++){ System.out.println("name: "+goodsArray[i].getName() +" price: "+goodsArray[i].getPrice() +" discounted: "+goodsArray[i].isDiscounted() +" rate: "+goodsArray[i].getRate()); } } }
さあ、データを表すクラスが新バージョンに変わりましたが、果たして本当にgoods.xmlから古いバージョンのオブジェクトを読み込めるでしょうか。XMLDecoderTestを実行してみましょう。
XMLDecoderTest新バージョンの実行結果
name: book price: 2000 discounted: false rate: 0 name: DVD price: 3990 discounted: true rate: 0 name: software price: 6800 discounted: true rate: 0
確かに何の問題も無く読み込めました。ここで先ほどのdiscountedの話を思い出してください。index="0"のdiscountedは保存されていないけれども、デフォルトコンストラクタによって、デフォルト値のfalseで初期化されましたね。
今回も結局同じことなのです。goods.xmlのどこにもrateというプロパティの値は記述されていません。そこでデフォルトコンストラクタは、rateをデフォルト値の0で初期化してくれたわけです。
このようにして、XMLEncoderとXMLDecoderによる保存と復元のメカニズムはクラスのバージョンアップに見事に対応してくれるのです。