一、类字面常量
Java还提供了另一种方法来生成对Class对象的引用,即使用类字母常量。对上述程序来说,就像这样:FancyToy.class。
这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且它根除了对forName()方法的调用,所以也更高效。
类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE。TYPE字段是一个引用,指向对应的基本数据类型的Class对象,如下所示:
等价于 | |
boolean.class | Boolean.TYPE |
char.class | Character.TYPE |
byte.class | Byte.TYPE |
short.class | Short.TYPE |
int.class | Integer.TYPE |
long.class | Long.TYPE |
float.class | Float.TYPE |
double.class | Double.TYPE |
void.class | Void.TYPE |
我建议使用“.class”的形式,以保持与普通类的一致性。
注意,有一点很有趣,当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象。为了使用类而做的准备工作实际包含三个步骤:
- 加载,这是由类加载器执行的。该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非是必需的),并从这些字节码中创建一个Class对象。
- 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。
- 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行:
import java.util.Random;
class Initable {
static final int staticFinal = 47;
static final int staticFinal2 = ClassInitialization.r.nextInt(1000);
static {
System.out.println("Initializing Initable");
}
}
class Initable2 {
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
static int staticNonFinal = 47;
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random r = new Random();
public static void main(String[] args) throws ClassNotFoundException {
Class initable = Initable.class;
System.out.println("After creating Initable ref");
System.out.println(Initable.staticFinal);
System.out.println(Initable.staticFinal2);
System.out.println(Initable2.staticNonFinal);
Class initable3 = Class.forName("Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
初始化有效地实现了尽可能的“惰性”。从对initable引用的创建中可以看到,仅使用.class语法来获得对类的引用不会引发初始化。但是,为了产生Class引用,Class.forName()立即就进行了初始化,就像在对initable3引用的创建中所看到的。
如果一个static final值是“编译期常量”,就像Initable.staticFinal那样,那么这个值不需要对Initable类进行初始化就可以被读取。但是,如果只是将一个域设置为static和final的,还不足以确保这种行为,例如,对Initable.staticFinal2的访问将强制进行类的初始化,因为它不是一个编译期常量。
如果一个static域不是final的,那么对它访问时,总是要求在它被读取之前,要先进行链接(为这个域分配空间)和初始化(初始化该存储空间),就像在对Initable2.staticNonFinal的访问中所看到的那样。
二、泛化的Class引用
Class引用总是指向某个Class对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码。它还包含该类的静态成员,因此,Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一个对象。
但是,java SE5的设计者们看准机会,将它的类型变得更具体了一些,而这是通过允许你对Class引用所指向的Class对象的类型进行限定而实现的,这里用到了泛型语法。在下面的实例中,两种语法都是正确的:
public class GenericClassReferences {
public static void main(String[] args) {
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class;
intClass = double.class;
// genericIntClass = double.class;
}
}
普通的类引用不会产生警告信息,你可以看到,尽管泛型类引用只能赋值为所指向其声明的类型,但是普通的类引用可以被重新赋值为指向任何其他的Class对象。通过使用泛型语法,可以让编译器强制执行额外的类型检查。
如果你希望稍微放松一些这种限制,应该怎么办呢?乍一看,好像你应该能够执行类似下面这样的操作:
Class<Number> genericNumberClass = int.class;
这看起来似乎是起作用的,因为Integer继承自Number。但是它无法工作,因为Integer Class对象不是Number Class对象的子类。为了在使用泛化的Class引用时放松限制,我使用了通配符,它是java泛型的一部分。通配符就是“?”,表示“任何事物”。因此,我们可以在上例的普通Class引用中添加通配符,并产生相同的结果:
public class WildcardClassReferences {
public static void main(String[] args) {
Class<?> intClass = int.class;
intClass = double.class;
}
}
在java SE5中,Class<?>优于平凡的Class,即便它们是等价的,并且平凡的Class如你所见,不会产生编译器警告信息。Class<?>的好处是它表示你并非是碰巧或者由于疏忽,而使用了一个非具体的类引用,你就是选择了非具体的版本。
为了创建一个Class引用,它被限定为某种类型,或该类型的任何子类型,你需要将通配符与extends关键字相结合,创建一个范围。因此,与仅仅声明Class<Number>不同,现在做如下声明:
public class BoundedClassRenferences {
public static void main(String[] args) {
Class<? extends Number> bounded = int.class;
bounded = double.class;
bounded = Number.class;
}
}
向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查,因此如果你操作有误,稍后立即就会发现这一点。在使用普通Class引用,你不会误入歧途,但是如果你确实犯了错误,那么直到运行时你才会发现它,而这显得很不方便。
下面的示例使用了泛型类语法,它存储了一个类引用,稍后又产生了一个List,填充这个List的对象是使用newInstance()方法,通过该引用生成的:
import java.util.ArrayList;
import java.util.List;
class CountedInteger {
private static long counter;
private final long id = counter++;
public String toString() {
return Long.toString(id);
}
}
public class FilledList<T> {
private Class<T> type;
public FilledList(Class<T> type) {
this.type = type;
}
public List<T> create(int nElements) {
List<T> result = new ArrayList<>();
for (int i = 0; i < nElements; i++)
try {
result.add(type.newInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
return result;
}
public static void main(String[] args) {
FilledList<CountedInteger> fl = new FilledList<CountedInteger>(CountedInteger.class);
System.out.println(fl.create(15));
}
}
注意,这个类必须假设与它一同工作的任何类型都具有一个默认的构造器(无参构造器),并且如果不符合该条件,你将得到一个异常。编译器对该程序不会产生任何警告信息。
当你将泛型语法用于Class对象时,会发生一件很有趣的事情:newInstance()将返回该对象的确切类型,而不仅仅只是在ToyTest.java中看到的基本的Object。这种某种程度上有些限制:
public class GenericToyTest {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
Class<FancyToy> ftClass = FancyToy.class;
FancyToy fancyToy = ftClass.newInstance();
Class<? super FancyToy> up = ftClass.getSuperclass();
// Class<Toy> up2 = ftClass.getSuperclass();
Object obj = up.newInstance();
}
}
如果你手头的是超类,那编译器将只允许你声明超类引用是“某个类,它是FancyToy超类”,就像在表达式Class<? Super FancyToy>中所看到的,而不会接受Class<Toy>这样的声明。这看上去显得有些怪,因为getSuperClass()方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了——在本例中就是Toy.class——而不仅仅只是“某个类,它是FancyToy超类”。不管怎样,正是由于这种含糊性,up.newInstance()的返回值不是精确类型,而只是Object。
三、新的转型语法
java SE5还添加了用于Class引用的转型语法,即cast()方法:
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);
h = (House) b;
}
}
cast()方法接受参数对象,并将其转型为Class引用的类型。当然,如果你观察上面的代码,则会发现,与实现了相同功能的main()中最后一行相比,这种转型好像做了很多额外的工作。型的转型语法对于无法使用普通转型的情况显得非常有用,在你编写泛型代码时,如果你存储了Class引用,并希望以后通过这个引用来执行转型,这种情况就会时有发生。这被证明是一种罕见的情况。
在java SE5中另一个没有任何用处的新特性就是Class.asSubclass(),该方法允许你将一个类对象转型为更加具体的类型。
如果本文对您有很大的帮助,还请点赞关注一下。