瘋耔java語言筆記

 一◐  java概述                                                                                       html

 1.1  Java的不一樣版本:J2SE、J2EE、J2ME的區別                                                 前端

1998年12月,SUN公司發佈了Java 1.2,開始使用「Java 2」 這一名稱,目前咱們已經不多使用1.2以前的版本,因此一般所說的Java都是指Java2。

Java 有三個版本,分別爲 J2SE、J2EE和J2ME,如下是詳細介紹。java

J2SE(Java 2 Platform Standard Edition) 標準版

J2SE是Java的標準版,主要用於開發客戶端(桌面應用軟件),例如經常使用的文本編輯器、下載軟件、即時通信工具等,均可以經過J2SE實現。

J2SE包含了Java的核心類庫,例如數據庫鏈接、接口定義、輸入/輸出、網絡編程等。

學習Java編程就是從J2SE入手。c++

J2EE(Java 2 Platform Enterprise Edition) 企業版

J2EE是功能最豐富的一個版本,主要用於開發高訪問量、大數據量、高併發量的網站,例如美團、去哪兒網的後臺都是J2EE。一般所說的JSP開發就是J2EE的一部分。

J2EE包含J2SE中的類,還包含用於開發企業級應用的類,例如EJB、servlet、JSP、XML、事務控制等。

J2EE也能夠用來開發技術比較龐雜的管理軟件,例如ERP系統(Enterprise Resource Planning,企業資源計劃系統)。程序員

J2ME(Java 2 Platform Micro Edition) 微型版

J2ME 只包含J2SE中的一部分類,受平臺影響比較大,主要用於嵌入式系統和移動平臺的開發,例如呼機、智能卡、手機(功能機)、機頂盒等。

在智能手機尚未進入公衆視野的時候,你是否還記得你的摩托羅拉、諾基亞手機上有不少Java小遊戲嗎?這就是用J2ME開發的。

Java的初衷就是作這一塊的開發。

注意:Android手機有本身的開發組件,不使用J2ME進行開發。

Java5.0版本後,J2SE、J2EE、J2ME分別改名爲Java SE、Java EE、Java ME,因爲習慣的緣由,咱們依然稱之爲J2SE、J2EE、J2ME。web

 

1.2  Java類庫及其組織結構(Java API)                                                       算法

Java 官方爲開發者提供了不少功能強大的類,這些類被分別放在各個包中,隨JDK一塊兒發佈,稱爲Java類庫或Java API。

API(Application Programming Interface, 應用程序編程接口)是一個通用概念。

例如我編寫了一個類,能夠獲取計算機的各類硬件信息,它很強大很穩定,若是你的項目也須要這樣一個功能,那麼你就無需再本身編寫代碼,將個人類拿來直接用就能夠。可是,個人類代碼很複雜,讓你讀完這些代碼不太現實,並且我也不但願你看到個人代碼(你也不必也不但願讀懂這些晦澀的代碼),我要保護個人版權,怎麼辦呢?

我能夠先將個人類編譯,並附帶一個文檔,告訴你個人類怎麼使用,有哪些方法和屬性,你只須要按照文檔的說明來調用就徹底沒問題,既節省了你閱讀代碼的時間,也保護了個人版權。例如,獲取CPU信息的方法:
    getCpuInfo(int cpuType);
這就是一個API。也就是說,該文檔中描述的類的使用方法,就叫作API。

我也能夠開發一個軟件,用來清理計算機中的垃圾文件,我比較有公益心,但願讓更多的開發人員使用個人軟件,我就會在發佈軟件的同時附帶一個說明文檔,告訴你怎樣在本身的程序中調用,這也叫作API。

Java API也有一個說明文檔,入口地址:http://www.oracle.com/technetwork/java/api

選擇對應版本的Java,點擊連接進入便可。J2SE 1.7 的API地址爲:http://docs.oracle.com/javase/7/docs/api/

這個文檔是在線的,官方會隨時更新。固然你也能夠下載到本地,請你們本身百度怎麼下載。

打開J2SE 1.7 的API文檔,以下圖所示:sql


圖1  API 文檔


Java類庫中有不少包:數據庫

  • 以 java.* 開頭的是Java的核心包,全部程序都會使用這些包中的類;
  • 以 javax.* 開頭的是擴展包,x 是 extension 的意思,也就是擴展。雖然 javax.* 是對 java.* 的優化和擴展,可是因爲 javax.* 使用的愈來愈多,不少程序都依賴於 javax.*,因此 javax.* 也是核心的一部分了,也隨JDK一塊兒發佈。
  • 以 org.* 開頭的是各個機構或組織發佈的包,由於這些組織頗有影響力,它們的代碼質量很高,因此也將它們開發的部分經常使用的類隨JDK一塊兒發佈。


在包的命名方面,爲了防止重名,有一個慣例:你們都以本身域名的倒寫形式做爲開頭來爲本身開發的包命名,例如百度發佈的包會以 com.baidu.* 開頭,w3c組織發佈的包會以 org.w3c.* 開頭,微學苑發佈的包會以 net.weixueyuan.* 開頭……

組織機構的域名後綴通常爲 org,公司的域名後綴通常爲 com,能夠認爲 org.* 開頭的包爲非盈利組織機構發佈的包,它們通常是開源的,能夠無償使用在本身的產品中,不用考慮侵權問題,而以 com.* 開頭的包每每由盈利性的公司發佈,可能會有版權問題,使用時要注意。

java中經常使用的幾個包介紹:編程

包名 說明
java.lang 該包提供了Java編程的基礎類,例如 Object、Math、String、StringBuffer、System、Thread等,不使用該包就很難編寫Java代碼了。
java.util 該包提供了包含集合框架、遺留的集合類、事件模型、日期和時間實施、國際化和各類實用工具類(字符串標記生成器、隨機數生成器和位數組)。
java.io 該包經過文件系統、數據流和序列化提供系統的輸入與輸出。
java.net 該包提供實現網絡應用與開發的類。
java.sql 該包提供了使用Java語言訪問並處理存儲在數據源(一般是一個關係型數據庫)中的數據API。
java.awt 這兩個包提供了GUI設計與開發的類。java.awt包提供了建立界面和繪製圖形圖像的全部類,而javax.swing包提供了一組「輕量級」的組件,儘可能讓這些組件在全部平臺上的工做方式相同。
javax.swing
java.text 提供了與天然語言無關的方式來處理文本、日期、數字和消息的類和接口。


更多的包和說明請參考API文檔。

 

1.3  Java import以及Java類的搜索路徑                                                             

若是你但願使用Java包中的類,就必須先使用import語句導入。

import語句與C語言中的 #include 有些相似,語法爲:
    import package1[.package2…].classname;
package 爲包名,classname 爲類名。例如:☆☆☆

import java.util.Date; // 導入 java.util 包下的 Date 類
import java.util.Scanner; // 導入 java.util 包下的 Scanner 類
import javax.swing.*; // 導入 javax.swing 包下的全部類,* 表示全部類

 

注意:

  • import 只能導入包所包含的類,而不能導入包。
  • 爲方便起見,咱們通常不導入單獨的類,而是導入包下全部的類,例如 import java.util.*;。


Java 編譯器默認爲全部的 Java 程序導入了 JDK 的 java.lang 包中全部的類(import java.lang.*;),其中定義了一些經常使用類,如 System、String、Object、Math 等,所以咱們能夠直接使用這些類而沒必要顯式導入。可是使用其餘類必須先導入。

前面講到的」Hello World「程序使用了System.out.println(); 語句,System 類位於 java.lang 包,雖然咱們沒有顯式導入這個包中的類,可是Java 編譯器默認已經爲咱們導入了,不然程序會執行失敗。

Java類的搜索路徑

Java程序運行時要導入相應的類,也就是加載 .class 文件的過程。

假設有以下的 import 語句:

import p1.Test;

 

該語句代表要導入 p1 包中的 Test 類。

安裝JDK時,咱們已經設置了環境變量 CLASSPATH 來指明類庫的路徑,它的值爲 .;%JAVA_HOME%\lib,而 JAVA_HOME 又爲 D:\Program Files\jdk1.7.0_71,因此 CLASSPATH 等價於 .;D:\Program Files\jdk1.7.0_71\lib。

Java 運行環境將依次到下面的路徑尋找並載入字節碼文件 Test.class:

  • .p1\Test.class("."表示當前路徑)
  • D:\Program Files\jdk1.7.0_71\lib\p1\Test.class


若是在第一個路徑下找到了所需的類文件,則中止搜索,不然繼續搜索後面的路徑,若是在全部的路徑下都未能找到所需的類文件,則編譯或運行出錯

你能夠在CLASSPATH變量中增長搜索路徑,例如 .;%JAVA_HOME%\lib;C:\javalib,那麼你就能夠將類文件放在 C:\javalib 目錄下,Java運行環境同樣會找到。

 

二◐  java語法基礎                                                              

 

2.1java數據類型以及變量的定義                                                                

 

Java 是一種強類型的語言,聲明變量時必須指明數據類型。變量(variable)的值佔據必定的內存空間。不一樣類型的變量佔據不一樣的大小。

Java中共有8種基本數據類型,包括4 種整型、2 種浮點型、1 種字符型、1 種布爾型,請見下表。

Java基本數據類型
數據類型 說明 所佔內存 舉例 備註
byte 字節型 1 byte 3, 127  
short 短整型 2 bytes 3, 32767  
int 整型 4 bytes 3, 21474836  
long 長整型 8 bytes 3L, 92233720368L long最後要有一個L字母(大小寫無所謂)。
float 單精度浮點型 4 bytes 1.2F, 223.56F float最後要有一個F字母(大小寫無所謂)。
double 雙精度浮點型 8 bytes 1.2, 1.2D, 223.56, 223.56D double最後最好有一個D字母(大小寫無所謂)。
char 字符型 2 bytes 'a', ‘A’ 字符型數據只能是一個字符,由單引號包圍。
boolean 布爾型 1 bit true, false  


對於整型數據,一般狀況下使用 int 類型。但若是表示投放廣島長崎的原子彈釋放出的能量,就須要使用 long 類型了。byte 和 short 類型主要用於特定的應用場合,例如,底層的文件處理或者須要控制佔用存儲空間量的大數組。

在Java中,整型數據的長度與平臺無關,這就解決了軟件從一個平臺移植到另外一個平臺時給程序員帶來的諸多問題。與此相反,C/C++ 整型數據的長度是與平臺相關的,程序員須要針對不一樣平臺選擇合適的整型,這就可能致使在64位系統上穩定運行的程序在32位系統上發生整型溢出。

八進制有一個前綴 0,例如 010 對應十進制中的 8;十六進制有一個前綴 0x,例如 0xCAFE;從 Java 7 開始,能夠使用前綴 0b 來表示二進制數據,例如 0b1001 對應十進制中的 9。一樣從 Java 7 開始,能夠使用下劃線來分隔數字,相似英文數字寫法,例如 1_000_000 表示 1,000,000,也就是一百萬。下劃線只是爲了讓代碼更加易讀,編譯器會刪除這些下劃線。

另外,不像 C/C++,Java 不支持無符號類型(unsigned)。

float 類型有效數字最長爲 7 位,有效數字長度包括了整數部分和小數部分。例如:

float x = 223.56F;
float y = 100.00f;

 

注意:每一個float類型後面都有一個標誌「F」或「f」,有這個標誌就表明是float類型。

double 類型有效數字最長爲 15 位。與 float 類型同樣,double 後面也帶有標誌「D」或「d」。例如:

double x = 23.45D;
double y = 422.22d;
double z = 562.234;

 

注意:不帶任何標誌的浮點型數據,系統默認是 double 類型

大多數狀況下都是用 double 類型,float 的精度很難知足需求

不一樣數據類型應用舉例:

public class Demo {
public static void main(String[] args){
// 字符型
char webName1 = '微';
char webName2 = '學';
char webName3 = '苑';
System.out.println("網站的名字是:" + webName1 + webName2 + webName3);
// 整型
short x=22; // 十進制
int y=022; // 八進制
long z=0x22L; // 十六進制
System.out.println("轉化成十進制:x = " + x + ", y = " + y + ", z = " + z);   //"+"先後字符串鏈接// 浮點型
float m = 22.45f;
double n = 10;
System.out.println("計算乘積:" + m + " * " + n + "=" + m*n);
}
}

 

運行結果:
網站的名字是:微學苑
轉化成十進制:x = 22, y = 18, z = 34
計算乘積:22.45 * 10.0=224.50000762939453

從運行結果能夠看出,即便浮點型數據只有整數沒有小數,在控制檯上輸出時系統也會自動加上小數點,而且小數位所有置爲 0。

對布爾型的說明

在C語言中,若是判斷條件成立,會返回1,不然返回0,例如:

#include <stdio.h>
int main(){
int x = 100>10;
int y = 100<10;
printf("100>10 = %d\n", x);
printf("100<10 = %d\n", y);
return 0;
}

 

運行結果:
100>10 = 1
100<10 = 0

可是在Java中不同,條件成立返回 true,不然返回 false,即布爾類型。例如:

public class Demo {
public static void main(String[] args){
// 字符型
boolean a = 100>10;
boolean b = 100<10;
System.out.println("100>10 = " + a);
System.out.println("100<10 = " + b);
if(a){
System.out.println("100<10是對的");
}else{
System.out.println("100<10是錯的");
}
}
}

 

運行結果:
100>10 = true
100<10 = false
100<10是對的

實際上,true 等同於1,false 等同於0,只不過換了個名稱,並單獨地成爲一種數據類型。

 

2.2  Java數據類型轉換(自動轉換和強制轉換)                                                        

數據類型的轉換,分爲自動轉換和強制轉換。自動轉換是程序在執行過程當中「悄然」進行的轉換,不須要用戶提早聲明,通常是從位數低的類型向位數高的類型轉換;強制類型轉換則必須在代碼中聲明,轉換順序不受限制。

自動數據類型轉換

自動轉換按從低到高的順序轉換。不一樣類型數據間的優先關係以下:
    低--------------------------------------------->高
    byte,short,char-> int -> long -> float -> double

運算中,不一樣類型的數據先轉化爲同一類型,而後進行運算,轉換規則以下:

操做數1類型 操做數2類型 轉換後的類型
byte、short、char int int
byte、short、char、int long long
byte、short、char、int、long float float
byte、short、char、int、long、float double double

強制數據類型轉換

強制轉換的格式是在須要轉型的數據前加上「( )」,而後在括號內加入須要轉化的數據類型。有的數據通過轉型運算後,精度會丟失,而有的會更加精確,下面的例子能夠說明這個問題。

public class Demo {
public static void main(String[] args){
int x;
double y;
x = (int)34.56 + (int)11.2; // 丟失精度
y = (double)x + (double)10 + 1; // 提升精度
System.out.println("x=" + x);
System.out.println("y=" + y);
}
}

 

運行結果:
x=45
y=56.0

仔細分析上面程序段:因爲在 34.56 前有一個 int 的強制類型轉化,因此 34.56 就變成了 34。一樣 11.2 就變成了 11 了,因此 x 的結果就是 45。在 x 前有一個 double 類型的強制轉換,因此 x 的值變爲 45.0,而 10 的前面也被強制成 double 類型,因此也變成 10.0,因此最後 y 的值變爲 56。 

 2.3  Java數組的定義和使用                                                      

 若是但願保存一組有相同類型的數據,能夠使用數組。

數組的定義和內存分配

Java 中定義數組的語法有兩種:
    type arrayName[];
    type[] arrayName;
type 爲Java中的任意數據類型,包括基本類型和組合類型,arrayName爲數組名,必須是一個合法的標識符,[ ] 指明該變量是一個數組類型變量。例如:

int demoArray[];
int[] demoArray;

 

這兩種形式沒有區別,使用效果徹底同樣,讀者可根據本身的編程習慣選擇。

與C、C++不一樣,Java在定義數組時並不爲數組元素分配內存,所以[ ]中無需指定數組元素的個數,即數組長度。並且對於如上定義的一個數組是不能訪問它的任何元素的,咱們必需要爲它分配內存空間,這時要用到運算符new,其格式以下:
    arrayName=new type[arraySize];
其中,arraySize 爲數組的長度,type 爲數組的類型。如:

demoArray=new int[3];

 

爲一個整型數組分配3個int 型整數所佔據的內存空間。

一般,你能夠在定義的同時分配空間,語法爲:
    type arrayName[] = new type[arraySize];
例如:

int demoArray[] = new int[3];

 

數組的初始化

你能夠在聲明數組的同時進行初始化(靜態初始化),也能夠在聲明之後進行初始化(動態初始化)。例如:

// 靜態初始化
// 靜態初始化的同時就爲數組元素分配空間並賦值
int intArray[] = {1,2,3,4};
String stringArray[] = {"微學苑", "http://www.weixueyuan.net", "一切編程語言都是紙老虎"};
// 動態初始化
float floatArray[] = new float[3];
floatArray[0] = 1.0f;
floatArray[1] = 132.63f;
floatArray[2] = 100F;

 

數組引用

能夠經過下標來引用數組:
    arrayName[index];
與C、C++不一樣,Java對數組元素要進行越界檢查以保證安全性。

每一個數組都有一個length屬性來指明它的長度,例如 intArray.length 指明數組 intArray 的長度。

【示例】寫一段代碼,要求輸入任意5個整數,輸出它們的和。

import java.util.*;
public class Demo {
public static void main(String[] args){
int intArray[] = new int[5];
long total = 0;
int len = intArray.length;
// 給數組元素賦值
System.out.print("請輸入" + len + "個整數,以空格爲分隔:");
Scanner sc = new Scanner(System.in);
for(int i=0; i<len; i++){
intArray[i] = sc.nextInt();
}
// 計算數組元素的和
for(int i=0; i<len; i++){
total += intArray[i];
}
System.out.println("全部數組元素的和爲:" + total);
}
}

 

運行結果:
請輸入5個整數,以空格爲分隔:10 20 15 25 50
全部數組元素的和爲:120

數組的遍歷

實際開發中,常常須要遍歷數組以獲取數組中的每個元素。最容易想到的方法是for循環,例如:

int arrayDemo[] = {1, 2, 4, 7, 9, 192, 100};
for(int i=0,len=arrayDemo.length; i<len; i++){
System.out.println(arrayDemo[i] + ", ");
}

 

輸出結果:
1, 2, 4, 7, 9, 192, 100,

不過,Java提供了」加強版「的for循環,專門用來遍歷數組,語法爲:

for( arrayType varName: arrayName ){
// Some Code
}

 

arrayType 爲數組類型(也是數組元素的類型);varName 是用來保存當前元素的變量,每次循環它的值都會改變;arrayName 爲數組名稱

每循環一次,就會獲取數組中下一個元素的值,保存到 varName 變量,直到數組結束。即,第一次循環 varName 的值爲第0個元素,第二次循環爲第1個元素......例如:

int arrayDemo[] = {1, 2, 4, 7, 9, 192, 100};
for(int x: arrayDemo){
System.out.println(x + ", ");
}

 

輸出結果與上面相同。

這種加強版的for循環也被稱爲」foreach循環「,它是普通for循環語句的特殊簡化版。全部的foreach循環均可以被改寫成for循環。

可是,若是你但願使用數組的索引,那麼加強版的 for 循環沒法作到

二維數組

二維數組的聲明、初始化和引用與一維數組類似:

int intArray[ ][ ] = { {1,2}, {2,3}, {4,5} };
int a[ ][ ] = new int[2][3];                //與c c++不一樣之處是定義的時候不佔內存,須要從新分配空間
a[0][0] = 12;
a[0][1] = 34;
// ......
a[1][2] = 93;

 

Java語言中,因爲把二維數組看做是數組的數組,數組空間不是連續分配的,因此不要求二維數組每一維的大小相同。例如:

int intArray[ ][ ] = { {1,2}, {2,3}, {3,4,5} };
int a[ ][ ] = new int[2][ ];
a[0] = new int[3];
a[1] = new int[5];

 


【示例】經過二維數組計算兩個矩陣的乘積。

public class Demo {
public static void main(String[] args){
// 第一個矩陣(動態初始化一個二維數組)
int a[][] = new int[2][3];
// 第二個矩陣(靜態初始化一個二維數組)
int b[][] = { {1,5,2,8}, {5,9,10,-3}, {2,7,-5,-18} };
// 結果矩陣
int c[][] = new int[2][4];
// 初始化第一個矩陣
for(int i=0; i<2; i++)
for(int j=0; j<3 ;j++)
a[i][j] = (i+1) * (j+2);
// 計算矩陣乘積
for (int i=0; i<2; i++){
for (int j=0; j<4; j++){
c[i][j]=0;
for(int k=0; k<3; k++)
c[i][j] += a[i][k] * b[k][j];
}
}
// 輸出結算結果
for(int i=0; i<2; i++){
for (int j=0; j<4; j++)
System.out.printf("%-5d", c[i][j]);
System.out.println();
}
}
}

 

運行結果:
25   65   14   -65 
50   130  28   -130

幾點說明:

    • 上面講的是靜態數組。靜態數組一旦被聲明,它的容量就固定了,不容改變。因此在聲明數組時,必定要考慮數組的最大容量,防止容量不夠的現象。
    • 若是想在運行程序時改變容量,就須要用到數組列表(ArrayList,也稱動態數組)或向量(Vector)。
    • 正是因爲靜態數組容量固定的缺點,實際開發中使用頻率不高,被 ArrayList 或 Vector 代替,由於實際開發中常常須要向數組中添加或刪除元素,而它的容量很差預估。

 

2.4  Java StringBuffer與StringBuider                                                       

String 的值是不可變的,每次對String的操做都會生成新的String對象,不只效率低,並且耗費大量內存空間

StringBuffer類和String類同樣,也用來表示字符串,可是StringBuffer的內部實現方式和String不一樣,在進行字符串處理時,不生成新的對象,在內存使用上要優於String

StringBuffer 默認分配16字節長度的緩衝區,當字符串超過該大小時,會自動增長緩衝區長度,而不是生成新的對象。

StringBuffer不像String,只能經過 new 來建立對象,不支持簡寫方式,例如:

StringBuffer str1 = new StringBuffer(); // 分配16個字節長度的緩衝區
StringBuffer str2 = =new StringBuffer(512); // 分配512個字節長度的緩衝區
// 在緩衝區中存放了字符串,並在後面預留了16個字節長度的空緩衝區
StringBuffer str3 = new StringBuffer("www.weixueyuan.net");

 

StringBuffer類的主要方法

StringBuffer類中的方法主要偏重於對於字符串的操做,例如追加、插入和刪除等,這個也是StringBuffer類和String類的主要區別。實際開發中,若是須要對一個字符串進行頻繁的修改,建議使用 StringBuffer

1) append() 方法

append() 方法用於向當前字符串的末尾追加內容,相似於字符串的鏈接。調用該方法之後,StringBuffer對象的內容也發生改變,例如:

StringBuffer str = new StringBuffer(「biancheng100」);
str.append(true);

 

則對象str的值將變成」biancheng100true」。注意是str指向的內容變了,不是str的指向變了。

字符串的」+「操做實際上也是先建立一個StringBuffer對象,而後調用append()方法將字符串片斷拼接起來,最後調用toString()方法轉換爲字符串。

這樣看來,String的鏈接操做就比StringBuffer多出了一些附加操做,效率上必然會打折扣。

可是,對於長度較小的字符串,」+「操做更加直觀,更具可讀性,有些時候能夠稍微犧牲一下效率。

2)  deleteCharAt()

deleteCharAt() 方法用來刪除指定位置的字符,並將剩餘的字符造成新的字符串。例如:

StringBuffer str = new StringBuffer("abcdef");
str. deleteCharAt(3);

 

該代碼將會刪除索引值爲3的字符,即」d「字符。

你也能夠經過delete()方法一次性刪除多個字符,例如:

StringBuffer str = new StringBuffer("abcdef");
str.delete(1, 4);

 

該代碼會刪除索引值爲1~4之間的字符,包括索引值1,但不包括4。

3) insert() 方法

insert() 用來在指定位置插入字符串,能夠認爲是append()的升級版。例如:

StringBuffer str = new StringBuffer("abcdef");
str.insert(3, "xyz");

 

最後str所指向的字符串爲 abcdxyzef。

4) setCharAt() 方法

setCharAt() 方法用來修改指定位置的字符。例如:

StringBuffer str = new StringBuffer("abcdef");
str.setCharAt(3, 'z');

 

該代碼將把索引值爲3的字符修改成 z,最後str所指向的字符串爲 abczef。

以上僅僅是部分經常使用方法的簡單說明,更多方法和解釋請查閱API文檔。

String和StringBuffer的效率對比

爲了更加明顯地看出它們的執行效率,下面的代碼,將26個英文字母加了10000次。

public class Demo {
public static void main(String[] args){
String fragment = "abcdefghijklmnopqrstuvwxyz";
int times = 10000;
// 經過String對象
long timeStart1 = System.currentTimeMillis();
String str1 = "";
for (int i=0; i<times; i++) {
str1 += fragment;
}
long timeEnd1 = System.currentTimeMillis();
System.out.println("String: " + (timeEnd1 - timeStart1) + "ms");
// 經過StringBuffer
long timeStart2 = System.currentTimeMillis();
StringBuffer str2 = new StringBuffer();
for (int i=0; i<times; i++) {
str2.append(fragment);
}
long timeEnd2 = System.currentTimeMillis();
System.out.println("StringBuffer: " + (timeEnd2 - timeStart2) + "ms");
}
}

 

運行結果:
String: 5287ms
StringBuffer: 3ms

結論很明顯,StringBuffer的執行效率比String快上千倍,這個差別隨着疊加次數的增長愈來愈明顯,當疊加次數達到30000次的時候,運行結果爲:
String: 35923ms
StringBuffer: 8ms

因此,強烈建議在涉及大量字符串操做時使用StringBuffer。

StringBuilder類

StringBuilder類和StringBuffer類功能基本類似,方法也差很少,主要區別在於StringBuffer類的方法是多線程安全的,而StringBuilder不是線程安全的,相比而言,StringBuilder類會略微快一點。

StringBuffer、StringBuilder、String中都實現了CharSequence接口。

CharSequence是一個定義字符串操做的接口,它只包括length()、charAt(int index)、subSequence(int start, int end) 這幾個API。

StringBuffer、StringBuilder、String對CharSequence接口的實現過程不同,以下圖所示:


圖1  對CharSequence接口的實現


可見,String直接實現了CharSequence接口;StringBuilder 和 StringBuffer都是可變的字符序列,它們都繼承於AbstractStringBuilder,實現了CharSequence接口。

總結

線程安全:

  • StringBuffer:線程安全
  • StringBuilder:線程不安全


速度:
通常狀況下,速度從快到慢爲 StringBuilder > StringBuffer > String,固然這是相對的,不是絕對的。

使用環境:

    • 操做少許的數據使用 String;
    • 單線程操做大量數據使用 StringBuilder;
    • 多線程操做大量數據使用 StringBuffer。

 

 三◐  java類和對象                                                        

  

3.1  Java類的定義及其實例化                                                                

類必須先定義才能使用。類是建立對象的模板,建立對象也叫類的實例化。所謂的 實例化 說白了就是 建立對象
實例化以後的對象 叫作實例
下面經過一個簡單的例子來理解Java中類的定義:

public class Dog{
    String name;
    int age;
   
    void bark(){  // 汪汪叫
        System.out.println("汪汪,不要過來");
    }
 
    void hungry(){  // 飢餓
        System.out.println("主人,我餓了");
    }
}

 

對示例的說明:

  • public 是類的修飾符,代表該類是公共類,能夠被其餘類訪問。修飾符將在下節講解。
  • class 是定義類的關鍵字。
  • Dog 是類名稱。
  • name、age 是類的成員變量,也叫屬性;bark()、hungry() 是類中的函數,也叫方法。


一個類能夠包含如下類型變量:

  • 局部變量:在方法或者語句塊中定義的變量被稱爲局部變量。變量聲明和初始化都是在方法中,方法結束後,變量就會自動銷燬。
  • 成員變量:成員變量是定義在類中、方法體以外的變量。這種變量在建立對象的時候實例化(分配內存)。成員變量能夠被類中的方法和特定類的語句訪問。
  • 類變量:類變量也聲明在類中,方法體以外,但必須聲明爲static類型。static 也是修飾符的一種,將在下節講解。

構造方法

在類實例化的過程當中自動執行的方法叫作構造方法,它不須要你手動調用。構造方法能夠在類實例化的過程當中作一些初始化的工做。

構造方法的名稱必須與類的名稱相同,而且沒有返回值。

每一個類都有構造方法。若是沒有顯式地爲類定義構造方法,Java編譯器將會爲該類提供一個默認的構造方法。

下面是一個構造方法示例:

public class Dog{
    String name;
    int age;
   
    // 構造方法,沒有返回值
    Dog(String name1, int age1){
        name = name1;
        age = age1;
        System.out.println("感謝主人領養了我");
    }
   
    // 普通方法,必須有返回值
    void bark(){
        System.out.println("汪汪,不要過來");
    }
 
    void hungry(){
        System.out.println("主人,我餓了");
    }
   
    public static void main(String arg[]){
        // 建立對象時傳遞的參數要與構造方法參數列表對應
        Dog myDog = new Dog("花花", 3);
    }
}

 

運行結果:
感謝主人領養了我

說明:

  • 構造方法不能被顯示調用。
  • 構造方法不能有返回值,由於沒有變量來接收返回值。

建立對象

對象是類的一個實例,建立對象的過程也叫類的實例化。對象是以類爲模板來建立的。

在Java中,使用new關鍵字來建立對象,通常有如下三個步驟:

  • 聲明:聲明一個對象,包括對象名稱和對象類型。
  • 實例化:使用關鍵字new來建立一個對象。
  • 初始化:使用new建立對象時,會調用構造方法初始化對象。


例如:

Dog myDog; // 聲明一個對象
myDog = new Dog("花花", 3); // 實例化  聲明並無分配空間,只有實例化後,纔有了本身的空間

 

也能夠在聲明的同時進行初始化:

Dog myDog = new Dog("花花", 3);

 

訪問成員變量和方法

經過已建立的對象來訪問成員變量和成員方法,例如:

// 實例化
Dog myDog = new Dog("花花", 3);
// 經過點號訪問成員變量
myDog.name;
// 經過點號訪問成員方法
myDog.bark();

下面的例子演示瞭如何訪問成員變量和方法:

public class Dog{
    String name;
    int age;
   
    Dog(String name1, int age1){
        name = name1;
        age = age1;
        System.out.println("感謝主人領養了我");
    }
   
    void bark(){
        System.out.println("汪汪,不要過來");
    }
 
    void hungry(){
        System.out.println("主人,我餓了");
    }
   
    public static void main(String arg[]){
        Dog myDog = new Dog("花花", 3);
        // 訪問成員變量
        String name = myDog.name;
        int age = myDog.age;
        System.out.println("我是一隻小狗,我名字叫" + name + ",我" + age + "歲了");
        // 訪問方法
        myDog.bark();
        myDog.hungry();
    }
}

 

