java陷进

1.    异常捕捉的陷阱

异常处理机制是java语言的特色之一,尤其是java语言的Checked异常,更是体现了java语言的严谨性:没有完善错误处理的代码根本不会被执行。对于Checked异常,java程序要么声明抛出,要么使用try……catch进行捕获。

1.1  正确关闭资源的方式

在实际开发中,经常需要在程序中打开一些物理资源,如数据库连接,网络连接,磁盘文件等,打开这些物理资源之后必须显示关闭,否则将会导致资源泄漏。因为垃圾回收机制属于java内存管理的一部分,它只是负责会受堆内存中分配出来的内存,至于程序中打开的物理资源,垃圾回收机制是无能为力的。

为了正常关闭程序中打开的物理资源,应该使用finally块来保证回收。比如下面三种关闭资源的方式,哪种更好些?

  1. public static void main(String args[]) throws Exception {  
  2.                    Student student_new = new Student("liyafang");  
  3.                    Student student_recover = null;  
  4.                    ObjectOutputStream oos = null;  
  5.                    ObjectInputStream ois = null;  
  6.                    try {  
  7.                             oos = new ObjectOutputStream(new FileOutputStream("liyafang.txt"));  
  8.                             ois = new ObjectInputStream(new FileInputStream("liyafang.txt"));  
  9.                             oos.writeObject(student_new);  
  10.                             oos.flush();  
  11.                             student_recover = (Student) ois.readObject();  
  12.                             System.out.println(student_recover.equals(student_new));  
  13.                             System.out.println(student_recover == student_new);  
  14.                    } finally {  
  15. //1.第一种关闭资源的方式(不够安全):程序刚开始指定oos = null;ois = null;完全有可能在程序运行过程中初始化oos之前就引发了异常,那么oos,ois还没有来得及初始化,因此oos,ois根本无需关闭。  
  16.                             oos.close();  
  17.                             ois.close();  
  18. //2.第二种关闭资源的方式(还是不够安全):假如程序开始已经正常初始化了oos,ois两个IO流,在关闭oos是出现了异常,那么程序将在关闭oos时非正常退出,这样就导致ois得不到关闭,从而导致资源泄漏。为了保证关闭各资源时出现的异常不会相互影响,应该在关闭每个资源时分开使用try catch块来保证关闭操作不会导致程序非正常退出。  
  19.                             if(oos != null){  
  20.                                      oos.close();  
  21.                             }  
  22.                             if(ois != null){  
  23.                                      ois.close();  
  24.                             }  
  25. //3.第三种关闭资源的方式(比较安全):主要保证一下几点:  
  26. //(1)使用finally块来关闭物理资源,保证关闭操作始终会被执行;  
  27. //(2)关闭每个资源之前首先保证引用该资源的引用变量不为null;  
  28. //(3)为每个物理资源使用单独的trycatch块关闭资源,保证关闭资源时引发的异常不会影响其他资源的关闭。  
  29.                             if(oos != null){  
  30.                                      try{  
  31.                                                oos.close();  
  32.                                      }catch (Exception ex){  
  33.                                                ex.printStackTrace();  
  34.                                      }  
  35.                             }  
  36.                             if(ois != null){  
  37.                                      try{  
  38.                                                ois.close();  
  39.                                      }catch (Exception ex){  
  40.                                                ex.printStackTrace();  
  41.                                      }  
  42.                             }                 }  
  43.          }  

1.2  finally块的陷阱

当程序在finally之前使用System.exit(0),finally将不执行。调用System.exit(0)将使JVM退出,只要JVM不退出,finally就一定会得到执行。

在java程序执行try块、catch块时遇到了return语句,return语句会导致该方法立即结束。统执行完return语句之后,并不会立即结束该方法,而是去寻找该异常处理流程中是否包含Finally块,如果没有Finally块,方法终止,返回相应的返回值。如果有Finally块,系统立即开始执行Finally块,只有当Finally执行完成后,系统才会再次跳回来根据return语句结束方法。如果Finally块使用了return语句来导致方法的结束,则finally块已经结束了方法,系统不会跳回去执行trycatch里的任何代码

  1. public int test(){  
  2.                    int count = 1;  
  3.                    try{  
  4.                             return ++count;  
  5.                    }finally{  
  6.                             return ++count;  
  7.                    }  
  8.          }  
  9. //以上代码最终返回值是:3  
  10. public int test(){  
  11.                    int count = 1;  
  12.                    try{  
  13.                             return ++count;  
  14.                    }finally{  
  15.                             return count++;  
  16.                    }  
  17.          }  
  18. //以上代码最终返回值是:2  

throw语句的执行和return语句比较类似。当程序执行trycatch块遇到throw语句时,throw语句会导致该方法立即结束,系统执行throw语句时并不会立即抛出异常,而是去寻找该异常处理流程中是否包含finally块。如果没有finally块,程序立即抛出异常。如果有finally块,系统立即执行finally块,只有当finally块执行完成之后,系统才会再次跳出来抛出异常。如果finally块里使用return语句来结束方法,系统将不会跳回去执行try块,catch块去抛出异常。

例如1:

  1. int count = 1;  
  2.                    try{  
  3.                             throw new RuntimeException("异常1");  
  4.                    }finally{  
  5.                             return count++;  
  6.                    }  
  7. //执行结果:返回值是1,同时不会抛出任何异常。  

例如2:

  1. int count = 1;  
  2.                    try{  
  3.                             throw new RuntimeException("异常1");  
  4.                    }finally{  
  5.                             throw new RuntimeException("异常2");  
  6.                    }  
  7. //执行结果:Exception in thread "main" java.lang.RuntimeException: 异常2  

1.3  catch块的用法

1.3.1 catch的顺序

