Java解惑学习有感(八)---更多的库之谜

来源:互联网 发布:百科门窗软件下载 编辑:程序博客网 时间:2024/06/06 11:04

谜题76:乒乓

1、当你想调用一个线程的 start 方法时要多加小心,别弄错成调用这个线程的 run 方法了。

2、一个线程可以重复地获得某个相同的锁

3、Thread 类之所以有一个公共的 run 方法,是因为它实现了 Runnable 接口。

谜题77:搞乱锁的妖怪

1、Thread.join 方法在表示正在被连接(join)的那个 Thread 实例上调用 Object.wait 方法。这样就在等待期间释放了该对象上的锁。

2、除非有关于某个类的详细说明作为保证,否则千万不要假设库中的这个类对它的实例或类上的锁会做(或者不会做)某些事情。对于库的任何调用都可能会产生对 wait、notify、notifyAll 方法或者某个同步化方法的调用。所有这些,都可能对应用级的代码产生影响。

3、在 Java 语言中,一个对象实际上就是一个锁:你在对象本身之上进行同步。

谜题78:反射的污染

以下代码:

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());
}
}

Api.member.hashCode()这样的访问是不被允许的,因为访问其他包中的非公共类型的成员是不合法的,即使这个成员同时也被声明为某个公共类型的公共成员也是如此,需要经过如此修正才可以:System.out.println(((Object)Api.member).hashCode());

同样,反射也有同样的情况

以下代码:

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));
}
}

这样的Method m = it.getClass().getMethod("hasNext");调用也是会报错的,可以这样修改:Method m =Iterator.class.getMethod("hasNext");

谜题79:这是狗的生活

以下代码:

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();
}
}

对于对 sleep 方法的调用,这个最内层的范围就是包含有该调用的匿名类(anonymous class),这个类继承了 Thread.sleep(long)方法和Thread.sleep(long,int)方法,它们是该范围内唯一的名称为 sleep 的方法,但是由于它们都带有参数,所以都不适用于这里的调用。由于该方法调用的 2 个候选方法都不适用,所以编译器就打印出了错误信息。从 Thread 那里继承到匿名类中的 2 个 sleep 方法遮蔽(shadow)了我们想要调用的 sleep 方法。

修订方法只要把把 Pet 中的 sleep 方法的名字改成 snooze, doze 或者 nap。

总之,要小心无意间产生的遮蔽,并且要学会识别表明存在这种情况的编译器错
误信息。对于编译器的编写者来说,你应该尽力去产生那些对程序员来说有意义
的错误消息。例如在我们的程序中,编译器应该可以警告程序员,存在着适用于
方法调用但却被遮蔽掉的方法

谜题80:更深层的反射

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

2、从 Java 程序到 class 文件的映射的复杂度,请避免使用反射来实例化内部类。更一般地讲,当我们在用高级语言特性定义的程序元素之上使用反射的时候,一定要小心,从反射的视角观察程序可能不同与从代码的视角去观察它。

3、一个非静态的嵌套类的构造器,在编译的时候会将一个隐藏的参数作为它的第一个参数,这个参数表示了它的直接外围实例(immediately enclosing instance)。当你在代码中任何可以让编译器找到合适的外围实例的地方去调用构造器的时候,这个参数就会被隐式地传递进去。但是,上述的过程只适用于普通的构造器调用,也就是不使用反射的情况。当你使用反射调用构造器时,这个隐藏的参数就需要被显式地传递,这对于 Class.newInstance 方法是不可能做到的。要传递这个隐藏参数的唯一办法就是使用java.lang.reflect.Constructor。

代码辅证:

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";
}

}

}

 Inner.class.newInstance() 是会运行出错的,可以做如下更改:

Constructor c = Inner.class.getConstructor(Outer.class);
System.out.println(c.newInstance(Outer.this));

这个程序因为内部类没有引用到外部类,也可以直接将 Inner 类型声明为静态的(static)

谜题81:烧焦到无法识别

System.out 是带有缓冲的

1、大多数的程序员认为,当有输出产生的时候 System.out 和 System.err 会自动地进行刷新,这并不完全正确。这 2 个流都属于 PrintStream 类型,在 5.0 版[Java-API]中,有关这个类型的文档叙述道:一个 PrintStream 可以被创建为自动刷新的;这意味着当一个字节数组(byte array)被写入,或者某个 println 方法被调用,或者一个换行字符或字节(‘\n’)被写入之后,PrintStream 类型的 flush 方法就会被自动地调用。

2、write(int)是唯一一个在自动刷新(automatic flushing)功能开启的情况下不刷新PrintStream的输出方法(output method)。即如下代码中,是无法正常输出Hello World的

String greeting = "Hello World";
for(int i = 0; i < greeting.length(); i++)
System.out.write(greeting.charAt(i));
}

修改这个程序最简单的方法就是在循环之后加上一个对 System.out.flush 方法
的调用

或者使用我们更熟悉的 System.out.println 方法在控制台上产生输出

谜题82:啤酒爆炸

讨论了一下多进程:

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参数来运行这个程序,它会启动一个 slave 进程来打印这首歌谣,但是你
看不到 slave 进程的输出

原因可以在可以在 Process 类的文档中找到,它叙述道:“由于某些本地平台只提供有限大小的缓冲,所以如果未能迅速地读取子进程(subprocess)的输出流,就有可能会导致子进程的阻塞,甚至是死锁”

这恰好就是这里所发生的事情:没有足够的缓冲空间来保存这首冗长的歌谣。为了确保 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 on IOException
}

}
}).start();
}

修改原有程序:

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)排空它们会导致子进程被挂起。

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

研究以下代码:

public class Dog extends Exception {
public static final Dog INSTANCE = new Dog();
private Dog() {}
public String toString(){
return "Woof";
}
}

1、Dog 扩展了 Exception,而 Exception 实现了 java.io.Serializable。这就意味着 Dog 是可序列化的(serializable),并且解序列(deserialization)会创建一个隐藏的构造器。正如下面的这段程序所演示的,如果你序列化了 Dog.INSTANCE,然后对得到的字节序列(byte sequence)进行解序列,最后你就会得到另外一个 Dog。该程序打印的是 false,表示新的 Dog 实例和原来的那个实例是不同的,并且它还打印了 Woof

代码如下:

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);
}
}
}

2、修订使得可序列化的Dog在解序列化后还是单一实例:

在 Dog 中添加一个 readResolve 方法,它可以将那个隐藏的构造器转变为一个隐藏的静态工厂(static factory)

private Object readResolve(){
// Accept no substitues!
return INSTANCE;
}

3、一个实现了 Serializable 的单件类,必须有一个readResolve 方法,用以返回它的唯一的实例。

谜题84:被粗暴地中断

1、不要使用 Thread.interrupted 方法,除非你想要清除当前线程的中断状态。如果你只是想查询中断状态,请使用 isInterrupted 方法。

2、根据Thread.interrupted 方法的行为,它的名称应该是 clearInterruptStatus,因
为相对于它对中断状态的改变,它的返回值是次要的。

谜题85:惰性初始化

1、不要在类进行初始化的时候启动任何后台线程:有些时候,2 个线程并不比 1 个线程好。更一般的讲,要让类的初始化尽可能地简单。

2、在类的初始化期间等待某个后台线程很可能会造成死锁。


如果有疑问或者对该博文有何看法或建议或有问题的,欢迎评论,恳请指正!


0 0