Hiểu nhanh toán tử thao tác bit trong Java

04 tháng 11, 2018 - 15567 lượt xem

                          Học lập trình Java thực hành 8 buổi tại Techmaster

I. Bit là gì?

Bit là đơn vị nhỏ nhất của dữ liệu máy tính, một bit chứa một giá trị nhị phân duy nhất (0 hoạc 1). Phép thao tác bit không hề lạ với những lập trình viên thành thạo Pascal và C, nhưng sẽ là khá mới mẻ với những ai mới vào nghề.

Khi bạn thao tác một thuật toán, bạn sẽ quan tâm đến kiểu của dữ liệu, nhưng khi máy tính xử lý thuật toán bạn viết, thì tất cả kiểu dữ liệu được quy về một kiểu bit. Và may thay, Java cho phép ta thao tác với các giá trị bit cụ thể đại diện cho dữ liệu. Trong một vài tình huống, bạn sẽ thấy thao tác này rất tiện lợi, khi chương trình xử lý sẽ nhanh hơn rõ rệt.

II. Các toán tử thao tác bit (Bitwise operators)

Chắc hẳn bạn không hề xa lạ với những toán tử toán học trong lập trình như + - * / và %, hoạc các toán tử logic như & và ||, nhưng thực ra vẫn còn các kiểu toán tử khác mà ta nên biết đó là các kiểu toán tử thao tác số dưới dạng bit. Vì đơn giản là, nằm sâu dưới lớp chương trình, tất cả các con số đều được lưu giữ và xử lý dưới dạng nhị phân là 0 và 1.

Ôn lại bài, toán tử thao tác trên các kiểu số nguyên và biến gồm có:

  • byte (8 bit)

  • short (16 bit)

  • int (32 bit)

  • long (64 bit)

  • và cả kiểu char (16 bit)

Okay, lý thuyết vậy đủ rồi, tiếp tục với những thú vị khác của các toán tử thao tác bit nhé.

1. Toán tử thao tác bit bổ sung đơn phân [~]

Toán tử này tên quá rối, nhưng hiểu đơn giản là cách đảo chiều giá trị bit. Khi ta đặt ký hiệu ~ trước một biến giá trị, nó sẽ đảo chiều tất cả các giá trị bit của biến đó (đảo từ 0 sang 1 và ngược lại.). Khi chuyển về dạng số nguyên, giá trị số nguyên mới sẽ cộng thêm 1 và bị đảo từ âm sang dương hoạc ngược lại.

Xem ví dụ sau để hiểu rõ hơn về toán tử này:

public static void main(String[] args) {

 int x = 5;
 System.out.println("Value of x: "+x);
 System.out.println("Value of x in binary form: "+Integer.toBinaryString(x));
 System.out.println("Value of ~x: "+~x);
 System.out.println("Value of ~x in binary form: "+ Integer.toBinaryString(~x));
}

//Output
//Value of x: 5
//Value of x in binary form: 11111111111111111111111111111010
//Value of ~x: -6
//Value of ~x in binary form: 000000000000000000000000000000101

2. Toán tử thao tác bit AND [&]

Khác với kiểu toán tử thao tác bit bổ sung đơn phân [~], kiểu toán tử thao tác bit AND [&] cần có 2 toán hạng (giá trị đầu vào) để thao tác.

Về cách thức, toán tử thao tác bit AND [&] rất giống toán tử thao tác AND [&] với giá trị nguyên, khi so sánh 2 giá trị bit là 1 thì kết quả trả về là 1, trong khi các trường hợp khác thì trả về 0.

Ví dụ: So sánh 0110 và 1100 thì kết quả trả về sẽ là 0100 vì chỉ có mỗi số thứ 2 giống nhau đều là 1.

3. Toán tử thao tác bit OR [|]

Toán tử thao tác bit OR [|] khi so sánh 2 giá trị bit chỉ cần chứa 1 giá trị là 1 thì sẽ trả về 1, nếu cả 2 giá trị bằng 0 thì sẽ trả về 0.

Ví dụ: so sánh 01010 và 10100 thì kết quả sẽ trả về là 11110 vì từ số thứ 1 đến thứ 4 đều chứa 1, số thứ 5 cả 2 giá trị đều là 0.

