Java_IO流 详解

x33g5p2x  于2021-12-09 转载在 Java  
字(31.1k)|赞(0)|评价(0)|浏览(288)

Java_IO流

学习IO流重要的就是:知道什么时候用什么流

一、磁盘操作

1.1、概念

什么是文件

文件,对我们并不陌生,文件是保存数据的地方,比如大家经常使用的word文档,txt文件,excel文件…都是文件。它既可以保存一张图片,也可以保存视频、声音…

文件流

文件在程序中是以流的形式来操作的

  • 流:数据在数据源(文件)和程序(内存)之间经历的路径
  • 输入流:数据从数据源(文件)到程序(内存)的路径
  • 输出流:数据从程序(内存)到数据源(文件)的路径

1.2、常用的文件操作

创建文件对象相关构造器和方法

new File(String pathname) //根据路径构建一个File对象
new File(File parent, String child) // 根据父目录文件 + 子路径构建
new File(String parent, String child)  // 根据父目录 + 子路径构建
    
createNewFile() // 创建新文件

public class FileCreate {
   public static void main(String[] args) {
   }

   // 方式1 new File(String pathname)
   public void create01() {
      String filePtah = "G:\\news1.txt";
      File file = new File(filePtah);
      try {
         file.createNewFile();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }

   // 方式2 new File(File parent,String child) //根据父目录文件+子路径构建
   public void create02() {
      File parentFile = new File("G:\\");
      String fileName = "news2.txt";
      File file = new File(parentFile, fileName);
      //这里的file对象,在java程序中,只是一个对象
      //只有执行了createNewFile 方法,才会真正的,在磁盘创建该文件
      try {
         file.createNewFile();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }

   //方式3 new File(String parent,String child) //根据父目录+子路径构建
   public void create03() {
      String parentPath = "G:\\";
      String fileName = "news3.txt";
      File file = new File(parentPath, fileName);

      try {
         file.createNewFile();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

获取文件的相关信息

getName、getAbsolutePath、getPatent、length、exists、isFile、isDirectory

public class FileInformation {
   public static void main(String[] args) {
   }
   
   // 获取文件的信息
   public void info() {
      // 先创建文件对象
      File file = new File("G:\\news1.txt");

      // 调用相应的方法,得到对应信息
      System.out.println("文件名字=" + file.getName());

      System.out.println("绝对路径=" + file.getAbsolutePath());
      System.out.println("父级目录=" + file.getParent());
       // 在utf-8下,一个英文字符是1个字节,一个汉字是3个字节
      System.out.println("文件大小(字节)=" + file.length());
      System.out.println("文件是否存在=" + file.exists());//T
      System.out.println("是不是一个文件=" + file.isFile());//T
      System.out.println("是不是一个目录=" + file.isDirectory());//F
   }
}

目录的操作和文件删除

mkdir 创建一级目录、mkdirs创建多级目录、delete删除空目录或文件

public class Directory {
   public static void main(String[] args) {
      m1();
      m2();
      m3();
   }

   //判断 G:\news1.txt 是否存在,如果存在就删除
   public static void m1() {
      String filePath = "G:\\news1.txt";
      File file = new File(filePath);
      if (file.exists()) {
         if (file.delete()) {
            System.out.println(filePath + "删除成功");
         } else {
            System.out.println(filePath + "删除失败");
         }
      } else {
         System.out.println("该文件不存在...");
      }
   }

   //判断 D:\\demo02 是否存在,存在就删除,否则提示不存在
   //这里我们需要体会到,在java编程中,目录也被当做文件
   public static void m2() {
      String filePath = "D:\\demo02";
      File file = new File(filePath);
      if (file.exists()) {
         if (file.delete()) {
            System.out.println(filePath + "删除成功");
         } else {
            System.out.println(filePath + "删除失败");
         }
      } else {
         System.out.println("该目录不存在...");
      }

   }

   //判断 D:\\demo\\a\\b\\c 目录是否存在,如果存在就提示已经存在,否则就创建
   public static void m3() {
      String directoryPath = "D:\\demo\\a\\b\\c";
      File file = new File(directoryPath);
      if (file.exists()) {
         System.out.println(directoryPath + "存在..");
      } else {
         if (file.mkdirs()) { //创建一级目录使用mkdir() ,创建多级目录使用mkdirs()
            System.out.println(directoryPath + "创建成功..");
         } else {
            System.out.println(directoryPath + "创建失败...");
         }
      }
   }
}

二、IO流原理和分类

2.1、Java IO流原理

  • I/O 是Input/Output的缩写,I/O技术是非常实用的技术,用于处理数据传输。如读/写文件,网路通讯等。
  • Java程序中,对于数据的输入/输出操作以“流(Stream)”的方式进行。
  • java.io 包下提供了各种 “流”流和接口,用以获取不同种类的数据,并通过方法输入或输出数据
  • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中
  • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中

2.2、流的分类

按操作数据单位不同分为:字节流(8 bit)二进制文件,字符流(按字符)文本文件

按数据流的流向不同分为:输入流,输出流

按流的角色的不同分为:字节流,处理流/包装流

Java的IO流共涉及40多个类,实际上非常规则,都是从如上4个抽象基类派生的。

由这四个类派生出来的子类名称都是以其父类名作为子类名后缀

2.3、常用的类

InputStream:字节输入流

InputStream 抽象类是所有类字节输入流的超类

InputStream 常用的子类

  • FileInputStream:文件输入流
  • BufferedInputStream:缓冲字节输入流
  • ObjectInputStream:对象字节输入流

FileInputStream

public class FileInputStream01 {
   public static void main(String[] args) {
// readFile01();
      readFile02();
   }

   /** * 读取文件 * 单个字节读取,效率低 * -> read(byte[] b) */
   public static void readFile01() {
      String filePath = "G:\\hello.txt";
      int readData = 0;
      FileInputStream fileInputStream = null;
      try {
         // 创建FileInputStream 对象,用于读取文件
         fileInputStream = new FileInputStream(filePath);
         // 从该输入流读取一个字节的数据。如果没有输入可用,此方法将阻止。
         // 如果返回-1,表示读取完毕
         while ((readData = fileInputStream.read()) != -1) {
            System.out.print((char) readData);
         }
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         try {
            fileInputStream.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }

   public static void readFile02() {
      String filePath = "G:\\hello.txt";
      int readData = 0;
      // 字节数组
      byte[] buf = new byte[8];  // 一次读取8个字节
      FileInputStream fileInputStream = null;
      try {
         // 创建FileInputStream 对象,用于读取文件
         fileInputStream = new FileInputStream(filePath);
         // 从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。
         // 如果读取正常,返回实际读取的字节数
         while ((readData = fileInputStream.read(buf)) != -1) {
            System.out.print(new String(buf, 0, readData));
         }
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         try {
            fileInputStream.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
}
FileOutputStream

// 演示FileInputStream的使用(字节输入流 文件--> 程序)
public class FileOutputStream01 {
   public static void main(String[] args) {
      writeFile();
   }

   public static void writeFile() {
      // 创建 FileOutputStream对象
      String filePath = "G:\\hello.txt";

      FileOutputStream fileOutputStream = null;

      try {
         /** * 1. new FileOutputStream(filePath) 创建方式,当写入内容时,会覆盖原来的内容 * 2. new FileOutputStream(filePath, true) 创建方式,当写入内容时,是追加到文件后面 */
         fileOutputStream = new FileOutputStream(filePath, true);
         // 写入一个字节
// fileOutputStream.write('a');
         // 写入字符串
         String str = "hello,world";
         fileOutputStream.write(str.getBytes());
         /** * write(byte[] b, int off, int len) * 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此文件输出流。 */
         fileOutputStream.write(str.getBytes(), 0, 0);

      } catch (Exception e) {
         e.printStackTrace();
      }finally {
         try {
            fileOutputStream.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }
}

文件拷贝

public class FileCopy {
   public static void main(String[] args) throws Exception {
      // 文件拷贝,将G:
      /* 思路分析 1. 创建文件的输入流,将文件读入到程序 2. 创建文件的输出流,将读取到的文件数据,写入到指定的文件 */
      String srcFilePath = "G:\\Koala.jpg";
      String destFilePath = "G:\\Koala2.jpg";

      FileInputStream fileInputStream = new FileInputStream(srcFilePath);
      FileOutputStream fileOutputStream = new FileOutputStream(destFilePath);

      // 定义一个字节数组,提高读取效果
      byte[] buf = new byte[1024];
      int readLen = 0;

      while ((readLen = fileInputStream.read(buf)) != -1) {
         // 读取到后,就写入到文件 通过 fileOutputStream
         fileOutputStream.write(buf, 0, readLen); // 一定要使用这个方法
      }
      System.out.println("拷贝ok~");

      fileInputStream.close();
      fileOutputStream.close();
   }
}
FileReader 和 FileWriter

FileReader和FileWriter 是字符流,即按照字符来操作IO

FileReader相关方法:

new FileReader(File/String)
read:每次读取单个字符,返回该字符,如果到文件末尾返回-1
read(char[]):批量读取多个字符到数组,返回读取到的字符数,如果到文件末尾返回-1
    
相关API
new String(char[]):将char[] 转换为String
new String(carr[],off,len):将char[]的指定部分转换成String

FileWriter常用方法

new FileWriter(File/String):覆盖模式,相当于流的指针在首端
new FileWriter(File/String, true):追加模式,相当于流的指针在尾端
writer(int): 写入单个字符
write(char[]):写入指定数组
write(char[], oof, len):写入指定数组的指定部分
write(string):写入真个字符串
write(string, off, len):写入字符串的指定部分

相关API:String类: toCharArray:将String转换成char[]

注意:FileWriter使用后,必须要关闭(close)或刷新(flush),否则写入不到指定的文件!

案例

public class FileReader01 {
   public static void main(String[] args) {
// readFile01();
      readFile02();
   }

   /** * 单个字符读取 */
   public static void readFile01() {
      String filePath = "G:\\hello.txt";
      FileReader fileReader = null;
      int data = 0;
      try {
         fileReader = new FileReader(filePath);
         // 循环读取 使用read,单个字符读取
         while ((data = fileReader.read()) != -1) {
            System.out.print((char) data);
         }
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         try {
            fileReader.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }

   /** * 字符数组读取 */
   public static void readFile02() {
      String filePath = "G:\\hello.txt";
      FileReader fileReader = null;

      int readLen = 0;
      char[] buf = new char[1024];
      try {
         fileReader = new FileReader(filePath);
         // 循环读取 使用read(buf),单个字符读取
         while ((readLen = fileReader.read(buf)) != -1) {
            System.out.print(new String(buf, 0, readLen));
         }
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         try {
            fileReader.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
   }

}
public class FileWriter01 {
   public static void main(String[] args) {
      String filePath = "G:\\hello.txt";
      FileWriter fileWriter = null;
      char[] chars = {'a', 'b', 'c'};
      try {
         fileWriter = new FileWriter(filePath);
         // 5种方式的写
         fileWriter.write("H");
         fileWriter.write(chars);
         fileWriter.write("hahahaha".toCharArray(), 0, 3);
         fileWriter.write("hello啊");
         fileWriter.write("上海天津", 0, 2);
      } catch (IOException e) {
         e.printStackTrace();
      } finally {
         try {
            // 对于FileWriter,一定要关闭流,或者flush才能真正的把数据写入到文件
            fileWriter.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
      System.out.println("程序结束。。。。");
   }
}

注意: FileWriter,一定要关闭流,或者flush才能真正的把数据写入到文件

三、节点流和处理流

3.1、概念

节点流可以从一个特定的数据源读写数据,如FileReader,FileWriter

处理流(也叫包装流)是“连接”在已存在的流(节点流或者处理流)之上,为程序提供更为强大的读写功能,如BufferedReader、BufferedWriter。

节点流和处理流一览图

节点流和处理流的区别和联系

  • 节点流是底层流/低级流,直接跟数据源相接。
  • 处理流包装节点流,既可以消除不同节点流的实现差异,一可以提供更方便的方法来完成输入输出。
  • 处理流(也叫包装流)对节点流进行包装,使用了装饰者模式,不会直接与数据源相连

处理流的功能主要体现在两方面:

  • 性能的提高:主要以增加缓冲的方式来提高输入输出的效率。
  • 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活方便

案例:模拟BufferedReader

public abstract class Reader_ { //抽象类
    public void readFile() {
    }
    public void readString() {
    }
    
    //在Reader_ 抽象类,使用read方法统一管理.
    //后面在调用时,利于对象动态绑定机制, 绑定到对应的实现子类即可.
    //public abstract void read();
}

public class FileReader_ extends Reader_ {

        public void readFile() {
        System.out.println("对文件进行读取...");
    }
}

public class StringReader_ extends Reader_ {
    public void readString() {
        System.out.println("读取字符串..");
    }
}

public class BufferedReader_ extends Reader_{

    private Reader_ reader_; //聚合 Reader_

    public BufferedReader_(Reader_ reader_) {
        this.reader_ = reader_;
    }

    public void readFile() { //封装一层
        reader_.readFile();
    }

    //让方法更加灵活, 多次读取文件, 或者加缓冲byte[] ....
    public void readFiles(int num) {
        for(int i = 0; i < num; i++) {
            reader_.readFile();
        }
    }

    //扩展 readString, 批量处理字符串数据
    public void readStrings(int num) {
        for(int i = 0; i <num; i++) {
            reader_.readString();
        }
    }
}

public class Test_ {
    public static void main(String[] args) {
        BufferedReader_ bufferedReader_ = new BufferedReader_(new FileReader_());
        bufferedReader_.readFiles(10);
        //bufferedReader_.readFile();

        //这次希望通过 BufferedReader_ 多次读取字符串
        BufferedReader_ bufferedReader_2 = new BufferedReader_(new StringReader_());
        bufferedReader_2.readStrings(5);
    }
}

3.2、BufferedReader

BufferedWriter 和 BufferedWriter 属于字符流,是按照字符来读取数据的

关闭时,只需要关闭外层流即可

public class BufferedReader_ {
   public static void main(String[] args) throws Exception {
      String filePath = "G:\\a.java";

      BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));

      String line;
      while ((line = bufferedReader.readLine()) != null) {
         System.out.println(line);
      }

      // 关闭流,这里注意,只需要关闭 BufferedReader,因为磁层会自动的去关闭 节点流
      bufferedReader.close();
   }
}

debug发现:这里关闭的是我们传进去的节点流 new FileReader(filePath)

3.3、BufferedWriter

public class BufferedWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "G:\\ok.txt";
        //创建BufferedWriter
        //说明:
        //1. new FileWriter(filePath, true) 表示以追加的方式写入
        //2. new FileWriter(filePath) , 表示以覆盖的方式写入
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath));
        bufferedWriter.write("hello, bufferedWriter1");
        bufferedWriter.newLine(); // 插入一个和系统相关的换行
        bufferedWriter.write("hello, bufferedWriter2");
        bufferedWriter.newLine();
        bufferedWriter.write("hello, bufferedWriter3");

        bufferedWriter.close();
    }
}

案例:使用BufferedReader 和BufferedReader 完成文件拷贝

public class BufferedCopy_ {
   public static void main(String[] args) {
      String srcFilePath = "G:\\a.java";
      String destFilePath = "G:\\b.java";
      // 注意:以为Reader 和 Writer 是按字符读取,因此不能操作二进制文件(字节文件),比如 图片、音乐,造成读取数据错误
      BufferedReader br = null;
      BufferedWriter bw = null;
      String line;
      try {
         br = new BufferedReader(new FileReader(srcFilePath));
         bw = new BufferedWriter(new FileWriter(destFilePath));
         while ((line = br.readLine()) != null) {
            // 读一行写一行
            bw.write(line);
            bw.newLine();
         }
         System.out.println("拷贝完毕");
      } catch (Exception e) {
         e.printStackTrace();
      } finally {
         try {
            bw.close();
            br.close();
         } catch (IOException e) {
            e.printStackTrace();
         }
      }

   }
}

3.4、BufferedInputStream

BufferedInputStream是字节流,在创建BufferedInputStream时,会创建一个内部缓冲区数组。

3.5、BufferedOutputStream

BufferedOutputStream是字节流,实现缓冲的输出流,可以将多个字节写入底层输出流中,而不必对每次字节写入调用底层系统

字节处理流拷贝文件 BufferedInputStream和BufferedOutputStream

public class BufferedCopy02 {
    public static void main(String[] args) {
        String srcFilePath = "G:\\koala.jpg";
        String destFilePath = "G:\\koala.jpg";
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            // FileInputStream 是 InputStream的子类
            bis = new BufferedInputStream(new FileInputStream(srcFilePath));
            bos = new BufferedOutputStream(new FileOutputStream(destFilePath));

            // 循环读取文件,并写入到 destFilePath
            byte[] buff = new byte[1024];
            int readLen = 0;
            // 返回-1表示文件读取完毕
            while ((readLen = bis.read(buff)) != -1) {
                bos.write(buff, 0, readLen);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                bos.close();
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

思考:字节流可以操作二进制文件,可以操作文本文件吗?当然可以

3.6、对象流

ObjectInputStream 和 ObjectOutputStream

序列化和反序列化

  • 序列化就是在保存数据时,保存数据的值和数据类型

  • 反序列化就是在恢复数据时,恢复数据的值和数据类型

  • 需要让某个对象支持序列化机制,则必须让其类是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一:

  • Serializable // 这个一个标记接口

  • Externalizable

ObjectOutputStream 提供 序列化功能

ObjectInputStream 提供反序列化功能

使用ObjectOutputStream序列化基本数据类型和一个Dog对象(name, age),并保存到data.dat 文件中

public class ObjectOutputStream_ {
   public static void main(String[] args) throws Exception {
      // 序列化后,保存的文件格式,不是存文本,而是按照他的格式来保存
      String filePath = "G:\\data.dat";

      ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));

      //序列化数据到 e:\data.dat
      oos.writeInt(100);// int -> Integer (实现了 Serializable)
      oos.writeBoolean(true);// boolean -> Boolean (实现了 Serializable)
      oos.writeChar('a');// char -> Character (实现了 Serializable)
      oos.writeDouble(9.5);// double -> Double (实现了 Serializable)
      oos.writeUTF("韩顺平教育");//String

      //保存一个dog对象
      oos.writeObject(new Dog("旺财", 10));
      oos.close();
      System.out.println("数据保存完毕(序列化形式)");
   }
}

public class ObjectInputStream_ {
   public static void main(String[] args) throws Exception {
      //指定反序列化的文件
      String filePath = "G:\\data.dat";
      ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
      //读取
      // 读取(反序列化)的顺序需要和你保存数据(序列化)的顺序一致 否则会出现异常
      System.out.println(ois.readInt());
      System.out.println(ois.readBoolean());
      System.out.println(ois.readChar());
      System.out.println(ois.readDouble());
      System.out.println(ois.readUTF());

      //dog 的编译类型是 Object , dog 的运行类型是 Dog
      Object dog = ois.readObject();
      System.out.println("运行类型=" + dog.getClass());
      System.out.println("dog信息=" + dog);//底层 Object -> Dog

      //这里是特别重要的细节:
      //1. 如果我们希望调用Dog的方法, 需要向下转型
      Dog dog2 = (Dog) dog;
      System.out.println(dog2.getName()); //旺财..

      //关闭流, 关闭外层流即可,底层会关闭 FileInputStream 流
      ois.close();
   }
}

注意事项和细节说明

  • 读写顺序要一致

  • 要求实现序列化或反序列化对象,需要实现Serializable接口

  • 序列化的类中建议添加SerialVersionUID, 为了提高版本的兼容性

  • 序列化对象时,默认将里面所有的属性都进行序列化,但除了static 或 transient 修饰的成员

  • static 或 transient 修饰的成员不会进行序列化

  • 序列化对象时,要求里面属性的类型也需要实现序列化接口

  • 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化

private static final long serialVersionUID = 1L;

3.7、标准输入输出流

  • 标准输入 --> System.in (类型 InputStream)
  • 标准输出 --> System.out (类型 PrintStream)
public class InputAndOutput_ {
   public static void main(String[] args) {
      //System 类 的 public final static InputStream in = null;
      // System.in 编译类型 InputStream
      // System.in 运行类型 BufferedInputStream
      // 表示的是标准输入 键盘
      System.out.println(System.in.getClass());

      //1. System.out public final static PrintStream out = null;
      //2. 编译类型 PrintStream
      //3. 运行类型 PrintStream
      //4. 表示标准输出 显示器
      System.out.println(System.out.getClass());

      System.out.println("hello, ~");

      Scanner scanner = new Scanner(System.in);
      System.out.println("输入内容");
      String next = scanner.next();
      System.out.println("next=" + next);
      scanner.close();
   }
}

3.8、转换流

新建一个txt文件,以ANSI格式保存

public class CodeQuestion {
   public static void main(String[] args) throws IOException {
      //读取 G:\\a.txt 文件到程序
      //思路
      //1. 创建字符输入流 BufferedReader [处理流]
      //2. 使用 BufferedReader 对象读取a.txt
      //3. 默认情况下,读取文件是按照 utf-8 编码
      String filePath = "G:\\a.txt";
      BufferedReader br = new BufferedReader(new FileReader(filePath));

      String s = br.readLine();
      System.out.println("读取到的内容: " + s);
      br.close();
   }
}

可以看到,读取乱码,因为文本文件不是utf-8

出现乱码是因为没有指定读取的编码方式,转换流可以把字节流转换成字符流,字节流是可以指定编码方式的

InputStreamReader 和 OutputStreamWriter

  • InputStreamReader 是 Reader 的子类,可以将 InputStream(字节流)转换成Reader(字符流)
  • OutputStreamWriter 是 Writer 的子类,可以将 OutputStream (字节流)转换成 Writer(字符流)
  • 当处理纯文本数据时,如果使用字符流效率更高,并且可以有效解决中文乱码问题,所以建议将字节流转换成字符流
  • 可以在使用时指定编码格式(比如 utf-8,gbk, gb2312 ,ISO8859-1 等)

/** * 演示使用 InputStreamReader 转换流解决中文乱码问题 * 将字节流 FileInputStream 转成字符流 InputStreamReader, 指定编码 gbk/utf-8 */
public class InputStreamReader_ {
   public static void main(String[] args) throws Exception {
      String filePath = "G:\\a.txt";

      //1. 把 FileInputStream 转成 InputStreamReader
      //2. 指定编码 gbk
      InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "gbk");
      //3. 把 InputStreamReader 传入 BufferedReader
      BufferedReader br = new BufferedReader(isr);
      
      //4. 读取
      String s = br.readLine();
      System.out.println("读取内容=" + s);
      //5. 关闭外层流
      br.close();
   }
}
/** * 演示 OutputStreamWriter 使用 * 把FileOutputStream 字节流,转成字符流 OutputStreamWriter * 指定处理的编码 gbk/utf-8/utf8 */
public class OutputStreamWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "G:\\lsp.txt";
        Charset charset = Charset.forName("gbk");
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath), charset);
        osw.write("hi, lsp");
        osw.close();

        System.out.println("按照:" + charset + " 保存文件成功!");
    }
}

3.9、打印流

PrintStream、PrintWriter

public class PrintStream_ {
    public static void main(String[] args) throws IOException {
        PrintStream out = System.out;
        // 在默认情况下,PrintStream 输出数据的位置是 标准输出,即显示器
        /* public void print(String var1) { if (var1 == null) { var1 = "null"; } this.write(var1); } */
        out.print("Hello");
        // 因为print底层使用的是write,所以我们可以直接调用write进行打印/输出
        out.write("hello, 你好".getBytes());
        out.close();

        //我们可以去修改打印流输出的位置/设备
        //1. 输出修改成到 "G:\f1.txt"
        //2. "hello, 韩顺平教育~" 就会输出到 e:\f1.txt
        //3. 
        /* public static void setOut(PrintStream out) { checkIO(); setOut0(out); // native 方法,修改了out } */
        System.setOut(new PrintStream("G:\\f1.txt"));
        System.out.println("hell啊,lsp");

    }
}

public class PrintWriter_ {
   public static void main(String[] args) throws IOException {

// PrintWriter printWriter = new PrintWriter(System.out);
      PrintWriter printWriter = new PrintWriter(new FileWriter("G:\\f2.txt"));
      printWriter.print("hi,北京你好!");
      printWriter.close();
   }
}

3.10、Properties类

// 传统的方法
public class Properties01 {
   public static void main(String[] args) throws IOException {

      //读取mysql.properties 文件,并得到ip, user 和 pwd
      BufferedReader br = new BufferedReader(new FileReader("mysql.properties"));
      String line = "";
      while ((line = br.readLine()) != null) {
         String[] split = line.split("=");
         //如果我们要求指定的ip值
         if ("ip".equals(split[0])) {
            System.out.println(split[0] + "值是: " + split[1]);
         }
      }
      br.close();
   }
}

Properties类

  • 专门用于读写配置文件的集合类

配置文件的格式:

  • 键=值
  • 注意:键值对不需要有空格,值不需要用引号引起来。默认类型是string
  • Properties的常见方法
- load:加载配置文件的键值对到Properties对象
- list:将数据显示到指定设备
- getProperty(key) :根据键获取值
- setProperty(key, value):设置键值对到Properties对象
- store:将Properties中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为unicode码
public class Properties02 {
   public static void main(String[] args) throws IOException {
      //使用Properties 类来读取mysql.properties 文件
      
      //1. 创建Properties 对象
      Properties properties = new Properties();
      //2. 加载指定配置文件
      properties.load(new FileReader("src\\mysql.properties"));
      //3. 把k-v显示控制台
      properties.list(System.out);
      //4. 根据key 获取对应的值
      String user = properties.getProperty("user");
      String pwd = properties.getProperty("pwd");
      System.out.println("用户名=" + user);
      System.out.println("密码是=" + pwd);
   }
}
public class Properties03 {
    public static void main(String[] args) throws IOException {
        //使用Properties 类来创建 配置文件, 修改配置文件内容
        Properties properties = new Properties();
        //创建
        //1.如果该文件没有key 就是创建
        //2.如果该文件有key ,就是修改
        /* Properties 父类是 Hashtable , 底层就是Hashtable 核心方法 public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value;//如果key 存在,就替换 return old; } } addEntry(hash, key, value, index);//如果是新k, 就addEntry return null; } */
        properties.setProperty("charset", "utf-8");
        properties.setProperty("user", "汤姆");//注意保存时,是中文的 unicode码值
        properties.setProperty("pwd", "888888");

        //将k-v 存储文件中即可
        properties.store(new FileOutputStream("src\\mysql2.properties"), null);
        System.out.println("保存配置文件成功~");
    }
}

练习

/** * (1) 在判断e盘下是否有文件夹mytemp ,如果没有就创建mytemp * (2) 在G:\\mytemp 目录下, 创建文件 hello.txt * (3) 如果hello.txt 已经存在,提示该文件已经存在,就不要再重复创建了 * (4) 并且在hello.txt 文件中,写入 hello,world~ */
public class Homework01 {
   public static void main(String[] args) throws IOException {
      String directoryPath = "G:\\mytemp";
      File file = new File(directoryPath);
      if (!file.exists()) {
         //创建
         if (file.mkdirs()) {
            System.out.println("创建 " + directoryPath + " 创建成功");
         } else {
            System.out.println("创建 " + directoryPath + " 创建失败");
         }
      }

      String filePath = directoryPath + "\\hello.txt";// e:\mytemp\hello.txt
      file = new File(filePath);
      if (!file.exists()) {
         //创建文件
         if (file.createNewFile()) {
            System.out.println(filePath + " 创建成功~");

         } else {
            System.out.println(filePath + " 创建失败~");
         }
      } else {
         //如果文件已经存在,给出提示信息
         System.out.println(filePath + " 已经存在,不在重复创建...");
      }
      //如果文件存在,我们就使用BufferedWriter 字符输入流写入内容
      BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file));
      bufferedWriter.write("hello, world~~ 韩顺平教育");
      bufferedWriter.close();
   }
}
/** * 要求: 使用BufferedReader读取一个文本文件,为每行加上行号, * 再连同内容一并输出到屏幕上。 */
public class Homework02 {
    public static void main(String[] args) throws IOException {
        String filePath = "G:\\mytemp\\hello.txt";

        BufferedReader br = new BufferedReader(new FileReader(filePath));
        String line = "";
        int lineNum = 0;
        while ((line = br.readLine()) != null) {
            System.out.println(++lineNum + " " + line);
        }
        br.close();
    }
}
/** * (1) 要编写一个dog.properties name=tom age=5 color=red * (2) 编写Dog 类(name,age,color) 创建一个dog对象,读取dog.properties 用相应的内容完成属性初始化, 并输出 * (3) 将创建的Dog 对象 ,序列化到 文件 e:\\dog.dat 文件 */
public class Homework03 {
   public static void main(String[] args) throws Exception {

      String filePath = "dog.properties";

      Properties properties = new Properties();
      properties.load(new FileInputStream(filePath));

      String name = properties.get("name") + ""; //Object -> String
      int age = Integer.parseInt(properties.get("age") + "");// Object -> int
      String color = properties.get("color") + "";//Object -> String

      Dog dog = new Dog(name, age, color);
      System.out.println("===dog对象信息====");
      System.out.println(dog);

      //将创建的Dog 对象 ,序列化到 文件 dog.dat 文件
      String serFilePath = "G:\\dog.bat";
      ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(serFilePath));
      objectOutputStream.writeObject(dog);

      objectOutputStream.close();
      System.out.println("dog 对象,序列化完成");

      m1();
   }

   // 反序列化
   public static void m1() throws Exception {
      String serFilePath = "G:\\dog.bat";
      ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(serFilePath));
      Dog dog = (Dog) objectInputStream.readObject();
      System.out.println("=======反序列化后的dog========");
      System.out.println(dog);
   }
}

class Dog implements Serializable {
   private static final long serialVersionUID = 1306781508659844850L;
   private String name;
   private int age;
   private String color;

   public Dog(String name, int age, String color) {
      this.name = name;
      this.age = age;
      this.color = color;
   }

   @Override
   public String toString() {
      return "Dog{" +
            "name='" + name + '\'' +
            ", age=" + age +
            ", color='" + color + '\'' +
            '}';
   }
}

四、操作Zip

ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容:

另一个JarInputStream是从ZipInputStream派生,它增加的主要功能是直接读取jar文件里面的MANIFEST.MF文件。因为本质上jar包就是zip包,只是额外附加了一些固定的描述文件。

读取zip包

我们来看看ZipInputStream的基本用法。

我们要创建一个ZipInputStream,通常是传入一个FileInputStream作为数据源,然后,循环调用getNextEntry(),直到返回null,表示zip流结束。

一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,我们就用read()方法不断读取,直到返回-1

try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
    ZipEntry entry = null;
    while ((entry = zip.getNextEntry()) != null) {
        String name = entry.getName();
        if (!entry.isDirectory()) {
            int n;
            while ((n = zip.read()) != -1) {
                ...
            }
        }
    }
}

写入zip包

ZipOutputStream是一种FilterOutputStream,它可以直接写入内容到zip包。我们要先创建一个ZipOutputStream,通常是包装一个FileOutputStream,然后,每写入一个文件前,先调用putNextEntry(),然后用write()写入byte[]数据,写入完毕后调用closeEntry()结束这个文件的打包。

try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
    File[] files = ...
    for (File file : files) {
        zip.putNextEntry(new ZipEntry(file.getName()));
        zip.write(getFileDataAsBytes(file));
        zip.closeEntry();
    }
}

上面的代码没有考虑文件的目录结构。如果要实现目录层次结构,new ZipEntry(name)传入的name要用相对路径。

小结

ZipInputStream可以读取zip格式的流,ZipOutputStream可以把多份数据写入zip包;

配合FileInputStreamFileOutputStream就可以读写zip文件。

五、使用Files

从Java 7开始,提供了FilesPaths这两个工具类,能极大地方便我们读写文件。

虽然FilesPathsjava.nio包里面的类,但他俩封装了很多读写文件的简单方法,例如,我们要把一个文件的全部内容读取为一个byte[],可以这么写:

byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));

