【第8天】Java方法過載、方法重寫(覆蓋)、構造方法及引數傳值
1 方法過載(overload)
- 方法過載的作用?
同時滿足使用者的不同需求。 同一個方法,使用者可以傳入不同的值進行操作(比如學信網按照不同的證件查資訊即一個查詢方法,傳不同型別的引數)。
-
方法過載的條件?
- 必須發生在同一個類體裡面
- 方法名需要完全一致(大小寫不同也不行,Java區分大小寫)
- 方法的引數不同(包括引數列表型別/個數/順序不同。因為引數名不被儲存在.class檔案裡,所以如果引數列表中只是引數名不同,其他都相同
-
綜上,方法過載對修飾符(訪問許可權、靜態) 和 返回型別沒有要求,只與引數型別個數順序相關。
-
如果一個方法引數列表可以傳入int,傳入short、byte、char、int都行,不能傳入比int更大的資料型別。如果傳入的引數沒有匹配資料型別的方法,從就近(向更大的範圍)的方法引數列表匹配。
-
例題
//1 下面哪些方法滿足方法過載?
//1245
//1
public static void test(){
System.out.println(1);
}
//2
public int test(int x){
return 2;
}
//3
public static void test1(int x){
System.out.println(3);
}
//4
public static String test(String x){
return "4";
}
//5
public static void test(double x,int y){
System.out.println(5);
}
//2 問下面的程式碼______和_____滿足方法過載?
// B類從A類繼承得到的test方法 和 B類自己的test方法 滿足方法過載
class A{
public void test(){
System.out.println("A類的test方法");
}
}
class B extends A{
//繼承而來的test()
public void test(int x){
System.out.println("B類的test方法");
}
}
2 方法重寫(覆蓋)(override)
-
方法重寫(覆蓋)的作用
可將方法重寫(覆蓋)類比為《進化論》。Sun公司認為子類重新實現的方法應該更加優秀,且更加優秀的方法應該給更多的人使用,利於Java的良性發展。 -
綜上,可以總結對重寫時方法的一些要求:
public | void test() | throws Exception |
---|---|---|
腦袋 | 軀幹 | 尾巴 |
腦袋:子類重寫時的訪問許可權修飾符一定要比父類的訪問許可權修飾符更開放。修飾為靜態的方法不允許被重寫。
軀幹:JDK5.0之前時“返回值+方法名”必須與父類統一。JDK5.0開始,允許協變返回型別,即允許子類使用自己的類作為返回型別(例題1)。無論什麼版本,引數列表一定要相同(子父類引數列表中引數的型別有繼承關係也不行)。
尾巴:異常處理部分,重寫的異常丟擲的範圍需要比父類拋的更小(如果父類是Exception這樣比較大的異常類,子類丟擲幾個小一些異常也可以),最好不拋。
-
方法重寫(覆蓋)有哪些條件
- 必須發生在有繼承關係的兩個類中的子類(子類在繼承得到父類的某個方法後,覺得父類的實現不好,於是在子類裡重新實現一遍)
- 方法名需要完全一致(大小寫不同也不行,Java區分大小寫)
- JDK5.0之前的版本返回型別必須與父類一致,5.0以後支援協變返回型別
- 子類的訪問許可權修飾符必須大於父類
- 父類若有異常丟擲,子類必須更少或者沒有
-
如果想在重寫的方法中使用父類的公共方法和變數,可以使用 “super.” 來呼叫這些方法和變數。
-
父類的私有屬性、構造方法子類都拿不到。
-
另外從JDK5.0開始,方法重寫時,可以在子類要重寫的方法上面加@Override(註解),表示下面的方法一定要重寫父類的某個方法。(註釋是給人看的,註解是給機器看的)
-
子類中,只有方法存在重寫覆蓋,變數不存在覆蓋,有幾個就存幾個,重名也可以。區分他們的方式是如何呼叫。(例題2)
-
例題
//1.協變返回型別的使用
class Animal{
public Animal givingBirth(){
Animal aa = new Animal();
return aa;
}
}
class Dog extends Animal{
@Override
public Dog givingBirth(){
Dog dd = new Dog();
return dd;
}
}
class Cat extends Animal{
@Override
//jdk5.0開始,返回型別可以變成父類方法返回型別的子類型別
public Cat givingBirth(){
Cat cc = new Cat();
return cc;
}
}
//2.繼承的同名變數問題
class A{
String x;
}
class B extends A{
String x;
public Teacher(String x){
System.out.println(super.x);//輸出父類的x
System.out.println(this.x);//輸出子類的x
System.out.println(x);//輸出方法引數的x
}
}
使用協變返回型別時需要注意,返回型別需要使用子類的型別而非父類。
- 面試題:override和overload之間的區別(重點,注意要點對點答)
名稱 | 含義 | 發生的位置 | 對返回型別的要求 | 對引數要求 |
---|---|---|---|---|
overload | 方法過載 | 同一個類 | 沒有要求 | 引數不同(型別/個數/順序) |
override | 方法覆蓋(重寫) | 有繼承關係的兩個類 | JDK5.0之前必須相同,JDK5.0開始允許協變返回型別 | 引數相同 |
3 構造方法
構造方法是建立物件時呼叫的方法。所有類中都有構造方法。建立物件是一個複雜的過程,在建立的最後,底層才呼叫了構造方法,在這之前底層還呼叫了其他的方法。注意Java中沒有建構函式的概念,函式這個名詞應用於C++中。構造方法也可以過載。
-
構造方法的作用
構造方法語法的出現是為了方便程式設計師的開發,可以實現在建立物件的同時直接給屬性進行賦值。 -
既然有了構造方法,還需要屬性的setter方法嗎?
需要,構造方法只是初始化屬性,後期如果想修改這個引用中的屬性,還是需要使用set方法。 -
構造方法的特點
- 沒有任何返回型別
- 構造方法的名字必須與類名相同
-
注意,Java中只要是個類就一定有構造方法,即使沒有寫,系統也會提供一個預設的無參、空體的構造方法。
-
如果想在建立物件的同時直接給屬性賦值,需要自己寫構造方法。一旦寫出自己的構造方法,預設的構造方法將不再提供。一般在聲明瞭有參有體的構造方法後,作為習慣也要再重新宣告一個無參空體的預設構造方法。
-
關於構造方法的第一行(用於共享程式碼,提高程式碼重用性) ★
預設第一行呼叫super()方法,也可在第一行顯式呼叫具有引數的super()或有無參均可的this()。這些被呼叫的語句必須在第一行呼叫,也就是說他們不可以共存。如果第一行呼叫了this(),即不再向上遞到父類執行構造方法。 一定要注意如果子類方法中第一行沒有super()也沒有this(),一定要先新增出來看一下。-
super()
表示執行本構造方法之前,預設先去執行父類無參的構造方法。即在執行子類時從輩分最高的父類依次向下執行無參的構造方法(super()在構造方法中的遞迴遞向上,再歸著執行下來)。即使是程式設計師寫出的輩分最高的父類也預設呼叫這個方法,它的父類是Object基類。它的作用是直接引用父類構造方法中的內容,相當於把父類這個構造方法中程式碼複製到子類裡來,其實執行還是在子類中執行。
super()與super.的區別是:super()使用於構造方法首行,用於要執行本構造方法之前將父類某個構造方法(看引數列表傳的值)中的內容“複製”到子類本方法中執行,完成程式碼共享減少程式碼量,並非直接在父類中執行。super.用於呼叫父類的成員,即使方法或者變數被重寫,它依然呼叫的是父類的成員。
如果要在父類無參方法被自定義有參構造方法覆蓋的情況下要定義子類,首先需要
1 自行去父類提供無參構造方法
2 或者向子類super()裡傳引數,指定它找父類的哪一個有參構造方法
以這樣為前提,子類才可以順利建立,否則報錯。 -
this()
表示要執行本構造方法前,先執行本類其他的構造方法。具體執行本類的哪一個構造方法,看括號中的引數型別。在呼叫this()時要注意避免在構造方法中的遞迴呼叫,即在一個構造方法中呼叫另外一個構造方法,使方法之間呼叫形成遞迴。this()與this.的區別是:this()使用於構造方法首行,用於執行本構造方法之前,將本類某個構造方法(看引數列表傳的值)中的內容“複製”到本法中執行,完成程式碼共享減少程式碼量;this.用於呼叫本類的成員。
-
構造方法可以過載,不能重寫,因為重寫的前提是先繼承得到,但是父類的構造方法不能被子類繼承,所以不能覆蓋。
-
例題
-
//1 列印語句輸出
public class Test1{
public static void main(String[] args){
Demo d1 = new Demo();
/**
列印結果為1,,呼叫無參構造方法給成員變數str賦值
*/
System.out.println(d1.str);
Demo d2 = new Demo("Hello world.");
/**
列印結果為null,呼叫有參構造方法就近給區域性變數str賦值,
區域性變數在方法結束後消亡
*/
System.out.println(d2.str);
Sample ss = new Sample("Hello world.");
/**
列印結果為1,呼叫有參子類構造方法,因為沒有宣告super(),
所以預設呼叫無參的父類構造方法,給成員變數str賦值,
另外子類繼承了父類的str變數,得到輸出
*/
System.out.println(ss.str);
Sample1 ss = new Sample1("Hello world.");
/**
列印結果為null,傳入構造方法後super將字串傳入父類的有參構造,
但是有參構造為區域性變數,方法結束即消亡
子類構造方法中的str也是區域性變數,方法結束即消亡
*/
System.out.println(ss.str);
}
}
class Demo{
String str;//成員變數
public Demo(){
str = "1";//成員變數
}
public Demo(String str){
str = "2";//區域性變數,想變為成員變數,str前面加this.
}
}
class Sample extends Demo{
//雖然沒寫,但是子類依然繼承了str變數
public Sample(String str){
//預設執行父類的無參構造方法,賦值成員變數↓↓↓↓↓↓↓↓
//super(); ---> str = "1";
str = "OK";//區域性變數,想變為成員變數,str前面加this.
}
}
class Sample1 extends Demo{
//雖然沒寫,但是子類依然繼承了str變數
public Sample1(String str){
//顯式呼叫了str
super(str);//傳入str的變數,但是父類中的str為區域性變數,方法結束即消亡
str = "OK";//區域性變數,想變為成員變數,str前面加this.
}
}
所以在執行尤其是子父類均有的方法時,一定要注意如果沒有super()一定要自動補齊,另外尤其注意重名變數的作用域、是否消亡的問題。
4 引數傳值
Java中只有值傳遞,基本資料型別傳值,引用資料型別傳地址。
- 結合簡書大佬@androidjp的博文,Java的記憶體分為棧記憶體和堆記憶體。方法區可以理解為:主要存放靜態資料、全域性 static 資料、String字面值的常量池和其他常量。這塊記憶體在程式編譯時就已經分配好,並且在程式整個執行期間都存在。總的來說:堆和棧針對非靜態資料,而方法區針對靜態資料。棧與堆都是Java用來在RAM中存放資料的地方。與C++不同,Java自動管理棧和堆,程式設計師不能直接地設定棧或堆。
棧:
簡單理解:堆疊(stack)是作業系統在建立某個程序或者執行緒(在支援多執行緒的作業系統中是執行緒)為這個執行緒建立的儲存區域,該區域具有先進後出的特性。
特點:存取速度比堆要快,僅次於直接位於CPU中的暫存器。棧中的資料可以共享(意思是:棧中的資料可以被多個變數共同引用)。
缺點:存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。
相關存放物件:①一些基本型別的變數(int, short, long, byte, float, double, boolean, char)和物件控制代碼(例如:在函式中定義的一些基本型別的變數和物件的引用變數)。②方法的形參 直接在棧空間分配,當方法呼叫完成後從棧空間回收。
特殊:①方法的引用引數,在棧空間分配一個地址空間,並指向堆空間的物件區,當方法呼叫完成後從棧空間回收。②區域性變數new出來之後,在棧空間和堆空間中分配空間,當局部變數生命週期結束後,它的棧空間立刻被回收,它的堆空間等待GC回收。
棧區:
每個執行緒包含自己的一個棧區,棧中只儲存基本資料型別的物件和自定義物件的引用。
每個棧中的資料(基本型別和物件引用)都是私有的,其他棧不可訪問。
棧 = 基本型別變數區 + 執行環境上下文 + 操作指令區(存放操作指令)
堆:
簡單理解:每個Java應用都唯一對應一個JVM例項,每一個JVM例項唯一對應一個堆。應用程式在執行中所建立的所有類例項或者陣列都放在這個堆中,並由應用所有的執行緒共享。Java中分配堆記憶體是自動初始化的,Java中所有物件的儲存空間都是在堆中分配的,但這些物件的引用則是在棧中分配,也就是一般在建立一個物件時,堆和棧都會分配記憶體。
特點:可以動態地分配記憶體大小、比較靈活,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的資料。在堆中分配的記憶體,由Java虛擬機器的自動垃圾回收器來管理。
缺點:由於要在執行時動態分配記憶體,存取速度較慢。
主要存放:①由new建立的物件和陣列 ;②this
特殊:引用資料型別(需要用new來建立),既在棧空間分配一個地址空間,又在堆空間分配物件的類變數。
堆區:
儲存的全是物件,每個物件都包含一個與之對應的class資訊(我們常說的類型別,Clazz.getClass()等方式獲取),class目的是得到操作指令。
JVM只有一個堆區(heap)被所有執行緒共享,堆中不存放基本型別和物件引用,只存放物件本身。(這裡的‘物件’,就不包括基本資料型別)
方法區
又稱為‘靜態區’,和堆一樣,被所有的執行緒共享。
方法區包含所有的class和static變數。
-
對於存在於方法中的區域性變數(自動變數),當程式呼叫方法時,系統會為該方法建立一個方法棧,這些變數就放在方法棧中,當方法結束系統會釋放方法棧,這些變數隨著這個方法棧的銷燬而自動消亡,這是區域性變數只能在方法中有效的原因。
- 當變數為基本資料型別,其變數名和值儲存在方法棧。
- 當變數為引用資料型別,所宣告的引用放在方法棧,這個引用指向的物件儲存在堆記憶體。
-
類體中的成員變數(基本、引用資料型別)的儲存也遵循上面的儲存方式。與區域性變數不同的是,因在程式中,非靜態的成員變數依賴於物件存在,當宣告物件時,類中的成員變數載入進入記憶體並賦預設值。每個成員變數存在一個有“指向堆記憶體中對應物件地址的指標”類似作用的連線。當物件執行結束之後,類也跟著呼叫這個方法的生命週期消亡。
-
例題(結合每個程式下面的記憶體圖理解,其中上半部分左邊是棧記憶體表示引用,右邊是堆記憶體表示物件)
//1
public class Test1{
public static void main(String[] args){
int a = 30;
change(a);
System.out.println(a);//--->30
}
public static void change(int x){
//int x = a; //將引數列表在方法體內部補全
x = 50;
}
}
- 第一題的記憶體圖
//2
public class Test2{
public static void main(String[] args){
int a = 60;
int b = 90;
change(a,b);
System.out.println(a); //--->60
System.out.println(b); //--->90
}
public static void change(int x,int y){
//int x = a;
//int y = b;
//x y z是區域性變數
int z = x;
x = y;
y = z;
}
}
- 第二題的記憶體圖
//3
public class Test3{
public static void main(String[] args){
String a = new String("O");
String b = new String("K");
change(a,b);
System.out.println(a);//--->輸出O
System.out.println(b);//--->輸出K
}
public static void change(String x,String y){
//String x = a;
//String y = b;
//x y z 是區域性變數
String z = x;
x = y;
y = z;
}
}
- 第三題的記憶體圖
//4
public class Test4{
public static void main(String[] args){
Student s = new Student(30);
change(s);
System.out.println(s.age);//--->55
}
public static void change(Student stu){
//Student stu = s;
//stu是區域性變數
stu.age = 55;
}
}
class Student{
int age;
public Student(int age){
this.age = age;
}
}
- 第四題的記憶體圖(Step3在最後與方法棧一起消亡)
//5
public class Test5{
public static void main(String[] args){
Student s = new Student(30);
change(s);
System.out.println(s.age);//--->30
}
public static void change(Student stu){
//Student stu = s;
//stu是區域性變數,但是在這裡new了新的物件
stu = new Student(55);
}
}
class Student{
int age;
public Student(int age){
this.age = age;
}
}
- 第五題的記憶體圖(Step3在Step4之前消亡)
//6
public class Test6{
public static void main(String[] args){
TestForm ff = new TestForm();
ff.setId(2);
int x = 2;
change(x,ff);
System.out.println("x=" + x);
System.out.println("ff.getId():" + ff.getId());
}
private static void change(int id,TestForm form){
//int id = x;
//TestForm form = ff;
//id form是區域性變數
id = 4;
form.setId(4);
}
}
class TestForm{
private int id;
public void setId(int id){
this.id = id;
}
public int getId(){
return id;
}
}
- 第六題的記憶體圖(Step6最後消亡)
//7
public class Test7{
public static void main(String[] args){
//如果方法沒有宣告static,就要建立物件呼叫它
Test7 t = new Test7();
A a = new A();
a.age = 10;
t.test(a);
System.out.println("a的年齡:" + a.age);//--->a的年齡:20
}
private void test(A x){
//x是區域性變數
//A x = a;
x.age = 20;
System.out.println("x的年齡" + x.age);//--->x的年齡:20
}
}
class A{
int age;
}
- 第七題的記憶體圖
public class Test8{
public static void main(String[] args){
A x = new A(0);
add(x);
System.out.println(x.value);
}
public static void add(A a){
//A a = x;
//a val是區域性變數
int val = a.value;
val += 3;
a = new A(val);
}
}
class A{
int value;
public A(int value){
this.value = value;
}
}
- 第八題的記憶體圖(Step3在Step6時消亡)
//9 附:注意,當迴圈時對指定下標進行操作時,一定使用for而不能使用foreach
//將陣列中所有值賦為5
int[] data = new int[5];//0 0 0 0 0
//無法操作控制下標
for(int x : data){
x = 5;
}
//正確做法
for(int x = 0;x < data.length;x++){
data[x] = 5;
}