JavaBase:类型信息

类型(Class)信息

运行时类型信息使得你可以在程序运行时发现和使用类型信息。他使你从只能在编译期执行面向类型的操作的禁锢中解脱了出来,并且可以使用某些非常强大的程序。

Java是如何让我们在运行时识别对象和类的信息的

  • 传统的RTTI,假定我们在编译时已经知道了所有的类型
  • 反射机制,允许我们在运行时发现和使用类的信息。

RTTI的形式包括:

  • 传统的类型转换,保证类型转换的正确性,如果执行了一个错误的转换,抛出ClassCastException
  • 代表对象的类型的Class对象,通过查询Class对象可以获取运行时所需的信息
  • instanceof,返回一个布尔值,告诉我们对象是不是某个特定类型的实例。

为什么需要RTTI

RTTI:Run-Time Type Identification运行时类型信息,通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用对象的实际派生类型。

考虑一个常见的多态场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class Shapes {
void draw() {
//传递this,间接使用toString打印类标识符。
System.out.println(this + ".draw");
}
//强制子类重写该方法
@Override
abstract public String toString();
}
public class Circle extends Shapes {
@Override
public String toString() {
return "Circle";
}
}
public class Squre extends Shapes {

@Override
public String toString() {
return "Shape";
}
}

public static void main(String[] args) {
List<Shapes> shapeList = Arrays.asList(new Circle(), new Squre());
for (Shapes shape : shapeList) {
shape.draw();
}
}

在这个例子中,当将Shape放入到List会向上转型,但向上转型的过程中也丢失了Shape对象的具体类型,对于List而言,只是Shape类对象

当从数组中取出对象,List是将所有的事物都当作Object持有,会自动将结果转型回Shape,这是RTTI最基本的使用,即在运行时识别对象的类型。在Java中所有的类型转换都是在运行时进行正确性检查的。

应用

用于泛型,在编译期,我们可以知道的是List内包含的是Shape对象,因此RTTI可以完成转换,即符合假定我们在编译时已经知道了所有的类型。

用于类型指定,RTTI可以查询某个Shape引用所指向的对象的确切类型,然后选择或者剔除特例

Class对象

Class对象表示着类型信息在运行时的表现,它包含了与类有关的信息,并且Class对象就是用来创建类的所有常规对象的。Java使用Class对象来执行其RTTI,

创建对象

类是程序的一部分,每个类有一个Class对象,每当编译一个新类,则会产生一个Class对象(保存在.class文件中),为了生成类的对象,JVM将使用类加载器进行加载,具体加载过程不展开描述了,可以去看JVM类加载器相关。

运用类型信息

无论何时想要在运行时使用类型信息,就必须先获得对恰当Class对象的引用,Class.forName是一个合适的方法,因为不必为了获得Class引用而持有该类型的对象。

层次结构

1
2
3
4
5
public final class Class<T> implements 
java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement

类字面常量

例如:UserInfo.class。这样做不仅更简单,而且更安全,因为在编译时就能受到检查,因此不需要try catch,并且根除了对forName的调用,更高效。

可以应用与普通类、接口、数组、基本数据类型。对于基本数据类型的包装类型,有一个标准字段TYPE,是一个指向对应基本数据类型的Clas对象。

1
2
boolean.class == Boolean.TYPE
int.class == Integer.TYPE

创建Class对象的引用

使用.class创建对class对象的引用时,不会自动初始化Class对象,初始化被延迟到了对静态方法或非常数静态域进行首次引用。即初始化有效地实现了尽可能惰性。

如果一个static&final值是编译器常量,即=47,那么这个值不需要对类进行初始化就可以立即读取,如果是static|final则需要初始化。

对比forName

forName在获得类的引用时,会立即进行初始化,而.class方法获得引用时,不会立即初始化。

泛化的Class引用

在Java SE5中将Class的类型变的更具体了一些,即允许对Class引用所指向的CLass对象的类型进行限定而实现,即使用泛型,

1
2
3
Class<Integer> genericIntClass = Integer.class;
//报警
genericIntClass = double.class

普通的类引用可以被重新赋值指向任何其他的Class对象,通过使用泛型,可以让编译器强制执行额外的检测

