IO流详解

文章目录

  • BIO/NIO/AIO简单区别
    • BIO(同步阻塞方式)
    • NIO(同步非阻塞IO)
    • AIO(异步非阻塞IO)
  • 明确IO的四个重点
  • IO类型
  • File类
  • IO学习
  • InputStream/OutputStream
    • OutputStream
    • FileOutputStream类
      • FileOutputStream写出字节数据
    • InputStream
  • 字符流Reader和Writer
    • Reader
    • Writer
  • 过滤流
    • FilterInputStream常见子类
    • FilterOutputStream子类
  • 缓冲流
    • 缓冲流的基本原理:
    • 字节缓冲流
    • 字符缓冲流
  • 转换流
    • 字符集
    • InputStreamReader类-----(字节流到字符流的桥梁)
    • OutputStreamWriter类-----(字符流到字节流的桥梁)
  • 序列化流
    • 什么是序列化
    • 序列化的优点
    • ObjectOutputStream类(对象输出流)
    • 反序列化操作1
    • 反序列化操作2
  • 打印流

BIO/NIO/AIO简单区别

BIO(同步阻塞方式)

BIO就是传统的java.io包,他是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前线程会一直处于阻塞状态。

优点:代码简单,直观

缺点:IO的效率和扩展性低,容易成为应用性能的瓶颈。

NIO(同步非阻塞IO)

NIO是java1.4引入的java.nio包,提供了Channel、Selector、Buffer等新的抽象类,可以构建多路复用的,同步非阻塞IO程序,同时提供了更新近操作系统底层高性能的数据操作方式

AIO(异步非阻塞IO)

AIO是java1.7之后引入的包,是NIO的升级版本,提供了异步非阻塞的IO操作方式(Asynchronous IO),异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完全,操作系统会通知相应的线程进行后续的操作

明确IO的四个重点

  1. 明确所要操作的数据(数据源/数据目的,即读/写)
    • 源:InputStream Reader
    • 目的: OutputStream Writer
  2. 明确要操作数据是字节还是字符
    • 字节:InputStream OutputStram
    • 字符:Reader Writer
  3. 明确数据所在的具体设备
    • 硬盘:文件File开头
    • 内存:数组,字符串
    • 屏幕:System.out
    • 网络:Socket
  4. 明确是否需要额外需求
    • 流是否需要转换:
      1. 字节转字符:InputStreamReader(读取字节,解码为字符)
      2. 字符转字节:OutputStreamWriter(写出字符,编码为字节)
    • 效率是否高效:增加缓冲流BufferedXXXX
    • 多个源:序列化SequenceInputStream
    • 对象序列化:
      1. ObjectInputStream(序列化——>对象转换为字节)
      2. ObjectOutputStream(反序列化——>字节重构为对象)
    • 保证数据的输入形式:打印流——>PrintStream、PrintWriter
    • 操作基本数据,保证字节原样性:DataOutputStream、DataInputStream

IO类型

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
访问字符串 StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
抽象基类 FileInputStream FileOutputStream FileReader FileWriter
打印流 PrintStream PrintWriter
推回输入流 PushBackInputStream PushbackReader
特殊流 DataInputStream DataOutputStream

IO流详解_第1张图片

File类

学习IO流的前提就是熟悉File类,数据的输入输出始终离不开文件,所以IO流的学习的起点就是File类

概述

java.io.File 类是专门对文件进行操作的类,只能对文件本身进行操作,而不能对文件的内容进行操作,

该类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

小结:

  1. File与流无关,File类不能对文件进行读写,即输入输出。
  2. File类主要表示类似 D:\\文件目录 与 D:\\文件目录\\文件.txt,前者是文件夹,后者是文件,File类就是操作这两个的类

构造方法

File(File parent, String child) 从父抽象路径名和子路径名字符串创建新的 File实例。
File(String pathname) 通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。
File(String parent, String child) 从父路径名字符串和子路径名字符串创建新的 File实例。
File(URI uri) 通过将给定的 file: URI转换为抽象路径名来创建新的 File实例

File对象代表硬盘中实际存在的一个文件或者目录

File类的构造方法不会给你检验该文件是否或者文件夹是否真实存在,因此无论如何该路径下是否存在文件或者目录,都不影响File对象的创建

public class FileStu {
    public static void main(String[] args) {
        String path = "D:\\";
        String fileName = "List.txt";

        File file1 = new File("D:\\List.txt");

        File file2 = new File(path,fileName);

        File parenDir = new File("D:\\");
        String child = "List.txt";
        File file3 = new File(parenDir,child);


    }
}

常用方法:

public String getPath() : 将File转换为路径名的字符串

public String getName():返回由此File表示的文件或者目录名称

public long length():返回File表示的

public String getAbsolutePath():返回此File的绝对路径名字符号

public boolean exists() : 此File表示的文件或者目录是否实际存在。

public boolean isDirectory(): 此File表示的是否为目录

public boolean isFile() :此File表示的是否文件

public boolean createNewFile(): 文件不存在则创建一个空的文件并返回true,文件存在,不创建文件并返回false

public boolean delete() : 删除由此File表示的文件或者目录

public boolean mkdir():创建由此File表示的目录

public boolean mkdirs():创建由此File表示的目录,包括任何必需但是不存在的父目录

注:mkdirs()与mkdir()方法类似,但是mkdir(),只能创建一级目录,mkdirs()则是可以创建多级目录,比如stu/lsit//a//b,所以开发中常用一般使用mkdirs()

public class FileStu01 {
    public static void main(String[] args) throws IOException {
        File file = new File("D:\\List.txt");
        System.out.println(file.getAbsoluteFile());
        System.out.println(file.getPath());
        System.out.println(file.getName());
        System.out.println(file.length());
        System.out.println(file.exists());
        System.out.println(file.isDirectory());
        System.out.println(file.isFile());

        //文件创建
        File f= new File("aaa.txt");
        System.out.println(f.exists());    //false
        System.out.println(f.createNewFile());  //抛异常  true
        System.out.println(f.createNewFile()); //已经创建 ,返回false
        System.out.println(f.exists());  //true

        //目录创建
        File f1 = new File("newDir");
        System.out.println(f1.exists());   //false
        System.out.println(f1.mkdir());     //true
        System.out.println(f1.exists());    //true

        //创建多级目录
        File f2 = new File("newDira\\newDirb");
        System.out.println(f2.mkdir());    //false
        File f3 = new File("newDira\\newDirb");
        System.out.println(f3.mkdir());   //true

        //文件的删除
        System.out.println(f.delete());//true

        //目录的删除
        System.out.println(f1.delete());  //true
        System.out.println(f3.delete());   //false
        //delete方法,如果此File表示目录,则目录必需为空才能删除
    }
}

