Java基础系列:IO流

小伙伴们,我们认识一下。

俗世游子:专注技术研究的程序猿

在前面的几篇文章中,我们分别介绍了

LinkedHashMap继承自HashMap,我们在这里不做过多介绍

根据之前的结构图,我们还缺少Set没有介绍。

说实在的,这里我不打算和详细的来介绍Set集合,介绍Set也只是调用其api方法。我们先快速过一下。

Set

Set是不包含重复元素的集合,底层基于Map实现

这也是为什么不介绍Set的原因

基础

Set集合中,其源码实现直接依赖Map来实现,Set添加的元素,通过MapKey来存储,Value部分是通过Object对象来填充,实现方式如下:

HashSet为例

private static final Object PRESENT = new Object();

public HashSet() {
    map = new HashMap<>();
}

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

可以看到上面代码的方式

其分类关系如下

Set子集实现
HashSetHashMap
TreeSetTreeMap
LinkedHashSetLinkedHashMap

根据上面简单的分析,我们也可以总结出Set的一些特点:

  • Set是基于Map来实现的,将元素存储在Map的Key中,Value部分采用Object对象进行填充
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
  • Set元素不可重复,唯一,且无序

  • TreeSet中可以通过设置排序器来对容器数据进行排序

public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}
  • HashSet在设置初始大小的时候其实也就是在对HashMap设置,所以这里推荐设置2的N次幂的数
public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}

这里我们引出一个方式叫做:组合方式

组合方式

在java中,基于基础类然后再其类中进行创新操作的方式,有两种方式:

  • 继承基础类(父类):在子类中重写其父类的方法。
  • 组合基础类:也就是说,在子类中,通过调用基础类的方法,来达到复用基础类的方式。在Set中,就是使用的组合基础类的方式

对比以上两种方式,组合基础类的优点:

  • 继承表示父子类是同一个类型,而 Set 和 Map 本来就是想表达两种类型,所以采用继承不合适,而且 Java 语法限制,子类只能继承一个父类,后续难以扩展。
  • 组合更加灵活,可以任意的组合现有的基础类,并且可以在基础类方法的基础上进行扩展、编排等,而且方法命名可以任意命名,无需和基础类的方法名称保持一致。

IO

本文章主要还是要介绍一下Java中的IO。IO也就是输入/输出;Java中的IO操作本质上是对磁盘文件的操作

这种方式属于磁盘IO,当然,还存在一种叫做:网络IO

可以说我们开发的程序,系统瓶颈主要就出现在这两块IO上,我们在开发中也是针对这两块在代码层面来进行优化的

从整体划分上,可以分为三大类

  • BIO
  • NIO
  • AIO

BIO也就是我们接下来要了解的IO类型,称为阻塞式IO

NIO可以叫做New IO,是一种非阻塞式的IO

NIO的执行效率比BIO要高一点

我们先聊基础的,在Netty系列章节中,我们再介绍NIO的相关使用

AIO暂时不做介绍,后面介绍到网络再说

那么我们就要先来了解下什么是文件

文件

文件可以认为是相关记录或放在一起的数据的集合。像我们电脑中的图片,视频等都是属于文件,需要采用相对应的软件才能打开,如果我们都是使用纯文本打开的话那么就是一堆乱码,其实我们也知道这是属于二进制数据

也就是说二进制数据通过一定的编码规则进行排序,然后通过相对应的软件打开,我们就可以很明白的查看当前文件

那么这些数据是存储存储在哪里呢?对应到硬件上可以存储在U盘,硬盘等硬件系统中。

大家肯定都见过的

那么再聊的深入一点,如何从存储盘中读取文件内容

我们可以这么来理解:

在电脑硬盘中存在一圈圈的磁道,这些磁道中存储的就是文件对应的数据,磁盘通过磁头来读写磁道,而磁头连接到一个传动臂,而传动臂将磁头定位到磁道的过程称之为寻址

盘片视图

该图来源:《深入理解计算机系统》第6章存储技术

但是我们要注意一点:硬件在读取数据的时候是有读取单位的

现在打个比方:

比如现在我们已经有了1G的数据,我们要寻找其中1个字节的数据,这个磁壁是怎么读取的?

我们应该明白一点,肯定不是1个字节1个字节的找,操作系统在读取的时候是已为读取单位(4K大小)

所以哪怕我们要读取1个字节的数据,但是磁盘读取的时候也会读取一页大小(4K),然后会将从磁盘读取到的数据读到内存,但是读到内存的时候又会有一个问题:

从磁盘读取的时候是4K,但是读到内存的时候不一定是4K,这和硬件和操作系统是相关的,有可能是4K或者是4K的整数倍,

但是能确定的是:读数据的时候一定是按照 为单位来读取

我们通过具体文件来看上述的说明:

文件大小

因此在硬件设备中存在一个概念:磁盘预读

就是说:每次不管需要多少字节的数据,都会将页的整数倍的数据读取进来

