深入浅出IO流知识——基础流

茫茫人海千千万万,感谢这一秒你看到这里。希望我的文章对你的有所帮助!

愿你在未来的日子,保持热爱,奔赴山海!

I/O基础流

Java对数据的操作是通过流的方式,IO流用来处理设备之间的数据传输,上传文件和下载文件,Java用于操作流的对象都在IO包中。 而流是一组有序,有起点和有终点的数据序列,其本质就是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

作用:为数据源和目的地建立一个输送通道。♒

1. File类

IO流操作中大部分都是对文件的操作,所以Java就提供了File类供我们来操作文件。

1.1 概述

java.io.File 类是文件和目录路径名的抽象表示。File类是对文件系统中文件以及文件夹进行封装的对象,可以通过对象的思想来操作文件和文件夹,主要可以用于文件和目录的创建、查找和删除等操作。

注意: File表示的是文件和目录信息,而不表示文件的内容。

1.2 构造方法️

public File(String pathname) {} //文件的绝对路径

public File(String parent, String child) {}//指定父文件绝对路径、子文件绝对路径

public File(File parent, String child) {}//指定父文件、子文件相对路径

public File(URI uri) {} //文件的URI地址

//下面这两个是File类中私有的构造函数,外面不能调用
private File(String pathname, int prefixLength) {}

private File(String child, File parent) {}public File(String pathname) {}

可以看到,File类的构造函数有6个:4个公共构造函数,2个私有构造函数。

而常用的构造方法有:

  • public File(String pathname) :通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。
  • public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的 File实例。
  • public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的 File实例。

看下代码演示:

//public File(String pathname)// 文件路径名
String pathName = "E:\\aaa.txt";
File file = new File(pathName);

//public File(String parent, String child) 通过父路径和子路径字符串
String parent = "E:\\aaa";
String child = "bbb.txt";
File file2 = new File(parent, child);

// public File(File parent, String child) 通过父级File对象和子路径字符串
File parentDir = new File("E:\\aaa");
String child2 = "bbb.txt";
File file3 = new File(parentDir, child2);

注意:不知道大家有没有注意到我写文件路径名是这样的"E:\\aaa" ,可是我们文件路径不是"E:\aaa" 这样吗?

因为在字符串中\是转义字符,如果你只是一个\,会认为你这个是转义字符,所以需要两个。

贴士:

  1. 一个File对象代表硬盘中实际存在的一个文件或者目录。
  2. 无论该路径下是否存在文件或者目录,都不影响File对象的创建。

1.3 File的常用方法⚪

深入浅出IO流知识——基础流_第1张图片

以下的需要文件是基于这个a.txt

深入浅出IO流知识——基础流_第2张图片

1.3.1 获取功能
  • public String getAbsolutePath() : 返回此File的绝对路径名字符串。
  • public String getPath() : 将此File转换为路径名字符串。
  • public String getName() : 返回由此File表示的文件或目录的名称。
  • public long length() : 返回由此File表示的文件的长度。
  • public long lastModified() : 获取最后一次的修改时间,毫秒值

代码演示下:

public class FileTest {
    public static void main(String[] args) {
        File file = new File("E:\\demo\\a.txt");

        //public String getAbsolutePath():获取绝对路径
        System.out.println("getAbsolutePath:" + file.getAbsolutePath());//getAbsolutePath:E:\demo\a.txt

        //public String getPath():获取相对路径
        System.out.println("getPath:" + file.getPath());//getPath:E:\demo\a.txt

        //public String getName():获取名称
        System.out.println("getName:" + file.getName());//getName:a.txt

        //public long length():获取长度。字节数
        System.out.println("length:" + file.length());//length:20

        //public long lastModified():获取最后一次的修改时间,毫秒值
        System.out.println("lastModified:" + file.lastModified());//lastModified:1606645407343

        //转换下格式毫秒值。
        Date time = new Date(file.lastModified());
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String s = sdf.format(time);
        System.out.println(s);//2020-11-29 18:23:27
    }
}

想必这个应该不用多讲吧,看了就会用吧,直接调用就行了。

1.3.2 判断功能
  • public boolean isDirectory() : 判断是否是目录
  • public boolean isFile() : 判断是否是文件
  • public boolean exists() : 判断是否存在
  • public boolean canRead() : 判断是否可读
  • public boolean canWrite() : 判断是否可写
  • public boolean isHidden() :判断是否隐藏

像往常一样,继续来一波演示,走起:

public class FileTest2 {
    public static void main(String[] args) {
        File file = new File("E:\\demo\\a.txt");

        //public boolean isDirectory():判断是否是目录
        System.out.println("isDirectory: " + file.isDirectory());//isDirectory: false

        //public boolean isFile():判断是否是文件
        System.out.println("isFile: " + file.isFile());//isFile: true

        //public boolean exists():判断是否存在
        System.out.println("exists: " + file.exists());//exists: true

        //public boolean canRead():判断是否可读
        System.out.println("canRead: " + file.canRead());//canRead: true

        //public boolean canWrite():判断是否可写
        System.out.println("canWrite: " + file.canWrite());//canWrite: true

        //public boolean isHidden():判断是否隐藏
        System.out.println("isHidden: " + file.isHidden());//isHidden: false
    }
}

不多说,看代码!看了就懂了!

1.3.3 创建功能
  • public boolean createNewFile(): 创建文件 如果存在这样的文件,就不创建了
  • public boolean mkdir() :创建文件夹 如果存在这样的文件夹,就不创建了
  • public boolean mkdirs() : 创建文件夹,如果父文件夹不存在,会帮你创建出来

先看下创建前的文件信息:

深入浅出IO流知识——基础流_第3张图片

然后像往常一样,继续来一波演示,走起:

