同じ結果を提供するプログラムであっても、プログラミング上の実現方法は一通りではありません。
プログラマは、それらの中から最適な方法を選択すべく考慮しなければなりません。
そのときのひとつの判断基準に「性能(処理速度,消費資源)」があります。
ここでは、Javaのプログラミングにおいて「性能」を意識したときに、いくつかの考慮すべき事項について、書籍「Javaの鉄則」から参考になると思ったものを紹介したいと思います。
オブジェクト(インスタンス)を作成するには大きなコスト(処理時間,メモリ消費)がかかります。
従って、オブジェクトの作成は最小限にとどめることがプログラムの性能向上に大きく寄与します。
派生クラスのオブジェクトを作成する場合、その上位クラスのコンストラクタを最上位クラスである java.lang.Object に達するまで順々に呼出します。
コンストラクタが呼び出されると、それぞれのクラスのインスタンス変数の初期化が行われ、そしてコンストラクタ本文が実行されます。
オブジェクトを作成するときは java.lang.Object から順に最下位クラスまでこの処理が実行されることになります。
従って、継承の多いクラスよりも継承の少ないクラスの方がオブジェクト作成のコストは少なくて済みます。
前述のようにオブジェクトの生成には大きなコストがかかります。
そこで、
C言語でのプログラミング経験のあるプログラマの場合、C言語のときの癖で、メソッドの先頭ですべての変数の宣言をするコードを書いてしまうことがあります。
結果的に宣言した変数の全てが使われるのであれば問題はありませんが、処理の流れによっては使われない場合があるのであれば、それは無駄です。
オブジェクトは必要となったときに作成するように心がけましょう。
何度もいうように、オブジェクトの生成には大きなコストがかかるのです。
例:2つの文字列を連結した文字列を返すメソッド
public String concat(String str1, String str2) throws IllegalArgumentException
{
IllegalArgumentException ex = new IllegalArgumentException("Parameter Error !");
String result = new String();
if ( str1 != null && str2 != null ) {
result = str1 + str2;
} else {
throw ex;
}
return result;
}
result
は使用しませんし、正常時には ex
は必要ありません。
public String concat(String str1, String str2) throws IllegalArgumentException
{
if ( str1 != null && str2 != null ) {
return new String(str1 + str2);
} else {
throw new IllegalArgumentException("Parameter Error !");
}
}
オブジェクト生成には大きなコストがかかります。再利用が可能な場合は再利用すべきです。
次のコードは、スタック変数,インスタンス変数,スタティック変数に繰り返しアクセスするものです。
class StackVars
{
private int instVar;
private static int staticVar;
// スタック変数へのアクセス
public void stackAccess(int val)
{
int j = 0;
for ( int i=0; i<val; i++ )
j += 1;
}
// インスタンス変数へのアクセス
public void instanceAccess(int val)
{
for ( int i=0; i<val; i++ )
instVar += 1;
}
// スタティック変数へのアクセス
public void staticAccess(int val)
{
for ( int i=0; i<val; i++ )
staticVar += 1;
}
}
class StackVars
{
private int instVar;
private static int staticVar;
// インスタンス変数へのアクセス
public void instanceAccess(int val)
{
int j = instVar;
for ( int i=0; i<val; i++ )
j += 1;
instVar = j;
}
// スタティック変数へのアクセス
public void staticAccess(int val)
{
int j = staticVar;
for ( int i=0; i<val; i++ )
j += 1;
staticVar = j;
}
}
変数には、プリミティブ型とそれに対するラッパークラス(int に対する Integer など)があります。
クラスを使用すると、その値にアクセスするときにオブジェクトを介すので当然遅くなります。
次の例では、プリミティブ型を使った方(前者)が30%ほど高速です。
// プリミティブ型を使う
public int usePrimitive(int increment)
{
int i = 5;
i = i + increment;
return i;
}
// ラッパークラスを使う
public int useObject(Integer increment)
{
int i = 5;
i = i + increment.intValue();
return i;
}
また、以下のように戻り値をオブジェクトにした場合は、前述のオブジェクト生成のコストがさらにかかることになります。
// ラッパークラスを使う
public Integer useObject(Integer increment)
{
int i = 5;
i = i + increment.intValue();
return new Integer(i);
}
こうなるとプリミティブ型だけ使っていてばいいように思えますが、ラッパークラスを使わなければならない場合も当然あります。
そのひとつの例にコレクションクラスがあります。
コレクションクラスは、プリミティブ型を扱えません。
以下のように Vector にプリミティブ型の値を格納することはできません。
Vector v = new Vector();
v.add(5);
Vector v = new Vector();
v.add(new Integer(5));
プリミティブ型とそれに対するラッパークラスは以下の通りです。
プリミティブ型 | ラッパークラス |
---|---|
boolean | Boolean |
char | Character |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
Javaでは、オブジェクト生成時のインスタンス変数とstatic変数の初期化(デフォルト値)は保証されています。
それらの変数にデフォルト値と同じ値を明示的に設定するのは、2度値を設定することになり無駄です。
次のコードでは、オブジェクト生成時に、インスタンス変数 count, done, pt, vec
は、それぞれ 0, false, null, null
に初期化されます。
そして、両コードとも count
と done
にはもう一度同じ値を設定することになります。
class Foo
{
private int count;
private boolean done;
private Point pt;
private Vector vec;
public Foo()
{
count = 0;
done = false;
pt = new Point(0, 0);
vec = new Vector(10);
}
}
class Foo
{
private int count = 0;
private boolean done = false;
private Point pt;
private Vector vec;
public Foo()
{
pt = new Point(0, 0);
vec = new Vector(10);
}
}
class Foo
{
private int count;
private boolean done;
private Point pt;
private Vector vec;
public Foo()
{
pt = new Point(0, 0);
vec = new Vector(10);
}
}
型 | デフォルト値 |
---|---|
boolean | false |
char | '\u0000' |
byte | 0 |
short | 0 |
int | 0 |
long | 0 |
float | 0 |
double | 0 |
オブジェクトの参照 | null |
static, final, private型のメソッドは、JavaコンパイラあるいはJITによってインライン化されます。
そのメソッドが頻繁に呼ばれるのであれば、インライン化されれば劇的に高速になります。
ただし、インライン化されるとプログラムサイズは大きくなりますから、そのメソッドの大きさを考慮することも必要です。
次の例のような、アクセッサ(getter, setter)メソッドなどは、サイズも小さく、使用頻度も多いので、インライン化の対象としては最適のものといえます。
class Test
{
private int length;
public int getLength()
{
return length;
}
public void setLength(int val)
{
length = val;
}
}
class Test
{
private int length;
public final int getLength()
{
return length;
}
public final void setLength(int val)
{
length = val;
}
}
定数を変数で指定する場合に final 指定をすると、コンパイラは定数に置き換えます。
以下の例では、a, b
を final 指定しないと c
は変数として扱われますが、
static int a = 30;
static int b = 60;
int c = a + b + 100;
static final int a = 30;
static final int b = 60;
int c = a + b + 100;
文字列を連結するときに、
String str = new String("Practical ");
str += "Java";
StringBuffer str = new StringBuffer("Practical ");
str.append("Java");
Javaコンパイラは、前者の場合、StringBuffer
を作成して append
メソッドを使って連結し、その結果を、新たな String
を作成して格納するコードを生成します。
Java Native Code 上では、前者は5つのオブジェクトを作成しますが、後者は3つのオブジェクトを作成するだけで済みます。
Java で Vector の要素を探索するには、以下の例のように Enumeration, Iterator, ListIterator, getメソッド を使う4つの方法があります。
// Enumeration を使った探索
public int enumVec(Vector vec)
{
Enumeration enum = vec.elements();
int total = 0;
while ( enum.hasMoreElements() )
total += ((Integer)(enum.nextElement())).intValue();
return total;
}
// Iterator を使った探索
public int iterVec(Vector vec)
{
Iterator iter = vec.iterator();
int total = 0;
while ( iter.hasNext() )
total += ((Integer)(iter.next())).intValue();
return total;
}
// ListIterator を使った探索
public int listIterVec(Vector vec)
{
ListIterator iter = vec.listIterator();
int total = 0;
while ( iter.hasNext() )
total += ((Integer)(iter.next())).intValue();
return total;
}
// forループ と getメソッド を使った探索
public int forVec(Vector vec)
{
int size = vec.size();
int total = 0;
for ( int i=0; i<size; i++ )
total += ((Integer)(vec.get(i))).intValue();
return total;
}
hasMoreElements(), hasNext(), hasNext()
)ためです。
以下は、探索して代入するだけの特に意味のないプログラムですが、ArrayList は Vector よりも4倍高速で、さらに、配列 は ArrayList よりも11倍高速です。
// Vector を使う
public void iterateVector(Vector vec)
{
int size = vec.size();
Object j;
for ( int i=0; i<size; i++ )
j = vec.get(i);
}
// ArrayList を使う
public void iterateArrayList(ArrayList al)
{
int size = al.size();
Object j;
for ( int i=0; i<size; i++ )
j = al.get(i);
}
// 配列 を使う
public void iterateArray(int[] ar)
{
int size = ar.length;
int j;
for ( int i=0; i<size; i++ )
j = ar[i];
}
配列をコピーしたいときは、素直に以下のようにするでしょう。
// forループ を使った配列のコピー
public void copyArray(int[] src, int[] dest)
{
int size = src.length;
for ( int i=0; i<size; i++ )
dest[i] = src[i];
}
// System.arraycopy を使った配列のコピー
public void copyArray(int[] src, int[] dest)
{
int size = src.length;
System.arraycopy(src, 0, dest, 0, size);
}
繰り返し使用する共通部分式は、変数に置き換えることによって式の評価は1回で済むことになり、処理時間が短縮されます。
次のコードは、
SomeObject[] someObj = new SomeObject[N];
someObj[i+j] = new SomeObject();
someObj[i+j].foo(k);
someObj[i+j].foo(k+1);
someObj[i+j].foo(k+2);
someObj[i+j].foo(k+3);
someObj[i+j].foo(k+4);
someObj[i+j]
を tempObj
に置き換えて、
SomeObject[] someObj = new SomeObject[N];
someObj[i+j] = new SomeObject();
SomeObject tempObj = someObj[i+j];
tempObj.foo(k);
tempObj.foo(k+1);
tempObj.foo(k+2);
tempObj.foo(k+3);
tempObj.foo(k+4);
これは、ループ内で同じ式を実行する場合も同じです。
int a = 10;
int b = 20;
int[] arr = new int[val];
for ( int i=0; i<i; i++ )
arr[i] = a + b;
int a = 10;
int b = 20;
int c = a + b;
int[] arr = new int[val];
for ( int i=0; i<i; i++ )
arr[i] = c;
ループを使わないで個々に処理すれば速くなりますが、普通これはしないでしょう。
以下の例では、
int[] ia = new int[4];
ia[0] = 10;
ia[1] = 10;
ia[2] = 10;
ia[3] = 10;
int[] ia = new int[4];
for ( int i=0; i<4; i++ )
ia[i] = 10;