やり直しJava

変数とデータ型

Jump to Section

変数と定数

変数

ここでは 値やオブジェクト参照を入れるための入れ物を「変数」と一括りに呼んでいます。変数にはローカル変数・インスタンスフィールド・クラスフィールドがあり、変数は型宣言が必要です。 (一部 型推論機能により省略できます)

変数の宣言

Javaは静的型付き言語で、変数を宣言する際にデータ型を指定します。データ型の種類についてはこのページの「データ型」で説明します。

データ型  変数;

// 配列の場合
データ型[]  変数;    // 一次元配列
データ型[][]  変数;  // 二次元配列
// C、C++の言語仕様の流れを汲んでいるので次のような宣言も可能だが、推奨されない
データ型  変数[];
データ型  変数[][];
int  i;
double  d;
String  str;
int[]  array;
double[][]  arrayList;
// int aray[];  // 推奨されない
// double  arrayList[][];  // 推奨されない

配列を宣言する場合[]を変数の後に置く書き方もできますが、一般的にはデータ型の後に置きます。

昔のCでは ローカル変数はスコープの先頭で宣言する必要がありましたが、Javaのローカル変数はスコープの先頭でなくても 必要なときに宣言できます。むしろ ブロックの先頭で宣言するよりも、ローカル変数が初めて使われるときに宣言した方が コードが読み易くなります。また、ローカル変数はスコープを最小限にとどめた方が コードが読み易くなります

特殊なローカル変数としてfor文・拡張for文・try-with-resource文の冒頭でローカル変数を宣言することができます。それらは スコープがそのfor文・拡張for文・try-with-resource文だけに限られるローカル変数です。

数の初期化

変数の宣言と同時に初期値を与えて変数を初期化することができます。

データ型  変数 = 初期値;
// 配列の場合
データ型[]  変数 = new データ型[配列の要素数];
データ型[]  変数 = new データ型[] { 初期値, 初期値, ... };
データ型[]  変数 = { 初期値, 初期値 , ... };

変数の宣言時に初期化を行わない場合、ローカル変数なのかフィールドなのかで 初期化されるかされないかが異なります。初期化される場合のデフォルト値は 大雑把に言うとプリミティブ型の場合は0(boolean型の場合はfalse)で参照型の場合はnullになります。各プリミティブ型のデフォルト値については「データ型(プリミティブ)一覧」にまとめています。

ローカル変数

ローカル変数の場合、変数を宣言しただけでは変数は初期化されません。初期化されていない変数を使用するとコンパイルエラーになります。(ただしローカル変数でも配列の場合は 配列の各要素はデフォルト値で初期化されます。)そのため、可能な限り ローカル変数は宣言と一緒に初期化を行うのが合理的です

ローカル変数の宣言時に初期化できないケースとしては、初期化の式やメソッドが例外を投げる場合です。そういう場合は 見た目がスマートではないのですが、一旦nullで初期化し、tryブロックで初期化を行うことになります。

SomeClass obj = null;
try {
    obj = SomeClass.newInstanceThrowable();
} catch(XXXException e) {
    // 例外処理
}
インスタンスフィールド、クラスフィールド

インスタンスフィールドとクラスフィールドは 明示的に初期値を指定しないとデフォルト値で初期化されます。但し、finalをつけたフィールドについては、明示的に初期値を指定しないとコンパイルエラーになります。詳しくは「クラス」の章の「finalフィールドの初期化」を参照してください。

変数に値やオブジェクト参照を代入

変数に値やオブジェクト参照を代入するには、演算子「=」を使用します。

finalを付けて宣言した変数は初期化した後は値を変更(再代入)することができません。詳しくは後述します。

変数名

変数名規則

変数名には次の規則があります。

  • 全てのUnicodeが使えます。
  • 先頭文字は数字にできません。(1_var 等はNG)
  • 大文字と小文字は区別されます。(var と VARは別物)
  • 予約語は変数名にできません。

全てのUnicodeが使えるので日本語や絵文字も変数名として使うことは可能なのですが一般的には変数名に日本語や絵文字は使いません

変数名付与則の慣例

Javaの変数名の付与則として次のような慣例があります。

  • 全てのUnicodeが使えますが、日本語等は用いず 英数字とアンダースコアのみで構成します
  • モジュール名・パッケージ名、クラス名、フィールド名、メソッド名、定数、ローカル変数に応じて次のような規則が良く使われます。
    • モジュール名・パッケージ名
      • 小文字、ピリオド区切りの階層構造
        外部に公開するパッケージの場合、パッケージ名の一意性を確保するために 組織のドメインをTLD(トップレベルドメイン)から順番に並べた形で始まる名前にすることが推奨されています。(例:jp.co.somecompany.somepackage)
    •  クラス名、インタフェース名
      • Pascal記法(単語の先頭を大文字、それ以外を小文字)
      • 名詞や名詞句。インタフェースの場合はableやibleで終わる形容詞の場合も。
        SomeClass、AnotherInterface、Cloneable  など。
    • フィールド名
      • camelCase記法(最初以外の単語の先頭を大文字、それ以外を小文字)
      • 名詞や名詞句
        someField、anotherField など。
    •  メソッド名
      • camelCase記法(最初以外の単語の先頭を大文字、それ以外を小文字)
      • 目的語を含む動詞や動詞句。
        start、doSomething など。
      • booleanを返すメソッドは 以下の通り。
        • is+形容詞。XXであるか否か。(isEmpty など)
        • has+過去分詞。XXしたか否か。(hasChanged など)
        • can+動詞。XXできるか否か。(canChange など)
      • getterは getXXX またはフィールド名(XXXの部分)そのまま。setterはsetXXX。
        getSize、size、setSizeなど。
      • 異なる型のオブジェクトを返すメソッドはtoXXX(XXXは変換後の型)
        toString、toArray など。
      • オブジェクトの異なる型のビューを返すメソッドはasXXX(XXXはビューの型)
        asList、asPoint など。
      • staticファクトリメソッドの場合は from、of、valueOf、getInstance、newInstance、getXXX(XXXはファクトリメソッドを持つクラスとは別の型)など。
    • 定数名
      • 大文字のsnake_case記法(単語をアンダースコアでつなぐ)
        SOME_CONSTANT、ANOTHER_CONSTANT など。
    • ローカル変数名
      • camelCase記法(最初以外の単語の先頭を大文字、それ以外を小文字)
        someVariable、anotherVariable など。
      • 他の要素と違って、省略形が多用される。

開発現場によってはコーディングルールで変数名の付与則が定められている場合もあります。

Java言語の命名指針」(Qiita)

定数

ときどき定数と不変の変数が同じような意味で使われることがありますが、定数と不変の変数では意味合いが異なります。

定数」:円周率の3.14…のような意味のある固定の値・文字列(・時にはオブジェクト)に対して 繰り返し参照できるように名前を付けた物

不変の変数」:初期化後にその値を変更することができない変数。例えば インスタンスの生成時刻をフィールドに持つような場合、そのフィールドは初期化後に誤って変更されないように不変の変数にします。

final修飾子は初期化後にその値を変更することができないという役割を果たすため、定数とする場合も不変の変数とする場合も 同じfinal修飾子を用います。一般には 定数とする場合はクラスフィールド(static final)として定義します。時にはメソッド内だけで定数が必要な場合にローカル変数にfinalを付けて定数とすることがあるかも知れません(稀です)。定数とする場合は 慣例として大文字のsnake_case表記の変数名が付与されます。

定数(ローカル変数)

ローカル変数にfinalを付けると 不変の変数やメソッド内の定数にすることができます。変数の宣言時に初期値を指定すると、それ以降は変数に値を代入することはできません。変数の宣言時に初期値を指定しないと、その後1度だけ代入ができます。(初期化に相当します)初期化後は変数に値を代入することはできません。

final int CONSTANT_A = 0;
// 初期化する場合は、代入不可。
// CONSTANT_A = 1;  // 初期化後の代入はエラー。

final int CONSNTANT_B;
CONSNTANT_B = 1;
// 初期化しない場合は、初回のみ代入可能。この場合、初回の代入が初期化に相当。
//CONSNTANT_B = 2;  // 初期化後の代入はエラー。

finalを付けた変数がオブジェクト参照の場合、初期化後に変数が指し示すオブジェクト参照を別の物に変更することはできませんが、参照先のオブジェクトの中身は変更できてしまうことに注意が必要です。

List <Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
System.out.println(list);  // [1, 2, 3]

final List<Integer> listRef = list;  // 初期化
// listRef = new ArrayList<>(); // 初期化後は別のオブジェクト参照に変更することはできない。

listRef.add(4);  // しかし、参照先のオブジェクトの中身は変更できてしまう。
System.out.println(list);  // [1, 2, 3, 4]

