1 变量的基本操作
介绍完八种基本类型,接下来我们看看对变量的一些基本操作。
1.1 强制类型转换
我们在
上一节中介绍,表示范围大的类型,不能给表示范围小的类型赋值。例如,下面的代码就会产生一个编译错误:
int a = 10;
byte b;
b = a; //!编译错误,
int
类型不能
直接给byte
类型赋值
编译出错的原因,在于
int
类型中能够保存的数值,有可能远大于byte类型的表示范围。就好像如果要把一个大汤盆中的东西倒到一个小碗中,很有可能会出问题。这时候,编译器
就会阻拦我们的这个操作。
但是,有些情况下,大汤盆中的东西只剩一点儿了,这个时候完全可以把大汤盆中的
东西倒到小碗中。对于上面的代码来说,虽然int的表示范围远大于byte类型,但是我们可
以确认,int变量a
中的数据,完全可以让一个byte变量来保存。这种情况,我们不希望编译器阻拦我们。因此,可以使用强制类型转换。
强制类型转换的语法如下:
(类型)变量
这个语法表示,把变量的值强制转换为某个特定的类型。例如上面的代码,我们要把a的值强制转换为byte类型,再给b赋值,则可以把代码写成:
b = (byte) a;
需要注意的是,上面的语法转换的是a变量的值。在执行过程中,会先分配一个byte类型的临时空间,然后把a的值转换成byte类型,然后放到这个临时空间去,最后把临时空间的值赋值给b。在转换过程中,并没有改变a变量的类型和值。
那a变量有4个字节,怎么转换成为1个字节的byte类型呢?Java会把a变量4个字节中,最后的一个字节取出来,然后把这个字节的值,放到临时空间去。而另外的3个字节的内容,则在强制类型转换的时候被舍弃。
因此,如果a中数值的范围超过了b所表示的范围,使用强制类型转换也能正常编译,但是转换的结果会有问题。例如
int a = 150;
byte b = (byte) a; // 编译通过,但是 b 的值为一个错误值
System.out.println(b); // 输出为 - 106
为什么一个超过表示范围的正数,转换之后会变成一个负数呢?这涉及到在计算机中如何用二进制表示正数。例如,下面的代码:
short s = 450;
byte b = (byte) s;
首先我们来看short类型的变量s。一个short类型占2个字节,一个字节是8个二进制位,因此一个short变量占16个二进制位。这16个二进制位中,有15个字节是表示数值,最高的一个二进制位表示的是数值的符号。因此,整数的最高位也被称之为符号位。如果一个整数是正数,则它的符号位为0;相反,负数的符号位为1。
450是正数,因此其符号位为0。然后,把十进制数450,转换成二进制之后,得到结果111000010。然后,除了符号和数值位之外,在其余位上补0。最后,450在内存中表示如下:
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
然后,把s强制转换成byte类型。此时,会开辟1个字节的空间,并把short类型最低8位取出来,得到结果如下:
1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
可以看到,在转换过程中,舍弃了short变量的部分内容。
然后,11000010这个数字,被当做byte类型时,首先要判断它的符号。由于这个数字的最高位为1,因此java会把这个数当做是负数来处理。负数的表示方式与正数不同,相对比较复杂,在此不多介绍。需要明确的是,在强制类型转换时,如果对超过表示范围的数做强制类型转换,有可能产生一些意想不到的情况,例如正数变成负数,或者负数变成正数。因此,使用强制类型转换应当要注意,避免发生类似的错误。
1.2 自动类型提升
请看下面的一段代码:
byte a = 10, b = 20;
byte c = a+b;
对于大部分语言而言,这都应该是一段合法的代码,而对于Java语言来说,这段代码会出一个编译时错误:
要理
解产生这个错误的原因,就要了解
Java
的自动类型提升特性。我们首先来看下面
这个表达式:
byte a = 10, b = 20, c = 40;
byte d = a + b + c;
在运行上面的代码时,首先把
a
、
b
、
c
三个变量的值分别设为
10
、
20
和
40
。然后,在
计算
d
的值时,首先需要计算“
=
”右边表达式的值。由于这个表达式有两个“
+
”,因此在
计算的时候,需要进行两次加法操作:首先计算
a+b
的值,在计算出结果之后,再把结果和
c
的值相加。为此,
Java
会首先获得
a
变量的值
10
,然后获得
b
变量的值
20
,之后
进行第
一次加法,计算出结果
30
。因为这个结果在后面的运算中还要使用,所以,
Java
会把这个
数值保存在一个临时变量中。然后,
Java
进行第二个加法运算时,首先取出临时变量的值
30
,然后取出
c
变量的值
40
,再计算出结果是
70
。
70
作为整个“
a+b+c
”表达式的值,也
会存在一个临时变量里。最后,把这个临时变量的值赋值给
d
变量。
可以这么来理解上述的过程:就好比我们在做
a+b+c
这道数学题时,首先,会把
a+b
的值计算出来,计算出来之后,会把这个值暂时写在草稿纸上。再然后,计算这个写在草稿
纸上的值和
c
相加的结果,并把
所得到的最终结果也写在草稿纸上。最后,再把草稿纸上的
值,重新写回到考卷上。
因此,
当
Java
遇到例如
c = a + b
这样的式子时,会首先计算等号右边的
a+b
的值,然
后赋值给等号左边的变量
c
。
在计算过程中,
Java
会创建一个
临时
变量
来保存
a+b
这个表达
式的计算结果,然后把这个临时变量
中的值赋值给
c
。基本过程如下:
计算
a+b
的值
--
>
保存到临时
变量
--
>
把临时
变量
的值赋给
c
。
问题在于,
尽管
a
和
b
两个变量都是
byte
类型,但是
Java
为临时变量选择类型时,会
将这个类型“自动的提升”为
int
类型
,大小
为
4
个字节。于是,上述过程中的第三步,就
成了把一个
int
类型赋值给
byte
类型的操作,从而产生一个编译时错误。这就是
Java
语言
中的自动类型提升特性。
要避免这个问题,只需要对其结果进行强制类型转换即可。即把原代码改为:
byte c = (byte)(a+b);
需要注意的是,由于是对
a+b
的结果进行强制转换,因此要对
a+b
这个表达式加上括号。
1.2.1 Java自动类型提升的规则
1.
如果运算数中存在
double
,则自动类型提升为
double
2.
如果运算数中没有
double
但存在
float
,则自动类型提升为
flo
at
3.
如果运算数中没有浮点类型,但存在
long
,则自动类型提升为
long
4.
其他所有情况,自动类型提升为
int
。
换而言之,
byte + byte
,
byte + short
之类的运算,都会被自动提升为
int
类型。需要说
明的是,
char
类型也能进行运算,并且
char
类型与其他类型运算时,也会进行相应的自动
类型提升。
2 运算符
在介绍
运算符
之前,首先介绍一个非常基本的概念:表达式。
所谓的表达式,指的是用
运算符
连接变量或字面值所形成的式子,例如:
a+b
,
2+c
等。
需要强
调的一点是,任何一个
表达式
都
会有
一个
值。也可以理解为,所有表达式都会返回一
个结果,例如
1+2
会返回
3
作为表达式的结果。
表达式的值也有不同类型。例如,布尔表达式,就说明这个表达式的值的类型为
boolean
类型。因此,在写程序的过程中,一定要明确表达式的值,以及这个值的类型。
理解了表达式的概念,我们来关注形成表达式的关键元素:
运算符。
2.1 赋值号(=)
赋值操作是编程中最常用的操作之一。
Java
中的赋值号为一个等号(
=
)。赋值号具有
右结合性,也就是说,会先计算赋值号右边的内容,然后把计算
结果赋值给左边的变量。
此外,
a = b
构成一个赋值表达式,
其作用就是将变量
b
中的值赋值给变量
a
,
这个表达
式也有值,表达式的值为赋值号右边的计算结果。
由这
个特性,在
Java
中可以进行连等操作,即:
a = b = c = 10
这样的赋值操作。
2.2 基本数学运算
基本数学运算包括加(
+
)减(
-
)乘(
*
)除(
/
)以及取余(
%
)操作。这些操作和数
学上的定义没有区别。
需要注意的是,类似于
3/2
这样的表达式。由于
3
和
2
都是整数类型,因此根据自动类
型提升的规则,结果也一定是整数类型。
因此,
3/2
这个表达式的值是
1
,如果希
望得到数
学上精确的结果
1.5
,
则需要使用
3.0/2
这样的表达式
,来保证结果数据是
double
类型的
。
此外,
Java
中的
数学运算符
符合先乘除,后加减的规则。
例如:
2+3*4
这个表达式的
值为
14
。
2.3 +=,*=,/=
在实际编程中,经常会写出类似于
a = a + 2
之类的表达式,表示把
a
在原有值的基础上
增加
2
。这种写法有一种更方便的简写形式:
a += 2
。
-
=
,
*=
,
/=
等
运算符
与
+=
类似。
2.4 ++与--
对于a+=1这样的表达式而言,还有一种更加简单的运算符:++,与之类似的,a-=1可以用a--来代替。要注意的是,使用++(或--)运算符有两种方式:前缀式或后缀式。例如:
a++; //后缀式
++a; //前缀式
要注意的是这两种方式的区别和联系。首先,对于a++和++a这两种方式而言,对a的操作是完全一样的,都会在表达式运算结束之后把a的值加1。这两种方式所不同的地方在于表达式的值不同。
例如,假设a = 5,则不管执行a++还是++a,执行之后a的值均为6。所不同的是,a++这个表达式的值为5(即a加1之前的值),而++a这个表达式的值为6(即a加1之后的值)。a--和--a有类似的关系。
2.5位运算符
Java
语言中包含了四种位
运算符
:按位与
(&)
,按位或(
|
),按位异或(
^
),取反
~
。
位
运算符
主要用于对数据的每个二进制位进行运算。
首先,从逻辑上说,与、或、异或的逻辑运算如下表所示:
运算值1 | 运算值2 | 与 | 或 | 异或 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
0 | 1 | 0 | 1 | 1 |
0 | 0 | 0 | 0 | 0 |
2.6 移位操作
Java中的移位运算符有三种:算术右移(>>),逻辑右移(>>>)和左移(<<)。移位操作是指,把一个整数的二进制表示形式,向某个方向(左或者右)移动,并按照一定的规律,丢弃和补充相应位上的值。
逻辑右移与算术右移的区别很微妙。对于n>>>m(逻辑右移)与n>>m(算术右移)来说,当n为正数时,两种运算的结果是一样的。所不同的是,当n为负数时,算术右移的结果为一个负数,并且是基本符合算术规律的(所以它叫算术右),而逻辑右移的结果为一个正数。例如下面的代码:
int n = 12;
System.out.println(n>>2); //结果为3
System.out.println(n>>>2); //结果也为3
n =-12;
System.out.println(n>>2); //结果为负
System.out.println(n>>>2); //结果为正!!!
产生这种不同的原因,与计算机中表示整数的方式有关。之前介绍过,在计算机中,保存一个整数时,整数的最高位用来表示符号。其中,符号位为0表示正数,1表示负数。在使用算术右移时,移动数值时不会改变符号位的值,因此正数移动之后,符号位为0,负数移动之后,符号位依然是1。这样,对一个整数进行算术右移之后,正数依然是正数,负数
依然是负数。
然而使用逻辑右移时,最高位总会补上0。对于正数来说,符号位没有改变;而对于负数来说,符号位由1
变成了0。因此,使用逻辑右移时,所得到的结果总是正数。
2.7 布尔运算
我们首先把所有布尔运算符分为两大类。
第一类运算符为:>(大于), >=(大于等于), <(小于), <=(小于等于), ==(相等), !=(不相等),这些运算符都接受两个参数,返回一个布尔值,表示判断结果。需要注意的是判断相等(==)是两个等号,一定要把这个布尔运算和赋值号(=)区分开来。
第二类运算符为:与(&&)、或(||)、非(!),这些运算符只能接受两个布尔类型的运算数。非运算符表示取反,即!true结果为false,而!false结果为true。对于与运算和或运算,运算规则见下表:
运算数1 | 运算数2 | 与 | 或 |
true | true | true | true |
true | false | false | true |
false | true | false | true |
false | false | false | false |
布尔运算与(&&)和位运算与(&)具有类似的运算结果,而布尔运算或(||)和位运算或(|)也具有类似的运算结果。所不同的有两点:
1、布尔运算(&&和||)只能接受布尔值作为运算数,而位运算除了能进行布尔值的运算之外,还能进行整数运算;
2、布尔运算具有短路特性。
什么是短路特性呢?对于与运算而言,如果运算数1的值为false,则无论运算数2的值是true还是false,结果一定是false。因此,如果计算机遇到第一个运算数为false的与(&&)操作,则不会去查看第二个运算数而直接返回。这就是所谓的短路特性。
“与运算”能被false短路,相对应的,“或运算”能被true短路(如果第一个操作数为true,则不管第二个操作数如何,结果一定为true)。在Java中,布尔运算(&&和||)具有短路特性,而位运算(&和|)不具有短路特性。
短路特性和++(--)运算符结合在一起会产生很多有趣的式子。例如下面的式子:
int a = 4, b = 5;
boolean flag = (a++>4) && (b++>3);
请读者思考一下,这段代码运行结束之后,flag、变量a、变量b的值分别是什么?
答案:flag为false,a的值为5,b的值也为5。
说明:在上述代码中,有两个括号,这两个括号的值会由左向右依次执行。
首先,计算a++>4。在计算这个式子时,首先会执行a++,执行完这个表达式之后,a的值变为5。但是,a++这个表达式的值是4,而4>4为false,因此第一个括号中,表达式的值为false。之后,由于后面遇到的是“&&”运算符,这个运算符能够被false短路,因此,第一个括号中的表达式计算出是false之后,第二个括号中的代码不会执行,因此b++这个代码没有被执行,因此b的值没有被改变。可是,如果表达式变为:
int a = 4, b = 5;
boolean flag = (a++>4) &(b++>3);
由于位运算符
&
不具有短路特性,因此,尽管第一个表达式已经被计算
出是
false
,第二
个括号中的代码依然会执行。最终的结果,
b
的值会变化为
6
。
2.8 三元操作符 ?:
Java中只有唯一的一个三元运算符,其基本语法如下:
布尔表达式 ? 表达式1 :表达式2
说这个运算符是三元运算符,指的是这个运算符在使用时,能够接受三个部分参与运算。
从语法上说,第一部分是一个布尔表达式,第二、第三个部分分别是一个表达式。第一部分和第二部分用
“?”隔开,第二部分和第三部分用“:”隔开。这三个部分构成一个完整的三元表达式。
第一部分布尔表达式有一个值,这个值要么是true,要么是false。而表达式1有一个值,表达式2也有一
个值。这三个值参与运算,最后能够得出整个三元表达式的值。
整个表达式的值由下面的原则确定:
如果布尔表达式的值为true,则整个三元表达式的值为表达式1的值;如果布尔表达式的值为false,则整
个三元表达式的值为表达式2的值。例如下面的代码:
c = a>b ? a-b : b-a;
上面的代码,a>b是布尔表达式,a-b是表达式1,b-a是表达式2。而整个表达式的值要么是a-b的值,要么
是b-a的值。
得到结果之后,
再把计算所得的值赋值给变量c。
那么上面的代码完成什么功能呢?我们分情况讨论。如果a大于b,则(a>b)这个表达式的值为true,这样,
整个表达式的值就是a-b。
如果a小于b,则(a<b)这个表达式的值为false。因此,整个三元表达式的值就
是b-a。
综上所述,无论a和b的值是多少,上述代码都能让a和b这两个变量中,较大的变量减去较小的变量,
并把获得的差赋值给c变量。
3 运算符的优先级
在我们小学学习四则混合运算的时候,老师曾经反复的跟我们说,一定要注意,先乘除,
后加减。对于下面的式子:
2 + 3 * 2
这个式子要先计算3*2,再把所得到的结果与2相加。在这个运算的过程中,“先乘除,后加减”,
体现的就是运算符优先级的思想:乘除法的优先级比较高,如果有乘除运算的话,应当先计算。
在Java中,同样有运算符优先级的概念。我们把本章提到的运算符的优先级罗列如下:
优先级 | 运算符 | 说明 |
1 | () | 最高优先级 |
2 | !,~,++,-- | 除了括号外,一元操作符优先级最高 |
3 | *,/,% | |
4 | +,- | Java中同样满足先乘除,后加减 |
5 | <<,>>, >>> | 移位操作 |
6 | <,<=,>,>= | 比大小 |
7 | ==,!= | 判断是否相等 |
8 | & | 按位与 |
9 | ^ | 异或 |
10 | | | 按位或 |
11 | && | 与(逻辑操作) |
12 | || | 或(逻辑操作) |
13 | ?: | 三元操作 |
14 | =,+=,-=,*=,... | 所有的赋值操作 |
在Java中进行计算时,会先进行优先级高的运算,再进行优先级低的运算。但是,圆括号
“()”的优先级是最高的,如果有圆括号的话,就先计算圆括号中的值。
在写代码的时候,一般不用刻意去记忆操作符的优先级。如果不能确定运算符的优先级,可以
使用圆括号()来指定运算的顺序。
本章的重点是Java中一些关于变量的基本知识。希望读者通过本章的学习,能掌握8种基本类型一
些基本操作,掌握自动类型提升的概念和原则,掌握运算符的用法.