在这里同时又隐含一个著名的原理:局部性原理

程序数据的访问都有聚集成群的,也就是说所有数据都是聚集放在一起的。因此磁盘预读可以将整块的数据读取进来,下次再要找类似数据的时候,就不需要去磁盘查找(类比缓存)

局部性原理分为两种不同形式:

  • 空间局部性

在一个具有良好空间局部性的程序中, 如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置

  • 时间局部性

在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来再被多次引用

在《深入理解计算机系统》中第6章6.2

File

那么接下来我们来看看应该如何操作文件,在Java中为我们提供了一个专门用来处理文件的类:File

下面我们来看构造方法:

File构造方法

相对比而言,使用第二种和第三种方式的构造方法的更多,我们已第二个构造方法为例:

File file = new File("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\FileDemo.java");
// 判断当前文件是否存在:true
System.out.println(file.exists());

同样,该类还有很多其他的api方法,这里给大家演示几个常用的,其余的方法大家可以去查看File的api文档:

File file = new File("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\FileDemo.java");
System.out.printf("当前是否是文件:%s \n", file.isFile());
System.out.printf("当前是否是文件夹:%s \n", file.isDirectory());
System.out.printf("当前文件名称:%s \n", file.getName());
System.out.printf("当前文件绝对路径:%s \n", file.getAbsolutePath());
System.out.printf("文件长度:%s \n", file.length());

File newFile = new File("newCreateFile.txt");
newFile.createNewFile();
System.out.printf("newFile是否存在:%s \n", newFile.exists());

newFile.delete();
System.out.printf("newFile是否存在:%s \n", newFile.exists());

大家最好亲自尝试一下

还要一个比较常用的方法,我们做个小例子:

列出指定文件夹下的所有文件,如果是文件夹的话,那么继续向下遍历

这里可以用到一个方法:listFiles(),该方法会得到指定文件夹下所有的文件且对象类型为File[]

与其相对应的还有一个方法:list(),该方法和listFiles()类似,不过得到的对象类型为字符串数组

private static void _demo2() {
    File file = new File("D:\\Working Directory\\project");
    printFile(file, "|-");
}

private static void printFile(File file, String level) {
    File[] files = file.listFiles();
    for (File f : files) {
        if (f.isDirectory()) {
            System.out.println(level + f.getName());
            printFile(f, level + "-");
        } else {
            System.out.println(level + f.getName());
        }
    }
}
// 输出很多,我就不给效果图了,大家自己尝试下

关于方法中的参数:FileFilter,主要是对文件进行过滤操作

这里还需要说明一点:如果大家在测试的时候,出现异常但是程序本身无错,那么我们需要想一想是不是指定的目录是受系统保护的目录

文档

关于File就介绍到这里,更多其api方法查看官方文档:

File API方法

这是关于文件方面的相关内容,下面我们来聊一聊另外一个内容

IO流

上面我们能得到当前文件了,但是我们如果想要通过代码来读取文件中的内容,那么我们应该怎么做呢?

这就是我们下面要聊的内容:

可以这么理解:

从一个文件将数据返送到另一个文件。这里包含一个流向问题,这里的流向我们需要指定一个参照物。

参照物肯定不用过多解释了:就是我们写的程序

  • 我们是通过程序从源文件读取到数据,这里是一个输入流
  • 然后通过程序将数据写入到目标文件中,这里是输出流

查看一下图,我们来清晰认识下流向

流

分类

以下类都是基类

按照流向分类
  • 输入流
    • InputStream
    • Reader
  • 输出流
    • OutputStream
    • Writer
按照内容处理方式
  • 字节流:8位通用字节流
    • InputStream
    • OutputStream
  • 字符流:16位unicode字符流
    • Reader
    • Writer

使用明白了其分类,那么我们来看看流到底是如何来操作文件的

字节流

FileInputStream

正如我们所说,InputStream是一个接口类,自然我们需要了解其子类

FileInputStream使我们最常用的一个实现类,读取文件原始字节流,所有图像,文件等读取方式官方都推荐我们采用该方法,但是并不只是限定于字节文件,如果我们想要读取文本文件,也是可以使用的

下面我们看具体方式:

FileInputStream fileInputStream = new FileInputStream("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\FileInputStreamDemo.java");

// 得到流中的文件大小
System.out.println(fileInputStream.available());
// 读取流中的内容
System.out.println((char)fileInputStream.read());

官方给定三种初始化方式:

  • 传入File对象的方式
new FileInputStream(new File("文件名称"));
  • 传入文件名称的方式

上面的方式

  • FileDescriptor

这种方式很少使用, 这是一个文件描述符类

上面的是一个个的读取,下面我们来看看如果简化其读取操作

input read

FileInputStream fileInputStream = new FileInputStream("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\FileInputStreamDemo.java");

// 读取方式
byte[] buffer = new byte[1024];
int len  = 0;
// 将内容读取到byte数组中暂存起来
while ((len = fileInputStream.read(buffer)) != -1) {
    // 输出到控制台
    System.out.println(new String(buffer, 0, len));
}
fileInputStream.close();