public class FileTest3 {
    public static void main(String[] args) throws IOException {
        //public boolean mkdir():创建文件夹 如果存在这样的文件夹,就不创建了
        // 在e盘文件夹demo下创建一个文件夹hello
        File file = new File("E:\\demo\\hello"); //根据上面的文件信息看到这个hello文件夹是没有的
        System.out.println("mkdir: " + file.mkdir());
        System.out.println("再来判断下文件夹是否存在: " + file.exists());

        //public boolean createNewFile():创建文件 如果存在这样的文件,就不创建了
        //在e盘目录demo下的文件夹hello创建一个文件hello.txt
        File file2 = new File(file,"\\hello.txt");//在上面已经创建了hello文件夹
        System.out.println("createNewFile: " + file2.createNewFile());
        System.out.println("再来判断下文件是否存在: " + file2.exists());

        //public boolean mkdirs():创建文件夹,如果父文件夹不存在,会帮你创建出来
        File file3 = new File("E:\\demo\\world\\world");//能不能两级文件夹直接创建出来呢
        System.out.println("mkdirs: " + file3.mkdirs());
        System.out.println("再来判断下文件夹是否存在: " + file3.exists());
    }
}

第一次执行结果可以看到:

mkdir: true
再来判断下文件夹是否存在: true
createNewFile: true
再来判断下文件是否存在: true
mkdirs: true
再来判断下文件夹是否存在: true

而现在是不是文件或者文件夹都创建出来了,我们再运行一次会怎样呢?

mkdir: false
再来判断下文件夹是否存在: true
createNewFile: false
再来判断下文件是否存在: true
mkdirs: false
再来判断下文件是否存在: true

方法调用上都为false,代表了如果有文件或者有文件夹了就都不再创建了。

提一个小问题,在创建的方法中哪个有可能会抛出异常的呢? 看下代码,觉得那个可能会出错呢

File file4 = new File("E:\\world\\world2"); 
System.out.println("mkdir: " + file4.mkdir());

File file5 = new File("E:\\world\\world2");
System.out.println("mkdirs: " + file5.mkdirs());

File file6 = new File("E:\\helloWorld\\helloWorld2"); 
System.out.println("createNewFile: " + file6.createNewFile());

运行结果可以看到:

mkdir: false
mkdirs: true
Exception in thread "main" java.io.IOException: 系统找不到指定的路径。
	at java.io.WinNTFileSystem.createFileExclusively(Native Method)
    at java.io.File.createNewFile(File.java:1012)
	at com.it.test1.FileTest3.main(FileTest3.java:33) 

小结⬜:

  1. mkdir如果在存在文件夹或者上级目录不存在,则会返回false即不创建,否则创建。
  2. mkdirs如果在存在文件夹,则会返回false即不创建,否则创建。
  3. createNewFile如果在存在文件,则会返回false即不创建,否则创建。如果上级目录不存在,则会报错,系统找不到指定的路径的IOException
1.3.4 删除功能

现在我们再看看下现在文件夹信息:

深入浅出IO流知识——基础流_第4张图片

在上面的创建过程中,我们已经有了hello文件夹及目录下的hello.txtworld文件夹下的world文件夹。

  • public boolean delete() :删除由此File表示的文件或目录。

话不在多说,上演示:

public class FileTest4 {
	public static void main(String[] args) {
        //public boolean delete():删除由此File表示的文件或目录。
        //接下来我们来删除之前创建的文件。
        //1.我们删除掉hello文件夹下的hello.txt
        File file = new File("e:\\demo\\hello\\hello.txt");
        System.out.println("删除hello.txt:" + file.delete());

        //2. 接下来我再删一次呢?会返回什么呢?
        System.out.println("删除hello.txt:" + file.delete());

        //3. 接下来我还想删除掉hello文件夹。
        File file2 = new File("e:\\demo\\hello");
        System.out.println("删除hello文件夹:" + file2.delete());

        //4. 接下来我想直接删除掉demo下的world文件夹。我们知道里面还有world文件夹,那会发生什么呢?
        File file3 = new File("e:\\demo\\world");
        System.out.println("删除world文件夹:" + file3.delete());
    }
}

我们猜测下最后删除world文件夹成功吗?

运行程序结果:

删除hello.txt:true
删除hello.txt:false
删除hello文件夹:true
删除world文件夹:false

注意:

  1. 可以看到如果我们删除hello.txt了,再删除,就返回false即删除失败。

  2. 如果文件夹下还有文件或者文件夹,也会删除失败。即我们想直接删除demo下的world文件夹就会失败,因为里面还有文件夹。

  3. 像我们平时删除文件的时候,他是不是就会去到回收站呢,那我们用Java删除的时候,文件也是去了回收站吗?

可以看到我们回收站是空的。并没有去到回收站,而是被JVM真正的删除掉了。

1.3.5 其他功能
  1. 重命名功能

    • public boolean renameTo(File file) : 重命名
    public class FileTest5 {
        public static void main(String[] args) {
            //public boolean renameTo(File file)
            File file = new File("e:\\demo\\a.txt");
            System.out.println("renameTo: " + file.renameTo(new File("e:\\demo\\b.txt")));//renameTo: true
        }
    }
    

    程序运行结果:

    renameTo: true
    

    那我们再去看下文件信息:

