倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

いまさら!?ビット演算 ~byte 編~

前々回前回で int 値に対するビット演算を見ましたが、今回は byte 値に対する同様のビット演算を見ていきます*1

int 値に使える論理演算 ~, &, |, ^ やビットシフト <<, >>, >>> は byte 値に対しても使えますが、根本的な違いは返り値が int 値であるところです。 正確に言うと、ビット演算を行う前に byte 値を int 値に変換してから、前回までで見た int 値に対するビット演算が施されます。 これは、例えば byte 値と int 値を + 演算子で足し算をするときに byte 値の方を int 値に自動にキャストするというのとは異なり、2つの byte 値同士を & 演算子などで計算する場合にも強制的に int 値に変換されます。

ほとんどの場合は、結果の int 値を byte 型にキャストして下位8ビットをとれば必要な byte 値の結果が得られて、計算に無駄なコストがかかってそうな気がする以外は特に問題はありませんが、符号なし右シフト演算子 >>> を使う場合などには予想と異なる結果が返ってくるので注意が必要です*2

準備

この記事でも、前回までと同様、ビット列(を表す文字列)では「0」の代わりにアンダーバー「_」を用います(見易さのため)。 一方、いままでビット列から int 値へ変換するメソッドを b() としていましたが、これを i() メソッドとし、代わりにビット列から byte 値へ変換するメソッドを b() メソッドとします(名前の付け方ミスったw)。 その他、前回と同様に0が24個の「___」と1が24個の「lll」も定義しています。 また、byte 値同士、int 値同士を確証 (assert) する

  • assertEqualBytes() メソッド
  • assertEqualInts() メソッド

も定義しています。 これはどういう型の値を比較しているのかを分かり易くするためです。 以下に一応実装を載せますが、あまり詳細は必要なく、その後の使い方を見た方が理解が早いかと思います:

    //***** byte *****
    /**
       * ビット列(を表す文字列)を byte 値に変換する。
       * 引数の文字列では「0」の代わりに「_」を使っていてもよい。
       */
    static byte b(String s){
        String str = s.replaceAll("_", "0");
        // 符号ビットも 1/0 で指定できるようにしてるため、ちょっと実装書いてます。
        if(str.length() == 8 && str.startsWith("1")) 
            return (byte)(Integer.valueOf("0"+str.substring(1), 2) - 128);
        else
            return Byte.valueOf(str, 2);
    }

    //***** int *****
    static final String ___ = "________"+"________"+"________";  // 24個の0
    static final String lll = ___.replaceAll("_", "1");      // 24個の1

    /**
       * ビット列(を表す文字列)を int 値に変換する。
       * 引数の文字列では「0」の代わりに「_」を使っていてもよい。
       */
    static int i(String s){
        return Integer.parseUnsignedInt(s.replaceAll("_", "0"), 2);
    }

    //***** assertions *****
    /** 2つの byte 値が等しいことを確証する */
    static void assertEqualBytes(byte a, byte b){ assert(a == b); }

    /** 2つの int 値が等しいことを確証する */
    static void assertEqualInts(int a, int b){ assert(a == b); }

上記で定義したメソッドの使い方を見ておきましょう:

// b() メソッドはビット文字列(を表す文字列)から byte 値を取得する
byte x = b("___11_1_");  // ==  26 : 正の byte 値その1
byte y = b("__1_1111");  // ==  47 : 正の byte 値その2
byte z = b("111__11_");  // == -26 : 負の byte 値

// 2つの byte 値を比較
assertEqualBytes(x, (byte)26);
assertEqualBytes(y, (byte)47);
assertEqualBytes(z, (byte)-26);


// int 値に変換
int X = x;
int Y = y;
int Z = z;

// 2つの int 値を比較 その1
assertEqualInts(X, 26);
assertEqualInts(Y, 47);
assertEqualInts(Z, -26);

// 2つの int 値を比較 その2 (i() メソッドで int 値を取得)
assertEqualInts( X, i(___+"___11_1_") );
assertEqualInts( Y, i(___+"__1_1111") );
assertEqualInts( Z, i(lll+"111__11_") );  // 24個の1が追加されていることに注意

ここで定義した byte 値 x, y, z、int 値 X, Y, Z (Java では、普通ローカル変数に大文字で始まる名前は付けませんが、ここでは同じ値で違う型であることを分かりやすくするためにしています)は、次節以降で各種演算を実行するときに使います。

byte 値と int 値のビット表現で少々注意が必要なのは、負の byte 値を int 値に変換する場合には24個の1が上位に追加されることです。 数値として等しい値となるためにはこれは当然の処理ですが、ビット演算を行っているときに暗黙的に変換されると、(後で見るように)予想外の結果になる場合があります。

ビット演算

では各演算子の処理を見ていきましょう。 基本的には byte 値を int 値に変換して int 値に対するビット演算をするだけです。 値の確証では、int 値での値比較をするので assertEqualInts() メソッドを使っています:

byte x = b("___11_1_");
byte y = b("__1_1111");
byte z = b("111__11_");

// 論理演算
assertEqualInts( ~x   , i(lll+"111__1_1") );
assertEqualInts( x & y, i(___+"____1_1_") );
assertEqualInts( x | y, i(___+"__111111") );
assertEqualInts( x ^ y, i(___+"__11_1_1") );