定数(クラスフィールド)

同様に インスタンスフィールドやクラスフィールドにfinalを付けると 不変の変数やクラス固有の定数にすることができます。ローカル変数の場合と同様に、フィールドがオブジェクト参照の場合は 初期化後にフィールドが指し示すオブジェクト参照を別の物に変更することはできませんが、参照先のオブジェクトの中身は変更できてしまうことに注意が必要です。

インスタンスフィールドやクラスフィールドにfinalを付ける場合は、インスタンスやクラスの初期化が完了するまでにそのフィールドが初期化されないとコンパイルエラーになります。詳しくは「クラス」の章の「finalフィールドの初期化」で説明します。

データ型

データ型はint・long・doubleなどのプリミティブ型と、オブジェクト・配列などの参照型に分類できます。Javaは静的型付け言語で 変数を宣言する際にデータ型を指定します。(型推論機能により指定不要な部分もあります。)

プリミティブ型

プリミティブ型の一覧は以下の通りです。

プリミティブ型一覧
プリミティブ型名称大きさ範囲デフォルト値リテラル
byteバイト型1Byte-128~12703桁区切り:「_」
2進数:0b~(0, 1)
8進数:0~(0~7)
16進数:0x~(0~f)
Long値:末尾に「L」
short短整数型2Byte-32,768~32,7670
int整数型4Byte-2,147,483,648~2,147,483,6470
long長整数型8ByteLong.MIN_VALUE ~Long.MAX_VALUE0
float単精度浮動小数点数型4Byte±Float.MIN_VALUE ~ ±Float.MAX_VALUE
0.0、Inf、-Inf、NaNなど。
0.0末尾に「f」または「F」
double倍精度浮動小数点数型8Byte±Double.MIN_VALUE ~ ±Double.MAX_VALUE、
0.0、Inf、-Inf、NaNなど。
0.0末尾に何もつけない
指数表記:「E」
char文字型2Byte‘\u0000’~’\uFFFF’‘\u0000’シングルクォートで囲む
文字そのものを指定するか、
\uに続いて16進数Unicodeを指定
booleanブーリアン型―(※)true/falsefalse

※ boolean型の大きさについてはBoolean.SIZEが定義されておらず、VMの実装依存になると思われます。

Javaのプリミティブ型の整数型は 符号付き(signed)のみで、CやC++のような符号なし(unsigned ~)がありません。しかし、Java SE8から符号なし整数を扱うメソッドがラッパークラスに追加されましたので、符号なし整数の扱いが少し便利になりました。

プリミティブ型のキャスト

ワイドニング変換

データ型(プリミティブ)一覧」の表で、大きさが小さい型の変数を 大きい型の変数に代入する場合(ワイドニング変換)は 暗黙のうちに変換されます。ワイドニング変換には次の組み合わせがあります。

  • byte から short、int、long、float、double のいずれか
  • short から int、long、float、double のいずれか
  • char から int、long、float、double のいずれか
  • int から long、float、double のいずれか
  • long から float、double のいずれか
  • float から double

多くのワイドニング変換では 情報の欠落は発生しませんが、次の変換の場合に精度の欠落が発生し得るため、変換先の型が表せる値の範囲内なのかどうかの確認が必要です。

  • int または long から float に変換
  • long から double に変換
ナローイング変換

逆に 大きさが大きい型の変数を 小さい型の変数に代入する場合(ナローイング変換)には明示的なキャストが必要になります。ナローイング変換には 次の組み合わせがあります。

  • short から byte、char のいずれか
  • char から byte、short のいずれか
  • int から byte、short、char のいずれか
  • long から byte、short、char、int のいずれか
  • float から byte、short、char、int、long のいずれか
  • double から byte、short、char、int、long、float のいずれか

ナローイング変換の場合は、変換後の型の範囲を超えないかどうかの確認が必要です。変換後の型の値範囲を超えていると、情報の欠落や 符号の変化が発生するため注意が必要です。

浮動小数点数型から整数型への変換のように、切捨てが意図したものである場合もあります。そのような場合には、切捨てが意図したものである旨のコメントを記載すると分かり易いです。

浮動小数点数型

整数型は単純ですが、浮動小数点数型は仕組みがやや複雑なため、仕組みを簡単に説明します。

Javaでは 浮動小数点の表現にIEEE754を使用しています。浮動小数点数型は 符号部・指数部・仮数部から構成されていて、単精度(float)と倍精度(double)は それぞれ次のようなビット構成になっています。

  • 単精度:符号部1ビット、指数部8ビット、仮数部23ビット
  • 倍精度:符号部1ビット、指数部11ビット、仮数部52ビット

精度こそ違えど 単精度と倍精度で仕組みは同じなので、単精度の場合で説明します

符号部s、指数部E、仮数部Mとすると、浮動小数点数は次のように計算されます(これ以外にいくつか特別な値も用意されています)。

((-1)×s) × M × (2のE乗)

符号部sは 正の数値が0で、負の数値は1になります。ちなみに 0に対しても符合があるため、正の0と負の0が存在します。

仮数部Mと指数部Eの説明の前に、正規化について説明します。例えば 数値の1を表現する場合、1.0 × (2の0乗)、0.5 × (2の1乗)、2.0 × (2の-1乗)… と、何通りもの表現方法が可能です。これを統一するために、基本的に仮数部の最上位ビットが1で、整数部分が2進数で1桁(つまり1)になるようにします。これが正規化数です。正規化によって 1の表現方法は 1.0 × (2の0乗) に限定されます。

正規化数は 最上位ビットが1であることが自明であるため、1.xxxxxxのxxxxxxの部分だけを表現することになっています。これによって 仮数部で表現できる桁数が1ビット増えて、24ビットの範囲を表現できます。

しかし、最上位ビットが1以外許容しないとなると、0が表現できないことになってしまいます。また、最上位ビットを0にすることができれば、±(0~正の正規化数の最小値)の数値も表現できるようになります。この ±(0~正の正規化数の最小値)の数値を表した浮動小数点数は 非正規化数と言います。非正規化数は有効桁数が小さくなるため、精度が落ちることに注意が必要です。0や非正規化数は 最上位ビットが0になるため、正規化数とは区別する必要があります。正規化数とそれ以外の区別は 指数部で判断します。浮動小数点数型には 0以外にも正の無限大(Infinity)・負の無限大(-Infinity)・無効な値(NaN:Not a number)を表す特別な値が用意されています。演算でオーバーフローが発生すると 結果はInfinityや-Infinityになります。(整数型のようにラップアラウンドするわけではありません。)また、(Infinity – Infinity)や(0.0 / 0.0)のように 演算不能な場合に NaNが返ります。

これらの特別な値も 指数部で判断します。

改めて仮数部と指数部について説明します。

仮数部Mは 有効数字を表します。正規化数では仮数部の最上位ビット1は省略され、1.xxxxxxのxxxxxxの部分だけが表現されることになります。非正規化数では仮数部の最上位ビット0は省略され、0.xxxxxxのxxxxxxの部分だけが表現されることになります。

指数部Eは 2の何乗かを表します。例えば 1は2の0乗、2は2の1乗、0.5は2の-1乗になります。負の数を表現する必要があるため、E乗に127を足した数字にします。(整数型のように補数で表現するわけではないため、混乱し易いポイントです。)指数部は8ビットなので 0~255を表現できますが、これは127が足された値なので 実際には -127~128まで表現できることになります。しかし、-127と128は前述の特別な値を表すために使われるため、正規化数で表現できるのは-126(Float.MIN_EXPONENT)~127(Float.MAX_EXPONENT)になります。

指数部128はInfinity・-Infinity・NaNを表すために使われます。InfinityはFloat.POSITIVE_INFINITY、-InfinityはFloat.NEGATIVE_INFINITYが定義されています。無効な値NaNは Float.NaNとして定義されています。

指数部-127は0や非正規化数を表すために使われます。実際に非正規化数を計算する場合にはEは-127ではなく-126になることに注意が必要です。

まとめると、floatで表現できる値と定義値・定数は次の表のようになります。

floatで表現できる値と定義値・定数
数値大小絶対値大小定義値(Floatクラス定数)や定数
↑大きい↑大きいPOSITIVE_INFINITY正の無限大(オーバーフロー)
MAX_VALUE正の最大値
MIN_NORMAL正の正規化数の最小値
MIN_VALUE正の非正規化数の最小値
アンダーフロー
↓小さい0.0f正の0
↑小さい-0.0f負の0
アンダーフロー
-1.0f * MIN_VALUE負の非正規化数の最大値(絶対値は最小)
-1.0f * MIN_NORMAL負の正規化数の最大値
-1.0f * MAX_VALUE負の最小値(絶対値は最大)
↓小さい↓大きいNEGATIVE_INFINITY負の無限大(オーバーフロー)
floatやdoubleでは正確な演算ができない

