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

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

擇日不如撞日,今天就把這件事情給安排了吧。
可以說,方法多得很!
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一個對象的具體流程,用一張圖可大致描述成如下所示:

- 首先,當(dāng)我們
new一個對象時,比如Sheep sheep = new Sheep(),JVM首先就回去檢查Sheep這個符號引用所代表的類是否已經(jīng)被加載過,如果沒有就要執(zhí)行對應(yīng)類的加載過程; - 聲明類型引用很簡單,比如
Sheep sheep = new Sheep()就會聲明一個Sheep類型的引用sheep; - 第一步類加載完成以后,對象所需的內(nèi)存大小其實(shí)就已經(jīng)確定下來了,接下來
JVM就會在堆上為對象分配內(nèi)存; - 所謂的屬性“
0”值初始化非常好理解,即為實(shí)例化對象的各個屬性賦上默認(rèn)初始化“0”值,比如int的初始化0值就是0,而一個對象的初始化0值就是null; - 接下來JVM會進(jìn)行對象頭的設(shè)置,這里面就主要包括對象的運(yùn)行時數(shù)據(jù)(比如Hash碼、分代年齡、鎖狀態(tài)標(biāo)志、鎖指針、偏向線程ID、偏向時間戳等)以及類型指針(JVM通過該類型指針來確定該對象是哪個類的實(shí)例);
- 屬性的顯示初始化也好理解,比如定義一個類的時候,針對某個屬性字段手動的賦值,如:
private String name = "codesheep";就在這時候給初始化上; - 最后是調(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、nio、juc等包中的代碼里還是有不少關(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程序員搞對象是件難事嗎?這么多花里胡哨的對象生成法還不夠你用的么。

咳咳,玩笑歸玩笑,這其實(shí)是面試時最常問到的基礎(chǔ)問題之一。有時候面試官冷不丁問一句:“在Java里,你有哪些方式可以創(chuàng)建一個對象呢?”。所以針對該問題,這篇來好好梳理和歸納一下。
好啦,一個小小的對象創(chuàng)建就能扯出這么多的花樣,好在經(jīng)過一番梳理和總結(jié),也更便于掌握和理解了。
就這樣吧,下篇見。