2.4 トークン
2.4.1 Java言語の基本構造
これまで、いくつかのプログラムをコンパイルしましたが、コンパイラはどのように私たちが書いた Java 言語ソースプログラムを分析するのでしょうか?コンパイラはプログラムなので私たちが日常使っている自然言語のように曖昧な解釈はしてくれません。ソースコードに必ず何らかの意味付けをして解釈します。これを理解することで、プログラミングの本質を知ることができるでしょう。
コンパイラは、私たちの脳とは異なり同時並行的に言語を理解することはできません。文字の並びを頭から順に解析して機械語(Java の場合はバイトコード)へと変換しているにすぎません。コンパイラにソースファイルを渡すと、コンパイラは最初に Unicode エスケープを検索して Unicode 文字に変換します。これは、「2.3 字句変換とUnicode」で解説しましたね。次に、行末記号の分析が開始されます。行末記号は ASCII の改行文字、または復帰文字によって行に分割され行番号が生成されます。
こうして、Unicode エスケープと行末記号を処理した最終的な結果を入力要素と呼びます。ソースプログラムは入力要素と還元され、さらに、入力要素はトークンへと分解されます。トークンとは、空白とコメントを除いた入力要素であると定義されています。コンパイラはこれを解析してプログラムを生成するのです。
トークンはソースプログラムの最小単位であり、これ以上に分解されることはありません。トークンは英文における英単語のような存在です。このコンパイル手順の考え方は Java 言語だけではなく C/C++ などのコンパイラ言語全般で共通です。開発者は最終的に、このトークンを意識してプログラムを記述しなければなりません。トークンを意識すれば、コンパイル・エラーの原因も見つけやすくなるでしょう。
入力要素がトークンに分解される時、空白文字とコメントは隣接したトークンを分離させる役割を持つことがあります。空白文字とはスペースキーの入力による単純な空白(全角は含まない)以外に、水平タブ文字やフォームフィード、行末記号を含みます。例えば、次のようなクラス宣言を思い出してください。
class Test { ...
これを、次のように記述した場合はコンパイル・エラーとなります。
classTest { ...
なぜならば、キーワード class とクラス名 Test の間に空白文字が存在しないからです。コンパイラはトークンとしてこれらを分離することができないため classTest というトークンとして扱いますが、classTest という名前のキーワードは Java には存在しないため、エラーを出すのです。逆に、トークンを意識してプログラムを見れば class と Test の間にコメントがあろうが、空白文字がいくつあろうがかまわないのです。
class/*Kitty*/Test { ...
このクラス宣言は何の問題もありません。コメントにはトークンを分離するという隠れた機能があるためです。逆に、次のようなクラス宣言は誤っています。
cla/*Kitty*/ss Test { ...
コメントは無視されるのだから、最終的に class Test という形になりそうですが、コメントによってトークンが分離されるため、この文は cla ss Test というように分解されてしまうのです。cla というキーワードは存在しないためエラーとなります。
トークンにはキーワード以外に、いくつかの種類に分類することが可能です。例えば class はキーワードですが、Test というのは識別子(名前)であると認識されるのです。そのトークンがどのような性質を持っているのかを理解することも重要です。
- キーワード
- 識別子
- リテラル
- 区切り子
- 演算子
キーワードとは、Java 言語仕様が予約している単語のことです。class や public などがキーワードになります。識別子とはプログラマが自由に付けることができる名前のことです。Test や main など、クラス名やメソッド名は識別子に分類されます。リテラルとは固定的な値のことで 10 とか "Kitty on your lap" という文字列はリテラルに分類されます。区切り子はメソッドなどに使われる括弧などのことで、これもトークンを分離させる効果を持っています。{ } や ( )、などの括弧はもちろんですが、セミコロン ; やドット . も区切り子に分類されます。演算子は式に使われる計算記号などで + や - などのことです。
これらトークンの種別認識は極めて重要です。今後もこうした用語を頻繁に使うと思われるので、忘れた時はこのページを読み直して思い出すようにしてください。
class Test { public static void main ( String args[ ]) { System.out. println("Kitty on your lap") ;}}
トークンをよく理解していればコード1がなぜ正しくコンパイルできるのかを説明することができます。一見するとこのソースは読みづらく汚いのですが、トークンに分解すると何の問題もないことがわかります。表1はコード1の一部をトークンとして分解したものです。実際に、コンピュータの中ではソーステキストをこのように分解して解析しているのです。
トークン | 種類 |
---|---|
class | キーワード |
Test | 識別子 |
{ | 区切り子 |
public | キーワード |
static | キーワード |
void | キーワード |
main | 識別子 |
( | 区切り子 |
String | 識別子 |
args | 識別子 |
[ | 区切り子 |
] | 区切り子 |
) | 区切り子 |
キーワードと識別子の間には空白を使ってトークンを分離させる必要がありますが、区切り子と他のトークンの間には必要ありません。Java 言語はコード1のように、空白や改行を自由に書くことができますし、逆に一行でプログラムを書き続けることも可能です。最終的に正しくトークンを分解することができれば体裁はどのような形でもかまいません。
コード1を見て明らかなのは、このようなソースコードは誰も読んではくれないだろうということです。多くの開発者は常識的に { } の中はインデントを 1 つ深くする習慣を持っています。
... { //何らかのプログラムコード ... { //何らかのプログラムコード ... { //何らかのプログラムコード ... } ... } ... }
このように記述すれば、対象のテキストがどのクラス、どのメソッドに属しているかを容易に認識することができます。