java技巧

编译器会在包含有正确名称的方法的最内层范围内查找需要调用的方法



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


关于Class.newInstance 的文档叙述道:如果那个Class 对象“代表了一个抽象类(abstract class),一个接口(interface),一个数组类(array class),一个原始类型(primitive type),或者是空(void);或者这个类没有任何空的[也就是无参数的]构造器;或者实例化由于某些其他原因而失败,那么它就会抛出异常。
一个非静态的嵌套类的构造器,在编译的时候会将一个隐藏的参数作为它的第一个参数,这个参数表示了它的直接外围实例(immediately enclosing instance)。当你在代码中任何可以让编译器找到合适的外围实例的地方去调用构造器的时候,这个参数就会被隐式地传递进去。但是,上述的过程只适用于普通的构造器调用,也就是不使用反射的情况。当你使用反射调用构造器时,这个隐藏的参数就需要被显式地传递,这对于Class.newInstance 方法是不可能做到的。要传递这个隐藏参数的唯一办法就是使用java.lang.reflect.Constructor。当对程序进行了这样的修改后,它就可以正常的打印出 Hello World 了:
private void greetWorld() throws Exception{
Constructor c = Inner.class.getConstructor(Outer.class);
System.out.println(c.newInstance(Outer.this));
}
作为其他的选择,你可能观察到了,Inner 实例并不需要一个外围的Outer 实例,所以可以将Inner 类型声明为静态的(static)。




System.out 是带有缓冲的。Hello World 中的字符被写入了System.out 的缓冲区,但是缓冲区从来都没有被刷新(flush)。这个流都属于PrintStream 类型,一个PrintStream 可以被创建为自动刷新的;这意味着当一个字节数组(bytearray)被写入,或者某个println 方法被调用,或者一个换行字符或字节(‘\n’)被写入之后,PrintStream 类型的flush 方法就会被自动地调用。
有关write(int)方法的文档叙述道:将指定的byte 写入流。如果这个byte 是一个换行字符,并且流可以自动刷新,那么flush 方法将被调用。实际上,write(int)是唯一一个在自动刷新(automatic flushing)功能开启的情况下不刷新PrintStream的输出方法(output method)。




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


由于某些本地平台只提供有限大小的缓冲,所以如果未能迅速地读取子进程(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();
}




一个实现了Serializable 的单件类,解序列会创建一个隐藏的构造器。如果序列化Dog.INSTANCE,然后对字节序列进行解序列,最后会得到另外一个Dog。必须有一个readResolve 方法,将隐藏的构造器转变为一个隐藏的静态工厂,用以返回它的唯一的实例:
private Object readResolve(){
// Accept no substitues!
return INSTANCE;
}




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());
}
}
}
当一个方法捕捉到了一个InterruptedException 异常,而且没有做好处理这个异常的准备时,那么这个方法通常会将该异常重新抛出(rethrow)。但是由于这是一个“被检查的异常”,所以只有在方法声明允许的情况下该方法才能够将异常重新抛出。如果不能重新抛出,该方法可以通过中断当前线程对异常“再构建”(reraise)。Thread.interrupted 方法第一次被调用的时候返回了true,并且清除了线程的中断状态,所以在if-then-else 语句的分支中第2 次调用该方法的时候,返回的就是false。调用Thread.interrupted 方法总是会清除当前线程的中断状态。
不要使用Thread.interrupted 方法,除非你想要清除当前线程的中断状态。如果你只是想查询中断状态,请使用isInterrupted 方法。




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);
}
}
当一个线程访问一个
类的某个成员的时候,它会去检查这个类是否已经被初始化。在忽略严重错误的
情况下,有4 种可能的情况:
? 这个类尚未被初始化。
? 这个类正在被当前线程初始化:这是对初始化的递归请求。
? 这个类正在被其他线程而不是当前线程初始化。
? 这个类已经被初始化。


