Java語言基礎及特性-01

一.博客摘抄

1.Java的String和StringBuffer和StringBuilder詳解

2.Java的引用類型、參數傳值與傳引用區別

3.Java的包裝類


二.java參數傳遞:java語言中,將實參的值傳遞給形參時,到底傳遞的是什麼?

我們知道,實參和形參在棧上分別有一個內存地址,內存裏面存放的內容即是他們的值,比如:

int number = 10;//在棧的內存上開闢一塊空間來存放number,具體的內存地址值由jvm來分配和管理,比如在0x12345678的內存地址上存放number的值10

Person person = new Person(“zhangsan”);//在棧上開闢一塊內存存放person,在堆上開闢一塊內存存放zhangsan對象,其中棧上的person內存地址裏面存放zhangsan對象在堆上的地址值,即棧上的person引用着堆上的zhangsan對象。

在參數傳遞過程中,實參將其自己的存儲內容傳給了形參的內存存儲中。接下來我們看三個例子來驗證下。

... ... 
//定義了一個改變參數值的函數  
public static void changeValue(int x) {  
x = x *2;  
}  
... ...  
//調用該函數  
int num = 5;  
System.out.println(num);  
changeValue(num);  
System.out.println(num);  
... ...  
答案顯而易見,調用函數changeValue()前後num的值都沒有改變。在該例子中changeValue函數的形參x的值是由實參num的內存存儲值傳過去的,即將5賦值給了形參x的內存存儲中。


在看一個例子

... ...  
class person {  
public static String name = "Jack";  
... ...  
}  
... ...  
//定義一個改變對象屬性的方法  
public static void changeName(Person p) {  
p.name = "Rose";  
}  
... ...  
public static void main(String[] args) {  
//定義一個Person對象,person是這個對象的引用  
Person person = new Person();  
//先顯示這個對象的name屬性  
System.out.println(person.name);  
//調用changeName(Person p)方法  
changeName(person);  
//再顯示這個對象的name屬性,看是否發生了變化  
System.out.println(person.name);  
}  

輸出的答案:

第一次顯示:“Jack”

第二次顯示:“Rose”

在該例子中,位於棧上的形成p的值是由同樣位於棧上的實參person給賦值的,由於實參person棧空間裏面存放的是堆空間Person的對象引用,即實參person裏面存放的是堆空間Person的地址,在發生實參向形參傳遞賦值的過程時,實參中將該地址傳給了形參,即形參的內存中也存放的是堆空間裏面的Person對象的地址。因此,在changeName中對形參的Person對象的name屬性修改時會反應到實參中。

在看一個例子

public class Example {
	String str = new String("good");
	char[] ch = { 'a', 'b', 'c' };

	public static void main(String args[]) {
		Example ex = new Example();
		ex.change(ex.str, ex.ch);
		System.out.print(ex.str + " and ");
		System.out.print(ex.ch);
	}

	public void change(String str, char ch[]) {
		str = "test ok";
		ch[0] = 'g';
	}
}

分析下這個Demo中爲什麼 str沒被改變,但是ch[0]卻改變了。

在main中new了一個Example對象,ex.str指向了該對象中的String成員對象(其值爲"good"),然後調用change函數,將ex.str作爲實參傳遞給形參str,此時ex.str和str都是引用,傳遞的過程是將ex.str引用的值傳遞給str,那麼str也將指向同一字符串對象("good"),
隨後進入change函數,str = "test ok"; 該語句將在JVM的字符串常量池中創建一個新的字符串常量("test ok")對象,賦值給str的其實是新字符串常量對象的地址值。好了,str的值改變了,而str本身是String類型的一個對象引用,那麼該引用現在指向了新的字符串對象。(注意,此處跟上面例子的區別,上面例子中形參始終指向實參,兩者的地址是一樣的,形參從未發生指向的改變,改變的只是地址裏面的屬性,而此處是將指向/地址發生了改變)   而我們外層的Example對象仍然由ex這個引用指向,並且在ex內部還有一個ex.str的引用,它仍然指向"good"字符串,一直沒有改變,當你使用ex.str打印時,就顯示了"good"沒有變化。

