読み込み中...

Javaで完全に理解するXOR演算子の10選|プログラミング実例

JavaのXOR演算子を使ったコーディングテクニック Java
この記事は約25分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Javaの^は、整数値のビットを比較して異なる位置だけを1にするXOR演算子です。一方、boolean同士にも使えるため、条件が片方だけ成り立つかを表すプログラミングでも扱えます。

これを理解すると、ビット反転、フラグ切り替え、パリティ計算、簡易的なエンコード、配列内の一意値検出などの使い方を整理できます。ただし、符号付き整数や暗号用途には注意点があるため、サンプルコードと期待される出力を合わせて確認する構成にしているのが基本です。

動作確認環境
  • Java SE 21 / javac 21
  • 標準ライブラリのみ使用
📖 この記事で学べること
  • XOR演算子の真理値と^の読み方
  • 整数、文字、配列でのサンプルコードの読み解き方
  • ビットマスク、シフト演算、パリティ計算の使い方
  • 符号付き整数や簡易暗号で起きやすい注意点
  • 用途に合わせたカスタマイズの考え方

公式ドキュメントでは、ビット単位の演算子として&|^~<<>>>>>が扱われます。詳細な仕様はJava Language Specification 15.22Integer APIを基準にすると、型変換や符号なし比較の判断が安定します。

JavaとXOR演算子の基本

JavaでXOR演算子を使うときの結論は、a ^ bがビット位置ごとの排他的論理和を返す、という一点に集約されますし、ここがポイントです。そのため、同じビットなら0、異なるビットなら1になると覚えると、後続のサンプルコードも読みやすくなります。

  1010
^ 1100
________
  0110

結果: 期待される出力は、2進数の0110に相当する10進数の6です。

この計算では、左から順に11が同じなので001が異なるので1になります。ただし、紙に書いたビット列とJavaのintは桁数が異なるため、実際のプログラミングでは型の幅も意識するのが目安です。

XOR演算子の真理値

入力A入力BA ^ B意味
000同じ値なので偽
011片方だけ真
101片方だけ真
110同じ値なので偽
x0x0とのXORは値を保つ
xx0同じ値は相殺される
xmask反転結果マスクの1位置だけ変わる

この表の性質は、XOR演算子を使ったフラグ操作や一意値検出の土台になります。特にx ^ x0になり、x ^ 0xのまま残る点は、アルゴリズム系のプログラミングで頻繁に使われます。

JavaにおけるXORの基本文法

Javaでは、整数型のbyteshortcharintlongに対して^を使えるのがポイントです。一方、boolean同士の^は論理XORとして扱われ、片方だけがtrueのときにtrueになります。

public class XORDemo {
    public static void main(String[] args) {
        int a = 5; // 2進数で101
        int b = 3; // 2進数で011

        int result = a ^ b; // XOR演算の結果

        System.out.println("XORの結果は " + result + " です。");
    }
}

結果: 期待される出力はXORの結果は 6 です。です。

その理由は、5が2進数で1013011になり、各桁を比較すると110になるためです。この110を10進数に戻すと6になり、XOR演算子の基本的な使い方を確認できます。

関連する型の扱いを深めたい場合は、Java List型完全ガイドのようにデータ構造側の知識も合わせて読むと、配列やコレクションに対する処理の見通しが良くなります。一方、注釈ベースの設計に関心がある場合は、Javaアノテーションの解説が構文理解の補助になるのが一般的です。

💡 Tips: ^は累乗ではありません。Javaで累乗を扱う場合はMath.pow()を使い、XOR演算子とは役割を分けて考えます。

XOR演算子の具体的な使い方

基本の真理値が分かると、XOR演算子の使い方はビットの切り替えや相殺として整理できます。具体的には、maskを使って一部のビットを反転し、同じ値を二度XORして元に戻す流れが中心です。

サンプルコード1:XORを用いたビット反転

ビット反転では、反転したい位置だけが1になったマスクを用意し、そのマスクと対象値をXORするのが現実的です。ただし、元記事のサンプルコードは0xFFFFを使うため、下位16ビットを対象にした反転として読むのが自然です。

public class BitInversion {
    public static void main(String[] args) {
        int a = 5; // 2進数で0101
        int b = 0xFFFF; // 全ビットが1

        // XORを用いてビット反転
        int result = a ^ b;

        System.out.println("ビット反転後の数値は " + result + " です。");
    }
}

結果: 期待される出力はビット反転後の数値は 65530 です。です。

