Java IO

前言

文件层面上面上的处理在日常生活中十分常见,Java提供了操作文件的各种方法,包括文件的创建目录的创建以及怎么在一个文件中写入数据或者从一个已有的文件中读取我们需要的信息到程序中,此文主要总结了File类的使用,字节流以及字符流的知识

File文件操作类

File类是唯一一个与文件本身操作有关的程序类

  • 不论是文件还是目录都使用File类来操作
  • File类只提供操作文件或目录的方法,不参与文件和目录本身内部的内容

    File类的使用

    创建单一文件: createNewFile()

    首先需要有一个File类的对象,通过对象调用方法来创建一个新的文件.createNewFile()此方法的返回值是一个boolean类型,如果指定路径下没有当前文件便可以创建一个文件
    特别注意:如果只是想用此方法单一的创建一个文件,那么就不要指定更长的路径.例如: e:/abc.txt,当使用createNewFile方法创建文件时就会直接在e盘下创建指定的文件abc.txt; 如果路径设置为:e:/新建文件夹/abc.txt 本意是想在e盘下创建一个新建文件夹在再其内部创建一个.txt文件,此时会出现错误,单单使用该方法是不够的,因为此方法只是创建一个单一文件,他不会去创建文件所依赖的目录
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class MakeANewFile {

    public static void main(String[] args) {
    /**
    * 创建一个新文件
    */

    //实例化File对象
    File newFile = new File("e:\\JavaIO测试文件夹");

    try {
    newFile.createNewFile();
    System.out.println("ok");
    } catch (IOException e) {
    e.printStackTrace();
    }

    }
    }

对文件的基本操作

