ビット演算とは、整数を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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?