医療情報学
システム工学
Java 解説 - C との比較
ホーム / Java 解説 - C との比較
* 更新情報
  • 2005.11.18 17:53 - final - const?
  • 2005.11.17 14:43 - string != char[]
  • 2005.11.02 03:10 - とりあえず基本的な事はを書き切りました
  • 2005.10.28 23:55 - 引越し

一つでもプログラミング言語をマスターしていれば、それから他の言語をマスターするのは簡単です。所詮、プログラミング言語は最終的には機械語として実行され計算機を制御しようとしているだけなのですから。つまり、最終的にはどの言語でも出来ることはほとんど同じ。だって、計算機にできる事はプログラミング言語が変わったからといて変わるものでは有りません。なので最初にマスターした言語が低レベルであればより有利です。C 言語をマスターしていれば、Java をマスターするのは楽勝。と言うことで、C との違いという形で Java について解説します。

Java では、クラスとか継承とか一見高度な概念が出て来ます。一つ理解しておけば良いのは所詮は構造体だと言うことです。C 言語を母国語とするプログラマは、最終的には、構造体、ポインタ、関数へのポインタ、の組合せとして分析することで Java のほとんどの機能をひもとくことができます。これは、VB を母国語とするプログラマに対して圧倒的なアドバンテージです。

常に、C 言語だったらどういう風になっているか、と思い浮かべながらコードを書きましょう。それができない? 一つのプログラミング言語をマスターできていないなら、次の言語をマスターしようとしても混乱するだけですよ。摘み食いは止めましょう。

なぜかと言えば、最初(あるいは初期)に習得した言語は、計算機についての理解の仕方そのものを決定付けてしまうので、違う理解をしている(違うメンタルモデルを持っている)人との間では、表層上は言葉が通じても考えていることは全く違うという状態になりますから。また、図を用いて説明することも避けています。これも誤った理解を植え付ける原因になりますので。

新しいプログラミング言語を使うとき、必ずやらなければならない古いしきたりです。

まず、ホームディレクトリにファイルが沢山ならぶと厄介なので、プログラミング作業用の src ディレクトリを作ります。

mobius1@phantom$ cd                           ホームディレクトリに移動します
mobius1@phantom$ pwd                          ホームディレクトリにいることを確認します
/students/mobius1
mobius1@phantom$ mkdir src                    src という名前のディレクトリを作ります
mobius1@phantom$ ls -ld src                   src という名前のディレクトリが存在することを確認します
drwxr-xr-x  2 mobius1  mobius1  512 Oct 19 13:29 src
mobius1@phantom$ cd src                       作業ディレクトリを src に移動します
mobius1@phantom$ pwd                          作業ディレクトリを確認します
/students/mobius1/src
mobius1@phantom$ 

次に、エディタ(Emcws か GEdit, vi) を起動して src ディレクトリのなかに次のようなファイルを作ります。ファイル名は hello.java あたりでいいでしょう。

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 との相性もあり、無理に、一つのルールを決めてしまって、必ずルールを守らなければならない、というようなやり方は絶対にうまく行きません。それに、ふと気づいたのですが、昔の javac は1ソース/1クラスを強いていたような気がするけど、J2SDK 1.5 では文句を言われなかった...


C コンパイラと Java コンパイラの大きな違いは、C コンパイラはコンパイルされていないソースコードしか解析できないですが、Java コンパイラはソースコードだけでなくコンパイル済みのクラスファイルも解析できるということです。

結果として、Java は #include が不要になっています。ただし、#include を処理しているのは C コンパイラではなくプリプロセッサであり、プリプロセッサの機能(ご利益)は他にも沢山あるので、それが使えない Java コンパイラは C コンパイラに比べて不便なことが多いです。勿論、プリプロセッサを Java 言語に対して使うことも出来ますが、これは Java の文化に合わないので無理に使うのは止めた方が良いです。


さて、C コンパイラでは、インクルードするヘッダファイルを探すパスを指定する際には -I オプションを付けました。Java コンパイラで、これに相当するのが、-cp です。先に書きましたように、Java コンパイラはクラスファイルを読み取ります。ですので、コンパイル時と実行時にアクセスするクラスファイルは同じものです。ですから、コマンドラインのオプションも共通で -cp になるわけです。

