WisdomSoft - for your serial experiences.

2.9 ビット処理

数値をビット列と解釈し、論理演算やシフト演算によるビット列の操作方法を説明します。

2.9.1 ビットごとの演算

これまでの計算は数学的な処理を行うための算術演算でしたが、コンピュータが得意とする2進数の制御を行うのには不向きです。例えば、2進数の特定のビットを 0 にしたい場合、加減演算は不適切です。こうした演算には、ビット制御を専門とする論理演算子を使います。

表1 論理演算子
演算子 名称
& 論理積(AND) 演算子 A & B
^ 排他的論理和(XOR) 演算子 A ^ B
| 論理和(OR) 演算子 A | B
~ 反転演算子 ~A

論理演算子には、整数型と boolean 型のオペランドのみ指定することができます。boolean 型の場合は true が 1、false が 0 の2進数値として考えて計算されます。ビット演算は初心者には難しく感じるかもしれませんが、実体は 1 と 0 の単純なパターンに過ぎないので、身構えることはありません。

2.9.2 論理積

論理積 & とは、双方のビットが 1 であれば 1、そうでなければ 0 という結果を返す演算です。 この性質は、2進数の特定の桁を削除する時に利用することができます。

boolean 型の論理積の場合は、双方が true であれば true、そうでなければ flase を返します。

表2 論理積
A B A & B
0 0 0
0 1 0
1 0 0
1 1 1
false false false
false true false
true false false
true true true

例えば 1100 1100 という8桁の2進数のうち、下位4ビットを取り出したい場合は、0000 1111 との論理積を求めます。16進数のリテラルに変換すると 0xCC & 0x0F という計算になります。各桁のビットごとに論理積が求められるため、2つの2進数値のうち両方とも1の桁のみが 1 となり、片方でも 0 であれば、その桁は 0 となります。

図1 論理積の例
論理積の例

論理積を使えば図1のような結果を得ることができます。0xCC & 0x0F という論理積演算では、0x0F が覆いの役割を果たしています。そのため、0x0F に対して論理積を求めた場合、下位4ビットだけが抽出されることになるのです。そのため、0xCC と 0x0F の論理積の結果は 0x0C となるのです。

コード1
class Test {
	public static void main(String args[]) {
		int iValue = 0xCC & 0x0F;
		System.out.print("0xCC & 0x0F = ");
		System.out.println(iValue);

		iValue = 0x7 & 0x4;
		System.out.print("0x07 & 0x04 = ");
		System.out.println(iValue);
	}
}
実行結果
>java Test
0xCC & 0x0F = 12
0x07 & 0x04 = 4

コード1の最初の論理積演算は、図1を実践しています。結果は 10 進数の 12、16 進数では 0x0C となるため、0xCC から下位4ビットだけを抽出できたことを確認できます。

次の 0x7 & 0x4 は、左オペランドの 3 桁目(右から 3 ビット目)が 1 になっているかどうか、ビットフラグ(ビット列の各桁に論理的な意味を定義し、その桁が 0 か 1 かで状態を通知する方法)を調べることを仮定した計算です。目的のビットが 1 であれば 0 以外の値が、そうでなければ 0 が返る仕組みになっています。実行結果を見ると、この演算の結果は 4 と表示されました。0 以外の値が得られたので、目的のビットが 1 であることを表しています。

2.9.3 論理和

論理和 | は、一方でも 1 であれば 1、双方が 0 であれば 0 という結果を出す論理演算です。この演算は、例えば特定のビットを 1 にしたい場合に利用することができます。実践では、何らかの情報を表している現在のビット列を、他のビット列で上書きする場合に使われたりします。

boolean 型の論理和の場合は、一方でも true であれば true、双方が false であれば false を返します。

表3 論理和
A B A | B
0 0 0
0 1 1
1 0 1
1 1 1
false false false
false true true
true false true
true true true

