《Java解惑》读书笔记

来源:互联网 发布:易语言数据库排序 编辑:程序博客网 时间:2024/06/05 16:01

《Java解惑》笔记

  • Java解惑笔记
    • 表达式谜题
    • 字符谜题
    • 循环谜题
    • 异常谜题
    • 类谜题
    • 库谜题
    • 更多类谜题
    • 更多库谜题
    • 高级谜题


表达式谜题

  1. 使用取余操作符时注意结果的符号性(1,-1),否则对负数不起作用;
  2. 并不是所有的小数都可以用二进制浮点数来精确表示,float、double对货币之类的精确计算非常不合适,1.1会被表示为最接近它的double值。使用BigDecimal(String)而不是BingDecimal(double)来执行精确小数运算。
  3. 当计算的因子都是int而结果类型定义为long时,还是会以int来计算,结果再转换为long,这样可能会导致数值溢出,得到不正确的结果。可以使用long类型作为算数的第一个因子。
  4. 注意小写l1的区别。
  5. 如果十六进制和八进制字面常量的最高位被置位了,那么它们就是负数。避免混合类型的计算
  6. 多重类型转换:从 int 到 byte 的转型是很简单的,它执行了一个窄化原始类型转化(narrowing primitive conversion),直接将除低 8 位之外的所有位全部砍掉。从 byte 到 char 的转换被认为不是一个拓宽原始类型的转换,而是一个拓宽并窄化原始类型的转换(widening and narrowing primitive conversion):byte 被转换成了 int,而这个 int 又被转换成了 char。
  7. 利用异或操作符(^)的属性(x ^ y ^ x) == y 来避免使用临时变量来交换的做法在Java中行不通:操作符的操作数是从左向右求值的。为了求表达式 x ^=expr 的值,x 的值是在计算 expr 之前被提取的,并且这两个值的异或结果被赋给变量 x。在 CleverSwap 程序中,变量 x 的值被提取了两次——每次在表达式中出现时都提取一次——但是两次提取都发生在所有的赋值操作之前。 在单个的表达式中不要对相同的变量赋值两次。
  8. 对于条件表达式:
    • 通常最好是在条件表达式中使用类型相同的第二和第三操作数
    • 如果第二个和第三个操作数具有相同的类型,那么它就是条件表达式的类型。换句话说,你可以通过绕过混合类型的计算来避免大麻烦。
    • 如果一个操作数的类型是 T,T 表示 byte、short 或 char,而另一个操作数是一个 int 类型的常量表达式,它的值是可以用类型 T 表示的,那么条件表达式的类型就是 T。
    • 否则,将对操作数类型运用二进制数字提升,而条件表达式的类型就是第二个和第三个操作数被提升之后的类型。
  9. 复合赋值表达式自动地将它们所执行的计算的结果转型为其左侧变量的类型。(复合赋值操作符包括+=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^=和|=
  10. 复合赋值操作符要求两个操作数都是原始类型的,例如 int,或包装了的原始类型,例如 Integer,但是有一个例外:如果在+=操作符左侧的操作数是String类型的,那么它允许右侧的操作数是任意类型,在这种情况下,该操作符执行的是字符串连接操作。简单赋值操作符(=)允许其左侧的是对象引用类型,这就显得要宽松许多了:你可以使用它们来表示任何你想要表示的内容,只要表达式的右侧与左侧的变量是赋值兼容的即可。

字符谜题

  1. +链接字符型字面常量时,因为两个操作数都不是字符串类型的,所以 + 操作符执行的是加法而不是字符串连接,会转换成int然后相加。要将字符连接在一起,可以使用:
    • 预置一个空字符串("" + 'a' + 'b');
    • 将第一个数值用 String.valueOf 显式地转换成一个字符串;
    • 使用一个字符串缓冲区StringBuffer
    • 或者如果你使用的 JDK 5.0,可以用 printf 方法System.out.printf("%c%c", 'H', 'a');
  2. println(char[])会进行重载打印出内容,而+进行字符串连接使用的toString()方法打印出的还是内存地址
    • 同时使用+==时,+的优先级高;
    • 比较对象引用时,你应该优先使用 equals 方法而不是 == 操作符
  3. System.out.println("a\u0022.length()+\u0022b".length());实际上是System.out.println("a".length()+"b".length());。编译器在将程序解析成各种符号之前,先将 Unicode转义字符转换成为它们所表示的字符。
  4. Unicode 转义字符必须是良构的,即使是出现在注释中也是如此,即\u后面必须跟四个十六进制的数字总之,要确保字符\u 不出现在一个合法的 Unicode 转义字符上下文之外,即使是在注释中也是如此。
  5. 编译器不仅会在将程序解析成为符号之前把 Unicode 转义字符转换成它们所表示的字符(谜题 14),而且它是在丢弃注释和空格之前做这些事的[JLS 3.2]。
  6. 可以完全用unicode字符来编写程序,但当然很不好。Unicode 转义字符只有在你要向程序中插入用其他任何方式都无法表示的字符时才是必需的,除此之外的任何情况都不应该避免使用它们。Unicode 转义字符降低了程序的清晰度,并且增加了产生 bug 的可能性。
  7. 在通过解码使用平台缺省字符集的指定 byte 数组来构造一个新的 String 时,该新String 的长度是字符集的一个函数,因此,它可能不等于 byte 数组的长度。当给定的所有字节在缺省字符集中并非全部有效时,这个构造器的行为是不确定的。每当你要将一个 byte 序列转换成一个 String 时,你都在使用某一个字符集,不管你是否显式地指定了它。如果你想让你的程序的行为是可预知的,那么就请你在每次使用字符集时都明确地指定。
  8. 注意注释的意外结束,注释不能嵌套。块注释不能可靠地注释掉代码段,注释掉一个代码段的最好的方式是使用单行的注释序列Alt text
  9. String.replaceAll 会接受一个正则表达式作为它的第一个参数,而并非接受了一个字符序列字面常量。5.0 版本提供了新的静态方法java.util.regex.Pattern.quote。它接受一个字符串作为参数,并可以添加必需的转义字符,它将返回一个正则表达式字符串,该字符串将精确匹配输入的字符串。
  10. String.replaceAll 的第二个参数不是一个普通的字符串,而是一个替代字符串(replacement string),就像在 java.util.regex 规范中所定义的
    那样[Java-API]。在替代字符串中出现的反斜杠会把紧随其后的字符进行转义,从而导致其被按字面含义而处理了。当你在 Windows 上运行该程序时,替代字符串是单独的一个反斜杠,它是无效的。5.0 版本提供了不是一个而是两个新的方法来解决它:
    • 第一个方法是 java.util.regex.Matcher.quoteReplacement,它将字符串转换成相应的替代字符串;
    • 第二个方法提供了一个更好的解决方案。该方法就是
      String.replace(CharSequence, CharSequence),它做的事情和String.replaceAll 相同,但是它将模式和替代物都当作字面含义的字符串处
      理。
  11. 可以在任何语句前面放置标号。这个程序标注了一个表达式语句,它是合法的,但是却没什么用处。Alt textAlt text
  12. Random.nextInt(int)的规范描述道:“返回一个伪随机的、均等地分布在从 0(包括)到指定的数值(不包括)之间的一个 int 数值”[Java-API]。这意味着表达式 rnd.nextInt(2)可能的取值只有 0 和 1,注意数值范围操作的端点;没有任何 break 语句时,不论 switch 表达式为何值,该程序都将执行其相对应的 case 以及所有后续的 case;StringBuffer 有一个无参数的构造器,一个接受一个 String 作为字符串缓冲区初始内容的构造器,以及一个接受一个 int 作为缓冲区初始容量的构造器,但没有接收char的构造器(会转换成int)。

    循环谜题

  13. byte 是有符号类型。常量 0x90 是一个正的最高位被置位的 8 位 int 数值。合法的 byte 数值是从-128 到+127,但是 int 常量 0x90 等于+144。((byte)0x90 == 0x90)返回的结果是false。Java 通过拓宽原始类型转换 将 byte 提升为一个 int[JLS 5.1.2],然后比较这两个 int 数值。因为 byte 是一个有符号类型,所以这个转换执行的是符号扩展,将负的 byte 数值提升为了 在数字上相等的 int 数值。在本例中,该转换将(byte)0x90 提升为 int 数值-112, 它不等于 int 数值 0x90,即+144。 解决这个问题,可以先将int转换为byte,或者用(b & 0xff)来消除符号扩展的影响。要避免混合类型比较,因为它们内在地容易引起混乱。
  14. i++整体的值还是i,避免出现i = i++;这种语句;不要在单个的表达式中对相同的变量赋值超过一次;
  15. int 不能表示所有的整数, 避免使用Integer.MAX_VALUE作为int循环的上限,当 i 达到 Integer.MAX_VALUE,并且再次被执行增 量操作时,它就有绕回到了 Integer.MIN_VALUE,程序永远无法结束。要解决这个问题,可以把变量设置成long格式;
  16. 移位长度是对 32 取余的,或者如果左操作数是 long 类型的,则对 64 取 余。因此,使用任何移位操作符和移位长度,都不可能将一个数值的所有位全部 移走。同时,我们也不可能用右移操作符来执行左移操作,反之亦然。
  17. 浮点数操作返回的是最接近其精确的数学结果的浮点数值。一旦毗邻的浮点数值之间的距离大于 2,那么对其中的一个浮点数值加 1 将不会产生任何效果,因为 其结果没有达到两个数值之间的一半。对于 float 类型,加 1 不会产生任何效果 的最小级数是 225,即 33,554,432;而对于 double 类型,最小级数是 254,大约是 1.8 × 1016。 对于i==i+1会返回正确的结果
  18. 满足i!=i的值:NaNdouble i = 0.0 / 0.0;double i = Double.NaN; 。任何浮点操作,只要它的一个或多个操作数为 NaN, 那么其结果为 NaN。
  19. 满足i != i + 0的值:i 的类型必须是非数值类型的
  20. 不要在 short、byte 或 char 类型的变量之上使用复合赋值操作符。因为 这样的表达式执行的是混合类型算术运算,它容易造成混乱。更糟的是,它们执 行将隐式地执行会丢失信息的窄化转型,其结果是灾难性的。在循环中可能因此无法达到指定值而无限循环
  21. 实数上的≤关系是反对称的。Java 的<=操作符在 5.0 版之前是 反对称的,但是这从 5.0 版之后就不再是了。新规范描述道:每一个操作数的类型必须可以转换成原始数字类型。对于i <= j && j <= i && i != jInteger i = new Integer(0);Integer j = new Integer(0); 可以满足。它们在前两个比较时比较int值的大小,而最后一个比较则是基于对象,无法相等。
  22. 2 的补码算术运算的一 个很大的优势是,0 具有唯一的表示形式。如果你要对 int 数值 0 取负值,你将 得到 0xffffffff+1,它仍然是 0。
    但是,这也有一个相应的不利之处,总共存在偶数个 int 数值——准确地说有 2^32个——其中一个用来表示 0,这样就剩些奇数个 int 数值来表示正整数和负整 数,这意味着正的和负的 int 数值的数量必然不相等。这暗示着至少有一个 int 数值,其负值不能正确地表示成为一个 int 数值,它就是 Integer.MIN_VALUE,即-231。他的十六进制表示是 0x80000000。其符号位为 1,其余所有的位都是 0。如果我 们对这个值取负值,那么我们将得到 0x7fffffff+1,也就是 0x80000000,即 Integer.MIN_VALUE!换句话说,Integer.MIN_VALUE 是它自己的负值, Long.MIN_VALUE 也是一样。
  23. 在将一个 int 与一个 float 进行比较时,会自动执行从 int 到 float 的提升,这种提升是会导致精度丢失的三种拓宽原始类型转换的一种(另外两个是从 long 到 float 和从 long 到 double。) (float)2000000000 == 2000000050
  24. 布尔表达式(ms % 60*1000 == 0)。你可能会认为这个表达式等价 于(ms % 60000 == 0),但是它们并不等价。取余和乘法操作符具有相同的优先 级[JLS 15.17],因此表达式 ms % 60*1000 等价于(ms % 60)*1000

异常谜题

  1. tryfinally语句块中都有return语句时,最终返回的是finally的结果。finally 语句块总是在控制权离开 try 语 句块时执行的[JLS 14.20.2]。无论 try 语句块是正常结束的,还是意外结束的, 情况都是如此。简单地讲,程序尝试着(try)返回(return)true,但是它最终(finally) 返回(return)的是 false。 每一个 finally 语句块都应该正常结束,除非抛出的是不受检查的异常。 千万不要用一个 return、break、continue 或 throw 来退出一个 finally 语句块。
    • 如果一个 catch 子句 要捕获一个类型为 E 的被检查异常,而其相对应的 try 子句不能抛出 E 的某种子 类型的异常,那么这就是一个编译期错误;
    • 但是捕获 Exception 或 Throwble 的 catch 子 句是合法的,不管与其相对应的 try 子句的内容为何;
    • 一个方法可以抛出的被检查异常集合是它所适 用的所有类型声明要抛出的被检查异常集合的交集,而不是合集,多个继承而来的 throws 子句的交 集,将减少而不是增加方法允许抛出的异常数量。
  2. 在程序中,一个空 final 域只有在它是明确未赋过值的地方才可 以被赋值。因此不能再catch语句中给final变量赋值。
  3. 如果在try语句中使用System.exit(0)结束进程,finally语句块则不会执行。不论 try 语句块的执行是正常地还是意外地结束,finally 语句块确实都会执行。 然而在这个程序中,try 语句块根本就没有结束其执行过程。System.exit 方法 将停止当前线程和所有其他当场死亡的线程。finally 子句的出现并不能给予线 程继续去执行的特殊权限。
  4. 对于一个对象包含与它自己类型相同的实例的情况下,如果在构造器的声明 中看到一个 throws 子句,实例初始化操作是先于构造器的程序体而运行的。实例初始化操作抛出的 任何异常都会传播给构造器。如果初始化操作抛出的是被检查异常,那么构造器 必须声明也会抛出这些异常。那么在实例化时会递归调用最后产生StackOverflowError,而不能用catch捕获。
  5. 当你在 finally 语句块中调用 close 方法时,要用一个嵌套的 try-catch 语句来保护它,以防止 IOException 的传播。更一般地讲,对于任何在 finally 语句块中可能会抛出的被检查异常都要进行处理,而不是任其传播。
  6. 不要使用异常来进行循环控制应该只为异常条件而使用异常,不要去用那些可怕的使用异常而不是使用显式的终止测试的循环惯用法,因为这种惯用法非常不清晰,而且会掩盖 bug。;注意区分&&&的用法,意识到逻辑 AND 和 OR 操作符 的存在。
  7. Thread.currentThread().stop(t);可以做出throw 语句要做的事情,但是它绕过了编译器的所有异常检查操作;不使用任何不推荐的方法,你也可以编写出在功能上等价于 此的方法。
    Java 的异常检查机制并不是虚拟机强制执行的。它只是一个编译期工具, 被设计用来帮助我们更加容易地编写正确的程序,但是在运行期可以绕过它。
    • 一种解决之道是利用 Class.newInstance 方法中的设计缺陷,该方法通 过反射来对一个类进行实例化。该方法将传播从空的[换句话说,就是无参数的]构造器所抛出的任何异常, 包括受检查的异常。使用这个方法可以有效地绕开在其他情况下都会执行的编译 期异常检查。
    • 另一种是利用泛型,在编译时将产生一条警告信息。
  8. 不要对捕获 NoClassDefFoundError 形成依赖。语言规范非常仔细地描述 了类初始化是在何时发生的[JLS 12.4.1],但是类被加载的时机却显得更加不可 预测。更一般地讲,捕获 Error 及其子类型几乎是完全不恰当的。这些异常是为 那些不能被恢复的错误而保留的。
  9. workHard 方法递归地 调用它自身,直到程序抛出 StackOverflowError,在此刻它以这个未捕获的异 常而终止。但是,try-finally 语句把事情搞得复杂了。当它试图抛出 StackOverflowError 时,程序将会在 finally 语句块的 workHard 方法中终止, 这样,它就递归调用了自己。这看起来确实就像是一个无限循环的秘方。假设栈的深度很小为3,运行时有一个深度为 0 的调用(即 main 中的调用),两个深度为 1 的调用, 四个深度为 2 的调用,和八个深度为 3 的调用,总共是 15 个调用。那八个深度 为 3 的调用每一个都会立即产生 StackOverflowError。在栈深度为1024的情况下,这个程序接近于无限循环。

类谜题

  1. Java 的重载解析过程是以两阶段运行的。第一阶段选取所有可获得并且可应用 的方法或构造器。第二阶段在第一阶段选取的方法或构造器中选取最精确的一 个。如果一个方法或构造器可以接受传递给另一个方法或构造器的任何参数,那 么我们就说第一个方法比第二个方法缺乏精确性。一旦编译器确定了哪 些重载版本是可获得且可应用的,它就会选择最精确的一个重载版本,而此时使 用的仅仅是形式参数:即出现在声明中的参数。
  2. 在设计一个类的时候,如果该类构建于另一个类的行为之上,那么你有两种选择: 一种是继承,即一个类扩展另一个类;另一种是组合,即在一个类中包含另一个 类的一个实例。选择的依据是,一个类的每一个实例都是另一个类的一个实例, 还是都有另一个类的一个实例。在第一种情况应该使用继承,而第二种情况应该 使用组合。当你拿不准时,优选组合而不是继承 。
    静态域由声明它的类及其所有子类所共享。如果你需要让每一个子类都具 有某个域的单独拷贝,那么你必须在每一个子类中声明一个单独的静态域。如果 每一个实例都需要一个单独的拷贝,那么你可以在基类中声明一个非静态域。
  3. 对静态方法的调用不存在任何动态的分派机 制[JLS 15.12.4.4]。当一个程序调用了一个静态方法时,要被调用的方法都是 在编译时刻被选定的,而这种选定是基于修饰符的编译期类型而做出的,修饰符 的编译期类型就是我们给出的方法调用表达式中圆点左边部分的名字。即,静态方法无法在子类中进行复写。
  4. 要当心类初始化循环。最简单的循环只涉及到一个单一的类,但是它们也 可能涉及多个类。类初始化循环也并非总是坏事,但是它们可能会导致在静态域 被初始化之前就调用构造器。静态域,甚至是 final 类型的静态域,可能会在它 们被初始化之前,被读走其缺省值。
  5. 尽管 null 对于每一个引用类型来说都是其子类型,但是 instanceof 操作符被定 义为在其左操作数为 null 时返回 false;instanceof 操作符有这样的要求:如果两个操作数的类 型都是类,其中一个必须是另一个的子类型,无法比较两个毫不相关的类;与 instanceof 操作相同,如果在一个转型操作中的两 种类型都是类,那么其中一个必须是另一个的子类型。
  6. 千万不要在构造器中调用可覆写的方法,直接调用或间接调用都 不行。在实例初始化中产生的循环将是致命的。该问题的解决方案就是惰性初始化。
  7. 要么使用惰性初始化,但是千万不要同时使用二
    者。 如果初始化一个域的时间和空间代价比较低,或者该域在程序的每一次执行中都 需要用到时,那么使用积极初始化是恰当的。如果其代价比较高,或者该域在某 些执行中并不会被用到,那么惰性初始化可能是更好的选择[EJ Item 48]。另外, 惰性初始化对于打破类或实例初始化中的循环也可能是必需的。
  8. 交替构造器调用机制(alternate constructor invocation) [JLS 8.8.7.1]。这个特征允许一个类中的某个构造器链接调用同一个类中的另 一个构造器。在本例中,MyThing()链接调用了私有构造器 MyThing(int),它执 行了所需的实例初始化。在这个私有构造器中,表达式 SomeOtherClass.func() 的值已经被捕获到了变量 i 中,并且它可以在超类构造器返回之后存储到 final 类型的域 param 中。
  9. 要么用某种类型来限定静态方法调 用,要么就压根不要限定它们。不允许用表达式来污染 静态方法调用的可能性存在,因为它们只会产生混乱。
  10. Java 语言规范不允许一个本地变量声明语句作为一条语句在 for、while 或 do 循环中重复执行,一个本地变量声明作为一条语句只能直接出现在一个语句块中。(一个语句块是由一对花括号以及包含在这对花括展中的语句 和声明构成的。)即不能在没有花括号的循环语句中声明变量。另外,在使用一个变量来对实例的创 建进行计数时,要使用 long 类型而不是 int 类型的变量。

库谜题

  1. BigInteger 实例是不可变的。StringBigDecimal 以及包装器类型:IntegerLongShortByteCharacterBoolean、 Float 和 Double 也是如此,你不能修改它们的值。我们不能修改现有实例的值, 对这些类型的操作将返回新的实例。
  2. 无论 何时,只要你覆写了 equals 方法,你就必须同时覆写 hashCode 方法,两个实 例必须具有相同的散列值才能让散列集合能够将它们识别为是相等的。
  3. 当你想要进行覆写时,千万不要进行重载。例如重写equals()方法,要注意参数是Object
  4. 以 0 开头的整数类型字面 常量将被解释成为八进制数值,千万不要在一个整型字面常量的前面加上一个 0。
    • ArrayList去重:new ArrayList<E>(new LinkedHashSet<E>(original));
    • 显示嵌套数组的详细信息:Arrays.deepToString()
  5. Date/Calendar 问题:Date 将一月表示为 0, 而 Calendar 延续了这个错误。Calendar 方法 get(Calendar.DAY_OF_WEEK) 不知为什么返回的是基于 1 的星期 日期值,而不是像 Date 的对应方法那样返回基于 0 的星期日期值。Calendar 其他的严重问题包括弱类型(几乎每样事物都是一个 int)、过于复杂 的状态空间、拙劣的结构、不一致的命名以及不一致的雨衣等。在使用 Calendar 和 Date 的时候一定要当心,千万要记着查阅 API 文档。
  6. 不要使用 IdentityHashMap,除非你需要其基于标识 的语义;它不是一个通用目的的 Map 实现。这些语义对于实现保持拓扑结构的对 象图转换(topology-preserving object graph transformations)非常有用, 例如序列化和深层复制;是字符串常量是内存限定的。
  7. 不要因为偶然地添加了一个返回类型,而将一个构造器声明变 成了一个方法声明。尽管一个方法的名字与声明它的类的名字相同是合法的,但 是你千万不要这么做。更一般地讲,要遵守标准的命名习惯,它强制要求方法名 必须以小写字母开头,而类名应该以大写字母开头。
  8. 对于Math.abs,如果其参数等于Integer.MIN_VALUE,那么产生的 结果与该参数相同,它是一个负数。这种行为的 根源在于 2 的补码算数具有不对称性。没有任何 int 数值可以表示 Integer.MIN_VALUE 的负值,也没有任何 long 数值可以表示 Long.MIN_VALUE 的负值。
  9. 定长的整数没有大到可以保存任意两个同 等长度的整数之差的程度。所以Comparator.compare在用来进行比较的两个数字的差大于 Integer.MAX_VALUE 的时候会出问题。所以不要使用基于减法的比较器,除非你能够确保 要比较的数值之间的差永远不会大于 Integer.MAX_VALUE

更多类谜题

  1. 一旦一个方法在子类中被覆写, 你就不能在子类的实例上调用它了(除了在子类内部,通过使用 super 关键字来 方法)。然而,你可以通过将子类实例转型为某个超类类型来访问到被隐藏的域, 在这个超类中该域未被隐藏。 当你在声明一个域、一个静态方法或一个嵌套类型时,如果其名字与基类 中相对应的某个可访问的域、方法或类型相同,就会发生隐藏。隐藏是容易产生 混乱的:违反包容性的隐藏域在某种意义上是特别有害的。更一般地讲,除了覆 写之外,要避免名字重用。
  2. 如果你确实需要编写自己的字符串类,要避免重用平台类的名字(String),并且千万不要重用 java.lang 中的类名,因 为这些名字会被各处的程序自动加载。
  3. 当一个变 量和一个类型具有相同的名字,并且它们位于相同的作用域时,变量名具有优先 权。变量名将遮掩(obscure)类型名。相似地,变量名和类型名可以遮掩包名。应该遵守标准的命名习惯以避免不同的命名空间之间的冲突,为了避免变 量名与通用的顶层包名相冲突,请使用 MixedCase 风格的类名,即使其名字是首 字母缩拼词也应如此。
  4. 承接上面的情况,如果不修改名字,可以用((X.Y)null).Zstatic class Xy extends X.Y{ }<T extends X.Y>来取到类型而不是变量
  5. 包内私有的方法不能直接被包外的方法声明所覆写。包内私有的方法是它们所属包的实现细节, 在包外重用它们的名字是不会对包内产生任何影响的。
  6. 一个声明只能遮蔽类型相同的另一个声明:一个类型声明可以遮蔽另一个
    类型声明,一个变量声明可以遮蔽另一个变量声明,一个方法声明可以遮蔽另一
    个方法声明。与其形成对照的是,变量声明可以遮掩类型和包声明,而类型声明
    也可以遮掩包声明。
    当某个方法的名字已经出现在某个作用域内时,静 态导入工具并不能被有效地作用于该方法上。这意味着静态导入不能用于那些与 通用接口中的方法共享方法名的静态方法,而且也从来不能用于那些与 Object 中的方法共享方法名的静态方法。
  7. ,final 修饰符对方法和域而言,意味着某些完全不同的事情。对于方 法,final 意味着该方法不能被覆写(对实例方法而言)或者隐藏(对静态方法 而言)。对于域,final 意味着该域不能被赋值超过一次。关键字相同,但是其行为却完全不相关。
  8. 如果使一个类成为可序列化的,并且接受缺省的序列化形式,那么该类的私有实例域将成为其导出 API 的一部分。
  9. 如果你重载了一个方法,那 么一定要确保所有的重载版本行为一致。
  10. 条件操作符(?:)的行为在 5.0 版本之前是非常受限的[JLS2 15.25]。当第二个和第三个操作数是引用类型时,条件操作符要求它们其中的一个必须是另一个的子类型。在 5.0 或更新的版本中,Java 语言显得更加宽大了,条件操作符在第二个和第 三个操作数是引用类型时总是合法的。其结果类型是这两种类型的最小公共超 类。

更多库谜题

  1. 当你想调用一个线程的 start 方法时要多加小心,别弄错成调 用这个线程的 run 方法了。Thread 类之所以 有一个公共的run 方法,是因为它实现了 Runnable 接口,但是这种方式并不是必须的。另外一种可选的设计方案是:使用组合(composition)来替代接口继承(interface inheritance),让每个 Thread 实例都封装一个 Runnable
  2. 无论 是 Timer 类还是 Thread.sleep 方法,都不能保证具有实时(real-time)性。这 就是说,由于这里计时的粒度太粗,所以几个事件很有可能会在时间轴上互 有重叠地交替发生。Thread.join 方法在表示正在被连接(join)的那个 Thread 实例上调 用 Object.wait 方法。这样就在等待期间释放了该对象上的锁。除非有关于某个类 的详细说明作为保证,否则千万不要假设库中的这个类对它的实例或类上的锁会 做(或者不会做)某些事情。对于库的任何调用都可能会产生对 waitnotifynotifyAll 方法或者某个同步化方法的调用。如果你需要获得某个锁的完全控制权,那么就要确定没有任何其他人能够访问到 它。如果你的类扩展了库中的某个类,而这个库中的类可能使用了它的锁,或者 如果某些不可信的人可能会获得对你的类的实例的访问权,那么请不要使用与这 个类或它的实例自动关联的那些锁。总之,永远不要假设库类会(或者不会)对它的锁做某些事情。为了隔离你自己 的程序与库类对锁的使用,除了那些专门设计用来被继承的库类之外,请避免继 承其它库类 [EJ Item 15]。为了确保你的锁不会遭受外部的干扰,可以将它们 设为私有以阻止其他人对它们的访问。
  3. 访问位于其他包中的非公共类型的 成员是不合法的[JLS 6.6.1]。无论是一般的访问还是通过反射的访问,上述的 禁律都是有效的,即使这个成员同时也被声明为某个公共类型的公共成员也是如此。
  4. 要小心无意间产生的遮蔽,并且要学会识别表明存在这种情况的编译器错 误信息。对于编译器的编写者来说,你应该尽力去产生那些对程序员来说有意义 的错误消息。
  5. 一个非静态的嵌套类的构造器,在编译的时候会将一个隐藏的参数作为它的第一 个参数,这个参数表示了它的直接外围实例。当你使用反射调用构造器时,这个 隐藏的参数就需要被显式地传递,这对于 Class.newInstance 方法是不可能做到 的。要传递这个隐藏参数的唯一办法就是使用 java.lang.reflect.Constructor。避免使用反射来实例化内 部类。
  6. System.out 和 System.err这 2 个流都属于 PrintStream 类型,write(int)是唯一一 个在自动刷新(automatic flushing)功能开启的情况下不刷新 PrintStream 的输 出方法(output method)。
  7. Process 类的文档:“由于某些本地平 台只提供有限大小的缓冲,所以如果未能迅速地读取子进程(subprocess)的输出 流,就有可能会导致子进程的阻塞,甚至是死锁”。为了确保子进程能够结束,你必须排空它的输出流;对于错误流 (error stream)也是一样,而且它可能会更麻烦,因为你无法预测进程什么时 候会倾倒(dump)一些输出到这个流中。
  8. 一个实现了 Serializable 的单件类,必须有一个 readResolve 方法,用以返回它的唯一的实例,否则序列化再反序列化之后,就会创建另一个对象。一个次要的教训就是,有可能由 于对一个实现了 Serializable 的类进行了扩展,或者由于实现了一个扩展自 Serializable 的接口,使得我们在无意中实现了 Serializable。
  9. 调用 Thread.interrupted 方法总是会 清除当前线程的中断状态。所以不要使用 Thread.interrupted 方法,除非你想要清除当前 线程的中断状态。如果你只是想查询中断状态,请使用 isInterrupted 方法。
  10. 不要在类进行初始化的 时候启动任何后台线程:在类的初始化期间等待某个后台线程很可能会造成死锁。有些时候,2 个线程并不比 1 个线程好。更一般的讲, 要让类的初始化尽可能地简单。

高级谜题

  1. 在两种情况下,插入 一对看上去没有影响的括号可能会令合法的 Java 程序变得不合法。这种奇怪的 情况是由于数值的二进制补码的不对称性引起的。Java 不支持负的十进制字面常量;int 和 long 类型的负数常量都是由正数十进 制字面常量前加一元负操作符(-)构成。这种构成方式是由一条特殊的语言规 则所决定的:在 int 类型的十进制字面常量中,最大的是 2147483648。符号-2147483648 构成了一个合 法的 Java 表达式,它由一元负操作符加上一个 int 型字面常量 2147483648 组成。 通过添加一对括号来注解(很不重要的)赋值顺序,即写成-(2147483648), 就会破坏这条规则。类似地,上述情况也适用于 long 型字面常量。
  2. 关系 是一种等价关系,当且仅当它是自反的、传递的和对称的。这些性质定义如下:
    • 自反性:对于所有 x,x ~ x。也就是说,每个值与其自身存在关系 ~ 。
    • 传递性:如果 x ~ y 并且 y ~ z,那么 x ~ z。也就是说,如果第一 个值与第二个值存在关系 ~,并且第二个值与第三个值存在关系 ~ , 那么第一个值与第三个值也存在关系 ~ 。
    • 对称性:如果 x ~ y,那么 y ~ x。也就是说,如果第一个值和第二个 值存在关系 ~ ,那么第二个值与第一个值也存在关系 ~ 。
      当比较两个原始类型数值时,操作符 == 首先进行二进制数 据类型提升(binary numeric promotion)[JLS 5.6.2]。这会导致这两个数值 中有一个会进行拓宽原始类型转换(widening primitive conversion)。大部 分拓宽原始类型转换是不会有问题的,但有三个值得注意的异常情况:将 int 或 long 值转换成 float 值,或 long 值转换成 double 值时,均会导致精度丢失。 这种精度丢失可以证明 == 操作符的不可传递性。 要警惕到 float 和 double 类型的拓宽原始类型转换所造成的 损失。它们是悄无声息的,但却是致命的。它们会违反你的直觉,并且可以造成 非常微妙的错误。
  3. 在泛型中,一个原生类型很像其对应的参数化类型,但是它的所有实例成员都要被替换掉, 而替换物就是这些实例成员被擦除掉对应部分之后剩下的东西。具体地说,在一 个实例方法声明中出现的每个参数化的类型都要被其对应的原生部分所取代。原生类型的成员被擦掉,是为了模拟泛型被添加到语言中之前的那些类型 的行为。如果你将原生类型和参数化类型混合使用,那么便无法获得使用泛型的 所有好处,而且有可能产生让你困惑的编译错误。另外,原生类型和以 Object 为类型参数的参数化类型也不相同。
  4. 泛型类的内部类可以访问到其外围类的类型参数,这可能会使得程序模糊 难懂。当你在一个泛型类中嵌套另一个泛型类时,最好 为它们的类型参数设置不同的名字,即使那个嵌套类是静态的也应如此。
  5. 要想实例化一个内部类,如类 Inner1,需要提供一个外部类的实例 给构造器。一般情况下,它是隐式地传递给构造器的,但是它也可以以 expression.super(args)的方式通过超类构造器调用(superclass constructor invovation)显式地传递[JLS 8.8.7]。如果外部类实例是隐式传递的,编译器会 自动产生表达式:它使用 this 来指代最内部的其超类是一个成员变量的外部类。无论何时你 写了一个成员类,都要问问你自己,是否这个成员类真的需要使用它的外部类实 例?如果答案是否定的,那么应该把它设为静态成员类。内部类有时是非常有用 的,但是它们很容易增加程序的复杂性,从而使程序难以被理解。一个类既是外部类又是其他类的超类的方式是很不合理的。更一般地 讲,扩展一个内部类的方式是很不恰当的;如果必须这样做的话,你也要好好考 虑其外部类实例的问题。另外,尽量用静态嵌套类而少用非静态的[EJ Item 18]。 大部分成员类可以并且应该被声明为静态的。
  6. 在某些情况下,HashSet 的 readObject 方法会间接地调用某 个未初始化对象的被覆写的方法。为了组装(populate)正在被反序列化的散列集 合,HashSet.readObject 调用了 HashMap.put 方法,而它会去调用每个键(key) 的 hashCode 方法。由于整个对象图(object graph)正在被反序列化,并没有 什么可以保证每个键在它的 hashCode 方法被调用的时候已经被完全初始化了。 实际上,这很少会成为一个问题,但是有时候它会造成绝对的混乱。这个缺陷会 在正在被反序列化的对象图的某些循环中出现。 包含了 HashMapreadObject 方法的序列化系统总体上违背了 不能从类的构造器或伪构造器(pseudoconstructor)中调用其可覆写方法的规 则。在 readObject 或 readResolve 方法中,请避免直接或间接地在正在进行反序列化的对象上调用任 何方法。如果你必须在某个类型 C 的 readObject 或 readResolve 方法中违背这 条建议,请确定没有 C 的实例会出现在正在被反序列化的对象图的某个循环内。 不幸的是,这不是一个本地的属性:一般说来,你需要考虑到整个系统来验证这 一点。
  7. 私有成员不会被继承。
  8. null 不是一个编译期常量表达式。由于常量域将会编译进客户端,如果你想让客户端程序感知并适应这个 域的变化,那么就不能让这个域成为一个常量。如果你使用了一个非常量的表达式去初始化一个域,甚至是一个 final 域, 那么这个域就不是一个常量。你可以通过将一个常量表达式传给一个方法使得它 变成一个非常量,该方法将直接返回其输入参数。
  9. 像很多算法一样,打乱一个数组是需要慎重对待的。这么做很容易犯错并 且很难发现错误。在其他条件相似的情况下,你应该优先使用类库而不是手写的 代码。
  10. 编写代码时注意标点符号和括号的位置,可能会带来意想不到的结果。