关于字节流和字符流的解析及使用

时间:2022-05-26 04:02:27



在平时的开发中,我们经常需要和系统I/O机制打交道。通常来说底层的数据交换都是通过二进制形式进行交换的,二进制是个好东西,但只是对于机器而言。对于我们人类而言,一串的数字太晦涩难懂了,所以Java给开发者封装了大量用于操作字符流和字节流的类,其中输出字符流和输出字节流是writer和outputStream,输入字符流和输入字节流是reader和inputStream。仔细了解它们的特征,有助于我们更好的操作数据的输入和输出,所以接下来会综合讲解outputStream、inputStream、writer、reader,接着在这基础上继续分析outputStreamWriiter,BufferWriter.....比较重要且日常较多使用的类。




一、字节流、字符流的综述:



1、OutputStream




OutputStream是一个抽象类,他是所有字节输出流的超类,它的作用在于接收需要输出的字节并将它们发送给目标接收池,任何它的子类都必须实现的一个方法是每次输出一个字节的方法,该方法原型如下:


 public abstract void write(int b) throws IOException;


此方法用于将指定的字节写入到输出流,写入的方法通常是将该字节的低8位写入,高24位则被忽略掉。这是所有字节输出流必须实现的方法,此方法在输出流被关闭后调用必须抛出一个IO异常。


对于此类,还有如下几个重要的方法需要了解。


①、输出字节数组,首先参考两个输出字节数组的重载方法:

public void write(byte b[]) throws IOException

public void write(byte b[], int off, int len) throws IOException 

上面两个都是用于输出字节数组到输出流的,先解释第二个,这个方法用于将指定的字节数组的部分内容写进输出流,这部分内容是介于off-len之间的字节,这个方法其实最终会调用outputStream中每次只输出一个字节的write方法,因此,它的输出顺序是有规律的,都是b[off]是第一个被写入的字节,b[len+off]是最后一个被写入的字节。如果off或者len是负数,或者off+len比b.length还要大的话,会抛出IndexOutOfBoundsException。那么第一个方法就很明显了,它用于将字节数组的所有字节写入输出流,等同于write(b,0,b.length)。


②、冲刷缓冲字节,首先提示大家,OutputStream中并没有对flush方法做任何实现,在这里,这个方法是给有需要的子类来重写的,以提高效率。方法原型如下:

public void flush() throws IOException

这个方法用于将之前缓存在输出流(此输出流需要提供缓存功能,此方法才有意义。比如BufferOutputStream)的字节立刻输出到目标接收池。如果此方法的目标接收池是有操作系统控制(比如将字节数组写到文件里面,文件一般是保存在硬盘上的,此过程需要操作系统来完成)的,flush不保证一定会把缓存的字节数组写入硬盘,它只保证把缓存数据交接给操作系统。


③、关闭输出流,方法原型如下:

public void close() throws IOException

close方法会将当前的输出流关闭掉并释放与此对象相关的任何资源,一旦调用了这个方法的话,之后便不能对此输出流对象进行任何输出操作,并且此对象无法再次打开来使用。在outputStream中,此方法是没有任何实现的,是没有意义的,所以必须由它的子类来重写这个方法,才有意义。




2、inputStream



InputStream是一个抽象类,他是所有字节输入流的超类,它的作用在于读取目标字节并返回,任何它的子类都必须实现的一个方法是读取下一字节并返回的方法,该方法原型如下:


public abstract int read() throws IOException


此方法用于读取输入流的的字节数据,返回值介于0-255之间,如果读取的字节流到达了末端导致没有可读数据就会返回-1。此方法还会导致线程阻塞,除非有可读数据返回或者到达了输入流尾端或者是抛出了异常,线程才会释放。这是所有输入流的子类必须实现的方法,也是各种输入流提高效率的途径。



对于此类,还有如下几个重要的方法需要了解。


①:读取输入流的字节数据并保存在指定的字节数组中,关注一下两个方法:


 public int read(byte b[]) throws IOException

 public int read(byte b[], int off, int len) throws IOException 


