1. 程式人生 > 實用技巧 >JavaSE之面向物件(中)

JavaSE之面向物件(中)

一、封裝

1、封裝概述:

面向物件程式語言是對客觀世界的描述,客觀世界裡的成員變數都是隱藏在物件內部,外界無法直接進行操作。封裝可以認為是一個保護罩,可以防止該類中的成員變數和方法被其他類隨意訪問和修改。要訪問該類的資料,必須通過指定的方法。

2、高內聚低耦合

高內聚:類內資料操作細節自己完成,不允許類外干涉
低耦合:僅對外部提供少量方法
隱藏物件內部的複雜性,只對外公開簡單的介面。便於外界呼叫,從而提高系統的可擴充套件性、可維護性。通俗的講,把該隱藏的隱藏起來,該暴露的暴露出來。這就是封裝性的設計思想。

3、類的封裝
把屬性和方法封裝在類內,外部通過呼叫方法完成對類內指定屬性或方法進行操做,不必知道類內具體實現。

二、屬性的封裝

1、原則
將屬性隱藏起來,若需要訪問某個屬性,提供公共方法對其訪問。

2、屬性封裝的目的
1.隱藏類的實現細節
2.使使用者只能通過指定方法訪問,限制危險訪問
3.可以進行資料檢查,從而有利於保證物件資訊的完整性
4.便於修改,提高程式碼的可維護性

3、實現步驟

1.使用private修飾成員變數
2.使用get/set方法訪問成員變數

三、子類繼承父類問題

class Father {
    private String name;
    
    public void setName(String name) {
        this.name = name;
    }
    public void sayHi() {
        System.out.println("My name is " + name);
    }
}

class Son extends Father {}

public class PrivateFieldTest {
    public static void main(String[] args) {
        Father f1 = new Father();
        Son s1 = new Son();
        f1.sayHi();
        s1.sayHi();
        System.out.println();
        f1.setName("Sam");
        f1.sayHi();
        s1.sayHi();
        System.out.println();
        s1.setName("Tom");
        f1.sayHi();
        s1.sayHi();
    }
}
  • 對於子類繼承問題,官方解釋為子類不繼承父類中private的成員。但是父類中如果有公用的方法可以訪問private成員的話,子類是可以訪問父類的private成員。
  • 巢狀類可以訪問其封閉類的所有private成員,包括欄位和方法。因此,子類繼承的公共或受保護的巢狀類可以間接訪問超類的所有private成員。
  • 另外一種解釋認為,子類將父類的全部成員和方法繼承。父類的私有屬性和方法子類無法訪問。而子類可以通過呼叫父類中的public修飾的get/set方法訪問私有成員。父類的private修飾的私有成員會繼承到子類中,子類可以擁有但不能去使用。
  • 記憶體中表現為,當一個子類被例項化時,預設先呼叫父類的構造方法對父類進行初始化,即在記憶體中建立一個父類物件,然後在父類物件外面建立子類獨有的屬性,這才是一個完整的子類物件。
  • 再次,我們總結一下,子類不能繼承父類的私有成員,但子類的物件包括自己獨有的成員、繼承來的成員以及父類的私有成員。
  • 通過上述的程式可以看出,雖然子類不能繼承父類的私有成員,但還是可以通過公有方法訪問私有成員。另外值得注意的是:如果父類有無參構造方法,那麼子類的構造方法中可以不使用super呼叫。

無參構造

class Father {
    private String name;
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getName(){
		return name;
	}
	
    public void sayHi() {
        System.out.println("My name is " + name);
    }
}

class Son extends Father {}

public class PrivateFieldTest {
    public static void main(String[] args) {
    	//建立子類物件
        Son s = new Son();
        //用set方法賦值
        s.setName("張三");
        System.out.println(s.getName);
    }
}
建立子類物件,呼叫空參構造,用set方法賦值,用get方法獲值是完全沒有問題的,因為子類繼承父類的時候,若子類中沒有任何成員時,子類只能通過無參構造對成員變數進行初始化,然後通過set方法賦值.

