我是靠谱客的博主 文艺茉莉,这篇文章主要介绍重拾Java基础知识:类型信息前言,现在分享给大家,希望可以做个参考。

类型信息

  • 前言
    • 为什么需要 RTTI
    • Class 对象
    • 类型转换检测
    • 类的等价比较
    • 反射:运行时类信息
      • 类方法提取器
    • 动态代理
    • 接口和类型
    • 本章小结

前言

RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息

RTTI 把我们从只能在编译期进行面向类型操作的禁锢中解脱了出来,并且让我们可以使用某些非常强大的程序。对 RTTI 的需要,揭示了面向对象设计中许多有趣(并且复杂)的特性,同时也带来了关于如何组织程序的基本问题。

为什么需要 RTTI

下面看一下我们已经很熟悉的一个例子,它使用了多态的类层次结构。

public abstract class A {
    void obj(){
        System.out.println(this+".obj()");
    }
    @Override
    public abstract String toString();

    public static void main(String[] args) {
        Stream.of(new B(),new C()).forEach(A::obj);
    }
    /** Output:
     *  B.obj()
     *  A.obj()
     */
}
public class B extends A{
    @Override
    public String toString() {
        return "B";
    }
}
public class C extends A{
    @Override
    public String toString() {
        return "A";
    }
}

这个例子中,在把A对象放入 Stream 中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对 Stream而言,它们只是A对象。严格来说,Stream 实际上是把放入其中的所有对象都当做 Object 对象来持有,只是取元素时会自动将其类型转为 A。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型。

  • 编译期,Stream 和 Java 泛型系统确保放入 Stream 的都是 A对象(A子类的对象也可视为 A的对象),否则编译器会报错;
  • 运行时,自动类型转换确保了从 Stream 中取出的对象都是 A类型。

接下来就是多态机制的事了。这也符合我们编写代码的一般需求,通常,我们希望大部分代码尽可能少了解对象的具体类型,而是只与对象家族中的一个通用表示打交道。这样,代码会更容易写,更易读和维护;设计也更容易实现,更易于理解和修改。所以多态是面向对象的基本目标。

Class 对象

