Skip to content

Files

Latest commit

ad48acc · Jul 15, 2021

History

History
1216 lines (1028 loc) · 51 KB

File metadata and controls

1216 lines (1028 loc) · 51 KB

3.1 使用Java运算符

运算符以一个或多个参数为基础,可生成一个新值。参数采用与原始方法调用不同的一种形式,但效果是相同的。根据以前写程序的经验,运算符的常规概念应该不难理解。

加号(+)、减号和负号(-)、乘号(*)、除号(/)以及等号(=)的用法与其他所有编程语言都是类似的。

所有运算符都能根据自己的运算对象生成一个值。除此以外,一个运算符可改变运算对象的值,这叫作“副作用”(Side Effect)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。 几乎所有运算符都只能操作“基本类型”(Primitives)。唯一的例外是===!=,它们能操作所有对象(也是对象易令人混淆的一个地方)。除此以外,String类支持++=

3.1.1 优先级

运算符的优先级决定了存在多个运算符时一个表达式各部分的计算顺序。Java对计算顺序作出了特别的规定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定计算顺序。例如:

A = X + Y - 2/2 + Z;

为上述表达式加上括号后,就有了一个不同的含义。

A = X + (Y - 2)/(2 + Z);

3.1.2 赋值

赋值是用等号运算符(=)进行的。它的意思是“取得右边的值,把它复制到左边”。右边的值可以是任何常数、变量或者表达式,只要能产生一个值就行。但左边的值必须是一个明确的、已命名的变量。也就是说,它必须有一个物理性的空间来保存右边的值。举个例子来说,可将一个常数赋给一个变量(A=4;),但不可将任何东西赋给一个常数(比如不能4=A)。

对主数据类型的赋值是非常直接的。由于基本类型容纳了实际的值,而且并非指向一个对象的引用,所以在为其赋值的时候,可将来自一个地方的内容复制到另一个地方。例如,假设为基本类型使用A=B,那么B处的内容就复制到A。若接着又修改了A,那么B根本不会受这种修改的影响。作为一名程序员,这应成为自己的常识。

但在为对象“赋值”的时候,情况却发生了变化。对一个对象进行操作时,我们真正操作的是它的引用。所以倘若“从一个对象到另一个对象”赋值,实际就是将引用从一个地方复制到另一个地方。这意味着假若为对象使用C=D,那么CD最终都会指向最初只有D才指向的那个对象。下面这个例子将向大家阐示这一点。

这里有一些题外话。在后面,大家在代码示例里看到的第一个语句将是package 03使用的package语句,它代表本书第3章。本书每一章的第一个代码清单都会包含象这样的一个package(封装、打包、包裹)语句,它的作用是为那一章剩余的代码建立章节编号。在第17章,大家会看到第3章的所有代码清单(除那些有不同封装名称的以外)都会自动置入一个名为c03的子目录里;第4章的代码置入c04;以此类推。所有这些都是通过第17章展示的CodePackage.java程序实现的;“封装”的基本概念会在第5章进行详尽的解释。就目前来说,大家只需记住象package 03这样的形式只是用于为某一章的代码清单建立相应的子目录。

为运行程序,必须保证在classpath里包含了我们安装本书源码文件的根目录(那个目录里包含了c02c03cc04等等子目录)。 对于Java后续的版本(1.1.4和更高版本),如果您的main()package语句封装到一个文件里,那么必须在程序名前面指定完整的包裹名称,否则不能运行程序。在这种情况下,命令行是:

java c03.Assignment

运行位于一个“包裹”里的程序时,随时都要注意这方面的问题。 下面是例子:

//: Assignment.java
// Assignment with objects is a bit tricky
package c03;

class Number {
  int i;
}

public class Assignment {
  public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    n1.i = 9;
    n2.i = 47;
    System.out.println("1: n1.i: " + n1.i +
      ", n2.i: " + n2.i);
    n1 = n2;
    System.out.println("2: n1.i: " + n1.i +
      ", n2.i: " + n2.i);
    n1.i = 27;
    System.out.println("3: n1.i: " + n1.i +
      ", n2.i: " + n2.i);
  }
} ///:~

Number类非常简单,它的两个实例(n1n2)是在main()里创建的。每个Number中的i值都赋予了一个不同的值。随后,将n2赋给n1,而且n1发生改变。在许多程序设计语言中,我们都希望n1n2任何时候都相互独立。但由于我们已赋予了一个引用,所以下面才是真实的输出:

1: n1.i: 9, n2.i: 47
2: n1.i: 47, n2.i: 47
3: n1.i: 27, n2.i: 27

看来改变n1的同时也改变了n2!这是由于无论n1还是n2都包含了相同的引用,它指向相同的对象(最初的引用位于n1内部,指向容纳了值9的一个对象。在赋值过程中,那个引用实际已经丢失;它的对象会由“垃圾收集器”自动清除)。

这种特殊的现象通常也叫作“别名”,是Java操作对象的一种基本方式。但假若不愿意在这种情况下出现别名,又该怎么操作呢?可放弃赋值,并写入下述代码:

n1.i = n2.i;

这样便可保留两个独立的对象,而不是将n1n2绑定到相同的对象。但您很快就会意识到,这样做会使对象内部的字段处理发生混乱,并与标准的面向对象设计准则相悖。由于这并非一个简单的话题,所以留待第12章详细论述,那一章是专门讨论别名的。其时,大家也会注意到对象的赋值会产生一些令人震惊的效果。

1. 方法调用中的别名处理

将一个对象传递到方法内部时,也会产生别名现象。

//: PassObject.java
// Passing objects to methods can be a bit tricky

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';
    System.out.println("1: x.c: " + x.c);
    f(x);
    System.out.println("2: x.c: " + x.c);
  }
} ///:~

在许多程序设计语言中,f()方法表面上似乎要在方法的作用域内制作自己的参数Letter y的一个副本。但同样地,实际传递的是一个引用。所以下面这个程序行:

y.c = 'z';

实际改变的是f()之外的对象。输出结果如下:

1: x.c: a
2: x.c: z
```java

别名和它的对策是非常复杂的一个问题。尽管必须等至第12章才可获得所有答案,但从现在开始就应加以重视,以便提早发现它的缺点。

**3.1.3 算术运算符**

Java的基本算术运算符与其他大多数程序设计语言是相同的。其中包括加号(`+`)、减号(`-`)、除号(`/`)、乘号(`*`)以及模数(`%`,从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。

Java也用一种简写形式进行运算,并同时进行赋值操作。这是由等号前的一个运算符标记的,而且对于语言中的所有运算符都是固定的。例如,为了将4加到变量`x`,并将结果赋给`x`,可用:`x+=4`。

下面这个例子展示了算术运算符的各种用法:

//: MathOps.java // Demonstrates the mathematical operators import java.util.*;

public class MathOps { // Create a shorthand to save typing: static void prt(String s) { System.out.println(s); } // shorthand to print a string and an int: static void pInt(String s, int i) { prt(s + " = " + i); } // shorthand to print a string and a float: static void pFlt(String s, float f) { prt(s + " = " + f); } public static void main(String[] args) { // Create a random number generator, // seeds with current time by default: Random rand = new Random(); int i, j, k; // '%' limits maximum value to 99: j = rand.nextInt() % 100; k = rand.nextInt() % 100; pInt("j",j); pInt("k",k); i = j + k; pInt("j + k", i); i = j - k; pInt("j - k", i); i = k / j; pInt("k / j", i); i = k * j; pInt("k * j", i); i = k % j; pInt("k % j", i); j %= k; pInt("j %= k", j); // Floating-point number tests: float u,v,w; // applies to doubles, too v = rand.nextFloat(); w = rand.nextFloat(); pFlt("v", v); pFlt("w", w); u = v + w; pFlt("v + w", u); u = v - w; pFlt("v - w", u); u = v * w; pFlt("v * w", u); u = v / w; pFlt("v / w", u); // the following also works for // char, byte, short, int, long, // and double: u += v; pFlt("u += v", u); u -= v; pFlt("u -= v", u); u *= v; pFlt("u *= v", u); u /= v; pFlt("u /= v", u); } } ///:~

我们注意到的第一件事情就是用于打印显示的一些快捷方法:`prt()`方法打印一个`String`;`pInt()`先打印一个`String`,再打印一个`int`;`pFlt()`先打印一个`String`,再打印一个`float`。当然它们最终都要用`System.out.println()`结尾为生成数字程序首先会创建一个`Random`(随机对象由于参数是在创建过程中传递的所以Java将当前时间作为一个种子值”,由随机数生成器利用通过`Random`对象程序可生成许多不同类型的随机数字做法很简单只需调用不同的方法即可:`nextInt()`,`nextLong()`,`nextFloat()`或者`nextDouble()`。

若随同随机数生成器的结果使用模数运算符(`%`)可将结果限制到运算对象减1的上限本例是99之下。

**1. 一元加减运算符**

一元减号(`-`)和一元加号(`+`)与二元加号和减号都是相同的运算符根据表达式的书写形式编译器会自动判断使用哪一种例如下述语句

x = -a;

它的含义是显然的编译器能正确识别下述语句

x = a * -b;

但读者会被搞糊涂所以最好更明确地写成

x = a * (-b);

一元减号得到的运算对象的负值一元加号的含义与一元减号相反虽然它实际并不做任何事情。

## 3.1.4 自动递增和递减

和C类似Java提供了丰富的快捷运算方式这些快捷运算可使代码更清爽更易录入也更易读者辨读两种很不错的快捷运算方式是递增和递减运算符常称作自动递增自动递减运算符)。其中递减运算符是`--`,意为减少一个单位”;递增运算符是`++`,意为增加一个单位”。举个例子来说假设A是一个`int`(整数则表达式`++A`就等价于(`A = A + 1`)。递增和递减运算符结果生成的是变量的值对每种类型的运算符都有两个版本可供选用通常将其称为前缀版后缀版”。“前递增表示`++`运算符位于变量或表达式的前面后递增表示`++`运算符位于变量或表达式的后面类似地,“前递减意味着`--`运算符位于变量或表达式的前面后递减意味着`--`运算符位于变量或表达式的后面对于前递增和前递减`++A``--A`),会先执行运算复用成值而对于后递增和后递减`A++``A--`),会先生成值再执行运算下面是一个例子

//: AutoInc.java // Demonstrates the ++ and -- operators

public class AutoInc { public static void main(String[] args) { int i = 1; prt("i : " + i); prt("++i : " + ++i); // Pre-increment prt("i++ : " + i++); // Post-increment prt("i : " + i); prt("--i : " + --i); // Pre-decrement prt("i-- : " + i--); // Post-decrement prt("i : " + i); } static void prt(String s) { System.out.println(s); } } ///:~

该程序的输出如下

i : 1 ++i : 2 i++ : 2 i : 3 --i : 2 i-- : 2 i : 1

从中可以看到对于前缀形式我们在执行完运算后才得到值但对于后缀形式则是在运算执行之前就得到值它们是唯一具有副作用的运算符除那些涉及赋值的以外)。也就是说它们会改变运算对象而不仅仅是使用自己的值递增运算符正是对C++”这个名字的一种解释暗示着超载C的一步”。在早期的一次Java演讲中Bill Joy始创人之一声称Java=C++--”(C加加减减),意味着Java已去除了C++一些没来由折磨人的地方形成一种更精简的语言正如大家会在这本书中学到的那样Java的许多地方都得到了简化所以Java的学习比C++更容易。

## 3.1.5 关系运算符

关系运算符生成的是一个布尔”(`Boolean`)结果它们评价的是运算对象值之间的关系若关系是真实的关系表达式会生成`true`();若关系不真实则生成`false`()。关系运算符包括小于(`<`)、大于(`>`)、小于或等于(`<=`)、大于或等于(`>=`)、等于(`==`)以及不等于(`!=`)。等于和不等于适用于所有内建的数据类型但其他比较不适用于`boolean`类型。

**1. 检查对象是否相等**

关系运算符`==``!=`也适用于所有对象但它们的含义通常会使初涉Java领域的人找不到北下面是一个例子

//: Equivalence.java

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); } } ///:~

其中表达式`System.out.println(n1 == n2)`可打印出内部的布尔比较结果一般人都会认为输出结果肯定先是`true`,再是`false`,因为两个`Integer`对象都是相同的但尽管对象的内容相同引用却是不同的`==``!=`比较的正好就是对象引用所以输出结果实际上先是`false`,再是`true`。这自然会使第一次接触的人感到惊奇若想对比两个对象的实际内容是否相同又该如何操作呢此时必须使用所有对象都适用的特殊方法`equals()`。但这个方法不适用于基本类型”,那些类型直接使用`==``!=`即可下面举例说明如何使用

//: EqualsMethod.java

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`。但事情并未到此结束假设您创建了自己的类就象下面这样

