Java IO流

一 IO流

用于处理设备上数据

流: 可以理解数据的流动 就是一个数据流 IO流最终要以对象来体现 对象都存在IO包中

1. 流的分类:

1> 输入流(读) 和输出流(写)

2> 因为处理的数据不同 分为字节流和字符流

2. 字节流

处理字节数据的流对象 设备上的数据无论是图片或者dvd, 文字, 它们都以二进制存储的 二进制的最终都是以一个8位为数据单元进行体现 所以计算机中的最小数据单元就是字节 意味着 字节流可以处理设备上的所有数据 所以字节流一样可以处理字符数据

3. 字符流

因为字符每个国家都不一样 所以涉及到了字符编码问题 那么GBK编码的中文用unicode编码解析是有问题的 所以需要获取中文字节数据的同时指定的编码表才可以解析正确数据 为了方便于文字的解析 所以将字节流和编码表封装成对象 这个对象就是字符流 只要操作字符数据 优先考虑使用字符流体系

4. 注意

流的操作只有两种 读和写

流的体系因为功能不同 但是有共性内容 不断抽取 形成继承体系 该体系一共有四个基类 而且都是抽象类

在这四个系统中 它们的子类 都有一个共性特点 子类名后缀都是父类名 前缀名都是这个子类的功能名称

5. 字节流

InputStream OutputStream

6. 字符流

Reader Writer

public static void main(String[] args) throws IOException { //读 写都会发生IO异常
    //1 创建一个字符输出流对象 用于操作文件 该对象一建立 就必须明确数据存储位置 是一个文件
    //2 对象产生后 会在堆内存中有一个实体 同时也调用了系统底层资源 在指定的位置创建了一个存储数据的文件
    //3 如果指定位置 出现了同名文件 文件会被覆盖
    FileWriter fw = new FileWriter("demo.txt"); //FileNotFoundException
    //调用Writer类中的write方法写入字符串 字符串并未直接写入到目的地中 而是写入到了流中(其实是写入到内存缓冲区中)
    fw.write("abcde");
    fw.flush(); //刷新缓冲区 将缓冲区中的数据刷到目的地文件中
    fw.close(); //关闭流 其实关闭的就是java调用的系统底层资源 在关闭前 会先刷新该流
    //close()和flush()的区别
    //flush(); 将缓冲区的数据刷到目的地中后 流可以使用
    //close(); 将缓冲区的数据刷到目的地中后 流就关闭了 该方法主要用于结束调用的底层资源 这个动作一定做
    //io异常的处理方式 io一定要写finally
}

//FileWriter写入数据的细节
//1> window中的换行符: \r\n两个符号组成   linux: \n
//2> 续写数据 只要在构造函数中传入新的参数true
//3> 目录分割符: window \\  /
public static void main(String[] args) {
    FileWriter fw = null;
    try {
        fw = new FileWriter("demo.txt", true);
        fw.write("abcde");
    }catch (IOException e ) {
        System.out.println(e.toString() + "....");
    }finally {
        if(fw != null)
        try {
            fw.close();
        }catch (IOException e) {
            System.out.println("close:" + e.toString());
        }
    }
}
//FileReader
//使用Reader体系 读取一个文本文件中的数据 返回-1 标志读到结尾
import java.io.*;
class  FileReaderDemo {
    public static void main(String[] args) throws IOException {
        //创建可以读取文本文件的流对象 FileReader让创建好的流对象和指定的文件相关联
        FileReader fr = new FileReader("demo.txt");
        int ch = 0;
        while ((ch = fr.read()) != -1) { //条件是没有读到结尾
            System.out.println((char)ch); //调用读取流的read方法 读取一个字符
        }
        fr.close();
    }
}

//读取数据的第二种方式: 第二种方式较为高效 自定义缓冲区
import java.io.*;
class FileReaderDemo2 {
    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("demo.txt"); //创建读取流对象和指定文件关联
        //因为要使用read(char[])方法 将读取到字符存入数组 所以要创建一个字符数组 一般数组的长度都是1024的整数倍
        char[] buf = new char[1024];
        int len = 0;
        while((len = fr.read(buf)) != -1) {
            System.out.println(new String(buf, 0, len));
        }
        fr.close();
    }
}

7. IO中的使用到了一个设计模式 装饰设计模式

装饰设计模式解决 对一组类进行功能的增强

包装 写一个类(包装类)对被包装对象进行包装

1> 包装类和被包装对象要实现同样的接口

2> 包装类要持有一个被包装对象

3> 包装类在实现接口时 大部分方法是靠调用被包装对象来实现的 对于需要修改的方法我们自己实现

二 字符流

Reader: 用于读取字符流的抽象类 子类必须实现的方法只有 read(char[], int, int) 和 close()

--> BufferedReader: 从字符输入流中读取文本 缓冲各个字符 从而实现字符, 数组和行的高效读取

--> InputStreamReader: 是字节流通向字符流的桥梁

--> FileReader: 用来读取字符文件的便捷类

Writer: 写入字符流的抽象类 子类必须实现的方法仅有 write(char[], int, int); flush() 和 close()

--> BufferedWriter: 将文本写入字符输出流 缓冲各个字符 从而提供单个字符, 数组和字符串的高效写入

--> OutputStreamWriter: 是字符流通向字节流的桥梁

--> FileWriter: 用来写入字符文件的便捷类

//使用字符流 复制文本
public static void method1() throws Exception {
    //封装数据源
    FileReader fr = new FileReader("E:\\FilePath.java");
    //封装目的地
    FileWriter fw = new FileWriter("FilePath.java");
    //从数据源中获取数据
    char[] chs = new char[1024];
    int len = 0;//记录每次获取到新数据的个数
    while((len = fr.read(chs)) != -1) {
        //数据写入到目的地
        fw.write(chs, 0, len);
    }
    //释放资源
    fr.close();
    fw.close();
}

//高效写入
FileWriter fw = new FileWriter("bufdemo.txt");
BufferedWriter bufw = new BufferedWriter(fw); //让缓冲区和指定流相关联
for(int x=0; x<4; x++) {
    bufw.write(x + "abc");
    bufw.newLine(); //写入一个换行符 这个换行符可以依据平台的不同写入不同的换行符
    bufw.flush(); //对缓冲区进行刷新 可以让数据到目的地中
}
bufw.close(); //关闭缓冲区 其实就是在关闭具体的流

//高效读取
FileReader fr = new FileReader("bufdemo.txt");
BufferedReader bufr  = new BufferedReader(fr);
String line = null;
while((line = bufr.readLine()) != null) { //readLine方法返回的时候是不带换行符的
    System.out.println(line);
}
bufr.close();

//键盘录入 然后输出
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));//键盘录入
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out)); //输出到控制台
String line = null;
while((line = bufr.readLine()) != null) {
    if("over".equals(line))
        break;
    bufw.write(line.toUpperCase()); //将输入的字符转成大写字符输出
    bufw.newLine();
    bufw.flush();
}
bufw.close();
bufr.close();

三 字节流

InputStream: 是表示字节输入流的所有类的超类

--> FileInputStream: 从文件系统中的某个文件中获得输入字节 FileInputStream 用于读取诸如图像数据之类的原始字节流

--> FilterInputStream: 包含其他一些输入流 它将这些流用作其基本数据源 它可以直接传输数据或提供一些额外的功能

--> BufferedInputStream: 该类实现缓冲的输入流

OutputStream: 此抽象类是表示输出字节流的所有类的超类

--> FileOutputStream: 文件输出流是用于将数据写入 File 或 FileDescriptor 的输出流

--> FilterOutputStream: 此类是过滤输出流的所有类的超类

--> BufferedOutputStream: 该类实现缓冲的输出流

//使用字节流 复制2进制文件(图片, 音频)
public static void main(String[] args) throws IOException {
    //封装数据源
    FileInputStream fis = new FileInputStream("E:\\resource\\IMG_1692.JPG");
    //封装目的地
    FileOutputStream fos = new FileOutputStream("1692.JPG");
    //从数据源获取数据
    //采用一次获取一个字节数组的内容
    byte[] bys = new byte[1024];//1Kb
    int len = 0;
    while((len = fis.read(bys)) != -1) {
        //把数据写入到目的地
        fos.write(bys, 0, len);
    }
    //释放资源
    fos.close();
    fis.close();
}

