第三章 操作符

39 阅读22分钟

Java 提供了丰富的操作符。它的大部分操作符可以分为以下四组:算术、位、关系和逻辑操作符。Java 还定义了一些处理特定情况的额外操作符。本章节介绍了除类型比较运算符 instanceof和箭头运算符(−>)之外的所有Java运算符。

3.1 赋值操作符

在讲操作符之前,我们很有必要说一下赋值操作符。在大多数编程语言中,=符号的含义与我们在数学中学到的=符号不同。在编程中,=符号被称为赋值运算符。它的意思是我们将=号右边的值赋给左边的变量。 因此,语句x = y和y = x的意义非常不同。

如果你感到困惑,下面这个例子例子可能会澄清这一点。 假设我们声明两个变量x和y,如下所示:

 int x = 5; 
 int y = 10; 

如果你写 x = y; 你的数学老师可能会对你感到不满,因为x不等于y。但是,在编程中,这是可以的。 这个语句的意思是我们将y的值赋给x。将一个变量的值赋给另一个变量是可以的。在我们的例子中,x的值现在变成了10,而y的值保持不变。换句话说,现在x = 10,y = 10。 如果我们现在通过写x = 3; y = 20;来更改x和y的值,并写下

y = x;

我们将x的值赋给变量y。因此,y变为3,而x的值保持不变(即y = 3,x = 3现在)。

除了=操作符外,Java(以及大多数编程语言)还有一些其他的赋值操作符,如+=、-=和*=。 假设我们有一个变量x,初始值为10。如果我们想要将x增加2,我们可以写成x = x + 2;。程序将首先计算右侧的表达式(x + 2)的值,然后将答案赋给左侧。因此最终x变成12。 我们也可以用x += 2来表示相同的意思。+=操作符是一个简写,它将赋值操作符与加法操作符结合起来。因此,x += 2简单地意味着x = x + 2。类似地,如果我们想要进行减法运算,我们可以写成x = x - 2或x -= 2。上面提到的所有5个操作符都适用于这种情况。

3.2 算术操作符

算术操作符在数学表达式中的使用方式与代数中相同。下表列出了算术运算符:

操作符描述例子
+加法 - 相加运算符两侧的值A + B 等于 30
-减法 - 左操作数减去右操作数A – B 等于 -10
*乘法 - 相乘操作符两侧的值A * B等于200
/除法 - 左操作数除以右操作数B / A等于2
取余 - 左操作数除以右操作数的余数B%A等于0
++自增: 操作数的值增加1B++ 或 ++B 等于 21(区别详见下文)
--自减: 操作数的值减少1B-- 或 --B 等于 19(区别详见下文)

算术操作符的操作数必须是数字类型。您不能在boolean类型上使用它们,但可以在char类型上使用它们,因为在Java中,char类型本质上是int类型的子集。

3.2.1 基本算术运算符

基本算术运算——加法、减法、乘法和除法——对所有数字类型都和数学上的做法是一致的。一元负号运算符对其单个操作数取负数。一元加号运算符仅返回其操作数的值。

请记住,当整型数应用除法运算符时,结果将不会附带任何小数部分。 以下简单示例程序演示了算术操作符。它还说明了浮点数除法和整数除法之间的差异。

int a = 10;
int b = 5;

System.out.println(a + b);//15
System.out.println(a - b);//5
System.out.println(a * b);//50
System.out.println(a / b);//2
System.out.println(a % b);//0

b = 3;
System.out.println(a + b);//13
System.out.println(a - b);//7
System.out.println(a * b);//30
System.out.println(a / b);//3
System.out.println(a % b);//1

对于初学者来说,加法(+)、减法(-)、乘法(*)很好理解,但除法(/)和取余(%)会有一点点疑惑。在以往的认知里,10/3 是除不尽的,结果应该是 3.333333...,而不应该是 3。相应的,余数也不应该是 1。这是为什么呢?

因为数字在程序中可以分为两种,一种是整型,一种是浮点型,整型和整型的运算结果就是整型,不会出现浮点型。否则,就会出现浮点型。

int a = 10;
float c = 3.0f;
double d = 3.0;
System.out.println(a / c); // 3.3333333
System.out.println(a / d); // 3.3333333333333335
System.out.println(a % c); // 1.0
System.out.println(a % d); // 1.0

需要注意的是,当浮点数除以 0 的时候,结果为 Infinity 或者 NaN。

System.out.println(10.0 / 0.0); // Infinity
System.out.println(0.0 / 0.0); // NaN

