I/O流的概念
I/O流 即输入Input流/ 输出Output流的缩写,常见的是电脑屏幕输出设备和键盘鼠标输入设备,其广义上的定义就是:数据在内部存储器和外部存储器或其他周边设备之间的输入和输出;即:数据/ 输入/ 输出。
流是一个抽象但形象的概念,Java中把不同的输入/输出源(键盘,文件,网络连接等)抽象的表述为 “流”(Stream)。可以简单理解成一个数据的序列,输入流表示从一个源读取数据,输出流则表示向一个目标写数据,在Java程序中,对于数据的输入和输出都是采用 “流” 这样的方式进行的。
I/O流的分类
按流向分:输入流,输出流
输入流:只能从中读取数据,不能写入数据
输出流,只能向其写入数据,不能读取数据
这里的输入输出是相对而言的,在本地用程序读写文件时,始终是以程序为中心,即程序从文件中读取时就是硬盘向内存中输入,程序向文件中写入就是内存向硬盘输出。
但对于服务器和客户端来说,服务器将数据输出到网络中,这是Sever端程序的输出流。客户端从网络中读取数据,这是Client端的输入流。
按操作单元分:字节流和字符流
字节流:图片、视频文件中存储的都是二进制的字节(byte)。直观的想法,以一个字节单位来运输的,比如一杯一杯的取水。
以InputStream和OutputStream作为基类
字符流:一般文本文件中存放都是有明确含义的,可供人阅读理解的字符(char)。直观的想法,以多个字节来运输的,比如一桶一桶的取水,一桶水又可以分为几杯水。
以Reader和Writer作为基类
不管是文本、还是图书、视频最终在磁盘上的时候都是按照byte存储的。因此,Java要提供基于字符流的机制,就要处理字节和字符的相互转化,这里就涉及字符集合字符编码的问题。
字节流和字符流的区别:
字节流读取单个字节,字符流读取单个字符(一个字符根据编码的不同,对应的字节也不同,如 UTF-8 编码是 3 个字节,中文编码是 2 个字节。)字节流用来处理二进制文件(图片、MP3、视频文件)。
字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)。
简而言之,字节是个计算机看的,字符才是给人看的。
其实现子类:FileInputStream和FileOutputStream可以对任意类型的文件进行读写操作,但 FileReader和FileWriter只能对纯文本文件进行操作。
从网上找来一张图,方便对Java I/O有个总统的认识。从这张图可以很清楚的看清Java I/O的字节流和字符流的整体情况。
IO体系的基类
InputStream/Reader,OutputStream/Writer
InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例来执行输入,但它们将成为所有输入流的模板,所以他们的方法是所有输入流都可使用的方法。
- InputStream 读文件方法
int read():从输入流中读取单个字节,返回所读取的字节数据(字节数据可直接转为int型)
int read(byte []):从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数。
int read(byte[], int off, int len): 从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入b数组时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数
- Reader读文件方法
int read():从输入流中读取单个字符,返回所读取的字符数据(可直接转为int类型)
int read(char[]):从输入流中最多读取b.length个字符的数据,并将其存储在数组b中,返回实际读取的字符数。
int read(char[] b, int off, int len):从输入流中最多读取len个字符的数据,并将其存储在数组b中,放入数组b时,并不是从数组七点开始,而是从off位置开始,返回实际读取的字符数。
- OutputStream写文件方法
void write(int c):将指定的字节/字符输出到输出流中,c可以代表字节,也可以代表字符
void write(byte[]/byte[] buf):将字节数组,字符数组的数据输出到指定输出流中
void write(byte[]/byte[] buf, int off, int len):将字节数组/字符数组从off位置开始,长度为len的字节/字符数组输出到输出流中
- Writer写文件方法
因为字符流直接以字符为操作单位,所以Writer可以用字符串代替字符数组,即以String对象为参数。Writer中还包括这两个方法:
void write(String str):将str字符串里包含的字符输出到指定输出流中
void write(String str, int off, int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中
IO常用类
- 文件流:FileInputStream/FileOutputStream, FileReader/FileWriter
FileInputStream/FileOutputStream, FileReader/FileWriter是专门操作文件流的,用法高度相似,区别在于前面两个是操作字节流,后面两个是操作字符流。它们都会直接操作文件流,直接与OS底层交互。因此他们也被称为节点流。
使用这几个流的对象之后,需要关闭流对象,因为java垃圾回收器不会主动回收。
不过在Java7之后,可以在 try() 括号中打开流,最后程序会自动关闭流对象,不再需要显示的close。
下面演示这四个流对象的基本用法
//FileInputStream一个一个字节读取
public class FileInputStreamDemo1 {
public static void main(String[] args) throws IOException{
FileInputStream fis = new FileInputStream("c:\\a.txt");
//读取一个字节,调用方法read 返回int
//使用循环方式,读取文件, 循环结束的条件 read()方法返回-1
int len = 0;//接受read方法的返回值
while( (len = fis.read()) != -1){
System.out.print((char)len);
}
//关闭资源
fis.close();
}
}
//FileInputStream读取字节数组
public class FileInputStreamDemo2 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("c:\\a.txt");
//创建字节数组
byte[] b = new byte[1024];
int len = 0 ;
while( (len = fis.read(b)) !=-1){
System.out.print(new String(b,0,len));
}
fis.close();
}
}
//FileOutputStream写单个字节
public class FileOutputStreamDemo {
public static void main(String[] args)throws IOException {
FileOutputStream fos = new FileOutputStream("c:\\a.txt");
//流对象的方法write写数据
//写1个字节
fos.write(97);
//关闭资源
fos.close();
}
}
//FileOutputStream写字节数组
public class FileOutputStreamDemo {
public static void main(String[] args)throws IOException {
FileOutputStream fos = new FileOutputStream("c:\\a.txt");
//流对象的方法write写数据
//写字节数组
byte[] bytes = {65,66,67,68};
fos.write(bytes);
//写字节数组的一部分,开始索引,写几个
fos.write(bytes, 1, 2);
//写入字节数组的简便方式
//写字符串
fos.write("hello".getBytes());
//关闭资源
fos.close();
}
}
//字符流复制文本
//FileReader读取数据源,FileWriter写入到数据目的
public class Copy {
public static void main(String[] args) {
FileReader fr = null;
FileWriter fw = null;
try{
fr = new FileReader("c:\\1.txt");
fw = new FileWriter("d:\\1.txt");
char[] cbuf = new char[1024];
int len = 0 ;
while(( len = fr.read(cbuf))!=-1){
fw.write(cbuf, 0, len);
fw.flush();
}
}catch(IOException ex){
System.out.println(ex);
throw new RuntimeException("复制失败");
}finally{
try{
if(fw!=null)
fw.close();
}catch(IOException ex){
throw new RuntimeException("释放资源失败");
}finally{
try{
if(fr!=null)
fr.close();
}catch(IOException ex){
throw new RuntimeException("释放资源失败");
}
}
}
}
}
- 转换流:InputStreamReader/OutputStreamWriter
InputStreamReader/OutputStreamWriter可以将字节流转换成字符流,被称为字节流与字符流之间的桥梁。经常在读取键盘输入(System.in)或网络通信的时候,需要使用这两个类。
转换流作用:
1 . 是字符流和字节流之间的桥梁
2 . 可对读取到的字节数据经过指定编码换成字符
3 . 可对读取到的字符数据经过指定编码转成字节
/*
* 转换流对象OutputStreamWriter写文本
* 采用UTF-8编码表写入
*/
public static void writeUTF()throws IOException{
//创建字节输出流,绑定文件
FileOutputStream fos = new FileOutputStream("c:\\utf.txt");
//创建转换流对象,构造方法保证字节输出流,并指定编码表是UTF-8
OutputStreamWriter osw = new OutputStreamWriter(fos,"UTF-8");
osw.write("你好");
osw.close();
}
/*
* 转换流对象 OutputStreamWriter写文本
* 文本采用GBK的形式写入
*/
public static void writeGBK()throws IOException{
//创建字节输出流,绑定数据文件
FileOutputStream fos = new FileOutputStream("c:\\gbk.txt");
//创建转换流对象,构造方法,绑定字节输出流,使用GBK编码表
OutputStreamWriter osw = new OutputStreamWriter(fos);
//转换流写数据
osw.write("你好");
osw.close();
}
/*
* 转换流,InputSteamReader读取文本
* 采用UTF-8编码表,读取文件utf
*/
public static void readUTF()throws IOException{
//创建自己输入流,传递文本文件
FileInputStream fis = new FileInputStream("c:\\utf.txt");
//创建转换流对象,构造方法中,包装字节输入流,同时写编码表名
InputStreamReader isr = new InputStreamReader(fis,"UTF-8");
char[] ch = new char[1024];
int len = isr.read(ch);
System.out.println(new String(ch,0,len));
isr.close();
}
/*
* 转换流,InputSteamReader读取文本
* 采用系统默认编码表,读取GBK文件
*/
public static void readGBK()throws IOException{
//创建自己输入流,传递文本文件
FileInputStream fis = new FileInputStream("c:\\gbk.txt");
//创建转换流对象,构造方法,包装字节输入流
InputStreamReader isr = new InputStreamReader(fis);
char[] ch = new char[1024];
int len = isr.read(ch);
System.out.println(new String(ch,0,len));
isr.close();
}
- 缓冲流:BufferedReader/BufferedWriter , BufferedInputStream/BufferedOutputStream
没有经过Buffered处理的IO, 意味着每一次读和写的请求都会由OS底层直接处理,这会导致非常低效的问题。
经过Buffered处理过的输入流将会从一个buffer内存区域读取数据,本地API只会在buffer空了之后才会被调用(可能一次调用会填充很多数据进buffer)。
经过Buffered处理过的输出流将会把数据写入到buffer中,本地API只会在buffer满了之后才会被调用。
BufferedReader/BufferedWriter可以将字符流(Reader)包装成缓冲流,这是最常见用的做法。
另外,BufferedReader提供一个readLine()可以方便地读取一行,因此BufferedReader也被称为行读取器。而FileInputStream和FileReader只能读取一个字节或者一个字符。
在原有的节点流对象外部包装缓冲流,为IO流增加了内存缓冲区,增加缓冲区的两个目的:
1 .允许IO一次不止操作一个字符,这样提高整个系统的性能
2 .由于有缓冲区,使得在流上执行skip,mark和reset方法都成为可能
缓冲流要套接在节点流之上,对读写的数据提供了缓冲功能,增加了读写的效率,同时增加了一些新的方法。例如:BufferedReader中的readLine方法,BufferedWriter中的newLine方法。
//字符输入流
BufferedReader(Reader in)//创建一个32字节的缓冲区
BufferedReader(Reader in, int size)//size为自定义缓存区的大小
//字符输出流
BufferedWriter(Writer out)
BufferedWriter(Writer out, int size)
//字节输入流
BufferedInputStream(InputStream in)
BufferedInputStream(InputStream in, int size)
//字节输出流
BufferedOutputStream(OutputStream in)
BufferedOutputStream(OutputStream in, int size)
对于输出的缓冲流,BufferedWriter和BufferedOutputStream会先在内存中缓存,使用flush方法会使内存中的数据立刻写出。
//字节输出流缓冲流BufferedOutputStream
public class BufferedOutputStreamDemo {
public static void main(String[] args)throws IOException {
//创建字节输出流,绑定文件
//FileOutputStream fos = new FileOutputStream("c:\\buffer.txt");
//创建字节输出流缓冲流的对象,构造方法中,传递字节输出流
BufferedOutputStream bos = new
BufferedOutputStream(new FileOutputStream("c:\\buffer.txt"));
bos.write(55);
byte[] bytes = "HelloWorld".getBytes();
bos.write(bytes);
bos.write(bytes, 3, 2);
bos.close();
}
}
//字节输入流缓冲流BufferedInputStream
public class BufferedInputStreamDemo {
public static void main(String[] args) throws IOException{
//创建字节输入流的缓冲流对象,构造方法中包装字节输入流,包装文件
BufferedInputStream bis = new
BufferedInputStream(new FileInputStream("c:\\buffer.txt"));
byte[] bytes = new byte[10];
int len = 0 ;
while((len = bis.read(bytes))!=-1){
System.out.print(new String(bytes,0,len));
}
bis.close();
}
}
//字符流缓冲区流复制文本文件
*
* 使用缓冲区流对象,复制文本文件
* 数据源 BufferedReader+FileReader 读取
* 数据目的 BufferedWriter+FileWriter 写入
* 读取文本行, 读一行,写一行,写换行
*/
public class Copy_1 {
public static void main(String[] args) throws IOException{
BufferedReader bfr = new BufferedReader(new FileReader("c:\\w.log"));
BufferedWriter bfw = new BufferedWriter(new FileWriter("d:\\w.log"));
//读取文本行, 读一行,写一行,写换行
String line = null;
while((line = bfr.readLine())!=null){
bfw.write(line);
bfw.newLine();
bfw.flush();
}
bfw.close();
bfr.close();
}
}
- 对象流(ObjectInputStream/ObjectOutputStream)
如果我们要写入文件内的数据不是基本数据类型(使用DataInputStream),也不是字符型或字节型数据,而是一个对象,应该怎么写?这个时候就用到了处理流中的对象流。
对象的序列化
对象中的数据,以流的形式,写入到文件中保存过程称为写出对象,对象的序列化
ObjectOutputStream将对象写道文件中,实现序列化
对象的反序列化
在文件中,以流的形式,将对象读出来,读取对象,对象的反序列化
ObjectInputStream 将文件对象读取出来
注意:
先序列化后反序列化; 反序列化顺序必须与序列化一致
不是所有对象都可以序列化 必须实现 java.io.Serializable
-
不是所有属性都需要序列化 不需要序列化的属性 要加 transient
public class Person implements Serializable { private String name; private int age; private transient String intro; //不需要序列化 public Person(String name, int age, String intro) { this.name = name; this.age = age; this.intro = intro; } public String getName() { return name; } public int getAge() { return age; } public String getIntro() { return intro; } public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public void setIntro(String intro) { this.intro = intro; } public String toString(){ return name + " " + age + " " + intro; } }
进行序列化和反序列化后:
public class WriteObject { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream( new File("d:\\test.txt")))); oos.writeObject(new Person("shi",13,"A")); oos.writeObject(new Person("zhang",15,"B")); oos.writeObject(new Person("hao",18,"C")); oos.close(); ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream( new FileInputStream(new File("d:\\test.txt")))); Person p1 = (Person) ois.readObject(); Person p2 = (Person) ois.readObject(); Person p3 = (Person) ois.readObject(); ois.close(); System.out.println(p1); System.out.println(p2); System.out.println(p3); } }
可以发现,自定义类中被transient 标记的数据将不被序列化和反序列化,而且自定义类也必须要实现Serializable接口。
注意:
ObjectOutputStream 对JAVA对象进行序列化处理,处理后的对象不是文本数据。所以数据保存到文件中后,用记事本、写字板、Word等文本编辑器打开,是无法识别的,一定会显示乱码。只有使用相同版本的Java的ObjectInputStream进行读取操作,方可获取文件中的对象内容。
序列化时的参数类型和反序列化时的返回类型都是Object类型,所以在反序列化接收类对象数据时要用强制类型转换。
总结上面几种流的应用场景:
- FileInputStream/FileOutputStream 需要逐个字节处理原始二进制流的时候使用,效率低下
- FileReader/FileWriter 需要组个字符处理的时候使用
- StringReader/StringWriter 需要处理字符串的时候,可以将字符串保存为字符数组
- PrintStream/PrintWriter 用来包装FileOutputStream 对象,方便直接将String字符串写入文件
- Scanner 用来包装System.in流,很方便地将输入的String字符串转换成需要的数据类型
- InputStreamReader/OutputStreamReader , 字节和字符的转换桥梁,在网络通信或者处理键盘输入的时候用
- BufferedReader/BufferedWriter , BufferedInputStream/BufferedOutputStream , 缓冲流用来包装字节流后者字符流,提升IO性能,BufferedReader还可以方便地读取一行,简化编程。
参考文章:
https://blog.csdn.net/wintershii/article/details/81281710
https://blog.csdn.net/sj13051180/article/details/47710099