Edited at

System.out.printlnに配列を渡すことから始まるtoString()の話

More than 1 year has passed since last update.

配列を作って、その中身を表示しようと思って、


配列をそのままprintlnに

int[] array = {1,2,3,4,5};

System.out.println(array);

のように、配列をそのまま引数にして標準出力に渡してしまう人、初心者で意外にいるようだ。

この出力結果(例)は

[I@1540e19d

のように、わけのわからないものになる。

これを見て、「うまく動かない」などと質問されることが…

なぜこんなことになるのか?


System.out.printlnとは

System → java.lang.Systemクラス

out → SystemクラスのPrintStream型のstatic変数

つまり、printlnはPrintStreamクラスのメソッドなのである。

そこでPrintStream#printlnを見てみると、いくつかのオーバーロードが存在する。

その多くは各種プリミティブ型に対応するためのオーバーロードなので、それ以外のものを挙げると、

println(char[] x)

println(Object x)

println(String x)

これらが存在する。プリミティブ型を含め、これらは引数を文字列化したものを出力する。

char[]の場合は各要素の文字をつなげて出力し、Stringもその内容をそのまま出力する。

Objectの場合はどうかというと、引数をString.valueOfに渡した結果の文字列が出力される。


String.valueOf(Object)


Object引数の文字列表現を返します。

パラメータ:

obj - Object。

戻り値:

引数がnullの場合は"null"に等しい文字列。それ以外の場合はobj.toString()の値が返される。


そのままtoString()にしないのはnullセーフのため。

ではそのtoString()とはどうなっているのか?


Object#toString


オブジェクトの文字列表現を返します。一般に、toStringメソッドは、このオブジェクトを「テキストで表す」文字列を返します。この結果は、人間が読める簡潔で有益な情報であるべきです。すべてのサブクラスで、このメソッドをオーバーライドすることをお薦めします。

クラスObjectのtoStringメソッドは、オブジェクトがインスタンスになっている元のクラスの名前、アットマーク文字「@」、およびオブジェクトのハッシュ・コードの符号なし16進数表現から構成される文字列を返します。つまり、このメソッドは次の値と等しい文字列を返します。

getClass().getName() + '@' + Integer.toHexString(hashCode())

戻り値:

このオブジェクトの文字列表現。


となっている。ここで最初の出力を見てみよう。

[I@1540e19d

お分かりだろうか?この@以降の文字列はInteger.toHexString(hashCode())の結果、つまりハッシュコードの16進表記ということである。しかし、この数字は見ても人にとっては意味がない。

メソッドの説明に「この結果は、人間が読める簡潔で有益な情報であるべきです。すべてのサブクラスで、このメソッドをオーバーライドすることをお薦めします。」とある通り、オーバーライドして使うのがこのメソッドの本来の使い方である。基底クラスであるObjectでは「とりあえず呼ばれたらこう出力する」とだけ決めてあって、内容がどうこうといった情報は入っていないのである。


@の前の文字列 - 何のクラスのオブジェクト?

1つだけ有益な情報があるとすれば、それは@の前の文字列である。この例で言えば[Iだ。

これが何…?そう思ったら、もう一度説明を読もう。

@の前の文字列、それはgetClass().getName()の結果である。

ではそちらはどういう仕様になっているのか?

getClass()の返り値の型Classについては詳しく説明するのも理解するのも難しいので、とりあえず「どのクラスのインスタンスかを表すオブジェクト」という程度に認識してもらいたい。


Class#getName


このClassオブジェクトが表すエンティティ(クラス、インタフェース、配列クラス、プリミティブ型、またはvoid)の名前を、Stringとして返します。

『Java(tm)言語仕様』で規定されているように、このクラス・オブジェクトが配列型ではない参照型を表す場合は、クラスのバイナリ名が返されます。

このクラス・オブジェクトがプリミティブ型またはvoidを表す場合、返される名前はプリミティブ型またはvoidに対応するJava言語キーワードと等価なStringです。

このクラス・オブジェクトが配列のクラスを表す場合、名前の内部形式は、配列の入れ子の深さを表す1つ以上の[文字、要素のタイプの名前という順序で構成されます。要素のタイプの名前のエンコーディングは、次のとおりです。

要素のタイプ エンコーディング

boolean型 Z

byte B

char C

classまたはinterface Lclassname;

double D

float F

int I

long J

short S

クラス名またはインタフェース名のclassnameは、上記の例のようにクラスのバイナリ名で指定されます。

String.class.getName()

returns "java.lang.String"

byte.class.getName()

returns "byte"

(new Object[3]).getClass().getName()

returns "[Ljava.lang.Object;"

(new int[3][4][5][6][7][8][9]).getClass().getName()

returns "[[[[[[[I"


簡単に言えば、そのインスタンスの型を表現する文字列である。

注目してほしいのは配列の時の場合。配列の次元数だけ[が並び、そのあとにクラス名(プリミティブ以外ならLが挟まる)が続く形式となっている。

例の最後の場合、配列が7次元になっているため、[が7個並び、そのあとにint配列であることを表すIが連なって表現されている。

つまり[Iは、このオブジェクトが1次元のint配列である、ということを表現していたのだ。

以上のように、最初の一見意味不明な出力は、きちんと仕様に従って出力されていたものなのだ。意味不明な文字列だからといってバグと決めつけるのではなく、きちんと本来の動きを調べよう。

ちなみに、配列の中身をきちんと出力したいのなら、Arrays.toString()(多次元配列ならArrays.deepToString())に渡して文字列化するとか、ループを使って出力するとかしましょう。