はじめに
Garbage in, garbage out
この言葉を聞いたことがありますでしょうか?
コンピュータサイエンスの分野で生まれた言葉で、日本語に訳すと「ゴミを入れたら、ゴミしかでない」という意味です。コンピュータが如何に高性能でも、コンピュータに入力するデータ(プログラム)が不完全なら出力される結果も不完全ということですね。最近は生成AIの登場でプロンプトの技術が重要視されていることもあり、この言葉を耳にする機会が増えています。
もちろんこの言葉はコンピュータを使う人間側を揶揄する言葉だと思いますが、この言葉には逆の真理も含まれていると思います。つまり「良質なものを入れたら、良質なものが出てくる」という真理です。
そこで今回の真髄は「標準ライブラリ」がテーマです。
標準ライブラリのソースコードは積極的に見るべし
ライブラリとは誰かが作ってくれた便利な道具集で、標準ライブラリはそのプログラミング言語用にあらかじめ用意されている基本的な機能やツールの集合体です。標準ライブラリの豊富さがその言語の人気を左右するくらい重要なものです。通常、ライブラリにはAPI仕様書が用意されており、プログラマはAPI仕様書を見てライブラリを使います。よってライブラリのAPI仕様書を見ることはプログラマとしての必須要件ですが、今回はさらに一歩踏み込んでソースコードもぜひ見てほしいという提案です。Javaの標準ライブラリに関してはソースコードも展開されていますので実装を見ることができます(後述)。
ライブラリを使用するだけならAPI仕様書を見れば十分なのですが、なぜライブラリの実装を見てほしいのか?それは標準ライブラリのソースコードはたくさんの専門家によってレビューされ、また利用者からのフィードバックにより改善され、洗練されたコードですので、プログラミング技法を勉強する上でこれほど良質なインプットはないでしょう。さらにライブラリの実装内容を見ることで「なぜこのライブラリはこのような使い方になるのか?」といったこともよく分かり、ライブラリを使う上での理解度も格段に上げてくれます。
では実際にいくつか例をご紹介します。
System.out.printlnの例
System.out.printlnはコンソールに文字列を出力するJavaの学習で最もお世話になるライブラリの一つですね。System.out.printlnには次の特徴があります。
- 引数で渡すインスタンスのtoStringメソッドを自動的に呼び出してくれる
- 引数で渡すインスタンスがnullなら”null”という文字列に変換され出力される
という仕様です。このように説明されるとJVM(Java仮想マシン)が動的に判断して処理を行っているかのように思われるかもしれませんが、そんなことはありません。
では実際に以下のコードを実行したときに、どのように実行されるのかをソースコード上で追いかけてみましょう。
Sample s = new Sample();
System.out.println(s);
printlnメソッドのソースコード
public class PrintStream extends FilterOutputStream implements Appendable, Closeable {
~中略~
public void println(Object x) {
String s = String.valueOf(x);
if (getClass() == PrintStream.class) {
// need to apply String.valueOf again since first invocation
// might return null
writeln(String.valueOf(s));
} else {
synchronized (this) {
print(s);
newLine();
}
}
}
printlnメソッドが呼ばれるとすぐにString.valueOf(x)を呼び出しています(4行目)。
valueOfメソッドのソースコード
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
~中略~
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
valueOfメソッドが呼び出されると6行目が実行され、このとき元の引数である s が null なら”null”という文字列に変換されますし、null でなければ toStringメソッドが呼び出されています。
このように実際にソースコードを追うことで、System.out.printlnの引数にインスタンスを渡すと”null”が出力されたり、toStringメソッドが呼ばれる仕組みがよく分かりますよね。決してJVMが「自動的に」処理しているわけではなく、単に用意されたプログラムを実行しているだけということが分かります。
次にStringクラスの実装を見てみましょう。
Stringクラスの例
Stringクラスは文字列を扱うクラスで、Javaではデフォルトの文字列はこのStringクラスになりますので非常によく使われるライブラリの一つです。Javaでは文字列リテラルはすべてStringクラスのインスタンスですし、文字列を操作するさまざまなメソッドが用意されていたり、とても便利なライブラリなのですが、Stringクラスで最も重要な特徴は「不変クラス(イミュータブル・クラス)」ではないでしょうか。不変クラスは一度インスタンスを作ったあとは値(フィールドの値)を変更することができないクラスのことです。つまり一度インスタンスを作ってしまえばプログラムの最後まで値が変わらないことが保証されますので、プログラムのどんな場所でも安全にデータを扱うことができます。
値が変更できない不変クラスは実際にどのように実現されているのでしょうか?これもソースコードを見ると一目瞭然です。
必要な部分だけ抽出したコードを下記の記載します。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
~中略~
private final byte[] value;
文字列を管理しているvalueフィールドにfinal修飾子が付いていることが分かります。これでvalueフィールドの値が変更できなくなり、不変になります。さらに文字列はStringクラスの内部ではbyte型の配列として管理されていることも分かります。このように実際にソースコードを見ることで不変クラスがどのように実現されているかがよく分かると思います。
最後にArraysクラスのasListメソッドの実装を見てみましょう。
ArraysクラスのasListメソッドの例
次のコードを実行すると結果がどうなるか分かりますでしょうか?
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
list1.add(0);
List<Integer> list2 = Arrays.asList(1,2,3);
list2.add(0);
}
}
実行結果を見て分かる通り11行目の「list2.add(0);」の行で例外(実行時エラー)が起こってしまいます。
ArraysクラスのasListメソッドは引数が可変長で指定出来て、List型のインスタンスを返却してくれますので、簡単にリストを作りたい時に便利なメソッドです。ただし実行結果を見て分かる通りリストに要素を追加したり削除したりするとjava.lang.UnsupportedOperationExceptionの例外が発生します。このことはJava Silverでも出題されるくらい重要な制約ですので知っておかないと現場で苦労することになるでしょう。
new ArrayListで作ったListインスタンスは追加(add)や削除(remove)しても問題はないのに、ArraysクラスのasListメソッドで作ったListインスタンスは例外が発生するのはどういった理由なのでしょうか?
これもライブラリのソースコードを見ると一発で理由が分かります。asListメソッドのソースコードで必要な部分だけ抽出したコードを下記の記載します。
public final class Arrays {
~中略~
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
/**
* @serial include
*/
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
@java.io.Serial
private static final long serialVersionUID = -2764017481108945198L;
@SuppressWarnings("serial") // Conditionally serializable
private final E[] a;
~以下略~
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
~中略~
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
~中略~
public E remove(int index) {
throw new UnsupportedOperationException();
}
~以下略~
asListメソッドの中では「new ArrayList<>(a)」でArrayListクラスのインスタンスを生成していますが、このArrayListクラスはutilパッケージのArrayListクラスではなく、asListメソッドのすぐ下で宣言されている内部クラスのArrayListが使われています。この内部クラスのArrayListの配列を管理しているフィールドをよく見てみましょう。「private final E[] a;」となっておりfinal修飾子が付けられています。つまり内部で管理されている配列が不変に設定されていることが分かります。さらにArrayListが継承しているAbstractListの実装を見ると、addメソッドやremoveメソッド内でUnsupportedOperationExceptionがスローされています。
ソースコードの実装を見ることでArraysクラスのasListメソッドを使用して取得したListインスタンスは不変オブジェクトであり、追加や削除を行うとUnsupportedOperationExceptionが発生するという理由が一目瞭然で理解できます。
まとめ
如何だったでしょうか?「標準ライブラリのソースコードは積極的に見るべし」というライブラリの真髄に触れていただけましたでしょうか?
ライブラリのソースコードを見ることで「なぜこのライブラリはこのような使い方になるのか?」がよく分かったと思います。しかもライブラリはたくさんの人のレビューを経て採用されているためコードも洗練されていますので、プログラムの実装力を向上させるのにとても良質なインプットになります。ライブラリの中には複雑な実装もあって初心者の方はプログラムを追いかけるのが大変かもしれませんが、1行1行細かく内容を理解する必要は全くありません。今回のように「フィールドの修飾子はどうなっているのかな」とか「どのクラスのインスタンスが使われいるのかな」といった観点でソースコードを見るだけでもかまいません。またライブラリはさまざまなアルゴリズムを実装していたり、デザインパターンも使われいたりしますので、自分のプログラミング力の引き出しを増やすのに最適なお手本にも出来ると思います。プログラマの皆さん、積極的にライブラリの実装を見るようにしましょう。
今回も最後までお読みいただき、ありがとうございました。
補足~SE標準ライブラリのソースコードの在り処~
JavaのSE標準ライブラリのソースコードはJDKのインストールフォルダー内のlibフォルダー内に「src.zip」という名前で圧縮されていますので、それをどこかにコピーして解凍するだけでライブラリのソースコードが見られます。

Eclipseをお使いの方はパッケージエクスプローラーでプロジェクトフォルダーを開き、その中にあるJREシステム・ライブラリー内の「java.base」パッケージ内にクラスファイルが格納されていますので、目的のクラスファイルをダブルクリックするとソースコードの中身が見られます。

<前の記事>

※アイキャッチ画像はChatGPTで作成しています