//: EqualsMethod2.java

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()`,否则不可能表现出我们希望的行为不幸的是要到第7章才会学习如何改变行为但要注意`equals()`的这种行为方式同时或许能够避免一些灾难性的事件大多数Java类库都实现了`equals()`,所以它实际比较的是对象的内容而非它们的引用。

## 3.1.6 逻辑运算符

逻辑运算符AND(`&&`)、OR(`||`)以及NOT(`!`)能生成一个布尔值(`true``false`)——以参数的逻辑关系为基础下面这个例子向大家展示了如何使用关系和逻辑运算符

//: Bool.java // Relational and logical operators import java.util.*;

public class Bool { public static void main(String[] args) { Random rand = new Random(); int i = rand.nextInt() % 100; int j = rand.nextInt() % 100; prt("i = " + i); prt("j = " + j); prt("i > j is " + (i > j)); prt("i < j is " + (i < j)); prt("i >= j is " + (i >= j)); prt("i <= j is " + (i <= j)); prt("i == j is " + (i == j)); prt("i != j is " + (i != j));

// Treating an int as a boolean is
// not legal Java

//! prt("i && j is " + (i && j)); //! prt("i || j is " + (i || j)); //! prt("!i is " + !i);

prt("(i < 10) && (j < 10) is "
   + ((i < 10) && (j < 10)) );
prt("(i < 10) || (j < 10) is "
   + ((i < 10) || (j < 10)) );

} static void prt(String s) { System.out.println(s); } } ///:~

只可将ANDOR或NOT应用于布尔值与在C及C++中不同不可将一个非布尔值当作布尔值在逻辑表达式中使用若这样做就会发现尝试失败并用一个`//!`标出。然而,后续的表达式利用关系比较生成布尔值,然后对结果进行逻辑运算。

输出列表看起来象下面这个样子

i = 85 j = 4 i > j is true i < j is false i >= j is true i <= j is false i == j is false i != j is true (i < 10) && (j < 10) is false (i < 10) || (j < 10) is true

注意若在预计为`String`值的地方使用布尔值会自动转换成适当的文本形式在上述程序中可将对`int`的定义替换成除`boolean`以外的其他任何主数据类型但要注意对浮点数字的比较是非常严格的即使一个数字仅在小数部分与另一个数字存在极微小的差异仍然认为它们是不相等即使一个数字只比零大一点点例如2不停地开平方根),它仍然属于非零。

**1. 短路**

操作逻辑运算符时我们会遇到一种名为短路的情况这意味着只有明确得出整个表达式真或假的结论才会对表达式进行逻辑求值因此一个逻辑表达式的所有部分都有可能不进行求值

//: ShortCircuit.java // Demonstrates short-circuiting behavior // with logical operators.

public class ShortCircuit { static boolean test1(int val) { System.out.println("test1(" + val + ")"); System.out.println("result: " + (val < 1)); return val < 1; } static boolean test2(int val) { System.out.println("test2(" + val + ")"); System.out.println("result: " + (val < 2)); return val < 2; } static boolean test3(int val) { System.out.println("test3(" + val + ")"); System.out.println("result: " + (val < 3)); return val < 3; } public static void main(String[] args) { if(test1(0) && test2(2) && test3(2)) System.out.println("expression is true"); else System.out.println("expression is false"); } } ///:~

