[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください
この記事では、文字列の文字コード(文字セット)を変換する方法を解説します。
Scalaには独自の変換方法が用意されていないので、JavaのAPIを利用して変換します。
Javaにおいては、String#getBytes
メソッドを使用して簡便に変換する方法と、java.nio.charset.CharsetEncoder
クラスを使用して厳密に変換する方法があります。
厳密に処理するならCharsetEncoder
、そうでなければString#getBytes
を使用する
まず、String#getBytes
メソッドとCharsetEncoder
クラスの使い分けについて説明します。
String#getBytes
メソッドを使用する場合、もし指定された文字セットに含まれず変換できない文字が含まれているときには、適切に変換することができません。
したがって、以下の2つの条件を満たす場合には、CharsetEncoder
を使用します。
- 変換先の文字セットに含まれない無効な文字が文字列中に存在する可能性がある
- そのような文字が存在したことを検知して独自に処理する必要がある
参考となるリンクを掲載しておきますので、必要に応じて参照してください。
java.nio.charset.CharsetEncoder
逆に、以下のいずれかの場合にはString#getBytes
を使用します。
- 変換先の文字セットに含まれないような無効な文字が文字列中に存在する可能性はない
- もしそのような文字が存在したとしても、メソッドに定められた標準の挙動で十分である
この記事では、String#getBytes
メソッドを使用する方法を解説します。
getBytes
メソッドを使用して文字セットを変換する
String#getBytes
メソッドを利用すると、文字列の文字セットを変更してバイト列に変換することができます。
Javapublic byte[] getBytes(String charsetName) public byte[] getBytes(Charset charset)
Scala風に書くと以下のようになります。
Scaladef getBytes(charsetName: String): Array[Byte] def getBytes(charset: Charset): Array[Byte]
文字セットの変換に使えるgetBytes
メソッドは2つありますが、これらの違い、使い分けについては後述します。
getBytes(charsetName: String)
を使用して変換する
まずは getBytes(charsetName: String)
を使用して文字セットを変換してみましょう。
こちらの文字列に対して変換処理を行います。
val s = s"Scala逆引き解説 scalapedia"
サンプルコードはこちらです。
val utf8Bytes = s.getBytes println(utf8Bytes.mkString(",")) val sjisBytes = s.getBytes("Shift-JIS") println(sjisBytes.mkString(","))
まずは標準の文字セットでバイト列を出力しています。
この環境では特に指定していないので、標準の文字セットはUTF-8となります。
中身の確認のため、コンマ区切りにして出力しています。
次に、文字列をShift-JISのバイト列に変換します。
そしてこちらも中身の確認のため、コンマ区切りにして出力します。
出力結果は以下のようになります。
83,99,97,108,97,-23,-128,-122,-27,-68,-107,-29,-127,-115,-24,-89,-93,-24,-86,-84,32,115,99,97,108,97,112,101,100,105,97 83,99,97,108,97,-117,116,-120,-8,-126,-85,-119,-16,-112,-32,32,115,99,97,108,97,112,101,100,105,97
一行目と二行目でそれぞれ出力が異なることがわかりますね。
今度はそれぞれ文字列に戻してみましょう。
String
のコンストラクタにバイト列を渡すと、文字列のインスタンスを生成できます。
println(new String(utf8Bytes)) println(new String(sjisBytes, "SHIFT_JIS"))
出力結果は以下のようになります。
Scala逆引き解説 scalapedia Scala逆引き解説 scalapedia
きちんと元に戻っていることがわかります。
java.lang.String#getBytes(java.lang.String)
getBytes(charset: Charset)
を使用して変換する
今度は getBytes(charset: Charset)
を使用して文字セットを変換してみましょう。
Charset
を取得する
このメソッドは引数としてCharset
を受け取ります。
つまりあらかじめ変換先のCharset
を取得して用意しておく必要があります。
使用するCharset
は Charset#forName
メソッドを使用することで取得できます。
import java.nio.charset.Charset val sjis: Charset = Charset.forName("sjis")
取得したCharsetの中に何が入っているのかみてみましょう。
println(sjis.name)
Shift_JIS
「正準名」が返ってきました。
取得した際の文字列とは異なります。
ここまでの例では、Shift-JISの表記をいろいろ変えて試してきました。
正準名以外に別名もカバーされているから、多少の表記ゆれが許容されるということがわかります。便利ですね。
サポートされている文字セットはこちらのページにまとめられています。
StandardCharsets
を使用する
また、頻出の文字セットはStandardCharsets
として定数があらかじめ用意されています。
使用できるのは以下の6種類です。
Charset | 通称 |
---|---|
ISO_8859_1 | ISO-LATIN-1 |
US_ASCII | ASCII文字 |
UTF_8 | UTF-8 |
UTF_16 | UTF-16 |
UTF_16BE | UTF-16 ビッグエンディアン・バイト順 |
UTF_16LE | UTF-16 リトルエンディアン・バイト順 |
これらの文字コードを使用する場合は表記ミスによる不具合を防ぐため、できる限りStandardCharsets
を使用していきましょう。
java.nio.charset.StandardCharsets
文字セットを変換する
それでは、また先ほどの文字列に対して変換処理を行いましょう。
val s = s"Scala逆引き解説 scalapedia"
サンプルコードはこちらです。
val sjisBytes = s.getBytes(sjis) println(sjisBytes.mkString(","))
文字列をShift-JISのバイト列に変換します。
中身の確認のため、コンマ区切りにして出力します。
出力結果は以下のようになります。
83,99,97,108,97,-117,116,-120,-8,-126,-85,-119,-16,-112,-32,32,115,99,97,108,97,112,101,100,105,97
先ほどの例の出力の二行目と同じであることがわかります。
java.lang.String#getBytes(java.nio.charset.Charset)
2つの getBytes
の違いは異常系の処理
上述のように、文字セットの変換に使えるgetBytes
メソッドは2つあります。
これらの違いは2つあります。
- 文字コードが存在しない場合に投げる例外が異なる
- 無効な文字が含まれる場合の挙動が異なる
結論としてはこの相違点はそれほど重大な問題とはなりにくいのですが、念のため、どう違うのかについて確認していきましょう。
文字コードが存在しない場合の例外は気にする必要なし
まず、両者のメソッドは文字コードが存在しない場合に投げる例外が異なります。
getBytes(charsetName: String)
では、引数に渡したcharsetName
に対応する文字コードが存在しない場合にjava.lang.UnsupportedEncodingException
を投げます。
UnsupportedEncodingException
はチェック例外なので、Javaのコードにおいてはエラーハンドリングを強制されます。
煩わしいですね。
他方、getBytes(charset: Charset)
自体が例外を投げるわけではないのですが、文字セットの取得のために使用するCharset#forName
が例外を投げます。
もし対応する文字コードが存在しない場合にはjava.nio.charset.UnsupportedCharsetException
を投げます。
UnsupportedCharsetException
は非チェック例外なので、エラーハンドリングは強制されません。
つまり、JavaにおいてはgetBytes(charset: Charset)
の方が取り回しがいいのです。
ただし、Scalaにはチェック例外がないので、いずれの場合にも関係ありません。
また、実際に変換するタイミングではなく、それよりも前の文字コードを検索する段階で例外が発生するようになります。 例外発生のタイミングが異なることと、投げられる例外のクラスが異なることに注意しましょう。 変換の都度検索するより多少効率がよくなります。
無効な文字が含まれる場合の挙動は注意が必要
また、両者のメソッドは無効な文字が含まれる場合の挙動が異なります。
getBytes(charsetName: String)
の無効な文字が含まれる場合の挙動は指定されておらず、JVMの実装依存となります。
気になる場合には検証するか、getBytes(charset: Charset)
を使用するか、CharsetEncoder
を使用しましょう。
getBytes(charset: Charset)
|デフォルトの置換バイト列で置き換えられます。
「デフォルトの置換バイト列」が何なのかは確認しておきましょう。
Charset#forName
を使って文字セットを事前検索すると良い
さて、文字セットを指定する方法は、直接文字列で指定する方法(getBytes(charsetName: String)
)と、 Charset#forName
を使って取得したCharset
型で指定する方法(getBytes(charset: Charset)
)の2つがありますね。
どちらを使えば良いでしょうか。
チェック例外のないScalaにおいては、両者それぞれそれなりに効率的なので、結論としては「どちらでもいい」ということになりますが、強いて言えば以下の点を参考にすると良いでしょう。
- 文字コードをあちこちで引き回す場合には、
getBytes(charset: Charset)
を使った方が型安全で良い。- 文字コードを引き回す予定がないなら、
Charset#forName
せず直接getBytes(charsetName: String)
を呼ぶだけで十分。
- 文字コードを引き回す予定がないなら、
getBytes(charsetName: String)
も実装上はCharset#forName
を使用している(ので、Charset#forName
を他で呼ばないような書き方をすると効率的で良い)。- 使用する文字セットが限られる場合には、
getBytes(charset: Charset)
を使った方が検索回数が節約できるので効率的で良い。
あるいは、もし無効な文字が含まれる場合の挙動が気になる場合は、getBytes(charset: Charset)
を使うか、あるいはCharsetEncoder
の使用を検討するべきかもしれません。
文字列の内容の編集は文字セットを変換する前に行っておく
ところで、getBytes
メソッドの戻り値はバイト列Array[Byte]
です。
言い換えると、文字セットの変換後にはString
としての形を保つことはできません。
これは、文字列の内部表現がUTF-16に固定されているからです。
文字列の内部表現で使用する文字セットを変換することはできません。
つまり変換後の状態では、内容を編集するのが難しいということになります。
文字列の中身に対する操作は変換前に済ませておきましょう。
まとめ
文字列の文字セットを簡便に変換したい場合はString#getBytes
メソッドを使用します。
また、厳密に変換したい場合は java.nio.charset.CharsetEncoder
を使用します。
文字列の内容について必要な編集をおこなった後、文字セットを変換するようにしましょう。