[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください
この記事では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ビット固定の値として表現されています。
Unicode文字表現(java.lang.Character)
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}です")
length
とcodePointCount
を使って取得したそれぞれの値を出力しています。
出力結果は以下のようになります。
文字数は16です 文字数は15です
見事に値が異なることがわかりますね。とっても危険です。
サロゲートペアが含まれうる場合には必ず codePointCount
を使いましょう。
length
メソッドは「Unicodeコード単位」の個数を返す
それでは改めて length
メソッドについて見てみましょう。
Javapublic int length()
Scala風に読み替えると以下のような感じになります。
Scaladef length: Int
APIドキュメントには以下のように書いてあります。
この文字列の長さを戻します。 長さは文字列内のUnicodeコード単位の数に等しくなります。
「Unicodeコード単位の数」を返すということは、サロゲートペア1文字に対しては2
を返すということです。
サロゲートペアを含む文字列に対して length
メソッドを使うと、実際の文字数とずれてしまうことがわかります。
java.lang.String#length()
codePointCount
メソッドは「Unicodeコード・ポイント」の個数(つまり文字数)を返す
それでは codePointCount
メソッドについて見てみましょう。
Javapublic int codePointCount(int beginIndex, int endIndex)
第一引数は数え初めの位置を示すインデックス、第二引数は数え終わりの位置を示すインデックスです。
Scala風に読み替えると以下のような感じになります。
Scaladef codePointCount(beginIndex: Int, endIndex: Int): Int
引数ゼロの codePointCount()
のようなメソッドはありません。不便ですね。
APIドキュメントには以下のように書いてあります。
このStringの指定されたテキスト範囲のUnicodeコード・ポイントの数を返します。
したがって、codePointCount
の返す値がまさしく「文字数」であることがわかります。
java.lang.String#codePointCount(int, int)
codePointCount
を実行すると計算量がかかる可能性がある
length
とcodePointCount
を実行することで計算コストはどれくらいかかるのでしょうか。
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です
length
とsize
の違いはない。好みで使ってOK
size
メソッドは、内部でlength
メソッドを呼び出しています。
内部実装が同じなので同じように使えます。
メソッド名の意味としては、"size"は抽象的に(順番の無関係な集合の個数としての)「大きさ」を、 "length"はより具体的に(一連の列になったものの個数としての)「長さ」を指しているという違いがあります。
強いて言えば、"length"の方がよりString
の実装に即している考えられますが、
ことString
に関してはどちらも実質的には同じものを指しているので、どちらを使っても大丈夫です。
Scalaのコードとしては特に気にする必要はありませんが、例えばプロジェクト内の他の箇所で"size"を(文字数ではなく)「データのサイズ」という意味で使っているなどしたら要注意です。
scala.collection.StringOps#size
まとめ
UTF-16の「サロゲートペア」に起因するトラブルを避けるため、まずはcodePointCount
メソッドを使用するよう心がけましょう。