大家好,我是程序员Forlan,本篇内容主要分享IO流方面的知识,属于基础内容,一方面是新人必备知识,另一方面也方便自己和大家后续查找相关资料
首先来介绍两个概念
对于面向对象编程,我们操作的是对象,盘符上的文件/文件夹,需要封装为对象,我们才能进行操作,这个对象就是File,通过File类,我们可以获取文件/文件夹的各种信息,操作文件/文件夹
File f1 = new File("D:\\text.txt");
File f2 = new File("D:/text.txt");
/**
* File.separator,本地文件系统的名称分隔符:在UNIX系统上,这个字段的值是'/';在Microsoft Windows系统上,它是'\\'
* 建议使用这种
*/
File f3 = new File("d:" + File.separator + "text.txt");
if (f1.exists()) {
// 如果文件存在,将文件删除操作
f1.delete();
} else {
// 如果文件不存在,就创建这个文件
f1.createNewFile();
}
System.out.println("文件是否可读:" + f1.canRead());
System.out.println("文件是否可写:" + f1.canWrite());
System.out.println("文件的名字:" + f1.getName());
System.out.println("上级目录:" + f1.getParent());
System.out.println("是否是一个目录:" + f1.isDirectory());
System.out.println("是否是一个文件:" + f1.isFile());
System.out.println("是否隐藏:" + f1.isHidden());
System.out.println("文件的大小:" + f1.length());
System.out.println("是否存在:" + f1.exists());
System.out.println("绝对路径:" + f1.getAbsolutePath()); // 完整路径
System.out.println("相对路径:" + f1.getPath()); // 相对某个参照物的路径,在junit的测试方法中,相对路径指的就是相对模块,就是模块名之后的路径
System.out.println("toString:" + f1.toString()); // 相对路径
System.out.println(f1 == f2);//比较两个对象的地址
System.out.println(f1.equals(f2));//比较两个对象对应的文件的路径
基本文件的命令,它也可以用,这里就不演示重复的了
File f = new File("D:\\forlan");
// 创建单层目录,前提:之前的目录已经存在,不然创建失败
f.mkdir();
// 创建多层目录
f.mkdirs();
// 删除:只会删除一层,前提:这层目录是空的,里面没有内容,如果有内容就不会被删除
f.delete();
// 获取当前文件夹下的文件或文件夹,不包括下一级
// File[] files = f.listFiles();
String[] list = f.list();
for (String s : list) {
System.out.println(s);
}
前面提到的FIle类,可以获取以及操作文件/文件夹表层信息,但拿不到里面的内容,这时候就出现了I/O流,用于处理设备之间的数据的传输,可以简单理解为一根“管”,连接不同东西进行传输
分类
注:字节流可以理解细管,字符流可以理解为粗管,传输大小不一样
所以按照不同的方向组合,就出现了4个基类:字节输入流、字节输出流、字符输入流、字节输出流
针对这4个抽象基类,又可以延伸出来不同类型的实现,具体如下图:
下面我们就针对不同的实现,来看看具体代码如何写
Windows的记事本默认保存的ANSI,也就是GBK,我们要转为UTF-8,它不支持,但我们可以使用转换流,这个后面会讲
// 读文件,先得到文件对象
File f = new File("D:\\text.txt");
// Windows的记事本默认保存的ANSI,也就是GBK,我们要转出UTF-8
try (FileReader fr = new FileReader(f);) {
int n;
// 如果到了文件的结尾处,那么读取的内容为-1
while ((n = fr.read()) != -1) {
System.out.print((char) n);
}
} catch (IOException e) {
e.printStackTrace();
}
// 读文件,先得到文件对象
File f = new File("D:\\text.txt");
try (FileReader fr = new FileReader(f);) {
// 使用数组,一次性读10个
char[] ch = new char[10];
int len = fr.read(ch);
// 如果到了文件的结尾处,那么读取的内容为-1
while (len != -1) {
System.out.print(new String(ch, 0, len));
len = fr.read(ch);
}
} catch (IOException e) {
e.printStackTrace();
}
代码中的new String(ch, 0, len)可以写成new String(ch, 0, ch.length)?
答案肯定是不行的,会重复读取内容,比如最后len=2,实际上只需要展示2个字符,但还是展示了10个字符,8个是之前的字符
FileWriter说明:
// 写的文件对象
File f = new File("D:\\text.txt");
try (FileWriter fw = new FileWriter(f, true);) {
String content = "你好,程序员Forlan";
for (int i = 0; i < content.length(); i++) {
fw.write(content.charAt(i));
}
} catch (IOException e) {
e.printStackTrace();
}
// 写的文件对象
File f = new File("D:\\text.txt");
try (FileWriter fw = new FileWriter(f, true);) {
String content = "你好,程序员Forlan";
// 转为字符数据,一次性写完
char[] chars = content.toCharArray();
fw.write(chars);
} catch (IOException e) {
e.printStackTrace();
}
// 源文件
File f1 = new File("D:\\text.txt");
// 目标文件
File f2 = new File("D:\\textCopy.txt");
try (FileReader fr = new FileReader(f1); FileWriter fw = new FileWriter(f2);) {
char[] ch = new char[10];
int len = fr.read(ch);
while (len != -1) {
fw.write(ch, 0, len);
len = fr.read(ch);
}
} catch (IOException e) {
e.printStackTrace();
}
缓冲流:BufferedReader、BufferedWriter ,这个后面会讲到
// 源文件
File f1 = new File("D:\\text.txt");
// 目标文件
File f2 = new File("D:\\textCopy.txt");
try (FileReader fr = new FileReader(f1); FileWriter fw = new FileWriter(f2);
BufferedReader br = new BufferedReader(fr); BufferedWriter bw = new BufferedWriter(fw);) {
char[] ch = new char[10];
int len = br.read(ch);
while (len != -1) {
bw.write(ch, 0, len);
len = br.read(ch);
}
} catch (IOException e) {
e.printStackTrace();
}
字节流,一般用来操作非文本文件,因为中文字符,不同编码,所占用的字节不同,文本文件,建议使用字符流操作
文本文件:txt、java、c…
非文本文件:jpg、mp3 、mp4…
File f = new File("D:\\forlan.png");
try (FileInputStream fis = new FileInputStream(f);) {
int len = fis.read();
// 如果到了文件的结尾处,那么读取的内容为-1
while (len != -1) {
System.out.print(len);
len = fis.read();
}
} catch (IOException e) {
e.printStackTrace();
}
File f = new File("D:\\forlan.png");
try (FileInputStream fis = new FileInputStream(f);) {
byte[] bytes = new byte[1024];
int len = fis.read(bytes);
while (len != -1) {
System.out.print(new String(bytes, 0, len));
len = fis.read(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
非文本很难写完整,下面我们以复制的例子来演示写操作
File f1 = new File("D:\\forlan.png");
File f2 = new File("D:\\forlanCopy.png");
try (FileInputStream fis = new FileInputStream(f1); FileOutputStream fos = new FileOutputStream(f2);) {
int len = fis.read();
while (len != -1) {
fos.write(len);
len = fis.read();
}
} catch (IOException e) {
e.printStackTrace();
}
File f1 = new File("D:\\forlan.png");
File f2 = new File("D:\\forlanCopy.png");
try (FileInputStream fis = new FileInputStream(f1); FileOutputStream fos = new FileOutputStream(f2);) {
byte[] bytes = new byte[1024];
int len = fis.read(bytes);
while (len != -1) {
fos.write(bytes, 0, len);
len = fis.read(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
File f1 = new File("D:\\forlan.png");
File f2 = new File("D:\\forlanCopy.png");
try (FileInputStream fis = new FileInputStream(f1); FileOutputStream fos = new FileOutputStream(f2);
BufferedInputStream bis = new BufferedInputStream(fis); BufferedOutputStream bos = new BufferedOutputStream(fos);) {
byte[] bytes = new byte[1024];
int len = bis.read(bytes);
while (len != -1) {
bos.write(bytes, 0, len);
len = bis.read(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
无论是字符流,还是字节流,为什么要用缓冲区?
假设有500b的文件,我们来看不同写法,具体是怎么操作的
一个个读写,要操作500次
利用byte[10],要操作50次
利用缓冲区,直接一次性全部读到缓冲区,底层会帮我们刷新缓冲区到磁盘
所以,很简单,缓冲区效率高
作用:将字节流和字符流进行转换,属于一个处理流
InputStreamReader :字节输入流 ——》字符的输入流
OutputStreamWriter :字符输出流 ——》字节的输出流
前面我们提到,我们读取的文件内容为乱码,使用转换流可以指定编码,正确展示
File f = new File("D:\\text.txt");
try (FileInputStream fis = new FileInputStream(f);
InputStreamReader isr = new InputStreamReader(fis);) {
// 可以指定一个编码,不知道的话,默认是程序本身的编码utf-8
// InputStreamReader isr = new InputStreamReader(fis,"utf-8");
char[] ch = new char[10];
int len = isr.read(ch);
while (len != -1) {
System.out.print(new String(ch, 0, len));
len = isr.read(ch);
}
} catch (IOException e) {
e.printStackTrace();
}
// 源文件
File f1 = new File("D:\\text.txt");
// 目标文件
File f2 = new File("D:\\textCopy.txt");
try (FileInputStream fis = new FileInputStream(f1); InputStreamReader isr = new InputStreamReader(fis, "utf-8");
FileOutputStream fos = new FileOutputStream(f2); OutputStreamWriter osw = new OutputStreamWriter(fos);) {
char[] ch = new char[10];
int len = isr.read(ch);
while (len != -1) {
osw.write(ch, 0, len);
len = isr.read(ch);
}
} catch (IOException e) {
e.printStackTrace();
}
System.in:标准输入流——》默认从键盘录入
System.out:标准输出流
读到的东西,会被转为ascall码
InputStream in = System.in;
int n = in.read();
扫描键盘
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
扫描文件
Scanner sc = new Scanner(new FileInputStream(new File("D:\\text.txt")));
while (sc.hasNext()) {
System.out.print(sc.next());
}
// 写法1
PrintStream out = System.out;
out.print("程序员forlan");
// 写法2
System.out.println("程序员forlan");
// 目标文件
File f = new File("D:\\text.txt");
// 属于字节流
InputStream in = System.in;
try (InputStreamReader isr = new InputStreamReader(in); BufferedReader br = new BufferedReader(isr);
FileWriter fw = new FileWriter(f); BufferedWriter bw = new BufferedWriter(fw);) {
String s = br.readLine();
while (!s.equals("quit")) {
bw.write(s);
bw.newLine();
s = br.readLine();
}
} catch (Exception e) {
e.printStackTrace();
}
对象流:ObjectInputStream,ObjectInputStream
作用:用于存储和读取基本数据类型数据或对象的处理流
亮点:可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来
File f = new File("D:\\text.txt");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));) {
oos.writeUTF("程序员forlan");
oos.writeInt(100);
} catch (IOException e) {
e.printStackTrace();
}
1)自定义对象
public class Forlan implements Serializable {
private String name;
private int level;
public Forlan(String name, int level) {
this.name = name;
this.level = level;
}
}
2)写入文件
File f = new File("D:\\text.txt");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));) {
Forlan forlan = new Forlan("程序员Forlan", 100);
oos.writeObject(forlan);
} catch (IOException e) {
e.printStackTrace();
}
我们注意到一个细节,自定义对象实现了Serializable接口 ,不实现就会报错,如下:
java.io.NotSerializableException: cn.lw.io.Forlan
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at cn.lw.io.ObjectStream.writeObject(ObjectStream.java:31)
at cn.lw.io.ObjectStream.main(ObjectStream.java:13)
File f = new File("D:\\text.txt");
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));) {
System.out.println(ois.readUTF());
} catch (Exception e) {
e.printStackTrace();
}
前面我们提到,自定义对象必须实现Serializable接口,否则,去写文件就会报错
这个接口里是空的,是一个标识接口,实现这个接口的类对象才能序列化
public interface Serializable {
}
像ObjectOutputStream类,是把内存中的Java对象转换成二进制数据,然后就可以把数据保存在磁盘上,相反,ObjectInputStream类,是把磁盘上的二进制数据转换成Java对象进行操作
序列化:数据被转换成二进制数据的过程
反序列化:数据从二进制数据恢复成原样的过程
注:写文件的对象需要序列化,网络传输的对象也需要序列化(比如rpc请求)
1)被序列化的类,内部的所有属性,必须是可序列化的
基本数据类型、String都是可序列化的,但自定义对象需要实现Serializable
2)static,transient修饰的属性不可以被序列化
不可以被序列化,指的是属性内容不被保存或传输,反序列化出来的内容为空
凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态常量,serialVersionUID,用来表明类的不同版本间的兼容性
1)作用
对序列化对象进行版本控制,有关反序加化时会检测是否兼容
2)生成时机
没有显示声明的话,它的值是Java运行时环境根据类的内部细节自动生成的,若类的实例变量做了修改,serialVersionUID 可能发生变化,所以建议还是显式声明
简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)
举个栗子:
原来Forlan类不加toString方法,进行序列化,也就是写文件
public class Forlan implements Serializable {
private String name;
private int level;
public Forlan(String name, int level) {
this.name = name;
this.level = level;
}
// @Override
// public String toString() {
// return "Forlan{" +
// "name='" + name + '\'' +
// ", level=" + level +
// '}';
// }
}
打开注释,再加上toString方法,进行反序列化会报错
java.io.InvalidClassException: cn.lw.io.Forlan; local class incompatible: stream classdesc serialVersionUID = 113793900596523614, local class serialVersionUID = -7076236456819894490
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1880)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1746)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2037)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1568)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:428)
at cn.lw.io.ObjectStream.readObject(ObjectStream.java:20)
at cn.lw.io.ObjectStream.main(ObjectStream.java:14)
解决,加serialVersionUID,因为值固定后,无论我们怎么改变类内容,都不会变,也就不会报错了,如果不加,serialVersionUID会隐式加,跟随类内部细节自动生成
数据流:用来操作基本数据类型和字符串的
DataInputStream:将文件中存储的基本数据类型和字符串 写入 内存的变量中
DataOutputStream: 将内存中的基本数据类型和字符串的变量 写出 文件中
写文件
File f = new File("D:\\text.txt");
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(f));) {
dos.writeUTF("程序员Forlan");
dos.writeBoolean(false);
dos.writeInt(100);
} catch (Exception e) {
e.printStackTrace();
}
读文件
File f = new File("D:\\text.txt");
try (DataInputStream dis = new DataInputStream(new FileInputStream(f));) {
System.out.println(dis.readUTF());
System.out.println(dis.readBoolean());
System.out.println(dis.readInt());
} catch (Exception e) {
e.printStackTrace();
}