目录
何为 I/O?
有哪些常见的 IO 模型?
获取用键盘输入常用的两种方法
Java 中 IO 流分为几种?
既然有了字节流,为什么还要有字符流?
File类
读写大文件的几种方式
注意:本文参考 docs/java/basis/io.md · SnailClimb/JavaGuide - Gitee.com
Java File文件类总结,看这一篇就够了(一) - 知乎
Java IO读写大文件的几种方式及测试_野草志-CSDN博客
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 内核将数据从内核空间拷贝到用户空间。
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 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图:
按操作对象分类结构图:
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
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; } }); }
以上两种方法感觉都别扭,还有就是可以自己分割成物理文件再循环调用,这个也不太美观。