1. 程式人生 > 實用技巧 >java學習筆記2-面向物件

java學習筆記2-面向物件

Java常見點解析

面向物件(注意聯絡C++)

面向物件基礎

面向過程與面向物件

​ 面向過程(Procedure Oriented)和麵向物件(Object Oriented,OO)都是對軟體分析、設計和開發的一種思想,它指導著人們以不同的方式去分析、設計和開發軟體。早期先有面向過程思想,隨著軟體規模的擴大,問題複雜性的提高,面向過程的弊端越來越明顯的顯示出來,出現了面向物件思想併成為目前主流的方式。兩者都貫穿於軟體分析、設計和開發各個階段,對應面向物件就分別稱為面向物件分析(OOA)、面向物件設計(OOD)和麵向物件程式設計(OOP)。

面向過程適合簡單、不需要協作的事務。面向物件(Object)思想更契合人的思維模式。

面向物件和麵向過程的總結

  1. 都是解決問題的思維方式,都是程式碼組織的方式。
  2. 解決簡單問題可以使用面向過程
  3. 解決複雜問題:巨集觀上使用面向物件把握,微觀處理上仍然是面向過程。

類:我們叫做class。 物件:我們叫做Object,instance(例項)。以後我們說某個類的物件,某個類的例項。是一樣的意思。

  1. 物件是具體的事物;類是對物件的抽象;
  2. 類可以看成一類物件的模板,物件可以看成該類的一個具體例項。
  3. 類是用於描述同一型別的物件的一個抽象概念,類中定義了這一類物件所應具有的共同的屬性、方法。
//模擬一個電腦類
class Computer {
    String brand;  //品牌
}
public class SxtStu {
    // field
    int id;
    String sname;
    int age;
    Computer comp;//計算機 
    void study() {
        System.out.println("我正在學習!使用我們的電腦,"+comp.brand);
    }
    
    //構造方法。回想C++
    SxtStu() {
    }
    public static void main(String[] args) {
        SxtStu stu1 = new SxtStu();
        stu1.sname = "張三";
        Computer comp1 = new Computer();
         comp1.brand = "聯想";
        stu1.comp = comp1;
        stu1.study();
    }
}

記憶體分析

 Java虛擬機器的記憶體可以分為三個區域:棧stack、堆heap、方法區method area。

棧的特點如下:

  1. 棧描述的是方法執行的記憶體模型。每個方法被呼叫都會建立一個棧幀(儲存區域性變數、運算元、方法出口等)
  2. JVM為每個執行緒建立一個棧,用於存放該執行緒執行方法的資訊(實際引數、區域性變數等)
  3. 棧屬於執行緒私有,不能實現執行緒間的共享!
  4. 棧的儲存特性是“先進後出,後進先出”
  5. 棧是由系統自動分配,速度快!棧是一個連續的記憶體空間!

堆的特點如下:

  1. 堆用於儲存建立好的物件和陣列(陣列也是物件)
  2. JVM只有一個堆,被所有執行緒共享
  3. 堆是一個不連續的記憶體空間,分配靈活,速度慢!

方法區(又叫靜態區)特點如下:

  1. JVM只有一個方法區,被所有執行緒共享!
  2. 方法區實際也是堆,只是用於儲存類、常量相關的資訊!
  3. 用來存放程式中永遠是不變或唯一的內容。(類資訊【Class物件】、靜態變數、字串常量等)

在最開始賦值時,記憶體中的堆與棧還有方法區如下:

那麼,接下來,就要通過地址相互指認(這個術語不專業,可以理解為工作牌和工作人一一對應關係);

給自己提個問,堆與棧的區別是什麼?

答:java中堆和棧的區別自然是面試中的常見問題,下面幾點就是其具體的區別

1、各司其職

最主要的區別就是棧記憶體用來儲存區域性變數和方法呼叫。而堆記憶體用來儲存Java中的物件。無論是成員變數,區域性變數,還是類變數,它們指向的物件都儲存在堆記憶體中。

2、獨有還是共享

棧記憶體歸屬於單個執行緒,每個執行緒都會有一個棧記憶體,其儲存的變數只能在其所屬執行緒中可見,即棧記憶體可以理解成執行緒的私有記憶體。而堆記憶體中的物件對所有執行緒可見。堆記憶體中的物件可以被所有執行緒訪問。