目录的遍历

public String[] list():返回一个String数组,表示该File目录中的所有子文件或者目录

public File[] listFiles() :返回一个File数组,表示该File目录的所有的子文件或者目录

public class FileStu02 {
    public static void main(String[] args) {
        File dir  = new File("D:\\java");

        //获取当前目录下的文件夹以及文件夹的名称
        String[] names = dir.list();
        for(String name:names){
            System.out.println(name);
        }
        System.out.println("----------------");
        //获取当前目录下的文件以及文件夹对象
        File[] files  = dir.listFiles();
        for(File file:files){
            System.out.println(file);
        }
    }
}

bin
COPYRIGHT
jdk api 1.8.CHM
jdk api 1.8.chw
JDK1.6 API帮助文档.CHM
jdk1.8.0_101
jdk1.8.0_101.zip
lib
LICENSE
README.txt
release
THIRDPARTYLICENSEREADME-JAVAFX.txt
THIRDPARTYLICENSEREADME.txt
Welcome.html
使用说明 .txt
----------------
D:\java\bin
D:\java\COPYRIGHT
D:\java\jdk api 1.8.CHM
D:\java\jdk api 1.8.chw
D:\java\JDK1.6 API帮助文档.CHM
D:\java\jdk1.8.0_101
D:\java\jdk1.8.0_101.zip
D:\java\lib
D:\java\LICENSE
D:\java\README.txt
D:\java\release
D:\java\THIRDPARTYLICENSEREADME-JAVAFX.txt
D:\java\THIRDPARTYLICENSEREADME.txt
D:\java\Welcome.html
D:\java\使用说明 .txt

listFiles 在获取指定目录下的文件或者文件夹时必需满足下面的两个条件

  • 指定的目录存在
  • 指定的必须是目录,否则容易引发返回数组为null,出现NullPointerException异常

递归遍历文件夹所有的文件以及子文件

public static void Recursion(File file){
    //1、判断传入的是否是目录
    if(!file.isDirectory()){
        //不是目录直接退出
        return;
    }
    //已经确保了传入的file是目录
    File[] files = file.listFiles();
    //遍历files
    for (File f: files) {
        //如果该目录下文件还是个文件夹就再进行递归遍历其子目录
        if(f.isDirectory()){
            //递归
            Recursion(f);
        }else {
            //如果该目录下文件是个文件,则打印对应的名字
            System.out.println(f.getName());
        }

    }
}

IO学习

在Java中,I/O操作主要是指使用java.io包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据

IO流简称流,目的在于永久性保存数据对象

  • 按流的方向分为输入输出流

    1. 输入流:读取数据,从数据源(源:磁盘,鼠标,键盘,网路等)到当前程序

      基类:InputStream Reader

    2. 输出流:写入数据,从当前程序到目的源(目的源:磁盘、网络通信、显示器等)

      基类:OutputStream Writer

  • 按数据的类型分为字节流字符流

    1. 字节流:机器可以识别的二进制文件

      基类:InputStream 、OutputStream(一般形如XXXStream为字节流)

    2. 字符流:人眼可以识别的

      基类:Reader 、Writer(一般形如XXXReader XXXWriter为字符流)

      字节流字符流的区别:

    3. 读写的字节数不同

      • 字符流处理的单元是2个字节的Unicode字符
      • 字节流处理的单元为1个字节
    4. 字符流是快读写,字节流是字节读写

    5. 字符流带有缓冲,字节流不带有。

    6. 属性范围不同

      • 字节流属性范围大,字节流中可以是字符、二进制文件、音频、图像等各种各样的类型
      • 字符流属性范围小,字符流只能是字符
  • 按流的角色分为节点流和处理流

    1. 节点流:可以从向特定的IO设备(磁盘,网络)读写数据的流,也成为低级流
    2. 处理流:用于对一个已存在的流进行连接或则封装,通过封装后的流来实现数据读写功能,也成为高级流

InputStream/OutputStream

我们必须明确一点的是,一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。

OutputStream

java.io.OutputStream抽象类是表示字节输出流的所有类的超类(父类),将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。

基本共性功能方法

1、 public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
2、 public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
3、 public void write(byte[] b):将 b.length个字节从指定的字节数组写入此输出流。
4、 public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。 也就是说从off个字节数开始读取一直到len个字节结束
5、 public abstract void write(int b) :将指定的字节输出流。
(以上五个方法则是字节输出流都具有的方法,由父类OutputStream定义提供,子类都会共享以上方法)

FileOutputStream类