对于java的异常捕获来说,每个try块至少需要一个catch块或一个finally块,绝不能只有单独一个孤零零try块。通常情况下,如果try块被执行一次,则try块后只有一个catch块会被执行,绝不可能有多个catch块被执行。除非在循环中使用了continue开始下一次循环,下一次循环又重新运行了try块,才可能导致多个catch块被执行。由于异常处理机制中排在前面的catch(XxxException ex)块总是会优先获得执行的机会,因此java对try块后的多个catch块的排列顺序是有要求的。

   因为java的异常有非常严格的继承体系,许多异常类之间有严格的父子关系,比如程序FileNotFoundException异常就是IOException的子类。捕获父类异常的catch块都应该排在捕捉子类异常的catch块之后【先处理小异常(子类异常),在处理大异常(父类异常)】,否则将出现编译错误

         例如以下两个catch语句不能颠倒顺序:

                 

  1. FileInputStream fis = null;  
  2.                   try{  
  3.                            fis = new FileInputStream("a.bin");  
  4.                            fis.read();  
  5.                   }catch(FileNotFoundException ex){  
  6.                            ex.printStackTrace();  
  7.                   }catch(IOException e){  
  8.                            e.printStackTrace();  
  9.                   }  

1.3.2不要用catch代替流程控制

如下边这个例子:

  1. String[] students = {"liyafang","zhoushilong","luorongbo"};  
  2.                    int i = 0;  
  3.                    while(true){  
  4.                             try{  
  5.                                      System.out.println(students[i++]);  
  6.                             }catch(IndexOutOfBoundsException ex){  
  7.                                      break;  
  8.                             }  
  9.                    }  

这种遍历数组的方式不仅难以阅读,而且运行速度还非常慢。

千万不要使用异常来进行流程控制。异常机制不是为流程控制而准备的,异常机制知识为程序的意外情况准备的,因此程序只应该为异常情况使用异常机制。所以,不要使用这种“别出心裁”的方法来遍历数组。

1.3.3只能catch可能抛出的异常

  1. /*public static void test1(){ 
  2.                try{ 
  3.                         System.out.println("something"); 
  4.                }catch(IOException e){ 
  5.                         e.printStackTrace(); 
  6.                } 
  7.      } 
  8.      public static void test2(){ 
  9.                try{ 
  10.                         System.out.println("something"); 
  11.                }catch(ClassNotFoundException e){ 
  12.                         e.printStackTrace(); 
  13.                } 
  14.      }*/  

以上代码java编译器是不允许的。

根据java语言规范,如果一个catch子句试图捕获一个类型为XxxException的Checked异常时,那么它对应的try子句必须可能抛出XxxException或其子类的异常,否则编译器将提示该程序具有编译错误—但在所有的Checked异常中,Exception是一个异类,无论try块是怎样的代码,catch(Exception e)总是正确的。

RuntimeException 类及其子类的实例被称为Runtime异常,不是RuntimeException类及其子类的异常实例则被称为Checked异常,只要愿意,程序员总可以使用catch(XxxException ex)来捕获运行时异常。

总之,程序使用catch捕捉异常时,其实并不能随心所欲地捕捉所有异常。程序可以在任意想捕捉的地方捕捉RuntimeException异常,Exception,但对于其他的Checked异常,只有当try块可能抛出该异常时(try块中调用的某个方法声明抛出了该Checked异常),catch块才捕捉该Checked异常。

 1.3.4实际的修复

如果程序知道如何修复指定异常,应该在catch块内尽量修复该异常,当该异常情况被修复后可以再次调用该方法;如果程序不知道如何修复该异常,也没有进行任何修复,千万不要再次调用可能导致该异常的方法。

无论如何不要在finally块中递归调用可能引起异常的方法,因为这将导致该方法的异常不能被正常抛出,甚至StackOverflowError错误也不能中止程序,只能采用强行结束java.exe进程的方法来中止程序的运行。

1.4  继承得到的异常

Java语言规定:子类重写父类的方法时,不能声明抛出比父类方法类型更多,范围更大的异常。也就是说,子类重写父类方法时,子类方法只能声明抛出父类方法所声明抛出的异常的子类。

例如:

  1. public interface Type1 {  
  2.          void test() throws ClassNotFoundException;  
  3. }  
  4. public interface Type2 {  
  5.          void test() throws NoSuchMethodException;  
  6. }  
  7. class Test implements Type1, Type2 {  
  8.          @Override  
  9.          public void test() {  
  10.          }  
  11. }  

上面代码的异常处理是正确的。

Test实现了Type1接口,实现Type1接口里的test()方法时可以声明抛出ClassNotFoundException异常或该异常的子类,或者不声明抛出;Test类实现了Type2接口,实现了Type2接口里的test()方法时可以声明抛出NoSuchMethodException异常或该异常的子类,或者不声明抛出。由于Test类同时实现了Type1,Type2两个接口,因此需要同时实现两个接口中的test()方法。只能是上面两种声明抛出的交集,不能声明抛出任何异常。

1.    表达式中的陷阱

1.1  关于字符串的陷阱

1. 在创建一个String s = new String("java");的时候,JVM会到常量池中去检查看是否有一个"java"对象了,如果没有则在常量池中创建一个。之后会在堆内存中分配了一个空间,放置这个new出来的String对象,形式如下:java.lang.String@123b, 常量池是在编译期生成的,而new一个对象是在运行时进行的,有一个先后顺序。

2. Java程序中创建对象的常规方式有如下四种:

ü  通过new调用构造器创建java对象。

ü  通过Class对象的newInstance()方法调用构造器创建java对象。

ü  通过java的反序列化机制从IO流中恢复java对象。

ü  通过java对象提供的clone()方法复制一个新的java对象。

