小记:

Reader是带编码转换器的InputStream,它把byte转换为char,
Writer就是带编码转换器的OutputStream,它把char转换为byte并输出

InputStream 字节输入流的超类(抽象) Reader 字符输入流的超类(抽象)
字节流,以byte为单位 字符流,以char为单位
读取字节(-1,0~255):int read() 读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b) 读到字符数组:int read(char[] c)
OutputStream Writer
字节流,以byte为单位 字符流,以char为单位
写入字节(0~255):void write(int b) 写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b) 写入字符数组:void write(char[] c)
无对应方法 写入String:void write(String s)

Reader和InputStream有什么关系?
除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需
要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们
查看FileReader的源码,它在内部实际上持有一个FileInputStream。

既然Reader本质上是一个基于InputStream的byte到char的转换器
那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。
InputStreamReader就是这样一个转换器,
它可以把任何InputStream转换为Reader。示例代码如下:
使用InputStreamReader,可以把一个InputStream转换成一个Reader。

  1. // 持有InputStream:
  2. InputStream input = new FileInputStream("src/readme.txt");
  3. // 变换为Reader:
  4. Reader reader = new InputStreamReader(input, "UTF-8");
  5. //上述代码可以通过try (resource)更简洁地改写如下
  6. try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
  7. // TODO:
  8. }

IO是指Input/Output,即输入和输出。以内存为中心:

  • Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
  • Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。

为什么要把数据读到内存才能处理这些数据?因为代码是在内存中运行的,数据也必须读到内存,最终的表示方式无非是byte数组,字符串等,都必须存放在内存里。
从Java代码来看,输入实际上就是从外部,例如,硬盘上的某个文件,把内容读到内存,并且以Java提供的某种数据类型表示,例如,byte[],String,这样,后续代码才能处理这些数据。
因为内存有“易失性”的特点,所以必须把处理后的数据以某种方式输出,例如,写入到文件。Output实际上就是把Java表示的数据格式,例如,byte[],String等输出到某个地方。
IO流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为IO流。

File对象

File对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。

调用isFile(),判断该File对象是否是一个已存在的文件,
调用isDirectory(),判断该File对象是否是一个已存在的目录:

用File对象获取到一个文件时,还可以进一步判断文件的权限和大小:

  • boolean canRead():是否可读;
  • boolean canWrite():是否可写;
  • boolean canExecute():是否可执行;
  • long length():文件字节大小。

对目录而言,是否可执行表示能否列出它包含的文件和子目录。

创建和删除文件

当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件:

  1. File file = new File("/path/to/file");
  2. if (file.createNewFile()) {
  3. // 文件创建成功:
  4. // TODO:
  5. if (file.delete()) {
  6. // 删除文件成功:
  7. }
  8. }

创建临时文件

有些时候,程序需要读写一些临时文件,File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。

  1. public class Main {
  2. public static void main(String[] args) throws IOException {
  3. File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
  4. f.deleteOnExit(); // JVM退出时自动删除
  5. System.out.println(f.isFile());
  6. System.out.println(f.getAbsolutePath());
  7. }
  8. }

遍历文件和目录

当File对象表示一个目录时,可以使用list()和listFiles()列出目录下的文件和子目录名。

list() 返回一个字符串数组,命名这个抽象路径名所表示的目录中的文件和目录。如果目录为空,则数组为空。如果这个抽象路径名不表示目录,或者发生I/O错误,则返回null。

listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录:

  1. public class Main {
  2. public static void main(String[] args) throws IOException {
  3. File f = new File("C:\\Windows");
  4. File[] fs1 = f.listFiles(); // 列出所有文件和子目录
  5. printFiles(fs1);
  6. File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
  7. public boolean accept(File dir, String name) {
  8. return name.endsWith(".exe"); // 返回true表示接受该文件
  9. }
  10. });
  11. printFiles(fs2);
  12. }
  13. static void printFiles(File[] files) {
  14. System.out.println("==========");
  15. if (files != null) {
  16. for (File f : files) {
  17. System.out.println(f);
  18. }
  19. }
  20. System.out.println("==========");
  21. }
  22. }

