Java设计思想深究----类与对象实例化(图文)

写在前面:这篇文章考究了大量的博文与国内外百科,以线性叙事方式解析内容,请按顺序耐心的读懂,如有疑论请不吝评论。

目录

类的编译

对象的声明

 对象的实例化

类的实例化

引用指向实例

几个类与对象的拓展的特性

反射Reflect

克隆(拷贝)Clone

单例类

隐式创建对象


public class AppObject {
    static{
        System.out.println("TT被类加载器加载");
    }
}
AppObject obj = new AppObject();

这是创建一个Java对象最基础的过程,

如果我们可以多停留一秒的思考,就会发现这一句编码,竟有4个组成部分:

通常我们只关注获取对象之后可以进行的事情,实际上在这一句Java代码中,做了十分多的事情。

不妨将这段编码的有效组成拓扑展开,可以得到3个过程:

  • 类的编译

public class AppObject {
    static{
        System.out.println("TT被类加载器加载");
    }
}
  • 对象的声明
AppObject obj;
  •  对象的实例化
obj = new AppObject();

 具体的流程如下:

类的编译

public class AppObject {
    public AppObject() {
    }

    static {
        System.out.println("TT被类加载器加载");
    }
}

运行启动后,IDE首先会根据javac命令编译项目下所有的Java文件为.class文件,这是一种16进制的字节码文件,如果用文本编辑器打开查看,会发现开头的魔数是"cafe babe",这是可被JVM读取的通行证,如果用IDE查看,会发现生成了一个缺省的构造方法。 

这里值得一提的是,对于各种引用包括:对象、变量、方法等,目前阶段无法得知引用的具体存储地址,因此用符号引用暂时代替。

对象的声明

public static void main(String[] args) {
        AppObject obj;
    }

运行主程序,这时你是否认为,JVM在内存上已经开辟了一个空间,预留给后续实例与引用对象关联?

继续看.class文件:

会发现声明并没有被编译,实际上不难理解,所有语言最终都会被解释为机器语言,在机器语言中,声明引用会为其分配一块内存空间,用以后续指向其他地址,如果没有后续初始化,将会造成资源浪费,Java在编译这一步就做了一定的性能优化。 

 对象的实例化

对象的实例化包括两个部分:类的实例化,引用指向实例

obj = new AppObject();

类的实例化

new AppObject()

类的实例化是一个统称,实际上由很多过程组成,由JVM主要完成。

首先, 在委托JVM前,我们已有的数据就只有.class文件,即接口入参。

数据流:.class文件字节码->字节流

随后类加载器ClassLoader会根据环境变量加载.class文件,利用Java I/O技术,将文件中的字节码解析为字节流缓存在机器内存中,这一过程称之为:加载

数据流:字节流接受校验

在引入字节流进入JVM之前,字节码校验器会检查那些无法执行的明显有破坏性的操作。除了系统类之外,其他类都要被校验,这一过程称之为:验证

数据流:准入JVM字节流->机器语言

值得一提的是JVM并没有明确固定虚拟机类型,但在Oracle或JDK中委托的都是Hotspot虚拟机,HotSpot虚拟机最明显的特点就是随用随编译的特性。这是因为,Hotspot拥有解释器与JIT编译器(Just in time Complier)。

当准入JVM字节流接入解释器后,解释器会根据字节映射Map去解释字节内容,详解请参考博文:深入理解JVM之Java字节码(.class)文件详解 - 简书

内容包括很多,比较值得我们注意的是,对应上图:

  • 解释出类元信息,即将类的结构体缓存至MetaSpace,相同的类名有且只解释一次,静态变量赋默认值,值得一提的是静态常量赋值,这是因为静态常量是类元信息,且直接解释为存储地址并存储数值,不同于静态变量是指针变量,指针变量是存储地址,指向地址存储数值,解析前是符号引用,无法完成变量赋值
  • 解释出常量信息到常量池,这里说的常量是指形如封装类的默认值、自定义的常量数据等
  • 解释出方法元,即类结构体只是字段以某种结构组织起来,方法无法简单以字段组织,需要同类的结构体一样,根据方法名将结构体缓存至MetaSpace

以上的过程称之为:准备

随后,在内存上已经缓存有静态结构、具有值的静态数据后,将符号引用(见类的编译)变为直接引用,即引用变量赋值真实的物理地址。这个过程称之为:解析

数据流:机器语言(未解释)->机器语言(数据、指令、地址)

这时MetaSpace完成了直接引用,具有了物理意义上的数据结构,开始执行静态代码(指令:赋值、寻址等),这称之为:初始化

JIT编译器在运行时与JVM交互,当JIT编译器利用热点代码探测技术找到运行高频的代码,并将适当的字节码序列编译为本地机器代码。使用JIT编译器时,硬件可以执行本机代码,而不是让JVM重复解释相同的字节码序列,并导致翻译过程相对冗长。这样可以提高执行速度,除非方法执行频率较低。

引用指向实例

public static void main(String[] args) {
        AppObject obj;
        obj = new AppObject();
    }

引用就是寻址,寻址就用指针变量,所以obj实际上就是main()方法下的一个局部指针变量,并在JVM栈中开辟空间,当类初始化完成后,该地址的数值存入类元信息结构体的首地址,即完成了对象的实例化。

几个类与对象的拓展的特性

反射Reflect

反射,笛卡尔曾提出:反射就是有机体对于规律性行为的应激反应

即Java的反射机制需要有:有机体(系统)、规律性行为(触发条件)、应激反应(元信息调取)

这样理解还是模糊,不急,感觉一下你的身体就是一个系统,大脑就是存储空间,并将你所有的应激反应都载入,当触发条件满足时,即作出应激反应,你身体的某些“数据”便发生了一些变化,而这一切并不是主观发起的,是被动适应环境的一种机制。

