【JAVA进化论】LV1-3:java中的运算符

一、运算符

1.1:算术运算符

算术运算符主要包含:

运算符 说明 示例 备注
+ 加法 a + b 计算变量间的和
- 减法 a - b 计算变量间的差
* 乘法 a * b 计算变量间的积
/ 除法 a / b 计算变量间的商
% 取余 a % b 计算变量间的余
++ 自增1 a++++a 等效于:a = a + 1; 或者 a += 1;(+=在下面赋值运算符会介绍)++放变量前面和后面,是有区别的,具体请参考第二部分的例3
自减1 a----a 等效于:a = a - 1; 或者 a-=1;(-=在下面赋值运算符会介绍)跟++一样,也有前后之分。
表1

1.2:位运算符

位运算符包含:

运算符 说明 示例 特性
& 与运算 a & b 1&1=11&0=00&1=00&0=0
| 或运算 a | b `1
~ 取反 ~a ~1=0~0=1
^ 异或运算 a ^ b 1^1=01^0=10^1=10^0=0
>>> 无符号右移 a >>> b 参考注1
>> 右移运算 a >> b 参考注1
<< 左移运算 a << b 参考注1
表2

⚜️ 注1:

关于位运算的一切,请参考:JAVA有关位运算的全套梳理

1.3:赋值运算符

赋值运算符主要包含:

运算符 说明 示例 备注
= 赋值 a = 1 让等号左边的值等于右边的值,这在之前的示例代码里已经体现过很多次了,简单一个变量声明的初始化值都需要用到赋值运算符:int a = 1;
+= 做加法操作后并且赋值给自身 a += 2 等效于:a = a + 2这种等式一般是从右往左执行,所以该表达式的意思是:先让a自身加上2,然后再把这个结果重新赋值给a。
-= 做减法操作后并且赋值给自身 a -= 2 同上,只不过换成减法
*= 做乘法操作后并且赋值给自身 a *= 2 同上,只不过换成乘法
/= 做除法操作后并且赋值给自身 a s/= 2 同上,只不过换成除法
%= 做取余运算并且赋值给自身 a %= 2 同上,只不过换成取余运算
<<= 做左移运算并且赋值给自身 a <<= 2 等同于:a = a << 2
>>= 做右移运算并且赋值给自身 a >>= 2 等同于:a = a >> 2
>>>= 做无符号左移运算并且赋值给自身 a >>>= 2 等同于:a = a >>>2
&= 做与运算并且赋值给自身 a &= 2 等同于:a = a & 2
|= 做或运算并且赋值给自身 `a = 2`
^= 做异或运算并且赋值给自身 a ^= 2 等同于:a = a ^ 2
表3

1.4:关系运算符

关系运算符用来运算两个值的大小、是否相等等关系,经过关系运算符运算后的两个值的结果一定是boolean类型的,结果只有真或假。

关系运算符主要包含:

运算符 说明 示例 备注
> 大于 a > b 参考注2
>= 大于等于 a >= b 同上,判断关系为大于等于
<= 小于等于 a <= b 同上,判断关系为小于等于
< 小于 a < b 同上,只不过判断关系为小于
== 等于 a == b 参考注3
!= 不等于 a != b 参考注4
表4

⚜️ 注2:

用于判断两个值大小,需要注意的是,比较大小的关系运算符无法用来比较大部分的引用变量,因此不存在cat1 > cat2这种代码。

代码块1
1
2
3
char a = '我';
int b = 25105;
boolean d = a > b;

上面结果为false

⚜️ 注3:

用来判断两个变量是否相等:

代码块2
1
2
3
4
5
6
7
//声明a、b、c,并为它们赋初始值
int a = 1;
int b = 1;
int c = 2;
//运算符==返回的是布尔型数据,因为其判定结果只有真或假两种状态
boolean d = a == b;
boolean e = a == c;

如上代码,d的结果为true,e的结果为false

简单的基本类型,判等方式是很简单的,值一样,就相等,哪怕类型不一致:

代码块3
1
2
3
char a = '我';
int b = 25105;
boolean d = a == b;

d的结果值依然是true,因为汉字“我”对应的Unicode码值就是25105。

通过上面两个例子,可以发现基本类型的判等很单纯,就是看数值是否相等而已。

⚠️下面的内容建议看完LV3-1后再来阅读

既然==是用来为变量判等的,那它是否也能用来判断复杂的引用变量是否相等呢?

现在让我们回忆一下LV3-1里的那只猫,通过那只猫的例子,我们知道了引用变量其实是一个保存了某个内存地址的变量,它并不是一个实际的对象,它只是保存了实际对象的地址,方便通过它在内存中访问到那块对象数据,我们还知道,当一个类利用其构造方法被new出来的时候,它就产生了一个对象,对象就会存在一块内存区域内,此时对象自然就有了对应的内存地址,而将该地址赋值给对应的引用变量,这样就可以通过此引用变量访问具体的对象内容了,如果还是不太熟悉这个过程,请再次翻看LV1-2中的图7

现在我们仍然用那个猫类来说明问题:

代码块4
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
int gender = 1;
int age = 5;
int color = 1;
String name = "加菲";

Cat cat1 = new Cat(gender, age, color, name);
Cat cat2 = new Cat(gender, age, color, name);

boolean result = cat1 == cat2;
System.out.println(result);
}

由上面的代码可知,我们造出来了两只猫,它们的各项属性全部一致,那么现在请思考一下,引用变量cat1和引用变量cat2是否相等呢?上面的结果应该是true还是false?

输出结果为false。

为什么呢?它们明明什么都一样!!回忆一下我们在上一节有关引用变量的解释,引用变量保存的是内存地址,而我们这个例子中,造出了两只猫,尽管它们啥都一致,但事实上,这种一致你可以理解成“巧合”,碰巧有两只名字都叫加菲的5岁黑色公猫,

所以实际上它们是不同的两个对象,它们在地球上所处的位置不可能穿透对方的身体重合存在,抽象到计算机层面,它们需要存储在不同的存储单元内,存储单元不同,自然存储地址不同,那么cat1和cat2又怎么会相等呢。

那么现在再思考下,如何让cat1和cat2相等?

现在让我们改造下上面的代码:

代码块5
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
int gender = 1;
int age = 5;
int color = 1;
String name = "加菲";

Cat cat1 = new Cat(gender, age, color, name);
Cat cat2 = cat1; //这里把cat1直接赋值给cat2

boolean result = cat1 == cat2;
System.out.println(result);
}

这时结果就为true,这里应该都理解为什么了,因为cat1本身是引用变量,保存的是new出来那个对象的内存地址,现在直接把它的值赋值给cat2,因此cat2里保存的地址跟cat1中是一样的,所以它们一定是相等的。

⚜️ 注4:

==性质一样,只不过它用来表示不等于,运算方式跟==相反

所以:

代码块6
1
2
3
char a = '我';
int b = 25105;
boolean d = a != b;

这个例子把==换成!=后,输出结果就为false了,引用变量就不举例了。

1.5:逻辑运算符

讲完了关系运算符,现在再来认识下逻辑运算符,逻辑运算符主要用来运算两个或多个布尔值间的真假,因此逻辑运算符两边连接的变量必须是boolean类型的数据。

运算符 说明 示例 备注
& 与运算 a & b 参考注5
| 或运算 `a b`
^ 异或 a ^ b 同上,只是逻辑同“异或”
! !a 参考注6
&& 短路与 a && b 参考注7
|| 短路或 `a
表5

