Skip to content

Instantly share code, notes, and snippets.

@nobuoka
Last active May 14, 2023 14:47
Show Gist options
  • Star 118 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save nobuoka/6546813 to your computer and use it in GitHub Desktop.
Save nobuoka/6546813 to your computer and use it in GitHub Desktop.
Android アプリ開発勉強会のために書いた Java の入門文書

Android アプリ開発のための Java 入門

MEMO

  • declaration は 「宣言」 と訳しているが、「定義」 の方が適しているような気がしなくもない。
  • 「インスタンス」 と 「オブジェクト」 という言葉を使うことがあるが、本文書中ではどちらも同じ意味で使用している。
  • String オブジェクト」 という表現は、「String クラスのインスタンス」 を意味している。 (Java に限らず一般的な表現だと思う。)

はじめに

この文書は Android アプリ開発をしようと思うプログラマのための Java の入門文書である。 まともに Android アプリを書くために最低限必要だと思われる知識をひととおり記述している。 また、C の流れをくむ文法であるため、C やその類似言語を知っている場合には既知であろうと考えられる項目については詳しく説明していない。

基本的なプログラミングの知識はあるが、Java の知識はないという人が対象である。 「Android アプリ開発のための」 というタイトルだが、Dalvik 仮想マシンなどの紹介などがある程度で、基本的には Android アプリ開発に限らない Java の話が書かれている。

Java の生態系

Java とは

単に言語としての Java を指して 「Java」 と呼ぶこともあるが、通常 「Java」 と言えば Java 言語だけでなく Java Platform も含むことが多いように思う。 Java Platform の主要な部分は、Java Application Programming Interface (Java API) と Java Virtual Machine (JVM) である。

  • Java API は、Java 言語から標準で使用できるライブラリ群の API である。
  • JVM は、Java バイトコードを実行するための仮想機械である。

Java のエディション

Java のエディションとしては以下のものがある。

  • Java Platform, Standard Edition (Java SE) : Java 言語、Java API、JVM の仕様を含んでいて、通常のデスクトップアプリケーションなどの開発に用いられる
  • Java Platform, Enterprise Edition (Java EE) : Java SE の内容に加え、エンタープライズ向けの内容が含まれる
  • Java Platform, Micro Edition (Java ME)

これらは仕様であり、JSR として定義 (?) されている。

実装

広く使われている Java SE 実装として、Oracle がリリースしている JDK とオープンソースの OpenJDK がある。 Java SE 7 のリファレンス実装は OpenJDK 7 である。

Android アプリ開発のための Java 環境の準備

Android アプリのための仮想マシン

Dalvik 仮想マシン (Dalvik VM) 上で Android アプリは動く。 Dalvik VM は Dalvik Executable (.dex) フォーマットのファイルを実行するものであり、JVM ではない。 しかし、Java API (のサブセット? 全部?) を使用できる。 Java 言語のコンパイラによってコンパイルされたクラスを dx ツールによって .dex フォーマットに変換することができる。

アプリはそれぞれ固有の VM 上で実行される。

Android includes a set of core libraries that provides most of the functionality available in the core libraries of the Java programming language.

Every Android application runs in its own process, with its own instance of the Dalvik virtual machine. Dalvik has been written so that a device can run multiple VMs efficiently. The Dalvik VM executes files in the Dalvik Executable (.dex) format which is optimized for minimal memory footprint. The VM is register-based, and runs classes compiled by a Java language compiler that have been transformed into the .dex format by the included "dx" tool.

開発環境の準備

Java アプリケーションの開発によく用いられる IDE として Eclipse というものがある。 Android アプリ開発用のプラグインがプリインストールされた Eclipse を含む開発キットが配布されているので、それを使用すれば簡単に Android アプリ開発環境を構築できる。

また、必要環境として JDK 6 がある。 2013 年 5 月現在の最新の JDK は JDK 7 であり、JDK 7 でも大丈夫そうな雰囲気を感じる。 (が、安全のため JDK 6 を使った方がいいのかもしれない。 JDK 7 を使っている場合でもコンパイラは Java SE 6 互換で動かす必要がある。)

Android 端末の実機を使ってデバッグをするために、実機のための USB ドライバをインストールする必要もある。

Android アプリ開発用のプラグインがインストールされている Eclipse でも、通常の Java プロジェクトを作成できるので、本文書のサンプルコードを Android アプリ開発用の Eclipse で書いて動かすこともできる。

しばらく前に Android Studio という IDE がプレビューリリースされていて、将来的には公式的な Android アプリ開発環境は Android Studio に移る雰囲気があるので、今から Android アプリ開発を始めるのであれば Android Studio を使う方が良いように思う。

実際にしばらく使用しているが、特にバグで困ることもなく開発できている。 Eclipse に慣れている人にとっては移行の障壁は大きいかもしれないが、ビルドシステムが Gradle であることなど Android Studio を使う利点はあるので、移行を検討しても良いかと思う。

参考文献

全体的なドキュメントのトップは下記ページ。 Java SE 6 の時代は日本語が充実してた。

言語仕様や API 仕様の各々は下記ページ。

実際にプログラムを書くときもここら辺を見ながらコードを書くことが多い。 本文書中で Java API ドキュメントなどへリンクしてる箇所では、Java SE 6 のドキュメントと 7 のドキュメントが混ざってる。

Java 言語の特徴

  • クラスベースのオブジェクト指向言語
  • 強い静的型付け
    • 型は大きく分けて数値や真偽値といったプリミティブ型と何らかのクラスやインターフェイスといった参照型の 2 つ
  • C や C++ の流れをくむ語句、文法
  • パッケージによるクラスとインターフェイスの名前空間分割

Hello world

Java 言語において、基本的なプログラミングの単位はクラスである。 主なプログラムの処理はクラス中のメソッドに記述する。

java コマンドで実行される処理は、java コマンドの引数として指定したクラスの main メソッドの処理である。 main メソッドは static で public でなければならず、返り値は void で、引数として String[] の値のみをとらなければならない。 クラスやメソッドについては詳細は後述する。

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

コマンドライン上で作業するならば、上記内容を Test.java というファイルに保存して、下記コマンドでコンパイルと実行。

$ # コンパイル
$ javac Test.java

$ # 実行
$ java Test
Hello world

$ # コンパイルと実行を 1 行で書く場合
$ javac Test.java && java Test
Hello world

IDE を使うなら同じ内容のファイルを作って、IDE からビルドと実行を行えるはず。

Eclipse の話

とりあえず Shift + Space でコード補完できるのさえわかってれば、あとは適当に右クリックとかでショートカット確認したりすれば良いような気がする。

Java プログラムがどのように実行されるか

Java のソースコードは、javac コマンドでクラスファイル (Java バイトコード) にコンパイルされる。 java コマンドはクラス名を引数にとり、JVM を起動してそのクラスに宣言されている main メソッドを実行する。

クラスファイルは、探索対象となっているディレクトリ内から探される。 探索対象のディレクトリは、java コマンドの -classpath オプションで渡したり、環境変数 CLASS_PATH で設定したりできる。

ちなみに、パッケージに属するクラスはそのパッケージに応じてディレクトリ分けされる。 (パッケージについては後で述べる。) 例えば、完全修飾名が com.example.SampleClass というクラスが存在する場合、そのクラスファイル (SmapleClass.class) はクラスファイル探索の対象となっているディレクトリや JAR ファイル、ZIP ファイルの中の com/example/SampleClass.class という位置に置かれる必要がある。

JAR ファイル

ライブラリを配布する場合など、複数のクラスファイルを 1 つのファイルにまとめたいことはよくある。

JAR ファイル (実態は ZIP ファイル) として複数のクラスファイルをまとめることができる。 この場合も JAR ファイルの中のクラスファイルのパスは、上に書いたようにパッケージに対応させる。

サンプルコードの実行

単に文が並んでいるようなサンプルコードの場合は、下記のようなファイルを作って、上の HelloWorld の場合と同じようにコンパイルと実行をすればよい。 ファイル名とクラス名は同じものにしておくこと。

// import 文があればここに書く

public class Test {
    public static void main(String[] args) {
        // ここにサンプルコードの文を入れる
    }
}

字句、値、変数

