Java程序員找對象攻虐

嗯?

在后臺經(jīng)常會收到這樣一類私信,大致是這樣描述的:

image

看來關(guān)于「程序員找對象」這個話題,非常有必要用一篇文章來專門梳理和歸納一下了。

image

擇日不如撞日,今天就把這件事情給安排了吧。

可以說,方法多得很!


new一個對象

用關(guān)鍵字new進(jìn)行對象的創(chuàng)建,幾乎是寫代碼時最常用的操作之一了,比如:

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

通過new的方式,我們可以調(diào)用類的無參或者有參構(gòu)造方法來實(shí)例化出一個對象。

表面上看,簡簡單單new一下對象就有了,但面試時如果僅僅答到這一層,大概率會撲街,因?yàn)楸冗@個更重要的是new對象時的原理和流程,因?yàn)?code>JVM這個牽線紅娘在背后默默地幫我們做了很多工作。

說到new一個對象的具體流程,用一張圖可大致描述成如下所示:

image
  1. 首先,當(dāng)我們new一個對象時,比如Sheep sheep = new Sheep(),JVM首先就回去檢查Sheep這個符號引用所代表的類是否已經(jīng)被加載過,如果沒有就要執(zhí)行對應(yīng)類的加載過程;
  2. 聲明類型引用很簡單,比如Sheep sheep = new Sheep()就會聲明一個Sheep類型的引用sheep;
  3. 第一步類加載完成以后,對象所需的內(nèi)存大小其實(shí)就已經(jīng)確定下來了,接下來JVM就會在堆上為對象分配內(nèi)存;
  4. 所謂的屬性“0”值初始化非常好理解,即為實(shí)例化對象的各個屬性賦上默認(rèn)初始化“0”值,比如int的初始化0值就是0,而一個對象的初始化0值就是null;
  5. 接下來JVM會進(jìn)行對象頭的設(shè)置,這里面就主要包括對象的運(yùn)行時數(shù)據(jù)(比如Hash碼、分代年齡、鎖狀態(tài)標(biāo)志、鎖指針、偏向線程ID、偏向時間戳等)以及類型指針(JVM通過該類型指針來確定該對象是哪個類的實(shí)例);
  6. 屬性的顯示初始化也好理解,比如定義一個類的時候,針對某個屬性字段手動的賦值,如:private String name = "codesheep"; 就在這時候給初始化上;
  7. 最后是調(diào)用類的構(gòu)造方法來進(jìn)行進(jìn)行構(gòu)造方法內(nèi)描述的初始化動作。

應(yīng)該說,經(jīng)過了這一系列步驟,一個新的可用對象方才得以誕生。


反射出一個對象

學(xué)過Java反射機(jī)制的都知道,只要能拿到類的Class對象,就可以通過強(qiáng)大的反射機(jī)制來創(chuàng)造出實(shí)例對象了。

拿到Class對象有三種方式:

  • 類名.class
  • 對象名.getClass()
  • Class.forName(全限定類名)

有了Class對象之后,接下來就可以調(diào)用其newInstance()方法來創(chuàng)建一個對象,就像這樣:

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

當(dāng)然,這種方式的局限性也有目共睹,因?yàn)槭褂玫氖穷惖?strong>無參構(gòu)造方法來創(chuàng)建的對象。

所以比這個更進(jìn)一步的方式是通過java.lang.relect.Constructor這個類的newInstance()方法來創(chuàng)建對象,因?yàn)樗梢悦鞔_指定某個構(gòu)造器來創(chuàng)建對象。

比如,在我們拿到了類的Class對象后,就可以通過getDeclaredConstructors()函數(shù)來獲取到類的所有構(gòu)造函數(shù)列表,這樣我們就可以調(diào)用對應(yīng)的構(gòu)造函數(shù)來創(chuàng)建對象了,就像這樣:

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

而且,如果我們想明確獲取類的某個構(gòu)造函數(shù),也可以在getDeclaredConstructors()函數(shù)里直接指定構(gòu)造函數(shù)傳參類型來精確控制,就像這樣:

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

克隆出一個對象

對象克隆在我們?nèi)粘懘a的時候基本上是剛性需求,基于一個對象克隆出另一個對象,這也是寫Java代碼時十分常見的操作。

關(guān)于對象拷貝這一知識點(diǎn),之前我已經(jīng)寫過了,詳細(xì)梳理過一篇:《一個工作三年的同事,居然還搞不清深拷貝、淺拷貝...》,里面詳細(xì)梳理了對象賦值、拷貝、深拷貝、淺拷貝等系列知識點(diǎn),本文便不再贅述了。


反序列化出一個對象

