文字列の長さを取得する方法:lengthとcodePointCountの使い分け

Scala 3 (Dotty 0.26.0-RC1) 2.13.3 2.12.12
最終更新:2020年7月6日

[AD] scalapediaでは記事作成ボランティアを募集しています

この記事ではScalaで文字列の長さを取得する方法をご紹介します。

基本的にはJavaの java.lang.String クラスのAPIを使用して取得します。

文字列の長さを取得する処理にはJavaの文字列の内部実装に起因する落とし穴がありますので、注意してください。

文字列の長さを取得するには codePointCount メソッドを使用する

codePointCount メソッドと length メソッドを使用すると、文字列の長さを取得することができます。

早速 サンプルコードを見てみましょう。
こちらの文字列の長さを取得します。

val s = "Scala逆引き解説 Scalapedia"

よかったら文字数を数えてみてください。21文字あるはずです。

そして、長さを取得するコードはこちらです。

val codeUnits = s.length val codePoints = s.codePointCount(0, codeUnits) println(s"文字数は${codePoints}です")

length メソッドの戻り値を使って、さらに codePointCount メソッドを実行することで文字数を取得しています。

出力結果は以下のようになります。

文字数は21です

先ほど数えた文字列の長さと一致していることを確認してください。

ところで、「length ってそもそも文字列の長さを取得するメソッドではないの?二度手間では?」と思った方もいると思います。
本当にそうですよね。まさにおっしゃる通りです。

しかし、Javaの文字列の内部実装について考えるとこのようにせざるを得ないのです。

codePointCount を使うのはUTF-16の「サロゲートペア」を適切に扱うため

ここから初心者向きでない話がしばらく続きます。
よくわからないという場合には、必ず先に挙げたサンプル通りcodePointCountを使用しましょう。

さて、なぜこのような非効率を甘んじて受ける必要があるのでしょうか。
それは、UTF-16の「サロゲートペア」を適切に扱うためです。

Javaの文字列の内部表現はUTF-16

突然UTF-16の話になりましたが、どうか落ち着いてください。
実はJavaの文字列の内部表現は基本的にUTF-16なのですが、これが少し厄介です。

(※ Java 9から追加されたString Compaction機能が有効であり、かつ文字列がLATIN1の文字しか含まない場合には内部表現にもLATIN1を使用します)

「Javaって標準ではUTF-8ではないの?」と思うかもしれませんが、それとは違います。
おそらく想像しているのは java.nio.charset.Charset#defaultCharset() で取得する文字コードではないでしょうか。
確かに、システムプロパティのfile.encodingにおいて特に指定がない場合には、defaultCharset()はUTF-8を返します。これは正しいです。

しかし、charや文字列(String)の内部表現はこれとは異なり、UTF-16が使用されています。
つまり、Javaプラットフォームにおけるあらゆる文字はメモリ上ではUTF-16で保持されているというわけです。
もう少し細かくいうと、UTF-16の当初の仕様に基づいて、16ビット固定の値として表現されています。

UTF-16のサロゲートペアは2文字分のデータで1文字を表現する

さて、UTF-16には「サロゲートペア」として表現される文字が存在します。 元々の仕様で定められた16ビットには収まりきらなかった文字を表現するための拡張機能です。

文字を表現するための最小の区切りを「Unicodeコード単位」(Unicode code unit)といいます。
UTF-16のUnicodeコード単位は16ビットです。
通常は1コード単位で1文字を表現しますが、サロゲートペアは2コード単位で1文字を表現します。

また、表現できる文字を「Unicodeコード・ポイント」(Unicode code point)と呼びます。
これが人間が目にするところの「1文字」にあたります。

つまり、サロゲートペアとは「2コード単位で1コードポイントとなるような文字」を指しているというわけです。

話が少し戻りますが、Javaにおける文字列が「当初の仕様に基づいて、16ビット固定の値として」表現されているというのと整合性がとれませんね。

まさに、Javaの文字列はそのままではサロゲートペアには対応していないのです。

したがって回避策をとる必要があるというわけです。

サロゲートペアを含む文字列の長さを取得してみる

有名なサロゲートペアとして「つちよし」(𠮷)があります。
この「𠮷」を含む文字列を使った文字列を考えてみます。

val surrogate = "うまい・やすい・はやい 𠮷野家"