同样的,先解释第二个,此方法用于从输入流中读取部分内容并将读取的值(注意读取的方法最终是调用read())存储在b中。其中off表示存储于b中的第一位字节的下标,len表示长度。如果出现len是0的话,此方法会直接返回0,否则的话部分数据将会存储于b中,存储数据是从b[off]-b[len-1],但是要注意,并不是一定能够读到指定长度的数据的,因为输入流的字节数据可能达不到指定的长度。假如当前读取的字节流数据长度是k的话,那么b[0]-b[off-1]以及b[off+k-1]之后的数据都是没有的,同时此方法在读取的时候会导致线程阻塞,除非有可读数据返回或者到达了输入流的末尾或者抛出了异常。很多输入流的子类都会重写此方法为了实现更高效的读取效果。



②、预测输入流可读数据的长度:

 public int available() throws IOException 

此方法用于预测当前输入流可读数据的长度,在调用此方法时,并不会影响其它线程对此输入流的操作,因此并不会造成线程堵塞。但要注意的是此方法一般是由具体的输入流的子类来实现的,有些实现是返回整个可读数据的长度,有的是直接返回0,因此如果想要用此方法来获取一个数值来申请缓存空间的大小是不可取的,因为这个方法返回的数据并不可靠。



③、关闭输入流并释放资源:

public void close() throws IOException

一旦调用了了close方法,就不能够在对此输入流执行任何操作了。



④、重置inputstream的内容,要想实现重复利用inputStream,必须要搞明白三个方法:

public boolean markSupported()

用于判断当前的输入流对象是否支持mark方法和reset方法。返回true表示支持,false相反。因此在调用mark方法之前应该用此方法先进行判断。接下俩是标记方法:

public synchronized void mark(int readlimit)

用于标记当前inputStream的位置,以至于后续调用reset方法的时候,可以恢复标记位置之后的数据。其中readlimit参数比较复杂,不同的子类有不同的含义,这里不进行解释了。即当我们读取到当前字节流的某个位置时,调用mark方法,那么在此方法之后所有被读取的字节数据都会被记住,这样,当我们调用reset方法的时候,就可以重新回到标志位,读取相同的数据。

 public synchronized void reset() throws IOException

这个方法的功能在上面已经讲到了,就是用于将mark位置之后的字节数据进行恢复。在讲完综合这一章节,会继续给大家一个例子




3、writer




Writer是一个抽象类,代表着一个字符流,作为它的子类,有三个方法是必须实现的,分别是write(char[],int,int),flush()和close()。但其实在writer里面的大部分方法都会被其子类重写,因为不同的子类,都会通过重写部分方法来达到提高读取效率的效果。在writer里面,会有一个字符数组用于缓存从输入流读取的字符,所以通常都需要调用flush方法将内容冲刷进目标接受池。


①、将字符写入字符流:


其中,writer的子类必须实现的抽象方法write(char[],int,int)的原型如下:

abstract public void write(char cbuf[], int off, int len) throws IOException;

此方法用于将cbuf的部分数据写入字符流,其中off、len参数和上面一样。对于读取字符的方法 ,还有以下两个重载:

public void write(int c) throws IOException 

此方法用于将单个字符写入字符流并且会进行缓存。同时还有:

 public void write(char cbuf[]) throws IOException

此方法等同于调用write(cbuf,0,cbuf.length)。但是用的最多的是输出字符串的方法,此功能函数有两个重载:

public void write(String str, int off, int len) throws IOException

 public void write(String str) throws IOException

先讲第一个,此方法用于将str的部分内容输出,off,len参数一样,在这个方法的内部,会通过string。getChar方法获得字符集合,然后再调用write(char[]buf,int,int)方法。第二个等同于writer(str,0,str.length)。


②、冲刷字符输出流:


abstract public void flush() throws IOException;

此方法和outputStream的特征一样,但是不同的地方在于,如果此字符输出流还包含着下层的字节输出流或者字符输出流的话,那么连同下层缓存的字节内容和字符内容都会一并冲刷至缓存区域。对于需要操作系统提供支持的写操作,此方法只保证将数据冲刷给操作系统,并不保证将数据写入硬件。


