タグ「Java コード サンプル」が付けられているもの

Java static クラス

いまのところ、「Google Androidアプリケーション開発入門 画面作成からデバイス制御まで――基本機能の全容」 を教科書としてサンプルを実装したりしてるのですが、SQLite のサンプルで、インナークラスを static 宣言してるんですね。

Google のチュートリアルでも同様でした。

ちょっと、見慣れない記法だと思ったのですが、どう使い分けるのだろう?

ちょっと挙動を試してみました。

static_class

Bar クラスと、Foo クラスを作成。

Barクラス

package test;

public class Bar {
    private String m_str;
    public Bar() {
        m_str = "Bar";
    }
    class Ham {
        public String getValue() {
            // 外部クラスの静的でないメンバーが参照できる
            return "Ham | " + Bar.this.m_str;
        }
    }
    static class Spam {
        public String getValue() {
            // ② 外部クラスの静的でないメンバーは参照できない
            // return Bar.this.m_str;
            return "Spam";
        }
    }
    
    public void makeInstance() {
        // ① 同じようにインスタンスは作れる
        System.out.println(new Ham().getValue());
        System.out.println(new Spam().getValue());
    }
}

Fooクラス

package test;

public class Foo {
    public static void main(String[] args) {
        
        // ③ クラスからインスタンス化できない。インスタンス化してから
        // Bar.Ham h = new Bar.Ham();
        Bar.Ham h = new Bar().new Ham();
        
        // ④ クラスからインスタンス化できる
        // Bar.Spam s = new Bar().new Spam();
        Bar.Spam s = new Bar.Spam();
        
        System.out.println(h.getValue());
        System.out.println(s.getValue());
        
        new Bar().makeInstance();
    }
}

結果

Ham | Bar
Spam
Ham | Bar
Spam

 

static クラス なので、なんか、インスタンスが作れない!? みたいに思ってしまうが、そんなことはない。

それぞれ、アウタークラスから、インナークラスのインスタンスは作成できる。(①)

ただし、当たり前と言えば当たり前だが、sutatic クラスからは、アウタークラスの static でないメンバー変数にアクセスできない(②)

Bar のインナークラスの、Ham と Spam のスコープ指定をしていないので、同一パッケージからは参照できる。

それぞれ、を同一パッケージの Foo クラスから参照してみる。

static でないインナークラスをアウタークラス以外のクラスからインスタンス化するには、アウタークラスをインスタンス化する必要がある。(③)

しかs、static なインナークラスは、アウタークラス以外のクラスからインスタンス化するには、static メソッドを参照するのと同じ仕方でインスタンス化できる。(④)

staticのついた内部クラスは、パッケージのように使える」 とはそういう(④)意味か。

ちょっとしたクラス群をパッケージではなくて、クラスにまとめて使うには、有用そうだ。これが使い方の本命かしら。

static でない場合の書き方、

new Bar().new Ham();

これは、覚えにくいし、違和感あるが、

new Bar.Spam();

こっちの書き方なら自然だ。

ちょっと、JNIを使って、Java用のユーティリティを作ろうと思ったら、意外とチェックポイントが多かったので、サンプルプログラムを作り、手順をメモっておく。

題材

題材は何でもよかったが、手ごろな感じがするので、OSのメモリ使用量を調べるJNI用DLLを作成する。

概要

uml01

MemoryCheckerクラスは、Javaのウィンドウで、TextAreaにメモリ情報を出力するメソッドを追加した、MemoryListenerTextAreaを配置。

メモリ使用量を調査する抽象クラス、AbsMemoryManagerから、JVMのメモリ調査用クラスJavaMemoryManagerと、OSのメモリ調査用クラスJNIMemoryManagerを派生させる。

メモリ使用量データ保持用にMemoryStatusクラスを用意

uml02

動きとしては、MemoryChecker(ウィンドウ)から、JNIMemoryManagerを生成する。JNIMemoryManagerは、DLLをロードし、MemoryStatusのインスタンスを渡す。DLLでは、メモリ量を指定インターバル毎に調査して、保持するMemoryStatusのインスタンスにセットし、changeメソッドを呼ぶ。これが順に伝わり、TextAreaに状況を表示する。

