关于java:Java程序员找对象攻虐

6次阅读

共计 4140 个字符,预计需要花费 11 分钟才能阅读完成。

嗯?

在后盾常常会收到这样一类私信,大抵是这样形容的:


看来对于「程序员找对象」这个话题,十分有必要用一篇文章来专门 梳理和演绎 一下了。

择日不如撞日,明天就把这件事件给安顿了吧。

能够说,办法多得很!


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 里,你有哪些形式能够创立一个对象呢?”。所以针对该问题,这篇来好好梳理和演绎一下。

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

就这样吧,下篇见。

正文完
 0