Java基礎復習計劃(三)
散碎知識點
Math.round() 方法進行四舍五入計算,實現是:
Math.floor(a + 0.5f)
floor : 意為地板,指向下取整,返回不大於它的最大整數
ceil : 意為天花板,指向上取整,返回不小於它的最小整數
round : 意為大約,表示“四舍五入”,而四舍五入是往大數方向入.關於方法區溢出:
經常動態生成大量 Class 的應用中,Spring、hibernate 對類進行增強的時候使用 CGLib 類字節碼技術 ,其他運行在 JVM 的動態語言;
常見的還有大量 JSP 或動態產生 JSP 文件的應用(JSP 第一次運行時需要編譯)public Method[] getDeclaredMethods()
public Method[] getMethods()
返回類的所有 public 方法,包括其繼承類的公用方法,當然也包括它所實現接口的方法。關於類加載器的簡要分類:
引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的。
擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。
系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。
tomcat 為每個 App 創建一個 Loader,裏面保存著此 WebApp 的 ClassLoader。需要加載 WebApp 下的類時,就取出 ClassLoader 來使用。方法內不能使用 static 修飾變量
線程
線程的五大狀態:
- 新生(Born)
- 就緒(Runnable)
- 運行(Running)
- 消亡(Dead)
- 阻塞(Blocking)
創建線程的方式:
- extends Thread
- implements Runnable
控制線程的幾種常見方法:
- setPriority(int)
設置線程的優先級,可選範圍 1- 10,默認為 5,越大優先級越高;
沒什麽意義,因為只是概率而已 - static sleep(long)
- join()
當前線程邀請另一個線程優先執行,比如主線程裏寫xx.join();
意思就是主線程讓 xx 線程執行完成後再執行,否則一直處於阻塞狀態。 - static yield()
讓當前線程放棄持有的時間片,直接回到就緒,當然也有可能出現放棄時間片後又被 Cpu 選中的情況。 - setName() + getName()
- static activeCount()
得到程序中所有活躍線程的總數,活躍線程:就緒 + 運行 + 阻塞;
這個方法永遠不可能返回 0,至少是 1. - static currentThread()
得到當前線程對象,比如獲得主線程的對象,在 run 方法調用的其他方法中使用;
在 run 方法中沒必要,直接 this 就是了。 - setDaemon(true)
設置成為守護進程,當程序中只剩下守護線程時會自動終結自己;
Java 中著名的守護線程 GC,一般的特性:
- 通常是無限循環的
- 守護線程一般有極低的優先級
- 設置守護線程必須在 start 之前
- interrupt()
中斷線程的阻塞狀態,比如 sleep 時間還沒到可以用 interrupt() 強制喚醒,但是會拋出一個異常。
線程中所有靜態方法不關註誰調用的,而是關註出現在哪裏,出現在哪裏就是操作那個線程。
線程中所有涉及主動進入阻塞狀態的方法都需要進行異常處理
關於鎖
鎖的出現就是為了解決並發錯誤,當多個線程共享同一個對象的時候,某一個線程未處理完成時 CPU 時間片就用盡了,然後就會出現並發錯誤。
然後就需要加鎖來保證不會出現錯誤,通常有兩種方案:
- 使用 synchronized 關鍵字
叫做互斥鎖,或者互斥鎖標記,它可以修飾方法或者代碼塊,用在代碼塊上要顯式的聲明鎖,用在方法上默認是 this。
還有就是 synchronized 特性本身不會被繼承 - java.util.concurrent.locks.ReentrantLock
可翻譯為可重用鎖,JDK1.5 加入的,遵循了 OO 思想,有兩個方法:lock() 和 unlock()
鎖如果使用不當就會形成死鎖,要解決死鎖一般需要用到 Object 的三個方法:
- wait()
讓當前線程放棄已持有的鎖標記,並且進入調用方對象的等待池。 - notify()
喚醒調用方對象中等待池中的某個線程,是隨機的。 - notifyAll()
喚醒調用方對象中等待池中的全部線程
這三個方法都必須在已經持有鎖標記的前提下才能使用,所以它們都必須出現在synchronized(){當中}
鎖池和等待池
利用每一個對象都有一個鎖旗標,擁有這個旗標後才可以訪問此對象的資源,當線程無法獲取此對象的鎖旗標時就會發生阻塞進入此對象的鎖池,等待旗標的釋放,當釋放後所有的等待旗標的線程會被喚醒,進入就緒狀態爭奪旗標。
使用 wait 會進入等待池,遇到 synchrnized 會進入鎖池;
進入等待池會釋放當時持有的鎖,而鎖池不會;
鎖池中,只要鎖標記再度可用 線程自動離開,等待池 必須要 notify() 或者 notifyAll();
關於離開的去向:離開鎖池前往就緒;離開等待池前往鎖池(之前釋放了鎖,必須得重新獲取鎖,既然有人喚醒它,說明此時旗標肯定在別人手裏)
關於線程池
關於這一塊,之前在 這裏 已經寫過了,這次提提怎麽使用就夠了:
常規使用,用 ExecutorService 這個接口來寫吧,以及線程的幾種定義方式:
public class TestThreadPool{
public static void main(String[] args) throws Exception{
ExecutorService es = Executors.newFixedThreadPool(2);
// newCachedThreadPool();
// newSingleThreadExecutor()
ThreadOne t1 = new ThreadOne();
es.submit(t1);
ThreadTwo t2 = new ThreadTwo();
es.submit(t2);
ThreadThree t3 = new ThreadThree();
Future<String> f = es.submit(t3);
System.out.println(f.get());
//es.shutdown();
es.shutdownNow();
}
}
class ThreadThree implements Callable<String>{//JDK5.0
@Override
public String call() throws Exception{
for(int i = 0;i<6666;i++){
System.out.println("我是第三種方式");
}
return "End";
}
}
class ThreadTwo implements Runnable{
@Override
public void run(){
for(int i = 0;i<6666;i++){
System.out.println("我是第二種方式");
}
}
}
class ThreadOne extends Thread{
@Override
public void run(){
for(int i = 0;i<6666;i++){
System.out.println("我是第一種方式");
}
}
}
線程池的創建官方推薦使用 Executors 來創建,常見的有 newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor;
第一種就是最簡單的,也是最常用的,會事先維護幾個線程,等任務來直接執行;
第二種是當有任務的時候再創建線程(避免浪費),任務執行完後會等待一分鐘(默認),如果一分鐘內沒有新任務,那麽此線程就會被終結;
第三種是同一時間只允許一個線程執行,其他的任務都排隊等著,適合用在秒殺的情況。
shutdown 和 shutdownNow 的區別:
shutdown :不再接受新的任務,當線程池中的任務(包括等待中的和正在執行的)執行完畢後銷毀線程池。
shutdownNow:試圖停止所有正在執行的活動任務(一般情況正在執行的任務都會正常跑完的),暫停處理正在等待的任務,不再接受新任務,並返回等待執行的任務列表。
還有就是通過實現 Callable<>
的 call 方法來定義線程,這種方式定義的線程解決了其他方式無法實現的問題:
- run() 被定義為 void 無法返回數據
- run() 沒有任何 throws 聲明
這種方式定義的線程,只能通過線程池的方式來啟動,並且 submit 的時候會返回一個 Future 對象,利用這個對象可以獲得線程的返回值,就是調用其 get 方法,註意:當線程未執行完時,此方法一直是阻塞狀態;當線程被意外終止那麽 get 方法可能會一直卡在阻塞中(當然有重載可以指定等待的最大時間)。
IO
這就是通常我們所說的 IO 流了,流可分為三類:
- 方向分: 輸入流 or 輸出流
- 單位分: 字節流 or 字符流
- 功能分: 節點流 or 過濾流(包裝流、處理流)
File對象
創建 File 對象的三種常見形式:
new File(String 完整路徑);
new File(String 父目錄,String 文件名);
new File(File 父目錄對象,String 文件名);
然後介紹常用的幾個方法:
static listRoots()
: 得到當前計算機所有根目錄String[] list()
: 動態的列出一個目錄當中所有的文件名字File[] listFiles()
: 動態的列出一個目錄當中所有的文件對象exists()
: 判斷 File 對象指代的文件或者目錄是否存在isFile()
: 判斷 File 對象指代的是不是一個文件isDirectory()
: 判斷 File 對象指代的是不是一個目錄length()
: 得到文件的字節個數;只能對文件調用,對目錄調用得到的沒有意義mkdirs()
: 創建多層不存在的目錄結構getName()
: 得到文件或者目錄的名字getParent()
: 得到文件或者目錄的父目錄getAbsolutePath()
: 得到文件或者目錄的絕對路徑setLastModified()
: 設置文件的最後一次修改時間,設置的是時間戳lastModified()
: 得到文件的最後一次修改時間delete()
: 刪除目錄或者文件
如果要刪除的是一個目錄 則必須保證目錄是空的renameTo()
: 重命名文件或者目錄
例如:a.renameTo(c);
a 代表源文件,必須存在;c 代表目標文件,必須不存在;
其中 a 和 c 可以是不同的目錄結構,從而實現剪切。
過濾器
使用 File 的 listFiles() 方法的時候可以傳入一個文件過濾器(FileFilter),用來過濾指定的文件,這樣能減輕接下來遍歷的壓力。
FileFilter 是個接口,並且它只定義了一個方法:boolean accept(File pathname)
比如:
class JavaFilter implements FileFilter{
private JavaFilter(){}
private static JavaFilter jf = new JavaFilter();
public static JavaFilter getFilter(){
return jf;
}
@Override
public boolean accept(File f){
return f.isFile() && f.getName().toLowerCase().endsWith(".java");
}
}
class DirFilter implements FileFilter{
private DirFilter(){}
private static DirFilter df = new DirFilter();
public static DirFilter getFilter(){
return df;
}
@Override
public boolean accept(File f){
return f.isDirectory();
}
}
// 使用 lambda
File[] files = file.listFiles(f -> f.isFile() && f.getName().endsWith(".java"));
File[] dirs = file.listFiles(f -> f.isDirectory());
if(files == null) return;
一個來過濾 Java 文件,一個來過濾目錄,這裏使用單例模式就比較適合了,另外,還可以直接使用 lambda 表達式,更加的爽
字節流
首先要認識的兩個對象是:InputStream 和 OutputStream,他們分別是:所有字節輸入流統一的父(抽象)類、所有字節輸出流統一的父(抽象)類。
方法一覽:
// 一次讀一個字節,並返回這個字節
int read();
// 一次讀一個數組,返回讀取的長度
int read(byte[] data);
// 一次讀一個數組,從 off 開始填充,填充 len 個
int read(byte[] data,int off,int len);
write(int data);
write(byte[] data);
write(byte[] data,int off,int len);
需要註意:一次讀一個字節但是返回的是 int,這是為了確保返回值 -1
表示文件的結束,假設讀到了一個字節是 -1,那麽先會進行類型提升到 int,這裏的提升是 &0xff
來確保前面補的是 0 而不是 1。
讀取數組時,如果讀到最後不足一個數組的大小,那麽返回的 int 就不是 data.length,所以說並不是絕對的。
上面說的是頂級的抽象類,下面就來說說最常用的兩個具體類:FileInputStream 和 FileOutputStream。
它們作為節點流,構造方法可以指定連接 String 文件名 File 對象
雖然貴為節點流,但是它們只能連接文件 不能連接目錄
節點輸出流連接的文件即便不存在在,創建流的時候也會被自動創建出來,但是如果連接的目錄結構都不存在 則直接異常【File 類還有個 mkdirs()】
節點輸出流連接的文件即便已經存在,在創建流的一刻 也會被新的空白文件直接替換。
如果我們的需求是想要在最後追加內容,那麽構造方法:new FileOutputStream("abc.txt",true);
FileInputStream 最常用的是
read(byte[] data);
;FileOutputStream 最常用的卻是write(byte[],int,int)
FileInputStream 以 -1 作為讀取結束的標識
下面看經典的復制文件的例子:
public class FileCopy{
public static void main(String[] args){
FileInputStream fis = null;
FileOutputStream fos = null;
try{
fis = new FileInputStream("1.mp3");
fos = new FileOutputStream("2.mp3");
// 2^10 = 1024 也就是字節 <<10 是 kb,再 <<10 是 mb
byte[] data = new byte[5<<20]; // 5MB
int len = 0;
while((len = fis.read(data)) != -1){
fos.write(data,0,len);
}
}catch(Exception e){
e.printStackTrace();
}finally{
try{
fis.close();
}catch(Exception e){
e.printStackTrace();
}finally{
try{
fos.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
}
// 簡寫
public class FileCopy2{
public static void main(String[] args){
// JDK7+ 特性。
try(FileInputStream fis = new FileInputStream("1.mp3");
FileOutputStream fos = new FileOutputStream("3.mp3")){
byte[] data = new byte[5<<20];
int len;
while((len = fis.read(data)) != -1){
fos.write(data,0,len);
}
}catch(Exception e){e.printStackTrace();}
}
}
設置緩沖大小使用位運算效率更高哦,只需要記住 x<<20 就是 xMB 大小的緩沖區。
JDK7 後有了特性,就不用在 finally 裏寫這麽惡心的代碼了。。。。
過濾流
之所以稱它們為過濾流是因為他們接收的對象是 stream,並不能直接傳 file 或者路徑,對於字節流來說,有幾個還算用的多的字節流過濾流:
BufferedInputStream、BufferedOutputStream、DataInputStream、DataOutputStream、ObjectInputStream、ObjectOutputStream。
經過過濾(包裝)後,後面的操作只需要對這個過濾流操作就行了,因為是裝飾(包裝)模式,最後關流的時候只關過濾流就 OK 了。
前兩個:
作為過濾流的它們是為了給原本的節點流添加緩沖空間,從而提高每次讀寫的吞吐量 進而提高效率。它們構造方法的第二個參數可以指定緩沖空間大小(默認只有8192字節,也就是 8k);一定記得及時清空緩沖空間 防止數據滯留緩沖區,其中有三種情況會刷新緩沖區:1、當緩存空間滿了的時候自動刷新;2、當關閉流操作時會自動刷新;3、手動調用 flush。
這樣,即使你調用 read 方法一個字節一個字節的讀其實它會一次讀指定的大小到緩沖區,然後一個個的給你;寫也是,並不是一個個的寫,而是寫到緩沖區,滿了以後一次性 flush 到硬盤。
中間兩個:
是為了給原本的節點流添加讀寫基本數據類型的功能;DataInputStream 提供了一組方法 readXxxx();
,DataOutputStream 提供一組方法 writeXxxx();
,這時候不再以 -1 作為讀取結束的標識了,而是如果已經到達文件結尾還繼續讀取,則直接出現 EOFException(End of File)。
public static void main(String[] args){
try(DataOutputStream dos = new DataOutputStream(new FileOutputStream("t.data"))){
dos.writeInt(4545);
}catch(Exception e){e.printStackTrace();}
try(DataInputStream dis = new DataInputStream(new FileInputStream("t.data"))){
int x = dis.readInt();
System.out.println(x);
}catch(Exception e){e.printStackTrace();}
}
最後兩個:
給原本的節點流添加讀寫對象的功能的,與上面類似,對應的方法就是 readObject();
和 writeObject();
。
同樣不以 -1 作為結束,而也會 EOFException。
要寫出的對象必須先要序列化 (implements Serializable),如果要持久化的對象當中有其它引用類型的屬性,那麽也要進行序列化標識;但是如果某些屬性無關緊要,不需要保存(那就相當於是 null 了),可以直接使用 transient 修飾。
如果要持久化的是一個使用了比較器的 TreeSet 或者 TreeMap,就連比較器的類型也要實現序列化接口。
字符流
和字節流一樣,兩大鼻祖:Read 和 Writer;接口中定義的方法也和字節流中的那三個對應,就不寫了。
兩個常用的具體類:FileReader 和 FileWriter,描述也不多說了,和上面 FileInputStream 和 FileOutputStream 一樣一樣的
過濾流
對於字符流來說,最常用的還是莫過於這裏說的過濾流,因為對於字符操作,過濾流提供了更方便的方法。
BufferedReader 和 BufferedWriter:
通過對原有的字符流添加緩沖空間,使其可以支持一次讀取一行(readLine),和一次寫入一個字符串(write + newLine 換行)。其中 BufferReader 使用的非常頻繁,必須要熟練的。
PrintStream 和 PrintWriter:
像是一對姐妹,他們的方法一致,不同的是一個輸出字節,一個輸出字符;PrintStream 我們最長見的就是打印語句中的 out,它就是 PrintStream 類型的,然後 PrintWriter 在 IO 操作中用的非常頻繁,相比 BufferedWriter 它更加的好用。
既然說好用,那就來看看它的特點吧:
既可以作為節點流,又可以作為過濾流;也就是可以直接往構造函數裏扔文件對象、路徑、節電流
既可以連接字節流,又可以連接字符流;是的,構造函數裏都可以扔,不需要轉換流
當做節點流的時候,構造方法第二個參數可以指定字符集
當做過濾流的時候,構造方法第二個參數可以指定自動清空緩沖
例如:new PrintWriter(new FileWriter("a.txt",true),true);
第一個 true 是開啟追加模式,第二個是自動刷新(flush)擁有 println() 方法,等價於 write() + newLine()
轉換流(橋轉換器):InputStreamReader 和 OutputStreamReader ,其中最常用的是 InputStreamReader,實際用法例如:
new BufferedReader(new InputStreamReader(new FileInputStream("a.txt")))
而 OutputStreamReader 基本不怎麽用,因為有 PrintWriter 啊,它可以字符流字節流通吃,也就是說內部會內置一個轉換流,就是這個 OutputStreamReader ,所以讓我們方便了。
再來補充個 RandomAccessFile 用來支持隨機文件的讀取和寫入,通常可以用它來占空間,然後用流來對其進行寫:
RandomAccessFile raf = new RandomAccessFile("d:\\abc.mp4","rw");
File d = new File("d:\\");
long free = d.getFreeSpace();//得到d盤的剩余空間
raf.setLength(free);
raf.close();
這只是個簡單的使用,這裏先 TODO
原碼反碼補碼
對於一個數, 計算機要使用一定的編碼方式進行存儲. 原碼, 反碼, 補碼是機器存儲一個具體數字的編碼方式.
原碼就是符號位加上真值的絕對值, 即用第一位表示符號, 其余位表示值,原碼是人腦最容易理解和計算的表示方式。
```
[+1]原 = 0000 0001
[-1]原 = 1000 0001
```
正數的反碼是其本身;負數的反碼是在其原碼的基礎上, 符號位不變,其余各個位取反.
```
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
```
正數的補碼就是其本身;負數的補碼是在其原碼的基礎上, 符號位不變, 其余各位取反, 最後+1. (即在反碼的基礎上+1)
```
[+1] = [00000001]原 = [00000001]反 = [00000001]補
[-1] = [10000001]原 = [11111110]反 = [11111111]補
```
那麽為何要使用原碼, 反碼和補碼?
首先, 因為人腦可以知道第一位是符號位, 在計算的時候我們會根據符號位, 選擇對真值區域(可理解為不加符號位的二進制表示)的加減。
但是對於計算機,加減乘數已經是最基礎的運算,要設計的盡量簡單; 計算機辨別 "符號位" 顯然會讓計算機的基礎電路設計變得十分復雜!
於是人們想出了將符號位也參與運算的方法; 我們知道, 根據運算法則減去一個正數等於加上一個負數, 即: 1-1 = 1 + (-1) = 0 , 所以機器可以只有加法而沒有減法,這樣計算機運算的設計就更簡單了.
於是人們開始探索 將符號位參與運算, 並且只保留加法的方法, 首先來看原碼:
計算十進制的表達式: 1-1=0
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2
如果用原碼表示, 讓符號位也參與計算, 顯然對於減法來說, 結果是不正確的;這也就是為何計算機內部不使用原碼表示一個數.
為了解決原碼做減法的問題, 出現了反碼:
計算十進制的表達式: 1-1=0
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0
發現用反碼計算減法, 結果的真值部分是正確的.
而唯一的問題其實就出現在 "0" 這個特殊的數值上, 雖然人們理解上 +0 和 -0 是一樣的,但是 0 帶符號是沒有任何意義的, 而且會有 [0000 0000]原 和 [1000 0000]原 兩個編碼表示 0.
於是補碼的出現, 解決了 0 的符號以及兩個編碼的問題:
1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]補 + [1111 1111]補 = [0000 0000]補=[0000 0000]原
這樣 0 用 [0000 0000] 表示, 而以前出現問題的 -0 則不存在了;而且可以用 [1000 0000] 表示 -128:
(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]補 + [1000 0001]補 = [1000 0000]補
-1-127 的結果應該是 -128, 在用補碼運算的結果中 [1000 0000]補 就是 -128, 但是註意因為實際上是使用以前的 -0 的補碼來表示 -128, 所以 -128 並沒有原碼和反碼表示(所以可以多表示一個最低數).(對 -128 的補碼表示 [1000 0000]補 算出來的原碼是 [0000 0000]原, 這是不正確的)
使用補碼, 不僅僅修復了 0 的符號以及存在兩個編碼的問題, 而且還能夠多表示一個最低數; 這就是為什麽 8 位二進制, 使用原碼或反碼表示的範圍為 [-127, +127], 而使用補碼表示的範圍為 [-128, 127].
因為機器使用補碼, 所以對於編程中常用到的 32 位 int 類型,可以表示範圍是: [-231, 231-1] 因為第一位表示的是符號位.而使用補碼表示時又可以多保存一個最小值.
出自:https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html
其他
查找一個字符串出現多少次?
可以使用 (str + "l").split("abc").length - 1
,源字符串加任意一個字符防止被 split 的字符在最後會導致少一個,然後它的 length - 1 就是個數。
Java基礎復習計劃(三)