面向對象程序設計概述
面向對象程序設計(簡稱OOP)是當今主流的程序設計範型,Java是
完全面向對象
的語言。
在OOP(Object Oriented Programming)中,不必關心對象的具體實現,只要能滿足用戶需求即可。 採用面向對象的方式進行開發:耦合度低,擴展力強
。
文章目錄
一、類與對象
類實際上在現實世界當中是不存在的,是一個抽象的概念,是一個
模板
,是我們人類大腦進行“思考、總結、抽象”的一個結果,類本質上是現實世界當中某些事物具有共同特徵
,將這些共同特徵提取出來形成的概念就是一個“類”。而對象是實際存在的個體,對象還有另一個名字叫做
實例
。類是構造對象的
模板
或藍圖
,通過類這個模板創建對象的過程,叫做:實例化
。
1.1 類之間的關係
在類之間,最常見的關係有:
(一)繼承 / 泛化關係
,即"is-a"關係。
用來描述繼承關係,在 Java 中使用 extends 關鍵字。
(二)實現關係
。
用來實現一個接口,在 Java 中使用 implements 關鍵字。
(三)聚合關係
,即"has-a"關係。
表示整體由部分組成,但是整體和部分不是強依賴的,整體不存在了部分還是會存在。
(四)依賴關係
,即"uses-a"關係。
例如,A 類和 B 類是依賴關係主要有三種形式:
- A 類是 B 類方法的局部變量;
- A 類是 B 類方法的參數;
- A 類向 B 類發送消息,從而影響 B 類發生變化。
1.2 對象構造
要想使用對象,就必須首先構造對象,並指定其初始狀態(實例變量)。什麼是實例變量?
對象又被稱爲實例。實例變量實際上就是:對象級別的變量
。
public class student{
String name;
double height;
boolean sex;
}
名字、身高、性別這些屬性所有的學生都有,但是每一個對象都有“自己的名字、性別、身高值”。
假設創建10個學生對象,name、height、sex變量應該有10份。
所以這種變量被稱爲對象級別的變量,屬於實例變量。
在Java程序設計語言中,使用構造器構造新實例。構造器是一種特殊的方法,編寫在類當中,用來構造並初始化對象。
public static void main(String[] args){
Student zhangsan = new Student(); // 其中Student()爲無參構造器
}
構造器的名字應該與類名相同。因此,Student類的構造器名爲Student。在構造器前面加上new操作符,即可構造一個Student對象。
注意:
這裏的對象變量zhangsan並沒有實際包含一個對象,而僅僅引用一個對象
。
對象和引用的區別?
對象是通過new出來的,在堆內存中存儲。
引用是:但凡是變量,並且該變量中保存了內存地址指向了堆內存當中的對象的。
無參構造器
public Student(){
}
由無參構造器創建出來的對象,其狀態(實例變量)會設置爲不同數據類型對應默認值。
即:
public Student(){
name = ""; // 即爲 null
height = 0.0;
sex = false;
}
方法重載
多個方法有相同的名字、不同的參數,便產生了重載。這樣的機制緩解了程序員苦想方法名的鴨力。
(方法重載只與參數的類型,參數的個數,和參數的順序有關,與返回值類型無關)
有參構造器便是其中的例子。
有參構造器
public Student(String name,double height,boolean sex){
this.name = name;
this.height = height; // 這裏的this表示當前對象
this.sex = sex;
}
此時,編譯器通過有參構造方法給出的參數類型與調用方法時傳入的參數類型進行匹配,如果不匹配則編譯時報錯。
1.3 封裝
面向對象的三大特徵:
封裝
、繼承
、多態
封裝使數據被保護在抽象數據類型的內部,儘可能地隱藏內部的細節,只保留一些對外的接口使其與外部發生聯繫。(一般提供提供一個公有的訪問getter方法,和一個公有的更改setter方法)
public class Student{
private double height;
public boolean getHeight() {
return height;
}
public void setHeight(boolean height) {
this.height = height;
}
}
還可以在setter方法中編寫代碼來執行錯誤檢查,避免一些錯誤修改。例如:將學生對象的身高設置爲負數。
1.4 final與static
final
對於基本類型,final 使數值不變;
對於引用類型,final 使引用不變,也就不能引用其它對象,但是被引用的對象本身是可以修改的。
final定義的變量必須顯示初始化。
final int x = 1;
// x = 2; // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;
final聲明的方法不能被子類重寫。
final聲明的類不允許被繼承。
static
靜態變量
當同一個類創建出來的對象中都含有一個相同值的變量時,往往將他們定義爲靜態static。
如一箇中國人類中,國籍屬性都爲中國,所以國籍就應該被定義爲靜態。
public class Chinese{
private int id;
static String country = china;
public static void eat(){}
}
類所有的實例都共享靜態變量,靜態變量在內存中只存在一份。
換句話來說,如果有100個Chinese類的對象,則有100個實例變量id,但是隻有一個靜態變量country,並且值都爲china。即使沒有一個chinese對象存在,靜態變量country也存在。它屬於類,不屬於任何獨立的對象。
靜態方法
靜態方法是一種不能向對象實施操作的方法。並且可以認爲靜態方法是沒有this參數的方法。
一般用 類名點 來調用靜態方法,不需要用對象調用靜態方法。
public static void main(String[] args){
chinese.eat();
}
靜態方法在類加載的時候就存在了,它不依賴於任何實例。
同理,這裏的main方法也是一個靜態方法。main方法不對任何對象進行操作。事實上,在啓動程序時還沒有任何對象。靜態的main方法將執行並創建程序所需要的對象。
靜態語句塊
靜態語句塊在類初始化時運行一次。
public class Chinese{
static{
System.out.println("類初始化時運行一次");
}
public static void main(String[] args){
Chinese a = new Chinese();
}
}
在初始化的順序中,靜態變量和靜態語句塊優先於實例變量和普通語句塊,靜態變量和靜態語句塊的初始化順序取決於它們在代碼中的順序。
二、繼承
利用
繼承
,人們可以基於已存在的類構造一個新類。繼承已存在的類就是複用(繼承)這些類的方法和實例變量。在此基礎上,還可以添加一些新的方法和實例變量,以滿足新的需求。
2.1 父類和子類
繼承的相關特性
① B類繼承A類,則稱A類爲 超類(superclass)、父類、基類
,
B類則稱爲子類(subclass)、派生類、擴展類
。
class A{}
class B extends A{}
② java 中的繼承只支持單繼承,不支持多繼承,C++中支持多繼承,
這也是 java 體現簡單性的一點,換句話說,java 中不允許這樣寫代碼:
//class B extends A,C{ } // error
③ 雖然 java 中不支持多繼承,但有的時候會產生間接繼承的效果,
例如:
class C extends B{}
class B extends A{}
也就是說,C 直接繼承 B,
其實 C 還間接繼承 A。
④ java 中規定,子類繼承父類,除構造方法
不能繼承之外,剩下都可以繼承。
但是私有的屬性
無法在子類中直接訪問。(父類中private修飾的不能在子類中
直接訪問。可以通過間接的手段來訪問。)
⑤ java 中的類沒有顯示的繼承任何類,則默認繼承 Object類
,Object類是
java 語言提供的根類(超類),也就是說,一個對象與生俱來就有
Object類型中所有的特徵。
⑥ 繼承也存在一些缺點,例如:CreditAccount 類繼承 Account 類會導致它
們之間的耦合度非常高,Account 類發生改變之後會馬上影響到 CreditAccount 類
方法覆蓋
父類中的方法無法滿足子類的業務需求,子類對繼承過來的方法進行
覆蓋/重寫
。
滿足方法覆蓋的條件:
第一:有繼承關係的兩個類
第二:具有相同方法名、返回值類型、形式參數列表
第三:訪問權限不能更低。
第四:拋出異常不能更多。
私有方法無法覆蓋。
方法覆蓋只是針對於“實例方法”,“靜態方法覆蓋”沒有意義。(這是因爲方法覆蓋通常和多態聯合起來)
super關鍵字
super關鍵字的兩個用途:1.調用父類的方法 2.調用父類的構造器
所以super能出現在實例方法和構造方法中。
super的語法是:“super.”、“super()”
super不能使用在靜態方法中。
super. 大部分情況下是可以省略的。
super.什麼時候不能省略呢?
父類和子類中有同名屬性,或者說有同樣的方法,想在子類中訪問父類的,super. 不能省略。
super() 只能出現在構造方法第一行,通過當前的構造方法去調用“父類”中的構造方法,目的是:創建子類對象的時候,先初始化父類型特徵。如果子類的構造器沒有顯式調用父類的構造器,則將自動調用父類的無參構造器。如果父類沒有無參構造器,並且子類構造器中沒有顯式地調用父類的其他構造器,則編譯器報錯。
class Animal{
int age;
public Animal(int age){
this.age = age;
}
}
class cat extends Animal{
public cat(int age){
super(age);
}
}
class dog extends Animal{
public dog(int age){
super(age);
}
}
2.2 多態
一個對象變量可以指示多種實際類型的現象被稱爲
多態
(polymorphism)。在運行時能夠自動地選擇調用哪個方法地現象稱爲動態綁定
(dynamic binding)。
多態在開發中的作用是:降低程序的耦合度
,提高程序的擴展力
。
例如下面這個例子:
public class Master{
public void feed(Dog d){
System.out.println("狗喫骨頭");
}
public void feed(Cat c){
System.out.println("貓喫魚");
}
}
class Dog{}
class Cat{}
以上的代碼中表示:Master和Dog以及Cat的關係很緊密(耦合度高)。導致擴展力
很差。
public class Master{
public void feed(Pet pet){
pet.eat();
}
public static void main(String [] args){
Master m = new Master();
// 向上轉型 Pet p1 = new Dog();
m.feed(new Dog()); // 狗喫骨頭
// 向上轉型 Pet p2 = new Cat();
m.feed(new Cat()); // 貓喫魚
}
}
class Pet{
public void eat(){}
}
class Dog extends Pet{
@Override
public void eat(){
System.out.println("狗喫骨頭");
}
}
class Cat extends Pet{
@Override
public void eat(){
System.out.println("貓喫魚");
}
}
以上的代表中表示:Master和Dog以及Cat的關係就脫離了,Master關注的是Pet類(指Dog、Cat類的父類)。
這樣Master和Dog以及Cat的耦合度就降低了,提高了軟件的擴展性
。
向上轉型和向下轉型
向上轉型
:子—>父 (upcasting)
又被稱爲自動類型轉換:Pet p1 = new Dog();
向下轉型
:父—>子 (downcasting)
又被稱爲強制類型轉換:Dog c = (Dog)p1;
需要添加強制類型轉換符。
什麼時候需要向下轉型?
需要調用或者執行子類對象中特有的方法,必須進行向下轉型,纔可以調用。
編譯時
多態主要指方法的重載
運行時
多態指程序中定義的對象引用所指向的具體類型在運行期間才確定
Pet p1 = new Dog();
// 編譯的時候編譯器發現p1的類型是Pet,所以編譯器會去Pet類中找eat()方法
// 找到了,綁定,編譯通過。但是運行的時候和底層堆內存當中的實際對象有關
// 真正執行的時候會自動調用“堆內存中真實對象”的相關方法。
m.feed();
多態的典型代碼:父類型的引用指向子類型的對象。
2.3 抽象類
例如,People、Student、Teacher這三個類,從我們使用的角度來看主要對 Student 和Teacher 進行實例化,Person 中主要包含了一些公共的屬性和方法,而 Person 我們通常不會實例化,所以我們可以把它定義成抽象的:
抽象類
和抽象方法
都使用 abstract
關鍵字進行聲明。如果一個類中包含抽象方法,那麼這個類必須聲明爲抽象類。
抽象類和普通類最大的區別是,抽象類不能被實例化,只能被繼承。
如果這個類是抽象的,那麼這個類被子類繼承,抽象方法必須被重寫。如果在子類中不復寫該抽象方法,那麼必須將此類再次聲明爲抽象類。
public abstract class People{
private String name;
protected boolean sex;
public abstarct void write(){}
}
public class Student extends People{
public Student(String name,boolean sex){
super(name);
this.sex = sex;
}
@Override
public void write(){
System.out.println("學生寫字");
}
}
2.4 Object類
Object類是Java中所有類的根基,在Java中每個類都是由它擴展而來的。如果在類的聲明中未使用 extends 關鍵字指明其基類,則默認基類爲 Object 類。
等號“==”與equals方法
等號可以比較基本類型和引用類型,等號比較的是值,特別是比較引用類型,比較的是引用的
內存地址,但對於多數類來說,這種判斷沒有什麼意義。
默認的equals方法也是判斷兩個對象是否具有相同的引用,雖然合理,同樣也意義不大,所以通常我們會在子類中覆蓋equals方法。
兩個對象具有等價關係,需要滿足以下五個條件:
①自反性
x.equals(x); // true
②對稱性
x.equals(y) == y.equals(x); // true
③傳遞性
if (x.equals(y) && y.equals(z))
x.equals(z); // true;
④一致性
多次調用equals方法結果不變
x.equals(y) == x.equals(y); // true
⑤對任何非空引用x,x.equals(null)應該返回false
實現建議:
1.檢查是否爲同一個對象的引用,如果是直接返回 true;
2.檢查是否是同一個類型,如果不是,直接返回 false;
3.將 Object 對象進行轉型;
4.判斷每個關鍵域是否相等。
public class EqualExample {
private int x;
private int y;
private int z;
public EqualExample(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EqualExample that = (EqualExample) o;
if (x != that.x) return false;
if (y != that.y) return false;
return z == that.z;
}
}
• 如果子類能夠擁有自己的相等概念,則對稱性需求將強制採用getclass進行檢測。
• 如果由超類決定相等的概念,那麼就可以使用instanceof進行檢測,這樣可以在不同子類的對象之間進行相等的判斷。
hashCode方法
hashCode()
返回哈希值,而 equals()
是用來判斷兩個對象是否等價。等價的兩個對象散列值一定相同,但是散列值相同的兩個對象不一定等價,這是因爲計算哈希值具有隨機性,兩個值不同的對象可能計算出相同的哈希值。
在覆蓋 equals()
方法時應當總是覆蓋 hashCode()
方法,保證等價的兩個對象哈希值也相等。
(到集合類再補充說明)
toString方法
返回該對象的字符串表示。通常 toString 方法會返回一個“以文本方式表示”此對象的字符串,Object 類的 toString 方法返回一個字符串,該字符串由類名加標記@和此對象哈希碼的無符號十六進制表示組成,Object 類 toString 源代碼如下:
getClass().getName() + '@' + Integer.toHexString(hashCode())
在進行 String 與其它類型數據的連接操作時,如:
System.out.println(student);
,它自動調用該對象的 toString()
方法。
三、接口
在Java程序設計語言中,接口不是類,而是對類的一組需求描述,這些類要遵從接口描述的統一格式進行定義。接口我們可以看作是
抽象類的一種特殊情況
,在接口中只能定義抽象的方法和常量
3.1 接口的特性
- 在 java 中接口採用
interface
聲明
public interface Comparable{}
- 接口中的方法默認都是
public abstract
(一般不需要手動標記) ,不能更改。 - 接口中的變量默認都是
public static final
類型的,不能更改,所以必須顯示的初始化 - 從 Java 8 開始,接口也可以擁有默認的方法實現(用default修飾符標記方法),這是因爲不支持默認方法的接口的維護成本太高了,而默認方法不需要實現類去實現。在 Java 8 之前,如果一個接口想要添加新的方法,那麼要修改所有實現了該接口的類,讓它們都實現新增的方法。
public interface Comparable{
public static final int a = 1;
public static final String b = "哈哈";
int compareTo(Employee other){
return 0;
}
default void func1(){
System.out.println("默認方法");
}
}
- 接口不能被實例化,接口中沒有構造函數的概念
- 接口之間可以繼承,但接口之間不能實現
- 接口中的方法只能通過類來實現,通過
implements
關鍵字 - 如果一個類實現了接口,那麼接口中所有的方法必須實現
class Student implements Comparable{
@Override
int compareTo(Employee other){
System.out.println("實現、覆蓋方法");
return 1;
}
}
- 一類可以實現多個接口
class Student implements Comparable,Person,Named{}
3.2 接口與抽象類的區別
a)從設計層面上看,抽象類提供了一種 IS-A 關係,需要滿足裏式替換原則,即子類對象必須能夠替換掉所有父類對象。而接口更像是一種 LIKE-A 關係,它只是提供一種方法實現契約,並不要求接口和實現接口的類具有 IS-A 關係。
b)接口的字段只能是 static 和 final 類型的,而抽象類的字段沒有這種限制。接口的成員只能是 public 的,而抽象類的成員可以有多種訪問權限。
c) 接口描述了方法的特徵,不給出實現,一方面解決 java 的單繼承問題,實現了強大的可接插性
d) 抽象類提供了部分實現,抽象類是不能實例化的,抽象類本身不能被使用者直接實例化,但被具體類繼承後,就可以在具體類實例化的過程中被實例化了,而接口是完全不能實例化的。抽象類的存在主要是可以把公共的代碼移植到子類中。
e) 面向接口編程,而不要面向具體編程(面向抽象編程,而不要面向具體編程)
f) 優先選擇接口(因爲繼承抽象類後,此類將無法再繼承,所以會喪失此類的靈活性,而且JAVA語言中對接口的限制可以避免因類繼承而引起的所有問題)
四、內部類
內部類(inner class)是定義在另一個類中的類。內部類主要分類:
實例內部類
局部內部類
靜態內部類
• 內部類方法可以訪問該類定義所在的作用域中的數據,包括私有的數據
• 內部類可以對同一個包中的其他類隱藏起來
實例內部類
創建實例內部類,外部類的實例必須已經創建
實例內部類會持有外部類的引用
實例內部不能定義 static 成員,只能定義實例成員
public class InnerClassTest01 {
private int a;
private int b;
InnerClassTest01(int a, int b) {
this.a = a;
this.b = b;
}
//內部類可以使用 private 和 protected 修飾
private class Inner1 {
int i1 = 0;
int i2 = 1;
int i3 = a;
int i4 = b;
//實例內部類不能採用 static 聲明
//static int i5 = 20;
}
public static void main(String[] args) {
InnerClassTest01.Inner1 inner1 = new InnerClassTest01(100, 200).new Inner1();
System.out.println(inner1.i1);
System.out.println(inner1.i2);
System.out.println(inner1.i3);
System.out.println(inner1.i4);
}
}
靜態內部類
靜態內部類不會持有外部的類的引用,創建時可以不用創建外部類
靜態內部類可以訪問外部的靜態變量,如果訪問外部類的成員變量必須通過外部類的實例訪問
public class InnerClassTest02 {
static int a = 200;
int b = 300;
static class Inner2 {
//在靜態內部類中可以定義實例變量
int i1 = 10;
int i2 = 20;
//可以定義靜態變量
static int i3 = 100;
//可以直接使用外部類的靜態變量
static int i4 = a;
//不能直接引用外部類的實例變量
//int i5 = b;
//採用外部類的引用可以取得成員變量的值
int i5 = new InnerClassTest02().b;
}
public static void main(String[] args) {
InnerClassTest02.Inner2 inner = new InnerClassTest02.Inner2();
System.out.println(inner.i1);
}
}
局部內部類
局部內部類是在方法中定義的,它只能在當前方法中使用。和局部變量的作用一樣
局部內部類和實例內部類一致,不能包含靜態成員
public class InnerClassTest03 {
private int a = 100;
//局部變量,在內部類中使用必須採用 final 修飾
public void method1(final int temp) {
class Inner3 {
int i1 = 10;
//可以訪問外部類的成員變量
int i2 = a;
int i3 = temp;
}
//使用內部類
Inner3 inner3 = new Inner3();
System.out.println(inner3.i1);
System.out.println(inner3.i3);
}
public static void main(String[] args) {
InnerClassTest03 innerClassTest03 = new InnerClassTest03();
i nnerClassTest03.method1(300);
}
}
匿名內部類
是一種特殊的內部類,該類沒有名字
• 沒有使用匿名類
public class InnerClassTest04 {
public static void main(String[] args) {
MyInterface myInterface = new MyInterfaceImpl();
myInterface.add();
}
}
interface MyInterface {
public void add();
}
class MyInterfaceImpl implements MyInterface {
public void add() {
System.out.println("-------add------");
}
}
• 使用匿名類
public class InnerClassTest05 {
public static void main(String[] args) {
InnerClassTest05 innerClassTest05 = new InnerClassTest05();
innerClassTest05.method1(new MyInterface() {
public void add() {
System.out.println("-------add------");
}
});
}
private void method1(MyInterface myInterface) {
myInterface.add();
}
}
interface MyInterface {
public void add();
}
本文參考文獻:
① Java核心技術卷一(第十版)
② https://github.com/CyC2018/CS-Notes
寫在最後:新手上路,如有問題,歡迎大家指出,給意見。如果這篇文章對你有幫助,麻煩幫忙點一下贊,你們小小的舉動能給我無限大的動力。拒絕白嫖,從我做起,從現在做起。Thank you for watching!