每次测试都会比较参数并返回真或假它不会显示与准备调用什么有关的资料测试在下面这个表达式中进行

if(test1(0)) && test2(2) && test3(2))

很自然地你也许认为所有这三个测试都会得以执行但希望输出结果不至于使你大吃一惊

if(test1(0) && test2(2) && test3(2))

第一个测试生成一个`true`结果所以表达式求值会继续下去然而第二个测试产生了一个`false`结果由于这意味着整个表达式肯定为`false`,所以为什么还要继续剩余的表达式呢这样做只会徒劳无益事实上,“短路一词的由来正种因于此如果一个逻辑表达式的所有部分都不必执行下去那么潜在的性能提升将是相当可观的。

## 3.1.7 按位运算符

按位运算符允许我们操作一个整数主数据类型中的单个比特”,即二进制位按位运算符会对两个参数中对应的位执行布尔代数并最终生成一个结果按位运算来源于C语言的低级操作我们经常都要直接操纵硬件需要频繁设置硬件寄存器内的二进制位Java的设计初衷是嵌入电视顶置盒内所以这种低级操作仍被保留下来了然而由于操作系统的进步现在也许不必过于频繁地进行按位运算若两个输入位都是1则按位AND运算符(`&`)在输出位里生成一个1否则生成0若两个输入位里至少有一个是1则按位OR运算符(`|`)在输出位里生成一个1只有在两个输入位都是0的情况下它才会生成一个0若两个输入位的某一个是1但不全都是1那么按位XOR(`^`,异或在输出位里生成一个1按位NOT(`~`,也叫作运算符属于一元运算符它只对一个参数进行操作其他所有运算符都是二元运算符)。按位NOT生成与输入位的相反的值——若输入0则输出1输入1则输出0按位运算符和逻辑运算符都使用了同样的字符只是数量不同因此我们能方便地记忆各自的含义由于是非常所以按位运算符仅使用了一个字符按位运算符可与等号(`=`)联合使用以便合并运算及赋值:`&=`,`|=``^=`都是合法的由于`~`是一元运算符所以不可与`=`联合使用)。

我们将`boolean`(布尔类型当作一种单位单比特值对待所以它多少有些独特的地方我们可执行按位ANDOR和XOR但不能执行按位NOT大概是为了避免与逻辑NOT混淆)。对于布尔值按位运算符具有与逻辑运算符相同的效果只是它们不会中途短路”。此外针对布尔值进行的按位运算为我们新增了一个XOR逻辑运算符它并未包括在逻辑运算符的列表中在移位表达式中我们被禁止使用布尔运算原因将在下面解释。

## 3.1.8 移位运算符

移位运算符面向的运算对象也是二进制的”。可单独用它们处理整数类型基本类型的一种)。左移位运算符(`<<`)能将运算符左边的运算对象向左移动运算符右侧指定的位数在低位补0)。“有符号右移位运算符(`>>`)则将运算符左边的运算对象向右移动运算符右侧指定的位数。“有符号右移位运算符使用了符号扩展”:若值为正则在高位插入0若值为负则在高位插入1Java也添加了一种无符号右移位运算符(`>>>`),它使用了零扩展”:无论正负都在高位插入0这一运算符是C或C++没有的若对`char`,`byte`或者`short`进行移位处理那么在移位进行之前它们会自动转换成一个`int`。只有右侧的5个低位才会用到这样可防止我们在一个`int`数里移动不切实际的位数若对一个`long`值进行处理最后得到的结果也是`long`。此时只会用到右侧的6个低位防止移动超过`long`值里现成的位数但在进行无符号右移位时也可能遇到一个问题若对`byte``short`值进行右移位运算得到的可能不是正确的结果Java 1.0和Java 1.1特别突出)。它们会自动转换成`int`类型并进行右移位零扩展不会发生所以在那些情况下会得到-1的结果可用下面这个例子检测自己的实现方案

//: URShift.java // Test of unsigned right shift

public class URShift { public static void main(String[] args) { int i = -1; i >>>= 10; System.out.println(i); long l = -1; l >>>= 10; System.out.println(l); short s = -1; s >>>= 10; System.out.println(s); byte b = -1; b >>>= 10; System.out.println(b); } } ///:~

移位可与等号(`<<=``>>=``>>>=`)组合使用此时运算符左边的值会移动由右边的值指定的位数再将得到的结果赋回左边的值下面这个例子向大家阐示了如何应用涉及按位操作的所有运算符以及它们的效果

//: BitManipulation.java // Using the bitwise operators import java.util.*;

