java對象和內存的控制深刻淺出

1、對象與內存控制的知識點 java

1.java變量的初始化過程,包括局部變量,成員變量(實例變量和類變量)。
2.繼承關係中,當使用的對象引用變量編譯時類型和運行時類型不一樣時,訪問該對象的屬性和方法是有區別的。
3.final修飾符特性。 閉包

2、java變量的劃分與初始化過程 jvm

     java程序的變量大致能夠分爲成員變量和局部變量,成員變量能夠分爲實例變量(非靜態變量)和類變量(靜態變量),通常咱們遇到的局部變量會在下列幾種狀況中出現:
     (1)形參:在方法簽名中定義的局部變量,由調用方爲其賦值,隨着方法結束消亡。
     (2)方法內的局部變量:在方法內定義的局部變量必須在方法內顯示的初始化(賦初始值),隨着變量初始化完成開始,到方法結束而消亡。
     (3)代碼塊內的局部變量:在代碼塊內定義的局部變量必須在代碼塊內顯示的初始化(賦初始值),隨着初始化完成開始生效,隨着代碼塊的結束而消亡。 ide

package com.zlc.array;

public class TestField {
	{
		String b ;
		//若是不初始化,編譯器就會報The local variable b may not have been initialized
		System.out.println(b);
	}
	public static void main(String[] args) {
		int a ;
		//若是不初始化,編譯器就會報The local variable a may not have been initialized
		System.out.println(a);
	}
}
    使用static修飾的成員變量是類變量,屬於類自己,沒有用static修飾的成員變量是實例變量,屬於該類的實例,在同一個JVM裏面,每一個類只能對應一個Class對象,但每一個類能夠建立多個java對象。(也就是說一個類變量只需一塊內存空間,而該類每建立一次實例,就須要爲實例變量分配一塊空間)
   
     實例變量的初始化過程:從語法角度來講,程序能夠在三個地方對實例變量執行初始化:
   (1)定義實例變量時指定初始值。
   (2)非靜態塊中對實例變量指定初始值。
   (3)構造器中對實例變量指定初始值。
    其中(1)和(2)這兩種方式初始化時間都比(3)在構造器中要早,(1)和(2)兩種初始化順序是按照他們在源碼中的排列順序決定的。
   
package com.zlc.array;

public class TestField {
	public TestField(int age){
		System.out.println("構造函數中初始化 this.age = "+this.age);
		this.age = age;
	}
	{
		System.out.println("非靜態塊中初始化");
		age = 22;
	}
	//定義的時候初始化
	int age = 15;
	public static void main(String[] args) {
		TestField field = new TestField(24);
		System.out.println("最終 age = "+field.age);
	}
}
    運行結果爲:非靜態塊中初始化
                     構造函數中初始化 this.age = 15
                     最終 age = 24
    若是會使用javap的話,能夠經過javap -c XXXX(class文件)看下改java類是如何編譯的。
    定義實例變量時指定初始值,初始化塊中爲實例變量指定初始值語句地位是平等的,當通過編譯器編譯處理以後,他們都被提到構造器中,上面所說的 int age = 15;會劃分下面兩個步驟執行:
    1)int age;建立java對象時系統根據該語句爲該對象分配內存。
    2)age = 15;這條語句會被提取到java類的構造器中執行。
   
    類變量的初始化過程
:從語法角度來講,程序能夠從兩個地方對類變量進行初始化賦值。
   (1)定義類變量時指定初始值。
   (2)靜態塊中對類變量指定初始值。
    兩種執行順序和他們在源碼中的排列順序相同,咱們舉個小變態的例子:
package com.zlc.array;

 class TestStatic {
	//類成員 DEMO TestStatic實例
	final static TestStatic DEMO = new TestStatic(15);
	//類成員 age
	static int age = 20;
	//實例變量 curAge
	int curAge;
	
	public TestStatic(int years) {
		// TODO Auto-generated constructor stub
		curAge = age - years;
	}
}
 public class Test{
	public static void main(String[] args) {
		System.out.println(TestStatic.DEMO.curAge);
		TestStatic staticDemo = new TestStatic(15);
		System.out.println(staticDemo.curAge);
	}
}
    輸出結果有兩行打印,一個是打印TestStatic類屬性DEMO的實例變量,第二個經過java對象staticDemo輸出TestStatic的實例屬性,根據咱們上面分析的實例變量和類變量的初始化流程能夠進行推斷:
    1)初始化第一階段,加載類的時候爲類變量DEMO、age分配內存空間,此時DEMO和age的默認值分別是null和0。

   2)初始化第二階段,程序按順序依次給DEMO、age賦初始值,TestStatic(15)須要調用TestStatic的構造器,此時age = 0 因此打印結果爲 -15,而當staticDemo被初始化的時候,age已經被賦值等於20了,因此輸出結果爲5。 函數