floatやdoubleの浮動小数点型は 正確な近似を素早く行うためのデータ型です。そのため、正確な結果が必要な場面では使うことができません。floatやdoubleは値を2進数で保持していますが 0.1というシンプルな少数ですら2進数で表すと0.00110011…と無限循環小数となります。そのため浮動小数点の演算を行うと丸め誤差が生じます。

System.out.println(1.0 - 0.9);    // 0.09999999999999998

丸め誤差の生じない厳密な計算が必要な場合は、floatやdoubleではなくBigDecimalクラスを用います

BigDecimal bd1 = new BigDecimal("1.0");
BigDecimal bd2 = new BigDecimal("0.1");
System.out.println(bd1.subtract(bd2));    // 結果:0.1

BigDecimalを使うと コードが煩雑になるのと 僅かではありますが性能を犠牲にするという短所があります。また、doubleを引数にとるBigDecimalのコンストラクタを呼び出す場合、例えば 0.1のように doubleで正確に表現できない数値を渡すと、近似値が設定されることに注意が必要です。そのような場合には、代わりに Stringを引数にとるコンストラクタを呼び出すことによって 正確な数値を設定することができます。

System.out.println(new BigDecimal(0.1));      // 0.1000000000000000055511151231257827021181583404541015625
System.out.println(new BigDecimal("0.1"));    // 0.1

別の方法として、小数点の位置を自分で覚えておいて intやlongに置き換えて演算を行う方法があります。例えば小数点2桁までが有効な数値の演算を行う場合は、100倍した整数値に置き換えて演算を行います。結果の出力などが必要な場合は100で割った値を出力します。intやlong等 整数型の演算は正確に行われるため、このような方法を採ることができます。

NaNやInfinityの扱い

(Infinity – Infinity)や(0.0 / 0.0)の演算結果は 無効な値NaNになります。NaNは何かしらの値を表しているわけではないので、等値判定や大小比較を行うことができません。そのため、次の式は全てfalseが返ります。

  • NaN == NaN
  • NaN < NaN、NaN <= NaN
  • NaN > NaN、NaN >= NaN

唯一、NaN != NaN だけが常にtrueを返します。そのため、演算結果がNaNかどうか判定するには NaNとの比較ではなく、Float.isNaN()やDouble.isNaN()を呼び出さないといけないことに注意が必要です。

double result = ...;  // 何らかの演算

// 誤
if(result == Double.NaN) {  // 常にfalse
}

// 正
if(Double.isNaN(result)) {
}

また、演算にInfinityやNaNが含まれていると、結果もInfinityやNaNになってしまいます。したがって、演算結果がInfinityやNaNになる可能性がある場合は、演算結果がInfinityやNaNでないかどうか確認する必要があります。演算結果がInfinityや-Infinityかどうかは、isInfinite()で確認することができます。演算結果がNaNかどうかは、前述の通り isNaN()で確認することができます。

整数型のラップアラウンドの検出や防止

前述のプリミティブ型一覧の表で 各データ型の値範囲を示しましたが、整数型においては 値範囲の最大値を超えたり 最小値より小さくなった場合、オーバーフローが発生するわけではなく、演算結果がラップアラウンドされます。(これに対して 浮動小数点数の場合は 演算結果がFloat.POSITIVE_INFINITYやDouble.NEGATIVE_INFINITY等になります。)そのため、値範囲から外れるような数値を扱う場合はラップアラウンドの検出や防止をするために 独自に処理を行う必要があります単純な演算であれば ライブラリを利用することもできます)。

例えば int型の引数の四則演算・符号反転・絶対値算出を行うメソッドのそれぞれについて ラップアラウンドの検査を行う場合を考えます。ラップアラウンド検出時には ArithmeticExceptionを投げることにします。ラップアラウンドを検出するには 大きく4通りの方法があります。

  1. メソッド引数の事前検査を行う
    メソッドの引数がラップアラウンドを起こさないかどうか 演算を行う前に検査を行います。演算内容ごとに検査の内容が異なるため、実装およびテストの量が嵩みます。
  2. ワイドニング変換を利用してラップアラウンドを検出するメソッドを用意する
    ワイドニング変換を利用して 汎用的にラップアラウンドを検出するメソッドを用意して活用します。
    ただし、long型の場合は ワイドニング変換ができないため、次のBigIntegerを利用することになります。
  3. BigIntegerを利用してラップアラウンドを検出するメソッドを用意する
    BigIntegerを利用して 汎用的にラップアラウンドを検出するメソッドを用意して活用します。
  4. Mathクラスのラップアラウンド検出可能メソッドを利用する
    単純な演算であれば、Java SE 8からMathクラスに追加されたラップアラウンド検出可能メソッドを利用することができます。

Mathクラスに用意されているメソッドで事足りれば それを利用するのが最善です。そうでない場合は 1~3の中から選択することになります。残りの3つの方法の中では 3が最も簡潔ですが、効率が悪いという欠点があります。2は簡潔で効率も良いのですが、long型の場合は 適用することができません。そのため、まず4が適用できるか確認し、適用できなければ long型以外では2を、long型の場合は3を選択すると良いです。

それぞれ詳しく見ていきます。

メソッド引数の事前検査を行う

それぞれのメソッドの事前条件として ラップアラウンドが発生するかどうか検査します。演算ごとに条件を整理して 実装・テストをしないといけないため、面倒なわりに品質確保が大変です。各メソッドごとに条件を整理すると 次のようになります。

  • 加算
    • いずれかのオペランドが0の場合は ラップアラウンドは発生しません。
    • オペランドの符号が異なる場合は ラップアラウンドは発生しません。
    • 右項が正の数の場合、(左項+右項)が Integerの最大値より大きいと、ラップアラウンドが発生します。右項を移項すると、左項が(Integerの最大値-右項)より大きい場合になります。
    • 右項が負の数の場合、(左項+右項)が Integerの最小値より小さいと、ラップアラウンドが発生します。右項を移項すると、左項が(Integerの最小値-右項)より小さい場合になります。
  • 減算
    • 右項が0の場合は ラップアラウンドは発生しません。左項が0の場合は ラップアラウンドの可能性があります。
    • オペランドの符号が同じ場合は ラップアラウンドは発生しません。
    • 右項が正の数の場合、(左項-右項)が Integerの最小値より小さいと、ラップアラウンドが発生します。右項を移項すると、左項が(Integerの最小値+右項)より小さい場合になります。
    • 右項が負の数の場合、(左項-右項)が Integerの最大値より大きいと、ラップアラウンドが発生します。右項を移項すると、左項が(Integerの最大値+右項)より大きい場合になります。
  • 乗算
    • いずれかのオペランドが0の場合は ラップアラウンドは発生しません。
    • 右項が正の数の場合、(左項×右項)が Integerの最大値より大きいか Integerの最小値より小さいと、ラップアラウンドが発生します。両辺を右項で割ると、左項が(Integerの最大値÷右項)より大きいか(Integerの最小値÷右項)より小さい場合になります。
    • 右項が-1を除く負の数の場合、(左項×右項)が Integerの最大値より大きいか Integerの最小値より小さいと、ラップアラウンドが発生します。両辺を右項で割りますが、右項が負の数なので 不等号の向きが変わります。左項が(Integerの最大値÷右項)より小さいか(Integerの最小値÷右項)より大きい場合になります。
      右項が-1の場合を除くのは、(Integerの最小値÷右項)がラップアラウンドしてしまうためです。
    • 右項が-1の場合、左項がIntegerの最小値の場合にラップアラウンドが発生します。
  • 除算
    • 左項がIntegerの最小値で 右項が-1の場合に ラップアラウンドが発生します。
  • 符号反転
    • 引数がIntegerの最小値の場合に ラップアラウンドが発生します。
  • 絶対値算出
    • 同様に、引数がIntegerの最小値の場合に ラップアラウンドが発生します。

このように、単純な演算だけ見ても 演算ごとに検査内容が異なり 実装もテストも大変であることが分かります。具体的なコードの例は 以下のJPCERTのサイトを参照してください。

ワイドニング変換を利用してラップアラウンドを検出するメソッドを用意する

整数型のプリミティブ型の場合、ワイドニング変換をして 大きな型で演算することによって オーバーフロー発生時のラップアラウンドを防ぐことができます。演算自体ではラップアラウンドは発生しないため、演算結果が元のプリミティブ型に収まるかどうかの検査だけを行えばよく、汎用的に検査を行うことができます