深入浅出IO流知识——基础流_第5张图片

  1. 绝对路径和相对路径

    • 绝对路径:从盘符开始的路径,这是一个完整的路径。如:e\\demo.a.txt

    • 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。如:a.txt

      public class FileTest6 {
          public static void main(String[] args) {
              // E盘下的bbb.java文件
              File f = new File("E:\\bbb.java");
              System.out.println(f.getAbsolutePath());
      
              // 项目下的bbb.java文件
              File f2 = new File("bbb.java");
              System.out.println(f2.getAbsolutePath());
          }
      }
      

      运行结果如下:

      E:\bbb.java
      E:\IdeaWorkSpace\project_File\bbb.java
      
  2. 目录的遍历

    • public String[] list() : 返回一个String数组,表示该File目录中的所有子文件或目录。
    • public File[] listFiles(): 返回一个File数组,表示该File目录中的所有的子文件或目录。

    一样继续演示吧:

    public class FileTest7 {
        public static void main(String[] args) {
            File dir = new File("e:\\demo");
            //public String[] list():返回一个String数组,表示该File目录中的所有子文件或目录。
            //获取当前目录下的文件以及文件夹的名称。
            String[] names = dir.list();
            for(String name : names){
                System.out.println(name);
            }
    
            //public File[] listFiles():返回一个File数组,表示该File目录中的所有的子文件或目录。
            //获取当前目录下的文件以及文件夹对象,只要拿到了文件对象,那么就可以获取更多信息
            File[] files = dir.listFiles();
            for (File file : files) {
                System.out.println(file);
            }
        }
    }
    

    程序运行结果:

    aaa
    b.txt
    bbb
    e:\demo\aaa
    e:\demo\b.txt
    e:\demo\bbb
    

    注意:调用listFiles方法的File对象,表示的必须是实际存在的目录,否则返回null,无法进行遍历。

1.3.6 小结File
  1. File可以用于表示文件和目录的信息,但是它不表示文件的内容。在后续的IO流操作中就可以对文件进行读写的操作啦。
  2. 无论该路径下是否存在文件或者目录,都不影响File对象的创建。虽然创建File对象没问题,可能后续操作对File对象的引用可能导致报错。
  3. 常用的方法介绍也在上面一一结束了,这里倒是没什么必要再写一遍了,嘿嘿!

2. 初窥IO流

终于到我们的重点主题啦,那什么是IO流呢?

2.1 什么是IO流

在生活中,你可能需要使用U盘传输文件。那这种数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input输出output ,即流向内存是输入流,流出内存的输出流。

Java对数据的操作是通过流的方式,IO流用来处理设备之间的数据传输,上传文件和下载文件,Java用于操作流的对象都在IO包中。

2.2 IO的分类

深入浅出IO流知识——基础流_第6张图片

可以看到:

  • 根据数据流向的不同可以分为:输入流和输出流
  • 根据处理数据类型的不同可以分为:字符流和字节流
  • 顶级的父类
输入流 输出流
字节流 字节输入流
InputStream
字节输出流
OutputStream
字符流 字符输入流
Reader
字符输出流
Writer

深入浅出IO流知识——基础流_第7张图片

以上只是列举了常见的类。如果需要看更多的类小伙伴们可以去查看API

2.3 字符字节流,输入输出流的概念

2.3.1 字符流和字节流

字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。 字节流和字符流的区别:

  • 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。

  • 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

    结论:只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。

2.3.2 输入流和输出流

输入流只能进行读操作,输出流只能进行写操作,程序中需要根据待传输数据的不同特性而使用不同的流。

3. 字节流

3.1 文件世界里,一切皆为字节

我们要知道,一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。

3.2 字节输出流的父类(OutputStream)

OutputStream,是所有字节输出流基类,是表示字节输出流的所有类的超类。主要作用是将指定的字节信息写出到目的地。它对整个体系的字节输出流制定了一些规范和方法。

3.2.1. 先来看看他的体系结构

深入浅出IO流知识——基础流_第8张图片

  • FileOutputStream:字节文件输出流是用于将数据写入到File,从程序中写入到其他位置。
  • FilterOutputStream:装饰者模式中处于装饰者,具体的装饰者都要继承它,所以在该类的子类下都是用来装饰别的流的,也就是处理类。
  • ObjectOutputStream:也是装饰类,用于序列化。主要是将Java对象的原始数据类型写出到文件,实现对象的持久存储。
  • BufferedInputStream:缓冲流,是FilterOutputStream装饰流的加强,提高读写的效率。
  • PrintStream:是FilterOutputStream装饰流的加强。能够方便地打印各种数据类型的值,是一种便捷的输出方式。
  • DataOutputStream:数据输出流,它是用来装饰其它输出流。
3.2.2. OutputStream中的方法

深入浅出IO流知识——基础流_第9张图片

后续继承的子类都有这些方法,基本字节输出流都需要write()方法进行写入文件中,使用close()方法关闭资源。

3.2.3. FileOutputStream
1. 构造方法
  • public FileOutputStream(File file):创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(File file, boolean append):跟上面类似,多了个属性,下面再讲
  • public FileOutputStream(String name): 创建文件输出流以指定的名称写入文件。
  • public FileOutputStream(String name, boolean append):跟上面类似,多了个属性,下面再讲

深入浅出IO流知识——基础流_第10张图片

这是还未创建构造方法前的文件夹信息

public class IOTest {
    public static void main(String[] args) throws FileNotFoundException {
        //public FileOutputStream(File file)
        //使用File对象创建流对象
        File file = new File("e:\\demo\\a.txt");
        FileOutputStream fos = new FileOutputStream(file);

        //public FileOutputStream(String name)
        // 使用文件名称创建流对象
        FileOutputStream fos2 = new FileOutputStream("E:\\demo\\b.txt");
    }
}

我们中的a.txt本来是没有的,而且b.txt里面是有内容的,让我们执行下代码后,会发生什么吧:

可以看到,控制台没有报错,也没有什么信息。那我们在来看看文件夹信息吧:

深入浅出IO流知识——基础流_第11张图片

注意:当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。

那为什么呢,有文件还会被清空数据呢,那我不想每次创建输出流对象,都要被清空目标文件中的数据,怎么做呢?

  • 我们先看下我们调用的构造方法具体过程吧:

    public FileOutputStream(File file) throws FileNotFoundException {
        this(file, false);
    }
    
    public FileOutputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null, false);
    }
    
    public FileOutputStream(File file, boolean append) throws FileNotFoundException {}
    
    public FileOutputStream(File file, boolean append) throws FileNotFoundException {}
    

    可以发现,最后都会调用了包含了append属性的构造方法,并且append为false。

    而这个append属性表示如果为true表示追加追加数据,false表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了。我先在b.txt加了hello。然后我们在试下文件是否又被清空掉

    public class AppendDemo {
        public static void main(String[] args) throws FileNotFoundException {
            FileOutputStream fos2 = new FileOutputStream("E:\\demo\\b.txt", true);
        }
    }
    
    执行后,可以看到,文件没有被清空数据了。
    

