🍇

16.1 文字列を表すクラスの特徴とAPI(Stringクラス、StringBuilderクラスなど)~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

16.1 文字列を表すクラスの特徴とAPI

チャプターの概要

このチャプターでは、Stringクラスを中心に、文字列を表すクラスの特徴とそのAPIについて学びます。

16.1.1 Stringクラスの特徴

Stringクラスの特徴

これまで何度も見てきたように、Javaでは文字列をjava.lang.Stringクラスによって表現します。このレッスンでは、Stringクラスの以下のような特徴を説明します。

  1. イミュータブルである
  2. メモリ上で再利用される

特徴(1):イミュータブルである

Stringクラスはイミュータブル(不変)であり、一度オブジェクトを生成したら、その後は値の変更ができない点が大きな特徴です。イミュータブルであることにより、「参照の値渡し」の副作用(レッスン13.1.3参照)や「マルチスレッド環境における不正な更新」(『Java Advanced編』参照)といった問題を回避することができます。
それでは、Stringクラスの挙動を具体的に見ていきましょう。まず以下のコードを見てください。

snippet_1 (pro.kensait.java.basic.lsn_16_1_1.Main)
String str = "foo"; //【1】
str = "bar"; //【2】

このコードを一見すると「String型変数strの値が"foo"から"bar"に書き換わった」と見えるかもしれませんが、そうではありません。前述したようにStringクラスはイミュータブルなため、値の変更はできません。内部的な挙動としては、文字列リテラル"foo"をString型変数strに代入する【1】と、値そのものが代入されるのではなく、割り当てられた文字列への「参照」が代入されます。
次に文字列リテラル"bar"を代入する【2】と、変数strが保持する「参照」が"bar"への「参照」に書き換わり、それまで保持していた"foo"への「参照」が切れる、という挙動をします。

【図16-1-1】String型変数の書き換え時の挙動
image.png

今度は以下のコードを見てください。

snippet_2 (pro.kensait.java.basic.lsn_16_1_1.Main)
String str1 = "foo";
String str2 = str1; //【1】
str2 = "bar"; //【2】

このコードでは、まず文字列"foo"を値として持つ変数str1をstr2に代入しています【1】。この時点で、変数str1とstr2は、同じ文字列"foo"への「参照」を持っています。
次に代入先であるstr2に、新しい文字列"bar"を代入します【2】。既出の内容で触れたとおり、通常、参照型変数は実データのメモリ上の配置場所を表すため、代入先変数を書き換えると連動して代入元変数も変更されます。ところがStringクラスの挙動は異なります。変数str2に文字列"bar"への「参照」が代入されると、str1とは「参照」が泣き別れになるため、str1の値は"foo"のままになるのです。

【図16-1-2】String型変数の代入先書き換え時の挙動
image.png

特徴(2):メモリ上で再利用される

Stringクラスのもう1つの大きな特徴は、実データの再利用にあります。
リテラルによって文字列を表記すると、実メモリ上にデータが割り当てられますが、すでに同じ文字列が存在していた場合はそのデータが再利用されます。
具体的な挙動をコードで説明します。

snippet_3 (pro.kensait.java.basic.lsn_16_1_1.Main)
String str1 = "foo"; //【1】
String str2 = "foo"; //【2】
boolean flag = str1 == str2; //【3】どうなる?

このようにString型変数str1とstr2を別々に宣言し、それぞれに対して文字列リテラル"foo"を代入しています【1、2】。変数str1とstr2は別々に宣言されているため、両者は別のオブジェクトであるように感じるかもしれません。ところがstr2に"foo"を代入したとき【2】、すでに存在する文字列"foo"が再利用されるため、変数str1とstr2は同じデータへの「参照」を持つことになります。従ってこの同一性判定【3】の結果はtrueになります。
さてここまでは文字列リテラルを直接変数に代入するケースを見てきましたが、Stringクラスにはコンストラクタがあり、new演算子によって文字列を生成することも可能です。ただしこのようにすると、前述した文字列の再利用は行われません。以下のコードを見てください。

