《Java核心技術 卷Ⅰ》 第4章 對象與類html
傳統的結構化程序設計:首先肯定如何操做數據,再決定如何組織數據。前端
面向對象程序設計:將數據放在第一位,再考慮操做數據的算法。java
類(class)是構造對象的模板或藍圖,
由類構造(construct)對象的過程稱爲建立類的實例(instance)。
封裝(encapsulation),也稱數據隱藏,封裝將數據和行爲組合在一個包中,並對對象使用者隱藏數據實現方式,對象中的數據域稱爲實例域(instance field),操做數據的過程稱爲方法(method)。git
對於每一個特定的類實例(對象)都有一組特定的實例域值,這些值的集合就是這個對象的當前狀態(state),只要向對象發送一個消息,它的狀態就有可能發生改變。程序員
實現封裝的關鍵:絕對不能讓類中的方法直接地訪問其餘類的實例域。github
OOP的另外一個原則:能夠經過擴展一個類來創建另一個新的類。在Java中,全部類都源於一個超類——Object
。算法
在擴展一個已有類時,新類具備這個類的所有屬性和方法,在新類中,只須要提供適用於這個新類的新方法和數據域就能夠了,這個過程稱爲繼承(inheritance)。sql
對象的三個主要特性:安全
識別類的簡單規則:併發
常見的關係有:
Customer "use-a" MobilePhone
。Programmer "has-a" Coffee
。Student "is-a" Person.
。想要使用對象,就必須首先構造對象,並指定其初始狀態,
而後對對象應用方法。
在Java中,使用構造器(constructor)構造新實例,它是一種特殊的方法,用於構造並初始化對象。
Date birthday = new Date(); String s = birthday.toString();
對象變量並無實際包含一個對象,而僅僅是一種引用,在Java中,任何對象變量的值都是對存儲在另一個地方的一個對象的引用,new操做符的返回值也是一個引用。
當一個對象變量只是聲明可是沒有具體的引用對象時,調用其方法會在編譯時產生變量未初始化錯誤。
// Error test P1 Date deadline; deadline.toString();
當一個對象變量只是聲明可是沒有具體的引用對象時,調用其方法會產生運行時錯誤(一般爲java.lang.NullPointerException
)。
// Error test P2 Date deadline = null; deadline.toString();
上面兩個例子說明,Java中的局部變量並不會自動地初始化爲null
,而必須經過調用new
或將他們設置爲null
進行初始化。
Date類的實例有一個狀態,即特定的時間點。
時間是距離紀元(epoch)的毫秒數(可正可負),紀元是UTC(Coordinated Universal Time)時間1970年1月1日 00:00:00。
類庫設計者把保存時間與給時間點命名分開,因此標準Java類庫分別包含了兩個類:
Date
類LocalDate
類不要使用構造器來構造LocalDate類的對象,應用靜態工廠方法(factory method)表明調用構造器。
// 當前時間的對象 LocalDate.now(); // 指定時間的對象 LocalDate.of(1996, 6, 30); // 保存對象 LocalDate birthday = LocalDate.of(1996, 6, 30);
有了對象就可使用方法得到年、月、日。
int year = birthday.getYear(); // 1996 int month = birthday.getMonthValue(); // 6 int day = birthday.getDayOfMonth(); // 30 int dayOfWeek = birthday.getDayOfWeek().getValue(); // 7
須要計算某個日期時:
LocalDate someday = birthday.plusDays(708); int year = someday.getYear(); // 1998 int month = someday.getMonthValue(); // 6 int day = someday.getDayOfMonth(); // 8 // 固然還有minusDays方法
Java簡單類的形式:
class ClassName { filed1 field2 ... constructor1 constructor2 ... method1 method2 ... }
一個使用簡單類的程序例子:
// File EmployeeTest.java public class EmployeeTest { public static void main(String[] args) { Employee[] staff = new Employee[3]; staff[0] = new Employee("Bob Hacker", 75000, 1996, 6, 30); ... } } class Employee { // instance fields private String name; private double salary; private LocalDate hireDay; // constructor public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } // methods public String getName() { return name; } ... }
注意,這個程序中包含兩個類:
Employee
類public
訪問修飾符的EmployeeTest
類源文件名是EmployeeTest.java
,這是由於文件名必須與public類的名字相匹配,在一個源文件中,只能有一個公有類,但能夠有任意個非公有類。
當編譯這段源碼時,編譯器會在目錄下生成兩個類文件:EmployeeTest.class
和 Employee.class
。
將程序中包含main方法的類名提供給字節碼解釋器,啓動程序:
java EmployeeTest
字節碼解釋器開始運行其中的main方法的代碼。
<!-- ### 多個源文件的使用
當習慣把各種單獨放在一個文件中時,好比上面的程序中,建立一個文件Employee.java
單獨存放這個類,可是在編譯時有兩種方法。
一是使用通配符將全部文件編譯:
javac Employee*.java
一種是編譯包含main方法的類:
javac EmployeeTest.java
後一種方法並無顯式地編譯Employee.java
,當Java編譯器發現EmployeeTest.java
使用了Employee
類時,會查找名爲Employee.class
的文件,若是沒有找到,就會自動搜索Employee.java
,而後自動的進行編譯。
而且當Employee.java
文件更新後,Java編譯器在編譯時會自動地從新編譯該文件。 -->
剛纔所使用類中的構造器:
class Employee { ... public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } ... }
構造器與類同名,在構造Employee
類對象時,對應的構造器會運行,執行代碼將實例域初始化所指定的狀態。
構造器與方法的其中一個不一樣是,構造器老是伴隨new
操做符的執行被調用,而且不能對已經存在的對象調用構造器來從新設置實例域。
Employee bob = new Employee("Bob", 47000, ...); bob.Employee("Bob", 47500, ...); // Compiler Error: Can't find symbol of method Person(String, int, ...)
構造器基礎的簡單總結:
方法用於操做對象以及存取他們的示例域。
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; }
當對象調用方法時
bob.raiseSalary(5);
raiseSalary
方法有兩個參數。
Employee
類的對象。在每個方法中,關鍵字this
表示隱式參數,上面的方法也能夠寫爲:
public void raiseSalary(double byPercent) { // double raise = salary * byPercent / 100; double raise = this.salary * byPercent / 100; // salary += raise; this.salary += raise; }
有些人偏心這樣寫(包括我),雖然費事點,可是能夠將實例域與局部變量明顯的區分開來。
封裝對於直接簡單的公有數據而言,提供了更多對公有數據保護的途徑。
對於訪問器來講,它們只返回實例域的值,而且在處理可引用的返回對象時,要經過clone
來建立新的對象來做爲返回值的載體,若是將可引用對象直接返回,而且該對象恰有一個可修改值的方法時,任何外部對這個返回值的處理都將會直接影響到這個對象內部的對象(Java引用在這部分的狀況相似與C中的指針)。
對於更改器來講,它們在被調用時能夠主動的執行數據合法性的檢查,從而避免破壞數據的合法性。
方法能夠訪問所調用對象的私有數據。
可是Java其實還要更進一步:一個方法能夠訪問所屬類的全部對象的私有數據。
// class class Employee { public boolean equals(Employee other) { return name.equals(other.name); } } ... // main if(harry.equals(boss)) ...
這個方法訪問harry
的私有域,同時它還訪問了boss
的私有域,這是合法的,boss
也是Employee
類對象,Employee
類的方法能夠訪問Employee
類的任何一個對象的私有域。
有時候爲了完成任務須要寫一些輔助方法,這些輔助方法不該該稱爲公有接口的一部分,這是因爲它們與當前的實現機制很是緊密,最好將這樣的方法設計爲private
。
簡單來講,爲了更好地封裝性,不在公有接口範圍內的方法都應該設計爲private
。
類中能夠定義實例域爲final
,可是必須確保在每個構造器執行以後,這個域的值會被設置,並在後面的操做中不能再對其進行修改。
可是這裏的不能修改大都應用於基本(primitive)類型和不可變(immutable)類型的域(若是類中每一個方法都不會改變對象狀態,則類就是不可變的類,例如String
類)。
對於可變的類(好比以前的StringBuilder
類),使用final
修飾符只是表示該變量的對象引用不會再指示其餘的對象,但其對象自己是能夠更改的(好比StringBuilder
類的對象執行append
方法)。
若是將一個域定義爲
static
,每一個類中只有這樣的一個域。
通俗來說,若是一個域被定義爲static
,那麼這個域屬於這個類,而不屬於任何這個類的對象,這些對象同時共享這個域(有點像類的一個全局變量域)。
一個簡單的靜態域用法:
// class Employee ... // 能夠在類定義中直接對靜態域賦予一個初值。 private static int nextId = 1; private int id; ... public void setId() { id = nextId; nextId++; }
靜態常量相比於靜態變量使用的要多一些。
例如Math
類中的PI
:
public class Math { ... publuc static final double PI = 3.14159265358979323846; ... }
程序經過Math.PI
的形式得到這個常量。
靜態方法是一種不能向對象實施操做的方法。
靜態方法在調用時,不使用任何實例對象,換句話說就是沒有隱式參數。
須要使用靜態方法的狀況:
好比以前LocalDate
類使用的靜態工廠方法(factory method)來構造對象。
不利用構造器完成這個操做的兩個緣由:
main
方法不對任何對象進行操做,由於事實上在啓動程序時尚未任何一個對象,靜態的main
方法將隨着執行建立程序所須要的對象。
同時,每個類均可以有一個main
方法,經常使用於進行類的單元測試。
在程序設計語言中有關參數傳遞給方法(函數)的一些專業術語:
Java程序設計語言老是採用按值調用,即方法獲得的只是參數值的一個拷貝,不能修改傳遞給它的任何參數變量的內容。
可是當對象引用做爲參數時,狀況就不一樣了,方法得到的是對象引用的拷貝,對象引用和其餘拷貝同時引用同一個對象。
可是這並非引用調用。
public static void swap(Obejct a, Obejct b) { Object tmp = a; a = b; b = tmp; }
若是Java在對象參數時採用的是按引用調用,上述方法就能實現交換數據的效果。
可是這裏的swap
方法並無改變存儲在調用參數中的對象引用,swap
方法的參數a
和b
被初始化爲兩個對象引用的拷貝,這個方法交換的是這兩個拷貝的引用。
Java中方法參數總結:
有些類有多個多個構造器。
這種特徵叫作重載(overloading),若是多個方法有相同的名字、不一樣的參數,便產生了重載。
編譯器經過用各個方法給出的參數類型與特定方法調用所使用的值類型進行匹配來挑選出相應的方法,若是編譯器找不到匹配的參數,就會產生編譯時錯誤。
Java容許重載任何方法,並不僅是構造器,所以要完整地描述一個方法,須要指出方法名以及參數類型,這叫方法的簽名(signature)。
// 方法重載的簽名舉例 indexOf(int) indexOf(int int) indexOf(String)
能夠看出,返回類型並非方法簽名的一部分,即不能有兩個名字相同、參數類型相同可是卻返回不一樣類型值的方法。
若是域沒有在構造器中被賦予初值,則會被自動地賦予默認值:
這與局部變量的聲明不一樣,局部變量必須明確的進行初始化。
構造器中若是不明確地進行初始化,會影響代碼的可讀性。
若是在編寫一個類時沒有編寫構造器,那麼系統會提供一個無參數構造器,這個構造器將全部的實例域設置爲默認值。
若是類中提供了至少一個構造器,可是沒有提供無參數構造器,則構造對象時若是沒有提供參數就會被視爲不合法。
經過重載類的構造器方法,能夠採用多種形式設置類的實例域的初始狀態。
能夠在類定義中,直接講一個值賦予給任何域。
class Employee { private String name = ""; ... }
在構造器執行以前,先執行賦值操做。
初始值也能夠不用是常量。
class Employee { private static int nextId; private int id = assignId(); ... private static int assignId() { int r = nextId; nextId++; return r; } ... }
上面的例子中,能夠調用方法對域進行初始化。
在編寫很小的構造器時,一般用單個字符命名:
public Employee(String n, double s) { name = n; salary = s; }
這樣的缺陷是失去了代碼可讀性,也能夠採用加前綴的方法:
public Employee(String aName, double aSalary) { name = aName; salary = aSalary; }
固然還有一種技巧,參數變量用一樣的名字將實例域屏蔽起來:
public Employee(String name, double salary) { this.name = name; this.salary = salary; }
若是構造器的第一個語句形如this(...)
,這個構造器將調用同一個類的另外一個構造器。
public Employee(double s) { // calls Employee(String, double) this("Employee #" + nextId, s); nextId++; }
除了前面提到的兩種初始化數據域的方法:
還有第三種,稱爲初始化塊(initialization block),在類定義中能夠包含多個代碼塊,只要構造類的對象,這些塊就會被執行。
class Employee { private static int nextId = 0; private int id; { id = nextId; nextId++; } ... }
不管哪一個構造器構造對象,初始化塊都會執行,首先運行初始化塊,而後才運行構造器的主體部分。
調用構造器的具體處理步驟:
初始化塊比較經常使用於代碼比較複雜的靜態域初始化:
static { Random generator = new Random(); nextId = generator.nextInt(10000); }
Java容許使用包(package)將類組織起來。
藉助於包能夠方便地組織本身的代碼,並將本身的代碼與別人提供的代碼庫分開管理。
標準的Java類庫分佈在多個包中,包括java.lang
、java.util
和java.net
等。
使用包的主要緣由是確保類名的惟一性。
假如兩個程序員都創建了Employee
類,只要將類放置在不一樣的包中,就不會產生衝突。
從編譯器角度來講,嵌套的包之間沒有任何關係。例如java.util
包與java.util.jar
包毫無關係。
一個類可使用所屬包的全部類,以及其餘包中的公有類(public class)。
可使用兩種方式訪問另外一個包中的公有類。
import
語句,能夠導入一個特定的類或者整個包。// 添加包名 java.time.LocalDate today = java.time.LocalDate.now(); // import import java.util.*; // or import java.time.LocalDate 引入特定類 LocalDate today = LocalDate.now();
大多數狀況下導入包便可,可是在發生命名衝突的時候,就要注意了,
import java.util.*; import java.sql.*; Date today; // Error
由於這兩個包都有Date
類,編譯器沒法肯定是哪個包的Date
類,因此這個時候能夠增長一個指定特定類的import
語句。
若是兩個類都要使用時,就在每一個類名前加上完整地包名。
import
語句還增長了導入靜態方法和靜態域的功能。
import static java.lang.System.*; // 而後可使用System類的靜態方法和靜態域而沒必要加前綴 out.println("Hohoho!"); // System.out.println()
另外,還能夠導入特定的方法或域:
import stattic java.lang.System.out; out.println("Hohoho!");
想將一個類放入包中,就必須將包的名字放在源文件的開頭。
package com.horstmann.corejava; public clas Employee { ... }
若是沒有在源文件中放置package
語句,源文件中的類被放置在默認包(default package)中,默認包是一個沒有名字的包。
通常須要把包中的文件放到與完整的包名匹配的子目錄中。
例如package com.horstmann.corejava
包中的全部源文件,應該被放置在子目錄com/horstmann/corejava
中。
public
的類、方法、變量能夠被任意的類使用private
的類、方法、變量只能被定義他們的類使用JDK包含一個很用有的工具——javadoc
,它能夠由源文件生成一個HTML文檔。
在源代碼中添加以專用的定界符/**
開始的註釋,則能夠容易地生成形式上專業的文檔,相比於把文檔和代碼單獨存放,修改代碼的同時修改文檔註釋再從新運行javadoc
,就不會出現不一致的問題。
javadoc
從下面幾個特性中抽取信息:
應該爲這幾部分編寫註釋,註釋應該放在所描述特性的前面。
註釋以/**
開始,以*/
結束。
每一個/**...*/
文檔註釋中使用自由格式文本(free-form text),標記由@
開始。
類註釋必須放在import
語句以後,類定義以前。
/** * Just some comment words here * another comment line * what is this class for? */ public class Card { ... }
方法註釋放在描述的方法前,除了通用標記,還可使用下面的標記:
@param
變量 描述:用於標記當前方法的參數部分的一個條目@return
描述:用於標記方法的返回部分@throws
類 描述:表示方法有可能拋出異常/** * Buy one coffee. * @param money the cost of coffee * @param coffeeTpye which coffee * @return coffee one hot coffee * @throws NoMoreCoffee */ public buyCoffee(double money, CoffeeType coffeeTpye) { ... }
只須要對公有域(一般是靜態常量)建議文檔。
/** * The ratio of a circle's circumference to its diameter */ public static final double PI = 3.1415926...;
可用在類文檔的註釋的標記:
@deprecated 文本:標記對類、方法或變量再也不使用,例如:
@deprecated Use <code> setVisible(true) </code> instead
@see 引用:增長一個超連接,能夠用於類、方法中,引用有如下狀況:
// 創建一個鏈接到com.horstmann.corejava.Employee類的raiseSalary(double)方法的超連接 @see com.horstmann.corejava.Employee#raiseSalary(double) // 能夠省略包名,甚至把包名和類名省去 @see Employee#raiseSalary(double) // 此時連接定位於當前包 @see raiseSalary(double) // 此時鏈接定位於當前類
<a href="...">label</a>
@see <a href="m«w.horstmann.com/corejava.htinl">The Core ]ava home page</a> // 此處可使用label標籤屬性來添加用戶看到的錨名稱
@see "Core Java 2 volume 2n"
若是願意的話,還能夠在註釋的任何位置放置指向其餘類和方法的超連接:
{ @link package.class#feature label } // 這裏的描述規則與@see標記規則同樣
若是想要包的註釋,就要在每個包的目錄中添加一個單獨的文件。
package.html
命名的文件,在<body>...</body>
之間的全部文本會被抽取。package-info.java
命名的文件,這個文件包含一個初始的以/**
和*/
界定的Javadoc
註釋,跟隨在一個包語句以後。還能夠爲全部的源文件提供一個概述性的註釋,這個註釋將被放置在一個名爲overview.html
的文件中,這個文件位於包含全部源文件的父目錄中,標記<body>...</body>
之間的全部文本會被抽取。當用戶選擇overview時,就會查看到這些註釋內容。
應用這些技巧能夠設計出更具備OOP專業水準的類。
絕對不要破壞封裝性,這是最重要的。
數據的表示形式極可能會改變,可是它們的使用方式卻不會常常發生變化,當數據保持私有時,它們的表示形式的變化不會對類的使用者產生影響,即便出現bug也易於檢測。
Java不對局部變量進行初始化,可是會對對象的實例域進行初始化。
可是也最好不要依賴系統的默認值,應該用構造器或者是提供默認值的方式來顯式地初始化全部的數據。
用其餘的類代替多個 相關的基本類型的使用。
這樣會使類更加易於理解和修改。
好比用一個Address
的類來代替下面的實例域:
private String street; private String city; private String state; private int zip;
這樣更容易理解和處理表示地址的域,而使用這些域的類並不用去關心這些域是怎麼具體變化的。
雖然這裏的「過多」對於我的來講是一個含糊的概念,可是若是明顯地能夠將一個複雜的類分解成兩個更爲簡單的類,則應該進行分解。
命名類名的良好習慣是採用:
Order
RushOrder
BillingAddress
對於方法來講:
get
開頭set
開頭更改對象的問題在於:若是多個線程視圖同時更新一個對象,就會發生併發更改,其結果是不可預料的。若是類時不可變的,就能夠安全地在多個線程間共享其對象。
我的靜態博客: