读写文件

读写 CSV 文件

CSV: Comma-Separated Values 逗号分隔的文本文件 读写csv文件和读写普通文件类似;写的时候给数据之间添加上逗号。

设定存储路径和文件名:

private static final String FILE_FOLDER =
        Environment.getExternalStorageDirectory().getAbsolutePath()
        + File.separator + "AboutView" + File.separator + "data";
private static final String FILE_CSV = "about_data.csv";

获取应用内的存储路径

File appDir = getFilesDir();
    if (appDir.exists()) {
        Log.d(TAG, "appDir.exists(): " + appDir.exists());
        Log.d(TAG, "appDir.isDirectory(): " + appDir.isDirectory());
        Log.d(TAG, "getFilesDir().getAbsolutePath(): " + getFilesDir().getAbsolutePath());
        for (File c : appDir.listFiles()) {
            if (c.isDirectory()) {
                Log.d(TAG, c.getAbsolutePath() + File.separator);
            } else {
                Log.d(TAG, c.getAbsolutePath());
            }
        }
    }

// appDir.exists(): true
// appDir.isDirectory(): true
// getFilesDir().getAbsolutePath(): /data/data/com.rustfisher.anmediacodec/files
// /data/data/com.rustfisher.anmediacodec/files/a.log
// /data/data/com.rustfisher.anmediacodec/files/log_folder/

写CSV文件

写文本文件。使用 FileOutputStream 来向文件尾部添加数据 FileOutputStream.write(byte[] data) 向文件流写入字节数据。

class WriteData2CSVThread extends Thread {

        short[] data;
        String fileName;
        String folder;
        StringBuilder sb;

        public WriteData2CSVThread(short[] data, String folder, String fileName) {
            this.data = data;
            this.folder = folder;
            this.fileName = fileName;
        }

        private void createFolder() {
            File fileDir = new File(folder);
            boolean hasDir = fileDir.exists();
            if (!hasDir) {
                fileDir.mkdirs();// 这里创建的是目录
            }
        }

