Java Stringクラスは少し変わってる?




なんだかんだで一番使うのはString。
ところが、Stringクラスは他のクラスとは違って、ちょっと変わったところがあります。

どんなところが違うんでしょ?

※このお話は続編です。
先にこちらの2つを見てくださいませ。

Java ==とequalsは違う

2023.06.14

Java hashcodeメソッドってなに?

2023.07.14

Stringはイミュータブル

まず基本的なとこですが、Stringはイミュータブル。不変オブジェクトです。

このことはjavadocにこんな感じで書かれてます。

文字列は定数です。この値を作成したあとに変更はできません。 文字列バッファは可変文字列をサポートします。 文字列オブジェクトは不変であるため、共用することができます。
Java SE 17 API仕様 String(java.lang.String) より

javadocにある「共用することができる」とは、誰かに値を変えられたりしないから、みんなで安心して使えるよ。って意味です。
ま、それはともかく、値は変えられません。
ただ、不変オブジェクトは、何もStringに限った話ではなく、IntegerとかBigDecimalとか、けっこうあったりします。

というわけで、Stringは少し変わっているってお話でした。おしまい。。
ではありません。

Objectクラスとの不思議な関係

さらに基本に立ち戻ってObjectのクラスを見てみます。

Object#toString()はご存じですよね。
誰でも一度は使ったことありますよね。さらに言えば、これを書かずに暗黙的に利用していることすらあります。
まぁ暗黙的に使えるだけでも十分に怪しいわけですが。。

それは置いといて、こっちのjavadocはこんな感じ。

Objectクラスは、クラス階層のルートです。 すべてのクラスは、スーパー・クラスとしてObjectを持ちます。
Java SE 17 API仕様 Object(java.lang.Object) より

というわけで、javaにおけるObjectクラスは、全てのクラスの先祖ですね。

ですが、なにげなく使っているObject#toString()って、すごく変です。
Objectは先祖。なのに、このメソッドはStringを返します。つまり親でありながら、子のStringが使われているということになります。
つまり、ObjectとStringは循環参照のような関係にある。ってことです。

※Objectに登場するクラスは、StringとClassのみ。ほかはプリミティブ。
(Classは名前からして怪しいってことにして、ここでは放っておきますw)

んで、この話に乗れば、Objectを作った後に、Stringを作るわけで。親に子が出てくることはなかなか無いですよね。
ちょっと変ですね。

他のクラスでは当たり前にやることをやらないString

ちょっと趣向を変えて。
ここに、簡単なコードがあります。

誰でも書きますよね?

でも、Stringではなく、ふつうのクラスならこんな感じに書くはずです。

ふつうなら、こう書きます・・・よね?

ところが、Stringにおいては、この書き方をすると、少し違う意味になりますw
“abc”自体が、すでにオブジェクトだからです。

“abc”はオブジェクト?

“abc”がオブジェクトであることは、こんなふうに書くことができるってことでわかると思います。

見ての通り、”abc”がオブジェクトだからこそ、操作ができるってことです。

簡単でしたけど、オブジェクトな気がしてきましたか?
ってなわけで、文字列のリテラルに対して、さらにnew Stringなんていらない。ってことです。

逆に言うと、さっきのようにnew Stringをすれば、Stringオブジェクトで、Stringオブジェクトを作るってことになります。
このあと出てきますが、実際そんな感じです。

そんなこんなで、”abc”のリテラルでStringのオブジェクトが用意できてしまうわけで、Stringはふつうのクラスではない。どころではないですねw
まぁ文法上”abc”って書けないと文字列を書けなくて困るから。ってのもあるんでしょう。Javaの文字列とはStringのオブジェクトなんで。

文字列の同一性と同値性

そろそろ伏線を回収しにいく必要があるので、いったん過去に話題を遡りますw

オブジェクトが”同値”かどうかの比較は、equalsでした。
2つ前のお話のことです。遠い昔のようで懐かしいですね。

あらためて、動きを見てみます。
Stringもオブジェクトなんで、equalsで比較しますが、==も一緒にやっておきます。
とりあえずnewして、別のオブジェクトにしてあります。

実行結果はこうなります。

当然ですね。

それでは、これに追加で、こんな感じの呪文を唱えてみます。

実行結果はこうなります。

両方ともtrueですね。
==は、同一性の比較でしたので、「同一だった」ってことになります。
これはもちろん、追加した呪文のString#intern()のおかげですね。
このメソッドは、文字列オブジェクトの正準表現を返します。
このあと出てきますが「同一」な文字列オブジェクトを拾うってことです。

さらに不思議な挙動をするリテラル

もう1つだけ実験します。今度はリテラルです。
ただリテラルだけnewしてないのは不公平なんで、代わりに少しいじわるしてありますw

こちらの結果はこうなります。

なんということでしょう。呪文を唱えていないのに「同一」ですね。
何もせずとも「同一」だったってことですね。

VMレベルですら優遇されているString

2つの実験で、オブジェクトに対するintern操作、あるいは、そもそもリテラルであれば、同値なのはもちろん、同一なオブジェクトであることがわかりました。
しかし、同一とはどういうことでしょうか?

String#intern()で出てきましたが、正準表現ってのがポイントですね。
これのjavadocを見てみます。

public String intern()
文字列オブジェクトの正準表現を返します。
文字列のプールは、初期状態では空で、クラスStringによってプライベートに保持されます。

internメソッドが呼び出されたときに、equals(Object)メソッドによってこのStringオブジェクトに等しいと判定される文字列がプールにすでにあった場合は、プール内の該当する文字列が返されます。 そうでない場合は、このStringオブジェクトがプールに追加され、このStringオブジェクトへの参照が返されます。

したがって、任意の2つの文字列sとtについて、s.intern() == t.intern()がtrueになるのは、s.equals(t)がtrueの場合だけです。

すべてのリテラル文字列および文字列値定数式が保持されます。 文字列リテラルは、「Java言語仕様」のセクション3.10.5で定義されます。

戻り値:
この文字列と同じ内容だが、一意の文字列のプールからのものであることが保証されている文字列。
Java SE 17 API仕様 String#intern() より

ということで、javadocにもありますが、JVMには、文字列プールというものがあります。
ここには、文字通り、文字列がプールされていて再利用できるように保持しています。
一言で言えば、キャッシュです。
JDBCを知ってる方なら、コネクションプールのようなもんだ。なんてイメージするのが良いかもです。

んで、「正準表現」とは、「文字列プールにある、同値の文字列オブジェクト」のことであり、
「正準表現を返す」とは、「文字列プールにある、同値の文字列オブジェクトの参照を返す」という意味です。
よって、internで取得した文字列は、同じオブジェクト。ってことになります。

また、文字列のリテラルについては、何もせずとも「正準表現」が強制されます。
同値のリテラルであれば、いつでも同じオブジェクトです。
もちろん、リテラルを代入した変数でも同じです。

ところが、こうなるとみんなで同じものを利用するんで危険な感じがしてきますが、そうでもないんです。
それは、最初に書いた「イミュータブル」だから。ってことですね。

ちなみに、実験でnewしていたのは、これを回避したかったからです。
ですので、newすると、正準表現のオブジェクトではない、新しいオブジェクトになってしまうってことですね。

ということで、一番身近な存在が一番遠い存在だった。的なお話でした。
変わっているのは少しどころじゃなかったですねw