我們都知道Java是一門面向對象的語言。什麼是面向對象,它有什麼特性呢,今天我們就來說一下這個"面向對象"到底是什麼意思。
面向對象簡稱 OO(Object Oriented),20 世紀 80 年代以後,其實就有了面向對象分析(OOA)、 面向對象設計(OOD)、面向對象程序設計(OOP)等新的系統開發方式模型的研究。對面對對象設計的語言來說,一切皆是對象。把現實世界中的對象抽象地體現在編程世界中,比如一個蘋果我們可以new Apple(),一個小狗我們可以new Dog(),一個對象代表了某個具體的操作。而一個個對象最終組成了完整的程序設計,這些對象可以是獨立存在的,也可以是從別的對象繼承過來的。對象之間通過相互作用傳遞信息,從而使用簡單的對象和對象的關係,實現龐大而又複雜的程序開發。
什麼是類
類是對現實生活中一類具有共同特徵的事物的抽象。比如我們都是人類,小狗是動物類,小花是植物類。如果一個程序裏提供的數據類型與應用中的概念有直接的對應,這個程序就會更容易理解,也更容易修改。一組經過很好選擇的用戶定義的類會使程序更簡潔。
在Java類的內部封裝了了屬性和方法,用於操作自身的成員。類是對某種對象的抽象定義,具有行爲,它描述一個對象能夠做什麼以及做的方法(method),它們是可以對這個對象進行操作的程序和過程。它包含有關對象行爲方式的信息,包括它的名稱、屬性、方法和事件。
/**
* @desc 定義一個人類
* @author dataozi
* @date 2020/6/7 15:24
*/
@Getter
@Setter
@NoArgsConstructor
public class Person {
// 人的名字
private String name;
// 人的年齡
private Integer age;
/**
* @desc 人會喫飯
* @author dataozi
* @date 2020/6/7 15:26
*/
public void eat(){
}
/**
* @desc 人會跑
* @author dataozi
* @date 2020/6/7 15:26
*/
public void run(){
}
}
什麼是對象
Java 是面向對象的編程語言,對象就是面向對象程序設計的核心。那麼什麼是對象呢?所謂對象就是真實世界中的實體,我們通過一種語言設計,將真實世界的事物通過程序中的對象表現出來,也就是對象與實體是一一對應的,現實世界中每一個實體都是一個對象,它是一種具體的概念。通常,對象有以下特點:
- 對象具有屬性和行爲。
- 對象具有變化的狀態。
- 對象具有唯一性。
- 對象都是某個類別的實例。
- 一切皆爲對象,真實世界中的所有事物都可以視爲對象。
拿自己來說,把我對應上一個對象的話,那麼像我的身高、體重、年齡等都是我的屬性,而我會喫飯、說話、走路等這些就是我的行爲;我會長大,我也會擁有很多角色,我的狀態會發生變化;全世界只有一個我(頓時自豪感爆棚,有沒有);我是男生類中的一員,我是程序猿類中的一員。
@Test
public void testForOO(){
// 我是人類中的一個真實的對象
Person dataozi = new Person();
// 我的名字叫大套子
dataozi.setName("大套子");
// 我年年都18
dataozi.setAge(18);
// 我是人,所以我會喫
dataozi.eat();
}
面向對象和麪向過程的區別
上邊我們瞭解到面向對象編程語言的特點,及面向的是一個個的對象,對象是整個程序設計的核心。那麼面向過程又是什麼呢?面向過程的語言也稱爲結構化程序設計語言,是高級語言的一種。在面向過程程序設計中,問題被看作一系列需要完成的任務,函數則用於完成這些任務,解決問題的焦點集中於函數。它的主要觀點是採用自頂向下、逐步求精的程序設計方法,使用三種基本控制結構構造程序,即任何程序都可由順序、選擇、循環三種基本控制結構構造。面向過程語言程序設計過程就是用一系列語句描述問題解決過程中的一系列步驟的過程【摘自百度百科】。簡單來說就是該思想是站着過程的角度思考問題,強調的就是功能行爲,功能的執行過程,即先後順序,而每一個功能我們都使用函數(類似於方法)把這些步驟一步一步實現。使用的時候依次調用函數就可以了。
面向過程:
- 性能比面向對象高,因爲類調用時需要實例化,開銷比較大,比較消耗資源;比如單片機、嵌入式開發、Linux/Unix等一般採用面向過程開發,性能是最重要的因素。
- 最小的程序單元是函數,每個函數負責完成某一個功能,用於接受輸入數據,函數對輸入數據進行處理,然後輸出結果數據,整個軟件系統由一個個的函數組成,其中作爲程序入口的函數稱之爲主函數,主函數依次調用其他函數,普通函數之間可以相互調用,從而實現整個系統功能。
-
自頂而下的設計模式,在設計階段就需要考慮每一個模塊應該分解成哪些子模塊,每一個子模塊又細分爲更小的子模塊,如此類推,直到將模塊細化爲一個個函數。
-
設計不夠直觀,與人類的思維習慣不一致 系統軟件適應新差,可拓展性差,維護性低、
面向對象:
-
面向對象最小的程序單元是:類。面向對象更加符合常規的思維方式,穩定性好,可重用性強,易於開發大型軟件產品,有良好的可維護性。
-
易維護,採用面向對象思想設計的結構,可讀性高,由於繼承的存在,即使改變需求,那麼維護也只是在局部模塊,所以維護起來是非常方便和較低成本的。
-
效率高,在軟件開發時,根據設計的需要對現實世界的事物進行抽象,產生類。使用這樣的方法解決問題,接近於日常生活和自然的思考方式,勢必提高軟件開發的效率和質量。
-
易擴展,由於繼承、封裝、多態的特性,自然設計出高內聚、低耦合的系統結構,使得系統更靈活、更容易擴展,而且成本較低。
面向對象的三大特性
面向對象開發模式更有利於人們開拓思維,在具體的開發過程中便於程序的劃分,方便程序員分工合作,提高開發效率。
該開發模式之所以使程序設計更加完善和強大,主要是因爲面向對象具有繼承、封裝和多態 3 個核心特性。
封裝
1、概念
封裝有點武裝的意思,將我們本身很重要的信息封裝起來,不對外界進行暴露,從而是信息更加安全,就像取款機一樣,銀行把錢放到取款機裏,取款機對我們來說就是一個封閉的私有的對象,它向我們提供了一個公共的出口,我們只能看到這個公共的功能,而它裏邊到底裝了多少錢,我們是不知道的,在面向對象程式設計方法中,封裝就是將抽象性函式接口的實現細節部份包裝、隱藏起來的方法。
- 封裝可以被認爲是一個保護屏障,防止該類的代碼和數據被外部類定義的代碼隨機訪問。
- 要訪問該類的代碼和數據,必須通過嚴格的接口控制。
- 封裝最主要的功能在於我們能修改自己的實現代碼,而不用修改那些調用我們代碼的程序片段。
- 適當的封裝可以讓程式碼更容易理解與維護,也加強了程式碼的安全性。
- 保護類中的信息,它可以阻止在外部定義的代碼隨意訪問內部代碼和數據。
- 隱藏細節信息,一些不需要程序員修改和使用的信息,比如取款機中的鍵盤,用戶只需要知道按哪個鍵實現什麼操作就可以,至於它內部是如何運行的,用戶不需要知道。
- 有助於建立各個系統之間的松耦合關係,提高系統的獨立性。當一個系統的實現方式發生變化時,只要它的接口不變,就不會影響其他系統的使用。例如 U 盤,不管裏面的存儲方式怎麼改變,只要 U 盤上的 USB 接口不變,就不會影響用戶的正常操作。
- 提高軟件的複用率,降低成本。每個系統都是一個相對獨立的整體,可以在不同的環境中得到使用。例如,一個 U 盤可以在多臺電腦上使用。
Java 語言的基本封裝單位是類。由於類的用途是封裝複雜性,所以類的內部有隱藏實現複雜性的機制。Java 提供了私有和公有的訪問模式,類的公有接口代表外部的用戶應該知道或可以知道的每件東西,私有的方法數據只能通過該類的成員代碼來訪問,這就可以確保不會發生不希望的事情。
2、優點
-
良好的封裝能夠減少耦合。
-
類內部的結構可以自由修改。
-
可以對成員變量進行更精確的控制。
-
隱藏信息,實現細節。
3、訪問修飾符
Java中通過訪問修飾符對類中的屬性和方法進行修飾,從而對對應的屬性和方法賦予相應的訪問權限控制。
修飾符 | 同類 | 同包 | 子類 | 其它 |
private | √ | |||
默認 | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
當我們對某個類中的屬性用private進行修飾,然後提供public的get()/set()方法時,我們其實對該類的屬性進行了封裝,因爲其它類無法直接訪問到這些屬性,只能通過公共的方法獲取或設置。
this關鍵字
關於this關鍵字詳情點擊這裏
內部類
內部類( Inner Class )就是定義在另外一個類裏面的類。與之對應,包含內部類的類被稱爲外部類。
那麼問題來了:那爲什麼要將一個類定義在另一個類裏面呢?清清爽爽的獨立的一個類多好啊!!
內部類的主要作用如下:
1. 內部類提供了更好的封裝,可以把內部類隱藏在外部類之內,不允許同一個包中的其他類訪問該類。
2. 內部類的方法可以直接訪問外部類的所有數據,包括私有的數據。
3. 內部類所實現的功能使用外部類同樣可以實現,只是有時使用內部類更方便。
內部類可分爲以下幾種:
-
成員內部類
-
靜態內部類
-
方法內部類
-
匿名內部類
關於內部類,詳情點擊這裏
繼承
1、概念
繼承這個詞我們在生活中也經常聽到,新聞報道某小夥不願繼承其父親的萬畝養豬場跑到城市打工。就像兒子繼承老子的財務一樣,在Java中繼承則表示子類可以繼承父類的屬性和方法,繼承是java面向對象編程技術的一塊基石,因爲它允許創建分等級層次的類。Java中只允許單繼承,及一個類繼承了一個類,則無法同時繼承其它類。
看下下面代碼:
/**
* @desc 定義一個男孩類
* @author dataozi
* @date 2020/6/7 16:00
*/
public class Boy {
// 男孩的名字
private String name;
// 男孩的年齡
private Integer age;
/**
* @desc 男孩會喫飯
* @author dataozi
* @date 2020/6/7 15:26
*/
public void eat(){
}
/**
* @desc 男孩會跑
* @author dataozi
* @date 2020/6/7 15:26
*/
public void run(){
}
}
/**
* @desc 定義一個女孩類
* @author dataozi
* @date 2020/6/7 16:00
*/
public class Girl {
// 女孩的名字
private String name;
// 女孩的年齡
private Integer age;
/**
* @desc 女孩會喫飯
* @author dataozi
* @date 2020/6/7 15:26
*/
public void eat(){
}
/**
* @desc 女孩會跑
* @author dataozi
* @date 2020/6/7 15:26
*/
public void run(){
}
}
從上面代碼中可以看到,男孩類和女孩類都有名字和年齡的屬性,都有喫飯、跑步的方法。但是我們在兩個類中都進行了聲明,這樣顯得很重複。所以我們嘗試將男孩和女孩公有的屬性和方法進行抽取,聲明一個人類,叫男孩和女孩分別繼承這個人類,使用這種層次形的分類方式,是爲了將多個類的通用屬性和方法提取出來,放在它們的父類中,然後只需要在子類中各自定義自己獨有的屬性和方法,並以繼承的形式在父類中獲取它們的通用屬性和方法即可。
繼承是類與類的一種關係,是一種“is a”的關係。比如“狗”繼承“動物”,這裏動物類是狗類的父類或者基類,狗類是動物類的子類或者派生類。Java中使用extends實現繼承。
/**
* @desc 定義一個男孩類
* @author dataozi
* @date 2020/6/7 16:00
*/
public class Boy extends Person{
}
/**
* @desc 定義一個女孩類
* @author dataozi
* @date 2020/6/7 16:00
*/
public class Girl extends Person{
}
@Test
public void testForOO(){
// 我是人類中的一個真實的對象
Boy dataozi = new Boy();
// 我的名字叫大套子
dataozi.setName("大套子");
// 我年年都18
dataozi.setAge(18);
// 我是人,所以我會喫
dataozi.eat();
}
可以看到,雖然Boy類沒有任何屬性和方法,但是因爲它繼承了Person類,所以Boy類擁有了Person類的屬性和方法。
補充:Java中的繼承只能單繼承,但是可以通過內部類繼承其他類來實現多繼承。
public class Son extends Father{
public void go () {
System.out.println("son go");
}
public void eat () {
System.out.println("son eat");
}
public void sleep() {
System.out.println("zzzzzz");
}
public void cook() {
//匿名內部類實現的多繼承
new Mother().cook();
//內部類繼承第二個父類來實現多繼承
Mom mom = new Mom();
mom.cook();
}
private class Mom extends Mother {
@Override
public void cook() {
System.out.println("mom cook");
}
}
}
2、優點
- 代碼共享,減少創建類的工作量,每個子類都擁有父類的方法和屬性;
- 提高代碼的重用性;
- 子類可以形似父類,但又異於父類,“龍生龍,鳳生鳳,老鼠生來會打洞”是說子擁有 父的“種”,“世界上沒有兩片完全相同的葉子”是指明子與父的不同;
- 提高代碼的可擴展性,實現父類的方法就可以“爲所欲爲”了,君不見很多開源框架的擴展接口都是通過繼承父類來完成的;
3、重寫
子類如果對繼承的父類的方法不滿意(不適合),可以自己編寫繼承的方法,這種方式就稱爲方法的重寫。當調用方法時會優先調用子類的方法。
重寫要注意:
a、返回值類型
b、方法名
c、參數類型及個數
都要與父類繼承的方法相同,才叫方法的重寫。
重載和重寫的區別:
方法重載:在同一個類中處理不同數據的多個相同方法名的多態手段。
方法重寫:相對繼承而言,子類中對父類已經存在的方法進行區別化的修改。
4、繼承的初始化順序
1、初始化父類再初始化子類
2、先執行初始化對象中屬性,再執行構造方法中的初始化。
基於上面兩點,我們就知道實例化一個子類,java程序的執行順序是:
父類對象屬性初始化---->父類對象構造方法---->子類對象屬性初始化--->子類對象構造方法
5、final關鍵字
使用final關鍵字做標識有“最終的”含義。
1. final 修飾類,則該類不允許被繼承。
2. final 修飾方法,則該方法不允許被覆蓋(重寫)。
3. final 修飾屬性,則該類的該屬性不會進行隱式的初始化,所以 該final 屬性的初始化屬性必須有值,或在**構造方法中賦值(但只能選其一,且必須選其一,因爲沒有默認值!),**且初始化之後就不能改了,只能賦值一次。
4. final 修飾變量,則該變量的值只能賦一次值,在聲明變量的時候才能賦值,即變爲常量。
6、super關鍵字
在對象的內部使用,可以代表父類對象。
1、訪問父類的屬性:super.age
2、訪問父類的方法:super.eat()
super的應用:
首先我們知道子類的構造的過程當中必須調用父類的構造方法。其實這個過程已經隱式地使用了我們的super關鍵字。
這是因爲如果子類的構造方法中沒有顯示調用父類的構造方法,則系統默認調用父類無參的構造方法。
那麼如果自己用super關鍵字在子類裏調用父類的構造方法,則必須在子類的構造方法中的第一行。
要注意的是:如果子類構造方法中既沒有顯示調用父類的構造方法,而父類沒有無參的構造方法,則編譯出錯。
(補充說明,雖然沒有顯示聲明父類的無參的構造方法,系統會自動默認生成一個無參構造方法,但是,如果你聲明瞭一個有參的構造方法,而沒有聲明無參的構造方法,這時系統不會動默認生成一個無參構造方法,此時稱爲父類有沒有無參的構造方法。)
多態
1、概念
面向對象的多態性,即“一個接口,多個方法”。就好比LoL中的Q、W、E、R四個鍵位一樣,同樣的鍵位,當你使用蓋倫時R鍵的技能就是從天而降的大寶劍,當你使用皇子時R鍵就是從天而降的岩石圈。Java中的多態性體現在父類中定義的屬性和方法被子類繼承後,可以具有不同的屬性或表現方式。多態性允許一個接口被多個同類使用,彌補了單繼承的不足。
同一個事件發生在不同的對象上會產生不同的結果。
多態是同一個行爲具有多個不同表現形式或形態的能力。
多態就是同一個接口,使用不同的實例而執行不同操作,如圖所示:
多態性是對象多種表現形式的體現。
2、重寫和重載
多態一般可以分爲兩種,一個是重寫override,一個是重載overload。
重寫是由於繼承關係中的子類有一個和父類同名同參數的方法,會覆蓋掉父類的方法。重載是因爲一個同名方法可以傳入多個參數組合。
注意,同名方法如果參數相同,即使返回值不同也是不能同時存在的,編譯會出錯。
從jvm實現的角度來看,重寫又叫運行時多態,編譯時看不出子類調用的是哪個方法,但是運行時操作數棧會先根據子類的引用去子類的類信息中查找方法,找不到的話再到父類的類信息中查找方法。
而重載則是編譯時多態,因爲編譯期就可以確定傳入的參數組合,決定調用的具體方法是哪一個了。
3、向上轉型和向下轉型
public static void main(String[] args) {
Son son = new Son();
//首先先明確一點,轉型指的是左側引用的改變。
//father引用類型是Father,指向Son實例,就是向上轉型,既可以使用子類的方法,也可以使用父類的方法。
//向上轉型,此時運行father的方法
Father father = son;
father.smoke();
//不能使用子類獨有的方法。
// father.play();編譯會報錯
father.drive();
//Son類型的引用指向Father的實例,所以是向下轉型,不能使用子類非重寫的方法,可以使用父類的方法。
//向下轉型,此時運行了son的方法
Son son1 = (Son) father;
//轉型後就是一個正常的Son實例
son1.play();
son1.drive();
son1.smoke();
//因爲向下轉型之前必須先經歷向上轉型。
//在向下轉型過程中,分爲兩種情況:
//情況一:如果父類引用的對象如果引用的是指向的子類對象,
//那麼在向下轉型的過程中是安全的。也就是編譯是不會出錯誤的。
//因爲運行期Son實例確實有這些方法
Father f1 = new Son();
Son s1 = (Son) f1;
s1.smoke();
s1.drive();
s1.play();
//情況二:如果父類引用的對象是父類本身,那麼在向下轉型的過程中是不安全的,編譯不會出錯,
//但是運行時會出現java.lang.ClassCastException錯誤。它可以使用instanceof來避免出錯此類錯誤。
//因爲運行期Father實例並沒有這些方法。
Father f2 = new Father();
Son s2 = (Son) f2;
s2.drive();
s2.smoke();
s2.play();
//向下轉型和向上轉型的應用,有些人覺得這個操作沒意義,何必先向上轉型再向下轉型呢,不是多此一舉麼。其實可以用於方法參數中的類型聚合,然後具體操作再進行分解。
//比如add方法用List引用類型作爲參數傳入,傳入具體類時經歷了向下轉型
add(new LinkedList());
add(new ArrayList());
//總結
//向上轉型和向下轉型都是針對引用的轉型,是編譯期進行的轉型,根據引用類型來判斷使用哪個方法
//並且在傳入方法時會自動進行轉型(有需要的話)。運行期將引用指向實例,如果是不安全的轉型則會報錯。
//若安全則繼續執行方法。
}
public static void add(List list) {
System.out.println(list);
//在操作具體集合時又經歷了向上轉型
// ArrayList arr = (ArrayList) list;
// LinkedList link = (LinkedList) list;
}
總結: 向上轉型和向下轉型都是針對引用的轉型,是編譯期進行的轉型,根據引用類型來判斷使用哪個方法。並且在傳入方法時會自動進行轉型(有需要的話)。運行期將引用指向實例,如果是不安全的轉型則會報錯,若安全則繼續執行方法。
4、編譯期的靜態分派
其實就是根據引用類型來調用對應方法。
public static void main(String[] args) {
Father father = new Son();
靜態分派 a= new 靜態分派();
//編譯期確定引用類型爲Father。
//所以調用的是第一個方法。
a.play(father);
//向下轉型後,引用類型爲Son,此時調用第二個方法。
//所以,編譯期只確定了引用,運行期再進行實例化。
a.play((Son)father);
//當沒有Son引用類型的方法時,會自動向上轉型調用第一個方法。
a.smoke(father);
//
}
public void smoke(Father father) {
System.out.println("father smoke");
}
public void play (Father father) {
System.out.println("father");
//father.drive();
}
public void play (Son son) {
System.out.println("son");
//son.drive();
}
3、方法重載優先級匹配
public static void main(String[] args) {
方法重載優先級匹配 a = new 方法重載優先級匹配();
//普通的重載一般就是同名方法不同參數。
//這裏我們來討論當同名方法只有一個參數時的情況。
//此時會調用char參數的方法。
//當沒有char參數的方法。會調用int類型的方法,如果沒有int就調用long
//即存在一個調用順序char -> int -> long ->double -> ..。
//當沒有基本類型對應的方法時,先自動裝箱,調用包裝類方法。
//如果沒有包裝類方法,則調用包裝類實現的接口的方法。
//最後再調用持有多個參數的char...方法。
a.eat('a');
a.eat('a','c','b');
}
public void eat(short i) {
System.out.println("short");
}
public void eat(int i) {
System.out.println("int");
}
public void eat(double i) {
System.out.println("double");
}
public void eat(long i) {
System.out.println("long");
}
public void eat(Character c) {
System.out.println("Character");
}
public void eat(Comparable c) {
System.out.println("Comparable");
}
public void eat(char ... c) {
System.out.println(Arrays.toString(c));
System.out.println("...");
}
// public void eat(char i) {
// System.out.println("char");
// }