//高效的字节流
private static void method3() throws IOException {
    //封装数据源
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\test.avi"));
    //封装目的地
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.avi"));
    //获取数据
    int by = 0;
    while((by = bis.read()) != -1) {
        //写入数据
        bos.write(by);
    }
    //释放资源
    bis.close();
    bos.close();
}

//ByteArrayOutputStream 可以不写入文件 直接将数据源变成字符串
FileInputStream fis = new FileInputStream("E:\\test.txt") //数据源
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while((len = fis.read(buffer)) != -1) {
    bos.write(buffer, 0, len);
}
String content = bos.toString();

1. 流的操作规律

1> 明确源和目的

数据源: 就是需要读取 可以使用两个体系 InputStream, Reader

数据汇: 就是需要写入 可以使用两个体系 OutputStream, Writer

2> 操作的数据是否是纯文本数据?

如果是: 数据源 Reader, 数据汇 Writer

如果不是: 数据源 InputStream, 数据汇 OutputStream

3> 虽然确定了一个体系 但是该体系中有太多的对象 到底用哪个呢? 明确操作的数据设备

数据源对应的设备: 硬盘(File), 内存(数组), 键盘(System.in)

数据汇对应的设备: 硬盘(File), 内存(数组), 控制台(System.out)

4> 需要在基本操作上附加其他功能吗? 比如缓冲

如果需要就进行装饰

2. 转换流特有功能

1> 转换流可以将字节转成字符 原因在于 将获取到的字节通过查编码表获取到指定对应字符

2> 转换流的最强功能就是基于 字节流 + 编码表 没有转换 没有字符流

3. 转换流有一个子类就是操作文件的字符流对象

InputStreamReader

--> FileReader

OutputStreamWriter

--> FileWrier

4. 想要操作文本文件 必须要进行编码转换 而编码转换动作转换流都完成了 所以操作文件的流对象只要继承自转换流就可以读取一个字符了

但是子类有一个局限性 就是子类中使用的编码是固定的 是本机默认的编码表 对于简体中文版的系统默认码表是GBK

FileReader fr = new FileReader("a.txt"); //使用平台默认码表

InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"), "gbk"); //自己定义码表 与上局功能一样

5. 如果需要制定码表 必须用转换流

转换流 = 字节流 + 编码表

转换流的子类File = 字节流 + 默认编码表

凡是操作设备上的文本数据 涉及编码转换 必须使用转换流。

6. IO包中扩展功能的流对象: 基本都是装饰设计模式

四 打印流

Java.io.outputstream.PrintStream

1. 特点

1> 提供了更多的功能 比如打印方法 可以直接打印任意类型的数据

2> 它有一个自动刷新机制 创建该对象 指定参数 对于指定方法可以自动刷新

3> 它使用的本机默认的字符编码.

4> 该流的print方法不抛出IOException

5> 该对象的构造函数

a. PrintStream(File file); 创建具有指定文件且不带自动行刷新的新打印流

b. PrintStream(File file, String csn); 创建具有指定文件名称和字符集且不带自动行刷新的新打印流

c. PrintStream(OutputStream out); 创建新的打印流

d. PrintStream(OutputStream out, boolean autoFlush); 创建新的打印流

e. PrintStream(OutputStream out, boolean autoFlush, String encoding); 创建新的打印流

f. PrintStream(String fileName); 创建具有指定文件名称且不带自动行刷新的新打印流

g. PrintStream(String fileName, String csn);

2. PrintStream可以操作目的地: File对象, 字符串路径, 字节输出流

前两个都JDK1.5版本才出现 而且在操作文本文件时 可指定字符编码了

当目的是一个字节输出流时 如果使用的println方法 可以在printStream对象上加入一个true参数 这样对于println方法可以进行自动的刷新 而不是等待缓冲区满了再刷新 最终print方法都将具体的数据转成字符串 而且都对IO异常进行了内部处理

既然操作的数据都转成了字符串 那么使用PrintWriter更好一些 因为PrintWrite是字符流的子类 可以直接操作字符数据 同时也可以指定具体的编码

3. PrintWriter: 具备了PrintStream的特点同时 还有自身特点:

该对象的目的地有四个: File对象, 字符串路径, 字节输出流, 字符输出流

开发时尽量使用PrintWriter