如果是文本文件,可以把一个文件的全部内容读取为String

// 默认使用UTF-8编码读取:
String content1 = Files.readString(Paths.get("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt"));

写入文件也非常方便:

// 写入二进制文件:
byte[] data = ...
Files.write(Paths.get("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Paths.get("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Paths.get("/path/to/file.txt"), lines);

此外,Files工具类还有copy()delete()exists()move()等快捷方法操作文件和目录。

最后需要特别注意的是,Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。

小结

对于简单的小文件读写操作,可以使用Files工具类简化代码。

六、网络操作

Java 中的网络支持:

  • InetAddress:用于表示网络上的硬件资源,即 IP 地址;
  • URL:统一资源定位符;
  • Sockets:使用 TCP 协议实现网络通信;
  • Datagram:使用 UDP 协议实现网络通信。

6.1、InetAddress

没有公有的构造函数,只能通过静态方法来创建实例。

InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);

6.2、URL

可以直接从 URL 中读取字节流数据。

public static void main(String[] args) throws IOException {
    URL url = new URL("http://www.baidu.com");

    /* 字节流 */
    InputStream is = url.openStream();

    /* 字符流 */
    InputStreamReader isr = new InputStreamReader(is, "utf-8");

    /* 提供缓存功能 */
    BufferedReader br = new BufferedReader(isr);

    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }

    br.close();
}