3、在繼承關係中繼承成員變量和繼承成員方法的區別。 this

    當建立任何java對象時,程序總會先調用父類的非靜態塊、父類構造器,最後才調用本類的非靜態塊和構造器。經過子類的構造器調用父類的構造器通常分爲兩種狀況,一個是隱式調用,一個經過super顯示調用父類的構造器。
    子類的方法能夠調用父類的實例變量,這是由於子類繼承了父類就會獲取父類的成員變量和方法,但父類的方法不能訪問子類的實例變量,由於父類不知道它將被哪一個類繼承,它的子類將會增長什麼樣的成員變量,固然在一些極端的例子裏面仍是能夠實現父類調用子類變量的,好比:子類重寫了父類的方法,通常都會打印出默認值,由於這個時候子類的實例變量尚未初始化。
spa

package com.zlc.array;
class Father{
	int age = 50;
	public Father() {
		// TODO Auto-generated constructor stub
                System.out.println(this.getClass());
                //this.sonMethod();沒法調用
		info();
	}
	public void info(){
		System.out.println(age);
	}
}
public class Son extends Father{
	int age = 24;
	public Son(int age) {
		// TODO Auto-generated constructor stub
		this.age = age;
	}
	@Override
	public void info() {
		// TODO Auto-generated method stub
		System.err.println(age);
	}
	public static void main(String[] args) {
		new Son(28);
	}
        //子類特有的方法
        public void sonMethod(){
                 System.out.println("Son method");
        }
}
    按照咱們正常推斷,經過子類隱式的調用父類的構造器,而在父類的構造器中調用了info()方法(注意:我這裏沒有說調用父類的),按道理來講是輸出了父類的age實例變量,打印結果預計是50,但實際輸出的結果爲0,分析緣由:
   1)java對象的內存分配不是在構造器中完成的,構造器只是完成了初始化賦值的過程,也就是在調用父類的構造器以前,jvm已經給這個Son對象分類好了內存空間,這個空間存放了兩個age屬性,一個是子類的age,一個是父類的age。
   2)在調用new Son(28)的時候,當前的this對象表明着是子類Son的對象,咱們能夠經過把對象.getClass()打印出來就會獲得class com.zlc.array.Son的結果,可是當前初始化過程又是在父類的構造器中進行的,經過this.sonMethod()又沒法被調用,這是由於this的編譯類型是Father的緣故。

   3)在變量的編譯時類型和運行時類型不一樣時,經過該變量訪問它的引用對象的實例變量時,該實例變量的值由聲明該變量的類型決定,但經過該變量調用它引用的對象的實例方法時,該方法的行爲由它實際引用的對象決定,因此這裏調用的是子類的info方法,因此打印的是子類的age,因爲age還沒來得急初始化因此打印默認值0。
    通俗的來講也就是當聲明的類型和真正new的類型不一致的時候,使用的屬性是父類的,調用的方法是子類的。
    經過javap -c咱們更能直接的體會爲何繼承屬性和方法會有很大的區別,若是咱們把上面例子裏面,子類Son的info重寫方法去掉,這個時候調用的會是父類的info方法,是由於在進行編譯的時候會把父類的info方法編譯轉移到子類裏面去,而聲名的成員變量會留在父類中不進行轉移,這樣子類和父類擁有了同名的實例變量,而若是子類重寫了父類的同名方法,則子類的方法會徹底覆蓋掉父類的方法(至於爲何java要這麼設計,我的也不太清楚)。同名變量能同時存在不覆蓋,同名方法子類會完全覆蓋父類同名方法。
    總的來講對於一個引用變量而言當經過該變量訪問它所引用的對象的實例變量時,該實例變量的值取決於聲明該變量時類型,當經過該變量訪問它所引用的對象的方法時,該方法行爲取決於它所實際引用的對象的類型。
最後拿個小case複習下: 線程