有參構造

class Son{
		public Son(){
			//為了可以使用無參,我們把無參也寫上
		}

		public Son(String name,int age){
			this.setName(name);
    		this.setAge(age);
			// 也可以用super.setName  和 super.setAge 
			// 也可以用 super(name, age) 呼叫父類有參構造對父類成員進行初始化(建議)
		}
}
public class test{
	public static void main(String[] args){
		//建立子類物件
		Son s = new Son("張三",20); 
		System.out.println(s.getName+","+s.getAge);
	}
}
如果父類中只有參構造而沒有無參構造時,在子類中必須對父類的有參構造進行顯式呼叫,因為子類成員初始化之前會對父類成員進行初始化. 也就是說,若子類構造第一行程式碼沒有呼叫父類構造,也沒有呼叫子類構造,則預設呼叫父類無參構造,但父類中若沒有無參構造,那隻能在子類中顯式調用出來,不然,父類成員變數無法進行初始化,子類無法使用.

原文連結:https://blog.csdn.net/qq_20085465/article/details/78439543

四、構造器

注意:

1.構造名必須與類名一致。
2.沒有返回值,因此不需要返回值型別,也不需要void
3.如果沒有定義構造器的話,會自動生成無參構造器,修飾符預設與類的修飾符一致
4.如果定義了構造器,則不會再提供無參構造器
5.構造器可以過載,包括無參與有參
6.構造器不能被static、final、synchronized、abstract、native修飾

五、this關鍵字

1、this代表當前物件的引用

1.this用於構造器中,表示正在建立的那個例項物件,正在new誰,誰就是this;比如Son s = new Son("張三",20); 此時this代表的就是s。
2.this用於例項方法中,表示呼叫方法的那個物件,誰在呼叫,誰就是this

2、this使用格式

1.當方法的區域性變數與當前物件的成員變數重名時。可以在成員變數前加this.;認為用於區分成員變數與區域性變數。即重名問題。[this.成員變數名]
2.呼叫當前物件自己的成員方法時,都可以加"this.",也可以省略,實際開發中都省略。[this.成員方法]
3.當需要呼叫本類的其他構造器時,就可以使用該形式。[this()或this(實參列表)]

需要注意的是

1.this不能用在static方法中!
2.如果一個類中聲明瞭n個構造器,則最多有 n - 1個構造器中使用了"this(【實參列表】)",否則會發生遞迴呼叫死迴圈

我們可以看一下程式碼解釋:

public class ThisTest {

  	 private int i=0;

    //第一個構造器:有一個int型形參

    ThisTest(int i){

       this.i=i+1;//此時this表示引用成員變數i,而非函式引數i

       System.out.println("Int constructor i——this.i:  "+i+"——"+this.i);

       System.out.println("i-1:"+(i-1)+"this.i+1:"+(this.i+1));

       //從兩個輸出結果充分證明了i和this.i是不一樣的!

    }

    //  第二個構造器:有一個String型形參

    ThisTest(String s){

       System.out.println("String constructor:  "+s);

    }

    //  第三個構造器:有一個int型形參和一個String型形參

    ThisTest(int i,String s){

       this(s);//this呼叫第二個構造器;此型別屬於第三種情況

       //this(i);

       /*此處不能用,因為其他任何方法都不能呼叫構造器,只有構造方法能呼叫他。

       但是必須注意:就算是構造方法呼叫構造器,也必須為於其第一行,構造方法也只能調

       用一個且僅一次構造器!*/

       this.i=i++;//this以引用該類的成員變數

       System.out.println("Int constructor:  "+i+"/n"+"String constructor:  "+s);

    }
此程式碼與部分解釋來自於:原文連結:https://blog.csdn.net/fzfengzhi/article/details/2174406

在此,我們談一下為何要使用構造器,同樣用程式碼進行解釋:

public class Student{
	private String name;
	private int age;
	
	public Student() {};
	
