零基礎學Java第六節(物件導向二)

程式設計攻略 發表於 2022-05-20
Java 物件導向

本篇文章是《零基礎學Java》專欄的第六篇文章,文章採用通俗易懂的文字、圖示及程式碼實戰,從零基礎開始帶大家走上高薪之路!

本文章首發於公眾號【程式設計攻略】

繼承

建立一個Person類

我們建立一個用於描述的類。我們怎麼抽象出一個這個類呢?我們以不同的角度做抽象,得到的屬性和行為都會有些區別。這裡,我們主要從的社會屬性來抽象。為了表示性別,我們先頂一個列舉型別,該列舉型別中有兩個值,用於表示,程式碼如下:

public enum Sex {male,female}

上面的程式碼寫入單獨的一個原始碼檔案中:Sex.java,因為上面的列舉型別是需要公開使用的,所以,也需要定義為public,因此,必須將之放在獨立的原始檔中 。

再定義Person這個類,程式碼如下:

public class Person{
	private String name;//姓名
	private Sex sex;//性別
	private Date birthday;//出生日期
	private String ID;//身份證號

	//下面是一系列上述私有成員變數的setter和getter
	public void setName(String name){
		this.name = name;
	}

	public String getName(){
		return name;
	}

	public void setSex(Sex sex){
		this.sex = sex;
	}

	public Sex getSex(){
		return sex;
	}

	public void setBirthday(Date date){
		birthday = date;
	}

	public Date getBirthday(){
		return birthday;
	}

	public void setID(String ID){
		this.ID = ID;
	}

	public String getID(){
		return ID;
	}

	public Person(String name, Sex sex, Date birthday, String ID){
		this.name = name;
		this.sex = sex;
		this.birthday = birthday;
		this.ID = ID;
	}
}

我們編譯上面的Person.java,它所涉及到的相關的類也會一併編譯完成,如:Sex.java檔案。

定義學生子類

如果我們考慮一個班級的組成,大致有三種類別的人員,學生教師輔導員,我們依次進行定義。這三類都具有所具有的屬性,因此,我們沒必要在三個類中重新定義那些公共屬性,我們只需繼承Person類即可。當然,這些類一定也具有不同於普通的屬性和行為。先定義學生類。

public class Student extends Person {
	private String studentID;//學號
	private Date rollInDate;//入學時間

	public void setStudentID(String studentID){
		this.studentID = studentID;
	}

	public String getStudentID(){
		return studentID;
	}

	public void setRollInDate(Date rollInDate){
		this.rollInDate = rollInDate;
	}

	public Date getRollInDate(){
		return rollInDate;
	}

	public Student(String name, 
					Sex sex, 
					Date birthday, 
					String ID, 
					String studentID, 
					Date rollInDate){
		super(name,sex,birthday,ID);//對父類的構造方法的呼叫
		//雖然Student能夠繼承父類中的私有成員,
		//但是卻不能直接像使用本類中的成員變數一樣直接呼叫,
		//因此,下句是不能呼叫的
		//this.name = name; 
		this.studentID = studentID; //本類的成員變數,當然可以這麼寫
		this.rollInDate = rollInDate;
	}
}

Student類繼承了Person中所有的成員,包括私有的,但是對於子類雖然擁有繼承自父類的成員,卻不能直接使用。子類可以直接使用的繼承來的成員有public和protected修飾的,對於無修飾符的成員變數,如果子類同父類不在同一個包,也是不能使用的。示例如下:

class A{
	private int x = 0;
	protected int y = 1;
	public int z = 2;
	int m = 3;

  	A(){
    	System.out.println("在父類裡");
  	}

  	A(String s){
    	System.out.println("在父類裡 "+s);
  	}
}

public class B extends A{
//在此處加上int x;會怎麼樣呢?
  	B(){
    	System.out.println("在子類裡");
  	}
  	B(String s){
    	super(s);
    	System.out.println("在子類裡 "+s);

  	}
 	public static void main(String s[]){
    	B b=new B("對嗎?");
    	System.out.println(b.x);//不對哦
    	System.out.println(b.y);
    	System.out.println(b.z);
    	System.out.println(b.m);
  	}
}

繼承具有以下特徵:

  • 子類只能繼承自一個父類,它繼承父類中的所有成員,甚至包括父類中private所修飾的成員變數或方法。子類中可以定義新的成員變數和成員方法,也可以對父類中的成員變數及成員方法進行重新定義,這種對方法的重新定義稱為複寫(override)

  • 如果改變了父類中某些功能,而這些功能在子類中未進行復寫,那麼修改父類的功能改變會影響到子類。

  • 父類具有所有繼承自它的子類的共同的特徵和行為。

繼承最基本的作用:程式碼重用。 繼承最重要的作用:方法可以複寫。

super的使用