Java のソースコードには Unicode 文字を使用できる。 (コンパイル時にエンコーディングの指定が可能。) 例えば、メソッド名に日本語を使用することも可能である。 また、ソースコード中にエスケープシーケンス \uXXXX も使用できる。 ここで XXXX は 16 進数値を表す。 エスケープシーケンスの u は 2 個以上並べることもできる (が、通常は 1 個だけ書けばよい)。

コメント

コメントは C99 形式の // で始まる行コメントと /**/ で囲まれたブロックコメント、そして /***/ で囲まれたドキュメンテーションコメントがある。 ドキュメンテーションコメントはクラスやフィールド、メソッドなどの宣言の前に記述され、自動的に生成されるドキュメンテーションに含まれる。

予約語

50 のキーワードが予約語となっている。

Java の型は大きく分けて基本データ型と参照型に分けられ、基本データ型は次の通り。

  • boolean : true または false。 真偽値。
  • char : UTF-16 の 1 文字分 (16 ビット)。 サロゲートペアの場合は上位と下位でそれぞれ 1 文字。
  • byte : 符号付 8 ビット整数
  • short : 符号付 16 ビット整数
  • int : 符号付 32 ビット整数
  • long : 符号付 64 ビット整数
  • float : 32 ビット浮動小数点数 (IEEE 754)
  • double : 64 ビット浮動小数点数 (IEEE 754)

各基本データ型に対して対応するクラスが存在し、それらは一般的にラッパークラスと呼ばれる。 例えば、int に対応するラッパークラスは Integer クラスである。 基本データ型とラッパークラスの間の変換は自動的に行われる。

Integer val = 3; // int 型の値が自動的にラッパーオブジェクトに変換される

基本データ型からラッパーオブジェクトへの変換はボクシング変換と呼ばれ、逆の変換はアンボクシング変換と呼ばれる。

ラッパーオブジェクトが存在する意義の 1 つは、HashMap のようにオブジェクトしか格納できないところに基本データを保持させたい場合に、ラッパーオブジェクトにして保存できるというところにある。

int key = 200;
map.put(key, value);

また、他の意義としてラッパーオブジェクトを表すクラスに、基本データ型のためのメソッドが定義されているというものがある。 具体的にどのようなメソッドがあるのかは API 仕様を見ること。

参照型は、クラスやインターフェイスなどによって定義される型である。 Java 言語や Java API によってあらかじめ定義されているもの (例えば上の IntegerCharacter) もあるし、開発者が独自に定義することもできる。 Java においては配列や文字列も参照型である。 詳細は後述する。

リテラル

基本データ型はそれぞれ対応するリテラルの記法を持っている。

  • 真偽値リテラル : truefalse
  • 文字リテラル : 'a''あ' など。 '\n' (改行) や '\'' (シングルクォーテーション) など、いくつかの特殊文字はエスケープシーケンスで表現できる。 コード中の他の場所でも使用できる \uXXXX 形式のエスケープシーケンスも当然使用できる。
  • 整数リテラル : 200042 (0 で始まると 8 進数)、0xF3F (16 進数) など。
  • 浮動小数点リテラル
    • 10 進数形式 : 18.1.8e1180.0e-1 など (指数部はオプション)
    • 16 進数形式 : 0x12p00x1.2p40x.12P+8 など (2 進指数部 (P あるいは p とその後ろの数字) は必須)
    • 後ろに F あるいは f を付けると float 型、何もついていないか D あるいは d が付けられていると double

基本データ型でなくてもリテラルは存在する。

  • null : オブジェクト参照の唯一のリテラル。 参照が期待されるところで使用できる。
  • 文字列リテラル : ダブルクォーテーションで囲まれた部分。 "日本語\u0039\n" とか。 文字列リテラル String オブジェクトへの参照である。
  • クラスリテラル : 全ての型には、対応する Class オブジェクトが存在する。 型名の後ろに .class と付けるとその Class オブジェクトへの参照を取得できる。 String.classboolean.class など。 (クラスリテラルはリフレクションで主には使用されるものであり、通常は目にすることはほとんどないはず。)

変数

変数の宣言として、型と識別子 (名前) が最低限必要である。 さらに、修飾子や初期化子を持つこともできる。

// String 型の変数 name ("太郎" で初期化)
String name = "太郎";
// final 修飾子が付けられた int 型の変数 count (1000 で初期化; final 修飾されているので初期化後は代入できない)
final int count = 1000;

配列変数は次のように宣言する。

// String 型の値を要素とする配列変数とその初期化
String[] names = { "太郎", "次郎", "三郎" };
// int[] 型の値を要素とする配列変数
int[][] values;

Java では、配列の配列を作ることで 2 次元配列 (あるいはより高次元の配列) を表現することができる。

演算子と式

算術演算

オペランドが基本数値型の場合。

  • + : 和
  • - : 差
  • * : 積
  • / : 商
  • % : 剰余

単項の +- もあり、それぞれ符号をそのままにしておくものと符号を反転するものである。 剰余は (x/y)*y + x%y == x の規則に従うようになっている。

整数割り算では、余りは 0 の方向に切り捨てられる。 (-7/2-3。) 浮動小数計算の場合とは違い、0 での除算でゼロ除算例外が発生したりする。

浮動小数計算では、無限大へオーバーフローしたり、アンダーフローしたりすることがある。 無限大で無限大を割るような不正な式の結果は NaN (Not a Number) になる。 除算や剰余算では無限大や NaN になることはあるが、例外を送出することはない。

インクリメントとデクリメント

  • ++, -- : インクリメント演算子とデクリメント演算子。

インクリメント演算子とデクリメント演算子は、数値変数か数値の配列要素にだけ適用できる。 前置と後置があり、後置の場合は元の値が使われた後に適用される。

関係演算子と等価演算子

  • >, >=, <, <= : 大なり、以上、小なり、以下
  • ==, != : 等しい、等しくない

結果は全て boolean 値になる。

NaN についてのみは注意を要する。 これらの演算子がオペランドとして NaN をとった場合、!= を除き全て false を返す。 != は常に true を返す。 NaN であるかどうかの検査には、Fload.isNaN(float)Double.isNaN(double) を使用すること。

論理演算子

真偽値に対する演算子。

  • & : 論理積
  • | : 論理和
  • ^ : 排他的論理和
  • ! : 論理否定
  • && : 条件積
  • || : 条件和

&&& と違い、左辺が偽の場合に右辺が評価されない。 同様に、|| は左辺が真の場合に右辺が評価されない。

ビット操作

オペランドは整数値 (char も含む) のみ。

  • & : ビット積
  • | : ビット和
  • ^ : 排他的ビット和
  • << : ビットを左にシフトさせて、右の空いたビットをゼロで埋める。
  • >> : ビットを右にシフトさせて、左の空いたビットを (シフト前の) 最上位ビットで埋める。
  • >>> : ビットを右にシフトさせて、右の空いたビットをゼロで埋める。

文字列結合

  • +

オペランドの片方が文字列であれば、文字列結合演算子だと判断される。 片方だけが文字列の場合、もう一方は暗黙的に文字列に変換される。

その他

  • ?: : 条件演算子。
  • = : 代入演算子。 += などの複合代入演算子もある

new によるインスタンス生成や型検査

new 演算子を使用して配列インスタンスやクラスインスタンスの生成ができる。

配列の場合。

int[] values;
// 長さ 10 の int 型配列のインスタンスを生成
values = new int[10];

// 配列の配列も簡単にインスタンス化できる
int[][] values2D;
values2D = new int[10][5];
// 上の文は次の 2 行と同等の処理

values2D = new int[10][];
for (int i = 0; i < values2D.length; ++i) values2D[i] = new int[5];

// 生成と同時に要素の初期化もできる
String names;
names = new String[]{ "太郎", "次郎", "三郎" };

クラスインスタンスの生成。

String str = new String("文字列"); // 実際には単なる文字列リテラルでよい

java.io.File testFile = new java.io.File("test.tmp");

instanceof 演算子で、インスタンスの実際の型が右辺の型と一致するあるいは右辺の型のサブクラスであるかどうかを検査できる。

boolean isStr = (obj instanceof String);

配列の要素へのアクセスやインスタンスのメンバーへのアクセス

int[] vals = { 1, 2, 3 };
// 配列の要素へのアクセス
System.out.println(val[1]); //=> 2

// フィールド参照
System.out.println(val.length); //=> 3

