目录
注意:本文参考 docs/java/basis/io.md · SnailClimb/JavaGuide - Gitee.com
Java File文件类总结,看这一篇就够了(一) - 知乎
Java IO读写大文件的几种方式及测试_野草志-CSDN博客
何为 I/O?
I/O(Input/Output) 即输入/输出 。
我们先从计算机结构的角度来解读一下 I/O。
根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。
输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
输入设备向计算机输入数据,输出设备接收计算机输出的数据。
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
我们再先从应用程序的角度来解读一下 I/O。
根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
并且,用户空间的程序不能直接访问内核空间。
当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。
因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和响应)。
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。
当应用程序发起 I/O 调用后,会经历两个步骤:
1 内核等待 I/O 设备准备好数据
2 内核将数据从内核空间拷贝到用户空间。
有哪些常见的 IO 模型?
UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
这也是我们经常提到的 5 种 IO 模型。
获取用键盘输入常用的两种方法
方法 1:通过 Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
方法 2:通过 BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
Java 中 IO 流分为几种?
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流。
Java IO 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图:
按操作对象分类结构图:
既然有了字节流,为什么还要有字符流?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
File类
java.io.File是文件和目录的重要类
文件包括文件里面的内容和文件基本属性
文件基本属性:名称、大小、扩展名、修改时间等
File不涉及到具体的文件内容,只涉及属性 !!!
createNewFile, delete, exits, getAbsolutePath, getName, getParent, getPath, isDirectory, isFile, length, listFiles, mkdir, mkdirs...
public class Main {
public static void main(String[] args)
{
String path = "/users/qjq/IdeaProjects"; //已存在的目录路径
// 1.创建目录
File d = new File(path + "/testFile");
if (!d.exists()) { // exits 判断File对象是否存在
d.mkdir(); // mkdirs 创建单级目录,mkdirs 连续创建多级目录
}
System.out.println("Is d directory? " + d.isDirectory()); // isDirectory 是否是目录
// 2.创建文件
File f = new File(path + "/testFile/abc.txt");
if (!f.exists())
{
try {
f.createNewFile(); // 创建新文件(非目录)
} catch (IOException e) {
e.printStackTrace(); // 可能会因为权限不足或磁盘已满报错
}
}
// 3.输出文件相关属性
System.out.println("Is f file? " + f.isFile());
System.out.println("Name: " + f.getName());
System.out.println("Path: " + f.getPath());
System.out.println("Parent: " + f.getParent());
System.out.println("Size: " + f.length() + " bytes");
System.out.println("Last Modified time: " + f.lastModified() + " ms");
// 4.便利d目录下所有的文件信息
System.out.println("list files in d directory");
File[] fs = d.listFiles(); // 列出d目录下所有的子文件,不包括子目录下的文件
for (File f1 : fs) {
if (f1.isDirectory()) {
// TO DO WITH 子目录 ...
}
System.out.println(f1.getPath());
}
// 5.删除此文件
f.delete(); // delete 删除文件或目录
// 6.删除目录
d.delete();
/**
* 输出:
Is d directory? true
Is f file? true
Name: abc.txt
Path: /users/qjq/IdeaProjects/testFile/abc.txt
Parent: /users/qjq/IdeaProjects/testFile
Size: 0 bytes
Last Modified time: 1571230583000 ms
list files in d directory
/users/qjq/IdeaProjects/testFile/abc.txt
*/
}
}
读写大文件的几种方式
第一种,OldIO:
public static void oldIOReadFile() throws IOException{
BufferedReader br = new BufferedReader(new FileReader("G://lily_947.txt"));
PrintWriter pw = new PrintWriter("G://oldIO.tmp");
char[] c = new char[100*1024*1024];
for(;;){
if(br.read(c)!=-1){
pw.print(c);
}else{
break;
}
}
pw.close();
br.close();
}
耗时70.79s
第二种,newIO:
public static void newIOReadFile() throws IOException{
FileChannel read = new RandomAccessFile("G://lily_947.txt","r").getChannel();
FileChannel writer = new RandomAccessFile("G://newIO.tmp","rw").getChannel();
ByteBuffer bb = ByteBuffer.allocate(200*1024*1024);
while(read.read(bb)!=-1){
bb.flip();
writer.write(bb);
bb.clear();
}
read.close();
writer.close();
}
耗时47.24s
第三种,RandomAccessFile:
public static void randomReadFile() throws IOException{
RandomAccessFile read = new RandomAccessFile("G://lily_947.txt","r");
RandomAccessFile writer = new RandomAccessFile("G://random.tmp","rw");
byte[] b = new byte[200*1024*1024];
while(read.read(b)!=-1){
writer.write(b);
}
writer.close();
read.close();
}
耗时46.65
第四种,MappedByteBuffer:
public static void mappedBuffer() throws IOException{
FileChannel read = new FileInputStream("G://lily_947.txt").getChannel();
FileChannel writer = new RandomAccessFile("G://buffer.tmp","rw").getChannel();
long i = 0;
long size = read.size()/30;
ByteBuffer bb,cc = null;
while(i<read.size()&&(read.size()-i)>size){
bb = read.map(FileChannel.MapMode.READ_ONLY, i, size);
cc = writer.map(FileChannel.MapMode.READ_WRITE, i, size);
cc.put(bb);
i+=size;
bb.clear();
cc.clear();
}
bb = read.map(FileChannel.MapMode.READ_ONLY, i, read.size()-i);
cc.put(bb);
bb.clear();
cc.clear();
read.close();
writer.close();
}
相对于最后一种内存直接映射方式前面的测试其实无意义,基本秒杀。。。。。
对于很大的文件直接分块映射时内存会不够,这是因为MappedByteBuffer未被释放造成的,sun未提供直接回收MappedByteBuffer区域的方法,这个时候有两种方法解决,第一种比较愚笨的:
System.gc();
System.runFinalization();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
第二种网上找来的,利用反射调用clean方法:
public static void unmap(final MappedByteBuffer buffer) {
if (buffer == null) {
return;
}
AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
try {
Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]);
if (getCleanerMethod != null) {
getCleanerMethod.setAccessible(true);
Object cleaner = getCleanerMethod.invoke(buffer, new Object[0]);
Method cleanMethod = cleaner.getClass().getMethod("clean", new Class[0]);
if (cleanMethod != null) {
cleanMethod.invoke(cleaner, new Object[0]);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
});
}
以上两种方法感觉都别扭,还有就是可以自己分割成物理文件再循环调用,这个也不太美观。