汎用的な検査メソッド rangeCheck()と、それを利用したラップアラウンド検出可能な演算メソッドの例 someOperation() を次に示します。

// value がIntegerの値範囲から外れていないか検査する。
// 外れている場合は ArithmeticException が発生する。
public static long rangeCheck(long value) {
    if((value < Integer.MIN_VALUE || value > Integer.MAX_VALUE)) {
        throw new ArithmeticException("detect wrap around:" + value);
    }
    return value;
}

// ラップアラウンド検出可能な演算メソッド。
// 単純に a+b×c を行い結果を返す。
public static int someOperation(int a, int b, int c) {
    // オペランドが両方intだと 結果がラップアラウンドされてしまうため、
    // オペランドをlongにワイドニング変換する。
    long result = rangeCheck((long) a + rangeCheck((long) b * (long) c));
    return (int) result;
}

注意したいのは someOperation()メソッド内で、int型の引数 a・b・cを long型にワイドニング変換している点です。(b * c)や (a+rangeCheck()の結果) は オペランドが両方int型のため、long型に変換しないで演算を行うと Integerの値範囲外になった場合に ラップアラウンドが発生してしまいます。ラップアラウンドされた結果を rangeCheck()に渡しても 何の意味もありません。各引数を明示的にキャストする必要があります

long型の場合は ワイドニング変換が行えないため、次に挙げる BigIntegerを利用する方法を採ることになります。

BigIntegerを利用してラップアラウンドを検出するメソッドを用意する

概念的には 前述のワイドニング変換を利用してラップアラウンドを検出する方法と全く同じです。ワイドニング変換の代わりに、long型でも対応できるようにBigIntegerを利用します。

汎用的な検査メソッド rangeCheck()と、それを利用したラップアラウンド検出可能な演算メソッドの例 anotherOperation() を次に示します。

private static final BigInteger intMax = BigInteger.valueOf(Integer.MAX_VALUE);
private static final BigInteger intMin = BigInteger.valueOf(Integer.MIN_VALUE);
// value がIntegerの値範囲から外れていないか検査する。
// 外れている場合は ArithmeticException が発生する。
public static BigInteger rangeCheck(BigInteger value) {
    if(value.compareTo(intMax) == 1 || value.compareTo(intMin) == -1) {
        throw new ArithmeticException("detect wrap around:" + value);
    }
    return value;
}

// ラップアラウンド検出可能な演算メソッド。
// 単純に a+b×c を行い結果を返す。
public static int anotherOperation(int a, int b, int c) {
    BigInteger tmp = BigInteger.valueOf(b).multiply(BigInteger.valueOf(c));
    BigInteger result = rangeCheck(BigInteger.valueOf(a).add(tmp));
    return result.intValue();
}

long型にも同じように対応できるため簡潔です。しかし、一目では どのような演算が行われるのか分かりづらい点と、BigIntegerのインスタンスを生成する必要があるため 性能を犠牲にする点がデメリットとして挙げられます。

Mathクラスのラップアラウンド検出可能メソッドを利用する(Java SE 8~)

Java SE 8から java.lang.Mathクラスに ラップアラウンド検出可能ないくつかのメソッドが追加されました。単純な演算を行うメソッドだけですが、独自に実装する手間を省いてくれます。特筆すべきは、long型のラップアラウンドを検出するのに BigIntegerを使っていないことです。いくつかの算術演算とビット演算を組み合わせることにより ラップアラウンドを検出しているため、BigIntegerを使う場合に比べて効率的です

Mathクラスのラップアラウンド検出可能なクラスメソッド
演算結果がintをオーバーフローした場合に例外発生結果がlongをオーバーフローした場合に例外発生
加算addExact(int, int)addExact(long, long)
減算subtractExact(int, int)subtractExact(long, long)
乗算multiplyExact(int, int)multiplyExact(long, int)
multiplyExact(long, long)
インクリメントincrementExact(int)incrementExact(long)
デクリメントdecrementExact(int)decrementExact(long)
符号反転negateExact(int)negateExact(long)
ダウンキャストtoIntExact(long)

ご覧の通り、単純な演算と言っても 除算や絶対値算出などは用意されていないため、独自に実装する必要があります。

マルチスレッド環境でのラップアラウンドの検出と防止

AtomicIntegerのようなスレッドセーフなクラスにおいても、値範囲を超えた場合にはラップアラウンドが発生します。AtomicIntegerの場合も、同じようにラップアラウンドの検出と防止方法を適用することができますが、前述のいずれの方法も 検査と演算をアトミックに行うわけではないので少し工夫が必要です

ロックを掛けて 検査と演算をアトミックに行うこともできますが、それでは ロックフリーなAtomicIntegerを使う意味がなくなってしまいます。ロックフリーなまま ラップアラウンドを検出・防止するには 次のように行います。

public class SomeClass {

    // someOperation の処理は 先ほどの例と同じ
    // ラップアラウンド検出可能な演算メソッド
    public static int someOperation(int a, int b, int c) { ... }

    // 演算結果を保持するフィールド
    private final AtomicInteger atomIValue = new AtomicInteger();

    // ラップアラウンド検出可能な演算メソッド。スレッドセーフ版。
    public void applaySomeOperation(int a, int b, int c) {
        while(true) {
            int oldValue = atomIValue.get();   // ①現在値を取得
            int newValue = someOperation(a, b, c);    // ②ラップアラウンド検出可能な演算。
                                                      // someOperation() は前述の通り。
            // ③CAS命令で設定
            if(atomIValue.compareAndSet(oldValue, newValue)) {
                break;
            }
        }
    }
}

このクラスでは 演算結果をインスタンスフィールドatomIValueで保持することとします。①で現在値を取得し、②で先ほどの例のラップアラウンド検出可能な演算メソッドを呼び出します。ラップアラウンドが発生すれば ここでArithmeticExceptionが発生します。ラップアラウンドが発生しなければ 演算結果が返ります。③でCAS命令を利用して演算結果をatomIValueに設定します。①から③の間に 他のスレッドが atomIValueの値を変更していなければ trueが返り、ループ終了です。①から③の間に 他のスレッドが atomIValueの値を変更していれば falseが返るので、ループの先頭に戻って同じ処理をやり直します。

AtomicXXXクラスやCAS命令の詳細については「マルチスレッド」の章の「アトミックな複合操作を提供するクラスを利用する」を参照してください。

数値リテラルのアンダースコア

Java SE7から数値リテラルにアンダースコア(_)を含めることができるようになりました。数値の3桁区切りやMACアドレスの区切り位置にアンダースコアを挿入することにより 数値リテラルが読み易くなります。

int amountOfSales = 123_456_789;
long macAddress = 0x01_23_45_67_89_ABL;

アンダースコアは任意の桁区切りに使用できますが、次の位置では使うことができずエラーとなります。

  • リテラルの先頭、末尾:_123、123_などはNG
  • ピリオドの前後:123_.456、123._456などはNG
  • 基数の接頭辞(0xや0bなど)の直後:0x_AA、0b_101などはNG
  • 型の接尾辞(Lやf)の直前:100_L、1.01_fなどはNG

Java SE 7 Project Coin」(TECHSCORE)

符号なし整数(unsigned)(Java SE 8~)

Java SE 8からByte、Short、Integer、Longクラスに符号なし整数を扱うメソッドが追加されました。ここではそのうちのいくつか紹介します。

前段としてint型の値の16進数表記、符号付き10進数表記、符号なし10進数表記の対応表を以下に示します。

16進数、符号付き10進数、符号なし10進数の対応表
16進数符号付き10進数符号なし10進数
0x0000000000最小値
0x7fffffff2,147,483,647最大値2,147,483,647
0x80000000-2,147,483,648最小値2,147,483,648
0xffffffff-14,294,967,295最大値
符号なしint値 → long値変換

Integerクラス
public static long toUnsignedLong(int i);

引数を符号なしint値とみなして、対応するlong値を返します。

System.out.println(Integer.toUnsignedLong(-1));            // 4294967295(符号付き10進数で指定)
System.out.println(Integer.toUnsignedLong(0xffffffff));    // 4294967295(同じ値を16進数で指定)

int値-1は16進数で表すと0xffffffffとなります。0xffffffffを符号なし10進数で表すと冒頭の表のとおり4,294,967,295になります。

符号なしint値 → 文字列変換

Integerクラス
public static String toUnsignedString(int i);

(対応する従来のメソッド:  public static String toString(int i);)

引数を符号なしint値とみなして、対応する文字列を返します。

System.out.println(Integer.toString(-1));                    // "-1"(符号付10進数で指定)
System.out.println(Integer.toString(0xffffffff));            // "-1"(同じ値を16進数で指定)
System.out.println(Integer.toUnsignedString(-1));            // "4294967295"(符号付10進数で指定)
System.out.println(Integer.toUnsignedString(0xffffffff));    // "4294967295"(同じ値を16進数で指定)

