PR

Java:String(文字列)の基本を1からわかりやすく

Java

JavaのStringは文字列を表すデータ型・・・というように想像・理解している方が多いかもしれません。

このページではその誤解を解きつつ、故にStringを扱うときにどのような注意点があるのか?Stringの正体は何か?について焦点を当てた解説を行います。

スポンサーリンク

「String はデータ型ではない」という事実

Javaにおける「データ型」―。これは厳密にはプリミティブ型(primitive type)を指しますが、Javaのプリミティブ型は以下8種類しか存在しません(参考 プリミティブ型/参照型)。

  1. boolean
  2. byte
  3. short
  4. int
  5. long
  6. float
  7. double
  8. char

上記をご覧いただければわかりますが、この中に今回の主題となるString は含まれておりません。対してcharはプリミティブ型として存在している点に注目してください。つまり

  • charデータ型(プリミティブ型)
  • Stringオブジェクト(参照型)

まずはここが大きな出発点です。Javaの世界において「文字列」は言語仕様レベルでは1つのデータ型ではなく、特別なクラス(java.lang.String)として扱われています。

なぜ String はプリミティブ型でないのか?

charは16ビットの値(UTF-16のコードユニット)を格納する1文字専用のデータ型です。しかし、現実世界で扱う文字列は複数文字の連続から成り、それらを安全かつ効率的に扱うためには、配列管理や文字コードの処理が必要になってきます。(現にC言語などでは、文字列は文字の配列として扱われます。)

これを配列でやるのではなく、クラス(オブジェクト)の仕組みを使ってより簡単に/直感的に管理できるようにしようというのがStringの基本思想です。

ポイント クラスであることで得られるメリット

  • メソッドを持てる
    length(), substring(), replace(), equals(), など、文字列操作を直感的に行える。
  • カプセル化
    内部の配列やコードポイントの取り扱いを隠蔽し、安全かつ簡潔に扱える。
  • イミュータブル化
    (後述します)Stringがもつ最大の特徴「イミュータブル(不変)」は、クラス設計を活かして実現しています。

String はイミュータブル

一度作られたStringオブジェクトの中身(文字列の内容)は変更できないという設計です。たとえば、str.replace("a", "b") を呼び出しても、オリジナルの str は変わらずに、新しい文字列オブジェクトが返ってきます。

ポイント イミュータブルなメリット

  1. 文字列プール(String Pool)の利用
    同じ内容の文字列リテラルがあれば、JVMがそれを再利用してメモリを節約します。
  2. スレッドセーフ
    どのスレッドからアクセスしても内容が変わらないため、ロックなしで安全に扱える。
  3. セキュリティ
    パスワード文字列やシステムプロパティなど、書き換えられると困る情報を安全に保持できる。

String ならではの注意点

== で比較しない

char はプリミティブ型なので、==で比較すれば「数値」としての等価比較ができます。しかし、String はオブジェクトなので、==で比較すると参照が同一かどうかを判定します。内容ではなく、実際に指し示すオブジェクトが同じかどうかの比較になります。

したがって、Java Silverでも頻出の「文字列比較問題」で失敗しやすいのがこれです。文字列の内容を比べたい場合はequals() を使わなければなりません。

char c1 = 'A';
char c2 = 'A';
System.out.println(c1 == c2); // true → 値('A'のコード=65)が同じなのでtrue

String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println(s1 == s2);      // false → 参照が異なる
System.out.println(s1.equals(s2)); // true  → 内容が同じ

文字列リテラルは同じオブジェクトを参照する場合がある

コードに直接 Stringリテラルを書いたとき、同一内容であれば同じオブジェクトを指すことが多いです。これがいわゆるString Poolの仕組みです。よって、次のようなケースは == でも true になり得ます。

String s3 = "Hello";
String s4 = "Hello";
System.out.println(s3 == s4); // true (プールの同じオブジェクトを参照する場合)

ただし、リテラル以外(例: new String("Hello"))では常に別オブジェクトを生成するので、==falseになりやすい。ここも資格試験でよく問われるポイントです。

String Poolをさらに詳しく

Java では、リテラル(参考 改めて「リテラル」とは?)で作成した文字列は自動的に String Pool(ヒープ内の特別な領域) に保存されます。

String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true(同じ参照を指す)

✅ ポイント: s1 と s2 は、同じ "Hello" を String Pool から取得しているため、== で true になる。

new String() を使うと?

