一、文件的编码

//获取字符串编码
public static void main(String[] args) {
    String s="慕课ABC";  //UTF-8
    //把字符串转换为字节数组(十进制)
    byte[] byte1=s.getBytes();  //参数可以指定编码,不指定就是项目默认编码
    byte[] byte2=s.getBytes("gbk"); //中文两个字节,英文一个字节
    byte[] byte3=s.getBytes("utf-16be");  //中文、英文都是两个字节
    for (byte b:byte1
         ) {
        //把字节(转换为了int)转换为十六进制
        //这里的结果只取了int(32位)的最后八位(一字节)
        System.out.print(Integer.toHexString(b & 0xff)+" ");
    }
}

关于 byte[] b & 0xff:

第一次遇到这个,也是有点懵,就记录一下。

  • 先说一下Integer.toHexString() ,这个方法是将int数转换为16进制数,而int是四个字节的,也就是说它的参数得是int类型的数

  • 然后是0xff,这是一个16进制数,但是它的类型为int,也就是四字节

  • b & 0xff也就是一个8位 按位与 32位的数,所以8位的数需要补上24个0

最终的计算结果也就是一个32位 四字节 的int类型数了,所以能被Integer.toHexString()接受

知道了流程,那么为什么能取到最后八位呢?

对于正数来说都是一样的,主要是负数的结果:

计算机中负数是用补码表示的,那么补得24个0,经过补码结果就变成了24个1了

-118 (补上24个0后)

原码: 00000000 00000000 00000000 10001010

补码:11111111 11111111 11111111 11110110

那么在和 0xff 按位与之后

11111111 11111111 11111111 11110110 (补码)

&

00000000 00000000 00000000 11111111 (补码)

=

00000000 00000000 00000000 11110110 (补码)

可以看到最后八位和前面的数一致,因此对于负数来说,相当于只取了最后八位

以下是慕课ABC的字节数列(我使用了UTF-8编码,所以汉字是三个字节)

image-20220323171755358

image-20220323171900872

//将字节数列转换为字符串
String s="慕课ABC"; //默认UTF-8
byte[] byte1=s.getBytes();

//将字节数列转换为字符串时,不指定编码就使用项目默认编码
String s1=new String(byte1,"gbk");  //这样就会乱码
System.out.println(s1);

二、File类常用API介绍

java.io.File类 用于表示文件(目录)

File类只能用于表示文件(目录)的信息(名称、大小等),不能用于文件的访问

public static void main(String[] args) throws Exception {
        File file = new File("C:\\Users\\shitian\\Desktop\\作业");
        if(!file.exists()){
            file.mkdir();  //创建目录
        }
        else{
            file.delete();
        }
        //是否是一个文件
        System.out.println(file.isFile());
        //是否是一个目录,是的话返回true,不是目录或者目录不存在返回false
        System.out.println(file.isDirectory());
    }
File file1 = new File("C:\\Users\\shitian\\Desktop\\作业\\1.txt");
      if(!file1.exists()){
          file1.createNewFile();  //创建文件
      }
      else{
          file1.delete();
      }
File file2 = new File("C:\\Users\\shitian\\Desktop","作业\\1.txt"); //构造函数的另一种用法
        if(!file2.exists()){
            file2.createNewFile();
        }
        else{
            file2.delete();
        }
File file = new File("C:\\Users\\shitian\\Desktop\\作业\\1.txt");
System.out.println(file);  //直接输出完整路径 C:\Users\shitian\Desktop\作业\1.txt
System.out.println(file.getName());  //输出当前文件名称 1.txt
System.out.println(file.getParent());  //输出父目录  C:\Users\shitian\Desktop\作业

三、遍历目录

//工具类 Test.java
public class Test {
    //一些File类的常用操作,比如过滤、遍历等
    public static  void listDirectory(File dir) throws Exception{
        if(!dir.exists()){
            throw new IllegalArgumentException("目录:"+dir+"不存在");
        }
        if(!dir.isDirectory()){
            throw new IllegalArgumentException(dir+" 不是目录");
        }
        String[] string=dir.list(); //list()方法 返回字符串数组(列出当前目录下的子目录和文件)
        for (String string1:string
             ) {
            System.out.println(string1);
        }
    }
     /**
     * File类提供的返回File对象的方法 listFiles() ,用于输出子目录中的文件
     * @param dir
     */
    public static void listAllDirdectory(File dir){
        File[] file = dir.listFiles();  //返回目录中所有文件或目录的File对象
        for (File file1: file
             ) {
            System.out.println(file1.getName()); //一个File对象输出的是完整路径,所以使用getName()
            if(file1.isDirectory()){
                listAllDirdectory(file1);  //递归调用
            }
        }
    }
}
//ClassDemo1.java
public class ClassDemo1 {
    public static void main(String[] args) throws Exception {
        Test.listDirectory(new File("C:\\Users\\shitian\\Desktop\\作业")); //就能输出
    }
}