//读取键盘录入将数据转成大写显示在控制台
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in)); //源 键盘输入
//目的: 把数据写到文件中 还想自动刷新
PrintWriter out = new PrintWriter(new FileWriter("out.txt"), true); //设置true后 自动刷新
String line = null;
while((line = bufr.readLine()) != null) {
    if("over".equals(line))
        break;
    out.println(line.toUpperCase()); //转大写输出
}
//注意: System.in, System.out这两个标准的输入输出流 在jvm启动时已经存在了 随时可以使用 当jvm结束了 这两个流就结束了 但是 当使用了显示的close方法关闭时 这两个流在提前结束了
out.close();
bufr.close();

五 SequenceInputStream 序列流

作用就是将多个读取流合并成一个读取流 实现数据合并

表示其他输入流的逻辑串联 它从输入流的有序集合开始 并从第一个输入流开始读取 直到到达文件末尾 接着从第二个输入流读取 依次类推 直到到达包含的最后一个输入流的文件末尾为止

这样做 可以更方便的操作多个读取流 其实这个序列流内部会有一个有序的集合容器 用于存储多个读取流对象

该对象的构造函数参数是枚举 想要获取枚举 需要有Vector集合 但不高效 需用ArrayList 但ArrayList中没有枚举 只有自己去创建枚举对象

但是方法怎么实现呢? 因为枚举操作的是具体集合中的元素 所以无法具体实现 但是枚举和迭代器是功能一样的 所以 可以用迭代替代枚举

合并原理: 多个读取流对应一个输出流

切割原理: 一个读取流对应多个输出流

import java.io.*;
import java.util.*;
class  SplitFileDemo {
    private static final String CFG = ".properties";
    private static final String SP = ".part";
    public static void main(String[] args) throws IOException {
        File file = new File("c:\\0.bmp");
        File dir = new File("c:\\partfiles");
        meger(dir);
    }
    //数据的合并
    public static void meger(File dir) throws IOException {
        if(!(dir.exists() && dir.isDirectory()))
            throw new RuntimeException("指定的目录不存在 或者不是正确的目录");
        File[] files = dir.listFiles(new SuffixFilter(CFG));
        if(files.length == 0)
            throw new RuntimeException("扩展名.proerpties的文件不存在");
        //获取到配置文件
        File config = files[0];
        //获取配置文件的信息
        Properties prop = new Properties();
        FileInputStream fis = new FileInputStream(config);
        prop.load(fis);
        String fileName = prop.getProperty("filename");
        int partcount = Integer.parseInt(prop.getProperty("partcount"));

        File[] partFiles = dir.listFiles(new SuffixFilter(SP));
        if(partFiles.length != partcount)
            throw new RuntimeException("缺少碎片文件");
        ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();
        for(int x=0; x<partcount; x++) {
            al.add(new FileInputStream(new File(dir, x + SP)));
        }
        Enumeration<FileInputStream> en = Collections.enumeration(al);
        SequenceInputStream sis = new SequenceInputStream(en);
        File file = new File(dir, fileName);
        FileOutputStream fos = new FileOutputStream(file);
        byte[] buf = new byte[1024];
        int len = 0;
        while((len = sis.read(buf)) != -1) {
            fos.write(buf, 0, len);
        }
        fos.close();
        sis.close();
    }
    //带有配置信息的数据切割
    public static void splitFile(File file) throws IOException {
        //用一个读取流和文件关联
        FileInputStream fis = new FileInputStream(file);
        //创建目的地 因为有多个 所以先创建引用
        FileOutputStream fos = null;
        //指定碎片的位置
        File dir = new File("c:\\partfiles");
        if(!dir.exists())
            dir.mkdir();
        //碎片文件大小引用
        File f = null;
        byte[] buf = new byte[1024*1024];
        //因为切割完的文件通常都有规律的 为了简单标记规律使用计数器
        int count = 0;
        int len = 0;
        while((len = fis.read(buf)) != -1) {
            f = new File(dir, (count++) + ".part");
            fos = new FileOutputStream(f);
            fos.write(buf, 0, len);
            fos.close();
        }
        //碎片文件生成后 还需要定义配置文件记录生成的碎片文件个数 以及被切割文件的名称
        //定义简单的键值信息 可是用Properties
        String filename = file.getName();
        Properties prop = new Properties();
        prop.setProperty("filename", filename);
        prop.setProperty("partcount", count + "");
        File config = new File(dir, count + ".properties");
        fos = new FileOutputStream(config);
        prop.store(fos, "");
        fos.close();
        fis.close();
    }
}
class SuffixFilter implements FileFilter {
    private String suffix;
    SuffixFilter(String suffix) {
        this.suffix  = suffix;
    }
    public boolean accept(File file) {
        return  file.getName().endsWith(suffix);
    }
}