因此,假设JVM(大脑)加载了项目下所有的类(元信息),我们即引用元信息所在的地址:

public static void main(String[] args) throws ClassNotFoundException{
        Class clazz = Class.forName("com.company.AppObject");
}

类元信息是该类所有对象的共享模版,因此可以根据类元信息生产出新的对象:

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        Class clazz = Class.forName("com.company.AppObject");
        AppObject appObject = (AppObject) clazz.newInstance();
}

那么,你肯定有个疑惑,这和new AppObject()一个有什么区别?确实,同样经历了类的加载、验证、准备、解析、初始化过程。然而new关键字做的事情就是反射机制,只不过委托JVM帮忙隐式完成这个过程。

所以,反射从JVM机制出发,允许开发者对元信息进行访问利用,降低代码的耦合,耦合?你肯定在想,为什么会有耦合问题,举个例子:我们写数据库连接的时候,假设有两种驱动MysqlDriver和OracleDriver,为了减少资源的浪费,肯定是用哪个就加载哪个,触发条件不同:

public void connect(){
  //伪代码

  if(MysqlDB){
    //MysqlDriver实例化
  }else{
    //OracleDriver实例化
  }
}

这样也使用了一定的反射机制,但造成了MysqlDriver与OracleDriver的耦合,这方法只能对付Mysql、Oracle两个数据库,假如需求又出现了新的DB,那将改动代码。假如使用反射机制:

public void connect(){
  //伪代码
  
  //InputStream读取配置文件,得到proporty

  //XDriver实例化
  Class.ForName(proporty.dbName);
  
  //这里加载、连接、初始化了[dbName]类,同时类元信息包括了Field、Constructor、Method等信息
}

这样我们将 MysqlDriver与OracleDriver的耦合清除,利用配置文件提高了代码的模块化。

克隆(拷贝)Clone

克隆,顾名思义创造一个(equals == true)的对象,对象即引用,引用即指针变量,克隆行为即栈内存上开辟新的地址、堆内存上克隆其数据,指针变量内容即克隆对象的类元首地址。

//类获得clone行为,需要实现Cloneable协议接口
public class AppObject implements Cloneable{
    int val;
    TT tt;

    public int getVal() {
        return val;
    }

    public void setVal(int val) {
        this.val = val;
    }

    public TT getTt() {
        return tt;
    }

    public void setTt(TT tt) {
        this.tt = tt;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public static void main(String[] args) throws CloneNotSupportedException {
        AppObject appObject = new AppObject();
        appObject.setVal(1);
        AppObject appObject2 = (AppObject) appObject.clone();
        appObject.setVal(0);
        AppObject appObject1 = appObject;
        System.out.println("原对象与克隆对象寻址比较:"+(appObject == appObject2));
        System.out.println("原对象与引用对象寻址比较:"+(appObject == appObject1));
        System.out.println("原对象val:"+appObject.getVal());
        System.out.println("引用对象val:"+appObject1.getVal());
        System.out.println("克隆对象val:"+appObject2.getVal());
        System.out.println("原对象内部引用与克隆对象内部引用寻址比较:"+(appObject.getTt() == appObject2.getTt()));
}

根据我们的推论,

  • 两个对象,实例地址不同,即两个指针变量存储的地址不同,“==” 号代表存储的地址比较,故返回false。
  • 两个对象的堆空间为相同数据分裂,不是同一处,因此修改appObject的字段,appObject2输出不变化。
  • 由于appObject内部tt也是指针变量,存储了指向的地址,所以复制后的对象tt存储的地址不变,因此是同一处。

这称之为:浅克隆(浅拷贝)

 即只对被克隆对象的栈空间、堆空间进行内容复制。

如果我们对克隆对象的内部引用也克隆:

@Override
    protected Object clone() throws CloneNotSupportedException {
        AppObject appObject = (AppObject) super.clone();
        appObject.setTt((TT) appObject.getTt().clone());
        return appObject;
    }

得到的结果:

这称之为:深克隆(深拷贝)

 单例类

单例类来自于设计模式中的单例模式。

单例模式 Singleton Pattern

 作者认为是Java 中最简单的设计模式,

该类负责创建自己的对象,同时确保只有单个对象被创建,只公开获取实例的方法。一提到类自动创建自己的对象,我们就必须要利用到类加载过程,因此,实例需要静态,这样在类加载时便会创建一个对象,然后实例与构造方法私有,达到单例的目的。

理论可行,实践:

public class SingleObject {
    static SingleObject singleObject = new SingleObject();

    SingleObject() {};

    public static SingleObject getInstance(){
        return singleObject;
    }
}

隐式创建对象

其实就是自动装箱机制。

int i = 1;
String s = 'hello';

封装函数都由JVM提供关键字并自动加载实例,一般而言,封装类的载入连接初始化,都是JVM启动时自动进行的,验证的方式也很简单,我们可以监控类的加载:

jvm参数: -XX:+TraceClassLoading

运行main():

 通过这样的机制,Java还提供了一种可变参数语法 "method(类 ...){}"

public double avg( int... nums ) {
    double sum = 0;
    int length = nums.length;
    for (int i = 0; i<length; ++i) {
        sum += nums[i];
    }
    return sum/length;
}
avg( 2, 2, 4 );
avg( 2, 2, 4, 4 );
avg( 2, 2, 4, 4, 5, 6 );

从表面上看,函数的调用处可以传入各种离散参数参与计算,而背地里可能会隐式地产生一个对应的数组对象进行计算。

结束语

本博文将会随着作者的不断学习而更新, 如有疑论请不吝评论。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值