当主线程调用Lazy.main 方法时,它会检查Lazy 类是否已经被初始化。此时它并没有被初始化(情况1),所以主线程会记录下当前正在进行初始化,并开始对这个类进行初始化。按照我们前面的分析,主线程会将initialized 的值设为false,创建并启动一个后台线程,该线程的run 方法会将initialized 设为true,然后主线程会等待后台线程执行完毕。此时,有趣的事情开始了。那个后台线程调用了它的run 方法。在该线程将Lazy.initialized 设为true之前,它也会去检查Lazy 类是否已经被初始化。这个时候,这个类正在被另外一个线程进行初始化(情况3)。在这种情况下,当前线程,也就是那个后台线程,会等待Class 对象直到初始化完成。遗憾的是,那个正在进行初始化工作的线程,也就是主线程,正在等待着后台线程运行结束。因为这2 个线程现在正相互等待着,该程序就死锁了(deadlock)。
有2种方法可以订正这个程序。到目前为止,最好的方法就是不要在类进行初始化的时候启动任何后台线程:有些时候,2 个线程并不比1 个线程好。更一般的讲,要让类的初始化尽可能地简单。订正这个程序的第2 种方法就是让主线程在等待。




import java.util.*;
public class Pair<T> {
private final T first;
private final T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T first() {
return first;
}
public T second() {
return second;
}
public List<String> stringList() {
return Arrays.asList(String.valueOf(first),
String.valueOf(second));
}
public static void main(String[] args) {
Pair p = new Pair<Object> (23, "skidoo");
System.out.println(p.first() + " " + p.second());
for (String s : p.stringList())
System.out.print(s + " ");
}
}
一个原生类型就是一个没有任何类型参数的泛型类或泛型接口的名字。例如,List<E>是一个泛型接口,List<String> 是一个参数化的类型,而List 就是一个原生类型。在我们的程序中,唯一用到原生类型的地方就是在main 方法中对局部变量p 的声明:
Pair p = new Pair<Object> (23, "skidoo")
一个原生类型很像其对应的参数化类型,但是它的所有实例成员都要被替换掉,而替换物就是这些实例成员被擦除掉对应部分之后剩下的东西。具体地说,在一个实例方法声明中出现的每个参数化的类型都要被其对应的原生部分所取代。我们程序中的变量p 是属于原生类型Pair 的,所以它的所有实例方法都要执行这种擦除。这也包括声明返回List<String>的方法stringList。编译器会将这个方法解释为返回原生类型List。
当List<String>实现了参数化类型Iterable<String>时,List 也实现了原生类型Iterable。Iterable<String>有一个iterator 方法返回参数化类型Iterator<String>,相应地,Iterable 也有一个iterator 方法返回原生类型Iterator。当Iterator<String>的next 方法返回String 时,Iterator 的next方法返回Object。因此,循环迭代p.stringList()需要一个Object 类型的循环变量,这就解释了编译器的那个奇怪的错误消息的由来。这种现象令人想不通的原因在于参数化类型List<String>虽然是方法stringList 的返回类型,但它与Pair 的类型参数没有关系,事实上最后它被擦除了。
正确解决这个问题的方法是为局部变量p 提供一个合适的参数化的声明:


Pair<Object> p = new Pair<Object>(23, "skidoo");


