56. 惰性初始化
- public class Lazy {
- private static boolean initial = false;
- static {
- Thread t = new Thread(new Runnable() {
- public void run() {
- System.out.println("befor...");//此句会输出
- /*
- * 由于使用Lazy.initial静态成员,又因为Lazy还未 初
- * 始化完成,所以该线程会在这里等待主线程初始化完成
- */
- initial = true;
- System.out.println("after...");//此句不会输出
- }
- });
- t.start();
- try {
- t.join();// 主线程等待t线程结束
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- public static void main(String[] args) {
- System.out.println(initial);
- }
- }
看看上面变态的程序,一个静态变量的初始化由静态块里的线程来初始化,最后的结果怎样?
当一个线程访问一个类的某个成员的时候,它会去检查这个类是否已经被初始化,在这一过程中会有以下四种情况:
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线程的运行结束。因为这两个线程现在正相互等待,形成了死锁。
修正这个程序的方法就是让主线程在等待线程前就完成初始化操作:
- public class Lazy {
- private static boolean initial = false;
- static Thread t = new Thread(new Runnable() {
- public void run() {
- initial = true;
- }
- });
- static {
- t.start();
- }
- public static void main(String[] args) {
- // 让Lazy类初始化完成后再调用join方法
- try {
- t.join();// 主线程等待t线程结束
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(initial);
- }
- }
虽然修正了该程序挂起问题,但如果还有另一线程要访问Lazy的initial时,则还是很有可能不等initial最后赋值就被使用了。
总之,在类的初始化期间等待某个线程很可能会造成死锁,要让类初始化的动作序列尽可能地简单。
57. 继承内部类
一般地,要想实例化一个内部类,如类Inner1,需要提供一个外围类的实例给构造器。一般情况下,它是隐式地传递给内部类的构造器,但是它也是可以以 expression.super(args) 的方式即通过调用超类的构造器显式的传递。
- public class Outer {
- class Inner1 extends Outer{
- Inner1(){
- super();
- }
- }
- class Inner2 extends Inner1{
- Inner2(){
- Outer.this.super();
- }
- Inner2(Outer outer){
- outer.super();
- }
- }
- }
- class WithInner {
- class Inner {}
- }
- class InheritInner extends WithInner.Inner {
- // ! InheritInner() {} // 不能编译
- /*
- * 这里的super指InheritInner类的父类WithInner.Inner的默认构造函数,而不是
- * WithInner的父类构造函数,这种特殊的语法只在继承一个非静态内部类时才用到,
- * 表示继承非静态内部类时,外围对象一定要存在,并且只能在 第一行调用,而且一
- * 定要调用一下。为什么不能直接使用 super()或不直接写出呢?最主要原因就是每个
- * 非静态的内部类都会与一个外围类实例对应,这个外围类实例是运行时传到内
- * 部类里去的,所以在内部类里可以直接使用那个对象(比如Outer.this),但这里
- * 是在外部内外 ,使用时还是需要存在外围类实例对象,所以这里就显示的通过构造
- * 器传递进来,并且在外围对象上显示的调用一下内部类的构造器,这样就确保了在
- * 继承至一个类部类的情况下 ,外围对象一类会存在的约束。
- */
- InheritInner(WithInner wi) {
- wi.super();
- }
- public static void main(String[] args) {
- WithInner wi = new WithInner();
- InheritInner ii = new InheritInner(wi);
- }
- }
58. Hash集合序列化问题
- class Super implements Serializable{
- // HashSet要放置在父类中会百分百机率出现
- // 放置到子类中就不一定会出现问题了
- final Set set = new HashSet();
- }
- class Sub extends Super {
- private int id;
- public Sub(int id) {
- this.id = id;
- set.add(this);
- }
- public int hashCode() {
- return id;
- }
- public boolean equals(Object o) {
- return (o instanceof Sub) && (id == ((Sub) o).id);
- }
- }
- public class SerialKiller {
- public static void main(String[] args) throws Exception {
- Sub sb = new Sub(888);
- System.out.println(sb.set.contains(sb));// true
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- new ObjectOutputStream(bos).writeObject(sb);
- ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
- sb = (Sub) new ObjectInputStream(bin).readObject();
- System.out.println(sb.set.contains(sb));// false
- }
- }
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. 迷惑的内部类
- public class Twisted {
- private final String name;
- Twisted(String name) {
- this.name = name;
- }
- // 私有的不能被继承,但能被內部类直接访问
- private String name() {
- return name;
- }
- private void reproduce() {
- new Twisted("reproduce") {
- void printName() {
- // name()为外部类的,因为没有被继承过来
- System.out.println(name());// main
- }
- }.printName();
- }
- public static void main(String[] args) {
- new Twisted("main").reproduce();
- }
- }
在顶层的类型中,即本例中的Twisted类,所有的本地的、内部的、嵌套的长匿名的类都可以毫无限制地访问彼此的成员。
另一个原因是私有的不能被继承。
60. 编译期常量表达式
第一个PrintWords代表客户端,第二个Words代表一个类库:
- class PrintWords {
- public static void main(String[] args) {
- System.out//引用常量变量
- .println(Words.FIRST + " "
- + Words.SECOND + " "
- + Words.THIRD);
- }
- }
- class Words {
- // 常量变量
- public static final String FIRST = "the";
- // 非常量变量
- public static final String SECOND = null;
- // 常量变量
- public static final String THIRD = "set";
- }
现在假设你像下面这样改变了那个库类并且重新编译了这个类,但并不重新编译客户端的程序PrintWords:
- class Words {
- public static final String FIRST = "physics";
- public static final String SECOND = "chemistry";
- public static final String THIRD = "biology";
- }
此时,端的程序会打印出什么呢?结果是 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或,那么这个域就不是一个常量。下面你可以通过将一个常量表达式传给一个方法使用得它变成一个非常量:
- class Words {
- // 以下都成非常量变量
- public static final String FIRST = ident("the");
- public static final String SECOND = ident(null);
- public static final String THIRD = ident("set");
- private static String ident(String s) {
- return s;
- }
- }
总之,常量变量将会被编译进那些引用它们的类中。一个常量变量就是任何常量表达式初始化的原生类型或字符串变量。且null不是一个常量表达式。
61. 打乱数组
- class Shuffle {
- private static Random rd = new Random();
- public static void shuffle(Object[] a) {
- for (int i = 0; i < a.length; i++) {
- swap(a, i, rd.nextInt(a.length));
- }
- }
- public static void swap(Object[] a, int i, int j) {
- Object tmp = a[i];
- a[i] = a[j];
- a[j] = tmp;
- }
- public static void main(String[] args) {
- Map map = new TreeMap();
- for (int i = 0; i < 9; i++) {
- map.put(i, 0);
- }
- // 测试数组上的每个位置放置的元素是否等概率
- for (int i = 0; i < 10000; i++) {
- Integer[] intArr = new Integer[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
- shuffle(intArr);
- for (int j = 0; j < 9; j++) {
- map.put(j,(Integer)map.get(j)+intArr[j]);
- }
- }
- System.out.println(map);
- for (int i = 0; i < 9; i++) {
- map.put(i,(Integer) map.get(i)/10000f);
- }
- System.out.println(map);
- }
- }
上面的算法不是很等概率的让某个元素打乱到其位置,程序运行了多次,大致的结果为:
{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,但测试的结果表明:开始的时候比较低,然后增长超过了平均值,最后又降下来了。
如果改用下面算法:
- public static void shuffle(Object[] a) {
- for (int i = 0; i < a.length; i++) {
- swap(a, i, i + rd.nextInt(a.length - i));
- }
- }
多次测试的结果大致如下:
{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工具类:
- public static void shuffle(Object[] a) {
- Collections.shuffle(Arrays.asList(a));
- }
其实算法与上面的基本相似,当然我们使用API中提供的会更好,会在效率上获得最大的受益。
相关推荐
你认为自己了解Java多少?你是个爱琢磨的代码侦探吗?你是否曾经花费数天时间去追踪一个由Java或其类库的陷阱和缺陷而导致的bug?你喜欢智力测验吗?本书正好适合你!.. Bloch和Gafter继承了Effective Jaya一书的传统,...
java 解惑 java 解惑 java 解惑 java 解惑 java 解惑 java 解惑
《Java解惑(中文版)》是一本专为Java初学者设计的学习资料,旨在帮助读者解答在学习Java过程中遇到的各种困惑。"solve65p"可能代表这本书包含65个问题或主题,每个都深入浅出地进行了讲解,旨在解决初学者在编程...
Java PUZZLE Java 解惑 Java PUZZLE Java 解惑 Java PUZZLE Java 解惑Java PUZZLE Java 解惑 Java PUZZLE Java 解惑 Java PUZZLE Java 解惑
"JAVA解惑"的主题针对的是Java学习过程中遇到的一些常见问题和难点,旨在帮助开发者深入理解和解决这些问题。以下是基于这个主题和描述可能涵盖的一些关键知识点: 1. **Java基础**:这可能包括变量、数据类型、...
《Java解惑中文版》是一本专为Java程序员设计的指南,旨在帮助读者解决在编程过程中遇到的各种问题,提升程序的健壮性。本书深入浅出地探讨了Java语言的核心概念、常见疑惑以及最佳实践,旨在使开发者能够编写出更...
这份“Java解惑.pdf”文档很可能包含了解决Java开发者在编程过程中遇到的常见问题和困惑的详细解答。以下是可能涵盖的一些Java相关知识点: 1. **基础语法**:Java的基础语法包括变量、数据类型、运算符、流程控制...
"java解惑" PDF版本
文档《java解惑 PDF版》中列举了95个这样的谜题,每个谜题都旨在帮助开发者理解并纠正一些常见的错误理解。以下是根据提供的部分内容解析的几个相关知识点。 ### 表达式谜题与取余操作符(%)的行为 在Java中,...
《JAVA解惑》这本书主要针对Java编程中遇到的各种常见问题和困惑进行了解答,旨在帮助开发者深入理解Java语言,提高编程技巧。以下是一些关键的知识点解析: 1. **异常处理**:Java中的异常处理是通过try-catch-...
《Java解惑》 布洛克 著;陈昊鹏 译 扫描清晰带目录,仅供参阅,请支持正版
《JAVA解惑》是Java开发者领域的一本经典著作,它被广大...总之,《JAVA解惑》是一本涵盖了Java核心知识点、实战技巧和高级特性的宝典,无论你是Java新手还是老手,都能从中受益匪浅,解决你在Java编程中的种种疑惑。
"Java解惑"这个主题,显然旨在帮助开发者解决他们在学习和实践中遇到的问题。在Java的世界里,疑惑可能涵盖语法、类库、框架、并发、内存管理等多个方面。下面,我们将深入探讨一些常见的Java解惑知识点。 1. **...
讲述如何在程序中避免程序缺陷和程序陷阱的,解惑的过程中,介绍了一些Java编程语言中许多不易被掌握的知识点,其阅读价值非常高,适合具有Java知识的学习者和有编程经验的Java程序员阅读。
JAVA解惑
"java解惑"这个主题旨在帮助初学者理解和解决在学习Java过程中遇到的问题,通过实例来深入浅出地讲解Java的基础知识,同时也强调了实用技巧和注意事项。 "Java解惑"的资料可能包含了两部分:`.chm`和`.pdf`格式的...
《Java解惑(中文)》是一本专门为Java开发者编写的指南书籍,旨在解决在实际编程过程中遇到的各种疑惑和难题。本书以中文语言呈现,使得国内的Java程序员能够更轻松地理解并应用其中的知识。通过阅读这本书,读者...