⚜️ 注5:

没错,又是这个符号,我们在1.2里的位运算符里已经碰到过了,只不过1.2里它连接两个非boolean类型的变量,用来表示两个变量的二进制数的与运算,

这里它被用来连接两个boolean类型的变量时,由于boolean类型的数据仅有真和假两种状态,因此它仅用来做简单的与门转换:

代码块7
1
2
3
4
5
6
boolean a = true;
boolean b = false;

boolean c = a & b; //a和b相与

System.out.println(c);

结果为false,因为true & false = false

结合1.4里的关系运算符,可以知道任意关系运算符连接的式子,其结果一定是一个boolean值,那么我们可以写更为复杂的相与代码:

代码块8
1
2
3
4
5
6
7
8
9
int a = 1;
int b = 1;

int c = 5;
int d = 6;

boolean r = (a == b) & (c != d);

System.out.println(r);

结果为true,因为使用关系运算符==连接的a、b结果为true,而用!=连接的c、d的结果也为true,那么最终符号&连接的boolean值就被简化成了:true & true

结果自然是true,利用这种特性我们后续在java程序里可以做很多逻辑判定,比如我们之前举得Cat类里的run方法:

代码块9
1
2
3
4
5
6
7
public void run() {
if (age > 10) { //这里就是一个逻辑判定,利用关系运算符>连接两个数,当age大于10的时候条件成立为true,输出10km/h,否则输出下面的15km/h
System.out.println("奔跑速度:10km/h");
} else {
System.out.println("奔跑速度:15km/h");
}
}

