java 关于流的使用
Posted _奇点
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 关于流的使用相关的知识,希望对你有一定的参考价值。
1. IO流体验与简介
File对象可以表示存在的文件或文件夹,也可以表示不存在的。
我们想要得到文件的内容怎么办,File只是操作文件,文件的内容如何处理就需要使用io流技术了。
例如在C盘下有一个名称为a.txt的文本文件.想要通过Java程序读出来文件中的内容,需要使用IO流技术.同样想要将程序中的数据,保存到硬盘的文件中,也需要IO流技术.
读和写文件文件示例:
public class IoTest { public static void main(String[] args) throws FileNotFoundException, IOException { writFileTest();
readFileTest(); }
private static void writFileTest() throws FileNotFoundException, IOException { // 创建文件对象 File file = new File("c:\\a.txt"); // 创建文件输出流 FileOutputStream fos = new FileOutputStream(file); fos.write(‘g‘); fos.write(‘z‘); fos.write(‘i‘); fos.write(‘t‘); fos.write(‘c‘); fos.write(‘a‘); fos.write(‘s‘); fos.write(‘t‘); fos.close(); }
private static void readFileTest() throws FileNotFoundException, IOException { // 创建文件对象 File file = new File("c:\\a.txt"); // 创建文件输入流 FileInputStream fis = new FileInputStream(file); // 有对多长,就读多少字节。 for (int i = 0; i < file.length(); i++) { System.out.print((char) fis.read()); } fis.close(); } }
|
当完成流的读写时,应该通过调用close方法来关闭它,这个方法会释放掉十分有限的操作系统资源.如果一个应用程序打开了过多的流而没有关闭它们,那么系统资源将被耗尽.
IO流简介:(Input/Output)
I/O类库中使用“流”这个抽象概念。Java对设备中数据的操作是通过流的方式。
表示任何有能力产出数据的数据源对象,或者是有能力接受数据的接收端对象。“流”屏蔽了实际的I/O设备中处理数据的细节。IO流用来处理设备之间的数据传输。设备是指硬盘、内存、键盘录入、网络等。
Java用于操作流的对象都在IO包中。IO流技术主要用来处理设备之间的数据传输。
由于Java用于操作流的对象都在IO包中。所以使用IO流需要导包如:import java.io.*;
IO流的分类
流按操作数据类型的不同分为两种:字节流与字符流。
流按流向分为:输入流,输出流(以程序为参照物,输入到程序,或是从程序输出)
2. 字节流
什么是字节流
计算机中都是二进制数据,一个字节是8个2进制位.字节可以表示所有的数据,比如文本,音频,视频.图片,都是作为字节存在的.也就是说字节流处理的数据非常多。
在文本文件中存储的数据是以我们能读懂的方式表示的。而在二进制文件中存储的数据是用二进制形式表示的。我们是读不懂二进制文件的,因为二进制文件是为了让程序来读取而设计的。例如,Java的源程序(.java源文件)存储在文本文件中,可以使用文本编辑器阅读,但是Java的类(字节码文件)存储在二进制文件中,可以被Java虚拟机阅读。二进制文件的优势在于它的处理效率比文本文件高。
我们已经知道File对象封装的是文件或者路径属性,但是不包含向(从)文件读(写)数据的方法。为了实现对文件的读和写操作需要学会正确的使用Java的IO创建对象。
字节流的抽象基类:
输入流:java.io.InputStream
输出流:java.io.OutputStream
特点:
字节流的抽象基类派生出来的子类名称都是以其父类名作为子类名的后缀。
如:FileInputStream, ByteArrayInputStream等。
说明:
字节流处理的单元是一个字节,用于操作二进制文件(计算机中所有文件都是二进制文件)
2.1. InputStream
案例:读取"c:/a.txt"文件中的所有内容并在控制台显示出来。
注意:事先准备一个a.txt并放到c:/下,不要保存中文。
a, 使用read()方法实现。
b, 使用int read(byte[] b)方法实现。
写代码读取"c:/a.txt"文件中的所有的内容并在控制台显示出来
实现:
查看api文档(自己一定要动手)
InputStream 有read方法,一次读取一个字节,OutputStream的write方法一次写一个int。发现这两个类都是抽象类。意味着不能创建对象,那么需要找到具体的子类来使用。
通过查看api文档,找到了FileInputStream类,该类正是我们体验Io流的一个输入流。
实现;显示指定文件内容。 明确使用流,使用哪一类流?使用输入流,FileInputStream 第一步: 1:打开流(即创建流) 第二步: 2:通过流读取内容 第三步: 3:用完后,关闭流资源 |
显然流是Java中的一类对象,要打开流其实就是创建具体流的对象,由于是读取硬盘上的文件,应该使用输入流。所以找到了InputStream类,但是InputStream是抽象类,需要使用它的具体实现类来创建对象就是FileInputStream。通过new 调用FileInputStream 的构造方法来创建对象。发现FileInputStream的构造方法需要指定文件的来源。查看构造方法,可以接受字符串也可以接受File对象。我们通过构建File对象指定文件路径。
使用流就像使用水管一样,要打开就要关闭。所以打开流和关闭流的动作是比不可少的。如何关闭流?使用close方法即可,当完成流的读写时,应该通过调用close方法来关闭它,这个方法会释放掉十分有限的操作系统资源.如果一个应用程序打开了过多的流而没有关闭它们,那么系统资源将被耗尽.
如何通过流读取内容?
查找api文档通过read方法,查看该方法,发现有返回值,并且是int类型的,该方法一次读取一个字节(byte)
2.1.1. 输入流读取方式1:
read方法()
一次读取一个字节,读到文件末尾返回-1.
仔细查看api文档发现read方法如果读到文件的末尾会返回-1。那么就可以通过read方法的返回值是否是-1来控制我们的循环读取。
/** * 根据read方法返回值的特性,如果独到文件的末尾返回-1,如果不为-1就继续向下读。 * */ private static void showContent(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
int len = fis.read(); while (len != -1) { System.out.print((char)len); len = fis.read();
} // 使用完关闭流 fis.close(); } |
我们习惯这样写:
/** * 根据read方法返回值的特性,如果独到文件的末尾返回-1,如果不为-1就继续向下读。 * */ private static void showContent(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
int len; while ((len = fis.read()) != -1) { System.out.print((char) len); } // 使用完关闭流 fis.close(); } |
2.1.2. 输入流读取方式2:
使用read(byte[] b) 方法。使用缓冲区(关键是缓冲区大小的确定)
使用read方法的时候,流需要读一次就处理一次,可以将读到的数据装入到字节数组中,一次性的操作数组,可以提高效率。
问题1:缓冲区大小
那么字节数组如何定义?定义多大?
可以尝试初始化长度为5的byte数组。通过read方法,往byte数组中存内容
那么该read方法返回的是往数组中存了多少字节。
/** * 使用字节数组存储读到的数据 * */ private static void showContent2(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = new byte[5]; int len = fis.read(byt); for (int i = 0; i < byt.length; i++) { System.out.print((char) byt[i]); }
// 使用完关闭流 fis.close(); } |
问题1: 缓冲区太小:
数据读取不完.
测试发现问题,由于数组太小,只装了5个字节。而文本的字节大于数组的长度。那么很显然可以将数组的长度定义大一些。例如1024个。
/** * 使用字节数组存储读到的数据 * */ private static void showContent2(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = new byte[1024]; int len = fis.read(byt); for (int i = 0; i < byt.length; i++) { System.out.print(byt[i]); }
// 使用完关闭流 fis.close(); } |
问题三:缓冲区有默认值.
测试,打印的效果打印出了很多0,因为数组数组有默认初始化值,所以,我们将数组的数据全部都遍历和出来.现在需要的是取出数组中的部分数据.需要将循环条件修改仔细查看api文档。发现该方法read(byte[] b)返回的是往数组中存入了多少个字节。就是数组实际存储的数据个数。
/** * 使用字节数组存储读到的数据 * */ private static void showContent2(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = new byte[1024]; int len = fis.read(byt); for (int i = 0; i <len; i++) { System.out.print(byt[i]); }
// 使用完关闭流 fis.close(); } |
总结:
问题一:为什么打印的不是字母而是数字,
是字母对应的码值。
如何显示字符,强转为char即可
问题二:注意:回车和换行的问题。
windows的换车和换行是"\r\n" 对应码表是13和10 。
2.1.3. 输入流读取方式3:
使用read(byte[] b,int off,int len)
查看api文档,
b显然是一个byte类型数组,当做容器来使用
off,是指定从数组的什么位置开始存字节
len,希望读多少个
其实就是把数组的一部分当做流的容器来使用。告诉容器,从什么地方开始装要装多少。
/** * 把数组的一部分当做流的容器来使用 * read(byte[] b,int off,int len) */ private static void showContent3(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = new byte[1024]; // 从什么地方开始存读到的数据 int start = 5;
// 希望最多读多少个(如果是流的末尾,流中没有足够数据) int maxLen = 6;
// 实际存放了多少个 int len = fis.read(byt, start, maxLen);
for (int i = start; i < start + maxLen; i++) { System.out.print((char) byt[i]); }
// 使用完关闭流 fis.close(); } |
需求2:测试skip方法
通过Io流,读取"c:/a.txt"文件中的第9个字节到最后所有的内容并在控制台显示出来。
分析:其实就是要跳过文件中的一部分字节,需要查找API文档。可以使用skip方法skip(long n),参数跟的是要跳过的字节数。
我们要从第9个开始读,那么要跳过前8个即可。
/** * skip方法 * * */ private static void showContent4(String path) throws IOException { // 打开流 FileInputStream fis = new FileInputStream(path);
// 通过流读取内容 byte[] byt = new byte[1024]; fis.skip(8); int len = fis.read(byt); System.out.println(len); System.out.println("**********"); for (int i = 0; i < len; i++) { System.out.println((char) byt[i]); } // 使用完关闭流 fis.close(); } |
2.1.4. 输入流读取方式4:
使用缓冲(提高效率),并循环读取(读完所有内容).
总结:读完文件的所有内容。很显然可以使用普通的read方法,一次读一个字节直到读到文件末尾。为了提高效率可以使用read(byte[] byt);方法就是所谓的使用缓冲提高效率。我们可以读取大文本数据测试(大于1K的文本文件.)
/** * 使用字节数组当缓冲 * */ private static void showContent5(String path) throws IOException { FileInputStream fis = new FileInputStream(path); byte[] byt = new byte[1024]; int len = fis.read(byt); System.out.println(len); String buffer = new String(byt, 0, len); System.out.print(buffer); } |
注意:如何将字节数组转成字符串? 可以通过创建字符串对象即可。
发现:一旦数据超过1024个字节,数组就存储不下。
如何将文件的剩余内容读完?
我们可以通过通过循环保证文件读取完。
/** * 使用字节数组当缓冲 * */ private static void showContent7(String path) throws IOException { FileInputStream fis = new FileInputStream(path); byte[] byt = new byte[1024]; int len = 0; while ((len = fis.read(byt)) != -1) { System.out.println(new String(byt, 0, len)); } } |
2.2. OutputStream
字节输出流
案例:
1,写代码实现把"Hello World!"写到"c:/a.txt"文件中。
a, c:/a.txt不存在时,测试一下。
b, c:/a.txt存在时,也测试一下。
要写两个版本:
a, 使用write(int b) 实现。
b, 使用write(byte[] b) 实现。
2,在已存在的c:/a.txt文本文件中追加内容:“Java IO”。
显然此时需要向指定文件中写入数据。
使用的就是可以操作文件的字节流对象。OutputStream。该类是抽象类,需要使用具体的实现类来创建对象查看API文档,找到了OutputStream的实现类FileOutputStream 创建FileOutputStream 流对象,必须指定数据要存放的目的地。通过构造函数的形式。创建流对象时,调用了系统底层的资源。在指定位置建立了数据存放的目的文件。
流程: 1:打开文件输出流,流的目的地是指定的文件 2:通过流向文件写数据 3: 用完流后关闭流 |
2.2.1. 输出流写出方式1:
使用write(int b)方法,一次写出一个字节.
在C盘下创建a.txt文本文件
import java.io.FileOutputStream; import java.io.IOException;
public class IoTest2 { public static void main(String[] args) throws IOException { String path = "c:\\a.txt"; writeTxtFile(path); }
private static void writeTxtFile(String path) throws IOException { // 1:打开文件输出流,流的目的地是指定的文件 FileOutputStream fos = new FileOutputStream(path);
// 2:通过流向文件写数据 fos.write(‘j‘); fos.write(‘a‘); fos.write(‘v‘); fos.write(‘a‘); // 3:用完流后关闭流 fos.close();
} }
|
当c盘下的a.txt不存在会怎么样?
测试:将c盘下的a.txt文件删除,发现当文件不存在时,会自动创建一个,但是创建不了多级目录。
注意:使用write(int b)方法,虽然接收的是int类型参数,但是write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。
2.2.2. 输出流写出方式2:
使用write(byte[] b),就是使用缓冲.提高效率.
上述案例中的使用了OutputStram 的write方法,一次只能写一个字节。成功的向文件中写入了内容。但是并不高效,如和提高效率呢?是否应该使用缓冲,根据字节输入流的缓冲原理,是否可以将数据保存中字节数组中。通过操作字节数组来提高效率。查找API文档,在OutputStram类中找到了write(byte[] b)方法,将 b.length 个字节从指定的 byte 数组写入此输出流中。
如何将字节数据保存在字节数组中,以字符串为例,”hello , world” 如何转为字节数组。显然通过字符串的getBytes方法即可。
public class IoTest2 { public static void main(String[] args) throws IOException { String path = "c:\\a.txt"; writeTxtFile(path); }
private static void writeTxtFile(String path) throws IOException { // 1:打开文件输出流,流的目的地是指定的文件 FileOutputStream fos = new FileOutputStream(path);
// 2:通过流向文件写数据 byte[] byt = "java".getBytes(); fos.write(byt); // 3:用完流后关闭流 fos.close(); } }
|
仔细查看a.txt文本文件发现上述程序每运行一次,老的内容就会被覆盖掉。,那么如何不覆盖已有信息,能够往a.txt里追加信息呢。查看API文档,发现FileOutputStream类中的构造方法中有一个构造可以实现追加的功能FileOutputStream(File file, boolean append) 第二个参数,append - 如果为 true,则将字节写入文件末尾处,而不是写入文件开始处
private static void writeTxtFile(String path) throws IOException { // 1:打开文件输出流,流的目的地是指定的文件 FileOutputStream fos = new FileOutputStream(path,true);
// 2:通过流向文件写数据 byte[] byt = "java".getBytes(); fos.write(byt); // 3:用完流后关闭流 fos.close(); } |
2.3. 字节流文件拷贝
2.3.1. 字节输入输出流综合使用
通过字节输出流向文件中写入一些信息,并使用字节输入流把文件中的信息显示到控制台上。
public class IoTest3 { public static void main(String[] args) throws IOException { String path = "c:\\b.txt"; String content = "hello java";
writeFile(path, content);
readFile(path); }
public static void writeFile(String path, String content) throws IOException { // 打开文件输出流 FileOutputStream fos = new FileOutputStream(path); byte[] buffer = content.getBytes(); // 向文件中写入内容 fos.write(buffer); // 关闭流 fos.close();
}
public static void readFile(String path) throws IOException { FileInputStream fis = new FileInputStream(path); byte[] byt = new byte[1024]; int len = 0; while ((len = fis.read(byt)) != -1) { System.out.println(new String(byt, 0, len)); } // 关闭流 fos.close();
} }
|
注意输出流的细节:
这个输出流显然只适合小数据的写入,如果有大数据想要写入,我们的byte数组,该如何定义?
上述案例中我们将输入流和输出流进行和综合使用,如果尝试进输出流换成文本文件就可以实现文件的拷贝了.
什么是文件拷贝?很显然,先开一个输入流,将文件加载到流中,再开一个输出流,将流中数据写到文件中。就实现了文件的拷贝。
分析: 第一步:需要打开输入流和输出流 第二步:读取数据并写出数据 第三步:关闭流 public class IoTest3 {
public static void main(String[] args) throws IOException {
String srcPath = "c:\\a.txt"; String destPath = "d:\\a.txt"; copyFile(srcPath, destPath); }
public static void copyFile(String srcPath, String destPath) throws IOException {
}
} |
1.1.1. 字节流拷贝文件实现1
读一个字节写一个字节read 和write
public class IoTest3 {
public static void main(String[] args) throws IOException {
String srcPath = "c:\\a.txt"; String destPath = "d:\\a.txt"; copyFile(srcPath, destPath); }
public static void copyFile(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 读取和写入信息 int len = 0; while ((len = fis.read()) != -1) { fos.write(len); }
// 关闭流 fis.close(); fos.close(); }
} |
文本文件在计算机中是以二进制形式存在的,可以通过io流来拷贝,那么图片能不能拷贝呢?视频呢?音频呢?
public class IoTest3 {
public static void main(String[] args) throws IOException {
String srcPath = "c:\\秋.jpg"; String destPath = "d:\\秋.jpg"; copyFile(srcPath, destPath); }
public static void copyFile(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 读取和写入信息 int len = 0; while ((len = fis.read()) != -1) { fos.write(len); }
// 关闭流 fis.close(); fos.close(); }
} |
测试统统通过,所以字节流可以操作所有的文件。只是发现程序很慢,需要很长时间。特别是拷贝音频和视频文件时。
为什么?因为每次读一个字节再写一个字节效率很低。很显然这样效率低下的操作不是我们想要的。有没有更快更好的方法呢,是否可以使用缓冲区来提高程序的效率呢。
1.1.2. 字节流拷贝文件实现2;
使用字节数组作为缓冲区
public static void copyFile2(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 读取和写入信息 int len = 0;
// 使用字节数组,当做缓冲区 byte[] byt = new byte[1024]; while ((len = fis.read(byt)) != -1) { fos.write(byt); }
// 关闭流 fis.close(); fos.close(); }
|
问题1: 使用缓冲(字节数组)拷贝数据,拷贝后的文件大于源文件.
测试该方法,拷贝文本文件,仔细观察发现和源文件不太一致。
打开文件发现拷贝后的文件和拷贝前的源文件不同,拷贝后的文件要比源文件多一些内容问题就在于我们使用的容器,这个容器我们是重复使用的,新的数据会覆盖掉老的数据,显然最后一次读文件的时候,容器并没有装满,出现了新老数据并存的情况。
所以最后一次把容器中数据写入到文件中就出现了问题。
如何避免?使用FileOutputStream 的write(byte[] b, int off, int len)
b 是容器,off是从数组的什么位置开始,len是获取的个数,容器用了多少就写出多少。
public static void copyFile2(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 读取和写入信息 int len = 0;
// 使用字节数组,当做缓冲区 byte[] byt = new byte[1024]; while ((len = fis.read(byt)) != -1) { fos.write(byt, 0, len); }
// 关闭流 fis.close(); fos.close(); }
|
使用缓冲拷贝视频,可以根据拷贝的需求调整数组的大小,一般是1024的整数倍。发现使用缓冲后效率大大提高。
1.1. 字节流的异常处理
上述案例中所有的异常都只是进行了抛出处理,这样是不合理的。所以上述代码并不完善,因为异常没有处理。
当我们打开流,读和写,关闭流的时候都会出现异常,异常出现后,后面的代码都不会执行了。假设打开和关闭流出现了异常,那么显然close方法就不会再执行。那么会对程序有什么影响?
案例:
public class IoTest4 { public static void main(String[] args) throws IOException, InterruptedException { String path = "c:\\b.txt"; readFile(path); }
private static void readFile(String path) throws IOException, InterruptedException { FileInputStream fis = new FileInputStream(path); byte[] byt = new byte[1024]; int len = fis.read(byt); System.out.println(new String(byt, 0, len)); // 让程序睡眠,无法执行到close方法。 Thread.sleep(1000 * 10); fis.close(); } } |
在执行该程序的同时我们尝试去删除b.txt文件。如果在该程序没有睡醒的话,我们是无法删除b.txt 文件的。因为b.txt还被该程序占用着,这是很严重的问题,所以一定要关闭流。
目前我们是抛出处理,一旦出现了异常,close就没有执行,也就没有释放资源。那么为了保证close的执行该如何处理呢。
那么就需要使用try{} catch(){}finally{}语句。try中放入可能出现异常的语句,catch是捕获异常对象,fianlly是一定要执行的代码
public class IoTest4 { public static void main(String[] args) throws IOException, InterruptedException { String path = "c:\\b.txt"; readFile(path); }
private static void readFile(String path) { FileInputStream fis = null; try { fis = new FileInputStream(path); byte[] byt = new byte[1024]; int len = fis.read(byt); System.out.println(new String(byt, 0, len)); } catch (IOException e) { // 抛出运行时异常 throw new RuntimeException(e); } finally { // 把close方法放入finally中保证一定会执行 // 先判断是否空指针 if (fis != null) { try { fis.close(); } catch (Exception e) { throw new RuntimeException(e); }
}
}
} } |
文件拷贝的异常处理:
public static void copyFile(String srcPath, String destPath) {
FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(srcPath); fos = new FileOutputStream(destPath);
byte[] byt = new byte[1024 * 1024]; int len = 0; while ((len = fis.read(byt)) != -1) {
fos.write(byt, 0, len); } } catch (IOException e) { throw new RuntimeException(e); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { throw new RuntimeException(e); } } if (fos != null) { try { fos.close(); } catch (IOException e) { throw new RuntimeException(e); } }
}
} |
注意:
在最后的close代码中可能会有问题,两个close,如果第一个close方法出现了异常,并抛出了运行时异常,那么程序还是停止了。下面的close方法就没有执行到。
那么为了保证close的执行,将第二个放到fianlly中即可。
public static void copyFile(String srcPath, String destPath) {
FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(srcPath); fos = new FileOutputStream(destPath);
byte[] byt = new byte[1024 * 1024]; int len = 0; while ((len = fis.read(byt)) != -1) {
fos.write(byt, 0, len); } } catch (IOException e) { throw new RuntimeException(e); } finally {
try { if (fis != null) { fis.close(); } } catch (IOException e) { throw new RuntimeException(e); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { throw new RuntimeException(e); } }
} }
} |
1.2. 字节缓冲流
1.2.1. 缓冲流
上述程序中我们为了提高流的使用效率,自定义了字节数组,作为缓冲区.Java其实提供了专门的字节流缓冲来提高效率.
BufferedInputStream和BufferedOutputStream
BufferedOutputStream和BufferedOutputStream类可以通过减少读写次数来提高输入和输出的速度。它们内部有一个缓冲区,用来提高处理效率。查看API文档,发现可以指定缓冲区的大小。其实内部也是封装了字节数组。没有指定缓冲区大小,默认的字节是8192。
显然缓冲区输入流和缓冲区输出流要配合使用。首先缓冲区输入流会将读取到的数据读入缓冲区,当缓冲区满时,或者调用flush方法,缓冲输出流会将数据写出。
注意:当然使用缓冲流来进行提高效率时,对于小文件可能看不到性能的提升。但是文件稍微大一些的话,就可以看到实质的性能提升了。
public class IoTest5 { public static void main(String[] args) throws IOException { String srcPath = "c:\\a.mp3"; String destPath = "d:\\copy.mp3"; copyFile(srcPath, destPath); }
public static void copyFile(String srcPath, String destPath) throws IOException { // 打开输入流,输出流 FileInputStream fis = new FileInputStream(srcPath); FileOutputStream fos = new FileOutputStream(destPath);
// 使用缓冲流 BufferedInputStream bis = new BufferedInputStream(fis); BufferedOutputStream bos = new BufferedOutputStream(fos);
// 读取和写入信息 int len = 0;
while ((len = bis.read()) != -1) { bos.write(len); }
// 关闭流 bis.close(); bos.close(); }
}
|
2. 字符流
计算机并不区分二进制文件与文本文件。所有的文件都是以二进制形式来存储的,因此,从本质上说,所有的文件都是二进制文件。所以字符流是建立在字节流之上的,它能够提供字符层次的编码和解码。例如,在写入一个字符时,Java虚拟机会将字符转为文件指定的编码(默认是系统默认编码),在读取字符时,再将文件指定的编码转化为字符。
常见的码表如下:
ASCII: 美国标准信息交换码。用一个字节的7位可以表示。
ISO8859-1: 拉丁码表。欧洲码表,用一个字节的8位表示。又称Latin-1(拉丁编码)或“西欧语言”。ASCII码是包含的仅仅是英文字母,并且没有完全占满256个编码位置,所以它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入192个字母及符号,
藉以供使用变音符号的拉丁字母语言使用。从而支持德文,法文等。因而它依然是一个单字节编码,只是比ASCII更全面。
GB2312: 英文占一个字节,中文占两个字节.中国的中文编码表。
GBK: 中国的中文编码表升级,融合了更多的中文文字符号。
Unicode: 国际标准码规范,融合了多种文字。所有文字都用两个字节来表示,Java语言使用的就是unicode。
UTF-8: 最多用三个字节来表示一个字符。
(我们以后接触最多的是iso8859-1、gbk、utf-8)
查看上述码表后,很显然中文的‘中’在iso8859-1中是没有对映的编码的。或者一个字符在2中码表中对应的编码不同,例如有一些字在不同的编码中是有交集的,例如bjg5 和gbk 中的汉字简体和繁体可能是一样的,就是有交集,但是在各自码表中的数字不一样。
例如
使用gbk 将中文保存在计算机中,
中 国
对映 100 200 如果使用big5 打开
可能 ? ...
不同的编码对映的是不一样的。
很显然,我们使用什么样的编码写数据,就需要使用什么样的编码来对数据。
ISO8859-1:一个字节
GBK: 两个字节包含了英文字符和扩展的中文 ISO8859-1+中文字符
UTF-8 万国码,推行的。是1~3个字节不等长。英文存的是1个字节,中文存的是3个字节,是为了节省空间。
那么我们之前学习的流称之为字节流,以字节为单位进行操作之情的操作全是英文,如果想要操作中文呢?
测试:将指定位置的文件通过字节流读取到控制台
public class TestIo { public static void main(String[] args) throws IOException { String path = "c:\\a.txt"; writFileTest(); readFileByInputStream(path); }
private static void readFileByInputStream(String path) throws IOException { FileInputStream fis = new FileInputStream(path);
int len = 0; while ((len = fis.read()) != -1) { System.out.print((char) len); } }
private static void writFileTest() throws FileNotFoundException, IOException { // 创建文件对象 File file = new File("c:\\a.txt"); // 创建文件输出流 FileOutputStream fos = new FileOutputStream(file); fos.write("中国".getBytes()); fos.close(); } }
|
发现控制台输出的信息:
???ú 是这样的东西,打开a.txt 文本发现汉字”中国”确实写入成功。
那么说明使用字节流处理中文有问题。
仔细分析,我们的FileInputStream输入流的read() 一次是读一个字节的,返回的是一个int显然进行了自动类型提升。那么我们来验证一下“中国”对应的字节是什么
使用:"中国".getBytes() 即可得到字符串对应的字节数组。是[-42, -48, -71, -6]
同样,将read方法返回值直接强转为byte ,发现结果也是-42, -48, -71, -6 。
代码:
public class TestIo { public static void main(String[] args) throws IOException { String path = "c:\\a.txt"; writFileTest(); readFileByInputStream(path); //查看中国对应的编码 System.out.println(Arrays.toString("中国".getBytes())); }
private static void readFileByInputStream(String path) throws IOException { FileInputStream fis = new FileInputStream(path); int len = 0; while ((len = fis.read()) != -1) { System.out.println((byte)len); } }
private static void writFileTest() throws FileNotFoundException, IOException { // 创建文件对象 File file = new File("c:\\a.txt"); // 创建文件输出流 FileOutputStream fos = new FileOutputStream(file); fos.write("中国\r\n".getBytes()); fos.close(); }
}
|
那么中国 对应的是-42, -48, -71, -6是4个字节。 那就是一个中文占2个字节,(这个和编码是有关系的)
很显然,我们的中文就不能够再一个字节一个字节的读了。所以字节流处理字符信息时并不方便那么就出现了字符流。
字节流是 字符流是以字符为单位。
体验字符流:
public static void main(String[] args) throws IOException {
String path = "c:\\a.txt"; readFileByReader(path); } private static void readFileByReader(String path) throws IOException { FileReader fr = new FileReader(path); int len = 0; while ((len = fr.read()) != -1) { System.out.print((char) len); } } |
总结:字符流就是:字节流 + 编码表,为了更便于操作文字数据。字符流的抽象基类:
Reader , Writer。
由这些类派生出来的子类名称都是以其父类名作为子类名的后缀,如FileReader、FileWriter。
2.1. Reader
方法:
1,int read():
读取一个字符。返回的是读到的那个字符。如果读到流的末尾,返回-1.
2,int read(char[]):
将读到的字符存入指定的数组中,返回的是读到的字符个数,也就是往数组里装的元素的个数。如果读到流的末尾,返回-1.
3,close()
读取字符其实用的是window系统的功能,就希望使用完毕后,进行资源的释放
由于Reader也是抽象类,所以想要使用字符输入流需要使用Reader的实现类。查看API文档。找到了FileReader。
1,用于读取文本文件的流对象。
2,用于关联文本文件。
构造函数:在读取流对象初始化的时候,必须要指定一个被读取的文件。
如果该文件不存在会发生FileNotFoundException.
public class IoTest1_Reader {
public static void main(String[] args) throws Exception { String path = "c:/a.txt"; // readFileByInputStream(path); readFileByReader(path); }
/** * 使用字节流读取文件内容 * * @param path */ public static void readFileByInputStream(String path) throws Exception { InputStream in = new FileInputStream(path);
int len = 0; while ((len = in.read()) != -1) { System.out.print((char) len); }
in.close(); }
/** * 使用字符流读取文件内容 */ public static void readFileByReader(String path) throws Exception { Reader reader = new FileReader(path); int len = 0; while ((len = reader.read()) != -1) { System.out.print((char) len); }
reader.close(); }
} |
2.2. Writer
Writer中的常见的方法:
1,write(ch): 将一个字符写入到流中。
2,write(char[]): 将一个字符数组写入到流中。
3,write(String): 将一个字符串写入到流中。
4,flush():刷新流,将流中的数据刷新到目的地中,流还存在。
5,close():关闭资源:在关闭前会先调用flush(),刷新流中的数据去目的地。然流关闭。
发现基本方法和OutputStream 类似,有write方法,功能更多一些。可以接收字符串。
同样道理Writer是抽象类无法创建对象。查阅API文档,找到了Writer的子类FileWriter
1:将文本数据存储到一个文件中。
public class IoTest2_Writer {
public static void main(String[] args) throws Exception { String path = "c:/ab.txt";
writeToFile(path); }
/** * 写指定数据到指定文件中 * */ public static void writeToFile(String path) throws Exception { Writer writer = new FileWriter(path); writer.write(‘中‘); writer.write("世界".toCharArray()); writer.write("中国");
writer.close(); } } |
2:追加文件:
默认的FileWriter方法新值会覆盖旧值,想要实现追加功能需要
使用如下构造函数创建输出流 append值为true即可。
FileWriter(String fileName, boolean append)
FileWriter(File file, boolean append)
3:flush方法
如果使用字符输出流,没有调用close方法,会发生什么?
private static void writeFileByWriter(File file) throws IOException { FileWriter fw = new FileWriter(file); fw.write(‘新‘); fw.flush(); fw.write("中国".toCharArray()); fw.write("世界你好!!!".toCharArray()); fw.write("明天"); // 关闭流资源 //fw.close(); } |
程序执行完毕打开文件,发现没有内容写入.原来需要使用flush方法. 刷新该流的缓冲。
为什么只要指定claose方法就不用再flush方法,因为close也调用了flush方法.
2.3. 字符流拷贝文件
一个文本文件中有中文有英文字母,有数字。想要把这个文件拷贝到别的目录中。
我们可以使用字节流进行拷贝,使用字符流呢?肯定也是可以的。
2.3.1. 字符流拷贝文件实现1
public static void main(String[] args) throws Exception { String path1 = "c:/a.txt"; String path2 = "c:/b.txt";
copyFile(path1, path2); }
/** * 使用字符流拷贝文件 */ public static void copyFile(String path1, String path2) throws Exception { Reader reader = new FileReader(path1); Writer writer = new FileWriter(path2);
int ch = -1; while ((ch = reader.read()) != -1) { writer.write(ch); }
reader.close(); writer.close(); } |
但是这个一次读一个字符就写一个字符,效率不高。把读到的字符放到字符数组中,再一次性的写出。
2.3.2. 字符流拷贝文件实现2
public static void main(String[] args) throws Exception { String path1 = "c:/a.txt"; String path2 = "c:/b.txt";
copyFile(path1, path2); }
public static void copyFile3(String path1, String path2) throws Exception { Reader reader = new FileReader(path1); Writer writer = new FileWriter(path2);
int ch = -1; char [] arr=new char[1024]; while ((ch = reader.read(arr)) != -1) { writer.write(arr,0,ch); }
reader.close(); writer.close(); } |
字节流可以拷贝视频和音频等文件,那么字符流可以拷贝这些吗?
经过验证拷贝图片是不行的。发现丢失了信息,为什么呢?
计算机中的所有信息都是以二进制形式进行的存储(1010)图片中的也都是二进制
在读取文件的时候字符流自动对这些二进制按照码表进行了编码处理,但是图片本来就是二进制文件,不需要进行编码。有一些巧合在码表中有对应,就可以处理,并不是所有的二进制都可以找到对应的。信息就会丢失。所以字符流只能拷贝以字符为单位的文本文件
(以ASCII码为例是127个,并不是所有的二进制都可以找到对应的ASCII,有些对不上的,就会丢失信息。)
2.4. 字符流的异常处理
public static void main(String[] args) throws Exception { String path1 = "c:/a.txt"; String path2 = "c:/b.txt";
copyFile2(path1, path2); }
/** * 使用字符流拷贝文件,有完善的异常处理 */ public static void copyFile2(String path1, String path2) { Reader reader = null; Writer writer = null; try { // 打开流 reader = new FileReader(path1); writer = new FileWriter(path2);
// 进行拷贝 int ch = -1; while ((ch = reader.read()) != -1) { writer.write(ch); } } catch (Exception e) { throw new RuntimeException(e); } finally { // 关闭流,注意一定要能执行到close()方法,所以都要放到finally代码块中 try { if (reader != null) { reader.close(); } } catch (Exception e) { throw new RuntimeException(e); } finally { try { if (writer != null) { writer.close(); } } catch (Exception e) { throw new RuntimeException(e); } } } } |
2.5. 字符流的缓冲区
查看Reader 发现Reader,操作的是字符,我们就不需要进行编码解码操作,由字符流读到二进制,自动进行解码得到字符,写入字符自动编码成二进制.
Reader有一个子类BufferedReader。子类继承父类显然子类可以重写父类的方法,也可以增加自己的新方法。例如一次读一行就是常用的操作.那么BufferedReader 类就提供了这个方法,可以查看readLine()方法具备 一次读取一个文本行的功能。很显然,该子类可以对功能进行增强。
体验BufferedReader
public class IoTest_BufferedReader { public static void main(String[] args) throws IOException { readFile("c:\\a.txt"); }
private static void readFile(String path) throws IOException { Reader read = new FileReader(path);
BufferedReader br = new BufferedReader(read);
String line = null; while ((line = br.readLine()) != null) { System.out.println(line); }
} }
|
注意:
在使用缓冲区对象时,要明确,缓冲的存在是为了增强流的功能而存在,所以在建立缓冲区对象时,要先有流对象存在.
缓冲区的出现提高了对流的操作效率。原理:其实就是将数组进行封装。
使用字符流缓冲区拷贝文本文件.
public class Demo7 { public static void main(String[] args) throws IOException { // 关联源文件 File srcFile = new File("c:\\linux大纲.txt"); // 关联目标文件 File destFile = new File("d:\\linux大纲.txt"); // 实现拷贝 copyFile(srcFile, destFile);
}
private static void copyFile(File srcFile, File destFile) throws IOException { // 创建字符输入流 FileReader fr = new FileReader(srcFile); // 创建字符输出流 FileWriter fw = new FileWriter(destFile);
// 字符输入流的缓冲流 BufferedReader br = new BufferedReader(fr); // 字符输出流的缓冲流 BufferedWriter bw = new BufferedWriter(fw);
String line = null; // 一次读取一行 while ((line = br.readLine()) != null) { // 一次写出一行. bw.write(line); // 刷新缓冲 bw.flush(); // 进行换行,由于readLine方法默认没有换行.需要手动换行 bw.newLine(); } // 关闭流 br.close(); bw.close(); } }
|
2.6. 装饰器模式
需求:想要在读取的文件的每一行添加行号。
public class IoTest7_BufferedReader {
public static void main(String[] args) throws IOException { readFile("c:\\a.txt"); }
private static void readFile(String path) throws IOException { Reader read = new FileReader(path);
BufferedReader br = new BufferedReader(read); int count = 0; String line = null; while ((line = br.readLine()) != null) { count++; System.out.println(count+":"+line); }
} }
|
很容易的就可以实现。如果每次使用BufferedReader 输出时都需要显示行号呢? 每次都加? 很显然,我们的BufferedReader继承了Reader 对父类进行了功能的增强,那么我们也可以继承BufferedReader 重写该类的readLine方法,进行功能的增强.
public class IoTest_BufferedReader { public static void main(String[] args) throws IOException { readFile("c:\\a.txt"); }
private static void readFile(String path) throws IOException { Reader read = new FileReader(path);
BufferedReader br = new MyBufferedReader(read); String line = null; while ((line = br.readLine()) != null) { System.out.println(line); }
} }
class MyBufferedReader extends BufferedReader { public MyBufferedReader(Reader read) { super(read); }
int count;
@Override public String readLine() throws IOException { String line = super.readLine(); if (line != null) { count++; return count + ":" + line;
} else { return null; }
} } |
需求:
要在输出的一行前加上引号
可以再定义一个BufferedReader的子类,继承BufferedReader增强功能.
public class IoTest_BufferedReader { public static void main(String[] args) throws IOException { readFile("c:\\a.txt"); }
private static void readFile(String path) throws IOException { Reader read = new FileReader(path); BufferedReader br = new MyQutoBufferedReader(read); int count = 0; String line = null; while ((line = br.readLine()) != null) { System.out.println(line); count++; }
} }
// quotation 引号 class MyQutoBufferedReader extends BufferedReader {
public MyQutoBufferedReader(Reader reader) { super(reader); }
public String readLine() throws IOException { String line = super.readLine(); if (line != null) {
return "\"" + line + "\"";
} else { return null; }
} } |
需求三:
既想要显示行号又想要显示引号
发现,就需要再定义子类,发现这样比较麻烦,代码臃肿.而且代码重复.
可以换一种方式.如下:
其实就是一个新类要对原有类进行功能增强.
1. 在增强类中维护一个被增强的父类引用变量
2. 在增强类的构造函数中初始化1中的变量
3. 创建需要增强的方法,在刚方法中调用被被增强类的方法,并加以增强。
public class IoTest_BufferedReader { public static void main(String[] args) throws IOException { readFile("c:\\a.txt"); }
private static void readFile(String path) throws IOException { Reader read = new FileReader(path); BufferedReader bufferedReader = new BufferedReader(read); BufferedReader br = new MyQutoBufferedReader2(bufferedReader); br = new MyLineBufferedReader2(br); String line = null; while ((line = br.readLine()) != null) { System.out.println(line); } } }
// quotation 引号 class MyQutoBufferedReader2 extends BufferedReader { private BufferedReader bufferedReader;
public MyQutoBufferedReader2(BufferedReader bufferedReader) { super(bufferedReader); this.bufferedReader = bufferedReader; }
public String readLine() throws IOException { String line = super.readLine(); if (line != null) {
return "\"" + line + "\"";
} else { return null; }
} }
class MyLineBufferedReader2 extends BufferedReader { private BufferedReader bufferedReader;
public MyLineBufferedReader2(BufferedReader bufferedReader) { super(bufferedReader); this.bufferedReader = bufferedReader; }
int count;
@Override public String readLine() throws IOException { String line = super.readLine(); if (line != null) { count++; return count + ":" + line;
} else { return null; }
} } |
这就是装饰器模式
装饰器模式:
使用分层对象来动态透明的向单个对象中添加责任(功能)。
装饰器指定包装在最初的对象周围的所有对象都具有相同的基本接口。
某些对象是可装饰的,可以通过将其他类包装在这个可装饰对象的四周,来将功能分层。
装饰器必须具有和他所装饰的对象相同的接口。
JavaIO中的应用:
Java I/O类库需要多种不同的功能组合,所以使用了装饰器模式。
FilterXxx类是JavaIO提供的装饰器基类,即我们要想实现一个新的装饰器,就要继承这些类。
装饰器与继承:
问题:
修饰模式做的增强功能按照继承的特点也是可以实现的,为什么还要提出修饰设计模式呢?
继承实现的增强类和修饰模式实现的增强类有何区别?
继承实现的增强类:
优点:代码结构清晰,而且实现简单
缺点:对于每一个的需要增强的类都要创建具体的子类来帮助其增强,这样会导致继承体系过于庞大。
修饰模式实现的增强类:
优点:内部可以通过多态技术对多个需要增强的类进行增强
缺点:需要内部通过多态技术维护需要增强的类的实例。进而使得代码稍微复杂。
1. 其他流
1.1. 序列流
也称为合并流。
1.1.1. SequenceInputStream
序列流,对多个流进行合并。
SequenceInputStream 表示其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。
注意:
构造函数 SequenceInputStream(InputStream s1, InputStream s2) SequenceInputStream(InputStream s1, InputStream s2) |
合并两个流
使用构造函数SequenceInputStream(InputStream s1, InputStream s2)
private static void testSequenceInputStream() throws IOException { FileInputStream fis1 = new FileInputStream("c:\\a.txt"); FileInputStream fis2 = new FileInputStream("c:\\b.txt");
SequenceInputStream s1 = new SequenceInputStream(fis1, fis2); int len = 0; byte[] byt = new byte[1024];
FileOutputStream fos = new FileOutputStream("c:\\z.txt");
while ((len = s1.read(byt)) != -1) { fos.write(byt, 0, len); } s1.close(); } |
合并多个流:
public static void testSequenceInputStream() throws Exception { InputStream in1 = new FileInputStream("c:/a.txt"); InputStream in2 = new FileInputStream("c:/b.txt"); InputStream in3 = new FileInputStream("c:/c.txt");
LinkedHashSet<InputStream> set = new LinkedHashSet<InputStream>(); set.add(in1); set.add(in2); set.add(in3); final Iterator<InputStream> iter = set.iterator();
SequenceInputStream sin = new SequenceInputStream( new Enumeration<InputStream>() { @Override public boolean hasMoreElements() { return iter.hasNext(); }
@Override public InputStream nextElement() { return iter.next(); } });
FileOutputStream out = new FileOutputStream("c:/z.txt");
for (int b = -1; (b = sin.read()) != -1;) { out.write(b); } sin.close(); out.close(); } |
案例:将map3歌曲文件进行切割拷贝,并合并.
public class Demo2 { public static void main(String[] args) throws IOException {
split(new File("c:\\a.mp3"), 10, new File("c:\\")); System.out.println("切割完毕");
LinkedHashSet<InputStream> hs = new LinkedHashSet<InputStream>(); hs.add(new FileInputStream(new File("c:\\part.1.mp3"))); hs.add(new FileInputStream(new File("c:\\part.2.mp3"))); hs.add(new FileInputStream(new File("c:\\part.3.mp3"))); hs.add(new FileInputStream(new File("c:\\part.4.mp3"))); merage(hs, new File("c:\\merage.mp3")); System.out.println("合并完毕"); }
private static void merage(LinkedHashSet<InputStream> hs, File dest) throws IOException {
final Iterator<InputStream> it = hs.iterator(); FileOutputStream fos = new FileOutputStream(dest); SequenceInputStream seq = new SequenceInputStream( new Enumeration<InputStream>() {
@Override public boolean hasMoreElements() {
return it.hasNext(); }
@Override public InputStream nextElement() { return it.next(); } }); byte[] byt = new byte[1024 * 1024]; int len = 0; while ((len = seq.read(byt)) != -1) { fos.write(byt, 0, len); } seq.close(); fos.close(); }
// 1. 切割文件 /* * 切割文件,切割份数, 切割后保存路径 */ private static void split(File src, int count, File dir) throws IOException { FileInputStream fis = new FileInputStream(src); FileOutputStream fos = null; byte[] byt = new byte[1024 * 1024]; int len = 0; for (int i = 1; i <= count; i++) { len = fis.read(byt); if (len != -1) { fos = new FileOutputStream(dir + "part." + i + ".mp3"); fos.write(byt, 0, len); }
// fos.close();
} fis.close();
} } |
1.2. 对象的序列化
当创建对象时,程序运行时它就会存在,但是程序停止时,对象也就消失了.但是如果希望对象在程序不运行的情况下仍能存在并保存其信息,将会非常有用,对象将被重建并且拥有与程序上次运行时拥有的信息相同。可以使用对象的序列化。
对象的序列化: 将内存中的对象直接写入到文件设备中
对象的反序列化: 将文件设备中持久化的数据转换为内存对象
基本的序列化由两个方法产生:一个方法用于序列化对象并将它们写入一个流,另一个方法用于读取流并反序列化对象。
ObjectOutput writeObject(Object obj) 将对象写入底层存储或流。 ObjectInput readObject() 读取并返回对象。 |
1.2.1. ObjectOutputStream
1.2.2. ObjectInputStream
由于上述ObjectOutput和ObjectInput是接口,所以需要使用具体实现类。
ObjectOutput ObjectOutputStream被写入的对象必须实现一个接口:Serializable 否则会抛出:NotSerializableException ObjectInput ObjectInputStream 该方法抛出异常:ClassNotFountException |
ObjectOutputStream和ObjectInputStream 对象分别需要字节输出流和字节输入流对象来构建对象。也就是这两个流对象需要操作已有对象将对象进行本地持久化存储。
案例:
序列化和反序列化Cat对象。
public class Demo3 { public static void main(String[] args) throws IOException, ClassNotFoundException { Cat cat = new Cat("tom", 3); FileOutputStream fos = new FileOutputStream(new File("c:\\Cat.txt")); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(cat); System.out.println(cat); oos.close(); // 反序列化 FileInputStream fis = new FileInputStream(new File("c:\\Cat.txt")); ObjectInputStream ois = new ObjectInputStream(fis); Object readObject = ois.readObject(); Cat cat2 = (Cat) readObject; System.out.println(cat2); fis.close(); }
class Cat implements Serializable { public String name; public int age;
public Cat() {
}
public Cat(String name, int age) {
this.name = name; this.age = age; }
@Override public String toString() { return "Cat [name=" + name + ", age=" + age + "]"; }
} |
例子关键点:
- 声明Cat类实现了Serializable接口。是一个标示器,没有要实现的方法。
- 新建Cat对象。
- 新建字节流对象(FileOutputStream)进序列化对象保存在本地文件中。
- 新建ObjectOutputStream对象,调用writeObject方法序列化Cat对象。
- writeObject方法会执行两个工作:序列化对象,然后将序列化的对象写入文件中。
- 反序列化就是调用ObjectInputStream的readObject()方法。
- 异常处理和流的关闭动作要执行。
1.2.3. Serializable:
类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
所以需要被序列化的类必须是实现Serializable接口,该接口中没有描述任何的属性和方法,称之为标记接口。
如果对象没有实现接口Serializable,在进行序列化时会抛出:NotSerializableException 异常。
注意:
保存一个对象的真正含义是什么?如果对象的实例变量都是基本数据类型,那么就非常简单。但是如果实例变量是包含对象的引用,会怎么样?保存的会是什么?很显然在Java中保存引用变量的实际值没有任何意义,因为Java引用的值是通过JVM的单一实例的上下文中才有意义。通过序列化后,尝试在JVM的另一个实例中恢复对象,是没有用处的。
如下:
首先建立一个Dog对象,也建立了一个Collar对象。Dog中包含了一个Collar(项圈)
现在想要保存Dog对象,但是Dog中有一个Collar,意味着保存Dog时也应该保存Collar。假如Collar也包含了其他对象的引用,那么会发生什么?意味着保存一个Dog对象需要清楚的知道Dog对象的内部结构。会是一件很麻烦的事情。
Java的序列化机制可以解决该类问题,当序列化一个对象时,Java的序列化机制会负责保存对象的所有关联的对象(就是对象图),反序列化时,也会恢复所有的相关内容。本例中:如果序列化Dog会自动序列化Collar。但是,只有实现了Serializable接口的类才可以序列化。如果只是Dog实现了该接口,而Collar没有实现该接口。会发生什么?
Dog类和Collar类
import java.io.Serializable;
public class Dog implements Serializable { private Collar collar; private String name;
public Dog(Collar collar, String name) {
this.collar = collar; this.name = name; }
public Collar getCollar() { return collar; }
}
class Collar { private int size;
public int getSize() { return size; }
public Collar(int size) { this.size = size; }
} |
序列化
import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream;
public class Demo4 { public static void main(String[] args) throws IOException { Collar coll = new Collar(10); Dog dog = new Dog(coll, "旺财");
FileOutputStream fis = new FileOutputStream(new File("c:\\dog.txt")); ObjectOutputStream os = new ObjectOutputStream(fis); os.writeObject(dog); } }
|
执行程序,出现了运行时异常。
Exception in thread "main" java.io.NotSerializableException: Collar |
所以我们也必须将Dog中使用的Collar序列化。但是如果我们无法访问Collar的源代码,或者无法使Collar可序列化,如何处理?
两种解决方法:
一:继承Collar类,使子类可序列化
但是:如果Collar是final类,就无法继承了。并且,如果Collar引用了其他非序列化对象,也无法解决该问题。
transient
此时就可以使用transient修饰符,可以将Dog类中的成员变量标识为transient
那么在序列化Dog对象时,序列化就会跳过Collar。
public class Demo4 { public static void main(String[] args) throws IOException, ClassNotFoundException { Collar coll = new Collar(10); Dog dog = new Dog(coll, "旺财"); System.out.println(dog.getCollar().getSize());
FileOutputStream fis = new FileOutputStream(new File("c:\\dog.txt")); ObjectOutputStream os = new ObjectOutputStream(fis); os.writeObject(dog);
// 反序列化 FileInputStream fos = new FileInputStream(new File("c:\\dog.txt")); ObjectInputStream ois = new ObjectInputStream(fos); Object readObject = ois.readObject(); Dog dog2 = (Dog) readObject; // Collar未序列化。 dog2.getCollar().getSize(); } }
|
这样我们具有一个序列化的Dog和非序列化的Collar。
此时反序列化Dog后,访问Collar,就会出现运行时异常
10 Exception in thread "main" java.lang.NullPointerException |
注意:序列化不适用于静态变量,因为静态变量并不属于对象的实例变量的一部分。静态变量随着类的加载而加载,是类变量。由于序列化只适用于对象。
基本数据类型可以被序列化
public class Demo5 { public static void main(String[] args) throws IOException { // 创建序列化流对象 FileOutputStream fis = new FileOutputStream(new File("c:\\basic.txt")); ObjectOutputStream os = new ObjectOutputStream(fis); // 序列化基本数据类型 os.writeDouble(3.14); os.writeBoolean(true); os.writeInt(100); os.writeInt(200);
// 关闭流 os.close();
// 反序列化 FileInputStream fos = new FileInputStream(new File("c:\\basic.txt")); ObjectInputStream ois = new ObjectInputStream(fos);
System.out.println(ois.readDouble()); System.out.println(ois.readBoolean()); System.out.println(ois.readInt()); System.out.println(ois.readInt());
fos.close(); } } |
serialVersionUID
用于给类指定一个UID。该UID是通过类中的可序列化成员的数字签名运算出来的一个long型的值。
只要是这些成员没有变化,那么该值每次运算都一样。
该值用于判断被序列化的对象和类文件是否兼容。
如果被序列化的对象需要被不同的类版本所兼容。可以在类中自定义UID。
定义方式:static final long serialVersionUID = 42L;
1.3. Properties.
可以和流相关联的集合对象Properties.
Map
|--Hashtable
|--Properties
Properties:该集合不需要泛型,因为该集合中的键值对都是String类型。
1,存入键值对:setProperty(key,value);
2,获取指定键对应的值:value getProperty(key);
3,获取集合中所有键元素:
Enumeration propertyNames();
在jdk1.6版本给该类提供一个新的方法。
Set<String> stringPropertyNames();
4,列出该集合中的所有键值对,可以通过参数打印流指定列出到的目的地。
list(PrintStream);
list(PrintWriter);
例:list(System.out):将集合中的键值对打印到控制台。
list(new PrintStream("prop.txt")):将集合中的键值对存储到prop.txt文件中。
5,可以将流中的规则数据加载进行集合,并称为键值对。
load(InputStream):
jdk1.6版本。提供了新的方法。
load(Reader):
注意:流中的数据要是"键=值" 的规则数据。
6,可以将集合中的数据进行指定目的的存储。
store(OutputStram,String comment)方法。
jdk1.6版本。提供了新的方法。
store(Writer ,String comment):
使用该方法存储时,会带着当时存储的时间。
注意:
Properties只加载key=value这样的键值对,与文件名无关,注释使用#
练习:记录一个程序运行的次数,当满足指定次数时,该程序就不可以再继续运行了。
通常可用于软件使用次数的限定。
public static void sysPropList() throws IOException { Properties prop = System.getProperties();
// prop.list(System.out);// 目的是控制台。 // 需求是:将jvm的属性信息存储到一个文件中。 prop.list(new PrintStream("java.txt")); }
public static void sysProp() { Properties prop = System.getProperties();
Set<String> keys = prop.stringPropertyNames();
for (String key : keys) { System.out.println(key + ":" + prop.getProperty(key)); } } |
Properties类与配置文件
Map
|--Hashtable
|--Properties
注意:是一个Map集合,该集合中的键值对都是字符串。该集合通常用于对键值对形式的配置文件进行操作.
配置文件:将软件中可变的部分数据可以定义到一个文件中,方便以后更改,该文件称之为配置文件。
优势: 提高代码的维护性。
Properties: 该类是一个Map的子类,提供了可以快速操作配置文件的方法
load() : 将文件设备数据装载为Map集合数据
get(key): 获取Map中的数据
getProperty()获取Map中的数据特有方法
案例:
/* * 将配置文件中的数据通过流加载到集合中。 */ public static void loadFile() throws IOException { // 1,创建Properties(Map)对象 Properties prop = new Properties();
// 2.使用流加载配置文件。 FileInputStream fis = new FileInputStream("c:\\qq.txt");
// 3。使用Properties 对象的load方法将流中数据加载到集合中。 prop.load(fis);
// 遍历该集合 Set<Entry<Object, Object>> entrySet = prop.entrySet(); Iterator<Entry<Object, Object>> it = entrySet.iterator(); while (it.hasNext()) { Entry<Object, Object> next = it.next(); Object key = next.getKey(); Object value = next.getValue(); } // 通过键获取指定的值 Object object = prop.get("jack"); System.out.println(object);
// 通过键修改值 prop.setProperty("jack", "888888");
// 将集合中的数据写入到配置文件中。 FileOutputStream fos = new FileOutputStream("c:\\qq.txt");
// 注释: prop.store(fos, "yes,qq");
fos.close(); fis.close();
}
|
获取记录程序运行次数:
public class Demo6 { public static void main(String[] args) throws IOException { int count = 0; Properties pro = new Properties();
File file = new File("c:\\count.ini"); FileInputStream fis = null; if (!file.exists()) { file.createNewFile(); } fis = new FileInputStream(file); pro.load(fis); String str = pro.getProperty("count"); if (str != null) { count = Integer.parseInt(str); } if (count == 3) { System.out.println("使用次数已到,请付费"); System.exit(0); }
count++; System.out.println("欢迎使用本软件" + "你已经使用了:" + count + " 次");
pro.setProperty("count", count + ""); FileOutputStream fos = new FileOutputStream(new File("c:\\count.ini")); pro.store(fos, "请保护知识产权");
fis.close(); fos.close();
} }
|
1.4. 打印流
PrintStream可以接受文件和其他字节输出流,所以打印流是对普通字节输出流的增强,其中定义了很多的重载的print()和println(),方便输出各种类型的数据。
1.4.1. PrintStream
PrintWriter
1,打印流。
PrintStream:
是一个字节打印流,System.out对应的类型就是PrintStream。
它的构造函数可以接收三种数据类型的值。
1,字符串路径。
2,File对象。
3,OutputStream。
public static void main(String[] args) throws IOException { PrintStream ps = System.out;
// 普通write方法需要调用flush或者close方法才会在控制台显示 // ps.write(100); // ps.close();
// 不换行打印 ps.print(100); ps.print(‘a‘); ps.print(100.5); ps.print("世界"); ps.print(new Object()); System.out.println("--------------"); // 换行 ps.println(100); ps.println(‘a‘); ps.println(100.5); ps.println("世界"); ps.println(new Object());
// 重定向打印流 PrintStream ps2 = new PrintStream(new File("c:\\a.txt")); System.setOut(ps2); // 换行 ps2.println(100); ps2.println(‘a‘); ps2.println(100.5); ps2.println("世界"); ps2.println(new Object());
// printf(); 格式化 ps2.printf("%d,%f,%c,%s", 100, 3.14, ‘中‘, "世界你好!!!"); ps2.printf("%4s和%8s 打价格战", "京东", "苏宁");
} } |
注意: 打印流的三种方法
void print(数据类型 变量)
println(数据类型 变量)
printf(String format, Object... args)
可以自定数据格式
print 和println方法的区别在于,一个换行一个不换行
print 方法和write方法的却别在于,print提供自动刷新.
普通的write方法需要调用flush或者close方法才可以看到数据.
JDK1.5之后Java对PrintStream进行了扩展,增加了格式化输出方式,可以使用printf()重载方法直接格式化输出。但是在格式化输出的时候需要指定输出的数据类型格式。
1.4.2. PrintWriter
是一个字符打印流。构造函数可以接收四种类型的值。
1,字符串路径。
2,File对象。
对于1,2类型的数据,还可以指定编码表。也就是字符集。
3,OutputStream
4,Writer
对于3,4类型的数据,可以指定自动刷新。
注意:该自动刷新值为true时,只有三个方法可以用:println,printf,format.
如果想要既有自动刷新,又可执行编码。如何完成流对象的包装?
PrintWrter pw =
new PrintWriter(new OutputSteamWriter(new FileOutputStream("a.txt"),"utf-8"),true);
如果想要提高效率。还要使用打印方法。
PrintWrter pw =
newPrintWriter(new BufferdWriter(new OutputSteamWriter(
newFileOutputStream("a.txt"),"utf-8")),true);
public static void testPrintWriter() throws Exception { PrintWriter pw = new PrintWriter("c:/b.txt", "gbk");
// pw.append("xxx"); // pw.println(55); // pw.println(‘c‘); // pw.printf("%.1s与%4s打价格战, %c", "京东","苏宁", ‘a‘);
pw.close();
} |
Scanner
public static void testScanner() throws Exception { // Scanner scanner = new Scanner(new File("c:/test.txt")); Scanner scanner = new Scanner(System.in);
System.out.println(scanner.nextInt()); System.out.println(scanner.nextBoolean());
scanner.close(); } |
1.5. 操作数组的流对象
1.5.1. 操作字节数组
ByteArrayInputStream
以及ByteArrayOutputStream
toByteArray();
toString();
writeTo(OutputStream);
public static void testByteArrayInputStream() throws Exception { InputStream in = new ByteArrayInputStream(new byte[] { 65, 66, 67 }); ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int b = -1; (b = in.read()) != -1;) { out.write(b); }
in.close(); out.close();
System.out.println(Arrays.toString(out.toByteArray())); System.out.println(out); } |
1.5.2. 操作字符数组
CharArrayReader
CharArrayWriter
对于这些流,源是内存。目的也是内存。
而且这些流并未调用系统资源。使用的就是内存中的数组。
所以这些在使用的时候不需要close。
操作数组的读取流在构造时,必须要明确一个数据源。所以要传入相对应的数组。
对于操作数组的写入流,在构造函数可以使用空参数。因为它内置了一个可变长度数组作为缓冲区。
public static void testCharArrayReader() throws Exception { CharArrayReader reader = new CharArrayReader(new char[] { ‘A‘, ‘b‘, ‘c‘ }); CharArrayWriter writer = new CharArrayWriter();
for (int b = -1; (b = reader.read()) != -1;) { writer.write(b); }
reader.close(); writer.close();
System.out.println(writer.toCharArray()); } |
这几个流的出现其实就是通过流的读写思想在操作数组。
类似的对象同理:
StringReader
StringWriter。
public static void testStringReader() throws Exception { StringReader reader = new StringReader("test 中国"); StringWriter writer = new StringWriter();
for (int b = -1; (b = reader.read()) != -1;) { writer.write(b); }
reader.close(); writer.close();
System.out.println(writer.toString()); } |
1.6. 操作基本数据类型的流对象
1.6.1. DataInputStream
以及DataOutputStream
查看API文档DataInputStream的信息。发现从底层输入流中读取基本 Java 数据类型。查看方法,有读一个字节,读一个char读一个double 的方法,
DataInputStream 从数据流读取字节,并将它们转换为正确的基本数据类型值或字符串。
该流有操作基本数据类型的方法.
有读的,那么必定有对应的写的就是DataOutputStream 将基本类型的值或字符串转换为字节,并且将字节输出到数据流。
DataInputStream类继承FilterInputStream类,并实现了DataInput接口。DataOutputStream
类继承FilterOutputStream 并实现了DataOutput 接口。
例如:
DataInputStream 操作基本数据类型的方法: int readInt():一次读取四个字节,并将其转成int值。 boolean readBoolean():一次读取一个字节。 short readShort(); long readLong(); 剩下的数据类型一样。 String readUTF():按照utf-8修改版读取字符。注意,它只能读writeUTF()写入的字符数据。 DataOutputStream DataOutputStream(OutputStream): 操作基本数据类型的方法: writeInt(int):一次写入四个字节。 注意和write(int)不同。write(int)只将该整数的最低一个8位写入。剩余三个8位丢弃。 writeBoolean(boolean); writeShort(short); writeLong(long); 剩下是数据类型也也一样。 writeUTF(String):按照utf-8修改版将字符数据进行存储。只能通过readUTF读取。 |
测试: DataOutputStream
使用DataOutputStream写数据文件。
public static void testDataInputStream() throws Exception { DataOutputStream out = new DataOutputStream(new FileOutputStream( "c:/a.txt"));
out.writeBoolean(true); out.writeByte(15); // 0x05 1 个字节 out.writeBytes("abc"); // 0x 0041 2个字节 out.writeChar(‘X‘); // ?? out.writeChars("xyz"); out.writeLong(111); out.writeUTF("中国");
out.close();
DataInputStream in = new DataInputStream( new FileInputStream("c:/a.txt")); System.out.println(in.readBoolean()); System.out.println(in.readByte());
System.out.println(in.readByte()); System.out.println(in.readByte()); System.out.println(in.readByte());
System.out.println(in.readChar());
System.out.println(in.readChar()); System.out.println(in.readChar()); System.out.println(in.readChar());
System.out.println(in.readLong());
System.out.println(in.readUTF()); in.close(); } |
以上是关于java 关于流的使用的主要内容,如果未能解决你的问题,请参考以下文章
关于java中io流的关闭问题,部分代码,这种情况用不用关闭new FileReader(file)?