1. 程式人生 > 實用技巧 >Java語法--輸入輸出

Java語法--輸入輸出

Input & Output

《Java Core》ed.11 學習筆記

最重要的事情:輸入和輸出都是相對於CPU來說的,輸入是把資料輸入給CPU,輸出是CPU把資料輸出到別的地方

Input/Output Streams

JavaAPI中,從input stream物件中讀取位元組序列,向output stream物件中寫入位元組序列。這些源和目的地可以是檔案、也可以是網路連線,甚至是一塊記憶體。抽象類InputStreamOutputStream形成了輸入輸出相關類的基礎

面向位元組的輸入輸出流對於處理用Unicode儲存的資料不方便,因此,另外一個繼承自ReaderWriter

類的繼承層次是用來處理Unicode字元的,讀寫操作都是基於兩位元組的char型別(碼元)

讀寫位元組

abstract int read()

這個方法讀1個位元組然後返回一個被讀的位元組,如果到了輸入源的尾部,則會返回-1。這個類的實現類的設計者提供了很多有用的功能(FileInputStream/System.in)

InputStream也有非抽象方法(讀取一個位元組陣列/跳過一定數量位元組)。Java9之後,多了一個從流中一次獲取所有位元組資料的方法readAllBytes();

這些方法呼叫了read()方法,所以子類必須要實現read()方法

OutputStream也定義了抽象方法abstract void write(int b)

,向輸出目的地寫入1個位元組資料,也有直接寫入一個位元組陣列資料的API。transferTo方法(Java 9)將輸入流中所有資料轉移到輸出流

無論是read還是write方法,在讀完和寫完之前都是阻塞(block)狀態的,這意味著,如果輸入流無法立刻被訪問(網路連線問題),當前執行緒會阻塞,其它執行緒在等待輸入流重新可用之前就有機會做其它事情

available方法可以檢查現在可以被用來讀取的位元組數

int bytesAvailable = in.available();
if (bytesAvailable > 0)
{
    var data = new byte[bytesAvailable];
    in.read(data);
}

上面的程式碼不會出現阻塞

在讀或寫結束的時候,通過close方法來關閉流,在關閉輸出流時,所有緩衝區(等待後續資料組成更大的資料包)的位元組會被重新整理掉(輸入到目的地)。如果一直不關閉流,有可能一部分資料無法輸出,也可以手動flush

Java9方法 int readNBytes(byte[] b, int off, int len),直到陣列讀滿前都會阻塞程序

所有的流

Java有超過60種不同的輸入輸出流型別

DataInputStreamDataOutputStream可以以二進位制的形式讀寫所有基本資料型別。ZipInputStreamZipOutputStream可以讀寫ZIP壓縮格式資料

WriterReader的繼承層次和InputStream OutputStream類似

輸入輸出流中有4個額外介面:Closeable(實現了java.lang.AutoCloseable介面,所以可以使用try-with-resource方式) Flushable Readable Appendable。前兩個就是closeflush()方法的介面

Readable介面有int read(CharBuffer cb)方法,CharBuffer類的方法可以序列化和隨機讀寫。它表示記憶體中的緩衝或者一個記憶體對映檔案(之後還有解釋)

Appendable介面有兩個方法用來追加單個字元和字元序列

Appendable append(char c)
Appendable append(CharSequence c)

CharSequence介面描述了char值序列的基本屬性。String CharBuffer StringBuilder StringBuffer Writer實現了這個介面

組合輸入輸出流(過濾器)

java.io下的類解釋相對路徑一律是從使用者工作目錄開始,這個目錄可以通過System.getProperty("user.dir");獲得

使用java.io.File.separator而不是字元

可以通過結合多個流來將位元組資料處理成各種其它的資料。舉例:如果想從檔案中讀取資料,可以建立一個FileInputStream給到DataInputStream的構造器當引數

流可以多層巢狀(BufferedInputStream緩衝流的作用是,如果不緩衝,每次read方法都會請求作業系統來讀出1個位元組,而一次讀出一塊資料明顯是更高效的)

在需要跟蹤中間輸入流的時候(讀取輸入時,可能想看看下一個位元組資料是不是想要的),這時可以使用PushbackInputStream