おなじみの𠮷野家のキャッチフレーズが出来上がりました。

それでは、サロゲートペアを含む文字列の長さを数えてみましょう。
サンプルコードはこちらです。

val codeUnits = surrogate.length println(s"文字数は${codeUnits}です") val codePoints = surrogate.codePointCount(0, codeUnits) println(s"文字数は${codePoints}です")

lengthcodePointCountを使って取得したそれぞれの値を出力しています。

出力結果は以下のようになります。

文字数は16です 文字数は15です

見事に値が異なることがわかりますね。とっても危険です。

サロゲートペアが含まれうる場合には必ず codePointCount を使いましょう。

lengthメソッドは「Unicodeコード単位」の個数を返す

それでは改めて length メソッドについて見てみましょう。

Java
public int length()

Scala風に読み替えると以下のような感じになります。

Scala
def length: Int

APIドキュメントには以下のように書いてあります。

この文字列の長さを戻します。 長さは文字列内のUnicodeコード単位の数に等しくなります。

「Unicodeコード単位の数」を返すということは、サロゲートペア1文字に対しては2を返すということです。

サロゲートペアを含む文字列に対して length メソッドを使うと、実際の文字数とずれてしまうことがわかります。

Java 11 API Docs:java.lang.String#length()

codePointCountメソッドは「Unicodeコード・ポイント」の個数(つまり文字数)を返す

それでは codePointCount メソッドについて見てみましょう。

Java
public int codePointCount(int beginIndex, int endIndex)

第一引数は数え初めの位置を示すインデックス、第二引数は数え終わりの位置を示すインデックスです。

Scala風に読み替えると以下のような感じになります。

Scala
def codePointCount(beginIndex: Int, endIndex: Int): Int

引数ゼロの codePointCount() のようなメソッドはありません。不便ですね。

APIドキュメントには以下のように書いてあります。

このStringの指定されたテキスト範囲のUnicodeコード・ポイントの数を返します。

したがって、codePointCountの返す値がまさしく「文字数」であることがわかります。

codePointCountを実行すると計算量がかかる可能性がある

lengthcodePointCountを実行することで計算コストはどれくらいかかるのでしょうか。

length の実行はごく短い定数時間で済む

String#length() は 内部に保持している配列のlengthフィールドにアクセスし、ビット演算で長さを計算します。
フィールドへのアクセスは定数時間で、ごく短時間です。
ビット演算も短時間で済みますから、String#length()にかかる計算量はごくわずかだと考えられます。

codePointCount は実行にO(n)のコストがかかる可能性がある

上述のように、Java 9からString Compaction機能が追加され、Stringの内部表現がUTF-16に加えてLATIN1である場合が生じるようになりました。

  • Java 9以降でコンパイルしている
  • String Compactionを有効にしている
  • 文字列が全てLATIN1の文字で構成される

これらの条件を同時に満たす場合には、計算はほぼ一瞬で終わります。 String Compactionによってchar内部の表現がUTF-16からLATIN1に切り替わっているので、全て1バイト文字であることが明らかなためです。

他方で、

  • Java 8以前でコンパイルしている
  • String Compactionを無効にしている
  • 文字列がLATIN1に含まれない文字を含んでいる

以上のいずれかの条件に当てはまる場合には、サロゲートペアかどうかについて指定された範囲をくまなく走査することになります。 したがって、計算はその範囲の大きさの分だけ、つまりO(n)の計算量がかかります。

以上のように、codePointCountで文字数を計算すると多少コストがかかる可能性がありますので、注意してください。

サロゲートペアを考慮しなくてもいい場合は length を使う

サロゲートペアを考慮しなくても良い場合には、lengthメソッドの戻り値を便宜上の「文字数」としても大丈夫です。

codePointCount を呼ばない分、高速に処理することができます。

文字列にサロゲートペアが入らないかどうかはよく確認しておきましょう。

length または sizeメソッドを使用して文字列の長さを取得する

さて、JavaにおいてはStringクラスにはlengthメソッドしか用意されていませんが、Scalaの StringOps クラスにはsizeメソッドがあります。
どちらのメソッドを使っても文字数を取得することができます。

println(s"文字数は${s.length}です")

結果は以下のようになります。

文字数は21です

sizeメソッドを使った場合のサンプルコードはこちらです。

println(s"文字数は${s.size}です")