3. 对于java程序中的字符直接量,JVM会使用一个字符串池来保存它们:当第一次使用某个字符串直接量时,JVM会将它放入字符串池进行缓存。在一般情况下,字符串池中字符串对象不会被垃圾回收,当程序再次需要使用字符串时,无需重新创建一个新的字符串,而是直接让引用变量指向字符串池中已有的字符串。

4. 关于以直接量的方式来创建java对象的一些问题:

  1. <pre class="java" name="code">String str1 = "liyafang";  
  2.          String str2 = "liyafang";  
  3.          System.out.println(str1==str2);//true  
  4.          //虽然str4的值不是直接量,但因为它的值可以在编译时确定,  
  5.          //所以str4也会直接引用字符串池中对应的字符串。  
  6.          String str3 = "liyafang2";  
  7.          String str4 = "li"+"yafang"+2;//只创建了一个字符串对象  
  8.          System.out.println(str3==str4);//true  
  9.          //str6的值包含了方法调用,不能再编译时确定,所以无法利用JVM的字符串池  
  10.          String str5 = "liyafang8";  
  11.          String str6 = "liyafang"+"liyafang".length();  
  12.          System.out.println(str5==str6);//false  
  13.          //str8的值使用了变量,不能再编译时确定,所以无法利用JVM的字符串池  
  14.          int len = 1;  
  15.          String str7 = "liyafang1";  
  16.          String str8 = "liyafang"+len;  
  17.          System.out.println(str7==str8);//false  
  18.          //字符串连接运算中的所有变量都可执行"宏替换",  
  19.          //那么JVM一样可以在编译时就确定字符串连接表达式的值  
  20.          final String temp = "li";  
  21.          String str9 = "liyafang";  
  22.          String str10 = temp+"yafang";  
  23.          System.out.println(str9==str10);//true  
  24.          final int leng = 8;  
  25.          String str11 = "liyafang8";  
  26.          String str12 = "liyafang"+leng;  
  27.          System.out.println(str11==str12);//true</pre><br>   

5.       不可变的字符串:

  1. String str = "I";  
  2.     System.out.println(System.identityHashCode(str));  
  3.     //该静态方法用于获取某个对象唯一的hashCode值,这个方法的返回值与该类是否重写  
  4.     //hashCode方法无关。只有当两个对象相同时,它们的identityHashCode值才会相等。  
  5.     str = str+" love ";  
  6.     System.out.println(System.identityHashCode(str));  
  7.     str = str+"java!";  
  8.     System.out.println(System.identityHashCode(str));  

运行结果:

31843011

25860399

5184781

看起来str对应的字符串序列可以发生改变。但是要记住,str只是一个引用类型变量,它并不是真正的String对象,只是指向String对象而已,真正发生改变的是str变量本身,它改变了指向,指向了一个新的String对象。以上程序总共会创建3个字符串对象,“I”,“I love ”,“I love java!”,其中前两个将一直存在于字符串常量池中-这就是java内存泄漏的原因之一

6.优先考虑使用StringBuilder

其与StringBuffer唯一的区别在于StringBuffer是线程安全的,也就说StringBuffer类里绝大部分方法都增加了synchronized修饰符。在单线程环境下,优先考虑StringBuilder,因其效率高

7.String类实现了Comparable接口:

可以通过compareTo()方法比较两个字符串之间的大小,当两个字符串所包含的字符序列相等时,返回值为0。比较规则:先将两个字符串左对齐,然后从左向右依次比较两个字符串所包含的每个字符,包含较大字符的字符串的值比较大。

1.2  表达式类型的陷阱

1.表达式类型的自动提升:

byte->short;

short,char->int->long->float->double

eg: short a = 5; a = a -2;此时将会发生错误,因为int赋给short类型的变量将发生错误。

2.复合赋值运算符的陷阱:

    short a = 5; a -=2; 没有错误,根据java语言规范,复合赋值运算符包含了一个隐式的类型转换,所以a-=2等价于a = (a的类型)(a-2);与此类似的有+=,*=,/=,%=,<<=,>>=,>>>=,&=,^=和|=等。

    再看下面这个例子:

             short st = 15;

             st += 90000;

             System.out.println(st);//输出结果不是900015,而是24479

    因为short类型的变量只能接受-32768~32767之间的整数,因此上面程序会将高位“截断”。

    复合赋值运算符简单,方便,而且具有性能上的优势,但复合赋值运算符可能有一定的风险:它潜在的隐式类型转换可能不知不觉中导致计算结果的高位被截断,为了避免潜在的危险,如下情况需要额外注意:

    将复合赋值运算符应用于byte,short,char等类型的变量;应用于int类型的变量,而表达式右边是long,float,double类的值;应用于float类型的变量,而表达式右侧是double类型的值。

    当+用于字符串连接符时,则+=的变量只能是String类型的,而不可是String的父类型(如Object或CharSequence)。

1.3  输入法导致的陷阱

基本上如果在编译java程序时提示形如:“非法字符:\xxxxx”的错误提示,那么就可断定该java程序中包含“全角字符”,逐个删除他们即可。

1.4  注释的字符必须合法

Java要求注释中所有的字符必须是合法的字符。Java程序允许直接使用\uXXXX的形式代表字符,它要求\u后面的4个字符必须是0~F字符,如果注释中出现“\unit5”,这不符合java对Unicode转义字符的要求。

1.5  转义字符的陷阱

Java程序提供三种方式表示字符:

直接使用单引号括起来的字符值,如’a’;

使用转义字符,如’\n’;

使用Unicode转义字符,如’\u0062’。

不过转移字符得慎用,如下:

System.out.println(“abc\u000a”.length());这句话无法通过编译,提示:未结束的字符串字面值。引起这个原因是java对Unicode转移字符不会进行任何特殊的处理,只是简单的将其替换成相应的字符。对于\u000a而言,相当于一个换行符(\n),因此上边的程序相当于:

System.out.println(“abc

”.length());

这样就不难理解出现的编译错误了。

类似的情况如下边的注释将通不过编译:

//\u000a代表一个换行符。

因为对于java编译器来说相当于

//

代表一个换行符。

1.5  正则表达式的陷阱

以下程序不会输出任何东西,想想为什么?

String str = "www.baidu.com";

                   String[] str1 = str.split(".");

                   for(String s:str1){

                            System.out.println(s);

                   }

从JDK1.4开始,java的String类提供了split()方法进行字符串的分割。JDK1.0原来提供的StringTokenizer基本上已经成为“历史遗物”了。

对于上面的程序,需要注意两点:

1.String提供的split(String regex)方法需要的参数是正则表达式;

2.正则表达式中的点号(“.”)可以匹配任意字符。

所以上面程序实际上不是以“.”作为分隔符,而是以任意字符作为分隔符。为了实现以“.”作为分隔符的目的,必须对“.”号进行转义,将上面的程序改为

String[] str1 = str.split("\\.");即可得到想要的结果。

从JDK1.4开始,Java加入了对正则表达式的支持,String类也增加了一些方法用于支持正则表达式,具体方法如下:

matches(String regex):判断该字符串是否匹配指定正则表达式。

String replaceAll(String regex,Stringreplacement):将字符串中所有匹配指定正则表达式的子串替换成replacement后返回。

String replaceFirst(String regex,Stringreplacement):将字符串中第一个匹配正则表达式的子串替换成replacement后返回。

String[] split(String regex):以regex正则表达式匹配的子串作为分割符来分割该字符串。

replace(CharSequence target,CharSequencereplacement):将字符串中所有target子串替换成replacement后返回。这个普通的replace()方法不支持正则表达式,开发中必须区别对待replaceAll和replace两个方法。

例如:

                   String str = "www.baidu.com";

                   String str1 = str.replace(".", "\\");

                   String str2 = str.replaceAll("\\.", "\\\\");

                   System.out.println(str1);//输出结果:www\baidu\com

                   System.out.println(str2);//输出结果:www\baidu\com



1.  流程控制的陷阱

1.1  Switch语句陷阱

Switch语句后可以指定一个表达式,系统根据表达式的值来决定执行哪个case分支的执行体。对于switch语句的表达式而言,只能是如下5种数据类型。

byte(字节整型),short(短整型),int(整型),char(字符型),enum(JDK1.5之后可以使用枚举型)。

绝不能是String类型,也不能是long,float,double等其它类型。

Eg:int a=5;switch(a+1.0) 是错误的,因为a+1.0表达式的类型自动提升为double类型。

枚举类型举例

  1. enum Season {  
  2.     SPRING, SUMMER, FALL, WINTER  
  3. }  
  4. public class EnumSwitch_lyf {  
  5.     public static void main(String[] args) {  
  6.         Season s = Season.FALL;  
  7.         switch (s) {  
  8.         case FALL:  
  9.             System.out.println("秋天");  
  10.             break;  
  11.         case SUMMER:  
  12.             System.out.println("夏天");  
  13.             break;  
  14.         case WINTER:  
  15.             System.out.println("冬天");  
  16.             break;  
  17.         case SPRING:  
  18.             System.out.println("春天");  
  19.             break;  
  20.         default:  
  21.             System.out.println("其他");  
  22.         }  
  23.     }  
  24. }    

枚举类型使用时需要注意:

程序在case分支中访问枚举值时不能使用枚举类名作为限定,在其他地方使用enum值时,通常应该使用枚举类名作为限定。

1.2  标签引起的陷阱

Java语句的标签是一个怪胎:它主要是为C语言中的goto语句而创建,但java程序中根本没有goto语句。虽然java一直将goto作为保留字,但估计java也没有引入goto语句的打算。因为java的标签一般都没有太大的作用。

1.3  If语句的陷阱

使用if……else语句有一条基本规则:先处理范围小的,在处理范围大的。如果先处理范围大的条件,接下来的情况是拿“后处理的小范围”与“else隐含条件:刨除大范围的小范围”计算交集,两个小范围求交集就很难有交集了,这将导致后处理的分支永远不会获得执行的机会。

 

1.4  for循环的陷阱

根据java语言规范:for循环里有且只有2个分号作为分隔符,第一个分号之前是初始化条件,两个分号之间是一个返回boolean的逻辑表达式,当它返回true是for循环才会执行下一次循环;第二个分号之后的循环迭代部分,每次循环结束之后会执行的循环迭代部分。

for (int j = 1, i = 0; i < 5 && j < 20; i++, j *= 2) {

           System.out.println("someting");}

1.5  foreach循环的循环计数器

foreach(type tempVariable : array|collection){}

tempVariable:是一个形参名,foreach循环自动将那个数组元素、集合元素依次赋给该变量。

当使用foreach循环来迭代输出数组元素或集合元素时,系统将数组元素、集合元素的副本传给循环计数器,即foreach循环中的循环计数器并不是数组元素、集合元素本身,因此通常不要对tempVariable赋值,虽然赋值语法上被允许,但是没有太大的意义,而且极容易出现错误。

public static void main(String[] args) {

       String[] names = { "liyafang", "zhoushilong", "luorongbo" };

       for (String temp : names) {

           temp = "shining";//并不会修改names中的内容

           System.out.println(temp);

       }

       for (String temp : names) {

           System.out.println(temp);

       }

    }

输出结果:

shining

shining

shining

liyafang

zhoushilong

luorongbo



1.1  构造器的陷阱