public class BitManipulation { public static void main(String[] args) { Random rand = new Random(); int i = rand.nextInt(); int j = rand.nextInt(); pBinInt("-1", -1); pBinInt("+1", +1); int maxpos = 2147483647; pBinInt("maxpos", maxpos); int maxneg = -2147483648; pBinInt("maxneg", maxneg); pBinInt("i", i); pBinInt("~i", ~i); pBinInt("-i", -i); pBinInt("j", j); pBinInt("i & j", i & j); pBinInt("i | j", i | j); pBinInt("i ^ j", i ^ j); pBinInt("i << 5", i << 5); pBinInt("i >> 5", i >> 5); pBinInt("(~i) >> 5", (~i) >> 5); pBinInt("i >>> 5", i >>> 5); pBinInt("(~i) >>> 5", (~i) >>> 5);

long l = rand.nextLong();
long m = rand.nextLong();
pBinLong("-1L", -1L);
pBinLong("+1L", +1L);
long ll = 9223372036854775807L;
pBinLong("maxpos", ll);
long lln = -9223372036854775808L;
pBinLong("maxneg", lln);
pBinLong("l", l);
pBinLong("~l", ~l);
pBinLong("-l", -l);
pBinLong("m", m);
pBinLong("l & m", l & m);
pBinLong("l | m", l | m);
pBinLong("l ^ m", l ^ m);
pBinLong("l << 5", l << 5);
pBinLong("l >> 5", l >> 5);
pBinLong("(~l) >> 5", (~l) >> 5);
pBinLong("l >>> 5", l >>> 5);
pBinLong("(~l) >>> 5", (~l) >>> 5);

} static void pBinInt(String s, int i) { System.out.println( s + ", int: " + i + ", binary: "); System.out.print(" "); for(int j = 31; j >=0; j--) if(((1 << j) & i) != 0) System.out.print("1"); else System.out.print("0"); System.out.println(); } static void pBinLong(String s, long l) { System.out.println( s + ", long: " + l + ", binary: "); System.out.print(" "); for(int i = 63; i >=0; i--) if(((1L << i) & l) != 0) System.out.print("1"); else System.out.print("0"); System.out.println(); } } ///:~

程序末尾调用了两个方法:`pBinInt()``pBinLong()`。它们分别操作一个`int``long`并用一种二进制格式输出同时附有简要的说明文字目前可暂时忽略它们具体的实现方案大家要注意的是`System.out.print()`的使用而不是`System.out.println()`。`print()`方法不会产生一个新行以便在同一行里罗列多种信息除展示所有按位运算符针对`int``long`的效果之外本例也展示了`int``long`的最小值最大值、+1-1使大家能体会它们的情况注意高位代表正负号0为正1为负下面列出`int`部分的输出

-1, int: -1, binary: 11111111111111111111111111111111 +1, int: 1, binary: 00000000000000000000000000000001 maxpos, int: 2147483647, binary: 01111111111111111111111111111111 maxneg, int: -2147483648, binary: 10000000000000000000000000000000 i, int: 59081716, binary: 00000011100001011000001111110100 ~i, int: -59081717, binary: 11111100011110100111110000001011 -i, int: -59081716, binary: 11111100011110100111110000001100 j, int: 198850956, binary: 00001011110110100011100110001100 i & j, int: 58720644, binary: 00000011100000000000000110000100 i | j, int: 199212028, binary: 00001011110111111011101111111100 i ^ j, int: 140491384, binary: 00001000010111111011101001111000 i << 5, int: 1890614912, binary: 01110000101100000111111010000000 i >> 5, int: 1846303, binary: 00000000000111000010110000011111 (~i) >> 5, int: -1846304, binary: 11111111111000111101001111100000 i >>> 5, int: 1846303, binary: 00000000000111000010110000011111 (~i) >>> 5, int: 132371424, binary: 00000111111000111101001111100000

数字的二进制形式表现为有符号2的补值”。

## 3.1.9 三元`if-else`运算符

这种运算符比较罕见因为它有三个运算对象但它确实属于运算符的一种因为它最终也会生成一个值这与本章后一节要讲述的普通`if-else`语句是不同的表达式采取下述形式

布尔表达式 ? 值0:值1

布尔表达式的结果为`true`,就计算值0”,而且它的结果成为最终由运算符产生的值但若布尔表达式的结果为`false`,计算的就是值1”,而且它的结果成为最终由运算符产生的值当然也可以换用普通的`if-else`语句在后面介绍),但三元运算符更加简洁尽管C引以为傲的就是它是一种简练的语言而且三元运算符的引入多半就是为了体现这种高效率的编程但假若您打算频繁用它还是要先多作一些思量——它很容易就会产生可读性极差的代码可将条件运算符用于自己的副作用”,或用于它生成的值但通常都应将其用于值因为那样做可将运算符与`if-else`明确区别开下面便是一个例子

static int ternary(int i) { return i < 10 ? i * 100 : i * 10; }

可以看出假设用普通的`if-else`结构写上述代码代码量会比上面多出许多如下所示

static int alternative(int i) { if (i < 10) return i * 100; return i * 10; }

但第二种形式更易理解而且不要求更多的录入所以在挑选三元运算符时请务必权衡一下利弊。

## 3.1.10 逗号运算符

在C和C++逗号不仅作为函数参数列表的分隔符使用也作为进行后续计算的一个运算符使用在Java里需要用到逗号的唯一场所就是`for`循环本章稍后会对此详加解释。

## 3.1.11 字符串运算符`+`

这个运算符在Java里有一项特殊用途连接不同的字符串这一点已在前面的例子中展示过了尽管与`+`的传统意义不符但用`+`来做这件事情仍然是非常自然的在C++这一功能看起来非常不错所以引入了一项运算符重载机制以便C++程序员为几乎所有运算符增加特殊的含义但非常不幸与C++的另外一些限制结合运算符重载成为一种非常复杂的特性程序员在设计自己的类时必须对此有周到的考虑与C++相比尽管运算符重载在Java里更易实现但迄今为止仍然认为这一特性过于复杂所以Java程序员不能象C++程序员那样设计自己的重载运算符我们注意到运用`String +`时一些有趣的现象若表达式以一个`String`起头那么后续所有运算对象都必须是字符串如下所示