四、RandomAccessFile

java提供的的对文件内容的访问,既可以读文件,也可以写文件

并且支持随机访问文件,可以访问文件的任意位置

(1)java文件模型:

​ 在硬盘上的文件是 byte byte byte存储的,是数据的集合

(2)打开文件:

​ 有两种模式 “rw”读写 “r”只读

​ RandomAccessFile raf = new RandomAccessFile(file,”rw”);

​ 方法内部存在文件指针,打开文件时指针在开头 pointer = 0;

(3)读方法

​ int b = raf.read(); —> 只读一个字节,然后转换为int

(4)写方法

​ raf.write(int); —> 只写一个字节(后8位),同时指针指向下一个位置,准备再次写入

(5)文件读写完成后一定要关闭,否则会出现意想不到的错误。(Oracle官方说明)

public static  void main(String[] args) throws Exception{
        File file = new File("file");
        if(!file.exists()){
            file.mkdir();
        }
        File file2 = new File(file,"file2.dat");
        if(!file2.exists())
            file2.createNewFile();

        RandomAccessFile raf = new RandomAccessFile(file2,"rw");
        //指针的位置
        System.out.println(raf.getFilePointer());

        raf.write('A'); //java中 char是两个字节
        System.out.println("写入A时指针的位置: "+raf.getFilePointer());
        raf.write('B');
        System.out.println("写入B时指针的位置: "+raf.getFilePointer());

        int i = 0x7fffffff;
        //write方法每次只能写一个字节,所以需要写入四次才能写入一个int
        raf.write(i >>> 24);
        raf.write(i >>> 16);
        raf.write(i >>> 8);
        raf.write(i);
        System.out.println(raf.getFilePointer());

        String s = "一";
        byte[] byte1 = s.getBytes("gbk");
        raf.write(byte1);  //可以直接写入一个数组
        System.out.println(raf.getFilePointer());
        System.out.println(raf.length()); //每次打开文件,指针总在开头,所以写入覆盖前面的内容,而不会覆盖后面的内容

        //读文件,将指针移到头部
        raf.seek(0);
        //一次性读取,把文件中的内容都读到字节数组中
        byte[] buf = new byte[(int)raf.length()];
        raf.read(buf); //默认只读一个字节,带上一个字节数组参数,可以都写入数组

        System.out.println(Arrays.toString(buf)); //将数组变为字符串
        String string = new String(buf,"gbk"); //原来写入怎么编码的,输出就怎么编码(但是由于是对整个数组进行编码,原来的中文可能会变成乱码)
        System.out.println(string);

        for (byte byte2: buf
             ) {
            System.out.println(Integer.toHexString(byte2 & 0xff)); //获取int后8位
        }

        raf.close();
    }

注意:打开文件时,指针都位于起始位置0,写入时都会覆盖原来的内容

五、字节流和字符流

1、字节流

1)InputStream、OutputStream

​ InputStream抽象了应用程序读取数据的方式

​ OutputStream抽象了应用程序写出数据的方式

2)EOF = End 读到-1就是结尾

3)输入流基本方法

​ int b = in.read(); 读取一个字节,无符号填充到int的低八位,其余补0,-1是EOF

​ in.read(byte[] 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,从buf的start位置开始写入size长度的字节到流

5)FileInputStream —> 具体实现了在文件上读取数据

/**
     *工具类 
     *读取指定文件内容,按照16进制输出到控制台
     * 并且每输出十个byte就换行
     * @param FileName
     * @throws Exception
     */
    public static  void printHex(String FileName) throws Exception{
        //把文件作为字节流进行读操作
        FileInputStream in = new FileInputStream(FileName);
        int b;
        int i=1;
        while((b=in.read())!=-1){
            if(b<=0xf){
                //少于两位16进制前面补0
                System.out.print('0');
            }
            System.out.print(Integer.toHexString(b)+" "); //将整型b转换为16进制字符串
            if(i++%10==0){
                System.out.println();
            }
        }
        in.close();
    }