File类本身只是创建文件,但是对于其内容是不做处理的

  • 判断文件是否存在: exists()
  • 删除文件: delete
    需求: 给定某路径,如果文件存在则删除文件,如果文件不存在则创建文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class 基本方法的使用 {

    public static void main(String[] args) {
    File file = new File("e:\\又是一个JavaIO");

    if(file.exists()){
    file.delete();
    }else{
    try {
    file.createNewFile();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    }
    }

文件操作时的两个问题

文件路径分隔符到底用哪个

在使用File类来创建文件的时候我们需要指定好文件的路径,在windows系统下我们可以直接写为: e:\\xxxx ,但是如果这段代码拿到了Linux系统下去执行,显然这个路径就不对了,因为在linux下路径的分割符应该使用 ‘/‘.如果在再其他不同的环境下运行,那么分割符又可能是另外一个样子了. 为了解决可移植性的问题,在File类中引入了一个静态常量:

1
public static final String separator

separator就代表了一个分割符,我们用separator代替分割符后,程序在不同环境中运行会被当前的JVM解析为所适配的分隔符

1
2
//路径分隔符使用File类所提供的separator
File file = new File("e:"+File.separator+"又是一个JavaIO");

避免出现同名文件

在Java中要进行文件处理的操作需要得到操作系统的支持,如果出现同名文件,那么再进行文件处理的时候则会出现延迟的现象

目录操作

层级创建目录: mkdirs()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class 创建一个目录 {

public static void main(String[] args) throws IOException {
//创建File对象指定文件路径(创建的文件会处于最后一个子目录下,即zzzz中)
File file2 = new File("e:/zzzz/zzz/zzzz");

//mkdirs(): 创建目录,无论有多少级都会进行创建
//getParentFile(): 取得父File对象-->返回一个File
//创建目录时,最好先获得父路径的File类对象
if(!file2.getParentFile().exists()){
file2.getParentFile().mkdirs();
}

//getParent 取得父路径对象: 当前处于路径下的最后一个文件夹内部,所以父路径是最后一个文件的上一个文件的路径
String fathPath = file2.getParent();
System.out.println(fathPath);

//创建文件
if(file2.exists()){
file2.delete();
System.out.println("删除成功");
}else{
//createNewFile: 只是创建一个文件,他不会去创建一个目录
file2.createNewFile();
}
}
}

解释:
File类是一个即能操作文件又能够操作目录的类,但是他不能对文件具体内容进行更改只是单纯的操作文件/目录
1.File类中createNewFile和mkdirs的区别
createNewFile是创建一个文件.他会在指定路径的最后一个子路径内创建一个新的文件; 而mkdirs他是按照指定路径层级的创建目录直至最后一个子目录之前
2.getParent和getParentFile的区别

  • 什么是父路径,什么是子路径?
    举个例子,有一个文件my.txt,它的绝对路径为D:\Program\myfile\my.txt,这里有三个目录。
    D:\,D:\Program\,D:\Program\myfile\。前者就是后者的父路径。

其实File类的对象就可以理解为一个文件(存在于指定路径中最小子路径中的一个文件),getParent返回的是该文件的父目录是一个String类型的字符串;而getParentFile返回的是一个File类对象,他是父路径的抽象对象.这就是两者的区别
3.File file2 = new File(“e:/zzzz/zzz/zzzzl”)语句中的路径究竟是文件路径还是目录路径
再该路径中最后一个子路径即zzzzl指的就是文件!!!,即我们使用createNewFile后产生的那个文件!!其他前面的都代表的是目录
4.为什么在创建目录是判断父File文件是否存在?
new File(“xxxx”);永远指代的是一个文件,既然是一个文件,我们肯定要先判断文件的路径是否存在吗,父File对象就是这个路径的抽象,所以要先进行判断,在mkdirs

究竟是目录还是文件总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class 取得文件信息 {

public static void main(String[] args) throws IOException {
/**
*总结: File对象代表的永远是一个文件对象; 例如.txt,.xml ....
* 但是File对象可以同时操作文件和目录
* mkdirs: 创建目录
* createNewFile: 创建文件
* File参数中的路径代表的是目录,而最后一个子路径代表的是文件(ccc.txt)!!!
*
*/

//给定整个文件的路径(包括了文件所处的目录+将要创建的文件的名称类型等)
File file = new File("e:/aaaa/bbbb/cccc.txt");

//先创建目录
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}

//创建文件
if (!file.exists()){
file.createNewFile();
}
}
}

文件信息

File可以操作文件也可以操作目录,很容易混淆一个File对象到底是文件还是一个目录,在File类中就提供了辨别的方法

  • 判断路径是否是文件: public boolean isFile()
  • 判断路径是否是目录: public boolean isDirectory()
  • 取得文件大小(字节): public long length()
  • 最后一次修改日期 : public long lastModified()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    //文件的一些基本方法操作

    public static void code1() throws IOException {
    File file = new File("e:/abcd/ade/aaaa/");

    if(!file.getParentFile().exists()){
    file.getParentFile().mkdirs();
    }

    if(!file.exists()){
    file.createNewFile();
    System.out.println("创建成功");
    }else {
    file.delete();
    System.out.println("删除成功");
    }

    //判断是否是一个文件
    if(file.exists() && file.isFile()){
    System.out.println("是一个文件");
    //输出文件的大小
    System.out.println("文件大小为:"+file.length());
    //文件最后一次修改时间
    System.out.println("文件的最后一次修改日期为:"+new Date(file.lastModified()));
    }

    //是否是目录
    System.out.println(file.isFile()+" "+file.isDirectory());
    }

字节流与字符流

File类只是操作文件的一个类,文件具体的内容属性他是没有办法处理的,如果要处理文件内容,就必须通过流的操作模式来完成. 流分为输出流和输入流
在java.io包中,流分为两种,字节流和字符流

  • 字节流:OutputStream(字节输出流) InputStream(字节输入流)
  • 字符流:Writer(字符输出流),Reader(字符输入流)

    什么是流

    “流”可以想作是水流,在我们生活中水管打开后,水就会从自来水厂中出发经过管道流至我们家中的水龙头处最终供我们使用,Java 流操作也是相同道理,只不过此时流的不是水,而是字节.通过的不是管道.而是网络或者操作系统. 当我们从某个地方获取流到终端时,叫做输入流;从终端出发,流向其他终端时,叫做输出流

    I/O操作基本流程

    1.根据文件路径创建File类对象
    2.根据字节流或字符流的子类实例化对象
    3.进行数据的读取或写入操作
    4.关闭流(close)
    所有类似于I/O操作的资源处理类操作最后必须要进行关闭

    字节流

    字节输出流(OutputStream)

    OutputStream是一个抽象类,必须用他的子类来进行对象的实例化,处理文件,我们使用他的子类FileOutputStream.
    实现文件的内容输出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class FileOutputStreamTest {

    public static void main(String[] args) throws IOException {
    //创建File对象
    File file = new File("e:/JavaIO练习文件夹/字节数输出流文件夹/内容.txt");

    //创建目录
    if(!file.getParentFile().exists()){
    file.getParentFile().mkdirs();
    }

    //创建输出流对象,并指明要输出给的对象是谁
    OutputStream outputStream = new FileOutputStream(file);

    //输出内容
    String msg = "来自终端的一句话";

    //调用输出流对象的输出方法,将目标内容输出
    //此处需要注意,这里是字节流的对象,所以输出时要以字节的方式输出 msg 从charactor-->字节bytes
    outputStream.write(msg.getBytes());

    //关闭流
    outputStream.close();
    }
    }

此段代码应该注意到的:

  1. 我们只需要创建好目录(mkdirs),不需要createNewFile来创建好文件,输出流会帮助我们自动创建目标文件
  2. 既然是字节输出流,就要把我们需要传递的字符在传递时转换为字节类型.否则会出现受查异常
  3. 重复执行此代码会发现文件内容是覆盖的而不是追加.如果想要程序出现内容追加的效果,则应使用FileOutputStream的另外一种构造方法
    1
    2
    1. 接收File类(覆盖):public FileOutputStream(File file) throws FileNotFoundException
    2. 接收File类(追加):public FileOutputStream(File file, boolean append)

部分内容输出
write的重载方法,传入参数指定输出哪几个字节的内容

1
2
void write(byte[] b, int off, int len) 
将 len字节从位于偏移量 off的指定字节数组写入此文件输出流。

流的自动关闭

当我们使用完I/O操作时,都需要手动的进行资源连接的关闭操作,在JDK1.7之后,新增了一个自动关闭接口,他可以在我们使用完资源处理类的操作后自动关闭
继承自动关闭接口的类实现自动关闭事例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class AutoCloseAbleTest {

public static void main(String[] args) {

try(Message message = new Message()){
message.print();
}catch(Exception e){
System.out.println("出现异常");
}

}
}

class Message implements AutoCloseable {

public Message(){
System.out.println("类对象创建完成");
}

public void print(){
System.out.println("自动关闭类的一个简单方法");
}

@Override
public void close() throws Exception {
System.out.println("已经自动关闭");
}
}

这种方法有一个不好的地方就是该方法需要结合try-catch语句一起使用,自动关闭的对象的创建必须在try中进行
上面输出字节流采用自动关闭的写法:
由于输出输入流都是继承过该接口的,所以我们直接按照规则直接使用就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//自动关闭
public static void code2(){
//创建File对象
File file = new File("e:/JavaIO练习文件夹/字节数输出流文件夹/内容.txt");

//创建目录
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}

String msg = "今天是个好天气,太阳很大";
//创建输出流对象-->要求能够自动关闭
//在try语句中直接创建对象即可使用自动关闭的功能
try(OutputStream outputStream = new FileOutputStream(file)){
//写出去
outputStream.write(msg.getBytes());
}catch(Exception e){
e.printStackTrace();
}
}

