开始使用
运算符接收一个或多个参数并生成新值。这个参数与普通方法调用的形式不同,但效果相同。所有运算符都能根据自己的运算对象生成一个值。除此之外,一些运算符可以改变运算对象的值,这叫作“副作用”(Side Effect)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。
几乎所有的运算符只能操作基本类型(Primitives)。唯一例外的是=、==、!=,它能操作所有对象。除此之外String类支持“+”和“+=”。
优先级
运算符的优先级决定了存在多个运算符时一个表达式各部分的运算顺序。Java对运算顺序作出了特别的规定。其中,最简单的就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先规则,所以应该用括号明确规定运算顺序。
1 | public class Precedence{ |
这些语句看起来大致相同,但从输出中可以看出它们具有非常不同的含义,具体取决于括号的使用。注意到,在System.out.println()语句中使用了+运算符。但在这里+代表的意思是字符串连接符。编译器会将+连接的非字符串尝试转化为字符串。上例中的输出结果说明了a和b都已经被转化成了字符串。
赋值
运算符的赋值是由符号=完成的。它代表着获取=右边的值并赋给左边的变量。右边可以是任何常量、变量或者可产生一个返回值的表达式。但左边必须是一个明确的、已命名的变量。也就是说,必须要有一个物理空间来存放右边的值。举个例子,可以将常数赋给一个变量(A=4),但不可以将任何东西赋给一个常数(4=A)。
基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。举个例子,a=b,如果b是基本类型,那么赋值操作会将b的值复制一份给变量a,此后若a的值发生改变是不会影响到b的。
如果是为对象赋值,那么结果就不一样了。对一个对象进行操作时,我们实际上操作的是它的引用。所以我们将右边的对象赋予给左边时,赋予的只是该对象的引用。此时,两者指向的堆中的对象还是同一个:
1 | class Tank{ |
这是一个简单的Tank类,在main()方法创建了两个实例对象。两个对象的level属性分别被赋予不同的值。然后,t2的值被赋予给t1。在Java中,由于赋予的只是对象的引用,改变t1也就改变t2.这是因为t1和t2此时指向的是堆中同一个对象。
这种现象通常称为别名(aliasing),这是Java处理对象的一种基本方式。但是你若不想出现这里的别名引起混淆的话,你可以这么做:
1 | t1.level = t2.level; |
这样做保留了两个单独的的对象,而不是丢弃一个并将t1和t2绑定到同一个对象。
方法调用中的别名现象
当我们把对象传递给方法时,会发生别名现象。
1 | class Letter{ |
算术运算符
Java的基本运算符与其他大多编程语言是相同的。
1 | // operators/MathOps.java |
为了生成随机数,程序先创建一个Random对象。不带参数的Random对象会利用当前时间用作随机数生成器的“种子”(seed),从而为程序的每次执行成不同的输出。为了每个示例末尾的输出尽可能一致,以便可以使用外部工具进行验证。所以我们在创建Random对象时提供种子(随机数生成器的初始值,其始终为特定种子值产生相同的序列),让程序每次执行都生成相同的随机数,如此以来输出结果就是可验证的。若需要生成随机值,可以删除种子参数。该对象通过调用方法nextInt()和nextFloat()(还可以调用nextLong()或nextDouble()),使用Random对象生成许多不同类型的随机数。nextInt()的参数设置生成的数字的上限,下限为零,为了避免零除的可能性,结果偏移1。
一元加减运算符
一元加+减-运算符的操作和二元是相同的。编译器可自动识别使用任何方式解析运算:
1 | x = -a; |
一元减号可以得到数据的负值。一元加号的作用相反,不过它唯一能影响的就是把较小的数值类型自动转换为int类型。
递增和递减
其中递增++和递减- -,意为“增加或减少一个单位”。举个例子来说,假设a是一个int类型的值,则表达式++a就等价于a=a+1。递增和递减运算符不仅可以修改变量,还可以生成变量的值。
每种类型的运算符,都有两个版本可供选择;通常将其称为“前缀”和“后缀”。“前递增”表示++运算符位于变量或表达式前面;而“后递增”表示++运算符位于变量的后面。类似地,“前递减”意味着- -运算符位于变量的前面;而“后递减”意味着 - -运算符位于变量的后面。对于前递增和前递减(如++a或- - a),先会执行递增/减运算,再返回值。而对于后递增和后递减(如a++或a - -),会先返回值,再执行递增/减运算:
1 | // operators/AutoInc.java |
对于前缀形式,我们将在执行递增/减操作后获取值;使用后缀形式,我们将在执行递增/减操作之前获取值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外)——它们修改了操作数的值。
关系运算符
关系运算符==和!=同样适用于所有对象之间的比较,但它们比较的内容却经常困扰Java初学者:
1 | // operators/Equivalence.java |
表达式System.out.println(n1 == n2)将会输出比较的结果。因为两个Integer对象相同,所以先输出true,再输出false。但是,尽管对象的内容一样,对象的引用却不一样。==和!=比较的是对象的引用,所以输出实际上应该先是false,再输出true(如你把45改成128,那么打印的结果就是这样,因为Integer内部维护着一个IntegerCache的缓存,,默认缓存范围是[-128,127],所以[-128,127]之间的值用\==和!=比较也能得到正确的结果,但是不推荐用关系运算符比较)。
那么怎么比较两个对象的内容是否相同呢?你必须使用所有对象(不包括基本类型)中都存在的equals()方法:
1 | // operators/EqualsMethod.java |
上面的结果看起来是我们所期望的。但其实事情并非那么简单。下面我们来创建自己的类:
1 | // operators/EqualsMethod2.java |
上例的结果再次令人困惑:结果是false。原因是equals()的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中覆写equals()方法,否则我们将获取不到现要的结果。
逻辑运算符
每个逻辑运算符&&(AND)、||(OR)、!(非)根据参数的逻辑关系生成布尔值true或false。下面的代码示例中使用了关系运算符和逻辑运算符:
1 | // operators/Bool.java |
在Java逻辑运算中,我们不能像C/C++那样使用非布尔值,而仅能使用AND、OR、NOT。上面的例子中,我们将使用非布尔值的表达式注释掉了。请注意,如果在预期为String类型的位置上使用boolean类型的值,则结果会自动转为适当的文本格式(即“true”或“false”字符串)。
我们可以将前一个程序中int的定义替换为除boolean之外的任何其他基本数据类型。但请注意,float类型的数值比较严格,只要两个数字的最小位不同则两个数仍然不相等;只要数字最小位是大于0的,那么它就不等于0。
短路
逻辑运算符支持一种称为“短路”(short-circuiting)的现象。整个表达式会在运算到可以明确结果时就停止并返回结果,这意味着该逻辑表达式的后半部分不会被执行到:
1 | // operators / ShortCircuit.java |
每个测试都对参数执行比较并返回true或false。同时控制台也会在方法执行时打印它们的执行状态:
1 | test1(0) && test2(2) && test3(2) |
你可能预期是程序会执行3个test方法并返回。我们来分析一下:第一个方法返回true,因此表达式会继续走下去。紧接着,第二个方法的返回结果是false。这就代表这整个表达式的结果肯定为false,所以就没有必须再判断剩下的表达式部分了。
所以,运用“短路”可以节省部分不必要的运算,从而提高程序潜在的性能。
字面值常量
通常,当我们程序中插入一个字面值常量(Literal)时,编译器会确切地识别它的类型。当类型不明确时,必须辅以字面值常量关联来帮助编译器识别:
1 | // operators/Literals.java |
在本文值的后面添加的字符可以让编译器识别该文本值得类型。对于Long型数值,结尾使用大写L或小写l皆可(不推荐使用l,因为与阿拉伯数字1容易混淆)。大写F或小写f表示float浮点数。大写D或小写d表示double双精度。
十六进制,适用于所有整型数据类型,由前导0x或0X表示,后跟0-9或a-f(大写或小写)。如果我们在初始化某个类型的数值时,赋值超出其范围,那么编译器会报错。在上例的代码中,char、byte和short的值已经是最大了。如果超过这些值,编译器将会自动转型为int,并且提示我们需要声明强制类型转换,意味着我们已经越过该类的范围界限。
八进制由0~7之间数字和前导0表示。
Java7引入了二进制的字面值常量,由前导0b或0B表示,它可以初始化所有的整数类型。
使用整数值类型时,显示其二进制形式会很有用。Long型Integer型中这很容易实现,调用其静态的toBinaryString()方法即可。但是请注意,若将较小的类型传递给Integer.toBinaryString()时,类型将自动转型为int。
下划线
Java7中有一个深思熟虑的补充:我们可以在数字字面量中包含下划线_,以使结果更加清晰。这对于大数值的分组特别有用。代码示例:
1 | // operators/Underscores.java |
下面为合理的使用规则:
- 仅限单_,不能多条相连;
- 数值开头和结尾不允许出现_;
- F、D和L的前后禁止出现_;
- 二进制前导b和十六进制x前后禁止出现_。
注意%n的使用。熟悉C风格的程序员可能习惯于看到\\n来表示换行符。问题在于它给你一个“Unix风格”的换行符。此外,如果我们使用的是Windows,则必须指定\\r\\n。这就是Java用%n实现的可以忽略平台间差异而生成适当的换行符,但只有当你使用System.out.printf()或System.out.format()时。对于System.out.println(),我们仍然必须使用\\n;如果你使用%n,println()只会输出%n而不是换行符。
指数计数法
1 | // operators/Exponents.java |
在科学与工程学领域,e代表自然对数的基数,约等于2.718(Java里用一种更精确的double值Math.E来表示自然对数)。指数表达式“1.39x e-43”一位置“1.39x2.718的-43次方”。然而,自FORTRAN语言发明后,人们自然而然地觉得e代表“10的几次幂”。
在Java中看到类似“1.39e-43f”这样的表达式,请转换思维,从程序设计的角度思考它;它正真的含义是“1.39x10的-43次方”。
注意若编译器能够正确地识别类型,就不必使用后缀字符:
1 | long n3 = 200; |
它并不存在含糊不清的地方,所以200后面的L大可省去。然而,对于以下语句:
1 | float f4 = 1e-43f; //10 的幂数 |
编译器通常会将指数作为double类型来处理,所以倘若没有这个后缀f,编译器就会报错,提示我们应该将double型转换成float型。
位运算符
位运算符允许我们操作一个整型数字中的单个二进制。位运算会对两个整数对应的位执行布尔代数,从而产生结果。
若两个输入位都是1,则按位“与运算符”\&运算后结果是1,否则结果是0。若两个输入位至少有一个是1,则按位“或运算”|运算后结果是1;只有在两个输入位都是0的情况下,运算结果才是0。若两个输入位的某一个是1,另一个不是1,那么按位“异或运算符”\^运算后结果才是1.按位“非运算符”~属于一元运算符;它只对一个自变量进行操作(其他所有运算符都是二元运算符)。按位非运算后结果于输入位相反。例如输入0,则输出1;输入1,则输出0。
位运算符和逻辑运算符都是用了同样的字符,只不过数量不同。位短,所以位运算符只有一个字符。位运算符可与等号\=联合使用已接收结果及赋值:\&=,|=,\^=都是合法的(由于~是一元运算符,所以不可与\=联合使用)。
我们将Boolean类型被视为“单位值”(one-bit value),所以它多少有些独特的地方。我们可以针对boolean变量执行与、或、异或运算,但不能执行非运算。对于布尔值,位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。此外,针对布尔值进行的位运算为我们新增了一个“异或”逻辑运算符,它并未包括在逻辑运算符的列表中。在移位表达式中,禁止使用布尔值。
移位运算符
移位运算符面向的对象也是二进制的“位”。它只能用于处理整数类型。左移位运算符\<<能将其左边的运算对象向左移动右侧指定的位数(在低位补0)。右移位运算符>>则相反。右移位运算符有“正”,“负”值:若值为正,则在高位插入0;若值为负,则在高位插入1。Java也添加了一种“不分正负”的右移位运算(>>>),它使用了“零扩展”(zero extension):无论正负,都在高位插入0。
若移动char、byte或short,则会在移动之前将其提升为int,结果为int。仅使用右值(rvalue)的5个低阶位。这可防止我们移动超过int范围的位数。若对一个long值进行处理,最后得到的结果也是long。
移位可以与等号\<<=或>>=或>>>=组合使用。左值被替换为其移位运算后的值。但是,问题来了,当无符号右移与赋值相结合时,若将其与byte或short一起使用的话,则结果错误。取而代之的是,它们被提升为int型并右移,但在重新赋值时被截断。在这种情况下,结果为-1。
1 | // operators/URShift.java |
1 | // operators/BitManipulation.java |
三元运算符
三元运算符,也称为条件运算符。下面是它的表达式格式:
布尔表达式?值1:值2
若表达式计算为true,则返回值1;如果表达式的计算为false,则返回值2。
当然,也可以换用普通的if-else语句,但三元运算符更加简洁。与if-else不同的是,三元运算符是有返回结果的:
1 | // operators/TernaryIfElse.java |
可以看出,ternary()中的代码更简短。然而,standardIfElse()中的代码更易于理解且不要求更多的录入。所以在挑选三元运算符时,请务必权衡一下利弊。
字符串运算
这个运算符在Java里有一项特殊用途:连接字符串。
我们注意到运用String + 时有一些有趣的现象。若表达式以一个String类型开头,那么后续所有运算对象都必须是字符串:
1 | // operators/StringOperators.java |
注意:上例中第1输出语句的执行结果是012而并非3,这是因为编译器将其分别转换为其字符串形式然后与字符串变量s连接。在第2条输出语句中,编译器将开头的变量转换为了字符串,由此可以看出,这种转换与数据位置无关,只要当中有一条数据是字符串类型,其他非字符串数据都被转换为字符串形式并连接。最后一条输出语句,我们可以看出+=运算符可以拼接其右侧的字符串连接结果并重赋值给自身变量s。括号()可以控制表达式的计算顺序,以便在显示int之前对其进行实际求和。
请注意最后方法中的最后一个例子:我们经常会看到一个空字符串“ ”跟着一个基本数据类型。这样可以隐式地将其转换为字符串,以代替繁琐的显示调用方法(Integer.toString())。
常见陷阱
使用运算符时很容易犯的一个错误是,在还没搞清楚表达式的计算方式时就试图忽略括号()。
1 | while(x = y) { |
显然,程序员愿意是测试等价性\==,而非赋值\=。在Java中,这样的表达式并结果并不会转化为一个布尔值。而编译器会试图把这个int类型的数据转换为预期应接收的布尔类型。最后,我们将会在试图运行前收到编译期错误。因此,Java天生避免了这种陷阱发生的可能性。
唯有一种情况例外:当变量x和y都是布尔值,例如x=y是一个逻辑表达式。除此之外,之前的那个例子,很大可能是错误的。
类型转换
“类型转换”(Casting)的作用是“与一个模型匹配”。在适当的时候,Java会将一种数据类型自动转换成另一种。例如,假设我们为float变量赋值一个整数型,计算机会将int自动转换为float。我们可以在程序未自动转换时显式、强制地使此类型发生转换。
要执行强制转换,需要将所需要的数据类型放在任何左侧的括号内:
1 | // operators/Casting.java |
你可以这样地去转换一个数值类型的变量。但上例这种做法是多余的:因为编译器会在必要时自动提升int数据类型为long型。
当然,为了程序逻辑清晰或提醒自己留意,我们也可以显示地类型转换。在其他情况下,类型转换只有在代码编译时才显出其重要特性。在Java里,类型转换则是一种比较安全的操作。但是,若将数据类型进行“向下转换”(Narrowing Conversion)的操作(将容量较大的数据类型转换成容量较小的类型),可能会发生信息丢失的危险。此时,编译器会强迫我们进行转型,好比在提醒我们:改操作可能危险,若你坚持着这么做,对不起,请明确需要转换的类型。对于“向上转换”(Widening conversion),则不必进行显示的类型转换,因为较大类型的数据肯定能容纳较小类的数据,不会造成任何信息的丢失。
除了布尔类型,Java允许任何基本类型的数据转换为另外一种基本类型的数据。此外,类是不能进行类型转换的。为了将一个类型转换为另一个类型,需要使用特殊的方法。
截断和舍入
在执行“向下转换”时,必须注意数据的截断和舍入问题。若从浮点数转换为整型值,Java会做什么呢?例如:浮点数29.7被转换为整型值,结果会是29还是30?
1 | // operators/CastingNumbers.java |
因此,答案是,从float和double转换为整数值,小数位将被截断。若你想对结果进行四舍五入,可以使用java.lang.Math的round()方法:
1 | // operators/RoundingNumbers.java |
因为round()方法是java.lang的一部分,所以我们无需通过import就可以使用。
类型提升
你会发现,如果对小于int的基本数据类型(char、byte和short)执行任何算术或按位操作,这些值会在执行操作之前类型提升为int,并且结果值的类型为int。若想重新使用较小的类型,必须使用强制转换。通常,表达式中最大的数据类型是决定表达式结果的数据类型。float类型和double型相乘,结果是double型的;int和long相加,结果是long型。
Java没有sizeof
Java不需要sizeof()方法来满足这种需求,因为所有类型的大小在不同平台上是相同的。我么不必考虑这个层次的移植问题——Java本身就是一种“与平台无关”的语言。
运算符总结
在char、byte和short类型中,我么可以看到算术运算符的“类型转换”效果。我们必须要显示强制类型转换才能将结果重新赋值为原始类型。对于int类型的运算则不用转换,因为默认就是int型。虽然我们不再停下来思考这一切是否安全,但是两个大的int型整数相乘时,结果有可能超出int型的范围,这种情况下结果会发生溢出:
1 | // operators/Overflow.java |
编译器没有报错或警告,运行时一切看起来都无异常。诚然,Java是优秀的,但是还不足够优秀。
对于char、byte或者short,混合赋值并不需要类型转换。即使为它们执行转换操作,也会获得与直接算术运算相同的结果。另外,省略类型转换可以使代码显得更加简练。总之,除boolean以外,其他任何两种类型间都可以进行类型转换。当我们向下转换类型时,需要注意结果的范围是否溢出,否则我们就很有可能在不知不觉中丢失精度。