Java 常用API


String是不變對象,即:字符串對象創建后,表示的字符內容不可變,若改變必定創建新對象
java對於字符串有一個優化,即:
字符串常量池,這是在堆內存中開辟的一塊空間,用於保存所有使用字面量形式創建的字符串對象
當再次使用該字面量創建新的字符串時會直接重用而不會再創建新的以達到節省內存開銷的目的

boolean tf=s2.equals(s4);
通常我們比較字符串都是比較字符串的內容,因此比較字符串時我們不會使用"=="而是使用字符串的方法equals.
equals方法是用來比較兩個字符串對象所表示的內容是否相同的

String s5="123"+"abc"
這里用到了編譯器的一個特性:
編譯器在編譯源代碼時,當遇到一個計算表達式可以在編譯期間確定結果時就會直接進行計算,並將結果編譯到class文件中,
這樣JVM每次運行程序時就無需再計算
比如: int a= 1+1; 編譯器編譯后class改為: int a=2;
下面的字符串也是如此,會被改為: String s5="123abc";

String substring(int start,int end)
截取當前字符串中指定范圍內的字符串.注:java API中,通常使用兩個數字表示范圍時,都是含頭不含尾的
String sub=line.substring(4, 8);//含頭不含尾
sub=line.substring(4);//從指定位置開始截取到字符串末尾

String toUpperCase()
String toLowerCase()
將當前字符串中的英文部分轉換為全大寫或全小寫

String trim()
去除當前字符串兩側的空白字符

char charAt(int index)
獲取當前字符串中指定位置對應的字符

int indexOf(String str)
查找給定字符串在當前字符串中的位置,若當前字符串不包含給定內容則返回值為-1(可利用這個特點查找字符串中
是否含有想要的字符,沒有則返回-1)

int indexOf(String str, int index)
重載的方法允許我們從指定位置開始查找第一次出現的指定字符的位置

index=str.lastIndexOf("in");
查找最后一次出現指定字符串的位置
可利用indexOf 和 lastIndexOf 判斷某個字符是否只出現一次,同一個位置則只出現一次

int len=str.length();
獲取當前字符串的長度(字符個數)

boolean matches(String regex) String支持正則表達式的方法之一
使用給定的正則表達式匹配當前字符串是否符合格式要求,符合則返回true
注意:給定的正則表達式就算不指定邊界匹配符即:(^...$)也是做完全匹配驗證的

String[] split(String regex) String支持正則表達式方法二
將當前字符串中按照滿足正則表達式的部分拆分,然后將拆分后的字符串以數組形式返回

String replaceAll(String regex,String str) String支持正則表達式方法三
將當前字符串中滿足正則表達式的部分替換為給定的內容 line=line.replaceAll("[a-zA-Z]+", "#NUMBER#");

boolean startsWith(String str)
boolean endsWith(String str)
判斷字符串是否是以給定的字符串開始或結尾的

java.lang.StringBuilder
1.String的優化措施僅照顧重用性,因此頻繁修改字符串會帶來內存開銷大,運行效率差的結果對此,
java提供一個專門用於修改字符串的API
2.其內部維護一個可變的char數組,所有的修改都是在這個數組中進行的,因此開銷小,性能好,並且其提供了
便於修改字符串的一系列方法,包括了增,刪,改,插等基本操作
builder.append(",為了找個好工作!"); //插入 ",為了找個好工作!"
str=builder.toString();//獲取StringBuilder內部的字符串
builder.replace(9, 16, "就是為了改變世界"); //好好學習java,為了找個好工作!"修改為 "好好學習java,就是為了改變世界!"
builder.delete(0, 8); // "好好學習java,就是為了改變世界!"刪除"好好學習java"
builder.insert(0, "活着"); // ",就是為了改變世界!" 插入"活着,就是為了改變世界!"

static String valueOf(XXX xxx)
字符串提供了若干的重載的valueOf方法,它們都是靜態方法.作用是將給定的內容轉換為字符串
str=123+""; //任何類型與字符串連接結果都是字符串


包裝類
包裝類是為了解決基本類型不能直接參與面向對象開發的問題
8個基本類型對應8個不同的包裝類,其中6個表示數字的包裝類繼承自java.lang.Number,其他兩個繼承自Object

Integer i1=Integer.valueOf(d);
java推薦我們使用包裝類的靜態方法:valueOf來將基本類型轉換為包裝類

int i=i1.intValue();
包裝類轉換為基本類型

int max=Integer.MAX_VALUE;
int min=Integer.MIN_VALUE;
數字類型的包裝類有兩個常量,MAX_VALUE,MIN_VALUE分別記錄了其對應的基本類型的范圍

String str="22";
int a=Integer.parseInt(str);
包裝類提供了一個功能,靜態方法parseXXX
該方法可以將字符串解析為對應的基本類型數據
前提是該字符串能正確描述要轉換的基本類型可以保存的值,異常是 NumberFormatException

自動拆裝箱
JDK5之后推出了一個新的特性:自動拆裝箱
該特性是編譯器認可的,而不是虛擬機,編譯器在看到有基本類型與包裝類之間互相賦值時
會添加轉換代碼將他們轉換
int i=new Integer(1); //這里觸發了編譯器的自動拆箱特性:
//代碼會被編譯器改為: int i=new Integer(1).intValue();
Integer in=1; //觸發自動裝箱特性,代碼會被改為: Integer in =Integer.valueOf(1);

input ouput

java.io.File File用於表示文件系統中的一個文件或目錄
使用File我們可以:
1.訪問其表示的文件或目錄的屬性(名字,大小等)
2.創建,刪除文件或目錄
3.訪問一個目錄中的子項
但是不能訪問文件數據

創建File是要指定路徑
路徑有兩種:絕對路徑和相對路徑
絕對路徑通常不適用,雖然清晰明了但是無法做到跨平台
相對路徑不能直觀體現出實際位置,但是靈活並適應各種不同運行環境
在eclipse中執行代碼時,"./"指的就是當前項目目錄

File file=new File("./demo.txt");//在當前項目目錄下新建一個文件demo.txt
String name=file.getName();//獲取文件名
long len=file.length();//獲取長度(單位是字節),返回值是long型
String path=file.getAbsolutePath();//獲取絕對路徑
boolean cr=file.canRead();//是否可讀
boolean cw=file.canWrite();//是否可寫
boolean ih=file.isHidden();//是否為隱藏文件
boolean exist=file.exists()//判斷當前File表示的文件或目錄是否已經存在,存在則返回true
file.createNewFile();//創建該文件,增加一個判斷if(!file.exists()),不存在再創建
file.delete();//刪除一個文件,增加一個判斷if(file.exists()),存在再刪除

File dir=new File(".");//"."表示當前目錄下
boolean isFile()
判斷當前file表示的是否文件
boolean isDirectory()
判斷當前File表示的是否為目錄
File[] listFiles()
獲取當前目錄中的所有子項,數組中每一個元素就是其中一個子項
File[] subs=dir.listFiles();// if(dir.isDirectory())判斷是否為目錄,是再獲取當前目錄中的所有子項

File[] listFiles(FileFilter filter)
將目錄中符合過濾器需求的子項獲取

File dir=new File(".");
FileFilter filter = new FileFilter() {
public boolean accept(File file) {//重寫FileFilter接口中的accept方法
return file.getName().startsWith(".");//返回當前目錄中所有名字以"."開頭的內容
}
}; //匿名內部類定義過濾器
File[] subs=dir.listFiles(filter);//回調模式,會調用匿名內部類中的方法

File dir=new File("./demo");//在當前目錄下新建一個名為demo的目錄
if(!dir.exists()) { //判斷目錄是否存在
dir.mkdir();} //創建新目錄

File dir=new File("./a/b/c/d/e/f");//在當前目錄下新建目錄:a/b/c/d/e/f
if(!dir.exists()) { //判斷多級目錄是否存在(判斷到最后一級/f)
dir.mkdirs();}//創建多級目錄
if(dir.exists()) { //判斷多級目錄是否存在(判斷到最后一級/f)
dir.delete();} //刪除目錄有一個前提條件就是該目錄是一個空目錄,只能刪一個,本例/f目錄沒數據才刪掉/f,上層目錄沒刪