Java コンパイラ に特有なオプションとしては、-d というのがあります。これは、コンパイル済のクラスファイルを出力するディレクトリを指定するオプションです。C コンパイラの -o とは違います。C コンパイラ は1ソース1オブジェクトすからファイル(名)で指定します。

Java コンパイラ はソースファイルが1つでも出力されるクラスは沢山かもしれませんのでディレクトリを指定するのです。また、C コンパイラ(正確にはリンカ(ld))はファイル名に意味を持たせませんが、Java コンパイラ(あるいは実行時の java)は、ファイル名=クラス名.class と解釈しますので、名前を勝手につけるわけには行かないのです。

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 言語では、コンパイラ(javac)とインタープリタ(java)を別のプログラムにしています。他のインタープリタ言語では同じコマンドがコンパイラとしてもインタープリタとしても動きます。なぜ分けたのかは...いろいろな理由があるのでしょう。


※さて、ちょっと無駄話がすぎました。恐らく、ここに来ている人は、母国語が C で、第一外国語として Java を学ぼうとしている人だと思います。もし、既に Perl 等の高度なインタープリタ言語をマスターしているのであれば、わざわざ Java なんか学ぶ必要はないですし、学ぶにしても、既に二つ目をマスターできたのであれば、帰納によりN個の言語をマスターできるのは自明ですからね。


Java コンパイラが出力するのは、クラスファイル(.class)です。これがバイトコード。java コマンドでは、エントリーポイントを指定してクラスファイルを実行します。

C コンパイラ(リンカ)が出力する実行可能ファイルにもエントリーポイントというものがあります。エントリーポイントとは、OS がプログラムファイルをメモリに読み込んで整頓したあと、最初に実行する(制御を渡す言う)命令のアドレスです。皆さんはそれが、main() だと思っていませんか?それは違います。C コンパイラが吐き出す実行プログラムのエントリーポイントは crt0 という特殊なライブラリの中に有り、そこから main が呼ばれるのです。よく考えてみましょう。main()int 型の値を返しますが、OS にどうやって返しているのでしょうか?そういう低レベルの仕事をするライブラリが必要なのです。それが crt0 (C Runtime 0)です。(また話が逸れてしまいました)

Java インタープリタは、コマンドラインに指定されたクラスのクラスファイルをメモリに読み込み、そのクラスの main() メソッドを実行します。

さて、では Java インタープリタ はどうやってクラスファイルを探すのでしょうか?それは、-cp コマンドラインオプション、あるいは、CLASSPATH 環境変数で指定されたディレクトリあるいは zip/jar ファイルの中を探しに行きます。

クラスファイルを探す際には、クラスファイル名=クラス名.classとして探します。

とりあえず、機能としては C の #include と同じようなものと考えていいですが、実は違います。まずは、C のおさらいをしましょう。


C の場合、コンパイラが実際にコンパイルする直前に、プリプロセッサというプログラム(コンパイラに組み込まれてしまっている場合もある)を呼び出し、#include 文のあるところに指定されたソースファイルの中身を(通常はヘッダ・ファイルで、.h と拡張子を区別はしているけど、実際の中身はソースコード)置き換え(展開とも言う)てから(これをプリプロセスという)、コンパイルを始めます。

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 は、ソースコードをコンパイルするときに、コンパイル中のソースに定義されていないクラスが見つかった場合、クラスファイル (.class あるいは、JAR ファイル(.jar)の中に入っているクラスファイル)の中から探そうとします。これが基本動作。

まず、なんといっても、C コンパイラと Java コンパイラの違いは、C コンパイラはコンパイル前のソースコードしか解析できないですが、Java コンパイラはソースコードだけでなくコンパイル済みのクラスファイルも解析できるということでです。

import 文と #include 文の決定的な違いは、import 文はあくまでも名前の解決のヒントを与えるだけで、この文のところに何等かコードが読み込まれるわけではありません。Java コンパイラがクラスファイルの中身を読みに行くのは、実際にクラスを使おうとしている所です。import 文を解釈した時点では、そういうクラスががあるかどうかの確認だけです。

さて、ここから先に進むには、package の説明をしなければなりません。

package とは、クラスの名前(interface も特殊なクラスですよ)を区別しやすくするよう、階層構造を持たせる仕組みです。名前空間(namespace)などとも呼ばれる仕組みです。実際、C++, C# では namespace 文で定義します。

まず、身近な例として、ディスクの中のディレクトリ構造のことを考えてみてください。例えば同じ名前のファイルは同じディレクトリの中には置けませんが、違うディレクトリには置けます。例えば、名簿.xls というファイルでクラブのメンバーを管理するとします。もし、あなたが複数のクラブに所属しているとすると、ファイルの名前が重なってしまってこまりますね。こういう場合、クラブ毎にディレクトリを作り、それぞれのディレクトリに名簿.xlsを保存すれば、名前の衝突を回避できますね。これと同じことは、プログラミング言語の中でも起こるわけです。

ディレクトリ=パッケージ、ファイル=クラスと思えばおおむね間違いはないです。

Java 言語の中の例で言えば、XML を処理する DOM API では、Element とか Document という名前の interface があります。こんな一般的な名前を予約されては困りますよね。これらのクラスは org.w3.dom という package の中で定義されています。Elementというクラスを使う場合、以下の二つのやり方があります。

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 コンパイラが Element クラスの情報をクラスファイルに探しに行くのは、elm 変数が Element 型だと指定され、Element クラスの宣言を探す必要が生じたときです。

当然、後者はソースを打つのが面倒になります。でも、自分のプログラムの中で自分用に Element という名前のクラスを使いたいなら工夫しなければなりません。

あるクラスをパッケージの中に納めることを宣言するには package 文を使います。

package mypackage;

class the_class {
   ...
}

上記のクラスにアクセスする場合は

import mypackage.*;

class main {
    public static void main(String[] args) {
        the_class cls = new the_class();
    }
}

package 宣言をしないと、無名パッケージに属していることになります。

さて、このパッケージの名前が . で区切って階層構造になっていますね。階層構造といえば、ファイルシステムのディレクトリ構造です。実はこれは直接対応しているのです。

java コマンドはクラスパスの指定が有る無しに係わらず、デフォルトでクラスファイルを探しに行くところが有ります。/usr/local/jdk1.5.0/jre/lib/rt.jar です。この中身を見て見ましょう。全部見ると面倒なので、上記の例でみた org.w3.dom パッケージについて見てみましょう。

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

これを見れば分かりますね。javapackage 指定の ./ に読み替えてクラスファイルを探しに行くのです。パッケージ階層の区切り文字を . にした理由はいまいち分かりませんが、プログラミング言語の中では / は割算演算子として定着していますので それ以外の用途に使うのは不自然かからでしょうか?また、ドメイン名とも関連付けたかったようです。ライブラリを作る団体毎に自然に分かれますから。

とりあえず、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 への翻訳プログラムという形で実装されていました(cfront と呼ばれていました)。

このような、構造体の名前とそれを処理する関数の名前を適宜、合成し、集合(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++ では class を導入することで、ずいぶんすっきりさせることが出来ます。

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);
    }
}

もし、foo_tbar_t に共通のデータ(フィールド)がないのであれば仮に同じ名前のメソッドがあったとしてもその内容は全く異なったものになります。しかしこの場合は、m_id という共通のフィールドがあり、print_id() メソッドの処理も全く同じです。同じコードを何度も書かなければならないのは面倒ですね。また、もし、同じようなクラスを沢山作る、しかも、いろんな人と共同で、となったら、約束を守らない人がいたりするかもしれません。

と言うことで共通のフィールドとそれにかかわるメソッドを持つクラスを束ねて扱う方法が継承と呼ばれる技術で、Java では extends キーワードで利用することが出来ます。

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);
    }
}

extendsキーワードは、現在定義しようとしているクラス(子クラス、導出クラスと言います)が、元になるクラス(親クラス、基底クラスと言います)のフィールド/メソッドを受け継ぎ、独自の拡張(フィールドやメソッドの追加)をしようとしていることをコンパイラに伝えます。

これは、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);
}

上記の例では、foo_print()/bar_print() の中で com_print_id() を呼び出すときに、メンバー参照をしていますが、メモリレイアウトからすれば、キャストでも同じ結果になります。

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);
}

上記のような構造体の包含関係をプログラミング言語による支援(構造体定義の構文としての継承メカニズム)無しにソースコードに直接書いていたら、いつバグが入ってしまうかわかりません。継承がプログラミング言語の構文として用意されているので、コンパイル時に誤りをみつける事ができるわけです。継承というメカニズムはいわゆるオブジェクト指向言語でしかできないと勘違いしている素人さんがいるようですが、それは大きな勘違いで、オブジェクト指向言語は、継承を「支援」していくれているだけです。


この継承という技法のご利益はクラスを作る側の手間を減らすだけでは有りません。クラスのインスタンスを使うが側にもご利益があります。

例えば、foo_tbar_t が混在した配列データを扱いたいとします。その場合、以下のようにシンプルに書くことが出来ます。

    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 で扱われる変数(オブジェクト)は、基本型(整数)を除けば、全てヒープに取られると言う事です。なので、*&(あるいは ->)が付けていなくても、全てポインタだという事です。上記の Java のコードを C 言語で考えてみると...

    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 の章でしています。

さて、extends の例では、foo_tbar_tprint() という同じ名前をもっているけど、処理内容が違うメソッドがありました。

名前が同じなので、com_t クラスに持って行きたいですが、処理内容(扱うデータ)が違いますので、それはできません。しかし、共通の名前で、概ね処理が同じであることを何とかして周知徹底させる方法が欲しいです。その為には interfaceimplements キーワードを使います。

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);
    }
}

implements キーワードは、定義中のクラス(実装クラス)は、インターフェイスで宣言されているメソッドを必ず持つ(実装する)ことを宣言します。ここで言えば、必ず print() メソッドを持っていることを「約束(契約)」しているわけです。おかげで、

    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 言語での static 宣言の効果は単に、シンボル名(変数や関数の名前)をソースコードの外に漏らさないことです。まぁ、例外的に関数内での変数定義に static 宣言をつけると、変数がスタックではなくデータ領域に取られてしまいますが(これ、結構、誤解している香具師が多い)。

それでは、Java ではどうなるか? Java では、クラスの外には変数や関数は存在できません。クラスの外に存在できるもの。それは当然ですが、それはクラス(の定義情報)だけです。そういう意味で言えば、クラスはグローバル(変数)です

クラスはプログラムの実行前からメモリ上に存在します。Java インタープリタがクラスファイルを読み込むとクラスの各種の情報がメモリ中に組み立てられます。Java で書かれたプログラム(バイトコード)の実行が始まるのは、クラス情報がメモリに読み込まれてからです。つまり、(機械語や C 言語を使って Java VM の動きに介入することの出来ない)普通のプログラマからみれば、実行前から存在しているものです。

それでは、Java 言語での static 宣言の効果はどういうものなのでしょうか?これは、実は単純なものです。クラスの生成と同時に生成されるモノになるという事です。これは、フィールドについての static 宣言については自然な感じです。しかし、メソッドについては何かピンと来ません。だって、メソッドは実行可能なコードなのだから、クラスをロードした時点で存在してるじゃん、と言いたくなります。

class の章で「実は、C++ コンパイラは、インスタンスを第一引数する、クラス名を関数の頭にくっつけるという変換をやってくれるのです」、と書きましたが、これは、そのまま Java コンパイラにも適用できます。つまり、(staticじゃやない)通常のメソッドには、ソースコード上は見えない第一引数が付いているのです。第一引数は、ヒープ領域に取られたオブジェクト。なので、オブジェクトが存在しなければ通常のメソッドは呼び出せないです。無理に(例えばリフレクションを使って)呼び出せば、第一引数は NULL なのでヌルポになるだけです。

一方、static 宣言されたメソッドは、オブジェクトに結びつけらません。言い替えると、オブジェクトを指す第一引数が不要なメソッドなので、何時でも呼び出せるのです。これで、なぜ、main() メソッドが static 宣言されているか分かりましたね?

ただし、static なメソッドを使うとき、以下のような勘違いを良くします。

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