toString()メソッドは引数を符号付きint値として扱い、toUnsignedString()メソッドは引数を符号なしint値として扱い、対応する文字列を返します。

符号なしint値文字列 → int値

Integerクラス
public static int parseUnsignedInt(String s);
(対応する従来のメソッド public static int parseInt(String s);)

引数を符号なしint値とみなして、対応する符号付きint値を返します。

System.out.println(Integer.parseInt("2147483647"));    // 2147483647(最大値)
System.out.println(Integer.parseInt("2147483648"));    // NumberFormatException(最大値より大きい)
System.out.println(Integer.parseInt("-2147483648"));   // -2147483648(最小値)
System.out.println(Integer.parseInt("-2147483649"));   // NumberFormatException(最小値より小さい)
System.out.println(Integer.parseInt("4294967295"));    // NumberFormatException(符号なしintの最大値)

System.out.println(Integer.parseUnsignedInt("4294967295"));  // -1(0xffffffffと解釈されるが
                                                             //   符号付きintで表現されて-1になる)
System.out.println(Integer.parseUnsignedInt("2147483648"));  // -2147483648(0x80000000と解釈されるが
                                                             //   符号付intで表現されて-2147483648になる)
System.out.println(Integer.parseUnsignedInt("4294967296"));  // NumberFormatException
                                                             // (符号なしintの最大値より大きい)
System.out.println(Integer.parseUnsignedInt("-1"));          // NumberFormatException
                                                             // (符号なしintの最小値より小さい)

int型は符号付きなので扱える範囲は-2,147,483,648 ~ 2,147,483,647となります。この範囲外の値を表す文字列をparseInt()メソッドに渡すとNumberFormatExceptoinが発生します。これに対してparseUnsignedInt()メソッドでは引数を符号なし整数として扱い、扱える範囲は0 ~ 4,294,967,295になります。例えば引数に”4294967295″を指定した場合は0xffffffffに変換して返します。(戻り値が符号付きのint型であるため-1が返ります。)

ラッパークラス

プリミティブ型には対応するラッパークラスが用意されています。(int型に対応するIntegerクラスなど。)総称型の型引数にはプリミティブ型を指定することができないため、対応するラッパークラスを指定します。

ラッパークラスではプリミティブ型を扱うための便利なメソッドが用意されています。例えばIntegerクラスでは他のプリミティブ型に変換するためのshortValue()・longValue()・doubleValue()といったメソッドや、int値を表す文字列からint値に変換するparseInt()・valueOf()といったクラスメソッドなどが用意されています。

また、ラッパークラスには対応するプリミティブ型に関する情報(最大値、最小値など)が定義されています。

オートボクシング(J2SE 5.0 ~)

オートボクシング(およびオートアンボクシング)の機能により、プリミティブ型と対応するラッパークラスの間の相互変換が簡単に行えるようになりました

Integer intObj = 123;  // int 値 -> Integerオブジェクトに変換:オートボクシング
int intVal = intObj;   // Integerオブジェクト -> int 値に変換:オートアンボクシング

特に恩恵を受けるのがコレクションへの要素の格納と取り出しの場面です。コレクションに格納できる要素はクラスオブジェクトに限られますが、プリミティブ型の要素を引数に渡すと ラッパークラスオブジェクトに変換して格納してくれます。(オートボクシング機能)逆にコレクションから取り出した要素を直接プリミティブ型の変数に代入することができます。(オートアンボクシング機能)この機能のおかげでコレクションに対する出し入れの際にラッパークラスへの変換を明示する必要がなくなりました。

List<Integer> list = new ArrayList<>();
list.add(123);  // int値 -> Integerオブジェクトに変換:ボクシング
list.add(456);

int intVal1 = list.get(0);  // Integerオブジェクト -> int値に変換:アンボクシング
ラッパークラスの注意点

プリミティブ型とラッパークラスは オートボクシング機能によりシームレスに変換されます。そのため 便利に使うことができるのですが、注意すべき点が何点かあります。

  • 比較演算子によってオートアンボクシングされたりされなかったりする。
  • 特定の型の特定の値範囲についてはキャッシュ機能が働く。
  • 二項数値昇格を意識する必要がある場合がある。
  • ラッパークラスは参照型であるため、nullを代入することが可能。
  • ラッパークラスの生成や変換には多少なりともコストがかかる。

それぞれの詳細について見ていきます。

比較演算子によってオートアンボクシングされたりされなかったりする

ラッパークラスのオブジェクトに対して 比較演算子 “>"、""、">="、"<=“を使って比較を行う場合は オートアンボクシングが行われ 値の比較が行われます。一方で”==“や”!=“を使ってラッパーオブジェクト同士の比較を行うとオートアンボクシングは行われず、値の比較ではなく オブジェクトの同一性判定が行われます。(ややこしいことに”==“や”!=“でラッパーオブジェクトとプリミティブ型を比較すると値の比較が行われます。)オートアンボクシングを期待して ラッパーオブジェクトのまま比較を行いたい場合に起こりやすい誤りです。

Integer integerA, integerB;
int intA;
...

// ラッパーオブジェクト同士の比較
if(integerA > integerB) {  // オートアンボクシングで値の比較が行われる
    // 処理...
}
if(integerA == integerB) {  // オートアンボクシングはされない。同一性の比較が行われる。
    // 処理...
}
// ラッパーオブジェクトとプリミティブ型の比較は問題ない
if(integerA > intA) {  // integerAはオートアンボクシングされ値の比較が行われる
    // 処理...
}
if(integerA == intA) {  // integerAはオートアンボクシングされ値の比較が行われる。
    // 処理...
}

ラッパーオブジェクト同士の値の比較を行いたい場合は 明示的にプリミティブ型に変換する必要があります。

Integer integerA, integerB;
...
int intA = integerA;
int intB = integerB;
if(intA == intB) {  // 値の比較を行う。
    // 処理...
}
特定の型の特定の値範囲についてはキャッシュ機能が働く

ラッパークラスオブジェクトの 値の比較同一性の比較違いを分かりづらくしているのがラッパークラスのキャッシュの仕組みです。値の比較と同一性の比較の違いを理解していると、次のようなコードの結果に戸惑うかもしれません。

Integer i1 = 100;
Integer i2 = 100;
System.out.println("i1 == i2:" + (i1 == i2));         // true
System.out.println("i1.eqauls(i2):" + i1.equals(i2)); // true 

i1とi2は同じオブジェクトを参照しています。そのため、"==“による同一性比較がtrueになります。

boolean型やint型などの一定範囲の値を オートボクシングでラッパークラスオブジェクトに変換する場合、キャッシュ機能が働きます。キャッシュ機能によって、同じプリミティブ値に対して同じラッパークラスオブジェクトが返されるため、上のような結果になります。

オートボクシングではなく、Integer.valueOf()でIntegerオブジェクトを取得した場合も 同様の結果になります。一方で、コンストラクタでインスタンスを生成した場合は、別のオブジェクトになることに注意が必要です(そもそも ラッパークラスのコンストラクタは非推奨のため 使うべきではありません)。

また、上の例で 初期値を100ではなく1000にすると、faleが返ります(JVMによってはtrueが返るかも知れません)。これは、Java言語仕様で int型の場合キャッシュしなければいけない範囲が-128~128と決まっているためです(その範囲外については 任意)。

Java言語仕様では データ型ごとに 次の範囲の値についてはキャッシュすることを定めています。それ以外の範囲については任意となっています。

ラッパークラスのキャッシュ範囲
プリミティブ型ラッパークラスキャッシュ範囲
boolean、byteBoolean、Byte全て
charCharacterU+0000~U+007F
short、intShort、Integer-128~128
二項数値昇格を意識する必要がある場合がある

オートボクシングでは、ときに二項数値昇格を意識する必要があります(二項数値昇格については「演算子」の章の「二項数値昇格」を参照してください)。少しわざとらしいコードになりますが、次に例を挙げます。

Set<Short> set = new HashSet<>();
for(short i = 0; i < 5; i ++) {
    set.add(i);    // 0~4が追加される
}
for(short i = 0; i < 5; i ++) {
    if(i == 2)     {
        set.remove(i + 1); // 3を削除したい
    }
}
System.out.println(set);    // [0, 1, 2, 3, 4]  3は削除されない

set.remove()の引数に「i + 1」を渡していますが、これは二項数値昇格によって iがint型に変換され、結果もint型になります。また、remove()の引数がObjectであるため、Integerにオートボクシングされます。その結果、setの要素には Short(3)は存在しますが Integer(3)は存在しないため、何も削除されずに終わってしまいます。

