前言
本文以图文并茂的形式重点记录了这一周学习Java中IO操作的心得,并配以大量练习代码。Java的IO流无外乎就是输入流和输出流,所以基础部分还是比较简单的。
简述
IO:用于处理设备上的数据的技术。设备:内存、硬盘、光盘。java中所涉及的功能对象都存储到java.io包中。
流:系统资源,windows系统本身就可以操作设备。各种语言只是使用了系统平台上的这个资源。并对外提供了各种语言自己的操作功能,这些功能最终调用的是系统资源。使用完资源一定要记住:释放。(也就是说IO一定要写finally!)
IO流也进行分类:
1:输入流(读)和输出流(写)。
2:因为处理的数据不同,分为字节流和字符流。
输入流和输出流相对于内存设备而言。
将外设中的数据读取到内存中:输入-->读
将内存的数写入到外设中:输出-->写
注意:流的操作只有两种:读和写。
字节流:处理字节数据的流对象。设备上的数据无论是图片或者dvd,文字,它们都以二进制存储的。二进制的最终都是以一个8位为数据单元进行体现,所以计算机中的最小数据单元就是字节。意味着,字节流可以处理设备上的所有数据,所以字节流一样可以处理字符数据。
字符流:因为字符每个国家都不一样,所以涉及到了字符编码问题,那么GBK编码的中文用unicode编码解析是有问题的,所以需要获取中文字节数据的同时+ 指定的编码表才可以解析正确数据。为了方便于文字的解析,所以将字节流和编码表封装成对象,这个对象就是字符流。只要操作字符数据,优先考虑使用字符流体系。
流的体系因为功能不同,但是有共性内容,不断抽取,形成继承体系。该体系一共有四个基类,而且都是抽象类。
字节流:InputStream OutputStream
字符流:Reader Writer
在这四个系统中,它们的子类,都有一个共性特点:子类名后缀都是父类名,前缀名都是这个子类的功能名称。
File类
java.io.File类将文件系统中的文件和文件夹封装成了对象。提供了更多的属性和行为可以对这些文件和文件夹进行操作。这些是流对象办不到的,因为流只操作数据。设备上的数据(0、1串)最常见的存储表现形式是文件file。先学习一下文件的基本操作。
查阅API,描述文件或者文件夹(目录路径名)的类是File类。常用方法需要查表。
首先我们来演示一下File构造函数的使用。
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
-
-
-
-
-
-
-
-
- public class FileDemo {
-
-
-
-
- public static void main(String[] args) {
-
-
-
-
- File file = new File("d:\\1.txt");
-
-
- File file1 = new File("d:\\", "1.txt");
-
- File dir = new File("d:\\");
- File file2 = new File(dir, "1.txt");
-
-
-
-
-
- File f = new File("d:"+File.separator+"abc"+File.separator+"1.txt");
-
- System.out.println(f);
- }
- }
如果把某个文件视为一个对象,让你来描述这个文件,你认为,它应该具备什么样的功能或者方法?
File类常见方法:
1:创建。
boolean createNewFile():在指定目录下创建文件,如果该文件已存在,则不创建。而对操作文件的输出流而言,比如FileOutputStream,输出流对象已建立,就会创建文件,如果文件已存在,会覆盖。除非续写。
boolean mkdir():创建此抽象路径名指定的目录。
boolean mkdirs():创建多级目录。
2:删除。
boolean delete():删除此抽象路径名表示的文件或目录。
void deleteOnExit():在虚拟机退出时删除。
注意:在删除文件夹时,必须保证这个文件夹中没有任何内容,才可以将该文件夹用delete删除。
window的删除动作,是从里往外删。注意:java删除文件不走回收站。要慎用。
3:获取。
long length():获取文件大小。
String getName():返回由此抽象路径名表示的文件或目录的名称。
String getPath():将此抽象路径名转换为一个路径名字符串。
String getAbsolutePath():返回此抽象路径名的绝对路径名字符串。
String getParent():返回此抽象路径名父目录的抽象路径名,如果此路径名没有指定父目录,则返回 null。
long lastModified():返回此抽象路径名表示的文件最后一次被修改的时间。
File.pathSeparator:返回当前系统默认的路径分隔符,windows默认为 “;”。
File.Separator:返回当前系统默认的目录分隔符,windows默认为 “\”。
4:判断。
boolean exists():判断文件或者文件夹是否存在。
boolean isDirectory():测试此抽象路径名表示的文件是否是一个目录。
boolean isFile():测试此抽象路径名表示的文件是否是一个标准文件。
boolean isHidden():测试此抽象路径名指定的文件是否是一个隐藏文件。
boolean isAbsolute():测试此抽象路径名是否为绝对路径名。
5:重命名。
boolean renameTo(File dest):可以实现移动的效果。剪切+重命名。
String[] list():列出指定目录下的当前的文件和文件夹的名称。包含隐藏文件。如果调用list方法的File 对象中封装的是一个文件,那么list方法返回数组为null。如果封装的对象不存在也会返回null。只有封装的对象存在并且是文件夹时,这个方法才有效。
注意:list(FilenameFilter filter)和listFiles(FileFilter filter)往往是和过滤器实例参数一起使用。
代码示例1:
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
-
- public class FileMethodDemo {
-
- public static void main(String[] args) {
-
-
-
-
-
-
-
-
-
-
-
-
-
- File file = new File("d:\\abc\\1.txt");
- String file_name = file.getName();
- System.out.println(file_name);
-
- long len = file.length();
- System.out.println(len);
-
- System.out.println(file.getParent());
- }
- }
代码示例2:
代码示例3:
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
-
- public class FileMethodTest2 {
-
- public static void main(String[] args) {
-
-
-
-
-
- File dir = new File("d:\\");
-
- String[] names = dir.list();
-
-
- if(names != null){
- for (String name : names) {
- System.out.println(name);
- }
- }
- System.out.println("-------------------------");
-
- File[] files = dir.listFiles();
- for(File f : files){
- System.out.println(f.getName()+"......"+f.length());
- }
- }
- }
过滤器
接下来我要介绍的是文件名过滤器和文件过滤器,它们的用途是为了获取指定目录下的指定类型文件。所采用到的设计模式属于策略设计模式,目的就是为了降低容器和过滤条件之间的耦合性。
1.文件名过滤器:FilenameFilter
它的底层源码如下:
- public String[] list(FilenameFilter filter) {
- String names[] = list();
- if ((names == null) || (filter == null)) {
- return names;
- }
- List v = new ArrayList<>();
- for (int i = 0 ; i < names.length ; i++) {
- if (filter.accept(this, names[i])) {
- v.add(names[i]);
- }
- }
- return v.toArray(new String[v.size()]);
- }
下面实现一个判断文件是否是以.java格式结尾的过滤器:
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FilenameFilter;
-
-
-
- public class FilterBySuffix implements FilenameFilter {
-
- private String suffix;
-
- public FilterBySuffix(String suffix) {
- super();
- this.suffix = suffix;
- }
-
-
-
-
- @Override
- public boolean accept(File dir, String name) {
-
- return name.endsWith(suffix);
- }
- }
示例1:获取指定目录下的.java文件:
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
-
- import ustc.lichunchun.filter.FilterBySuffix;
-
- public class FilenameFilterDemo {
-
- public static void main(String[] args) {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- File dir = new File("d:\\");
-
- String[] names = dir.list(new FilterBySuffix(".java"));
- for(String name : names){
- System.out.println(name);
- }
- }
- }
我又实现了一个文件名包含指定字段的过滤器:
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FilenameFilter;
-
- public class FilterByContain implements FilenameFilter {
-
- private String content;
-
- public FilterByContain(String content) {
- super();
- this.content = content;
- }
-
- @Override
- public boolean accept(File dir, String name) {
-
- return name.contains(content);
- }
- }
示例2:获取指定目录下,文件名中包含指定字段的文件。
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
- import java.io.FilenameFilter;
-
- import ustc.lichunchun.filter.FilterByContain;
- import ustc.lichunchun.filter.FilterBySuffix;
-
- public class FilenameFilterDemo2 {
-
- public static void main(String[] args) {
-
-
- File dir = new File("d:\\");
-
- FilenameFilter filter = new FilterBySuffix(".java");
- filter = new FilterByContain("Demo");
-
- String[] names = dir.list(filter);
-
- for(String name : names){
- System.out.println(name);
- }
- }
- }
2.文件过滤器:FileFilter
文件过滤器其实更为常用。因为过滤器中pathname.getName().endsWith(".java")可以实现同样的文件名过滤操作。
我再用文件过滤器的方法,实现上面文件名过滤器所示例的,过滤指定类型文件的过滤器:
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FileFilter;
-
- public class FilterBySuffix2 implements FileFilter {
-
- private String suffix;
-
- public FilterBySuffix2(String suffix) {
- super();
- this.suffix = suffix;
- }
-
- @Override
- public boolean accept(File pathname) {
-
- return pathname.getName().endsWith(suffix);
- }
- }
为了更好地让读者理解,下面还实现了当前目录下,只过滤出文件和只过滤出文件夹的文件过滤器:
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FileFilter;
-
- public class FilterByFile implements FileFilter {
-
- @Override
- public boolean accept(File pathname) {
-
- return pathname.isFile();
- }
- }
- package ustc.lichunchun.filter;
-
- import java.io.File;
- import java.io.FileFilter;
-
- public class FilterByDirectory implements FileFilter {
-
- @Override
- public boolean accept(File pathname) {
-
- return pathname.isDirectory();
- }
- }
示例3:
- package ustc.lichunchun.file.demo;
-
- import java.io.File;
- import java.io.FileFilter;
-
- import ustc.lichunchun.filter.FilterByDirectory;
- import ustc.lichunchun.filter.FilterByFile;
- import ustc.lichunchun.filter.FilterBySuffix2;
-
- public class FileFilterDemo {
-
- public static void main(String[] args) {
-
- File dir = new File("d:\\");
-
- FileFilter filter = new FilterByFile();
- filter = new FilterByDirectory();
- filter = new FilterBySuffix2(".java");
-
- File[] files = dir.listFiles(filter);
-
- for(File file : files){
- System.out.println(file);
- }
- System.out.println("-------------------------");
- }
- }
练习:编写一个完整的Java Application程序。主类为NumberArray,要求NumberArray能够通过对接口NumberFilter的调用完成从一组整数中过滤出满足NumberArray使用者筛选条件的部分整数,设计中涉及到的类和接口的具体要求和含义如下:
(1)接口NumberFilter:表示过滤器接口。接口NumberFilter的抽象方法boolean is(int n)供使用者实现以设置筛选条件。
(2)类NumberArray:表示一组整数的对象(可用int型数组作为其成员来存放一组整数)。类NumberArray的方法print(0输出满足过滤器NumberFilter设置的筛选条件的部分整数。类NumberArray的方法setFilter(NumberFilter nf)用于设置过滤器。
(3)类Filter:表示具体的过滤器,实现接口NumberFilter,实现is()方法用于设置筛选条件,这里要求过滤出偶数。
- package ustc.lichunchun.filter;
- public class Test {
- public static void main(String[] args) {
- int[] array = {1,2,3,4,5,6,7,8,9};
- NumberFilter nf = new Filter();
- NumberArray na = new NumberArray(array, nf);
- na.print();
- }
- }
- class NumberArray {
- private int[] array;
- public NumberArray(int[] array, NumberFilter nf) {
- super();
- this.array = array;
- this.nf = nf;
- }
- private NumberFilter nf;
- public void print(){
- for(int i : array){
- if(nf.is(i)){
- System.out.print(i+ " ");
- }
- }
- System.out.println();
- }
- public void setFilter(NumberFilter nf){
- this.nf = nf;
- }
- }
- class Filter implements NumberFilter {
- @Override
- public boolean is(int n) {
- if(n % 2 == 0)
- return true;
- return false;
- }
- }
- interface NumberFilter {
- boolean is(int n);
- }
递归及其IO应用
说完了上面的过滤器以后,我们已经可以实现过滤出指定目录下的特定类型文件了。但是现在我又有一个需求:遍历指定目录下的内容(包含子目录中的内容)。这该如何处理呢?再解决这个问题之前,我们下来简单回顾一下算法课中所学的递归思想。
递归:就是函数自身调用自身。
什么时候用递归呢?当一个功能被重复使用,而每一次使用该功能时的参数不确定,都由上次的功能元素结果来确定。简单说:功能内部又用到该功能,但是传递的参数值不确定。(每次功能参与运算的未知内容不确定)。
递归的注意事项:
1:一定要定义递归的条件。
2:递归的次数不要过多。容易出现 StackOverflowError 栈内存溢出错误。
其实递归就是在栈内存中不断的加载同一个函数。
递归示例:
- package ustc.lichunchun.recursion;
-
- public class RecursionDemo {
-
- public static void main(String[] args) {
-
-
-
-
-
-
-
- int sum = getSum(3);
- System.out.println("sum = "+sum);
-
- int sum1 = getSum(999999);
- }
-
- public static int getSum(int num){
- if(num == 1)
- return 1;
- return num + getSum(num - 1);
- }
-
-
-
-
-
-
-
-
-
- }
IO递归示例1:遍历指定目录下的内容,要求包含子目录的内容。
- package ustc.lichunchun.file.test;
-
- import java.io.File;
-
- public class GetAllFilesTest {
-
- public static void main(String[] args) {
-
-
-
-
-
-
-
-
-
- File dir = new File("d:\\JavaSE_code");
- getAllFiles(dir);
-
- }
- public static void getAllFiles(File dir){
-
- System.out.println("dir: "+dir);
-
-
- File[] files = dir.listFiles();
-
-
- if(files != null){
- for(File file : files){
- if(file.isDirectory()){
- getAllFiles(file);
- }else{
- System.out.println("file: "+file);
- }
- }
- }
- }
- }
IO递归示例2:删除一个带内容的文件夹。
- package ustc.lichunchun.file.test;
-
- import java.io.File;
-
- public class DeleteDirTest {
-
- public static void main(String[] args) {
-
-
-
-
-
- File dir = new File("d:\\JavaSE_code");
-
-
-
- deleteDir(dir);
- }
- public static void deleteDir(File dir){
-
-
- File[] files = dir.listFiles();
-
-
- for(File file : files){
-
-
- if(file.isDirectory()){
- deleteDir(file);
- }else{
- System.out.println(file + ":" +file.delete());
- }
- }
-
- System.out.println(dir + ":" +dir.delete());
- }
- }
File类综合练习
获取一个想要的指定文件的集合。获取JavaSE_code下(包含子目录)的所有的.java的文件对象。并存储到集合中。
思路:
1.既然包含子目录,就需要递归。
2.在递归的过程中,需要过滤器。
3.凡是满足条件的,都添加到集合中。
- package ustc.lichunchun.test;
-
- import java.io.File;
- import java.io.FileFilter;
- import java.util.ArrayList;
- import java.util.List;
-
- import ustc.lichunchun.filter.FilterBySuffix2;
-
- public class Test {
-
- public static void main(String[] args) {
-
-
-
-
-
-
-
-
-
- File dir = new File("e:\\JavaSE_code");
- List list = fileList(dir, ".java");
- for(File file : list){
- System.out.println(file);
- }
- }
-
-
-
- public static List fileList(File dir, String suffix){
-
-
- List list = new ArrayList();
-
-
- FileFilter filter = new FilterBySuffix2(suffix);
-
-
-
-
-
-
-
-
-
- getFileList(dir, list, filter);
- return list;
- }
-
-
-
-
-
-
-
-
-
-
- public static void getFileList(File dir, List list, FileFilter filter){
-
-
- File[] files = dir.listFiles();
-
-
- for(File file : files){
-
-
- if (file.isDirectory()){
- getFileList(file, list, filter);
- }else{
-
- if(filter.accept(file)){
- list.add(file);
- }
- }
- }
- }
- }
其中所用到的文件过滤器上面有,这里就不单独再列出来了。
字节流
File文件对象只能操作文件或者文件夹的属性,例如文件或文件夹的创建、删除、获取文件属性(大小、所在目录等),我们最终建立文件的目的,是往文件里面存数据,File对象是做不了这个的。这时,我们就要用到IO流。
InputStream:是表示字节输入流的所有类的超类。
|--FileInputStream:从文件系统中的某个文件中获得输入字节。哪些文件可用取决于主机环境。
用于读取诸如图像数据之类的原始字节流。要读取字符流,请考虑使用 FileReader。
|--FilterInputStream:包含其他一些输入流,它将这些流用作其基本数据源,它可以直接传输数据或提供一些额外的功能。
|--BufferedInputStream:该类实现缓冲的输入流。
|--DataInputStreamStream:操作基本数据类型值的流。
|--ObjectInputStream:对象的序列化。
|--PipedInputStream:管道输出流是管道的发送端。
|--SequenceInputStream:序列流。
|--ByteArrayInputStream:操作内存数组。关闭动作无效。
|--System.in:键盘录入。
OutputStream:此抽象类是表示输出字节流的所有类的超类。
|--FileoutputStream:文件输出流是用于将数据写入File或FileDescriptor的输出流。
注意处理IO异常。续写和换行。
|--FilterOutputStream:此类是过滤输出流的所有类的超类。
|--BufferedOutputStream:该类实现缓冲的输出流。
|--PrintStream:字节打印流,保证数值的表现形式不变,实现自动刷新和换行。
|--DataOutputStream:操作基本数据类型值的流。
|--ObjectOutputStream:对象的反序列化。
|--PipedOutputStream:管道输入流应该连接到管道输出流。
|--ByteArrayOutputStream:操作内存数组。关闭动作无效。
|--System.out:控制台打印到屏幕上。
FileOutputStream
将数据写入到文件中,使用字节输出流:FileOutputStream。
在演示字节输出流之前,有以下三点需要注意:
1.输出流所关联的目的地,如果不存在,会自动创建。如果存在,则替换并覆盖。(这与File对象,如果存在、创建失败有所区别)
2.底层流资源使用完以后一定要记得释放资源。也即IO一定要写finally。
3.一定要在释放资源前先判断输出流对象是否为空。因为try中创建输出流对象失败,则fos依然是null,但是空指针没法调用close()函数释放资源,这回导致抛出NullPointerException异常。
下面演示一下创建字节输出流对象、调用输出流的写功能的代码。
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class FileOutputStreamDemo {
-
- public static void main(String[] args) throws IOException {
-
-
-
-
-
-
- File dir = new File("tempfile");
- if(!dir.exists()){
- dir.mkdir();
- }
-
-
-
- FileOutputStream fos = new FileOutputStream("tempfile\\fos.txt");
-
-
-
-
- fos.write("abcde".getBytes());
-
-
- fos.close();
- }
- }
接着,我们演示一下带处理IO异常的规范写法:
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class IOExceptionDemo {
-
-
-
- public static void main(String[] args) {
-
- File dir = new File("tempfile");
- if(!dir.exists()){
- dir.mkdir();
- }
-
- FileOutputStream fos = null;
-
- try {
- fos = new FileOutputStream("tempfile\\fos.txt");
-
- fos.write("abcdefg".getBytes());
-
- } catch (IOException e) {
- System.out.println(e.toString() + "---");
-
- } finally {
- if (fos != null) {
- try {
- fos.close();
- } catch (IOException e) {
-
- throw new RuntimeException("关闭失败" + e);
- }
- }
- }
- }
- }
需求:实现续写和换行操作。注意:行分隔符可以通过System.getProperty("line.separator");获取。
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class NewlineDemo {
-
- private static final String LINE_SEPARATOR = System.getProperty("line.separator");
-
- public static void main(String[] args) {
-
-
-
-
-
-
-
-
- File dir = new File("tempfile");
- if(!dir.exists()){
- dir.mkdir();
- }
-
- FileOutputStream fos = null;
- try{
- fos = new FileOutputStream("tempfile\\fos.txt",true);
- String str = LINE_SEPARATOR + "abc";
- fos.write(str.getBytes());
- }catch(IOException e){
- System.out.println(e.toString()+"--");
- }finally{
- if(fos != null){
- try{
- fos.close();
- }catch(IOException e){
- throw new RuntimeException(""+e);
- }
- }
- }
- }
- }
FileInputStream
那如何将已有文件的数据读取出来呢?既然是读,使用InputStream,而且是要操作文件,FileInpuStream。
为了确保文件一定在读之前是存在的,可以先将字符串路径封装成File对象。
下面演示创建文件字节读取流对象、逐个读取并打印文本文件中的字节。
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
-
- public class FileInputStreamDemo {
-
- public static void main(String[] args) throws IOException {
-
-
-
-
-
-
-
-
- File file = new File("tempfile\\fos.txt");
- if(!file.exists()){
- throw new RuntimeException("要读取的文件不存在");
- }
-
-
- FileInputStream fis = new FileInputStream(file);
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- int by = 0;
- while((by = fis.read()) != -1){
- System.out.println(by);
- }
-
-
- fis.close();
- }
- }
但是你肯定会觉得说,这样一个一个字节的读取文件中的字节,那得有多慢啊?
所以我建议使用下面的这第二种字节流读取方式:创建一个缓冲区字节数组,大小自定义,
然后调用FileInputStream的read(byte[])方法,这样一来,效率会提升不少。
建议这里介绍的三种方法中,选择此种方法。当然,最好是用BufferedInputStream,这我稍后便会阐述。
- package ustc.lichunchun.bytestream;
-
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
-
- public class FileInputStreamDemo2 {
-
- private static final int DEFAULT_SIZE = 1024*1024*2;
-
- public static void main(String[] args) {
-
-
- File file = new File("tempfile\\fos.txt");
- if(!file.exists()){
- throw new RuntimeException("要读取的文件不存在");
- }
-
- FileInputStream fis = null;
- try{
- fis = new FileInputStream(file);
-
-
- byte[] buf = new byte[DEFAULT_SIZE];
-
-
-
-
-
-
-
-
-
-
-
- int len = 0;
- while((len = fis.read(buf)) != -1){
- System.out.println(new String(buf,0,len));
- }
- }catch(IOException e){
-
- }finally{
- if(fis != null){
- try{
- fis.close();
- }catch(IOException e){
-
- }
- }
- }
- }
- }
当然了,其实还有一种比较蠢方式,就是把自定义缓冲区的大小设置为和文件本身大小一样大,示例如下。不过我非常不建议用这种方法。
- package ustc.lichunchun.bytestream;
-
- import java.io.FileInputStream;
- import java.io.IOException;
-
- public class FileInputStreamDemo3 {
-
- public static void main(String[] args) throws IOException {
-
- FileInputStream fis = new FileInputStream("tempfile\\fos.txt");
-
- System.out.println(fis.available());
-
- byte[] buf = new byte[fis.available()];
- fis.read(buf);
- String s = new String(buf);
- System.out.println(s);
-
- fis.close();
- }
- }
需求:复制一个文件。(文件类型不限,可以是文本数据,也可以是媒体数据)
思路:
读取源数据,将数据写到目的中。用到了流,操作设备上的数据。
读,用到输入流;写,用到输出流。而且操作的还是文件。需要用到字节流中操作文件的流对象。
- package ustc.lichunchun.copy;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class CopyTextTest {
-
- public static void main(String[] args) throws IOException {
-
-
-
-
-
-
-
-
-
- copyText();
- }
-
- public static void copyText() throws IOException {
-
-
- FileInputStream fis = new FileInputStream("复制文本文件图解.bmp");
-
-
- FileOutputStream fos = new FileOutputStream("tempfile\\io_copy.bmp");
-
-
- int by = 0;
- while((by = fis.read()) != -1){
- fos.write(by);
- }
- fos.close();
- fis.close();
- }
- }
很显然,上面这种逐字节读取文件的方式效率太低,所以我们考虑使用在FileInputStream中所讲的第二种方法,自定义缓冲区。
- package ustc.lichunchun.copy;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class CopyTextByBufTest {
-
- public static void main(String[] args) {
- copyTextByBuf();
- }
-
- public static void copyTextByBuf() {
- FileInputStream fis = null;
- FileOutputStream fos = null;
- try{
- fis = new FileInputStream("tempfile\\fos.txt");
- fos = new FileOutputStream("tempfile\\copy_fos.txt");
-
- byte[] buf = new byte[1024];
-
- int len = 0;
-
- while((len = fis.read(buf)) != -1){
- fos.write(buf, 0, len);
- }
- }catch(IOException e){
-
- }finally{
- if(fos != null){
- try {
- fos.close();
- } catch (IOException e) {
-
- }
- }
- if(fis != null){
- try {
- fis.close();
- } catch (IOException e) {
-
- }
- }
- }
- }
- }
BufferedInputStream、BufferedOutputStream
前面在FileInputStream小节中,我已经介绍了利用自定义缓冲区实现高效的字节流读取。
java也考虑到了这一点,在底层将缓冲区封装成了对象,实际上就是在一个类中封装了数组,
对流所操作的数据进行缓存。缓冲区的作用就是为了提高操作数据的效率。
这样可以避免频繁的在硬盘上寻道操作。缓冲区创建时,必须有被缓冲的流对象与之相关联。
原理图解:
需求:利用缓冲区完成复制图片的例子。
- package ustc.lichunchun.bytestream.buffer;
-
- import java.io.BufferedInputStream;
- import java.io.BufferedOutputStream;
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class CopyPicByBufferDemo {
-
- public static void main(String[] args) throws IOException {
-
-
-
-
-
-
-
-
- copyPicByBuffer();
- }
-
- public static void copyPicByBuffer() throws IOException {
-
-
-
- FileInputStream fis = new FileInputStream("tempfile\\1.jpg");
-
-
-
- BufferedInputStream bufis = new BufferedInputStream(fis);
-
- FileOutputStream fos = new FileOutputStream("tempfile\\copy_1.jpg");
-
- BufferedOutputStream bufos = new BufferedOutputStream(fos);
-
- byte[] buf = new byte[1024];
-
- int len = 0;
-
- while((len = bufis.read(buf)) != -1){
- bufos.write(buf, 0, len);
- bufos.flush();
- }
-
-
- bufos.close();
- bufis.close();
- }
- }
问题探讨:用字节流操作中文数据。
在计算机中,无论是文本,还是图片、mp3、视频等,所有数据最终都是以字节形式存在。
字节流:能操作以字节为单位的文件的流对象。所以字节流能操作计算机上的一切数据。
- package ustc.lichunchun.readcn;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
-
- public class ReadCNDemo {
-
- public static void main(String[] args) throws IOException {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- readCNText();
- }
-
- public static void writeCNText() throws IOException {
- FileOutputStream fos = new FileOutputStream("tempfile\\cn.txt");
- fos.write("你A好".getBytes());
-
- fos.close();
- }
-
- public static void readCNText() throws IOException {
- FileInputStream fis = new FileInputStream("tempfile\\cn.txt");
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- byte[] buf = new byte[1024];
- int len = 0;
- len = fis.read(buf);
- String s = new String(buf,0,len);
- System.out.println(s);
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- fis.close();
- }
- }
正如上面这段代码最后一段的注释中所说,对于纯文本数据的文件,用字节流操作中英文数据时由于编码问题,不同语言的字符对应的字节数不同,会显得比较繁琐。这时我们就该考虑字符流了。但是在这之前,我先把调研到的编码相关背景知识总结分享一下。
编码表
在计算机中,无论是文本,还是图片、mp3、视频等,所有数据最终都是以字节形式存在。字节流:能操作以字节为单位的文件的流对象。所以字节流能操作计算机上的一切数据。文本数据、媒体数据等,什么数据字节流都可以搞定。而字符流只能操作以字符为单位的文本数据。
主要的编码表有:ASCII、ISO8859-1、GB2312、GBK、GB18030、Unicode、UTF-8(标识头信息)等。如果想要获取到当前系统使用的默认编码表,可以使用如下代码获取系统平台默认字符集:System.getProperty("file.encoding")--> GBK。
ASCII:美国标准信息交换码,0打头,用一个字节的后7位表示,读一个字节。
ISO8859-1:欧洲码表,用一个字节的8位表示,1打头代表拉丁文,读一个字节。
GB2312:中文编码表,两个字节都是1打头,读两个字节
GBK:扩展版中文编码表,第一个字节1打头,读两个字节10101011 01010110 打头都是1,读到1打头,就读两个字节,再去查GBK表。
Unicode:国际标准码,重新编排码表,统一规定所有文字都是两个字节,Java语言使用的就是unicode。弊端:能用一个字节表示的,都用两个字节装,浪费空间。
UTF-8:对Unicode优化,能用一个字节表示的就不用两个字节,最多用三个字节来表示一个字符。读完相应若干字节后,再去查表。01111010 单字节字符,0打头,读完单字节就去查表;11001010 10101010 两个字节字符,110打头第一个字节,10打头第二个字节,读完两个字节再去查表;11100101 10101001 10010001三个字节字符,1110打头第一个字节,10打头第二、三个字节,读完三个字节再去查表。
字符流为什么不能复制图片?
字符流读完字节数据后,并没有直接往目的地里面写,而是去查表了。但是读到的媒体性字节数据,都有自身千变万化的编码方式,在码表里没有对应内容。那么它就会在表的未知字符区域随便找一些数据写到目的地中,这时,目的数据和源数据就不一致,就不能图片编辑器解析为图片。所以,字符流不能用来操作媒体文件。
编解码详述
编码:字符串 --> 字节数组。(默认都是按照windows系统本地的编码GBK存储的)
解码:字节数组 --> 字符串。
代码示例:
上述代码示例中的test2()函数,我们有如下的处理过程:
这种原理在实际中,也有应用,比如tomcat服务器端,就是有这样的转换:
练习:按照字节数截取一个字符串。"abc你好"如果截取到半个中文,舍弃。比如截取4字节,abc,截取5个字节abc你。定义功能实现。(友情提示:GB2312编码的一个中文是两个字节,而且两个字节的最高位都是1,也就是说是一个负数。)
思路:
1.提示告诉我中文两个字节都是负数。
2.判断截取的最后一个字节是否是负数。如果不是,直接截取;如果是,就往回判断,前一个是否是负数。并记录住负数的个数。如果连续的负数有奇数个,舍弃最后一个字节;如果连续的负数是偶数个,不舍弃。
- package ustc.lichunchun.encode;
-
- import java.io.IOException;
-
- public class Test {
-
- public static void main(String[] args) throws IOException {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- String str = "a琲bc你好d琲e";
- byte[] buf = str.getBytes("utf-8");
-
- for(int i = 1; i <= buf.length; i++){
- String temp = cutStringByCount2(str,i);
- System.out.println("截取"+i+"个字节是:"+temp);
- }
- }
-
- public static String cutStringByCount1(String str, int len) throws IOException {
-
-
- byte[] bytes = str.getBytes("gbk");
-
- int count = 0;
-
- for(int x = len-1; x >= 0; x--){
-
- if(bytes[x]<0){
- count++;
- }else{
- break;
- }
- }
-
-
- if(count%2 == 0)
- return new String(bytes, 0, len);
- else
- return new String(bytes, 0, len-1);
- }
-
- public static String cutStringByCount2(String str, int len) throws IOException {
- byte[] bytes = str.getBytes("utf-8");
- int count = 0;
- for(int x = len-1; x >= 0; x--){
- if(bytes[x] < 0){
- count++;
- }else{
- break;
- }
- }
- if(count%3 ==0)
- return new String(bytes, 0, len, "utf-8");
- else if(count%3 == 1)
- return new String(bytes, 0, len-1, "utf-8");
- else
- return new String(bytes, 0, len-2, "utf-8");
- }
- }
字符流
字符流只能用来操作字符数据--文本。但是由于字符流封装了编码部分,所以操作字符比较方便。字符流=字节流+编码表。
Reader:用于读取字符流的抽象类。子类必须实现的方法只有 read(char[],int,int)和 close()。
|--BufferedReader:从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。
|--LineNumberReader:跟踪行号的缓冲字符输入流。此类定义了方法 setLineNumber(int)和 getLineNumber(),
它们可分别用于设置和获取当前行号。
|--InputStreamReader:是字节流通向字符流的桥梁:它使用指定的 charset读取字节并将其解码为字符。
它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。
|--FileReader:用来读取字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是适当的。
要自己指定这些值,可以先在 FileInputStream上构造一个InputStreamReader。
|--CharArrayReader:没有调用系统流,只是操作内存中的数组。
|--StringReader:没有调用系统流,只是操作内存中的数组。
Writer:写入字符流的抽象类。子类必须实现的方法仅有 write(char[],int,int)、flush()和 close()。
|--BufferedWriter:将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
|--OutputStreamWriter:是字符流通向字节流的桥梁:可使用指定的 charset将要写入流中的字符编码成字节。
它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。
|--FileWriter:用来写入字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是可接受的。
要自己指定这些值,可以先在 FileOutputStream上构造一个OutputStreamWriter。
|--PrintWriter:字符打印流,保证数值的表现形式不变,可以对字节流、字符流自动刷新并换行。
|--CharArrayWriter:没有调用系统流,只是操作内存中的数组。
|--StringWriter:没有调用系统流,只是操作内存中的数组。
注意:只要是输出字符流,都有查表后的缓冲区。所以它的write()方法后面一定要跟随flush()刷新操作!(即使没写但运行通过,那也是因为close()封装了flush方法)
转换流
通过编码表的演示,读者应该很容易发现针对纯文本文件,用字符流的方式处理更为方便。那就很自然的考虑到,我如何才能将字节流转换成字符流呢?下面我就来介绍一下字节和字符,二者之间转换的桥梁——转换流。
转换流:
字节-->字符:解码(看不懂的-->看得懂的):InputStreamReader-->字节通向字符的桥梁,将读到的若干字节进行解码。
字符-->字节:编码(看得懂的-->看不懂的):OutputStreamWriter-->字符通向字节的桥梁,将字符编码成若干字节。
记住:只要是字符输出流,都有查表后的缓冲区。所以它的write()方法后面一定要跟随flush()刷新操作!
缓冲的原因:
每次调用 write方法都会导致在给定字符(或字符集)上调用编码转换器,在写入底层输出流之前,得到的这些字节将在缓冲区中累积。中文最终变成字节才能出去,中文变成什么字节呢?识别中文的码表不止一个,没有指定,会按默认码表GBK。这意味着,会把中文先临时存储起来,去查相应的码表,再把查码表得到的字节存起来缓冲一下,再刷新写出去。之前学的字节流,是因为不用进行查码表、缓冲转换,字节该什么样就什么样,直接读写即可。现在需要拿着字符数据去查码表,把查到的字节数据存起来,再写出去。所以,有了缓冲区,就得做刷新。
转换流代码示例:
- package ustc.lichunchun.charstream;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.OutputStreamWriter;
-
- public class TransStreamDemo {
-
- public static void main(String[] args) throws IOException {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- writeCNText();
- }
-
- public static void readCnText() throws IOException {
-
-
- FileInputStream fis = new FileInputStream("tempfile\\cn.txt");
-
-
- InputStreamReader isr = new InputStreamReader(fis);
-
- int ch = isr.read();
- System.out.println((char)ch);
- int ch1 = isr.read();
- System.out.println((char)ch1);
- int ch2 = isr.read();
- System.out.println((char)ch2);
- int ch3 = isr.read();
- System.out.println(ch3);
- isr.close();
- }
-
- public static void writeCNText() throws IOException {
-
-
- FileOutputStream fos = new FileOutputStream("tempfile\\GBK.txt");
-
-
- OutputStreamWriter osw = new OutputStreamWriter(fos);
-
-
- osw.write("你A好");
-
-
-
-
-
- osw.close();
-
-
-
-
-
-
-
-
- }
- }
flush和close区别:
flush()刷新完,流可以继续使用;
close()刷新完,流直接关闭,流结束了,无法再用。再用,会报Stream closed异常。
转换流的好处:可以指定编码表。
什么时候使用转换流呢?
1.操作文本数据。
2.需要指定编码表。
下面我们使用不同的编码表来演示转换流:
- package ustc.lichunchun.charstream;
-
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.OutputStreamWriter;
-
- public