Object.clone()の性能

Objectにはclone()というメソッドが存在します。このメソッドは自身と同じ内容のオブジェクトを新たに生成してそれを返却します。
コピー元のオブジェクトが保持しているオブジェクトに関しては同一のものを参照するので、自分の頭の中のイメージとしてはオブジェクト用に確保した領域に対してmemcpyを行っているイメージ。
このメソッドはprotectedで実装されており、なおかつコピー元のオブジェクトがCloneableを実装してないとCloneNotSupportedExceptionを発生するのでちょっと使いにくい。
どのように使用するべきかの実装例を探してみたらいいのが見つかったので引用。

public class Foo implements Cloneable {
    public Object clone() {
        try {
            return (super.clone());
        } catch (CloneNotSupportedException e) {
            throw (new InternalError(e.getMessage()));
        }
    }
}

引用元:クローンナブル


コピーコンストラクタを作成してオブジェクトの複製を作成した場合との性能差が気になったので下記のようなテストクラスを作成して実行してみました。

public class TestClone implements Cloneable {
	
	private static final int SET = 10;
	private static final int ROOP = 100000;
	
	private int value1;
	private int value2;
	private int value3;
	
	public static void main(String[] args) {
		
		int i;
		long time;
		TestClone test = new TestClone();
		
		for (int cycle = 0; cycle < SET; ++cycle) {
			time = System.nanoTime();
			for (i = ROOP; i > 0; --i) {
				new TestClone(test);
			}
			System.out.println("COPY[" + cycle + "]  " + (System.nanoTime() - time));
			time = System.nanoTime();
			for (i = ROOP; i > 0; --i) {
				test.clone();
			}
			System.out.println("CLONE[" + cycle + "] " + (System.nanoTime() - time));
		}
	}
	
	public Object clone() {
		try {
			return super.clone();
		} catch (CloneNotSupportedException e) {
			throw new InternalError(e.getMessage());
		}
	}
	
	public TestClone() {
		value1 = 1;
		value2 = 2;
		value3 = 3;
	}
	
	public TestClone(TestClone src) {
		value1 = src.value1;
		value2 = src.value2;
		value3 = src.value3;
	}
}


実行結果

COPY[0]  10954208
CLONE[0] 41769220
COPY[1]  2810436
CLONE[1] 38234045
COPY[2]  1876126
CLONE[2] 26948310
COPY[3]  1970632
CLONE[3] 27934632
COPY[4]  1601817
CLONE[4] 25787446
COPY[5]  1805355
CLONE[5] 25462086
COPY[6]  3114246
CLONE[6] 28914970
COPY[7]  1825418
CLONE[7] 26942646
COPY[8]  3111301
CLONE[8] 28188760
COPY[9]  1574968
CLONE[9] 26175436


…コピーコンストラクタの方が断然速い。
Object.clone()はnativeなのでnativeメソッド呼び出しに時間がかかっているのではないかと予測。
この結果を見てObject.clone()がさらに使いにくくなりました。

FileDialogのsetFilenameFilter

FileDialogのsetFilenameFilterを呼んでFilenameFilterを設定してもまったく機能しないという現象が発生しました。
javadocを確認してみると、どうやらSunのWindows用のVMでは実装されてないようです。

setFilenameFilter

public void setFilenameFilter(FilenameFilter filter)

ファイルダイアログウィンドウのファイル名フィルタを指定されたフィルタに設定します。ファイル名フィルタは、Microsoft Windows 用の Sun の参照実装では機能しません。

http://java.sun.com/javase/ja/6/docs/ja/api/java/awt/FileDialog.html

JFileDialogを使えばよいのですが、FileDialogを用いるとネイティブのファイル選択ダイアログが表示されます。
JFileDialogで提供されるダイアログよりもネイティブのファイル選択ダイアログが便利なので特に必要のない場合にはFileDialogを用いるようにしています。

ファイルの拡張子の判別方法

ファイルの拡張子が必要なものであるかを判断する場合、Fileクラスに拡張子を取得するメソッドがないので結局ファイル名から拡張子を取得するのですが、その時に気にした方が良いかも知れないことのメモ。

File#getName()よりもFile#getPath()を用いた方が良い

FileクラスはStringフィールドでpathを保持しており、getPath()はそれをそのまま返しているのですが、getName()はsubstringしたものを返します。
この場合は、ファイル名のみを切り出してもらう必要はないため、余計なオブジェクトを生成しないgetPath()を用いた方が良い。
ちなみにFileクラスはimmutableです。

拡張子だけではなくちゃんとファイルかどうかも調べよう