java.io.RandomAccessFile
RAF是專門用來讀寫文件數據的API,其基於指針對文件任意位置進行讀寫

RandomAccessFile raf = new RandomAccessFile("./raf.dat","rw");
對當前目錄下的raf.dat文件讀寫數據 String path 權限

void write(int d)
向文件中寫入1個字節,寫入的是給定的int值所對應的2進制的"低八位"
vvvvvvvv
00000000 00000000 00000000 00000001
raf.write(257);//只會存二進制"低八位" 取出來十進制值為1


int read()
從文件中讀取一個字節,並以int形式返回.若返回值為-1則表示為文件末尾

long pos=raf.getFilePointer();//獲取指針位置,返回long值

向文件中寫入一個int最大值,用右移運算符,把高位移到低八位,保存4個字節,即可把一個4字節的int值保存了
void writeInt(int d);//將給定的int值對應的4字節一次性寫出,等同上面的操作
raf.writeLong(213L);
raf.writeDouble(345.343);

void seek(long pos) //移動指針到指定位置
raf.seek(0);

int readInt()
連續讀取4個字節並返回該int值,若連續讀取4個字節的過程中發現讀取到了文件末尾,
此時會直接拋出異常EOFException. EOF(end of file)文件末尾
int d1=raf.readInt();
long l=raf.readLong();
double dou=raf.readDouble();

String提供了轉換為字節的方法
byte[] getBytes() //將當前字符串按照系統默認字符集轉換為一組字節
byte[] getBytes(String csn) // csn------>比如:GBK,UTF-8
重載的getByts方法要求我們傳入一個字符串參數表示字符集的名稱,該名稱不區分大小寫,
作用是將當前字符串按照給定的字符集轉換為一組字節,推薦用這種方法,不要按照系統默認字符集操作
line.getBytes("utf-8");

使用RAF向文件中寫入字符串
raf.write(",能否聽清,".getBytes("UTF-8"));

單字節讀寫是隨機讀寫,效率差
塊讀寫是一組一組字節的讀寫,效率高 通過提高每次讀寫的數據量,減少實際讀寫的次數,可以提高讀寫效率

int read(byte[] data)
一次性從文件中讀取給定數組總長度的字節量,並將讀取到的數據存入到該數組中,返回值為實際讀取到的字節量
若返回值為-1,則表示文件末尾(本次沒有讀取到任何數據)

void write(byte[] data)
一次性將給定字節數組中所有字節寫入文件
void write(byte[] data,int s,int len)
將給定字節數組從下標s處開始的連續len個字節一次性寫入文件
byte[] data=new byte[1024*10];//10K每次
int len=-1;//len保存每次實際讀取到的字節數
while((len=src.read(data))!=-1) {
desc.write(data,0,len);
}

java io 標准的輸入與輸出
使用java IO我們可以對外界設備以相同的方式進行讀寫完成數據交換

java IO將"讀"與"寫"按照方向進行了划分:
輸入:從外界到程序的方向,用於讓程序獲取外界數據,因此輸入是"讀"數據的操作
輸出:從程序到外界的方向,用於將數據"寫"出的操作

java IO以"流"的形式表示讀寫功能
java.io.InputStream 輸入流,通過輸入流我們可以連接上外界設備從而讀取該設備數據
java.io.OutputStream 輸出流
以上兩個流是所有字節輸入流和輸出流的超類,規定了所有輸入流與輸出流的基本讀寫功能

java將流分為兩大類:節點流與處理流
節點流:又稱為"低級流",是真實連接程序與數據源的"管道",用於實際搬運數據的流.讀寫一定是建立在節點流的基礎上進行的
處理流:又稱為"高級流",高級流不能獨立存在,必須連接在其他流上,目的是當數據流經當前流時對其做某些加工處理,簡化我們讀寫
數據時的相應操作

實際使用IO時,我們通常會串聯若干的高級流最終連接到低級流上,使得讀寫數據以流水線式的加工處理完成,這個操作稱為"流的連接",
也是IO的精髓所在

文件流
文件流是一對低級流,作用是連接到文件上,用於讀寫文件數據.
java.io.FileOutputStream:文件輸出流
java.io.FileInputStream:文件輸入流
文件流提供的構造方法:
FileOutputStream(File file)
FileOutputStream(String path)
以上兩種創建方式,默認為覆蓋寫模式,即:若指定的文件已經存在,那么會將該文件原有數據全部刪除,然后再將新數據寫入文件
FileOutputStream(File file,boolean append)
FileOutputStream(String path,boolean append)
以上兩種構造器允許再傳入一個boolean值類型的參數,如果該值為true時,文件輸出流就是追加寫模式,
即數據中原有數據都保留,新內容會被追加到文件末尾
寫文件
FileOutputStream fos=new FileOutputStream("./fos.txt",true);
fos.write("干".getBytes("UTF-8"));
讀文件
FileInputStream fis=new FileInputStream("fos.txt");
byte[] data=new byte[1000];
int len=fis.read(data);
String str=new String(data,0,len,"UTF-8");

文件流與RAF的區別:
RAF是基於指針的隨機讀寫形式,可以對文件任意位置進行讀或寫操作,可以做到對文件部分數據覆蓋等操作,讀寫更靈活
文件流是基於java IO的標准讀寫,而IO是順序讀寫模式,即:只能向后寫或讀數據,不能回退
單從讀寫靈活度來講RAF是優於文件流的,但是文件流可以基於java IO的流連接完成 一個復雜數據的讀寫,這是RAF不容易做到的

緩沖流
java.io.BufferedOutputStream
java.io.BufferedInputStream
緩沖流是一對高級流,在流連接中的作用是提高讀寫效率,使得我們在進行讀寫操作時用單字節讀寫也能提高讀寫的效率
緩沖流之所以可以提高讀寫效率,是因為緩沖流內部有一個緩沖區(一個字節數組),無論我們使用緩沖流進行何種讀寫
(單字節或塊讀寫),最終都會被緩沖流轉換為塊讀寫來提高效率
int d=-1;
while((d=bis.read())!=-1) { //讀到文件末尾,返回-1
bos.write(d);
}

void flush()
flush方法是OutputStream中定義的方法,所有的輸出流都具有該方法,但是是有緩沖流的該方法有實際意義.
其他的流具有該方法的目的是在流連接中傳遞緩沖操作給緩沖流
flush的作用是將緩沖流中已經緩存的數據一次性寫出.
頻繁調用flush方法會提高寫出的次數從而降低寫出效率,但是能保證數據寫出的即時性

對象流
java.io.ObjectOutputStream
java.io.ObjectInputStream
對象流是一對高級流,在流連接中的作用是方便讀寫java對象.(對象與字節的轉換由對象流完成)
該方法可能拋出:NotSerializableException
當寫出的對象所屬的類沒有實現Serializable接口時就會拋出該異常
寫入文件后發現該文件的實際數據量比當前對象保存的內容要大,這是因為這組字節除了包含了該對象的數據外
還有這個對象的結構信息
Serializable 序列化接口
private static final long serialVersionUID = 1L;//手動定義ID值
private transient String[] otherInfo;
當一個屬性被transient修飾后,該對象在序列化時會忽略此屬性的值
忽略不必要的屬性值可以達到對象序列化瘦身的效果,減少不必要的資源開銷
Person p=(Person)ois.readObject();//readObject()返回Object,需要強制轉型

字符流
java.io.Reader和java.io.Writer
上述兩個類是所有字符流的超類,規定了所有字符流都必須具備的讀寫字符的相關方法
java將流按照讀寫單位划分為字節流和字符流.
字符流以char為單位讀寫數據,但是底層本身還是讀寫字節,只是字符與字節的轉換字符流自行完成

轉換流
java.io.InputStreamReader
java.io.OutputStreamWriter
轉換流是一對字符流,同時他們也是高級流,在實際開發時我們通常不會直接操作這兩個流,但是在讀寫文本數據時,
流連接中他們是非常重要的一環,負責銜接其他字符高級流與字節流
FileOutputStream fos=new FileOutputStream("osw.txt");
OutputStreamWriter osw=new OutputStreamWriter(fos,"UTF-8");
osw.write("直到所有的燈都熄滅了也不停留");
轉換流在創建時通常會傳入第二個參數,這個參數用來指定字符集,這樣一來通過當前流寫出的文本數據
都會按照該字符集轉換為字節后寫出
FileInputStream fis=new FileInputStream("osw.txt");
InputStreamReader isr=new InputStreamReader(fis,"UTF-8");
int d=-1;
while((d=isr.read())!=-1) { //讀到文件末尾,返回-1
System.out.print((char)d);// 可以一次讀一個字符
}