// メソッド呼び出し
String str = "文字列";
System.out.println(str.length()); //=> 3
    // 文字列の char 数 (Unicode 文字数ではない) を取得するのは length メソッド.
    // JavaScript と違って、メソッド呼び出しのための `()` を付けなかった場合にメソッドを
    // 表すオブジェクトを取得できたりはしない. (メソッドは第 1 級のオブジェクトではない)

優先順位

上の方が優先順位が高い。

  • 後置演算子 [], ., (params), expr++, expr--
  • 単項演算子 ++expr, --expr, +expr -expr, ~, !
  • 生成とキャスト new, (type)expr
  • 乗除 *, /, %
  • 加減 +, -
  • シフト <<, >>, >>>
  • 関係 <, >, >=, <=, instanceof
  • 等値 ==, !=
  • &
  • 排他的和 |
  • |
  • 条件積 &&
  • 条件和 ||
  • 条件 ?:
  • 代入 =, +=, -=, *=, %=, >>=, <<=, >>>=, &=, ^=, |=

文字列と正規表現

  • UTF-16 の 1 文字 (サロゲートペアは上位と下位でそれぞれ 1 文字) を表す char 型の値の連続物としてテキストを扱う

文字列 (String オブジェクト)

文字列は String オブジェクト (CharSequence インターフェイスを実装している)。

  • オブジェクトなので、同じ内容の文字列 (同値) でも同一とは限らない
  • str.equals("文字列") が真でも str == "文字列" が偽になりえる (equals メソッドで同値性チェックすること)
  • 文字列内の char 値の個数は length メソッドで取得できる (フィールドではない)
    • 文字数を数えるための codePointCount メソッド など、Unicode 文字を 1 文字として扱うためのいくつかのメソッドがある
System.out.println("文字列".length()); //=> 3 (Unicode 文字の個数ではなく char 値の個数なのでサロゲートペアによる 1 文字は 2 個分になる)

同じ内容の文字列であれば常に同じインスタンスを示すようにするために intern メソッド が使用できる。 同一性検査の速度的優位を利用したい場合に有用である。

String s1 = new String("文字列");
String s2 = new String("文字列");
System.out.println(s1          == s2         ); //=> false
System.out.println(s1.intern() == s2.intern()); //=> true

文字列とバイト列の変換

// "あいうえお" という文字列が UTF-8 エンコードされたバイナリ列
byte[] encodedStr = "あいうえお".getBytes( Charset.forName("UTF-8") );

String str = new String( encodedStr, Charset.forName("UTF-8") );

短い文字列の変換ならこんな感じで良い。 長ければ出力ストリームを使うとか。

StringBuilder クラス

1 文字ずつ追加していってテキストを構築する場合などには StringBuilder クラス を使用すると便利。

正規表現マッチングと置換

import java.util.regex.Pattern;
import java.util.regex.Matcher;

// 正規表現マッチング
Pattern pat = Pattern.compile("aa");
Matcher matcher = pat.matcher("ababaabbb");
System.out.println(matcher.find()); //=> true

// 置換
Matcher matcher2 = pat.matcher("[aa]ccc[aa]");
StringBuffer sb = new StringBuffer();
while (matcher2.find()) matcher2.appendReplacement(sb, "bb");
matcher2.appendTail(sb);
System.out.println(sb.toString()); //=> [bb]ccc[bb]
// より簡単な方法としては replaceAll メソッドを使用する
Matcher matcher3 = pat.matcher("[aa]ccc[aa]");
System.out.println(matcher3.replaceAll("bb")); //=> [bb]ccc[bb]

基本データ型の操作や配列の操作

上にも書いたように、基本データ型のための便利なメソッドはそのラッパークラスに定義されている。 また、配列操作のための便利なメソッドは java.util.Arrays クラス に定義されている。

// 文字列 "FF" を 16 進数としてパース
int val = Integer.parseInt("FF", 16);

// 配列の中身を昇順でソート
int[] vals = { 3, 2, 5, 2 };
java.util.Arrays.sort(vals);

配列の中身のコピーのための System.arraycopy メソッド というものもある。

制御の流れ

  • 2 つの基本文
    • 式の文 : 全ての式が文になれるわけでない。 単に変数を評価するだけの式など、無意味な式を文にしようとするとコンパイルエラー。
    • 宣言文 : ローカル変数宣言文とかローカルクラス宣言文。 ブロック内のどこにでも書くことができる。 スコープはブロックスコープ。
  • Java ではセミコロンは文を終了させるもの (ターミネータ); なので、ブロックの最後の文でもセミコロンを記述しなければならない。

本節では、条件分岐やループなどのための文を説明する。

if-else

if (val < 100)
    /* statement */ ;
else
    /* statement */ ;

条件の式の評価結果は boolean 型か Boolean 型の値でなければならない。 C のように、if の中身を {} で囲む必要はない。 (単一の文が if の中身とみなされる。 複数の文を実行したい場合はブロックにして複数の文を単一の文にまとめる必要がある。)

switch

switch (val) {
  case 10:
    break; // 何もしない (switch ブロックを抜けるために break する必要がある)
  case 20:
    val2 = 20;
    // FALLTHROUGH (break (あるいは return など) がなければ switch ブロックを抜けずに下に進む)
  case 30:
    val += 10;
    break;
  default: // default ラベルはなくてもよい
    // ...
}

switch の式として使用できるのは、整数型かそのラッパークラス、あるいは enum 型である。 Java SE 7 からは文字列も使用できるようになった。

while と do-while

while (val < 10) {
    val++;
}

do {
    val++;
} while (val < 10);

if 文と同じく本体をブロックにしなければならないわけではないが、複数の文を実行したい場合はブロックでまとめる必要がある。

for

C 形式の通常の for 文。 例によって本体をブロックにしなければならないわけではない。

int val = 0;
for (int i = 0; i < 10; ++i) {
    val += i;
}

拡張された for 文 (for-each ループ) もある。 配列もしくは インターフェイス java.lang.Iterable を実装したオブジェクト (Java API の全てのコレクションクラスはこれを実装している) に対して使用できる。

int[] nums = { 0, 1, 2, 3 };
for (int n : nums) {
    System.out.println(n);
}

break, continue, return とラベル

(ラベルなしの) break は、最も内側の switch 文、for 文、while 文、do 文を抜ける。 なお、それらの文の中でしか使用できない。

文にはラベルを付けることができ、ラベル付で break を使用することもできる。 ラベル付 break は、ラベルが付けられた文を抜ける (ラベルは、ラベルなし break によって抜けることができる種類の文以外にも付けることができる)。

// for 文に AAA_LOOP というラベルを付ける
AAA_LOOP: for (int val : vals) {
    while (val < 30) {
        val++;
        if (val == 20) break; // ラベル無し break なので、最も内側の while 文を抜ける
        if (val == 10) break AAA_LOOP; // ラベル付 break で外側の for 文を抜ける
    }
}

continue はループ中 (forwhiledo) でのみ使用でき、ループを続けるためにループ本体の最後に制御を移す。 continue でもラベルを指定することができて、ラベルを指定した場合は指定のラベルが付けられたループの最後に制御が移る。 ラベルはループに付けられていなければならない。 ラベルなし continue では最も内側のループの最後に制御が移る。

return は、メソッドの実行を終了させて、呼び出し元に制御を戻す。 メソッドが戻り値型を持っていれば、その型に代入可能な型の式を return によって返さなければならない。 メソッドについては詳細は後述する。

クラスとオブジェクト

Java 言語や Java API で予め定義されているクラスを使うだけでなく、開発者がクラスを自由に定義できる。 クラスの宣言は、下記のように class 予約語の後にクラス名を記述し、その後ろのブレースで囲まれた中にクラスのメンバーを記述する。

class SampleClass {
    // ここにクラスのメンバーやコンストラクタなどを記述
}

通常、public のクラスは、対応するパスのファイルに記述する。 すなわち、com.example.SampleClass というクラスは com/example/SampleClass.java というファイルに記述する。

クラス修飾子

クラス宣言の前に修飾子を付けて、クラスに属性を付与することができる。

  • アノテーション : アノテーションについては後で述べる。
  • public : アクセス修飾子。 public のクラスはどこからでもアクセスできる。 public が付けられていないクラスはパッケージプライベートであり、そのクラスが属するパッケージ内からしかアクセスできない。
  • abstract : abstract が付けられたクラスは抽象クラスとなり、そのクラスはインスタンス化できない。 (サブクラスはインスタンス化できる。) 抽象メソッドを持たせて、抽象メソッドの実装をサブクラスで実装させる、という使い方をする。
  • final : final が付けられたクラスのサブクラスを作ることができない。 (基本的には使わないと思う。)
  • strictfp : そのクラス内で定義されたすべての浮動小数点計算の評価が厳密になる。 (基本的には使わないと思う。)