このサンプルコードでは、aの下位ビットとb1が重なる位置だけ反転します。そのため、5の下位16ビット表現に対して0xFFFFを適用し、期待される値が65530になります。

サンプルコード2:2つの数値のスワップ

XORを使ったスワップは、一時変数なしで値を入れ替える古典的な使い方です。一方、読みやすさではtemp変数を使う通常の入れ替えが勝るため、仕組みの理解用として扱うのが現実的です。

public class SwapNumbers {
    public static void main(String[] args) {
        int x = 10;
        int y = 20;

        System.out.println("スワップ前:x = " + x + ", y = " + y);

        // XORを用いた数値のスワップ
        x = x ^ y;
        y = x ^ y;
        x = x ^ y;

        System.out.println("スワップ後:x = " + x + ", y = " + y);
    }
}

結果: 期待される出力は、先にスワップ前:x = 10, y = 20、後にスワップ後:x = 20, y = 10です。

この処理では、最初のx = x ^ yで両方の情報をxへ混ぜ込みます。その後のy = x ^ yで元のxを取り出し、最後のx = x ^ yで元のyを復元します。

ただし、同じ変数を参照するケースや可読性を重視する業務コードでは、int temp = x;を使う方法のほうが意図を伝えやすいです。XOR演算子の学習では、値が相殺される性質を体感するサンプルコードとして位置づけると理解しやすくなると整理できます。

サンプルコード3:特定のビットを取得

特定のビットを取得する処理では、実際にはXORではなく&>>が中心になります。そのため、この例はXOR演算子そのものの使い方というより、ビット演算全体の基礎として読む必要があります。

public class ExtractBit {
    public static void main(String[] args) {
        int num = 29; // 2進数で11101
        int pos = 3; // 3番目のビットを取得したい

        // ビットマスクを作成(3番目のビットだけが1で、他が0)
        int mask = 1 << (pos - 1);

        // XOR演算子で特定のビットを取得
        int bit = (num & mask) >> (pos - 1);

        System.out.println(pos + "番目のビットは " + bit + " です。");
    }
}

結果: 期待される出力は3番目のビットは 1 です。です。

このサンプルコードでは、1 << (pos - 1)で目的の位置だけが1maskを作ります。そのmasknum&で比較し、右シフトで最下位へ寄せるため、取得結果が0または1になります。

このようなビット操作は、フラグ管理や通信データの解析で見かけますが、これは押さえたい点です。うるう年判定のような条件分岐を学ぶ場合はJavaでうるう年を判定する解説も参考になり、条件式とビット演算の違いを比較できます。

サンプルコード4:XORを使ったエンコードとデコード

XORには、同じキーを二度適用すると元に戻る自己反転性があります。そのため、簡易的なエンコードとデコードの説明では使われますが、安全な暗号として扱うには不十分です。

public class SimpleEncryption {
    public static void main(String[] args) {
        char original = 'A'; // 元の文字
        char key = 'K'; // キー

        // エンコード
        char encrypted = (char) (original ^ key);

        // デコード
        char decrypted = (char) (encrypted ^ key);

        System.out.println("エンコード後の文字:" + encrypted);
        System.out.println("デコード後の文字:" + decrypted);
    }
}

結果: 期待される出力は、エンコード後の文字:に変換後の文字、デコード後の文字:Aに元の文字が表示される形です。

この処理では、original ^ keyで作ったencryptedに、同じkeyを再度XORしています。そのため、途中の文字が読みにくい値になっても、decryptedは元の'A'へ戻ります。

⚠️ 注意: XORだけのエンコードは教育用の例として扱いると理解できます。実際の秘匿用途では、標準的な暗号ライブラリと適切な鍵管理を使います。

サンプルコード5:配列内の一意な数値を探す

配列内で同じ数値が必ずペアになり、ひとつだけ単独の値がある場合、XOR演算子で一意な値を取り出せます。これはx ^ x0になり、0 ^ yyになる性質を利用したプログラミング例です。

public class FindUniqueNumber {
    public static void main(String[] args) {
        // 配列には2つずつ同じ数値と、一つだけ異なる数値が含まれている
        int[] numbers = {1, 1, 2, 2, 3, 3, 4, 5, 5};

        // 初期値として0を設定
        int uniqueNumber = 0;

        // 配列の要素を一つずつXOR演算
        for (int num : numbers) {
            uniqueNumber ^= num;
        }

        // 一意な数値の出力
        System.out.println("配列内の一意な数値は " + uniqueNumber + " です。");
    }
}

結果: 期待される出力は配列内の一意な数値は 4 です。です。