Fileクラスは名前こそFileですが、その実態はファイルパスを表すクラスです。当然それがディレクトリをさしていることもあります。
ファイルがちゃんとファイルとして扱えるかどうかはFile#isFile()で取得できます。めんどくさがらずに呼んであげましょう。
ちなみにFileクラスのisFile()、isDirectory()、canRead()、canWrite()などのメソッドはファイルパスの指し示す対象が存在しない場合、falseを返します。

例:拡張子がjarであり読込可能なファイルかどうかのチェック

public boolean isJarFile(File file) {
	return file.isFile() && file.canRead() && file.getPath().endsWith(".jar");
}

substringを呼んで余計なStringを生成したくないのでので、拡張子の判断にはendsWithを用いています。
チェックする拡張子が一つであり、かつ大文字小文字を無視しなくてよい場合にはこれで十分です。
大文字小文字を無視する場合はregionMatches()を用いれば大文字小文字を無視した部分一致のチェックができますが、可読性を考えると、素直にsubstringしてequalsIgnoreCaseしたほうが良いかと。

GUIアプリの起動ロジッククラス

ちょっとしたGUIアプリを作成する際にmainメソッドで毎回似たような処理を行うのですが、毎回似たような処理を書くのもなんなのでクラスにまとめてみました。

import java.awt.Dimension;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

/**
 * GUIアプリの起動ロジッククラス
 */
public class Main {
	
	/**
	 * コンストラクタ
	 * <p>
	 * 生成させないために存在します
	 */
	private Main() {
	}
	
	/**
	 * 引数で指定されたクラスをもつフレームを表示します
	 * <p>
	 * clazzに指定するクラスがJComponentのサブクラスでない場合、例外を発生します
	 * 
	 * @param clazz フレームで表示を行うJComponentのサブクラス
	 * @param title フレームのタイトル
	 */
	public static void main(final Class clazz, final String title) {
		if (clazz == null) {
			throw new NullPointerException();
		}
		if (! JComponent.class.isAssignableFrom(clazz)) {
			throw new IllegalArgumentException();
		}
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				initSystemLookAndFeel();
				try {
					createAndShowGUI((JComponent)clazz.newInstance(), title);
				} catch (InstantiationException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				}
			}
		});
	}

	/**
	 * 引数で指定されたコンポーネントをもつフレームを表示します
	 * 
	 * @param component コンポーネント
	 * @param title フレームのタイトル
	 */
	public static void createAndShowGUI(final JComponent component, final String title) {
		JFrame frame = new JFrame(title);
		frame.getContentPane().add(component);
		frame.pack();
		
		Dimension size = frame.getSize();
		Dimension screenSize = frame.getToolkit().getScreenSize();
		
		frame.setLocation((screenSize.width - size.width) / 2, (screenSize.height - size.height) / 2);
		
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setVisible(true);
	}
	
	/**
	 * システムのルックアンドフィールに設定します
	 */
	public static void initSystemLookAndFeel() {
		try {
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (UnsupportedLookAndFeelException e) {
			e.printStackTrace();
		}
	}
}

このように使用します。

import javax.swing.JPanel;

public class SamplePanel extends JPanel {

	public static void main(String[] args) {
		Main.main(SamplePanel.class, "タイトル");
	}
}

三項演算子の有効性についての調査

三項演算子について性能面の観点から有効性について調査してみました。
調査方法としてはMath.min(int, int)と同様の処理を行うメソッドを三項演算子を用いたものとそうでないものを作成しバイトコードの比較を行うことで確認します。

public class TestTernaryOperator {
	
	public static int min1(int a, int b) {
		//max_stack = 2, max_locals = 2, code_length = 11
		//[1]	iload_0
		//[2]	iload_1
		//[5]	if_icmpge		#9
		//[6]	iload_0
		//[9]	goto		#10
		//[10]	iload_1
		//[11]	ireturn
		return (a < b) ? a : b;
	}
	
	public static int min2(int a, int b) {
		//max_stack = 2, max_locals = 2, code_length = 9
		//[1]	iload_0
		//[2]	iload_1
		//[5]	if_icmpge		#7
		//[6]	iload_0
		//[7]	ireturn
		//[8]	iload_1
		//[9]	ireturn
		if (a < b) {
			return a;
		} else {
			return b;
		}
	}
}

aの方が小さい場合に三項演算子を用いたmin1よりもifで処理しているmin2の方がgoto命令1つ分早いです。
(ifの各ブロックごとにreturnを記述しているのが効いています)
自分としては常に三項演算子のほうが高速なのではないかと予想していたのですが、必ずしもそうではないことが解りました。

複数のフィールドを一つの配列フィールドにまとめるという手法の有効性に関する調査

