Javaにおける4種類のインナークラス

Javaはクラス内にクラスを定義することが出来ます。クラス内に定義されたクラスはインナークラス(内部クラス)と呼ばれており、4種類の定義方法が存在します。定義方法によって異なった特性を持っており、間違った使い方をすると思わぬバグの元になり得ます。

本記事では4種類のインナークラスの違いのみを扱います。インナークラスの用途等は扱いません。今後取り上げるかもしれませんが。

本記事では、エンクロージングクラス = インナークラスからみて外側にあるクラスとします。

Javaにおける4種類のインナークラス

  • 非staticインナークラス
  • staticインナークラス
  • ローカルクラス
  • 無名クラス

非staticインナークラス

普通に定義したインナークラスです。

public class OuterClass {
    public static void main(String[] args) {
        new OuterClass();
    }

    private String className = OuterClass.class.getSimpleName();
    public OuterClass() {
        new InnerClass();
    }

    private class InnerClass {
        public InnerClass() {
            System.out.println(className); // OuterClass
        }
    }
}

非staticインナークラスはエンクロージングクラスへの暗黙的な参照を持ちます。その証拠として上記のコードのようにインナークラス内からエンクロージングクラスのインスタンス変数にアクセス可能です。この特徴は便利なように聞こえるかもしれません。確かに便利ではあるのですが、暗黙的な参照があるということはメモリリークの原因になることがあります。さらに言うと、暗黙的な参照はコード上に現れないため原因を特定するのが非常に困難である場合があります。

staticインナークラス

インナークラスの修飾子にstaticを付けるとstaticインナークラスとなります。

public class OuterClass {
    public static void main(String[] args) {
        new OuterClass();
    }

    private String className = OuterClass.class.getSimpleName();
    public OuterClass() {
        new InnerClass();
    }

    private static class InnerClass {
        public InnerClass() {
            System.out.println(className); // コンパイルエラー
        }
    }
}

staticインナークラスはエンクロージングクラスへの参照を持ちません。上記のコードで示したように、インナークラス内からエンクロージングクラスのインスタンス変数にアクセスすることは出来ません。

また、staticインナークラスはトップレベルに定義したクラスとほぼ同様に扱うことが出来ます。

ローカルクラス

メソッド内で定義するクラスです。正直あまり使ったことがありません。ローカルクラスを使うとクラスのスコープを狭めることが出来るため、バグを減らすテクニックとして使われるようです。

public class OuterClass {
    public static void main(String[] args) {
        class LocalClass {
            private String className = LocalClass.class.getSimpleName();
            public LocalClass() {
                System.out.println(className); // LocalClass
            }
        }
        new LocalClass();
    }
}

無名クラス

クラスの実装とインスタンス化を同時に行う手法です。

public interface OnClickListener {
    public void onClick();
}
public class OuterClass {
    private OnClickListener listener = new OnClickListener() {
        @Override
        public void onClick() {}
    };
}

まとめ

上記で説明したように非staticインナークラスはエンクロージングクラスへの暗黙的な参照を持ち、思わぬバグの原因となることがあります。Effective Javaでも言及されていますが、基本的にはインナークラスを作る際は非staticインナークラスではなく、staticインナークラスとして作る方が良さそうです。

非rootなAndroid端末でHierarchy Viewerを使う方法

Android SDKにはUI Viewの階層を確認することが出来るHierarchy Viewerというツールが入っています。このツールはrootedな端末かエミュレータでないと動作しません。最近はrootedでなくとも動くというという記述をいくつか見つけましたが、自分の環境(Nexus 5、MacBook AirAndroid Studio)ではうまく動作しませんでした。おそらく私が使い方を間違えてる可能性が濃厚ですが...

というわけで何か代替法はないかと探してみたところ、ViewServerというものを見つけました。これを使ってみたところ、自分の環境でもHierarchy Viewerが動作しましたので、手順をまとめておきます。

作業環境

導入

導入はとても簡単です。

まずは、ここから適当にクローンし、viewserver/src/main/java/com/android/debug/hv/ViewServer.javaを自分のプロジェクトにコピーすることで導入は完了です。本当はgradleを使って管理した方が良いと思いますが、とりあえず動作させるだけであればViewServer.javaを自プロジェクトにコピーするだけで動きます。

準備

ViewServerを動作させるためにソースコードを少しだけ修正する必要があります。

この辺はサンプルコードを見たら分かりますが、一応。

アプリケーションを起動した際に一番最初に起動されるエントリーポイントとなるActivityに以下の記述を追加してください。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ViewServer.get(this).addWindow(this);
}

@Override
protected void onResume() {
    super.onResume();
    ViewServer.get(this).setFocusedWindow(this);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    ViewServer.get(this).removeWindow(this);
}

Hierarchy Viewerの起動

USBで端末を繋いだら一応確認として以下のコマンドを実行します。

$ adb devices

繋いでいる端末が表示されればOKです。

以下のコマンドでHierarchy Viewerを起動することが出来ます。 パスは擬似的に書いていますので、適宜読み替えてください。

