Java学习之路05---运算子

架构图

前言

表达式是程序进行算术运算中的表示方式,我们可以简单地把表达式拆解为表达式 = 运算子 + 操作变数,也就是说任何运算子都不能脱离变数单独存在

在java中依据使用功能可以区分成多种不同的运算子,该小节主要聚焦介绍几种常见的运算子,并探讨表达式中若存在多个不同运算子时该如何判断优先顺序

重点运算子

  1. 算术运算子(Arithmetic Operators)
  2. 赋值运算子(Assignment Operators)
  3. 关系运算子(Relational Operators)
  4. *三目运算子(Ternary Operators)
  5. 逻辑运算子(Logical Operators)

算数运算子

实现最基本的算术运算。与算术相关的运算子有+, -, *, /, %(加、减、乘、除、取余),整数类型与浮点类型多使用算术运算子进行数学运算,字串与字元则可以使用+进行拼接

案例

int num1 = 10;
int num2 = 20
int num3 = 0;	

num3 = 12 + num2;
num3 = num1 * 2;
num3 = num1 / 2;
num3 = num2 % 2;

使用括号
在进行运算时请铭记先乘除後加减法则,如果想要更改运算顺序可以透过括号来实现优先运算(优先级部分将会提及)

int num1 = 5;
int num2 = 2;
int num3 = 0;

num3 = num1 + num2 * 2; // 9
num3 = (num1 + num2) * 2; // 14

除法与取余结果
注意除法运算後的变数类型,例如以下例子,当运算结果为整数类型时,小数点会被无条件舍去,所以在进行相关运算时请特别注意。可以透过将转型使输出结果转为浮点类型

int num = 17;
int op1 = 3;
float op2 = 3.0f;

System.out.println("test 1 = " +num/op1); // 5
System.out.println("test 2 = " +num/op2); // 5.6666665

取余数运算相信大家一定不陌生,当除数具有小数点时,取余结果也会拥有小数点,当然浮点数运算多少会存在一些误差值,例如下面例子

float num1 = 17.0f;
float num2 = 17.9f;
float op1 = 3.0f;

System.out.println("test 1 = " +num1%op1); // 2.0
System.out.println("test 2 = " +num2%op1); // 2.8999996

可以在运算子左右两侧留下空白增加易读性

单目运算子

与上述我们介绍的表达式不同,加减乘除运算均需要两个或以上的变数参与,而单目运算子仅需要一个变数与一个运算子就可以进行运算

常见的单目算术运算子包括变数正负号表示、递增与递减运算:

/* 正负号表示 */
int num1 = -10;
int num2 = +15;

/* 自增与自减运算 */
System.out.println(num1++); // -10
System.out.println(--num2); // 14

常见的递增运算可以分为前置与後置两种,在操作上稍微有些不同
前置递增、递减
先进行递增与递减运算,然後回传变数值:

int num = 10;
int result = 0;

result = ++num;

System.out.println("result = " + result);
System.out.println("num = " + num);

运算结果:

result = 11
num = 11

後置递增、递减
先回传变数值,在进行递增或递减运算:

int num = 10;
int result = 0;

result = num--;

System.out.println("result = " + result);
System.out.println("num = " + num);

运算结果:

result = 10
num = 9

我们再来举几个小例子来练练手:

int a = 4;

a = (++a) + 4; // a = 5 + 4
System.out.println("a = "+a);

a = (a++) + 4; // a = 9 + 4
System.out.println("a = "+a);

a += (++a); // a = 13 + 14
System.out.println("a = "+a);

a -= (a--); // a = 27 - 27
System.out.println("a = "+a);

运算结果:

a = 9
a = 13
a = 27
a = 0

实际编写程序时应当尽量避免以下这种风格写法,也就是把大量赋值运算、算术运算、递增递减运算混合在一起。除了降低可读性外还可以造成赋值错误

int num = 10;
int i = 5;

/* 尽量避免使用这种风格的写法 */
num += i++;
num += ++i;
num = (num++) * (num++);
num = (++num) + (num++);

指定运算子

最常见的指定运算子莫过於=,它将等号右侧的数值赋值给等号左侧的变数

int num1;
char num2;
String num3;

num1 = 15;
num2 = 'a';
num3 = "string...";

特别注意赋值方向又由右向左,等号只有一个,且左值必须为一个变数,下面的例子是一些错误的写法:

100.0d = d;
num1 == 0;
num1 + num2 = 5;

复合指定运算子

很多时候赋值运算子会结合算术运算子使用,为了节省语句长度,我们可以将表达式简写成特殊的表达形式,例如:

  1. +=
  2. -=
  3. *=
  4. /=
  5. %=

这种表达式称作赋和指定运算子( Compound Statement),除了算术运算子之外,还可以结合位元运算子(之後的章节会提到)

int num1 = 10;
float num2 = 122.6.f;
double num3 = 1.0;

num3 += num1; // 1.0 + 10
num3 = 1.0;

num3 -= num1; // 1.0 - 10
num3 = 1.0;

num3 *= num2; // 1.0 * 122.6
num3 = 1.0;

num3 /= num2; // 1.0 / 122.6
num3 = 1.0;

num3 %= num1; // 1.0 % 10
num3 = 1.0;

双目运算子的赋值方向

在双目运算子的操作运算中,操作顺序始终是由左至右的,也就是说等号右侧的一连串运算一定是由左侧发起

int num1 = 3;
int num2 = (num1=4) * num1; // num1已经被改为4,所以执行运算为4 * 4

System.out.println(num2);

输出结果:

num2 = 16

同理复合指定运算子也适用这个规则

int num = 9;
num += (num=10); // 9 + 10
System.out.println("num = " + num);

num = (num=3) + num; // 3 + 3
System.out.println("num = " + num);

num -= (num=1); // 6 - 1
System.out.println("num = " + num);

输出结果:

num = 19
num = 6
num = 5

因此每次进行表达式运算时,都从等号右侧开始运算,运算规则始终是左至右,了解这个规则後,我们把将多个复合指定运算子结合起来看看输出效果如何

int num = 10;
num += num -= num *= num /= num;

/* 相当於 */
num = num + (num - (num * (num / num)));

输出结果:

num = 10

关系运算子

关系运算子多用在条件判断与循环语句中,例如if, while等,为了要使判断语句执行,必须判断执行或不执行,因此可以得知关系运算子的输出结果不是true就是false

int num = 10;

if(num < 100)
	System.out.println(num < 100);
else
	System.out.println(num < 100);

输出结果:

true

java提供多组关系运算子给条件语句进行判断

  1. >
  2. <
  3. >=
  4. <=
  5. ==
  6. !=

另外关系运算子的判断不限於整数型态,例如下面例子就是整数与浮点数关系运算子范例,只要两个变数值相同就会返回true

int num1 = 10;
float num2 = 10.0f;

if(num1 == num2){
	System.out.println(num1 == num2);
}
else{
	System.out.println(num1 == num2);
}

输出结果:

true

除此之外字元也是一个常见的判断变数,字元形式主要利用ASCII码进判断

char ch = 'a'; // a ASCII码为97

if(ch >= 100)
	System.out.println(ch > 100);
else
	System.out.println(ch > 100);

输出结果:

false

另外布林、字串字面值也可以进行判断,不过仅限於==与!=

System.out.println(10 <= 100);

System.out.println(true == false);
System.out.println("123" != "124");

输出结果:

true
false
true

三目运算子

所谓三目运算子,是指条件式与计算返回值共有三个不同的区块。java提供的三目运算子解析如下:
条件判断 ? 成立返回值 : 失败返回值
举几个例子比较好懂:

int num1=6, num2=9;
boolean b = num1 > num2 ? (13 > 6) : (true == false);