このような誤りは 抽象度の高いクラスを引数に取るメソッドに 二項数値昇格を伴う演算結果を渡すような場合に発生し得ます。(上の例では 書き手はshort型を渡したつもりが 実際にはint型が渡され、受取側はShortもIntegerも受け取れるため、int型が渡されていることに気付かないことになります。)

しかし、この問題は IDEを使って適切な警告レベルを設定していればコーディングの時点で誤りに気付くことができます。例えば Eclipseでは デフォルトの警告レベルの設定で、set.remove()の行に 「ありそうもない Collection の remove(Object) の引数型 int」の旨の警告が表示されるため、コーディングの時点で誤りに気付くことができます。

ラッパークラスは参照型であるため、nullを代入することが可能

ラッパークラスは参照型であるため、nullを代入することが可能です。特にオートアンボクシングを期待して ラッパーオブジェクトとプリミティブ型を”==“や”!=“で比較を行う場合、ラッパークラスのオブジェクトがnullの場合にNullPointerExceptionが投げられます。これは null参照に対してメソッドを呼び出しているわけではないのに NullPointerExceptionが発生するため、混乱を招き易いです。

Integer integerA = null;
int intB;
...
if(integerA == intB) {  // integerAがnullの場合 NullPointerExceptionが発生する
    // 処理...
}
ラッパークラスの生成や変換には多少なりともコストがかかる

ラッパークラスの生成や変換には多少なりともコストがかかります。そのため、ループの中で繰り返しボクシングやアンボクシングを行うような場合は 性能劣化を招きます

文字列からプリミティブ型やラッパークラスに変換

文字列からプリミティブ型やラッパークラスに変換するには、各ラッパークラスのクラスメソッドvalueOf()やparseXXX()を利用します。(XXXはプリミティブ型の名前で、例えばIntegerの場合はvalueOf()とparseInt()です。)

Integerクラス
public static Integer valueOf(String s);
public static int parseInt(String s);

2つのメソッドの違いは戻り値の型でvalueOf()はラッパークラスのオブジェクトを、parseXXX()はプリミティブ型を返します。しかし、オートボクシング機能によりどちらを使ってもプリミティブ型もラッパークラスのオブジェクトも得ることができます。(Javaの内部ではvalueOf()からparseInt()を呼び出していて、valueOf()ではキャッシュしているIntegerインスタンスがあれば それを返しています。)

valueOf()やparseXXX()では基数(10進数や16進数など)を指定することができ、10進数以外の形式の文字列を指定することもできます。

参照型

クラスのインスタンスや配列は参照型になります。参照型の特徴は次の通りです。

  • new演算子でインスタンスや配列を作成する。
  • 何も参照していないことを示すnullを代入することができる。

new演算子で生成したインスタンスや配列はヒープ領域に置かれ、変数にはヒープ領域内の位置を示すポインタのような物が格納されます。ただし、CやC++のように このポインタのような物を演算することはできません。

参照型については、別途「クラス」・「インタフェース」・「配列」の章で詳細に触れていきます。

String

StringはJava標準ライブラリのクラスの1つですが、プリミティブ型と同様に使用頻度の高く重要なクラスなので個別に取り上げます。始めに根幹となる文字コードについて触れ、インスタンス生成時の注意点や主なメソッドについて見て行きます。

文字コード

Javaの内部文字コードはUnicodeの符号化の一つであるUTF-16です。UnicodeではなくUTF-16であるというのがポイントで、符号化方式を直接意識する必要があります。Javaが登場した当時はUnicodeが16bit固定長の仕様だったため char型は16bit(2Byte)に決められ 全てのUnicode文字を表現できる予定でした。しかし、Unicodeのバージョンアップに伴い16bitが21bitに拡張され、Javaでも対応する必要が出てきました。Javaではchar型の16bitはそのままにして、CharacterクラスやStringクラスで変更を吸収するような形が取られてきました。Javaの内部文字コードの扱いの変遷にはUnicodeの仕様や歴史の理解が必要なため、まずはUnicodeの仕様や歴史について説明します。

UnicodeとUTF-16

Unicodeでは「符号化文字集合」(UCS:Universal Coded character Set)と「文字符号化方式」(character encoding scheme)が明確に分かれています。名前が長いので ここでは便宜上「文字集合」と「符号化方式」と呼ぶことにします。

文字集合はUnicodeで表現できる文字の集まりのことを表し 一意の番号が振られています。この一意の番号は0x0000~0x10FFFFの21bitでUnicodeコードポイントと呼び、「U+」に続けて16進数の数字を付けて表します。(U+~で表す値をUnicodeスカラ値と呼びます。)

符号化方式は その名の通り文字の符号化の方式で、符号化の主な目的はデータの圧縮です。符号化することによりデータを圧縮できますが、変換処理が必要なため処理負荷が伴います。非圧縮であれば変換処理の負荷はかかりませんがデータ容量が嵩みます。圧縮率の高い方式であれば容量は抑えられますが、たいていは相応の処理負荷を必要とします。符号化方式は容量と処理負荷の兼ね合いになります。

符号化方式にはUnicodeコードポイントをそのまま使う非圧縮のUTF-32、2または4バイトに圧縮するUTF-16、1~4バイトに圧縮するUTF-8などがあります。下の表のように、同じ文字でも符号化方式によって異なるバイト列に符号化されます。この中で Javaの内部文字コードはUTF-16の符号化方式を採用しています。

符号化方式の違いによるバイト列の違い
文字の例UnicodeコードポイントUTF-32UTF-16UTF-8
A(大文字A)U+004100 00 00 4100 4141
Ω(オーム)U+03A900 00 03 A903 A9CE A9
語(ご)U+8A9E00 00 8A 9E8A 9EE8 AA 9E
𠀋
(丈の右上に点)
U+2000B00 02 00 0BD8 40 DC 0BF0 A0 80 8B
UnicodeとJavaの歴史

Unicodeは1980年代当初の構想では16bit(2Byte)で1文字を表す予定だったので、Javaではchar型を2Byteとしてchar型1つでUnicodeの1文字を表していました。しかし、世界中の各国から文字追加の要求が起こった結果 早々に16bitでは足りなくなり、21bitに拡張することになりました。(最近では世界各国の文字に加えて絵文字なども組み込まれています。)

Unicodeが16bitだった頃は UTF-16が非圧縮16bit固定長の符号化方式でしたが、Unicodeが21bitに拡張されたことによりUTF-16の16bit固定長では足りなくなってしまいました。そこでサロゲートペアという方法が導入され、1文字=16bit の基本は維持しつつ、一部の文字については 1文字=16bitのペア(32bit)で表現することになりました。1文字=16bitの範囲は基本多言語面BMP)と呼ばれ、1文字=16bitのペアの範囲はいくつかの領域に区切られ追加多言語面SMP)や追加漢字面SIP)等の名前が付けられていますが ここではまとめて拡張領域と呼ぶことにします。

拡張領域の文字をUTF-16で符号化する場合は Unicodeコードポイントを単純に16bitで分けるのではなく、16bitのペアそれぞれがBMPの未使用領域(サロゲートコードポイント)に収まるように変換されます。変換された16bitのペアをサロゲートペアと呼び、それぞれを高位サロゲート下位サロゲートと呼びます。

BMPの文字はUnicodeコードポイントとUTF-16の符号化値は同じですが、拡張領域の文字はUnicodeコードポイントとUTF-16の符号化値(サロゲートペア)は異なります。例を挙げると、冒頭の表でBMPの文字「A」「Ω」「語」はUnicodeコードポイントとUTF-16符号化値が同じですが、拡張領域の文字「𠀋」はUnicodeコードポイントとUTF-16符号化値が異なります。

Javaで文字を扱う場合、その文字がBMPの文字なのか拡張領域の文字なのかを区別する必要があります。BMPの文字なのかそうでないかで 使うメソッドが違ったり、メソッドが返す値が変わってくるからです。アルファベット・記号や大抵の日本語はBMPに収まるため、以前は「𠀋」のような特殊な文字を使わない限りはサロゲートペアを意識する必要はありませんでした。しかし、近年絵文字が拡張領域に収められるようになってきたため、絵文字を扱うような開発を行う場合には 文字コードの理解が不可欠になります

尚、PythonやRubyといった言語では Unicodeコードポイントを指定できるため、拡張領域の文字を扱う場合でも コードポイントやサロゲートと言ったUTF-16の符号化の部分を意識する必要はありません。Javaの場合はUnicodeの変遷に対応してきたため このようになってしまっているということです。

