Java核心技術筆記 對象與類

《Java核心技術 卷Ⅰ》 第4章 對象與類html

  • 面向對象程序設計
  • 建立標準Java類庫中的類對象
  • 如何編寫本身的類

OOP

傳統的結構化程序設計:首先肯定如何操做數據,再決定如何組織數據。前端

面向對象程序設計:將數據放在第一位,再考慮操做數據的算法。java

類(class)是構造對象的模板或藍圖,
由類構造(construct)對象的過程稱爲建立類的實例(instance)。

封裝(encapsulation),也稱數據隱藏,封裝將數據和行爲組合在一個包中,並對對象使用者隱藏數據實現方式,對象中的數據域稱爲實例域(instance field),操做數據的過程稱爲方法(method)。git

對於每一個特定的類實例(對象)都有一組特定的實例域值,這些值的集合就是這個對象的當前狀態(state),只要向對象發送一個消息,它的狀態就有可能發生改變。程序員

實現封裝的關鍵:絕對不能讓類中的方法直接地訪問其餘類的實例域。github

OOP的另外一個原則:能夠經過擴展一個類來創建另一個新的類。在Java中,全部類都源於一個超類——Object算法

在擴展一個已有類時,新類具備這個類的所有屬性和方法,在新類中,只須要提供適用於這個新類的新方法和數據域就能夠了,這個過程稱爲繼承(inheritance)。sql

對象

對象的三個主要特性:安全

  • 行爲:可讓對象作什麼?
  • 狀態:被使用時,如何響應對應的行爲?
  • 標示:如何辨別具備相同行爲與狀態的不一樣對象?

識別類

識別類的簡單規則:併發

  • 使用的名詞:類
  • 使用的動詞:類的方法

類之間的關係

常見的關係有:

  • 依賴(use-a):若是一個類的方法操縱另外一個類的對象,咱們說一個類依賴另外一個類(即沒有這個類就沒法完成指定的方法),好比消費者想要支付,TA須要操做手機去完成具體的支付方式,即Customer "use-a" MobilePhone
  • 聚合(has-a):聚合關係意味着類A的對象包含B的對象,好比程序員要喝咖啡,TA有一個杯咖啡,即Programmer "has-a" Coffee
  • 繼承(is-a):類A擴展類B,要作學生,先要作人,即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進行初始化。

LocalDate類

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方法

更改器方法與訪問器方法

  • 更改器方法(mutator method):調用後,對象的狀態會改變。
  • 訪問器方法(accessor method):只訪問對象而不修改對象狀態的方法。

用戶自定義類

簡單類定義

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.classEmployee.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, ...)

構造器基礎的簡單總結:

  • 構造器與類同名
  • 構造器能夠有任意個參數,甚至沒有參數
  • 構造器沒有返回值
  • 構造器伴隨new操做一塊兒調用
  • 每一個類能夠有一個以上的構造器
  • 多個構造器時,根據調用new的參數類型來進行選擇

隱式參數與顯式參數

方法用於操做對象以及存取他們的示例域。
public void raiseSalary(double byPercent)
{
  double raise = salary * byPercent / 100;
  salary += raise;
}

當對象調用方法時

bob.raiseSalary(5);

raiseSalary方法有兩個參數。

  • 隱式(implicit)參數,這裏指的是如今方法名前的Employee類的對象。
  • 顯示(explicit)參數,這裏指的是位於方法名後括號中的數據。

在每個方法中,關鍵字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實例域

類中能夠定義實例域爲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方法將隨着執行建立程序所須要的對象。

同時,每個類均可以有一個main方法,經常使用於進行類的單元測試。

方法參數

在程序設計語言中有關參數傳遞給方法(函數)的一些專業術語:

  • 按值調用(call by value):表示方法接收的是調用者提供的值。
  • 按引用調用(call by reference):標識方法接收的是調用者提供的變量地址。

Java程序設計語言老是採用按值調用,即方法獲得的只是參數值的一個拷貝,不能修改傳遞給它的任何參數變量的內容。

可是當對象引用做爲參數時,狀況就不一樣了,方法得到的是對象引用的拷貝,對象引用和其餘拷貝同時引用同一個對象。

可是這並非引用調用。

public static void swap(Obejct a, Obejct b)
{
  Object tmp = a;
  a = b;
  b = tmp;
}

若是Java在對象參數時採用的是按引用調用,上述方法就能實現交換數據的效果。

可是這裏的swap方法並無改變存儲在調用參數中的對象引用,swap方法的參數ab被初始化爲兩個對象引用的拷貝,這個方法交換的是這兩個拷貝的引用。

Java中方法參數總結:

  • 方法不能修改基本數據類型的參數
  • 方法能夠改變對象參數的狀態
  • 方法不能讓對象參數引用一個新的對象