为了在使用泛化的Class引用时放松限制,则需要使用通配符”?”,并且可以使用extends进行一定程度的限制

1
2
3
4
5
Class<?> genericIntClass = Integer.class;
Class<? extends Number> genericIntClass = Integer.class;
genericIntClass = double.class;
//报警
genericIntClass = User.class;

获得Class对象

  • Class.forName(String name),一般使用类字面常量。

常见的应用场景为:在 JDBC 开发中常用此方法加载数据库驱动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package io.github.dunwu.javacore.reflect;

public class ReflectClassDemo01 {
public static void main(String[] args) throws ClassNotFoundException {
Class c1 = Class.forName("io.github.dunwu.javacore.reflect.ReflectClassDemo01");
System.out.println(c1.getCanonicalName());

Class c2 = Class.forName("[D");
System.out.println(c2.getCanonicalName());

Class c3 = Class.forName("[[Ljava.lang.String;");
System.out.println(c3.getCanonicalName());
}
}
//Output:
//io.github.dunwu.javacore.reflect.ReflectClassDemo01
//double[]
//java.lang.String[][]
  • Object.getClass()

Object 类中有 getClass 方法,因为所有类都继承 Object 类。从而调用 Object 类来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.github.dunwu.javacore.reflect;

import java.util.HashSet;
import java.util.Set;

public class ReflectClassDemo03 {
enum E {A, B}

public static void main(String[] args) {
Class c = "foo".getClass();
System.out.println(c.getCanonicalName());

Class c2 = ReflectClassDemo03.E.A.getClass();
System.out.println(c2.getCanonicalName());

byte[] bytes = new byte[1024];
Class c3 = bytes.getClass();
System.out.println(c3.getCanonicalName());

Set<String> set = new HashSet<>();
Class c4 = set.getClass();
System.out.println(c4.getCanonicalName());
}
}
//Output:
//java.lang.String
//io.github.dunwu.javacore.reflect.ReflectClassDemo.E
//byte[]
//java.util.HashSet
  • 类字面常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ReflectClassDemo02 {
public static void main(String[] args) {
boolean b;
// Class c = b.getClass(); // 编译错误
Class c1 = boolean.class;
System.out.println(c1.getCanonicalName());

Class c2 = java.io.PrintStream.class;
System.out.println(c2.getCanonicalName());

Class c3 = int[][][].class;
System.out.println(c3.getCanonicalName());
}
}
//Output:
//boolean
//java.io.PrintStream
//int[][][]

获得Class

Class.forName(String)

该方法是Class类的一个static成员,forName()是取得Class引用的一种方法,其参数为目标类的文本名称(需要包括包名称),返回一个Class对象的引用。

如果找不到需要加载的类,则会抛出异常ClassNotFoundException

ClassObj.getSuperClass()

获得Clss对象的直接基类

ClassObj.getInterfaces()

返回Class对象,表示在感兴趣的Class对象中包含的接口

类型转换

Class.cast(Object)

接受参数对象,并转型为Class引用的类型。对于无法使用普通转型的情况比较有用。例如有时存储了Class引用,并希望以后通过这个引用来执行转型

1
2
3
Building b = new House();
Class<House> houseClass = House.class;
House h = houseClass.cast(b);

Class.isInstance(Object)

接收一个对象参数,判断该对象是否属于该Class,返回boolean值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InstanceofDemo {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
if (arrayList instanceof List) {
System.out.println("ArrayList is List");
}
if (List.class.isInstance(arrayList)) {
System.out.println("ArrayList is List");
}
}
}
//Output:
//ArrayList is List
//ArrayList is List

创建实例

Class对象创建实例对象主要有两种方式

  • Class 对象的 newInstance 方法。
  • Constructor 对象的 newInstance 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NewInstanceDemo {
public static void main(String[] args)
throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> c1 = StringBuilder.class;
StringBuilder sb = (StringBuilder) c1.newInstance();
sb.append("aaa");
System.out.println(sb.toString());

//获取String所对应的Class对象
Class<?> c2 = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c2.getConstructor(String.class);
//根据构造器创建实例
String str2 = (String) constructor.newInstance("bbb");
System.out.println(str2);
}
}
//Output:
//aaa
//bbb

ClassObj.newInstance()