Path

Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单:

  1. public class Main {
  2. public static void main(String[] args) throws IOException {
  3. Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
  4. System.out.println(p1);
  5. Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
  6. System.out.println(p2);
  7. Path p3 = p2.normalize(); // 转换为规范路径
  8. System.out.println(p3);
  9. File f = p3.toFile(); // 转换为File对象
  10. System.out.println(f);
  11. for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
  12. System.out.println(" " + p);
  13. }
  14. }
  15. }

InputStream / OutputStream

小结:read :文件读入内存 write :内存写到文件
InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。
这个抽象类定义的一个最重要的方法就是int read()

OutputStream也是抽象类,它是所有输出流的超类。
这个抽象类定义的一个最重要的方法就是void write(int b)

IO流以byte(字节)为最小单位,因此也称为字节流。例如,我们要从磁盘读入一个文件,包含6个字节,就相当于读入了6个字节的数据:
image.png
这6个字节是按顺序读入的,所以是输入字节流。
反过来,我们把6个字节从内存写入磁盘文件,就是输出字节流:
image.png
在Java中,InputStream代表输入字节流,OuputStream代表输出字节流,这是最基本的两种IO流。

缓冲

在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:

  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数

利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:

  1. public void readFile() throws IOException {
  2. try (InputStream input = new FileInputStream("src/readme.txt")) {
  3. // 定义1000个字节大小的缓冲区:
  4. byte[] buffer = new byte[1000];
  5. int n;
  6. while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
  7. System.out.println("read " + n + " bytes.");
  8. }
  9. }
  10. }

ByteArrayInputStream

用FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。

ByteArrayInputStream可以在内存中模拟一个InputStream:

  1. public class Main {
  2. public static void main(String[] args) throws IOException {
  3. byte[] data = { 72, 101, 108, 108, 111, 33 };
  4. try (InputStream input = new ByteArrayInputStream(data)) {
  5. int n;
  6. while ((n = input.read()) != -1) {
  7. System.out.println((char)n);
  8. }
  9. }
  10. }
  11. }

Reader / Writer

如果我们需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char来读写显然更方便,这种流称为字符流
Java提供了Reader和Writer表示字符流,字符流传输的最小数据单位是char。
Reader和Writer本质上是一个能自动编解码的InputStream和OutputStream。
使用Reader,数据源虽然是字节,但我们读入的数据都是char类型的字符,原因是Reader内部把读入的byte做了解码,转换成了char。使用InputStream,我们读入的数据和原始二进制数据一模一样,是byte[]数组,但是我们可以自己把二进制byte[]数组按照某种编码转换为字符串。究竟使用Reader还是InputStream,要取决于具体的使用场景。如果数据源不是文本,就只能使用InputStream,如果数据源是文本,使用Reader更方便一些。Writer和OutputStream是类似的。

PrintStream和PrintWriter

PrintStream是一种能接收各种数据类型的输出,打印数据时比较方便:

  • System.out是标准输出;
  • System.err是标准错误输出。

PrintWriter是基于Writer的输出,它的print()/println()方法最终输出的是char数据

PrintStream是一种FilterOutputStream,它在OutputStream的接口上,
额外提供了一些写入各种数据类型的方法:

  • 写入int:print(int)
  • 写入boolean:print(boolean)
  • 写入String:print(String)
  • 写入Object:print(Object),实际上相当于print(object.toString())

PrintStream和OutputStream相比,除了添加了一组print()/println()方法,可以打印各种数据类型,
比较方便外,它还有一个额外的优点,就是不会抛出IOException
这样我们在编写代码的时候,就不必捕获IOException。

System.err是系统默认提供的标准错误输出。

PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,
它的print()/println()方法最终输出的是char数据。两者的使用方法几乎是一模一样的

  1. public class Main {
  2. public static void main(String[] args) {
  3. StringWriter buffer = new StringWriter();
  4. try (PrintWriter pw = new PrintWriter(buffer)) {
  5. pw.println("Hello");
  6. pw.println(12345);
  7. pw.println(true);
  8. }
  9. System.out.println(buffer.toString());
  10. }
  11. }