運行結果:
感謝主人領養了我
我是一隻小狗,我名字叫花花,我3歲了
汪汪,不要過來
主人,我餓了

 

 3.2  Java訪問修飾符(訪問控制符)                                                                         

 Java 經過修飾符來控制類、屬性和方法的訪問權限和其餘功能,一般放在語句的最前端。例如:

public class className {
// body of class
}
private boolean myFlag;
static final double weeks = 9.5;
protected static final int BOXWIDTH = 42;
public static void main(String[] arguments) {
// body of method
}

 


Java 的修飾符不少,分爲訪問修飾符和非訪問修飾符。本節僅介紹訪問修飾符,非訪問修飾符會在後續介紹。

訪問修飾符也叫訪問控制符,是指可以控制類、成員變量、方法的使用權限的關鍵字。

在面向對象編程中,訪問控制符是一個很重要的概念,能夠使用它來保護對類、變量、方法和構造方法的訪問。

Java支持四種不一樣的訪問權限:

修飾符 說明
public 共有的,對全部類可見。
protected 受保護的,對同一包內的類和全部子類可見。
private 私有的,在同一類內可見。
默認的 在同一包內可見。默認不使用任何修飾符。

public:公有的

被聲明爲public的類、方法、構造方法和接口可以被任何其餘類訪問。

若是幾個相互訪問的public類分佈在不用的包中,則須要導入相應public類所在的包。因爲類的繼承性,類全部的公有方法和變量都能被其子類繼承。

下面的方法使用了公有訪問控制:

public static void main(String[] arguments) {
// body of method
}

 

Java程序的main() 方法必須設置成公有的,不然,Java解釋器將不能運行該類

protected:受保護的

被聲明爲protected的變量、方法和構造方法能被同一個包中的任何其餘類訪問,也可以被不一樣包中的子類訪問

protected訪問修飾符不能修飾類和接口,方法和成員變量可以聲明爲protected,可是接口的成員變量和成員方法不能聲明爲protected。

子類能訪問protected修飾符聲明的方法和變量,這樣就能保護不相關的類使用這些方法和變量。

下面的父類使用了protected訪問修飾符,子類重載了父類的bark()方法。

public class Dog{
    protected void bark() {
        System.out.println("汪汪,不要過來");
    }
}
class Teddy extends Dog{  // 泰迪
    void bark() {
        System.out.println("汪汪,我好怕,不要跟着我");
    }
}

 

若是把bark()方法聲明爲private,那麼除了Dog以外的類將不能訪問該方法。若是把bark()聲明爲public,那麼全部的類都可以訪問該方法。若是咱們只想讓該方法對其所在類的子類可見,則將該方法聲明爲protected。 

private:私有的

私有訪問修飾符是最嚴格的訪問級別,因此被聲明爲private的方法、變量和構造方法只能被所屬類訪問,而且類和接口不能聲明爲private。

聲明爲私有訪問類型的變量只能經過類中公共的Getter/Setter方法被外部類訪問。

private訪問修飾符的使用主要用來隱藏類的實現細節和保護類的數據。

下面的類使用了私有訪問修飾符:

public class Dog{
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

 

例子中,Dog類中的name、age變量爲私有變量,因此其餘類不能直接獲得和設置該變量的值。爲了使其餘類可以操做該變量,定義了兩對public方法,getName()/setName() 和 getAge()/setAge(),它們用來獲取和設置私有變量的值。

this 是Java中的一個關鍵字,本章會講到,你能夠點擊 Java this關鍵字詳解 預覽。

在類中定義訪問私有變量的方法,習慣上是這樣命名的:在變量名稱前面加「get」或「set」,並將變量的首字母大寫。例如,獲取私有變量 name 的方法爲 getName(),設置 name 的方法爲 setName()。這些方法常用,也有了特定的稱呼,稱爲 Getter 和 Setter 方法。

默認的:不使用任何關鍵字

不使用任何修飾符聲明的屬性和方法,對同一個包內的類是可見的。接口裏的變量都隱式聲明爲public static final,而接口裏的方法默認狀況下訪問權限爲public。

以下例所示,類、變量和方法的定義沒有使用任何修飾符:

class Dog{
    String name;
    int age;
  
    void bark(){  // 汪汪叫
        System.out.println("汪汪,不要過來");
    }
    void hungry(){  // 飢餓
        System.out.println("主人,我餓了");
    }
}

 

訪問控制和繼承

請注意如下方法繼承(不瞭解繼承概念的讀者能夠跳過這裏,或者點擊 Java繼承和多態 預覽)的規則:

  • 父類中聲明爲public的方法在子類中也必須爲public。

  • 父類中聲明爲protected的方法在子類中要麼聲明爲protected,要麼聲明爲public。不能聲明爲private。

  • 父類中默認修飾符聲明的方法,可以在子類中聲明爲private。

  • 父類中聲明爲private的方法,不可以被繼承。

如何使用訪問控制符

訪問控制符可讓咱們很方便的控制代碼的權限:

    • 當須要讓本身編寫的類被全部的其餘類訪問時,就能夠將類的訪問控制符聲明爲 public。
    • 當須要讓本身的類只能被本身的包中的類訪問時,就能夠省略訪問控制符。
    • 當須要控制一個類中的成員數據時,能夠將這個類中的成員數據訪問控制符設置爲 public、protected,或者省略。

 

 3.3  Java變量的做用域                                                               

 在Java中,變量的做用域分爲四個級別:類級、對象實例級、方法級、塊級

類級變量又稱全局級變量或靜態變量,須要使用static關鍵字修飾,你能夠與 C/C++ 中的 static 變量對比學習。類級變量在類定義後就已經存在,佔用內存空間,能夠經過類名來訪問,不須要實例化。

對象實例級變量就是成員變量,實例化後纔會分配內存空間,才能訪問。

方法級變量就是在方法內部定義的變量,就是局部變量

塊級變量就是定義在一個塊內部的變量,變量的生存週期就是這個塊,出了這個塊就消失了,好比 if、for 語句的塊。塊是指由大括號包圍的代碼,例如:

{
int age = 3;
String name = "www.weixueyuan.net";
// 正確,在塊內部能夠訪問 age 和 name 變量
System.out.println( name + "已經" + age + "歲了");
}
// 錯誤,在塊外部沒法訪問 age 和 name 變量
System.out.println( name + "已經" + age + "歲了");

 


說明:

  • 方法內部除了能訪問方法級的變量,還能夠訪問類級和實例級的變量。
  • 塊內部可以訪問類級、實例級變量,若是塊被包含在方法內部,它還能夠訪問方法級的變量。
  • 方法級和塊級的變量必須被顯示地初始化,不然不能訪問。


演示代碼:

public class Demo{
public static String name = "微學苑"; // 類級變量
public int i; // 對象實例級變量
// 屬性塊,在類初始化屬性時候運行
{
int j = 2;// 塊級變量
}
public void test1() {
int j = 3; // 方法級變量
if(j == 3) {
int k = 5; // 塊級變量
}
// 這裏不能訪問塊級變量,塊級變量只能在塊內部訪問
System.out.println("name=" + name + ", i=" + i + ", j=" + j);
}
public static void main(String[] args) {
// 不建立對象,直接經過類名訪問類級變量
System.out.println(Demo.name);
// 建立對象並訪問它的方法
Demo t = new Demo();
t.test1();
}
}

 

運行結果:
微學苑
name=微學苑, i=0, j=3

 

 (this關鍵字和C++用法同樣)

 (java方法重載和C++用法同樣)

 

 3.4  Java類的基本運行順序                                                                                     

 咱們如下面的類來講明一個基本的 Java 類的運行順序:

public class Demo{
private String name;
private int age;
public Demo(){
name = "微學苑";
age = 3;
}
public static void main(String[] args){
Demo obj = new Demo();
System.out.println(obj.name + "的年齡是" + obj.age);
}
}

 

基本運行順序是:

  1. 先運行到第 9 行,這是程序的入口。
  2. 而後運行到第 10 行,這裏要 new 一個Demo,就要調用 Demo 的構造方法。
  3. 就運行到第 5 行,注意:可能不少人以爲接下來就應該運行第 6 行了,錯!初始化一個類,必須先初始化它的屬性。
  4. 所以運行到第 2 行,而後是第 3 行。
  5. 屬性初始化完事後,纔回到構造方法,執行裏面的代碼,也就是第 6 行、第 7 行。
  6. 而後是第8行,表示 new 一個Demo實例完成。
  7. 而後回到 main 方法中執行第 11 行。
  8. 而後是第 12 行,main方法執行完畢。


做爲程序員,應該清楚程序的基本運行過程,不然糊里糊塗的,不利於編寫代碼,也不利於技術上的發展。

 

 3.5  Java包裝類、拆箱和裝箱詳解   (類型轉換)                                                              

 雖然 Java 語言是典型的面向對象編程語言,但其中的八種基本數據類型並不支持面向對象編程,基本類型的數據不具有「對象」的特性——不攜帶屬性、沒有方法可調用。 沿用它們只是爲了迎合人類根深蒂固的習慣,並的確能簡單、有效地進行常規數據處理。

這種藉助於非面向對象技術的作法有時也會帶來不便,好比引用類型數據均繼承了 Object 類的特性,要轉換爲 String 類型(常常有這種須要)時只要簡單調用 Object 類中定義的toString()便可,而基本數據類型轉換爲 String 類型則要麻煩得多。爲解決此類問題 ,Java爲每種基本數據類型分別設計了對應的類,稱之爲包裝類(Wrapper Classes),也有教材稱爲外覆類數據類型類

基本數據類型及對應的包裝類
基本數據類型 對應的包裝類
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double
boolean Boolean

以上是基本類型;

下面是C++和java中的一些概念

C++      java

類        類

對象    對象   或 實例  (借用類佔對象後的叫類)

類中函數   方法

 

      包   (放功能類似的類的一個文件夾

 


每一個包裝類的對象能夠封裝一個相應的基本類型的數據,並提供了其它一些有用的方法。包裝類對象一經建立,其內容(所封裝的基本類型數據值)不可改變

基本類型和對應的包裝類能夠相互裝換:

裝箱:int→Integer

拆箱:Integer→int

  • 基本類型向對應的包裝類轉換稱爲裝箱,例如把 int 包裝成 Integer 類的對象;
  • 包裝類向對應的基本類型轉換稱爲拆箱,例如把 Integer 類的對象從新簡化爲 int

包裝類的應用

八個包裝類的使用比較類似,下面是常見的應用場景。

1) 實現 int 和 Integer 的相互轉換

能夠經過 Integer 類的構造方法將 int 裝箱,經過 Integer 類的 intValue 方法將 Integer 拆箱。例如:

public class Demo {
public static void main(String[] args) {
int m = 500;
Integer obj = new Integer(m); // 手動裝箱
int n = obj.intValue(); // 手動拆箱
System.out.println("n = " + n);
Integer obj1 = new Integer(500);   //裝箱
System.out.println("obj 等價於 obj1?" + obj.equals(obj1));//拆箱
}
}

 

運行結果:
n = 500
obj 等價於 obj1?true

2) 將字符串轉換爲整數

Integer 類有一個靜態的 paseInt() 方法,能夠將字符串轉換爲整數,語法爲:

parseInt(String s, int radix);

 

s 爲要轉換的字符串,radix 爲進制,可選,默認爲十進制

下面的代碼將會告訴你什麼樣的字符串能夠轉換爲整數:

public class Demo {
public static void main(String[] args) {
String str[] = {"123", "123abc", "abc123", "abcxyz"};
for(String str1 : str){                    //怎麼理解
try{
int m = Integer.parseInt(str1, 10);
System.out.println(str1 + " 能夠轉換爲整數 " + m);
}catch(Exception e){
System.out.println(str1 + " 沒法轉換爲整數");
}
}
}
}

 

運行結果:
123 能夠轉換爲整數 123
123abc 沒法轉換爲整數
abc123 沒法轉換爲整數
abcxyz 沒法轉換爲整數

3) 將整數轉換爲字符串

Integer 類有一個靜態的 toString() 方法,能夠將整數轉換爲字符串。例如:

public class Demo {
public static void main(String[] args) {
int m = 500;
String s = Integer.toString(m);
System.out.println("s = " + s);
}
}

 

運行結果:
s = 500

自動拆箱和裝箱

上面的例子都須要手動實例化一個包裝類,稱爲手動拆箱裝箱。Java 1.5(5.0) 以前必須手動拆箱裝箱。

Java 1.5 以後能夠自動拆箱裝箱,也就是在進行基本數據類型和對應的包裝類轉換時,系統將自動進行,這將大大方便程序員的代碼書寫。例如:

public class Demo {
public static void main(String[] args) {
int m = 500;
Integer obj = m; // 自動裝箱
int n = obj; // 自動拆箱
System.out.println("n = " + n);
Integer obj1 = 500;
System.out.println("obj 等價於 obj1?" + obj.equals(obj1));  //判斷相等運算
}
}

 

運行結果:
n = 500
obj 等價於 obj1?true

自動拆箱裝箱是經常使用的一個功能,讀者須要重點掌握。

 

 3.6   再談Java包                                                                                  

 在Java中,爲了組織代碼的方便,能夠將功能類似的類放到一個文件夾內這個文件夾就叫作

包不但能夠包含類,還能夠包含接口和其餘的包。

目錄以"\"來表示層級關係,例如 E:\Java\workspace\Demo\bin\p1\p2\Test.java。

以"."來表示層級關係,例如 p1.p2.Test 表示的目錄爲 \p1\p2\Test.class。

如何實現包

經過 package 關鍵字能夠聲明一個包,例如:
    package p1.p2;
必須將 package 語句放在全部語句的前面,例如:

package p1.p2;
public class Test {
public Test(){
System.out.println("我是Test類的構造方法");
}
}

代表 Test 類位於 p1.p2 包中。

包的調用

在Java中,調用其餘包中的類共有兩種方式。

方法1) 在每一個類名前面加上完整的包名

程序舉例:

public class Demo {
public static void main(String[] args) {
java.util.Date today=new java.util.Date();  
System.out.println(today);
}
}

 

運行結果:
Wed Dec 03 11:20:13 CST 2014

方法2) 經過 import 語句引入包中的類

程序舉例:

import java.util.Date;
// 也能夠引入 java.util 包中的全部類
// import java.util.*;
public class Demo {
public static void main(String[] args) {
Date today=new Date();
System.out.println(today);
}
}

 

運行結果與上面相同。

實際編程中,沒有必要把要引入的類寫的那麼詳細,能夠直接引入特定包中全部的類,例如 import java.util.*;。

類的路徑

Java 在導入類時必需要知道類的絕對路徑

首先在 E:\Java\workspace\Demo\src\p0\ 目錄(E:\Java\workspace\Demo\src\ 是項目源文件的根目錄)下建立 Demo.java,輸入以下代碼:

package p0;
import p1.p2.Test;
public class Demo{
public static void main(String[] args){
Test obj = new Test();
}
}

再在 E:\Java\workspace\Demo\src\p1\p2 目錄下建立 Test.java,輸入以下代碼:

package p1.p2;
public class Test {
public Test(){
System.out.println("我是Test類的構造方法");
}
}

假設咱們將 classpath 環境變量設置爲 .;D:\Program Files\jdk1.7.0_71\lib,源文件 Demo.java 開頭有 import p1.p2.Test; 語句,那麼編譯器會先檢查 E:\Java\workspace\Demo\src\p0\p1\p2\ 目錄下是否存在 Test.java 或 Test.class 文件若是不存在,會繼續檢索 D:\Program Files\jdk1.7.0_71\lib\p1\p2\ 目錄兩個目錄下都不存在就會報錯。顯然,Test.java 位於 E:\Java\workspace\Demo\src\p1\p2\ 目錄,編譯器找不到,會報錯,怎麼辦呢?

能夠經過 javac 命令的 classpath 選項來指定類路徑。

打開CMD,進入 Demo.java 文件所在目錄,執行 javac 命令,並將 classpath 設置爲 E:\Java\workspace\Demo\src,以下圖所示:


運行Java程序時,也須要知道類的絕對路徑,除了 classpath 環境變量指定的路徑,也能夠經過 java 命令的 classpath 選項來增長路徑,以下圖所示:


注意 java 命令與 javac 命令的區別,執行 javac 命令須要進入當前目錄,而執行 java 命令須要進入當前目錄的上級目錄,而且類名前面要帶上包名。

能夠這樣來理解,javac是一個平臺命令,它對具體的平臺文件進行操做,要指明被編譯的文件路徑。而java是一個虛擬機命令,它對類操做,即對類的描述要用 點 分的描述形式,而且不能加擴展名,還要注意類名的大小寫

這些命令比較繁雜,實際開發都須要藉助 Eclipse,在Eclipse下管理包、編譯運行程序都很是方便。Eclipse 實際上也是執行這些命令。

包的訪問權限

被聲明爲 public 的類、方法或成員變量,能夠被任何包下的任何類使用,而聲明爲 private 的類、方法或成員變量,只能被本類使用。

沒有任何修飾符的類、方法和成員變量,只能被本包中的全部類訪問,在包之外任何類都沒法訪問它

 

3.7  Java源文件的聲明規則                                                                           

當在一個源文件中定義多個類,而且還有import語句和package語句時,要特別注意這些規則:

  • 一個源文件中只能有一個public類
  • 一個源文件能夠有多個非public類。
  • 源文件的名稱應該和public類的類名保持一致。例如:源文件中public類的類名是Employee,那麼源文件應該命名爲Employee.java。
  • 若是一個類定義在某個包中,那麼package語句應該在源文件的首行。
  • 若是源文件包含import語句,那麼應該放在package語句和類定義之間。若是沒有package語句,那麼import語句應該在源文件中最前面。
  • import語句和package語句對源文件中定義的全部類都有效。在同一源文件中,不能給不一樣的類不一樣的包聲明
  • 類有若干種訪問級別,而且類也分不一樣的類型:抽象類和final類等。這些將在後續章節介紹。
  • 除了上面提到的幾種類型,Java還有一些特殊的類,如內部類、匿名類。 

一個簡單的例子

在該例子中,咱們建立兩個類 Employee 和 EmployeeTest,分別放在包 p1 和 p2 中。

Employee類有四個成員變量,分別是 name、age、designation和salary。該類顯式聲明瞭一個構造方法,該方法只有一個參數。

在Eclipse中,創建一個包,命名爲 p1,在該包中建立一個類,命名爲 Employee,將下面的代碼複製到源文件中:

package p1;
public class Employee{
String name;
int age;
String designation;
double salary;
// Employee 類的構造方法
public Employee(String name){
this.name = name;
}
// 設置age的值
public void empAge(int empAge){
age = empAge;
}
// 設置designation的值
public void empDesignation(String empDesig){
designation = empDesig;
}
// 設置salary的值
public void empSalary(double empSalary){
salary = empSalary;
}
// 輸出信息
public void printEmployee(){
System.out.println("Name:"+ name );
System.out.println("Age:" + age );
System.out.println("Designation:" + designation );
System.out.println("Salary:" + salary);
}
}

程序都是從main方法開始執行。爲了能運行這個程序,必須包含main方法而且建立一個對象。

下面給出EmployeeTest類,該類建立兩個Employee對象,並調用方法設置變量的值。

在Eclipse中再建立一個包,命名爲 p2,在該包中建立一個類,命名爲 EmployeeTest,將下面的代碼複製到源文件中:

package p2;
import p1.*;  //這些是在EmployeeTest以外 public class EmployeeTest{
public static void main(String args[]){
// 建立兩個對象
Employee empOne = new Employee("James Smith");
Employee empTwo = new Employee("Mary Anne");
// 調用這兩個對象的成員方法
empOne.empAge(26);
empOne.empDesignation("Senior Software Engineer");
empOne.empSalary(1000);
empOne.printEmployee();
empTwo.empAge(21);
empTwo.empDesignation("Software Engineer");
empTwo.empSalary(500);
empTwo.printEmployee();
}
}

 

編譯並運行 EmployeeTest 類,能夠看到以下的輸出結果:
Name:James Smith
Age:26
Designation:Senior Software Engineer
Salary:1000.0
Name:Mary Anne
Age:21
Designation:Software Engineer
Salary:500.0

 

 

 四◐  java繼承和多態                                                    

4.1   java中繼承的概念與實現                                                              

繼承是類與類之間的關係,是一個很簡單很直觀的概念,與現實世界中的繼承(例如兒子繼承父親財產)相似。

繼承能夠理解爲一個類從另外一個類獲取方法和屬性的過程。若是類B繼承於類A,那麼B就擁有A的方法和屬性。

繼承使用 extends 關鍵字

例如咱們已經定義了一個類 People:

class People{
String name;
int age;
int height;
void say(){
System.out.println("個人名字是 " + name + ",年齡是 " + age + ",身高是 " + height);
}
}

 

若是如今須要定義一個類 Teacher,它也有 name、age、height 屬性和 say() 方法,另外還須要增長 school、seniority、subject 屬性和 lecturing() 方法,怎麼辦呢?咱們要從新定義一個類嗎?

徹底不必,能夠先繼承 People 類的成員,再增長本身的成員便可,例如:

class Teacher extends People{         //在C++中 class teacher:public student
String school; // 所在學校
String subject; // 學科
int seniority; // 教齡
// 覆蓋 People 類中的 say() 方法
void say(){
System.out.println("我叫" + name + ",在" + school + "教" + subject + ",有" + seniority + "年教齡");
}
void lecturing(){
System.out.println("我已經" + age + "歲了,依然站在講臺上講課");
}
}

 

對程序的說明

  • name 和 age 變量雖然沒有在 Teacher 中定義,可是已在 People 中定義,能夠直接拿來用。
  • Teacher 是 People 的子類,People 是Teacher 類的父類。
  • 子類能夠覆蓋父類的方法。
  • 子類能夠繼承父類除private覺得的全部的成員。
  • 構造方法不能被繼承。


繼承是在維護和可靠性方面的一個偉大進步。若是在 People 類中進行修改,那麼 Teacher 類就會自動修改,而不須要程序員作任何工做,除了對它進行編譯。

單繼承性:Java 容許一個類僅能繼承一個其它類,即一個類只能有一個父類,這個限制被稱作單繼承性。後面將會學到接口(interface)的概念,接口容許多繼承。(這一點是與C++不一樣的了,在C++中容許多繼承,但沒有接口interface這一說)

最後對上面的代碼進行整理:

public class Demo {
public static void main(String[] args) {
Teacher t = new Teacher();
t.name = "小布";
t.age = 70;
t.school = "清華大學";
t.subject = "Java";
t.seniority = 12;
t.say();
t.lecturing();
}
}
class People{
String name;
int age;
int height;
void say(){
System.out.println("個人名字是 " + name + ",年齡是 " + age + ",身高是 " + height);
}
}
class Teacher extends People{
String school; // 所在學校
String subject; // 學科
int seniority; // 教齡
// 覆蓋 People 類中的 say() 方法
void say(){
System.out.println("我叫" + name + ",在" + school + "教" + subject + ",有" + seniority + "年教齡");
}
void lecturing(){
System.out.println("我已經" + age + "歲了,依然站在講臺上講課");
}
}

 

運行結果:
我叫小布,在清華大學教Java,有12年教齡
我已經70歲了,依然站在講臺上講課

注意:構造方法不能被繼承,掌握這一點很重要。 一個類能獲得構造方法,只有兩個辦法:編寫構造方法,或者根本沒有構造方法,類有一個默認的構造方法。

 

4.2   java super 關鍵字                                                                         

super 關鍵字與 this 相似,this 用來表示當前類的實例,super 用來表示父類

super 能夠用在子類中,經過點號(.)來獲取父類的成員變量和方法。super 也能夠用在子類的子類中,Java 能自動向上層類追溯。

父類行爲被調用,就好象該行爲是本類的行爲同樣,並且調用行爲沒必要發生在父類中,它能自動向上層類追溯。

super 關鍵字的功能:
  • 調用父類中聲明爲 private 的變量(意味着public成員能夠直接調用)在C++中 子類是嚴格不能調用父類中私有成員的! 
  • 點取已經覆蓋了的方法。
  • 做爲方法名錶示父類構造方法。

調用隱藏變量和被覆蓋的方法

public class Demo{
public static void main(String[] args) {
Dog obj = new Dog();
obj.move();
}
}
class Animal{
private String desc = "Animals are human's good friends";
// 必需要聲明一個 getter 方法
public String getDesc() { return desc; }
public void move(){
System.out.println("Animals can move");
}
}
class Dog extends Animal{
public void move(){
super.move(); // 調用父類的方法
System.out.println("Dogs can walk and run");
// 經過 getter 方法調用父類隱藏變量
System.out.println("Please remember: " + super.getDesc());
}
}

 

運行結果:
Animals can move
Dogs can walk and run
Please remember: Animals are human's good friends

move() 方法也能夠定義在某些祖先類中,好比父類的父類,Java 具備追溯性,會一直向上找,直到找到該方法爲止。

經過 super 調用父類的隱藏變量,必需要在父類中聲明 getter 方法,由於聲明爲 private 的數據成員對子類是不可見的。

調用父類的構造方法

在許多狀況下,使用默認構造方法來對父類對象進行初始化。固然也能夠使用 super 來顯示調用父類的構造方法。
public class Demo{
public static void main(String[] args) {
Dog obj = new Dog("花花", 3);
obj.say();
}
}
class Animal{
String name;
public Animal(String name){
this.name = name;
}
}
class Dog extends Animal{
int age;
public Dog(String name, int age){
super(name);                                     
this.age = age;
}
public void say(){
System.out.println("我是一隻可愛的小狗,個人名字叫" + name + ",我" + age + "歲了");
}
}

 

運行結果:
我是一隻可愛的小狗,個人名字叫花花,我3歲了
 
與C++比較:
//java
public Dog(String name, int age){
super(name);  this.age = age;
}
//C++
 Student::Student(char *name, int age, float score): People(name, age){ this->score = score; }
注意:不管是 super() 仍是 this(),都必須放在構造方法的第一行。

值得注意的是:
  • 在構造方法中調用另外一個構造方法,調用動做必須置於最起始的位置。
  • 不能在構造方法之外的任何方法內調用構造方法。
  • 在一個構造方法內只能調用一個構造方法。

若是編寫一個構造方法,既沒有調用 super() 也沒有調用 this(),編譯器會自動插入一個調用到父類構造方法中,並且不帶參數( 和C++同樣)。 

最後注意 super 與 this 的區別:super 不是一個對象的引用,不能將 super 賦值給另外一個對象變量,它只是一個指示編譯器調用父類方法的特殊關鍵字。
 
4.3  Java繼承中方法的覆蓋和重載                                                  

在類繼承中,子類能夠修改從父類繼承來的方法,也就是說子類能建立一個與父類方法有不一樣功能的方法,但具備相同的名稱、返回值類型、參數列表。

若是在新類中定義一個方法,其名稱、返回值類型和參數列表正好與父類中的相同,那麼,新方法被稱作覆蓋舊方法

參數列表又叫參數簽名,包括參數的類型、參數的個數和參數的順序,只要有一個不一樣就叫作參數列表不一樣。

被覆蓋的方法在子類中只能經過super調用

注意:覆蓋不會刪除父類中的方法,而是對子類的實例隱藏,暫時不使用。
也就是說,當子類中有雨父類如出一轍的方法(函數)時,父類中該函數雖然是public但在子類中倒是隱藏的,要想調用父類中這個函數須要用super;
請看下面的例子:

public class Demo{
public static void main(String[] args) {
Dog myDog = new Dog("花花");
myDog.say(); // 子類的實例調用子類中的方法
Animal myAnmial = new Animal("貝貝");
myAnmial.say(); // 父類的實例調用父類中的方法
}
}
class Animal{
String name;
public Animal(String name){
this.name = name;
}
public void say(){
System.out.println("我是一隻小動物,個人名字叫" + name + ",我會發出叫聲");
}
}
class Dog extends Animal{
// 構造方法不能被繼承,經過super()調用
public Dog(String name){
super(name);
}
// 覆蓋say() 方法
public void say(){
System.out.println("我是一隻小狗,個人名字叫" + name + ",我會發出汪汪的叫聲");
}
}

 

運行結果:
我是一隻小狗,個人名字叫花花,我會發出汪汪的叫聲
我是一隻小動物,個人名字叫貝貝,我會發出叫聲

方法覆蓋的原則:

  • 覆蓋方法的返回類型、方法名稱、參數列表必須與原方法的相同。
  • 覆蓋方法不能比原方法訪問性差(即訪問權限不容許縮小)。
  • 覆蓋方法不能比原方法拋出更多的異常。
  • 被覆蓋的方法不能是final類型,由於final修飾的方法是沒法覆蓋的
  • 被覆蓋的方法不能爲private,不然在其子類中只是新定義了一個方法,並無對其進行覆蓋。
  • 被覆蓋的方法不能爲static。若是父類中的方法爲靜態的,而子類中的方法不是靜態的,可是兩個方法除了這一點外其餘都知足覆蓋條件,那麼會發生編譯錯誤;反之亦然。即便父類和子類中的方法都是靜態的,而且知足覆蓋條件,可是仍然不會發生覆蓋,由於靜態方法是在編譯的時候把靜態方法和類的引用類型進行匹配。


方法的重載:
前面已經對Java方法重載進行了說明,這裏再強調一下,Java父類和子類中的方法都會參與重載,例如,父類中有一個方法是 func(){ ... },子類中有一個方法是 func(int i){ ... },就構成了方法的重載。

覆蓋和重載的不一樣:

    • 方法覆蓋要求參數列表必須一致,而方法重載要求參數列表必須不一致。
    • 方法覆蓋要求返回類型必須一致,方法重載對此沒有要求。
    • 方法覆蓋只能用於子類覆蓋父類的方法,方法重載用於同一個類中的全部方法(包括從父類中繼承而來的方法)。
    • 方法覆蓋對方法的訪問權限和拋出的異常有特殊的要求,而方法重載在這方面沒有任何限制。
    • 父類的一個方法只能被子類覆蓋一次,而一個方法能夠在全部的類中能夠被重載屢次。

 

4.4  Java多態和動態綁定                                                              

 在Java中,父類的變量能夠引用父類的實例,也能夠引用子類的實例。 

請讀者先看一段代碼:

public class Demo {
    public static void main(String[] args){
        Animal obj = new Animal();
        obj.cry();
        obj = new Cat();
        obj.cry();
        obj = new Dog();
        obj.cry();
    }
}
class Animal{
    // 動物的叫聲
    public void cry(){
        System.out.println("不知道怎麼叫");
    }
   
}
class Cat extends Animal{
    // 貓的叫聲
    public void cry(){
        System.out.println("喵喵~");
    }
}
class Dog extends Animal{
    // 狗的叫聲
    public void cry(){
        System.out.println("汪汪~");
    }
}

 

運行結果:
不知道怎麼叫
喵喵~
汪汪~

上面的代碼,定義了三個類,分別是 Animal、Cat 和 Dog,Cat 和 Dog 類都繼承自 Animal 類。obj 變量的類型爲 Animal,它既能夠指向 Animal 類的實例,也能夠指向 Cat 和 Dog 類的實例,這是正確的。也就是說,父類的變量能夠引用父類的實例,也能夠引用子類的實例。注意反過來是錯誤的,由於全部的貓都是動物,但不是全部的動物都是貓。