1.构造器不能声明返回值类型,也不能使用void声明构造器没有返回值。当为构造器声明添加任何返回值类型声明,或者添加void声明该构造器没有返回值时,编译器并不会提示这个构造器有错误,只是系统会把这个所谓的“构造器”当成普通方法处理。这个时候初始化类实例时系统会调用默认的无参数的构造器。

2.构造器创建对象吗?大部分java书籍都笼统的说:通过构造器来创建一个java对象。这样容易给人一个感觉,构造器负责创建java对象。但是实际上构造器并不会创建java对对象,构造器只是负责执行初始化,在构造器执行之前,java对象所需要的内存空间,应该说是有new关键字申请出来的。

3.绝大部分,程序使用new关键字为一个java对象申请空间之后,都需要使用构造器为这个对象执行初始化。但在某些时候,程序创建java对象无需调用构造器,以下两种方式创建java对象无需使用构造器

(1)使用反序列化的方式恢复java对象

(2)使用clone方法复制java对象

1.1.1使用序列化恢复java对象无需构造器:

 

  1. import java.io.FileInputStream;  
  2. import java.io.FileOutputStream;  
  3. import java.io.ObjectInputStream;  
  4. import java.io.ObjectOutputStream;  
  5. import java.io.Serializable;  
  6.   
  7. public class Student implements Serializable {//注意必须实现序列化接口  
  8.     private String name;  
  9.   
  10.     public Student(String name) {  
  11.         System.out.println("调用构造器");  
  12.         this.name = name;  
  13.     }  
  14.   
  15.     public boolean equals(Object object) {  
  16.         if (this == object) {  
  17.             return true;  
  18.         }  
  19.         if (object.getClass() == Student.class) {  
  20.             Student s = (Student) object;  
  21.             return s.name.equals(this.name);  
  22.         }  
  23.         return false;  
  24.     }  
  25.   
  26.     public int hashCode() {  
  27.         return name.hashCode();  
  28.     }  
  29.   
  30.     public static void main(String args[]) throws Exception {  
  31.         Student student_new = new Student("liyafang");  
  32.         Student student_recover = null;  
  33.         ObjectOutputStream oos = null;  
  34.         ObjectInputStream ois = null;  
  35.         try {  
  36.             //创建对象输出流  
  37.             oos = new ObjectOutputStream(new FileOutputStream("liyafang.txt"));  
  38.             //创建对象输入流  
  39.             ois = new ObjectInputStream(new FileInputStream("liyafang.txt"));  
  40.             //序列化输出由构造器初始化的java对象  
  41.             oos.writeObject(student_new);  
  42.             oos.flush();  
  43.             //反序列化恢复java对象  
  44.             student_recover = (Student) ois.readObject();  
  45.             //两个对象的实例变量值完全相等,下面输出为true  
  46.             System.out.println(student_recover.equals(student_new));  
  47.             //两个对象不同,下面输出false  
  48.             System.out.println(student_recover == student_new);  
  49.         } finally {  
  50.             if (oos != null) {  
  51.                 oos.close();  
  52.             }  
  53.             if (ois != null) {  
  54.                 ois.close();  
  55.             }  
  56.         }  
  57.     }  
  58. }  

由此可见,程序完全通过这种反序列化机制确实会破坏单例类的规则,当然,大部分时候不会主动使用反序列化技术去破坏单例规则的。如果想保证反序列化时也不会产生多个java实例,则应该为单例类提供readResolve()方法,该方法保证反序列化时得到已有的java对象。

以下是完美的单例模式:

  1. <pre class="java" name="code">import java.io.FileInputStream;  
  2. import java.io.FileOutputStream;  
  3. import java.io.ObjectInputStream;  
  4. import java.io.ObjectOutputStream;  
  5. import java.io.ObjectStreamException;  
  6. import java.io.Serializable;  
  7.   
  8. public class President implements Serializable {// 注意必须实现序列化接口  
  9.     private static President instance;  
  10.     private String name;  
  11.   
  12.     private President(String name) {  
  13.         System.out.println("调用构造器");  
  14.         this.name = name;  
  15.     }  
  16.     //注意这是lazy的单例模式,要使用同步方法,否则多线程情况下会出现多个实例。  
  17.     public synchronized static President getInstance() {  
  18.         if (instance == null) {  
  19.             instance = new President("hejicheng");  
  20.         }  
  21.         return instance;  
  22.     }  
  23.         //该方法保证反序列化时得到已有的java对象。当JVM反序列化地恢复一个新对象时,  
  24.     //系统对自动调用这个readResolve()方法返回指定的对象,从而保证系统通过反序  
  25.     //列化机制不会产生多个java对象。  
  26.     private Object readResolve() throws ObjectStreamException {  
  27.         return this.instance;  
  28.     }  
  29.   
  30.     public static void main(String args[]) throws Exception {  
  31.         President president_new = President.getInstance();  
  32.         President president_recover = null;  
  33.         ObjectOutputStream oos = null;  
  34.         ObjectInputStream ois = null;  
  35.         try {  
  36.             oos = new ObjectOutputStream(new FileOutputStream("liyafang.txt"));  
  37.             ois = new ObjectInputStream(new FileInputStream("liyafang.txt"));  
  38.             oos.writeObject(president_new);  
  39.             oos.flush();  
  40.             president_recover = (President) ois.readObject();  
  41.             System.out.println(president_recover.equals(president_new));//为true  
  42.             System.out.println(president_recover == president_new);//为true  
  43.         } finally {  
  44.             if (oos != null) {  
  45.                 oos.close();  
  46.             }  
  47.             if (ois != null) {  
  48.                 ois.close();  
  49.             }  
  50.         }  
  51.     }  
  52. }  
  53. </pre><br>  