序列化

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
为什么要把Java对象序列化呢?
因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,
这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。
有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,
或者从网络上读取byte[]并把它“变回”Java对象。

一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口

Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

  1. public interface Serializable {}

序列化:
把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流:

  1. public class Main {
  2. public static void main(String[] args) throws IOException, ClassNotFoundException {
  3. int i = 12345;
  4. String s = "Hello";
  5. //序列化
  6. ByteArrayOutputStream buffer = new ByteArrayOutputStream();
  7. try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
  8. output.writeObject(i);
  9. output.writeUTF(s);
  10. output.writeObject(Double.valueOf(123.456));
  11. }
  12. byte[] a = buffer.toByteArray();
  13. System.out.println(Arrays.toString(a));
  14. System.out.println("=======================");
  15. //反序列化
  16. ByteArrayInputStream bu = new ByteArrayInputStream(a);
  17. try (ObjectInputStream in = new ObjectInputStream(bu)) {
  18. int i1 = (int) in.readObject(); //
  19. String s1 = (String) in.readUTF();
  20. Double d1 = (Double) in.readObject();
  21. System.out.println(i1 + ", " + s1 + ", " + d1);
  22. }
  23. }
  24. }

新i/o

JDK1.4中的java.nio.* 引入了新的JavaI/O 类库, 新IO提高了速度

速度的提高来自于所使用的结构更接近于于操作系统执行I/O的方式:通道和缓冲器.

通道是数据的来源和去处,信道是去来往信道的载具.

通道要么从缓冲器获得数据,要么向缓冲器发送数据.

唯一直接与通道交互的缓冲器是ByteBuffer——即可以存储未加工字节的缓冲器
通过告知分配多少存储空间来创建一个ByteBuffer对象,并且还有一个方法选择集,用于以原始的字节形式或基本数据类型输出和读取数据. 但是没办法输出或读取对象,即使是字符串对象也不行.

channel

旧I/O类库又三个类被修改了,用以产生FileChannel.分别是FileInputStream,FileOutStream,以及用于既读又写的RandomAccessFile.这些都是字节操纵流,与低层的nio性质一致.

Reader和Writer这种字符模式类不能用于产生通道,但是java.nio.channels.Channels类提供了实用方法,用以在通道中产生Reader和Writer.

  1. public class GetChannel {
  2. // private static final int BSIZE = 1024;
  3. public static void main(String[] args) throws Exception {
  4. // 写一个文件: 并获取 getChannel() :返回与此文件输出流关联的唯一FileChannel对象 fc
  5. FileChannel fc = new FileOutputStream("data.txt").getChannel();
  6. // ByteBuffer.wrap(byte[] array) : 将字节数组封装到缓冲区中
  7. fc.write(ByteBuffer.wrap("Some text ".getBytes()));
  8. fc.close();
  9. // 添加到文件末尾:
  10. fc = new RandomAccessFile("data.txt", "rw").getChannel();
  11. fc.position(fc.size()); // 将位置设置为末尾
  12. fc.write(ByteBuffer.wrap("Some more".getBytes()));
  13. fc.close();
  14. // 读文件:
  15. fc = new FileInputStream("data.txt").getChannel();
  16. //分配一个新的字节缓冲区 ByteBuffer allocate(int capacity) capacity -新缓冲区的容量,以字节为单位
  17. //对于只读操作,我们必须显示的用静态的allocate()方法来分配ByteBuffer
  18. ByteBuffer buff = ByteBuffer.allocate(1024);
  19. fc.read(buff);
  20. buff.flip();//一旦调用read()来告知FileChannel 向 ByteBuffer 存储字节,
  21. //就必须调用缓冲器上的flip(),让它做好别人读取字节的准备
  22. //while (buff.hasRemaining()) {
  23. //相对的get方法。读取位于该缓冲区当前位置的字节,然后将该位置加1。
  24. //System.out.print((char) buff.get());
  25. System.out.println(buff.asCharBuffer());
  26. }
  27. }
  1. public class ChannelCopy {
  2. //一个简单的文件复制程序
  3. private static final int BSIZE = 1024;
  4. public static void main(String[] args) throws IOException {
  5. args = new String[]{"data.txt","X.file"};
  6. if (args.length !=2 ){
  7. System.out.println("argument sourcefile destfie");
  8. System.exit(-1);
  9. }
  10. FileChannel
  11. in = new FileInputStream(args[0]).getChannel(),
  12. out = new FileOutputStream(args[1]).getChannel();
  13. //通过缓冲器两个信道直接相连
  14. ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
  15. //把in中的 信息 复制到 out 中
  16. while (in.read(buffer) != -1){
  17. System.out.println("--");
  18. buffer.flip();
  19. out.write(buffer);
  20. buffer.clear();
  21. }
  22. }
  23. }