4. Toán tử thao tác bit đơn nhất XOR[^]

Toán tử thao tác bit XOR trả về kết quả 1 chỉ với trường hợp duy nhất khi cả 2 giá trị bit so sánh đều là 1.

Ví dụ: so sánh 0101 và 1011 thì kết quả sẽ trả về là 0001 vì chỉ có duy nhất số thứ 2 là có kết quả bit của cả 2 giá trị bằng 1.

5. Bảng tóm tắt các toán tử thao tác bit

Bảng tổng hợp dưới đây tóm tắt các toán tử thao tác bit đã học ở trên với giá trị so sánh là A và B.

A

B

A & B

A | B

A ^ B

1

0

0

1

1

0

1

0

1

1

1

1

1

1

0

0

0

0

0

0

III. Các toán tử đổi vị trí bit

1. Toán tử Signed Left Shift [<<]

Toán tử Signed Left Shift – gọi ngắn gọn là bit left shift cần 2 toán hạng để thực hiện, địa chỉ bit của toán hạng thứ nhất sẽ lùi về bên trái một số bằng với số của toán hạng thứ 2. Ví dụ:

Giá trị bit của số 5 có dạng:

00000000 00000000 00000000 00000101

Ta thực hiện toán tử bit left shift, lùi về 3 số bên trái với giá trị bit của 5: 5 << 3

Khi này giá trị bit của 5 sẽ trở thành:

00000000 00000000 00000000 00101000

Nếu in giá trị bit này ra, ta sẽ được giá trị số nguyên mới là 40. Quy tắc đơn giản để tính giá trị số nguyên mới sau khi thực hiện toán tử bit shift là: giá trị số nguyên mới = giá trị số nguyên cũ nhân với 2 bậc n, trong đó n là chính số lùi về bên trái.  Vậy trong trường hợp này, địa chỉ mới sẽ có giá trị là 5 x 2^3 = 40.

Địa chỉ bit ở trên có 32 số, với phép toán tử bit left shift, bạn có vài lưu ý hết sức thú vị sau:

  • Kiểu dữ liệu byte, short hay char đều chuyển về kiểu số int (32 bit) với phép toán tử này.

  • Giới hạn phép toán tử bit shift chỉ lùi về được 32 số, nếu ta thực hiện toán tử với một số >32, phép tính sẽ thực hiện với chỉ duy nhất số hàng đơn vị. Ví dụ 5 << 35 sẽ tương đương với 5 < 3.

  • Toán tử bit shift không có xử lý ngoại lệ (exception).

2. Toán tử Right Shift [>>] và số âm dạng bit trong Java

Toán tử Right Shift chia làm hai kiểu, kiểu nhận dấu (signed) và kiểu không nhận dấu (unsigned), điểm khác biệt giữa hai toán tử dạng bit right shift nằm ở kiểu bit của số âm.

Và trước khi đi vào tìm hiểu kỹ về hai toán tử này, ta ôn qua một chút về kiểu bit số âm trong Java. Khi một số nguyên được biểu thị trên màn hình máy tính, ta có thể dễ dàng phân biệt qua dấu “-“ phía trước để biết đâu là số âm, đâu là số dương. Nhưng biểu diễn kiểu gì đễ phân biệt số âm và số dương kiểu bit?, vì kiểu bit chỉ có thể biểu đạt 2 giá trị duy nhất là 0 và 1.

Trong Java, để biểu diễn một số âm dưới dạng bit được chuyển từ các bước sau:

·         28 được viết dưới dạng bit là: 00011100

·         Đảo số từ 1 và 0 cho nhau ta được: 11100011

·         Thêm 1 vào số bit mới ta được số bit của giá trị -28 là: 11100100

Cần chú ý là theo quy tắc "Most Significant", số bit hàng đầu tiên bên trái là 1 nếu là số âm, và là 0 nếu là số dương. 

3. Toán tử nhận dấu Signed Right Shift [>>]

Tương tự với toán tử bit left shift, cách tính >> n sẽ là giá trị mới = giá trị cũ chia cho 2^n nếu n là số chẵn và = giá trị cũ cộng 1 và chia với 2^n nếu n là số lẻ.

Điểm đặc biệt với toán tử bit signed right shift là giá trị cũ là giá trị âm thì sẽ luôn là âm, giá trị cũ là dương thì sẽ luôn là dương sau khi thao tác toán tử này. Xem ví dụ thao tác trên bit sau để hiểu rõ nguyên tắc toán tử này.

// Using right shift operator with negative number in Java

int number = -2;

System.out.println(number);

System.out.println("Before shift : " + Integer.toBinaryString(number));

number = number >> 1;

System.out.println(number);

System.out.println("After shift : " + Integer.toBinaryString(number));

//Output

//                -2
//Before shift : 11111111111111111111111111111110    
//                -1
//After shift :  11111111111111111111111111111111

Như vậy, phép toán tử này luôn giữ giá trị đầu tiên bên trái, nên giá trị âm sẽ luôn âm và dương thì luôn dương.

4. Toán tử không nhận dấu Unsigned Right Shift [>>]

Với phép toán tử này, giá trị đầu tiên bên trái sẽ chuyển thành 0 do các giá trị sau bị lùi đi một số bằng n lần sang bên phải. Chính vì vậy cả giá trị âm lẫn dương đều chuyển thành số dương sau khi toán tử được thực hiện. Xem ví dụ sau để hiểu rõ hơn về toán tử này.

public class RightShiftWithoutSignDemo{ public static void main(String args[]) {

// Using unsigned right shift with negative number in Java

// we can use binary literals from JDK 1.7 to assign

// binary values to an integer, 0b is for binary, similar to 0x of hexadecimal

int number = 0b1111_1000_1111_1010_1111_1000_1111_1010;

System.out.println("Before unsigned right shift : " + Integer.toBinaryString(number));

System.out.println("number in decimal format : " + number); number = number >>> 1;

//shift 1 bit using right shift without sign

System.out.println("After unsigned right shift : " + Integer.toBinaryString(number));

System.out.println("number in decimal format : " + number); } }


//Output

//Before unsigned right shift : 11111000111110101111100011111010

//number in decimal format : -117769990

//After unsigned right shift : 01111100011111010111110001111101

//number in decimal format : 2088598653

5. Toán tử gán kép với bit

Tương tự với toán tử gán kép với số nguyên, ta cũng có kiểu toán tử gán kép với bit được thực hiện bằng cách rút gọn như sau.

|=

x |= 5

x = x | 5

^=

x ^= 5

x = x ^ 5

&=

x &= 5

x = x & 5

<<=

x <<= 5

x = x << 5

>>=

x >>= 5

x = x >> 5

>>>=

x >>>= 5

x = x >>> 5

|=

x |= 5

x = x | 5

^=

x ^= 5

x = x ^ 5

IV. Kết luận

Sử dụng toán tử thao tác bit cực kỳ hữu dụng trong trường hợp bạn muốn tăng tốc độ xử lý của phần cứng, cụ thể là trong lập trình nhúng C. Nhưng cái giá phải trả là những đoạn code khó đọc với những ai chưa quen các toán tử dạng này.

Theo quan điểm của tác giả, toán tử thao tác bit là một khái niệm thú vị liên quan đến lập trình và khoa học máy tính mà bạn nên biết, nhưng bạn phải hạn chế sử dụng toán tử bit chừng nào có thể vì theo thời gian, phong cách lập trình hiện đại luôn đề cao tính tường minh và dễ hiểu của những đoạn code.

Lập trình Java cơ bản và nâng cao 

Lập trình Web Java Spring 2018

Bình luận

avatar
duongtieuchi 2020-02-01 03:55:46.133432 +0000 UTC
Phần toán từ Xor sai rồi!!!
Avatar
avatar
Quang Nh 2020-02-01 04:57:28.866591 +0000 UTC
Đúng rồi xor trả về 1 khi 1 trong 2 phần tử so sánh = 1, còn lại trả về 0, edit sau, thanks bạn nhé
Avatar
avatar
anh minh 2021-07-30 09:33:35.18262 +0000 UTC

phần toán tử thao tác bit bổ sung đơn phân chỗ ví dụ output sai không nhỉ, mình chạy thử thì nó ra như này

Value of x: 5
Value of x in binary form: 101
Value of ~x: -6
Value of ~x in binary form: 11111111111111111111111111111010
 

Avatar
* Vui lòng trước khi bình luận.
Ảnh đại diện
  0 Thích
0