《Java解惑》读书笔记

来源:互联网 发布:db2 执行sql 编辑:程序博客网 时间:2024/06/06 02:33


摘选自《Java解惑》一书,之前整理了部分,一直没看完,最近为了督促自己每天读点这本书,决定一天至少更新一个谜题的内容,欢迎讨论。

欢迎关注技术博客http://blog.sina.com.cn/u/1822488043


Java解惑读书笔记

谜题1:奇数性

取余操作的定义:

( a / b ) * b + ( a % b ) = a

其中(a/b)java运算的结果,也就是a/b是一个整数,比如3/2=1

所以当取余操作返回一个非零结果的时候,它与左操作数具有相同符号。

请测试你的方法在为每一个数值型参数传递负数、零和正数数值时,其行为是否正确。

判断奇偶更好的办法:

i & 1 == 0为偶数,因为二进制原因导致,只有第一位为1,才是奇数,想想二进制的第一位即可。

仓促地优化是不好的。

使用取余操作的时候,要考虑到操作数一个或者两个是负数的情况。

 

谜题2找零时刻

double类型的值转换为字符串,是将其转换成足以将double类型的值与最靠近它的临近值区分出来的最短小数。(这部分待解释

并不是所有的小树都可以用二进制浮点数精确表示。(书中所给的例子2.00-1.10,在C++环境下并没有出错,所以不是最底层的二进制表示的问题,应该是java底层实现的问题)。

二进制浮点对于货币计算是非常不适合的。表示书中所给例子结果出错的原因是1.10不能正确的表示为一个double类型,但是如果将计算过程拆分成2.00-1.00-0.10,那么输出的结果是正确的,也就是这样拆分使用是可以的,但是这显然过于麻烦。

解决的办法有几种,第一种将浮点数扩大几倍,然后完全用intlong型表示所需要表示的数字。第二种,采用BigDecimal类执行精确小数运算。一定要采用BigDecimal(String)构造器,而不是BigDecimal( double )构造器,但是Java并没有为BigDecimal提供任何语言上的支持,所以使用BigDecimal的计算很有可能比使用原生类型的更慢。

 

谜题3 长整除

当在操作很大的数字时,千万要提防溢出。

首先要提防的溢出不是运用的数据类型对接收运算结果是否会溢出,而是提防运算过程中采用的默认数据类型计算,然后再进行原生类型转换之前就发生了溢出。书中例子给出的24 * 60 * 60 * 1000 * 1000的计算中所有数据先默认为了int类型,然后最后将结果提升为long,如果指定计算全部用long类型计算就不会发生溢出,如24L * 60 * 60 * 1000 * 1000

 

谜题4 初级问题

1l在某些字体中区别很小,导致会出现阅读错误的现象(比如Times New Roman字体)。

long类型字面常量中,一定要用大写的L,千万不要用小写的l

同样也要避免使用单个的l字母作为变量名。

 

谜底5:十六进制的趣事

负的十进制常数可以很明确的用一个减号来标识。

对于十六进制和八进制的负数是用数字的最高位来标识,所以不如十进制的数那么明显,同时我们的思维又一般都是十进制的思维,所以经常会忽略掉这种区别。

通常我们需要避免混合类型计算,比如int类型的数据与long类型的数据进行加法,与此同时,最好自己减少使用十六进制字面常量来表示符号没有任何含义的数值。

 

谜底6多重转型

窄化基本类型转换

去掉高位多余位数

 

较窄的类型扩展到较宽的类型

如果最初的类型是有符号的,就执行符号扩展;如果最初类型是无符号的或者char型,执行零扩展。

如果你通过观察不能确定程序将要做什么,那么它做的就很有可能不是你想要的。

 

谜底7互换内容

Java操作符的操作数是从左向右求值的。

在单个表达式中不要对相同的变量赋值两次。

 

谜底8 Dos Equis

混合类型的计算会引起混乱,这点在条件表达式中尤为重要。

在条件表达式中使用类型相同的第二个和第三个操作数。

 

谜底9半斤

复合赋值操作符包括:+=-=*=/=%=<<=>>=>>>=&=^=、和|=

复合赋值表达式自动将所执行计算的结果转型为其左侧变量的类型。

不要将符合赋值操作符作用于byteshortchar类型的变量。尽可能不要使复合赋值表达式的左侧操作符的类型精度低于右侧。

 

谜题十 八两

复合赋值操作符要求两个操作都是基本类型的,或者左侧的操作数是String类型的;

简单赋值操作符(=)允许其左侧的是对象引用的类型,只要表达式的右侧与左侧的变量是赋值兼容的类型即可。

 

谜题十一 最后的笑声

单个字符的连接有以下几个方案:

1.使用字符串缓冲区对象StringBuffer

2.以一个“”空字符串开始连接

3.String.valueOf的方法显式的转换第一个字符为字符串

当且仅当+操作符的操作数中至少有一个是String类型时,才会执行字符串连接操作,否则,执行加法

 

谜题十二 ABC

一个为nullchar型数组的toString结果是”null”,而非空的char型数组的toString方法是直接调用ObjectString方法,其方法的内容是类名,加上@符号,以及表示对象散列码的一个无符号十六进制整数。

想要将一个char数组转换成一个字符串至少有两种方法:

1.调用String.valueOf( char[] );

2.char数组构造String对象

 

谜题十三 动物庄园

String类型的编译期常量是内存限定的。任何两个String类型的常量表达式,如果指定的是相同的字符串序列,那么它们就用同一个对象引用来表示。

==用来测试两个对象的引用是否相同,也就是是不是同一个对象;equals用来检查两个对象的内容是否相等。

+操作符比==操作符的优先级高,注意使用表达式时,各个操作符的优先级。

在使用字符串连接操作符时,总是将重要的操作数用括号括起来。

如果可以的话,你的代码应该很少依赖于字符串常量的内存限定机制。

在比较对象引用时,应该优先使用equals方法而不是==操作符。但是对于基本类型数据,无需有这种考虑,因为对于基本数据,==检测的就是两个数据的内容是否相等。

 

谜题十四 转义字符的溃败

Java对在字符串字面常量中的Unicode转义字符没有提供任何特殊处理。

普通的转义字符序列和八进制转义字符都比Unicode转义字符要好,因为与Unicode转义字符不同,编译器在程序被解析为各种符号之后,才会处理这些转义字符。

一个Unicode转义字符精确地等价于它所表示的字符。它们主要用于将非ASCII字符置于标识符、字符串字面常量、字符字面常量以及注释中。

在字符串和字符字面常量中,优先选择的是转义字符序列,而不是Unicode转义字符。

不要使用Unicode转义字符来表示ASCII字符。

 

谜题十五 令人晕头转向的Hello

Unicode转义字符必须是良构的,即使出现在注释中也是如此。

Javadoc注释中,应该使用HTML实体转义字符来代替Unicode转义字符。

工具应该确保不将Windows文件名置于所生成的Java源文件的注释中。

要确保字符\u不在一个合法的Unicode转义字符上下文之外出现,即使在注释中也是如此。

 

谜题十六 行打印程序

除非确实是必需的,否则就不要使用Unicode转义字符。

 

谜底十七 嗯?

其实大部分估计关注的是那段代码,代码如下:

\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020

\u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079

\u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020

\u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063

\u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028

\u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020

\u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b

\u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074

\u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020

\u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b

\u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d

只有在你要向程序中插入用其他任何方式都无法表示的字符时,Unicode转义字符才是必需的,除此之外的任何情况都应该避免使用它们。

 

谜题十八 字符串奶酪

String(byte[])构造器的规范描述:在通过解码使用平台默认字符集的指定byte数组构造一个新的String时,该新String的长度是字符集的一个函数,因此,它可能不等于byte数组的长度。当给定的所有字节在默认字符集中并非全部有效时,这个构造器的行为是不确定的。

char序列和byte序列之间转换时,可以且通常应该显示地指定字符集。

每当要将一个byte序列转换成一个String时,你都在使用一个字符集,不管是否显式指定了它。

 

谜题十九 漂亮的火花

在注释中没有特殊处理字符串字面常量,会导致注释失败。

块注释不能嵌套。

注释掉代码段的最好方式是使用单行的注释序列。

块注释不能可靠地注释掉代码段,应该用单行的注释序列来代替。

 

谜题二十 我的类是什么

String.replaceAll接受正则表达式作为它的第一个参数(从Java1.4开始已经添加正则表达式),在正则表达式中“.”表示匹配除“\r\n”之外的任何单个字符。因此要想匹配“.”,需要转义字符\,但是\表示转义字符序列的开始,而\.不是转义字符序列,因此要表示为\\.才能正确表达意思,第一个\表示转义字符,转义的是下面一个\,因此两个\\表示得到了一个转义字符要转义其后面的字符“.”

上面为什么要两个\做转义确实挺绕的,但是在Java5.0版本中提供了java.util.regex.Pattern.quote的方法,方法接收一个字符串,返回字符串的正则表达式字符串,因此要匹配“.”,直接用Pattern.quote(“.”)即可解决问题。

 

谜题二十一 我的类是什么?镜头2

File.separator返回平台相关的文件名分隔符。

String.replaceAll的第二个参数是一个替代字符串,在替代字符串中出现“\”会把紧随其后的字符进行转义,于是这样字符串变成转义字符序列了。

Java5.0版本中提供两个解决办法:java.util.regex.Matcher.qutoReplacement方法,这个方法接收字符串,返回该字符串对应的替代字符串;另外,就是使用String.replace方法,方法接收两个字符串,都是字符串常量。

如果早于Java5.0版本,不使用正则表达式的话,就只能使用String.replace(char, char)方法了。

在使用不熟悉的类库方法时一定要格外小心。设计API时,需要将行为差别大的方法在方法命名中予以区分。正则表达式所引发的问题容易在运行时,而不是编译时暴露。

 

谜题二十二 URL的愚弄

URL一般为http://www.google.com形式,直接放在Java代码中加上尾部加上分号,是合法的Java语句,但是其含义是http:为语句标号,//后面为尾注释。

仔细地写注释,并让它们跟上时代;去除那些已遭废弃的代码。

 

谜题二十三 不劳无获

Random.nextInt(int number)产生的是一个从0到(不包括)number之间的数。处理长度、范围或模数的时候,注意确定其端点是否包含

注意switchcase语句后是否有breakJava编程规范中有指出,要注释清楚一定要用的贯通的case,注释如下形式

case 0:…

//go through

case 1:…

break;

这里指出,不要从一个非空的case向下进入另一个case

StringBuffer带参的构造函数有三个,其介绍如下:

//构造一个字符串缓冲区,它包含与指定的CharSequence 相同的字符。

StringBuffer(CharSequence seq)

//构造一个不带字符,但具有指定初始容量的字符串缓冲区。

StringBuffer(int capacity)

//构造一个字符串缓冲区,并将其内容初始化为指定的字符串内容。

StringBuffer(String str)

 

不管在什么时候,都要尽可能使用熟悉的惯用法和API。如果必须使用不熟悉的API,那么请仔细阅读器文档。

char不是String,其更像int

 

谜题二十四 尽情享受每一个字节

0x90代表一个十六进制数,十六进制一位代表二进制的四位,因为24=16byte最大值为27-1,最小值为-27

要避免混合类型比较,因为它们内在地容易引起混乱。使用声明的常量代替魔数

 

谜题二十五 无情的增量操作

后缀增量操作符:表达式j++的值等于j在执行增量操作之前的初始值。

j=j++;等同于

int tmp = j;

j = j + 1;

j = tmp;

但是注意的是j=j++这个表达式在C++中的结果和Java不一定相同。

不要在单个表达式中对相同的变量赋值超过一次。

 

谜题二十六 在循环中

int型的i变量自加,到Integer.MAX_VALUE的时候,再加又绕回到Integer.MIN_VALUE

无论你在任何时候使用了一个整数类型(整数类型包括bytecharshortintlong),都要意识到其边界条件。

 

谜题二十七 变幻莫测的i

移位操作符只使用其右操作数的低5位作为移位长度。5位二进制最大为2^5-1=31,这个数值小于int型变量在系统所占用的32位,这样能保证不在一次中将int所有位重置。

如果可能的话,移位长度应该是常量。不能用右移操作符进行左移操作,反之亦然。比如,如果要将一个int数值左移,移位长度为-1,其并不会执行右移一位,而-1的二进制表示为0xffff,取其低5位,结果是左移31位。

 

谜题二十八 循环者

Double中存在无穷大为Double.POSITIVE_INFINITY

浮点数操作返回的是最接近精确数学结果的浮点数值。一旦毗邻的浮点数值之间的距离大于2,那么对其中一个浮点数值加1将不会产生任何效果。对于float类型,加1不会产生任何效果的最小级数是225=33554432;对于double类型,这一值为254,约为1.8*1016

毗邻的浮点数值之间的距离被称为最小单位(ulp),Java5.0版本中Math.ulp可以计算floatdouble类型的ulp

用一个doublefloat数值来表示无穷大是可以的。

将一个很小的浮点数加到一个很大的浮点数上时,将不会改变大浮点数的数值。

二进制浮点算术只是对实际算术的一种近似。

 

谜题二十九 循环者的新娘

IEEE 754浮点算术保留了一个特殊值来表示一个不是数字的数量,这个值就是NaNNaN不等于任何浮点数值,包括它自身在内。

任何浮点操作,只要它的一个或多个操作数为NaN,那么其结果为NaN。因为一旦一个计算产生了NaN,没有任何更进一步的计算可以修复这样的损坏。

 

谜题三十 循环者的爱子

操作符重载是很容易令人误解的。

对于程序的可读性来说,好的变量名、方法名和类名至少与好的注释同等重要。

 

谜题三十一 循环者的鬼魂

执行移位操作,只能是一个整型类型(包括bytecharshortintlong)。

复合赋值操作符包括*=/=%=+=-=<<=>>=>>>=&=^=|=

复合赋值操作符可能会自动执行窄化基本类型转换。

窄化基本类型转换可能修饰级数的信息,或者是数值的精度。

不要在bytecharshort类型的变量之上使用复合赋值操作符。

 

谜题三十二 循环者的诅咒

反对称关系定义:

 

Java5.0版之前,Java的数字比较操作符要求两个操作数是原始数据类型。但5.0版本之后,数字比较的每一个操作数的类型必须可以转换成基本数字类型。

当两个操作数都是被包装的数字类型时,数值比较操作符和判等操作符的行为存在根本的差异:数值比较操作符执行的是值比较,而判等操作符执行的是引用标识的比较。

 

循环三十三 循环者遇到了狼人

由于计算机采用补码表示数,正数的补码就是其原码;负数的补码就是其原码按位取反+1的结果,由于这样0只有一个表示,所以负数会比正数多一个,多出的一个其补码是最高位为1其余位为0的数,就是负数最小的数。因此Integer.MIN_VALUELong.MIN_VALUE是它自己的负值。

千万要小心溢出。

 

谜题三十四 被计数击倒了

int31位用于精确表示数据,然后float类型只能提供24位的精度。

会导致精度丢失的三种拓宽基本类型转换:int型到float型的转换,longfloat的转换,以及longdouble的转换。

31位的int提升到具有24位精度的float会在第7位和第8位之间四舍五入,从而直接舍弃最右边的7位。

不要使用浮点数作为循环索引。

在将一个intlong转换成一个floatdouble时,可能会丢失精度。当使用浮点数时,要使用double而不是float

 

谜题三十五 分钟计数器

用被恰当命名的常量来代替所有的魔数。

千万不要使用空格来表示分组,要使用括号。

 

谜题三十六 优柔寡断

在一个try-finally语句中,无论try语句块是正常结束的,还是意外结束的,finally语句块总是在控制权离开try语句块时执行。

千万不要用returnbreakcontinuethrow来退出finally语句块,并且千万不要允许让受检查的异常传播到finally语句块之外。

 

谜题三十七 极端不可思议

如果一个catch子句要捕获一个类型为E的受检查异常,而其相对应的try子句不能抛出E的某种子类型的异常,那么这就是一个编译期错误。

捕获ExceptionThrowablecatch子句是合法的,不管与其对应的try子句内容为何。

一个方法可以抛出的受检查异常集合是它所适用的所有类型声明要抛出的受检查异常集合的交集。

 

谜题三十八 不受欢迎的宾客

在程序中,一个空final字段只有在它的确未赋值的地方才可以被赋值。因为定义的保守,编译器必须拒绝某些可以证明是安全的程序。

如果必须重构一个程序,以消除由明确赋值规则所引发的错误,那么应该考虑添加一个新方法。

 

谜题三十九 您好,再见

System.exit方法将停止当前线程和所有其他当场死亡的线程。

当调用System.exit时,虚拟机在关闭前要执行两项清理工作:

1.执行所有的关闭挂钩操作,这些挂钩已经注册到Runtime.addShutdownHook

2.与终结器有关。如果System.runFinalizerOnExitRuntime.runFinalizersOnExit被调用了,那么虚拟机将在所有还未终结的对象上调用终结器。

务必要为那些必须在虚拟机退出之前发生的行为关闭挂钩。

无论什么原因,永远都不要调用System.runFinalizerOnExitRuntime.runFinalizersOnExit:它们属于Java类库中最危险的方法。

 

      2015年7月7日

谜题四十不情愿的构造器

实例变量的初始化操作将先于构造器程序体而运行。

抛出的异常分为ErrorException两种,都是Throwable的子类。

 

构造器必须声明其实例初始化操作会抛出的所有检查异常。

 

2015年7月8日

谜题四十一 字段和流

文件操作close也可能会抛出IOException异常。

关闭文件每一个close都包装在一个嵌套的try语句中。从Java5.0版本开始,可以利用Closeable接口,用于关闭文件。

对于任何在finally语句块中可能抛出的受检查异常都要进行处理,而不是任其传播。

 

2015年7月9日

谜题四十二异常为循环而抛

一般在C++中二维数组只能是n*n的形式,但是在Java中二维数组的行数,和每行的列数都可以自定义。

不要使用异常控制循环,应该职位异常条件而使用异常。

条件操作符:&&(条件与操作符)||(条件或操作符)

逻辑操作符:&(逻辑与操作符)|(逻辑或操作符)

条件操作符,比如条件与操作&&,在编译时,采用的是短路代码翻译,也就是当&&操作符左边为false,是不计算右边运算的;同理,条件或操作符||,在编译时,只要左边为true,则不查看右边。

但是逻辑操作符不同,无论什么情况,都要查看左右两个操作数。

很少使用逻辑操作符,因此,在使用逻辑操作符时,注释表明。

要意识到逻辑与和逻辑或操作符的存在,并且不要因无意识的无用而受害。

语言设计,首先考虑两个类似的运算分别提供运算符的必要性;其次,只要是两个不同的运算,应该其操作符在视觉上存在明显差异。

 

7月10日

谜题四十三异常地危险

Class.newInstance可以抛出它没有声明过的受检查的异常。

为了实现最大的兼容性,Java泛型是通过类型擦出来实现的:泛型信息是在编译期而非运行期检查的。

当你获得了一个不受检查的转型警告时,应该修改你的程序以消除它,或者可以确信这个转型不会失败。

Java的异常检查机制并不是虚拟机强制执行的,它只是一个编译期工具,被设计用来帮助我们更容易地编写正确的程序,但是在运行期可以绕过它。要想较少为这类问题而被曝光的次数,就不要忽视编译器给出的警告信息。

 

7月11日

谜题四十四删除类

NoClassDefFoundError异常可以在(直接或间接)使用某个类的程序中的任何地方抛出。

给出代码的字节码解释部分完全看不懂,因为不知道字节码执行顺序是不是按照从上到下的顺序,截图一张两个方法的字节码对比。

 

合并的两个类型是通过计算它们的首个公共超类而合并的。

Strange1加载验证类的时刻在校验期间,而不是main方法执行之时(这部分的缘由没有看懂)。

想要编写一个能够探测类丢失的程序,请使用反射来引用类。比如:

try{

Object m = Class.forName(“Missing”).newInstance();

}catch(ClassNotFundException ex){

//do nothing

}

不要对捕获ClassNotFundException形成依赖。类加载的时机不可预测。捕获Error及其子类型几乎是完全不恰当的。因为Error及其子类的异常是为那些不能恢复的错误而保留的(意思就是捕获了,也不能通过异常处理使得程序继续正常运作下去)。

 

7月12日

谜题四十五令人疲惫不堪的测验

try-finally语句调用,先运行try语句,发生异常,则转入finally语句块。指数算法对于最小输入之外的所有情况都是不可行的。

 

7月13日

谜题四十六令人混淆的构造器案例

Javapublicprivateprotect的区别是,public是所有类可用,是公开接口private仅限于定义该类中可使用,如私有的财产protect在定义类和其子类中可用,是受保护的传家宝

Java的重载解析过程分两个阶段运行,第一阶段选取所有可获得并且可应用的方法或构造器;第二阶段在第一阶段选取的方法或构造器中选取最精确的一个。

如果一个方法或构造器可以接受传递给另一个方法或构造器的任何参数,那么我们说第一个方法比第二个方法缺乏精确性。(就是范围小的比范围大的精确。)

谜题中出现问题的原因在于在测试哪一个方法或构造器最精确时,并没有使用实参。因此,要强制要求编译器选择一个精确的重载版本,需要将实参转型为形参所声明的类型。

在理想状态下,你应该避免使用重载。

如果确实进行了重载,那么确保所有重载版本所接受的参数都互不兼容;如果做不到,那么确保所有可应用的重载版本都具有相同的行为。

 

7月14日

谜题四十七啊呀!狸猫变犬子

每一个静态字段在声明它的类及其所有子类中共享一份单一的副本。

继承和组合的关系。is a是继承关系,has a是组合关系。当你拿不准的时候,优选组合而不是继承。

 

7月15日

谜题四十八我所得到的都是静态的

对静态方法的调用不存在任何的分派机制。(Java中除了static方法和final方法(private方法属于final方法)之外,其他所有方法都是后期绑定。)

变量的编译期类型和运行期类型有时候不会相同。比如像创建一个子类的对象,然后将其引用赋值给超类(例如Shape s = new Circle(),其中Shape是超类,Circle是子类),那么其编译期类型是超类,运行期类型由于动态绑定,变成了子类。

千万不要用一个表达式来标识一个静态方法的调用(就是说,不要使用一个类的对象来调用类中静态方法,而是应该直接用类名加.运算符调用静态方法)。

千万不要隐藏静态方法。

注意覆盖和隐藏的区别。非静态函数存在覆盖,就是子类函数覆盖了超类同名函数;静态函数存在隐藏,就是超类函数隐藏了子类同名函数。(比如一个超类S有静态方法a,非静态方法b;其子类C也有静态方法a,非静态方法b,如果S s = new C(); C.a()调用超类的a(子类a被隐藏),C.b()调用子类的b(超类b被覆盖))。

 

7月16日 

谜题四十九比生命更大

类初始化阶段,静态字段被设置为默认值,然后静态字段初始器按照其出现的顺序执行初始化。因此如果存在依赖链,A依赖BB依赖C,如果出现的顺序是ABC,那么这样初始化的结果肯定会出现错误。

final类型的静态字段被初始化之前,存在着读取其值的可能。

要想改正一个类初始化循环,需要重新对静态字段的初始器进行排序,使得每一个初始器都出现在任何依赖于它的初始器之前。(如上述例子,他们正确的出现顺序应该是CBA)。

要当心类初始化循环。

 

7月17日

谜题五十不是你的类型

null对于每一个引用类型来说都是其子类型。

instanceof操作符在左操作数为null时,返回false

instanceof要求,如果两个操作数的类型都是类的情况下,其中一个必须是另一个的子类型。(这样有个疑惑,就是instanceof的实际作用,首先常有动态绑定,基类引用指向子类对象,instaceof可以找到其实际的所属类;也有说作用是在做类型转换的之前,进行判断,防止类型转换失败。)

Java中如果没有指定父类,那么其父类为Object。转型只能向下转型(只能子类转为父类,父类不能转换为子类)。

 

7月18日

谜题五十一要点何在

千万不要在构造器中调用可覆写的方法。

Java在调用构造器时,先调用父类构造器,然后再调用子类构造器。但是在调用父类构造器时,依旧将对象作为子类对象,也就是如果父类构造器如果调用了被子类覆盖的方法,那么按照子类的方法执行;然后再继续执行自身的构造。

C++先调用父类构造器,后调用子类构造器。在调用父类构造器时,将其看做父类对象(覆盖的函数不会存在动态绑定的调用结果),当执行完父类构造器时,返回执行子类构造器,又将其作为子类对象。

 

7月19日

谜题五十二总和的玩笑

要么使用积极初始化,要么使用延迟初始化,千万不要同时使用二者。

尽管调用类的静态方法不需要创建类的对象,用类名直接可以访问,但是在执行的时候,VM依旧要初始化静态方法对应的类。

请考虑初始化顺序,特别是当初始化显得很重要时更是如此。

初始化顺序是:静态变量,静态初始化块,变量,初始化块,构造器,而这些初始化过程,先父类后子类。

 

7月20日 

谜题五十三做你的事吧

交替构造器调用机制:允许一个类中的某个构造器链接调用同一个类中的另一个构造器。

 

7月21日

谜题五十四Null Void

静态方法调用的限定表达式是可以计算的,但是它的值将被忽略。

要么用某种类型限定静态方法调用,要么就根本不要限定它们。(基本意思就是调用静态方法,直接用类名加.访问即可。)

 

7月22日

谜题五十五特创论

一个局部变量声明,是一个局部变量声明语句(如例子中的Creature creature = new Creature();)。

Java语言规范不允许一个变量声明语句作为一个语句在forwhiledo循环中重复执行。一个局部变量声明作为一个语句只能出现在一个语句块中(一个语句块是由一对花括号以及包含在这对花括号中的语句和声明构成的)。

Java5.0版本,可以使用一个AtomicLong实例,它在面临并发时可以绕过对同步的需求。

在使用一个变量来对实例的创建进行计数时,要使用long类型而不是int类型的变量,以防止溢出。

在多线程中创建实例并统计实例个数,要么将实例计数器的访问进行同步,要么使用AtomicLong类型的计数器。

 

7月23日

谜题五十六大问题

BigIntegerStringBigDecimal以及包装器类型:IntegerLongShortByteCharacterBooleanFloatDouble等实例是不可变的。不可变是指其在进行计算的时候,不会改变其值,而是将结果作为新的对象返回。

不要被误导,认为不可变类型是可变的。

对于API设计者,在命名不可变类型的方法时,应该优先选用介词和名词,而不是动词。介词适用于带有参数的方法,而名词适用于不带参数的方法。

 

7月24日

谜题五十七名字里有什么

无论何时,只要你覆写了equals方法,就必须同时覆写hashCode方法。

类从Object那里继承的hashCode实现,返回的是基于标识的散列码(不同的对象几乎总是产生不相等的散列值)。

一般而言,当你覆写一个方法时,如果它具有一个通用的约定,那么一定要遵守它。

 

7月25日

谜题五十八产生它的散列码

方法重载:相同的方法名,不同的参数列表。

覆盖方法:相同的方法名,相同的参数列表。(Java SE5中可以用函数名前加@Override注解来强制检查是否覆盖方法,而不是重载方法)

HashSet类使用equals(Object)方法来测试元素的相等性。

重载为错误和混乱提供了机会。

为了避免无意识地重载,你应该机械地对你想要覆写的每一个超类方法都复制其声明。

 

7月26日

谜题五十九差是什么

八进制数12 =1010

0开头的整型字面常量将被解释成为八进制数值。类似的,十六进制是以0x或者0X开头。

千万不要在一个整型字面常量前面加上一个0

 

7月27日

谜题六十一行以毙之

LinkedHashSet可以以插入的顺序添加非重复的元素。

字符串解析成标志,除了StringTokenizer之外,在java1.4版本以后,还可以采用正则表达式。String.split接受一个正则表达式作为参数来拆分字符串。

Arrays.deepToString可以返回指定数组“深层内容”的字符串表示形式。

IntegerLongShortByteChar类型中都支持通用的位处理操作,包括highestOneBitlowestOneBitnumberOfLeadingZerosnumberOfTrailingZerosbitCountrotateLeftrotateLeftreversesignumreverseBytes

了解库中有些什么可以为你节省大量的时间和精力,并且可以提高程序的速度和质量。

 

7月28日

谜题六十一日期游戏

Date类和其改善类Calendar类有很多缺陷,使用需要注意。

Date类和Calendar中将一月表示为0,也就是只有0-11月,如果设置12或者大于12的值,并不会抛出异常,而是往后挪到下一年。

Data,geDay返回的是Date实例所表示的星期日期,而不是月份日期。(就是说返回的数字代表的星期几,而不是今天多少号)。同时这个返回值基于0的,从星期天开始计算。但是Calendar.get(Calendar.DAY_OF_WEEK)返回值是基于1的星期日期。

在使用CalendarDate的时候一定要当心,千万要记着查阅API文档。

 

7月29日

谜题六十二名字游戏

IdentityHashMap在比较键值相等时,采用的是==,也就是比较的不是对象值相等的情况,而是比较的地址相同情况。其不是一个通用Map实现,通用Map强制使用equals进行比较。

相等的字符串常量同时也是相同的。

不要使用IdentityHashMap,除非你需要其基于标识的语义,:它不是一个通用目的的Map实现。

 

7月30日

谜题六十四按余数编组

Math类中绝对值方法:Math.abs对于Integer.MIN_VALUELong.MIN_VALUE的求值结果等于其本身。

Math.abs不能保证一定会返回非负的结果。原因是因为采用的补码的原因,Integer.MIN_VALUE的数值为-232,但是整数能达到的最大值只有232-1,无法对称。补码和反码表示的数都无法对称,但是取消了令人疑惑的正0和负0的表示,而原码在表示的数上是对称的,且存在正0和负0

 

7月31日

谜题六十五疑似排序的惊人传奇

Collections类中提供了一个可以排序的比较器,Collections.reverseOrder()方法。

不要使用基于减法的比较器,除非你能够确保要比较的数值之间的差永远不会大于Integer.MAX_VALUE

 

8月1日

谜题六十六一件私事

一个覆写方法的访问修饰符所提供的访问权限与被覆盖方法的访问修饰符所提供的访问权限相比,至少要一样多。

覆写和隐藏(hiding,有翻译成屏蔽的)的区别在于,覆写的方法,子类实例无法调用超类被覆写的方法;隐藏的字段,可以通过将子类转换成超类类型来访问。

Java允许隐藏变量、嵌套类型以及静态方法。

要避免隐藏。

当你在声明一个字段、一个静态方法或一个嵌套类型时,如果其名字与基类中相对应的某个可访问的字段、方法或类型相同,就会发生隐藏。

除了覆写之外,要避免名字重用。

 

谜题六十七对字符串上瘾

main方法必须接受一个字符串数组参数。

要避免重用平台类的名字,并且千万不要重用java.lang中的类名(C++中的命名空间,using namespace std)。

除非覆写,一般情况下应该避免名字重用 


8月1日


谜题六十六一件私事

一个覆写方法的访问修饰符所提供的访问权限与被覆盖方法的访问修饰符所提供的访问权限相比,至少要一样多。

覆写和隐藏(hiding,有翻译成屏蔽的)的区别在于,覆写的方法,子类实例无法调用超类被覆写的方法;隐藏的字段,可以通过将子类转换成超类类型来访问。

Java允许隐藏变量、嵌套类型以及静态方法。

要避免隐藏。

当你在声明一个字段、一个静态方法或一个嵌套类型时,如果其名字与基类中相对应的某个可访问的字段、方法或类型相同,就会发生隐藏。

除了覆写之外,要避免名字重用。


谜题六十七对字符串上瘾

main方法必须接受一个字符串数组参数。

要避免重用平台类的名字,并且千万不要重用java.lang中的类名(C++中的命名空间,using namespace std)。

除非覆写,一般情况下应该避免名字重用 


8月2日

谜题六十八灰色的阴影

当一个变量和一个类型具有相同的名字,并且它们位于相同的作用域,变量名具有优先权,优先权的排序为变量名>类型名>包名。

Java命名约定如下:类应该以大写字母开头,以MixedCase的形式书写(就是每个单词开头都大写的形式,适用于类名和接口名);变量名应该以一个小写字母开头,以mixedCase的形式书写(就是开头小写,接下来每个单词首字母大写,变量名和方法命名都遵循此规则,但是一般用动词命名方法,用名词命名变量);常量应该以一个大写字母开头,以ALL_CAPS的方式书写(就是所有单词大写,每个单词间用下划线连接);包名应该以lower.case的方式命名。

为了避免变量名与包名的冲突,请不要使用顶层的包名或领域名作为变量的名字,特别是不要将变量命名为comorgnetedujavajavax


谜题六十九黑色的渐隐

我们是可以引用一个被遮蔽的类型名的,其技巧就是在某一种特殊的语法上下文环境中使用该名字。


8月3日

谜题七十一揽子交易

Java的四种访问权限分别是:privatedefaultprotectedpublic

private不能修饰类(不考虑内部类)。被private修饰的成员(数据成员,构造方法,方法成员)只能在其定义的类中访问。

protect不能修饰类(不考虑内部类)。被protect修饰的成员,只能在其定义的类,同包的类,以及继承其定义类的类中访问。

public修饰的成员,可以在任何一个类中调用。

default指默认访问,即在不添加任何关键字的情况下的访问权限。默认权限即同包权限。同包权限的元素,只能在其定义类,以及同包类中调用。

一个包内私有的方法不能被位于另一个包中的某个方法直接覆写。

包内私有的方法是它们所属包的实现细节,在包外重用它们的名字是不会对包内产生任何影响的(C++中的命名空间)。


谜题七十二进口税

编译器在选择运行期所调用的方法时,所做的第一件事就是在肯定能找到该方法的作用域内挑选。编译器将在包含了具有恰当名字的方法的最小闭合作用域进行挑选。

隐藏(hide)、遮掩(obscure)和遮蔽(shadow)。

隐藏:子类的某个字段、静态方法、成员内部类与其父类的具有相同名字(对于静态方法还需要相同的参数列表),此时父类对应的字段、静态方法、成员内部类就被隐藏了。

遮掩:变量名可以遮掩类名和包名,类名可以遮掩包名。变量名>类名>包名。

遮蔽:类似局部变量遮蔽全局变量。一个声明只能遮蔽类型相同的另一个声明,一个变量声明可以遮蔽另一个类型声明,一个变量声明可以遮蔽另一个变量声明,一个方法声明可以遮蔽另一个方法声明。

当一个声明遮蔽了另一个声明时,简单名将引用到遮蔽声明中的实体。(简单名是指没有带作用域限定的名字,这句话的意思是,如果一个声明遮蔽了另一个声明(重名的这个名字为A,其就是简单名),那么调用A的时候,调用的是遮蔽其他声明的这个声明的实体内容)。本身就属于某个作用域的成员在该作用域内与静态导入相比具有优先权。应该有节制的使用静态导入,只有在非常需要的情况下才使用它们


8月4日

谜题七十三隐私在公开

严格地讲,构造器不是成员。

重用名字是危险的;应该避免隐藏、遮蔽和遮掩。


谜题七十四同一性的危机

如果同一个方法的两个重载版本都可以应用于某些参数,那么它们应该具有相同的行为。

可以在构造器中,退出执行。


第8章总结:

Java的覆写、重载、隐藏、遮蔽、遮掩

覆写(Override):对应方法

一个实例方法可以覆写在其超类中可以访问到的具有相同签名的所有实例方法,从而使得动态分派成为可能;换而言之,VM将给予实例的运行期类型来选择要调用的覆写方法。覆写是面向对象编程技术的基础,并且是唯一没有被普遍劝阻的名字重用形式。

重载(Overload):对应方法

在同一个类中,具有相同的名字和不同的签名的方法,它们互相构成重载。(相同函数名,不同参数列表,不能是不同返回值)调用所指定的重载方法在编译期选定。

隐藏(hide):字段、静态方法、成员类型

一个字段、静态方法(静态方法无法动态绑定)或成员类型可以分别隐藏在其超类中可访问的具有相同名字(对方法而言就是相同的方法签名)的所有字段、静态方法或成员类型。隐藏一个成员将组织其被继承。

遮蔽(Shadow):变量、方法、类型

一个变量、方法或类型可以分别遮蔽在一个闭合的文本作用域内的具有相同名字的所有变量、方法或类型。如果一个实体被遮蔽了,那么你用它的简单名是无法引用到它的;根据实体的不同,有时你根本就无法引用到它。

遮掩(Obscure)变量、类型、包

一个变量可以遮掩具有相同名字的一个类型,只要它们都在同一个作用域内;如果这个名字被用于变量与类型都许可的范围,那么它将引用到变量上。相似地,一个变量或一个类型可以遮掩一个包。遮掩是唯一一种两个名字位于不同的名字空间的名字重用形式,这些名字空间包括:变量、包、方法或类型。如果一个类型或一个包被遮掩了,那么你不能通过其简单名引用到它,除非是在这样一个上下文环境中,即语法只允许在其名字空间出现的一种名字。遵守命名约定就可以极大地消除产生遮掩的可能性。


8月5日

谜题七十五头还是尾?

条件操作符(?)的行为在5.0版本之前是非常受限的,当第二个和第三个操作数是引用类型时,条件操作符要求它们其中的一个必须是另一个的子类型。

5.0版本以后,条件操作符在第二个和第三个操作数是引用类型时总是合法的,同时其结果类型是这两种类型的最小公共超类。

应该升级到最新的Java的平台版本上。


谜题七十六乒乓

同步化静态方法执行之前,它会获取与它的Class对象相关联的一个管理锁。

当你想调用一个行程的start方法时要多加小心,别弄错成调用这个线程的run方法。


8月6日

谜题七十七乱锁之妖

在内部,Thread.join方法在正在被连接的线程实例上,调用了Object.wait方法。这样就在等待期间释放了该对象的锁。

除非有关于某个类的详细说明作为保证,否则千万不要假设库中的这个类对它的实例或类上的锁会做(或不会做)某些事。

如果你需要获得某个锁的完全控制权,那么就要确定没有任何其他人能够访问到它。


题七十八反射的影响

访问位于其他包中的非公共类型的成员是不合法的。

Object.getClass().getMethod(“methodName”)这种惯用法虽然很常见,但是却有问题,它不应该被使用,这种用法很容易在运行期产生一个IllegallAccessException

在使用反射访问某个类型时,请使用表示某种可访问类型的Class对象。


8月7日

谜题七十九狗狗的幸福生活

编译器会在包含有正确名称的方法的最内层作用域内查找需要调用的方法。

应该避免遮蔽。

使用Thread(Runnable)构造器来替代对Thread的继承。

对于编译器的编写者来说,应该尽力去产生那些对程序员来说有意义的错误消息,例如编译器可以警告程序员,存在着适用于调用却被遮蔽的方法。


谜题八十更深层的反射

Class.newInstance抛出InstantiationException原因:Class表示一个抽象类、接口数组类、基本类型或void;该类没有null构造方法;由于其他原因导致实例化失败。

一个非静态的嵌套类的构造器,在编译的时候会将一个隐藏的参数作为它的第一个参数,这个参数表示它的直接外围实例。

除非你确实需要一个外围实例,否则应该优先使用静态成员类而不是非静态成员类。

请避免使用反射来实例化内部类。


8月8日

谜题八十一无法识别的字符化

一个PrintStream可以被创建为自动刷新的;这意味着当一个字节数组被写入,或者某个println方法被调用,或者一个换行字符或字节(’\n’)被写入之后,PrintStream类型的flush方法就会被自动调用。

write(int)将指定的byte写入流,如果这个byte是一个换行字符,并且流可以自动刷新,那么flush方法将被调用。write(int)是唯一一个在自动刷新功能开启的情况下不刷新PrintStream的输出方法。

尽可能使用熟悉的惯用法;如果你不得不使用陌生的API,请一定要参考相关的文档。

对于API设计者:请让你们的方法的行为能够清晰地反映在方法名上;请清楚而详细地给出这些行为的文档;请正确地实现这些行为。


谜题八十二啤酒爆炸

对于Process类,由于某些本地平台只提供优先大小的缓冲,所以如果不能迅速地读取子进程的输出流,就有可能会导致子进程的阻塞,甚至是死锁。(JDK API 1.6上描述为:因为有些本机平台仅针对标准输入和输出流提供有限的缓冲区大小,如果读写子进程的输出流或输入流迅速出现失败,则可能导致子进程阻塞,甚至产生死锁。)

为了确保子进程能够结束,你必须排空它的输出流;对于错误流也是一样。使用ProcessBuilder的类,调用redirectErrorStream方法将各个流合并,然后排空。

如果你决定不合并输出流和错误流,你必须并行地排空它们。试图顺序地排空它们会导致子进程被挂起。

API应该设计得更容易做出正确的事,而很难或不可能做出错误的事。

调用ProcesswaitFor方法无返回值,导致子进程阻塞,可以尝试上述办法解决。


8月9日

谜题八十三诵读困难者的一神论

一个实现了Serializable的单件类,必须有一个readResolve方法,用以返回它的唯一实例。


谜题八十四戛然而止

调用Thread.interrupted方法总是会清除当前线程的中断状态。

isInterrupted方法用以查询当前线程中断状态,不会对状态造成影响。

不要使用Thread.interrupted方法,除非你想要清除当前线程的中断状态。


8月10日

谜题八十五延迟初始化

当一个线程访问一个类的某个成员的时候,它会去检查这个类是否已经被初始化。

要让类的初始化尽可能地简单。

在类的初始化期间等待某个后台线程很可能会造成死锁。


谜题八十六有害的括号垃圾

int i = -(2147483648);

long j = - (9223372036854775808L);

两个数会编译报错,但是去除括号,又能通过编译,原因是负数最大值int-231,但是正数只有231-1,同理long类型。


8月11日

谜题八十七紧张的关系

Java中的操作符==不是自反的,并且不是传递的。

不是自反的原因是,存在表达式Double.NaN == Double.NaNFloat.NaN == Float.NaN,结果都是false.

不是传递的原因是,操作符==进行计算的时候,首先进行二进制数据类型提升,那么两个操作数中有一个会进行拓宽基本类型转换。将intlong值转换成float值,或将long值转换成double值时,均会导致精度丢失。

要警惕floatdouble类型的拓宽基本类型转换所造成的损失。


谜题八十八原始类型的处理

一个原始类型就是一个没有任何类型参数的泛型接口的名字。

一个原始类型很想其对应的参数化类型,但是它的所有实例成员都要被替换掉,而替换物就是这些实例成员被擦掉对应部分之后剩下的东西。具体地说,在一个实例方法声明中出现的每个参数化的类型都要被其对应的原始部分所取代。(意思应该就是,如果采用了一个原始类型,比如定义一个泛型类Test,其中声明一个Test对象,那么Test类中所有泛型类,都要变成原始类型)。

原始类型List和参数化类型List是不一样的。

原始类型是为兼容5.0以前的已有代码而设计的。

请避免在打算使用5.0或更新的版本来运行的代码中编写原始类型。

类名称字面常量提供了泛型信息(如Author.class的类型是Class)。类名称字面常量可以传递运行时和编译期的类型信息。

如果你想重构现有的代码以利用泛型的优点,那么最好的方法是一次只重构一个API,并且保证新的代码中绝不使用原始类型。


8月12日

谜题八十九泛型迷药

要避免遮蔽类型参数的名字。

一个泛型类的内部类可以访问到它的外围类的类型参数。

你应该优先使用静态成员类而不是非静态成员类。

应该只在类自己的方法中修改该类的实例字段。

在一个泛型类中设置一个内部类并不是必错的,但是很少用到这种情况,而且你应该考虑重构你的代码来避免这种情况。


谜题九十荒谬痛苦的超类

无论何时你写了一个成员类,都要问问你自己,是否这个成员类真的需要外围类实例?如果答案是否定的,那么应该把它设为静态成员类。

扩展一个内部类的方式是很不恰当的;如果必须这样做的话,你也要好好考虑其外围类实例的问题。


8月13日

谜题九十一序列杀手

如果一个HashSetHashtableHashMap被序列化,那么请确认它们的内容没有直接或间接地引用它们自身。这里的内容,指的是元素、键和值。

readObjectreadResolve方法中,请避免直接或间接地在正在进行反序列化的对象上调用任何方法。

Java的序列化系统是很脆弱的。为了正确而且高效地序列化大量的类,你必须编写readObjectreadResolve方法。


谜题九十二双绞线

私有成员不会被继承。

如果你不能通过阅读代码来分辨程序会做什么,那么它很可能不会做你想让它做的事。


8月14日最后一更

谜题九十三类的战争

对于常量字段的引用会在编译期被转换为它们所表示的常量的值。

一个常量变量的定义是,一个在编译期被常量表达式初始化的final的基本类型或String类型的变量。

null不是一个编译期常量表达式。

API的设计者在设计一个常量字段之前应该深思熟虑。


谜题九十四迷失在混乱中

Collections类中提供shuffle方法,用以随机对指定列表进行置换。

如果库中有可以满足你需要的方法,请务必使用它。

java.util.Random使用的是一个48位的种子(书中说是64位,查了jdk上指出是48位),能产生种子248个。

如果产生的种子过少,可以使用java.security.SecureRandom产生随机数。


谜题九十五来份甜点

多余的标点会造成程序的错误。

第二个表达式i1 < i2 ? -1 : ( i2 > i1 ? 1 : 0 ),对于i2>i1的情况,先化简为i1,然后结果是-1

==的优先级是比三目运算符的优先级高。true?false:true==true?false:true的运算等同于:true?false:(true==true)?false:true

最后的教训是,不要像我的兄弟那样编写代码。


1 0
原创粉丝点击