image-20220328160826313

public static void printHexByByteArray(String FileName)throws IOException{
        FileInputStream in = new FileInputStream(FileName);
        byte[] buf = new byte[20*1024];
        //从in中批量读取字节,放到buf字节数组中,从第0个位置,最多读取buf长度的字节数
        //返回的是读到的字节数,因为字节数组有可能读不满
        int bytes = in.read(buf,0,buf.length);
        int j=1;
        for(int i=0; i<bytes; i++){
            if(buf[i]<= 0xf && buf[i]>=0){ //有可能遇到负数
                System.out.print("0");
            }
            System.out.print(Integer.toHexString(buf[i] & 0xff)+" ");
            if(j++%10==0){
                System.out.println();
            }
        }
        in.close();
    }

6)FileOutputStream 实现了向文件中写入byte数据的方法

public static void main(String[] args) throws Exception {
        //如果文件不存在,则直接创建,如果存在,删除后创建
        //存在一个boolean append 参数,默认为false,若设置为true,则文件若存在,则在其中追加内容
        FileOutputStream out = new FileOutputStream("file/file2.dat");
        out.write('A'); //写出了A的低八位
        out.write('B');
        int a = 10; //write()一次只能写入一个字节,需要写四次
        out.write(a >>> 24);
        out.write(a >>> 16);
        out.write(a >>> 8);
        out.write(a);
        byte[] gbk = "中国".getBytes("gbk");
        out.write(gbk);
        out.close();

        printHex("file/file2.dat");
    }
public static void copyFile(File srcFile,File destFile)throws IOException{
        if(!srcFile.exists()){
            throw new IllegalArgumentException("文件:"+srcFile+"不存在");
        }
        if(!destFile.isFile()){
            throw new IllegalArgumentException(destFile+"不是文件");
        }
        FileInputStream in = new FileInputStream(srcFile);
        FileOutputStream out = new FileOutputStream(destFile);
        byte[] bytes = new byte[8*1024];
        int b;
        //批量读取到字节数组bytes,读完返回读取字节的个数
        /**
         * 这里一个问题:while不是死循环吗?b是一个不为-1的数
         * 解答:在while体中,read读到结尾时,还是会返回-1
         */
        while((b=in.read(bytes,0,bytes.length))!=-1){
            out.write(bytes,0,b);
            out.flush(); //最好加上
        }
        out.close();
        in.close();
    }

7)DataOutputStream/DataInputStream

​ 对“流”的扩展,可以更加方面的读取 int、long、字符等类型数据

​ DataOutputStream

​ writeInt()/writeDouble()/writeUTF()

​ DataInputStream

​ readInut()/readUTF()/readDouble()

public static void main(String[] args) throws Exception {
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("file/file2.dat"));
        dos.writeInt(10);
        dos.writeInt(-10);
        //采用UTF-8编码写出,三个字节
        dos.writeUTF("中国");
        //采用UTF-16编码写出,两个字节
        dos.writeChars("中国");
        dos.writeChar('a');
        dos.close();
    }
public static void main(String[] args) throws Exception {
        String file = "file/file2.dat";
        Test.printHex(file);
        DataInputStream dis = new DataInputStream(new FileInputStream(file));
        int i = dis.readInt();
        System.out.println(i);
        i = dis.readInt();
        System.out.println(i);


        String s1=dis.readUTF();
        System.out.println(s1);

        //读取两个字节,因为java是双字节编码
    	//前面使用UTF-16写入中文(双字节),readChar()也就能读取一个中文(两个字节)
        char c = dis.readChar();
        System.out.println(c);
    
        c=dis.readChar();
        System.out.println(c);
    
        c=dis.readChar();
        System.out.println(c);
        dis.close();

    }

image-20220330171413062

8)BufferedInputStream/BufferedOutputStream

​ 这两个流为IO提供了带缓冲区的操作,一般打开文件进行写入或读取操作时,都会加上缓冲,这种流模式提高了IO的性能

从应用程序把输入方法文件中,相当于将一缸水转移到另外一缸水:

FileOutputStream —> write()方法 相当于一滴一滴的把水“转移”过去

DataOutputStream —> writeXxx()方法 相当于一瓢一瓢的把水“转移”过去,会方便一点

BufferedOutputStream —> write()方法 更方便,相当于一瓢一瓢的把水先放入一个桶中(缓冲区),然后再从桶中倒入到缸中

