最近准备重新把《Java解惑》看一遍,为了以后可以快速的回忆起书中说到的一些陷阱,我把认为值得记下来的简要记录一下,以备以后看。
毕竟一天是看不完的,所以本博客是持续更新的.......
1. 想通过num%2==1判断num是不是奇数是有问题的,因为对于负奇数会等于-1而不是1。所以可以通过num%2!=0来判断是否为奇数。为了提高性能可以采用(num&1)!=0来判断是否为奇数。
2. 二进制数是无法准确表达所有小数的,比如说0.9。在需要精确答案的地方避免使用float和double;尽量使用int,long,BigDecimal。
3. JAVA不具有目标确定类型的特性,比如说: long num=50000*50000; 执行之后num并不等于2500000000。因为JAVA并不能根据结果是long型的而在计算中采用long型数进行计算。实际上50000*50000的计算是按照int型计算的。所以应该这样:long num=50000L*50000;
4. 十六进制数和八进制数和十进制数是不同的,如果十六进制或八进制数的最高位被置位了,那么他们就是负数。而十进制数是通过加一个减号实现负数的。所以对于十六进制和八进制数要特别注意。对于0xcafebabe被提升为long型数值是会发生符号扩展变为0xffffffffcafebabeL。
5. 从较窄的整数转换成较宽的整数时候有一条规则:如果最初的数值类型是有符号的,那么就执行符号扩展;如果是char,那么不管它将要被转换成什么类型都执行零扩展。
6. 在单个表达式中更不要对相同的变量赋值两次。
7. 确定条件表达式结果类型的规则;1:如果第二个和第三个操作数类型相同那么它既是条件表达式的类型。2:如果一个操作数的类型是T,T表示byte,short,char。而另一个操作数是一个int型的常量表达式,他的值也可以用类型T表示,那么条件表达式的类型就是T;若不能被T表示,比如说超出了范围,比如:
char x='a';
System.out.println(true ? 50000 : x );
//char无法表示50000,所以结果是:
?
3:否则对类型进行二进制提升。条件表达式的类型就是提升后的类型。
8. 复合赋值表达式自动将所执行计算的结果转换为其左侧变量的类型。比如:
short x=0;
int num=12345;
x+=i;
System.out.println(x);
//计算结果会转型成short,由于short无法表示12345所以结果为:
-7616
所以请不要将复合赋值操作符作用于byte,short,char类型的变量。
9. 当且仅当+操作符的操作数中至少有一个是String类型时,才会执行字符串连接操作。 比如:
String str='a'+'a'+"";
String string=""+'a'+'a';
System.out.println(str);
System.out.println(string);
//运行结果:
194
aa
10. 对所有数组调用toString()的结果就是 巴拉巴拉hashCode神马的。还有就是如果引用为null,调用toString会转换成“null”。(我在这里吃过苦头)。比如:
String str=null;
String string="hello";
str=str+string;
System.out.println(str);
//运行结果:
nullhello
要想讲一个char数组转换成一个字符串调用toString()可不行,要调用String.valueOf(char[])比如:
char arr[]={'a','v','c'};
System.out.println(String.valueOf(arr));
//运行结果:
avc
11. String类型的编译期常量是内存限定的,也就是说任何两个String类型的常量表达式,如果指定的是相同的字符序列,那么他们就用同一对象引用来表示。如:
String string="hello";
String str="hello";
System.out.println(str==string);
//运行结果:
true
而以下也是:
String string="hello";
String str="he"+"llo";
System.out.println(str==string);
//运行结果:
true
但是如果改为:
String string="hello2";
String str="hello"+"he".length();
System.out.println(str==string);
//结果:
false
好好体会一下。
再注意一个问题:+的优先级比==高,所以:
if ("hello"+"java"=="hellojava") {
System.out.println("OK");
}
//结果:
OK
12. 我们来看一下Unicode转义字符,Java对再字符串字面常量中的Unicode转义字符没有提供任何特殊处理,会直接将其转化为相应字符,比如:
System.out.println("hello\u0022.length());
//\u0022被直接转成了"所以,相当于"hello".length()
5
想在字符串常量表达式中用一些特殊符号怎么办呢?可以使用转义字符序列,就是在特殊字符前加上\,举例如下:
System.out.println("hello\\".length());
System.out.println("\\\\".length());
System.out.println("\"".length());
//结果:
6
2
1
Unicode转义字符带来的混乱还不止是这些,比如如果我们在注释中这样写:
// \u
编译器是一定会报错的,因为\u表示一个Unicode转义字符的开始,而且Unicode转义字符必须是良构的,在注释中也必须是。所以要在\u后跟四个数字(16进制大小写均可)。Unicode转义字符是很危险的,比如\u000A会转义成换行。
13. 将byte数组转化为String的时候,我们都在使用一个字符集。无论是否指定。所以最好这样做:String str=new String(bytes,"ISO-8859-1");我们指定字符集。
缺省的会使用OS的缺省字符集。
14. 块注释符(/* */)是不支持嵌套的。比如下面语句就会出错:
/*
if("+-*/".equals("hello"))
*/
所以注释掉代码的最好方式是使用单行的注释。//
15. String.replaceAll接受了一个正则表达式做为第一个参数。\后面跟非特殊字符是非法的。比如 \c就是编译不通过的。我们应该使用String.replace。
16. Random.nextInt(int):返回一个伪随机数,均等地分布在从0(包含)到指定的数值(不包含)之间的一个int数值。StringBuffer有一个无参的构造函数,一个接受一个String做为字符串缓冲区初始内容的构造器,以及一个接受int做为缓冲区初始容量的构造器。new StringBuffer('a');实际上是计算‘a’的int型值然后开辟这么大的缓冲区。
17. byte是有符号的。((byte)0x90==0x90)会返回false。
18. for(int i=Integer.MIN_VALUE; i<=Integer.MAX_VALUE; i++)是个无限循环。因为i递增到Integer.MAX_VALUE的时候再递增就变成Integer.MIN_VALUE。
19. 最近发现Java中的移位操作符的问题,详见这篇博客:
http://wjy320.iteye.com/blog/2070199
20. 我们都知道,一个无穷大的数字加上1之后还是无穷大。事实上可以用double或者float数值来表示无穷大。例如:double i=1.0e40; (i==i+1)返回的true。
对于float类型,加1不会产生任何效果的最小级数是2的25次方。对于double类型,最小级数是2的54次方。还有请记住:将一个很小的浮点数加到一个很大的浮点数上时,将不会改变大浮点数的值。所以:二进制浮点数只是对实际算数的一种近似。
21. 知道0.0/0.0等于多少吗?这肯定是有问题的不是吗?因为怎么可能去除0呢?在Java中,这个特殊的数字是有名字的叫NaN。可以通过:
Float.NaN 和 Double.NaN获得。要注意一下:1,NaN不等于任何浮点数包括它自身(NaN!=NaN)。2,任何浮点操作,只要它的一个或者多个操作数为NaN,那么其结果为NaN。
22. >>> 和 <<<是对应于无符号移位操作的操作符。之前我们提过对于复合操作符在计算的时候会执行拓宽原始类型的操作(如果两个操作数不一样长的话,会将短的操作数执行拓宽操作(对于有符号数执行符号扩展,对于char永远执行零扩展。))。
23. 注意一下比较操作符(<,>,>=,<=)。左边这些比较操作无论什么时候都是进行的数值比较。但是==操作可就不同了,对于==而言必须至少有一个操作数是原生类型时才执行数值比较,否则执行的是判断是否引用相等的操作。比如:
Integer i=new Integer(3);
Integer j=new Integer(3);
System.out.println(i==j);
System.out.println(i>=j);
System.out.println(i<=j);
//运行结果:
false
true
true
24. java使用2的补码的算术运算是不对称的。对于每一种有符号的整数类型(int,long,byte和short),负的数值总是比正的数值多一个,这个多出来的数值总是这种类型所能表示的最小的数值。(比如int表示的数据范围是负的2的32次方到正的2的32次方减1)。对Integer.MIN_VALUE取负数还等于他自身。对Short,Byte...同样适用。
25. 不要使用浮点数做为循环索引,将一个int或者long转换成一个float或double时,可能会丢失精度。事实上float虽然是32位的,但是只能提供24位的精度,指数神马的还要占位的。
26. 取余操作和乘法操作具有相同的优先级,所以ms%60*1000相当于(ms%60)*1000。
27. 千万不要时间用return,break,continue或者throw来退出finally语句块,千万不要允许让受检验的异常传播到finally语句块之外。finally中直接写break或continue是不合法的。比如:finally{break;} 因为break和continue必须在循环中,finally并不是循环。一般在打开IO流的时候我们在finally中关闭它,像这样:
finally{
if(in!=null) in.close();
if(out!=null) out.close();
}
其实以上写法是不好的,close可能抛出IOException异常,如果in.close()抛出了异常会导致out.close()永远不会执行到。从而输出流永远保持开放。所以正确的写法应该是:
finally{
if(in!=null){
try {
in.close();
} catch (Exception e2) {
// TODO: handle exception
}
}
if(out!=null){
try {
out.close();
} catch (Exception e2) {
// TODO: handle exception
}
}
}
从java5.0开始,可以使用Closeable接口进行重构:
private static void closeIgnoringException(Closeable c){
if(c!=null){
try {
c.close();
} catch (Exception e) {
// TODO: handle exception
}
}
}
finally{
closeIgnoringException(in);
closeIgnoringException(out);
}
28. 1,如果一个catch子句要捕获一个类型为E的受检查异常,而其相对应的try子句不能抛出E的某种子类型的异常,那么这就是一个编译期错误;但是捕获Exception或者Throwable的catch子句是永远合法的,无论对应的try子句内容为何。2,一个方法必须要么捕获其方法体可以抛出的所有受检验的异常,要么声明它将抛出这些异常。3,一个方法可以抛出的受检查的异常集合是它所适用的所有类型声明要抛出的受检查异常集合的交集。比如:
interface A{
void f() throws CloneNotSupportedException;
}
interface B{
void f() throws InterruptedException;
}
interface C extends A,B{
}
public class Test implements C{
public static void main(String args[]){
C c=new Test();
c.f();
}
@Override
public void f(){
// TODO Auto-generated method stub
System.out.println("HELLO");
}
}
就是说接口C的方法f()会抛出A和B中方法f()的抛出异常的交集。所以接口C中的方法f()不会抛出异常。
29. 一个空的final域只有在它的确未被附过值的地方才可以被赋值。比如下面是不合法的:
//错误原因:编译器会认为可能对USER_ID重复赋值,try中赋值语句后面的语句若抛出异常,会在catch中重复赋值。
private static final int USER_ID;
static{
try {
USER_ID=1;
} catch (Exception e) {
USER_ID=2;
// TODO: handle exception
}
}
//其实下面的也是错的,因为不能保证USER_ID一定被赋值成功。而final是必须被初始化的。
private static final int USER_ID;
static{
try {
USER_ID=1;
} catch (Exception e) {
// TODO: handle exception
}
}
解决办法,加个方法就OK了:
private static final int USER_ID=getUserId();
private static int getUserId(){
try {
return 1;
} catch (Exception e) {
return 2;
// TODO: handle exception
}
}
30. 下面的finally中的代码永远都执行不到:
public static void main(String args[]){
try {
System.exit(0);
} finally{
// TODO: handle exception
System.out.println("Good bye.");
}
}
是不是感觉有悖常理,其实System.exit方法将停止当前线程和所有其他当场死亡的线程。finally子句并不能给予线程继续执行的特殊权限。
31. 实例(不管是静态的还是非静态的)初始化操作是先于构造器的程序体而运行的。实例初始化操作抛出的任何异常都会传播给构造器。如果初始化操作抛出的是受检验的异常,那么构造器必须声明也会抛出这些异常,但是应该避免这样做。看看下面的程序,会产生栈溢出:
public class Test{
private Test instance=new Test();
public static void main(String args[]){
Test test=new Test();
}
}
//这就是没有将构造函数私有化的恶果。
32. &操作符除了常见的做为整形操作数的位AND操作符之外,当用于布尔操作数时,他的功能被重载为逻辑AND操作符,但是他和常用的条件操作符&&有很大的不同。&总是要计算他的两个操作数,而&&在其左边的操作数被计算为false时,就不在计算右边的操作数了。|也是一样的,|总是要计算他的两个操作符,而||在其左边的操作数被计算为true时,就不再计算右边的操作数了。
33. Class的成员变量如果是final的,要求必须对其初始化。其实将其的初始化延迟到构造函数中也是可以的。像下面这样也可以:
private final int num=getNum();
public int getNum(){
return 1;
}
34. 1.一定要意识到Class.newInstance可以抛出它没有声明过的受检查的异常。2.泛型信息是在编译期而非运行期检查的。3.Java的异常检查机制并不是虚拟机强制执行的,它只是一个编译期工具,被设计用来帮助我们更加容易的编写正确的程序,但是在运行期可以绕过它。所以,不要忽视编译器给出的警告信息。
35. 要是想检测一个类是否丢失的程序,应该使用反射:
try {
Object mObject=Class.forName("ClassName").newInstance();
} catch (ClassNotFoundException e) {
// TODO: handle exception
e.printStackTrace();
}
不要对捕获NoClassDefFoundError形成依赖,类的初始化的时机是很明确的,但是类被加载的时机是不可预测的,捕获Error及其子类型几乎是不应该的。
36. 谜题45没看。待补充。
下部分:http://wjy320.iteye.com/blog/2076381