上帝视角学JAVA- 基础17-IO流【2021-09-03】

1、IO流

用于处理设备之间的数据传输。不仅仅是内存到硬盘,还包括不同设备之间通过网络进行传输。涉及到输入输出就需要使用IO流

I input 输入

O output 输出

输入输出是一个相对概念。

以内存为中心,内存到硬盘就是输出、硬盘到内存就是输入。

以硬盘为中心,内存到硬盘就是输入,硬盘到内存就是输出。

JAVA写的是程序,程序运行在内存当中,因此,JAVA 是以内存为中心谈输入与输出。

外界内容到内存中,叫做输入。

内存到外界,叫做输出。

流的分类:

按照操作数据的单位不同,分为字节流,一个字节8位。字符流,16位。

按照流向分为:输入流、输出流

按照流的角色:节点流:字节作用在文件上;处理流:在流的基础上,再包装一下。包装流的流

 

 

JAVA中对IO的操作主要就是上面这4个抽象基类派生而出的。

字节流可以处理一切数据,为什么还需要字符流呢?因为文本类数据处理非常常见,使用字符流一次处理2个字节,更快。

 

 

访问文件有4个典型的节点流。

下面的都是处理流。

以Stream结尾的都是字节流,其他以Reader 或者Writer 结尾的都是字符流。

上图中蓝色背景的流比较常用,需要重点掌握。

1.1 节点流

1.1.1 FileReader 读文件字符流

读文件字符流实际是 内存加载文件,即文件输入到内存中。属于输入流的一种。

使用这个流一般用来操作文本等字符内容。要求文件一定要存在。不然会有文件找不到的异常。

有一个 hello.txt 文件。内容是 IO流测试文件

// 1、实例化File 类的对象
File file = new File("hello.txt");
// 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
FileReader fr = new FileReader(file);
// 3、数据的读入
// read 方法返回读入的一个字符,如果达到文件末尾,就返回-1
int data = fr.read();
while (data != -1){
    System.out.print((char)data);
    data = fr.read();
}
// 4、流的关闭操作
fr.close();
​
// 输出   IO流测试文件

代码优化: 看起来更简洁一些

// 1、实例化File 类的对象
File file = new File("hello.txt");
// 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
FileReader fr = new FileReader(file);
// 3、数据的读入
// read 方法返回读入的一个字符,如果达到文件末尾,就返回-1
int data;
while ((data = fr.read()) != -1) {
    System.out.print((char) data);
}
// 4、流的关闭操作
fr.close();
// 输出   IO流测试文件

代码优化,使用try catch final 处理异常。为了保证不管是否有异常,流都要关闭

