第二十章 JAVAIO流文件传输基础学习

本文主要会讲到以下部分:
编码问题
File类的使用
RandomAccessFile的使用
字节流的使用
字符流的使用
对象的序列化和反序列化

第一节 文件的编码

我们首先建立三个文本文件联通 联想 联,并把这几个汉字也存在文本当中。当建立好后,重新打开出现乱码,除联想外,其它两个文档的内容呈乱码状态。那么这是为什么呢?其中蕴含的就是编码的问题,当我们把记事本打开另存为时,联还有联通另存为的编码都是UTF-8,而联想另存为的格式是ANSI编码。这就是由编码引起的。
下面我们用java来做一个案例,通过解析java的一个字符串,这个字符串包含中文英文,我们把它以多种编码的形式解析成字节序列,然后我们采取分析这些字节序列,来比较这些编码之间的一些区别。

import java.nio.charset.Charset;

/**
 * Created by Administrator on 2017/4/12.
 */
public class EncodeDemo {
    public static void main(String[] args)throws Exception {
        String s="慕课ABC";
        byte[] bytes1=s.getBytes();//课程里转换成字节序列,用的是项目默认的编码UTF-8
        //三个字节表示一个汉字。
        for(byte b:bytes1){
            System.out.print(Integer.toHexString(b&0xff)+" ");
            //Integer.toHexString把byte转换成int,用16进制来输出每个字节
            //但是如我们所知byte转换成int会扩充为32位,此时对我们有用的只有低八位
            //所以要和0xff相与,把前面24个0去掉,得到我们所需的字节输出对应的后八位字节码
        }
        System.out.println();
        //如果想要显示的指定某种编码,可以写在参数里
        byte[] bytes2=s.getBytes("utf-8");
        for (byte b:bytes2) {
            System.out.print(Integer.toHexString(b&0xff)+" ");
        }
        //从输出的结果可以看到,utf-8一个汉字占三个字节,英文字母是一个字节
        System.out.println();
        //指定编码为gbk
        byte[] bytes3=s.getBytes("gbk");
        for (byte b:bytes3) {
            System.out.print(Integer.toHexString(b&0xff)+" ");
        }
        //从输出的结果可以看到,gbk一个汉字占两个字节,英文字母是一个字节
        System.out.println();
        //java是双字节编码,java里的一个字符占用两个字节,所以有时候有这么
        // 问的,java里面一个字符可以表示一个汉字吗?可以,gbk就可以放进去。
        // 双字节编码 utf-16be编码
        byte[] bytes4=s.getBytes("utf-16be");
        //UTF-16be中文占用两个字节,英文占用也是两个字节
        for (byte b:bytes4) {
            System.out.print(Integer.toHexString(b&0xff)+" ");
        }
        System.out.println();
        //当你的字节序列是某种编码时,这个时候想把字节序列变成字符串
        // 也需要用这种编码模式,否则会出现乱码
        String str1=new String(bytes4,"utf-16be");
        //这里注意转换的时候要指定编码的格式,否则会使用项目默认的编码出现乱码。
        System.out.println(str1);
        System.out.println(System.getProperty("file.encoding"));//获取当前编码格式
        System.out.println(Charset.defaultCharset().name());//获取编码格式
        /**
         *回到之前文本文件也就是记事本,就是字节序列,可以是任意编码的字节序列,
         *如果我们在中文机器上直接创建文本文件,那么该文本文件只认识ANSI编码,
         *文本文件本身是可以存放任意编码的。联通,联仅仅是一种巧合,它们正好符合
         *了UTF-8编码的规则。举例来说,你创建两个项目分别指定编码为gbk和utf-8。
         *当你在utf-8的项目里创建联通的文本文件,此时是utf-8编码的, 将其复制到
         *gbk编码的项目里,由于它只认识gbk编码的文件,你是utf-8的文件打开就会是乱码
         *如果你把它拷贝到系统桌面上它不会出现乱码,因为它不是直接在中文机器上创建的,
         *是可以认识的,直接创建的则只认识ANSI编码。还有则是这两个项目,
         *如果你只是打开文件复制内容进去,那么它会自动转化成gbk的编码,也不会出现乱码,会自动转换。
         *还应该注意的是不同编码字节对应汉字大小不同,比如联通文本utf-8编码占用六个字节,
         *联想占用4个字节,在读取联通时要六个字节读完,并且转换时编码正确才会不出现乱码
         */
    }
}
第二节 File类的使用

java.io.File类用于表示文件(目录)。程序员可以通过file类来操作硬盘上的文件和目录。File类只用于表示文件(目录)的信息(名称、大小等),不能用于文件内容的访问。在接下来的案例里我们主要演示一下file类的基本API操作。

import java.io.File;
import java.io.IOException;

/**
 * Created by Administrator on 2017/4/15.
 */