我們在Student的構造方法中使用了supper(name,sex,birthday,ID);,這是對父類的構造方法的呼叫。對於繼承關係的類,子類的構造方法總是會呼叫父類的構造方法:如果沒有顯式呼叫,系統會自動呼叫父類的無參構造方法,但是一旦顯式呼叫,系統就不會再呼叫父類中的無參構造方法。

class A{
	A(){
		System.out.println("in A()");	
	}
}

class B extends A{
	B(){
		System.out.println("in B()");	
	}
}

class C extends B{
	C(){
		System.out.println("in C()");	
	}
}

class TestExtends{
	public static void main(String[] args){
		C c=new C();
	}
}

super的使用同this相似,但是它們兩者的本質是不同的,this是引用,而super不是,super指向的不是父類物件,它代表當前子類物件中繼承自父類的屬性和行為。如圖:

零基礎學Java第六節(物件導向二)

什麼時候使用super呢?當子類和父類中具有同名的成員時,例如,子類和父類中都有name這個屬性,如果要在子類物件中訪問繼承自父類中的name屬性,就需要使用 super進行區分。

super可以用在什麼地方?

  • 第一:super和this一樣可以用在非靜態方法中,不能用在靜態方法中。
  • 第二:super可以用在構造方法中。一個構造方法第一行如果沒有this(...);,也沒有顯式的去呼叫super(...);,系統會預設呼叫super(),因此,如果父類中沒有預設構造方法,則會出錯(大家如果註釋掉上面程式碼中的super,測試一下);
  • 注意:super(...);的呼叫只能放在構造方法的第一行。因此,super(....)this(....)不能共存。super(...);只是呼叫了父類中的構造方法,並不會建立父類物件。

複寫(override)

複寫指子類重定義了父類中的同名方法。什麼時候方法要進行復寫?如果父類中的方法已經無法滿足當前子類的業務需求,需要將父類中的方法功能進行重新進行定義。子類如果複寫父類中的方法之後,子類物件一定呼叫的是複寫之後的方法。

發生方法複寫的條件:

  1. 發生在具有繼承關係的兩個類之間
  2. 複寫發生在繼承關係中,必須具有相同的方法名,相同的返回值型別,相同的引數列表
  3. 複寫後的方法不能比被複寫的方法擁有縮小的訪問許可權。
  4. 複寫後的方法所丟擲的異常必須和被覆蓋方法的所丟擲的異常一致,或者是其子類;
  5. 私有的方法不能被複寫,但可以在子類定義和父類中同名的方法(但不稱之為複寫)
  6. 構造方法無法被複寫。因為子類同父類的構造方法不同。
  7. 複寫指的是成員方法,和成員變數無關。
class A{
  	private int x=0;
  	protected int y=1;
  	public int z=2;
  	int m=3;

  	void t1(){
    	System.out.println("在父類t1裡");
  	}
}

public class B extends A{
  	void t1(){
    	super.t1();
    	System.out.println("在子類t1裡");
  	}
  	public static void main(String s[]){
    	B b=new B();
    	b.t1();
    	System.out.println(b.m);
  	}
}

final關鍵字

final關鍵字可以出現在類名前、方法前、區域性變數前、成員變數前、形參前,它具有如下作用:

  • final修飾的類無法被繼承
  • final修飾的方法無法被複寫
  • final修飾的區域性變數,一旦賦值,不可再改變
  • final修飾形參,形參的值在方法體中也無法改變
  • final修飾的成員變數必須手動初始化,不能取它的預設值,一般和static聯用,如:
public static final int i = 100;
  • final修飾的引用型別,該引用不可再重新指向其他的java物件。但是final修飾的引用指向的物件的屬性是可以修改的,切不可混為一談。

定義教師子類

教師類除了它具有作為所具有的特徵之外,更多的是教師的教務特徵和功能,由此,我們先定義一個描述課程的類:Course,如下:

public class Course{
	private String courseName;
	private int courseId;

	public Course(int courseId, String courseName){
		this.courseId = courseId;
		this.courseName = courseName;
	}

	public void setCourseName(String courseName){
		this.courseName = courseName;
	}

	public String getCourseName(){
		return courseName;
	}

	public void setCourseId(int courseId){
		this.courseId = courseId;
	}

	//toString方法繼承自Object類
	public String toString(){
		System.out.println("課程編號:" + courseId + "\n課程名稱:" + courseName);
	}
}

我們定義教師類:Teacher如下:

public class Teacher extends Person {
	private String teacherID;//工號
	private Date rollInDate;//入職時間
	private Course[] teachingCourse; //教授的課程

	public void setTeacherID(String studentID){
		this.teacherID = teacherID;
	}

	public String getTeacherID(){
		return teacherID;
	}

	public void setRollInDate(Date rollInDate){
		this.rollInDate = rollInDate;
	}

	public Date getRollInDate(){
		return rollInDate;
	}