⚜️ 注6:

类似位运算里的按位取反~,即让自己的结果值反过来,例如:

代码块10
1
2
3
4
5
6
int a = 1;
int b = 1;

boolean c = !(a == b);

System.out.println(c);

a == b这个关系语句的结果肯定是true,但是它在外层加了!,意味着结果要跟实际值反过来,因此c的结果为false。

⚜️ 注7:

相比&,短路与更加“智能”,如果第一个条件不成立,则直接返回结果,这么说不容易理解,下面通过一个例子来说明:

代码块11
1
2
3
4
5
6
7
8
9
10
11
int a = 1;
int b = 1;

int c = 5;
int d = 6;

boolean r1 = (a != b) & (c != d);
boolean r2 = (a != b) && (c != d);

System.out.println(r1);
System.out.println(r2);

上面r1和r2的执行结果是一致的,都是false,但执行的方式却不一样:

图1

上述就是短路与和正常与参与运算时的区别,但是如果第一个条件成立(true),仍然需要进行后续条件的判断,这时与&无异,但短路与总是尽可能的减少关系运算的次数。

一般建议使用短路与做与逻辑运算。

1.6:三元运算符

通过对上面关系运算符的了解后,现在来了解一种特殊的运算符:三元运算符(xx ? yy : zz)。

三元运算符会结合赋值运算符和关系运算符合计使用,旨在挑选符合自己预期的值(其实跟后面要讲的条件语句if/else有些像)。

现在来简单了解下其结构:

图2

上图展示了一个三元表达式以及其组成部分。

最终s的值为11;

我们还可以嵌套三元表达式:

代码块12
1
2
3
4
5
6
7
//声明变量&赋值
int a = 1;
int b = 2;
int c = 5;
int d = 6;

int s = (a > b) ? 10 : ((c < d) ? 66 : 77);

首先判断a>b是否成立,由a和b的值可知是不成立的,所以会跳过10这个结果,选取冒号后面的值,而冒号后面仍然是一个三元表达式,这个三元表达式的值为66,因此s最终等于66。理论上java允许你无限套娃,但为保证代码可读性,不建议嵌套三元表达式

二、运算符间的优先级

2.1:运算顺序猜想

我们在之前的示例代码里经常会看到类似下面这样的代码:

代码块13
1
2
3
int a = 1; //声明变量a,并通过赋值运算符为其赋值
int b = 2; //声明变量b,并通过赋值运算符为其赋值
int c = a + b; //声明变量c,然后通过赋值运算符为其赋值,值为a和b通过算术运算符”+“运算后的结果值

那么类似int c = a + b;这种语句,它的执行顺序为什么不是直接给c通过”=“运算符赋上a的值,然后再计算a和b的”+“运算呢?即:

图3

如图,如果我们把int c = a + b;这种语句按照上图的拆分方式进行运算,那么这段代码的最终运行结果将是c等于a的值,其余无事发生,然而经过前面那么多示例,我们知道这是不对的,c的值应该等于a+b的运行结果才对,那么这就意味着加法运算发生在赋值运算之前,即:

图4

真正的执行过程应该是图4中的过程,这样的结果似乎在告诉我们,运算符之间存在着某种等级,等级高的先执行,等级低的往后稍稍,这就是运算符之间的执行优先级,通过我们对int c = a + b;这种语句的了解,我们至少已经知道了”+“应该发生在”=“之前,即算术运算符应发生在赋值运算符之前执行。

2.2:运算符的优先级列表

那么我们现在就来列一下所有运算符的执行优先级:

优先级(从高至低排序) 运算符 结合性
1 ( ) [ ]  . 左→右
2 ! ~  ++  – 右→左
3 * / % 左→右
4 + - 左→右
5 << >> >>> 左→右
6 < <= > >= instanceof 左→右
7 == != 左→右
8 & 左→右
9 ^ 左→右
10 | 左→右
11 && 左→右
12 || 左→右
13 ? : 右→左
14 = += -= *= /= &= |= ^= ~= <<= >>= >>>= 右→左
表6

2.3:实例

结合上面的表,我们通过几个例子来固化一下我们的记忆:

例1:括号的运算等级是最高的

代码块14
1
2
3
int a = 1;
int b = (a + 1) * 2; //这里由于括号运算符的优先级高于一切,所以括号内的内容a+1先被执行
int c = (a + b) + (22 + 5); //如果有多个括号,则依据"结合性"可知,应该由左至右执行,因此这里的执行顺序为先执行a+b,再执行22+5

这个符合数学里的运算规则,例如:

乘除法的优先级高于加减法,所以类似“int c = a + b * 2”,首先计算的是b2的值,然后再拿着b2的结果跟a相加,但是如果改成“int c = (a + b) *2”的话,就变成先执行括号内的加法运算,然后拿着和去和2相乘。

例2:括号、非运算、与运算、或运算结合

普通非运算+括号:

代码块15
1
2
3
4
//非
int a = 1;
int b = 2;
boolean s = !(a < b); // 按照优先级,括号内的先运算,a<b结果为true,然后再运算仅次于括号的运算符!,最后赋值给s,s的值为false

嵌套非运算:

代码块16
1
2
3
4
5
6
int a = 1;
int b = 2;

boolean s = !!(a < b);
// ↑首先计算优先级高的括号内的值,为true,然后同级非运算的结合性按照从右到左,因此右侧的"!"首先参与运算,
// 结果为false,然后拿着结果再用最外侧的"!"参与运算,结果为true

括号、嵌套非运算、与、或运算结合:

代码块17
1
2
3
4
5
6
int a = 1;
int b = 2;
boolean c = false;
boolean d = true;
boolean e = true;
boolean s = !!(a < b) & c & d | !!!e;

这个看起来很复杂,我们借助流程图来梳理下:

图5

例3:i++、++i问题

表6可知++运算符优先级仅次于括号。

类似这种++的方式,有两种写法:a++和++a

++位于变量的前面和后面最终变量的结果都是一样的:

代码块18
1
2
3
4
5
6
7
8
9
//声明变量a和b,它们的值都是1
int a = 1;
int b = 1;

a++; //a自增1,++放其后面
++b; //b自增1,++放其前面

System.out.println(a);
System.out.println(b);

最终输出结果都是2,那么++放前面和放后面有何区别呢?

