一、引言

IO(输入/输出),输入是指允许程序读取外部数据(包括来自磁盘、光盘等存储设备的数据)、用户输入数据。输出是指允许程序记录运行状态,将程序数据输出到磁盘、光盘等存储设备中。

IO的主要内容包括输入、输出两种IO流,这两种流中又分为字节流和字符流,字节流是以字节为单位来处理输入、输出流,而字符流是以字符为单位来处理输入、输出流。

二、File 类

File 类是用来操作文件和目录的,File能创建、删除、重命名文件和目录,File不能访问文件内容本身,File 类可以通过文件路径字符串来创建对象,创建完对象之后有很多方法来操作文件和目录:

2.1 构造方法

  • File(String pathname):根据一个路径得到File对象

  • File(String parent, String child):根据一个目录和一个子文件/目录得到File对象

  • File(File parent, String child):根据一个父File对象和一个子文件/目录得到File对

2.2 创建方法

//在当前路径来创建一个File对象
File file = new File("1.txt");
//创建文件
System.out.println(file.createNewFile());
File file2 = new File("temp");
 //创建对象对应的目录
System.out.println(file2.mkdir());

2.3 重命名和删除功能

//把文件重命名为指定的文件路径
file2.renameTo(new File("temp2"));
//删除文件或者文件夹
file2.delete();

注:重命名中如果路径名相同,就是改名,如果路径名不同,就是改名并剪切。删除不走回收站,要删除一个文件夹,请注意该文件夹内不能包含文件或者文件夹。

2.4 判断功能

//判断文件或目录是否存在
System.out.println(file.exists());
//判断是否是文件
System.out.println(file.isFile());
//判断是否是目录
System.out.println(file.isDirectory());
//是否为绝对路径
System.out.println(file.isAbsolute());
//文件或目录是否可读
System.out.println(file.canRead());
//文件或目录是否可写
System.out.println(file.canWrite());

2.5 获取功能

//返回文件内容长度
System.out.println(file.length());
//获取文件或目录名
System.out.println(file.getName());
//获取文件或目录相对路径
System.out.println(file.getPath());
//获取文件或目录绝对路径
System.out.println(file.getAbsolutePath());
//获取上一级路径
System.out.println(file.getAbsoluteFile().getParent());
//返回当前目录的子目录或文件的名称
String[] list = file1.list();
for (String fileName : list) {
    System.out.println(fileName);
}
//返回当前目录的子目录或文件,返回的是File数组
File[] files = file1.listFiles();
//返回系统的所有根路径
File[] listRoots = File.listRoots();
for (File root : listRoots) {
    System.out.println(root);
}

三、IO 流

实现输入/输出的基础是IO流,Java把不同的源之间的数据交互抽象表达为流,通过流的方式允许Java程序使用相同的方式来访问不同的数据源。用于操作流的类都在IO包中。

3.1 流的分类

按照不同的分类方式,流也可以分为不同类型

  1. 输入流和输出流:根据流向来分,可以分为输入流与输出流

    • 输入流:从中读取数据,而不能向其写入数据

    • 输出流:向其写入数据,而不能读取数据

  2. 字节流和字符流:这两种流用法几乎完全一样,区别在于所操作的数据单元不一样,字节流操作的数据单元是8位的字节,而字符流是16位的字符。

3.2 InputStream与Reader

InputStream和Reader是所有输入流的抽象基类,这是输入流的模板,InputStream中有三个方法

  • int read() :从输入流读取单个字节,返回所读取的字节数据。

  • int read(byte b[]):从输入流中最多读取b.length个字节的数据,并将其存储在数组b中。

  • int read(byte b[], int off, int len):从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入的位置是从off中开始。