当整数除以 0 的时候(10 / 0),会抛出异常open in new window

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.itwanger.eleven.ArithmeticOperator.main(ArithmeticOperator.java:32)

所以整数在进行除法运算时,需要先判断除数是否为 0,以免程序抛出异常。

算术运算符中还有两种特殊的运算符,自增运算符(++)和自减运算符(--),它们也叫做一元运算符,只有一个操作数。

int x = 10;
System.out.println(x++);//10 (11)  
System.out.println(++x);//12  
System.out.println(x--);//12 (11)  
System.out.println(--x);//10  

求模运算符(modulus operator)%,返回一个除法操作的余数。它可以应用于浮点数类型和整数类型。以下示例程序演示了%的用法:

package com.mycompany.modulus;

//展示%操作符号
public class Modulus {
    public static void main(String[] args) {
        int x =42;
        double y=42.25;
        
        System.out.println("x mod 10="+x%10);
        System.out.println("y mod 10="+y%10);
    }
}

它的输出结果为:

x mod 10=2
y mod 10=2.25

3.3 位操作符

Java 定义了几个可以应用于 long、int、short、char 和 byte 整数类型的位运算符。这些运算符作用于操作数的每个位。下表列出了位运算符的基本运算,假设整数变量 A 的值为 60 和变量 B 的值为 13:

操作符描述例子
与操作符号,如果相对应位都是1,则结果为1,否则为0(A&B),得到12,即0000 1100
或操作符号,如果相对应位都是 0,则结果为 0,否则为 1(AB)得到61,即 0011 1101
亦或操作符号,如果相对应位值相同,则结果为0,否则为1(A ^ B)得到49,即 0011 0001
按位取反运算符翻转操作数的每一位,即0变成1,1变成0。(〜A)得到-61,即1100 0011
<<按位左移运算符。左操作数按位左移右操作数指定的位数。A << 2得到240,即 1111 0000
>>按位右移运算符。左操作数按位右移右操作数指定的位数。A >> 2得到15即 1111
>>>按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充。A>>>2得到15即0000 1111
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A | B = 0011 1101
A ^ B = 0011 0001
~A= 1100 0011

由于位运算符操作的对象是变量二进制表示的每一位。Java所有的数值都是以二进制的形式存储的,例如,42的二进制表示为00101010,其中每个位置表示2的幂次,从最右边的位开始为2^0。向左移动一个位位置将是2^1,继续向左移动2^2,然后是8、16、32等等。42在位置1、3和5(从右边的0开始计数)上是1;因此,42是21+23+25的总和,即2+8+32。

所有整数类型(除了char)都是有符号整数。这意味着它们可以表示负值和正值。Java使用二进制补码来表示有符号的整数,这意味着负数通过取反(将1变为0,将0变为1)值的所有位,然后将结果加1来表示。例如,-42通过取反42的所有位或00101010表示,结果为11010101,然后加1,结果为11010110或-42。要解码负数,首先要取反所有位,然后加1。例如,-42或11010110取反得到00101001或41,因此当您加1时,您得到42。

Java(以及大多数其他计算机语言)使用二进制补码的原因是为了让0有唯一的二进制表示。当使用二进制补码表示-0时,对反码加1,得到100000000。而最高位将会被舍去,这样结果和+0是一致的。尽管在前面的示例中仅仅使用了一个字节的情况,但相同的基本原理适用于Java的所有整数类型。

因为Java使用二进制补码来存储负数,而且在Java中所有的整数都是有符号的,所以应用按位运算符可能会产生意外的结果。只需记住,最高位决定了整数的符号!

3.3.1 按位与操作符

按位与运算符“&”表示,只有当两个操作数都是1,则产生一个1。在所有其他情况下都产生0。下面是一个例子:

image-20230415203151149.png

3.3.2 按位或操作符

按位或运算符“|”表示,只要操作数中的任一位是1,则结果位是1,如下所示:

image-20230415204204579.png

3.3.3 按位异或操作符

按位异或运算符为“^”,他表示如果仅一个操作数为1,则结果为1。异或还有一种描述方式,即两个操作数不同时位1,相同时为0。下面的例子展示了“^”的效果。这个例子还展示了XOR运算的一个有用特性。那就是第二个操作数为1的时候,第一个操作数会取反。

image-20230415223634980.png

3.3.4 按位取反操作符

按位取反运算符“~”是一元运算符,它反转其操作数的所有位。例如,数字42具有以下位模式:

00101010 

应用NOT运算符后变为

11010101 