win01

出来上がりはこんな感じ。

作成手順(Eclipse側)

DLL呼び出し元クラス

  1. DLLをロードする記述を追加
  2. DLLの関数を呼び出すメソッドをnative宣言
public class JNIMemoryManager extends AbsMemoryManager { 
  /** DLLをロード */ 
  static { 
    System.loadLibrary("MemStat"); 
  }  
  public JNIMemoryManager(IMemoryStatusListener listener, long inverval) { 
    super(listener, inverval); 
  } 
  public void changeStatus() { 
    this.listener.statusChenged(getMemoryStatus()); 
  } 
  /** DLLからMemoryStatusを取得 */ 
  public native MemoryStatus getMemoryStatus(); 
  /** DLLにMemoryStatusをセット */ 
  public native void setMemoryStatus(MemoryStatus memstat); 
  /** DLLのメモリ調査を開始 */ 
  public native void run(); 
  /** DLLのメモリ調査を終了 */ 
  public native void stop(); 
}

DLL指定の拡張子部分、dll、soは、Javaのライブラリが勝手に補完してくれるので不要。


C/C++ ヘッダーファイルを作成

  1. コンパイル済みJavaクラスから、javah コマンドを利用して、C/C++用のヘッダファイルを生成
  2. C/C++での処理用に利用するJavaクラスのシグネチャをjavapコマンドを利用して取得

上記の1.は最低限必要な作業。2.はDLL側からJavaのクラスのメソッドを呼び出すときに、メソッド名と、戻り値、引数のシグネチャが必要だが、この記述がJavaでの通常の記述とはことなるので、簡単に取得するために行う。

また、上記作業をつどコマンドラインから実行するのも面倒なので、以下の手順でAntのタスクとして登録しておく。

eclipse01

プロジェクトから、新規作成-ファイルで、build.xml という名前のファイルを追加し、以下の内容を記述する。

<project basedir="./"> 
  <property name="cpdir" value="${basedir}\bin"/> 
  <target name="create c header file"> 
    <!-- 
      JNI用のCヘッダーファイルを生成 
      -eg batch file 
         cd "C:\Program Files\eclipse3.3.2\workspace\JNISample\bin" 
             javah -jni info.typea.jnisample.mem.JNIMemoryManager 
     --> 
    <exec executable="javah"> 
      <arg value="-classpath"/> 
      <arg value="${cpdir}"/> 
      <arg value="-jni"/> 
      <arg value="info.typea.jnisample.mem.JNIMemoryManager"/> 
    </exec> 
  </target> 
  <target name="print signeture (MemoryStatus)"> 
    <!-- 
      JNI用シグネチャ確認(標準出力に出力) 
      -eg batch file 
      cd "C:\Program Files\eclipse3.3.2\workspace\JNISample\bin" 
      javap -private -s -classpath . info.typea.jnisample.mem.MemoryStatus 
    --> 
    <exec executable="javap"> 
      <arg value="-private"/> 
      <arg value="-s"/> 
      <arg value="-classpath"/>  
      <arg value="${cpdir}"/> 
      <arg value="info.typea.jnisample.mem.MemoryStatus"/> 
    </exec> 
  </target> 
</project>

あとは、Eclipseのメニュー- Window- Show View から、Antを開くと、今の設定が表示されるので、該当を選択して、実行ボタンを押すとヘッダーファイルが作成される。

今回の例では、パッケージを展開したやたら長い名前のヘッダーファイルが作成される。

info_typea_jnisample_mem_JNIMemoryManager.h

eclipse02

とりあえず、ここまででEclipse側は終了。

作成手順(VisualStudio側)

プロジェクトの作成

VisualStudio側では、VC++を利用して、MFC拡張DLLを作成する。

mfc01

プロジェクト名を、上記Java側でロードするDLLの名称にする。

mfc02

MFC拡張DLLを指定

JNI用ヘッダーファイルの取り込み

mfc03

先ほど作成した「info_typea_jnisample_mem_JNIMemoryManager.h」を、作成されたプロジェクトの適当なところにコピーして、ソリューションエクスプローラのヘッダーファイルのコンテキストメニューから、既存の項目を選んで取り込む。

