Java基础-IO学习之内存操作流,打印流 ...(上)

来源:互联网 发布:秦罗敷与刘兰芝 知乎 编辑:程序博客网 时间:2024/05/17 03:31

前面学习了字节流(传送门)和字符流(传送门),基本上大体的IO已经学习完毕了,剩下的还有一些零零碎碎的IO流对象,标题后面还有一堆的省略号,表明该篇博客是一篇大杂烩,自己学习的时候也就本着了解即可,看到有印象,查阅一些资料便能基本使用的思想去学习。东西实在有点多,重点放在前面的字节流和字符流。

废话不多说,下面进行大杂烩学习。


序列流

什么是序列流

序列流可以把多个字节输入流整合成一个, 从序列流中读取数据时, 将从被整合的第一个流开始读, 读完一个之后继续读第二个, 以此类推.

使用方式

1.整合两个: SequenceInputStream(InputStream, InputStream)

public static void main(String[] args) throws IOException {FileInputStream fis1 = new FileInputStream("read.txt");FileInputStream fis2 = new FileInputStream("read2.txt");SequenceInputStream sis = new SequenceInputStream(fis1, fis2);//将两个流整合成一个流FileOutputStream fos = new FileOutputStream("write.txt");int b;while((b = sis.read()) != -1) {//用整合后的读fos.write(b);}sis.close();fos.close();}
2.整合多个

想出的第一种方案:

public static void main(String[] args) throws IOException {SequenceInputStream sis = new SequenceInputStream(new FileInputStream("read.txt"), new FileInputStream("read2.txt"));SequenceInputStream sis2 = new SequenceInputStream(sis, new FileInputStream("read3.txt"));FileOutputStream fos = new FileOutputStream("write.txt");int b;while((b = sis2.read()) != -1) {fos.write(b);}sis2.close();fos.close();}
每一个SequenceInputStream都为其InputStream的子类,当然可以当做其参数进行传递,但是万一有100个文件,你可以算算需要写多少行代码~~

查看其构造方法:

public SequenceInputStream(Enumeration<? extends InputStream> e) {}
可以传递一个Enumeration,这个是不是在前面的集合学习中为Vector特有的遍历方式

public static void main(String[] args) throws IOException {FileInputStream fis1 = new FileInputStream("read.txt");//创建输入流对象,关联read.txt,下同FileInputStream fis2 = new FileInputStream("read2.txt");FileInputStream fis3 = new FileInputStream("read3.txt");Vector<InputStream> vector = new Vector<>();//创建vector集合对象vector.add(fis1);vector.add(fis2);vector.add(fis3);Enumeration<InputStream> enumeration = vector.elements();//获取枚举引用SequenceInputStream sis = new SequenceInputStream(enumeration);//传递给SequenceInputStream构造FileOutputStream fos = new FileOutputStream("write.txt");int b;while((b = sis.read()) != -1) {fos.write(b);}sis.close();fos.close();}
三个源数据

输出的结果

分析下该源码(JDK1.8)

因为这个流代码不太复杂,就分析一下

publicclass SequenceInputStream extends InputStream {Enumeration<? extends InputStream> e;InputStream in;    public SequenceInputStream(Enumeration<? extends InputStream> e) {        this.e = e;        try {            nextStream();//获取第一个InputStream,即fis1        } catch (IOException ex) {            // This should never happen            throw new Error("panic");        }    }    final void nextStream() throws IOException {        if (in != null) {//如果in不为空则关流            in.close();        }        if (e.hasMoreElements()) {//这里类似进行遍历            in = (InputStream) e.nextElement();            if (in == null)                throw new NullPointerException();        }        else in = null;//遍历完所有流后将in设置为null    }    public int read() throws IOException {        while (in != null) {            int c = in.read();            if (c != -1) {//这里会一直读取in知道取读到末尾                return c;            }            nextStream();//这里会关闭该in,并获取下一个vector中的InputStream        }        return -1;//所有流都读取完毕后返回-1    }    public void close() throws IOException {        do {//这里read若是一切正常,无特殊情况,这个方法是无用的,因为上面代码read完一个流后便立即将关闭了            nextStream();        } while (in != null);    }}


内存操作流


什么是内存输入流(ByteArrayInputStream)

其包含一个内部缓冲区,该缓冲区包含从流中读取的字节。内部计数器跟踪 read 方法要提供的下一个字节。

用于以IO流的方式来完成对字节数组内容的读写,来支持类似内存虚拟文件或者内存映射文件的功能。将一个字节数组作为输入来源,将字节数组转换为流来处理,对字节数组进行读写,会方便很多。可以被高级输入工具DataInputStream(后面讲解)输入成java能直接处理的格式,比如处理成各种类型,double,float,char,int, short,long,或任何对象,或字符串,或媒体数据,是把一块内存作为输入的一种方式。用处很多。(以上摘自网上,东拼西凑而成)

ByteArrayInputStream源码(JDK1.8)

publicclass ByteArrayInputStream extends InputStream {        protected byte buf[];    protected int count;    protected int pos;    //使用一个字节数组当中所有的数据做为数据源,程序可以像输入流方式一样读取字节,    //可以看做一个虚拟的文件,用文件的方式去读取它里面的数据。    public ByteArrayInputStream(byte buf[]) {        this.buf = buf;        this.pos = 0;        this.count = buf.length;    }    public ByteArrayInputStream(byte buf[], int offset, int length) {        this.buf = buf;        this.pos = offset;        this.count = Math.min(offset + length, buf.length);        this.mark = offset;    }    public synchronized int read() {//这里相当于遍历buf        return (pos < count) ? (buf[pos++] & 0xff) : -1;//将byte提升为int返回    }    public void close() throws IOException {    }}

对ByteArrayInputStream了解不多,不做深入了解,下次遇到时再来更新此贴


什么是内存输出流(ByteArrayOutputStream)

该输出流可以向内存中写数据, 把内存当作一个缓冲区, 写出之后可以一次性获取出所有数据

此类实现了一个输出流,其中的数据被写入一个 byte 数组。缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray() 和 toString() 获取数据

下面讲讲ByteArrayOutputStream

创建对象: new ByteArrayOutputStream()

写出数据: write(int), write(byte[])

获取数据: toByteArray()

先来回顾一个知识:读取一串字符是不是只能用字符流读取呢?使用字节流读取一般都会乱码?

在不知道ByteArrayOutputStream之前那是肯定的,下面来尝试下:(read.txt文件内容:我读书少,你可别骗我)

public static void main(String[] args) throws IOException {FileInputStream fis = new FileInputStream("read.txt");ByteArrayOutputStream baos = new ByteArrayOutputStream();int b;while((b = fis.read()) != -1) {baos.write(b);//写入到内存字节数组中}byte[] newArr = baos.toByteArray();////将内存缓冲区中所有的字节存储在newArr中System.out.println(new String(newArr));System.out.println(baos); //上面两句可合并成一句,说明该方法重写了 toString方法}/* * Output: * 我读书少,你可别骗我 * 我读书少,你可别骗我 */

ByteArrayOutputStream源码分析(JDK1.8)

public class ByteArrayOutputStream extends OutputStream {    protected byte buf[];//缓冲数组    protected int count;//缓冲字节个数,即buf size        //缓冲区最大容量    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;    public ByteArrayOutputStream() {        this(32);//默认缓冲数组大小为32    }    public ByteArrayOutputStream(int size) {        if (size < 0) {            throw new IllegalArgumentException("Negative initial size: "                                               + size);        }        buf = new byte[size];//分配缓冲区    }    private void ensureCapacity(int minCapacity) {        //当前容量大于缓冲区大小,需扩容        if (minCapacity - buf.length > 0)            grow(minCapacity);    }    //扩容    private void grow(int minCapacity) {        // overflow-conscious code        int oldCapacity = buf.length;        int newCapacity = oldCapacity << 1;//扩容一倍        if (newCapacity - minCapacity < 0)//若是还不够            newCapacity = minCapacity;//直接等于需要的容量(minCapacity)        if (newCapacity - MAX_ARRAY_SIZE > 0)            newCapacity = hugeCapacity(minCapacity);        buf = Arrays.copyOf(buf, newCapacity);//将buf拷贝到新数组中    }      //写入一个字节    public synchronized void write(int b) {        ensureCapacity(count + 1);//判断是否需要扩容        buf[count] = (byte) b;//强制转化为byte后写入        count += 1;//size++    }    //copy一份buf并返回其引用    public synchronized byte toByteArray()[] {        return Arrays.copyOf(buf, count);    }    //将buf作为构造参数生成一个String对象(使用平台默认编码表进行转化)    public synchronized String toString() {        return new String(buf, 0, count);    }    //关闭流    public void close() throws IOException {    }}

我们还可将内存输出流保存一些临时文件信息(最后一起写出),缓存一些数据,因为这些数据没有必要将其写入到文件中。

有些人在此可能晕了,内存操作流什么鬼,其实什么内存不内存的(扯到内存后有些人可能就晕了),简单的说,ByteArrayInputStream就是往字节数组里写数据,ByteArrayOutputStream是往字节数组里读取数据。以前我们只会往文件中写数据,但是现在我们可以往内存中写数据了(存在字节数组中),更方便方便我们的使用了,不是所有的东西我们都需存在文件中的。

仔细看其源码,你可以发现其close是一个空方法,也就是无效的,为什么?

既然其数组在内存中分配,当没有强引用的时候会自动被垃圾回收了,所以close实现为空是可以的。

Q:什么情况是需要关闭流呢?为什么我们需要手动关闭流?

一般当需要和硬盘上的文件进行交互读取数据(文件操作相关)的流必须手动关闭,因为GC只管内存不管别的资源。假如有内存以外的其它资源依附在Java对象上,如FileInputStream,因为java怎么知道你要用到什么时候,比如读取硬盘上文件,那么在硬盘和内存间会建立一根管道(抽象)用于传输数据,你不把管道给关了,那么表明它是一直流通可用的,所以自然也不会被回收,就算gc会回收好了,gc的运行的时间点是不确定的(只有在内存不足是才会执行),久了便内存溢出了。

关闭原则:尽量晚开早关,多层依赖关系关闭最外层即可

小习1

使用内存操作流,完成一个字符串(英文字符)小写字母变为大写字母的操作。

public static void main(String[] args) throws IOException {String str = "HeLlo worlD";ByteArrayInputStream bais = new ByteArrayInputStream(str.getBytes());ByteArrayOutputStream baos = new ByteArrayOutputStream();int b;while((b = bais.read()) != -1) {char c = (char)b;baos.write(Character.toUpperCase(c));}System.out.println(baos);//bais 和 baos 关流无效}/* * Output: * HELLO WORLD */

小习2

定义一个文件输入流,调用read(byte[] b)方法,将a.txt文件中的内容打印出来(byte数组大小限制为5)

/* * 分析: * 首先read(byte[] b)说明我们只能用字节流, * 读取a.txt文件,文件中是可能有中文的, * byte数组大小限制为5,而且要打印. * 因为一次读取5个字节,直接将5个字节转换为字符串输出再循环操作,若有中文很容易造成乱码 * 那么我们可以定义一个字节数组,将所有字节都存进去再一起转换 * 但是我们已经学习了ByteArrayOutputStream,上述这些都应经封装好了,直接调用即可 */public static void main(String[] args) throws IOException {FileInputStream fis = new FileInputStream("a.txt");ByteArrayOutputStream baos = new ByteArrayOutputStream();byte[] bf = new byte[5];int len;while((len = fis.read(bf)) != -1) {baos.write(bf,0,len);//注意这里要写入0~len个字节长度}System.out.println(baos);fis.close();//fis流一定要手动关闭}

对象操作流

什么是对象操作流

该流可以将一个对象写出, 或者读取一个对象到程序中. 也就是执行了序列化和反序列化的操作.

ObjecOutputStream

写出: new ObjectOutputStream(OutputStream), writeObject()

简单构建一个Person类:

class Person {private String name;private int age;public Person(String name, int age) {super();this.name = name;this.age = age;}@Overridepublic String toString() {return "Person [name=" + name + ", age=" + age + "]";}}

下面进行序列化操作(相等于存档):

public static void main(String[] args) throws IOException {FileOutputStream fos = new FileOutputStream("obj.txt");ObjectOutputStream oos = new ObjectOutputStream(fos);Person person = new Person("张三", 23);oos.writeObject(person);}/* * Output: * Exception in thread "main" java.io.NotSerializableException: info.InputStream.Person */

很遗憾,存档失败,查看异常信息,意思是不是被序列化的异常,我们要将对象写出去,那么对象必须能够序列化才可写出去,就像我们需要制定一份规则去存档,什么规则?就是实现Serializable接口,查看API可得知,这是一个空接口,实现空接口有什么意义呢?无非就是起标识的作用罢了,这就是所谓的规则,你只要实现了该接口,就是可以被序列化的,否则就不能被序列化。举个栗子,你会发现食品包装袋上都有一个QS的图标,有生产许可几个字,在包装袋上有该图标便表明该食品是经过强制性检查的,没有你便要考虑下该食品的是否安全了。

改进我们的Person:

class Person implements Serializable {//...}
中间内容一样便省略了

重新进行序列化

public static void main(String[] args) throws IOException {Person person1 = new Person("张三", 23);Person person2 = new Person("李四", 24);FileOutputStream fos = new FileOutputStream("obj.txt");//无论是字节输出流,还是字符输出流都不能直接写出对象//fos.write(person1);  编译错误//FileWriter fw = new FileWriter("obj.txt");//fw.write(person1);编译错误ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(person1);oos.writeObject(person2);oos.close();}

可以发现其是乱码的,这是正常的现象,看上面代码,我们使用了ObjectOutputStream包装了FileOutputStream,那么说明我们将对象转换为了字节进行写入,使用我们的码表进行翻译时,码表上肯定没有所匹配的值,所以码表翻译不过来的便乱码了

这个都乱码了,我们又看不懂,还有什么意义?

举个栗子:你打游戏存档,你会去看你的存档文件吗?你会打开存档文件去看你游戏人物几级了,什么装备等等。我们根本不会看,我们只要保证下一次玩游戏能把它读出来即可。所以在这里看不懂没关系,我们也不需要看懂,我们只要保证程序能把它读出来即可。


ObjectInputStream

读取: new ObjectInputStream(InputStream), readObject()

下面进行反序列化操作

public static void main(String[] args) throws IOException, ClassNotFoundException {ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"));Person person1 = (Person)ois.readObject();Person person2 = (Person)ois.readObject();System.out.println(person1);System.out.println(person2);ois.close();}/* * Output: * Person [name=张三, age=23] * Person [name=李四, age=24] */
读档成功

对象操作流优化

将对象存储在集合中写出,读取到的是一个集合对象。

public static void main(String[] args) throws IOException, ClassNotFoundException {Person person1 = new Person("张三", 23);Person person2 = new Person("李四", 24);Person person3 = new Person("王五", 25);ArrayList<Person> listOut = new ArrayList<>();//将对象存储在集合中listOut.add(person1);listOut.add(person2);listOut.add(person3);ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.txt"));oos.writeObject(listOut);//写入集合oos.close();ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"));ArrayList<Person> listRead = (ArrayList<Person>)ois.readObject(); //黄色警告线,因为泛型在运行期会被擦除,所以运行期相当于没有泛型for(Person person : listRead) {System.out.println(person);}ois.close();}/* * Output: * Person [name=张三, age=23] * Person [name=李四, age=24] * Person [name=王五, age=25] */

Serializable的ID号

前面讲了要写出的对象必须实现Serializable接口才能被序列化。

查看java源码的时候你会发现,许多的类都实现了Serializable接口,但是他们还有一个东西,那就是id号。

但是上面我们的Person没有啊,没关系,因为系统会自动生成一个。

我们对Person随意进行修改(可以加个随意属性),但是不对其进行序列化(存档),直接进行读取

Exception in thread "main" java.io.InvalidClassException: info.InputStream.Person; local class incompatible: stream classdesc serialVersionUID = -3166338895792538591, local class serialVersionUID = 3283676469632569740
报错了,这个意思就是相当于说我以前的版本是-3166338895792538591,但是现在的版本是3283676469632569740,这么大的一串数字,有点心累~
改回原来的版本并加个id号,对其进行重新存档操作。

class Person implements Serializable {private static final long serialVersionUID = 1L;//...}
随后对其进行修改,并更改id号

class Person implements Serializable {private static final long serialVersionUID = 2L;private int test;//...}
还是不对其进行存档操作,直接进行读档,你发现会怎么样?

Exception in thread "main" java.io.InvalidClassException: info.InputStream.Person; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
现在看起来便一目了然了,你以前的版本号为1,现在改为2了,很明确的告诉你第几次改版了。

不加id号是时随机生成的,所以id号的作用就是在报错的时候为了让你看的更清晰点,根据id号可判断。

如果你遵循每次读档前都进行存档操作,那么这种问题永远不会发生。


打印流

什么是打印流

该流可以很方便的将对象的toString()结果输出, 并且自动加上换行, 而且可以使用自动刷出的模式

打印流只操作数据目的

PrintStream(字节流)

System.out就是一个PrintStream, 其默认向控制台输出信息

public static void main(String[] args) throws IOException {System.out.println("aaa");PrintStream ps = System.out;//获取标准输出流ps.println(97);//底层通过Integer.toString()将97转换成字符串并打印ps.write(97);//查找码表,找到对应的a并打印Person p1 = new Person("张三", 23);ps.println(p1);Person p2 = null;ps.println(p2);ps.close();}/* * Output: * aaa * 97 * aPerson [name=张三, age=23] * null */

其中write是不具备刷新功能的,但是为什么也输出了?那是因为下面的println(p1)对其进行刷新时一起输出的。

public static void main(String[] args) throws IOException {PrintStream ps = System.out;//获取标准输出流ps.write(97);//查找码表,找到对应的a并打印//ps.close();}

注释掉close后你会发现什么都没有输出。

简单源码分析(JDK1.8)

public class PrintStream extends FilterOutputStreamimplements Appendable, Closeable{    private final boolean autoFlush;    private BufferedWriter textOut;    private OutputStreamWriter charOut;        public PrintStream(OutputStream out, boolean autoFlush) {        this(autoFlush, requireNonNull(out, "Null output stream"));    }    //私有构造    private PrintStream(boolean autoFlush, OutputStream out) {        super(out);        this.autoFlush = autoFlush;        this.charOut = new OutputStreamWriter(this);        this.textOut = new BufferedWriter(charOut);//使用buffer包装OutputStreamWriter    }    //看源码可发现,所有的println方法调用print方法,最后都调用write(String s)    //而该方法无论是否开启autoFlush,都会自动刷新,所以print和println都具备自动刷新功能    private void write(String s) {        try {            synchronized (this) {                ensureOpen();                textOut.write(s);//写入                textOut.flushBuffer();//将buf内的数据都刷新到charOut中                charOut.flushBuffer();//这里的刷新会打印到控制台                if (autoFlush && (s.indexOf('\n') >= 0))                    out.flush();            }        }        catch (InterruptedIOException x) {            Thread.currentThread().interrupt();        }        catch (IOException x) {            trouble = true;        }    }    public void print(Object obj) {        write(String.valueOf(obj));         //return (obj == null) ? "null" : obj.toString();    }    public void print(int i) {        write(String.valueOf(i));//将int转换为String,再调用上面write这个方法    }    public void println(int x) {        synchronized (this) {            print(x);//调用上面这个方法            newLine();//打印换行        }    }    //而该方法必须在构造传入autoFlush(true)才可自动刷新    public void write(int b) {        try {            synchronized (this) {                ensureOpen();                out.write(b);//写入                if ((b == '\n') && autoFlush)                    out.flush();//这里要开启autoFlush后才可刷新            }        }        catch (InterruptedIOException x) {            Thread.currentThread().interrupt();        }        catch (IOException x) {            trouble = true;        }    }}

PrintWriter(字符流)

打印: print(), println()

自动刷出: PrintWriter(OutputStream out, boolean autoFlush, String encoding)
PrintWriter对于前面的PrintStream就娄很多了,为啥?因为他的自动刷新也就仅仅针对println()有效(前提还得开启autoFlush)

public static void main(String[] args) throws IOException {PrintWriter pw = new PrintWriter(new FileOutputStream("write.txt"),true);pw.println(97);//自动刷出功能只针对的是println方法pw.print(97);pw.write(97);//pw.close();}
public static void main(String[] args) throws IOException {PrintWriter pw = new PrintWriter(new FileOutputStream("write.txt"),true);pw.println(97);//自动刷出功能只针对的是println方法pw.print(97);pw.write(97);pw.println();//pw.close();}

简单源码分析(JDK1.8)

public class PrintWriter extends Writer {    protected Writer out;    private final boolean autoFlush;    public PrintWriter (Writer out) {        this(out, false);//默认不开启刷新    }public PrintWriter(Writer out, boolean autoFlush) {super(out);this.out = out;this.autoFlush = autoFlush;lineSeparator = java.security.AccessController.doPrivileged(new sun.security.action.GetPropertyAction("line.separator"));}//这里这部分和PrintStream都是很像的,println调用print,在调用write    public void println(int x) {        synchronized (lock) {            print(x);            println();//若autoFlush=true,使用该方法可刷新        }    }    public void print(int i) {        write(String.valueOf(i));    }    public void write(String s) {        write(s, 0, s.length());    }//上面的write调用下面的写入数据,但其内部无刷新(PrintStream将刷新放在这里)    public void write(String s, int off, int len) {        try {            synchronized (lock) {                ensureOpen();                out.write(s, off, len);            }        }        catch (InterruptedIOException x) {            Thread.currentThread().interrupt();        }        catch (IOException x) {            trouble = true;        }    }    public void println() {        newLine();//调用该方法换行,并判断刷新    }    //换行后判断自动刷新    //而print和write都是不用换行的,即使autoFlush=true,也都不会自动刷新    private void newLine() {        try {            synchronized (lock) {                ensureOpen();                out.write(lineSeparator);//换行                if (autoFlush)//刷新                    out.flush();            }        }        catch (InterruptedIOException x) {            Thread.currentThread().interrupt();        }        catch (IOException x) {            trouble = true;        }    }}

本想简单记录下,以后方便自己回顾,但是写的时候发现有些地方不太懂,便忍不住去看了下源码,看完源码就把源码也贴出来把,于是便有点长了,这些流其实主要掌握即可,不必太深入的,剩下的还有一些流就再写一篇来续写吧

0 0
原创粉丝点击