第十二章通过异常处理错误
使用异常的好处:一是使用程序更加的健壮。二是它往往能够降低错误处理的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它,而如果使用异常,那就不必在方法调用处进行检查,因为异常机制保证能够捕获这个错误,并且,只需在一个地方处理错误,即所谓的异常处理程序块中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离,总之,与以前的错误处理方法相比,异常机制使用代码的阅读、编写和调试工作更加高效。
当抛出异常后,有几件事会随之发生。首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使用程序能要么换一种方式运行,要么继续运行下去。
通常把错误信息输出到e.printStackTrace(System.out)要比直接e.printStackTrace()要好,因为System.out也许会重定向,而e.printStackTrace()默认则是将信息输出到操作系统标准错误流,所以一般我们使用e.printStackTrace(System.out)打印异常信息。如果把结果送到System.err,它就会把会随System.out一起被重定向,这样更容易被用户注意。
使用程序包的客户端程序员可能仅仅只是查看一下抛出的异常类型,其他的就不管了(大多数Java库里的异常都是这么用的),所以对异常所添加的其他功能也许根本用不上,名称代表发生的问题,并且异常的名称应该可以望文知意。
可以声明方法将抛出异常,实际上却不抛出,这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。
Exception的方法
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("My Exception");
} catch (Exception e) {
System.out.println("getMessage():" + e.getMessage());
System.out.println("getLocalizedMessage():" + e.getLocalizedMessage());
System.out.println("toString():" + e);
System.out.println("printStackTrace():");
e.printStackTrace(System.out);
}
}
}
/*
getMessage():My Exception
getLocalizedMessage():My Exception
toString():java.lang.Exception: My Exception
printStackTrace():
java.lang.Exception: My Exception
at excep.ExceptionMethods.main(ExceptionMethods.java:10)
*/
使用JDK日志器记录日志
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;
class MyException extends Exception {
String errKey;//错误键
public MyException setErrKey(String errKey) {
this.errKey = errKey;
return this;
}
//重写父类方法,输出详细信息
public String getMessage() {
return "errKey = " + errKey + " " + super.getMessage();
}
}
public class LoggingExceptions {
// JDK日志记录器
private static Logger logger = Logger.getLogger("LoggingExceptions");
static void logException(Exception e) {// 记录异常日志
// 字符串缓存
StringWriter trace = new StringWriter();
// 将日志输出到缓存
e.printStackTrace(new PrintWriter(trace));
// 输出异常日志
logger.severe(trace.toString());
}
public static void main(String[] args) {
try {
throw new MyException().setErrKey("100");
} catch (MyException e) {
logException(e);
}
}
}
/*
2010-2-9 11:17:38 excep.LoggingExceptions logException
严重: excep.MyException: errKey = 100 null
at excep.LoggingExceptions.main(LoggingExceptions.java:37)
*/
printStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,这个方法返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一桢。元素0是栈顶元素,并且是调用序列中的最后一个方法调用。数组中的最后一个元素即栈底是调用序列中的第一个方法调用。
public class WhoCalled {
static void f() {
// Generate an exception to fill in the stack trace
try {
throw new Exception();
} catch (Exception e) {
for (StackTraceElement ste : e.getStackTrace())
System.out.println("getClassName: " + ste.getClassName()
+ " getFileName: " + ste.getFileName()
+ " getLineNumber: " + ste.getLineNumber()
+ " getMethodName: " + ste.getMethodName()
+ " isNativeMethod: " + ste.isNativeMethod());
}
}
static void g() {
f();
}
static void h() {
g();
}
public static void main(String[] args) {
f();
System.out.println("--------------------------------");
g();
System.out.println("--------------------------------");
h();
}
}
/*
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 8 getMethodName: f isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 28 getMethodName: main isNativeMethod: false
--------------------------------
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 8 getMethodName: f isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 20 getMethodName: g isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 30 getMethodName: main isNativeMethod: false
--------------------------------
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 8 getMethodName: f isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 20 getMethodName: g isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 24 getMethodName: h isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 32 getMethodName: main isNativeMethod: false
*/
如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的:
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside g(),e.printStackTrace()");
e.printStackTrace(System.out);
throw e;//再次抛出
}
}
public static void h() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside h(),e.printStackTrace()");
e.printStackTrace(System.out);
//重新抛出,这一行将成为异常的新发生行了,好比在这里重新包装new后抛出:throw new Exception();
throw (Exception)e.fillInStackTrace();
}
}
public static void main(String[] args) {
try {
g();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
try {
h();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
} /* Output:
originating the exception in f()
Inside g(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:4)
at Rethrowing.g(Rethrowing.java:8)
at Rethrowing.main(Rethrowing.java:27)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:4)
at Rethrowing.g(Rethrowing.java:8)
at Rethrowing.main(Rethrowing.java:27)
originating the exception in f()
Inside h(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:4)
at Rethrowing.h(Rethrowing.java:17)
at Rethrowing.main(Rethrowing.java:33)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.h(Rethrowing.java:22)
at Rethrowing.main(Rethrowing.java:33)
*///:~
常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这个被称为异常链。在JDK1.4以前,程序员必须自己编写代码来保存原始中异常的信息。现在所有Throwable的子类在构造器中都可能接受一个cause对象作为参数(Throwable(Throwable cause))。这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并了新的异常,也能通过这个异常链追踪到异常最初发生的位置。并可以使用initCause(Throwable cause)来重样设置异常根原因,但此方法至多可以调用一次,如果抛出的异常是通过 Throwable(Throwable) 或 Throwable(String,Throwable) 创建的,则该异常对象的此方法甚至一次也不能调用。
public class ExcpTest {
public static void nullExc() {
throw new NullPointerException();
}
public static void call1() throws Exception {
try {
nullExc();
} catch (Exception e) {
throw new Exception(e);
}
}
public static void call2() throws Exception {
try {
call1();
} catch (Exception e) {
//该行运行时会抛异常,因为e已经设置过 cause 了
throw (Exception) e.initCause(new ArithmeticException());
}
}
public static void call3() throws Exception {
try {
nullExc();
} catch (Exception e) {
//该行运行没问题,因为还没有给 e 设置 cause
throw (Exception) e.initCause(new ArithmeticException());
}
}
public static void main(String[] args) {
try {
call1();
} catch (Exception e) {
e.printStackTrace();
}
try {
call2();
} catch (Exception e) {
e.printStackTrace();
}
try {
call3();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
java.lang.Exception: java.lang.NullPointerException
at ExcpTest.call1(ExcpTest.java:11)
at ExcpTest.main(ExcpTest.java:37)
Caused by: java.lang.NullPointerException
at ExcpTest.nullExc(ExcpTest.java:4)
at ExcpTest.call1(ExcpTest.java:9)
... 1 more
java.lang.IllegalStateException: Can't overwrite cause
at java.lang.Throwable.initCause(Unknown Source)
at ExcpTest.call2(ExcpTest.java:21)
at ExcpTest.main(ExcpTest.java:42)
java.lang.NullPointerException
at ExcpTest.nullExc(ExcpTest.java:4)
at ExcpTest.call3(ExcpTest.java:27)
at ExcpTest.main(ExcpTest.java:47)
Caused by: java.lang.ArithmeticException
at ExcpTest.call3(ExcpTest.java:30)
... 1 more
*/
RuntimeException:属于运行时异常(即非捕获性异常),它包括继承自它的所有子类会自动被Java虚拟机抛出,所以不向在方法声明时抛出异常说明,所以下面的代码也将是多余的:
if(t == null){
throw new NullPointerException();
}
如果不捕获运行时异常,则异常会穿过(抛出)所有的调用路径直达main()方法,而不会被捕获,并在程序退出前将自动调用异常的printStackTrace()方法。
受检查异常是程序可以处理的异常。如果抛出异常的方法本身不能处理它,那么方法调用者应该去处理它,从而使程序运行,不至于终止程序。
运行时异常表示无法让程序恢复运行的异常,导致这种异常的原因通常是由于执行了错误操作,一旦出现了错误操作,建议终止程序,因此Java编译器不检查这种异常。如果出现运行时异常,则表示你的程序代码本身的问题,而不是由程序外界引起的,比如读文件时文件不存在,这不是由程序本身引起的,所以文件不存在的抛出的是检查行异常。
RuntimeException代表的是编程错误:
1、无法预料的错误,比如从你控制范围之外传递进来的null引用。
2、作为程序员,应该在代码中进行检查错误。(比如对于ArrayIndexOutOfBoundsException,就得注意一下数组的大小了。)在一个地方发生的异常,常常会在另一个地方导致错误。
运行时异常是应该尽量避免的(也是完全可能避免的,既然是可以避免的,所以运行时异常不需捕获),在程序调试阶段,遇到这种异常时,正确的做法是程序的设计和实现方式,修改程序中的错误,从而避免这种异常。捕获运行时异常并且使程序恢复运行并不是明智的办法,这主要有两方面的原因:
1、这种异常一旦发生,损失严重。
2、即使程序恢复运行,也可能会导致程序的业务逻辑错乱,甚至导致更严重的异常,或都得到错误的运行结果。
Error类及其子类表示程序本身无法修复的错误,它和运行时异常的相同之处是:Java编译器都会检查它们,当程序运行时出现它们时都会终止程序。
如果有必要,一般将try放循环里,这样就避免了当Java中的异常出现时我们回到异常抛出地点再次执行的问题,这样可以建立了一个“程序继续执行之前必须要达到”的条件。
避免过于庞大的try代码块,因为try代码块越庞大,出现异常的地方就越多,要发生异常的原因就越困难。
不要使用catch(Exception ex)子句来捕获所有异常,理由如下:
1、对不同的异常通常有不现的处理方式,不同的错误使用同样的处理方式是不现实的。
2、会捕获本应该抛出的运行异常,掩盖程序中的错误。
什么情况下才用到finally?当要把除内存之外的资源恢复到它们的初始状态时,就要用到finally子名,这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部对象某个状态的恢复。
Finllay块的异常丢失
public class ExceptionSilencer {
public static void f() {
try {
throw new RuntimeException();
} finally {
// 从这里返回时,异常将丢失,不會再向外拋出了
return;
}
}
public static void main(String[] args) {
f();//得不到打印的信息
}
}
如果一个类继承了某个类同时又实现了某个接口,他们有同样的接口方法,但都抛出了不同的捕获性异常,则该子类实现与重写该方法时,则方法声明处不能抛出任何捕获性异常了。
如果调用的父类构造器抛出捕获性异常,则子类相应的构造器也只能抛出,不能在构造器里进行捕获。
构造器抛出异常时正确的清理方式:
比如在构造器中打开了一个文件,清理动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理,而不能直接在构造器里的finally块上关闭,因为finally块是不管是否有异常都会关闭,而构造器执行成功能外界需要这个文件流。但如果在文件成功打开后才抛出异常,则需要关闭文件,并向外界抛出异常信息:
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class InputFile {
private BufferedReader in;
// 抛出异常的构造器
public InputFile(String fname) throws Exception {
try {
// 在构造器中打开一个文件流
in = new BufferedReader(new FileReader(fname));
} catch (FileNotFoundException e) {
System.out.println("Could not open " + fname);
// 如果是文件没有找到,则不需要关闭流,只需重新抛出
throw e;
} catch (Exception e) {
// 如果是其他异常,则需要关闭流,因為文件已打开
try {
in.close();
} catch (IOException e2) {
System.out.println("in.close() unsuccessful");
}
throw e; // 再重新抛出
} finally {
// 这里不能关闭流!!!
}
}
public String getLine() {
String s;
try {
s = in.readLine();
} catch (IOException e) {
throw new RuntimeException("readLine() failed");
}
return s;
}
// 在文件成功打开后,j由外界使用完后调用
public void dispose() {
try {
in.close();
System.out.println("dispose() successful");
} catch (IOException e2) {
throw new RuntimeException("in.close() failed");
}
}
}
class Cleanup {
public static void main(String[] args) {
// 该try是对构造器异常的捕获,如果出现了异常则不需关闭,
// 因为构造器内部已处理
try {
InputFile in = new InputFile("InputFile.java");
// 如果运行到这里说明文件已正常打开,所以后面需关闭
try {
String s;
int i = 1;
while ((s = in.getLine()) != null)
; // 读文件...
} catch (Exception e) {
System.out.println("Caught Exception in main");
e.printStackTrace(System.out);
} finally {
// 不管读取是否正常,用完后一定要关闭
in.dispose();
}
} catch (Exception e) {
System.out.println("InputFile construction failed");
}
}
}
异常处理的一个重要的原则是“只有在你知道如何处理的情况下才捕获异常”。实际上,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中。这样以来,主干代码就不会与错误处理逻辑混在一起,也更容易理解和维护。
“被检查的异常”可能使问题变得复杂,因为它们强制你在可能还没有准备好处理错误的时候被迫加上cacth子名,即使我们不知道如何处理的情况下,这就导致了异常的隐藏:
try{
//… throw …
}catch(Exception e){//什么都不做}
把“被检查的异常”转换为“不检查的异常”:当在一个普通方法里调用别的方法时,要考虑到“我不知道该怎样处理这个异常,但是也不能把它‘吞’了,或者只打印一些无用的消息”。JDK1.4的异常链提供了一种新的思路来解决这个问题,可以直接把“被检查的异常”包装进RuntimeException里面:
try{
//… throw 检查异常…
}catch(IDontKnowWhatToDoWithThisCheckedException e){
Throw new RuntimeException(e);
}
如果想把“被检查的异常”这种功能“屏蔽”掉的话,上面是一个好的办法。不用“吞”掉异常,也不必把它放到方法的异常声明里面,而异常链还能保证你不会丢失任何原始异常的信息。你还可以在“知道如何处理的地方”来处理它,也可以其他上层catch里通过 throw e.getCause(); 再次抛出原始的“被检查的异常”:
public class Test {
static void f() {
try {
throw new Exception();
} catch (Exception e) {
//将检测性异常转换成非检测性异常后继续抛出
throw new RuntimeException(e);
}
}
static void g() {
//不用捕获,因为检测异常转换了运行异常
f();
}
static void h() throws Exception {
try {
g();
} catch (Exception e) {
e.printStackTrace();
System.out.println("-----");
//可以将检测异常继续做为检测异常抛出
throw new Exception(e.getCause());
}
}
public static void main(String[] args) {
try {
h();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
java.lang.RuntimeException: java.lang.Exception
at Test.f(Test.java:7)
at Test.g(Test.java:13)
at Test.h(Test.java:18)
at Test.main(Test.java:29)
Caused by: java.lang.Exception
at Test.f(Test.java:4)
... 3 more
-----
java.lang.Exception: java.lang.Exception
at Test.h(Test.java:23)
at Test.main(Test.java:29)
Caused by: java.lang.Exception
at Test.f(Test.java:4)
at Test.g(Test.java:13)
at Test.h(Test.java:18)
... 1 more
*/
第十三章字符串
字符串是不可变的:是final类固不能继承它;也不能通过指向它的引用来修改它的内容。
StringBuilder是Java SE5引用的,在这之前用的是StringBuffer。后者是线程安全的,因此开销也会大些,所以在Java SE5/6中,字符串操作应该还会更快一点。
在JDK1.5中:String s = "a" + "b" + "c"; 在编译时,编译器会自动引入java.lang.StringBuilder类,并使用StringBuilder.append方法来连接。虽然我们在源码中并没有使用StringBuilder类,但是编译器自动地使用了它,因为它更高效。
虽然在JDK1.5或以上版本中使用“+”连接字符串时为避免产生过多的字符串对象,编译器会自加使用StringBuilder类来优化,但是如果连接操作在循环里,编译器会为每次循环都创建一个StringBuilder对象,所以在循环里一般我们不要直接使用“+”连接字符串,而是自己在循环外显示的创建一个StringBuilder对象,用它来构造最终的结果。但是在使用StringBuilder类时也要注意,不要这样使用:StringBuilder.append(a + ":" + c); ,如果这样,那编译器就会掉入陷井,从而为你另外创建一个StringBuilder对象处理括号内的字符串连接操作。
如果重写了父类的toString方法(一般是Object的toString),当需要打印对象的内存地址时,应该调用super.toString()方法,而不是直接打印this,否则会发生StackOverflowError异常。
Java SE5的PrintStream与PrintWriter对象都引入了format()方法,那我们就要可以使用System.out.format()格式化输出了。format()方法模仿自C语言的printf(),如果你比较怀旧的话,也可以使用printf(),它还是调用format()来实现的,只不过换了个名而已:
public class SimpleFormat {
public static void main(String[] args) {
int x = 5;
double y = 5.332542;
// 以前我们是这样打印的:
System.out.println("Row 1: [" + x + " " + y + "]");
// 现在我们是这样打印的:
System.out.format("Row 1: [%d %f]\n", x, y);
// 或者是
System.out.printf("Row 1: [%d %f]\n", x, y);
}
} /* Output:
Row 1: [5 5.332542]
Row 1: [5 5.332542]
Row 1: [5 5.332542]
*///:~
在Java中,所有新的格式化功能(PrintStream与PrintWriter的format方法以及String的静态方法format)都是由java.util.Formatter类来处理的,创建时需要指定输出到哪。
Formatter的对齐格式化输出抽象语法:
%[argument_index$][flags][width][.precision]conversion
可选的 argument_index是一个十进制整数,用于表明参数在参数列表中的位置。第一个参数由 "1$" 引用,第二个参数由 "2$" 引用,依此类推。
Width用来控制一个域的最小尺寸,如果输出的参数值不够宽,则添加空格来确保一个域至少达到某个宽度,在默认的情况下,数据是右对齐的,但我们可以使用“-” flags标志来改变对齐方向。
与width相对的是precision,它用来指明输出的最大尺寸。Width可以应用于各种数据类型时其行为方式都是一样,但precision不一样,并不是所有类型都能应用precision,而且,应用于不同类型的数据转换时,precision的意义也不同:将precision应用String时,它表示打印String时输出字符的最大个数;而在将precision应用于浮点数时,它表示小数部分要显示出来的位数(默认是6位小数位),如果小数位过多则舍入,太少则在尾部补零。由于整数没有小数部分,固不能应用于整形数据类型,否则抛异常:
import java.util.Formatter;
public class Receipt {
private double total = 0;
// 这里输出到控制台,我们也可以格式化后输出到文件、StringBuilder、CharBuffer
private Formatter f = new Formatter(System.out);
public void printTitle() {
// %-15s表示输出一个最小宽度为15的且左对齐的字符,2$表示左起输出参数位置
f.format("%2$-15s %1$5s %3$10s\n", "Qty", "Item", "Price");
f.format("%-15s %5s %10s\n", "----", "---", "-----");
}
public void print(String name, int qty, double price) {
// %-15.15s在%-15s基础上最多能输出15个字符
f.format("%-15.15s %5d %10.2f\n", name, qty, price);
total += price;
}
public void printTotal() {
// %10.2f表示输出一个最小宽度为10,右对齐小数点后两位的浮点数
f.format("%-15s %5s %10.2f = %s\n", "Tax", "", total * 0.06, total + " * 0.06");
f.format("%-15s %5s %10s\n", "", "", "-----");
f.format("%-15s %5s %10.2f = %s\n", "Total", "", total * 1.06, total + " * 1.06");
}
public static void main(String[] args) {
Receipt receipt = new Receipt();
receipt.printTitle();
receipt.print("Jack's Magic Beans", 4, 4.254);
receipt.print("Princess Peas", 3, 5.1);
receipt.print("Three Bears Porridge", 1, 14.285);
receipt.printTotal();
}
}
/*
* Output:
Item Qty Price
---- --- -----
Jack's Magic Be 4 4.25
Princess Peas 3 5.10
Three Bears Por 1 14.29
Tax 1.42 = 23.639 * 0.06
-----
Total 25.06 = 23.639 * 1.06
*/
Fomatter常用类型转换字符
%d :整数型(十进制)
%c :Unicode字符
%b :Boolean值
%s :String
%f :浮点数(十进制)
%e :浮点数(科学计数)
%x :整数(十六进制)
%h :散列码(十六进制)
%% :字符“%”
“%b”对于boolean基本类型及Boolean,其转换结果为对应的true或false,但是,对其他类型的参数,只要该参数不为null,那转换的结果就永远都是true,即使是数字0,转换结果依然为true,而这不像其他语言(如C)为false。上面列举的是常用的格式字符,其他可以在JDK文档中的Formatter类部分找到。
String.format():String.format()是一个Static方法,当我们只需要使用format()方法一次时很方便,其实在String.format()内部,它也是创建一个Formatter对象,格式化后传进的字符串后返回新的字符串。下面是一个十六进制工具:
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class Hex {
/**
* 以十六进制格式输出数据
* @param counts 每行多少个
* @param data 要格式化的数据
* @return 格式化后的数据
*/
public static String format(int counts, byte[] data) {
StringBuilder result = new StringBuilder();
int n = 0;
int rows = 0;
for (byte b : data) {
if (n % counts == 0) {
rows++;
result.append(String.format("%05d: ", rows));
}
result.append(String.format("%02X ", b));
n++;
if (n % counts == 0) {
result.deleteCharAt(result.length() - 1);
result.append("\n");
}
}
result.append("\n");
return result.toString();
}
/**
* 读取二进制文件
* @param bFile
* @return
* @throws IOException
*/
public static byte[] read(File bFile) throws IOException {
BufferedInputStream bf = new BufferedInputStream(new FileInputStream(
bFile));
try {
byte[] data = new byte[bf.available()];
bf.read(data);
return data;
} finally {
bf.close();
}
}
public static void main(String[] args) throws IOException {
System.out.println(format(16, read(new File("src/Hex.class")
.getAbsoluteFile())));
}
}
/*
* Output:
00001: CA FE BA BE 00 00 00 31 00 7F 07 00 02 01 00 03
00002: 48 65 78 07 00 04 01 00 10 6A 61 76 61 2F 6C 61
00003: 6E 67 2F 4F 62 6A 65 63 74 01 00 06 3C 69 6E 69
...
*/
判断一个字符串是否匹配指定的模式,最简单的是使用String对象的matches方法,需传递正则式参数,实质上是调用Pattern.matches(regex, string)来实现的。
String的split()方法也使用到了正则式,该方法的重载版本允许你限制字符串分割次数split(String regex,int limit) :limit 参数控制模式应用的次数,因此影响结果数组的长度,默认就是0。如果该限制 n大于 0,则模式将被最多应用 n - 1 次,数组的长度将不会大于 n,而且数组的最后项将包含超出最后匹配的定界符的所有输入。如果 n为非正,则模式将被应用尽可能多的次数,而且数组可以是任意长度。如果 n为零,则模式将被应用尽可能多的次数,数组可有任何长度,并且结尾空字符串将被丢弃。例如,字符串 "boo:and:foo" 使用这些参数可生成下列结果:
Regex Limit 结果
: 2 { "boo", "and:foo" }
: 5 { "boo", "and", "foo" }
: -2 { "boo", "and", "foo" }
o 5 { "b", "", ":and:f", "", "" }
o -2 { "b", "", ":and:f", "", "" }
o 0 { "b", "", ":and:f" }
CharBuffer、String、StringBuffer、StringBuilder都实现了CharSequence接口,大多数的正则表达式操作都接受CharSequence类型的参数。
如果使用功能强大的正则表达式对象,我们使用静态的Patter.compile()方法来编译正则表达式即可,它会生成一个Patter对象。接下来将想要检测的字符串传Patter对象的matcher()方法,会生成一个Matcher对象,该对象有很多的功能可用。
另外,Patter类还提供了静态的方法:static boolean matches(String regex, CharSequence input)
Matcher对象的groupCount方法返回该匹配器的模式中的分组数目,但第0组不包括在内。
String的split()方法实质上是调用Pattern对象的split方法:Pattern.compile(regex).split(string, limit)来实现的。String的replace方法则是通过调用Matcher对象的replace方法来实现的。
Matcher对象的appendReplacement(StringBuffer sbuf,String replacement)执行渐进式的替换,而不是像replaceFirst()和replaceAll()那样只替换第一个匹配或全部匹配。这是一个非常重要的方法,它允许你调用其他方法来生成或处理replacemanet(replaceFirst()和replaceAll()则只能使用一个固定的字符串),使你能够以编程的方式将目标分割成组,从而具备列强大的替换功能,appendTail(StringBuffer sbuf),在执行了一次或多次appendReplacement之后,调用此方法可以将输入字符串余下的部分复制到sbuf中。
Matcher对象的reset()、reset(CharSequence input)将Matcher对象重新设置到当前字符序列的起始位置,可以重用Pattern与Matcher对象。
Java SE5新增了Scanner类,它可以大大减轻扫描输入的工作。可以通过useDelimiter(String pattern)设置next操作的定界符。还可通过hasNext(String pattern)判断是否还存在指定正则式的串,如果有,则可通过String next(String pattern)来读取。除此之后,它还有很多的读取各种不同基本类型的数据的nextXX方法。
第十四章类型信息
在运行时识别对象和类的信息有两种方式:一种是“传统的”RTTI(如“(Circle)”),它假设我们在编译时已经知道了所有的类型,但易引起ClassCastException异常,不过我们可以通过 instanceof 先检测具体类型;另一种是“反射”机制(使用类型的Class对象),它允许我们在运行时查询Class对象的信息。
Class对象相关方法:
getName():回此 Class对象所表示的实体(类、接口、数组类、基本类型或 void)名称。如果此类对象表示的是非数组类型的引用类型,则返回该类的二进制名称,包括包名;如果此类对象表示一个基本类型或 void,则返回的名字是一个与该基本类型或void 所对应的 Java 语言关键字相同的串;如果此类对象表示一个数组类,该数组嵌套深度的一个或多个 '[' 字符加元素类型名。元素类型名的编码如下:
元素类型编码
boolean Z
byte B
char C
类或接口 Lclassname;
double D
float F
int I
long J
short S
如:
String.class.getName():java.lang.String
byte.class.getName():byte
(new Object[3]).getClass().getName():[Ljava.lang.Object;
(new int[3][4][5][6][7][8][9]).getClass().getName():<<[I
getSimpleName():产生不包括包名的类名。
getInterfaces():返回所有实现的接口的Class对象数组。
getSuperclass():返回直接基类。如果此 Class 表示 Object 类、接口、基本类型或 void,则返回 null。如果此对象表示一个数组类,则返回表示该 Object 类的 Class 对象。
newInstance():创建此Class对象所表示的类的一个新实例。如同用一个带有一个空参数列表的 new表达式实例化该类。使用该方法创建对象实例时,必须要有默认构造函数。
获取某个类的Class对象:
l Class.forName(String className)。
l 通过对象的getClass()方法。
l 直接通过类的静态属性class,如Test.class,这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且不需要调用forName()方法,所以更高效。
另外,对于基本数据类型的包装器类,还有一个标准字段TYPE,该字段是一个引用,指向对象的基本数据类型的Class对象。但还是建议使用“.class”的形式,以保持与普通类型的一致性。
>>>Class.forName、Object.class、classLoader.loadClass异同<<<
Class.forName与“.class”区别在于:前者会初始化Class对象(如静态数据成员的初始化与静态块的执行),而后者不会。“.class”只是去加载类,不会链接(即不会给静态域分配空间);ClassLoader类的loadClass()方法只是加载一个类,并不会分配内存空间,更不会导致类的初始化:
public class ClassLoadTest {
// 测试时请调整虚拟机的堆大小:-Xms32M -Xmx1024M
public static void main(String[] args) {
// 编译时直接将2替换,不加导致类的加载与初始化
System.out.println("Bean.y=" + Bean.y);
sleep("->调用类的静态字面常量不会导致类的加载");
/*
* 不会初始化静态块与静态成员,只是加载到内存,也没进行
* 链接操作(静态成员分配空间),更没有初始化类(静态成
* 员初始化与静态块的执行)
*/
Class cl = Bean.class;
sleep("->Bean.class只会加载类,不会分配内存空间与初始化类");
ClassLoader cld = ClassLoader.getSystemClassLoader();
try {
// 也只是加载到内存,没有进行空间的分配与初始化类
cld.loadClass("Bean");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
sleep("->ClassLoader的loadClass()方法也只会加载类,不会" +
"分配内存空间与初始化类");
try {
// 加载与初始化类,并可看到内存猛增
cl = Class.forName("Bean");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
sleep("->Class.forName()会加载类,且分配内存空间与初始化类");
}
private static void sleep(String msg) {
System.out.println(msg);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Bean {
static {
System.out.println("static block");
}
public static final int y = 2;
public static final int i = f(1);
public static int j = f(2);
// 测试 A.class是否进行了链接操作
public static long[] l = h(21474836);
static {
System.out.println("i=" + i);
System.out.println("j=" + j);
}
static int f(int i) {
System.out.println("static f(" + i + ")");
return i;
}
static long[] h(int len) {
long[] l = new long[len];
for (int i = 0; i < len; i++) {
l[i] = i;
}
return l;
}
}
使用一个类前需做的三个准备步骤:
1、加载:这是由类加载器执行的。该步骤将查找字节码文件并读取到内存,并从这些字节码中创建一个Class对象。
2、链接:验证被加载类的正确性(验证)、为类的静态变量分配内存空间,并将其初始化为默认值(准备)、把类中的符号引用转换为直接引用(解析),如将方法的调用解析成直接方法在方法区的内存地址调用。
3、类的初始化(初始化Class对象):初始化静态成员和执行静态初始化块。
调用类一个 static final(编译期常量,注一定要是在定义时就初始化了,否则还是会初始化静态块与成员的)成员时,类的Calss对象不会执行初始化操作,也就是说“编译期常量”在类Class对象还没有初始化就可以引用了。
如果一个static域不是final的,那么在对它访问时,总是要求在它被读取前,要先对类进行链接(为这个域分配存储空间)和初始化(初始化该存储空间)操作。
类的初始化时机发生在类首次主动使用时,类主动使用发生在以下时机:
1、 创建类的实例。创建实例途经:使用new(构造器隐式也是静态的)、反射、克隆、反序列化
2、 调用类的静态方法。
3、 访问某个类或接口的静态变量(注意,不能是静态的字面常量域,静态的字面常量在编译时就确定,使用之前不会先加载类,更不会初始化类)。
4、 调用Class.forName()加载类。
5、 初始化一个类的子类,会导致父类初始化。
6、 Java虚拟机启动时被标明为启动类的类,例如: “java Test”命令,Test类就是启动类,Java虚拟机会先初始化它。
Java程序对类的使用方式可分为两种:主动使用与被动使用。
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
一个奇怪的初始化问题:
publicclass Singleton {
/*
* 这里如果为非静态时,会因递归构造而堆栈溢出:
* private Singleton s = new Singleton();
* 因为构造器在调用前需要先初始化所有的非静态成员,
* 而静态成员则不会在此时再被初始化,因为静态成员
* 是在类加载时就已初始了。
*
* 成员field1未初始化,field2需初始化。
*
* 初始化的动作是依照编写的顺序执行的,由于s先于
* field2的初始化,所以在构造函数调用完后,虽然
* field2为1了,但又会被紧拉着的field2初始化给覆
* 盖。如果将s的构造放在field2之后又会正常
*/
privatestatic Singleton s = new Singleton();
publicstaticintfield1;// 未初始化
publicstaticintfield2 = eval();// 初始化
private Singleton() {
field1++;
field2++;
System.out.println("Sigleton field2=" + field2);
}
privatestaticint eval() {
System.out.println("eval()");
return 0;
}
publicstatic Singleton getInStance() {
returns;
}
publicstaticvoid main(String[] args) {
Singleton.getInStance();
System.out.println("Main field1=" + Singleton.field1);// 1
System.out.println("Main field2=" + Singleton.field2);// 0
}
}
当Java虚拟机初始化一个类时,要求它的所有父类都像自己那样被初始,但是这条规则并不适用接口:
1、 在初始化一个类时,并不会先初始化它所实现的接口。
2、 在初始化一个接口时,并不会先初始化它的父接口。
因此,一个父接口并不会因为它的子接口或都实现类的初始化而初始化。只有当程序首次使用真真属于他们的特定接口的静态变量时,才会导致该接口的初始化,或者换句话来说就是只有当程序访问的静态变量或静态方法的确在当前类或接口中定义时,才会引起类的加载与初始化:
class Rd {
public static int getNumber(String msg) {
System.out.println(msg);
return new Random().nextInt(100);
}
}
interface I1 {
//注,getNumber不能抛出检测异常,因为不能捕获
public final int j = Rd.getNumber("init j");
}
interface I2 extends I1 {
public final int i = Rd.getNumber("init i");
public final int y = Rd.getNumber("init y");
public final int x = 1*2;//编译时就已计算出结果
}
class Imp implements I2 {
public static void main(String[] args) {
//创建子类实现类时不会去初始化父接口
new Imp();
System.out.println("------");
//由于x为静态的字面常量,所以不会引起类加载
System.out.println(I2.x);
System.out.println("------");
//子接口的初始化不会引起父接口的初始化
System.out.println(I2.i);
System.out.println("------");
//只有在使用到接口中的静态域时再初始化接口
System.out.println(I2.j);
}
}
只有当程序访问的静态变量或静态方法的确在当前类或接口中定义时,才可看做是对类或接口的主动使用:
public class P {
static int i = prt();
static int prt(){
System.out.println("init i");
return 1;
}
}
class S extends P {
static {
System.out.println("init S");
}
}
class T {
public static void main(String[] args) {
//不会初始化S类
System.out.println(S.i);
}
}
对于final类型的静态变量,如果在编译时就能计算取变量的取值,那么这种变量被看做编译地时常量,即不需要等到运行时确定(如:private static final int i = 2,注:private static final int i = 2*2 也属于编译时常量,因为表达式是由常量组成,编译后会使用常量 4 替换表达式);但是,对于那些在编译时无法计算出的final类型的静态变量,则调用他们时需要先加载类并初始化类才能使用(如:private static final int i = new Random().nextInt())。另外,如果类A中引用了B类的static final int i = 2;成员,则在编译A类时,就会把2直接编译到A中,因此在运行时可以不需要B.class文件都可运行。所以引用一个类的静态字面常量的成员时,该类不会被加载。
>>>类加载器<<<
类表示被执行的代码,而数据则表示与代码相关联的状态信息。状态信息可以改变,而代码则一般不会变更。一个类的代码通常都保存在一个.class为后缀的文件中。
在Java中,一个类的固定标识为其完整的具限类名称。该具限类名由该类的包名加上类名组成。但是在JVM中,唯一标识一个类的方式为:其具限类名与装载该类的装载器ClassLoader实例的组合。因此,如果一个包名为Pg,类名为C1的类,被类装载器KlassLoader的实例kl1装载,该类的实例C1(即C1.class)在JVM中的索引值将为(C1, Pg, kl1)。这意味着如果两个类装载器实例,装载了两个完全相同的类,则这两个类在虚拟机中的表示(C1, Pg, kl1)和(C1, Pg, kl2)将完全不一样,并且他们的对象实例也将完全不同,相互之间再也不能类型兼容了。
除了引导类装载器以外,所有的类装载器均有一个父类装载器。此外,所有的类装载器均为类型java.lang.ClassLoader的子类。
ClassLoader的loadClass(String name)实现如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protectedsynchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//如果还有父加载,则递归由父加载器去加载
c = parent.loadClass(name, false);
} else {
/*
* 通过上面递归的找父加载器,最终会因为父加器为null,
* 即直到父加载器为根(Bootstrap)类加载器时,才结束
* 递归,并开始从根类加载器开始加载指定的类。又由于根
* 类加载只能加载核心库,所以肯定会失败,失败后会调用
* 异常块中的 findClass(name)方法,这个方法默认是抛出
* ClassNotFoundException异常,这会导致返回上层调用
* 的 c 为null,即加载类失败,这样会再次由上层调用者,
* 即子加载器去加载,如果子加载器还是失败,则再让子子
* 加载去加载,直接自己实现的类加载器去加载,此时就需
* 要我们去实现 ClassLoader 的 findClass
*/
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
//外界调用loadClass(String name)方法时,实际上是执
//行loadClass(name, false),所以这里不会执行
if (resolve) {
resolveClass(c);
}
return c;
}
当要加载一个类时,调用的是ClassLoader的loadClass方法,loadClass方法先查找这个类是否己被加载,如果没有加载则委托其父级类装载器去加载这个类,如果父级的类装载器无法装载这个类,子级类装载器才调用自己内部的findClass方法去进行真正的加载。父级类装载器调用loadClass方法去装载一个类时,它也是先查找其父级类装载器,这徉一直追溯到没有父级的类装载器时(例如ExtClassLoader),则使用Java虚拟机内嵌的Bootstrap类装载器进行装载,当Bootstrap无法加载当前所要加载的类时,然后才一级级回退到子孙类装载器去进行真正的加载。当回退到最初的类装载器时,如果它自己也不能完成类的装载,那就应报告ClassNotFoundException异常。
一个类装载器只能创建某个类的一份字节码数据,即只能为某个类创建一个与之对应的Class实例对象,而不能为同样的一个类创建多个Class实例对象。在一个Java虚拟机中可以存在多个类装载器,每个类装载器都拥有自己的名称空间,对于同一个类每个类装载器都可以创建出它的一个Class实例对象,即每个类装载器都可以分别创建出某个类的一份字节码数据。两个类装载器分别创建的同一个类的字节码数据属于两个完全不同的对象,相互之间没有任何关联,例如,在某个类中定义了一个静态成员变量,它在不同的类装载器之间是不可以实现教据共享的。采用委托模式给类的加载管理带来了明显的好处,当父级的类装载器加载了某个类,那么子级的类装载器就不要再去加载这个类,这样就可以避免一个Java虚拟机中的多个类装载器为同一个类创建多份字节码数据的情况。
如果在类A中使用出new关键字创建类B, Java虚拟机将使用加载类A的类装载器来加载类B。如果在一个类中调用Class.forName方法来动态加载另外一个类,可以通过传递给Class.forName(String name, boolean initialize, ClassLoader loader)方法的一个参数来指定另外那个类的类装载器,如果没有指定该参数,则使用加载当前类的类装载器来加载。
每个运行中的线程都有一个关联的上下文装载器,可以使用Thread.setContextCIassLoader()方法设置线程的上下文类装载器。每个线程默认的上下文类装载器是其父线程的上下文类装载器,而主线程的类装载器初始被设置为 ClassLoader.getSystemC1assLoader()方法返回的系统类装载器。当线程中运行的代码需要使用某个类时,它使用上下文类装载器来装载这个类,上下文类装载器首先会委托它的父级类装载器来装载这个类,如果父级的类装载器无法装载时,上下文类装载器才自己进行装载。
Java虚拟机自带了以下几种加载器:
1. 根(Bootstrap)类加载器:该加载器没有父加载器。它负责加载虚拟机核心类库,如java.lang.*等,这些核心的运行期Java类位于<JAVA_HOME>\jre\lib\rt.jar文件中。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类。
2. 扩展(Extension)类加载器(实现为:sun.misc.Launcher$ExtClassLoader):它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的<JAVA_HOME>\jre\lib\ext子目(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类。
3. 系统(System)类加载器(实现为:sun.misc.Launcher$AppClassLoader,由ClassLoader.getSystemClassLoader()来获取,并且内存中只有一个):也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯Java类,是java.lang.ClassLoader类的子类。
4. 自定义类加载器:Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器应该继承ClassLoader类。
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader cl, cl1;
// 获取系统类加载器
cl = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + getClassName(cl));
// 打印父加载器
while (cl != null) {
cl1 = cl;
cl = cl.getParent();
System.out.println(getClassName(cl1) + " 的父加载器:" + getClassName(cl) + ",且由 "
+ cl1.getClass().getClassLoader()+ " 加载。");
}
// Object类的加载器
System.out.println("Object类的加载器:" + Object.class.getClassLoader());
// 用户定义应用类的加载器
System.out.println("应用程序的类加载器:"
+ getClassName(ClassLoaderTest.class.getClassLoader()));
}
private static String getClassName(Object o) {
return o == null ? "null" : o.getClass().getSimpleName();
}
/*
系统类加载器:AppClassLoader
AppClassLoader 的父加载器:ExtClassLoader,且由 null 加载。
ExtClassLoader 的父加载器:null,且由 null 加载。
Object类的加载器:null
应用程序的类加载器:AppClassLoader
*/
}
从上面的打印可看出:
l 系统类加载器为AppClassLoader类的实例。
l 系统类加载的父加载器为扩展类加载器,即ExtClassLoader类的实例。
l 扩展类加载器的父加载器为根类加载。但是,VM并不会向Java程序提供根类加载器的引用,而是返回Null,这是为了VM的安全。
l Object类是由根类加载器加载的。
l 用户自定义类是由系统类加载器加载的。
l 系统类加载器、扩展类加载器由根类加载器来加载。
当通过某类加载器加载类时,首先从自己的命名空间查找是否已经加载,如果已加载,则直接返回已加载的Class对象引用。如果没有加载,则请求父类去加载,父类再去请求父类的父类去加载,加载请求就这样一层层向上传,直到根加载器,如果根加载器加载不成功,则将请求往回传,直到有一个加载器能加载为止,再将加载的Class对象返回给最开始发起加载动作的类加载器。
加载器之间的父子关系实际上指的是加载器对象之间的关系,而不是类之间的继承关系。
可能通过ClassLoader的构造函数指定父加载器(ClassLoader(ClassLoader parent)),如果构造时没有指定,则使用系统类加载器作为父加载器,如果设置成null,则父加载器为根加载器。
父亲委托机制的优点是能够提高软件系统的安全性。因数在此机制下,用户自定的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。如java.lang.String类总是由根类加载器加载,其他任何用户自定义加载器都不可能加载含有恶意代码的自定义的java.lang.String类,因为加载时先会去看上层父加载器是否加载,由于java.lang.String已被根类加载器加载过了,所以不会再加载我们自已定义的java.lang.String类。
命名空间:每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。但在不同的命名空间是可以的。
运行时包:由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行包的类才能互相访问包可见(即默认访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充类库的类,去访问核心类库的包可见成员。假设用户自定义了一个类java.lang.XXX,并由用户自定义的类加载器加载,由于java.lang.XX和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行时包,所以java.lang.XX不能访问核心类库java.lang包中的包可见成员,所以即使我们把类所在包定义成与核心类库的包一样,如将自定义的类放在java.lang包中,但由于加载是我们自定义的类是由系统类加载器或是自己定义类加载加载的,所以运行时所在包是不一样的,所以即使我们冒充在同一包中,但还是不能访问那些具有包访问权限的类及成员。
若有一个类加载器能成功加载Sample类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象的引用的类加载(包括定义类加载器)都被称为初始类加载器。
>>>创建用户自定义的类加载器<<<
要创建用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,该方法根据参数指定的类的名字,返回对应的Class对象的引用。
下面是自定义的MyClassLoader加载器:
public class MyClassLoader extends ClassLoader {
private String classLoaderName;// 自定义类加载器的名字
private String loadPath;// 类加载的加载路径
public MyClassLoader(String classLoaderName) {
super();// 使用系统类加载器作为父加载器
this.classLoaderName = classLoaderName;
}
public MyClassLoader(ClassLoader parentLoader, String classLoaderName) {
super(parentLoader);// 指定parentLoader为父类加载器
this.classLoaderName = classLoaderName;
}
public String toString() {
return classLoaderName;
}
public void setPath(String path) {
this.loadPath = path;
}
@Override
protected Class findClass(String className) throws ClassNotFoundException {
FileInputStream fis = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try {
fis = new FileInputStream(new File(loadPath
+ className.replaceAll("\\.", "\\\\") + ".class"));
baos = new ByteArrayOutputStream();
int tmpByte = 0;
while ((tmpByte = fis.read()) != -1) {
baos.write(tmpByte);
}
data = baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("class is not found:" + className,
e);
} finally {
try {
if(fis != null){
fis.close();
}
if(fis != null){
baos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return defineClass(className, data, 0, data.length);
}
public static void main(String[] args) throws Exception {
// loader1的父加载器为系统类加载器
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("d:/myapp/serverlib/");
// loader2的父加载器为loader1
MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("d:/myapp/clientlib/");
// loader3的父加载器根加载器
MyClassLoader loader3 = new MyClassLoader(null, "loader3");
loader3.setPath("d:/myapp/otherlib/");
test(loader2);// 使用loader2测试
System.out.println("-------------------");
test(loader3);// 使用loader3测试
}
public static void test(ClassLoader loader) throws Exception {
// 注,loadClass的参数为类的完整名,即包括包名
Class objClass = loader.loadClass("pkg.Sample");
Object obj = objClass.newInstance();
System.out.println("obj=" + obj);
}
}
package pkg;
public class Sample {
public int v1 = 1;
public Sample() {
// 我们可以通过class对象的getClassLoader方法获取当
// 前class对象类加载器
System.out.println("Sample loaded by "
+ this.getClass().getClassLoader());
new Dog();// 引用Dog,会导致Dog类的加载动作
}
}
class Dog {
public Dog() {
System.out.println("Dog loaded by " +
this.getClass().getClassLoader());
}
}
开始编译:
D:\myapp\syslib>javac -d . MyClassLoader.java
D:\myapp\syslib>javac -d . pkg/Sample.java
编译后整个系统的目录结构如下:
D:\MYAPP
├─clientlib
├─otherlib
├─serverlib
└─syslib
│ MyClassLoader.java
│ MyClassLoader.class
└─pkg
Sample.java
Sample.class
Dog.class
接下来通过改变Sample类和Dog类的存放路径,或者修改源程序,来演示类加载器的各种特性:
1) 构造以下目录结构:
D:\MYAPP
├─clientlib
├─otherlib
│ └─pkg
│ Sample.class
│ Dog.class
├─serverlib
│ └─pkg
│ Sample.class
│ Dog.class
└─syslib
│ MyClassLoader.java
│ MyClassLoader.class
└─pkg
Sample.java
Sample.class
Dog.class
D:\myapp>java -classpath ./syslib MyClassLoader
Sample loaded by sun.misc.Launcher$AppClassLoader@82ba41
Dog loaded by sun.misc.Launcher$AppClassLoader@82ba41
obj=pkg.Sample@1a46e30
-------------------
Sample loaded by loader3
Dog loaded by loader3
obj=pkg.Sample@addbf1
由于父类加载的委托机制,加载一个类时,会先从根加载器开始加载,如果根加载器加载不成功或拒绝加载,则由扩展器来加载,同样不成功或拒绝加载则由系统加载器来加载,如果还不成功则由自定义的类加载器来加载。loder2的父加载器结构为 loder2àloder1à AppClassLoaderà ExtClassLoaderà Bootstrap,由于我们自己定义的类默认是由系统类加载器AppClassLoader来加载的,系统类加载器会加载环境变量classpath的类,而运行时设置的classpath 为./syslib,且在该路径下有Sample.class与Dog.class,所以loader2.loadClass("pkg.Sample")加载时使用系统类加载器从classpath路径中来加载Sample.class,又由于Sample.class的构造函数中引用Dog.class,所以先默认采用同样的类加载器来加载Dog.class,如果系统类加载器加载Dog.class失败时,将会怎么样,请继承往下看。
loader3.loadClass("pkg.Sample")就更简单了,因为loader3的父加载器结构为loder3à Bootstrap,又根加载器不能加载用户自定义类,所以只能由loder3从D:\myapp\otherlib路径中加载了。
从这个例子还可以看出,在loader1和loader3各自的命名空间中,都在Sample类和Dog类,也就是说,在VM中有两个Sample类Class对象和两个Dog类的Class对象。
2) 构造以下目录结构:
D:\MYAPP
├─clientlib
├─otherlib
│ └─pkg
│ Sample.class
│ Dog.class
├─serverlib
│ └─pkg
│ Sample.class
│ Dog.class
└─syslib
│ MyClassLoader.java
│ MyClassLoader.class
└─pkg
Sample.java
D:\myapp>java -classpath ./syslib MyClassLoader
Sample loaded by loader1
Dog loaded by loader1
obj=pkg.Sample@3e25a5
-------------------
Sample loaded by loader3
Dog loaded by loader3
obj=pkg.Sample@42e816
由于系统类加载器不能加载Sample.class,所以由loader1来尝试,且加载成功。
3) 构造以下目录结构:
D:\MYAPP
├─clientlib
│ └─pkg
│ Sample.class
│ Dog.class
├─otherlib
│ └─pkg
│ Sample.class
│ Dog.class
├─serverlib
└─syslib
│ MyClassLoader.java
│ MyClassLoader.class
└─pkg
Sample.java
D:\myapp>java -classpath ./syslib MyClassLoader
Sample loaded by loader2
Dog loaded by loader2
obj=pkg.Sample@3e25a5
-------------------
Sample loaded by loader3
Dog loaded by loader3
obj=pkg.Sample@42e816
4) 构造以下目录结构:
D:\MYAPP
├─clientlib
├─otherlib
│ └─pkg
│ Sample.class
│ Dog.class
├─serverlib
│ └─pkg
│ Sample.class
└─syslib
│ MyClassLoader.java
│ MyClassLoader.class
└─pkg
Sample.java
Dog.class
D:\myapp>java -classpath ./syslib MyClassLoader
Sample loaded by loader1
Exception in thread "main" java.lang.IllegalAccessError: tried to access class pkg.Dog from class pkg.Sample
。。。
虽然Sample.class由loader1来加载,而Dog.class由系统类加载器来加载的,而系统类加载器又是loader1的父加载器,根据后面的规则:“子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。”,似乎可以正常运行,但这只是说能看到这个类,并不代表你能够访问到这个类(如果是包访问权限的话,而这里的Dog类恰好就是包访问权限的,所以你不能访问这个类)及这个类里的包访问权限的成员及方法;再根据规则“由同一类加载器加载的属于相同包的类组成了运行时包,只有属于同一运行包的类才能互相访问包可见(即默认访问级别)的类和类成员”,由于Sample与Dog由不同的类加载器来加载的,他们不属于同一个运行时包,所以就出现了上面运行时错误。但如果将Dog类访问权限修改成public时,则可以访问,请继续往下看。
从上面我们要注意,不是只要两个类在同一个包中就可以相互访问包访问权限的类及类的成员,还要看他们是否是由同一加载器来加载的,即属于同一运行时包的类才真正属于同一包。
5) 构造以下目录结构(将Dog类单独写成一个类,并将class定义成public):
D:\MYAPP
├─clientlib
├─otherlib
│ └─pkg
│ Sample.class
│ Dog.class
├─serverlib
│ └─pkg
│ Sample.class
└─syslib
│ MyClassLoader.java
│ MyClassLoader.class
└─pkg
Sample.java
Dog.java
Dog.class
D:\myapp>java -classpath ./syslib MyClassLoader
Sample loaded by loader1
Dog loaded by sun.misc.Launcher$AppClassLoader@82ba41
obj=pkg.Sample@3e25a5
-------------------
Sample loaded by loader3
Dog loaded by loader3
obj=pkg.Sample@42e816
由于子加载器加载的类(Sample)能看见父加载器加载的类(Dog),所以以上运行正常。
6) 构造以下目录结构:
D:\MYAPP
├─clientlib
├─otherlib
│ └─pkg
│ Sample.class
│ Dog.class
├─serverlib
│ └─pkg
│ Dog.class
└─syslib
│ MyClassLoader.java
│ MyClassLoader.class
└─pkg
Sample.java
Dog.java
Sample.class
D:\myapp>java -classpath ./syslib MyClassLoader
Sample loaded by sun.misc.Launcher$AppClassLoader@82ba41
Exception in thread "main" java.lang.NoClassDefFoundError: pkg/Dog
。。。
由于父加载器加载的类(Sample)不能看见子加载器加载的类(Dog),所以运行错误。
7) 不同类加载器的命名空间存在以下关系:
l 同一个命名空间内的类是相互可见的。
l 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
l 由父加载器加载的类不能看见子加载器加载的类。
l 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
所谓类A能看见类B,就是指在类A的程序代码中可以引用类B的名字,例如:
Class A{ B b = new B();}
下面把Sample.class和Dog.class仅仅拷贝到D:\myapp\serverlib目录下,然后把MyClassLoader类的main()方法修改为下面代码:
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("d:/myapp/serverlib/");
Class objClass = loader1.loadClass("pkg.Sample");
Object obj = objClass.newInstance();
Sample sample = (Sample)obj;//抛出NoClassDefFoundError错误
System.out.println(sample.v1);
此时的目录结构如下:
D:\MYAPP
├─clientlib
├─otherlib
├─serverlib
│ └─pkg
│ Dog.class
│ Sample.class
└─syslib
│ MyClassLoader.java
│ MyClassLoader.class
└─pkg
Sample.java
Dog.java
D:\myapp>java -classpath ./syslib MyClassLoader
Sample loaded by loader1
Dog loaded by loader1
Exception in thread "main" java.lang.NoClassDefFoundError: pkg/Sample
at MyClassLoader.main(MyClassLoader.java:70)
由于MyClassLoader类由系统类加载器加载,而Sample类由loader1类加载,因此MyClassLoader类看不见Sample类(根据规则“由父加载器加载的类不能看见子加载器加载的类”)。在MyClassLoader类的main()方法中使用Sample类,会导致NoClassDefFoundError错误。但把Sample类与Dog类的类文件拷贝到D:\myapp\syslib下时,却又能正常,因为此时他们都是由系统类加载器加载,MyClassLoader与Sample、Dog属于同一命名空间中的类,所以MyClassLoader能正常访问Sample类。
当两个不同命名空间内的类相互不可见时,可采用反射机制来访问对方实例的属性和方法,如果把MyClassLoader类的main()方法替换为如下代码:
MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("d:/myapp/serverlib/");
Class objClass = loader1.loadClass("pkg.Sample");
Object obj = objClass.newInstance();
Field f = objClass.getField("v1");
int v1 = f.getInt(obj);
System.out.println(v1);
D:\myapp>java -classpath ./syslib MyClassLoader
Sample loaded by loader1
Dog loaded by loader1
v1=1
>>>使用URLClassLoader类<<<
URLClassLoader为在,它扩展了ClassLoader类,它不仅能从本地文件系统中加载类,还可以从网上下载类。下面程序演示了从jar文件中加载Sample类:
URLClassLoader urlLoader = new URLClassLoader(new URL[] { new URL(
"file:d:/sample.jar") });
Class c = urlLoader.loadClass("pkg.Sample");
System.out.println(c.newInstance());
输出:
Sample loaded by java.net.URLClassLoader@757aef
Dog loaded by java.net.URLClassLoader@757aef
pkg.Sample@19821f
>>>类的卸载<<<
当Sample类被加载、连接和初始化后,它的生命周期就开始了。当代表Sample类的Class对象不再被引用,即不可达时,Class对象生命就会结束,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
由VM自带的类加载器所加载的类,在VM的生命周期中,始终不会被卸载(如Object类)。VM自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。VM本身会始终引用这此类加载器,而这些类加载器则会始终引用它们所加的类的Class对象,因此这此Class对象始终是可达的,所以说由VM自带的类加载器所加载的类始终不会被卸载。
由用户自定义的类加载器所加载的类是可以被卸载的。
下面以MyClassLoader类为例,介绍Sample类被卸载时机。把Sample.class和Dog.class拷贝到D:\myapp\serverlib目录下,然后把MyClassLoader类的main()方法替换为:
MyClassLoader loader1 = new MyClassLoader("loader1");// 1
loader1.setPath("d:/myapp/serverlib/");// 2
Class objClass = loader1.loadClass("pkg.Sample");// 3
System.out.println("objClass hashCode:" + objClass.hashCode());// 4
Object obj = objClass.newInstance();// 5
loader1 = null;// 6
objClass = null;// 7
obj = null; // 8
loader1 = new MyClassLoader("loader1");// 9
loader1.setPath("d:/myapp/serverlib/");//10
objClass = loader1.loadClass("pkg.Sample");// 11
System.out.println("objClass hashCode:" + objClass.hashCode());
运行以上程序时,Sample类由loader1加载。在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表Sample类的Class实例与loader1之间为双向关系。
一个类实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性Class,它引用代表这个类的Class对象。
当程序执行完第5步时,引用变量与对象之间的引用关系如图:
从图可以看出,loader1变量和obj变量间接引用代表Sample类的Class对象,而objClass变量则直接引用它。
当程序执行完第8步,所有的引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载(注,但这里并不代表立即被回收了)。
当程序执行完第11步时,Sample类又重新被加载,在堆区会生成一个新的代表Sample类的Class实例:
以上程序输出结果如下:
objClass hashCode:10267414
Sample loaded by loader1
Dog loaded by loader1
objClass hashCode:11394033
注,运行之前一定要先删除syslib下面的Sample.class与Dog.class类文件,否则上面的程序会由系统类加载器去加载Sample类,此时Sample类在执行第8行后Sample的Class对象也不会卸载,因为该Class对象是由系统类加载器加的。如果不删除syslib下的Sample.class与Dog.class类文件,则输出结果为:
objClass hashCode:14285251
Sample loaded by sun.misc.Launcher$AppClassLoader@82ba41
Dog loaded by sun.misc.Launcher$AppClassLoader@82ba41
objClass hashCode:14285251
Class中的getResourceAsStream
===============Class中的getResourceAsStream===================
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();//获取类加载
if (cl==null) {//如果类加载器为null,则为根类加载器
// A system class. 而不是应用类时,使用根类加载器来加载资源
return ClassLoader.getSystemResourceAsStream(name);
}
//通过调用ClassLoader的getResourceAsStream在类加载器搜索路径中加载指定的路径资源
return cl.getResourceAsStream(name);
}
/*
* 从实现可以看出,传进来的资源路径 name 有两种形式: 一是以 / 开头的路径,
* 它是相对于 <CLASSPATH> 路径的文件路径 二是不以 / 开头的路径,它是相对
* 于当前类所在包的路径。
*/
private String resolveName(String name) {
if (name == null) {
return name;
}
/*
* 如果传进来的路径不是以 / 开头,则name表示只能是相对于 包路径的文件
* 名路径,如某个类的完整类名为 pak1.pak2.ClassXXX 传递进来的 name
* 为 dir/filename.txt,则最后返回的路径为 pak1/pak2/dir/filename.txt,
* 它表示在<CLASSPATH>/pak1/pak2/dir 上当下有 filename.txt 文件
*
* 如果name是以 / 开头,则会去掉 开头的 / 后返回。要注意的是, 此时的路
* 径开头的 / 表示的是 <CLASSPATH>,如果此时某个文件是在 某个包目录下,
* 则 name 一定要带上包路径
*/
if (!name.startsWith("/")) {
Class c = this.getClass();
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) {
name = baseName.substring(0, index).replace('.', '/') + "/"
+ name;
}
} else {
name = name.substring(1);
}
return name;
}
===============ClassLoader的getResourceAsStream===================
public InputStream getResourceAsStream(String name) {
// 获取资源的URL
URL url = getResource(name);
try {
// 打开资源流
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
//获取资源的URL对象
public URL getResource(String name) {
URL url;
//如果类加载器还有父加载器,则由父加载器加载
if (parent != null) {
url = parent.getResource(name);
} else {//一直递归到根加载器。按理说根加载器的搜索路径为<JAVA_HOME>/jre/lib/rt.jar,所以url会返回null?????
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);// ClassLoader中此方法的实现返回 null
}
return url;
}
上面是ClassLoader类的实现,在不同的类加载器中资源的加载方式实现是不同的,比如ExtClassLoader、AppClassLoader实现就可能不一样。
比如在pak1.pak2包下有xx.txt文件,下面的三个语句是等效的:
// 相对于当前类所在的包路径 pak1/pak2
pak1.pak2.Resource.class.getResourceAsStream("xx.txt");
// /pak1/pak2/xx.txt 相对于<JAVA_HOME>
pak1.pak2.Resource.class.getResourceAsStream("/pak1/pak2/xx.txt");
/*
* Class的getResourceAsStream就是调用ClassLoader.getResourceAsStream来实现的,
* 而Class的getResourceAsStream再调用ClassLoader.getResourceAsStream之前,
* 就已经将路径开头的 / 去掉了,所以如果是直接调用ClassLoader.getResourceAsStream
* 时,一定不能以/开头
*/
pak1.pak2.Resource.class.getClassLoader().getResourceAsStream("pak1/pak2/xx.txt");
通过Class类的getResourceAsStream获取 jar 包里的资源
可以通过Class类的getResourceAsStream来获取 jar 包里的资源,比如jar包里含有一个 /resource/res.txt 文件,并与Resource 类的class打一个 jar,结构如下:
1、src/
src/pak1/pak2/Resource.java
2、bin/
bin/resource/res.txt
bin/pak1/pak2/Resource.class
可以通过下面的方法来获取jar包里的资源
public class Resource {
public void getResource() throws IOException{
//在<CLASSPATH>路径中搜索资源,这里的classpath为jar包所在的路径
InputStream is=this.getClass().getResourceAsStream("/resource/res.txt");
BufferedReader br=new BufferedReader(new InputStreamReader(is));
String s="";
while((s=br.readLine())!=null)
System.out.println(s);
}
}
上面是资源文件与访问的它的类在同一jar包中,如果它们不在同一jar包中,可以这样访问:
public class Resource {
public void getResource() throws IOException, Exception {
URLClassLoader urlLoader = new URLClassLoader(new URL[] { new URL(
"file:d:/sample.jar") });
// Class c = urlLoader.loadClass("pkg.Sample");
InputStream is = urlLoader.getResourceAsStream("/resource/res.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String s = "";
while ((s = br.readLine()) != null)
System.out.println(s);
}
}
通过JarURLConnection获取 jar 包里的资源
JAR URL 的语法为: jar:<url>!/{entry} 如,jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class
JarURLConnection 实例只能用于从 JAR 文件读取内容。
新的转型语法:
class Building {}
class House extends Building {}
public class ClassCasts {
public static void main(String[] args) {
Building b = new House();
Class<House> houseType = House.class;
House h = houseType.cast(b);// 将b向下转型,这样在Java SE5不会发生警告
h = (House)b; // 以前的强制转法,但这样在Java SE5会发生警告
// 所以为了在Java SE5强转时不发生警告,则请使用新的转型方式
}
}
动态的instanceof:Class的isInstance(Object obj)方法与instanceof完全等价,判定指定的Object是否与此 Class所表示的对象赋值兼容。此方法是 Java 语言 instanceof运算符的动态等效方法。如果指定的 Object参数非空,且能够在不引发 ClassCastException的情况下被强制转换成该 Class对象所表示的引用类型,则返回 true,否则返回 false。
class.isAssignableFrom(Class<?> cls):判定此class对象所表示的类或接口与指定的cls参数所表示的类或接口是否相同,或是class否是cls的超类或超接口。如果是则返回true,否则返回 false。
反射:运行时的类信息
Class类与java.lang.reflect类库一起对反射的进行了支持,该类库包含了Field、Method以及Constructor类(都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用来表示未知类里对应的成员。这样你就可以使用Constructor创建新的对象,用get()和set()方法读取和修改与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用Class对象的getFields()、getMethods()和getConstructors()等很便利的方法,以返回表示字段、方法以及构造器的对象的数组。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情,但是,这个类的.class文件对于JVM来说必须是可获取的,要么在本机上,要么可以通过网络取得。
RTTI与反射之间真正的区别只在于:对于RTTI来说,编译器在编译时打开与检查.class文件,换句话说,我们可以用“普通”方式调用对象的所有方法;而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开与检查.class文件。
通过反射可以访问类中的所有东西,包括private修饰的。
动态代理:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//实现调用处理器接口
class MethodSelectorHandler implements InvocationHandler {
private Object proxied;// 引用被代理的真真对象
public MethodSelectorHandler(Object proxied) {
this.proxied = proxied;
}
// 实现接口,动态代理可以将所有调用重定向到该方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 在这里做额外的工作
if (method.getName().equals("interesting")) {
System.out.println("Proxy detected the interesting method");
}
return method.invoke(proxied, args);
}
}
// 代理接口,Java中的动态代理对象一类要实现某个接口
interface SomeMethods {
void boring1();
void boring2();
void interesting(String arg);
void boring3();
}
// 真真被代理的类
class Implementation implements SomeMethods {
public void boring1() {
System.out.println("boring1");
}
public void boring2() {
System.out.println("boring2");
}
public void interesting(String arg) {
System.out.println("interesting " + arg);
}
public void boring3() {
System.out.println("boring3");
}
}
class SelectingMethods {
public static void main(String[] args) {
// 创建代理对象 第一个参数为类加载器;第二个为所实现的接口,可有多个;第
// 三个为处理器,构建处理器时需指定真真被代理的对象。返回的是代理对象
SomeMethods proxy = (SomeMethods) Proxy.newProxyInstance(
SomeMethods.class.getClassLoader(),
new Class[] { SomeMethods.class }, new MethodSelectorHandler(
new Implementation()));
// 通过代理对象调用
proxy.boring1();
proxy.boring2();
proxy.interesting("bonobo");
proxy.boring3();
}
}
如果使用反射创建一个对象时,而需要调用带参的构造函数,则可以使用Constructor的newInstance(Object[] initargs)方法来代替Class的newInstance()方法。
>>>反射工具<<<
//反射类信息
public class Reflection {
//打印构造器
public static void printConstructors(Class cl) {
/*
* 返回一个包含某些 Constructor 对象的数组,这些对象反映此 Class 对象所表
* 示的类的所有公共构造方法。
*/
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors) {
String name = c.getName();//构造方法的名称
// 输出构造器前所有修饰符
System.out.print(" " + Modifier.toString(c.getModifiers()));
System.out.print(" " + name + "(");
// 输出参数类型与名称
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0) {
System.out.print(", ");
}
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
//打印方法
public static void printMethods(Class cl) {
/*
* getDeclaredMethods():
* 返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明
* 的所有方法,包括公共、保护、包访问和私有方法,但不包括继承的方法。
*/
Method[] methods = cl.getDeclaredMethods();
//准备输出方法
for (Method m : methods) {
Class retType = m.getReturnType();//方法返回类型
String name = m.getName();//方法的名字
//输出方法的修饰符、返回类型以及方法名
System.out.print(" " + Modifier.toString(m.getModifiers()));
System.out.print(" " + retType.getName() + " " + name + "(");
//输出参数类型
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0) {
System.out.print(", ");
}
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
//打印字段
public static void printFields(Class cl) {
Field[] fields = cl.getDeclaredFields();
for (Field f : fields) {
Class type = f.getType();//字段类型
String name = f.getName();//字段名
System.out.print(" " + Modifier.toString(f.getModifiers()));
System.out.println(" " + type.getName() + " " + name + ";");
}
}
public static void main(String[] args) {
String name;
if (args.length > 0) {
name = args[0];
} else {
Scanner in = new Scanner(System.in);
System.out.println("Enter class name(e.g. java.util.Date)");
name = in.next();
}
try {
Class cl = Class.forName(name);
Class supercl = cl.getSuperclass();
//打印类的定义
System.out.print("class " + name);
if (supercl != null && supercl != Object.class) {
System.out.println(" extends " + supercl.getName());
}
System.out.println("\n{\n //构造器");
printConstructors(cl);//打印构造器
System.out.println(" //字段");
printFields(cl);//打印字段
System.out.println(" //方法");
printMethods(cl);//打印方法
System.out.println("}");
} catch (Exception e) {
e.printStackTrace();
}
}
}
应用反射机制打印对象成员值信息请看XXXXAbstractDTO
第十五章泛型
参见《XXXXXX》
类型推断:
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LimitsOfInference {
/*
* 外界创建一个Map对象时只需执行
* Map<String, List<String>> map = newMap();
*
* 类似的语句,而不必麻烦地使用
* Map<String, List<String>> map = new Map<String, List<String>>();
*
* 所以可以试着把这些创建集合的代码集中封装到一个公共类中,省去创建时指
* 定类型,它可根据赋值语句前部分声明来推导出类型参数
*/
static <K, V> Map<K, V> newMap() {
/*
* 编译时会根据赋值语句来推断 K,V 的参数类型。
*
* 所谓的方法类型推断是指:
* 方法是泛型的,但在执行过程中方法体中不知道确切的参数类型,即泛型
* 方法不带泛型参数,就像该方法是泛型方法但没有传递参数类型,但如果
* 该方法带类型参数时(如 newMap(K k,V v)),调用时就不存在类型推
* 断问题了,因为在调用时参数类型已经传进了,执行时就已确定了,则最
* 后泛型方法返回的结果类型就可以确定了。
*/
return new HashMap<K, V>();
}
static void f(Map<String, List<String>> map) {}
static Map<String, List<String>> h() {
//将泛型方法的结果作为某方法的返回值,此时也会发生类型推断
return newMap();
}
public static void main(String[] args) {
/*
* 类型推断发生在两个时机,第一个就是直接赋值语句中。
* 第二就是将泛型方法的结果作为某方法的返回值
*/
// 赋值语句能推断出newMap方法中的类型参数类型
Map<String, List<String>> map = newMap();
// 编译没问题,因为h()返回的类型是确定的
f(h());
//!! Does not compile,因为类型推断只发生在赋值语句与返回时
// f(newMap());
// 但可以显示的指定返回参数类型
f(LimitsOfInference.<String, List<String>>newMap());
}
}
泛型类型参数将擦除到它的第一个边界(因为可能会有多个边界),而普通的类型变量在未指定边界的情况下被擦除为Object。使用与不使用泛型生成的字节码是一样的。
推荐使用Array.newInstance()方式创建泛型数组:
T[] arrayMaker(Class<T> kind,int size) {
return (T[])Array.newInstance(kind, size);
}
但也可这样:
T[] arrayMaker(int size) {
return (T[])new Object[size];
}
这与上面相同的是最后创建出的数组类型表面上(返回给别人的)都是Object类型(因为擦除关系),但前者的真真数组类型还是由传递进来的Class类类型来决定。
边界:即对象进入和离开方法的地点,这些也是编译器在编译期执行类型检查并插入转型代码的地点。泛型中的所有动作都发生在边界处——对象传递进来的值进行额外的编译检查,并插入对传递出去的值的转型。
extends边界:因为擦除了类型信息,所以,能通过无界(Colored<T>)泛型参数调用的方法只是那些Object中的方法(因为未使用extends定界时上界默认就是Object)。但是,如果将这个参数T限制为某个类型的子类型,那么我们就可以用这些类型子类的相关方法,所以extends的作用在于定界,而定界又是为了调用某个泛型类的方法:
//边界接口
interface HasColor { java.awt.Color getColor(); }
class Colored<T extends HasColor> {
T item;
Colored(T item) { this.item = item; }
T getItem() { return item; }
// 允许调用边界接口的方法:
java.awt.Color color() { return item.getColor(); }
}
//边界类
class Dimension { public int x, y, z; }
// 当同时有边界类与接口时,接口放在类的后面,与继承一样:
//!! class ColoredDimension<T extends HasColor & Dimension> {}
// 多个边界时,边界接口要放在边界类后面:
class ColoredDimension<T extends Dimension & HasColor> {
T item;
ColoredDimension(T item) { this.item = item; }
T getItem() { return item; }
//访问边界接口方法
java.awt.Color color() { return item.getColor(); }
//访问边界类中的成员
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
//另一边界接口
interface Weight { int weight(); }
// 与继承一样,多个边界时,只允许一个边界类,但允许多个边界接口:
class Solid<T extends Dimension & HasColor & Weight> {
T item;
Solid(T item) { this.item = item; }
T getItem() { return item; }
//访问边界接口
java.awt.Color color() { return item.getColor(); }
//访问边界类中的成员
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
//访问边界接口
int weight() { return item.weight(); }
}
//Solid类中的类型参数实现类
class Bounded
extends Dimension implements HasColor, Weight {
public java.awt.Color getColor() { return null; }
public int weight() { return 0; }
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid =
new Solid<Bounded>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
}
边界与继承:可以在继承的每个层次上逐渐添加边界限制。使用继承的方式修改上面泛型类,这样不必在每个类中重复定义与实现某些方法:
class HoldItem<T> {
T item;
HoldItem(T item) { this.item = item; }
T getItem() { return item; }
}
//添加HasColor边界接口,此时的类型参数T要是实现了HasColor的类
class Colored2<T extends HasColor> extends HoldItem<T> {
Colored2(T item) { super(item); }
//访问边界接口中的方法
java.awt.Color color() { return item.getColor(); }
}
//添加Dimension边界类,此时的类型参数T要是继承了Dimension类并是
//实现HasColor接口的类
class ColoredDimension2<T extends Dimension & HasColor>
extends Colored2<T> {
ColoredDimension2(T item) { super(item); }
//访问新添加的边界类中的成员
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
//添加Weight边界类,此时的类型参数T要是继承了Dimension类并
//实现HasColor接口与Weight接口的类
class Solid2<T extends Dimension & HasColor & Weight>
extends ColoredDimension2<T> {
Solid2(T item) { super(item); }
//访问新添加的边界接口中的方法weight()
int weight() { return item.weight(); }
}
//继承边界类测试
public class InheritBounds {
public static void main(String[] args) {
//Bounded符合Solid2中类型参数T
Solid2<Bounded> solid2 =
new Solid2<Bounded>(new Bounded());
solid2.color();//访问父类Colored2中的方法
solid2.getY();//访问父类ColoredDimension2中的方法
solid2.weight();//访问自身的方法
}
}
通配符?的疑问:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CompilerIntelligence {
public static void main(String[] args) {
//? 通配符,flist指向存放Fruit及任何子类List容器
List<? extends Fruit> flist =Arrays.asList(new Apple());
//使用通配符定义的引用,不能通过该引用调用任何带有泛型参数的方法
//!! flist.add(new Apple());
//!! flist.add(new Fruit());
//但可调用返回参数类型是泛型的方法,尽管返回类型为泛型
//但不管返回的是什么,至少是Fruit类型,所以是合理的
Apple a = (Apple)flist.get(0); // No warning
//但可调用参数不是泛型参数的方法,通过查看源码,我们发现
//contains与indexOf的参数类型都是Object
flist.contains(new Jonathan());
flist.indexOf(new Fruit());
}
}
从上面程序可以知道,在使用通配符定义的引用后,为什么add方法不能使用,而contains与indexOf却可以。该限制不是用编译器去检查特定的方法是否修改了它的对象。其实编译器并没有这么聪明。add()接受的是一个具有泛型参数类型的参数,但是contains()和indexOf()接受的是一个Object类型的参数。因此当你定义一个ArrayList<? extends Fruit>时,add(E o)的参数 E 就变成了“? extends Fruit”,因此编译器并不知道这里需要Fruit的哪个具体子类型,所以它不能接受任何类型的Fruit,编译器直接拒绝对参数列表中涉及通配符的方法(例如add(E o))的调用。在使用contains(Object elem)和indexOf(Object elem)时,参数类型是Object,因此不涉及任何通配符,所以编译器将允许这样调用。因此,为了在类型中使用了通配符的情况下禁止这个类的调用,我们需要在参数列表中使用类型参数。
从下面程序也可看出这一点:
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
//参数不是泛型类型参数
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> apple = new Holder<Apple>(new Apple());
Apple d = apple.get();
apple.set(d);
//Holder<Apple>类型不是Holder<Fruit>的子类
//!! Holder<Fruit> Fruit = apple; // Cannot upcast
//但使用通配符是可以的
Holder<? extends Fruit> fruit = apple; // OK
//可以调用它的get方法,因为该方法不带参数,尽管返回类型为泛型
//因为不管返回的是什么,但至少是Fruit类型,所以是合理的
Fruit p = fruit.get();
//因为本身就是Apple类型,所以能安全强制向下转型
d = (Apple)fruit.get();
try {
//编译时不会警告,但运行时发生ClassCastException
Orange c = (Orange)fruit.get();
} catch(Exception e) { System.out.println(e); }
//因为fruit是通过通配符方式定义的,所以不能调用带类型参数的方法
//!! fruit.set(new Apple());
//!! fruit.set(new Fruit());
//但是equals方法参数是Object类型,所以可以调用
System.out.println(fruit.equals(d)); // OK
}
}
<T extends MyClass>是用来解决不能调用泛型类型参T及实例的方法的问题,即解决了类型边界问题。
<? extends MyClass>是用来解决 ArrayList<Fruit> list = new ArrayList< Apple>();的问题或者是方法参数的传递问题。
超类型通配符:通配符是由某个特定类的任何基类来界定,方法是指定<? super MyClass>,甚至使用类型参数:<? super T>,但你不能对泛型参数指定一个超类型边界(如<T super MyClass>,但<T extends MyClass>却是可以的)。只能用于方法的参数类型说明与变量的定义,不能用于类,也不可用来定义某个类型参数T,因为没有<T super MyClass>形式。
定义变量:List<? super Apple> l = new ArrayList<Fruit>();
方法的参数类型说明:Collections.static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
ArrayList<? extends Fruit>与ArrayList<? super Jonathan>的区别:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class OtherJonathan extends Jonathan {}
public class SuperTypeWildcards {
static void writeTo(List<? extends Apple> apples) {
/*
* ArrayList<? extends Fruit>表示它定义的list1引用可指向能存放
* Fruit及其子类的容器实例,但不能真真的向容器里放入任何东西除了
* null。
*
* 这里使用的是 extends ,所以new ArrayList<XXX>()中的XXX只能是
* Fruit 或其子类。
*/
ArrayList<? extends Fruit> list1 = new ArrayList<Apple>();
// !! list1.add(new Apple());
// !! list1.add(new Fruit());
/*
* ArrayList<? super Jonathan>表示它定义的list2引用可指向能存
* 放Jonathan及子类实例的容器,与上面不同的是可以向其中放入对象。
*
* 这里使用的是 super ,所以new ArrayList<XXX>()中的XXX只能是
* Jonathan 或其父类,因为只有这样才能确保创建出来的容器能存放
* Jonathan及子类实例
*/
ArrayList<? super Jonathan> list2 = new ArrayList<Fruit>();
// !! list2.add(new Fruit());//不能存入Fruit
// !! list2.add(new Apple());//不能存入Apple
list2.add(new Jonathan());
list2.add(new OtherJonathan());
// 类型参数中含有super关键字所定义的引用只能指向类型参数及父类的实例
// !! ArrayList<? super Apple> list13 = new ArrayList<Jonathan>();
}
}
再看另一个实例:
class GenericWriteReading {
//----------写入
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
/*
* 返回的参数类型为 t1与t2的公共父类,因为Object为任何类的父类,所
* 以t1与t2可以是任何类型参数都可以
*/
static <T> T writeExact1(T t1, T t2) {
return t1 == null ? t2 : t1;
}
static void write1() {
writeExact(new ArrayList<Apple>(), new Apple());
writeExact(new ArrayList<Fruit>(), new Fruit());
//书说这行不能编译通过,源码已被注释掉了,但运行了一下可以,why?
writeExact(new ArrayList<Fruit>(), new Apple());
/*
* 编译不能通过,上面行可以,编译器是如何做得到的?
*
* 经过自己推敲,上下两行不同的是,list的参数类型如果是后面item参数类
* 型的父类就可以,把List换成自己创建的类也是这样的,编译器也许就是根
* 据 List<XXX> 中的类型XXX 是否与item的类型相同或是父类来判断的。
*
* 另外,从writeExact1方法可知,如果类型参数不是作为其他类型的类型参数
* 使用(如writeExact1方法中的类型参数)时,这此参数的类型之间可以说没
* 有任何限制,可以传递任何类型的参数,因为Object为他们的公共父类。
*
* 但如果把类型参数作为某个类的类型参数使用时(如writeExact中T被应用到
* 了List<T>中),则参数间就会有直接关系了:两个T要么相同,要么List<T>
* 中的T类型是第二个T的父类(注,不能反过来),这其实与
* writeWithWildcard(List<? super T> list, T item)作用是一样的,在下
* 面我们将会看到。
* 在此种情况下,为什么参数类型要有直接的父子关系呢?其实也是有道理的,
* 因为第一个List<T>类型的参变量list对象的某些方法还有可能要使用到第二
* 个参变量item,只有在第二个参数的类型T是第一个参数类型T的子类或本身时
* ,才能将item传到需要它的相应方法中去。
*
*/
//!! writeExact(new ArrayList<Apple>(), new Fruit());
Apple app = writeExact1(new Apple(), new Apple());
/*
* 下面两行都可以,方法声明的是t1与t2的类型一样,但下面为父子关系也可
* 以,原因就是他们的父类为Object,所以可以两者任意交换
*/
Fruit fru = writeExact1(new Fruit(), new Apple());
fru = writeExact1(new Apple(), new Fruit());
//!! 返回类型只能是参数类型的公共父类,即Fruit
//!! app = writeExact1( new Apple(),new Fruit());
// ArrayList与String的公共类型有Serializable与Object,所以返回类型有两种
Serializable s = writeExact1(new ArrayList(), new String());
Object o = writeExact1(new ArrayList(), new String());
}
static <T> void writeWithWildcard(List<? super T> list, T item) {
list.add(item);
}
static void write2() {
writeWithWildcard(new ArrayList<Apple>(), new Apple());
writeWithWildcard(new ArrayList<Fruit>(), new Fruit());
// 父类类型的容器可以存储子类对象,并且取出时的类型至少为父类类型
writeWithWildcard(new ArrayList<Fruit>(), new Apple());
//!! writeWithWildcard(new ArrayList<Apple>(), new Fruit());
}
//----------读取
static <T> T readExact(List<T> list) {
return list.get(0);
}
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit = Arrays.asList(new Fruit());
// 通过方法直接读取:
static void f1() {
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
}
/*
* 然而,如果你定义的是一个泛型类,而不是方法时,当你实例化这个类
* 时,参数类型就已经确定,调用它的方法时就不能改变了,这与泛型方
* 法是不一样的:
*/
static class Reader<T> {
T readExact(List<T> list) {
return list.get(0);
}
}
// 通过类来读取
static void f2() {
//泛型类创建时需指定类型,即创建时类型就已确定
Reader<Fruit> fruitReader = new Reader<Fruit>();
Fruit f = fruitReader.readExact(fruit);
//因为创建时类型就已定为Fruit,所以不能传递Apple:
//!! Fruit a = fruitReader.readExact(apples);
}
static class CovariantReader<T> {
/*
* 可以通过通配符边界<? extends T>在运行时传递子类类型。下面方法与
* readCovariant(List<T> list, T t) 是不一样的,该方法中隐含着两类
* 型参数有直接的父子关系或相同,因为只有这样list对象才能使用t对象。
* 而下面的方法第一个参数list的类型参数声明成List<? extends T>,这
* 就已经明显的表明了第一个类型参数与第二个类型参数的关系,即第二个
* 类型参数要是第一个类型参数的父类或相同,这与readCovariant(List<T>
* list, T t)形式的方法恰好相反。同样
* readCovariant2(List<? super T> list, T t)也明确说明了两个参数的
* 的父子关系,第一个T为第二个T的父类或相同。
*/
T readCovariant(List<? extends T> list, T t) {
/*
* 假如定义如下:List<? extends Apple> list = new ArrayList<Jonathan>();
* 则不可以通过引用list调用任何泛型方法,为什么?
* 原因就是这些泛型方法的真真参数类型比定义时类型要窄,如这里的Jonathan就要
* 比Apple类型就要窄,所你不能通过一个Jonathan类型的变量来接收一个Apple类型
* 的实例吧,所以不能通过被<? extends T>的引用来调用其任何泛型方法。
*
* 经过上面我们会很清楚的知道下面语句为什么不行了
*/
//!! list.add(t);
/*
* 假如定义如下:List<? extends Apple> list = new ArrayList<Jonathan>();
* 则可以通过引用list调用返回类型为泛型的方法呢(当然方法参数不能是泛型的),
* 并且返回类型为Apple,为什么?因为即使你将list引用指向成ArrayList<Jonathan>
* 类型的实例,还是将它指向ArrayList<OtherJonathan>类型的实例,但他们的都
* 不会超过上界类型Apple,所以返回的类型至少为上界类型Apple。
*
* 经过上面我们会很清楚的知道下面语句为什么返回的类型自然就是T了。
*/
return list.get(0);// 但返回类型至少是T
}
//可以通过通配符边界<? super T>在运行时传递父类类型,但不能作为返回类型,即可以通过泛型方法传进,但不能通过泛型方法返回
T readCovariant2(List<? super T> list, T t) {
/*
* 假如定义如下:List<? super Jonathan> list = new ArrayList<Apple>();
* 则可以通过引用list调用其泛型方法,并且传递的参数只能是Jonathan或其子类
* 为什么我们可以通过<? super Jonathan>类型的引用list来调用<Apple>类型
* 实例的泛型方法?原因就是这些泛型方法的真真参数类型比定义时类型要宽,
* 如这里的Apple就要比Jonathan类型就要宽,所以我们传递给泛型方法真实现类
* 的子类或本身是可以的,平时我们也是这样做的,即使用父类的引用指向子类对象
*
* 经过上面我们会很清楚的知道下面语句为什么可行了
*/
list.add(t);
/*
* 假如定义如下:List<? super Jonathan> list = new ArrayList<Apple>();
* 为什么调用返回类型为泛型类型时,返回的类型却只能是Object呢?因为你可
* 以将list引用指向成ArrayList<Apple>类型的实例,你也可能将它指向ArrayList
* <Object>类型的实例,这将导致通过list引用调用返回结果为泛型类型的方法时,
* 有可能是Jonathan、Apple、还有 Object,所以最终只能为Object类型。最主
* 要因为类型参数被<? super T> 修饰时,该类型参数的最上界就是Object类了
* ,所以通过被<? super T>修饰的引用调用返回类型为泛型方法时,返回的类型
* 只能是最上界Object,而不能是下界T,也不能为它们之间的某个类型。
*
* 从上述就可以很清楚的知道了为什么list.get(0)返回的是Object类型了。
*/
// !! return list.get(0);
return (T) list.get(0);//但可以强转
}
}
static void f3() {
CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
Fruit f = fruitReader.readCovariant(fruit, new Fruit());
f = fruitReader.readCovariant(fruit, new Apple());
Fruit a = fruitReader.readCovariant(apples, new Apple());
}
public static void main(String[] args) {
write1();
write2();
f1();
f2();
f3();
}
}
无界通配符:
List<?> list1:没有明确的边界,可以引用存放任何类型的List,List<?> list1 = new ArrayList();不会发生警告,与有边界的通配符一样,也不能通过list1向容器中添加除null的任何类型的对象。此种情况的边界实质上为Object,但又与List<? extends Object> list1不一样。
List<? extends Object> list1: 可以引用存放任何类型的List,List<? extends Object > list1 = new ArrayList();会发生警告,因为它指定了明确的边界,所以赋值时要指定明确的边界。
编译器处理List<?>与List<? extends Object>是不一样的,前者没有明确的边界,后者有,但两者的边界都是Object。
List list1与List<?> list1的区别:
由于擦除操作,List<?>看起来等价于List<Object>(但又不完全相同,因为至少List<Object> l = new ArrayList<String>();是有问题的,而List<?> l = new ArrayList<String>();却是可以的)。而List实际上也是List<Object>。
List实际上表示“可以存放任何Object类型的原生List”,而List<?>表示“只能存放某种类型的非原生List,只是我们不知道那种类型是什么”。
原生类型List和参数化类型List<Object>是不一样的。如果使用了原生类型,编译器不会知道在list允许接受的元素类型上是否有任何限制,它会允许你添加任何类型的元素到list中。这不是类型安全的,但如果使用了参数化类型List<Object>,编译器便会明白这个list可以包含任何类型的元素,所以你添加任何对象都是安全的。
>>>泛型问题<<<
不能将基本类型用作类型参数,如果是基本类型时只能使用其包装类型或自动包装机制。
实现参数化接口:一个类不能实现同一个泛型接口多次,下面的Hourly编译不能通过:
interface Payable<T> {}
class Employee implements Payable<Employee> {}
class Hourly extends Employee implements Payable<Hourly> {}
但去掉泛型参数编译就可以通过了:
class Employee implements Payable{}
class Hourly extends Employee implements Payable{}
或者是将泛型参数置为相同也可:
class Employee implements Payable<Employee> {}
class Hourly extends Employee implements Payable<Employee> {}
在使用某些更基本的Java接口,例如Comparable<T>时,这个问题可能会变得十分头痛。
在Java泛型中,有一个好像是经常使用的语法,但它有点令人费解:
class A<T extends A<T>>{},这是允许的,这个说明了extends关键字用于边界与用来创建子类明显是不同的。A类接受泛型参数T,而T是由一个边界类来限定,这个边界就是接受T作为类型参数的A类。这种自限定所做的,就是要求在继承关系中,像下面这样使用这个类:
class B extends A<B>{}
这会强制要求将正在定义的类当作参数传递给基类,它可以保证类型参数必须与正在被定义的类相同。
class A<T>{}
class B extends A<B>{}
以上也是允许的,它表示“我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数”,即父类使用子类替代其类型参数。
参数协变:
方法参数类型会随子类而变化。尽管自限定类型可以产生子类类型相同的返回类型,但在JavaSE 5中已引入参数协变:
interface Base {
Base get();
}
interface Derived extends Base {
/*
* 从Java SE5开始子类方法可以返回比它重写的基类方法更
* 具体的类型,但是这在早先的Java版本是不允许——重写
* 时子类的返回类型一定要与基类相同。
*
* 但要注意的是:子类方法返回类型要是父类方法返回类型
* 的子类,而不能反过来,即父类 Derived get(); 而重
* 写时子类为Base get();是不行的。
*/
Derived get();
}
public class CovariantReturnTypes {
void test(Derived d) {
Derived d1 = d.get();
Base d2 = d.get();
}
}
使用自限定泛型修改上面程序:
interface Base<T extends Base<T>> {
T get();
}
interface Derived extends Base<Derived> {}
public class CovariantReturnTypes {
void test(Derived d) {
Derived d1 = d.get();
Base d2 = d.get();//也可返回基类类型
}
}
上面程序是方法返回类型协变,返回类型协变在Java SE5中得到了支持——方法可以返回比它重写的基类方法更具体的类型。但如果子类的方法参数是更具体类型时,这时是重载而不是重写了(注,以前版本就是这样):
class Base {
void get(HashMap l) {
System.out.println("Base get()");
}
}
class Derived extends Base {
// 这是重载,而不是重写,重载
void get(LinkedHashMap l) {
System.out.println("Derived get()");
}
public static void main(String[] args) {
Derived d = new Derived();
d.get(new HashMap());//Base get()
d.get(new LinkedHashMap());//Derived get()
}
}
java编程思想--概述2
猜你喜欢
转载自zhyp29.iteye.com/blog/2307335
今日推荐
周排行