6.3、Sockets

  • ServerSocket:服务器端类
  • Socket:客户端类
  • 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。

6.4、Datagram

  • DatagramSocket:通信类
  • DatagramPacket:数据包类

七、NIO

New IO

新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。

7.1、流与块

I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。

面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

I/O 包和 NIO 已经很好地集成了,java.io.* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。

7.2、通道与缓冲区

通道

通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。

通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。

通道包括以下类型:

  • FileChannel:从文件中读写数据;
  • DatagramChannel:通过 UDP 读写网络中数据;
  • SocketChannel:通过 TCP 读写网络中数据;
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

缓冲区

发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。

缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

缓冲区包括以下类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

7.3、缓冲区状态变量

  • capacity:最大容量;
  • position:当前已经读写的字节数;
  • limit:还可以读写的字节数。

状态变量的改变过程举例:

① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。

② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。

③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。

④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。

⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。

7.4、文件 NIO 实例

以下展示了使用 NIO 快速复制文件的实例:

public static void fastCopy(String src, String dist) throws IOException {
    /* 获得源文件的输入字节流 */
    FileInputStream fin = new FileInputStream(src);

    /* 获取输入字节流的文件通道 */
    FileChannel fcin = fin.getChannel();

    /* 获取目标文件的输出字节流 */
    FileOutputStream fout = new FileOutputStream(dist);

    /* 获取输出字节流的文件通道 */
    FileChannel fcout = fout.getChannel();

    /* 为缓冲区分配 1024 个字节 */
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

    while (true) {
        /* 从输入通道中读取数据到缓冲区中 */
        int r = fcin.read(buffer);

        /* read() 返回 -1 表示 EOF */
        if (r == -1) {
            break;
        }

        /* 切换读写 */
        buffer.flip();

        /* 把缓冲区的内容写入输出文件中 */
        fcout.write(buffer);

        /* 清空缓冲区 */
        buffer.clear();
    }
}

