java解惑--总结--Joshua Bloch & Neal Gaffer
越朗
2023-12-01
1>谜题1:奇数性
下面的方法意图确定它那唯一的参数是否是一个奇数
public static boolean isOdd(int i){
return i % 2 == 1;
}
因为当 i 是一个负奇数时,i % 2 等于-1而不是1,所以应为:
public static boolean isOdd(int i){
return i % 2 != 0;
}
总之:无论你何时使用到了取余操作符,都要考虑到操作数和结果的符号;
2>谜题2:找零时刻
下面是一个试图解决上述问题的程序,它会打印出什么呢?
public class Change{
public static void main(String args[]){
System.out.println(2.00 - 1.10);
}
}
//打印的是0.8999999999999999
解决方案-->
import java.math.BigDecimal;
public class Change1{
public static void main(String args[]){
System.out.println(new BigDecimal("2.00").
subtract(new BigDecimal("1.10")));
}
}
//打印出0.90
总之, 在需要精确答案的地方,要避免使用float和double;对于货币计算,要使用int、long或BigDecimal
3>谜题3:长整除
这个程序会打印出什么呢?
public class LongDivision{
public static void main(String args[]){
final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
}
}
//它打印的是5
解决方案-->
public class LongDivision{
public static void main(String args[ ]){
final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
}
}
//打印出100
当你在操作很大的数字时,千万要提防溢出——它可是一个缄默杀手。
4>谜题4:初级问题
System.out.println(12345+5432l);//打印出17777
解决方案:-->System.out.println(12345+5432L);
数字1的水平笔划(称为“臂(arm)”)和垂直笔划(称为“茎(stem)”)之间是一个锐角,而与此相对照的是,小写字母l的臂和茎之间是一个直角。
总之,小写字母l和数字1在大多数打字机字体中都是几乎一样的,所以long型用L;
5>谜题5:十六进制的趣事
这个程序会打印出什么呢?
public class JoyOfHex{
public static void main(String[] args){
System.out.println(
Long.toHexString(0x100000000L + 0xcafebabe));
}
}
//打印的是cafebabe
解决方案:
public class JoyOfHex{
public static void main(String[] args){
System.out.println(
Long.toHexString(0x100000000L + 0xcafebabeL));
}
}
//打印出1cafebabe
总之:混合类型的计算可能会产生混淆,尤其是十六进制和八进制字面常量无需显式的减号符号就可以表示负的数值。为了避免这种窘境,通常最好是避免混合类型的计算
6>谜题6:多重转型
它到底会打印出什么呢?
public class Multicast{
public static void main (String[] args){
System.out.println((int)(char)(byte) -1);
}
}
//它打印出的是65535
有一条很简单的规则能够描述从较窄的整型转换成较宽的整型时的符号扩展行为:如果最初的数值类型是有符号的,那么就执行符号扩展;如果它是char无符号位,那么不管它将要被转换成什么类型,都执行零扩展,所以过程为:-1-->-1-->65535-->65535
7>那么它会打印出什么呢?
public class CleverSwap{
public static void main(String[] args){
int x = 1984; // (0x7c0)
int y = 2001; // (0x7d1)
x^= y^= x^= y;
System.out.println("x= " + x + "; y= " + y);
}
}
//打印的是 x = 0; y = 1984
这个教训很简单:在单个的表达式中不要对相同的变量赋值两次
8>谜题8:Dos Equis
下面的程序将会打印出什么呢?
public class DosEquis{
public static void main(String[] args){
char x = 'X';
int i = 0;
System.out.println(true ? x : 0);
System.out.println(false ? i : x);
}
}
//它打印出来的是X88
如果一个操作数的类型是T,T表示byte、short或char,而另一个操作数是一个int类型的常量表达式,它的值是可以用类型T表示的,那么条件表达式的类型就是T。否则,将对操作数类型运用二进制数字提升,而条件表达式的类型就是第二个和第三个操作数被提升之后的类型
总之,通常最好是在条件表达式中使用类型相同的第二和第三操作数。
9>谜题9:半斤
复合赋值操作符包括 +=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^=和|=)Java语言规范中讲到,复合赋值 E1 op= E2等价于简单赋值E1 = (T)((E1)op(E2)),其中T是E1的类型,除非E1只被计算一次
总之,复合赋值操作符会悄悄地产生一个转型
10>谜题12:ABC
public class ABC{
public static void main(String[] args){
String letters = "ABC";
char[] numbers = {'1', '2', '3'};
System.out.println(letters + " easy as " + numbers);
}
}
//ABC easy as [C@16f0472
解决方法:System.out.println(letters + " easy as " +String.valueOf(numbers));
总之,char数组不是字符串。要想将一个char数组转换成一个字符串,就要调用String.valueOf(char[])方法。
11>总之,要确保字符\u不出现在一个合法的Unicode转义字符上下文之外,即使是在注释中也是如此。在机器生成的代码中要特别注意此问题。
12>要想将一个char数组转换成一个字符串,就要调用String.valueOf(char[])方法。例如:
char[] numbers = {'1', '2', '3'};
System.out.println(" easy as " +String.valueOf(numbers));//easy as 123;
13>在比较对象引用时,你应该优先使用equals方法而不是 == 操作符,除非你需要比较的是对象的标识而不是对象的值。
14>如果不是必要的话,请不要使用Unicode;
15>千万不要用一个return、break、continue或throw来退出一个finally语句块,并且千万不要允许将一个受检查的异常传播到一个finally语句块之外去。
类之惑:
16>谜题46:令人混淆的构造器案例
public class Confusing {
private Confusing(Object o) {
System.out.println("Object");
}
private Confusing(double[] dArray) {
System.out.println("double array");
}
public static void main(String[] args) {
new Confusing(null);
}
}
//double array
Java的重载解析过程是以两阶段运行的。第一阶段选取所有可获得并且可应用的方法或构造器。第二阶段在第一阶段选取的方法或构造器中选取最精确的一个。如果一个方法或构造器可以接受传递给另一个方法或构造器的任何参数,那么我们就说第一个方法比第二个方法缺乏精确性;
17>静态域由声明它的类和子类所共享同一份;
18>在任何情况下,你都务必要记住:不要在构造器中调用可覆 写的方法。在实例初始化中产生的循环将是致命的。
19>名字重用的术语表
覆写(override)
一个实例方法可以覆写(override)在其超类中可访问到的具有相同签名的所有实例方法[JLS 8.4.8.1],从而使能了动态分派(dynamic dispatch);换句话说,VM将基于实例的运行期类型来选择要调用的覆写方法[JLS 15.12.4.4]。覆写是面向对象编程技术的基础,并且是唯一没有被普遍劝阻的名字重用形式:
class Base {
public void f() { }
}
class Derived extends Base {
public void f() { } // overrides Base.f()
}
隐藏(hide)
一个域、静态方法或成员类型可以分别隐藏(hide)在其超类中可访问到的具有相同名字(对方法而言就是相同的方法签名)的所有域、静态方法或成员类型。隐藏一个成员将阻止其被继承[JLS 8.3, 8.4.8.2, 8.5]:
class Base {
public static void f() { }
}
class Derived extends Base {
private static void f() { } // hides Base.f()
}
重载(overload)
在某个类中的方法可以重载(overload)另一个方法,只要它们具有相同的名字和不同的签名。由调用所指定的重载方法是在编译期选定的[JLS 8.4.9, 15.12.2]:
class CircuitBreaker {
public void f(int i) { } // int overloading
public void f(String s) { } // String overloading
}
遮蔽(shadow)
一个变量、方法或类型可以分别遮蔽(shadow)在一个闭合的文本范围内的具有相同名字的所有变量、方法或类型。如果一个实体被遮蔽了,那么你用它的简单名是无法引用到它的;根据实体的不同,有时你根本就无法引用到它[JLS 6.3.1]:
class WhoKnows {
static String sentence = "I don't know.";
public static woid main(String[ ] args) {
String sentence = “I know!”; // shadows static field
System.out.println(sentence); // prints local variable
}
}
尽管遮蔽通常是被劝阻的,但是有一种通用的惯用法确实涉及遮蔽。构造器经常将来自其所在类的某个域名重用为一个参数,以传递这个命名域的值。这种惯用法并不是没有风险,但是大多数Java程序员都认为这种风格带来的实惠要超过其风险:
class Belt {
private final int size;
public Belt(int size) { // Parameter shadows Belt.size
this.size = size;
}
}
遮掩(obscure)
一个变量可以遮掩具有相同名字的一个类型,只要它们都在同一个范围内:如果这个名字被用于变量与类型都被许可的范围,那么它将引用到变量上。相似地,一个变量或一个类型可以遮掩一个包。遮掩是唯一一种两个名字位于不同的名字空间的名字重用形式,这些名字空间包括:变量、包、方法或类型。如果一个类型或一个包被遮掩了,那么你不能通过其简单名引用到它,除非是在这样一个上下文环境中,即语法只允许在其名字空间中出现一种名字。遵守命名习惯就可以极大地消除产生遮掩的可能性[JLS 6.3.2, 6.5]:
public class Obscure {
static String System; // Obscures type java.lang.System
public static void main(String[ ] args) {
// Next line won't compile: System refers to static field
System.out.println(“hello, obscure world!”);
}
}
20>本书小结:
a,long类型应加L;
b,?:操作数时,2,3操作数应使用相同的数据类型。
c,整数类型是不对称的。
d,Integer.MAX_VALUE上终止以int为索引的循环是困难的。(ps:Integer.MAX_VALUE=9223372036854775807,呵呵,够大了吧!!0)
e,如果你想让一个类不可实例化,那么就添加一个私有构造器吧。
f,char类型值只会默认地转换成int,而不是String,想把一个char的类型转换成一个字符串,应该使用String.valueOf(char)。
g,永远不要使用Thread.run()。
h,覆写了equals方法而不覆写hashCode方法可能会引发不定的行为,所以覆写了equals方法就要一并覆写hashCode方法。