Java核心技術卷一 2. java對象與類
面向對象程序設計概述
面向對象程序設計(簡稱 OOP )是主流的程序設計範型,取代了早年的結構化過程化程序設計開發技術。Java 是完全面向對象的,必須熟悉 OOP 才能夠編寫 Java 程序。
面向對象的程序是由對象組成的,每個對象包含對用戶公開的特定功能部分和隱藏的實現部分。程序中的很多對象來自標準庫,還有一些自定義的。在 OOP 中,不必關心對象的具體實現,只要能夠滿足用戶的需求即可。
傳統的結構化程序設計首先要確定如何操作數據,然後在決定如何組織數據,以便於數據操作。而 OOP 將數據放在第一位,然後在考慮操作數據的算法。
面向對象更加適用於解決規模較大的問題。比如,如果實現一個簡單的 web 遊覽器可能需要大約 2000 個過程,這些過程需要對一組全局數據進行操作。采用面向對象的設計風格,可能只需要大約100個類、每個類平均包含20個方法,這更容易程序員掌握,也容易掌握 bug。對象的數據出錯了,只需要在數據項的20個方法中查找錯誤,這比在2000個過程中查找容易的多。
類與封裝
類(class)是構造對象的模版或藍圖。由類構造(construct)對象的過程稱為創建類的實例(instance)。標準的 java 庫提供了幾千個類。
封裝(encapsulation)是與對象有關的一個重要概念。形式上,封裝將數據和行為組合在一個包中,並對對象的使用者隱藏了數據的實現方式。對象中的數據稱為實例域(instance field),操縱數據的過程稱為方法(method)。對於每個特定的類實例(對象)都有一組特定的實例域值。這些值的集合就是對象的當前狀態(state)。無論何時,只要向對象發送一個信息,他的狀態就有可能發生改變。
實現封裝的關鍵在於絕對不能讓類中的方法直接地訪問其他類的實例域。程序僅通過對象的方法與對象數據進行交互。封裝給對象賦予了“黑盒”特征,這是提高重用性和可靠性的關鍵。
可以通過擴展一個類來建立另外一個新的類,這個過程稱為繼承,所有的類都源自於一個超類 Object。
對象
對象的三個主要特性:
- 對象的行為——可對對象施加的操作或方法
- 對象的狀態——施加方法時,如何響應
- 對象標識——辨別具有相同行為與狀態的不同對象
同一個類的所有實例對象,具有家族式的相似性。對象的行為是用可調用的方法定義的。
對象保存著描述當前特征的信息,這是對象的狀態,對象狀態的改變必須通過調用方法實現。
對象的狀態並不能完全描述一個對象。每個對象都有一個唯一的身份。作為類的一個實例,每個對象的標識永遠是不同的,狀態也嘗嘗存在著差異。
對象的關鍵特性在彼此之間項目影響著。例如,對象的狀態影響它的行為(行為肯定會根據狀態做出不同的操作)
識別類
面向對象程序設計,沒有所謂的從頂部的 main 函數開始編寫。首先從設計類開始,然後再往每個類中添加方法。
比如在訂單處理系統中,有這樣一些名詞:
- 項目
- 訂單
- 送貨地址
- 付款
- 賬戶
還有一些動詞:
- 添加
- 發送
- 取消
- 支付
對應的名詞是類的一個參數,動詞應該是類的一個方法。
類之間的關系
類之間常見的關系:
- 依賴
uses-a
- 聚合
has-a
- 繼承
is-a
依賴(dependence):是一種最明顯的、最常見的關系。如,A類使用B類因為A對象需要訪問B對象查看狀態。如果一個類的方法操縱另一個類的對象,就可以說一個類依賴於另一個類。盡可能將相互依賴的類減至最少,讓類之間的耦合度最小。
聚合(aggregation):聚合關系意味著類A的對象包含類B的對象。整體與部分之間是可以分離的。
繼承(inheritance):如果類A擴展類B,類A不但包含類B繼承的方法,還會擁有 一個額外的功能。
UML符號:
繼承 ---------|>
接口實現 - - - - - |>
依賴 - - - - - >
聚合 <>---------
關聯 -----------
直接關聯 ---------->
使用預定義類
並不是所有類都有面向對象特征,比如 Math 類,只需要知道方法名和參數,不需要知道了解它的具體過程,這正是封裝的關鍵所在。Math 類只封裝了功能,它不需要也不必隱藏數據。由於沒有數據,因此也不必擔心生成對象以及初始化實例域。
對象與對象變量
使用對象,首先構造對象,使用構造器構造新實例,然後可以對對象應用方法。
new Date();
new Date().toString();
通常,希望構造的對象可以多次使用,因此,需要將對象存放在一個變量中。此時,birthday 就是對象變量,對象變量初始化之後的值是對存儲在另外一個地方的一個對象的引用。
Date birthday = new Date();
birthday.toString();
未初始化的對象變量不能調用對象的方法,否則會編譯錯誤;對象變量如果賦值為null
則表明對象變量目前沒有引用任何對象,此時調用對象的方法會產生運行錯誤。
Date birthday;
birthday.toString();//編譯錯誤
birthday = null;
birthday.toString();//運行錯誤
日歷類
Date類的實例有一個狀態,即特定的時間點。
時間是用距離一個固定時間點的毫秒數(可正可負),這個點成為紀元,UTC 的時間為1970年1月1日 00:00:00
。
有個日歷表示法類LocalDate
類,不要使用構造器來構造LocalDate
類的對象。
LocalDate.now();//靜態工廠方法,表示構造這個對象時的日期。
LocalDate.of(1999, 12, 31);//使用年月日來構造一個日期。
LocalDate newYearsEve = LocalDate.of(1999, 12, 31);//保存在一個對象變量中。
int year = newYearsEve.getYear();//獲取年
int month = newYearsEve.getMonthValue();//獲取月
int day = newYearsEve.getDayOfMonth();//獲取日
LocalDate aThousandDaysLater = new newYearsEve.plusDays(1000);//距離當前日期1000天的日期。
更改器方法與訪問器方法
使用類的 get 方法可以返回對象的狀態,使用類的 set 方法可以改變對象的狀態。通常在更改器名前面加上前綴 set ,在訪問器名前面加上前綴 get 。
用戶自定義類
想創建一個完整的程序,因該將若幹類組合在一起,其中只有一個類有 main 方法。
Employee 類
簡單的 Employee 類:
class Employee{
// instance fields
private String name;
private double salary;
private Date hireDay;
// constructor
public Employee(String n, double s, int year, int month, int day){
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
hireDay = calendar.getTime();
}
// method
public String getName(){
return name;
}
}
實際用處,構造了一個類數組,並填入了三個雇員信息。
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker",...);
staff[1] = new Employee("Harry Hacker",...);
staff[2] = new Employee("Tony Tester",...);
利用 raiseSalary 方法將每個雇員的薪水提高 5%
for (Employee e : staff){
e.raiseSalary(5);
}
最後調用,get 方法,或者,toString 方法將每個雇員的信息打印出來。
for (Employee e : staff){
System.out.println("name=" + e.getName()
+ ",salary=" + e.getSalary
+ ",hireDay=" + e.getHireDay());
// e.toString();
}
多個源文件的使用
在一個源文件中,只能有一個公有類,但可以有任意數目的非公有類。
程序員習慣於將每一個類存在一個單獨的文件中。
此時,就會有多個文件,當我們執行一個程序時,在命令行只需要編譯程序的入口。當程序的入口使用到其他類時首先會查找.class
的文件。如果沒有找到這個文件,就會自動搜索.java
文件,對它進行編譯。如果.java
比.class
版本新,java 編譯器會自動地重新編譯這個文件。
構造器
我們可以在構造的同事給實例域初始化為所希望的狀態。
構造器的註意點:
- 構造器與類同名
- 每個類可以有一個以上的構造器,用戶不定義構造器,編譯器會自動生成構造器
- 構造器可以有0個、1個或多個參數
- 構造器沒有返回值
- 構造器總是伴隨著 new 操作一起調用
隱式參數與顯式參數
方法可以操作實例域,對象變量調用類的方法存取它的實例域。
類的方法有兩種參數:
- 隱式參數:出現在方法名前的 Employee 類對象。
- 顯式參數:方面括號內定義聲明的數值。
public void raiseSalary(boucle byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
每一個方法中,關鍵字 this 表示隱式參數,帶上 this 可以將實例域與局部變量明顯的區分開來。
如,在構造函數中,利用 this 將局部參數賦值給同名的隱式參數。
// instance fields
private String name;
private double salary;
private Date hireDay;
// constructor
public Employee(String name, double salary, int year, int month, int day){
this.name = name;
this.salary = salary;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
this.hireDay = calendar.getTime();
}
封裝的優點
get 方法只返回實例域的值,又稱為域訪問器。
為了防止破壞這個域值的搗亂者,因該提供下面三項內容:
- 一個私有的數據域
- 一個公有的域訪問器方法
- 一個私有的域更改器方法
封裝的優點:
- 可以改變內部實現,除了該方法之外,不會影響其他代碼。
- 更改器方法可以執行錯誤檢察,直接對域進行賦值不會進行這些處理。
註意:不要編寫返回引用可變對象的訪問器方法,會破壞程序的封裝性,如果需要返回一個可變對象的引用,因該首先對它進行克隆,對象 clone 是指存放在另一個位置上的對象的副本。
class Employee{
public Date getHireDay(){
return hireDay.clone();
}
}
基於類的訪問權限
一個方法可以訪問所屬類的所有對象的私有數據。A類的方法可以訪問A類的任何一個對象的私有域。
私有方法
方法可以調用對象的私有數據。一個方法可以訪問所屬類的所有對象的私有數據。
在某些特殊情況下,可以將方法設計為私有。如一些輔助方法,它們只是用於自身的方法內使用,因該使用 private 的修飾符,這些輔助方法不應該成為公有接口的一部分。
如果未來這個類不需要這個私有方法,可以直接刪去,如果是公有方法就不能隨便刪除,因為其他文件的代碼有可能依賴它,如果是私有的我們完全不對單位外部對它的依賴。
final 實例域
實例域定義為 final ,每一個構造器執行之後,實例域的值被設置,並且後面的操作不能對它進行更改。
final 修飾符大都應用於基本類型域或不可變類的域(如 String)。
靜態域和靜態方法
main 方法都被標記為 static 修飾符。
靜態域
如果一個類中定義了一個靜態域和一個實例域,並且生成了1000個類的對象,則此時有1000個實例域。但是只有一個靜態域,即使沒有一個類對象生成,靜態域也存在。
- 即使沒有一個類對象生成,靜態域也存在
- 靜態域屬於類,不屬於任何對象
- 靜態域內的變量改變是永久的,不隨著新對象的生成改變
- 靜態域可以被對象調用
class Employee{
private static int nextId = 1;
private int id;
}
靜態常量
靜態變量使用的比較少,但靜態常量卻使用的比較多,例如,Math 類中的 PI 。
好處:
- 靜態常量是常量不可修改
- 靜態常量是靜態域屬於類,不需要生成對象即可使用
- 靜態常量可以被對象調用
public class Math{
public static final double PI = 3.14.....;
}
靜態方法
靜態方法是一種不能向對象實施操作的方法,也就是沒有隱式的參數,可以認為靜態方法是沒有 this 參數的方法,但是靜態方法可以訪問靜態域。
- 靜態方法不可以操作對象,沒有隱式的參數
- 靜態方法可以訪問靜態域
- 靜態方法可以被對象調用,靜態方法與對象之間沒有任何關系,所有為了避免混亂,建議使用類名來調用靜態方法。
public static int getNextId(){
return nextId;
}
以下兩種情況使用靜態方法:
- 一個方法不需要訪問對象狀態,其所需參數都是通過顯式參數提供的。
- 一個方法只需要訪問類的靜態域。
工廠方法
一個類可以使用靜態工廠方法來構造對象。
public final class LocalDate
implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
...
public static LocalDate now(Clock clock) {
Objects.requireNonNull(clock, "clock");
// inline to avoid creating object and Instant checks
final Instant now = clock.instant(); // called once
ZoneOffset offset = clock.getZone().getRules().getOffset(now);
long epochSec = now.getEpochSecond() + offset.getTotalSeconds(); // overflow caught later
long epochDay = Math.floorDiv(epochSec, SECONDS_PER_DAY);
return LocalDate.ofEpochDay(epochDay);
}
public static LocalDate of(int year, Month month, int dayOfMonth) {
YEAR.checkValidValue(year);
Objects.requireNonNull(month, "month");
DAY_OF_MONTH.checkValidValue(dayOfMonth);
return create(year, month.getValue(), dayOfMonth);
}
...
}
這樣構造日期對象:
LocalDate.now();//靜態工廠方法,表示構造這個對象時的日期。
LocalDate.of(1999, 12, 31);//使用年月日來構造一個日期。
方法參數
按值調用:方法接收的是調用者提供的值。
按引用調用:方法接收的是調用者提供的變量地址。
方法參數共有兩個類型:基本數據類型、對象引用。
java 語言總是采用按值調用的程序設計語言。
方法參數分為兩種類型:
- 基本數據類型
- 對象引用
基本數據類型
假設有一個方法可以將參數值增加3倍:
public static void tripleValue(double x){
x = 3 * x;
}
double percent = 10;
tripleVlaue(percent);
調用這個方法之後,percent
的值還是 10 。
執行過程:
- x 被初始化為 percent 的一個拷貝。
- x 被乘以 3 後等於 30。percent 還是 10 。
- 方法結束 x 不再使用。
總結:一個方法不肯能修改一個基本數據類型的參數。
對象引用
以下代碼可以將雇員的薪水提高兩倍:
public static void tripleSalary(Employee x){
x.raiseSalary(200);
}
Employee harry = new Employee(...);
tripleSalary(harry);
執行過程:
- x 被初始化為 harry 值的拷貝,這是對象的引用的拷貝。
- raiseSalary 方法應用對象的引用,x 和 harry 同時引用的對象的薪水提高了 200%。
- 方法結束後 x 不在使用。harry 指向的引用以改變。
總結:方法得到了對象引用的拷貝,對象引用及其他的拷貝同時引用同一個對象。
java 對對象采用的也是按值傳遞
引用類型獲取的不是對象的地址,得到的是對象引用的拷貝,對象引用及其他的拷貝引用同一個對象。
下面一個例子來說明 java 不是按引用傳遞:
private static void swap(Employee x, Employee y) {
// 此時,x 指向 對象 a,y 指向對象 b
// 賦值,改變了 x 與 y 引用的對象,但是 a 與 b 引用的對象不變。
// 所以 java 都是按值傳參的
Employee temp = x;
x = y;
y = temp;
// 此時,x 指向 對象 b,y 指向對象 a
// 如果 x 與 y 使用 set 改變了引用對象的值, a 與 b 引用的對象也改變,因為他們引用同一個對象。
x.setName("x"); // 改變了對象 b 的值
y.setName("y"); // 改變了對象 a 的值
}
public static void main(String[] args) {
Employee a = new Employee("a");
Employee b = new Employee("b");
//此時,a 指向對象a,b 指向對象 b
swap(a,b);
//此時,a 指向對象 a,b 指向對象 b
System.out.println(a.getName());// y
System.out.println(b.getName());// x
}
總結方法參數的使用:
- 一個方法不能修改一個基本數據類型的參數
- 一個方法可以改變一個對象的參數的狀態
- 一個方法不能讓對象參數引用一個新的對象,改變的知識自身的引用。
對象構造
重載
如果多個方法有相同的名字、不同的參數(不同的參數類型或數量),便產生了重載。
編譯器通過各個參數給出的參數類型與特定方法調用所使用的值類型進行匹配來挑選相應的方法。
註意:實現重載返回值也可以不同。但只有返回值不同不是重載的要求,不能有兩個名字相同、參數類型也相同卻返回不同類型值的方法。
默認域初始化
如果構造器沒有顯式地給域賦予初值,那麽就會被自動地賦予默認值(0、false、null)。這是一個不好的習慣,如果一個對象默認值為 null ,調用 get 方法會得到一個 null 引用,這不是我們所希望的結果。
無參數的構造函數
即默認的構造器,當沒有其他構造器時,系統默認自動創建,這個構造器將所有的實例域設置為默認值。
顯示域初始化
確保不管怎麽調用構造器,每個實例域都有自己的初始值,可以在類定義中將一個值賦給任何域。
class Employee{
private String name = "";
}
參數名
參數變量用同樣的名字將實例域屏蔽起來,可以使用 this 指定隱式參數。
public Employee(String name, double salary){
this.name = name;
this.salary = salary;
}
調用另一個構造器
關鍵字 this 可以引用方法的隱式參數,還有另一個含義,如果構造函數的第一句是this(...)
,這個構造器將調用同一個類的另一個構造器。
public Employee(double s){
this(name, s);
name++;
}
采用這種方法使用 this 關鍵字非常有用,這樣對公共的構造器代碼部分只編寫一次即可。
初始化塊
三種初始化數據域的方法:
- 在構造器中設置值
- 在聲明中賦值
- 在初始化塊中設置值
在一個類的聲明中,可以包含多個代碼塊。只要構造類的對象,這些代碼塊就會被執行。
class Employee{
private static int nextId;
private int id;
private String name;
private double salary;
{
id = nextId;
nextId++;
}
public Employee(String n, double s){
name = n;
salary = s;
}
public Employee(){
name = "";
salary = 0;
}
}
這種機制不是必須的,也不常見,通常都是將初始化代碼放到構造器中。
在Java中,有兩種初始化塊:靜態初始化塊和非靜態初始化塊
靜態初始化塊:使用static
定義,當類裝載到系統時執行一次。若在靜態初始化塊中想初始化變量,那僅能初始化靜態類變量,即static修飾的數據成員。
非靜態初始化塊:在每個對象生成時都會被執行一次,可以初始化類的實例變量。
生成對象時,執行的順序:
- 所有數據域初始化為默認值
- 只在第一次生成對象時,調用靜態初始化塊(只能初始化靜態變量)
- 調用非靜態初始化塊(都能初始化)
- 調用構造函數主體代碼
存在繼承關系時,父類與子類的執行順序:
先初始化父類的靜態代碼 ---> 初始化子類的靜態代碼 --->
初始化父類的非靜態代碼 ---> 初始化父類構造函數 --->
初始化子類非靜態代碼 ---> 初始化子類構造函數
對象析構與 finalize 方法
C++有顯式的析構器方法,放置一些不再使用時需要執行的清理代碼。
java有自動的垃圾回收器,不需要人工回收內存,所以 java 不支持析構器。
可以給java的類添加 finalize 方法,他會在垃圾回收器清除對象之前調用。
包
java 允許使用包將類組織起來,便於組織代碼,分開管理,使用包的主要原因是確保類名的唯一性。
可以使用嵌套層次組織包,所有標準的 Java 包都處於 java 和 javax 包層次中。
常用的 java 類庫的包:
java.lang -- 語言包:Java語言的基礎類,包括Object類、Thread類、String、Math、System、Runtime、Class、Exception、Process等;
java.io -- 輸入輸出包:提供與流相關的各種包;
java.awt -- 抽象窗口工具包:Java的GUI類庫,一般網絡開發用不上
java.util -- 實用工具包:Scanner、Date、Calendar、LinkedList、Hashtable、Stack、TreeSet等;
java.net -- 網絡功能包:URL、Socket、ServerSocket等;
java.sql -- 數據庫連接包:實現JDBC的類庫;
java.text -- 文本包:Format、DataFormat等。
類的導入
一個類可以使用所屬包中的所有類,以及其他包的公有類。
訪問別的包公有類的方法:
java.util.Date today = new java.util.Date();
import java.util.Date;
Date today = new Date();
使用*
可以導入一個包中的所有類
import java.util.*
但是不能用*
導入以java為前綴的包,如:
import java.*;
import java.*.*;
如果兩個包中有相同類名,會發生沖突,導致出錯,如,util與sql都有Date類:
import java.util.*;
import java.sql.*;
Date today;// ERROR java.util.Date or java.sql.Date ?
//可以增加一個特性的類解決
import java.util.*;
import java.sql.*;
import java.util.Date;
Date today;
//如果都需要用到就要加上特定的類名了
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date(...);
靜態導入
import 還可以導入靜態方法和靜態域。
如在開通增加一條指令:
import static java.lang.Math.*;
就可以直接使用 Math 的靜態域和靜態方法,如
sqrt(pow(x, 2) + pow(y, 2))
講類放入包中
直接看代碼:
package com.xul.javaPrimary;
此時類文件放入了對應的包裏,默認情況下,類放在沒有名字的默認包裏(defaulf package)。
包作用域
標記為 public 的部分可以被任意的類使用;
標記為 private 的部分只能被定義他們的類使用;
沒有定義修飾符的可以被同一個包中的所有方法訪問。
類路徑
Java 類路徑告訴 java 解釋器和 javac 編譯器去哪裏找它們要執行或導入的類。類(您可能註意到的那些 *.class 文件)可以存儲在目錄或 jar 文件中,或者存儲在兩者的組合中,但是只有在它們位於類路徑中的某個地方時,Java 編譯器或解釋器才可以找到它們。
設置類路徑
java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyProg
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg
文檔註釋
JDK 的工具 javadoc,可以由源文件生成一個 HTML 文檔。
註釋的插入
javadoc 實用程序從下面幾個特性中抽取信息:
- 包
- 公有類與接口
- 公有的和受保護的構造器及方法
- 公有的和受保護的域
註釋以/**
開始,並以*/
結束。
文檔後面緊跟自由格式文本,標記由@
開始。
第一句通常是概要性的句子,可以通過 HTML 標簽修飾
類註釋
類註釋必須要 import 語句之後,類定義之前。
/**
* adfasdfasdfasdfasdf
* adfsdfasfsdafsadfsdaf
*/
public class Card{
...
}
方法註釋
方法註釋必須放在所描述的方法之前。除了通用標記外,還可以使用下面標記:
- @param 變量描述
- @return 描述
- @throws 類描述
域註釋
只需要對公有域(通常為靜態常量)建立文檔。
通用註釋
可以在類文檔中的標記:
- @author 姓名 對應一名作者
- @version 文本 當前版本的描述
可以用於所有文檔中的標記:
@since 文本 對引入特性的版本描述
@deprecated 文本 標記對類、方法或變量添加一個不再使用的註釋,應給出取代建議。
@deprecated Use <code> setVisible(true) </code> instead
@see 和 @link 標記,增加超鏈接
包與註釋概述
可以直接將類、方法和變量的註釋放到源文件中就可以了。想要產生包註釋,就需要在每一個包目錄中添加一個單獨的文件。
- 提供一個以 package.html 命名的 HTML 文件。在標記
<body></body>
之間所有文本都會被抽取出來。 - 提供一個以 package-info.java 命名的 JAVA 文件。文件必須包含
/** .. */
的 javadoc 註釋,跟隨在一個包語句之後。
還可以為所有的源文件添加概述性的註釋。放置在名為 overview.html 文件中。位於包含所有文件的父目錄中,在標記<body></body>
之間所有文本都會被抽取出來。用戶從導航欄中點擊 Overview 時就會被顯示。
註釋的抽取
假設抽取的文件放到 docDirectory 下:
切換到包含要生成文檔的源文件目錄。
如果是一個包
javadoc -d docDirectory nameOfPackage
如果文件在默認包中
javadoc -d docDirectory *.java
類設計技巧
讓類更有 OOP 專業水準:
- 一定要保證數據私有
- 一定要對數據初始化
- 不要在類中使用過多的基本類型,使用其他類代替多個相關的基本類型的使用
- 不是所有的域都需要獨立的域訪問器和域更改器
- 將職責過多的類進行分解
- 類名和方法名要能夠體現他們的職責
Java核心技術卷一 2. java對象與類