是实现虚拟构造器的一种途径。虚拟构造器允许你声明“我不知道你的确切类型,但是无论如何要正确地创建你自己”。

返回的类型为Class,当创建新的实例时,会得到Object的引用,但是这个引用指向的是具体类型的对象,因此需要转型。

使用newInstance()创建新类必须带有默认的构造器(无参)

Field

Class 对象提供以下方法获取对象的成员(Field):

  • getFiled - 根据名称获取公有的(public)类成员。
  • getDeclaredField - 根据名称获取已声明的类成员。但不能得到其父类的类成员。
  • getFields - 获取所有公有的(public)类成员。
  • getDeclaredFields - 获取所有已声明的类成员。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ReflectFieldDemo {
class FieldSpy<T> {
public boolean[][] b = {{false, false}, {true, true}};
public String name = "Alice";
public List<Integer> list;
public T val;
}

public static void main(String[] args) throws NoSuchFieldException {
Field f1 = FieldSpy.class.getField("b");
System.out.format("Type: %s%n", f1.getType());

Field f2 = FieldSpy.class.getField("name");
System.out.format("Type: %s%n", f2.getType());

Field f3 = FieldSpy.class.getField("list");
System.out.format("Type: %s%n", f3.getType());

Field f4 = FieldSpy.class.getField("val");
System.out.format("Type: %s%n", f4.getType());
}
}
//Output:
//Type: class [[Z
//Type: class java.lang.String
//Type: interface java.util.List
//Type: class java.lang.Object

Method

Class 对象提供以下方法获取对象的方法(Method):

  • getMethod - 返回类或接口的特定方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
  • getDeclaredMethod - 返回类或接口的特定声明方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
  • getMethods - 返回类或接口的所有 public 方法,包括其父类的 public 方法。
  • getDeclaredMethods - 返回类或接口声明的所有方法,包括 public、protected、默认(包)访问和 private 方法,但不包括继承的方法。

获取一个 Method 对象后,可以用 invoke 方法来调用这个方法。

invoke 方法的原型为:

1
2
3
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ReflectMethodDemo {
public static void main(String[] args)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

// 返回所有方法
Method[] methods1 = System.class.getDeclaredMethods();
System.out.println("System getDeclaredMethods 清单(数量 = " + methods1.length + "):");
for (Method m : methods1) {
System.out.println(m);
}

// 返回所有 public 方法
Method[] methods2 = System.class.getMethods();
System.out.println("System getMethods 清单(数量 = " + methods2.length + "):");
for (Method m : methods2) {
System.out.println(m);
}

// 利用 Method 的 invoke 方法调用 System.currentTimeMillis()
Method method = System.class.getMethod("currentTimeMillis");
System.out.println(method);
System.out.println(method.invoke(null));
}
}

Constructor

Class 对象提供以下方法获取对象的构造方法(Constructor):

  • getConstructor - 返回类的特定 public 构造方法。参数为方法参数对应 Class 的对象。
  • getDeclaredConstructor - 返回类的特定构造方法。参数为方法参数对应 Class 的对象。
  • getConstructors - 返回类的所有 public 构造方法。
  • getDeclaredConstructors - 返回类的所有构造方法。

获取一个 Constructor 对象后,可以用 newInstance 方法来创建类实例。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ReflectMethodConstructorDemo {
public static void main(String[] args)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<?>[] constructors1 = String.class.getDeclaredConstructors();
System.out.println("String getDeclaredConstructors 清单(数量 = " + constructors1.length + "):");
for (Constructor c : constructors1) {
System.out.println(c);
}

Constructor<?>[] constructors2 = String.class.getConstructors();
System.out.println("String getConstructors 清单(数量 = " + constructors2.length + "):");
for (Constructor c : constructors2) {
System.out.println(c);
}

System.out.println("====================");
Constructor constructor = String.class.getConstructor(String.class);
System.out.println(constructor);
String str = (String) constructor.newInstance("bbb");
System.out.println(str);
}
}

Array

数组在 Java 里是比较特殊的一种类型,它可以赋值给一个对象引用。下面我们看一看利用反射创建数组的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ReflectArrayDemo {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls, 25);
//往数组里添加内容
Array.set(array, 0, "Scala");
Array.set(array, 1, "Java");
Array.set(array, 2, "Groovy");
Array.set(array, 3, "Scala");
Array.set(array, 4, "Clojure");
//获取某一项的内容
System.out.println(Array.get(array, 3));
}
}
//Output:
//Scala