結果はlengthメソッドを使った場合と同様です。

文字数は21です

lengthsize の違いはない。好みで使ってOK

sizeメソッドは、内部でlengthメソッドを呼び出しています。
内部実装が同じなので同じように使えます。

メソッド名の意味としては、"size"は抽象的に(順番の無関係な集合の個数としての)「大きさ」を、 "length"はより具体的に(一連の列になったものの個数としての)「長さ」を指しているという違いがあります。

強いて言えば、"length"の方がよりStringの実装に即している考えられますが、 ことStringに関してはどちらも実質的には同じものを指しているので、どちらを使っても大丈夫です。

Scalaのコードとしては特に気にする必要はありませんが、例えばプロジェクト内の他の箇所で"size"を(文字数ではなく)「データのサイズ」という意味で使っているなどしたら要注意です。

Scala Standard Library:scala.collection.StringOps#size

まとめ

UTF-16の「サロゲートペア」に起因するトラブルを避けるため、まずはcodePointCountメソッドを使用するよう心がけましょう。

サイト内検索


カテゴリ「文字列処理」の記事

文字列をエスケープしたり復元したりする方法(Apache Commons Text) JavaとScalaのString/StringBuilder/StringBuffer使い分け事情 文字列を分割する方法(split・splitAt・linesIterator・linesWithSeparatorsメソッド) trimメソッドで文字列の前後の空白を除去する 文字列が一致するか比較する方法/大文字・小文字を区別せずに比較する方法 特定の文字の文字コード(コード・ポイント)を取得する replaceメソッドなど、文字列を置換する方法を紹介 文字列の先頭や末尾を、取得したり切り落としたりする方法 containsメソッドでStringに特定の文字列が含まれるか調べる方法 数値を文字列に変換する方法 stripメソッドで文字列の前後の全角空白を除去する 文字列を数値に変換するには?to○○メソッドと注意点について 文字列を辞書的に比較する方法/大文字・小文字を区別せずに比較する方法 文字列が特定の文字列で始まるか・終わるかを調べる方法 【getBytes&size】文字列のバイト長を取得する方法 substringでStringを切り取り、部分文字列を抽出する方法 StringOpsとWrappedStringの違いは? 文字列を連結するには?+演算子やString interpolationの使い方 文字列の大文字へ・小文字へ変換する方法 文字列を逆順にする方法 文字列の文字コード(文字セット)を変換する方法 文字列の長さを取得する方法:lengthとcodePointCountの使い分け 文字列をURLエンコード・デコードする方法 文字列が正規表現に合致するか調べる方法 この文字の位置はどこ?文字列のインデックスを取得する方法 文字列をバイト列に、またはバイト列を文字列に変換する方法

カテゴリ「文字列処理」の記事

文字列をエスケープしたり復元したりする方法(Apache Commons Text) JavaとScalaのString/StringBuilder/StringBuffer使い分け事情 文字列を分割する方法(split・splitAt・linesIterator・linesWithSeparatorsメソッド) trimメソッドで文字列の前後の空白を除去する 文字列が一致するか比較する方法/大文字・小文字を区別せずに比較する方法 特定の文字の文字コード(コード・ポイント)を取得する replaceメソッドなど、文字列を置換する方法を紹介 文字列の先頭や末尾を、取得したり切り落としたりする方法 containsメソッドでStringに特定の文字列が含まれるか調べる方法 数値を文字列に変換する方法 stripメソッドで文字列の前後の全角空白を除去する 文字列を数値に変換するには?to○○メソッドと注意点について 文字列を辞書的に比較する方法/大文字・小文字を区別せずに比較する方法 文字列が特定の文字列で始まるか・終わるかを調べる方法 【getBytes&size】文字列のバイト長を取得する方法 substringでStringを切り取り、部分文字列を抽出する方法 StringOpsとWrappedStringの違いは? 文字列を連結するには?+演算子やString interpolationの使い方 文字列の大文字へ・小文字へ変換する方法 文字列を逆順にする方法 文字列の文字コード(文字セット)を変換する方法 文字列の長さを取得する方法:lengthとcodePointCountの使い分け 文字列をURLエンコード・デコードする方法 文字列が正規表現に合致するか調べる方法 この文字の位置はどこ?文字列のインデックスを取得する方法 文字列をバイト列に、またはバイト列を文字列に変換する方法