Java作爲一門完全面向對象的語言,筆者感到其威力體現在面向對象的概念大大抽象了程序繁瑣的細節,提取了程序間的共性和聯繫,使得開發者可以投入更多精力在設計程序而不是寫代碼上。由於Java面向對象的內容過多,這裏只列舉一些容易混淆和出錯的地方。畢竟寫技術日記的目的是爲了利於學習,鬍子眉毛一把抓就無法提高效率。
1. 多態
1.1 重載的條件
方法的重載必須滿足以下全部條件:
方法名相同;
參數列表不同;
返回類型可以相同也可以不同;
訪問權限可以相同也可以不同。
其中參數列表不同可以是參數的類型不同,個數不同或者順序不同。因爲方法名和參數列表共同構成了函數簽名(signature),執行某個方法時JVM看到的只是函數簽名。
正確的重載:
public class Test
{
public static void main(String[] args)
{
Add a = new Add();
System.out.println(a.add(1, 1));
System.out.println(a.add(1, 1, 1));
}
}
class Add
{
public int add(int x, int y)
{
return x + y;
}
public int add(int x, int y, int z)
{
return x + y + z;
}
}
同樣函數簽名的方法是不允許同時出現在一個類中的,否則JVM無法決定執行哪個方法,比如:錯誤的重載:
class Add
{
public int add(int x, int y)
{
return x + y;
}
public char add(int x, int y) //報錯,已在Add中定義add(int, int)
{
return (char)(x + y);
}
}
正確的重載:class Add
{
public int add(int x, int y)
{
return x + y;
}
public char add(int x, int y, int z)
{
return (char)(x + y + z);
}
}
兩個add方法參數列表不同,返回值類型也可以不同,扔屬於重載。
1.2 重寫的條件
子類重寫方法的名稱,參數簽名和返回類型必須與父類原方法的名稱,參數簽名和返回類型一致;
重寫方法的訪問權限大於等於原方法;
重寫方法的異常拋出範圍小於等於原方法;
重寫只能發生在子類和父類之間,重寫方法和原方法必須分別存在於子類和父類中;
重寫只是針對方法,屬性不能被重寫,屬性跟引用變量的聲明類型綁定,
靜態方法不能被重寫,因爲靜態方法跟其被聲明的類綁定。
正確的重寫:
public class Test
{
public static void main(String[] args)
{
Child c = new Child();
c.display(); //輸出:Child: Child, 調用的是子類的重寫方法
}
}
class Parent
{
void display()
{
System.out.println(this.getClass().getName() + ": " + "Parent");
}
}
class Child extends Parent
{
void display()
{
System.out.println(this.getClass().getName() + ": " + "Child");
}
}
1.3 動態綁定
public class Test
{
public static void main(String[] args)
{
//PP情況
Parent pp = new Parent(); //輸出:Parent: Parent
pp.display();
//PC情況
Parent pc = new Child(); //輸出:Child: Child
pc.display();
//CP情況
//Child cp = new Parent(); //報錯:不兼容的類型
//cp.display();
//CC情況
Child cc = new Child(); //輸出:Child: Child
cc.display();
}
}
class Parent
{
void display()
{
System.out.println(this.getClass().getName() + ": " + "Parent");
}
}
class Child extends Parent
{
void display()
{
System.out.println(this.getClass().getName() + ": " + "Child");
}
}
由上面例子可見,PP和CC情況下引用變量的聲明類型和指向的對象類型一致,則調用其聲明類中的方法。CP情況報錯,因爲屬於子類的聲明類型的引用若要被賦值爲屬於父類的對象類型,需要上轉換,而此處無法進行上轉換。比較複雜的是PC情況,這種情況體現了動態綁定的思想,我們來分析一下:Parent pc = new Child();
pc.display();
首先,編譯器根據引用的聲明類型(Parent)和方法名(display),搜索相應類(Parent)及其子類(Child)的“方法表”,找出所有名字爲display的方法。可能存在多個方法名爲display的方法,只是參數類型或數量不同。然後,根據函數簽名找出完全匹配的方法。因爲display方法被重寫,所以父子類中的display方法都符合要求。如果display方法的訪問權限爲private,或者訪問修飾符爲static或者final,則立刻可以明確這兩個display方法選哪一個執行(事實上有可能不會出現兩個重名方法),因爲private方法不能被繼承也就不能被重寫,不會出現重名的方法,static方法與類綁定按調用該方法的引用的聲明類型執行,final方法與private方法類似,無法繼承和重寫。經過這三步編譯階段結束。如果在這三步中能確定父子類中重名的display方法(一個是父類的,一個是被子類重寫的)哪一個應當被引用變量調用,則稱其爲靜態綁定。如果不能確定,則需要在運行階段進行第四步:根據引用變量指向的對象類型來確定調用哪個方法。這裏pc引用變量指向的對象類型是Child類型,所以調用Child類型中重寫後的display方法。
2. 對象
2.1 生命週期
離開作用域:
{
Person p1 = new Person(); //離開作用域時,p1失效,Person對象成爲垃圾
}
引用變量指向null:{
Person p1 = new Person();
p1 = null; //p1失效,Person對象成爲垃圾
}
引用變量的賦值可以延長生命週期:{
Person p1 = new Person();
Person p2 = p1; //p1, p2兩個引用變量指向同一個Person對象
p1 = null; //p1失效,但p2仍指向Person對象,直到超出作用域後成爲垃圾
}
2.2 比較
public class Test
{
public static void main(String[] args)
{
String str1 = new String("abc");
String str2 = new String("abc");
String str3 = str1;
System.out.println(str1 == str2); //false, 兩個引用分別指向堆內存中的兩個對象
System.out.println(str1 == str3); //true, 兩個引用的值相等
}
}
2.3 構造方法
特別需要注意的是,如果一個類中輸入了有參構造方法,則其默認生成的無參構造方法不再生成。如果父類使用了有參構造方法,則子類在生成對象時必須用super關鍵字調用父類的有參構造方法,否則子類對象無法生成,比如:
public class Test
{
public static void main(String[] args)
{
Parent p = new Parent(1);
System.out.println(p); //輸出1
Child c = new Child(2);
System.out.println(c); //輸出3, 2
}
}
class Parent
{
int x;
Parent(int x)
//輸入了有參構造方法,默認的無參構造方法不再生成,子類也不可能繼承
{
this.x = x;
}
@Override
public String toString()
{
return String.valueOf(x);
}
}
class Child extends Parent
{
int y;
Child(int y) //子類構造方法中調用父類構造方法
{
super(3); //super關鍵字調用父類的有參構造方法,給x賦值
this.y = y; //子類構造方法給子類獨有的y屬性賦值
}
@Override
public String toString()
{
return String.valueOf(x) + ", " + String.valueOf(y);
}
}
2.4 this關鍵字
區分重名的本類屬性和形參變量:
public class Test
{
public static void main(String[] args)
{
Student stu = new Student("Jack");
}
}
class Student
{
String name;
Student(String name)
{
this.name = name; //this.name代表本類屬性
}
}
通過this引用把當前的對象作爲一個參數傳遞給其他的方法,通常用於生成一個包含其它對象引用的對象:public class Test
{
public static void main(String[] args)
{
School sch = new School();
sch.addStudent();
}
}
class School
{
Student stu;
public void addStudent()
{
stu = new Student("Jack", this); //this將調用本addStudent方法的School對象的引用傳遞給新的Student對象
}
}
class Student
{
String name;
School sch;
Student(String name, School sch) //sch引用即是School類中的this
{
this.name = name;
this.sch = sch;
}
}
構造方法是在產生對象時被Java系統自動調用的,我們不能在程序中像其他調用其他方法一樣去調用構造方法。但是我們可以通過this在一個構造方法裏調用執行其他重載的構造方法:class Student
{
String name;
School sch;
Student(String name)
{
this.name = name;
}
Student(String name, School sch)
{
this(name); //this調用了前面的重載構造方法給name屬性賦了值,本構造方法給sch賦值
this.sch = sch;
}
}
3. 參數傳遞
3.1 基本類型變量的參數傳遞
方法的形式參數就相當於方法中的局部變量,方法調用結束時被釋放,並不會影響到主程序中同名的局部變量。 基本類型的變量作爲實參傳遞,並不能改變這個變量的值。例如:public class Test
{
public static void main(String[] args)
{
int x = 1; //x存在於main靜態方法的棧區中
change(x);
System.out.println(x);
}
public static void change(int x)
{
x = 2; //x存在於change靜態方法的棧區中,不會改變main方法中的x
}
}
String類型的傳遞規則與基本類型的傳遞規則一致。3.2 引用類型變量的參數傳遞
public class Test
{
public static void main(String[] args)
{
int[] arr = {1, 2}; //引用arr存在於main靜態方法棧區
change(arr);
for(int tmp : arr)
{
System.out.print(tmp + " "); //輸出1, 1
}
}
public static void change(int[] arr)
{
arr[1] = 1; //同一個引用arr
}
}
4. 內部類
4.1 定義
內部類可以直接訪問嵌套它的類的成員,包括private成員,但是嵌套類的成員卻不能被嵌套它的類直接訪問,比如:
public class Test
{
public static void main(String[] args)
{
Outer.Inner in = new Outer().new Inner(); //先生成外部類的實例,再生成內部類的實例
in.display(); //輸出outer
}
}
class Outer
{
String outer_str = "outer";
void display()
{
//System.out.println(Inner.inner_str); //報錯:無法在靜態上下文中引用非靜態變量
}
class Inner
{
String inner_str = "inner";
void display()
{
System.out.println(outer_str);
}
}
}
當一個類中的程序代碼要用到另外一個類的實例對象,而另外一個類中的程序代碼又要訪問第一個類中的成員,就應當將另外一個類做成第一個類的內部類。4.2 外部訪問內部類
public class Test
{
public static void main(String[] args)
{
Outer.Inner in = new Outer().new Inner();
System.out.println(in.inner_str); //輸出inner
}
}
class Outer
{
String outer_str = "outer";
class Inner
{
String inner_str = "inner";
void display()
{
System.out.println(outer_str);
}
}
}
4.3 匿名內部類
import java.awt.*;
import java.awt.event.*;
public class QFrame extends Frame
{
public QFrame()
{
this.setTitle("my application");
addWindowListener
(
new WindowAdapter() //WindowAdapter是個抽象類,匿名內部類繼承了這個抽象類
//匿名內部類開始
{ //可以通過new .....()後面緊跟的{}來判斷這是匿名內部類的開始
//重寫WindowAdapter的windowClosing方法
public void windowClosing(WindowEvent e)
{
dispose();
System.exit(0);
}
}
//匿名內部類結束
); //有分號
this.setBounds(10,10,200,200);
}
}
在使用匿名內部類時,有一些需要特別注意的限制:匿名內部類不能有構造方法。
匿名內部類不能定義任何靜態成員、方法和類。
匿名內部類不能是public,protected,private,static。
只能創建匿名內部類的一個實例。
一個匿名內部類一定是在new的後面,用其隱含實現一個接口或實現一個類。
因匿名內部類爲內部類,所以內部類的所有限制都對其生效。
內部類只能訪問外部類的靜態變量或靜態方法。
從上面的分析可以看出,匿名內部類就是繼承了某個抽象類或者接口的內部類的簡化寫法。