だって、main() が呼ばれただけでは、foo_t 型のオブジェクトは一つも存在していません。正しくは(コンパイルが通るとすれば)、

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";    // static  宣言をすれば最初から存在する

    static void main(String[] args) {
        System.out.println("Hello, my call sign is " + m_id);
    }
}

のどちらかです。

final 宣言は、クラスやメソッドに適用する場合と、フィールドや変数に適用する場合で全く違う意味を持ちます。


まずは、フィールドや変数に適用する場合について考えます。

一旦、C 言語で考えてみます。

C 言語のプログラムで、同じ値(数値、文字列。定数と呼ぶ)を何度も使う必要がある場合、古風なプログラマは #define を使います。

#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);
}

一方、現代のプログラマは const を使います。

#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);
}

#define はプリプロセッサが処理しています。一方、const はコンパイラが認識します。この違いはどこに現れるでしょう。

例えば、以下のような間違ったコードを書いたとします。コンパイラがエラーを発見する場所が変わります。

#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);
}

コンパイラはデータ型を常にチェックしていますが、プリプロセッサは字面しか見ていません。短いプログラムでは大きな差が現れませんが、複雑なプログラムでは見付けにくいコンパイルエラー、更には、潜伏してなかなか解決できないバグの原因となります。

const 宣言は、コンパイル時に決まっている定数として使うだけでなく、プログラムの実行中に決められる値についても使うことができます。これは、コンパイラによるコード生成の最適化やバグの予防に大きな効果があります。

#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 でフィールドや通常の変数に final 宣言をするのは、const 宣言をするのとほぼ等価です。

フィールドの場合、代入する(代入文の左辺になる)のは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 の値が設定されないのでエラー
    }
}

クラスに対して final 宣言をするのは、それに対応する C 言語での機能はありません。Java 言語のクラスと C 言語の構造体は一見似ていますが、中身はかなり違ったものですから。

クラスに対して final 宣言をするのは、もう継承はしない(させない)という宣言です。つまり、種として最後であり、これ以上進化はしないという宣言です。子クラスは作れません。

また、個々のメソッドに対して final 宣言をすることもできます。これは、このメソッドを子クラスがオーバーライドするのを禁じる宣言です。

ここまで何度も、メソッドには見えない最初の引数としてインスタンスが渡されている、と書いて来ました。

一方、メソッドの中では、そのインスタンスを指す変数が全く見当たりません。実は、隠された引数として渡されるオブジェクトは、見えないのではなくて、単に省略できるというだけです。

インスタンスへの参照(ポインタ)は this キーワードです。普通は、タイプするのが面倒なので省略してしまいます。

ただ、this を省略すると、変数がローカル変数なのか、フィールドなのか区別が付きにくくなります。対策としては、ローカル変数とフィールドで命名規則を変えるというのがよく行われます。私は M$ 風のコーディング・スタイルをちょっとだけ真似して、フィールド(C で言えばメンバー変数)の名前には、先頭に m_ を付けています。

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++ コンパイラでは setjmp()/longjmp() を使って例外を実装していたという噂を聞いた事が有りますが、今ではコンパイラがかなり面倒なコードを吐き出すと同時に、crt0 にも対応した隠し関数が入っています。特にマルチスレッド対応のコードがかなり複雑になることは想像に堅くありません。まぁ何であれコンパイラメーカの秘密。さもなければ GNU C コンパイラの膨大なソースコードを読み解かないと...そのうちやってみよ


例外処理は要するにエラー処理です。古典的な C のプログラムでは(Java でもやってしまうこと有りますが)

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

int divid(int p, int q)
{
    if (q == 0)           // ゼロで割ったら大変...
        return INT_MIN;   // とりあえず、int の最小の数をエラーとすることに...
    return p / q;
}

int main(int argc, char** argv)
{
    int d = divid(atoi(argv[1]), atoi(argv[2]));
    if (d == INT_MIN) {
        printf("divide by zero\n");
    } else {
        printf("%d\n", d);
    }
}

リターン値でエラーを判断しようとするのは場合によっては不可能な場合があります。ここでは、q がゼロの時だけでなく、除算の結果として INT_MIN になることだって有り得ます。

仕方が無いので、エラーが起こったことを通知するようにします。