想一下我们在创建字节输出流对象了做了几件事情,是不是就认为不是直接创建的吗?还是底层有步骤的啦:

  1. 调用系统功能去创建文件。
  2. 创建fos对象。
  3. fos对象指向这个文件。
2. 写入数据

既然我们要创建出字节输出流,不就是为了写入数据吗,不然创建出来有什么用呢?那我们看下怎么用吧:

  1. public void write(int b):每次写一个字节。

    public class WriteTest {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建输出流对象
            FileOutputStream fos = new FileOutputStream("E:\\demo\\a.txt");
    		
            //public void write(int b)
            fos.write(97);
    
            fos.close();
            //如果在流关闭后再写入数据会怎样。
            //fos.write(98);//Exception in thread "main" java.io.IOException: Stream Closed
        }
    }
    
    执行结果后,查看文件a.txt得到的是:
    a
    

    有几点所需要注意:

    • 一定要关闭资源:

      为了让流对象变成垃圾,这样就可以被垃圾回收器回收了。

      通知系统去释放跟该文件相关的资源。

    • 为什么写的97,然后写到文件后变成字符a了:

      因为97代表了底层二进制数据,通过这个然后在字符表中找到对应的字符a。最后显示的也是a,而不是97,但是底层数据是97。我们看下完整的ASCII码表:

      深入浅出IO流知识——基础流_第12张图片

    • 如果关闭了流资源后,则无法继续对这个流进行操作,否则会报IOException异常。

    • 现在我们进行IO流操作,除了可能报错会打印在控制台上,其他都是直接在文件中进行操作,结果是直接显示在文件里,如果要看结果,打开文件就看到。

  2. public void write(byte[] b, int off, int len):每次写出从off索引开始,len个字节。相当于写入一个字节数组的一部分。

    public class WriteTest2 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建输出流对象
            FileOutputStream fos = new FileOutputStream("E:\\demo\\a.txt");
    
            //public void write(byte[] b)
            byte[] b = {97, 98, 99, 100, 101};
            fos.write(b);
    
            fos.close();
        }
    }
    
    执行结果后,查看文件a.txt得到的是:
    abcde
    
  3. public void write(byte[] b):每次写一个字节数组。

    public class WriteTest3 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建输出流对象
            FileOutputStream fos = new FileOutputStream("E:\\demo\\a.txt");
    
    
            //public void write(byte[] b, int off, int len)
            byte[] b = {97, 98, 99, 100, 101};
            fos.write(b,0,3);
    
            fos.close();
        }
    }
    
    执行结果后,查看文件a.txt得到的是:
    abc
    
3. 实现换行

我们以上操作好像都是写在一行上,那我们可不可以实现换行呢?

那我先看看怎样换行符是什么吧?

  • 回车符\r和换行符\n
    • 回车符:回到一行的开头(return)。
    • 换行符:下一行(newline)。
  • 系统中的换行:
    • Windows系统里,每行结尾是 回车+换行 ,即\r\n
    • Unix系统里,每行结尾只有 换行 ,即\n
    • Mac系统里,每行结尾是 回车 ,即\r。从 Mac OS X开始与Linux统一。

而我们现在默认使用的应该都是Windows系统,那我们需要加入的是\r\n。那我们代码演示下看看吧:

执行结果后,查看文件a.txt得到的是:
epublic class WriteTest4 {
    public static void main(String[] args) throws IOException {
        // 使用文件名称创建输出流对象
        FileOutputStream fos = new FileOutputStream("E:\\demo\\a.txt");

        //实现写入每一个字符后都换行
        byte[] b = {97, 98, 99, 100, 101};
        for (byte bs : b) {
            fos.write(bs);
            // 写出一个换行, 换行符号转成数组写出
            fos.write("\r\n".getBytes());
        }

        fos.close();
    }
}

执行结果后,查看文件a.txt得到的是:
a
b
c
d
e

是不是觉得每次都要查看文件,然后确认是否添加成功,很麻烦呢。接下来我们就学习字节流中的输入流,学习如何读取文件。

3.3 字节输入流的父类(InputStream)

InputStream,是所有字节输入流基类,是表示字节输入流的所有类的超类。主要作用是读取字节信息到内存中。它对整个体系的字节输入流制定了一些规范和方法。

3.3.1 先来看看他的体系结构

深入浅出IO流知识——基础流_第13张图片

  • FileInputSream:文件输入流。它通常用于对文件进行读取操作。
  • FilterInputStream:装饰者模式中处于装饰者,具体的装饰者都要继承它,所以在该类的子类下都是用来装饰别的流的,也就是处理类。
  • BufferedInputStream:缓冲流,对处理流进行装饰,增强,内部会有一个缓存区,用来存放字节,每次都是将缓存区存满然后发送,而不是一个字节或两个字节这样发送。效率更高。
  • ObjectInputStream:对象输入流,用来提供对基本数据或对象的持久存储。也就是能直接传输对象(反序列化中使用)。
  • DataInputStream:数据输入流,它是用来装饰其它输入流。
3.3.2 InputStream中的方法

深入浅出IO流知识——基础流_第14张图片

后续继承的子类都有这些方法,基本字节输入流都需要read()方法进行读取文件,使用close()方法关闭资源。

3.3.3. FileInputStream

FileInputStream的使用与FileOutputStream基本类似。

1. 构造方法
  • public FileInputStream(File file):通过打开与实际文件的连接来创建一个 FileInputStream,该文件由文件系统中的 File对象 File命名。
  • public FileInputStream(String name):通过打开与实际文件的连接来创建一个 FileInputStream,该文件由文件系统中的路径名 name命名。