7.5、选择器

NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。

NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。

通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。

应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。

1、创建选择器

Selector selector = Selector.open();

2、将通道注册到选择器上

ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。

在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

它们在 SelectionKey 的定义如下:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

3、监听事件

int num = selector.select();

使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。

4、获取到达的事件

Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
 SelectionKey key = keyIterator.next();
 if (key.isAcceptable()) {
     // ...
 } else if (key.isReadable()) {
     // ...
 }
 keyIterator.remove();
}

5、事件循环
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。

while (true) {
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            // ...
        } else if (key.isReadable()) {
            // ...
        }
        keyIterator.remove();
    }
}

7.6、套接字 NIO 实例

public class NIOServer {
    public static void main(String[] args) throws IOException {

        Selector selector = Selector.open();

        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        ServerSocket serverSocket = ssChannel.socket();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        serverSocket.bind(address);

        while (true) {

            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();

            while (keyIterator.hasNext()) {

                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {

                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

                    // 服务器会为每个新连接创建一个 SocketChannel
                    SocketChannel sChannel = ssChannel1.accept();
                    sChannel.configureBlocking(false);

                    // 这个新连接主要用于从客户端读取数据
                    sChannel.register(selector, SelectionKey.OP_READ);

                } else if (key.isReadable()) {

                    SocketChannel sChannel = (SocketChannel) key.channel();
                    System.out.println(readDataFromSocketChannel(sChannel));
                    sChannel.close();
                }

                keyIterator.remove();
            }
        }
    }

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder data = new StringBuilder();

        while (true) {

            buffer.clear();
            int n = sChannel.read(buffer);
            if (n == -1) {
                break;
            }
            buffer.flip();
            int limit = buffer.limit();
            char[] dst = new char[limit];
            for (int i = 0; i < limit; i++) {
                dst[i] = (char) buffer.get(i);
            }
            data.append(dst);
            buffer.clear();
        }
        return data.toString();
    }
}
public class NIOClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 8888);
        OutputStream out = socket.getOutputStream();
        String s = "hello world";
        out.write(s.getBytes());
        out.close();
    }
}

7.7、内存映射文件

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

7.8、对比

NIO 与普通 I/O 的区别主要有以下两点:

  • NIO 是非阻塞的;
  • NIO 面向块,I/O 面向流。

相关文章