③、关闭字符输出流:

abstract public void close() throws IOException

此方法用于关闭字符输出流,不同的地方在于,在关闭字符输出流的时候,所缓存的数据是会被冲刷掉的。这个和字节输出流(比如outputStream并没有进行缓存字节的操作)有所不同。


在writer里面还包含了很多append方法的重载,用于在字符输出流中添加字符集合。因为这些方法和调用write方法差不多,因此这里不过多解释了。不过值得注意的是,append方法返回的结果是当前的writer对象,因此可用链式的方式不断的添加字符集合输出内容。




4、Reader


Reader是一个抽象类,是所有字符输入流的基类。作为它的子类,必须要实现的方法是read(char[],int,int)以及close方法,但通常情况下很多子类都会重写reader的部分方法一获得更高的效率或者实现一些附加的功能函数。

①、读取输入流的字符并存储于指定字符数组中:

abstract public int read(char cbuf[], int off, int len) throws IOException

其中cbuf表示存储读取的数据的字符数组,off,len分别表示存储的起始位置。

public int read(char cbuf[]) throws IOException

等同于read(cbuf,0,cbuf.length)。


②、读取单个字符:


 public int read() throws IOException 

此方法会读取下一个字符并返回。调用read的方法都会导致线程堵塞,除非返回了可读数据或者是抛出了异常又或者都到了文件的末尾。


③、恢复字符流的顺序:

  public boolean markSupported()


检测当前字符流是否支持标记当前位置的操作。

然后是标记当前位置的方法:

  public void mark(int readAheadLimit) throws IOException 

最后是重新配置字符流的方法,此方法会让mark标记的位置之后的所有字符恢复,从而能够读取一模一样的数据。


④、关闭字符输出流


abstract public void close() throws IOException

用于关闭当前的字符输入流并且释放相关的资源。调用此方法后,任何有关此操作对象的方法都将失效。


5、总结


可以知道outputStream主要用于输出字节数据,通过write方法可以输出当个字符或者字符数组,这是个抽象类,并不能直接使用,所以我们一般使用它的子类来操作字节输出。inputStream用于读取字节数据,可以读取当个字节并返回,也可以将读取的字节读取后存放到我们指定的字节数组中。writer用于输出字符数据,reader用于读取字符数据。writer和read方法和outputStream以及inputStream都是一样的,除了操作的对象之外。下面举个简单的例子来综合运用上面提到的知识。


此列子是输出一串字符串,并标记'y'字符,使得'y'字符后面的字符可以重复读取。

package cn.com.chinaweal.stream;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        String content = "lalalalayonggandekaiba";
        ByteArrayInputStream bios = new ByteArrayInputStream(content.getBytes());
        StringBuffer sb = new StringBuffer();
        int i = 0;
        boolean isReapter = false;
        while ((i = bios.read()) != -1) {
            char temp=(char)i;
            sb.append(temp);
            //当调用reset之后,会恢复y后面的字符
            if ((temp == 'y') && bios.markSupported() && !isReapter)
                bios.mark(bios.available());
            if ((temp == 'e') && bios.markSupported() && !isReapter) {
                bios.reset();
                isReapter = true;
            }
        }
        try {
            bios.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        Log.i("test", sb.toString());
    }
}

输出结果如下:

08-05 13:36:16.464 7760-7760/cn.com.chinaweal.stream I/test: lalalalayonggandeonggandekaiba






在第一部分呢,讲解了字符流和字节流的超类,通过对这些超类的基本了解,可以知道,对于所有的字节流和字符流都有哪些基本操作。通过第一部分的了解,也得出了reader,writer,inputStream,outputStream都是抽象类,这意味着,它们并不能直接被我们开发者使用。同时,因为这些超类的基本读写方法都是抽象的,所以这就要求我们对于它们的子类有一定的了解,才能结合具体的场景,去搭配不同的子类一提供更加便利高效的读写方法。所以很有必要对字符流和字节流中比较特殊的子类有一定的了解。



二、更好的具有缓存功能的字符流和字节流