#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 への対抗というだけではなく、返り血(HRESULT) ベースのエラー処理システムでは、難易度ハードがやっとの連中が書いた不安定なアプリケーションが出回るおかげで、Windows 自体まで不安定だと攻撃されるのにうんざりしたからだと推測しています。


ここまで、Java ではなく C の話でしたが、これはなぜ例外という仕組みが必要かを理解する上での重要なものです。要するに、返り値でエラーを表すのはダメだと言うことです。となると、エラー情報を引き渡す、全く新たな仕組みが必要になります。それが例外処理で、そのために使う構文要素が trycatchthrow なのです。

まず、例外処理機構の全く無いコードでエラーを起こさせてみます。

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" が出力されていません。これはどういうことかと言えば、例外が起こると、そのメソッドの実行は即座に中断され、return 文が無いにも係わらず即座にリターンするということです。

これは、エラーが発生した所で例外(Exception)というオブジェクトが生成され、return とは違った別の経路で上がって(返って)いるのです。例外オブジェクトは、メソッドの呼び出しの階層を履歴を積み重ねて上がるのです

もうちょっと複雑なプログラムにしてみます。

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");
    }
}

標準入力からデータを読み込んで繰り返し除算をするようにしました。もし、大量のデータを読み込ませている間に変な入力をもらってエラーになった場合、プログラムではなく、データをデバッグしなければなりません。

このシンプルな例ではエラーの原因となった文字列が表示されているので原因を特定するのはさほど大変ではないですが、もっとデータが多かったり複雑だったりする場合は、出来たらデータの何処(何行目)にエラーの原因が有ったかを教えてもらいたいものです。また、エラーが起こっても処理(データの読み込み)を継続したい場合もあると思います。上がって来る例外が main() を付き抜けて Java インタープリタに捕まえられてプログラムの実行を停止させるのを阻止し、かつ、どこでエラーが起こったかを報告できるようにしなければなりません。

そういう場合は例外を捕獲(キャッチ)します。

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");
    }
}

これで、おかしな文字列を入力しても処理は止まることなく、かつ、エラーの原因も入手できます。


ここまでは、自分でエラーを見つけた場合ではなく、エラーが起こってしまってからの対処について書きました。それでは、自分がエラーを見つけた場合はどうしましょうか?

ちょっと、不自然な例ですが、pq が等しいのはデータの仕様上エラーだという事にします。そういう場合は自分の為の例外オブジェクト(クラス)を作って投げます。

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");
    }
}

自分用の例外クラスを作る際には、かならず Exception から継承されたクラスにしなければなりません。そうしないと、throw できません。Exception の直接の子供でもいいですし、場合によっては他の適切な例外クラスの子供にした方が良いかも知れません。


上の例で、メソッド定義の所に throws というのがくっついています。これは何かと言えば、そのメソッドが投げる可能性のある例外を宣言しているのです。

なんでこれが必要かといえば、例外をどこで捕まえるかはシステム全体の設計ポリシーで決まります。例外(エラー)が起こった処ですぐに対処(catch)しなければならないわけではありませんし、それだったら例外メカニズムは不要です。

catch されない例外はメソッドの実行を中断して外に飛び出し、呼び出し側にとびこみます。見方を変えると、メソッドの返り値みたいなものです。返り値があるなら、そのデータ型を定義しないと、そのメソッドを呼び出す側が対処できません。そのために、メソッドの宣言に、投げる可能性のある例外を全てリストするのです。

この解説では、ほとんどは Java にはあって C には無いものばかりでした。しかし、C には有って Java には無いものもあります。それがデータ領域の開放です。

Java と C の等価なコードの例で、C の方には領域を確保する関数がありましたし、Java も new で領域の確保をします。しかし、C の方には領域を開放する関数がありましたが、Java にはありません。これはなぜか?

C では malloc() でプログラムがメモリブロックを確保したあと、そのメモリブロックの管理はプログラムが自分で最後までやらなければなりません。なぜなら、ヒープ領域を管理しているランタイム・ライブラリ(libc.so) は、malloc() でプログラムにメモリブロックを提供したあとの管理はしてくれないからです。これは、管理が面倒だからです。なぜなら、malloc() が確保してくれるメモリブロックは、本当にただのメモリ領域でタグも rfID もなにも付いていません。だから管理しようがないのです。強いて言うと提供したことを台帳に付けているくらいです。それに、ランタイムライブラリ、プログラムが何時そのメモリブロックを使い終ったか分からないので、勝手に回収することもできません。プログラムは free() を呼んでメモリブロックをもう使わないことを宣言します。そうすると、ランタイムライブラリはメモリブロックのアドレスと管理台帳を照合して回収を確認します。

