java IO理论总结

目录

何为 I/O?

有哪些常见的 IO 模型?

获取用键盘输入常用的两种方法

Java 中 IO 流分为几种?

既然有了字节流,为什么还要有字符流?

File类

读写大文件的几种方式


注意:本文参考  docs/java/basis/io.md · SnailClimb/JavaGuide - Gitee.com

Java File文件类总结,看这一篇就够了(一) - 知乎

Java IO读写大文件的几种方式及测试_野草志-CSDN博客

何为 I/O?

I/O(Input/Output) 即输入/输出 。

我们先从计算机结构的角度来解读一下 I/O。

根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。

java IO理论总结

输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。

输入设备向计算机输入数据,输出设备接收计算机输出的数据。

从计算机结构的视角来看的话, 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/OI/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: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

按操作方式分类结构图:

java IO理论总结

按操作对象分类结构图:

java IO理论总结

既然有了字节流,为什么还要有字符流?

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 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;
			}
 
		});
	}

以上两种方法感觉都别扭,还有就是可以自己分割成物理文件再循环调用,这个也不太美观。

上一篇:Java8新特性 - 03 - 函数式接口03 - Consumer详解


下一篇:DOS命令大全(转载)