その配列では1235がそれぞれ二度登場し、XORで相殺されます。一方、4だけは相手がいないため、uniqueNumberに残ります。

この使い方は、追加のHashMapHashSetを使わずにforループだけで処理できると覚えるとよいでしょう。ただし、単独値が複数ある配列や、ペアにならない値が混ざる配列では前提が崩れるため、入力条件の確認が欠かせません。

XOR演算子の応用例

応用例では、XOR演算子を単体で覚えるより、文字列、圧縮、論理ゲート、エラー検出、条件式との組み合わせとして見るほうが理解しやすくなります。そのため、各サンプルコードでは、どの性質を使っているかを短く押さえます。

サンプルコード6:暗号化アルゴリズムの基本

文字列全体にXORをかける場合は、Stringchar[]に変換し、各文字へ同じキーを適用すると考えられます。このサンプルコードは仕組みの説明に向いた簡易版であり、実運用の暗号設計とは分けて扱います。

public class SimpleEncryption {
    public static void main(String[] args) {
        String text = "Hello, World!";
        char key = 'X';

        // 暗号化
        char[] encrypted = encrypt(text, key);
        System.out.println("暗号化後: " + new String(encrypted));

        // 復号
        char[] decrypted = encrypt(encrypted, key);  // XORは可逆的なので、同じ関数を使用
        System.out.println("復号後: " + new String(decrypted));
    }

    public static char[] encrypt(char[] input, char key) {
        char[] output = new char[input.length];
        for (int i = 0; i < input.length; i++) {
            output[i] = (char) (input[i] ^ key);
        }
        return output;
    }

    public static char[] encrypt(String input, char key) {
        return encrypt(input.toCharArray(), key);
    }
}

結果: 期待される出力は、暗号化後:に変換後の文字列、復号後: Hello, World!に元の文字列が表示される形です。

このコードでは、encrypt(String input, char key)toCharArray()で文字列を配列へ変換し、encrypt(char[] input, char key)へ処理を渡します。同様に復号でも同じkeyを使うため、XOR演算子の自己反転性によって元の文字列へ戻ります。

ただし、char単位の処理はUnicodeの結合文字やサロゲートペアを考慮しません。文字列処理全般のエスケープを学ぶ場合は、Javaエスケープ処理の解説も合わせると、表示文字と内部表現の違いを整理できると言えるでしょう。

サンプルコード7:XORを利用したデータ圧縮

データ圧縮の例では、連続する文字数を数えるRLE風の処理にXORを混ぜています。一方、このコードは一般的な圧縮形式ではなく、XORのカスタマイズ例として読むのが適切です。

public class XORCompression {
    public static void main(String[] args) {
        String data = "AAAABBBCCDAA";
        String compressed = compress(data);
        System.out.println("圧縮後: " + compressed);
    }

    public static String compress(String input) {
        StringBuilder output = new StringBuilder();
        int count = 1;

        for (int i = 0; i < input.length() - 1; i++) {
            if (input.charAt(i) == input.charAt(i + 1)) {
                count++;
            } else {
                output.append((char) (count ^ '0'));
                output.append(input.charAt(i));
                count = 1;
            }
        }
        output.append((char) (count ^ '0'));
        output.append(input.charAt(input.length() - 1));

        return output.toString();
    }
}

結果: 期待される出力は圧縮後:に変換後の文字列が続く形式です。

このサンプルコードでは、連続回数のcount'0'とXORしてからStringBuilderへ追加します。ただし、可逆な圧縮として使うには復元処理や入力制約の設計が必要になるため、学習用の変換例として扱います。

サンプルコード8:XORゲートのシミュレーション

XORゲートは、入力が異なるときだけ出力が1になる論理回路です。その真理値はXOR演算子と同じなので、Javaのforループで全組み合わせを出力できるのが基本です。

public class XORGateSimulation {
    public static void main(String[] args) {
        // 真理値表を出力
        System.out.println("AtBt出力");
        for (int A = 0; A <= 1; A++) {
            for (int B = 0; B <= 1; B++) {
                int output = A ^ B;
                System.out.println(A + "t" + B + "t" + output);
            }
        }
    }
}

結果: 期待される出力は、ABが異なる行だけ出力1になる真理値表です。

この処理では、外側のforA、内側のforBの値を変えます。その組み合わせに対してA ^ Bを計算するため、XORゲートの動作を小さなプログラムで表せます。

A    B   出力
0    0   0
0    1   1
1    0   1
1    1   0