可以看到,打开一个FileChannel 以用于读 ,而打开另一个用于写。ByteBuffer 被分配了空间
当FileChannel.read()返回-1 时,表示我们已经达到了输入的末尾,每次read()操作后,就会将数据输入到
缓冲器中,flip()作用是翻转这个缓冲区,以便于它的信息可以由write()提取。
write()操作后,信息仍然在缓冲器中,接着clear()操作则对所有的内部指针重新安排,以便缓冲器在另一个read()操作期间能够做好接受数据的准备。

Files:轻松复制、移动、删除或处理文件的工具类,有你需要的所有方法

  1. public class FilesDemo {
  2. public static void main(String[] args) throws IOException {
  3. Path path = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
  4. //Path file = Files.createFile(path);
  5. //System.out.println(file);
  6. Files.delete(path);
  7. //记得在main()方法里 调用方法哦---
  8. }
  9. public static void copy() throws IOException {
  10. Path source = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
  11. Path target = Paths.get("D:/demo.txt");
  12. //copy 可以设置选项 move方法类似
  13. Files.copy(source,target);
  14. }
  15. public static void read(){
  16. Path file = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
  17. try(BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)){
  18. String line;
  19. while ((line = reader.readLine())!=null){
  20. System.out.println(line);
  21. }
  22. }catch (IOException e){
  23. }
  24. }
  25. public static void write(){
  26. Path file = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
  27. try(BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8, StandardOpenOption.WRITE)){
  28. //从头写
  29. //writer.write("hello world");
  30. writer.append("hello world");
  31. }catch (IOException e){
  32. }
  33. }
  34. //简化读写
  35. public static void simpleRead() throws IOException{
  36. Path file = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
  37. List<String> allLines = Files.readAllLines(file, StandardCharsets.UTF_8);
  38. for (String line : allLines) {
  39. System.out.println(line);
  40. }
  41. }
  42. public static void simpleRead2() throws IOException{
  43. Path file = Paths.get("D:/work/zk/opensource/javase/ThinkingInJava/bak.txt");
  44. byte[] bytes = Files.readAllBytes(file);
  45. System.out.println(new String(bytes));
  46. }
  47. }

指定一个接口,获取其实现类

指定一个接口FooInter,以及一个包(包中包含各种类以及FooInter的实现【不限个数】),如何获取该接口或抽象类的实现类?

  1. public class TestFoo {
  2. public static void main(String[] args) throws IOException {
  3. Path path = Paths.get("src/com/java/chapter18/FooDemo");
  4. Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
  5. @Override
  6. public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
  7. String replace = file.toString().replace('\\', '.');
  8. String dir = replace.substring(0,replace.length()-5).substring(4);
  9. // System.out.println(dir);
  10. try {
  11. //Class.forName(“类的全路径”) 类的全路径=包+类名 本例是从com.java包下开始的
  12. //之所以length-5 是因为类名不带后面的 .java
  13. if(FooInter.class.isAssignableFrom(Class.forName(dir))){
  14. System.out.println(file.getFileName());
  15. }
  16. } catch (ClassNotFoundException e) {
  17. e.printStackTrace();
  18. }
  19. return FileVisitResult.CONTINUE;
  20. }
  21. });
  22. /* URL url = TestFoo.class.getClassLoader().getResource("com/java/chapter1");
  23. System.out.println(url.toString());
  24. Path path1 = Paths.get(url.toString());*/
  25. }
  26. }