一、方法 1、概述 方法,也可以稱之為函數,在其他語言中可能方法和函數的概念和語法是不同的,比如Python中的函數是可以在文件中獨立定義並存在的,而方法則是在類之中定義的函數,但是在Java中,方法和函數都指的是同一個語法,都是一樣的,既可以稱它為方法,也可以稱它為函數。需要註意以下幾點: 方法是 ...
一、方法
1、概述
方法,也可以稱之為函數,在其他語言中可能方法和函數的概念和語法是不同的,比如Python中的函數是可以在文件中獨立定義並存在的,而方法則是在類之中定義的函數,但是在Java中,方法和函數都指的是同一個語法,都是一樣的,既可以稱它為方法,也可以稱它為函數。需要註意以下幾點:
- 方法是定義在類體之中的。
- 類體之中的多個方法之間是沒有順序關係的。
- 方法體之中不能再定義方法。
2、定義方法
語法如下:
[修飾符列表] 返回值類型 方法名(形式參數列表){
方法體;
}
修飾符列表:這是可選項,不是必須的。如果不寫,則使用預設選項,對於訪問控制符,如public、private等,預設的訪問控制許可權為包範圍內。
返回值類型:使用“return 值”返回一個值,且這個值的類型必須和指定的返回值類型一致,指定的返回值類型可以是Java中的任何數據類型,包括基本數據類型和所有引用類型。如果此方法不返回任何值,就必須將其指定為void,表示此方法不返回任何值,即方法體中不能有“return 值”這樣的語句,但是可以寫“return;”表示返回void。
方法名:遵循標識符的定義規則,但通常應該註意以下幾點:
- 最好見名知意,不要為了方便省時,就寫func1、func2等這種看不出方法大概功能的方法名。
- 最好是動詞。
- 首字母小寫,其後的每個單詞的首字母大寫,即遵循駝峰命名法。
形式參數列表:
- 形參是局部變數,即它們的作用範圍只在方法體之內。
- 多個形參之間使用逗號隔開。
- 形參中起決定性作用的是其數據類型,形參的名字就是局部變數的名字。
- 在調用方法時給方法傳遞的真實數據被稱為“實際參數”,即實參。
- 對於實參列表和形參列表的使用,需要註意,它們的數量必須相同,而且對應的數據類型也必須一致。
方法調用:使用點號“.”進行調用,但是註意,方法體中的程式只有在調用時才會去執行,定義或編譯時都不會執行。
3、static方法調用
如果修飾符列表中有static的話,則稱之為靜態方法,調用此方法的語法格式為“類名.方法名(實際參數列表)”,但如果是調用本類中的static方法,則可以省略類名,直接使用方法名進行調用。
4、參數值的傳遞
在調用方法時,給方法傳遞的參數為變數的值(即值傳遞),而不是變數本身,因為如果傳遞的是變數本身,那豈不是就可以在調用的方法中使用這個變數了,但實際情況卻是,調用的方法只能使用自己這個作用域中的局部變數,而不能使用它的調用者所在的作用域中的變數。所以傳遞的就是變數的值,從而使得方法的形參有了自己的值。
5、方法重載(overload)
重載原則:重載的方法功能相似的時候可以考慮使用方法重載,但是功能不同的時候儘量使用方法名稱不同的方法來定義。
重載機制:
- 重載的方法必須在同一個類中。
- 重載的方法它們的方法名必須相同。
- 參數列表不同:包括參數數量不同,以及數量相同但順序上類型不同兩種情況。
- 註意:方法重載只和方法名與參數列表有關,與返回值與修飾符列表無關,即只要方法名和參數列表滿足重載機制,就算作方法的重載。
6、JVM記憶體分配
如果只是定義方法,不去調用,則不會被執行,在JVM中也不會給該方法分配運行所需的記憶體,只有在調用這個方法的時候才會動態的分配該方法所需的記憶體空間。
JVM的記憶體劃分中主要有三大塊的記憶體空間(當然也還有其他的記憶體空間):
- 方法區記憶體:專門用於代碼的存放,當然方法的代碼片段也是存放在這裡的。方法的代碼片段屬於“.class”文件的一部分,在JVM載入“.class”位元組碼文件的時候就會將對應的方法載入到方法區記憶體中,所以這三塊記憶體中最先有數據的就是方法區記憶體。並且方法代碼片段在記憶體中只有一份,但是可以重覆調用。
- 堆記憶體(heap):主要用於存放創建的實例對象,包括對象自身的數據,如屬性值等,因為對象的數據是存放在對象內部的。Java的垃圾回收機制主要針對的也是這塊記憶體區域,只要堆記憶體中的對象沒有其他地方對它進行引用的話就會被回收掉。
- 棧記憶體:在方法調用執行的時候,需要給該方法分配獨立的記憶體空間,這個記憶體空間就是在棧記憶體空間中進行分配的(方法在調用的瞬間,會在棧中給該方法分配獨立的記憶體空間,此時,會在棧中發生壓棧動作,方法執行結束之後,給該方法分配的記憶體空間就會被全部釋放,此時,會在棧中發生彈棧動作)。方法中的局部變數因為是在方法體中聲明定義,所以局部變數的記憶體空間也是在棧中分配的(即棧記憶體中主要存儲的是局部變數)。
- 關於棧(stack):棧是一種數據結構(數據結構反應的是數據的存儲形態,數據結構這個詞和概念是獨立的,並不只是在Java中有),棧中包括棧底元素、棧中元素、棧頂元素和棧幀,棧幀永遠指向棧頂元素,並且只有棧頂元素處於活躍狀態,其他元素則處於靜止狀態。
- 壓棧/入棧/push:將一個元素加入棧的棧頂。
- 彈棧/出棧/pop:將一個棧頂元素彈出棧。
- 棧數據結構的特點:先進後出,後進先出。
- 註意:堆記憶體和方法區記憶體都只是各有一個,但是棧記憶體是一個線程有一個棧記憶體。
7、方法遞歸
- 方法遞歸非常的耗費棧記憶體,所以能不用遞歸就儘量不用遞歸。
- 將程式的實現原理的複雜性進行封裝,只對外提供簡單的操作入口。
- 封裝之後才能形成真正的“對象”和“獨立體”的概念。
- 封裝意味著程式可以重覆使用。
- 封裝之後,對於對象本身,提高了安全性。
一個普通的類的定義語法如下:
[修飾符列表] class 類名{ 屬性; 方法; }
成員變數:在類體之中、方法之外的變數稱之為成員變數。成員變數如果沒有手動賦值的話,系統會自動賦予預設值(一切向“0”看齊)。成員變數又分為:
- 實例變數(沒有static修飾符)
- 靜態變數(有static修飾符)
註意:類也是一種數據類型,屬於引用數據類型,它的類型名稱就是對應的類名。
2、對象創建和記憶體分配
對象就是類實例化之後的具體個體,類到對象的過程稱之為實例化,反過來,對象到類的過程則稱之為抽象。
new關鍵字:Java中使用new關鍵字來創建一個對象,new關鍵字也是Java中的一個運算符。
記憶體分配:當在方法區記憶體中的代碼執行時,會在棧記憶體中開闢一塊該方法對應的記憶體空間,而在方法執行過程中使用new關鍵字創建一個對象時,則會在堆記憶體中開闢一塊該對象對應的記憶體空間。所以方法中定義的局部變數是在棧中的,而創建的對象則是在堆記憶體中的。實例對象每一個對象都會有自己的一塊記憶體空間,即100個對象就會分配100個記憶體空間。
指針屏蔽:Java中想要訪問堆記憶體中的數據,必須通過引用,而不能直接操作堆記憶體,因為Java中屏蔽了指針的概念,不能通過指針的方式直接訪問或操作記憶體中的數據。
訪問屬性:對於實例變數屬性的讀取和修改,使用語法格式“引用.變數名”進行讀取,使用語法格式“引用.變數名=值”對屬性進行修改。註意,實例變數存儲在堆記憶體中對應的實例對象內部,且不能通過類名的方式來訪問。
3、空指針異常NullPointerException
當一個引用類型的變數的值不再是指向某個對象的記憶體地址,而是null,此時再去訪問對象的相關屬性或方法就會發生空指針異常,因為此時的變數不再指向該對象,而是值為null了,無法去訪問該對象了,更不要說訪問對象中的屬性和方法了,空引用訪問實例相關數據就一定會出現空指針異常。
4、get方法和set方法
屬性私有化:在封裝特性中,類中的所有屬性都應該使用private修飾符進行修飾,private表示私有的,表示此屬性只有在本類中才能訪問,在類的外部不能訪問。但是在類中應該為外部訪問這些屬性提供一些簡單的公開的(public)操作入口,如對應的get方法和set方法。
get方法和set方法的寫法如下:
// get方法 public 返回值類型 get+屬性名首字母大寫(){ return 屬性名; } // set方法 // 註意,形參的名字如果和屬性名相同了,那麼屬性名前面應該加一個this關鍵字 // 因為不加this關鍵字的話,由於名稱是相同的,Java的就近原則會認為它倆都是同一個局部變數,即形參 public void set+屬性名首字母大寫(形參列表){ 屬性名=形參值; ... }
註意:get和set方法是沒有static修飾符的,使用的是public修飾符,沒有static修飾符的方法的訪問方式為“引用.方法名(實參)”。
5、引用參數的傳遞
對象變數通常也稱之為引用,因為在棧中這個變數只是個局部變數,而對象變數的值是該對象在堆記憶體中的記憶體地址,當然,這個記憶體地址則指向堆記憶體中的該對象實例。所以對於基本數據類型,值的傳遞不會影響到原本變數的值,但是對於類的實例,因為傳遞的值是記憶體地址,所以它雖然不會影響原本局部變數的值(即記憶體地址),但是如果對記憶體地址中的對象實例進行修改則會影響到記憶體地址指向的實例對象,即原本的局部變數指向的實例對象會被修改。
6、構造方法(constructor)
語法如下:
// 構造方法,也稱為構造器(constructor)。 // 構造方法是不用也不能指定返回值類型的。 // 註意,構造方法名必須和類名相同,所以這裡的語法就直接寫類名了。 [修飾符列表] 類名(形式參數列表){ 構造方法體; }
示例:
public class A{ private int i; // 下麵的兩個構造方法使用了方法的重載機制 public A(){ System.out.println("類A的無參構造方法!"); } public A(int i){ // 使用this關鍵字區分實例變數和方法的局部變數 this.i = i; System.out.println("類A的有參構造方法!"); } }
構造方法的調用:構造方法的作用是通過調用構造方法來創建對象並初始化實例變數的值,而構造方法的調用使用new關鍵字“new 構造方法名(實參列表)”,註意new之後調用的其實是構造方法名而不是類名,但因為兩者是相同的,所以可能會讓人誤以為調用的是類名。
構造方法返回值:雖然沒有指定返回值類型,但是構造方法的返回值類型就是其所在類的類型,返回值就是新創建的對象的引用,但是註意的是這個返回值是不需要開發人員手動編寫的,即構造方法的定義中,返回值類型和返回值都不需要人為的去定義。
預設構造方法:當類中沒有定義構造方法時,系統會給該類提供一個無參數的預設構造器。需要特別註意的是,如果類中提供了構造方法,那麼系統就不再為這個類提供預設的無參數構造方法了,所以,如果在類中提供了自己的構造方法,那麼推薦手動將無參的構造方法加上,因為這個構造方法太常用了。
關於構造方法,還應該註意以下幾點:
- 構造方法支持重載機制。
- 因為實例變數是屬於實例的,所以構造方法是先創建對象再初始化實例變數。
7、this關鍵字
其實每一個實例對象中都有一個this變數,this中保存的是自身所在實例對象的記憶體地址,即this是指向實例對象本身的一個引用類型的變數。可以換一種方式理解,this可以出現在實例方法中,而方法中的this代表當前正在執行這個方法動作的實例對象。
在實例方法中對實例變數的訪問,由於它是實例變數,所以不使用this關鍵字也是可以訪問的,所以this在多數情況下是可以不寫的。this主要用於區分實例變數和局部變數,比如setter方法和構造方法中就比較常用。
當然,this不能在含有static修飾符的方法中使用。
this關鍵字除了使用“this.xxx”的方式表示實例對象的使用之外,還可以在構造方法中以“this(實參列表)”形式表示調用本類的另一個構造方法,但是註意,使用這種用法時這個語句只能出現在構造方法的第一行(當然這個語句之後可以添加其他的語句,但前面就不能有其他任何語句了),如:
public class User{ private int age; public User(int age){ this.age = age; } public User(){ // 此處表示調用另一個構造方法 // 但是註意,這個語句只能是此構造方法的第一個語句 this(18); // 之後可以加別的語句 System.out.println("my age is " + this.age); } }
8、繼承
繼承特性優點:繼承最基本的作用是代碼復用,但是最重要的作用卻是有了繼承才有了方法的覆蓋和多態機制。
單繼承:Java中的繼承機制只支持單繼承,一個類不能同時繼承多個類,只能繼承一個類。語法如下:
// 繼承使用extends關鍵字 [修飾符列表] class 類名 extends 父類名{ 類體; }
可以繼承的數據:
- private私有的不支持繼承。
- 構造方法不支持繼承。
- 其他數據可以被繼承。
多繼承:Java中雖然只支持單繼承,但是可以間接實現多繼承:
C extends B{ } B extends A{ } A extends T{ } // 這樣C直接繼承B,但間接繼承了T和A類
預設基類:Java中一個類如果沒有顯式繼承任何類,那麼該類預設繼承javaSE庫中提供的java.lang.Object類。
需要註意一個概念,當一個子類在繼承某個父類時,在運行時,不是說在子類中查找對應方法或屬性,子類中沒有再到父類中查找,而是在定義時,如果繼承了某個父類,那麼這個類的定義中就包含了父類繼承過來的某些方法和屬性,即子類對象執行的方法和屬性總是自己的屬性和方法。
9、方法的覆蓋/重寫(override)
方法的覆蓋也稱為方法的重寫,子類將父類繼承過來的方法進行重新編寫被稱為方法的重寫,方法重寫時需要註意:
- 方法重寫發生在具有繼承關係的父子類之間,且是可以繼承的方法上(私有的以及構造方法不能繼承,也就不能進行重寫了)。
- 重寫時必須遵守:返回值類型相同,方法名相同,形參列表相同。
- 訪問許可權不能更低,但是可以更高,private最低,public最高。
- 拋出異常不能更多,但是可以更少。
- 靜態方法不存在重寫。
- 覆蓋只談方法,不談屬性。
10、多態
向上轉型(upcasting):子類型 --> 父類型,可以理解為自動類型轉換。
向下轉型(downcasting):父類型 --> 子類型,可以理解為強制類型轉換。
無論是向上轉型還是向下轉型,都必須具有繼承關係,不然編譯不通過。
多態語法機制:父類型的引用指向子類型對象這種機制導致程式在編譯階段和運行階段出現了兩種不同的形態或狀態,這種機制可以稱為一種多態語法機制。
多態的作用:降低程式的耦合度,提高程式的擴展力。能使用多態就多使用多態,即父類型引用指向子類型對象。
多態的核心思想:面向抽象編程,儘量不要面向具體編程。
示例:重點在註釋哦
public class Animal{ public void run(){ System.out.println("動物在移動!"); } } public class Cat extends Animal{ public void run(){ System.out.println("貓在散步!"); } public void catchMouse(){ System.out.println("貓在抓老鼠!"); } } public class Bird extends Animal{ public void run(){ System.out.println("鳥兒在飛翔!"); } } public class Test{ public static void main(String[] args){ // 此處為向上轉型,從Cat類型自動轉換為Animal類型 Animal cat1 = new Cat(); // 向上轉型之後,可以訪問父類型中的方法,但是如果這個方法被子類型中重寫了 // 那麼執行的就是子類型中的方法了,並且類型轉化之後不能再執行子類型中特有的方法了 // 比如catchMouse方法,但是需要註意的是,雖然類型轉換了,但是引用指向的堆記憶體中的 // 對象依然是最開始創建的Cat類型的源對象cat1,所以執行方法時原則就是子類型中沒有就執行繼承自父類型的方法,如果子類型中有這個方法時就執行子類型中的方法,但是不能執行子類型中特有的方法。 // 在編譯階段會將符合語法的該對象的方法綁定,這個過程稱之為靜態綁定,只有靜態綁定成功之後才能運行程式。這個例子中,靜態綁定是將Animal的move方法綁定到cat1對象,因為cat1是聲明為Animal類型的,而Animal類是有move方法的,所以能綁定成功。 // 在運行階段則會將實際運行的方法綁定到該對象上,這個過程稱之為動態綁定,這個例子中,動態綁定是,在運行時,由於是先在記憶體中生成的對象是new出來的Cat類型的對象,雖然在等號賦值運算時類型被轉換為Animal類型了,但是記憶體中其實還是那個被創建好的Cat類型的cat1對象,所以會執行Cat類中的move方法。 cat1.run(); // 輸出為:貓在散步! // 此處會編譯不通過,雖然cat對象有catchMouse方法,但是類型轉換後,因為Animal類型中沒有catchMouse方法,所以編譯不通過,即靜態綁定失敗。當然,也就不可能繼續運行了。 cat1.catchMouse(); // 向下轉型,這裡不僅能編譯通過,還能正確執行catchMouse方法,因為cat1其本質就是最初在記憶體中創建的Cat類型對象,而Cat類是由這個方法的 Cat cat2 = (Cat)cat1; cat2.catchMouse(); // 此處的向下轉型編譯能能通過,但是運行會報錯java.lang.ClassCastException(除了空指針異常之外另一個著名的異常),即類型轉換異常,而且只有在向下轉型的時候會發生。 // 因為第一個語句向上轉型後,其實際還是個Bird類型對象,在第二個語句的向下轉型,因為 // Animal類型和Cat類型之間具有繼承關係,所以可以編譯通過,但是運行時由於它本質是Bird類型 // 對象,不能轉換成Cat類型對象,因為Bird和Cat之間沒有繼承關係,所以會報錯。 Animal bird1 = new Bird(); Cat cat3 = (Cat)bird1; } }
11、instanceof運算符
語法:“引用 instanceof 數據類型名”,返回值為true/false,true表示這個引用指向的記憶體真實對象就是該數據類型的對象,false則表示這個引用指向的記憶體真實對象不是該數據類型的對象。如上例中“Animal bird1 = new Bird();”的bird1雖然轉換成了Animal類型,但其真實記憶體對象其實是Bird類型的,所以如果執行“bird1 isinstanceof Bird”就會返回true。
Java編程規範中,在進行強制類型轉換時,建議先使用instanceof運算符判斷引用的類型再進行轉換。
三、修飾符
1、static
靜態變數:帶有static關鍵字的變數,稱之為靜態變數,並且,在類載入的時候就靜態變數開始初始化了,不需要創建對象它的記憶體就已經開闢了,並且是存儲在方法區中的。
靜態方法:帶有tatic修飾符的方法稱為靜態方法,在靜態方法中不能訪問實例變數和實例方法,當然,也包括this關鍵字,而是只能訪問同樣帶有static修飾符的變數(靜態變數)。
使用原則:當一個方法或變數它的執行與具體的對象無關,或者說所有對象都會用到這個方法或變數,並且還不會因為對象的不同而發生變化,此時就應該將它定義為static類型。而當一個行為或動作執行的過程中需要對象參與,或者說不同對象執行這個動作的結果可能會不同,那麼這個方法就應該定義為實例方法,不應該加static關鍵字。同理,當一個屬性在不同的對象中可能會不同時,那這個屬性就應該定義為實例變數,也不應該加static關鍵字。
訪問static變數和方法:帶有static的方法和變數,可以使用類名的方式去訪問,也可以使用引用的方式去訪問,但使用引用的方式去訪問,其實本質上也是使用類名的方式去訪問的,因為你會發現當這個引用為null時也能去訪問static的方法和變數,而不會報空指針異常,所以不推薦使用引用的方式去使用static方法。
static另一種語法的使用:靜態代碼塊,在類載入的時候就會去執行這個方法,定義和使用示例如下:
// 語法 static{ java語句; } // 靜態代碼塊在一個類中可以編寫多個
public class StaticTest{ static{ System.out.println("--->1"); } static{ System.out.println("--->2"); } public static void main(String[] args){ System.out.println("main method!!!"); } } // 運行結果為:可以看到靜態代碼塊在main方法之前運行了。 // --->1 // --->2 // main method!!!
2、final
final關鍵字的使用,需要註意以下幾點:
- final是一個關鍵字,表示最終的、不可變的。
- final修飾的類無法被繼承。
- final修飾的方法無法被覆蓋。
- final修飾的變數一旦賦值之後,不可重新賦值。
實例變數如果聲明為final變數,那麼在聲明的同時就需要給它賦值,不然就會報錯。因為類的實例變數在調用構造方法之後還沒有被賦值的話就會被系統賦予預設值,而final變數是不能重新賦值的,所以如果允許final實例變數聲明的時候不賦值,那麼這個變數將永遠是系統預設的值,這樣肯定是不行的,所以語法上就要求final的實例變數必須在聲明的同時必須手動賦值或者在構造方法中給它賦值。示例如下:
public class A{ // 第一種方式:聲明的同時賦值 final int a = 10; // 第二種方式:先聲明,然後在構造方法中賦值 final int b; public A(){ this.b = 20; } // 註:其實這兩種方式都是一種方式,都是在構造方法執行過程中給實例變數賦值的。 }
註意,final修飾的引用雖然不能再指向其他對象,但是所指向的對象內部的記憶體是可以被修改的。
final修飾的實例變數通常會和static聯合使用,被稱為常量。
3、常量
語法格式:“public static final 類型 常量名 = 值”。final表示不可被修改,static表示無論實例化多少對象都只會保存一份數據在方法區記憶體中,此時,就算被聲明為public也不用擔心被別人修改,因為它本身就是final不可被修改的。
Java規範中,所有常量必須全部使用大寫,單詞之間使用下劃線連接。
4、訪問控制許可權修飾符
對於屬性和方法,以下4種都可以使用,但是對於類,只可以使用public和預設的方式定義,但是無論是類還是屬性和方法,這4中的作用範圍都是相同的:
- public:表示公開的,在任何位置都可以訪問。
- protected:在同一個包下,或者在其子類中,那就可以訪問。
- 預設:只允許在同一個包下的類訪問。
- private:表示私有的,只能在本類中可以訪問。
- 修飾符的作用範圍:private < 預設 < protected < public。