概述
什么是IO
- 生活中,你肯定经历过这样的场景。当你编辑一个文本文件,忘记了 ctrl+s ,可能文件就白白编辑了。当你电脑 上插入一个U盘,可以把一个视频,拷贝到你的电脑硬盘里。那么数据都是在哪些设备上的呢?键盘、内存、硬 盘、外接设备等等。
- 我们把这种数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为 输入input 和 输出 output ,即流向内存是输入流,流出内存的输出流。
- Java中I/O操作主要是指使用 java.io 包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写 出数据。
IO的分类
根据数据的流向分为:输入流和输出流。
- 输入流 :把数据从 其他设备 上读取到 内存 中的流。
- 输出流 :把数据从 内存 中写出到 其他设备 上的流。
格局数据的类型分为:字节流和字符流。
- 字节流 :以字节为单位,读写数据的流。
- 字符流 :以字符为单位,读写数据的流。
顶级父类
字节流
FileInputStream类
概述
java.io.FileInputStream 类是文件输入流,从文件中读取字节。
构造方法
FileInputStream(File file) : 通过打开与实际文件的连接来创建一个
FileInputStream ,该文件由文件系 统中的 File对象 file命名。
FileInputStream(String name) : 通过打开与实际文件的连接来创建一
个FileInputStream ,该文件由文件 系统中的路径名 name命名。
import java.io.FileInputStream;
import java.io.IOException;
public class demo2 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("demo2.txt");
int temp ;
int count = 0 ;
while((temp = fis.read()) != -1){
count++;
System.out.print((char)temp);
}
System.out.print("\n"+count);
}
}
运行结果:
abc
def
ghi
jkl
18
仔细观察上面可知,read()方法返回的是一个int值,而不是一个byte值呢?
- 因为字节输入流可以操作任意类型的文件,比如图片音频等,这些文件底层都是以二进制形式的存储的,如果每次读取都返回byte,有可能在读到中间的时候遇到111111111那么这11111111是byte类型的-1,我们的程序是遇到-1就会停止不读了,后面的数据就读不到了,所以在读取的时候用int类型接收,如果11111111会在其前面补上24个0凑足4个字节,那么byte类型的-1就变成int类型的255了这样可以保证整个数据读完,而结束标记的-1就是int类型。
字节数组读取
- 使用字节数组读取: read(byte[] b) ,每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读 取到末尾时,返回 -1
import java.io.FileInputStream;
import java.io.IOException;
public class Input_char {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("demo1.txt");
/*文件中的内容:
窝窝头,一块钱四个,嘿嘿!!!
谁TM买小米!!!
菠菜~,菠菜~,贱卖!!!
*/
//定义字节数组,作为装字节的容器
byte[] bytes = new byte[1024];
//字节的有效个数
int flag ;
while( (flag = fis.read(bytes)) != -1 ){
//通过String的构造方法输出,不懂就看手册就完事
System.out.print(new String(bytes,0,flag));
}
fis.close();
}
}
输出:
窝窝头,一块钱四个,嘿嘿!!!
谁TM买小米!!!
菠菜~,菠菜~,贱卖!!!
FileOutputStream类
import java.io.FileOutputStream;
import java.io.IOException;
public class Output_demo1 {
public static void main(String[] args) throws IOException {
//如果没有对应的文件,则会创建一个相应文件
FileOutputStream fos = new FileOutputStream("demo2.txt");
//97的字节数就是a,虽然写进去的是一个int值,但是前三个八位都为0,所以就....不知道了
fos.write(97);
fos.write(98);
fos.write(99);
//用完记得关掉
fos.close();
}
}
如果demo1.txt中有内容,进行上述操作之后会发现,其中的内容没有了,只有abc,这是因为她的构造方法
FileOutputStream(File file, boolean append)
创建文件输出流以写入由指定的 File对象表示的文件。
如果要进行追加,就必须声明,如下
FileOutputStream fos = new FileOutputStream("demo2.txt",true);
构造方法
public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
public void write(byte[] b) :将 b.length字节从指定的字节数组写入此输出流。
public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输 出到此输出流。
public abstract void write(int b) :将指定的字节输出流。
import java.io.FileOutputStream;
import java.io.IOException;
public class Output_byte {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("demo3.txt");
//将字符串转换为字节,并存入字节数组。
byte[] bytes = "敲代码要时不时看看自己左手的劳力士,这样才有动力\n".getBytes();
fos.write(bytes);
fos.close();
}
}
结果肯定是写进去了呗,但是还是一样,如果当时文件中有内容,肯定得被清空,然后再次写入内容,解决方法和上面一样。
拷贝图片
先看看代码:
import java.io.FileOutputStream;
import java.io.IOException;
public class Copy_pictrue {
public static void main(String[] args) throws IOException {
//获取被拷贝的对象
FileInputStream fis = new FileInputStream("C:\\Users\\DELL-pc\\Pictures\\头像.jpg");
//创建拷贝的对象,我也说不清楚这是啥
//反正就是把上面路径的文件拷贝到下面的文件中
FileOutputStream fos = new FileOutputStream("copy.jpg");
int len ;
while((len = fis.read()) != -1){
fos.write(len);
}
fis.close();
fos.close();
}
}
一运行,就会发现这个有点问题,并不是说这个程序有问题,程序本身是没有问题的,就是如果拷贝稍微大一点的文件,这样一个字节一个字节拷贝肯定是不行的,效率太低了。自己可以试一试拷贝一个Mp3文件试试,骗你就是窝窝头!!!!
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Copy_pictrue {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("C:\\Users\\DELL-pc\\Music\\梁博 - 男孩 (伴奏).tkm");
FileOutputStream fos = new FileOutputStream("C:\\Users\\DELL-pc\\Music\\copy_男孩.tkm");
int len ;
byte[] bytes = new byte[1024*8];
//还有一种方法就是使用FileInputStream中的available()方法
//byte[] bytes = new byte[fis.available()];,但是这种方法容易出现内存溢出,不推荐
while((len = fis.read(bytes)) != -1){
fos.write(bytes);
}
fis.close();
fos.close();
}
}
一个文件少说几万个字节,多则,多则,,多则,,,反正很多就是了,采用一个字节一个字节读取的话,消耗的时间代价就很大。
BufferedInputStream和BufferOutputStream拷贝
- A:缓冲思想
- 字节流一次读写一个数组的速度明显比一次读写一个字节的速度快很多,
- 这是加入了数组这样的缓冲区效果,java本身在设计的时候,
- 也考虑到了这样的设计思想(装饰设计模式后面讲解),所以提供了字节缓冲区流
- B.BufferedInputStream
- BufferedInputStream内置了一个缓冲区(数组)
- 从BufferedInputStream中读取一个字节时
- BufferedInputStream会一次性从文件中读取8192个, 存在缓冲区中, 返回给程序一个
- 程序再次读取时, 就不用找文件了, 直接从缓冲区中获取
- 直到缓冲区中所有的都被使用过, 才重新从文件中读取8192个
- C.BufferedOutputStream
- BufferedOutputStream也内置了一个缓冲区(数组)
- 程序向流中写出字节时, 不会直接写到文件, 先写到缓冲区中
- 直到缓冲区写满, BufferedOutputStream才会把缓冲区中的数据一次性写到文件里
代码实现:
import java.io.*;
public class Buffered_copy {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("C:\\Users\\DELL-pc\\Music\\梁博 - 男孩 (伴奏).tkm");
FileOutputStream fos = new FileOutputStream("C:\\Users\\DELL-pc\\Music\\copy_男孩.tkm");
//对fis,fos进行包装,更牛逼了
BufferedInputStream bis= new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
int len ;
//看到这里有人可能会问,这不是一个字节一个字节读取的?,不得不说,源码是个好东西,自己去看看就知道了
while((len = bis.read()) != -1){
bos.write(len);
}
//关闭时只需要关闭这些。
bis.close();
bos.close();
}
}
个人理解:定义一个小的字节数组和上面差不多,都可以实现,且差别不大。
flush和close方法的区别
- flush()方法
- 用来刷新缓冲区的,刷新后可以再次写出 。
- close()方法
- 用来关闭流释放资源的的,如果是带缓冲区的流对象的close()方法,不但会关闭流,还会再关闭流之前刷新缓冲区,关闭后不能再写出 。
注解:流的关闭原则:先开后关,后开先关。
咱们得按照原则来,哎!学习就是这么枯燥!!!
字符流
java.io.Reader 抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入 流的基本共性功能方法。
public void close() :关闭此流并释放与此流相关联的任何系统资源。
public int read() : 从输入流读取一个字符。
public int read(char[] cbuf) : 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。
FileReader类
java.io.FileReader 类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
构造方法
FileReader(File file) : 创建一个新的 FileReader ,给定要读取的File对象。
FileReader(String fileName) : 创建一个新的 FileReader ,给定要读取的文件的名称。
单个字符读取:
public class Demo_FileReader {
public static void main(String[] args) throws IOException {
//学完字节流之后,只需要看文档,再手操一遍即可
FileReader fr = new FileReader("demo1.txt");
int c ;
while((c = fr.read()) != -1){
System.out.print((char)c);
}
fr.close();
}
}
字符数组读取:
FileReader fr = new FileReader("demo1.txt");
int c;
char[] arr = new char[4];
while(( c = fr.read(arr)) != -1) {
System.out.print(new String(arr,0,c));
}
fr.close();
看吧,差不懂的。
FileWriter类
java.io.Writer 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节 输出流的基本共性功能方法。
void write(int c) 写入单个字符。
void write(char[] cbuf) 写入字符数组。
abstract void write(char[] cbuf, int off, int len) 写入字符数组的某一部分,off数组的开始索引,len 写的字符个数。
void write(String str) 写入字符串。
void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个 数。
void flush() 刷新该流的缓冲。
void close() 关闭此流,但要先刷新它。
构造方法
FileWriter(File file) : 创建一个新的 FileWriter,给定要读取的File对象。
FileWriter(String fileName) : 创建一个新的 FileWriter,给定要读取的文件的名称。
IO流的构造方法都差不多,一个把文件封装为File类传入参数,一个是将文件路径名作为字符串传入,后面的话我就不写,因为此篇博客作为自学笔记使用。
import java.io.FileWriter;
import java.io.IOException;
public class Demo_FileWriter {
public static void main(String[] args) throws IOException {
//需要追加则加true
FileWriter fw = new FileWriter("demo2.txt");
//可传入,int值,字符,字符串,字符数组
fw.write('窝');
fw.write('窝');
fw.write('头');
fw.write("一块钱四个");
fw.write("嘿嘿!!!".toCharArray());
fw.close();
}
}
注:字符流只适用于文本文件。
BufferedReader类和BufferedWriter类
public BufferedReader(Reader in) :创建一个 新的缓冲输入流。
public BufferedWriter(Writer out) : 创建一个新的缓冲输出流。
- BufferedReader的read()方法读取字符时会一次读取若干字符到缓冲区, 然后逐个返回给程序, 降低读取文件的次数, 提高效率
- BufferedWriter的write()方法写出字符时会先写到缓冲区, 缓冲区写满时才会写到文件, 降低写文件的次数, 提高效率
import java.io.*;
public class Demo_Buffered {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("demo.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("demo3.txt"));
int len ;
while((len = br.read()) != -1){
bw.write(len);
}
bw.close();
br.close();
}
}
特有方法
BufferedReader: public String readLine() : 读一行文字。
BufferedWriter: public void newLine() : 写一行行分隔符,由系统属性定义符号。
public class Demo_Buffered {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("demo.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("demo3.txt"));
String len ;
while((len = br.readLine()) != null){
bw.write(len);
bw.newLine();
}
bw.close();
br.close();
}
}
LineNumberReader类
LineNumberReader是BufferedReader的子类, 具有相同的功能, 并且可以统计行号。
* 调用getLineNumber()方法可以获取当前行号
* 调用setLineNumber()方法可以设置当前行号
转换流
字符编码和字符集
概述
-
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制 数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照 某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本f符 号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。
字符编码 Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。 字符集 Charset :也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符 号、数字等。
编码引出的问题
- 在IDEA中,使用 FileReader 读取项目中的文本文件。由于IDEA的设置,都是默认的 UTF-8 编码,所以没有任何 问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统的默认是GBK编码,就会出现乱码。
InputStreamReader
转换流 java.io.InputStreamReader ,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定 的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造方法
InputStreamReader(InputStream in) : 创建一个使用默认字符集的字符流。
InputStreamReader(InputStream in, String charsetName) : 创建一个指定字符集的字符流。
代码演示:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class Demo_InputStreamReader {
public static void main(String[] args) throws IOException {
//创建文本utf8.txt的File对象,编码为utf8
FileInputStream fis1 = new FileInputStream("E:\\IdeaProjects\\basic-code\\IO_code\\src\\ReaderAndWriter\\utf8.txt");
//创建文本GBK.txt的File对象,编码为GBK
FileInputStream fis2 = new FileInputStream("E:\\IdeaProjects\\basic-code\\IO_code\\src\\ReaderAndWriter\\GBK.txt");
//如果不指定字符集,则默认为utf8
InputStreamReader isr1 = new InputStreamReader(fis1);
int len1;
while( (len1 = isr1.read()) !=-1){
System.out.print((char)len1);
}
isr1.close();
//指定字符集为GBK码表
InputStreamReader isr2 = new InputStreamReader(fis2,"GBK");
int len2 ;
while( (len2 = isr2.read()) !=-1){
System.out.print((char)len2);
}
isr2.close();
}
}
输出:
窝窝头,一块钱四个,嘿嘿!!!谁TM买小米!!!
当然,也可以使用read(char[] cbuf, int offset, int length) 来读取,方法和以前的没有太大区别。同样的,也可以通过BufferedReader封装。
OutputStreamWriter
转换流 java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符 编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造方法
OutputStreamWriter(OutputStream in) : 创建一个使用默认字符集的字符流。
OutputStreamWriter(OutputStream in, String charsetName) : 创建一个指定字符集的字符流。
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo_OutputStreamWriter {
public static void main(String[] args) throws IOException {
//默认为utf8编码
FileOutputStream fos = new FileOutputStream("IO_code\\src\\ReaderAndWriter\\utf8.txt",true);
fos.write("妈妈,我想吃烤山药".getBytes());
fos.write("吃,吃大块的,两块够吗?".getBytes());
fos.write("够了,谢谢妈妈,妈妈真好".getBytes());
fos.close();
}
}
转换流图解
序列流
概述
- Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据 、 对象的 类型 和 对象中存储的属性 等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
- 反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。 对象的数据 、 对象的类型 和 对象中 存储的数据 信息,都可以用来在内存中创建对象。
看图理解序列化:
ObjectOutputStream类
java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。
构造方法
public ObjectOutputStream(OutputStream out) : 创建一个指定OutputStream的ObjectOutputStream。
序列化操作
一个对象要想序列化,必须满足两个条件:
-
该类必须实现 java.io.Serializable 接口, Serializable 是一个标记接口,不实现此接口的类将不会使任 何状态序列化或反序列化,会抛出 NotSerializableException 。
-
该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用 transient 关键字修饰。
public final void writeObject (Object obj) : 将指定的对象写出。
定义一个Person类
public class Person implements Serializable {
private String name ;
//测试transient
//private transient int age;
private int age ;
}
代码:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Demo_ObjectOutputStream {
public static void main(String[] args) throws IOException {
Person[] arr ={
new Person("张三",21),
new Person("李四",25)
};
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("IO_code\\src\\Demo_object\\person.txt"));
oos.writeObject(arr);
oos.close();
}
}
ObjectInputStream类
ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。
构造方法
public ObjectInputStream(InputStream in) : 创建一个指定InputStream的ObjectInputStream。
反序列操作1
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用 ObjectInputStream 读取对象的方法:
public final Object readObject () : 读取一个对象。
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Arrays;
public class Demo_ObjectInputStream {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("IO_code\\src\\Demo_object\\person.txt"));
Person[] arr = (Person[]) ois.readObject();
ois.close();
System.out.println(Arrays.toString(arr));
}
}
对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。
反序列操作2
另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操 作也会失败,抛出一个 InvalidClassException 异常。发生这个异常的原因如下:
- 该类的序列版本号与从流中读取的类描述符的版本号不匹配 。
- 该类包含未知数据类型 。
- 该类没有可访问的无参数构造方法 。
- Serializable 接口给需要序列化的类,提供了一个序列版本号。 serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。
在Person中添加代码:
private static final long serialVersionUID = 42L ;
原理图:
练习:
- 将存有多个自定义对象的集合序列化操作,保存到 person.txt 文件中。
- 反序列化 person.txt ,并遍历集合,打印对象信息。
import java.io.*;
import java.util.ArrayList;
public class Test {
public static void init(ArrayList<Person> list){
Person p1 = new Person("张三",25);
Person p2 = new Person("李四",25);
Person p3 = new Person("王五",25);
list.add(p1);
list.add(p2);
list.add(p3);
}
public static void main(String[] args) {
ArrayList<Person> list = new ArrayList<>();
//初始化操作
init(list);
//进行序列化操作
serializ(list);
//进行反序列化操作
list = reserializ();
for (Person person : list) {
System.out.println(person.getName()+"="+person.getAge() );
}
}
public static ArrayList<Person> reserializ() {
ArrayList<Person> list = null ;
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("IO_code\\src\\Demo_object\\person.txt"));
ArrayList<Person> list1 = (ArrayList<Person>) ois.readObject();
ois.close();
list = list1 ;
} catch (IOException e) {
e.printStackTrace();
}catch (ClassNotFoundException c){
c.printStackTrace();
System.out.println("Person.class is not found");
}finally {
return list ;
}
}
public static void serializ(ArrayList<Person> list) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("IO_code\\src\\Demo_object\\person.txt"));
oos.writeObject(list);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出:
张三=25
李四=25
王五=25
注:练习代码的时候尽量使用try{}catch{}语句。
打印流
概述
平时我们在控制台打印输出,是调用 print 方法和 println 方法完成的。
这两个方法都来自于 java.io.PrintStream 类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。
构造方法
public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。
代码实现:
import java.io.FileNotFoundException;
import java.io.PrintStream;
public class Demo_print {
public static void main(String[] args) throws FileNotFoundException {
PrintStream ps = new PrintStream("IO_code\\src\\Demo_object\\print.txt");
//调用父类的方法
ps.write(97);
//调用自己特有的方法,可以输出任何类型的语句
ps.println(97);
ps.println("窝窝头");
ps.println('a');
ps.close();
}
}
person.txt:
a97
窝窝头
a