java解惑你知多少(八)

系统 2009 0

56. 惰性初始化

Java代码   收藏代码
  1. public   class  Lazy {  
  2.   private   static   boolean  initial =  false ;  
  3.   static  {  
  4.   Thread t =  new  Thread( new  Runnable() {  
  5.     public   void  run() {  
  6.     System.out.println( "befor..." ); //此句会输出   
  7.      /*  
  8.      * 由于使用Lazy.initial静态成员,又因为Lazy还未 初  
  9.      * 始化完成,所以该线程会在这里等待主线程初始化完成  
  10.      */   
  11.     initial =  true ;  
  12.     System.out.println( "after..." ); //此句不会输出   
  13.    }  
  14.   });  
  15.   t.start();  
  16.    try  {  
  17.    t.join(); // 主线程等待t线程结束   
  18.   }  catch  (InterruptedException e) {  
  19.    e.printStackTrace();  
  20.   }  
  21.  }  
  22.   
  23.   public   static   void  main(String[] args) {  
  24.   System.out.println(initial);  
  25.  }  
  26. }  

看看上面变态的程序,一个静态变量的初始化由静态块里的线程来初始化,最后的结果怎样?

 

当一个线程访问一个类的某个成员的时候,它会去检查这个类是否已经被初始化,在这一过程中会有以下四种情况:
1、 这个类尚未被初始化
2、 这个类正在被当前线程初始化:这是对初始化的递归请求,会直接忽略掉(另,请参考《构造器中静态常量的引用问题》一节)
3、 这个类正在被其他线程而不是当前线程初始化:需等待其他线程初始化完成再使用类的Class对象,而不会两个线程都会去初始化一遍(如果这样,那不类会初始化两遍,这显示不合理)
4、 这个类已经被初始化


当主线程调用Lazy.main,它会检查Lazy类是否已经被初始化。此时它并没有被初始化(情况1),所以主线程会记录下当前正在进行的初始化,并开始对这个类进行初始化。这个过程是:主线程会将initial的值设为false,然后在静态块中创建并启动一个初始化initial的线程t,该线程的run方法会将initial设为true,然后主线程会等待t线程执行完毕,此时,问题就来了。


由于t线程将Lazy.initial设为true之前,它也会去检查Lazy类是否已经被初始化。这时,这个类正在被另外一个线程(mian线程)进行初始化(情况3)。在这种情况下,当前线程,也就是t线程,会等待Class对象直到初始化完成,可惜的是,那个正在进行初始化工作的main线程,也正在等待t线程的运行结束。因为这两个线程现在正相互等待,形成了死锁。

 

修正这个程序的方法就是让主线程在等待线程前就完成初始化操作:

Java代码   收藏代码
  1. public   class  Lazy {  
  2.   private   static   boolean  initial =  false ;  
  3.   static  Thread t =  new  Thread( new  Runnable() {  
  4.    public   void  run() {  
  5.    initial =  true ;  
  6.   }  
  7.  });  
  8.   static  {  
  9.   t.start();  
  10.  }  
  11.   
  12.   public   static   void  main(String[] args) {  
  13.    // 让Lazy类初始化完成后再调用join方法   
  14.    try  {  
  15.    t.join(); // 主线程等待t线程结束   
  16.   }  catch  (InterruptedException e) {  
  17.    e.printStackTrace();  
  18.   }  
  19.   System.out.println(initial);  
  20.  }  
  21. }  

虽然修正了该程序挂起问题,但如果还有另一线程要访问Lazy的initial时,则还是很有可能不等initial最后赋值就被使用了。

 

总之,在类的初始化期间等待某个线程很可能会造成死锁,要让类初始化的动作序列尽可能地简单。


57. 继承内部类

一般地,要想实例化一个内部类,如类Inner1,需要提供一个外围类的实例给构造器。一般情况下,它是隐式地传递给内部类的构造器,但是它也是可以以 expression.super(args) 的方式即通过调用超类的构造器显式的传递。