也称为按位补码,一元NOT运算符“~”反转其操作数的所有位。

3.3.5 左移操作符

左移操作符“<<”可以将一个数值的所有位向左移动指定的位数。它的一般形式为:

 value << num 

这里,num指定了要将value中的值向左移动的位数。也就是说,"<<"将指定值中的所有位向左移动num指定的位数。对于每次左移,最高位被移出(且丢失),在右边会带入一个0。这意味着当对int类型的操作数进行左移操作时,一旦它们向左移动超过31位,则会丢失这些位。如果操作数是long类型,则会在位63后丢失这些位。

Java的自动类型提升会在移位byte和short值时产生意外结果。当计算表达式时,byte和short值会被提升为int类型。此外,这种表达式的结果也是int类型。这意味着对byte或short值进行左移的结果将是int类型,直到移位超过第31位时,移位的位数才会丢失。此外,当将负的byte或short值提升为int类型时,它们会进行符号扩展,因此高位会被填充为1。因此,对byte或short进行左移意味着必须舍弃int结果的高位字节。例如,如果对一个byte值进行左移,该值首先会被提升为int,然后再进行移位。这意味着如果您想要一个移位后的byte值,必须舍弃结果的前三个字节。最简单的方法是将结果简单地转换回byte类型。以下程序演示了这个过程:

package com.mycompany.byteshift;

//左移一个byte类型
public class ByteShift {

    public static void main(String[] args) {
        byte a=64,b;
        int i;
        
        i=a<<2;
        b=(byte)(a<<2);
        
        System.out.println("原始的a的值为"+a);
        System.out.println("i 和 b的值为"+i+" "+b);
    }
}

这个程序的结果为:

原始的a的值为64
ib的值为256 0

由于a在计算时被提升为int类型,将值64(0100 0000)左移两次会导致i包含值256(1 0000 0000)。然而,b中的值为0,因为移位后,低位字节现在为零。它唯一的1位已经被移出。

由于每次左移的效果是将原始值加倍,程序员通常将这个事实作为乘以2的高效替代方法。但需要注意的是,如果将1位移入高位位置(第31位或第63位),则该值将变为负数。以下程序说明了这一点:

package com.mycompany.multbytwo;

//左移一位以得到乘以二的效果
public class MultByTwo {
    public static void main(String[] args) {
        int i;
        int num=0xFFFFFFE;
        
        for(i=0;i<4;i++)
        {
            num=num<<1;
            System.out.println(num);
        }
    }
}

这个程序的输出为:

536870908
1073741816
2147483632
-32

3.3.6 右移操作符

右移运算符“>>”会将一个数值中的所有位向右移动指定次数。它的一般形式如下:

value >> num 

这里,num指定了要将value中的值向右移动的位数。也就是说,“>>”运算符将指定值中的所有位向右移动num所指定的位数。 下面的代码片段将数值32向右移动两个位置,结果将a设置为8:

 int a = 32; 
 a = a >> 2; // 现在a为8 

当一个数值的位数“移位超出”时,这些位会丢失。例如,下一个代码片段将数值35向右移动两个位置,导致最低的两个位被丢失,结果再次将a设置为8:

int a = 35;
a = a >> 2; // a contains 8

让我们把这些变量用二进制表示,这个过程就显而易见了:

00100011 35
>> 2
00001000 8

每次将一个值向右移位时,它都会将该值除以二,并丢弃任何余数。在某些情况下,您可以利用这种方式来实现高性能的整数除以2。 当向右移位时,通过右移操作所暴露的最高(最左边)位被填充为原先最高位的内容。这称为符号扩展(sign extension),并用于在向右移动负数时保留其符号。例如,-8 >> 1 的结果是-4,在二进制中是:

111110008
>> 1
111111004

3.3.7 无符号右移位操作符

>>操作符每次进行移位时自动填充高位比特位的先前内容,以保留值的符号。然而,有时这是不可取的。例如,如果您要移动的是不表示数字值的内容,则可能不希望发生符号扩展。在像素值和图形处理中,这种情况很常见。在这些情况下,我们通常希望将0移入高位比特中,而不管变量的初始值是正的还是负的。这称为无符号移位。为了实现这一点,我们可以使用Java的无符号右移位操作符>>>,它始终将0移入高位比特中。下面的代码片段演示了>>>。 变量a的初始值设置为-1,这意味着变量a的32位二进制都是1。然后将这个值向右移24位,将前24位填充为0,忽略正常的符号扩展。a就变为255。

int a = -1; 

a = a >>> 24;