以下是要点强调:原生类型List 和参数化类型List<Object>是不一样的。如果使用了原生类型,编译器不会知道在list 允许接受的元素类型上是否有任何限制,它会允许你添加任何类型的元素到list 中。这不是类型安全的:如果你添加了一个错误类型的对象,那么在程序接下来的执行中的某个时刻,你会得到一个ClassCastException 异常。如果使用了参数化类型List<Object>,编译器便会明白这个list 可以包含任何类型的元素,所以你添加任何对象都是安全的。
还有第三种与以上两种类型密切相关的类型:List<?>是一种特殊的参数化类型,被称为通配符类型(wildcard type)。像原生类型List 一样,编译器也不会知道它接受哪种类型的元素,但是因为List<?>是一个参数化类型,从语言上来说需要更强的类型检查。为了避免出现ClassCastException 异常,编译器不允许你添加除null 以外的任何元素到一个类型为List<?>的list 中。原生类型是为兼容5.0 版以前的已有代码而设计的,因为它们不能使用泛型。5.0版中的许多核心库类,collections,已经利用泛型做了改变,但是使用这些类的已有程序的行为仍然与在以前的版本上运行一样。这些原生类型及其成员的行为被设计成可以镜像映射到5.0 之前的Java 语言上,从而保持了兼容性。
这个Pair 程序的真正问题在于编程者没有决定究竟使用哪种Java 版本。尽管程序中大部分使用了泛型,而变量p 却被声明成原生类型。为了避免被编译错误所迷惑,请避免在打算用5.0 或更新的版本来运行的代码中编写原生类型。如果一个已有的库方法返回了一个原生类型,那么请将它的结果存储在一个恰当的参数化类型的变量中。然而,最好的办法还是尽量将该库升级到使用泛型的版本上。虽然Java 提供了原生类型和参数化类型间的良好的互用性,但是原生类型的局限性会妨碍泛型的使用。


实际上,这种问题在用getAnnotation 方法在运行期读取Class 的注解(annotations)的情况下也会发生,该方法是在5.0 版中新添加到Class 类中的。每次调用getAnnotation 方法时都会涉及到两个Class 对象:一个是在其上调用该方法的对象,另一个是作为传递参数指出需要哪个类的注解的对象。在一个典型的调用中,前者是通过反射获得的,而后者是一个类名称字面常量,如下例所示:


Author a = Class.forName(name).getAnnotation(Author.class);


你不必把getAnnotation 的返回值转型为Author。以下两种机制保证了这种做法可以正常工作:(1)getAnnotation 方法是泛型的。它是通过它的参数类型来确定返回类型的。具体地说,它接受一个Class<T>类型的参数,返回一个T类型的值。(2)类名称字面常量提供了泛型信息。例如,Author.class 的类型是Class<Author>。类名称字面常量可以传递运行时和编译时的类型信息。以这
种方式使用的类名称字面常量被称作类型符号(type token)。与类名称字面常量不同的是,通过反射获得的Class 对象不能提供完整的泛型类型信息:Class.forName的返回类型是通配类型Class<?>。在调用getAnnotation方法的表达式中,使用的是通配类型而不是原生类型Class,这一点很重要。如
果你采用了原生类型,返回的注解具有的就是编译期的Annotation 类型而不是通过类名称字面常量指示的类型了。下面的程序片断错误地使用了原生类型,和本谜题中最初的程序一样不能通过编译,其原因也一样:


Class c = Class.forName(name); // Raw type!
Author a = c.getAnnotation(Author.class); // Type mismatch


总之,原生类型的成员被擦掉,是为了模拟泛型被添加到语言中之前的那些类型的行为。如果你将原生类型和参数化类型混合使用,那么便无法获得使用泛型的所有好处,而且有可能产生让你困惑的编译错误。另外,原生类型和以Object为类型参数的参数化类型也不相同。最后,如果你想重构现有的代码以利用泛型的优点,那么最好的方法是一次只重构一个API,并且保证新的代码中绝不使用原生类型。








public class LinkedList<E> {
private Node<E> head = null;
private class Node<E> {
E value;
Node<E> next;
// Node constructor links the node as a new head
Node(E value) {
this.value = value;
this.next = head;
head = this;
}
}
public void add(E e) {
new Node<E>(e);
// Link node as new head
}
public void dump() {
for (Node<E> n = head; n != null; n = n.next)
System.out.println(n.value + " ");
}
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<String>();
list.add("world");
list.add("Hello");
list.dump();
}
}







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值