瞭解類和對象前,簡單提及面向對象程序設計。面向對象程序設計就是通過對象來進行程序設計,對象表示一個可以明確標識的實體。例如:一個人、一本書、一個學校或一臺電腦等等。每個對象都有自己獨特的標識、狀態和行爲。
對象的狀態(特徵或屬性,即實例變量),由該對象的數據域來表示。 例如:一個人可以具有名字、年齡、身高、體重、家庭地址等等屬性,這些就是“人這個對象的數據域”。
對象的行爲(對象執行的動作,即功能),由方法來定義。例如:定義getName()來獲取姓名, getHeight()獲取身高,setAddress(String addr)修改地址。
類和對象的關係
類是一種抽象的概念集合,是最基礎的組織單位,作爲對象的模板、合約或藍圖。
類是對象的類型,使用一個通用類可以定義同一類型的對象,類中定義對象的數據域是什麼以及方法是做什麼的。 對象是類的實例,一個類可以擁有多個實例,創建實例的過程叫做實例化。實例也稱爲對象,兩者說法一致。
用幾個案例來加深理解類和對象。
上面的幾個類定義了該類對象的數據域和方法, 但這些類中都沒有main()方法,所以無法運行。注意:擁有main()方法的類稱爲主類,是執行程序的入口。使用類中定義的數據域和方法需要創建該類的實例,然後用實例來調用。
public class TestCreateInstance {
public static void main(String[] args) {
//創建Fruit類的實例
Fruit f = new Fruit("香蕉", "甜味");
System.out.println(f.toString()); //顯示f實例中的水果信息
//創建Book類的實例
Book b = new Book("Java案例學習", "IT", 25.5, "A0001");
System.out.println(b.toString()); //顯示b實例中的書籍信息
//創建Person類的實例
Person p = new Person("張三", 1, 18, "中國北京");
System.out.println(p.toString());
//result
// 水果名:香蕉, 口味:甜味
// 書名:Java案例學習, 書的種類:IT, 單價:25.5, 書籍編號:A0001
// 名字:張三, 性別:1,年齡:18, 出生地:中國北京
}
}
這裏定義了帶有main()方法的TestCreateInstance類,用於測試其他三個類。在main方法中,一共創建了三個對象, 創建對象使用new操作符, new Class(parameter)表示調用該類中相應參數的構造方法。
例如 new Fruit("香蕉", "甜味") 會調用 Fruit(String name, String state) 這個構造方法來創建對象。
構造方法
構造方法在使用new操作符創建對象時被調用,作用就是用於初始化對象數據域。
構造方法相比於普通方法比較特殊的地方: 構造方法名和所在類的類名一致;無返回值(即void也沒有);只有創建對象時纔會被調用。 構造方法和普通方法一樣,也可以重載,根據不同的初始參數,來構造對象。
//只初始化名字和性別
public Person(String name, int sex) {
this.name = name;
this.sex = sex;
}
public Person(String name, int sex, int age, String birthplace) {
this(name, sex); //使用this()來調用類中的構造器
this.age = age;
this.birthplace = birthplace;
}
除了定義有參構造方法,也可以定義無參構造方法: public Person(){}。若一個類中沒有定義任何構造方法,那麼在類中會隱式存在一個方法體爲空的無參構造方法,也叫默認構造方法。如果顯式的定義構造方法,則默認構造方法失效。
爲什麼要有默認構造方法呢?這就要提到構造方法中的調用流程了,即“構造方法鏈”。 這裏涉及到繼承的相關知識,這裏不給出。
引用變量訪問對象和調用數據域和方法
對象是通過對象類型變量來訪問,該變量包含了對對象的引用。對象類型變量使用操作符(.)來 訪問對象數據和方法。
創建的對象會在內存中分配空間, 然後通過引用變量來訪問。 包含一個引用地址的變量就是引用變量, 即引用類型。 Java中,除了基本類型外,就是引用類型(對象)。
聲明對象類型變量的兩種形式
Fruit f ; //只聲明,未指向一個引用地址 Fruit f = null; //f指向一個空地址 //兩者基本無區別,null是引用類型的默認值
本質上來看,類是一種自定義類型,是一種引用類型,所以該類類型的變量可以引用該類的一個實例。
Fruit f = new Fruit("西瓜", "“甜味”) //表示創建一個Fruit對象,並返回該對象的引用,賦給Fruit類型的f變量。 變量f包含了一個Fruit對象的引用地址。 但通常情況下,直接稱變量f爲Fruit對象。
引用類型變量和基本類型變量的區別
每一個變量都代表一個存儲值的內存位置。 聲明變量時,就是告知編譯器該變量可以存儲什麼類型的值。對基本類型變量來說,對應內存所存儲的值就是基本類型值。而對於引用類型變量來說,對應內存所存儲的值是一個引用,指向對象在內存中的位置。
除了基本類型,就是引用類型,引用類型包含對象引用,可以將引用類型看作對象。
int a = 6; int b = a; //將a的實際值賦給b TestReferance tr = new TestReferance(); TestReferance t2 = tr; //將tr的引用賦給t2 , tr和t2指向同一對象 t2.a = 10; System.out.println(tr.a); // 10
所以,將引用類型變量賦值給另一個同類型引用變量,兩者會指向同一個對象,而不是獨立的對象。 如果想要指向一個具有同樣內容,但不是同一個對象,可以使用clone()方法。
【技巧】:如果不再需要某個對象時,也就是不引用該對象,可以將引用類型變量賦值null,表示引用爲空。 若創建的對象沒有被任何變量所引用,JVM會自動回收它所佔的空間。
TestReferance t1 = new TestReferance(); t1 = new TestReferance(); //t1指向一個新的對象。 t1原來指向的對象會被回收 //創建一個匿名對象,執行完構造方法後,就會被回收。 new TestReferance();
PS:關於NullPointerException異常,一般都是因爲操作的引用類型變量指向null,所以對引用類型變量操作時,最好先判斷一下是否爲null。
靜態與實例的區別
上面的類中定義的都是實例變量和實例方法,這些數據域和方法屬於類的某個特定實例,只有創建該類的實例後,纔可以訪問對象的數據域和方法。
靜態變量和方法屬於類本身,靜態變量被類中的所有對象共享,在靜態方法中不能直接訪問實例變量和調用實例方法。
public class Test { public int a = 5; public static int staB = 10; public Test(int a) { this.a = a; } public static void main(String[] args) { Test t1 = new Test(6); Test t2 = new Test(8); System.out.println(t1.a + " " + t2.a); // 6 8, 兩個對象互不相關 t1.staB = 15; //t1對象修改靜態變量 staB System.out.println(t2.staB); //影響t2對象 } }
public class Test { public int a = 5; //實例變量 public static int staB = 10; //靜態變量 public Test(int a) { this.a = a; } public static void staMethod() { System.out.println(a); //error, 不允許直接訪問實例變量 insMethod(); //不允許直接調用實例方法 //通過對象來調用 System.out.println(new Test(5).a); new Test(5).insMethod(); } //實例方法中,可直接訪問靜態變量和調用靜態方法 public void insMethod() { staMethod(); System.out.println(staB); } }
因爲靜態變量將變量值存儲在一個公共地址,被該類的所有對象共享,當某個對象對其修改時,會影響到其他對象。而實例的實例變量則是存儲在不同的內存位置中,不會相互影響。
訪問靜態變量和靜態方法時,可以不用創建對象,通過“類名.靜態變量/靜態方法”來訪問調用。 雖然能通過對象來訪問靜態變量和方法,但爲了可讀性,方便分辨靜態變量,應該通過類名來調用。
【技巧】:若想要某個數據被所有對象共享,就可以使用static修飾,例如常量,修飾常量使用public static final。
【技巧】:如果某個變量或方法依賴於類的某個實例,則應該定義成實例變量或實例方法。若某個變量或方法不依賴於類的某個實例,則應該定義成靜態變量和靜態方法。 例如Math類,只有靜態方法和靜態變量,禁止創建Math對象。
不可變對象和類
通過定不可變類來產生不可變對象,不可變對象的內容不能被改變。就像文件中的“只讀”概念。
通常情況下,創建一個對象後,該對象的內容可以允許之後修改。但有時候我們需要一個一旦創建,其內容就不能改變的對象。
定義不可變類的要素:
類中所有數據域都是私有的,並且沒有提供任何一個數據域的setXxx()方法。若數據域是可變的引用類型,不提供返回該引用類型變量的getXxx()方法。
public class Test { private int a = 5; private String str; //String是不可變對象,它的方法都是返回一個新的String對象 private Date date; public Test(int a, String str, Date date) { super(); this.a = a; this.str = str; this.date = date; } public static void main(String[] args) { Test t = new Test(5, "ABC", new Date()); System.out.println(t.toString()); //獲取引用類型的數據域,其中Date是可變的 String s = t.getStr(); Date d = t.getDate(); s.toLowerCase(); //可變數據域 d.setDate(541313); System.out.println(t.toString()); } public int getA() { return a; } public String getStr() { return str; }
從以上案例看出,如果數據域是一個可變的引用類型,那麼不要返回該數據域,不然對返回的引用變量進行操作,會導致對象內容改變。
this關鍵字的使用
關鍵字this表示當前對象,引用對象自身。 可以用於訪問實例的數據域, 尤其是實例變量和局部變量同名時,進行分辨。除此之外,可以在構造方法內部調用同一個類的其他構造方法。
以構造方法爲例
public int a; public String str; //不使用this來初始化構造方法 public TestThis(int a, String str) { //隱式存在於每個構造方法的第一行 super(); a = a; //指向同名局部變量形參a str = str; //指向同名局部變量形參str }
形參名和全局變量同名,但形參是局部變量,所以在方法中優先使用局部變量,這裏的賦值是賦值給形參了。解決這個問題,可以修改形參名,但形參名和要初始化的變量名不相等容易引起歧義。
所以使用this來引用實例變量,一舉兩得。
public int a; public int b; public int c; //構造方法1 public TestThis(int a, int b) { this.a = a; this.b = b; } //構造方法2 public TestThis(int a, int b, int c) { this(a, b); //調用構造方法1來簡化初始化操作 this.c = c; }
引用參數傳遞給方法
方法是一種功能集合,表明可以做什麼,封裝了實現功能的代碼,要實現某個功能只需要調用相關方法即可,無需關注功能實現細節。大部分方法都需要傳遞參數來實現相關功能,但是傳遞基本類型和傳遞引用類型有什麼區別呢?
給方法傳遞一個對象參數,實際上是將對象的引用傳遞給方法。
public class TestThis { public static void main(String[] args) { int[] arr = {1, 2}; swap(arr[0], arr[1]); System.out.println(Arrays.toString(arr)); //[1, 2] swap(arr); System.out.println(Arrays.toString(arr)); //[2, 1] } public static void swap(int a, int b) { int temp = a; a = b; b = temp; } public static void swap(int[] a) { int temp = a[0]; a[0] = a[1]; a[1] = temp; } }
第一個swap()方法,接收基本類型參數,所以通過訪問下標獲取數組元素傳遞給形參,相當於賦一個實際值給形參,所以對形參交換,不會影響實際參數。
第二個swap()接收一個int類型數組, 數組也是一個對象, 所以此時形參a包含一個該數組的引用,在方法內部對a數組操作,會影響實際參數 arr。
上面方法中使用到了方法重載,方法重載就是使用同樣的名字,但根據方法簽名來定義多個方法。
方法重載只與方法簽名有關,和修飾符以及返回類型無關。被重載的方法必須有不同的參數列表或者參數類型不同。
參數列表不同必然重載,若參數列表相同,但類型相近時,會引發匹配歧義,因爲類型不明確。
//因爲double類型可以匹配int類型,所以會導致編譯器匹配歧義 public static void sum(int a, double b) { } public static void sum(double a, int b) { }