这次我们来看下使用构造前的文件信息:

深入浅出IO流知识——基础流_第15张图片

public class FileInputStreamTest {
    public static void main(String[] args) throws FileNotFoundException {
        //public FileInputStream(File file)
        //使用File对象创建流对象
        File file = new File("e:\\demo\\b.txt");
        FileInputStream fis = new FileInputStream(file);

        //public FileInputStream(String name)
        // 使用文件名称创建流对象
        FileInputStream fis2 = new FileInputStream("e:\\demo\\c.txt");
    }
}

执行程序,结果:

Exception in thread "main" java.io.FileNotFoundException: e:\demo\c.txt (系统找不到指定的文件。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(FileInputStream.java:93)
	at com.it.test3.FileInputStreamTest.main(FileInputStreamTest.java:16) 
    这里的16行错误即: FileInputStream fis2 = new FileInputStream("e:\\demo\\c.txt");

注意:

  • 与输出流不同的是,FileInputStream如果传入的路径是空的话,就会报错,而FileOutputStream则会创建文件。
  • FileInputStream创建出来输入流对象,是不会清空对象的。很好理解,输入流本来就是要读取文件信息创建出来的流,如果清空了,那我还读个啥对吧,读出一片寂寞吗。
2. 读取数据

既然我们要创建出字节输入流,不就是为了读取数据吗,不然创建出来有什么用呢?那我们看下怎么用吧:

  1. public int read():每次可以读取一个字节的数据,读出的类型为int类型,当读取到文件末尾即没有数据,返回-1

    public class ReadTest {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            FileInputStream fis = new FileInputStream("e:\\demo\\b.txt");
    
            //int read():一次读取一个字节
            int read = fis.read();
            System.out.println(read);
            System.out.println((char) read);
            
            fis.close();
        }
    }
    

    输出结果:

    104
    h
    

    我们b.txt中存储的是hello。根据输入流读取的结果返回的是int类型,那我们就需要强转成char字符类型,返回得到原来的文件信息。

    可是如果每次都是这样一个字节慢慢读取的话,是不是很麻烦,那这种重复度很高的话,就可以考虑循环优化它,可是我们现在不知道循环结束的条件是什么?那我们来测试下,读完后是什么结果呢?

    public class ReadTest2 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            FileInputStream fis = new FileInputStream("e:\\demo\\b.txt");
    
            //int read():一次读取一个字节
            int read = fis.read();
            System.out.print(read);
            System.out.println(" : "+(char) read);
    
            read = fis.read();
            System.out.print(read);
            System.out.println(" : "+(char) read);
    
            read = fis.read();
            System.out.print(read);
            System.out.println(" : "+(char) read);
    
            read = fis.read();
            System.out.print(read);
            System.out.println(" : "+(char) read);
    
            read = fis.read();
            System.out.print(read);
            System.out.println(" : "+(char) read);
    
            //b.txt中,我们知道hello是5次就可以读完,那我们读第六次会怎样呢?
            read = fis.read();
            System.out.print(read);
            System.out.println(" : "+(char) read);
    
            fis.close();
        }
    }
    

    程序运行结果:

    104 : h
    101 : e
    108 : l
    108 : l
    111 : o
    -1 : 
    

    可以发现如果读到没有数据,最后读到的结果是-1,那我们是不是知道了循环结束的条件呢,如果没读到数据了就返回-1

    那我们使用循环改进:

    public class ReadTest3 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            FileInputStream fis = new FileInputStream("e:\\demo\\b.txt");
            // 定义变量,保存数据
            int b;
            // 循环读取
            while ((b = fis.read())!=-1) {
                System.out.println((char)b);
            }
            // 关闭资源
            fis.close();
        }
    }
    

    程序运行结果:

    h
    e
    l
    l
    o
    

    使用输入流的有几点注意:

    • 虽然读取了一个字节,但是会自动提升为int类型。需要强转回字符。
    • 跟输出流一样,输入流操作完毕后,必须释放系统资源,调用close()方法
  2. public int read(byte b[]):每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回-1

    public class ReadTest4 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            // b.txt中信息为:
            FileInputStream fis = new FileInputStream("e:\\demo\\b.txt");
    
            //public int read(byte b[])
            byte[] b = new byte[3];
            int len = fis.read(b);
            System.out.println(len);// 可以看到读到是实际长度
            System.out.println(new String(b));
    
            len = fis.read(b);
            System.out.println(len);// 可以看到读到是实际长度
            System.out.println(new String(b));
    
            //到这么,已经读取完了,看看是不是和读取一个一样是读到-1。
            len = fis.read(b);
            System.out.println(len);// 可以看到读到是实际长度
    
            // 关闭资源
            fis.close();
        }
    }
    

    执行结果为:

    3
    hel
    2
    lol
    -1
    

    可以发现:

    • 如果读取到的实际长度是-1,就说明没有数据了。这就可以利用这个改进行循环优化了。

    • 而且我们知道System.out.println,每打印出字符串后,就会进行换行,则有可能对我们的获取产生错乱,因为可能我们读取的字符就在一行呢,所以我们也得进行更换为System.out.print

    • len获取到是实际的数据长度,第一次得到是3,获得了hel,而第二次获取的也就是2,可以看到确实不错,但按道理来说,我们得到不应该是lo吗,怎么会是lol呢,这是在提示我要打LOL了吗,嘿嘿!你看我是想喜欢打游戏吗?而且是像我这种经常拿五杀的人,并且经常CARRY的人,排位打到王者的男人。这里我战绩就不放出来了,怕吓到你们。而且我现在不打了,如果打下去,就可能是下一个UZI,theShy了。

深入浅出IO流知识——基础流_第16张图片

 好了好了,会到重点,我们看下为什么最后会打出`lol`呢,而不是`lo`

深入浅出IO流知识——基础流_第17张图片

在知道了以上的步骤后,我们进行循环优化,并对之前的发生的问题进行修改。

