第3章 操作符
在最底层,Java中的数据是通过使用操作符来操作的。
Java是建立在C++的基础之上的,Java相比C和C++,在操作符上也做了一些改进与简化。
3.1 更简单的打印语句
在上一章中,我们介绍了Java的打印语句:
System.out.println("Rather a lot to type");
可以看到,这条语句不仅涉及多种类型,而且不便于阅读。所以,我们可以使用静态导入(static import)并创建一个小类库来简化上一章的程序:
import static com.jiao.util.Print.print;
import java.util.Date;
public class HelloDate {
public static void main(String[] args) {
print("Hello,it's:");
print(new Date());
}
}
3.2 使用Java操作符
操作符作用于操作数,生成一个新值。
有些操作符可能会改变操作数自身的值,这被称为副作用。
大部分操作符只能操作基本类型,例外的一些,如:=、==、!= 这些操作符能操作所有的对象。
String类支持 + 和 += 。
3.3 优先级
当一个表达式中存在多个操作符时,操作符的优先级就决定了各部分的计算顺序。Java对计算顺序做了如下规定:括号中优先级最高,然后,先乘除后加减。 例如:
public class Precedence {
public static void main(String[] args) {
int x = 1, y = 2, z = 3;
int a = x + y - 2 / 2 + z;
int b = x + (y - 2) / (2 + z);
System.out.println("a=" + a + " b=" + b);
}
}
3.4 赋值
赋值使用操作符 "=" 。它的意思是:取右值,赋予给左值。
- 右值:可以是任何常数、变量或表达式。
- 左值:必须是一个明确的、已命名的变量。
Java中的赋值,存在两种情况:
-
基本数据类型:将右值赋予左值后,左值改变不影响右值。
-
引用类型:将右值赋予左值后,对左值操作会影响右值。即两个变量存储的都是同一个对象的引用,无论对哪个变量操作,都是对该对象进行操作。
下面,我们通过例子说明上述情况:
class Tank {
int level;
}
public class Assignment {
public static void main(String[] args) {
int a = 39;
int b = 78;
print("a = " + a + ",b = " + b);
a = b;
print("a = " + a + ",b = " + b);
a = 99;
print("a = " + a + ",b = " + b);
Tank t1 = new Tank();
Tank t2 = new Tank();
t1.level = 8;
t2.level = 88;
print("t1.level:" + t1.level + ",t2.level:" + t2.level);
t1 = t2;
print("t1.level:" + t1.level + ",t2.level:" + t2.level);
t1.level = 99;
print("t1.level:" + t1.level + ",t2.level:" + t2.level);
}
} /* Output:
a = 78,b = 78
a = 99,b = 78
t1.level:8,t2.level:88
t1.level:88,t2.level:88
t1.level:99,t2.level:99
*/
在上例中,将t2赋给t1,则两个变量持有同一个引用,此时对任一变量操作,均会改变引用所指向的对象信息,这种特殊的现象被称为:别名现象,是Java操作对象的一种基本方式。
如果我们需要避免别名问题,可以采取下面的做法:
t1.level = t2.level;
3.4.1 方法调用中的别名问题
将一个对象传递给方法时,也会产生别名问题:
class Letter {
char c;
}
public class PassObject {
static void f(Letter y) {
y.c = 'z';
}
public static void main(String[] args) {
Letter x = new Letter();
x.c = 'a';
print("x.c: " + x.c);
f(x);
print("x.c: " + x.c);
}
}
通过上例,我们发现,在方法调用过程中,形参实际上是接收到了实参传递的对象引用。
3.5 算数操作符
Java的基本算数操作符包括:加号(+)、减号(-)、除号(/)、乘号(*)以及取模操作符(%)。 并且,使用操作符后紧跟一个等号可以同时进行运算与赋值操作。 例如:
public class MathOps {
public static void main(String[] args) {
Random rand = new Random(88);
int i, j, k;
j = rand.nextInt(100) + 1;
print("j: " + j);
k = rand.nextInt(100) + 1;
print("k: " + k);
i = j + k;
print("j + k : " + i);
i = j - k;
print("j - k : " + i);
i = k / j;
print("k / j : " + i);
i = k % j;
print("k % j : " + i);
j %= k;
print("j %= k : " + j);
}
}
上例使用了Random类来帮助我们生成随机数,参数为随机数生成器的种子,随机数生成器对于特定的种子值总是产生相同的随机数序列。
3.5.1 一元加、减操作符
一元减号和加号与二元减号和加号都使用相同的符号,根据表达式的书写形式,编译器会自己判断出使用的是哪一种。 例如:
x = -a;
x = a * (-b);
一元减号用于转变数据的符号,而一元加号则是为了与一元减号相对应,它唯一的作用是将较小类型的操作数提升为int。
3.6 自动递增和递减
Java提供了大量的快捷运算,使得编码更方便,但有时可能使代码阅读起来更困难。
递增和递减是两种相当不错的快捷运算,通常称为"自动递增"和"自动递减":
- 递减操作符为:--,意为减少一个单位。
- 递增操作符为:++,意为增加一个单位。
这两个操作符各有两种使用方式,分别为前缀式和后缀式:
- 前缀式:操作符位于变量或表达式的前面,先执行递增或递减。
- 后缀式:操作符位于变量或表达式的后面,后执行递增或递减。
下面是一个例子:
public class AutoInc {
public static void main(String[] args) {
int i = 1;
print("i: " + i);
print("++i: " + ++i);
print("i++: " + i++);
print("i: " + i);
print("--i: " + --i);
print("i--: " + i--);
print("i: " + i);
}
}
递增和递减是除了那些涉及赋值的操作符以外,唯一具有副作用的操作符,即,它们在进行运算的同时会改变操作数。
3.7 关系操作符
关系操作符生成的是一个boolean(布尔)结果,它们计算的是操作数的值之间的关系。
关系操作符包括小于(<)、大于(>)、小于或等于(<=)、大于或等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有的基本数据类型,而其他比较符不适用于boolean类型。
3.7.1 测试对象的等价性
关系操作符==和!=也适用于所有对象,下面是一个例子:
public class Equivalence {
public static void main(String[] args) {
Integer n1 = new Integer(88);
Integer n2 = new Integer(88);
System.out.println(n1 == n2);
System.out.println(n1 != n2);
}
}
我们发现,结果出乎我们意料,因为,==和!=作用于对象时,比较的是对象引用而非对象内容。
我们需要比较两个对象的实际内容时,可以使用equals()方法:
public class EqualsMethod {
public static void main(String[] args) {
Integer n1 = new Integer(88);
Integer n2 = new Integer(88);
System.out.println(n1.equals(n2));
}
}
上例结果如我们所料,但比较的是自己的类型时:
class Value {
int i;
}
public class EqualsMethod2 {
public static void main(String[] args) {
Value v1 = new Value();
Value v2 = new Value();
v1.i = v2.i = 100;
System.out.println(v1.equals(v2));
}
}
这次的结果却是false,这是由于equals()的默认行为是比较对象引用,所以除非在自定义的新类中覆盖equals()方法,否则无法表现出我们希望的行为。
大多数Java类型都重写了equals()方法,以便用来比较对象的内容而非引用。
3.8 逻辑操作符
逻辑操作符:与(&&)、或(||)、非(!)能根据参数的逻辑关系,生成一个布尔值(true或false)。 下面的例子使用了关系操作符和逻辑操作符:
public class Bool {
public static void main(String[] args) {
Random rand = new Random(66);
int i = rand.nextInt(100);
int j = rand.nextInt(100);
print("i = " + i);
print("j = " + j);
print("i > j is " + (i > j));
print("i < j is " + (i < j));
print("i >= j is " + (i >= j));
print("i <= j is " + (i <= j));
print("i == j is " + (i == j));
print("i != j is " + (i != j));
print("(i < 10) && (j < 10) is " + ((i < 10) && (j < 10)));
print("(i < 10) || (j < 10) is " + ((i < 10) || (j < 10)));
}
}
逻辑操作符只能作用于布尔值,不可将一个非布尔值当作布尔值在逻辑表达式中使用。
3.8.1 短路
当使用逻辑操作符时,我们会遇到一种短路现象。即一旦能够明确无误地确定整个表达式的值,就不再计算表达式的余下部分了。 下面是演示短路现象的例子:
public class ShortCircuit {
static boolean test1(int val) {
print("test1(" + val + ")");
print("result: " + (val < 1));
return val < 1;
}
static boolean test2(int val) {
print("test1(" + val + ")");
print("result: " + (val < 2));
return val < 2;
}
static boolean test3(int val) {
print("test1(" + val + ")");
print("result: " + (val < 3));
return val < 3;
}
public static void main(String[] args) {
boolean b = test1(0) && test2(2) && test3(2);
print("expression is " + b);
}
}
在上例中,当方法被调用,则会输出信息。当第二个测试产生了false结果时,意味着整个表达式肯定为false,所以没必要继续计算剩余的表达式。如果所有的逻辑表达式都有一部分不必计算,则会获得潜在的性能提升。
3.9 直接常量
当我们在程序中使用直接常量时,有时编译器无法准确知道该常量类型,需要使用某些字符来额外增加一些信息帮助编译器得知其类型。 下面的代码展示了这些字符:
public class Literals {
public static void main(String[] args) {
int i1 = 0x2f;
print("i1: " + Integer.toBinaryString(i1));
int i2 = 0X2F;
print("i2: " + Integer.toBinaryString(i2));
int i3 = 0177;
print("i3: " + Integer.toBinaryString(i3));
char c = 0xffff;
print("c: " + Integer.toBinaryString(c));
byte b = 0x7f;
print("b: " + Integer.toBinaryString(b));
short s = 0x7fff;
print("s: " + Integer.toBinaryString(s));
long n1 = 200L;
long n2 = 200l;
long n3 = 200;
float f1 = 1;
float f2 = 1F;
float f3 = 1f;
double d1 = 1d;
double d2 = 1D;
}
}
直接常量后面的后缀字符标志了它的类型:
- 大写或小写的L,代表long。
- 大写或小写的F,代表float。
- 大写或小写的D,代表double。
十六进制数适用于所有的整数数据类型,以前缀Ox或0X,后面跟随数字0~9或字符a~f来表示。 在上面的代码中已经给出了char、byte以及short所能表示的最大十六位进制值,如果试图将一个变量初始化成超出自身表示范围的值,编译器则会该值作为int型处理,并报告一条错误信息,告诉我们需要对将int型进行窄化转换。
八进制数由前缀0以及后续的0~7的数字来表示。
二进制则没有直接常量的表示方式,但是在使用十六进制和八进制记数法时,以二进制形式显示结果非常有用。通过使用Integer和Long的静态方法toBinaryString()可以很容易地实现。不过将较小类型传递给Integer.toBinaryString(),则该类型会自动被转换为int。
数字的二进制表示形式称为:有符号的二进制补码。
3.9.1 指数记数法
Java采用了一种不直观的记数法来表示指数,例如:
public class Exponents {
public static void main(String[] args) {
float expFloat = 1.39e-43f;
System.out.println(expFloat);
double expDouble = 66e66d;
System.out.println(expDouble);
}
}
在科学与工程领域上,e代表自然对数的基数,约等于2.718,Java中的Math.E给出更精确的double型的值。
在编程语言中,e则代表10的幂次。
编译器会将小数默认作为双精度数(double)处理,上例中,如果不在变量expFloat后加上f,则会收到一条出错提示,告诉我们必须使用强制类型转换将double转型为float。
3.10 按位操作符
按位操作符用来操作整数基本数据类型中的单个比特,即二进制位。按位操作符会对两个参数中对应的位执行布尔代数运算,并最终生成一个结果。
按位操作符有以下几种:
- 按位与操作符(&):两个输入位都为1,则生成一个输出位1。
- 按位或操作符(|):两个输出位中有一个1,则生成一个输出位1;都为0时,输出0。
- 按位异或操作符(^):输入位的某一个是1,但不全是1,则生成一个输出位1。
- 按位非操作符(~):也称为取反操作符,它属于一元操作符,只对一个操作数进行操作。
3.11 移位操作符
移位操作符操作的运算对象也是二进制的位,只可用来处理整数类型。
-
左移位操作符(<<):按照操作符右侧指定的位数将操作数向左移动(低位补0)。
-
有符号右移位操作符(>>):按照操作符右侧指定的位数将操作数向右移动,若符号为正,则在高位插入0,符号为负,则在高位插入1。
-
无符号右移位操作符(>>>):无论正负,都在高位插入0。
如果对于char、byte或者short类型的数值进行移位处理,那么在移位前,它们会被转换位int类型,并且得到的结果也是一个int类型的值。
移位可与等号组合使用(<<=、>>=、>>>=),此时,操作符左边的值会移动由右边指定的位数,再将得到的结果赋给左边的变量。但在进行无符号右移位结合赋值时,操作数如果是byte或short类型,它们会先被转为int类型,再进行右移操作,然后被截断,赋值给原来的类型,这种情况下可能得到-1的结果。 下面的例子演示了这种情况:
public class URShirft {
public static void main(String[] args) {
int i = -1;
print(Integer.toBinaryString(i));
i >>>= 10;
print(Integer.toBinaryString(i));
long l = -1;
print(Long.toBinaryString(l));
l >>>= 10;
print(Long.toBinaryString(l));
short s = -1;
print(Integer.toBinaryString(s));
s >>>= 10;
print(Integer.toBinaryString(s));
byte b = -1;
print(Integer.toBinaryString(b));
b >>>= 10;
print(Integer.toBinaryString(b));
b = -1;
print(Integer.toBinaryString(b));
print(Integer.toBinaryString(b >>> 10));
}
}
在最后一个移位运算中,结果没有赋给b,而是直接打印出来,所以其结果是正确的。
3.12 三元操作符 if-else
三元操作符也称为条件操作符,其表达式采取下述形式:
boolean-exp ? value0 : value1
- boolean-exp的结果为true,则计算value0,并且将其作为表达式的结果。
- boolean-exp的结果为true,则计算value1,并且将其作为表达式的结果。
下面通过示例说明条件操作符的用法:
public class TernaryIfElse {
static int ternary(int i) {
return i < 10 ? i * 100 : i * 10;
}
public static void main(String[] args) {
print(ternary(9));
print(ternary(90));
}
}
3.13 字符串操作符 + 和 +=
为了更方便地操作字符串,Java对+和+=进行了操作符重载(operator overloading),使得它们可以用于连接字符串。并且,如果表达式以字符串开头,那么后续的+操作符均被当成字符串操作符使用,操作数也均被转型为String:
public class StringOperators {
public static void main(String[] args) {
int x = 0, y = 1, z = 2;
String s = "x, y, z ";
print(s + x + y + z);
print(x + " " + s);
s += "(summed) = ";
print(s + (x + y + z));
print("" + x);
}
}
3.14 使用操作符时常犯的错误
程序员在使用操作符时,经常会犯如下错误:
- 对表达式如何计算有点不确定,也不愿意使用括号。
- 将&和| 与 &&和||使用错误。
3.15 类型转换操作符
在适当的时候,Java会将一种数据类型自动转换成另一种。例如,将浮点变量赋以以一个整数值,编译器会将int自动转换成float。类型转换(cast)运算符,允许我们在不能进行自动转换的时候强制进行类型转换。
进行类型转换时,将转换后类型置于圆括号内,放在要进行类型转换的值左边,如下例所示:
public class Casting {
public static void main(String[] args) {
int i = 200;
long lng = (long) i;
lng = i;
long lng2 = (long) 200;
i = (int) lng2;
}
}
在Java中,一般有两种类型转换方式:
-
窄化转换:将能容纳更多信息的数据类型转换成容纳较少信息的数据类型,面临信息丢失的危险。此时,编译器会强制进行类型转换。 如:将int型变量赋以long类型数值。
-
扩展转换:将能容纳较少信息的数据类型转换成容纳更多信息的数据类型,该行为是安全的,不必显式地进行类型转换。
Java允许除了布尔型之外的任何基本数据类型之间相互转换。
3.15.1 截尾和舍入
在执行窄化转换时,必须注意截尾和舍入的问题。例如,将一个浮点值转换为整型值。下例展示了Java的处理结果:
public class CastingNumbers {
public static void main(String[] args) {
double above = 0.7, below = 0.4;
float fabove = 0.7f, fbelow = 0.4f;
print("(int)above: " + (int) above);
print("(int)below: " + (int) below);
print("(int)fabove: " + (int) fabove);
print("(int)fbelow: " + (int) fbelow);
}
}
从上例的输出结果中可以看出,在将double或float转型位整型值时,总是对该数字进行截尾。如果想要得到舍入的结果,则需要使用java.lang.Math中的round()方法:
public class RoundingNumbers {
public static void main(String[] args) {
double above = 0.7, below = 0.4;
float fabove = 0.7f, fbelow = 0.4f;
print("Math.round(above): " + Math.round(above));
print("Math.round(below): " + Math.round(below));
print("Math.round(fabove): " + Math.round(fabove));
print("Math.round(fbelow): " + Math.round(fbelow));
}
}
3.15.2 提升
在对基本数据类型执行算术运算或按位运算时,只要类型比int小,那么在运算之前会自动转换成int,最终生成的结果也就是int类型。如果将结果赋值给较小类型,就会发生窄化转换,可能丢失信息。 通常,表达式中出现的最大的数据类型决定了表达式最终结果的数据类型。 例如,一个float值与一个double值相乘,则结果为double类型。
3.16 Java没有sizeof
在C和C++中,由于不同的数据类型在不同的机器上可能有不同的大小,在进行一些与存储空间有关的问题时,需要知道那些类型的大小,sizeof()用于查看数据类型的字节数。
在Java中,所有的数据类型在所有的机器中的大小是相同的,所以不需要sizeof()。
3.17 操作符小结
能够对布尔值进行的运算非常有限,我们只赋予它true和false值,并测试它们为真还是为假。
在char、byte和short中,我们可看到使用算术操作符中数据类型提升的效果。对于这些类型的任何一个进行算术运算,都会获得一个int结果,必须将其显式地类型转换回原来的类型,以将该值赋予原类型的变量。
而对于int类型,我们需要注意运算是否会造成溢出,如下例所示:
public class Overflow {
public static void main(String[] args) {
int big = Integer.MAX_VALUE;
System.out.println("big = " + big);
int bigger = big * 4;
System.out.println("bigger = " + bigger);
}
}
/* Output:
big = 2147483647
bigger = -4
*/
从上例中看出,当出现溢出时,编译器不会提示错误和警告,运行时也没有出现异常,结果发生了错误。
通过前面的学习,我们可以发现:除boolean以外,任何一种基本类型都可通过类型转换变为其他基本类型,当类型转换成一种较小的类型时,可能会丢失信息。