public final class SampleClass2 {
    // どこからでもアクセス可能なサブクラス化できないクラス
}

クラスのメンバー

クラスのメンバーとしては下記のものがある。

  • フィールド : クラスやインスタンスに属する変数。
  • メソッド : 実行可能なコード。
  • 入れ子のクラスと入れ子のインターフェイス : クラス宣言の中に宣言されたクラスやインターフェイス。

フィールド

クラスやインスタンスに属する変数であるフィールドは、クラス宣言の中に、型と識別子を記述して宣言する。 また、宣言時に初期化することも可能。

クラス修飾子のように、フィールドに修飾子を付けることもできる。

class FieldSample {
    // public 修飾された char 型のフィールド
    public char c;
    // int 型のプライベートな static フィールド. 0 で初期化
    private static int count = 0;
}

フィールドの修飾子としては次のものがある。

  • アノテーション : 詳細は後で。
  • アクセス修飾子 : 後で。
  • static : static フィールドかどうかを表す。 static フィールドはいわゆるクラス変数である。
  • final : 初期化された後に変更できないことを表す。 初期化は、static フィールドの場合はクラスの初期化時までに、非 static フィールドの場合はインスタンス生成完了時までに行われなければならない。
    • コンパイル時に値が決定できる定数式で初期化された基本データ型、あるいは非 nullString オブジェクトで初期化された String 型のフィールドは、定数変数として扱われる。 すなわち、フィールドではなく値として扱われるという最適化が行われる。 有用であるが、値を変更した場合はそのフィールドを参照するコードを全てコンパイルし直す必要がある。

フィールドの参照は、クラス内部からであればフィールド名のみで可能。 外部からの参照は、ドット演算子を使用してどのクラスあるいはインスタンスのフィールドを参照するのかを指定する。

// 外部からの参照 (fieldSample 変数は FieldSample 型)
char currentChar = fieldSample.c;

// クラス内部からの参照の場合、特にクラス名などを前に付けなくてよい
c = 'a';
count++;
// 付けてもよい
this.c = 'b';
FieldSample.count++;

アクセス制御

クラス自体や、クラスのメンバーがどこからアクセス可能かを表すための修飾子として、アクセス修飾子というものがある。

  • private : この修飾子が付けられたメンバーは、そのクラス内部からのみアクセス可能。
  • 修飾子無し : 特にアクセス修飾子が付けられていない場合、そのクラスが属するパッケージ内の他のクラスからアクセス可能。
  • protected : この修飾子が付けられたメンバーは、そのクラス自身とそのクラスのサブクラス、そして、そのクラスが属するパッケージ内の他のクラスからアクセス可能。
  • public : この修飾子が付けられたクラスやメンバーは、どこからでもアクセス可能。 (メンバーの場合、たとえ public のメンバーであってもそのメンバーが宣言されているクラスにアクセスできない場所からはアクセスできない。)

コンストラクタとインスタンス化

new 演算子を用いてインスタンス化する。

FieldSample fs = new FieldSample();

初期化処理のためにコンストラクタを宣言できる。 コンストラクタにより、new によりインスタンスへの参照が返される前に初期化処理が可能である。

コンストラクタはそのクラスと同じ名前をもつ。 コンストラクタには引数を取らせることが可能であり、異なる引数を取る複数のコンストラクタを宣言することも可能である。 コンストラクタの中から、別のコンストラクタを呼び出すこともできる (下記の例参照)。

class ConstructorSample {
    int count;
    // 引数を取らないコンストラクタ
    ConstructorSample() {
        // 別のコンストラクタを呼び出し
        this(0);
    }
    // int 型の引数を取るコンストラクタ
    ConstructorSample(int aCount) {
        // 引数として渡された値をフィールド count に代入
        count = aCount; // this.count = aCount でもよい
    }
}

コンストラクタが 1 つも宣言されていない場合は、何もしない引数なしのコンストラクタ (デフォルトコンストラクタ) が自動的に用意される。

コンストラクタの修飾子として、アノテーションとアクセス修飾子が使用できる。 private で修飾されたコンストラクタは、そのクラス内部からしか使用できない (外部からインスタンス化できない)。

コンストラクタはクラスのメンバーではなく、当然ながらメソッドでもないので注意すること。

初期化ブロック

コンストラクタ以外で初期化する方法として、初期化ブロックというものを使用できる。 コンストラクタを宣言できない無名内部クラス (後で出てくる) を書く場合に特に有用である。

class InitializationBlockSample {
    /** インスタンス固有の ID として次に使用する値 */
    private static int nextId;
    /** インスタンス固有の ID */
    private int id;

    // 初期化ブロック
    {
        id = nextId++;
    }
    /// static な初期化ブロック
    static {
        nextId = 0; // この例は特に有用なものでなく、実際にはフィールド nextId の宣言時に初期化子で初期化しておけばよい
    }
}

static な初期化ブロックはクラスがロードされた後で、クラスが実際に使用される前に実行される。 非 static な初期化ブロックは、コンストラクタよりも先に実行される。

static な初期化ブロックはチェックされる例外を投げることはできない。 非 static な初期化ブロックは、チェックされる例外 (後で詳しく説明する) を送出できるが、その場合はすべてのコンストラクタでその例外を送出することが宣言されていなければならない。

なお、初期化ブロックはクラス内に複数持つことができ、宣言されている順番に実行される。

メソッド

メソッド宣言は、メソッドヘッダとメソッド本体からなる。 メソッドヘッダは、修飾子 (ない場合もある) と型パラメータの集まり (ない場合もある)、メソッドの返り値型、シグニチャ (メソッド名と引数宣言)、メソッドが投げる例外を列挙したオプションである throws 節からなる。

class MethodSample {
    // int 型の引数を取るが何もしないメソッド
    // void は返り値がないことを表す
    public static void doNothing(int unusedValue) {
        return; // 返り値が void の場合は return で何も返さない
    }

    // 引数をとらずに int 型の値を返すメソッド
    public int callDoNothingMethod() {
        doNothing(100);
        return 0; // int 型の値を返す
    }
}

メソッド呼び出しは、クラス内部からだと単にメソッド名に続けて括弧を付けてメソッド呼び出しができる。 クラス外部からの呼び出しには、JavaScript のようにドット演算子を使ってメソッドを呼び出す対象を決定する必要がある。

// MethodSample クラスの static メソッド doNothing の呼び出し
MethodSample.doNothing(100);

// MethodSample オブジェクトの callDoNothingMethod の呼び出し
int value = new MethodSample().callDoNothingMethod();

メソッドにつけることができる修飾子はつぎのとおり。

  • アノテーション
  • アクセス修飾子 (private とか public とか)
  • abstract : メソッド本体なしのメソッド宣言。 メソッド本体はサブクラスで実装されなければならない。
  • static : いわゆるクラスメソッド。
  • final : サブクラスでオーバーライドできないことを示す。 (基本的には使わないと思う。)
  • synchronized : 並行動作のスレッド制御に関連する。 本文書では特に説明しない。 (多分。)
  • native : 他の言語 (主には C か C++) で実装されたメソッドであることを示す。 宣言部分にはメソッド本体はない。 (基本的には使わないと思う。)
  • strictfp : メソッド内の浮動小数点演算を厳密に行う。 (基本的には使わないと思う。)

可変長引数を受け取るメソッドは、引数の型の後ろにエリプス (...) を記述することで宣言できる。

    /** 複数の数値の合計を求める */
    public long sum(int... vals) {
        long sum = 0;
        // vals は int[] 型の配列; 長さが 0 である可能性もある
        for (int v : vals) sum += v;
        return sum;
    }

setter / getter

Java には、C# でいうところのプロパティ、JavaScript でいうところのアクセサの仕組みがない。 つまり、外部からはフィールドのように操作できるが、実際にはメソッド呼び出しで処理させる、ということができない。

一般的に、フィールドに外部から自由にアクセスできるようにするのは良いプラクティスではない。 そこで Java では、setXXX, getXXX というメソッドを定義して、それを介してフィールドの値を書きかえるようにすることが多い。

this 参照

