Welcome to ray's blog home page
在Java中,一個物件在可以被使用之前必須要被正確地初始化,這一點是Java規範規定的。本文試圖對Java如何執行物件的初始化做一個詳細深入地介紹(與物件初始化相同,類在被載入之後也是需要初始化的,本文在最後也會對類的初始化進行介紹,相對於物件初始化來說,類的初始化要相對簡單一些)。
1.Java物件何時被初始化
Java物件在其被建立時初始化,在Java程式碼中,有兩種行為可以引起物件的建立。其中比較直觀的一種,也就是通常所說的顯式物件建立,就是通過new關鍵字來呼叫一個類的建構函式,通過建構函式來建立一個物件,這種方式在java規範中被稱為“由執行類例項建立表示式而引起的物件建立”。
當然,除了顯式地建立物件,以下的幾種行為也會引起物件的建立,但是並不是通過new關鍵字來完成的,因此被稱作隱式物件建立,他們分別是:
● 載入一個包含String字面量的類或者介面會引起一個新的String物件被建立,除非包含相同字面量的String物件已經存在與虛擬機器內了(JVM會在記憶體中會為所有碰到String字面量維護一份列表,程式中使用的相同字面量都會指向同一個String物件),比如,
class StringLiteral {
private String str = "literal";
private static String sstr = "s_literal";
}
● 自動裝箱機制可能會引起一個原子型別的包裝類物件被建立,比如,
class PrimitiveWrapper {
private Integer iWrapper = 1;
}
● String連線符也可能會引起新的String或者StringBuilder物件被建立,同時還可能引起原子型別的包裝物件被建立,比如(本人試了下,在mac ox下1.6.0_29版本的javac,對待下面的程式碼會通過StringBuilder來完成字串的連線,並沒有將i包裝成Integer,因為StringBuilder的append方法有一個過載,其方法引數是int),
public class StringConcatenation {
private static int i = 1;
public static void main(String... args) {
System.out.println("literal" + i);
}
}
2.Java如何初始化物件
當一個物件被建立之後,虛擬機器會為其分配記憶體,主要用來存放物件的例項變數及其從超類繼承過來的例項變數(即使這些從超類繼承過來的例項變數有可能被隱藏也會被分配空間)。在為這些例項變數分配記憶體的同時,這些例項變數也會被賦予預設值。
引用
關於例項變數隱藏
class Foo {
int i = 0;
}
class Bar extends Foo {
int i = 1;
public static void main(String... args) {
Foo foo = new Bar();
System.out.println(foo.i);
}
}
上面的程式碼中,Foo和Bar中都定義了變數i,在main方法中,我們用Foo引用一個Bar物件,如果例項變數與方法一樣,允許被覆蓋,那麼列印的結果應該是1,但是實際的結果確是0。
但是如果我們在Bar的方法中直接使用i,那麼用的會是Bar物件自己定義的例項變數i,這就是隱藏,Bar物件中的i把Foo物件中的i給隱藏了,這條規則對於靜態變數同樣適用。
在記憶體分配完成之後,java的虛擬機器就會開始對新建立的物件執行初始化操作,因為java規範要求在一個物件的引用可見之前需要對其進行初始化。在Java中,三種執行物件初始化的結構,分別是例項初始化器、例項變數初始化器以及建構函式。
2.1. Java的建構函式
每一個Java中的物件都至少會有一個建構函式,如果我們沒有顯式定義建構函式,那麼Java編譯器會為我們自動生成一個建構函式。建構函式與類中定義的其他方法基本一樣,除了建構函式沒有返回值,名字與類名一樣之外。在生成的位元組碼中,這些建構函式會被命名成<init>方法,引數列表與Java語言書寫的建構函式的引數列表相同(<init>這樣的方法名在Java語言中是非法的,但是對於JVM來說,是合法的)。另外,建構函式也可以被過載。
Java要求一個物件被初始化之前,其超類也必須被初始化,這一點是在建構函式中保證的。Java強制要求Object物件(Object是Java的頂層物件,沒有超類)之外的所有物件建構函式的第一條語句必須是超類建構函式的呼叫語句或者是類中定義的其他的建構函式,如果我們即沒有呼叫其他的建構函式,也沒有顯式呼叫超類的建構函式,那麼編譯器會為我們自動生成一個對超類建構函式的呼叫指令,比如,
public class ConstructorExample {
}
對於上面程式碼中定義的類,如果觀察編譯之後的位元組碼,我們會發現編譯器為我們生成一個建構函式,如下,
aload_0
invokespecial #8;
//Method java/lang/Object."<init>":()V
return
上面程式碼的第二行就是呼叫Object物件的預設建構函式的指令。
正因為如此,如果我們顯式呼叫超類的建構函式,那麼呼叫指令必須放在建構函式所有程式碼的最前面,是建構函式的第一條指令。這麼做才可以保證一個物件在初始化之前其所有的超類都被初始化完成。
如果我們在一個建構函式中呼叫另外一個建構函式,如下所示,
public class ConstructorExample {
private int i;
ConstructorExample() {
this(1);
}
ConstructorExample(int i) {
....
this.i = i;
....
}
}
對於這種情況,Java只允許在ConstructorExample(int i)內出現呼叫超類的建構函式,也就是說,下面的程式碼編譯是無法通過的,
public class ConstructorExample {
private int i;
ConstructorExample() {
super();
this(1);
....
}
ConstructorExample(int i) {
....
this.i = i;
....
}
}
或者,
public class ConstructorExample {
private int i;
ConstructorExample() {
this(1);
super();
....
}
ConstructorExample(int i) {
....
this.i = i;
....
}
}
Java對建構函式作出這種限制,目的是為了要保證一個類中的例項變數在被使用之前已經被正確地初始化,不會導致程式執行過程中的錯誤。但是,與C或者C++不同,Java執行建構函式的過程與執行其他方法並沒有什麼區別,因此,如果我們不小心,有可能會導致在物件的構建過程中使用了沒有被正確初始化的例項變數,如下所示,
class Foo {
int i;
Foo() {
i = 1;
int x = getValue();
System.out.println(x);
}
protected int getValue() {
return i;
}
}
class Bar extends Foo {
int j;
Bar() {
j = 2;
}
@Override
protected int getValue() {
return j;
}
}
public class ConstructorExample {
public static void main(String... args) {
Bar bar = new Bar();
}
}
如果執行上面這段程式碼,會發現打印出來的結果既不是1,也不是2,而是0。根本原因就是Bar過載了Foo中的getValue方法。在執行Bar的建構函式是,編譯器會為我們在Bar建構函式開頭插入呼叫Foo的建構函式的程式碼,而在Foo的建構函式中呼叫了getValue方法。由於Java對建構函式的執行沒有做特殊處理,因此這個getValue方法是被Bar過載的那個getValue方法,而在呼叫Bar的getValue方法時,Bar的建構函式還沒有被執行,這個時候j的值還是預設值0,因此我們就看到了打印出來的0。
2.2. 例項變數初始化器與例項初始化器
我們可以在定義例項變數的同時,對例項變數進行賦值,賦值語句就時例項變數初始化器了,比如,
public class InstanceVariableInitializer {
private int i = 1;
private int j = i + 1;
}
如果我們以這種方式為例項變數賦值,那麼在建構函式執行之前會先完成這些初始化操作。
我們還可以通過例項初始化器來執行物件的初始化操作,比如,
public class InstanceInitializer {
private int i = 1;
private int j;
{
j = 2;
}
}
上面程式碼中花括號內程式碼,在Java中就被稱作例項初始化器,其中的程式碼同樣會先於建構函式被執行。
如果我們定義了例項變數初始化器與例項初始化器,那麼編譯器會將其中的程式碼放到類的建構函式中去,這些程式碼會被放在對超類建構函式的呼叫語句之後(還記得嗎?Java要求建構函式的第一條語句必須是超類建構函式的呼叫語句),建構函式本身的程式碼之前。我們來看下下面這段Java程式碼被編譯之後的位元組碼,Java程式碼如下,
public class InstanceInitializer {
private int i = 1;
private int j;
{
j = 2;
}
public InstanceInitializer() {
i = 3;
j = 4;
}
}
編譯之後的位元組碼如下,
aload_0
invokespecial #11;
//Method java/lang/Object."<init>":()V
aload_0
iconst_1
putfield #13;
//Field i:I
aload_0
iconst_2
putfield #15;
//Field j:I
aload_0
iconst_3
putfield #13;
//Field i:I
aload_0
iconst_4
putfield #15;
//Field j:I
return
上面的位元組碼,第4,5行是執行的是原始碼中i=1的操作,第6,7行執行的原始碼中j=2的操作,第8-11行才是建構函式中i=3和j=4的操作。
Java是按照程式設計順序來執行例項變數初始化器和例項初始化器中的程式碼的,並且不允許順序靠前的例項初始化器或者例項變數初始化器使用在其後被定義和初始化的例項變數,比如,
public class InstanceInitializer {
{
j = i;
}
private int i = 1;
private int j;
}
public class InstanceInitializer {
private int j = i;
private int i = 1;
上面的這些程式碼都是無法通過編譯的,編譯器會抱怨說我們使用了一個未經定義的變數。之所以要這麼做,是為了保證一個變數在被使用之前已經被正確地初始化。但是我們仍然有辦法繞過這種檢查,比如,
public class InstanceInitializer {
private int j = getI();
private int i = 1;
public InstanceInitializer() {
i = 2;
}
private int getI() {
return i;
}
public static void main(String[] args) {
InstanceInitializer ii = new InstanceInitializer();
System.out.println(ii.j);
}
}
如果我們執行上面這段程式碼,那麼會發現列印的結果是0。因此我們可以確信,變數j被賦予了i的預設值0,而不是經過例項變數初始化器和建構函式初始化之後的值。
引用
一個例項變數在物件初始化的過程中會被賦值幾次?
在本文的前面部分,我們提到過,JVM在為一個物件分配完記憶體之後,會給每一個例項變數賦予預設值,這個時候例項變數被第一次賦值,這個賦值過程是沒有辦法避免的。
如果我們在例項變數初始化器中對某個例項x變數做了初始化操作,那麼這個時候,這個例項變數就被第二次賦值了。
如果我們在例項初始化器中,又對變數x做了初始化操作,那麼這個時候,這個例項變數就被第三次賦值了。
如果我們在類的建構函式中,也對變數x做了初始化操作,那麼這個時候,變數x就被第四次賦值。
也就是說,一個例項變數,在Java的物件初始化過程中,最多可以被初始化4次。
2.3. 總結
通過上面的介紹,我們對Java中初始化物件的幾種方式以及通過何種方式執行初始化程式碼有了瞭解,同時也對何種情況下我們可能會使用到未經初始化的變數進行了介紹。在對這些問題有了詳細的瞭解之後,就可以在編碼中規避一些風險,保證一個物件在可見之前是完全被初始化的。
3.關於類的初始化
Java規範中關於類在何時被初始化有詳細的介紹,在3.0規範中的12.4.1節可以找到,這裡就不再多說了。簡單來說,就是當類被第一次使用的時候會被初始化,而且只會被一個執行緒初始化一次。我們可以通過靜態初始化器和靜態變數初始化器來完成對類變數的初始化工作,比如,
public class StaticInitializer {
static int i = 1;
static {
i = 2;
}
}
上面通過兩種方式對類變數i進行了賦值操作,分別通過靜態變數初始化器(程式碼第2行)以及靜態初始化器(程式碼第5-6行)完成。
靜態變數初始化器和靜態初始化器基本同例項變數初始化器和例項初始化器相同,也有相同的限制(按照編碼順序被執行,不能引用後定義和初始化的類變數)。靜態變數初始化器和靜態初始化器中的程式碼會被編譯器放到一個名為static的方法中(static是Java語言的關鍵字,因此不能被用作方法名,但是JVM卻沒有這個限制),在類被第一次使用時,這個static方法就會被執行。上面的Java程式碼編譯之後的位元組碼如下,我們看到其中的static方法,
static {};
Code:
Stack=1, Locals=0, Args_size=0
iconst_1
putstatic #10;
//Field i:I
iconst_2
putstatic #10;
//Field i:I
return
在第2節中,我們介紹了可以通過特殊的方式來使用未經初始化的例項變數,對於類變數也同樣適用,比如,
public class StaticInitializer {
static int j = getI();
static int i = 1;
static int getI () {
return i;
}
public static void main(String[] args) {
System.out.println(StaticInitializer.j);
}
}
上面這段程式碼的列印結果是0,類變數的值是i的預設值0。但是,由於靜態方法是不能被覆寫的,因此第2節中關於建構函式呼叫被覆寫方法引起的問題不會在此出現。
相關推薦
Welcome to ray's blog home page
在Java中,一個物件在可以被使用之前必須要被正確地初始化,這一點是Java規範規定的。本文試圖對Java如何執行物件的初始化做一個詳細深入地介紹(與物件初始化相同,類在被載入之後也是需要初始化的,本文在最後也會對類的初始化進行介紹,相對於物件初始化來說,類的初始化要相對
Welcome to JRX2015U43's blog!
【英文題目】 A sequence of N positive integers (10 < N < 100 000), each of them less than or equal 10000, and a positive integer S (S <
Welcome to yjjr's blog!
T1 yyy點餐 題意 給出長度為nnn的序列,求有所有不同的組合的代價總和(每種組合的代價為該組合內所有數之和) 對於全部資料,有1≤n≤1000000,0≤ai<9982443531\
Welcome to oopos's Blog
/*最大子段和問題:對於一個序列: -6,9,8,-10,100,-99其中:最大子段和為:100 子段長度為:10*/#include <stdio.h>#include <stdlib.h>#define MAX 101int main(void){ int i,j,k,n,
Welcome to DaxinPai's Blog
在資料分析中,遇到統計問題的時候,基本可以按照下表來: (圖片來源自網上,出處不詳) 那麼首先我們需要判斷是否是正態分佈(Normal Distribution), 四種方法: 繪製資料的直方圖,看疊加線——這是一種粗略的方法,且不是硬性( ha
Welcome to tikeyc's column
蘋果靜止熱更新,可惜我的是企業APP...(當然有些熱更新已經可以通過蘋果稽核了,比如JSPatch) 最近公司要新增熱修復BUG,其實早之前本人就有簡單實現過,剛好契合公司需求,在此總結一下iOS熱更新實現方式 這個是我根據JSPatch寫的一個Demo:https://github.com/tike
Welcome to Simmel's Garden
看別人的論文時看到利用腳註將冗餘資訊放到頁面腳註位置,使得論文作者區域清爽、簡潔、高階。想要效仿。於是在網路中進行了一番搜尋,發現了一篇博文給了我答案:實現的效果是任意指定作者的標註符號,包括多個作者可
【Welcome to Smile-Huang 's Blog.】This Blog aims to share my experience with you. Please leave comments if you have any thoughts.
This Blog aims to share my experience with you. Please leave comments if you have any thoughts.
Welcome to Smile-Huang 's Blog.
#include<iostream> #include<string> #include<fstream> using namespace std; //字元數目為n的詞項k-gram數目為n+1-k //預定的閾值為0.1 #define threshold 0.1
Welcome to Feng.Chang's Blog
1、檢視當前登入使用者 [[email protected] ~]$ whatis w w (1) - Show who is logged on
welcome to 浩·C's blog
#include <iostream> using namespace std; /* 思路: 注意題中說的“好晶片比壞晶片多”,所以對於每個晶片,可以記錄其他行的晶片對該
Welcome to WindowsCE's World.
因為專案需要,準備學習 WinCE 了。以前一直在弄嵌入式 Linux,突然進入Windows平臺,還真不太適應,許多WinCE的概念與思維與Linux相去甚遠,更談不上 Linux 下開源給人的透明與暢快。 仗著自己在VC上還算“有點”小經
welcome to 一點點 home
微服務的誕生: 越來越多的使用者參與 業務場景越來越複雜 傳統的單體架構已經很難滿足網際網路技術的的發展要求 程式碼的可維護性,擴充套件性和可讀性在降低 維護系統的成本,修改系統的成本在提高。 微服務:是著名的OO(面向物件Object oriented)專家Mar
Quinn's blog ! I'm glad to be here!
數學推導 比例縮法填充,滿足最大邊填充,故此,首先找出相對於填充的最大邊 let fromW = from.width let fromH = from.height let toH = to
Welcome to my blog. I hope you can communicate with me.
以上程式碼實現了利用Js獲取裝置螢幕的寬度,並根據螢幕的寬度動態改變根元素html的font-siz屬性的作用。比如說,對於iphone6而言,螢幕尺寸為750,那麼在iPhone6下html的font-size為1px,所以1rem = 1px;對於iPhone5而言,螢幕尺寸為640,那麼在iPhone5
Deploying Facebox to AWS ECS @ Alex Pliutau's Blog
Currently I am building a product on top of face recognition functionality and I am using Facebox with go-sdk as it’s the easiest way to add
o means open. Simple CLI tool to open repository in browser. @ Alex Pliutau's Blog
Here is my small bash function! When you run it from the terminal it opens the GitHub/BitBucket/GitLab page in your browser for the git reposi
How to build Go plugin with data inside @ Alex Pliutau's Blog
Go 1.8 gives us a new tool for creating shared libraries, called plugins! This new plugin buildmode is currently only supported on Linux. But
Building Google Home Action in Go @ Alex Pliutau's Blog
Google Home Google Home is a voice Assistant, similar to Amazon Alexa, but working with Google services. It has a lot of built-in integra
Different ways to block Go runtime forever @ Alex Pliutau's Blog
The current design of Go’s runtime assumes that the programmer is responsible for detecting when to terminate a goroutine and when to terminat