资讯专栏INFORMATION COLUMN

《Java编程思想》笔记14.类型信息

Hwg / 2784人阅读

摘要:接口与类型信息关键字的一种重要目标就是允许程序员隔离构件,进而降低耦合性。如果你编写接口,那么就可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去接口并非是对解耦的一种无懈可击的保障。

点击进入我的博客

运行时类型信息使得你可以在运行时发现和使用类型信息,主要有两种方式:

“传统的”RTTI,它假定我们在编译时已经知道了所有的类型;

“反射”机制,它允许我们在运行时发现和使用类的信息。

14.1 为什么需要RTTI

RTTI维护类型类型的信息,为多态机制的实现提供基础。

14.2 Class对象

类型信息在运行时是通过Class对象来表示的,完成的Class对象包含了与类有关的信息。Class对象就是用来创建所有“常规”对象的,Java使用Class对象来执行RTTI
类是程序的一部分,每个类都有一个Class对象,被保存在一个同名的.class文件中。

类加载器

类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的是可信类,包括Java API类。

所有类都是在对其第一次使用(静态成员或new对象)时,动态加载到JVM的。

Class对象仅在需要的时候才会加载,static初始化是在类加载时进行的。

类加载器首先会检查这个类的Class对象是否已被加载过,如果尚未加载,默认的类加载器就会根据类名查找对应的.class文件。

Class类方法

想在运行时使用类型信息,必须获取对象的Class对象的引用:Class.forName("s2.A");该方法会自动初始化该Class对象,注意必须使用全限定名(包含包名)

// 获取类名
clz.getSimpleName()
// 获取全限定名
clz.getCanonicalName()
// 获取接口
clz.getInterfaces();
// 获取父类
clz.getSuperClass();
// 创建该类对象
clz.newInstance();
14.2.1 类字面常量

Java还提供了类字面常量的方式来生成对Class对象的引用:Class clz = A.class。注意这种方式不会自动初始化该Class对象

基本类型

类字面常量不仅可以用于普通的类,还可以用于接口、数组(int[].class)基本数据类型(int.class)

基本类型的包装类,都有一个标准字段TYPE,这是一个指向对应基本数据类型Class对象的引用:如public static final Class TYPE = (Class) Class.getPrimitiveClass("int");

类的准备过程

加载:由类加载器完成,该步骤查找对应的字节码,创建一个Class对象

链接:验证类中的字节码,为静态域分配空间;并且如果必须的话,将解析这个类创建的对其他类的所有引用。

初始化:如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块

初始化的惰性

使用Class.forName()会自动初始化;使用A.class不会自动初始化类

编译期常量:static final int i = 1;,则不需要初始化就可以被读取。

如果只是将一个域设置为static final不足以保证是编译器常量,如static final int ii = new Random().nextInt();

如果一个static域不是final的,那么访问之前要先进行链接和初始化。

14.2.2 泛化的Class引用

Java SE5之后,Class也可以支持范型了。
向Class引用添加范型语法的原因仅仅是为了提供编译期类型检查。

14.2.3 新的转型语法

cast()方法接受参数对象,将其转型为Class引用的类型。

Class.asSubclass(),该方法允许你讲一个类对象转型为更加具体的类型。

    Class clz = String.class;
    String str1 = clz.cast("");
14.3 类型转换前先做检查

Java提供类instanceOf关键字,可以判断对象是否是某个类(及其父类)的实例。

clz.isInstance()方法接受一个对象,判断该对象是否是该clz指向的类的实例。

clz.isAssignableFrom()方法接受一个Class对象,判断该Class对象是否是clz自身或子类。

public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println(A.class.isAssignableFrom(C.class));
        System.out.println(B.class.isAssignableFrom(C.class));
        System.out.println(A.class.isInstance(new C()));
        System.out.println(B.class.isInstance(new C()));
    }
}

class A { }
interface B {}
class C extends A implements B {}
// Output:
// true
// true
// true
// true
14.4 注册工厂

使用工厂方法设计模式, 将对象的创建工作交给类自己去完成。 工厂方法可以被多态地调用, 从而为你创建恰当类型的对象。

14.5 instanceOf和Class的等价性

instanceOf和isInstance()的结果完全一样,比较的时候都考虑了继承关系

A.class.equals(B.class) 和 A.class == B.class 只能比较是否为同一个类,没有考虑继承关系

14.6 反射:运行时的类信息
RTTI的限制

如果不知道某个对象的确切类型,RTTI可以告诉你,但这有个限制:这个类型在编译时必须已知。换句话说,编译器在编译时必须知道所有要通过RTTI来处理的类。

假设你获取了一个指向某个并不在你程序空间中对象的引用,在编译时你的程序根本无法获知这个对象所属的类。

运行时获取类的信息场景:基于构件的编程、远程方法调用(RMI)。

反射机制并没有什么神奇之处

当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类(就像RTTI那样)。

