【Java】文件读写和输入输出

写在前面的话:

  1. 版权声明:本文为博主原创文章,转载请注明出处!
  2. 博主是一个小菜鸟,并且非常玻璃心!如果文中有什么问题,请友好地指出来,博主查证后会进行更正,啾咪~~
  3. 每篇文章都是博主现阶段的理解,如果理解的更深入的话,博主会不定时更新文章。
  4. 本文初次更新时间:2021.01.01,最后更新时间:2021.01.05

正文开始

1. 文件操作 - File

File用来表示文件系统中的一个文件或目录。

使用 File 可以:

  1. 访问其表示的文件或目录的属性信息(名字,大小等信息)
  2. 创建或删除文件及目录
  3. 访问一个目录中的子项

重点: File 不能访问文件数据(就是说不能读写文件内容)。

1.1 基本操作

关于File的一些基本操作:

  • String getName()
    获取文件名称
  • long length()
    获取文件长度
  • boolean canRead()
    判断文件是否可读,可读返回true
  • boolean canWrite()
    判断文件是否可写,可写返回true
  • boolean isHidden()
    判断文件是否隐藏,隐藏返回true

示例如下:

package file;

import java.io.File;

/**
 * File 用来表示文件系统中的一个文件或目录
 * @author returnzc
 */
public class FileDemo {
    public static void main(String[] args) {
        //访问当前项目目录下的 demo.txt 文件
        File file = new File("./demo.txt");
        
        //获取名字
        String name = file.getName();
        System.out.println(name);
        
        //获取长度(单位是字节)
        long len = file.length();
        System.out.println(len);
        
        boolean cr = file.canRead();
        boolean cw = file.canWrite();
        System.out.println("可读:" + cr);
        System.out.println("可写:" + cw);
        
        boolean ih = file.isHidden();
        System.out.println("是否隐藏:" + ih);
    }
}

运行结果:

demo.txt
18
可读:true
可写:true
是否隐藏:false

1.2 创建、删除文件

判断文件或目录是否存在:

  • boolean exists()
    判断当前 File 表示的文件或目录是否已经存在,存在返回 true

创建文件:

  • boolean createNewFile() throws IOException
    创建新文件,需要处理异常

创建文件示例:

package file;

import java.io.File;
import java.io.IOException;

/**
 * 使用 File 创建一个文件
 * @author returnzc
 *
 */
public class CreateNewFileDemo {
    public static void main(String[] args) throws IOException {
        //访问当前项目目录下的 test.txt 文件
        File file = new File("./test.txt");
        
        if (!file.exists()) {
            /*
             * 创建该文件
             * 需要注意,创建的文件所在的目录必须存在,否则会抛出异常。
             */
            file.createNewFile();
            System.out.println("文件已创建");
        } else {
            System.out.println("文件已存在");
        }
    }
}

删除文件、目录:

  • boolean delete()
    删除文件和目录。删除目录时只能删除空目录,非空目录需要挨个把所有子项都删掉才能删除。

删除文件:

package file;
import java.io.File;
/**
 * 删除一个文件
 * @author returnzc
 *
 */
public class DeleteFileDemo {
    public static void main(String[] args) {
        /*
         * 将当前项目目录下的test.txt文件删除
         * 在相对路径中,"./"是可以忽略不写的,默认就是从当前目录开始的。
         */
        File file = new File("./test.txt");
        if (file.exists()) {
            file.delete();
            System.out.println("文件已删除");
        } else {
            System.out.println("文件不存在");
        }
    }
}

1.3 创建、删除目录

创建目录:

  • boolean mkdir()
    创建目录,要求创建的目录所在的父目录必须存在
  • boolean mkdirs()
    创建多级目录,会将不存在的父目录一同创建出来

创建目录:

package file;
import java.io.File;
/**
 * 创建一个目录
 * @author returnzc
 *
 */
public class MkDirDemo {
    public static void main(String[] args) {
        File dir = new File("demo");
        
        if (!dir.exists()) {
            //创建该目录
            dir.mkdir();
            System.out.println("目录已创建");
        } else {
            System.out.println("目录已存在");
        }
    }
}

创建多级目录:

package file;
import java.io.File;
/**
 * 创建一个多级目录
 * @author returnzc
 *
 */
