NUM14-J. シフト演算子を正しく使う
『Java言語仕様』§15.19「シフト演算子」[JLS 2015] によると、Javaのシフト演算子には以下の特性がある。
>>右シフトは算術シフトであり、>>>右シフトは論理シフトである。boolean型、float型、double型にはビットシフト演算子を使えない。- シフトされる値(左オペランド)が
int型の場合、右オペランドの下位5ビットのみがシフト距離として使われる。つまり、シフト距離は右オペランドの値を31(0x1F)でマスクしたものである。したがって、実際に使われるシフト距離は、常に0以上31以下となる。 - シフトされる値(左オペランド)が
long型の場合、右オペランドの下位6ビットのみがシフト距離として使われる。つまり、シフト距離は右オペランドの値を63(0x3F)でマスクしたものである。したがって、実際に使われるシフト距離は、常に0以上63以下となる。
シフト演算の右側の値は、シフト演算の左側の数値の型に対して適切な範囲内でなければならない。すなわち、左側の数値の型がlongの場合、右側の値は[0, 63]の範囲内でなければならず、それ以外の場合は[0, 31]の範囲内でなければならない。
算術シフトと論理シフト
Java言語仕様§15.19 [JLS 2015] では、算術シフト演算子の動作を以下のように定義している。
n >> sの値は、符号拡張によりnをsビット右シフトしたものである。結果の値は、floor(n / 2s)である。非負値のnに対しては、これは整数除算演算子/で計算される、2のs乗による切り捨て整数除算と同等である。
また、Java言語仕様は、論理シフト演算子の動作を以下のように定義している。
n >>> sの値は、ゼロ拡張によりnをsビット右シフトしたものである。
nが正の場合、結果はn >> sの結果と同じである。nが負で、左オペランドの型がintの場合、結果は式(n >> s) + (2 << ~s)の値と等しい。nが負で、左オペランドの型がlongの場合、結果は式(n >> s) + (2L << ~s)の値と等しい。追加された項
(2 << ~s)と(2L << ~s)は、伝播した符号ビットを打ち消す。シフト演算子の右オペランドは暗黙的にマスクされるため、
~sをシフト距離として使う場合、int型の値をシフトする際には31 - sと等しくなり、long型の値をシフトする際には63 - sと等しくなることに注意が必要である。
論理シフト演算子が必要な場合に算術シフト演算子を使わないこと。
違反コード(算術と論理)
以下の違反コード例では、メソッドcountOneBitsは負の入力に対して無限ループに陥る。>>演算子は論理シフトではなく算術シフトを行う。
static int countOneBits(long value) {
int bits = 0;
while (value != 0) {
bits += value & 1L;
value >>= 1; // 1ビットの符号付き右シフト
}
return bits;
}
適合コード(算術と論理)
以下の適合コードでは、論理シフト演算子>>>を使って、空いたビットをクリアしている(つまり、左側にゼロのビットを挿入している)。
static int countOneBits( long value ) {
int bits = 0;
while (value != 0) {
bits += value & 1L;
value >>>= 1;
}
return bits;
}
格上げ
シフトされるオペランドがbyte型、short型、char型の場合、これらはint型に格上げされる。オペランドがint型またはlong型の場合、その型が維持される。他の算術演算子とは異なり、シフト演算子の左オペランドの格上げは右オペランドの型に依存しない。したがって、左オペランドがbyte型の場合、シフト距離(右オペランド)がlong型であっても、int型に格上げされる。
違反コード(格上げ)
以下の違反コード例では、プログラマはバイト値を右に2ビットシフト(ゼロ埋め)することを意図している。しかし、Java言語仕様は、左オペランドがint型またはlong型(この場合はint型)に格上げされる必要があると規定しており、この格上げは符号拡張を行う。格上げのため、負の入力値に対するシフトの結果は大きな正の数となり、プログラマーはこの結果に驚くかもしれない。
byte b = /* 初期化 */;
int result = b >>> 2;
適合コード(格上げ)
以下の適合コードでは、ビットマスクを使用して上位24ビット(符号拡張によって生成された値)をクリアし、意図した結果を生成している。
byte b = /* 初期化 */;
int result = ((int) b & 0xFF) >>> 2;
明示的なint型へのキャストは冗長だが、期待される型を明確にする。
シフト距離の切り捨て
左オペランドがint型の場合、右オペランド(exp)は31(0x1F)でマスクされる。左オペランドがlong型の場合、右オペランド(exp)は63(0x3F)でマスクされる。したがって、シフト距離が左オペランドのビット数を超えると、シフト距離は(距離 % ビット数)として解釈される。
違反コード(切り捨て)
以下の違反コード例では、シフト距離の切り捨てを避けるための明示的な範囲チェックを行っていない。
public int doOperation(int exp) {
// 2^expを計算する
int temp = 1 << exp;
// 他の処理を行う
return temp;
}
適合コード(切り捨て)
以下の適合コードでは、シフト距離を範囲チェックして予期せぬ動作を避けている。
public int doOperation(int exp) throws ArithmeticException {
if ((exp < 0) || (exp >= 32)) {
throw new ArithmeticException("Exponent out of range");
}
// 2^expを安全に計算する
int temp = 1 << exp;
// 他の処理を行う
return temp;
}
算術演算に対する明示的な範囲チェックの詳細は「NUM00-J. 整数オーバーフローを検出あるいは防止する」を参照。
違反コード(切り捨てゼロ化)
以下の違反コード例では、iの値を増加させながら値-1のシフトを試みており、プログラマは32回の反復後に結果が0になると思い込んでいる。しかし、シフトが発生する際に考慮されるのはiの最下位5ビットのみであり、iの値が32に達すると、最下位5ビットの値は0になる。そのため、int型の値を32ビットシフトしようとすると、元の値(−1)が返され、期待される値0にはならない。したがって、実際にはループは決して終了しない [Bloch 2005]。
int i = 0;
while ((-1 << i) != 0) {
i++;
}
適合コード(切り捨てゼロ化)
この適合コードは、32回連続してシフトを行い、32回目の反復で値0を達成する。
for (int val = -1; val != 0; val <<= 1) { /* ... */ }
乗算と除算
「NUM01-J. 同一のデータに対してビット演算と算術演算の両方を行わない」を参照。
リスク評価
シフト演算子の使用を誤ると、予期せぬ結果につながり、不規則な制御フローや予期せぬプログラムの動作を引き起こす可能性がある。
|
ルール |
深刻度 |
可能性 |
自動検出 |
自動修正 |
優先度 |
レベル |
|---|---|---|---|---|---|---|
|
NUM14-J |
低 |
中 |
不可 |
不可 |
P2 |
L3 |
自動検出
|
ツール |
バージョン |
チェッカー |
説明 |
|---|---|---|---|
| PVS-Studio |
7.38 |
V6034 |
|
関連ガイドライン
参考文献
| [Bloch 2005] |
|
| [FindBugs 2008] | ICAST_BAD_SHIFT_AMOUNT |
|
[JLS 2015] |
翻訳元
これは以下のページを翻訳したものです。
NUM14-J. Use shift operators correctly (revision 19)