以下是相同操作的二进制形式,以进一步说明发生了什么:

11111111 11111111 11111111 11111111 // -1 的二进制补码
>>>24
00000000 00000000 00000000 11111111 //二进制表示的255

无符号右移位操作符>>>并不是对所有类型有用的,它仅对32位和64位值有意义。在表达式中,较小的值会自动提升为int。这意味着发生符号扩展,并且移位将在32位而不是在原来的8位或16位类型的值上进行。也就是说,我们可能期望在字节值上进行无符号右移,从第7位开始填充为零。但实际上情况并非如此,因为实际上是在移动32位值。以下程序演示了这种效果:

package com.mycompany.byteushift;

//无符号右移一个二进制数
public class ByteUShift {

    public static void main(String[] args) {
        char hex[]={
            '0','1','2','3','4','5','6','7',
            '8','9','a','b','c','d','e','f'
        };
        byte b=(byte) 0xf1;
        byte c=(byte) (b>>4);
        byte d=(byte) (b>>>4);
        byte e=(byte) ((b&0xff)>>4);
        
        System.out.println("            b=0x"+hex[(b>>4)&0x0f]+hex[b&0x0f]);
        System.out.println("         b>>4=0x"+hex[(c>>4)&0x0f]+hex[c&0x0f]);
        System.out.println("        b>>>4=0x"+hex[(d>>4)&0x0f]+hex[d&0x0f]);
        System.out.println("  (b&0xff)>>4=0x"+hex[(e>>4)&0x0f]+hex[e&0x0f]);
    }
}

它的输出为:

            b=0xf1
         b>>4=0xff
        b>>>4=0xff
  (b&0xff)>>4=0x0f

该程序的输出显示了当处理字节时,>>>运算符似乎不起作用。该程序将变量b设置为一个负数进行演示。然后将c设置为右移四位的b的字节值,由于符号扩展,c的值为0xff。接下来将d赋值为b的无符号右移四位的字节值,你可能期望的值为0x0f,但实际上是0xff,因为在移位之前,b被提升为int类型时发生了符号扩展。最后一个表达式使用AND运算符将e赋值为b的8位掩码字节值,然后右移四位,这产生了期望的值0x0f。对于表达式 (b&0xff)>>4,首先使用 & 运算符对b进行了一个掩码操作,将b的高24位都置为0(b被自动升级为了int),只保留了b的低8位。然后,再对这个结果进行右移4位操作,得到一个无符号的整数值,其值为0x0f。这是因为,掩码操作将原来的有符号byte类型的b转换为了一个无符号的int类型,因此不会发生符号扩展。最终得到的结果是我们所期望的。

3.3.8 位运算复合赋值操作符

所有的二进制位运算符都有一个类似于代数运算的复合形式,它将赋值和位运算结合起来。例如,以下两个语句将值a向右移动四位,它们是等价的:

a = a >> 4;

a >>= 4; 

同样,以下两个语句将使变量a被赋值为位运算表达式a或b的结果,它们是等价的:

a = a | b; 

a |= b; 

下面的程序创建了几个整数变量,并使用复合位运算符赋值操作符来操作这些变量:

package com.mycompany.opbitequals;

public class OpBitEquals {

    public static void main(String[] args) {
        int a=1;
        int b=2;
        int c=3;
        
        a|=4;
        b>>=1;
        c<<=1;
        a^=c;
        System.out.println("a="+a);
        System.out.println("b="+b);
        System.out.println("c="+c);
        
    }
}

它的结果为:

a = 3
b = 1
c = 6

最后用一个程序总结一下以上几种位运算:

public class BinaryTest {
  public static void main(String[] args) {
     int a = 60; /* 60 = 0011 1100 */ 
     int b = 13; /* 13 = 0000 1101 */
     int c = 0;
     c = a & b;       /* 12 = 0000 1100 */
     System.out.println("a & b = " + c );
 
     c = a | b;       /* 61 = 0011 1101 */
     System.out.println("a | b = " + c );
 
     c = a ^ b;       /* 49 = 0011 0001 */
     System.out.println("a ^ b = " + c );
 
     c = ~a;          /*-61 = 1100 0011 */
     System.out.println("~a = " + c );
 
     c = a << 2;     /* 240 = 1111 0000 */
     System.out.println("a << 2 = " + c );
 
     c = a >> 2;     /* 15 = 1111 */
     System.out.println("a >> 2  = " + c );
  
     c = a >>> 2;     /* 15 = 0000 1111 */
     System.out.println("a >>> 2 = " + c );
  }
} 