能夠看出,obj 既能夠是人類,也能夠是貓、狗,它有不一樣的表現形式,這就被稱爲多態。多態是指一個事物有不一樣的表現形式或形態

再好比「人類」,也有不少不一樣的表達或實現,TA 能夠是司機、教師、醫生等,你憎恨本身的時候會說「下輩子從新作人」,那麼你下輩子成爲司機、教師、醫生均可以,咱們就說「人類」具有了多態性。

多態存在的三個必要條件:要有繼承、要有重寫、父類變量引用子類對象。

當使用多態方式調用方法時:

  • 首先檢查父類中是否有該方法,若是沒有,則編譯錯誤;若是有,則檢查子類是否覆蓋了該方法
  • 若是子類覆蓋了該方法,就調用子類的方法,不然調用父類方法。


從上面的例子能夠看出,多態的一個好處是:當子類比較多時,也不須要定義多個變量,能夠只定義一個父類類型的變量來引用不一樣子類的實例。請再看下面的一個例子:

public class Demo {
public static void main(String[] args){
// 藉助多態,主人能夠給不少動物餵食
Master ma = new Master();
ma.feed(new Animal(), new Food());
ma.feed(new Cat(), new Fish());
ma.feed(new Dog(), new Bone());
}
}
// Animal類及其子類
class Animal{
public void eat(Food f){
System.out.println("我是一個小動物,正在吃" + f.getFood());
}
}
class Cat extends Animal{
public void eat(Food f){
System.out.println("我是一隻小貓咪,正在吃" + f.getFood());
}
}
class Dog extends Animal{
public void eat(Food f){
System.out.println("我是一隻狗狗,正在吃" + f.getFood());
}
}
// Food及其子類
class Food{
public String getFood(){
return "事物";
}
}
class Fish extends Food{
public String getFood(){
return "魚";
}
}
class Bone extends Food{
public String getFood(){
return "骨頭";
}
}
// Master類
class Master{
public void feed(Animal an, Food f){
an.eat(f);
}
}

 

運行結果:
我是一個小動物,正在吃事物
我是一隻小貓咪,正在吃魚
我是一隻狗狗,正在吃骨頭

Master 類的 feed 方法有兩個參數,分別是 Animal 類型和 Food 類型,由於是父類,因此能夠將子類的實例傳遞給它,這樣 Master 類就不須要多個方法來給不一樣的動物餵食。

動態綁定

爲了理解多態的本質,下面講一下Java調用方法的詳細流程。

1) 編譯器查看對象的聲明類型和方法名。

假設調用 obj.func(param),obj 爲 Cat 類的對象。須要注意的是,有可能存在多個名字爲func但參數簽名不同的方法。例如,可能存在方法 func(int) 和 func(String)。編譯器將會一一列舉全部 Cat 類中名爲func的方法和其父類 Animal 中訪問屬性爲 public 且名爲func的方法。

這樣,編譯器就得到了全部可能被調用的候選方法列表。

2) 接下來,編澤器將檢查調用方法時提供的參數簽名。

若是在全部名爲func的方法中存在一個與提供的參數簽名徹底匹配的方法,那麼就選擇這個方法。這個過程被稱爲重載解析(overloading resolution)。例如,若是調用 func("hello"),編譯器會選擇 func(String),而不是 func(int)。因爲自動類型轉換的存在,例如 int 能夠轉換爲 double,若是沒有找到與調用方法參數簽名相同的方法,就進行類型轉換後再繼續查找,若是最終沒有匹配的類型或者有多個方法與之匹配,那麼編譯錯誤。

這樣,編譯器就得到了須要調用的方法名字和參數簽名。

3) 若是方法的修飾符是private、static、final(static和final將在後續講解),或者是構造方法,那麼編譯器將能夠準確地知道應該調用哪一個方法,咱們將這種調用方式 稱爲靜態綁定(static binding)。(說白了是編譯器編譯那些已經定好了)

與此對應的是,調用的方法依賴於對象的實際類型, 並在運行時實現動態綁。例如調用 func("hello"),編澤器將採用動態綁定的方式生成一條調用 func(String) 的指令。(說白了就是編譯的時候編譯器有選擇的進行編譯)

4)當程序運行,而且釆用動態綁定調用方法時,JVM必定會調用與 obj 所引用對象的實際類型最合適的那個類的方法。咱們已經假設 obj 的實際類型是 Cat,它是 Animal 的子類,若是 Cat 中定義了 func(String),就調用它,不然將在 Animal 類及其父類中尋找。

每次調用方法都要進行搜索,時間開銷至關大,所以,JVM預先爲每一個類建立了一個方法表(method lable),其中列出了全部方法的名稱、參數簽名和所屬的類。這樣一來,在真正調用方法的時候,虛擬機僅查找這個表就好了。在上面的例子中,JVM 搜索 Cat 類的方法表,以便尋找與調用 func("hello") 相匹配的方法。這個方法既有多是 Cat.func(String),也有多是 Animal.func(String)。注意,若是調用super.func("hello"),編譯器將對父類的方法表迸行搜索。

假設 Animal 類包含cry()、getName()、getAge() 三個方法,那麼它的方法表以下:
cry() -> Animal.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()

實際上,Animal 也有默認的父類 Object(後續會講解),會繼承 Object 的方法,因此上面列舉的方法並不完整。

假設 Cat 類覆蓋了 Animal 類中的 cry() 方法,而且新增了一個方法 climbTree(),那麼它的參數列表爲:
cry() -> Cat.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()
climbTree() -> Cat.climbTree()

在運行的時候,調用 obj.cry() 方法的過程以下:

    • JVM 首先訪問 obj 的實際類型的方法表,多是 Animal 類的方法表,也多是 Cat 類及其子類的方法表。
    • JVM 在方法表中搜索與 cry() 匹配的方法,找到後,就知道它屬於哪一個類了。
    • JVM 調用該方法。

 

4.5  Java instanceof 運算符                                                                           

 多態性帶來了一個問題,就是如何判斷一個變量所實際引用的對象的類型C++使用runtime-type information(RTTI),Java 使用 instanceof 操做符。

instanceof 運算符用來判斷一個變量所引用的對象實際類型,注意是它引用的對象的類型,不是變量的類型。請看下面的代碼:

public final class Demo{
public static void main(String[] args) {
// 引用 People 類的實例
People obj = new People();
if(obj instanceof Object){
System.out.println("我是一個對象");
}
if(obj instanceof People){
System.out.println("我是人類");
}
if(obj instanceof Teacher){
System.out.println("我是一名教師");
}
if(obj instanceof President){
System.out.println("我是校長");
}
System.out.println("-----------"); // 分界線
// 引用 Teacher 類的實例
obj = new Teacher();
if(obj instanceof Object){
System.out.println("我是一個對象");
}
if(obj instanceof People){
System.out.println("我是人類");
}
if(obj instanceof Teacher){
System.out.println("我是一名教師");
}
if(obj instanceof President){
System.out.println("我是校長");
}
}
}
class People{ }
class Teacher extends People{ }
class President extends Teacher{ }

 

運行結果:
我是一個對象
我是人類
-----------
我是一個對象
我是人類
我是一名教師

 

 

說白了是     前者  instanceof  後者; 先後二者嫡系  前者大於等於後者  前者是實例 後者是類
能夠看出,若是變量引用的是當前類或它的子類的實例,instanceof 返回 true,不然返回 false

 

4.6  多態對象的類型轉換                                                                       

 這裏所說的對象類型轉換,是指存在繼承關係的對象,不是任意類型的對象當對不存在繼承關係的對象進行強制類型轉換時,java 運行時將拋出 java.lang.ClassCastException 異常

在繼承鏈中,咱們將子類向父類轉換稱爲「向上轉型」,將父類向子類轉換稱爲「向下轉型」

不少時候,咱們會將變量定義爲父類的類型,卻引用子類的對象,這個過程就是向上轉型。程序運行時經過動態綁定來實現對子類方法的調用,也就是多態性。

然而有些時候爲了完成某些父類沒有的功能,咱們須要將向上轉型後的子類對象再轉成子類,調用子類的方法,這就是向下轉型。

注意:不能直接將父類的對象強制轉換爲子類類型,只能將向上轉型後的子類對象再次轉換爲子類類型。也就是說,子類對象必須向上轉型後,才能再向下轉型。請看下面的代碼:

public class Demo {
public static void main(String args[]) {
SuperClass superObj = new SuperClass();
SonClass sonObj = new SonClass();
// 下面的代碼運行時會拋出異常,不能將父類對象直接轉換爲子類類型 // SonClass sonObj2 = (SonClass)superObj;
// 先向上轉型,再向下轉型
superObj = sonObj;   //實際的效果是完成地址轉移 SonClass sonObj1 = (SonClass)superObj;
}
}
class SuperClass{ }
class SonClass extends SuperClass{ }

 

將第7行的註釋去掉,運行時會拋出異常,可是編譯能夠經過。

由於向下轉型存在風險,因此在接收到父類的一個引用時,請務必使用 instanceof 運算符來判斷該對象是不是你所要的子類,請看下面的代碼:

public class Demo {
public static void main(String args[]) {
SuperClass superObj = new SuperClass();
SonClass sonObj = new SonClass();
// superObj 不是 SonClass 類的實例               
if(superObj instanceof SonClass){    
SonClass sonObj1 = (SonClass)superObj;
}else{
System.out.println("①不能轉換");
}
superObj = sonObj;
// superObj 是 SonClass 類的實例
if(superObj instanceof SonClass){
SonClass sonObj2 = (SonClass)superObj;
}else{
System.out.println("②不能轉換");
}
}
}
class SuperClass{ }
class SonClass extends SuperClass{ }

 

運行結果:
①不能轉換

總結:對象的類型轉換在程序運行時檢查,向上轉型會自動進行,向下轉型的對象必須是當前引用類型的子類。

 

4.7  Java static關鍵字以及Java靜態變量和靜態方法                                                    

static 修飾符可以與變量、方法一塊兒使用,表示是「靜態」的。

靜態變量和靜態方法可以經過類名來訪問,不須要建立一個類的對象來訪問該類的靜態成員,因此static修飾的成員又稱做類變量和類方法。靜態變量與實例變量不一樣,實例變量老是經過對象來訪問,由於它們的值在對象和對象之間有所不一樣。

請看下面的例子:

public class Demo {
static int i = 10;
int j;
Demo() {
this.j = 20;
}
public static void main(String[] args) {
System.out.println("類變量 i=" + Demo.i);
Demo obj = new Demo();        //這樣能夠直接用
System.out.println("實例變量 j=" + obj.j);
}
}

 

運行結果:
類變量 i=10
實例變量 j=20

static 的內存分配

靜態變量屬於類不屬於任何獨立的對象,因此無需建立類的實例就能夠訪問靜態變量。之因此會產生這樣的結果,是由於編譯器只爲整個類建立了一個靜態變量的副本,也就是隻分配一個內存空間,雖然有多個實例,但這些實例共享該內存。實例變量則不一樣,每建立一個對象,都會分配一次內存空間,不一樣變量的內存相互獨立,互不影響,改變 a 對象的實例變量不會影響 b 對象。

請看下面的代碼:

public class Demo {
static int i;   //i只佔一塊固定的內存 int j;
public static void main(String[] args) {
Demo obj1 = new Demo();
obj1.i = 10;
obj1.j = 20;
Demo obj2 = new Demo();
System.out.println("obj1.i=" + obj1.i + ", obj1.j=" + obj1.j);
System.out.println("obj2.i=" + obj2.i + ", obj2.j=" + obj2.j);
}
}

 

運行結果:
obj1.i=10, obj1.j=20
obj2.i=10, obj2.j=0

注意:靜態變量雖然也能夠經過對象來訪問,可是不被提倡,編譯器也會產生警告。

上面的代碼中,i 是靜態變量,經過 obj1 改變 i 的值,會影響到 obj2;j 是實例變量,經過 obj1 改變 j 的值,不會影響到 obj2。這是由於 obj1.i 和 obj2.i 指向同一個內存空間,而 obj1.j 和 obj2.j 指向不一樣的內存空間,請看下圖:


圖1  靜態變量內存分配


注意:static 的變量是在類裝載的時候就會被初始化。也就是說,只要類被裝載,無論你是否使用了這個static 變量,它都會被初始化。

小結:類變量(class variables)用關鍵字 static 修飾,在類加載的時候,分配類變量的內存,之後再生成類的實例對象時,將共享這塊內存(類變量),任何一個對象對類變量的修改,都會影響其它對象。外部有兩種訪問方式:經過對象來訪問或經過類名來訪問。

靜態方法

靜態方法是一種不能向對象實施操做的方法。例如,Math 類的 pow() 方法就是一個靜態方法,語法爲 Math.pow(x, a),用來計算 x 的 a 次冪,在使用時無需建立任何 Math 對象。

由於靜態方法不能操做對象,因此不能在靜態方法中訪問實例變量,只能訪問自身類的靜態變量。

如下情形能夠使用靜態方法:

  • 一個方法不須要訪問對象狀態,其所需參數都是經過顯式參數提供(例如 Math.pow())。
  • 一個方法只須要訪問類的靜態變量


讀者確定注意到,main() 也是一個靜態方法,不對任何對象進行操做。實際上,在程序啓動時尚未任何對象,main() 方法是程序的入口,將被執行並建立程序所需的對象。

關於靜態變量和靜態方法的總結:

  • 一個類的靜態方法只能訪問靜態變量;
  • 一個類的靜態方法不可以直接調用非靜態方法;
  • 如訪問控制權限容許,靜態變量和靜態方法也能夠經過對象來訪問,可是不被推薦;
  • 靜態方法中不存在當前對象,於是不能使用 this,固然也不能使用 super
  • 靜態方法不能被非靜態方法覆蓋;
  • 構造方法不容許聲明爲 static 的;
  • 局部變量不能使用static修飾。


靜態方法舉例:

public class Demo {
static int sum(int x, int y){
return x + y;
}
public static void main(String[] args) {
//這裏沒有任何 「實例化」 下面是直接調用
int sum = Demo.sum(10, 10); System.out.println("10+10=" + sum); } }

 

運行結果:
10+10=20

static 方法不需它所屬的類的任何實例就會被調用,所以沒有 this 值,不能訪問實例變量,不然會引發編譯錯誤。

注意:實例變量只能經過對象來訪問,不能經過類訪問。

靜態初始器(靜態塊)

塊是由大括號包圍的一段代碼。靜態初始器(Static Initializer)是一個存在於類中、方法外面的靜態塊。靜態初始器僅僅在類裝載的時候(第一次使用類的時候)執行一次,每每用來初始化靜態變量。

示例代碼:

public class Demo {
public static int i;
static{
i = 10;
System.out.println("Now in static block.");
}
public void test() {
System.out.println("test method: i=" + i);
}
public static void main(String[] args) {
System.out.println("Demo.i=" + Demo.i);
new Demo().test();
}
}

 

運行結果是:
Now in static block.
Demo.i=10
test method: i=10

靜態導入

靜態導入是 Java 5 的新增特性,用來導入類的靜態變量和靜態方法。

通常咱們導入類都這樣寫:

import packageName.className; // 導入某個特定的類

 

import packageName.*; // 導入包中的全部類

 


靜態導入能夠這樣寫:

import static packageName.className.methonName; // 導入某個特定的靜態方法

 

import static packageName.className.*; // 導入類中的全部靜態成員

 


導入後,能夠在當前類中直接用方法名調用靜態方法,沒必要再用 className.methodName 來訪問。

對於使用頻繁的靜態變量和靜態方法,能夠將其靜態導入。靜態導入的好處是能夠簡化一些操做,例如輸出語句 System.out.println(); 中的 out 就是 System 類的靜態變量,能夠經過 import static java.lang.System.*; 將其導入,下次直接調用 out.println() 就能夠了。

請看下面的代碼:

import static java.lang.System.*;
import static java.lang.Math.random;
public class Demo {
public static void main(String[] args) {
out.println("產生的一個隨機數:" + random());
}
}

 

運行結果:
產生的一個隨機數:0.05800891549018705

 

 4.8  Java final關鍵字:阻止繼承和多態                                                                          

 在 Java 中,聲明類、變量和方法時,可以使用關鍵字 final 來修飾。final 所修飾的數據具備「終態」的特徵,表示「最終的」意思。具體規定以下:

  • final 修飾的類不能被繼承。
  • final 修飾的方法不能被子類重寫。
  • final 修飾的變量(成員變量或局部變量)即成爲常量,只能賦值一次。
  • final 修飾的成員變量必須在聲明的同時賦值若是在聲明的時候沒有賦值,那麼只有 一次賦值的機會,並且只能在構造方法中顯式賦值,而後才能使用
  • final 修飾的局部變量能夠只聲明不賦值,而後再進行一次性的賦值。


final 通常用於修飾那些通用性的功能、實現方式或取值不能隨意被改變的數據,以免被誤用,例如實現數學三角方法、冪運算等功能的方法,以及數學常量π=3.14159三、e=2.71828 等。

事實上,爲確保終態性,提供了上述方法和常量的 java.lang.Math 類也已被定義爲final 的。

須要注意的是,若是將引用類型(任何類的類型)的變量標記爲 final,那麼該變量不能指向任何其它對象。但能夠改變對象的內容,由於只有引用自己是 final 的。

若是變量被標記爲 final,其結果是使它成爲常數。想改變 final 變量的值會致使一個編譯錯誤。下面是一個正肯定義 final 變量的例子:

public final int MAX_ARRAY_SIZE = 25; // 常量名通常大寫

 

常量由於有 final 修飾,因此不能被繼承。

請看下面的代碼:

public final class Demo{
public static final int TOTAL_NUMBER = 5;
public int id;
public Demo() {
// 非法,對final變量TOTAL_NUMBER進行二次賦值了
// 由於++TOTAL_NUMBER至關於 TOTAL_NUMBER=TOTAL_NUMBER+1
id = ++TOTAL_NUMBER;
}
public static void main(String[] args) {
final Demo t = new Demo();
final int i = 10;
final int j;
j = 20;
j = 30; // 非法,對final變量進行二次賦值
}
}

 


final 也能夠用來修飾類(放在 class 關鍵字前面),阻止該類再派生出子類,例如 Java.lang.String 就是一個 final 類。這樣作是出於安全緣由,由於要保證一旦有字符串的引用,就必須是類 String 的字符串,而不是某個其它類的字符串(String 類可能被惡意繼承並篡改)。

方法也能夠被 final 修飾,被 final 修飾的方法不能被覆蓋;變量也能夠被 final 修飾,被 final 修飾的變量在建立對象之後就不容許改變它們的值了。一旦將一個類聲明爲 final,那麼該類包含的方法也將被隱式地聲明爲 final,可是變量不是。

被 final 修飾的方法爲靜態綁定,不會產生多態(動態綁定),程序在運行時不須要再檢索方法表,可以提升代碼的執行效率。在Java中,被 static 或 private 修飾的方法會被隱式的聲明爲 final,由於動態綁定沒有意義。

因爲動態綁定會消耗資源而且不少時候沒有必要,因此有一些程序員認爲:除非有足夠的理由使用多態性,不然應該將全部的方法都用 final 修飾。

這樣的認識未免有些偏激,由於 JVM 中的即時編譯器可以實時監控程序的運行信息,能夠準確的知道類之間的繼承關係。若是一個方法沒有被覆蓋而且很短,編譯器就可以對它進行優化處理,這個過程爲稱爲內聯(inlining)。例如,內聯調用 e.getName() 將被替換爲訪問 e.name 變量。這是一項頗有意義的改進,這是因爲CPU在處理調用方法的指令時,使用的分支轉移會擾亂預取指令的策略,因此,這被視爲不受歡迎的。然而,若是 getName() 在另一個類中被覆蓋,那麼編譯器就沒法知道覆蓋的代碼將會作什麼操做,所以也就不能對它進行內聯處理了。

 

4.9  java Object類                                                                                

Object 類位於 java.lang 包中,是全部 Java 類的祖先,Java 中的每一個類都由它擴展而來

定義Java類時若是沒有顯示的指明父類,那麼就默認繼承了 Object 類。例如:

public class Demo{
// ...
}

 

其實是下面代碼的簡寫形式:

public class Demo extends Object{
// ...
}

 

在Java中,只有基本類型不是對象,例如數值、字符和布爾型的值都不是對象,全部的數組類型,無論是對象數組仍是基本類型數組都是繼承自 Object 類

Object 類定義了一些有用的方法(以下),因爲是根類,這些方法在其餘類中都存在,通常是進行了重載或覆蓋,實現了各自的具體功能。

equals() 方法

Object 類中的 equals() 方法用來檢測一個對象是否等價於另一個對象,語法爲:
    public boolean equals(Object obj)
例如:

obj1.equals(obj2);

 

在Java中,數據等價的基本含義是指兩個數據的值相等。在經過 equals() 和「==」進行比較的時候,引用類型數據比較的是引用,即內存地址,基本數據類型比較的是值。

注意:

  • equals()方法只能比較引用類型,「==」能夠比較引用類型及基本類型。
  • 當用 equals() 方法進行比較時,對類 File、String、Date 及包裝類來講,是比較類型及內容而不考慮引用的是不是同一個實例。
  • 用「==」進行比較時,符號兩邊的數據類型必須一致(可自動轉換的數據類型除外),不然編譯出錯,而用 equals 方法比較的兩個數據只要都是引用類型便可。

hashCode() 方法

散列碼(hashCode)是按照必定的算法由對象獲得的一個數值,散列碼沒有規律。若是 x 和 y 是不一樣的對象,x.hashCode() 與 y.hashCode() 基本上不會相同。

hashCode() 方法主要用來在集合中實現快速查找等操做,也能夠用於對象的比較。

在 Java 中,對 hashCode 的規定以下:

  • 在同一個應用程序執行期間,對同一個對象調用 hashCode(),必須返回相同的整數結果——前提是 equals() 所比較的信息都未曾被改動過。至於同一個應用程序在不一樣執行期所得的調用結果,無需一致
  • 若是兩個對象被 equals() 方法視爲相等,那麼對這兩個對象調用 hashCode() 必須得到相同的整數結果
  • 若是兩個對象被 equals() 方法視爲不相等,那麼對這兩個對象調用 hashCode() 沒必要產生不一樣的整數結果。然而程序員應該意識到,對不一樣對象產生不一樣的整數結果,有可能提高hashTable(後面會學到,集合框架中的一個類)的效率。


簡單地說:若是兩個對象相同,那麼它們的 hashCode 值必定要相同;若是兩個對象的 hashCode 值相同,它們並不必定相同。在 Java 規範裏面規定,通常是覆蓋 equals() 方法應該連帶覆蓋 hashCode() 方法。

toString() 方法

toString() 方法是 Object 類中定義的另外一個重要方法,是對象的字符串表現形式,語法爲:
    public String toString()
返回值是 String 類型,用於描述當前對象的有關信息。Object 類中實現的 toString() 方法是返回當前對象的類型和內存地址信息,但在一些子類(如 String、Date 等)中進行了 重寫,也能夠根據須要在用戶自定義類型中重寫 toString() 方法,以返回更適用的信息。

除顯式調用對象的 toString() 方法外,在進行 String 與其它類型數據的鏈接操做時,會自動調用 toString() 方法。

以上幾種方法,在Java中是常常用到的,這裏僅做簡單介紹,讓你們對Object類和其餘類有所瞭解,詳細說明請參考 Java API 文檔。

 

五◐  面向對象高級特性                                                             

 

5.1  Java內部類及其實例化                                

 在 Java 中,容許在一個類(或方法、語句塊)的內部定義另外一個類,稱爲內部類(Inner Class),有時也稱爲嵌套類(Nested Class)

內部類和外層封裝它的類之間存在邏輯上的所屬關係,通常只用在定義它的類或語句塊以內,實現一些沒有通用意義的功能邏輯,在外部引用它時必須給出完整的名稱。

使用內部類的主要緣由有:

  • 內部類能夠訪問外部類中的數據,包括私有的數據。
  • 內部類能夠對同一個包中的其餘類隱藏起來。
  • 當想要定義一個回調函數且不想編寫大量代碼時,使用匿名(anonymous)內部類比較便捷。
  • 減小類的命名衝突。


請看下面的例子:

public class Outer {
private int size;
public class Inner {
private int counter = 10;
public void doStuff() {
size++;
}
}
public static void main(String args[]) {
Outer outer = new Outer();
Inner inner = outer.new Inner();           //請記住此處定義方法
inner.doStuff();
System.out.println(outer.size);
System.out.println(inner.counter);
// 編譯錯誤,外部類不能訪問內部類的變量
System.out.println(counter);  //是這句錯
}
}

 

這段代碼定義了一個外部類 Outer,它包含了一個內部類 Inner。將錯誤語句註釋掉,編譯,會生成兩個 .class 文件:Outer.class 和 Outer$Inner.class。也就是說,內部類會被編譯成獨立的字節碼文件。

內部類是一種編譯器現象,與虛擬機無關。編譯器將會把內部類翻譯成用 $ 符號分隔外部類名與內部類名的常規類文件,而虛擬機則對此一無所知。

注意:必須先有外部類的對象才能生成內部類的對象,由於內部類須要訪問外部類中的成員變量,成員變量必須實例化纔有意義。

內部類是 Java 1.1 的新增特性,有些程序員認爲這是一個值得稱讚的進步,可是內部類的語法很複雜,嚴重破壞了良好的代碼結構, 違背了Java要比C++更加簡單的設計理念。

內部類看似增長了—些優美有趣,實屬不必的特性,這是否是也讓Java開始走上了許多語言飽受折磨的毀滅性道路呢?本教程並不打算就這個問題給予一個確定的答案。

 

5.2   java 靜態內部類、匿名內部類、成員式內部類和局部內部類                                            

 內部類能夠是靜態(static)的,能夠使用 public、protected 和 private 訪問控制符,而外部類只能使用 public,或者默認。

成員式內部類

在外部類內部直接定義(不在方法內部或代碼塊內部)的類就是成員式內部類,它能夠直接使用外部類的全部變量和方法,即便是 private 的。外部類要想訪問內部類的成員變量和方法,則須要經過內部類的對象來獲取。

請看下面的代碼:

public class Outer{
private int size;
public class Inner {
public void dostuff() {
size++;
}
}
public void testTheInner() {
Inner in = new Inner();
in.dostuff();
}
}

 

成員式內部類如同外部類的一個普通成員。

成員式內部類能夠使用各類修飾符,包括 public、protected、private、static、final 和 abstract,也能夠不寫。

如有 static 修飾符,就爲類級,不然爲對象級。類級能夠經過外部類直接訪問,對象級須要先生成外部的對象後才能訪問。

非靜態內部類中不能聲明任何 static 成員。

內部類能夠相互調用,例如:

class A {
// B、C 間能夠互相調用
class B {}
class C {}
}

 

成員式內部類的訪問

內部類的對象以成員變量的方式記錄其所依賴的外層類對象的引用,於是能夠找到該外層類對象並訪問其成員。該成員變量是系統自動爲非 static 的內部類添加的,名稱約定爲「outClassName.this」。

1) 使用內部類中定義的非靜態變量和方法時,要先建立外部類的對象,再由「outObjectName.new」操做符建立內部類的對象,再調用內部類的方法,以下所示:

public class Demo{
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();    //這句
inner.dostuff();
}
}
class Outer{
private int size;
class Inner{
public void dostuff() {
size++;
}
}
}

2) static 內部類至關於其外部類的 static 成員,它的對象與外部類對象間不存在依賴關係,所以可直接建立。示例以下:

public class Demo{
public static void main(String[] args) {
Outer.Inner inner = new Outer.Inner();      //
inner.dostuff();
}
}
class Outer{
private static int size;
static class Inner {
public void dostuff() {
size++;
System.out.println("size=" + size);
}
}
}

 

運行結果:
size=1

3) 因爲內部類能夠直接訪問其外部類的成分,所以當內部類與其外部類中存在同名屬性或方法時,也將致使命名衝突。因此在多層調用時要指明,以下所示:

public class Outer{
private int size;
public class Inner{
private int size;
public void dostuff(int size){
size++; // 局部變量 size;
this.size; // 內部類的 size
Outer.this.size++; // 外部類的 size
}
}
}//這裏你能夠看到this的強大之處

 

局部內部類

局部內部類(Local class)是定義在代碼塊中的類。它們只在定義它們的代碼塊中是可見的。

局部類有幾個重要特性:

  1. 僅在定義了它們的代碼塊中是可見的;
  2. 能夠使用定義它們的代碼塊中的任何局部 final 變量;
  3. 局部類不能夠是 static 的,裏邊也不能定義 static 成員;
  4. 局部類不能夠用 public、private、protected 修飾,只能使用缺省的;
  5. 局部類能夠是 abstract 的。


請看下面的代碼:

public class Outer {
public static final int TOTAL_NUMBER = 5;     //此處能夠記好關鍵詞使用的前後順序 public int id = 123;
public void func() {
final int age = 15;
String str = "http://www.weixueyuan.net";
class Inner {
public void innerTest() {
System.out.println(TOTAL_NUMBER);
System.out.println(id);
// System.out.println(str);不合法,只能訪問本地方法的final變量     ???
System.out.println(age);
}
}
new Inner().innerTest();
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.func();
}
}

 

運行結果:
5
123
15

匿名內部類

 

1 abstract類中必須有abstract方法      ×
2 abstract方法所在的類必須用abstract修飾  
abstract類 及 抽象類
抽象類中能夠沒有抽象的方法,只是抽象類不能實例化
可是一旦一個類中有抽象方法,所在class一定要是abstract,不然會有編譯錯誤

匿名內部類是局部內部類的一種特殊形式,也就是沒有變量名指向這個類的實例,並且具體的類實現會寫在這個內部類裏面。

注意:匿名類必須繼承一個父類或實現一個接口。

不使用匿名內部類來實現抽象方法:

abstract class Person {
public abstract void eat();
}
class Child extends Person {
public void eat() {
System.out.println("eat something");
}
}
public class Demo {
public static void main(String[] args) {
Person p = new Child();
p.eat();
}
}

 

運行結果:
eat something

能夠看到,咱們用Child繼承了Person類,而後實現了Child的一個實例,將其向上轉型爲Person類的引用。可是,若是此處的Child類只使用一次,那麼將其編寫爲獨立的一個類豈不是很麻煩?

這個時候就引入了匿名內部類。使用匿名內部類實現:

 

能夠看到,匿名類繼承了 Person 類並在大括號中實現了抽象類的方法。

內部類的語法比較複雜,實際開發中也較少用到,本教程不打算進行深刻講解,各位讀者也不該該將內部類做爲學習Java的重點。

 

 5.3  Java抽象類的概念和使用                                                        

 在自上而下的繼承層次結構中,位於上層的類更具備通用性,甚至可能更加抽象。從某種角度看,祖先類更加通用,它只包含一些最基本的成員,人們只將它做爲派生其餘類的基類,而不會用來建立對象。甚至,你能夠只給出方法的定義而不實現,由子類根據具體需求來具體實現。

這種只給出方法定義而不具體實現的方法被稱爲抽象方法,抽象方法是沒有方法體的,在代碼的表達上就是沒有「{}」。包含一個或多個抽象方法的類也必須被聲明爲抽象類。

