第三章 操作符
更简单的打印语句
在后面的知识会介绍静态导入(static import),这个在Java SE5中新增加的概念。
话不多说,直接上代码
import java.util.Date;
import static net.mindview.util.Print.*; // 静态导入:需要下载引入mindview的jar包
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/14 17:29
*/
public class HelloDate {
public static void main(String[] args) {
/**
* 简短的打印语句
*/
print("hello, it's:");
print(new Date());
/**
* 正常的打印语句
*/
System.out.println("你好,现在是:");
System.out.println(new Date());
}
}
Java操作符和优先级
操作符接受一个或多个参数,并生成一个新值。参数的形式与普通的方法调用不同,但效果是相同的。加号和一元的正号(+)、减号和一元的负号(一)、乘号(*)、除号(/)以及赋值号(=)的用法与其他编程语言类似。
优先级
当一个表达式存在多个操作符时,操作符的优先级就决定了各部分的计算顺序。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);
} // a = 5 b = 1
}
这两个语句看起来大体相同,但是从输出就可以看出它们具有迥然不同的含义,而这正是使用括号的结果。
在输出语句中,我们看到了“+”操作符,这意味着字符串的连接,当编译器观察到一个String后面紧跟着一个“+”,而这个“+”后面又紧跟着一个非String类型的元素,就会尝试将这个非String类型的数据转化为String类型的数据。
赋值
赋值使用操作符“=”。它的意思是“取右边的值(即右值),把它复制给左边(即左值)右值可以是任何常数、变量或者表达式(只要它能生成一个值就行)。但左值必须是一个明确的、已命名的变量。也就是说,必须有一个物理空间可以存储等号右边的值。举例来说,可将一个常数赋给一个变量:a = 4;
对基本数据类型的赋值是很简单的。基本类型存储了实际的数值,而并非指向一个对象的引用,所以在为其赋值的时候,是直接将一个地方的内容复制到了另ー个地方。例如,对基本数据类型使用a=b,那么b的内容就复制给a。若接着又修改了a,而b根本不会受这种修改的影响。
但是在为对象“赋值”的时候,情况却发生了变化。对一个对象进行操作时,我们真正操作的是对对象的引用。所以倘若“将一个对象赋值给另一个对象”,实际是将“引用”从一个地方复制到另一个地方。这意味着假若对对象使用c=d,那么c和d都指向原本只有d指向的那个对象。例如:
import static net.mindview.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/14 17:59
*/
class Tank{
int level;
}
public class Assignment {
public static void main(String[] args) {
Tank t1 = new Tank();
Tank t2 = new Tank();
t1.level = 9;
t2.level= 47;
print("1.: t1.level = "+t1.level+" t2.level = "+t2.level);
t1 = t2;
print("2.: t1.level = "+t1.level+" t2.level = "+t2.level);
t1.level = 27;
print("3.: t1.level = "+t1.level+" t2.level = "+t2.level);
}
}
1.: t1.level = 9 t2.level = 47
2.: t1.level = 47 t2.level = 47
3.: t1.level = 27 t2.level = 27
Tank类非常简单,它的两个实例(t1和t2)是在 main()里创建的。对每个Tank类对象的level域都赋予了一个不同的值,然后,将t2赋给t1,接着又修改了t1。在许多编程语言中,我们可能会期望t1和t2总是相互独立的。但由于赋值操作的是一个对象的引用,所以修改t1的同时也改变了t2!
这是由于t1和t2包含的是相同的引用,它们指向相同的对象。(原本t1包含的对对象的引用,是指向一个值为9的对象。在对t1赋值的时候,这个引用被覆盖,也就是丢失了;而那个不再被引用的对象会由“垃圾回收器”自动清理。)
这种特殊的现象通常称作“别名现象”,是Java操作对象的一种基本方式。在这个例子中,如果想避免别名问題应该怎么办呢?可以这样写:
t1.level = t2.level;
这样便可以保持两个对象彼此独立,而不是将t1和t2绑定到相同的对象。但你很快就会意识到,直接操作对象内的域容易导致混乱,并且,违背了良好的面向对象程序设计的原则。这可不是一个小问题,所以从现在开始大家就应该留意,为对象赋值可能会产生意想不到的结果。
方法调用中的别名问题
将一个对象传递给方法时,也会产生别名的问题:
class Letter{
float f;
}
public class PassObj {
static void f(Letter y){
y.f = 1.0f;
}
public static void main(String[] args) {
Letter x = new Letter();
x.f = 3.14f;
f(x);
System.out.println("x.f = "+x.f);
}
}
x.f = 1.0
在许多编程语言中,方法f()似乎要在它的作用域内复制其参数 Letter y的一个副本;但实际上只是传递了一个引用。所以代码行:
y.f = 1.0f;
实际上改变的是f()之外的对象。
算术操作符
Java的基本算术操作符与其他大多数程序设计语言是相同的。其中包括加号(+)、减号(-)、除号(/)、乘号(*)以及取模操作符(%,它从整数除法中产生余数)。整数除法会直接去掉结果的小数位,而不是四舍五入地圆整结果。
下面这个例子展示了各种算数操作符的用法:
import java.util.Random;
import static net.mindview.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/14 22:58
*/
public class MathOps {
public static void main(String[] args) {
/**
* 47是“魔幻数字”,至今为止都是这样,要想每次随机的数都不一致,可以移除47
*/
Random random = new Random(47);
int i,j,k;
j = random.nextInt(100)+1;// j的值从1-100随机取
print("j = "+j);
k = random.nextInt(100)+1;// k的值从1-100随机取
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);
i = k%j;
print("k%j = "+i);
/**
* 此处省略float、byte、short、int、long、double测试,其运算方式都差不多
*/
}
}
j = 59
k = 56
j + k = 115
j -k = 3
k/j = 0
k*j = 3304
k%j = 56
自动递增和递减
递增和递减这两个操作符各有两种使用方式,通常称为“前缀式”和“后缀式”。“前缀递增”表示操作符位于变量或表达式的前面;而“后缀递增”表示操作符位于变量或表达式的后面。类似地,“前缀递减”意味着“–”操作符位于变量或表达式的前面;而“后缀递减”意味着“–”操作符位于变量或表达式的后面。
对于前缀递增和前缀递减(如++a或–a),会先执行运算,再生成值。而对于后缀递增和后缀递减(如a++或a–),会先生成值,再执行运算。
下面是一个例子:
public class AutoInc {
public static void main(String[] args) {
int i = 1;
System.out.println("i = "+i);
System.out.println("++i = "+ ++i);
System.out.println("--i = "+ --i);
System.out.println("i++ = "+ i++);
System.out.println("i-- = "+ i--);
}
}
i = 1
++i = 2
--i = 1
i++ = 1
i-- = 2
从中可以看到,对于前缀形式,我们在执行完运算后才得到值。但对于后缀形式,则是在运算执行之前就得到值。它们是除那些涉及赋值的操作符以外,唯一具有“副作用”的操作符。也就是说,它们会改变操作数,而不仅仅是使用自己的值。
关系操作符
关系操作符生成的是一个boolean(布尔)结果,它们计算的是操作数的值之间的关系。
测试对象的等价性
关系操作符==和!=也适用于所有对象,但是这个两个操作符通常会使第一次接触的人们感到迷惑,以下是一个例子:
public class Equivalence {
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1==n2);
System.out.println(n1!=n2);
}
}
false
true
语句 System.out.println(nl==n2)将打印出括号内的比较式的布尔值结果。读者可能认为输出结果肯定先是tue,再是 false,因为两个 Integer对象都是相同的。但是尽管对象的内容相同,然而对象的引用却是不同的,而=和!=比较的就是对象的引用。所以输出结果实际上先是 false,再是true。这自然会使第一次接触关系操作符的人感到惊奇。
但是如果相比较两个对象的实际内容是否相同又该如何操作呢?此时,必须使用所有对象都适用的特殊方法 equals()。但这个方法不适用于“基本类型”,基本类型直接使用==和!=即可,下面举例说明如何使用:
public class EqualsMethod {
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1.equals(n2));
}
}
true
但是其实,事情并非我们想象的那么简单,假如你创建了自己的类:
class Value{
int i;
}
public class EqualsMethod2 {
public static void main(String[] args) {
Value v1 = new Value();
Value v2 = new Value();
v1.i = 100;
v2.i = 100;
System.out.println(v1.equals(v2));
}
}
false
这一次却是false,这是由于 equals()的默认行为是比较引用。所以除非在自己的新类中覆盖 equals方法,否则不可能表现出我们希望的行为。
逻辑操作符
逻辑操作符“与”(&&)、“或”(||)、“非”(!)能根据参数的逻辑关系,生成一个布尔值(true或 false),且只可以应用于布尔值。
短路
当使用逻辑操作符时,我们会遇到一种“短路”现象。即一旦能够明确无误地确定整个表达式的值,就不再计算表达式余下部分了。因此,整个逻辑表达式靠后的部分有可能不会被运算。下面是演示短路现象的例子:
import static net.mindview.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/16 16:05
*/
public class ShortCircuit {
static boolean test1(int val){
print("test1("+val+")");
print("result:"+(val<1));
return val<1;
}
static boolean test2(int val){
print("test2("+val+")");
print("result:"+(val<2));
return val<1;
}
static boolean test3(int val){
print("test3("+val+")");
print("result:"+(val<3));
return val<1;
}
public static void main(String[] args) {
boolean b = test1(0)&&test2(2)&&test3(3);
print("expression is:"+b);
}
}
test1(0)
result:true
test2(2)
result:false
expression is:false
你会很自然地认为所有这三个测试都会得以执行。但输出显示却并非这样。第一个测试生成结果true,所以表达式计算会继续下去。然而,第二个测试产生了一个 false结果。由于这意味着整个表达式肯定为 false,所以没必要继续计算剩余的表达式,那样只是浪费。“短路”一词的由来正源于此。事实上,如果所有的逻辑表达式都有一部分不必计算,那将获得潜在的性能提升。
直接常量
直接常量后面的后缀字符标志了它的类型。若为大写(或小写)的L,代表long(但是,使用小写字母l容易造成混淆,因为它看起来很像数字1)。大写(或小写)字母F,代表float,大写(或小写)字母D,则代表 double。
指数记法
Java采用了一种很不直观的记数法来表示指数:
public class Exponents {
public static void main(String[] args) {
float expFloat = 1.39e-43f;
System.out.println(expFloat);
double expDouble = 47e47d;
double expDouble2 = 47e47;
System.out.println(expDouble);
}
}
1.39E-43
4.7E48
其中的基数E表示10,1.39E-43表示1.39x10-43,4.7E48表示4.7x1048;
按位操作符
按位操作符用来操作整数基本数据类型中的单个“比特”(bit),即二进制位。按位操作符会对两个参数中对应的位执行布尔代数运算,并最终生成一个结果。
练习:
编写一个具有两个常量值的程序,一个具有交替的二进制位1和0,其中最低有效位为0,另一个也具有交替的二进制位1和0,但是其最低有效位为1(提示:使用十六进制常量来表示是最简单的方法)。取这两个值,用按位操作符以所有可能的方式结合运算它们,然后用 Integer. toBinarystring()显示。
import static net.mindview.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/17 13:48
*/
public class OperExp {
public static void main(String[] args) {
int a = 1+2+4+16+64;
int b = 2+8+32+128;
print("a = "+Integer.toBinaryString(a));
print("b = "+Integer.toBinaryString(b));
print("a&b = "+Integer.toBinaryString(a&b));
print("a|b = "+Integer.toBinaryString(a|b));
print("a^b = "+Integer.toBinaryString(a^b));
print("~a = "+Integer.toBinaryString(~a));
print("~b = "+Integer.toBinaryString(~b));
}
}
a = 1010111
b = 10101010
a&b = 10
a|b = 11111111
a^b = 11111101
~a = 11111111111111111111111110101000
~b = 11111111111111111111111101010101
位移操作符
移位操作符操作的运算对象也是二进制的“位”。移位操作符只可用来处理整数类型(基本类型的一种)。左移位操作符(<<)能按照操作符右侧指定的位数将操作符左边的操作数向左移动(在低位补0)。“有符号”右移位操作符(>>)则按照操作符右侧指定的位数将操作符左边的操作数向右移动。“有符号”右移位操作符使用“符号扩展”:若符号为正,则在高位插入0,若符号为负,则在高位插入1。Java中增加了一种“无符号”右移位操作符(>),它使用“零扩展”:无论正负,都在高位插入0。这一操作符是C或C++中所没有的。
如果对char、byte或者 short类型的数值进行移位处理,那么在移位进行之前,它们会被转换为int类型,并且得到的结果也是一个int类型的值。只有数值右端的低5位才有用。这样可防止我们移位超过int型值所具有的位数。(译注:因为2的5次方为32,而int型值只有32位。)若对一个long类型的数值进行处理,最后得到的结果也是long。此时只会用到数值右端的低6位,以防止移位超过long型数值具有的位数。
三元操作符 if-else
三元操作符也称为条件操作符,它显得比较特别,因为它有三个操作数;但它确实属于操作符的一种,因为它最终也会生成一个值,这与本章下一节中介绍的普通if-else语句是不同的。其表达式采取下述形式:
boolean-exp ? value0:value1;
如果 boolean-exp(布尔表达式)的结果为true,就计算 value0,而且这个计算结果也就是操作符最终产生的值。如果 boolean-exp的结果为 false,就计算 value1,同样,它的结果也就成为了操作符最终产生的值。
条件操作符和if-else完全不同,因为它会产生一个值,下面是对这两者进行的比较示例:
import static net.mindview.util.Print.print;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/17 16:29
*/
public class TernaryIfElse {
static int ternary(int i){
return i<10? i*100:i*10;
}
static int ifElse(int i){
if(i<10)
return i*100;
else
return i*10;
}
public static void main(String[] args) {
print(ternary(9));
print(ternary(10));
print(ifElse(9));
print(ifElse(10));
}
}
900
100
900
100
可以看出,上面的 ternary()中的代码与 ifElse()中不用三元操作符的代码相比,显得更加紧凑;但 ifElse()更易理解,而且不需要太多的录入。所以在选择使用三元操作符时,请务必仔细考虑。
类型转换操作符
**类型转換(cast)**的原意是“模型铸造”。在适当的时候,Java会将一种数据类型自动转换成另一种。例如,假设我们为某浮点变量赋以一个整数值,编译器会将int自动转换成float。类型转换运算允许我们显式地进行这种类型的转换,或者在不能自动进行转换的时侯强制进行类型转换。
import static net.mindview.util.Print.print;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/17 16:35
*/
public class Casting {
public static void main(String[] args) {
double above = 0.7,below = 0.4;
float fabove = 0.7f,fbelow = 0.4f;
print((int)above);
print((int)below);
print((int)fabove);
print((int)fbelow);
}
}
以上输出的结果全是0,如果在讲float或double转化为整型值时,总是对数字执行截尾。如果想要得到舍入的结果,那就需要用到Math.round()方法:
import static net.mindview.util.Print.print;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/17 16:35
*/
public class Casting {
public static void main(String[] args) {
double above = 0.7,below = 0.4;
float fabove = 0.7f,fbelow = 0.4f;
print(Math.round(above));
print(Math.round(below));
print(Math.round(fabove));
print(Math.round(fbelow));
}
}
1
0
1
0
如果对基本数据类型执行算术运算或按位运算,大家会发现,只要类型比int小(即char、byte或者 short),那么在运算之前,这些值会自动转换成int。这样一来,最终生成的结果就是int类型。如果想把结果赋值给较小的类型,就必须使用类型转换(既然把结果赋给了较小的类型,就可能出现信息丢失)。通常,表达式中出现的最大的数据类型决定了表达式最终结果的数据类型。如果将一个float值与ー个 double值相乘,结果就是 double,如果将一个int和一个long值相加,则结果为long。
练习
编写一个接收两个字符串参数的方法,用各种布尔值的比较关系来比较这两个字符串,然后把结果打印出来。做=和!=比较的同时,用 equals()测试。在main()里面用几个不同的字符串对象调用这个方法。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/17 16:56
*/
public class StringCompare {
static void f(boolean b){
if(b==true){
System.out.println(true);
}else
System.out.println(false);
}
static void stringTest(String str1,String str2){
f(str1==str2);
f(str1!=str2);
f(str1.equals(str2));
f(str2.equals(str1));
}
public static void main(String[] args) {
String str1 = "one";
String str2 = "two";
stringTest(str1,str2);
}
}
false
true
false
false