Reader中也有三个方法

  • int read() :从输入流读取单个字符,返回所读取的字符数据。

  • int read(char cbuf[]):从输入流中最多读取cbuf.length个字符的数据,并将其存储在数组cbuf中。

  • int read(char cbuf[], int off, int len):从输入流中最多读取len个字符的数据,并将其存储在数组cbuf中,放入的位置是从off中开始。

    两个类的方法基本相同,用法相同,只是操作单位不一样

InputStream inputStream = new FileInputStream("StreamTest.java");
byte[] bytes = new byte[1024];
int hasRead = 0;
while ((hasRead = inputStream.read(bytes)) > 0) {
System.out.println(new String(bytes, 0, hasRead));
}

inputStream.close();

3.3 OutputStream与Writer

OutputStream与Writer是所有输出流的抽象基类,是输出流模板,OutputStream有三个方法:

  • void write(int b):指定字节输出到流中

  • void write(byte b[]):将指定字节数组输出到流中

  • void write(byte b[], int off, int len):将指定字节数组从off位置到len长度输出到流中

Writer中也有三个方法:

  • void write(int b):指定字符输出到流中

  • void write(char buf[]):将指定字符数组输出到流中

  • void write(char cubf[], int off, int len):将指定字符数组从off位置到len长度输出到流中

由于Writer是以字符为单位进行操作,那可以使用String 来代替,于是有另外的方法

  • void write(String str):将str字符串输出到流中

  • void write(String str, int off, int len):将str从off位置开始长度为len输出到流中

FileWriter fileWriter = new FileWriter("test.txt");
fileWriter.write("日照香炉生紫烟\r\n");
fileWriter.write("遥看瀑布挂前川\r\n");
fileWriter.write("飞流直下三千尺\r\n");
fileWriter.write("遥看瀑布挂前川\r\n");
fileWriter.close();

注:操作流时一定要记得关闭流,因为打开的IO资源不属于内存资源,垃圾回收无法回收。

四、输入/输出流体系

Java的输入输出流提供了40多个类,要全部都记住很困难也没有必要,我们可以按照功能进行下分类,其实是非常有规律的

一般如果输入/输出的内容是文本内容,应该考虑使用字符流,如果输入/输出内容是二进制内容,则应该考虑使用字节流。

4.1 转换流

体系中提供了两个转换流,实现将字节流转换成字符流,InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输出流转换成字符输出流,System.in代表标准输入,这个标准输入是字节输入流,但是键盘输入的都是文本内容,这个时候我们可以InputStreamReader转换成字符输入流,普通的Reader读取内容不方便,我们可以使用BufferedReader一次读取一行数据,如:

//先将System.in转换成Reader 对象
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
//再将Reader包装成BufferedReader
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line = null;
while ((line = bufferedReader.readLine()) != null) {
    if (line.equals("exit")) {
        System.exit(1);
    }
    System.out.println("输入的内容是:" + line);
}

BufferedReader具有缓冲功能,在没有读到换行符则阻塞,读到换行符再继续。

4.2 推回输入流

推回输入流PushbackInputStream和PushbackReader中都提供了如下方法:

  • void unread(int b) :将一个字节/字符推回到推回缓冲区,从而允许重复读取刚刚读取的内容。

  • void unread(byte[] b/char[] b, int off, int len) :将一个字节/字符数组里从off开始,长度为len字节/字符的内容推回到推回缓冲区,从而允许重复读取刚刚读取的内容。

  • void unread(byte[] b/char[]):将一个字节/字符数组内容推回到推回缓冲区,从而允许重复读取刚刚读取的内容。

这两个推回流都带有一个推回缓冲区,当调用unread()方法时,系统将会把指定的内容推回到该缓冲区,而当每次调用read方法时会优先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有read()所需的数组时才会从原输入流中读取。

 //创建PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pushbackReader = new PushbackReader(new FileReader("StreamTest.java"), 64);
