- JAVA
Javaビット演算入門|シフト演算>>と>>>の違いとフラグ管理
ビット演算とは、整数を2進数の0と1の並びとして扱い、桁ごとに計算したり、桁を左右にずらしたりする演算です。 Java では &(AND)、|(OR)、^(XOR)、~(NOT)の4つのビット演算子と、<<、>>、>>> の3つのシフト演算子が使えます。なかでも >> と >>> は、負の数を右にずらしたときに、空いた左側を1で埋めるか0で埋めるか が違います。
この記事では、-8 を >> と >>> でシフトした結果を 32桁の2進数で0埋めして並べ、2の補数の仕組みから違いを説明します。後半では、権限フラグを題材に「立てる・下ろす・判定する・反転する」のコピペ用コードと実行結果、int 型で32以上シフトしたときの挙動、EnumSet との使い分けまで扱います。コードはすべて OpenJDK 21.0.10 で動かし、出力をそのまま載せています。
Javaのビット演算子・シフト演算子の早見表
a = 12(2進数で 1100)、b = 10(2進数で 1010)のときの結果です。
| 演算子 | 名前 | 動き | 例 | 結果(2進数) | 結果(10進数) |
|---|---|---|---|---|---|
& |
AND(論理積) | 両方が1の桁だけ1 | a & b |
1000 | 8 |
| |
OR(論理和) | どちらかが1の桁は1 | a | b |
1110 | 14 |
^ |
XOR(排他的論理和) | 2つが違う桁だけ1 | a ^ b |
0110 | 6 |
~ |
NOT(ビット反転) | 全部の桁の0と1を入れ替える | ~a |
1…10011(32桁) | -13 |
<< |
左シフト | 左へずらし、右の空きは0 | a << 2 |
110000 | 48 |
>> |
符号付き右シフト | 右へずらし、左の空きは符号ビットと同じ値 | a >> 2 |
11 | 3 |
>>> |
符号なし右シフト | 右へずらし、左の空きは常に0 | a >>> 2 |
11 | 3 |
フラグ管理でよく使う形は次の4つです。
| やりたいこと | 書き方 | 使う演算 |
|---|---|---|
| フラグを立てる | flags |= WRITE; |
OR |
| フラグを下ろす | flags &= ~WRITE; |
AND と NOT |
| フラグを判定する | (flags & WRITE) != 0 |
AND |
| フラグを反転する | flags ^= WRITE; |
XOR |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaビット演算入門|シフト演算>>と>>>の違いとフラグ管理」
この記事の動作環境
| 項目 | 内容 |
|---|---|
| JDK | OpenJDK 21.0.10 |
| OS | Linux(Ubuntu 24.04 系) |
| 実行方法 | java ファイル名.java(単一ファイル実行) |
| 対象の型 | 主に int(32ビット)。long(64ビット)は必要な箇所で補足 |
コード中の 0b1100 は2進数リテラル、0xFF は16進数リテラルです。数字の間の _ は読みやすくするための区切りで、値には影響しません。
AND・OR・XOR・NOTの使い方|4つのビット演算
ビット演算は、2つの整数を2進数で縦に並べ、同じ位置の桁どうしで計算 します。1桁ごとの結果は次の表のとおりです。
| A | B | A & B | A | B | A ^ B |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 |
| 1 | 1 | 1 | 1 | 0 |
実際に動かして確かめます。
public class Ops {
static String b8(int v) {
return String.format("%8s", Integer.toBinaryString(v & 0xFF)).replace(' ', '0');
}
public static void main(String[] args) {
int a = 0b1100; // 12
int b = 0b1010; // 10
System.out.println("a = " + b8(a) + " (" + a + ")");
System.out.println("b = " + b8(b) + " (" + b + ")");
System.out.println("a & b = " + b8(a & b) + " (" + (a & b) + ")");
System.out.println("a | b = " + b8(a | b) + " (" + (a | b) + ")");
System.out.println("a ^ b = " + b8(a ^ b) + " (" + (a ^ b) + ")");
System.out.println("~a = " + Integer.toBinaryString(~a) + " (" + (~a) + ")");
System.out.println("a << 2 = " + b8(a << 2) + " (" + (a << 2) + ")");
System.out.println("a >> 2 = " + b8(a >> 2) + " (" + (a >> 2) + ")");
System.out.println("a >>> 2 = " + b8(a >>> 2) + " (" + (a >>> 2) + ")");
}
}
実行結果:
a = 00001100 (12)
b = 00001010 (10)
a & b = 00001000 (8)
a | b = 00001110 (14)
a ^ b = 00000110 (6)
~a = 11111111111111111111111111110011 (-13)
a << 2 = 00110000 (48)
a >> 2 = 00000011 (3)
a >>> 2 = 00000011 (3)
a と b を縦に見比べると、表のとおりに計算されていることが確認できます。~a だけ32桁で表示しているのは、int 型が32ビットで、NOT は上位の桁も含めて32桁すべてを反転する ためです。12 を反転すると -13 になる理由は、次の「2の補数」でわかります。
なお &、|、^ は boolean どうしにも使えます。この場合は論理演算になり、&& や || と違って右側も必ず評価されます。
2の補数とは|Javaで負の数がどう表されるか
>> と >>> の違いを理解する鍵が、2の補数 という負の数の表し方です。Java の int は32桁の2進数で、一番左の桁(最上位ビット)が0なら0以上、1なら負の数 を表します。この一番左の桁を符号ビットと呼びます。
ある正の数からマイナスの数を作る手順は、「全部の桁を反転して、1を足す」 です。8 から -8 を作ってみます。
public class TwoComp {
static String bin(int v) {
String s = String.format("%32s", Integer.toBinaryString(v)).replace(' ', '0');
return s.replaceAll("(.{8})(?!$)", "$1_");
}
public static void main(String[] args) {
int x = 8;
System.out.println("8 " + bin(x));
System.out.println("~8 " + bin(~x) + " (" + (~x) + ")");
System.out.println("~8 + 1 " + bin(~x + 1) + " (" + (~x + 1) + ")");
System.out.println("-8 " + bin(-8));
System.out.println("~8 + 1 == -8 : " + ((~x + 1) == -8));
System.out.println();
System.out.println("0 " + bin(0));
System.out.println("-1 " + bin(-1));
System.out.println("MAX " + bin(Integer.MAX_VALUE) + " (" + Integer.MAX_VALUE + ")");
System.out.println("MIN " + bin(Integer.MIN_VALUE) + " (" + Integer.MIN_VALUE + ")");
System.out.println("MAX + 1 " + bin(Integer.MAX_VALUE + 1) + " (" + (Integer.MAX_VALUE + 1) + ")");
System.out.println();
System.out.println("-7 / 2 = " + (-7 / 2));
System.out.println("-7 >> 1 = " + (-7 >> 1));
System.out.println("Math.floorDiv(-7, 2) = " + Math.floorDiv(-7, 2));
}
}
実行結果:
8 00000000_00000000_00000000_00001000
~8 11111111_11111111_11111111_11110111 (-9)
~8 + 1 11111111_11111111_11111111_11111000 (-8)
-8 11111111_11111111_11111111_11111000
~8 + 1 == -8 : true
0 00000000_00000000_00000000_00000000
-1 11111111_11111111_11111111_11111111
MAX 01111111_11111111_11111111_11111111 (2147483647)
MIN 10000000_00000000_00000000_00000000 (-2147483648)
MAX + 1 10000000_00000000_00000000_00000000 (-2147483648)
-7 / 2 = -3
-7 >> 1 = -4
Math.floorDiv(-7, 2) = -4
ここから読み取れることを整理します。
| わかること | 根拠になる出力 |
|---|---|
-x は ~x + 1 と同じ |
~8 + 1 と -8 の2進数が一致 |
~x は -x - 1 と同じ |
~8 が -9(早見表の ~12 が -13 になるのも同じ理由) |
| -1 は32桁すべてが1 | -1 の行 |
| 最大値に1を足すと最小値になる | MAX + 1 が MIN と同じ並び |
| 負の数は左側に1が並ぶ | -8 は上位29桁が1 |
小さな負の数ほど、左側にたくさんの1が並んでいる という点が、次のシフト演算で効いてきます。
シフト演算 >> と >>> の違い|-8を32桁の2進数で比べる
シフト演算は、2進数の桁を左か右へ指定した数だけずらします。3つの演算子の違いは、ずらして空いた桁を何で埋めるか です。
| 演算子 | ずらす方向 | 空いた桁に入る値 | 符号 |
|---|---|---|---|
<< |
左 | 右側に0 | 変わることがある |
>> |
右 | 左側に元の符号ビットと同じ値(正なら0、負なら1) | 保たれる |
>>> |
右 | 左側に常に0 | 負の数は正の数になる |
8 と -8 を実際にシフトし、32桁の2進数を0埋めして並べます。
public class ShiftDemo {
// int を 32 桁の 2 進数(0 埋め)にする
static String bin(int v) {
String s = String.format("%32s", Integer.toBinaryString(v)).replace(' ', '0');
return s.replaceAll("(.{8})(?!$)", "$1_");
}
static void show(String label, int v) {
System.out.printf("%-10s %s (%d)%n", label, bin(v), v);
}
public static void main(String[] args) {
System.out.println("--- 正の数 8 ---");
show("8", 8);
show("8 << 1", 8 << 1);
show("8 >> 1", 8 >> 1);
show("8 >>> 1", 8 >>> 1);
System.out.println("--- 負の数 -8 ---");
show("-8", -8);
show("-8 << 1", -8 << 1);
show("-8 >> 1", -8 >> 1);
show("-8 >>> 1", -8 >>> 1);
show("-8 >> 2", -8 >> 2);
show("-8 >>> 2", -8 >>> 2);
show("-8 >> 28", -8 >> 28);
show("-8 >>> 28", -8 >>> 28);
show("-8 >> 31", -8 >> 31);
show("-8 >>> 31", -8 >>> 31);
}
}
実行結果:
--- 正の数 8 ---
8 00000000_00000000_00000000_00001000 (8)
8 << 1 00000000_00000000_00000000_00010000 (16)
8 >> 1 00000000_00000000_00000000_00000100 (4)
8 >>> 1 00000000_00000000_00000000_00000100 (4)
--- 負の数 -8 ---
-8 11111111_11111111_11111111_11111000 (-8)
-8 << 1 11111111_11111111_11111111_11110000 (-16)
-8 >> 1 11111111_11111111_11111111_11111100 (-4)
-8 >>> 1 01111111_11111111_11111111_11111100 (2147483644)
-8 >> 2 11111111_11111111_11111111_11111110 (-2)
-8 >>> 2 00111111_11111111_11111111_11111110 (1073741822)
-8 >> 28 11111111_11111111_11111111_11111111 (-1)
-8 >>> 28 00000000_00000000_00000000_00001111 (15)
-8 >> 31 11111111_11111111_11111111_11111111 (-1)
-8 >>> 31 00000000_00000000_00000000_00000001 (1)
正の数では >> と >>> の結果は同じ
8 の符号ビットは0なので、>> が左に入れる値も0です。>>> と同じ結果(4)になります。0以上の数だけを扱うなら、どちらを使っても結果は変わりません。
負の数では左端に入る値が違う
-8 の行を見比べてください。
-8 >> 1は、左端に 1 が入り、1111…1100(-4)になります。符号が保たれ、「2で割る」に近い結果です。-8 >>> 1は、左端に 0 が入り、0111…1100(2147483644)になります。符号ビットが0になったので、大きな正の数として読まれます。
2の補数では負の数の左側に1が並んでいるため、そこへ0を入れると値の意味が大きく変わります。これが >>> で負の数が突然大きな正の数になる理由です。
シフト数を増やしたときの行き着く先
>>でシフトを続けると、負の数は -1(全部1)、正の数は 0 に行き着きます。-8 >> 28も-8 >> 31も -1 です。>>>では、-8 >>> 28が 15(下位4桁に 1111)となり、上位の桁を符号と関係なく取り出せます。-8 >>> 31は符号ビットだけを取り出した 1 です。
使い分けの判断表
| やりたいこと | 選ぶ演算子 | 理由 |
|---|---|---|
| 符号を保ったまま 2 のべき乗で割りたい | >> |
負の数でも符号が保たれる |
| int を「32個のビットの並び」として扱い、上位の桁を取り出したい | >>> |
左に0が入るので、取り出した値に符号の1が混ざらない |
| ハッシュ値の上位ビットを下位に混ぜたい | >>> |
負のハッシュ値でも結果が偏らない |
| 色(RGB)やフラグなど、0以上の値だけを扱う | どちらでも同じ | 符号ビットが0のため |
>> と割り算は負の奇数で結果が違う
上の TwoComp の出力の最後の3行に注目してください。-7 / 2 は -3 ですが、-7 >> 1 は -4 です。割り算の / は0に近いほうへ丸め、>> は小さいほう(負の無限大の方向)へ丸める ためです。>> と同じ結果になるのは Math.floorDiv(-7, 2) です。「2で割る」つもりで >> 1 を使うときは、負の数が入るかどうかを確認しておくと安心です。
int型で32以上シフトしたときの挙動
1 << 32 は「32桁ぶん左にずらすから0になる」と思いがちですが、実行すると 1 が返ります。
public class ShiftOver {
public static void main(String[] args) {
System.out.println("1 << 31 = " + (1 << 31));
System.out.println("1 << 32 = " + (1 << 32));
System.out.println("1 << 33 = " + (1 << 33));
System.out.println("1 << 35 = " + (1 << 35));
System.out.println("-8 >> 32 = " + (-8 >> 32));
System.out.println("-8 >>> 32 = " + (-8 >>> 32));
System.out.println("1 << -1 = " + (1 << -1));
System.out.println("1L << 32 = " + (1L << 32));
System.out.println("1L << 63 = " + (1L << 63));
System.out.println("1L << 64 = " + (1L << 64));
int n = 40;
System.out.println("n = 40 のとき 1 << n = " + (1 << n) + " (40 & 31 = " + (40 & 31) + ")");
System.out.println("n = 40 のとき 1L << n = " + (1L << n));
}
}
実行結果:
1 << 31 = -2147483648
1 << 32 = 1
1 << 33 = 2
1 << 35 = 8
-8 >> 32 = -8
-8 >>> 32 = -8
1 << -1 = -2147483648
1L << 32 = 4294967296
1L << 63 = -9223372036854775808
1L << 64 = 1
n = 40 のとき 1 << n = 256 (40 & 31 = 8)
n = 40 のとき 1L << n = 1099511627776
結果から、次の規則が読み取れます(Java の仕様として一般に知られている動きで、ここでは Java 21 の実行結果で確かめています)。
| 左辺の型 | 実際に使われるシフト数 | 例 |
|---|---|---|
| int | シフト数の下位5ビット(n & 31、つまり 0〜31) |
1 << 32 は 1 << 0、1 << 35 は 1 << 3、1 << -1 は 1 << 31 |
| long | シフト数の下位6ビット(n & 63、つまり 0〜63) |
1L << 64 は 1L << 0 |
int のまま32以上ずらしたいときは、左辺を long にします。 1 << 40 は 256 ですが、1L << 40 は 1099511627776 になります。フラグの数が32個を超えそうなとき、1 << n を 1L << n に変え忘れると、別のフラグと同じ値になる点に気をつけてください。
また 1 << 31 は符号ビットに1が入るため、-2147483648(int の最小値)になります。32個目のフラグとして使うことはできますが、大小比較や表示では負の数として扱われます。
byte・shortでシフトするときの注意
byte や short にビット演算・シフト演算をすると、計算の前に int へ変換されます(結果も int)。そのため、次のコードはコンパイルエラーになります。
public class SmallErr {
public static void main(String[] args) {
byte b = 8;
byte c = b << 1;
System.out.println(c);
}
}
SmallErr.java:4: error: incompatible types: possible lossy conversion from int to byte
byte c = b << 1;
^
1 error
byte c = (byte) (b << 1); とキャストすると通ります。もう1つ気をつけたいのが、負の byte に >>> を使う場合です。
public class SmallTypes {
public static void main(String[] args) {
byte b = (byte) 0xF0; // -16
System.out.println("b = " + b);
System.out.println("b >> 2 = " + (b >> 2));
System.out.println("b >>> 2 = " + (b >>> 2));
System.out.println("(byte)(b >>> 2) = " + (byte) (b >>> 2));
System.out.println("(b & 0xFF) >>> 2 = " + ((b & 0xFF) >>> 2));
System.out.println("b & 0xFF = " + (b & 0xFF));
System.out.println("Byte.toUnsignedInt(b) = " + Byte.toUnsignedInt(b));
}
}
実行結果:
b = -16
b >> 2 = -4
b >>> 2 = 1073741820
(byte)(b >>> 2) = -4
(b & 0xFF) >>> 2 = 60
b & 0xFF = 240
Byte.toUnsignedInt(b) = 240
byte の -16(11110000)は、int に変換されると上位24桁が1で埋まります。そこから >>> 2 をしても、byte に戻すと下位8桁は >> と同じ並びになり、結果は -4 です。byte を「0〜255 の値」として扱いたいときは、先に b & 0xFF(または Byte.toUnsignedInt(b))で上位の1を取り除いてからシフト します。
ビットフラグで権限を管理する|立てる・下ろす・判定・反転
ビット演算がもっともよく使われるのが、複数の ON/OFF を1つの整数にまとめるフラグ管理 です。1桁を1つのフラグに割り当てると、int 1つで最大32個の ON/OFF を持てます。ここでは「閲覧・編集・削除・公開」の4つの権限を例にします。
public class PermissionFlags {
// 1 ビットずつずらして、権限ごとに桁を割り当てる
static final int READ = 1 << 0; // 0001 = 1
static final int WRITE = 1 << 1; // 0010 = 2
static final int DELETE = 1 << 2; // 0100 = 4
static final int PUBLISH = 1 << 3; // 1000 = 8
static String bin(int v) {
return String.format("%4s", Integer.toBinaryString(v)).replace(' ', '0');
}
static boolean has(int flags, int permission) {
return (flags & permission) == permission;
}
static String describe(int flags) {
StringBuilder sb = new StringBuilder();
if (has(flags, READ)) sb.append("READ ");
if (has(flags, WRITE)) sb.append("WRITE ");
if (has(flags, DELETE)) sb.append("DELETE ");
if (has(flags, PUBLISH)) sb.append("PUBLISH ");
return sb.isEmpty() ? "(なし)" : sb.toString().trim();
}
static void show(String label, int flags) {
System.out.printf("%-28s %s (%2d) %s%n", label, bin(flags), flags, describe(flags));
}
public static void main(String[] args) {
int flags = 0;
show("flags = 0", flags);
// 立てる(OR)
flags |= READ;
show("flags |= READ", flags);
flags |= WRITE | DELETE;
show("flags |= WRITE | DELETE", flags);
// 判定(AND)
System.out.println("WRITE を持つ? " + ((flags & WRITE) != 0));
System.out.println("PUBLISH を持つ? " + ((flags & PUBLISH) != 0));
System.out.println("READ と WRITE を両方持つ? " + has(flags, READ | WRITE));
System.out.println("READ と PUBLISH を両方持つ? " + has(flags, READ | PUBLISH));
System.out.println("WRITE か PUBLISH のどれか? " + ((flags & (WRITE | PUBLISH)) != 0));
// 下ろす(AND NOT)
flags &= ~DELETE;
show("flags &= ~DELETE", flags);
flags &= ~DELETE; // すでに下りていても結果は同じ
show("flags &= ~DELETE (again)", flags);
// 反転(XOR)
flags ^= PUBLISH;
show("flags ^= PUBLISH", flags);
flags ^= PUBLISH;
show("flags ^= PUBLISH (again)", flags);
// まとめて操作
int all = READ | WRITE | DELETE | PUBLISH;
show("all", all);
show("all & ~(WRITE | DELETE)", all & ~(WRITE | DELETE));
System.out.println("立っている数: " + Integer.bitCount(flags));
}
}
実行結果:
flags = 0 0000 ( 0) (なし)
flags |= READ 0001 ( 1) READ
flags |= WRITE | DELETE 0111 ( 7) READ WRITE DELETE
WRITE を持つ? true
PUBLISH を持つ? false
READ と WRITE を両方持つ? true
READ と PUBLISH を両方持つ? false
WRITE か PUBLISH のどれか? true
flags &= ~DELETE 0011 ( 3) READ WRITE
flags &= ~DELETE (again) 0011 ( 3) READ WRITE
flags ^= PUBLISH 1011 (11) READ WRITE PUBLISH
flags ^= PUBLISH (again) 0011 ( 3) READ WRITE
all 1111 (15) READ WRITE DELETE PUBLISH
all & ~(WRITE | DELETE) 1001 ( 9) READ PUBLISH
立っている数: 2
フラグ操作の早見表(コピペ用)
| 操作 | コード | なぜそうなるか |
|---|---|---|
| 定数を定義する | static final int WRITE = 1 << 1; |
1 を左にずらすと、1桁だけが1の値になる |
| 立てる | flags |= WRITE; |
OR は「どちらかが1なら1」。他の桁はそのまま |
| 複数まとめて立てる | flags |= WRITE | DELETE; |
先に OR で合成してから立てる |
| 下ろす | flags &= ~WRITE; |
~WRITE は対象の桁だけ0。AND で対象の桁だけ0になる |
| 反転する | flags ^= WRITE; |
XOR は「1と XOR した桁だけ入れ替わる」 |
| 1つを判定する | (flags & WRITE) != 0 |
AND で対象の桁だけ残し、0かどうかを見る |
| すべて持つか判定する | (flags & mask) == mask |
残った桁が mask と完全に一致するか |
| どれか1つでも持つか判定する | (flags & mask) != 0 |
1桁でも残れば0以外になる |
| 立っている数を数える | Integer.bitCount(flags) |
1 の桁の個数を返す |
| すべて下ろす | flags = 0; |
全桁0 |
出力で確かめられるポイントは次の3つです。
- 「下ろす」は何度実行しても結果が同じ です。
flags &= ~DELETEを2回実行しても 0011 のままです。 - 「反転」は実行するたびに切り替わります。
flags ^= PUBLISHの1回目で 1011、2回目で 0011 に戻っています。下ろすつもりで^=を使うと、もともと下りていたフラグが立つので、目的に合わせて使い分けます。 - 「両方持つか」は
== maskで比べます。READ | PUBLISHのうち READ しか持たない場合、!= 0では true、== maskでは false になります。
判定の式には必ずかっこを付ける
判定で (flags & WRITE) != 0 のかっこを省くと、コンパイルエラーになります。
public class Precedence {
public static void main(String[] args) {
int flags = 0b0110;
int WRITE = 0b0010;
if (flags & WRITE != 0) {
System.out.println("WRITE あり");
}
}
}
Precedence.java:5: error: bad operand types for binary operator '&'
if (flags & WRITE != 0) {
^
first type: int
second type: boolean
1 error
!= や == は & より優先順位が高いため、flags & (WRITE != 0) と解釈され、int と boolean の AND になってしまうためです。シフト演算も同じで、算術演算子より優先順位が低くなっています。
public class Precedence2 {
public static void main(String[] args) {
System.out.println("1 << 2 + 1 = " + (1 << 2 + 1));
System.out.println("(1 << 2) + 1 = " + ((1 << 2) + 1));
int flags = 0b0110, WRITE = 0b0010;
System.out.println("(flags & WRITE) != 0 : " + ((flags & WRITE) != 0));
System.out.println("true & false = " + (true & false) + " / true ^ true = " + (true ^ true));
}
}
1 << 2 + 1 = 8
(1 << 2) + 1 = 5
(flags & WRITE) != 0 : true
true & false = false / true ^ true = false
1 << 2 + 1 は 1 << 3 として計算されます。関係する演算子の優先順位を高い順に並べると次のとおりです。
| 優先順位 | 演算子 |
|---|---|
| 高い | ~(単項) |
| ↓ | 乗算・除算・剰余 |
| ↓ | + - |
| ↓ | << >> >>> |
| ↓ | < > <= >= |
| ↓ | == != |
| ↓ | & |
| ↓ | ^ |
| ↓ | | |
| 低い | &&、その次に || |
ビット演算とシフト演算を他の演算と混ぜるときは、かっこで囲む と覚えておけば、順位表を暗記しなくても意図どおりに書けます。
EnumSetとビットフラグの使い分け
Java には、ビットフラグと同じことを型安全に書ける EnumSet が標準で用意されています。enum の定数を要素にする Set で、同じ4つの権限を次のように扱えます。
import java.util.EnumSet;
public class EnumSetDemo {
enum Permission { READ, WRITE, DELETE, PUBLISH }
public static void main(String[] args) {
EnumSet<Permission> perms = EnumSet.noneOf(Permission.class);
System.out.println("初期状態 " + perms);
perms.add(Permission.READ); // 立てる
perms.addAll(EnumSet.of(Permission.WRITE, Permission.DELETE));
System.out.println("add / addAll " + perms);
System.out.println("WRITE を持つ? " + perms.contains(Permission.WRITE)); // 判定
System.out.println("両方持つ? " + perms.containsAll(EnumSet.of(Permission.READ, Permission.PUBLISH)));
perms.remove(Permission.DELETE); // 下ろす
System.out.println("remove " + perms);
// 反転(あれば外す、なければ入れる)
if (!perms.remove(Permission.PUBLISH)) perms.add(Permission.PUBLISH);
System.out.println("toggle " + perms);
System.out.println("allOf " + EnumSet.allOf(Permission.class));
System.out.println("complementOf " + EnumSet.complementOf(perms));
// int のビットフラグと相互変換(保存用)
int bits = 0;
for (Permission p : perms) bits |= 1 << p.ordinal();
System.out.println("int に変換 " + bits + " (" + Integer.toBinaryString(bits) + ")");
EnumSet<Permission> restored = EnumSet.noneOf(Permission.class);
for (Permission p : Permission.values()) {
if ((bits & (1 << p.ordinal())) != 0) restored.add(p);
}
System.out.println("int から復元 " + restored);
}
}
実行結果:
初期状態 []
add / addAll [READ, WRITE, DELETE]
WRITE を持つ? true
両方持つ? false
remove [READ, WRITE]
toggle [READ, WRITE, PUBLISH]
allOf [READ, WRITE, DELETE, PUBLISH]
complementOf [DELETE]
int に変換 11 (1011)
int から復元 [READ, WRITE, PUBLISH]
表示がそのまま [READ, WRITE] と読めるので、デバッグのときに2進数を読み解く手間がかかりません。操作の対応は次のとおりです。
| 操作 | ビットフラグ(int) | EnumSet |
|---|---|---|
| 立てる | flags |= WRITE |
set.add(WRITE) |
| 下ろす | flags &= ~WRITE |
set.remove(WRITE) |
| 判定する | (flags & WRITE) != 0 |
set.contains(WRITE) |
| すべて持つか | (flags & mask) == mask |
set.containsAll(other) |
| 全部立てる | 全定数を OR | EnumSet.allOf(型) |
| 立っていないものを得る | ~flags & all |
EnumSet.complementOf(set) |
どちらを選ぶかの目安です。
| 状況 | 向いているほう | 理由 |
|---|---|---|
| Java のコードの中だけで ON/OFF の集合を扱う | EnumSet | 型が合わない値を渡せない。表示が読みやすい。定数の数に上限がない |
| データベースの整数カラムやファイルに、1つの数値として保存する | ビットフラグ | そのまま int や long として読み書きできる |
| 他のシステムや通信の取り決めで、ビット位置が決まっている | ビットフラグ | 決められた桁をそのまま操作できる |
| 大量のデータを少ないメモリで持ちたい、配列に詰めたい | ビットフラグ | int 1つで32個の ON/OFF を持てる |
| 保存は整数、コード上は読みやすくしたい | 両方を組み合わせる | 上のコードのように、境界で相互に変換する |
上のコードの変換では ordinal()(enum 定数の定義順の番号)をビット位置に使っています。この方法は、enum 定数の並び順を変えると保存済みの値の意味が変わる ため、保存に使う場合は enum にビット位置を表すフィールドを持たせておくと、あとから定数を追加しやすくなります。
知っておくと便利なIntegerクラスのメソッドと小技
ビットの確認や変換には、Integer クラスのメソッドが役立ちます。
import java.util.HexFormat;
public class Utils {
public static void main(String[] args) {
int v = 0b0010_1000; // 40
System.out.println("toBinaryString(40) " + Integer.toBinaryString(v));
System.out.println("toHexString(255) " + Integer.toHexString(255));
System.out.println("toBinaryString(-8) " + Integer.toBinaryString(-8));
System.out.println("parseInt(\"1010\", 2) " + Integer.parseInt("1010", 2));
System.out.println("parseUnsignedInt(32桁,2) " + Integer.parseUnsignedInt("11111111111111111111111111111000", 2));
System.out.println("bitCount(40) " + Integer.bitCount(v));
System.out.println("highestOneBit(40) " + Integer.highestOneBit(v));
System.out.println("lowestOneBit(40) " + Integer.lowestOneBit(v));
System.out.println("numberOfLeadingZeros(40) " + Integer.numberOfLeadingZeros(v));
System.out.println("numberOfTrailingZeros(40)" + Integer.numberOfTrailingZeros(v));
System.out.println("rotateLeft(1, 33) " + Integer.rotateLeft(1, 33));
System.out.println("toUnsignedString(-8) " + Integer.toUnsignedString(-8));
System.out.println("toUnsignedLong(-8) " + Integer.toUnsignedLong(-8));
System.out.println("divideUnsigned(-8, 2) " + Integer.divideUnsigned(-8, 2));
// よく使う小技
int n = 37;
System.out.println("37 は奇数? (n & 1) == 1 " + ((n & 1) == 1));
System.out.println("64 は2のべき乗? (x & (x-1)) == 0 " + ((64 & 63) == 0));
System.out.println("5 * 8 を 5 << 3 で " + (5 << 3));
int rgb = 0x3366CC;
System.out.printf("RGB 分解 R=%d G=%d B=%d%n", (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF);
System.out.printf("RGB 合成 0x%06X%n", (51 << 16) | (102 << 8) | 204);
}
}
実行結果:
toBinaryString(40) 101000
toHexString(255) ff
toBinaryString(-8) 11111111111111111111111111111000
parseInt("1010", 2) 10
parseUnsignedInt(32桁,2) -8
bitCount(40) 2
highestOneBit(40) 32
lowestOneBit(40) 8
numberOfLeadingZeros(40) 26
numberOfTrailingZeros(40)3
rotateLeft(1, 33) 2
toUnsignedString(-8) 4294967288
toUnsignedLong(-8) 4294967288
divideUnsigned(-8, 2) 2147483644
37 は奇数? (n & 1) == 1 true
64 は2のべき乗? (x & (x-1)) == 0 true
5 * 8 を 5 << 3 で 40
RGB 分解 R=51 G=102 B=204
RGB 合成 0x3366CC
| やりたいこと | 書き方 | 補足 |
|---|---|---|
| 2進数の文字列にする | Integer.toBinaryString(v) |
正の数は先頭の0が省かれる。負の数は32桁 |
| 32桁に0埋めする | String.format("%32s", Integer.toBinaryString(v)).replace(' ', '0') |
本記事の bin メソッドで使用 |
| 2進数の文字列を int にする | Integer.parseInt("1010", 2) |
32桁で先頭が1の文字列は parseUnsignedInt を使う |
| 1 の桁を数える | Integer.bitCount(v) |
フラグの個数を数えるのに使える |
| 奇数か調べる | (n & 1) == 1 |
最下位ビットが1なら奇数。負の数でも使える |
| 2のべき乗か調べる | x > 0 && (x & (x - 1)) == 0 |
1 の桁が1つだけなら2のべき乗 |
| 色を成分に分ける | (rgb >> 16) & 0xFF |
シフトで下位に寄せ、& 0xFF で8桁だけ残す |
| int を符号なしとして扱う | Integer.toUnsignedString(v)、Integer.divideUnsigned(a, b) |
-8 >>> 1 と divideUnsigned(-8, 2) は同じ 2147483644 |
& 0xFF のように、残したい桁だけを1にした値(マスク)と AND を取る 操作は、フラグの判定と同じ考え方です。
よくある質問
Q. >> と >>> は結局どちらを使えばよいですか?
A. 0以上の値だけを扱うなら結果は同じです。負の数が入る可能性があり、数値として「2のべき乗で割る」意味で使うなら >>、int を32個のビットの並びとして扱い、上位の桁を下位へ取り出すなら >>> を選びます。
Q. <<< という演算子はありますか?
A. ありません。左シフトでは右側の空きに0が入るだけで、符号を考慮する・しないの区別が生じないため、<< の1種類です。
Q. ビット演算を使うと計算が速くなりますか?
A. 2倍を << 1 と書くような置き換えは、読みやすさを優先して通常の掛け算・割り算で書いて問題ありません(本記事では速度の計測は行っていません)。ビット演算が役立つのは、フラグをまとめて持つ、値から特定の桁を取り出す、といった「ビットの並びそのものに意味がある」処理です。
Q. フラグが32個を超えたらどうすればよいですか?
A. long にすると64個まで持てます。その場合、定数は 1L << n と L を付けて定義します。Java のコードの中だけで扱うなら、個数の上限を気にせずに使える EnumSet が向いています。
まとめ
- ビット演算は
&・|・^・~の4つ、シフト演算は<<・>>・>>>の3つ >>は左の空きを符号ビットと同じ値で、>>>は常に0で埋める。-8 >> 1は -4、-8 >>> 1は 2147483644- 負の数は2の補数(全桁を反転して1を足す)で表される。左側に1が並ぶので、
>>>で0が入ると大きな正の数になる - int のシフト数は下位5ビットだけが使われる。
1 << 32は 1。32以上ずらすときは1L << n - フラグは「立てる
|=」「下ろす&= ~」「判定(flags & X) != 0」「反転^=」 の4つの形で扱える - 判定の式はかっこで囲む。
==や!=は&より優先順位が高い - Java のコード内で扱う集合は EnumSet、整数として保存・受け渡しする値はビットフラグ が向いている
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Javaビット演算入門|シフト演算>>と>>>の違いとフラグ管理」
株式会社RJCでは、Javaのビット演算やシフト演算の仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。