關(guān)于對象「序列化和反序列化」這個知識點(diǎn),重要且有用,但聽很多朋友反映初學(xué)時有點(diǎn)糊。當(dāng)我們作序列化和反序列化操作時,背后也會創(chuàng)建對象,關(guān)于「序列化和反序列化」這個知識點(diǎn)的詳細(xì)理解+梳理,之前我也寫過了,鏈接在此:序列化/反序列化,我忍你很久了,淦!。


Unsafe黑魔法

Unsafe類這個名字一聽就有點(diǎn)懸了,的確,我們平時的業(yè)務(wù)代碼里接觸得好像并不多。

我們都知道寫Java代碼,很少會去操作位于底層的一些資源,比如內(nèi)存等這些。而位于sun.misc.Unsafe包路徑下的Unsafe類提供了一種直接訪問系統(tǒng)資源的途徑和方法,可以進(jìn)行一些底層的操作。比如借助Unsafe我們就可以分配內(nèi)存、創(chuàng)建對象、釋放內(nèi)存、定位對象某個字段的內(nèi)存位置甚至并修改它等等。

可見這玩意誤用時的破壞力是很大的,所以一般也都是受控使用的。業(yè)務(wù)代碼里很少能看到它的身影,但是JDK內(nèi)部的一些諸如io、niojuc等包中的代碼里還是有不少關(guān)于它的身影存在的。

Unsafe類中有一個allocateInstance()方法,通過其就可以創(chuàng)建一個對象。為此我們只需要獲取到一個Unsafe類的實(shí)例對象,我們自然就可以調(diào)用allocateInstance()來創(chuàng)建對象了。

那如何才能獲取到一個Unsafe類的實(shí)例對象呢?

大致瞅一眼Unsafe類的源碼我們就會發(fā)現(xiàn),它是一個單例類,其構(gòu)造方法是私有的,所以直接構(gòu)造是不太現(xiàn)實(shí)了:

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;
        }
    }
    
    // ... 省略 ...
    
}

而且獲取單例對象的入口函數(shù)getUnsafe()上也做了特殊標(biāo)記,意思是只能從引導(dǎo)加載的類才可以調(diào)用該方法,這意味著該方法也是供JVM內(nèi)部使用的,外部代碼直接使用會報類似這樣的異常:

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

走投無路,我們只能再次重拾強(qiáng)大的反射機(jī)制來創(chuàng)建Unsafe類的實(shí)例了:

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

然后接下來我們就可以愉快地利用它來創(chuàng)建對象了:

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

對象的隱式創(chuàng)建場景

當(dāng)然除了上述這幾種顯式地對象創(chuàng)建場景之外,還有一些我們并沒有進(jìn)行手動對象創(chuàng)建的隱式場景,舉幾個常見例子。

Class類實(shí)例隱式創(chuàng)建

我們都知道JVM虛擬機(jī)在加載一個類的時候,也都會創(chuàng)建一個類對應(yīng)的Class實(shí)例對象,很明顯這一過程是JVM偷偷地背著我們干的。

字符串隱式對象創(chuàng)建

典型的,比如定義一個String類型的字面變量時,就可能會引起一個新的String對象的創(chuàng)建,就像這樣:

String name = "codesheep";

還常見的比如String+號連接符也會隱式地導(dǎo)致新String對象的創(chuàng)建等:

String str = str1 + str2;

自動裝箱機(jī)制

這種例子也有很多,比如在執(zhí)行類似如下代碼時:

Integer codeSheepAge = 18;

其觸發(fā)的自動裝箱機(jī)制就會導(dǎo)致一個新的包裝類型的對象在后臺被隱式地創(chuàng)建出來。

函數(shù)可變參數(shù)

比如像下面這樣,當(dāng)我們使用可變參數(shù)語法int... nums來描述一個函數(shù)的入?yún)r:

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

從表面上看,函數(shù)的調(diào)用處可以傳入各種離散參數(shù)參與計(jì)算:

avg( 2, 2, 4 );
avg( 2, 2, 4, 4 );
avg( 2, 2, 4, 4, 5, 6 );

而背地里可能會隱式地產(chǎn)生一個對應(yīng)的數(shù)組對象進(jìn)行計(jì)算。


總而總之,很多場景下對象的隱式創(chuàng)建也是數(shù)見不鮮,我們最起碼要做到心中大致有數(shù)。


后 記

所以看完文章,再回到文章開頭提到的問題,你還覺得Java程序員搞對象是件難事嗎?這么多花里胡哨的對象生成法還不夠你用的么。

image

咳咳,玩笑歸玩笑,這其實(shí)是面試時最常問到的基礎(chǔ)問題之一。有時候面試官冷不丁問一句:“在Java里,你有哪些方式可以創(chuàng)建一個對象呢?”。所以針對該問題,這篇來好好梳理和歸納一下。

好啦,一個小小的對象創(chuàng)建就能扯出這么多的花樣,好在經(jīng)過一番梳理和總結(jié),也更便于掌握和理解了。

就這樣吧,下篇見。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容