サロゲートペア入門」(CodeZine)
Unicode一覧表」(Wiki)
Unicode のサゲートペアとは何か」(ひだまりソケットは壊れない)
文字コードの考え方から理解するUnicodeとUTF-8違い」(ギークを目指して)
Unicode, UTF についひっかかったので色々メモ<」(Qiita)
【第511回】UnicodeじゃなくUTF-8にしてください!?」(イジハピ)
エンコード / デコード」(一括エンコード/デコード)

Unicodeと正規化

Unicodeには 結合文字列があります。例えば 「が」という文字は、次の2通りで表すことができます。

  • 「が(U+304C)」
  • 「か(U+304B)」と「濁点(U+3099)」の組み合わせ
    (ただし、フォントによっては 正しく表示されません。WindowsのEclipseのデフォルトフォントConsolasでは 正しく表示されません。メイリオ等にすると正しく表示されます。)

見た目は同じ「が」なのですが、文字数(codePointCount()の結果)や 文字コードが異なります

後者の「濁点(U+3099)」は、結合するための文字であり 単独で使ってはいけないもので、結合文字(combining character)と言います。これに対して、結合文字で結合される対象の「か(U+304B)」は 基底文字(base character)と言います。基底文字に結合文字を結合してできた文字を 結合文字列と言います。見た目は一文字ですが、codePointCount()の結果は2になります。一方で、結合文字列相当を一文字で表せる「が(U+304C)」は 合成済み文字(precomposed character)と言います。

見た目は同じ「が」でも、結合文字列と合成済み文字では 文字数・文字コードが異なるため、equal()で比較をすると falseになってしまいます。そのため、結合文字と合成済み文字が混在していると 比較や検索が機能しなくなります。Unicodeでは これに対応するために、正規化を規定していて Java SE 6で正規化に対応しました。正規化によって 見た目が同じ文字(場合によっては意味的に同じ文字)は 同じコードにすることができるため、比較や検索を正しく行うことができるようになります。

Unicodeの正規化には次の4種類があります。

  • NFD(Normalization Form Canonical Decomposition)
    正準等価性に基づく分解
  • NFC(Normalization Form Canonical Composition)
    正準等価性に基づく分解後、正規等価性に基づいて再度合成
  • NFKD(Normalization Form Compatibility Decomposition)
    互換等価性に基づく分解
  • NFKC(Normalization Form Compatibility Composition)
    互換等価性に基づく分解後、正規等価性に基づいて再度合成

分解は 合成済み文字列を 結合文字列に変換するような処理になります。合成は その逆です。

等価性には 正準等価性互換等価性がありますが、大雑把に言うと 正準等価性は見た目も機能も同じ文字を等価とみなしますが、互換等価性は 等価とみなす範囲が広くなります。例えば、次のようになります。

  • 半角「ガ(U+FF76 U+FF9E)」を 全角「ガ(U+30AC)」と等価とみなすことができる
  • 丸囲み文字「①(U+2460)」を 通常の文字「1(U+0031)」と等価とみなすことができる
  • 組み文字「㈱(U+3231)」を「(株)(U+0028 U+682A U+0029)」と等価とみなすことができる

しかし、等価性と言っても 双方向に等価とみなすことはできません。互換等価性では、特殊な形(半角「ガ」)を 一般的な形(全角「ガ」)に変換することができる と言った方が適切です。特殊な形から一般的な形へは不可逆変換であり、一般的な形から特殊な形には変換できないことに注意が必要です(互換等価性によって 半角「ガ」から全角「ガ」には変換できますが、半角「ガ」に戻すことはできません)。

正準等価性の場合は少しやっかいで、特殊な形を正規化しても ほとんどの場合は変換されないのですが、一部 一般的な形に変換される物があります。前述の「ガ(U+FF76 U+FF9E)」、「①(U+2460)」、「㈱(U+3231)」を NFDやNFCで正規化しても変化はないのですが、「神(U+FA19)」のような旧字体を NFDやNFCで正規化すると「神(U+795E)」に変換されます。「神」は UnicodeのCJK互換漢字の文字で 「神」はCJK統合漢字の文字であり、CJK互換漢字の文字を NFDやNFCで正規化すると CJK統合漢字に変換されることに注意が必要です。

外部から検索文字を入力する場合や、UIからの入力に<script>タグ等 不正な文字が含まれていないか検査する場合等には 入力文字を正規化した後に検索や検査を行う必要があります。また、上述のように 正規化すると元に戻せない場合があるため、正規化によって 元の情報が失われても構わないかどうか確認する必要があります

文字コード地獄秘話 第2話:聖母マリアよ、二人を故別々に?」(ALBERT Engineer Blog)
文字コード地獄秘話 第3話:後戻りの効かないnicode正規化」(ALBERT Engineer Blog)
Unicodeの特殊な文字 “結合文字列”」(ものかの)

Unicode非対応文字の除外などは検証の前に行う

正規化をする場合は 正規化をしてから 文字列の正当性の検証を行う必要があり、順序が大切であることを説明しました。このことは 正規化に限った話ではなく、文字列に何らかの変更を加える場合は、文字列の正当性を検証する前に行うことが重要です

特にやってしまいがちなのが、Unicode非対応文字の除外による文字列の変更です。他の文字コードからUnicodeに変換する場合などに、ベンダ依存文字などのUnicode非対応の文字が含まれると U+FFFD(REPLACEMENT CHARACTAR)に変換されます。文字列に U+FFFDが含まれると 文字列を表示する場合等で何かと不便なため 除外したくなるかも知れません。このような Unicode非対応文字を除外する場合は、文字列の正当性の検証の前に行う必要があります。

例えば クロスサイトスクリプティング(XSS)の対策として、HTMLの入力に”<script>”タグが含まれていないかどうかの検査を行う場合を考えます。入力文字列に”<script>”が含まれていないことの検証を行った後に Unicode非対応文字の除外処理を行うと、入力文字列が”<scr\uFFFDipt>” の場合に 検証をすり抜けてしまいます。

Stringインスタンスの生成

Stringは他のクラスのようにnew演算子でインスタンス化はせずに変数に文字列リテラルを直接代入します

String str1 = "SomeString";

newでコンストラクタを呼び出して文字列リテラルを渡すと、文字列リテラルでインスタンスが一つ作成され、new演算子で別のインスタンスが生成され、二重にインスタンスが作成されることになり無駄になってしまいます

String str = new String("SomeString");  // 二重にインスタンスが生成される。

文字列リテラルはダブルクォート「”」で囲みます。シングルクォート「’」は単独の文字リテラルを囲むときに使います。文字列の中にダブルクォート自体や制御文字を含めたい場合はバックスラッシュ「\」でエスケープします。

String str = "string";  // 文字列リテラル
char ch = 'A';  // 文字リテラル
String includeNewLine = "first\nsecond";  // \n は改行
String includeTab = "cell1\tcell2";  // \t はタブ

文字を Unicodeコードポイントで記述することもできます。ただし、拡張領域の文字についてはUnicodeコードポイントではなくサロゲートペアを指定する必要があります

String japanese = "\u65e5\u672c\u8a9e";  // "日本語"
String str1 = "\u2000b";      // " b":「𠀋」のつもりでUnicodeコードポイントを指定しても
                              //    下4桁のみが有効なため \u000b と解釈されてしまう。
String str2 = "\ud840\udc0b";  // "𠀋":拡張領域の文字はサロゲートペアを指定する必要がある。

文字列操作

文字列操作を行う主なメソッドを以下の表にまとめます。

Stringクラスのインスタンスは不変(immutable)オブジェクトです。不変オブジェクトということは、操作系のtrim()やreplace()といったメソッドを呼び出しても 自身が持つ文字列は変更されず、操作を行った結果の文字列を指す別のオブジェクトを返すということです。

Stringクラスに限らず、クラスが不変か可変かを理解しておくことは重要です。Java APIドキュメントを呼んでも不変か可変かの記述がない場合は サンプルコードを書いて確認するのも一つの手です。

