类型信息

  2020-4-17 


JAVA类型信息

本节涉及很多JVM知识,结合JVM内容理解,这里只有一部分

类动态加载机制

JAVA是动态加载类的,这是JAVA与其它静态加载语言如CPP的主要区别之一

整个初始化流程

加载类并创建实例的流程:类加载-实例化。注意加载类不一定要创建实例

类加载阶段包括加载、连接(验证、准备、解析)、初始化、使用和卸载,它们开始的顺序一定,但是完成的顺序并不一定,因为在一个阶段内会调用其他阶段。

加载就是加载Class对象,见3

类加载的准备步骤中给静态成员变量开辟了内存,也就相当于全部为0或null,在初始化步骤中才执行初始化赋值(final成员除外,final成员在准备阶段即进行初始化赋值

举例:public static int a=1,变量在准备阶段过后的初始值是0而不是1,如果是引用成员变量就会为null,其他基本类型对应的是其基本默认值。只有在初始化阶段过后,才会变成1。

(类加载也可以叫类初始化过程,注意类初始化不是子过程的字段初始化)

类加载流程更多内容见JAVA虚拟机

实例化阶段包括新建对象-非静态成员初始化-调用构造器

这里的非静态成员变量也和类加载同理,先分配内存(准备),再调用构造器,赋予初值(初始化)

不过要先新建一个对象(开辟内存),对位于类加载中加载Class

Class对象

①每个类在编译的时候都会生成一个Class对象,固化储存在类名.class文件中,由JVM中的类加载器子系统加载进内存中的方法区里。Class对象也是对象,可以由个Class类型的引用指向它

Class对象包含了类的元数据(包括类名,变量名,方法),加载后储存在方法区中。与类绑定的成员和方法(静态成员、静态方法)都在Class对象中。(也就是说,这时候类信息、静态成员和静态方法已经被初始化了,此时对象还没生成,此时构造器还没调用)

③在程序第一次引用(见下)会触发加载该类的Class对象。(构造器是一个类的静态方法)。

引用:分为主动引用和被动引用(这里参考ZYG同学的博客)

a.主动引用

五种情况会触发初始化阶段,这五类情况是主动引用

  1. new、getstatic、putstatic和invokestatic命令

    后三个是对于静态字段的读写与静态方法的调用

  2. 反射调用某个类

  3. 加载某个类,如果父类没有被加载,那么也进行加载

  4. 执行主类(包含main函数)

  5. 使用MethodHanler(类似于反射的一个针对于方法的东西,链接https://my.oschina.net/floor/blog/1535062)

b.被动引用

  1. 子类调用父类的静态字段,只会初始化父类
  2. 初始化该类的数组
  3. 调用静态(static)常量(final)已经优化到常量池里面

一旦Class对象被加载进内存方法区,它就可以被用来在堆上创建这个类的所有对象(类加载器先会检查是否已加载Class对象,已加载类就可以直接用)【对象实例是由该类的Class对象实例来创建的】

⑥所有Class对象都属于Class

初始化流程

于是现在就可以更底层理解Ch.5中的初始化顺序了

初始化顺序:静态块=静态成员>非静态块=非静态成员>构造器调用

生成对象过程:非静态部分初始化、构造器调用

new命令为例,new引用类会触发初始化,其目的是新建该类对象,如果该类没有在内存方法区中(即Class对象未加载),那么就会在方法区上先加载Class对象,走类加载过程;然后再走实例化过程

加载类不一定会生成对象

(比如调用类的静态成员/方法)这样就不会初始化非静态成员/方法、调用构造器了

双亲委派机制

类加载的双亲委派机制

  1. 当某个类加载器需要加载某个.class文件时,它首先把这个任务[从当前类加载器]委托[给他的上级类加载器][递归]这个操作,如果上级的类加载器没有加载,自己才会去加载这个类,加载了就直接返回。这是一种代理模式

  2. 意义

    ①保证一个类只被加载一次,省内存

    ②安全(保证不被篡改)

    为了系统类的安全,类似“ java.lang.Object”这种核心类,JVM需要保证他们生成的对象都会被认定为同一种类型。即“通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的”。

    具体来说:比如我们想写一个java.lang.Object类,我们写的类属于程序的类,当前由系统类(应用程序类)加载器加载,它是启动类加载器和扩展类加载器的儿子,那么根据双亲委托机制,就会先由父类的类加载器来加载java.lang.Object,那么这样我们自己写的类就无法被加载比如我们自己定义包名和类名叫java.lang.Object),那么我们的系统类加载器加载这个类之前先会找更上层的类加载器,更上层的类加载器在java核心类库找到了java.lang.Object,那么久不会再调用系统类加载器加载我们自己写的了);不过我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器加载一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载

    类加载器&java双亲委派机制及作用为何采用双亲委派机制