int x = 0, y = 1, z = 2; String sString = "x, y, z "; System.out.println(sString + x + y + z);

在这里Java编译程序会将`x`,`y``z`转换成它们的字符串形式而不是先把它们加到一起然而如果使用下述语句

System.out.println(x + sString);

那么早期版本的Java就会提示出错以后的版本能将`x`转换成一个字符串)。因此如果想通过加号连接字符串使用Java的早期版本),请务必保证第一个元素是字符串或加上引号的一系列字符编译能将其识别成一个字符串)。

## 3.1.12 运算符常规操作规则

使用运算符的一个缺点是括号的运用经常容易搞错即使对一个表达式如何计算有丝毫不确定的因素都容易混淆括号的用法这个问题在Java里仍然存在在C和C++一个特别常见的错误如下

while(x = y) { //... }

程序的意图是测试是否相等”(`==`),而不是进行赋值操作在C和C++`y`是一个非零值那么这种赋值的结果肯定是`true`。这样使可能得到一个无限循环在Java里这个表达式的结果并不是布尔值而编译器期望的是一个布尔值而且不会从一个`int`数值中转换得来所以在编译时系统就会提示出现错误有效地阻止我们进一步运行程序所以这个缺点在Java里永远不会造成更严重的后果唯一不会得到编译错误的时候是`x``y`都为布尔值在这种情况下,`x = y`属于合法表达式而在上述情况下则可能是一个错误在C和C++类似的一个问题是使用按位AND和OR而不是逻辑AND和OR按位AND和OR使用两个字符之一(`&``|`),而逻辑AND和OR使用两个相同的字符(`&&``||`)。就象`=``==`一样键入一个字符当然要比键入两个简单在Java里编译器同样可防止这一点因为它不允许我们强行使用一种并不属于的类型。

## 3.1.13 转换运算符转换”(Cast的作用是与一个模型匹配”。在适当的时候Java会将一种数据类型自动转换成另一种例如假设我们为浮点变量分配一个整数值计算机会将`int`自动转换成`float`。通过转换我们可明确设置这种类型的转换或者在一般没有可能进行的时候强迫它进行为进行一次转换要将括号中希望的数据类型包括所有修改符置于其他任何值的左侧下面是一个例子

void casts() { int i = 200; long l = (long)i; long l2 = (long)200; }

正如您看到的那样既可对一个数值进行转换处理亦可对一个变量进行转换处理但在这儿展示的两种情况下转换均是多余的因为编译器在必要的时候会自动进行`int`值到`long`值的转换当然仍然可以设置一个转换提醒自己留意也使程序更清楚在其他情况下转换只有在代码编译时才显出重要性在C和C++转换有时会让人头痛在Java里转换则是一种比较安全的操作但是若进行一种名为缩小转换”(Narrowing Conversion的操作也就是说脚本是能容纳更多信息的数据类型将其转换成容量较小的类型),此时就可能面临信息丢失的危险此时编译器会强迫我们进行转换就好象说:“这可能是一件危险的事情——如果您想让我不顾一切地做那么对不起请明确转换。”而对于放大转换”(Widening conversion),则不必进行明确转换因为新类型肯定能容纳原来类型的信息不会造成任何信息的丢失Java允许我们将任何基本类型转换为其他任何一种基本类型但布尔值(`bollean`)要除外后者根本不允许进行任何转换处理。“不允许进行转换为了将一种类转换成另一种必须采用特殊的方法字符串是一种特殊的情况本书后面会讲到将对象转换到一个类型家族例如,“橡树可转换为”;反之亦然但对于其他外来类型岩石”,则不能转换为”)。

**1. 字面值**

最开始的时候若在一个程序里插入字面值”(Literal),编译器通常能准确知道要生成什么样的类型但在有些时候对于类型却是暧昧不清的若发生这种情况必须对编译器加以适当的指导”。方法是用与字面值关联的字符形式加入一些额外的信息下面这段代码向大家展示了这些字符

//: Literals.java

class Literals { char c = 0xffff; // max char hex value byte b = 0x7f; // max byte hex value short s = 0x7fff; // max short hex value int i1 = 0x2f; // Hexadecimal (lowercase) int i2 = 0X2F; // Hexadecimal (uppercase) int i3 = 0177; // Octal (leading zero) // Hex and Oct also work with long. long n1 = 200L; // long suffix long n2 = 200l; // long suffix long n3 = 200; //! long l6(200); // not allowed float f1 = 1; float f2 = 1F; // float suffix float f3 = 1f; // float suffix float f4 = 1e-45f; // 10 to the power float f5 = 1e+9f; // float suffix double d1 = 1d; // double suffix double d2 = 1D; // double suffix double d3 = 47e47d; // 10 to the power } ///:~

十六进制Base 16)——它适用于所有整数数据类型——用一个前置的`0x``0X`指示并在后面跟随采用大写或小写形式的`0-9`以及`a-f`。若试图将一个变量初始化成超出自身能力的一个值无论这个值的数值形式如何),编译器就会向我们报告一条出错消息注意在上述代码中最大的十六进制值只会在`char`,`byte`以及`short`身上出现若超出这一限制编译器会将值自动变成一个`int`,并告诉我们需要对这一次赋值进行缩小转换”。这样一来我们就可清楚获知自己已超载了边界八进制Base 8是用数字中的一个前置`0`以及`0-7`的数位指示的在CC++或者Java中对二进制数字没有相应的字面表示方法字面值后的尾随字符标志着它的类型若为大写或小写的`L`,代表`long`;大写或小写的`F`,代表`float`;大写或小写的`D`,则代表`double`。

