概述
第十三章字符串
字符串是不可变的:是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
5s %3$10sn", "Qty", "Item", "Price");
f.format("%-15s %5s %10sn", "----", "---", "-----");
}
public void print(String name, int qty, double price) {
// %-15.15s在%-15s基础上最多能输出15个字符
f.format("%-15.15s %5d %10.2fn", name, qty, price);
total += price;
}
public void printTotal() {
// %10.2f表示输出一个最小宽度为10,右对齐小数点后两位的浮点数
f.format("%-15s %5s %10.2f = %sn", "Tax", "", total * 0.06, total + " * 0.06");
f.format("%-15s %5s %10sn", "", "", "-----");
f.format("%-15s %5s %10.2f = %sn", "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>jrelibrt.jar文件中。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类。
2. 扩展(Extension)类加载器(实现为:sun.misc.Launcher$ExtClassLoader):它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的<JAVA_HOME>jrelibext子目(扩展目录)下加载类库,如果把用户创建的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:myappsyslib>javac -d . MyClassLoader.java
D:myappsyslib>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:myappotherlib路径中加载了。
从这个例子还可以看出,在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:myappserverlib目录下,然后把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:myappsyslib下时,却又能正常,因为此时他们都是由系统类加载器加载,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:myappserverlib目录下,然后把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 SE5后泛型与自动装箱的出现,数组的优点就只是效率了。
无论使用哪种类型的数组,数组标识符其实只是一个引用,指向在堆中创建的一个真实对象,这个(数组)对象又可能保存指向其他对象的引用。
新创建的数组未初始化时,如果存储的是对象,则所有元素自动初始化为null,基本类型初始化为0,字符型自动初始为(char)0,布尔型自动初始化为false。
不能创建泛型数组以及带类型参数的数组:
//T[] array = new T[SIZE]; // 不能创建泛型数组
//ArrayList<String> [] list = new ArrayList<String>[1]; // 也不能创建带类型参数的数组
但可以定义一个泛型数组的引用:
T[] array;
ArrayList<String> [] list;
虽然你不能创建泛型数组,但是可以创建非泛型数组然后将其转型:
public class ArrayOfGenerics {
public static void main(String[] args) {
List<String>[] ls;
List[] la = new List[10];
ls = (List<String>[])la; //会发生 "Unchecked" 警告
ls[0] = new ArrayList<String>();
//上面其实相当于下面一条语句
//List<String>[] ls1 = (List<String>[])new List[10];
// 编译时会产生错误:
//! ls[1] = new ArrayList<Integer>();
// 问题是: List<String> 是Object子类
Object[] objects = ls; //所以可以赋值给Object数组引用
// 编译与运行都没有错误,因为创建的List数组本身就是原生数组:
objects[1] = new ArrayList<Integer>();
}
}
Random实例对象可随机返回各种基本类型的数,可设置种子。但Math中的random方法只能返回[0.0, 1)之间的double型小数,实质上该方法就是调用Random实例的nextDouble()来实现的。
Arrays类提供了重载的equals()方法。数组相等的条件是元素个数必须相等,并且对应位置的元素也相等。
>>>动态创建数组<<<
现有如下应用,Employee[]数组满后扩容,该如何做?
Employee[] a = new Employee[100]
…
//arry is full
a = (Employee[])arrayGrow(a);
错误作法:
static Object[] arrayGrow (Object[] a) {
int newLength = a.length * 11 / 10 + 10;
Object[] newArray = new Object[newLength];
System.arraycopy(a, 0, newArray, 0, a.length);
return newArray;
}
上面在实际应用中会遇到一个问题,这段代码返回的数组类型是Object[],这是由于使用下面这行代码创建的数组:new Object[newLength]。如果现在我们要对Employee[]数组进行扩展时,则在扩充后我们不能将返回的Object[]类型的对象数组转换成Employee[]数组了(当然如果从Object[]类型对象数组中取出一个个元素后再强转为Employee是没问题)。将一个Employee[]临时地转换成Object[]数组,然后再把它转换回来是可以的,但一个从开始就是Object[]的数组却永远不能转换成Employee[]数组。为了编写这类通用的数组代码,需要能够创建与原数组类型相同的新数组。因此需用到反射包中的Array类的静态方法newInstance.:
static Object arrayGrow (Object a) {//参数是Object而不是Object[],因为整型数组类型int[]可以被转换成Object,但不能转换成对象数组
Class cl = a.getClass();
if (!cl.isArray()) {
return null;
}
Class componentType = cl.getComponentType();
int length = Array.getLength(a);
int newLength = length * 11 / 10 + 10;
Object newArray = Array.newInstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0, newLength);
return newArray;
}
另外,以下转换也是可以的:
String[] strArr = new String[10];
Object o = strArr;
strArr= (String[]) o;
第十七章容器的深入研究
可以使用Arrays.asList将数组或可变参数转换成AbstractList列表,其底层数据实现就是我们传进的参数数组,因此不能像其他List列表那样调整尺寸,如果你试图用add()或delete()方法在这种列表中添加或删除元素,就有可能会引发去修改数组尺寸的尝试,因此你将在运行时获得“Unsupported Operation(不支持的操作)”错误,该列表只支持读取与修改操作(另外,我们还可以通Collections.unmodifiableList(List)来对一个列表包装之后,只能进行读取操作,即使写是不行了)。
class Snow {}
class Powder extends Snow {}
class Light extends Powder {}
class Heavy extends Powder {}
class Crusty extends Snow {}
class Slush extends Snow {}
public class AsListInference {
public static void main(String[] args) {
List<Snow> snow1 = Arrays.asList(
new Crusty(), new Slush(), new Powder());
// Won't compile:
// List<Snow> snow2 = Arrays.asList(
// new Light(), new Heavy());
// Compiler says:
// found : java.util.List<Powder>
// required: java.util.List<Snow>
// Collections.addAll() doesn't get confused:
List<Snow> snow3 = new ArrayList<Snow>();
Collections.addAll(snow3, new Light(), new Heavy());
// Give a hint using an
// explicit type argument specification:
List<Snow> snow4 = Arrays.<Snow>asList(
new Light(), new Heavy());
}
}
当试图创建snow2时,Arrays.asList()中只有Powder类型,因此它会创建List<Powder>而不是List<Snow>,尽管Collections.addAll()工作的很好,因为它从第一个参数中了解到了目标类型是什么。
正如你从创建snow4的操作中所看到的,可以在Arrays.asList()中间插入具体的类型,以告诉编译器对于由Arrays.asList()产生的List类型,实际的类型应该是什么。这称为显示类型参数说明。
Queue接口与List、Set同一级别,都是继承了Collection接口。LinkedList实现了Queue接口。Queue接口窄化了对LinkedList的方法的访问权限(即在方法中的参数类型如果是Queue时,就完全只能访问Queue接口所定义的方法了,而不能直接访问LinkedList的非Queue的方法),以使得只有恰当的方法才可以使用。
offer(),将一个元素插入到队尾,或者返回false。
peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,而element()会抛出NoSuchElementExcetption异常。
poll()和remove()方法将移除并返回队头,但是poll()在队列为空时返回null,而remove()会抛出NoSuchElementExcetption异常。
PriorityQueue:优先级队列。先进先出描述了最典型的队列规则,但优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。如果构建一个消息系统,某些消息比其他消息更重要,因而应该更快地得到外理,那么它们何时得到处理就与它们何时到达无关。PriorityQueue添加到JSE5中,是为了提供这种行为的一种自动实现。当你在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序,但是你可能通过提供自己的Comparator来修改这个顺序。PriorityQueue可以确保当你调用peek()、poll()和remove()方法时,获取元素将是队列中优先级最高的元素。
Collection接口继承了Iterable接口,该接口包含一个能够产生java.util.Iterator的iterator()方法,可用于foreach语句中。
foreach语句可以用于数组或其他任何Iterable,但是这并不意味着数组是一个Iterable,数组与Iterable接口没有直接的关系。
你必须为散列存储和树型存储都创建一个equals()方法,但是hashCode()只有在这个类将会存放到HashMap、HashSet或者LinkedHashMap、LinkedHashSet中时才是必需的,因为这类Hash最终都是能过HashMap的containsKey方法使用if (e.hash == hash && eq(k, e.key))来实现对比的。
Set —— 存入Set的每个元素都必须是唯一的,因为Set不允许保存重复的元素。放入到Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。
HashSet(优先选择) —— 底层以HashMap来实现。为快速查找而设计的Set。存入HashSet的元素必须定义hashCode()。
TreeSet —— 保持次序的Set,底层为树结构。使用它可以从Set中读取有序的序列。放入的元素必须实现Comparable接口。
LinkedHashSet —— 底层以LinkedHashMap来实现。继承自HashSet,具有HashSet的查询速度,且内部使用链表维护元素顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素的插入次序显示。存入的元素也必须定义hashCode()方法。
HashMap(优先选择) —— Map基于散列表的实现(它取代了Hashtable)。插入和查询“键值对”的开销是固定的。可以通过构造器设置容量和负载因子,以调容器的性能。
LinkedHashMap —— 继承自HashMap,但是迭代遍历它时,取得“键值对”的顺序是其插入次序,或者是最近最小使用(LRU)的次序。只比HashMap慢一点,而在迭代访问时反而更快,因为它使用链表维护内部次序。
TreeMap —— 基于红黑树的实现。查看“健”或“键值对”时,它们会被排序(次序由Comparable或Comparator决定)。TreeMap的特点在于,所得到的结果是经过排序的。TreeMap是唯一带有subMap()方法的Map,它可以返回一个子树。
WeakHashMap —— 弱键映射,允许垃圾回收器回收无外界引用指向象Map中键,这是为解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则此“键”可以被垃圾收集器回收。
ConcurrentHashMap —— 一种线程安全的Map,它不synchronized同步加锁,而是使用新的锁机制。尽管所有操作都是线程安全的,但检索操作不必锁定,并且不支持以某种防止所有访问的方式锁定整个表。
IdentityHashMap —— 使用 == 代替equals()对“键”进行比较的散列映射。
看一个弱引用例子:
class WeakObject {
String name;
public WeakObject(String mwname) {
this.name = mwname;
}
public void finalize() {
System.out.println(name + "对象满足垃圾收集条件,被收集!");
}
public void show() {
System.out.println(name + "对象还可以使用!");
}
}
public class WeakReferenceTest {
public static void main(String[] args) {
System.out.println("---对象弱引用---");
WeakObject wo = new WeakObject("weakObject");
//包装成弱引用对象
WeakReference wr = new WeakReference(wo);
wo = null;
((WeakObject) wr.get()).show();
System.out.println("第一次垃圾收集!");
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (wr.get() != null) {
((WeakObject) wr.get()).show();
}
System.out.println("---弱引用map---");
WeakHashMap whm = new WeakHashMap();
WeakObject wo2 = new WeakObject("weakObjectKey");
//这里的值不会被回收
whm.put(wo2, new WeakObject("weakObjectValue"));
wo2 = null;
((WeakObject) whm.keySet().iterator().next()).show();
System.out.println("第二次垃圾回收!");
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
((WeakObject) whm.keySet().iterator().next()).show();
}
}
对于底层采用数组的ArrayList,无论列表的大小如何,这些访问都很快和一致。而对于LinkedList,访问时间对于较大的列表将明显增加。很显然,如果你需要执行大量的随机访问,链接链表不会是一种好的选择;
插入时,对于ArrayList,当列表变大时,其开销将变得很高昂,但是对于LinkedList,相对来说比较低廉,并且不随列表尺寸而发生变化,这是因为ArrayList在插入时,插入点后面所有元素将后移,如果在插入时超过了数组的最大容量,则会重新创建一个新的数组,并将所有元素复制到新的数组中(不过插入与扩容时使用的都是System.arraycopy方法,效率上比使用循环一个个移动要高),这会随ArrayList的尺寸增加而产生高昂的代价。LinkedList只需链接新的元素,而不必修改列表中剩余的元素,因此可以认为无论列表尺寸如何变化,其代价大致相同。
在LinkedList中的插入和移除代价相当低廉,并且不随列表尺寸发生变化,但是对于ArrayList插入操作代价特别高昂,并且其代价将随列表尺寸增加而增加。
HashSet的性能基本上总是比TreeSet好,特别是在添加和查询元素时,而这两个操作也是最重要的操作。TreeSet存在的唯一原因是它可以维持元素的排序状态;所以,只有当需要一个排好序的Set时,才应该使用TreeSet。因为其内部结构支持排序,并且因为迭代是我们更有可能执行的操作,所以,用TreeSet迭代通常比用HashSet要快。
除了IdentityHashMap,所有的Map实现的插入操作都会随着Map尺寸变大而明显变慢,但是,查找的代价通常比插入要小得多。
Hashtable的性能大体上与HashMap相当,因为HashMap是用来替代Hashtable的,因为它们使用了相同的底层存储和查找机制,这并没有什么令人奇怪的。
TreeMap通常比HashMap要慢。
LinkedHashMap在插入 时比HashMap慢一点,因为它维护散列数据结构的同时还要维护链表(以保持插入顺序),正是由于这个列表,使得其迭代速度更快一点。
IdentityHashMap则具有完全不同的性能,因为它使用==而不是equals()来比较元素。
负载因子小的Hash表产生冲突的可能性小,因此对于插入和查找都是最理想(但是会减慢使用迭代器进行遍历的过程,因为还有很多的空位置,这些空的位置也会在循环中遍历到)。当容量扩大时,现有的对象将重新分布到亲的桶位中(这被称为再散列)。HashMap使用的默认负载因子为0.75(只有当表的饱和度达到四分之三时,才进行再散列),这个因子在时间和空间代价之间达到平衡,更大的负载因子可以提高空间的利用率,但是会增加查找代价。
如果你知道将要在HashMap中存储多少项,那么创建一个具有恰当大小的初始容量将可以避免自动再散列的开销。
形如Collections.unmodifiableCollection(Collection<? extends T> c)一类方能产生只读容器。
形如Collections.synchronizedCollection(Collection<T> c, Object mutex)一类方能产生同步容器。
快速报错:Java容器有一种保护机制,能够防止多个进程(或直接通过容器本身而不是迭代器)同时修改同一个容器的内容。如果在你迭代遍历某个容器的过程中,另一个进程介入其中,并且插入、删除或修改此容器内的某个对象,那就会出现问题:也迭代过程已经处理过容器中的该元素,也许还没处理,也许在调用size()之后容器的尺寸缩小了等等,Java容器类类库采用快速报错机制。它会先检测容器上的任何除了你的进程所进行的操作或使用迭代器外的操作所引起的变化,一进发现改变,就会立刻抛ConcurrentModificationExcetion异常。这就是“快速报错”的意思——即,不是使用复杂的算法在事后来检测问题。所以应该在添加、删除、修改完所有元素之后,再获取迭代器。ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet都使用了可以避免ConcurrentModificationExcetion的技术。
第十八章I/O
第十九章枚举
enum的values()方法返回enum实例的数组,而且该数组中的元素严格保持其在enum中声声明时的顺序。
创建enum时,编译器会为你生成一个相关的类,这个类继承自java.lang.Enum。
Enum类实例的ordinal()方法返回一个int值,这是每个enum实例在声明时的次序,从0开始。可以使用==来比较enum实例,编译器会自动为你提供equals()和hashCode()方法。
Enum类实现了Comparable接口,所以它具有compareTo()方法,同时,它还实现了Serializable接口。
name()方法返回enum实例声明的名字,与使用toString()方法效果一样。
valueOf(Class<T> enumType, String name)是在Enum中定义的static方法,它根据所给定的名字返回相应的enum实例,如果不存在给定名字的实例,将会抛出异常。
除了不能继承自一个enum之外,我们基本上可以将enum看作一个常规的类,也就是说,我们可以向enum中添加方法属性。
public enum OzWitch {
// enum实例必须定义在最前,并在方法之前:
WEST("west."), NORTH("north."),
EAST("east."), SOUTH("south.");//如果还有其他方法,则这个分号一定要写上
private String description;//可以定义属性
// 注,枚举的构造函数只能是包访问或private访问权限:
private OzWitch(String description) {
this.description = description;
}
//也可以有方法
public String getDescription() {
return description;
}
//还可以有main方法
public static void main(String[] args) {
for (OzWitch witch : OzWitch.values())
System.out.println(witch + ": " + witch.getDescription());
}
}
>>>values()的神秘之处<<<
public enum Explore {
HERE, THERE
}
使用javap反编译后:
public final class Explore extends java.lang.Enum{
public static final Explore HERE;
public static final Explore THERE;
public static final Explore[] values();// 编译器自动添加的方法
public static Explore valueOf(java.lang.String);// 编译器自动添加的方法
static {};
}
Enum类没有values()方法,values()是由编译器为enum添加的static方法,在创建Explore的过程中,编译器还为它创建了valueOf(String name)方法,这可能有点怪了,Enum类不是已经有valueOf(Class<T> enumType, String name)了吗?不过Enum中的valueOf()方法需要两个参数,而这个新增的方法只需一个参数。
由于values()方法是由编译器插入到enum定义中的static方法,所以,如果你将enum实例向上转型为Enum,那么values()方法就不可访问了。不过,在Class中有一个getEnumConstants()方法,所以即使Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有enum实例(注:只有是Enum类型的Class才能调用getEnumConstants方法,否则抛空指针异常):
enum Search { HITHER, YON }
public class UpcastEnum {
public static void main(String[] args) {
Search[] vals = Search.values();
Enum e = Search.HITHER; // 向上转型
// e.values(); // Enum中没有 values()方法
// 但我们可以通过Class对象的getEnumConstants()反射出enum实例
for(Enum en : e.getClass().getEnumConstants())
System.out.println(en);
}
}
>>>enum还可实现其它接口<<<
由于enum可以看是一个普通的类,所以他还可以实现其他接口。
enum CartoonCharacter implements Generator<CartoonCharacter> {
SLAPPY, SPANKY, BOB;
private Random rand = new Random(47);
public CartoonCharacter next() {
return values()[rand.nextInt(values().length)];
}
}
>>>枚举实例的“多态”表现<<<
enum LikeClasses {
WINKEN { void behavior() { System.out.println("Behavior1"); } },
BLINKEN { void behavior() { System.out.println("Behavior2"); } },
NOD { void behavior() { System.out.println("Behavior3"); } };
abstract void behavior();//抽象方法,由每个enum实例实现,当然也可是一个实体方法
public static void main(String[] args) {
for(LikeClasses en:values()){
en.behavior();//好比多态
}
}
// 但不能把枚举实例看作是类,因为它们只是一个实例
// void f1(LikeClasses.WINKEN instance) {}
}
经过反编译后,我们发现生成了四个类LikeClasses.class、LikeClasses$2.class、
LikeClasses$3.class、LikeClasses$1.class,而且LikeClasses$xx.class继承自LikeClasses.class类,并将各自的behavior方法代码放入到自己相应的类文件中。
第二十章注解
JavaSE5内置了三种标准注解,定义在java.lang中的注解:
@Override,表示当前的方法定义将覆盖超类中的方法。如果没有重写,编译器会发出错误提示。
@Deprecated,如果程序员使用了该注解注解过的元素,那么编译器会发出警告信息。
@SuppressWarnings,关闭不当的编译器警告信息。
元注解是负责注解其他的注解,有四种元注解:
@Target 表示该注解可以用于什么地方。可能的ElementType(枚举)参数包括:
CONSTRUCTOR:构造方法声明
FIELD:域声明(包括enum实例)
LOCAL_VARIABLE:局部变量声明
METHOD:方法声明
PACKAGE:包声明
PARAMETER:参数声明
TYPE:类,接口(包括注解类型)或enum声明
@Retention 表示需要在什么级别保存注解信息。可用的RetentionPolicy(枚举)参数包括:
SOURCE:注解将被编译地器丢弃。
CLASS:注解在class文件中可用,但会被VM丢弃。
RUNTIME:VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
@Documented 将此注解包含在JavaDoc中。
@Inherited 允许子类继承父类中的注解。
Class、Method、Field、Constructor类都实现了AnnotatedElement接口,他们的getAnnotation方法都返回指定类型的注解对象,如果没有该类型的注解,则返回null值,以下是这些方法的原型:
class. getAnnotation(Class<T> annotationClass)
method. getAnnotation(Class<T> annotationClass)
field. getAnnotation(Class<T> annotationClass)
constructor. getAnnotation(Class<T> annotationClass)
注解的定义看起来很像接口的定义,事实上,与其他任何Java接口一样,注解也会编译成class文件。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*
* 这是一个简单的注解,我们可以用它来跟踪一个项目中的用例。如果一个方法
* 或一组方法实现了某个用例的需求,那么程序员可以为此方法加上该注解。
*/
//注解的定义
@Target(ElementType.METHOD)//该注解用于方法
@Retention(RetentionPolicy.RUNTIME)//注解信息保留到运行期
public @interface UseCase {
public int id();//int型的元素
public String description() default "no description";//String型的元素
}
//注解的使用
class PasswordUtils {
@UseCase(id = 47, description = "密码必须至少包含一个数字")
public boolean validatePassword(String password) {
return (password.matches("\w*\d\w*"));
}
@UseCase(id = 48)//没有描述,使用默认的描述信息
public String encryptPassword(String password) {
return new StringBuilder(password).reverse().toString();
}
@UseCase(id = 49, description = "新密码不能使用以前使用过的密码")
public boolean checkForNewPassword(
List<String> prevPasswords, String password) {
return !prevPasswords.contains(password);
}
}
//注解处理器
class UseCaseTracker {
public static void
trackUseCases(List<Integer> useCases, Class<?> cl) {
for(Method m : cl.getDeclaredMethods()) {
// 通过反射获取某个方法特定的注解信息
UseCase uc = m.getAnnotation(UseCase.class);
if(uc != null) {
System.out.println("找到用例:" + uc.id() +
" " + uc.description());
useCases.remove(new Integer(uc.id()));
}
}
for(int i : useCases) {
System.out.println("警告: 所缺用例-" + i);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<Integer>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
}
/*
找到用例:47 密码必须至少包含一个数字
找到用例:48 no description
找到用例:49 新密码不能使用以前使用过的密码
警告: 所缺用例-50
*/
注解里的组成元素类型:
1、 所有基本类型
2、 String
3、 Class
4、 enum
5、 Annotation
6、 以上类型的数组
如果你使用了其他类型,那编译器就会报错。
注解组成元素的默认值限制:首先,元素不能有不确定的值,也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值,不允许即没给定默认值,在使用进也没指定值的情况出现。其次,对于非基本类型的元素,无论是在声明还是在使用时,都不能以null作为其值。
注解不支持继承。
第二十一章并发
Thread.yield():对线程调度器(Java线程机制的一部分,可以将CPU从一个线程转移给另一个线程)的一种建议,它是在说“我已经执行完生命期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机”,即对线程的一种让步,暂停当前正在执行的线程,并执行其他线程。
>>>使用Executor<<<
Java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。
Executors:Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类的工厂和实用方法。
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor
构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。
public class LiftOff implements Runnable {
protected int countDown = 10; // Default
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String status() {
return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "), ";
}
public void run() {
while(countDown-- > 0) {
System.out.print(status());
Thread.yield();
}
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
/*
#2(9), #0(9), #4(9), #1(9), #3(9), #2(8), #0(8), #4(8), #1(8), #3(8), #2(7), #0(7), #4(7), #1(7), #3(7), #2(6), #0(6), #4(6), #1(6), #3(6), #2(5), #0(5), #4(5), #1(5), #3(5), #2(4), #0(4), #4(4), #1(4), #3(4), #2(3), #0(3), #4(3), #1(3), #3(3), #2(2), #4(2), #1(2), #3(2), #2(1), #4(1), #1(1), #3(1), #2(Liftoff!), #4(Liftoff!), #1(Liftoff!), #3(Liftoff!), #0(2), #0(1), #0(Liftoff!),
*/
Executors. newFixedThreadPool (int nThreads):可一次性预先执行代价高昂的线程分配,因而也就可以限制线程数量。不用为每个任务都固定地付出创建线程的开销。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPool {
public static void main(String[] args) {
// Constructor argument is number of threads:
ExecutorService exec = Executors.newFixedThreadPool(5);
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
/*
#3(9), #4(9), #0(9), #2(9), #1(9), #3(8), #4(8), #0(8), #2(8), #1(8), #3(7), #4(7), #0(7), #2(7), #3(6), #4(6), #0(6), #2(6), #3(5), #4(5), #0(5), #2(5), #3(4), #4(4), #0(4), #2(4), #3(3), #4(3), #0(3), #2(3), #3(2), #4(2), #0(2), #2(2), #3(1), #4(1), #0(1), #2(1), #3(Liftoff!), #4(Liftoff!), #0(Liftoff!), #2(Liftoff!), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!),
*/
Executors. newSingleThreadExecutor (int nThreads):确保任意时刻在任何都只有唯一的任务在运行,你不需要在共享资源上处理同步,可以让你省去只是为了维持某些事物的原型而进行的各种协调努力。
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService exec =
Executors.newSingleThreadExecutor();
for(int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
/*
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!), #1(9), #1(8), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!), #2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(Liftoff!), #3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1), #3(Liftoff!), #4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(Liftoff!),
*/
Thread.sleep(100);等同于TimeUnit.MILLISECONDS.sleep(100);
Daemom线程:后面线程不属于程序中不可缺少的部分,因此,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。返过来说,只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
如果一个线程是后台线程,那么它创建的任何线程将被自动设置成后台线程。
后台线程在不执行finally子句的情况下就会终止其run()方法,即后台线程的finally子句不一定执行。
在构造器中启动线程可能会有问题,因为线程可能会在构造器结束之前开始执行,这意味着该线程能够访问处于不稳定状态的对象。
异常不能跨线程传播给main(),所以你必须在了本地处理所有在线程内部产生的异常。
public class ExceptionThread implements Runnable {
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
try {
ExecutorService exec =
Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
} catch(RuntimeException ue) {
// 这句将不会被执行,因为线程的异常是不会传递到调用它的线程的
System.out.println("Exception has been handled!");
}
}
}
Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()会在因未捕获的异常而临近死亡时被调用。为了使用它,我们创建了一个新类型的ThreadFactory,它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler,并将这个工厂传递给Exceutors创建新的ExcecutorService方法:
// 线程
class ExceptionThread2 implements Runnable {
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t);
System.out.println("1.eh = " + t.getUncaughtExceptionHandler());
throw new RuntimeException();//线程运行时一定会抛出运行异常
}
}
// 线程异常处理器
class MyUncaughtExceptionHandler implements
Thread.UncaughtExceptionHandler {
// 异常处理方法
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
// 线程工厂,创建线程时会调用该工厂
class HandlerThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {//线程创建工厂方法
System.out.println(this + " creating new Thread");
Thread t = new Thread(r);
System.out.println("created " + t);
//设置异常处理器
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
System.out.println("2.eh = " + t.getUncaughtExceptionHandler());
return t;
}
}
public class CaptureUncaughtException {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool(
new HandlerThreadFactory());
exec.execute(new ExceptionThread2());
}
} /*
HandlerThreadFactory@1a758cb creating new Thread
created Thread[Thread-0,5,main]
2.eh = MyUncaughtExceptionHandler@69b332
run() by Thread[Thread-0,5,main]
1.eh = MyUncaughtExceptionHandler@69b332
caught java.lang.RuntimeException
*/
如果你知道将要在代码中处处使用相同的异常处理器,那么更简单的方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器:Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
>>>使用Lock对象<<<
Lock对象必须被显式地创建、锁定和释放。因此,它与内建的锁形式相比,代码缺乏优雅性,但对于解决某些类型的问题来说,它更加灵活。
private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
try {
//…
} finally {
lock.unlock();
}
}
使用lock()和unlock()方法在next()内部创建了临界资源。还可以尝试获取锁:
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
boolean captured = lock.tryLock();
try {
//…
} finally {
if(captured)
lock.unlock();
}
}
>>>使用volatile对象<<<
原子操作是不能被线程调试机制中断的操作,一旦开始操作,那么它一定会在切换到其他线程前执行完毕。
原子操作可以应用于除long和double之外的所有基本类型之上的“简单操作”,对于读取和写入除long和double之外的基本类型变量这种的操作,可以保证它们会被当作原子操作来操作内存。但是JVM可以将64位(long和double变量)的读取和写入当作两个分离的32位操作来执行,这就可能会产生了在一个读取和写入操作中间切换线程,从而导致不同的线程看到不正确结果的可能性。但是,当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性,注:在Java SE5之前,volatile一直未能正确的工作。
volatile关键字还确保了应用中的可视性,如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改,即便使用了本地缓存,情况确实如此,volatile域会立即被写入到主存中。
在非volatile域上的操作没有刷新到主存中去,因此其他读取该域的线程将不能必看到这个新值。因此,如果多个线程同时访问了某个域,那么这个域就应该是volatile的,否则,这个域应该只能由同步来访问,同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来保护,那就不必将其设置为volatile了。
什么才属于原子操作呢?对域中的值做赋值和返回操作都是原子性的。但i++; i+=2; 这样的操作肯定不是原子性的,即线程有可能从语句的中间切换。下面来证明i++在java里不是原子性操作的:
class SerialNumberGenerator {
private static volatile int serialNumber = 0;
public static /* synchronized */int nextSerialNumber() {
// 不是线程安全,因为i++在Java里不是原子操作,
// 即使將serialNumber设置成了volatile
return serialNumber++;
}
}
class CircularSet {
private int[] array;
private int len;
private int index = 0;
public CircularSet(int size) {
array = new int[size];
len = size;
// 初始化为-1
for (int i = 0; i < size; i++) {
array[i] = -1;
}
}
public synchronized void add(int i) {
array[index] = i;
// 如果数组满后从头开始填充,好比循环数组:
index = ++index % len;
}
public synchronized boolean contains(int val) {
for (int i = 0; i < len; i++) {
if (array[i] == val) {
return true;
}
}
return false;
}
}
public class SerialNumberChecker {
private static final int SIZE = 10;
private static CircularSet serials = new CircularSet(1000);
private static ExecutorService exec = Executors.newCachedThreadPool();
static class SerialChecker implements Runnable {
public void run() {
while (true) {
int serial = SerialNumberGenerator.nextSerialNumber();
if (serials.contains(serial)) {// 如果数组中存在则退出
System.out.println("Duplicate: " + serial);
System.exit(0);
}
serials.add(serial);// 如果不存在,则放入
}
}
}
public static void main(String[] args) throws Exception {
SerialChecker sc = new SerialChecker();
// 启动10线程
for (int i = 0; i < SIZE; i++) {
exec.execute(sc);
}
}
}
public class Increament extends Thread {
public static volatile int x = 0;
public void run() {
// synchronized (Increament.class) {
// x++与 x = x + 1都不是原子操作
x++;
// x = x + 1;
// }
}
public static void main(String[] args) throws Exception {
Thread threads[] = new Thread[10000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Increament();
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
// 等待计算线程运行完
threads[i].join();
}
System.out.println("n=" + Increament.x);
}
}
如果对x的操作是原子级别的,最后输出的结果应该为x=10000,而在执行上面积代码时,很多时侯输出的x都小于10000,这说明x++ 不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用。
同一时刻只有一个线程能访问synchronized块,synchronized块并不是一下子要执行完毕,CPU调试可能从synchronized块中的某个语句切换到其它的线程,再其它线程执行完毕后再继续执行该同步块。切换到其他线程时是否释放synchronized块上的锁,这要看切换所采用的方式:如果是CPU自动或调用Thread.yeild切换,则不会释放;如果是调用wait,则会释放;如果是调用的Thread.sleep,则不会;如果是调用的thread.join,则要看synchronized块上的锁是否是thread线程对象,如果不是,则不会释放,如果是,则会释放。
只能在同步控制方法或同步控制块里调用wait()、notify()和notifyAll(),并且释放操作锁,但sleep()可以在非同步控制方法里调用,不会释放锁。
sleep、yield都是Thread的静态方法,join属于Thread的非静态方式,如果将它们放入在同步块中调用时都不会释放锁。但wait属于Object类的方法,在wait()期间对象锁是释放的。
在执行同步代码块的过程中,遇到异常而导致线程终止,锁会释放。
执行线程的suspend()方法会导致线程被暂停,并使用resume()可唤醒,但不会释放锁。
当线程在运行中执行了Thread类的yield()静态方法,如果此时具有相同优先级的其他线程处于就绪状态,那么yield()方法将把当前运行的线程放到可运行池中并使用中另一线程运行。如果没有相同优先级的可运行进程,则该方法什么也不做。
sleep方法与yield方法都是Thread类的静态方法,都会使当前处于运行的线程放弃CPU,把运行机会让给另的线程。两都的区别:
1. sleep方法会给其他线程运行的机会以,而不考虑其他线程的优先级,因此会给较低优先级线程一个运行的机会;yield方法只会给相同或更高优先级的线程一个运行的机会。
2. 当线程执行了sleep方法后,将转到阻塞状态。当线程执行了yield方法后,将转入就绪状态。
3. Sleep方法比yield方法具有更好的可移植性。不能依靠yield方法来提高程序的并发性能。对于大多数程序员来说,yield方法的唯一用途是在测试期间人为地提高程序的并发性能,以帮助发现一些隐藏的错误。
thread.join():当前线程调用另一线程thread.join()时,则当前运行的线程将转到阻塞状态,并且等待thread线程运行结束后,当前线程程才会恢复运行(从阻塞状态到就绪状态)。比如有3个线程在执行计算任务,必须等三个线程都执行完才能汇总,那么这时候在主线程里面让三个线程join,最后计算结果既可:
public class JoinTest {
public static void main(String[] args) {
Rt[] ct = new Rt[3];
for (int i = 0; i < ct.length; i++) {
ct[i] = new Rt();
ct[i].start();
try {
//主线等待三个线程终止后再继续运行
ct[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int total = 0;
for (int j = 0; j < ct.length; j++) {
total += ct[j].getResult();
}
System.out.println("total = " + total);
}
}
class Rt extends Thread {
private int result;
public int getResult() {
return result;
}
public void run() {
try {
Thread.sleep(1000);
result = (int) (Math.random() * 100);
System.out.println(this.getName() + " result=" + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
join()只能由线程实例调用,如果thread.join()在同步块中调用,并且同步锁对象也是thread对象,由于thread.join()是调用thread.wait()来实现的,wait会释放thread对象锁,则thread.join()与在同步块的锁也会一并释放;如果thread.join()在同步块的锁对象不是thread对象,则thread线程阻塞时不会释放锁:
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
JThread t = new JThread();
start(t, t);
System.out.println("--------");
t = new JThread();
start(t, JThread.class);
}
static void start(JThread t, Object lock) {
t.setLock(lock);
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果锁对象是JThread.class时,则主线程会一直阻塞
t.f();
}
}
class JThread extends Thread {
private Object lock;
void setLock(Object lock) {
this.lock = lock;
}
public void run() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " - join before");
/*
* 当前线程阻塞,又要等待自己运行完,这是矛盾的,所以其实该线程永远不会恢复执
* 行,除非使用 join(long millis)方式。实际上我们看this.join()源码就会
* 看出,this.join()就是调用了this.wait()方法,因为了this.wait()会释放
* this对象上的锁,所以当lock对象是自身时,主线程不会被锁住,所以第一个线程
* 会打印 "main - f()"。第二个线程的锁对象是JThread的Class对象,由于join
* 时不会释放JThread.class对象上的锁, 第二个线程会一直阻塞,所以第二个线程
* 不会打印 "main - f()",
*
*/
this.join();
/*
* 这样可以正常结束整个程序,因为this线程一直会阻塞直到对方(也是this的线程)运行完
* 或者是对方没有运行完等 1 毫秒后thsi线程继续运行,所以以这样的方式一定不会出现死锁
* 现象
*/
//this.join(1);
System.out.println(Thread.currentThread().getName() + " - join after");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void f() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " - f()");
}
}
}
sleep与join都会使当前线程处于阻塞状态,而yield则是进行就绪状态。
同步的静态方法的锁对象是方法所属类的Class对象,而同步的非静态方法的锁对象是所调用方法实例所对应的this对象。
继承Runnable与Thread的区别:Thread类本身也实现了Runnable接口。 因此除了构造 Runnable对象并把它作为构造函数的参数传递给Thread类之外,你也可以生成Thread类的一个子类,通过覆盖这个run方法来执行相应的操作。不过,通常最好的策略是把Runnable接口当作一个单独的类来实现,并把它作为参数传递给个Thread的构造函数。通过将代码隔离在单独的类中可以使你不必担心Runnable类中使用的同步方法和同步块与在相应线程类中所使用的其他任何方法之间的潜在操行所带来的影响。更一般地说,这种分离允许独立控制相关的操作和运行这些操作的上下文,同一个Runnable对象既可以传递给多个使用不同方式初抬化的Thread对象,也可以传递给其他的轻量级执行者(executor)。同样需要注意的是,继承了Thread类的刘象不能再同时继承其他类了。
如果线程被启动并且没有终止,调用方法isAlive将返回true。如果线程仅仅是因为某个原因阻塞,该方法也会返回true。
通过调用线程t的join方法将调用者挂起,直到目标线程t结束运行:t.join方法会在当t.isAlive方法的结果为false时返回。
有一些Thread类的方法只能应用于当前正在运行的那个线程中(也就是,调用Thread静态方法的线程),为了强制实施,这些方法都被声明为static:Thread.currentThread、Thread.interrupted、Thread.sleep、Thread.yield。
Thread.yield:仅仅是一个建议——放弃当前线程去运行其他的线程,JVM可以使用自己的方式理解这个建议。尽管缺少保证,但yield方法仍旧可以在一些单CPU的JVM实现上起到相应的效果,只要这些实现不使用分时抢占式的调用机制,在这种机制下,只有当一个线程阻塞时,CPU才会切换到其他线程上执行。如果在系统中线程执行了耗时的非阻塞计算任务的会占有更多的CPU时间,因而降低了应用程序的响应,为了安全起见,当执行非阻塞的计算任务的方法时,则可以在执行过程中插入yield方法(甚至是sleep方法)。为了减少不必要的影响,可以只在偶尔的情况下调用yield方法,比如一个包含如下语句的循环:
if(Math.random() < 0.01) Thread.yield();
使用抢占式调度机制的JVM实现,特别是在多处理器的情况下,yield才可能显得没有什么意义。
最后
以上就是谨慎山水为你收集整理的Java编程思想读书笔记(02)第十三章字符串第十四章类型信息第十五章泛型第十六章数组第十七章容器的深入研究第十八章I/O第十九章枚举第二十章注解第二十一章并发的全部内容,希望文章能够帮你解决Java编程思想读书笔记(02)第十三章字符串第十四章类型信息第十五章泛型第十六章数组第十七章容器的深入研究第十八章I/O第十九章枚举第二十章注解第二十一章并发所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复