携帯電話向けのアプリを作成する際にクラスサイズを小さくする手法として複数のフィールドを一つの配列フィールドにまとめるという手法がありますが、個人的にはこの手法は可読性が低下し余計な配列アクセスを行うことにより性能も劣化するものと思われるので、使用するべきではないものと考えています。
クラスサイズに関しては確かにフィールドの定義に必要なバイト数は削減されますが、配列フィールドにまとめた場合には個々の領域を使用する際にフィールドに対するアクセスに加え配列要素に対するアクセスを行う必要があるので、フィールドを使用する箇所ごとに配列要素に対するアクセスの分のバイトコードが追加されます。
これにより、場合によっては逆にクラスファイルが大きくなる可能性があるのではないかという疑念を抱いたので調査を行いました。
今回は性能面などは無視して単純にクラスサイズに与える影響のみを考慮し、フィールドの定義に必要なバイトサイズおよびフィールドに対するアクセスと配列フィールドの要素に対するアクセスに必要なバイトコードの差分のみを調査しています。

一つのフィールドを定義するのに必要なバイトサイズ

一つのフィールドには一つのfield_info構造体を必要とします。

field_info {
  u2 access_flag;
  u2 name_index;
  u2 descriptor_index;
  u2 attributes_count;
  attribute_info attributes[attributes_count];
}

必須であるattributeは通常のフィールドには存在しないので、ここではattributes_countは0とします。
また、name_indexはフィールド名を持つCONSTANT_Utf8_info構造体を指し示しています。フィールド名はユニークでないといけないので、これも必要であるものとします。

CONSTANT_Utf8_info {
  u1 tag;
  u2 length;
  u1 bytes[length];
}

descriptor_indexもフィールドの型を表すCONSTANT_Utf8_info構造体を指し示していますが、こちらは同じ型を持つフィールド同士で使いまわされるので、必要な領域としてカウントしません。
ここまででfield_infoで8byte、CONSTANT_Utf8_Infoで最低4byteのあわせて12byte使用します。


またフィールドにアクセスする際にはCONSTANT_Fieldref_infoが必要になります。これも各フィールドことに必要になります。

CONSTANT_Fieldref_info {
  u1 tag;
  u2 class_index;
  u2 name_and_type_index;
}

name_and_type_indexはCONSTANT_NameAndType_info構造体を指し示しています。名前に絡んでいるのでこちらも各フィールドごとに必要になります。

CONSTANT_Fieldref_info {
  u1 tag;
  u2 name_index;
  u2 descriptor_index;
}

name_indexおよびdescriptor_indexはそれぞれフィールド名とフィールドの型を表すCONSTANT_Utf8_info構造体を指し示していますが、指し示している先はfield_infoで使用しているものと同じものであるため、必要な領域としてカウントしません。
こちらはCONSTANT_Fieldref_infoで5byte、CONSTANT_Fieldref_infoで5byteのあわせて10byte使用します。先の12byteと合わせて22byteが一つのフィールドを定義するのに少なくとも必要なサイズになります。
(実を言うとこれ以上減らすことも可能ですが、フィールド名をメソッド名などと重複させるなどのテクニックが必要となります)

フィールドに対するアクセスと配列フィールドの要素に対するアクセスに必要なバイトコードの差分

差分を調べるために簡単なクラスを作成し、そのバイトコードを取得してみました。
調査用のクラスはintとintの配列のフィールドを持ち、それぞれに対してのgetter、setterのメソッドを実装しています。それぞれのメソッドのサイズを比較することにより増加分を検証します。メソッドのサイズはmethod_infoに付属するCode_attributeのcode_lengthで判断します。

//Classname       : TestByteCodeSize
//SourceFile      : <Unknown>
//Compiler Version: 46.0
//Constant Pool   : 24 entries
//	[1]:CONSTANT_Class[7](name_index = 2)
//	[2]:CONSTANT_Utf8[1]("TestByteCodeSize")
//	[3]:CONSTANT_Class[7](name_index = 4)
//	[4]:CONSTANT_Utf8[1]("java/lang/Object")
//	[5]:CONSTANT_Utf8[1]("i")
//	[6]:CONSTANT_Utf8[1]("I")
//	[7]:CONSTANT_Utf8[1]("a")
//	[8]:CONSTANT_Utf8[1]("[I")
//	[9]:CONSTANT_Utf8[1]("<init>")
//	[10]:CONSTANT_Utf8[1]("()V")
//	[11]:CONSTANT_Utf8[1]("Code")
//	[12]:CONSTANT_Methodref[10](class_index = 3, name_and_type_index = 13)
//	[13]:CONSTANT_NameAndType[12](name_index = 9, signature_index = 10)
//	[14]:CONSTANT_Fieldref[9](class_index = 1, name_and_type_index = 15)
//	[15]:CONSTANT_NameAndType[12](name_index = 7, signature_index = 8)
//	[16]:CONSTANT_Utf8[1]("getI")
//	[17]:CONSTANT_Utf8[1]("()I")
//	[18]:CONSTANT_Fieldref[9](class_index = 1, name_and_type_index = 19)
//	[19]:CONSTANT_NameAndType[12](name_index = 5, signature_index = 6)
//	[20]:CONSTANT_Utf8[1]("getA")
//	[21]:CONSTANT_Utf8[1]("setI")
//	[22]:CONSTANT_Utf8[1]("(I)V")
//	[23]:CONSTANT_Utf8[1]("setA")
//ACC_SUPER       : true
public class TestByteCodeSize {
	