Class常用API

首先获取class对象(Class类引用):

Ⅰ.Class.forName(className):使用加载当前类的类加载器来装载制定的类,可以获取className类的Class对象的引用如果获取的时候还未加载Class对象,那就执行类加载流程;必须检查异常,如果找不到,抛出ClassNotFoundException;这里的className要包含包名)

Ⅱ.Object.getName(objectName)可以获取objectName实例的类的Class引用

注意this指代的是这个类的实例,而不是这个类!所以要获取this的类信息得用getClass()

Ⅲ.className.class ,类字面常量,获取className类的Class对象的引用,在编译时检查,安全,所以可以不检查异常,且使用方便,高效、推荐使用这个替代forName;且不但适用于普通的类,也适用于接口、数组、基本数据类型。(基本数据类型及其封装类都可以使用class,基本数据类型封装类还有个TYPE字段指向该类未封装的基本数据类型Class对象);但是使用.class创建对象引用的时候,不会自动初始化Class对象(类加载流程:加载-链接-初始化),仅在对静态方法/非常数静态域(非static final)第一次引用时才初始化

于是可以这样写Class abcclass = abc.class

然后就可以通过class对象搞事情可以做到很多类信息相关的事情:

获取到Class引用之后(设名字为class)可以使用class.getSimpleName()来获取不含包名的类名、class.getCanonicalName()获取含包名的类名,class.getInterfaces()获取该class对象中包含的所有接口,class.getSuperclass()查询基类

class.isInterface()可以通过Class对象来判断该类是否为接口,同理还有isXXX()等一堆方法,获取类信息,总之,很牛逼

class.getDeclaredConstructor().newInstance()可以创建实例并返回引用。getDeclaredConstructor()获得class的构造器对象并调用其newInstance()方法创建对象创建实例,适用于有参构造器和无参构造器;该方法需要检查异常NoSuchMethodExceptionclass.newInstance()仅适用无参构造器且已被JAVA9不推荐);注意,即使使用泛型,返回的引用类型也为确切类型。

③类型判断

object instanceof className:判断实例是否是某类,返回布尔值;注意是className类名,而不是Class对象

class.isInstance(object):判断实例是否是class对象的类的实例(是否由class类创建)

父类.class.isAssignableFrom(子类.class):判断子类是否继承父类

泛化Class的引用(Class与泛型)

BA的子类,但B.class并不是A.class的子类,所以Class<?> A是强制检查是A的Class实例,B不行;只有写Class<? extends A> intClass = B.class,才会同时检查其子类Class

见Ch.15-通配符