public class ReadTest5 {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("e:\\demo\\b.txt");

        byte[] b = new byte[3];
        int len;
        while((len = fis.read(b)) != -1) {
            System.out.print(new String(b, 0, len));//如此便也可以返回实际的字符
        }

        // 关闭资源
        fis.close();
    }
}

执行程序结果为:
hello

可以看到,完美的在控制台上打印出了hello,并且不会每次读取,都会换行,完美复刻。

深入浅出IO流知识——基础流_第18张图片

小结下:

在开发中,我们使用数组读取,每次读取多个字节,减少了系统间的IO操作次数,从而提高了读写的效率。

3.4 字节流练习:图片复制

既然我们已经学习了字节的输出流和输入流,那何不妨来具体实战一下:

先来解释一波,复制的原理,就像电脑的右键复制一样:

深入浅出IO流知识——基础流_第19张图片

复制前,先看看文件信息,免得你说我偷偷右键复制过去。

深入浅出IO流知识——基础流_第20张图片

跟着我走,使用Java复制图片,你将会觉得很简单。So Easy

public class CopyTest {
    public static void main(String[] args) throws IOException {
        // 1. 创建输入和输出流对象。
        // 1.1 指定要读文件的路径。
        FileInputStream fis = new FileInputStream("e:\\demo\\北极星.jpg");
        // 1.2 指定目的地文件的路径。
        FileOutputStream fos = new FileOutputStream("e:\\demoCopy\\北极星.jpg");

        // 2.读写数据
        // 2.1 定义字节数组
        byte[] b = new byte[1024];
        int len;
        // 2.2 循环读取出数据
        while ((len = fis.read(b))!=-1) {
            // 2.3 将得到的字节数据写出
            fos.write(b, 0 , len);
        }

        // 3.关闭所有资源
        fos.close();
        fis.close();
    }
}

程序运行后,你将会看到很快就完成,再到文件查看下,发现复制好的北极星.jpg已经完整的躺好在哪里了。

深入浅出IO流知识——基础流_第21张图片

大家要看下北极星小姐姐吗,不知道大家还记得北极星小姐姐吗,真的太可了,真的awsl,为此我多年前还专门刷完了天赋异禀整部剧,看到北极星和交食两人在一起产生极光,真的Interesting。极光是因为太阳带电粒子流进入地球磁场而产生的。
嘿嘿,好东西还是自己偷偷看吧,哈哈,还是默默躺在我文件夹中吧。

4. 字符流

字符输入输出流与字节输入输出流有相同的功能,但传送数据的方式不一样,字节流以字节为单位传送数据,可以使任何类型的数据,例如文本、音频、视频、图片等。字符流以字符为单位传送数据,只能传送文本类型的数据。使用字符输入输出流的好处是,当读取中文时不会出现乱码问题,而使用字节输入输出流时,却不能保证这一点。因为一个中文字符可能占用了多个字节存储。

具体会发生什么问题呢,我们来看看吧,那有办法解决吗,

public class CharaterTest {
    public static void main(String[] args) throws IOException {
        //FileInputStream为操作文件的字符输入流
        FileInputStream fis = new FileInputStream("E:\\demo\\testTxt.txt");

        int len;
        while ((len = fis.read()) != -1) {
            System.out.print((char) len);
        }

        fis.close();
    }
}

程序运行结果: 
hello±±¼«ÐÇÏòÄã·¢³öÊÓƵÁÄÌì

这。。。都是些什么,我猜你肯定在怀疑我是不是坑你,是不是故意这么写的?当然,不是的啦,像我这么善良、纯真、质朴的集优秀品质为一身的这么一个人,会坑你?还是来看下我的文件信息吧。

深入浅出IO流知识——基础流_第22张图片

因为,字节流读取中文字符时,可能不会显示完整的字符,那是因为一个中文字符占用多个字节存储。可是可是,我没办法使用字节流来读取了吗,那还有什么办法读取啊?

方法肯定还是有的,只是对于读取中文来说,不是特别的方便,而且后面我们也进行学习字符流,这个对读取中文来说,特别的方便,对于这种纯文本来说,首选是字符流。

那我们先来看看怎么解决吧

public class CharaterTest2 {
    public static void main(String[] args) throws IOException {
        //FileInputStream为操作文件的字符输入流
        FileInputStream fis = new FileInputStream("E:\\demo\\testTxt.txt");

        byte[] b = new byte[1024];
        int len = 0;
        while ((len = fis.read(b)) != -1){
            System.out.print(new String(b, 0, len));
        }

        fis.close();
    }
}

程序运行结果:
    hello北极星向你发出视频聊天

可以读取到了,你说,要是女神突然发信息说要找你视频聊天,可是你的电脑或者手机显示的是一堆乱码,你的脑袋是否有一堆问号。

深入浅出IO流知识——基础流_第23张图片

可能你还在想女神发的是啥,是不是在骂你。但是可能这就是你错过了拥有女神的机会了,所以要学好Java啊。

回到正题,那为什么使用了String了就可以正常显示了呢,不要着急,容我带大家一起来看看。

public String(byte bytes[], int offset, int length) {
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(bytes, offset, length);
}

static char[] decode(byte[] ba, int off, int len) {
    ....  
}

可以看到String的构造方法,有decode解码功能, 默认为utf-8,也是现在默认的编码格式,如果各位通过String的构造方法还是乱码的话,就代表了可能你的文本不是utf-8格式,可能是GBK或者是ANSI格式等其他格式编码,后面再具体讲解如何解决编码格式的问题。现在可以先这样:

深入浅出IO流知识——基础流_第24张图片

可以这样认为:字符流 = 字节流 + 编码表。

4.1 字符输入流的父类(Reader)

Reader 是所有的输入字符流的父类,它是一个抽象类。主要作用是可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

4.1.1 体系结构

深入浅出IO流知识——基础流_第25张图片

  • InputStreamReader是一个连接字节流和字符流的桥梁,它读取字节,并使用指定的字符集将其解码为字符。
  • FileReader是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
  • BufferedReader字符缓冲流,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
  • FilterReader是所有自定义具体装饰流的父类。
4.1.2 Reader中的方法

深入浅出IO流知识——基础流_第26张图片

在我们学完字节流后,可以发现IO流中输入流都是一样有read()。字符输入流后续继承的子类都有这些方法,需要read()方法进行读取文件,使用close()方法关闭资源。

4.1.3 FileReader
1.构造方法
  • public FileReader(File file): 创建一个新的FileReader ,给定要读取的File 对象。

  • public FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称。

    创建代码演示:

    public class FileReaderTest {
        public static void main(String[] args) throws FileNotFoundException {
    
            // 使用File对象创建流对象
            File file = new File("e:\\demo\\a.txt");
            FileReader fr = new FileReader(file);
    
            // 使用文件名称创建流对象
            FileReader fr2 = new FileReader("e:\\demo\\b.txt");
            
            //FileReader fr3 = new FileReader("e:\\demo\\c.txt");
            //一样如果读取不到文件就会报异常,
            //Exception in thread "main" java.io.FileNotFoundException: e:\demo\c.txt (系统找不到指定的文件。)
        }
    }
    

    可以看到和字节流的创建方式基本一样,读取不到文件会报异常。

2. 读取字符数据

字符流就是用来读取文本文件的,那接下来我们来测试下吧:

  1. public int read() :每次可以读取一个字符的数据,提升为int 类型,读取到文件末尾,返回-1 。可以测试得出和字节流一样,如果读到没有数据,就会返回-1 。直接使用循环读取:

    public class ReaderTest {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            FileReader fr = new FileReader("e:\\demo\\testTxt.txt");
            // 定义变量,保存数据
            int ch = 0;
            // 循环读取
            //public int read()
            while ((ch = fr.read())!=-1) {
                System.out.print((char) ch);
            }
            // 关闭资源
            fr.close();
        }
    }
    
    程序执行结果:
       hello北极星向你发出视频聊天
    

    注意:

    • 这里一样读取得到还是int类型,需要强转为字符类型。才能显示为原本文件的信息。
    • 还是像之前一样,必须得关闭资源。
  2. public int read(char cbuf[]):每次读取b的长度个字符到数组中,返回读取到的有效字符个数,读取到末尾时,返回-1 。直接使用循环演示:

    public class ReaderTest2 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            FileReader fr = new FileReader("e:\\demo\\testTxt.txt");
            // 定义变量,保存有效字符个数
            int len = 0;
            // 定义字符数组,作为装字符数据的容器
            char[] cbuf = new char[2];
            // 循环读取
            //public int read(char cbuf[])
            while ((len = fr.read(cbuf)) != -1) {
                System.out.println(new String(cbuf));
            }
            // 关闭资源
            fr.close();
        }
    }
    
    程序执行结果:
    北极
    星向
    你发
    出视
    频聊
    天聊
    

    可以看到,他还是多了一个字符,跟字节输入流一样的问题,那我们使用获取有效的字符改进:

    public class ReaderTest3 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            FileReader fr = new FileReader("e:\\demo\\testTxt.txt");
            // 定义变量,保存有效字符个数
            int len = 0;
            // 定义字符数组,作为装字符数据的容器
            char[] cbuf = new char[2];
            // 循环读取
            //public int read(char cbuf[])
            while ((len = fr.read(cbuf)) != -1) {
                System.out.println(new String(cbuf, 0, len));
            }
            // 关闭资源
            fr.close();
        }
    }
    
    程序执行结果:
    北极
    星向
    你发
    出视
    频聊
    天
    

    小结下:

    我们发现其实大多数方法我们在字节流都使用过,基本来说用起来基本都差不多。对于纯文本来说优先考虑字符流,而其他情况就只能用字节流了。

4.2 字符输出流的父类(Writer)

Writer是所有的输出字符流的父类,它是一个抽象类。主要作用是可以将指定的字符信息写出到目的地。它定义了字符输出流的基本共性功能方法。

4.2.1 体系结构

深入浅出IO流知识——基础流_第27张图片

  • OutputStreamWriter是一个连接字节流和字符流的桥梁。
  • FilterWriter是所有自定义具体装饰流的父类。
  • BufferedWriter字符缓冲流,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
  • FileWriter是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
4.2.2 Writer中的方法

深入浅出IO流知识——基础流_第28张图片

与字节输入流的用法完全类似,输入流后续继承的子类都有这些方法,需要writer()方法进行读取文件,使用close()方法关闭资源。

4.2.3 FileWriter
1. 构造方法
  • public FileWriter(File file):创建一个新的 FileWriter,给定要读取的File对象。
  • public FileWriter(File file, boolean append):创建一个新的 FileWriter,可以追加数据。
  • public FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。
  • public FileWriter(String fileName, boolean append):创建一个新的 FileWriter,可以追加数据。

演示如下:

public class WriterTest {
    public static void main(String[] args) throws IOException {
        //public FileWriter(File file)
        // 使用File对象创建流对象
        File file = new File("e:\\demo\\bjx.txt");
        FileWriter fw = new FileWriter(file);


        //public FileWriter(String fileName)
        // 使用文件名称创建流对象
        FileWriter fw2 = new FileWriter("e:\\demo\\bjx.txt");
    }
}

可以发现,程序运行后,在文件中自动帮我们生成了bjx.txt文件。跟字节输出流一样。

追加数据的构造方法就不带大家演示了,上面也有专门演示过了。嘿嘿,不要说我懒,因为我相信你学了上面的字节输出流的,这里你肯定懂了,对吧。大家都这么聪明,学习Java的天才,一点就懂的啦。

2. 写出数据