ここまでも使ってきたが、コンストラクタやメソッドの中ではインスタンス自身の参照を表すための this 参照が使用できる。 JavaScript とは違い、Java では必ずしも this を使用する必要はないが、フィールドと同じ識別子をもつ局所変数が宣言されている場合などに this 参照を使用すると便利である。

class ThisSample {
    private int count;
    ThisSample(int count) {
        // 単に count としただけでは引数の count を指してしまうが
        // this.count とするとフィールドの count であることが明示される
        this.count = count;
    }
}

メソッドのオーバーロード

異なる引数を取るのであれば、同じ名前のメソッドを複数宣言することができる。 (シグニチャが異なるため。) これをメソッドのオーバーロードという。 どのメソッドが呼び出されるかは、呼び出し時の引数の型によって決定される。

class OverloadSample {
    // getNames(int limit) の limit のデフォルト値を 10 にしたい場合
    // オーバーロードで対応する

    String[] getNames() {
        getNames(10);
    }
    String[] getNames(int limit)
        String[] names;
        // ... (最大 limit 件の name を取得する処理) ...
        return names;
    }
}

Java では引数の省略ができない (あるいはデフォルト引数の宣言ができない) ため、メソッドのオーバーロードで対応することが多い。

クラスの拡張 (サブクラス化)

サブクラス化により、クラスの拡張が可能である。 クラス拡張により、次の 2 つの形式の継承がなされる。

  • 型の継承 : サブクラスはスーパークラスの型として使用できる。 すなわち、スーパークラスの型が期待される箇所でサブクラスをポリモフィズム的に使用できる。
  • 実装の継承 : サブクラスは、アクセス可能なフィールドとメソッドに関してスーパークラスの実装を得る。

クラス拡張により、上記の 2 つの形式の継承が常に同時に発生するが、必ずしもそれが常に良い選択であるとは限らない。 インターフェイスを使用すると、実装に関係なく型の継承を実現できる。 また、コンポジションや転送を使用して、型に影響させることなく既存の実装の再利用が可能である。 クラスの拡張を行うことを検討する際には、別の方法としてこれらも検討すること。

なお、Java ではクラスの多重継承はできない。 (インターフェイスを使用することで型を多重継承することは可能。)

クラス拡張は、クラス宣言時に extends キーワードを用いて行う。

class BaseClass {
    int count = 100;
    String getMyName() {
        return "BaseClass";
    }
}

// BaseClass を拡張したクラス
class SubClass extends BaseClass {
    // BaseClass で宣言されているフィールドと同名のフィールドの宣言
    int count = 200;
    // BaseClass で宣言されているメソッドのオーバーライド
    @Override
    String getMyName() {
        return "SubClass";
    }
    // BaseClass で宣言されていないメソッドの宣言
    String getMyBaseClassName() {
        return "BaseClass";
    }
}

class Test {
    public static void main(String[] args) {
        SubClass s = new SubClass();
        BaseClass b = s; // 同じオブジェクトを参照するが、変数の型は異なる
        System.out.println(s.count + ", " + s.getMyName()); //=> 200, SubClass
        System.out.println(b.count + ", " + b.getMyName()); //=> 100, SubClass
    }
}

Java のメソッドは全て C++ の仮想関数のように扱われ、スーパークラスで宣言されているメソッドと同じシグニチャをもつメソッドをサブクラスで宣言した場合、オーバーライドしたことになる。 つまり、呼び出し側でオブジェクトがどの型として扱われているかによらず、オブジェクトの実際のクラスによって使用されるメソッドが決定される。 上の例では、SubClass オブジェクトを SubClass 型として使用している場合でも、BaseClass 型として使用している場合でも、SubClass に実装された getMyName メソッドが呼び出されている。 一方で、フィールドはオーバーライドされない。 すなわち、スーパークラスで宣言されているフィールドと同名のフィールドをサブクラスで宣言したとしても、それはオーバーライドにはならずに単にスーパークラスのフィールドを隠ぺいするだけである。 上の例では、SubClass オブジェクトを BaseClass 型として使用した場合、BaseClass 側のフィールドにアクセスしている。

サブクラスのメソッド内からスーパークラスで実装されたメソッドを呼び出すために、super 予約語を使用できる。

class BaseClass {
    String getMyName() {
        return "BaseClass";
    }
}

class SubClass extends BaseClass {
    @Override
    String getMyName() {
        return "SubClass extends " + super.getMyName(); // super を使ってスーパークラスで実装されたメソッドを呼び出し
    }
}

ちなみに、スーパークラスのメソッドにアクセスできない場合は、サブクラスで同じシグニチャのメソッドを宣言したとしてもオーバーライドにはならない。 (そもそもアクセスできなくて継承されないので。 パッケージプライベートなメソッドの場合でサブクラスのパッケージが違う場合はやや複雑である。)

スーパークラスのコンストラクタ呼び出し

スーパークラスの初期化について知っているのはスーパークラスだけなので、明示的にしろ暗黙的にしろ、サブクラスからスーパークラスのコンストラクタの呼び出しが行われる必要がある。 明示的に呼び出す場合は、super 構文を使用して、サブクラスのコンストラクタの最初の文でスーパークラス・コンストラクタ呼び出しを行う。

class BaseClass {
    BaseClass() {
        // 初期化処理
    }
}

class SubClass extends BaseClass {
    SubClass() {
        super(); // BaseClass のコンストラクタ呼び出し
    }
}

基本的に super はコンストラクタの 1 文目である必要があるが、super でなく this による別コンストラクタの呼び出しを 1 文目に書いた場合はスーパークラスのコンストラクタ呼び出しを先送りすることができる。 コンストラクタの最初の文が自身のクラスの別のコンストラクタ呼び出しでもスーパークラスのコンストラクタ呼び出しでもなかった場合は、暗黙的にスーパークラスのコンストラクタ (引数なし) が呼び出される。 引数なしのコンストラクタがスーパークラスに宣言されていない場合 (あるいはアクセスできない場合) は、明示的な呼び出しを行う必要がある。

オブジェクトの生成処理の順序は次のようになる。

  • コンストラクタ呼び出し
  • スーパークラスが存在する場合、スーパークラスの初期化処理 (オブジェクトの生成処理が再帰的に行われる)
  • フィールドの初期化と初期化フィールドの実行
  • コンストラクタ本体の実行

この処理の途中でメソッド呼び出しをする場合、実際のクラスのメソッドの実装が呼び出されることに注意すること。 (メソッドを呼び出すとサブクラスのメソッドが呼び出されるが、サブクラスのフィールドの初期化がまだ終わっていない、という可能性がある。) privatestaticfinal で修飾されているメソッドを呼び出す場合はその心配はない。

暗黙的な Object クラスの継承

明示的に他のクラスを拡張していないクラスは、暗黙的に Object クラス を継承する。 つまり、クラス階層のルートに存在しているのが Object クラスである。 したがって、Object 型の変数はあらゆるオブジェクトを参照可能である。

Object クラスで定義されているメソッドはいろいろあるが、特に次のメソッドは重要である。

  • equals(Object obj) メソッド : オブジェクトが別のオブジェクトと等しいかどうかを検査する。 デフォルト実装では同一性検査である。 必要に応じてオーバーライドすること。
    • 同一性は、普通 == 演算子で検査する。
  • hashCode() メソッド : ハッシュテーブルで使用するためのハッシュ値を返す。 equals メソッドをオーバーライドして実装を変更した場合は、等しいオブジェクトが同じハッシュ値を返すようにこのメソッドもオーバーライドすべきである。
  • toString() : オブジェクトの文字列表現を返す。 + 演算子で文字列結合される際には、暗黙的にこのメソッドが呼び出される。

型の互換性と変換

  • 型階層の上位の型は、階層の下位の型よりもより広い (あるいは、より一般的) と言われる
  • 逆に、階層の下位の型は、上位の型よりもより狭い (あるいは、より特殊) と言われる

上位の型が期待されるところに下位の型が渡されると、広くする変換が行われる。 これはコンパイル時にチェック可能であり、プログラミング上明示的に何かする必要はない。 一方、下位の型が期待されるところに上位の型を渡す場合、狭くする変換を明示的に行う必要がある。

// 広くする変換
BaseClass b = new SubClass();
// キャストによる狭くする変換
SubClass s = (SubClass)b;

キャストが正しいものであるとコンパイラが判断できない場合は、実行時に型検査が行われ、検査が失敗した場合は ClassCastException 例外が送出される。