Java代码   收藏代码
  1. public   class  Outer {  
  2.   class  Inner1  extends  Outer{  
  3.   Inner1(){  
  4.     super ();  
  5.   }  
  6.  }  
  7.   class  Inner2  extends  Inner1{  
  8.   Inner2(){  
  9.    Outer. this . super ();  
  10.   }  
  11.   Inner2(Outer outer){  
  12.    outer. super ();  
  13.   }  
  14.  }  
  15. }  
Java代码   收藏代码
  1. class  WithInner {  
  2.   class  Inner {}  
  3. }  
  4. class  InheritInner  extends  WithInner.Inner {  
  5.   // ! InheritInner() {} // 不能编译   
  6.   /*  
  7.   * 这里的super指InheritInner类的父类WithInner.Inner的默认构造函数,而不是  
  8.   * WithInner的父类构造函数,这种特殊的语法只在继承一个非静态内部类时才用到,  
  9.   * 表示继承非静态内部类时,外围对象一定要存在,并且只能在 第一行调用,而且一  
  10.   * 定要调用一下。为什么不能直接使用 super()或不直接写出呢?最主要原因就是每个  
  11.   * 非静态的内部类都会与一个外围类实例对应,这个外围类实例是运行时传到内  
  12.   * 部类里去的,所以在内部类里可以直接使用那个对象(比如Outer.this),但这里  
  13.   * 是在外部内外 ,使用时还是需要存在外围类实例对象,所以这里就显示的通过构造  
  14.   * 器传递进来,并且在外围对象上显示的调用一下内部类的构造器,这样就确保了在  
  15.   * 继承至一个类部类的情况下 ,外围对象一类会存在的约束。  
  16.   */   
  17.  InheritInner(WithInner wi) {  
  18.   wi. super ();  
  19.  }  
  20.   
  21.   public   static   void  main(String[] args) {  
  22.   WithInner wi =  new  WithInner();  
  23.   InheritInner ii =  new  InheritInner(wi);  
  24.  }  
  25. }  

 

58. Hash集合序列化问题

Java代码   收藏代码
  1. class  Super  implements  Serializable{  
  2.   // HashSet要放置在父类中会百分百机率出现   
  3.   // 放置到子类中就不一定会出现问题了   
  4.   final  Set set =  new  HashSet();   
  5. }  
  6. class  Sub  extends  Super {  
  7.   private   int  id;  
  8.   public  Sub( int  id) {  
  9.    this .id = id;  
  10.   set.add( this );  
  11.  }  
  12.   public   int  hashCode() {  
  13.    return  id;  
  14.  }  
  15.   public   boolean  equals(Object o) {  
  16.    return  (o  instanceof  Sub) && (id == ((Sub) o).id);  
  17.  }  
  18. }  
  19.   
  20. public   class  SerialKiller {  
  21.   public   static   void  main(String[] args)  throws  Exception {  
  22.   Sub sb =  new  Sub( 888 );  
  23.   System.out.println(sb.set.contains(sb)); // true   
  24.     
  25.   ByteArrayOutputStream bos =  new  ByteArrayOutputStream();  
  26.    new  ObjectOutputStream(bos).writeObject(sb);  
  27.     
  28.   ByteArrayInputStream bin =  new  ByteArrayInputStream(bos.toByteArray());  
  29.   sb = (Sub)  new  ObjectInputStream(bin).readObject();  
  30.     
  31.   System.out.println(sb.set.contains(sb)); // false   
  32.  }  
  33. }  

Hash一类集合都实现了序列化的writeObject()与readObject()方法。这里错误原因是由HashSet的readObject方法引起的。在某些情况下,这个方法会间接地调用某个未初始化对象的被覆写的方法。为了组装正在反序列化的HashSet,HashSet.readObject调用了HashMap.put方法,而put方法会去调用键的hashCode方法。由于整个对象图正在被反序列

