Java I/O
I/O就是输入、输出,我们读取硬盘上的文件、网络文件传输、鼠标键盘输入或者是单片机发回的数据,支持这些操作的设备就是I/O设备
计算机的总线结构:
常见的I/O设备一般是鼠标、键盘这类通过USB进行传输的外设或者是通过Sata接口或是M.2连接的硬盘。一般情况下,这些设备是由CPU发出指令通过南桥芯片间接进行控制,而不是由CPU直接操作。
而我们在程序中,想要读取这些外部连接的I/O设备中的内容,就需要将数据传输到内存中。而需要实现这样的操作,单单凭借一个小的程序是无法做到的,而操作系统(如:Windows/Linux/MacOS)就是专门用于控制和管理计算机硬件和软件资源的软件,我们需要读取一个IO设备的内容时,就可以向操作系统发出请求,由操作系统帮助我们来和底层的硬件交互以完成我们的读取/写入请求。
从读取硬盘文件的角度来说,不同的操作系统有着不同的文件系统(也就是文件在硬盘中的存储排列方式,如Windows就是NTFS、MacOS就是APFS),硬盘只能存储一个个0和1这样的二进制数据,至于0和1如何排列,各自又代表什么意思,就是由操作系统的文件系统来决定的。从网络通信角度来说,网络信号通过网卡等设备翻译为二进制信号,再交给系统进行读取,最后再由操作系统来给到程序。
Java提供了一套用于用于IO操作的框架。根据IO流的传输方向和读取单位,分为字节流InputStream
和OutputStream
以及字符流Reader
和Writer
的IO框架。通过流,我们就可以一直从流中读取数据,直到读取到尽头,或是不断向其中写入数据,直到我们写入完成,而这类IO就是我们所说的BIO。
字节流一次读取一个字节,也就是一个byte
的大小,字符流一次读取一个字符也就是一个char
的大小(在读取纯文本文件的时候更加适合)。
文件字节流
我们可以使用FileInputStream
来获取文件的输入流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException;
public class Main { public static void main(String[] args) { try { FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt"); } catch (FileNotFoundException e) { e.printStackTrace(); } } }
|
在使用完成一个流之后,必须关闭这个流来完成对资源的释放,否则资源会被一直占用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { FileInputStream inputStream = null; try { inputStream = new FileInputStream("D:\\桌面\\test.txt"); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { try { if (inputStream != null) inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
|
但是因为这种方法过于繁琐,所以在JDK1.7中新增了try-with-resource
语法,用于简化这种写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) { } catch (IOException e) { e.printStackTrace(); } } }
|
现在我们拿到了文件的输入流,那么怎么才能读取文件里面的内容呢?我们可以使用read()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) { int i =inputStream.read(); System.out.println((char) i); } catch (IOException e) { e.printStackTrace(); } } }
|
无参的read()
方法会返回一个int
值来代表从流里读到的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) { int i =inputStream.read(); System.out.println((char) i);
int j =inputStream.read(); System.out.println((char) j); } catch (IOException e) { e.printStackTrace(); } } }
|
使用read()
方法可以直接读取一个字节的数据,但是流的内容是有限的,读取一个就少一个。如果想一次性全部读取完毕,可以直接使用一个while
循环来完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) { int tmp; while ((tmp = inputStream.read()) != -1) { System.out.println((char) tmp); } } catch (IOException e) { e.printStackTrace(); } } }
|
还可以给read
方法添加参数来实现其他功能,如read(byte[] b)
,通过定义一个byte数组的长度来控制read
方法每次读取的字节数量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")){ byte[] bytes = new byte[3]; while (inputStream.read(bytes) != -1) System.out.println(new String(bytes)); } catch (IOException e) { e.printStackTrace(); } } }
|
可以发现输出有一些问题,这是因为我们在test.txt文件中”Hello World”有11个字节,但是我们每次读取三个字节,就会导致:
- 第一次读取时,byte[]数组中存的内容是:
Hel
- 第二次读取时,byte[]数组中存的内容是:
lo
- 第三次读取时,byte[]数组中存的内容是:
Wor
- 第四次读取时,只能读取两个新的字节,即
ld
,而第三位没有被更新所以还是ldr
为了解决这个问题,我们可以使用inputStream.read(bytes)
返回的字节数,以便只将实际读取的字节转换为字符串,而不是整个 bytes 数组。这将确保只有有效的字符被转换并输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")){ byte[] bytes = new byte[3]; int bytesRead; while ((bytesRead = inputStream.read(bytes)) != -1) { System.out.println(new String(bytes, 0, bytesRead)); } } catch (IOException e) { e.printStackTrace(); } } }
|
使用available
方法可以查看当前可读的剩余字节数量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")){ System.out.println(inputStream.available()); } catch (IOException e) { e.printStackTrace(); } } }
|
也就是说如果我们不知道我们的文件里到底有多少个字节就可以用这种方法输出全部内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { try(FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")) { byte[] bytes = new byte[inputStream.available()]; System.out.println(inputStream.read(bytes)); System.out.println(new String(bytes)); } catch (IOException e){ e.printStackTrace(); } } }
|
但是这种方法仅限于纯文本文件。
也可以控制要读取数量:
1
| System.out.println(inputStream.read(bytes, 1, 2));
|
注意:一次性读取同单个读取一样,当没有任何数据可读时,依然会返回-1
通过skip()
方法可以跳过指定数量的字节:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("D:\\桌面\\test.txt")){ System.out.println(inputStream.skip(1)); System.out.println((char) inputStream.read()); } catch (IOException e) { e.printStackTrace(); } } }
|
既然有输入流,那么也有输出流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileOutputStream outputStream = new FileOutputStream("D:\\桌面\\test.txt")){ outputStream.write('A'); } catch (IOException e) { e.printStackTrace(); } } }
|
但是这种方法一次只能写入一个字符,要想写入一个字符串,需要借助调用字符串的getBytes()
方法将其转化为byte[]数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileOutputStream outputStream = new FileOutputStream("D:\\桌面\\test.txt")){ outputStream.write("Hello World!".getBytes()); } catch (IOException e) { e.printStackTrace(); } } }
|
与输出流相同,我们也可以控制输入的范围:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileOutputStream outputStream = new FileOutputStream("D:\\桌面\\test.txt")){ outputStream.write("Hello World!".getBytes(), 1, 3); } catch (IOException e) { e.printStackTrace(); } } }
|
可以在最后执行一次刷新操作(强制写入)来保证数据正确写入到硬盘文件中:
我们发现,使用以上的方法会覆盖文件原有的内容,那么想要只在文件尾部进行写入操作,我们需要一个构造方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileOutputStream outputStream = new FileOutputStream("D:\\桌面\\test.txt", true)){ outputStream.write("enen".getBytes()); } catch (IOException e) { e.printStackTrace(); } } }
|
利用输入流和输出流,可以轻松实现文件的拷贝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("02-2.mp4"); FileOutputStream outputStream = new FileOutputStream("02.mp4")){ int i; while ((i = inputStream.read()) != -1) { outputStream.write(i); } } catch (IOException e) { e.printStackTrace(); } } }
|
但是以上方法拷贝的速度较慢,所以我们采用了另一种方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package org.ep;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException;
public class Main { public static void main(String[] args) { try (FileInputStream inputStream = new FileInputStream("02-2.mp4"); FileOutputStream outputStream = new FileOutputStream("02-1.mp4")){ byte[] bytes = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, bytesRead); } } catch (IOException e) { e.printStackTrace(); } } }
|
使用这种方法拷贝会非常快。
文件字符流
字符流不同于字节流,字符流是以一个具体的字符进行读取,因此它只适合读纯文本的文件,如果是其他类型的文件不适用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package org.ep;
import java.io.*;
public class Main { public static void main(String[] args) { try (FileReader reader = new FileReader("D:\\桌面\\test.txt")) { System.out.println((char) reader.read()); } catch (IOException e) { e.printStackTrace(); } } }
|
字符流支持char[]
类型作为存储:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.ep;
import java.io.*;
public class Main { public static void main(String[] args) { try (FileReader reader = new FileReader("D:\\桌面\\test.txt")) { char[] str = new char[3]; reader.read(str); System.out.println(str); } catch (IOException e) { e.printStackTrace(); } } }
|
与Reader
相对的,我们可以使用Writer
写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package org.ep;
import java.io.*;
public class Main { public static void main(String[] args) { try (FileWriter writer = new FileWriter("D:\\桌面\\test.txt", true)) { writer.getEncoding(); writer.write("噫"); writer.write("唔"); writer.flush(); } catch (IOException e) { e.printStackTrace(); } } }
|
我们也可以用append()
方法来写入,append
支持链式调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.ep;
import java.io.*;
public class Main { public static void main(String[] args) { try (FileWriter writer = new FileWriter("D:\\桌面\\test.txt",true)) { writer.getEncoding(); writer.append("1").append("5"); writer.flush(); } catch (IOException e) { e.printStackTrace(); } } }
|
我们也可以使用Reader
和Writer
来拷贝纯文本文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package org.ep;
import java.io.*;
public class Main { public static void main(String[] args) { try (FileReader reader = new FileReader("test.txt"); FileWriter writer = new FileWriter("copy.txt")) { char[] chars = new char[3]; int len; while((len = reader.read(chars)) != -1) { writer.write(chars, 0, len); } } catch (IOException e) { e.printStackTrace(); } } }
|
Flie类
File
类专门用于表示一个文件或文件夹,只不过只是代表这个文件,但并不是这个文件本身。通过File
对象可以更好地管理和操作硬盘上的文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.ep;
import java.io.*;
public class Main { public static void main(String[] args) { File file = new File("test.txt"); System.out.println(file.exists()); System.out.println(file.length()); System.out.println(file.isDirectory()); System.out.println(file.canRead()); System.out.println(file.canWrite()); System.out.println(file.canExecute()); } }
|
1 2 3 4 5 6
| true 12 false true true true
|
此外,我们还可以使用file.mkdir()
创建文件夹,或者使用file.createNewFile()
创建文件。