指数总是采用一种我们认为很不直观的记号方法:`1.39e-47f`。在科学与工程学领域,`e`代表自然对数的基数约等于`2.718`(Java一种更精确的`double`值采用`Math.E`的形式)。它在象“`1.39×e`-47次方这样的指数表达式中使用意味着“`1.39×2.718`-47次方”。然而自FORTRAN语言发明后人们自然而然地觉得`e`代表10多少次幂”。这种做法显得颇为古怪因为FORTRAN最初面向的是科学与工程设计领域理所当然它的设计者应对这样的混淆概念持谨慎态度注释①)。但不管怎样这种特别的表达方法在CC++以及现在的Java中顽固地保留下来了所以倘若您习惯将`e`作为自然对数的基数使用那么在Java中看到象`1.39e-47f`这样的表达式时请转换您的思维从程序设计的角度思考它它真正的含义是“`1.39×10`-47次方”。

①:John Kirkham这样写道:“我最早于1962年在一部IBM 1620机器上使用FORTRAN II那时——包括60年代以及70年代的早期FORTRAN一直都是使用大写字母之所以会出现这一情况可能是由于早期的输入设备大多是老式电传打字机使用5位Baudot码那种码并不具备小写能力乘幂表达式中的E也肯定是大写的所以不会与自然对数的基数`e`发生冲突后者必然是小写的。`E`这个字母的含义其实很简单就是Exponential的意思指数幂数’,代表计算系统的基数——一般都是10当时八进制也在程序员中广泛使用尽管我自己未看到它的使用但假若我在乘幂表达式中看到一个八进制数字就会把它认作Base 8我记得第一次看到用小写`e`表示指数是在70年代末期我当时也觉得它极易产生混淆所以说这个问题完全是自己潜入FORTRAN里去的并非一开始就有如果你真的想使用自然对数的基数实际有现成的函数可供利用但它们都是大写的。”

注意如果编译器能够正确地识别类型就不必使用尾随字符对于下述语句

long n3 = 200;

它并不存在含混不清的地方所以200后面的一个`L`大可省去然而对于下述语句

float f4 = 1e-47f; //10的幂数

编译器通常会将指数作为双精度数(`double`)处理所以假如没有这个尾随的`f`,就会收到一条出错提示告诉我们须用一个转换`double`转换成`float`。

**2. 转型**

大家会发现假若对主数据类型执行任何算术或按位运算只要它们`int`”(`char`,`byte`或者`short`),那么在正式执行运算之前那些值会自动转换成`int`。这样一来最终生成的值就是`int`类型所以只要把一个值赋回较小的类型就必须使用转换”。此外由于是将值赋回给较小的类型所以可能出现信息丢失的情况)。通常表达式中最大的数据类型是决定了表达式最终结果大小的那个类型若将一个`float`值与一个`double`值相乘结果就是`double`;如将一个`int`和一个`long`值相加则结果为`long`。

## 3.1.14 Java没有`sizeof`

在C和C++,`sizeof()`运算符能满足我们的一项特殊需要获知为数据项目分配的字符数量在C和C++,`size()`最常见的一种应用就是移植”。不同的数据在不同的机器上可能有不同的大小所以在进行一些对大小敏感的运算时程序员必须对那些类型有多大做到心中有数例如一台计算机可用32位来保存整数而另一台只用16位保存显然在第一台机器中程序可保存更大的值正如您可能已经想到的那样移植是令C和C++程序员颇为头痛的一个问题Java不需要`sizeof()`运算符来满足这方面的需要因为所有数据类型在所有机器的大小都是相同的我们不必考虑移植问题——Java本身就是一种与平台无关的语言。

## 3.1.15 复习计算顺序

在我举办的一次培训班中有人抱怨运算符的优先顺序太难记了一名学生推荐用一句话来帮助记忆:“Ulcer Addicts Really Like C A lot”,溃疡患者特别喜欢维生素C”。

| 助记词  |       运算符类型       |                              运算符 |
|---------|:----------------------:|------------------------------------:|
| Ulcer   |          Unary         |      ` + - ++ – [[ rest...]]` |
| Addicts | Arithmetic (and shift) |      ` * / % + - << >>` |
| Really  |       Relational       |       `> < >= <= == != ` |
| Like    | Logical (and bitwise)  |   ** &&||&|^ **  |
| C       | Conditional (ternary)  | `A > B ? X : Y   ` |
| A Lot   | Assignment             | `= (and compound assignment like *=)` |


当然对于移位和按位运算符上表并不是完美的助记方法但对于其他运算来说它确实很管用。

## 3.1.16 运算符总结
下面这个例子向大家展示了如何随同特定的运算符使用主数据类型从根本上说它是同一个例子反迭代复地执行只是使用了不同的主数据类型文件编译时不会报错因为那些会导致错误的行已用`//!`变成了注释内容。

//: AllOps.java // Tests all the operators on all the // primitive data types to show which // ones are accepted by the Java compiler.

