【初心者向け】コンパイル時にクラスが見つからない時、どこを見直すべきか

# 記事の対象読者

Javaコマンドを自分でタカタカして、コンパイルとか実行周りの勉強してみようと思った時、
ソースコード書いて、フォルダ階層作って、ソースコードを配置して、と手順を踏み、
いざ、javacコマンドでコンパイルする段になると

「シンボルを見つけられません」

この１行のせいでいつまでたってもクラスファイルの作成ができないなんてことありませんか。
構文エラーって割と細かく情報が出るので、原因箇所の特定がしやすいのですが
ことクラスが見つからないってエラーに関しては実装内容ごとに修正点は違うので、
「ここをこう直せば直るよ！」というものはなかったりします。

ですが、初学者に真に必要なのは
「どう直すか」という個別ケースにしか使えない情報ではなく、
「仕組み上、ここを見れば原因がわかる」という使い回しのきく
デバッグの知識なんじゃないかなと思います。

この度、コンパイル周りの仕組みを勉強したら「シンボルを見つけられません」に対して
どのようにアプローチすればいいのか、というものがそれなりに見えてきたので、
現時点での私のデバッグ手順に関して、紹介したいなと思います。

# 視点１：クラス名は合っているか

傷は浅いほうがいい。のちに紹介する観点で壮大な調査を行なった結果、
エラー原因がただのタイピングミスだった日にはつらすぎるので、
まずはTest.javaクラスの宣言が```public class Tst```になってたりしないか(一例です)
タイピングミスを疑います。
クラスの宣言、パッケージの指定以外は、割と構文エラーに引っかかってくるので
私はそこまで深追いしません。

# 視点２：クラスの種類は何か

ここでの種類とは、見つからないクラスが
**ブートストラップクラス**なのか、**jarファイルに格納されたクラス**なのか、
**ユーザクラス**なのかということです。

ブートストラップクラス、ユーザクラスの詳細について知りたい方は、
Javaのクラスローダやコンパイル時の型の検索について調べてみてください。

ここでは細かな話は抜きにして、ざっくりと紹介します。

- ブートストラップクラス  
JDKのコアAPIなど、よく使うjava.langパッケージなどの中核機能はこちらに含まれます。
- ユーザクラス  
ユーザ(プログラマ)が独自に作成するクラスです。ユーザ定義クラスとも言います。
- jarファイルに格納されたクラス  
サードパーティライブラリ、ユーザが独自に定義したライブラリなどいろいろですが、  
eclipseなんかでよくビルドパスに追加するアレです。

ここで判断できることは何かというと、見つからないクラスが
ブートストラップクラスの場合には「JDKを指定してるJavaの環境変数」を
ユーザクラスの場合には「クラスパス変数」を疑おうぜっていうことです。

