Bài 12: Các phép toán trong Java
Nhiệm vụ chính của các chương trình là thực hiện các phép tính toán để xử lý dữ liệu. Để lập trình đúng các yêu cầu tính toán, ta cần phải nắm vững được các thứ tự ưu tiên cũng như các quy tắc kết hợp tính toán của các phép toán trong ngôn ngữ lập trình đó.
1. Thứ tự ưu tiên và quy tắc kết hợp thực hiện của các phép toán
Mức độ ưu tiên và quy tắc kết hợp của các phép toán cần phải được xác định để tính toán các biểu thức. Các mức ưu tiên và các quy tắc kết hợp của các phép toán trong Java được cho bởi bảng sau:
Số ưu tiên |
Loại | Các phép toán |
Quy tắc kết hợp |
1 | Phép toán 1 ngôi hậu tố (postfix) | [] . (tham số) exp++ exp– | Thực hiện từ trái qua phải |
2 | Phép toán 1 ngôi tiền tố | ++exp –exp +exp -exp ~ ! | Thực hiện từ phải qua trái |
3 | Tạo lập đối tượng và ép kiểu | new (type) | Thực hiện từ phải qua trái |
4 | Loại phép nhân | * / % | Thực hiện từ trái qua phải |
5 | Loại phép cộng | + – | Thực hiện từ trái qua phải |
6 | Chuyển dịch | << >> >>> | Thực hiện từ trái qua phải |
7 | Phép toán quan hệ | < <= > >= instanceof | Thực hiện từ trái qua phải |
8 | Phép so sánh đẳng thức | == != | Thực hiện từ trái qua phải |
9 | Phép và (AND) trên bitwise/boolean | & | Thực hiện từ trái qua phải |
10 | Phép hoặc loại trừ (XOR) trên bitsiwe/boolean | ^ | Thực hiện từ trái qua phải |
11 | Phép hoặc (OR) trên bitsiwe/boolean | | | Thực hiện từ trái qua phải |
12 | Phép và (AND) logic | && | Thực hiện từ trái qua phải |
13 | Phép hoặc (OR) logic | || | Thực hiện từ trái qua phải |
14 | Phép toán điều kiện | ?: | Thực hiện từ trái qua phải |
15 | Cá phép gán | = += -= *= /= %= <<= >>= >>>= &= ^= |= | Thực hiện từ phải qua trái |
Lưu ý:
- Số ưu tiên là tỷ lệ nghịch với mức ưu tiên thực hiện trong các biểu thức. Trong biểu thức, những phép toán có số ưu tiên thấp hơn sẽ được thực hiện trước.
- Những phép toán có cùng số ưu tiên trong một biểu thức sẽ được thực hiện theo quy tắc kết hợp từ trái qua phải hoặc từ phải qua trái như trong bảng.
- Để thay đổi thứ tự thực hiện theo ý muốn thì có thể sử dụng cặp ngoặc đơn ( và ) giống như trong toán học.
2. Các phép toán số học
Các phép toán số học được chia thành 2 loại:
- Các phép toán 1 ngôi (đơn nguyên): + (cộng) và – (trừ), các phép đổi dấu.
- Các phép toán 2 ngồi (nhị nguyên): * (nhân), / (chia), % (lấy modul – phép chia lấy phần dư), + (cộng), – (trừ).
Các phép toán (toán tử) số học được sử dụng để thiết lập các biểu thức toán học như trong đại số. Các toán hạng (đối số) của các phép toán là các biểu thức có giá trị kiểu số. Riêng phép + (cộng) còn được sử dụng nạp chồng để ghép các xâu nếu có một toán hạng là đối tượng của lớp String.
Lưu ý:
- Thứ tự kết hợp thực hiện của các phép toán số học đơn nguyên là từ phải qua trái. Ví dụ:
int tg= - -20; // (-(-20)) cho 20
Chú ý giữa 2 phép toán đơn nguyên phải có dấu cách.
- Thứ tự kết hợp thực hiện của các phép toán số học nhị nguyên là từ trái qua phải. Ví dụ:
int a = 10%4*4; // ((10%4)*4) cho 8 int b = a/5; // Thực hiện chia nguyên và cho kết quả là 1
- Lúc thực hiện, các toán hạng phải được tính toán từ trái qua phải trước khi áo dụng với phép toán. Khi 2 toán hạng khác nhau về kiểu thì thực hiện chuyển đổi (ép kiểu) như trong bài trước đã học
- Phép chia nguyên (2 toán hạng đều là số nguyên) đòi hỏi số chia phải khác 0.
int a = 1/0; // Lỗi biệt lệ số học (Phần sau sẽ đề cập)
- Phép chia số thực (ít nhất một toán hạng là kiểu số thực) cho phép chia cho 0 và kết quả phép chia 0.0 là INF (số lớn vô cùng) hoặc -INF (số âm vô cùng), hai hằng đặc biệt trong Java.
float m = 4.5/0.0; // Cho m la INF float n = -4.5/0.0; // Cho n là -INF
- Trong Java, phép chia lấy số dư % thực hiện được cả đối với số thực
float m = 11.5%2.5; // Cho m là 1.5
- Khi sử dụng các phép toán đơn nguyên đối với các đối số kiểu byte, short, hoặc char thì trước tiên toán hạng phải được tính rồi chuyển về kiểu int và kết quả là kiểu int. Ví dụ:
byte b1, b = 4; // OK, số 4 kiểu int nhưng cho phép thu hẹp kiểu mặc định về byte b = 1 - b; // Lỗi vì 1 - b có kết quả kiểu int do vậy thu hẹp kiểu đòi hỏi phải tường minh b1 = (byte)(1-b); // Hoàn toàn đúng short h = 30; // OK, 30 kiểu int chuyển về short (mặc định đối với hằng nguyên) h = h + 4; // Lỗi vì h + 4 cho kết quả kiểu int vì thế không thể gán trực tiếp cho h kiểu short. h = (short)(h+4); // lại đúng hoặc có thể viết h = h + (short)4
3. Các phép chuyển dịch <<, >>, >>>
Các phép chuyển dịch <<, >>, >>> thực hiện dịch dạng biểu diễn nhị phân của toán hạng thứ nhất sang trái, sang phải số lần bằng giá trị số nguyên của toán hạng thứ hai. Các đối số của chúng luôn là kiểu nguyên.
3.1 Biểu diễn nhị phân của các số nguyên
Đầu tiên, ta cần lưu ý rằng các giá trị nguyên biểu diễn cho cả giá trị dương và giá trị âm. Java sử dụng phần bù 2 để lưu trữ các giá trị nguyên.
Các bit trong dạng biểu biễn nhị phân của các số nguyên (kiểu char, short, int, long) được đánh số từ phải qua trái và bắt đầu bằng 0. Bit cao nhất là bit dấu, 0 cho số dương và 1 cho số âm.
Ví dụ: Bảng sau cho một số biểu diễn phần bù 2 của các số chiếm 1 byte
Giá trị thập phân |
Biểu diễn nhị phân 8 bit | Giá trị Hexa |
127 |
0111 1111 | 0x7f |
126 |
0111 1110 |
0x7e |
… |
… |
… |
41 |
0010 1001 |
0x29 |
… |
… |
… |
2 |
0000 0010 |
0x02 |
1 |
0000 0001 |
0x01 |
0 |
0000 0000 |
0x0 |
-1 |
1111 1111 |
0xff |
-2 |
1111 1110 |
0xfe |
… |
… |
… |
-41 |
1101 0111 |
0xd7 |
… |
… |
… |
-127 | 1000 0001 |
0x81 |
-128 | 1000 0000 |
0x80 |
Lưu ý: Cách tính phần bù 2 được thực hiện như sau:
Cho trước giá trị nguyên dương, ví dụ 41. Biểu diễn nhị phân của -41 được tính như sau:
Biểu diễn nhị phân | Giá trị thập phân | |
Cho trước giá trị | 0010 1001 | 41 |
Lấy phần bù 1 | 1101 0110 | |
Cộng thêm 1 | 0000 0001 | |
Kết quả là phần bù 2 | 1101 0111 | -41 |
Tương tự như vậy đối với số âm, lấy phần bù 2 của số âm sẽ dược số dương.
3.2 Phép dịch trái: <<
a << n Dịch tất cả các bit của a sang trái n lần, điều số 0 vào bên phải.
Ví dụ:
int i = 12;
int re = i << 4; // 192
Java sử dụng 4 byte để lưu trữ số nguyên do vậy
12 << 4 = 0000 0000 0000 0000 0000 0000 0000 1100 << 4
= 0000 0000 0000 0000 0000 0000 1100 0000 = 192
Có thể nhận thấy mỗi lần dịch sang trái một vị trí thì tương đương với việc lấy giá trị trước đó nhân với 2 (với điều kiện bit cao nhất dịch ra ngoài phải bằng 0). Trong ví dụ trên re = 12 * 24 = 192.
Khi đó số bên trái có kiểu byte hoặc short thì sẽ được chuyển sang kiểu tương ứng của đối số thứ hai. Nếu đối số thứ hai là int thì bit dấu của giá trị byte, short sẽ được mở rộng để điều vào những bit cao hơn, ví dụ:
byte b = -42; // 1101 0110
int re = b << 4; // -672
Khi giá trị byte là -42 được chuyển sang int thì bit dấu 1 sẽ được điền vào các bit cao hơn như sau:
b << 4 = 1111 1111 1111 1111 1111 1111 1101 0110 << 4
= 1111 1111 1111 1111 1111 1101 0110 0000 = 0xfffffd60 = -672
= -42 * 24.
3.3 Phép dịch phải và điền bit dấu: >>
a > n Dịch tất cả các bit của a sang phải n lần, điền bit dấu vào bên trái.
Ví dụ:
int i = 12;
int re = i >> 2; // 3
Java sử dụng 4 byte để lưu trữ số nguyên do vậy:
12 << 4 = 0000 0000 0000 0000 0000 0000 0000 1100 >> 2
= 0000 0000 0000 0000 0000 0000 0000 0011 = 0x00000003 = 3
Mặt khác có thể nhận thấy mỗi lần dịch sang phải 1 vị trí thì tương đương với việc lấy giá trị trước đó chia cho hai (chia đôi và làm tròn dưới). Trong ví dụ trên: re = 12 / 22 = 3.
Khi đối số bên trái có kiểu byte hoặc short thì sẽ được chuyển sang kiểu tương ứng của đối số 2. Nếu đối số thứ hai là int thì bit dấu của giá trị byte, short sẽ được mở rộng để điền vào những bit cao hơn, ví dụ:
byte b = -42; // 1101 0110
int re = b >> 4; // -3
Khi giá trị byte là -42 được chuyển sang int thì bit dấu 1 sẽ được điền vào các bit cao hơn như sau:
b >> 4 = 1111 1111 1111 1111 1111 1111 1101 0110 >> 4
= 1111 1111 1111 1111 1111 1111 1111 1101 = 0xfffffffd = -3
3.4 Phép dịch phải và điền bit 0: >>>
a >>> Dịch tất cả các bit của a sang phải n lần, điền 0 vào bên trái.
Ví dụ:
byte b = -42; // 1101 0110
int re = b >>> 4; // 268435453 - khác kết quả trên rất nhiều
Khi giá trị byte là -42 được chuyển sang int thì bit dấu 1 sẽ được điền vào các bit cao hơn như sau:
b >>> 4 = 1111 1111 1111 1111 1111 1111 1101 0110 >> 4
= 0000 1111 1111 1111 1111 1111 1111 1101 = 0x0ffffffd = 268435453
Lưu ý:
- Giá trị của đối số bên phải (ở trên là n) luôn là số nguyên (dương) do vậy đối số bên trái (ở trên là a) nếu là byte hoặc short thì phải đổi sang kiểu int
- Kết quả của các phép chuyển dịch vì vậy sẽ luôn là int (hoặc là kiểu long nếu đối số thứ nhất là long)
4. Các phép gán mở rộng
Các phép toán gán mở rộng có cú pháp dạng:
<var> <op> = <exp>
Trong đó: <var> là biến, <op> là một trong các phép toán: +=, -=, *=, /=, %=, <<=, >>=, >>>=, &=, ^=, |= và <exp> là biểu thức tương ứng với các phép toán đã chọn.
4.1. Các phép gán số học mở rộng
Đối với các phép gán số học mở rộng thì quy tắc cú pháp trên tương đương ngữ nghĩa với lệnh sau:
<var> = (<type>)(<var> <op> (<exp>));
Trong đó <type> là các kiểu số và <op> là phép toán số học +, -, *, /, %. Bảng sau mô tả chi tiết các phép gán số học mở rộng:
Câu lệnh gán |
Cho trước kiểu số T của biến x và biểu thức e |
x+=e; | x=(T)(x+(e)); |
x-=e; | x=(T)(x-(e)); |
x*=e; | x=(T)(x*(e)); |
x/=e; | x=(T)(x/(e)); |
x%=e; | x=(T)(x%(e)); |
Do quy định các phép gán số học mở rộng có dạng tương đương về mặt ngữ nghĩa như trên nên thực chất là đã có sự ép kiểu giữa các kết quả của biểu thức ở vế phải về kiểu T của biến x. Ví dụ:
int i=5;
i*=i+2; // Tính như là: i=(int)(i*(i+2));
byte b = 3; // Các hằng nguyên cho phép thu hẹp kiểu như mặc định
b+=4; // Tính như là: b=(byte)((int)b+4);
b=b+4; // Sai vì vế phải có kết quả kiểu int còn b kiểu byte đòi ép kiểu
4.2. Các phép gán logic mở rộng
Các phép gán logic mở rộng được định nghĩa như trong bảng dưới:
Các lệnh gán | Cho b và biểu thức e kiểu boolean |
b&=e; | b=(b&(e)); |
b^=e; | b=(b^(e)); |
b|=e; | b=(b|(e)); |
Ví dụ:
boolean b1 = false, b2 = false, b3 = true;
b3&=b3&b1|b2; // false vì b3 = ((b3&b1) | b2);
4.3. Các phép gán mở rộng trên bit (bitwise)
Các phép gán mở rộng trên bit có dạng ngữ nghĩa tương tự như các phép gán số học mở rộng, nghĩa là kết quả của biểu thức bên phải được ép về kiểu của biến nhận kết qua (vế trái). Những phép này được định nghĩa như trong bảng dưới đây:
Các lệnh gán |
Cho T là kiểu nguyên của b và biểu thức e |
b&=e; |
b=(T)(b&(e)); |
b^=e; |
b=(T)(b^(e)); |
b|=e; |
b=(T)(b|(e)); |
Ví dụ:
int v0=-42;
char v1=')'; // 41
byte v2=13;
v0&=15; // 1...1101 0110 & 0...0000 1111 = 0...0000 0110 (=6)
Đối với những phép gán mở rộng trên bit được quy định như trong bảng trên, do vậy không cần ép kiểu khi thu hẹp kiểu.
4.4. Các phép gán chuyển dịch mở rộng
Các phép gán chuyển dịch mở rộng: <<=, >>=, >>>= được định nghĩa như trong bảng dưới đây:
Các lệnh gán |
Cho T là kiểu nguyên của b và biểu thức e |
b<<=e; |
b=(T)(b<<(e)); |
b>>=e; |
b=(T)(b>>(e)); |
b>>>=e; |
b=(T)(b>>>(e)); |
Ví dụ:
int i=-42; // Biểu diễn -42 ở dạng nhị phân phần bù 2: 1..11010110
i>>4; // 1...11010110 >> 4 -> 1...11111101 (=3);
byte a = 12;
a<<=5; // Cho a=-128 vid a = (byte)((int)a<<5)
a=a<<5; // Sai bởi a<<5 là kiểu int do vậy đòi hỏi ép kiểu tường minh
5. Các phép toán so sánh đẳng thức
Các phép toán so sánh <, <=, >, >= của Java được định nghĩa giống như trong các ngôn ngữ lập trình khác (như C/C++ chẳng hạn). Trong đó chỉ cần lưu ý 2 đối số phải là các biểu thức số. Riêng phép so sánh đẳng thức (bằng) thì phức tạp hơn.
5.1. So sánh đẳng thức trên các giá trị kiểu nguyên thủy: ==, !=
Cho trước 2 toán hạng a và b, có kiểu dữ liệu nguyên thủy.
- a==b: a và b có bằng nhau không? Nghĩa là nếu chúng có các giá trị kiểu nguyên thủy bằng nhau thì có kết quả đúng (true), ngược lại cho kết quả sai (false)
- a!=b: a và b có khác nhau không? Nghĩa là nếu chúng có các giá trị kiểu nguyên thủy khác nhau thì cho kết quả đúng (true), ngược lại thì cho kết quả sai (false)
Lưu ý: Kiểu các đối số có thể là kiểu số hoặc kiểu boolean, nhưng 2 đối số phải luôn có kiểu tương thích và so sánh được với nhau. Ví dụ (<boolean value> == c) sẽ không hợp lệ nếu c có kiểu số. Tuy nhiên nếu biểu thức có nhiều ghép == kết hợp thì phải thận trọng và căn cứ vào thứ tự thực hiện vì có thể tất cả các toán hạng đều có kiểu số nhưng biểu thức vẫn không hợp lệ, hoặc ngược lại khi các đối số có kiểu khác nhau nhưng vẫn hợp lệ.
Ví dụ:
int a, b, c;
boolean h1 = a == b == c; // Sai bởi phải thực hiện a== b cho giá trị boolean là true, sau đó so với a (giá trị số) thì 2 đối số khác nhau kiểu
boolean h2 = a==b == true; // Hợp lệ
5.2. So sánh đẳng thức trên các tham chiếu đối tượng: ==, !=
Phần kiến thức này sẽ được trình bày sau khi học lập trình hướng đối tượng với Java