《Java核心技術·卷Ⅰ:基礎知識(原版10》學習筆記 第5章 繼承
《Java核心技術·卷Ⅰ:基礎知識(原版10》學習筆記 第5章 繼承
5.1 類、超類和子類
5.1.1 定義子類
關鍵字 extends表示繼承
public class Manager extends Employee
{
添加方法和域
}
關鍵字extends表明正在構建的新類派生於一個已存在的類。已存在的類稱為 超類、基類或父類;新類稱為 子類、派生類或孩子類。
5.1.2覆蓋方法
子類提供一個新的方法來覆蓋超類中的方法:
public class Manager extends Employee
{
public double getSalary()
{
double baseSalary = super.getSalary(); //super調用超類的方法
return baseSalary + bonus;
}
}
子類的方法不能直接訪問超類的私有域
super 調用分類的方法
在子類可以 增加域、增加方法或覆蓋超類的方法,然而絕對不能 刪除繼承的任何域和方法。
5.1.3子類構造器
public Manager(String name,double salary,int year,int month,int day)
{
super(name,salary,year,month,day);
bonus = 0;
}
這裡關鍵字 super具有不同的含義。
語句
super(name,salary,year,month,day);
是」用超類Employee中含有name,salary,year,month和day參數的構造器「簡寫形式
由於子類的構造器不能訪問父類的私有域,所以必須利用父類的構造器對這部分私有域進行初始化,我們可以通過super實現對父類構造器的調用。使用 super 調用構造器的語句必須是子類構造器的第一條語句。
如果子類構造器沒有顯式地調用父類的構造器,則將自動調用父類默認(沒有參數)的構造器。如果超類沒有不帶參數的構造器,並且子類的構造器中又沒有顯式地調用父類的其他構造器,則Java編譯器將報告錯誤。
下面程式展現了Employee對象與Manager對象在薪水計算上的區別。
/**
* This program demonstrates inheritance
* @version 2022-01-16
* @author zengzhicheng
*
*/
public class ManagerTest {
public static void main(String[] args) {
Manager boss = new Manager("Carl Cracker",80000,1987,12,15);
boss.setBonus(5000);
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
for(Employee e:staff)
System.out.println("name="+e.getName()+",salary="+e.getSalary());
}
}
import java.time.LocalDate;
public class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name,double salary,int year,int month,int day)
{
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
public class Manager extends Employee
{
private double bonus;
/**
* @param name 是employee的name
* @param salary the salary
* @param year the hire year
* @param month the hire month
* @param day the hire day
*/
public Manager(String name,double salary,int year,int month,int day)
{
super(name,salary,year,month,day);
bonus = 0;
}
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b)
{
bonus =b;
}
}
5.1.4 繼承的層次
由一個公共父類派生出來的所有類的集合被稱為 繼承層次;
在繼承層次中,從某個特定的類到其祖先的路徑被稱為該類的 繼承鏈;
Java不支援多繼承
5.1.5 多態
多態:一個對象變數可以指示多種實際類型的現象
將一個子類對象賦值給父類變數
Employee e;
e = new Employee(...);
e = new Manager(...);
在Java程式設計語言中,對象變數是 多態的。
一個Employee變數既可以引用一個Employee類對象,也可以引用一個Employee類的任何一個子類對象;
不能將一個父類的引用變數賦給子類變數。
在Java中,子類數組的引用可以轉換為超類數組的引用,而不需要採用強制類型轉換
5.1.6 理解方法調用
假設要調用x.f(args),隱式參數x聲明為類c的一個對象。下面是調用過程的詳細描述:
1)編譯器查看對象的聲明類型和方法名。假設調用x.f(param),且隱式參數x聲明為C類的對象。需要注意的是:有可能存在多個名字為
f,但參數類型不一樣的方法。例如,可能存在方法f(int)和方法f(String)。編譯器將會一一列舉所有C類中名為f的方法和其超類中訪問屬性
為public且名為f的方法(父類的私有方法不可訪問)。
至此,編譯器已獲得所有可能被調用的候選方法。
2)接下來,編譯器將查看調用方法時提供的參數類型。如果在所有名為f的方法中存在一個與提供的參數類型完全匹配,就選擇這個方
法。這個過程被稱為重載解析(overloading resolution)。例如,對於調用x.f(「Hello」)來說,編譯器將會挑選f(String),而不是f(int)。由
於允許類型轉換(int可以轉換成double, Manager可以轉換成Employee,等等),所以這個過程可能很複雜。如果編譯器沒有找到與參
數類型匹配的方法,或者發現經過類型轉換後有多個方法與之匹配,就會報告一個錯誤。
至此,編譯器已獲得需要調用的方法名字和參數類型。
方法的名字和參數列表稱為方法的 簽名
覆蓋方法時,一定要保證返回類型的兼容性
3)如果是private方法、static方法、final方法或者構造器,那麼編譯器將可以準確地知道應該調用哪個方法,我們將這種調用方式稱為
靜態綁定(static binding)。與此對應的是,調用的方法依賴於隱式參數的實際類型,並且在運行時實現動態綁定。在我們列舉的示例
中,編譯器採用動態綁定的方式生成一條調用f (String)的指令。
4)當程式運行,並且採用動態綁定調用方法時,虛擬機一定調用與x所引用對象的實際類型最合適的那個類的方法。假設x的實際類型是
D,它是C類的子類。如果D類定義了方法f(String),就直接調用它;否則,將在D類的父類中尋找f(String),以此類推。
每次調用方法都要進行搜索,時間開銷相當大。因此,虛擬機預先為每個類創建了一個方法表(method table),其中列出了所有方法的簽名和實際調用的方法。這樣一來,在真正調用方法的時候,虛擬機僅查找這個表就行了。
動態綁定有一個非常重要的特性:無需對現存的程式碼進行修改,就可以對程式進行擴展。假設增加一個新類Executive,並且變數e有可能引用這個類的對象,我們不需要對包含調用e.getSalary()的程式碼進行重新編譯。如果e恰好引用一個Executive類的對象,就會自動地調用Executive.getSalary()方法。
5.1.7 阻止繼承:final類和方法
不允許擴展的類被稱為final類
假設希望阻止人們定義Executive類的子類,就可以在定義這個類的時候,使用final類修飾符聲明。
聲明格式如下:
public final class Executive extends Manager
{
....
}
類中特定方法也可以被聲明為final。如果這樣做,子類就不能覆蓋這個方法。(final類中所有方法自動地成為final方法)。
例如:
public class Employee
{
...
public final String getName()
{
return name;
}
}
5.1.8 強制類型轉換
double x = 3.405;
intnx = (int) x;
對象引用的轉換語法與數值表達式的類型轉換類似,僅需要一對圓括弧將目標類名包括起來,並放置在需要轉換的對象引用之前就可以了。例如:
Manager boss = (Manager) staff[0];
進行類型轉換的唯一原因是:在暫時忽略對象的實際類型之後,使用對象的全部功能
1.向下轉型,來訪問子類擴展的方法。
2.向上轉型,來訪問父類的成員變數。
在進行類型轉換之前,先查看一下是否能夠成功地轉換。這個過程簡單使用 instanceof 操作符就可以實現。例如:
if (staff[i] instanceof Manager)
{
boss = (Manager) staff[1];
...
}
如果這個類型轉換不成功,編譯器就不會進行這個轉換。
綜上所述:
1.只能在繼承層次內進行類型轉換。
2.在將父類轉換為子類之前,應當使用 instanceof 進行檢查。
5.1.9 抽象類
使用 abstract 關鍵字,聲明抽象類和抽象方法
為了提高程式的清晰度,包含一個或多個抽象方法的類本身必須被聲明為抽象的。
public abstract class Person
{
...
public abstract String getDescription();
}
除了抽象方法之外,抽象類還可以包含具體數據和具體方法。
public abstract class Person
{
public abstract String getDescription();
private String name;
public Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
盡量將通用的域和方法(不管是否抽象的)放在父類(不管是否為抽象類)中。
抽象方法僅充當著佔位符的角色,他的具體實現在子類中。
類即使不含抽象方法,也可以將類聲明為抽象類。
5.1.10 受保護訪問
1)僅對本類可見—— private
-
對所有類可見—— public
-
對本包和所有子類可見——protected
4)對本包可見——默認,不需要修飾符
5.2 Object: 所有類的超類
Object 類是 java 中所有類的始祖,在 java 中每個類都是由它擴展而來的。
可以用 Object 類型的變數引用任何類型的對象:
Object obj = new Employee("Harry Hacker",35000);
當然,Object 類型的變數只能用於作為各種值的通用支援者。
在Java中,只有基本類型(primitive types)不是對象,例如,數值、字元和布爾類型的值都不是對象。
所有的數組類型,不管是對象數組還是基本類型的數組都擴展了Object類。
Employee[] staff = new Emplyee[10];
obj = staff; //ok
obj = new int[10]; //ok
5.2.1 equals方法
equeals 方法:Object 類中用於檢測一個對象是否等於另外一個對象。
在 Object 類中,這個方法判斷兩個對象是否具有相同的引用。如果兩個對象具有相同的引用那麼他們一定相等。
在檢測中只有兩個對象屬於同一個類時,才有可能相等。
在子類定義equals方法時,首先調用父類的equals。如果檢測失敗,對象就不可能相等。如果父類中的域都相等,就需要比較子類的實例域。
public class Manager extends Employee
{
'''
public boolen equals(Object otherObject)
{
if(!super.equals(otherObject)) return false;
Manager other = (Manager) othetObject;
return bonus == other.bonus;
}
}
5.2.2 相等測試域繼承
Java語言規範要求equals方法具有下面的特性:
1)自反性:對於任何非空引用 x , x.equals(x) 應該返回 true 。
2)對稱性:對於任何引用x和y,當且僅當 y.equals(x) 返回 true , x.equals(y) 也應該返回true。
3)傳遞性:對於任何引用 x 、y 和 z ,如果 x.equals(y) 返回 true , y.equals(z) 返回true, x.equals(z) 也應該返回 true 。
4)一致性:如果 x 和 y 引用的對象沒有發生變化,反覆調用 x.equals(y) 應該返回同樣的結果。
5)對於任意非空引用 x , x.equals(null) 應該返回 false 。
這些規則十分合乎情理,從而避免了類庫實現者在數據結構中定位一個元素時還要考慮調用x.equals(y),還是調用y.equals(x)的問題。
如果子類能夠擁有自己的相等概念,則對稱性需求將強制採用getClass進行檢測。
如果由超類決定相等的概念,那麼就可以使用instanceof進行檢測,這樣可以在不同子類的對象之間進行相等的比較。
對象之間編寫equals方法的步驟:
1.比較 this == otherObject
2.檢測otherObject == null
3.比較this和otherObject是否同一個類的對象,this.getClass == otherObject.getClass() 或者判斷otherObject是否為某類的子類
4.將otherObject轉成相應的類類型變數
5.開始對所有需要比較的屬性進行比較了,基本類型使用== 屬性為對象使用Object.equals()比較
對於數組類型的域,可以使用靜態的 Arrays.equals 方法檢測相應的數組元素是否相等
5.2.3 hashCode 方法
hash 是散列的意,就是把任意長度的輸入,通過散列演算法變換成固定長度的輸出,該輸出就是散列值。
1.如果散列表中存在和散列原始輸入K相等的記錄,那麼K必定在f(K)的存儲位置上;
2.不同關鍵字經過散列演算法變換後可能得到同一個散列地址,這種現象稱為碰撞;
3.如果兩個Hash值不同(前提是同一Hash演算法),那麼這兩個Hash值對應的原始輸入必定不同。
HashCode,散列碼,關鍵點如下
- HashCode 的存在主要是為了查找的快捷性,HashCode 是用來在散列存儲結構中確定對象的存儲地址的;
2.如果兩個對象equals相等,那麼這兩個對象的 HashCode一定也相同;
3.如果對象的equals方法被重寫,那麼對象的HashCode方法也盡量重寫;
4.如果兩個對象的HashCode相同,不代表兩個對象就相同,只能說明這兩個對象在散列存儲結構中,存放於同一個位置
散列碼(hash code)是由對象導出的一個整型值。散列碼是沒有規律的。如果x和y是兩個不同的對象,x.hashCode( )與y.hashCode( )基本上不會相同。
String 類使用下列演算法計算散列碼:
int hash = 0;
for (int i = 0 ; i<length();i++)
hash = 31 * hash + charAt(i);
如果重新定義 equals 方法,就必須定義 hashCode 方法,以便用戶可以將對象插入到散列表中。
Equals 與 hashCode 的定義必須一致:如果 x.equals(y) 返回 true ,那麼 x.hashCode( ) 就必須與 y.hashCode( ) 具有相同的值。例如,如果用定義的 Employee.equals 比較僱員的ID,那麼 hashCode 方法就需要散列 ID ,而不是僱員的姓名或存儲地址。
如果存在數組類型的域,那麼可以使用靜態的Arrays.hashCode方法計算一個散列碼,這個散列碼由數組元素的散列碼組成。
int hashCode()
返回對象的散列碼。散列碼可以是任意的整數,包括正數或負數。兩個相等的對象要求返回相等的散列碼。
static int hash(Object... object)
返回一個散列碼,由提供的所有對象的散列碼組合而得到
5.2.4 toString 方法
在 Object 中還有一個重要的方法,就是 toString 方法,它用於返回表示對象值的字元串。下面是一個典型的例子。Point類的toString方法將返回下面這樣的字元串:
java.awt.Point[x=10,y=20]
絕大多數(但不是全部)的 toString 方法都遵循這樣的格式:類的名字,隨後是一對方括弧括起來的域值。下面是Employee類中的toString 方法的實現:
public String toString()
{
return getClass().getName()
+"[name=" + name
+",salary = " +salary
",hireDay=" + hireDay
+"]";
}
to.String 方法也可以供子類調用。
當然,設計子類的程式設計師也應該定義自己的 toString 方法,並將子類域的描述添加進去。如果超類使用了 getClass( ).getName( ),那麼子類只要調用super.toString( )就可以了。例如,下面是Manager類中的 toString 方法:
public class Manager extends Employess
{
'''
public String toString()
{
return super.toString()
+ "[bonus" +bonus
+ "]";
}
}
現在將 Manager 對象列印輸出內容:
Manager[name=...,salary=...,hireDay=...][bonus=...]
隨處可見 toString 方法的主要原因是:只要對象與一個字元串通過操作符「+」連接起來,Java 編譯就會自動地調用 toString 方法,以便獲得這個對象的字元串描述。例如,
Point p = new Ponit(10,20);
String message = "The current position is" + p;
在調用 x.toString( ) 的地方可以用””+x替代。這條語句將一個空串與 x 的字元串表示相連接。這裡的 x 就是 x.toString( ) 。與 toString 不同的是,如果 x 是基本類型,這條語句照樣能夠執行。
Class getClass()
返回包含對象資訊的類對象。稍後會看到Java提供了類運行時的描述,它的內容被封裝在Class類中。
boolean equals(Object otherObject)
比較兩個對象是否相等,如果兩個對象指向同一塊存儲區域,方法返回true;否則方法返回false。在自定義的類中,應該覆蓋這個方法。
String toString()
返回描述該對象值的字元串。在自定義的類中,應該覆蓋這個方法。
Sring getName()
返回這個類的名字。
Class getSuperclass()
以Class對象的形式返回這個類的父類資訊。
5.3 泛型數組列表
Java 允許在運行時確定數組大小。
int actualSize = "";
Employee[] staff = new Employee[actualSize];
當然這段程式碼並沒有完全解決運行時動態更改數組的問題。一旦確定了數組大小,改變他就太不容易了。
在 java 最簡單的方法是使用 Java 中另外被稱為 ArrayList 的類。它使用起來有點像數組,但在添加或刪除元素時,具有自動調節數組容量的功能,而不需要為此編寫任何程式碼。
ArrayList 是一個採用類型參數的泛型類。為了指定數組列表保存的元素對象類型,需要用一對尖括弧將類名括起來加在後面,例如,ArrayList
ArrayList<Employee> staff = new ArrayList<>();
這也被稱為菱形語法。
使用add方法可以將元素添加到數組列表中。例如,下面展示了如何將僱員對象添加到數組列表中的方法:
staff.add(new Employee("Harry Hacker",...));
staff.add(new Employee("Tony Tester",...));
數組列表管理著對象引用的一個內部數組。最終,數組的全部空間有可能被用盡。這就顯現出數組列表的操作魅力:如果調用 add 且內部數組已經滿了,數組列表就將自動地創建一個更大的數組,並將所有的對象從較小的數組中拷貝到較大的數組中。
數組列表的容量與數組的大小有一個非常重要的區別。如果為數組分配100個元素的存儲空間,數組就有100個空位置可以使用。而容量為100個元素的數組列表只是擁有保存100個元素的潛力(實際上,重新分配空間的話,將會超過100),但是在最初,甚至完成初始化構造之後,數組列表根本就不含有任何元素。
size 方法將返回數組列表中包含的實際元素數目。例如,
staff.size()
將返回staff數組列表的當前元素數量,它等價於數組 a 的 a.length 。
一旦能夠確認數組列表的大小不再發生變化,就可以調用 trimToSize 方法。這個方法將存儲區域的大小調整為當前元素數量所需要的存儲空間數目。垃圾回收器將回收多餘的存儲空間。
一旦整理了數組列表的大小,添加新元素就需要花時間再次移動存儲塊,所以應該在確認不會添加任何元素時,再調用trimToSize。
ArrayList<E>()
;
構造一個空數組列表.
ArrayList<E>(int initialCapacity)
用指定容量構造一個空數組列表。
參數:initalCapacity 數組列表的最初容量
boolean add(E obj)
在數組列表的尾端添加一個元素。永遠返回true。
參數:obj 添加的元素
int size()
返回存儲在數組列表中的當前元素數量。(這個值將小於或等於數組列表的容量。)
void ensureCapacity(int capacity)
確保數組列表在不重新分配存儲空間的情況下就能夠保存給定數量的元素。
參數:capacity 需要的存儲容量
void trimToSize()
將數組列表的存儲容量削減到當前尺寸
5.3.1 訪問數組列表元素
使用 ge t和 set 方法實現訪問或改變數組元素的操作,而不使用人們喜愛的[ ]語法格式。
例如,要設置第i個元素,可以使用:
staff.set(i,harry);
他等價於對數組a的元素賦值(數組 的下表從0開始);
只有i小於或等於數組列表的大小時,才能夠調用list.set(i, x)。例如,下面這段程式碼是錯誤的:
ArrayList<Employee> staff = new ArrayList<>(100); list.set(0,x);
使用add方法為數組添加新元素,而不要使用set方法,它只能替換數組中已經存在的元素內容。
使用下列格式獲取數組列表的元素:
Employee e = staff.get(i);
下面這個技巧可以一舉多得,既可以靈活地擴展數組,又可以方便地訪問數組元素。
首先,創建一個數組,並添加所有的元素:
ArrayList<X> list = new ArrayList<>();
while (...)
{
x = ...;
list.add(x);
}
執行完上述操作後,使用toArray方法將數組元素拷貝到一個數組中。
X[] a = new X[list.size()];
list.toArray(a);
除了在數組列表的尾部追加元素外,還可以在數組列表的中間插入元素,使用帶索引參數的add方法。
int n = staff.size()/2;
staff.add(n,e);
為了插入一個新元素,位於n之後的所有元素都要向後移動一個位置。如果插入新元素後,數組列表的大小超過了容量,數組列表就會被重新分配存儲空間。
同樣地,可以從數組列表中間刪除一個元素。
Employee e = staff.remove(n);
位於這個位置之後的所有元素都向前移動一個位置,並且數組的大小減1。
可以使用「 for each 」 循環遍曆數組列表:
for (Employee e : staff)
do something with e;
import java.util.ArrayList;
public class ArrayListTest {
public static void main(String[] args) {
ArrayList<Employee> staff = new ArrayList<>();
staff.add(new Employee("Carl Cracke",75000,1987,12,15));
staff.add(new Employee("Harry Hacker",50000,1989,10,1));
staff.add(new Employee("Tony Tester",40000,1990,3,15));
for(Employee e:staff)
e.raiseSalary(5);
for (Employee e :staff)
System.out.println("name="+e.getName()+",salary="+e.getSalary()+",hireDay="+e.getHireDay());
}
}
void set(int index)
設置數組列表指定位置的元素值,這個操作將覆蓋這個位置的原有內容。
參數:index 位置(必須介於0~size()-1之間)
e get(int index)
獲得指定位置的元素值。
參數:index 獲得的元素位置(必須介於0~size()-1之間)
void add(int index,E obj)
向後移動元素,以便插入元素
E remove(int index)
刪除一個元素,並將後面的元素向前移動。被刪除的元素由返回值返回。
參數:index 被刪除的元素位置(必須介於0~size()-1之間
5.3.2 類型化與原始數組列表的兼容性
在這一節學習了如何與沒有使用類型參數的遺留程式碼交互操作
假設有下面這個遺留下來的類:
public class EmployeeDb
{
public void update(ArrayList list){...}
public ArrayList find(String query){...}
}
可以將一個類型化的數組列表傳遞給update方法,而不需要進行任何類型轉換。
ArrayList<Employee> staff = ...;
employeeDB.update(staff);
也可以將staff對象傳遞給update方法。
使用類型轉換並不能避免警告。
一旦能確保不會造成嚴重的後果,可以用 @SuppressWarnings(“unchecked”) 標註來標記這個變數能夠接受類型轉換,如下所示:
@SuppressWarnings("unchecked") ArrayList<Employee> result =
(ArrayList<Employee> employeeDB.find(query));
5.4 對象包裝器與自動裝箱
有時,需要將 int 這樣的基本類型轉換為對象。所有的基本類型都有一個與之對應的類。例如,Integer 類對應基本類型 int 。通常,這些類稱為包裝器(wrapper)。這些對象包裝器類擁有很明顯的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和 Boolean(前6個類派生於公共的超類Number)。對象包裝器類是不可變的,即一旦構造了包裝器,就不允許更改包裝在其中的值。同時,對象包裝器類還是final,因此不能定義它們的子類。
假設想定義一個整型數組列表。而尖括弧中的類型參數不允許是基本類型,也就是說,不允許寫成 ArrayList
ArrayList<Integer> list = new ArrayList<>()
;
幸運的是,有一個很有用的特性,從而更加便於添加int類型的元素到ArrayList
list.add(3);
將自動地變成
list.add(Integer.valueOf(3));
這種變換被稱為 自動裝箱。
相反地,當將一個Integer對象賦給一個int值時,將會自動地拆箱。也就是說,編譯器將下列語句:
int n = list.get(i)
;
翻譯成
int n = list.get(i).intValue();
在算術表達式中也能自動裝箱和拆箱
兩個包裝器對象比較時調用 equals 方法 。
強調一下,裝箱和拆箱是編譯器認可的,而不是虛擬機。編譯器在生成類的位元組碼時,插入必要的方法調用。虛擬機只是執行這些位元組碼。
使用數值對象包裝器還有另外一個好處。Java設計者發現,可以將某些基本方法放置在包裝器中,例如,將一個數字字元串轉換成數值。
要想將字元串轉換成整型,可以使用下面這條語句:
int x = Integer.parseInt(s);
這裡與 Integer 對象沒有任何關係,parseInt 是一個靜態方法。但 Integer 類是放置這個方法的一個好地方。
int intValue()
以int的形式返回Integer對象的值(在Number類中覆蓋了intValue方法)。
static String toString(int i)
以一個新String對象的形式返回給定數值i的十進位表示。
static String toString(int i,int radix)
返回數值 i 的基於給定 radix 參數進位的表示。
static int parseInt(String s)
static int parseInt(String s,int radix)
返回字元串s表示的整型數值,給定字元串表示的是十進位的整數(第一種方法),或者是radix參數進位的整數(第二種方法)
static Integer valueOf(String s)
Static Integer value Of(String s,int radix)
返回用s表示的整型數值進行初始化後的一個新Integer對象,給定字元串表示的是十進位的整數(第一種方法),或者是radix參數進位的整數(第二種方法)
Number parse(String s)
返回數字值,假設給定的String表示了一個數值。
5.5 參數數量可變的方法
用戶自己也可以定義可變參數的方法,並將參數指定為任意類型,甚至是基本類型。
下面是一個簡單示例:其功能是計算若干個數值的最大值。
public static double max(double... values)
{
double largest = Double.NEGATIVE_INFINITY;
for(double v:values) if (v > largest) largest = v;
return largest;
}
可以像這樣調用方法:
double m = max(3.1,40.4,-5);
編譯器將 new double[] {3.1,40.4,-5}
傳遞給 max 方法。
5.6 枚舉類
例子:
public enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE};
實際上,這個聲明定義的類型是一個類,它剛好有4個實例,在此盡量不要構造新對象。
因此,在比較兩個枚舉類型的值時,永遠不需要調用 equals ,而直接使用「= =」就可以了。
如果需要的話,可以在枚舉類型中添加一些構造器、方法和域。當然,構造器只是在構造枚舉常量的時候被調用。下面是一個示例:
public enum Size
{
SAMLL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
private String abbreviation;
private Size(String abbreviation) {this.abbreviation = abbreviation;}
public String getAbbreviation(){return abbrevation;}
}
所有的枚舉類型都是Enum類的子類。它們繼承了這個類的許多方法。其中最有用的一個是 toString ,這個方法能夠返回枚舉常量名。例如,Size.SMALL.toString( ) 將返回字元串 「SMALL」 。
toString的逆方法是靜態方法valueOf。例如,語句:
Size s = Enum.valueOf(Size.class,"SMALL");
將s設置成了Size.SMALL
import java.util.Scanner;
public class EnumTest {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Enter a size:(SMALL.MEDIUM,LARGE,EXTRA_LARGE)");
String input = in.next().toUpperCase();
Size size = Enum.valueOf(Size.class, input);
System.out.println("size="+size);
System.out.println("abbreviation="+size.getAbbreviation());
if (size == Size.EXTRA_LARGE)
{
System.out.println("Good job--you paid attention to the _.");
}
}
}
enum Size
{
SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
private String abbreviation;
private Size(String abbreviation) {this.abbreviation = abbreviation;}
public String getAbbreviation(){return abbreviation;}
}
5.7 反射
反射庫(reflection library)提供了一個非常豐富且精心設計的工具集,以便編寫能夠動態操縱 Java 程式碼的程式。這項功能被大量地應用於 JavaBeans 中,它是Java組件的體系結構。使用反射,Java 可以支援 Visual Basic 用戶習慣使用的工具。特別是在設計或運行中添加新類時,能夠快速地應用開發工具動態地查詢新添加類的能力。
能夠分析類能力的程式稱為反射。
反射機制可以用來:
- 在運行時分析類的能力。
- 在運行時查看對象,例如,編寫一個 toString 方法供所有類使用。
- 實現通用的數組操作程式碼。
- 利用Method對象,這個對象很像C++中的函數指針。
反射是一種功能強大且複雜的機制。使用它的主要人員是工具構造者,而不是應用程式設計師。
5.7.1 Class類
在程式運行期間,Java運行時系統始終為所有的對象維護一個被稱為運行時的類型標識。這個資訊跟蹤著每個對象所屬的類。虛擬機利用運行時類型資訊選擇相應的方法執行。
保存這些資訊的類被稱為 Class ,Object 類中的 getClass( )方 法或者 Object.class 將會返回一個 Class 類型的實例。可以通過返回的 Classs 實例獲取該類的全類名、動態創建該 Class 的實例。(類/實例.getClass().getName() 、類/實例.getClass().newInstance())
最常用的 Class 方法是 getName 。這個方法將返回類的名字。
System.out.println(e.getClass().getName()+""+e.getName());
還可以用靜態方法 forName 獲得類名對應的Class對象
String className = "java.util.Random";
Class cl = Class.forName(className);
無論何時使用這個方法,都應該提供一個異常處理器。
一個類只有一個Class對象,虛擬機為每個類型管理 Class 對象,獲取方法有三種:
- 通過對象實例獲取;
Person p = new Person();
Class clazz = p.getClass();
- 通過forName()方法獲取
Class clazz = Class.forName("com.xxx.xxx.Person");
- 通過類名獲取
Class clazz = Person.class;
可以利用==運算符實現兩個類對象比較的操作。例如,
if (e.getClass() == Employee.class) . . .
還有一個很有用的方法 newInstance( ) ,可以用來動態地創建一個類的實例。例如,
e.getClass().newInstance();
創建了一個與 e 具有相同類類型的實例。newInstance 方法調用默認的構造器(沒有參數的構造器)初始化新創建的對象。如果這個類沒有默認的構造器,就會拋出一個異常。
5.7.2 捕獲異常
可以提供一個「捕獲」異常的處理器(handler)對異常情況進行處理。
異常有兩種類型:未檢查異常和已檢查異常。對於已檢查異常,編譯器將會檢查是否提供了處理器。然而,有很多常見的異常,例如,訪問 null 引用,都屬於未檢查異常。編譯器不會查看是否為這些錯誤提供了處理器。畢竟,應該精心地編寫程式碼來避免這些錯誤的發生,而不要將精力花在編寫異常處理器上。
現在,介紹一下如何實現最簡單的處理器。
將可能拋出已檢查異常的一個或多個方法調用程式碼放在try塊中,然後在catch子句中提供處理器程式碼。
try
{
statements that might throw exceptions
}
catch (Exception e)
{
handler action
}
下面是一個示例
try
{
String name = ...;
Class cl = Class.forName(name);
do something wih cl
}
catch (Exception e)
{
e.printStackTrace();
}
如果類名不存在,則將跳過 try 塊中的剩餘程式碼,程式直接進入 catch 子句(這裡,利用 Throwable 類的 printStackTrace 方法列印出棧的軌跡。Throwable 是 Exception 類的超類)。如果 try 塊中沒有拋出任何異常,那麼會跳過 catch 子句的處理器程式碼。
5.7.3 利用反射分析類的能力
下面簡要地介紹一下反射機制最重要的內容——檢查類的結構。
在 java.lang.reflect 包中有三個類 Field 、Method 和 Constructor 分別用於描述類的域、方法和構造器。這三個類都有一個叫做getName 的方法,用來返回項目的名稱。Field 類有一個 getType 方法,用來返回描述域所屬類型的 Class 對象。Method和Constructor 類有能夠報告參數類型的方法,Method 類還有一個可以報告返回類型的方法。這三個類還有一個叫做 getModifier s的方法,它將返回一個整型數值,用不同的位開關描述 public 和 static 這樣的修飾符使用狀況。另外,還可以利用 java.lang.reflect 包中的Modifier 類的靜態方法分析 getModifiers 返回的整型數值。例如,可以使用 Modifier 類中的 isPublic 、isPrivate 或 isFinal 判斷方法或構造器是否是 public 、private 或 final 。我們需要做的全部工作就是調用 Modifier 類的相應方法,並對返回的整型數值進行分析,另外,還可以利用 Modifier.toString 方法將修飾符列印出來。
Class 類中的 getFields 、getMethods 和 getConstructors 方法將分別返回類提供的 public 域、方法和構造器數組,其中包括超類的公有成員。Class 類的 getDeclareFields 、getDeclareMethods 和 getDeclaredConstructors 方法將分別返回類中聲明的全部域、方法和構造器,其中包括私有和受保護成員,但不包括超類的成員。
5.7.4 在運行時使用反射分析對象
查看對象域的關鍵方法是 Field 類中的 get 方法。如果f是一個 Field 類型的對象(例如,通過 getDeclaredFields 得到的對象), obj是某個包含 f 域的類的對象,f.get(obj) 將返回一個對象,其值為obj域的當前值。
反射機制的默認行為受限於 Java 的訪問控制。然而,如果一個 Java 程式沒有受到安全管理器的控制,就可以覆蓋訪問控制。為了達到這個目的,需要調用 Field、Method 或 Constructor 對象的 setAccessible 方法。例如,
f.setAccessible(true);
setAccessible 方法是 AccessibleObject 類中的一個方法,它是 Field、Method 和 Constructor 類的公共超類。這個特性是為調試、持久存儲和相似機制提供的。
5.8 繼承的設計技巧
1.將公共操作和域放在超類
這就是為什麼將姓名域放在 Person 類中,而沒有將它放在 Employee 和 Student 類中的原因。
2.不要使用受保護的域
有些程式設計師認為,將大多數的實例域定義為 protected 是一個不錯的主意,只有這樣,子類才能夠在需要的時候直接訪問它們。然而,protected 機制並不能夠帶來更好的保護,其原因主要有兩點。第一,子類集合是無限制的,任何一個人都能夠由某個類派生一個子類,並編寫程式碼以直接訪問 protected 的實例域,從而破壞了封裝性。第二,在Java程式設計語言中,在同一個包中的所有類都可以訪問proteced 域,而不管它是否為這個類的子類。
不過,protected 方法對於指示那些不提供一般用途而應在子類中重新定義的方法很有用。
3.使用繼承實現「is-a」關係
使用繼承很容易達到節省程式碼的目的,但有時候也被人們濫用了。例如,假設需要定義一個鐘點工類。鐘點工的資訊包含姓名和僱傭日期,但是沒有薪水。他們按小時計薪,並且不會因為拖延時間而獲得加薪。這似乎在誘導人們由 Employee 派生出子類 Contractor,然後再增加一個hourlyWage域。這並不是一個好主意。因為這樣一來,每個鐘點工對象中都包含了薪水和計時工資這兩個域。在實現列印支票或稅單方法的時候,會帶來無盡的麻煩,並且與不採用繼承,會多寫很多程式碼。鐘點工與僱員之間不屬於「is-a」關係。鐘點工不是特殊的僱員。
4.除非所有繼承的方法都有意義,否則不要使用繼承
5.在覆蓋方法時,不要改變預期的行為
6.使用多態,而非類型資訊
無論什麼時候,對於下面這種形式的程式碼
if(x is of type 1)
action1(x);
else if (x is type 2)
action2(x);
都應該考慮使用多態性。
action1 與 action2 表示的是相同的概念嗎?如果是相同的概念,就應該為這個概念定義一個方法,並將其放置在兩個類的超類或介面中,然後,就可以調用
x.action();
以便使用多態性提供的動態分派機制執行相應的動作。
使用多態方法或介面編寫的程式碼比使用對多種類型進行檢測的程式碼更加易於維護和擴展。
7.不要過多地使用反射
反射機制使得人們可以通過在運行時查看域和方法,讓人們編寫出更具有通用性的程式。這種功能對於編寫系統程式來說極其實用,但是通常不適於編寫應用程式。反射是很脆弱的,即編譯器很難幫助人們發現程式中的錯誤,因此只有在運行時才發現錯誤並導致異常。
總結
終於學完了這一章,花了大概三天,十二個小時左右,重新學了一遍,掌握了很多之前為了應付考試而沒有掌握的知識點,但是還是需要具體項目練手,最後的反射還是有很多知識點沒弄很清楚,打算看完整本書再回來看看 。接下來是兩個高級主題:介面和lambda表達式;繼續努力鴨ヾ(≧▽≦*)o