Tech Tips
2001
   2002
2001
   2000
   1999
   1998
   1997
 
 
Tech Tips archive

Tech Tips
2001年 4月 10日

ようこそ、Java Developer Connection(SM) (JDC) テクニカルティップへ。テクニ カルティップ 2001 年 4 月 10 日号をお届けします。本号では次のテーマについ て説明します。

これらのティップは Java(TM) 2 SDK, Standard Edition, v 1.3 を使って開発さ れています。

このテクニカルティップの英語版 (原文) は次の Web サイトでご覧いただけます。 http://java.sun.com/jdc/JDCTechTips/2001/tt0410.html


オブジェクトの「深いコピー」の作成

JDC Tech Tip 2001 年 3月 6 日号「オブジェクトの複製 (Cloning)」(http://java.sun.com/jdc/JDCTechTips/2001/tt0306.html#cloning) では、Object.clone と独自の clone メソッドを組み合わせて使用し、オブジェクトのコピーを作成する方法について説明しました。Object.clone および一部のユーザ定義 clone メソッドは、いわゆる「浅いコピー (shallow copy)」を作成します。つまり、オブジェクトのフィールドと参照はコピーされますが、それらのフィールドによって参照されるオブジェクトは再帰的にコピーされないということです。

ある ArrayList オブジェクトがあるとしましょう。その clone メソッドを呼び出すと、新しい ArrayList オブジェクトが作成され、元のリストのさまざまな要素の参照がその中にコピーされます。これらの要素が可変である、つまりこれらの要素が変更できる場合、元のリストに含まれるいずれかの要素を変更すると、新しいリストに含まれる要素も変更されます。つまり、2 つの ArrayList オブジェクトは、共有された参照を介して共通の要素を持つことになります。しかし、このような共有が目的に合わない場合もあります。リストのコピーを作成する必要があっても、2 つのリストの要素に加えられた更新を共有したくない場合があります。

この種のまったく異なるコピーは「深いコピー (deep copy) 」と呼ばれます。次のプログラムを参考にして、「深いコピー」を作成するための、いくつかのテクニックを見ていきましょう。


import java.io.*;

import java.util.*;



class A implements java.io.Serializable {

private int x;



public A(int i) {

setX(i);

}

public void setX(int i) {

x = i;

}

public int getX() {

return x;

}

}



public class DeepDemo {



// 浅いコピー

static ArrayList copy1(ArrayList list) {

return (ArrayList)list.clone();

}



// ハードコードによる深いコピー

static ArrayList copy2(ArrayList list) {

ArrayList newlist = new ArrayList();

A aobj = (A)list.get(0);

newlist.add(new A(aobj.getX()));

return newlist;

}



// 直列化による深いコピー

static ArrayList copy3(ArrayList list) throws Exception {



// ArrayList をバイト配列に直列化する



ByteArrayOutputStream baos =

new ByteArrayOutputStream(100);

ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.writeObject(list);

byte buf[] = baos.toByteArray();

oos.close();



// バイト配列を ArrayList に直列化復元する



ByteArrayInputStream bais =

new ByteArrayInputStream(buf);

ObjectInputStream ois = new ObjectInputStream(bais);

ArrayList newlist = (ArrayList)ois.readObject();

ois.close();



return newlist;

}



static final int NUMITERS = 50000;



public static void main(String args[]) throws Exception {

ArrayList listold, listnew;

long currtime, elapsedtime2, elapsedtime3;



// 浅いコピー



listold = new ArrayList();

listold.add(new A(37));

listnew = copy1(listold);

((A)listold.get(0)).setX(47);

System.out.print("copy1 old/new = ");

System.out.print(((A)listold.get(0)).getX() + " ");

System.out.println(((A)listnew.get(0)).getX());



// ハードコードによる深いコピー



listold = new ArrayList();

listold.add(new A(37));

currtime = System.currentTimeMillis();

for (int i = 1; i <= NUMITERS; i++) {

listnew = copy2(listold);

}

elapsedtime2 = System.currentTimeMillis() - currtime;

((A)listold.get(0)).setX(47);

System.out.print("copy2 old/new = ");

System.out.print(((A)listold.get(0)).getX() + " ");

System.out.println(((A)listnew.get(0)).getX());



// 直列化による深いコピー



listold = new ArrayList();

listold.add(new A(37));

currtime = System.currentTimeMillis();

for (int i = 1; i <= NUMITERS; i++) {

listnew = copy3(listold);

}

elapsedtime3 = System.currentTimeMillis() - currtime;

((A)listold.get(0)).setX(47);

System.out.print("copy3 old/new = ");

System.out.print(((A)listold.get(0)).getX() + " ");

System.out.println(((A)listnew.get(0)).getX());



System.out.println();

System.out.println("copy2 time = " + elapsedtime2);

System.out.println("copy3 time = " + elapsedtime3);

}

}

DeepDemo プログラムでは、A 型の単一の要素を含む ArrayList オブジェクトを使用します。A 型のオブジェクトは、setX メソッドを介して可変です。

このプログラムでは、3 つの copy メソッドを定義しています。第 1 のメソッドである copy1 は、ArrayList.clone メソッドを使って浅いコピーを作成します。このメソッドは新しいリストを作成して、元のリストのオブジェクト参照をコピーします。copy1 メソッドが呼び出された後、プログラムは元のリストの単一の要素の値を変更し、新しいリストに含まれる要素の値も変更されるかどうかをチェックします。その結果、新しいリストにも変更が加えられていることから、copy1 メソッドによるコピーは浅いコピーであることが示されます。

第 2 のアプローチである copy2 は、ハードコードにより深いコピーを作成します。このアプローチでは、新しい ArrayList をセットアップし、元のリストの要素の値を取得して、この値を含む新しい A オブジェクトを新しいリストに追加します。

第 3 のアプローチである copy3 は直列化を使用します。このアプローチでは、まず writeObject を使って、ArrayList を ByteArrayOutputStream オブジェクト内に格納されたバイトストリームに変換し、さらにその後でこのプロセスを逆転させ、バイトのストリームを ArrayList に変換します。このプロセスにより、新しい ArrayList および A オブジェクトが強制的に作成されます。

プログラムを実行するとき、その出力は次のようになります。


copy1 old/new = 47 47

copy2 old/new = 47 37

copy3 old/new = 47 37



copy2 time = 47

copy3 time = 8500

ハードコードによる深いコピー (copy2) と直列化による深いコピー (copy3) の両方は、copy1 とは異なる方法でコピーを保持します。しかし、これら 2 つのアプローチの間には、パフォーマンスという見かけ上の問題が存在します。直列化によるアプローチは、ハードコードによるアプローチよりも処理速度がかなり遅くなります。ただし、このような処理時間の違いがあるからといって、深いコピーを作成するときに、単純に、直列化によるアプローチを避けるべきであるという結論にはなりません。DeepDemo の実行結果は、コピーを作成するために 8,500 ミリ秒を要したことを示してします。しかし、DeepDemo を詳しく検討すると、それが 50,000 回のコピーを行っていることがわかります。8,500 ミリ秒間に 50,000 回のコピーを行うということは、単一のコピーを行うための時間はほんのわずかであることを意味します。したがって、このようなコピーを作成するアプリケーションの一部では、処理時間は重要な要因にはなりません。

パフォーマンスという問題と並んで、このプログラムにはもう一つの大きな問題があります。マニュアルまたはハードコードによるアプローチは、複雑なデータ構造体へのスケールアップが容易ではありません。たとえば、グラフまたはツリー構造の深いコピーを作成する必要がある場合、その処理を自分でコードしようとすると、コーディング作業が相当に複雑なものになる可能性があります。したがって、直列化によるアプローチとマニュアルによるアプローチを比較すると、次のように結論付けることができます。「直列化によるアプローチは、処理時間を基準にすれば、マニュアルによるアプローチよりもコストがかかるが、全体として考えれば直列化によるアプローチの方が望ましい」

直列化によるコピーの最後のポイントは、直列化プロセスが処理するすべてのオブジェクトは直列化可能でなければならないという点です。DeepDemo サンプルの場合、A は、java.io.Serializable インタフェースを実装することで、この要件をみたします。ArrayList も同様です。

深いコピーの詳細については、Arnold、Gosling、Holmes 共著『The Java(TM) Programming Language Third Edition』(http://java.sun.com/docs/books/javaprog/thirdedition/) のセクション 3.9.3「Shallow versus Deep Cloning」とセクション 15.7「Object Serialization」を参照してください。


strictfp の使用

「strictfp」というキーワードは、Java プログラミング言語に対して最近追加されたものの 1 つです。このキーワードは、浮動小数点演算のいくつかの側面を制御するために使用されます。strictfp の説明と具体例の提示は若干複雑になる可能性があるため、このティップでは段階的に説明を進めていきます。このティップでは、いくつかの構文のサンプルから話を始め、その後で、strictfp が重要になる可能性のある具体的なケースを示します。

strictfp は、次のように、クラス、インタフェース、およびメソッドの修飾子として使用できます。


// strictfp の正しい使い方



strictfp interface A {}



public strictfp class FpDemo1 {

strictfp void f() {}

}

strictfp をコンストラクタやインタフェース内のメソッドに対して使用することはできません。


// strictfp の誤った使い方



interface A {

strictfp void f();

}



public class FpDemo2 {

strictfp FpDemo2() {}

}

strictfp キーワードは、式が「FP-strict」であることを示すために使用します。クラス、インタフェース、またはメソッドが strictfp を使って宣言されていると、そのクラス、インタフェース、またはメソッドは FP-strict になります。したがって、その宣言内にあるクラス、インタフェース、メソッド、コンストラクタ、インスタンス初期化子、変数初期化子、および static 初期化子もすべてFP-strict になります

ある式がこれらの FP-strict 宣言のいずれかの内部に現れる場合、あるいは式がコンパイル時定数式である場合、その式は FP-strict になります。実際的な言い方をすれば、このことは、あるクラスまたはメソッドが strictfp を使って宣言されている場合、そのクラスまたはメソッド内に現れる任意の式が FP-strict 式になるということを意味します。

コンストラクタは strictfp を使って宣言できないため、その定義クラスが FP- strict である場合のみ FP-strict になります。strictfp はインタフェースプロパティではなく実装であるため、strictfp を使って、インタフェース内のメソッドを宣言することはできません。

それでは実際には、FP-strict にはどんな意味があるのでしょうか。次のサンプルを検討してみましょう。


public strictfp class FpDemo3 {

public static void main(String[] args) {

double d = 8e+307;

System.out.println(4.0 * d * 0.5);

System.out.println(2.0 * d);

}

}

double の最大値 (Double.MAX_VALUE) はほぼ 1.8e+308 です。FpDemo3 サンプルの場合、第 1 の式は次のように評価されます。 (4.0 * d) * 0.5

これは、Java プログラミング言語が左から右へという評価の順序を保証しているためです。この式の最初の部分を評価すると、その結果は 32e+307、つまり 3.2e+308 になりますが、これは Double.MAX_VALUE を超える値です。

この式は FP-strict であるため、実装では、式全体が正の無限大になると評価する必要があります (つまり、プログラムの出力は「Infinity」になります)。後半の部分で 0.5 を乗算することで、この式の最終的な値は Double.MAX_VALUE を超えない 1.6e+308 になりますが、式全体が正の無限大になるという点に変わりはありません。

これとは反対に、式が FP-strict でない場合、実装では、拡張指数範囲を使って中間結果を表現することが許可されます。FpDemo3 サンプルの場合、このことによって式のオーバーフローを防ぎ、範囲内に収まる最終的な結果を出すことができます。ただし、実装ではこのような処理を行う必要はありません。というのも、FP-strict は事実上どこででも使用できるためです。

なお、このサンプルの乗算には、次のような結合法則は成立しません。


(4.0 * d) * 0.5 != 4.0 * (d * 0.5)

strictfp は、その使用により、さまざまな Java 実装全体を通じて共通する動作が保証されるため重要です。つまり、アプリケーションを異なる Java 実装やハードウェアプラットフォームに移動しても、アプリケーション内の浮動小数点演算が同じように動作することをあらかじめ予想できるということです。

strictfp の詳細については、Arnold、Gosling、Holmes 共著『The Java(TM) Programming Language Third Edition』(http://java.sun.com/docs/books/javaprog/thirdedition/) のセクション 6.6.3「Strict and non-Strict Floating- Point Arithmetic」を参照してください。また、Gosling、Joy、Steele、Bracha 共著『The Java(TM) Language Specification Second Edition』(http://java.sun.com/docs/books/jls/) のセクション 15.4「FP-strict Expressions」も参照してください。


文字列のパフォーマンスの最適化

整数ラッパーオブジェクトのリストを含む Java プログラミング言語アプリケーションを開発しているとしましょう。しかも、そのアプリケーションがこれらのリストを頻繁に文字列に変換することがあるとします。つまり、アプリケーションに次のようなコードが含まれているということです。


List list = new ArrayList();

list.add(new Integer(37));

list.add(new Integer(47));

...

String s = list.toString();

toString メソッドは、リストを次のような形式の文字列に変換します。


[37, 47]

このような変換を最適化できる方法はあるのでしょうか。この問いに答えるためには、リストが文字列に変換されるときに内部的に何が起こっているかを検討してみる必要があります。まず、StringBuffer オブジェクトがセットアップされます。この後、リストの各要素が文字列に変換され、バッファに追加されます。そして最後の段階で、StringBuffer オブジェクトそのものが String オブジェクトに変換されます。

リストの要素は、一連のメソッドを呼び出すことによって文字列に変換されます。これらのメソッドの中で最も重要なのは、Integer.toString(value,radix) です。このメソッドは、59 のような整数値を受け付け、それから新しい文字列を生成します。しかし、このような変換は負荷が大きくなります。というのも一つには、文字列表現の中で個別の桁を取得するため、値を個々に選択する必要があるためです。また、新しい文字列を作成する必要があるため、負荷が大きくなります。

標準的なアプローチよりも望ましい方法はあるのでしょうか。少なくともいくつかのケースでは、その答えはイエスです。リストの要素が 0 から 99 までの範囲の値を含む整数オブジェクトであるサンプルを検討してみましょう。この場合は、標準的なメソッドよりも高速に動作する toString のローカルバージョンを書くことが可能です。そのコードは次のようになります。


import java.util.*;



public class PerfDemo {

// ArrayList オブジェクトの長さ

static final int MAXLISTLEN = 10000;



// 整数オブジェクトの最大値

static final int MAXNUM = 99;



// 小さな整数に対する書式設定された文字列を格納するキャッシュ

static String cache[] = new String[MAXNUM + 1];



// プログラムの起動時にキャッシュをいっぱいにする

static {

for (int i = 0; i <= MAXNUM; i++) {

cache[i] = Integer.toString(i);

}

}



// 上記のキャッシュを使用する toString のローカルバージョン

static String localtoString(List list) {

StringBuffer sb = new StringBuffer();

sb.append("[");

int size = list.size();

for (int i = 0; i < size; i++) {

Integer iobj = (Integer)list.get(i);

sb.append(cache[iobj.intValue()]);

if (i + 1 < size) {

sb.append(", ");

}

}

sb.append("]");

return sb.toString();

}



public static void main(String args[]) {



Random rn = new Random(0);



List list = new ArrayList();



// 0 から 99 までの範囲のランダムな整数でリストをいっぱいにする



for (int i = 1; i <= MAXLISTLEN; i++) {

int r = rn.nextInt(MAXNUM + 1);

list.add(new Integer(r));

}



String s1 = null;

String s2 = null;



// 標準的なアプローチの処理時間を評価する



long currtime = System.currentTimeMillis();

for (int i = 1; i <= 100; i++) {

s1 = list.toString();

}

long elapsed = System.currentTimeMillis() - currtime;

System.out.println("standard toString time = " +

elapsed);



// 独自のアプローチの処理時間を評価する



currtime = System.currentTimeMillis();

for (int i = 1; i <= 100; i++) {

s2 = localtoString(list);

}

elapsed = System.currentTimeMillis() - currtime;

System.out.println("local toString time = " + elapsed);



// 同じ文字列が生成されているかどうかチェックする



if (!s1.equals(s2)) {

System.out.println("error");

}

}

}

PerfDemo プログラムは、0 から 99 までの範囲の整数値に対する文字列キャッシュをセットアップします。つまり、このプログラムでは、0 から 99 までの範囲の整数を文字列に変換した結果である文字列値をあらかじめ計算してしまうということです。このキャッシュを使用すれば、localtoString 内で各整数オブジェクトに対して intValue メソッドを呼び出し、さらにその値をキャッシュテーブルに対するインデックスとして使用することはごく簡単なことです。

PerfDemo を実行すると、次のような結果が出力されます。


standard toString time = 1719



local toString time = 781

これらの結果は、ローカルメソッドが標準的なメソッドよりも 2 倍以上高速であることを示しています。これは、ローカルメソッドが整数から文字列への変換と文字列の作成を回避しているためです。

ただし、この最適化は一般的なものではありません。というのも、このキャッシュによるテクニックを拡張して、任意の範囲の整数を取り扱うのは困難であるためです。パフォーマンスを向上させるためのこのテクニックは、「空間を時間に交換する」テクニックまたは「結果を先に計算してしまう」テクニックと呼ぶことができます。ただ残念なことに、キャッシュテーブルのサイズがあまりにも大きくなりすぎると、このテクニックは合理的でなくなってしまいます。しかし、最適化の多くはこのタイプのものです。つまり、あるアプリケーション内の特定のデータや操作に関する知識を利用することでパフォーマンスの向上を実現するということです。

別の角度からすると、標準ライブラリのメソッドを独自のメソッドに置き換えることには信頼性の問題が伴います。ライブラリのメソッドは、開発者が独自に書こうとするメソッドの何倍も厳重に検証されています。ときには、ライブラリのメソッドに優先して独自のメソッドを使用することに意味があることもありますが、そのマイナス面を検討してみることには十分な価値があります。たとえば、PerfDemo サンプルの場合は、toString メソッドのパフォーマンスに実際上の問題があるかどうか、あるいは toString メソッドを高速化することで実際にアプリケーションの全般的なパフォーマンスが向上するかどうかを検討してみる必要があります。

パフォーマンスの最適化については、Jon Bentley 著『Programming Pearls』を参照してください。この書籍の抜粋は、http://www.programmingpearls.com/ で閲覧することができます。特に、Appendix 4「Rules for Code Tuning」(http://www.programmingpearls.com/apprules.html) が参考になります。


JDC Tech Tips
2001 年 4 月 10 日

JDC テクニカル・ティップ (日本語版)
この日本語版 JDC テクニカル・ティップは、英語版 JDC Tech Tips April 10, 2001 を翻訳したものです。

JDC テクニカル・ティップ (日本語版)のバックナンバーは、以下のサイトでご覧いただけます。
/java