Java基础IO流

流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。IO流最终要以对象来体现,对象都存在IO包中。

IO流的分类

根据处理数据类型的不同分为:字符流和字节流

根据数据流向不同分为:输入流和输出流

注意:流的操作只有两种:读和写。

流的体系因为功能不同,但是有共性内容,不断抽取,形成继承体系。该体系一共有四个基类,而且都是抽象类。

字节流:InputStream  OutputStream

字符流:Reader  Writer

在这四个系统中,它们的子类,都有一个共性特点:子类名后缀都是父类名,前缀名都是这个子类的功能名称。

Java流类图结构

字节流

InputStream 和 OutputStream 是两个 abstact 类,对于字节为导向的 stream 都扩展这两个基类

InputStream:是表示字节输入流的所有类的超类。

OutputStream:此抽象类是表示输出字节流的所有类的超类。

处理字节数据的流对象。设备上的数据无论是图片或者dvd,文字,它们都以二进制存储的。二进制的最终都是以一个8位为数据单元进行体现,所以计算机中的最小数据单元就是字节。意味着,字节流可以处理设备上的所有数据,所以字节流一样可以处理字符数据。

  • 向文件中写入字符串
  • 向文件中追加新内容
  • 读取文件内容
    • 例子读取出来会有大量的空格,我们可以利用in.read(b);的返回值来设计程序。如下:
    • 观察上面的例子可以看出,我们预先申请了一个指定大小的空间,但是有时候这个空间可能太小,有时候可能太大,我们需要准确的大小,这样节省空间,那么我们可以这样干:

    • 将上面的例子改为一个一个读:
    • 上面的几个例子都是在知道文件的内容多大,然后才展开的,有时候我们不知道文件有多大,这种情况下,我们需要判断是否独到文件的末尾:

字符流

Reader:用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。

Writer:写入字符流的抽象类。子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。

那么为什么要有字符流呢?因为字符每个国家都不一样,所以涉及到了字符编码问题,那么GBK编码的中文用unicode编码解析是有问题的,所以需要获取中文字节数据的同时+ 指定的编码表才可以解析正确数据。为了方便于文字的解析,所以将字节流和编码表封装成对象,这个对象就是字符流。只要操作字符数据,优先考虑使用字符流体系。

  • 向文件中写入数据
    • 这个例子和之前的例子没什么区别,只是你可以直接输入字符串,而不需要你将字符串转化为字节数组。

      当你如果想在文件中追加内容的时候,可以使用将上面的声明out的哪一行换为:

      Writer out =new FileWriter(f,true);

      这样,当你运行程序的时候,会发现文件内容变为:hellohello如果想在文件中换行的话,需要使用“\r\n”

      比如将str变为String str="\r\nhello";这样文件追加的str的内容就会换行了。

    • 从文件中读内容:
    • 当然最好采用循环读取的方式,因为我们有时候不知道文件到底有多大:

BufferedWriter

BufferedWriter是给字符输出流提高效率用的,那就意味着,缓冲区对象建立时,必须要先有流对象。明确要提高具体的流对象的效率。

 

BufferedReader

记住:

读取键盘录入:BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));

输出到控制台:BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));

字节流和字符流的区别

实际上字节流在操作的时候本身是不会用到缓冲区的,是文件本身的直接操作的,但是字符流在操作的时候是会用到缓冲区的,是通过缓冲区来操作文件的。如果将上面的字节流和字符流的程序的最后一行关闭文件的代码注释掉,然后运行程序看看。你就会发现使用字节流的话,文件中已经存在内容,但是使用字符流的时候,文件中还是没有内容的,这个时候就要刷新缓冲区。

使用字节流好还是字符流好呢?

答案是字节流。首先因为硬盘上的所有文件都是以字节的形式进行传输或者保存的,包括图片等内容。但是字符只是在内存中才会形成的,所以在开发中,字节流使用广泛。

因为功能的不同,流的体系中提供N多的对象。那么开始时,到底该用哪个对象更为合适呢?这就需要明确流的操作规律。

流的操作规律

  • 明确源和目的。

数据源:就是需要读取,可以使用两个体系:InputStream、Reader;

数据汇:就是需要写入,可以使用两个体系:OutputStream、Writer;

  • 操作的数据是否是纯文本数据?

如果是:数据源:Reader    数据汇:Writer

如果不是:数据源:InputStream      数据汇:OutputStream

  • 虽然确定了一个体系,但是该体系中有太多的对象,到底用哪个呢?明确操作的数据设备。