緩沖字符流
java.io.BufferedWriter
java.io.BufferedReader
緩沖流是塊讀寫文本數據,提高讀寫效率,並且可以按行讀寫字符串
java.io.PrintWriter具有自動刷新的緩沖字符輸出流,內部總是連接BufferedWriter作為其緩沖加速操作

PrintWriter pw=new PrintWriter("pw.txt","UTF-8");
pw.println("讓我掉下眼淚的,不止昨夜的酒");
向pw.txt文件中寫入字符串,該構造方法內部會自動進行流連接操作,分別連接緩沖字符流,轉換流和文件流

FileOutputStream fos=new FileOutputStream(fileName);
OutputStreamWriter osw=new OutputStreamWriter(fos,"UTF-8");
BufferedWriter bw=new BufferedWriter(osw);
PrintWriter pw=new PrintWriter(bw,true);//true 打開行自動刷新
創建PrintWriter時使用的構造方法如果第一個參數是一個流,那么就支持第二個boolean參數,
這個參數是 一個boolean值,該值為true時就打開了自動行刷新功能,此時每當調用println方法寫出一行字符串都會自動flush

緩沖字符輸入流
java.io.BufferedReader
塊讀文本數據,並且可以按行讀取字符串
String str=null;
while((str=br.readLine())!=null) {
System.out.println(str);
}
String readLine();
連續讀取若干字符直到讀取到換行符為止
將換行符之前的字符組成一個字符串返回.如果返回值為null表示流讀取到末尾了

 

正則表達式:記錄文本規則的代碼
[abc]:a b c中任意一個字符 [^abc]除了a b c的任意一個字符 [a-zA-Z0-9] [a-z&&[^bd]]
. :任意一個字符 \d:相當於[0-9](在java代碼中須寫成"\\d") \w:相當於[a-zA-Z0-9_]
\s:空白字符(比如\t\n 空格) \D: 非\d \W: 非\w \S: 非\s
?:表示0或1個 *:表示0~任意多 +:表示任意多
{n}:表示有n個 {n,}:表示從n~任意多 {n,m}:表示n~m個
():表示分組,用()|()|()表示"或"關系
^:表示字符串開始 $:表示字符串結尾

異常處理機制
java異常處理機制中的try-catch
try{
程序代碼片段
}catch(XXXException e){
當try中出現XXXException后的解決代碼
}finally{ //有必要的時候加入
finally塊是異常處理機制中的最后一部分,他可以直接跟在try語句塊之后或者最后一個catch塊之后
finally可以保證只要程序執行到try當中,無論是否出現異常,finally中的代碼都必定執行
通常我們可以將釋放資源這樣的必須操作的代碼放在這里
JDK7之后推出了一個特性:AutoCloseable
該特性旨在讓我們在源代碼中可以以更簡化的代碼完成在finally中關閉流
}
try(
/*
* 在這里定義的流最終會被編譯器改為在finally中關閉
* 只有實現了AutoCloseable接口的類才能在這里定義並實例化
* 流和RandomAccessFile都實現了該接口
*/
FileOutputStream fos=new FileOutputStream("fos.txt");
){
fos.write(1);
}catch(Exception e) {
System.out.println("出錯了");
}
當JVM執行程序時發現某個錯誤時就會實例化對應的異常實例並將程序執行過程設置好然后將異常拋出
此時若沒有異常處理機制,該異常會被繼續拋出到當前方法之外(這里就拋出到main方法外)若最終拋給虛擬機,則會直接中斷
catch是可以定義多個的,針對try中出現的不同異常有不同處理方式時可以分別捕獲它們
可以在最后一個catch處捕獲Exception來避免因為一個未捕獲的異常導致程序的中斷

面試中finally常見問題
請分別說明final,finally,finalize

finalize是Object定義的方法,該方法是當GC釋放該對象資源時調用此方法,調用后該對象即被釋放
注意:此方法若重寫,里面不應當有耗時的操作
e.printStackTrace();// 將當前的錯誤信息輸出到控制台上
String message=e.getMessage();// 獲取錯誤信息


8.1

java.net.Socket 套接字
Socket封裝了TCP協議傳輸數據的細節,使得我們可以通過兩條流的讀寫完成與遠端計算機的數據交互

socket=new Socket("localhost",8088); // localhost代表本機IP,端口一般選擇8000以后的
實例化Socket的過程就是連接服務端的過程
* 參數1:服務端的IP地址
* 參數2:服務端程序打開的端口
我們通過IP可以找到網絡上的服務端所在計算機,通過端口可以找到該計算機上運行的服務端應用從而建立連接

運行在服務端的ServerSocket主要有兩個作用
1:向系統申請服務端口,客戶端就是通過這個端口與服務端程序建立連接的
2:監聽服務端口,一旦一個客戶端建立連接就會返回一個Socket實例,服務端就可以通過這個Socket實例與該客戶端交互了

server=new ServerSocket(8088);
實例化ServerSocket時要指定申請的服務端口,如果該端口被系統的其它程序使用時會拋出異常

Socket socket=server.accept();
Socket accept() 該方法是一個阻塞方法,調用后就"卡住了",此時開始等待客戶端的連接,一旦一個客戶端建立連接,
此時該方法會立即返回一個Socket實例,通過這個Socket就可以與該客戶端交互了

OutputStream getOutputStream()
OutputStream out=socket.getOutputStream();
Socket提供的方法,通過該方法獲取的輸出流寫出的字節會通過網絡發送給遠端計算機

InputStream getInputStream()
InputStream in=socket.getInputStream();
socket的該方法獲取的輸入流讀取的是遠端計算機發送過來的字節


while((message=br.readLine())!=null) {}
當客戶端斷開連接時,服務端這邊readLine方法有兩種情況:
1.返回值為null,通常Linux的客戶端斷開時會出現這種情況
2.直接拋出SocketException:connection reset

8.2
多線程
多線程改變了代碼的執行方式,從原有的所有代碼都串行操作改變為多個代碼片段之間並行操作.
因此多線程允許多個代碼片段"同時運行"

創建線程的方式有兩種:
1.繼承線程並重寫run方法,在run方法中定義線程要執行的任務.
啟動線程時調用線程的start方法而不是直接調用run方法.當線程的start方法調用后,線程納入到
線程調度中,當其第一次分配到的時間片開始運行時,它的run方法會自動被執行
這種方式優點是創建簡單方便.但是缺點也比較明顯:
1.由於java是單繼承的,這導致繼承了線程就無法在繼承其他的類,這會導致無法重用其他超類
的方法而產生繼承沖突問題.
2.定義線程的同時重寫run方法,這就等於規定了線程要執行的具體任務,導致線程與其執行的任務
產生必然的耦合關系,不利於線程的重用
2.實現Runnable接口並重寫run方法來單獨定義線程的任務
單獨實例化任務,創建線程(傳入實例化任務對象的引用)

static Thread currentThread()
線程提供了一個靜態方法,該方法可以獲取運行這個方法的線程

實際上java的所有代碼都是靠線程運行的,main方法也不例外.
運行main方法的線程不是由我們創建的,而是JVM自行創建的,並用來運行main方法.
而我們通常稱這個線程為"主線程"

ClientHandler handler=new ClientHandler(socket);// 啟動一個線程處理該客戶端交互

String host=socket.getInetAddress().getHostAddress();// 通過socket獲取遠端計算機地址信息

Thread main=Thread.currentThread();//獲取當前線程
String name=main.getName();//獲取線程的名字
long id=main.getId();//獲取唯一標識
int priority=main.getPriority();//獲取線程的優先級
boolean isAlive=main.isAlive();//是否活着
boolean isDaemon=main.isDaemon();//是否被守護
boolean isInterrupted=main.isInterrupted();//是否被打斷