以上就是str沒有改變的原因,至於ch[0]是數組,跟上面例子中的Person一樣,修改的是地址裏面的值。

通過上面的三個例子,我們可以得出結論,實參向形參傳遞的是實參內存存儲單元裏面的值(該值即可能是基本數據類型也可能是引用類型,如果是引用類型的話則是一個地址)。如果實參裏面直接存放的是基本數據類型的話就直接拷貝一份賦值給形參,如果實參裏面存放的是堆空間引用類型對象的話,自己存儲單元裏面存放的就是地址,因此拷貝一份地址傳給形參。從這個角度來說的話,無論實參是基本類型還是引用類型,都是將自己存儲單元的值直接拷貝了一份給形參,這也解釋了java中參數傳遞都是值傳遞而沒有引用傳遞的原因。

備註:Java參數傳遞的一篇文章值得參考


三.C語言參數傳遞

接來下我們在來看看C語言中,實參向形參傳遞時是否跟java一樣?

typedef struct mystruct mystruct;
struct  mystruct
{
	int i;
	char c;
	int arrary[2];
	char *pChar;
};

void pointTest(mystruct *pmystruct)
{
	int a = 100;
	int *ap = &a;

	printf("%p\n",&a);  //輸出:002AF744
	printf("%p\n",ap);  //輸出:002AF744
	printf("%d\n",*ap); //輸出:100
	printf("%p\n",&ap); //輸出:002AF738
	printf("%p\n",&*ap);//輸出:002AF744

	printf("&pmystruct address = %p\n", &pmystruct);
	printf(" pmystruct address = %p\n", pmystruct);

	scanf("%d"); 
}

 int _tmain(int argc, _TCHAR* argv[])
 {
	mystruct temp;
	printf("temp address = %p\n", &temp);
	pointTest(&temp);//測試指針本身地址、指針指向地址及指向的內容

	return 0;
 }
輸出結果:

temp address = 0041F770
0041F688
0041F688
100
0041F67C
0041F688
&pmystruct address = 0041F69C
 pmystruct address = 0041F770
分析:

1. printf("%d\n",&a);//輸出:002AF744

這一句輸出的是變量a的地址,毋庸置疑。

2. printf("%d\n",ap);//輸出:002AF744

這一句是輸出的是指針的值,也就是說指針的值是指針所指向的變量的地址

3. printf("%d\n",*ap);//輸出:100

在指針變量的前面加了一個*號,不加星號的ap指針是指向變量a的地址,而加了*真變成了指針ap所指向的變量a的內容,所以,我們可以理解爲*號是獲取指針變量所指向的地址所存放的內容的操作。

4. printf("%d\n",&ap);//輸出:002AF738

這一句(同1)是取得指針變量ap的地址

5. printf("%d\n",&*ap);//輸出:002AF744

這一句根據第3點的分析,*ap指向的是變量a的內容,而&*ap即是獲取變量a的內容的地址,即是變量a的地址所以輸出內容同(1)。

小結一:

C語言中在發生有參函數調用時,實參變量與形參變量之間的數據都是單向的“值傳遞”方式。包括指針變量和數組名作參數的情況。C語言要求函數的實參要有確定的值,在函數調用時給形參分配相應的內存單元,同時將實參的“值”賦(複製)給形參,實現數據從實參到形參的傳遞(‘值傳遞’方式)。因爲是複製,所以在操作副本(形參)過程中不會影響到原本(實參)內容。

作爲函數實參的量包括常量、變量和表達式,其中變量又包括簡單變量、數組元素、數組名、指針變量等。不同類


型變量作參數實現的數據傳遞方式相同,效果不同。所謂方式相同即都是參數間數據單向的“值傳遞”,效果不同


