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

时间:2021-12-21 11:58:51

前面学习了字节流(传送门)和字符流(传送门),基本上大体的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();
}
三个源数据 Java基础-IO学习之内存操作流,打印流 ...(上)

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

分析下该源码(JDK1.8)

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

public
class 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)

public
class 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;
}
@Override
public 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();
}
Java基础-IO学习之内存操作流,打印流 ...(上)
可以发现其是乱码的,这是正常的现象,看上面代码,我们使用了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 FilterOutputStream
implements 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();}
Java基础-IO学习之内存操作流,打印流 ...(上)Java基础-IO学习之内存操作流,打印流 ...(上)

简单源码分析(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;
}
}
}

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