本文结构
- 字节流
- 字符流
- 异常的处理
- File类
- Properties集合
- 缓冲流
- 转换流
- 对象序列化
- 一点小问题
IO
- 在Java中,I/O流分为字节流和字符流,分类如下:
- 根据操作对象主要分为:
1. 字节流
字节流可以传输任意文件数据,在操作流的时候,无论使用什么样的流对象,底层传输的始终都是二进制数据
1.1 字节输出流抽象类 OutputStream
OutputStream
是Object
的直接子类,此抽象类是表示输出字节流的所有类的父类,输出流接受输出字节并将这些字节发送到某个接收器。
- 成员方法 :
void close()
: 关闭此输出流,释放相关的所有系统资源void flush()
: 刷新此输出流,并强制任何缓冲的输出字节被写出void write(byte[] b)
: 将 b.length 字节从指定的字节数组写入此输出流void write(byte[] b, int off, int len)
: 从偏移量 off 开始写入 len 字节到输出流abstract void write(int b)
: 将指定的字节输出流
1.2 字节输入流抽象类 InputStream
- 成员方法 :
int available ()
: 返回可读字节数量abstract int read()
:读取数据int read(byte[] b, int off, int len)
: 读取指定长度到byte数组long skip(long n)
: 跳过指定个数字节boolean markSupported()
: 是否支持 mark()/reset()synchronized void mark(int readlimit)
: mark一个位置synchronized void reset()
: 重置位置为上次mark的位置void close()
: 关闭流,释放资源
1.3 文件字节流
1.3.1 文件字节输出流类 FileOutputStream
- 构造方法:
FileOutputStream(String name)
: 创建一个向指定名称的文件中写入数据的文件输出流FileOutputStream(File file)
: 创建一个向指定 File 对象表示的文件中写入数据的文件输出流FileOutputStream(String name, boolean append)
: 可以进行追加写FileOutputStream(File file, boolean append)
- 构造方法的作用:
创建FileOutputStream
对象,根据构造方法中的文件路径/文件创建一个空的文件,把FileOutputStream
对象指向创建好的文件 - 写入数据的原理:
java程序 --> JVM ---> OS ---> OS调用写数据的方法 ---> 将数据写入到文件中 - 一次写入单个字符
//创建FileOutputStream对象,传入文件
FileOutputStream fos = new FileOutputStream("D:a.txt");
//写入数据
fos.write('b');
//释放资源
fos.close();
- 一次写入多个字符
如果第一个字节是正数(0-127),直接查询 ASCII 表,如果第一个字节是负数,那么前两个字节组合,查询 GBK 表
//使用字符串转字节数组写入
byte[] in1 = "大家好才是真的好".getBytes();
fos.write(in1,0,21); //大家好才是真的,UTF-8编码
fos.close();
- 换行符号
Windows: rn
Linux: /n
MacOS: /r
1.3.2 文件字节输入流 FileInputStream
- 构造方法:
FileInputStream(String name)
FileInputStream(File file)
- 按单个字节读入:
int read()
返回读取到的字节的int
值
FileInputStream fis = new FileInputStream("D:a.txt");
int readByte = 0;
while((readByte = fis.read())!=-1){ //java中赋值语句会返回该值,读取到结束标记时read()会返回-1
System.out.println(readByte);
}
fis.close();
- 一次读取多个字节:
此处的 bytes 起到缓冲作用,存储每次读取到的多个字节,数组的长度定义为 1024 即可一次读取 1KBint read(byte[])
返回的是每次读取到的有效字节个数
FileInputStream fis = new FileInputStream("D:a.txt"); //a中存放abcde
byte[] bytes = new byte[2];
int len = 0;
while(len>=0) {
len = fis.read(bytes);
System.out.println("len="+len+",bytes="+new String(bytes, 0, len));
//使用 String(bytes, 0, len)保证只把有效读取的字符转换为字符串~~~
}
fis.close();
/*
len=2,bytes=ab
len=2,bytes=cd
len=1,bytes=ed //只读取到一个e,覆盖掉c
len=-1,bytes=ed //读取到结束标记,返回-1
*/
2. 字符流
字节流读取中文字符的时候,由于中文一个字符可能占用多个字节存储,可能不会显示完整字符,所以java提供了字符流,以字符为单位读写数据,专门处理文本文件,底层也是字节流
2.2 字符输入流抽象类 Reader
- 成员方法:
boolean ready()
int read()
:读取单个字符,可读返回读取的整型数,遇到文件末尾返回-1int read(char[] cbuf)
: 读取到char数组abstract int read(char[] cbuf, int off, int len)
void reset()
long skip(long n)
abstract void close()
2.1 字符输出流抽象类 Writer
- 成员方法:
abstract void flush()
void write(int c)
void write(char[] cbuf)
abstract void write(char[] cbuf, int off, int len)
void write(String str)
void write(String str, int off, int len)
2.3 文件字符流
java.io.FileReader
extends InputStreamReader
extends Reader
java.io.FileWriter
extends OutputStreamWriter
extends writer
2.3.1 文件字符输入流 FileReader
- 构造方法:
FileReader(File file)
FileReader(String fileName)
- read 读取到的是字符的编码,需要转换为 char
2.3.2 文件字符输出流 FileWriter
- 构造方法:
FileWriter(String fileName)
FileWriter(String fileName, boolean append)
FileWriter(File file)
FileWriter(File file, boolean append)
字符流输出的时候,write方法先把数据写入到内存缓冲区中,字符转换为字节,再利用flush刷新内存缓冲区中的数据,把数据写入到文件中,释放资源也会自动把内存缓冲区中的数据刷新到文件中 - 字节输出流即使不调用
close()
方法,也会把数据写入到文件中,但是字符传输流如果不调用close()
或者flush()
,数据不会写入到文件,只在内存缓冲区中,程序运行结束时便消失了 - 与 try catch 结合使用
FileWriter fw = null; //提高变量作用域,使finally中可以使用
// 必须给初值,否则new出现异常无法执行finally
try {
fw = new FileWriter("D:a.txt");
fw.write(97); //写入int的ASCII码
char[] chs = {'a', 'b', 'c', 'r', 'n'};
fw.write(chs); //写入字符数组
String s = "写入字符串了rn";
fw.write(s); //写入字符串
fw.write(s, 0, 5);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fw != null) { //不为null时才close,否则会出现空指针异常
try { //close()的异常再次进行捕获
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 异常的处理
3.1 jdk7新特性
- 在jdk7之后,
try
之后可以加()
,在括号中定义流对象,此流对象作用域只在try中有效,try中代码执行完毕自动释放流对象不需要再写finally
public static void main(String[] args) {
try ( //在try中创建流对象
FileInputStream fis = new FileInputStream("D:lena.bmp");
FileOutputStream fos = new FileOutputStream("D:lena_copy.bmp");
) {
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
} //try中代码执行完之后自动释放流对象
} catch (IOException e) {
e.printStackTrace();
}
}
3.2 jdk9新特性
- 可以在 try之前定义流对象,在try的括号中引入流对象的名称,try的代码执行完毕之后,流对象也可以自动释放,不用写finally释放流对象
public static void main(String[] args) throws FileNotFoundException {
FileInputStream fis = new FileInputStream("D:lena.bmp");
FileOutputStream fos = new FileOutputStream("D:lena_copy.bmp");
try (fis;fos) {
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (IOException e) {
System.out.println("出错了" + e);
e.printStackTrace();
}
}
4. File类
- 构造函数:
File(String directoryPath)
File(String directoryPath, String filename)
: 操作同目录多个文件时比较方便File(File dirObj, String filename)
- 常用方法:
String getName()
String getParent()
: 返回父路径名String getPath()
: 路径名转字符串boolean isFile()
: 此路径名是表示的文件是否是标准文件boolean equals()
String[] list()
: 返回目录中所有文件和文件夹名的字符串数组boolean mkdir()
: 创建路径对应的目录String getAbsolutePath()
: 返回绝对路径名boolean exist()
File file = new File("D:b.txt");
file.createNewFile(); //创建对应的文件
System.out.println(File.pathSeparator); //路径分隔符,Windows是; linux是:
System.out.println(File.separator); //路径名称分隔符,Windows是 linux是/
if(file.exists()){ //删除文件
file.delete();
}
//也可以对文件夹操作
String fileName = "D" + File.separator + "aabb";
File file1 = new File(fileName);
file.mkdir(); //创建文件夹
file.listFiles(); //列出所有文件,包括隐藏文件
file.isDirectory(); //判断指定路径是否是目录
5. Properties集合
Properties extends Hashtable<k, v> implements Map<k, v>
- Properties 类表示了一个持久的属性集,是唯一和IO流相结合的集合,可以保存在流中或者从流中加载
- Properties 集合的 key 和 value 默认都是字符串
Object setProperities(String key, String value)
: 调用 Hashtable 的 put 方法String getPreperities(String key)
: 通过 key 找到对应 valueSet<String> ProperityNames()
: 返回所有的key的集合,相当于 Map.keySet() - 写
void store(Writer writer, String comments)
: 使用字符输出流输出(中文用字符流), comments是注解(unicode),用中文会输出unicodevoid store(OutputStream out, String comments)
: 使用字节输出流输出,输出中文会得到unicode码 - 读
synchronized void load(Reader reader)
synchronized void load(InputStream inStream)
读取的时候,key和value的连接符可以是等号=,空格,以及其他符号
存储的文件中可以使用 # 对键值对进行注释,被注释的键值对不再读取
键值都是字符串,不用再加 ""
Properties pp = new Properties();
pp.setProperty("11201209120","小明");
pp.setProperty("11201209121","小红");
pp.setProperty("11201209122","小蓝");
pp.setProperty("11201209123","小绿");
Set<String> keys= pp.stringPropertyNames(); //得到keys(names)
System.out.println(keys);
try(FileWriter fw = new FileWriter("D:a.txt");){ //字符流写入
pp.store(fw,"中文some coments...");
} catch (IOException e) {
e.printStackTrace();
}
try(FileOutputStream fos = new FileOutputStream("D:b.txt");){ //字节流写入
pp.store(fos, "中文some coments...");
} catch (IOException e) {
e.printStackTrace();
}
/*结果
a.txt
#u4E2Du6587some coments...
#Sun Aug 02 20:25:39 CST 2020
11201209122=小蓝
11201209121=小红
11201209123=小绿
11201209120=小明
b.txt
#u4E2Du6587some coments...
#Sun Aug 02 20:25:39 CST 2020
11201209122=u5C0Fu84DD
11201209121=u5C0Fu7EA2
11201209123=u5C0Fu7EFF
11201209120=u5C0Fu660E
*/
接下来读一下文件到集合:
pp.clear();
try(FileReader fr = new FileReader("D:b.txt")){
pp.load(fr);
} catch (IOException e) {
e.printStackTrace();
}
for(Map.Entry<Object, Object> entry : pp.entrySet()){
System.out.println(entry.getKey()+"--->"+entry.getValue());
}
6. 缓冲流(高效流)
- 上面的字节流和字符流在每次读写数据时,都要经过jvm,os,读写文件,效率低下
- 缓冲流给基本的输入输出流增加一个缓冲区(数组),提高基本的字符输入流的读取效率
6.1 字节缓冲流
- 字节缓冲流:
BufferedInputStream extends InputStream
BufferedOutputStream extends OutputStream - 构造方法:
BufferedInputStream(InputStream in)
BufferedInputStream(InputStream in, int size)
: 指定缓冲区的大小BufferedOutputStream(OutputStream out)
BufferedOutputStream(OutputStream out, int size)
long start = System.currentTimeMillis();
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:lena.bmp"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:lena_copy.bmp"));
) {
byte[] bytes = new byte[1024];
int len = 0;
while((len = bis.read(bytes))!=-1){
bos.write(bytes, 0, len);
bos.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("用时"+(end-start)+"ms"); // 3ms
6.2 字符缓冲流
- 字符缓冲流
BufferedWriter extends Writer
BufferedReader extends Reader - 构造方法:
BufferedReader(Reader in)
BufferedReader(Reader in, int size)
: 指定缓冲区的大小BufferedWriter(Writer out)
BufferedWriter(Writer out, int size)
- 其他方法:
String readLine()
: 读取一行,不会读取换行符,到达文件末尾时返回null
void newLine()
: 根据不同系统,写入一个行分割符(println也是调用的newLine)
String line = null;
while((line = br.readLine())!=null){
System.out.println(line);
}
- 注意:第一行直接回车,也能读取到换行符,并不会返回null结束读取
7. 转换流
- 之前的
FileReader
和FileWriter
只能按UTF-8编码 InputStreamReader : 字节流向字符的桥梁,是FileReader的父类,可以指定编码方式 OutputStreamWriter - 构造方法:
OutputStreamWriter(OutputStream out, String charsetName)
OutputStreamWriter(OutputStream out, Charset cs)
InputStreamReader(InputStream in, String charsetName)
InputStreamReader(InputStream in, Charset cs)
//以GBK编码格式写入和读取
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream("D:a.txt"), Charset.forName("GBK")))) {
bw.write("缓冲的带格式的写入方法".toCharArray());
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream("D:a.txt"), Charset.forName("GBK")))) {
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
}
8. 对象序列化
8.1 序列化操作
ObjectOutputStream
extends OutputStream
ObjectInputStream
extends InputStream
- 写对象,可以把对象转换为字节序列,写入到文件(序列化),或者把文件中的对象读出来(反序列化)
- 类必须实现
Serializable
接口,才能被序列化 - 构造方法:
ObjectOutputStream(OutputStream out)
ObjectInputStream(InputStream in)
//序列化对象
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(new File("D:" + File.separator + "a.txt")));
) {
Student s1 = new Student("小明", 12, "男"); //Student必须实现Serializable接口
oos.writeObject(s1);
} catch (IOException e) {
e.printStackTrace();
}
//反序列化
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:" +
File.separator + "a.txt")))){
Object s1 = ois.readObject();
System.out.println(((Student)s1).getName());
System.out.println(s1);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
8.2 瞬态关键字 transient
- 只要实现了
Serilizable
接口,对象就可以被序列化,但是有时候为了安全起见,类的一些敏感属性不希望被序列化后传输,这事就需要瞬态关键字 - 在对象序列化的时候,
transient1
修饰的成员变量不能被序列化,生命周期仅存在于调用者的内存中,不会写到磁盘里持久化 -
transient
只能修饰变量,不能修饰方法和类 -
stastic
变量属于类,不管是否被`transient`修饰,都不能被序列化 - 需要用两个进程分别测试序列化和反序列化,否则jvm已经把类加载进来了,可以读取到类的static变量信息
测试:
public class Student implements Serializable {
private String name;
private static int age;
private transient String sex;
constructor...getter and setter....
}
//序列化
public class DemoObjectOutputStream {
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
new File("D:" + File.separator + "a.txt")));
) {
Student s1 = new Student("小明", 12, "男");
oos.writeObject(s1);
} catch (IOException e) {
e.printStackTrace();
}
}
}
//反序列化
public class DemoObjectInputStream {
public static void main(String[] args) {
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
new File("D:" + File.separator + "a.txt")))){
Object s1 = ois.readObject();
System.out.println(s1);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
}
}
// 结果:
// Student{name='小明', age=0, sex='null'}
8.3 InvalidClassException
- 当我们反序列化时,如果对应的 class 和序列化时相比已经发生了变化,就会出现
InvalidClassException
,class文件和序列化文件的SerialVersionUID
不匹配 - 如果我们想修改class之后不让
SerialVersionUID
不发生变化,可以在类中通过显示声明Static final long SerialVersionUID = ...
定义自己的SerialVersionUID
- IDEA也可以自动生成
serialVersionUID
,settings -> Editor -> Inspections -> Serialization issues -> Serialization class without 'serialVersionUID' 打上勾即可
8.4 writeObject()与readObject()
- 对于被transient修饰的字段,除了将transitive关键字去掉之外,还可以在类中添加两个方法:writeObject()与readObject(),如下所示:
//学生类,添加writeObject()和readObject()方法
public class Student implements Serializable {
private static final long serialVersionUID = 1686151893029100782L;
private String name;
private static int age;
private transient String sex;
Constructor...
private void writeObject(ObjectOutputStream oos) throws IOException { //添加writeObject方法
oos.defaultWriteObject();
oos.writeChars(sex);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { //添加readObject方法
ois.defaultReadObject();
sex = String.valueOf(ois.readChar());
}
getter and setter...
}
public class DemoObjectOutputStream {
public static void main(String[] args) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:" + File.separator + "a.txt")));
) {
Student s1 = new Student("小明", 12, "男");
oos.writeObject(s1);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class DemoObjectInputStream {
public static void main(String[] args) {
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:" + File.separator + "a.txt")))){
Object s1 = ois.readObject();
System.out.println(s1);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
}
}
//结果
//Student{name='小明', age=12, sex='男'}
//成功的写入了。。。。
9. 一点小问题
可以看到,java字节流和字符流的输入,都是在遇到文件末尾的时候返回-1,那如果我们直接往文件中写入-1的话,会发生什么情况呢?会不会读取不到之后的信息呢?
FileOutputStream fos = new FileOutputStream("D:a.txt");
fos.write(-1);
fos.write(97);
fos.write((97-256));
fos.close();
FileInputStream fis = new FileInputStream("D:a.txt");
int len = 0;
byte[] chs = new byte[10];
len = fis.read(chs);
System.out.println(Arrays.toString(chs));
System.out.println(new String(chs, 0, len));
fis.close();
FileInputStream fis2 = new FileInputStream("D:a.txt");
int value = 0;
while((value=fis2.read())!=-1){
System.out.print(value+" ");
}
//最后得到的结果是
//[-1, 97, 97, 0, 0, 0, 0, 0, 0, 0]
//�aa
//255 97 97
可以看到,当我们以byte数组接收的时候,接收到的仍然是-1,以int接受的时候返回的是255,这是怎么回事呢?
原来,在java中,byte
以一个字节表示的是有符号整数,也就是[-128,127]
当我们传入-1的时候(1000 0001)b,存储的是-1的补码(1111 1111)b
在int
形式输出的时候,int
是32位,自然就是255了,而read()
方法返回值就是int
啊,所以写入-1,实际上读取到的是int
的255,所以不会停止读入
而在用byte[]
接收的时候又进行了int
转byte
,byte
接收到(11111111)b,自然就输出-1了
我们如果按下面的方式读取,就不会读到任何东西了
FileInputStream fis2 = new FileInputStream("D:a.txt");
byte value = 0;
while((value=(byte)fis2.read())!=-1){
System.out.print(value+" ");
}