public class FileDemo {
    public static void main(String[] args) {
        //了解File类构造函数的几种情况,查帮助,Eclipce可以使用alt+/
        //idea用shift+space不过由于这个键和输入法键重合需要设置下。
        //File类里参数可以是(字符串),或者(URI),或者(类的对象,后面跟文件)
        //还有(String arg0,String arg1)都可以用来使用。
        File file=new File("E:\\新建文件夹 (2)\\imooc");
        //注意E盘后面必须是双斜杠,因为这个是个转义字符
        //在这里中间的分隔符我们可以用双斜杠也可以用反斜杠。
        //还可以用File的一个静态成员,File.separator设置分隔符
        //这样不管是windows系统还是哪个系统它都认识。
        //File file=new File("e:"+File.separator+"新建文件夹 (2)"+File.separator+"imooc");
        System.out.println(file.exists());
        //exists()方法判断文件是否存在
        if(!file.exists()){
            //如果这个目录不存在我们可以直接创建这个目录
            file.mkdir();//这里还有一个方法叫 file.mkdirs()创建多级目录
        }else{
            file.delete();
            //如果有的话删除掉
        }
        //判断是否是一个目录,是目录返回true,不存在或不是目录返回false
        System.out.println(file.isDirectory());
        //判断是否是一个文件
        System.out.println(file.isFile());
        //直接创建一个文件目录
        //File file2=new File("E:\\新建文件夹 (2)\\不想学习啊.txt");
        //这里还可以换一种构造函数来构造,前面写目录后面写子目录
        File file2=new File("E:\\新建文件夹 (2)","不想学习啊.txt");
        //createNewFile创建文件。
        if(!file2.exists()){
            try {
                file2.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }else{file2.delete();}
        System.out.println(file2.isFile());
        //其他常用的file对象的API
        System.out.println(file);//直接打印file,打印的是file的toString()内容,还是地址
        System.out.println(file.getAbsolutePath());//打印比较抽象的路径,直接打印它得到的也是目录
        System.out.println(file.getName());//打印名字
        System.out.println(file2.getName());
        System.out.println(file.getParent());//得到父目录
        System.out.println(file2.getParent());
        System.out.println(file.getParentFile().getAbsolutePath());
        System.out.println(file2.getParentFile());
        //可以直接拿到文件对象来打印,或者.getAbsolutePath打印都行
    }
}
  • File file=new File();参数有多种用法可以是(字符串路径),或者(URI),或者(类的对象,后面跟文件)还有(String arg0,String arg1)都可以用来使用,最后一个是目录和子目录。注意盘后面必须是双斜杠,因为这个是个转义字符,在这里中间的分隔符我们可以用双斜杠也可以用反斜杠。还可以用File的一个静态成员,File.separator设置分隔符,这样不管是windows系统还是哪个系统它都认识。File file=new File(“e:”+File.separator+”新建文件夹 (2)”+File.separator+”imooc”);后面是文本或其他格式的文件须加属性后缀。file.separator返回文件的分隔符,如果是在LUNIX系统上,该值为/,在microsoft windows系统上,为\\,这里File()参数如果没有写入 绝对路径,是相对路径,如果不存在,可以在项目下直接创建。
  • exists()方法判断文件是否存在
  • mkdir()创建目录,mkdirs()创建多级目录
  • createNewFile()创建文件,这个可能会报出io异常需要抛出异常或trycatch处理异常
  • delete()删除目录或者文件
  • isDirectory()方法判断是否是一个目录
  • isFile()判断是否是一个文件
  • 当直接打印file时,打印的是file的toString()内容,打印出来的还是地址
  • System.out.println(file.getAbsolutePath());//打印比较抽象的路径,直接打印它得到的也是目录
  • System.out.println(file.getName());//打印名字
  • System.out.println(file.getParent());//得到父目录
  • System.out.println(file.getParentFile().getAbsolutePath());System.out.println(file2.getParentFile());可以直接拿到文件对象来打印,或者.getAbsolutePath打印都行,还是父目录。

这里来写一个工具类来看一下file类的过滤操作。
第一个是工具类,里面定义了一个可以遍历文件的静态方法,会把目录下所有文件遍历出来。

import java.io.File;
import java.io.IOException;

/**
 * Created by Administrator on 2017/4/15.
 */
public class FileUtils {
   //列出file的一些常用操作比如过滤遍历等操作,把它包装成工具类,以后使用会比较方便。

    /**
     * 列出指定目录下(包括其子目录下的所有文件)
     * @param dir
     * @throws IOException
     */
    public static void listDirectory(File dir)throws IOException{
        //这里首先判断是否存在这个目录,是否是一个目录,不是的话抛出异常。
        if(!dir.exists()) throw new IllegalArgumentException("目录:" + dir + "不存在.");
        if(!dir.isDirectory())throw new IllegalArgumentException(dir + "不是目录.");
        //String[] fileNames=dir.list();//返回的是一个String类型的数组。
        //list()方法用于列出当前目录下的子目录和文件,返回的是文件或目录的名称
        //只包含子目录的名称,不包含子目录下的内容和文件
        //此时如果要遍历子目录下的操作,一个可行的办法是,判断这是否是目录,如果
        //目录,就需要构成File对象,继续遍历做递归操作。那么需要直接这么做吗?
        //其实File提供了直接返回file对象的API。
        File[] files=dir.listFiles();//返回的是一个File类的数组。返回的是直接子目录的抽象
        //分别利用foreach循环对比输出的结果、
        //for(String str:fileNames){
          //  System.out.println(str);
        //}
        //首先判断files是否为空
        if(files!=null&&files.length>0) {
            for (File file : files) {
                if(file.isDirectory()){
                    //递归
                    listDirectory(file);
                }else{
                    System.out.println(file);
                }
            }
        }
    }
}

测试类

public class FileUtilTest {
    public static void main(String[] args)throws Exception {
        FileUtils.listDirectory(new File("E:\\新建文件夹 (2)"));
    }
}
第三节 RandomAccessFile的使用

这是Java提供的对文件内容的访问类,既可以读文件,也可以写文件,RandomAccessFile支持随机访问文件,可以访问文件的任意位置。
要使用RandomAccessFile类我们需要先了解的知识:

  • JAVA文件的模型在硬盘上的文件是byte byte byte存储的,是数据的集合
  • 打开文件怎么打开?有两种模式“rw”(读写),“r”(只读),读写操作和只读操作,后者的模式不可以进行写操作。
  • RandomAccessFile构造的时候是这样,除了打开了一个文件对象之外,你还要告诉它读写的方式,因为它是随机访问文件,所以它内部还包含一个指针,文件指针,打开文件时指针在开头,pointer=0,随着你读写操作的时候,指针会往后挪。所以它在写的时候也好,读的时候也好它的指针都会移动
RandomAccessFile raf=new RandomAccessFile(fiel,"rw");
  • 写方法 raf.write(int)里面可以写一个整数当然也可以写其它的, 这个时候write只能写一个字节,raf.write(int)——只写一个字节,后八位的。你写一个整型,它只会取后八位,如果真想把一个完整的整型写进去,应该写四次,第一次应该是高八个位,第二次是中八个字节,同时指针指向下一个字节的位置准备再次写入。
  • 读方法 int b=raf.read()——读一个字节,从指针在的位置读一个字节,然后把这个字节转换成整数。
  • 文件读写完成以后一定要关闭(Oracle官方说明)如果不关闭可能会有意想不到的错误。

下面我们就来实际使用下吧,创建案例利用RandomAccessFile进行读操作和写操作。

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;

/**
 * Created by Administrator on 2017/4/15.
 */
public class RafDemo {
    public static void main(String[] args) throws IOException {
        File Demo=new File("demo");//如果我们没有写绝对路径,那它就是相对路径
        //就在我们的项目下, 如果这个文件不存在,我们可以直接创建出来。
        if(!Demo.exists()) Demo.mkdir();//创建demo
        File file=new File("demo","raf.dat");//父目录,子目录,不存在即创建它
        if(!file.exists()) file.createNewFile();

        RandomAccessFile raf=new RandomAccessFile(file,"rw");//需要放一个file对象,读写方法
        //这个时候我们可以看一下指针的位置
        System.out.println(raf.getFilePointer());//返回当前指针的位置。此时是0
        //随机读取有一个很大的好处,将来我们在做文件下载的时候,我们自己做一个文件下载,这个文件很大
        //我们用五个程序去下载它,每个下载它的一部分,最后再拼接成一个文件,迅雷就是这样的一个操作,
        //其实迅雷里面是有多个进程或者称之为线程在下载同一个文件,每个线程下载文件的一部分,有使用
        //迅雷的经验一般都看到过这种情况。最后的拼接就需要知道位置在哪。一定会用到RandomAccessFile

        raf.write('A');//只写了一个字节,
        System.out.println(raf.getFilePointer());
        raf.write('B');
        System.out.println(raf.getFilePointer());
        //这里的指针应该在2的位置
        //这里要写一个整数,因为是32位的所以要从高八位开始写入,4个字节。
        int i=0x7fffffff;
        raf.write(i>>>24);
        raf.write(i>>>16);
        raf.write(i>>>8);
        raf.write(i);
        //也可以直接写入一个int。RandomAccessFile提供了直接写入int型类型数据的方法
        raf.writeInt(i);
        //这里直接利用Ctrl+Alt+B查看writeInt的底层源码,和我们刚才写的差不多,多了一步&0xff的操作
        //右移去零得到的结果还是自身。
        String s="中";
        byte[] sBytes=s.getBytes();
        raf.write(sBytes);
        System.out.println(raf.getFilePointer());//此时我们看到的长度是13,两次输入int数字,
        // 中文utf-8三个字节,数字4个字节两遍,8个字节,8+3+2=13个字节的长度。

        //读文件必须把指针移到头部。
        raf.seek(0);//把文件移到开头
        //一次性读取。把文件中的内容都读到字节数组中
        byte[] rafRead=new byte[(int) raf.length()];//这里打开源码返回的值是long型,所以要强制转换一下
        raf.read(rafRead);
        System.out.println(Arrays.toString(rafRead));
        //也可以把字节数组构造成字符串
        String s2=new String(rafRead);
        System.out.println(s2);
        //转换成字符串输出
        for(byte b:rafRead){
            System.out.print(Integer.toHexString(b&0xff)+" ");
            //这里注意如果不进行取与,按int型操作,负数会出现一些问题,取与取后八位。
        }
        raf.close();//将来使用时一定要注意这个操作
    }
}
第四节 字节流

字节流和字符流其实就是我们一直强调的IO流,IO流是JAVA做输入输出的基础,所以IO流可以分为输入流和输出流,在这里面又分为字节流和字符流。在JAVA当中你去读文件或者写文件的时候,你可以以字节为单位,也可以以字符为单位。
1.字节流
(1).字节流也要对应输入和输出就是读和写,它有两个抽象的父类:InputStream/OutStream。

  • InputStream抽象了应用程序读取数据的方式。
  • OutStream抽象了应用程序写出数据的方式。

(2).读写文件在字节流里它都会有一个读写结束的问题,读写结束的时候在字节流里面都是这样达到文件结尾,我们把它称之为EOF=End 或者说读到-1了,读到-1就读到结尾了。
(3).在这两个抽象类里有一些重要的方法是必须我们要掌握的。首先我们来看输入流,比如说我们把键盘作为一个文件,我们对键盘是进行读还是写呢,键盘是输入还是输出?键盘其实是输入,比如说我往某个记事本文件里面写东西,实际上我是从键盘这个文件读了数据,然后写到了这个记事本文件里。输入流的最基本方法主要是读,它主要有这么几个方法:

  • int b=in.read();读取一个字节,无符号填充到整型int的低八位,高八位就补0了,-1是EOF,-1就结束了。in代表的就是输入流的对象
  • in.read(byte[] buf);读取数据直接填充到字节数组buf
  • in.read(byte[] buf,int start,int size);读取数据到字节数组buf,从buf的start位置开始,存放size长度的数据。

(4).输出流是用来进行写的操作,输出流的基本方法写,它和上面的输入都会去对应

