Java笔记:IO流

1. IO流理解

IO流中的I是单词Input的缩写,表示输入或者读(Read),O是单词Output的缩写,表示输出或写(Write),输入输出或者读写都是相对于内存而言的,输入即从硬盘中读取数据到内存中,输出即将内存中的数据写入到硬盘。IO流就是输入和输出时的数据流(内存和硬盘之间的管道),IO的输入或输出也可以看做是文件的读和写,因为硬盘中的数据其实都是以文件的形式存储的,在硬盘上对文件的读写操作就对应了内存中对数据的输入和输出操作。

 

2. 流的读取方式

按字节:一次只读取一个字节byte(8个二进制位bit),这种读取方式的特点是什么类型的文件都能读取,包括普通文本、图片、声音等。

按字符:一次只读取一个字符,这种读取方式的特点是只能读取普通文本,它能自动识别一个字符所占用的字节数,然后进行读取。如英文字母中一个字母只占用1个字节,而中文中一个文字会占用2个字节,如“a啊”,按字符的方式只需读取两次即可,而按字节的方式会读取三次,因为有三个字节。

 

3. File类

java.io.File”类和文件流的操作没有关系,也就是不能对文件进行读和写,File类是文件和目录路径名的一种抽象表示形式,即一个文件是File对象,一个目录也是File对象,它的构造方法中只需要传入一个文件或目录的路径名即可,注意,对于路径分隔符,Windows和Linux使用的分别是“\”和“/”,但是File中传入的路径可以统一使用“/”,在Windows中也可以识别。

File中的常用方法:

  • boolean exists():判断File对象表示的文件或目录是否存在。

  • boolean createNewFile():如果File对象不存在,则将File对象创建为一个新的文件,如果存在则不能创建。

  • boolean mkdir():如果File对象不存在,则将File对象创建为一个新的目录。注意,此方法只能创建单个目录,不能递归创建目录。

  • boolean mkdirs():如果File对象不存在,则将File对象以递归方式创建目录。

  • String getParent():获取File对象的父路径(上一级路径)。

  • File getParentFile():返回File对象的父路径的File对象。

  • String getAbsolutePath():返回File对象的绝对路径。

  • boolean delete():删除File对象表示的文件或目录。

  • String getName():获取File对象的文件名或目录名。

  • boolean isFile():判断File对象是否是一个文件。

  • boolean isDirectory():判断File对象是否是一个目录。

  • long lastModified():返回File对象最后一次修改的时间的总毫秒数(从1970年1月1日开始)。

  • long length():返回File对象的总字节数。

  • boolean renameTo(File dest):File对象重命名。

  • File[] listFiles():返回File对象的所有子文件和子目录的File对象数组。

 获取一个文件及文件路径的通用方式:

// 返回文件的绝对路径
String path = Thread.currentThread().getContextClassLoader().getResource("myfile.txt").getPath();
// 返回文件对应的流
InputStream reader = Thread.currentThread().getContextClassLoader().getResourceAsStream("myfile.txt");

注:这种方式能获取到一个文件的绝对路径或文件流,但是需要注意,使用这种方式的前提是这个文件是在类路径下,IDEA工具的目录结构中就是在“[module_name]/src”路径下,即getResource中的参数是一个src文件夹下的相对路径。

 

4. Java中的IO流

Java中IO流的类包都在“Java.io”中,共分为四大类:“java.io.InputStream”、“java.io.OutputStream”、“java.io.Reader”和“java.io.Writer”,前两个以Stream结尾的属于以字节流方式进行读写,后两个则是以字符流的方式进行读写,如果想要知道它们的某个子类是以字节流还是字符流的方式来进行文件数据的读取的,可以使用类名的结尾单词进行快速区分,以Stream结尾的是以字节流方式进行读写,而以Reader或Writer结尾的则是以字符流的方式进行读写,需要注意的是,类似“xxxStreamReader”这样的类同样是看结尾单词,即它也是字符流的方式进行读写。

Java中的所有IO流都有一个close方法,使用完IO流之后一定记得要使用close方法关闭这个管道。对于输出流,都有一个flush方法,表示将管道(缓冲)中的数据强制输出并清空,其作用就是清空管道,在手动输出完成后一定记得使用flush方法刷新一下,否则可能会导致输出的数据不完整,即数据丢失。

 