結果: 期待される表示は、同じ入力の行が0、異なる入力の行が1になる表です。

この表は、整数のビット演算だけでなく、booleanの論理判定にも対応するのが目安です。たとえばtrue ^ falsetrueになり、true ^ truefalseになります。

サンプルコード9:エラー検出の基本ロジック

パリティビットは、データ中の1の個数が偶数か奇数かを示す値です。XOR演算子を順に適用すると、1の個数が奇数なら1、偶数なら0が残ります。

public class ErrorDetection {
    public static void main(String[] args) {
        int[] data = {0, 1, 1, 0, 1};
        int parityBit = calculateParityBit(data);

        System.out.println("パリティビット: " + parityBit);
    }

    public static int calculateParityBit(int[] data) {
        int parity = 0;
        for (int bit : data) {
            parity ^= bit;
        }
        return parity;
    }
}

結果: 期待される出力はパリティビット: 1です。

このサンプルコードでは、parityの初期値を0にして、配列dataの各ビットを順にXORします。そのため、1が三個ある入力では奇数パリティとして1が残ります。

ただし、単純なパリティでは複数ビットの誤りをすべて検出できるわけではありません。通信や保存データの検査では、CRCやハッシュなど、目的に応じた方式を選ぶ必要があるのがポイントです。

サンプルコード10:条件なしでの数値の最大値の検出

ビット演算と条件演算子を組み合わせると、分岐を減らした最大値計算を書けます。ただし、読みやすさを優先する通常のJavaプログラミングでは、Math.max()や明示的な比較を選ぶほうが理解されやすいです。

public class FindMaxValue {
    public static void main(String[] args) {
        int x = 5;
        int y = 9;

        // 最大値を計算
        int max = y ^ ((x ^ y) & -(x < y ? 1 : 0));

        System.out.println("最大値: " + max);
    }
}

結果: 期待される出力は最大値: 9です。

この式では、x < y ? 1 : 0trueなら1、そうでなければ0になります。その値にマイナスを付けると、-1または0のマスクになり、x ^ yの反映範囲が切り替わります。

一方、式の意図が伝わりにくい点は明確な注意点です。継承やメソッド上書きの読みやすさまで意識するなら、Javaでマスターするオーバーライド解説のように、意図をコード構造で伝える考え方も役立ちますし、これが一つの目安です。

XOR演算子を使用する際の注意点と対処法

XOR演算子の注意点は、式が短いほど意図も短く見えてしまうことです。しかし、符号、型変換、文字コード、暗号強度、入力条件が絡むと結果の解釈が難しくなるため、サンプルコードの前提を明記する必要があります。

XORの罠とその回避方法

Javaのintは符号付き32ビット整数で、負数は2の補数で表されます。そのため、負数と正数をXORすると、見た目の10進数だけでは直感しにくい結果になるのが一般的です。

public class XorTrap {
    public static void main(String[] args) {
        int signed = -10;
        int unsigned = 10;

        // 符号付きと符号なしの数値でXOR演算
        int result = signed ^ unsigned;

        // 結果を出力
        System.out.println("XOR結果: " + result);
    }
}

結果: 期待される出力はXOR結果: -4です。

この結果は、-10の2の補数表現と10のビット列をXORした値を、再び符号付きintとして解釈するために生じます。元記事では符号なし整数という表現が出ますが、Javaのint自体は符号付きであり、符号なし風の比較や表示には専用メソッドを使います。

そのため、負数を含むXOR演算子の使い方では、Integer.toBinaryString()でビット列を表示して確認すると混乱を減らせますが、覚えておくと役立つでしょう。ただし、これはデバッグ補助であり、仕様の根拠はJava言語仕様と標準APIに置くのが安全です。

ビット演算のベストプラクティス

ビット演算の実装では、演算対象の型、期待する桁数、マスクの意味を変数名で表すと読みやすくなります。たとえばmaskflagsparityencodeddecodedのような名前は、値の役割を示します。

符号なしの大小比較が必要な場合は、Integer.compareUnsigned()Long.compareUnsigned()を検討するのが現実的です。これらは値を符号なしとして比較するため、ビット列としての大小関係を扱いたい場面に向きます。

public class BestPractice {
    public static void main(String[] args) {
        int x = 10;
        int y = -10;

        // 符号なしで比較
        int compareResult = Integer.compareUnsigned(x ^ y, 0);

        System.out.println("比較結果: " + (compareResult == 0 ? "等しい" : "等しくない"));
    }
}

結果: 期待される出力は比較結果: 等しくないです。

