LoginSignup
15
10

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

15
10
0

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
  3. You can use dark theme
What you can do with signing up
15
10