OutputStream有很多子类,我们从最简单的一个子类FileOutputStream开始。看名字就知道是文件输出流,用于将数据写出到文件。

  1. FileOutputStream构造方法
  • public FileOutputStream(File file):根据File对象为参数创建对象。
  • public FileOutputStream(String name): 根据名称字符串为参数创建对象。推荐第二种构造方法(开发常用)
  1. 创建字节输出流对象

    FileOutputStream outputStream = new FileOutputStream(“abc.txt”);
    就以上面这句代码来讲,类似这样创建字节输出流对象都做了三件事情:
    1、调用系统功能去创建文件【输出流对象才会自动创建】
    2、创建outputStream对象
    3、把foutputStream对象指向这个文件

    注意:创建输出流对象的时候,系统会自动去对应位置创建对应文件,而创建输入流对象的时候,文件不存在则会报FileNotFoundException异常,也就是系统找不到指定的文件异常。

    当你创建一个流对象时,必须直接或者间接传入一个文件路径。比如现在我们创建一个FileOutputStream流对象,在该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。如果不想对该文件进行清空,即进行追加操作,可设置对象参数进行实现。

    具体代码如下:

    public class StreamStu {
        public static void main(String[] args) throws IOException {
            FileOutputStream fileOutputStream= new FileOutputStream("abc.txt");//不存在文件,自动创建abc.txt
           // FileInputStream fileInputStream = new FileInputStream("acc.txt"); //报错:FileNotFoundException,acc.txt (系统找不到指定的文件。)
            fileOutputStream.write('g');
            FileInputStream fileInputStream = new FileInputStream("abc.txt");
            System.out.println(fileInputStream.read());  //103
            fileOutputStream.close();
            fileInputStream.close();
    
    //        FileOutputStream fileOutputStream1= new FileOutputStream("abc.txt");  //存在文件,不创建,但是清空数据。
    //        fileOutputStream1.close();
    //        FileInputStream fileInputStream1 = new FileInputStream("abc.txt");
    //        System.out.println(fileInputStream1.read());   // -1 数据被清空
    //        fileOutputStream.close();
    //        fileInputStream.close();
    
            //追加操作
            FileOutputStream fileOutputStream2= new FileOutputStream("abc.txt",true);  //存在文件,不创建,不清空数据。默认false,改为true
            fileOutputStream2.close();
            FileInputStream fileInputStream2 = new FileInputStream("abc.txt");
            System.out.println(fileInputStream2.read());   // 103
            fileOutputStream.close();
            fileInputStream.close();
    
        }
    }
    
  2. FileOutputStream写出字节数据

    使用FileOutputStream写出字节数据主要通过write方法,而write方法分如下三种

    public void write(int b)
    public void write(byte[] b)
    public void write(byte[] b,int off,int len) //从off索引开始,len个字节

    1. 写出字节:write(int b) 方法,每次可以写出一个字节数据,
      然参数为int类型四个字节,但是只会保留一个字节的信息写出。
    2. 写出字节数组:write(byte[] b),每次可以写出数组中的数据
    3. 写出指定长度字节数组:write(byte[] b, int off, int len) ,每次写出从off索引开始,len个字节,

    代码如下:

    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    
    public class StreamStu01 {
        public static void main(String[] args) throws IOException {
            FileOutputStream f1 = new FileOutputStream("ddd.txt",true);
            //字节输出
            f1.write(97);
            f1.write(98);
            f1.write(99);       
            //输出abc
            byte[] bytes = "你好,很高兴认识你".getBytes(StandardCharsets.UTF_8);
            f1.write(bytes);
            //输出abc你好,很高兴认识你
            byte[] bytes1 = "abcd".getBytes();
            f1.write(bytes,2,2);
            //写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
            f1.close();
            //输出abc你好,很高兴认识你cd
        }
    }
    
    
    1. FileOutputStream实现数据追加续写、换行
      经过以上的代码测试,每次程序运行,每次创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续追加新数据呢?并且实现换行呢?其实很简单,这个时候我们又要再学习FileOutputStream的另外两个构造方法了,如下:

      1、public FileOutputStream(File file, boolean append)

      2、public FileOutputStream(String name, boolean append)

      这两个构造方法,第二个参数中都需要传入一个boolean类型的值,true 表示追加数据,false 表示不追加也就是清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了,至于Windows换行则是 \n\r ,下面将会详细讲到。

      代码如下:

      public class FOSWrite {
      public static void main(String[] args) throws IOException {
      // 使用文件名称创建流对象
      FileOutputStream fos = new FileOutputStream(“fos.txt”,true);
      // 定义字节数组
      byte[] words = {97,98,99,100,101};
      // 遍历数组
      for (int i = 0; i < words.length; i++) {
      // 写出一个字节
      fos.write(words[i]);
      // 写出一个换行, 换行符号转成数组写出
      fos.write("\r\n".getBytes());
      }
      // 关闭资源
      fos.close();
      }
      }

回车符\r和换行符\n :
回车符:回到一行的开头(return)。
换行符:下一行(newline)。
系统中的换行:
Windows系统里,每行结尾是 回车+换行 ,即\r\n;
Unix系统里,每行结尾只有 换行 ,即\n;
Mac系统里,每行结尾是 回车 ,即\r。从 Mac OS X开始与Linux统一。

InputStream

java.io.InputStream抽象类是表示字节输入流的所有类的超类(父类),可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

  1. 字节输入流的基本共性功能方法:

    1、 public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
    2、public abstract int read(): 从输入流读取数据的下一个字节。

    3、 public int read(byte[] b): 该方法返回的int值代表的是读取了多少个字节,读到几个返回几个,读取不到返回-1

  2. FileInputStream类
    java.io.FileInputStream类是文件输入流,从文件中读取字节。

  3. FileInputStream的构造方法
    1、 FileInputStream(File file): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。
    2、 FileInputStream(String name): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名name命名。同样的,推荐使用第二种构造方法。

  4. 创建FileInputStream对象

FileInputStream inputStream = new FileInputStream(“a.txt”);
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,不会自动创建文件,会抛出FileNotFoundException 。

  1. FileInputStream读取字节数据(重点)
    读取字节:read方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回-1,代码如下

    public class StreamStu02 {
        public static void main(String[] args) throws FileNotFoundException {
            FileInputStream f1 = new FileInputStream("abc.txt");
            try{
                System.out.println((char)f1.read());
                System.out.println((char)f1.read());
                System.out.println((char)f1.read());
                System.out.println((char)f1.read());
                System.out.println((char)f1.read());
                System.out.println((char)f1.read());
                System.out.println((char)f1.read());
            }catch(IOException e){
                e.printStackTrace();
            }finally{
                try{f1.close();}catch(Exception e){}
            }
        }
    }
    

输出:

a
b
c
d
e
f
g

改进读取方式(循环读取)