字节输入流(InputStream)

输入流: 从本地文件读取信息到程序
同字节输出流一样,这也是一个抽象类,我们使用它的子类FileInputStream来实现
读取文件基本步骤:

  1. 创建文件对象
  2. 创建输入流对象
  3. 指定每次读取的最大长度
  4. 使用read方法开始读取数据

关于read方法:
输入流中读取文件使用read方法,此方法共有三种不同的重载方式:

  1. public abstract int read() throws IOException; –> 此种方式一次读一个字节,直到没有数据了返回 -1
  • 显然这是一种很不高效的方法

2.public int read(byte b[]) throws IOException 常用 –>此种方式一次读取指定字节数组大小的内容

  • 如果读取的文本内容>数组大小 –>返回 数组的大小
  • 如果读取的文本内容<数组大小 –>返回 读取的个数
  • 如果没有数据了还在读 –>返回 -1
  1. public int read(byte b[], int off,int len) throws IOException –> 读取部分数据到字节数组中,此种方式每次只读取传递数组的部分内容
  • 如果读取满了则返回长度(len),如果没有
  • 读取满则返回读取的数据个数,如果读取到最后没有数据了返回-1

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class InputStreamTest {

public static void main(String[] args) throws IOException {
//创建文件对象
File file = new File("E:\\JavaIO练习文件夹\\字节数输出流文件夹\\作文2.txt");


//首先要保证文件存在
if(file.isFile() && file.exists()){

//创建输入流对象,并指定要输入的对象
InputStream inputStream = new FileInputStream(file);

//每次可以读取的最大数量
byte arr[] = new byte[1024];


//开始读,此时的数据读取到了数组中
/**
* 1.如果读取的数据大于数组长度,返回数组的长度
* 2.读取的数据小于数组长度,返回读取的数据大小
* 3.已经读完还在继续读,返回-1
*/
while (inputStream.read(arr) != -1) {

//将字节数组转换为String
String result = new String(arr,0,arr.length);

System.out.println(result);

}

inputStream.close();
}
}
}

