3
头图

嗯?

在后台经常会收到这样一类私信,大致是这样描述的:


看来关于「程序员找对象」这个话题,非常有必要用一篇文章来专门梳理和归纳一下了。

择日不如撞日,今天就把这件事情给安排了吧。

可以说,方法多得很!


new一个对象

用关键字new进行对象的创建,几乎是写代码时最常用的操作之一了,比如:

Sheep sheep1 = new Sheep();
Sheep sheep2 = new Sheep( "codesheep", 18, 65.0f );

通过new的方式,我们可以调用类的无参或者有参构造方法来实例化出一个对象。

表面上看,简简单单new一下对象就有了,但面试时如果仅仅答到这一层,大概率会扑街,因为比这个更重要的是new对象时的原理和流程,因为JVM这个牵线红娘在背后默默地帮我们做了很多工作。

说到new一个对象的具体流程,用一张图可大致描述成如下所示:

  1. 首先,当我们new一个对象时,比如Sheep sheep = new Sheep()JVM首先就回去检查Sheep这个符号引用所代表的类是否已经被加载过,如果没有就要执行对应类的加载过程;
  2. 声明类型引用很简单,比如Sheep sheep = new Sheep()就会声明一个Sheep类型的引用sheep
  3. 第一步类加载完成以后,对象所需的内存大小其实就已经确定下来了,接下来JVM就会在堆上为对象分配内存;
  4. 所谓的属性“0”值初始化非常好理解,即为实例化对象的各个属性赋上默认初始化“0”值,比如int的初始化0值就是0,而一个对象的初始化0值就是null;
  5. 接下来JVM会进行对象头的设置,这里面就主要包括对象的运行时数据(比如Hash码、分代年龄、锁状态标志、锁指针、偏向线程ID、偏向时间戳等)以及类型指针(JVM通过该类型指针来确定该对象是哪个类的实例);
  6. 属性的显示初始化也好理解,比如定义一个类的时候,针对某个属性字段手动的赋值,如:private String name = "codesheep"; 就在这时候给初始化上;
  7. 最后是调用类的构造方法来进行进行构造方法内描述的初始化动作。

应该说,经过了这一系列步骤,一个新的可用对象方才得以诞生。


反射出一个对象

学过Java反射机制的都知道,只要能拿到类的Class对象,就可以通过强大的反射机制来创造出实例对象了。

拿到Class对象有三种方式:

  • 类名.class
  • 对象名.getClass()
  • Class.forName(全限定类名)

有了Class对象之后,接下来就可以调用其newInstance()方法来创建一个对象,就像这样:

Sheep sheep3 = (Sheep) Class.forName( "cn.codesheep.article.obj.Sheep" ).newInstance();
Sheep sheep4 = Sheep.class.newInstance();

当然,这种方式的局限性也有目共睹,因为使用的是类的无参构造方法来创建的对象。

所以比这个更进一步的方式是通过java.lang.relect.Constructor这个类的newInstance()方法来创建对象,因为它可以明确指定某个构造器来创建对象。

比如,在我们拿到了类的Class对象后,就可以通过getDeclaredConstructors()函数来获取到类的所有构造函数列表,这样我们就可以调用对应的构造函数来创建对象了,就像这样:

Constructor<?>[] constructors = Sheep.class.getDeclaredConstructors();
Sheep sheep5 = (Sheep) constructors[0].newInstance(); 
Sheep sheep6 = (Sheep) constructors[1].newInstance( "codesheep", 18, 65.1f );

而且,如果我们想明确获取类的某个构造函数,也可以在getDeclaredConstructors()函数里直接指定构造函数传参类型来精确控制,就像这样:

Constructor constructor = Sheep.class.getDeclaredConstructor( String.class, Integer.class, Float.class );
Sheep sheep7 = (Sheep) constructor.newInstance( "codesheep", 18, 65.2f );

克隆出一个对象

对象克隆在我们日常写代码的时候基本上是刚性需求,基于一个对象克隆出另一个对象,这也是写Java代码时十分常见的操作。

