Java I/O系統學習系列一:File和RandomAccessFile


  I/O系統即輸入/輸出系統,對於一門程序語言來說,創建一個好的輸入/輸出系統並非易事。因為不僅存在各種I/O源端和想要與之通信的接收端(文件、控制台、網絡鏈接等),而且還需要支持多種不同方式的通信(順序、隨機存取、緩沖、二進制、按字符、按行、按字等)。

  Java類庫的設計者通過創建大量的類來解決這個難題,比如面向字節的類(字節流,InputStream、OutputStream)、面向字符和基於Unicode的類(字節流,Reader、Writer)、nio類(新I/O,為了改進性能及功能)等。所以,在充分理解Java I/O系統以便正確地運用之前,我們需要學習相當數量的類。因此一開始可能會對Java I/O系統提供的如此多的類感到迷惑,不過在我們系統地梳理完整個Java I/O系統並將這部分知識與融入到自我的整個知識體系中后,我們就能很快消除這種迷惑。

  在I/O這個專題里面,我會總結Java 中涉及到的大多數I/O相關類的用法,從傳統I/O諸如:File、字節流、字符流、序列化到新I/O:nio。在本節中我會先總結File和RandomAccessFile的相關知識,按照如下順序:

  File

  RandomAccessFile

  總結

 

1. File

1.1 File簡介常用方法

  根據官方文檔的解釋,Java中的File類是文件和目錄路徑的抽象,用戶通過File直接執行與文件或目錄相關的操作。我的理解就是File類的作用是用來指代文件或者目錄的,通過File的抽象我們可以很方便的操作文件或目錄,無需關心操作系統的差異。官方文檔是這樣描述的:

An abstract representation of file and directory pathnames.

User interfaces and operating systems use system-dependent pathname strings to name files and directories.  This class presents an abstract, system-independent view of hierarchical pathnames. 

  用戶接口和操作系統通過系統相關的路徑名來命名文件和目錄。而File類提供了一個抽象地、系統無關的視角來描述分層次路徑名。File代表抽象路徑名,有兩個部分組成:  

  • 一個可選的系統相關的前綴,比如磁盤驅動器說明符(disk-drive specifier),unix系統中是“/”而windows系統中則是“\”;
  • 0或多個字符串名稱組成的序列;

  關於File的用法,我覺得直接通過示例來學習會比較高效:  

public class FileDemo {    
    public static void main(String[] args) throws IOException {
        File dir = new File("f:/dirDemo");
        System.out.println("dir exists: " + dir.exists());
        dir.mkdirs();
        System.out.println("dir exists: " + dir.exists());
        if(dir.isFile()) {
            System.out.println("dir is a file.");
        }else if(dir.isDirectory()) {
            System.out.println("dir is a directory");
        }
        
        File file = new File("f:/dirDemo/fileDemo");        
        System.out.println(
                "\n Absolute path: " + file.getAbsolutePath() +
                "\n Can read: " + file.canRead() + 
                "\n Can write: " + file.canWrite() +
                "\n getName: " + file.getName() +
                "\n getParent: " + file.getParent() +
                "\n getPath: " + file.getPath() +
                "\n length: " + file.length() +
                "\n lastModified: " + file.lastModified() +
                "\n isExist: " + file.exists());
        file.createNewFile();
        System.out.println("is file exist: " + file.exists());
        if(file.isFile()) {
            System.out.println("file is a file.");
        }else if(file.isDirectory()) {
            System.out.println("file is a directory");
        }

        System.out.println();
        for(String filename : dir.list()) {
            System.out.println(filename);
        }
    }    
}

  輸出結果:

dir exists: false
dir exists: true
dir is a directory

 Absolute path: f:\dirDemo\fileDemo
 Can read: false
 Can write: false
 getName: fileDemo
 getParent: f:\dirDemo
 getPath: f:\dirDemo\fileDemo
 length: 0
 lastModified: 0
 isExist: false
is file exist: true
file is a file.

fileDemo

  在這個簡單demo中我們用到多種不同的文件特征查詢方法來顯示文件或目錄路徑的信息:

  • getAbsolutePath(),獲取文件或目錄的絕對路徑;
  • canRead()、canWrite(),文件是否可讀/可寫;
  • getName(),獲取文件名;
  • getParent(),獲取父一級的目錄路徑名;
  • getPath(),獲取文件路徑名;
  • length(),文件長度;
  • lastModified(),文件最后修改時間,返回時間戳;
  • exists(),文件是否存在;
  • isFile(),是否是文件;
  • isDirectory(),是否是目錄;
  • mkdirs(),創建目錄,會把不存在的目錄一並創建出來;
  • createNewFile(),創建文件;
  • list(),可以返回目錄下的所有File名,以字符數組的形式返回;

  exists()方法可以返回一個File實例是否存在,這里的存在是指是否在磁盤上存在,而不是指File實例存在於虛擬機堆內存中。一個File類的實例可能表示一個實際的文件系統如文件或目錄,也可能沒有實際意義,僅僅只是一個File類,並沒有關聯實際文件,如果沒有則exists()返回false。