以上实例编译运行结果如下:

a & b = 12
a | b = 61
a ^ b = 49
~a = -61
a << 2 = 240
a >> 2  = 15
a >>> 2 = 15

3.4 关系操作符

在某些情况下,当设计解决问题的方案时,我们可能需要引入条件判断来控制执行流程。条件判断需要使用比较运算符对两个值进行比较。在本节中,将描述Java中使用的所有比较运算符,并提供代码示例。

下表为Java支持的关系运算符

表格中的实例整数变量A的值为10,变量B的值为20:

运算符描述例子
==检查如果两个操作数的值是否相等,如果相等则条件为真。(A == B)为假。
!=检查如果两个操作数的值是否相等,如果值不相等则条件为真。(A != B) 为真。
检查左操作数的值是否大于右操作数的值,如果是那么条件为真。(A> B)为假。
<检查左操作数的值是否小于右操作数的值,如果是那么条件为真。(A <B)为真。
>=检查左操作数的值是否大于或等于右操作数的值,如果是那么条件为真。(A> = B)为假。
<=检查左操作数的值是否小于或等于右操作数的值,如果是那么条件为真。(A <= B)为真。

这些操作的结果是一个布尔值。关系运算符最常用于控制 if 语句和各种循环语句的表达式中。下面我们逐个细说。

== 检测操作符左右两边的操作数是否相等。因为在Java中单个等号(=)被用作赋值,开发人员就用了两个等号来判断两个值是否相等。在以下代码示例中,我们将看到一个测试==比较器的示例,以在数组中查找值2。如果找到该值,则在控制台中打印索引。

package com.mycompany.comparisonoperatorsdemo;

public class ComparisonOperatorsDemo {

    public static void main(String[] args) {
        int[] values={1,7,9,2,6,};
        
        for(int i=0;i<values.length;i++)
        {
            if(values[i]==2)\(*)
            {
                System.out.println("在索引"+i+"处找到了2");
            }
        }
        
    }
}

在标记为(*)的行中测试条件是否被满足,测试的结果是一个布尔值。当结果为false时,不会执行任何操作,但如果结果为true,则打印索引。提醒一下,这里一定是用==而不是=。同时,==只能用于原始数据类型。

!= 用于测试两边的操作数是否不等。它是 == 运算符的相反操作。作为练习,你可以修改前面的示例代码让它在数组值不为 2 时打印一条消息。该运算符也适用于引用类型。但是,如果您想测试引用值的不等性,则必须使用类似于 !a.equals(b) 的表达式。

< 和 <= 与您在数学课上学到的目的相同。第一个 (<) 测试运算符左侧的项是否小于右侧的项。第二个运算符 (<=) 测试运算符左侧的项是否小于或等于右侧的项。此运算符不能用于引用类型。

和 >= 也与您在数学课上学到的目的相同。第一个 (>) 测试运算符左侧的项是否大于右侧的项。第二个运算符 (>=) 测试运算符左侧的项是否大于或等于右侧的项。此运算符也不能用于引用类型。

几乎所有的数值运算符都可以用于不同基本类型的变量,因为它们会自动升级为具有更大区间的数值类型。以下代码反映了这个事实:

package com.mycompany.mixedoperationsdemo;


public class MixedOperationsDemo {
    public static void main(String[] args) {
        byte b=1;
        short s=2;
        int i=3;
        long l=4;
        
        float f=5;
        double d=6;
        int ii=6;
        
        double red=l+d;
        long resl=s+3;
        //etc
        
        if(b<=s)
        {
            System.out.println("byte val < short val");
        }
        if(i>=b)
        {
            System.out.println("int val >= byte val");
        }
        if(l>b)
        {
            System.out.println("long val > byte val");
        }
        if(d > i) 
        {
            System.out.println("double val > byte val");
        }
        if(i == i) 
        {
            System.out.println("double val == int val");
        }
        
        
    }
}

这样的代码是非常不好的,在处理浮点数的时候,它会得到与事实相反的结果,比如以下的代码:

package com.mycompany.decimalpointdemo;


public class DecimalPointDemo {

    public static void main(String[] args) {
        float f1=2.2f;
        float f2=2.0f;
        float f3=f1*f2;
        if(f3==4.4)
        {
            System.out.println("f3的值和预期相同,维4.4");
        }
        else
        {
            System.out.println("f3的值和事实不同,它的值为"+f3);
        }
    }
}

它的输出结果为:

f3的值和事实不同,它的值为4.4

由于浮点数的表示原因(IEEE 754,如果对这部分内容感兴趣,可以去学习计算机组成原理),对于浮点数的比较我们不能简单粗暴地用“==”,取而代之的,我们应该用 Float.compare,下面这个例子说明如何正确地比较浮点数:

package com.mycompany.decimalpointcorrecteddemo;

//纠正过后的浮点数比较程序
public class DecimalPointCorrectedDemo {

    public static void main(String[] args) {
        float f1=2.2f;
        float f2=2.0f;
        float f3=f1*f2;
        if(Float.compare(f3, 4.4f)==0)
        {
            System.out.println("f3的值和预期相同,为4.4");
        }
        else
        {
            System.out.println("f3的值和事实不同,它的值为"+f3);
        }
    }
}

它的结果和我们预料的就一样了:

f3的值和预期相同,为4.4

3.5 逻辑操作符

逻辑运算符仅在两个布尔操作数上运算。所有二元逻辑运算符都将两个布尔值组合起来形成一个新的boolean结果。下表列出了逻辑运算符的基本运算,假设布尔变量A为真,变量B为假

操作符描述例子
&&称为逻辑与运算符。当且仅当两个操作数都为真,条件才为真。(A && B)为假。
称为逻辑或操作符。如果任何两个操作数任何一个为真,条件为真。(AB)为真。
称为逻辑非运算符。用来反转操作数的逻辑状态。如果条件为true,则逻辑非运算符将得到false。!(A && B)为真。

下面的简单示例程序演示了逻辑运算符。

public class LogicalOperatorDemo {
  public static void main(String[] args) {
     boolean a = true;
     boolean b = false;
     System.out.println("a && b = " + (a&&b));
     System.out.println("a || b = " + (a||b) );
     System.out.println("!(a && b) = " + !(a && b));
  }
}

3.5.1 短路逻辑操作符

有四个运算符用于构建逻辑运算符;其中两个是位运算符,可以在逻辑运算中重复使用:&(AND) 和 |(OR);但是它们需要对运算符两端的表达式求值判断。运算符&&(AND) 和 ||(OR) 具有相同的效果,但不需要对两端的表达式都去求值,这就是它们也被称为快捷运算符的原因。为了解释这些运算符的行为,这里有一个典型的例子。我们声明了一个包含十个项的列表(其中一些为空),以及一个用于生成随机索引的方法,用于从列表中选择一个项。然后,我们测试从列表中选择的元素是否不为空并且等于预期值。如果两个条件都为真,则在控制台上打印一条消息:

package com.mycompany.logicaldemo;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class LogicalDemo {
    static List<String> terms=new ArrayList<>(){//声明一个包含十个项的列表(其中一些为空)以及一个用于生成随机索引的方法
        {
            add("Rose");
            add(null);
            add("River");
            add("Clara");
            add("Vastra");
            add("Psi");
            add("Cas");
            add(null);
            add("Nardhole");
            add("Strax");
        }
    };

    public static void main(String[] args) {
        for(int i=0;i<20;++i)
        {
            int rnd=getRandomNumber();
            String term=terms.get(rnd);
            System.out.println("Generated index:"+rnd);
            if(term!=null&term.equals("Rose")) 
            {
                System.out.println("Rose was found");
            }
        }
    }
    private static int getRandomNumber()
    {
        Random r=new Random();
        return r.nextInt(10);
    }
}

为了确保我们得到预期的结果,我们重复选择随机项的操作20次。在ift条件控制语句的这一行,&将两个表达式组合在一起。因此,只有当term变量的值不为空且等于Rose时,才应在控制台上打印出"Rose was found"文本。因此,当运行前面的代码时,预计会在控制台上看到类似以下内容的输出结果。

Generated index:0
Rose was found
Generated index:2
Generated index:6
Generated index:1
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "term" is null

但是,这样考虑一下:如果左边的条件是term==null,我们是否应该判断等term是否等于“Rose”?而且在null对象上调用方法会导致运行时错误。显然&就不适用于以上的情况。如果term==null,则第一个条件不成立,这种情况下再去判断第二个条件毫无意义。因此,就有了&&快速运算符。在使用&&逻辑操作符的时候,如果第一项不成立(false),那么第二项是否满足是无关紧要的,结果始终为false。因此,我们可以将先前的代码示例更正如下:

package com.mycompany.logicaldemo;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class LogicalDemo {
    static List<String> terms=new ArrayList<>(){//声明一个包含十个项的列表(其中一些为空)以及一个用于生成随机索引的方法
        {
            add("Rose");
            add(null);
            add("River");
            add("Clara");
            add("Vastra");
            add("Psi");
            add("Cas");
            add(null);
            add("Nardhole");
            add("Strax");
        }
    };

    public static void main(String[] args) {
        for(int i=0;i<20;++i)
        {
            int rnd=getRandomNumber();
            String term=terms.get(rnd);
            System.out.println("Generated index:"+rnd);
            if(term!=null&&term.equals("Rose")) //这里改成了&&
            {
                System.out.println("Rose was found");
            }
        }
    }
    private static int getRandomNumber()
    {
        Random r=new Random();
        return r.nextInt(10);
    }
}

这次就不会有报错信息了,因为如果第一项term是null,根本就不会执行第二项的比较。

我们继续修改上面的代码,这次我们的目的是让程序找到null或者找到“Rose”的时候打印信息:

package com.mycompany.logicaldemo;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class LogicalDemo {
    static List<String> terms=new ArrayList<>(){//声明一个包含十个项的列表(其中一些为空)以及一个用于生成随机索引的方法
        {
            add("Rose");
            add(null);
            add("River");
            add("Clara");
            add("Vastra");
            add("Psi");
            add("Cas");
            add(null);
            add("Nardhole");
            add("Strax");
        }
    };

    public static void main(String[] args) {
        for(int i=0;i<20;++i)
        {
            int rnd=getRandomNumber();
            String term=terms.get(rnd);
            System.out.println("Generated index:"+rnd);
            if(term==null|term.equals("Rose")) //修改条件以达成目的
            {
                System.out.println("Rose was found");
            }
        }
    }
    private static int getRandomNumber()
    {
        Random r=new Random();
        return r.nextInt(10);
    }
}

如果我们运行上面的代码,使用|会抛出NullPointerException,因为该运算符要求判断两个表达式。因此,如果term为null,则调用.equals(...)会导致异常抛出。因此,为了确保代码按预期工作,须使用||替换|,它会快速跳过第二个条件的判断。因为如果第一个表达式为true,则第二个术语等于什么并不重要;结果始终为true。当然我们有时候可能会判断多个条件,

package com.mycompany.logicaldemo;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class LogicalDemo {
    static List<String> terms=new ArrayList<>(){
        {
            add("Rose");
            add(null);
            add("River");
            add("Clara");
            add("Vastra");
            add("Psi");
            add("Cas");
            add(null);
            add("Nardhole");
            add("Strax");
        }
    };

    public static void main(String[] args) {
        for(int i=0;i<20;++i)
        {
            int rnd=getRandomNumber();
            String term=terms.get(rnd);
            
            if(rnd==0||rnd==1||rnd<=3)
            {
                System.out.println(rnd+":this works");
            }
            if(rnd>3&&rnd<=6||rnd<3&&rnd>0)
            {
                System.out.println(rnd+":this works too...");
            }
        }
    }
    private static int getRandomNumber()
    {
        Random r=new Random();
        return r.nextInt(10);
    }
}

当然我们还是要尽可能地让条件简化,使得程序逻辑清晰,易读。

3.6 Elvis操作符

Elvis运算符是Java中唯一的三元运算符。它的功能是判断一个条件并根据判断结果返回一个值。这个操作符就是?,以下是Elvis操作符的模板。

expression1 ? expression2 : expression3

在这里,expression1可以是任何结果为布尔值的表达式。如果expression1为true,则计算expression2;否则,计算expression3。?操作的结果类型是所计算表达式的结果类型。expression2和expression3都必须返回相同(或兼容)类型,不能是void类型。

以下是使用?的示例:

ratio = denom == 0 ? 0 : num / denom;

当Java计算这个赋值表达式时,它首先查看问号左侧的表达式。如果denom等于零,则计算问号和冒号之间的表达式的值,并用其作为整个?表达式的结果。如果denom不等于零,则计算冒号后面的表达式,并将其用于整个?表达式的计算结果。然后,?运算符产生的结果被赋给ratio。

下面是一个演示?运算符的程序。它使用它来获取变量的绝对值。

package com.mycompany.ternary;

//?操作符的示例
public class Ternary {
    public static void main(String[] args) {
        int i,k;
        
        i=10;
        k=i<0?-i:i;//取i的绝对值
        System.out.println("i的绝对值为"+k);
        
        i=-10;
        k=i<0?-i:i;//同上,取i的绝对值
        System.out.println("i的绝对值为"+k);
        
    }
}

它的结果为:

i的绝对值为10
i的绝对值为10