import java.io.FileInputStream;
public class StreamStu03 {
    public static void main(String[] args) {
        try{
            FileInputStream f = new FileInputStream("abc.txt");
            while (f.read()!= -1){
                System.out.println((char)f.read());
            }
            f.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
//?????输出结果:
//b
//d
//f
//
import java.io.FileInputStream;

public class StreamStu03 {
    public static void main(String[] args) {
        int b=0;
        try{
            FileInputStream f = new FileInputStream("abc.txt");
            while ((b=f.read())!= -1){
                System.out.println((char)b);
            }
            f.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
//输出
//a
//b
//c
//d
//e
//f
//g
  1. 使用字节数组读取 , read(byte[] b),每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回-1

    import java.io.FileInputStream;
    
    public class StreamStu04 {
        public static void main(String[] args) {
            FileInputStream f = null;
            int len = 0;
            try{
                f = new FileInputStream("abc.txt");
                byte[] bytes = new byte[2];   //两个字节,用于装字节数据的容器
                while ((len = f.read(bytes)) != -1){
                    System.out.print(new String(bytes)+"\t");
                }
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                 try{f.close();}catch(Exception e){}
            }
    
        }
    }
    

输出结果:ab cd ef gf

由于abc.txt文件中内容为abcdefg,而错误数据g不应该出现,但是是由于最后一次读取时,只有一个字节f,但是又需要读取两个字节,故替换了前一次的数据ef中的e,f没有被替换,保留下来了。

第一次:ab 第二次 ab都替换为cd 第三次 cd都替换为ef 第四次只能替换e(数据不够),f保留,为gf

改进代码如下:

import java.io.FileInputStream;

public class StreamStu05 {
    public static void main(String[] args) {
        FileInputStream f = null;
        int len = 0;
        try{
            f = new FileInputStream("abc.txt");
            byte[] bytes = new byte[2];
            while((len = f.read(bytes))!=-1){
                System.out.print(new String(bytes,0,len)+"\t");    len 每次读取的有效字节个数
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            try{f.close();}catch(Exception e){}
        }
    }
}

输出:ab cd ef g

在开发中一般强烈推荐使用数组读取文件,代码如下:

import java.io.FileInputStream;

public class StreamStu05 {
    public static void main(String[] args) {
        FileInputStream f = null;
        int len = 0;
        try{
            f = new FileInputStream("abc.txt");
            byte[] bytes = new byte[1024];
            while((len = f.read(bytes))!=-1){
                System.out.print(new String(bytes,0,len));    len 每次读取的有效字节个数
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            try{f.close();}catch(Exception e){}
        }
    }
}

字节流FileInputstream复制图片

代码实现

import java.io.FileInputStream;
import java.io.FileOutputStream;
//复制图像
public class StreamStu06 {
    public static void main(String[] args) {
        FileInputStream fin = null;
        FileOutputStream fout = null;
        int len = 0;
        byte[] bytes;
        try{
            fin = new FileInputStream("D://SnipasteImg/test.png");
            fout = new FileOutputStream("test1.png");
            bytes = new byte[1024];
            while ((len = fin.read(bytes))!=-1){
                fout.write(bytes,0,len);
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            try{
                fin.close();
                fout.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}

注:复制文本、图片、mp3、视频等的方式一样。

字符流Reader和Writer

字符流的由来:因为数据编码的不同,因而有了对字符进行高效操作的流对象,字符流本质其实就是基于字节流读取时,去查了指定的码表,而字节流直接读取数据会有乱码的问题(读中文会乱码),简单来说就是字符流 = 字节流 + 编码表

字节流读取中文字符时,可能不会显示完整的字符,那是因为一个中文字符占用多个字节存储,造成乱码情况,字节流虽然也可以通过一些方法防止乱码,但是较为复杂,如果处理纯文本的数据优先考虑字符流

字节流防止中文乱码(方式一):

import java.io.FileInputStream;

public class StreamStu05 {
    public static void main(String[] args) {
        FileInputStream f = null;
        int len = 0;
        try{
            f = new FileInputStream("abc.txt");
            byte[] bytes = new byte[1024];
            while((len = f.read(bytes))!=-1){
                System.out.print(new String(bytes,0,len));    len 每次读取的有效字节个数,也防止读取乱码
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            try{f.close();}catch(Exception e){}
        }
    }
}

原因:解码的是String,查看new String()的源码,String构造方法有解码功能,并且默认编码是utf-8,代码如下:

this.value = StringCoding.decode(bytes, offset, length);

再点进decode,循序渐进发现,默认编码是UTF-8,因此不会发生乱码行为

字节流防止中文乱码(方式二):

public class StreamStu01 {
    public static void main(String[] args) throws IOException {
        FileOutputStream f1 = new FileOutputStream("ddd.txt",true);
        //字节输出
        f1.write(97);
        f1.write(98);
        f1.write(99);       
        //输出abc
        byte[] bytes = "你好,很高兴认识你".getBytes(StandardCharsets.UTF_8);
        f1.write(bytes);
        //输出abc你好,很高兴认识你
        byte[] bytes1 = "abcd".getBytes();
        f1.write(bytes,2,2);
        //写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
        f1.close();
        //输出abc你好,很高兴认识你cd
    }
}

尽管字节流也能有办法决绝乱码问题,但是还是比较麻烦,于是java就有了字符流,字符为单位读写数据,字符流专门用于处理文本文件。如果处理纯文本的数据优先考虑字符流,其他情况就只能用字节流了(图片、视频、等等只文本例外)。

Reader

已知直接子类: BufferedReader , CharArrayReader , FilterReader , InputStreamReader, PipedReader, StringReader

用于读取字符流的抽象类。 子类必须实现的唯一方法是read(char [],int,int)和close()。 然而,大多数子类将覆盖这里定义的一些方法,以便提供更高的效率,附加的功能或两者。

字符输入流的共性方法:

1、public void close() :关闭此流并释放与此流相关联的任何系统资源。
2、 public int read(): 从输入流读取一个字符。
3、 public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中

FileReader类
java.io.FileReader类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

  • 构造方法
    1、FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
    2、 FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的字符串名称。

  • 读取字符

    read方法,每次可以读取一个字符的数据,提升为int类型,读取到文件末尾,返回-1,循环读取,

    import java.io.FileReader;
    
    public class ReaderStu01 {
        public static void main(String[] args) {
            FileReader fr = null;
            int len = 0;
            try{
                fr = new FileReader("bbb.txt");
    
                while((len=fr.read()) != -1){
                    System.out.print((char)len);
                }
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                try{fr.close();}catch (Exception e){e.printStackTrace();}
            }
        }
    }
    

​ 至于读取的写法类似字节流的写法,只是读取单位不同罢了。

Writer

java.io.Writer抽象类是字符输出流的所有类的超类(父类),将指定的字符信息写出到目的地。它同样定义了字符输出流的基本共性功能方法。

已知直接子类: BufferedWriter, CharArrayWriter , FilterWriter, OutputStreamWriter, PipedWriter, PrintWriter, StringWriter

用于写入字符流的抽象类。 子类必须实现的唯一方法是write(char [],int,int),flush()和close()。 然而,大多数子类将覆盖这里定义的一些方法,以便提供更高的效率,附加的功能或两者

  • 字符输出流的基本共性功能方法

    1、void write(int c) 写入单个字符。
    2、void write(char[] cbuf)写入字符数组。
    3、 abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
    4、 void write(String str)写入字符串。
    5、void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。
    6、void flush()刷新该流的缓冲。
    7、void close() 关闭此流,但要先刷新它。

  • FileWriter类
    java.io.FileWriter类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

    构造方法
    1、 FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。
    2、FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。

    FileWriter写出数据
    写出字符:write(int b) 方法,每次可以写出一个字符数据,(注意:关闭资源时,与FileOutputStream不同。 如果不关闭,数据只是保存到缓冲区,并未保存到文件。) 代码如下

    import java.io.FileWriter;
    
    public class WriterStu {
        public static void main(String[] args) {
            FileWriter fw = null;
            try{
                fw = new FileWriter("fff.txt");
                fw.write("你好");
    
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                try{
                    fw.close();
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
    

    关闭close和刷新flush
    因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush 方法了。

    • flush :刷新缓冲区,流对象可以继续使用。

    • close:先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。

      
      import java.io.FileWriter;
      
      public class WriterStu {
          public static void main(String[] args) {
              FileWriter fw = null;
              try{
                  fw = new FileWriter("fff.txt");
                  fw.write("该数据在缓冲区");
                  fw.flush();  //通过flush将缓冲区的数据强行写入当文本,没有此方法,文本为空,没有数据
      
              }catch(Exception e){
                  e.printStackTrace();
              }finally{
                  try{
                      //fw.close();
                  }catch(Exception e){
                      e.printStackTrace();
                  }
              }
          }
      }
      

      注意这里是没有使用close关闭流,开发中不能这样做,但是为了更好的体会flush的作用,如果不关闭,数据只是保存到缓冲区,并未保存到文件。所 以,在以上的代码中再添加下面三句代码,就完美了,文件就能复制到源文件的数据了!

      fr.close();
      fw.flush();
      fw.close();
      flush()这个函数是清空的意思,用于清空缓冲区的数据流,进行流的操作时,数据先被读到内存中,然后再用数据写到文件中,那么当你数据读完时,我们如果这时调用close()方法关闭读写流,这时就可能造成数据丢失,为什么呢?因为,读入数据完成时不代表写入数据完成,一部分数据可能会留在缓存区中,这个时候flush()方法就格外重要了。

      即便是flush方法写出了数据,操作的最后还是要调用close方法,释放系统资源。

FileWriter的续写和换行
​ 续写和换行:操作类似于FileOutputStream操作

FileReader和FileWriter类完成文本文件复制

import java.io.FileWriter;

public class WriterStu {
    public static void main(String[] args) {
        FileWriter fw = null;
        try{
            fw = new FileWriter("fff.txt");
            fw.write("你好");

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

最后再次强调:
字符流,只能操作文本文件,不能操作图片,视频等非文本文件。当我们单纯读或者写文本文件时 使用字符流 其他情况使用字节流

IO异常的处理
我们在学习的过程中可能习惯把异常抛出,而实际开发中并不能这样处理,建议使用try…catch…finally 代码块,处理异常部分

过滤流

过滤流有FilterInputStream和FilterOutputStream,分别是过滤输入流和过滤输出流,他们的作用是为基础流提供一些额外的功能,

FilterInputStream常见子类

  • DataInputStream,数据输入流,以机器无关的方式读取Java的基本类型.
  • BufferedInputStream缓冲输入流,由于基础输入流一个字节一个字节读取,频繁与磁盘进行交互,造成读取速度较低.缓冲流的存在就是先将数据读取到缓冲流(内存中),然后一次性从内存中读取多个字符.提高读取的效率.
  • PushInputStream回退输入流,java中读取数据的方式是顺序读取,如果某个数据不需要读取,需要程序处理.PushBackInputStream就可以将某些不需要的数据回退到缓冲中。

FilterInputStream所有的子类都是为基础流输入提供了一些额外的功能,为什么不直接继承基础流,将基础流中的方法直进行"装饰",这里要说到一个装饰者模式和继承的区别。

装饰者模式和继承的区别

装饰者模式:就是将原有的基础流进行"装饰",那么装饰后的方法要与原先被装饰的基础类要保持一致,也可以在对基础流进行扩展,而继承是继承父类的属性和方法,通过重写父类里面的方法也可以起到"装饰"作用.比如强化或者优化父类里面的一些方法.两者的区别是装饰者模式可以动态地扩展一个对象.给对象添加额外的功能.而且装饰者和被装饰者之间不会产生耦合.

先看一下继承,例如要实现DataInputStream的功能,我们需要继承每个基础输入流,FileInputStream所有子类以及InputStream子类等等类,都需要继承,那么这些类就会爆炸式的增长.而且类之间耦合性特别高.如果使用继承,那么IO流系统何其庞大。
那么装饰者模式存在意义就在此处,相比继承,没有这么多繁杂的类,而且类与类的之间的耦合性降低,具体做法就是将提出一个类FilterInputStream.而其子类就是各个功能的实现类.如果想要基础输入流要某个功能,那么就可以将对应的基础输入流传到对应的子类构造方法中.代码也是来源于生活,这个装饰者模式跟生活中很多实例相似。

FilterOutputStream子类

  • DataOutputStream 数据输出流,以机器无关的方式写入Java的基本类型.
  • BufferedOutputStream 缓冲输出流,由于基础输出流一个字节一个字节写入,频繁与磁盘进行交互,造成写入速度较低。缓冲流的存在就是先将数据写入到缓冲流(内存中),然后一次性从内存中写入多个字符.提高读取的效率.
  • PrintStream打印流,在控制台打印输出,是调用print方法和println方法完成的,输出语句这两个方法都来自于java.io.PrintStream类

缓冲流

首先我们来认识认识一下缓冲流,也叫高效流,是对4个FileXxx 流的“增强流”。

缓冲流的基本原理:

1、使用了底层流对象从具体设备上获取数据,并将数据存储到缓冲区的数组内。
2、通过缓冲区的read()方法从缓冲区获取具体的字符数据,这样就提高了效率。
3、如果用read方法读取字符数据,并存储到另一个容器中,直到读取到了换行符时,将另一个容器临时存储的数据转成字符串返回,就形成了readLine()功能。

也就是说在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

缓冲书写格式为BufferedXxx,按照数据类型分类:

字节缓冲流:BufferedInputStream,BufferedOutputStream
字符缓冲流:BufferedReader,BufferedWriter

字节缓冲流

是FilterInputStream过滤输入流的子类

构造方法
public BufferedInputStream(InputStream in) :创建一个新的缓冲输入流,注意参数类型为InputStream。
public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流,注意参数类型为OutputStream。
构造举例代码如下:

//构造方式一: 创建字节缓冲输入流【但是开发中一般常用下面的格式申明】
FileInputStream fps = new FileInputStream("b.txt");
BufferedInputStream bis = new BufferedInputStream(fps)

//构造方式二: 
//创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("b.txt"));

//创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt"));

感受缓冲流的高效

缓冲流读写方法与基本的流是一致的,我们通过复制370多MB的大文件,测试它的效率。

基本流,代码如下:
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
FileInputStream fis = new FileInputStream(“py.exe”);//exe文件够大
FileOutputStream fos = new FileOutputStream(“copyPy.exe”)
){
// 读写数据
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println(“普通流复制时间:”+(end - start)+" 毫秒");
}
}

缓冲流,代码如下:
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(“py.exe”));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(“copyPy.exe”));
){
// 读写数据
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println(“缓冲流复制时间:”+(end - start)+" 毫秒");
}
}

缓冲流复制时间:8016 毫秒

有的童鞋就要说了,我要更快的速度!最近看速度与激情7有点上头,能不能再快些?答案是当然可以

想要更快可以使用数组的方式,代码如下:

public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(“py.exe”));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(“copyPy.exe”));
){
// 读写数据
int len;
byte[] bytes = new byte[8*1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0 , len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println(“缓冲流使用数组复制时间:”+(end - start)+" 毫秒");
}
}
缓冲流使用数组复制时间:521 毫秒

字符缓冲流

构造方法
相同的来看看其构造,其格式以及原理和字节缓冲流是一样一样的!

public BufferedReader(Reader in) :创建一个新的缓冲输入流,注意参数类型为Reader。
public BufferedWriter(Writer out): 创建一个新的缓冲输出流,注意参数类型为Writer。
构造举例,代码如下:

// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader(“b.txt”));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter(“b.txt”));

字符缓冲流特有方法
字符缓冲流的基本方法与普通字符流调用方式一致,这里不再阐述,我们来看字符缓冲流具备的特有方法。

BufferedReader:public String readLine(): 读一行数据。 读取到最后返回null
BufferedWriter:public void newLine(): 换行,由系统属性定义符号。
readLine方法演示代码如下:

public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader(“a.txt”));
// 定义字符串,保存读取的一行文字
String line = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.print(line);
System.out.println("------");
}
// 释放资源
br.close();
}
}

newLine方法演示代码如下:

public class BufferedWriterDemo throws IOException {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedWriter bw = new BufferedWriter(new FileWriter(“b.txt”));
// 写出数据
bw.write(“哥”);
// 写出换行
bw.newLine();
bw.write(“敢”);
bw.newLine();
bw.write(“摸屎”);
bw.newLine();
bw.write(“你敢吗?”);
bw.newLine();
// 释放资源
bw.close();
}
}
输出效果:


摸屎
你敢吗?
1.4 字符缓冲流练习
字符缓冲流练习啥捏?先放松一下吧各位,先欣赏欣赏我写的下面的诗篇

6.你说你的程序叫简单,我说我的代码叫诗篇
1.一想到你我就哦豁豁豁豁豁豁豁豁豁豁…哦nima个头啊,完全不理人家受得了受不了
8.Just 简单你和我 ,Just 简单程序员
3.约了地点却忘了见面 ,懂得寂寞才明白浩瀚
5.沉默是最大的发言权
2.总是喜欢坐在电脑前, 总是喜欢工作到很晚
7.向左走 又向右走,我们转了好多的弯
4.你从来就不问我,你还是不是那个程序员

欣赏完了咩?没错咋们就练习如何使用缓冲流的技术把上面的诗篇归顺序,都编过号了就是前面的1到8的编号

分析:首先用字符输入缓冲流创建个源,里面放没有排过序的文字,之后用字符输出缓冲流创建个目标接收,排序的过程就要自己写方法了哦,可以从每条诗词的共同点“.”符号下手!

代码实现
public class BufferedTest {
public static void main(String[] args) throws IOException {
// 创建map集合,保存文本数据,键为序号,值为文字
HashMap lineMap = new HashMap<>();

    // 创建流对象  源
    BufferedReader br = new BufferedReader(new FileReader("a.txt"));
    //目标
    BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));

    // 读取数据
    String line  = null;
    while ((line = br.readLine())!=null) {
        // 解析文本
        String[] split = line.split("\\.");
        // 保存到集合
        lineMap.put(split[0],split[1]);
    }
    // 释放资源
    br.close();

    // 遍历map集合
    for (int i = 1; i <= lineMap.size(); i++) {
        String key = String.valueOf(i);
        // 获取map中文本
        String value = lineMap.get(key);
      	// 写出拼接文本
        bw.write(key+"."+value);
      	// 写出换行
        bw.newLine();
    }
	// 释放资源
    bw.close();
}

}

运行效果

1.一想到你我就哦豁豁豁豁豁豁豁豁豁豁…哦nima个头啊,完全不理人家受得了受不了
2.总是喜欢坐在电脑前, 总是喜欢工作到很晚
3.约了地点却忘了见面 ,懂得寂寞才明白浩瀚
4.你从来就不问我,你还是不是那个程序员
5.沉默是最大的发言权
6.你说你的程序叫简单,我说我的代码叫诗篇
7.向左走 又向右走,我们转了好多的弯
8.Just 简单你和我 ,Just 简单程序员

转换流

何谓转换流?为何由来?暂时带着问题让我们先来了解了解字符编码和字符集!

2.1 字符编码与解码
众所周知,计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

简单一点的说就是:

编码:字符(能看懂的)–字节(看不懂的)

解码:字节(看不懂的)–>字符(能看懂的)

代码解释则是

String(byte[] bytes, String charsetName):通过指定的字符集解码字节数组
byte[] getBytes(String charsetName):使用指定的字符集合把字符串编码为字节数组

编码:把看得懂的变成看不懂的
String – byte[]

解码:把看不懂的变成看得懂的
byte[] – String

字符编码 Character Encoding: 就是一套自然语言的字符与二进制数之间的对应规则。

而编码表则是生活中文字和计算机中二进制的对应规则

字符集

IO流详解_第2张图片:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。
计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。

可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。

ASCII字符集
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。
ISO-8859-1字符集:
拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。
ISO-8859-1使用单字节编码,兼容ASCII编码。
GBxxx字符集
GB就是国标的意思,是为了显示中文而设计的一套字符集。
GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。
GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。
Unicode字符集
Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。
它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。
UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:
128个US-ASCII字符,只需一个字节编码。
拉丁文等字符,需要二个字节编码。
大部分常用字(含中文),使用三个字节编码。
其他极少使用的Unicode辅助字符,使用四字节编码。
2.2 编码问题导致乱码
在java开发工具IDEA中,使用FileReader 读取项目中的文本文件。由于IDEA的设置,都是默认的UTF-8编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统的默认是GBK编码,就会出现乱码。

public class ReaderDemo {
public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader(“C:\a.txt”);
int read;
while ((read = fileReader.read()) != -1) {
System.out.print((char)read);
}
fileReader.close();
}
}
输出结果:���

那么如何读取GBK编码的文件呢? 这个时候就得讲讲转换流了!

从另一角度来讲:字符流=字节流+编码表

InputStreamReader类-----(字节流到字符流的桥梁)

转换流java.io.InputStreamReader,是Reader的子类,从字面意思可以看出它是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法
InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。

构造代码如下:

InputStreamReader isr = new InputStreamReader(new FileInputStream(“in.txt”));
InputStreamReader isr2 = new InputStreamReader(new FileInputStream(“in.txt”) , “GBK”);

使用转换流解决编码问题

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class InputStreamReader_stu {
    public static void main(String[] args) throws IOException {
        //字节流转化为字符流
        FileInputStream f = null;
        InputStreamReader fto = null;
        int read = 0;
        try{
            f = new FileInputStream("bbb.txt");
           // fto = new InputStreamReader(f,"GBK");   //乱码
            fto = new InputStreamReader(f,"UTF8");    //不乱码
            //读取字节,解码为字符
            while((read=fto.read())!= -1){
                System.out.println((char)read);
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            try{
                if(f!=null) f.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }

    }
}

OutputStreamWriter类-----(字符流到字节流的桥梁)

转换流java.io.OutputStreamWriter ,是Writer的子类,字面看容易混淆会误以为是转为字符流,其实不然,OutputStreamWriter为从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法
OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。

构造举例,代码如下:

OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream(“a.txt”));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream(“b.txt”) , “GBK”);

指定编码构造代码

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class OutputStreamWriter_stu {
    public static void main(String[] args) {
        //字符流转化为字节流
        OutputStreamWriter fto = null;
        try{
            fto = new OutputStreamWriter(new FileOutputStream("fff.txt"),"GBK"); //设置为GBK保存为4个字符
           // fto = new OutputStreamWriter(new FileOutputStream("fff.txt"),"UTF8"); //设置为UTF8保存为6个字符
            //写出字符,编码为字节
            fto.write("傻der");
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            try{
                if(fto!=null)fto.close();
            }catch(IOException e){
                e.printStackTrace();
            }

        }

    }
}

为了达到最高效率,可以考虑在 BufferedReader 内包装 InputStreamReader

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

序列化流

问题:当我们想把对象信息永久的存储时?该怎么办

(1)可以把对象写入文本文件或者在网络中传输
(2)如何实现序列化呢?
让被序列化的对象所属类实现序列化接口。该接口是一个标记接口,没有功能需要实现,但是必须实现该接口
(3)注意问题:
把数据写到文件后,在去修改类会产生一个问题。
如何解决该问题呢?
在类文件中,给出一个固定的序列化id值。
而且,这样也可以解决黄色警告线问题
(4)面试题:
什么时候序列化?
如何实现序列化?
什么是反序列化?

什么是序列化

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据、对象的类型和对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。

反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。

简而言之:

对象 ——>字节 序列化

字节 ——>对象 反序列化

初次体验:

import java.io.*;

public class SerialiazableStu implements Serializable {
    public static void main(String[] args) throws Exception{
        //序列化
        ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("ttt.txt"));
        TestSerialiable ts = new TestSerialiable();
        obj.writeObject(ts);
        //反序列化
        int read;
        ObjectInputStream fobj = new ObjectInputStream(new FileInputStream("ttt.txt"));
        //读取一个对象
        TestSerialiable ob = (TestSerialiable)fobj.readObject();
        System.out.println(ob.name);
        ob.action();

    }
}
class TestSerialiable  implements Serializable{
    String name = "张三";
    int age = 18;
    String address = "中国";
    transient String text = "没有被序列化的变量";
    boolean sex = true;  // 0:女  1:男
    public void action(){
        System.out.println("跑步");
    }
    public static void test(){
        System.out.println("不想被序列化的方法");
    }

}

序列化的优点

  1. 将对象转换为字节流存储在硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)

