注:本博客内容是本人在看《Jave编程思想》这本书时从该书上抄录下来的一些片段。这里也强烈建议各位读者去购买这本书进行阅读学习。
一、Java操作符
操作符接受一个或多个参数,并生成一个新值。参数的形式与普通的方法调用不同,但效果相同。加号和一元的正号(+)、减号和一元的负号(-)、乘号(*)、除号(/)以及赋值号(=)的用法与其他编程语言一致。
操作符作用于操作数,生成一个新值。另外,有些操作符可能会改变操作数自身的值,这被称为“副作用”。那些能改变其操作数的操作符,最普遍的用途就是用来产生副作用;但要记住,使用此类操作符生成的值,与使用没有副作用的操作符生成的值没有什么区别。
几乎所有的操作符都只能操作“基本类型”。例外的操作符是"=",“==”和“!=”,这些操作符能操作所有对象。除此之外,String类型支持“+”和“+=”,用于字符串拼接。
二、优先级
当一个表达式中存在多个操作符时,操作符的优先级决定了各部分的计算顺序。Java对计算顺序做了特别的规定。其中,最简单的规则就是先乘除后加减。可以使用括号明确规定计算的顺序。使用括号时优先计算括号中的数据。例如:
@Test
public void precedence() {
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);
System.out.println("b = " + b);
}
输出结果
a = 5
b = 1
这两个语句看起来大体相同,但是输出结却大不相同,而这正是使用括号的结果。
三、赋值
赋值使用操作符“=”。它表示:取右边的值(即右值),把它复制给左边(即左值)。右值可以是任何常数、变量或者表达式(只要该表达式能生成一个值就行)。但是左值必须是明确的、已命名的变量。也就是说,必须有一个物理空间可以存储等号右边的值。举一个简单的例子,例如将一个常量赋值给一个变量:
a = 4;
但是不能把任何东西赋值给一个常量,常量不能作为左值(比如说不能说4=a)。
对于基本数据类型的赋值是很简单的。基本类型存储了实际的数值,而非指向一个对象的引用,所以在为其赋值的时候,是直接将一个地方的内容复制到另一个地方。例如,对基本数据类型使用a=b,那么b的内容就复制给a。若接着修改了a,而b根本不会受这种修改的影响。而这正是大多数情况下我们所期望的。
但是在为对象“赋值”的时候,情况发生了变化。对一个对象就行操作时,我们真正操作的的是对象的引用。所以倘若“将一个对象赋值给另一个对象”,实际上就是将“引用”从一个地方复制到另一个地方。这意味着假如对对象使用c=d,那么c和d都指向原本只有d指向的那个对象。示例代码如下:
定一个Task.java类:
public class Task {
int level;
}
测试代码:
public class TaskTest {
public static void main(String[] args) {
Task task1 = new Task();
Task task2 = new Task();
task1.level = 20;
task2.level = 40;
System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
// 将task2赋值给task1
task1 = task2;
System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
// 修改task1
task1.level = 22;
System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
}
}
输出结果:
task1.level = 20, task2.level = 40
task1.level = 40, task2.level = 40
task1.level = 22, task2.level = 22
该测试类非常简单。创建两个Task对象task1和task2,分别对它们的level赋予不同的值,然后将task2赋值给task1,紧接着又修改了task1.在许多编程语言中,我们可能期望task1和task2是相互独立的。但是由于赋值操作的是一个对象的引用,所以修改task1的同时也修改task2。这是由于task1和task2包含的是相同的引用,它们指向相同的对象。(原本task1包含的对对象的引用,是指向一个值为20的对象,但是在对task1赋值的时候,即task1=task2时,这个引用被覆盖(task1指向task2对象的引用),也就丢失了;而那个不再被引用的对象会被“垃圾回收器”自动清理)。
这种特殊的现象通常被称为“别名现象”,这是Java操作对象的一种基本方式。在该例中,若想避免别名现象问题该如何做呢?可以这样写:
public class TaskTest {
public static void main(String[] args) {
Task task1 = new Task();
Task task2 = new Task();
task1.level = 20;
task2.level = 40;
System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
// 将taks1 = task2修改为 task1.level = task2.level;
task1.level = task2.level;
System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
// 修改task1
task1.level = 22;
System.out.println("task1.level = " + task1.level + ", task2.level = " + task2.level);
}
}
输出结果:
task1.level = 20, task2.level = 40
task1.level = 40, task2.level = 40
task1.level = 22, task2.level = 40
丛输出结果来看使用task1.level = task2.level的方式可以保持对象的彼此独立,而不是将task1和task2绑定到相同的对象。但是,这样直接操作对象内的域容易导致混乱,并且违背了良好的面向对象程序设计的原则。
方法调用中的别名现象
将一个对象传递给方法时,也会产生别名现像:例如:
创建一个简单的Letter对象:
public class Letter {
char ch;
}
测试方法:
public class MothedTest {
static void testReference(Letter letter) {
letter.ch = 'x';
}
public static void main(String[] args) {
Letter letter = new Letter();
letter.ch = 'a';
System.out.println("before invoke letter.ch = " + letter.ch);
// 调用方法
testReference(letter);
System.out.println("after invoke letter.ch = " + letter.ch);
}
}
测试结果:
before invoke letter.ch = a
after invoke letter.ch = x
在许多编程语言中,方法testReference()似乎要在它的作用域内复制其参数Letter letter的一个副本;但实际上只是传递了一个引用。所以代码行:
letter.ch = 'x';
实际上改变的是testReference()之外的对象。
别名现象引起的问题及其解决方法是比较复杂,这里先不做相关介绍。但是你至少应该知道它的存在,并在使用中注意这个陷阱。
四、算术操作符
Java的基本算术操作符与其他大多数编程语言是相同的。其中包括加号(+)、减号(-)、乘号(*)、除号(/)以及取模操作符(%,它从整数除法中产生余数)。整数除法会直接去掉结果的小数位,而不是四舍五入的结果。
Java也使用一种来自C和C++的简化符号同时进行运算和赋值操作。这用操作符后紧跟一个等号来表示,它对Java中的所有操作符都适用。例如,要将x加4,并将结果赋回给x,可以这么些:x += 4。
下面一些例子展示了各种算术操作符的用法:
@Test
public void multOperator () {
Random random = new Random(47);
int i, j, k;
// 从1到100中取一个值
j = random.nextInt(100) + 1;
System.out.println("j = " + j);
k = random.nextInt(100) + 1;
System.out.println("k = " + k);
// 加
i = j + k;
System.out.println("j + k = " + i);
// 减
i = j - k;
System.out.println("j - k = " + i);
// 乘
i = j * k;
System.out.println("j * k = " + i);
// 除
i = j / k;
System.out.println("j/k = " + i);
// 取模(或求余)
i = j % k;
System.out.println("j%k = " + i);
// 浮点数测试
float u, v, w;
v = random.nextFloat();
System.out.println("v = " + v);
w = random.nextFloat();
System.out.println("w = " + w);
// 加
u = v + w;
System.out.println("v + w = " + u);
// 减
u = v -w;
System.out.println("v - w = " + u);
// 乘
u = v * w;
System.out.println("v * w = " + u);
// 除
u = v / w;
System.out.println("v/w = " + u);
// 运算与赋值操作符
// +=
u += v;
System.out.println("u +=v :" + u);
// -=
u -= v;
System.out.println("u -=v :" + u);
// *=
u *= v;
System.out.println("u *=v :" + u);
// /=
u /= v;
System.out.println("u /=v :" + u);
// %=;
i %= j;
System.out.println("i %=j :" + i);
}
输出结果如下:
j = 59
k = 56
j + k = 115
j - k = 3
j * k = 3304
j/k = 1
j%k = 3
v = 0.5309454
w = 0.0534122
v + w = 0.5843576
v - w = 0.47753322
v * w = 0.028358962
v/w = 9.940527
u +=v :10.471473
u -=v :9.940527
u *=v :5.2778773
u /=v :9.940527
i %=j :3
一元加、减操作符
一元减号(-)和一元加号(+)与二元减号和二元加号都使用相同的符号。根据表达式的书写形式,编辑器会自动判断出使用哪一种。例如:
int a = 10;
int x = -a;
System.out.println("x = " + x);
输出结果:
x = -10
再看一下如下例子:
int a = 10;
int b = 2;
x = a * -b;
System.out.println("a * -b = " + x);
输出结果:
a * -b = -20
x = a * -b的写法虽然编辑器可以正确的识别,但是读者可能会被搞糊涂,所以有时可以明确地写成:
x = a * (-b);
System.out.println("a * (-b) = " + x);
其输出结果是一样的:
a * (-b) = -20
一元减号用于转变数据的符号,而一元加只是为了与一元减号相对应,但是它唯一的作用仅仅是将较小类型的操作数提升为int类型。
五、自动递增和递减
递增和递减运算是两种相当不错的快捷运算(通常被称为“自动递增”和“自动递减”运算)。其中递减操作符是“--”,意为“减少一个单位”;递增操作符是“++”,意为“增加一个单位”。例如:假设a是一个int类型的值,则表达式++a等价于(a = a +1)。递增和递减操作符不仅改变了变量,而且以变量的值作为生成的结果。
这两种操作符有两种用法,通常被称为“前缀式”和“后缀式‘。”前缀式递增“表示”++“操作符位于变量或表达式的前面;而”后缀式递增“表示”++“操作符位于变量或表达式的后面。类似地,”前缀式递减“意味着”--“操作符位于变量或表达式的前面;而”会缀式递减“意味着”--“操作符位于变量或表达式的后面。对于前缀式递增和前缀式递减(如++a或--a),回先执行计算,再生成值。而对于后缀式递增和后缀式递减(如a++或a--),会先生成值,再执行运算。例如:
@Test
public void autoInc () {
int i = 1;
System.out.println("i :" + i); // 1
System.out.println("++ i : " + ++ i); // 先自增 2
System.out.println("i ++ : " + i ++); // 后自增 2
System.out.println("i :" + i); // 3
System.out.println("-- i : " + -- i); // 先自减 2
System.out.println("i -- : " + i --); // 后自减 2
System.out.println("i : " + i); // 1
}
执行结果:
i :1
++ i : 2
i ++ : 2
i :3
-- i : 2
i -- : 2
i : 1
六、关系操作符
关系操作符生成的是一个boolean(布尔)结果,它们计算的是操作数之间的关系。如果关系是真实的的,关系表达式会生成true(真);如果关系不真实,则生成false(假)。关系操作符包括小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有的数据类型,而其他比较符不适用于boolean类型。因为boolean值只能为true和false。“大于”和“小于”没有实际意义。
测试对象的等价性。
关系操作符==和!=也适用于所有对象,但是这两个操作符通常会使第一次接触Java的程序员感到迷惑。例如:
@Test
public void equalsObject() {
Integer t1 = new Integer(50);
Integer t2 = new Integer(50);
System.out.println("t1 == t2 :" + (t1 == t2));
System.out.println("t1 != t2 :" + (t1 != t2));
}
输出结果:
t1 == t2 :false
t1 != t2 :true
一些读者可能会认为上面程序输出的结果应该先是true,再是false,因为两个Integer对象都是相同的。但是尽管对象的内容相同,然而对象的引用却是不同的,而==和!=比较的就是对象的引用。所以输出结果实际上先是false,再是true。
如果想要比较两个对象的实际类容是否相同,又该如何操作呢?此时,必须使用所有对象都有的特殊方法equals()。但是这个方法不适用于“基本数据类型”,基本数据类型直接使用==和!=即可。如:
@Test
public void equalsObject() {
Integer t1 = new Integer(50);
Integer t2 = new Integer(50);
System.out.println("t1的值和t2的值是否相当?" + t1.equals(t2));
}
输出结果:
t1的值和t2的值是否相当?true
结果正如我们所预料的那样。但事情并不总是这么简单。假设你创建了自己的类。如下这样:
class Value {
int v;
}
@Test
public void compareObject() {
Value value1 = new Value();
Value value2 = new Value();
value1.v = value2.v = 100;
System.out.println(value1.equals(value2));
}
输出结果:
flase
事情再次变得让人费解了:结果又是false,而不是true!这是由于equals()方法的默认行为是比较引用。所以除非在自己的新的类中覆盖equals()方法,否则不可能表现出我们希望的样子。至于如何重写覆盖equals()方法,后续章节再继续介绍。这里你需要注意equals()方法的这种行为表现方式,这样或许能够避免一些“灾难”。
七、逻辑操作符
逻辑操作符“与”(&&)、“或”(||)、非(!)能够根据参数的逻辑关系,生成一个布尔值(true或false)。“与”、“或”、“非”操作只可应用于布尔值。
逻辑操作符的短路现象
当我们使用逻辑操作符的时候,我们会遇到一种“短路”现象。即一旦能够明确确认整个表达式的值,就不会在计算表达式余下的部分了。因此,整个逻辑表达式靠后的部分有可能不会被运算。以下是一个简单的短路现象例子:
@Test
public void shortCircuit() {
System.out.println(1 == 1 && 1 > 2 && test01());
}
public boolean test01() {
System.out.println("3 > 4 ?");
return 3 > 4;
}
输出结果:
false
正常情况下如果三个测试语句都会执行。那么肯定会输出“3 > 4”这句字符串,但是从输出结果来看,并没有输出“3 > 4”。这里第一个判断1 ==1 等式成立,所以表达式会继续执行下去。然而第一个测试结果产生了一个false结果。由于这是三个“于”(&&)进行运算,有一个为false,那么就可以肯定表达式最终结果是false,所以没有必要继续执行下去,那样只是浪费。“短路”一词正是源于此。事实上,如果所有的逻辑表达式都有一部风不必计算,那么将获得潜在的性能提升。
八、直接常量
一般来说,如果在程序里使用“直接常量”,编译器可以准确地知道要生成什么样的类型,但有时候却是模凌两可的。如果发生这种情况,必须对编译器加以适当的“指导”,用与直接常量相关联的某些字符来额外增加一些信息。例如:
@Test
public void literals () {
int t1 = 0x2f;
System.out.println("t1 :" + Integer.toBinaryString(t1));
int t2 = 0X2f;
System.out.println("t2 :" + Integer.toBinaryString(t2));
int t3 = 0177;
System.out.println("t3 :" + Integer.toBinaryString(t3));
char c = 0xffff;
System.out.println("c :" + Integer.toBinaryString(c));
byte b = 0x7f;
System.out.println("b :" + Integer.toBinaryString(b));
short s = 0x7fff;
System.out.println("s :" + Integer.toBinaryString(s));
long l1 = 200;
long l2 = 200l;
long l3 = 200L;
System.out.println("l3 :" + l3);
float f1 = 1;
float f2 = 1f;
float f3 = 1F;
System.out.println("f3 :" + f3);
double d1 = 1d;
double d2 = 1D;
System.out.println("d2 :" +d2);
}
输出结果如下:
t1 :101111
t2 :101111
t3 :1111111
c :1111111111111111
b :1111111
s :111111111111111
l3 :200
f3 :1.0
d2 :1.0
直接常量后面的后缀字符标记了它的类型。若为大写(或小写)的L,代表的long(但是使用小写的字面l容易造成混淆,因为它看起来像数字1,所以建议使用大写L)。大写(或小写)字母F,代表float;大写(或小写)字母D,代表double。十六进制数适用于所有整数数据类型,以前缀0x(0X),后面跟随0-9会小写(或大写)的a-f(A-F)来表示。八进制数由前缀0以及后续的0~7的数字来表示。
在C、C++和Java中,二进制数没有直接常量表示方法。但是使用十六进制和八进制记数时,以二进制形式显示结果非常有用。通常使用Integer和Long类型的静态方法toBinaryString()方法可以很容易实现这一点。请注意,如果将较小的类型传递给Integer.toBinaryString()方法,则该类型将自动转为int。
九、按位操作符
按位操作符用来操作基本数据类型中的单个“比特”(bit),即二进制位。按位操作符会对两个参数中对应的位执行布尔代数运算,并最终生成一个结果。
如果两个输入位都是1,则按位“与”操作符(&)生成一个输出位1;否则生成一个输出位0(即“与“与”操作符只有1 & 1才等于1,其他的都为0);如果两个输入为里只要有一个是1,则按位“或”操作符(|)生成一个输出为1,只有在两个输入位都为0时,它才会生成一个输出为0;如果输入位的某一个是1,但不全都是1,那么按位“异或”操作(^)生成一个输出为1,有且仅当两个输入位都是1是,按位“异或”才输出一位0;按位“非”(~),也称为取反操作符,它属于一元操作符,只对一个操作数进行操作(其他按位操作符都是二元操作符),按位“非”生成与输入位相反的值——如输入0,则输出1,若输入1,则输出0。
按位操作符可以与等号(=)联合使用,以便合并运算赋值:&=、|=和^=都是合法的(由于‘~“是一元操作符,所以不可以和“=”联合使用)。
我们将布尔类型作为一种单比特值对待,所以它有点特殊。我们可以对布尔类型的值(true和false)执行按位“与”、按位“或”和按位“异或”,但不能执行按位“非”(大概是为了避免与逻辑NOT混淆)。对于布尔值,按位操作符具有与逻辑操作符相同的效果,只是它们不会“短路”。
十、位移操作符
位移操作符的运算对象也是二进制的“位”。位移操作符只可用来处理整数类型。左位移操作符(<<)能够按照操作符右侧指定的位数将操作符左边的操作数向左移动(在低位补0)。“有符号”右移(>>)则按照操作符右侧指定的位数将操作符左边的操作数向右移动。“有符号”右移位操作符使用“符号扩展”:若符号位正,则在高位补0;若符号为负,则在高位补1。Java中增加了一种“无符号”右移位操作符(>>>),它使用“零扩展”:无论正负,都在高位插入0。这是C和C++没有的。
”位移“可以和等号“=”组合使用(<<=或>>=或>>>=)。此时,操作符左边的值会移动由右边的指定的位数,再将得到的结果赋值给左边的变量。但是在进行“无符号”右移位结合赋值操作时,可能会遇到一个问题:如果对byte或short值进行这样的位移运算,得到的可能不是正确的结果。它们会被先转成int类型,在进行右移操作,然后被截断,赋值给原来的类型,在这种情况下可能得到一个-1的结果。
十一、三元操作符
三元操作符也称为条件操作符,它显得比较特殊,因为它有三个操作数;但它确实属于操作符的一种,因为它最终也会生成一个值。其表达形式如下:
boolean-exp ? value0 : value1;
如果boolean-exp(布尔表达式)的结果为true,就计算value0,而且这个计算结果也就是操作符最终产生的值;如果boolean-exp(布尔表达式)的结果是false,就计算value1,同样,它的结果也就成了最终操作符产生的值。例如:
@Test
public void ternaryIfElse() {
int result = 1 > 2 ? 100 : 200;
System.out.println("result = " +result);
}
执行结果:
result = 200
十二、字符串操作符+和+=
在Java中+和+=有一项特殊的用途:连接不同的字符串。字符串操作符有一些有趣的行为。如果表达式以一个字符串开头,那么后续的所有操作数都必须是字符串类型(请记住,编译器会把双引号内的字符序列自动转成字符串)。例如:
@Test
public void stringOperator() {
int x = 1, y = 2, z = 3;
String str = "x , y , z ";
System.out.println(str + x + y + z);
str += "hello ";
System.out.println(str + (x + y + z));
}
输出结果:
x , y , z 123
x , y , z hello
x , y , z hello 6
请注意,第一个打印结果输出得是x , y , z 123,而不是x , y , z 6(1 + 2 + 3 = 6),之所以出现这个结果是因为操作符“+”的起始是一个字符串,所以编译器就会将x,y,z的值转换成它们的字符串形式,然后连接这些字符串。最有一个之所以输出结果为6,是因为使用括号来控制表达式的赋值顺序,以使得int类型的变量在显示之前确实进行了求和运算。
十三、类型转换操作符
类型转换(cast)的愿意是“模型铸造”。在适当的时候,Java会将一种数据类型自动转换成另一种。例如,假设我们为某浮点数赋予一个整数值,编译器会将int自动转换成float。类型转换运算应许我们显示地进行这种转换,或者在不能自动转换的时候强制进行类型转换。
要想执行类型转换,需要将希望得到的类型置于圆括号内,放在要进行类型转换的值的左边。例如:
@Test
public void casting () {
int i = 200;
long lng = (long) i;
long l = 100;
int j = (int) l;
}
需要注意的是,如果要执行一种名为窄化转换的操作(也就是说,将能够容纳更多信息的数据类型转换成无法容纳那么多信息的类型),就有可能面临信息丢失的危险。此时,编译器会强制我们进行类型转换,这实际上是说:“这可能是一件危险的事情,如果无论如何都要这么做,必须显示进行类型转换”。而对于扩展转换,则不必显示地进行类型转换,因为新的类型肯定能够容纳原来的类型,不会造成任何信息丢失。
Java允许我们把任何基本数据类型转换成别的其他的基本数据类型,但布尔类型除外,后者根本不允许进行任何类型的转换处理。
(1)截尾和舍如
在执行窄化转换时,必须注意截尾和舍入问题。例如,如果将一个浮点值转换成整型值,Java会进行如何处理?例如将23.7转换成int,结果会是23还是24?看代码如下:
@Test
public void castingNumber() {
double value = 23.7, value1 = 23.4;
float fl = 0.7f, lowe = 0.4f;
System.out.println("(int) value = " + (int) value);
System.out.println("(int) value1 = " + (int) value1);
System.out.println("(int) fl = " + (int) fl);
System.out.println("(int) lowe = " + (int)(lowe));
}
输出结果:
(int) value = 23
(int) value1 = 23
(int) fl = 0
(int) lowe = 0
因此答案是在将float和double类型转换成int类型是,总是对该数执行截尾操作。如果想要达到四舍五入的值,就需要使用java.lang.Math中的roound()方法。如:
@Test
public void mathRound() {
double value = 23.7, value1 = 23.4;
float fl = 0.7f, lowe = 0.4f;
System.out.println("Math.round(value) = " + Math.round(value));
System.out.println("Math.round(value1) = " + Math.round(value1));
System.out.println("Math.round(fl) = " + Math.round(fl));
System.out.println("Math.round(lowe) = " + Math.round(lowe));
}
输出结果:
Math.round(value) = 24
Math.round(value1) = 23
Math.round(fl) = 1
Math.round(lowe) = 0
(2)提升
如果对基本数据类型进行算术运算或者按位运算,大家就会发现,只要类型比int小(即char、byte或者short),那么在运算之前,这些值会自动转换成int。这样一来,最终生成的结果就是int类型。如果想要把结果赋值给较小的类型,就必须使用类型转换(既然把结果赋给了较小的类型,就可能出现信息丢失)。通常,表达式中出现的最大的数据类型决定了表达的最终结果的数据类型。如果将一个float值与一个double值相乘,结果就是double;如果将一个int和一个long值相加,则结果就是long。
在Java中若不使用直接常量做显示声明,默认定义的整数的数据类型是int类型,小数默认是double类型。