注意问题:
1.read的使用问题
2.在从文件中把数据读入自己定义好的数组中时要注意此时数据是字节类型的,我们还需要把他们变为字符串
3.还要注意自己读取的文件编码格式是否和自己程序所默认的编码格式一致,否则会出现乱码的情况

字符流

字符输出流(Writer)

字符输出流和字节输出流的最大区别在于字符输出流对中文提供了很好的支持,可以直接将字符串输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class WriterTest {

public static void main(String[] args) throws IOException {
File file = new File("E:\\JavaIO练习文件夹\\测试文件夹\\内容.txt");

if(file.isFile() && file.exists()){
Writer writer = new FileWriter(file);
String msg = "我要输出的内容";
writer.write(msg);
writer.close();
}

}
}

可以看到Writer和字节输出流使用基本一样,但是他更支持中文,可以直接将字符串输出

字符输入流

Reader依然也是一个抽象类。如果要进行文件读取,同样的,使用FileReader
Writer有可以直接输出字符串的方法write,但Reader没有可以直接读取字符串的方法,所以需要我们创建一个字符数组进行接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ReaderTest {
public static void main(String[] args) throws IOException {

File file = new File("E:\\JavaIO练习文件夹\\测试文件夹\\作文2.txt");

if(file.isFile() && file.exists()){
Reader reader = new FileReader(file);
char[] arr = new char[100];
while(reader.read(arr)!=-1){
String result = new String(arr,0,arr.length);
System.out.println(result);
}
reader.close();
}else{
System.out.println("文件不存在");
}
}
}

与字节输出流基本一致,就是接收数据所用的数组类型不同

字符流刷新操作 flush()

如果字符流不关闭,数据就有可能保存在缓存中并没有输出到目标源。这种情况下就必须强制刷新才能够得到完整
数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WriterTest {

public static void main(String[] args) throws IOException {
File file = new File("E:\\JavaIO练习文件夹\\测试文件夹\\内容.txt");

if(file.isFile() && file.exists()){
Writer writer = new FileWriter(file,true);
String msg = "我要输出的内容";
writer.write(msg);
//不关闭流,使用字符流刷新操作强制输出
//表示强制清空缓冲内容,所有内容都输出
writer.flush();

//当然我们还是要记得关闭流
}
}
}

字符流与字节流的区别

他们的本质区别就一句话: 字节流是原生的操作而字符流是经过处理后的操作
为什么这样说呢?—->>
因为在真正传输的过程中(网络数据传输,磁盘数据传输),传送的都是字节,而不是字符, 那为什么要有字符流呢?—->>
因为字符流更加适合于处理中文,所有在磁盘中的数据都是以二进制的形式保存的,如果想要读取这些数据,就必须先将这些二进制数据读取到内存,而内存中会帮助我们把字节变为字符
区别:

  • 字节流操作的基本单位为字节;字符流操作的基本单位为Unicod码元(具体的字符类型要根据不同的编码规则确定 一个Unicode码元 = 2个字节)
  • 字节流默认不使用缓冲区;字符流默认使用缓冲区(因为字符流在读取/写入硬盘文件时,需要将二进制转换为字符,即编码/解码)
  • 字节流通常用于处理二进制数据,实际上他可以处理任意类型的数据,但他不支持直接写入或读取Unicode码元;字符流通常处理文本数据,他支持直接写入或读取Unicode码元