3、異常錯誤

如果棧記憶體沒有可用的空間儲存方法呼叫和區域性變數,JVM會丟擲java.lang.StackOverFlowError。
而如果是堆記憶體沒有可用的空間儲存生成的物件,JVM會丟擲java.lang.OutOfMemoryError。

4、空間大小

棧的記憶體要遠遠小於堆記憶體,如果你使用遞迴的話,那麼你的棧很快就會充滿。如果遞迴沒有及時跳出,很可能發生StackOverFlowError問題。

你可以通過-Xss選項設定棧記憶體的大小。-Xms選項可以設定堆的開始時的大小,-Xmx選項可以設定堆的最大值。

這就是Java中堆和棧的區別。理解好這個問題的話,可以對你解決開發中的問題,分析堆記憶體和棧記憶體使用,甚至效能調優都有幫助。

構造方法

 構造器也叫構造方法(constructor),用於物件的初始化。構造器是一個建立物件時被自動呼叫的特殊方法,目的是物件的初始化。構造器的名稱應與類的名稱一致。Java通過new關鍵字來呼叫構造器,從而返回該類的例項,是一種特殊的方法。

要點:

  1. 通過new關鍵字呼叫!!
  2. 構造器雖然有返回值,但是不能定義返回值型別(返回值的型別肯定是本類),不能在構造器裡使用return返回某個值。
  3. 如果我們沒有定義構造器,則編譯器會自動定義一個無參的建構函式。如果已定義則編譯器不會自動新增!
  4. 構造器的方法名必須和類名一致!

構造方法類的過載

 構造方法也是方法,只不過有特殊的作用而已。與普通方法一樣,構造方法也可以過載。如果方法構造中形參名與屬性名相同時,需要使用this關鍵字區分屬性與形參,如this.id 表示屬性id;id表示形參id。

Java引入了垃圾回收機制,令C++程式設計師最頭疼的記憶體管理問題迎刃而解。Java程式設計師可以將更多的精力放到業務邏輯上而不是記憶體管理工作上,大大的提高了開發效率。

通用的分代垃圾回收機制(重中之重)

​ 分代垃圾回收機制,是基於這樣一個事實:不同的物件的生命週期是不一樣的。因此,不同生命週期的物件可以採取不同的回收演算法,以便提高回收效率。我們將物件分為三種狀態:年輕代、年老代、持久代。JVM將堆記憶體劃分為 Eden、Survivor 和 Tenured/Old 空間。

  1. 年輕代

  所有新生成的物件首先都是放在Eden區。 年輕代的目標就是儘可能快速的收集掉那些生命週期短的物件,對應的是Minor GC,每次 Minor GC 會清理年輕代的記憶體,演算法採用效率較高的複製演算法,頻繁的操作,但是會浪費記憶體空間。當“年輕代”區域存放滿物件後,就將物件存放到年老代區域。

  2. 年老代

  在年輕代中經歷了N(預設15)次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。年老代物件越來越多,我們就需要啟動Major GC和Full GC(全量回收),來一次大掃除,全面清理年輕代區域和年老代區域。

  3. 持久代

  用於存放靜態檔案,如Java類、方法等。持久代對垃圾回收沒有顯著影響。

圖4-7 堆記憶體的劃分細節

  ·Minor GC:

  用於清理年輕代區域。Eden區滿了就會觸發一次Minor GC。清理無用物件,將有用物件複製到“Survivor1”、“Survivor2”區中(這兩個區,大小空間也相同,同一時刻Survivor1和Survivor2只有一個在用,一個為空)

  ·Major GC:

  用於清理老年代區域。

  ·Full GC:

  用於清理年輕代、年老代區域。 成本較高,會對系統性能產生影響。

垃圾回收過程:

1、新建立的物件,絕大多數都會儲存在Eden中,

2、當Eden滿了(達到一定比例)不能建立新物件,則觸發垃圾回收(GC),將無用物件清理掉,

​ 然後剩餘物件複製到某個Survivor中,如S1,同時清空Eden區

3、當Eden區再次滿了,會將S1中的不能清空的物件存到另外一個Survivor中,如S2,

​ 同時將Eden區中的不能清空的物件,也複製到S1中,保證Eden和S1,均被清空。