mfc04

実装用に、「info_typea_jnisample_mem_JNIMemoryManager.cpp」ファイルを、同じくソリューションエクスプローラのソースファイルから新しい項目として追加する。

環境設定

JNI用のインクルードファイルの場所をVisualStudioに設定する。

メニュー-ツール-オプションでオプションダイアログを開き、「プロジェクトおよびソリューション-VC++ディレクトリ」を選択し、「ディレクトリを表示するプロジェクト」に、「インクルードファイル」を選択

mfc05

ここに、JavaSDKのjni.hを含む、includeディレクトリ、jni_md.hを含むwin32ディレクトリを指定する。

ちなみに、今回の環境だと、

  • C:\Program Files\Java\jdk1.6.0_06\include
  • C:\Program Files\Java\jdk1.6.0_06\include\win32

の2つとなる。

JVM引数の指定(Eclipse側)

テスト段階では、コンパイルされたDLLをEclipseのデバッグ環境から認識させるために、以下の設定を行う。

mfc06

実行するクラスのコンテキストメニュー-Debug As-Open Debug Dialogを選択すると、上記ダイアログが起動するので、Argument タブの VM argumentsに、-Djava.library.path オプションを指定する。値は、DLLのパス。

今回の例では、

-Djava.library.path="C:\Documents and Settings\Administrator\My Documents\Visual Studio 2005\Projects\MemStat\Debug"

となる。

ここまでで、基本的な設定は完了なので、後は、実装を行う。

DLLの実装

JNIのリファレンスは、ここらあたりに、あるので結局はそこを読めばよいのだが、いかんせんなじみがないので、素直には読み進められなかった。

上記シナリオに基づいて、作成したコードに補足する。

#include "stdafx.h"
#include "info_typea_jnisample_mem_JNIMemoryManager.h"

jobject global_memstat; // (2)
boolean _stopFlag;

/*
* Class: info_typea_jnisample_mem_JNIMemoryManager
* Method: getMemoryStatus
* Signature: ()Linfo/typea/jnisample/mem/MemoryStatus;
*/
JNIEXPORT jobject JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_getMemoryStatus
(JNIEnv *env, jobject thisObj){ // (1)
return global_memstat;
}

/*
* Class: info_typea_jnisample_mem_JNIMemoryManager
* Method: setMemoryStatus
* Signature: (Linfo/typea/jnisample/mem/MemoryStatus;)V
*/
JNIEXPORT void JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_setMemoryStatus
(JNIEnv *env, jobject thisObj, jobject memstat) {

global_memstat = env->NewGlobalRef(memstat); // (2-1)
}