  • Out.write(int b) 写出一个byte到流,写的是b的低八位
  • Out.write(byte[] buf) 将buf字节数组都写入到流
  • Out.write(byte[] buf,int start,int size) 字节数组buf从start位置开始,写size长度的字节到流

(5). InputStream和OutStream有第一组子类,一般来说,它们都会搭配使用。
FileInputStream,继承了InputStream,具体实现了在文件上读取数据下面我们也写一个工具类,来实现一下。
工具类

import java.io.FileInputStream;
import java.io.IOException;

/**
 * Created by Administrator on 2017/4/16.
 */
public class IOUtil {
    /**
     * 读取指定文件内容,按照16进制输出到控制台
     * 并且没输出10个byte换行
     * @param fileName
     */
    public static void printHex(String fileName)throws IOException{
        //构造出了一个FileInputStream对象,把文件作为字节流进行读操作。
        FileInputStream in=new FileInputStream(fileName);//这里可以是一个file对象也可以是一个文件路径
        int b;//用来读
        int i=0;
        while((b=in.read())!=-1){
            if(b<=0xf)
                System.out.print("0");//补0,为了更严谨都显示双位。
            System.out.print(Integer.toHexString(b)+" ");
            //将整型转换成16进制,这里为什么不与&0xff,打开源码,read方法输出结果已经与操作过了。
            i++;
            if (i%10==0) System.out.println();
        }
        in.close();
    }
    public static void printHexByByteArray(String fileName)throws IOException{
        FileInputStream in=new FileInputStream(fileName);
        byte[] buf=new byte[8*1024];
        //从in中批量读取字节,放入到buf这个字节数组中,从第0个位置开始放,最多放
        //buf.length个,返回的是读到的字节的个数。因为它有这个情况,第一它可能读不满
        //此时就返回in具体读到的个数,还有另一种情况就是它可能不够放。
        int bytes=in.read(buf,0,buf.length);//一次性读完说明数组足够大
        //是in.read读取文件存放在buf中的长度
        int j=0;
        for (int i=0;i<bytes;i++){
            if((buf[i]&0xff)<=0xf) System.out.print("0");
            System.out.print(Integer.toHexString(buf[i]&0xff)+" ");
            j++;
            if(j%10==0) System.out.println();
        }
        in.close();
       //while循环写法
       /*int bytes=0;
       while((bytes=in.read(buf,0,buf.length))!=-1){
           int j=0;
           for (int i=0;i<bytes;i++){
               if((buf[i]&0xff)<=0xf) System.out.print("0");
               System.out.print(Integer.toHexString(buf[i]&0xff)+" ");
               j++;
               if(j%10==0) System.out.println();
           }
       }
        in.close();*/
    }
}

测试类1

public class IOUtilTest1 {
    public static void main(String[] args) {
        try {
            IOUtil.printHex("E:\\新建文件夹 (2)\\学习啊.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

测试类2

import java.io.IOException;

/**
 * Created by Administrator on 2017/4/16.
 */
public class IOUtilTest2 {
    public static void main(String[] args) {
        try {
            IOUtil.printHexByByteArray("E:\\新建文件夹 (2)\\学习啊.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

read源码

 public int read() throws IOException {
        if (eof) {
            return -1;
        }
        temp = new byte[1];
        int n = read(temp, 0, 1);
        if (n <= 0) {
            return -1;
        }
        return temp[0] & 0xff;
    }

在这里遇到了几个问题呢?

  • 首先是read()方法返回值问题,这里看到在第一个方法时没有使用&0xff就可以正常输出16位,打开read源码看到源码中已经进行了该操作。read第二个方法是数组,就需要加&0xff之后数值输出结果正确。
  • 还出现一个问题是输出结果减少了,两次不一致,一开始最后没有.close,补充上了, 而且是在一个测试类里使用的,因为考虑到指针随机问题,重新设置了一个测试类,两次结果就一致了。
  • while循环里((bytes=in.read(buf,0,buf.length))!=-1),这里的bytes实际是in.read读取文件存放在buf数组中的长度,当读完时结果就是-1,读到文件尾时流没有可用的字节,就返回-1。所以如果文件没有被读完,则返回读取长度而不是-1,while继续执行,循环利用buf。如果文件被读完,则返回-1,while循环结束。
  • 关于read方法,读完返回结果是-1,那么中间的参数0,read(buf,0,buf.length),是代表文件的起始位置还是指针位置呢?如果是while循环是不是下次再循环时,这个0就是代表文件指针现在所在的位置了,会继续原有读到的位置接着往下读?这里我自己测试的结果是,把承装的数组改下后,比如说之前一次尝试读取20*1024个字节然后装入buf数组中。现在改成10,还能正确读完结果。

(6).FileOutPutStream本身是输出,应用程序写数据的一个抽象。继承了OutStream,实现了向文件中写出byte的数据的方法。我们可以做一个简单的操作,向文件中写几个字节。

import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Created by Administrator on 2017/4/16.
 */
public class FileOutDemo {
    public static void main(String[] args) throws IOException{
        // 与FileInputStream类似,
        // 如果只写"demo/out.dat"不存在则创建,如果存在,删除后创建.意思也就是完全清零覆盖
        // 但是后面加上true(boolean append)又不一样,代表存在不会删除,会直接在后面追加内容
        // 如果不存在的话,加上,true,它也会直接创建出来。
        FileOutputStream out=new FileOutputStream("demo/out.dat");//看构造函数,
        // 根据提示可以是file对象或者路径,还可以在对象参数后加一个boolean append,表示新建文件
        // 或是向文件里追加内容。boolean append是问我们是否要追加
        // 要追加内容就加true,否则就会被清空掉
        out.write('A');//一次写出一个字节,代表写出A字符的低八位。
        out.write('B');//写出了B的低八位
        int a=10;//write只能写八位,那么写一个int需要四次,每次八位
        out.write(a>>>24);
        out.write(a>>>16);
        out.write(a>>>8);
        out.write(a);
        //也可以写入一个byte数组
        byte[] utf="中国".getBytes("UTF-8");//这里可以指定编码格式,因为项目编码就是
        //utf-8,也可以不写。
        out.write(utf);
        out.close();
        IOUtil.printHex("demo/out.dat");
    }
}

再编写一个可以copy的工具类

public static void copyFile(File srcFile,File destFile) throws IOException{
        if(!srcFile.exists())
            throw new IllegalArgumentException("文件"+srcFile+"不存在");//参数有问题的异常,以后遇到这种情况
        //也可以抛出这个参数有问题的异常
        if(!srcFile.isFile())
            throw new IllegalArgumentException(srcFile+"不是文件");
        FileInputStream in=new FileInputStream(srcFile);
        FileOutputStream out=new FileOutputStream(destFile);
        byte[] bytes=new byte[8*1024];
        int b;
        while((b=in.read(bytes,0,bytes.length))!=-1){
            out.write(bytes,0,b);
            out.flush();//最好加上,不加也没关系对字节流而言。
        }
        in.close();
        out.close();

    }
//测试类
import java.io.File;
import java.io.IOException;

/**
 * Created by Administrator on 2017/4/17.
 */
public class IOUtilTest3 {
    public static void main(String[] args) {
        try {
            IOUtil.copyFile(new File("E:\\新建文件夹 (2)/学习啊.txt"),new File("E:\\新建文件夹 (2)/学习啊1.txt"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这里遇到了哪些问题呢?

  • 关于在编程中读取一个数字时,设置一个int变量来存放读取的这个文件的长度,b=in.read(bytes,0,bytes.length),这里在定义变量时不能图省事顺便就写成int b=in.read(bytes,0,bytes.length),然后后面再调用b时,如果不写后面的in.read()方法时,等于是只调用了一个int值,while(b!=-1),while循环成了一个无限循环。在while里应该添加的是读取的操作,这样在变成-1时就可以正确停止。在这里亲测,1KB的文件copy出了一个700M的txt文件,电脑快卡死了。
  • out.flush();课程里讲的是最好加上,不加对字节流而言也没有关系。flush是把流里的缓冲数据输出,close是把这个流关闭了,关闭之前会把缓冲的数据都输出。这个应该是后面缓冲中可能学的内容,flush()方法可以强迫输出流(或缓冲的流)发送数据,即使此时缓冲区还没有填满,以此来打破这种死锁的状态。当我们使用输出流发送数据时,当数据不能填满输出流的缓冲区时,这时,数据就会被存储在输出流的缓冲区中。如果,我们这个时候调用关闭(close)输出流,存储在输出流的缓冲区中的数据就会丢失。所以说,关闭(close)输出流时,应先刷新(flush)换冲的输出流,话句话说就是:“迫使所有缓冲的输出数据被写出到底层输出流中”。
  • IllegalArgumentException此异常表明向方法传递了一个不合法或不正确的参数,在以后遇到参数不正确的情况下都可以throw该异常。运行时异常的一个子类
  • 关于FileOutputStream构造方法的参数问题,后面加不加true。其实正常来说如果想覆盖,不想要原文件的内容,那么不加,如果文件存在,删除它并新建,如果不存在,那么直接创建一个。加了true,如果文件存在,就在文件后面追加内容,如果不存在,即创建。

(7)DataOutputStream和DataInputStream,这组流实际上是对我们普通流,对“流”功能的扩展,可以更加方便的读取int、long、char字符等类型数据,使我们使用时更加方便。对于OutputStream我们就多了一些方法,比如**writeInt() writeDouble() writeUTF()**utf代表utf-8,其实这种都是以一种设计模式,就是装饰模式来实现的,并不是特别复杂。writeInt()能够写一个整型,其实就是包装了前面讲过的,四个字节作四次write的操作,包装好之后我们可以直接用。我们来写两个个案例,往一个文件里面写数字,或者说写数据普通的数据类型,并且直接读取出来。

import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Created by Administrator on 2017/4/17.
 */
public class DosDemo {
    public static void main(String[] args) throws IOException {
        String file="demo/dos.dat";
        DataOutputStream dos=new DataOutputStream(new FileOutputStream(file));
        //CTRL+Q查看帮助文档,把FileOutStream作为参数扩展后就具备了想要的功能。
        //这里包装FileOutStream就是使用它的write方法
        dos.writeInt(10);
        dos.writeInt(-10);
        dos.writeLong(10l);
        dos.writeDouble(10.5);
        //采用UTF-8编码写出,一个中文三个字节
        dos.writeUTF("中国");
        //采用java里的UTF-16be编码写出,一个中文两个字节,
        dos.writeChars("中国");
        dos.close();
        IOUtil.printHex(file);
    }
}
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * Created by Administrator on 2017/4/17.
 */
public class DisDemo {
    public static void main(String[] args) throws IOException {
        String file="demo/dos.dat";
        IOUtil.printHex(file);
        DataInputStream dis=new DataInputStream(new FileInputStream(file));
        int i=dis.readInt();
        System.out.println(i);
        int j=dis.readInt();
        System.out.println(j);
        long x=dis.readLong();
        System.out.println(x);
        Double y=dis.readDouble();
        System.out.println(y);
        String m=dis.readUTF();
        System.out.println(m);
        char n=dis.readChar();
        System.out.println(n);
        char n2=dis.readChar();
        System.out.println(n2);
    }
}

(8)BufferedInputStream和BufferedOutputStream这两个流类为IO提供了带缓冲区的操作,一般打开文件进行写入或读取操作时,都会加上缓冲这种流模式提高了IO的性能,(主要指的是输入输出的性能)。举例来说从应用程序中,我们把数据放入文件,相当于把一缸水倒入另一个缸中,最早之前我们用FileOutputStream的write()方法,相当于一滴一滴的把水“转移”过去。而我们用DataOutputStream,writeXxx()方法会方便一些,相当于一瓢一瓢的来转移。那么bufferedOutStream当中它也有write方法,更方便,相当于一瓢一瓢水先放入桶中,桶就是我们说的缓冲区,再从桶中倒入到另一个缸中,性能提高了。以后使用它性能会更好。重新写一个coby文件的案例。用三种方法一种带缓冲区一个字节一个字节读取,一种不带缓冲区一个字节一个字节读取,联合上面的批量字节读取。三者比较一下时间,带缓冲区的会比不带缓冲区的一个字节读取方法要快,批量读取的最快。获取当前系统时间long a=System.currentTimeMillis()。
拷贝工具类

 /**
     * 进行文件的拷贝,利用带缓冲的字节流
     * @param srcFile
     * @param destFile
     * @throws IOException
     */
    public static void copyFileByBuffer(File srcFile,File destFile) throws IOException{
        if(!srcFile.exists())
            throw new IllegalArgumentException("文件"+srcFile+"不存在");//参数有问题的异常,以后遇到这种情况
        //也可以抛出这个参数有问题的异常
        if(!srcFile.isFile())
            throw new IllegalArgumentException(srcFile+"不是文件");
        BufferedInputStream bis=new BufferedInputStream(new FileInputStream(srcFile));
        //这里的参数需要用inputStream来构造,也可以设置缓冲区的大小,(参数1,size)
        BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(destFile));
        int c;
        while((c=bis.read())!=-1){
            bos.write(c);
            bos.flush();//刷新缓冲区
        }
        bis.close();
        bos.close();
    }

    /**
     * 也是一个字节一个字节读取,不带缓冲区注意,带缓冲区的一定要flush一下
     * @param srcFile
     * @param destFile
     * @throws IOException
     */
    public static void copyFileByByteByter(File srcFile,File destFile) throws IOException{
        if(!srcFile.exists())
            throw new IllegalArgumentException("文件"+srcFile+"不存在");//参数有问题的异常,以后遇到这种情况
        //也可以抛出这个参数有问题的异常
        if(!srcFile.isFile())
            throw new IllegalArgumentException(srcFile+"不是文件");
        FileInputStream in=new FileInputStream(srcFile);
        FileOutputStream out=new FileOutputStream(destFile);
        int c;
        while((c=in.read())!=-1){
            out.write(c);
            out.flush();
        }
        in.close();
        out.close();
    }

测试类

import java.io.File;
import java.io.IOException;

/**
 * Created by Administrator on 2017/4/17.
 */
public class IOUtilTest4 {
    public static void main(String[] args) {
        try {
            long start=System.currentTimeMillis();//获取毫秒数
            IOUtil.copyFileByByteByter(new File("E:\\武神.txt"),
                    new File("E:\\新建文件夹 (2)/武神1.txt"));
            long end=System.currentTimeMillis();//获取毫秒数
            System.out.println("一个字节一个字节读取的时间"+(end-start));//46615毫秒
        } catch (IOException e) {
            e.printStackTrace();
        }
        /*try {
            long start=System.currentTimeMillis();//获取毫秒数
            IOUtil.copyFileByBuffer(new File("E:\\武神.txt"),
                    new File("E:\\新建文件夹 (2)/武神1.txt"));
            long end=System.currentTimeMillis();//获取毫秒数
            System.out.println("一个字节一个字节带缓冲区的时间"+(end-start));//28601毫秒
        } catch (IOException e) {
            e.printStackTrace();
        }*/
        /*try {
            long start=System.currentTimeMillis();//获取毫秒数
            IOUtil.copyFile(new File("E:\\武神.txt"),
                    new File("E:\\新建文件夹 (2)/武神2.txt"));
            long end=System.currentTimeMillis();//获取毫秒数
            System.out.println("字节批量读取的时间"+(end-start));//46毫秒
        } catch (IOException e) {
            e.printStackTrace();
        }*/
    }
}
第五节 字符流

在学习本节时我们需要对一些基本的概念有所了解
(1).编码问题
(2).认识文本和文本文件,

  • JAVA中的文本(char)是16位无符号整数,是字符的unicode编码(双字节编码)
  • 文件是byte byte byte……的数据序列
  • 文本文件是文本(char)序列按照某种编码方案(utf-8,utf-16be,gbk)序列化为byte的存储结果。

(3).字符流里也分为输入流和输出流(Reader Writer)。这里如果用它读MP3是没有字符的,我们的字符流操作的大部分都是文本文件,比如说一个mp3文件,都是字节序列,那么你读其中一个字符就没有任何意义,默认是按照项目编码来解析的。

  • 字符的处理,一次处理一个字符。
  • 字符的底层仍然是基本的字节序列,这就是字符流的处理方式。
  • 我们知道字符可能是中文,也可能是英文,可能是UTF-8编码的,也可能是UTF-16BE编码的,那么所占大小都是不一样的,那么在这里,字符流的基本实现:
    • InputStreamReader 完成Byte流解析为char流,按照编码解析
    • OutputStreamWriter 提供char流到byte流,按照编码处理,(就是将字符写入byte)

注意flush方法,结束缓冲,下面来做案例

import java.io.*;

/**
 * Created by Administrator on 2017/4/18.
 */
public class IsrAndOswDemo {
    public static void main(String[] args) throws IOException{
        InputStreamReader isr=new InputStreamReader(new FileInputStream
                ("E:\\新建文件夹 (2)\\学习啊.txt"),"gbk");
        OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("E:\\新建文件夹 (2)\\学习啊5.txt"),"gbk");
        //通常我们新建文本文档的ANSI编码,其实就是gbk编码。不写参数即默认项目编码
        //我们一般要写你要操作的文件的编码
        /*int c;
        while((c=isr.read())!=-1){
            System.out.print((char) c);//这里需要强制转换下字符
        }*/
        char[] buffer=new char[8*1024];
        int bytes;
        //批量读取,放入buffer这个字符数组,从第0个位置开始放置,最多放buffer.length个。
        //返回的是读到的字符的个数,0是从这个数组的第0位开始放起
        while((bytes=isr.read(buffer,0,buffer.length))!=-1){
            osw.write(buffer,0,bytes);
            osw.flush();//漏写这个,第一次写入没成功,加上它就行了
            String s=new String(buffer,0,bytes);//ctrl+Q查看帮助API文档
            System.out.println(s);
            /*for(char ch:buffer){
                System.out.print(buffer);//这样读取出现了数据丢失,用上面的方法。
            }*/
        }
        isr.close();
        osw.close();
    }
}

(4).字符流的FileReader和FileWriter,和我们上面学的InputStreamReader,OutputStreamWrite很像,我们做了一个嵌套来构造,那么在FileReader和FileWriter就非常方便了。但是有一点是因为它不能设置编码,所以有时编码不一致时,需要使用之前的方法来确定编码。

import java.io.*;

/**
 * Created by Administrator on 2017/4/18.
 */
public class FrAndFwDemo {
    public static void main(String[] args) throws IOException {
        FileReader fr=new FileReader("E:\\新建文件夹 (2)/学习啊3.txt");
        /*OutputStreamWriter osw2=new OutputStreamWriter(new FileOutputStream("E:\\新建文件夹 (2)/学习啊6.txt")
        ,"gbk");*/
        FileWriter ft=new FileWriter("E:\\新建文件夹 (2)/学习啊6.txt");
        char[] buffer=new char[8*1024];
        int c;
        while((c=fr.read(buffer,0,buffer.length))!=-1){
            String s=new String(buffer,0,c);
            ft.write(s);
            ft.flush();
        }
        fr.close();
        ft.close();
    }
}

(5).字符流的过滤器,我们可以对字符流加过滤,使得字符流具备更强大的功能,比如说对于普通的流,我们加了过滤之后,可以有一个BufferedReader,我们把它称之为过滤流,字符流的过滤器,它有最强大的功能readLine当然,除了基本的读的功能以外,它可以一次读一行。BufferedWriter,一次写一行。,也可以结合PrintWriter一起使用。我们来写一个案例,这个案例也是对文件进行读写操作

import java.io.*;

/**
 * Created by Administrator on 2017/4/18.
 */
public class BrAndBwOrPwDemo {
    public static void main(String[] args) throws IOException {
        //对文件进行读写操作
        BufferedReader br=new BufferedReader(new InputStreamReader
                (new FileInputStream("E:\\新建文件夹 (2)/学习啊.txt"),"gbk"));//里面的参数是reader,也就是对字符流进行过滤
       /* BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream
                ("E:\\新建文件夹 (2)/学习啊7.txt"),"gbk"));*/
        PrintWriter pw=new PrintWriter("E:\\\\新建文件夹 (2)/学习啊8.txt","gbk");
        //PrintWriter pw1=new PrintWriter(OutputStream,boolean autoFlush) 参数众多,后者代表自动刷新缓冲区,也非常实用。
        //经常用printWriter和bufferedReader搭配使用,也是非常实用的。
        String line;
        while((line=br.readLine())!=null){
            System.out.print(line);//一次读一行并不能识别换行
//            bw.write(line);
//            //直接写出,换行仍然没用,单独写出换行操作
//            bw.newLine();
//            bw.flush();
            pw.println(line);//有这个ln就是换行,没有就不换行
            pw.flush();
        }
        br.close();
        pw.close();
    }
}
第六节 对象的序列化和反序列化

1.什么是对象的序列化和反序列化呢?

  • 对象序列化,就是将object对象转换成byte序列,反之叫对象的反序列化
  • 做序列化需要有流类,
    • 序列化流(ObjectOutPutStream)是过滤流,是字节的过滤流。—-writeObject
    • 反序列化流(ObjectInputStream)—-readObject方法
  • 在java里面对象如果要进行序列化,有这样一个接口,我们称之为序列化接口(Serializable),对象必须实现序列化接口,才能进行序列化,否则将出现异常。这个接口没有任何方法,只是一个标准。

下面我们来做一个案例,就是对student做一个序列化和反序列化的工作,首先创建学生类的对象,基本都是快捷键,注意带参构造方法的快捷键alt+FN+Insert,第一个Constructor就是

import java.io.Serializable;

/**
 * Created by Administrator on 2017/4/18.
 */
    public class Student implements Serializable{
    private String stuNo;
    private String stuName;
    private int stuAge;
    public Student(){

    }

    public Student(String stuNo, String stuName, int stuAge) {
        this.stuNo = stuNo;
        this.stuName = stuName;
        this.stuAge = stuAge;
    }

    public String getStuNo() {
        return stuNo;
    }

    public void setStuNo(String stuNo) {
        this.stuNo = stuNo;
    }

    public String getStuName() {
        return stuName;
    }

    public void setStuName(String stuName) {
        this.stuName = stuName;
    }

    public int getStuAge() {
        return stuAge;
    }

    public void setStuAge(int stuAge) {
        this.stuAge = stuAge;
    }

    @Override
    public String toString() {
        return "Student{" +
                "stuNo='" + stuNo + '\'' +
                ", stuName='" + stuName + '\'' +
                ", stuAge=" + stuAge +
                '}';
    }
}

第二个就是实现序列化和反序列化的类

import java.io.*;

/**
 * Created by Administrator on 2017/4/18.
 */
public class ObjectSeriaDemo {
    public static void main(String[] args) throws IOException{
        String file="demo/obj.dat";//把对象存储在这里面
        //1.我们做对象的序列化
        /*ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(file));
        Student stu=new Student("10001","张三",20);
        oos.writeObject(stu);//第一次运行异常,是student类没有实现可序列化的接口
        oos.flush();
        oos.close();*/
        //2.对象的反序列化
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
        try {
            Student stu=(Student)ois.readObject();
            System.out.println(stu);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        ois.close();
    }
}

输出结果Student{stuNo=’10001’, stuName=’张三’, stuAge=20}
2.在java中,并不是我们什么时候都希望我们所有的元素都要进行序列化的工作,所以在java中有一个关键字 transient, transient这个关键字修饰了之后,该元素不会被默认的进行序列化,不会进行虚拟机jvm默认的序列化,在上面的age属性前加上这个关键字transient,再运行20的年龄就读取不到了,结果是Student{stuNo=’10001’, stuName=’张三’, stuAge=0}。这样的话我们就看age元素并没有被序列化,因为很多时候,我们也要考虑一些我并不是所有的元素都要进行序列化,特别是将来要把一些对象在网络中进行传输,转化成字节序列,但是有一些元素我们可能没有必要使用,那么放到网络中进行传输的时候,它就会浪费这个流量,特别是这个元素占用的空间比较大的话,传输起来就比较慢,那么这个时候我们就可以利用transient关键字。
transient有些情况下能够帮助我们提高性能,分析ArrayList源码中序列化和反序列化的问题,我们知道ArrayList数组列表,它下面无非是封装了数组的操作,数组的扩容,移除插入等操作。我们看到ArrayList有个元素,private transient Object[] elementData这就是它底层包装的数组,就是对这个数组进行扩容,插入元素的操作,进行移除的操作。但是这个数组元素是用transient来修饰的,它不能进行默认的序列化,但是可以自己序列化,有时候数组中并未放满,可以实现有效元素的序列化,提高性能。
transient还有一个什么好处呢?并不是说不做默认的序列化它就不能序列化了,也可以自己完成这个元素的序列化。这时该怎么进行序列化呢,有两个方法签名很重要,而且这两个方法签名都是私有了。我们可以参照一个类,一会也可以分析这个类的源码,有一个ArrayList这个类,打开这个类,我们会发现它里面就有一个元素就是transient来修饰的,接下来搜索writeObject,我们来使用这样一个方法签名

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{}

这是ArrayList的源代码,我们把方法签名直接拿到原来的Student类里即可,然后拿过来我们怎么做序列化呢

 private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
        s.defaultWriteObject();//把虚拟机jvm能默认序列化的元素进行序列化操作
        //打开String的源码,里面已经实现了序列化的接口Serializable
        s.writeInt(stuAge);//这里writeInt,writeObject都可以,因为正好是一个整型
        //如果是其他元素,可以直接writeObject。
        //这是自己完成stuage的序列化。
        //其实我们都用transient修饰也可以,这样我们就自己一个一个来进行序列化
    }
    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();//把jvm默认能反序列化的元素进行反序列化操作
        this.stuAge = s.readInt();//自己完成stuAge的反序列化操作
    }

3.序列化中子类和父类构造函数的调用问题
如果父类实现了序列化接口,那么子类就不用去实现接口,可以直接进行序列化

import java.io.*;

/**
 * Created by Administrator on 2017/4/18.
 */
public class ObjectSeriaDemo2{
    public static void main(String[] args) throws Exception {
       /* ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("demo/obj2.dat"));
        Fool3 fool3=new Fool3();
        oos.writeObject(fool3);
        oos.flush();
        oos.close();*/
        //序列化的结果fool....
        //fool2....
        //fool3.... 这里 Fool3 fool3=new Fool3()递归调用父类的构造函数没有问题
        //主要看反序列化的时候是否递归调用父类的构造函数
        /*ObjectInputStream ois=new ObjectInputStream(new FileInputStream("demo/obj2.dat"));
        Fool3 fool3=(Fool3)ois.readObject();
        System.out.println(fool3);
        ois.close();*///第一次运行控制台没有任何调用,打印了对象的情况,此时我们是不是可以证明
        //反序列不调用父类的构造方法呢,完全不能
        /*ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("demo/obj2.dat"));
        Bar2 bar2=new Bar2();
        oos.writeObject(bar2);
        oos.flush();
        oos.close();*/
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("demo/obj2.dat"));
        Bar2 bar2=(Bar2)ois.readObject();
        System.out.println(bar2);
        ois.close();
        //运行结果,bar类构造方法显示的调用了,Bar类是另两个类的父类,但是没有实现序列化接口,所以被
        // 显式的调用了。此时做一个小小的改动,将子类1也去掉接口的实现,在子类2中实现了接口。重新运行
        // bar和bar1都被显式的调用了,这样其实我们会得到一个结论。
        /**
         * 对子类对象进行反序列化操作时,如果其父类没有实现序列化接口,那么其父类的构造函数会被调用,
         * 如果实现了序列化接口,那么父类的构造函数我们是看不到被调用了,它相当于从你存的这个文件里
         * 就能读到了,如果您父类不能进行序列化,在反序列化子类对象时,其父类的构造方法会被调用
         */

    }
}
/**
 * 一个类实现了序列化接口,那么其子类都可以进行序列化
 */
class Fool implements Serializable{
    public Fool(){
        System.out.println("fool....");
    }
}
class Fool2 extends Fool{
    public Fool2(){
        System.out.println("fool2....");
    }
}
class Fool3 extends Fool2{
    public Fool3(){
        System.out.println("fool3....");
    }
}
class Bar{
    public Bar(){
        System.out.println("Bar....");
    }
}
class Bar1 extends Bar{
    public Bar1(){
        System.out.println("Bar1....");
    }
}
class Bar2 extends Bar1 implements Serializable {
    public Bar2(){
        System.out.println("Bar2....");
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值