instanceof 演算子を使用してオブジェクトのクラスを検査することで、安全にキャストすることが可能である。

void doCastSample(BaseClass b) {
    if (b instanceof SubClass) {
        // instanceof 演算子により b が SubClass であることがわかっているので安全にキャストできる
        SubClass s = (SubClass)b;
    }
}

インターフェイス

クラスは型を定義するが、クラスを定義せずに型を定義できると有用である。 Java では、インターフェイスによって型を定義できる。

インターフェイスはメソッドやフィールドの集合として抽象的な形で型を定義する。 クラスと違い、インターフェイスは実装を含まない。

クラスは、1 つ以上のインターフェイスを実装することができる。 インターフェイスを実装するということは、すなわちインターフェイスの型を継承するということであり、複数のインターフェイスを実装可能ということは型の多重継承が可能ということである。

Java API に含まれるインターフェイスの一例

  • Cloneable : この型のオブジェクトは、複製をサポートしている。
  • Comparable : この型のオブジェクトは、比較のための順序を持っている
  • Runnable : この型のオブジェクトは、独立したスレッドで実行可能なふるまいを持っている。
  • Serializable : この型のオブジェクトは、オブジェクトバイトストリームに書きだすことができ、それらのバイトからオブジェクトとして再構築できる。

インターフェイスの宣言

予約語 interface を用いてインターフェイスの宣言を行う。 インターフェイスには次のメンバーを宣言できる。

  • 定数 (フィールド)
  • メソッド
  • 入れ子のクラスとインターフェイス

インターフェイスのメンバーは暗黙的に public となる。 public 修飾子を付けても良いが、通常は省略される。

インターフェイスにフィールドを宣言した場合、それは暗黙的に publicfinalstatic の修飾子が付けられたものとなる。 すなわち、定数となる。

メソッド宣言は、暗黙的に abstract 修飾子が付けられたものとなる。 publicabstract、アノテーション以外の修飾子を付けることはできない。

interface IPoint {
    int getX();
    int getY();
}

インターフェイス自身の修飾子としては、次のものがある。

  • アノテーション
  • public : クラスの場合と同様。 public 修飾子がなければパッケージプライベート。
  • abstract : すべてのインターフェイスは暗黙的に abstract である。 通常は省略される。
  • strictfp : 定数に対する初期化式とインターフェイス内に宣言された入れ子のクラス内で、浮動小数点計算が厳密になる。 (通常は使用しない。)

インターフェイスの拡張

extends を使ってインターフェイスの拡張が可能である。 複数のインターフェイスを継承することも可能であり、その場合は継承するインターフェイスをカンマで区切って複数記述する。

interface BaseInterface {
}

interface SubInterface extends BaseInterface {
}

クラスがインターフェイスを実装する

クラス宣言時に implements を使用してインターフェイスを実装できる。 複数のインターフェイスを実装することも可能であり、その場合は実装するインターフェイスをカンマ区切りで複数記述する。

class Point implements IPoint {
    private int x;
    private int y;
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    @Override
    public int getX() {
        return x;
    }
    @Override
    public int getY() {
        return y;
    }
}

マーカーインターフェイス

Cloneable インターフェイスには、特にメンバーが宣言されていない。 このインターフェイスの役割は、「この型のオブジェクトが複製の仕組みを持っている」 ことを表すことである。

このように、クラスが単に何らかの属性を持っていることを表すようなインターフェイスをマーカーインターフェイスと呼ぶ。 マーカーインターフェイスを用いることで、それを実装するクラスの振る舞いに関して意味深い影響を与えることができる。 型システムの中で、マーカーインターフェイスは有用である。

入れ子のクラスとインターフェイス

クラスやインターフェイスの中に、別のクラスやインターフェイスを宣言することが可能である。 あるいは、コードブロックの中にクラスやインターフェイスを宣言することもできる。

static な入れ子の型

static な入れ子の型は、単にその型を囲むの外部の型 (エンクロージング型) のスコープの中で宣言された型であるといえる。 エンクロージング型のメンバーなので、エンクロージング型の private なメンバーにアクセス可能である。

static な入れ子のクラス

public class BankAccount {
    private int privateField;
    // 外部からは BankAccount.Permission としてアクセス可能
    public static class Permission {
        // Permission の中から BankAccount オブジェクトの private なメンバーにアクセス可能
        public int getBankAccountPrivateFieldValue(BankAccount ba) {
            return ba.privateField;
        }
    }
}

入れ子のインターフェイス

入れ子のインターフェイスは、static 修飾子が付けられていなくても常に static である。 インターフェイスは特に実装を持たないため、入れ子になっていても通常のパッケージ直下に宣言される場合とほとんど変わらない。

内部クラス (非 static な入れ子のクラス)

static でない入れ子のクラスは内部クラスと呼ばれる。 static でないので、インスタンスと関連付けられる。 つまり、内部クラスのインスタンスは、常にエンクロージングクラスの何れかのインスタンスに紐づいている。

public class BankAccount {
    public void doAction() {
        // act はこのオブジェクトと紐づけられている
        Action act = new Action();
           // より厳密に書くと this.new Action() となる
    }
    // 外部からは BankAccount.Action としてアクセス可能
    public class Action {
        // Permission の中から BankAccount オブジェクトの private なメンバーにアクセス可能
    }
}

例に示した通り、厳密に書くと this.new Action() という形でインスタンス化される。 あるいは、とある BankAccount オブジェクト ba に結び付けられた BankAccount.Action オブジェクトを生成する方法として ba.new Action() とも書ける。

内部クラスからは、エンクロージングクラスの全てのフィールドに修飾なしでアクセスできる。

public class BankAccount {
    private String accountName;
    private int id;

    public class Action {
        private int id;
        public getAccountName() {
            // 単にフィールド名だけでエンクロージングクラスのフィールドにアクセス可能
            return accountName;
        }
        public getAccountId() {
            // フィールド id は BankAccount にも Action にも宣言されているので
            // BankAccount の id フィールドにアクセスするには BankAccount.this.id とする
            return BankAccount.this.id;
        }
    }
}

例の BankAccount.this.id のようなアクセスは、限定的な this 式によるアクセスと呼ばれる。

ローカル内部クラス

メソッド本体やコンストラクタ、初期化ブロックなどのコードブロック中で内部クラスを定義することも可能である。 それらの内部クラスはローカル内部クラスと呼ばれ、コードが含まれているクラスのメンバーではなく、ローカル変数と同様そのブロックに対してローカルである。 つまり、コードブロックの外部からは一切アクセスできない。

ローカル内部クラスからは、そのクラスが定義されているスコープ内の変数やメソッドの全て (ローカル変数、メソッドパラメータ、非 static フィールド (非 static ブロックの場合)、static フィールドなど) にアクセス可能である。 ただし、ローカル変数やメソッドパラメータに関しては、final 宣言されていなければならない。

class LocalInnerClassSample {
    int count = 100;
    public void createLocalInnerClass() {
        final int count2 = 100;
        // メソッド内でのクラス宣言
        class LocalInnerClass {
            int doMethod() {
                // final なローカル変数やエンクロージングクラスのフィールドにアクセス可能
                return count + count2;
            }
        }
        LocalInnerClass c = new LocalInnerClass();
        c.doMethod();
    }
}

無名内部クラス

ローカル内部クラスを宣言するまでもない場合、クラスやインターフェイスを実装した無名内部クラスを宣言できる。 無名内部クラスは new によるインスタンス化時に定義される。

Android アプリ開発においては、ユーザー入力に対するコールバックのためのオブジェクトを生成するために無名内部クラスを多用することになると思われる。

class AnonymousInnerClassSample {
    public Runnable createRunnableObject() {
        // Runnable インターフェイスを実装した無名内部クラス
        return new Runnable() {
            @Override
            public void run() {
                 // ... (処理) ...
            }
        };
    }
}

無名内部クラスは明示的に extends 節や implements 節を持つことはできない。 また、アノテーションや他の修飾子を持つこともできない。 コンストラクタを持つこともできないので、初期化が必要な場合は初期化ブロックを使用することになる。

ラムダ式と無名内部クラス

無名内部クラスの紹介で書いたとおり、コールバックのための 1 つのメソッドのみを持つ無名内部クラスを生成することは多々あるだろう。 Java SE 8 で導入されるラムダ式を使うことで、より簡潔に記述することができるようになる。