在用它做其他事情之前必须先加载那个类的Class对象。因此,那个类的.class文件对于JVM来说必须是可获取的:要么在本地机器上,要么可以通过网络取得。

所以RTTI和反射之间真正的区别只在于,对RTTI来说,编译器在编译时打开和检查.class文件;而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查class文件。

反射的作用

反射在Java中是用来支持其他特性的,例如对象序列化和JavaBean。

14.7 动态代理

代理是基本的设计模式之一,它是为你提供额外的或者不同的操作,而插入的用来代替“实际”对象的对象。这些操作通常设计与“实际”对象的通信,因此代理通常充当着中间人的角色。

静态代理

静态代理就是写死了在代理对象中执行这个方法前后执行添加功能的形式。

优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点:我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。

public class Test {
    public static void main(String[] args) throws Exception {
        new RealObject().doSomething();
        System.out.println("代理之后:");
        new SimpleProxy(new RealObject()).doSomething();
    }
}

interface MyInterface {
    void doSomething();
}

class RealObject implements MyInterface {
    @Override
    public void doSomething() {
        System.out.println("RealObject");
    }
}

class SimpleProxy implements MyInterface {
    private MyInterface myInterface;

    public SimpleProxy(MyInterface myInterface) {
        this.myInterface = myInterface;
    }

    // 代理后增加方法
    @Override
    public void doSomething() {
        System.out.println("SimpleProxy");
        myInterface.doSomething();
    }
}
动态代理

Java的动态代理比代理的思想更向前迈进了一步, 因为它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上。

通过Proxy.newProxyInstance()可以创建动态代理,需要一个类加载器(通常是被加载的对象获取)、一个希望实现的接口列表(不是类或抽象类)、以及InvocationHandler的一个实现。

动态代理可以将所有对接口的调用重定向为对代理的调用。

使用动态代理来编写一个系统以实现事务,其中,代理在被代理的调用执行成功(不抛出任何异常)执行提交,而在执行失败时执行回滚。你的提交和回滚都针对一个外部的文本文件,该文件不在Java异常的控制范围之内。你必须注意操作的原子性

    MyInterface myInterface = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(), new Class[]{MyInterface.class}, new InvocationHandler() {
            @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("代理方法");
            return method.invoke(new RealObject(), args);
        }
    });
    myInterface.doSomething();
14.8 空对象

使用null的时候每次都要检查是否为null,这是一件很麻烦的事。

引人空对象的思想将会很有用,它可以接受传递给它的所代表的对象的消息,但是将返回表示为实际上并不存在任何“真实”对象的值。通过这种方式,你可以假设所有的对象都是有效的,而不必浪费编程精力去检查null

通常空对象是单例的,所以你不仅可以用instanceOf来比较,还可以用equals==来比较。

注意:在某些地方仍然必须测试空对象,这与检查是否为null没有区别,但在很多地方就不必执行额外的测试了,可以直接假设所有对象都是有效的。

public class Test {
    public static void main(String[] args) throws Exception {
       // 在使用的时候可以直接使用而不会报错空指针
        Person p = Person.NULL_PERSON;
        System.out.println(p.toString());
    }
}

// 空标记接口
interface Null {}

class Person {
    void func() {
        System.out.println("Person");
    }
    
    // 空对象
    private static class NullPerson extends Person implements Null {
        private NullPerson() {}

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

    public static final Person NULL_PERSON = new NullPerson();
}
动态代理创建空对象

假设有不同的多个Person的子类,我们相对每一个都创建一个空对象。无论何时,如果你需要一个空Person对象,只需要调用newNullPerson()并传递需要代理的Person的类型。

public class Test {
    public static Person newNullPerson(Class type) {
        return (Person) Proxy.newProxyInstance(NullPerson.NULL_PERSON.getClass().getClassLoader(), new Class[]{type}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("代理");
                return method.invoke(NullPerson.NULL_PERSON, args);
            }
        });
    }

    public static void main(String[] args) throws Exception {
        Person p = newNullPerson(Person.class);
        p.func();
    }
}
// 空标记接口
interface Null {}

// 父接口
interface Person {
    void func();
}

// 空Person
class NullPerson implements Person, Null {
    @Override
    public void func() {
        System.out.println("NullPerson");
    }

    public static final Person NULL_PERSON = new NullPerson();
    private NullPerson() {}
}
14.8.1 模拟对象与桩

空对象的逻辑变体是模拟对象。与空对象一样,它们都表示在最终的程序中所使用的“实际”对象。但是,模拟对象和桩都只是假扮可以传递实际信息的存活对象,而不是像空对象那样可以成为null的一种更加智能化的替代物。

模拟对象和桩之间的差异在于程度不同。模拟对象往往是轻量级和自测试的,通常很多模拟对象被创建出来是为了处理各种不同的测试情况。桩只是返回桩数据,它通常是重量级的,并且经常在测试之间被复用。桩可以根据它们被调用的方式,通过配置进行修改,因此桩是一 种复杂对象,它要做很多事。然而对于模拟对象,如果你需要做很多事情,通常会创建大量小而简单的模拟对象。