数据源对应的设备:硬盘(File),内存(数组),键盘(System.in)

数据汇对应的设备:硬盘(File),内存(数组),控制台(System.out)。

  • 需要在基本操作上附加其他功能吗?比如缓冲。【如果需要就进行装饰

转换流特有功能:转换流可以将字节转成字符,原因在于,将获取到的字节通过查编码表获取到指定对应字符。

转换流的最强功能就是基于 字节流 + 编码表 。没有转换,没有字符流。

发现转换流有一个子类就是操作文件的字符流对象:

InputStreamReader

|--FileReader

OutputStreamWriter

|--FileWrier

想要操作文本文件,必须要进行编码转换,而编码转换动作转换流都完成了。所以操作文件的流对象只要继承自转换流就可以读取一个字符了。

但是子类有一个局限性,就是子类中使用的编码是固定的,是本机默认的编码表,对于简体中文版的系统默认码表是GBK。

1
2
3
FileReader fr = new FileReader("a.txt");
 
InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"),"gbk");

以上两句代码功能一致,

  • 如果仅仅使用平台默认码表,就使用FileReader fr = new FileReader("a.txt"); //因为简化。
  • 如果需要制定码表,必须用转换流。

转换流 = 字节流+编码表。

转换流的子类File = 字节流 + 默认编码表。

凡是操作设备上的文本数据,涉及编码转换,必须使用转换流。

OutputStreramWriter 和InputStreamReader类

OutputStreramWriter将输出的字符流转化为字节流

InputStreamReader将输入的字节流转换为字符流

但是不管如何操作,最后都是以字节的形式保存在文件中的。

  • 将字节输出流转化为字符输出流

  • 将字节输入流变为字符输入流

  • 前面列举的输出输入都是以文件进行的,现在我们以内容为输出输入目的地,使用内存操作流:

     ByteArrayInputStream 主要将内容写入内存

     ByteArrayOutputStream  主要将内容从内存输出

    使用内存操作流将一个大写字母转化为小写字母:

    内容操作流一般使用来生成一些临时信息采用的,这样可以避免删除的麻烦。 

File类

将文件系统中的文件和文件夹封装成了对象。提供了更多的属性和行为可以对这些文件和文件夹进行操作。

这些是流对象办不到的,因为流只操作数据。

File类常见方法

1:创建

boolean createNewFile():在指定目录下创建文件,如果该文件已存在,则不创建。

而对操作文件的输出流而言,输出流对象已建立,就会创建文件,如果文件已存在,会覆盖。除非续写。

boolean mkdir():创建此抽象路径名指定的目录。

boolean mkdirs():创建多级目录。 

2:删除

boolean delete():删除此抽象路径名表示的文件或目录。

void deleteOnExit():在虚拟机退出时删除。

注意:在删除文件夹时,必须保证这个文件夹中没有任何内容,才可以将该文件夹用delete删除。

window的删除动作,是从里往外删。注意:java删除文件不走回收站。要慎用。

3:获取

long length():获取文件大小。

String getName():返回由此抽象路径名表示的文件或目录的名称。

String getPath():将此抽象路径名转换为一个路径名字符串。

String getAbsolutePath():返回此抽象路径名的绝对路径名字符串。

String getParent():返回此抽象路径名父目录的抽象路径名,如果此路径名没有指定父目录,则返回 null。

long lastModified():返回此抽象路径名表示的文件最后一次被修改的时间。

File.pathSeparator:返回当前系统默认的路径分隔符,windows默认为 “;”。

File.Separator:返回当前系统默认的目录分隔符,windows默认为 “\

4:判断

boolean exists():判断文件或者文件夹是否存在。

boolean isDirectory():测试此抽象路径名表示的文件是否是一个目录。

boolean isFile():测试此抽象路径名表示的文件是否是一个标准文件。

boolean isHidden():测试此抽象路径名指定的文件是否是一个隐藏文件。

boolean isAbsolute():测试此抽象路径名是否为绝对路径名。

5:重命名

 boolean renameTo(File dest):可以实现移动的效果。剪切+重命名。

 String[] list():列出指定目录下的当前的文件和文件夹的名称。包含隐藏文件。

如果调用list方法的File 对象中封装的是一个文件,那么list方法返回数组为null。如果封装的对象不存在也会返回null。只有封装的对象存在并且是文件夹时,这个方法才有效。

创建一个新文件

File类的两个常量 