既然能创建出来FileWriter对象了,那我们就具体实践下写出数据吧。

  1. public void write(int c):每次可以写出一个字符数据。

    public class WriterTest2 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            FileWriter fw = new FileWriter("e:\\demo\\bjx.txt");
    
            // 写出数据
            fw.write(97);//存入到文件中应为:a
            fw.write(26666);//存入到文件中应为:株
    
            /*
            【特别注意】与字节输出流FileOutputStream不同的是。
          	 如果不关闭资源,数据只是保存到缓冲区,并未保存到文件。
            */
            //fw.close();
        }
    }
    

    程序运行后,可以看到如果我没有注释掉fw.close()即有关闭资源的情况下,是可以保存到文件中,但是呢,如果没有关闭资源,它则没有保存到文件。这是为什么呢?

    • 一个字符 = 两个字节,而文本中数据存储的基本单位是字节。是不是字节输出流就可以直接写进去呢,而字符流,是没办法直接存储进去。

    • 而字符流是通过缓冲区的方式进行存储的,如果没有使用方法是无法将缓冲区的内容保存到文件中的。

      可以通过write()方法一步一步点进去看源码,可以发现他存进去了一个CharBuffer缓冲区中。

      深入浅出IO流知识——基础流_第29张图片

    • 因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。而如果一旦关闭流资源后,我们就没办法再写入数据,所以如果我我们既想写出数据,又想继续使用流,就需要flush() 方法了。

      看源码可以发现,flush() 方法和close() 方法最终都会调用StreamEncoder中的writeBytes()

      深入浅出IO流知识——基础流_第30张图片

      flush() :刷新缓冲区,流对象可以继续使用。
      close() :先刷新缓冲区,然后通知系统释放资源,流对象不可以再被使用了。

    最终可以改进为:

    public class WriterTest3 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            FileWriter fw = new FileWriter("e:\\demo\\bjx.txt");
    
            // 写出数据
            //public void write(String str)
            fw.write("北极星");
            //fw.write('叫我');//字符不能多写,而字符串可以
            fw.flush();
            fw.write("叫我");
            fw.flush();//一般每写入一个就刷新下缓冲区。
            // 如果你在最后写,如果这过程中出现异常等错误,是不是写入了呢?
            fw.write("视频聊天啦");
            fw.flush();
    
            fw.close();//就算我注释掉,也是有加入数据了的
            //fw.write("能成功吗"); // 当我们要执行这里后,就会【报错】java.io.IOException: Stream closed
            //fw.close();
        }
    }
    

    可以发现:

    • 我们可以通过flush()方法实现写入数据到文件中。
    • 即便是flush()方法写出了数据,操作的最后还是要调用close() 方法,释放系统资源。
  2. puiblic write(char[] cbuf) public write(char[] cbuf, int off, int len) :每次可以写出字符数组中的数据。这两个功能用法和字节输出流类似,直接演示看下吧:

    public class WriterTest4 {
        public static void main(String[] args) throws IOException {
            // 使用文件名称创建流对象
            FileWriter fw = new FileWriter("e:\\demo\\bjx.txt");
    
            // 字符串转换为字节数组
            char[] chars = "北极星".toCharArray();
    
            // 写出字符数组
            //puiblic write(char[] cbuf)
            fw.write(chars); // 北极星
            fw.flush();
    
            //写出从索引0开始,2个字节
            //public write(char[] cbuf, int off, int len)
            fw.write(chars,0,2);//北极
            fw.flush();
    
            fw.close();
        }
    }
    
    程序执行后,bjx.txt的文件信息为:
    北极星北极
    
3. 实现换行

既然在字节输出流中,我们可以实现换行的功能,那我们字符流可以吗?答案当然可以的啦,跟字节输出流一样的操作。来看看吧:

public class WriterTest5 {
    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("e:\\demo\\bjx.txt");

        fw.write("北极星叫");
        fw.flush();
        // 写出换行
        fw.write("\r\n");
        fw.flush();
        fw.write("我一起看流星雨");
        fw.flush();

        fw.close();
    }
}

程序执行后,bjx.txt的文件信息为:
北极星叫
我一起看流星雨

4.3 复制文本案例

复制的原理已经在字节流的时候讲解过了。现在直接上代码给各位看官瞧瞧如何:

public class CopyTxtTest {
    public static void main(String[] args) throws IOException {
        // 1. 创建输入和输出流对象。
        // 1.1 指定要读文件的路径。
        FileReader fr = new FileReader("E:\\demo\\青春.txt");
        // 1.2 指定目的地文件的路径。
        FileWriter fw = new FileWriter("E:\\demoCopy\\青春.txt");

        // 2.读写数据
        // 2.1 定义字符数组
        char[] chs = new char[1024];
        int len;
        // 2.2 循环读取出数据
        while ((len = fr.read(chs)) != -1) {
            // 2.3 将得到的字节数据写出
            fw.write(chs, 0, len);
            fw.flush();
        }

        // 3.关闭所有资源
        fw.close();
        fr.close();

    }
}

程序执行后,看下文件的信息:

深入浅出IO流知识——基础流_第31张图片

4.4 总结

字符流,只能操作文本文件,不能操作图片,视频等非文本文件。当我们单纯读或者写文本文件时可以使用字符流更高效的操作文本文件,而其他情况使用字节流。

  • 两者的区别:
    • 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
    • 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

总结

相信各位看官都对IO流一些基础知识和使用有了一定了解,等待下一章高级流如缓冲流(俗称高效流)的学习吧!当然还有很多流等着下次一起看吧!走过路过,瞧一瞧,保证不吃亏呀!!!

让我们也一起加油吧!本人不才,如有什么缺漏、错误的地方,也欢迎各位人才大佬评论中批评指正!当然如果这篇文章确定对你有点小小帮助的话,也请亲切可爱的人才大佬们给个点赞、收藏下吧,一键三连,非常感谢!

学到这里,今天的世界打烊了,晚安!虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!

感谢各位看到这里!愿你韶华不负,青春无悔!

你可能感兴趣的:(重学JavaSE基础,java,开发语言,后端,IO流)