第六章:IO流
一:流的划分与简介
1. 什么是流??
I(Input,输入) O(Output,输出)流,是java中用来传输数据的方式。
2. 流的划分
-
按流向划分:
输入流:读数据(是将文件或其它输入设备的数据加载到内存的过程)
输出流:写数据(是将内存中的数据保存到文件或其他输出设备)
-
按操作分:
字节流:文件通常是由一连串的字节或字符构成,组成文件的字节序列称为字节流(以Stream结尾)。
字符流:组成文件的字符序列称为字符流(以Reader或者Writer结尾)。
3. 四种流的继承结构
- 字节输入流(InputStream)
InputStream 是字节输入流,InputStream 是一个抽象类,所有继承了 InputStream 的类都是字节输入流,主要了解以下子类即可:
- 字节输出流(OutputStream)
所有继承了 OutputStream 都是字节输出流
- 字符输入流(Reader)
所有继承了 Reader 都是字符输入流
- 字符输出流(Writer)
所有继承了 Writer 都是字符输出流
4. java.io下必须掌握的16种流
- 文件专属流
java.io.FileInputStream
java.io.FileOutputStream
java.io.FileReader
java.io.FileWriter - 转换流
java.io.InputStreamReader
java.io.OutputStreamWriter - 缓冲流
java.io.BufferedInputStream
java.io.BufferedOutputStream
java.io.BufferedReader
java.io.BufferedWriter - 数据流专属
java.io.DataInputStream
java.io.DataOutputStream - 标准输出流
java.io.PrintWriter
java.io.PrintStream - 对象专属流
java.io.ObjectInputStream
java.io.ObjectOutputStream
二:16种流的学习
1. 流中必须注意的两个方法
- close() 方法: 所有的流(输入和输出)都实现了:java.io.Closeable接口,都是可关闭的。
- flush() 方法: 所有的输出流都实现了: java.io.Flushable接口,都是可刷新的。
2.文件专属流
-
java.io.FileInputStream
/* java.io.FileInputStream 1.文件字节输入流,万能的,任何类型的文件都可以采用这个流来读 2.字节的方式,完成输入操作,完成读的操作(硬盘----》内存) */ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; public class FileInputStreamTest01 { public static void main(String[] args) { //创建文件字节输入流对象 FileInputStream fis = null; try { fis = new FileInputStream("E:\\Workspace\\Study\\javase\\src\\进阶\\IO流06\\temp"); //开始读 int read() 从此输入流中读取一个数据字节。 int readData = fis.read();//这个方法的返回值是:读取到的“字节”的本身 System.out.println(readData);//97 readData = fis.read(); System.out.println(readData);//98 readData = fis.read(); System.out.println(readData);//99 readData = fis.read(); System.out.println(readData);//100 readData = fis.read(); System.out.println(readData);//101 readData = fis.read(); System.out.println(readData);//102 //读到文件的末尾,再也读不到数据,返回-1 readData = fis.read(); System.out.println(readData);//-1 readData = fis.read(); System.out.println(readData);//-1 readData = fis.read(); System.out.println(readData);//-1 } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { //关闭流的前提是流不是空,流是null没必要关闭 if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
/* 采用循环方式读取 分析这个程序的缺点: 一次读取一个字节byte,这样内存和硬盘交互太频繁,基本上时间/资源都耗费在交互上面了 能不能一次读取多个字符串呢??可以 */ public class FileInputStreamTest02 { public static void main(String[] args) { FileInputStream fis = null; try { fis = new FileInputStream("E:\\Workspace\\Study\\javase\\src\\进阶\\IO流06\\temp"); // while (true){ // int readData = fis.read(); // if (readData == -1){ // break; // } // System.out.println(readData); // } //改造while循环 int readData = 0; while ((readData = fis.read()) != -1){ System.out.println(readData); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
/* int read(byte[] b) 一次读取b.length个字节 减少硬盘和内存的交互,提高程序的执行效率 */ public class FileInputStreamTest03 { public static void main(String[] args) { FileInputStream fis = null; try { //相对路径的话呢?相对路径一定是从当前所在的位置作为起点开始找!! //idea默认的当前路径在哪??工程Project的跟就是IDEA的当前的路径 fis = new FileInputStream("E:\\Workspace\\Study\\javase\\src\\进阶\\IO流06\\temp"); //开始读,采用byte数组,一次读取多个字节。最多读取"数组.length"个字节 byte[] bytes = new byte[4]; //这个方法返回值是读取到的字节的数量,不是字节本身 int readCount = fis.read(bytes); System.out.println(readCount);//第一次读到4个字节 //将字节数组全部转换成字符串 //System.out.println(new String(bytes));//abcd //不应该全部都转换,应该是读取了多少个字节,转换成多少个 System.out.println(new String(bytes,0,readCount)); readCount = fis.read(bytes); System.out.println(readCount);//第二次读到2个字节 //System.out.println(new String(bytes));//efcd readCount = fis.read(bytes);//一个字节都没有了,返回-1 System.out.println(readCount); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
public class FileInputStreamTest04 { public static void main(String[] args) { FileInputStream fis = null; try { fis = new FileInputStream("E:\\Workspace\\Study\\javase\\src\\进阶\\IO流06\\HelloWorldTest"); //准备一个byte数组 byte [] bytes = new byte[4]; int readCount = 0; while ((readCount = fis.read(bytes)) != -1){ System.out.print(new String(bytes,0,readCount)); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
-
java.io.OutputInputStream
/* FileOutputStream(String name) 这种方式谨慎使用,当没有文件时会新建,每次运行会先将源文件清空然后再写入即 (无法对源文件进行追加) FileOutputStream(String name, boolean append) //以追加的方式写入 */ public class FileOutputStreamTest01 { public static void main(String[] args) { FileOutputStream fos = null; try { //当myfile文件不存在的时候,会自动创建 //这种方式谨慎使用,当没有文件时会新建,每次运行会先将源文件清空然后再写入即(无法对源文件进行追加) // fos = new FileOutputStream("myfile"); //以追加的方式写入 fos = new FileOutputStream("myfile",true); //开始写 byte[] bytes = { 97,98,99,100}; //将byte数组全部写入 fos.write(bytes); //将byte数组一部分写入 fos.write(bytes,0,2); //字符串 String s = "My name is hanjian"; byte [] bytes1 = s.getBytes(); fos.write(bytes1); //写完之后一定要刷新 fos.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
-
java.io. FileReader
/* FileReader: 文本字符输入流,只能读取普通文本 读取文本内容时,比较方便,快捷 */ public class FileReaderTest01 { public static void main(String[] args) { FileReader reader = null; try { reader = new FileReader("myfile"); //开始读 char [] chars = new char[2]; int readerCount = 0; while ((readerCount = reader.read(chars)) != -1){ System.out.print(new String(chars,0,readerCount)); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
-
FileWriter
/* FileWriter: 文件字符输出流,写 只能输出普通文本。 */ public class FileWriterTest01 { public static void main(String[] args) { FileWriter out = null; try { out = new FileWriter("myfile",true); //开始写 char[] chars = { '我','是','中','国','人'}; out.write(chars); out.write("hello world"); out.write("\n"); out.write("hello world"); //刷新 out.flush(); } catch (IOException e) { e.printStackTrace(); }finally { if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
-
如何使用FileInputStream + FileOutputStream完成文件的拷贝
/* 使用FileInputStream + FileOutputStream完成文件的拷贝。 拷贝的过程应该是一边读,一边写。 使用以上的字节流拷贝文件的时候,文件的类型随意 */ public class Copy01 { public static void main(String[] args) { FileInputStream fis = null; FileOutputStream fos = null; try { //创建一个输入流对象 fis = new FileInputStream("001.qlv"); //创建一个输出流对象 fos = new FileOutputStream("002.qlv"); //核心:一边读,一边写 byte[] bytes = new byte[1024 * 1024];//1MB int readCount = 0; while ((readCount = fis.read(bytes)) != -1){ fos.write(bytes,0,readCount); } fos.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { //分开try //一起try的时候可能会导致另一个流的关闭异常 if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
-
如何使用FileReader FileWriter完成普通文件的拷贝
/* 使用FileReader FileWriter进行拷贝的话,只能拷贝"普通文本"文件 */ public class Copy02 { public static void main(String[] args) { FileReader in = null; FileWriter out = null; try { //读 in = new FileReader("javase\\src\\进阶\\IO流06\\文件专属\\Copy02.java"); //写 out = new FileWriter("Copy02.java"); //一边读一边写 //先准备个char数组 char[] chars = new char[1024 * 512];//1MB int readCount = 0; while ((readCount = in.read(chars)) != -1){ out.write(chars,0,readCount); } out.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
3. 缓冲流和转换流
/*
BufferedReader:
带有缓冲区的字符输入流。
使用这些流的时候不需要自定义char数组,或自定义byte数组。自带缓冲
*/
public class BufferedReaderTest01 {
public static void main(String[] args){
FileReader reader = null;
BufferedReader br = null;
try {
reader = new FileReader("Copy02.java");
//当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做:节点流
//外部负责包装的这个流叫做:包装流(处理流)
//向当前的程序来说:FileReader叫做节点流,BufferedReader叫做包装流(处理流)
br = new BufferedReader(reader);
// //读一行
// String firstLine = br.readLine();
// System.out.println(firstLine);
//
// //读第二行
// String secondLine = br.readLine();
// System.out.println(secondLine);
//
// //读第三行
// String thirdLine = br.readLine();
// System.out.println(thirdLine);
// br.readLine()放法读取一个文本行,但是它不会将文本的换行符读取进来
String s = null;
while ((s = br.readLine()) != null){
System.out.println(s);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (br != null) {
try {
//关闭流
br.close();//对于包装流来水,只需要关闭最外层流就行,里面的节点流会自动关闭
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/*
转换流InputStreamReader
*/
public class BufferedReaderTest02 {
public static void main(String[] args) throws Exception {
/*
//节点流
FileInputStream in = new FileInputStream("Copy02.java");
//通过转换流转换
//in是节点流,reader是包装流
InputStreamReader reader = new InputStreamReader(in);
//reader是节点流,br是包装流
BufferedReader br = new BufferedReader(reader);
*/
//合并
BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream("Copy02.java")));
String line = null;
while ((line = br.readLine())!=null){
System.out.println(line);
}
br.close();
}
}
/*
BufferedWriter:带有缓冲的字符输出流
*/
public class BufferedWriterTest01 {
public static void main(String[] args) throws IOException {
//BufferedWriter out = new BufferedWriter(new FileWriter("copy"));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream("copy",true)));
//开始写
out.write("hello world");
out.write("\n");
out.write("hello world");
//刷新
out.flush();
//关闭最外层
out.close();
}
}
4. 标准输出流
/*
java.io.PrintStream:标准的字节输出流。默认输出到控制台上
*/
public class printStream {
public static void main(String[] args)throws Exception {
//联合起来写
System.out.println("hello world");
//分开写
PrintStream ps = System.out;
ps.println("hello 111");
ps.println("hello 222");
//标准输出流不需要手动close()关闭
//可以改变标准输出流的输出方向吗??可以
//标准输出流不在指向控制台,指向"log"文件
PrintStream printStream = new PrintStream(new FileOutputStream("log"));
//修改输出方向,将输出方向改到“log”文件
System.setOut(printStream);
//再输出
System.out.println("hello world");
System.out.println("hello 111");
System.out.println("hello 222");
}
}
-
如何利用标准输出流做一个简单的日志文件
/* 日志工具 */ public class Logger { public static void log(String msg){ /* 记录日志的方法 */ //指向一个日志文件 PrintStream out = null; try { out = new PrintStream(new FileOutputStream("log.txt",true)); //改变输出方向 System.setOut(out); //日期当前时间 Date nowTime = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss:SSSS"); String strTime = sdf.format(nowTime); System.out.println(strTime+":" + msg); } catch (FileNotFoundException e) { e.printStackTrace(); } } }
public class LogTest { public static void main(String[] args) { Logger.log("调用了System类的gc方法,建议启动垃圾回收期回收"); Logger.log("hello"); Logger.log("world"); } }
5. 数据专属流
-
java.io. DataOutputStream
/* java.io.DataOutputStream:数据专属的流 这个流可以将数据连同数据的类型一并写入文件 注意:这个文件不是普通文本文档。 */ public class DataOutputStreamTest01 { public static void main(String[] args) throws Exception { DataOutputStream dos = new DataOutputStream(new FileOutputStream("data")); //写数据 byte b = 100; short s = 200; int i = 300; long l = 400L; float f = 3.0F; double d = 3.14; boolean sex = false; char c = 'a'; //写 dos.writeByte(b); dos.writeShort(s); dos.writeInt(i); dos.writeLong(l); dos.writeFloat(f); dos.writeDouble(d); dos.writeBoolean(sex); dos.writeChar(c); //刷新 dos.flush(); //关闭 dos.close(); } }
-
java.io. DataInputStream
/* DataInputStream:数据字节输入流 DataOutputStream写的文件,只能使用DataInputStream去读。并且读的时候你需要提前知道写入的顺序。 读的顺序需要和写的顺序一致。才可以正常取出数据。 */ public class DataInputStreamTest01 { public static void main(String[] args) throws Exception { DataInputStream dis = new DataInputStream(new FileInputStream("data")); //开始读 byte b = dis.readByte(); short s = dis.readShort(); int i = dis.readInt(); long l = dis.readLong(); float f = dis.readFloat(); double d = dis.readDouble(); boolean sex = dis.readBoolean(); char c = dis.readChar(); System.out.println(b); System.out.println(s); System.out.println(i); System.out.println(l); System.out.println(f); System.out.println(d); System.out.println(sex); System.out.println(c); dis.close(); } }
6.对象专属流又称与序列化(ObjectOutputStream)与反序列化(ObjectInputStream)
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。
对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。
注意事项:
-
参与序列化和反序列化的对象,必须实现 Serializable接口
-
Serializable接口是什么??有什么作用??
注意:通过源代码发现, Serializable接口只是一个标志接口 public interface Serializable { } 这个接口中什么代码都没有。 那么它起到什么作用呢?? 起到标识作用,标志的作用,java虚拟机看到这个类实现了这个接口,可能会对这个类 进行特殊待遇。 Serializable这个接口是给java虚拟机参考的,java虚拟机看到这个接口后,会为该类自 动生成一个序列化版本号
-
序列化版本号有什么用??
java语言是采用什么机制来区分类的?? 1. 类名,如果类名不一样肯定不是同一个类 2. 序列化版本号:如果类名一样,靠的是序列化版本号来区分类 序列化版本号是用来区分类的。
-
自动生成的版本号有什么缺点??
这种自动生成的序列化版本的缺点是:一旦代码确定之后,不能进行后续的修改, 因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java会 认为这是一个全新的类。 如对于student对象可能会发生如下错误: java.io.InvalidClassException: 进阶.IO流06.对象流.Student; local class incompatible: stream classdesc serialVersionUID = -4362722773073344459,(之后) local class serialVersionUID = 9047878168635619625(之前)
-
最终结论:
凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号。 这样,以后这个类即使代码修改了,但是版本号不变,java虚拟号会认为是同一个类。
序列化与反序列化例子:
-
序列化(ObjectOutputStream )
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。public class Student implements Serializable { //java虚拟机看到Serializable接口后,会自动生成一个序列化版本号。 //这里没有手动写出来,java虚拟机默认提供这个序列化版本号 //建议序列化版本号手动写出来,不建议自动生成 private static final long serialVersionUID = 1L;//java虚拟机识别一个类的时候先通过类名,如果类名一样,则通过序列化版本号 private int no; private String name; /* 过了很久,Student这个类源代码改动了。 源代码改动之后,需要重新编译,编译之后生成了权限的字节码文件。 并且class文件再次运行的时候,java虚拟机生成的序列化版本号也会发生相应的改遍。 */ private int age; private String email; public Student() { } public Student(int no, String name) { this.no = no; this.name = name; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student{" + "no=" + no + ", name='" + name + '\'' + '}'; } }
public class ObjectOutputStreamTest01 { public static void main(String[] args) throws Exception { //创建java对象 Student s = new Student(111,"zhangsan");//Student对象没改变之前只有no和name //序列化 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("students")); //序列化对象 oos.writeObject(s); //刷新 oos.flush(); //关闭 oos.close(); } }
注意:此时会生成一个student文件,这个文件是不可打开的,不是普通文件。
-
反序列化(ObjectInputStream)
对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。public class ObjectInputStreamTest01 { public static void main(String[] args) throws Exception{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("students")); //开始反序列化,读 Object obj = ois.readObject(); //反序列化回来是一个学生对象,所以会调用学生对象的toString方法 System.out.println(obj); ois.close(); } }
上面的反序列化无问题:
注意以下事项:如果我们在序列化对象之前没有提供序列化版本号,序列化后的Student文件,也可以参与反序列化,但当我们序列化后,在反序列化之前更改了Student对象的属性内容,则反序列化将失败,因此参与序列化和反序列化的对象必须实现Serializable接口。
如何序列化多个多个对象,transient关键字是什么:
-
transient是让参与序列化的对象中的某个属性不参与序列化。(默认的情况下对象的所有属性都是参与序列化的)
-
如何序列化多个对象??
将多个对象先存放在集合中,然后序列化集合,但是注意需要的是,一个文件不可以序列化多个集合,只可以序列化一个。 -
参与序列化的集合和对象中都必须实现了 Serializable接口。
public class User implements Serializable { private int no; //transient关键字表示游离的不参与序列化 //private transient String name; private String name; public User() { } public User(int no, String name) { this.no = no; this.name = name; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "User{" + "no=" + no + ", name='" + name + '\'' + '}'; } }
public class ObjectOutputStreamTest02 { public static void main(String[] args) throws Exception { List<User> userList = new ArrayList<>(); userList.add(new User(1,"zhangsan")); userList.add(new User(2,"lisi")); userList.add(new User(3,"wangwu")); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users")); //序列化一个集合 oos.writeObject(userList); oos.flush(); oos.close(); } }
public class ObjectInputStreamTest02 { public static void main(String[] args) throws Exception { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users")); Object obj = ois.readObject(); System.out.println(obj instanceof List); List<User> userList = (List<User>)obj; for (User user:userList){ System.out.println(user); } ois.close(); } }