其中的 Array 类为 java.lang.reflect.Array 类。我们通过 Array.newInstance 创建数组对象,它的原型是:

1
2
3
4
public static Object newInstance(Class<?> componentType, int length)
throws NegativeArraySizeException {
return newArray(componentType, length);
}

Method对象

java.lang.reflect.Method提供有关类或接口上单个方法的信息和访问权限。反映的方法可以是类方法或实例方法(包括抽象方法)

层次结构

public final class Method extends Executable

Annotation

  • T getAnnotation(Class annotationClass)。如果存在这样的注解则返回该元素的指定类型的注解,否则返回Null
  • Annotation[] getAnnotation()。返回此元素上直接存在的所有注释

Field对象

层次结构

public final class Field extends AccessibleObject implements Member

Parameter对象

层次结构

public final class Parameter implements AnnotatedElement

Constructor对象

层次结构

public final class Constructor<T> extends Executable

公共接口

  • abstract Class Executable
    • Interface Member
    • Interface GenericDeclaration
    • Class AccessibleObject
      • Interface AnnotatedElement
  • Interface Type

类型转换前先检查

  • 关键字instanceof返回一个布尔值,告诉我们对象是不是某个特定类型的实例
1
if(object instanceof Dog)

若不使用instanceof,并且object并不是Dog类型,则会得到ClassCastException异常

  • Class.isInstance()方法,(Native方法)

动态的instanceof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class PetCount3 {
static class PetCounter extends LinkedHashMap<Class<? extends Pet>, Integer> {
public PetCounter() {
//加载LiteralPetCreator.allTypes里所有的类型,当新增一个类型也只需要在List里面新增值
//MapData接收一个list作为键,0作为值构造一个Map
super(MapData.map(LiteralPetCreator.allTypes, 0));
}

public void count(Pet pet) {
// Class.isInstance() eliminates instanceofs:
for (Map.Entry<Class<? extends Pet>, Integer> pair : entrySet()) {
//IsInstance判断是否属于该类型
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());
result.append("=");
result.append(pair.getValue());
result.append(", ");
}
result.delete(result.length() - 2, result.length());
result.append("}");
return result.toString();
}
}

public static void main(String[] args) {
PetCounter petCount = new PetCounter();
for (Pet pet : Pets.createArray(20)) {
printnb(pet.getClass().getSimpleName() + " ");
petCount.count(pet);
}
print();
print(petCount);
}
}

instanceof与Class的等价性

在查询类型信息时,instanceof与直接比较Class对象有一个重要差别

  • instanceof保持了类型的概念,指的是该对象是这个类吗,或者是这个类的派生类吗。
  • 比较Class对象,则没有考虑继承问题,即它是这个确切的类吗

接口与类型信息

interface的一种重要目标就是允许程序员隔离构件,进而降低耦合性,但是通过类型信息,这种耦合性还是会传播出去,即接口并非是对解耦的无懈可击的保障。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
// a.g(); // Compile error
System.out.println(a.getClass().getName());
if(a instanceof B) {
B b = (B)a;
b.g();
}
}
} /* Output:
B
*///:~

通过RTTI,a是被当做B实现的,通过将a转型为B,可以调用不在A中的方法。但是这种方式给了一个机会即代码耦合度很高。

解决的办法即使用包访问权限,其中只有HiddenC是public的,在调用时产生一个A接口类型的对象,虽然返回的是C类型,但是在包外部不能使用A外的任何方法,因为无法在包外部命名C

1
2
3
4
5
6
7
8
9
10
11
12
//包访问权限
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(); }
} ///:~

但是即使RTTI无效,利用反射依然可以到达并调用所有方法,即使是private方法。

总结

  • 不使用多态的时候,很容易出现一系列的switch语句,而使用RTTI可以做到简洁,但是会损失多态机制的重要价值
  • 使用多态机制的方法调用,要求我们有基类定义的控制权,而当我们扩展的基类并没有包含我们想要的方法,这时RTTI是一种解决之道,你可以检查自己特定类型然后调用自己的方法。因此不需要switch语句。

参考