目录
优先级
赋值
递减和递增操作符
关系操作符
逻辑操作符
字面量
字面量中的下划线
科学记数法
按位操作符
移位操作符
三元操作符
字符串操作符+和+=
类型转换操作符
截尾和舍入
本笔记参考自: 《On Java 中文版》
Java的操作符大多继承自C++,但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) / (x - 2);
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
上述程序使用了两条相似的语句,但语句的输出结果因为括号而不同:
另外,在System.out.println()语句中使用的操作符+,这里的+代表着字符串的连接(如果必要,也会进行字符串的转换)。
通过操作符=可以进行将符号右边的值赋给等号左边。其中左值必须是一个独特的命名变量(必须有一个可以储存值的物理空间)。
对于基本类型而言,赋值很好理解,因为基本类型储存的是实际的值,赋值的过程就是将内容复制到了另一个地方。
int a = 10;
int b = a;
但在为对象进行赋值的时候,情况就不一样了。因为操作对象,实际上是在操作这个对象的引用。如果“将一个对象赋值给另一个对象”,那么真正发生的是将一个引用复制到了另一个地方。例如:
class Tank {
int level;
}
public class Assignment {
public static void main(String[] args) {
Tank t1 = new Tank();
Tank t2 = new Tank();
t1.level = 10;
t2.level = 20;
System.out.println("t1.level = " + t1.level + ", t2.level = " + t2.level);
t1 = t2; //此处t1的引用关联的是t2的指向的对象,t1原本的对象被覆盖了。
System.out.println("t1.level = " + t1.level + ", t2.level = " + t2.level);
t1.level = 30;
System.out.println("t1.level = " + t1.level + ", t2.level = " + t2.level);
}
}
程序执行的结果如下:
在上述的代码中,t1 = t2将t1原本关联的对象覆盖了,此后t1关联的对象就变为了t2关联的对象。这种现象被称为“别名”,是Java操作对象的一种基本方式。若不想产生这个别名,可以这样做:
t1.level = t2.level;
但是直接操作对象内部的字段违背了Java的设计原则,这是需要注意的。
别名陷阱
将一个对象作为参数传递给方法时,也会产生别名。
class Letter {
char c;
}
public class PassObject {
static void f(Letter y) {
y.c = 'z';
}
}
在上述代码中,方法f()会在其作用域的范围内生成一个参数Letter y的副本。由于实际上传递的是一个引用,所以若方法f()中的 y.c = 'z'; 实际上会改变f()之外的对象。
Java中存在两种快捷运算符:递减操作符--(减少一个单位)和递增操作符++(增加一个单位)。这两种操作符都有两种用法,即前缀式和后缀式,用法的区别在于操作符与变量的相对位置:在变量之前是前缀,反之是后缀。
对于前缀的递增和递减,程序会先执行运算,再返回生成的结果;而对于后缀的二者而言,程序会优先返回变量的值,再执行运算。以递增为例:
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);
}
}
程序的执行结果如下:
关系操作符会根据操作数的值之间的关系生成一个布尔结果(true和false)。关系操作符有六种:<、>、<=、>=、==和!=。在Java中,等于和不等于可用于所有类型,但是其他的比较操作不适用于boolean类型。因为boolean值只有真(true)和假(false)。
对象是否相等
尽管操作符==和!=适用范围广,但是在面对一些和对象相关的比较时,就会出现问题。例如:
public class Equivalence {
static void show(String desc, Integer n1, Integer n2) {
System.out.println(desc + ":");
System.out.printf(
"%d==%d %b %b%n", n1, n2, n1 == n2, n1.equals(n2));
} //%b表示布尔值
public static void test(int value) {
Integer i1 = value;
Integer i2 = value;
show("直接创建对象时", i1, i2);
Integer r1 = Integer.valueOf(value);
Integer r2 = Integer.valueOf(value);
show("使用Integer.valueOf()创建对象时", r1, r2);
int x = value;
int y = value;
System.out.println("使用原始的int时:");
System.out.printf("%d==%d %b%n", x, y, x == y);
}
public static void main(String[] args) {
test(127);
System.out.println("\n");
test(128);
}
}
该程序的输出结果是:
上述比较之所以出现差异,关键在于:操作符==比较的是Integer对象的引用。
Integer会根据值的大小范围使用两种不同的缓存方式:
· 若值的范围在-128~127之间,Integer会使用享元模式进行缓存,因此即使多次调用Integer.valueOf(127),其生成的也是同一个对象。
· 若在上述范围之外,Integer每次产生的将是不同的对象,因此两次调用Integer.valueOf(127)返回的是不同的对象。
上述程序中,在test(127)的执行过程中,之所以操作符==可以返回正确的布尔值,不是因为该操作符进行了正确的比较,而只是因为两者的引用恰好相同罢了。因此,如果要对Integer对象进行比较,就需要使用equals()。
但equals()也不是万能的, 该方法的默认行为是比较引用。这意味着,如果对一个自定义的类使用equals(),例如:
class ValA {
int i;
}
public class EqualsMethod {
public static void main(String[] args) {
ValA va1 = new ValA();
ValA va2 = new ValA();
va1.i = va2.i = 100;
System.out.println(va1.equals(va2));
}
}
上述程序的结果是false,这就是因为默认的equals()比较的是引用。所以为了能够正确比较对象的值,就需要通过重写equals()等方式。
另一种相等比较问题发生在进行极小数的比较时。例如:
public class DoubleEquivalence {
static void show(String desc, Double n1, Double n2) {
System.out.println(desc + ":");
System.out.printf(
"%e==%e %b %b%n", n1, n2, n1 == n2, n1.equals(n2));
}
public static void test(double x1, double x2) {
System.out.printf("%e==%e %b%n", x1, x2, x1 == x2); // %e表示科学记数法
Double d1 = x1;
Double d2 = x2;
show("直接创建对象时", d1, d2);
Double r1 = Double.valueOf(x1);
Double r2 = Double.valueOf(x2);
show("使用Double.valueOf()创建对象时", r1, r2);
}
public static void main(String[] args) {
test(Double.MAX_VALUE, Double.MAX_VALUE - Double.MIN_VALUE * 1_000_000);
}
}
程序运行的结果如下:
这种错误出现的原因是因为,当一个非常大的值减去一个较小的值时,非常大的值不会出现明显变化,这就是舍入误差。而这种误差的出现,则来自于机器无法储存足够的信息来表示这种大数值的小变化。
逻辑操作符“与”(&&)、“或”(||)和“非”(!)会根据参数的逻辑关系,生成布尔值结果。但在Java中,“与”和“或”操作只能用于布尔值。
短路
逻辑操作符支持“短路”现象:一旦表达式当前部分的计算结果能准确表达整个表达式的值,那么表达式剩余的部分将不再被执行。
public class ShortCircuit {
static boolean test1(int val) {
System.out.println("test1(" + val + ")");
System.out.println("结果:" + (val < 1));
return val < 1;
}
static boolean test2(int val) {
System.out.println("test2(" + val + ")");
System.out.println("结果:" + (val < 2));
return val < 2;
}
static boolean test3(int val) {
System.out.println("test3(" + val + ")");
System.out.println("结果:" + (val < 3));
return val < 3;
}
public static void main(String[] args) {
boolean b = test1(0) && test2(2) && test3(2);
System.out.println("\n表达式的值是:" + b);
}
}
上述程序的运行结果是:
很明显,test3()没有被执行。这是因为test2()的结果是false,因此整个表达式的值肯定是false,剩下的表达式就没有必要执行了。通过这种短路的形式,可以减少资源的消耗。
一般,编译器能够准确识别一个字面量的类型。但是在一些类型模糊的情况下,就必须使用与这些字面量相关的字符来进行引导。例如:
public class Literals {
public static void main(String[] args) {
int l1 = 0x2f; // 十六进制(小写)
System.out.println("l1: " + Integer.toBinaryString(l1));
int l2 = 0x2F; // 十六进制(大写)
System.out.println("l2: " + Integer.toBinaryString(l2));
int l3 = 0177; // 八进制(前置0)
System.out.println("l3: " + Integer.toBinaryString(l3));
char c = 0xffff; // char类型的最大十六进制值
System.out.println("c: " + Integer.toBinaryString(c));
byte b = 0x7f; // byte类型的最大十六进制值
System.out.println("b: " + Integer.toBinaryString(b));
long n1 = 200L; // L是long类型后缀
long n2 = 200l; // L的大小写都可以
long n3 = 200;
float f1 = 2L;
//以此类推...
double d1 = 1D;
//以此类推...
}
}
程序执行的结果如下:
若试图将一个变量初始化为超出其范围的值,编译器会报错。上述程序展示了char和byte类型的最大十六进制值,当赋值超出了这一范围,编译器会自动将值转换为int类型,并说明本次赋值需要进行“窄化转型”。
若将较小的类型传递给方法Integer.toBinaryString()方法,该类型会被自动转换为int类型。
在Java7之后,在数字字面量里使用下划线是被允许的。这在表示大数值时大有帮助。例如,可以这样初始化:
int bin = 0b0010_1111_1010_1111_1001_1000;
在使用下划线时有几条规则:
在方法System.out.printf()中可以使用%n替代惯用的\n。这样替代的理由是在不同的平台上,换行符的表达是不同的。使用%n,Java会自动匹配各个平台的换行符,因此可以省去不必要的麻烦。
在科学与工程领域,“e”表示自然对数的基数(约为2.718)。但是在一些程序语言中(Java也包含在内),e被用来表示“10的幂次”。所以如果在Java中看到类似于1.28e-43f的表达式时,其的含义实际上是:。
一般,若编译器能够准确识别类型,那么就不需要在数值后面添加后缀名。但是如果出现类似于下方展示的情况:
float f = 1e-23f;
在编译器中,指数通常被作为double类型处理,此时尾部的f就是必须的(否则我们将收到一条报错提示)。
按位操作符可以操作整数基本类型中的单个二进制位(bit)。总共有四个操作符:按位与(&)、按位或(|)、按位异或(^)和按位非(~)。和加减乘除类似,按位操作符也可以和等号(=)联用:&=、|=和^=(~是一元操作符,不能与等号联用)。
布尔值只能进行按位与、或和异或操作,而不能执行按位非操作。另外,对于布尔值,按位操作符和逻辑操作符有相同的效果,但是按位操作符不会“短路”。
移位操作符也能操作二进制位,且只能用来处理基本类型中的整数类型。移位操作符包括了三种操作符:
当对char、byte或short这些较小的类型进行移位运算时,在开始运算前会自动将它们的类型转变为int,并且结果也是int类型。
若对int类型进行移位操作,在进行运算的时候,操作符右侧的可移位数只会用到低5位。而如果是处理long类型,那么只会用到可移位数的低6位。通过这种方式,可以放置移位超出类型允许的范围。
若使用的是<<=、>>=或>>>=,那么在对char、byte或short等类型进行移位时,可能发生截断(此时得到的结果会是-1):
public class URshift {
public static void main(String[] args) {
short s = -1;
System.out.println("没有移位时: " + Integer.toBinaryString(s));
System.out.println("正确的移位: " + Integer.toBinaryString(s >>> 10));
s >>>= 10;
System.out.println("错误的移位: " + Integer.toBinaryString(s));
}
}
程序执行的结果如下:
语句Integer.toBinaryString(s >>> 10)之所以没有出错,就是因为在移位之后,结果没有重新赋值给short类型的变量s,而是在完成运算之后直接打印,因此没有发生截断。
在程序中表示的数字的二进制形式是“有符号的二进制补码”。
三元操作符,即条件操作符,它有三个操作数。其形式如下:
boolean-exp ? value0: value1
若boolean-exp的结果是true,则执行value0,其结果就是该三元操作符的结果值;反之,则执行value1,同样得到一个来自value1的结果值。
三元操作符的应用场景主要在于从两个值中选择一个给变量赋值。
在Java中,+和+=操作符可以用来连接字符串。
C++可以通过操作符重载来达成这一功能,但由于这一特性被认为过于复杂,所以Java无法像C++这样进行操作符的重载。
若一个表达式以字符串开头,则其后面的所以操作数都得是字符串类型(编译器会强制转换):
public class StringOperators {
public static void main(String[] args) {
int x = 0, y = 1, z = 2;
String s = "x, y, z ";
System.out.println(s + x + y + z); // x、y和z的内容会被转变为字符串
System.out.println(x + " " + s); // 开头的x的内容会被转换为字符串
s += "(总和) = ";
System.out.println(s + (x + y + z)); // 使用括号控制先后顺序
System.out.println("" + x); //类似于Integer.toString()的用法,但更简单
}
}
程序执行的结果如下:
对语句System.out.println(s + x + y + z);而言,编译器会先将x、y和z转换为对应的字符串形式,这也是为什么没有发生整数求和,得到一个结果3。而在语句System.out.println(s + (x + y + z));中,由于通过括号控制了字符串转换的先后顺序,所以能够得到一个最终的结果。
在适合的时候,Java会自动将一种类型转变为另一个类型。例如将整数值赋值给浮点变量时,编译器会自动将int类型转换为float类型。
编译器可以自动地将较小的类型提升为较大的类型,但是有时候,我们会需要将较大的类型转变为较小的类型,即窄化转型:
int i = 200;
long lng = i; // 向上提升,可以省略强制类型转换
i = (int)lng; // 窄化转型,需要强制类型转换
在Java中,类型转换是比较安全的,但是在进行窄化转型时,依旧存在丢失信息的风险。这是因为较小的数据类型无法容纳更多的信息。
boolean是应该特殊的类型,它不允许任何的类型转换。除此之外,“类”类型也不被允许转换为其他类型。
通常地,若将一个较大的类型转换为较小的类型,程序往往会对数值进行截尾:
public class CastingNumbers {
public static void main(String[] args) {
double above = 0.7, below = 0.4;
System.out.println("(int)above: " + (int)above);
System.out.println("(int)below: " + (int)below);
}
}
程序执行的结果是:
在上述程序中,我们将double类型的数据强制转换为int类型,并且发生了截尾,double类型数值的小数位被截断了。
若需要进行舍入,可以使用java.lang.Math中的round()方法。