第14章 类型信息
运行时类型信息使我们可以在程序运行时发现和使用类型信息。Java有两种方式让我们在运行时识别对象和类的信息:
- RTTI(Run Time Type Identification):在运行时识别一个对象的类型,它假定我们在编译时已经知道了所有的类型。
- 反射机制:它允许我们在允许时发现和使用类的信息。
14.1 为什么需要RTTI
RTTI能够保证我们的类型转换。在面向对象程序设计中,我们通常的做法是:让代码只操纵对基类的引用。这样的话,再添加一个新类来扩展程序,则不会影响原来的代码。 例如:
abstract class Shape {
void draw() {
System.out.println(this + ".draw()");
}
abstract public String toString();
}
class Circle extends Shape {
public String toString() { return "Circle"; }
}
class Square extends Shape {
public String toString() { return "Square"; }
}
class Triangle extends Shape {
public String toString() { return "Triangle"; }
}
public class Shapes {
public static void main(String[] args) {
List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
for (Shape shape : shapeList) {
shape.draw();
}
}
}
当将具体元素(Circle,Square,Triangle)放入List容器中时,会发生向上转型,并且会丢失元素具体类型。 List容器实际上将所有事物都当成Object持有,并且在元素取出时,会自动转型为Shape。
在这个例子中,RTTI类转换并不彻底:Object转型为Shape,而不是具体的Circle,Square或Triangle。因为在编译时,由容器和Java的泛型确保List中保存的是Shape,运行时由类型转换操作来确保这一点。
接下来就是多态机制:即Shape对象具体执行什么代码,是由引用所指向的具体对象(Circle,Square,Triangle)而决定的。优点是:容易编写、阅读和维护,设计上容易实现、理解和改变。
14.2 Class对象
在Java中,类型信息由Class对象保存,该对象就是用来创建类的所有常规对象的。
类是程序的一部分,每个类都有一个Class对象。即每编写且编译了一个新类,就会产生一个Class对象(保存在一个同名的.class文件中)。Java虚拟机(JVM)通过使用类加载器子系统,可以生成类的Class对象。
类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。原生加载器加载的是可信类,包括Java API类,它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,如果你有特殊需求(例如以某种特殊的方式加载类,以支持Web服务器应用,或者再网络中下载类),那么你有一种方式可以挂接额外的类加载器。
所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员引用时,就会加载这个类。因此,Java程序在它开始运行之前并非全部被加载,其各个部分是在必需时才加载的。
在程序运行过程中,如果使用到某个类,类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找.class文件。
在运行时,我们获取类型信息一般有两种方式:
- CLass.forName(String):通过Class类的静态方法,使用全限定类名获取。
- Object.getClass():通过根基类Object的方法,获取其Class对象。
Class对象包含了很多有用的方法,下面是其中的一部分:
interface HasBatteries {}
interface Waterproof {}
interface Shoots {}
class Toy {
Toy() {}
Toy(int i) {}
}
class FancyToy extends Toy implements HasBatteries,Waterproof,Shoots {
FancyToy() { super(1); }
}
public class ToyTest {
static void printInfo(Class cc) {
print("Class name: " + cc.getName() +
" is interface? [" + cc.isInterface() + "]");
print("Simple name: " + cc.getSimpleName());
print("Canonical name : " + cc.getCanonicalName());
}
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName("com.jiao.thinkInJava.example.FancyToy");
} catch (ClassNotFoundException e) {
print("Can't find FancyToy");
System.exit(1);
}
printInfo(c);
for (Class face : c.getInterfaces())
printInfo(face);
Class up = c.getSuperclass();
Object obj = null;
try {
obj = up.newInstance();
} catch (InstantiationException e) {
print("Cannot instantiate");
System.exit(1);
} catch (IllegalAccessException e) {
print("Cannot access");
System.exit(1);
}
printInfo(obj.getClass());
}
}
上例中使用了以下几个方法:
- String getName():获取全限定类名
- String getSimpleName():获取不包含包名的类名
- String getCanonicalName():获取全限定类名
- boolean isInterface():该Class对象是否表示某个接口
- Class[] getInterfaces():获取该Class对象中所包含的接口
- Class getSuperclass():获取该Class对象的基类Class对象
- newInstance():使用Class对象通过默认构造器创建该类实例
14.2.1 类字面常量
Java还提供了另一种方式来生成对Class对象的引用,即类字面常量:
FancyToy.class
这样做会在编译时就受到检查,简单、高效。
并且,类字面常量不仅可以应用于普通的类,也可应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE,其指向对应的基本数据类型的Class对象,例如:
- boolean.class 等价于 Boolean.TYPE
- char.class 等价于 Character.TYPE
- ......
为了使用类而做的准备工作实际包含三个步骤:
- 加载:由类加载器执行,通过查找字节码,从而创建一个Class对象。
- 连接:验证字节码并为静态域分配存储空间,在必需的情况下,解析该类所创建的其他类的引用。
- 初始化:如果该类具有超类,则对其初始化,执行静态初始化器和初始化块。
而使用.class来创建Class对象的引用时,不会自动初始化该Class对象,初始化被延迟到了对静态方法或非常数静态域进行首次引用时才执行:
class Initable {
static final int staticFinal = 66;
static final int staticFinal2 = ClassInitialization.random.nextInt(1000);
static { System.out.println("Initializing Initable"); }
}
class Initable2 {
static int staticNotFinal = 99;
static { System.out.println("Initializing Initable2"); }
}
class Initable3 {
static int staticNotFinal = 33;
static { System.out.println("Initializing Initable3"); }
}
public class ClassInitialization {
public static Random random = new Random(66);
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.staticNotFinal);
Class initable3 = Class.forName("com.jiao.thinkInJava.example.Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNotFinal);
}
}
我们发现:
- 访问某个类的编译期常量时,不会初始化该类。
- 访问某个类的非编译期常量时,总要求其在被读取前,要先进行链接(为该域分配存储空间)和初始化(初始化该存储空间)。
14.2.2 泛化的Class引用
Java SE5中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的类型限制,可以使用通配符?,它表示任何事物:
public class WildcardClassReferences {
public static void main(String[] args) {
Class<?> intClass = int.class;
intClass = double.class;
}
}
如果需要将Class引用所指向对象类型限定为某种类型,或该类型的任何子类型,则需要将通配符与extends关键字相结合:
public class BoundedClassReferences {
public static void main(String[] args) {
Class<? extends Number> bounded = int.class;
bounded = double.class;
bounded = Number.class;
}
}
向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查,而使用普通Class引用,则只能在运行时才会发现错误。
下面的示例使用泛型类语法,填充一个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 n) {
List<T> result = new ArrayList<T>();
try {
for (int i = 0; i < n; i++)
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()将返回该对象的确切类型:
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();
Object obj = up.newInstance();
}
表达式<? super FancyToy>表示FancyToy的父类。
14.2.3 新的转型语法
Java SE5还添加了用于Class引用的转型语法:
class Building {}
class House extends Building {}
public class ClassCasts {
public static void main(String[] args) {
Building b = new Building();
Class<House> houseType = House.class;
House h = houseType.cast(b);
h = (House) b;
}
}
Class.cast(Object)方法接收一个对象,并将其转型为Class引用的类型。我们发现,它和强制转型所实现的功能一样,但更加麻烦。
14.3 类型转换前先做检查
Java中的RTTI有三种形式:
- 传统的类型转换:由编译器检查转型是否合理。
- Class对象:通过该对象获取运行时所需的类型信息。
- instanceof:用于判断指定对象是否是某个特定类型的实例。
下面是继承自Individual的类继承体系:
public class Person extends Individual {
public Person(String name) { super(name); }
}
public class Pet extends Individual {
public Pet() { super(); }
public Pet(String name) { super(name); }
}
public class Dog extends Pet {
public Dog() { super(); }
public Dog(String name) { super(name); }
}
public class Mutt extends Dog {
public Mutt() { super(); }
public Mutt(String name) { super(name); }
}
public class Pug extends Dog {
public Pug() { super(); }
public Pug(String name) { super(name); }
}
public class Cat extends Pet {
public Cat() { super(); }
public Cat(String name) { super(name); }
}
public class EgyptianMau extends Cat {
public EgyptianMau() { super(); }
public EgyptianMau(String name) { super(name); }
}
public class Manx extends Cat {
public Manx() { super(); }
public Manx(String name) { super(name); }
}
public class Cymric extends Manx {
public Cymric() { super(); }
public Cymric(String name) { super(name); }
}
public class Rodent extends Pet {
public Rodent() { super(); }
public Rodent(String name) { super(name); }
}
public class Rat extends Rodent {
public Rat() { super(); }
public Rat(String name) { super(name); }
}
public class Mouse extends Rodent {
public Mouse() { super(); }
public Mouse(String name) { super(name); }
}
public class Hamster extends Rodent {
public Hamster() { super(); }
public Hamster(String name) { super(name); }
}
下面是一个能够随机创建不同类型宠物的抽象类:
public abstract class PetCreator {
private Random random = new Random(47);
public abstract List<Class<? extends Pet>> types();
public Pet randomPet() {
int n = random.nextInt(types().size());
try {
return types().get(n).newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public Pet[] createArray(int size) {
Pet[] result = new Pet[size];
for (int i = 0; i < size; i++)
result[i] = randomPet();
return result;
}
public ArrayList<Pet> arrayList(int size) {
ArrayList<Pet> result = new ArrayList<Pet>();
Collections.addAll(result, createArray(size));
return result;
}
}
其中,抽象方法types()用于获取由Class对象构成的List。下面是使用forName()的一个具体实现:
public class ForNameCreator extends PetCreator {
private static List<Class<? extends Pet>> types = new ArrayList<Class<? extends Pet>>();
private static String[] typeNames = {
"typeinfo.pets.Mutt",
"typeinfo.pets.Pug",
"typeinfo.pets.EgyptianMau",
"typeinfo.pets.Manx",
"typeinfo.pets.Cymric",
"typeinfo.pets.Rat",
"typeinfo.pets.Mouse",
"typeinfo.pets.Hamster"
};
static { loader(); }
private static void loader() {
try {
for (String name : typeNames)
types.add((Class<? extends Pet>)Class.forName(name));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public List<Class<? extends Pet>> types() { return types; }
}
下面,我们就可以使用instanceof来对Pet进行计数了:
public class PetCount {
static class PetCounter extends HashMap<String, Integer> {
public void count(String type) {
Integer quantity = get(type);
if(quantity == null)
put(type, 1);
else
put(type, quantity + 1);
}
}
public static void countPets(PetCreator creator) {
PetCounter counter = new PetCounter();
for (Pet pet : creator.createArray(20)) {
System.out.print(pet.getClass().getSimpleName() + " ");
if(pet instanceof Pet) counter.count("Pet");
if(pet instanceof Dog) counter.count("Dog");
if(pet instanceof Mutt) counter.count("Mutt");
if(pet instanceof Pug) counter.count("Pug");
if(pet instanceof Cat) counter.count("Cat");
if(pet instanceof Manx) counter.count("Manx");
if(pet instanceof EgyptianMau) counter.count("EgyptianMau");
if(pet instanceof Cymric) counter.count("Cymric");
if(pet instanceof Rodent) counter.count("Rodent");
if(pet instanceof Rat) counter.count("Rat");
if(pet instanceof Mouse) counter.count("Mouse");
if(pet instanceof Hamster) counter.count("Hamster");
}
System.out.println();
System.out.println(counter);
}
public static void main(String[] args) {
countPets(new ForNameCreator());
}
}
instanof只能用于对象与具体类型进行判断,而无法将对象与Class对象作比较。 上述程序中,大量的instanceof使我们的代码出现了冗余。
14.3.1 使用类字面常量
下面,我们使用类字面常量重新实现PetCount:
public class LiteralPetCreator extends PetCreator {
public static final List<Class<? extends Pet>> allTypes =
Collections.unmodifiableList(
Arrays.asList(Pet.class, Dog.class, Cat.class, Rodent.class,
Mutt.class, Pug.class, EgyptianMau.class, Manx.class,
Cymric.class, Rat.class, Mouse.class, Hamster.class));
private static final List<Class<? extends Pet>> types =
allTypes.subList(allTypes.indexOf(Mutt.class), allTypes.size());
public List<Class<? extends Pet>> types() {
return types;
}
public static void main(String[] args) {
System.out.println(types);
}
}
为了将该方式作为默认实现,我们使用代理模式,将LiteralPetCreator类包装起来:
public class Pets {
public static final PetCreator creator = new LiteralPetCreator();
public static Pet randomPet() { return creator.randomPet(); }
public static Pet[] createArray(int size) { return creator.createArray(size); }
public static ArrayList<Pet> arrayList(int size) { return creator.arrayList(size); }
}
现在,我们可以对其进行测试了:
public class PetCount2 {
public static void main(String[] args) {
PetCount.countPets(Pets.creator);
}
}
可以发现,其输出与PetCount.java完全相同。
14.3.2 动态的instanceof
在之前的例子中,大量的instanceof使得代码变得冗余,而Class.isInstance()方法通过动态测试对象的途径解决了上述问题:
public class PetCount3 {
static class PetCounter extends LinkedHashMap<Class<? extends Pet>, Integer> {
public PetCounter() { super(MapData.map(LiteralPetCreator.allTypes, 0)); }
public void count(Pet pet) {
for (Map.Entry<Class<? extends Pet>, Integer> pair : entrySet())
if(pair.getKey().isInstance(pet))
put(pair.getKey(), pair.getValue() + 1);
}
public String toString() {
StringBuilder result = new StringBuilder("{");
for (Map.Entry<Class<? extends Pet>, Integer> pair : entrySet())
result.append(pair.getKey().getSimpleName())
.append("=")
.append(pair.getValue())
.append(", ");
result.delete(result.length()-2, result.length());
result.append("}");
return result.toString();
}
}
public static void main(String[] args) {
PetCounter petCounter = new PetCounter();
for (Pet pet : Pets.createArray(20)) {
System.out.print(pet.getClass().getSimpleName() + " ");
petCounter.count(pet);
}
System.out.println();
System.out.println(petCounter);
}
}
可以看到,isInstance()方法使我们的代码变得简洁。并且,如果要添加新类型的Pet,只需简单地改变LiteralPetCreator.java中的常量数组即可。
14.3.3 递归计数
在上述代码中,我们使用Map预加载了所有不同的Pet类,但其局限于Pet类。下面,我们创建一个通用的计数工具:
public class TypeCounter extends HashMap<Class<?>, Integer>{
private Class<?> baseType;
public TypeCounter(Class<?> baseType) {
this.baseType = baseType;
}
public void count(Object obj) {
Class<?> type = obj.getClass();
if(!baseType.isAssignableFrom(type))
throw new RuntimeException(obj + " incorrect type: "
+ type +", should be type or subtype of " + baseType);
countClass(type);
}
private void countClass(Class<?> type) {
Integer quantity = get(type);
put(type, quantity == null ? 1 :quantity+1);
Class<?> superClass = type.getSuperclass();
if(superClass != null && baseType.isAssignableFrom(superClass))
countClass(superClass);
}
public String toString() {
StringBuilder result = new StringBuilder("{");
for (Map.Entry<Class<?>, Integer> pair : entrySet())
result.append(pair.getKey().getSimpleName())
.append("=")
.append(pair.getValue())
.append(", ");
result.delete(result.length()-2, result.length());
result.append("}");
return result.toString();
}
}
我们通过isAssignableFrom()方法来执行运行时的检查,以校验所传递的对象是否属于我们所指定的继承结构。如果该类型含有父类,则将其父类进行递归计数。
下面,通过测试证明该通用计数工具:
public class PetCount4 {
public static void main(String[] args) {
TypeCounter counter = new TypeCounter(Pet.class);
for (Pet pet : Pets.createArray(20)) {
System.out.print(pet.getClass().getSimpleName() + " ");
counter.count(pet);
}
System.out.println();
System.out.println(counter);
}
}
14.4 注册工厂
我们生成Pet继承结构中的对象时,存在一个问题:每次向该继承结构添加新的Pet类型时,必须将其添加进LiteralPetCreator.java中的成员List中。其原因是:硬编码。
解决硬编码的方式有很多:将信息存入文件或数据库等。而如果不得不硬编码,最佳做法是:将该列表置于一个位于中心、位置明显的地方,即继承结构的基类。
下面,我们通过使用工厂方法设计模式,将对象的创建工作交给类自己完成:
public interface Factory <T> { T create(); }
class Part {
public String toString() { return getClass().getSimpleName(); }
static List<Factory<? extends Part>> partFactories = new ArrayList<Factory<? extends Part>>();
static {
partFactories.add(new FuelFilter.Factory());
partFactories.add(new AirFilter.Factory());
partFactories.add(new CabinAirFilter.Factory());
partFactories.add(new OilFilter.Factory());
partFactories.add(new FanBelt.Factory());
partFactories.add(new PowerSteeringBelt.Factory());
partFactories.add(new GeneratorBelt.Factory());
}
private static Random random = new Random(66);
public static Part createRandom() {
int n = random.nextInt(partFactories.size());
return partFactories.get(n).create();
}
}
class Filter extends Part {}
class FuelFilter extends Filter {
public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<FuelFilter> {
public FuelFilter create() { return new FuelFilter(); }
}
}
class AirFilter extends Filter {
public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<AirFilter> {
public AirFilter create() { return new AirFilter(); }
}
}
class CabinAirFilter extends Filter {
public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<CabinAirFilter> {
public CabinAirFilter create() { return new CabinAirFilter(); }
}
}
class OilFilter extends Filter {
public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<OilFilter> {
public OilFilter create() { return new OilFilter(); }
}
}
class Belt extends Part {}
class FanBelt extends Belt {
public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<FanBelt> {
public FanBelt create() { return new FanBelt(); }
}
}
class GeneratorBelt extends Belt {
public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<GeneratorBelt> {
public GeneratorBelt create() { return new GeneratorBelt(); }
}
}
class PowerSteeringBelt extends Belt {
public static class Factory implements com.jiao.thinkInJava.test.typeinfo.pets.Factory<PowerSteeringBelt> {
public PowerSteeringBelt create() { return new PowerSteeringBelt(); }
}
}
public class RegisteredFactory {
public static void main(String[] args) {
for (int i = 0; i < 10; i++)
Part.createRandom();
}
}
14.5 instanceof与Class的等价性
在查询类型信息时,以instanceof的形式与直接比较Class对象有一个很重要的差别:
class Base {}
class Derived extends Base {}
public class FamilyVsExactType {
static void test(Object x) {
print("Testing x of type " + x.getClass());
print("x instanceof Base " + (x instanceof Base));
print("x instanceof Derived " + (x instanceof Derived));
print("Base.isInstance(x) " + Base.class.isInstance(x));
print("Derived.isInstance(x) " + Derived.class.isInstance(x));
print("x.getClass() == Base.class " + (x.getClass() == Base.class));
print("x.getClass() == Derived.class " + (x.getClass() == Base.class));
print("x.getClass().equals(Base.class) " + (x.getClass().equals(Base.class)));
print("x.getClass().equals(Derived.class) " + (x.getClass().equals(Derived.class)));
}
}
通过测试发现:
- instanceof和isInstance()的结果是一致的,==和equals()的结果是一致的。
- instanceof:该对象的类型是指定类型或该类型子类,均返回true。
- ==:该对象的类型只有和指定类型完全一致时,才返回true。
14.6 反射:运行时的类型信息
使用RTTI获取类型信息有一个限制:在编译时,编译器必须知道所有要通过RTTI来处理的类。
但如果获取了一个指向某个并不在当前程序空间的对象的引用,那么在编译时则无法获知这个对象所属的类。例如,假设从磁盘文件,或者网络连接中获取了一串字节码,并被告知这些字节码代表了一个类。
我们想要在运行时获取类的信息的另一个动机,便是希望提供在跨网络的远程平台上创建和运行对象的能力。即远程方法调用(RMI):它允许一个Java程序将对象分布到多台机器上。
反射机制则可以满足我们上述需求。Class类与java.lang.reflect类库一起对反射进行了支持,该类库包含了Field、Method以及Constructor类。
注意,RTTI和反射之间真正的区别在于:
- RTTI:编译器在编译时打开和检查.class文件。
- 反射:.class文件在编译时无法获取,是在运行时打开和检查.class文件。
14.6.1 类方法提取器
在我们需要创建更加动态的代码时,反射会体现其价值。在Java中,反射被用来支持其他特性,如对象序列化和JavaBean。
下面的例子使用反射动态提取某个类的信息,进而展示该类中的所有方法:
//Args:ShowMethods
public class ShowMethods {
public static String usage =
"usage:\n" +
"ShowMethods qualified.class.name\n" +
"To show all methods in class or:\n" +
"ShowMethods qualified.class.name word\n" +
"To search for methods involving 'word'";
private static Pattern pattern = Pattern.compile("\\w+\\.");
public static void main(String[] args) {
if(args.length < 1) {
System.out.println(usage);
System.exit(0);
}
int lines = 0 ;
try {
Class<?> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
Constructor<?>[] constructors = c.getConstructors();
if(args.length == 1) {
for (Method method : methods)
print(pattern.matcher(method.toString()).replaceAll(""));
for (Constructor ctor : constructors)
print(pattern.matcher(ctor.toString()).replaceAll(""));
lines = methods.length + constructors.length;
} else {
for (Method method : methods)
if(method.toString().indexOf(args[1]) != -1) {
print(pattern.matcher(method.toString()).replaceAll(""));
lines ++;
}
for (Constructor ctor : constructors)
if(ctor.toString().indexOf(args[1]) != -1) {
print(pattern.matcher(ctor.toString()).replaceAll(""));
lines ++;
}
}
} catch (Exception e) {
print("No such class: " + e);
}
}
}
上例中使用了以下方法:
- Class.getMethods():返回Method数组,从而通过Method对象获取与方法相关的信息。
- Class.getConstructors():返回Constructor数组,从而通过Constructor获取与构造器相关的信息。
由于Class.forName()所产生的结果在编译期是不可知的,因此所有的方法特征签名信息都是在执行时被提取出来的。反射的强大作用在于:它能够创建一个在编译时完全未知的对象,并调用此对象的方法。
14.7 动态代理
代理是基本的设计模式之一,通常情况下,我们使用代理对象代替真实对象,从而提供额外的行为。下面是一个用来展示代理结构的简单示例:
interface Interface {
void doSomething();
void somethingElse(String arg);
}
class RealObject implements Interface {
public void doSomething() { print("doSomething"); }
public void somethingElse(String arg) { print("somethingElse " + arg); }
}
class SimpleProxy implements Interface {
private Interface proxied;
public SimpleProxy(Interface proxied) {
this.proxied = proxied;
}
public void doSomething() {
print("SimpleProxy doSomething");
proxied.doSomething();
}
public void somethingElse(String arg) {
print("SimpleProxy somethingElse " + arg);
proxied.somethingElse(arg);
}
}
public class SimpleProxyDemo {
public static void consumer (Interface iface) {
iface.doSomething();
iface.somethingElse("bonobo");
}
public static void main(String[] args) {
consumer(new RealObject());
consumer(new SimpleProxy(new RealObject()));
}
}
Java的动态代理比代理的思想更向前迈进了一步,它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上。下面使用了动态代理重写了上例:
class DynamicProxyHandler implements InvocationHandler{
private Object proxied;
public DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("**** proxy: " + proxy.getClass()
+ ", method: " + method + ", args: " + args);
if(args != null )
for (Object arg : args)
System.out.println(" " + arg);
return method.invoke(proxied, args);
}
}
public class SimpleDynamicProxy {
public static void consumer(Interface iface) {
iface.doSomething();
iface.somethingElse("bonobo");
}
public static void main(String[] args) {
RealObject real = new RealObject();
consumer(real);
Interface proxy = (Interface) Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{ Interface.class },
new DynamicProxyHandler(real));
consumer(proxy);
}
}
通过静态方法Proxy.newProxyInstance()可以创建动态代理对象,其需要三个参数:
- ClassLoader:类加载器,通常从已被加载的类中获取其类加载器。
- Class<?>[]:希望该代理实现的接口列表。
- InvocationHandler:InvocationHandler接口的一个实现类。
通常在执行代理操作时,需要使用Method.invoke()将请求转发给被代理的对象,并传入必需的参数。并且,我们可以通过方法的信息,过滤我们所希望加强的方法:
class MethodSelector implements InvocationHandler {
private Object proxied;
public MethodSelector(Object proxied) {
this.proxied = proxied;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if(method.getName().equals("interesting"))
print("Proxy detected the interesting method");
return method.invoke(proxied, args);
}
}
interface SomeMethods {
void boring1();
void boring2();
void boring3();
void interesting(String arg);
}
class Implementation implements SomeMethods {
public void boring1() { print("boring1"); }
public void boring2() { print("boring2"); }
public void boring3() { print("boring3"); }
public void interesting(String arg) { print("interesting " + arg ); }
}
public class SelectingMethods{
public static void main(String[] args) {
SomeMethods proxy = (SomeMethods) Proxy.newProxyInstance(
SomeMethods.class.getClassLoader(),
new Class[]{SomeMethods.class},
new MethodSelector(new Implementation()));
proxy.boring1();
proxy.boring2();
proxy.boring3();
proxy.interesting("bonobo");
}
}
14.8 空对象
当我们使用内置的null表示缺少对象时,经常需要在使用前对其判断,否则将会产生异常。有时引入空对象的思想将会有所用途:它可以接受传递给它所代表对象的所有消息,但其返回却不具实际意义。
通过这种方式,我们可以假设所有的对象都是有效的,而不必浪费编程精力去检查null。即使空对象可以响应实际对象的所有消息,我们仍需要某种方式去测试其是否为空,而最简单的方式就是创建一个标记接口:
public interface Null {}
这使得我们可以使用intanceof探测空对象,并且不必在所有类中都添加isNull()方法了。
很多系统都会有一个Person类,很多情况我们没有一个实际的人或不具备这个人的全部信息,如公司正在招聘的某个岗位:
public class Person {
public final String first;
public final String last;
public final String address;
public Person(String first, String last, String address) {
this.first = first;
this.last = last;
this.address = address;
}
public String toString() {
return "Person: " + first + " " + last + " " + address ;
}
public static class NullPerson extends Person implements Null {
private NullPerson() { super("None", "None", "None"); }
public String toString() { return "NullPerson"; }
}
public static final Person NULL = new NullPerson();
}
空对象一般是不可变的,我们将其构造器设为private,并将该对象设置为静态final,使成为单例。
下面的Position类封装了岗位名和与之对应的人的信息:
public class {
private String title;
private Person person;
public Position(String title, Person person) {
this.title = title;
this.person = person;
if(person == null)
person = Person.NULL;
}
public Position(String title) {
this.title = title;
person = Person.NULL;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
if(person == null)
person = Person.NULL;
}
public String toString() {
return "Position: " + title + " " + person ;
}
}
接下来,我们可以使用Staff类填充职位:
public class Staff extends ArrayList<Position>{
public void add(String title,Person person) {
add(new Position(title,person));
}
public void add(String... titles) {
for (String title : titles)
add(new Position(title));
}
public Staff(String... titles) { add(titles); }
public boolean positionAvailable(String title) {
for (Position position : this)
if(position.getTitle().equals(title)
&& position.getPerson() == Person.NULL)
return true;
return false;
}
public void fillPosition(String title,Person hire) {
for (Position position : this) {
if(position.getTitle().equals(title)
&& position.getPerson() == Person.NULL) {
position.setPerson(hire);
return;
}
}
throw new RuntimeException("Position " + title + "not available");
}
public static void main(String[] args) {
Staff staff = new Staff("President","CTO","Marketing Manager",
"Product Manager","Project Lead","Software Engineer",
"Software Engineer","Test Engineer","Technical Writer");
staff.fillPosition("President", new Person("Me", "last", "The Top, Lonelt At"));
staff.fillPosition("Project Lead", new Person("Janet", "Planner", "The Burbs"));
if(staff.positionAvailable("Software Engineer"))
staff.fillPosition("Software Engineer", new Person("Bob", "Coder", "Bright Light City"));
}
}
我们发现,在某些地方仍必须测试空对象,这与检查是否为null没有差异。但在其他地方,例如本例中的toString()中,则无需执行额外的测试了。
14.9 接口与类型信息
接口的一个重要目标就是允许程序员隔离构件,进而降低耦合性。所以,在大多数情况下,我们会将实现类向上转型为接口,并返回给外界,从而实现隐藏细节。
但是,我们经常在实现接口时,并非是与接口中的方法完全一致,时常会在实现类中增加额外的方法。但在这种情况下,客户端程序员可以通过使用RTTI,将接口进行转型,进而可以访问到这些额外的方法,使得他们的代码和类库存在耦合:
public interface A {
void f();
}
class B implements A {
public void f() {}
public void g() {}
}
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
System.out.println(a.getClass().getSimpleName());
if(a instanceof B) {
B b = (B)a;
b.g();
}
}
}
最简单的方式是对实现使用包访问权限:
class C implements A {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
}
public class HiddenC {
public static A makeA() { return new C(); }
}
这样,外界所能访问的唯一方法就是makeA():获取A类型对象。由于C类型在外界是无法访问到的,所以也无法将其进行向下转型:
public class HiddenImplementation {
public static void main(String[] args) {
A a = HiddenC.makeA();
a.f();
System.out.println(a.getClass().getName());
// Compile error: cannot find symbol C
// if( a instanceof C )
}
}
不过,这也不是完全隐蔽的,通过反射,仍旧可以调用所有方法,甚至是private方法:
public class ReflectImplement {
public static void main(String[] args) throws Exception {
A a = HiddenC.makeA();
callHiddenMethod(a, "g");
callHiddenMethod(a, "u");
callHiddenMethod(a, "v");
callHiddenMethod(a, "w");
}
static void callHiddenMethod(Object obj,String methodName) throws Exception {
Method method = obj.getClass().getDeclaredMethod(methodName);
method.setAccessible(true);
method.invoke(obj);
}
}
如果将接口实现为一个私有内部类呢:
class InnerA {
private static class C implements A {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
}
public static A makeA() { return new C(); }
}
public class InnerImplementation {
public static void main(String[] args) throws Exception {
A a = InnerA.makeA();
a.f();
System.out.println(a.getClass().getName());
ReflectImplement.callHiddenMethod(a, "g");
ReflectImplement.callHiddenMethod(a, "u");
ReflectImplement.callHiddenMethod(a, "v");
ReflectImplement.callHiddenMethod(a, "w");
}
}
结果发现仍旧无法对反射进行隐藏,那么如果是匿名类呢:
class AnonymousA {
public static A makeA() {
return new A() {
public void f() { print("public C.f()"); }
public void g() { print("public C.g()"); }
void u() { print("package C.u()"); }
protected void v() { print("protected C.v()"); }
private void w() { print("private C.w()"); }
};
}
}
public class AnonymousImplementation {
public static void main(String[] args) throws Exception {
A a = AnonymousA.makeA();
a.f();
System.out.println(a.getClass().getName());
ReflectImplement.callHiddenMethod(a, "g");
ReflectImplement.callHiddenMethod(a, "u");
ReflectImplement.callHiddenMethod(a, "v");
ReflectImplement.callHiddenMethod(a, "w");
}
}
目前尝试下来,没有任何方式可以阻止反射调用那些非公共访问权限的方法。下面将尝试反射对于域的访问和修改情况:
class WithPrivateFinalField {
private int i = 1;
private final String s = "I'm totally safe";
private String s2 = "Am I safe?";
public String toString() {
return "i = " + i + ", " + s + ", " + s2;
}
}
public class ModifyingPrivateFields {
public static void main(String[] args) throws Exception {
WithPrivateFinalField pf = new WithPrivateFinalField();
System.out.println(pf);
Field f = pf.getClass().getDeclaredField("i");
f.setAccessible(true);
System.out.println("f.getInt(pf): " + f.getInt(pf) );
f.setInt(pf, 66);
System.out.println(pf);
f = pf.getClass().getDeclaredField("s");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf) );
f.set(pf, "No, you're not!");
System.out.println(pf);
f = pf.getClass().getDeclaredField("s2");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf) );
f.set(pf, "No, you're not!");
System.out.println(pf);
}
}
可以发现,当反射作用于域时,仍然可以访问并修改非公共访问权限的域。注意,在final域遭遇反射修改时,运行系统会在不抛异常的情况下接受任何修改,但实际上并没有做出修改。
因此,反射机制是一把双刃剑。它违反了访问权限,但也能解决一些疑难问题。
14.10 总结
RTTI允许通过匿名基类的引用来发现类型信息。但在面向对象程序语言中:凡是可以使用的地方都使用多态机制,只在必需时使用RTTI。
反射允许更加动态的编程风格,其开创了编程的新世界。