public static void copyFileByBuffer(File srcFile,File destFile)throws IOException{
        if(!srcFile.exists()){
            throw new IllegalArgumentException("文件:"+srcFile+"不存在");
        }
        if(!destFile.isFile()){
            throw new IllegalArgumentException(destFile+"不是文件");
        }
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));

        long start = System.currentTimeMillis();
        int c;
        while((c=bis.read())!=-1){
            bos.write(c);
            bos.flush(); //刷新缓冲区
        }
        long end = System.currentTimeMillis();
        System.out.println(end-start);
        bis.close();
        bos.close();
    }

9)最后比较了三种方式读取文件的速度

  • 利用FileInputStream/FileOutputStream,实现单字节复制文件
  • 利用BufferedInputStream/BufferedOutputStream,实现利用缓冲区复制文件
  • 利用FileInputStream/FileOutputStream,实现字节批量复制文件(先放到字节数组)
public static void main(String[] args) throws Exception {
       //缓冲区复制文件需要 12680毫秒
   Test.copyFileByBuffer(new File("C:\\Users\\shitian\\Desktop\\作业\\创建于管理数据库.pptx"),new File("C:\\Users\\shitian\\Desktop\\作业\\1.pptx"));

       //单字节复制文件需要 13365毫秒
   Test.copyFileByByte(new File("C:\\Users\\shitian\\Desktop\\作业\\创建于管理数据库.pptx"),new File("C:\\Users\\shitian\\Desktop\\作业\\2.pptx"));

       //批量复制文件只需要 8毫秒
   Test.copyFile(new File("C:\\Users\\shitian\\Desktop\\作业\\创建于管理数据库.pptx"),new File("C:\\Users\\shitian\\Desktop\\作业\\3.pptx"));

单字节传输 < 缓冲区传输 < 批量传输

“不过这差的也太多了qwq”

2、字符流

1)编码问题

2)认识文本和文本文件

java的文本(char)是16位无符号整数,是字符的Unicode编码(双字节编码)

文件是byte byte byte … 的数据序列

文本文件是文本(char)序列按照某种编码方案(utf-8、utf-16be、gbk)序列化为byte的存储结果

3)字符流(Reader Writer)

操作的大部分是文本文件

字符的处理,一次处理一个字符

字符的底层仍然是基本的字节序列

字符流的基本实现:

​ InputStreamReader 完成byte流解析为char流,按照编码解析

​ OutputStreamWriter 提供char流到byte流,按照编码处理

/**
     * 字符流,批量读取、写入数据
     * @throws IOException
     */
    public static void charReader()throws IOException{
        FileInputStream fis = new FileInputStream("file/file.dat");
        InputStreamReader isr = new InputStreamReader(fis); //可以设置编码,默认项目编码
        FileOutputStream out = new FileOutputStream("file/file2.dat");
        OutputStreamWriter osw = new OutputStreamWriter(out);
//        int c;
//        while((c=isr.read())!=-1){   //单字符读取
//            System.out.print((char) c);
//        }
        char[] buffer = new char[8*1024];
        int c;
        //批量读取,放入buffer字符数组,从第0个位置开始放,最多放.length个
        //返回读到字符的个数
        while((c=isr.read(buffer,0,buffer.length))!=-1){
            String s = new String(buffer,0,c);
            System.out.print(s);
            osw.write(buffer,0,c);
            osw.flush();

        }
        isr.close();
        osw.close();
    }

4)字符流(FileWriter FileReader)

public static void charFileReader()throws IOException{
        FileReader fr = new FileReader("file/file.dat");
        FileWriter fw = new FileWriter("file/file2.dat");//不提供编码参数,不同编码使用StreamWriter
        //想要追加写入文件内容,添加一个boolean参数,true为追加,默认为false
        //FileWriter fw = new FileWriter("file/file.dat",true);
        char[] buffer =new char[8*1024];
        int c;
        while((c=fr.read(buffer,0,buffer.length))!=-1){
            String s= new String(buffer,0,c);
            System.out.print(s);
            fw.write(buffer,0,c);
            fw.flush();
        }
        fw.close();
        fr.close();
    }

5)字符流的过滤器

对字符流加过滤,使得字符流有更强大的功能

  • BufferedReader—> 一次读一行
  • BufferedWriter/PrintWriter —> 写一行