> ブートストラップクラスなのか判断できん。
という場合は、[コアAPIドキュメント](https://www.oracle.com/technetwork/jp/java/javase/documentation/api-jsp-316041-ja.html)などを参考に。

## ブートストラップクラスが見つからない場合

JDKが指定できていない、ということなので、環境変数「JAVA_HOME」が
きちんと設定できているか、見直してみましょう。
JAVA_HOMEの設定方法に関しては記事も多く出ているので割愛します。

## jarファイル内のクラスが見つからない場合

jarファイルのクラスパス指定は、クラスファイルのクラスパスの指定とは少し異なります。
とは行っても、指定するのがディレクトリなのか、jarファイルそのものなのかの違いだけです。
コマンドの文法が正しいか、確認してみましょう。

```shell
# jarの場合
$ javac -classpath /lib/example-lib.jar Example.java
# クラスファイルの場合
$ javac -classpath /lib Example.java
```

## ユーザクラスが見つからない場合

こっちのケースの場合には見るべき観点が少し増えます。
こちらに該当する場合は、以降の内容を読み進めてください。

# 視点３：クラスパスは正しく指定されているか

ユーザクラス、サードパーティライブラリの読み込みは、
兎にも角にもクラスパスの指定が正しくなければなりませんし、
クラスパス指定の仕組みをきちんと理解しないとなんとなくでは動いてくれません。

以下、クラスパスを見直す際の観点について紹介していきます。
なお、紹介順通りに調査しなければならないということはありません。

## 視点3-1：パッケージルートを指定しているか

以外と他の説明に押しやられてさらっと無視されている、
「クラスパスにパッケージの階層を指定するとうまくimportされない」という事実。
というか当たり前すぎて書かれていないんだろうけど、私はここで引っかかりました。

どういうことかというと、以下のようなフォルダ構成があり、UseCommons.javaでは
StrFactoryを使用したいので```com.example.util```をインポートしているとします。
<img width="287" alt="スクリーンショット 2020-05-25 16.25.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619923/bf9c7ad8-9897-5d8b-457b-546ce08ef469.png">

```java:UseCommons.java
package com.example.util;
```

上記の場合、srcはパッケージルート、com〜appはパッケージ階層という風に認識されます。
これらをコンパイルする際、私が何をしたかというと

```shell
# カレントディレクトリは/srcです
$ javac -classpath /src/com/example/util com/example/app/UseCommons.java
```
上記のように、クラスパスに```/src/com/example/util```を指定することで、
util配下のクラスを読み込んで、UseCommons.javaをコンパイルしたかったわけです。
ですが、Javaは**クラスパスで指定したディレクトリを起点にパッケージ階層をたどる**ため、
上記のように設定すると、UseCommons.javaがStrFactory.javaを見に行く際に、
```/src/com/example/util/com/example/util```を見に行ってしまうわけです。

つまり、正しいクラスパスはパッケージルートであるsrcを指定した、以下になります。

```shell
# カレントディレクトリは/srcです
$ javac -classpath /src com/example/app/UseCommons.java
```

以上の観点から、-classpathオプションに指定したパッケージルートと
クラスファイルに宣言したimport文の内容を連結させた場所に、
読み込みたいクラスが配置されているかを確認してみましょう。

## 視点3-2：デフォルト設定が無効になっていないか

これが一番の曲者だと思います。
デフォルトで設定していたものがオプションの指定などで上書きされ、
必要な設定まで無効になっていたりすると、普段は意識していない分、気づきづらいです。

-classpathには通常、「.(カレントディレクトリ)」が設定されています。
それらを踏まえ、以下の例を見てください。
<img width="280" alt="スクリーンショット 2020-05-25 16.16.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619923/386a86b2-4d6f-7363-ea13-f1c6c8f7b967.png">

最初は、上記のような構成だったため、以下のコマンドで正常にコンパイルできます。

```shell
# カレントディレクトリは/srcです
$ javac com/example/app/UseCommons.java
```

ところがこの後、UseCommons.javaで外部のライブラリ「commons-lang3-3.10.jar」を
使用することになり、フォルダ構成を以下の通り変更しました。
<img width="280" alt="スクリーンショット 2020-05-25 17.00.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619923/02c33ee7-26a5-daaa-1020-e6dcb72a4fd8.png">
そして、commonsのライブラリをクラスパスに追加し、以下のコマンドを実行したところ、
StrFactoryが見つからないというエラーが発生します。

```shell
# カレントディレクトリは/srcです
$ javac -classpath /lib/commons-lang3-3.10.jar com/example/app/UseCommons.java
```

これは、classpathオプションのデフォルト設定である「.(例のカレントディレクトリは/src)」が
上書きされたことで、Javaがsrc配下のクラスを検索できなくなったためです。
よって、上記のコマンドを以下のように修正すると、エラーは解消されます。

```shell
# カレントディレクトリは/srcです
$ javac -classpath /lib/commons-lang3-3.10.jar:. com/example/app/UseCommons.java
```

# 終わりに

以上が、現在の私が持ちうるコンパイル時のデバッグ観点です。
この他にも様々な見方があると思いますので、あくまでも初心者の方の勉強の
最初の一歩の手助けとして、認識していただければと思います。
観点は広い方がいいので、私自身もさらに学びを深めたいと思います。

また、他の記事でJavaのコンパイルについても書いておりますので、
コンパイルの仕組みについて興味が出たという方は、以下の記事も参照してみてください。

記事へのリンク：[Javaコマンド入門　第３章 ソースコードをコンパイルする](https://qiita.com/maple_syrup/items/c0aa2eacb8f486dc52a8#%E7%AC%AC%EF%BC%93%E7%AB%A0-%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB%E3%81%99%E3%82%8B)