接下来,会介绍用于提高读写效率的具有缓冲数据作用的子类,通常来说,如果我们需要和字符流或者字节流打交道,最好就是用含有缓冲功能的子类进行对字符流或者字节流的包裹,这样对于我们的读写操作会有更高的表现性能以及更多的便捷操作方式。接下来将介绍FilterInputStream,FilterOuputStream,BufferedInputStream,BufferedOutPutStream,BufferedReader,BufferedWriter这几个常用的具有高效率的读写方法的类。



1、FilterInputStream,FilterOutputStream


之所以将这两个类一起解释,是因为这两个类相对于inputStream和outputStream来说,都是具有一样的效果的。先解释一下,为什么在这个章节需要了解这两个类。因为这两个类是BufferedInputStream和BufferedOutputStream的父类,其实不止这两个类,很多字节输出流和字节输入流的子类,但凡涉及到含有底层输出流或者输入流的,都会分别继承这两个类。这么理解,比如,BufferedOutputStream是一个含有outputStream对象作为底层输出流的对象,这从它的的构造方法就可以看出来。因此了解这两个类,是了解 BufferedInputStream和BufferedOutputStream的基础。但是,这两个类其实在另一个方面来说,也没必要去了解,为什么?这就涉及到这两个类的实现了。这两个类分别是inputStream和outputStream的子类,它里面含有的方法都是inputStream和outputStream的基本方法。而这些方法的实现,都是用通过调用底层输入流的对应的方法来实现的。举个例子。FilterInputStream初始化的时候会给它指定一个inputStream对象作为它的底层输入流,当我们调用FilterInputStream的read方法的时候,实际上是直接调用底层输入流对象的read方法,并且没有任何其他的多余的逻辑操作了。因此,知道了这个点以及知道了inputStream和outputStream都有哪些基本方法,就等于知道了这两个类是干什么的了。



2、BufferedOutputStream



这是一个具有缓存输出的字节数据作用的字节输出流,它的存在,意义在于当开发者调用write方法进行写数据的时候,不用每次都调用I/O系统,而是会将数据线缓存起来,等到到达了一个限度之后,在一次性调用系统的I/O操作,这样就大大提高了写数据的效率。要了解它是怎么起到缓存效果的,我们需要了解一下几个方面:



①、缓存数组以及缓存大小。


protected byte buf[];


public BufferedOutputStream(OutputStream out)


public BufferedOutputStream(OutputStream out, int size)


在此类中含有一个Byte数组用于缓存读取的字节数据,等到达到一定的缓存闲置之后,才会调用底层的输出流进行写操作。缓存闲置默认是1024*8(8kb)个字,当然,从上面的两个构造方法可以看出,通过调用第二个构造函数,就可以通过size来指定buf数组的大小,即缓存数据的大小。


②、起到缓存数据达到限制后,将数据交由底层输出流进行写操作的方法:


  private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);
            count = 0;
        }
    }

此方法适用于将数据交由底层输出流的,其中count表示当前buf里面缓存的数据的大小,可以看到,每次调用此方法后,count都变为0了,意味着,调用此方法之前的缓存数据都会被底层输出流操作,此方法之后的缓存数据会从头开始进行保存,即覆盖旧的缓存数据。


此类中,还含有两个write方法以及一个flush方法,前者用于书写要输出的数据,都是先判断当前的缓存数据是否达到了限制,达到的话就会调用上面的方法进行将缓存的数据输出并将现在要输出的数据进行缓存。没有达到的话,就直接进行缓存。flush方法就是直接将缓存的数据交由底层输出流操作。



3、BufferdInputStream



BufferedInputStream具有将数据源的数据缓存的作用。缓存的目的在于,此类给我们提供了mark和reset方法的支持,以至于后续的操作我们可以读取重复的数据。这个类会包含一个底层输入流,以及一个缓存数组,还有很多的常量用于进行标识作用。使用此类对于读取操作的最大用处就是,可以进行数据的重复读取,接下里就了解一下,这个过程怎么实现的。


①、缓冲数组及缓存数据的容量


protected volatile byte buf[]

public BufferedInputStream(InputStream in)

public BufferedInputStream(InputStream in, int size)

其中buf用于缓存读取的数据,便于后续进行重复读取,缓存的大小默认是8kb,但是同样的,可以通过调用第二个构造函数的size来指定缓存容量的大小。


②、具有标记功能的常量


protected int count;


这是一个用于指示当前缓存数组中已缓存的数据的大小,通常它是介入0-buf.length之间。


protected int pos;


这是一个用于标记下一个应该读取的缓存数据的索引值。比如pos=1,表示下一个应该读取的数据是buf[1]。


protected int markpos


这是一个用于标记后续需要重复读取的缓存数据区间的起始索引。只有当调用了mark方法之后,这个值才有意义。


③、起到将数据进行缓存的方法


 private void fill() throws IOException


此方法是我们在调用read方法的时候,api调用的方法,它的作用是将读取过的字节数据进行缓存便于后续我们需要进行重复读取数据的操作。


④、重复读取数据


在第一部分已经提到过了,要想重复读取一样的字节数据,必须结合使用mark,reset以及markSupported方法,在BufferInputStream中,markSupported方法是返回true的。因此,此类是支持重复读取数据的操作的。剩余的方法和签名提到的一样。另外,此类的avaliable方法返回的结果是,当前输入流可被读取的字节的总数。



4、BufferedWriter



此类是具有缓存字符功能的字符输出流,同样的,它的缓存容量大小可以通过构造方法来制定。这个类中有一个特殊的方法是newLine方法,作用是进行换行,为什么说它特殊,是因为,这个方法返回的值,并不是单纯的\n换行字符,而是一个当前的操作平台获取的具有换行效应的属性值。如果开发者需要进行字符的血操作的话,是很强烈要求推荐用这个类来包括其他的字符输出流,从而来提高我们输出的字符的效率的。


同样的,此类中和BufferedOuputStream类一样,都具有一个起到缓存作用的数组,不同的是這里的是char数组,另外就是,此类的write方法不仅可以写char数组,还可以直接写string类型的值。剩余的,关于这个类的基本操作和writer是一样的,不明白的朋友去看我写的第一部分。



5、BufferedReader


此类一样是一个具有缓存作用的字符输入流,使用此类的目的在于提高读取效率,如果我们要操作的字符输入流的对象的读取方法是很耗时的,就应该用此类来进行包裹,这样在每次我们调用read方法的时候,就不是从底层的输入流对象数据而是从当前的缓存字符数组中读取,从而提高了效率。此类中的大部分方法都是和bufferdInputStream的类似,不同的是操作的对象不同,前者操作的是字节数据,后者是字符数据。在此类中比较特殊的是readLine方法,此方法的作用是读取输入流中的数据,读取的方式是一行一行的都,因此如果此方法返回的是null,表面以及读取到了内容的结尾处。此方法也有重复读取数据的功能,方法和上面的一样,就不再累赘了。



下面通过一个例子来使用上面的知识。例子中将一串字符写入文件然后在读取出来。关注点是如何搭配这些输入输出对象。


package cn.com.chinaweal.stream;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * Created by Myy on 2016/8/6.
 */
public class BufferActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        String content = "content to be write,content to be write,content to be write,content to be write";
        File file = new File(Environment.getExternalStorageDirectory().getPath() + "/buffer.txt");
        PrintWriter stringWriter = null;

        try {
            //BufferedWriter的作用是用于提高输出效率。
            //PrinterWriter的做用在于提供了类似println等操作的便捷方式
            stringWriter = new PrintWriter(new BufferedWriter(new FileWriter(file)));
            stringWriter.println(content);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            stringWriter.close();
        }
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(file));
            StringBuilder sb = new StringBuilder();
            String temp = null;
            while ((temp = reader.readLine()) != null) {
                sb.append(temp);

            }
            Log.i("test", sb.toString());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}


还有第三部分,用于讲解具有不同功能的输入流和输出流。


---------文章写自:HyHarden---------

--------博客地址:http://blog.csdn.net/qq_25722767-----------