char[] buf = new char[32];
//用以保存上次读取的字符串内容
String lastContent = "";
int hasRead = 0;
//循环读取文件内容
while ((hasRead = pushbackReader.read(buf)) > 0) {
    //将读取的内容转换成字符串
    String content = new String(buf, 0, hasRead);
    int targetIndex = 0;
    if ((targetIndex = (lastContent + content).indexOf("new PushbackReader")) > 0) {
        //将本次内容和上次的内容一起推回缓冲区
        pushbackReader.unread((lastContent + content).toCharArray());
        //重新定义一个长度为targetIndex的char数组
        if (targetIndex > 32) {
            buf = new char[targetIndex];
        }
        //再次读取指定长度的内容
        pushbackReader.read(buf, 0, targetIndex);
        //打印读取的内容
        System.out.print(new String(buf, 0, targetIndex));
        System.exit(0);
    } else {
        //打印上次读取的内容
        System.out.print(lastContent);
        //将本次内容设为上次读取的内容
        lastContent = content;
    }
}

五、RandomAccessFile

RandomAccessFile是Java输入/输出流体系中最丰富的文件内容访问类,提供了众多的方法来访问文件内容,既可读取文件内容,也可以向文件输出数据,RandomAccessFile可以自由访问文件的任意位置。

RandomAccessFile包含一个记录指针,用以标识当前读和写的位置,当创建新对象时,指针位置在0处,而当读/写了N个字节后,指针就会向后移动N个字节,并且RandomAccessFile可以自动的移动该指针位置,当然我们也可以直接的获取指针的位置。

  • getFilePointer():获取文件记录指针的当前位置。

  • seek(long pos):将文件记录指针定位到pos位置。

RandomAccessFile有两个构造函数:

  • RandomAccessFile(File file, String mode):使用File文件,指定文件本身 RandomAccessFile(String name, String mode):使用文件名称,指定文件

其中还有一个参数mode(访问模式),访问模式有4个值:

  • r:以只读方式打开文件

  • rw:以读、写方式打开文件,如果文件不存在,则创建

  • rws:以读、写方式打开文件,并要求对文件的内容或者元数据的每个更新都同步写入到底层存储设备

  • rwd:以读、写方式打开文件,并要求对文件的内容的每个更新都同步写入到底层存储设备

RandomAccessFile raf = new RandomAccessFile("StreamTest.java", "r");
System.out.println("文件指针的初始位置:" + raf.getFilePointer());
//移动指针位置
raf.seek(300);
byte[] buf = new byte[1024];
int hasRead = 0;
while ((hasRead = raf.read(buf)) > 0) {
    //读取数据
    System.out.println(new String(buf, 0, hasRead));
}
//追加内容
RandomAccessFile randomAccessFile=new RandomAccessFile("out.txt","rw");
randomAccessFile.setLength(randomAccessFile.length());
randomAccessFile.write("追加的内容!\r\n".getBytes());

六、对象序列化

对象序列化机制是允许把内存中的java对象转换成平台无关的二进制流,这样我们可以将这二进制流保存在磁盘上或者通过网络将起传输到另一个网络节点,其他程序获取到此二进制流后,可以将其恢复成原来的java对象。

要使一个对象是可序列化的,只需要继承Serializable或者Externalizable接口,无需实现任何方法。所有可能在网络上传输的对象的类都应该是可序列化的,如我们JavaWeb中的输入参数及返回结果。

6.1 使用对象流实现序列化

我们使用一个对象流来实现序列化对象

先建一个对象类:

@Data
public class Person implements Serializable {

    private int age;

    private String name;

    public Person(String name, int age) {
        System.out.println("有参数的构造器");
        this.age = age;
        this.name = name;
    }
}

序列化对象与反序列化对象

