(復習瘋狂Java的筆記)
1.實例變量和類變量
Java程序的變量大體可分為成員變量和局部變量。其中局部變量可分為如下二類。
- 形參:在方法簽名中定義的局部變量,由方法調用者負責為其賦值,隨方法的結束而消亡。
- 方法內的局部變量:在方法內定義的局部變量,必須在方法內對其進行顯式初始化口這種類型的局部變量從初始化完成后開始生效,隨方法的結束而消亡。
- 代碼塊內的局部變量:在代碼塊內定義的局部變量,必須在代碼塊內對其進行顯式初始化。這種類型的局部變量從初始化完成后開始生效,隨代碼塊的結束而消亡。
局部變量的作用時間很短暫,他們都被存儲在棧內存中。
類體內定義的變量被稱為成員變量〔英文是Field)。如果定義該成員變量時沒有使用static
修飾,該成員變量又被稱為非靜態變量或實例變量;如果使用了static修飾,則該成員變量又可被稱為靜態變量或類變量
(坑:表面上看定義成員變量是沒有先后順序的,實際上還是要采用合法的前向引用)如:
int num=num2+1;
int num2=2;
是會報錯的,出得num2位靜態比變量的時候。
2.實例變量和類變量的屬性
使用static修飾的成員變量是類變量,屬于該類本身:沒有使用屬于該類的實例。在同一個JVM內,侮個類只對應一個
Java對象口static修飾的成員變量是Class對象,但侮個類可以創建多個
由于同一個JVM內每個類只對應一個static對象,因此同一個JVM內的一個類的類變量只需一塊內存空間;但對于實例變量而言,改類每創建一次實例,就需要為實例變量分配一塊內存空間。也就是說,程序中有幾個實例,實例變量就需要幾塊內存空間。
3.實例變量的初始化時機
對于實例變量,它是Java對象本身。每創建Java對象時都需要為實例變量分配內存空間,并對實例進行初始化。
程序可以在三個地方進行初始化:
- 定義實例變量時指定初始值。
- 非靜態初始化塊中對實例變量指定初始值。
- 構造器中對實例變量指定初始值。
其中第1,2種方式都比在構造器初始化更早執行,當第1,2種的執行順序與他們在源程序中的排列順序相同。
4.類變量的初始化時機
類變量是屬于Java類本身。從程序運行的角度來看,每個jvm對一個Java類只初始化一次,因此只有每次運行Java程序時,才會初始化該Java類,才會為該類的類變量分配內存空間,并執行初始化。
程序可以在兩個地方對類變量執行初始化:
- 定義類變量時指定初始值。
- 靜態初始化塊中對類變量指定初始值。
這兩種方式的執行順序與它們在源程序中的排列順序相同。
父類構造器
1.隱式調用和顯式調用
當創建Java對象時,系統會先調用父類的非靜態初始化塊進行初始化。而這種調用是隱式調用。而第一次初始化時最優先初始化的是靜態初始化塊。接著會調用父類的一個或多個構造器進行初始化,這個調用是用過super()的方法來顯式調用或者隱式調用。當所有父類初始化完之后才初始化子類。實例代碼如下:
class Animal{
static{
System.out.println("Animal靜態初始化塊");
}
{
System.out.println("Animal初始化塊");
}
public Animal(){
System.out.println("Animal構造器");
}
}
class Cat extends Animal{
public Cat(String name,int age){
super();
System.out.println("Cat構造器");
}
static{
System.out.println("Cat靜態初始化塊");
}
{
System.out.println("Cat初始化塊");
weight=2.0;
}
double weight=2.3;
public String toString(){
return "weight="+weight;
}
}
public class JavaTest {
public static void main(String[] args) {
Cat cat=new Cat("kitty",2);
System.out.println(cat);
// Cat cat2=new Cat("Garfied",3);
// System.out.println(cat2);
}
}
輸出的結果是:
2訪問子類對象的實例變量
子類因為繼承父類所以可以訪問父類的成員方法和變量,當一般情況下父類是訪問不了子類的,因為父類不知道哪個子類繼承。但是在特殊情況下是可以的,如下代碼:
class BaseClass{
private int i=2;
public BaseClass(){
this.display();
}
public void display(){
System.out.println("BaseClass");
System.out.println(i);
}
}
class Derived extends BaseClass{
private int i=22;
public Derived(){
i=222;
}
public void display(){
System.out.println("Derived");
System.out.println(i);
}
public void sub(){
System.out.println("sub");
}
}
public class JavaTest {
public static void main(String[] args) {
Derived derived=new Derived();
}
}
結果如下:
仔細看代碼,好像怎么也不會輸出0吧,為什么呢。
首先我們要知道Java構造器只是起到對變量進行初始化的作用,而在執行構造器之前我們的對象已經初始化了,在內存中已經被分配起來了,而這些值默認是空值。
其次this在代表正在初始化的對象,一般看會以為就是BaseClass對象,不過在上面代碼里,this是放在BaseClass的構造器里,當時我們是在Derived()構造器執行的,是Derived()構造器隱式調用了BaseClass()構造器的代碼,所以在這個情況下是this是Derived對象。所以當我們改為this.sub()時是報錯的。
此外這個this的編譯類型是BaseClass,所以我們改為this.i的時候輸出是2.
所以應該避免在父類構造器中調用被子類重寫的方法。
父子實例的內存控制
1.繼承成員變量和繼承方法的區別
class Animal{
public String name="Animal";
public void sub(){
System.out.println("AnimalSub");
}
}
class Wolf extends Animal{
public String name="Wolf";
public void sub(){
System.out.println("WolfSub");
}
}
public class JavaTest {
public static void main(String[] args) {
Animal animal=new Animal();
System.out.println(animal.name);
animal.sub();
Wolf wolf=new Wolf();
System.out.println(wolf.name);
wolf.sub();
Animal sub=new Wolf();
System.out.println(sub.name);
sub.sub();
}
}
結果如下:
所以當聲明類型為父類,運行類型為子類是,成員變量表現出父類,而方法表現出子類,這就是多態。
2.內存中的子類實例
class Fruit{
String color="未確定顏色";
public Fruit getThis(){
return this;
}
public void info(){
System.out.println("Fruit方法");
}
}
public class JavaTest extends Fruit{
@Override
public void info() {
System.out.println("JavaTest方法");
}
public void AccessSuperInfo(){
super.info();
}
public Fruit getSuper(){
return super.getThis();
}
String color="紅色";
public static void main(String[] args) {
JavaTest javaTest=new JavaTest();
Fruit f=javaTest.getSuper();
System.out.println("javaTest和f所引用的對象是否相同:"+(javaTest==f));
System.out.println("所引用對象的color實例變量:"+javaTest.color);
System.out.println("所引用對象的color實例變量:"+f.color);
javaTest.info();
f.info();
javaTest.AccessSuperInfo();
}
}
當創建一個對象時,系統不僅為該類的實例變量分配內存,同時也為其父類定義的所有實例變量分配內存,即是子類定義了與父類同名的實例變量。也就是說,當系統創建一個Java對象時,如果該Java類有兩個父類(一個直接父類A,一個間接父類g ),假設A類中定義了2個實例變量,B類
中定義了3個實例變量,當前類中定義了2個實例變量,那么這個Java對象將會保存2+3十2個實例變量。
如果子類里定義了與父類中已有變量同名的變量,那么子類中定義的變量會隱藏父類中定義的變量,而不是覆蓋。因此系統創建子類對象是依然會為父類定義的,被隱藏的變量分配內存空間。
為了在子類中訪問父類定義的,被隱藏的變量和方法,可以使用super來限定修飾這些變量和方法。
3.父,子類的類變量
如果在子類中要訪問父類中被隱藏的靜態變量和方法,程序有兩種方式:
- 直接使用父類的類名作為主調來訪問類變量
- 使用super.作為限定來訪問類變量
一般情況下,都建議使用第一種方式訪問類變量,因為類變量屬于類本身,使用類名做主調來訪問可以較好的可讀性
final修飾符
1.final 修飾的變量
final修飾的實例變量必須顯示指定初始值,只能在如下三個位置指定初始值。
- 定義final實例變量時指定初始值
- 在非靜態初始化塊中為final實例變量指定初始值
- 在構造器中為final實例變量指定初始值
對于普通實例java可以指定默認初始化,而final實例變量只能顯示指定初始化。
2.執行‘宏替換’的變量
在定義時final類變量指定了初始值,該初始值在編譯時就被確定下來,這個final變量本質上已經不再是變量而是一個直接量,如果被賦的表達式只是基木的算術表達式或字符串連接運算,沒有訪問普通變量,調用方法,Java編譯器同樣會將這種final變量當成“宏變量”來處理。
3.final方法不能重寫
如果父類中某個方法使用了final修飾符進行修飾,那么這個方法將不可能被他的子類訪問到,因此這個方法也不可能被他的子類重寫。從這個層面說,private和final同時修飾某個方法沒有太大的意義,但是被java語法允許。
4.內部類中的局部變量
Java要求所有被內部類訪問的局部變量都使用final修飾也是有其原因的。對于井通的局部變量而言,‘它的作用域就停留在該方法內,當方法執行結束后,該局部變量也隨之消失;但內部類則可能產生隱式的“閉包(Closure)”,閉包將使得局部變量脫離它所在的方法繼續存在。