Java基礎之IO(1)-InputStream

概述

之前介紹過一篇IO總覽的文章,概述性的講解了現有的一些Java IO。從這一篇開始,詳細講解Java IO的各個模塊,今天首先講一下InputStream。

InputStream是一個字節輸入流,什麼是字節流呢?我們知道在java中有一個叫byte的基本類型,就是同一個。字節是一個8位表示的二進制。很多人會誤認爲輸入流就是打開一個磁盤文件然後從裏邊讀數據,但是輸入流並不侷限於文件,應該說IO並不代表和磁盤交互。所以讀取磁盤文件僅僅是輸入流中的一種FileInputStream,在輸入流中還有很多其他的輸入流。接下會介紹幾種,雖然有些不常用。在介紹之前先看一下在JDK中,InputStream的繼承關係:

在這裏插入圖片描述

AutoCloseable

這裏的AutoCloseable是jdk1.7引入的一種語法糖,可配合jdk1.7出現的try-with-resources新語法特性一起使用,用於在使用完資源後自動關閉。Closeable是表示這是一個可關閉的數據源或目標。

public class AutoCloseableDemo {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("/tmp/test")){
            int i;
            while ((i=fis.read()) != -1){
                System.out.println(i);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

節點流

什麼是節點流呢?節點流指可以從一個數據源讀寫數據,這個數據源可以是磁盤文件,內存或者網絡。根據這個定義可以將上圖中的流FileInputStream、PipedInputStream、ByteArrayInputStream劃分爲節點流。最熟悉的可能就是FileInputStream,因爲平時讀取磁盤文件應該用得最多,先介紹FileInputStream。

FileInputStream

看一下該類的構造函數

//參數:文件路徑
public FileInputStream(String name) throws FileNotFoundException;
//參數:一個File對象
public FileInputStream(File file) throws FileNotFoundException;
//一個文件描述符
public FileInputStream(FileDescriptor fdObj);

通過jdk api文檔可以看到FileInputStream有三個構造函數,第一個和第二個比較常用,第三個是一個已存在的文件系統的真實連接。

public class FileInputStreamDemo {
    /**
     * 文件路徑參數創建
     * @throws IOException
     */
    public void FilePathDemo() throws IOException {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("/tmp/test");
            int i;
            while ((i=fis.read()) != -1){
                System.out.println(i);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                fis.close();
            }
        }
    }

    /**
     * 以File對象方式創建
     */
    public void FileDemo() throws IOException {
        File file = new File("/tmp/test");
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            int i;
            while ((i=fis.read()) != -1){
                System.out.println(i);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                fis.close();
            }
        }
    }

    /**
     * 以文件描述符參數創建
     */
    public void FileDescriptorDemo() throws IOException {
        FileInputStream fis = new FileInputStream(FileDescriptor.in);
        System.out.println(fis.read());
        fis.close();
    }

    public static void main(String[] args) {
        FileInputStreamDemo demo = new FileInputStreamDemo();
        try {
            demo.FilePathDemo();
            demo.FileDemo();
            demo.FileDescriptorDemo();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
ByteArrayInputStream

ByteArrayInputStream比較少見,ByteArrayInputStream內部維護着一個緩衝區,從該緩衝區中讀取字節,內部計數器跟蹤read要讀取的下一個字節。看一下構造函數:

ByteArrayInputStream(byte[] buf);

ByteArrayInputStream(byte[] buf, int offset, int length)

可以看到ByteArrayInputStream以byte數組爲參數構造ByteArrayInputStream對象。ByteArrayInputStream和FileInputStream的區別在於,FileInputStream以磁盤文件爲數據源,ByteArrayInputStream以內存數據爲數據源。ByteArrayInputStream的用法:

public class ByteArrayInputStreamDemo {
    public static void main(String[] args) {
        byte[] bytes = new byte[]{1, 2, 3, 4, 5};
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes, 2, 2);
        int i;
        while ((i = byteArrayInputStream.read()) != -1){
            System.out.println(i);
        }
        try {
            byteArrayInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果:

Connected to the target VM, address: '127.0.0.1:38135', transport: 'socket'
3
4
Disconnected from the target VM, address: '127.0.0.1:38135', transport: 'socket'

Process finished with exit code 0

從結果中看到雖然bytes有5個數據,但是隻能讀取2個數據,因爲在構造時傳入了offset和length表示從offset開始的length個數據。至於ByteArrayInputStream的內部實現細節,在以後的進階文章中再詳細論述。

PipedInputStream

PipedInputStream是一種很特殊的輸入流,因爲它單獨存在沒有什麼意義,它需要和PipedOutputStream一起使用,數據的流向是從PipedOutputStream流向PipedInputStream的緩存區,然後PipedInputStream的read方法從PipedInputStream的緩衝區中讀取數據。如下圖

在這裏插入圖片描述

接下來看一下使用PipedInputStream來實現一個簡單線程通信例子,當然線程通信有其他方式來實現,但是如果兩個線程之間需要傳遞字節數組時,PipedInputStream還是不錯的選擇。

public class PipedInputStreamDemo {
    public static void main(String[] args) {
        PipedInputStream pipedInputStream = new PipedInputStream();
        PipedOutputStream pipedOutputStream = new PipedOutputStream();

        try {
            pipedOutputStream.connect(pipedInputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }

        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new ReadWorker(pipedInputStream));
        executor.execute(new WirteWorker(pipedOutputStream));

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executor.shutdown();
    }
}

class ReadWorker implements Runnable {
    private PipedInputStream pipedInputStream;

    public ReadWorker(PipedInputStream pipedInputStream){
        this.pipedInputStream = pipedInputStream;
    }

    @Override
    public void run() {
        byte[] bytes = new byte[1024];
        try {
            while (pipedInputStream.read(bytes) != -1){
                System.out.println(new String(bytes, Charset.forName("UTF-8")));
            }
        } catch (IOException e){

        } finally {
            try {
                pipedInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

class WirteWorker implements Runnable {
    private PipedOutputStream pipedOutputStream;

    public WirteWorker(PipedOutputStream pipedOutputStream){
        this.pipedOutputStream = pipedOutputStream;
    }
    @Override
    public void run() {
        String str = "hello world!";
        try {
            pipedOutputStream.write(str.getBytes(Charset.forName("UTF-8")));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                pipedOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

處理流

什麼又是處理流呢?處理流是連接在已存在的流(節點流或者處理流)之上,通過對數據的處理提供更強大的讀寫功能。在InputStream中有如下幾種處理流,BufferedInputStream、DataInputStream、ObjectInputStream、SequenceInputStream。接下來簡單介紹一下幾種流。

BufferedInputStream

BufferedInputStream是一個處理流,它的作用是增強InputStream,爲其提供緩衝區的功能。這裏可能會有疑問,BufferedInputStream實現的方法和InputStream的方法差不多,它存在的意義是什麼?

這裏要從Java IO讀取數據的過程說起,僅僅簡單的介紹一下,InputStream讀取數據源的數據需要操作系統支持,也就是說Java的線程發出讀取數據(read方法)的請求,操作系統接收到這個請求,然後從數據源讀取一個byte(read方法,不是read(byte[])),然後將數據從內核空間複製到用戶空間,java線程拿到要的數據,返回。可以看到,如果調用read()方法,那麼讀取10個字節,就要進行10個這樣的流程,是相當低效的,當然可以用read(byte[]),但是有時需要read()的場景,所以就出現了InputStream的增強類BufferedInputStream,該BufferedInputStream提供了緩存區的功能。

舉個例子,以前你要把一堆磚從一個地方搬到另一個地方整理好,你力氣小一次只能搬一塊,當然別人可能力氣大,一次可以搬兩塊,這樣你搬10塊磚要來回跑10次,那現在爲了增強你的能力,給你提供一個小車,你一次可以運50塊,這個時候你只需要一次就可以運50塊磚,而你要整理好這些磚,只需要從小車上一次一塊拿下來放好,就像從緩衝區裏讀取數據一樣。通過提供緩衝區來增強IO的能力,減小系統開銷。

簡單使用

public void readDemo(String path){
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        try {
            fis = new FileInputStream(path);
            //可以在構造時傳入參數指定緩衝區的大小
            bis = new BufferedInputStream(fis, 2048);
            int data;
            while ((data = bis.read()) != -1){
                System.out.println(data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void readBytesDemo(String path){
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        try {
            fis = new FileInputStream(path);
            bis = new BufferedInputStream(fis);
            byte[] bytes = new byte[1024];
            while (bis.read(bytes) != -1){
                System.out.println(bytes);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        BufferedInputStreamDemo demo = new BufferedInputStreamDemo();
        demo.readDemo("/tmp/test");
        demo.readBytesDemo("/tmp/test");
    }
DataInputStream

DataInputStream提供了從字節輸入流中讀取Java基本類型的能力,比如,如果要想從輸入流中讀取一個int類型的數據,需要讀取4個字節然後自己組裝成一個int,而DataInputStream通過提供一個InputStream的裝飾器,增加了自動包裝的功能。

簡單使用

public class DataInputStreamDemo {
    public static void main(String[] args) {
        FileInputStream fis = null;
        DataInputStream dis = null;
        FileOutputStream fos = null;
        DataOutputStream dos = null;

        try {
            fos = new FileOutputStream("/tmp/test1");

            dos = new DataOutputStream(fos);

            dos.writeChars("hello world");
            dos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (dos != null){
                try {
                    dos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        try {
            fis = new FileInputStream("/tmp/test1");
            dis = new DataInputStream(fis);
            while(dis.available() > 0) {
                System.out.println(dis.readChar());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (dis != null){
                try {
                    dis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

打開文件test1看到的結果,如果以字節流讀取出來,將兩兩字節組合轉換爲char便可以還原。DataInputStream提供的就是這種功能,此外還有readInt()、readLong()、readFloat()等,比較特殊一點的readUTF(),readUTF是轉化成utf-8編碼格式的,用readUTF()讀,那麼你寫的時候也要用writeUTF()寫,writeUTF在寫時會在開頭寫兩個字節表示寫入的數據大小。readUTF時會首先讀取這兩個字節,來設置讀取的字節數。

^@h^@e^@l^@l^@o^@ ^@w^@o^@r^@l^@d

讀取結果:

Connected to the target VM, address: '127.0.0.1:44515', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:44515', transport: 'socket'
h
e
l
l
o
 
w
o
r
l
d

Process finished with exit code 0
ObjectInputStream

ObjectInputStream是java提供的反序列化的功能,ObjectOutputStream提供序列化輸出的功能,能將java對象序列化之後輸出到指定的輸出流中。被持久化的對象需要實現Serializable、Externalizable接口。

public class ObjectOutputStreamDemo {
    public static void main(String[] args) {
        FileOutputStream fileOutputStream;
        ObjectOutputStream objectOutputStream = null;
        {
            try {
                fileOutputStream = new FileOutputStream("/tmp/person");
                objectOutputStream = new ObjectOutputStream(fileOutputStream);
                Person person = new Person();
                person.setName("mark");
                person.setAge(20);
                objectOutputStream.writeObject(person);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (objectOutputStream != null) {
                        objectOutputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            FileInputStream fileInputStream;
            ObjectInputStream objectInputStream = null;
            try {
                fileInputStream = new FileInputStream("/tmp/person");
                objectInputStream = new ObjectInputStream(fileInputStream);
                Person person = (Person) objectInputStream.readObject();
                System.out.println("Person name:" + person.getName() + " age:" + person.getAge());
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (objectInputStream != null) {
                        objectInputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class Person implements Serializable {
    private String name;

    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}
SequenceInputStream

SequenceInputStream的作用是將多個輸入流合併爲一個SequenceInputStream輸入流,然後從第一個輸入流開始讀取數據,接着第二個,直到最後一個。

public class SequenceInputStreamDemo {
    public static void main(String[] args) {
        try {
            FileInputStream is1 = new FileInputStream("/tmp/test1");
            FileInputStream is2 = new FileInputStream("/tmp/test2");

            SequenceInputStream sis = new SequenceInputStream(is1, is2);
            int i;
            while ((i = sis.read()) != -1){
                System.out.println(i);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

文章首發地址:IkanのBolg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章