1.2 File過濾器

  list()方法返回的數組中包含此File下的所有文件名,如果想要獲得一個指定的列表,比如,希望得到所有擴展名為.java的文件,可以使用“目錄過濾器”(實現了FilenameFilter接口),在這個類里面可以指定怎樣顯示符合條件的File對象。我們把一個自己實現的FilenameFilter傳入list(FilenameFilter filter)方法中,在這個被當做參數的FilenameFilter中重寫其accept()方法,指定我們自己想要的邏輯即可,這其實是策略模式的體現。

  比如我們只要獲取當前項目跟目錄下的xml文件:

public class XmlList {    
    public static void main(final String[] args) {
        File file = new File(".");
        String list;
        list = file.list(new FilenameFilter(){
            @Override
            public boolean accept(File dir, String name) {
                Pattern pattern = Pattern.compile("(.*)\\.xml");
                return pattern.matcher(name).matches();
            }
        });
        Arrays.sort(list,String.CASE_INSENSITIVE_ORDER);
        for(String dirItem : list)
            System.out.println(dirItem);
    }
}

  在這個例子中,我們用匿名內部類的方式給list()傳參,accept()方法內部我們指定了正則過濾策略,在調用File的list()方法時會自動為此目錄對象下的每個文件名調用accept()方法,來判斷是否要將該文件包含在內,判斷結果由accept()返回的布爾值來表示。

  如上也只是羅列了一些個人認為File類較常用的方法,也只是一部分,若需要更詳細信息請參考官方文檔。

1.3 目錄工具

  接下來我們來看一個實用工具,可以獲得指定目錄下的所有或者符合要求的File集合:

public class Directory {
    // local方法可以獲得指定目錄下指定文件的集合
    public static File[] local(File dir,String regex) {
        
        return dir.listFiles(new FilenameFilter() {
            private Pattern pattern = Pattern.compile(regex);
            @Override
            public boolean accept(File dir, String name) {
                return pattern.matcher(new File(name).getName()).matches();
            }
        });
    }
    
    public static File[] local(String dir,String regex) {
        return local(new File(dir),regex);
    }
    
    // walk()方法可以獲得指定目錄下所有符合要求的文件或目錄,包括子目錄下
    public static TreeInfo walk(String start,String regex) {
        return recurseDirs(new File(start),regex);
    }
    
    public static TreeInfo walk(File start,String regex) {
        return recurseDirs(start,regex);
    }
    
    public static TreeInfo walk(String start) {
        return recurseDirs(new File(start),".*");
    }
    
    public static TreeInfo walk(File start) {
        return recurseDirs(start,".*");
    }
    
    static TreeInfo recurseDirs(File startDir,String regex) {
        TreeInfo treeInfo = new TreeInfo();
        for(File item : startDir.listFiles()) {
            if(item.isDirectory()) {
                treeInfo.dirs.add(item);
                treeInfo.addAll(recurseDirs(item,regex));
            }else {
                if(item.getName().matches(regex))
                    treeInfo.files.add(item);
            }
        }
        return treeInfo;
    }
    
    public static class TreeInfo implements Iterable<File>{

        public List<File> files = new ArrayList();
        public List<File> dirs = new ArrayList();
        
        @Override
        public Iterator<File> iterator() {
            return files.iterator();
        }
        
        void addAll(TreeInfo other) {
            files.addAll(other.files);
            dirs.addAll(other.dirs);
        }
    }
}

  通過工具中的local()方法,我們可以獲得指定目錄下符合要求文件的集合,通過walk()方法可以獲得指定目錄下所有符合要求的文件或目錄,包括其子目錄下的文件,這個工具只是記錄在這里以備不時之需。

 

 2. RandomAccessFile

  因為File類知識文件的抽象表示,並沒有指定信息怎樣從文件讀取或向文件存儲,而向文件讀取或存儲信息主要有兩種方式:

  • 通過輸入輸出流,即InputStream、OutputStream;
  • 通過RandomAccessFile;

  輸入輸出流的方式我們后面會專門總結,這也是Java I/O系統中很大的一塊,本文會講一下RandomAccessFile,因為它比較獨立,和流的相關性不大。

  RandomAccessFile是一個完全獨立的類,其擁有和我們后面將總結的IO類型有本質不同的行為,可以在一個文件內向前和向后移動。我們來看一下其主要方法:

  • void write(int d) 向文件中寫入1個字節,寫入的是傳入的int值對應二進制的低8位;
  • int read() 讀取1個字節,並以int形式返回,如果返回-1則代表已到文件末尾;
  • int read(byte[] data) 一次性從文件中讀取字節數組總長度的字節量,並存入到該字節數組中,返回的int值代表讀入的總字節數,如果返回-1則代表未讀取到任何數據。通常字節數組的長度可以指定為1024*10(大概10Kb的樣子,效率比較好);
  • int read(byte[] data, int off, int len) 一次性從文件中讀取最多len個字節,並存入到data數組中,從下標off處開始;
  • void write(int b) 往文件中寫入1個字節的內容,所寫的內容為傳入的int值對應二進制的低8位;
  • write(byte b[]) 往文件中寫入一個字節數組的內容;
  • write(byte b[], int off, int len) 往文件中寫入從數組b的下標off開始len個字節的內容;
  • seek(long pos) 設置文件指針偏移量為指定值,即在文件內移動至新的位置;
  • long getFilePointer() 獲取文件指針的當前位置;
  • void close() 關閉RandomAccessFile;

  上面只是一部分方法,更多請參考官方文檔。我們再來看一個簡單demo學習一下:

public class RandomAccessFileDemo {  
  public static void main(String[] args) {
    File file = new File("./test.txt");
    if(!file.exists()) {
      try {
        file.createNewFile();
      } catch (IOException e1) {
        e1.printStackTrace();
      }
    }
    RandomAccessFile raf = null;
    try {
      raf = new RandomAccessFile("./test.txt","rw");
      raf.write(1000);
      raf.seek(0);
      System.out.println(raf.read());
      raf.seek(0);      
      System.out.println(raf.readInt());
    } catch (FileNotFoundException e) {
      System.out.println("file not found");
    } catch (EOFException e) {
      System.out.println("reachs end before read enough bytes");
      e.printStackTrace();
    } catch(IOException e) {
      e.printStackTrace();
    }finally {
      try {
        raf.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

  輸出結果:

232
reachs end before read enough bytes

  在RandomAccessFile的構造器中有兩個參數,第一個是文件路徑或者File,代表該RandomAccessFile要操作的文件,第二個是讀寫模式。如果操作的文件不存在,在模式為“rw”時會直接創建文件,如果是“r”則會拋出異常。

  這是一個簡單的例子,首先創建文件test.txt,然后創建一個和該文件關聯的RandomAccessFile,指定讀寫模式為讀寫,調用write()寫入1000,這里只會寫入一個字節,跳到文件頭部,讀取1個字節,輸出232(正好是1000對應二進制的低8位),再跳到文件頭部,調用readInt()讀取1個整數,這時候因為文件中只有1個字節,所以拋出EOFException異常,最后關閉RandomAccessFile。

  如上例子我們學習了RandomAccessFile的基本用法,這里有一點需要注意,RandomAccessFile是基於文件指針從當前位置來讀寫的,並且寫入操作是直接將插入點后面的內容覆蓋而不是插入。如果我們想實現插入操作,則需要將插入點后面的內容先保存下來,再寫入要插入的內容,最后將保存的內容添加進來,看下面的例子:

public class RandomAccessFileDemo {
    
    public static void main(String[] args) throws IOException {
        File file = new File("f:/test.txt");
        file.createNewFile();
        // 創建臨時空文件用於緩沖,並指定在虛擬機停止時將其刪除
        File temp = File.createTempFile("temp", null);
        temp.deleteOnExit();
        RandomAccessFile raf = null;
        try {
            // 首先往文件中寫入下面的詩句,並讀取出來在控制台打印
            raf = new RandomAccessFile(file,"rw");
            raf.write("明月幾時有,把酒問青天".getBytes());
            raf.seek(0);            
            byte[] b = new byte[60];
            raf.read(b, 0, 30);
            System.out.println(new String(b));
            
            // 接下來在詩句中間再插入一句詩
            raf.seek(12);
            FileOutputStream fos = new FileOutputStream(temp);
            FileInputStream fis = new FileInputStream(temp);
            byte[] buffer = new byte[10];
            int num = 0;
            while(-1 != (num = raf.read(buffer))) {
                fos.write(buffer, 0, num);
            }
            raf.seek(12);
            raf.write("但願人長久,千里共嬋娟。".getBytes());
            // 插入完成后將緩沖的后半部分內容添加進來
            while(-1 != (num = fis.read(buffer))) {
                raf.write(buffer, 0, num);
            }
            raf.seek(0);
            raf.read(b, 0, 60);
            System.out.println(new String(b));
            System.out.println();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            raf.close();
        }
    }
}

  輸出結果,插入詩句成功:

明月幾時有,把酒問青天
明月幾時有,但願人長久,千里共嬋娟。把酒問青天

 

3. 總結

  本文是Java I/O系統系列第一篇,主要總結了File和RandomAccessFile的一些知識。

  • File類是對文件和目錄路徑的抽象,用戶通過File來直接執行與文件或目錄相關的操作,無需關心操作系統的差異。
  • RandomAccessFile類可以寫入和讀取文件,其最大的特點就是可以在任意位置讀取文件(random access的意思),是通過文件指針實現的。

 


免責聲明!

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



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