1
2
System.out.println(File.separator);//  \
System.out.println(File.pathSeparator);//  ;

删除一个文件

创建一个文件夹

文件的复制

采用DOS命令:copy d:\hello.txt d:\rollen.txt

使用程序来复制文件的基本思路还是从一个文件中读入内容,边读边写入另一个文件,就是这么简单:

然后在命令行下面

javac hello.java

java hello d:\hello.txt d:\rollen.txt

现在你就会在d盘看到rollen.txt了。

列出指定目录的全部文件(包括隐藏文件)

判断一个指定的路径是否为目录

搜索指定目录的全部内容

使用RandomAccessFile写入文件

Java.io.outputstream.PrintStream:打印流

  1. 提供了更多的功能,比如打印方法。可以直接打印任意类型的数据。
  2. 它有一个自动刷新机制,创建该对象,指定参数,对于指定方法可以自动刷新。
  3. 它使用的本机默认的字符编码. 
  4. 该流的print方法不抛出IOException。

该对象的构造函数:

  • PrintStream(File file)  :创建具有指定文件且不带自动行刷新的新打印流。 
  • PrintStream(File file, String csn) :创建具有指定文件名称和字符集且不带自动行刷新的新打印流。 
  • PrintStream(OutputStream out) :创建新的打印流。 
  • PrintStream(OutputStream out, boolean autoFlush) :创建新的打印流。 
  • PrintStream(OutputStream out, boolean autoFlush, String encoding) :创建新的打印流。 
  • PrintStream(String fileName) :创建具有指定文件名称且不带自动行刷新的新打印流。 
  • PrintStream(String fileName, String csn) 

PrintStream可以操作目的:1:File对象。2:字符串路径。3:字节输出流。

前两个都JDK1.5版本才出现。而且在操作文本文件时,可指定字符编码了。

当目的是一个字节输出流时,如果使用的println方法,可以在printStream对象上加入一个true参数。这样对于println方法可以进行自动的刷新,而不是等待缓冲区满了再刷新。最终print方法都将具体的数据转成字符串,而且都对IO异常进行了内部处理。

既然操作的数据都转成了字符串,那么使用PrintWriter更好一些。因为PrintWrite是字符流的子类,可以直接操作字符数据,同时也可以指定具体的编码。

PrintWriter:具备了PrintStream的特点同时,还有自身特点:

该对象的目的地有四个:1:File对象。2:字符串路径。3:字节输出流。4:字符输出流。【开发时尽量使用PrintWriter】

方法中直接操作文件的第二参数是编码表。

直接操作输出流的,第二参数是自动刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 读取键盘录入将数据转成大写显示在控制台.
BufferedReader bufr = new BufferedReader(new InputStreamReader(
        System.in));// 源:键盘输入
 
// 目的:把数据写到文件中,还想自动刷新。
PrintWriter out = new PrintWriter(new FileWriter("out.txt"), true);// 设置true后自动刷新
 
String line = null;
while ((line = bufr.readLine()) != null) {
    if ("over".equals(line))
        break;
    out.println(line.toUpperCase());// 转大写输出
}
// 注意:System.in,System.out这两个标准的输入输出流,在jvm启动时已经存在了。随时可以使用。当jvm结束了,这两个流就结束了。但是,当使用了显示的close方法关闭时,这两个流在提前结束了。
out.close();
bufr.close();

管道流

管道流主要可以进行两个线程之间的通信。

PipedOutputStream 管道输出流

PipedInputStream 管道输入流

  • 验证管道流

  • 打印流

  • 当然也可以格式化输出
  • 使用OutputStream向屏幕上输出内容
  • 输入输出重定向

    【运行结果】:eclipse的控制台输出的是hello。然后当我们查看d盘下面的hello.txt文件的时候,会在里面看到:iadanac!

    【运行结果】:你会在eclipse的控制台看到红色的输出:“这些在控制台输出”,然后在d盘下面的hello.txt中会看到:这些在文件中才能看到哦!

    【运行结果】:前提是我的d盘下面的hello.txt中的内容是:“这些文件中的内容哦!”,然后运行程序,输出的结果为:读入的内容为:这些文件中的内容哦!

BufferedReader

注意: BufferedReader只能接受字符流的缓冲区,因为每一个中文需要占据两个字节,所以需要将System.in这个字节输入流变为字符输入流,采用:

1
2
BufferedReader buf = new BufferedReader(
                new InputStreamReader(System.in));

下面给一个实例:

Scanner