        @Override
        public void run() {
            super.run();
            createFolder();
            File eFile = new File(folder + File.separator + fileName);
            if (!eFile.exists()) {
                try {
                    boolean newFile = eFile.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            try {
                FileOutputStream os = new FileOutputStream(eFile, true);
                sb = new StringBuilder();
                for (int i = 0; i < data.length; i++) {
                    sb.append(data[i]).append(",");
                }
                sb.append("\n");
                os.write(sb.toString().getBytes());
                os.flush();
                os.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

读CSV文件

使用 BufferedReader 读取每一行内容;读出来的数据带有逗号分隔符

class ReadCSVThread extends Thread {

        String fileName;
        String folder;

        public ReadCSVThread(String folder, String fileName) {
            this.folder = folder;
            this.fileName = fileName;
        }

        @Override
        public void run() {
            super.run();
            File inFile = new File(folder + File.separator + fileName);
            final StringBuilder cSb = new StringBuilder();
            String inString;
            try {
                BufferedReader reader =
                    new BufferedReader(new FileReader(inFile));
                while ((inString = reader.readLine()) != null) {
                    cSb.append(inString).append("\n");
                }
                reader.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mCSVTv.setText(cSb.toString());// 显示
                }
            });
        }

    }

读写二进制文件

以流的形式来读写文件

写文件

主要代码:

DataOutputStream os = new DataOutputStream(new FileOutputStream(file, true));

这里的 true 表示在文件尾追加数据。 写二进制文件时,文件路径和文件名分开传递。如果路径不存在,则用 mkdirs() 创建;若 文件不存在,用 createNewFile() 来创建新文件。

DataOutputStream 的写数据方法各有不同(以 Galaxy Note4 为例)

  • writeByte 写入1个字节
  • writeInt 写入4个字节 big-endian
  • writeFloat 写入4个字节 使用 Float 类中的 floatToIntBits 方法将 float 参数转换 为一个 int 值,然后将该 int 值以 4-byte 值形式写入基础输出流中,先写入高字节。 如果没有抛出异常,则计数器 written 增加 4。
  • writeDouble 使用 Double 类中的 doubleToLongBits 方法将 double 参数转换为一个 long 值,然后将该 long 值以 8-byte 值形式写入基础输出流中,先写入高字节。如果没有抛出异常,则计数器 written 增加 8。

其他方法可以参见 DataOutputStream 的API文档

class SaveHexFileThread extends Thread {
    private int mmValue1;
    private int mmValue2;
    private int mmValue3;
    private String mmDir;
    private String mmName;
    private HEX_TYPE type;

    // 文件路径和文件名分开传送
    public SaveHexFileThread(HEX_TYPE type, int v1, int v2, int v3,
                             String dir, String name) {
        this.mmValue1 = v1;
        this.mmValue2 = v2;
        this.mmValue3 = v3;
        mmDir = dir;
        mmName = name;
        this.type = type;
    }

    public void saveData() {
        try {
            File file = new File(mmDir + File.separator + mmName);
            if (!file.exists()) {
                boolean newFile = file.createNewFile();
                Log.d(TAG, "创建新文件 " + file.getName() + " " + newFile);
            }
            DataOutputStream os = new DataOutputStream(new FileOutputStream(file, true));
            switch (type) {
                case BYTE:
                    os.writeByte(mmValue1);
                    os.writeByte(mmValue2);
                    os.writeByte(mmValue3);
                    break;
                case INT:
                    os.writeInt(mmValue1);
                    os.writeInt(mmValue2);
                    os.writeInt(mmValue3);
                    break;
                case FLOAT:
                    os.writeFloat(mmValue1);
                    os.writeFloat(mmValue2);
                    os.writeFloat(mmValue3);
                    break;
                case DOUBLE:
                    os.writeDouble(mmValue1);
                    os.writeDouble(mmValue2);
                    os.writeDouble(mmValue3);
                    break;
            }
            os.close();
        } catch (IOException ioe) {
            Log.e(TAG, "write data error ", ioe);
        }
    }

    private void createFiles() {
        File fileDir = new File(mmDir);
        boolean hasDir = fileDir.exists();
        if (!hasDir) {
            fileDir.mkdirs();// 这里创建的是目录
        }
    }

    @Override
    public void run() {
        super.run();
        createFiles();
        saveData();
        new ReadAllHexThread(mmDir).start();
    }
}

读取文件

以什么形式写的文件,就以什么形式读取。以免弄错数据。 使用 DataInputStream 来读取数据,拼接后显示出来。 关键代码:

File file = new File(mmDir + File.separator + fileName);
try {
    if (file.exists()) {
        DataInputStream is = new DataInputStream(new FileInputStream(file));
        int readRes = is.read(dataBuffer);
        StringBuilder contentBuilder = new StringBuilder(type.toString());
        contentBuilder.append(": \n");
        for (int i = 0; i < readRes; i++) {
            contentBuilder.append(Integer.toHexString(dataBuffer[i] & 0xff));
            contentBuilder.append(", ");
        }
        final String finalContent = contentBuilder.toString();
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // update UI
            }
        });
    } else {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // update UI
            }
        });
    }
} catch (Exception e) {
    e.printStackTrace();
}

可以把二进制文件复制到电脑上,用软件(例如Hex Editor Neo)来查看里面的数据。

读写 json 文件

这里讲的 json 是指 key-value 类型的文本文件。

例如

{
    "key1": 1,
    "key2": "Rust Fisher"
}

接下来的例子会分为几个步骤:

  • 定义数据类
  • 写文件
  • 读文件

定义数据类

为了更直观,我们定义一个类 Info1 来存放数据

public class Info1 {

    private String name;
    private long time;

    // 构造器和get set方法省略

    @NonNull
    @Override
    public String toString() {
        return name + ", " + time;
    }
}

重写了 toString 方法,打 log 的时候可以更方便地看到感兴趣的信息。

现在这个类是用来模拟业务数据的。实际工程中可能是后台传回的具体数据。

写文件

为了更方便操作 json,我们使用 Google 的 gson 库来操作 json。

dependencies {
    implementation 'com.google.code.gson:gson:2.8.5'
}

目标文件存放在 app 内部存储中。

mFile1Path = getFilesDir().getAbsolutePath() + File.separator + "rwFile"
        + File.separator + "jsonFile1.json";

读写文件等 io 操作,应该放在子线程内。

关键代码:

private void write1(Info1 info1) {
    Gson gson = new Gson();
    String jsonText = gson.toJson(info1);
    Log.d(TAG, "write1: jsonText: " + jsonText);
    File file = new File(mFile1Path);
    try {
        if (file.exists()) {
            boolean d1 = file.delete();
            Log.d(TAG, "delete old file: " + d1);
        }
        File dir = file.getParentFile();
        if (dir != null && !dir.exists()) {
            boolean m1 = dir.mkdirs();
            Log.d(TAG, "write1: create dir: " + m1);
        }
        boolean c1 = file.createNewFile();
        Log.d(TAG, "create new file: " + c1);
        OutputStream os = new FileOutputStream(file);
        os.write(jsonText.getBytes());
        os.flush();
        os.close();
    } catch (IOException e) {
        e.printStackTrace();
        Log.e(TAG, "write1: ", e);
    }
}

使用 Gson.toJson 方法,把一个对象转换为 json 格式的字符串。 然后用 FileOutputStream 写入文件中。

执行这个方法后,我们可以用 as 的 Device File Explorer 面板找到这个文件。

本例中,文件路径是 /data/data/com.rustfisher.tutorial2020/files/rwFile/jsonFile1.json

用 as 双击它可以看到内容。例如:

{"name":"RustFisher","time":1605266868256}

读文件

从文件中读出文本,然后转换为相应的对象。

private Info1 read1() {
    File file = new File(mFile1Path);
    if (file.exists() && file.isFile()) {
        try {
            BufferedReader reader = new BufferedReader(new FileReader(file));
            String inString;
            StringBuilder sb = new StringBuilder();
            while ((inString = reader.readLine()) != null) {
                sb.append(inString);
            }
            Gson gson = new Gson();
            return gson.fromJson(sb.toString(), Info1.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
    } else {
        Log.d(TAG, "read1: 文件不存在 " + file);
    }
    return null;
}

BufferedReader 读出文件的字符串。 用 Gson.fromJson 方法将其转换成目标对象。

小结一下:

  • 首先自定义一个类,用来模拟业务数据
  • 确定文件存放路径,例如存放在app内部路径Context.getFilesDir()
  • 将对象转换为json格式字符串,使用Gson.toJson方法
  • json格式字符串转为对象,使用Gson.fromJson方法
  • 文件操作,把文本写入文件FileOutputStream
  • 读取文件中的文本,BufferedReader

IO 相关面试题

1. java中有几种类型的流?jdk为每种类型的流提供了一些抽象类以供继承,请说出他们分别是那些类?

  • 字节流:继承自 InputStream 和 OutputStream
  • 字符流:继承自 InputStreamReader 和 OutputStreamWriter

2. 字节流有字符流的区别?

  • 字节流操作的最基本的单元是字节,字符流操作的基本单位是字符,也就是Unicode码元。
  • 字节流默认不使用缓冲区,字符流使用缓冲区。
  • 在硬盘上的所有文件都是以字节形式存在的(图片,声音,视频),而字符只在内存中才会形成。

3. 简单介绍一下 io 流?

  • IO 流是用来进行文件的数据传输,java对数据操作主要通过流的方式,操作的类都在IO包中,按流的走向可以分为输入流和输出流两种。
  • 常见的应用是:文件复制,文件上传、文件下载。

4. 输入流和输出流?

  • 读数据是输入流
  • 写数据是输出流
  • 此时对象代表的是 java 程序。

5. IO流的分类?

6. 什么叫缓冲区? 有什么作用?

  • 缓冲区就是一片特殊的内存
  • 在程序频繁的操作内存或者数据库时,为了提升性能,可以将程序先读到内存中的一块区域中,以后可以从内存中直接读取,这样读取速度较块,提升性能。
  • 在字符流的操作中,所有字符都是在内存中形成的,在输出前会将所有的内容都保存在内存之中,所以使用了缓冲区暂存数据。

7.什么是 java 的序列化,如何实现 java 的序列化,请解释一下 serializable 接口的作用?

  • java 对象的序列化是指将 java 对象写入 io 流中,与此对应的是,对象的反序列化,则从 io 流中恢复该 java 对象。
  • 如果要让某个对象支持序列化机制,则必须让它的类是可序列化的,必须实现serializable接口或者Externalizable接口。
  • 序列化:把对象转换为字节序列的过程称为对象的序列化。
  • 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。

8. io流中常见方法?

  • flush,字符流中刷新缓存区的方法。
  • close,关闭io流。
  • read,读字节,write,写字节。

******************