String型って結局なんなんだろう?
今しがた風呂に入っていてふと思いました。Java言語にてリテラルはプリミティブ型とString型とnull型の値を直接コード上で表したものだよなあ,と。プリミティブ型は参照型とは違った特性として「そうなんだからしょうがない」(などと言ってしまってはいけないかもしれませんが)として,null型も「null」なんだから,そのデータを直接記述し扱えるのも同意できる。じゃあなんで参照型の中でStringだけ,リテラルが存在するのか。なんでだ?感覚的になんでだ?と思ってしまった。よし,調べよう。
私の環境
- Eclipse 4.6 Neon (エディタの文字サイズを変更しやすくなって嬉しい)
- JavaSE-1.8(jre1.8.0_111) (大雑把に使っているだけで,あまりJava8(の有難さ)に触れられる機会がない)
とりあえず,String型を見る
ここがすべての間違いの始まりなのですが,そもそも文字列「リテラル(「文字通り」の意)」だから String そのものなんですよ。でも「空」。nullではないけど,「空」。「空」って結構重要な概念ですよね。なんで文字列リテラルが用意されたのかなんて,Stringクラス見に行ってもわかるようなことが書いてあると思えません。
でも何の気なしに,ぼんやり F3 キーを押して String クラスを眺めに行きました。こんにちは。私の謎の1時間。
まず飛び込んでくるのが,
private final char value[];
というフィールド。String型の文字列データを保存するのは不変なchar値の配列。一連の順序,流れが重要なのでシーケンスとか言われるやつです。newすると新しいデータを生成しまくるとか最初習ったなあ。変更可能な文字列には StringBuilder 使えとかいうのはこのせいですね。
超大量にあるコンストラクタの最初のほう見て「うんうん」ってなりそうで今更ながらにびっくりしたのが,
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// 以下省略
this.value = "".value;
ってどうなるんだろう?です。
空文字列ってことで,String str = "";
で初期化するとか見るし,昔に教わって私も使ってますが,よく考えたらchar c = '';
は不可じゃないかと。インスタンスの生成はできても初期値の代入はできないのでは???
String str = "";
System.out.println(str.length()); // 0
System.out.println(str.charAt(0)); // Exception!
よく考えたら初期値入ってねえええ。生成できてるけど値は入ってねええええ。
空文字列というデータが入っているという夢を見た。そんなことなかった。いや,「空」という文字ではない文字が入っているのですが,それはメモリ領域が確保されているとはいえ文字ではないわけで。文字としてのデータが「空」なので文字列の長さが 0 でもメモリ領域は確保されるわけで。在るのは在るんだけど,無いという何とも言えない「空」の概念。奥深いです。
String.charAt()はまんまと StringIndexOutOfBoundsException と囁いて止まりました。
よし,再チャレンジ。
String str = "I love you.";
System.out.println(str.length()); // 11
str = "I love you." + "";
System.out.println(str.length()); // 11
うん,文字列長は変わらないですね。当然ですね。「空」ですから。
ここでふと我に返りました。例えばPoint p = new Point();
が x, y ともに初期値 0 で設定されるのはシーケンスじゃないからかじゃないか,と。
シーケンス
StringクラスはCharSequenceを実装していて,StringBuilder も StringBuffer もそう。なのでこいつらは,引数に何も与えずにインスタンスを生成したら,データを入れる領域が確保されるだけで,そんなん配列そのものの特性やのに。Stringとはちょっと違うけれど,生成するだけではいけないのになぜ気づかんかった自分。と悶えながらこれを書いています。Collection も配列とは違いますが,まず List などの枠組みそのもののインスタンスが生成されて,データは後から入れますよね。
空文字列であっても文字列は文字列(有意の何かしらのデータ)だろうといった気の緩みがイケてなかったです。何故忘れていたのか。基礎の基礎って大事ですよね。痛感しました。
同時に今世紀最大の自分へのがっかりを感じました。
String 型のインスタンスの空文字列での初期化の意義
初期化って宣言とともに値を代入することだと思っています。明確な文字列というデータは代入できていなくても,Stringのインスタンスという情報は生成されて出来上がっているわけで,初期化は初期化なのだと思うけれど,「空」という素敵概念のおかげでなんだかほかのインスタンスにはない初期化のややこしさを含んでいる気がします。
まあ,空文字列は文字列のリテラルなので,文字列型(String 型)であるという情報は持てるという意義はあるやも知れませんが,昔は納得できた
「null による例外発生の可能性を低減する」という空文字列での代入の意義の1つ(と認識していた)が今はしっくりきません。Optional が使えるようになったからでしょうか。しかし文字列リテラルを使用したインスタンスの初期化に対して Optional とか,使い方が変です。Optional はメソッドの戻り値が null の場合が想定される場合にいい感じに使えるものだと思っています。こんなところには有難さが感じられません。逆にただただめんどくさいです。
Eclipse を使っていると,初期化ができていない状態で変数を使おうとすると鬼神の如く怒られますし,null や空文字列で初期化することが無駄に思えてしまいます。もはや宣言だけして値を入れないのが気持ち悪いとかいうそういう感覚で空文字列代入しています。null は初期化できるけど null(何もない。参照先すらない) なのでなんだかヤダって感覚です。これはまずいです。
いつでも同じ書き方で(2017-1-24追記)
@hiuchida さんから,null と非 null の切り分けしないでいいっていう楽さが素敵じゃないですかとコメントいただきました。例をそのまま載せさせていただきます。
String empty = "";
for (char ch : empty.toCharArray()) {
確かに,null か非 null をいつも意識しているのは疲れます。おっしゃられている通り,楽できるからという観点も素敵ですよね。
グダグダでしたが,何かの拍子に同じような「何故?」に再度捕らわれないようにとの戒めにこのまま残します。そして,空文字列の健全な利用方法探しの旅に出ます。
結論
- 文字列を使用する機会はすごく多いし,書くという特性上文字列リテラルが無いのは縛りプレイもいいところなので,文字列という存在のリテラルはあったほうがいいんじゃないかということでリテラルがあるのでは?<-結局調べてない
- そもそも,用意されているリテラルという存在に,「存在意義」を求めるのはいかがなものかと思うので有難く使わせていただく。
- 空文字列での初期化で初期化はできるが,値としては「空」
心に誓ったこと
- 空文字列のハッピーな使い方を模索する。
- Java8もちゃんと使う機会を増やすよう努める。(もうじき9が来るというのに…)
- 色即是空。空即是色。