//创建输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.txt"));
Person person = new Person("张三", 10);
//将person写入文件中
objectOutputStream.writeObject(person);
//创建输入流
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("object.txt"));
try {
    //读出数据
    Person p = (Person) objectInputStream.readObject();
    System.out.println(p);
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

反序列化读取的仅仅是Java对象的数据,而不java类,因此反序列化时必须提供对象所属类的class文件,在反序列化对象时没有调用有参数的构造器,说明反序列化时不需要通过构造器来初始化Java对象。

如果一个类中包含了引用类型,那么引用类型也必须是可序列化的,否则该类也是不可序列化的。

如果我们不希望某个变量被序列化,比如敏感信息,那需要使用transient来修饰此变量即可。

七、NIO

上面学习的IO都是阻塞式的,而且是底层都是通过字节的移动来处理的,这样明显效率不高,于是后面新增了NIO来进行改进,这些类都放在java.nio包中。

新IO 是将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件中的内容,相当于虚拟内存概念,这种方式比传统的IO快很多。

新IO的两大核心对象是Channel(通道)与Buffer(缓冲),Channel与传统的InputStream、OutputStream最大的区别在于提供了一个map()方法,这个方法是将一块数据映射到内存中,这样新IO就是面向块进行处理;Buffer本质是一个数组,可以看做一个容器,发送到Channel中的所有对象都必须首先放在Buffer中,读取数据也是从Buffer中读取。

7.1 Buffer

Buffer是一个抽象类,最常用的子类是ByteChannel和CharBuffer,Buffer类都没有提供构造器,都是通过XXXBuffer allocate(int capacity) 来得到对象,如

CharBuffer allocate = CharBuffer.allocate(8);

Buffer有三个重要概念:

  • 容量(capacity):缓冲区的容量,表示该buffer的最大数据容量,即最多可存储多少数据,创建后不可改变。

  • 界限(limit):位于limit后的数据既不可以读,也不可以写。

  • 位置(position):用于指明下一个可以被读出或写入的缓冲区位置索引,类似IO中的指针。

Java核心技术梳理-IO-LMLPHP

Buffer的主要作用是装入数据,然后输出,当创建buffer时,position在0位置,limit在capacity,当添加数据时,position向后移动。

当Buffer装好数据时,调用flip()方法,这个方法将limit设置为position,position设置为0,也就是说不能继续输入,这就给输出数据做好准备了,而当输出数据结束后,调用clear()方法,这是将position设置为0,limit设置为capacity,这样就为装入数据做好了准备。

除了上面的几个概念,Buffer还有两个重要方法,即put()与get()方法,就是存储与读取数据方法,在存储和读取数据时,分为相对和绝对两种:

  • 相对:从Buffer的position位置开始读取或者写入数据,这时候会改变position的数值。

  • 绝对:根据索引读取或写入数据,这个时候不会影响position的数值。

//创建buffer
CharBuffer buffer = CharBuffer.allocate(10);
System.out.println("capacity: " + buffer.capacity());
System.out.println("limit:" + buffer.limit());
System.out.println("position:" + buffer.position());
//加入数据
buffer.put('a');
buffer.put('b');
buffer.put('c');
System.out.println("加入元素后,position:" + buffer.position());
buffer.flip();
System.out.println("执行flip后,limit:" + buffer.limit());
System.out.println("position:" + buffer.position());
System.out.println("取出一个数据," + buffer.get());
System.out.println("取出数据后,position:" + buffer.position());
buffer.clear();
System.out.println("执行clear后,limit:" + buffer.limit());
System.out.println(",position:" + buffer.position());
System.out.println("执行clear后缓冲区未被清空:" + buffer.get(2));
System.out.println("绝对读取后,position不会改变:" + buffer.position());

7.2 Channel

Channel类似传统流对象,主要区别在于Channel可以将指定文件的部分或者全部直接映射成Buffer,程序不能直接对Channel中的数据进行读写,只能通过Channel来进行数据读写。我们用FileChannel来看看如何使用:

File file = new File("StreamTest.java");
//输入流创建FileChannel
FileChannel inChannel = new FileInputStream(file).getChannel();
//以文件输出流创建FileChannel,控制输出
FileChannel outChannel = new FileOutputStream("a.txt").getChannel();
//将FileChannel映射成ByteBuffer,
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
Charset charset = Charset.forName("GBK");
//输出数据
outChannel.write(buffer);
buffer.clear();
CharsetDecoder charsetDecoder = charset.newDecoder();
//转换成CharBuffer进行输出
CharBuffer charBuffer = charsetDecoder.decode(buffer);
System.out.println(charBuffer);

7.3 字符集与Charset

我们知道,在计算机底层文件都是二进制文件,都是字节码,那为什么我们还能看到字符,这里面涉及编码和解码两个概念,简单讲,将字符转换成二进制为编码,而将二进制转成字符为解码。

Java默认使用Unicode字符集(字符集是指二进制序列与字符之间的对应关系),但很多操作系统不使用Unicode字符集,这样就会出错,我们要根据实际情况来使用对应的字符集。

Charset包含了创建解码器和编码器的方法,还提供了获取Charset所支持字符集的方法,我们可以通过Charset的forName()获取对象,通过对象获取到CharsetEncoder和CharsetDecoder对象,再通过此对象进行字符序列与字节序列的转换。

SortedMap<String, Charset> stringCharsetSortedMap = Charset.availableCharsets();
for(String name:stringCharsetSortedMap.keySet()){
    System.out.println(name);
}
//创建简体中文对应的Charset
Charset cn = Charset.forName("GBK");
//创建对应的编码器及解码器
CharsetEncoder cnEncoder = cn.newEncoder();
CharsetDecoder cnDecoder = cn.newDecoder();
CharBuffer buff = CharBuffer.allocate(8);
buff.put('李');
buff.put('白');
buff.flip();
//将buff的字符转成字节序列
ByteBuffer bbuff = cnEncoder.encode(buff);
for (int i = 0; i <bbuff.capacity() ; i++) {
    System.out.print(bbuff.get(i)+ " ");
}
//将bbuff的数据解码成字符
System.out.println("\n"+cnDecoder.decode(bbuff));

7.4 Path、Paths、Files

早期的Java只提供了File类来访问文件系统,功能比较有限且性能不高,后面又提供了Path接口,Path代表一个平台无关路径,并提供了Paths与Files两个工具类,提供了大量的方法来操作文件。

Path path = Paths.get(".");
System.out.println("path包含的文件数量:" + path.getNameCount());
System.out.println("path的根路径:" + path.getRoot());
Path path1 = path.toAbsolutePath();
System.out.println("path的绝对路径:" + path1);
//多个String构建路径
Path path2 = Paths.get("G:", "test", "codes");
System.out.println("path2的路径:" + path2);

System.out.println("StreamTest.java是否为隐藏文件:" + Files.isHidden(Paths.get("StreamTest.java")));
//一次性读取所有行
List<String> allLines = Files.readAllLines(Paths.get("StreamTest.java"), Charset.forName("gbk"));
System.out.println(allLines);
//读取大小
System.out.println("StreamTest.java文件大小:" + Files.size(Paths.get("StreamTest.java")));
List<String> poem = new ArrayList<>();
poem.add("问君能有几多愁");
poem.add("恰似一江春水向东流");
//一次性写入数据
Files.write(Paths.get("poem.txt"), poem, Charset.forName("gbk"));

可以看到Paths与Files非常的强大,提供了很多方法供我们使用,在之前这些方法我们自己写的话比较麻烦,更多的方法可以自己去看API。

7.5 文件属性

java.nio.file.attribute包下提供了大量的属性工具类,提供了很方便的方法去获取文件的属性:

BasicFileAttributeView baseView = Files.getFileAttributeView(Paths.get("poem.txt"), BasicFileAttributeView.class);
BasicFileAttributes basicFileAttributes = baseView.readAttributes();
System.out.println("创建时间:" + basicFileAttributes.creationTime().toMillis());
System.out.println("最后更新时间:" + basicFileAttributes.lastModifiedTime().toMillis());
06-18 19:13