六 管道流

管道读取流和管道写入流可以像管道一样对接上 管道读取流就可以读取管道写入流写入的数据

注意: 需要加入多线程技术 因为单线程 先执行read 会发生死锁 因为read方法是阻塞式的 没有数据的read方法会让线程等待

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

PipedInputStream pipin = new PipedInputStream();

PipedOutputStream pipout = new PipedOutputStream();

pipin.connect(pipout);

new Thread(new Input(pipin)).start();

new Thread(new Output(pipout)).start();

}

七 对象的序列化

目的: 将一个具体的对象进行持久化 写入到硬盘上

注意: 静态数据不能被序列化 因为静态数据不在堆内存中 是存储在静态方法区中

用transient 关键字修饰变量 可以将非静态的数据不进行序列化

Serializable: 用于启动对象的序列化功能 可以强制让指定类具备序列化功能 该接口中没有成员 这是一个标记接口 这个标记接口用于给序列化类提供UID 这个uid是依据类中的成员的数字签名进行运行获取的 如果不需要自动获取一个uid 可以在类中 手动指定一个名称为serialVersionUID id号 依据编译器的不同 或者对信息的高度敏感性 最好每一个序列化的类都进行手动显示的UID的指定

import java.io.*;
class ObjectStreamDemo {
    public static void main(String[] args) throws Exception {
        writeObj();
        readObj();
    }
    public static void readObj() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"));
        Object obj = ois.readObject(); //读取一个对象
        System.out.println(obj.toString());
    }
    public static void writeObj() throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"));
        oos.writeObject(new Person("lisi", 25)); //写入一个对象
        oos.close();
    }
}

class Person implements Serializable {
    private static final long serialVersionUID = 42L;
    private transient String name; //用transient修饰后name将不会进行序列化
    public int age;
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return name + "::" + age;
    }
}

八 RandomAccessFile

该类主要用于提供对文件的读写功能 与普通的输入输出流不一样的是RamdomAccessFile可以任意的访问文件的任何地方 这就是"Random"的意义所在

特点

1> 该对象即可读取 又可写入

2> 该对象中的定义了一个大型的byte数组 通过定义指针来操作这个数组

3> 可以通过该对象的getFilePointer()获取指针的位置 通过seek()方法设置指针的位置

4> 该对象操作的源和目的必须是文件

5> 其实该对象内部封装了字节读取流和字节写入流

注意: 实现随机访问 最好是数据有规律

class RandomAccessFileDemo{
    public static void main(String[] args) throws IOException {
        write();
        read();
        randomWrite();
    }
    //随机写入数据 可以实现已有数据的修改
    public static void randomWrite() throws IOException {
        RandomAccessFile raf = new RandomAccessFile("random.txt", "rw");
        raf.seek(8*4);
        System.out.println("pos :" + raf.getFilePointer());
        raf.write("王武".getBytes());
        raf.writeInt(102);
        raf.close();
    }
    public static void read() throws IOException {
        RandomAccessFile raf = new RandomAccessFile("random.txt","r"); //只读模式
        //指定指针的位置
        raf.seek(8*1); //实现随机读取文件中的数据 注意: 数据最好有规律
        System.out.println("pos1 :" + raf.getFilePointer());
        byte[] buf = new byte[4];
        raf.read(buf);
        String name = new String(buf);
        int age = raf.readInt();
        System.out.println(name + "::" + age);
        System.out.println("pos2 :" + raf.getFilePointer());
        raf.close();
    }
    public static void write() throws IOException {
        //rw: 当这个文件不存在 会创建该文件 当文件已存在 不会创建 所以不会像输出流一样覆盖
        RandomAccessFile raf = new RandomAccessFile("random.txt", "rw"); //rw读写模式
        //往文件中写入人的基本信息, 姓名, 年龄
        raf.write("张三".getBytes());
        raf.writeInt(97);
        raf.close();
    }
}