|
||||||
ホーム / Java 解説 - C との比較 | ||||||
Java 解説 - C との比較
|
* 更新情報
一つでもプログラミング言語をマスターしていれば、それから他の言語をマスターするのは簡単です。所詮、プログラミング言語は最終的には機械語として実行され計算機を制御しようとしているだけなのですから。つまり、最終的にはどの言語でも出来ることはほとんど同じ。だって、計算機にできる事はプログラミング言語が変わったからといて変わるものでは有りません。なので最初にマスターした言語が低レベルであればより有利です。C 言語をマスターしていれば、Java をマスターするのは楽勝。と言うことで、C との違いという形で Java について解説します。 Java では、クラスとか継承とか一見高度な概念が出て来ます。一つ理解しておけば良いのは所詮は構造体だと言うことです。C 言語を母国語とするプログラマは、最終的には、構造体、ポインタ、関数へのポインタ、の組合せとして分析することで Java のほとんどの機能をひもとくことができます。これは、VB を母国語とするプログラマに対して圧倒的なアドバンテージです。 常に、C 言語だったらどういう風になっているか、と思い浮かべながらコードを書きましょう。それができない? 一つのプログラミング言語をマスターできていないなら、次の言語をマスターしようとしても混乱するだけですよ。摘み食いは止めましょう。 なぜかと言えば、最初(あるいは初期)に習得した言語は、計算機についての理解の仕方そのものを決定付けてしまうので、違う理解をしている(違うメンタルモデルを持っている)人との間では、表層上は言葉が通じても考えていることは全く違うという状態になりますから。また、図を用いて説明することも避けています。これも誤った理解を植え付ける原因になりますので。 新しいプログラミング言語を使うとき、必ずやらなければならない古いしきたりです。 まず、ホームディレクトリにファイルが沢山ならぶと厄介なので、プログラミング作業用の mobius1@phantom$ cd ホームディレクトリに移動します mobius1@phantom$ pwd ホームディレクトリにいることを確認します /students/mobius1 mobius1@phantom$ mkdir src 次に、エディタ(Emcws か GEdit, vi) を起動して public class hello { public static void main(String[] args) { System.out.println("Hello, World"); } } 一応、ファイルがちゃんとできているか確認をし、それからコンパイルし、実行します。 mobius1@phantom$ cd mobius1@phantom$ cd src mobius1@phantom$ pwd /students/mobius1/src mobius1@phantom$ ls -l drwxr-xr-x 1 mobius1 mobius1 119 Oct 19 13:34 hello.java mobius1@phantom$ javac hello.java コンパイルします mobius1@phantom$ ls -l クラス・ファイル(.class、C で言うところの drwxr-xr-x 1 mobius1 mobius1 416 Oct 19 13:36 hello.class オブジェクト・コード)ができたことを確認します drwxr-xr-x 1 mobius1 mobius1 119 Oct 19 13:34 hello.java mobius1@phantom$ java hello 実行します Hello, World mobius1@phantom$ ※コマンドラインというものの使い方は大丈夫ですよね? C コンパイラと Java コンパイラについて、プログラマから見た一番の違いは、C コンパイラは一つのソースファイルから一つのオブジェクトファイルを吐き出すのに対して、Java コンパイラはコマンドラインで指定したソースコードの数に係わらず、一つのクラス定義から一つのクラスファイルを吐き出します。 ですので、C で取られるような機能毎に(よくあるのは、処理対象とするデータ構造毎に)ソースファイルを分けるやり方と同様に、Java ではクラス毎にソースファイルを分けるのが普通です。 ただ、これはプロジェクトの体制や環境によって状況が変わりますし、単一のプロジェクトの中でも、使っている API との相性もあり、無理に、一つのルールを決めてしまって、必ずルールを守らなければならない、というようなやり方は絶対にうまく行きません。それに、ふと気づいたのですが、昔の C コンパイラと Java コンパイラの大きな違いは、C コンパイラはコンパイルされていないソースコードしか解析できないですが、Java コンパイラはソースコードだけでなくコンパイル済みのクラスファイルも解析できるということです。 結果として、Java は さて、C コンパイラでは、インクルードするヘッダファイルを探すパスを指定する際には Java コンパイラ に特有なオプションとしては、 Java コンパイラ はソースファイルが1つでも出力されるクラスは沢山かもしれませんのでディレクトリを指定するのです。また、C コンパイラ(正確にはリンカ( Java はコンパイラ言語ではありません。間違えないでください。コンパイラ言語の定義を何と考えるかによりますが、Java コンパイラの出力したコードは、実際の CPU では実行できません(Sun は Java の仮想マシンコードを直接実行できる CPU を開発する、とか言ってましたが...)。いくら仮想マシン (Virtual Machine)とか呼び名を変えても、IT素人はだませても我々プログラマをだますことは出来やせん。 ということで、此の点においては Java と C を比較することは根本的に出来ません。 コンパイラ言語ではないのですから、インタープリタ言語です。インタープリタ言語で書かれたプログラムを実行するには必ずインタープリタのプログラムが要ります。これは、Perl, Ruby, Python, Emacs, JavaScript と全く同じです。 ただ、実際のインタープリタ言語は、ソースコードを1文字/1文/1行づつ解析ながら実行しているものはもう有りません。一旦ソースコードをコンパイルします。ただし、コンパイルして機械語に変換するのではなく、各インタープリタ固有の内部的なデータ構造に変換します。これは、実行している最中に構文エラーがあって止まると悲惨だからです。コンパイルが済んでから、その内部データを1命令づつ解釈実行します。 最近のインタープリタは、その内部データをファイルに保存できるようになって来ました。多分は Emacs Lisp が最初だと思います。今はほとんどの言語で実現されています。こうなると、C 言語のコンパイルとインタープリタ言語のコンパイルの区別しなければ「コンパイル」という言葉の意味が分からなくなってしまいます。そこで、インタープリタ言語で、言語固有のデータ構造を「バイトコード」と呼び、ソースコードをバイトコードに変換することを「バイトコード・コンパイル」と呼びます。バイトコードは、いにしえのオブジェクト指向言語 Smalltalk でのコンパイル済データの呼び名です。 さて、このバイトコード、言語に固有ではありますが、マシン(機械語)には固有ではありません。つまり、一つのバイトコードは、それぞれの CPU に対応したバイトコード・インタープリタの上で動きます。Sun の Java についての登録商標は、Java 固有の特徴ではありません。 強いて言うと、Java 言語では、コンパイラ( ※さて、ちょっと無駄話がすぎました。恐らく、ここに来ている人は、母国語が C で、第一外国語として Java を学ぼうとしている人だと思います。もし、既に Perl 等の高度なインタープリタ言語をマスターしているのであれば、わざわざ Java なんか学ぶ必要はないですし、学ぶにしても、既に二つ目をマスターできたのであれば、帰納によりN個の言語をマスターできるのは自明ですからね。 Java コンパイラが出力するのは、クラスファイル(.class)です。これがバイトコード。 C コンパイラ(リンカ)が出力する実行可能ファイルにもエントリーポイントというものがあります。エントリーポイントとは、OS がプログラムファイルをメモリに読み込んで整頓したあと、最初に実行する(制御を渡す言う)命令のアドレスです。皆さんはそれが、 Java インタープリタは、コマンドラインに指定されたクラスのクラスファイルをメモリに読み込み、そのクラスの さて、では Java インタープリタ はどうやってクラスファイルを探すのでしょうか?それは、 クラスファイルを探す際には、クラスファイル名=クラス名.classとして探します。 とりあえず、機能としては C の C の場合、コンパイラが実際にコンパイルする直前に、プリプロセッサというプログラム(コンパイラに組み込まれてしまっている場合もある)を呼び出し、 main.c
#include "sub.h" int main(int argc, char** argv) { printf("ping from %s\n", sub); } sub.h
char* sub = "Hrimfaxi"; をコンパイルしようとすると、プリプロセスされるので、実質的には、 プリプロセス済みのmain.c
char* sub = "Hrimfaxi"; int main(int argc, char** argv) { printf("ping from %s\n", sub); } がコンパイルされます。 Java は、ソースコードをコンパイルするときに、コンパイル中のソースに定義されていないクラスが見つかった場合、クラスファイル ( まず、なんといっても、C コンパイラと Java コンパイラの違いは、C コンパイラはコンパイル前のソースコードしか解析できないですが、Java コンパイラはソースコードだけでなくコンパイル済みのクラスファイルも解析できるということでです。
さて、ここから先に進むには、
まず、身近な例として、ディスクの中のディレクトリ構造のことを考えてみてください。例えば同じ名前のファイルは同じディレクトリの中には置けませんが、違うディレクトリには置けます。例えば、 ディレクトリ=パッケージ、ファイル=クラスと思えばおおむね間違いはないです。 Java 言語の中の例で言えば、XML を処理する DOM API では、 import を使って、そのパッケージを使うことを宣言するimport org.w3.dom.*; ... Element elm = (Element)node.getFirstChild(); import は使わずに、パッケージ名をつけて使う。org.w3.dom.Element elm = (org.w3.dom.Element)node.getFirstChild(); import で途中まで指定する。import org.w3.*; dom.Element elm = (dom.Element)node.getFirstChild(); これらのコードの内容(コンパイルされる結果)は全く同じです。Java コンパイラが 当然、後者はソースを打つのが面倒になります。でも、自分のプログラムの中で自分用に あるクラスをパッケージの中に納めることを宣言するには package mypackage; class the_class { ... } 上記のクラスにアクセスする場合は import mypackage.*; class main { public static void main(String[] args) { the_class cls = new the_class(); } }
さて、このパッケージの名前が
mobius1@tiger$ jar -tvf /usr/local/jdk1.5.0/jre/lib/rt.jar | grep org.w3c.dom | more 445 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/DOMConfiguration.class 470 Fri Oct 14 03:23:54 JST 2005 org/w3c/dom/DOMError.class 173 Fri Oct 14 03:23:54 JST 2005 org/w3c/dom/DOMErrorHandler.class 561 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/DOMImplementation.class 213 Fri Oct 14 03:23:54 JST 2005 org/w3c/dom/DOMImplementationList.class 317 Fri Oct 14 03:23:54 JST 2005 org/w3c/dom/DOMImplementationSource.class .... 315 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/ProcessingInstruction.class 147 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/EntityReference.class 432 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/Attr.class 330 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/DocumentType.class 200 Fri Oct 14 03:23:54 JST 2005 org/w3c/dom/Notation.class 1078 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/DOMException.class 141 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/CDATASection.class 398 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/Text.class 558 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/CharacterData.class 1532 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/Element.class ← org.w3c.dom.Element 298 Fri Oct 14 03:23:54 JST 2005 org/w3c/dom/Entity.class 179 Fri Oct 14 03:23:56 JST 2005 org/w3c/dom/ranges/DocumentRange.class 250 Fri Oct 14 03:23:56 JST 2005 org/w3c/dom/events/DocumentEvent.class 444 Fri Oct 14 03:23:56 JST 2005 org/w3c/dom/traversal/DocumentTraversal.class 356 Fri Oct 14 03:23:54 JST 2005 org/w3c/dom/events/EventTarget.class 174 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/NodeList.class 2096 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/Document.class 2560 Fri Oct 14 03:18:34 JST 2005 org/w3c/dom/Node.class これを見れば分かりますね。 とりあえず、Java と C で等価なコードを例示します。まずは、C から... C 言語では、宣言と定義を分けて記述します(Java ではできません)。なので、通常、実践的な C 言語を用いたプログラミングでは、宣言だけ書いたヘッダファイル、定義を書いたソースファイル、それらの機能をつかうソースファイル、という三つの役割のソースに分けるというパターンが普通です。 宣言だけ書いたヘッダファイル: foo.h
struct foo_t; // foo_t という名前の構造体が存在することだけ「宣言」する。 // その大きさとかどんなメンバーがあるかは利用者には知らせ(たく)ない。 foo_t* foo_new(char*, int); void foo_delete(foo_t*); void foo_print(foo_t*, FILE*); 定義するソースファイル: foo.c
#include "foo.h" struct foo_t { // foo_t という名前の構造体の「定義」をする。 char* m_name; int m_value; }; foo_t foo_new(char* a_name, int a_value) { foo_t this* = (foo_t*)malloc(sizeof(foo_t)); // 構造体の領域を確保 this->m_name = (char*)malloc(strlen(a_name) + 1); // 文字列型のメンバーの領域を確保 strcpy(this->m_name, a_name); // 初期化 this->m_value = a_value; // 初期化 return this; } void foo_delete(foo_t* this) { free(this->m_name); // 領域開放の順序を間違えないように... free(this); } void foo_print(foo_t* this, FILE* fp) { fprintf(fp, "%s=%d\n", this->m_name, this->m_value); } foo_t を使うプログラム: main.c
#include "foo.h" int main(int argc, char** argv) { /* foo_t foo; だとコンパイルエラーになるなぜなら、foo_t の大きさが分からないから */ foo_t* foo = foo_new(argv[1], atoi(argv[2])); // ポインタとしてしか参照できない。 foo_print(foo, STDOUT); foo_delete(foo); } では、これと等価な Java のコードはどうなるでしょう。Java では、宣言と定義は区別ができませんので、ヘッダファイルというのはありません。なので、ファイルは二つ。定義する側と使う側です。 foo.java
class foo_t { String m_name; int m_value; public foo_t(String a_name, int a_value) { m_name = a_name; m_value = a_value; } public void print(PrintStream a_ps) { ps.print(m_name + "=" + m_value + "\n"); } } main.java
class main { static public void main(String[] args) { foo_t foo = new foo_t(args[0], Integer.toInt(args[1])); foo.print(System.out); } } クラスの中にメソッドを書くという事の意義はなんなのか?これを説明するため、一旦 Java vs C ではなく C++ vs C に話をずらします。 C++ 言語のコードと等価な C 言語のコード(C++ コンパイラが中間コードとして扱っている C のコード)はどうなるか、と考えてみると、 foo.cpp
class foo_t { ... }; foo_t foov; foo_t* foop; foov.print(stdout); foop->print(stderr); foo.c
struct foo_t { ... }; foo_t foov; foo_t* foop; foo_print(&foov, stdout); ↑ ↑ | オブジェクトへのポインタを第一引数にする(&をつける) | 構造体名を関数の頭に付ける | | オブジェクトへのポインタを第一引数にする(もともとポインタ) ↓ ↓ foo_print(foop, studerr); 実は、C++ コンパイラは、インスタンスを第一引数する、クラス名を関数にくっつける、という変換をやってくれるのです。実際、C++ コンパイラはその昔、登場時には C++ から C への翻訳プログラムという形で実装されていました( このような、構造体の名前とそれを処理する関数の名前を適宜、合成し、集合(class)として管理する仕組みが、オブジェクト指向におけるクラスの基礎です。ところで、なぜ関数とは呼ばずにメソッドと呼ぶか?これは、オブジェクト指向言語の始祖 Simula の設計にまで遡ることになります。その説明はいずれ... 関数名を処理するデータ(構造体)と結びつける仕組みのご利益は沢山あります。まず、何と言っても、同じ内容の処理を違うデータ構造にする関数の名前を同じにできるということです。 C だと、違う構造体について、その内容をプリントする関数を作るなら、それぞれ別の名前を(普通は素直に構造体名を関数の前か後に)付ける必要があります。 foobar.c
struct foo_t { .... }; struct bar_t { .... }; void print_foo(foo_t* this, FILE* fp) { ... } void print_bar(bar_t* this, FILE* fp) { .... } あるいは、どうしても同じ関数名で使いたい(使わせてたい)なら、一つの関数にまとめてしまい、入力となるデータがどっちの構造体かを示すフラグをつけるなんてことをしてました。 barfoo.c
#define IS_BAR 1 #define IS_FOO 2 struct bar_t { .... }; struct foo_t { .... }; static void print_bar(bar_t* this, FILE* fp) { .... } static void print_foo(foo_t* this, FILE* fp) { ... } void print(void* this, int type, FILE* fp) { switch (type) { case IS_BAR: print_bar((bar_t*)this, fp); break; case IS_FOO: print_foo((foo_t*)this, fp); break; } } いずれにせよ、非常に繁雑でバグが入り込みやすいです。しかし、C++ では foo.h
class foo_t {
....
void print(FILE* fp); // メンバー関数の「宣言」
};
foo.cpp
void foo_t::print(FILE* fp) // メンバー関数の「定義」
{
...
}
bar.h
class bar_t { .... void print(FILE* fp) { // C++ では 「宣言」と「定義」を同時にもできる .... // inline 定義と言う } // 見ためは Java と変わらない }; foo, bar を使うコード
#include "foo.h" #include "bar.h" .... foo_t* foo = new foo_t; bar_t bar; foo->print(stdout); // foo はポインタ変数なので -> でメンバー関数を参照 bar.print(stderr); // bar はスタック変数なので . でメンバー関数を参照 それでは、やっと話題を Java に戻しましょうか...とは言ってももう語り尽くしました。 強いていうなら、Java ではクラスの外に如何なるものも存在できないということです。 クラスを地球のような天体と考えてみましょう(フィールドやメソッドは鉱物や生物かな?)。人間は地球の外では生きて行けません。地球の外で生き延びるには、空気が逃げるのを防ぎ宇宙線がとびこむのを防ぐシールドと生命維持装置を備えた、人工衛星、航宙機、宇宙ステーションという人工天体の中に守られていなければなりません。 Java プログラマの仕事は、母なる地球(Java が用意しているクラス)から素材を取り出して、自分たちが乗り込む人工天体を構築することです。ポイントとなるのはクラスを如何に安全で安定なものにするかになります。Java プログラマにとっては、すでに人間が居住可能な天体の実例としての「地球」が示されていますので、どうやったら自分が作っている人工天体を、地球のように安定で安全にできるかを想像するのです。ですから大自然(すでに書かれたコード)に学ぶというのが大事になるわけです。 一方、C 言語プログラマは、クラスによる保護を受けられず、裸同然で宇宙空間に飛び出し、OS やメモリ管理、入出力と直接格闘しながら、宇宙空間に浮遊しているプリミティブな素材を自分で加工しながら生き抜かなければならないのです。一瞬でも気を抜けば死んでしまいます。ですから、C プログラマには無から有を生み出す位の創造力が求められます。しかし、労力が多くてなかなか大きな仕事をするのは難しいです。 これくらい、C プログラマに求められる資質と Java プログラマに求められる資質は違うのです。 さて、クラスを導入することによって、違ったデータ構造に同じ様な処理をする関数を作って、同じ名前にできました。これだけでハッピー?。そうは問屋がおろしません。 例えば、こんな状況を考えてみましょう。(ここで話 Java に戻します。混乱しないでね!) class foo_t { int m_id; ... public void print_id(PrintStream a_ps) { a_ps.print(m_id); } } class bar_t { int m_id; ... public void print_id(PrintStream a_ps) { a_ps.print(m_id); } } もし、 と言うことで共通のフィールドとそれにかかわるメソッドを持つクラスを束ねて扱う方法が継承と呼ばれる技術で、Java では class com_t { int m_id; public void print_id(PrintStream a_ps) { a_ps.print(m_id); } } class foo_t extends com_t { String m_data; public void print(PrintStream a_ps) { print_id(a_ps); a_ps.print(m_data); } } class bar_t extends com_t { int m_data; public void print(PrintStream a_ps) { print_id(a_ps); a_ps.print(m_data); } }
これは、C で言えば、親クラスのオブジェクトの先頭メンバーに置いたようなメモリレイアウトを想像すれば良いでしょう。 struct com_t { int m_id; }; struct foo_t { com_t m_com; char* m_data; }; struct bar_t { com_t m_com; int m_data; }; void com_print_id(com_t* this, FILE* fp) { fprintf(fp, "id = %d\n", this->m_id); } void foo_print(foo_t* this, FILE* fp) { com_print_id(this->m_id, fp); fprintf(fp, "data = %s\n", this->m_data); } void bar_print(bar_t* this, FILE* fp) { com_print_id(this->m_id, fp); fprintf(fp, "data = %d\n", this->m_data); } 上記の例では、 void foo_print(foo_t* this, FILE* fp) { com_print_id((com_t*)this, fp); fprintf(fp, "data = %s\n", this->m_data); } void bar_print(bar_t* this, FILE* fp) { com_print_id((com_t*)this, fp); fprintf(fp, "data = %d\n", this->m_data); } 上記のような構造体の包含関係をプログラミング言語による支援(構造体定義の構文としての継承メカニズム)無しにソースコードに直接書いていたら、いつバグが入ってしまうかわかりません。継承がプログラミング言語の構文として用意されているので、コンパイル時に誤りをみつける事ができるわけです。継承というメカニズムはいわゆるオブジェクト指向言語でしかできないと勘違いしている素人さんがいるようですが、それは大きな勘違いで、オブジェクト指向言語は、継承を「支援」していくれているだけです。 この継承という技法のご利益はクラスを作る側の手間を減らすだけでは有りません。クラスのインスタンスを使うが側にもご利益があります。 例えば、 com_t[] com_array = new com_t[10]; com_array[0] = new foo_t(); com_array[1] = new bar_t(); ... for (int i = 0; i < com_array.length; i++) { com_array[i].print_id(System.out); } なぜでしょう。Java で扱われる変数(オブジェクト)は、基本型(整数)を除けば、全てヒープに取られると言う事です。なので、 com_t** com_array = (com_t**)malloc(sizeof(void*) * 10); com_array[0] = (com_t*)malloc(sizeof(foo_t)); com_array[1] = (com_t*)malloc(sizeof(bar_t)); ... for (int i = 0; i < sizeof(com_array) / sizeof(void*); i++) { com_print_id(com_array[i], stdout); } 所で、継承された子クラスの中には、自分だけは親とは違うことをしたがる子どもがいる場合もあります。そんなわがままを言う場合は、子クラスは親クラスと同じ名前(と引数のパターン)でメソッドを自前で実装することになります。これをメソッドのオーバーライド(over ride)と言います。これの説明は string の章でしています。 さて、 名前が同じなので、 class com_t { int m_id; public void print_id(PrintStream a_ps) { a_ps.print(m_id); } } interface printable_t { public void print(PrintStream a_ps); } class foo_t extends com_t implements printable_t { String m_data; public void print(PrintStream a_ps) { print_id(a_ps); a_ps.print(m_data); } } class bar_t extends com_t implements printable_t { int m_data; public void print(PrintStream a_ps) { print_id(a_ps); a_ps.print(m_data); } }
printable_t[] pt_array = new printable_t[10]; pt_array[0] = new foo_t(); pt_array[1] = new bar_t(); ... for (int i = 0; i < pt_array.length; i++) { pt_array[i].print(System.out); } とすることができます。 まず、C 言語のおさらい。変数を宣言したら、それが実際にメモリの何処に置かれるか理解しています? 基本的には、データ領域(初期化データ、BSS)、ヒープ領域、スタック領域の三つの領域です。C 言語での それでは、Java ではどうなるか? Java では、クラスの外には変数や関数は存在できません。クラスの外に存在できるもの。それは当然ですが、それはクラス(の定義情報)だけです。そういう意味で言えば、クラスはグローバル(変数)です。 クラスはプログラムの実行前からメモリ上に存在します。Java インタープリタがクラスファイルを読み込むとクラスの各種の情報がメモリ中に組み立てられます。Java で書かれたプログラム(バイトコード)の実行が始まるのは、クラス情報がメモリに読み込まれてからです。つまり、(機械語や C 言語を使って Java VM の動きに介入することの出来ない)普通のプログラマからみれば、実行前から存在しているものです。 それでは、Java 言語での
一方、 ただし、 class foo_t { String m_id = "Mobius 1"; static void main(String[] args) { System.out.println("Hello, my call sign is " + m_id); } } これはコンパイルエラーになります。 mobius1@thunderbolt$ javac foo_t.java foo_t.java:6: non-static variable m_id cannot be referenced from a static context System.out.println("Hello, my call sign is " + m_id); ^ 1 error だって、
class foo_t {
String m_id = "Rapier 8";
static void main(String[] args) {
foo_t foo = new foo_t(); // インスタンスを作る
System.out.println("Hello, my call sign is " + foo.m_id);
}
}
か、
class foo_t {
static String m_id = "Halo 8"; //
のどちらかです。
まずは、フィールドや変数に適用する場合について考えます。 一旦、C 言語で考えてみます。 C 言語のプログラムで、同じ値(数値、文字列。定数と呼ぶ)を何度も使う必要がある場合、古風なプログラマは #include <stdio.h> #define FORMAT_STRING "%s-%d, engage!\n" #define HALO "Halo" #define VIPER "Viper" int main(int argc, char** argv) { printf(FORMAT_STRING, HALO, 2); printf(FORMAT_STRING, VIPER, 3); printf(FORMAT_STRING, HALO, 10); printf(FORMAT_STRING, VIPER, 9); } 一方、現代のプログラマは #include <stdio.h> const char* format_string = "%s-%d, engage!\n"; const char* halo = "Halo"; const char* viper = "Viper"; int main(int argc, char** argv) { printf(format_string, halo, 2); printf(format_string, viper, 2); printf(format_string, halo, 10); printf(format_string, viper, 9); }
例えば、以下のような間違ったコードを書いたとします。コンパイラがエラーを発見する場所が変わります。 #include <stdio.h> #define FORMAT_STRING "%s-%d, engage!\n" #define HALO Halo // ダブルクウォートで囲むのを忘れている #define VIPER "Viper" int main(int argc, char** argv) { printf(FORMAT_STRING, HALO, 2); // コンパイラはここでエラーを見付ける。 printf(FORMAT_STRING, VIPER, 3); printf(FORMAT_STRING, HALO, 10); // コンパイラはここでエラーを見付ける。 printf(FORMAT_STRING, VIPER, 9); }
#include <stdio.h>
const char* format_string = "%s-%d, engage!\n";
const char* halo = Halo; // コンパイラはここでエラーを見付ける
const char* viper = "Viper";
int main(int argc, char** argv)
{
printf(format_string, halo, 2);
printf(format_string, viper, 2);
printf(format_string, halo, 10);
printf(format_string, viper, 9);
}
コンパイラはデータ型を常にチェックしていますが、プリプロセッサは字面しか見ていません。短いプログラムでは大きな差が現れませんが、複雑なプログラムでは見付けにくいコンパイルエラー、更には、潜伏してなかなか解決できないバグの原因となります。
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { const double p = atof(argv[1]); // p、q の値は決定(代入)以後、 const double q = atof(argv[2]); // 変化しないはず(変化させないつもり)、と宣言する printf("%g / %g", p, q); p /= q; // error: assignment of read-only variable `p' // p の値は変化しない(させない)と宣言したにも係わらず // 値を変えようとしているので、コンパイルエラーになる printf(" = %g\n", p); } Java でフィールドや通常の変数に フィールドの場合、代入する(代入文の左辺になる)のは1度だけそれもコンストラクタの中か、定義しているところでしか許されません。また、未定のままにすることも許されません。 class NothingEverLasts { final int m_employment_age = 22; final int m_property; static final int m_life = 80; public NothingEverLasts(int a_age, int a_salary) { if (a_age < m_employment_age) m_employment_age = a_age; // cannot assign a value to final variable m_employment_age // 既に初期化されているのでエラー if (a_age < m_life) m_property = a_salary * (m_life - m_employment_age); // variable m_property might not have been initialized // if に入らなかった場合、m_property の値が設定されないのでエラー } } クラスに対して クラスに対して また、個々のメソッドに対して ここまで何度も、メソッドには見えない最初の引数としてインスタンスが渡されている、と書いて来ました。 一方、メソッドの中では、そのインスタンスを指す変数が全く見当たりません。実は、隠された引数として渡されるオブジェクトは、見えないのではなくて、単に省略できるというだけです。 インスタンスへの参照(ポインタ)は ただ、 class foo_t { String name1 = "Skyeye"; String m_name2 = "Mobius 1"; void print(String name) { System.out.println(name + " here call sign " + name1 + ", do you read?"); // this 省略 System.out.println("Your call sign is " + this.m_name2 + "."); // this 明示 } public static void main(String[] args) { foo_t foo = new foo_t(); foo.print("AWACS"); } } 例外処理は、C には言語が提供する仕組みとしては装備されていません。C++ に導入されたのも C++ に例外処理の仕様が入ってから実際のコンパイラに搭載されるまではずいぶん時間がかかりました。そもそも、例外をコンパイラ言語で実現するのはかなり無茶なのです。初期の C++ コンパイラでは 例外処理は要するにエラー処理です。古典的な C のプログラムでは(Java でもやってしまうこと有りますが)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int divid(int p, int q)
{
if (q == 0) // ゼロで割ったら大変...
return INT_MIN; // とりあえず、
リターン値でエラーを判断しようとするのは場合によっては不可能な場合があります。ここでは、q がゼロの時だけでなく、除算の結果として 仕方が無いので、エラーが起こったことを通知するようにします。 #include <stdio.h> #include <stdlib.h> #include <limits.h> int divid(int p, int q, bool* err) { if (q == 0) { // ゼロで割ったら大変... *err = true; return 0 // 何を返すかを考えてもしょうがない。 } *err = false; return p / q; } int main(int argc, char** argv) { bool err; int d = divid(atoi(argv[1]), atoi(argv[2]), &err); if (err) { printf("divide by zero\n"); } else { printf("%d\n", d); } } このやり方で生き残れると主ったら大間違い。関数の仕様がちょっと複雑になると、エラーの原因も多様化します。 #include <stdio.h> #include <stdlib.h> #include <limits.h> int divid(char* ps, char* qs, bool* err) { if (ps == NULL || strlen(ps) == 0 || qs == NULL || strlen(qs) == 0 || atoi(qs) == 0) { *err = true; return 0; } *err = false; return p / q; } int main(int argc, char** argv) { bool err; int d = divid(argv[1], argv[2], &err); if (err) { printf("divide by zero\n"); } else { printf("%d\n", d); } } しかし、これでは何がエラーの原因か、呼び出した側は全く分かりません。ならば、エラーの原因をコード化すればと思っていまいます。 #include <stdio.h> #include <stdlib.h> #include <limits.h> int divid(char* ps, char* qs, int* err_code) { if (ps == NULL) *err_code = -1; else if (strlen(ps) == 0) *err_code = -2; else if (qs == NULL) *err_code = -3; else if (strlen(qs) == 0) *err_code = -4; else if (atoi(qs) == 0) *err_code = 1; else *err_code = 0; if (*err_code != 0) return 0; *err = false; return p / q; } int main(int argc, char** argv) { int err; int d = divid(argv[1], argv[2], &err); if (err) { printf("divid() error = %d\n", err); } else { printf("%d\n", d); } } さぁ、いよいよドツボにはまってきました。 さらに、現実的なプログラムでは考えなければならないことはもっと増えます。ユーザーに分かりやすいエラーメッセージはどうします(数字のエラーメッセージを食らうと如何に頭にくるか。分かりますよね?)。更には、関数の内部で別の関数を呼び出していてそいつがエラーになったらどうしますか? 難易度エースで楽々ランク S を取る、年季の入った C 言語プログラマでもこの問題をクリアするの容易ではありません。何とかクリアしても、そのおかげでコードはメチャクチャ複雑になったり(例えば、どんな小さな関数にも、エラーコードとエラーメッセージを生成する仕組みを組み込むとか)します。ましてや、大人数のプロジェクトでは大変。大きなプロジェクトで使いものになるエラー処理システムの設計は普通の人にはまずできません。 ※ M$ が、Windows 全体で使うオブジェクト指向システムとして COM を捨てて CLR に移行しようとしているのは、Java への対抗というだけではなく、返り血( ここまで、Java ではなく C の話でしたが、これはなぜ例外という仕組みが必要かを理解する上での重要なものです。要するに、返り値でエラーを表すのはダメだと言うことです。となると、エラー情報を引き渡す、全く新たな仕組みが必要になります。それが例外処理で、そのために使う構文要素が まず、例外処理機構の全く無いコードでエラーを起こさせてみます。 class Divider { static int divid(String ps, String qs) { return Integer.parseInt(ps) / Integer.parseInt(qs); } public static void main(String[] args) { int d = divid(args[0], args[1]); System.out.println(d); System.out.println("Divider, mission completd"); } } mobius1@fulcrum$ java Divider 20344 232 87 Divider, mission completd mobisu1@fulcrum$ java Divider 20344.0 232 # エラー#1 Exception in thread "main" java.lang.NumberFormatException: For input string: "20344.0" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:456) at java.lang.Integer.parseInt(Integer.java:497) at Divider.divid(Divider.java:3) at Divider.main(Divider.java:7) mobius1@fulcrum$ java Divider 20344 / # エラー#2 Exception in thread "main" java.lang.NumberFormatException: For input string: "/" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:447) at java.lang.Integer.parseInt(Integer.java:497) at Divider.divid(Divider.java:3) at Divider.main(Divider.java:7) mobisu1@fulcrum$ java Divider 20344 0 # エラー#3 Exception in thread "main" java.lang.ArithmeticException: / by zero at Divider.divid(Divider.java:3) at Divider.main(Divider.java:7) mobius1@fulcrum$ java Divider # エラー#4 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0 at Divider.main(Divider.java:7) ※ 例外(エラー)のメッセージは標準出力ではなく標準エラーにプリントされています。 例外が起こると、どういう例外が起こったか、そして、その原因についての説明文がプリントされ、その後、例外が起こった場所(メソッド名、クラス名、行番号)が呼び出しの逆順にプリントされます。 また、例外が起こったときのメッセージを良く見てください。"Divider, mission completd" が出力されていません。これはどういうことかと言えば、例外が起こると、そのメソッドの実行は即座に中断され、 これは、エラーが発生した所で例外(Exception)というオブジェクトが生成され、 もうちょっと複雑なプログラムにしてみます。 import java.io.*; class Divider { static int divid(String ps, String qs) { return Integer.parseInt(ps) / Integer.parseInt(qs); } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)) for (String line = br.readLine(); line != null; line = br.readLine()) { int d = divid(args[0], line); System.out.println(d); } System.out.println("Divider, mission completd"); } } 標準入力からデータを読み込んで繰り返し除算をするようにしました。もし、大量のデータを読み込ませている間に変な入力をもらってエラーになった場合、プログラムではなく、データをデバッグしなければなりません。 このシンプルな例ではエラーの原因となった文字列が表示されているので原因を特定するのはさほど大変ではないですが、もっとデータが多かったり複雑だったりする場合は、出来たらデータの何処(何行目)にエラーの原因が有ったかを教えてもらいたいものです。また、エラーが起こっても処理(データの読み込み)を継続したい場合もあると思います。上がって来る例外が そういう場合は例外を捕獲(キャッチ)します。 import java.io.*; class Divider { static int divid(String ps, String qs) { return Integer.parseInt(ps) / Integer.parseInt(qs); } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int count = 0; int err_count = 0; for (String line = br.readLine(); line != null; line = br.readLine()) { try { int d = divid(args[0], line); System.out.println(d); } catch (NumberFormatException ex) { System.err.println("DATA ERROR AT LINE: " + count); ex.printStackTrace(System.err); System.out.println("# DATA ERROR"); err_count++; } catch (ArithmeticException ex) { System.err.println("ARITH ERROR AT LINE: " + count); ex.printStackTrace(System.err); System.out.println("# ARITH ERROR"); err_count++; } count++; } System.out.println("Divider, tried " + count + " missions, failed + " err_count + " missions"); } } これで、おかしな文字列を入力しても処理は止まることなく、かつ、エラーの原因も入手できます。 ここまでは、自分でエラーを見つけた場合ではなく、エラーが起こってしまってからの対処について書きました。それでは、自分がエラーを見つけた場合はどうしましょうか? ちょっと、不自然な例ですが、 import java.io.*; class UniqueException extends Exception { public UniqueException(String message) { super(message); } } class Divider { static int divid(String ps, String qs) throws UniqueException { int p = Integer.parseInt(ps); int q = Integer.parseInt(qs); if (ps.equals(qs)) throw new UniqueException("unique as string: " + ps); if (p == q) throw new UniqueException("unique as int: " + p); return Integer.parseInt(ps) / Integer.parseInt(qs); } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int count = 0; int err_count = 0; for (String line = br.readLine(); line != null; line = br.readLine()) { try { int d = divid(args[0], line); System.out.println(d); } catch (NumberFormatException ex) { System.err.println("DATA ERROR AT LINE: " + count); ex.printStackTrace(System.err); System.out.println("# DATA ERROR"); err_count++; } catch (ArithmeticException ex) { System.err.println("ARITH ERROR AT LINE: " + count); ex.printStackTrace(System.err); System.out.println("# ARITH ERROR"); err_count++; } catch (UniqueException ex) { System.err.println("UNIQUE ERROR AT LINE: " + count); ex.printStackTrace(System.err); System.out.println("# UNIQUE ERROR"); err_count++; } count++; } System.out.println("Divider, tried " + count + " missions, failed " + err_count + " missions"); } } 自分用の例外クラスを作る際には、かならず 上の例で、メソッド定義の所に なんでこれが必要かといえば、例外をどこで捕まえるかはシステム全体の設計ポリシーで決まります。例外(エラー)が起こった処ですぐに対処(
この解説では、ほとんどは Java にはあって C には無いものばかりでした。しかし、C には有って Java には無いものもあります。それがデータ領域の開放です。 Java と C の等価なコードの例で、C の方には領域を確保する関数がありましたし、Java も C では 一方、Java で class proc { void proc1() { String str = new String("Hello World"); return str; } void proc2(String s) { System.out.println(s); } public static void main(String[] args) { String s = proc1(); proc2(s); } }
Java インタープリタはそういうゴミを適当なタイミングで回収にきます。これが Garbage Collection (GC) です。 Java では、使われなくなったデータの開放は GC によって自動で行われますので、メソッドから出て行くときにとかに開放するのを気遣う必要はありません。しかし、実際のプログラムが扱うのはデータはメモリの中だけではありません。メモリの外に恒久的(persistently)に存在するデータをいじります。具体的にはファイルとかデータベースです。 例えば、テンポラリファイルを使うプログラムであれば、プログラムが正常であれ異常であれ、終了したらテンポラリファイルを消さなければなりません。
import java.io.*;
class temp {
void proc(FileOutputStream fos) {
// 何か処理をする
}
public static void main(String[] args) throws IOException {
File tempfile = File.createTempFile("foo", ".tmp");
FileOutputStream fos = new FileOutputStream(tempfile);
proc(fos);
fos.close();
tempfile.delete();
}
}
ここで、 import java.io.*; class temp { static void proc(FileOutputStream fos) throws Exception { // どんな例外を投げるか不明 // 何か処理をする } public static void main(String[] args) { File tempfile = File.createTempFile("foo", ".tmp"); FileOutputStream fos = new FileOutputStream(tempfile); try { proc(fos); } catch (Exception ex) { } fos.close(); tempfile.delete(); } } でとりあえず、例外を捕獲、拘束することで、テンポラリファイルを消す機会を確保できます。しかし、これでは、例外の処理(例外の内容をプリントするとか、応急処置をするとか)ができません。こういう場合、最終的にしたいことを必ずさせるの強いる仕組みが import java.io.*; class temp { static void proc(FileOutputStream fos) throws Exception { // どんな例外を投げるか不明 // 何か処理をする } public static void main(String[] args) throws Exception { File tempfile = null; FileOutputStream fos = null; try { tempfile = File.createTempFile("foo", ".tmp"); fos = new FileOutputStream(tempfile); proc(fos); } finally { if (fos != null) fos.close(); if (tempfile != null && tempfile.exists()) tempfile.delete(); } } } こうすると、 所で、 public static void main(String[] args) throws Exception { try { File tempfile = File.createTempFile("foo", ".tmp"); FileOutputStreamfos = new FileOutputStream(tempfile); proc(fos); } finally { if (fos != null) fos.close(); if (tempfile != null && tempfile.exists()) tempfile.delete(); } } 変数はブロック( 当然ですが、 public static void main(String[] args) throws Exception { File tempfile = null; FileOutputStream fos = null; try { tempfile = File.createTempFile("foo", ".tmp"); fos = new FileOutputStream(tempfile); proc(fos); } catch (UniqueException ex) { ex.printStackTrace(System.err); System.err.println("こまったエラーが起こったぞ!"); return; } finally { if (fos != null) fos.close(); if (tempfile != null && tempfile.exists()) tempfile.delete(); } System.err.println("うまく行きました!"); } ※ 余談ですが、実際にこんな単純なテンポラリファイルの使い方の場合は、 C 言語において、初級プログラマの敵、英語以外を扱うプログラムの悩みの種、そしてセキュリティホールの最多要因である文字列の扱いが、Java では大幅に楽になっています(もっとも、メモリ管理機能をもつインタープリタ言語には共通の事ですが)。 C 言語では文字は CJK (Chinese, Japanese, Korean)な人たちにとって文字とは1バイトの整数で表せる255個で収まるものではありません。漢字辞典の厚さをみれば明らかですが、万の単位で表現しなければなりません。現時点では、Java を含むほとんどのインタープリタ言語、および、Windows 等のエンドユーザー向けオペレーティングシステムでは、1文字を2バイトの整数で表現する(対応づける) Unicode とよばれるコード化システムが使われています。では、文字は さて、そういう文字が並んでいる文字列を扱うのは、考えただけで頭がおかしくなりそうで、通常のプログラマには手に負えません。よく、メモリ管理の観点から文字列を扱う際の C の難しさが言われますが、それは単純に ※ C 言語向けに、国際化文字列を扱うライブラリパッケージはたくさん出回っていますので、それを使いこなせば C 言語でも CJK 対応のプログラムを書くことができます。もちろん、その場合はメモリ管理の問題を自力で解決するか、別途、メモリ管理用のライブラリを使う事になります。 ということで、そんな悪夢を緩和するために、インタープリタ言語では、文字は文字、文字列は文字列として扱えるようにいろいろな工夫をしてくれています。 Java では文字列は、クラス・ライブラリが提供するデータ型(クラス)ではなく、言語仕様の一部としての基本型として定義されています。これが また、基本型ということは配列でもありません。つまり、 ただ、 Java のクラスライブラリの説明に、「文字列は定数です。この値を作成したあとに変更はできません。...。文字列オブジェクトは不変であるため...」と書かれています。これは C 言語上がりのプログラマにはなかなか理解できない表現です。
struct string_t { unsigned long m_length; char[1] m_str; }; /* * Java で言えば、定数で初期化する時に呼ばれる * string str = "Mobius 1 is here. We will win."; */ string_t* new_string_from_literal(char* str) { const unsigned long len = strlen(str); string_t* new_str = (string_t*)malloc(sizeof(string_t) + sizeof(char) * len); new_str->m_length = len; strcpy(new_str->m_str, str); return new_str; } /* * Java で言えば、文字列を連結する(+演算子)時に呼ばれる。 * (実際には、StringBuilder (または StringBuffer) クラスを一時的に使って実現さているそうです) */ string_t* add_string(string_t* s1, string_t* s2) { const unsigned long len = s1->m_length + s2->m_length; string_t* new_str = (string_t*)malloc(sizeof(string_t) + sizeof(char) * len); new_str->m_length = len; strcpy(new_str->m_str, s1->m_str); strcat(new_str->m_str, s2->m_str); return new_str; } /* * Java での String.substring() に相当 */ string_t* substring_string(string_t* str, unsigned long begin_index, unsigned long end_index) { const long unsigned len = (unsigned long)(end_index - begin_index); string_t* new_str = (string_t*)malloc(sizeof(string_t) + sizeof(char) * len); new_str->m_length = len; strncpy(new_str->m_str, str->m_str + begin_index, len); return new_str; } /* * Java での '==' に相当 */ bool identity_check_string(string_t* s1, string_t* s2) { return s1 == s2; } /* * Java での equals() に相当 */ bool equals_string(string_t* s1, string_t* s2) { return identity_check_string(s1, s2) || (s1->m_length == s2->m_length && strcmp(s1->m_str, s2->m_str) == 0); } ポイントは、なにか操作をしたら必ず新たな
次のプログラムを見てみましょう class CountDown { public static void main(String[] args) { System.out.println("Fifteen seconds to launch. " + "All aircraft and vehicles flow to safe area."); Integer count = 10; System.out.println(count-- + ", " + count-- + ", " + count-- + ", " + count-- + ", " + count-- + ", Starting ignition."); count = 3; System.out.println(count-- + ", " + count-- + ", " + count-- + ", " + "Ignition! Lift off!"); } } mobius1@hornet$ javac CountDown.java mobius1@hornet$ java CountDown Fifteen seconds to launch. All aircraft and vehicles flow to safe area. 10, 9, 8, 7, 6, Starting ignition. 3, 2, 1, Ignition! Lift off!
これは、 Java では文字列へのキャストが必要になると、そのオブジェクトの ただ、無償で付いている
コメントは C と同じ囲み形式( なお、囲み形式の場合、囲みコメントは入れ子にはできないので注意してください。コメントつきのソースをコピペして、さらにそれをコメントで囲もうとしたときによくやります。
/* 外側のコメント
* /* 内側のコメント */ 内側のコメント閉じは、外側のコメントの閉じと認識される。
*/ ×。これはコードの一部と認識され、コンパイルエラーになる。
|
|||||
Copyright © 2005 Takaya Sakusabe All rights reserved.
Generated: Nov 28, 2005 6:11:34 PM UTC. |