PushbackInputStream的理解

這個流中的unread()方法可以把一個位元組扔到pushback的緩衝區,此時再使用read()方法會訪問到這個位元組。所以在使用的時候,unread()之後要接一個read()。這個流的場景是可以過濾一些間隔符之類的固定格式的資料,當作下一個流處理的源

怎麼寫入文字輸出

使用PrintWriter來輸出文字,這個類中的方法可以以文字格式列印字串和數字。使用檔名和字元編碼當引數的構造器可以將輸出列印到檔案中

在輸出流物件打印出的字元會轉換為位元組存入檔案,換行符和系統有關,通過System.getProperty("line.separator")來獲取系統分隔符

如果writer設定為autoflush模式,那麼在println()被呼叫時,所有緩衝區的字元會被髮送到它們的目的地(列印writer總是緩衝的)預設不是自動重新整理。通過構造器裡的對應引數來開關自動重新整理

print方法不丟擲遺產。可以使用checkError方法來看輸出流是否出問題了

怎麼讀取文字輸入

最簡單的訪問任意文字的方式是Scanner,可以通過任意輸入流來構造Scanner物件

// java 9
// 使用這種方式可以讀取短文字檔案
var content = new String(Files.readAllBytes(path), charset);
// java 9
List<String> lines = Files.readAllLines(path, charset);
// 如果檔案過大,可以懶載入(處理行)
try (Stream<String> lines = Files.lines(path, charset))
{
. . .
}
// 可以使用Scanner來讀取Token(被分隔符劃分的字串),預設分隔符是空格。接收任何非Unicode字母當分隔符
Scanner in = ...;
in.useDelimiter("\\PL+");
// 獲取所有Token
Stream<String> words = in.tokens();
// 用next獲取Token
while (in.hasNext())
{
    String word = in.next();
    ...
}

Java早期使用BufferedReader來處理文字,現在這個類中的lines方法也可以獲取Stream<String>,但是這個類沒有讀數字的方法

以文字格式儲存物件

使用PrintWriter將物件的toString方法返回的字串列印到檔案中是一種方式。對應來說,要讀出物件就按行讀取,然後用分隔符把字串解析(String.split())回到物件

字元編碼

在處理字元時,它們是通過什麼編碼方式成為位元組資料。在Java中,使用Unicode標準來處理字元,一個字元(碼點)是21-bit的整數。常用的編碼方式是UTF-8,它包括了所有英文字母,並且只佔一個位元組

另一個常用編碼方式是UTF-16,將每個Unicode碼編碼成1個或2個16-bit的值,Java的String是使用的這個編碼方式。UTF-16編碼方式有兩種形式(big-endian little-endian)即從大到小和從小到大。一個檔案可以以位元組順序標識開頭來標明用的哪種形式(16-bit數0xFEFF),一個reader可以使用這個值判斷,然後丟棄這個值

編碼方式太多,所以在寫入和讀取資料的時候要儘量指定編碼方式(在讀網頁的時候,要看Content-Type頭)

平臺編碼可以通過Charset.defaultCharsetCharset.availableCharsets會返回一個Map,key是名字,value是Charset物件

StandardCharsets類中的靜態成員變數(Charset型別)

// 獲取字符集物件
Charset shiftJIS = Charset.forName("Shift-JIS");

Java10之後,java.io包允許指定字元編碼

有些方法的預設編碼是平臺編碼(UTF-8)

讀寫二進位制資料

處理文字格式的資料很方便,因為它是可以直接閱讀的內容,但是傳輸效率不如位元組資料(二進位制)高

DataInput和DataOutput介面

DataOutput介面定義了一系列方法writeChars writeInt等,這些方法的輸出是不可讀的,(寫整型數是固定4位元組,不管其真是數字佔多少位)。對於每個給定型別的資料所佔的空間都是一樣的,在讀入的時候也比轉成文字要快

在記憶體中儲存浮點數和整數的方式有兩種(正序和倒序),Java統一正序,所以平臺獨立。而C/C++儲存檔案可能會跟平臺(處理器)有關

