14 类型信息
本章讨论Java如何在运行时识别对象和类的信息。
主要有两种方式
(1)传统的”RTTI“,在编译时已经知道所有类型
(2)”反射“机制,允许在运行时发现和使用类的信息
14.1 为什么需要RTTI
interface Animal{
void bark();
}
class Dog implements Animal{
@Override
public void bark() {
System.out.println("汪汪汪");
}
}
class Cat implements Animal{
@Override
public void bark() {
System.out.println("喵喵喵");
}
}
这是RTTI最基本的使用形式。在Java中,所有的类型转换都是在运行时进行正确性检查的。即RTTI的含义:在运行时,识别一个对象的类型。
但是RTTI类型的转换并不彻底:Object被转型为Animal,而不是转型为Dog、Cat。
但是,加入你要知道某个类的确切类型,如Dog或者Cat,那么用多态的方式就无法解决了。
使用RTTI,就可以查询Animal引用所指的确切对象,从而实现特定的操作。
14.2 Class对象
要了解RTTI的工作原理,必须先知道类型信息在运行时是如何表示的。
该工作由Class对象
的特殊对象来完成,它包含了与类有关的信息。
Class类
中包含了该具体类的所有方法。但是使用多态机制只展示了多态中的方法,还有其他的方法在Class类中,没有被表现。
每个类都会有一个Class对象
Java的各个部分一开始并没有被加载,而是在需要的时候被加载,这和传统语言不同。
一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象。
package com.test;
class Keji{
static {
System.out.println("keji,类被加载时才调用");
}
}
class Hasiqi{
static {
System.out.println("hasiqi,类别加载时才调用");
}
}
public class Test {
public static void main(String[] args) {
System.out.println("开始");
new Keji();
try {
Class.forName("Hasiqi"); // 加载Hasiqi这个类
// 加载不出来,因为没有全限定类名
}catch(ClassNotFoundException e) {
System.out.println("没有找到Hasiqi这个类");
}
try {
Class.forName("com.test.Hasiqi");
}catch(ClassNotFoundException e) {
System.out.println("没有找到Hasiqi这个类");
}
}
}
// 输出结果
// 开始
// keji,类被加载时才调用
// 没有找到Hasiqi这个类
// hasiqi,类别加载时才调用
Class.forName("全限定类名")
会根据字符串,返回一个Class类型的对象
根据Class对象就可以获得对象的接口,基类等信息。还可以new一个新的对象
interface Animal{}
class Dog implements Animal{}
public class Test {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
try {
Class dog = Class.forName("com.test.Dog");
for (Class cls : dog.getInterfaces()) {//获得接口的信息
System.out.println(cls);
}
Class superclass = dog.getSuperclass();//获得基类(父类)的信息
System.out.println(superclass);
// 根据类,new一个新的对象
Object obj = dog.newInstance();
System.out.println(obj.getClass());//obj的所属类别
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
14.2.1 类字面常量
Java还提高类字面常量
来生成对Class对象的引用
class Dog {}
public class Test {
public static void main(String[] args){
Class Dog = Dog.class;
}
}
类字面常量更加的安全。
类字面常量不仅可以用于普通的类,还可以用于接口、数组以及基本数据类型。
public class Test {
public static void main(String[] args){
Class int_ = int.class;
Class int_2 = Integer.TYPE; //这两个是等价的
Class b = boolean.class; //基本类型的类字面常量
Class b_2 = Boolean.TYPE;
Class void_ = void.class;
Class void_2 = Void.class;
}
}
当使用.class的时候,不会初始化对象,而是做如下三个步骤
(1)加载:由类加载器执行的 。从字节码创建一个Class对象
(2)链接:验证类中的字节码。
(3)初始化:初始化其超类(基类,父类),执行静态初始化器和静态初始化块。
14.2.2 泛化的Class引用
Class引用指向某个Class对象,它可以创造类的实例
通过泛型语法,限制Class指向的具体类
public class Test {
public static void main(String[] args){
Class in = int.class;
in = double.class; //不会报错
Class<Integer> i = int.class;
//i = double.class; //报错,语法错误
// 使用通配符就可以解决这个问题
Class<?> ii = int.class;
ii = double.class; //不会报错
Class<? extends Number> iii = int.class;
iii = double.class;//不会报错,因为int和double都继承于Number
}
}
14.2.3 新的转型语法
Java SE5 添加了用于Class引用的转型语法,即cast()方法
Class对象中的cast方法,将其他转型转化为当前对象
class Animal {}
class Dog extends Animal{}
public class Test {
public static void main(String[] args){
Animal a = new Dog();
Class<Dog> dogType = Dog.class;
Dog d = dogType.cast(a); //把a转换为Dog
// 等价于
d = (Dog)a;
}
}
14.3 类型转换前先做检查
截至目前,我们已知的RTTI形式包括
(1)传统的类型转换。将Animal类转换为Cat类
(2)代表对象的类型的Class对象。通过Class对象获取运行时所需要的信息。
(3)使用instanceof 进行转换
if (x instanceof Dog)
((Dog)x).speak();//如果是Dog类型,则将其强制转换为Dog类型
class Animal {}
class Dog extends Animal{}
class Cat extends Animal{}
public class Test {
public static void main(String[] args){
Animal d = new Dog();
Animal c = new Cat();
if(d instanceof Dog) {
Dog dog = (Dog)d;
}else if(d instanceof Cat) {
Cat cat = (Cat)d;
}
}
}
instance需要大量的if进行判断,这对设计者来说并不好。
14.3.1 使用类字面常量
通过类字面常量,将这些加入数组或者容器中,通过for循环进行比较,使代码稍微清晰一点
import java.util.ArrayList;
import java.util.List;
class Animal {}
class Dog extends Animal{}
class Cat extends Animal{}
public class Test {
private static List<Class<? extends Animal>> list = new ArrayList<Class<? extends Animal>>();
public static void main(String[] args){
list.add(Dog.class);
list.add(Cat.class);
}
}
14.3.2 动态instanceof
Class.isInstance 方法提供了一种动态地测试对象的途径。
class Animal {}
class Dog extends Animal{}
class Cat extends Animal{}
public class Test {
public static void main(String[] args){
Animal a = new Dog();
Class t = a.getClass();
if (t.isInstance(Dog.class)) {
Dog d = (Dog)a;
}else if(t.isInstance(Cat.class)) {
Cat c = (Cat)a;
}
}
}
14.3.3 递归计数
。。。
14.4 注册工厂
如果要在每个子类中添加静态初始化器,以使得初始化器可以将她的类添加到某个List中。
但是静态初始化器只能在类加载时才能调用,这就变成了“先有鸡还是先有蛋”的问题。
因此最佳的做法是,将这个列表置于一个显眼的位置,将需要的类继承于该类。
package com.test;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
// 工厂接口
interface Factory<T> {
T create();
}
class Animals{
// 该list中,放的Factory的子类,
// 且Factory的泛型是Animals的子类
static List<Factory<? extends Animals>> animalFactory =
new ArrayList<Factory<? extends Animals>>();
static {
animalFactory.add(new Dog.Factory());
animalFactory.add(new Cat.Factory());
animalFactory.add(new Bird.Factory());
}
public static Animals createRandomAnimal() {
Random rand = new Random();
int n = rand.nextInt(animalFactory.size());
return animalFactory.get(n).create(); // 构造一个新的对象
}
}
class Dog extends Animals {
// 内部类,该内部类继承Factory接口
public static class Factory implements com.test.Factory<Dog>{
@Override
public Dog create() {
return new Dog();
}
}
}
class Cat extends Animals {
// 内部类,该内部类继承Factory接口
public static class Factory implements com.test.Factory<Cat>{
@Override
public Cat create() {
return new Cat();
}
}
}
class Bird extends Animals {
// 内部类,该内部类继承Factory接口
public static class Factory implements com.test.Factory<Bird>{
@Override
public Bird create() {
return new Bird();
}
}
}
public class Test {
// Dog,Cat,Bird必须通过Factory才可以创建
// 且Dog,Cat,Bird都是Animals的子类
public static void main(String[] args){
System.out.println(Animals.createRandomAnimal());
}
}
14.5 instanceof 与 Class的等价性
class Father{}
class Son extends Father{ }
public class Test2 {
public static void main(String[] args) {
Father f = new Son();
// 必须是确切的类
System.out.println(f.getClass() == Father.class); //false
System.out.println(f.getClass().equals(Father.class)); //false
// 只要是派生类就可以
System.out.println(f instanceof Father); //true
System.out.println(Father.class.isInstance(f)); //true
}
}
instanceof 与 isInstance是等价的,都是判断是否为父类的子类
而==,equals 则是判断类是否完全相等。
14.6 反射:运行时的类信息
加入你获得了一个不在程序空间中的对象的引用。编译时无法知道该对象所属的类。比如说你从磁盘空间读取一串字节码,怎样才能使用这个类?
Class类 和 java.lang.reflect类对反射
进行了支持,该类库包括Field、Method以及Constructor类。
(1)可以使用Constructor创建新的对象
(2)用get()和set()方法读取和修改Field对象关联的字段
(3)用invoke()方法调用与Method对象关联的方法
(4)还可以调用getFields(),getMethods()和getConstructors()等方法
反射与RTTI的区别之处
RTTI在编译时时会打开和检查.class文件
而反射机制,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件
14.6.1 类方法提取器
发射在Java中用来支持其他特性的,例如对象序列化和JavaBean
反射机制还可以展示类中的方法、字段等信息。
package com.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
class Dog2 {
String color;
public Dog2(){color = "white";}
public Dog2(String color) {this.color = color;}
public void bark() {System.out.println("汪汪汪");}
public void sleep() {System.out.println("zzzzzzzzz");}
public int eat() {return 1;}
}
public class Test2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> dogType = Class.forName("com.test.Dog2");
Method[] methods = dogType.getMethods();//获得所有的方法
for (Method method : methods) {
System.out.println(method);
}
System.out.println("==============");
Constructor<?>[] constructors = dogType.getConstructors();//获得所有构造方法
for (Constructor<?> constructor : constructors) {
System.out.println(constructor);
}
}
}
// public void com.test.Dog2.sleep()
// public void com.test.Dog2.bark()
// public int com.test.Dog2.eat()
// public final void java.lang.Object.wait() throws java.lang.InterruptedException
// public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
// public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
// public boolean java.lang.Object.equals(java.lang.Object)
// public java.lang.String java.lang.Object.toString()
// public native int java.lang.Object.hashCode()
// public final native java.lang.Class java.lang.Object.getClass()
// public final native void java.lang.Object.notify()
// public final native void java.lang.Object.notifyAll()
// ==============
// public com.test.Dog2()
// public com.test.Dog2(java.lang.String)
Class.forName()
生成的结果在编译时是不可知的,所有的方法都是在执行时被提取出来的。
14.7 动态代理
代理是基本的设计模式之一,相当于一个中间人的角色,通过中间人进行沟通。
interface Person{
void sing();//唱歌
void talk();//访谈
}
class Singer implements Person{// 歌手
@Override
public void sing() { // 歌手唱歌
System.out.println("大河向东流。。");
}
@Override
public void talk() { // 访谈唱歌
System.out.println("进行访谈");
}
}
class Proxy implements Person{ //经纪人
private Person singer;
public Proxy(Person singer) {
this.singer = singer;
}
@Override
public void sing() {
System.out.println("先签合同");
singer.sing();
}
@Override
public void talk() {
System.out.println("先对台本");//先对台本,再进行访谈。
singer.talk();
}
}
public class Test2 {
public static void main(String[] args){
Person p = new Proxy(new Singer());
p.sing();
p.talk();
}
}
Java的动态代理
比代理的思想更进一步,它可以动态地创建代理并动态地处理对所代理方法的调用。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Person{
void sing();//歌手唱歌
void talk();//歌手访谈
}
class Singer implements Person{// 歌手
@Override
public void sing() {
System.out.println("大河向东流。。");
}
@Override
public void talk() {
System.out.println("进行访谈");
}
}
class DynamicProxy implements InvocationHandler{
private Object singer;
public DynamicProxy(Object singer) {//构造方法
this.singer = singer;
}
//代理
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始执行代理");
if(args != null ) {
for (Object arg : args) {
System.out.println("参数:"+arg);
}
}
return method.invoke(singer, args); //执行代理的方法
}
}
public class Test2 {
public static void main(String[] args){
Singer singer = new Singer();
Person proxy = (Person)Proxy.newProxyInstance(
Person.class.getClassLoader(),
new Class[] {Person.class},
new DynamicProxy(singer));//获得动态代理
proxy.sing();
proxy.talk();
}
}
// 输出结果
// 开始执行代理
// 大河向东流。。
// 开始执行代理
// 进行访谈
14.8 空对象
在每次使用引用时必须测试其是否为null,这显得很麻烦
所有Java引入的空对象,该对象可以返回一个有效的值,这样就不需要每次进行判断了。
只需要自己定义一个名字为Null的接口,认为其为空对象。
interface Null{}// 定义一个NULL接口
class Person{
String name;
int age;
// 定义一个空对象的内部类
public static class NullPerson extends Person implements Null{
private NullPerson() {
super();
}
}
public static final Person NULL = new NullPerson();
}
14.9 接口与类型信息
interface允许程序员隔离构建,降低耦合性。
但是接口对于解耦还是有一些缺陷。
interface A{
void f();
}
class C implements A{
@Override
public void f() {}
public void g() {}
}
public class Test3 {
public static void main(String[] args) {
A a = new C();
a.f();
//a.g();//编译错误
}
}
虽然a是按照C这个对象进行创建的,但是却用不了g()方法。
14.10 总结
面向对象编程语言的目的是尽量使用多态,只在必需的时候使用RTTI