1.1.2使用clone()方法复制java对象也无需构造器:
  1. public class Student implements Cloneable {//注意必须Cloneable接口  
  2.     private String name;  
  3.         private int age;  
  4.     public Student(String name,int age) {  
  5.         System.out.println("调用构造器");  
  6.         this.name = name;  
  7.     }  
  8.     public Object clone(){  
  9.         Student s = null;  
  10.         try {  
  11.             s = (Student)super.clone();  
  12.         } catch (CloneNotSupportedException e) {  
  13.             // TODO Auto-generated catch block  
  14.             e.printStackTrace();  
  15.         }  
  16.         return s;  
  17.     }  
  18.     public boolean equals(Object object) {  
  19.         if (this == object) {  
  20.             return true;  
  21.         }  
  22.         if (object.getClass() == Student.class) {  
  23.             Student s = (Student) object;  
  24.             return s.name.equals(this.name)&&s.age==(this.age);  
  25.         }  
  26.         return false;  
  27.     }  
  28.     public int hashCode() {  
  29.         return name.hashCode()*17+age;  
  30.     }  
  31.     public static void main(String args[]) throws Exception {  
  32.         Student student_new = new Student("liyafang",22);  
  33.         Student student_clone = (Student)student_new.clone();//没有调用构造器  
  34.         System.out.println(student_new.equals(student_clone));//true  
  35.         System.out.println(student_new==(student_clone));//false  
  36.     }  
  37. }  

补充:

当对象作为集合里的key时,需要复写equals()和hashCode()方法

Set的存储机制是equals与hashcode相结合的。一般ADD一个对象会先根据equals方法判断与其他对象是否相等,因为Set是不允许重复add的。如你不覆盖equals方法,JAVA默认所有的对象都是不同的,也就是它们的内存地址。假如你NEW一个对象,人,你认为只要它们名字相同就是同一个对象,此时你就需要覆盖equals方法了,否则同名也是两个对象。java先通过equals方法判断存储位置,如果不同直接存入;如果通过equals方法比较现在要存入的对象与集合中的某个对象相等,那么它就会再根据hashcode来判断它们是否hashcode也相等,如果相等那就存不进去了,说明它们确实是同一个对象,不等就可存入。所以一般在写程序的时候,两个对象你认为它们不同就去覆盖equals方法。这样可以提高效率,不要让JAVA再去判断hashcode

 

1.2  无限递归的构造器

  1. public class ConstrucorRecursion {  
  2.     ConstrucorRecursion cr;  
  3.     {  
  4.         cr = new ConstrucorRecursion();  
  5.     }  
  6.     public ConstrucorRecursion() {  
  7.         System.out.println("程序执行无参数的构造函数");  
  8.     }  
  9.     public static void main(String args[]) {  
  10.         ConstrucorRecursion rc = new ConstrucorRecursion();  
  11.     }  
  12. }  

 

运行结果:

Exception in thread "main" java.lang.StackOverflowError

         at ConstrucorRecursion.<init>(ConstrucorRecursion.java:4)

         at ConstrucorRecursion.<init>(ConstrucorRecursion.java:4)

原因:

表面上看,程序没有什么问题,ConstrucorRecursion类的构造器中没有任何代码,它的构造器中只有一行简单的输出语句。但是不要忘记了,不管是定义实例变量时指定的初始值,还是在非静态初始化块中执行的初始化操作,最终都将提取到构造器中执行。

教训:

尽量不要在定义实例变量时指定实例变量的值为当前类的实例。

尽量不要初始化块中创建当前类的实例。

尽量不要在构造器内调用本构造器创建java对象。

1.3  持有当前类的实例

但在某些情况下,程序必须让某个类的一个实例持有当前类的另一个实例,例如链表,每个节点都持有一个引用,该引用指向下一个链表节点。

以下是一个简单的链表结构:

  1. public class LinkNode {  
  2.     String name;  
  3.     LinkNode node;  
  4.     public LinkNode(){};  
  5.     public String getName() {  
  6.         return name;  
  7.     }  
  8.     public LinkNode getNode() {  
  9.         return node;  
  10.     }  
  11.     public LinkNode(String name) {  
  12.         this.name = name;  
  13.         this.node = new LinkNode();  
  14.     }  
  15.     public static void main(String args[]) {  
  16.         LinkNode n1 = new LinkNode("软件0801班");  
  17.         LinkNode n2 = new LinkNode("软件0802班");  
  18.         LinkNode n3 = new LinkNode("软件0803班");  
  19.         n1.node = n2;  
  20.         n2.node = n3;  
  21.         System.out.println(n1.getNode().getNode().getName());  
  22.     }  
  23. }  

1. 从JDK1.5开始,Java提供了3种方式来创建,启动多线程

Ø  继承Thread类来创建线程类,重写run()方法作为线程执行体。

Ø  实现Runnable接口来创建线程类,重写run()方法作为线程执行体。

Ø  实现Callable接口来创建线程类,重写run()方法作为线程执行体。

其中第一种方式效果最差,它有2点坏处:

l  线程类继承了Thread类,无法在继承其他父类。

l  因为每条线程都是一个Thread子类的实例,因此多个线程之间共享数据比较麻烦。

对于第二三种方式,它们的本质是一样的,只是Callable接口里包含的call()方法既可以声明抛出异常,也可以拥有返回值。

2.此外启动线程应该使用start()方法,而不是run()方法。如果程序从未调用线程对象的start()方法来启动它,那么这个线程对象将一直处于”新建”状态(1.新建 2.就绪 3.运行 4.阻塞 5.死亡总共5个状态),它永远也不会作为线程获得执行的机会,它只是一个普通的Java对象。当程序调用线程对象的run()方法时,与调用普通Java对象的普通方法并无任何区别,因此绝对不会启动一条新线程的。