public static void charBufferedReader()throws IOException{
    BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("file/file.dat")));
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("file/file2.dat")));
    //PrintWriter pw = new PrintWriter("file/file.dat");  //使用相对简单

    String line;
    while((line=br.readLine())!=null){
        System.out.println(line);//一次读一行不识别换行符(需要我们主动换行)
        bw.write(line);//.append()方法追加内容
        bw.newLine(); //读一行后换行
        bw.flush();
        //pw.println(line); //可以直接换行
        //pw.flush();
    }
    bw.close();
    br.close();
    //pw.close();
}

六、反序列化

1、反序列化的基本操作

对象的序列化就是将Object转换为byte序列,反之叫对象的反序列化

1)序列化流(ObjectOutputStream),是过滤流 —— WriteObject

​ 反序列化流(ObjectInputStream)—— ReadObject

2)序列化接口(Serializable)

​ 对象必须实现序列化接口,才能进行序列化,否则会出现异常

​ 这个接口没有任何方法,只是一个标准

3)transient 关键字,修饰一个元素之后,该元素不会进行jvm默认的序列化,但是可以自己完成这个元素的序列化

​ 以及两个方法签名:(参考 ArrayList 源码中的两个方法签名http://www.hollischuang.com/archives/1140)

private void writeObject(java.io.ObjectOutputStream s)  //方法签名,可以自己自定义序列化的过程
        throws java.io.IOException {}
private void readObject(java.io.ObjectInputStream s)  
            throws java.io.IOException, ClassNotFoundException {}

问题:为什么类中定义了方法签名,就能自定义序列化的过程?

解答:

在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。

如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。

用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

简而言之,自己类中定义的这些方法签名,序列化时jvm就试图调用这些方法,从而进行自定义操作

public static void main(String[] args) throws Exception {
        //1、对象序列化
//        String file = "file/file.dat";
//
//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
//
//        ClassDemo2 stu = new ClassDemo2("jack",99,"202001");
//        oos.writeObject(stu);
//        oos.flush();
//        oos.close();
    //2、对象反序列化
        String file2 = "file/file.dat";   //对该文件内容进行反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file2));

        ClassDemo2 stu2 = (ClassDemo2) ois.readObject();  //强制类型转换
        System.out.println(stu2.toString());  //ClassDemo2{name='jack', score=99, num='202001'}
        ois.close();
    }
//类中自己定义的方法签名
private void writeObject(java.io.ObjectOutputStream s)  //方法签名,可以自己自定义序列化的过程
            throws java.io.IOException {
        s.defaultWriteObject();  //把虚拟机能默认序列化的元素进行序列化
        s.writeObject(num);  //自己完成序列化的操作
    }

    private void readObject(java.io.ObjectInputStream s)   //方法签名
            throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();  //把jvm默认能反序列化的元素进行反序列化
        this.num=(String) s.readObject();  //强制类型转换,因为num为String类型
    }

2、序列化中子父类构造函数问题

一个类实现了序列化接口,其子类都能进行序列化操作

对象被序列化时,会调用该类及其父类的构造方法

对象被反序列化时,如果父类实现了序列化接口,则不会调用构造方法;反之,父类没有实现,则会调用

public static void main(String[] args) throws Exception {
        //反序列化类
//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file/file.dat"));
//        Foo2 foo2 = new Foo2();
//        oos.writeObject(foo2); //反序列化
//        oos.flush();
//        oos.close();

        //反序列化时,是否递归调用构造函数
//        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file/file.dat"));
//        Foo2 foo2 = (Foo2)ois.readObject();
//        System.out.println(foo2);
//        ois.close();

//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file/file.dat"));
//        Bar3 bar3 = new Bar3();
//        oos.writeObject(bar3); //反序列化
//        oos.flush();
//        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file/file.dat"));
        Bar3 bar3 = (Bar3)ois.readObject();
        System.out.println(bar3);
        ois.close();
    }
}

/**
 * 一个类实现了序列化接口,那么其子类都可以进行序列化
 */
class Foo implements Serializable{
    public Foo(){
        System.out.println("foo");
    }
}

class Foo2 extends Foo{
    public Foo2() {
        System.out.println("foo2");
    }
}


class Bar{
    public Bar() {
        System.out.println("bar");
    }
}

class Bar2 extends Bar implements Serializable{  //子类实现序列化接口,父类没有
    public Bar2() {
        System.out.println("bar2");
    }
}

class Bar3 extends Bar2{
    public Bar3() {
        System.out.println("bar3");
    }
}