LoginSignup
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-04-22

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

配列をそのまま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())に渡して文字列化するとか、ループを使って出力するとかしましょう。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
9