$ cd sdk/tools
$ ./hierarchyviewer

あとは、アプリを選択してLoad View Hierarchyを押せばOKです。

参考サイト

Javaのリフレクション

まずは以下のコードを見てください。

public class Hoge {
    private String str;

    public Hoge(String str) {
        this.str = str;
    }

    @Override
    public String toString() {
        return "str = " + str;
    }

    public static void main(String[] args) {
        Hoge hoge = new Hoge("Hoge");
        System.out.println(hoge);
    }
}

上記のコードをコンパイルして実行すると以下のようになります。

$ javac Hoge.java
$ java Hoge
str = Hoge

上記のコードに以下のコード片を追記します。

Class<Hoge> clazz = Hoge.class;
try {
    Field field = clazz.getDeclaredField("str");
        if (!field.isAccessible()) {
            field.setAccessible(true);
        }
        field.set(hoge, "Fuga");
        System.out.println(hoge);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}  

再度コンパイルして実行してみます。

$ javac Hoge.java
$ java Hoge
str = Hoge
str = Fuga

私のようなJava初心者は、privateなフィールドは何人たりとも犯すことは出来ない聖域であると固く信じていた訳ですが、どうやらそうではないようです。

UnicodeとUTFの違い

この2つは多分違うものなんだろうと思いつつ、今まで曖昧なままこれらの用語を使っていたので調べてみた。

Unicode

Unicodeは世界で使われる全ての文字を統一的にまとめた文字集合のことを指す。Unicodeという文字コードがあるわけではない。

UTF

計算機でUnicodeをどのようなバイト列として扱うかを規定したもので、所謂文字コードの1種である。UTFという名前の付いた文字コードはいくつもある。

以下は1バイト=8ビットで話を進める。

1-6バイト可変長の8ビット単位でUnicodeを表現する文字コード。ASCIIに含まれる文字は1バイト、それ以外の文字は2-6バイトで表現するため、ASCIIの上位互換に当たる。8ビット単位で符号化するためバイトオーダーは関係ない。

2バイト固定長の16ビット単位でUnicodeを表現する文字コード。実はUnicodeは16ビットでは全てを表現することが出来ないため、2つ合わせて4バイトで1文字を表す場合もある。16ビット単位で符号化するため、バイトオーダーを考慮する必要あり。

4バイト固定長の32ビット単位でUnicodeを表現する文字コード。もちろんバイトオーダーを考慮する必要あり。

  • その他

上記の他にも、UTF-5、UTF-7、UTF-9、UTF-18などがある。

補足

  • バイトオーダー

CPUはデータをメモリにどのように格納するかで2種類に分けることが出来る。それらはリトルエンディアンとビッグエンディアンと呼ばれる。リトルエンディアンはデータの上位から順番にメモリに格納する、ビッグエンディアンは逆。UTF-8はそもそも8ビット(=1バイト)単位であるため、バイトオーダーに依存しないという訳である。

まとめ

やたらUTF-8を持ち上げる形になってしまった。

参考サイト

Fragmentをネストする際の注意点

Fragmentが登場してからActivityに全てのコントローラーロジックが集約される悪夢から開放された訳ですが、公式リファレンスをちゃんと読まずに実装しているとハマりますよ、というお話。

そうそう、Fragment自体はHoneycombで登場しましたが、ネスト出来るようになったのはJelly Beanからなんですね。

作業環境

Fragmentのネスト

Fragmentを使って開発をしていると当然それらをネストすることもあると思います。その際にFragmentTransactionやFragmentManagerを使うと思いますが、ActivityのRoot ViewにFragmentを入れる場合とFragmentにFragmentを入れる場合では微妙にやり方が異なります。

前者の場合はいつも通りにトランザクションを発行すれば良いんですが、後者の場合はそれだと思った通りに動きません。

公式リファレンスには以下のような記述があります。

To nest a fragment, simply call getChildFragmentManager() on the Fragment in which you want to add a fragment. This returns a FragmentManager that you can use like you normally do from the top-level activity to create fragment transactions.

Fragmentをネストしたい場合は、getChildFragmentManager()を使わないといけないようです。以下はFragmentをネストする際のサンプルコードです。

Fragment childFragment = new ChildFragment();
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.add(R.id.child_fragment, childFragment).commit();

ChildFragment内から親Fragmentの参照が欲しい場合には、getParentFragment()で取得することが出来ます。ちなみに、getChildFragmentManager()は、Support Library v4に含まれているため1.6から使うことが出来ます。

余談

上記の公式リファレンスの最後にこのような注釈があります。

Note: You cannot inflate a layout into a fragment when that layout includes a . Nested fragments are only supported when added to a fragment dynamically.

Fragmentをネストする際は必ず動的に追加しないといけないようです。

参考サイト

iOS7でHTMLを取得してパースする

作業環境

準備

1. Objective-C-HMTL-Parserの導入

