String クラスは「特別扱い」されているクラス
String は、Java で文字列を扱うためのクラスです。
でも、ただのクラスではなく「言語レベルで特別扱い」されている存在です。
ダブルクォーテーション "..." で書いたものは全部 String
import しなくても最初から使える
ほとんどのプログラムで必ず登場する
なので、String の特性をしっかり理解しておくと、
コードの読みやすさ・バグの少なさ・性能の面でかなり差が付きます。
ここでは、初心者のうちに押さえておくべきポイントに絞って、
例と一緒に丁寧に解説していきます。
特性1:String は「イミュータブル(不変)」である(最重要)
一度作ったら中身は絶対に変わらない
String の一番大事な特性は「不変(immutable)」です。
一度作られた String の中身(文字列)は、絶対に変わりません。
コードで確かめてみます。
String s = "hello";
s.toUpperCase();
System.out.println(s); // 何が出る?
JavatoUpperCase() を呼んだので "HELLO" になると思うかもしれませんが、実際の出力は
hello
です。
toUpperCase() は、元の String を書き換えるのではなく、
「大文字にした新しい String を返す」メソッドだからです。
正しい使い方はこうです。
String s = "hello";
String upper = s.toUpperCase();
System.out.println(upper); // HELLO
System.out.println(s); // hello(元はそのまま)
Javaこの「変更系メソッドに見えても、全部“新しい String を返す”だけ」という性質は、
すべての String メソッドに共通です。
replace
substring
concat
trim
など、全部「元の s は変えず、新しいインスタンスを返す」です。
なぜ不変だとうれしいのか(少し深掘り)
不変であることには、たくさんのメリットがあります。
スレッドセーフ
複数のスレッドから同じ String を読んでも、安全に使える。
「途中で誰かに中身を書き換えられる心配がない」。
hashCode の再計算が不要
equals / hashCode の振る舞いが安定しているので、HashMap<String, ...> や HashSet<String> でキーに使いやすい。
参照を共有しても安全
同じ String インスタンスをいろんな場所で共有しても、
誰かが中身を変えて他に影響を与える、ということが起きない。
この「不変である」という特性を理解しておくだけで、String の扱い方に迷わなくなります。
特性2:文字列リテラルと String プール
同じ文字列リテラルは実は「共有されている」
次のコードを見てください。
String a = "hello";
String b = "hello";
System.out.println(a == b); // true になる
System.out.println(a.equals(b)); // true
Java== が true になっているのがポイントです。== は「同じインスタンスか?」を見る演算子なので、a と b は同じオブジェクトを参照しています。
Java には「String プール(intern プール)」という仕組みがあり、
同じ文字列リテラル "hello" は一度だけ作って、後は共有します。
これは「メモリの節約」や「比較の高速化」のための最適化です。
new String(…) を使うと話が変わる
一方で、こう書くとどうでしょう。
String a = "hello";
String b = new String("hello");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
Javanew String("hello") は、文字列プールとは別に新しいインスタンスを作ります。
だから == では false になります。
初心者がよくハマるポイントですが、
「文字列の等価比較には == ではなく equals を使う」が鉄則になります。
特性3:== と equals の違い(ここもかなり重要)
String の比較でやってはいけないこと
やってはいけない比較の例です。
String s1 = new String("abc");
String s2 = new String("abc");
if (s1 == s2) {
System.out.println("同じ");
} else {
System.out.println("違う");
}
Java出力はほぼ確実に
違う
です。== は「同じインスタンスか?」を見るので、new String した時点で別オブジェクトです。
正しくは equals で比較する
文字列の内容(値)を比較したいなら、equals を使います。
if (s1.equals(s2)) {
System.out.println("同じ文字列です");
}
Javaそして、null 安全に書きたいときは、
「null にならない側」から equals を呼ぶのが定番です。
String s = null;
if ("abc".equals(s)) {
System.out.println("一致");
}
Javaこれなら、s が null でも NullPointerException になりません。
特性4:文字列連結とパフォーマンス(StringBuilder の話)
文字列連結の基本と落とし穴
Java では + で文字列を連結できます。
String message = "Hello, " + name + " さん";
Javaこれはシンプルで読みやすいです。
小さなコードではこれで十分です。
ただし、ループの中で大量に + 連結をすると、
毎回新しい String が作られるため、パフォーマンスが悪くなります。
例えば:
String s = "";
for (int i = 0; i < 10000; i++) {
s = s + i;
}
Javaこの書き方は、
毎回 s + i で新しい String を生成する → 古いものは捨てる
という動作を何千回も繰り返します。
大量連結は StringBuilder を使う
大量連結をする場合は、StringBuilder を使うのが定番です。
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();
JavaStringBuilder は可変のバッファを持っていて、
中身を書き換えながら文字列を構築できます。
最後に toString() で String に変換します。
初心者のうちは
ちょっとした連結 → + でOK
ループなどで大量連結 → StringBuilder を使う
という感覚を持っておけば十分です。
特性5:length と char 型、substring の基本
length は「文字数」、charAt は「1文字」
String.length() は、文字列の長さ(文字数)を返します。
String s = "abc";
System.out.println(s.length()); // 3
Java1文字取り出したいときは charAt を使います。
char c = s.charAt(0); // 'a'
Java配列と同じで、0 から始まるインデックスです。
部分文字列を取り出す substring
substring で一部だけ取り出せます。
String s = "abcdef";
String sub1 = s.substring(2); // "cdef"(2番目以降)
String sub2 = s.substring(1, 4); // "bcd"(1〜3番目)
Javasubstring(beginIndex, endIndex) はbeginIndex 番目から、endIndex - 1 番目まで
というイメージです。
特性6:String は配列や Map のキーとして「超優秀」
不変で hashCode が安定している
String は不変なので、一度作ったら hashCode が変わりません。
そのため、HashMap<String, ...> や HashSet<String> のキーに非常によく使われます。
例えば:
Map<String, Integer> scores = new HashMap<>();
scores.put("taro", 80);
scores.put("hanako", 95);
System.out.println(scores.get("taro")); // 80
JavaString の equals / hashCode は Java 本体側でしっかり実装されているので、
自分で何も考えずにキーに使って大丈夫です。
自作クラスでこれをやろうとすると、
equals / hashCode 契約を守る必要が出てきますが、
String はそのあたりを全部面倒見てくれています。
ちょっとだけ:文字コード(エンコーディング)の話
String は「文字」であって「バイト列」ではない
String は「文字列」を表すクラスであって、
「バイト列」ではありません。
ファイルやネットワークに送るときは、String をバイト列に変換する必要があります。
String s = "こんにちは";
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
Java逆に、バイト列から文字列にするには
String s2 = new String(bytes, StandardCharsets.UTF_8);
Javaのように、エンコーディング(UTF-8 など)を明示するのが大事です。
ここは少しレベルが上がりますが、
「画面に出す文字列(String)」と「通信やファイルのバイト列」は別物
という意識だけ持っておくと、後々混乱しにくくなります。
まとめ:String の特性をどう活かすか
初心者として、まず意識しておきたい String の特性はこのあたりです。
イミュータブル(不変)なので、変更系メソッドは全部「新しい String を返す」== ではなく equals で中身を比較する(特にリテラルと new の違いに注意)
大量連結では StringBuilder を使う
不変で hashCode が安定しているので、Map / Set のキーに向いている
「文字列は当たり前に使うもの」だからこそ、
ここをちゃんと抑えておくと、他のクラスの理解もスムーズになります。