	public Student(String name, int age) {
		this.name = name;
		this.age = age;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public String getName() {
		return name;
	}
	
	public void setAge(int age) {
		this.age = age;
	}
	
	public int getAge() {
		return age;
	}
}

以上程式碼中的無參構造和有參構造部分主要是為了建立物件和物件的初始化。

構造方法的作用:
1.為了初始化成員屬性,而不是初始化物件,初始化物件是通過new關鍵字實現的
2.通過new呼叫構造方法初始化物件,編譯時根據引數簽名來檢查建構函式,稱為靜態聯編和編譯多型(引數簽名:引數的型別,引數個數和引數順序)
3.建立子類物件會呼叫父類構造方法但不會建立父類物件,只是呼叫父類構造方法初始化父類成員屬性

構造器與方法的總結:

方法實際上是需要用於執行java程式碼的
構造器實際上是一個類的例項,這是因為在初始化一個類時,java需要開闢一個新的記憶體空間,而java又是根據什麼?所以我們需要為這個類建立一個構造器,而這個構造器的名稱要和類名一致,所以java才能識別這個類,進而為這個類開闢記憶體空間。總之,我們在手動的為類“初始化”。

原文連結:https://blog.csdn.net/qiuzhongweiwei/article/details/78965788

六、包(Package)

1、包的作用:

1.可以避免重名,比如:包.類名
2.分類組織管理眾多的類:java.lang、java.net、java.util、java.io 、java.text、java.sql和javax.sql、java.awt和java.swing
3.可以控制某些型別或成員的可見範圍:如果某個型別或者成員的許可權修飾預設的話,那麼就僅限於本包使用

2、包的命名格式

1.package 包名;
2.包名:com.atguigu.xxx;
(1)所有單詞都小寫,每一個單詞之間使用.分割
(2)習慣用公司的域名倒置

七、static關鍵字

stactic是一個成員修飾符,可以修飾的成員包括成員變數、成員方法、成員內部類、程式碼快。而被修飾的成員是屬於類的,屬於類就可以不通過建立物件來呼叫。

1、靜態方法

[修飾符] static 返回值型別 方法名([形參列表]) {}

注意:

1.本類中,靜態方法可以直接訪問靜態變數和靜態方法
2.在其他類中,可以使用“類名.方法"進行呼叫,也可以使用"物件名.方法",推薦使用“類名.方法"
3.在靜態方法中,不能出現this!不能直接使用本類的非靜態成員。
4.非靜態的例項成員方法可以直接訪問靜態的類變數和方法。

這是因為this與非靜態的成員,這些都是需要建立物件時,才能使用的,而靜態方法呼叫時,可能沒有物件。

2、靜態變數:由static修飾的成員變數

1.該成員變數可以由該類所有物件共享
2.類變數的值和類資訊一起存放在方法區中
3.在static方法中如果有區域性變數與類變數重名時,使用“類名.成員變數"進行區別
4.該成員變數的get/set也是靜態的。

我們來看一下有關於呼叫靜態成員的程式碼和記憶體模型

class Preson{
    private static String school = "張老大幼兒園";
    private String name;