String s3 = new String("Hello");
System.out.println(s1 == s3); // false(異なる参照)

new String("Hello") を使うと、ヒープ領域に新しい String インスタンスが作成されるため、String Pool とは異なるオブジェクトになります。

後述しますが、intern() を使うと String Pool の参照を取得するようになります。

String s4 = new String("Hello").intern();
System.out.println(s1 == s4); // true(String Pool の "Hello" を参照)

✅ ポイント:

  • s4 は intern() を呼び出すことで、String Pool 内の "Hello" を参照するようになる。
  • これにより、s1 == s4 は true になる。

StringBuilder / StringBuffer

Stringはイミュータブルであるが故に「連結」を頻繁に行うと、そのたびに新しいオブジェクトを作るため、パフォーマンスが落ちる場合があります。そこでJavaは、可変文字列を扱うStringBuilder(単一スレッド向け)や StringBuffer(スレッドセーフ)を提供しています。

StringBuilder sb = new StringBuilder();
sb.append("Java");
sb.append("Silver");
String result = sb.toString(); // "JavaSilver"

資格試験でも、「String をループ内で += しているコードが非効率」といった内容の設問がよく出ます。イミュータブルな特性を理解していれば、StringBuilderを使うべき理由がスッと頭に入るでしょう。

まとめ JavaのString

①文字列はオブジェクトである

  • Javaでは、String型はクラスとして定義されています。つまり、文字列は単なるデータではなく「Stringオブジェクト」です。
  • 文字列リテラル(例: "Hello")は、実はStringオブジェクトとして扱われます。
String str = "Hello"; // これは String オブジェクトを参照している

イミュータブル(不変)である

  • JavaのStringオブジェクトはイミュータブル(不変)という性質を持っています。一度作成されたStringオブジェクトの中身は変更できません。
  • 例えば、str.replace()str.substring()などの操作をしても、新しいStringオブジェクトを返すだけで、もとのStringは変更されません。

String Pool(文字列プール)

  • JavaにはString Poolと呼ばれる仕組みがあり、同じ内容の文字列リテラルは同じオブジェクトを参照します。
  • Stringリテラル("Hello"のようにソースコード上で直接書かれた文字列)を使った場合、同じ内容のリテラルが既にあれば、それと同じStringオブジェクトが使われます。
String s1 = "Java";
String s2 = "Java";

// s1 と s2 は同じオブジェクトを参照している可能性が高い(String Poolにより)
System.out.println(s1 == s2); // true

一方、newキーワードを使って文字列を生成すると、必ず別のオブジェクトが生成されます。

String s3 = new String("Java");
System.out.println(s1 == s3); // false

Stringクラスの主要メソッド

JavaのStringクラスは、文字列操作に関する多種多様なメソッドを提供しています。ここでは、よく使われる主要なメソッドとその使い方を、注意点を含めてわかりやすくまとめます。

ポイントは、Stringがイミュータブル(不変)であることを前提に、各メソッドが「元の文字列を変更せず、新しい文字列を返す」という点です。

長さや文字を取得するメソッド

length()

  • 概要: 文字列の長さ(char単位)を返す。
  • 使用例:
String str = "Hello";
int len = str.length();  // 5

Javaは内部的にUTF-16で文字を管理しているため、サロゲートペア(絵文字など)が含まれる場合、画面に見える「文字数」とlength()が返す値が異なることがあります。

charAt(int index)

  • 概要: 指定した位置の文字(char)を取得する。
  • 使用例:
String str = "Hello";
char c = str.charAt(1); // 'e'

インデックスは 0から始まります。範囲外を指定すると StringIndexOutOfBoundsException が発生します。

文字列比較系

equals(Object anObject)

  • 概要: 文字列の内容が同じかを比較する。最も基本的な文字列比較。
  • 使用例
String s1 = "Hello";
String s2 = new String("Hello");
System.out.println(s1.equals(s2));  // true (内容が同じかを判定)

何度か解説した通り == はオブジェクト参照が同一かどうかを比較するため、文字列内容の比較には常にequals()を使う点に注意しましょう

equalsIgnoreCase(String anotherString)

  • 概要: 大文字・小文字を区別せずに内容が同じかを比較する。
  • 使用例
String s1 = "Hello";
String s2 = "hello";
System.out.println(s1.equalsIgnoreCase(s2)); // true

