一、對象與內存控制的知識點
1.java變量的初始化過程,包括局部變量,成員變量(實例變量和類變量)。
2.繼承關係中,當使用的對象引用變量編譯時類型和運行時類型不同時,訪問該對象的屬性和方法是有區別的。
3.final修飾符特性。
二、java變量的劃分與初始化過程
java程序的變量大體可以分為成員變量和局部變量,成員變量可以分為實例變量(非靜態變量)和類變量(靜態變量),一般我們遇到的局部變量會在下列幾種情況中出現:
(1)形參:在方法簽名中定義的局部變量,由調用方為其賦值,隨著方法結束消亡。
(2)方法內的局部變量:在方法內定義的局部變量必須在方法內顯示的初始化(賦初始值),隨著變量初始化完成開始,到方法結束而消亡。
(3)代碼塊內的局部變量:在代碼塊內定義的局部變量必須在代碼塊內顯示的初始化(賦初始值),隨著初始化完成開始生效,隨著代碼塊的結束而消亡。
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。
三、在繼承關係中繼承成員變量和繼承成員方法的區別
當創建任何java對象時,程序總會先調用父類的非靜態塊、父類構造器,最後才調用本類的非靜態塊和構造器。通過子類的構造器調用父類的構造器一般分為兩種情況,一個是隱式調用,一個通過super顯示調用父類的構造器。
子類的方法可以調用父類的實例變量,這是因為子類繼承了父類就會獲取父類的成員變量和方法,但父類的方法不能訪問子類的實例變量,因為父類不知道它將被哪個類繼承,它的子類將會增加什麼樣的成員變量,當然在一些極端的例子裡面還是可以實現父類調用子類變量的,比如:子類重寫了父類的方法,一般都會打印出默認值,因為這個時候子類的實例變量還沒有初始化。
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關鍵字沒有引用任何對象,它不能當做真正的引用變量來使用,有興趣的朋友可以自己研究下。
上面介紹的都是實例變量和方法,類變量和類方法要簡單多了,直接使用類名.方法就方便了很多,也不會遇到那麼多麻煩。
四、final修飾符的使用(特別是宏替換)
(1)inal可以修飾變量,被final修飾的變量被賦初始值之後,不能對他重新賦值。
(2)inal可以修飾方法,被final修飾的方法不能被重寫。
(3)inal可以修飾類,被final修飾的類不能派生子類。
被final修飾的變量必須顯示的指定初始值:
對於是final修飾的是實例變量,則只能在下列三個指定位置賦初始值。
(1)定義final實例變量時指定初始值。
(2)在非靜態塊中為final實例變量指定初始值。
(3)在構造器中為final實例變量指定初始值。
最終都會被提到構造器中進行初始化。
對於用final指定的類變量:只能在指定的兩個地方進行賦初始值。
(1)定義final類變量的時候指定初始值。
(2)在靜態塊中為final類變量指定初始值。
同樣經過編譯器處理,不同於實例變量的是,類變量都是提到靜態塊中進行賦初始值,而實例變量是提到構造器中完成。
被final修飾的類變量還有一種特性,就是“宏替換”,當被修飾的類變量滿足在定義該變量的時候就指定初始值,而且這個初始值在編譯的時候就能確定下來(比如:18、"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修飾的。
五、對象佔用內存的計算方法
使用system.gc()和java.lang.Runtime類中的freeMemory(),totalMemory(),maxMemory()這幾個方法測量Java對象的大小。這種方法通常使用在需要對很多資源進行精確確定對象的大小。這種方法幾乎無用等生產系統緩存的實現。這種方法的優點是數據類型大小無關的,不同的操作系統,都可以得到佔用的內存。
它使用反射API用於遍歷對象的成員變量的層次結構和計算所有原始變量的大小。這種方法不需要如此多的資源,可用於緩存的實現。缺點是原始類型大小是不同的不同的JVM實現對應有不同的計算方法。
JDK5.0之後Instrumentation API提供了getObjectSize方法來計算對象佔用的內存大小。
默認情況下並沒有計算到引用對象的大小,為了計算引用對象,可以使用反射獲取。下面這個方法是上面文章裡面提供的一個計算包含引用對像大小的實現:
public class SizeOfAgent { static Instrumentation inst; /** initializes agent */ public static void premain(String agentArgs, Instrumentation instP) { inst = instP; } /** * Returns object size without member sub-objects. * @param o object to get size of * @return object size */ public static long sizeOf(Object o) { if(inst == null) { throw new IllegalStateException("Can not access instrumentation environment./n" + "Please check if jar file containing SizeOfAgent class is /n" + "specified in the java's /"-javaagent/" command line argument."); } return inst.getObjectSize(o); } /** * Calculates full size of object iterating over * its hierarchy graph. * @param obj object to calculate size of * @return object size */ public static long fullSizeOf(Object obj) { Map<Object, Object> visited = new IdentityHashMap<Object, Object>(); Stack<Object> stack = new Stack<Object>(); long result = internalSizeOf(obj, stack, visited); while (!stack.isEmpty()) { result += internalSizeOf(stack.pop(), stack, visited); } visited.clear(); return result; } private static boolean skipObject(Object obj, Map<Object, Object> visited) { if (obj instanceof String) { // skip interned string if (obj == ((String) obj).intern()) { return true; } } return (obj == null) // skip visited object || visited.containsKey(obj); } private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited) { if (skipObject(obj, visited)){ return 0; } visited.put(obj, null); long result = 0; // get size of object + primitive variables + member pointers result += SizeOfAgent.sizeOf(obj); // process all array elements Class clazz = obj.getClass(); if (clazz.isArray()) { if(clazz.getName().length() != 2) {// skip primitive type array int length = Array.getLength(obj); for (int i = 0; i < length; i++) { stack.add(Array.get(obj, i)); } } return result; } // process all fields of the object while (clazz != null) { Field[] fields = clazz.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { if (!Modifier.isStatic(fields[i].getModifiers())) { if (fields[i].getType().isPrimitive()) { continue; // skip primitive fields } else { fields[i].setAccessible(true); try { // objects to be estimated are put to stack Object objectToAdd = fields[i].get(obj); if (objectToAdd != null) { stack.add(objectToAdd); } } catch (IllegalAccessException ex) { assert false; } } } } clazz = clazz.getSuperclass(); } return result; }}