    public Preson(String name) {
        super();
        this.name = name;
    }
    // 省略掉get/set部分
}

public class Student {
    public static void main(String[] args) {
        Preson p1 = new Preson("張三");
        Preson p2 = new Preson("李四");

        p1.setSchool("王老大幼兒園");

        Preson.setSchool("wangSchool");
    }
}

八、變數的分類與區別

1、按照資料型別:

1.基本資料型別,存放的是資料值
2.引用資料型別,存放的是物件的地址值

2、按照宣告的位置:

1.成員變數:類內,方法外
2.區域性變數:類內,方法內

3、成員變數與區域性變數的區別

1.按照宣告位置
2.儲存的位置:

(1)成員變數:
靜態成員變數在方法區中的常量池中
非靜態的成員變數(例項變數),在堆中
(2)區域性變數在棧中

3.修飾符不同

(1)成員變數有四種許可權修飾符等其他修飾符,static修飾靜態
(2)區域性變數不使用修飾符

4.作用域不同

(1)靜態變數作用於整個類,在其他類中通過“類名.靜態變數”使用
(2)非靜態變數只能在非靜態的成員中使用,在其他類中通過“物件名.變數名”使用
(3)區域性變數,作用於本方法中

5.生命週期不同

(1)靜態變數:隨著類的載入而分配,類的解除安裝而消亡
(2)非靜態變數:隨著物件的建立才會在堆中分配記憶體,物件被垃圾回收而消亡
(3)區域性變數:每次方法呼叫時建立,方法結束時被回收。

在一些java的工具類裡,封裝了一些靜態方法,這樣做可以直接為靜態方法分配了一個固定的記憶體空間,可以被類名呼叫,不需要建立物件。可以直接傳入靜態變數訪問靜態方法。

九、繼承

1. 敘述:

繼承描述的是事物的所屬關係(is-a)。繼承就是子類繼承父類的屬性和方法,子類物件擁有與父類相同的屬性和行為,同時又擁有自己專屬的屬性和方法。

2. 繼承的好處

提高程式碼的複用性,擴充套件性。在類和類之間建立關係,是學習多型的前提。

3. 繼承的特點

1.私有化
父類的物件,無論是public公共的還是private私有的都會被子類繼承。但不同的是,對於私有成員,子類只是擁有卻無法直接使用,可以通過繼承父類的公共方法進行訪問。

2.成員變數重名情況
如果子類中存在與父類重名的成員變數時,莽撞的訪問會帶來問題。

我們來看一下下面程式碼

class Animal {

    public String name;
    public int age;

    int num = 3;
    /*public void eat() {
        System.out.println(name + ' ' + age + ' ' + "吃飯");
    }*/
}

class Cat extends Animal {
    int num = 4;

    public void move() {
        System.out.println(num);
    }
}

public class AnimalText {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.name = "Tom";
        cat.age = 2;
        //cat.eat();
        cat.move();
    }
}
我們在子類中呼叫和父類相同名字的成員變數時可以發現,父類的成員變數被子類修飾掉了。因此想要分別呼叫父類和子類的成員變數需要進行以下修改
class Animal {

    public String name;
    public int age;

    int num = 3;
    /*public void eat() {
        System.out.println(name + ' ' + age + ' ' + "吃飯");
    }*/
}

class Cat extends Animal {
    int num = 4;

    public void move() {
        System.out.println(this.num);
        System.out.println(super.num);
    }
}

public class AnimalText {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.name = "Tom";
        cat.age = 2;
        //cat.eat();
        cat.move();
    }
}

子類中呼叫本類的成員變數使用this.關鍵字;子類中呼叫父類成員變數使用super.關鍵字。這麼使用的前提是子類和父類的成員變數名相同。

> 3.成員方法重名情況
當子類的成員方法與父類的成員方法重名時,這個時候就是一種特殊的情況,方法重寫。
class Animal {

    public String name;
    public int age;

    int num = 3;
    public void eat() {
        System.out.println(name + ' ' + age + ' ' + "吃飯");
    }
}

class Cat extends Animal {
    int num = 4;

    /*public void move() {
        System.out.println(this.num);
        System.out.println(super.num);
    }*/

    public void eat() {
        System.out.println("eat方法重寫了!");
    }
}

public class AnimalText {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.name = "Tom";
        cat.age = 2;
        cat.eat();
        //cat.move();
    }
}

我們發現子類的方法實際上覆蓋了父類的方法。
在父子類的繼承關係中,建立子類物件,呼叫成員方法,建立的物件是誰的就優先用誰,找不到向上查詢(即父類),不會再向下一級查詢。
如果相對父類的方法進行更新,可以採用覆蓋重寫。

注意事項:

1.必須保證方法名與引數列表一致,在方法前使用@Override註解檢測覆蓋重寫是否正確,但卻不是必須的。
2.子類方法的返回值型別必須【小於等於】父類方法的返回值型別
3.子類方法的許可權必須【大於等於】父類方法的許可權修飾符。(public > protected > 預設 > private);預設是什麼都不寫,留空。
4.靜態方法不能被重寫;私有等在子類中不可見的方法不能被重寫;final方法不能被重寫

我覺得這裡需要說明一下重寫與過載的區別:

1.重寫:方法名一樣,引數列表一樣,且是父子類關係
2.過載:方法名一樣,引數列表不一樣,在同一個類內

構造方法

1.由於構造方法名與類名一致,故子類無法繼承父類的構造方法
2.但是在子類的初始化過程中,會先執行父類的初始化。子類的構造方法中會有一個super();表示呼叫了父類的構造方法。
3.如果父類沒有無參構造,同樣在子類的構造方法中用super(...,...)呼叫父類的構造器。

十、final

1. 概述

最終的,不可更改的
final類:太監類,沒有子類
final方法:表示這個方法不能被子類重寫
final變數:表示此變數值不可更改,即常量

2. final變數

final可以修飾成員變數和區域性變數;
修飾成員變數時,沒有set方法,並且必須有顯示賦值語句,不能使用成員變數預設值;
final修飾的變數名,所有字母都大寫

3. final修飾符、finally修飾符與finallize()

在異常處理時提供finally塊來執行任何清除操作。如果丟擲一個異常,那麼相匹配的catch字句就會執行,然後控制就會進入finally塊中。

finalize是方法名。Java技術允許使用finallize()方法在垃圾收集器將物件從記憶體中清除出去之前做必要的清理工作。這個方法使用垃圾收集器在確定這個物件沒有被引用時對這個物件呼叫的。總之,finalize()方法是在垃圾收集器刪除物件之前對這個物件呼叫的。

十一、類初始化

1. 靜態變數在類中的初始化

類被載入記憶體後,會在方法區建立一個class物件來儲存該類的資訊。此時會在方法區為靜態變數開闢記憶體,然後為類變數進行初始化。實際上,類初始化的過程時在呼叫一個()方法,而這個方法是編譯器自動生成的。編譯器會將如下兩部分的所有程式碼,按順序合併到類初始化()方法體中。

(1)靜態成員變數的顯示賦值語句
(2)靜態程式碼塊中的語句
(3)靜態程式碼塊對靜態變數的二次賦值會覆蓋之前的靜態變數宣告的初始值。

public class Test{
    public static void main(String[] args){
    	Father.test();
    }
}
class Father{
	private static int a = getNumber();
	static{
		System.out.println("Father(1)");
	}
	private static int b = getNumber();
	static{
		System.out.println("Father(2)");
	}
	
	public static int getNumber(){
		System.out.println("getNumber()");
		return 1;
	}
	
	public static void test(){
		System.out.println("Father:test()");
	}
}
執行結果:
getNumber()
Father(1)
getNumber()
Father(2)
Father:test()

附圖:單個類的靜態成員變數的初始化記憶體模型

原文連結:https://blog.csdn.net/weixin_41043145/article/details/95868669

public class Test{
    public static void main(String[] args){
    	Son.test();
        System.out.println("-----------------------------");
        Son.test();
    }
}
class Father{
	private static int a = getNumber();
	static{
		System.out.println("Father(1)");
	}
	private static int b = getNumber();
	static{
		System.out.println("Father(2)");
	}
	
	public static int getNumber(){
		System.out.println("Father:getNumber()");
		return 1;
	}
}
class Son extends Father{
	private static int a = getNumber();
	static{
		System.out.println("Son(1)");
	}
	private static int b = getNumber();
	static{
		System.out.println("Son(2)");
	}
	
	public static int getNumber(){
		System.out.println("Son:getNumber()");
		return 1;
	}
	
	public static void test(){
		System.out.println("Son:test()");
	}	
}
執行結果:
Father:getNumber()
Father(1)
Father:getNumber()
Father(2)
Son:getNumber()
Son(1)
Son:getNumber()
Son(2)
Son:test()
-----------------------------
Son:test()

