在 Java 中,运算符是对字面量、变量等操作数进行计算、比较或赋值的符号。例如,+ 可以用于数值相加,> 可以比较两个数的大小。

运算符与操作数按照 Java 的语法规则组合起来,就构成了表达式。例如:

  • int a = 10;:定义变量 a。
  • int b = 20;:定义变量 b。
  • int c = a + b;:计算表达式 a + b,再将结果赋值给 c。

其中,+ 是运算符,a + b 是表达式,= 则将计算结果赋给变量。

算术运算符的基本用法

运算符作用
+加法
-减法
*乘法
/除法
%取余

下面直接用代码看看这些运算符的结果:

java
public class Test1 {
    public static void main(String[] args) {
        System.out.println("3+2的结果:" + (3 + 2));
        System.out.println("5-1的结果:" + (5 - 1));
        System.out.println("7*5的结果:" + (7 * 5));
        // 如果在计算中有小数参与,结果有可能不准
        System.out.println("0.1+0.2的结果:" + (0.1 + 0.2));// 0.30000000000000004
        // 整数参与运算,结果还是整数,会直接丢掉小数点后的值。
        System.out.println("10/3的结果:" + (10 / 3));
        // 有浮点数参与计算,结果就为浮点数
        System.out.println("10.0/3的结果:" + (10.0 / 3));
        System.out.println("10%3的结果:" + (10 % 3));
    }
}

运行后,终端中可以看到以下输出(节选):

Shell
7*5的结果:35
0.1+0.2的结果:0.30000000000000004
10/3的结果:3
10.0/3的结果:3.3333333333333335
10%3的结果:1

从运行结果中,可以看到浮点数计算、整数除法和取余运算的几个特点:

1. 小数参与计算时,结果有可能不太准确。 比如 0.1 + 0.2,我们平时算出来是 0.3,程序却输出了 0.30000000000000004。这与浮点数在计算机中的存储方式有关:有些十进制小数无法用有限的二进制位精确表示。这里先知道会出现精度误差就行,后续用到浮点数时再深入了解。

2. 整数除法会截去小数部分。 10 / 3 的两个操作数都是整数,结果就是 3;而 10.0 / 3 中有 double 类型的操作数,因此得到浮点数 3.3333333333333335。想保留除法的小数部分,可以让至少一个操作数使用浮点类型。

3. % 用来取余数。 这个概念其实我们小学就接触过,只是在日常算术里不一定经常用到。放到程序里,它很适合判断整除和奇偶性:10 % 2 == 0,说明 10 能被 2 整除,是偶数;11 % 2 == 1,说明 11 是奇数。

隐式转换与强制转换

隐式转换(自动类型提升)

不同数值类型参与运算时,Java 会按照运算规则进行类型提升。例如,int 和 double 相加,会先把 int 转换为 double,再进行计算,因此结果也是 double。

java
public class Test1 {
    public static void main(String[] args) {
        int a = 10;
        double b = 10.33;
        System.out.println(a+b);
    }
}

对于基本数值类型的算术运算,可以先记住两条规则:

  1. 不同类型的数值参与运算时,会按类型提升规则确定计算使用的类型。例如 int 与 double 运算,结果是 double。
  2. byte、short、char 参与算术运算时,至少会提升为 int。所以两个 byte 相加,得到的也是 int,不会自动变回 byte。

对于这些常见数值类型,可以按下面的顺序理解提升方向:

byte / short / char → int → long → float → double

这条顺序描述的是常见数值运算的类型提升过程。char 是无符号类型,short 是有符号类型;整数转换为浮点数时也可能损失精度,因此它并不是严格按照取值范围大小排列的。

强制转换

如果需要明确将一个数值转换成另一种类型,可以使用强制类型转换。尤其是把较大范围类型的变量值赋给较小范围类型的变量时,通常需要显式写出目标类型:

目标类型 变量名 = (目标类型) 表达式;

例如,两个 byte 相加后得到的是 int,因此要把结果赋给 byte 变量,需要对相加的结果进行转换。

java
public class Test1 {
    public static void main(String[] args) {
        byte b1 = 10;
        byte b2 = 20;
        byte b3 = (byte) (b1 + b2);
        System.out.println(b3);
    }
}

(byte) (b1 + b2) 先计算 b1 + b2,再把计算结果转换为 byte。

如果只写 (byte) b1 + (byte) b2,那么两个操作数虽然分别经过了强制转换,但在相加时仍会提升为 int,不能直接将这个表达式赋给 byte 变量。

强制转换可能丢失数据。例如数值超出了目标类型的表示范围,转换后的结果可能与原值不同,所以要留意目标类型的范围。

字符串的 + 操作

+ 不仅能进行数值加法,还能连接字符串。当 + 的任一操作数是字符串时,它会执行字符串拼接。例如,"123" + 123 的结果是字符串 "123123"。

连续使用 + 时,运算会按照表达式的结合顺序进行:

  • 2020 + 6 + "你好":先计算 2020 + 6,再与字符串拼接,结果是 "2026你好"。
  • "你好" + 2020 + 6:先与 2020 拼接,再与 6 拼接,结果是 "你好20206"。

代码中也可以直接观察这两种情况:

java
public class Test1 {
    public static void main(String[] args) {
        System.out.println("abc" + 123);
        System.out.println(2020 + 6 + "你好");
    }
}

终端输出:

Shell
abc123
2026你好

字符相加

在 Java 中,char 表示一个 UTF-16 代码单元。对于 ASCII 范围内的字符,其数值与 ASCII 编码相同,例如小写字母 'a' 对应十进制的 97。

当字符参与数值加法时,会先进行数值类型提升。因此,'a' + 0 的结果是整数 97;但当字符与字符串拼接时,'a' + "bc" 得到的是字符串 "abc"。

java
public class Test1 {
    public static void main(String[] args) {
        System.out.println('a' + 0);//97
        System.out.println('a'+"bc");//abc
    }
}

因此,看到 + 时,需要结合操作数的类型判断它是数值加法还是字符串拼接。

常用 ASCII 编码与速查表

ASCII 使用 7 位二进制编码,共有 0~127 这 128 个编码值,包括英文字母、数字、符号和控制字符。写 Java 代码时,下面几个字符的编码比较常用:

字符十进制十六进制
'0'4830
'A'6541
'a'9761

其他字符可以在下面的完整码表中查找。

查看完整 ASCII 码表(0~127)

按十进制编码排序;展开后可在表格内滚动查看全部 128 个字符。

十进制十六进制字符 / 说明
000NUL(空字符)
101SOH(标题开始)
202STX(正文开始)
303ETX(正文结束)
404EOT(传输结束)
505ENQ(询问)
606ACK(确认)
707BEL(响铃)
808BS(退格)
909HT(水平制表)
100ALF(换行)
110BVT(垂直制表)
120CFF(换页)
130DCR(回车)
140ESO(移出)
150FSI(移入)
1610DLE(数据链路转义)
1711DC1(设备控制 1)
1812DC2(设备控制 2)
1913DC3(设备控制 3)
2014DC4(设备控制 4)
2115NAK(否认)
2216SYN(同步空闲)
2317ETB(传输块结束)
2418CAN(取消)
2519EM(介质结束)
261ASUB(替换)
271BESC(转义)
281CFS(文件分隔符)
291DGS(组分隔符)
301ERS(记录分隔符)
311FUS(单元分隔符)
3220SPACE(空格)
3321!
3422"
3523#
3624$
3725%
3826&
3927'
4028(
4129)
422A*
432B+
442C,
452D-
462E.
472F/
48300
49311
50322
51333
52344
53355
54366
55377
56388
57399
583A:
593B;
603C<
613D=
623E>
633F?
6440@
6541A
6642B
6743C
6844D
6945E
7046F
7147G
7248H
7349I
744AJ
754BK
764CL
774DM
784EN
794FO
8050P
8151Q
8252R
8353S
8454T
8555U
8656V
8757W
8858X
8959Y
905AZ
915B[
925C\
935D]
945E^
955F_
9660`
9761a
9862b
9963c
10064d
10165e
10266f
10367g
10468h
10569i
1066Aj
1076Bk
1086Cl
1096Dm
1106En
1116Fo
11270p
11371q
11472r
11573s
11674t
11775u
11876v
11977w
12078x
12179y
1227Az
1237B{
1247C|
1257D}
1267E~
1277FDEL(删除)

自增与自减运算符

运算符作用
++使变量的值增加 1
--使变量的值减少 1

++ 和 -- 都有前置和后置两种写法。单独执行 a++ 或 ++a,都会让 a 增加 1;自减同理。

下面的代码先执行 a++,再执行 ++a,因此 a 从 10 变为 12。

java
public class Test1 {
    public static void main(String[] args) {
        int a = 10;
        a++;
        ++a;
        System.out.println(a);
    }
}

不过一旦要参与其他计算,就需要区分前置和后置了。可以先记一个很直观的说法:a++ 是“先用后加”,++a 是“先加后用”。 这是什么意思?我们直接看下面的例子。

  • a++:表达式先使用 a 原来的值,再将 a 加 1。
  • ++a:先将 a 加 1,表达式再使用更新后的值。
java
public class Test1 {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + 1;
        System.out.println(b);//11
        // 重置a为10,避免上面的运算影响下面的结果
        a = 10;
        int c = 1 + ++a;//12
        System.out.println(c);
    }
}

第一句 int b = a++ + 1; 是先用后加:先拿 a 原来的值 10 去计算 10 + 1,所以 b 得到 11;然后 a 自己再加 1,变成 11。

为了不让上一句影响第二个例子,我们把 a 重新设为 10。接下来 int c = 1 + ++a; 是先加后用:a 先变为 11,再参与 1 + 11,因此 c 得到 12。这样对比起来,就比较容易看出两种写法的区别了。

赋值运算符

运算符作用示例
=直接赋值a = 10
+=相加后赋值a += b
-=相减后赋值a -= b
*=相乘后赋值a *= b
/=相除后赋值a /= b
%=取余后赋值a %= b

以 += 为例,a += b 表示把 a + b 的结果重新赋给 a。其他复合赋值运算符也遵循类似规则。

java
public class Test1 {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        b += a;
        System.out.println(b);//30
        b -= a;
        System.out.println(b);//20
        b *= a;
        System.out.println(b);//200
    }
}

这段代码先让 b 从 20 增加到 30,随后减去 a 变回 20,最后乘以 a 得到 200。三次输出依次是 30、20、200。

复合赋值还有一个类型转换细节:两个 byte 相加时,表达式结果是 int。但写成 b1 += b2,Java 会隐式地将结果转换回 byte;如果写成 b1 = b1 + b2,则会因为类型不匹配而无法编译。这里的隐式转换也可能造成数据丢失。

关系运算符

关系运算符可以比较数值大小或判断两个值是否相等。下面以基本类型的数值比较为例:

运算符含义示例
==等于a == b
!=不等于a != b
>大于a > b
>=大于等于a >= b
<小于a < b
<=小于等于a <= b

比较表达式的结果是 boolean 类型,即 true 或 false。其中 == 用于比较,= 用于赋值,二者的含义不同。

java
public class Test1 {
    public static void main(String[] args) {
        System.out.println(1 == 1);// true
        System.out.println(1 != 1);//false
        System.out.println(2 > 3);//false
        System.out.println(2 < 3);//true
        System.out.println(3<=3);//true

    }
}

对于引用类型,== 比较的是两个引用是否指向同一个对象;判断字符串的内容是否相同,通常使用 equals() 方法。

逻辑运算符

基本逻辑运算符

假设要判断一个整数 x 是否大于 5 且小于 15。数学中可以写成 5 < x < 15,但在 Java 中不能直接照搬这种链式比较,需要分别写出两个条件,再用逻辑运算符连接:

x > 5 && x < 15

当 x > 5 和 x < 15 都为 true 时,条件才成立。这里使用的是短路与 &&,后面会讲它与 & 的区别。

运算符作用结果规则
&逻辑与两边都为 true,结果才为 true
|逻辑或只要有一边为 true,结果就是 true
^逻辑异或两边不同为 true,相同为 false
!逻辑非对一个布尔值取反

下面的代码分别展示了逻辑与、逻辑或、逻辑异或和逻辑非:

java
public class Test1 {
    public static void main(String[] args) {
        System.out.println(true & false);//false
        System.out.println(true | false);//true
        System.out.println(true ^ true);//false
        System.out.println(!true);//false
    }
}

短路逻辑运算符

比如我们写一个登录程序,想先检查用户名是否存在、格式是否正确,再继续检查密码。如果用户名这一关就没通过,后面很多判断是不是就没必要继续做了?短路逻辑运算符就是这样:左侧条件已经能决定最终结果时,右侧表达式就不会执行。

运算符作用何时跳过右侧表达式
&&短路与左侧为 false
||短路或左侧为 true

对于布尔值,&& 与 & 的真假规则相同,|| 与 | 的真假规则也相同;关键区别是 && 和 || 可能不执行右侧表达式,而 & 和 | 会计算两侧。

java
public class Test1 {
    public static void main(String[] args) {
        int a = 10;
        // 短路或,左边为true,整个表达式既为真
        // 右边不会进行判断和运算,a还是10
        boolean b = true || (a++ > 0);
        System.out.println(a);// 10
        // 短路与,左边条件为假,整个表达式既为假
        // 右边不会进行判断和运算,a还是10
        b = false && (a++ < 0);
        System.out.println(a);// 10
    }
}

true || (a++ > 0) 的左侧已经为 true,所以右侧不会执行;false && (a++ < 0) 的左侧为 false,右侧同样会被跳过。因此,两次输出的 a 都是 10,右侧的 a++ 没有发生。

三元运算符

条件运算符 ?: 可以根据一个布尔表达式,从两个表达式中选择一个结果,基本形式为:

条件表达式 ? 表达式1 : 表达式2

条件为 true 时取表达式 1 的结果,否则取表达式 2 的结果。例如,要比较两个整数的大小,并把较大的值赋给 c:

java
public class Test1 {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int c = a > b ? a : b;
        System.out.println(c);//20
    }
}

可以把 a > b ? a : b 读成一个问题:“a 比 b 大吗?”如果是,就取 a;如果不是,就取 b。这里 a = 10、b = 20,显然不满足 a > b,所以最后把 20 赋给 c。

运算符优先级

Java 表达式中的运算符有优先级,例如乘除的优先级高于加减。下面按从高到低的顺序整理常见运算符,数字越小表示优先级越高。

优先级运算符说明
1.、()、[]成员访问、方法调用、数组下标
2a++、a--后置自增、自减
3++a、--a、!、~、一元 + 和 -、(类型)前置运算、取反、类型转换
4*、/、%乘、除、取余
5+、-加、减
6<<、>>、>>>移位
7<、<=、>、>=、instanceof大小比较、类型判断
8==、!=相等比较
9&按位与 / 布尔逻辑与
10^按位异或 / 布尔逻辑异或
11|按位或 / 布尔逻辑或
12&&短路与
13||短路或
14?:三元运算符
15=、+=、-=、*=、/=、%= 等赋值

例如 2 + 3 * 4,会先算乘法,得到 14;如果想先算加法,就写成 (2 + 3) * 4,结果是 20。

不确定表达式如何结合时,可以使用小括号明确计算顺序。例如 (2 + 3) * 4,一眼就能看出先计算加法。实际写代码时,比起硬背整张表,保证表达式容易理解更重要。

原码、反码与补码

理解位运算前,可以先看看有符号整数如何用二进制表示。下面用 8 位二进制演示原码、反码和补码的计算过程。Java 的 byte 为 8 位,int 为 32 位;具体到 Java 的运算时,还需要按实际的数据类型判断位宽。

原码

原码(符号和数值分开表示)把最高位作为符号位:0 表示正数,1 表示负数,其余位记录数值的大小。

以 8 位为例:

  • 15 的原码是 0000 1111。
  • -1 的原码是 1000 0001。
  • 正数的最大值为 0111 1111,即 127;负数的最小值为 1111 1111,即 -127。

原码还存在两种零:0000 0000 表示 +0,1000 0000 表示 -0。

8 位原码:最左侧是符号位
+10000 0001
-11000 0001
+00000 0000
-01000 0000

红色数字是符号位;后面 7 位记录数值大小。

如果直接把原码当作普通二进制数相加,就会出现问题。比如 -1 + 1 应该为 0,但直接相加得到 1000 0010,按原码解释却是 -2。也就是说,原码直接按普通二进制加法计算,并不能正确处理这种正负数相加的情况。

反码

我们前面试过,原码直接用普通二进制加法处理负数会得到错误结果。为了让负数也能参与计算,可以尝试另一种表示方法:反码。

反码的规则并不复杂:正数保持不变;负数在原码的基础上保留符号位,将后面的数值位取反,也就是 0 变 1、1 变 0。例如:

  • -1 的原码是 1000 0001,反码是 1111 1110。
  • -2 的原码是 1000 0010,反码是 1111 1101。

还记得刚才 -1 + 1 得不到零的问题吗?现在用反码试一下:1111 1110 + 0000 0001 = 1111 1111。在反码表示中,1111 1111 对应的是 -0,所以这个结果的数值确实是 0。

不过,反码有两个零:0000 0000 表示 +0,1111 1111 表示 -0。这就引出了另一个有意思的问题:如果一个负数加上正数,结果跨过了零,会怎么样?

例如 -2 + 5,我们知道答案应该是 3。但把 -2 的反码 1111 1101 和 5 的二进制 0000 0101 直接相加,会得到:

  1. 1111 1101 + 0000 0101 = 1 0000 0010。

  2. 如果像普通固定宽度二进制加法那样直接丢弃最左边的进位,只留下 0000 0010,得到的却是 2。

为什么少了 1? 从反码有 +0、-0 两种表示这个特点来理解,就好像跨过零时还多经过了一个零。但要注意,这只是帮助我们理解现象的直观说法,并不代表反码本身算错了。

反码加法有自己的规则:最高位产生的进位要重新加回最低位,这叫“循环进位”。刚才多出来的最高位是 1,把它加回去,0000 0010 + 1 = 0000 0011,得到的就是正确答案 3。

反码加法:-2 + 5 = 3
-2 的反码1111 1101
+50000 0101
普通相加1 0000 0010
进位加回末位0000 0010 + 1
最终结果0000 0011

只丢弃进位会得到 2;按反码的循环进位规则把它加回去,最终得到 3。

也就是说,反码已经能够表示和计算负数,只是需要额外处理循环进位,而且一个零还有两种表示。能不能让零只有一种表示,同时又让二进制加法更简单呢?这就要用到补码了。

补码

补码可以在负数的反码基础上再加 1 得到。正数的补码仍然和普通二进制写法一样。先看两个例子:

  • -1:原码 1000 0001 → 反码 1111 1110 → 补码 1111 1111。
  • -2:原码 1000 0010 → 反码 1111 1101 → 补码 1111 1110。

你可以把这个变化理解成:把负数在反码中的表示整体往后移动了一位。特别是原来反码中的 -0,它的编码为 1111 1111,再加 1 后只保留低 8 位,就变成了 0000 0000,和 +0 使用同一种表示了。

再拿刚才的 -2 + 5 试试。-2 的补码是 1111 1110,加上 0000 0101:

1111 1110 + 0000 0101 = 1 0000 0011

这一次直接舍弃超出 8 位的最高位,留下 0000 0011,结果就是 3,不需要再把进位绕回来加一次。

补码加法:-2 + 5 = 3
-2 的补码1111 1110
+50000 0101
相加1 0000 0011
保留低 8 位0000 0011

补码进行固定宽度加法时,丢弃超出位宽的进位即可,不需要循环进位。

补码还有一个比较特别的地方:在 8 位补码中,1000 0000 表示 -128,所以它的取值范围是 -128~127。8 位原码、反码只能表达最低到 -127 的负数,而补码把原本重复表示零的一个编码利用了起来。

Java 的 byte、short、int、long 都使用补码表示有符号整数。理解了补码,再去看负数的位运算和移位运算就容易多了。

按位运算与移位运算

&、|、^ 既可以作用于 boolean 值,也可以用于整数。用于整数时,它们会按二进制位分别进行计算。

运算符作用基本规则
&按位与对应位都为 1 才得到 1
|按位或对应位有一个为 1 就得到 1
^按位异或对应位不同得到 1
~按位取反将每一位的 0 与 1 互换
<<左移位整体向左移动,右侧补 0
>>有符号右移位整体向右移动,左侧补符号位
>>>无符号右移位整体向右移动,左侧补 0

例如 3 & 5,先把两个正数写成二进制,再把每一位对齐计算:

按位与:3 & 5
30000 0011
50000 0101
按位与 &0000 0001

只有两个数的对应位都是 1 时,该位结果才是 1。

类似地,3 | 5 的结果为 7(0000 0111),3 ^ 5 的结果为 6(0000 0110)。按位或只要一边是 1 就留下 1;按位异或则是两个对应位不同才留下 1。~ 会把每一位取反,例如 Java 中 ~3 的结果是 -4,因为实际参与计算的是 32 位补码。

遇到负数时,需要按补码理解它的二进制位。例如计算 -3 >> 2:

  • -3 的 8 位补码示意为 1111 1101。
  • 向右移动两位,高位补 1,得到 1111 1111。
  • 这个补码对应的数是 -1,所以 -3 >> 2 的结果为 -1。
有符号右移:-3 >> 2(8 位示意)
-3 的补码1111 1101
右移 1 位1111 1110
右移 2 位1111 1111

蓝色位表示从左侧补入的 1;得到的 8 位补码 1111 1111 表示 -1。

上面的图使用 8 位来展示补位过程。在 Java 中,-3 的类型是 int,实际右移发生在 32 位二进制上;由于高位同样补 1,结果仍是 -1。

>>> 会从高位补 0。例如,int 类型的 -3 >>> 2 按 32 位计算,结果为 1073741823。

无符号右移:-3 >>> 2(Java int,32 位)
原始 32 位11111111 11111111 11111111 11111101
右移 2 位00111111 11111111 11111111 11111111

左边补入两个 0,按 32 位计算得到正数 1073741823。

处理负数位移时,关键是看补码的位模式:>> 在左侧补符号位,>>> 则补 0,因此二者可能得到完全不同的结果。

还有一点:对于 int 和 long,位移操作会分别按 32 位或 64 位进行;byte、short、char 参与位移时会先提升为 int。左移时如果高位被移出,数据也可能丢失,因此不能简单地认为左移一定等于乘以 2 的某次方。

谢谢你读到这里。 ✧

聊两句

评论审核后显示

有疑问、补充,或者想交流一下,都可以写在这里。

评论区将在滚动到这里时加载。