
この頁では、Sun Microsystems社が開発したJavaというプログラミング言語に関して、主にWWWに関連する部分を中心に解説します。
Javaは、Sun Microsystems社が開発したプログラミング言語で、1995年に公開されて以来、インターネットの広がりとともに世界各地の様々な箇所で利用されています。
Javaが「インターネットの広がりとともに」普及した理由の1つとして、ネットワーク環境での利用を強く意識した設計がなされているということが挙げられます。
標準パッケージであるjava.netには、ソケットなどの豊富なネットワーク機能を含んでおり、さらにHTTPに限っていえば、URL, HttpURLConnectionの2つのクラスにより、ソケットすら意識せずにHTTP通信を行うこともできます。
ネットワーク対応という意味では、Javaは早くからXMLを扱うための機能も盛り込まれています。 XML自身もインターネット上でのデータ交換を意識して設計されており、インターネットとの親和性が高いものとなっています。 現在、HTTP + Java + XML という組み合わせのWebアプリケーションは、非常に多くの場面で利用されています。
他にも、Windows, Macintosh, Unix系などのコンピュータだけでなく、携帯電話や組み込み機器のような小型端末など、様々なプラットフォーム(動作環境)に対応していることが挙げられます。 これは、同一のコードを異なるプラットフォームに持ち込んでも同じ振る舞いをするということを意味します。
(※) このように、複数の動作環境に対応していることをクロスプラットフォームと言い、Sun Microsystems社ではこれを“Write Once, Run Anywhere”というフレーズで示しています。
プログラム上の特徴としては、Javaはインタプリタ方式を採用している点が挙げられます。 一般にプログラムの実行方式には、ハードウェア依存の機械語に変換して実行する「コンパイラ方式」と、ソースコードを一行一行解釈することでプログラムを実行する「インタプリタ方式」がありますが、インタプリタ方式はプラットフォーム毎に仮想マシンのインタプリタさえ用意しておけば、どんなプラットフォーム上でも実行可能となるため、より柔軟なシステムを実現できるという点で有利です。 インタプリタ方式は、一般に実行速度の点でコンパイラ方式より劣るため、初期のJavaは「実行速度が遅い」と言われ、敬遠されることもありましたが、現在ではインタプリタに様々な改良が加えられ、またマシンのCPUも日々向上しているので、実行速度も以前よりは上がってきています。
更に、プログラム上の大きな特徴として、Javaは「言語仕様中にマルチスレッドが組み込まれている」という特徴があります。 マルチスレッドを利用すると、同時に複数の処理を実行することができるので、たとえば「数値の計算のような時間が余りかからない処理」と「GUIや通信のような時間のかかる処理」を一つのプログラムの中で並行で行うことができます。
Javaで開発されるプログラムには様々な形態があります。
このように、JavaはWeb時代のプログラミング言語として、非常に多くの場面で利用されています。
Javaの開発環境をJDKと言い、http://java.sun.com/javase/ などから無料で入手する事ができます。 JDKには、以下のものが含まれます。
このうち、JREとは“Java Runtime Environment”、その名の通り「Javaを実行環境するための環境」の事で、この中には、Java VM や標準ライブラリと呼ばれる基本的なクラス、それにJava プラグインと呼ばれる、Web ブラウザ上でアプレットを動かすためのプラグインも含まれています。 したがって、「自分で作る事はないが、動かすことができればよい」と言うニーズに対しては、JRE があれば十分です。 JRE 単体でも、http://www.java.com/ などから無料で入手する事ができます。
Java の開発は、基本的に「テキストエディタ」、「コンパイラ (javac)」、「インタプリタ (java)」だけあれば可能です。
まずテキストエディタでソースコード (通常、拡張子は ".java") を書き、これを javac というコンパイラで、コンパイル(ソースコードをコンピュータ上で実行可能な形式に変換すること)します。
通常のプログラミング言語では、コンパイルを一度すれば直ちに実行可能なソフトウェアとなりますが、Java では動作プラットフォームに依存させないために、一度“.class”という拡張子を持つ Java バイトコードに変換します。
そして実行時に、java というソフトウェアによって、このバイトコードを実行するプラットフォームに対応したマシン語、すなわちネイティブコードに変換する事で実行します。
プラットフォーム間の違いは、実行時に Java 仮想マシンが吸収してしまうので、Java プログラマはプラットフォームの違いを意識しなくてすむのです。
プログラマとしては、コンパイラや Java 仮想マシンの詳細を理解する必要はありませんが、プログラミングした機能が「コンパイルの時点で実現している」のか「Java 仮想マシンによる実行中に実現している」のかを正確に把握していなければなりません。 そうでなければ、もしエラーが発生した場合にその原因を究明したい時、あるいは正常に動作しているプログラムでも更なるパフォーマンスの向上を望む時に、面倒な手間を増やす事になるからです。
このように、Java の開発は JDK に含まれるツールだけで可能ではあるのですが、一人で作る小規模なプログラムならともかく、企業の製品となるような大規模なプログラムを開発するような場合には、大量のファイルを管理したり、デバッグを効率よく進める必要が出てくるので、これだけでは不十分となります。 そこで、統合開発環境 (IDE) と呼ばれる、ソフトウェアツール群が利用されています。 IDE のほとんどは、GUIを持ち、基本的な操作は全てマウスなどから行う事ができるようになっており、開発効率を高めるものになっています。
Java の IDE には様々なものがありますが、このうち無償で利用できるものもいくつかあるので、以下に紹介します。
これらはフリーのソフトウェアであるため、無償で使える反面、世の中のフリーソフトウェア全般と同様に、基本的にその動作やサポートについては無保証なので、その使用には自分で責任を負わなければなりません。 但し、これらの IDE は既に世界中のたくさんの人に使用されており、関連の Web サイトや、関連書籍もたくさん存在していますので、それらを参考にすれば、きっと使えるようになるでしょう。
Javaアプレットは、Internet ExplorerやMozilla Firefox 等の Web ブラウザ上で動くように作られた Java プログラムで、HTMLにより Web ブラウザ内に埋め込まれる事で、画像や Macromedia Flash のように、インラインオブジェクトとして利用する事ができます。
アプレットの使用例として、HTTPステータスコードの説明句を検索するための“HttpStatusCodeFindlet”をご覧ください。(JREの中に同梱されていJavaプラグインが必要です。)
Java アプレットは、JRE によって用意された sandbox モデルの中に隔離されています。 (Java のセキュリティアーキテクチャとポリシーファイル参照)の中で アプレットによってユーザのコンピュータに被害を与える事がないように、ユーザのハードディスクの内容を読み書きしたり、自分が呼び出された Web サーバ以外のコンピュータに接続したり、他のアプリケーションソフトを起動したりするためには、ユーザの認証が必要になっています。 認証されていないアプレットでは、Java6 update10 以降の場合、最上位ウィンドウの右上隅に、「△内に!」のマークが表示され、そのアイコンの上にマウスポインタを置くと、「Java Applet Window」というメッセージがツールヒントとして表示されるようになっています。
Java アプレットが、ブラウザによってサーバ上からダウンロードされると、そのライフサイクルが始まります。 ライフサイクルに対応したクラスには、以下のようなものがあります。
init()start()init()メソッドが呼ばれた後に自動的に呼び出されます。一旦別のページにアクセスした後に元のページに戻ってきた場合、init()メソッドは実行されませんが、start()メソッドはその都度実行されます。stop()destroyこれを状態遷移図で表すと、以下のようになります。
また、Java アプレット内のメソッドから HTML 上に記述された JavaScript のメソッドを呼び出したり、その逆に JavaScript のメソッドからアプレット内の Java のメソッドを呼び出したりすることができます。 たとえば、Java から JavaScriptを呼び出す場合には、JRE に標準で含まれている netscape.javascript.JSObject オブジェクトを利用することで可能になります。
javadoc とは、Java ソースファイル内にある宣言及びコメントを解析して、そのコードが提供する API の HTML 文書を生成するためのものです。
既定では public クラス、protected クラス、入れ子にされたクラス、インタフェース、コンストラクタ、メソッド、及びフィールドについて説明する一連の HTML ページを生成します。
(※) API とは、あるプラットフォーム (OS やミドルウェア) 向けのソフトウェアを開発する際に使用できる命令や関数の集合
であり、普通は単にインタフェースと呼ばれます。
ところが、Java では inferface というキーワードが既に使用されているので、Java ではこの二つは特に区別されています。
通常、プログラム作成者は、プログラムとは別に、仕様書等と呼ばれる、プログラムの API ドキュメントを作成する必要があります。 仕様書は、第三者がプログラムを直接読まなくても、プログラムの機能を知る事ができるようにするためのものであり、プログラムが変更された場合は、直ちに仕様書も更新されなければなりません。 ところが、様々な理由によってこの作業が省かれると、その場合仕様書は参照するに値しないものとなってしまいます。 javadoc は、ドキュメントをソースコードから直接作成するため、このような問題を起こりにくくします。 プログラムが変更された場合、直ちに javadoc を利用すれば、簡単に仕様書を更新する事ができるからです。
javadoc は、コメント部分に記述されたタグと呼ばれる @ で始まる文字列を解析していきます。
タグには、メソッドの引数を示すための @param や、メソッドの戻り値を示すための @return 等の他に、作成者を記述するための @author や、関連項目を記述するための @see 等が用意されていますが、それ以外にも、カスタムタグと呼ばれる自分専用のタグを作成して使用する事もできます。
JDK には、javadoc というコマンドが含まれており、簡単に利用する事ができます。
以下は、"hoge.class" というクラスの API リファレンスを docs/ ディレクトリに収めるためのコマンドです。
>javadoc -d docs hoge.class
javadoc について、更に詳しい情報を手に入れたい場合は、Sun 社による Javadoc Tool を参照して下さい。
Javaは、マルチスレッドをサポートしています。 スレッド{thread} とは、本来「糸」という意味ですが、この場合「プログラムの処理単位」を指します。 すなわち、マルチスレッドとは「複数のプログラムを並行して処理することが可能」という意味になります。
Javaの場合、mainメソッドを実行しているスレッドをメインスレッドと呼び、メインスレッドだけを使ったプログラムを「シングルスレッドのプログラム」と呼びます。
シングルスレッドのプログラムとは、要するに「普通のプログラム」であり、mainメソッドの先頭から順に記述してあるコードを逐次的に処理していくプログラムです。
そのため、シングルスレッドのプログラムは逐次プログラムと言うこともあります。
これに対し、あるスレッド(mainスレッドを含む)から別のスレッドを作成し、それら複数のスレッドを“同時に”利用するプログラムを「マルチスレッドのプログラム」と呼びます。
マルチスレッドのプログラムは並行プログラムとも呼ばれ、“同時に”複数の処理を実行することができるので、たとえば「数値計算の処理」、「GUIの処理」、「通信の処理」……のように、機能ごとにスレッドを作成することができます。
これにより、プログラムの見通しが良くなったり、新たな機能を追加しやすいという意味で拡張性が高いという利点があります。
一方で、複数のスレッドから同時に参照されるリソース(プログラム上の変数だけでなく、ファイルや通信ソケットなどのI/Oなども含む)に関しては、プログラム上の矛盾が起きないように同期をとるなどの、並行作業を行うための処理が必要になります。
(※) 厳密に言うと、CPUが一つしかないマシンの場合、“同時に”処理を行うことはできません。 実際には、CPUの処理時間を非常に短い単位に分割し、複数のスレッドを順番に切り替えることによって、複数の処理を“同時に”行っているように見せています。
Javaでは、「Threadクラスを継承する方法」と「Runnableインターフェースを実装する方法」のどちらかでマルチスレッドプログラムを記述することができ、どちらでも同じ結果となります。
以下のプログラムは、上記の2種類の方法を用いて2種類のスレッドを作成し、それらを“同時に”処理させるプログラムです。
(ThreadTester.java)
01: package net.studyinghttp;
02:
03: public class ThreadTester {
04: // メイン関数
05: public static void main(String[] args) {
06: // インスタンスを生成
07: MyThread1 th1 = new MyThread1();
08: MyThread2 th2 = new MyThread2();
09:
10: // スレッドを開始
11: th1.startThread();
12: th2.startThread();
13: }
14:
15: }
16:
17: // Threadクラスを継承してスレッドを実現するクラス
18: class MyThread1 extends Thread {
19: // カウンタ
20: int cnt = 0;
21:
22: /**
23: * スレッド内の処理を記述
24: * @param なし
25: */
26: public void run() {
27: for(int i=0;i<10;i++) {
28: System.out.print (++cnt);
29: System.out.println(": This is MyThread1");
30:
31: // プリエンプト
32: try {
33: sleep(500);
34: } catch (Exception ex) {
35:
36: }
37: }
38: }
39: /**
40: * スレッドを開始するためのメソッド
41: * @param なし
42: */
43: public void startThread() {
44: this.start();
45: }
46:
47: }
48:
49: // Runnableインタフェースを実装してスレッドを実現するクラス
50: class MyThread2 implements Runnable {
51: // カウンタ
52: int cnt = 0;
53: // 継承しない場合はインスタンスを作っておく
54: Thread thread = new Thread(this);
55:
56: /**
57: * スレッド内の処理を記述
58: * @param なし
59: */
60: public void run() {
61: for(int i=0;i<10;i++) {
62: System.out.print (++cnt);
63: System.out.println(": This is MyThread2");
64:
65: // プリエンプト
66: try {
67: thread.sleep(500);
68: } catch (Exception ex) {
69:
70: }
71: }
72: }
73: /**
74: * スレッドを開始するためのメソッド
75: * @param なし
76: */
77: public void startThread() {
78: thread.start();
79: }
80: }
(※) スレッドの実装方法が2種類ある理由は、Javaでは多重継承ができない(既にあるクラスのサブクラスである場合は、他のクラスを継承できない)からです。
複数のスレッドを並行処理する場合の注意点は、「複数のスレッドがある共有データを別々に操作することで、その共有データが破壊されてしまう」、あるいは「ある順序に沿って呼び出すべきメソッドを複数のスレッドが別々のタイミングで呼び出すことで、プログラムが機能どおりに動作しない」といったなどがあります。
たとえば、以下のプログラムでは、2つのスレッドが共有データである gCounter を操作しようとして、プログラムが破壊される例です。
(ThreadTester2.java)
01: package net.studyinghttp;
02:
03: public class ThreadTester2 {
04: // メイン関数
05: public static void main(String[] args) {
06: // インスタンスを生成
07: MyThread1b th1 = new MyThread1b();
08: MyThread2b th2 = new MyThread2b();
09:
10: // スレッドを開始
11: th1.startThread();
12: th2.startThread();
13: }
14:
15: // グローバルカウンタ
16: private static int gCounter = 0;
17:
18: /**
19: * 数字と文字を出力する
20: * @param thName スレッド名
21: */
22: // synchronized // このメソッドにロックをかける
23: static void printOut (String thName, int lCnt) {
24: ++gCounter;
25:
26: for (int i=0;i<5000;i++) {
27: if (i%100 ==0) {
28: System.out.print ("-");
29: }
30: }
31: System.out.println("#");
32:
33: System.out.print ("ThreadName: "+ thName);
34: System.out.print (" ("+ lCnt +"times)");
35: System.out.println(", Total :"+ gCounter +" times");
36: }
37: }
38:
39: // Threadクラスを継承してスレッドを実現するクラス
40: class MyThread1b extends Thread {
41: // カウンタ
42: int lCnt = 0;
43:
44: /**
45: * スレッド内の処理を記述
46: * @param なし
47: */
48: public void run() {
49: for(lCnt=1; lCnt<=10; lCnt++) {
50: // プリントアウト
51: ThreadTester2.printOut("MyThread1", lCnt);
52:
53: // プリエンプト
54: yield();
55: }
56: }
57: /**
58: * スレッドを開始するためのメソッド
59: * @param なし
60: */
61: public void startThread() {
62: this.start();
63: }
64:
65: }
66:
67: // Runnableインタフェースを実装してスレッドを実現するクラス
68: class MyThread2b implements Runnable {
69: // カウンタ
70: int lCnt = 0;
71: // 継承しない場合はインスタンスを作っておく
72: Thread thread = new Thread(this);
73:
74: /**
75: * スレッド内の処理を記述
76: * @param なし
77: */
78: public void run() {
79: for(lCnt=1; lCnt<=10; lCnt++) {
80: // プリントアウト
81: ThreadTester2.printOut("MyThread2", lCnt);
82:
83: // プリエンプト
84: Thread.yield();
85:
86: }
87: }
88:
89: /**
90: * スレッドを開始するためのメソッド
91: * @param なし
92: */
93: public void startThread() {
94: thread.start();
95: }
96: }
このプログラムの実行例は以下のようになります。(実行結果は毎回変わります)
-------------------------------------------------------------------------------- -------------------# ThreadName: MyThread1 (1times), Total :2 times -# ThreadName: MyThread2 (1times), Total :2 times -------------------------------------------------------------------------------- ----# ----------------# ThreadName: MyThread1 (2times), Total :4 times ThreadName: MyThread2 (2times), Total :4 times -------------------------------------------------------------------------------- ----# ----------------# ThreadName: MyThread1 (3times), Total :6 times ThreadName: MyThread2 (3times), Total :6 times -------------------------------------------------------------------------------- -------------------# -# ThreadName: MyThread2 (4times), Total :8 times ThreadName: MyThread1 (4times), Total :8 times -------------------------------------------------------------------------------- -----# ---------------# ThreadName: MyThread1 (5times), Total :10 times ThreadName: MyThread2 (5times), Total :10 times ------------------------------------------------------------------# ThreadName: MyThread1----------------- (6times), Total :12 times -----------------# --------------ThreadName: MyThread2 (6times), Total :13 times ----------------------------------------------------------------------# ThreadName: MyThread1 (7times), Total :14 times ----------------# -----------------ThreadName: MyThread2 (7times), Total :15 times --------------------------------------------------# ThreadName: MyThread1 (8times), Total :16 times --------------------------------------------------# -----------------ThreadName: MyThread2 (8times), Total :17 times ----------------# -----------------ThreadName: MyThread1 (9times), Total :18 times --------------------------------------------------# -----------------ThreadName: MyThread2 (9times), Total :19 times ----------------# -----------------ThreadName: MyThread1 (10times), Total :20 times ---------------------------------# ThreadName: MyThread2 (10times), Total :20 times
複数スレッド実行時に、単一スレッドと同じ動きを保証するプログラムをスレッドセーフなプログラムと言います。 今回の場合、一番時間のかかる(=“重い”動作である)「標準出力への文字の書き出し」が間に入っているため、スレッド間の同期がうまく取れず、プログラムが意図どおりに動作しないという結果になってしまいました。
スレッドセーフを実現するためには、アトミック性を保証する必要があります。
アトミック性とは「不可分操作」とも言い、要するにあるスレッドがある処理を実行中に、他のスレッドがその処理を実行することができないという意味です。
今回のプログラムでは printOut() というメソッドは1まとまりで実行されないと表示が乱れてしまうので、この処理のアトミック性が確保されていなければなりません。
アトミック性を確保するための仕組みのには色々なものがありますが、「共有資源へのアクセスを一つのスレッドのみに限定する」という方法があり、これを相互排除(MUTEX)と言います。 相互排除は、しばしばロックをかけるとも呼ばれます。
Javaでは、メソッドかブロックに対してsynchronizedキーワードを利用することでMUTEXを実現できます。
上記のプログラムでは、22行目のコメントアウトを外すことで、以下のように意図どおりの動作をさせることができます。
--------------------------------------------------# ThreadName: MyThread1 (1times), Total :1 times --------------------------------------------------# ThreadName: MyThread2 (1times), Total :2 times --------------------------------------------------# ThreadName: MyThread1 (2times), Total :3 times --------------------------------------------------# ThreadName: MyThread2 (2times), Total :4 times --------------------------------------------------# ThreadName: MyThread1 (3times), Total :5 times --------------------------------------------------# ThreadName: MyThread2 (3times), Total :6 times --------------------------------------------------# ThreadName: MyThread1 (4times), Total :7 times --------------------------------------------------# ThreadName: MyThread2 (4times), Total :8 times --------------------------------------------------# ThreadName: MyThread1 (5times), Total :9 times --------------------------------------------------# ThreadName: MyThread2 (5times), Total :10 times --------------------------------------------------# ThreadName: MyThread1 (6times), Total :11 times --------------------------------------------------# ThreadName: MyThread2 (6times), Total :12 times --------------------------------------------------# ThreadName: MyThread1 (7times), Total :13 times --------------------------------------------------# ThreadName: MyThread2 (7times), Total :14 times --------------------------------------------------# ThreadName: MyThread1 (8times), Total :15 times --------------------------------------------------# ThreadName: MyThread2 (8times), Total :16 times --------------------------------------------------# ThreadName: MyThread1 (9times), Total :17 times --------------------------------------------------# ThreadName: MyThread2 (9times), Total :18 times --------------------------------------------------# ThreadName: MyThread1 (10times), Total :19 times --------------------------------------------------# ThreadName: MyThread2 (10times), Total :20 times
Javaでは、データの入出力をストリーム {Stream} という抽象的な概念で扱います。 例えば、ディスク上のファイルやネットワークを通じてデータをやり取りする場合でも、全てのデータは最終的に 0 か 1 のバイナリデータになります。 そこで、このデータの入出力のインターフェース部分を「ストリーム」として抽象化する事で、プログラマはそのデータが行き着く先の実際のデバイスを意識する事なく、データ処理ができるようになるのです。
より具体的な例を挙げましょう。 たとえば、今までローカルのディスク上に出力していたデータを、今度はネットワーク越しに出力したいとします。 この場合、以下のようなプログラムを作成すれば目的を達する事ができます。 (StreamTester.java)
01: package net.studyinghttp;
02:
03: import java.io.*;
04: import java.net.*;
05:
06: public class StreamTester {
07: public static void main(String args[]) {
08: Writer out;
09: String data = "テスト";
10: try {
11: // ファイルへの出力
12: out = outputToFileStream("data.dat");
13: writeData(out, data);
14: // ネットワーク越しに 127.0.0.1:30000 への出力
15: out = outputToNetworkStream("127.0.0.1", 30000);
16: writeData(out, data);
17: } catch (IOException e) {
18: e.printStackTrace();
19: }
20: }
21: static void writeData (Writer out, String data) throws IOException {
22: BufferedWriter bw = new BufferedWriter(out);
23: bw.write(data);
24: bw.close();
25: }
26:
27: static Writer outputToFileStream (String name) throws IOException {
28: return new FileWriter(name);
29: }
30: static Writer outputToNetworkStream (String host, int port) throws IOException {
31: Socket s = new Socket(host, port);
32: return new OutputStreamWriter(s.getOutputStream());
33: }
34: }
このコードのポイントは、13行目のファイルへの出力と16行目のネットワーク越しの出力が全く同じメソッドで利用できるという点にあります。 Javaのストリームを扱うクラスは、様々なラッパークラスが用意されているので、インスタンスをこのようにラップするすることによって、汎用性の高いプログラムを記述できるようになります。
その他に、ストリームに流し込むデータ単位を、バイト (byte) を単位としたバイトストリームから、文字 (char) を単位とした文字ストリームに変換するためのクラスや、バイトストリームを GZIP圧縮/展開できる GZIP[Input/Output]Streamなど、便利なストリームがたくさん用意されています。
さらに、HTTPに限っていえば、URL, HttpURLConnectionの2つのクラスによって、ソケットすら意識せずにHTTP通信を行うことができます。
詳しくは、HttpClientTester.javaを参照ください。
Java のパッケージ/クラスのうち、(故意/偶然にかかわらず) その利用の仕方によって、危険性を孕んでいるものに、以下のようなものがあります。 外部から取得した Java プログラムが、これらに含まれるクラス/メソッドを利用していたような場合、そのプログラムを実行したユーザは不利益を被る可能性があります。
private や protected といったメンバの保護指定を無視してオブジェクトの内容にアクセスできてしまう特権的な機能が存在するので、その使用には更なるセキュリティリスクが伴う。
getProperty() が用意されている。
システムプロパティとは、Java の System クラスが持っているプロパティのセットの事で、例えば以下のようなものが含まれる。
| キー | 対応する値 |
|---|---|
java.version |
JRE のバージョン |
java.home |
Java のインストール先ディレクトリ |
os.name |
OS 名 |
os.version |
OS のバージョン |
file.separator |
ファイル区切り文字 (UNIX では "/") |
path.separator |
パス区切り文字 (UNIX では ":") |
line.separator |
行区切り文字 (UNIX では "\n") |
user.name |
ユーザのアカウント名 |
user.home |
ユーザのホームディレクトリ |
user.dir |
ユーザの現在の作業ディレクトリ |
java.version を知られる事によって、そのバージョンに存在する既知のセキュリティホールを攻撃される可能性がある。
setProperty() も用意されている。
しかし、このメソッドの誤った使用により、上記のシステムプロパティが書き換えられ、プログラムが意図しない動作を引き起こす可能性がある。
特に、システムプロパティは、他のクラスで使用される事もあるので、できる限り書き換えるべきではない。
これらの危険なパッケージ/クラスは、ユーザによって認識されないうちに実行されるべきではありません。 そこで、Java では、これらの使用を制御するためにポリシーファイルと呼ばれる、セキュリティポリシーが記述されたファイルを、システム単位・ユーザ単位で用意しています。 Java プログラムを実行するシステム、あるいはそのユーザは、このファイルに必要なポリシーを適宜追加する事によって、プログラムの動作を細かく制御する事ができるようになるのです。
Java は、オブジェクト指向プログラミング言語です。 オブジェクト指向技術は、Simula と呼ばれるシミュレーション言語を起源とし、その後発表された Smalltalk-80 が、最初のオブジェクト指向プログラミング言語と言われています。 その後、この考え方が様々な言語に取り入れられる事になっていきますが、その代表的なものに C++ があります。 Java の言語仕様は、C++ を母体に設計されましたが、C++ よりも更に改善されたオブジェクト指向プログラミング言語となっています。
ところで、この「オブジェクト指向」というのは、具体的なコーディングテクニックではありません。 むしろ、コーディングの前の、ソフトウェアの設計の段階で有効な考え方であり、結果的にコード化の作業を有益にします。 この考え方は、プログラムの内容が大規模になればなるほど、より有効性を増すでしょう。
元々、コンピュータは非常に高価で、またその用途もエンジニアによってのみ扱われるような非常に限定的なものばかりでした。 しかし近年は、インターネットが一般へ開放され、また様々なソフトウェアが開発された事で、パーソナルコンピュータ (パソコン) も広く普及しました。 それに伴い、コンピュータを利用したシステムもどんどん利用されるようになって来ましたが、その反面、そのようなシステムはどんどん複雑化の一途を辿っています。 そのようなシステムを作る場合、開発は当然分担作業で行われる事になりますが、その際でも、全てのデータ状態を把握する事は不可能であるし、またそもそも意味がありません。 元来、プログラムとは逐次処理をするものだったのですが、現在利用されているシステムではそれよりもプログラムの体系化という要素の方がより重要になってきたのです。
つまり、「オブジェクト指向」とは人間がごくごく普通に認識している考え方、あるいは当たり前すぎて普段は認識すらしないであろう考え方を抽象化し、概念に名前をつける事で、体系化したものであり、言い換えると設計論をプログラミングに適用できるようにしたものが「オブジェクト指向」であるという事ができます。
オブジェクト指向の発想の原点は、現実の「物」および「物同士の関係」に注目するところにあります。 「物」は、それぞれ独特な「性質」を持っており、決まった「振る舞い」を行うようにできています。 すなわち、オブジェクト指向とは、システムの構造をこのように現実に近い形でモデル化する事により、その構造がより直感的でわかりやすくなるという考え方なのです。 オブジェクト指向では、「物」にあたるものをオブジェクトと呼んでいます。
この「オブジェクト指向」という抽象的な概念を、プログラムという具体的な形で表現するためにはどうすればよいのでしょうか? とりあえず、次のコードをご覧下さい。 (HelloTester.java)
01: package net.studyinghttp;
02:
03: class MyClass {
04: String callMe () { return "Hello!"; }
05: int randInt = (int)(Math.random() * 10);
06: }
07: class HelloTester {
08: public static void main(String args[]) {
09: MyClass obj = new MyClass();
10: System.out.println(obj.callMe());
11: System.out.println(obj.randInt);
12: }
13: }
まず、3 〜 6 行目で MyClass という名前のクラスを作成しています。
オブジェクト指向では、「ある概念をプログラム化したもの」をクラスと呼びます。
そして、クラス内の「ある振る舞い」をメソッド、「ある性質」をフィールドと言い、メソッドとフィールドを合わせて、そのクラスのメンバと言います。
つまり、この場合 MyClass というクラスには callMe というメソッドと、randInt というフィールドが存在している事になります。
次に、7 〜 13 行目では HelloTester という別の名前のクラスを作成しています。
この中には、main というメソッドがあり、全ての Java プログラムはこの main メソッドを実行する事で処理を実行します。
このあたりは、main()関数から処理が始まるC言語と同じ形式です。
9 行目では、obj という変数に HelloTester クラスを指し示すための「物」を格納しました。
この「クラスを指し示すための識別子」をオブジェクトと言います。
そして 10, 11 行目で、その obj というオブジェクトを通して、HelloTester 内に定義されたメソッド (4 行目) や、フィールド (5 行目) を呼び出しています。
(※) 9 行目で作られたオブジェクト obj を特にインスタンスと呼ぶ事があります。
このように、obj という「オブジェクト」を中心にプログラムを設計する手法が、オブジェクト指向プログラミング (OOP) です。
オブジェクト指向プログラミングでは、あるクラスのメソッドやフィールドは、そのインスタンスを介してのみ扱う事ができ、他のオブジェクトから直接利用できないように隔離する事ができます。 これをカプセル化と言います。
カプセル化を行う事によって、外部からデータを書き換えるような事ができなくなります。 外部からデータを書き換えられるようになっていた場合、例えば同じ変数名を作ってしまうと、他人がそのデータを上書きしてしまい、結果的にクラスが想定した動作を行わなくなるかもしれません。 カプセル化は、そのような余計な心配から、プログラマを解放します。
また、データの構造を隠蔽する事により、内部的なデータ構造を独自に変更したとしても、外部とのインタフェースであるメソッドさえ変更しなければ、そのクラスを使うユーザはその変更をまったく考慮する必要がありません。 この独立性により、クラスのメンテナンス性を向上させ、またそのクラスの再利用性を高めます。
オブジェクト指向プログラミングでは、あるクラスを定義する時に、他のクラスのメンバを受け継ぐ事ができます。 これを継承と言います。 この時、元になるクラスをスーパークラス (基底クラス)、受け継ぐ方のクラスをサブクラス (派生クラス) と言います。 スーパークラスの性質は全てサブクラスに受け継がれますので、サブクラスではスーパークラスとの違いを定義するだけでよく、振る舞いは似ているがわずかに違うようなソースコードを繰り返し記述する事を避ける事ができ、プログラムの開発効率が高まります。
また、継承される事が前提となるようなクラスを作る事ができます。
そのようなクラスを抽象クラスと言います。
抽象クラスでは、abstract というキーワードを用いる事で、クラスに属するメソッドの一部 (または全部) の名前のみを宣言しておく事ができます。
それらメソッドは、抽象クラスをスーパークラスとするサブクラスで個別に定義する事で、使用できるようになります。
例として、以下のコードをご覧下さい。
(InheritanceTester.java)
01: package net.studyinghttp;
02:
03: class InheritanceTester {
04: public static void main(String args[]) {
05: Fruit apple = new Apple();
06: System.out.println(apple.color());
07: System.out.println(apple.taste());
08:
09: Fruit banana = new Banana();
10: System.out.println(banana.color());
11: System.out.println(banana.taste());
12:
13: Fruit melon = new Melon();
14: System.out.println(melon.color());
15: System.out.println(melon.taste());
16: }
17: }
18:
19: abstract class Fruit {
20: abstract String color();
21: String taste () {
22: return "Sweet";
23: }
24: }
25:
26: class Apple extends Fruit {
27: String color() {
28: return "Red";
29: }
30: }
31: class Banana extends Fruit {
32: String color() {
33: return "Yellow";
34: }
35: }
36: class Melon extends Fruit {
37: String color() {
38: return "Green";
39: }
40: }
このプログラムで言えば、スーパークラスは Fruit、サブクラスは Apple, Banana, Melon となります。
ここで注目すべき点は、Fruit というクラス名と、color() というメソッド名に、それぞれ abstract というキーワードが使用されている事 (19, 20 行目) です。
これによって、color() というメソッドは、これを継承するサブクラスで個別に定義される事になります。
また、これ以外に taste() というメソッド (21 〜 23 行目) を持っており、これは各サブクラスで共通するメソッドとして使用する事ができます。
オブジェクト指向プログラミングでは、ある名前のメソッドやフィールドに複数の機能を持たせる事ができます。 これを多態性 (ポリモーフィズム) と言います。 多態性によって、プログラマはより理解しやすい (人間にとって読みやすい) プログラムを書く事ができるようになります。 例えば、次のコードをご覧下さい。 (InheritanceTester2.java)
01: package net.studyinghttp;
02:
03: class InheritanceTester2 {
04: public static void main(String args[]) {
05: Fruit apple = new Apple();
06: System.out.println(apple.color());
07: System.out.println(apple.color("codling"));
08: System.out.println(apple.taste());
09: }
10: }
11:
12: class Apple extends Fruit {
13: String color (String type) {
14: if (type.equals("codling")) {
15: return "Green";
16: } else {
17: return "Red";
18: }
19: }
20: String color () {
21: return color("normal");
22: }
23:
24: String taste () {
25: return "Sour";
26: }
27: }
28:
29: abstract class Fruit {
30: abstract String color();
31: abstract String color(String type);
32: String taste () {
33: return "Sweet";
34: }
35: }
Java では、多態性 (ポリモーフィズム) を実現するために、二種類の方法が提供されています。
一つはオーバーロード {overload} で、これは一つのクラスでメソッドを多重定義する事です。
複数定義されたメソッドのうち、実際にどれを実行するかは、引数の数や種類で区別されます。
上の例で言うと、color() がこれにあたり、「引数なし」と「引数が String 型」のものとか区別されています。
しかし実際には、21 行目より、「引数なし」の color() は「引数が String 型」のものを呼び出しています。
このように、ほとんど同じような処理を行うメソッドを、よりシンプルに提供できる仕組みとして、オーバーロードは利用されています。
もう一つはオーバーライド {override} で、これはスーパークラスで定義されたメソッドをサブクラスで再定義する事を言います。
上の例で言うと、taste() がこれにあたり、8 行目の結果は、スーパークラスで定義された値 (33 行目) ではなく、サブクラスで再定義された値 (25 行目) のものになります。
この仕組みにより、いちいち全てのクラスで値を定義する必要がなくなります。
従って、例えば「ほとんどのクラス (Banana や Melon 等) では "Sweet" なのだが、Apple でのみ "Sour" を使いたい」という場合に便利です。
プログラマにとって、メソッドや変数の型についての考慮を減らす事ができる事は大変有用です。 この点からも、オブジェクト指向プログラミングが、複数人による、大規模なプログラミングに有用であるという事が言えるでしょう。
(※) ちなみに、このような仕組みは、たとえばC言語では提供されていないので、関数名(Javaにおける“メソッド”)は異なるものにしなければなりません。
ここでは、Java の誕生から現在までの進化について、主に WWW に関連する部分を中心に記述します。
Sun Microsystems 社は、1991 年に家電機器用のプログラミング言語の開発を目的とする Green プロジェクトを立ち上げました。 家電機器は、メモリやマシンパワーが小さく、また異なる製造メーカーのマイクロコンピュータに対応する必要があるので、家電用のコンパクトなプログラミング言語を開発する必要があったのです。 Green プロジェクトは、この目的に合うプログラミング言語を開発し、それにOakという名前を付けました。
Oak は、しばらくはあまり注目されない存在でしたが、Kim Polese という女性がプロジェクトマネージャに就任したことにより、状況は大きく変わります。 1994 年、Mosaicという Web ブラウザが世に出た事により、Web は爆発的に広まり、Kim Polese はインターネットこそ Oak の活躍の舞台だと確信しました。
1995 年、Sun Microsystems 社は Oak 言語を“Java”と改名し、Java だけで実装した Web ブラウザである HotJava を発表しました。 HotJava では、HTML に埋め込まれて Web ブラウザ上で動かすように作られたJava アプレットが実行でき、クライアント側にダイナミックな動きを加える事ができました。 中でも Java アプレットが Web に与えた衝撃は大きく、Netscape Navigator 2.0 をはじめ、他の Web ブラウザも続々とアプレットのサポートを表明しました。
Java と HotJava は、発表されるや否や大評判になりました。 Sun はすぐさま Java コードを開発するためのキットである JDK について、α版(内部評価版)、β版(公開テスト版)と立て続けにリリースし、そしてついに正式版である JDK 1.0 を 1996 年 1 月にリリースしました。 これを機に、Java の存在はインターネットと共に世界中に広まっていったのです。
1997 年、Sun は JDK 1.1 をリリースしました。 大きな変更点としては、以下が挙げられます。
Java 1.0 では、日本を始めとして、国際化サポートはされていませんでした。 これに対し、Java 1.1 では、Unicode 文字や地域ごとのタイムゾーン等をサポートする事により、英語圏以外での使用も可能になりました。
Java では、“.class ファイル”や、画像ファイルなどの複数のファイルをアーカイブするためのファイル形式として、JAR というものを作りました。 アーカイブとは「複数のファイルを一つのファイルに統合する」という意味ですが、これにより必要なファイル群のダウンロードは一度で済むようになり、TCP 接続数を節約する事ができます。
たとえば、以下は“hoge.jar”というファイルを作成し、その中にこのディレクトリ内の全ての .class ファイルと、img/ ディレクトリにある全てのファイル (ディレクトリは再帰的に探査される) を含めるためのコマンドです。
>jar cvf hoge.jar *.class img/*
また、JAR ファイルの展開も jar で行う事ができます。
>jar xvf hoge.jar
(※) アーカイブを行うためのツールをアーカイバと言いますが、UNIX で古くから使用されているアーカイバ用コマンドに tar というものがあり、jar コマンドはそれに似せて作られています。
デジタル署名とは、Java アプリケーション(アプレット)を公開した人を保証するための仕組みです。
JDK 1.1 では javakey というツールが公開されました。
上記のような大きな変更の他にも、Java 1.1 では Java 1.0 から大きく言語仕様が変わり、非常に多くのメソッドが非推奨となりました。 しかし、次バージョンである Java 1.2 では、更に大きく仕様が変化する事になるのでした。
1998 年、Sun は次バージョンをリリースしました。 このバージョンは、これまでのバージョンと比べ、あまりにも大幅な機能面の拡張があったことを理由に、「それまでの Java とは区別される、新世代の Java」という意味を込め、Java 2 と名づけられました。 また、製品の規模に合わせて、開発環境が 3 種類提供されることになりました。
J2SDK 1.2 の大きな変更点としては、以下が挙げられます。
Java では GUI を実現するための基本的なクラスライブラリとして、AWT というツールキットを Java 1.0 から提供していました。 AWT には、テキストエリアやボタン等の部品 {Components} が含まれており、これらを組み合わせる事で GUI を実現する事ができるようになっているのですが、実際に AWT を使った人からは、「思い通りの位置にボタン等の部品を配置する事が難しい」とか「GUI 部品の作成に実行環境のウィンドウシステムのコードを利用しているので、異なる OSやブラウザ間で見た時に、見た目が変わってしまうのが気に入らない」といった不満の声が出てきました。
(※) GUI とは、状態を視覚的に表現し、多くの操作をマウス等のポインティングデバイスによって行なう事ができるユーザインターフェースの事です。 これに対して、情報の表示を文字によって行い、全ての操作をキーボードを用いて行なうユーザインターフェースを CUI と言います。
そこで、これらの問題を解消するために Swing という新たなクラスライブラリが提供されました。 Swing は元々 JFC (Java 言語を使って本格的な業務用ネットワークアプリケーションを開発する際に有用なクラスライブラリ) の一部として提供されており、 Java 2 以降で標準クラスパッケージに統合されたものです。 Swing は基本的には Java のみで書かれており、互換性や実行速度の向上、あるいはHTMLへの対応といった改良が施されています。
ここでいうプラグインとは「Webブラウザの機能を拡張するために追加する個別プログラム」を指し、たとえばAdobe Flash Playerなどが有名です。 Javaプラグインという仕組みが無い時代は、Javaアプレットを実行するためのJava実行環境はブラウザに同梱されたものしかなかったため、Javaに起因する実行速度やセキュリティを改善するためにはブラウザごとアップデートするしかありませんでした。 しかし、プラグイン形式が採用されたことにより、ユーザが最新のJava実行環境を手に入れたい場合はプラグインのみをアップデートすればよいようになりました。 現在、最新のJavaプラグインはJREに含まれています。
Java では、セキュリティアーキテクチャと呼ばれる、プログラムの安全性を確保するためのsandbox(砂場)モデルというものが導入されています。 これは、「外部からダウンロードしたプログラム (Java アプレット) は安全ではないとみなし、sandbox(砂場)の中の限られたリソースにのみアクセス可能とし、その外へアクセスすることは禁止する」というものです。 しかし、このモデルでは、安全であることがわかっているプログラムに読み書きを許可したり、あるいはユーザが意図しないうちにインストールさせられたプログラムが外部へ通信を行うといったことを禁止することはできません。
そこで、上記の問題を解決するために、Java 2 の sandbox モデルを「全てのJavaプログラムはセキュリティポリシーによって制御される」と改良しました。 セキュリティポリシーには、システムリソースへのアクセスを表すアクセス権 (パーミッション)が記されており、それに基づいて Java プログラムがファイルの読み書きや通信を行うためのアクセス制御を行っています。
具体的にどのようなクラスやパッケージが制御されるかについてはJava のセキュリティアーキテクチャとポリシーファイルをご覧ください。
2000 年、Sunは次バージョンのJavaをリリースしました。 J2SDK 1.3 の Web に関する変更点のうち主なものとして、以下が挙げられます。
jarsigner
J2SDK 1.3 では、これまでの JAR 署名用ツールであるjavakeyを完全に置き換えるjarsignerというツールが公開されました。
署名を行うには、keytool というツールであらかじめ鍵を生成しておいてから、その鍵と合わせて使用します。
>keytool -genkey -keyalg rsa -alias hHash -storepass myPwrd -validity 5475 >jarsigner -storepass myPwrd hoge.jar hHash
より詳しい使い方は jarsigner コマンドのヘルプをご覧下さい。