writeUTF方法使用修改版本的8-bit UTF編碼格式寫入字串資料。碼元先用UTF-16的方式表示,然後結果使用UTF-8的規則編碼。在0xFFFF之後的編碼會有不同(相比直接UTF-8),這是為了相容Unicode還沒有升到16-bit的虛擬機器

因為沒有其它地方用這種修改版的UTF-8,所以只有在寫入專為JVM工作的字串才用writeUTF(),其它用途使用writeChar()

DataInputDataOutput介面實現的方法互為映象。且DataInputStream實現了這個介面(輸出同上)

隨機訪問檔案

RandomAccessFile類讓你從檔案的任何位置開始讀寫資料。磁碟檔案是隨機訪問的,但是和網路socket有關的輸入輸出流不是。可以使用隨機訪問的方式開啟只讀檔案和讀寫檔案(在構造器的第二個引數傳入'r','rw')

在用隨機訪問方式開啟檔案之後,這個檔案不能被刪除

隨機訪問檔案有一個檔案指標,用來指定要被讀或寫的下一個位元組的位置,seek()方法可以被用來設定檔案指標到檔案的任意一個位元組資料的位置(引數範圍是0-檔案位元組數)

getFilePointer返回當前檔案指標的位置。寫的方法是覆蓋寫法,而不是插入

RandomAccessFile實現了DataInputDataOutput介面

length()方法可以返回整個檔案的位元組大小

讀取字元、整數、浮點數這種固定大小的值比較簡單,如果想讀取固定大小的字串,要寫幫助方法(思路是使用readCharwriteChar,即將字串分解成字元處理)

ZIP壓縮包

zip壓縮包有一個頭用來儲存資訊(每個檔案的檔名,壓縮方法等),Java中可以使用ZipInputStream來讀取zip壓縮包。需要檢視包裡每個獨立的檔案?(entries)。getNextEntry方法返回一個型別為ZipEntry的物件來描述這個entry。從流中讀取到最後,然後呼叫closeEntry方法來讀取下一個entry。在讀完最後一個entry不要關閉整個流

var zin = new ZinInputStream(new FileInputStream());
ZipEntry ze;
while((ze = zin.getNextEntry) != null) {
    process
    ze.closeEntry();
}
zin.close();

如果要寫入資料到zip壓縮包,使用ZipOutputStream,建立ZipEntry,引數是檔名和其它資訊(檔案日期和解壓方法)

var fout = new FileOutputStream("test.zip");
var zout = new ZipOutStream(fout);
for all files {
    ZipEntry ze = new ZipEntry(filename);
    zout.writeEntry(ze);
    send data to ze
    zout.closeEntry();
}
zout.close();

JAR檔案是有特殊entry(manifest)的ZIP檔案,對應的有JarInputStreamJarOutputStream

ZIP流中的位元組資料不需要是檔案,允許來自網路連線。且讀取壓縮形式的資料不需要擔心其被解壓

物件輸入/輸出流和序列化

如果儲存相同型別的資料,應該使用固定長度的記錄格式。然而自己建立的物件基本不是相同型別。例如一個數組宣告為Employee型別,但是儲存的可能是Manager物件

儲存和載入序列化物件

要儲存一個物件,首先建立一個ObjectOutputStream物件

var out = new ObjectOutputStream(new FileOutputStream("xxx"));

Manager m = new Manager();
Employee e = new Employee();

out.writeObject(m);
out.writeObject(e);

要載入回一個物件,首先獲取一個ObjectInputStream

var in = new ObjectInputStream(new FileInputStream("xxx"));

var e = (Employee) in.readObject();
var e1 = (Employee) in.readObject();

這個類如果想通過輸入輸出流來儲存和載入物件,需要實現Serializable介面。ObjectInputStreamObjectOutputStream實現了DataInputStreamDataOutputStream,所以可以通過readInt等方法來讀寫基本資料型別

有一種場景需要考慮:如果一個物件作為其它幾個物件共享的物件,那麼會發生什麼。首先不能儲存每個物件的記憶體地址(重新載入之後記憶體地址完全不一樣)。實際上是序列號解決了這個問題(每個物件有有一個對應的序列號,因此叫做物件序列化機制)

例如:兩個Manager物件都有一個共同的祕書Employee物件,那麼存在檔案中大概是,祕書物件序列號為1,經理物件的祕書欄位都是祕書(employee)1

當第一次碰到任何一個物件引用,那麼存到檔案中,之後再碰到則寫入其它資訊(之前已經存過序列號為x的物件)。當讀入物件時,做的事情相反

寫入物件的內容不包含該類以及其超類的靜態成員、transient成員

理解物件序列化檔案格式

這節主要是講把物件寫入檔案之後,檔案的組成結構。這裡簡單記錄一下即可,感覺
實際意義不大

本章所有數字都是16進位制數

  1. 每個檔案以兩位元組的魔術數開頭AC ED
  2. 之後是物件序列化格式的版本號,當前是00 05
  3. 根據儲存順序儲存的一系列物件(如果是Unicode的字串,儲存的是修改過的UTF-8編碼的字元)
  4. 儲存了物件,其對應的類資訊也會被儲存
  5. 類名,唯一序列號(資料成員型別和方法簽名的指紋),描述序列化方法的flag,資料欄位的描述資訊(指紋:按標準方式排列類、超類、介面、欄位型別、方法簽名,然後通過SHA來生成)
  6. SHA通常是20位元組大小,Java選取前8個位元組,這也可以保證類有變動,指紋基本上也會變
  7. Externalizable介面,實現此介面的類可以提供自定義的讀寫方法來代替它們的例項欄位的輸出

需要記住的事情:

  1. 序列化的格式包括所有物件的欄位的型別
  2. 每個物件都被給與了序列號
  3. 一個物件重複出現會被儲存為相同序列號的引用

修改預設的序列化機制

有些資料欄位是不應該被序列化的,例如只對native方法有意義的儲存檔案控制代碼的整數值。這種資訊在後續反序列化出物件時或者在另一臺機器上反序列化就完全沒用了,甚至可能會導致native方法崩潰

如果要防止它們被序列化,只需要用關鍵字transient來標記欄位,如果它們屬於不可序列化的類,也需要使用此關鍵字標記

根據這個機制可以自定義readObjectwriteObject方法來定義讀寫物件,接收一個物件輸入輸出流

defaultWriteObject()這個方法比較特殊,它只能在序列化類的writeObject方法中被呼叫,將所有的非transient欄位寫入檔案

每個類可以定義自己的機制,實現Externalizable介面,readExternal(ObjectInputStream) writeExternal(ObjectOutputStream)。這兩個方法是儲存和讀取這個物件的全部資訊,包括超類的資料。在寫入物件時,序列化機制在輸出流幾乎不記錄物件的類,在讀取externalizable物件時,物件輸入流通過無參構造器初始化物件,然後呼叫readExternal方法

readObjectwriteObject方法是私有的,供序列化機制使用。此外readExternal方法潛在允許改變創建出來的物件

序列化單例和型別安全的列舉

在序列化和反序列化被假定為唯一的物件(單例和列舉)需要特別注意

舉例

public class Orientation {
    public static final Orientation HORIZONTAL = new Orientation(1);
    public static final Orientation Vertical = new Orientation(2);
    
    private int value;
    
    private Orientation(int value) {this.value = value;}
}

這個類的構造器是私有的,所以用這個類建立的任何物件都只能是其兩個靜態常量,此時如果直接實現序列化介面然後寫入到一個檔案,在讀出的時候如果執行if(orientation == Orientation.HORIZONTAL)結果是false。即使構造器是私有的,序列化機制也可以用來構造出新物件

如果要解決這個問題,可以另外新寫一個方法,對讀出的物件進行一個比較然後選取一個常量列舉返回

if (value == 1) return Orientation.HORIZONTAL;
if (value == 2) return Orientation.Vertical;

版本

如果使用序列化來儲存物件,那麼需要考慮程序升級之後(類簽名改變)和舊物件檔案的相容性

通過JDK的程式serialver可以得到某個類的序列號(serialVersionUID)。然後在類中宣告一個靜態成員變數,使用這個值來初始化。這個序列號代表新版本的類和舊版本相容。此後序列化不再計算這個類的序列號而是使用這個變數。如果類的欄位發生了變化(數量、型別),那麼輸入流中會盡力將物件改變成新版本的物件,如果同名欄位型別不同,輸入流不會進行進行型別轉換。如果舊物件有新物件不需要的欄位,那麼輸入流會忽略。如果當前版本有新的欄位,那麼輸入流會設定成預設值

注意:

  1. 如果只是短期的持久化,那麼不需要考慮新增版本管理(序列號)
  2. 擴充套件了一個序列化的類但是不需要序列化它的例項,那麼忽略IDE的警告(@SuppressWarnings("serial")),這比設定了序列號但是忘記改更安全

使用序列化來克隆

這是一個tricky,使用序列化可以使用深拷貝的方式建立新物件。即不把物件寫入到檔案中(使用ByteArrayOutputStream 位元組陣列接收資料),然後馬上讀出

這種方式很慢!!!(流資源開銷)

操作檔案

Path介面和Files類包含了在使用者的機器上有關檔案操作的所有功能,它們關注的是檔案在硬碟上的儲存。這兩個介面/類是在Java7加入的(比Java1.0的File類好用的多)

Paths

Path是一系列資料夾的名稱(可能會跟檔名),構造Path物件的第一個部分必須是根目錄(windows的碟符),以根目錄為第一個部分的Path是絕對路徑,否則是相對路徑

Path absolute = Paths.get("/home", "harry");
Path relative = Paths.get("myprog", "conf", "user.properties");

靜態方法Paths.get接收一些字串最後用分隔符(根據系統選擇)連線起來。這個方法也可以傳入一個完整的檔案路徑

一個Path並不需要繫結一個真正存在的檔案(只是一堆名字)

組合或resolve路徑是常見操作,p.resolve(q)根據以下情況返回一個path

  1. 如果q是絕對路徑,返回q
  2. 否則,結果是p then q即拼接兩個路徑

resolveSibling方法可以用引數給的路徑替換掉當前路徑的最後一個子路徑(sibling的含義)

對應resolve方法還有relativise方法,它可以將兩個路徑的共同路徑轉為..,然後拼接上方法引數的非公共路徑的子路徑(引數路徑必須和呼叫方法的路徑物件型別相同(相對、絕對))

Path aPath = Paths.get("F:", "tmp", "sb.txt");
Path oPath = Paths.get("F:", "tmp", "xdd", "dv.dat");
Path finalPath = aPath.relativize(oPath);
// ..\xdd\dv.dat
System.out.println(finalPath);

normalize方法移除了冗餘的...

toAbsolutePath方法返回絕對路徑,以根目錄開頭(開發過程中是以工作目錄為基本目錄)

getParent() getFileName() getRoot()這些方法見名知意

Path介面有toFile()方法,File類有toPath()方法

讀寫檔案

Files類可以快速實現對檔案的一般操作

Files.readAllBytes(path)讀取檔案全部內容。var content = new String(prev, charset)將其轉為字串。Files.readAllLines(path, charset),返回一個List<String>

Files.write(path, content.getBytes(charset)是寫入資料,追加寫入使用Files.write(path, charset, StandardOpenOption.APPEND)Files.write(path, lines)可以直接寫多行(換行符)

上述方法是為了處理長度始終的文字資料,如果檔案過大或者是二進位制資料,還是使用輸入輸出流的方式比較好

建立檔案和資料夾

建立一個資料夾(引數中除了最後一部分,其它部分都不能不存在)

Files.createDirectory(path);

建立一個資料夾(中間不存在資料夾也建立)

Files.createDirectories(path);

建立一個空檔案(如果檔案存在會丟擲異常,檢查存在和建立是原子性的,如果檔案不存在,那麼這個操作在所有其它能做這個操作的動作之前完成)

Files.createFile(path);

建立臨時檔案、臨時資料夾(用的時候細看API吧)

Files.createTempFile
Files.createTempDirectory

複製、移動和刪除檔案

複製一個檔案

Files.copy(fromPath, toPath);

移動一個檔案(先複製後刪除)

Files.move(fromPath, toPath);

如果目標檔案存在,那麼複製或移動會失敗,如果想覆蓋目標檔案,可以使用REPLACE_EXISTING,如果想複製檔案的所有屬性過去,使用COPY_ATTRIBUTES,將這兩個常量當作引數傳入即可

移動操作可以使用ATOMIC_MOVE來保證原子性

可以將輸入流複製進一個路徑(檔案),將一個路徑(檔案)複製進一個輸出流

Files.copy(inputStream, path);
Files.copy(path, outStream);

刪除檔案

// 如果檔案不存在,丟擲異常
Files.delete(path);
// 刪除可以這樣做
boolean deleted = Files.deleteIfExists(path);

檔案操作選項有一堆常量標誌,需要再檢視

獲取檔案資訊

檢查一個檔案是否具有某種屬性,返回boolean

exists()
isHidden()
isReadable() isWritable() isExecutable()
isRegularFile() isDirectory() isSymbolicLink()

size()方法返回檔案的大小位元組數。getOwner方法返回檔案的擁有者,返回值是java.nio.file.attribute.UserPrincipal的例項

其它的功能看API即可

遍歷資料夾(entries)

Files.list方法會讀取一個路徑下的所有目錄(懶讀入),讀取目錄需要系統資源,所以要記得把資源關閉

try(Stream<Path> entries = Files.list(path)) {
    ...
}

list方法不遍歷子目錄,要遍歷所有子目錄(及其後代),可以使用walk方法,此方法可以傳入引數來控制深度,這個方法接收(FOLLOW_LINKS)作為引數來追蹤符號連結

使用資料夾流(Directory Stream)

Files.walk方法會建立一個Stream<Path>物件,當需要更細粒度的控制時,使用Files.newDirectoryStream方法,它返回一個DirectoryStream物件(需要關閉系統資源),它並不是java.util.stream.Stream的子介面,它是專門用於遍歷資料夾的,且是Iterator的子介面(可以使用foreach)

try(DirectoryStream<Path> entries = Files.newDirectoryStream(dir)) {
    for (entry: entries) {
        ...
    }
}

可以通過glob pattern來過濾檔案(API可查)

Files.walkFileTree(Paths.get("/"))方法可以定義訪問每個檔案/資料夾的具體行為(使用FileVisitor<Path>介面實現)(這部分內容用時再看)

ZIP檔案系統

ZIP也是一種檔案系統(目錄樹結構)

FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname), null)

這就構建了一個包含ZIP包中所有檔案的檔案系統物件

Files.copy(fs.getPath(sourceName), target);

遍歷ZIP中所有的檔案,可以使用Files.walkFileTree(fs.getPath("/"), new SimpleFileVisitor<Path>() {...});。具體實現跟上一節描述的差不多

Memory-Mapped檔案

很多作業系統可以利用虛擬記憶體來”map“檔案或檔案塊到記憶體。然後這些檔案訪問就可以像記憶體中的陣列一樣,比傳統檔案操作更快

記憶體對映檔案的效能

計算37M的rt.jar的CRC32校驗碼。效能比較

隨機訪問 < 普通輸入流 < 緩衝流 < 記憶體對映檔案

對於順序讀取一般大小的檔案,不需要使用記憶體對映檔案

java.nio做記憶體對映非常簡單

  1. 獲取一個channel(對應檔案)(對磁碟檔案的抽象,使其可以訪問作業系統的特性--例如記憶體對映,檔案鎖,檔案間的快速資料轉換)FileChannel channel = FileChannel.open(path, options);
  2. 通過呼叫channel物件的map()方法來獲取一個ByteBuffer物件,指定想對映的檔案區域和對映模式(FileChannel.MapMode規定)(多執行緒讀寫的結果依賴於作業系統)
  3. 通過ByteBufferBuffer中的方法來操作資料
  4. Buffer支援順序訪問也支援隨機訪問,一個buffer是由getput方法來控制的(get是讀,put是寫)

Buffer的資料結構

一個buffer是一個相同型別的值的陣列,Buffer是一個抽象類,它有很多實現類(略)(StringBuffer不是)

結構圖如下:

4個標誌點:

  1. 0--position:已經被讀或寫的部分
  2. position--limit:還沒被讀或寫
  3. limit--capacity:超出限制
  4. mark:用來重複讀或寫
  5. remaining:剩下的

capacity:不會改變
position:下一個讀或寫的值
limit:超過此位置的讀或寫無意義

開始,position=0,limit=capacity,不斷put值之後,達到capacity,就該變成讀模式了,呼叫flip方法設定limit為當前position,然後position設為0,然後呼叫get(當remaining方法返回true),讀完所有的值之後,呼叫clear來準備進入寫模式,它設position為0並且limit為capacity

如果要重讀buffer,使用rewind或者mark/reset(API詳解)

然後,可以用channel的值來填充buffer,也可以把buffer的值寫到channel

檔案鎖

解決多執行緒同時修改同一個檔案的衝突問題

例如兩個程序修改同一個檔案,那麼一個程序鎖住檔案之後,第二個程序決定是等待釋放鎖還是直接跳過

要鎖住一個檔案,使用

FileChannel channel = FileChannel.open(path);
// tryLock() 非阻塞式 沒鎖返回null
// 阻塞式
FileLock lock = channel.lock();

還有方法可以鎖檔案的一部分(API詳解)

shared引數,如果是false,那麼鎖讀和寫,否則只鎖寫,即多個程序可以同時讀檔案(不是所有作業系統都支援這個共享鎖FileLock.isShared方法可以檢視是否支援的狀態)

要注意解鎖操作,最佳實踐是使用try-with-resource,即

try(FileLock lock = channel.lock()) {
    ...
}

檔案鎖是依賴系統實現的,有幾點注意:

  1. 一些系統中,即使應用沒獲得鎖,還是可以修改其它應用獲得鎖的檔案
  2. 一些系統中,無法同時鎖一個檔案並且對映到記憶體
  3. 所有的檔案鎖都被JVM持有,所以一個虛擬機器啟動的兩個程式,不能同時持有一個檔案的兩把鎖
  4. 一些系統中,關閉一個channel會釋放這個JVM持有的檔案上的所有鎖。應該避免一個上鎖的檔案上開多個channel
  5. 在網路檔案系統上鎖一個檔案的行為高度依賴系統,最好避免

正則表示式

正則表示式是用來定位匹配某些特定模式的字串的

正則表示式語法

[Jj]ava.+

解釋如下

  1. 第一個字母是J或j
  2. 後三個字母是ava
  3. 剩下的字串包含一個或多個任意字元

語法實在太多,就不放了,實際用的時候有工具(網站)

匹配字串

測試一個給定字串是否符合某個規則

  1. 建立一個Pattern字串 Pattern pattern = Pattern.compile(patternString)
  2. 獲取一個Matcher物件,Matcher matcher = pattern.matcher(input);
  3. if(matchr.matches())

input是任何實現了CharSequence介面的物件(StringBuffer String CharBuffer),在compile方法中可以指定其它標誌(flag-API詳解)

如果想在流中匹配模式

Stream<String> strings = ...;
Stream<String> result = strings.filter(pattern.asPredicate());

如果正則中包含組,Matcher物件可以顯示出組的邊界(start(groupIdx) end(groupIdx)group(groupIdx)直接拿出匹配到的字元

其它功能待使用再看

多次匹配

一次只找匹配的一個或多個字串。Matcher類的find方法可以找到寫一個匹配的值,如果返回true,使用start end group進行後續操作

也可呼叫results()方法,返回Stream<MatchResult>物件,然後再進行操作。Scanner.findAll()返回的也是這個物件,操作同上

通過分隔符拆分

Pattern.split方法可以去掉分隔符,返回剩下的token(資料),返回值是一個數組。如果有很多token,可以使用splitAsStream方法,返回一個流物件供操作。如果不在乎是懶拉取(資料)還是預編譯,可以直接使用String.split(正則)。如果是檔案,可以使用ScanneruseDelimiter方法,引數是正則表示式,然後呼叫tokens()方法

替換匹配串

MatcherreplaceAll方法將所有正則匹配的串替換為給定串

替換串可以包括在正則中組的引用。$n代替第n組,${name}代替有名字的組。如果包含$可以使用轉義

matcher.replaceAll(Matcher.quoteReplacement(str))可以忽略所有所有的$\

在這個方法裡可以提供一個替換方法(引數是MatchResult,返回值是字串)

replaceFirst只能替換第一個匹配的串