public class MkDirsDemo {
    public static void main(String[] args) {
        File dir = new File("a/b/c/d/e/f");
        
        if (!dir.exists()) {
            //mkdir()方法要求创建的目录所在的父目录必须存在
            //mkdirs()方法则会将不存在的父目录一同创建出来
            dir.mkdirs();
            System.out.println("创建完毕!");
        } else {
            System.out.println("目录已存在!");
        }
    }
}

删除目录:

package file;
import java.io.File;
/**
 * 删除目录
 * @author returnzc
 *
 */
public class DeleteDirDemo {
    public static void main(String[] args) {
        File dir = new File("a");
        
        if (dir.exists()) {
            //delete方法在删除目录时要求该目录必须是一个空目录
            dir.delete();
            System.out.println("目录已删除");
        } else {
            System.out.println("目录不存在");
        }
    }
}

1.4 其他操作

  • boolean isDirectory()
    判断是否为目录
  • boolean isFile()
    判断是否为文件
  • File[] listFiles()
    列出所有文件

列出指定目录的所有文件:

package file;
import java.io.File;
/**
 * 获取一个目录中的所有子项
 * @author returnzc
 *
 */
public class ListFilesDemo {
    public static void main(String[] args) {
        File dir = new File(".");  //当前目录
        
        if (dir.isDirectory()) {
            File[] subs = dir.listFiles();
            for (int i = 0; i < subs.length; i++) {
                System.out.println(subs[i].getName());
            }
        }
    }
}

运行结果:

6
.classpath
.project
.settings
bin
demo.txt
src

删除文件或目录(递归调用):

package file;
import java.io.File;
/**
 * 完成方法,实现将给定的File对象表示的文件或目录删除
 * @author returnzc
 *
 */
public class DeleteTestDemo {
    public static void main(String[] args) {
        File dir = new File("a");
        delete(dir);
    }

    public static void delete(File f) {
        if (f.isDirectory()) {
            //先将该目录清空
            File[] subs = f.listFiles();
            for (int i = 0; i < subs.length; i++) {
                File sub = subs[i];
                //先删除当前子项
                delete(sub);
            }
        }
        f.delete();
    }
}

1.5 文件过滤器

重载的ListFiles方法允许我们传入一个文件过滤器(作为参数),然后将目录中满足过滤器要求的子项返回。

文件过滤器是一个接口,如果用的话要实现类:

class MyFilter implements FileFilter {
    public boolean accept(File file) {
        String name = file.getName();
        return name.startsWith(".");
    }
}

示例:

package file;

import java.io.File;
import java.io.FileFilter;

/**
 * 重载的 ListFiles 方法,允许我们传入一个文件过滤器
 * 然后将目录中满足过滤器要求的子项返回
 */
public class FileDemo {
    public static void main(String[] args) {
        File dir = new File(".");

        /*
         * FileFilter 文件过滤器
         * 获取当前目录下所有文字以"."开头的子项
         */
        MyFilter filter = new MyFilter();
        File[] subs = dir.listFiles(filter);
        System.out.println(subs.length);
        for (int i = 0; i < subs.length; i++) {
            System.out.println(subs[i].getName());
        }
    }
}

class MyFilter implements FileFilter {
    public boolean accept(File file) {
        String name = file.getName();
        System.out.println("正在过滤:" + name);
        return name.startsWith(".");
    }
}

运行结果:

正在过滤:.classpath
正在过滤:.project
正在过滤:.settings
正在过滤:bin
正在过滤:demo.txt
正在过滤:src
3
.classpath
.project
.settings

可以用匿名内部类:

/*
* FileFilter 文件过滤器
*/
FileFilter filter = new FileFilter() {
    public boolean accept(File file) {
        String name = file.getName();
        System.out.println("正在过滤:" + name);
        return name.startsWith(".");
    }
};

能少写就少写:

File[] subs = dir.listFiles(new FileFilter() {
    public boolean accept(File file) {
        String name = file.getName();
        System.out.println("正在过滤:" + name);
        return name.startsWith(".");
    }
});

JDK8后有lambda表达式

File[] subs = dir.listFiles((file)->{
        String name = file.getName();
        System.out.println("正在过滤:" + name);
        return name.startsWith(".");
    }
});

最终可以简化为:

File[] subs = dir.listFiles(
    (file)->file.getName().startsWith(".")
);

2. RandomAccessFile