snippet_4 (pro.kensait.java.basic.lsn_16_1_1.Main)
String str1 = new String("foo"); //【1】
String str2 = new String("foo"); //【2】
boolean flag = str1 == str2; //【3】どうなる?

String型変数str1とstr2に対して、new演算子によって生成した文字列を代入しています【1、2】。この場合、文字列の再利用は行われず、変数str1とstr2は別のオブジェクトとなるため、この同一性判定【3】の結果はfalseになります。

このようにStringオブジェクトには、文字列リテラルを直接記述する方法と、new演算子を使う方法と、2つの生成方法がありますが、再利用の恩恵を受けるために、特別な理由がない限りは前者の方法(文字列リテラル)を使うようにしましょう。
またどちらの方法を使うかによって、同一性の判定結果が異なります。従って「文字列の値が同じであること(等価性)」を判定するためには、==演算子ではなく、equals()メソッドを使用するようにしてください。

Stringクラスの必然性

前項で説明したように、Stringクラスには、イミュータブルである、そして同じ文字列はメモリ上で再利用される、という特徴があります。
このような仕様になっている理由は、メモリリソースへの影響を極小化することにあります。文字列はアプリケーションの中でも特に多用されるデータ型であり、むやみやたらと文字列が生成されると、メモリリソースを圧迫する可能性があるためです。
またメモリ上で再利用される点と、イミュータブルである点には関連性があります。というのも、仮にStringクラスがミュータブルであり、データの書き換えが可能だとすると、ある変数の変更が他の変数に影響を及ぼしてしまうことになるため、同一の文字列を様々な変数が「参照」するわけにはいかなくなります。つまりミュータブルな仕様では、再利用性を確保することは困難です。Stringクラスはイミュータブルであるがゆえに、メモリ上での再利用を可能にしているのです。

【図16-1-3】Stringクラスの必然性
image.png

16.1.2 StringクラスのAPI

StringクラスのAPI全体像

Stringクラスには、文字列を操作するための豊富なAPIが備わっています。このレッスンでは、その中から特に利用頻度の高いものをピックアップし、説明します。
Stringクラスの主要はAPIは、以下のように分類されます。

分類 API(メソッド)
文字列の情報を取得するためのAPI length()
文字列を変換するためのAPI toUpperCase()、toLowerCase()、concat()、trim()
文字列同士を比較するためのAPI equals()、equalsIgnoreCase()、compareTo()、compareToIgnoreCase()
文字列が条件を満たしているか判定するためのAPI contains()、startsWith()、endsWith()、isEmpty()、isBlank()
部分文字列を検索するためのAPI indexOf()、lastIndexOf()
部分文字列を取得するためのAPI substring()
文字列を分割・結合するためのAPI split()、join()
文字列のマッチングを判定するためのAPI matches()*
文字列を置換するためのAPI replace()、replaceAll()*、replaceFirst()*
文字列をフォーマッティングするためのAPI format()
プリミティブ型の文字列表現を返すAPI valueOf()

これらのAPIの中で、*が付いているものは、正規表現によるパターンマッチングを活用したもののため、詳細はチャプター16.2で取り上げます。
それ以外のAPIの使用方法を、以下にまとめて示します。幾つかのAPIの引数に、CharSequenceインタフェースがありますが、FQCNはjava.lang.CharSequenceです。このインタフェースは、Stringクラスや、後述するStringBuilderクラスなどが実装しているため、String型と読み替えても差し支えはありません。
なおsplit()メソッドも正規表現が利用できますが、必ずしも正規表現が前提とは限らないため、このレッスンで紹介します。

API(メソッド) 説明
int length() この文字列の長さを返す。
String toUpperCase() この文字列のすべての文字を大文字に変換して返す。
String toLowerCase() この文字列のすべての文字を小文字に変換して返す。
String concat(String) この文字列の最後に指定された文字列を連結して返す。
String trim() この文字列の先頭と末尾のすべての空白を削除して返す。
boolean equals(Object) この文字列と指定されたオブジェクトの等価性を判定して返す。指定されたオブジェクトがString型以外の場合は、falseを返す。
boolean equalsIgnoreCase(String) この文字列と指定された文字列の等価性を、大文字と小文字を区別せずに判定して返す。
int compareTo(String) この文字列と指定された文字列を辞書的に比較する。比較の結果、自身の方が比較対象よりも前の場合は負の値を、後の場合は正の値を、等しい場合は0を、それぞれ返す。
int compareToIgnoreCase(String) この文字列と指定された文字列を、大文字と小文字の区別せずに辞書的に比較する。返す値はcompareTo()メソッドと同様。
boolean contains(CharSequence) この文字列が指定されたCharSequence(文字列)を含む場合、trueを返す。
boolean startsWith(String) この文字列が、指定された接頭辞で始まるかどうかを判定して返す。
boolean startsWith(String, int) この文字列の指定されたインデックス(0始まり)以降の部分文字列が、指定された接頭辞で始まるかどうかを判定して返す。
boolean endsWith(String) この文字列が、指定された接尾辞で終わるかどうかを判定して返す。
boolean isEmpty() この文字列が空(長さ0)の場合にtrueを返す。
boolean isBlank() この文字列が空(長さ0)か、空白のみが含まれる場合はtrueを戻し、それ以外の場合はfalseを返す。
int indexOf(String) この文字列内で、指定された部分文字列が最初に出現する位置のインデックス(0始まり)を返す。
int indexOf(String, int) 指定されたインデックス(0始まり)以降で、指定された部分文字列が、この文字列内で最初に出現する位置のインデックスを返す。
int lastIndexOf(String) この文字列内で、指定された部分文字列が最後に出現する位置のインデックス(0始まり)を返す。
int lastIndexOf(String, int) この文字列内で、指定された部分文字列が最後に出現する位置のインデックス(0始まり)を返す。検索は指定されたインデックスから開始され、先頭方向に行われる。
String substring(int) この文字列から部分文字列を切り出して返す。部分文字列は、指定されたインデックス(0始まり)から文字列の最後までになる。
String substring(int, int) この文字列から部分文字列を切り出して返す。部分文字列は、第一引数の開始インデックス(0始まり)から第二引数の終了インデックス(含まず)の間で、切り出される。
String[] split(String) この文字列を、指定された文字列(または正規表現)に一致する位置で分割し、配列にして返す。
static String join(CharSequence, CharSequence...) 第一引数の区切り文字を使用して、それ以降の引数に指定された文字列を結合し、生成された新しい文字列を返す。
String replace(CharSequence, CharSequence) この文字列を、第一引数の文字列から第二引数の文字列に置換した、新しい文字列を返す。
static String format(String, Object...) 指定された書式文字列に引数の値を埋め込んで、フォーマッティングされた新しい文字列を返す。
static String valueOf(boolean) 指定されたboolean型引数の文字列表現を返す。
static String valueOf(char) char型引数の文字列表現を返す。
static String valueOf(char[]) char型配列引数の文字列表現を返す。
static String valueOf(int) int型引数の文字列表現を返す。
static String valueOf(long) long型引数の文字列表現を返す。
static String valueOf(float) float型引数の文字列表現を返す。
static String valueOf(double) double型引数の文字列表現を返す。

文字列の情報を取得するためのAPI

ここでは、文字列の情報を取得するためのAPIについて説明します。
この分類に属するAPIは、length()メソッドです。length()メソッドを呼び出すと、文字列の長さを取得することができます。

snippet (pro.kensait.java.basic.lsn_16_1_2.Main_Length)
String str = "Foo_Bar_Foobar";
int length = str.length(); // 14