Stringクラスの主なメソッド
分類操作メソッド概要
取得系長さを取得lengthcharの個数を返します。拡張領域の文字を含まない場合は文字数と一致します。
codePointCount文字数を返します。拡張領域の文字を含む場合は 上のlengthではなくこちらを使います。
文字を取得charAt指定した位置のcharの値を返します。拡張領域の文字の場合、高位、下位の順番でサロゲートペアが並んでいます。
codePointAt指定した位置のUnicodeコードポイントを返します。位置はchar配列中の位置を指定します。
拡張領域の文字の場合 高位サロゲートの位置を指定する必要があります。下位サロゲートの位置を指定しても下位サロゲートの値がそのまま返ってきます。
部分文字列取得substring指定された開始・終了位置の部分文字列を返します。
比較系一致するかどうか判定equals文字列が一致するかどうか判定します。別々のStringオブジェクトでも文字列が一致すればtrueが返ります。
equalsIgnoreCase大文字・小文字の区別をしないで一致するかどうか判定します。
部分比較startsWith先頭が指定した文字列と一致するかどうか判定します。
endsWith末尾が指定した文字列と一致するかどうか判定します。
大小比較compareTo文字列の辞書順での大小を比較します。
compareToIgnoreCase大文字・小文字の区別をしないで辞書順での大小を比較します。
空かどうか判定isEmpty文字列が空かどうか判定します。
検索系含まれるかどうか判定contains指定した文字が含まれるかどうか判定します。
出現位置を取得indexOf指定した文字列が出現する位置を返します。出現しない場合は-1を返すので 指定した文字が含まれるかどうかの判定に使うこともできます。
lastIndexOf末尾から検索して指定した文字列が出現する位置を返します。出現しない場合は-1を返します。
操作系結合+演算子文字列を結合します。実質的にはStringBuilder.append()として処理されます。Stringの+演算子は 多用すると性能に影響が出るので注意が必要です。(後述します。)
concat文字列を結合します。
置換replace置換対象文字(列)と置換後の文字(列)を指定して文字列を置換します。正規表現は使えず、正規表現を使う場合はreplaceFirst()やreplaceAll()を使用します。
空白除去trim先頭と末尾の空白(半角スペース、改行、タブ)を取り除きます。
分割
結合
分割split指定した区切り文字で分割します。区切り文字には正規表現を使うことができます。
結合join指定した連結文字で複数の文字列を連結します。
書式書式整形format指定した書式の文字列を作成します。C言語のsprintf()と似たような働きをします。指定した書式で文字列を出力する場合はSystem.out.printf()を利用します。

繰り返しになりますが、操作系のメソッドは自身の状態を変更するのではなく、操作した結果の別のインスタンスを返すことに注意が必要です。

immutableなStringとmutableなArrayListな話」(define-ayalog ‘())

StringBuilder/StringBuffer

+演算子で文字列を結合する場合、実質的にはStringBuilderStringBufferに置き換えられます。

String str1 = "abc";
String str2 = "def" + s;

// 実質的に以下のように置き換えられます。
String str1 = "abc";
String str2 = (new StringBuilder("def")).append(str1).toString();

StringBufferはStringBuilderのスレッドセーフ版です。各メソッドにsynchronizedがついているため、シングルスレッドで利用する場合は ロック獲得・解除のオーバーヘッドが無駄になります。シングルスレッドでアクセスする場合はStringBuilderを用います

繰り返しループの中で+演算子による文字列結合を行うと、そのたびにStringBuilderインスタンスが生成されてしまいパフォーマンスの低下を招いてしまいます。そのため繰り返処理の中で文字列結合を行う場合は ループの外でStringBuilderインスタンスを生成してから append()メソッドで追加していきます

// 問題のある使い方
String str = "";
for(int i = 0; i < 10000; i ++) {
    str += "a"; // 毎回 StringBuilder インスタンスを生成してしまう
}
System.out.println(str);

// 改善されたコード
StringBuilder builder = new StringBuilder();  // ループの外でStringBuilderを生成
for(int i = 0; i < 10000; i ++) {
    builder.append("a");
}
System.out.println(builder);

文字の区切り

前述の通り Javaの内部文字コードは UTF-16であり、BMPの文字は1つのchar型で表すことができますが、拡張領域の文字を表すには 2つのchar型が必要です。更に Unicodeには結合文字列があるため、見た目は1つの文字が2つ以上のchar型で構成されていることもあります。そのため、文字の区切りを判定するためには、ちょっとしたテクニックが必要になります。

例えば、任意の文字列を受けとり、最初から区切り文字の手前までを返すメソッドを作成するとします。(例:「あがる,さがる」という文字列を渡すと、「あがる」という文字列を返す。)区切り文字は Character.isLetter(char)の結果が falseであることとすると、このメソッド extractFirst1()は次のようになります。

public static String extractFirst1(String str) {
    char ch;
    int i;
    for(i = 0; i < str.length(); i += 1) {
        ch = str.charAt(i);
        if(!Character.isLetter(ch)) {
            break;
        }
    }
    return str.substring(0, i);
}

public static void main(String[] args) {
    String str1 = "あがる,さがる";
    System.out.println(extractFirst1(str1));    // あがる
}

Character.isLetter(char)は 引数がchar型であるため、拡張領域の文字に対応できません。Characterクラスには 拡張領域の文字にも対応している isLetter(int)が用意されているので、拡張領域の文字にも対応できるようにするには そちらを使うようにします

public static String extractFirst2(String str) {
    int ch;
    int i;
    for(i = 0; i < str.length(); i += Character.charCount(ch)) {
        ch = str.codePointAt(i);
        if(!Character.isLetter(ch)) {  // 引数はcharではなくint
            break;
        }
    }
    return str.substring(0, i);
}

public static void main(String[] args) {
    String str2 = "𠀋,𠀉,𠂌";  // いずれも拡張領域の文字

    System.out.println(extractFirst2(str2));    // 𠀋
}

これで 拡張領域の文字にも対応することができるようになりますが、このメソッドは結合文字列には対応できません。isLetter()に結合文字を渡すと falseが返るため、結合文字列を渡すと 次のように結合文字の手前までの文字列が返ってきてしまいます。

public static void main(String[] args) {
    String str3 = "\u3042\u304b\u3099\u308b,さがる";    // あがる(結合文字列),さがる
    System.out.println(extractFirst2(str3));    // あか
    // isLetter()に U+3099(濁点)を渡すと false が返るため
}

結合文字列に対応するには Characterクラスのメソッドでは吸収できません。このような場合は BreakIteratorクラスを使うと 文字の境界を検出することができます

public static String extractFirst3(String str) {
    BreakIterator it = BreakIterator.getCharacterInstance();
    it.setText(str);
    
    int start = it.first();
    for(int end = it.next() ; end != BreakIterator.DONE; start = end, end = it.next()) {
        int ch = str.codePointAt(start);
        if(!Character.isLetter(ch)) {
            break;
        }
    }
    
    return str.substring(0, start);
}

public static void main(String[] args) {
    String str3 = "\u3042\u304b\u3099\u308b,さがる";    // あがる(結合文字列),さがる
    System.out.println(extractFirst3(str3));    // あがる
}

このように BreakIteratorを上手く利用することによって、結合文字列が混在している状況でも 適切に文字の区切りを検出することができます。

文字コード変換

Stringクラスには、指定した文字コードにエンコード・デコードする次のようなメソッドやコンストラクタが用意されています。

① Stringから 指定した文字コードにエンコードしたバイト配列を返すメソッド。

byte[]  getBytes(String charsetName)  throws  UnsupportedEncodingException

② 指定した文字コードのバイト配列をデコードして Stringオブジェクトを生成するコンストラクタ。

String(byte[] bytes,  String charsetName)

しかし、いずれも変換後の文字コードに変換できない文字が含まれている場合、動作は不定となります。試しにShift-JISに変換できない文字「𠀋」を表すStringオブジェクトに対して①を呼び出してみると、例外が発生するわけでもなく 「?」(0x3F)を表す長さ1のbyte配列が返ってきました。そのため、変換後の文字コードに対応できない文字が含まれていても、それに気付くことができません。

StringクラスのJavaDocにも記載されているように、異なる文字コード間でエンコード・デコードを行う場合は CharsetEncoderクラスやCharsetDecoderクラスを利用します。例えば、①のString.getBytes(String)は 次のようなコードで置き換えることができます。

Charset charset = Charset.forName("Shift_JIS");
CharsetEncoder encoder = charset.newEncoder();
ByteBuffer byteBuf = encoder.encode(CharBuffer.wrap(str));
byte[] bytes = new byte[byteBuf.limit()];
byteBuf.get(bytes);

Shift-JISに変換できない文字が含まれているような場合には、encoder.encode()の呼び出しで UnmappableCharacterExceptionが発生するので、エラーに気付くことができます。

上のコードのByteBufferは Bufferクラスのサブクラスです。BufferクラスのサブクラスにはCharBufferやIntBufferなどがあります。CharBufferは CharSequenceインタフェースも実装していて StringBuilderと似ていますが、CharBufferは固定長で StringBuilderのように自動的に拡張されることはありません。また、CharBufferは 現在書き込みを行っているのか 読み出しを行っているかを意識する必要があり、正しく機能させるためには 正しい使い方をする必要があります。使い方を誤ると正しく機能しません。Bufferクラスについては 次のサイトで分かり易く説明されています。

Buffer(ByteBuffer・CharBuffer)」(ひしだま’s 技術メモページ)