通过上述这一系列流的讲解可以发现,使用字节流和字符流从代码形式上区别不大。但是如果从实际开发来讲,字
节流一定是优先考虑的,只有在处理中文时才会考虑字符流。因为所有的字符都需要通过内存缓冲来进行处理。
所有字符流的操作,无论是写入还是输出,数据都先保存在缓存中
(所有的字符流都需要通过内存缓冲来进行处理,因为真正保存在硬盘上的还是二进制的代码。)

转换流

可以将字节流转换成为字符流,以便于对中文更加方便的操作
字符输出转换流: OutputStreamWriter
字符输入转换流: InputStreamRerder

字符输出转换流示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class 转换流 {

public static void main(String[] args) throws IOException {
File file = new File("E:\\JavaIO练习文件夹\\测试文件夹\\内容.txt");

if(file.exists()){
OutputStream outputStream = new FileOutputStream(file);

//字节流转换为字符流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
outputStreamWriter.write("字节流转换成为了字符流");
outputStreamWriter.close();

}
}
}

此举意义不大,但是能够帮助我们理解继承关系

实战: 给定两个文件,实现文件copy功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//未封装过的文件拷贝代码
public static void code(){

File srcFile = new File("E:\\JavaIO练习文件夹\\测试文件夹\\作文2.txt");
File destFile = new File("E:\\JavaIO练习文件夹2\\测试文件夹\\目标文件.txt");

if(!destFile.getParentFile().exists()){
destFile.getParentFile().mkdirs();
System.out.println("目录创建成功");
}

if(srcFile.isFile() && srcFile.exists()){
System.out.println("文件存在,可以继续进行操作");
//创建输入流读取文件
//创建输出流写入文件
try (InputStream inputStream = new FileInputStream(srcFile);
OutputStream outputStream = new FileOutputStream(destFile)){
byte[] arr = new byte[1024];
//将文件内容读入数组
while(inputStream.read(arr)!=-1){

//打印测试输入流是否成功读取文件
// String result = new String(arr,0,arr.length);
// System.out.println(result);
//将数组内的内容写入目标文件
outputStream.write(arr,0,arr.length);
System.out.println("copy成功");
}

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

}else{
System.out.println("文件不存在");
}
}

没有经过封装的代码用户十分难用,不能从外界传入参数使得每次路径的修改都要变动源码,所以采用面向对象封装的思想写出一个copy文件的工具类,在工具类中定义我们需要的方法,在次进行文件拷贝的时候直接调用工具类就好了,这样一来使得main方法更加的简介,代码也更加明了

工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//copyUntil工具类
public class CopyUnitl {

//判断源文件是否存在
public boolean isFileExist(String srcFile){
return new File(srcFile).exists();
}

//给目标文件创建目录
public void createDestFilePath(String destFile){
File file = new File(destFile);
if(!file.getParentFile().exists()){
System.out.println("目标目录开始创建...");
file.getParentFile().mkdirs();
System.out.println("目标目录创建完成");
}else{
System.out.println("目标目录存在");
}
}

//实例化两个文件对象,准备开始拷贝
public boolean copyPrepare(String srcFilePath,String destFilePath){
//源文件对象
File srcFile = new File(srcFilePath);
//目标文件对象
File destFile = new File(destFilePath);

try(InputStream inputStream = new FileInputStream(srcFile);
OutputStream outputStream = new FileOutputStream(destFile)){
copying(inputStream,outputStream);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}

//具体拷贝方法: 使用同一块内存,将读出来的数据直接写入目标文件
private void copying(InputStream inputStream,OutputStream outputStream) throws IOException {
byte[] arr = new byte[1024];
while(inputStream.read(arr)!=-1){
outputStream.write(arr,0,arr.length);
}
}

}

主方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//主方法
public class Test {
public static void main(String[] args) {
if(args.length != 2){
System.out.println("请传入两个正确的路径");
return;
}

//创建工具类
CopyUnitl copyUnitl = new CopyUnitl();

//取得源文件和目标文件的路径
String srcFile = args[0];
String destFile = args[1];

//判断源文件是否存在,目标文件路径是否创建完成
if(copyUnitl.isFileExist(srcFile)){
//源文件存在则开始准备目标文件
//判断目标文件目录是否创建,如果没有则创建目录
copyUnitl.createDestFilePath(destFile);
//开始拷贝
copyUnitl.copyPrepare(srcFile,destFile);
}
}
}