System.out.println(b);

输出结果:

false

首先判断条件语句num1 > num2是否成立,若成立则返回第一个区块的值,也就是(13 > 6),若不成立则返回第二个区块的值(true == false)

假如我们把三目运算子写成if-else判断式的话,就可以看出三目运算子的优势在於简洁性了。不过若是牵涉到多条判断语句(if-else if-else)还是要乖乖地使用判断式

int num1=6, num2=9;
boolean b=false;

if(num1 > num2)
	b = (13 > 6);
else
	b = (true == false);

System.out.println(b);

接下来我们把三目运算子的返回值双双填入另一个三目运算子,让判断式多加一层

int num1 = 18, num2 = 44, num3 = 90;
int max;

max = ((num1 > num2) ? (num1 > num3) ? num1 : num3 : (num2 > num3) ? num2 : num3);

System.out.println("最大值是: " + max);

输出结果:

最大值是: 90

首先判断num1 > num2是否成立,若成立则只需要再判断num1 > num3就可以;相反的若是不成立,则需要考虑num2 > num3是否成立

逻辑运算子

前一小节中我们有提到关系运算子与判断语句的关系,如果关系运算成立就执行该条语句,但假如需要进行判断的关系运算语句超过一条,就需要使用逻辑运算子

java主要提供三种逻辑运算子,分别是AND, OR, NOT运算:

  1. &
  2. |
  3. !
int num1 = 10;
int num2 = 101;

if(num1 > 5 & num2 > 100)
	System.out.println(num1 >5 & num2 > 100); // true AND ture = true

if(num1 > 10 | num2 > 100) // false OR true = true
	System.out.println(num1 > 10 | num2 > 100);

if(!(num1 > 10) && !(num2 < 100)) // NOT(false) AND NOT(false) = true
	System.out.println(!(num1 > 10) && !(num2 < 100));

输出结果:

true
true
true

使用短路运算子提高效率

短路运算子的功能与普通的逻辑运算子相同,它的表示方式为&&, ||,都是AND, OR操作,差别在於它不用执行所有的判断语句。

举例来说,判断式A在执行判断时,遇到第一个关系判断式为false後就不会再执行(7 > 3)的判断了,因为不管无论false如何进行AND运算,结果始终为false

判断式B也是相同的道理,当(4 < 5)为true後,不管接下来为false或为true返回值始终为true,所以不需要做额外的判断

int num = 0;

if((4 > 5) && (7 > 3)){ // A
	num++;
}

if((4 < 5) || (5 < 6)){ // B
	num++;
}

为了验证这个运算结果,我们使用普通的逻辑运算子与短路运算子进行比对,发现使用短路运算子确实省略掉判断式

int i = 5;

if((2*i > 5) | (++i > 2))
	System.out.println("i = " + i);

i = 5;

if((2*i > 5) || (++i > 2))
	System.out.println("i = " + i);

输出结果:

i = 6
i = 5

运算子优先级

假如有多条运算子存在一条表达式中,编译器必须要有一个规则可以区分出谁先做谁後做,例如我们举一个简单的运算表达式为例子,很明显的奉行先成除後加减的规定

double x=1,y=2,z=3;
double d = 2 * x * y + z - 1 / x + z % 2; // 4 + 3 - 1 + 1

System.out.println("d = " + d);

输出结果:

d = 7.0

但除了普通的算术运算子之外,java还规定了我们上述提及的运算子优先顺序,例如常见的指定运算子与逻辑运算子等,下表介绍常见运算子的执行优先顺序

优先级 结合顺序 类型
++, -- 右到左 算术运算子
+(正号), -(负号), ! 右到左 算术运算子
*, % 左到右 算术运算子
+, - 左到右 算术运算子
>, <, >=, <= 左到右 关系运算子
==, != 左到右 关系运算子
& 左到右 逻辑运算子
| 左到右 逻辑运算子
&& 左到右 逻辑运算子
|| 左到右 逻辑运算子
?: 右到左 三目运算子(条件运算子)
=, +=, -=, *=, /=, %= 右到左 复合指定运算子