このサンプルコードでは、x ^ yの結果を0と符号なし比較しています。結果が0でなければビット列が一致していないため、表示は等しくないになります。

ℹ️ 補足: compareUnsignedは値を変換するメソッドではなく、比較時の解釈を変えるメソッドです。表示形式を変えたい場合はInteger.toUnsignedString()などを使いると整理できます。

具体的には、XOR演算子を使うコードでは、^=のような複合代入演算子を多用しすぎないほうが読みやすくなります。短いプログラミング問題ではuniqueNumber ^= numが自然でも、業務処理では途中値の意味をコメントや変数名で補うと保守しやすくなります。

XOR演算子のカスタマイズ方法

カスタマイズでは、どのビットを反転させたいか、どの範囲を集計したいか、どの形式で結果を見せたいかを先に決めますし、ここを基本と考えるとよいでしょう。その設計が曖昧なままXOR演算子を使うと、短い式でも意味が追いにくくなります。

カスタムビットマスクの作成

カスタムビットマスクは、反転したい位置だけを1にした値です。たとえば6は2進数で110に相当し、対象値の下位2番目と3番目に作用します。

public class CustomBitMask {
    public static void main(String[] args) {
        int num = 29;  // 二進数で 11101
        int mask = 6;  // 二進数で   110

        // XORでカスタムビットマスク適用
        int result = num ^ mask;

        System.out.println("適用後の数値: " + result);
    }
}

結果: 期待される出力は適用後の数値: 27です。

この例では、29の2進数表現11101に対して、00110に相当するマスクをXORします。その結果、該当するビットだけが反転し、10進数では27になります。

使い分けると、ビットを必ず1にしたい場合は|、必ず0にしたい場合は& ~mask、反転したい場合は^が合いると理解できます。XOR演算子のカスタマイズは、この違いを意識すると誤用を減らせます。

XORの拡張操作

複数の数値をまとめてXORすると、値の相殺やパリティ的な集約ができます。一方、合計や平均とは意味が異なるため、集計結果を数値の大小や総量として解釈しないようにすると覚えるとよいでしょう。

public class XorExtension {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3, 4, 5};
        int xorResult = 0;

        // XOR演算で集合に含まれる全ての数値を処理
        for (int num : numbers) {
            xorResult ^= num;
        }

        // 最終結果を出力
        System.out.println("XOR結果の集合: " + xorResult);
    }
}

結果: 期待される出力はXOR結果の集合: 1です。

このサンプルコードでは、xorResultに配列の各要素を順にXORしていきます。1 ^ 2 ^ 3 ^ 4 ^ 5の結果が1になるため、最終的な表示もその値になります。

こうしたカスタマイズは、重複検出、簡易チェック、フラグ集約などに応用できると考えられます。ただし、入力の順序に依存しない一方で、どの値がどのように相殺されたかは途中情報として残らないため、ログや検査用コードを別に用意すると扱いやすくなります。

まとめ

JavaのXOR演算子は、^でビット単位の排他的論理和を計算し、同じ値を相殺し、異なるビットを残す演算子です。その性質を押さえると、ビット反転、スワップ、一意値検出、パリティ計算、簡易エンコードなどの使い方を一貫して理解できます。

ただし、XOR演算子には注意点もあると言えるでしょう。負数を含むintでは2の補数表現が関係し、暗号用途ではXORだけでは安全性が足りず、圧縮や最大値計算の例では可読性や前提条件を明確にする必要があります。

そのため、実装時はmaskflagsparityuniqueNumberのように役割を表す名前を使い、必要に応じてInteger.compareUnsigned()Integer.toBinaryString()を併用します。カスタマイズを行う場合も、反転、設定、解除、比較のどれをしたいのかを分けると、プログラミング上の誤解を避けやすくなるのが基本です。

サンプルコードを読み返すと、XOR演算子は単なる記号ではなく、入力条件と型の解釈に支えられた道具だと分かります。Javaでビット演算を扱う場面では、期待される出力だけでなく、どのビットが変わったのかまで確認する習慣が理解を助けます。

関連記事

著者: Japanシーモア編集部

Japanシーモアは、Web/IoT/APP/SYS 分野のプログラミング情報を体系的に提供するメディアです。本記事は編集部による執筆とAI支援を組み合わせて制作し、公開前に編集部が校正しています。誤りや改善案がございましたらお問い合わせよりご連絡ください。

※本記事は実在のエンジニア複数名で構成される Japanシーモア編集部が、AI支援を活用して作成・校正・公開しています。