化,并没有什么可以保证每个键在它的hashCode方法被调用时已经被完全初始化了,因为HashSet是在父类中定义的,而在序列化HashSet时子类还没有开始初始化(这里应该是序列化)子类,所以这就造成了在父类中调用还没有初始完成(此时id为0)的被子类覆写的hashCode方法,导致该对象重新放入hash表格的位置与反序列化前不一样了。hashCode返回了错误的值,相应的键值对条目将会放入错误的单元格中,当id被初始化为888时,一切都太迟了。

 

这个程序的说明,包含了HashMap的readObject方法的序列化系统总体上违背了不能从类的构造器或伪构造器(如序列化的readObject)中调用可覆写方法的规则。

 

如果一个HashSet、Hashtable或HashMap被序列化,那么请确认它们的内容没有直接或间接地引用它们自身,即正在被序列化的对象。

 

另外,在readObject或readResolve方法中,请避免直接或间接地在正在进行反序列化的对象上调用任何方法,因为正在反序列化的对象处于不稳定状态。


59. 迷惑的内部类

Java代码   收藏代码
  1. public   class  Twisted {  
  2.   private   final  String name;  
  3.  Twisted(String name) {  
  4.    this .name = name;  
  5.  }  
  6.   // 私有的不能被继承,但能被內部类直接访问   
  7.   private  String name() {  
  8.    return  name;  
  9.  }  
  10.   private   void  reproduce() {  
  11.    new  Twisted( "reproduce" ) {  
  12.     void  printName() {  
  13.      // name()为外部类的,因为没有被继承过来   
  14.     System.out.println(name()); // main   
  15.    }  
  16.   }.printName();  
  17.  }  
  18.   
  19.   public   static   void  main(String[] args) {  
  20.    new  Twisted( "main" ).reproduce();  
  21.  }  
  22. }  

在顶层的类型中,即本例中的Twisted类,所有的本地的、内部的、嵌套的长匿名的类都可以毫无限制地访问彼此的成员。

 

另一个原因是私有的不能被继承。


60. 编译期常量表达式

第一个PrintWords代表客户端,第二个Words代表一个类库:

Java代码   收藏代码
  1. class  PrintWords {  
  2.   public   static   void  main(String[] args) {  
  3.   System.out //引用常量变量   
  4.     .println(Words.FIRST +  " "    
  5.       + Words.SECOND +  " "    
  6.       + Words.THIRD);  
  7.  }  
  8. }  
  9.   
  10. class  Words {  
  11.   // 常量变量   
  12.   public   static   final  String FIRST =  "the" ;  
  13.   // 非常量变量   
  14.   public   static   final  String SECOND =  null ;  
  15.   // 常量变量   
  16.   public   static   final  String THIRD =  "set" ;  
  17. }  

现在假设你像下面这样改变了那个库类并且重新编译了这个类,但并不重新编译客户端的程序PrintWords:

Java代码   收藏代码
  1. class  Words {  
  2.   public   static   final  String FIRST =  "physics" ;  
  3.   public   static   final  String SECOND =  "chemistry" ;  
  4.   public   static   final  String THIRD =  "biology" ;  
  5. }  

此时,端的程序会打印出什么呢?结果是 the chemistry set,不是the null set,也不是physics chemistry biology,为什么?原因就是 null 不是一个编译期常量表达式,而其他两个都是。

 

对于常量变量(如上面Words类中的FIRST、THIRD)的引用(如在PrintWords类中对Words.FIRST、Words.THIRD的引用)会在编译期被转换为它们所表示的常量的值(即PrintWords类中的Words.FIRST、Words.THIRD引用会替换成"the"与"set")。

 

一个常量变量(如上面Words类中的FIRST、THIRD)的定义是,一个在编译期被常量表达式(即编译期常量表达式)初

始化的final的原生类型或String类型的变量。

 

那什么是“编译期常量表达式”?精确定义在[JLS 15.28]中可以找到,这样要说的是null不是一个编译期常量表达式。

 

由于常量变量会编译进客户端,API的设计者在设计一个常量域之前应该仔细考虑一下是否应该定义成常量变量。

 