2. 例項初始化

例項初始化方法的方法體,由四部分構成:
(1)super()或super(實參列表) 這裡選擇哪個,看原來構造器首行是哪句,如果沒寫super()或super(實參列表),預設就是super()
(2)非靜態例項變數的顯示賦值語句
(3)非靜態程式碼塊
(4)對應構造器中的程式碼

例項化的特點:
(1)只有建立物件時,才會執行對應的初始化
(2)呼叫哪個構造器,就是指定它對應的例項初始化方法
(3)建立子類物件時,父類對應的例項初始化會被先執行,執行父類哪個例項初始化方法,看用super()還是super(實參列表)

public class Test{
    public static void main(String[] args){
    	Son s1 = new Son();
    	System.out.println("----------------------------");
    	Son s2 = new Son();
    }
}
class Father{
	static{
		System.out.println("Father:static");
	}
	{
		System.out.println("Father:not_static");
	}
	Father(){
		System.out.println("Father()無參構造");
	}
}
class Son extends Father{
	static{
		System.out.println("Son:static");
	}
	{
		System.out.println("Son:not_static");
	}
	Son(){
		System.out.println("Son()無參構造");
	}
}

執行結果:
Father:static
Son:static
Father:not_static
Father()無參構造
Son:not_static
Son()無參構造
----------------------------
Father:not_static
Father()無參構造
Son:not_static
Son()無參構造

3. 結論(類初始化與例項初始化)

(1)類初始化先於例項初始化
(2)類初始化只執行一次,在第一次被載入時
(3)例項初始化每次建立物件時都要執行

4. 構造器和非靜態程式碼塊

  從某種程度上,我們可以將非靜態程式碼塊看作是對構造器的補充,非靜態程式碼塊總是在構造器之前執行。與構造器不同的是,非靜態程式碼塊是一段固定的程式碼,不能接受任何資料。因此非靜態程式碼塊對同一個類的所有物件所進行的初始化處理完全相同。如果有一段初始化處理程式碼對所有物件完全相同,且無須接收任何引數,就可以把這段初始化處理程式碼提取到非靜態程式碼塊中。
  因此,非靜態程式碼塊中存放的是一段初始化處理程式碼對所有物件完全相同,且無需接受任何引數。通過把多個構造器中相同程式碼提取到非靜態程式碼塊中定義,能更好地提高初始程式碼的複用,提高整個應用的可維護性。

this/super問題

(1)當方法中訪問變數時,如果沒有this.,super.,那麼都是遵循就近原則,找最近宣告的。

public class Test{
    public static void main(String[] args){
    	Son s = new Son();
    	System.out.println(s.getNum());//10
    	
    	Daughter d = new Daughter();
    	System.out.println(d.getNum());//20
    }
}
class Father{
	protected int num = 10;
	public int getNum(){
		return num;
	}
}
class Son extends Father{
	private int num = 20;
}
class Daughter extends Father{
	private int num = 20;
	public int getNum(){
		return num;
	}
}

(2)找方法時

沒有加this.和super.的,預設就是加了this.的。
如果加了this.,先從當前物件的類中找,如果沒有自動從父類中查詢。
如果加了super.,直接從父類開始找

public class Test{
    public static void main(String[] args){
    	Son s = new Son();
    	s.test();
    	
    	Daughter d = new Daughter();
    	d.test();
    }
}
class Father{
	protected int num = 10;
	public int getNum(){
		return num;
	}
	
}
class Son extends Father{
	private int num = 20;
	public void test(){
		System.out.println(getNum());//10
		System.out.println(this.getNum());//10
		System.out.println(super.getNum());//10
	}
}
class Daughter extends Father{
	private int num = 20;
	public int getNum(){
		return num;
	}
	public void test(){
		System.out.println(getNum());//20
		System.out.println(this.getNum());//20
		System.out.println(super.getNum());//10
	}
}