class AllOps { // To accept the results of a boolean test: void f(boolean b) {} void boolTest(boolean x, boolean y) { // Arithmetic operators: //! x = x * y; //! x = x / y; //! x = x % y; //! x = x + y; //! x = x - y; //! x++; //! x--; //! x = +y; //! x = -y; // Relational and logical: //! f(x > y); //! f(x >= y); //! f(x < y); //! f(x <= y); f(x == y); f(x != y); f(!y); x = x && y; x = x || y; // Bitwise operators: //! x = ~y; x = x & y; x = x | y; x = x ^ y; //! x = x << 1; //! x = x >> 1; //! x = x >>> 1; // Compound assignment: //! x += y; //! x -= y; //! x *= y; //! x /= y; //! x %= y; //! x <<= 1; //! x >>= 1; //! x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! char c = (char)x; //! byte B = (byte)x; //! short s = (short)x; //! int i = (int)x; //! long l = (long)x; //! float f = (float)x; //! double d = (double)x; } void charTest(char x, char y) { // Arithmetic operators: x = (char)(x * y); x = (char)(x / y); x = (char)(x % y); x = (char)(x + y); x = (char)(x - y); x++; x--; x = (char)+y; x = (char)-y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x= (char)~y; x = (char)(x & y); x = (char)(x | y); x = (char)(x ^ y); x = (char)(x << 1); x = (char)(x >> 1); x = (char)(x >>> 1); // Compound assignment: x += y; x -= y; x = y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean b = (boolean)x; byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void byteTest(byte x, byte y) { // Arithmetic operators: x = (byte)(x y); x = (byte)(x / y); x = (byte)(x % y); x = (byte)(x + y); x = (byte)(x - y); x++; x--; x = (byte)+ y; x = (byte)- y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = (byte)~y; x = (byte)(x & y); x = (byte)(x | y); x = (byte)(x ^ y); x = (byte)(x << 1); x = (byte)(x >> 1); x = (byte)(x >>> 1); // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean b = (boolean)x; char c = (char)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void shortTest(short x, short y) { // Arithmetic operators: x = (short)(x * y); x = (short)(x / y); x = (short)(x % y); x = (short)(x + y); x = (short)(x - y); x++; x--; x = (short)+y; x = (short)-y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = (short)~y; x = (short)(x & y); x = (short)(x | y); x = (short)(x ^ y); x = (short)(x << 1); x = (short)(x >> 1); x = (short)(x >>> 1); // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void intTest(int x, int y) { // Arithmetic operators: x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = ~y; x = x & y; x = x | y; x = x ^ y; x = x << 1; x = x >> 1; x = x >>> 1; // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; short s = (short)x; long l = (long)x; float f = (float)x; double d = (double)x; } void longTest(long x, long y) { // Arithmetic operators: x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = ~y; x = x & y; x = x | y; x = x ^ y; x = x << 1; x = x >> 1; x = x >>> 1; // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; x <<= 1; x >>= 1; x >>>= 1; x &= y; x ^= y; x |= y; // Casting: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; float f = (float)x; double d = (double)x; } void floatTest(float x, float y) { // Arithmetic operators: x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: //! x = ~y; //! x = x & y; //! x = x | y; //! x = x ^ y; //! x = x << 1; //! x = x >> 1; //! x = x >>> 1; // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; //! x <<= 1; //! x >>= 1; //! x >>>= 1; //! x &= y; //! x ^= y; //! x |= y; // Casting: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; double d = (double)x; } void doubleTest(double x, double y) { // Arithmetic operators: x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x--; x = +y; x = -y; // Relational and logical: f(x > y); f(x >= y); f(x < y); f(x <= y); f(x == y); f(x != y); //! f(!x); //! f(x && y); //! f(x || y); // Bitwise operators: //! x = y; //! x = x & y; //! x = x | y; //! x = x ^ y; //! x = x << 1; //! x = x >> 1; //! x = x >>> 1; // Compound assignment: x += y; x -= y; x *= y; x /= y; x %= y; //! x <<= 1; //! x >>= 1; //! x >>>= 1; //! x &= y; //! x ^= y; //! x |= y; // Casting: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; } } ///:

注意布尔值(`boolean`)的能力非常有限我们只能为其赋予`true``false`而且可测试它为真还是为假但不可为它们再添加布尔值或进行其他其他任何类型运算`char`,`byte``short`我们可看到算术运算符的转型效果对这些类型的任何一个进行算术运算都会获得一个`int`结果必须将其明确转换回原来的类型缩小转换会造成信息的丢失),以便将值赋回那个类型但对于`int`却不必进行转换处理因为所有数据都已经属于`int`类型然而不要放松警惕认为一切事情都是安全的如果对两个足够大的int值执行乘法运算结果值就会溢出下面这个例子向大家展示了这一点

//: Overflow.java // Surprise! Java lets you overflow.

public class Overflow { public static void main(String[] args) { int big = 0x7fffffff; // max int value prt("big = " + big); int bigger = big * 4; prt("bigger = " + bigger); } static void prt(String s) { System.out.println(s); } } ///:~

输出结果如下

big = 2147483647 bigger = -4

而且不会从编译器那里收到出错提示,运行时也不会出现异常反应。爪哇咖啡(Java)确实是很好的东西,但却没有“那么”好!

对于`char`,`byte`或者`short`,混合赋值并不需要转换。即使它们执行转型操作,也会获得与直接算术运算相同的结果。而在另一方面,将转换略去可使代码显得更加简练。

大家可以看到,除`boolean`以外,任何一种基本类型都可通过转换变为其他基本类型。同样地,当转换成一种较小的类型时,必须留意“缩小转换”的后果。否则会在转换过程中不知不觉地丢失信息。