	public void setTeachingCourse(Course[] teachingCourse){
		this.teachingCourse = teachingCourse;
	}

	public Course[] getTeachingCourse(){
		return teachingCourse;
	}

	public Teacher(String name, 
					Sex sex, 
					Date birthday, 
					String ID, 
					String teacherID, 
					Date rollInDate,
					Course[] teachingCourse){
		super(name,sex,birthday,ID);//對父類的構造方法的呼叫
		//雖然Student能夠繼承父類中的私有成員,
		//但是卻不能直接像使用本類中的成員變數一樣直接呼叫,
		//因此,下句是不能呼叫的
		//this.name = name; 
		this.teacherID = teacherID; //本類的成員變數,當然可以這麼寫
		this.rollInDate = rollInDate;
		this.teachingCourse = teachingCourse;
	}
}

定義輔導員

輔導員是教師,但輔導員在教師的功能基礎上又多了一些管理功能,所以輔導員類應該繼承自教師類,程式碼如下:

public class Counsellor extends Teacher{
	
}

轉型(cast)

對於上面所定義的類,我們可以說:a student is a persona teacher is a persona counsellor is a teachera counsellor is a person。從這些說法,我們可以看出來一個子類物件的型別是可以看作父類型別的。因此,下面的語句是合法的:

Person p = new Student();

這種情況,我們稱之為向上轉型(upcast),這有點類似於基本型別中的自動型別轉換。

有時候,我們也需要把某個父類物件當作一個子類物件使用,我們不鼓勵這麼用,因為這是不自然的,就像我們不能說:a person is a counsellor一樣,如果非要這麼做,我們必須像基本型別的強制型別轉換一樣使用強制轉換,我們稱之為向下轉型(downcast),如下例:

Teacher teacher = new Teacher();
Counsellor counsellor = (Counsellor)teacher;

上例中,我們使用了預設構造方法,但是,我們並沒有定義相關類的預設構造方法,我們只是為了說明轉型這個概念,大家為了使得上例合法,要麼使用帶參的構造方法,要麼,修正前面定義的相關類。

轉型示例:

class T1{
    int i = 1;
}

class T2 extends T1{
    int i = 2;
}

public class TUpcast{
    public static void main(String[] args){
        T2 t2 = new T2();
        System.out.println("t2.i=" + t2.i +"upcast t2.i=" + ((T1)t2).i);
        //這說明子類中可以重新定義父類中的成員變數,同名的可以隱藏父類中的同名變數,也就是說他們是不同的變數
    }
}

定義班級

一個班級的基本組成,有若干學生、若干教師、一個輔導員,我們定義班級類如下:

public class Classes{
	public String className;
	public int classId;
	public Student[] students;
	public Teacher[] teachers;
	public Counsellor counsellor;

	public Classes(int classId, 
					String className, 
					Student[] students, 
					Teacher[] teachers, 
					Counsellor counsellor){
		this.classId = classId;
		this.className = className;
		this.students = students;
		this.teachers = teachers;
		this.counsellor = counsellor;
	}
}

一旦某個類建立及測試後,它便具有了某種有用的功能,我們可以建立該類的物件,通過該物件,使用定義好的功能。除此之外,我們還可以通過使用類的繼承機制進行復用,也可以把物件放入一個新的類中,稱為成員物件,這種新類由其它類物件組成的情況,稱為組合(composition)。如上面的Classes類就是由已有的類成員組合而成。

多型

我們由下面的例子來解釋什麼是多型,如下:

class Animal{
	void eat(){

	}
}

class Cat extends Animal{
	void eat(){
		System.out.println("魚好吃!");
	}

	void chasingMouse(){
		System.out.println("追啊追,追的好開心!");
	}
}

class Rabbit extends Animal{
	void eat(){
		System.out.println("蘿蔔比魚好吃!");
	}

	void run(){
		System.out.println("我比烏龜跑得快!");
	}
}

class Test{

	public static void callEat(Animal animal){
		animal.eat();
	}

	public static void main(String[] args) {
		Cat cat = new Cat();
		Rabbit rabbit = new Rabbit();

		//下面的兩個calEat方法在執行的時候,會根據傳入的實參型別,選擇執行不同類中的eat
		//這種情況就是多型
		callEat(cat);
		callEat(rabbit);

		//animal變數存放的為其子類的物件,執行子類物件中的eat方法
		Animal animal = cat;
		animal.eat();

		animal = rabbit;
		animal.eat();
		//animal.chasingMouse();
		((Rabbit)animal).run(); //原本animal引用的就是Rabbit物件,downcast是可以的
		((Cat)animal).chasingMouse();//原本animal引用的是Rabbit,rabbit是不能轉換為cat的
		
	}
}

多型性是通過過載和複寫及upcast來體現的。使用多型可以使程式碼之間的耦合度降低。專案的擴充套件能力增強。