符合一定语法规则的运算符和操作数的序列被称为表达式,一个常量、变量也可以认为是一个表达式,该常量或变量的值即为该表达式的值。在使用表达式的时候,我们经常会碰到类型转换,所以很有必要深入研究一下!
◆ JShell 的使用
在介绍类型转换及运算符之前,我们不妨来了解一项JDK9的新特性 JShell ,顾名思义,它允许我们在不想写一些类及方法的时候,仅通过dos窗口,模拟脚本进行一些简单代码的编译即运行动作,类似脚本代码一样,快速获得需要的反馈。
只需在打开的JShell 命令中,输入 jshell
就可以类似 linux 命令一样,键入简洁的 java 代码了。
jshell
C:\Users\Coder>jshell
| 欢迎使用 JShell -- 版本 11.0.6
| 要大致了解该版本, 请键入: /help intro
jshell> int a=1; int b=2; System.out.println(a+b);
a ==> 1
b ==> 2
3
jshell> int a = 1.5;
| 错误:
| 不兼容的类型: 从double转换到int可能会有损失
| int a = 1.5;
| ^-^
jshell> byte b1=1;byte b2=2;byte b3=1+2;byte b4=b1+b2;
b1 ==> 1
b2 ==> 2
b3 ==> 3
| 错误:
| 不兼容的类型: 从int转换到byte可能会有损失
| byte b4=b1+b2;
| ^---^
jshell>
◆ 类型转换
一个合法的表达式在经过计算后应该具有明确的类型和值。表达式在使用过程中,当其所处的上下文要求的类型与表达式类型不一致时就会发生类型转换。
1. 自动类型转换
自动类型转换又被称作隐式转换或者宽转换,它一般发生在数值类型之间。当两个数值进行二元操作时,先要将两个操作数转换为同一种类型,然后才可以进行计算
- 如果两个操作数中有一个是 double 类型, 另一个操作数就会转换为 double 类型
- 否则,如果其中一个操作数是 float 类型,另一个操作数将会转换为 float 类型
- 否则, 如果其中一个操作数是 long 类型, 另一个操作数将会转换为 long 类型
- 否则, 两个操作数都将被转换为 int 类型
由此我们一般可以推出这些转换规则:
- byte 可自动转换成 short 、int 、long 、float 、double
- short 可自动转换成 int 、long 、float 、double
- char 可自动转换成 int 、long 、float 、double
- int 可自动转换成 long 、float 、double
- long 可自动转换成 float 、double
- float 可自动转换成 double
注:
- byte 、short 、int 类型都是有符号数,在发生自动类型转换时会进行符号位扩展,也就是说无论转换成什么类型,它始终保持最高位与原符号位相同,其他位由 0 补上
- char 类型实际上是 0 ~ 65535 的无符号数,可以认为它的符号位为 0 ,发生转换时,直接用 0 进行扩展
- int 转换成 float ,long 转换成 double 以及 long 转换成 float 时,都很有可能发生精度丢失
//DemoDataType.java
class DemoDataType
{
public static void main(String[] args)
{
int aInt;
float aFloat;
aInt = 1234567891;
System.out.println(aInt);
aFloat = aInt;
System.out.println(aFloat);
}
}
2. 强制类型转换
强制类型转换又被称作显式转换或者窄转换,它是沿着箭头的反方向进行的类型转换,一般这种转换都伴随着精度的损失。它的转换规则如下:
- short 可直接转换成 byte 、char
- char 可直接转换成 byte 、short
- int 可直接转换成 byte 、shot 、char
- long 可直接转换成 byte 、short 、char 、int
- float 可直接转换成 byte 、short 、char 、int 、long
- double 可直接转换成 byte 、short 、char 、int 、long 、float
注:
- 强制类型转换通常伴随着信息的丢失,因此大多数的转换都要求我们显式地声明
//DemoDataType.java
class DemoDataType
{
public static void main(String[] args)
{
int aInt;
char aChar;
byte aByte;
aChar = 'a';
System.out.println(aChar);
aInt = (int) aChar;
System.out.println(aInt);
aByte = (byte) 300;
System.out.println(aByte);
}
}
- char 类型转换成 short 类型可能会由正数变成负数
- 如果被转换类型超出了目标类型的表示范围,则会改变符号或者发生截断,如 (byte)129 的实际值是 -127 ,这是因为在计算机中所有的数据都用二进制补码来存储,byte 类型占据 1 个字节,即 8 个比特位,所能存储数据的取值范围为 -128 ~ 127 ,二进制补码表示为 1000_0000 ~ 0111_1111 ,而 129 的二进制补码为 0_1000_0001 ,当它因为强转放入存储单元后,只保留最后 8 位,即 1000_0001 ,转换成二进制原码即 1111_1111 ,十进制打印输出即 -127 ,至于进制转换,原码补码的换算,应该不用我再过多赘述了
- byte 类型转换成 char 类型时,会先发生 byte 到 int 的自动类型转换,然后再发生 int 到 char 的强制类型转换
◆ 运算符
Java 中用于运算的各种符号称为运算符,参与运算的对象称为操作数,根据操作数的多少,运算符被分为单目运算符、双目运算符和三目运算符,也被称作一元运算符、二元运算符和三元运算符,但我接下来的介绍却是按照运算符的作用来进行的……
1. 算术运算符
算术运算符用于处理整型、浮点型字符型的数据,进行算术运算。
运算符 | 名称 | 目数 | 结合性 |
---|---|---|---|
+ | 加法运算\字符串连接\正号 | 双目\双目\单目 | 从左向右\从左向右\从右向左 |
- | 减法运算\负号 | 双目\单目 | 从左向右\从右向左 |
* | 乘法运算 | 双目 | 从左向右 |
/ | 除法运算 | 双目 | 从左向右 |
% | 取模运算 | 双目 | 从左向右 |
++ | 自增运算 | 单目 | 从左向右\从右向左 |
– | 自减运算 | 单目 | 从左向右\从右向左 |
加减乘就像我们熟悉的数学四则运算一样
//Demo.java
class Demo
{
public static void main(String[] args)
{
int x=4, y=5;
System.out.println("x=5; y=4; z=12");
System.out.println("x+y = " + (x + y));
System.out.println("x-y = " + (x - y));
System.out.println("x*y = " + (x * y));
}
}
加法运算符还被重载为了字符串连接符,它可以将不同的字符串连接为一个,也可以将不同的输出语句合并为一条
//Demo.java
class Demo
{
public static void main(String[] args)
{
int xInt = 2019;
String str1 = "Hello ";
String str2 = "world!";
//合并两条输出语句
System.out.println(str1);
System.out.println(str2);
System.out.println(str1 + str2);
//合并两条输出语句
System.out.println(str1);
System.out.println(xInt);
System.out.println(str1 + xInt);
//连接两个字符串,然后输出
str1 = str1 + str2;
System.out.println(str1);
}
}
只有整型数据参与运算的表达式得到的结果只能是整型,只有一个操作数是浮点数,则表达式的结果类型为浮点型,这主要体现于 / 运算
//Demo.java
class Demo
{
public static void main(String[] args)
{
int xInt;
int yInt;
int zInt;
double xDouble;
double yDouble;
double zDouble;
xInt = 3;
yInt = 5;
zInt = 6;
xDouble = 3.0;
yDouble = 5.0;
zDouble = 6.0;
//只有整型变量参与的除法运算
System.out.println("zInt / xInt = "+zInt / xInt);
System.out.println("yInt / xInt = "+yInt / xInt);
//只有浮点型变量参与的除法运算
System.out.println("zDouble / xDouble = "+zDouble / xDouble);
System.out.println("yDouble / xDouble = "+yDouble / xDouble);
//有一个操作数是浮点型的除法运算
System.out.println("yDouble / xInt = "+yDouble / xInt);
System.out.println("yInt / xDouble = "+yInt / xDouble);
//临时转换整型变量数据类型的除法运算
System.out.println("yInt * 1.0 / xInt = "+yInt * 1.0 / xInt);
System.out.println("yInt / xInt * 1.0 = "+yInt / xInt * 1.0);
System.out.println("yInt / (xInt * 1.0) = "+yInt / (xInt * 1.0));
}
}
- 第 23 行输出的结果为 1 ,这是因为整型数据之间的除法只能得到整数,如果没办法整除,并非像自然计算一样进行四舍五入,而是向下取整,即不小于精确值的最大整数
- 如果想要两个整型数据进行除法运算可以得到较为精确的结果,我们可以选择让其中一个操作数与 1.0 相乘,进而自动类型转换成浮点型,见第 34 行
- 第 35 行输出结果为 1.0 ,这是因为运算符的优先级问题,从左至右进行结合,会先运算 yInt/xInt ,然后将结果与 1.0 相乘,所以需要加上括号提升优先级,见第 36 行
自增和自减运算符有着相同的特性,这里以自增为例进行解释,这两种符号为单目运算符,既可以放在变量前面,也可以放在变量后面,这两种方式如果是单独的自增自减运算没有什么差异,但如果是作为赋值的表达式,就会有一些细微的差异
//Demo.java
class Demo
{
public static void main(String[] args)
{
int xInt;
int yInt;
int zInt;
int iInt;
iInt = xInt = yInt = zInt = 4;
xInt++;
System.out.println("xInt = " + xInt);
++yInt;
System.out.println("yInt = " + yInt);
zInt = xInt++;
System.out.println("xInt = " + xInt);
System.out.println("zInt = " + zInt);
zInt = ++yInt;
System.out.println("yInt = " + yInt);
System.out.println("zInt = " + zInt);
iInt = iInt++;
System.out.println("iInt = " + iInt);
}
}
- 当自增语句独立运行时,无论自增符号在前面还是在后面,都相当于 xInt=xInt + 1 ,这也是第 14 、17 行打印结果都是 5 的原因
- 当自增语句作为赋值的表达式时,根据第 20 、21 、24 、25 行的输出结果,我们会想当然的认为,如果自增符号在变量后面,则先将 xInt 的值赋给 zInt ,然后再执行 xInt++ ;如果自增符号在变量前面,则先执行 yInt++ ,然后再将 yInt 的值赋给 zInt ,这样确实说得通,但事实却并非如此
- 根据上面一条总结出来的经验,iInt=iInt++ 这一语句的执行顺序应该是先将 xInt 的值赋给 iInt ,然后再执行 iInt++ ,这样得出的结果应该是 5,但事实上我们得到的结果是 4 ,这证明了我们刚才总结的规律其实是不成立的,关于自增符在前在后导致的结果的差异其实是因为运算符的优先级导致的
- 当执行 zInt=xInt++ 时,= 的结合性是从右向左,++ 的结合性却是从左向右,当 xInt 想把它的值赋给 zInt 时,却发现右面的运算也需要它参加运算,这时候根据运算符的优先级,赋值运算总是最低级的,所以 xInt 要先进行自增运算,但是自增运算会改变 xInt 的值,运算过后 xInt 就不再是本来的 xInt 了,所以这时候 JVM 创建了一个临时存储空间用于存储 xInt 本来的值,当 xInt 执行完优先级较高的运算后,再将之前临时存储的本来 xInt 的值赋给 zInt ;对于 zInt=++yInt ,并没有产生这样的冲突,它按照顺序就是先算出 ++yInt 的值,然后将之赋给 zInt
- 根据上面的经验,我们再来看一下 iInt=iInt++ 这一语句,首先,建立临时存储空间存下 iInt 的当前值 4 ,然后执行 iInt++ 语句,此时 iInt 的值为 5 ,然后将临时存储的 4 赋给 iInt ,就覆盖掉了之前的 5 ,所以第 28 行输出的结果为 4
取模是只针对整型数据的一种运算,它的值是两个数想相除后的余数,一般 %2 是一种比较常用的操作,因为任意一个数对 2 取模得到的结果只可能是 0 或者 1 ,它可以被用于一些开关控制算法,也可以用于判断一个数是否是偶数
//Demo.java
/*
求一个数值得所有计数单位中包含多少个偶数,
假设这个数是6512458
*/
class Demo
{
public static void main(String[] args)
{
int number, digit, sum;
number = 6512458;
sum = 0;
while (number != 0) //如果number!=0,继续执行循环体
{
digit = number % 10; //取当前number的个位数
if (digit % 2 == 0) //如果digit为偶数,sum就加1
sum++;
number = number / 10; //number对10取整
}
System.out.println("sum = " + sum);
}
}
2. 关系运算符
运算符 | 名称 | 目数 | 结合性 |
---|---|---|---|
> | 大于 | 双目 | 从左向右 |
< | 小于 | 双目 | 从左向右 |
== | 是否等于 | 双目 | 从左向右 |
>= | 大于等于 | 双目 | 从左向右 |
<= | 大于等于 | 双目 | 从左向右 |
!= | 不等于 | 双目 | 从左向右 |
关系运算符通常用于逻辑判断,如果判断满足关系,则为真,否则为假
//Demo.java
/*
判断x,y,z中最大的数,假设x,y,z分别为7,6,9
*/
class Demo
{
public static void main(String[] args)
{
int x, y, z;
x = 7;
y = 6;
z = 9;
if(x<y)
x=y;
if(x<z)
x=z;
System.out.println(x);
}
}
- 判断相等是 == ,很容易写成 = ,而且如果符号左边是变量,就符合语法规则,编译并不会失败,所以如果两个操作数中有常量的话,建议将常量写在前面,这样即使误将 == 写成了 = ,也无法通过编译,方便我们修改程序
3. 逻辑运算符
运算符 | 名称 | 目数 | 结合性 |
---|---|---|---|
& | 逻辑与 | 双目 | 从左向右 |
| | 逻辑或 | 双目 | 从左向右 |
! | 逻辑非 | 双目 | 从左向右 |
^ | 逻辑异或 | 双目 | 从左向右 |
&& | 短路逻辑与 | 双目 | 从左向右 |
|| | 短路逻辑或 | 双目 | 从左向右 |
逻辑运算符通常用于将多个关系表达式合为一个,以增强代码的简洁性
- boolean temp = A & B ,A 和 B 同时为真时,temp = true ,否则 temp = false
- boolean temp = A | B ,A 和 B 同时为假时,temp = false ,否则 temp = true
- boolean temp = !A,A 为真时,temp = false ,否则 temp = true
- boolean temp = A ^ B,A 和 B 同时为真或同时为假时,temp = false ,否则 temp = true
- boolean temp = A && B ,与 & 功用基本相同,不同的是 A 为假,不需要判断 B
- boolean temp = A || B ,与 | 功用基本相同,不同的是 A 为真,不需要判断 B
- A 和 B 可以是布尔型的变量,也可以是关系表达式,一般我们使用 && 和 || ,而不使用 & 和 | ,因为可以有效避免一些错误,比如:x==0||y/x==1,如果 x 为 0,继续执行的话,0 就会别当做除数,程序就会编译失败并抛出一个错误
4. 位运算符
运算符 | 描述 | 目数 | 结合性 |
---|---|---|---|
& | 位与 | 双目 | 从左向右 |
| | 位或 | 双目 | 从左向右 |
~ | 按位求反 | 单目 | 从右向左 |
^ | 位异或 | 双目 | 从左向右 |
<< | 左移 | 双目 | 从左向右 |
>> | 保留符号的右移 | 双目 | 从左向右 |
>>> | 不保留符号的右移 | 双目 | 从左向右 |
位运算符涉及到很底层的东西,我们很少会碰到应用它们的地方,但它们通常具有更高的执行性能
//Demo.java
/*
交换x和y的值,假设x=4,y=20
*/
class Demo
{
public static void main(String[] args)
{
int x, y, temp;
//空间换时间
x = 4;
y = 20;
temp = x;
x = y;
y = temp;
System.out.print("x =" + x + "; ");
System.out.println("y = " + y);
//时间换空间
x = 4;
y = 20;
x = x + y;
y = x - y;
x = x - y;
System.out.print("x =" + x + "; ");
System.out.println("y = " + y);
//位运算,高效率
x = 4;
y = 20;
x = x ^ y;
y = x ^ y;
x = x ^ y;
System.out.print("x =" + x + "; ");
System.out.println("y = " + y);
}
}
- 一条有意思的规律,任何数与同一个数异或两次仍旧是它自己
- 除了移位运算,其他的位运算,都是操作数用二进制表示后,从右至左依次按位进行运算,每一次的结果非 0 即 1 ,直到某个操组数的不再具有二进制位为止
- 移位运算符,则是表示将左操作数用二进制表示后,像某一方向移动右操作数位,如果是 << 则于右侧空出的二进制位补 0 ,如果是 >>> 则于左侧空出的二进制位补 0 ,如果是 >> ,则最高位为符号位,其余空位补 0
5. 赋值运算符
运算符 | 名称 | 目数 | 结合性 |
---|---|---|---|
= | 等号 | 双目 | 从右向左 |
+= | 加等 | 双目 | 从右向左 |
-= | 减等 | 双目 | 从右向左 |
*= | 乘等 | 双目 | 从右向左 |
/= | 除等 | 双目 | 从右向左 |
%= | 模等 | 双目 | 从右向左 |
&= | 与等 | 双目 | 从右向左 |
|= | 或等 | 双目 | 从右向左 |
^= | 异或等 | 双目 | 从右向左 |
<<= | 左移等 | 双目 | 从右向左 |
>>= | 保留符号的右移等 | 双目 | 从右向左 |
>>>= | 不保留符号的右移等 | 双目 | 从右向左 |
如果使用常量对变量赋值,因为出现在程序中的整型默认为 int 类型,所以当待赋值类型是 byte 、short 或者 char 时,就有可能会发生强制类型转换,不过这种转换被 JVM 悄悄完成了,不需要我们显示的声明,但如果常量值超出待赋值的变量类型的取值范围时,程序会编译失败
class Demo
{
public static void main(String[] args)
{
byte aByte = 6;
short aShort = 120;
System.out.println(aByte);
System.out.println(aShort);
}
}
class Demo
{
public static void main(String[] args)
{
byte aByte = 129;
System.out.println(aByte);
}
}
如果使用变量对变量进行赋值,如果两个变量是同一类型,则没什么影响;但如果两者类型不同,且右侧变量取值范围较大,程序会编译失败
class Demo
{
public static void main(String[] args)
{
byte xByte;
byte xByte;
xByte = 12;
yByte = xByte;
System.out.println(yByte);
}
}
class Demo
{
public static void main(String[] args)
{
int xInt;
byte yByte;
xInt = 12;
yByte = xInt;
System.out.println(yByte);
}
}
如果使用含有变量的表达式对变量进行赋值,如果表达式含有的变量与待赋值变量是同一类型或者其取值范围大于待赋值变量类型的取值范围时,编译会失败,它同样有一些特例,即当待赋值的变量以及表达式包含的变量都是默认类型 int 或者 double 时,程序可以编译通过,如果右侧变量的值确实超出左侧变量的取值范围时,则会发生截断
class Demo
{
public static void main(String[] args)
{
byte xByte;
byte yByte;
xByte = 3;
yByte = xByte + 2;
System.out.println(yByte);
}
}
class Demo
{
public static void main(String[] args)
{
int xInt;
int yInt;
xInt = 3;
yInt = xInt + 2;
System.out.println(yInt);
}
}
赋值运算符有很多个,最基础的是 = ,其他的赋值运算符在逻辑上相当于执行这样一个运算:比如 x*=3 相当于 x=x*3,y+=2 相当于 y=y+2;但是赋值在本质上只是执行一次赋值运算,而与之在逻辑上相当的执行语句其实是先计算右侧表达式的值,然后将值赋给左侧的变量,这是两次运算
class Demo
{
public static void main(String[] args)
{
byte xByte;
xByte = 3;
xByte += 2;
System.out.println(xByte);
}
}
class Demo
{
public static void main(String[] args)
{
byte xByte;
xByte = 3;
xByte = xByte + 2;
System.out.println(xByte);
}
}
jshell> short s=1; s+=1;System.out.print(s);
s ==> 1
$5 ==> 2
2
jshell> short s =1; s= s+1;
s ==> 1
| 错误:
| 不兼容的类型: 从int转换到short可能会有损失
| s= s+1;
| ^-^
6. 条件运算符
运算符 | 名称 | 目数 | 结合性 |
---|---|---|---|
?: | 条件运算符 | 三目 | 从右向左 |
条件运算符相当于是分支语句的一种简写方式
//Demo.java
/*
判断x,y,z中最大的数,假设x,y,z分别为7,6,9
*/
class Demo
{
public static void main(String[] args)
{
int x, y, z;
x = 7;
y = 6;
z = 9;
//判断x是否大于y,是,则表达式的值为x;否,则表达式的值为y
x = x > y ? x : y;
x = x > z ? x : z;
System.out.println(x);
}
}