字节流、字符流及其转换、比较与使用场景
本文包含以下知识点:
- [x] 字节流OutputStream和InputStream类及其子类
- [x] 字符流Writer和Reader类及其子类
- [x] 转换流OutputStreamWriter和InputStreamReader
- [x] 代码示例
- [x] 字节流、字符流的比较
- [x] 各类的使用场景
流
在程序中所有的数据都是以流的方式进行传输和保存的。程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。
在java.io包中操作文件内容的主要有两大类:字节流、字符流。两类都分为输入和输出操作。输入流基本方法是读,输出流基本方法是写。
大致操作流程:(以File为例)
- 使用File类打开一个文件;
- 通过字节流或字符流的子类,指定输出的位置;
- 进行读/写操作;
- 关闭输入/输出。
java中的流是对字节序列的抽象,流中保存的实际上全都是字节文件。
字节流
字节流处理的基本单元为1个字节(byte),操作字节和字节数组,通常用来处理二进制数据。
面向字节流的OutputStream和InputStream是一切的基础。均为抽象类,实际使用的是它们的一系列子类。
字节输出流OutputStream
类与方法定义
抽象类,定义如下:
public abstract class OutputStream
implements Closeable, Flushable {}
//Closeable表示可关闭,Flushable表示刷新,清空内存中的数据
方法有:
1.将一个字节数据写入数据流
public abstract void write(int b) throws IOException;
2.将一个byte数组写入数据流
public void write(byte b[]) throws IOException
3.将指定byte数组中从偏移量off开始的len个字节写入数据流
public void write(byte b[], int off, int len) throws IOException
4.刷新缓冲区
public void flush() throws IOException {}
5.关闭输出流
public void close() throws IOException {}
要想使用此类及以上方法,必须通过子类实例化对象。
继承OutputStream的子类
imagesFileOutputStream
文件输出流,是向File或FileDescriptor输出数据的一个输出流。
FileOutputStream类的构造方法有:
1.创建一个文件输出流,向指定的File对象输出数据(写入)
public FileOutputStream(File file) throws FileNotFoundException
//字节写到文件的开始,输出会进行覆盖
public FileOutputStream(File file, boolean append) throws FileNotFoundException
//如果append设为true,字节将写到文件末尾
2.创建一个文件输出流,向指定名称的文件输出数据(写入到指定名称的文件中)
public FileOutputStream(String name) throws FileNotFoundException
//输出会进行覆盖
public FileOutputStream(String name, boolean append)
throws FileNotFoundException
//如果append设为true,字节将写到文件末尾
3.创建一个文件输出流,向指定的文件描述器输出数据,该文件描述符表示文件系统中一个实际文件的现有连接。
public FileOutputStream(FileDescriptor fdObj)
OutputStream的其他子类
1.ObjectOutputStream(对象输出流)
对象输出流将Java对象的原始数据类型和对象图写入输出流。只有支持可序列化接口的对象才可以写入到输出流。
2.ByteArrayOutputStream(字节数组输出流)
该类实现了一个以字节数组形式写入数据的输出流,缓冲区会随着数据的写入而自动扩大。可以用toByteArray()和toString()检索数据。
构造方法有:
(1)创建一个新的字节数组输出流
public ByteArrayOutputStream() {
this(32);}
//缓冲区容量最初是32字节,如果需要,大小会增加
(2)创建一个新的字节数组输出流,并带有制定大小字节的缓冲区容量
public ByteArrayOutputStream(int size)
3.PipedOutputStream(管道输出流)
管道输出流可以连接到管道输入流来创建通信管道,它是这个管道的发送端。
一个线程通过管道输出流写入(发送)数据,另一个线程通过管道输入流读取数据,这样可实现两个线程之间的通讯。
不建议尝试从单个线程使用这两个管道流对象,因为它可能会造成线程死锁。
构造方法有:
(1)创建连接到指定管道输入流的管道输出流
public PipedOutputStream(PipedInputStream snk) throws IOException
(2)创建一个管道输出流,该输出流尚未连接到管道输入流
public PipedOutputStream()
4.FilterOutputStream
其子类有DataOutputStream、BufferedOutputStream、PrintStream。
代码示例:将序列化对象写入文件
创建文件,输出字节的代码示例:
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();
}
}
}
将序列化对象写入文件,会用到字节输出流:
/**
* 将序列化对象存储到文件
*/
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
类与方法定义
抽象类,定义如下:
public abstract class InputStream implements Closeable
方法有:
1.读取一个字节无符号填充到int的低8位,高8位补零,读取到-1结束
public abstract int read() throws IOException;
//read()返回读取的字节或-1(-1表示读取结束)
2.将当前输入流中b.length个字节读入到byte数组中,实际读取的字节数作为整数返回
public int read(byte b[]) throws IOException
3.将输入流中len个字节数据读入一个字节数组中,从数组的off位置开始存放len长度的数据
public int read(byte b[], int off, int len) throws IOException
//带有参数的read方法返回的是读取字节数或-1,内部也在调用read(),读取字节
4.从该输入流中跳过或忽略n字节
public long skip(long n) throws IOException
5.返回这个输入流中可读(或跳过)的字节数估计值,即可取得输入文件的大小
public int available() throws IOException
6.关闭输入流
public void close() throws IOException
要想使用此类及以上方法,必须通过子类实例化对象。
继承InputStream的子类
imageFileInputStream
文件输入流,从文件系统中的文件读取输入字节。
FileInputStream的构造方法有:
1.创建一个输入文件流,从指定的File对象读取数据
public FileInputStream(File file) throws FileNotFoundException
2.创建一个输入文件流,从指定名称的文件读取数据
public FileInputStream(String name) throws FileNotFoundException
3.创建一个输入文件流,从指定的文件描述器读取数据
public FileInputStream(FileDescriptor fdObj)
InputStream的其他子类
1.ObjectInputStream(对象输入流)
2.ByteArrayInputStream(字节数组输入流)
把内存的一个缓冲区作为InputStream使用。
构造方法有:
(1)创建一个字节数组输入流,使用buf作为它的缓冲区数组,从缓冲区数组中读取数据
public ByteArrayInputStream(byte buf[])
(2)创建一个字节数组输入流,从指定字节数组中读取数据
public ByteArrayInputStream(byte buf[], int offset, int length)
//offset表示读取缓冲区中第一个字节的偏移
//length表示从缓冲区读取的最大字符数
3.PipedInputStream(管道输入流)
管道输入流是一个通讯管道的接收端。
构造方法有:
(1)创建连接到指定管道输出流的管道输入流
public PipedInputStream(PipedOutputStream src) throws IOException
public PipedInputStream(PipedOutputStream src, int pipeSize)
throws IOException
//使用指定的管道大小作为管道的缓冲
(2)创建一个管道输入流,该输出流尚未连接到管道输出流
public PipedInputStream()
public PipedInputStream(int pipeSize)
//使用指定的管道大小
4.SequenceInputStream(序列输入流)
此类允许应用程序把几个输入连续地合并起来,它从一个有序的输入流集合开始,每个输入流依次被读取到文件的末尾,直到到达最后一个输入流的文件末尾。
构造方法有:
(1)参数中枚举生成运行时类型为输入流的对象。新创建一个序列输入流,通过读取由枚举产生的输入流来初始化它。
public SequenceInputStream(Enumeration<? extends InputStream> e)
(2)新创建一个序列输入流,按参数顺序读取输入流s1,s2来初始化它。
public SequenceInputStream(InputStream s1, InputStream s2)
5.StringBufferInputStream
不推荐使用,此类不能将字符正确地转换为字节。从一个串创建一个流的最佳方法是采用StringReader类。
6.FilterInputStream
其子类有LineNumberInputStream(java8中已不建议使用)、DataInputStream、BufferedInputStream、PushbackInputStream。
代码示例:解序列化
读取文件test1.txt内容的代码示例:
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();
}
}
}
把序列化对象从文件中读取出来,会用到字节输入流(与上文把序列化对象写入文件关联):
/**
* 把序列化对象从文件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
类与方法定义
抽象类,定义如下:
public abstract class Writer implements Appendable, Closeable, Flushable
常用方法有:
1.写一个字符
public void write(int c) throws IOException
2.写一个字符数组
public void write(char cbuf[]) throws IOException
abstract public void write(char cbuf[], int off, int len) throws IOException;
//从cbuf[]的off位置开始,写入字符数为len的字符数组
3.写入一个字符串
public void write(String str) throws IOException
public void write(String str, int off, int len) throws IOException
//从字符串str的off位置开始,写入的字符数为len
4.将指定的字符序列caq追加到该writer
public Writer append(CharSequence csq) throws IOException
//追加从指定序列的start位置开始,至end位置的字符序列
public Writer append(CharSequence csq, int start, int end) throws IOException
5.将指定字符追加到该writer
public Writer append(char c) throws IOException
6.强制性清空缓存
abstract public void flush() throws IOException;
7.关闭输出流
abstract public void close() throws IOException;
要想使用此类,必须通过子类实例化对象,使用子类。
继承Writer的子类
imageFileWriter
如果是向文件中写入内容,应该使用FileWriter子类。与FileOutputStream对应。
++FileWriter继承自OutputStreamWriter,OutputStreamWriter继承自Writer++
构造方法:
1.构造给定File对象的FileWriter(文件写入器对象)
public FileWriter(File file) throws IOException
//如果apped设为true,数据将写到文件的末尾而不是开始
public FileWriter(File file, boolean append) throws IOException
2.构造给定文件名的FileWriter
public FileWriter(String fileName) throws IOException
//如果append设为true,数据将写到文件的末尾
public FileWriter(String fileName, boolean append) throws IOException
3.构造与文件描述器相关联的FileWriter
public FileWriter(FileDescriptor fd)
Writer的其他子类
1.BufferedWriter
2.CharArrayWriter
与ByteArrayOutputStream对应
3.PipedWriter
与PipedOutputStream对应
4.FilterWriter
5.StringWriter
代码示例:将字符串写入文本文件
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
类与方法定义
抽象类,定义如下:
public abstract class Reader implements Readable, Closeable
常用方法有:
1.读取单个字符
public int read() throws IOException
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;
3.关闭输入流,并释放与之关联的任何系统资源
abstract public void close() throws IOException;
要想使用此类,必须通过子类实例化对象,使用子类。
继承Reader的子类
imageFileReader
如果要从文件中读取内容,可以直接使用FileReader子类。FileReader与FileInputStream对应。
++FileReader继承自InputStreamReader,InputStreamReader继承自Reader++。
构造方法:
1.创建一个新的FileReader,从指定的File对象读取数据
public FileReader(File file) throws FileNotFoundException
2.创建一个新的FileReader,从指定名称的文件中读取数据
public FileReader(String fileName) throws FileNotFoundException
3.创建一个新的FileReader,从指定的文件描述器读取数据
public FileReader(FileDescriptor fd)
Reader的其他子类
1.BufferedReader
2.CharArrayReader
与ByteArrayInputStream对应。此类实现一个可用作字符输入流的字符缓冲区。子类有LineNumberReader
3.PipedReader
与PipedInputStream对应
4.FilterReader
子类有PushbackReader。
5.StringReader
代码示例:
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();
}
}
}
//还可以以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));
//把字符数组变成字符串输出
//还可以用readLine()方法
//直接用String变量来承接所读取结果
String line=null;
while((line=bw.readLine())!=null){
System.out.println(line);
}
字节—字符转换
OutputStreamWriter和InputStreamReader是字节流与字符流的转换类。从源码中分析:
++OutputStreamWriter是Writer的子类,将输出的字符流转换成字节流。++
++InputStreamReader是Reader的子类,将输入的字节流转换成字符流。++
OutputStreamWriter
public class OutputStreamWriter extends Writer
OutputStreamWriter类是从字符流到字节流的桥梁。在OutputStreamWriter类中需要一个字节流的对象(OutputStream out)。
OutputStreamWriter将多个字符写入到一个输出流,根据指定的字符编码将写入的字符编码成字节。实际负责编码的是StreamEncoder类,过程中必须使用指定的编码集。
构造方法:
1.可以通过名称指定支持的字符编码集:
public OutputStreamWriter(OutputStream out, String charsetName)
throws UnsupportedEncodingException
2.构造器中指定Charset类型:
public OutputStreamWriter(OutputStream out, Charset cs)
3.也可以接受平台的默认编码集:
public OutputStreamWriter(OutputStream out)
4.可以使用指定charset encoder:
public OutputStreamWriter(OutputStream out, CharsetEncoder enc)
write()方法的每次调用都会在给定字符(或字符串)上调用编码转换器。在写入底层输出流之前,将在缓冲区中积累产生的字节。
注意,传递给write()方法的字符没有缓冲。OutputStreamWriter的write()方法写入的是字符(或字符串),而非字节。写入字节的是输出字节流(OutputStream)。
为了提高效率,可以考虑将OutputStreamWriter包装在一个BufferedWriter中,以避免频繁的转换。例如:
Writer out = new BufferedWriter(new OutputStreamWriter(System.out));
InputStreamReader
public class InputStreamReader extends Reader
InputStreamReader类是从字节流到字符流的桥梁。在InputStreamReader类中需要一个字节流的对象(InputStream in)。
它使用的字符集可以通过名称指定,也可以明确Charset类型,也可接受平台的默认字符集。与OutputStreamWriter类似。
构造函数:
public InputStreamReader(InputStream in, String charsetName)
public InputStreamReader(InputStream in, Charset cs)
public InputStreamReader(InputStream in)
每次调用InputStreamReader的read()方法,都可能导致从底层字节输入流中读取一个或多个字节。为了能有效地将字节转化为字符,可从底层输入流中读取更多字节,而不只是满足当前读取操作所需的字节。
注意,InputStreamReader是Reader的子类,其read()方法读取的是一个或多个字符。读取字节的是字节输入流(InputStream)。
为了提高效率,可以考虑将InputStreamReader包装在一个BufferedReader中,比如:
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
代码示例:用Buffered从Socket上读取数据
/**
*从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字节流与字符流的比较
-
字节流操作的基本单元是字节;字符流操作的基本单元为Unicode码元。
-
字节流在操作的时候本身不会用到缓冲区的,是与文件本身直接操作的;而字符流在操作的时候使用到缓冲区的。
所有文件的存储都是字节(byte)的存储,在磁盘上保留的是字节。
- 在使用字节流操作中,即使没有关闭资源(close方法),也能输出;而字符流不使用clode方法的话,不会输出任何内容。
说明字符流用到了缓冲区,如果执行关闭输出流的话会刷新缓冲区,所以可以把内容输出。如果没有关闭,可以调用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的子类)