文字列を変換するためのAPI

ここでは、文字列を変換するためのAPIについて説明します。前述したようにStringクラスはイミュータブルなため、APIによって自身の状態を変化させることはできません。例えばtoUpperCase()メソッドは、すべての文字を大文字に変換するAPIですが、以下のようにしても、変数strは何も書き換わりません。

snippet_1 (pro.kensait.java.basic.lsn_16_1_2.Main_Conversion)
String str = "Foo_Bar_Foobar";
str.toUpperCase(); // 何も変化なし!

toUpperCase()メソッドの戻りがString型になっていることに注意してください。toUpperCase()メソッドを呼び出すと、自身が変化するのではなく、大文字化した新しい文字列が生成され、それが戻り値として返されます。従って、正しくは以下のようにする必要があります。

snippet_2 (pro.kensait.java.basic.lsn_16_1_2.Main_Conversion)
String str1 = "Foo_Bar_Foobar";
String str2 = str1.toUpperCase(); // "FOO_BAR_FOOBAR"

次にtoLowerCase()メソッドです。toLowerCase()メソッドを呼び出すと、すべての文字を小文字に変換することができます。

snippet_3 (pro.kensait.java.basic.lsn_16_1_2.Main_Conversion)
String str1 = "Foo_Bar_Foobar";
String str2 = str1.toLowerCase(); // "foo_bar_foobar"

続いてconcat()メソッドです。concat()メソッドを呼び出すと、指定された文字列を連結することができます。

snippet_4 (pro.kensait.java.basic.lsn_16_1_2.Main_Conversion)
String str1 = "Foo_Bar_Foobar";
String str2 = "@mail.kensait.pro";
String str3 = str1.concat(str2); // "Foo_Bar_Foobar@mail.kensait.pro"

変数str3には、2つの文字列が連結された結果"Foo_Bar_Foobar@mail.kensait.pro"が代入されます。

最後にtrim()メソッドです。trim()メソッドを呼び出すと、先頭と末尾のすべての空白を削除することができます。

snippet_5 (pro.kensait.java.basic.lsn_16_1_2.Main_Conversion)
String str1 = " Foo Bar Foobar   ";
String str2 = str1.trim(); // "Foo Bar Foobar"

文字列同士を比較するためのAPI

ここでは、文字列同士を比較するためのAPIについて説明します。equals()メソッドは既出なのでここでは割愛し、equalsIgnoreCase()メソッドから説明します。equalsIgnoreCase()メソッドを呼び出すと、文字列の等価性を、大文字と小文字を区別せずに判定することができます。

snippet_1 (pro.kensait.java.basic.lsn_16_1_2.Main_Compare)
String str1 = "Foo_Bar_Foobar";
String str2 = "foo_bar_fooBAR";
boolean result = str1.equalsIgnoreCase(str2); // true

大文字と小文字を区別せずに"Foo_Bar_Foobar"を比較するため、変数resultにはtrueが代入されます。

次にcompareTo()メソッドです。compareTo()メソッドを呼び出すと、2つの文字列を辞書的に比較することができます。比較の結果、自身の方が比較対象よりも前の場合は負の値が、後の場合は正の値が、等しい場合は0が返されます。

snippet_2 (pro.kensait.java.basic.lsn_16_1_2.Main_Compare)
String str1 = "foo";
String str2 = "bar";
int result = str1.compareTo(str2); // 正の値

自身つまり"foo"の方が、比較対象"bar"よりも後になるため、変数resultには正の値が代入されます。

最後にcompareToIgnoreCase()メソッドです。compareToIgnoreCase()メソッドを呼び出すと、2つの文字列を辞書的に比較することができます。compareTo()メソッドとは異なり、大文字と小文字が区別されません。

snippet_3 (pro.kensait.java.basic.lsn_16_1_2.Main_Compare)
String str1 = "foo";
String str2 = "Foo";
int result = str1.compareToIgnoreCase(str2); // 0

大文字と小文字を区別せずに辞書順で比較するため、変数resultには0が代入されます。

文字列が条件を満たしているか判定するためのAPI

ここでは、文字列が何らかの条件を満たしているかを判定し、その結果をboolean型で返すAPIについて説明します。
まずはcontains()メソッドです。contains()メソッドを呼び出すと、指定された文字列を含むかどうかを判定することができます。

snippet_1 (pro.kensait.java.basic.lsn_16_1_2.Main_Condition)
String str1 = "Foo_Bar_Foobar";
String str2 = "Bar";
boolean result = str1.contains(str2); // true

大文字と小文字を区別せずに"Foo_Bar_Foobar"を比較するため、変数resultにはtrueが代入されます。

次にstartsWith()メソッドです。startsWith()メソッドを呼び出すと、文字列が指定された接頭辞で始まるかどうかを判定することができます。

snippet_2 (pro.kensait.java.basic.lsn_16_1_2.Main_Condition)
String str1 = "Foo_Bar_Foobar";
String str2 = "Foo";
boolean result = str1.startsWith(str2); // true

文字列"Foo_Bar_Foobar"は"Foo"から開始するため、変数resultにはtrueが代入されます。

続いてendsWith()メソッドです。endsWith()メソッドを呼び出すと、startsWith()メソッドとは逆に、文字列が指定された接尾辞で終わるかどうかを判定することができます。

snippet_3 (pro.kensait.java.basic.lsn_16_1_2.Main_Condition)
String str1 = "Foo_Bar_Foobar";
String str2 = "bar";
boolean result = str1.endsWith(str2); // true

文字列"Foo_Bar_Foobar"は末尾が"bar"で終わるため、変数resultにはtrueが代入されます。

次にisEmpty()メソッドです。isEmpty()メソッドを呼び出すと、文字列が空(長さ0)かどうかを判定することができます。

snippet_4 (pro.kensait.java.basic.lsn_16_1_2.Main_Condition)
String str = "";
boolean result = str.isEmpty(); // true

この文字列は空文字のため、変数resultにはtrueが代入されます。

最後にisBlank()メソッドです。isBlank()メソッドを呼び出すと、文字列が空(長さ0)あるいは空白のみが含まれるかを判定することができます。

snippet_5 (pro.kensait.java.basic.lsn_16_1_2.Main_Condition)
String str = "   ";
boolean result = str.isBlank(); // true

この文字列はブランクのみで構成されるため、変数resultにはtrueが代入されます。

部分文字列を検索するためのAPI

ここでは、部分文字列を検索するためのAPIについて説明します。まずはindexOf()メソッドです。indexOf()メソッドを呼び出すと、指定された部分文字列が最初に出現する位置のインデックスを取得することができます。なお文字列のインデックスとは、文字列内の位置情報で、左端を0として数えます。

snippet_1 (pro.kensait.java.basic.lsn_16_1_2.Main_Index)
String str1 = "Foo_Bar_Foobar";
String str2 = "ar";
int result = str1.indexOf(str2); // 5

文字列"Foo_Bar_Foobar"の中に、"ar"は2か所登場しますが、最初に出現する方に注目してください。左端を0として数えると、arが登場する位置は5、ということになります。
次にlastIndexOf()メソッドです。以下のようにlastIndexOf()メソッドを呼び出すと、指定された部分文字列が最後に出現する位置のインデックスを取得することができます。

snippet_2 (pro.kensait.java.basic.lsn_16_1_2.Main_Index)
String str1 = "Foo_Bar_Foobar";
String str2 = "ar";
int result = str1.lastIndexOf(str2); // 12

先のコードと同じように、文字列"Foo_Bar_Foobar"の中における部分文字列"ar"のインデックスを取得しようとしてます。lastIndexOf()メソッドでは最後に登場する"ar"の位置を取得しますので、左端を0として数えると最後にarが登場する位置は12、ということになります。