使用 abstract 修飾符來表示抽象方法和抽象類。 (抽象類至關於C++中的基類但C++基類能夠直接使用

抽象類除了包含抽象方法外,還能夠包含具體的變量和具體的方法。類即便不包含抽象方法,也能夠被聲明爲抽象類,防止被實例化。

抽象類不能被實例化,抽象方法必須在子類中被實現。請看下面的代碼:

import static java.lang.System.*;
public final class Demo{
public static void main(String[] args) {
Teacher t = new Teacher();
t.setName("王明");
t.work();
Driver d = new Driver();
d.setName("小陳");
d.work();
}
}
// 定義一個抽象類
abstract class People{
private String name; // 實例變量
// 共有的 setter 和 getter 方法
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
// 抽象方法
public abstract void work();    //空的;
}
class Teacher extends People{
// 必須實現該方法
public void work(){
out.println("個人名字叫" + this.getName() + ",我正在講課,請你們不要東張西望...");
}
}
class Driver extends People{
// 必須實現該方法
public void work(){
out.println("個人名字叫" + this.getName() + ",我正在開車,不能接聽電話...");
}
}

 

運行結果:
個人名字叫王明,我正在講課,請你們不要東張西望...
個人名字叫小陳,我正在開車,不能接聽電話...

關於抽象類的幾點說明:

  • 抽象類不能直接使用,必須用子類去實現抽象類,而後使用其子類的實例。然而能夠建立一個變量,其類型是一個抽象類,並讓它指向具體子類的一個實例,也就是能夠使用抽象類來充當形參,實際實現類做爲實參,也就是多態的應用。
  • 不能有抽象構造方法或抽象靜態方法。 



在下列狀況下,一個類將成爲抽象類

    1. 當一個類的一個或多個方法是抽象方法時;
    2. 當類是一個抽象類的子類,而且不能爲任何抽象方法提供任何實現細節或方法主體時
    3. 當一個類實現一個接口,而且不能爲任何抽象方法提供實現細節或方法主體時;注意:
      • 這裏說的是這些狀況下一個類將成爲抽象類,沒有說抽象類必定會有這些狀況。
      • 一個典型的錯誤:抽象類必定包含抽象方法。 可是反過來講「包含抽象方法的類必定是抽象類」就是正確的。
      • 事實上,抽象類能夠是一個徹底正常實現的類

 

5.4  Java接口(interface)的概念及使用                                      

在抽象類中,能夠包含一個或多個抽象方法;但在接口(interface)中,全部的方法必須都是抽象的,不能有方法體,它比抽象類更加「抽象」

接口使用 interface 關鍵字來聲明,能夠看作是一種特殊的抽象類,能夠指定一個類必須作什麼,而不是規定它如何去作。

現實中也有不少接口的實例,好比說串口電腦硬盤,Serial ATA委員會指定了Serial ATA 2.0規範,這種規範就是接口。Serial ATA委員會不負責生產硬盤,只是指定通用的規範。

希捷、日立、三星等生產廠家會按照規範生產符合接口的硬盤,這些硬盤就能夠實現通用化,若是正在用一塊160G日立的串口硬盤,如今要升級了,能夠購買一塊320G的希捷串口硬盤,安裝上去就能夠繼續使用了。

下面的代碼能夠模擬Serial ATA委員會定義如下串口硬盤接口:

//串行硬盤接口
public interface SataHdd{
//鏈接線的數量
public static final int CONNECT_LINE=4;
//寫數據
public void writeData(String data);
//讀數據
public String readData();
}

 

注意:接口中聲明的成員變量默認都是 public static final 的,必須顯示的初始化。於是在常量聲明時能夠省略這些修飾符。

接口是若干常量和抽象方法的集合,目前看來和抽象類差很少。確實如此,接口本就是從抽象類中演化而來的,於是除特別規定,接口享有和類一樣的「待遇」。好比,源程序中能夠定義多個類或接口,但最多隻能有一個public 的類或接口,若是有則源文件必須取和public的類和接口相同的名字。和類的繼承格式同樣,接口之間也能夠繼承,子接口能夠繼承父接口中的常量和抽象方法並添加新的抽象方法等。

但接口有其自身的一些特性,概括以下。

1) 接口中只能定義抽象方法這些方法默認爲 public abstract 的,於是在聲明方法時能夠省略這些修飾符。試圖在接口中定義實例變量、非抽象的實例方法及靜態方法,都是非法的。例如:

public interface SataHdd{
//鏈接線的數量
public int connectLine; //編譯出錯,connectLine被看作靜態常量,必須顯式初始化
//寫數據
protected void writeData(String data); //編譯出錯,必須是public類型
//讀數據
public static String readData(){ //編譯出錯,接口中不能包含靜態方法
return "數據"; //編譯出錯,接口中只能包含抽象方法,
}
}

 


3) 接口中沒有構造方法,不能被實例化。

4) 一個接口不實現另外一個接口,但能夠繼承多個其餘接口。接口的多繼承特色彌補了類的單繼承。例如:

//串行硬盤接口
public interface SataHdd extends A,B{
// 鏈接線的數量
public static final int CONNECT_LINE = 4;
// 寫數據
public void writeData(String data);
// 讀數據
public String readData();
}
interface A{
public void a();
}
interface B{
public void b();
}

 

爲何使用接口

大型項目開發中,可能須要從繼承鏈的中間插入一個類,讓它的子類具有某些功能而不影響它們的父類。例如 A -> B -> C -> D -> E,A 是祖先類,若是須要爲C、D、E類添加某些通用的功能,最簡單的方法是讓C類再繼承另一個類。可是問題來了,Java 是一種單繼承的語言,不能再讓C繼承另一個父類了,只到移動到繼承鏈的最頂端,讓A再繼承一個父類。這樣一來,對C、D、E類的修改,影響到了整個繼承鏈,不具有可插入性的設計。

接口是可插入性的保證。在一個繼承鏈中的任何一個類均可以實現一個接口,這個接口會影響到此類的全部子類,但不會影響到此類的任何父類。此類將不得不實現這個接口所規定的方法,而子類能夠今後類自動繼承這些方法,這時候,這些子類具備了可插入性。

咱們關心的不是哪個具體的類,而是這個類是否實現了咱們須要的接口。

接口提供了關聯以及方法調用上的可插入性,軟件系統的規模越大,生命週期越長,接口使得軟件系統的靈活性和可擴展性,可插入性方面獲得保證。

接口在面向對象的 Java 程序設計中佔有舉足輕重的地位。事實上在設計階段最重要的任務之一就是設計出各部分的接口,而後經過接口的組合,造成程序的基本框架結構。

接口的使用

接口的使用與類的使用有些不一樣。在須要使用的地方,會直接使用new關鍵字來構建一個類的實例,但接口不能夠這樣使用,由於接口不能直接使用 new 關鍵字來構建實例。

接口必須經過類來實現(implements)它的抽象方法,而後再實例化類。類實現接口的關鍵字爲implements

若是一個類不能實現該接口的全部抽象方法,那麼這個類必須被定義爲抽象方法

不容許建立接口的實例,但容許定義接口類型的引用變量,該變量指向了實現接口的類的實例。

一個類只能繼承一個父類,但卻能夠實現多個接口。

實現接口的格式以下:
修飾符 class 類名 extends 父類 implements 多個接口 {
    實現方法
}

請看下面的例子:

import static java.lang.System.*;
public class Demo{
public static void main(String[] args) {
SataHdd sh1=new SeagateHdd(); //初始化希捷硬盤    能夠直接初始化,也就是能夠直接用了,這是和   ???這難道不是實例化了嗎
SataHdd sh2=new SamsungHdd(); //初始化三星硬盤
}
}
//串行硬盤接口
interface SataHdd{
//鏈接線的數量
public static final int CONNECT_LINE=4;
//寫數據
public void writeData(String data);
//讀數據
public String readData();
}
// 維修硬盤接口
interface fixHdd{
// 維修地址
String address = "北京市海淀區";
// 開始維修
boolean doFix();
}
//希捷硬盤
class SeagateHdd implements SataHdd, fixHdd{      //這裏 //希捷硬盤讀取數據
public String readData(){
return "數據";
}
//希捷硬盤寫入數據
public void writeData(String data) {
out.println("寫入成功");
}
// 維修希捷硬盤
public boolean doFix(){
return true;
}
}
//三星硬盤
class SamsungHdd implements SataHdd{
//三星硬盤讀取數據
public String readData(){
return "數據";
}
//三星硬盤寫入數據
public void writeData(String data){
out.println("寫入成功");
}
}
//某劣質硬盤,不能寫數據
abstract class XXHdd implements SataHdd{
//硬盤讀取數據
public String readData() {
return "數據";
}
}

 

接口做爲類型使用

接口做爲引用類型來使用,任何實現該接口的類的實例均可以存儲在該接口類型的變量中,經過這些變量能夠訪問類中所實現的接口中的方法,Java 運行時系統會動態地肯定應該使用哪一個類中的方法,其實是調用相應的實現類的方法。

示例以下:

public class Demo{
public void test1(A a) {
a.doSth();
}
public static void main(String[] args) {
Demo d = new Demo();
A a = new B();      //這一步影響了;
d.test1(a);
}
}
interface A {
public int doSth();
}
class B implements A {
public int doSth() {
System.out.println("now in B");
return 123;
}
}

 

運行結果:
now in B

你們看到接口能夠做爲一個類型來使用,把接口做爲方法的參數和返回類型。 

 

5.5  Java接口和抽象類的區別                                                                                             

 類是對象的模板,抽象類和接口能夠看作是具體的類的模板。

因爲從某種角度講,接口是一種特殊的抽象類,它們的淵源頗深,有很大的類似之處,因此在選擇使用誰的問題上很容易迷糊。咱們首先分析它們具備的相同點

  • 都表明類樹形結構的抽象層。在使用引用變量時,儘可能使用類結構的抽象層,使方法的定義和實現分離,這樣作對於代碼有鬆散耦合的好處。
  • 都不能被實例化
  • 都能包含抽象方法。抽象方法用來描述系統提供哪些功能,而沒必要關心具體的實現。


下面說一下抽象類和接口的主要區別。

1) 抽象類能夠爲部分方法提供實現,避免了在子類中重複實現這些方法,提升了代碼的可重用性,這是抽象類的優點;而接口中只能包含抽象方法,不能包含任何實現。

public abstract class A{
public abstract void method1();
public void method2(){
//A method2
}
}
public class B extends A{
public void method1(){
//B method1
}
}
public class C extends A{
public void method1(){
//C method1
}
}

 

抽象類A有兩個子類B、C,因爲A中有方法method2的實現子類B、C中不須要重寫method2方法,咱們就說A爲子類提供了公共的功能,或A約束了子類的行爲。method2就是代碼可重用的例子。A 並無定義 method1的實現,也就是說B、C 能夠根據本身的特色實現method1方法,這又體現了鬆散耦合的特性。

再換成接口看看:

public interface A{
public void method1();    //也就是說method1(),method2()不能有本身的函數體,函數體須要在調用它的子類中書寫 public void method2();
}
public class B implements A{
public void method1(){
//B method1
}
public void method2(){
//B method2
}
}
public class C implements A{
public void method1(){
//C method1
}
public void method2(){
//C method2
}
}

 

接口A沒法爲實現類B、C提供公共的功能,也就是說A沒法約束B、C的行爲。B、C能夠自由地發揮本身的特色現實 method1和 method2方法,接口A毫無掌控能力。

2) 一個類只能繼承一個直接的父類(多是抽象類),但一個類能夠實現多個接口,這個就是接口的優點。

interface A{
public void method2();
}
interface B{
public void method1();
}
class C implements A,B{
public void method1(){
//C method1
}
public void method2(){
//C method2
}
}
//能夠如此靈活的使用C,而且C還有機會進行擴展,實現其餘接口
A a=new C();
B b=new C();
abstract class A{
public abstract void method1();
}
abstract class B extends A{
public abstract void method2();
}
class C extends B{
public void method1(){
//C method1
}
public void method2() {
//C method2
}
}

 

對於C類,將沒有機會繼承其餘父類了。

綜上所述,接口和抽象類各有優缺點,在接口和抽象類的選擇上,必須遵照這樣一個原則:

    • 行爲模型應該老是經過接口而不是抽象類定義,因此一般是優先選用接口,儘可能少用抽象類。
    • 選擇抽象類的時候一般是以下狀況:須要定義子類的行爲,又要爲子類提供通用的功能

 

 5.6  java泛型                                                                                                                

咱們知道,使用變量以前要定義,定義一個變量時必需要指明它的數據類型,什麼樣的數據類型賦給什麼樣的值。

假如咱們如今要定義一個類來表示座標,要求座標的數據類型能夠是整數、小數和字符串,例如:

  • x = 十、y = 10
  • x = 12.8八、y = 129.65
  • x = "東京180度"、y = "北緯210度"


針對不一樣的數據類型,除了藉助方法重載,還能夠藉助自動裝箱和向上轉型。咱們知道,基本數據類型能夠自動裝箱,被轉換成對應的包裝類;Object 是全部類的祖先類,任何一個類的實例均可以向上轉型爲 Object 類型,例如:

  • int --> Integer --> Object
  • double -->Double --> Object
  • String --> Object


這樣,只須要定義一個方法,就能夠接收全部類型的數據。請看下面的代碼:

public class Demo {
    public static void main(String[] args){
        Point p = new Point();
        p.setX(10);  // int -> Integer -> Object
        p.setY(20);
        int x = (Integer)p.getX();  // 必須向下轉型
        int y = (Integer)p.getY();
        System.out.println("This point is:" + x + ", " + y);
       
        p.setX(25.4);  // double -> Integer -> Object
        p.setY("東京180度");
        double m = (Double)p.getX();  // 必須向下轉型
        double n = (Double)p.getY(); // 運行期間拋出異常
        System.out.println("This point is:" + m + ", " + n);
    }
}
class Point{
    Object x = 0;
    Object y = 0;
    public Object getX() {
        return x;
    }
    public void setX(Object x) {
        this.x = x;
    }
    public Object getY() {
        return y;
    }
    public void setY(Object y) {
        this.y = y;
    }
}

 

上面的代碼中,生成座標時不會有任何問題,可是取出座標時,要向下轉型,在 Java多態對象的類型轉換 一文中咱們講到,向下轉型存在着風險,並且編譯期間不容易發現,只有在運行期間纔會拋出異常,因此要儘可能避免使用向下轉型。運行上面的代碼,第12行會拋出 java.lang.ClassCastException 異常。

那麼,有沒有更好的辦法,既能夠不使用重載(有重複代碼),又能把風險降到最低呢?

有,能夠使用泛型類(Java Class),它能夠接受任意類型的數據。所謂「泛型」,就是「寬泛的數據類型」,任意的數據類型。

更改上面的代碼,使用泛型類:

public class Demo {
    public static void main(String[] args){
        // 實例化泛型類
        Point<Integer, Integer> p1 = new Point<Integer, Integer>();  //指出類型
        p1.setX(10);
        p1.setY(20);
        int x = p1.getX();
        int y = p1.getY();
        System.out.println("This point is:" + x + ", " + y);
       
        Point<Double, String> p2 = new Point<Double, String>();
        p2.setX(25.4);
        p2.setY("東京180度");
        double m = p2.getX();
        String n = p2.getY();
        System.out.println("This point is:" + m + ", " + n);
    }
}
// 定義泛型類
class Point<T1, T2>{
    T1 x;
    T2 y;
    public T1 getX() {
        return x;
    }
    public void setX(T1 x) {
        this.x = x;
    }
    public T2 getY() {
        return y;
    }
    public void setY(T2 y) {
        this.y = y;
    }
}

 

運行結果:
This point is:10, 20
This point is:25.4, 東京180度

與普通類的定義相比,上面的代碼在類名後面多出了 <T1, T2>,T1, T2 是自定義的標識符,也是參數,用來傳遞數據的類型,而不是數據的值,咱們稱之爲類型參數。在泛型中,不但數據的值能夠經過參數傳遞,數據的類型也能夠經過參數傳遞。T1, T2 只是數據類型的佔位符,運行時會被替換爲真正的數據類型。

傳值參數(咱們一般所說的參數)由小括號包圍,如 (int x, double y),類型參數(泛型參數)由尖括號包圍,多個參數由逗號分隔,如 <T> 或 <T, E>。

類型參數須要在類名後面給出。一旦給出了類型參數,就能夠在類中使用了。類型參數必須是一個合法的標識符,習慣上使用單個大寫字母,一般狀況下,K 表示鍵,V 表示值,E 表示異常或錯誤,T 表示通常意義上的數據類型(這些都是泛類型能用的)

泛型類在實例化時必須指出具體的類型,也就是向類型參數傳值,格式爲:
    className variable<dataType1, dataType2> = new className<dataType1, dataType2>();
也能夠省略等號右邊的數據類型,可是會產生警告,即:
    className variable<dataType1, dataType2> = new className();

由於在使用泛型類時指明瞭數據類型,賦給其餘類型的值會拋出異常,既不須要向下轉型,也沒有潛在的風險,比本文一開始介紹的自動裝箱和向上轉型要更加實用。

注意:

  • 泛型是 Java 1.5 的新增特性,它以C++模板爲參照,本質是參數化類型(Parameterized Type)的應用。
  • 類型參數只能用來表示引用類型,不能用來表示基本類型,如  int、double、char 等。可是傳遞基本類型不會報錯,由於它們會自動裝箱成對應的包裝類。

泛型方法

除了定義泛型類,還能夠定義泛型方法,例如,定義一個打印座標的泛型方法:

public class Demo {
public static void main(String[] args){
// 實例化泛型類
Point<Integer, Integer> p1 = new Point<Integer, Integer>();
p1.setX(10);
p1.setY(20);
p1.printPoint(p1.getX(), p1.getY());
Point<Double, String> p2 = new Point<Double, String>();
p2.setX(25.4);
p2.setY("東京180度");
p2.printPoint(p2.getX(), p2.getY());
}
}
// 定義泛型類
class Point<T1, T2>{
T1 x;
T2 y;
public T1 getX() {
return x;
}
public void setX(T1 x) {
this.x = x;
}
public T2 getY() {
return y;
}
public void setY(T2 y) {
this.y = y;
}
// 定義泛型方法
public <T1, T2> void printPoint(T1 x, T2 y){
T1 m = x;
T2 n = y;
System.out.println("This point is:" + m + ", " + n);
}
}

 

運行結果:
This point is:10, 20
This point is:25.4, 東京180度

上面的代碼中定義了一個泛型方法 printPoint(),既有普通參數,也有類型參數,類型參數須要放在修飾符後面、返回值類型前面。一旦定義了類型參數,就能夠在參數列表、方法體和返回值類型中使用了。

與使用泛型類不一樣,使用泛型方法時沒必要指明參數類型,編譯器會根據傳遞的參數自動查找出具體的類型。泛型方法除了定義不一樣,調用就像普通方法同樣。 

注意:泛型方法與泛型類沒有必然的聯繫,泛型方法有本身的類型參數,在普通類中也能夠定義泛型方法。泛型方法 printPoint() 中的類型參數 T1, T2 與泛型類 Point 中的 T1, T2 沒有必然的聯繫,也能夠使用其餘的標識符代替:

public static <V1, V2> void printPoint(V1 x, V2 y){
V1 m = x;
V2 n = y;
System.out.println("This point is:" + m + ", " + n);
}

 

泛型接口

在Java中也能夠定義泛型接口,這裏再也不贅述,僅僅給出示例代碼:

public class Demo {
public static void main(String arsg[]) {
Info<String> obj = new InfoImp<String>("www.weixueyuan.net");
System.out.println("Length Of String: " + obj.getVar().length());
}
}
//定義泛型接口
interface Info<T> {
public T getVar();
}
//實現接口
class InfoImp<T> implements Info<T> {
private T var;
// 定義泛型構造方法
public InfoImp(T var) {
this.setVar(var);
}
public void setVar(T var) {
this.var = var;
}
public T getVar() {
return this.var;
}
}

 

運行結果:
Length Of String: 18

類型擦除

若是在使用泛型時沒有指明數據類型那麼就會擦除泛型類型,請看下面的代碼:

public class Demo {
public static void main(String[] args){
Point p = new Point(); // 類型擦除  並無指明類型
p.setX(10);
p.setY(20.8);
int x = (Integer)p.getX(); // 向下轉型
double y = (Double)p.getY();
System.out.println("This point is:" + x + ", " + y);
}
}
class Point<T1, T2>{
T1 x;
T2 y;
public T1 getX() {
return x;
}
public void setX(T1 x) {
this.x = x;
}
public T2 getY() {
return y;
}
public void setY(T2 y) {
this.y = y;
}
}

 

運行結果:
This point is:10, 20.8

由於在使用泛型時沒有指明數據類型,爲了避免出現錯誤,編譯器會將全部數據向上轉型爲 Object,因此在取出座標使用時要向下轉型,這與本文一開始不使用泛型沒什麼兩樣。

限制泛型的可用類型

在上面的代碼中,類型參數能夠接受任意的數據類型,只要它是被定義過的。可是,不少時候咱們只須要一部分數據類型就夠了,用戶傳遞其餘數據類型可能會引發錯誤。例如,編寫一個泛型函數用於返回不一樣類型數組(Integer 數組、Double 數組、Character 數組等)中的最大值:

public <T> T getMax(T array[]){
T max = null;
for(T element : array){
max = element.doubleValue() > max.doubleValue() ? element : max;
}
return max;
}

 

上面的代碼會報錯,doubleValue() 是 Number 類的方法,不是全部的類都有該方法,因此咱們要限制類型參數 T,讓它只能接受 Number 及其子類(Integer、Double、Character 等)

經過 extends 關鍵字能夠限制泛型的類型,改進上面的代碼:

public <T extends Number> T getMax(T array[]){
T max = null;
for(T element : array){
max = element.doubleValue() > max.doubleValue() ? element : max;
}
return max;
}

 

<T extends Number> 表示 T 只接受 Number 及其子類,傳入其餘類型的數據會報錯。這裏的限定使用關鍵字 extends,後面能夠是類也能夠是接口。但這裏的 extends 已經不是繼承的含義了,應該理解爲 T 是繼承自 Number 類的類型,或者 T 是實現了 XX 接口的類型。

注意:通常的應用開發中泛型使用較少,多用在框架或者庫的設計中,這裏再也不深刻講解,主要讓你們對泛型有所認識,爲後面的教程作鋪墊。

 

5.7  java泛型通配符合類型參數的範圍                                                                

 

通配符(?)

上一節的例子中提到要定義一個泛型類來表示座標,座標能夠是整數、小數或字符串,請看下面的代碼:

class Point<T1, T2>{
T1 x;
T2 y;
public T1 getX() {
return x;
}
public void setX(T1 x) {
this.x = x;
}
public T2 getY() {
return y;
}
public void setY(T2 y) {
this.y = y;
}
}

 

如今要求在類的外部定義一個 printPoint() 方法用於輸出座標,怎麼辦呢?

能夠這樣來定義方法:

public void printPoint(Point p){
System.out.println("This point is: " + p.getX() + ", " + p.getY());
}

 

咱們知道,若是在使用泛型時沒有指名具體的數據類型,就會擦除泛型類型,並向上轉型爲 Object,這與不使用泛型沒什麼兩樣。上面的代碼沒有指明數據類型,至關於:

public void printPoint(Point<Object, Object> p){
System.out.println("This point is: " + p.getX() + ", " + p.getY());
}

 

爲了不類型擦除,能夠使用通配符(?)

public void printPoint(Point<?, ?> p){
System.out.println("This point is: " + p.getX() + ", " + p.getY());
}

 

通配符(?)能夠表示任意的數據類型。將代碼補充完整:

public class Demo {
public static void main(String[] args){
Point<Integer, Integer> p1 = new Point<Integer, Integer>();
p1.setX(10);
p1.setY(20);
printPoint(p1);
Point<String, String> p2 = new Point<String, String>();
p2.setX("東京180度");
p2.setY("北緯210度");
printPoint(p2);
}
public static void printPoint(Point<?, ?> p){ // 使用通配符            //請注意使用的位置,在main以後 , Demo以內;  
System.out.println("This point is: " + p.getX() + ", " + p.getY());
}
}
class Point<T1, T2>{
T1 x;
T2 y;
public T1 getX() {
return x;
}
public void setX(T1 x) {
this.x = x;
}
public T2 getY() {
return y;
}
public void setY(T2 y) {
this.y = y;
}
}

 

運行結果:
This point is: 10, 20
This point is: 東京180度, 北緯210度

可是,數字座標與字符串座標又有區別:數字能夠表示x軸或y軸的座標,字符串能夠表示地球經緯度。如今又要求定義兩個方法分別處理不一樣的座標,一個方法只能接受數字類型的座標,另外一個方法只能接受字符串類型的座標,怎麼辦呢?

這個問題的關鍵是要限制類型參數的範圍,請先看下面的代碼:

public class Demo {
public static void main(String[] args){
Point<Integer, Integer> p1 = new Point<Integer, Integer>();
p1.setX(10);
p1.setY(20);
printNumPoint(p1);
Point<String, String> p2 = new Point<String, String>();
p2.setX("東京180度");
p2.setY("北緯210度");
printStrPoint(p2);
}
// 藉助通配符限制泛型的範圍
public static void printNumPoint(Point<? extends Number, ? extends Number> p){
System.out.println("x: " + p.getX() + ", y: " + p.getY());
}
public static void printStrPoint(Point<? extends String, ? extends String> p){
System.out.println("GPS: " + p.getX() + "," + p.getY());
}
}
class Point<T1, T2>{
T1 x;
T2 y;
public T1 getX() {
return x;
}
public void setX(T1 x) {
this.x = x;
}
public T2 getY() {
return y;
}
public void setY(T2 y) {
this.y = y;
}
}

 

運行結果:
x: 10, y: 20
GPS: 東京180度,北緯210度

? extends Number 表示泛型的類型參數只能是 Number 及其子類,? extends String 也同樣,這與定義泛型類或泛型方法時限制類型參數的範圍相似。

不過,使用通配符(?)不但能夠限制類型的上限,還能夠限制下限。限制下限使用 super 關鍵字,例如 <? super Number> 表示只能接受 Number 及其父類。

注意:通常的項目中不多會去設計泛型,這裏主要是讓讀者學會如何使用,爲後面的教程作鋪墊。

 

 六◐  java異常處理                            

 6.1  異常處理基礎                                      

Java異常是一個描述在代碼段中發生的異常(也就是出錯)狀況的對象。當異常狀況發生,一個表明該異常的對象被建立而且在致使該錯誤的方法中被拋出(throw)。該方法能夠選擇本身處理異常或傳遞該異常。兩種狀況下,該異常被捕獲(caught)並處理。異常多是由Java運行時系統產生,或者是由你的手工代碼產生。被Java拋出的異常與違反語言規範或超出Java執行環境限制的基本錯誤有關。手工編碼產生的異常基本上用於報告方法調用程序的出錯情況。

Java異常處理經過5個關鍵字控制:try、catch、throw、throws和 finally。下面講述它們如何工做的。程序聲明瞭你想要的異常監控包含在一個try塊中。若是在try塊中發生異常,它被拋出。你的代碼能夠捕捉這個異常(用catch)而且用某種合理的方法處理該異常。系統產生的異常被Java運行時系統自動拋出。手動拋出一個異常,用關鍵字throw。任何被拋出方法的異常都必須經過throws子句定義。任何在方法返回前絕對被執行的代碼被放置在finally塊中。

下面是一個異常處理塊的一般形式:

try {
    // block of code to monitor for errors
}
catch (ExceptionType1 exOb) {
    // exception handler for ExceptionType1
}
catch (ExceptionType2 exOb) {
    // exception handler for ExceptionType2
}
// ...
finally {
    // block of code to be executed before try block ends
}

 


這裏,ExceptionType 是發生異常的類型。下面將介紹怎樣應用這個框架。                

 

 6.2  異常類型                                  

全部異常類型都是內置類Throwable的子類。所以,Throwable在異常類層次結構的頂層。緊接着Throwable下面的是兩個把異常分紅兩個不一樣分支的子類。一個分支是Exception。

該類用於用戶程序可能捕捉的異常狀況。它也是你能夠用來建立你本身用戶異常類型子類的類。在Exception分支中有一個重要子類RuntimeException。該類型的異常自動爲你所編寫的程序定義而且包括被零除和非法數組索引這樣的錯誤。

另外一類分支由Error做爲頂層,Error定義了在一般環境下不但願被程序捕獲的異常。Error類型的異經常使用於Java運行時系統來顯示與運行時系統自己有關的錯誤。堆棧溢出是這種錯誤的一例。本章將不討論關於Error類型的異常處理,由於它們一般是災難性的致命錯誤,不是你的程序能夠控制的。

6.3  Java未被捕獲的異常                            

在你學習在程序中處理異常以前,看一看若是你不處理它們會有什麼狀況發生是頗有好處的。下面的小程序包括一個故意致使被零除錯誤的表達式。

class Exc0 {
    public static void main(String args[]) {
        int d = 0;
        int a = 42 / d;
    }
}

 


當Java運行時系統檢查到被零除的狀況,它構造一個新的異常對象而後拋出該異常。這致使Exc0的執行中止,由於一旦一個異常被拋出,它必須被一個異常處理程序捕獲而且被當即處理。該例中,咱們沒有提供任何咱們本身的異常處理程序,因此異常被Java運行時系統的默認處理程序捕獲。任何不是被你程序捕獲的異常最終都會被該默認處理程序處理。默認處理程序顯示一個描述異常的字符串,打印異常發生處的堆棧軌跡而且終止程序。

下面是由標準javaJDK運行時解釋器執行該程序所產生的輸出:

    java.lang.ArithmeticException: / by zero
    at Exc0.main(Exc0.java:4)

注意,類名Exc0,方法名main,文件名Exc0.java和行數4是怎樣被包括在一個簡單的堆棧使用軌跡中的。還有,注意拋出的異常類型是Exception的一個名爲ArithmeticException的子類,該子類更明確的描述了何種類型的錯誤方法。本章後面部分將討論,Java提供多個內置的與可能產生的不一樣種類運行時錯誤相匹配的異常類型。

堆棧軌跡將顯示致使錯誤產生的方法調用序列。例如,下面是前面程序的另外一個版本,它介紹了相同的錯誤,可是錯誤是在main( )方法以外的另外一個方法中產生的:

class Exc1 {
    static void subroutine() {
        int d = 0;
        int a = 10 / d;
    }
    public static void main(String args[]) {
        Exc1.subroutine();
    }
}

 


默認異常處理器的堆棧軌跡結果代表了整個調用棧是怎樣顯示的:

    java.lang.ArithmeticException: / by zero
    at Exc1.subroutine(Exc1.java:4)
    at Exc1.main(Exc1.java:7)

如你所見,棧底是main的第7行,該行調用了subroutine( )方法。該方法在第4行致使了異常。調用堆棧對於調試來講是很重要的,由於它查明瞭致使錯誤的精確的步驟。

 

6.4  java try和catch的使用                        