論理積はビット列から特定のビットだけを抽出する機能があったのに対し、論理和は特定のビットを加える機能があるのです。算術演算の加算に似ているような気がしますが、位が上がることがありません。1 | 2 は 3 ですが、1 | 1 は 1 です。

コード2
class Test {
	public static void main(String args[]) {
		int iValue = 0x7C | 0x0F;
		System.out.print("iValue = ");
		System.out.println(iValue);
	}
}
実行結果
>java Test
iValue = 127

コード2は、2 進数 0111 1100 と 0000 1111 の論理和を求めています。この場合、後者のビット列の下位 4 ビットが 1 なので、前者のビット列の下位 4 ビットが 1 で埋められ、残りのビットはそのままの結果が得られます。そのため 0111 1111 すなわち10進数で 127 という結果が得られたのです

2.9.4 排他的論理和

排他的論理和は、双方のビットのうち一方が 1、もう一方が 0 の場合のみ 1 となり、そうでなければ 0 となる論理演算です。逆に表現すれば、双方が 1 または双方が 0 の場合は 0 になります。このように、常に 1 つのみが選択されている独占的状態を排他的と呼びます。

boolean 型の排他的論理和の場合は、一方が true、他方が false の時のみ true となり、そうでなければ false となります。

表4 排他的論理和
A B A | B
0 0 0
0 1 1
1 0 1
1 1 0
false false false
false true true
true false true
true true false

排他的論理和は、双方の値が等しい場合は 0 となる性質なので A ^ A は常に 0 となります。他にも A と B の排他的論理和の結果 C に対して、C と B の排他的論理和の結果は必ず A となる性質もあります。表4を見れば、なぜこのような結果になるか理解できるでしょう。A が 0、B が 1 の時の結果は 1 であり、結果 1 と B の排他的論理和を求めると、0 すなわち A の値が得られます。

コード3
class Test {
	public static void main(String args[]) {
		int iValue = 0xF0F0 ^ 0xFFFF;
		System.out.print("0xF0F0 ^ 0xFFFF = ");
		System.out.println(iValue);

		iValue = iValue ^ 0xFFFF;
		System.out.print("iValue ^ 0xFFFF = ");
		System.out.println(iValue);
	}
}
実行結果
>java Test
0xF0F0 ^ 0xFFFF = 3855
iValue ^ 0xFFFF = 61680

コード3では、まず 0xF0F0 ^ 0xFFFF を求めています。これは、2進数で表現すると 1111 0000 1111 0000 をすべてのビットが 1 である 1111 1111 1111 1111 との排他的論理和を求めていることから、ビット列が反転した 0000 1111 0000 1111 すなわち10進数の 3855 が得られます。次に、再びすべてのビットが 1 の整数との排他的論理和を求め、元の値 1111 0000 1111 0000 すなわち10進数の 61680 に復元しています。

このように、排他的論理和は「どちらか一方だけが有効である」という排他的な関係処理に利用することができます。

2.9.5 反転と負数表現

ビットごとの反転演算子 ~ を使えば、整数型のビット列をすべて反転させることができます。つまり、1 の場合は 0、0 の場合は 1 という結果を得ることができます。反転演算子は前置式の単項演算子であり、オペランドは常に整数型でなければなりません。

32 ビットの2進数値 0000 0000 0000 0000 0000 0000 0000 1010 を反転させると 1111 1111 1111 1111 1111 1111 1111 0101 となります。これは一見すると10進数の値 10 のビットを反転させ、4294967285 になるかのように思えますが、int 型の整数は 2147483647 間でしか表現できなかったことを思い出してください。では、10進数値 10 のビットを反転させると、結果はどのようになるのでしょうか。

コード4
class Test {
	public static void main(String args[]) {
		String str = "~10 = " + ~10;
		System.out.println(str);
	}
}
実行結果
>java Test
~10 = -11

コード4の実行結果を見ると、驚くべきことに10進数値 10 を反転させた結果は -11 となっています。これは、Java がどのように負数を表現しているかという原理に秘密が隠されています。

2進数の世界では負数というものがそもそも存在しません。そこで、Java では最上位ビットが 1 であれば負数であるという決まりになっています。最上位ビットが 1 ならば、その数値の2の補数を求めることで絶対値を得ることができます。逆に考えるとすれば、10 の負数を表現したい場合は、これの2の補数を求めればよいのです。

ビット列を反転させるという行為は 1 の補数を求めることに等しく、これに 1 を加算すればマイナス演算子を使わずに値の符号を変換することができます。すなわち、反転演算子 ~A の効果は常に (-A)-1 に等しいのです。

2.9.6 ビットシフト

ビット列を左右にそのまま移動させるには、シフト演算子を利用します。左右にシフトすることで生じる空きビットは 0 で埋められるため、2進数の値を左にシフトする行為は2で乗算することに等しく、右にシフトすることは2で除算することに等しいと考えることができます。

表5 シフト演算子
演算子 名称
<< 左シフト演算子 A << B
>> 符号付き右シフト演算子 A >> B
>>> 符号なし右シフト演算子 A >>> B

表5のように、すべてのシフト演算子は2項演算子です。オペランドは必ず整数型でなければならず、A を B だけシフトします。例えば、2進数値 0101 1010 を右に1だけシフトすれば 0010 1101 となりますし、左に1だけシフトすれば 1011 0100 となります。これは、10進数で表現すると 90 という値が右シフトによって 45、左シフトによって 180 という値に変換されており、2で除算、及び乗算された結果と同じであることがわかります。

コード5
class Test {
	public static void main(String args[]) {
		String str = "10 >> 2 = " + (10 >> 2) +
			"\n10 << 2 = " + (10 << 2);
		System.out.println(str);
	}
}
実行結果
>java Test
10 >> 2 = 2
10 << 2 = 40

コード5では、整数値 10 を右に 2 シフトしたものと、左に 2 シフトした結果を表示するプログラムです。整数値 10 を 2 進数で表現すると 0000 1010 であり、これを右に 2 シフトした場合は 0000 0010、左に2シフトした場合は 0010 1000 となるため、正しくシフトされていることが結果から確認することができます。

左シフトの場合はともかく、右シフトの場合はある問題が浮上します。負数の値を右シフトした場合、最上位ビットの空きが 0 で埋められると符合が変わってしまうのです。そこで、Java は符号付きと符号なし右シフト演算子を用意しています。符号付き右シフト演算は最上位ビットが負数であれば 1 で空きビットを埋めますが、符号なし右シフト演算は空きビットを 0 で埋めます。

右シフトを行うとき、最上位ビットの符号でビットを埋めることを符号拡張と呼び、0 で埋めることをゼロ拡張と呼びます。すなわち符号付き右シフト演算子は符号拡張でシフトし、符号なし右シフト演算子はゼロ拡張でシフトするのです。

コード6
class Test {
	public static void main(String args[]) {
		String str = "-10 >> 2 = " + (-10 >> 2) +
			"\n-10 >>> 2 = " + (-10 >>> 2);
		System.out.println(str);
	}
}
実行結果
>java Test
-10 >> 2 = -3
-10 >>> 2 = 1073741821

コード6は、符号付きシフト演算と符号なしシフト演算の違いを調べるためのプログラムです。整数 -10 は2進数で 1111 1111 1111 1111 1111 1111 1111 0110 であり、符号拡張で右に 2 シフトした場合は 1111 1111 1111 1111 1111 1111 1111 1101 となります。この値の2の補数を求めれば、この数の絶対値が 3 であることがわかります。一方、ゼロ拡張で右に 2 シフトした場合は 0011 1111 1111 1111 1111 1111 1111 1101 となります。最上位ビットが 0 なので、これはそのまま正数として認識することができます。