上面我们讲到 File 不能访问文件数据,接下来说一个可以读写文件数据的类RandomAccessFile,这个类是基于指针操作的。

这是一个专门用来读写文件数据的 API,RandomAccessFile基于指针对文件数据读写,可以移动指针读写任意位置,所以可以灵活的对文件数据进行编辑工作,即可以对文件随机访问。

2.1 创建 RandomAccessFile 实例

创建 RandomAccessFile 时有两种常见模式:

  • r:只读模式,只能读,如果文件不存在则会抛出异常。
  • rw:读写模式,如果文件不存在,则会自动创建该文件。

关于创建实例,以下两种方法都可以:

//方法 1
RandomAccessFile raf = new RandomAccessFile("raf.dat", "rw");

//方法 2
File file = new File("raf.dat");
RandomAccessFile raf = new RandomAccessFile(file, "rw");

因为有这两种常用的构造方法(还有其他构造方法,不过没有下面两种常用):

//构造方法 1
RandomAccessFile(String name, String mode) throws FileNotFoundException
//构造方法 2
RandomAccessFile(File file, String mode) throws FileNotFoundException

2.2 字节数据读写操作

2.2.1 写一个字节

一个简单的示例:

package raf;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 创建 RandomAccessFile 时有两种常见模式:
 * r:只读模式
 * rw:读写模式
 * 
 * @author returnzc
 *
 */
public class RandomAccessFileDemo {
    public static void main(String[] args) throws IOException {
        //向当前目录中的文件 raf.dat 中写入一个字节
        RandomAccessFile raf = new RandomAccessFile("raf.dat", "rw");
        
        raf.write(1);
        System.out.println("写出完毕!");
        
        raf.close();
    }
}

其中:

void write(int b) throws IOException

根据当前指针所在位置处向文件中写入1个字节,写入的是给定的 int 值所对应的二进制的低8位

例如:int 是4字节,每8位二进制一个字节,这里只是写入了低8位,具体为,整数1的二进制为 00000000 00000000 00000000 00000001,低8位即为00000001,所以 raf.write(1) 表示写入了 00000001

写操作完毕之后,需要close(),会释放文件的写操作,如果不释放可能会出现“正在读写不能删除”这样子。

上面的示例程序运行完毕后,项目目录下会出现一个 raf.dat 文件,大小为1字节,不过内容没有定义对应的编解码,所以打开看该文件的内容可能是乱码:
在这里插入图片描述
值得注意的是,write()每次创建后都是从头写的,这样就会覆盖之前的数据。并且一个字节无法表示int型的-1(int 型的 -1 为 11111111 11111111 11111111 11111111),只存低 8 位,所以只能写 0-255 之间的数据。

2.2.2 读一个字节

从文件中读取 1 个字节,并以 int 形式返回:

int read() throws IOException

若返回值为-1,则表示读取到了文件末尾。该方法会从文件中读取一个字节(8位),填充到 int 的低 8 位,剩下的高 24 位为 0,每次读取后自动移动文件指针,准备下次读取。

关于写的小示例:

package raf;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 读取文件数据
 * @author returnzc
 *
 */
public class ReadDemo {
    public static void main(String[] args) throws IOException {
        //从 raf.dat 文件中读取字节
        File file = new File("raf.dat");
        RandomAccessFile raf = new RandomAccessFile(file, "r");
        
        int d = raf.read();
        System.out.println("first: " + d);
        
        d = raf.read();
        System.out.println("second: " + d);
        
        raf.close();
    }
}

运行结果:

first: 1
second: -1

可以看到,第一次读的时候,我们读取了最开始写进去的 1 的低 8 位,第二次读的时候,由于这时已经读到了文件末尾,所以返回值为 -1。

上面我们提到一个字节无法表示int型的-1(int 型的 -1 为 11111111 11111111 11111111 11111111),只存低 8 位,所以只能写 0-255 之间的数据。做一个小测试,如果一定要使用 write() 写入 -1,那么再用 read() 读出来是多少呢?

简单测试一下就知道啦:

package raf;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileDemo {
    public static void main(String[] args) throws IOException {
        File file = new File("raf.dat");
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        
        raf.write(-1);
        System.out.println("写出完毕!");
        
        raf = new RandomAccessFile(file, "r");  //重新打开指针移到文件开始位置
        int d = raf.read();
        System.out.println("读出完毕:" + d);
        
        raf.close();
    }
}

