1. 程式人生 > >Java基礎復習計劃(三)

Java基礎復習計劃(三)

.html 節點 fix 減法 形式 頂級 分別是 字符流 TP

散碎知識點

  • Math.round() 方法進行四舍五入計算,實現是:Math.floor(a + 0.5f)
    floor : 意為地板,指向下取整,返回不大於它的最大整數
    ceil : 意為天花板,指向上取整,返回不小於它的最小整數
    round : 意為大約,表示“四舍五入”,而四舍五入是往大數方向入.

  • 關於方法區溢出:
    經常動態生成大量 Class 的應用中,Spring、hibernate 對類進行增強的時候使用 CGLib 類字節碼技術 ,其他運行在 JVM 的動態語言;
    常見的還有大量 JSP 或動態產生 JSP 文件的應用(JSP 第一次運行時需要編譯)

  • public Method[] getDeclaredMethods()

    返回類或接口聲明的所有方法,包括 public, protected, default (package) 訪問和 private 方法的 Method 對象,但不包括繼承的方法。當然也包括它所實現接口的方法。
    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 修飾變量

線程

線程的五大狀態:

  1. 新生(Born)
  2. 就緒(Runnable)
  3. 運行(Running)
  4. 消亡(Dead)
  5. 阻塞(Blocking)

創建線程的方式:

  1. extends Thread
  2. 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,一般的特性:
  1. 通常是無限循環的
  2. 守護線程一般有極低的優先級
  3. 設置守護線程必須在 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 流了,流可分為三類:

  1. 方向分: 輸入流 or 輸出流
  2. 單位分: 字節流 or 字符流
  3. 功能分: 節點流 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基礎復習計劃(三)