3.7 Java操作符优先级

所有的数学运算都认为是从左向右运算的,Java语言中大部分运算符也是从左向右结合的,只有单目运算符、赋值运算符和三目运算符例外,其中,单目运算符、赋值运算符和三目运算符是从右向左结合的,也就是从右向左运算。

乘法和加法是两个可结合的运算,也就是说,这两个运算符左右两边的操作数可以互换位置而不会影响结果。运算符有不同的优先级,所谓优先级就是在表达式运算中的运算顺序。

一般而言,单目运算符优先级较高,赋值运算符优先级较低。算术运算符优先级较高,关系和逻辑运算符优先级较低。多数运算符具有左结合性,单目运算符、三目运算符、赋值运算符具有右结合性。

Java 语言中运算符的优先级共分为 14 级,其中 1 级最高,14 级最低。在同一个表达式中运算符优先级高的先执行。下面的表格 列出了所有的运算符的优先级以及结合性。

优先级运算符结合性
1()、[]、{}从左向右
2!、+、-、~、++、--从右向左
3*、/、%从左向右
4+、-从左向右
5«、»、>>>从左向右
6<、<=、>、>=、instanceof从左向右
7==、!=从左向右
8&从左向右
9从左向右
10从左向右
11&&从左向右
12从左向右
13?:从右向左
14=、+=、-=、*=、/=、&=、=、^=、~=、«=、»=、>>>=从右向左

括号可以提高括号内的操作优先级。这通常是必要的,以获得所需的结果。例如,考虑以下表达式:

a >> b + 3 

这个表达式首先将3加到b上,然后将a向右移动该结果。也就是说,这个表达式可以使用冗余的括号重写如下:

a >> (b + 3) 

然而,如果您想首先将a向右移动b个位置,然后将3加到该结果中,您需要像这样将表达式加括号:

(a >> b) + 3

除了更改运算符的正常优先级之外,括号有时可以用来帮助澄清表达式的含义。对于任何阅读您的代码的人来说,复杂的表达式可能很难理解。在复杂表达式中添加冗余但说明性的括号可以帮助防止后来的混淆。例如,以下哪个表达式更容易阅读?

a | 4 + c >> b & 7 (a | (((4 + c) >> b) & 7))

另一个要点是:括号(无论是否冗余)不会降低程序的性能。因此,添加括号以减少歧义不会对您的程序产生负面影响。

3.8 一些面试题

这里总结一些常见的面试题,如果现在还不是很理解,不要紧,先把本笔记过一遍再回来理解。

  1. Java中有哪些不同类型的操作符?它们的优先级如何?

答:Java中的操作符可以分为以下几类:

  • 赋值操作符
  • 算术操作符
  • 关系操作符
  • 逻辑操作符
  • 位运算符
  • 条件操作符
  • instanceof操作符
  • 递增和递减操作符

它们的优先级如下(从高到低):

  1. 递增和递减操作符
  2. 单目操作符(例如,取反符号)
  3. 算术操作符
  4. 移位操作符
  5. 关系操作符
  6. 相等操作符
  7. 位操作符
  8. 逻辑操作符
  9. 条件操作符
  10. 赋值操作符
  1. Java中什么是赋值操作符?如何使用它?

答:赋值操作符用于将一个值赋给一个变量。Java中的赋值操作符是等号(=),其语法如下:

variable = expression;

其中,variable是变量的名称,expression是要赋给变量的值。

  1. Java中的算术操作符有哪些?它们的优先级如何?

答:Java中的算术操作符包括加号(+)、减号(-)、乘号(*)、除号(/)和取模运算符(%)。它们的优先级如下(从高到低):

  1. 乘号(*)、除号(/)和取模运算符(%)
  2. 加号(+)和减号(-)
  1. Java中的逻辑操作符有哪些?它们的优先级如何?

答:Java中的逻辑操作符包括逻辑与(&&)、逻辑或(||)和逻辑非(!)。它们的优先级如下(从高到低):

  1. 逻辑非(!)
  2. 逻辑与(&&)
  3. 逻辑或(||)
  1. Java中的位运算符有哪些?它们的优先级如何?

答:Java中的位运算符包括按位与(&)、按位或(|)、按位异或(^)、左移(<<)、带符号右移(>>)和无符号右移(>>>)。它们的优先级如下(从高到低):

  1. 按位非(~)
  2. 左移(<<)、右移(>>)和无符号右移(>>>)
  3. 按位与(&)
  4. 按位异或(^)
  5. 按位或(|)