一方、Java で new を使って確保した領域は必ず、何らかのクラスのインスタンスです。だから、いろいろな情報(タグ)が付いています。また、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);
    }
}

proc1() のなかで、新たに確保された String 型のデータのアドレスは str に代入されます。proc1() から抜けると、str (ポインタ)は参照されなくなります。ポインタが使われなくなるのですから、ポインタが指しているデータ("Hello World")を誰も指さなくなってしまう可能性があります。もしそうなら Java インタープリタはそのデータの回収に入ろうとします。しかし良く見ると、return で返しています。なので、すぐには回収してしまうとまずいだろうということがわかります。

main() に返ったポインタを追跡すると、s に代入されています。ということはまだ、使われるかも知れないと言うことが分かります。そのあと、proc2() に渡されます。proc2() が終った後、もうだれも s を使おうとしません。だれも使わなくなったデータ("Hello World" が内容の String 型のデータ)は、ゴミ(Garbage)です。

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();
    }
}

ここで、proc() のなかで例外が発生したらどうしましょう。例外を捕まえないと、テンポラリファイルが消されないで残ってしまいます。これを対策するとしたら、

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();
    }
}

でとりあえず、例外を捕獲、拘束することで、テンポラリファイルを消す機会を確保できます。しかし、これでは、例外の処理(例外の内容をプリントするとか、応急処置をするとか)ができません。こういう場合、最終的にしたいことを必ずさせるの強いる仕組みが finally です

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();
        }
    }
}

こうすると、try ブロックを抜ける際、正常に抜ける場合も、例外で抜け出す場合でも、必ず、finally の中のコードを実行してくれます。このおかげでゴミファイルが残ったり、データベースへの接続が切れずに残ったりすることを防げて、プログラムの安全性が非常に高くなります。

所で、try/finallyの所をこんな風に書くとコンパイルエラーになります。理由がわかりますか?

    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();
        }
    }

変数はブロック({ ... })の外には見えないというを忘れないように。

当然ですが、finallycatch とも併用できます。

    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("うまく行きました!");
    }

※ 余談ですが、実際にこんな単純なテンポラリファイルの使い方の場合は、File.deleteOnExit() を使った方が簡単ですが。

C 言語において、初級プログラマの敵、英語以外を扱うプログラムの悩みの種、そしてセキュリティホールの最多要因である文字列の扱いが、Java では大幅に楽になっています(もっとも、メモリ管理機能をもつインタープリタ言語には共通の事ですが)。

C 言語では文字は char 型とされています...ちょっと待て!。たった今言った「文字」とは一般論としての文字ではなく ASCII コードとしての文字です。C 言語での char は文字ではなく「1バイト符号付き整数」です(だから unsigned 修飾子が付けられるわけです。符号無し文字っておかしくないですか?)。

CJK (Chinese, Japanese, Korean)な人たちにとって文字とは1バイトの整数で表せる255個で収まるものではありません。漢字辞典の厚さをみれば明らかですが、万の単位で表現しなければなりません。現時点では、Java を含むほとんどのインタープリタ言語、および、Windows 等のエンドユーザー向けオペレーティングシステムでは、1文字を2バイトの整数で表現する(対応づける) Unicode とよばれるコード化システムが使われています。では、文字は short で扱えばよいのかというとそうでもありません。なぜなら、コード化のルールの都合で 0xFFFF では収まらなくなってしまったのです。それに、日本だけでも6万以上の漢字がありますから、そもそも2バイトというのも十分ではなかったのです。そういう場合は拡張技法により1文字3バイト以上で表現します。つまり1文字は2バイト以上なのです。