FileReader fr = null;
try {
    // 1、实例化File 类的对象
    File file = new File("hello.txt");
    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    fr = new FileReader(file);
    // 3、数据的读入
    // read 方法返回读入的一个字符,如果达到文件末尾,就返回-1
    int data;
    while ((data = fr.read()) != -1) {
        System.out.print((char) data);
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    // 4、流的关闭操作
    try {
       if ( fr != null){
             fr.close();
          }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
// 输出  IO流测试文件

上述代码中,使用了 read 方法进行读取。下面对读取操作进行优化。因为read是一个字符一个字符进行读取,速度慢。

使用read的重载方法,一次性读一个数组。加快速度。这个方法返回 真实读取的个数。 读取的数据保存到了数组中!

注意,如果使用 for 来遍历读取的数据,下面这种写法是错误的!

FileReader fr = null;
try {
    // 1、实例化File 类的对象
    File file = new File("hello.txt");
    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    fr = new FileReader(file);
    // 3、数据的读入
    // read 方法返回读入的一个字符,如果达到文件末尾,就返回-1
    int len;
    char[] buf = new char[4];
    while ((len = fr.read(buf)) != -1) {
        // i 的条件这里有错误,不是 buf.length
        for (int i = 0; i < buf.length; i++) {
            System.out.print(buf[i]);
        }
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 4、流的关闭操作
    try {
        if (fr != null) {
            fr.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
// 输出
//IO流测试文件
//文件

用 增强for 也是不对的。

FileReader fr = null;
try {
    // 1、实例化File 类的对象
    File file = new File("hello.txt");
    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    fr = new FileReader(file);
    // 3、数据的读入
    // read 方法返回读入的一个字符,如果达到文件末尾,就返回-1
    int data;
    // 使用一个数组来接收读取的数据,一次性读数组长度这么多
    char[] buf = new char[2];
    while ((data = fr.read(buf)) != -1) {
        for (char c : buf) {
            System.out.print(c);
        }
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 4、流的关闭操作
    try {
        if (fr != null) {
            fr.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
//输出
//IO流测试文件
//文件

错误的原因是因为 装内容的 数组是重复利用的,当第一次读取4个字符时,读取了 "IO流测试", 刚好放满了 数组。还没有读完,继续读取

因为只有 "文件",因此 "文件" 会覆盖 之前数组的前面的2个位置,"IO流",就变成了 "文件文件"

上面是全量输出 整个buf。实际上需要按需输出。

注:英文是占用1个字节,所以 IO 占用2个字节,而1个汉字是占用2个字节的【utf-8是3个字节,不指定编码默认是2个字节】。一个char 类型是2个字节。所以一个char类型的数组2个位置是4个字节。

正确的代码:

FileReader fr = null;
try {
    // 1、实例化File 类的对象
    File file = new File("hello.txt");
    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    fr = new FileReader(file);
    // 3、数据的读入
    // read 方法返回读入的一个字符,如果达到文件末尾,就返回-1
    int len;
    char[] buf = new char[4];
    while ((len = fr.read(buf)) != -1) {
        // i 的条件应该是 len 而不是 buf的长度
        for (int i = 0; i < len; i++) {
            System.out.print(buf[i]);
        }
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 4、流的关闭操作
    try {
        if (fr != null) {
            fr.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
//输出
//IO流测试文件

还可以这样写:使用String 的 String(char value[], int offset, int count) 构造方法

FileReader fr = null;
try {
    // 1、实例化File 类的对象
    File file = new File("hello.txt");
    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    fr = new FileReader(file);
    // 3、数据的读入
    // read 方法返回读入的一个字符,如果达到文件末尾,就返回-1
    int len;
    char[] buf = new char[4];
    while ((len = fr.read(buf)) != -1) {
        // String(char value[], int offset, int count)
        String s = new String(buf, 0, len);
        System.out.println(s);
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 4、流的关闭操作
    try {
        if (fr != null) {
            fr.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

1.1.2 FileWriter 写文件字符流

写文件字符流 是将内存中数据输出搭配硬盘中,属于输出流的一种。

文件字符输出流:写入文件到硬盘中

FileWriter fw = null;
try {
    // 1、实例化File 类的对象.   这个文件可以是不存在的!
    File file = new File("hello1.txt");
    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    fw = new FileWriter(file);
    // 3、数据的写出
    // write 方法 写出数据
    fw.write("FileWriter 文件字符流");
    fw.write("写出测试");
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 4、流的关闭操作
    try {
        if (fw != null) {
            fw.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
// hello1.txt 文件是不存在的,不存在的文件写出会自动生成这个文件,并写入内容到文件中
// 文件中的内容:FileWriter 文件字符流写出测试
// 可以看到,虽然分2次写入,但是并没有换行。换行 需要希尔换行符号 "\n"

如果是对已存在的文件进行写入呢?注意:对文件是写入,对java叫做从内存输出到文件!用的是输出流。

如果存在的情况非常简单,上述代码再执行一遍就知道了。

结果文件内容还是:FileWriter 文件字符流写出测试

即,如果文件存在,会先清空文件内容,再重新写入数据。

事实上,有时候我们需要追加内容。那些在new FileWriter 时,调用重载的构造器。

FileWriter fw = null;
try {
    // 1、实例化File 类的对象
    File file = new File("hello1.txt");
    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    // 第二个参数 为 是否要对文件进行追加,true代表追加在后面。false则表示清空重新写入
    fw = new FileWriter(file, true);
    // 3、数据的写出
    // write 方法 写出数据
    // "\n" 表示换行
    fw.write("\n FileWriter 文件字符流");
    fw.write("写出测试");
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 4、流的关闭操作
    try {
        if (fw != null) {
            fw.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
// 文件内容是: 
//FileWriter 文件字符流写出测试
//FileWriter 文件字符流写出测试

使用文件写和文件读就可以实现文件的复制了。

FileWriter fw = null;
FileReader fr = null;
try {
    // 1、实例化File 类的对象
    File src = new File("hello1.txt"); // 源文件
    File tar = new File("hello.txt");	// 目标文件
    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    fw = new FileWriter(tar);
    fr = new FileReader(src);
    // 3、数据的读与写
    char[] buf = new char[4];
    int len;
    while ((len = fr.read(buf))!=-1){
        // 将buf写入,从index=0 开始写,写len个
        // 这个就不是全部将buf写入,而是可以控制写那一部分。
        fw.write(buf,0, len);
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 4、流的关闭操作
    try {
        if (fw != null) {
            fw.close();
        }
        if (fr != null){
            fr.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

注意,文件字符流不能处理图片视频类数据。需要使用字节流处理

1.1.3 FileInputStream 、FileOutputStream

FileInputStream :输入文件字节流 ,将文件输入到内存中。

FileOutputStream: 输出文件字节流,将文件从内存输出到硬盘

FileInputStream fi = null;
try {
    // 1、实例化File 类的对象
    File src = new File("hello1.txt");

    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    fi = new FileInputStream(src);
    // 3、数据的读取。使用 byte型数组
    byte[] buf = new byte[4];
    int len;
    while ((len = fi.read(buf))!=-1){
        String s = new String(buf, 0, len);
        System.out.print(s);
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 4、流的关闭操作
    try {
        if (fi != null) {
            fi.close();
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}
// 输出
//FileWriter �����字符�����出测���
//FileWriter �����字符�����出测���

可以看到,使用输入文件字节流读取文本文本,出现了乱码。

什么是文本文件:常用的有 .txt、.java、.c、.cpp、.md、.csv、

非文本文件:常用的有:.jpg、.mp3、.mp4、.avi、.doc | .docx、.ppt、.pdf 等

但是对于非文本文件,就可以使用字节流。

实现对图片的复制。

以下面的图片为例

 

FileInputStream fi = null;
FileOutputStream fo = null;
try {
    // 1、实例化File 类的对象
    File src = new File("java.jpg");
    File tar = new File("newJava.jpg");
    // 2、选择合适的流,传入File对象。File对象的作用是告诉流要操作哪一个文件。
    fi = new FileInputStream(src);
    fo = new FileOutputStream(tar);
    // 3、数据的读与写
    byte[] buf = new byte[4];
    int len;
    while ((len = fi.read(buf))!=-1){
        fo.write(buf, 0, len);
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 4、流的关闭操作
    try {
        if (fi != null) {
            fi.close();
        }
        if (fo != null) {
            fo.close();
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}

结果会生成新的图片文件 newJava.jpg 与源文件一样。

可以看到,基本和文件的复制没有什么区别。

文件复制使用的是char数组,而字节流则是使用 byte数组。

事实上,使用字节流也是可以直接复制文本文件的。因为以什么方式读取,就以什么方式写入,直接以字节形式复制,不涉及到编码

但是字符流是不可以复制图片等数据的,因为会以编码的形式读取,再以编码的形式写入。改变了字节文件的编码。

还有一个文件,不管是字符数组,还是字节数组,数组的大小如何确定呢?

没有一个固定的数,一般以 1024 作为数组的大小。因为这个数组大了,速度快,但是消耗内存。小了省内存,但是速度慢。

前面,使用的都是 4,是因为文件都太小了。这也说明,这个数不是固定的。但是有常用的 1024 或者 1024的倍数。

1.2 缓冲流

以上四个节点流是基本的流,用的较少。一般使用速度更快的 缓存流。

缓存流也有四个,分别是对节点流的包装。

BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter

包装流不能自接作用于文件对象上。需要接收对应的节点流对象。

提高速度的原因是:内部提供了一个缓冲区 默认大小是 8192 个字节大小。当达到这个大小时,才会进行flush操作。

flush是将缓冲区的数据写入到文件并清空缓冲区。

1.2.1 BufferedInputStream、BufferedOutputStream

输入缓冲字节流、输出缓冲字节流。

// 指定文件
File src = new File("java.jpg");
File tar = new File("buff_java.jpg");
// 节点流
FileInputStream fi = null;
FileOutputStream fo = null;
// 缓冲流
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
    // 节点流,直接接收文件对象
    fi = new FileInputStream(src);
    fo = new FileOutputStream(tar);
    // 包装流,接收对应的节点流对象
    bis = new BufferedInputStream(fi);
    bos = new BufferedOutputStream(fo);

    // 文件的读写
    byte[] buf = new byte[1024];
    int len;
    while ((len = bis.read(buf))!=-1){
        bos.write(buf, 0, len);
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    try {
        // 先关闭外层流,再关闭内存流。同级别流无顺序。
        // 但是关闭外层流时,内层流会自动关闭,因此内层流的关闭可以省略。
        if (bos != null){
            bos.close();
        }
        if (bis != null){
            bis.close();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

上面是 使用 缓冲字节流对图片的复制代码。使用时基本与节点流一致。

只是需要注意 流的关闭顺序。

代码优化: 节点流是内层流,无序手动关闭。可使用匿名对象方式进行优化

// 指定文件
File src = new File("java.jpg");
File tar = new File("buff_java.jpg");
// 缓冲流
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
    bis = new BufferedInputStream(new FileInputStream(src));
    bos = new BufferedOutputStream(new FileOutputStream(tar));

    // 文件的读写
    byte[] buf = new byte[1024];
    int len;
    while ((len = bis.read(buf))!=-1){
        bos.write(buf, 0, len);
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    try {
        // 先关闭外层流,再关闭内存流。同级别流无顺序。
        // 但是关闭外层流时,内层流会自动关闭,因此内层流的关闭可以省略。
        if (bos != null){
            bos.close();
        }
        if (bis != null){
            bis.close();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

1.2.2 BufferedReader、BufferedWriter

输入缓冲字符流、输出缓冲字符流。

这2个流的使用与字节流类似,只不过是适用于文本数据。

// 指定文件
File src = new File("hello.txt");
File tar = new File("buff_hello.txt");
// 缓冲流
BufferedReader br = null;
BufferedWriter bw = null;
try {
    br = new BufferedReader(new FileReader(src));
    bw = new BufferedWriter(new FileWriter(tar));

    // 文件的读写
    char[] buf = new char[1024];
    int len;
    while ((len = br.read(buf))!=-1){
        bw.write(buf, 0, len);
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    try {
        // 先关闭外层流,再关闭内存流。同级别流无顺序。
        // 但是关闭外层流时,内层流会自动关闭,因此内层流的关闭可以省略。
        if (bw != null){
            bw.close();
        }
        if (br != null){
            br.close();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

BufferedReader 多一个方法 readLine() 可以对文本进行一行一行的读

// 指定文件
File src = new File("hello.txt");
File tar = new File("buff_hello.txt");
// 缓冲流
BufferedReader br = null;
BufferedWriter bw = null;
try {
    br = new BufferedReader(new FileReader(src));
    bw = new BufferedWriter(new FileWriter(tar));

    // 文件的读写
    String data;
    // 注意:readLine 方法当读取到文件默认会返回null,并且 data 是 不包含换行符的
    while ((data = br.readLine())!=null){
        bw.write(data);
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    try {
        // 先关闭外层流,再关闭内存流。
        // 但是关闭外层流时,内层流会自动关闭,因此内层流的关闭可以省略。
        if (bw != null){
            bw.close();
        }
        if (br != null){
            br.close();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

使用这种方式,读取的行数据被去掉了换行符号。可以自己处理一下。

// 指定文件
File src = new File("hello.txt");
File tar = new File("buff_hello.txt");
// 缓冲流
BufferedReader br = null;
BufferedWriter bw = null;
try {
    br = new BufferedReader(new FileReader(src));
    bw = new BufferedWriter(new FileWriter(tar));

    // 文件的读写
    String data;
    // 注意:readLine 方法当读取到文件默认会返回null,并且 data 是 不包含换行符的
    while ((data = br.readLine())!=null){
        bw.write(data);
        // 可以自己手动写一个换行符;  使用 bw.write(data+"\n"); 也是可以的
        bw.newLine();
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    try {
        // 先关闭外层流,再关闭内存流。
        // 但是关闭外层流时,内层流会自动关闭,因此内层流的关闭可以省略。
        if (bw != null){
            bw.close();
        }
        if (br != null){
            br.close();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

1.3 转换流

提供字节流与字符流之间的转换。

转换流本身都是属于字符流:

字节到字符:InputStreamReader 输入字节流转输入字符流。 在加载到内存时,将字节转字符方便显示。

字符到字节:OutputStreamWriter 输出字符流转输出字节流。在输出到硬盘时,将字符以字节形式写入。

1.3.1 InputStreamReader 字节转字符

// 指定文件
File src = new File("hello1.txt");
// 缓冲流
InputStreamReader isr = null;
try {
    // 接收的是 字节流 以及 字符集,自己本身是字符流。实现字节输入流到字符输入流的转换
    isr = new InputStreamReader(new FileInputStream(src), "UTF-8");
    // 文件的读写
    char[] buf = new char[1024];
    int len;
    // 注意:readLine 方法当读取到文件默认会返回null,并且 data 是 不包含换行符的
    while ((len = isr.read(buf))!= -1){
        String s = new String(buf, 0, len);
        System.out.print(s);
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    try {
        if (isr != null){
            isr.close();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}
// 这里是输出到控制台 不会乱码

1.3.2 OutputStreamWriter 字符转字节

例子,将utf8编码的文本文件以gbk输出

// 指定文件
File src = new File("hello1.txt");
File tar = new File("gbk_hello.txt");
// 缓冲流
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
    // 接收的是 字节流 以及 字符集,自己本身是字符流。
    isr = new InputStreamReader(new FileInputStream(src), "UTF-8");
    // 接收的是 字节流 以及 字符集,自己本身是字符流。
    osw = new OutputStreamWriter(new FileOutputStream(tar), "GBK");
    // 文件的读写
    char[] buf = new char[1024];
    int len;
    while ((len = isr.read(buf))!= -1){
        osw.write(buf,0, len);
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    try {
        if (isr != null){
            isr.close();
        }
        if (osw != null){
            osw.close();
        }
    }catch (Exception e){
        e.printStackTrace();
    }
}

1.4 字符集

ASCII :美国标注信息交换码 用1个字节的7位表示

ISO8859-1: 拉丁码表,是欧洲码表 用1个字节的8位表示

GB2312: 中文码表,最多2个字节 编码。 看最高位是0 表示是1个字节表示的,是1表示需要2个字节表示

BGK:中国的中文码表升级,融合了更多的中文字符,最多2个字节。也是看最高位区分

Unicode:国际标准码,融合了目前人类使用的所有字符。为每一个字符都分配唯一的字符码,所有的字符都用2个字节表示。

Unicode不够完美,因为计算机不知道如何区分Unicode和ASCII ,如果采用和gbk或者GB2312使用最高位来区分,就无法表示很多字符。直到互联网出现,面向传输的UTF标注出现了。

每次8个位传输数据就是utf-8,每次16位就是utf-16

因此,Unicode知识定义了一个庞大的、全球通用的字符集,具体如何存储,取决于编码方案。就是一个标准。

而 utf-8或者utf-16 是这个标准的不同的实现。

utf-8:变长的编码方式,用1-4个字节表示一个字符。

 

 

什么是ANSI编码呢? ANSI是美国国家标准学会

指的是平台的默认编码,在英文操作系统中,就是ISO-8859-1,中文系统就是GBK。

而Unicode是一种编码规范,utf系列是该规范的实现。

1.5 标准的输入、输出流、打印流

标准输出流 很早就见过了就是

System.out.println();
// System 类对 in的 定义,可以看到是一个 输入字节流
public final static InputStream in = null;

System.out 就是标准的输出流,默认从控制台输出

// System 类对 out的 定义,可以看到是一个 PrintStream 打印流,也是一个字节流
public final static PrintStream out = null;

System.in 标准的输入流,默认从键盘输入

可以通过System类的 setIn、setOut 方法重新指定输入或者输出的位置。

// 使用System.in 做类似于Scanner类的事。从键盘接收数据并打印。
//idea的 try catch 快捷键  ctrl+ alt + t
BufferedReader br = null;
try {
    // 使用了 字节转字符流,System.in 是字节流。最后传给缓冲流
    br = new BufferedReader(new InputStreamReader(System.in));
    while (true){
        System.out.println("请输入字符串,输入'e'或者'exit' 退出");
        String data = br.readLine();
        if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)){
            System.out.println("退出!");
            break;
        }
        String s = data.toUpperCase(Locale.ROOT);
        System.out.println(s);
    }
    br.close();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (br != null){
            br.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

打印流 有2个 PrintStream、PrintWriter 一个是字节打印流,一个是字符打印流

PrintStream ps = null;
try {
    FileOutputStream fos = new FileOutputStream("ascii.txt");
    // 字节打印流
    ps = new PrintStream(fos, true);
    // 设置标注输出为 字节打印流而不再是 控制台
    System.setOut(ps);
    for (int i = 0; i < 255; i++) {
        // 输出 字符,由于改成向 ps对象输出了,即向文件写入字符
        System.out.print((char) i);
        // 每10个
        if (i % 10 == 0){
            // 向文件写入 换行
            System.out.println();
        }
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (ps != null) {
        ps.close();
    }
}

以上的代码是将 askii码对应的字符输出到文件"ascii.txt"中。

可以看到使用 FileOutputStream 并没有传入一个文件对象,而是直接传入了路径。是的,这个也是FileOutputStream的一个构造方法。

1.6 数据流

有2个, DataInputStream、DataOutputStream

为了方便的操作java的基本数据类型个String类型的数据。可以使用数据流

DataInputStream 包装在 InputStream 的子类流上

DataOutputStream 包装在 OutputStream 的子类流上

DataInputStream有如下的一些常用方法:

readBoolean、readChar、readDouble、readLong、readUTF、readByte、reeadFloat、readShort、readInt、readFully(byte[] b)

DataOutputStream有如下的一些常用方法:

writeBoolean、writeChar、writeDouble、writeLong、writeUTF、writeByte、reeadFloat、writeShort、writeInt、writeFully(byte[] b)

写文件

DataOutputStream dos = null;
try {
    dos = new DataOutputStream(new FileOutputStream("hello.txt"));
    dos.writeUTF("xxx");
    dos.writeInt(23);
    dos.flush();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (dos != null) {
            dos.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

读文件:主要先写的要先读

DataInputStream dis = null;
try {
    dis = new DataInputStream(new FileInputStream("hello.txt"));
    // 一定要先写的先读
    String s = dis.readUTF();
    int i = dis.readInt();
    System.out.println(i);
    System.out.println(s);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (dis != null) {
            dis.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

1.7 对象流 【序列化】

有2个:ObjectOutputStream 、ObjectInputStream

对象流也可以用来存取基本数据类型数据,但是它还可以用来存取对象类型的数据即 class 类型

可以将对象写入到数据源当中,也能把对象从数据源还原出来。

  • 序列化: 用ObjectOutputStream 类保存基本类型或者对象的机制

  • 反序列化:用ObjectInputStream 类读取基本类型数据或对象的机制

ObjectOutputStream 和ObjectInputStream 不能序列化 static 和 transient 修饰的成员变量。

原因是 static 修饰的不属于对象,而属于 类或者接口。

而 transient 作用就是我们需要某些变量不要序列化,就用 transient 修饰。transient 是专门解决这个需求而出现的关键字,所以被transient 修饰的不能序列化。

序列化机制是允许将内存中的 java对象 转换为与平台无关的二进制流,从而允许将这种二进制流持久的保存到磁盘上,当然你也可以将这种二进制流通过网络传输给另一个接收者。当其他人接收之后,可以恢复为原来的java对象。

如果要使某个对象支持这种序列化机制,是有条件的。即 这个类必须要实现 2个接口之一。

Serializable 或者 Externalizable

以String 为例,String类实现了 Serializable 接口。

ObjectOutputStream oos = null;
try {
    oos = new ObjectOutputStream(new FileOutputStream("object.data"));
    // 写对象
    oos.writeObject(new String("我爱你"));
    // 刷新
    oos.flush();
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if ( oos != null){
            oos.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

执行完上述代码,会生成一个 object.data 文件。 这就是序列化,将String 对象写入到了 硬盘中的 object.data 文件内。

注意。这个文件不能直接打开,只能通过反序列化进行读取。

ObjectInputStream ois = null;
try {
    ois = new ObjectInputStream(new FileInputStream("object.data"));
    // 读对象
    Object o = ois.readObject();
    // 已知是 String 类型,可以进行强转
    String str = (String) o;
    System.out.println(str); // 我爱你
} catch (Exception e) {
    e.printStackTrace();
} finally {
    try {
        if ( ois != null){
            ois.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

以上是反序列化过程,读取 object.data 文件中的 String 对象。

如果是自定义的对象,必须要 实现 Serializable 或者 Externalizable 接口。

// Serializable 接口源码
public interface Serializable {
}

可以看到,Serializable 是一个空接口。里面即没有属性也没有方法。这是一种标识接口。即标记这个类可以干什么事的。

除了实现这个接口还不行,还需要提供一个 【不是必须,但是强烈建议提供】

public static final long serialVersionUID = xxx;

xxx 可以自己设置,是一个long型整数。

提供这个整数,主要是为了防止很多对象在反序列化时不会乱套。

public class Cat implements Serializable {
    
    public static final long serialVersionUID = 1L;
}

上面这个Cat 类就是可序列化的了。

其实还不够,还要求 Cat 类的所有属性是可序列化的。这里的Cat吗,没有属性,因此它就是可序列化的。

如果Cat里面有个 属性不是 static、也没有被transient 修饰,就必须也是可序列化的。

public class Cat implements Comparable, Serializable {

    public static final long serialVersionUID = 1L;
    
    private Car car;
    }

看这个例子,Cat 里面有个私有属性 是Car 类型的car,那么Car也必须满足 是可序列化的。

  • 实现 Serializable 或者 Externalizable 接口

  • 提供这个 public static final long serialVersionUID = xxx;

  • 属性必须也是可序列化的

  • 基本数据类型都是可序列化的

1.7.1 serialVersionUID

这个long型值用来表明类的不同版本间的兼容性。其目的是以序列化对象进行版本控制,有关各版本反序列化是否兼容。

如果类没有显示定义这个静态变量,它的值是java运行时环境根据类的内部细节自动生成。若类的实例变量做了修改,

serialVersionUID 可能发生变化。因此需要显示声明。

什么时候会用到呢?

当接收了对象流之后,进行反序列化之前,会进行 本地类的 serialVersionUID 与接收的 serialVersionUID 对比,如果相同就认为是可以反序列化的,否则认为 版本不一致,不能反序列化。

1.8 随机存取文件流

RandomAccessFile 类

这个类 声明在java.io 包下,但是是直接继承的Object 类

并且实现了DataInput、DataOutput 这2个接口。意味着这个类可读可写。

public class RandomAccessFile implements DataOutput, DataInput, Closeable {}

上面是这个类的类头,没有继承其他类,说明是直接继承的Object类,还实现了 Closeable 接口。

这个类支持随机访问,即可以跳转到文件的任意位置进行 读写操作。

  • 支持只访问文件的部分内容

  • 可以向已存在的文件后追加内容

这个类有一个记录指针,用于记录当前位置。并且这个类是可以自由移动这个记录指针。

getFilePointer方法 获取位置、seek方法移动位置

// 获取文件记录指针的当前位置
public native long getFilePointer() throws IOException;
// 将文件记录指针定位到pos位置
public void seek(long pos) throws IOException {
        if (pos < 0) {
            throw new IOException("Negative seek offset");
        } else {
            seek0(pos);
        }
    }

复制图片测试

/** 模式
         * r: 只读 。 如果文件不存在会报错!
         * rw: 读写。 文件不存在会创建文件。如果存在,写内容会覆盖,写了多少覆盖多少。
         * rws:读写,同步文件内容和元数据更新。文件不存在会创建文件。
         * rwd:读写,同步文件内容的更新。文件不存在会创建文件。
        * */
RandomAccessFile r = null;
RandomAccessFile rw = null;
try {
    r = new RandomAccessFile("java.jpg", "r");
    rw = new RandomAccessFile("rw_java.jpg", "rw");
    byte[] buf = new byte[1024];
    int len;
    while ((len=r.read(buf))!=-1){
        rw.write(buf, 0, len);
    }
} catch (Exception e) {
    e.printStackTrace();
}finally {
    try {
        if (r != null){
            r.close();
        }
        if (rw != null){
            rw.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

文件的插入覆盖

  RandomAccessFile rw = null;
        try {
             rw = new RandomAccessFile("hello.txt", "rw");
             // 移动位置到 角标为 3,即第4个位置
             rw.seek(3);
            // 写数据,需要些 字节类型的数据。
             rw.write("xgz".getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
               if (rw != null){
                   rw.close();
               }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
// 原始文件内容:FileWriter 文件字符流写出测试
// 执行结果:Filxgziter 文件字符流写出测试

可以看到,write 方法就是覆盖,通过seek方法移动了位置。现在是从 位置3开始执行write方法。写了3个字符xgz,就从位置3开始 覆盖了3个。

如果想要追加在末尾,可以实体seek方法移动到文件末尾。使用File对象的length方法获取文件长度。

如何实现一种插入效果,而不是覆盖呢?

RandomAccessFile rw = null;
        try {
             rw = new RandomAccessFile("hello.txt", "rw");
             // 移动位置到 角标为 3,即第4个位置
             rw.seek(3);
             // 用一个String来存。使用 StringBuilder 类来构造长的字符串,为了避免扩容,可以用文件长度来初始化。
             StringBuilder sb = new StringBuilder((int) new File("hello.txt").length());
             byte[] buf = new byte[1024];
             int len;
             while ((len=rw.read(buf))!=-1){
                sb.append(new String(buf, 0, len));
             }
             rw.seek(3);
             rw.write(("xgz"+ sb).getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
               if (rw != null){
                   rw.close();
               }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

采用的方法是 先移动位置,再将后面的数据读取保存。然后再移动位置,将需要的数据和保存的数据一起写入即可。

可以看到,插入操作很慢的。我们一般是使用追加操作。

使用这个类可以实现多线程断点下载。

下载前会建立2个临时文件,一个与下载文件大小相同的空文件,另一个是记录文件指针的位置文件。每次暂停都会保存上一次的指针,然后断点下载时,会继续从上一次的地方进行下载,从而实现断点或上传的功能。

2、NIO

NIo 可以是叫new io 新的Io、或者叫 Non-Blocking Io 非阻塞IO。

是从JDK 4.0 开始引入的一套全新的IO api可以替换标准的java io api。

NIO 与原来的IO具有相同的作用和目的。但是使用方式完全不同。Nio是支持面向缓冲区的,基于通道的IO操作,而io是面向流的。

NIO会以更加高效的方式进行文件的读写操作。

NIO有2套,一套是针对标准输入输出的NIO、另一套是网络编程NIO

NIO2 是jdk7对Nio的增强,为了区分4到6的Nio,将7以后的NIo称为Nio2

2.1 Path、Paths、Files 核心API

早期的java只提供了一个File类来访问文件系统,但是File类功能有限,提供的方法性能也不高,大多数方法仅返回成功或失败,不会返回失败的原因。

NIO2 为了弥补不足,引入了Path 接口。代表与平台无关的路径。描述了目录结构中文件的位置。Path可以看做是File的升级版。

Path也可以是不存在的文件路径。

以前这样写:

File file = new File("hello.txt");

现在这样写:

Path path = Paths.get("hello.txt");

Nio2 提供了 Files、Paths工具类、Files包含了大量的静态工具方法来操作文件;Paths则包含了2个返回Path的静态工厂方法。

上面的例子就算使用Paths类的静态方法get 来获取Path接口的实现类对象。get方法还有一个重载方法,接收的是URI对象参数。

public static Path get(String first, String... more) {
    return FileSystems.getDefault().getPath(first, more);
}

public static Path get(URI uri) {
    String scheme =  uri.getScheme();
    if (scheme == null)
        throw new IllegalArgumentException("Missing scheme");
    if (scheme.equalsIgnoreCase("file"))
        return FileSystems.getDefault().provider().getPath(uri);
    for (FileSystemProvider provider: FileSystemProvider.installedProviders()) {
        if (provider.getScheme().equalsIgnoreCase(scheme)) {
            return provider.getPath(uri);
        }
    }
    throw new FileSystemNotFoundException("Provider \"" + scheme + "\" not installed");
}

这里只是简单介绍,有机会再详细说明。

2.2 第三方jar 包的使用

使用 apache.commons.io 包提供的api进行文件操作。

先引入Maven依赖:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.9.0</version>
</dependency>

// 导包
import org.apache.commons.io.FileUtils;
File src = new File("java.jpg");
File tar = new File("apa_java.jpg");
try {
    // 使用
    FileUtils.copyFile(src, tar);
} catch (IOException e) {
    e.printStackTrace();
}

上面是复制文件的一个例子。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值