代码块19
1
2
3
4
5
6
7
8
9
10
11
12
//声明变量a和b,它们的值都是1
int a = 1;
int b = 1;

int c = a++; //a自增1,++放其后面,将a的值赋值给c
int d = ++b; //b自增1,++放其前面,将b的值赋值给d

System.out.println(a);
System.out.println(b);

System.out.println(c);
System.out.println(d);

上面输出结果,a和b自然还是等于2,但是c和d却不相同:

c最终等于1,d最终等于2.

首先我们知道++操作相当于变量自增,a++相当于a = a+1,所以类似这种语句:

int c = a++;

相当于有两个赋值运算符:

图6

先记住这个流程,下面来看下++放到变量后面和前面的区别:

图7

所以 int c = a ++;这段代码是先执行的赋值运算符,因此c的值等于还没完成自增前的a。相反的,如果++放到自增变量前面,则意味着它的执行优先级高于赋值运算符,所以 int c = ++ a;这段代码是先执行++操作,然后再将++后的值赋值给c。

❓ 思考:请问下方的a、b、c的最终值分别等于多少?

代码块20
1
2
3
4
5
int a = 1;
int b = a++ + 1;
int c = ++b + 1;
System.out.println(b);
System.out.println(c);

答案:a=2,b=3,c=4

三、运算符下的自动转型

我们通过LV-2可以知道基本类型是会区分按照取值范围来区分大小的,我们再次把它们的大小等级贴出来:

图8

我们还知道了小类型不可以接收大类型的数据,如必须要接收,需要做强转,而如果使用一个大类型接一个小类型的值,则这个小类型的值会自动转换成对应的大类型,这种类型的转换叫类型自动转换,贴一下当时的例子。

例1:

代码块21
1
2
int α = 1; //声明int型的变量α,值为1
long β = α; //声明long型的变量β,使其值等于α的值,由于long型表示范围比int型大,因此α的值不需要做类型强转,但其实是α的值类型已经自动转成long了

现在结合本节的运算符知识再来看下这个类型自动转换是如何发生的:

首先声明的α是一个int型,然后使用大类型的β接收,此时α的值(也就是1)已经自动转成了long型,这就是运算符导致了类型的转换,在本例中,=就是这个运算符(赋值运算符),因为α的值被赋给了β,因此α值的类型自动提升为long。

结论:运算符可以导致某些数据变量值的类型转换。

例2:

代码块22
1
2
3
int a = 1;
float b = 1;
int s = a + b; //这是不允许的,经过加法运算,a的值已经被自动提升为float型,因此a+b的值是一个float型,较小的int是不能接收较大的float型的

上述注释内的内容用下图来描述:

图9

其实最终都要回归到强制类型转换那一部分内容来。

现在改一下代码,解决上述问题:

代码块23
1
2
3
4
5
6
int a = 1;
float b = 1;

float s = a + b; //这种改法就是直接用大类型接收
或:
int s = a + (int)b; //这种改法就是将大类型强转成小类型,这样它们经过加法运算后类型不会提升,还是int

参考注释,最终还是回归到要么用大类型接收小类型,要么强转的解决方法上来,针对例1,我们可以说是“=”运算符让变量α的值类型提升,而在本例,我们可以说是“+”运算符让a的值类型得到提升。

为了巩固这块的知识,再让我们改造下上面的代码:

代码块24
1
2
3
4
5
6
int a = 5;
int b = 2;

float s = a / b;

System.out.println(s);

这个结果是2.5吗?答案并不是,最终结果是2.

为什么??明明5/2是2.5啊,这里结合前面的例子,你只需要稍微思考一下就可以得到答案:

图10

❓ 思考:那如何才能得到自己想要的2.5呢?

答案:很简单,只需要提升a+b的类型就行了,即:(float)a+b 或 a+(float)b,当然,两个都强转也行:(float)a+(float)b,这样在上图①处在除法运算时就会将a/b的结果提升为float,自然就不会失精。