5. 文件流

FileInputStream(常用,重点掌握):对文件以字节流的方式进行读取,构造方法可以传入一个文件路径,也可以传入一个表示文件的File对象。

常用的方法有:

  • int read():从文件开始每次往后读取一个字节,注意返回的是字节本身,并没有将字节转换为对应的字符,例如第一个字节的字符是“a”,读取时则返回的是97。如果到了文件末尾则返回-1。注意文件指针一开始并没有指向文件的第一个字节,而是在第一次调用read()方法时才指向并返回文件的第一个字节,之后每次调用read()方法就会往后“挪”一个字节。但是读取文件内容时这个方法并不常用,因为每次只读取一个字节的方式导致和硬盘的交互太频繁了。
  • int read(byte[] b):传入一个byte数组,每次读取数组的length个字节,将读取的内容覆盖数组原有的值,并返回读取的字节数;如果文件剩余字节数不足length,那么就会取出全部剩余字节,并将数组从头开始覆盖,而数组剩余的部分不会做任何操作;当文件指针已处在末尾了,将返回-1,数组同样不会做任何操作。
  • int available():返回流当中剩余的没有读到的字节数。可以在读之前使用这个方法查看文件的总字节数。
  • long skip(long n):跳过指定数量的字节(不去读)。

 使用示例:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class FileInputStreamTest {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try{
            // FileInputStream中抛出了一个FileNotFoundException异常,即编译时异常
            // 需要程序员手动处理,这里加一个try进行捕获
            // input.txt文件中只有一行数据:helloworld
            fis = new FileInputStream("Z:\\Study\\Java\\input.txt");

            /*
            // 以下read()只是演示,实际并不常用
            // 打印结果为:
            // 104
            // 101
            // 108
            // 108
            // 111
            // 119
            // 111
            // 114
            // 108
            // 100
            int byteData = 0;
            while ((byteData = fis.read()) != -1) {
                System.out.println(byteData);
            }
            */


            // 以下使用read(byte[] b)方法读取文件内容
            byte[] bytes = new byte[6];
            int count = fis.read(bytes);
            System.out.println(count);  // 6
            System.out.println(new String(bytes));  // hellow
            System.out.println(new String(bytes, 0, count));  // hellow

            count = fis.read(bytes);
            System.out.println(count);  // 4
            System.out.println(new String(bytes));  // orldow
            System.out.println(new String(bytes, 0, count));  // orld

            count = fis.read(bytes);
            System.out.println(count);  // -1
            System.out.println(new String(bytes));  // orldow

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    // close方法也是抛出了一个IOException异常的编译时异常,同样加一个try进行捕获
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 

FileOutpuStream(常用,重点掌握):将字节内容写入到文件中,构造方法中同样可以传入一个文件路径或者表示文件的File对象,同时还可以指定第二参数为true。当只传入一个文件路径或File对象,如果文件已存在,会清空文件内容,如果文件不存在,则会自动创建该文件;也可以指定第二个参数为true,表示如果文件已存在则不会清空文件,而是会往文件末尾追加数据。

常用的方法有:

  • void write(byte[] b):将byte数组中的全部内容写入到文件中。

  • void write(byte[] b, int off, int len):将byte数组中的部分内容写入到文件中。

  • 输出字符串:可以将字符串通过自身的getBytes()转换为byte数组之后再进行输出。

使用示例:

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamTest {
    public static void main(String[] args) {
        FileOutputStream fos = null;
        try {
            // 只传入一个文件路径:如果文件已存在,会清空文件内容,如果文件不存在,则会自动创建该文件
            // fos = new FileOutputStream("Z:\\Study\\Java\\output.txt");

            // 第二个参数表示如果文件已存在则不会清空文件,而是会往文件末尾追加数据
            fos = new FileOutputStream("Z:\\Study\\Java\\output.txt", true);

            byte[] bytes = {97, 98, 99, 100};
            // 将bytes数组全部写到文件中
            fos.write(bytes);
            // 将bytes数组的一部分写到文件中
            fos.write(bytes, 0, 2);

            // 将字符串输出到文件中
            String s = "你好,世界";
            byte[] bs = s.getBytes();
            fos.write(bs);

            // 文件输出完成后,一定记得刷新一下
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    // 文件操作完成后一定记得调用close()方法
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 

FileReader:和FileInputStream相比,在使用上都是类似的,只是需要把byte数组换成char数组即可。

FileWriter:和FileOutpuStream相比,在使用上也都是类似的,只是要把byte数组换成char数组,另外,由于字符串的特殊性,FileWriter的write方法还可以直接写入一个字符串到文件。

 

6. 缓冲流

缓冲流表示带有缓冲区的字节流或字符流,特点是使用这些流的时候不需要自定义byte数组或char数组。

包装流和节点流:缓冲流的使用涉及到一些概念,当一个流的构造方法中需要另一个流的时候,被传入的这个流称为节点流,外部负责包装这个节点流的流称为包装流或处理流。关闭包装流的时候也是只需要调用最外层包装流的close()方法即可,里面的节点流会自动关闭。同样,对于输出的流,也是只需要调用最外层包装流的flush()方法即可。

BufferedInputStream:是一个包装流,构造方法中需要传入一个InputStream类型(字节类型)的节点流。

BufferedOutputStream:是一个包装流,构造方法中需要传入一个OutputStream类型(字节类型)的节点流。

BufferedReader:是一个包装流,构造方法中需要传入一个Reader类型(字符类型)的节点流。

BufferedWriter:是一个包装流,构造方法中需要传入一个Writer类型(字符类型)的节点流。

使用示例:

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReaderTest {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            // 这里reader就是一个节点流,br就是一个包装流或处理流
            // 文件内容只有一行:helloworld
            FileReader reader = new FileReader("Z:\\Study\\Java\\input.txt");
            br = new BufferedReader(reader);

            // 读取一行数据,注意返回的数据是不包含末尾的换行符的
            String line = br.readLine();
            System.out.println(line);  // helloworld

            // 读取到文件末尾,返回null
            line = br.readLine();
            System.out.println(line);  // null

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 对于包装流来说,只需要调用最外层包装流的close()方法即可,里面的节点流会自动关闭。
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 

 

7. 数据流

这种流的特点是会将数据以及数据的类型一起写入文件,每次写入文件的时候都可以使用不同的数据类型,而读取的时候则需要按照写入的顺序按照数据类型读取出来。

DataInputStream:按照指定规则从文件中读取特定类型的数据。

DataOutputStream:按照指定规则向文件中写入特定类型的数据。

 

8. 标准输出流

标准输出不需要手动close()关闭。

PrintStream:标准的字节输出流,默认输出到控制台。System.out其实就是一个PrintStream标准输出字节流,可以使用System.setOut设置标准输出流输出到指定指定文件,不再默认输出到控制台。

PrintWriter:标准的字符输出流,默认输出到控制台,同样可以使用System.setOut更改默认输出方向。

使用示例:

PrintStream out = new PrintStream(new FileOutputStreasm("log.txt", true));
System.setOut(out);

 

 

9. 对象流(序列化和反序列化)

序列化和反序列化:对象流的使用涉及到两个概念,即序列化和反序列化,序列化(Serialize)是指将java对象存储到文件中,将java对象的状态以文件的形式保存下来。而反序列化(Deserialize)则是序列化的反过程,即将文件中的数据恢复到内存当中,恢复为java对象的过程。序列化的过程需要使用到ObjectOutputStream,而反序列化的过程需要使用到ObjectInputStream。

ObjectOutputStream:构造方法中传入一个FileOutputStream对象,然后调用writeObject方法就可以将一个需要序列化的java对象保存到指定的文件中了。如果需要序列化多个对象,可以将这些对象放到ArrayList集合中,最后将这个集合传入writeObject方法即可,但是需要注意一点,就是ArrayList集合本身也是实现了Serializable接口的,也就是说,使用的集合以及集合中的元素都需要实现Serializable接口才能进行序列化的操作。关于序列化多个对象,建议直接使用集合的方式就好了,如果采用多次调用writeObject的方式每次写入一个对象,这种方式是不允许的。

ObjectInputStream:构造方法中传入一个FileInputStream对象,然后调用readObject方法就可以从指定文件的反序列化结果中读取一个java对象(普通java对象或集合对象)到内存中了。

Serializable接口:参与序列化和反序列化的对象需要实现Serializable接口,但是注意,这个接口并没有任何方法,它只是起到了一个标识的作用(这种接口也称为标志接口),通过这个接口告诉JVM此类的对象可能会进行序列化的操作。有了这个标志接口,编译时JVM就会为这个java对象自动生成一个序列化版本号,这个序列化版本号的作用就是反序列化时用来识别当下源代码中的class定义和之前执行序列化操作时的class定义是否一致,如果不一致则会报错,不能进行反序列化的操作,即反序列化的对象的类在当前源代码中的定义已经被修改,无法反序列化(反序列化回来时也需要一个对应class类型的变量去接收,两者不一样的话,自然就无法通过了),所以由此看出,这个序列化版本号本质上就是用来区分类的,判断两个类的定义是否相同。通常建议这个序列化版本由我们程序员自己手动定义,而不是让JVM自动生成。

transient关键字:序列化时,如果希望类中的某个字段不要将它序列化到文件中,只需要在属性定义的时候加上这个transient关键字即可。

手动生成序列化版本号:通常来讲,一个类定义之后,不可能保证它永远都不会被修改,但是当它被修改之后,它的序列化版本号就变了,这就会导致之前序列化到文件中的数据不能反序列化回来了,这样的结果通常是不行的,所以建议需要序列化的类都自己手动编写一个序列化版本号,而不是采用JVM自动生成的方式。手动编写的方式就是在类定义中添加一个序列化版本号的属性即可“private static final long serialVersionUID = 233L;”,这里的属性值通常来讲需要是保证它是项目中唯一的编号即可,所以可以根据项目情况自定义就可以了,如果不放心,自己生成一个全球唯一的UID也可以(IDEA也是可以自动生成这个属性的,在设置界面直接搜索serialVersionUID,可以看到Inspections下有个对应的检查项“Serializable class without 'serialVersionUID'”,勾上就可以了)。

使用示例:

普通java对象

// 实现Serializable,但这个接口没有任何方法需要去重写
public class User implements Serializable {
    private int id;
    private String name;
    ...
}

 

序列化单个java对象

User u = new User(123, "zhangsan");
// 将user对象序列化到Z:\\userinfo文件中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Z:\\userinfo"));
oos.writeObject(s);
oos.flush();
oos.close();

 

序列化多个java对象

// 这里使用ArrayList存放多个User对象
List<User> userList = new ArrayList<>();
userList.add(new User(111, "zhangyi"));
userList.add(new User(222, "zhanger"));
userList.add(new User(333, "zhangsan"));

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Z:\\userinfo"));
oos.writeObject(userList);
oos.flush();
oos.close();

反序列化java对象

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Z:\\userinfo"));
// 反序列化出来的对象需要进行强制类型转换一下
List<User> userList = (List<User>)ois.readObject();
ois.close();

 

 

10. io和Properties联合使用以读取配置文件
如果一个文本文件中的每一行数据是像这样“key=value”使用等号或冒号(冒号不建议)分隔成两部分,就可以使用一个文件输入流读取出来,再使用Properties对象的load方法加载这个文件流输入对象,然后在Properties的getProperty方法传入等号左边的字符串就可以取出等号右边的字符串了。

user-conf.properties

 username=root
 password=123456
 FileReader reader = new FileReader("user-conf.properties");
 Properties pro = new Properties();
 pro.load(reader);
 String username = pro.getProperty("username");
 System.out.println(username);  // root

 资源绑定器:对于上面示例中的这类配置文件,相较于使用Properties对象来读取,更常用的方法是使用资源绑定器,具体使用示例如下:

// 资源绑定器,在java.util.ResourceBundle下,并且只能绑定xxx.properties文件
// 这个文件必须在类路径下(IDEA工具的结构中就是module下的src目录),而且参数传入时也不能加文件后缀“.properties”。
ResourceBundle bundle = ResourceBundle.getBundle("user-conf");
String value = bundle.getString("username");

 

posted @ 2020-06-16 23:12  山上下了雪-bky  阅读(409)  评论(0编辑  收藏  举报