类是程序的一部分,每个类都有一个 Class 对象。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用"类加载器"子系统把这个类加载到内存中。类加载器子系统可能包含一条类加载器链,但有且只有一个原生类加载器,它是 JVM 实现的一部分。原生类加载器加载的是”可信类”(包括 Java API 类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持 Web 服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。

所有的类都是第一次使用时动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。

其实构造器也是类的静态方法,虽然构造器前面并没有 static 关键字。所以,使用 new 操作符创建类的新对象,这个操作也算作对类的静态成员引用。

类加载器首先会检查这个类的 Class 对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找 .class 文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM 会对其进行验证,确保它没有损坏,并且不包含不良的 Java 代码(这是 Java 安全防范的一种措施)。

public class A {
    static {
        System.out.println("A");
    }

    public static void main(String[] args) throws ClassNotFoundException {
		Class aClass=A.class;
        new A();
        Class.forName("com.test.B");
        new C();
    }
    /** Output:
     *  A
     *  B
     *  C
     */
}
public class B {
    static {
        System.out.println("B");
    }
}
public class C {
    static {
        System.out.println("C");
    }
}

上面的代码中,这几个类都有一个 static{...} 静态初始化块,这些静态初始化块在类第一次被加载的时候就会执行。当使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。其它,静态初始化块会打印出相应的信息,告诉我们这些类分别是什么时候被加载了。代码中通过目标类的类名调用Class.forName()的静态方法,得到该类的 Class 对象。还需要注意的是,如果找不到要加载的类,它就会抛出异常 ClassNotFoundException。

如果你已经拥有了目标类的对象,那就可以通过调用 getClass() 方法来获取 Class 引用了,这个方法来自根类 Object,它将返回表示该对象实际类型的 Class 对象的引用。Class 包含很多有用的方法,下面代码展示了其中的一部分:

public class A {
    public static void main(String[] args) throws ClassNotFoundException {
        Class aClass = Class.forName("com.test.A");
        //获取名称
        System.out.println("name: "+aClass.getName());
        //获取完整名称
        System.out.println("canonicalName: "+aClass.getCanonicalName());
        //是否是一个接口
        System.out.println("isInterface: "+aClass.isInterface());
        //获取父类
        System.out.println("superClass: "+aClass.getSuperclass());
    }
    /** Output:
     *  name: com.test.A
     *  canonicalName: com.test.A
     *  isInterface: false
     *  superClass: class java.lang.Object
     */
}

Java 引入泛型语法之后,我们可以使用泛型对 Class 引用所指向的 Class 对象的类型进行限定。向 Class 引用添加泛型语法的原因只是为了提供编译期类型检查,因此如果你操作有误,稍后就会发现这点。使用普通的 Class 引用你要确保自己不会犯错,因为一旦你犯了错误,就要等到运行时才能发现它,很不方便。

public class A {
    public static void main(String[] args){
        Class<A> aClass= A.class;
        try {
            A a = aClass.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

当你将泛型语法用于 Class 对象时,newInstance() 方法将返回该对象的确切类型。

除此之外Class 引用的转型语法,即 cast() 方法:

public class A {
    public static void main(String[] args) {
        Class<A> aClass = A.class;
        B cast = (B) aClass.cast(new B());
    }
}
public class B extends A{
}

Java 类库中另一个没有任何用处的特性就是 Class.asSubclass(),该方法允许你将一个 Class 对象转型为更加具体的类型。

类型转换检测

将 B转换为 A是一次向上转型, 将 A转换为 B是一次向下转型。但是, 因为我们知道 B肯定是一个 A,所以编译器允许我们自由地做向上转型的赋值操作,且不需要任何显式的转型操作。当你给编译器一个 A的时候,编译器并不知道它到底是什么类型的 A——它可能是 A,也可能是 A的子类型,例如 A、B、C或某种其他的类型。在编译期,编译器只能知道它是 A。因此,你需要使用显式地进行类型转换,以告知编译器你想转换的特定类型,否则编译器就不允许你执行向下转型赋值。 (编译器将会检查向下转型是否合理,因此它不允许向下转型到实际不是待转型类型的子类类型上)。

RTTI 在 Java 中还有第三种形式,那就是关键字 instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,可以用提问的方式使用它,就像这个样子:

public class A {
    public static void main(String[] args) {
        A a = new B();
        System.out.println(a instanceof A);
        /** Output:
         *  true
         */
    }
}

如果没有其他信息可以告诉你这个对象是什么类型,那么使用 instanceof 是非常重要的,否则会得到一个 ClassCastException 异常。

类的等价比较

当你查询类型信息时,需要注意:instanceof 的形式(即 instanceof 或 isInstance() ,这两者产生的结果相同) 和 与 Class 对象直接比较 这两者间存在重要区别。下面的例子展示了这种区别:

public class A {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        System.out.println("instanceof:"+ (b instanceof A));
        System.out.println("isInstance:"+B.class.isInstance(b));
        System.out.println("==:"+(a.getClass() == A.class));
        System.out.println("equals():"+a.getClass().equals(A.class));
        /** Output:
         *  instanceof:true
         *  isInstance:true
         *  ==:true
         *  equals():true
         */
    }
}

instanceof 和 isInstance() 产生的结果相同, equals() 和 == 产生的结果也相同。但测试本身得出了不同的结论。与类型的概念一致,instanceof 说的是“你是这个类,还是从这个类派生的类?”。而如果使用 == 比较实际的Class 对象,则与继承无关 —— 它要么是确切的类型,要么不是。

反射:运行时类信息

类 Class 支持反射的概念, java.lang.reflect 库中包含类 Field、Method 和 Constructor(每一个都实现了 Member 接口)。这些类型的对象由 JVM 在运行时创建,以表示未知类中的对应成员。然后,可以使用 Constructor 创建新对象,get() 和 set() 方法读取和修改与 Field 对象关联的字段,invoke() 方法调用与 Method 对象关联的方法。此外,还可以调用便利方法 getFields()、getMethods()、getConstructors() 等,以返回表示字段、方法和构造函数的对象数组。(你可以通过在 JDK 文档中查找类 Class 来了解更多信息。)因此,匿名对象的类信息可以在运行时完全确定,编译时不需要知道任何信息。

重要的是要意识到反射没有什么魔力。当你使用反射与未知类型的对象交互时,JVM 将查看该对象,并看到它属于特定的类(就像普通的 RTTI)。在对其执行任何操作之前,必须加载 Class 对象。因此,该特定类型的 .class 文件必须在本地计算机上或通过网络对 JVM 仍然可用。因此,RTTI 和反射的真正区别在于,使用 RTTI 时,编译器在编译时会打开并检查 .class 文件。换句话说,你可以用“正常”的方式调用一个对象的所有方法。通过反射,.class 文件在编译时不可用;它由运行时环境打开并检查。

类方法提取器

通常,你不会直接使用反射工具,但它们可以帮助你创建更多的动态代码。反射提供了一种方法,可以简单地编写一个工具类自动地向你展示所有的接口:

public class A {
    public static void main(String[] args) throws ClassNotFoundException {
        Class aClass = Class.forName("com.test.A");
        Method[] methods = aClass.getMethods();
        for (Method m:methods) {
            System.out.println(m.toString());
        }
        System.out.println("==============================");
        Constructor[] constructors = aClass.getConstructors();
        for (Constructor c:constructors) {
            System.out.println(c.toString());
        }
    }
    public void method(){
        System.out.println("method");
    }
    public String method2(){
        return "method2";
    }
    /** Output:
     *  public static void com.test.A.main(java.lang.String[]) throws java.lang.ClassNotFoundException
     * public void com.test.A.method()
     * public java.lang.String com.test.A.method2()
     * 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.A()
     */
}

Class 方法 getmethods() 和 getconstructors() 分别返回 Method 数组和 Constructor 数组。这些类中的每一个都有进一步的方法来解析它们所表示的方法的名称、参数和返回值。编译时无法知道 Class.forName() 生成的结果,因此所有方法签名信息都是在运行时提取的。

动态代理

代理是基本的设计模式之一。一个对象封装真实对象,代替其提供其他或不同的操作—这些操作通常涉及到与“真实”对象的通信,因此代理通常充当中间对象。先来讲解下什么是静态代理,代码如下:

public interface Book {
    public void write();
}
public class MathBook implements Book {
    @Override
    public void write() {
        System.out.println("write MathBook");
    }
}
public class ChineseBook implements Book {
    @Override
    public void write() {
        System.out.println("write ChineseBook");
    }
}
public class BookProxy implements Book {
    private MathBook mathBook = new MathBook();
    @Override
    public void write() {
        mathBook.write();
    }
}
public class TestBook {
    public static void main(String[] args) {
        BookProxy bookProxy = new BookProxy();
        bookProxy.write();
        /** Output:
         *  write MathBook
         */
    }
}

Java 的动态代理更进一步,不仅动态创建代理对象而且动态处理对代理方法的调用。在动态代理上进行的所有调用都被重定向到单个调用处理程序,该处理程序负责发现调用的内容并决定如何处理。

public interface Book {
    public void write();
}
public class MathBook implements Book {
    @Override
    public void write() {
        System.out.println("write MathBook");
    }
}
public class ChineseBook implements Book {
    @Override
    public void write() {
        System.out.println("write ChineseBook");
    }
}
public class BookProxy implements InvocationHandler {

    private Object object;

    public Object bind(Object o) {
        this.object = o;
        return Proxy.newProxyInstance(o.getClass().getClassLoader(), 
                o.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(object, args);
    }
}
public class TestBook {
    public static void main(String[] args) {
        BookProxy bookProxy = new BookProxy();
        Book book = (Book) bookProxy.bind(new MathBook());
        book.write();
        /** Output:
         *  write MathBook
         */
    }
}

通过调用静态方法 Proxy.newProxyInstance() 来创建动态代理。Proxy.newProxyInstance()方法有三个参数:第一个使用类装入器来定义代理类、第二个代理类的所有接口、第三个当前对象即实现了InvocationHandler接口的类的对象,在调用方法时会调用它的invoke()方法。invoke() 方法被传递给代理对象,以防万一你必须区分请求的来源—但是在很多情况下都无需关心。但是,在 invoke() 内的代理上调用方法时要小心,因为接口的调用是通过代理重定向的。通常执行代理操作,然后使用 Method.invoke() 将请求转发给被代理对象,并携带必要的参数。

接口和类型

interface 关键字的一个重要目标就是允许程序员隔离组件,进而降低耦合度。使用接口可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障。比如我们先写一个接口:

public interface A {
    void method();
}
public class B implements A {
    @Override
    public void method() {

    }
    public void bMethod(){

    }
}
public class Test {
    public static void main(String[] args) {
        A a = new B();
        System.out.println(a.getClass().getSimpleName());
        System.out.println(a instanceof B);
        /** Output:
         *  B
         *  true
         */
    }
}

通过使用 RTTI,我们发现 a 是用 B 实现的。这样的操作完全是合情合理的,但是你也许并不想让客户端开发者这么做,因为这给了他们一个机会,使得他们的代码与你的代码的耦合度超过了你的预期。也就是说,你可能认为 interface 关键字正在保护你,但其实并没有。

public interface A {
    void method();
}
public class B implements A {
    @Override
    public void method() {

    }
    public void bMethod(){
        System.out.println("bMethod");
    }
}
public class Test {
    public static void main(String[] args){
        A a = new B();
        Method g = null;
        try {
            g = a.getClass().getDeclaredMethod("bMethod");
            g.setAccessible(false);
            g.invoke(a);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        /** Output:
         *  bMethod
         */
    }
}

正如你所看到的,通过使用反射,仍然可以调用所有方法。如果知道方法名,你就可以在其 Method 对象上调用 setAccessible(true)

通常,所有这些违反访问权限的操作并不是什么十恶不赦的。总之,不可否认,反射给我们带来了很多好处。

程序员往往对编程语言提供的访问控制过于自信,甚至认为 Java 在安全性上比其它提供了(明显)更宽松的访问控制的语言要优越。然而,正如你所看到的,事实并不是这样。

本章小结

最后一点,RTTI 有时候也能解决效率问题。假设你的代码运用了多态,但是为了实现多态,导致其中某个对象的效率非常低。这时候,你就可以挑出那个类,使用 RTTI 为它编写一段特别的代码以提高效率。然而必须注意的是,不要太早地关注程序的效率问题,这是个诱人的陷阱。最好先让程序能跑起来,然后再去看看程序能不能跑得更快,下一步才是去解决效率问题。

我们已经看到,反射,因其更加动态的编程风格,为我们开创了编程的新世界。但对有些人来说,反射的动态特性却是一种困扰。对那些已经习惯于静态类型检查的安全性的人来说,Java 中允许这种动态类型检查(只在运行时才能检查到,并以异常的形式上报检查结果)的操作似乎是一种错误的方向。有些人想得更远,他们认为引入运行时异常本身就是一种指示,指示我们应该避免这种代码。我发现这种意义的安全是一种错觉,因为总是有些事情是在运行时才发生并抛出异常的,即使是在那些不包含任何 try 语句块或异常声明的程序中也是如此。因此,我认为一致性错误报告模型的存在使我们能够通过使用反射编写动态代码。当然,尽力编写能够进行静态检查的代码是有价值的,只要你有这样的能力。但是我相信动态代码是将 Java 与其它诸如 C++ 这样的语言区分开的重要工具之一。

最后

以上就是文艺茉莉最近收集整理的关于重拾Java基础知识:类型信息前言的全部内容,更多相关重拾Java基础知识内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(267)

评论列表共有 0 条评论

立即
投稿
返回
顶部