[雑談] 試験の役に立たないJava講座 (1)
ということでインプレス社の「徹底攻略Java SE 11 Silver問題集」の章立てに沿って、少し雑談していきたいと思います。
問題集の内容に沿ったものではなく抜けているところも多々ありますので、認定資格を取得しようというのであれば問題集を購入して、そちらをしっかり勉強してください。
こちらは章立てに沿って適当に書き散らかしているものですので、試験の役には立ちません。
ということで第1章から始めます。
第1章ではパッケージ、クラスのインポート、mainメソッド、javaコマンドを扱っています。
Javaの基本
WindowsアプリケーションやmacOSアプリケーションはWindowsやmacOSによってメモリに読み込まれ、CPUで直接実行されます。
これに対して、JavaプログラムはJava実行環境がJavaプログラムを読み込んで、Java実行環境に含まれているJava仮想マシンというソフトウエアが実行します。
プログラムを実行するといった場合、大きく2種類の実行方法があります。
一つはコンパイラを使用してプログラムのソースコードをOSが読み込んでCPUが直接実行するプログラムファイルに変換 (コンパイル) する方法です。
この方法はコンパイルというひと手間が発生しますが、一度コンパイルするとその後はCPUが直接実行できるようになるので、プログラムを高速に実行することできます。
Windowsアプリケーションの多くはこのような形態をとっているます。
もう一つはプログラムのソースコードを読み込んで、逐次処理していくインタプリタというソフトウエアを使用する方法です。
文字列として記録されたソースコードを読み込んで解釈し、実行を行うので実行速度は望めませんが、インタプリタを用意すればCPUやOSにかかわらず実行できるという利点があります。
Javaが開発された1980年代、WindowsやMacintoshなどのパーソナルコンピュータは技術的な制約が多々あり、ソフトウエアとしてできることが限られていたため、ソフトウエア開発や業務の世界ではUnixをはじめとする様々なOSが使用され、コンピュータメーカー各社が独自のCPUを開発するなど多種のCPUが使用されていました。
また、ネットワークの普及もあって、クライアントコンピュータとサーバとの間で機能分担、負荷分散を行う分散コンピューティングが求められるようになってきました。
こういった様々なOSやCPU (合わせてプラットフォームと呼んでいます) がある中で分散コンピューティングを成り立たせるにはプラットフォームに依存しないアプリケーションが必要でしたが、当時のプラットフォームの能力では実用的なソフトウエアシステムをインタプリタで実現することは困難でした。
そのような状況の中で考え出されたのが、さまざまなCPUの命令に容易に変換できるような命令を持った仮想CPUの仕様を決めて、仮想CPUの命令としてプログラムのソースコードをコンパイルし、ぞれぞれのCPUに対応した仮想CPUから実際のCPUの命令に変換するソフトウエア (仮想マシン) を用意するという方法です。
仮想マシンの考え方自体はUCSD p-SystemなどJava以前から存在していたものですが、マルチプラットフォームでの分散コンピューティングという80年代の時代的要請の中、Javaが導入したことで広く用いられるようになりました。
そしてWebが普及していくとWebブラウザを通して分散処理を行う手段、例えばクライアント側でのユーザ入力の妥当性検査や表示の並べ替え、書式変更などを行う手段としてJavaを使用するAppletが導入され、主に業務系Webアプリケーションとして使用されていました。
今はCPU性能が高くなり、インタプリタでも相応の性能が望めるようになったため、このような用途ではソースコードをそのまま流通させるJavaScriptが主流となっています。
JavaプログラムはCPUで直接実行することはできないため、まずjavaコマンドでJava実行環境を実行させ、Java実行環境にJavaプログラムを読み込ませることでJavaプログラムを実行する形式となっています。
Javaプログラムの実行ファイル
話はまたWindowsから始めます。
Windowsのアプリケーション (もちろん、macOSなどでも構いませんが) を見ると、ボタンやプルダウンメニュー、プリントダイアログなどがどのアプリケーションでも同じデザインになっていることに気付くかと思います。
プリントダイアログは新しいプリンタドライバをインストールすると新しいプリンタドライバ用のプリントダイアログが表示されるようになります。
これはボタンやプルダウンメニュー、プリントダイアログなどのプログラム要素がアプリケーションに埋め込まれているのではなく、共有ライブラリ (Windowsではダイナミックリンクライブラリと呼んでいます) として実装され、アプリケーションの実行時にそれらを使用しているからです。
このように共有ライブラリを使用するとそれぞれのプログラム機能をアプリケーションに埋め込まなくても済むため、アプリケーションプログラムのサイズと実行時メモリサイズを削減することができるとともにプログラム要素に不具合があった場合にはそのプログラム要素を修正するだけでアプリケーションソフトウエアを変更、コンパイルなどすることなく修正を反映することができます。
Javaではこのようなプログラム要素をクラスとして実装し、クラス単位でプログラムファイルとすることでプログラム要素の共有を実現しています。
クラスについては後程他の章で詳しく触れることがあると思いますので、ここではプログラム要素を実装する単位とだけ考えてください。
Javaプログラムを実行する際にはJava実行環境に読み込むべきJavaプログラムファイルを指定する必要があるため、javaコマンドの最初の引数にはクラス名を指定します。
javaコマンドはJava実行環境に最初に読み込んで実行するJavaプログラムとして、この引数で渡されたクラス名を渡すことでJavaプログラムが実行できるようになります。
Javaプログラムのエントリポイント
先ほどjavaコマンドの引数で指定されたクラス名を持つJavaプログラムファイルを読み込んで実行すると言いましたが、クラスのどこから実行を始めるのかが次の話題です。
では、Javaプログラムの例を見てみましょう。
class Hello1 {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
毎度おなじみのHelloWorldプログラムです。
Java実行環境は指定されたクラス名 (上の例ではclass HelloのHelloになります) のクラスが記録されたJavaプログラムファイル (ファイル名がHello.classのようにクラス名と同じ名前になっています) を読み出し、mainメソッドを実行します。
ここでmainメソッドについているpublicはJava実行環境から見えるようにと付けるもので、staticはJava実行環境が実行できるようにと付けるもの、voidはmainが値を返さない (そのうち説明します) ことを知らせるためにつけるもので、それぞれ必須となります。
Javaプログラムのコンパイルと実行
このプログラムをコンパイルしてjava Helloで実行するとHello, World!が表示されます。
では、実際にコンパイルして実行してみましょう。
まず、Javaプログラムをコンパイル、実行するにはJava開発キット (JDK) が必要になります。
オープンライセンスのJDKであるOpenJDKがいくつかの企業、団体によって配布されているのでライセンス条項を参照したうえで適当と思われるものをダウンロードし、展開してください。
今回私はOracle OpenJDKバージョン19 (19.0.2) をダウンロードしてきたのでCドライブのルートフォルダ直下にopenjdkフォルダを作成し、その中にダウンロードしたZIPファイルの内容を展開しました。
次に環境変数を設定します。
Windows 11での環境変数の設定はスタートメニューから「設定」(歯車アイコン) を選択して「設定」ダイアログを開き、「システム」の「バージョン情報」をクリックします。
「システム > バージョン情報」に「関連リンク」として「システムの詳細設定」のリンクがありますので、クリックして「システムのプロパティ」ダイアログを開いてください。
「システムのプロパティ」ダイアログの「詳細設定」タブが開かれていると思いますので、その中にある「環境変数 (N)...」ボタンをクリックして「環境変数」ダイアログを表示します。
「環境変数」ダイアログの下段「システム環境変数」で「新規 (W)...」ボタンをクリックして「新しいシステム変数」ダイアログを表示し、「変数名(N)」に「JAVA_HOME」、「変数値(V)」にJDKを展開したフォルダパス、先ほど書いた私の例では「C:\openjdk\jdk-19.0.2」を入力します。
フォルダパスを入力する際には入力したいフォルダをエクスプローラで開いたのちにアドレスバーでパスを選択してコピーして、「変数値(V)」の入力欄にペーストするのが間違いがなくていいと思います。
次にPath変数を修正します。
「システム環境変数」の一覧で「Path」の項目をクリックして「編集(I)...」ボタンをクリックして「環境変数名の編集」ダイアログを表示します。
「環境変数名の編集」ダイアログで「新規(N)」ボタンをクリックして新規入力欄を表示させ、「%JAVA_HOME%\bin」と入力し、「OK」ボタンをクリックして「環境変数名の編集」ダイアログを閉じてください。
続いて「環境変数」ダイアログで「OK」ボタンをクリックして「環境変数」ダイアログを閉じてください。
ここまで出来たら一度コマンドプロンプトを起動して、java -versionを実行してください。
正しくインストールされていれば以下のようにJDKバージョンが表示されるはずです。
C:\Users\tom_a>java -version
openjdk version "19.0.2" 2023-01-17
OpenJDK Runtime Environment (build 19.0.2+7-44)
OpenJDK 64-Bit Server VM (build 19.0.2+7-44, mixed mode, sharing)
それでは先程のプログラムをメモ帳などで入力してHello1.javaという名前で適当なフォルダに保存してください。
ここではドキュメントフォルダの下にjavastudyというフォルダを作ってファイルを保存しました。
C:\Users\tom_a\Documents\javastudy>dir
ドライブ C のボリューム ラベルは Windows-SSD です
ボリューム シリアル番号は 8807-E74C です
C:\Users\tom_a\Documents\javastudy のディレクトリ
2023/01/20 21:11 <DIR> .2023/01/20 21:07 <DIR> ..
2023/01/19 23:43 112 Hello1.java
1 個のファイル 112 バイト
2 個のディレクトリ 26,016,333,824 バイトの空き領域
javac Hello1.javaを実行してコンパイルします。
C:\Users\tom_a\Documents\javastudy>javac Hello1.java
C:\Users\tom_a\Documents\javastudy>dir
ドライブ C のボリューム ラベルは Windows-SSD です
ボリューム シリアル番号は 8807-E74C です
C:\Users\tom_a\Documents\javastudy のディレクトリ2023/01/20 21:12 <DIR> .
2023/01/20 21:07 <DIR> ..
2023/01/20 21:12 419 Hello1.class
2023/01/19 23:43 112 Hello1.java
2 個のファイル 531 バイト
2 個のディレクトリ 26,004,340,736 バイトの空き領域
プログラムに問題はないので、javacコマンドは何も表示せずに終了しました。
dirコマンドでフォルダの内容を見てみると、Hello1.classファイルができていることがわかると思います。
これがコンパイルされたJavaプログラムファイルになります。
ではjavaコマンドを使ってこのプログラムを実行してみましょう。
C:\Users\tom_a\Documents\javastudy>java Hello1
Hello, World!
上のように正しく実行されることが確認できました。
プログラムに渡す引数
さて、次に誰かの名前で呼びかけるようにプログラムを変更してみましょう。
class Hello2 {
public static void main(String[] args) {
System.out.println("Hello, " + args[0] + "!");
}
}
詳細は吹っ飛ばしますが、このプログラムをコンパイルしてjava Hello tomと実行するとjavaコマンドの2番目の引数tomをargs[0]で読み出すことができるので、Hello, tom!と表示できるようになります。
とりあえずここではmain(String[] args)のargsにはjavaコマンドの2番目以降の引数が記録されるということ、そして引数の有無にかかわらずこのように書いておく必要がある (argsの部分は別の名前でも構いません) ということを覚えておいてください。
この辺の詳細は後程クラスとメソッドについてで詳しく触れる予定です。
クラス名についてもう少し
突然話は変わりますが、レストランの管理システムを作ってくださいという依頼があったとします。
先ほどちょっと触れたようにJavaプログラムではプログラム要素を一つずつクラスとして実装していきます。
例えばレストランシステムであれば食材を保存している冷蔵庫、食材を調理する調理場、料理をお客に出すテーブルなどがプログラム要素になると思います。
それとは別に売り上げや仕入れ費用を管理する表もプログラム要素として必要でしょう。
これをプログラムとして実装する場合、テーブル用にTableクラスを用意して、表のためにTableクラスを用意してと同じ名前のクラスができてしまうことがあります。
このくらいのプログラムであればクラス名を適当に調整する必要があります。
例えば、食事をするためのテーブルであればクラス名の先頭にDiningを付けてDiningTableといったクラス名に、取引を管理する表であればクラス名の先頭にTransactionを付けてTransactionTableというクラス名にすることでクラス名の重複を避けることができます。
クラスを業務範囲や対象などで分類し、それぞれの分類に対応した文字列をクラス名の先頭につけることでクラス名の管理もしやすくなると思います。
しかし、このやり方だとクラス名が長くなるため、プログラムが読みにくくなるといった問題が発生します。
特に大規模なプログラムになると何段階かの階層にわけて分類したくなるわけですが、そういった場合にはクラス名の先頭につく文字列が非常に長くなってきます。
こういった問題を避けるため、Javaではパッケージという仕組みを用意しています。
以前に作成したプログラムをパッケージを使って書くと以下のようになります。
package hello;
class Hello3 {
public static void main(String[] args) {
System.out.println("Hello, World!");
}}
ファイルの先頭にpackage hello;と書くことで、このクラスがhelloパッケージに属するクラスであるということ宣言します。
このプログラムを実行する際にはjava hello.Hello3のようにパッケージ名とクラス名の間を「.」でつないだ名前でアクセスします。
このようにパッケージを使用するとパッケージ名とクラス名とを組み合わせた名前が正式なクラス名となるため、クラス名の衝突を避けることができるようになります。
パッケージとインポート
ところで、たぶん気付かれているかとは思いますが、これでは長いクラス名を付けるのとあまり変わりはありません。
そこでパッケージにはいくつかの工夫が入れられています。
まず、同じパッケージ内のクラスを参照する場合はパッケージ名をつける必要がないという点です。
ソフトウエアを作成する際に関連性の強いクラス群を1個のパッケージにまとめるとクラス参照に長い名前を使う必要がなくなります。
同じ業務範囲や対象を扱うクラス群は多くの場合関連性が高くなるのでこの特徴は有用だと思います。
もう一つはインポート機能です。
他のパッケージにあるクラスを頻繁に参照する必要がある場合、例えばユーティリティ的な機能をまとめたクラスを使用する場合などではパッケージ宣言の直後にimport java.util.List;と記述することでListクラス (これについては後程出てきます) がListという名前で使えるようになります。
ほかにもパッケージには機能があるのですが、それは後程の章で説明されますので、ここでは同一パッケージ内のクラスとインポートしたクラスは短いクラス名で、それ以外のクラスはパッケージ名を含む長いクラス名で参照できるという点だけを覚えてください。
パッケージが指定されたプログラムのコンパイルと実行
では、先程のHello3.javaをコンパイルしてみましょう。
C:\Users\tom_a\Documents\javastudy>dir
ドライブ C のボリューム ラベルは Windows-SSD です
ボリューム シリアル番号は 8807-E74C です
C:\Users\tom_a\Documents\javastudy のディレクトリ
2023/01/20 21:46 <DIR> .2023/01/20 21:07 <DIR> ..
2023/01/20 21:12 419 Hello1.class
2023/01/19 23:43 112 Hello1.java
2023/01/20 21:46 861 Hello2.class
2023/01/19 23:46 122 Hello2.java
2023/01/19 23:48 128 Hello3.java
5 個のファイル 1,642 バイト
2 個のディレクトリ 26,079,563,776 バイトの空き領域
コンパイル前のフォルダ内容は上記のようになっています。
C:\Users\tom_a\Documents\javastudy>javac Hello3.java
C:\Users\tom_a\Documents\javastudy>dir
ドライブ C のボリューム ラベルは Windows-SSD です
ボリューム シリアル番号は 8807-E74C です
C:\Users\tom_a\Documents\javastudy のディレクトリ2023/01/20 21:48 <DIR> .
2023/01/20 21:07 <DIR> ..
2023/01/20 21:12 419 Hello1.class
2023/01/19 23:43 112 Hello1.java
2023/01/20 21:46 861 Hello2.class
2023/01/19 23:46 122 Hello2.java
2023/01/20 21:48 425 Hello3.class
2023/01/19 23:48 128 Hello3.java
6 個のファイル 2,067 バイト
2 個のディレクトリ 26,060,812,288 バイトの空き領域
コンパイルは正常に終了し、Hello3.classができています。
では、実行してみましょう。
C:\Users\tom_a\Documents\javastudy>java hello.Hello3
エラー: メイン・クラスhello.Hello3を検出およびロードできませんでした
原因: java.lang.ClassNotFoundException: hello.Hello3
何かエラーになっています。
では、javaコマンドの引数にHello3だけを指定してみましょう。
C:\Users\tom_a\Documents\javastudy>java Hello3
エラー: メイン・クラスHello3を検出およびロードできませんでした
原因: java.lang.NoClassDefFoundError: hello/Hello3 (wrong name: Hello3)
やっぱりエラーになってしまいます。
実はjavaのパッケージはフォルダ構成と強くかかわっています。
Hello3クラスはhelloパッケージに属しているので、helloフォルダを作って、その中にHello3.classファイルがなければなりません。
フォルダhelloを作成し、Hello3.classファイルを移動してみました。
C:\Users\tom_a\Documents\javastudy>mkdir hello
C:\Users\tom_a\Documents\javastudy>move Hello3.class hello
1 個のファイルを移動しました。
C:\Users\tom_a\Documents\javastudy>dir
ドライブ C のボリューム ラベルは Windows-SSD ですボリューム シリアル番号は 8807-E74C です
C:\Users\tom_a\Documents\javastudy のディレクトリ
2023/01/20 21:53 <DIR> .
2023/01/20 21:07 <DIR> ..
2023/01/20 21:53 <DIR> hello
2023/01/20 21:12 419 Hello1.class
2023/01/19 23:43 112 Hello1.java
2023/01/20 21:46 861 Hello2.class
2023/01/19 23:46 122 Hello2.java
2023/01/19 23:48 128 Hello3.java
5 個のファイル 1,642 バイト
3 個のディレクトリ 26,051,334,144 バイトの空き領域
C:\Users\tom_a\Documents\javastudy>dir hello
ドライブ C のボリューム ラベルは Windows-SSD です
ボリューム シリアル番号は 8807-E74C です
C:\Users\tom_a\Documents\javastudy\hello のディレクトリ
2023/01/20 21:53 <DIR> .
2023/01/20 21:53 <DIR> ..
2023/01/20 21:48 425 Hello3.class
1 個のファイル 425 バイト
2 個のディレクトリ 26,051,325,952 バイトの空き領域
それでは実行してみます。helloフォルダではなくjavastudyフォルダで実行していることに注意してください。
C:\Users\tom_a\Documents\javastudy>java hello.Hello3
Hello, World!
今度はエラーなく実行できました。
Javaはクラス名とJavaプログラムファイル名が一致する仕様となっています。
また、パッケージが異なっていればクラス名が重複しても良いという仕様となっています。
このため、複数のパッケージのJavaプログラムファイルを1個のフォルダに入れた場合、重複したクラス名を持つクラスのJavaプログラムファイルはどれか1個だけしか置くことができず、ソフトウエアとしては困ったことになってしまいます。
このため、パッケージ名の付いたクラスのJavaプログラムファイルはパッケージ名で指定されたフォルダに配置するようにしています。
なお、パッケージ名に「.」が含まれている場合には「.」で区切られた範囲ごとにフォルダが作られ、例えばjava.utilパッケージであれば「java\util」フォルダにクラスファイルが配置されます。
なお、ソースコードも同様にフォルダで管理するのが普通で、上の例ではHello3.javaファイルをhelloフォルダに配置してjavac hello\Hello3.javaを実行するとhelloフォルダにHello3.classファイルができるようになっています。
無名パッケージと暗黙のインポート
ところで、これまで見てきたプログラム例で気づくところがいくつかあったと思います。
まず、package宣言がないクラスはどういう扱いになるかという点です。
package宣言がないクラスは無名パッケージに所属するという仕様になっています。
無名パッケージというパッケージに所属しているので他のパッケージに所属するからクラス名で参照することはできません。
なぜならパッケージ名が指定されていないクラスは同一パッケージ内のクラスという扱いになるからです。
同様にクラスをインポートすることもできません。
このように無名パッケージのクラスは他のパッケージから参照できないクラスとなってしまいます。
このため、無名パッケージは非常に小さなプログラムぐらいでしか使い道がないので、普通は使用しないのがいいと思います。
もう一つはSystem.out.printlnです。
System (あとStringもですが) はjava.langというパッケージに含まれています。
java.langパッケージには使えないと困るレベルのプログラミングの基本となるクラスがまとめられているので、いちいちインポートするのは面倒だろうということでjava.langパッケージに含まれるクラスはすべて暗黙の裡にインポートされています。
ついでに話しておくと、パッケージ内のクラス全部、例えばjava.langパッケージのすべてのクラスをインポートする際にはクラス名の代わりに「*」を記述する方法が使えます。
java.langであればimport java.lang.*になります。
つまり、すべてのJavaプログラムにはimport java.lang.*がこっそりと埋め込まれているということです。
以上、パッケージ、クラスのインポート、mainメソッド、javaコマンドについて雑談レベルでだらだらと書いてみました。
今回のまとめ
最後にまとめです。
Javaプログラムのソースコードでは先頭でパッケージ宣言を行います。
パッケージ宣言がないクラスは無名パッケージに所属します。
同一パッケージ内のクラスはクラス名で参照できますが、他のパッケージに所属するクラスはパッケージ名を含む長いクラス名を使うかインポートしてクラス名を使うかのどちらかを選択します。
Javaプログラムはjavaコマンドの最初の引数で指定されたクラスのpublic static void main(String[] args)で指定されたメソッドから実行されます。
javaコマンドの2番目以降の引数はmainメソッドのargs引数に渡されます。
以上です。
0コメント