是指被調函數能否改變主調函數中變量的值。


四.使用靜態內部類實現延遲加載單例模式

單例模式,簡單來說,就是在整個應用中保證只有一個類的實例存在。就像是Java Web中的application,也就是提供了一個全局變量。單例模式確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。這個類就稱爲單例類。

單例的創建一般分爲懶漢式,餓漢式,雙重鎖檢查,枚舉等,其中雙重鎖檢查只能在jdk1.5以後纔能有效,1.5以前在java對象模型中的無序寫問題不能保證。

下面就介紹使用內部類的機制來巧妙實現懶漢式單例模式的實現,這個模式綜合使用了Java的類級內部類和多線程缺省同步鎖的知識(jvm確保了一個類只會加載一次),很巧妙地同時實現了延遲加載和線程安全,同時不受jdk版本的影響。

內部類簡單介紹

內部類分爲對象級別和類級別,類級內部類指的是,有static修飾的成員變量的內部類。如果沒有static修飾的成員變量的內部類被稱爲對象級內部類。類級內部類相當於其外部類的static成員,它的對象與外部類對象間不存在依賴關係,相互獨立,因此可直接創建,而對象級內部類的實例,是必須綁定在外部對象實例上的。類級內部類只有在第一次被使用的時候才被會裝載。

要想很簡單地實現線程安全,可以採用靜態初始化器的方式,它可以由JVM來保證線程的安全性,如餓漢式單例,這種實現方式,會在類裝載的時候就初始化對象,有可能浪費一定的內存(假設你不需要的話),有一種方法能夠讓類裝載的時候不去初始化對象,就是採用類級內部類,在這個類級內部類裏面去創建對象實例。

代碼如下:

/**
 * 利用靜態內部類實現懶漢模式/線程安全的單例模式。
 * 線程安全:靜態內部類只有在通過getInstance()方法調用時纔會加載而且也只加載一次,而這就是靜態內部類實現線程安全的單例模式的理論基礎。
 * jvm機制中規範了一個類只會加載一次。
 *
 * 參考 http://blog.csdn.net/liushuijinger/article/details/9069801
 * Created by ZhengGuangGuo on 2017/12/19 10:51.
 */
public class Singleton {
    private Singleton() {}

    public Singleton getInstance() {
        return SingletonHold.instance;
    }

    public static class SingletonHold {
        private final static Singleton instance = new Singleton();
    }
}

//雙重非空校驗+鎖+volatile實現線程安全
public class Singleton {
    private volatile static Singleton singleton = null;

    private Singleton() {
    }

    public Singleton getInstance() {
        //避免每次都加鎖synchronized校驗影響性能,因此採用雙重非空校驗
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            } //end synchronized
        }
        return singleton;
    }
}

/**
 * 餓漢式
 * 利用jvm類加載機制實現線程安全的單例模式,一個類當且僅當加載一次
 */
public class Singleton {
    //單例變量,static, 在類加載時進行初始化一次,保證線程安全
    private static Singleton instance = new Singleton();

    //私有化的構造方法,保證外部的類不能通過構造器來實例化。
    private Singleton() {
    }

    //獲取單例對象實例
    public static Singleton getInstance() {
        System.out.println("我是餓漢式單例!");
        return instance;
    }
}

當getInstance方法第一次被調用的時候,它第一次讀取SingletonHolder.instance,內部類SingletonHolder類得到初始化;而這個類在裝載並被初始化的時候,會初始化它的靜態域,從而創建Singleton的實例,由於是靜態的域,因此只會在虛擬機裝載類的時候初始化一次,並由虛擬機來保證它的線程安全性。這個模式的優勢在於,getInstance方法並沒有被同步,並且只是執行一個域的訪問,因此延遲初始化並沒有增加任何訪問成本。

參考:菜鳥學設計模式(一)——小單例有大祕密





發佈了96 篇原創文章 · 獲贊 155 · 訪問量 82萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章