部分文字列を取得するためのAPI

ここでは、部分文字列を取得するためのAPIについて説明します。この分類に属するAPIは、substring()メソッドです。substring()メソッドを呼び出すと、指定された部分文字列を切り出して取得することができます。切り出される部分文字列は、指定されたインデックス(0始まり)から文字列の最後までになります。

snippet_1 (pro.kensait.java.basic.lsn_16_1_2.Main_Substring)
String str1 = "Foo_Bar_Foobar";
String str2 = str1.substring(4); // "Bar_Foobar"

またsubstring()メソッドには文字列の開始インデックスと終了インデックスを指定することができます。このとき終了インデックスはその位置を含まないため、注意してください。

snippet_2 (pro.kensait.java.basic.lsn_16_1_2.Main_Substring)
String str1 = "Foo_Bar_Foobar";
String str2 = str1.substring(4, 7); // "Bar"

このコードでは、文字列"Foo_Bar_Foobar"において、インデックス7の位置ある「2度目のアンダースコア」の前の文字までを切り出すため、変数str2には"Bar"が代入されます。

文字列を分割・結合するためのAPI

ここではStringクラスのAPIの中で、文字列を分割・結合するためのAPIについて説明します。まずはsplit()メソッドです。split()メソッドを呼び出すと、指定された文字列に一致する位置で分割し、それを配列として取得することができます。split()メソッドでは、分割対象の文字列に正規表現を利用可能ですが、ここでは通常の文字列を指定しています。

snippet_1 (pro.kensait.java.basic.lsn_16_1_2.Main_SplitJoin)
String str = "foo,bar,baz";
String[] strArray = str.split(","); // 配列{foo, bar, baz}

変数strArrayには、foo, bar, bazからなる配列が代入されます。

次にjoin()メソッドです。join()メソッドはスタティックメソッドで、第一引数の区切り文字を使用して、それ以降の引数に指定された文字列(配列または可変引数)を結合することができます。

snippet_2 (pro.kensait.java.basic.lsn_16_1_2.Main_SplitJoin)
String[] strArray = {"foo", "bar", "baz"};
String str = String.join("#", strArray); // "foo#bar#baz"

変数strには、文字列"foo#bar#baz"が代入されます。

文字列を置換するためのAPI

ここではStringクラスのAPIの中で、文字列を置換するためのAPIについて説明します。この分類に該当するAPIには、replace()メソッド、replaceAll()メソッド、replaceFirst()メソッドなどがありますが、replaceAll()とreplaceFirst()は正規表現の利用が前提になるため、ここではreplace()メソッドのみを取り上げます。
replace()メソッドを呼び出すと、文字列内の指定された文字列を指定された別の文字列に置換することができます。

snippet (pro.kensait.java.basic.lsn_16_1_2.Main_Replace)
String str1 = "Foo_Bar_Foobar";
String str2 = str1.replace("Foo", "Hoge"); // "Hoge_Bar_Hogebar"

変数strには、文字列"Hoge_Bar_Hogebar"が代入されます。

プリミティブ型の文字列表現を返すAPI

ここでは、プリミティブ型の文字列表現を返すAPIについて説明します。この分類に属するAPIはvalueOf()メソッドですが、プリミティブ型の種類に応じてオーバーロードされています。valueOf()メソッドはスタティックメソッドで、指定されたプリミティブ型変数の文字列表現を取得することができます。

snippet (pro.kensait.java.basic.lsn_16_1_2.Main_ValueOf)
String str1 = String.valueOf(1234567L);
System.out.println(str1); // "1234567"
String str2 = String.valueOf(123.4567);
System.out.println(str2); // "123.4567"

16.1.3 文字列をフォーマッティングするためのAPI

文字列のフォーマッティング

