字节流、字符流及其转换、比较与使用场景

本文包含以下知识点:

  • 字节流OutputStream和InputStream类及其子类
  • 字符流Writer和Reader类及其子类
  • 转换流OutputStreamWriter和InputStreamReader
  • 代码示例
  • 字节流、字符流的比较
  • 各类的使用场景

在程序中所有的数据都是以流的方式进行传输和保存的。程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成

在java.io包中操作文件内容的主要有两大类:字节流、字符流。两类都分为输入和输出操作。输入流基本方法是读,输出流基本方法是写。

输入流输出流是相对于内存而言的,程序运行在内存中。

从外存读取数据到内存以及将数据从内存写到外存中:

输入:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中,输入流。

输出:将数据从程序 (内存) 中写到磁盘、光盘等存储设备中,输出流。

大致操作流程:(以File为例)

  1. 使用File类打开一个文件;
  2. 通过字节流或字符流的子类,指定输出的位置;
  3. 进行读/写操作;
  4. 关闭输入/输出。

java中的流是对字节序列的抽象,流中保存的实际上全都是字节文件。

字节流

字节流处理的基本单元为1个字节(byte),操作字节和字节数组,通常用来处理二进制数据。

面向字节流的OutputStream和InputStream是一切的基础。均为抽象类,实际使用的是它们的一系列子类。

字节输出流OutputStream

类与方法定义

抽象类,定义如下:

1
2
3
public abstract class OutputStream
implements Closeable, Flushable {}
//Closeable表示可关闭,Flushable表示刷新,清空内存中的数据

方法有:

1
2
1.将一个字节数据写入数据流
public abstract void write(int b) throws IOException;
1
2
2.将一个byte数组写入数据流
public void write(byte b[]) throws IOException
1
2
3.将指定byte数组中从偏移量off开始的len个字节写入数据流
public void write(byte b[], int off, int len) throws IOException
1
2
3
4
4.刷新缓冲区
public void flush() throws IOException {}
5.关闭输出流
public void close() throws IOException {}

要想使用此类及以上方法,必须通过子类实例化对象。

继承OutputStream的子类

images

FileOutputStream

文件输出流,是向File或FileDescriptor输出数据的一个输出流。

FileOutputStream类的构造方法有:

1.创建一个文件输出流,向指定的File对象输出数据(写入)

1
2
3
4
5
public FileOutputStream(File file) throws FileNotFoundException 
//字节写到文件的开始,输出会进行覆盖

public FileOutputStream(File file, boolean append) throws FileNotFoundException
//如果append设为true,字节将写到文件末尾

2.创建一个文件输出流,向指定名称的文件输出数据(写入到指定名称的文件中)

1
2
3
4
5
6
public FileOutputStream(String name) throws FileNotFoundException
//输出会进行覆盖

public FileOutputStream(String name, boolean append)
throws FileNotFoundException
//如果append设为true,字节将写到文件末尾

3.创建一个文件输出流,向指定的文件描述器输出数据,该文件描述符表示文件系统中一个实际文件的现有连接。

1
public FileOutputStream(FileDescriptor fdObj)

OutputStream的其他子类

1.ObjectOutputStream(对象输出流)

对象输出流将Java对象的原始数据类型和对象图写入输出流。只有支持可序列化接口的对象才可以写入到输出流。

2.ByteArrayOutputStream(字节数组输出流)

该类实现了一个以字节数组形式写入数据的输出流,缓冲区会随着数据的写入而自动扩大。可以用toByteArray()和toString()检索数据。

构造方法有:

(1)创建一个新的字节数组输出流

1
2
3
public ByteArrayOutputStream() {
this(32);}
//缓冲区容量最初是32字节,如果需要,大小会增加

(2)创建一个新的字节数组输出流,并带有制定大小字节的缓冲区容量

1
public ByteArrayOutputStream(int size)

3.PipedOutputStream(管道输出流)

管道输出流可以连接到管道输入流来创建通信管道,它是这个管道的发送端。

一个线程通过管道输出流写入(发送)数据,另一个线程通过管道输入流读取数据,这样可实现两个线程之间的通讯。

不建议尝试从单个线程使用这两个管道流对象,因为它可能会造成线程死锁。

构造方法有:

(1)创建连接到指定管道输入流的管道输出流

1
public PipedOutputStream(PipedInputStream snk)  throws IOException

(2)创建一个管道输出流,该输出流尚未连接到管道输入流

1
public PipedOutputStream()

4.FilterOutputStream

其子类有DataOutputStream、BufferedOutputStream、PrintStream。

代码示例:将序列化对象写入文件

创建文件,输出字节的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public static void main(String[] args) {
try {
File f = new File("E:/test1.txt");
//如果文件不存在,会自动创建出来
FileOutputStream fs = new FileOutputStream(f);
//如果想在文件中执行追加,构造方法中append设为true
String str = "Hello World!";
byte[] b = str.getBytes();
//因为是字节流,先将字符串转化为字节数组
fs.write(b);
//这里也可以一个字节一个字节进行输出
//for(int i=0;i<b.length;i++){fs.write([i]);}
fs.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

将序列化对象写入文件,会用到字节输出流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 将序列化对象存储到文件
*/
public class Test {
public static void main(String[] args) {
//创建一个Use对象
User user = new User(1, "amy");
//创建一个List对象
List<String> list = new ArrayList<String>();
list.add("hello");
list.add("I am");
list.add("amy");
try {

FileOutputStream fs = new FileOutputStream("E:/test.ser");
//如果文件不存在,会自动创建出来
ObjectOutputStream os = new ObjectOutputStream(fs);
//ObjectOutputStream能让写入对象,但无法直接连接文件,所以需要参数指引
os.writeObject(user);
os.writeObject(list);
//写入对象
os.close();
//关闭ObjectOutputStream
} catch (IOException ex) {
ex.printStackTrace();
}catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
public class User implements Serializable {
//User类实现序列化
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}

如下图所示

image

字节输入流InputStream

类与方法定义

抽象类,定义如下:

1
public abstract class InputStream implements Closeable

方法有:

1
2
3
1.读取一个字节无符号填充到int的低8位,高8位补零,读取到-1结束
public abstract int read() throws IOException;
//read()返回读取的字节或-1(-1表示读取结束)
1
2
2.将当前输入流中b.length个字节读入到byte数组中,实际读取的字节数作为整数返回
public int read(byte b[]) throws IOException
1
2
3
3.将输入流中len个字节数据读入一个字节数组中,从数组的off位置开始存放len长度的数据
public int read(byte b[], int off, int len) throws IOException
//带有参数的read方法返回的是读取字节数或-1,内部也在调用read(),读取字节
1
2
3
4
5
6
7
8
4.从该输入流中跳过或忽略n字节
public long skip(long n) throws IOException

5.返回这个输入流中可读(或跳过)的字节数估计值,即可取得输入文件的大小
public int available() throws IOException

6.关闭输入流
public void close() throws IOException

要想使用此类及以上方法,必须通过子类实例化对象。

继承InputStream的子类

image

FileInputStream

文件输入流,从文件系统中的文件读取输入字节。

FileInputStream的构造方法有:

1.创建一个输入文件流,从指定的File对象读取数据

1
public FileInputStream(File file) throws FileNotFoundException

2.创建一个输入文件流,从指定名称的文件读取数据

1
public FileInputStream(String name) throws FileNotFoundException

3.创建一个输入文件流,从指定的文件描述器读取数据

1
public FileInputStream(FileDescriptor fdObj)

InputStream的其他子类

1.ObjectInputStream(对象输入流)

2.ByteArrayInputStream(字节数组输入流)

把内存的一个缓冲区作为InputStream使用。

构造方法有:

(1)创建一个字节数组输入流,使用buf作为它的缓冲区数组,从缓冲区数组中读取数据

1
public ByteArrayInputStream(byte buf[])

(2)创建一个字节数组输入流,从指定字节数组中读取数据

1
2
3
public ByteArrayInputStream(byte buf[], int offset, int length)
//offset表示读取缓冲区中第一个字节的偏移
//length表示从缓冲区读取的最大字符数

3.PipedInputStream(管道输入流)

管道输入流是一个通讯管道的接收端。

构造方法有:

(1)创建连接到指定管道输出流的管道输入流

1
2
3
4
5
 public PipedInputStream(PipedOutputStream src) throws IOException

public PipedInputStream(PipedOutputStream src, int pipeSize)
throws IOException
//使用指定的管道大小作为管道的缓冲

(2)创建一个管道输入流,该输出流尚未连接到管道输出流

1
2
3
4
public PipedInputStream()

public PipedInputStream(int pipeSize)
//使用指定的管道大小

4.SequenceInputStream(序列输入流)

此类允许应用程序把几个输入连续地合并起来,它从一个有序的输入流集合开始,每个输入流依次被读取到文件的末尾,直到到达最后一个输入流的文件末尾。

构造方法有:

(1)参数中枚举生成运行时类型为输入流的对象。新创建一个序列输入流,通过读取由枚举产生的输入流来初始化它。

1
public SequenceInputStream(Enumeration<? extends InputStream> e)

(2)新创建一个序列输入流,按参数顺序读取输入流s1,s2来初始化它。

1
public SequenceInputStream(InputStream s1, InputStream s2)

5.StringBufferInputStream

不推荐使用,此类不能将字符正确地转换为字节。从一个串创建一个流的最佳方法是采用StringReader类。

6.FilterInputStream

其子类有LineNumberInputStream(java8中已不建议使用)、DataInputStream、BufferedInputStream、PushbackInputStream。

代码示例:解序列化

读取文件test1.txt内容的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test {
public static void main(String[] args) {
try {
File f = new File("E:/test1.txt");
//如果文件不存在,就会抛出异常
FileInputStream fs = new FileInputStream(f);
byte[] b = new byte[(int) f.length()];
//文件中内容都读到此数组中,数组大小由文件决定
//在不知道输入文件大小时,可用while循环
int temp = 0;
int len = 0;
while ((temp = fs.read()) != -1) {
//-1为文件读完的标志
b[len] = (byte) temp;
len++;
}
//可以直接fs.read(b);读取内容
//也可以一个一个读取:for(int i=0;i<b.length;i++){b[i]=(byte)fs.read()};
fs.close();
//关闭输出流
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

把序列化对象从文件中读取出来,会用到字节输入流(与上文把序列化对象写入文件关联):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 把序列化对象从文件test.ser中读取出来
* 读取的时候,读取顺序一定要与写入顺序相同
*/
public class Test {
public static void main(String[] args) {
try {
FileInputStream fs = new FileInputStream("E:/test.ser");
//如果文件不存在,会抛出异常
ObjectInputStream os = new ObjectInputStream(fs);
//ObjectOutputStream知道如何提取对象,但是要靠链接的stream提供文件存取
User a1 = (User) os.readObject();
//readObject()的返回类型是Object,要转换为原来的User类型
System.out.println("user的内容为");
System.out.println(a1.getId());
System.out.println(a1.getName());
List a2 = (List) os.readObject();
//读取顺序与读入顺序一致
System.out.println("list的内容为");
System.out.println(a2);
os.close();
//关闭ObjectOutputStream,FileInputStream会自动跟着关闭
} catch (IOException ex) {
ex.printStackTrace();
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
}

如下图所示:

image

字符流

字符流处理的基本单元为2个字节的Unicode字符,操作字符、字符数组或字符串,它通常用来处理文本数据。

java提供了Writer、Reader两个专门操作字符流的类。表示以Unicode字符为单位往stream中写入或读取信息。

字符输出流Writer

类与方法定义

抽象类,定义如下:

1
public abstract class Writer implements Appendable, Closeable, Flushable

常用方法有:

1
2
1.写一个字符
public void write(int c) throws IOException
1
2
3
4
5
2.写一个字符数组
public void write(char cbuf[]) throws IOException

abstract public void write(char cbuf[], int off, int len) throws IOException;
//从cbuf[]的off位置开始,写入字符数为len的字符数组
1
2
3
4
5
3.写入一个字符串
public void write(String str) throws IOException

public void write(String str, int off, int len) throws IOException
//从字符串str的off位置开始,写入的字符数为len
1
2
3
4
5
4.将指定的字符序列caq追加到该writer
public Writer append(CharSequence csq) throws IOException

//追加从指定序列的start位置开始,至end位置的字符序列
public Writer append(CharSequence csq, int start, int end) throws IOException
1
2
5.将指定字符追加到该writer
public Writer append(char c) throws IOException
1
2
6.强制性清空缓存
abstract public void flush() throws IOException;
1
2
7.关闭输出流
abstract public void close() throws IOException;

要想使用此类,必须通过子类实例化对象,使用子类。

继承Writer的子类

image

FileWriter

如果是向文件中写入内容,应该使用FileWriter子类。与FileOutputStream对应。

++FileWriter继承自OutputStreamWriter,OutputStreamWriter继承自Writer++

构造方法:

1.构造给定File对象的FileWriter(文件写入器对象)

1
2
3
4
public FileWriter(File file) throws IOException

//如果apped设为true,数据将写到文件的末尾而不是开始
public FileWriter(File file, boolean append) throws IOException

2.构造给定文件名的FileWriter

1
2
3
4
public FileWriter(String fileName) throws IOException

//如果append设为true,数据将写到文件的末尾
public FileWriter(String fileName, boolean append) throws IOException

3.构造与文件描述器相关联的FileWriter

1
public FileWriter(FileDescriptor fd)

Writer的其他子类

1.BufferedWriter

2.CharArrayWriter
与ByteArrayOutputStream对应

3.PipedWriter
与PipedOutputStream对应

4.FilterWriter

5.StringWriter

代码示例:将字符串写入文本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static void main(String[] args) {
try {
FileWriter fw = new FileWriter("E:/test2.txt");
//文件如果不存在就会被创建
//如果是在文件中追加内容,构造方法中append的值设为true
BufferedWriter bw = new BufferedWriter(fw);
bw.write("Hello,World");
//这里以字符作参数
bw.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

字符输入流Reader

类与方法定义

抽象类,定义如下:

1
public abstract class Reader implements Readable, Closeable

常用方法有:

1
2
1.读取单个字符
public int read() throws IOException
1
2
3
4
5
6
2.将内容读入字符数组,返回读入的长度,如果到达输入流末端,返回-1
public int read(char cbuf[]) throws IOException

//len表示要读取的最大字符数
//将输入流中len个字符数据读入一个字符数组中,从数组的off位置开始存放len长度的数据
abstract public int read(char cbuf[], int off, int len) throws IOException;
1
2
3.关闭输入流,并释放与之关联的任何系统资源
abstract public void close() throws IOException;

要想使用此类,必须通过子类实例化对象,使用子类。

继承Reader的子类

image

FileReader

如果要从文件中读取内容,可以直接使用FileReader子类。FileReader与FileInputStream对应。

++FileReader继承自InputStreamReader,InputStreamReader继承自Reader++。

构造方法:

1.创建一个新的FileReader,从指定的File对象读取数据

1
public FileReader(File file) throws FileNotFoundException

2.创建一个新的FileReader,从指定名称的文件中读取数据

1
public FileReader(String fileName) throws FileNotFoundException

3.创建一个新的FileReader,从指定的文件描述器读取数据

1
public FileReader(FileDescriptor fd)

Reader的其他子类

1.BufferedReader

2.CharArrayReader

与ByteArrayInputStream对应。此类实现一个可用作字符输入流的字符缓冲区。子类有LineNumberReader

3.PipedReader
与PipedInputStream对应

4.FilterReader
子类有PushbackReader。

5.StringReader

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public static void main(String[] args) {
try {
FileReader fw = new FileReader("E:/test2.txt");
BufferedReader bw = new BufferedReader(fw);
//它只会在缓冲区读空的时候才会回头去读取
//以字符数组的形式读取数据
char[] c = new char[1024];
//所有内容都读到此数组中
int len = bw.read(c);
//read(char c[])返回读入长度
bw.close();
System.out.println(new String(c, 0, len));
//把字符数组变成字符串输出
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
//还可以以while循环判断是否读到底
int len=0;
int temp=0;
char[] c = new char[1024];
while((temp=bw.read())!=-1){
c[len]=(char)temp;
len++;
}
bw.close();
System.out.println(new String(c, 0, len));
//把字符数组变成字符串输出
1
2
3
4
5
6
 //还可以用readLine()方法
//直接用String变量来承接所读取结果
String line=null;
while((line=bw.readLine())!=null){
System.out.println(line);
}

字节—字符转换

OutputStreamWriter和InputStreamReader是字节流与字符流的转换类。从源码中分析:

++OutputStreamWriter是Writer的子类,将输出的字符流转换成字节流。++
++InputStreamReader是Reader的子类,将输入的字节流转换成字符流。++

OutputStreamWriter

1
public class OutputStreamWriter extends Writer

OutputStreamWriter类是从字符流到字节流的桥梁。在OutputStreamWriter类中需要一个字节流的对象(OutputStream out)。

OutputStreamWriter将多个字符写入到一个输出流,根据指定的字符编码将写入的字符编码成字节。实际负责编码的是StreamEncoder类,过程中必须使用指定的编码集。

构造方法:

1.可以通过名称指定支持的字符编码集:

1
2
public OutputStreamWriter(OutputStream out, String charsetName)
throws UnsupportedEncodingException

2.构造器中指定Charset类型:

1
public OutputStreamWriter(OutputStream out, Charset cs)

3.也可以接受平台的默认编码集:

1
public OutputStreamWriter(OutputStream out)

4.可以使用指定charset encoder:

1
public OutputStreamWriter(OutputStream out, CharsetEncoder enc)

write()方法的每次调用都会在给定字符(或字符串)上调用编码转换器。在写入底层输出流之前,将在缓冲区中积累产生的字节

注意,传递给write()方法的字符没有缓冲。OutputStreamWriter的write()方法写入的是字符(或字符串),而非字节。写入字节的是输出字节流(OutputStream)。

为了提高效率,可以考虑将OutputStreamWriter包装在一个BufferedWriter中,以避免频繁的转换。例如:

1
Writer out  = new BufferedWriter(new OutputStreamWriter(System.out));

InputStreamReader

1
public class InputStreamReader extends Reader

InputStreamReader类是从字节流到字符流的桥梁。在InputStreamReader类中需要一个字节流的对象(InputStream in)。

它使用的字符集可以通过名称指定,也可以明确Charset类型,也可接受平台的默认字符集。与OutputStreamWriter类似。

构造函数:

1
2
3
public InputStreamReader(InputStream in, String charsetName)
public InputStreamReader(InputStream in, Charset cs)
public InputStreamReader(InputStream in)

每次调用InputStreamReader的read()方法,都可能导致从底层字节输入流中读取一个或多个字节。为了能有效地将字节转化为字符,可从底层输入流中读取更多字节,而不只是满足当前读取操作所需的字节。

注意,InputStreamReader是Reader的子类,其read()方法读取的是一个或多个字符。读取字节的是字节输入流(InputStream)。

为了提高效率,可以考虑将InputStreamReader包装在一个BufferedReader中,比如:

1
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

代码示例:用Buffered从Socket上读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
*从Socket上读取数据
*/
public class Test {
public static void main(String[] args) {
try {
Socket chatSocket = new Socket("127.0.0.1", 5000);
//建立对服务器的Socket连接
InputStreamReader stream = new InputStreamReader(chatSocket.getInputStream());
//从Socket取得输入串流
//建立连接到Socket上的输入串流InputStreamReader(转换字节成字符的桥梁)
BufferedReader reader = new BufferedReader(stream);
String message = reader.readLine();
reader.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

如下图所示:

image

字节流与字符流的比较

  1. 字节流操作的基本单元是字节;字符流操作的基本单元为Unicode码元。

  2. 字节流在操作的时候本身不会用到缓冲区的,是与文件本身直接操作的;而字符流在操作的时候使用到缓冲区的。

所有文件的存储都是字节(byte)的存储,在磁盘上保留的是字节。

  1. 在使用字节流操作中,即使没有关闭资源(close方法),也能输出;而字符流不使用close方法的话,不会输出任何内容。

说明字符流用到了缓冲区,如果执行关闭输出流的话会刷新缓冲区,所以可以把内容输出。如果没有关闭,可以调用flush()方法强制刷新缓冲区,这样可以在不使用close()的情况下输出内容。

输入流、输出流相关类的使用判断

根据使用场景决定使用哪个类。参考[字符流和字节流的区别,使用场景,相关类]。如下(不考虑特殊需要):

1.考虑最原始的数据格式是什么:

(1)二进制格式(只要不能确定是纯文本的):InputStream,OutputStream及其子类(字节流)。

(2)纯文本格式(含纯英文与汉字或其他编码方式):Reader,Writer及其子类(字符流)。

2.是输入还是输出:

(1)输入:Reader,InputStream类型的子类。

(2)输出:Writer,OutputStream类型的子类。

3.是否需要转换流:

字节到字符:InputStreamReader

字符到字节:OutputStreamWriter

4.数据来源(去向)是什么:

(1)是文件:FileInputStream,FileOutputStream ; FileReader,FileWriter

(2)是byte[]: ByteArrayInputStream, ByteArrayOutputStream

(3)是Char[]:CharArrayReader,CharArrayWriter

(4)是String:StringBufferInputStream,StringBufferOutputStream;StringReader,StringWriter

(5)是网络数据流:InputStream,OutputStream;Reader,Writer

5.是否要缓冲:(要注意readLine()是否有定义,有什么比read(),writer()更特殊的输入或输出方法)

要缓冲:BufferedInputStream, BufferedOutputStream; BufferedReader, BufferedWriter

6.是否要格式化输出:

要格式化输出:PrintStream, PrintWriter

PrintStream是FilterOutputStream的子类。
PrintWriter是Writer的子类。可用PrintWriter写数据到Socket上。

还有如下特殊需要的情景:

(1)对象输入输出:ObjectInputStream, ObjectOutputStream

(2)进程间通信:PipedInputStream, PipedOutputStream, PipedReader, PipedWriter

(3)合并输入: SequenceInputStream

(4)更特殊的需要:

PushbackInputStream(FilterInputStream的子类),LineNumberInputStream(FilterInputStream的子类,java8中已不建议使用);

LineNumberReader(BufferedReader的子类),PushbackReader(FilterReader的子类)