HTMLをパースするために「Objective-C-HMTL-Parser」というライブラリを使います。ソースコードGitHubで公開されていますので、上記のリンクから適当にローカルにクローンしてください。クローンしたもののうち、README.md以外をXcodeの自分のプロジェクトにコピーしてください。コピーする際に、「Copy items to destination's group folder」にチェックを必ず付けてください。

2. 依存ライブラリの追加

Objective-C-HMTL-Parserはlibxml2.dylibのラッパーですので、libxml2.dylibを追加しないと動きません。

General -> Linked Frameworks and Librariesでlibxml2.dylibを追加してください。

3. インクルードパスの追加

Objective-C-HMTL-Parserとlibxml2.dylibを追加していざ実行しようとするとビルドが失敗します。libxml/HTMLtree.hとlibxml/HTMLparser.hが見つからないようですので、libxmlをインクルードパスとして追加しないといけません。

Build Settings -> All -> Search Paths -> Header Search Paths -> Debugで$(SDKROOT)/usr/include/libxml2を追加してください。

Header Search Pathsは少々見つけにくい所にあります。Build Settingsを選択した後に、Allタブを選択しないとHeader Search Pathsが出てこないと思います。どうしても見つからない場合はこちら

HTMLの取得

以下は指定したページのHTMLを取ってくる部分のソースコードです。

NSURL *url = [NSURL URLWithString:@"URL"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];

if (!connection) {
    NSLog(@"Connection error");
}

NSURLConnectionをallocした時点で同期的にWebページにアクセスします。

データを受け取る

Delegateを用いてデータを受け取る部分のソースコードです。

データを受け取るクラスのヘッダでNSURLConnectionDataDelegateをプロトコルとして追加してください。

- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    receivedData = [[NSMutableData alloc] init];
}

- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [receivedData appendData:data];
}

- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
    NSString *html = [[NSString alloc] initWithBytes:receivedData.bytes length:receivedData.length encoding:NSShiftJISStringEncoding];
}

上記のソースコードでは、たまたまShift-JISなページを取得していたのですが、取得対象がUTF-8なページであれば、encodingをNSUTF8StringEncodingに変更してください。そして、connectionDidFinishLoadingメソッド内でNSStringに変換した後は煮るなり焼くなり好きにしましょう。

HTMLをパースする

Objective-C-HMTL-Parserを使って取得したHTMLをパースします。

NSError *error = nil;
HTMLParser *parser = [[HTMLParser alloc] initWithString:html error:&error];
HTMLNode *bodyNode = [parser body];
NSArray *aNodes = [bodyNode findChildTags:@"a"];
for (HTMLNode *node in aNodes) {
    NSLog(@"%@", [node getAttributeNamed:@"href"]);
}

上記のコードでは、HTMLのBodyに含まれるaタグを全て取得して、hrefをNSLogで吐き出しています。

参考サイト

VMware Toolsのインストール時にkernel headerが見つからない場合の対処法

ここ一週間程悩んでいた問題が解決したので、その対処法をまとめておく。

環境

ちなみに、Ubuntu 12.04 LTS、Ubuntu 13.04でも同様の問題が発生しました。

症状

UbuntuVMware Toolsをインストールしようとすると、以下のような文言が表示されてインストール出来ない。

Searching for a valid kernel header path...
The path "" is not a valid path to the 3.11.0-12-generic kernel headers.
Would you like to change it? [yes] 

Enter the path to the kernel header files for the 3.11.0-12-generic kernel?

原因の調査

カーネルヘッダーなるものが入ってないからだろうと思い、インストールを試みる。

$ sudo apt-get update
$ sudo apt-get install linux-headers-$(uname -r)
Reading package lists... Done
Building dependency tree
Reading state information... Done
linux-headers-3.11.0-12-generic is already the newest version.
0 upgraded, 0 newly installed, 0 to remove and 16 not upgraded.

どうやら既にインストールされている模様。

それなら話は早い。インストールされているディレクトリを探してみる。
どうやら以下のディレクトリにあるらしい。

/usr/src/linux-headers-3.11.0-12-generic

このパスをVMware Toolsのインストーラに食わせてみる。

The path "/usr/src/linux-headers-3.11.0-12-generic" is not a valid path to the 3.11.0-12-generic kernel headers.

やっぱりダメかー。
上記のディレクトリ配下のそれらしきディレクトリを指定してみるも、あえなく撃沈。

ここで自力での解決を諦め、Google先生に頼る。

割りとメジャーなエラーらしく対処方法がたくさん出てくる。

エラーの原因は、VMware Toolsがカーネルモジュールをコンパイルする際に使用するヘッダファイルが指定のディレクトリにないという感じらしい。

そして、その原因のファイルはこれ。

/usr/src/linux-headers-3.11.0-12-generic/include/generated/uapi/linux/version.h

このファイルが以下のディレクトリにないといけないらしい。

/usr/src/linux-headers-3.11.0-12-generic/include/linux

解決方法

version.hへのシンボリックリンクを置いておく。

sudo ln -s /usr/src/linux-headers-$(uname -r)/include/generated/uapi/linux/version.h /usr/src/linux-headers-$(uname -r)/include/linux/version.h