package com.zlc.array;
class Animal{
	int age ;
	public Animal(){
		
	}
	public Animal(int age) {
		// TODO Auto-generated constructor stub
		this.age = age;
	}
	void run(){
		System.out.println("animal run "+age);
	}
}
class Dog extends Animal{
	int age;
	String name;
	public Dog(int age,String name) {
		// TODO Auto-generated constructor stub
		this.age = age;
		this.name = name;
	}
	@Override
	void run(){
		System.out.println("dog run "+age);
	}
}
public class TestExtends {
	public static void main(String[] args) {
		Animal animal = new Animal(5);
		System.out.println(animal.age);
		animal.run();
		
		Dog dog = new Dog(1, "xiaobai");
		System.out.println(dog.age);
		dog.run();
		
		Animal animal2 = new Dog(11, "wangcai");
		System.out.println(animal2.age);
		animal2.run();
		
		Animal animal3;
		animal3 = dog;
		System.out.println(animal3.age);
		animal3.run();
	}
}
     想要調用父類的方法:能夠經過super來調用,但super關鍵字沒有引用任何對象,它不能當作真正的引用變量來使用,有興趣的朋友能夠本身研究下。
     上面介紹的都是實例變量和方法,類變量和類方法要簡單多了,直接使用類名.方法就方便了不少,也不會遇到那麼多麻煩。


4、final修飾符的使用(特別是宏替換) 設計

   final能夠修飾變量,被final修飾的變量被賦初始值以後,不能對他從新賦值。
   final能夠修飾方法,被final修飾的方法不能被重寫。
   final能夠修飾類,被final修飾的類不能派生子類。

code

被final修飾的變量必須顯示的指定初始值:
對因而final修飾的是實例變量,則只能在下列三個指定位置賦初始值。
    (1)定義final實例變量時指定初始值。
    (2)在非靜態塊中爲final實例變量指定初始值。
    (3)在構造器中爲final實例變量指定初始值。
     最終都會被提到構造器中進行初始化。
對於用final指定的類變量:只能在指定的兩個地方進行賦初始值。
    (1)定義final類變量的時候指定初始值。
    (2)在靜態塊中爲final類變量指定初始值。
     一樣通過編譯器處理,不一樣於實例變量的是,類變量都是提到靜態塊中進行賦初始值,而實例變量是提到構造器中完成。
    被final修飾的類變量還有一種特性,就是「宏替換」,當被修飾的類變量知足在定義該變量的時候就指定初始值,並且這個初始值在編譯的時候就能肯定下來(好比:1八、"aaaa"、16.78等一些直接量),那麼該final修飾的類變量不在是一個變量,系統就會當成「宏變量」處理(就是咱們常說的常量),若是在編譯的時候就能肯定初始值,則就不會被提到靜態塊中進行初始化了,直接在類定義中直接使該初始值代替掉final變量。咱們仍是舉那個年齡減去year的例子:

package com.zlc.array;

 class TestStatic {
	//類成員 DEMO TestStatic實例
	final static TestStatic DEMO = new TestStatic(15);
	//類成員 age
	final static int age = 20;
	//實例變量 curAge
	int curAge;
	
	public TestStatic(int years) {
		// TODO Auto-generated constructor stub
		curAge = age - years;
	}
}
 public class Test{
	public static void main(String[] args) {
		System.out.println(TestStatic.DEMO.curAge);
		TestStatic static1 = new TestStatic(15);
		System.out.println(static1.curAge);
	}
}
    這個時候的age 被final修飾了,因此在編譯的時候,父類中全部的age都變成了20,而不是一個變量,這樣輸出的結果就能達到咱們的預期。
    特別是在對字符串進行比較的時候更能顯示出
package com.zlc.array;

public class TestString {
	static String static_name1 = "java";
	static String static_name2 = "me";
	static String statci_name3 = static_name1+static_name2;
	
	final static String final_static_name1 = "java";
	final static String final_static_name2 = "me";
	//加final 或者不加都行 前面兩個能被宏替換就好了
	final static String final_statci_name3 = final_static_name1+final_static_name2;
	
	public static void main(String[] args) {
		String name1 = "java";
		String name2 = "me";
		String name3 = name1+name2;
		//(1)
		System.out.println(name3 == "javame");
		//(2)
		System.out.println(TestString.statci_name3 == "javame");
		//(3)
		System.out.println(TestString.final_statci_name3 == "javame");
	}
}
    用final修飾方法和類沒有什麼好說的,只是一個不能被子類重寫(和private同樣),一個不能派生子類。     用final修飾局部變量的時候,Java要求被內部類訪問的局部變量都是用final修飾,這個是有緣由的,對於普通局部變量而言,它的做用域就停留在該方法內,當方法結束時,該局部變量也就消失了,但內部類可能產生隱式的「閉包」,閉包使得局部變量脫離他所在的方法繼續存在。      有時候在會在一個方法裏面new 一個線程,而後調用該方法的局部變量,這個時候須要把改變量聲明爲final修飾的。     
相關文章
相關標籤/搜索