// ビットシフト
assertEqualInts( x << 3, i(___+"11_1____") );
assertEqualInts( x >> 3, i(___+"______11") );
assertEqualInts( z >> 3, i(lll+"111111__") );
assertEqualInts( x >>> 3, i(___+"_____11") );
assertEqualInts( z >>> 3, i("___"+lll+"111__") );  // 左端から3ビットが0

z (負の値)に対して符号なしビットシフトを行う「z >>> 3」では、左端から3ビットが0 (_) で、そこから1が24個続いていることに注意。 これだけを見ると当然の結果ですが、次節で見る「byte 値としてのビット演算」を見ると少々間違いを誘発しそうな結果であることが分かると思います。

ちなみに、当然のことながら事前に int 値に変換して各ビット演算を行っても同じ結果になります:

// 任意の byte 値 x, y について
int X = x;
int Y = y;

// 論理演算
assertEqualInts( ~x   , ~X );
assertEqualInts( x & y, X & Y );
assertEqualInts( x | y, X | Y );
assertEqualInts( x ^ y, X ^ Y );

// ビットシフト(任意の int 値 p について)
int p = ...;
assertEqualInts( x << p, X << p );
assertEqualInts( x >> p, X >> p );
assertEqualInts( x >>> p, X >>> p );

byte としてのビット演算

さて、byte 値を使ったビット演算で結果も byte 値として得たい場合、前節で得られた int 値を byte 値にダウンキャスト(プリミティブ値についてはふつうダウンキャストと言わないかもしれませんが、ここではそう言うことにしましょう)すれば良さそうですが、実際にうまくいくか試してみましょう:

byte x = b("___11_1_");
byte y = b("__1_1111");
byte z = b("111__11_");

// 論理演算
assertEqualBytes( (byte)~x     , b("111__1_1") );
assertEqualBytes( (byte)(x & y), b("____1_1_") );
assertEqualBytes( (byte)(x | y), b("__111111") );
assertEqualBytes( (byte)(x ^ y), b("__11_1_1") );

// ビットシフト
assertEqualBytes( (byte)(x << 3), b("11_1____") );
assertEqualBytes( (byte)(x >> 3), b("______11") );
assertEqualBytes( (byte)(z >> 3), b("111111__") );
assertEqualBytes( (byte)(x >>> 3), b("_____11") );
assertEqualBytes( (byte)(z >>> 3), b("111111__") );  // 結果に注意

ほとんどの場合、int 値への変換を気にせずに、byte 値の各ビットごとに論理演算をしたり、8ビットと思ってビットシフトすれば、byte 値にダウンキャストした結果と等しくなりますが、符号なし右シフトだけは結果が異なります。

もう少し詳しく言うと、上記の結果では「111__11_」(= z) を3だけ符号なし右シフトすると「111111__」となっていますが、純粋に byte 値としてこの演算を行うと「___111__」のように左端から3ビットが0 (_) になっていることが期待されます。

もし実際に必要な結果がこの「byte 値としての符号なし右シフト」なら、以下のように「0xFF」(下位8ビットのマスク)との論理積をとってから符号なし右シフトを行う必要があります:

byte z = b("111__11_");

// 0xFF は下位8ビットのマスク
assertEqualInts( 0xFF, i(___+"11111111") );     // == 00000000 00000000 00000000 11111111
assertEqualInts( z & 0xFF, i(___+"111__11_") ); // == 00000000 00000000 00000000 11100110

int t = (z & 0xFF) >>> 3  // 0xFF でマスクしてから符号なし右シフトを施す
assertEqualInts( t, i(___+"111__11_") );        // == 00000000 00000000 00000000 00011100

// byte 値としての符号なし右シフト
assertEqualBytes( (byte)((z & 0xFF) >>> 3), b("___111__"));

まぁ、int 値への変換が施されていることさえ理解していればそんなに難しい話ではないですね。 ちなみ、Java8 以降を使えるなら Byte#toUnsignedInt() メソッドを使えます:

Byte.toUnsignedInt(z) >>> 3;

Byte#toUnsignedInt() メソッドは「0xFF」と論理積をとったものと同じ結果を返します(実装を確認してませんが)。 かなり最近の Java8 で導入されたのと、ちょっとメソッド名が長いのが悩ましいところ。

代入演算子
ちょっとミスリーディングな言語仕様な気もするんですが、ビット演算に関連する代入演算子 &=, |=, ^=, <<=, >>=, >>>= は byte 値に対しても適用可能です:

byte x = b("___11_1_");
x &= b("__1_1111");
assertEqualBytes( x, b("111__1_1") );

さて、これは代入後の変数も byte 値なので、もしかしたら「byte 値としてのビット演算」をやってくれているのかも、と思って符号なし右シフトを試してみると

byte z = b("111__11_");
z >>>= 3;
assertEqualBytes( z, b("111111__") );

となって、残念ながら(?)やはり int 値に変換して符号なし右シフトを行った結果を byte 値に変換しているようです。 「byte 値としての符号なし右シフト」と代入演算子を合わせて簡単には書くのは無理そうなので、多用するならメソッドとして定義しておくのが良さそうです。

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

Javaによる関数型プログラミング ―Java 8ラムダ式とStream

  • 作者: Venkat Subramaniam,株式会社プログラミングシステム社
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2014/10/24
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (3件) を見る

*1:short 値に対するビット演算も、今回見ていく byte 値に対するものと基本的には同じです。

*2:バイトストリームを読み込んで誤り符号などを計算する際に、この int 値への変換が利いてくる場合があるので注意。