文章目錄
8、java中的類與對象
面向對象簡稱 OO(Object Oriented),20 世紀 80 年代以後,有了面向對象分析(OOA)、 面向對象設計(OOD)、面向對象程序設計(OOP)等新的系統開發方式模型的研究。
對 Java 語言來說,一切皆是對象。
把現實世界中的對象抽象地體現在編程世界中,一個對象代表了某個具體的操作。一個個對象最終組成了完整的程序設計,這些對象可以是獨立存在的,也可以是從別的對象繼承過來的。對象之間通過相互作用傳遞信息,實現程序開發。
對象的概念
Java 是面向對象的編程語言,對象就是面向對象程序設計的核心。所謂對象就是真實世界中的實體,對象與實體是一一對應的,也就是說現實世界中每一個實體都是一個對象,它是一種具體的概念。對象有以下特點:
- 對象具有屬性和行爲。
- 對象具有變化的狀態。
- 對象具有唯一性。
- 對象都是某個類別的實例。
一切皆爲對象,真實世界中的所有事物都可以視爲對象。
例如,在真實世界的學校裏,會有學生和老師等實體,學生有學號、姓名、所在班級等屬性(數據),學生還有學習、提問、喫飯和走路等操作。學生只是抽象的描述,這個抽象的描述稱爲“類”。在學校裏活動的是學生個體,即張同學、李同學等,這些具體的個體稱爲“對象”,“對象”也稱爲“實例”。
面向對象的三大核心特性
面向對象開發模式更有利於人們開拓思維,在具體的開發過程中便於程序的劃分,方便程序員分工合作,提高開發效率。面向對象程序設計有以下優點。
- 可重用性:代碼重複使用,減少代碼量,提高開發效率。下面介紹的面向對象的三大核心特性(繼承、封裝和多態)都圍繞這個核心。
- 可擴展性:指新的功能可以很容易地加入到系統中來,便於軟件的修改。
- 可管理性:能夠將功能與數據結合,方便管理。
該開發模式之所以使程序設計更加完善和強大,主要是因爲面向對象具有繼承、封裝和多態 3 個核心特性。
1) 繼承性
如同生活中的子女繼承父母擁有的所有財產,程序中的繼承性是指子類擁有父類的全部特徵和行爲,這是類之間的一種關係。Java 只支持單繼承。
例如定義一個語文老師類和數學老師類,如果不採用繼承方式,那麼兩個類中需要定義的屬性和方法如圖 1 所示。
從圖 1 能夠看出,語文老師類和數學老師類中的許多屬性和方法相同,這些相同的屬性和方法可以提取出來放在一個父類中,這個父類用於被語文老師類和數學老師類繼承。當然父類還可以繼承別的類,如圖 2 所示。
圖 2 父類繼承示例圖
總結圖 2 的繼承關係,可以用概括的樹形關係來表示,如圖 3 所示。
圖 3 類繼承示例圖
從圖 3 中可以看出,學校主要人員是一個大的類別,老師和學生是學校主要人員的兩個子類,而老師又可以分爲語文老師和數學老師兩個子類,學生也可以分爲班長和組長兩個子類。
使用這種層次形的分類方式,是爲了將多個類的通用屬性和方法提取出來,放在它們的父類中,然後只需要在子類中各自定義自己獨有的屬性和方法,並以繼承的形式在父類中獲取它們的通用屬性和方法即可。
C++ 支持多繼承,多繼承就是一個子類可有多個父類。例如,客輪是輪船也是交通工具,客輪的父類是輪船和交通工具。多繼承會引起很多衝突問題,因此現在很多面向對象的語言都不支持多繼承。Java 語言是單繼承的,即只能有一個父類,但 Java 可以實現多個接口(接口類似於類,但接口的成員沒有執行體。詳細瞭解可參考《Java接口》一節),可以防止多繼承所引起的衝突問題。(java通過接口可以實現多繼承)
2)封裝性
封裝是將代碼及其處理的數據綁定在一起的一種編程機制,該機制保證了程序和數據都不受外部干擾且不被誤用。封裝的目的在於保護信息,使用它的主要優點如下。
- 保護類中的信息,它可以阻止在外部定義的代碼隨意訪問內部代碼和數據。
- 隱藏細節信息,一些不需要程序員修改和使用的信息,比如取款機中的鍵盤,用戶只需要知道按哪個鍵實現什麼操作就可以,至於它內部是如何運行的,用戶不需要知道。
- 有助於建立各個系統之間的松耦合關係,提高系統的獨立性。當一個系統的實現方式發生變化時,只要它的接口不變,就不會影響其他系統的使用。例如 U 盤,不管裏面的存儲方式怎麼改變,只要 U 盤上的 USB 接口不變,就不會影響用戶的正常操作。
- 提高軟件的複用率,降低成本。每個系統都是一個相對獨立的整體,可以在不同的環境中得到使用。例如,一個 U 盤可以在多臺電腦上使用。
**Java 語言的基本封裝單位是類。**由於類的用途是封裝複雜性,所以類的內部有隱藏實現複雜性的機制。Java 提供了私有和公有的訪問模式,類的公有接口代表外部的用戶應該知道或可以知道的每件東西,私有的方法數據只能通過該類的成員代碼來訪問,這就可以確保不會發生不希望的事情。
3)多態性
面向對象的多態性,即“一個接口,多個方法”。多態性體現在父類中定義的屬性和方法被子類繼承後,可以具有不同的屬性或表現方式。多態性允許一個接口被多個同類使用,彌補了單繼承的不足。
多態概念可以用樹形關係來表示,如圖 4 所示。
圖 4 多態示例圖
從圖 4 中可以看出,老師類中的許多屬性和方法可以被語文老師類和數學老師類同時使用,這樣也不易出錯。
8.1 引用類型——類(對比C++)
8.1.1 定義形式差異
java裏邊定義的類
[public][abstract|final]class<class_name>[extends<class_name>][implements<interface_name>] {
// 定義屬性部分
<property_type><property1>;
<property_type><property2>;
<property_type><property3>;
…
// 定義方法部分
function1();
function2();
function3();
…
}
上述語法中,中括號“[]”中的部分表示可以省略
,豎線“|”表示“或關係”,例如 abstract|final,說明可以使用 abstract 或 final 關鍵字,但是兩個關鍵字不能同時出現。
上述語法中各關鍵字的描述如下。
- public:表示“共有”的意思。如果使用 public 修飾,則可以被其他類和程序訪問。每個 Java 程序的主類都必須是 public 類,作爲公共工具供其他類和程序使用的類應定義爲 public 類。
- abstract:如果類被 abstract 修飾,則該類爲抽象類,抽象類不能被實例化,但抽象類中可以有抽象方法(使用 abstract 修飾的方法)和具體方法(沒有使用 abstract 修飾的方法)。繼承該抽象類的所有子類都必須實現該抽象類中的所有抽象方法(除非子類也是抽象類)。
- final:如果類被 final 修飾,則不允許被繼承。
- class:聲明類的關鍵字。
- extends:表示繼承其他類。
- implements:表示實現某些接口。
property_type:表示成員變量的類型。
property:表示成員變量名稱。
function():表示成員方法名稱。
Java 類名的命名規則:
- 類名應該以下劃線(_)或字母開頭,最好以字母開頭。
- 第一個字母最好大寫,如果類名由多個單詞組成,則每個單詞的首字母最好都大寫。
- 類名不能爲 Java 中的關鍵字,例如 boolean、this、int 等。
- 類名不能包含任何嵌入的空格或點號以及除了下劃線(_)和美元符號($)字符之外的特殊字符。
c++裏邊定義
class Student{//類名前面沒有訪問權限修飾符
public://相同的成員屬性寫一次,也可以每次寫一個,但是public要重新寫
//成員變量
char *name;
int age;
float score;
//成員函數
void say(){
cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}
};//java沒有
1.注意在類定義的最後有一個分號;,它是類定義的一部分,表示類定義結束了,不能省略。
2.成員函數可以寫類裏邊,也可以寫類的外邊,聲明必須在類內。在類體中定義的成員函數會自動成爲內聯函數,在類體外定義的不會。
3.class沒有訪問權限修飾
還有其他的區別,我們在下面講。(如析構函數)
8.1.2 類的屬性
Java裏邊
可以在聲明成員變量的同時對其進行初始化,如果聲明成員變量時沒有對其初始化,則系統會使用默認值初始化成員變量。
初始化的默認值如下:
- 整數型(byte、short、int 和 long)的基本類型變量的默認值爲 0。
- 單精度浮點型(float)的基本類型變量的默認值爲 0.0f。
3.雙精度浮點型(double)的基本類型變量的默認值爲 0.0d。 - 字符型(char)的基本類型變量的默認值爲 “\u0000”。
- 布爾型的基本類型變量的默認值爲 false。
- 數組引用類型的變量的默認值爲 null。
如果創建了數組變量的實例,但沒有顯式地爲每個元素賦值,則數組中的元素初始化值採用數組數據類型對應的默認值。
public class Student {
public String name; // 姓名
final int sex = 0; // 性別:0表示女孩,1表示男孩
private int age; // 年齡,默認賦值爲 0
}
c++裏邊
必須通過構造函數賦予初值。構造函數的一項重要功能是對成員變量進行初始化,爲了達到這個目的,可以在構造函數的函數體中對成員變量一一賦值,還可以採用初始化列表。
以下的類只給出一部分
class Student{
private:
char *m_name;
int m_age;
float m_score;
public:
Student(char *name, int age, float score);
void show();
};
//採用初始化列表
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
//TODO:
}
8.1.3 this指針
java裏邊
this 關鍵字是 Java 常用的關鍵字,可用於任何實例方法內指向當前對象,也可指向對其調用當前方法的對象,或者在需要當前類型對象引用時使用。其實很類似於C++的this指針
1)但是如果在內部類中需要使用外部類中的對象,這時就需要使用外部類的類名進行限定。
package twlkyao;
public class A {
public A() {
Inner inner = new Inner();
inner.outer(); // call the inner class's outer method.
this.outer(); // call A's outer method.
}
public void outer() {
System.out.println("outer run");
}
class Inner {
public void outer(){
System.out.println("inner run");
A.this.outer(); // 這裏就引用了外部類
System.out.println("--------");
}
}
public static void main(String[] args) {
A a = new A();
}
}
2)另外,在構造方法中,經常使用this(參數表)來調用參數多的構造方法,並且Java要求在構造方法中,`this(參數表)要出現在任何其他語句之前。this( ) 不能在普通方法中使用,只能寫在構造方法中。
public class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public Circle() {
this(1.0); //調用有參構造函數,必須出現在最前面,否則報錯
this.radius = 2.0;
}
}
c++裏邊的this
1、this指針的概念
在 C++ 中,每一個對象都能通過 this 指針來訪問自己的地址。this 指針是所有成員函數的隱含參數。
因此,在成員函數內部,它可以用來指向調用對象。
2、this只能在成員函數中使用
(java一樣)
3、this指針不能再靜態函數中使用
(java一樣)
靜態函數如同靜態變量一樣,他不屬於具體的哪一個對象,靜態函數表示了整個類範圍意義上的信息,而this指針卻實實在在的對應一個對象,所以this指針不能被靜態函數使用。
4、this指針的創建
this指針在成員函數的開始執行前構造的,在成員的執行結束後清除。
5、this指針只有在成員函數中才有定義。
創建一個對象後,不能通過對象使用this指針。也無法知道一個對象的this指針的位置(只有在成員函數裏纔有this指針的位置)。當然,在成員函數裏,你是可以知道this指針的位置的(可以&this獲得),也可以直接使用的。
8.1.4 對象的創建
java裏邊
在 Java 語言中創建對象分顯式創建與隱含創建兩種情況。
1)顯式創建對象
對象的顯式創建方式有 4 種。
1.使用 new 關鍵字創建對象
這是常用的創建對象的方法,語法格式如下:
類名 對象名 = new 類名();
2.調用 java.lang.Class 或者 java.lang.reflect.Constuctor 類的 newlnstance() 實例方法
在 Java 中,可以使用 java.lang.Class 或者 java.lang.reflect.Constuctor 類的 newlnstance() 實例方法來創建對象,代碼格式如下:
java.lang.Class Class 類對象名稱 = java.lang.Class.forName(要實例化的類全稱);
類名 對象名 = (類名)Class類對象名稱.newInstance();
調用 java.lang.Class 類中的 forName() 方法時,需要將要實例化的類的全稱(比如 com.mxl.package.Student)作爲參數傳遞過去,然後再調用 java.lang.Class 類對象的 newInstance() 方法創建對象。
3.調用對象的 clone() 方法
該方法不常用,使用該方法創建對象時,要實例化的類必須繼承 java.lang.Cloneable 接口。
調用對象的 clone() 方法創建對象的語法格式如下:
類名對象名 = (類名)已創建好的類對象名.clone();
4.調用 java.io.ObjectlnputStream 對象的 readObject() 方法
說明:
- 使用 new 關鍵字或 Class 對象的 newInstance() 方法創建對象時,都會調用類的構造方法。
- 使用 Class 類的 newInstance() 方法創建對象時,會調用類的默認構造方法,即無參構造方法。
- 使用 Object 類的 clone() 方法創建對象時,不會調用類的構造方法,它會創建一個複製的對象,這個對象和原來的對象具有不同的內存地址,但它們的屬性值相同。
- 如果類沒有實現 Cloneable 接口,就使用 clone。方法會拋java.lang.CloneNotSupportedException 異常,所以應該讓類實現 Cloneable 接口。
2)隱式創建對象
例如下面幾種情況。
String strName = "strValue",其中的“strValue”就是一個 String 對象,由 Java 虛擬機隱含地創建。
2.字符串的“+”運算符運算的結果爲一個新的 String 對象,示例如下:
String str1 = "Hello";
String str2 = "Java";
String str3 = str1+str2; // str3引用一個新的String對象
3.當 Java 虛擬機加載一個類時,會隱含地創建描述這個類的 Class 實例。
類的加載是指把類的 .class 文件中的二進制數據讀入內存中,把它存放在運行時數據區的方法區內,然後在堆區創建一個 java.lang.Class 對象,用來封裝類在方法區內的數據結構。
無論釆用哪種方式創建對象,Java 虛擬機在創建一個對象時都包含以下步驟:
- 給對象分配內存。
- 將對象的實例變量自動初始化爲其變量類型的默認值。
- 初始化對象,給實例變量賦予正確的初始值。
每個對象都是相互獨立的,在內存中佔有獨立的內存地址,並且每個對象都具有自己的生命週期,
當一個對象的生命週期結束時,對象就變成了垃圾,由 Java 虛擬機自帶的垃圾回收機制處理。
注意:一個對象要被使用,則對象必須被實例化,如果一個對象沒有被實例化而直接調用了對象中的屬性或方法,
Student stu = null;//null意味則沒有分配空間,對象沒有實例化
stu.Name = "李子文";
stu.Sex = true;
stu.Age = 15;
//則程序運行時會出現以下異常:
//Exception in thread "main" java.lang.NullPointerException
c++裏邊
明顯我們可以使用一下幾種方式:
Student a = Student();//棧上創建
Student *c = new Student();//堆上創建
String s = "hedigl";//通過運算符重載
意義上僅兩種:
一種是在棧上創建,形式和定義普通變量類似;另外一種是在堆上使用 new 關鍵字創建,返回一個匿名指針,必須要用一個指針指向它
8.1.5 匿名對象
我們知道創建對象的標準格式如下:
類名稱 對象名 = new 類名稱();
每次 new 都相當於開闢了一個新的對象,並開闢了一個新的物理內存空間。如果一個對象只需要使用唯一的一次,就可以使用匿名對象,匿名對象還可以作爲實際參數傳遞。
匿名對象就是沒有明確的給出名字的對象,是對象的一種簡寫形式。一般匿名對象只使用一次,而且匿名對象只在堆內存中開闢空間,而不存在棧內存的引用。
public class Person {
public String name; // 姓名
public int age; // 年齡
// 定義構造方法,爲屬性初始化
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 獲取信息的方法
public void tell() {
System.out.println("姓名:" + name + ",年齡:" + age);
}
public static void main(String[] args) {
new Person("張三", 30).tell(); // 匿名對象
}
}
程序運行結果爲:
姓名:張三,年齡:30
在以上程序的主方法中可以發現,直接使用了“new Person(“張三”,30)”語句,這實際上就是一個匿名對象,與之前聲明的對象不同,此處沒有任何棧內存引用它
,所以此對象使用一次之後就等待被 GC(垃圾收集機制)回收。
匿名對象在實際開發中基本都是作爲其他類實例化對象的參數傳遞的,在後面的 Java 應用部分的很多地方都可以發現其用法,而且細心的讀者可以發現,匿名對象實際上就是個堆內存空間,對象不管是匿名的還是非匿名的,都必須在開闢堆空間之後纔可以使用。
8.1.6 對象的銷燬、及析構函數
1.對象的銷燬
對象使用完之後需要對其進行清除。對象的清除是指釋放對象佔用的內存。在創建對象時,用戶必須使用 new 操作符爲對象分配內存。不過,在清除對象時,由系統自動進行內存回收,不需要用戶額外處理。這也是 Java 語言的一大特色
,某種程度上方便了程序員對內存的管理。
Java 語言的內存自動回收稱爲垃圾回收(Garbage Collection)機制,簡稱 GC
。垃圾回收機制是指 JVM 用於釋放那些不再使用的對象所佔用的內存。
Java 語言並不要求 JVM 有 GC,也沒有規定 GC 如何工作。不過常用的 JVM 都有 GC,而且大多數 GC 都使用類似的算法管理內存和執行回收操作。具體的垃圾回收實現策略有好多種,在此不再贅述。
注意:C++語言對象是通過 delete 語句手動釋放。如果回收內存的任務由程序負責,也就是說必須在程序中顯式地進行內存回收,這無疑會增加程序員的負擔,而且存在很多弊端。
Java 語言對象是由垃圾回收器收集然後釋放,程序員不用關係釋放的細節。自動內存管理是現代計算機語言發展趨勢,例如:C# 語言的垃圾回收,Objective-C 和 Swift 語言的 ARC(內存自動引用計數管理)。
一個對象被當作垃圾回收的情況主要如下兩種。
1)對象的引用超過其作用範圍。
{ Object o = new Object(); // 對象o的作用範圍,超過這個範圍對象將被視爲垃圾
}
2)對象被賦值爲 null。
{
Object o = new Object();
o = null; // 對象被賦值爲null將被視爲垃圾
}
在 Java 的 Object 類中還提供了一個 protected 類型的 finalize() 方法,因此任何 Java 類都可以覆蓋這個方法,在這個方法中進行釋放對象所佔有的相關資源的操作。
在 Java 虛擬機的堆區,每個對象都可能處於以下三種狀態之一。
1)可觸及狀態:當一個對象被創建後,只要程序中還有引用變量引用它,那麼它就始終處於可觸及狀態。
2)可復活狀態:當程序不再有任何引用變量引用該對象時,該對象就進入可復活狀態。在這個狀態下,垃圾回收器會準備釋放它所佔用的內存,在釋放之前,會調用它及其他處於可復活狀態的對象的 finalize() 方法,這些 finalize() 方法有可能使該對象重新轉到可觸及狀態。
3)不可觸及狀態:當 Java 虛擬機執行完所有可復活對象的 finalize() 方法後,如果這些方法都沒有使該對象轉到可觸及狀態,垃圾回收器纔會真正回收它佔用的內存。
注意:調用 System.gc() 或者 Runtime.gc() 方法也不能保證回收操作一定執行,它只是提高了 Java 垃圾回收器儘快回收垃圾的可能性。
2 析構函數(finalize)
析構方法與構造方法相反,當對象脫離其作用域時(例如對象所在的方法已調用完畢),系統自動執行析構方法。析構方法往往用來做清理垃圾碎片的工作,例如在建立對象時用 new 開闢了一片內存空間,應退出前在析構方法中將其釋放。
在 Java 的 Object 類中還提供了一個 protected 類型的 finalize() 方法,因此任何 Java 類都可以覆蓋這個方法,在這個方法中進行釋放對象所佔有的相關資源的操作。
對象的 finalize() 方法具有如下特點:
- 垃圾回收器是否會執行該方法以及何時執行該方法,都是不確定的。
- finalize() 方法有可能使用對象復活,使對象恢復到可觸及狀態。
- 垃圾回收器在執行 finalize() 方法時,如果出現異常,垃圾回收器不會報告異常,程序繼續正常運行。
例如:
protected void finalize() {
// 對象的清理工作
}
例 1
下面通過一個例子來講解析構方法的使用。該例子計算從類中實例化對象的個數。
1)Counter 類在構造方法中增值,在析構方法中減值。如下所示爲計數器類 Counter 的代碼:
public class Counter {
private static int count = 0; // 計數器變量
public Counter() {
// 構造方法
this.count++; // 創建實例時增加值
}
public int getCount() {
// 獲取計數器的值
return this.count;
}
protected void finalize() {
// 析構方法
this.count--; // 實例銷燬時減少值
System.out.println("對象銷燬");
}
}
2)創建一個帶 main() 的 TestCounter 類對計數器進行測試,示例代碼如下:
public class TestCounter {
public static void main(String[] args) {
Counter cnt1 = new Counter(); // 建立第一個實例
System.out.println("數量:"+cnt1.getCount()); // 輸出1
Counter cnt2 = new Counter(); // 建立第二個實例
System.out.println("數量:"+cnt2.getCount()); // 輸出2
cnt2 = null; // 銷燬實例2
try {
System.gc(); // 清理內存
Thread.currentThread().sleep(1000); // 延時1000毫秒
System.out.println("數量:"+cnt1.getCount()); // 輸出1
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
執行後輸出結果如下:
數量:1
數量:2
對象銷燬
數量:1
由於 finalize() 方法的不確定性,所以在程序中可以調用 System.gc() 或者 Runtime.gc() 方法提示垃圾回收器儘快執行垃圾回收操作。
C++裏的析構函數
析構函數(Destructor)也是一種特殊的成員函數,沒有返回值,不需要程序員顯式調用(程序員也沒法顯式調用),而是在銷燬對象時自動執行。構造函數的名字和類名相同,而析構函數的名字是在類名前面加一個~符號。
如
public class demo{
public:
~demo(){}//析構函數
}
注意:析構函數沒有參數,不能被重載,因此一個類只能有一個析構函數。如果用戶沒有定義,編譯器會自動生成一個默認的析構函數。