このレッスンでは、StringクラスのAPIの中で、文字列をフォーマッティングするためのAPIについて説明します。
この分類に属するAPIはformat()メソッドです。このメソッドはスタティックメソッドで、指定された書式文字列に引数を埋め込んで、フォーマッティングされた文字列を生成することができます。このメソッドに指定する書式文字列には、「書式指定子」を埋め込むことができます。
書式指定子とは文字列の中に値を埋め込むための記法で、以下のように記述します。

  • %型 … 基本的な記法
  • %[インデックス]型 … インデックス(1$、2$…)を明示する場合
  • %[文字数]型 … 埋め込む文字列の長さを明示する場合(正数だと左詰め、負数だと右詰め)
  • %[桁数]型 … 埋め込む数値の桁数を明示する場合

指定可能な型には多くの種類がありますが、比較的利用頻度の高いものを以下に示します。

代入する値
s、S 文字列
d 10進数
x、X 16進数
o 8進数
f 浮動小数点(整数部+小数部)
e、E 浮動小数点(指数表現)
n プラットフォーム固有の行区切り文字

format()メソッドの具体的な使用方法

それでは、format()メソッドの使用方法を具体的に見ていきましょう。以下のコードを見てください。

snippet_1 (pro.kensait.java.basic.lsn_16_1_3.Main)
String str = String.format("私は%s、%d歳です。", "Alice", 25);
System.out.println(str); // "私はAlice、25歳です。"

format()メソッドの第一引数である書式文字列を見ると、書式指定子として、最初に%s、二番目に%dという記述があることが分かります。この部分に可変引数として指定された引数の値が、順番に埋め込まれていきます。すなわち最初の%sには文字列"Alice"が、二番目の%dには10進数25が、それぞれ埋め込まれ、結果として"私はAlice、25歳です。"という文字列が生成されます。

【図16-1-4】format()メソッド
image.png

続いてインデックスを指定する記述方法です。

snippet_2 (pro.kensait.java.basic.lsn_16_1_3.Main)
String str = String.format("彼女は%1$sです。%1$sは%2$d歳です。", "Alice", 25);
System.out.println(str); // "彼女はAliceです。Aliceは25歳です。"

書式文字列を見ると、%1$s%2$dといった具合に、%と型との間に1$2$といったインデックスが指定されています。1$は1番目のインデックスを表すため、文字列"Alice"が、2$は2番目のインデックスを表すため、10進数25が、それぞれ埋め込まれます。このコードのように同じ引数(ここでは"Alice")を複数個所に埋め込みたい場合は、インデックスを利用すると良いでしょう。

続いて埋め込まれた部分の文字数を指定する方法です。

snippet_3 (pro.kensait.java.basic.lsn_16_1_3.Main)
String str = String.format("私は%-7s、%05d歳です。", "Alice", 25);
System.out.println(str); // "私はAlice  、00025歳です。"

このように%と、型であるsdの間に文字数を指定します。正数を指定した場合は右詰めになり、負数を指定した場合は左詰めになります。詰められた残りの部分は、基本的に空白になります。ただし数値の場合は05dのように頭に0を付けると、残りの部分は0で埋まります。

最後に浮動小数点の桁数を指定する方法です。

snippet_4 (pro.kensait.java.basic.lsn_16_1_3.Main)
String str = String.format("私の身長は、%.2fcmです。", 165.5);
System.out.println(str); // "私の身長は、165.50cmです。"

このように%と型であるfとの間に、.2といった具合に小数点以下の桁数を指定します。すると引数である165.5が、小数点2桁までの表記で"165.50"という文字列として埋め込まれます。

テキストブロックへの変数埋め込み

複数行からなる文字列をテキストブロック(レッスン3.2)で表現し、それを一種のテンプレートと見なして、後から特定の場所に任意の変数を埋め込みたい、というケースがあります。そのような場合も、このフォーマッティング機能によって変数を埋め込むと良いでしょう。
具体例を以下に示します。

