1、
一个覆写方法的访问修饰符所提供的访问权限与被覆写方法的访问修饰符所提供的访问权限相比,至少要一样多。
2、
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)锁。在上面的程序中,主线程会在创建第二个线程之前获得与PingPong.class 相关联的那个锁。只要主线程占有着这个锁,第二个线程就不可能执行同步化的静态方法。在本例中,主线程会调用那个新的线程实例的run 方法,这个run 方法会在主线程中同步地运行。由于一个线程可以重复地获得某个相同的锁 [JLS 17.1] ,所以当run 方法调用pong 方法的时候,主线程就被允许再次获得与PingPong.class 相关联的锁。pong 方法打印了Pong 并且返回到了run方法,而run方法又返回到main方法。最后,main方法打印了Ping,这就解释了我们看到的输出结果是怎么来的。
要订正这个程序很简单,只需将 t.run 改写成 t.start。这么做之后,这个程序就会如你所愿的总是打印出 PingPong 了。
3、
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();
}
}
无论是Timer 类还是Thread.sleep 方法,都不能保证具有实时(real-time)性。这就是说,由于这里计时的粒度太粗,所以上述几个事件很有可能会在时间轴上互有重叠地交替发生。在500ms 时,当作为恶毒老板的定时器任务运行时,根据时间轴的显示,它对keepWorking 方法的调用会被阻塞,因为keepWorking 是一个同步化的方法并且主线程正在同一个对象上执行着同步化方法quit(在Thread.join 中等待着)。这些都是对的,keepWorking 确实是一个同步化的方法,并且主线程确实正在同一个对象上执行着同步化的quit 方法。即使如此,定时器线程仍然可以获得这个对象上的锁,并且执行keepWorking方法。在内部,Thread.join 方法在表示正在被连接(join)的那个Thread 实例上调
用Object.wait 方法。这样就在等待期间释放了该对象上的锁。在我们的程序中,这就使得作为恶毒老板的定时器线程能够堂而皇之的将quittingTime 重新设置成false,尽管此时主线程正在执行同步化的quit 方法。这样的结果是,工人线程永远不会看到停止时间的到来,它会永远运行下去。作为善良的老板的主线程也就永远不会从join 方法中返回了。
使这个程序产生了预料之外的行为的根本原因就是WorkerThread 类的作者使用了实例上的锁来确保quit 方法和keepWorking 方法的互斥,但是这种用法与超类(Thread)内部对该锁的用法发生了冲突。这里的教训是:除非有关于某个类的详细说明作为保证,否则千万不要假设库中的这个类对它的实例或类上的锁会做(或者不会做)某些事情。对于库的任何调用都可能会产生对wait、notify、notifyAll 方法或者某个同步化方法的调用。所有这些,都可能对应用级的代码产生影响。
如果你需要获得某个锁的完全控制权,那么就要确定没有任何其他人能够访问到它。如果你的类扩展了库中的某个类,而这个库中的类可能使用了它的锁,或者如果某些不可信的人可能会获得对你的类的实例的访问权,那么请不要使用与这个类或它的实例自动关联的那些锁。取而代之的,你应该在一个私有的域中创建一个单独的锁对象。从5.0 版本开始,java.util.concurrent.locks 提供了2 种可选方案:ReentrantLock 和ReentrantReadWriteLock。它们不能被用在同步化的语句块(synchronized block)中,而且必须辅以try-finally 语句对其进行显式的获取和释放。
订正这个程序最直接的方法是添加一个Object 类型的私有域作为锁,并且在quit 和keepWorking 方法中对这个锁对象进行同步。通过上述修改之后,该程序就会打印出我们所期望的Beer is good。可以看出,该程序能够产生正确行为并不依赖于它必须遵从我们前面分析的时间轴:
private final Object lock = new Object();
// It's quitting time, wait for worker - Called by good boss
void quit() throws InterruptedException{
synchronized (lock){
quittingTime = true;
join();
}
}
// Rescind quitting time - Called by evil boss
void keepWorking(){
synchronized(lock){
quittingTime = false;
}
}
}
另外一种可以修复这个程序的方法是让Worker 类实现Runnable 而不是扩展Thread,然后在创建每个工人线程的时候都使用Thread(Runnable)构造器。这样可以将每个Worker 实例上的锁与其线程上的锁进行解耦。这是一个规模稍大一些的重构。正如库类对锁的使用会干扰应用程序一样,应用程序中对锁的使用也会干扰库类。例如,在迄今为止发布的所有版本的JDK(包括5.0 版本)中,为了创建一个新的Thread 实例,系统都会去获取Thread 类上的锁。而执行下面的代码就可以阻止任何新线程的创建:
synchronized(Thread.class){
Thread.sleep(Long.MAX_VALUE);
}
总之,永远不要假设库类会(或者不会)对它的锁做某些事情。为了隔离你自己的程序与库类对锁的使用,除了那些专门设计用来被继承的库类之外,请避免继承其它库类 。为了确保你的锁不会遭受外部的干扰,可以将它们设为私有以阻止其他人对它们的访问。
4、
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));
}
}
在这个程序中,该方法是从某个类中选择出来的,而这个类型是由从it.getClass 方法返回的Class 对象表示的。这是迭代器的动态类型(dynamic type),它恰好是私有的嵌套类(nested class)java.util.HashMap.KeyIterator。出现 IllegalAccessException 异常的原因就是这个类不是公共的,它来自另外一个包:访问位于其他包中的非公共类型的成员是不合法的。
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());
}
}
这个错误与前面那个由含有反射的程序所产生的运行期错误具有相同的意义。Object 类型和hashCode 方法都是公共的。问题在于hashCode 方法是通过一个限定类型调用的,但用户访问不到这个类型。该方法调用的限定类型是library.Api.PackagePrivate,这是一个位于其他包的非公共类型。
这并不意味着Client 就不能调用Api.member 的hashCode 方法。要做到这一点,只需要使用一个可访问的限定类型即可,在这里可以将Api.member 转型成Object。经过这样的修改之后,Client 类就可以顺利地编译和运行了:
System.out.println(((Object)Api.member).hashCode());
在使用反射访问某个类型时,请使用表示某种可访问类型的Class 对象。回到我们前面的那个程序,hasNext 方法是声明在一个公共类型 java.util.Iterator中的,所以它的类对象应该被用来进行反射访问。经过这样的修改后,这个Reflector 程序就会打印出true:
Method m = Iterator.class.getMethod("hasNext");
你完全可以避免这一类的问题,你应该只有在实例化时才使用反射,而方法调用都通过使用接口进行。这种使用反射的用法,可以将那些调用方法的类与那些实现这些方法的类隔离开,并且提供了更高程度的类型安全。这种用法在“服务提供者框架”(Service Provider Frameworks)中很常见。这种模式并不能解决反射访问中的所有问题,但是如果它可以解决你所遇到的问题,请务必使用它。
总之,访问其他包中的非公共类型的成员是不合法的,即使这个成员同时也被声明为某个公共类型的公共成员也是如此。不论这个成员是否是通过反射被访问的,上述规则都是成立的。这个问题很有可能只在反射访问中才会出现。