关于对象拷贝这一知识点,之前我已经写过了,详细梳理过一篇:《一个工作三年的同事,居然还搞不清深拷贝、浅拷贝...》,里面详细梳理了对象赋值、拷贝、深拷贝、浅拷贝等系列知识点,本文便不再赘述了。


反序列化出一个对象

关于对象「序列化和反序列化」这个知识点,重要且有用,但听很多朋友反映初学时有点糊。当我们作序列化和反序列化操作时,背后也会创建对象,关于「序列化和反序列化」这个知识点的详细理解+梳理,之前我也写过了,链接在此:序列化/反序列化,我忍你很久了,淦!


Unsafe黑魔法

Unsafe类这个名字一听就有点悬了,的确,我们平时的业务代码里接触得好像并不多。

我们都知道写Java代码,很少会去操作位于底层的一些资源,比如内存等这些。而位于sun.misc.Unsafe包路径下的Unsafe类提供了一种直接访问系统资源的途径和方法,可以进行一些底层的操作。比如借助Unsafe我们就可以分配内存、创建对象、释放内存、定位对象某个字段的内存位置甚至并修改它等等。

可见这玩意误用时的破坏力是很大的,所以一般也都是受控使用的。业务代码里很少能看到它的身影,但是JDK内部的一些诸如ioniojuc等包中的代码里还是有不少关于它的身影存在的。

Unsafe类中有一个allocateInstance()方法,通过其就可以创建一个对象。为此我们只需要获取到一个Unsafe类的实例对象,我们自然就可以调用allocateInstance()来创建对象了。

那如何才能获取到一个Unsafe类的实例对象呢?

大致瞅一眼Unsafe类的源码我们就会发现,它是一个单例类,其构造方法是私有的,所以直接构造是不太现实了:

public final class Unsafe {

    private static final Unsafe theUnsafe;

    // ... 省略 ...

    private static native void registerNatives();

    private Unsafe() {
    }
    
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
    
    // ... 省略 ...
    
}

而且获取单例对象的入口函数getUnsafe()上也做了特殊标记,意思是只能从引导加载的类才可以调用该方法,这意味着该方法也是供JVM内部使用的,外部代码直接使用会报类似这样的异常:

Exception in thread "main" java.lang.SecurityException: Unsafe

走投无路,我们只能再次重拾强大的反射机制来创建Unsafe类的实例了:

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

然后接下来我们就可以愉快地利用它来创建对象了:

Sheep sheep8 = (Sheep) unsafe.allocateInstance( Sheep.class );

对象的隐式创建场景

当然除了上述这几种显式地对象创建场景之外,还有一些我们并没有进行手动对象创建的隐式场景,举几个常见例子。

Class类实例隐式创建

我们都知道JVM虚拟机在加载一个类的时候,也都会创建一个类对应的Class实例对象,很明显这一过程是JVM偷偷地背着我们干的。

字符串隐式对象创建

典型的,比如定义一个String类型的字面变量时,就可能会引起一个新的String对象的创建,就像这样:

String name = "codesheep";

还常见的比如String+号连接符也会隐式地导致新String对象的创建等:

String str = str1 + str2;

自动装箱机制

这种例子也有很多,比如在执行类似如下代码时:

Integer codeSheepAge = 18;

其触发的自动装箱机制就会导致一个新的包装类型的对象在后台被隐式地创建出来。

函数可变参数

比如像下面这样,当我们使用可变参数语法int... nums来描述一个函数的入参时:

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 );

而背地里可能会隐式地产生一个对应的数组对象进行计算。


总而总之,很多场景下对象的隐式创建也是数见不鲜,我们最起码要做到心中大致有数。


后 记

所以看完文章,再回到文章开头提到的问题,你还觉得Java程序员搞对象是件难事吗?这么多花里胡哨的对象生成法还不够你用的么。

咳咳,玩笑归玩笑,这其实是面试时最常问到的基础问题之一。有时候面试官冷不丁问一句:“在Java里,你有哪些方式可以创建一个对象呢?”。所以针对该问题,这篇来好好梳理和归纳一下。

好啦,一个小小的对象创建就能扯出这么多的花样,好在经过一番梳理和总结,也更便于掌握和理解了。

就这样吧,下篇见。


CodeSheep
3.4k 声望7.6k 粉丝