類和物件
定義類
面向物件的程式設計程序中有兩個重要概念:類(class)和物件(object,也被稱為實體,instance),其中類是某一批物件的抽象,可以把類理解成某種概念;物件才是一個具體存在的物體
Java語言是面向物件的而程式設計語言,類和物件是面向物件的核心
Java語言里定義的簡單語法如下:
[修飾符] class 類名
{
零個到多個構造器定義...
零個到多個成員變數..
零個到多個方法..
}
類里各成員之間的定義順序沒有任何影響,各成員之間可以相互呼叫,但需要指出的是,static修飾的成員不能訪問沒有static修飾的成員
成員變數用于定義該類或該類的實體所包含的狀態資料,方法則用一定義該類或該類的實體的行為特征或者功能實作,構造器用于構造該類的實體,Java語言通過new關鍵字來呼叫構造器,從而回傳該類的實體
構造器是一個類創建實體的根本途徑,如果一個類沒有構造器,這個類通常無法創建實體,因此Java語言提供了一個功能:如果程式員沒有為一個類撰寫構造器,則系統會為該類提供一個默認的構造器,一旦程式員為一個類提供了構造器,系統將不再為該類提供構造器
定義成員變數的語法格式
[修飾符] 型別 成員變數名 [=默認值];
- 修飾符:修飾符可以省略,也可以是public、protected、private、static、final,其中public、protected、private三個最多只能出現其中之一,可以與static、final組合起來修飾成員變數
- 型別:Java語言允許的任何資料型別,包括基本型別和現在介紹的參考型別
- 成員變數名:合法識別符號即可,最好由一個或多個有意義的單詞連綴而成
定義方法的語法格式
[修飾符] 方法回傳值型別 方法名(形參串列)
- 方法回傳值型別:Java語言允許的任何資料型別,包括基本型別和現在介紹的參考型別,如果宣告了回傳值型別,則方法體內必須有一個有效的return陳述句,回傳一個與回傳值型別相匹配的變數或運算式,如果沒有回傳值,則必須使用void來宣告沒有回傳值
- 形參串列:定義方法可以接受的引數,形參串列由零組到多組“引數型別 形參名”組成,多組引數以
,
隔開,形參型別與形參名以空格隔開,一旦方法中定義了引數串列,呼叫該方法時必須傳入對應的引數值
構造器的語法格式
[修飾符] 方法名(形參串列)
{
// 由零到多條可執行陳述句組成的構造器執行體
}
構造器既不能定義回傳值型別,也不能使用void宣告構造器沒有回傳值,如果定義了回傳值型別,或者使用void宣告構造器沒有回傳值,則Java會將這個所謂的構造器當成方法類處理——它就不再是構造器
物件的產生和使用
創建物件的根本途徑是構造器,通過new
關鍵字來呼叫某個類的構造器即可創建這個類的實作類
// 通過new關鍵字呼叫Person類的構造器,回傳一個Person實體
// 將該Person實體賦給p變數
Person p = new Person();
物件、參考和指標
類是一種參考資料型別,因此程式中的Person型別的變數實際上是一個參考,它被存放在堆疊記憶體里,指向實際的Person物件;而真正的Person物件則存放在堆記憶體中
堆疊記憶體里的參考變數并未真正存盤物件的成員變數,物件的成員變數資料實際存放在堆記憶體里;而參考變數只是只想該堆記憶體里的物件,
當一個物件被創建成功以后,這個物件將保存在堆記憶體中,Java程式不允許直接訪問堆記憶體中的物件,只能通過該物件的參考操作該物件
物件的this參考
Java提供了一個this關鍵字,this關鍵字總是只想呼叫該方法的物件,根據this出現位置的不同,this作為物件的默認參考有兩種情形
- 構造器中應用該構造器正在初始化的物件
- 方法中參考呼叫該方法的物件
this關鍵字最大的作用就是讓類中一個方法,訪問該類里的另一個方法或實體變數
public class Dog
{
public void jump()
{
System.out.println("正在執行jump方法");
}
public void run()
{
this.jump();//呼叫當前實體的jump方法
//Java物件中的一個成員呼叫另一個成員可以省略this前綴,因此,也可以改為如下形式
//jump();
System.out.println("正在執行run方法");
}
}
static修飾的方法中不能使用this參考
靜態成員不能訪問非靜態成員
方法
方法時類或物件的行為特征的抽象,方法是類或物件最重要的組成部分,Java里的方法不能獨立存在,所有的方法必須定義在類里,方法在邏輯上要么屬于類,要么屬于物件,
- 方法不能獨立定義,方法只能在類體里定義
- 從邏輯意義上來看,方法要么屬于該類本身,要么屬于該類的一個物件
- 永遠不能獨立執行方法,執行方法必須使用類或物件作為呼叫者,
方法引數的傳遞機制
Java里方法的引數傳遞方式只有一種:值傳遞,指將實際引數值的副本(復制品)傳入方法內,而引數本身不會受到任何影響
public static void swap(int a,int b)
{
int tmp = a;
a = b;
b = tmp;
System.out.println("swap方法中 a="+a+",b="+b);
}
public static void main(String[] args)
{
int a = 5;
int b = 8;
swap(a, b);
System.out.println("交換結束后,a="+a+",b="+b);
}
運行結果
swap方法中 a=8,b=5
交換結束后,a=5,b=8
在main()方法中呼叫swap()方法時,main()方法還未結束,因此,系統分別為main()方法和swap()方法分配兩塊堆疊區,用于保存main()方法和swap()方法的區域變數,main()方法中的a、b變數作為引數傳入swap()方法,實際上是在swap()方法堆疊區中重新產生了兩個變數a、b,并將main()方法堆疊區中a、b變數分別賦給swap()方法戰區中的a、b引數
當系統開始執行方法時,系統為形參執行初始化,就是把實參變數的值賦值給方法的形參變數,方法里操作的并不是實際的形參變數
Java對于參考型別的引數傳遞,一樣采用的值傳遞方式,不過傳遞的不是物件本身,而是物件的參考,因此在被呼叫的方法內修改引數參考的物件時,由于參考的是同一個物件,所以原堆疊區變數參考的物件也會同時修改
形參個數可變
JDK1.5之后,Java允許定義形參個數可變的引數,從而允許為方法指定數量不確定的形參,
如果在定義方法時,在最后一個形參的型別后加三點(...),則表明該形參可以接收多個引數值,多個引數值會被當成陣列傳入,
public static void test(int a,String... books){
// books會被當成陣列處理
for(String tmp : books){
System.out.println("tmp");
}
}
// 呼叫test方法
test(3,"測驗1","測驗2");
// 呼叫test方法傳入陣列
test(3,new String[]{"測驗1","測驗2"});
注意,個數可變的形參只能處于形參串列的最后,一個方法中最多只能包含一個個數可變的形參,個數可變的形參本質就是一個陣列型別的形參,因此既可以傳入多個引數,也可以傳入一個陣列
方法多載
Java 允許同一個類定義多個同名方法,如果同一個類中包含了兩個或兩個以上的方法名相同,但形參串列不同,則被稱為多載,
方法多載的要求時兩同一不同:同一個類中方法名相同,形參串列不同,
public class Overload {
public void test(){
System.out.println("無引數");
}
public void test(String msg){
System.out.println("多載的test方法 " + msg);
}
public void test(String... msg){
System.out.println("形參個數可變的test方法 " + Arrays.toString(msg));
}
}
public static void main(String[] args) {
Overload overload = new Overload();
//當呼叫方法時,將會根據傳入的實參串列匹配
overload.test();
overload.test("hello");
overload.test("h1","h2");
}
輸出
無引數
多載的test方法 hello
形參個數可變的test方法 [h1, h2]
如果同時包含了型別相同并且 個數可變的形參和一個形參的多載方法,當傳入一個引數時會優先呼叫一個引數的方法,如果需要呼叫形參個數可變的方法則需要通過傳入陣列的方式呼叫,例如
overload.test(new String[]{"hello"});
成員變數 和 區域變數
成員變數指的是在類里定義的變數,區域變數指的是在方法里定義的變數
成員變數被分為靜態變數和實體變數兩種,定義成員變數時沒有static修飾的就是實體變數,有static修飾的就是靜態變數,其中靜態變數從該類的準備階段起開始存在,直到系統完全銷毀這個類,靜態變數的作用域與這個類的生存范圍相同;而實體變數則從該類的實體被創建起開始存在,直到系統完全銷毀這個實體,實體變數的作用域與對應實體的生存范圍相同
區域變數根據定義形式的不同,可以被分為一下三種
- 形參:在定義方法簽名時定義的變數,形參的作用域在整個方法內有效
- 方法區域變數:在方法體內定義的區域變數,它的作用域是從定義該變數的地方生效,到該方法結束時失效
- 代碼塊區域變數:在代碼塊中定義的區域變數,這個區域變數的作用域從定義該變數的地方生效,到該代碼塊結束時失效
java語法中允許通過實體呼叫靜態成員,但是static修飾的成員屬于類本身,不屬于實體本身,所以盡量不要使用實體物件去呼叫靜態成員,
構造器
什么是構造器
構造器是一個特殊的方法,這個特殊方法用于創建實體時執行初始化,
public class ConstructorTest {
public String name;
public int count;
// 提供自定義的構造器,該構造器需要兩個引數
public ConstructorTest(String name, int count){
// 初始化成員變數
this.name = name;
this.count = count;
}
public static void main(String[] args) {
//使用自定義構造器來創建物件
ConstructorTest tc = new ConstructorTest("張三",20);
System.out.println(tc.name);
System.out.println(tc.count);
}
}
如果程式員沒有為Java類提供任何構造器,則系統會為這個類提供一個無引數的構造器,這個構造器的執行體為空,不做任何事情,無論如何,Java類至少包含一個構造器
一旦程式員提供了自定義的構造器,系統就不再提供默認的構造器
構造器多載
同一個類里具有多個構造器,多個構造器的形參串列不同,即被稱為構造器多載
public class ConstructorTest {
public String name;
public int count;
public double price;
// 兩個引數構造器
public ConstructorTest(String name, int count){
this.name = name;
this.count = count;
}
// 三個引數構造器
public ConstructorTest(String name, int count,double price){
// 通過this呼叫另一個構造器的初始化代碼
this(name,count);
this.price = price;
}
}
使用this可以呼叫另一個多載的構造器只能在構造器,而且必須作為構造器執行體的第一條陳述句,使用this呼叫多載的構造器時,系統會根據this后括號里的實參來呼叫形參串列與之對應的構造器,
訪問修飾符
-
private(當前類訪問權限):只能在當前類的內部被訪問
-
default(包訪問權限):如果一個類里的成員或者一個外部類不適用任何訪問控制符修飾,就稱它是包訪問權限的,default訪問控制的成員或外部類可以被相同包下的其他類訪問
-
protected(子類訪問權限):即可以被同一個包中的其他類訪問,也可以被不同包中的子類訪問,
-
public(公共訪問權限):可以被所有類訪問
private | default | protected | public | |
---|---|---|---|---|
同一個類中 | √ | √ | √ | √ |
同一個包中 | √ | √ | √ | |
子類中 | √ | √ | ||
全域范圍內 | √ |
封裝和隱藏
封裝(Encapsulation)是面向物件的三大特征之一,它指的是將物件的狀態資訊隱藏在物件內部,不允許外部程式直接訪問物件內部資訊,而是通過該類所提供的方法來實作對內部資訊的操作和訪問
對一個類或物件實作良好的封裝,可以實作以下目的
- 隱藏類的實作細節
- 讓使用者只能通過事先預定的方法來訪問資料,從而可以在該方法里加入控制邏輯,限制對成員變數的不合理訪問
- 可進行資料檢查,從而有利于保證物件資訊的完整性
- 便于修改,提高代碼的可維護性
實作良好的封裝,需要從兩個方面考慮
- 將物件的成員變數和實作細節隱藏起來,不允許外部直接訪問
- 把方法暴漏出來,讓方法來控制對這些成員變數進行安全的訪問操作
類的繼承
繼承是面向物件三大特征之一,也是實作軟體復用的重要手段,Java的繼承具有單繼承的特點,每個子類只能有一個父類,
繼承的特點
Java的繼承通過extends
關鍵字來實作,實作繼承的類被稱為子類,被繼承的類被稱為父類
Java 里子類繼承父類的方式只需在原來的類定義上增加extends SuperClass
(父類)即可
修飾符 class SubClass extends SuperClass{
//類定義部分
}
子類可以從其父類中獲得成員變數、方法和內部類(包括內部介面,列舉),不能獲得構造器和初始化塊
重寫父類的方法
子類包含與父類同名方法的現象被稱為方法重寫(Override),也被稱為方法覆寫,
方法的重寫要遵循“兩同兩小一大”規則:“兩同”即方法名相同、形參串列相同;“兩小”指的是子類方法回傳值型別應比父類方法回傳值型別更小或相等,子類方法宣告拋出的例外類應比父類方法宣告拋出的例外類更小或相等;“一大”指的是子類方法的訪問權限比父類方法的訪問權限更大或相等
覆寫方法和被覆寫方法要么都是類方法,要么都是實體方法
super限定
super是Java提供的一個關鍵字,super用于限定該物件呼叫它從父類繼承得到的實體變數或方法
如果子類定義了和父類相同的實體變數,則會發生子類實體變數隱藏父類實體變數的情形,正常情況下,子類定義的方法直接訪問該實體變數會默認訪問到子類中定義的實體變數,無法訪問到父類中被隱藏的實力變數,在子類定義的實體方法中可以通過super來訪問父類中被隱藏的實體變數
public class BaseClass {
public int a = 5;
}
public class SubClass extends BaseClass {
public int a = 7;
public void accessOwner(){
System.out.println(a);
}
public void accessBase(){
System.out.println(super.a);
}
}
public static void main(String[] args) {
SubClass sc = new SubClass();
sc.accessBase();
sc.accessOwner();
}
5
7
呼叫父類構造器
子類不會獲得父類的構造器,但子類構造器里可以呼叫父類構造器的初始化代碼,在子類構造器中呼叫父類構造器使用super
呼叫完成
不管是否使用super
呼叫來執行父類構造器的初始化代碼,子類構造器總會呼叫父類構造器一次,子類構造器呼叫父類構造器分如下幾種情況
- 子類構造器執行體的第一行使用super顯式呼叫父類構造器,系統將根據super呼叫里傳入的實參串列呼叫父類對應的構造器
- 子類構造器執行體的第一行代碼使用this顯示呼叫本類父類構造器,系統將根據super呼叫里傳入的實傳入的實參串列呼叫本類中的另一個構造器
- 子類構造器執行體中既沒有super呼叫,也沒有this呼叫,系統將會在執行子類構造器之前,隱式呼叫父類無引數的構造器
當呼叫子類構造器來初始化子類物件時,父類構造器總會在子類構造器之前執行;不僅如此,執行父類構造器時,系統會再次向上溯執行其父類構造器...以此類推,創建任何Java物件,最先執行的總是java.lang.Object
類的構造器
public class Creature {
public Creature(){
System.out.println("Creature無引數的構造器");
}
}
public class Animal extends Creature {
public Animal(String name){
System.out.println("Animal帶一個引數的構造器," + "該動物的name為" + name);
}
public Animal(String name, int age){
this(name);
System.out.println("Animal代兩個引數的構造器," + "其age為" + age);
}
}
public class Wolf extends Animal {
public Wolf(){
super("灰太狼",12);
System.out.println("Wolf 無引數的構造器");
}
public static void main(String[] args) {
Wolf wolf = new Wolf();
}
}
Creature無引數的構造器
Animal帶一個引數的構造器,該動物的name為灰太狼
Animal代兩個引數的構造器,其age為12
Wolf 無引數的構造器
多型
Java參考變數有兩個型別:一個是編譯型別,一個是運行時型別,編譯時型別由宣告該變數時使用的型別決定,運行時型別有實際賦給變數的物件決定,如果編譯時型別和運行時型別不一致,就可能出現所謂的多型(Polymorphism),
多型性
當把一個子類物件直接付給父類參考變數后,運行時呼叫該參考變數的方法時,其方法行為總是表現出子類的方法的行為特征,而不是父類方法的行為特征,這就可能出現:相同型別的變數、呼叫同一個方法是呈現出多種不同的行為特征,這就是多型
public class BaseClass {
public void run(){
System.out.println("執行BaseClass的run方法");
}
}
public class SubClass extends BaseClass {
public void run(){
System.out.println("執行SubClass的run方法");
}
}
public static void main(String[] args) {
BaseClass sc = new SubClass();
sc.run();
System.out.println("運行時型別:" + sc.getClass());
}
執行SubClass的run方法
運行時型別:class SubClass
參考變數在編譯階段只能呼叫其編譯型別時型別所具有的方法,但運行時則執行它運行時型別所具有的方法
通過參考變數來訪問其包含的實體變數時,系統總是試圖訪問它編譯時型別所定義的成員變數,而不是它運行時型別所定義的成員變數
參考型別的強制轉換
參考型別之間的轉換只能在具有繼承關系的兩個型別之間進行,如果是兩個沒有任何繼承關系的型別,則無法進行型別轉換,否則編譯時會出現錯誤,如果試圖把一個父類實體轉換成子型別別,則這個物件必須實際上是子類實體才行,否則將在運行時引發ClassCastException
例外,
public static void main(String[] args) {
// SubClass是BaseClass型別的子類 可轉換
SubClass subClass = new SubClass();
BaseClass baseClass1 = (SubClass)subClass;
// 編譯時是BaseClass型別,運行時為SubClass型別,可轉換
BaseClass baseClass2 = new SubClass();
SubClass subClass1 = (SubClass) baseClass2;
// 父類實體強轉子型別別 引發ClassCastException例外
BaseClass baseClass3 = new BaseClass();
SubClass subClass2 = (SubClass) baseClass3;
}
instanceof
運算子
instanceof
運算子的錢一個運算元通常是一個參考型別變數,后一個運算元通常是一個類,它用于判斷前面的物件是否是后面的類,或者其子類、實作類的實體,如果是,則回傳true,否則回傳false
public static void main(String[] args) {
SubClass subClass1 = new SubClass();
if(subClass1 instanceof BaseClass){
BaseClass baseClass = (SubClass)subClass1;
}
BaseClass baseClass1 = new SubClass();
if(baseClass1 instanceof SubClass){
SubClass subClass = (SubClass) baseClass1;
}
// 這里增加通過instanceof判斷是否可以轉換,可以保證程式不會出現錯誤
BaseClass baseClass2 = new BaseClass();
if(baseClass2 instanceof SubClass){
SubClass subClass = (SubClass) baseClass2;
}
// 沒有繼承關系所以引起編譯錯誤,Inconvertible types; cannot cast 'SubClass' to 'Fruit'
if(subClass1 instanceof Fruit){
}
}
isntanceof
運算子前面運算元的編譯時型別要么與后面的類相同,要么與后面的類具有父子繼承關系,否則會引起編譯錯誤
初始化塊
初始化塊是Java類里可出現的第4種成員(前面依次有成員變數、方法和構造器),一個類里可以有多個初始化塊,相同型別的初始化塊之間有順序:前面定義的初始化塊先執行,后面定義的初始化塊后執行,
初始化塊的語法格式如下
[修飾符] {
// 初始化塊的可執行性代碼
...
}
初始化塊的修飾符只能是static
,使用static
修飾的初始化塊被稱為類初始化塊(靜態初始化塊),沒有static
修飾的初始化塊被稱為實體初始化塊(非靜態初始化塊),
實體初始化塊
初始化塊里的代碼可以包含任何可執行的陳述句,包括定義區域變數、呼叫其他物件的方法,以及使用分支、回圈陳述句等
public class Person {
{
int a = 4;
if(a > 3){
System.out.println("Person的實體初始化塊:區域變數a大于3");
}
System.out.println("Person的實體初始化塊");
}
{
System.out.println("Person的第二個實體初始化塊");
}
public Person(){
System.out.println("Person的無引數構造器");
}
public static void main(String[] args) {
Person person = new Person();
}
}
Person的實體初始化塊:區域變數a大于3
Person的實體初始化塊
Person的第二個實體初始化塊
Person的無引數構造器
實體初始化塊只在創建Java物件時隱式執行,而且在構造器執行之前自動執行,類初始化則在類初始化階段自動執行
實際上實體初始化塊是一個假象,使用
javac
命令編譯Java類后,該Java類中的實體初始化塊會消失—實體初始化塊種代碼會被“還原”到每個構造器中,且位于構造器所有代碼的前面
與構造器類似,創建一個Java物件時,不僅會執行該類的實體初始化器和構造器,而且系統會一直上溯到java.lang.Object
類,先執行java.lang.Object
類的實體初始化塊,開始執行java.lang.Object
的構造器,依次向下執行其父類的實體初始化塊,開始執行其弗雷德構造器......最后才執行該類的實體初始化塊和構造器
類初始化塊
如果定義初始化塊時使用了static
修飾符,則這個初始化塊就變成了類初始化塊,也被稱為靜態初始化塊,
類初始化塊時類相關的,系統將在類初始化階段執行類初始化塊,而不是在創建物件是才執行,因此類初始化塊總是比實體初始化塊先執行
同樣的,系統在類初始化階段執行類初始化時,不僅會執行本類的類初始化,而且會一直上溯到java.object.Object
類(如果它包含類初始化塊)
public class Root {
static {
System.out.println("Root的類初始化塊");
}
{
System.out.println("Root的實體初始化塊");
}
}
public class Mid extends Root{
static {
System.out.println("Mid的類初始化塊");
}
{
System.out.println("Mid的實體初始化塊");
}
public Mid(){
System.out.println("Mid的無引數構造器");
}
public Mid(String name){
this();
System.out.println("Mid的帶引數構造器,其引數值:" + name);
}
}
public class Leaf extends Mid {
static {
System.out.println("Leaf的類初始化塊");
}
{
System.out.println("Leaf的實體初始化塊");
}
public Leaf(){
super("Leaf");
System.out.println("Leaf的無引數構造器");
}
}
public class Test {
public static void main(String[] args) {
new Leaf();
}
}
// 類初始化階段,先執行最頂層父類的類初始化塊,然后依次向下,直到執行當前類的類初始化塊
Root的類初始化塊
Mid的類初始化塊
Leaf的類初始化塊
// 物件初始化階段
Root的實體初始化塊
Mid的實體初始化塊
Mid的無引數構造器
Mid的帶引數構造器,其引數值:Leaf
Leaf的實體初始化塊
Leaf的無引數構造器
// 類初始化完畢后會一直在虛擬機里存在,無須再進行類初始化
Root的實體初始化塊
Mid的實體初始化塊
Mid的無引數構造器
Mid的帶引數構造器,其引數值:Leaf
Leaf的實體初始化塊
Leaf的無引數構造器
初始化塊和宣告變數時所指定的初始值都屬于初始化代碼,執行順序會按照代碼的先后順序執行
public class Test {
{
a = 10;
}
int a = 9;
public static void main(String[] args) {
Test test = new Test();
//輸出9
System.out.println(test.a);
}
}
包裝類
為了解決八種基本型別不能當成Object型別變數使用的問題,java提供了包裝類(Wrapper Class)的概念,為八種基本型別定義了相應的參考型別,并稱之為八種基本型別的包裝類
基本資料型別 | 包 裝 類 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
JDK1.5提供了自動裝箱和自動拆箱的功能,自動裝箱:把一個基本型別變數直接賦值給對應的包裝類變數或者賦值給Object變數,自動拆箱與之相反
Integer inObj = 5; //通過 自動裝箱
Object obj = true; // 通過 自動裝箱
int it = inObj // 通過 自動拆箱
if(obj instanceof boolean){
//通過強制轉換成Boolean物件,在自動拆箱為boolean
boolean bl = (Boolean)obj;
}
包裝類還可以實作基本型別變數和字串之間的轉換
- 利用包裝類提供
parseXxx(String s)
靜態方法(除了Character都提供了該方法) - 利用包裝類提供的
valueOf(String s)
靜態方法,
處理物件
toString
方法
toString
方法時Object類里的一個實體方法,因此所有的Java物件都具有toString
方法,Object類提供的toString()
方法總是回傳物件實作類的“類名+@+hashCode
”值,我們可以通過重寫toString
方法來讓物件輸出該物件的內容
public class Apple {
String Color;
double weight;
public Apple(String color, double weight) {
Color = color;
this.weight = weight;
}
@Override
public String toString() {
return "Apple{" +
"Color='" + Color + '\'' +
", weight=" + weight +
'}';
}
}
public static void main(String[] args) {
Apple apple = new Apple("red",5.68);
//println()方法自動輸出物件的toString()方法的回傳值
System.out.println(apple);
}
Apple{Color='red', weight=5.68}
==和equals方法
Java程式中測驗兩個變數是否相等的方式有兩個:一種時利用==
運算子,另一種是利用equals()
方法,
當使用==
來判斷兩個變數是否相等時,如果兩個變數是基本型別變數,且都是數值型別,只要兩個變數的值相等,就將回傳true,
但對于兩個參考型別變數,只有它們指向同一個物件時,==
判斷才會回傳true
當Java程式直接使用形如"hello"
的字串直接量(或者在編譯時就計算出來的字串值)是,JVM將會使用常量池來管理這些字串;當使用new String("hello")
時,JVM會先使用常量池來管理"hello"
直接量,再呼叫String的構造器來創建一個新的String物件,新創建的String物件被保存在堆記憶體中,
JVM常量池保證相同的字串直接量只有一個,不會產生多個副本
常量池專門用于管理在編譯期間被確認并保存在已編譯的.class檔案中的一些資料,它包括了關于類、方法、介面中的常量,還包括字串常量
public static void main(String[] args) {
String s1 = "這是一個測驗";
String s2 = "這是";
String s3 = "一個測驗";
String s4 = s2 + s3;
String s5 = "這" + "是一個測驗";
String s6 = new String("這是一個測驗");
//s4無法在編譯時確定下來值
System.out.println(s1 == s4);
//s5在編譯時就已經確認下來
System.out.println(s1 == s5);
//s4是新創建String物件,存盤在堆記憶體中
System.out.println(s1 == s6);
System.out.println(s1.equals(s4));
System.out.println(s1.equals(s5));
System.out.println(s1.equals(s6));
}
false
true
false
true
true
true
String 重寫Object的equals()方法,判斷兩個字串相等的標準是:只要兩個字串包含的字符序列相同,通過
equals
比較將回傳true,否則回傳false
equals()
是Object類提供的一個實體方法,所有參考變數均可通過此方法來判斷與其他參考變數是否相等,但使用Object提供的equals()
方法判斷兩個物件相等和==
沒有區別,我們可以采用重寫equals
方法來實作自定義equals()
方法
正確重寫equals()
方法應該滿足一下條件
- 自反性:對任意x,
x.equals(x)
一定回傳true - 對稱性:對任何x和y,如果
y.equals(x)
回傳true,則x.equals(y)
也回傳true - 傳遞性:對任意x,y,z,如果
x.equals(y)
回傳true,y.equals(z)
回傳true,則x.equals(z)
一定回傳true - 一致性:對任意x和y,如果物件中用于等價比較的資訊沒有改變,那么無論呼叫
x.equals(y)
多少次,回傳的結果都應該是一直 - 對任何不是null的x,
x.equals(null)
一定回傳false
靜態成員
static關鍵字修飾的成員就是靜態成員,static關鍵字不能修飾構造器,static修飾的成員屬于整個類,不屬于單個實體
在Java類里只能包含成員變數、方法、構造器、初始化塊、內部類(包含介面、列舉)五種成員,其中static可以修飾成員變數、方法、初始化塊、內部類,以static修飾的成員就是靜態成員,靜態成員屬于整個類,而不屬于單個物件,
當通過類物件訪問靜態成員時,系統會在底層轉換為通過該類來訪問靜態變數
public class NullAccessStatic {
public static void test(){
System.out.println("靜態方法執行了");
}
}
public static void main(String[] args) {
NullAccessStatic nullAccessStatic = null;
nullAccessStatic.test();
}
執行結果
靜態方法執行了
final 修飾符
final
關鍵字可用于修飾類、變數和方法,表示它修飾的類、變數和方法不可變,final修飾變數時,表示該變數一旦獲得了初始值就不可被改變
final 成員變數
final
修飾的成員變數必須由程式員顯示地指定初始值
final修飾的成員變數,靜態變數能指定初始值的地方如下
- 靜態變數:必須在靜態初始塊中指定初始值或宣告該變數時指定初始值,而且只能在兩個地方中的其中之一指定
- 實體變數:必須在非靜態初始化塊、宣告該變數時或構造器中指定初始值,只能在三個地方中的其中之一指定
public class Test {
final int a = 0;
final String str;
final int c;
static final int d;
{
// 在初始化塊中指定初始值
str = "";
// 定義a變數時已經指定了默認值,非法
// a = 2;
}
static {
// 在靜態初始化塊中指定初始值
d = 2;
}
public Test(){
// 在構造器中指定初始值
c = 5;
// 初始化塊中已被指定初始值,下面代碼報錯
// str = "123";
}
}
如果打算在構造器、初始化塊中對final成員變數進行初始化,則不要在初始化之前訪問final成員變數;如果在初始化之前訪問會直接報錯
public class Test {
final int age;
{
// 沒有初始化無法直接訪問所以報錯
// System.out.println(age);
// 可以通過方法類訪問
printAge(); // 輸出0
age = 6;
System.out.println(age);
}
public void printAge(){
System.out.println(age);
}
public static void main(String[] args) {
new Test();
}
}
final成員變數在顯示初始化之前不能直接訪問,但可以通過方法來訪問,這是java設計的一個缺陷,按照正常邏輯,final成員變數在顯式初始化之前時不應該允許被訪問的,
final區域變數
系統不會對區域變數進行初始化,區域變數必須由程式員顯式初始化,因此final修飾區域變數時,既可以在定義是指定默認值,也可以不指定默認值,但只能賦值一次,不能重復賦值,
public static void main(String[] args) {
final String a = "hello";
final int b;
// 變數a已被賦值,所以下面陳述句非法
// a = "321";
// 賦初始值
b = 3;
// 重復賦值報錯
// b = 4;
}
public void test(final int a){
// 重復賦值報錯
// a = 12;
}
當使用final修飾基本型別變數時,不能對基本型別變數重新賦值,因此基本型別不能被改變,但對于參考型別來說,它保存的僅僅是一個參考,final只能保證這個參考型別所參考的地址不能被改變,參考的這個物件完全可以發生改變
可執行“宏替換”的final變數
對于一個final變數來說,不管它是靜態變數、實體變數,還是區域變數,只要改變數滿足三個條件,這個final變數就不再是一個變數,而是 相當于一個直接量
- 使用final修飾符修飾
- 在定義該final變數時指定了初始值
- 該初始值可以在編譯時就被確定下來
//普通變數
String s1 = "hello";
String s2 = " world";
//由于s1,s2是兩個變數,無法在編譯時確定s的值
String s = s1 + s2;
//直接量
final String sf1 = "hello";
final String sf2 = " world";
//由于sf1,sf2是兩個直接量,所以可以在編譯時確定sf的值,即指向字串池中快取的字串
String sf = sf1 + sf2;
//由于s沒有指向字串池中的字串,所以結果為false
System.out.println("hello world" == s);
//sf在編譯時就已經確定,sf的值是指向字串池中的字串,所以結果為true
System.out.println("hello world" == sf);
Java會使用常量池來管理曾經用的字串直接量,例如執行
String a = "java";
陳述句后,常量池中就會快取一個字串“java”;如果程式在執行String b= "java";
,系統將會讓b直接指向常量池中的“java”字串,因此a==b將會回傳true
final方法
final修飾的方法不可被重寫
public class Test {
//Object類里有個final方法getClass(); 所以下行代碼會報錯
//'getClass()' cannot override 'getClass()' in 'java.lang.Object'; overridden method is final
public void getClass(){}
}
對于一個private方法,因為它僅在當前類中可見,其子類無法訪問該方法,所以子類無法重寫該方法,如果子類定義了一個與父類private方法有相同方法名、相同引數串列、相同回傳值的方法,也不是方法重寫,只是重新定義了一個方法,因此,即使使用final修飾一個private訪問權限的方法,依舊可以在子類定義于該方法具有相同方法名、相同形參串列、相同回傳值型別的方法,
final類
final修飾的類不可以有子類
public final class FinalClass {}
//下面的將出現編譯錯誤
class sub extends FinalClass {}
抽象類
抽象方法和抽象類
抽象方法和抽象類必須使用abstract修飾符類定義,有抽象方法的類只能被定義成抽象類,抽象類里可以沒有抽象方法
抽象方法和抽象類的規則如下
- 抽象類必須使用abstract修飾符來修飾,抽象方法也必須使用abstract修飾符來修飾,抽象方法不能有方法體
- 抽象類不能被實體化,無法使用new關鍵字來呼叫抽象類的構造器創建抽象類的實體
- 抽象類可以包含成員變數、方法、構造器、初始化塊、內部類
- 含有抽象方法的類只能被定義成抽象類
定義抽象方法需要在普通方法上增加abstract
修飾符,并把普通方法的方法體(也就是方法后花括號括起來的的部分)全部去掉,并在方法后增加分號即可,
定義抽象類只需在普通類上增加abstract
修飾符即可,
public abstract class Shape {
{
System.out.println("執行Shape的初始化塊");
}
private String color;
// 定義一個計算周長的方法
public abstract double calPerimeter();
// 定義一個回傳形狀的放啊發
public abstract String getType();
public Shape(){
}
public Shape(String color){
System.out.println("執行Shape的構造器");
this.color = color;
}
}
public class Triangle extends Shape{
//定義三角形的三邊
private double a;
private double b;
private double c;
public Triangle(String color,double a,double b,double c){
super(color);
this.setSides(a,b,c);
}
public void setSides(double a,double b,double c){
if(a >= b + c || b >= a + c || c >= a + b){
System.out.println("三角形兩邊之和必須大于第三邊");
return;
}
this.a = a;
this.b = b;
this.c = c;
}
// 重寫父類的周長方法
@Override
public double calPerimeter() {
return a + b + c;
}
// 重寫父類回傳形狀的方法
@Override
public String getType() {
return "三角形";
}
}
利用抽象類和抽象方法的優勢,可以更好地發揮多型的優勢,使得程式更加靈活
當使用abstract修飾類時,表明這個類只能被繼承;當使用abstract修飾方法時,表明這個方法必須由子類提供實作(即重寫),而final修飾的類不能被繼承,final修飾的方法不能被重寫,因此final和abstract永遠不能同時使用
介面
介面用于定義某一批類所需要遵循的規范,介面不關心這些類的內部狀態資料,也不關心這些類里方法的實作細節,只規定這批類里必須提供某些方法,
介面的定義
和類定義不同,定義介面不再使用class關鍵字,而是使用interface關鍵字,介面定義的基本語法如下
[修飾符] interface 介面名 extends 父介面1,父介面2...
{
零到多個常量定義...
零到多個抽象方法定義...
零到多個內部類、介面、列舉定義...
零到多個私有方法、默認方法或靜態方法定義...
}
- 修飾符可以是public或者省略,如果省略了public訪問控制符,默認采用protect訪問控制符
- 介面名應與類名采用相同的命名規則
- 一個介面可以有多個直接父介面,但介面只能繼承介面,不能繼承類
在介面中定義成員變數時,不管是否適用public、static、final修飾符,介面里的成員變數總是用著三個修飾符來修飾,而且介面里面沒有構造器和初始化塊,因此介面里定義成員變數只能在定義時指定默認值,
介面里的定義的方法只能是抽象方法、靜態方法、默認方法或私有方法,因此如果不是定義默認方法、類方法或私有方法,系統將自動為普通方法增加abstract修飾符
從Java8開始,在即口里允許定義默認方法,默認方法必須使用default修飾,該方法不能使用static修飾,無論程式是否指定,默認方法總是使用public
修飾
public interface IOutPut {
// 介面里定義的成員變數總是用public static final來修飾,所以以下兩行代碼一樣
//public final static int MAX_CACHE_LINE = 50;
int MAX_CACHE_LINE = 50;
// 在介面中定義默認方法,需要用default修飾
default void print(String msg){
System.out.println(msg);
}
// 在介面中定義靜態方法
static void staticTest(){
System.out.println("介面里的靜態方法");
}
//普通方法自動增加abstract修飾符
public void test();
}
介面的繼承
介面支持多繼承,即一個介面可以有多個直接父介面,和類繼承相似,子介面擴展某個父介面,將會獲得父介面里定義的所有抽象方法、常量
一個介面繼承多個父介面時,多個父介面排在extends
關鍵字之后,多個父介面之間可以以英文逗號(,)隔開
public interface InterfaceA {
int PROP_A = 5;
void testA();
}
public interface InterfaceB {
int PROP_B = 5;
void testB();
}
public interface InterfaceC extends InterfaceA,InterfaceB{
int PROP_C = 7;
void testC();
}
public static void main(String[] args) {
System.out.println(InterfaceC.PROP_A);
System.out.println(InterfaceC.PROP_B);
System.out.println(InterfaceC.PROP_C);
}
介面的實作
一個類可以實作一個或多個介面,通過implements
關鍵字實作介面
類實作介面的語法格式如下
[修飾符] class 類名 extends 父類 implements 介面1,介面2...
{
類體部分
}
實作介面與繼承父類相似,一樣可以獲得所實作介面里定義的常量(成員變數)、方法(包括抽象方法和默認方法),implements
部分必須放在extends
部分之后,
一個類實作了一個或多個介面之后,這個類必須完全實作這些介面里定義的全部抽象方法(也就是重寫這些抽象方法);否則,該類將保留從父介面哪里繼承得到的抽象方法,該類也必須定義成抽象類,
public class InterfaceImpl implements InterfaceC{
@Override
public void testA() {
System.out.println(PROP_A);
}
@Override
public void testB() {
System.out.println(PROP_B);
}
@Override
public void testC() {
System.out.println(PROP_C);
}
}
實作介面方法時,必須使用public訪問控制修飾符,因為介面里的方法都是public的,而子類(相當于實作類)重寫父類方法是訪問權限只能更大或相等,所以實作類實作介面里的方法時只能使用
public
訪問權限
介面和抽象類
介面和抽象類很像,它們都具有以下特征
- 介面和抽象類都不能被實體化,
- 介面和抽象類都可以包含抽象方法, 實作介面或繼承抽象類的普通子類都必須實作這些抽象方法
除此之外,介面和抽象類在用法上也存在如下差別
- 介面里只能包含抽象方法、靜態方法、默認方法和私有方法(java9),不能為普通方法提供方法實作;抽象類則完全可以包含普通方法,
- 介面里只能定義靜態常量,不能定義普通成員變數;抽象類里既可以定義普通成員變數,也可以定義靜態常量
- 介面里不包含構造器;抽象類里可以包含構造器,抽象類里的構造器并不是用于創建物件,而是讓其子類呼叫這些構造器來完成屬于抽象類的初始化操作
- 介面里不能包含初始化塊;但抽像類則可以完全包含初始化塊,
- 一個類最多只能有一個直接父類,包括軸向類;但一個類可以直接實作多個介面,通過實作多個介面可以彌補Java單繼承的不足
內部類
某項情況下,會把一個類放在另一個類的內部定義,這個定義在其它類內部的類就被稱為內部類(嵌套類),包含內部類的類也被稱為外部類(宿主類)
內部類主要有一下作用
- 內部類提供了更好的封裝,可以把內部類隱藏在外部類之內,不允許同一個包的其他類訪問該類
- 內部類成員可以直接訪問外部類的私有資料
- 匿名內部類適合用于創建那些僅需要一次使用的類,
內部類除了需要定義在其他類里面之外,存在以下兩條區別
- 內部類比外部類可以多使用三個修飾符:private、protected、static—外部類不可以使用這三個修飾符
- 非靜態內部類不能擁有靜態成員
非靜態內部類
內部類定義語法格式如下
public class OuterClass
{
//此處定義內部類
}
因為內部類作為其外部類的成員,所以可以使用任意訪問控制符如private
,protected
和public
等修飾
非靜態內部類可以直接訪問外部類的任何成員
public class Cow {
private double weight;
public Cow(){}
public Cow(double weight){
this.weight = weight;
double temp = new CowLeg().length;
}
//定義一個內部類
private class CowLeg{
private double length;
private String color;
public CowLeg(){}
public CowLeg(double length,String color){
this.length = length;
this.color = color;
}
public void info(){
System.out.println("當前牛腿顏色是"+color+",高:"+length);
//直接訪問外部類的private修飾的的成員變數
System.out.println("本牛腿所在奶牛重" + weight);
}
}
public static void main(){
CowLeg cowLeg = new Cow(). new CowLeg();
}
}
如果外部類成員變數、內部類成員變數與內部類里方法的區域變數同名,可以通過使用this、外部類類名.this 作為限定來區分
System.out.println("本牛腿所在奶牛重" + Cow.this.weight);
在非靜態內部物件里,保存了一個它所寄生的外部類物件的參考(當呼叫非靜態內部類的實體方法時,必須有一個非靜態內部類實體,非靜態內部類實體必須集成在外部類實體里)
非靜態內部類不能有靜態方法、靜態成員變數、靜態初始化塊
靜態內部類
如果使用static
來修飾一個內部類,則這個內部類就屬于外部類本身,而不屬于外部類的某個物件,因此使用static修飾的內部類稱為靜態內部類
靜態內部類可以包含靜態成員,也可以包含非靜態成員,根據靜態成員不能訪問非靜態成員的規則,靜態內部類不能訪問外部類的實體成員,只能訪問外部類的靜態成員,即使時靜態內部類的實體方法也不能訪問外部類的實體成員,之恩那個訪問外部類的靜態成員
public class StaticInnerClassTest {
private int prop1 = 5;
private static int prop2 = 9;
static class StaticInnerClass {
// 靜態內部類可以包含靜態成員
private static int age;
public void accessOuterProp(){
//靜態內部類無法訪問外部類的實體變數,報錯
System.out.println(prop1);
System.out.println(prop2);
}
}
}
外部類依然不能直接訪問靜態內部類的成員,但可以使用靜態內部類的類名作為呼叫者來訪問靜態內部類中的靜態成員,也可以使用靜態內部類物件作為呼叫者來訪問講臺內部類的實體成員
public class AccessStaticInnerClass {
static class StaticInnerClass {
private static int prop1 = 5;
private int prop2 = 9;
}
public void accessInnerProp() {
//通過類名訪問靜態內部類的靜態成員
System.out.println(StaticInnerClass.prop1);
//通過實體訪問靜態內部類的實體成員
System.out.println(new StaticInnerClass().prop2);
}
}
除此之外,Java允許在介面定義內部類,介面里定義的內部類默認用public static修飾,也就是說,介面內部類只能是靜態內部類
如果為介面內部類指定訪問控制符,則只能是public訪問控制符;如果介面定義介面內部類是省略訪問控制符,則該內部類默認是public訪問控制權限
使用內部類
在外部類內部使用內部類
在外部類內部使用內部類時,與平常使用普通類不沒太大的區別,一樣可以直接通過內部類類名定義變數,通過new呼叫內部類構造器來創建實體
唯一的區別是:不要在外部類的靜態成員(包括靜態方法和靜態初始化塊)中使用非靜態內部類,因為靜態成員不能訪問非靜態成員
在外部類以外使用非靜態類
如果希望在外部類以外的地方訪問內部類(包括靜態和非靜態),則內部類不能使用private
訪問控制權限,private
修飾的內部類只能在外部類內部使用
對于使用其他訪問控制符修飾的內部類,則能在訪問控制符對應的訪問權限內使用
- 省略訪問控制符的內部類,只能被與外部類處于同一個包中的其他類所訪問
- 使用protected修飾的內部類,可被與外部類處于同一個包中的其他類和外部類的子類所訪問
- 使用public修飾的內部類,可以在任何地方被訪問.
在外部類以外的地方定義內部類(包括靜態和非靜態兩種)變數的語法格式如下
OuterClass.InnerClass varName
在外部類以外的地方創建非靜態內部類實體的語法如下
outerInstance.new InnerConstructor()
public class Out {
class In{
public In(String msg){
System.out.println(msg);
}
}
}
public static void main(String[] args) {
Out.In in = new Out().new In("helloword");
}
在創建非靜態內部類的子類時,必須保證讓子類構造器可以呼叫非靜態內部類的構造器,呼叫非靜態內部類的構造器時,必須存在一個外部類物件
public class SubClass extends Out.In {
//顯示定義SubClass的構造器
public SubClass(Out out) {
// 通過傳入的Out物件顯示呼叫In的構造器
out.super("hello");
}
}
在外部類意外使用靜態內部類
在外部類以外的地方創建靜態內部類實體的語法如下
new OuterClass.InnerConstructor()
事例
public class StaticOut {
static class StaticIn{
public StaticIn(){
System.out.println("靜態內部類的構造器");
}
}
}
public static void main(String[] args){
StaticOut.StaticIn in = new StaticOut.StaticIn();
}
區域內部類
如果把一個內部類放在方法里定義,則這個內部類就是一個區域內部類,區域內部類僅在該方法里有效,由于區域內部類不能再外部類的方法以外的地方使用,因此區域內部類也不能使用訪問控制符和static修飾符修飾
如果需要用區域內部類定義變數、創建實體或派生子類,那么都只能在區域內部類所在的方法內進行
public class LocalInnerClass {
public static void main(String[] args){
// 定義區域內部類
class InnerClass{
int a;
}
// 定義區域內部類的子類
class InnerSub extends InnerClass{
int b;
}
// 創建區域內部類物件
InnerSub is = new InnerSub();
is.a = 5;
is.b = 9;
}
}
匿名內部類
匿名內部類適合創建那種只需要一次使用的類,創建匿名內部類是會立即創建一個該類的實力,這個類定義立即小時,匿名內部類不能重復使用
定義匿名內部類的格式如下
new 實作介面() | 父類構造器(實在串列)
{
//匿名內部類的類體部分
}
匿名內部類必須繼承一個父類,或實作一個介面,但最多只能繼承一個類或實作一個介面
關于匿名內部類還有如下兩條規則
- 匿名內部類不能是抽象類,因為系統在創建匿名內部類時,會立即創建匿名內部類的物件,因此不允許將匿名內部類定義成抽象類
- 匿名內部類不能定義構造器,由于匿名內部類沒有類名,所以無法定義構造器,但匿名內部類可以定義初始化塊,可以通過實體初始化塊來完成構造器需要完成的事情
public interface Product {
double getPrice();
String getName();
}
public class AnoymousTest {
public void test(Product p){
System.out.println("購買了一個" + p.getName() + ",花掉了" + p.getPrice());
}
public static void main(String[] args){
AnoymousTest an = new AnoymousTest();
// 傳入其匿名實作類的實體
an.test(new Product() {
@Override
public double getPrice() {
return 13000;
}
@Override
public String getName() {
return "4090顯卡";
}
});
}
}
通過繼承父類來創建匿名內部類時,匿名內部類將擁有和父類相似的構造器,此處的相似指的是擁有相同的形參串列
public abstract class Device {
private String name;
public abstract double getPrice();
public Device(){};
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Device(String name){
this.name = name;
}
}
package 匿名內部類;
public class AnoymousInner {
public void test(Device d) {
System.out.println("購買了一個" + d.getName() + ",花掉了" + d.getPrice());
}
public static void main(String[] args) {
AnoymousInner ai = new AnoymousInner();
//呼叫有引數的構造器創建Device匿名實作類的物件
ai.test(new Device("4090") {
@Override
public double getPrice() {
return 13000;
}
});
//呼叫無引數的構造器創建Device匿名實作類的物件
ai.test(new Device() {
//初始化塊
{
System.out.println("初始化匿名內部類的初始化塊");
}
// 實作抽象方法
@Override
public double getPrice() {
return 56.2;
}
// 重寫父類的實體方法
@Override
public String getName() {
return "鍵盤";
}
});
}
}
購買了一個4090,花掉了13000.0
初始化匿名內部類的初始化塊
購買了一個鍵盤,花掉了56.2
在Java 8 之前,Java要求被區域內部類、匿名內部類訪問的區域變數必須使用final修飾,從Java 8 開始這個限制被取消了,Java 8 更加智能:如果區域變數被內部類訪問,那么該區域變數相當于自動使用final修飾
public static void main(String[] args) {
AnoymousInner ai = new AnoymousInner();
int a = 12;
ai.test(new Device("4090") {
@Override
public double getPrice() {
// 報錯 Variable 'a' is accessed from within inner class, needs to be final or effectively final
a = 2;
return 13000;
}
});
}
Lambda 運算式
Lambda基礎
Lambda 運算式的型別,也被稱為“目標型別(target type)”,Lambda運算式的目標型別必須是“函式式介面(functional interface)”,函式式介面代表只包含一個抽象方法的介面,函式式介面可以包含多個默認方法,靜態方法,但只能宣告一個抽象方法,
如果采用匿名內部類語法來創建函式式介面的實體,則只需要要實作一個抽象方法,在這種情況下可采用Lambda運算式來創建物件,Java 8 含有大量函式式介面,例如 Runnable、ActionListener等介面都是函式時介面
Java 8 專門為函式式介面提供了
@FunctionalInterface
注解,該注解通常被放在解耦定義的前面,該注解對程式程序沒有任何作用,它用于告訴編譯器執行更嚴格檢查(檢查該介面必須是函式式介面,否則編譯器會報錯)
public static void main(String[] args) {
//Runnable 是Java本身提供的一個函式式介面
Runnable runnable = () -> {
for (int i = 0; i < 20; i++) {
System.out.println(i);
}
};
// 如果Lambda運算式只有一條代碼,程式可以省略Lambda運算式中的花括號
Runnable runnable2 = () -> System.out.println("123");
}
Lambda運算式有如下兩個限制
- Lambda 運算式的目標型別必須是明確的函式時介面
- Lambda 運算式只能為函式式介面創建物件,Lambda 運算式只能實作一個方法,因此它只能為只有一個抽象想方法的介面(函式式介面)創建物件,
方法參考和構造器參考
方法參考和構造器參考都可以讓Lambda運算式的代碼塊更加簡潔,方法參考和構造器參考都需要使用兩個英文冒號
種 類 | 示 例 | 說 明 | 對應的的Lambda運算式 |
---|---|---|---|
參考靜態方法 | 類名::靜態方法 | 函式式介面中被實作方法的全部引數傳給該類方法作為引數 | (a,b,...)->類名.靜態方法(a,b,...) |
參考特定物件的實體方法 | 特定物件::實體方法 | 函式式介面中被實作方法的全部引數傳給該方法作為引數 | (a,b,...)->實體.實體方法(a,b,...) |
參考某類物件的實體方法 | 類名::實體方法 | 函式式介面中被實作方法的第一個引數作為呼叫者,后面的引數全部傳給該方法作為引數 | (a,b,...)->a.實體方法(b,...) |
參考構造器 | 類名::new | 函式式介面中被實作方法的全部引數傳給該構造器作為參考引數 | (a,b,...)->new 類名(a,b,...) |
參考靜態方法
@FunctionalInterface
public interface Converter {
Integer convert(String from);
}
Converter converter = from -> Integer.valueOf(from);
上面Lambda運算式的代碼塊只有一條陳述句,因此程式省略了改代碼塊的花括號;而且由于運算式所實作的convert()
方法需要回傳值,因此Lambda運算式將會把這條代碼的值作為回傳值
下面使用靜態方法參考進行替換
// 函式式介面中被實作方法的全部引數傳給該類方法作為引數
Converter converter = Integer::valueOf;
當上述代碼呼叫Converter介面中唯一的抽象方法時,呼叫引數將會傳給Integer類的valueOf()
靜態方法
參考特定物件的實體方法
Converter converter = from -> "hello".indexOf(from);
使用實體方法轉換
Converter converter = "hello"::indexOf;
當呼叫Converter介面中唯一的抽象方法時,呼叫引數將會傳給"hello"
物件的IndexOf()
實體方法
參考某類物件的實體方法
@FunctionalInterface
public interface MyTest {
String test(String a, int b, int c);
}
使用Lambda運算式創建一個物件
MyTest mt = (a, b, c) -> a.substring(b, c);
String str = mt.test("Java I Love", 2, 9);
System.out.println(str); //輸出 va I Lo
替換方式
MyTest mt = String::substring;
當呼叫Converter介面中唯一的抽象方法時,第一個呼叫引數將作為substring()
方法的呼叫者,剩下的呼叫引數會作為substring()
實體方法的呼叫引數
參考構造器
函式式介面中抽象方法的回傳值的類中的建構式的形參和函式式介面中抽象方法的形參一致時,可以同過參考構造器的方式來創建實體物件
public class Person {
private String name;
public Person(String name){
this.name = name;
}
public String getName() {
return name;
}
}
@FunctionalInterface
public interface ConstructionTest {
public Person getPerson(String name);
}
Lambda運算式
public static void main(String[] args) {
ConstructionTest ct = name -> new Person(name);
Person person = ct.getPerson("張三");
}
參考構造器
public static void main(String[] args) {
ConstructionTest ct = Person::new;
Person person = ct.getPerson("張三");
}
Lambda 運算式與匿名內部類的聯系和區別
Lambda 運算式是匿名內部類的一種簡化,因此它可以部分取代匿名內部類的作用,Lambda 運算式與匿名內部類存在如下相同點
- Lambda 運算式與匿名內部類一樣,都可以直接訪問 “effectively final” 的區域變數,以及外部類的成員變數(包括實體變數和靜態變數)
- Lambda 運算式創建的物件與匿名內部類生成的物件一樣,都可以直接呼叫從介面中繼承的默認方法
@FunctionalInterface
public interface Displayable {
void display();
default int add(int a,int b){
return a + b;
}
}
public class LambdaAndInner {
private int age = 12;
private static String name = "張三";
public void test() {
String book = "hello world!";
Displayable dis = () -> {
// 訪問外部類的實體變數
System.out.println(age);
// 訪問外部類的靜態變數
System.out.println(name);
// 呼叫區域變數
System.out.println(book);
};
dis.display();
// 呼叫默認方法
dis.add(1, 2);
}
}
Lambda 運算式與匿名內部類主要存在以下區別
- 匿名內部類可以為任意介面創建實體——不管介面包含多少個抽象想方法,只要匿名內部類實作所有的抽象方法即可;但Lambda 運算式只能為函式式介面創建實體
- 匿名內部類可以為抽象類甚至普通類創建實體;但Lambda 運算式只能為函式式介面創建實體,
- 匿名內部類實作的抽象方法的方法體允許呼叫介面中定義的默認方法; 但 Lambda 運算式的代碼塊不允許呼叫介面中定義的默認方法,
列舉類
列舉類入門
Java 5 新增了一個enum
關鍵字(它與class、interface關鍵字的地位相同),用于定義列舉類,列舉類一種特殊的類,它一樣可以有自己的成員變數、方法,可以實作一個或者多個介面,也可以定義自己的構造器,
一個Java源檔案中最多只能定義一個public訪問權限的列舉類,且該Java源檔案也必須和該列舉類的類名相同
列舉類和普通類的簡單區別
- 列舉類可以實作一個或多個介面,使用
enum
定義的列舉類默認繼承了java.lang.Enum
類,而不是默認繼承Object
類,因此列舉類不能顯示繼承其他父類,其中java.lang.Enum
類實作了java.lang.Serializable
和java.lang.Comparable
兩個介面 - 使用
enum
定義、非抽象的列舉類默認會使用final修飾, - 列舉類的構造器只能使用
private
訪問控制符,如果省略了構造器的訪問控制符,則默認使用private
修飾,由于列舉類的所有構造器都是private
的,因此列舉類也不能派生子類 - 列舉類的所有實體必須在列舉類的第一行顯示列出,否則這個列舉類永遠都不能產生實體,列出這些事例時,系統會自動添加
public static final
修飾,無須程式員顯式添加
列舉類默認提供了一個values()
方法,該方法可以很方便地便利所有的列舉值
定義列舉類時,需要顯示列出所有的列舉值,所有的列舉值之間以英文逗號(,)隔開,列舉值列舉結束后以英文分號作為結束,這些列舉值代表了該列舉類的所有可能的實體
public enum SeasonEnum {
SPRING, SUMMER, FALL, WINTER;
}
public class EnumTest {
public static void judge(SeasonEnum s){
switch (s){
case SPRING:
System.out.println("春暖花開");
break;
case SUMMER:
System.out.println("夏日炎炎");
break;
case FALL:
System.out.println("秋高氣爽");
break;
case WINTER:
System.out.println("冬日雪飄");
break;
}
}
public static void main(String[] args) {
// 列舉類默認有一個values()方法,回傳該列舉類的所有實體
for (SeasonEnum s : SeasonEnum.values()){
judge(s);
}
// 使用列舉實體時,可通過EnumClass.variable 形式來訪問
judge(SeasonEnum.SUMMER);
}
}
switch的控制運算式可以是任何列舉型別,當switch控制運算式使用列舉型別時,后面case運算式中的值直接使用列舉值的名字,無需添加列舉類作為限定
所有列舉類都繼承了java.lang.Enum
類,所以列舉類可以直接使用java.lang.Enum
類中包含的方法,java.lang.Enum
類中提供了如下幾個方法
int compareTo(E o)
該方法用于指定列舉物件比較順序,同一個列舉實體只能與相同型別列舉實體進行比較,如果該列舉物件位于指定列舉物件之后,則回傳增證書;如果該列舉物件位于指定列舉物件之前,則回傳負整數,否則回傳零String name()
回傳此列舉實體的名稱,這個名稱就是定義列舉類時列出的所有列舉值之一int ordinal()
回傳列舉值在列舉類的索引值(就是列舉值在列舉宣告中的位置,第一個列舉值的索引為0)String toString()
回傳列舉常量的名稱,與name
方法相似,但toString()
方法更常用public static<Textends Enum<T>>T valueOf(Class<T> enumType,String name)
這是一個靜態方法,用于回傳指定列舉類中指定名稱的列舉值,名稱必須與在該列舉類中宣告列舉值時所用的識別符號完全匹配,不允許使用額外的空白字符
列舉類的成員變數、方法和構造器
列舉類同樣可以定義成員變數、方法和構造器,使用成員變數和方法與普通類沒什么區別,差別只是產生物件實體的方式不同,列舉類的實體只能是列舉值,而不是隨意通過new來創建列舉類物件
一旦為列舉類顯示定義了帶引數的構造器,列出列舉值時就必須對應地傳入引數
public enum Gender {
// 此處列舉值必須呼叫對應的構造器來創建
MALE("男"),FEMALE("女");
private Gender(String name){
this.name = name;
}
private String name;
public String memo;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public static void main(String[] args) {
Gender.FEMALE.setName("男");
Gender.valueOf("FEMALE").setName("女");
Gender.FEMALE.memo = "備注";
System.out.println("FEMALE中文釋義:"+Gender.FEMALE.getName());
}
實作介面的列舉類
列舉類可以實作一個或多個介面,與普通類實作一個或多個介面完全一樣,列舉類實作一個或多個介面時,也需要實作該介面所包含的抽象方法
public interface GenderDesc {
void info();
}
public enum Gender implements GenderDesc{
...
@Override
public void info() {
}
}
如果需要每個列舉值在呼叫該方法時呈現不同的行為方式,則可以讓每個列舉值分別來實作該方法,每個列舉值提供不同的實作方式,從而讓不同的列舉值呼叫該方法時具有不同的行為方式
public enum Gender implements GenderDesc {
MALE("男") {
@Override
public void info() {
System.out.println("這個列舉值代表男性");
}
}, FEMALE("女") {
@Override
public void info() {
System.out.println("這個列舉值代表女性");
}
};
private Gender(String name) {
this.name = name;
}
}
上面代碼創建MALE和FEMALE列舉值時,并不是直接創建Gender列舉類的實體,而是相當于創建Gender的匿名子類的實體
包含抽象方法的列舉類
列舉類里定義抽象方法時不能使用abstract
關鍵字將列舉定義成抽象類(因為系統會自動為它添加abstract關鍵字),但因為列舉類需要顯示創建列舉值,而不是作為父類,所以定義每個列舉值時必須為抽象方法提供實作,否則將出現編譯錯誤
public enum Operation {
PLUS {
@Override
public double eval(double x, double y) {
return x + y;
}
},
MINUS {
@Override
public double eval(double x, double y) {
return x - y;
}
},
TIMES {
@Override
public double eval(double x, double y) {
return x * y;
}
},
DIVIDE {
@Override
public double eval(double x, double y) {
return x / y;
}
};
// 為列舉值定義一個抽象方法
// 這個抽象方法有不同的列舉值提供不同的實作
public abstract double eval(double x, double y);
public static void main(String[] args) {
System.out.println(Operation.PLUS.eval(2, 5));
System.out.println(Operation.MINUS.eval(10, 2));
System.out.println(Operation.TIMES.eval(2, 5));
System.out.println(Operation.DIVIDE.eval(4, 2));
}
}
7.0
8.0
10.0
2.0
物件與垃圾回收
當程式創建物件、陣列等參考型別物體時,系統都會在堆記憶體中為止分配一塊記憶體區,物件就保存在這塊記憶體中,當這塊記憶體不再被任何參考變數參考時,這塊記憶體就變成垃圾,等待垃圾回識訓制進行回收,
垃圾回識訓制具有如下特征
- 垃圾回識訓制只負責回收堆記憶體中的物件,不會回收任何物理資源(例如資料庫連接,網路IO等資源)
- 程式無法精確控制垃圾回收的運行,垃圾回識訓在合適的時候進行,當物件永久性失去參考后,系統就會在合適的時候回收它所占的記憶體,
- 在垃圾回識訓制回收任何物件之前,總會呼叫它的
finalize()
方法,該方法可能使該物件重新復核(讓一個參考變數重新參考該物件),從而導致垃圾回識訓制取消回收
物件在記憶體中的狀態
當一個物件在堆記憶體中運行時,根據它被參考變數所參考的狀態,可以把他所處的狀態分成一下三種,
- 可達狀態:當一個物件被創建后,若有一個以上的參考變數參考它,則這個物件在程式中處于可達狀態,程式可通過參考變數來呼叫該物件的實體變數和方法
- 可恢復狀態:物件不再有任何參考變數參考它,它就進入了可恢復狀態,在這種狀態下,系統在垃圾回收之前會呼叫此物件的
finalize()
方法進行資源清理,如果呼叫此方法時重新讓一個變數參考該物件,那么該物件再次變為可達狀態;否則進入不可達狀態 - 不可達狀態:當物件永久性的失去參考后,即為不可達狀態,系統才會真正回收該物件所占有的資源
public class StatusTranfer {
public static void Test(){
String a = new String("hello"); // "hello"被創建,并含有一個參考,進入可達狀態
a = new String("bye bye"); //"bye bye"被創建,并含有一個參考,進入可達狀態,"hello"失去參考,進入可恢復狀態,執行完finalize進入不可達狀態
}
public static void main(String[] args) {
Test();
//Test()執行結束 "bye bye"失去參考,進入可恢復狀態,執行完finalize進入不可達狀態
}
}
強制垃圾回收
程式無法精確控制Java 垃圾回收的時機,但依然可以強制系統進行垃圾回收——這種強制只是通知系統進行垃圾回收,但系統是否進行垃圾回收依然不穩定,
強制系統垃圾回收有如下兩種方式
- 呼叫
System
類的gc()
靜態方法:System.gc()
- 呼叫
Runtime
物件的gc()
實體方法:Runtime.getRuntime().gc()
public class GcTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new GcTest();
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("系統正在清理GcTest物件的資源");
}
}
運行以上程式沒有任何輸出
在程式上增加強制垃圾回收
public class GcTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new GcTest();
// 強制系統進行垃圾回收
Runtime.getRuntime().gc();
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("系統正在清理GcTest物件的資源");
}
}
輸出
系統正在清理GcTest物件的資源
系統正在清理GcTest物件的資源
系統正在清理GcTest物件的資源
系統正在清理GcTest物件的資源
系統正在清理GcTest物件的資源
系統正在清理GcTest物件的資源
finalize 方法
在垃圾回識訓制回收某個物件所占用的記憶體之前,通常要求程式呼叫適當的方法來清理資源,在沒有明確指定清理資源的情況下,Java提供了默認機制來清理該物件的資源,這個機制就是finalize()方法, 該方法時定義在Object類里的實體方法,方法原型為:
protected void finalize() throws Throwable
finalize()
方法具有如下4個特點
- 永遠不要主動呼叫某個物件的
finalize()
方法,該方法應交給垃圾回識訓制呼叫 finalize()
方法何時被呼叫,是否被呼叫具有不確定性,不要把finalize()
方法當成一定會執行的方法- 當JVM執行可恢復物件的
finalize()
方法時,可能使該物件或系統中其他物件重新變成可達狀態 - 當JVM執行
finalize()
方法出現例外時,垃圾回識訓制不會報告例外,程式繼續執行
物件的軟、弱和虛參考
對大部分物件而言,程式里會有一個參考變數參考該物件,這是最常見的參考方式,除此之外,java.lang.ref
包下提供了三個類:SoftReference PhantomReference WeakReference
,它們分別代表了系統對物件的三種參考方式:軟參考、虛參考和弱參考
Java語言對物件的參考有如下四種方式
- 強參考(StongReference)
這是Java程式中最常見的參考方式,程式創建一個物件,并把這個物件賦給一個參考變數,程式通過該參考變數來操作實際的物件,當一個物件被一個或多個參考變數所參考時,它處于可達狀態,不可能被系統垃圾回識訓制回收,
- 軟參考(SoftReference)
軟參考需要通過 SoftReference
類來實作,當一個物件只有軟參考時,它有可能被垃圾回識訓制回收,對于只有軟參考的物件而言,當系統記憶體空間足夠時,它不會被系統回收,程式也可使用該物件;當系統記憶體空間不足時,系統可能會回收它,軟參考通常用于對記憶體敏感的程式中
- 弱參考(WeakReference)
弱參考通過 WeakReference
類實作,弱參考和軟參考很想,但弱參考的級別更低,對于只有弱參考的物件而言,當系統垃圾會機制運行時,不管系統記憶體是否足夠,總會回收該物件所占用的記憶體,
- 虛參考(PhantomReference)
虛參考通過PhantomReference
類實作,虛參考完全類似于沒有參考,虛參考對物件本身沒有太大影響,物件甚至感覺不到虛參考的存在,如果一個物件只有一個虛參考時,那么它和沒有參考的效果大致相同,虛參考主要用于跟蹤物件被垃圾回收時的狀態,虛參考不能單獨使用,虛參考必須和參考佇列(ReferenceQueue)聯合使用,
上面三個參考都包含了一個get()
方法,用于獲取被它們所參考的物件
參考佇列由java.lang.ref.ReferenceQueue
類表示,它用于保存被回收后物件的參考,當聯合使用軟參考、弱參考和參考物件列時,系統再回首被參考的物件之后,將把被回收物件對應的參考添加到關聯的參考佇列中,與軟參考和弱參考不同的是,虛參考在物件被釋放之前,將把它對應的虛參考添加到它關聯的參考佇列中,這使得可以在物件被回收之前采取行動
弱參考示例
public class ReferenceTest {
public static void main(String[] args) {
// 創建一個字串物件
String str = new String("Java");
// 創建一個弱參考,將此弱參考參考到 “Java”
WeakReference wr = new WeakReference(str);
// 切斷str與 “Java”的參考
str = null;
// 取出弱參考所參考的物件
System.out.println(wr.get());
// 強制垃圾回收
System.gc();
System.runFinalization();
System.out.println(wr.get());
}
}
輸出
Java
null
虛參考示例
public class PhantomReferenceTest {
public static void main(String[] args) {
String str= new String("Java");
// 創建一個參考佇列
ReferenceQueue rq = new ReferenceQueue();
// 創建一個虛參考,讓此虛參考參考到“Java”
PhantomReference pr = new PhantomReference(str,rq);
// 切斷str變數的參考
str = null;
// 取出虛參考所參考的物件,并不能通過虛參考獲取被參考的物件,所以輸出null
System.out.println(pr.get());
// 強制回收
System.gc();
System.runFinalization();
// 垃圾回收之后,虛參考被放入參考佇列之中
// 取出參考佇列中最先進入佇列的參考與pr進行比較
System.out.println(rq.poll() == pr); // true
}
}
修飾符的適用范圍
外部類介面 | 成員屬性 | 方法 | 構造器 | 初始化塊 | 成員內部類 | 區域成員 | |
---|---|---|---|---|---|---|---|
public | √ | √ | √ | √ | √ | ||
protected | √ | √ | √ | √ | |||
包訪問控制符 | √ | √ | √ | √ | ? | √ | ? |
private | √ | √ | √ | √ | |||
abstract | √ | √ | √ | ||||
final | √ | √ | √ | √ | √ | ||
static | √ | √ | √ | √ | |||
strictfp |
√ | √ | √ | ||||
synchroized | √ | ||||||
native | √ | ||||||
transient | √ | ||||||
volatile | √ | ||||||
default | √ |
strictfp
精確浮點,使用此關鍵字修飾類、介面或者方法時,所在范圍會按照浮點規范IEEE-745來執行native
類似抽象方法,與抽象方法不同的是,native方法通常采用C語言來實作,如果某個方法需要利用平臺相關特性,或者訪問系統硬體等,則可以使用native修飾該方法,再把該方法交給C去實作,一旦Java程式中包含了native方法,這個程式將失去跨平臺的功能
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/556282.html
標籤:Java
上一篇:【后端面經-Java】AQS詳解
下一篇:返回列表