如果你使用了一个非常量的表达式去初始化一个域,甚至是一个final或,那么这个域就不是一个常量。下面你可以通过将一个常量表达式传给一个方法使用得它变成一个非常量:

Java代码   收藏代码
  1. class  Words {  
  2.   // 以下都成非常量变量   
  3.   public   static   final  String FIRST = ident( "the" );  
  4.   public   static   final  String SECOND = ident( null );  
  5.   public   static   final  String THIRD = ident( "set" );  
  6.   private   static  String ident(String s) {  
  7.    return  s;  
  8.  }  
  9. }  

总之,常量变量将会被编译进那些引用它们的类中。一个常量变量就是任何常量表达式初始化的原生类型或字符串变量。且null不是一个常量表达式。


61. 打乱数组

Java代码   收藏代码
  1. class  Shuffle {  
  2.   private   static  Random rd =  new  Random();  
  3.   public   static   void  shuffle(Object[] a) {  
  4.    for  ( int  i =  0 ; i < a.length; i++) {  
  5.    swap(a, i, rd.nextInt(a.length));  
  6.   }  
  7.  }  
  8.   public   static   void  swap(Object[] a,  int  i,  int  j) {  
  9.   Object tmp = a[i];  
  10.   a[i] = a[j];  
  11.   a[j] = tmp;  
  12.  }  
  13.   public   static   void  main(String[] args) {  
  14.   Map map =  new  TreeMap();  
  15.    for  ( int  i =  0 ; i <  9 ; i++) {  
  16.    map.put(i,  0 );  
  17.   }  
  18.     
  19.    // 测试数组上的每个位置放置的元素是否等概率   
  20.    for  ( int  i =  0 ; i <  10000 ; i++) {  
  21.    Integer[] intArr =  new  Integer[] {  0 1 2 3 4 5 6 7 8  };  
  22.    shuffle(intArr);  
  23.     for  ( int  j =  0 ; j <  9 ; j++) {  
  24.     map.put(j,(Integer)map.get(j)+intArr[j]);  
  25.    }  
  26.   }  
  27.   System.out.println(map);  
  28.    for  ( int  i =  0 ; i <  9 ; i++) {  
  29.    map.put(i,(Integer) map.get(i)/10000f);  
  30.   }  
  31.   System.out.println(map);  
  32.  }  
  33. }  

上面的算法不是很等概率的让某个元素打乱到其位置,程序运行了多次,大致的结果为:
{0=36031, 1=38094, 2=39347, 3=40264, 4=41374, 5=41648, 6=41780, 7=41188, 8=40274}
{0=3.6031, 1=3.8094, 2=3.9347, 3=4.0264, 4=4.1374, 5=4.1648, 6=4.178, 7=4.1188, 8=4.0274}

 

如果某个位置上等概率出现这9个值的话,则平均值会趋近于4,但测试的结果表明:开始的时候比较低,然后增长超过了平均值,最后又降下来了。

 

如果改用下面算法:

Java代码   收藏代码
  1. public   static   void  shuffle(Object[] a) {  
  2.   for  ( int  i =  0 ; i < a.length; i++) {  
  3.   swap(a, i, i + rd.nextInt(a.length - i));  
  4.  }  
  5. }  

多次测试的结果大致如下:
{0=40207, 1=40398, 2=40179, 3=39766, 4=39735, 5=39710, 6=40074, 7=39871, 8=40060}
{0=4.0207, 1=4.0398, 2=4.0179, 3=3.9766, 4=3.9735, 5=3.971, 6=4.0074, 7=3.9871, 8=4.006}
所以修改后的算法是合理的。

 

另一种打乱集合的方式是通过Api中的Collections工具类:

Java代码   收藏代码
  1. public   static   void  shuffle(Object[] a) {  
  2.  Collections.shuffle(Arrays.asList(a));  
  3. }  

其实算法与上面的基本相似,当然我们使用API中提供的会更好,会在效率上获得最大的受益。

java解惑你知多少(八)


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论