3. 静态的同步方法:

   Java语言规定:任何线程进入同步方法,同步代码块之前,必须先获取同步方法,同步代码块对应的同步监视器。对于同步代码块而言,程序必须显示为它指定同步监视器;对于同步非静态方法而言,该方法的同步监视器是this-即调用该方法的Java对象;对于静态的同步方法而言,该方法的同步监视器不是this,而是该类本身

如以下代码:

  1. class SynchronizedStatic implements Runnable {  
  2.     static boolean flag = true;  
  3.     public static synchronized void test0() {//同步监视器是该类本身  
  4.         for (int i = 0; i < 1000; i++) {  
  5.             System.out.println("test0: " + Thread.currentThread().getName()  
  6.                     + " " + i);  
  7.         }  
  8.     }  
  9.     public void test1() {  
  10.         synchronized (this) {//同步监视器是this,即调用该方法的Java对象。  
  11.             for (int i = 0; i < 1000; i++) {  
  12.                 System.out.println("test1: " + Thread.currentThread().getName()  
  13.                         + " " + i);  
  14.             }  
  15.         }  
  16.     }  
  17.     public void run() {  
  18.         if (flag) {  
  19.             flag = false;  
  20.             test0();  
  21.         } else {  
  22.             flag = true;  
  23.             test1();  
  24.         }  
  25.     }  
  26.     public static void main(String args[]) throws InterruptedException {  
  27.         SynchronizedStatic ss = new SynchronizedStatic();  
  28.         new Thread(ss).start();  
  29.         Thread.sleep(1);  
  30.         new Thread(ss).start();  
  31.     }  
  32. }  

运行结果:

test0: Thread-0 244

test1: Thread-1 7

test0: Thread-0 245

test1: Thread-1 8

test0: Thread-0 246

test1: Thread-1 9

test0: Thread-0 247

test1: Thread-1 10

test1: Thread-1 11

test1: Thread-1 12

test0: Thread-0 248

test1: Thread-1 13

test0: Thread-0 249

test0: Thread-0 250

从运行结果可以看出:静态同步方法可以和以this为同步监视器的同步代码块同时执行。因为两者的同步监视器不一样,前者是对SynchronizedStatic类的锁定,后者是对ss变量所引用的对象的锁定,因此程序可以在两个线程间相互切换。

若将test1()方法做以下更改:

  1. public void test1() {  
  2.         synchronized (<span style="color:#FF0000;">SynchronizedStatic.class</span>) {//和静态方法具有了相同的同步监视器  
  3.             for (int i = 0; i < 1000; i++) {  
  4.                 System.out.println("test1: " + Thread.currentThread().getName()  
  5.                         + " " + i);  
  6.             }  
  7.         }  
  8.     }  

运行结果:

test0: Thread-0 995

test0: Thread-0 996

test0: Thread-0 997

test0: Thread-0 998

test0: Thread-0 999

test1: Thread-1 0

test1: Thread-1 1

test1: Thread-1 2

test1: Thread-1 3

test1: Thread-1 4

结果说明:静态同步方法和以当前类为同步监视器的同步代码块不能同时执行。



通过面向对象的思路,我们可以把任何事物都看成一个对象然后单独处理,从理想的角度,任何一个微小的单元都可以以一个对象的形式表示。比如我们可以用如下代码表示一个人以及它的姓名:

1.不分离姓名

class 人 {

     public string 姓名;

}

但是这个世界是很复杂的,姓名本身是由姓和名组成的。如果我们需要需要单独处理姓和名时,要怎么办?于是我们可以这么拆分:

2.直接分离姓名

class 人 {

     public string 姓;
     public string 名;

}

但我们会碰到这个问题:姓和名本身是一个整体,处理姓名的逻辑不应该放在人这个类里面,而应该单独提取出来。于是代码改为:

3. 提取姓名为单独一个类,然后单独处理

复制代码
class 人 {

     public 姓名 姓名;

}

class 姓名 {
  
    public string 姓;
    public string 名;

}
复制代码

这是一个非常理想的状态:任何事物都被表示成了一个独立的对象。但其实作者都是懒的,没人会愿意为姓名单独写一个类然后单独处理它。除非逻辑非常复杂需要单独处理时,才会选择把它提取出来。

同样一个人,把它写成面向对象的代码之后,却有3种不同的写法(1. 不分离姓名。 2. 直接分离姓名。 3.提取姓名为单独一个类,然后单独处理)。而决定我们用哪个写法的是最需求。需求是软件开发中最不稳定的因素,因此面向对象的代码经常需要重构和重写。这就是面向对象中的一个设计陷阱。

再来看一个例子:

复制代码
abstract class 鱼 {private int 价格;
    private int 口感;public int get价格(){  return 价格;  }
    public int get口感(){  return 口感;  }
    public abstract string get名字();
}

class 鲤鱼: 鱼 {
    public override  string get名字() {
        return "鲤鱼";
    }
}

class 桂鱼: 鱼 {
  public override string get名字() {
        return "桂鱼";
  }
}
复制代码

这是一个最普通的面向对象的代码了。从上面看似乎完美到没有任何问题:通过鱼这个类以及它的多态特性,我们可以很轻松地处理所有鱼。

但是这时加了一个需求,我们需要处理金鱼,于是写了这么一行代码:

class 金鱼: 鱼 {
  public override string get名字() {
        return "金鱼";
  }
}

看上去依然完美,但问题是:金鱼是不能吃的,获取它的口感是没有任何意义的!但是通过继承,我们的金鱼也是可以有口感的。同时,还浪费了一个内存用来存储这个没有意义的口感字段。

现实中,很多人都忽视了这个问题:反正没意义,这个函数不要调用就行了。对的,但如果一个继承了一个父类之后,5个函数是有意义的,20个函数是没有意义的,这时我想作者该犯洁癖了吧。问题主要是在于继承一个类之后,有一些成员是不必要或者无意的,有2个改法:

1. 让金鱼不继承于鱼:这不科学,金鱼本来就是鱼,除了口感,其它的继承都是有意义并且需要使用的。

2. 提取一个公共父类:

复制代码
abstract class 鱼 {
    private int 价格;
    public int get价格(){  return 价格;  }
    public virtual int get口感() { throw new Exception("无意义") }
    public abstract string get名字();
}

abstract class 可以吃的鱼 : 鱼 {
    private int 口感;
    public override int get口感() {  return 口感;    }
}

class 鲤鱼: 可以吃的鱼 {
    public override  string get名字() {
        return "鲤鱼";
    }
}

class 桂鱼: 可以吃的鱼 {
  public override string get名字() {
        return "桂鱼";
  }
}


class 金鱼: 鱼 {
  public override string get名字() {
        return "金鱼";
  }
}
复制代码

仔细研究,你会发现网上很多代码,它定义了一个抽象类,并且名字是 XXXBase, XXXCore 或者一个类XXX还定义了一个类叫   XXXImpl  。这些抽象类就像上例中可以吃的鱼这个类一样,都是为了继承而存在的。这里,我们同样有这个问题:

父类的设计会因为需求和子类的增加和不得不作一些修改(如提取另外一个公共父类)。因此面向对象的代码经常需要重构和重写。这就是面向对象中的又一个设计陷阱。

 

总结一下上面的2个例子,就是:需求增加,代码就要重构。所以很多人认为这是一个只能靠经验才能解决的问题,只要写的代码够多了,就能预感到未来的需求并减少重构量。就像上例中,我们有经验就会先写好一个可以吃的鱼这个类。但这终究是一个幻想,没有谁能真正预知到未来的需求。很多作者都喜欢预留一个XXX接口,然后写了一个XXXImpl的实现类,认为以后只写另外一个XXXImpl2类,就可以重用处理XXX接口时的所有代码。其实以后需要写另外一个XXXImpl2类的概率几乎为0 。这样反而让修改XXX接口的成本上升不少。

1. 无限递归的构造器

在一些情况下,程序可能导致构造器无限递归,比如:

[java]  view plain copy
  1. public class ConstrucorRecursion {  
  2.     ConstrucorRecursion rc;   
  3.     {  
  4.         rc = new ConstrucorRecursion();  
  5.     }  
  6.     public ConstrucorRecursion() {  
  7.         System.out.println("程序执行无参构造函数");  
  8.     }  
  9.     public static void main(String[] args) {  
  10.         ConstrucorRecursion rc = new ConstrucorRecursion();  
  11.     }  
  12. }  
       从表面上看,该程序似乎没有问题,构造器中只有一行输出代码,但是我们应该要知道,不管是定义实例变量时指定初始值,还是在非静态代码块中执行初始化操作,最终都将被提取到构造器中执行,最终导致了在构造器中再次执行了new ConstrucorRecursion()代码,导致递归,从而运行时报了java.lang.StackOverflowError异常

2. 方法重写的陷阱

(1). 重写private方法

       对于使用private修饰符的方法,只能在当前类中使用该方法,子类无法访问父类用private修饰符修饰的方法。既然不能访问父类的private方法,当然也无法重写该方法了。对于在子类中定义了一个和父类private方法具有相同的方法名,相同参数列表,相同返回值类型的方法,依然不是重写,而是在子类中定义了一个新的方法而已

(2). 重写其他访问权限的方法

       对于不使用访问控制符修饰的方法,表明它是包访问控制权限,它只能被与当前类处于同一个包内的其他类访问,其他包中的子类依然不能访问该方法,比如:

[java]  view plain copy
  1. package com.zhi.test1;  
  2.   
  3. public class Animal {  
  4.     void run() {  
  5.         System.out.println("Animal中的run方法");  
  6.     }  
  7. }  

上面的run方法可能被重写:若子类与该类处于同一个package内,子类就可以重写run方法,否则不能被重写。下面的程序就不与Animal类处于同一包内,所以就不能重写Animal类中的run方法,只是重新定义了一个run方法而已

[java]  view plain copy
  1. package com.tempfile_1;  
  2.   
  3. public class Wolf extends com.zhi.test1.Animal {  
  4.       
  5.     private void run() {  
  6.         System.out.println("Wolf中的run方法");  
  7.     }  
  8. }  

3. static关键字

对于static我们只需要记住一句话:被static关键字修饰的成员(Field,方法,内部类,初始化块,内部枚举类)属于类本身,而不是单个的java对象。具体到静态方法也是这样,静态方法属于类,而不是java对象

从下面一个程序中我们可以理解这句话:

[java]  view plain copy
  1. class Animal {  
  2.     public static void info() {  
  3.         System.out.println("Animal类中的info方法");  
  4.     }  
  5. }  
  6.   
  7. @SuppressWarnings("static-access")  
  8. public class Wolf extends Animal {  
  9.       
  10.     public static void info() {  
  11.         System.out.println("Wolf类中的info方法");  
  12.     }  
  13.       
  14.     public static void main(String[] args) {  
  15.         Animal a1 = new Animal();  
  16.         a1.info();  
  17.         Animal a2 = new Wolf();  
  18.         a2.info();  
  19.         Animal a3 = null;  
  20.         a3.info();  
  21.     }  
  22. }  
结果输出3个同样的值:Animal类中info方法

从上面的程序中,我们一开始可能认为应该输出:Animal类中info方法  Wolf类中info方法,因为应该表现出new的对象中的info方法,但是我们要记住的是:上面的info是静态方法,而静态方法是属于类的,而不是属于对象的,上面的程序中a1,a2,a3调用info时,实际上都会委托声明a1,a2,a3的类来执行,也就是Animal来执行info方法


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值