结合顺序
所谓结合顺序就是运算子会优先向左或右与操作变数结合的特性。例如负号具有由左到右的结合特性a = 4 + - 5,会先向右侧寻找可结合的操作数(常数5)。同理a++也是先向右侧寻找操作数,没有的话向左侧寻找(因为单目运算子的特性)

养成使用小括号习惯
虽然我们知道x + y * 2 - 1的执行顺序,但若加上小括号x + (y * 2) - 1可以让其他开发者更快读懂程序码

运算运算与优先级问题

优先级让人头痛的地方就是该如何在一连串运算式抓出谁该先算,谁又该慢点。尤其是递增递减运算子的前後置最容易搞混

其实根据官方文件递增递减运算子的优先级,後置(post-fix)是高於前置(pre-fix)。下面我们举个例子:

int num = 5;

System.out.println(num++ * ++num * num++);

输出结果

245

其实很多人会将优先级高的运算子解释成先行运算,也就是说他们认为运算顺序应该是:
num++ -> num++ -> ++num
其实先行运算这个观念没有错,不过前提是该操作变数的前後范围内,而不会直接就找最高优先级的操作数进行运算。这间接带出一个观念,在选择优先级之前首先要确认整个运算式由左至右的,可以从官方文件中查到这个规则。

说白了就是你要先从左到右进行运算操作,遇到前後均有运算子时才可以进行优先级判断,而不是直接跳去执行优先级最高的运算式,你可以想像优先级主要是为了处理下面这种场景

int num1 = 5;
int num2 = 6;
int num3 = 1;
int result = 0;

result = num1 * ++num2 * num3++;
result = ((num1 * (++num2)) * (num3++)); // 解释成这样

跳回刚刚的例子,若原本的假设成立,编译器真的会一开始就直接执行优先级高的运算子,而忽略由左到右的这条规则的话,下面这个例子的输出应该会相等

int num = 5;

System.out.println("test1 = " + (num++ * ++num));

num = 5;

System.out.println("test2 = " + (++num * num++));

输出结果:

test1 = 35
test2 = 36

看到了吧,输出结果证明原本的假设不成立,但假如我们考虑到由左到右运算的这条规则时,一切就说得通了

test1的运算顺序:

  1. num++得到返回值5
  2. ++num得到返回值7
  3. 两个操作变数相乘得到35

test2的运算顺序

  1. ++num得到返回值6
  2. num++得到返回值6
  3. 两个操作变数相乘得到36

最後回到原先的例子: num++ * ++num * num++

  1. num++得到返回值5
  2. ++num得到返回值7
  3. 两个操作变数相乘得到35
  4. num++得到返回值7
  5. 两个操作变数相乘得到245

<<:  Python 学习笔记_装饰器(decorator) 与重试(retry)

>>:  Python & GCP 学习笔记_Gmail API 操作

Day-04 如何将APP安装到手机上

在昨天的内容我们认识了Android模拟器,模拟器固然方便,但是或许会碰上模拟器可执行、而装置却不支...

Day.21 「物件也有继承问题?」 —— JavaScript 继承 与 原型链

我们每新增一个函式,浏览器都会向函式内新增一个属性叫 prototype function Per...

[Day 26] 专案执行(下)

接续上一章 规划阶段 专案工作分解 将工作分成小项目,以便分工、时间规划 目标分解结构 OBS (o...

Day22 - 针对 Metasploitable 3 进行渗透测试(3) - Msfvenom 与 multi/handler

复习 Revershell:在受害主机启动连线 shell,连接回攻击主机(会预先监听 port)...

【Day 3】机器学习基本功(一)

机器学习三大步骤 定义一个模型(model) 从模型里挑出好的函式(function) 经由演算法找...