線程的優先級
* 線程啟動后就納入到了線程調度中統一管理,什么時候獲取CPU時間片 完全取決於線程調度.
* 線程是不能主動索取的,通過調整線程的優先級可以最大程度的干涉分配CPU時間片的幾率
*
* 理論上線程優先級越高的線程獲取CPU時間片的幾率越高
*
* 線程的優先級有10個等級,用整數1~10表示
* 1是最小的,5是默認,10是最高的
min.setPriority(Thread.MIN_PRIORITY);
max.setPriority(Thread.MAX_PRIORITY);

static void sleep(long ms) 線程提供了一個靜態方法,該方法可以讓運行這個方法的線程處於阻塞狀態
指定的毫秒,超時后線程會自動回到RUNNABLE狀態再次並發運行
Thread.sleep(5000);//阻塞5s
sleep方法要求處理中斷異常
當一個線程調用sleep方法處於阻塞狀態的過程中,此時被其他線程調用了該線程
的interrupt方法,那么就會打斷這個線程的睡眠阻塞,此時sleep方法就會拋出中斷異常告知

守護線程
守護線程也稱為后台線程,創建和使用上與前台線程一樣,但是有一點不同:
當進程結束時,所有正在運行的守護線程都會被強制停止
進程的結束:當所有普通線程都結束時,進程結束
設置為守護線程要在線程啟動前進行 setDaemon(true);//記得主線程的狀態
//GC是一個典型的守護線程

join方法允許當前線程在join方法所屬線程上等待,直到該線程結束后結束join阻塞繼續后續操作
所有join方法可以協調線程的同步運行 //download.join();
同步運行:多個線程之間實行有順序
異步運行:多個線程之間各自執行各自的

當一個方法的局部內部類中引用了這個方法的其他局部變量時,該變量必須聲明為final的
JDK8之后可以不寫final,但是該變量依然會被編譯器最終改為final的
源自JVM的內存分配問題

多線程並發的安全問題
當多個線程並發訪問同一臨界資源,由於線程切換時機不確定,導致多個線程操作該資源未按照
程序設計的順序進行,導致出現錯誤,嚴重時可能出現系統癱瘓等情況
臨界資源:同一時間只能被一條線程操作的資源

Thread.yield(); //static void yield()
當一個線程執行到這個方法時會主動讓出本次CPU時間片並回到RUNNABLE狀態

當一個方法使用關鍵字synchronized修飾后,這個方法稱為"同步方法",多個線程不能同時在方法內部執行
將多個線程異步操作臨界資源改為同步操作就可以解決多線程的並發安全問題
若在方法上直接使用synchronized,那么同步監視器對象就是當前方法所屬對象this

同步塊
有效的縮小同步范圍可以保證並發安全的前提下盡可能的提高並發的效率
synchronized(同步監視器對象){
需要多線程同步運行的代碼片段
}
同步塊可以更准確的鎖定需要同步運行的代碼片段從而有效控制同步范圍;
使用同步塊時要注意,多個需要同步運行該代碼片段的線程看到的同步監視器對象,即上鎖的對象必須是
同一個才可以.否則沒有同步效果;

靜態方法若使用synchronized修飾后,那么該方法一定具有同步效果
靜態方法的同步監視器對象使用的是當前類的類對象(Class的實例,類對象后面反射的課程中會介紹)
靜態方法里使用synchronized塊時,監視器對象是當前類名.Class// Boo.Class

互斥鎖
當使用synchronized鎖定多個代碼片段,並且指定的同步監視器對象是同一個時,這
些代碼片段間就是互斥的,多個線程不能同時在這些代碼片段間一起執行

8.6
通過socket獲取輸出流,用於將消息發送給當前客戶端//在Server端的ClientHandler的run方法里添加

pw.println(host+"說:"+message); // 將消息發送給客戶端

Client start方法里//通過socket獲取輸入流,讀取服務端發送過來的消息

str=br.readLine(); // 讀取服務端發送過來的一行字符串

private PrintWriter[] allOut= {};
用來保存所有對應客戶端的輸出流,用於讓ClientHandler之間廣播消息使用
內部類是可以訪問外部類的屬性的,因此所有ClientHandler都可以看到它們的外部類Server
定義的屬性,因此這里定義的屬性可以作為它們共享數據的地方使用

將該輸出流存入allOut數組中,這樣每個ClientHandler實例都會做這樣的事,那么所有的ClientHandler
也都可以通過這個數組獲取到其他ClientHandler放進來的輸出流了,從而做到共享
//1.對allOut數組擴容
allOut=Arrays.copyOf(allOut, allOut.length+1);
//2.將當前pw存入到該數組最后一個位置
allOut[allOut.length-1]=pw;


java.util.Collection 接口
集合框架
集合是用來保存一組元素的,不同的實現類實現了不同數據結構
Collection是所有集合的頂級接口,規定了所有集合都必須具備的功能
集合與數組一樣,保存一組元素,但是操作元素的方法集合已經提供了

Collection下面有兩個常見的子接口(分類)
java.util.List:線性表,特點是可以存放重復元素,並且有序
java.util.Set:不可以重復的集合,大部分實現類是無序的
是否為重復元素是根據元素自身equals比較的結果判定的

boolean add(E e)
向集合中添加元素,若成功添加則返回值為true
只能添加引用數據類型數據

boolean isEmpty()
判斷當前集合是否為空集

int size();//返回當前集合的元素個數

c.clear();//清空集合

boolean contains(Object o)
判斷集合是否包含給定元素
contains的判斷依據是用給定的元素與集合每一個元素進行equals比較,只要有為true的,就認為包含,
因此元素的equals方法直接影響contains的判斷結果

c.remove(p);
刪除集合元素,remove方法刪除元素也是依靠元素的equals方法的

集合存放元素的引用

集合操作
boolean addAll(Collection c)
將給定集合中的所有元素添加到當前集合,當調用后當前集合元素發生改變則返回true

boolean containsAll(Collection c)
判斷當前集合是否包含給定集合中的所有元素

boolean removeAll(Collection c);
刪除當前集合中與給定集合的共有元素,給定的集合元素不發生變化

遍歷集合
集合提供了統一的遍歷操作,無論哪種數據結構
實現的集合都提供了該遍歷方式:迭代器模式

Iterator iterator()
Collection提供的iterator方法可以提供一個用於遍歷當前集合的迭代器

java.util.Iterator接口
該接口是迭代器接口,規定了遍歷集合的相關操作
所有集合都有一個用於遍歷自身的迭代器實現類,我們無需關注它們的類名,以多態的角度用該接口看待並調用相關遍歷方法即可

使用迭代器遍歷集合的統一方式遵循為:問->取->刪 (主要是問->取->問->取...)
其中刪除元素不是必須操作
問 boolean hasNext()
通過迭代器查看是否還有元素可以遍歷
取 E next()
通過迭代器遍歷下一個元素

it.remove(); // 迭代器提供了remove方法,該方法不需要傳入參數,刪除的就是本次遍歷通過next獲取的元素
迭代器有一個要求,就是遍歷的過程中不能通過集合的方法增刪元素,否則遍歷時會拋出異常

JDK5之后推出了一個特性:增強型for循環
也稱為新循環,它不取代傳統for循環的工作, 僅用來遍歷集合或數組使用

for(String str:array) {
System.out.println(str);
}
新循環是編譯器認可,而不是虛擬機,編譯器在編譯源代碼時會將新循環遍歷數組改為傳統for循環遍歷

for(Object o:c) {
String str=(String)o;
System.out.println(str);
}
使用新循環遍歷集合會被編譯器編譯時改為使用迭代器遍歷,因此要遵循迭代器使用規范,
遍歷過程中不能通過集合的方法增刪元素

泛型是JDK5之后推出的一個特性,又稱為參數化類型,允許我們在使用一個類時指定它的屬性、方法的參數和
返回值的類型,使得我們使用起來更靈活
泛型的原型是Object,不指定時就是用它
泛型在集合中廣泛使用,用於規定集合中的元素類型

Collection<String> c=new ArrayList<String>();// 規定當前集合元素為String,添加元素時只能傳入String類型元素
Iterator<String> it=c.iterator();// 迭代器的泛型與其遍歷的集合指定的泛型一致即可
while(it.hasNext()) {
String str=it.next();//不用強轉,直接用String類型接
System.out.println(str);
}

