Java操作符
一、引言
- 操作符在编程中的核心地位
操作符是程序中对数据进行计算、判断和转换的核心工具。操作符定义数据被处理方式,将原始输入的数据(操作数)转化为有意义的中间结果,为程序逻辑提供计算能力。在Java编程中,操作符是实现数据运算和赋值的关键。对于初学者来说,理解和掌握各种操作符的用法和特性是非常重要的。
- Java操作符的设计哲学与特性
Java操作符的设计哲学以简约性、强类型安全和封装性为核心,通过严格规范操作符行为与优先级,确保代码的可预测性和可维护性。理解Java操作符的特性,有助于开发者规避常见陷阱(如整数除法截断、别名问题),并充分利用位运算等特性优化性能关键代码。
二、Java操作符分类与详解
Java中的操作符,按照操作数被处理的方式,可以分为算术操作符、赋值操作符、位操作符、关系操作符、逻辑操作符以及三元操作符。不管哪种操作符,都可以通过一个或多个操作数来生成新的值,有的操作符还可以修改其操作数的值。另外,几乎所有的操作符(除开=、==、!=)都只能操作基本数据类型的操作数。
1.算术操作符
Java的算数操作符与其他大部分编程语言相同,包括加法(+
)、减法(-
)、乘法(*
)、除法(/
)、求模(%
)和自增(++
)、自减(--
),具体见下表:
操作符 | 名称 | 示例 | 功能描述 |
---|---|---|---|
+ |
加法操作符 | a + b |
加法 - 计算操作符两侧的值的和 |
- |
减法操作符 | a - b |
减法 - 计算操作符左侧的值减去右侧的值的差 |
* |
乘法操作符 | a * b |
乘法 - 计算操作符两侧的值的积 |
/ |
除法操作符 | a / b |
除法 - 计算操作符左侧的值除以右侧的值的商 |
% |
求模操作符 | a % b |
求模 - 计算操作符左侧的值除以右侧的值的余数 |
++ |
自增操作符 | a++ 或++a |
操作数的值增加1 |
-- |
自减操作符 | a-- 或--a |
操作数的值增减1 |
下面的示例展示了在四则运算中,部分算数操作符(+、-、*、/、%)的用法。
import java.util.Random;
public class MathOps {
public static void main(String[] args) {
// 创建Random对象,为后续四则运算生成随机数:
Random rand = new Random(27);
int i, a, b;
// 生成随机整数a和b并进行四则运算:
a = rand.nextInt(100) + 1;
System.out.println("a : " + a);
b = rand.nextInt(100) + 1;
System.out.println("b : " + b);
i = a + b;
System.out.println("a + b : " + i);
i = a - b;
System.out.println("a - b : " + i);
i = a / b;
System.out.println("a / b : " + i);
i = a * b;
System.out.println("a * b : " + i);
i = a % b;
System.out.println("a % b : " + i);
a %= b;
System.out.println("a %= b : " + a);
// 生成随机浮点数s和n并进行四则运算:
double u, s, n;
s = rand.nextDouble();
System.out.println("s : " + s);
n = rand.nextDouble();
System.out.println("n : " + n);
u = s + n;
System.out.println("s + n : " + u);
u = s - n;
System.out.println("s - n : " + u);
u = s * n;
System.out.println("s * n : " + u);
u = s / n;
System.out.println("s / n : " + u);
s %= n;
System.out.println("s % n : " + s);
}
}
程序执行后的结果如下:
a : 31
b : 53
a + b : 84
a - b : -22
a / b : 0
a * b : 1643
a % b : 31
a %= b : 31
s : 0.7146368532850442
n : 0.5047406370101457
s + n : 1.21937749029519
s - n : 0.20989621627489852
s * n : 0.36070626055801924
s / n : 1.4158496480850609
s % n : 0.20989621627489852
上面的程序首先创建了一个Random对象,后续进行算术运算的整数和浮点数都通过调用Random对象的nextInt()
方法和nextDouble()
方法生成。但执行多次程序会发现每次生成的随机数都一样(所以运算结果也一样)。这是因为在创建Random对象,传递了随机种子(示例中是整数27),相同的随机种子会保证按照相同的序列生成随机数。如果去除创建Random对象的随机种子,那么Java会使用操作系统的当前时间作为随机种子。另外,Random对象除了nextInt()
方法和nextDouble()
方法之外,还有用于生成其他基本数据类型(除了char类型)的方法,方法名为next单词在前,首字母大写的基本数据类型名称在后。
一元加(
+
)和一元减(-
)与二元加减法使用了相同的符号,Java会根据+、-操作符前后的操作数自行判断运算方式。一元加(+
)会将比int类型小的数据类型(byte或者short)的操作数转换为int类型,一元减(-
)会反转数据的正负值。
下面的示例展示了自增(++
)、自减(--
)运算符的用法:
public class AutoInc {
public static void main(String[] args) {
int i = 1;
System.out.println("i: " + i);
System.out.println("++i: " + ++i); // 前缀式自增
System.out.println("i++: " + i++); // 后缀式自增
System.out.println("i: " + i);
System.out.println("--i: " + --i); // 前缀式自减
System.out.println("i--: " + i--); // 后缀式递减
System.out.println("i: " + i);
}
}
程序执行后的结果如下:
i: 1
++i: 2
i++: 2
i: 3
--i: 2
i--: 2
i: 1
自增(++
)、自减(--
)操作符都有两种使用方式,一种是放在操作数之前,称为前缀式自增或前缀式自减,如++i
或--i
,另一种是放在操作数之后,称为后缀式自增或后缀式自减,如i++
或i--
。
自增(++
)、自减(--
)操作符对操作数的影响无论前缀式还是后缀式,都一样。假如i是一个int类型的数据,i++
和++i
,对i值的影响都等价于i = i + 1
,同理,i--
和--i
,对i值的影响都等价于i = i - 1
。
那么自增(++
)、自减(--
)运算符作为前缀和后缀到底有什么区别呢?其实它们的区别主要在于整个表达式的值。前缀式自增或前缀式自减会先对操作数进行自增或自减操作,然后将操作后的结果作为整个表达式的值,后缀式自增或后缀式自减会先把操作数作为整个表达式的值,然后对操作数进行自增或自减的操作。这样,就可以上面程序的运行结果了。int类型的变量i初始保存整数1,经过两次自增(++i
和i++
)之后值变为3,再经过两次自减(--i
和i--
)之后值变为1。两次自增的操作返回表达式++i
和i++
的值虽然相同,但其中i值是不同的;同理,两次自减操作返回表达式--i
和i--
虽然相同,但其中的i值也是不同的。
2.赋值操作符
赋值操作符(=
)用于进行赋值操作。赋值操作符(=
)的操作过程就是,取=右边的数据(通常简称右值),将其保存到=左边的变量(通常简称左值)。需要注意的是,在赋值操作过程中,右值可以是其他变量、常量或者是表达式,但左值只能是命名的变量。比如,int i = 0;
表示把整数0保存到整型变量i中,10 = a
这种写法就是错误的。
赋值操作符(=
)操作的右值是基本数据类型时比较好理解,因为基本数据类型的数据是直接保存在左值的变量中,例如int a = 0; int b = a;
中b = a
就是直接将a中保存的0保存到b中。后续操作a或者b都不影响另一个变量中的数据,大多数情况下这也是我们希望的。
但赋值操作符(=
)操作的右值如果是引用数据类型,就需要注意了,因为引用数据类型的数据保存在左值中的是对该对象的引用。例如Object a = new Object();Object b = a;
中b = a
实际上是将a指向的对象的引用,复制了一个相同的引用到b。后续操作a或者b都会影响另一个变量的数据,因为它们实际上都指向同一个对象。下面这个示例将演示这种情况:
class Dog {
int age;
}
public class Assignment {
public static void main(String[] args) {
Dog dog001 = new Dog();
dog001.age = 3;
Dog dog002 = new Dog();
dog002.age = 5;
System.out.println("1:dog001.age=" + dog001.age + ",dog002.age=" + dog002.age);
dog001 = dog002;
System.out.println("2:dog001.age=" + dog001.age + ",dog002.age=" + dog002.age);
dog001.age = 7;
System.out.println("3:dog001.age=" + dog001.age + ",dog002.age=" + dog002.age);
}
}
程序执行后的结果如下:
1:dog001.age=3,dog002.age=5
2:dog001.age=5,dog002.age=5
3:dog001.age=7,dog002.age=7
Dog类只有一个成员变量age保存整型数据。它的两个实例dog001和dog002都在main()
方法中实例化出来,每个实例的age属性都保存了一个不同的整型数据。然后dog001 = dog002;
将dog002赋值给了dog001,接着修改了dog001的age。由于赋值操作符在dog001 = dog002;
中操作的是引用数据类型,它实际上让dog001和dog002指向了一个相同的Dog实例,即原来的dog002,而原来的dog001实例因为不再被引用而被垃圾回收机制清理掉。这种情况在编程中通常成为“别名”,也是Java操作对象的一种基本方式。如果想避免对象出现别名,可以将dog001 = dog002;
替换为dog001.age = dog002.age;
,这样就可以保持dog001和dog002两个实例独立,避免别名将dog001和dog002指向同一个Dog的实例。
注意上面避免别名的方式中,直接操作了对象的属性,这种操作方式不符合Java面向对象的封装特性:隐藏对象的属性和实现细节,仅对外提供公有的访问方式。
赋值操作符(=
)还可以和其他部分操作符合并使用,这种合并使用的操作符被称为复合赋值操作符。它将算术操作、位操作与赋值操作结合为单一操作符。
- 算术复合赋值操作符
通用形式为“算数操作符=”,具体见下表:
操作符 | 名称 | 示例 | 说明 |
---|---|---|---|
+= |
加法赋值 | a += 5 |
等价于 a = a + 5 |
-= |
减法赋值 | a -= 3 |
等价于 a = a - 3 |
*= |
乘法赋值 | a *= 2 |
等价于 a = a * 2 |
/= |
除法赋值 | a /= 4 |
等价于 a = a / 4 |
%= |
取模赋值 | a %= 5 |
等价于 a = a % 5 |
- 位运算复合赋值操作符
通用形式为“算数操作符=”,具体见下表:
操作符 | 名称 | 示例 | 说明 |
---|---|---|---|
&= |
按位与赋值 | a += 5 |
等价于 a = a + 5 |
|= |
按位或赋值 | a |= b |
等价于 a = a | b |
^= |
按位异或赋值 | a ^= b |
等价于 a = a ^ b |
<<= |
左移赋值 | a <<= 2 |
等价于 a = a << 2 |
>>= |
带符号右移赋值 | a >>= 1 |
等价于 a = a >> 1 |
>>>= |
无符号右移赋值 | a >>>= 1 |
等价于 a = a >>> 1 |
Java的复合赋值操作符通过简化表达式和隐式优化,成为提升代码效率的核心工具,涵盖算术操作和位操作,支持自动类型转换,但需警惕数据溢出和优先级陷阱。
另外+
和+=
还有一种特殊的用法,它们可以对Java中的字符串String对象进行连接操作。但当字符串String对象与其他数据类型进行连接时,一定要注意数据类型转换,下面这个示例将演示这种情况:
public class StringOperation {
public static void main(String[] args) {
int a = 0, b = 1, c = 2;
String s = "a, b, c ";
System.out.println(s + a + b + c); // 变量a, b, c转换为字符串
System.out.println(a + " " + s); // 变量a转换为字符串
s += "(summed) = "; // 字符串s连接新的字符串
System.out.println(s + (a + b + c));// 变量a, b, c中的整型求和
System.out.println("" + a);// 变量a转换为字符串,Integer.toString()的简化版
}
}
程序执行后的结果如下:
a, b, c 012
0 a, b, c
a, b, c (summed) = 3
0
注意第一个输出内容中的012,实际上是s和a,b,c中的0,1,2被转化的字符串进行了连接操作。第二个输出内容中的0也是a中的数值被转换为字符串后与s进行了连接操作。因此使用字符串连接操作符时,其他数据类型是否转换为字符串并不取决于先后顺序。第三个输出内容通过()
改变了a + b + c
的优先级,所以优先进行了求和才被转换为字符串。第四个输出内容使用了"" + a
的形式进行输出,实际上这是一种针对基本数据类型进行转换的方式,它等效于调用a
的基本数据类型对应的包装类的toString()
方法。
3.位操作符
前面在符合赋值操作符中见到的位操作符主要用于操作二进制数据。位操作符源自C语言,由于C语言面向底层的特性,它经常需要直接操作底层硬件寄存器中的二进制位。Java也用位操作符实现了C语言中类似的底层操作。通过对整数类型(byte、short、int、long)的二进制位进行运算,可实现高性能计算、内存优化等场景需求。
Java支持的位操作符按照对二进制位的操作方式主要分为按位操作和移位操作,具体见下表:
操作符 | 名称 | 分类 | 示例 | 功能描述 |
---|---|---|---|---|
& |
按位与 | 按位 | a & b |
两个对应位均为1时结果为1,否则为0 |
| |
按位或 | 按位 | a | b |
任意一个对应位为1时结果为1 |
^ |
按位异或 | 按位 | a ^ b |
对应位不同时结果为1,相同时为0 |
~ |
按位非 | 按位 | ~a |
每一位取反(0变1,1变0) |
<< |
左移 | 移位 | a << n |
所有位左移n位,低位补0,相当于乘以2ⁿ |
>> |
带符号右移 | 移位 | a >> n |
所有位右移n位,高位补符号位(正数补0,负数补1),相当于除以2ⁿ并向下取整 |
>>> |
无符号右移 | 移位 | a >>> n |
所有位右移n位,高位补0 |
上表中只有按位非操作符(~
)是一元操作符,它只对其右侧的操作数进行按位取反操作,所以它不能跟赋值操作符结合成复合赋值操作符使用。其他的位操作符都是二元操作符,可以跟赋值操作符结合成复合赋值操作符。按位与(&
)、按位或(|
)使用了和逻辑与(&&
)、逻辑或(||
)相同的字符,使用时一定要注意区分。
字符类型(char)由于可以转换成整型,也可以使用位操作符操作。
布尔类型(boolean)在被位操作符操作时,也需要注意,只能对布尔值进行按位与(
&
)、按位或(|
)和按位异或(^
)操作,不能对其进行按位非(~
)操作,需要对布尔值取反只能使用逻辑非(!
)。另外,对布尔值进行按位与(&
)、按位或(|
)操作时,效果和逻辑与(&&
)、逻辑或(||
)相同,但按位与(&
)、按位或(|
)不会“短路”。
下面这个示例将演示所有位操作符的操作情况:
import java.util.Random;
public class BitOperation {
public static void main(String[] args) {
Random rand = new Random(27);
printBinary("-1", -1);
printBinary("+1", +1);
int maxInt = 2147483647;
printBinary("maxInt", maxInt);
int minInt = -2147483648;
printBinary("minInt", minInt);
int i = rand.nextInt();
printBinary("i", i);
printBinary("~i", ~i);
printBinary("-i", -i);
printBinary("i << 5", i << 5);
printBinary("i >> 5", i >> 5);
printBinary("(~i) >> 5", (~i) >> 5);
printBinary("i >>> 5", i >>> 5);
printBinary("(~i) >>> 5", (~i) >>> 5);
int j = rand.nextInt();
printBinary("j", j);
printBinary("i & j", i & j);
printBinary("i | j", i | j);
printBinary("i ^ j", i ^ j);
printBinary("-1L", -1L);
printBinary("+1L", +1L);
long maxLong = 9223372036854775807L;
printBinary("maxLong", maxLong);
long minLong = -9223372036854775808L;
printBinary("minLong", minLong);
long l = rand.nextLong();
printBinary("l", l);
printBinary("~l", ~l);
printBinary("-l", -l);
printBinary("l << 5", l << 5);
printBinary("l >> 5", l >> 5);
printBinary("(~l) >> 5", (~l) >> 5);
printBinary("l >>> 5", l >>> 5);
printBinary("(~l) >>> 5", (~l) >>> 5);
long m = rand.nextLong();
printBinary("m", m);
printBinary("l & m", l & m);
printBinary("l | m", l | m);
printBinary("l ^ m", l ^ m);
}
static void printBinary(String s, int i) {
String BinaryStr = Integer.toBinaryString(i);
while(BinaryStr.length() < 32){
BinaryStr = "0"+BinaryStr;
}
System.out.println(s + ", int: " + i + ", binary:\n " + BinaryStr);
}
static void printBinary(String s, long l) {
String BinaryStr = Long.toBinaryString(l);
while(BinaryStr.length() < 64){
BinaryStr = "0"+BinaryStr;
}
System.out.println(s + ", long: " + l + ", binary:\n " + BinaryStr);
}
}
程序执行后的结果如下:
-1, int: -1, binary:
11111111111111111111111111111111
+1, int: 1, binary:
00000000000000000000000000000001
maxInt, int: 2147483647, binary:
01111111111111111111111111111111
minInt, int: -2147483648, binary:
10000000000000000000000000000000
i, int: -1152021836, binary:
10111011010101011000101010110100
~i, int: 1152021835, binary:
01000100101010100111010101001011
-i, int: 1152021836, binary:
01000100101010100111010101001100
i << 5, int: 1790006912, binary:
01101010101100010101011010000000
i >> 5, int: -36000683, binary:
11111101110110101010110001010101
(~i) >> 5, int: 36000682, binary:
00000010001001010101001110101010
i >>> 5, int: 98217045, binary:
00000101110110101010110001010101
(~i) >>> 5, int: 36000682, binary:
00000010001001010101001110101010
j, int: 1761585305, binary:
01101000111111111010100010011001
i & j, int: 676694160, binary:
00101000010101011000100010010000
i | j, int: -67130691, binary:
11111011111111111010101010111101
i ^ j, int: -743824851, binary:
11010011101010100010001000101101
……此处省略长整型(long)数据的操作结果
在上面的示例代码中,BitOperation类中,main()
方法之外定义了两个无返回值的静态方法printBinary()
,它们的方法名相同,但通过第二个参数的数据类型(int或者long)进行方法重载,将传入的int类型参数或者long类型参数通过其包装类的toBinaryString()
方法转换成字符串,由于toBinaryString()
方法在转换字符串时会忽略数值的二进制位高位中的前导0,所以方法中通过一个while循环对转换之后字符串补充高位0。这个示例对int和long类型的数值使用所有的位操作符进行了操作并展示了结果操作结果,此外,还展示了int和long的最大值、最小值、正1值和负1值的二进制形式。
注意所有整型数据都用二进制形式的最高位表示符号:0表示正数,1表示负数。
4.关系与逻辑操作符
关系与逻辑操作符返回的操作结果都是布尔值。
关系操作符会根据操作符左右两侧的操作数的值之间的关系(大小或相等),返回布尔值(true/false)。如果关系为真,关系表达式会返回true
;如果关系不为真,关系表达式会返回false
。关系操作符是控制程序逻辑的核心工具,常用于条件判断和循环控制。
关系操作符包括小于(<
)、大于(>
)、小于等于(<=
)、大于等于(>=
)、相等于(==
)和不相等于(!=
),具体见下表:
操作符 | 名称 | 示例 | 功能描述 |
---|---|---|---|
< |
小于操作符 | a < b |
检查左侧操作数的值是否小于右侧操作数的值 |
> |
大于操作符 | a > b |
检查左侧操作数的值是否大于右侧操作数的值 |
<= |
小于等于操作符 | a <= b |
检查左侧操作数的值是否小于等于右侧操作数的值 |
>= |
大于等于操作符 | a >= b |
检查左侧操作数的值是否大于等于右侧操作数的值 |
== |
相等于操作符 | a == b |
检查左侧操作数的值是否相等于右侧操作数的值 |
!= |
不相等于操作符 | a != b |
检查左侧操作数的值是否不相等于右侧操作数的值 |
下面这个示例将演示所有关系操作符的操作情况:
import java.util.Random;
public class RelationalOperation {
public static void main(String[] args) {
Random rand = new Random(27);
int a = rand.nextInt();
int b = rand.nextInt();
System.out.println("a: " + a);
System.out.println("b: " + b);
System.out.println("a < b : " + (a < b));
System.out.println("a > b : " + (a > b));
System.out.println("b <= a : " + (b <= a));
System.out.println("b >= a : " + (b >= a));
System.out.println("a == b : " + (a == b));
System.out.println("a != b : " + (a != b));
}
}
程序执行后的结果如下:
a: -1152021836
b: 1761585305
a < b : true
a > b : false
b <= a : false
b >= a : true
a == b : false
a != b : true
小于(<
)、大于(>
)、小于等于(<=
)和大于等于(>=
)适用于除布尔类型(boolean)之外的基本数据类型,因为布尔类型只有true和false两个值,对这两个值比较大小没有意义;相等于(==
)和不相等于(!=
)适用于所有基本数据类型。
相等于(==
)和不相等于(!=
)还适用于所有引用数据类型,但在操作引用数据类型时,它们的返回结果可能会引起困惑。
下面这个示例将演示相等于(==
)操作符对引用数据类型的操作情况:
public class EqualsOperation {
static void show(String desc, Integer n1, Integer n2) {
System.out.println(desc + ":");
System.out.printf("%d==%d的返回值:%b,换用equals()时的返回值:%b%n", n1, n2, n1 == n2, n1.equals(n2));
}
public static void testEquals(int value) {
// [1] 自动转换为Integer对象
Integer i1 = value;
Integer i2 = value;
show("[1]自动转换", i1, i2);
// [2] 使用Integer的构造方法实例化Integer对象
Integer r1 = new Integer(value);
Integer r2 = new Integer(value);
show("[2]构造方法", r1, r2);
// [3] 使用Integer.valueOf()方法创建Integer对象
Integer v1 = Integer.valueOf(value);
Integer v2 = Integer.valueOf(value);
show("[3]Integer.valueOf()", v1, v2);
// [4] 基本数据类型int
int x = value;
int y = value;
// x.equals(y); // 基本数据类型不能使用equals()比较
show("[4]基本数据类型int", x, y);
}
public static void main(String[] args) {
testEquals(127);
testEquals(128);
}
}
程序执行后的结果如下:
[1]自动转换:
127==127的返回值:true,换用equals()时的返回值:true
[2]构造方法:
127==127的返回值:false,换用equals()时的返回值:true
[3]Integer.valueOf():
127==127的返回值:true,换用equals()时的返回值:true
[4]基本数据类型int:
127==127的返回值:true,换用equals()时的返回值:true
[1]自动转换:
128==128的返回值:false,换用equals()时的返回值:true
[2]构造方法:
128==128的返回值:false,换用equals()时的返回值:true
[3]Integer.valueOf():
128==128的返回值:false,换用equals()时的返回值:true
[4]基本数据类型int:
128==128的返回值:false,换用equals()时的返回值:true
在上面的示例代码中,EqualsOperation类中的main()
方法调用testEquals()
方法,testEquals()
方法将传入的整型数据按照不同的方式转换为Integer对象,再将转换后的两个Integer对象通过参数传递给show()
方法,在show()
方法中分别使用相等于(==
)操作符和equals()
方法对两个Integer对象进行比较并输出结果。
如果要查看不相等于(
!=
)操作符,可以将show()
方法中的相等于(==
)操作符替换为不相等于(!=
)操作符,并将n1.equals(n2)
修改为!n1.equals(n2)
。
需要注意testEquals()
方法中将接受的整型参数转换为Integer对象的几种不同方式:
[1] 自动转换为Integer对象,本质上是通过调用Integer.valueOf()方法创建Integer对象。
[2] 使用Integer的构造方法实例化Integer对象,这种方式在Java 9之后的版本中已经不推荐使用。它是以前使用包装类进行装箱操作的主要方式。
[3] 使用Integer.valueOf()方法创建Integer对象,从Java 9开始推荐的使用包装类进行装箱操作的方式,它的效率优于方式[2],但代码较[1]复杂。
[4] 基本数据类型int也可以直接当做参数传递给show(),虽然show()方法接受的参数类型是Integer。
在程序执行后的结果中,127的比较结果相对容易理解,传递给show()方法的4种参数都产生了预期的结果。方式[2]中的相等于(==
)操作符返回了false是因为虽然两个Integer对象保存的数据都是127,但两个Integer对象是各自通过构造方法创建的,所以它们通过相等于(==
)操作符比较的引用地址是不同的。但128的比较结果就容易让人疑惑,转换为Integer对象之后,所有保存了128的对象通过相等于(==
)操作符返回的结果都是false,所有通过equals()
方法比较返回的结果都是true。这是因为在使用Integer对象时,Java会通过享元模式来缓存数值范围在-128到+127内的对象,因此在这个范围内无论使用多少次方式[1]和方式[3]来创建Integer对象,只要数值相同,它们引用的都是同一个Integer对象。而超出这个数值范围,即使数值相同,Java也会实例化新的Integer对象。所以,通常都会建议:使用操作符操作基本数据类型,使用equals()
方法操作引用数据类型。
另外使用关系操作符操作浮点数时,也会遇到一些返回结果超出预期的情况,下面这个示例将演示相等于(==
)操作符对浮点数的操作情况:
public class EqualsDouble {
static void show(String desc, Double n1, Double n2) {
System.out.println(desc + ":");
System.out.printf("%e==%e的返回值:%b,换用equals()时的返回值:%b%n", n1, n2, n1 == n2, n1.equals(n2));
}
public static void testEquals(double value1, double value2) {
// [1] 自动转换为Double对象
Double d1 = value1;
Double d2 = value2;
show("[1]自动转换", d1, d2);
// [2] 使用Double的构造方法实例化Double对象
Double r1 = new Double(value1);
Double r2 = new Double(value2);
show("[2]构造方法", r1, r2);
// [3] 使用Double.valueOf()方法创建Integer对象
Double v1 = Double.valueOf(value1);
Double v2 = Double.valueOf(value2);
show("[3]Double.valueOf()", v1, v2);
// [4] 基本数据类型double
double x = value1;
double y = value2;
// x.equals(y); // 基本数据类型不能使用equals()比较
show("[4]基本数据类型int", x, y);
}
public static void main(String[] args) {
testEquals(0, Double.MIN_VALUE);
testEquals(Double.MAX_VALUE, Double.MAX_VALUE - Double.MIN_VALUE * 1_000_000);
}
}
程序执行后的结果如下:
[1]自动转换:
0.000000e+00==4.900000e-324的返回值:false,换用equals()时的返回值:false
[2]构造方法:
0.000000e+00==4.900000e-324的返回值:false,换用equals()时的返回值:false
[3]Integer.valueOf():
0.000000e+00==4.900000e-324的返回值:false,换用equals()时的返回值:false
[4]基本数据类型int:
0.000000e+00==4.900000e-324的返回值:false,换用equals()时的返回值:false
[1]自动转换:
1.797693e+308==1.797693e+308的返回值:false,换用equals()时的返回值:true
[2]构造方法:
1.797693e+308==1.797693e+308的返回值:false,换用equals()时的返回值:true
[3]Integer.valueOf():
1.797693e+308==1.797693e+308的返回值:false,换用equals()时的返回值:true
[4]基本数据类型int:
1.797693e+308==1.797693e+308的返回值:false,换用equals()时的返回值:true
在上面的示例代码中,沿用EqualsOperation类的代码,只是将需要比较的数据类型换成了Double对象。代码中的Double.MIN_VALUE
和Double.MAX_VALUE
分别表示Double对象可以表示的最小浮点数和最大浮点数,为了方便查看输出结果,在格式化输出内容时使用了e%
指定数值使用科学计数法方式输出。第一次比较0和最小浮点数,两个数值明显不同,所以返回结果false还在预想之中,但第二次比较最大浮点数,和最大浮点数减去一百万倍的最小浮点数,从数学角度看,这两个数值明显不同,但equals()
方法都返回了true值。这是由浮点数在计算机中的特性决定的,当一个非常大的数值减去一个相对较小的数值时,这个非常大的数值不会发生明显变化,这就是编程语言中的“舍入误差”,这种误差产生的原因主要是因为计算机不能用足够的二进制位数精确的表示一个非常大的数值的微小变化。另外,相等于(==
)操作符返回的结果都是false,是因为相等于(==
)操作符比较的是Double对象的引用地址。
基于上面的两个示例,在进行关系操作时,通常都会建议:使用操作符操作基本数据类型,使用equals()方法操作引用数据类型。但是需要注意的是,并不是每个引用数据类型都重写了
equals()
方法。大部分标准库会重写equals()
方法来比较对象的内容而非对象的引用。
逻辑操作符用于根据操作数的逻辑关系,进行组合或修改,返回新的布尔值。逻辑操作符是构建复杂条件判断的基础。
逻辑操作符包括逻辑与(&&
)、逻辑或(||
)和逻辑非(!
),具体见下表:
操作符 | 名称 | 示例 | 功能描述 |
---|---|---|---|
&& |
逻辑与操作符 | a && b |
当且仅当左右两个操作数都为true,才返回true |
|| |
逻辑或于操作符 | a || b |
左右两个操作数任一为true,就返回true |
! |
逻辑非操作符 | !a |
用来反转操作数的逻辑状态,将true反转为false,将false反转为true |
下面这个示例将演示关系操作符和逻辑操作符的操作情况:
import java.util.Random;
public class LogicOperation {
public static void main(String[] args) {
Random rand = new Random(27);
int a = rand.nextInt();
int b = rand.nextInt();
System.out.println("a: " + a);
System.out.println("b: " + b);
System.out.println("a < b : " + (a < b));
System.out.println("a > b : " + (a > b));
System.out.println("b <= a : " + (b <= a));
System.out.println("b >= a : " + (b >= a));
System.out.println("a == b : " + (a == b));
System.out.println("a != b : " + (a != b));
// Java中不允许将int类型当作boolean类型使用,以下注释的代码是错误的:
// System.out.println("a && b : " + (a && b));
// System.out.println("a || b : " + (a || b));
// System.out.println("!a : " + !a);
System.out.println("(a < 10) && (b < 10) : " + ((a < 10) && (b < 10)));
System.out.println("(a < 10) || (b < 10) : " + ((a < 10) || (b < 10)));
}
}
程序执行后的结果如下:
a: -1152021836
b: 1761585305
a < b : true
a > b : false
b <= a : false
b >= a : true
a == b : false
a != b : true
(a < 10) && (b < 10) : false
(a < 10) || (b < 10) : true
在上面的示例代码中,注释的代码内容是错误的用法,因为在Java中,逻辑与(&&
)、逻辑或(||
)和逻辑非(!
)只能用于操作boolean类型的数据,所以即使将示例代码中的int类型替换为其他除boolean之外的任何基本数据类型,代码都可以正常运行。示例代码的最后两个输出内容,先由关系操作符比较变量a、b和10的大小关系,再由逻辑操作符根据返回的布尔值结果进行逻辑操作,最终返回逻辑操作的布尔值结果。
逻辑与(&&
)和逻辑或(||
)在进行逻辑操作时,还具有“短路”的特性,这种特性导致它们在进行逻辑操作是,一旦表达式的部分操作结果能够确定整个表达式的返回值,那么表达式剩余的部分就不会继续操作。比如,condition1 && condition2
,如果condition1
返回了false,无论condition2
返回的布尔值是什么,整个表达式的值都已经由逻辑与(&&
)左侧的false确定了整个表达式的返回值是false,那么condition2
就不会被执行。同样,condition1 || condition2
,如果condition1
返回了true,无论condition2
返回的布尔值是什么,整个表达式的值都已经由逻辑或(||
)左侧的true确定了整个表达式的返回值是true,那么condition2
就不会被执行。
下面这个示例将演示逻辑与(&&
)和逻辑或(||
)的短路操作情况:
public class LogicShortCircuit {
static boolean condition1(int val) {
System.out.println("condition1(" + val + ")");
System.out.println("返回的结果是: " + (val < 1));
return val < 1;
}
static boolean condition2(int val) {
System.out.println("condition2(" + val + ")");
System.out.println("返回的结果是: " + (val < 2));
return val < 2;
}
static boolean condition3(int val) {
System.out.println("condition3(" + val + ")");
System.out.println("返回的结果是: " + (val < 3));
return val < 3;
}
public static void main(String[] args) {
boolean b1 = condition1(0) && condition2(2) && condition3(2);
System.out.println("表达式condition1(0) && condition2(2) && condition3(2)");
System.out.println("返回的结果是: " + b1);
boolean b2 = condition1(0) || condition2(2) || condition3(2);
System.out.println("表达式condition1(0) || condition2(2) || condition3(2)");
System.out.println("返回的结果是: " + b2);
}
}
程序执行后的结果如下:
condition1(0)
返回的结果是: true
condition2(2)
返回的结果是: false
表达式condition1(0) && condition2(2) && condition3(2)
返回的结果是: false
condition1(0)
返回的结果是: true
表达式condition1(0) || condition2(2) || condition3(2)
返回的结果是: true
在上面的示例代码中,condition1()
、condition2()
和condition3()
分别将接受到的整型参数同0,1和2进行小于比较,并返回结果。表达式condition1(0) && condition2(2) && condition3(2)
在计算到condition2(2)
时得到了false,就将false作为整个表达式的返回值并结束操作,所以condition3(2)
被短路未执行;同理,表达式condition1(0) || condition2(2) || condition3(2)
在计算到condition1(0)
时得到了true,就将true作为整个表达式的返回值并结束操作,所以condition2(2)
和condition3(2)
被短路未执行。Java的逻辑短路特性可以快速返回逻辑操作结果,从某种程度上看,还可以减少系统资源开销。但是需要注意避免在可能逻辑短路的表达式中对数据进行操作。
5.三元操作符
三元操作符比较特殊,它有三个操作数,不过它确实是一个操作符,因为它会根据它的三个操作数返回一个操作结果。三元表达式的形式如下:
expression ? value0 : value1
如果expression的操作结果是布尔值true,value0就会返回操作结果,这个操作结果会作为整个三元表达式的操作结果;反之,如果expression的操作结果是布尔值false,value1就会返回操作结果,这个操作结果会作为整个三元表达式的操作结果。所以,三元操作符也常被称为条件操作符。但三元操作符与程序流程控制中的if…else…语句仍要进行区分,因为三元操作符会返回操作结果。
下面这个示例将演示三元操作符的操作情况:
public class TernaryOperation {
static int ternary(int i) {
return (i < 10) ? i * 10 : i / 2;
}
static int standardIfElse(int i) {
if (i < 10)
return i * 10;
else
return i / 2;
}
public static void main(String[] args) {
System.out.println(ternary(9));
System.out.println(ternary(10));
System.out.println(standardIfElse(9));
System.out.println(standardIfElse(10));
}
}
程序执行后的结果如下:
90
5
90
5
在上面的示例代码中,ternary()
方法接受int类型参数,将接收的参数值通过三元操作符处理并返回三元表达式的返回值,standardIfElse()
方法接受int类型参数,通过if…else…语句判断接收的参数值是否小于10,再决定返回哪一个计算结果。这两个方法相比,三元操作符使得代码更加简洁,if…else…语句更容易理解,各有优势,但三元操作符更适合需要从两个值选择一个赋值给变量的操作场景。
6.操作符的优先级和结合性
当一个表达式中出现了多个操作符时,操作符的优先级会决定表达式的操作顺序。操作符的结合性则决定了操作数的操作方向。
优先级 | 操作符名称 | 操作符说明 | 结合性 |
---|---|---|---|
1 | () |
分隔符 | 从左向右 |
2 | ! + (一元正) - (一元负) ~ ++ -- |
一元操作符 | 从右向左 |
3 | * / % |
算术操作符 | 从左向右 |
4 | + (加法) - (减法) |
算术操作符 | 从左向右 |
5 | << >> >>> |
移位操作符 | 从左向右 |
6 | < <= >= > instanceof |
关系操作符 | 从左向右 |
7 | == != |
关系操作符 | 从左向右 |
8 | & |
按位操作符 | 从左向右 |
9 | ^ |
按位操作符 | 从左向右 |
10 | | |
按位操作符 | 从左向右 |
11 | && |
逻辑操作符 | 从左向右 |
12 | || |
逻辑操作符 | 从左向右 |
13 | ?: |
条件操作符 | 从右向左 |
14 | = += *= /= %= &= ^= <<= >>= >>>= |
赋值操作符 | 从右向左 |
在编写复杂的表达式时,如果不确定操作符的优先级,可以使用括号来明确指定操作的顺序。例如,表达式 a + b * c
中,乘法操作 b * c
会先于加法操作执行,因为乘法操作符的优先级高于加法操作符。如果想要先执行加法操作,则应该写成 (a + b) * c
。在实际编程中,合理利用括号来避免优先级引起的错误是一个好习惯。
在使用按位操作符和移位操作符时,需要特别注意它们的优先级,因为它们可能会影响到数值的正负和大小。此外,逻辑操作符 (
&&
)和(||
)具有短路特性,即在确定整个逻辑表达式的结果后,可能不会操作所有的操作数。
评论已关闭