さて、そういう文字が並んでいる文字列を扱うのは、考えただけで頭がおかしくなりそうで、通常のプログラマには手に負えません。よく、メモリ管理の観点から文字列を扱う際の C の難しさが言われますが、それは単純に char を文字として扱える英米の人達の話で、CJK な人にとってはむしろ文字コードの問題の方が遥かに面倒なものです。

※ C 言語向けに、国際化文字列を扱うライブラリパッケージはたくさん出回っていますので、それを使いこなせば C 言語でも CJK 対応のプログラムを書くことができます。もちろん、その場合はメモリ管理の問題を自力で解決するか、別途、メモリ管理用のライブラリを使う事になります。

ということで、そんな悪夢を緩和するために、インタープリタ言語では、文字は文字、文字列は文字列として扱えるようにいろいろな工夫をしてくれています。

Java では文字列は、クラス・ライブラリが提供するデータ型(クラス)ではなく、言語仕様の一部としての基本型として定義されています。これが string 型です。基本型ですので、演算子が適用できます(とは言っても + だけですが...演算子までオーバーライドができる C++ プログラマからみると Java の不備の一つ)。

また、基本型ということは配列でもありません。つまり、stringchar の配列ではありません。ですから、C 言語のように単純に [] でインデックスを指定して一文字を取り出したり、置き換えたりすることはできません。

ただ、string を、配列のように扱うこと([] で要素の取り出し)ができず、その上演算子が + しかないのでは悲惨なので、クラス・ライブラリの java.lang パッケージに String クラスが用意されています。Java コンパイラは、基本型の string と クラスの String を(ほぼ)同じものとして扱ってくれます。

Java のクラスライブラリの説明に、「文字列は定数です。この値を作成したあとに変更はできません。...。文字列オブジェクトは不変であるため...」と書かれています。これは C 言語上がりのプログラマにはなかなか理解できない表現です。

string とそれに関係するメソッドを C 言語で表すとしたら以下のような感じです(面倒なので、文字=char と単純化しているのに注意)。

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);
}

ポイントは、なにか操作をしたら必ず新たな string_t が作られているという事で、見方を変えると、元の string_t には手が加えられないという事です。だから不変(定数)ということなのです。

string がどんどん生み出され、捨てられて行くわけです。C プログラマからすればその資源浪費/環境破壊には呆れるところですが、Java インタープリタ(他のインタープリタでも)では、文字列専用のメモリバッファとメモリ管理メカニズムを用意して何とかしているそうです。もっとも、C プログラマと Java プログラマでは意識するメモリ量や時間の単位が全然違うので気にしてもしょうがないです。


次のプログラムを見てみましょう

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!

countInteger 型ですね。それに + 演算子で string リテラルを加算している。結果は文字列としてプリントされる。何だかものすごい違和感を感じます。

これは、string 型へのキャストが暗黙にかけられているのです。なぜなら、System.out.println() メソッドは引数として string 型を要求していますので、コンパイラはソースコードに明記されていなくてもキャストしようとします(この動きは C でも同じです)。

Java では文字列へのキャストが必要になると、そのオブジェクトの toString() メソッドを呼び出すようになっています。toString() メソッドは全てのクラスの先祖である Object クラス(java.lang パッケージ)で定義されています。つまり、全てのクラスには toString() メソッドが無償で付いている(継承されている)のです。

ただ、無償で付いている toString() メソッドはあまり当てになりません。何しろ原始人ですからプリミティブな事しか喋れません(クラス名とかハッシュ値)。ですので、素性を尋ねられたとき(stringにキャストされるとき)に自分が原始人だとは間違われたくないクラスは自前の toString() メソッドを用意して、先祖の素性に上塗りをします(「メソッドをオーバーライドする」と言います)。

Integer クラスには、自前の toString() メソッドがあり、普通に整数を表す文字列を作ってくれます。

コメントは C と同じ囲み形式(/* ... */ )と、C++ から導入された「// からは改行までコメントとなる」形式の両方が使えます。

なお、囲み形式の場合、囲みコメントは入れ子にはできないので注意してください。コメントつきのソースをコピペして、さらにそれをコメントで囲もうとしたときによくやります。

       /*  外側のコメント
        *   /* 内側のコメント */ 内側のコメント閉じは、外側のコメントの閉じと認識される。
        */ ×。これはコードの一部と認識され、コンパイルエラーになる。