ActionListener listener = event ‑> { ui.showSomething(); };

列挙型

列挙型 (あるいは enum) は、型が定義されたときに、その型のすべての値がわかっているような型である。 例えば、トランプの絵柄を表す型を定義するとき、値としては、ハート、ダイヤモンド、クラブ、スペースの 4 つだけを定義すればよいはずである。 曜日を表す型であれば、日曜日から土曜日までの 7 つの値があれば良いだろう。

C などでは enum はただの名前付きの整数型の定数の集まりに過ぎないが、Java では enum は特殊な種類のクラスであり、enum の個々の値を表すインスタンスが存在する。

単純な enum の例

トランプの絵柄の例であれば、つぎのように宣言できる。

enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }

Suit という特殊なクラスが作られる。 Suit.CLUBS という形でそれぞれの値を表すインスタンスを取得できる。

Suit currentSuit = ... ;
if (currentSuit == Suit.DIAMONDS) ... ;

単なる名前付き定数整数値とは違い、型安全である。

すべての enum 型 E は、コンパイラによって自動的に生成される次の static メソッドを持つ。

  • public static E[] values() : 宣言された順序で各 enum 定数を含む配列を返す。
  • public static E valueOf(String name) : 指定された名前をもつ enum 定数を返す。 一致する名前の enum 定数がなければ、IllegalArgumentException 例外が送出される。

複雑な enum

enum に enum 定数宣言以外のメソッド宣言などを記述することも可能である。 ここでは説明しない。

ジェネリック型

J2SE 5.0 で導入された。 コレクションが保持する要素の型として任意の型を扱えるが、実際に使用される場合にはコレクションが保持する要素の型は特定の型としたい、という場合などに有用である。

ジェネリック型が導入される前は、コレクションが扱える要素の型として型階層のルートである Object 型を指定しておき、要素取り出し後はダウンキャストして使用するということをしていた。

// Integer 型の要素を保持するリスト
java.util.LinkedList list = new java.util.LinkedList();
list.add(3);

// 取り出し (型安全ではない)
int val = (int)list.removeLast();

当然ながらこれは型安全ではない。

ジェネリック型の使用

ジェネリック型導入後は、以下のように型引数で要素の型を指定することができる。

// Integer 型の要素を保持するリスト
java.util.LinkedList<Integer> list = new java.util.LinkedList<Integer>();
        // Java SE 7 では new java.util.LinkedList<>() という感じで型パラメータを省略できる
list.add(3);

// 取り出し
int val = list.removeLast();

ジェネリック型の宣言

// 型変数を使用したクラスの宣言
class SimpleWrapper<E> {
    // クラス宣言における型変数はクラス内の非 static なところで型名として使用できる

    private E elem;
    public SimpleWrapper(E elem) {
        this.elem = elem;
    }
    public E getElem() {
        return elem;
    }
    // メソッド固有の型変数を宣言することもできる。
    // このメソッドは特に何の意味もないが、渡されたオブジェクトをそのまま返す (返り値の型は渡された値の型と同じ)。
    public <T> T doNothing(T obj) {
        return obj;
    }
}

Map<K,V> 型など、型変数はカンマ区切りで複数指定することができる。

詳細は省略

Integer 型は Number 型のサブタイプだが、List<Integer> 型は List<Number> 型のサブタイプではない。 Number 型の任意の要素型を持つ List<E> 型として、List<? extends Number> というのが使用できたりする。 詳細は省略する。

例外とアサーション

3 種類の例外

Java では例外はオブジェクトで表現される。 例外を表すクラスは、Throwable か、そのサブクラスを拡張したものである必要がある。

慣習的に、新たな例外を定義する場合は Throwable のサブクラスである Exception を拡張する。

例外は大きく分けて次の 3 種類。

  • エラー : 仮想マシン自身で何かが失敗した場合など、アプリケーションが制御できる範囲を超えている例外。 この例外は Throwable のサブクラスである Error のサブクラスで表現される。
  • チェックされる例外 : メソッドが送出することを宣言しなければメソッド内から送出できない例外。 慣例的に Throwable のサブクラスである Exception のサブクラスで表現される。
  • チェックされない例外 : Exception のサブクラスである RuntimeException のサブクラスで表現される。

チェックされる例外は、例外的ではあるが発生する可能性が存在し、発生した場合にはアプリケーションで何かしらの対応をすべきであると考えられる例外を表す。 例えば、ネットワーク通信中にネットワークが切断された場合に発生する例外はチェックされる例外であるべきである。

チェックされない例外は、本来は発生してはいけない例外を表す。 例えば、配列の範囲外にアクセスした場合に発生する ArrayIndexOutOfBoundsException などである。 これらはアプリケーションが対応すべき例外ではなく、アプリケーションのコードを修正することで対応すべき例外である。

例外の送出

例外は throw 文を使用して送出する。

throw new IllegalArgumentException("引数 `XXXX` は 0 から 10 までの値でなければならない");

throws 節によるメソッドが送出しうる例外の宣言

メソッドがチェックされる例外を送出する可能性がある場合、その例外を throws 節で宣言する必要がある。

// チェックされる例外 XXXException が送出される可能性があるので、throws 節で宣言する
public void doSomething() throws XXXException {
    if (...) {
        ...
    } else {
        throw new XXXException("...");
    }
}

try ... catch ... finally

1 つの try ブロックに対して、少なくとも 1 つの catch ブロックか finally ブロックが必要。

try {
    // ここで例外が発生した場合は catch で捕まえられる (型が合う場合)
    // 必ず finally 節の内容が実行される
} catch (AAAException err) {
    // try ブロックの中で AAAException エラー (またはそのサブクラス) が発生した場合はここにくる
} catch (BBBException err) {
    // try ブロックの中で BBBException エラー (またはそのサブクラス) が発生した場合はここにくる
} finally {
    // try ブロック中で例外が発生したとしてもしなかったとしても、必ずここにくる
    // catch 節の中で例外が発生した場合でもここにくる
    // 必ずしなければならないリソース開放などをここで行う
}

finally 節は、try ブロックの中の例外発生だけでなく、try ブロックからの breakcontinuereturn での制御の移動の際にも実行される。

スタックトレース

例外オブジェクトはスタックトレースを持っているのでデバッグ時に役立つ。 printStackTrace メソッドで表示させることが可能。

アサーション

アサーションは、バグの早期発見を目的として、常に成り立つべき条件を検査するものである。 ここでは詳細には触れない。

public static void calculateDistance(Point p1, Point p2) {
    int distance = calculateDistanceImpl(p1, p2);
    // distance は常に非負でなければならない (そうでなければバグ) ということをアサーションで検査する
    assert (distance >= 0) : "Negative distance";
    return distance;
}

アノテーション

アノテーションはクラスやメソッドといったプログラムの構成要素に対する注釈である。 実態としては特別な種類のインターフェイスである。

様々な利用法があるが、Android アプリ開発においては特別なアノテーションの知識は不要だと考えられる。 Java API に含まれており、Android アプリ開発に限らずよく使われる次のアノテーションを知っておけばよいだろう。

  • @Override : スーパークラスのメソッドをオーバーライドしたとき、そのメソッドにはこのアノテーションを付けるべきである。 オーバーライドのつもりでオーバーロードしてしまっていた場合、コンパイラはエラーメッセージを生成する必要がある。
  • @Deprecated : 使用すべきでないメソッドなどに付けるべきアノテーション。

パッケージとインポート

Java のパッケージには次のような役割がある。

  • 関連するクラスとインターフェイスのグループを作りだす役割。
  • 型の名前衝突を避けるための名前空間としての役割。
  • アクセスのスコープを作りだす役割。

パッケージの宣言は、ソースファイルの最初に次のような形式で書かれる。

package com.example.image;

習慣として、開発者が保持するドメイン名をドット区切りで逆順に並べたものをパッケージの接頭辞として使用する。 (例えば、hatena.ne.jp というドメインを保持している場合、パッケージ名は jp.ne.hatena で始めるべきである。) この命名規則により、世界的に名前衝突の心配がなくなる。

パッケージの宣言がなされたソースファイル中に書かれたクラスやインターフェイスは、暗黙的にそのパッケージに属することになる。

package com.example.image;

public class ImageData {
    // ...
}