在流中,如果读到文件的最后一个位置,我们通过read()方法来读取的时候会得到-1,所以我们只需要通过判断len是否为-1就可以知道文件是否已经读取完成

在使用流来操作文件的时候,我们在最后最好将流进行关闭:

  • 流操作是一个特别消耗资源的过程,如果我们只开启流而不关闭的话,那么资源消耗会非常大
FileOutputStream

说实话,我们在进行文件操作的时候,不可能说将读取到的文件输出到控制台啊,这没有任何意义

简单点说,如果我们需要需要将内容输出到一个文件中,那么又是该怎么处理呢?

这里就用到了我们的输出流:FileOutputStream

下面我们来操作下:

FileOutputStream fileOutputStream = new FileOutputStream("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\_FileInputStreamDemo.java", true);

// 输出到文件中
fileOutputStream.write(65);
fileOutputStream.write(66);
fileOutputStream.write(67);

// 关闭流:和FileInputStream一样
fileOutputStream.close();

FileInputStream构造方式类似,不同点在于FileOutputStream中多了一个参数:boolean append

  • 默认情况下:append=false,通过FileOutputStream进行输出的时候会对文件中的内容进行覆盖,
  • 如果append=true的话,那么这里会已追加的方式将内容输出到文件的尾部

重点:我们一定要搞清楚一个问题

  • 就是流的流向问题:记清楚上面那张流向图,永远已程序为参照物
小案例:复制文件

FileInputStreamFileOutputStream都已经说完了,那么我们来做一个小案例:文件的复制,考虑一下应该如何实现

给你们3秒钟

private static void copyFile() {
    FileInputStream fis = null;
    FileOutputStream fos = null;

    try {
        File sourceFile = new File("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\FileInputStreamDemo.java");
        File targetFile = new File("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\_FileInputStreamDemo.java");

        fis = new FileInputStream(sourceFile);
        fos = new FileOutputStream(targetFile);

        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, len);
        }
    } catch (IOException e ) {
        e.printStackTrace();
    } finally {
        if (null != fis) {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (null != fos) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

有时候我们会忘记关闭流,所以我们也可以采用下面这种方式

private static void copyFile2() {
        File sourceFile = new File("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\FileInputStreamDemo.java");
        File targetFile = new File("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\_FileInputStreamDemo.java");

    try (FileInputStream fis = new FileInputStream(sourceFile);FileOutputStream fos = new FileOutputStream(targetFile);) {
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

该方式会帮助我们关闭流,减少我们的一步操作

这里是字节流的方式,下面我们来看看字符流的方式

字符流

上面我们说过,如果我们要处理文本文件之类的,推荐使用字符流的方式,官方为我们提供了比较方便的操作

这里我想多一句,大家觉得字符是物理概念还是逻辑概念?

大家要清楚:字符是逻辑概念,在计算机的世界里,没有什么东西是代表字符的

FileReader

FileReaderReader的子类实现,我们来看看具体操作

File file = new File("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\ReadDemo.java");

FileReader fileReader = new FileReader(file);
System.out.println((char)fileReader.read());

大体上和FileInputStream是一样的方式,这里就不过多介绍

FileWriter

FileWriter是Writer的实现,以下为具体操作:

FileWriter fileWriter = new FileWriter("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\_ReadDemo.java");
fileWriter.write("这是我通过FileWriter写入的");
fileWriter.append("append追加到内容后面");
fileWriter.flush();

FileOutputStream的构造方法相同,但是在api方法上和其有些区别:

  • write():提供了直接写入字符串的方式
  • append():提供了追加的方法

最大的一个区别在于:

  • 虽然我们在通过write()方法将内容输出到了指定的文件,但是我们通过打开文件发现并没有内容,因为FileWriter会将内容暂存在流内存中,我们需要手动刷新才能将内容从内存刷新到文件中:也就是调用flush()方法

  • 调用的close()方法,相当于做了两个操作:

    • 刷新内容到文件

    • 关闭流

不过,一般情况下,我们建议手动调用flush()

好,我们了解了这些之后,在通过上面复制文件的小案例来熟悉下两者:

小案例:复制文件

同样,3秒钟考虑时间:

private static void copyFile() {
    File sourceFile = new File("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\ReadDemo.java");
    File targetFile = new File("D:\\Working Directory\\project\\study\\study-java\\src\\main\\java\\zopx\\top\\study\\jav\\_file\\_AA.txt");
    try(FileReader fr = new FileReader(sourceFile); FileWriter fw = new FileWriter(targetFile)) {
        char[] buffer = new char[1024];
        int len = 0;
        while ((len = fr.read(buffer)) != -1) {
            fw.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

文档

下面给出他们的具体官方文档,更多的方法推荐查看API:

FileInputStream

FileOutputStream

FileReader

FileWriter

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值