對象構造

重載

有些類有多個多個構造器。

這種特徵叫作重載(overloading),若是多個方法有相同的名字、不一樣的參數,便產生了重載。

編譯器經過用各個方法給出的參數類型與特定方法調用所使用的值類型進行匹配來挑選出相應的方法,若是編譯器找不到匹配的參數,就會產生編譯時錯誤。

Java容許重載任何方法,並不僅是構造器,所以要完整地描述一個方法,須要指出方法名以及參數類型,這叫方法的簽名(signature)。

// 方法重載的簽名舉例
indexOf(int)
indexOf(int int)
indexOf(String)

能夠看出,返回類型並非方法簽名的一部分,即不能有兩個名字相同、參數類型相同可是卻返回不一樣類型值的方法。

默認域初始化

若是域沒有在構造器中被賦予初值,則會被自動地賦予默認值

  • 數值:0
  • 布爾:false
  • 對象引用:null

這與局部變量的聲明不一樣,局部變量必須明確的進行初始化。

構造器中若是不明確地進行初始化,會影響代碼的可讀性。

無參數的構造器

若是在編寫一個類時沒有編寫構造器,那麼系統會提供一個無參數構造器,這個構造器將全部的實例域設置爲默認值。

若是類中提供了至少一個構造器,可是沒有提供無參數構造器,則構造對象時若是沒有提供參數就會被視爲不合法。

顯式域初始化

經過重載類的構造器方法,能夠採用多種形式設置類的實例域的初始狀態。

能夠在類定義中,直接講一個值賦予給任何域。

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++;
  }
  ...
}

不管哪一個構造器構造對象,初始化塊都會執行,首先運行初始化塊,而後才運行構造器的主體部分。

調用構造器的具體處理步驟:

  1. 全部數據域被初始化爲默認值
  2. 按照出現次序執行初始化語句和初始化塊
  3. 若是構造器調用了第二個構造器,則執行第二個構造器主體
  4. 執行這個構造器主體

初始化塊比較經常使用於代碼比較複雜的靜態域初始化:

static
{
  Random generator = new Random();
  nextId = generator.nextInt(10000);
}

Java容許使用包(package)將類組織起來。
藉助於包能夠方便地組織本身的代碼,並將本身的代碼與別人提供的代碼庫分開管理。

標準的Java類庫分佈在多個包中,包括java.langjava.utiljava.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...;

通用註釋

可用在類文檔的註釋的標記:

  • @author 姓名:可使用多個
  • @version 文本:版本條目
  • @since 文本:始於...條目,這裏的文本能夠是對版本的描述
  • @deprecated 文本:標記對類、方法或變量再也不使用,例如:

    @deprecated Use <code> setVisible(true) </code> instead
  • @see 引用:增長一個超連接,能夠用於類、方法中,引用有如下狀況:

    • package.class#feature label
    // 創建一個鏈接到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標籤屬性來添加用戶看到的錨名稱
    • "text"
    @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;

這樣更容易理解和處理表示地址的域,而使用這些域的類並不用去關心這些域是怎麼具體變化的。

不是全部的域都須要獨立的域訪問和更改器

  1. 有些域在對象被構造出來後,在類的設計上,可能再也不容許被修改
  2. 在對象中有時候包含一些不但願別人得到或設置的實例域

將職責過多的類進行分解

雖然這裏的「過多」對於我的來講是一個含糊的概念,可是若是明顯地能夠將一個複雜的類分解成兩個更爲簡單的類,則應該進行分解。

類名和方法名要能體現職責

命名類名的良好習慣是採用:

  • 名詞 Order
  • 前面有形容詞修飾的名詞 RushOrder
  • 動名詞修飾的名詞 BillingAddress

對於方法來講:

  • 訪問器用小寫的get開頭
  • 更改起用小寫的set開頭

優先使用不可變的類

更改對象的問題在於:若是多個線程視圖同時更新一個對象,就會發生併發更改,其結果是不可預料的。若是類時不可變的,就能夠安全地在多個線程間共享其對象。

Java對象與類總結

  • OOP的簡要概念
  • 類與對象
  • 類之間的關係
  • 對象與對象變量
  • 更改器與訪問器
  • 自定義類
  • 構造器
  • 隱式參數與顯式參數
  • 封裝
  • 基於類的訪問權限
  • 私有方法
  • final實例域
  • 靜態域與靜態方法
  • 按值調用
  • 重載
  • 默認域的初始化
  • 無參數構造器
  • 顯式域初始化
  • 初始化塊
  • 文檔註釋
  • 類設計技巧

我的靜態博客:

相關文章
相關標籤/搜索