其实我们比较常用的是采用Scanner类来进行数据输入,下面来给一个Scanner的例子吧

其实Scanner可以接受任何的输入流,下面给一个使用Scanner类从文件中读出内容:

数据操作流DataOutputStream、DataInputStream类

 现在我们在上面例子的基础上,使用DataInputStream读出内容

合并流 SequenceInputStream

 SequenceInputStream主要用来将2个流合并在一起,比如将两个txt中的内容合并为另外一个txt。下面给出一个实例:

文件压缩 ZipOutputStream类

上面的这个例子测试的是压缩单个文件,下面的们来看看如何压缩多个文件:

 大家自然想到,既然能压缩,自然能解压缩,在谈解压缩之前,我们会用到一个ZipFile类,先给一个这个例子吧。java中的每一个压缩文件都是可以使用ZipFile来进行表示的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.File;
import java.io.IOException;
import java.util.zip.ZipFile;
  
/**
 * ZipFile演示
 * */
public class ZipFileDemo{
    public static void main(String[] args) throws IOException{
        File file = new File("d:" + File.separator + "hello.zip");
        ZipFile zipFile = new ZipFile(file);
        System.out.println("压缩文件的名称为:" + zipFile.getName());
    }
}

 来看看如何加压缩文件了,和之前一样,先让我们来解压单个压缩文件(也就是压缩文件中只有一个文件的情况),我们采用前面的例子产生的压缩文件hello.zip

ZipInputStream类来解压一个压缩文件中包含多个文件

当我们需要解压缩多个文件的时候,ZipEntry就无法使用了,如果想操作更加复杂的压缩文件,我们就必须使用ZipInputStream类

 PushBackInputStream回退流

1
2
3
4
5
6
7
8
/**
 * 取得本地的默认编码
 * */
public class CharSetDemo{
    public static void main(String[] args){
        System.out.println("系统默认编码为:" + System.getProperty("file.encoding"));
    }
}

 乱码的产生:

 一般情况下产生乱码,都是由于编码不一致的问题。

对象的序列化

对象序列化就是把一个对象变为二进制数据流的一种方法。

一个类要想被序列化,就行必须实现java.io.Serializable接口。虽然这个接口中没有任何方法,就如同之前的cloneable接口一样。实现了这个接口之后,就表示这个类具有被序列化的能力。

先让我们实现一个具有序列化能力的类吧:

这个类就具有实现序列化能力,在继续将序列化之前,先看一下ObjectInputStream和ObjectOutputStream这两个类

当我们查看产生的hello.txt的时候,看到的是乱码,因为是二进制文件。

虽然我们不能直接查看里面的内容,但是我们可以使用ObjectInputStream类查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.File;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
  
/**
 * ObjectInputStream示范
 * */
public class ObjectInputStreamDemo{
    public static void main(String[] args) throws Exception{
        File file = new File("d:" + File.separator + "hello.txt");
        ObjectInputStream input = new ObjectInputStream(new FileInputStream(
                file));
        Object obj = input.readObject();
        input.close();
        System.out.println(obj);
    }
}

到底序列化什么内容呢?其实只有属性会被序列化。

Externalizable接口

被Serializable接口声明的类的对象的属性都将被序列化,但是如果想自定义序列化的内容的时候,就需要实现Externalizable接口。

当一个类要使用Externalizable这个接口的时候,这个类中必须要有一个无参的构造函数,如果没有的话,在构造的时候会产生异常,这是因为在反序列话的时候会默认调用无参的构造函数。

现在我们来演示一下序列化和反序列话:

【运行结果】:

姓名:rollen  年龄:20

本例中,我们将全部的属性都保留了下来,

Serializable接口实现的操作其实是吧一个对象中的全部属性进行序列化,当然也可以使用我们上使用是Externalizable接口以实现部分属性的序列化,但是这样的操作比较麻烦,

当我们使用Serializable接口实现序列化操作的时候,如果一个对象的某一个属性不想被序列化保存下来,那么我们可以使用transient关键字进行说明:

【运行结果】:
姓名:null  年龄:20
最后在给一个序列化一组对象的例子吧:

【运行结果】:

姓名:  hello  年龄:20

姓名:  world  年龄:30

姓名:  rollen  年龄:40

posted @   iadanac  阅读(339)  评论(0编辑  收藏  举报
编辑推荐:
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
阅读排行:
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 趁着过年的时候手搓了一个低代码框架
· 用 C# 插值字符串处理器写一个 sscanf
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!
点击右上角即可分享
微信分享提示