儘管由Java運行時系統提供的默認異常處理程序對於調試是頗有用的,但一般你但願本身處理異常。這樣作有兩個好處。第一,它容許你修正錯誤。第二,它防止程序自動終止。大多數用戶對於在程序終止運行和在不管什麼時候錯誤發生都會打印堆棧軌跡感到很煩惱(至少能夠這麼說)。幸運的是,這很容易避免。

爲防止和處理一個運行時錯誤,只須要把你所要監控的代碼放進一個try塊就能夠了。緊跟着try塊的,包括一個說明你但願捕獲的錯誤類型的catch子句。完成這個任務很簡單,下面的程序包含一個處理由於被零除而產生的ArithmeticException 異常的try塊和一個catch子句。

class Exc2 {
    public static void main(String args[]) {
        int d, a;
        try { // monitor a block of code.
            d = 0;
            a = 42 / d;
            System.out.println("This will not be printed.");
        } catch (ArithmeticException e) { // catch divide-by-zero error
            System.out.println("Division by zero.");
        }
        System.out.println("After catch statement.");
    }
}


該程序輸出以下:
Division by zero.
After catch statement.

注意在try塊中的對println( )的調用是永遠不會執行的。一旦異常被引起,程序控制由try塊轉到catch塊。執行永遠不會從catch塊「返回」到try塊。所以,「This will not be printed。」

將不會被顯示。一旦執行了catch語句,程序控制從整個try/catch機制的下面一行繼續。

一個try和它的catch語句造成了一個單元。catch子句的範圍限制於try語句前面所定義的語句。一個catch語句不能捕獲另外一個try聲明所引起的異常(除非是嵌套的try語句狀況)。

被try保護的語句聲明必須在一個大括號以內(也就是說,它們必須在一個塊中)。你不能單獨使用try。

構造catch子句的目的是解決異常狀況而且像錯誤沒有發生同樣繼續運行。例如,下面的程序中,每個for循環的反覆獲得兩個隨機整數。這兩個整數分別被對方除,結果用來除12345。最後的結果存在a中。若是一個除法操做致使被零除錯誤,它將被捕獲,a的值設爲零,程序繼續運行。

// Handle an exception and move on.
import java.util.Random;

class HandleError {
    public static void main(String args[]) {
        int a=0, b=0, c=0;
        Random r = new Random();

        for(int i=0; i<32000; i++) {
            try {
                b = r.nextInt();
                c = r.nextInt();
                a = 12345 / (b/c);
            } catch (ArithmeticException e) {
                System.out.println("Division by zero.");
                a = 0; // set a to zero and continue
            }
            System.out.println("a: " + a);
        }
    }
}

 

顯示一個異常的描述

Throwable重載toString( )方法(由Object定義),因此它返回一個包含異常描述的字符串。你能夠經過在println( )中傳給異常一個參數來顯示該異常的描述。例如,前面程序的catch塊能夠被重寫成

catch (ArithmeticException e) {
    System.out.println("Exception: " + e);
    a = 0; // set a to zero and continue
}

當這個版本代替原程序中的版本,程序在標準javaJDK解釋器下運行,每個被零除錯誤顯示下面的消息:

  Exception: java.lang.ArithmeticException: / by zero

儘管在上下文中沒有特殊的值,顯示一個異常描述的能力在其餘狀況下是頗有價值的——特別是當你對異常進行實驗和調試時。

 

6.5  多重catch語句的使用                            

 某些狀況,由單個代碼段可能引發多個異常。處理這種狀況,你能夠定義兩個或更多的catch子句,每一個子句捕獲一種類型的異常。當異常被引起時,每個catch子句被依次檢查,第一個匹配異常類型的子句執行。當一個catch語句執行之後,其餘的子句被旁路,執行從try/catch塊之後的代碼開始繼續。下面的例子設計了兩種不一樣的異常類型:

// Demonstrate multiple catch statements.
class MultiCatch {
    public static void main(String args[]) {
        try {
            int a = args.length;
            System.out.println("a = " + a);
            int b = 42 / a;
            int c[] = { 1 };
            c[42] = 99;
        } catch(ArithmeticException e) {
            System.out.println("Divide by 0: " + e);
        } catch(ArrayIndexOutOfBoundsException e) {
            System.out.println("Array index oob: " + e);
        }
        System.out.println("After try/catch blocks.");
    }
}

 

ArithmeticException 和 ArrayIndexOutOfBoundsException 與之同類的還有那些,分別是什麼做用???


該程序在沒有命令行參數的起始條件下運行致使被零除異常,由於a爲0。若是你提供一個命令行參數,它將倖免於難,把a設成大於零的數值。可是它將致使ArrayIndexOutOf BoundsException異常,由於整型數組c的長度爲1,而程序試圖給c[42]賦值。

下面是運行在兩種不一樣狀況下程序的輸出:

C:\>java MultiCatch
a = 0
Divide by 0: java.lang.ArithmeticException: / by zero After try/catch blocks.
C:\>java MultiCatch TestArg
a = 1
Array index oob: java.lang.ArrayIndexOutOfBoundsException After try/catch blocks.

 


當你用多catch語句時,記住異常子類必須在它們任何父類以前使用是很重要的。這是由於運用父類的catch語句將捕獲該類型及其全部子類類型的異常。這樣,若是子類在父類後面,子類將永遠不會到達。並且,Java中不能到達的代碼是一個錯誤。例如,考慮下面的程序:

/* This program contains an error.
A subclass must come before its superclass in a series of catch statements. If not,unreachable code will be created and acompile-time error will result.
*/
class SuperSubCatch {
    public static void main(String args[]) {
        try {
            int a = 0;
            int b = 42 / a;
        } catch(Exception e) {
            System.out.println("Generic Exception catch.");
        }
        /* This catch is never reached because
        ArithmeticException is a subclass of Exception. */
        catch(ArithmeticException e) { // ERROR - unreachable
            System.out.println("This is never reached.");
        }
    }
}

 


若是你試着編譯該程序,你會收到一個錯誤消息,該錯誤消息說明第二個catch語句不會到達,由於該異常已經被捕獲。由於ArithmeticException 是Exception的子類,第一個catch語句將處理全部的面向Exception的錯誤,包括ArithmeticException。這意味着第二個catch語句永遠不會執行。爲修改程序,顛倒兩個catch語句的次序。

 

6.6  java中try語句的嵌套                        

Try語句能夠被嵌套。也就是說,一個try語句能夠在另外一個try塊內部。每次進入try語句,異常的先後關係都會被推入堆棧。若是一個內部的try語句不含特殊異常的catch處理程序,堆棧將彈出,下一個try語句的catch處理程序將檢查是否與之匹配。這個過程將繼續直到一個catch語句匹配成功,或者是直到全部的嵌套try語句被檢查耗盡。若是沒有catch語句匹配,Java的運行時系統將處理這個異常。下面是運用嵌套try語句的一個例子:

// An example of nested try statements.
class NestTry {
    public static void main(String args[]) {
        try {
            int a = args.length;
            /* If no command-line args are present,the following statement will generate a divide-by-zero exception. */
            int b = 42 / a;
            System.out.println("a = " + a);
            try { // nested try block
                /* If one command-line arg is used,then a divide-by-zero exception will be generated by the following code. */
                if(a==1) a = a/(a-a); // division by zero
                /* If two command-line args are used,then generate an out-of-bounds exception. */
                if(a==2) {
                    int c[] = { 1 };
                    c[42] = 99; // generate an out-of-bounds exception
                }
            } catch(ArrayIndexOutOfBoundsException e) {
                System.out.println("Array index out-of-bounds: " + e);
            }
        } catch(ArithmeticException e) {
            System.out.println("Divide by 0: " + e);
        }
    }
}//當第一個if否定後,if中的語句還會執行嗎? 仍是直接跳出????

 



如你所見,該程序在一個try塊中嵌套了另外一個try塊。程序工做以下:當你在沒有命令行參數的狀況下執行該程序,外面的try塊將產生一個被零除的異常。程序在有一個命令行參數條件下執行,由嵌套的try塊產生一個被零除的錯誤。由於內部的塊不匹配這個異常,它將把異常傳給外部的try塊,在那裏異常被處理。若是你在具備兩個命令行參數的條件下執行該程序,由內部try塊產生一個數組邊界異常。下面的結果闡述了每一種狀況:

C:\>java NestTry
Divide by 0: java.lang.ArithmeticException: / by zero
C:\>java NestTry One
a = 1
Divide by 0: java.lang.ArithmeticException: / by zero
C:\>java NestTry One Two
a = 2
Array index out-of-bounds: java.lang.ArrayIndexOutOfBoundsException


當有方法調用時,try語句的嵌套能夠很隱蔽的發生。例如,你能夠把對方法的調用放在一個try塊中。在該方法內部,有另外一個try語句。這種狀況下,方法內部的try仍然是嵌套在外部調用該方法的try塊中的。下面是前面例子的修改,嵌套的try塊移到了方法nesttry( )的內部:

/* Try statements can be implicitly nested via calls to methods. */
class MethNestTry {
    static void nesttry(int a) {
        try { // nested try block
            /* If one command-line arg is used,then a divide-by-zero exception will be generated by the following code. */
            if(a==1) a = a/(a-a); // division by zero
            /* If two command-line args are used,then generate an out-of-bounds exception. */
            if(a==2) {
                int c[] = { 1 };
                c[42] = 99; // generate an out-of-bounds exception
            }
        } catch(ArrayIndexOutOfBoundsException e) {
            System.out.println("Array index out-of-bounds: " + e);
        }
    }

    public static void main(String args[]) {
        try {
            int a = args.length;
           /* If no command-line args are present,the following statement will generate a divide-by-zero exception. */
           int b = 42 / a;
           System.out.println("a = " + a);
           nesttry(a);
        } catch(ArithmeticException e) {
            System.out.println("Divide by 0: " + e);
        }
    }
}

該程序的輸出與前面的例子相同。

 

6.7  java throw:異常的拋出                          

到目前爲止,你只是獲取了被Java運行時系統拋出的異常。然而,程序能夠用throw語句拋出明確的異常。Throw語句的一般形式以下:

throw ThrowableInstance;

這裏,ThrowableInstance必定是Throwable類類型或Throwable子類類型的一個對象。簡單類型,例如int或char,以及非Throwable類,例如String或Object,不能用做異常。有兩種能夠得到Throwable對象的方法:在catch子句中使用參數或者用new操做符建立。

程序執行在throw語句以後當即中止;後面的任何語句不被執行。最牢牢包圍的try塊用來檢查它是否含有一個與異常類型匹配的catch語句。若是發現了匹配的塊,控制轉向該語句;若是沒有發現,次包圍的try塊來檢查,以此類推。若是沒有發現匹配的catch塊,默認異常處理程序中斷程序的執行而且打印堆棧軌跡。

下面是一個建立並拋出異常的例子程序,與異常匹配的處理程序再把它拋出給外層的處理程序。

// Demonstrate throw.
class ThrowDemo {
    static void demoproc() {
      try {
         throw new NullPointerException("demo");
      } catch(NullPointerException e) {
         System.out.println("Caught inside demoproc.");
         throw e; // rethrow the exception
      }
   }

   public static void main(String args[]) {
      try {
         demoproc();
      } catch(NullPointerException e) {
         System.out.println("Recaught: " + e);
      }
   }
}

該程序有兩個機會處理相同的錯誤。首先,main()設立了一個異常關係而後調用demoproc( )。 demoproc( )方法而後設立了另外一個異常處理關係而且當即拋出一個新的NullPointerException實例,NullPointerException在下一行被捕獲。異常因而被再次拋出。下面是輸出結果:

Caught inside demoproc.
Recaught: java.lang.NullPointerException: demo

 


該程序還闡述了怎樣建立Java的標準異常對象,特別注意下面這一行:

 throw new NullPointerException("demo");

這裏,new用來構造一個NullPointerException實例。全部的Java內置的運行時異常有兩個構造函數:一個沒有參數,一個帶有一個字符串參數。當用到第二種形式時,參數指定描述異常的字符串。若是對象用做 print( )或println( )的參數時,該字符串被顯示。這一樣能夠經過調用getMessage( )來實現,getMessage( )是由Throwable定義的

 

 6.8  java throws子句                        

若是一個方法能夠致使一個異常但不處理它,它必須指定這種行爲以使方法的調用者能夠保護它們本身而不發生異常。作到這點你能夠在方法聲明中包含一個throws子句。一個 throws 子句列舉了一個方法可能拋出的全部異常類型。這對於除Error或RuntimeException及它們子類之外類型的全部異常是必要的。一個方法能夠拋出的全部其餘類型的異常必須在throws子句中聲明。若是不這樣作,將會致使編譯錯誤。

下面是包含一個throws子句的方法聲明的通用形式:

type method-name(parameter-list) throws exception-list{
    // body of method
}

 


這裏,exception-list是該方法能夠拋出的以有逗號分割的異常列表。

下面是一個不正確的例子。該例試圖拋出一個它不能捕獲的異常。由於程序沒有指定一個throws子句來聲明這一事實,程序將不會編譯。

// This program contains an error and will not compile.
class ThrowsDemo {
    static void throwOne() {
        System.out.println("Inside throwOne.");
        throw new IllegalAccessException("demo");
    }
    public static void main(String args[]) {
        throwOne();
    }
}  錯誤的!

 



爲編譯該程序,須要改變兩個地方。第一,須要聲明throwOne( )引起IllegalAccess Exception異常。第二,main( )必須定義一個try/catch 語句來捕獲該異常。正確的例子以下:

// This is now correct.
class ThrowsDemo {
    static void throwOne() throws IllegalAccessException {
      System.out.println("Inside throwOne.");
      throw new IllegalAccessException("demo");
   }
   public static void main(String args[]) {
      try {
         throwOne();
      } catch (IllegalAccessException e) {
         System.out.println("Caught " + e);
      }
   }
}

 



下面是例題的輸出結果:
inside throwOne
caught java.lang.IllegalAccessException: demo

 

6.9  java finally                                  

當異常被拋出,一般方法的執行將做一個陡峭的非線性的轉向。依賴於方法是怎樣編碼的,異常甚至能夠致使方法過早返回。這在一些方法中是一個問題。例如,若是一個方法打開一個文件項並關閉,而後退出,你不但願關閉文件的代碼被異常處理機制旁路。finally關鍵字爲處理這種意外而設計。

finally建立一個代碼塊。該代碼塊在一個try/catch 塊完成以後另外一個try/catch出現以前執行。finally塊不管有沒有異常拋出都會執行。若是異常被拋出,finally甚至是在沒有與該異常相匹配的catch子句狀況下也將執行。一個方法將從一個try/catch塊返回到調用程序的任什麼時候候,通過一個未捕獲的異常或者是一個明確的返回語句,finally子句在方法返回以前仍將執行。這在關閉文件句柄和釋聽任何在方法開始時被分配的其餘資源是頗有用的。finally子句是可選項,能夠有也能夠無。然而每個try語句至少須要一個catch或finally子句

下面的例子顯示了3種不一樣的退出方法。每個都執行了finally子句:

// Demonstrate finally.
class FinallyDemo {
    // Through an exception out of the method.
    static void procA() {
        try {
           System.out.println("inside procA");
           throw new RuntimeException("demo");
        } finally {
           System.out.println("procA's finally");
        }
    }

    // Return from within a try block.
    static void procB() {
        try {
           System.out.println("inside procB");
           return;
        } finally {
           System.out.println("procB's finally");
        }
    }
    // Execute a try block normally.
    static void procC() {
        try {
           System.out.println("inside procC");
        } finally {
           System.out.println("procC's finally");
        }
    }

    public static void main(String args[]) {
       try {
          procA();
       } catch (Exception e) {
          System.out.println("Exception caught");
       }
       procB();
       procC();
    }
}

 


該例中,procA( )過早地經過拋出一個異常中斷了try。Finally子句在退出時執行。procB( )的try語句經過一個return語句退出。在procB( )返回以前finally子句執行。在procC()中,try語句正常執行,沒有錯誤。然而,finally塊仍將執行。

注意:若是finally塊與一個try聯合使用,finally塊將在try結束以前執行。

下面是上述程序產生的輸出:
inside procA
procA’s finally
Exception caught
inside procB
procB’s finally
inside procC
procC’s finally

 

6.10  java的內置異常                                            

在標準包java.lang中,Java定義了若干個異常類。前面的例子曾用到其中一些。這些異常通常是標準類RuntimeException的子類。由於java.lang實際上被全部的Java程序引入,多數從RuntimeException派生的異常都自動可用。並且,它們不須要被包含在任何方法的throws列表中。Java語言中,這被叫作未經檢查的異常(unchecked exceptions )。由於編譯器不檢查它來看一個方法是否處理或拋出了這些異常。 java.lang中定義的未經檢查的異常列於表10-1。表10-2列出了由 java.lang定義的必須在方法的throws列表中包括的異常,若是這些方法能產生其中的某個異常可是不能本身處理它。這些叫作受檢查的異常(checked exceptions)。Java定義了幾種與不一樣類庫相關的其餘的異常類型。

表 10-1 Java 的 java.lang 中定義的未檢查異常子類
異常 說明
ArithmeticException 算術錯誤,如被0除
ArrayIndexOutOfBoundsException 數組下標出界
ArrayStoreException 數組元素賦值類型不兼容
ClassCastException 非法強制轉換類型
IllegalArgumentException 調用方法的參數非法
IllegalMonitorStateException 非法監控操做,如等待一個未鎖定線程
IllegalStateException 環境或應用狀態不正確
IllegalThreadStateException 請求操做與當前線程狀態不兼容
IndexOutOfBoundsException 某些類型索引越界
NullPointerException 非法使用空引用
NumberFormatException 字符串到數字格式非法轉換
SecurityException 試圖違反安全性
StringIndexOutOfBounds 試圖在字符串邊界以外索引
UnsupportedOperationException 遇到不支持的操做

 

 

表 10-2  java.lang 中定義的檢查異常
異常 意義
ClassNotFoundException 找不到類
CloneNotSupportedException 試圖克隆一個不能實現Cloneable接口的對象
IllegalAccessException 對一個類的訪問被拒絕
InstantiationException 試圖建立一個抽象類或者抽象接口的對象
InterruptedException 一個線程被另外一個線程中斷
NoSuchFieldException 請求的字段不存在
NoSuchMethodException 請求的方法不存在

 

 

6.11  使用Java建立本身的異常子類                                          

儘管Java的內置異常處理大多數常見錯誤,你也許但願創建你本身的異常類型來處理你所應用的特殊狀況。這是很是簡單的:只要定義Exception的一個子類就能夠了(Exception固然是Throwable的一個子類)。你的子類不須要實際執行什麼——它們在類型系統中的存在容許你把它們當成異常使用。

Exception類本身沒有定義任何方法。固然,它繼承了Throwable提供的一些方法。所以,全部異常,包括你建立的,均可以得到Throwable定義的方法。這些方法顯示在表10-3中。你還能夠在你建立的異常類中覆蓋一個或多個這樣的方法。

表 10-3 Throwable 定義的方法
方法 描述
Throwable fillInStackTrace( ) 返回一個包含完整堆棧軌跡的Throwable對象,該對象可能被再次引起。
String getLocalizedMessage( ) 返回一個異常的局部描述
String getMessage( ) 返回一個異常的描述
void printStackTrace( ) 顯示堆棧軌跡
void printStackTrace(PrintStreamstream) 把堆棧軌跡送到指定的流
void printStackTrace(PrintWriterstream) 把堆棧軌跡送到指定的流
String toString( ) 返回一個包含異常描述的String對象。當輸出一個Throwable對象時,該方法被println( )調用


下面的例子聲明瞭Exception的一個新子類,而後該子類看成方法中出錯情形的信號。它重載了toString( )方法,這樣能夠用println( )顯示異常的描述。

// This program creates a custom exception type.
class MyException extends Exception {
    private int detail;
    MyException(int a) {
        detail = a;
    }

    public String toString() {
        return "MyException[" + detail + "]";
    }
}

class ExceptionDemo {
    static void compute(int a) throws MyException {
        System.out.println("Called compute(" + a + ")");
       if(a > 10)
          throw new MyException(a);
       System.out.println("Normal exit");
    }

    public static void main(String args[]) {
       try {
           compute(1);
           compute(20);
        } catch (MyException e) {
            System.out.println("Caught " + e);
        }
    }
}

 


該例題定義了Exception的一個子類MyException。該子類很是簡單:它只含有一個構造函數和一個重載的顯示異常值的toString( )方法。ExceptionDemo類定義了一個compute( )方法。該方法拋出一個MyException對象。當compute( )的整型參數比10大時該異常被引起。

main( )方法爲MyException設立了一個異常處理程序,而後用一個合法的值和不合法的值調用compute( )來顯示執行通過代碼的不一樣路徑。下面是結果:
Called compute(1)
Normal exit
Called compute(20)
Caught MyException[20]

 

 6.12  java斷言                                                             

斷言的概念

斷言用於證實和測試程序的假設,好比「這裏的值大於 5」。
斷言能夠在運行時從代碼中徹底刪除,因此對代碼的運行速度沒有影響

斷言的使用

斷言有兩種方法:

  • 一種是 assert<<布爾表達式>> ;
  • 另外一種是 assert<<布爾表達式>> :<<細節描述>>。

若是布爾表達式的值爲false , 將拋出AssertionError 異常; 細節描述是AssertionError異常的描述文本使用 javac –source 1.4 MyClass.java 的方式進行編譯示例以下:

public class AssertExample {
    public static void main(String[] args) {
        int x = 10;
        if (args.length > 0) {
            try {
                x = Integer.parseInt(args[0]);
            } catch (NumberFormatException nfe) {
                /* Ignore */
            }
        }
        System.out.println("Testing assertion that x == 10");
        assert x == 10 : "Our assertion failed";
        System.out.println("Test passed");
    }
}

因爲引入了一個新的關鍵字,因此在編譯的時候就須要增長額外的參數,要編譯成功,必須使用 JDK1.4 的 javac 並加上參數'-source 1.4',例如能夠使用如下的命令編譯上面的代碼:
    javac -source 1.4 AssertExample.java


以上程序運行使用斷言功能也須要使用額外的參數(而且須要一個數字的命令行參數),例如:
    java -ea AssertExample 1


程序的輸出爲:
Testing assertion that x == 10
Exception in thread "main" java.lang.AssertionError:Our assertion failed
at AssertExample.main(AssertExample.java:20)

因爲輸入的參數不等於 10,所以斷言功能使得程序運行時拋出斷言錯誤,注意是錯誤, 這意味着程序發生嚴重錯誤而且將強制退出。斷言使用 boolean 值,若是其值不爲 true 則 拋出 AssertionError 並終止程序的運行。

斷言推薦使用方法

用於驗證方法中的內部邏輯,包括:

  • 內在不變式
  • 控制流程不變式
  • 後置條件和類不變式

注意:不推薦用於公有方法內的前置條件的檢查。

運行時屏蔽斷言

運行時要屏蔽斷言,能夠用以下方法:
    java –disableassertions 或 java –da 類名


運行時要容許斷言,能夠用以下方法:
    java –enableassertions 或 java –ea類名

 

 七◐  java多線程編程                                

7.1  java線程的概念                                          