	private int i;
	private int[] a;
	
	public TestByteCodeSize()  {
		//max_stack = 2, max_locals = 1, code_length = 12
		//[1]	aload_0
		//[4]	invokespecial	java.lang.Object.<init> ()V (12)
		//[5]	aload_0
		//[6]	iconst_1
		//[8]	newarray		<int>
		//[11]	putfield		TestByteCodeSize.a [I (14)
		//[12]	return
		a = new int[1];
	}
	
	public int getI() {
		//max_stack = 1, max_locals = 1, code_length = 5
		//[1]	aload_0
		//[4]	getfield		TestByteCodeSize.i I (18)
		//[5]	ireturn
		return i;
	}
	
	public int getA() {
		//max_stack = 2, max_locals = 1, code_length = 7
		//[1]	aload_0
		//[4]	getfield		TestByteCodeSize.a [I (14)
		//[5]	iconst_0
		//[6]	iaload
		//[7]	ireturn
		return a[0];
	}
	
	public void setI(int i) {
		//max_stack = 2, max_locals = 2, code_length = 6
		//[1]	aload_0
		//[2]	iload_1
		//[5]	putfield		TestByteCodeSize.i I (18)
		//[6]	return
		this.i = i;
	}
	
	public void setA(int a) {
		//max_stack = 3, max_locals = 2, code_length = 8
		//[1]	aload_0
		//[4]	getfield		TestByteCodeSize.a [I (14)
		//[5]	iconst_0
		//[6]	iload_1
		//[7]	iastore
		//[8]	return
		this.a[0] = a;
	}
}

get、setのメソッドのいずれも配列フィールドの要素に対するアクセスにおいて2byte多くなっています。また、配列のインデックス時定時にiconst_0を用いているので、インデックスが6以上の場合はこの箇所がbipushになるため差は3byteに拡大します。
これより普通に用いた場合、インデックスの5までに割り振った変数の場合11回以上、6以上に割り振った変数の場合8回以上使用した場合に逆にクラスサイズが増大する可能性があります。
(状況によりますが実はこちらもフィールドの配列をローカルに転記することで更なるバイトサイズの縮小が可能です)

iアプリにおける通信先制限

通常のiアプリはアプリをダウンロードしたサーバとしか通信できません。
このことは概念としては知っていたのですが、仕様書をじっくり読んで厳密に仕様を調べてみました。

アプリをダウンロードしたサーバとは

iアプリはその構成上jamファイルとjarファイルから成り立ちますが、
ここでいうダウンロードしたサーバとはjarファイルをダウンロードしたサーバを指します。(jamファイルのPackageURLで指定したURL)

アクセスできる範囲は

同一のサーバであるかどうかは、URLのプロトコル、ホスト名、ポートが同一かどうかで判断しています。
(例:http://www.foo.com/download/prog/App1.jar からダウンロードした場合、http://www.foo.com/ 配下にアクセス可能です)

アプリ内でダウンロード元のURLを取得する方法はあるか

ダウンロード元のURLを取得するためのメソッドとして、
com.nttdocomo.ui.IApplication#getSourceURL()
が用意されています。
このメソッドの戻り値もjarファイルをダウンロードしたURLより作成されます。
(例:http://www.foo.com/download/prog/App1.jar からダウンロードした場合、 戻り値は、"http://www.foo.com/download/prog/" になります)

エミュレータで通信を行うアプリをテストするためには

iアプリエミュレータはこの通信制限を模擬します。
その際に用いるダウンロードしたサーバのURLはjamファイルのPackageURLで指定されたURLが絶対URLである場合これを用いますが、相対URLである場合、エミュレータでADFのURLを指定する必要があります。
ここで指定するURLは実際のADF(jamファイル)のURLではなく、実際のADFを配置する一つ上のディレクトリのURLを指定する必要があります。
(例:実際のADF(jamファイル)のURLが http://www.foo.com/download/prog/App1.jam である場合、"http://www.foo.com/download/prog/" を指定します)