1、IO流
我们一般把数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为 输入input 和 输出 output ,即流向内存是输入流,流出内存的输出流。
在JDK java.io 包下定义了对这些数据流的操作的类。将操作方法分为四大类并声明了4个顶级父类用来不同功能的子类实现和继承。
字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|
InputStream | OutputStream | reader | Write |
一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底 层传输的始终为二进制数据。
字符流依照编码表一般用来处理包含中文的文本数据,字节流也可以处理但在一些特殊情况下需要将传输数据实时显示时会因为拆分中文字符而出现乱码的情况,更多的则是用它来处理一些图片视频等等。
2、文件操作流
/**
* FileOutputStream:用于将内存中的数据写入到硬盘文件当中
* 1、创建一个FileOutputStream对象,传入数据写出的目的地,以及写入方式 false(默认)和 true
* 2、默认会根据路径或文件名创建文件,会覆盖已有的重名文件,修改为true后会在原有文件内容后追加
* 3、通过对象调入Write方法,将数据写入到文件当中
* 4、释放资源
*/
@Test
public void fileOutputStream_Test(){
FileOutputStream fos1 = null;
FileOutputStream fos2 = null;
try {
//创建对象
fos1 = new FileOutputStream("测试文件.txt",true);
/*写入数据
* 会将写入的十进制数据转换为二进制数据:97-->1100001
* 打开时会将二进制以字节的形式根据相应的编码表转换为字符表示
* 0-127:会查询ASCII表
* 其它值查询系统默编码表
* */
fos1.write(97);
//字符->Byte数组->write
byte[] bytes = "好好学习天天向上!\r\n".getBytes();
fos1.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos1!=null){
//关闭流资源
try {
fos1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* FileInputStream:用于将硬盘中的文件读取到内存中
* 1、创建一个FileInputStream对象,传入数据读取的目的地
* 2、通过对象调入read方法,将数据读取到内存中
* 3、释放资源
*/
@Test
public void fileIputStream_Test() {
FileInputStream fis = null;
try {
//创建对象
fis = new FileInputStream("测试文件.txt");
//读取数据的一个字节,指针向后移动一位读取到末尾返回-1
System.out.println(fis.read());
byte[] bytes = new byte[4];
int len;
while ((len=fis.read(bytes))!= -1){
System.out.print(new String(bytes,0,len));//乱码问题
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis != null){
//关闭流资源
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 在不同的编码环境下一个中文字符往往要占用多个字节,
* 在使用字节流读取时,往往需要将中文字符做拆分,这样会导致在读取过程中如果需要对内容做显示时中文字符会出现乱码
* 使用字符输入流可以解决字节流读取中文字符过程中需要显示时出现的乱码问题(一般没有这种需求,字符流只针对文本无法处理图片等内容)
*
* 继承关系:FileReader extends InputStreamReader extends Reader
* FileReader:用于将硬盘中的文件以字符的方式读取到内存中
* 1、创建一个FileReader对象,传入数据以字符的方式读取的目的地
* 2、通过对象调入read方法,将数据以字符的方式读取到内存中
* 3、释放资源
*/
@Test
public void fileReader_Test(){
FileReader fr = null;
try {
fr = new FileReader("测试文件.txt");
char[] chars = new char[5];
int len;
while ((len=fr.read(chars))!=-1){
System.out.print(new String(chars, 0, len));//显示读取到的字符内容
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr!=null){
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 继承关系:FileWrite extends OutputStreamWrite extends Write
* FileReader:用于将内存中的字符的方式写出到硬盘中的文件
* 1、创建一个FileWrite对象,传入数据以字符的方式写出的目的地
* 2、通过对象调入read方法,将数据以字符的方式写出到硬盘中
* 3、释放资源
*
* flush()和close()的区别
* flush():刷新缓冲区,不关闭流还可以继续写入
* close():先刷新缓冲区,然后释放流资源不可以再继续写入
*/
@Test
public void fileWrite_Test(){
FileWriter fw = null;
try {
fw = new FileWriter("测试文件.txt",true);
fw.write("刷新后会先被写入到文件当中\r\n");
fw.flush();//刷新缓冲区,之后可以再次继续写入
char[] chars = new char[]{'恭','喜','发','财','\n'};
fw.write(chars);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fw!=null){
try {
fw.close();//刷新缓冲区,关闭流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
通过字节文件流实现图片的复制
/**复制一个图片*/
@Test
public void fileIputOutputStream_Test(){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("测试图片.jpg");
fos = new FileOutputStream("NEW测试图片.jpg");
byte[] bytes = new byte[10];//创建一个缓存区
int len;//用来接收每次读取有效字节的个数
while ((len=fis.read(bytes))!=-1){
fos.write(bytes,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
JDK1.7及以后的版本中针对try-catch语法进行了优化使得在处理流资源不再显的那么臃肿
/**
* JDK7新增特性
* 在try的后面可以增加一个(),在括号中创建流对象
* 那么这个流对象的作用域就在try中有效
* try中的代码执行完毕 就会自动将流释放 不用再使用finally关闭流资源了
* JDK9新增特性
* 在原有的基础上在try外面创建好对象,将对象名放到括号中即可(创建异常直接throws)
* 这里只实现JDK7中的功能,俺用的JDK8实现不了...
*/
@Test
public void test_01() {
//优化前
// FileInputStream fis = null;
// FileOutputStream fos = null;
// try {
// fis = new FileInputStream("测试图片.jpg");
// fos = new FileOutputStream("NEW测试图片.jpg");
// byte[] bytes = new byte[10];//创建一个缓存区
// int len;//用来接收每次读取有效字节的个数
// while ((len=fis.read(bytes))!=-1){
// fos.write(bytes,0,len);
// }
// } catch (IOException e) {
// e.printStackTrace();
// } finally {
// if(fis!=null){
// try {
// fis.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// if(fos!=null){
// try {
// fos.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
//优化后
try(FileInputStream fis = new FileInputStream("测试图片.jpg");
FileOutputStream fos = new FileOutputStream("NEW测试图片.jpg");){
byte[] bytes = new byte[10];//创建一个缓存区
int len;//用来接收每次读取有效字节的个数
while ((len=fis.read(bytes))!=-1){
fos.write(bytes,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
3、Properties属性集
/**
* properties集合
* 继承关系:Properties extends HashTable implement Map
* Properties表示一个持久的属性集,Properties可保存在流中或从流中加载
* properties实现map接口是一个双列集合,key和value默认都是字符串类型
*
* setProperty():向properties集合中添加键值对
* getProperty():通过key获取value值
* stringPropertyNames():将集合中的所有Key保存到set集合中返回
*/
@Test
public void properties_Test(){
Properties properties = new Properties();
properties.setProperty("name","XiaYu");
properties.setProperty("sex","man");
properties.setProperty("age","21");
Set<String> strings = properties.stringPropertyNames();
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()){
String next = iterator.next();
System.out.println(next);
}
}
Properties是唯一一个可以和IO流相结合的集合,一般都会用它来读写或是操作一些配置文件
/**
* Properties是唯一一个可以和IO流相结合的集合
*
* 使用Properties中的方法 store(IO,str) 可以将集合中的临时数据持久化的写入到硬盘当中
* IO:可以传入OutputStream或者是write流,前者不可以写入中文,后者可以写入中文
* str:注释,解释说明保存内容不可以写入中文
*
* 使用步骤:
* 1、创建Properties对象,添加数据
* 2、创建字节或字符输出流对象,传入存储目的地的
* 3、使用properties集合中的store()将集合中的数据持久化的写入到硬盘中
* 4、释放资源
*
* 使用Properties中的方法 load(IO) 可以将硬盘中保存的文件(键值对),读取到集合中使用
* IO:可以传入InputStream或者是Reader流,前者不可以读取中文,后者可以读取中文
* 使用步骤:
* 1、创建Properties对象
* 2、使用Properties中的load()方法,读取保存键值对的文件
* 3、遍历
*/
@Test//写入
public void propertiesWrite_Test() throws IOException {
Properties properties = new Properties();
properties.setProperty("name","0606");
properties.setProperty("age","秘密");
properties.store(new FileWriter("数据文件.txt"),"个人信息");
}
@Test//读取
public void propertiesReader_Test() throws IOException {
Properties properties = new Properties();
properties.load(new FileReader("数据文件.txt"));
Set<String> strings = properties.stringPropertyNames();
Iterator<String> iterator = strings.iterator();
while(iterator.hasNext()){
String key = iterator.next();
String value = properties.getProperty(key);
System.out.println(key+"->"+value);
}
}
4、缓冲流
在前面完成文件复制操作时,为了进一步节省复制所消耗的时间,我们会声明一个数组作为中间传输的缓冲区,来完成一次性读写更多内容。
缓冲流实际上也是在实现这一功能,缓冲流的基本原理,就是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO 次数,从而提高读写的效率。
/**
* 字节/字符 缓冲输出流
* 会给字节/字符流增加缓冲区提高写入速度
* 继承关系:[BufferedOutputStream/BufferedWriter] extends OutputStream
*
* 使用步骤:
* 1、创建[BufferedOutputStream/BufferedWriter]对象,传入写出路径
* 2、创建[BufferedOutputStream/BufferedWriter]对象,传入创建好的[FileOutputStream/Filewrite]对象
* 3、使用[BufferedOutputStream/BufferedWriter]中的write()方法写入到缓冲区
* 4、使用[BufferedOutputStream/BufferedWriter]中的flush()将缓冲区数据刷新至文件中(可选)
* 5、使用close()关闭流资源,关闭前会将数据刷新到文件当中
*/
@Test
public void bufferedOutputStream_Test() throws Exception {
//BufferedOutputStream
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("测试文件.txt", true));
bos.write("我是缓冲流写入的数据1\n".getBytes());
bos.flush();
bos.write("我是缓冲流写入的数据2\n".getBytes());
bos.close();
//BufferedWrite
BufferedWriter bws = new BufferedWriter(new FileWriter("测试文件.txt",true));
bws.write("我是缓冲流写入的数据3\n");
bws.flush();
bws.write("我是缓冲流写入的数据4\n");
bws.close();
}
/**
* 字节/字符 缓冲输入流
* 会给字节/字符流增加缓冲区提高读取速度
* 继承关系:[BufferedInputStream/BufferedReader] extends OutputStream
*
* 使用步骤:
* 1、创建[BufferedInputStream/BufferedReader]对象,传入读取路径
* 2、创建[BufferedInputStream/BufferedReader]对象,传入创建好的[FileInputStream/FileReader]对象
* 3、使用[BufferedInputStream/BufferedReader]中的reade()读取文件
* 4、使用close()关闭流资源
*/
@Test
public void bufferedInputStream() throws Exception {
//BufferedInputStream
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("测试文件.txt"));
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
System.out.println(new String(bytes,0,len));
}
//BufferedReader
BufferedReader br = new BufferedReader(new FileReader("测试文件.txt"));
char[] chars = new char[1024];
int len1;
while ((len1=br.read(chars))!=-1){
System.out.println(new String(chars, 0, len1));
}
}
我们可以使用缓冲流更高效的实现我们之前的文件复制程序
//文件复制加强版
@Test
public void test_02(){
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream("测试图片.jpg"));
bos = new BufferedOutputStream(new FileOutputStream("new测试图片.jpg"));
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes))!=-1){
bos.write(bytes,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bis!=null){
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bos!=null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
5、转换流
字符编码和字符集
按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照 某种规则解析显示出来,称为解码 。
一套字符集必然至少有一套字符编码。常见字符 集有ASCII字符集、GBK字符集、Unicode字符集等。
转换流就是可以用自定义编码集来进行读取和写入
/**
* 转换流 InputStreamReader/OutputStreamWriter
* 解码:将字节根据字符集转换为字符
* 编码:将字符根据字符集转化为字节
* 我们在使用FileReader读取字符时实际上底层源码还是用FileInputStream实现的只不过FileReader在读取的过程中会使用系统默认编码表进行解码
* 系统默认编码表一般为UTF-8(IDEA右下角可以看到),之就导致如果FileReader读取的文件如果是其他编码格式的(如GBK)则会在解码时出现乱码的情况
* GBK编码表的中文字符是占用两个字节,而UTF-8则是占用三个字节
*
* InputStreamReader/OutputStreamWrite(stream,charsetName)
* 转换流构造器包含两个参数一个是文件流,一个是指定的编码解码格式(utf-8,gbk....)
* 可以使用InputStreamReader为文件指定编码表进行解码(指定编码集如果和文件所使用的编码不一致则会乱码)
* 可以使用OutputStreamWriter把文件根据指定编码表进行编码
*
* 使用转换流对文件进行转码 utf-8 --> GBK
*/
@Test
public void inputStreamReader_Test(){
InputStreamReader utf8 = null;
OutputStreamWriter gbk = null;
try {
utf8 = new InputStreamReader(new FileInputStream("测试文件.txt"), "utf-8");
gbk = new OutputStreamWriter(new FileOutputStream("测试文件(gbk).txt"), "gbk");
char[] chars = new char[1024];
int len;
while ((len=utf8.read(chars))!=-1){
gbk.write(chars,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (utf8!=null){
try {
utf8.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (gbk!=null){
try {
gbk.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//文件转码 GBK --> UTF-8
@Test
public void outputStreamWriter_Test(){
InputStreamReader gbk = null;
OutputStreamWriter utf8 = null;
try {
gbk = new InputStreamReader(new FileInputStream("测试文件(gbk).txt"),"GBK");
utf8 = new OutputStreamWriter(new FileOutputStream("测试文件(utf-8).txt"), "UTF-8");
char[] chars = new char[1024];
int len;
while ((len=gbk.read(chars))!=-1){
utf8.write(chars,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (gbk!=null){
try {
gbk.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (utf8!=null){
try {
utf8.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
6、序列化流
Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据 ,对象的类型 和 对象中存储的属性等信息。字节序列化写出到文件之后,相当于文件中持久保存了一个对象的信息。该字节序列还可以从文件中读取回来,对它进行反序列化。
使用序列化之前需要自定义一个支持序列化的类,也就是让类去实现Serializable接口
序列化冲突异常
反序列化时注意不能对原有的类进行修改,修改类会导致通过Serializable接口生成的序列号和反序列化后的类的序列号不一致,抛出序列化冲突异常,通过手动给类添加一个序列号,来保证在对类进行修改时Serializable不会再次生成新的序列号。
解决的方法就是:在类中声明一个:static final long serialVersionUID 的long型变量
(Serializable接口源码下定义了格式内容,我把那一段内容粘贴到了代码里↓↓↓↓↓↓↓)
//1、创建一个实现Serializable接口的Person类
class Person implements Serializable{
private String name;
private int age;
/**
* 自定义序列号
* The serialization runtime associates with each serializable class a version
* number, called a serialVersionUID, which is used during deserialization to
* verify that the sender and receiver of a serialized object have loaded
* classes for that object that are compatible with respect to serialization.
* If the receiver has loaded a class for the object that has a different
* serialVersionUID than that of the corresponding sender's class, then
* deserialization will result in an {@link InvalidClassException}. A
* serializable class can declare its own serialVersionUID explicitly by
* declaring a field named <code>"serialVersionUID"</code> that must be static,
* final, and of type <code>long</code>:
*/
static final long serialVersionUID = 66L;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
实现序列化和反序列化操作
/**
* ObjectOutputStream序列化 和 ObjectInputStream反序列化
*
* ObjectOutputStream(fileOutputStream(path)):将对象以流的方式写入到文件当中,
* writeObject(obj):将指定对象写入到文件当中
* ObjectInputStream(fileInputStream(path)):将文件中保存的对象以流的方式读取出来
* readObject(obj):将文件中的对象读取出来
*
* 想要使类可以被序列化,必须满足两个条件
* 1、该类必须实现java.io.Serializable接口(标记接口)
* 2、定义的属性也是要保证为可序列化的,对于不想序列换的属性 可以使用transient(瞬态关键字)修饰,static修饰过的属性也无法被序列化
*
* 要使类可以被反序列化,必须满足两个条件
* 1、该类必须实现java.io.Serializable接口(标记接口)
* 2、硬盘中必须存在对应的.class文件
*
*/
//ObjectOutputStream序列化
@Test
public void objectOutputStream_Test(){
ObjectOutputStream oos = null;
try {
Person person = new Person("0606",66);
oos = new ObjectOutputStream(new FileOutputStream("PersonObject.txt"));
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos!=null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//ObjectInputStream反序列化
@Test
public void objectInputStream() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("PersonObject.txt"));
Object object = ois.readObject();
ois.close();
System.out.println(object.toString());
}
小练习:序列化对象集合
@Test
public void test_03() throws Exception {
//1、创建集合
ArrayList<Person> arrayList = new ArrayList<>();
//2、向集合存储多个Person对象
arrayList.add(new Person("小王",18));
arrayList.add(new Person("小李",28));
arrayList.add(new Person("小阿Giao",38));
//3.使用ObjectOutputStream将集合对象序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("信息文件.txt"));
oos.writeObject(arrayList);
//4.使用ObjectInputStream将文件反序列化到内存
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("信息文件.txt"));
//5.读取文件中保存的集合
Object object = ois.readObject();
//6.强转为ArrayList进行遍历
ArrayList<Person> arr = (ArrayList<Person>) object;
Iterator iterator = arr.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next().toString());
}
//7.关闭流资源
oos.close();
ois.close();
}