snippet (pro.kensait.java.basic.lsn_16_1_3.Main_TextBlock)
String template = """
%s様
キャンペーンのお知らせ
現在%sが通常価格の%d%%オフで購入可能です!
""";
String message1 = String.format(template, "Alice", "赤ワイン", 20);
System.out.println(message1);
String message2 = String.format(template, "Bob", "白ワイン", 30);
System.out.println(message2);

このように典型的なメール文をテキストブロックによってテンプレート化し、宛先に応じて異なる文字列をメール文に埋め込めば、繰り返し処理によって効率的にメールを作成することができるでしょう。

16.1.4 StringBuilderクラスによる文字列連結

Stringクラスの課題

このレッスンでは、java.lang.StringBuilderクラスを取り上げます。
このクラスの必要性を説明する前に、まずはStringクラスの課題を理解する必要があります。前述したようにStringクラスには、イミュータブルである、そして同じ文字列はメモリ上で再利用される、という特徴があります。この特徴を踏まえたときに、文字列を繰り返し連結するような処理を行うと、何が起きるでしょうか。
以下のコードでは、ループ処理によって"A"を10万回連結する処理を行っています。

snippet_1 (pro.kensait.java.basic.lsn_16_1_4.Main)
String str = "";
for (int i = 0; i < 100000; i++) {
    str = str + "A";
}

このコードでは、変数strに対して"A"が繰り返し連結されることにより、"A"、"AA"、"AAA"....と変数strの値が変化していきます。
Stringクラスはイミュータブルなので、"A"が連結されるたびに新しいStringオブジェクトが生成され、連結前の文字列は即座にメモリ上から廃棄されていきます。つまりStringクラスは、イミュータブルであることと引き換えに、連結処理に対するリソース効率は必ずしも高いとは言えないのです。
このように大量の文字列連結を行う場合は、StringクラスではなくStringBuilderクラスを利用します。

StringBuilderクラスによる文字列連結

java.lang.StringBuilderクラスは、主に文字列の連結を目的としたクラスです。Stringクラスとは異なり、このクラスは内部の状態を書き換えることが可能なため、連結処理を効率的に行うことが可能です。その反面スレッドセーフではないため、フィールドの型としてではなく主にローカル変数として一時的に利用します。
それではStringBuilderクラスによる文字列連結の処理を、具体的に見ていきましょう。前項同様ループ処理によって"A"を10万回連結する処理を、StringBuilderクラスによって行うコードを、以下に示します。

snippet_2 (pro.kensait.java.basic.lsn_16_1_4.Main)
StringBuilder sb = new StringBuilder(""); //【1】
for (int i = 0; i < 100000; i++) {
    sb.append("A"); //【2】
}
String str = sb.toString(); //【3】

まずコンストラクタに初期値を指定して、StringBuilderのオブジェクトを生成します【1】。次に文字列を連結するために、append()メソッドに連結対象の文字列を渡します【2】。連結が終わったらtoString()メソッドを呼び出して、連結された文字列をString型として取得します【3】。
なおStringBuilderクラスが適合するのは、あくまでも大量の文字列を連結するケースです。文字列を数回程度、連結する程度の処理であれば、Stringクラスでもリソースへのインパクトを気にする必要は全くありません。
またStringBuilderクラスとよく似た特徴を持つクラスに、java.lang.StringBufferがあります。StringBufferはスレッドセーフですが、その分、連結処理の効率性の観点では、StringBuilderの方に優位性があります。
以上から話を整理すると、通常の文字列連結にはStringクラスの+演算子をそのまま使用し、大量の文字列連結を行う必要がある場合に限りStringBuilderクラスを利用する、というのが基本的な考え方になります。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. Stringクラスには、イミュータブルである、メモリ上で再利用される、という特徴があること。
  2. Stringクラスの主要なAPI(文字列情報取得、変換、比較、判定、部分文字列検索、分割・結合、置換、フォーマッティングなど)の使い方について。
  3. StringBuilderクラスによって文字列連結する方法について。

Discussion