和其餘多數計算機語言不一樣,Java內置支持多線程編程(multithreaded programming)。(這應該是JVM的功勞

多線程程序包含兩條或兩條以上併發運行的部分。程序中每一個這樣的部分都叫一個線程(thread),每一個線程都有獨立的執行路徑。所以,多線程是多任務處理的一種特殊形式。

你必定知道多任務處理,由於它實際上被全部的現代操做系統所支持。然而,多任務處理有兩種大相徑庭的類型:基於進程的和基於線程的。認識二者的不一樣是十分重要的。

對不少讀者,基於進程的多任務處理是更熟悉的形式。進程(process)本質上是一個執行的程序。所以,基於進程(process-based) 的多任務處理的特色是容許你的計算機同時運行兩個或更多的程序。舉例來講,基於進程的多任務處理使你在運用文本編輯器的時候能夠同時運行Java編譯器。在基於進程的多任務處理中,程序是調度程序所分派的最小代碼單位。

基於線程(thread-based) 的多任務處理環境中,線程是最小的執行單位。這意味着一個程序能夠同時執行兩個或者多個任務的功能。例如,一個文本編輯器能夠在打印的同時格式化文本。因此,多進程程序處理「大圖片」,而多線程程序處理細節問題

多線程程序比多進程程序須要更少的管理費用。進程是重量級的任務,須要分配它們本身獨立的地址空間。進程間通訊是昂貴和受限的。進程間的轉換也是很須要花費的。另外一方面,線程是輕量級的選手。它們共享相同的地址空間而且共同分享同一個進程。線程間通訊是便宜的,線程間的轉換也是低成本的。當Java程序使用多進程任務處理環境時,多進程程序不受Java的控制,而多線程則受Java控制

多線程幫助你寫出CPU最大利用率的高效程序,由於空閒時間保持最低。這對Java運行的交互式的網絡互連環境是相當重要的,由於空閒時間是公共的。舉個例子來講,網絡的數據傳輸速率遠低於計算機處理能力,本地文件系統資源的讀寫速度遠低於CPU的處理能力,固然,用戶輸入也比計算機慢不少。在傳統的單線程環境中,你的程序必須等待每個這樣的任務完成之後才能執行下一步——儘管CPU有不少空閒時間。多線程使你可以得到並充分利用這些空閒時間。

若是你在Windows 98 或Windows 2000這樣的操做系統下有編程經驗,那麼你已經熟悉了多線程。然而,Java管理線程使多線程處理尤爲方便,由於不少細節對你來講是易於處理的。

 

7.2  java線程模型                                          

Java運行系統在不少方面依賴於線程,全部的類庫設計都考慮到多線程。實際上,Java使用線程來使整個環境異步。這有利於經過防止CPU循環的浪費來減小無效部分。

爲更好的理解多線程環境的優點能夠將它與它的對照物相比較。單線程系統的處理途徑是使用一種叫做輪詢的事件循環方法。在該模型中,單線程控制在一無限循環中運行,輪詢一個事件序列來決定下一步作什麼。一旦輪詢裝置返回信號代表,已準備好讀取網絡文件,事件循環調度控制管理到適當的事件處理程序。直到事件處理程序返回,系統中沒有其餘事件發生。這就浪費了CPU時間。這致使了程序的一部分獨佔了系統,阻止了其餘事件的執行。總的來講,單線程環境,當一個線程由於等待資源時阻塞(block,掛起執行),整個程序中止運行。

Java多線程的優勢在於取消了主循環/輪詢機制。一個線程能夠暫停而不影響程序的其餘部分。例如,當一個線程從網絡讀取數據或等待用戶輸入時產生的空閒時間能夠被利用到其餘地方。多線程容許活的循環在每一幀間隙中沉睡一秒而不暫停整個系統。在Java程序中出現線程阻塞,僅有一個線程暫停,其餘線程繼續運行。

線程存在於好幾種狀態。線程能夠正在運行( running)。只要得到CPU時間它就能夠運行。運行的線程能夠被掛起( suspend),並臨時中斷它的執行。一個掛起的線程能夠被恢復( resume,容許它從中止的地方繼續運行。一個線程能夠在等待資源時被阻塞( block)。

在任什麼時候候,線程能夠終止( terminate),這當即中斷了它的運行。一旦終止,線程不能被恢復。

線程優先級

Java給每一個線程安排 優先級以決定與其餘線程比較時該如何對待該線程。線程優先級是詳細說明線程間優先關係的整數。做爲絕對值,優先級是毫無心義的;當只有一個線程時,優先級高的線程並不比優先權低的線程運行的快。相反,線程的優先級是用來決定什麼時候從一個運行的線程切換到另外一個。這叫「上下文轉換」(context switch)。決定上下文轉換髮生的規則很簡單:
  • 線程能夠自動放棄控制。在I/O未決定的狀況下,睡眠或阻塞由明確的讓步來完成。在這種假定下,全部其餘的線程被檢測,準備運行的最高優先級線程被授予CPU。
  • 線程能夠被高優先級的線程搶佔。在這種狀況下,低優先級線程不主動放棄,處理器只是被先佔——不管它正在幹什麼——處理器被高優先級的線程佔據。基本上,一旦高優先級線程要運行,它就執行。這叫作有優先權的多任務處理。

當兩個相同優先級的線程競爭CPU週期時,情形有一點複雜。對於Windows98這樣的操做系統,等優先級的線程是在循環模式下自動劃分時間的。對於其餘操做系統,例如Solaris 2.x,等優先級線程相對於它們的對等體自動放棄。若是不這樣,其餘的線程就不會運行。

警告:不一樣的操做系統下等優先級線程的上下文轉換可能會產生錯誤。

同步性

由於多線程在你的程序中引入了一個異步行爲,因此在你須要的時候必須有增強同步性的方法。舉例來講,若是你但願兩個線程相互通訊並共享一個複雜的數據結構,例如鏈表序列,你須要某些方法來確保它們沒有相互衝突。也就是說,你必須防止一個線程寫入數據而另外一個線程正在讀取鏈表中的數據。爲此目的,Java在進程間同步性的老模式基礎上實行了另外一種方法:管程( monitor)。管程是一種由C.A.R.Hoare首先定義的控制機制。

你能夠把管程想象成一個僅控制一個線程的小盒子。一旦線程進入管程,全部線程必須等待直到該線程退出了管程。用這種方法,管程能夠用來防止共享的資源被多個線程操縱。

不少多線程系統把管程做爲程序必須明確的引用和操做的對象。Java提供一個清晰的解決方案。沒有「Monitor」類;相反,每一個對象都擁有本身的隱式管程,當對象的同步方法被調用時管程自動載入。一旦一個線程包含在一個同步方法中,沒有其餘線程能夠調用相同對象的同步方法。這就使你能夠編寫很是清晰和簡潔的多線程代碼,由於同步支持是語言內置的。

消息傳遞

在你把程序分紅若干線程後,你就要定義各線程之間的聯繫。用大多數其餘語言規劃時,你必須依賴於操做系統來確立線程間通訊。這樣固然增長花費。然而,Java提供了多線程間談話清潔的、低成本的途徑——經過調用全部對象都有的預先肯定的方法。Java的消息傳遞系統容許一個線程進入一個對象的一個同步方法,而後在那裏等待,直到其餘線程明確通知它出來。

Thread 類和Runnable 接口

Java的多線程系統創建於Thread類,它的方法,它的共伴接口Runnable基礎上。Thread類封裝了線程的執行。既然你不能直接引用運行着的線程的狀態,你要經過它的代理處理它,因而Thread 實例產生了。爲建立一個新的線程,你的程序必須擴展Thread 或實現Runnable接口。

Thread類定義了好幾種方法來幫助管理線程。本章用到的方法如表11-1所示:
表 11-1 管理線程的方法
方法 意義
getName 得到線程名稱
getPriority 得到線程優先級
jsAlive 斷定線程是否仍在運行
join 等待一個線程終止
run 線程的入口點.
sleep 在一段時間內掛起線程
start 經過調用運行方法來啓動線程

到目前爲止,本書所應用的例子都是用單線程的。本章剩餘部分解釋如何用Thread 和 Runnable 來建立、管理線程。讓咱們從全部Java程序都有的線程:主線程開始。
 

 7.3  java主線程

urrentThread( )
該方法返回一個調用它的線程的引用。一旦你得到主線程的引用,你就能夠像控制其餘線程那樣控制主線程。

讓咱們從複習下面例題開始:

// Controlling the main Thread.
class CurrentThreadDemo {
    public static void main(String args[]) {
        Thread t = Thread.currentThread();
        System.out.println("Current thread: " + t);
        // change the name of the thread
        t.setName("My Thread");
        System.out.println("After name change: " + t);
        try {
            for(int n = 5; n > 0; n--) {
                System.out.println(n);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted");
        }
    }
}


在本程序中,當前線程(天然是主線程)的引用經過調用currentThread()得到,該引用保存在局部變量t中。而後,程序顯示了線程的信息。接着程序調用setName()改變線程的內部名稱。線程信息又被顯示。而後,一個循環數從5開始遞減,每數一次暫停一秒。暫停是由sleep()方法來完成的。Sleep()語句明確規定延遲時間是1毫秒。注意循環外的try/catch塊。

Thread類的sleep()方法可能引起一個InterruptedException異常。這種情形會在其餘線程想要打攪沉睡線程時發生。本例只是打印了它是否被打斷的消息。在實際的程序中,你必須靈活處理此類問題。下面是本程序的輸出:
Current thread: Thread[main,5,main]
After name change: Thread[My Thread,5,main]
5
4
3
2
1

注意t做爲語句println()中參數運用時輸出的產生。該顯示順序:線程名稱,優先級以及組的名稱。默認狀況下,主線程的名稱是main。它的優先級是5,這也是默認值,main也是所屬線程組的名稱一個線程組(thread group)是一種將線程做爲一個總體集合的狀態控制的數據結構。這個過程由專有的運行時環境來處理,在此就不贅述了。線程名改變後,t又被輸出。此次,顯示了新的線程名。

讓咱們更仔細的研究程序中Thread類定義的方法。sleep()方法按照毫秒級的時間指示使線程從被調用到掛起。它的一般形式以下:

 static void sleep(long milliseconds) throws InterruptedException

掛起的時間被明肯定義爲毫秒。該方法可能引起InterruptedException異常。

sleep()方法還有第二種形式,顯示以下,該方法容許你指定時間是以毫秒仍是以納秒爲週期。

static void sleep(long milliseconds, int nanoseconds) throws InterruptedException

第二種形式僅當容許以納秒爲時間週期時可用。如上述程序所示,你能夠用setName()設置線程名稱,用getName()來得到線程名稱(該過程在程序中沒有體現)。這些方法都是Thread 類的成員,聲明以下:

    final void setName(String threadName)
    final String getName( )

這裏,threadName 特指線程名稱。

 

7.4  java建立線程(Runnable接口和Thread類)                  

大多數狀況,經過實例化一個Thread對象來建立一個線程。Java定義了兩種方式:
  • 實現Runnable 接口;
  • 能夠繼承Thread類。

下面的兩小節依次介紹了每一種方式。

實現Runnable接口

建立線程的最簡單的方法就是建立一個實現Runnable 接口的類。Runnable抽象了一個執行代碼單元。你能夠經過實現Runnable接口的方法建立每個對象的線程。 爲實現Runnable 接口,一個類僅需實現一個run()的簡單方法,該方法聲明以下:
    public void run( )

在run()中能夠定義代碼來構建新的線程。理解下面內容是相當重要的:run()方法可以像主線程那樣調用其餘方法,引用其餘類,聲明變量。 僅有的不一樣是run()在程序中確立另外一個併發的線程執行入口。當run()返回時,該線程結束

在你已經建立了實現Runnable接口的類之後,你要在類內部實例化一個Thread類的對象。Thread 類定義了好幾種構造函數。咱們會用到的以下:
Thread(Runnable threadOb, String threadName)
該構造函數中, threadOb是一個實現 Runnable接口類的實例。這定義了線程執行的起點。新線程的名稱由threadName定義。

創建新的線程後,它並不運行直到調用了它的 start()方法,該方法在Thread 類中定義。本質上, start() 執行的是一個對run()的調用。 Start()方法聲明以下:
   void start( )
下面的例子是建立一個新的線程並啓動它運行:
// Create a second thread.
class NewThread implements Runnable {
    Thread t;
    NewThread() {
        // Create a new, second thread
        t = new Thread(this, "Demo Thread");
        System.out.println("Child thread: " + t);
        t.start(); // Start the thread
    }

    // This is the entry point for the second thread.
    public void run() {
        try {
            for(int i = 5; i > 0; i--) {
                System.out.println("Child Thread: " + i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            System.out.println("Child interrupted.");
        }
        System.out.println("Exiting child thread.");
    }
}

class ThreadDemo {
    public static void main(String args[]) {
        new NewThread(); // create a new thread
        try {
            for(int i = 5; i > 0; i--) {
                System.out.println("Main Thread: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
           System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread exiting.");
    }
}
在NewThread 構造函數中,新的Thread對象由下面的語句建立:
    t = new Thread(this, "Demo Thread");
經過前面的語句this 代表在this對象中你想要新的線程調用run()方法。而後,start() 被調用,以run()方法爲開始啓動了線程的執行。這使子線程for 循環開始執行。調用start()以後,NewThread 的構造函數返回到main()。當主線程被恢復,它到達for 循環。兩個線程繼續運行,共享CPU,直到它們的循環結束。該程序的輸出以下:
Child thread: Thread[Demo Thread,5,main]
Main Thread: 5
Child Thread: 5
Child Thread: 4
Main Thread: 4
Child Thread: 3
Child Thread: 2
Main Thread: 3
Child Thread: 1
Exiting child thread.
Main Thread: 2
Main Thread: 1
Main thread exiting.

如前面提到的,在多線程程序中,一般主線程必須是結束運行的最後一個線程。實際上,一些老的JVM,若是主線程先於子線程結束,Java的運行時間系統就可能「掛起」。前述程序保證了主線程最後結束, 由於主線程沉睡週期1000毫秒,而子線程僅爲500毫秒。這就使子線程在主線程結束以前先結束。簡而言之,你將看到等待線程結束的更好途徑

擴展Thread

建立線程的另外一個途徑是建立一個新類來擴展Thread類,而後建立該類的實例。當一個類繼承Thread時,它必須重載 run()方法,這個run()方法是新線程的入口。它也必須調用 start()方法去啓動新線程執行。下面用擴展thread類重寫前面的程序:
// Create a second thread by extending Thread
class NewThread extends Thread {
    NewThread() {
        // Create a new, second thread
        super("Demo Thread");
        System.out.println("Child thread: " + this);
        start(); // Start the thread
    }

    // This is the entry point for the second thread.
    public void run() {
        try {
            for(int i = 5; i > 0; i--) {
                System.out.println("Child Thread: " + i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            System.out.println("Child interrupted.");
        }
        System.out.println("Exiting child thread.");
    }
}

class ExtendThread {
    public static void main(String args[]) {
        new NewThread(); // create a new thread
        try {
            for(int i = 5; i > 0; i--) {
                System.out.println("Main Thread: " + i);
                Thread.sleep(1000);
           }
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread exiting.");
    }
}
該程序生成和前述版本相同的輸出。子線程是由實例化NewThread對象生成的,該對象從Thread類派生。注意NewThread 中super()的調用。該方法調用了下列形式的Thread構造函數:
 public Thread(String threadName)
這裏,threadName指定線程名稱。

選擇合適方法

到這裏,你必定會奇怪爲何Java有兩種建立子線程的方法,哪種更好呢。全部的問題都歸於一點。Thread類定義了多種方法能夠被派生類重載。對於全部的方法,唯一的必須被重載的是run()方法。這固然是實現Runnable接口所需的一樣的方法。不少Java程序員認爲類僅在它們被增強或修改時應該被擴展。所以,若是你不重載Thread的其餘方法時,最好只實現Runnable 接口。這固然由你決定。然而,在本章的其餘部分,咱們應用實現runnable接口的類來建立線程。
 

 7.5  java建立多線程                                  

 到目前爲止,咱們僅用到兩個線程:主線程和一個子線程。然而,你的程序能夠建立所需的更多線程。例如,下面的程序建立了三個子線程:

// Create multiple threads.
class NewThread implements Runnable {      
    String name; // name of thread
    Thread t;
    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        t.start(); // Start the thread
    }

    // This is the entry point for thread.
    public void run() {
        try {
            for(int i = 5; i > 0; i--) {
               System.out.println(name + ": " + i);
               Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println(name + "Interrupted");
        }
        System.out.println(name + " exiting.");
    }
}

class MultiThreadDemo {
    public static void main(String args[]) {
        new NewThread("One"); // start threads
        new NewThread("Two");
        new NewThread("Three");
        try {
            // wait for other threads to end
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        System.out.println("Main thread exiting.");
    }
}

 



程序輸出以下所示:
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread: Thread[Three,5,main]
One: 5
Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Three: 3
Two: 3
One: 2
Three: 2
Two: 2
One: 1
Three: 1
Two: 1
One exiting.
Two exiting.
Three exiting.
Main thread exiting.

如你所見,一旦啓動,全部三個子線程共享CPU。注意main()中對sleep(10000)的調用。這使主線程沉睡十秒確保它最後結束。(這只是穿件幾個相同的線程,線程中run函數修改後能夠實現不一樣的線程

 

7.6  java isAlive()和join()的使用                    

如前所述,一般你但願主線程最後結束。在前面的例子中,這點是經過在main()中調用sleep()來實現的,通過足夠長時間的延遲以確保全部子線程都先於主線程結束。然而,這不是一個使人滿意的解決方法,它也帶來一個大問題: 一個線程如何知道另外一線程已經結束?幸運的是,Thread類提供了回答此問題的方法。

兩種方法能夠斷定一個線程是否結束。第一,能夠在線程中調用isAlive()。這種方法由Thread定義,它的一般形式以下:
   
 final boolean isAlive( )
若是所調用線程仍在運行,isAlive()方法返回true,若是不是則返回false。但isAlive()不多用到,等待線程結束的更經常使用的方法是調用join(),描述以下:
   
 final void join( ) throws InterruptedExceptio
該方法等待所調用線程結束。該名字來自於要求線程等待直到指定線程參與的概念。 join()的附加形式 容許給等待指定線程結束定義一個最大時間。下面是前面例子的改進版本。 運用join()以確保主線程最後結束。一樣,它也演示了isAlive()方法。
// Using join() to wait for threads to finish.
class NewThread implements Runnable {
    String name; // name of thread
    Thread t;
    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        t.start(); // Start the thread
    }
    // This is the entry point for thread.
    public void run() {
        try {
            for(int i = 5; i > 0; i--) {
               System.out.println(name + ": " + i);
               Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println(name + " interrupted.");
        }
        System.out.println(name + " exiting.");
    }
}

class DemoJoin {
    public static void main(String args[]) {
        NewThread ob1 = new NewThread("One");
        NewThread ob2 = new NewThread("Two");
        NewThread ob3 = new NewThread("Three");
        System.out.println("Thread One is alive: "+ ob1.t.isAlive());  //isAlive
        System.out.println("Thread Two is alive: "+ ob2.t.isAlive());
        System.out.println("Thread Three is alive: "+ ob3.t.isAlive());
        // wait for threads to finish
        try {
            System.out.println("Waiting for threads to finish.");
            ob1.t.join();    //join
            ob2.t.join();
            ob3.t.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        System.out.println("Thread One is alive: "+ ob1.t.isAlive());
        System.out.println("Thread Two is alive: "+ ob2.t.isAlive());
        System.out.println("Thread Three is alive: "+ ob3.t.isAlive());
        System.out.println("Main thread exiting.");
    }
}
程序輸出以下所示:
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread: Thread[Three,5,main]
Thread One is alive: true
Thread Two is alive: true
Thread Three is alive: true
Waiting for threads to finish.
One: 5
Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Two: 3
Three: 3
One: 2
Two: 2
Three: 2
One: 1
Two: 1
Three: 1
Two exiting.
Three exiting.
One exiting.
Thread One is alive: false
Thread Two is alive: false
Thread Three is alive: false
Main thread exiting.

如你所見, 調用join()後返回,線程終止執行
 

7.7  java線程優先級                                

線程優先級被線程調度用來斷定什麼時候每一個線程容許運行。理論上,優先級高的線程比優先級低的線程得到更多的CPU時間。實際上,線程得到的CPU時間一般由包括優先級在內的多個因素決定(例如,一個實行多任務處理的操做系統如何更有效的利用CPU時間)。

一個優先級高的線程天然比優先級低的線程優先。舉例來講,當低優先級線程正在運行,而一個高優先級的線程被恢復(例如從沉睡中或等待I/O中),它將搶佔低優先級線程所使用的CPU。

理論上,等優先級線程有同等的權利使用CPU。但你必須當心了。記住,Java是被設計成能在不少環境下工做的。一些環境下實現多任務處理從本質上與其餘環境不一樣。爲安全起見,等優先級線程偶爾也受控制。這保證了全部線程在無優先級的操做系統下都有機會運行。實際上,在無優先級的環境下,多數線程仍然有機會運行,由於不少線程不可避免的會遭遇阻塞,例如等待輸入輸出。遇到這種情形,阻塞的線程掛起,其餘線程運行。

可是若是你但願多線程執行的順利的話,最好不要採用這種方法。一樣,有些類型的任務是佔CPU的。對於這些支配CPU類型的線程,有時你但願可以支配它們,以便使其餘線程能夠運行。

設置線程的優先級,用setPriority()方法,該方法也是Tread 的成員。它的一般形式爲:

  final void setPriority(int level)

這 裏 , level 指 定了對所調用的線程的新的優先權的設置。Level的值必須在MIN_PRIORITY到MAX_PRIORITY範圍內。一般,它們的值分別是1和10。要返回一個線程爲默認的優先級,指定NORM_PRIORITY,一般值爲5。這些優先級在Thread中都被定義爲final型變量。

你能夠經過調用Thread的getPriority()方法來得到當前的優先級設置。該方法以下:

final int getPriority( )

當涉及調度時,Java的執行能夠有本質上不一樣的行爲。Windows 95/98/NT/2000 的工做或多或少如你所願。但其餘版本可能工做的徹底不一樣。大多數矛盾發生在你使用有優先級行爲的線程,而不是協同的騰出CPU時間。最安全的辦法是得到可預先性的優先權,Java得到跨平臺的線程行爲的方法是自動放棄對CPU的控制。

下面的例子闡述了兩個不一樣優先級的線程,運行於具備優先權的平臺,這與運行於無優先級的平臺不一樣。一個線程經過Thread.NORM_PRIORITY設置了高於普通優先級兩級的級數,另外一線程設置的優先級則低於普通級兩級。兩線程被啓動並容許運行10秒。每一個線程執行一個循環,記錄反覆的次數。10秒後,主線程終止了兩線程。每一個線程通過循環的次數被顯示。

// Demonstrate thread priorities.
class clicker implements Runnable {
     int click = 0;
    Thread t;
    private volatile boolean running = true;
    public clicker(int p) {
        t = new Thread(this);
        t.setPriority(p);            //設置線程優先級
    }

    public void run() {
        while (running) {
            click++;
        }
    }

    public void stop() {
        running = false;
    }

    public void start() {
        t.start();
    }
}

class HiLoPri {
    public static void main(String args[]) {
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);    //主線程優先級
        clicker hi = new clicker(Thread.NORM_PRIORITY + 2);
        clicker lo = new clicker(Thread.NORM_PRIORITY - 2);
        lo.start();
        hi.start();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        lo.stop();
        hi.stop();
        // Wait for child threads to terminate.
        try {
            hi.t.join();
            lo.t.join();
        } catch (InterruptedException e) {
            System.out.println("InterruptedException caught");
        }

        System.out.println("Low-priority thread: " + lo.click);
        System.out.println("High-priority thread: " + hi.click);
    }
}

該程序在Windows 98下運行的輸出,代表線程確實上下轉換,甚至既不屈從於CPU,也不被輸入輸出阻塞。優先級高的線程得到大約90%的CPU時間。
Low-priority thread: 4408112
High-priority thread: 589626904

固然,該程序的精確的輸出結果依賴於你的CPU的速度和運行的其餘任務的數量。當一樣的程序運行於無優先級的系統,將會有不一樣的結果。

上述程序還有個值得注意的地方。注意running前的關鍵字volatile。儘管volatile 在下章會被很仔細的討論,用在此處以確保running的值在下面的循環中每次都獲得驗證。

while (running) {
click++;
}

若是不用volatile,Java能夠自由的優化循環:running的值被存在CPU的一個寄存器中,
每次重複不必定須要複檢。volatile的運用阻止了該優化,告知Java running能夠改變,改變
方式並不以直接代碼形式顯示。

 

7.8  java線程同步                    

當兩個或兩個以上的線程須要共享資源,它們須要某種方法來肯定資源在某一刻僅被一個線程佔用。達到此目的的過程叫作同步(synchronization)。像你所看到的,Java爲此提供了獨特的,語言水平上的支持。

同步的關鍵是管程(也叫信號量semaphore)的概念。管程是一個互斥獨佔鎖定的對象,或稱互斥體(mutex)。在給定的時間,僅有一個線程能夠得到管程。當一個線程須要鎖定,它必須進入管程。全部其餘的試圖進入已經鎖定的管程的線程必須掛起直到第一個線程退出管程。這些其餘的線程被稱爲等待管程。一個擁有管程的線程若是願意的話能夠再次進入相同的管程。

若是你用其餘語言例如C或C++時用到過同步,你會知道它用起來有一點詭異。這是由於不少語言它們本身不支持同步。相反,對同步線程,程序必須利用操做系統源語。幸運的是Java經過語言元素實現同步,大多數的與同步相關的複雜性都被消除。

你能夠用兩種方法同步化代碼。二者都包括synchronized關鍵字的運用,下面分別說明這兩種方法。

使用同步方法

Java中同步是簡單的,由於全部對象都有它們與之對應的隱式管程。進入某一對象的管程,就是調用被synchronized關鍵字修飾的方法。當一個線程在一個同步方法內部,全部試圖調用該方法(或其餘同步方法)的同實例的其餘線程必須等待。爲了退出管程,並放棄對對象的控制權給其餘等待的線程,擁有管程的線程僅需從同步方法中返回。

爲理解同步的必要性,讓咱們從一個應該使用同步卻沒有用的簡單例子開始。下面的程序有三個簡單類。首先是Callme,它有一個簡單的方法call( )。call( )方法有一個名爲msg的String參數。該方法試圖在方括號內打印msg 字符串。有趣的事是在調用call( ) 打印左括號和msg字符串後,調用Thread.sleep(1000),該方法使當前線程暫停1秒。

下一個類的構造函數Caller,引用了Callme的一個實例以及一個String,它們被分別存在target 和 msg 中。構造函數也建立了一個調用該對象的run( )方法的新線程。該線程當即啓動。Caller類的run( )方法經過參數msg字符串調用Callme實例target的call( ) 方法。最後,Synch類由建立Callme的一個簡單實例和Caller的三個具備不一樣消息字符串的實例開始。

Callme的同一實例傳給每一個Caller實例。

// This program is not synchronized.
class Callme {
    void call(String msg) {
        System.out.print("[" + msg);
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            System.out.println("Interrupted");
       }
       System.out.println("]");
    }
}

class Caller implements Runnable {
    String msg;
    Callme target;
    Thread t;
    public Caller(Callme targ, String s) {
        target = targ;
        msg = s;
        t = new Thread(this);
        t.start();
    }
    public void run() {
        target.call(msg);
    }
}

class Synch {
    public static void main(String args[]) {
        Callme target = new Callme();
        Caller ob1 = new Caller(target, "Hello");
        Caller ob2 = new Caller(target, "Synchronized");
        Caller ob3 = new Caller(target, "World");
        // wait for threads to end
        try {
          ob1.t.join();
          ob2.t.join();
          ob3.t.join();
       } catch(InterruptedException e) {
          System.out.println("Interrupted");
       }
    }
}

該程序的輸出以下:
Hello[Synchronized[World]
]
]

在本例中,經過調用sleep( ),call( )方法容許執行轉換到另外一個線程。該結果是三個消息字符串的混合輸出。該程序中,沒有阻止三個線程同時調用同一對象的同一方法的方法存在。這是一種競爭,由於三個線程爭着完成方法。例題用sleep( )使該影響重複和明顯。在大多數狀況,競爭是更爲複雜和不可預知的,由於你不能肯定什麼時候上下文轉換會發生。這使程序時而運行正常時而出錯。

爲達到上例所想達到的目的,必須有權連續的使用call( )。也就是說,在某一時刻,必須限制只有一個線程能夠支配它。爲此,你只需在call( ) 定義前加上關鍵字synchronized,以下:

class Callme {
    synchronized void call(String msg) {
        ...

這防止了在一個線程使用call( )時其餘線程進入call( )。在synchronized加到call( )前面之後,程序輸出以下:
[Hello]
[Synchronized]
[World]

任什麼時候候在多線程狀況下,你有一個方法或多個方法操縱對象的內部狀態,都必須用synchronized 關鍵字來防止狀態出現競爭。記住,一旦線程進入實例的同步方法,沒有其餘線程能夠進入相同實例的同步方法。然而,該實例的其餘不一樣步方法卻仍然能夠被調用。

同步語句

儘管在建立的類的內部建立同步方法是得到同步的簡單和有效的方法,但它並不是在任什麼時候候都有效。這其中的緣由,請跟着思考。假設你想得到不爲多線程訪問設計的類對象的同步訪問,也就是,該類沒有用到synchronized方法。並且,該類不是你本身,而是第三方建立的,你不能得到它的源代碼。這樣,你不能在相關方法前加synchronized修飾符。怎樣才能使該類的一個對象同步化呢?很幸運,解決方法很簡單:你只需將對這個類定義的方法的調用放入一個synchronized塊內就能夠了

下面是synchronized語句的普通形式:

synchronized(object) {
    // statements to be synchronized
}

其中,object是被同步對象的引用。若是你想要同步的只是一個語句,那麼不須要花括號。一個同步塊確保對object成員方法的調用僅在當前線程成功進入object管程後發生。

下面是前面程序的修改版本,在run( )方法內用了同步塊:

// This program uses a synchronized block.
class Callme {
    void call(String msg) {
        System.out.print("[" + msg);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("Interrupted");
        }
        System.out.println("]");
    }
}

class Caller implements Runnable {
    String msg;
    Callme target;
    Thread t;
    public Caller(Callme targ, String s) {
        target = targ;
        msg = s;
        t = new Thread(this);
        t.start();
    }

    // synchronize calls to call()
    public void run() {
        synchronized(target) { // synchronized block    
            target.call(msg);
        }
    }
}

class Synch1 {
    public static void main(String args[]) {
        Callme target = new Callme();
        Caller ob1 = new Caller(target, "Hello");
        Caller ob2 = new Caller(target, "Synchronized");
        Caller ob3 = new Caller(target, "World");

        // wait for threads to end
        try {
            ob1.t.join();
            ob2.t.join();
            ob3.t.join();
        } catch(InterruptedException e) {
            System.out.println("Interrupted");
        }
    }
}

這裏,call( )方法沒有被synchronized修飾。而synchronized是在Caller類的run( )方法中聲明的。這能夠獲得上例中一樣正確的結果,由於每一個線程運行前都等待先前的一個線程結束。

 

7.9  java線程間通訊

上述例題無條件的阻塞了其餘線程異步訪問某個方法。Java對象中隱式管程的應用是很強大的,可是你能夠經過進程間通訊達到更微妙的境界。這在Java中是尤其簡單的。

像前面所討論過的,多線程經過把任務分紅離散的和合乎邏輯的單元代替了事件循環程序。線程還有第二優勢:它遠離了輪詢。輪詢一般由重複監測條件的循環實現。一旦條件成立,就要採起適當的行動。這浪費了CPU時間。舉例來講,考慮經典的序列問題,當一個線程正在產生數據而另外一個程序正在消費它。爲使問題變得更有趣,假設數據產生器必須等待消費者完成工做才能產生新的數據。在輪詢系統,消費者在等待生產者產生數據時會浪費不少CPU週期。一旦生產者完成工做,它將啓動輪詢,浪費更多的CPU時間等待消費者的工做結束,如此下去。很明顯,這種情形不受歡迎。

爲避免輪詢,Java包含了經過wait( ),notify( )和notifyAll( )方法實現的一個進程間通訊機制。這些方法在對象中是用final方法實現的,因此全部的類都含有它們。這三個方法僅在synchronized方法中才能被調用。儘管這些方法從計算機科學遠景方向上來講具備概念的高度先進性,實際中用起來是很簡單的:

  • wait( ) 告知被調用的線程放棄管程進入睡眠直到其餘線程進入相同管程而且調用notify( )
  • notify( ) 恢復相同對象中第一個調用 wait( ) 的線程。
  • notifyAll( ) 恢復相同對象中全部調用 wait( ) 的線程。具備最高優先級的線程最早運行。


這些方法在Object中被聲明,以下所示:

    final void wait( ) throws InterruptedException
    final void notify( )
    final void notifyAll( )

wait( )存在的另外的形式容許你定義等待時間。

下面的例子程序錯誤的實行了一個簡單生產者/消費者的問題。它由四個類組成:Q,設法得到同步的序列;Producer,產生排隊的線程對象;Consumer,消費序列的線程對象;以及PC,建立單個Q,Producer,和Consumer的小類。

// An incorrect implementation of a producer and consumer.
class Q {
    int n;
    synchronized int get() {
        System.out.println("Got: " + n);
        return n;
    }
    synchronized void put(int n) {
        this.n = n;
        System.out.println("Put: " + n);
    }
}
class Producer implements Runnable {
    Q q;
    Producer(Q q) {
        this.q = q;
        new Thread(this, "Producer").start();
    }
    public void run() {
        int i = 0;
        while(true) {
            q.put(i++);
        }
    }
}
class Consumer implements Runnable {
    Q q;
    Consumer(Q q) {
        this.q = q;
        new Thread(this, "Consumer").start();
    }
    public void run() {
        while(true) {
           q.get();
        }
    }
}
class PC {
    public static void main(String args[]) {
        Q q = new Q();
        new Producer(q);
        new Consumer(q);
        System.out.println("Press Control-C to stop.");
    }
}

儘管Q類中的put( )和get( )方法是同步的,沒有東西阻止生產者超越消費者,也沒有東西阻止消費者消費一樣的序列兩次。這樣,你就獲得下面的錯誤輸出(輸出將隨處理器速度和裝載的任務而改變):
Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7
生產者生成1後,消費者依次得到一樣的1五次。生產者在繼續生成2到7,消費者沒有機會得到它們。

用Java正確的編寫該程序是用wait( )和notify( )來對兩個方向進行標誌,以下所示:

// A correct implementation of a producer and consumer.
class Q {
    int n;
    boolean valueSet = false;
       synchronized int get() {
           if(!valueSet)
            try {
                wait();    //
            } catch(InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
            System.out.println("Got: " + n);
            valueSet = false;
            notify();     //
            return n;
        }
        synchronized void put(int n) {
            if(valueSet)
            try {
                wait();  //
            } catch(InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
            this.n = n;
            valueSet = true;
            System.out.println("Put: " + n);
            notify();     //
        }
    }
    class Producer implements Runnable {
        Q q;
        Producer(Q q) {
        this.q = q;
        new Thread(this, "Producer").start();
    }
    public void run() {
        int i = 0;
        while(true) {
            q.put(i++);
        }
    }
}
class Consumer implements Runnable {
    Q q;
    Consumer(Q q) {
        this.q = q;
        new Thread(this, "Consumer").start();
    }
    public void run() {
        while(true) {
            q.get();
        }
    }
}
class PCFixed {
    public static void main(String args[]) {
        Q q = new Q();
        new Producer(q);
        new Consumer(q);
        System.out.println("Press Control-C to stop.");
    }
}//wait()後本線程釋放CPU,等待任何一個notify到來; 典

內部get( ), wait( )被調用。這使執行掛起直到Producer 告知數據已經預備好。這時,內部get( ) 被恢復執行。獲取數據後,get( )調用notify( )。這告訴Producer能夠向序列中輸入更多數據。在put( )內,wait( )掛起執行直到Consumer取走了序列中的項目。當執行再繼續,下一個數據項目被放入序列,notify( )被調用,這通知Consumer它應該移走該數據。

下面是該程序的輸出,它清楚的顯示了同步行爲:
Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5

 

7.10  java線程死鎖                                

須要避免的與多任務處理有關的特殊錯誤類型是死鎖(deadlock)。死鎖發生在當兩個線程對一對同步對象有循環依賴關係時。例如,假定一個線程進入了對象X的管程而另外一個線程進入了對象Y的管程。若是X的線程試圖調用Y的同步方法,它將像預料的同樣被鎖定。而Y的線程一樣但願調用X的一些同步方法,線程永遠等待,由於爲到達X,必須釋放本身的Y的鎖定以使第一個線程能夠完成。死鎖是很難調試的錯誤,由於:

  • 一般,它極少發生,只有到兩線程的時間段恰好符合時才能發生。
  • 它可能包含多於兩個的線程和同步對象(也就是說,死鎖在比剛講述的例子有更多複雜的事件序列的時候能夠發生)。


爲充分理解死鎖,觀察它的行爲是頗有用的。下面的例子生成了兩個類,A和B,分別有foo( )和bar( )方法。這兩種方法在調用其餘類的方法前有一個短暫的停頓。主類,名爲Deadlock,建立了A和B的實例,而後啓動第二個線程去設置死鎖環境。foo( )和bar( )方法使用sleep( )強迫死鎖現象發生

// An example of deadlock.
class A {
    synchronized void foo(B b) {
        String name = Thread.currentThread().getName();
        System.out.println(name + " entered A.foo");
        try {
            Thread.sleep(1000);
        } catch(Exception e) {
            System.out.println("A Interrupted");
        }
        System.out.println(name + " trying to call B.last()");
        b.last();
    }
    synchronized void last() {
        System.out.println("Inside A.last");
    }
}
class B {
    synchronized void bar(A a) {
        String name = Thread.currentThread().getName();
        System.out.println(name + " entered B.bar");
        try {
            Thread.sleep(1000);
        } catch(Exception e) {
            System.out.println("B Interrupted");
        }
        System.out.println(name + " trying to call A.last()");
        a.last();
    }
    synchronized void last() {
        System.out.println("Inside A.last");
    }
}
class Deadlock implements Runnable {
    A a = new A();
    B b = new B();
    Deadlock() {
        Thread.currentThread().setName("MainThread");
        Thread t = new Thread(this, "RacingThread");
        t.start();
        a.foo(b); // get lock on a in this thread.
        System.out.println("Back in main thread");
    }
    public void run() {
        b.bar(a); // get lock on b in other thread.
        System.out.println("Back in other thread");
    }
    public static void main(String args[]) {
        new Deadlock();
    }
}

 


運行程序後,輸出以下:
MainThread entered A.foo
RacingThread entered B.bar
MainThread trying to call B.last()
RacingThread trying to call A.last()

由於程序死鎖,你須要按CTRL-C來結束程序。在PC機上按CTRL-BREAK(或在Solaris下按CTRL-\)你能夠看到全線程和管程緩衝堆。你會看到RacingThread在等待管程a時佔用管程b,同時,MainThread佔用a等待b。該程序永遠都不會結束。像該例闡明的,你的多線程程序常常被鎖定,死鎖是你首先應檢查的問題。

 

7.11  java線程的掛起、恢復和終止                            

有時,線程的掛起是頗有用的。例如,一個獨立的線程能夠用來顯示當日的時間。若是用戶不但願用時鐘,線程被掛起。在任何情形下,掛起線程是很簡單的,一旦掛起,從新啓動線程也是一件簡單的事。

掛起,終止和恢復線程機制在Java 2和早期版本中有所不一樣。儘管你運用Java 2的途徑編寫代碼,你仍需瞭解這些操做在早期Java環境下是如何完成的。例如,你也許須要更新或維護老的代碼。你也須要了解爲何Java 2會有這樣的變化。由於這些緣由,下面內容描述了執行線程控制的原始方法,接着是Java 2的方法。

Java 1.1或更早版本的線程的掛起、恢復和終止

先於Java2的版本,程序用Thread 定義的suspend() 和 resume() 來暫停和再啓動線程。它們的形式以下:

    final void suspend( )      //暫定
    final void resume( )      //啓動

下面的程序描述了這些方法:

// Using suspend() and resume().
class NewThread implements Runnable {
    String name; // name of thread
    Thread t;
    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        t.start(); // Start the thread
    }
    // This is the entry point for thread.
    public void run() {
        try {
            for(int i = 15; i > 0; i--) {
                System.out.println(name + ": " + i);
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            System.out.println(name + " interrupted.");
        }
        System.out.println(name + " exiting.");
    }
}
class SuspendResume {
    public static void main(String args[]) {
        NewThread ob1 = new NewThread("One");
        NewThread ob2 = new NewThread("Two");
        try {
            Thread.sleep(1000);
            ob1.t.suspend();      //掛起
            System.out.println("Suspending thread One");
            Thread.sleep(1000);
            ob1.t.resume();         //啓動線程
            System.out.println("Resuming thread One");
            ob2.t.suspend();
            System.out.println("Suspending thread Two");
            Thread.sleep(1000);
            ob2.t.resume();
            System.out.println("Resuming thread Two");
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        // wait for threads to finish
        try {
            System.out.println("Waiting for threads to finish.");
            ob1.t.join();
            ob2.t.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        System.out.println("Main thread exiting.");
    }
}

 


程序的部分輸出以下:
New thread: Thread[One,5,main]
One: 15
New thread: Thread[Two,5,main]
Two: 15
One: 14
Two: 14
One: 13
Two: 13
One: 12
Two: 12
One: 11
Two: 11
Suspending thread One
Two: 10
Two: 9
Two: 8
Two: 7
Two: 6
Resuming thread One
Suspending thread Two
One: 10
One: 9
One: 8
One: 7
One: 6
Resuming thread Two
Waiting for threads to finish.
Two: 5
One: 5
Two: 4
One: 4
Two: 3
One: 3
Two: 2
One: 2
Two: 1
One: 1
Two exiting.
One exiting.
Main thread exiting.

Thread類一樣定義了stop() 來終止線程。它的形式以下:
    void stop( )
一旦線程被終止,它不能被resume() 恢復繼續運行。

Java 2中掛起、恢復和終止線程

Thread定義的suspend(),resume()和stop()方法看起來是管理線程的完美的和方便的方法,它們不能用於新Java版本的程序。下面是其中的緣由。Thread類的suspend()方法在Java2中不被同意,由於suspend()有時會形成嚴重的系統故障。假定對關鍵的數據結構的一個線程被鎖定的狀況,若是該線程在那裏掛起,這些鎖定的線程並無放棄對資源的控制。其餘的等待這些資源的線程可能死鎖。

Resume()方法一樣不被贊同。它不引發問題,但不能離開suspend()方法而獨立使用。Thread類的stop()方法一樣在Java 2中受到反對。這是由於該方法可能致使嚴重的系統故障。設想一個線程正在寫一個精密的重要的數據結構且僅完成一個零頭。若是該線程在此刻終止,則數據結構可能會停留在崩潰狀態。

由於在Java 2中不能使用suspend(),resume()和stop() 方法來控制線程,你也許會想那就沒有辦法來中止,恢復和結束線程。其實否則。相反,線程必須被設計以使run() 方法按期檢查以來斷定線程是否應該被掛起,恢復或終止它本身的執行。有表明性的,這由創建一個指示線程狀態的標誌變量來完成。只要該標誌設爲「running」,run()方法必須繼續讓線程執行。若是標誌爲「suspend」,線程必須暫停。若設爲「stop」,線程必須終止。

固然,編寫這樣的代碼有不少方法,但中心主題對全部的程序應該是相同的。

下面的例題闡述了從Object繼承的wait()和notify()方法怎樣控制線程的執行。該例與前面講過的程序很像。然而,不被贊同的方法都沒有用到。讓咱們思考程序的執行。

NewTread 類包含了用來控制線程執行的布爾型的實例變量suspendFlag。它被構造函數初始化爲false。Run()方法包含一個監測suspendFlag 的同步聲明的塊。若是變量是true,wait()方法被調用以掛起線程。Mysuspend()方法設置suspendFlag爲true。Myresume()方法設置suspendFlag爲false而且調用notify()方法來喚起線程。最後,main()方法被修改以調用mysuspend()和myresume()方法。

// Suspending and resuming a thread for Java2
class NewThread implements Runnable {
    String name; // name of thread
    Thread t;
    boolean suspendFlag;
    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        suspendFlag = false;
        t.start(); // Start the thread
    }
    // This is the entry point for thread.
    public void run() {
        try {
            for(int i = 15; i > 0; i--) {
                System.out.println(name + ": " + i);
                Thread.sleep(200);
                synchronized(this) {
                    while(suspendFlag) {            //☆☆ 自定義函數
                        wait();                //說白了,系統自帶的 suspend、resume都沒wait穩定
                    }
                }
            }
        } catch (InterruptedException e) {
            System.out.println(name + " interrupted.");
        }
        System.out.println(name + " exiting.");
    }
    void mysuspend() {              //☆☆ 自定義函數
        suspendFlag = true;
    }
    synchronized void myresume() {
        suspendFlag = false;
        notify();
    }
}
class SuspendResume {
    public static void main(String args[]) {
       NewThread ob1 = new NewThread("One");
       NewThread ob2 = new NewThread("Two");
       try {
          Thread.sleep(1000);
          ob1.mysuspend();
          System.out.println("Suspending thread One");
          Thread.sleep(1000);
          ob1.myresume();      //
          System.out.println("Resuming thread One");
          ob2.mysuspend();
          System.out.println("Suspending thread Two");
          Thread.sleep(1000);
          ob2.myresume();
          System.out.println("Resuming thread Two");
       } catch (InterruptedException e) {
          System.out.println("Main thread Interrupted");
       }
       // wait for threads to finish
       try {
          System.out.println("Waiting for threads to finish.");
          ob1.t.join();
          ob2.t.join();
       } catch (InterruptedException e) {
           System.out.println("Main thread Interrupted");
       }
       System.out.println("Main thread exiting.");
    }
}

該程序的輸出與前面的程序相同。此書的後面部分,你將看到用Java 2機制控制線程的更多例子。儘管這種機制不像老方法那樣「乾淨」,然而,它是確保運行時不發生錯誤的方法。它是全部新的代碼必須採用的方法。

 

八◐   輸入輸出(IO)操做                      

8.1   java輸入輸出(IO)和流的基本概念                         

輸入輸出(I/O)是指程序與外部設備或其餘計算機進行交互的操做。幾乎全部的程序都具備輸入與輸出操做,如從鍵盤上讀取數據,從本地或網絡上的文件讀取數據或寫入數據等。經過輸入和輸出操做能夠從外界接收信息,或者是把信息傳遞給外界。Java把這些輸入與輸出操做用流來實現,經過統一的接口來表示,從而使程序設計更爲簡單。

Java流的概念

流(Stream)是指在計算機的輸入輸出操做中各部件之間的數據流動。按照數據的傳輸方向,流可分爲輸入流與輸出流。Java語言裏的流序列中的數據既能夠是未經加工的原始二進制數據,也能夠是通過必定編碼處理後符合某種特定格式的數據。

1.輸入輸出流
在Java中,把不一樣類型的輸入輸出源抽象爲流,其中輸入和輸出的數據稱爲數據流(Data Stream)。數據流是Java程序發送和接收數據的一個通道,數據流中包括輸入流(Input Stream)和輸出流(Output Stream)。一般應用程序中使用輸入流讀出數據,輸出流寫入數據。 流式輸入、輸出的特色是數據的獲取和發送均沿數據序列順序進行。相對於程序來講,輸出流是往存儲介質或數據通道寫入數據,而輸入流是從存儲介質或數據通道中讀取數據,通常來講關於流的特性有下面幾點:

  • 先進先出,最早寫入輸出流的數據最早被輸入流讀取到。
  • 順序存取,能夠一個接一個地往流中寫入一串字節,讀出時也將按寫入順序讀取一串字節,不能隨機訪問中間的數據。
  • 只讀或只寫,每一個流只能是輸入流或輸出流的一種,不能同時具有兩個功能,在一個數據傳輸通道中,若是既要寫入數據,又要讀取數據,則要分別提供兩個流。


2.緩衝流
爲了提升數據的傳輸效率,引入了緩衝流(Buffered Stream)的概念,即爲一個流配備一個緩衝區(Buffer),一個緩衝區就是專門用於傳送數據的一塊內存。

當向一個緩衝流寫入數據時,系統將數據發送到緩衝區,而不是直接發送到外部設備。緩衝區自動記錄數據,當緩衝區滿時,系統將數據所有發送到相應的外部設備。當從一個緩衝流中讀取數據時,系統實際是從緩衝區中讀取數據,當緩衝區爲空時,系統就會從相關外部設備自動讀取數據,並讀取儘量多的數據填滿緩衝區。 使用數據流來處理輸入輸出的目的是使程序的輸入輸出操做獨立於相關設備,因爲程序不需關注具體設備實現的細節(具體細節由系統處理),因此對於各類輸入輸出設備,只要針對流作處理便可,不需修改源程序,從而加強了程序的可移植性。

I/O流類概述

爲了方便流的處理,Java語言提供了java.io包,在該包中的每個類都表明了一種特定的輸入或輸出流。爲了使用這些流類,編程時須要引入這個包。 Java提供了兩種類型的輸入輸出流:一種是面向字節的流,數據的處理以字節爲基本單位;另外一種是面向字符的流,用於字符數據的處理。字節流(Byte Stream)每次讀寫8位二進制數,也稱爲二進制字節流或位流。字符流一次讀寫16位二進制數,並將其作一個字符而不是二進制位來處理。須要注意的是,爲知足字符的國際化表示,Java語言的字符編碼採用的是16位的Unicode碼,而普通文本文件中採用的是8位ASCⅡ碼。

java.io中類的層次結構如圖10-1所示。

圖10-1 java.io包的頂級層次結構圖
圖10-1 java.io包的頂級層次結構圖


針對一些頻繁的設備交互,Java語言系統預約了3個能夠直接使用的流對象,分別是:

  • System.in(標準輸入),一般表明鍵盤輸入。
  • System.out(標準輸出):一般寫往顯示器。
  • System.err(標準錯誤輸出):一般寫往顯示器

在Java語言中使用字節流和字符流的步驟基本相同,以輸入流爲例,首先建立一個與數據源相關的流對象,而後利用流對象的方法從流輸入數據,最後執行close()方法關閉流。

 

8.2  java中面向字符的輸入流                            

字符流是針對字符數據的特色進行過優化的,於是提供一些面向字符的有用特性,字符流的源或目標一般是文本文件。 Reader和Writer是java.io包中全部字符流的父類。因爲它們都是抽象類,因此應使用它們的子類來建立實體對象,利用對象來處理相關的讀寫操做。Reader和Writer的子類又能夠分爲兩大類:一類用來從數據源讀入數據或往目的地寫出數據(稱爲節點流),另外一類對數據執行某種處理(稱爲處理流)。

面向字符的輸入流類都是Reader的子類,其類層次結構如圖10-2所示。

圖10-2 Reader的類層次結構圖
圖10-2 Reader的類層次結構圖


表 10-1 列出了 Reader 的主要子類及說明。

表 10-1 Reader 的主要子類
類名 功能描述
CharArrayReader 從字符數組讀取的輸入流
BufferedReader 緩衝輸入字符流
PipedReader 輸入管道
InputStreamReader 將字節轉換到字符的輸入流
FilterReader 過濾輸入流
StringReader 從字符串讀取的輸入流
LineNumberReader 爲輸入數據附加行號
PushbackReader 返回一個字符並把此字節放回輸入流
FileReader 從文件讀取的輸入流


Reader 所提供的方法如表 10-2 所示,能夠利用這些方法來得到流內的位數據。

表 10-2 Reader 的經常使用方法
方法 功能描述
void close() 關閉輸入流
void mark() 標記輸入流的當前位置
boolean markSupported() 測試輸入流是否支持 mark
int read() 從輸入流中讀取一個字符
int read(char[] ch) 從輸入流中讀取字符數組
int read(char[] ch, int off, int len) 從輸入流中讀 len 長的字符到 ch 內
boolean ready() 測試流是否能夠讀取
void reset() 重定位輸入流
long skip(long n) 跳過流內的 n 個字符

使用 FileReader 類讀取文件

FileReader 類是 Reader 子類 InputStreamReader 類的子類,所以 FileReader 類既能夠使用Reader 類的方法也能夠使用 InputStreamReader 類的方法來建立對象。

在使用 FileReader 類讀取文件時,必須先調用 FileReader()構造方法建立 FileReader 類的對象,再調用 read()方法。FileReader 構造方法的格式爲:

    public FileReader(String name);  //根據文件名建立一個可讀取的輸入流對象

【例 10-1】利用 FileReader 類讀取純文本文件的內容(查看源代碼)。

運行結果如圖 10-3 所示:


圖 10-3  例 10_1 運行結果(輸出內容爲文件ep10_1.txt的內容)


須要注意的是,Java 把一個漢字或英文字母做爲一個字符對待,回車或換行做爲兩個字符對待。

使用 BufferedReader 類讀取文件

BufferedReader 類是用來讀取緩衝區中的數據。使用時必須建立 FileReader 類對象,再以該對象爲參數建立 BufferedReader 類的對象。BufferedReader 類有兩個構造方法,其格式爲:

    public BufferedReader(Reader in);  //建立緩衝區字符輸入流
    public BufferedReader(Reader in,int size);  //建立輸入流並設置緩衝區大小

【例 10-2】利用 BufferedReader 類讀取純文本文件的內容(查看源代碼)。

運行結果如圖 10-4 所示:


圖 10-4  例 10_2 運行結果


須要注意的是,執行 read()或 write()方法時,可能因爲 IO 錯誤,系統拋出 IOException 異常,須要將執行讀寫操做的語句包括在 try 塊中,並經過相應的 catch 塊來處理可能產生的異常。

 

8.3  Java面向字符的輸出流                            

面向字符的輸出流都是類 Writer 的子類,其類層次結構如圖 10-5 所示。

圖10-5 Writer的類層次結構圖
圖10-5 Writer的類層次結構圖


表 10-3 列出了 Writer 的主要子類及說明。

表 10-3 Writer 的主要子類
類名 功能說明
CharArrayWriter 寫到字符數組的輸出流
BufferedWriter 緩衝輸出字符流
PipedWriter 輸出管道
OutputStreamWriter 轉換字符到字節的輸出流
FilterWriter 過濾輸出流
StringWriter 輸出到字符串的輸出流
PrintWriter 包含 print()和 println()的輸出流
FileWriter 輸出到文件的輸出流


Writer 所提供的方法如表 10-4 所示。

表 10-4 Writer 的經常使用方法
方法 功能描述
void close() 關閉輸出流
void flush() 將緩衝區中的數據寫到文件中
void writer(int c) 將單一字符 c 輸出到流中
void writer(String str) 將字符串 str 輸出到流中
void writer(char[] ch) 將字符數組 ch 輸出到流
void writer(char[] ch, int offset, int length) 將一個數組內自 offset 起到 length 長的字符輸出到流

使用 FileWriter 類寫入文件

FileWriter 類是 Writer 子類 OutputStreamWriter 類的子類,所以 FileWriter 類既能夠使用 Writer類的方法也能夠使用 OutputStreamWriter 類的方法來建立對象。

在使用 FileWriter 類寫入文件時,必須先調用 FileWriter()構造方法建立 FileWriter 類的對象,再調用 writer()方法。FileWriter 構造方法的格式爲:

    public FileWriter(String name);  //根據文件名建立一個可寫入的輸出流對象
    public FileWriter(String name,Boolean a);  //a 爲真,數據將追加在文件後面

【例 10-3】利用 FileWriter 類將 ASCⅡ字符寫入到文件中(查看源代碼)。

運行後程序後,打開 ep10_3.txt 文件,顯示內容爲:
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}

使用 BufferedWriter 類寫入文件

BufferedWriter 類是用來將數據寫入到緩衝區。使用時必須建立 FileWriter 類對象,再以該對象爲參數建立 BufferedWriter 類的對象,最後須要用 flush()方法將緩衝區清空。BufferedWriter類有兩個構造方法,其格式爲:

    public BufferedWriter(Writer out);  //建立緩衝區字符輸出流
    public BufferedWriter(Writer out,int size);  //建立輸出流並設置緩衝區大小

【例 10-4】利用 BufferedWriter 類進行文件複製(查看源代碼)。

須要注意的是,調用 out 對象的 write()方法寫入數據時,不會寫入回車,所以須要使用newLine()方法在每行數據後加入回車,以保證目標文件與源文件相一致。

 

8.4  java中面向字節的輸入輸出流                      

字節流以字節爲傳輸單位,用來讀寫8位的數據,除了可以處理純文本文件以外,還能用來處理二進制文件的數據。InputStream類和OutputStream類是全部字節流的父類。

InputStream類

面向字節的輸入流都是InputStream類的子類,其類層次結構如圖10-6所示。


圖10-6 InputStream的類層次結構圖


表 10-5 列出了 InputStream 的主要子類及說明。

表 10-5 InputStream 的主要子類
類名 功能描述
FileInputStream 從文件中讀取的輸入流
PipedInputStream 輸入管道
FilterInputStream 過濾輸入流
ByteArrayInputStream 從字節數組讀取的輸入流
SequenceInputStream 兩個或多個輸入流的聯合輸入流,按順序讀取
ObjectInputStream 對象的輸入流
LineNumberInputStream 爲文本文件輸入流附加行號
DataInputStream 包含讀取 Java 標準數據類型方法的輸入流
BufferedInputStream 緩衝輸入流
PushbackInputStream 返回一個字節並把此字節放回輸入流


InputStream 流類中包含一套全部輸入都須要的方法,能夠完成最基本的從輸入流讀入數據的功能。表 10-6 列出了其中經常使用的方法及說明。

表 10-6 InputStream 的經常使用方法
方法 功能描述
void close() 關閉輸入流
void mark() 標記輸入流的當前位置
void reset() 將讀取位置返回到標記處
int read() 從輸入流中當前位置讀入一個字節的二進制數據,以此數據爲低位字節,補足16位的整型量(0~255)後返回,若輸入流中當前位置沒有數據,則返回-1
int read(byte b[]) 從輸入流中的當前位置連續讀入多個字節保存在數組中,並返回所讀取的字節數
int read(byte b[], int off, int len) 從輸入流中當前位置連續讀len長的字節,從數組第off+1個元素位置處開始存放,並返回所讀取的字節數
int available() 返回輸入流中能夠讀取的字節數
long skip(long n) 略過n個字節
long skip(long n) 跳過流內的n個字符
boolean markSupported() 測試輸入數據流是否支持標記

OutputStream類

面向字節的輸出流都是OutputStream類的子類,其類層次結構如圖10-7所示。

圖10-7 OutputStream的類層次結構圖
圖10-7 OutputStream的類層次結構圖


10-7列出了OutputStream的主要子類及說明。 

表10-7 OutputStream的主要子類
類名 功能描述
FileOutputStream 寫入文件的輸出流
PipedOutputStream 輸出管道
FilterOutputStream 過濾輸出流
ByteArrayOutputStream 寫入字節數組的輸出流
ObjectOutputStream 對象的輸出流
DataOutputStream 包含寫Java標準數據類型方法的輸出流
BufferedOutputStream 緩衝輸出流
PrintStream 包含print()和println()的輸出流


OutputStream流類中包含一套全部輸出都須要的方法,能夠完成最基本的向輸出流寫入數據的功能。表10-8列出了其中經常使用的方法及說明。 

表10-8 OutputStream的經常使用方法
方法 功能描述
void close() 關閉輸出流
void flush() 強制清空緩衝區並執行向外設輸出數據
void write(int b) 將參數b的低位字節寫入到輸出流
void write(byte b[]) 按順序將數組b[]中的所有字節寫入到輸出流
void write(byte b[], int off, int len) 按順序將數組b[]中第off+1個元素開始的len個數據寫入到輸出流


因爲InputStream和OutputStream都是抽象類,因此在程序中建立的輸入流對象通常是它們某個子類的對象,經過調用對象繼承的read()和write()方法就可實現對相應外設的輸入輸出操做。

 

8.5  java面向字節流的應用

文件輸入輸出流

文件輸入輸出流 FileInputStream 和 FileOutputStream 負責完成對本地磁盤文件的順序輸入輸出操做。

【例 10-5】經過程序建立一個文件,從鍵盤輸入字符,當遇到字符「#」時結束,在屏幕上顯示該文件的全部內容(查看源代碼)。

運行後在程序目錄創建一個名稱爲 ep10_5 的文件,運行結果如圖 10-8 所示:

圖 10-8   例 10_5 運行結果
圖 10-8  例 10_5 運行結果


FileDescriptor 是 java.io 中的一個類,該類不能實例化,其中包含三個靜態成員:in、out 和err,分別對應於標準輸入流、標準輸出流和標準錯誤流,利用它們能夠在標準輸入輸出流上創建文件輸入輸出流,實現鍵盤輸入或屏幕輸出操做。

【例 10-6】實現對二進制圖形文件(.gif)的備份(查看源代碼)。

運行後在程序目錄備份了一個名稱爲 ep10_6_a.gif 的文件,運行結果如圖 10-9 所示:


圖 10-9  例 10_6 運行結果

過濾流

FilterInputStream 和 FileOutputStream 是 InputStream 和 OutputStream 的直接子類,分別實現了在數據的讀、寫操做的同時能對所傳輸的數據作指定類型或格式的轉換,便可實現對二進制字節數據的理解和編碼轉換。

經常使用的兩個過濾流是數據輸入流 DataInputStream 和數據輸出流 DataOutputStream。其構造方法爲:
    DataInputStream(InputStream in);  //建立新輸入流,從指定的輸入流 in 讀數據
    DataOutputStream(OutputStream out);  //建立新輸出流,向指定的輸出流 out 寫數據

因爲 DataInputStream 和 DataOutputStream 分別實現了 DataInput 和 DataOutput 兩個接口(這兩個接口規定了基本類型數據的輸入輸出方法)中定義的獨立於具體機器的帶格式的讀寫操做,從而實現了對不一樣類型數據的讀寫。由構造方法能夠看出,輸入輸出流分別做爲數據輸入輸出流的構造方法參數,即做爲過濾流必須與相應的數據流相連。

DataInputStream 和 DataOutputStream 類提供了不少個針對不一樣類型數據的讀寫方法,具體內容讀者可參看 Java 的幫助文檔。

【例 10-7】將三個 int 型數字 100,0,-100 寫入數據文件 ep10_6.dat 中(查看源代碼)。

運行後在程序目錄中生成數據文件 ep10_7.dat,用文本編輯器打開後發現內容爲二進制的:
00 00 00 64 00 00 00 00 FF FF FF 9C。

【例 10-8】讀取數據文件 ep10_6.dat 中的三個 int 型數字,求和並顯示(查看源代碼)。

運行結果:
三個數的和爲:0

readInt 方法能夠從輸入輸出流中讀入 4 個字節並將其做爲 int 型數據直接參與運算。因爲已經知道文件中有 3 個數據,因此能夠使用 3 個讀入語句,但若只知道文件中是 int 型數據而不知道數據的個數時該怎麼辦呢?由於 DataInputStream 的讀入操做如遇到文件結尾就會拋出 EOFException 異常,因此可將讀操做放入 try 中。
try{
    while(true)
    sum+=a.readInt();
}
catch(EOFException e){
    System.out.pritnln("三個數的和爲:"+sum);
    a.close();
}
EOFException 是 IOException 的子類,只有文件結束異常時纔會被捕捉到,但若是沒有讀到文件結尾,在讀取過程當中出現異常就屬於 IOException。

【例 10-9】從鍵盤輸入一個整數,求該數的各位數字之和(查看源代碼)。

運行結果:
請輸入一個整數:26
842403082 的各位數字之和=31

須要注意的是,輸入的數據 26 爲變成了 842403082,緣由在於輸入數據不符合基本類型數據的格式,從鍵盤提供的數據是字符的字節碼錶示方式,若輸入 26,只表明 2 和 6 兩個字符的字節數據,而不是表明整數 26 的字節碼。

若要從鍵盤獲得整數須要先讀取字符串,再利用其餘方法將字符串轉化爲整數。

標準輸入輸出

System.in、System.out、System.err 這 3 個標準輸入輸流對象定義在 java.lang.System 包中,這 3 個對象在 Java 源程序編譯時會被自動加載。

  1. 標準輸入:標準輸入 System.in 是 BufferedInputStream 類的對象,當程序須要從鍵盤上讀入數據時,只須要調用 System.in 的 read()方法便可,該方法從鍵盤緩衝區讀入一個字節的二進制數據,返回以此字節爲低位字節,高位字節爲 0 的整型數據。
  2. 標準輸出:標準輸出 System.out 是打印輸出流 PrintStream 類的對象。PrintStream 類是過濾輸出流類 FilterOutputStream 的一個子類,其中定義了向屏幕輸出不一樣類型數據的方法print()和 println()。
  3. 標準錯誤輸出:System.err 用於爲用戶顯示錯誤信息,也是由 PrintStream 類派生出來的錯誤流。Err 流的做用是使 print()和 println()將信息輸出到 err 流並顯示在屏幕上,以方便用戶使用和調試程序。


【例 10-10】輸入一串字符顯示出來,並顯示 System.in 和 System.out 所屬的類(查看源代碼)。

運行結果如圖 10-10 所示:

圖 10-10  例 10_10 運行結果
圖 10-10  例 10_10 運行結果


須要注意的是,輸入了 3 個字符按回車後,輸出的結果顯示爲 5 個字符。這是因爲 Java 中回車被看成兩個字符,一個是 ASCⅡ爲 13 的回車符,一個是值爲 10 的換行符。程序中 getClass()和 ToString()是 Object 類的方法,做用分別是返回當前對象所對應的類和返回當前對象的字符串表示。

 

8.6  java中文件與目錄管理                          

目錄是管理文件的特殊機制,同類文件保存在同一個目錄下不只能夠簡化文件管理,並且還能夠提升工做效率。Java 語言在 java.io 包中定義了一個 File 類專門用來管理磁盤文件和目錄。

每一個 File 類對象表示一個磁盤文件或目錄,其對象屬性中包含了文件或目錄的相關信息。經過調用 File 類提供的各類方法,可以建立、刪除、重名名文件、判斷文件的讀寫權限以及是否存在,設置和查詢文件的最近修改時間等。不一樣操做系統具備不一樣的文件系統組織方式,經過使用 File 類對象,Java 程序能夠用與平臺無關的、統一的方式來處理文件和目錄。

建立 File 類的對象

建立 File 類對象須要給出其所對應的文件名或目錄名,File 類的構造方法如表 10-9 所示。

表 10-9 File 類的構造方法
構造方法 功能描述
public File(String path) 指定與 File 對象關聯的文件或目錄名,path 能夠包含路徑及文件和目錄名
public File(String path, String name) 以 path 爲路徑,以 name 爲文件或目錄名建立 File 對象
public File(File dir, String name) 用現有的 File 對象 dir 做爲目錄,以 name 做爲文件或目錄名建立 File 對象
public File(UR ui) 使用給定的統一資源定位符來定位文件


在使用 File 類的構造方法時,須要注意下面幾點:
(1)path 參數能夠是絕對路徑,也能夠是相對路徑,也能夠是磁盤上的某個目錄。
( 2)因爲不一樣操做系統使用的目錄分隔符不一樣,能夠使用 System 類的一個靜態變量System.dirSep,來實如今不一樣操做系統下都通用的路徑。如:
    "d:"+System.dirSep+"myjava"+System.dirSep+"file"

獲取屬性和操做

藉助 File 對象,能夠獲取文件和相關目錄的屬性信息並能夠對其進行管理和操做。表 10-10列出了其經常使用的方法及說明。

表 10-10 File 的經常使用方法
方法 功能描述
boolean canRead() 若是文件可讀,返回真,不然返回假
boolean canWrite() 若是文件可寫,返回真,不然返回假
boolean exists() 判斷文件或目錄是否存在
boolean createNewFile() 若文件不存在,則建立指定名字的空文件,並返回真,若不存在返回假
boolean isFile() 判斷對象是否表明有效文件
boolean isDirectory() 判斷對象是否表明有效目錄
boolean equals(File f) 比較兩個文件或目錄是否相同
string getName() 返回文件名或目錄名的字符串
string getPath() 返回文件或目錄路徑的字符串
long length() 返回文件的字節數,若 File 對象表明目錄,則返回 0
long lastModified() 返回文件或目錄最近一次修改的時間
String[] list() 將目錄中全部文件名保存在字符串數組中並返回,若 File 對象不是目錄返回 null
boolean delete() 刪除文件或目錄,必須是空目錄才能刪除,刪除成功返回真,不然返回假
boolean mkdir() 建立當前目錄的子目錄,成功返回真,不然返回假
boolean renameTo(File newFile) 將文件重命名爲指定的文件名


【例 10-11】判斷輸入的絕對路徑是表明一個文件或一個目錄。如果文件輸出此文件的絕對路徑,並判斷此文件的文件屬性(是否可讀寫或隱藏);如果目錄則輸出該目錄下全部文件(不包括隱藏文件)(查看源代碼)。

運行結果如圖 10-11 所示:

圖 10-11  輸入一個文件路徑後例 10_11 的運行結果
圖 10-11  輸入一個文件路徑後例 10_11 的運行結果
 
8.7  java中文件的隨機讀寫                            

Java.io 包提供了 RandomAccessFile 類用於隨機文件的建立和訪問。使用這個類,能夠跳轉到文件的任意位置讀寫數據。程序能夠在隨機文件中插入數據,而不會破壞該文件的其餘數據。此外,程序也能夠更新或刪除先前存儲的數據,而不用重寫整個文件。

RandomAccessFile類是Object類的直接子類,包含兩個主要的構造方法用來創 建RandomAccessFile 的對象,如表 10-11 所示。

表 10-11 RandomAccessFile 類的構造方法
構造方法 功能描述
public RandomAccessFile(String name, String mode) 指定隨機文件流對象所對應的文件名,以 mode 表示對文件的訪問模式
public RandomAccessFile (File file, String mode) 以 file 指定隨機文件流對象所對應的文件名,以 mode 表示訪問模式


須要注意的是,mode 表示所建立的隨機讀寫文件的操做狀態,其取值包括:

  • r:表示以只讀方式打開文件。
  • rw:表示以讀寫方式打開文件,使用該模式只用一個對象便可同時實現讀寫操做。


表 10-12 列出了 RandowAccessFile 類經常使用的方法及說明。

表 10-12 RandowAccessFile 的經常使用方法
方法 功能描述
long length() 返回文件長度
void seek(long pos) 移動文件位置指示器,pos 指定從文件開頭的偏離字節數
int skipBytes(int n) 跳過 n 個字節,返回數爲實際跳過的字節數
int read() 從文件中讀取一個字節,字節的高 24 位爲 0,若遇到文件結尾,返回-1
final byte readByte() 從文件中讀取帶符號的字節值
final char readChar() 從文件中讀取一個 Unicode 字符
final void writeChar(inte c) 寫入一個字符,兩個字節


【例 10-12】模仿系統日誌,將數據寫入到文件尾部。

//********** ep10_12.java **********
import java.io.*;
class ep10_12{
    public static void main(String args[]) throws IOException{
        try{
            BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
            String s=in.readLine();
            RandomAccessFile myFile=new RandomAccessFile("ep10_12.log","rw");    //
            myFile.seek(myFile.length());  //移動到文件結尾
            myFile.writeBytes(s+"\n");  //寫入數據
            myFile.close();
        }
        catch(IOException e){}
    }
}

程序運行後在目錄中創建一個 ep10_12.log 的文件,每次運行時輸入的內容都會在該文件內容的結尾處添加。

 

8.8  java中文件的壓縮處理                      

Java.util.zip 包中提供了可對文件的壓縮和解壓縮進行處理的類,它們繼承自字節流類OutputSteam 和 InputStream。其中 GZIPOutputStream 和 ZipOutputStream 可分別把數據壓縮成 GZIP 和 Zip 格式,GZIPInpputStream 和 ZipInputStream 又可將壓縮的數據進行還原。

將文件寫入壓縮文件的通常步驟以下:

  1. 生成和所要生成的壓縮文件相關聯的壓縮類對象。
  2. 壓縮文件一般不僅包含一個文件,將每一個要加入的文件稱爲一個壓縮入口,使用ZipEntry(String FileName)生成壓縮入口對象。
  3. 使用 putNextEntry(ZipEntry entry)將壓縮入口加入壓縮文件。
  4. 將文件內容寫入此壓縮文件。
  5. 使用 closeEntry()結束目前的壓縮入口,繼續下一個壓縮入口。


將文件從壓縮文件中讀出的通常步驟以下:

  1. 生成和所要讀入的壓縮文件相關聯的壓縮類對象。
  2. 利用 getNextEntry()獲得下一個壓縮入口。


【例 10-13】輸入若干文件名,將全部文件壓縮爲「ep10_13.zip」,再從壓縮文件中解壓並顯示。

//********** ep10_13.java **********
import java.io.*;
import java.util.*;
import java.util.zip.*;
class ep10_13{
    public static void main(String args[]) throws IOException{
        FileOutputStream a=new FileOutputStream("ep10_13.zip");
        //處理壓縮文件
        ZipOutputStream out=new ZipOutputStream(new BufferedOutputStream(a));
        for(int i=0;i<args.length;i++){  //對命令行輸入的每一個文件進行處理
            System.out.println("Writing file"+args[i]);
            BufferedInputStream in=new BufferedInputStream(new FileInputStream(args[i]));
            out.putNextEntry(new ZipEntry(args[i]));  //設置 ZipEntry 對象
            int b;
            while((b=in.read())!=-1)
                out.write(b);  //從源文件讀出,往壓縮文件中寫入
            in.close();
        }
        out.close();
        //解壓縮文件並顯示
        System.out.println("Reading file");
        FileInputStream d=new FileInputStream("ep10_13.zip");
        ZipInputStream  inout=new  ZipInputStream(new BufferedInputStream(d));
        ZipEntry z;

        while((z=inout.getNextEntry())!=null){  //得到入口
            System.out.println("Reading file"+z.getName());  //顯示文件初始名
            int x;
            while((x=inout.read())!=-1)
                System.out.write(x);
            System.out.println();
        }
        inout.close();
    }
}

 


例 10-13 運行後,在程序目錄創建一個 ep10_13.zip 的壓縮文件,使用解壓縮軟件(如 WinRAR等),能夠將其打開。命令提示符下,程序運行結果如圖 10-12 所示:

圖 10-12  例 10_13 運行結果
圖 10-12  例 10_13 運行結果

 

九◐  java 經常使用類庫、向量與哈希                            

9.1  java基礎類庫                                              

Java 的類庫是 Java 語言提供的已經實現的標準類的集合,是 Java 編程的 API(Application Program Interface),它能夠幫助開發者方便、快捷地開發 Java 程序。這些類根據實現的功能不一樣,能夠劃分爲不一樣的集合,每一個集合組成一個包,稱爲類庫。Java 類庫中大部分都是由Sun 公司提供的,這些類庫稱爲基礎類庫。

Java 語言中提供了大量的類庫共程序開發者來使用,瞭解類庫的結構能夠幫助開發者節省大量的編程時間,並且可以使編寫的程序更簡單更實用。Java 中豐富的類庫資源也是 Java 語言的一大特點,是 Java 程序設計的基礎。

Java 經常使用包的簡單介紹以下:

  1. java.lang 包:主要含有與語言相關的類。java.lang 包由解釋程序自動加載,不須要顯示說明。
  2. java.io 包:主要含有與輸入/輸出相關的類,這些類提供了對不一樣的輸入和輸出設備讀寫數據的支持,這些輸入和輸出設備包括鍵盤、顯示器、打印機、磁盤文件等。
  3. java.util 包:包括許多具備特定功能的類,有日期、向量、哈希表、堆棧等,其中 Date類支持與時間有關的操做。
  4. java.swing 包和 java.awt 包:提供了建立圖形用戶界面元素的類。經過這些元素,編程者能夠控制所寫的 Applet 或 Application 的外觀界面。包中包含了窗口、對話框、菜單等類。
  5. java.net 包:含有與網絡操做相關的類,如 TCP Scokets、URL 等工具。
  6. java.applet 包:含有控制 HTML 文檔格式、應用程序中的聲音等資源的類,其中 Applet類是用來建立包含於 HTML 的 Applet 必不可少的類。
  7. java.beans 包:定義了應用程序編程接口(API),Java Beans 是 Java 應用程序環境的中性平臺組件結構。

 

9.2  java Object類                        

Object 類位於 java.lang 包中,是全部 Java 類的祖先,Java 中的每一個類都由它擴展而來。

定義Java類時若是沒有顯示的指明父類,那麼就默認繼承了 Object 類。例如:

public class Demo{
// ...
}

 

其實是下面代碼的簡寫形式:

public class Demo extends Object{
// ...
}

 

在Java中,只有基本類型不是對象,例如數值、字符和布爾型的值都不是對象,全部的數組類型,無論是對象數組仍是基本類型數組都是繼承自 Object 類。

Object 類定義了一些有用的方法,因爲是根類,這些方法在其餘類中都存在,通常是進行了重載或覆蓋,實現了各自的具體功能。

equals() 方法

Object 類中的 equals() 方法用來檢測一個對象是否等價於另一個對象,語法爲:
    public boolean equals(Object obj)
例如:

obj1.equals(obj2);

 

在Java中,數據等價的基本含義是指兩個數據的值相等。在經過 equals() 和「==」進行比較的時候,引用類型數據比較的是引用,即內存地址,基本數據類型比較的是值。

注意:

  • equals()方法只能比較引用類型,「==」能夠比較引用類型及基本類型。
  • 當用 equals() 方法進行比較時,對類 File、String、Date 及包裝類來講,是比較類型及內容而不考慮引用的是不是同一個實例。
  • 用「==」進行比較時,符號兩邊的數據類型必須一致(可自動轉換的數據類型除外),不然編譯出錯,而用 equals 方法比較的兩個數據只要都是引用類型便可。

hashCode() 方法

散列碼(hashCode)是按照必定的算法由對象獲得的一個數值,散列碼沒有規律。若是 x 和 y 是不一樣的對象,x.hashCode() 與 y.hashCode() 基本上不會相同。

hashCode() 方法主要用來在集合中實現快速查找等操做,也能夠用於對象的比較。

在 Java 中,對 hashCode 的規定以下:

  • 在同一個應用程序執行期間,對同一個對象調用 hashCode(),必須返回相同的整數結果——前提是 equals() 所比較的信息都未曾被改動過。至於同一個應用程序在不一樣執行期所得的調用結果,無需一致。
  • 若是兩個對象被 equals() 方法視爲相等,那麼對這兩個對象調用 hashCode() 必須得到相同的整數結果。
  • 若是兩個對象被 equals() 方法視爲不相等,那麼對這兩個對象調用 hashCode() 沒必要產生不一樣的整數結果。然而程序員應該意識到,對不一樣對象產生不一樣的整數結果,有可能提高hashTable(後面會學到,集合框架中的一個類)的效率。


簡單地說:若是兩個對象相同,那麼它們的 hashCode 值必定要相同;若是兩個對象的 hashCode 值相同,它們並不必定相同。在 Java 規範裏面規定,通常是覆蓋 equals() 方法應該連帶覆蓋 hashCode() 方法。

toString() 方法

toString() 方法是 Object 類中定義的另外一個重要方法,是對象的字符串表現形式,語法爲:
    public String toString()
返回值是 String 類型,用於描述當前對象的有關信息。Object 類中實現的 toString() 方法是返回當前對象的類型和內存地址信息,但在一些子類(如 String、Date 等)中進行了 重寫,也能夠根據須要在用戶自定義類型中重寫 toString() 方法,以返回更適用的信息。

除顯式調用對象的 toString() 方法外,在進行 String 與其它類型數據的鏈接操做時,會自動調用 toString() 方法。

以上幾種方法,在Java中是常常用到的,這裏僅做簡單介紹,讓你們對Object類和其餘類有所瞭解,詳細說明請參考 Java API 文檔。

 

9.3  java語言包(java.lang)簡介                      

Java語言包(java.lang)定義了Java中的大多數基本類,由Java語言自動調用,不須要顯示聲明。該包中包含了Object類,Object類是整個類層次結構的根結點,同時還定義了基本數據類型的類,如:String、Boolean、Byter、Short等。這些類支持數字類型的轉換和字符串的操做等,下面將進行簡單介紹。

Math類

Math類提供了經常使用的數學運算方法以及Math.PI和Math.E兩個數學常量。該類是final的,不能被繼承,類中的方法和屬性所有是靜態,不容許在類的外部建立Math類的對象。所以,只能使用Math類的方法而不能對其做任何更改。表8-1列出了Math類的主要方法。 

表8-1 Math類的主要方法
方法 功能
int abs(int i) 求整數的絕對值(另有針對long、float、double的方法)
double ceil(double d) 不小於d的最小整數(返回值爲double型)
double floor(double d) 不大於d的最大整數(返回值爲double型)
int max(int i1,int i2) 求兩個整數中最大數(另有針對long、float、double的方法)
int min(int i1,int i2) 求兩個整數中最小數(另有針對long、float、double的方法)
double random() 產生0~1之間的隨機數
int round(float f) 求最靠近f的整數
long round(double d) 求最靠近d的長整數
double sqrt(double a) 求平方根
double sin(double d) 求d的sin值(另有求其餘三角函數的方法如cos,tan,atan)
double log(double x) 求天然對數
double exp(double x) 求e的x次冪(ex
double pow(double a, double b) 求a的b次冪


【例8-2】產生10個10~100之間的隨機整數。

//********** ep8_2.java **********
class ep8_2{
    public static void main(String args[]){
        int a;
        System.out.print("隨機數爲:");
        for(int i=1;i<=10;i++){
            a=(int)((100-10+1)*Math.random()+10);
            System.out.print(" "+a);
        }
        System.out.println();
    }
}

運行結果: 隨機數爲:12 26 21 68 56 98 22 69 68 31

因爲產生的是隨機數,例8-2每次運行的結果都不會相同。若要產生[a,b]之間的隨機數其通式爲:
    (b-a+1)*Math.random()+a

字符串類

字符串是字符的序列。在 Java 中,字符串不管是常量仍是變量都是用類的對象來實現的。java.lang 提供了兩種字符串類:String 類和 StringBuffer 類。

1.String 類
按照 Java 語言的規定,String 類是 immutable 的 Unicode 字符序列,其做用是實現一種不能改變的靜態字符串。例如,把兩個字符串鏈接起來的結果是生成一個新的字符串,而不會使原來的字符串改變。實際上,全部改變字符串的結果都是生成新的字符串,而不是改變原來字符串。

字符串與數組的實現很類似,也是經過 index 編號來指出字符在字符串中的位置的,編號從0 開始,第 2 個字符的編號爲 1,以此類推。若是要訪問的編號不在合法的範圍內,系統會產生 StringIndexOutOfBoundsExecption 異常。若是 index 的值不是整數,則會產生編譯錯誤。

String 類提供瞭如表 8-2 所示的幾種字符串建立方法。

表 8-2 String 建立字符串的方法
方法 功能
String s=」Hello!」 用字符串常量自動建立 String 實例。
String s=new String(String s) 經過 String 對象或字符串常量傳遞給構造方法。
public String(char value[]) 將整個字符數組賦給 String 構造方法。
public String(char value[], int offset, int count) 將字符數組的一部分賦給 String 構造方法,offset 爲起始下標,count爲子數組長度。


2.StringBuffer 類
String 類不能改變字符串對象中的內容,只能經過創建一個新串來實現字符串的變化。若是字符串須要動態改變,就須要用 StringBuffer 類。StringBuffer 類主要用來實現字符串內容的添加、修改、刪除,也就是說該類對象實體的內存空間能夠自動改變大小,以便於存放一個可變的字符序列。

StringBuffer 類提供的三種構造方法
構造方法 說明
StringBuffer() 使用該無參數的構造方法建立的 StringBuffer 對象,初始容量爲 16 個字符,當對象存放的字符序列大於 16 個字符時,對象的容量自動增長。該對象能夠經過 length()方法獲取實體中存放的字符序列的長度,經過 capacity()方法獲取當前對象的實際容量。
StringBuffer(int length) 使用該構造方法建立的 StringBuffer 對象,其初始容量爲參數 length 指定的字符個數,當對象存放的字符序列的長度大於 length 時,對象的容量自動增長,以便存放所增長的字符。
StringBuffer(Strin str) 使用該構造方法建立的 StringBuffer 對象,其初始容量爲參數字符串 str 的長度再加上 16 個字符。

 

 

幾種 StringBuffer 類經常使用的方法
方法 說明
append() 使用 append() 方法能夠將其餘 Java 類型數據轉化爲字符串後再追加到 StringBuffer 的對象中。
insert(int index, String str) insert() 方法將一個字符串插入對象的字符序列中的某個位置。
setCharAt(int n, char ch) 將當前 StringBuffer 對象中的字符序列 n 處的字符用參數 ch 指定的字符替換,n 的值必須是非負的,而且小於當前對象中字符串序列的長度。
reverse() 使用 reverse()方法能夠將對象中的字符序列翻轉。
delete(int n, int m) 從當前 StringBuffer 對象中的字符序列刪除一個子字符序列。這裏的 n 指定了須要刪除的第一個字符的下標,m 指定了須要刪除的最後一個字符的下一個字符的下標,所以刪除的子字符串從 n~m-1。
replace(int n, int m, String str) 用 str 替換對象中的字符序列,被替換的子字符序列由下標 n 和 m 指定。

 

 9.4  日期和時間類簡介                                      

Java 的日期和時間類位於 java.util 包中。利用日期時間類提供的方法,能夠獲取當前的日期和時間,建立日期和時間參數,計算和比較時間。

Date 類

Date 類是 Java 中的日期時間類,其構造方法比較多,下面是經常使用的兩個:

  • Date():使用當前的日期和時間初始化一個對象。
  • Date(long millisec):從1970年01月01日00時(格林威治時間)開始以毫秒計算時間,計算 millisec 毫秒。若是運行 Java 程序的本地時區是北京時區(與格林威治時間相差 8 小時),Date dt1=new Date(1000);,那麼對象 dt1 就是1970年01月01日08時00分01秒。


請看一個顯示日期時間的例子:

import java.util.Date;
public class Demo{
public static void main(String args[]){
Date da=new Date(); //建立時間對象
System.out.println(da); //顯示時間和日期
long msec=da.getTime();
System.out.println("從1970年1月1日0時到如今共有:" + msec + "毫秒");
}
}

 

運行結果:
Mon Feb 05 22:50:05 CST 2007
從1970年1月1日0時到如今共有:1170687005390 毫秒

一些比較經常使用的 Date 類方法:

方法 功能
boolean after(Date date) 若調用 Date 對象所包含的日期比 date 指定的對象所包含的日期晚,返回 true,不然返回 false。
boolean before(Date date) 若調用 Date 對象所包含的日期比 date 指定的對象所包含的日期早,返回 true,不然返回 false。
Object clone() 複製調用 Date 對象。
int compareTo(Date date) 比較調用對象所包含的日期和指定的對象包含的日期,若相等返回 0;若前者比後者早,返回負值;不然返回正值。
long getTime() 以毫秒數返回從 1970 年 01 月 01 日 00 時到目前的時間。
int hashCode() 返回調用對象的散列值。
void setTime(long time) 根據 time 的值,設置時間和日期。time 值從 1970 年 01 月 01 日 00 時開始計算。
String toString() 把調用的 Date 對象轉換成字符串並返回結果。
public Static String valueOf(type variable) 把 variable 轉換爲字符串。

Date 對象表示時間的默認順序是星期、月、日、小時、分、秒、年。若須要修改時間顯示的格式能夠使用「SimpleDateFormat(String pattern)」方法。

例如,用不一樣的格式輸出時間:

import java.util.Date;
import java.text.SimpleDateFormat;
public class Demo{
public static void main(String args[]){
Date da=new Date();
System.out.println(da);
SimpleDateFormat ma1=new SimpleDateFormat("yyyy 年 MM 月 dd 日 E 北京時間");
System.out.println(ma1.format(da));
SimpleDateFormat ma2=new SimpleDateFormat("北京時間:yyyy 年 MM 月 dd 日 HH 時 mm 分 ss 秒");
System.out.println(ma2.format(-1000));
}
}

 

運行結果:
Sun Jan 04 17:31:36 CST 2015
2015 年 01 月 04 日 星期日 北京時間
北京時間:1970 年 01 月 01 日 07 時 59 分 59 秒

Calendar 類

抽象類 Calendar 提供了一組方法,容許把以毫秒爲單位的時間轉換成一些有用的時間組成部分。Calendar 不能直接建立對象,但能夠使用靜態方法 getInstance() 得到表明當前日期的日曆對象,如:

Calendar calendar=Calendar.getInstance();

該對象能夠調用下面的方法將日曆翻到指定的一個時間:

void set(int year,int month,int date);
void set(int year,int month,int date,int hour,int minute);
void set(int year,int month,int date,int hour,int minute,int second);

 

若要調用有關年份、月份、小時、星期等信息,能夠經過調用下面的方法實現:

int get(int field);

其中,參數 field 的值由 Calendar 類的靜態常量決定。其中:YEAR 表明年,MONTH 表明月,HOUR 表明小時,MINUTE 表明分,如:

calendar.get(Calendar.MONTH);

若是返回值爲 0 表明當前日曆是一月份,若是返回 1 表明二月份,依此類推。

由 Calendar 定義的一些經常使用方法以下表所示:

方法 功能
abstract void add(int which,int val) 將 val 加到 which 所指定的時間或者日期中,若是須要實現減的功能,能夠加一個負數。which 必須是 Calendar 類定義的字段之一,如 Calendar.HOUR
boolean after(Object calendarObj) 若是調用 Calendar 對象所包含的日期比 calendarObj 指定的對象所包含的日期晚,返回 true,不然返回 false
boolean before(Object calendarObj) 若是調用 Calendar 對象所包含的日期比 calendarObj 指定的對象所包含的日期早,返回 true,不然返回 false
final void clear() 對調用對象包含的全部時間組成部分清零
final void clear(int which) 對調用對象包含的 which 所指定的時間組成部分清零
boolean equals(Object calendarObj) 若是調用 Calendar 對象所包含的日期和 calendarObj 指定的對象所包含的日期相等,返回 true,不然返回 false
int get(int calendarField) 返回調用 Calendar 對象的一個時間組成部分的值,這個組成部分由 calendarField指定,能夠被返回的組成部分如:Calendar.YEAR,Calendar.MONTH 等
static Calendar getInstance() 返回使用默認地域和時區的一個 Calendar 對象
final Date getTime() 返回一個和調用對象時間相等的 Date 對象
final boolean isSet(int which) 若是調用對象所包含的 which 指定的時間部分被設置了,返回 true,不然返回 false
final void set(int year,int month) 設置調用對象的各類日期和時間部分
final void setTime(Date d) 從 Date 對象 d 中得到日期和時間部分
void setTimeZone(TimeZone t) 設置調用對象的時區爲 t 指定的那個時區

GregorianCalendar 類

GregorianCalendar 是一個具體實現 Calendar 類的類,該類實現了公曆日曆。Calendar 類的 getInstance() 方法返回一個 GregorianCalendar,它被初始化爲默認的地域和時區下的當前日期和時間。

GregorianCalendar 類定義了兩個字段:AD 和 BC,分別表明公元前和公元后。其默認的構造方法 GregorianCalendar() 以默認的地域和時區的當前日期和時間初始化對象,另外也能夠指定地域和時區來創建一個 GregorianCalendar 對象,例如:

GregorianCalendar(Locale locale);
GregorianCalendar(TimeZone timeZone);
GregorianCalendar(TimeZone timeZone,Locale locale);

 

GregorianCalendar 類提供了 Calendar 類中全部的抽象方法的實現,同時還提供了一些附加的方法,其中用來判斷閏年的方法爲:
    Boolean isLeapYear(int year);
若是 year 是閏年,該方法返回 true,不然返回 false。

 

9.5  java向量(vector)及其應用                        

Vector(向量)是 java.util 包中的一個類,該類實現了相似動態數組的功能

向量和數組類似,均可以保存一組數據(數據列表)。可是數組的大小是固定的,一旦指定,就不能改變,而向量卻提供了一種相似於「動態數組」的功能,向量與數組的重要區別之一就是向量的容量是可變的。

能夠在向量的任意位置插入不一樣類型的對象,無需考慮對象的類型,也無需考慮向量的容量。

向量和數組分別適用於不一樣的場合,通常來講,下列場合更適合於使用向量:
  • 若是須要頻繁進行對象的插入和刪除工做,或者由於須要處理的對象數目不定。
  • 列表成員所有都是對象,或者能夠方便的用對象表示。
  • 須要很快肯定列表內是否存在某一特定對象,而且但願很快了解到對象的存放位置。

向量做爲一種對象提供了比數組更多的方法,但須要注意的是,向量只能存儲對象,不能直接存儲簡單數據類型,所以下列場合適用於使用數組:
  • 所需處理的對象數目大體能夠肯定。
  • 所需處理的是簡單數據類型。

向量的使用

向量必需要先建立後使用,向量的大小是向量中元素的個數,向量的容量是被分配用來存儲元素的內存大小,其大小老是大於向量的大小。下面是 Vector 的構造方法:
Vector(); //①建立空向量,初始大小爲 10
Vector(int initialCapacity); //②建立初始容量爲 capacity 的空向量
Vector(int initialCapacity,int capacityIncrement); //③建立初始容量爲 initialCapacity,增量爲 capacityIncrement 的空向量

 

使用第①種方式系統會自動對向量進行管理。

使用第②種方式,會建立一個初始容量(即向量可存儲數據的大小)爲 initialCapacity 的空向量,當真正存放的數據超過該容量時,系統會自動擴充容量,每次增長一倍。

使用第③中方式,會建立一個初始容量爲 initialCapacity 的空向量,當真正存放的數據超過該容量時,系統每次會自動擴充 capacityIncrement。若是 capacityIncrement 爲0,那麼每次增長一倍,。

經過分配多於所需的內存空間,向量減小了必須的內存分配的數目。這樣可以有效地減小分配所消耗的時間,每次分配的額外空間數目將由建立向量時指定的增量所決定。

除了構造方法外,向量類還提供了三個屬性變量,分別爲:
protected int capacityIncrement; //當向量大小不足時,所用的增量大小
protected int elementCount; //向量的元素個數
protected Object elementData[]; //向量成員數據所用的緩衝

 

一旦建立了Vector類的實例,就能夠用其方法來執行插入、刪除以及查找對象等操做,向量類提供了極爲豐富的方法,下表給出了一些經常使用的方法:
方法 功能
void addElement(Object element) 將給定對象 element 增長到向量末尾
int capacity() 返回向量容量
boolean contains(Object element) 若向量中包含了 element 返回 true,不然返回 false
void copyInto(Object Array[]) 將向量元素複製到指定數組
synchronized Object elementAt(int index) 返回指定下標的元素,若下標非法,拋出 ArrayIndexOutOfBoundsExecption 異常
void ensureCapacity(int size) 將向量的最小容量設爲 size
synchronized Object firstElement() 返回向量的第一個元素,若向量爲空,拋出 NoSuchElementException 異常
int indexOf(Object element) 返回 element 的下標,若對象不存在返回-1
int indexOf (Object element,int start) 從指定位置(start)開始搜索向量,返回對象所對應的下標值,若未找到返回-1
void insertElementAt (Object obj,int index) 將給定的對象插入到指定的下標處
boolean isEmpty() 若向量不包括任何元素,返回 true,不然返回 false
synchronized Object lastElement() 返回向量的最後一個元素,若向量爲空,拋出 NoSuchElementException 異常
int lastIndexOf(Object element) 從向量末尾向前搜索向量,返回對象的下標值
int lastIndexOf(Object element,int start) 從指定位置開始向前搜索向量,返回給定對象的下標值,若未找到返回-1
void removeAllElements() 刪除向量中的全部對象,向量變成空向量
boolean removeElement(Object element) 從向量中刪除指定對象 element,若給定的對象在向量中保存屢次,則只刪除其第一個實例,若是刪除成功,返回 true,若是沒發現對象,則返回 false
void removeElementAt(int index) 刪除由 index 指定位置處的元素
void setElementAt(Object obj,int index) 將給定對象存放到給定下標處,該下標處的原有對象丟失
void setSize(int size) 將向量中的元素個數設爲 size,若是新的長度小於原來的長度,元素將丟失,若新的長度大於原來的長度,則在其後增長 null 元素
int size() 返回向量中當前元素的個數
String toString() 將向量轉換成字符串
void trimToSize() 將向量的容量設爲與當前擁有的元素個數相等
與數組相同,向量對象也能夠經過 new 操做符實現。其語句爲:
    Vector vector=new Vector();
 

 9.6  java哈希表及其應用                          

哈希表也稱爲散列表,是用來存儲羣體對象的集合類結構

什麼是哈希表

數組和向量均可以存儲對象,但對象的存儲位置是隨機的,也就是說對象自己與其存儲位置之間沒有必然的聯繫。當要查找一個對象時,只能以某種順序(如順序查找或二分查找)與各個元素進行比較,當數組或向量中的元素數量不少時,查找的效率會明顯的下降。

一種有效的存儲方式,是不與其餘元素進行比較,一次存取便能獲得所須要的記錄。這就須要在對象的存儲位置和對象的關鍵屬性(設爲 k)之間創建一個特定的對應關係(設爲 f),使每一個對象與一個惟一的存儲位置相對應。在查找時,只要根據待查對象的關鍵屬性 k 計算f(k)的值便可。若是此對象在集合中,則一定在存儲位置 f(k)上,所以不須要與集合中的其餘元素進行比較。稱這種對應關係 f 爲哈希(hash)方法,按照這種思想創建的表爲哈希表。

Java 使用哈希表類(Hashtable)來實現哈希表,如下是與哈希表相關的一些概念:

  • 容量(Capacity):Hashtable 的容量不是固定的,隨對象的加入其容量也能夠自動增加。
  • 關鍵字(Key):每一個存儲的對象都須要有一個關鍵字,key 能夠是對象自己,也能夠是對象的一部分(如某個屬性)。要求在一個 Hashtable 中的全部關鍵字都是惟一的。
  • 哈希碼(Hash Code):若要將對象存儲到 Hashtable 上,就須要將其關鍵字 key 映射到一個整型數據,成爲 key 的哈希碼。
  • 項(Item):Hashtable 中的每一項都有兩個域,分別是關鍵字域 key 和值域 value(存儲的對象)。Key 和 value 均可以是任意的 Object 類型的對象,但不能爲空。
  • 裝填因子(Load Factor):裝填因子表示爲哈希表的裝滿程度,其值等於元素數比上哈希表的長度。

哈希表的使用

哈希表類主要有三種形式的構造方法:

    Hashtable(); //默認構造函數,初始容量爲 101,最大填充因子 0.75
    Hashtable(int capacity);
    Hashtable(int capacity,float loadFactor)

哈希表類的主要方法如表 8-6 所示。

表 8-6 哈希表定義的常見方法
方法 功能
void clear() 從新設置並清空哈希表
boolean contains(Object value) 肯定哈希表內是否包含了給定的對象,如有返回 true,不然返回 false
boolean containsKey(Object key) 肯定哈希表內是否包含了給定的關鍵字,如有返回 true,不然返回 false
boolean isEmpty() 確認哈希表是否爲空,如果返回 true,不然返回 false
Object get(Object key) 獲取對應關鍵字的對象,若不存在返回 null
void rehash() 再哈希,擴充哈希表使之能夠保存更多的元素,當哈希表達到飽和時,系統自動調用此方法
Object put(Object key,Object value) 用給定的關鍵字把對象保存到哈希表中,此處的關鍵字和元素均不可爲空
Object remove(Object key) 從哈希表中刪除與給定關鍵字相對應的對象,若該對象不存在返回 null
int size() 返回哈希表的大小
String toString() 將哈希表內容轉換爲字符串


哈希表的建立也能夠經過 new 操做符實現。其語句爲:

 HashTable has=new HashTable();

【例 8-12】哈希表的遍歷。

//********** ep8_12.java **********
import java.util.*;
class ep8_12{
    public static void main(String args[]){
        Hashtable has=new Hashtable();
        has.put("one",new Integer(1));
        has.put("two",new Integer(2));
        has.put("three",new Integer(3));
        has.put("four",new Double(12.3));
        Set s=has.keySet();
        for(Iterator<String> i=s.iterator();i.hasNext();){
            System.out.println(has.get(i.next()));
        }
    }
}

 


運行結果:
2
1
3
12.3

 

                                                                                                                                                                                                                         2015年10月14日

                                                             瘋子java閱讀筆記

--------------------

相關文章
相關標籤/搜索