/*
* Class: info_typea_jnisample_mem_JNIMemoryManager
* Method: start
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_run
(JNIEnv *env, jobject thisObj){

// (3)
jclass mng = env->GetObjectClass(thisObj);
jmethodID jmIntvl = env->GetMethodID(mng, "getInterval", "()J");
jlong interval = env->CallLongMethod(thisObj, jmIntvl);

_stopFlag = false;

jclass jc = env->GetObjectClass(global_memstat);
jmethodID jmChng = env->GetMethodID(jc, "change", "()V");

jmethodID jmMemLd
= env->GetMethodID(jc, "setMemoryLoad", "(I)V");
jmethodID jmTtlPs
= env->GetMethodID(jc, "setTotalPhys", "(I)V");
jmethodID jmAvlPs
= env->GetMethodID(jc, "setAvailPhys", "(I)V");
jmethodID jmTtlPf
= env->GetMethodID(jc, "setTotalPageFile", "(I)V");
jmethodID jmAvlPf
= env->GetMethodID(jc, "setAvailPageFile", "(I)V");
jmethodID jmTtlVr
= env->GetMethodID(jc, "setTotalVirtual", "(I)V");
jmethodID jmAvlVr
= env->GetMethodID(jc, "setAvailVirtual", "(I)V");

MEMORYSTATUS memstat;

while (!_stopFlag) {
GlobalMemoryStatus( &memstat );

env->CallVoidMethod
(global_memstat, jmMemLd, memstat.dwMemoryLoad);
env->CallVoidMethod
(global_memstat, jmTtlPs, memstat.dwTotalPhys / 1024);
env->CallVoidMethod
(global_memstat, jmAvlPs, memstat.dwAvailPhys / 1024);
env->CallVoidMethod
(global_memstat, jmTtlPf, memstat.dwTotalPageFile / 1024);
env->CallVoidMethod
(global_memstat, jmAvlPf, memstat.dwAvailPageFile / 1024);
env->CallVoidMethod
(global_memstat, jmTtlVr, memstat.dwTotalVirtual / 1024);
env->CallVoidMethod
(global_memstat, jmAvlVr, memstat.dwAvailVirtual / 1024);
env->CallVoidMethod(global_memstat, jmChng);

SleepEx(interval, FALSE);
}
}

/*
* Class: info_typea_jnisample_mem_JNIMemoryManager
* Method: stop
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_stop
(JNIEnv *env, jobject thisObj) {
_stopFlag = true;

env->DeleteGlobalRef(global_memstat); // (2-1)

global_memstat = NULL;
}

 

(1) 関数宣言

1つ目の引数に、JNIEnvとして、JNI用の関数テーブルが渡される。これを利用して各種操作を行う。

2つ目の引数には、呼び出し元オブジェクトが渡される。

(2) グローバル参照

DLLの関数をまたがって、Javaのインスタンスを保持したい場合、(2)のように宣言して、初回の関数呼び出し時の引数jobjectを代入しておくだけでは、次回の関数呼び出し時に、その値は利用できない。そのような場合、グローバル参照を利用する必要があり、作成時には、

NewGlobalRef

破棄時には、

DeleteGlobalRef

を使用する。

(3) Javaメソッドの呼び出し

1:  JNIEXPORT void JNICALL Java_info_typea_jnisample_mem_JNIMemoryManager_run


    (JNIEnv *env, jobject thisObj){

2:  jclass mng = env->GetObjectClass(thisObj);
3:  jmethodID jmIntvl = env->GetMethodID(mng, "getInterval", "()J");


4:  jlong interval = env->CallLongMethod(thisObj, jmIntvl);

今回の例で、nativeメソッド呼び出し元である、JNIMemoryManagerクラスのgetIntervalメソッドを、JNI関数の中から呼び出す場合、

1行目:JNIMemoryManagerのnativeメソッドrunが呼び出される。2つ目の引数に、JNIMemoryManager自身のインスタンスが設定されている。

2行目:JNIMemoryManager自身のインスタンスから、GetObjectClass関数を利用して、jclassを取得する。

3行目:取得した、jclassに、メソッド名、シグネチャを指定(ここで、上記で作成した、Antタスクを利用すると便利 ちなみに、()Jは、引数なし、戻り値long)してGetMethodIDを呼び出すと、jmethodIDを取得できる。

4:戻り値にあわせたCallxxxMethod関数を利用して、メソッドを呼び出す。

 

と。とりあえずはこんなところ。

まぁあとはリファレンスを見ながら何とかなるかな。

ソースコードはここ

ちょっと古い本だけど、JNIについて、丁寧に説明されています。

java を学んだ人にとって、「Stringの比較は、== ではなく、equalsメソッドを利用する」というのは、常識でしょう。

「==」はオブジェクトのインスタンスが同じかどうかを判定するんであって、インスタンスが同値かは「equals」を使って判定します。

ちょっとややこしいのは、文字列プールという仕組みがあるために、リテラルの文字列については、インスタンスを共有するので、次のように同じ"abc"をあらわす文字列でも、「==」で比較したときに、結果がtrueになったりfalseになったりする点でしょう。

    String a = "abc";
    String b = "abc";
    String c = new String("abc");
    System.out.println(a == b);   // これは、true
    System.out.println(a == c);   // これは、false


でも、equalsメソッドなら、文字列が表す値が同じならtrueを返してくれます。
なので、文字列の比較では「常に equalsメソッドを使う」という単純な戦略でオーケーです。

で、待望(?)のオートボクシング/アンボクシング機能を見てみますと、参照型で「==」はオブジェクトのインスタンスが同じかどうかの比較だったんですが、アンボクシングが行われるため、以下のようなプリミティブ型との比較の場合、同一インスタンスであるかどうかの比較ではなく、同じ値であるかという比較が行われます。

    int i1       = 77; // プリミティブ型
    Integer i2 = 77;
    System.out.println(i1 == i2); // true 同値

また、以下の例のように、参照型どうしを比較しても、trueが返ります。

    // インスタンスの比較1
    Integer i3 = 99;
    Integer i4 = 99;
    System.out.println(i3 == i4); // true

    Boolean b1 = true;
    Boolean b2 = true;
    System.out.println(b1 == b2); // true

すばらしい!今までプリミティブ型を使ってたところをクラスに置き換えちゃえ!富豪的プログラマ(?)を目指すなら、次のようなコードもためらわずにかけちゃうかもしれません。オブジェクト指向的にもベター(プリミティブ型の存在が欺瞞だったのだ!)のかも?

    Integer max   = 999;
    Integer case1 = 100;
    Integer case2 = 300;

    for (Integer i=0; i < max; i++) {
        if (i == case1) {
            :
        }
        if (i == case2) {
            :
        }
    }

しかし、ちょちょっとまってください。一見便利になったような「==」による比較ですが、簡単に信用するのは甘すぎました。上の例では、 case2 のブロックが実行されない可能性があるんです。

    // インスタンスの比較2
    Integer j1 = new Integer(99);
    Integer j2 = new Integer(99);
    System.out.println(j1 == j2); // false

    // -128 ~ 127 以外は false となる!!
    Integer j3 = 200;
    Integer j4 = 200;
    System.out.println(j3 == j4); // false

    // Boolean も同様!
    Boolean b3 = new Boolean(true);
    Boolean b4 = new Boolean(true);
    System.out.println(b3 == b4); // false

確かに、プリミティブ型と参照型との比較では、参照型がアンボクシングされてプリミティブ型どうしの比較と同じことになっていました。要するに、

   i1 == i2

というのは、

   i1 == i2.intValue(); // i2 がアンボクシングされる。

のシンタックスシュガーだったわけです。

けれども、実は、参照型どうしの比較の場合、アンボクシングは行われません。要するに今までどおりの同一インスタンスかを比較する「==」なのです。

ん?

    Integer i3 = 99;  // ボクシング
    Integer i4 = 99;  // ボクシング
    System.out.println(i3 == i4); // true

    Boolean b1 = true; // ボクシング
    Boolean b2 = true; // ボクシング
    System.out.println(b1 == b2); // true

さっき、このコードでは true になったやんけ~

なんと、実は、true, false, byte, char(\u0000 から \u007f), int もしくは short (-128 and 127) をボクシングにより作成した場合は、「==」を使った結果も等しくなる。。。という仕様なのです。

実際ソースコードを覗いてみると、例えば、Integer.IntegerCache というスタティックなインナークラスに、-128 ~ 127 のインスタンスがキャッシュされていて、その範囲の値は、そのキャッシュされたインスタンスを利用するようになっているようです。

要するに、ボクシングを利用して生成されたインスタンスは、上記の範囲の値に限って、Stringの文字列プールと同様のことがおこるわけです。

しかも、「プール」されるのは、-128 ~ 127 の範囲に限られうえ、文字列のようにリテラルに限られるわけでもありません。

public static void main(String[] args) {
    int i0 = Integer.parseInt(args[0]);
    Integer i1 = i0;
    Integer i2 = i0;
    System.out.println(i1 == i2);
}

のようなプログラムでは、arg[0] の値が、-128 ~ 127 に含まれるか否かによって、結果が変わってきてしまいます。

なかなか、複雑な仕様ではありませんか!?
文字列の比較では「常に equalsメソッドを使う」という戦略でOKでしたが、ボクシング、アンボクシングを考慮したときにはどういう戦略をとるのがベターなんでしょう?

まぁ、こういう仕様になったのにはおそらく深遠な理由があるのでしょうが。
わかりにくーーいバグの温床になっちゃう予感が激しくするのは僕だけでしょうか。

C#とかどういう仕様になってるんだろ。