Java解惑精炼版(一)
Java解惑精炼版(一)
1、找零时刻(货币计算问题)
问题简述:Tom现有$2.0,购买了$1.10美元的货物,店主应该找他多少零钱?
1 public class Change {
2 public static void main(String[] args) {
3 System.out.println(2.00-1.10);//0.8999999999999999
4 }
5 }
运行结果:0.8999999999999999
问题在于1.10这个数字不能被精确表示成为一个double,因为它被表示成为最接近它的double值。更一般地说,问题在于并不是所有的小数都可以用二进制浮点数来精确表示的。
解决该问题的一种方式是使用某种整数类型,例如int或long,并且以分为单位来执行计算。如果采纳了此路线,请确保该整数类型大到足够表示在程序中你将要用的所有值。
System.out.println((200-110)+"cents");
解决该问题的另一种方式是使用执行精确小数运算的BigDecimal。告诫:一定要用BigDecimal(String)构造器,而千万不要用BigDecimal(double)构造器。BigDecimal:不可变的、任意精度的有符号十进制数。BigDecimal表示的数值是(unscaledValue × 10-scale)。
1 import java.math.BigDecimal;
2
3 public class Change {
4 public static void main(String[] args) {
5 System.out.println(new BigDecimal("2.00")
6 .subtract(new BigDecimal("1.10")));//0.90
7 }
8 }
总结:在需要精确答案的地方,要避免使用float和double;对于货币计算,要使用int、long或BigDecimal。使用BigDecimal的计算很有可能比那些原始类型的计算要慢一些,对某些大量使用小数计算的程序来说,这可能会成为问题,对于大多数程序来说,这显得一点也不重要。
2、条件运算符
先看如下程序代码:
1 public class DosEquis {
2 public static void main(String[] args) {
3 char x='X';
4 int i=0;
5 System.out.println(true?x:0);//X
6 System.out.println(false?i:x);//88
7 }
8 }
运行结果: X 88
运行结果是:X88,而不是XX,这是为什么呢?
下面看条件表达式结果类型的规范中的核心三点:
1)如果第二个和第三个操作数具有相同的类型,那么它就是条件表达式的类型。换句话说,你可以通过绕过混合类型的计算来避免大麻烦。
2)果一个操作数的类型是T,T表示byte、short或char,而另一个操作数是一个int类型的常量表达式,它的值是可以用类型T表示的,那么条件表达式的类型就是T。
3)否则,将对操作数类型运用二进制数字提升,而条件表达式的类型就是第二个和第三个操作数被提升之后的类型。
2、3两点对本谜题是关键。在程序的两个条件表达式中,一个操作数的类型是char,另一个的类型是int。在两个表达式中,int操作数都是0,它可以被表示成一个char。然而,只有第一个表达式中的int操作数是常量(0),而第二个表达式中的int操作数是变量(i)。因此,第2点被应用到了第一 个表达式上,它返回的类型是char,而第3点被应用到了第二个表达式上,其返回的类型是对int和char运用了二进制数字提升之后的类型,即 int。
演示示例扩展:
1 public class DosEquis {
2 public static void main(String[] args) {
3 char x='X';
4 //对int类型的常量进行测试
5 int i=12;
6 System.out.println(true?x:12);//X
7 System.out.println(true?x:i);//88
8 //对long类型的常量进行测试
9 long l=12L;
10 System.out.println(true?x:12L);//88
11 System.out.println(true?x:l);//88
12 //对float类型的常量进行测试
13 double d=12.0;
14 System.out.println(true?x:12.0);//88.0
15 System.out.println(true?x:d);//88.0
16 }
17 }
3、赋值运算符(复合赋值表达式)
半斤:
我们给出一个对变量x和i的声明即可,它肯定是一个合法的语句:
x += i;
但是,它并不是:
x = x + i;
许多程序员都会认为表达式(x=x+i;)是表达式(x+=il;)的简写形式。但是这并不十分准确。这两个表达式都是赋值表达式,第二天语句使用的是简单赋值操作符(=),而第一条语句使用的是复合赋值操作符。(复合赋值操作符包括:+=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^=和!=).Java语言规范中讲到,复合赋值 E1 op= E2; 等价于简单赋值 E1 = (T) ((E1) op (E2)); ,其中T是E1的类型,除非E1只被计算一次。
换句话说,复合赋值表达式自动地将它们所执行的计算的结果转型为其左侧变量的类型。如果结果的类型与该变量的类型相同,那么这个类型不会造成任何影响。然而,如果结果的类型比该变量的类型要宽,那么复合赋值操作符将隐形的执行强制类型转换。因此,我们有很好的理由去解释为什么在尝试着执行等价的简单赋值可能会产生一个编译错误。
代码示例演示:
1 public class Demo {
2 public static void main(String[] args) {
3 short x=0;
4 int i=123456;
5 x += i;//复合赋值编译将不会产生任何错误:包含了一个隐藏的转型!
6
7 //x=x+i;//编译错误:Type mismatch:cannot convert from int to short
8 }
9 }
为了避免复合赋值表达式隐形的进行类型转换(会就是精度),请不要讲复合赋值操作符作用于byte、short或char类型的变量上。在将复合赋值操作符作用于int类型的变量上时,要确保表达式右侧不是long、float或double类型。在将复合赋值操作符作用于float类型的变量上时,要确保表达式右侧不是double类型。
八两:
与上面的例子相反,如果我们给出的关于变量x和i的声明是如下的合法语句:
x = x + i;
但是,它并不是:
x += i;
乍一看,这个谜题可能看起来与前面一个谜题相同。但是请放心,它们并不一样。这两个谜题在哪一条语句必是合法的,以及哪一条语句必是不合法的方面,正好相反。
就像前面的谜题一样,这个谜题也依赖于有关复合赋值操作符的规范中的细节。二者的相似之处就此打住。基于前面的谜题,你可能会想:复合赋值操作符比简单赋值操作符的限制要少一些。在一般情况下,这是对的,但是有这么一个领域,在其中简单赋值操作符会显得更宽松一些。
复合赋值操作符要求两个操作数都是原始类型的,例如int,或包装了的原始类型,例如Integer,但是有一个例外:如果在+=操作符左侧的操作数是String类型的,那么它允许右侧的操作数是任意类型,在这种情况下,该操作符执行的是字符串连接操作。简单赋值操作符(=)允许其左侧的是对象引用类型,这就显得要宽松许多了:你可以使用它们来表示任何你想要表示的内容,只要表达式的右侧与左侧的变量是赋值兼容的即可。
代码示例演示:
1 public class Demo {
2 public static void main(String[] args) {
3 Object o="Buy";
4 String s="Effective Java!";
5 //简单赋值是合法的,因为o+s是String类型的,而String类型又是与Object赋值兼容的:
6 o=o+s;
7 //复合赋值是非法的,因为左侧是一个Object引用类型,而右侧是一个String类型:
8 //o+=s;
9 //编译错误:The operator += is undefined for the argument type(s)Object,String
10 }
11 }
4、String “+” 连接操作:畜牧场
George Orwell的《畜牧场(Animal Farm)》一书的读者可能还记得老上校的宣言:“所有的动物都是平等的。”下面的Java程序试图要测试这项宣言。那么,它将打印出什么呢?
1 public class Demo {
2 public static void main(String[] args) {
3 final String pig = "length: 10";
4 final String dog = "length: " + pig.length();
5 System.out.println("pig=:"+pig+",dog="+dog);
6 //pig=:length: 10,dog=length: 10
7 System.out. println("Animals are equal: "+ pig == dog);//false
8
9 String str1="length: 10";
10 String str2="length: " + str1.length();
11 String str3="length: 10";
12 System.out. println(str1==str2);//false
13 System.out.println(str1==str3);//true
14 }
15 }
对该程序的表面分析可能会认为它应该打印出Animal are equal: true。毕竟,pig和dog都是final的string类型变量,它们都被初始化为字符序列“length: 10”。换句话说,被pig和dog引用的字符串是且永远是彼此相等的。然而,==操作符测试的是这两个对象引用是否正好引用到了相同的对象上。在本例中,它们并非引用到了相同的对象上。
String类型的编译期常量是内存限定的。换句话说,任何两个String类型的常量表达式,如果标明的是相同的字符序列,那么它们就用相同的对象引用来表示。如果用常量表达式来初始化pig和dog,那么它们确实会指向相同的对象,但是dog并不是用常量表达式初始化的。既然语言已经对在常量表达式中允许出现的操作作出了限制,而方法调用又不在其中,那么,这个程序就应该打印Animal are equal: false,对吗?
实际上不对。如果你运行该程序,你就会发现它打印的只是false,并没有其它的任何东西。它没有打印Animal are equal: 。它怎么会不打印这个字符串字面常量呢?毕竟打印它才是正确的呀!因为:+ 操作符,不论是用作加法还是字符串连接操作,它都比 == 操作符的优先级高。因此,println方法的参数是按照下面的方式计算的:
System.out.println(("Animals are equal: " + pig) == dog);
这个布尔表达式的值当然是false,它正是该程序的所打印的输出。
令人晕头转向的Hello
请看下面的程序:
1 /**
2 * Generated by the IBM IDL-to-Java compiler, version 1.0
3 * from F:\TestRoot\apps\a1\units\include\PolicyHome.idl
4 * Wednesday, June 17, 1998 6:44:40 o’clock AM GMT+00:00
5 */
6 public class Demo {
7 public static void main(String[] args) {
8 System.out.print("Hell");
9 System.out.println("o world!");
10 }
11 }
这个谜题看起来相当简单。该程序包含了两条语句,第一条打印Hell,而第二条在同一行打印o world,从而将两个字符串有效地连接在了一起。因此,你可能期望该程序打印出Hello world。但是很可惜,你犯了错,实际上,它根本就通不过编译。
问题在于注释的第三行,它包含了字符\units。这些字符以反斜杠(\)以及紧跟着的字母u开头的,而它(\u)表示的是一个Unicode转义字符的开始。遗憾的是,这些字符后面没有紧跟四个十六进制的数字,因此,这个Unicode转义字符是病构的,而编译器则被要求拒绝该程序。Unicode转义字符必须是良构的,即使是出现在注释中也是如此。
1 public class Demo {
2 public static void main(String[] args) {
3 //\\u后面必须跟4个16进制的数字,否则会出现编译错误
4 char ch='\u0065';
5 System.out.println(ch);//e
6 }
7 }