まずは環境整備
何を思い立ったか、Javaを勉強してみたくなったのでひとまずコンパイラ等々を探すことから始めました。 最初はJDKだけをインストしてみたのですが、コマンドラインからのコンパイル・実行は一度ならまだしも、何度もやっているとめんどくさいので、「JBuilder 7 Personal」を使うことにしました。
これなら、フリーだし(登録をする必要はありますが)開発環境がある程度Visual Studioに似ているので、勉強には最適です。 また、F5で実行できます(僕の中ではこれは重要なことで、文章を入力しているときでも、できあがった時点でついF5に指が向かいます。 とあるフリーのBASICコンパイラではなぜかF2が実行で非常に使いにくかったのを覚えています。 「実行」は古の N86-BASICからF5と決まっているのです。) ただ、このJBuilderはJavaで作られているらしく、非常に動作が重いです。 再描画の際によくチラつくし、起動自体も30秒程度かかりました。 Visual Studio .NETの起動よりも若干遅いです。
ひとまずインストが終わったものとして早速起動してみました。 多少カスタマイズし終わった後のものですが、JBuilderの画面はこのような感じです。
最初のプログラムは「Hello, world!」
開発環境が一応整ったということで、慣例(?)に従って「Hello, world!」を出力させてみます。
JBuilderでは特別コンソールウィンドウは表示されず、System.out.printlnメソッドに出力は下にあるペイン(なんていうのかはわからないけど・・・)に出力されます。 このペインに表示されている一行目はビルドなどの状況、二行目は実行するクラスの名前、それ以降が出力になっているようです。
ソースコードの見た目はほとんどC#のものと一緒です。 packageはC++, C#などにおけるnamespaceに相当するもので、そのほかのクラス・インターフェイスなどの名前との衝突を防ぎ、クラス・インターフェイスを一つの「パッケージ」にするためのものです。 ただ、namespaceとは違い、 { }(ブレース)でくくることはせず、先頭にpackageキーワードでパッケージを指定します。 そのため、実質的には1ファイルには1packageしか含めないことになります。 packageはパッケージ化する必要がなければ記述する必要はないのですが、プロジェクトを生成する際に自動的に付加されたので、そのままつけておくことにします。
名前付け基準について
これ以下で使用する名前付け基準について記述します。 以下のコードを読むに当たって、名前付け基準に特にこだわらないという人はここを読み飛ばしてしまってかまいません。 また、ここで紹介する名前付け基準が気にくわない人はこれ以下の文章すべて読み飛ばしてしまって下さい(笑)。
Javaではメソッド名にCamel形式(sampleMethod()のようにラクダのこぶみたいに大文字があらわれることから「ラクダ」形式と呼ばれるようです)を使うようですが、これはあまり好きではないのでこれ以降、基本的にPascal形式(各単語の頭文字を大文字で初める。 例えば MyClass, SampleMethod)で記述することにします。 ただ、Javaにはプロパティ構文がないので、privateな変数へのアクセサメソッド(getterとsetter)については特別にCamel形式を用いることにします(例えばgetValue, setValue)。 ちなみに僕はハンガリー記法賛成派です。 キーボード上を見てもEsc, Ctrl, Ins, Del, Altなどハンガリー記法っぽいものがいくつかあります。 ハンガリー記法は直感的かつ効率的であるので僕は好きなのですが、暗号っぽくて読みにくいということで反対する人が多いようです・・・
また、ローカル変数の命名については型の名前の省略形(一文字ないし二文字)をつけることにします。 例えばiCounterやfRatioなどです。 byte型とboolean型は両方頭文字がbなので、byte型はby、 boolean型はbをつけることにします。 文字列型にはstrを付加します。 さらに、クラス内のprivate/protected変数にはm_を付加することにします。 また、クラス名やフィールド名はPascal形式で名付けることにします。
さらにブレースについても、Javaではその行の最後につけることが多いようですが、これだと行間が詰まり読みづらくなるので、ブレースは次の行に落とすことにします。
クラスと継承
JavaでもC#同様、クラス型は暗黙の基底クラス、java.lang.Object型から派生したものになります。 このクラスは、clone(), hashCode(), toString()など、C#でのobject型と似たようなメソッドをいくつかもっています。 これを意識しつつ、独自にGirlクラスを作り、さらにそれをスーパークラス(基底クラス)とするMusumeクラスを作ってみます。 また、この二つのクラスはtoString()をオーバーライドします。
Girlクラス、MusumeクラスともにMethod()メソッドを持ち、ObjectクラスのtoString()メソッドをオーバーライドしています。 Javaでは継承を行う際にはextendsキーワードを用い「class クラス名 extends スーパークラス(基底クラス)名」と記述します。 C#同様多重継承を行うことはできないので、その場合はinterfaceを用います。
38, 39行目のメソッドの呼び出しは問題ありませんが、41, 42行目ではprintln()メソッドに直接クラスのインスタンスを渡しています。 しかし、これら二つのクラスはtoString()をオーバーライドしているので、適切な文字列が表示されます。 toString()をオーバーライドしなかった場合の実行結果を次に示します。
出力結果
このように、「パッケージ名.クラス名@ハッシュコード」の様に表示されます。 これはObject.toString()の動作によるものです。
メソッドのオーバーライドと仮想関数
Javaでは全てのメソッドが既定で仮想関数(virtual)となります。 つまり、基底クラスに存在するメソッドを、派生クラスでオーバーライドすると、基底クラスのメソッドは置き換えられることになります。 そのため、ある派生クラスのインスタンスを基底クラス型にキャストしても、そのインスタンスの実際のクラスに属するメソッドが呼び出されます。 次のコードはその例です。 この場合はtoStringをオーバーライドしています。
この例の示すように、Musume型のインスタンスを基底クラスであるどの型にキャストしても、実際にはMusume型のtoString()メソッドが呼び出されます。 言い換えると、もとのインスタンスの型がわからない場合、どの型にキャストされていても、実際にはどの型のメソッドが呼び出されるかはわからないわけです。 そこで、instanceof演算子が活躍するのですが、instanceof演算子の例はこの次に回すことにして、C#での挙動を見てみたいと思います。
C#ではクラスのメソッドは既定でvirtualではありません。 仮想関数にしたい場合は明示的にvirtualを指定しなければなりません。 また、オーバーライドする場合はoverrideを指定しないと警告が出ます。 先ほどのJavaコードと同じ動作をするものをC#で作ると次のようになります。 ここでobject.ToString()はvirtualであり、Girlクラス及びMusumeクラスは暗黙的にobject クラスを継承しています。
この実行結果を見ればわかるとおり、virtual指定されたメソッドをオーバーライドしているので、変数mをどの型にキャストしても実際はMusume型であるため、Musume型のToString()メソッドが呼び出されます。
しかし、先ほどvirtual指定されたメソッドをオーバーライドする場合はoverrideを明示的に指定しないと警告が出るとしましたが、C#では override修飾子ではなくnew修飾子を指定してオーバーライドをすることができます。 正しくは、new修飾子はオーバーライドではなくメソッドの隠蔽(いんぺい)を行います。 隠蔽を行うと、new修飾子で修飾されたメソッドはそのクラスで定義された「新しい」メソッドとして扱われます。 その例を示したものが次のコードです。
これを実行すると上のような結果になります。 30〜32行目では、Console.WriteLine()メソッドが呼び出される際に、各引数は object型として扱われます。 WriteLine()メソッド内ではToString()を呼び出しますが、Girl, MusumeクラスではobjectクラスのToString()メソッドをオーバーライドしていないので、結果は全てobject型の ToString()メソッドが呼び出され、その値が出力されているのです。
対して、34〜36行目では明示的に各クラスのToString()メソッドを呼び出しています。 new修飾子によって修飾されたメソッドは、そのクラスで新たに定義されたものとして扱われるのでこのような結果になるわけです。 この例のように、C#ではJavaに比べ継承におけるオーバーライドの動作にある程度柔軟性を持たせることができるといえます。
instanceof演算子
先ほどの例で紹介したとおり、インスタンスの型を特定するために、Javaにはinstanceof演算子というものが存在します。 C#ではこれと同様のものにis演算子があります。 また、typeof演算子というものもありますが、typeof演算子はinstanceof演算子とは異なり、 System.Type型を返すことによってより多くの型情報を取得することができます。 Javaにおけるinstanceof演算子を用いたコードの例を見てみます。 この例では型としてさえ存在していればいいので、クラスの中には何も記述していません。
このように、実行時にインスタンスの型を特定したい場合は、instanceof演算子を用います。
このinstanceof演算子の表記についてなんですが、先ほどのextendsキーワードのように「class A extends B」という記述は「クラスAはBを拡張する」と読むことができ直感的であるのに対し、instanceof演算子は「if ( a instanceof A )」のように使用するためなんて読んでいいのかよくわからないところです。 せめてC#のis演算子や、C++のsizeof演算子の様に「if ( instanceof(a) == A )」とできるようにするか、「if ( a isinstanceof A )」というように、もう少し読みやすい形式になっていればいいと思うのですが・・・
コンストラクタとプロパティ
Javaでもコンストラクタは当然存在しますが、派生クラスから基底クラスのコンストラクタを呼び出すときは、C#とは異なり、むしろVB.NET風の呼び出し方をします。 また、プロパティ構文が存在しないので、アクセサメソッドによって再現します。 その例が次のコードです。
まずコンストラクタについて、Girlクラスのコンストラクタは基底クラスのコンストラクタを呼び出すことはないのでいいとして、設計上MusumeクラスではGirlクラスのコンストラクタを呼び出す必要があります。 基底クラスのコンストラクタを呼び出す場合、C#では「コンストラクタ(引数) : base(引数)」という形式をとりましたが、JavaではVB.NETの様にコンストラクタ内の任意の位置で「super(引数)」という形式で呼び出すことができます。 それを実際に行っているのが70行目です。
続いて、プロパティについて見てみます。 Javaではプロパティ構文がないので、カプセル化された内部変数へのアクセスは、アクセサメソッドと呼ばれる一組のメソッド(getter及びsetter)を用いて行います。 アクセサメソッドは通常のメソッドと同様のものですが、一般的にはプロパティ名にプリフィックス「get / set」または「get_ / set_」をつけたものをアクセサメソッド名として用いることが多いようです。
こうして作成したクラスを用いてその動作を確認するためのコードが104〜121行目です。 実際の動作は特に問題ないと思います。 プロパティ値の設定・取得をアクセサメソッドで行わなければならないこと以外には特に注意すべきことも無いと思います。
Javaにおける参照型と値型
ご存じの通り、Javaには構造体(共用体も)が存在しません。 曰く「構造体・共用体はクラスで代用可能なのでJavaには存在しない」そうです。 共用体はともかく、小さな数のデータを多量に扱うときなどは、構造体で扱うべきで、クラス型で代用するとムダが多いといわれています。 実際、C#には構造体型が存在し、使い分けがなされています。 まず、Javaにおける値型であるint型と参照型であるString型を用いてその挙動を見てみます。
int型の動作自体は問題ないと思います。 値型であるint型の代入では、その値のコピーが代入されます。 参照型における代入では、コピーが代入されるわけではなく参照が代入されますが、同じ参照型であるString型については、21行目や27行目の様に文字列の代入を行う際には文字列自体が新しいインスタンスとして代入され、そのインスタンスへの参照が代入されるため、このような結果になります。 次に、クラス型について見てみます。
クラス型は参照型なので、23行目の様な代入を行うと、aibonの参照しているインスタンスへの参照がkonkonに代入されます。 つまり、 aibonとkonkonは同じインスタンスを参照することになります。 すなわち、aibonとkonkonは異なる変数でありながら、実際に参照しているものは同じということになります。 そのため、28行目で行われているように、konkonの参照するインスタンスへの変更が行われることは、 aibonの参照しているインスタンスへの変更と同じことですから、30, 31行目では上の様な表示がなされるわけです。