完全にケースを無視するため、英字以外の言語や特殊なケース変換(ß → SSなど)では予期しない結果となることもあり得るので注意が必要です。

compareTo(String anotherString)

  • 概要: 辞書順で大小関係を比較する。返り値は負数・0・正数。
  • 使用例:
String s1 = "apple";
String s2 = "banana";
System.out.println(s1.compareTo(s2)); // 負の値(辞書順でappleはbananaより小さい)

部分文字列・位置検索系

substring(int beginIndex, int endIndex)

  • 概要: 文字列の一部を切り出して、新しい文字列を返す。endIndex最後の位置の1つ後ろを指定する。
  • 使用例:
String str = "HelloWorld";
String sub = str.substring(1, 5); // "ello" (1~4文字目)

範囲外を指定すると StringIndexOutOfBoundsException が発生します。また、元の文字列は変更されない(イミュータブル)。

indexOf(String str) / lastIndexOf(String str)

  • 概要: 指定した文字列や文字が、最初または最後に登場する位置を返す。見つからなければ -1
  • 使用例
String text = "Hello World";
int index = text.indexOf("o");      // 4 ("o"の最初の出現位置)
int last = text.lastIndexOf("o");   // 7 ("o"の最後の出現位置)

文字列の置換・分割系

replace(CharSequence target, CharSequence replacement)

  • 概要: 指定した文字列を、別の文字列に すべて 置き換える。
  • 使用例:
String str = "banana";
String replaced = str.replace("a", "o"); 
System.out.println(replaced); // "bonono"

イミュータブルゆえに、新しい文字列が返る。元の str は変化しません。
replaceAll(String regex, String replacement) は正規表現に対応し、注意が必要です。

split(String regex)

  • 概要: 正規表現regexを区切りとして、文字列を分割し、文字列配列を返す。
  • 使用例
String fruits = "apple,banana,cherry";
String[] arr = fruits.split(",");
// arr[0] = "apple", arr[1] = "banana", arr[2] = "cherry"

大文字・小文字変換系

toUpperCase() / toLowerCase()

  • 概要: 文字列をすべて大文字/小文字に変換して返す。
  • 使用例:
String str = "Hello World";
String upper = str.toUpperCase(); // "HELLO WORLD"
String lower = str.toLowerCase(); // "hello world"

前後の空白除去・判定系

trim()

  • 概要: 文字列の先頭と末尾の**空白文字(半角スペースやタブ、改行など)**を除去する。
  • 使用例
String str = "  Hello World  ";
String trimmed = str.trim(); // "Hello World"

真ん中のスペースは削除されません。Java 11以降には全Unicode空白を対象にするstrip()なども存在するが、trim()はASCII制御文字を中心に取り扱うのがポイントです。

isEmpty() / isBlank()(Java 11+)

  • 概要:
    • isEmpty()は文字列の長さが0かどうかを判定。
    • isBlank()空白文字のみで構成されている、または長さが0かどうかを判定(Java 11で追加)。
  • 使用例
String s1 = "";
System.out.println(s1.isEmpty()); // true
System.out.println(s1.isBlank()); // true

String s2 = "   ";
System.out.println(s2.isEmpty()); // false (空白があるので長さは3)
System.out.println(s2.isBlank()); // true

その他特殊メソッド

intern()

intern() メソッドは、String Poolに文字列を登録し、プール内の同じ内容の文字列を返します。
このメソッドを使用することで、同じ内容の文字列が複数存在する場合にメモリ使用量を削減し、== による比較が可能になります。

わかりやすく言えば、String Pool に文字列を登録し、既に同じ内容の文字列がある場合はそれを参照するようにするということ。

public class InternExample {
    public static void main(String[] args) {
        // 通常の文字列生成
        String str1 = new String("Hello");
        String str2 = new String("Hello");

        // 参照が異なるため false
        System.out.println(str1 == str2); 

        // intern() を使用してプールに登録
        String internedStr1 = str1.intern();
        String internedStr2 = str2.intern();

        // 参照が同じになるため true
        System.out.println(internedStr1 == internedStr2); 
    }
}
  • intern() を頻繁に使いすぎるとヒープ領域の String Pool に負荷がかかり、パフォーマンスが低下する可能性があります。
  • Java 7以降では、String Pool は ヒープ領域に移動したため、大量の intern() 呼び出しでメモリ不足(OutOfMemoryError) を引き起こす可能性があります。
タイトルとURLをコピーしました