Java谜题8-更多的库谜题

来源:互联网 发布:mysql 严格模式关闭 编辑:程序博客网 时间:2024/06/03 16:54

谜题76:乒乓

下面的程序全部是由同步化(synchronized)的静态方法组成的。那么它会打印出什么呢?在你每次运行这段程序的时候,它都能保证会打印出相同的内容吗? public class PingPong{  public static synchronized void main(String[] a){    Thread t = new Thread(){      public void run(){ pong(); }    };    t.run();    System.out.print( "Ping" );  }  static synchronized void pong(){    System.out.print( "Pong" );  }}
               题目:在一个同步化的静态方法执行之前,它会获取与它的Class对象相关联的一个管程(monitor)锁[JLS 8.4.3.6]。

                           输出  pong ping!
               原因: run function should be start

谜题77:搞乱锁的妖怪

下面的这段程序模拟了一个小车间。程序首先启动了一个工人线程,该线程在停止时间到来之前会一直工作(至少是假装在工作),然后程序安排了一个定时器任务(timer task)用来模拟一个恶毒的老板,他会试图阻止停止时间的到来。最后,主线程作为一个善良的老板会告诉工人停止时间到了,并且等待工人停止工作。那么这个程序会打印什么呢? import java.util.*;public class Worker extends Thread {    private volatile boolean quittingTime = false;    public void run() {        while (!quittingTime)            pretendToWork();        System.out.println("Beer is good");    }        private void pretendToWork() {        try {            Thread.sleep(300); // Sleeping on the job?        } catch (InterruptedException ex) { }    }    // It's quitting time, wait for worker - Called by good boss    synchronized void quit() throws InterruptedException {        quittingTime = true;        join();    }    // Rescind quitting time - Called by evil boss    synchronized void keepWorking() {        quittingTime = false;    }        public static void main(String[] args)            throws InterruptedException {        final Worker worker = new Worker();        worker.start();        Timer t = new Timer(true); // Daemon thread        t.schedule(new TimerTask() {            public void run() { worker.keepWorking(); }        }, 500);        Thread.sleep(400);        worker.quit();    }}
  • 300 ms:工人线程去检查易变的quittingTime 域,看看停止时间是否已经到了。这个时候并没有到停止时间,所以工人线程会回去继续“工作”。
  • 400ms:作为善良的老板的主线程会去调用工人线程的quit方法。主线程会获得工人线程实例上的锁(因为quit是一个同步化的方法),将quittingTime的值设为true,并且调用工人线程上的join方法。这个对join方法的调用并不会马上返回,而是会等待工人线程执行完毕。
  • 500m:作为恶毒的老板定时器任务开始执行。它将试图调用工人线程的keepWorking方法,但是这个调用将会被阻塞,因为keepWorking是一个同步化的方法,而主线程当时正在执行工人线程上的另一个同步化方法(quit方法)。
  • 600ms:工人线程会再次检查停止时间是否已经到来。由于quittingTime域是易变的,那么工人线程肯定会看到新的值true,所以它会打印 Beer is good 并结束运行。这会让主线程对join方法的调用执行返回,随后主线程也结束了运行。而定时器线程是后台的,所以它也会随之结束运行,整个程序也就结束了。               
             程序输出 : 挂起!

涉及到了Thread.join的实现。这部分内容在关于该方法的文档中(JDK文档)是找不到的,至少在迄今为止发布的文档中如此,也包括5.0版。在内部,Thread.join方法在表示正在被连接(join)的那个Thread实例上调用Object.wait方法。这样就在等待期间释放了该对象上的锁。在我们的程序中,这就使得作为恶毒老板的定时器线程能够堂而皇之的将quittingTime重新设置成false,尽管此时主线程正在执行同步化的quit方法。这样的结果是,工人线程永远不会看到停止时间的到来,它会永远运行下去。作为善良的老板的主线程也就永远不会从join方法中返回了


谜题78:反射的污染

这个谜题举例说明了一个关于反射的简单应用。这个程序会打印出什么呢? import java.util.*;import java.lang.reflect.*;public class Reflector {    public static void main(String[] args) throws Exception {        Set<String> s = new HashSet<String>();        s.add("foo");        Iterator it = s.iterator();        Method m = it.getClass().getMethod("hasNext");        System.out.println(m.invoke(it));    }}
                 题目:

Exception in thread "main"java.lang.IllegalAccessException:

  Class Reflector can not access a member of class HashMap$HashIterator withmodifiers "public"

      at Reflection.ensureMemberAccess(Reflection.java:65)

      at Method.invoke(Method.java:578)

      at Reflector.main(Reflector.java:11)

                 原因:而这个类型是由从it.getClass方法返回的Class对象表示的。这是迭代器的动态类型(dynamic type),它恰好是私有的嵌套类(nested class) java.util.HashMap.KeyIterator。出现 IllegalAccessException 异常的原因就是这个类不是公共的,它来自另外一个包:访问位于其他包中的非公共类型的成员是不合法的[JLS 6.6.1]。无论是一般的访问还是通过反射的访问,上述的禁律都是有效的。
                 详细:

package library;public class Api{    static class PackagePrivate{}    public static PackagePrivate member = new PackagePrivate();}package client;import library.Api;class Client{    public static void main(String[] args){        System.out.println(Api.member.hashCode());    }}尝试编译这段程序会得到如下的错误: Client.java:5: Object.hashCode() isn't defined in a public class or interface; can't be accessed from outside package      System.out.println(Api.member.hashCode());
       修改:

Method m =Iterator.class.getMethod("hasNext");


谜题79:这是狗的生活

下面的这个类模拟了一个家庭宠物的生活。main方法创建了一个Pet实例,用它来表示一只名叫Fido的狗,然后让它运行。虽然绝大部分的狗都在后院里奔跑(run),这只狗却是在后台运行(run)。那么,这个程序会打印出什么呢? public class Pet{    public final String name;    public final String food;    public final String sound;    public Pet(String name, String food, String sound){        this.name = name;        this.food = food;        this.sound = sound;    }        public void eat(){        System.out.println(name + ": Mmmmm, " + food );    }    public void play(){        System.out.println(name + ": " + sound + " " + sound);    }    public void sleep(){        System.out.println(name + ": Zzzzzzz...");    }    public void live(){        new Thread(){            public void run(){                while(true){                    eat();                    play();                    sleep();                }            }        }.start();    }        public static void main(String[] args){        new Pet("Fido", "beef", "Woof").live();    }}

              原因:Thread的sleep遮掩了Pet的sleep!

             修改: 用Runnable

谜题80:更深层的反射

下面这个程序通过打印一个由反射创建的对象来产生输出。那么它会打印出什么呢? public class Outer{    public static void main(String[] args) throws Exception{        new Outer().greetWorld();    }    private void greetWorld()throws Exception {        System.out.println( Inner.class.newInstance() );    }    public class Inner{        public String toString(){            return "Hello world";        }    }}
                   题目:编译失败

                  原因: 从5.0版本开始,关于Class.newInstance的文档叙述道:如果那个Class对象“代表了一个抽象类(abstractclass),一个接口(interface),一个数组类(arrayclass),一个原始类型(primitive type),或者是空(void);或者这个类没有任何空的[也就是无参数的]构造器;或者实例化由于某些其他原因而失败,那么它就会抛出异常”[JAVA-API]。

                   解决:

private void greetWorld() throws Exception{

 Constructor c = Inner.class.getConstructor(Outer.class);

 System.out.println(c.newInstance(Outer.this));

}

谜题81:烧焦到无法识别

下面这个程序看起来是在用一种特殊的方法做一件普通的事。那么,它会打印出什么呢? public class Greeter{    public static void main(String[] args){        String greeting = "Hello World";        for(int i = 0; i < greeting.length(); i++)            System.out.write(greeting.charAt(i));    }}
                  题目:

当有输出产生的时候System.out和System.err会自动地进行刷新,这并不完全正确。这2个流都属于PrintStream类型,在5.0版[Java-API]中,有关这个类型的文档叙述道:

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

                 原因: 

在5.0版[Java-API]中,有关这个类型的文档叙述道:

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

                  解决:

令人好奇的是,如果这个程序改用print(char)去替代write(int),它就会刷新System.out并打印出Hello World。这种行为与print(char)的文档是矛盾的,因为其文档叙述道[Java-API]:

打印一个字符:这个字符将根据平台缺省的字符编码方式被翻译成为一个或多个字节,并且这些字节将完全按照write(int)方法的方式被写出。

类似地,如果程序改用print(String),它也会对流进行刷新,虽然文档中是禁止这么做的。相应的文档确实应该被修改为描述该方法的实际行为,而修改方法的行为则会破坏稳定性。

修改这个程序最简单的方法就是在循环之后加上一个对System.out.flush方法的调用。经过这样的修改之后,程序就会正常地打印出HelloWorld。当然,更好的办法是重写这个程序,使用我们更熟悉的System.out.println方法在控制台上产生输出。

这个谜题的教训与谜题23一样:尽可能使用熟悉的惯用法;如果你不得不使用陌生的API,请一定要参考相关的文档。这里有3条教训给API的设计者们:请让你们的方法的行为能够清晰的反映在方法名上;请清楚而详细地给出这些行为的文档;请正确地实现这些行为。


谜题82:啤酒爆炸

这一章的许多谜题都涉及到了多线程,而这个谜题涉及到了多进程。如果你用一行命令行带上参数slave去运行这个程序,它会打印什么呢?如果你使用的命令行不带任何参数,它又会打印什么呢? public class BeerBlast{    static final String COMMAND = "java BeerBlast slave";    public static void main(String[] args) throws Exception{        if(args.length == 1 && args[0].equals("slave")) {            for(int i = 99; i > 0; i--){                System.out.println( i +                        " bottles of beer on the wall" );                System.out.println(i + " bottles of beer");                System.out.println(                    "You take on down, pass it around,");                System.out.println( (i-1) +                    " bottles of beer on the wall");                System.out.println();            }        }else{            // Master            Process process = Runtime.getRuntime().exec(COMMAND);            int exitValue = process.waitFor();            System.out.println("exit value = " + exitValue);        }    }}
               题目:

如果你使用参数slave来运行该程序,它就会打印出那首激动人心的名为”99 Bottles ofBeer on the Wall”的童谣的歌词,这没有什么神秘的。如果你不使用该参数来运行这个程序,它会启动一个slave进程来打印这首歌谣,但是你看不到slave进程的输出。主进程会等待slave进程结束,然后打印出slave进程的退出值(exit value)。根据惯例,0值表示正常结束,所以0就是你可能期望该程序打印的东西。如果你运行了程序,你可能会发现该程序只会悬挂在那里,不会打印任何东西,看起来slave进程好像永远都在运行着。所以你可能会觉得你应该一直都能听到”99 Bottlesof Beer on the Wall”这首童谣,即使是这首歌被唱走调了也是如此,但是这首歌只有99句,而且,电脑是很快的,你假设的情况应该是不存在的,那么这个程序出了什么问题呢?

这个秘密的线索可以在Process类的文档中找到,它叙述道:“由于某些本地平台只提供有限大小的缓冲,所以如果未能迅速地读取子进程(subprocess)的输出流,就有可能会导致子进程的阻塞,甚至是死锁”[Java-API]。这恰好就是这里所发生的事情:没有足够的缓冲空间来保存这首冗长的歌谣。为了确保slave进程能够结束,父进程必须排空(drain)它的输出流,而这个输出流从master线程的角度来看是输入流。下面的这个工具方法会在后台线程中完成这项工作:

   static void drainInBackground(final InputStream is) {

       new Thread(new Runnable(){

           public void run(){

                try{

                    while( is.read() >= 0 );

                } catch(IOException e){

                    // return onIOException               

                }

           }

       }).start();

    }

如果我们修改原有的程序,在等待slave进程之前调用这个方法,程序就会打印出0:

   }else{ // Master

       Process process = Runtime.getRuntime().exec(COMMAND);

       drainInBackground(process.getInputStream());

       int exitValue = process.waitFor();

       System.out.println("exit value = " + exitValue);

    }

这里的教训是:为了确保子进程能够结束,你必须排空它的输出流;对于错误流(error stream)也是一样,而且它可能会更麻烦,因为你无法预测进程什么时候会倾倒(dump)一些输出到这个流中。在5.0版本中,加入了一个名为ProcessBuilder的类用于排空这些流。它的redirectErrorStream方法将各个流合并起来,所以你只需要排空这一个流。如果你决定不合并输出流和错误流,你必须并行地(concurrently)排空它们。试图顺序化地(sequentially)排空它们会导致子进程被挂起。

多年以来,很多程序员都被这个缺陷所刺痛。这里对于API设计者们的教训是,Process类应该避免这个错误,也许应该自动地排空输出流和错误流,除非用户表示要读取它们。更一般的讲,API应该设计得更容易做出正确的事,而很难或不可能做出错误的事。


谜题83:诵读困难者的一神论

从前有一个人,他认为世上只有一只不寻常的狗,所以他写出了如下的类,将它作为一个单件(singleton)[Gamma95]: public class Dog extends Exception {  public static final Dog  INSTANCE = new Dog();  private Dog() {}  public String toString(){    return "Woof";  }}
                 题目: 

从前有一个人,他认为世上只有一只不寻常的狗,所以他写出了如下的类,将它作为一个单件(singleton)[Gamma95]: public class Dog extends Exception {  public static final Dog  INSTANCE = new Dog();  private Dog() {}  public String toString(){    return "Woof";  }}结果证明这个人的做法是错误的。你能够在这个类的外部不使用反射来创建出第2个Dog实例吗? 这个类可能看起来像一个单件,但它并不是。问题在于,Dog扩展了Exception,而Exception实现了java.io.Serializable。这就意味着Dog是可序列化的(serializable),并且解序列(deserialization)会创建一个隐藏的构造器。正如下面的这段程序所演示的,如果你序列化了Dog.INSTANCE,然后对得到的字节序列(byte sequence)进行解序列,最后你就会得到另外一个Dog。该程序打印的是false,表示新的Dog实例和原来的那个实例是不同的,并且它还打印了Woof,说明新的Dog实例也具有相应的功能: import java.io.*;public class CopyDog{ // Not to be confused with copycat    public static void main(String[] args){         Dog newDog = (Dog) deepCopy(Dog.INSTANCE);         System.out.println(newDog == Dog.INSTANCE);         System.out.println(newDog);    }           // This method is very slow and generally a bad idea!    static public Object deepCopy(Object obj){        try{            ByteArrayOutputStream bos =                new ByteArrayOutputStream();           new ObjectOutputStream(bos).writeObject(obj);            ByteArrayInputStream bin =                new ByteArrayInputStream(bos.toByteArray());            return new ObjectInputStream(bin).readObject();        } catch(Exception e) {            throw new IllegalArgumentException(e);        }    }}要订正这个问题,可在Dog中添加一个readResolve方法,它可以将那个隐藏的构造器转变为一个隐藏的静态工厂(static factory),以返回原来那个的Dog [EJ Items 2,57]。在Dog中添加了这个方法之后,CopyDog将打印true而不是false,表示那个“复本”实际上就是原来的那个实例:     private Object readResolve(){        // Accept no substitues!        return INSTANCE;    }这个谜题的主要教训就是一个实现了Serializable的单件类,必须有一个readResolve方法,用以返回它的唯一的实例。一个次要的教训就是,有可能由于对一个实现了Serializable的类进行了扩展,或者由于实现了一个扩展自Serializable的接口,使得我们在无意中实现了Serializable。给平台设计者的教训是,隐藏的构造器,例如序列化中产生的那个,会让读者对程序行为的产生错觉。 

谜题84:被粗暴地中断

在下面这个程序中,一个线程试图中断自己,然后检查中断是否成功。它会打印什么呢? public class SelfInterruption {    public static void main(String[ ] args) {        Thread.currentThread().interrupt();        if(Thread.interrupted()) {            System.out.println("Interrupted: " +                    Thread.interrupted());        } else{            System.out.println("Not interrupted: " +                    Thread.interrupted());        }    }}
                 题目: 
Thread.interrupted方法第一次被调用的时候返回了true,并且清除了线程的中断状态,所以在if-then-else语句的分支中第2次调用该方法的时候,返回的就是false。调用Thread.interrupted方法总是会清除当前线程的中断状态。方法的名称没有为这种行为提供任何线索,而对于5.0版本,在相应的文档中有一句话概要地也同样具有误导性地叙述道:“测试当前的线程是否中断”[Java-API]。所以,可以理解为什么很多程序员都没有意识到Thread.interrupted方法会对线程的中断状态造成影响。 Thread类有2个方法可以查询一个线程的中断状态。另外一个方法是一个名为isInterrupted的实例方法,而它不会清除线程的中断状态。如果使用这个方法重写程序,它就会打印出我们想要的结果true: public class SelfInterruption {    public static void main(String[ ] args) {        Thread.currentThread().interrupt();        if(Thread.currentThread().isInterrupted()) {            System.out.println("Interrupted: " +                    Thread.currentThread().isInterrupted());        }else{            System.out.println("Not interrupted: " +                    Thread.currentThread().isInterrupted());        }    }}这个谜题的教训是:不要使用Thread.interrupted方法,除非你想要清除当前线程的中断状态。如果你只是想查询中断状态,请使用isInterrupted方法。这里给API设计者们的教训是方法的名称应该用来描述它们主要功能。根据Thread.interrupted方法的行为,它的名称应该是 clearInterruptStatus,因为相对于它对中断状态的改变,它的返回值是次要的。特别是当一个方法的名称并不完美的时候,文档是否能清楚地描述它的行为就显得非常重要了。

谜题85:惰性初始化

下面这个可怜的小类实在是太懒了,甚至于都不愿意用通常的方法进行初始化,所以它求助于后台线程。这个程序会打印什么呢?每次你运行它的时候都会打印出相同的东西吗? public class Lazy {    private static boolean initialized = false;    static {        Thread t = new Thread(new Runnable() {                public void run() {                    initialized = true;                }            });        t.start();        try{            t.join();        }catch (InterruptedException e){            throw new AssertionError(e);        }    }     public static void main(String[] args){        System.out.println(initialized);    }}
            题目: 

如果你运行该程序,你会发现它不会打印任何东西,它只是被挂起了。

            原因: 

虽然有点奇怪,但是这个程序看起来很直观的。静态域initialized初始时被设为false。然后主线程创建了一个后台线程,该线程的run方法将initialized的值设为true。主线程启动了后台线程之后,就调用了join方法等待它的结束。当后台线程完成运行的时候,毫无疑问initialized的值已经被设为了true。当且仅当这个时候,调用了main方法的主线程会打印出initialized的值。如果是这样的话,程序肯定会打印出true吗?如果你运行该程序,你会发现它不会打印任何东西,它只是被挂起了。 为了理解这个程序的行为,我们需要模拟它初始化的细节。当一个线程访问一个类的某个成员的时候,它会去检查这个类是否已经被初始化。在忽略严重错误的情况下,有4种可能的情况[JLS 12.4.2]: •这个类尚未被初始化。 •这个类正在被当前线程初始化:这是对初始化的递归请求。 •这个类正在被其他线程而不是当前线程初始化。 •这个类已经被初始化。 当主线程调用Lazy.main方法时,它会检查Lazy类是否已经被初始化。此时它并没有被初始化(情况1),所以主线程会记录下当前正在进行初始化,并开始对这个类进行初始化。按照我们前面的分析,主线程会将initialized的值设为false,创建并启动一个后台线程,该线程的run方法会将initialized设为true,然后主线程会等待后台线程执行完毕。此时,有趣的事情开始了。 那个后台线程调用了它的run方法。在该线程将Lazy.initialized设为true之前,它也会去检查Lazy类是否已经被初始化。这个时候,这个类正在被另外一个线程进行初始化(情况3)。在这种情况下,当前线程,也就是那个后台线程,会等待Class对象直到初始化完成。遗憾的是,那个正在进行初始化工作的线程,也就是主线程,正在等待着后台线程运行结束。因为这2个线程现在正相互等待着,该程序就死锁了(deadlock)。这就是所有的一切,真是遗憾。有2种方法可以订正这个程序。到目前为止,最好的方法就是不要在类进行初始化的时候启动任何后台线程:有些时候,2个线程并不比1个线程好。更一般的讲,要让类的初始化尽可能地简单。订正这个程序的第2种方法就是让主线程在等待后台线程之前就完成类的初始化: // Bad way to eliminate the deadlock. Complex and error pronepublic class Lazy {    private static boolean initialized = false;    private static Thread t = new Thread(new Runnable() {                    public void run() {                            initialized = true;                    }            });    static {        t.start();    }        public static void main(String[] args){        try{            t.join();        }catch (InterruptedException e){            throw new AssertionError(e);        }        System.out.println(initialized);    }}虽然这么做确实消除了死锁,但是它却是一个非常不好的想法。主线程需要等待后台线程完成工作,但是其他的线程不需要这么做。一旦主线程完成了对Lazy类的初始化,其他线程就可以使用这个类了。这使得在initialized的值还是false的时候,其他线程就可以观察到它。 总之,在类的初始化期间等待某个后台线程很可能会造成死锁。要让类初始化的动作序列尽可能地简单。类的自动初始化被公认为是语言设计上的难题,Java的设计者们在这个方面做得很不错。如果你写了一些复杂的类初始化代码,很多种情况下,你这是在搬起石头砸自己的脚。






原创粉丝点击