4、重複多次(預設15次)Survivor中沒有被清理的物件,則會複製到老年代Old(Tenured)區中,

5、當Old區滿了,則會觸發一個一次完整地垃圾回收(FullGC),之前新生代的垃圾回收稱為(minorGC)

在實際開發中,經常會造成系統的崩潰。如下這些操作我們應該注意這些使用場景。

  1. 建立大量無用物件

    比如,我們在需要大量拼接字串時,使用了String而不是StringBuilder。

    String str = "";``for (int i = 0; i < 10000; i++) {
        str += i;   //相當於產生了10000個String物件
    }
    
  2. 靜態集合類的使用

     像HashMap、Vector、List等的使用最容易出現記憶體洩露,這些靜態變數的生命週期和應用程式一致,所有的物件Object也不能被釋放。

  3. 各種連線物件(IO流物件、資料庫連線物件、網路連線物件)未關閉

    IO流物件、資料庫連線物件、網路連線物件等連線物件屬於物理連線,和硬碟或者網路連線,不使用的時候一定要關閉。

  4. 監聽器的使用

釋放物件時,沒有刪除相應的監聽器。

注意:

  1. 程式設計師無權呼叫垃圾回收器。
  2. 程式設計師可以呼叫System.gc(),該方法只是通知JVM,並不是執行垃圾回收器。儘量少用,會申請啟動Full GC,成本高,影響系統性能。
  3. finalize方法,是Java提供給程式設計師用來釋放物件或資源的方法,但是儘量少

this關鍵字

物件建立的過程和this的本質

  構造方法是建立Java物件的重要途徑,通過new關鍵字呼叫構造器時,構造器也確實返回該類的物件,但這個物件並不是完全由構造器負責建立。建立一個物件分為如下四步:

  1. 分配物件空間,並將物件成員變數初始化為0或空
  2. 執行屬性值的顯示初始化
  3. 執行構造方法
  4. 返回物件的地址給相關的變數

this的本質就是“建立好的物件的地址”! 由於在構造方法呼叫前,物件已經建立。因此,在構造方法中也可以使用this代表“當前物件” 。

this最常的用法:

  1. 在程式中產生二義性之處,應使用this來指明當前物件;普通方法中,this總是指向呼叫該方法的物件。構造方法中,this總是指向正要初始化的物件。
  2. 使用this關鍵字呼叫過載的構造方法,避免相同的初始化程式碼。但只能在構造方法中用,並且必須位於構造方法的第一句。
  3. this不能用於static方法中。

static 關鍵字

在類中,用static宣告的成員變數為靜態成員變數,也稱為類變數。 類變數的生命週期和類相同,在整個應用程式執行期間都有效。它有如下特點:

  1. 為該類的公用變數,屬於類,被該類的所有例項共享,在類被載入時被顯式初始化。
  2. 對於該類的所有物件來說,static成員變數只有一份。被該類的所有物件共享!!
  3. 一般用“類名.類屬性/方法”來呼叫。(也可以通過物件引用或類名(不需要例項化)訪問靜態成員。)
  4. 在static方法中不可直接訪問非static的成員。

核心要點:

static修飾的成員變數和方法,從屬於類。

普通變數和方法從屬於物件的。

靜態初始塊

構造方法用於物件的初始化!靜態初始化塊,用於類的初始化操作!在靜態初始化塊中不能直接訪問非static成員。

注意事項:

  靜態初始化塊執行順序(學完繼承再看這裡):

  1. 上溯到Object類,先執行Object的靜態初始化塊,再向下執行子類的靜態初始化塊,直到我們的類的靜態初始化塊為止。

  2. 構造方法執行順序和上面順序一樣!!

引數傳值機制

  Java中,方法中所有引數都是“值傳遞”,也就是“傳遞的是值的副本”。 也就是說,我們得到的是“原引數的影印件,而不是原件”。因此,影印件改變不會影響原件。

基本資料型別引數的傳值

  傳遞的是值的副本。 副本改變不會影響原件。

引用型別引數的傳值

  傳遞的是值的副本。但是引用型別指的是“物件的地址”。因此,副本和原引數都指向了同一個“地址”,改變“副本指向地址物件的值,也意味著原引數指向物件的值也發生了改變”。

包機制是Java中管理類的重要手段。 開發中,我們會遇到大量同名的類,通過包我們很容易對解決類重名的問題,也可以實現對類的有效管理。 包對於類,相當於資料夾對於檔案的作用。

package

 我們通過package實現對類的管理,package的使用有兩個要點:

  1. 通常是類的第一句非註釋性語句。

  2. 包名:域名倒著寫即可,再加上模組名,便於內部管理類。

Java中的常用包 說明
java.lang 包含一些Java語言的核心類,如String、Math、Integer、System和Thread,提供常用功能。
java.awt 包含了構成抽象視窗工具集(abstract window toolkits)的多個類,這些類被用來構建和管理應用程式的圖形使用者介面(GUI)。
java.net 包含執行與網路相關的操作的類。
java.io 包含能提供多種輸入/輸出功能的類。
java.util 包含一些實用工具類,如定義系統特性、使用與日期日曆相關的函式。
靜態匯入

靜態匯入(static import)是在JDK1.5新增加的功能,其作用是用於匯入指定類的靜態屬性,這樣我們可以直接使用靜態屬性。

面向物件進階

繼承的實現

繼承讓我們更加容易實現類的擴充套件。 比如,我們定義了人類,再定義Boy類就只需要擴充套件人類即可。實現了程式碼的重用,不用再重新發明輪子。但是這裡要注意,java中一個類可以實現多個介面,但只能繼承一個類。

instanceof 運算子

instanceof是二元運算子,左邊是物件,右邊是類;當物件是右面類或子類所建立物件時,返回true;否則,返回false。

方法的重寫override

子類通過重寫父類的方法,可以用自身的行為替換父類的行為。方法的重寫是實現多型的必要條件。

方法的重寫需要符合下面的三個要點:

  1. “==”: 方法名、形參列表相同。
  2. “≤”:返回值型別和宣告異常型別,子類小於等於父類。
  3. “≥”: 訪問許可權,子類大於等於父類。

Object類基本特性

Object類是所有Java類的根基類,也就意味著所有的Java物件都擁有Object類的屬性和方法。如果在類的宣告中未使用extends關鍵字指明其父類,則預設繼承Object類。

toString方法

Object類中定義有public String toString()方法,其返回值是 String 型別。Object類中toString方法的原始碼為:

/**
在這個原始碼閱讀註釋的過程中,我的理解如下:
  建議所有子類(即要使用此方法時)都重寫此方法
  該字串包含物件為例項的類的名稱,符號字元`{@code @} ',以及*物件的雜湊碼的無符號十六進位制表示形式。
*/

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

根據如上原始碼得知,預設會返回“類名+@+16進位制的hashcode”。在列印輸出或者用字串連線物件時,會自動呼叫該物件的toString()方法。

==和equals方法

“==”代表比較雙方是否相同。如果是基本型別則表示值相等,如果是引用型別則表示地址相等即是同一個物件。

Object類中定義有:public boolean equals(Object obj)方法,提供定義“物件內容相等”的邏輯。比如,我們在公安系統中認為id相同的人就是同一個人、學籍系統中認為學號相同的人就是同一個人。

Object 的 equals 方法預設就是比較兩個物件的hashcode,是同一個物件的引用時返回 true 否則返回 false。但是,我們可以根據我們自己的要求重寫equals方法。

super關鍵字

​ super是直接父類物件的引用。可以通過super來訪問父類中被子類覆蓋的方法或屬性。

​ 使用super呼叫普通方法,語句沒有位置限制,可以在子類中隨便呼叫。

​ 若是構造方法的第一行程式碼沒有顯式的呼叫super(...)或者this(...);那麼Java預設都會呼叫super(),含義是呼叫父類的無引數構造方法。這裡的super()可以省略。

繼承樹追溯

屬性/方法查詢順序:(比如:查詢變數h)

  1. 查詢當前類中有沒有屬性h

  2. 依次上溯每個父類,檢視每個父類中是否有h,直到Object

  3. 如果沒找到,則出現編譯錯誤。

  4. 上面步驟,只要找到h變數,則這個過程終止。

構造方法呼叫順序:

構造方法第一句總是:super(…)來呼叫父類對應的構造方法。所以,流程就是:先向上追溯到Object,然後再依次向下執行類的初始化塊和構造方法,直到當前子類為止。

注:靜態初始化塊呼叫順序,與構造方法呼叫順序一樣,不再重複。

封裝的作用和含義

​ 作用就是具體內部是怎麼實現的,我們不需要操心。

程式設計中封裝的具體優點:

  1. 提高程式碼的安全性。

  2. 提高程式碼的複用性。

  3. “高內聚”:封裝細節,便於修改內部程式碼,提高可維護性。

  4. “低耦合”:簡化外部呼叫,便於呼叫者使用,便於擴充套件和協作。

沒有封裝的程式碼會出現一些問題

class  Person {
    String name;
    int  age;
    @Override
    public  String toString() {
        return  "Person [name="  + name +  ", age="  + age + "]";
    }
}
public  class  Test {
    public  static  void  main(String[] args) {
        Person p =  new Person();
        p.name =  "小紅";    
        p.age = -45;//年齡可以通過這種方式隨意賦值,沒有任何限制    
        System.out.println(p);
    }
}

我們都知道,年齡不可能是負數,也不可能超過130歲,但是如果沒有使用封裝的話,便可以給年齡賦值成任意的整數,這顯然不符合我們的正常邏輯思維。執行結果如圖所示:

再比如說,如果哪天我們需要將Person類中的age屬性修改為String型別的,你會怎麼辦?你只有一處使用了這個類的話那還比較幸運,但如果你有幾十處甚至上百處都用到了,那你豈不是要改到崩潰。而封裝恰恰能解決這樣的問題。如果使用封裝,我們只需要稍微修改下Person類的setAge()方法即可,而無需修改使用了該類的客戶程式碼。

封裝的實現—使用訪問控制符

Java是使用“訪問控制符”來控制哪些細節需要封裝,哪些細節需要暴露的。 Java中4種“訪問控制符”分別為private、default、protected、public,它們說明了面向物件的封裝性,所以我們要利用它們儘可能的讓訪問許可權降到最低,從而提高安全性。

  1. private 表示私有,只有自己類能訪問

  2. default表示沒有修飾符修飾,只有同一個包的類能訪問

  3. protected表示可以被同一個包的類以及其他包中的子類訪問

  4. public表示可以被該專案的所有包中的所有類訪問

封裝的使用細節

類的屬性的處理:

  1. 一般使用private訪問許可權。

  2. 提供相應的get/set方法來訪問相關屬性,這些方法通常是public修飾的,以提供對屬性的賦值與讀取操作(注意:boolean變數的get方法是is開頭!)。

  3. 一些只用於本類的輔助性方法可以用private修飾,希望其他類呼叫的方法用public修飾。

    public Person(String name, int age) {
        this.name = name;
        // this.age = age;//構造方法中不能直接賦值,應該呼叫setAge方法
        setAge(age);
    }

    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setAge(int age) {
        //在賦值之前先判斷年齡是否合法
        if (age > 130 || age < 0) {
            this.age = 18;//不合法賦預設值18
        } else {
            this.age = age;//合法才能賦值給屬性age
        }
    }
    public int getAge() {
        return age;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

public class Test2 {
    public static void main(String[] args) {
        Person p1 = new Person();
        //p1.name = "小紅"; //編譯錯誤
        //p1.age = -45;  //編譯錯誤
        p1.setName("小紅");
        p1.setAge(-45);
        System.out.println(p1);

        Person p2 = new Person("小白", 300);
        System.out.println(p2);
    }
}

簡言之就是set get方法

多型

多型指的是同一個方法呼叫,由於物件不同可能會有不同的行為。

多型的要點:

  1. 多型是方法的多型,不是屬性的多型(多型與屬性無關)。

  2. 多型的存在要有3個必要條件:繼承,方法重寫,父類引用指向子類物件。

  3. 父類引用指向子類物件後,用該父類引用呼叫子類重寫的方法,此時多型就出現了。

物件的轉型

父類引用指向子類物件,我們稱這個過程為向上轉型,屬於自動型別轉換。

向上轉型後的父類引用變數只能呼叫它編譯型別的方法,不能呼叫它執行時型別的方法。這時,我們就需要進行型別的強制轉換,我們稱之為向下轉型!

在向下轉型過程中,必須將引用變數轉成真實的子類型別(執行時型別)否則會出現型別轉換異常ClassCastException。

為了避免出現這種異常,我們可以使用instanceof運算子進行判斷。