反射机制

  1. 反射机制可以提供运行时的类信息。如果不能在主程序运行的时候知晓某类的类型和类信息,我们就可以通过反射机制来让我们在运行的时候获取类信息,使用该类。

  2. 反射机制应用场景:

    ①使用实现SPI的库(如JDBC),SPI(Service Provider Interface)由JAVA核心库提供接口,由不同厂商具体实现。那么我们就可以通过反射动态加载不同厂商的SPI实现

    ②动态代理:根据对象在内存中加载的Class类创建运行时类对象,从而调用代理类方法和属性。我也不知道我的接口,真正实现是谁传过来后我用类名加载,屏蔽掉实现的细节,让使用者更加方便好用,提高程序的灵活性

    静态代理通俗点将就是自己手写一个代理类,而动态代理则不用我们手写,而是依赖于java反射机制

    ③越权:可以用反射改变类方法或变量的权限,比如私有的改成共有的

  3. 反射机制实现:反射由Class类与java.lang.reflect类库提供支持,该类库包含Field类、Method类、Constructor类。这些类型的对象是由JVM在运行时创建的,用以表示位置类里对应的成员。

    反射常用APIJava高级特性——反射(记住常用的,这篇总结的非常好!)

    注意使用:getMethod(),getConstructor()都只能获取公有的,getDeclaredMethodgetDeclaredConstructor能获取任意权限的,带s的方法即为获取全部,返回列表

    反射机制四大类:Class、Field、Method、Constructor,都有实例化对象

    ②从Class获取到的Field、Method都是类信息,想要调用该成员、方法,首先得构建对象实例,然后方法调用invoke(Object,Object...args)还得传入对应的参数,Object...args表示是边长参数列表,args只要是Object类即可(即一切)

    ③获取Class中的Method、Constructor的方法中,获取单个的需要明确指定方法的参数表,比如getMethod(String name, Class...<?> parameterTypes),其中Class...<?> parameterTypes表示参数类型的Class类。距离:假设参数是两个字符串,那就应该写`getMethod(String name, String.class,String.class)

java的反射到底是有什么用处?怎么用? - Java3y的回答 - 知乎

动态代理

代理模式是用代理类来代理实现类进行操作,在代理类中生成目标类的对象,但又增强了其它方法。代理其实就是个每一层代理加强一点的套娃。其目的是想要将额外的操作从“实际”对象中分离到不同的地方

但是静态代理的缺陷是,每有一个目标类 ,我们就要写一个代理类,这样不好

在静态代理中,我们需要写一个代理类(大套娃),而在动态代理中,我们想不写代理类,而是生成一个代理Class对象,然后用它创建代理实例,再由代理 实例调用目标对象。(也就是说Class对象就是代理,基于反射机制,也就成了个动态套娃

动态代理的调用处理程序必须事先java.lang.reflect.InvocationHandler接口,及使用java.lang.reflect.Proxy类中的newProxyInstance方法动态的创建代理类。

preview

首先用Proxy.getProxyClass()用实际接口class去构造代理class,然后在代理class中建立代理对象。代理Class的构造器创建对象时,需要传入InvocationHandler(一般用匿名内部类)。每次调用代理对象的method方法,最终都会调用InvocationHandler的invoke()方法(将代理代理对象方法导向invoke),在invoke里新建目标类实例(目标类以参数形式传入),然后使用method.invoke来调用该实例的method方法

preview

这里参考:Java 动态代理作用是什么? - bravo1988的回答 - 知乎 (这篇写得非常清晰)

参考:Java 动态代理作用是什么? - ZeaTalk的回答 - 知乎 https://www.zhihu.com/question/20794107/answer/23330381

package test;

//接口
public interface Subject   
{   
  public void doSomething();   
}
package test;

//真实类
public class RealSubject implements Subject   
{   
  public void doSomething()   
  {   
    System.out.println( "call doSomething()" );   
  }   
}
package test;

import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
import java.lang.reflect.Proxy;  

public class ProxyHandler implements InvocationHandler
{
    //真实对象
    private Object tar;

    //绑定委托对象,并返回代理类
    public Object bind(Object tar)
    {
        this.tar = tar;
        //绑定该类实现的所有接口,取得代理类 ,这里就是前面所说的,用真实接口class来构造代理class,再用代理class的构造器生成代理对象
        return Proxy.newProxyInstance(tar.getClass().getClassLoader(),
                                      tar.getClass().getInterfaces(),
                                      this);
    }    

    //调用代理对象实际上就会调用该方法
    public Object invoke(Object proxy , Method method , Object[] args)throws Throwable
    {
        Object result = null;
        //这里就可以进行所谓的AOP编程了
        //在调用具体函数方法前,执行功能处理
        //【这里就是,让真实对象tar,调用method方法,参数为args】,在这前后就可以进行增强
        //这里就可以看出动态代理的优越处了:实例对象、方法全是动态的
        result = method.invoke(tar,args);
        //在调用具体函数方法后,执行功能处理
        return result;
    }
}
public class TestProxy
{
    public static void main(String args[])
    {
           ProxyHandler proxy = new ProxyHandler();
           //绑定该类实现的所有接口
           Subject sub = (Subject) proxy.bind(new RealSubject());
           sub.doSomething();
    }
}

如上:在实际使用中直接Proxy.newProxyInstance(),然后就返回代理实例,简化使用

Spring AOP基于动态代理的

反射可以做到解耦:编译期不依赖、运行时依赖

bean:可重用组件

反射加载配置+(单例)bean工厂 解耦类依赖 IOC=====再发展一下,框架做工厂的事情,那就是Spring框架

P.S.JAVA是个大玩具,很有意思,反射机制下,JAVA没有秘密(所以真秘密要用NDK)


且听风吟