  2. 序列化成字节流形式的对象可以进行网络 传输(二进制形式),方便网络传输

  3. 通过序列化可以在进程间传递对象

    简而言之:永久保存对象,使对象在网络中传递,使对象在进程中传递

ObjectOutputStream类(对象输出流)

java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

构造方法
public ObjectOutputStream(OutputStream out): 创建一个指定OutputStream的ObjectOutputStream。

构造代码如下:

FileOutputStream fileOut = new FileOutputStream(“aa.txt”);
ObjectOutputStream out = new ObjectOutputStream(fileOut);

序列化操作
一个对象要想序列化,必须满足两个条件:

  1. 该类必须实现java.io.Serializable 接口

    Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException

    该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。(注:static修饰的变量也是不可以序列化的,因为static是类)

  2. 写出对象方法

    public final void writeObject (Object obj) : 将指定的对象写出。

    ObjectInputStream类(对象输入流)
    ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

    构造方法
    public ObjectInputStream(InputStream in): 创建一个指定InputStream的ObjectInputStream。

public class Employee implements java.io.Serializable {
    public String name;
    public String address;
    public transient int age; // transient瞬态修饰成员,不会被序列化
    public void addressCheck() {
      	System.out.println("Address  check : " + name + " -- " + address);
    }
}
public class SerializeDemo{
   	public static void main(String [] args)   {
    	Employee e = new Employee();
    	e.name = "zhangsan";
    	e.address = "beiqinglu";
    	e.age = 20; 
    	try {
      		// 创建序列化流对象
          ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
        	// 写出对象
        	out.writeObject(e);
        	// 释放资源
        	out.close();
        	fileOut.close();
        	System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化。
        } catch(IOException i)   {
            i.printStackTrace();
        }
   	}
}

输出结果:
Serialized data is saved

反序列化操作1

如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream读取对象的方法:

public final Object readObject () : 读取一个对象。
public class DeserializeDemo {
   public static void main(String [] args)   {
        Employee e = null;
        try {		
             // 创建反序列化流
             FileInputStream fileIn = new FileInputStream("employee.txt");
             ObjectInputStream in = new ObjectInputStream(fileIn);
             // 读取一个对象
             e = (Employee) in.readObject();
             // 释放资源
             in.close();
             fileIn.close();
        }catch(IOException i) {
             // 捕获其他异常
             i.printStackTrace();
             return;
        }catch(ClassNotFoundException c)  {
        	// 捕获类找不到异常
             System.out.println("Employee class not found");
             c.printStackTrace();
             return;
        }
        // 无异常,直接打印输出
        System.out.println("Name: " + e.name);	// zhangsan
        System.out.println("Address: " + e.address); // beiqinglu
        System.out.println("age: " + e.age); // 0
    }
}

对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。

反序列化操作2

另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。发生这个异常的原因如下:

1、该类的序列版本号与从流中读取的类描述符的版本号不匹配
2、该类包含未知数据类型
2、该类没有可访问的无参数构造方法

Serializable 接口给需要序列化的类,提供了一个序列版本号。serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。

public class Employee implements java.io.Serializable {
     // 加入序列版本号
     private static final long serialVersionUID = 1L;
     public String name;
     public String address;
     // 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值.
     public int eid; 

     public void addressCheck() {
        System.out.println("Address  check : " + name + " -- " + address);
    }

}

3.4 序列化集合练习
将存有多个自定义对象的集合序列化操作,保存到list.txt文件中。
反序列化list.txt ,并遍历集合,打印对象信息。
案例分析
把若干学生对象 ,保存到集合中。
把集合序列化。
反序列化读取时,只需要读取一次,转换为集合类型。
遍历集合,可以打印所有的学生信息
案例代码实现

public class SerTest {
	public static void main(String[] args) throws Exception {
		// 创建 学生对象
		Student student = new Student("老王", "laow");
		Student student2 = new Student("老张", "laoz");
		Student student3 = new Student("老李", "laol");
	ArrayList arrayList = new ArrayList<>();
	arrayList.add(student);
	arrayList.add(student2);
	arrayList.add(student3);
	// 序列化操作
	// serializ(arrayList);
	
	// 反序列化  
	ObjectInputStream ois  = new ObjectInputStream(new FileInputStream("list.txt"));
	// 读取对象,强转为ArrayList类型
	ArrayList list  = (ArrayList)ois.readObject();
	
  	for (int i = 0; i < list.size(); i++ ){
      	Student s = list.get(i);
    	System.out.println(s.getName()+"--"+ s.getPwd());
  	}
}

private static void serializ(ArrayList arrayList) throws Exception {
	// 创建 序列化流 
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
	// 写出对象
	oos.writeObject(arrayList);
	// 释放资源
	oos.close();
}

}

打印流

4.1 何谓打印流
平时我们在控制台打印输出,是调用print方法和println方法完成的,各位用了这么久的输出语句肯定没想过这两个方法都来自于java.io.PrintStream类吧,哈哈。该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

打印流分类:

字节打印流PrintStream,字符打印流PrintWriter

打印流特点:

A:只操作目的地,不操作数据源
B:可以操作任意类型的数据
C:如果启用了自动刷新,在调用println()方法的时候,能够换行并刷新
D:可以直接操作文件

这个时候有人就要问了,哪些流可以直接操作文件呢?答案很简单,如果该流的构造方法能够同时接收File和String类型的参数,一般都是可以直接操作文件的!

PrintStream是OutputStream的子类,PrintWriter是Writer的子类,两者处于对等的位置上,所以它们的API是非常相似的。二者区别无非一个是字节打印流,一个是字符打印流。

4.2 字节输出打印流PrintStream复制文本文件

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;

public class PrintStreamDemo {
    public static void main(String[] args) throws IOException {
        BufferedReader br=new BufferedReader(new FileReader("copy.txt"));
        PrintStream ps=new PrintStream("printcopy.txt");
        String line;
        while((line=br.readLine())!=null) {
            ps.println(line);
        }
        br.close();
        ps.close();
    }
}

4.3 字符输出打印流PrintWriter复制文本文件

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
/**

 * 使用打印流复制文本文件
   */
   public class PrintWriterDemo {
   public static void main(String[] args) throws IOException {
       BufferedReader br=new BufferedReader(new FileReader("aa.txt"));
       PrintWriter pw=new PrintWriter("printcopyaa.txt");
       String line;
       while((line=br.readLine())!=null) {
           pw.println(line);
       }
       br.close();
       pw.close();
   }
   }
  • 5、Properties属性类
    我想各位对这个Properties类多多少少也接触过了,首先Properties类并不在IO包下,那为啥要和IO流一起讲呢?原因很简单因为properties类经常和io流的联合一起使用。

(1)是一个集合类,Hashtable的子类

(2)特有功能
A:public Object setProperty(String key,String value)
B:public String getProperty(String key)
C:public Set stringPropertyNames()

(3)和IO流结合的方法
把键值对形式的文本文件内容加载到集合中
public void load(Reader reader)
public void load(InputStream inStream)
把集合中的数据存储到文本文件中
public void store(Writer writer,String comments)
public void store(OutputStream out,String comments)

5.1 Properties概述
java.util.Properties 继承于Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties 方法就是返回一个Properties对象。

5.2 Properties类
构造方法
public Properties() :创建一个空的属性列表。

基本的存储方法

public Object setProperty(String key, String value) : 保存一对属性。
public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。
public Set<String> stringPropertyNames() :所有键的名称的集合。
public class ProDemo {
    public static void main(String[] args) throws FileNotFoundException {
        // 创建属性集对象
        Properties properties = new Properties();
        // 添加键值对元素
        properties.setProperty("filename", "a.txt");
        properties.setProperty("length", "209385038");
        properties.setProperty("location", "D:\\a.txt");
        // 打印属性集对象
        System.out.println(properties);
        // 通过键,获取属性值
        System.out.println(properties.getProperty("filename"));
        System.out.println(properties.getProperty("length"));
        System.out.println(properties.getProperty("location"));  
  // 遍历属性集,获取所有键的集合
    Set<String> strings = properties.stringPropertyNames();
    // 打印键值对
    for (String key : strings ) {
      	System.out.println(key+" -- "+properties.getProperty(key));
    }
}

}
输出结果:
{filename=a.txt, length=209385038, location=D:\a.txt}
a.txt
209385038
D:\a.txt
filename – a.txt
length – 209385038
location – D:\a.txt

与流相关的方法
public void load(InputStream inStream): 从字节输入流中读取键值对。

参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。现在文本数据格式如下:

filename=Properties.txt
length=123
location=C:\Properties.txt

加载代码演示:

public class ProDemo {
    public static void main(String[] args) throws FileNotFoundException {
        // 创建属性集对象
        Properties pro = new Properties();
        // 加载文本中信息到属性集
        pro.load(new FileInputStream("Properties.txt"));
        // 遍历集合并打印
        Set<String> strings = pro.stringPropertyNames();
        for (String key : strings ) {
          	System.out.println(key+" -- "+pro.getProperty(key));
        }
     }
}

输出结果:
filename – Properties.txt
length – 123
location – C:\Properties.txt

文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

你可能感兴趣的:(java,开发语言,后端)