运行结果:

写出完毕!
读出完毕:255

答案是 255。解释一下:int 型的 -1 为 11111111 11111111 11111111 11111111,写的时候只写入了低8位,即写入了 11111111,那么读的时候自然是读出了 00000000 00000000 00000000 11111111,就是 255 啦。

需要注意的是,这里在写入完毕之后,需要重新打开使指针移动到文件开始的位置,如果写完直接读,写完成时指针指向文件末尾,接着读的话返回值就是 -1 了,当然还有其他方法可以移动指针,而不用重新打开,这个等下会讲。

2.2.3 利用读、写进行复制操作

复制很简单,就是一边读一遍写的操作,把读到的数据原封不动地写出来就好了:

package raf;

import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 复制文件
 * @author returnzc
 *
 */
public class RandomAccessFileDemo {
    public static void main(String[] args) throws IOException {
        RandomAccessFile src = new RandomAccessFile("test.pdf", "r");  //原文件
        RandomAccessFile desc = new RandomAccessFile("cp.pdf", "rw");  //复制出来的文件

        //保存每次读取到的字节
        int d = -1;
        while ( (d = src.read()) != -1 ) {
            desc.write(d);
        }

        //复制完毕
        System.out.println("copy done!");
        src.close();
        desc.close();
    }
}
2.2.4 写一组字节

根据当前指针所在位置处,连续写出给定数组中的所有字节:

void write(byte b[]) throws IOException

根据当前指针所在位置处,连续写出给定数组中的部分字节,从 off 处开始,连续写 len 个字节:

void write(byte b[], int off, int len) throws IOException

示例:

package raf;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 写入一组字节
 * @author returnzc
 *
 */
public class RandomAccessFileDemo {
    public static void main(String[] args) throws IOException {
        File file = new File("raf.dat");
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        
        byte[] data = new byte[100];
        raf.write(data);
        System.out.println("写出完毕!");
        
        raf.close();
    }
}

可以看到这次写入的文件大小为 100 字节:
在这里插入图片描述

2.2.5 读一组字节

一次性读取给定字节数组总长度的字节量,并存入到该数组中,返回值为实际读取到的字节量,若返回值为 -1,则表示文件末尾:

int read(byte b[]) throws IOException

将给定字节数组从下标 off 处开始的连续 len 个字节一次性写出,返回值为实际读取到的字节量:

int read(byte b[], int off, int len) throws IOException

示例:

package raf;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 读出一组字节
 * @author returnzc
 *
 */
public class RandomAccessFileDemo {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("raf.dat", "rw");

        int len = -1;
        len = raf.read(data);
        System.out.println("读出完毕: " + len);
        
        raf.close();
    }
}

运行结果:

读出完毕: 100
2.2.6 改进复制文件

上面我们写到利用读写来复制文件,不过当时读取和写入都是 1 字节,速度很慢:

package raf;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 复制文件,字节读写
 * @author returnzc
 *
 */
public class CopyDemo {
    public static void main(String[] args) throws IOException {
        RandomAccessFile src = new RandomAccessFile("服务器.xmind", "r");
        RandomAccessFile desc = new RandomAccessFile("服务器_cp.xmind", "rw");
        long start = System.currentTimeMillis();
        
        int d = -1;    //保存每次读取到的字节
        while ((d = src.read()) != -1) {
            desc.write(d);
        }
        long end = System.currentTimeMillis();
        
        //复制完毕了
        System.out.println("复制完毕!耗时:" + (end - start) + "ms");
        src.close();
        desc.close();
    }
}

运行结果:

复制完毕!耗时:700ms

接下来我们改进一下,读、写都是一组字节:

package raf;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 提高每次读写的数据量,减少实际发生的读写次数,可以提高读写效率。
 * @author returnzc
 *
 */
public class CopyDemo2 {
    public static void main(String[] args) throws IOException {
        RandomAccessFile src = new RandomAccessFile("服务器.xmind", "r");
        RandomAccessFile desc = new RandomAccessFile("服务器_cp.xmind", "rw");
        
        byte[] data = new byte[1024];    //1KB
        int len = -1;    //每次实际读取到的字节量
        
        long start = System.currentTimeMillis();
        
        while((len = src.read(data)) != -1) {
            desc.write(data, 0, len);
        }
        long end = System.currentTimeMillis();
        
        System.out.println("复制完毕!耗时:" + (end - start) + "ms");
        src.close();
        desc.close();
    }
}

运行结果:

复制完毕!耗时:2ms

单字节读写的模式,一般称为随机读写;一组字节读写的模式一般称为块读写。可见使用块读写,复制的效率大大提高。不过需要注意的是,每次读写的大小需要考虑一下硬盘的读写上限哟。

2.3 文件指针操作

RandomAccessFile 的读写操作都是基于指针的,也就是说总是在指针当前所指向的位置进行读写操作。

2.3.1 getFilePointer() 方法

该方法用于获取当前 RandomAccessFile 的指针位置:

long getFilePointer() throws IOException

小示例:

/*
 * long getFilePointer()
 * 获取当前 RandomAccessFile 的指针位置
 */
long pos = raf.getFilePointer();
System.out.println("pos: " + pos);    //pos: 0(类似数组下标,第1个字节的位置)
2.3.2 seek() 方法

该方法用于移动当前 RandomAccessFile 的指针位置:

void seek(long pos) throws IOException

小示例:

raf.seek(0);  //将指针移动到文件开始处(第一个字节的位置)

2.4 基本类型读写

2.4.1 如何写入一个 int 值

上面我们讲到字节读写,那么如何利用单个字节读写来写入一个 int 值呢。

答案是利用位运算(>>>)

假设需要写入的值的二进制为 01111111 11111111 11111111 11111111,那么我们可以先把高8位01111111取出来写入,然后再写次高8位,一直到把低8位都写进去。如下:

假设:max = 01111111 11111111 11111111 11111111
执行:int d = max >>> 24;
此时:d = 00000000 00000000 00000000 01111111

测试一下:

package raf;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * RAF读写基本类型数据,以及RAF基于指针的读写操作
 * 
 * RAF总是在指针指向位置要么读,要么写一个字节,并且无论是读还是写进行
 * 后,指针都会自动向后移动一个字节。
 * @author returnzc
 *
 */
public class RandomAccessFileDemo2 {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("int.dat", "rw");
        
        long pos = raf.getFilePointer();
        System.out.println("pos: " + pos);    //pos: 0
        
        //将一个int最大值写入到文件中
        int max = Integer.MAX_VALUE;
        raf.write(max >>> 24);    //高8位
        System.out.println("*pos: " + raf.getFilePointer());  //pos: 1
        raf.write(max >>> 16);    //次高8
        System.out.println("*pos: " + raf.getFilePointer());  //pos: 2
        raf.write(max >>> 8);     //次低8位
        System.out.println("*pos: " + raf.getFilePointer());  //pos: 3
        raf.write(max);           //最低8位
        System.out.println("*pos: " + raf.getFilePointer());  //pos: 4
        
        System.out.println("写出int值完毕!");
        raf.close();
    }
}

运行结果:

pos: 0
*pos: 1
*pos: 2
*pos: 3
*pos: 4
写出int值完毕!

可以看到生成的文件为 4 字节:
在这里插入图片描述
但是像上面那样写会很麻烦,而这里有一个提供给我们的方法:

void writeInt(int v) throws IOException

该方法可以一次性写4个字节,将给定的 int 值写出。

可以看一下该方法的源码,跟我们刚刚讲的方法是一样滴:

public final void writeInt(int v) throws IOException {
    write((v >>> 24) & 0xFF);
    write((v >>> 16) & 0xFF);
    write((v >>>  8) & 0xFF);
    write((v >>>  0) & 0xFF);
    //written += 4;
}
2.4.2 各种基本类型读写

看名字就懂啦:

void writeBoolean(boolean v)
void writeByte(int v)
void writeShort(int v)
void writeChar(int v)
void writeInt(int v)
void writeLong(long v)
void writeFloat(float v)
void writeDouble(double v)

小示例:

raf.writeInt(123);         //4字节
raf.writeLong(123L);       //8字节
raf.writeDouble(123.123);  //8字节

2.5 字符串读写

byte[] getBytes()
byte[] getBytes(String charsetName) throws UnsupportedEncodingException

3. 基本 IO 操作

4. 文件数据 IO 操作

占个坑,不要急,正在写~~

在这里插入图片描述

参考

相关文章

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值