|
| 1 | +## IO 流的分类和概述 |
| 2 | + |
| 3 | +“老王,Java IO 也太上头了吧?”新兵蛋子小二向头顶很凉快的老王抱怨道,“你瞧,我就按照传输方式对 IO 进行了一个简单的分类,就能搞出来这么多的玩意!” |
| 4 | + |
| 5 | + |
| 6 | + |
| 7 | +好久没搞过 IO 了,老王看到这幅思维导图也是吃了一惊。想想也是,他当初学习 Java IO 的时候头也大,乌央乌央的一片,全是类,估计是所有 Java 包里面类最多的,一会是 Input 一会是 Output,一会是 Reader 一会是 Writer,真不知道 Java 的设计者是怎么想的。 |
| 8 | + |
| 9 | +看着肺都快要气炸的小二,老王深深地吸了一口气,耐心地对小二说:“主要是 Java 的设计者考虑得比较多吧,所以 IO 给人一种很乱的感觉,我来给你梳理一下。” |
| 10 | + |
| 11 | +### 01、传输方式划分 |
| 12 | + |
| 13 | +就按照你的那副思维导图来说吧。 |
| 14 | + |
| 15 | +传输方式有两种,字节和字符,那首先得搞明白字节和字符有什么区别,对吧? |
| 16 | + |
| 17 | +字节(byte)是计算机中用来表示存储容量的一个计量单位,通常情况下,一个字节有 8 位(bit)。 |
| 18 | + |
| 19 | +字符(char)可以是计算机中使用的字母、数字、和符号,比如说 A 1 $ 这些。 |
| 20 | + |
| 21 | +通常来说,一个字母或者一个字符占用一个字节,一个汉字占用两个字节。 |
| 22 | + |
| 23 | + |
| 24 | + |
| 25 | +具体还要看字符编码,比如说在 UTF-8 编码下,一个英文字母(不分大小写)为一个字节,一个中文汉字为三个字节;在 Unicode 编码中,一个英文字母为一个字节,一个中文汉字为两个字节。 |
| 26 | + |
| 27 | + PS:关于字符编码,可以看前面的章节:[锟斤拷](https://mp.weixin.qq.com/s/pNQjlXOivIgO3pbYc0GnpA) |
| 28 | + |
| 29 | +明白了字节与字符的区别,再来看字节流和字符流就会轻松多了。 |
| 30 | + |
| 31 | +字节流用来处理二进制文件,比如说图片啊、MP3 啊、视频啊。 |
| 32 | + |
| 33 | +字符流用来处理文本文件,文本文件可以看作是一种特殊的二进制文件,只不过经过了编码,便于人们阅读。 |
| 34 | + |
| 35 | +换句话说就是,字节流可以处理一切文件,而字符流只能处理文本。 |
| 36 | + |
| 37 | +虽然 IO 类很多,但核心的就是 4 个抽象类:InputStream、OutputStream、Reader、Writer。 |
| 38 | + |
| 39 | +(**抽象大法真好**) |
| 40 | + |
| 41 | +虽然 IO 类的方法也很多,但核心的也就 2 个:read 和 write。 |
| 42 | + |
| 43 | +**InputStream 类** |
| 44 | + |
| 45 | +- `int read()`:读取数据 |
| 46 | +- `int read(byte b[], int off, int len)`:从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中 |
| 47 | +- `long skip(long n)`:跳过指定个数的字节 |
| 48 | +- `int available()`:返回可读的字节数 |
| 49 | +- `void close()`:关闭流,释放资源 |
| 50 | + |
| 51 | +**OutputStream 类** |
| 52 | + |
| 53 | +- `void write(int b)`: 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲) |
| 54 | +- `void write(byte b[], int off, int len)`: 将数组 b 中的从 off 位置开始,长度为 len 的字节写入 |
| 55 | +- `void flush()`: 强制刷新,将缓冲区的数据写入 |
| 56 | +- `void close()`:关闭流 |
| 57 | + |
| 58 | +**Reader 类** |
| 59 | + |
| 60 | +- `int read()`:读取单个字符 |
| 61 | +- `int read(char cbuf[], int off, int len)`:从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中 |
| 62 | +- `long skip(long n)`:跳过指定个数的字符 |
| 63 | +- `int ready()`:是否可以读了 |
| 64 | +- `void close()`:关闭流,释放资源 |
| 65 | + |
| 66 | +**Writer 类** |
| 67 | + |
| 68 | +- `void write(int c)`: 写入一个字符 |
| 69 | +- `void write( char cbuf[], int off, int len)`: 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入 |
| 70 | +- `void flush()`: 强制刷新,将缓冲区的数据写入 |
| 71 | +- `void close()`:关闭流 |
| 72 | + |
| 73 | +理解了上面这些方法,基本上 IO 的灵魂也就全部掌握了。 |
| 74 | + |
| 75 | +### 二、操作对象划分 |
| 76 | + |
| 77 | +小二,你细想一下,IO IO,不就是输入输出(Input/Output)嘛: |
| 78 | + |
| 79 | +- Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等 |
| 80 | +- Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。 |
| 81 | + |
| 82 | +所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部,比如说文件。 |
| 83 | + |
| 84 | +文件操作算是 IO 中最典型的操作了,也是最频繁的操作。那其实你可以换个角度来思考,比如说按照 IO 的操作对象来思考,IO 就可以分类为:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。 |
| 85 | + |
| 86 | + |
| 87 | + |
| 88 | + |
| 89 | +**1)文件** |
| 90 | + |
| 91 | +文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。 |
| 92 | + |
| 93 | +FileInputStream 的例子: |
| 94 | + |
| 95 | +```java |
| 96 | +int b; |
| 97 | +FileInputStream fis1 = new FileInputStream("fis.txt"); |
| 98 | +// 循环读取 |
| 99 | +while ((b = fis1.read())!=-1) { |
| 100 | + System.out.println((char)b); |
| 101 | +} |
| 102 | +// 关闭资源 |
| 103 | +fis1.close(); |
| 104 | +``` |
| 105 | + |
| 106 | +FileOutputStream 的例子: |
| 107 | + |
| 108 | +```java |
| 109 | +FileOutputStream fos = new FileOutputStream("fos.txt"); |
| 110 | +fos.write("沉默王二".getBytes()); |
| 111 | +fos.close(); |
| 112 | +``` |
| 113 | + |
| 114 | +FileReader 的例子: |
| 115 | + |
| 116 | +```java |
| 117 | +int b = 0; |
| 118 | +FileReader fileReader = new FileReader("read.txt"); |
| 119 | +// 循环读取 |
| 120 | +while ((b = fileReader.read())!=-1) { |
| 121 | + // 自动提升类型提升为 int 类型,所以用 char 强转 |
| 122 | + System.out.println((char)b); |
| 123 | +} |
| 124 | +// 关闭流 |
| 125 | +fileReader.close(); |
| 126 | +``` |
| 127 | + |
| 128 | +FileWriter 的例子: |
| 129 | + |
| 130 | +```java |
| 131 | +FileWriter fileWriter = new FileWriter("fw.txt"); |
| 132 | +char[] chars = "沉默王二".toCharArray(); |
| 133 | +fileWriter.write(chars, 0, chars.length); |
| 134 | +fileWriter.close(); |
| 135 | +``` |
| 136 | + |
| 137 | +当掌握了文件的输入输出,其他的自然也就掌握了,都大差不差。 |
| 138 | + |
| 139 | +**2)数组** |
| 140 | + |
| 141 | +通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流。 |
| 142 | + |
| 143 | +ByteArrayInputStream 的例子: |
| 144 | + |
| 145 | +```java |
| 146 | +InputStream is =new BufferedInputStream( |
| 147 | + new ByteArrayInputStream( |
| 148 | + "沉默王二".getBytes(StandardCharsets.UTF_8))); |
| 149 | +//操作 |
| 150 | +byte[] flush =new byte[1024]; |
| 151 | +int len =0; |
| 152 | +while(-1!=(len=is.read(flush))){ |
| 153 | + System.out.println(new String(flush,0,len)); |
| 154 | +} |
| 155 | +//释放资源 |
| 156 | +is.close(); |
| 157 | +``` |
| 158 | + |
| 159 | +ByteArrayOutputStream 的例子: |
| 160 | + |
| 161 | +```java |
| 162 | +ByteArrayOutputStream bos =new ByteArrayOutputStream(); |
| 163 | +byte[] info ="沉默王二".getBytes(); |
| 164 | +bos.write(info, 0, info.length); |
| 165 | +//获取数据 |
| 166 | +byte[] dest =bos.toByteArray(); |
| 167 | +//释放资源 |
| 168 | +bos.close(); |
| 169 | +``` |
| 170 | + |
| 171 | +**3)管道** |
| 172 | + |
| 173 | +Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。 |
| 174 | + |
| 175 | +一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来。 |
| 176 | + |
| 177 | +```java |
| 178 | +final PipedOutputStream pipedOutputStream = new PipedOutputStream(); |
| 179 | +final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream); |
| 180 | + |
| 181 | +Thread thread1 = new Thread(new Runnable() { |
| 182 | + @Override |
| 183 | + public void run() { |
| 184 | + try { |
| 185 | + pipedOutputStream.write("沉默王二".getBytes(StandardCharsets.UTF_8)); |
| 186 | + pipedOutputStream.close(); |
| 187 | + } catch (IOException e) { |
| 188 | + e.printStackTrace(); |
| 189 | + } |
| 190 | + } |
| 191 | +}); |
| 192 | + |
| 193 | +Thread thread2 = new Thread(new Runnable() { |
| 194 | + @Override |
| 195 | + public void run() { |
| 196 | + try { |
| 197 | + byte[] flush =new byte[1024]; |
| 198 | + int len =0; |
| 199 | + while(-1!=(len=pipedInputStream.read(flush))){ |
| 200 | + System.out.println(new String(flush,0,len)); |
| 201 | + } |
| 202 | + |
| 203 | + pipedInputStream.close(); |
| 204 | + } catch (IOException e) { |
| 205 | + e.printStackTrace(); |
| 206 | + } |
| 207 | + |
| 208 | + } |
| 209 | +}); |
| 210 | +thread1.start(); |
| 211 | +thread2.start(); |
| 212 | +``` |
| 213 | + |
| 214 | +**4)基本数据类型** |
| 215 | + |
| 216 | +基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。 |
| 217 | + |
| 218 | +DataInputStream 提供了一系列可以读基本数据类型的方法: |
| 219 | + |
| 220 | +```java |
| 221 | +DataInputStream dis = new DataInputStream(new FileInputStream(“das.txt”)) ; |
| 222 | +byte b = dis.readByte() ; |
| 223 | +short s = dis.readShort() ; |
| 224 | +int i = dis.readInt(); |
| 225 | +long l = dis.readLong() ; |
| 226 | +float f = dis.readFloat() ; |
| 227 | +double d = dis.readDouble() ; |
| 228 | +boolean bb = dis.readBoolean() ; |
| 229 | +char ch = dis.readChar() ; |
| 230 | +``` |
| 231 | + |
| 232 | +DataOutputStream 提供了一系列可以写基本数据类型的方法: |
| 233 | + |
| 234 | +```java |
| 235 | +DataOutputStream das = new DataOutputStream(new FileOutputStream(“das.txt”)); |
| 236 | +das.writeByte(10); |
| 237 | +das.writeShort(100); |
| 238 | +das.writeInt(1000); |
| 239 | +das.writeLong(10000L); |
| 240 | +das.writeFloat(12.34F); |
| 241 | +das.writeDouble(12.56); |
| 242 | +das.writeBoolean(true); |
| 243 | +das.writeChar('A'); |
| 244 | +``` |
| 245 | + |
| 246 | +**5)缓冲** |
| 247 | + |
| 248 | +CPU 很快,它比内存快 100 倍,比磁盘快百万倍。那也就意味着,程序和内存交互会很快,和硬盘交互相对就很慢,这样就会导致性能问题。 |
| 249 | + |
| 250 | +为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,比如说 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。 |
| 251 | + |
| 252 | + |
| 253 | + |
| 254 | + |
| 255 | +缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高。 |
| 256 | + |
| 257 | +**6)打印** |
| 258 | + |
| 259 | +恐怕 Java 程序员一生当中最常用的就是打印流了:`System.out` 其实返回的就是一个 PrintStream 对象,可以用来打印各式各样的对象。 |
| 260 | + |
| 261 | +```java |
| 262 | +System.out.println("沉默王二是真的二!"); |
| 263 | +``` |
| 264 | + |
| 265 | +PrintStream 最终输出的是字节数据,而 PrintWriter 则是扩展了 Writer 接口,所以它的 `print()/println()` 方法最终输出的是字符数据。使用上几乎和 PrintStream 一模一样。 |
| 266 | + |
| 267 | +```java |
| 268 | +StringWriter buffer = new StringWriter(); |
| 269 | +try (PrintWriter pw = new PrintWriter(buffer)) { |
| 270 | + pw.println("沉默王二"); |
| 271 | +} |
| 272 | +System.out.println(buffer.toString()); |
| 273 | +``` |
| 274 | + |
| 275 | +**7)对象序列化/反序列化** |
| 276 | + |
| 277 | +序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程。 |
| 278 | + |
| 279 | +```java |
| 280 | +ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
| 281 | +try (ObjectOutputStream output = new ObjectOutputStream(buffer)) { |
| 282 | + output.writeUTF("沉默王二"); |
| 283 | +} |
| 284 | +System.out.println(Arrays.toString(buffer.toByteArray())); |
| 285 | +``` |
| 286 | + |
| 287 | +与其对应的,有序列化,就有反序列化,也就是再将字节数组转成 Java 对象的过程。 |
| 288 | + |
| 289 | +```java |
| 290 | +try (ObjectInputStream input = new ObjectInputStream(new FileInputStream( |
| 291 | + new File("Person.txt")))) { |
| 292 | + String s = input.readUTF(); |
| 293 | +} |
| 294 | +``` |
| 295 | + |
| 296 | + |
| 297 | +**8)转换** |
| 298 | + |
| 299 | +InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符。 |
| 300 | + |
| 301 | +```java |
| 302 | +InputStreamReader isr = new InputStreamReader( |
| 303 | + new FileInputStream("demo.txt")); |
| 304 | +char []cha = new char[1024]; |
| 305 | +int len = isr.read(cha); |
| 306 | +System.out.println(new String(cha,0,len)); |
| 307 | +isr.close(); |
| 308 | +``` |
| 309 | + |
| 310 | +OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁。 |
| 311 | + |
| 312 | +```java |
| 313 | +File f = new File("test.txt") ; |
| 314 | +Writer out = new OutputStreamWriter(new FileOutputStream(f)) ; // 字节流变为字符流 |
| 315 | +out.write("hello world!!") ; // 使用字符流输出 |
| 316 | +out.close() ; |
| 317 | +``` |
| 318 | + |
| 319 | +“小二啊,你看,经过我的梳理,是不是感觉 IO 也没多少东西!针对不同的场景、不同的业务,选择对应的 IO 流就可以了,用法上就是读和写。”老王一口气讲完这些,长长的舒了一口气。 |
| 320 | + |
| 321 | +此时此刻的小二,还沉浸在老王的滔滔不绝中。不仅感觉老王的肺活量是真的大,还感慨老王不愧是工作了十多年的“老油条”,一下子就把自己感觉头大的 IO 给梳理得很清晰了。 |
| 322 | + |
| 323 | +--------- |
| 324 | + |
| 325 | + |
| 326 | +**这是《Java 程序员进阶之路》专栏的第 68 篇。Java 程序员进阶之路,该专栏风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点**。 |
| 327 | + |
| 328 | +GitHub 地址:<https://github.com/itwanger/toBeBetterJavaer> |
| 329 | + |
| 330 | +码云地址:[https://gitee.com/itwanger/toBeBetterJavaer](https://gitee.com/itwanger/toBeBetterJavaer) |
| 331 | + |
| 332 | +CodeChina 直达地址:[https://codechina.csdn.net/qing_gee/toBeBetterJavaer](https://codechina.csdn.net/qing_gee/toBeBetterJavaer) |
| 333 | + |
| 334 | +亮白版和暗黑版的 PDF 也准备好了呢,让我们一起成为更好的 Java 工程师吧,一起冲! |
| 335 | + |
| 336 | + |
| 337 | + |
| 338 | + |
| 339 | + |
| 340 | + |
| 341 | + |
| 342 | + |
| 343 | + |
| 344 | + |
| 345 | + |
| 346 | + |
| 347 | + |
0 commit comments