14.9 接口与类型信息

interface关键字的一种重要目标就是允许程序员隔离构件,进而降低耦合性。如果你编写接口,那么就可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并非是对解耦的一种无懈可击的保障

public class Test {
    public static void main(String[] args) {
        A a = new B();
        a.a();
        // 我们需要的是用户使用接口,但是强制转型还是可以访问不存在于接口中的方法
        ((B) a).b();
    }
}

interface A {
    void a();
}

class B implements A {
    @Override
    public void a() {}
    public void b() {}
}
解决方法1:方法是直接声明

如果程序员不使用接口而是子类,它们要对自己负责。即B a = new B();代替A a = new B();

解决方法2:包访问权限隐藏

此时在此包外只能使用Hidden.newA()来获取对象,而且由于没有B类的信息,也无法强制转型。

class B implements A {
    @Override
    public void a() {}
    public void b() {}
}

public class HiddenB {
    public static A newA() {
        return new B();
    }
}
反射的后门

通过使用反射,仍旧可以到达并调用所有方法,甚至是private方法!如果知道方法名,你就可以在其Method对象上调用setAccessible(true)

final域实际上在遭遇修改时是安全的。运行时系统会在不抛异常的情况下接受任何修改尝试,但是实际上不会发生任何修改。

14.10 总结

RTTI允许通过匿名基类的引用来发现类型信息。

面向对象编程语言的目的是让我们在凡是可以使用的地方都使用多态机制,只在必需的时候使用RTTI。

可继承一个新类,然后添加你需要的方法。在代码的其他地方,可以检査你自己特定的类型,并调用你自己的方法,这样做不会破坏多态性以及程序的扩展能力。

但如果在程序主体中添加需要的新特性的代码,就必须使用RTTI来检査你的特定的类型。

一致的错误报告模型的存在使我们能够通过使用反射编写动态代码。当然,尽力编写能够进行静态检査的代码是值得的,只要你确实能够这么做。但是我相信动态代码是将Java与其他例如C++这样的语言区分开的重要工具之一。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72201.html

相关文章

  • Java编程思想笔记3.操作符

    摘要:自动拆箱用赋值运算符把一个包装类赋值给一个基本类型变量,或者是在包装类进行数值运算时。指数计数,表示的幂按位操作符可以把值看成单比特值对待,的操作相同,但是不能用于布尔值。移位操作符高位包括符号位舍弃,低位补零。 点击进入我的博客 3.1更简单的打印语句 System.out.println(imbug); 通过编写一个小类库,并通过import static该方法来实现简化打印(基...

    wpw 评论0 收藏0
  • Java编程思想笔记2.一切都是对象

    摘要:方法的基本组成包括名称参数返回值方法体方法名和参数列表唯一的标识出某个方法。如果返回的类型是,则的作用仅是退出方法否则必须返回正确的返回值包名名字可见性约定以域名反转作为包名,用来划分子目录,并且全部小写。 点击进入我的博客 2.1用引用操纵对象 尽管一切都看作对象,但操纵的标识符实际上是对象的一个引用。 String s; // s是一个String类型的引用, 并没有任何对象与其...

    taohonghui 评论0 收藏0
  • Java编程思想笔记4.控制执行流程

    摘要:在初始化和步进控制部分,可以用一系列由逗号分割的语句,而且那些语句会独立执行。和都表示无限循环语法数组等关键词有两个方面的用途一方面指定一个方法返回什么值另一个方面指定当前的方法退出,并返回那个值。 点击进入我的博客 4.1 true&false Java的条件语句只能使用布尔值来决定执行路径 4.2 if-else 4.3 循环语句 while、for、do-while do-...

    PingCAP 评论0 收藏0
  • Java编程思想笔记01------一切都是对象

    摘要:一引用操纵对象在的世界里,一切都被视为对象。特点创建程序时,需要知道存储在栈内所有数据的确切生命周期,以便上下移动堆栈指针。因为,指向同一块内存空间除了通过对象引用静态变量,我们还可以通过类直接引用静态变量 一、引用操纵对象 在Java的世界里,一切都被视为对象。操纵的标识符实际上是对象的引用, 例如:遥控器与电视的关系。 可以在没有对象关联的情况下,拥有一个引用。没有电视机,也可以拥...

    yuanxin 评论0 收藏0
  • Java编程思想笔记8.多态

    摘要:多态的作用是消除类型之间的耦合关系。编写构造器准则用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法。 点击进入我的博客 在面向对象的程序设计语言中,多态是继数据抽象(封装)和继承之后的第三种基本特征。多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态的作用是消除类型之间的耦合关系。 8.1 再论向上转型 对象既可以作为它自己的本类使用,也可以作为它的...

    chinafgj 评论0 收藏0

发表评论

0条评论

Hwg

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<