上の例だと、ImageData クラスはパッケージ com.example.image に属する。 このとき、ImageData クラスの完全修飾名は com.example.image.ImageData である。 この例では ImageData クラスは public なクラスなので、別のパッケージから com.example.image.ImageData としてアクセス可能である。

ここまでのサンプルコードでも使ってきたが、import することによって、いちいち完全修飾名を使用しなくてもよくなる。

package com.example.otherpackage;

import com.example.image.ImageData;
import java.util.*; // アスタリスクを使うことで、パッケージ内の全クラスとインターフェイス (なのかな?) のインポートもできる
                    // が、使用しているクラスがどのパッケージに属しているかわかりにくくなるのでオススメしない
import java.lang.Math.log; // static メソッドのインポートも可能

public class ImportSample {
    public static void main(String[] args) {
        // import しているので、いちいち com.example.image.ImageData と書く必要がない
        ImageData dat = new ImageData();
    }
}

ファイルのパス

最初の方でも書いたように、完全修飾名が com.example.SampleClass というクラスが存在する場合、そのクラスファイル (SmapleClass.class) はクラスファイル探索の対象となっているディレクトリや JAR ファイル、ZIP ファイルの中の com/example/SampleClass.class という位置に置かれる必要がある。

ソースファイルも同じディレクトリ構成にするのが一般的である。

ドキュメンテーションコメント

/** で始まり */ で終わるコメントによってドキュメンテーションコメントを記述できる。 ドキュメンテーションコメントを元に、javadoc コマンドによって HTML ファイルを生成したりできる。

/**
 * Returns an Image object that can then be painted on the screen. 
 * The url argument must specify an absolute {@link URL}. ...
 *
 * @param  url  an absolute URL giving the base location of the image
 * @param  name the location of the image, relative to the url argument
 * @return      the image at the specified URL
 * @see         Image
 */
 public Image getImage(URL url, String name) {
        try {
            return getImage(new URL(url, name));
        } catch (MalformedURLException e) {
            return null;
        }
 }

スレッド

Android アプリ開発においても、マルチスレッド処理は重要だと思われる。 Java では、Thread クラス によって扱いやすいスレッドが提供される。 Android アプリ開発用には、Thread クラスを内部的に扱ってより簡易にスレッド処理をしやすくした android.os.AsyncTask クラス が提供されている。

マルチスレッド対応のためのロックのためにメソッド宣言に synchronized 修飾子を付けたり、クリティカルセクションを synchronized ブロックにしたりできる。 全てのオブジェクトは、自身のためのロックを 1 つ持っている。 synchronized なメソッドに制御が移るとき、そのオブジェクトのロックが取得される。 既に他のスレッドがロックを取得している場合、ロックが解放されるまで待つ。 単純ではあるが、簡単なマルチスレッド対応ができる。 ただしデッドロックしないように注意が必要。 synchronized 文を使用すると、任意のオブジェクトのロックを取得してスレッドの排他制御が可能である。

また、java.util.concurrent パッケージ で、マルチスレッド処理に有用な機能がいろいろ提供されている。

などなど、マルチスレッディングについてはいろいろあるが、あまり複雑にマルチスレッド処理をしないような Android アプリを開発するうえで必要となる最低限のスレッドの知識 (GUI 操作は main thread (UI thread) から行わなければならない、とか) は Android アプリ側のドキュメントで解説されそうな気がするので省略する。 Java 一般の話として高度なスレッド処理について知りたい場合は、

  • Java並行処理プログラミング ―その「基盤」と「最新API」を究める―
  • 増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編

あたりがおすすめ?

I/O パッケージ

Java におけるデータの入出力には入出力ストリームが使用されることが多い。 Android アプリでも、ネットワークの入出力やファイル入出力でよく使用するようである。

入力を扱うための入力ストリーム、出力を扱うための出力ストリームがある。 また、入出力ストリームを扱う側から見て、データがバイト列の場合はバイトストリームであり、テキストの場合は文字ストリームである。

これら 4 つの入出力ストリームの抽象クラスとして、次のものが java.io パッケージに定義されている。

実装は様々あり、単にファイルとの入出力を担うクラス (FileInputStream など) もあれば、バッファを持っていて別の入出力ストリームをラップするようなストリームクラス (BufferedReader など) もあり、バイト出力ストリームをラップしていて、受け取ったテキストを指定の文字エンコーディングでエンコードした後バイトストリームに書きだすような文字出力ストリームクラス (OutputStreamWriter など) などもある。

// この例に必要な import
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.Charset;

        // 実際の処理
        BufferedReader br = null;
        try {
            // InputStream の例として FileInputStream を使用; この例に関していえば実際には FileWriter を使用すればよい
            InputStream is = new FileInputStream("Test.java");
            // InputStream からバイト列を読み込んで指定の文字エンコーディングに変換する Reader
            InputStreamReader isr = new InputStreamReader(is, Charset.forName("UTF-8"));
            // バッファを持っている Reader
            br = new BufferedReader(isr);

            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException err) {
            // 例外時の処理
        } finally {
            try {
                // ストリームの使用を終えたときには閉じること
                if (br != null) br.close();
            } catch (IOException err) { /* close の失敗は特にハンドルしないことが多い */ }
        }

コレクション

動的型付け言語では、可変長配列や辞書 (連想配列) を簡単に使うことができて便利であることが多い。 Java では、それらに近い機能がコレクションとして提供される。

java.util パッケージには、一般的なコレクションを表すためのインターフェイス群とそれらの実装が含まれている。 コレクション関係のインターフェイスとしては次のようなものがある。

  • Collection<E> : コレクションの大本。 addremovesizetoArray といった基本的なメソッドが宣言されている。
  • Set<E> : 重複した要素を含まず、必ずしも順序を持つとは限らないコレクション。 (Collection<E> のサブインターフェイス)
  • SortedSet<E> : 順序を持った Set。 (Set<E> のサブインターフェイス)
  • List<E> : 特定の順序で要素が並んでいるコレクション。 (Collection<E> のサブインターフェイス)
  • Queue<E> : キューとして使用できる型を表すインターフェイス。 (Collection<E> のサブインターフェイス)
  • Map<K,V> : マップ。 (あるいは連想配列、辞書。) Collection<E> のサブインターフェイスではない。
  • SortedMap<K,V> : キーがソートされているマップ。 (Map<K,V> のサブインターフェイス)
  • Iterator<E> : コレクションの要素への参照を保持しているオブジェクト (?)。
  • ListIterator<E> : List 関係の有用なメソッドが追加されているイテレータ。
  • Iterable<E> : イテレータを返すことができるオブジェクトが実装すべきインターフェイス。 このインターフェイスを実装しているオブジェクトは for-each 文で使用できる。 (上のインターフェイスと違い java.lang パッケージに属する。)

Java API では、同じインターフェイスの異なる実装が提供されている。 例えば、ArrayList<E>LinkedList<E> である。 どちらも List<E> インターフェイスを実装するが、前者は配列的な実装であり、後者は連結リストによる実装である。 とりあえず次の実装を知っていればある程度は困らないはず。

  • HashSet<E> : ハッシュテーブルを用いた Set<E> 実装。
  • ArrayList<E> : 配列のように使用する List<E> 実装が欲しい場合はこれを使用すればよい。 インデックスを指定しての要素へのアクセスは速い一方、要素のインデックスが変化してしまう操作にはコストがかかる。
  • LinkedList<E> : 連結リストのように使用する List<E> 実装が欲しい場合はこれを使用すればよい。 リストの先頭に要素を追加するなどの操作が速い一方、インデックスを指定しての要素へのアクセスにコストがかかったりする。
  • HashMap<K,V> : Map のハッシュテーブル実装。 汎用的に使用できる。

他にも特定の条件において有用な実装が様々存在する。 必要に応じて API ドキュメントから探すとよい。

また、スレッドセーフな実装が java.util.concurrent にあったりするので、マルチスレッドでコレクションを使う場合は、自分でスレッドセーフな処理を書く前に java.util.concurrent パッケージ内を探すと良いだろう。

さらに学ぶために

  • プログラミング言語 Java : Java を使うなら知っておきたい内容が一通り書かれている。 日本語訳はよくないのと内容が少し古い (Java SE 5) のが玉に傷。 この文書も主に本書を参考にして書いた。
  • Effective Java : Java を使うなら読んでおきたいベストプラクティス本。
  • Java 並行処理プログラミング : 読んだことないけど Java で並行処理するなら読んどけ、って感じらしい。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment