IO流
引用
-
IO流概念
用于设备间的数据传输,传输操作以“ 流 ” 的形式进行。此处的数据是指永久性存储数据(硬盘中的数据)。
-
IO流的分类
-
基本概念
① 字节流:以字节为单位(8bit)的非文本格式文件进行数据传输,多用于图片、视频
② 字符流:以字符为单位(16bit)的文件格式进行数据传输,多用于文本
③ 输入流:数据从文件(外存)流向程序(内存)
④ 输出流:数据从程序(内存)流向文件(外存)
⑤ 节点流:直接操作文件的流
⑥ 处理流:操作节点流或者其它处理流的流 -
java.io包
提供了各种类的接口,通过标准方法输入输出数据
-
IO流体系表
-
重点掌握
① 抽象基类
② 访问文件中流的操作
其余流的操作与上面类似
一、节点流 - - > 文件中流的操作
-
将文件中的内容读取到程序中
① 实例化File类的对象,指明要操作的文件
File file = new File("Hello1.txt");
② 提供具体的流(会因文件找不到而抛出异常)
FileReader fr = new FileReader(file);
③ 数据的读入(运行时会因阻塞而抛出异常中断程序)
// 读入单个字符 int data ; while ((data = fr.read(cbuf)) != -1){ System.out.print((char)data); }
④ 流的关闭(会因空指针而抛出异常)
stream.close();
完整的读入程序(考虑异常)
/* 多个语句会抛异常的情况,需要用try-catch-finally结构,确保流操作可以关闭 */ @Test public void test1() { File file = new File("Hello.txt"); // 在try-catch-finally结构前,声明流 FileReader fr = null; try { if (!file.exists()) { file.createNewFile(); } FileReader fr = new FileReader("Hello2.txt"); char[] cbuf = new char[5]; int len ; // read(char[] cbuf):返回每次读入cbuf数组中的字符个数。 // 如果剩余字符不足数组长度,返回实际读入个数。直到文件末尾,返回-1 while ((len = fr.read(cbuf)) != -1){ for (int i = 0; i < len; i++) { System.out.print(cbuf[i]); } } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fr != null) { fr.close(); } } catch (IOException e) { e.printStackTrace(); } } }
-
从内存中写出数据的到硬盘的文件中
① 创建文件对象
File e_n = new File("e_nanxu");
② 创建文件写入操作流
FileWreiter fw = new FileWriter(e_n);
③ 数据写入
fw.write("hellojava");
④ 关闭流
fw.close();
完整程序(考虑异常情况)
@Test public void fileWriterTest() { FileWriter fw = null; try { File e_n = new File("e_nanxu"); fw = new FileWriter(e_n); fw.write("hello"); } catch (IOException e) { e.printStackTrace(); } finally { try { if (fw != null) { fw.close(); } } catch (IOException e) { e.printStackTrace(); } } }
注:输出操作,对应的文件可以不存在,不会抛异常
- 文件不存在,在输出操作时会自动创建
- 文件存在
- File.Write( ) / File . Write( file , false )会对原文件覆盖
- File . Write( file , true ) 不会对原文件覆盖,会在原文件末端追加内容
-
图像文件的复制(读取后写入)
① 创建两个File对象
② 创建字节输入流和字节输出流
③ 调用字节数组byte[]
④ 进行数据的读取和写入,实现复制
⑤ 流关闭
@Test public void copyTest() { //第1步:创建两个File对象 File file = new File("C:\\Users\\NLC\\Desktop\\《新程序员》海报.png"); File file1 = new File("C:\\Users\\NLC\\Desktop\\《新程序员》杂志海报.png"); // 第2步:创建字节输入流和字节输出流 FileInputStream fi = null; FileOutputStream fo = null; try { fi = new FileInputStream(file); fo = new FileOutputStream(file1); // 第3步 调用字节数组byte[] byte[] bytes = new byte[5]; int len; // 第4步 进行数据的读取和写入,实现复制 while ((len = fi.read(bytes)) != -1) { fo.write(bytes,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { // 第5步 关闭流 try { fi.close(); } catch (IOException e) { e.printStackTrace(); } try { fo.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
复制视频,测试复制所需时间(随后与缓冲流进行对比)
步骤于复制图片一样,此次使用方法传参的方式
@Test // 测试复制时间的方法 public void timeTest() { String s = "C:\\Users\\NLC\\Download\\《灵笼》.mp4"; String d = "F:\\影视收藏\\热漫\\国漫之光\\《灵笼》.mp4"; long start = System.currentTimeMillis(); videoCopyTest(s,d); long end = System.currentTimeMillis(); System.out.println(end-start); } @Test // 复制非文本文件方法 public void videoCopyTest(String srcPath,String dest) { File file = new File(srcPath); File file1 = new File(dest); FileInputStream fi = null; FileOutputStream fo = null; try { fi = new FileInputStream(file); fo = new FileOutputStream(file1); byte[] bytes = new byte[1024]; int len; while ((len = fi.read(bytes)) != -1) { fo.write(bytes,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { try { fi.close(); } catch (IOException e) { e.printStackTrace(); } try { fo.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
读取写入方法总结
-
read()
第1种:读取单个字符,用于字节流操作
第2种:读取字字节数组 byte[ ] , 用于字节流操作
第3种:读取字符数组 char[ ] ,用于字符流操作
-
write()
第1种:覆盖写入Write( ) / Write( file , false )
第2种:末尾写入Write( file , true )
第3种:将file文件中的len个字符写入新的文件种 Write( file , 0, len )
-
-
两种错误的写法
① 错误写法
// 第一种错误写法 char[] cbuf = new char[5]; int len; while((len = fr.read(cbuf)) != -1) { // i < cbuf.lengtn 应该写为 i < len for(i = 0; i < cbuf.length; i++) { System.out.println(cbuf[i]); } } // 第二种错误写法 char[] cbuf = new char[5]; int len; while((len = fr.read(cbuf)) != -1) { // 字符数组转变为字符串,,同样可能会在最后一次读取中,把上一次读取的内容未完全覆盖引发错误 String str = new String(cbuf); System.out.println(str); }
② 错误原因图示
二、处理流之一 - - > 缓冲流(Buffered)
-
缓冲流的作用:
套接在流的基础上,提升流的读取、写入速度(流:可以是字节流,也可以是处理流)
① 缓冲流内部提供了一个缓冲区,8192是213
private static int defaultCharBufferSize = 8192;
② 提供了一个缓冲区刷新方法flush Buffer(),write( ) 方法内会自动调用
-
使用缓冲流复制文本文件
① 创建文本对象
② 创建节点流
③ 创建缓冲流
④ 读写操作
读操作有一个新的方法readLine(),一行行的读入,返回字符串类型,空值是返回null;
write( )方法不提供换行服务,可手动加上“ \n ”,这样按行读入的数据就可以按行写入,保持原文档格式。⑤ 流关闭
@Test public void bufferedReanerTest() { BufferedReader br= null; BufferedWriter bw = null; try { // 1.创建文件对象、创建节点流合并 FileReader fr = new FileReader(new File("e_nanxu")); FileWriter fw = new FileWriter(new File("e_n.txt")); // 2.创建缓冲流 br = new BufferedReader(fr); bw = new BufferedWriter(fw); // 3.读写操作 String line; while ((line = br.readLine()) != null) { bw.write(line + "\n"); } } catch (IOException e) { e.printStackTrace(); } finally { // 4.流关闭 try { if (br != null) { br.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (bw != null) { bw.close(); } } catch (IOException e) { e.printStackTrace(); } } }
-
文件加密与解密
- 加密:利用异或 ^ 运算,在写入数据的时候,将每个字符进行异或运算,就使原来字符发生变化,再去读取时,看到的就是乱码
- 解密:对加密文件再进行异或运算,就得到了正确的字符
@Test // 加密过程 public void secretTest() { FileReader fr = null; FileWriter fw = null; try { File e_nanxu = new File("e_nanxu"); File secret_e = new File("secret_e"); fr = new FileReader(e_nanxu); fw = new FileWriter(secret_e); char[] cbuf = new char[5]; int len; while ((len = fr.read(cbuf)) != -1) { for (int i = 0; i < len; i++) { fw.write(cbuf[i] ^ 5); } } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fr != null) { fr.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (fw != null) { fw.close(); } } catch (IOException e) { e.printStackTrace(); } } } // 解密:将里面的File对象更改一下即可 File decrypt = new File("secret_e"); File e_n = new File("e_n");
-
获取文件中每个字符出现的次数
@Test /** * 遍历文中的每一个字符:读取 * 字符出现的次数都保存在Map中 * 将Map中的数据写入文件 * */ public void countTest() { FileReader fr = null; FileWriter fw = null; try { // 1.创建文件对象 File min = new File("min.txt"); File count = new File("count.txt"); // 2.创建节点流 fr = new FileReader(min); fw = new FileWriter(count); // 3.创建Map对象,并将文件读入Map集合中 Map<Character, Integer> map = new HashMap<>(); int ch; while ((ch = fr.read()) != -1) { char c = (char)ch; if (!map.containsKey(c)) { map.put(c,1); } if (map.containsKey(c)) { map.put(c,map.get(c)+1); } } // 4.将Map中的键值对存入Set集合中(无序、不可重复) Set<Entry<Character, Integer>> set = map.entrySet(); // 5.遍历Set集合,将其写入文件中,文件中有一些换行符、空格等无法显示,所以分情况标以文本 for (Entry<Character, Integer> e : set) { switch (e.getKey()) { case ' ' : fw.write("空格 = " + e.getValue() + '\n'); break; case '\t': fw.write("tab键=" + e.getValue() + '\n'); break; case '\r': fw.write("回车=" + e.getValue() + '\n'); break; case '\n': fw.write("换行=" + e.getValue() + '\n'); break; default: fw.write(e.getKey() + "=" + e.getValue() + "\n"); } } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fr != null) { fr.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (fw != null) { fw.close(); } } catch (IOException e) { e.printStackTrace(); } } }
三、处理流之二 - - - > 转换流
-
本质:字符流
① InputStreamReader: 解码,将一个字节的输入流转换为字符的输入流
② OutputStreamWriter:编码,将一个字符的输出流转换为字节的输出流
-
转换流参数
① 第1个参数:需要转换的文件
② 第2个参数:指定字符集,如果没有,使用系统默认字符集
@Test public void inputStreamReaderTest() { InputStreamReader reader = null; OutputStreamWriter wr = null; try { // 1.创建文件对象、节点流 File e_nanxu = new File("e_nanxu"); File code_nanxu = new File("code_nanxu.txt"); FileInputStream fi = new FileInputStream(e_nanxu); FileOutputStream fo = new FileOutputStream(code_nanxu); // 2.造转换流(处理流) reader = new InputStreamReader(fi); wr = new OutputStreamWriter(fo,"UTF-8"); // 3.读取和写入 char[] cbuf = new char[10]; int len; while ((len = reader.read(cbuf)) != -1) { for (int i = 0; i < len; i++) { wr.write(cbuf[i]); } } } catch (IOException e) { e.printStackTrace(); } finally { // 4.流关闭 try { if (reader != null) { reader.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (wr != null) { wr.close(); } } catch (IOException e) { e.printStackTrace(); } } } ```
-
UTF-8的二进制表示原理
-
字符集的演变及应用
四、处理流之三 - - - > 标准输入输出流
-
规范
① System.in :标准输入流,默认设备键盘
② System.out :标准输出流,默认从控制台输出(显示器) -
两个属性
① in :
public final static InputStream in = null;
setIn:重新设置输入流的作用对象,InputStream类型
public static void setIn(InputStream in)
② out :
public final static PrintStream out = null;
setOut:重新设置输出流的作用对象,PrintStream类型
public static void setOut(PrintStream out)
-
测试程序
public class IOTest1 { public static void main(String[] args) { IOTest1 iot = new IOTest1(); iot.systemInOutTest1(); } /** * 用System.in从键盘输入字符串, * 将读取到的整行字符串变为大写,在继续输入下一行 * 直到输入'e' 或 'exit' 退出程序 */ public void systemInOutTest1() { InputStreamReader isr = null; BufferedReader br = null; try { // 1.将System.in的InputStream类型转换为Reader类型 isr = new InputStreamReader(System.in); // 2.利用缓冲流处理,可以使用readLine()方法整行读入数据 br = new BufferedReader(isr); String data; String dataUpper; while (true) { data = br.readLine(); if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) { break; } dataUpper = data.toUpperCase(); System.out.println(); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (isr != null) { isr.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (br != null) { br.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
五、处理流之四 - - - > 打印流
-
作用:
① 实现将基本数据类型的数据格式转换为字符串的输出
② System.out.printn() == (new PrintStream).println() -
方法及特性
① 提供了一系列重载的print() 和 println() 方法,用于多种数据类型的输出
② PrintStream和PrintWriter的输出不会抛出IOException异常
③ 自带有自动flush功能
④ PrintStream打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况,应使用PrintWrite类
⑤ System.out返回的是PrintStream实例 -
创建PrintStream对象,改变System.out的默认值,使输出平台改变
@Test public void printStreamTest() { PrintStream ps = null; try { // 1.创建文件对象和节点流 File file = new File("F:\\Java\\规范代码1.0\\IO流\\outt.txt"); FileOutputStream fos = new FileOutputStream(file); // 2.利用打印流和标准输出流处理节点流 ps = new PrintStream(fos); if (ps != null) { System.setOut(ps); } // 下面的System.out. 都可以替换为 ps. for (int i = 0; i < 256; i++) { System.out.print((char)i); if (i % 50 == 0) { System.out.println(); } } } catch (FileNotFoundException e) { e.printStackTrace(); } finally { try { if (ps != null) { ps.close(); } } catch (Exception e) { e.printStackTrace(); } } } ```
六、处理流之五 - - - > 数据流
-
作用:
方便操作基本数据类型和字符串,套接在InputStream和OutputStream子类流上
-
数据流中的方法
boolean readBoolean( ) byte readByte( ) write方法类同, char readChar( ) float readFloat( ) 只要把read double readDouble( ) short readShort( ) 换成write即可 long readLong( ) int readInt( ) String readUTF( ) void readFully(byte[] b) -
DataOutputStream
将基本数据类型和字符串写入到文件中
@Test public void dataOutputStreamTest() { DataOutputStream dos = null; try { File file = new File("wdata.txt"); FileOutputStream fos = new FileOutputStream(file); // 下面写入到的wdata文件不能打开查看,需要用DataInputStream读取到控制台看 dos = new DataOutputStream(fos); dos.writeInt(123); dos.writeBoolean(true); dos.writeUTF("hijava"); } catch (IOException e) { e.printStackTrace(); } finally { try { if (dos != null) { dos.close(); } } catch (IOException e) { e.printStackTrace(); } } }
-
DataInputStream
将用DataOutputStream写入文件中的内容读出到控制台上
@Test public void dataInputStreamTest() { DataInputStream dis = null; try { File file = new File("wdata.txt"); FileInputStream fis = new FileInputStream(file); dis = new DataInputStream(fis); // 读取数据的顺序需要和写入文件中的数据顺序相同 int r = dis.readInt(); boolean b = dis.readBoolean(); String s = dis.readUTF(); System.out.println(r); System.out.println(b); System.out.println(s);; } catch (IOException e) { e.printStackTrace(); } finally { try { if (dis != null) { dis.close(); } } catch (IOException e) { e.printStackTrace(); } } }
七、处理流之六 - - - > 对象流
-
作用
用于存储和读取基本数据类型数据或对象的处理流。
强大之处是可以把Java中的对象写入数据源中,也能把对象从数据源中还原回来
-
对象序列化机制
2.1 机制作用和实现条件
① 可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输后可被还原
② 序列化是RMI(Remote Method Invoke - - - 远程方法调用)过程的参数和返回值都必须实现的机制,RMI是JavaEE的基础。故序列化机制是JavaEE平台的基础
③ 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一:Serializable Externalizable2.2 序列化:ObjectOutputStream
① 允许把内存中的Java对象转换成成与平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络这种二进制流传输到另一个网络节点。
@Test public void objectOutputStreamTest() { ObjectOutputStream dat = null; try { dat = new ObjectOutputStream(new FileOutputStream(new File("oos.dat"))); dat.writeObject(new String("Java全系列学习")); } catch (IOException e) { e.printStackTrace(); } finally { try { if (dat != null) { dat.close(); } } catch (IOException e) { e.printStackTrace(); } } }
2.3 反序列化:ObjectInputStream
当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
@Test public void objectInputStreamTest() { ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream("oos.dat")); Object o = ois.readObject(); String s = (String)o; System.out.println(s); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { try { if (ois != null) { ois.close(); } } catch (IOException e) { e.printStackTrace(); } } }
-
依次实现的各接口
① Serializable 标识接口,什么方法都没有
public interface Serializable {}
② Externalizable
public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; }
③ ObjectInput/ObjectOutput
public interface ObjectInput extends DataInput, AutoCloseable {
④ ObjectInputStream/ObjectOutputStream
public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants {
-
自定义类实现Serializable接口的序列化和反序列化
4.1 凡是实现Serializable接口的类都有一个表示序列化版本表示的静态变量
private static final long serialVersionUID = -754667710L;
① serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化是否兼容
② 如果类没有显示定义这个静态常量,它的值时Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID可能发生变化,故建议显示声明。
③ 简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为一致,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)
4.2 内部属性:
① 必须可序列化:基本数据类型本身时可序列化,引用类型数据需保证可序列化
② 不可以是static、transient类型4.3 程序实例
@Test // 自定义类的序列化 public void customizeOutputTest() { ObjectOutputStream coos = null; try { coos = new ObjectOutputStream(new FileOutputStream("coos.dat")); Customize c1 = new Customize("詹姆斯·高斯林 ", 65); Customize c2 = new Customize("James Gosling", 66); coos.writeObject(c1); coos.writeObject(c2); } catch (IOException e) { e.printStackTrace(); } finally { try { if (coos != null) { coos.close(); } } catch (IOException e) { e.printStackTrace(); } } }
@Test // 自定义类的反序列化 public void customizeInputTest() { ObjectInputStream cois = null; try { cois = new ObjectInputStream(new FileInputStream("coos.dat")); Object o3 = cois.readObject(); Object o4 = cois.readObject(); Customize c3 = (Customize)o3; Customize c4 = (Customize)o4; System.out.println(c3); System.out.println(c4); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { try { if (cois != null) { cois.close(); } } catch (IOException e) { e.printStackTrace(); } } }
// 自定义类 class Customize implements Serializable { private static final long serialVersionUID = -754667710L; private String name; private int age; public Customize(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } ```
八、处理流之七 - - - > 随机存取文件流
-
RandomAccessFile声明在java.io包下,直接继承于java.lang.Object类。并且它实现了DataInput、DataOutput两个接口,也意味着这个类既可以读也可以写(即使输入流,也是输出流)。
public class RandomAccessFile implements DataOutput, DataInput, Closeable {
-
RandomAccessFile类支持“随机访问”的方式,程序可以直接跳到文件的任意位置读、写文件
① 支持只访问文件的部分内容
② 可以向已存在的文件后追加内容 -
RandomAccessFile对象包含一个记录指针,用以标示当前读写处的位置;也可以自由移动记录指针
① long getFilePointer( ) :获取文件记录指针的当前位置
② void seek( long pos): 将文件记录指针定位到 pos 位置 -
构造器
public RandomAccessFile(String name, String mode) throws FileNotFoundException { this(name != null ? new File(name) : null, mode); } public RandomAccessFile(File file, String mode) throws FileNotFoundException {
① 第一各参数,可以是文件名,也可以是文件对象,但最终执行的构造器是参数为文件对象
② 第二个参数:mode参数
r: 以只读方式打开
rw: 打开以便读取和写入
rwd:打开以便读取和写入;同步文件内容的更新
rws:打开以便读取和写入;同步文件内容和元数据的更新。③ 如果模式为只读 r ,则不会创建文件,而是会去读取一个已经存在的文件,读取的文件不存在则会异常。
④ 如果模式为读写 rw ,文件不存在则回去创建文件;存在则不会创建,并对原文件进行覆盖,覆盖长度取决于写入文件的多少
-
可以通过相关操作,实现数据的插入
① 将文件中插入位置后的字符全部保存到一个StringBuffer中
② 插入相关数据后,再将StringBuffer中的内容写到文件中
@Test public void randomInsertText() { RandomAccessFile insert = null; try { File file = new File("hi.txt"); insert = new RandomAccessFile(file, "rw"); // 1.调后指针,将文件中该指针后的余部存入StringBuffer insert.seek(3); // File类中的length()返回long类型整数 StringBuffer sb = new StringBuffer((int) file.length()); byte[] cbuf = new byte[20]; int len; while ((len = insert.read(cbuf)) != -1) { sb.append(new String(cbuf,0,len)); } // 2.插入后,继续将复制到StringBuffer中的数据写入文件 insert.seek(3); // write()内的参数只有byte[],需要用getBytes()将String转变为byte[] // 写入后指针指向了插入数据后的位置 insert.write("xyz".getBytes(StandardCharsets.UTF_8)); insert.write(sb.toString().getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { e.printStackTrace(); } finally { try { if (insert != null) { insert.close(); } } catch (IOException e) { e.printStackTrace(); } } } ```
-
特殊之处
- 提供了指针偏移功能 - - - > seek( int pos );
- 可以用RandomAccessFile类,实现多线程断点下载。下载中有两个临时文件,一个临时文件是与被下载文件大小相同的空文件,一个用来记录文件指针的位置文件,每次暂停,都会保存上一次的指针。