java.util.List 線性表
List集合是有個可以重復的集合,並且有序,特點是提供了一組通過下標操作元素的方法
常見實現類
java.util.ArrayList
內部使用數組實現,查詢性能更好,但是增刪元素性能差
java.util.LinkedList
內部使用鏈表實現,增刪元素性能好,尤其首尾增刪元素性能最佳,但是查詢性能差
如果對性能沒有特別苛刻的要求時,通常使用ArrayList即可

List<String> list=new ArrayList<>();//JDK7之后,右側的<>指定泛型部分可以不再寫具體的類型了
// 編譯器會理解為與前面指定的一致

E get(int index) // E是指泛型,前面指定為String,則E變為String
獲取指定下標處對應的元素

for(int i=0;i<list.size();i++) { // 傳統for循環可以遍歷List集合
str=list.get(i);
System.out.println(str);
}

E set(int index,E e)
將給定元素設置到指定位置上,返回值為原位置對應的元素,所以set是替換元素操作

List提供了一對重載的add,remove方法
void add(int index,E e) //將給定元素插入到指定位置
E remove(int index) //刪除並返回給定位置上的元素

List subList(int start,int end) //獲取當前集合中指定范圍的子集(含頭不含尾)
修改子集元素就是修改原集合對應元素

list.subList(2, 9).clear(); //刪除2-8位置的元素

Collection中定義了一個方法toArray,可以將當前集合轉換為數組
String[] array=c.toArray(new String[c.size()]); //new String[] 長度小於c.size(),方法會自動生成一個新的長度為c.size()
// 的數組,如果大於c.size(),后面的元素為null

數組轉換為List集合
數組的工具類Arrays提供了一個靜態方法asList,可以將給定的數組轉換為一個List集合
List<String> list=Arrays.asList(array);

list.add("five"); // 拋出異常 java.lang.UnsupportedOperationException
由於數組是定長的,因此會改變數組元素個數的操作都是不支持的會拋出異常

List<String> list2=new ArrayList<String>(list);
若希望對集合元素增刪操作,可以另外創建一個集合
所有的集合都支持一個參數為Collection類型的構造方法,作用是創建該集合的同時包含給定集合中的所有元素

List集合的排序
Collections.sort(list); //Collections是集合一個工具類,可以提供很多便於我們操作集合的方法
Collecitons.sort(list, new ByAge()); // 自定義排序方法
new ByAge()是一個比較器(Comparator),需要實現Comparator接口自定義排序方法

return o1.age-o2.age; //簡化ByAge類中的compare方法的方法體

利用匿名內部類實現比較器
Collections.sort(list,new Comparator<Person>() {
public int compare(Person o1,Person o2) {
return o1.age-o2.age;
}
});
課后任務:實現對一組連衣裙進行自定義排序,按照價格排序,尺碼排序

字符串大小比較問題
按照字符中的字符編碼進行比較
其規則是按照字符中的字符編碼進行比較
返回0表示相等
返回正數表示第一個字符串大
返回負數表示第一個字符串小
int n=s1.compareTo(s1); //用String類提供的compareTo方法比較,一個一個字符的編碼大小比較,第一個相同則比較下一個字符...


java集合API中的Map是映射的問題
Map是一個接口,其目的是為了解決高性能查找問題。
實現Map接口的類都封裝了高性能查找算法,利用Map接口的實現類就可以提高軟件的查找性能,提供優秀的用戶體驗
Map的實現類HashMap 是最快的查找算法 沒有之一!!!最常用
Map的實現類TreeMap是比較快的查找算法

key:關鍵信息
value:有價值的信息
Map的使用:
1.創建Map
2.將需要查詢的數據,按照key-value成對存儲到Map對象中,key是被檢索的關鍵字,value是被查找到的信息
3.查詢使用的時候,根據key查詢對應的value

特點:
1.key不能重復!value可以重復。添加時候如果key重復就替換原有的value
2.HashMap根據key檢索到value的速度非常快,和存儲容量無關

put(key, value)
put方法,將被查詢的數據成對的添加到map中,其中key是被檢索的關鍵字,value是檢索到的結果
將key-value成對的添加到map中,如果map中已經有key了,則替換原有的value返回被替換value。如果沒有替換返回null
String val=map.put("莫言", "檀香刑");//第一次,map中沒有“莫言”是添加操作 ,返回null,表示新添加
val=map.put("莫言","檀香刑,生死疲勞");//第二次,map中已經存在“莫言”,再次put方法的時候,
//是進行替換操作。返回被替換的value。key不能重復
get(key) //用於在map中根據key檢索value,返回值是檢索到value
如果沒有檢索到,返回null
由於map中允許value是null,所以檢索到value為空時候,也返回null

System.nanoTime(); //納秒,1毫秒=1000000納秒

containsKey(key)
檢查map中是否包含指定的key,如果包含返回true,不包含返回false

size() //返回key-value對的數量

isEmpty() //檢查集合是否為空
如果map集合中沒有數據則返回true,否則返回false

val=remove(key) //從集合中刪除key對應的value,返回刪除的value。如果key不存在不會產生任何結果(返回值為null)

clear() //清空集合

Map集合的遍歷、迭代
import java.util.Map.Entry; //手動導入Entry類型。 Entry 條目,是一個key-value對
Set<Entry<String,String>> set=map.entrySet(); //利用map.entrySet()方法將map轉換為set集合,再對set進行遍歷
此set集合中的元素類型是Entry
如何遍歷一個map
1.導入Entry類型
2.利用entrySet()將map存儲到一個set集合,其中set中每個元素是一個Entry
3.對set集合進行遍歷處理,遍歷時候set的元素是Entry類型
4.每個entry對象包含兩個方法,getKey()獲取遍歷時候的key,getValue()獲取遍歷時候的value

hashMap==散列表==哈希表
Map中的key的類型,Value類
1.Value可以是任何引用類型
Integer、String、List、int[].....
2.key的類型,引用類型,要求必須很好的成對重寫hashCode和equals
java API一般都很好地重寫了這兩個方法,如:String,Integer等
3.如果不很好的成對重寫key的hashCode和equals方法,會造成散列表工作異常!
4.如果使用String作為key,總是沒有問題的!
5.開發工具提供了自動成對重寫hashCode和equals方法的功能


8.9
XML
可擴展的標記語言

為何要有XML
1.數據文件內容沒有統一標准,每個廠商都不同
2.W3C組織就設計了XML統一了數據文件標准,得到了業界廣泛認同!
3.XML具有可擴展性,可以支持任何數據
4.XML還具有統一的訪問API,可以大大簡化編程,使用方便
5.XML格式復雜,臃腫,有些場合已經使用JSON格式代替

XML基本語法
標記 tag (標簽)
語法:
<標記名> 開始標記
</標記名> 結束標記
<標記名/> 自結束標記

例子
<book>
</book>
<date/> 等價於 <date> </date>

規則
1.標記名區別大小寫
2.標記名可以任意書寫,也就是標記名可以擴展
3.可以使用中文標記名,但是不建議使用中文
4.XML文件只能有唯一的一個根標記
5.標記必須配對使用,自結束標記相當於一對標記
6.標記必須合理嵌套,不能交叉嵌套
7.標記的嵌套關系可以任意擴展

內容(Content)
開始標記和結束標記中間的部分稱為內容
1.可以是文本內容
2.可以是標記
3.可以是標記和文本的混合
元素
開始標記+內容+結束標記
1.根標記和全部內容稱為根元素,根元素只有一個
2.元素內容中的元素稱為子元素
3.沒有內容的元素稱為空元素
4.當前元素的上一層元素稱為父元素

屬性 Attribute
1.在元素的開始標記上聲明屬性
2.屬性屬於元素
3.屬性的名不能重復
4.屬性沒有順序
5.寫法 name="value" ,其中value必須寫引號
6.屬性的名稱和個數可以擴展
7.注意:如果有id屬性,一般id的值是不同的,並且以字母為開頭

實體(Entity)替換
實體相當於java的轉義字符(XML沒有轉義字符的概念!!!用來理解而已)
特殊字符需要使用“實體”進行替換
< 替換為 &lt;
> 替換為 &gt;
& 替換為 &amp;
......

CDATA
CDATA內部的特殊符號無需進行實體替換
語法:
<![CDATA[無需替換的內容]]>
CDATA不能嵌套使用

XML文件讀取解析

什么是dom4j
1.dom4j 是第三方開源XML API
2.利用這個API可以以非常簡潔的方式訪問XML文件
3.dom4j底層依賴於文件流

使用Dom4j
1.導入Dom4j包
2.使用Dom4j API讀取XML文件
3.利用SAXReader讀取book.xml,如果讀取成功就會創建一個dom對象,其結構是樹形的。
如果讀取失敗則拋出異常:xml格式,文件找不到

Document doc=reader.read(file);
System.out.println(doc.asXML());// 檢查讀取結果

Maven的pom.xml配置
<!-- dependencies依賴 -->
<dependencies>
<!-- dom4j的坐標 -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>

Dom4j提供了非常豐富方便的API
Element root=doc.getRootElement();//找到根元素,作為訪問入口
List<Element> list=root.elements();//獲取全部子元素API
for(Element e:list) {
System.out.println(e.asXML());//用新循環輸出子元素
}
list=root.elements("book");//查找指定的全部子元素
Element n=e.element("name");//找到元素中的第一個符合條件的子元素
String name=n.getTextTrim();//獲得元素的文本數據
String id=e.attributeValue("id");//讀取元素的屬性,沒有元素則返回null
1.從根元素作為入口
Element root=doc.getRootElement();
2.搜索全部子元素,獲得元素的全部子元素
List<Element> list=element.elements();
3.搜索指定名字的全部子元素,獲得一個元素列表
List<Element> list=element.elements("元素名");
4.搜索第一個指定名字的子元素,一般用在子元素名唯一的時候,當子元素不存在的時候,返回null
Element e=element.element("元素名");
5.獲取元素中的文本
String str=element.getText();
String str=element.getTextTrim(); //去除空白,常用!
6.可以直接獲取子元素中的文本
String str=element.elementTextTrim("子元素名");
等價於
String str=element.element("子元素名").getTextTrim();
7.獲取元素中的屬性
String value=element.attributeValue("屬性名");

回顧:
1.XML語法:注釋、標記(標簽)、內容、元素、屬性、實體、CDATA
2.XML解析:Dom4j、W3c DOM、SAX、Jdom、XML Stream
-編碼最便捷的是Dom4j 網站 https://dom4j.github.io/

 

WebServer是模擬TomCat的一個web容器
web容器可以同時管理多個網絡應用,並且提供了與客戶端(通常是瀏覽器)的網絡連接以及傳輸數據
和與客戶端的應用層交互(涉及到TCP協議以及HTTP協議)上面的支持。有了web容器,使得程序員
更多的精力是放在具體web應用的業務上
webapp(網絡應用):它包含的內容大致有網頁,圖片,其它靜態素材以及java程序代碼,就是我們上網
時俗稱的一個“網站”的全部內容

TCP協議,處於傳輸層,負責兩台計算機之間通過網絡傳輸數據的協議
HTTP協議,處於應用層,規定了雙方發送數據的格式,以及交互規則

URL 統一資源定位
http:// doc.tedu.cn /maven/index.html
協議名稱 計算機地址信息(host) 抽象路徑

ClientHandler線程
該線程負責處理與指定客戶端的交互工作,處理過程分為三步:
1:准備工作
2:處理請求
3:發送響應

WebServer start() 啟動一個線程處理該客戶端交互

測試讀取一行字符串的操作
客戶端連接后會主動發送一個HTTP請求(request),而一個請求中的請求行和消息頭部分有一個
特點,都是以行為單位的字符串,結尾為CRLF。因此我們在做解析請求之前,先測試一個功能,讀取
一行字符串
實現:
1:添加ClientHandler類,與聊天室一樣,當一個客戶端連接后啟動一個線程處理該客戶端(這樣
才能支持同時多個客戶端的連接)
2:ClientHandler中通過socket獲取輸入流后,測試讀取一行字符串並將其輸出到控制台

開始解析請求
一個HTTP的請求包含三部分,請求行,消息頭,消息正文
因此我們設計一個類HttpRequest,用這個類的每一個實例表示客戶端發送過來的一個具體請求內容
所有請求的部分都設計對應的屬性來保存對應值
實現:
1.創建一個包:com.webserver.http
這個包保存所有與HTTP協議有關的類
2.在http包中定義類:HttpRequest
定義屬性對應請求中各項內容
3.定義構造方法用於初始化HttpRequest
初始化的過程就是讀取客戶端發送的請求內容並進行解析的過程
4.在ClientHandler的第一步“准備工作”里實例化HttpRequest
完成請求的解析

HttpRequest類
請求對象
該類的每一個實例用於表示客戶端發送過來的一個具體的請求內容
一個請求由三部分組成:請求行,消息頭,消息正文

三個String變量保存//請求行相關信息
用Map保存//消息頭相關信息
構造方法,用於初始化請求對象//消息正文相關信息
定義三個方法 解析請求行 解析消息頭 解析消息正文

parseHeaders方法
循環調用readLine方法讀取每一行字符串,每一行就是一個消息頭,如果readLine方法
返回的字符串時一個空字符串時就應當停止循環讀取操作了(因為單獨讀取到了CRLF)
讀取到每一個消息頭后,我們可以按照": "(即:冒號空格)來進行拆分,將消息頭的名字
作為key,消息頭的值作為value保存到header這個Map中完成消息頭的解析工作

ClientHander應當根據request對象中用戶請求的資源的抽象路徑(uri屬性的值)來判斷該資源是否存在
如果存在,我們將在下一步響應客戶端時將該資源回復.

因此,我們要先准備一些必要的資源,如頁面:
首先我們在項目目錄下新建一個名為webapps的目錄用於保存我們這個web容器下每一個具體的網絡應用,
每個網絡應用以一個子目錄的形式保存,目錄名就是這個網絡應用的名字,然后將該網絡應用下的內容存於
該目錄里(頁面/圖片等等)

實現:
1.在當前項目目錄下新建目錄:webapps用於保存所有的網絡應用
2.在webapps目錄下新建一個網絡應用(一個目錄),起名為myweb
3.在myweb目錄下新建一個頁面index.html
4.在ClientHandler中完成第二步處理請求,首先通過request獲取抽象路徑,然后從webapps
目錄下根據該抽象路徑尋找資源並定義分支判斷然后分別打樁

完成后,在另一個分支中(資源不存在)完成響應客戶端404頁面的操作
實現:
1.在webapps目錄下新建一個子目錄root
2.在root目錄下新建一個404.html頁面,不創建在myweb目錄下是因為無論哪個應用只要
用戶請求的資源不存在都要響應404,所以這個頁面應當是所有網絡應用公用的頁面
3.在404.html中居中顯示一行字:
404,資源不存在!
4.在ClientHandler分支中沒有該資源,響應404頁面給客戶端
其中狀態代碼為404,狀態描述為NOT FOUND
響應頭還是Content-Type與Content-Length
響應正文為: ./webapps/root/404.html頁面內容

v6 本版本重構響應
上個版本中,我們在ClientHandler中處理請求后的分支中都是直接將發送響應的具體細節寫在這里
這里造成了代碼的重復,因此本版本我們先要重用發送響應的代碼
與請求設計思路一致,在定義一個類:HttpResponse,用它的每一個實例表示一個具體的響應對象,
設置要給客戶端發送的具體信息后將響應對象以一個標准的響應格式發送給客戶端

響應對象
該類的每一個實例用於表示發送給客戶端的一個具體的響應內容
每個響應包含三部分內容:狀態行,響應頭,響應正文

頁面上在使用路徑指定某個資源時,我們通常也可以使用相對路徑,
而在頁面中"./"當前目錄會被瀏覽器理解為當前頁面所在的目錄。
比如,我們訪問此頁面路徑為:
http://localhost:8088/myweb/index.html
那么瀏覽器認為"./"是:
http://localhost:8088/myweb/
因此下面的圖片指定路徑"./logo.png",那么它的實際路徑就是:
http://localhost:8088/myweb/logo.png

本版本繼續重構響應

上個版本中,我們已經將響應以HttpRequest對象形式表示並利用flush發送給客戶端。並且在主類
WebServer中循環接收客戶端的連接並進行了處理

本版本導入了doc.tedu.cn網站上提供的學子商城項目后發現問題:頁面無法正常響應所有資源,原因在於
無論瀏覽器請求頁面中的任何資源(css,js,png等)我們服務端在響應該資源時在響應頭Content-Type指明
資源類型時都是發送固定的text/html,這使得瀏覽器無法正確理解其請求的資源出現無法正確顯示的情況

本版本先解決HttpResponse發送響應頭時固定發送兩個頭:Content-Type和Content-Length,改為
根據設置的響應頭進行發送

實現思路:
在HttpReponse中添加一個Map類型的屬性,用於保存所有要發送給客戶端的響應頭
然后在發送響應頭的方法中遍歷這個Map將所有的響應頭發送給客戶端
這樣,將來在發送前,我們可以通過處理結果來向相應對象中設置要發送的響應頭來進行發送

本版本繼續重構響應

本版本要根據實際客戶端請求的資源響應對應的類型來達到讓客戶端能
正確顯示出學子商場頁面及以后所有的頁面內容

常見的資源類型與Content-Type值的對應關系
html text/html
css text/css
png image/png
jpg image/jpeg
gif image/gif
js application/javascript

本版本繼續重構響應

上個版本中我們在ClientHandler處理請求的過程中創建了一個Map保存了所有的資源后綴
和對應的Content-Type的值,並且根據資源的后綴獲取到對應的該值的信息來設置響應頭
從而實現了正常顯示學子商場這種有較多資源的頁面
但是,這種實現方法會導致每次請求一個資源都要創建這個Map,而這個Map實際要有1000
多種資源對應並且內容是固定的,因此我們不應當每次處理時都創建一份,將其定義為靜態內容存
一份即可

實現:
1.在com.webserver.http包中定義一個類:HttpContext,使用這個類保存所有HTTP協議規定的內容
2.在HttpContext中定義常量:static Map<String,String> mimeMapping
3.完成相關初始化工作並對外提供get方法,可以根據后綴名獲取對應的Content-Type的值
解析conf/web.xml文件
將根標簽下所有名為<mime-mapping>的子標簽獲取出來,並將它下面的:
<extension>標簽中的文本作為key
<mime-type>標簽中的文本作為value
初始化mimeMapping這個Map,初始化完畢后,mimeMapping這個Map中應當有1000多個元素

引入tomcat整理的所有資源類型與Content-Type對應值的xml文件用於初始化mimeMapping。
在項目目錄下新建conf,並導入web.xml文件
然后在HttpContext的初始化方法initMimeMapping中通過解析該xml文件完成初始化

現在ClientHandler還存在一個小問題,當我們設置響應對象時,設置響應正文的同時還要設置
兩個用於說明正文長度和類型的Content-Length,Content-Type,我們完全可以將設置兩個響應頭的工作
放到設置響應正文的方法中,這樣我們在設置響應正文時就不用每次都為其添加這兩個響應頭了

本版本開始處理業務

以用戶注冊操作為例,來實現。

正常的注冊流程:
1.用戶請求注冊頁面
2.在注冊頁面輸入注冊信息並點擊注冊按鈕
3.數據提交到服務端
4.服務端根據提交的數據保存該注冊信息
5.服務端響應注冊結果頁面給客戶端

實現:
1.在webapps/myweb目錄下新建一個頁面reg.html並定義表單,提交路徑
action="./reg" method="get"並在表單中定義注冊信息對應的輸入框
2.當該表單提交后,我們發現,get方式提交表單時所有輸入框中的值是拼接帶URL
中的,以"?"隔開每個參數再以"&"分割傳遞。因此我們在解析請求時要針對請求行中的
抽象路徑部分進一步解析。
首先在HttpRequest類中添加三個新的屬性
String requestURI:記錄uri中請求部分(?左側內容)
String queryString:記錄參數部分(?右側內容)
Map parameter:保存參數部分中的每一個參數

首先在解析之前,要先判斷當前uri是否含有參數,判斷的依據是看uri當中是否含有"?",有則說明含有參數,否則就是沒有參數
如果沒有參數,那么直接將uri的值賦值給requestURI即可;
若有參數,則應當先按照"?"將uri拆分然后將"?"左側內容賦值給requestURI,將"?"右側內容賦值給queryString;
接着將參數部分在按照"&"拆分為每一組參數,每組參數再按照"="拆分為參數名與參數值,並將名字作為key,值作為value
保存到parameter這個Map中完成解析;

上一個版本中,我們已經完成了在HttpRequest中解析客戶端提交數據的操作了。本版本開始來
根據用戶的請求及提交的數據完成對應的業務處理
1.通過request獲取用戶提交的注冊信息
2.將信息寫入user.dat文件保存
3.設置response響應注冊結果頁面

實現
1.首先創建一個新的包:com.webserver.servlet
2.在這個包中定義用於處理注冊的類:RegServlet
3.在RegServlet中定義service方法用於處理業務
4.ClientHandler在處理業務的分支中實例化RegServlet並調用其service方法處理注冊業務
5.在webapps/myweb目錄下新建一個頁面reg_succecc.html的注冊信息,該頁面用於提示
用戶注冊成功。
6.service方法中首先通過request獲取用戶表單提交的注冊信息,然后打開RandomAccessFile,
將該條記錄寫入user.dat文件中,寫入后設置response對象響應注冊成功頁面

本版本補全注冊業務操作

本版本將注冊中驗證用戶是否已經存在的工作完成
實際上網中,通常注冊時用戶名是不允許重復的,因此我們也要做該驗證

實現:
1.首先在webapps/myweb目錄下新建一個頁面:
hava_user.html,該頁面提示一行字:該用戶已存在,請重新注冊
並在下方添加一個超鏈接點擊后返回到注冊頁面
2.在RegServlet的service方法中,當通過request獲取到用戶注冊信息(用戶名,密碼
昵稱,年齡)並創建RandomAccessFile后:
首先應當循環讀取user.dat文件中現有的每條記錄然后比對每條記錄的用戶名是否是
當前提交的注冊用戶的名字,若是則說明該用戶已存在,直接設置response響應
have_user.html即可
若讀取完user.dat文件並且不存在重名的,則執行原有的邏輯將該記錄寫入user.dat文件

本版本解決地址欄傳遞中文的問題

上個版本中我們完成了用戶注冊功能,但是如果輸入框中輸入了中文字符,會出現地址欄
提交數據如:/myweb/reg?username=%E8%8C%83%xxx=xxx.....
而不是直接看到:/myweb/reg?username=范=。。。

這是因為抽象路徑部分是包含在請求的請求行當中的,而HTTP協議要求請求的文本部分
只能是ISO8859字符集中的內容(歐洲字符集,不支持中文)

因此解決辦法為:
首先將中文這樣的字符按照指定編碼轉換為字節,2進制的內容是由1,0兩個數字,這兩個
是ISO8859中支持的字符,但是一個字節就由8位2進制組成,內容又太長,因此我們再將
2進制每個字節的8位改為用2位16進制表示。為了和實際的英文數字區分開,16進制的
內容前面用"%"標識
因此有了"%XX"這樣表示1字節的內容了。
例如:
"范"用UTF-8轉換為2進制后內容為:
11101000 10001100 10000011
用16進制表示后內容為:
%E8%8C%83

queryString=URLDecoder.decode(queryString, "UTF-8");//對queryString中包含的%XX進行轉碼

本版本解決空請求問題

HTTP協議中對此有說明,允許客戶端發送空請求。即:客戶端連接服務端后
沒有發送一個標准的HTTP請求過來。
但是這樣導致的后果是我們在解析請求時讀取第一行字符串后解析請求行就會出現
拆分得到數據下標越界。並且客戶端發送空請求這個操作完畢后就會與服務端
斷開連接,如果按照我們正常處理的流程,最終要發送響應給客戶端,測試還會
出現Socket異常

思路:
在解析請求行時若發現本次請求是空請求時,我們就對外拋出一個空請求異常(自
定義的異常),然后再通過HttpRequest的構造方法將該異常拋出給ClientHandler,
使得它可以忽略后續所有處理工作

此版本支持post請求提交數據

注冊頁面中我們將form表單的提交方式改為:post
此時再次提交注冊信息會發現用戶輸入的信息不再
顯示在URL當中,而是會被包含在請求的消息正文
部分.因此我們在HttpRequest要請求中提供的消息
正文內容才可以得到.

完成用戶登錄操作

用戶請求登錄頁面,然后頁面上輸入用戶名和密碼,點擊登錄按鈕后提交登錄操作。之后
看到登錄成功或失敗的頁面

實現:
1.在webapps/myweb目錄下准備對應的頁面
1.1 login.html,登錄頁面表單action指定提交路徑:action="./login"
1.2 login_success.html,登錄成功提示頁面。顯示一行字:登錄成功,歡迎回來
1.3 login_fail.html,登錄失敗提示頁面,顯示一行字:登錄失敗,用戶名或密碼不正確

2.在com.webserver.servlet中添加用於處理登錄業務的類:LoginServlet,並定義service方法

3.修改ClientHandler,在第二步處理請求的環節,當判斷path的值不是注冊業務下面添加一個
新的分支,判斷是否為登錄的值:/myweb/login,如果是,則實例化LoginServlet並調用
其service方法處理登錄

4.完成service方法的邏輯
4.1:首先通過request獲取用戶登錄頁面上表單提交的登錄信息(用戶名和密碼)
4.2:使用RandomAccessFile讀取user.dat文件並逐條比對用戶名和密碼,如果比對成功
則設置response響應登錄成功,如果密碼有誤或者user.dat文件中沒有該用戶則響應登錄失敗頁面

重構操作

我們定義一個HttpServlet,作為所有Servlet的超類在這里定義一個抽象方法:service,這樣
就要求所有的子類必須要有這個方法來處理業務了。
然后再將一些重復的代碼提取到這里定義為方法,使所有子類都可以復用,比如轉跳頁面的操作

java反射機制
java反射機制是一個動態的機制,允許我們在程序的運行過程中通過字符串來指揮程序實例化,操作屬性,調用方法等。
這使得代碼提高了靈活度,但是反射機制會帶來更多的資源開銷和較慢的運行效率(相較於硬編碼)。
程序不應當過於依賴反射,它應當只是起到畫龍點睛的作用,在合適的時候使用

Class類
Class類稱為類對象,它的每一個實例表示JVM加載的一個類,並且JVM內部每個被加載的類都有且只有唯一一個Class實例與之對應
通過類對象我們可以得到其表示的類的一切信息,便於我們在程序運行期間來通過它操作其表示的類

獲取一個類的類對象有三種方式:
1.調用該類的靜態屬性class,每個類都有,獲取的就是該類的類對象,基本類型也有
2.通過Class的靜態方法forName,該方法可以指定要獲取的類的名字從而得到該類的類對象
3.通過類加載器ClassLoader得到

Class獲取其表示的類的方法有兩種
getMethods():獲取這個類所有方法(包含繼承的)
getDeclaredMethods():僅獲取本類自己定義的

通過反射機制進行實例化對象
1.加載要實例化的類的類對象
2.通過類對象的newInstance()方法實例化
newInstance方法時調用無參構造器實例化的,因此要求Class表示的類必須有無參構造器才可以

通過反射機制調用方法
1、加載要操作的類的類對象
2、利用類對象實例化其表示的類的實例
3、通過類對象獲取要調用的方法
4、調用該方法

Method method2=cls.getMethod("sayHello", String.class,int.class);//調用有參

JDK5之后推出了一個特性:變長參數
變長參數的實際類型為數組,而且變長參數只能是當前方法的最后一個參數,且只能定義一個
public static void test(int a,String... s) {
System.out.println(s.length);
}

//強制訪問該方法
method.setAccessible(true);
method.invoke(o);//反射就可以調用私有方法了

解析conf/servlets.xml文件
將根標簽下所有的<servlet>標簽獲取到並且將每個<servlet>標簽中的屬性:path的值作為key
className的值利用反射加載對應的類並實例化,將實例化的對象作為value存入到servletMapping完成初始化

線程池
線程池是管理線程的一套解決方案,主要作用:
1.控制線程數量
線程數量過多會導致過多的資源消耗,並且會導致CPU過度切換降低整體並發性能
2.重用線程
線程不應當隨着任務的生命周期一致,頻繁的創建和銷毀線程會給系統帶來額外的開銷

ExecutorService threadPool=Executors.newFixedThreadPool(3);//創建一個固定大小的線程池
threadPool.execute(r);//執行線程
threadPool.shutdown();//停止線程池,等待目前的任務做完就停止
threadPool.shutdownNow();//立即停止線程池,不等待目前的任務做完就停止


java.util.Date
Date的每一個實例用於表示一個確切的時間(精度為毫秒)
內部維護一個long值,該值表示的自1970年1月1日00:00:00到當前Date表示的時間之間經過的毫秒
Date存在時區等設計缺陷,因此大部分方法都被聲明為過時的在以后的開發中不應當再使用

long getTime()
獲取Date內部維護的毫秒值

java.text.SimpleDateFormat
該類可以將Date和String之間按照指定的格式進行相互轉換

SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//按指定格式 yyyy-MM-dd HH:mm:ss
String format(Date date)
將給定的Date對象按照當前SimpleDateFormat指定的日期格式轉換為字符串

Date date = sdf.parse(str);//將一個字符串解析為Date對象

java.util.Calendar 日歷類
Calendar calendar = Calendar.getInstance();
Calendar是一個抽象類,規定了日歷類操作時間的一系列方法
可以使用其提供的靜態方法:getInstance來獲取一個當前系統所在地區適用的實現類,大部分地區獲取
回來的是GregorianCalendar,即:陽歷實現類

Calendar提供的方法:
Date getTime()
可以將當前Calendar表示的日期以一個Date實例形式返回

void setTime(Date date)
調整當前Calendar使其表示給定的Date所表示的日期

int get(int field)
獲取當前Calendar表示的日期中指定時間分量所對應的值不同的時間分量用不同的整數表示,無需記憶,Calendar提供了大量的常量與之對應
Calendar calendar=Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month=calendar.get(Calendar.MONTH)+1;//月需要注意從0開始,即:0表示1月,1表示2月
int day=calendar.get(Calendar.DAY_OF_MONTH);
和"天"相關的時間分量有:
DAY_OF_MONTH:月中的天
DAY_OF_WEEK:周中的天
DAY_OF_YEAR:年中的天
DATE:月中的天,與DAY_OF_MONTH一致
int h=calendar.get(Calendar.HOUR_OF_DAY);//24小時制的時
int m=calendar.get(Calendar.MINUTE);
int s=calendar.get(Calendar.SECOND);
//按照美國的習慣,一周的第一天是周日
days=calendar.get(Calendar.DAY_OF_WEEK)-1;
String[] date= {"日","一","二","三","四","五","六"};
System.out.println("今天是周"+date[days]);
int d=calendar.getActualMaximum(Calendar.DAY_OF_YEAR);//獲取指定時間分量所允許的最大值


void set(int field,int value)
調整當前Calendar指定時間分量為給定的值
//調整年為2008
calendar.set(Calendar.YEAR, 2008);
//調整月為8月
calendar.set(Calendar.MONTH, Calendar.AUGUST);
//調整為8號
calendar.set(Calendar.DATE, 8);

void add(int field,int amount)
對指定的時間分量加上給定的值,若給定的值為負數則是減去

//查看3年2個月零25天以后那周的周三是哪天?
calendar.add(Calendar.YEAR, 3);
calendar.add(Calendar.MONTH, 2);
calendar.add(Calendar.DAY_OF_YEAR, 25);
calendar.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY);


lambda表達式 JDK8之后推出的一個特性
lambda可以用更簡短的方式創建匿名內部類。該語法使得我們可以以"函數式編程"
lambda創建匿名內部類時實現的接口必須只能有一個抽象方法,否則不可以使用

語法:
(參數列表)->{
方法體
}
Runnable r2=()->{
System.out.println("hello");
};
//如果只有一行代碼,{}可以省略
Runnable r3=()->System.out.println("hello");

//方法含有參數時使用lambda,lambda中參數是不需要指定類型的,編譯器會根據代碼分析出類型
Comparator<String> c=(o1,o2)->{
return o1.length()-o2.length();
};

//如果方法最終使用return關鍵字進行返回,那么當可以忽略"{}"時,return關鍵字也要一同忽略
Comparator<String> c=(o1,o2)->o1.length()-o2.length();


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM