1. 内存操作流
除了文件之外,I/O
的操作也可以发生在内存之中,这种流被称为内存操作流。文件流不管最后这个文件数据是否会被保留都会产生一个文件数据,而内存流是跟内存有关的,不需要关联文件。如果有个需求是要进行I/O处理,但是又不希望产生临时文件,这种情况下就可以使用内存流。
1.1 基本使用
内存流的分类:
1. 字节内存流:ByteArrayInputStream、 ByteArrayOutputStream
2. 字符内存流:CharArrayReader、 CharArrayWriter
- 通过内存流实现小写转大写:
package com.file;
import java.io.*;
//将字符串中的字符转换成大写,要求使用内存流
public class TestMemoryStream {
public static void main(String[] args) {
String message = "hello world";
byte[] messageBytes = message.getBytes();
//内存操作,不需要捕获异常
//in out 都是内存流,数据都存在内存中
ByteArrayInputStream in = new ByteArrayInputStream(messageBytes);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
int c = -1;
while((c = in.read()) != -1) {
//转大写
c = c-32;
out.write(c);
}
out.flush();
//输出流里面没有把输出流变成字节数组的方法,是ByteArray里面有的
//byte[] newMessage = ((ByteArrayOutputStream) out).toByteArray();
byte[] newMessage = out.toByteArray();
System.out.println(new String(newMessage));
} catch (IOException e) {
}
}
}
这个时候发生了I/O
操作,但是没有文件产生,可以理解为一个临时文件处理。
1.2 文件合并
package com.file;
import java.io.*;
public class MemoryStreamMerge {
public static void main(String[] args) {
//data-1.txt + data-2.txt = data.txt
/*步骤:
* 1. data-1.txt 复制到内存的输出流
* 2. data-2.txt 复制到内存的输出流
* 3. 内存的输出流 -> byte[]字节流
* 4. byte[] -> 输出到文件的输出流
*/
File part1 = new File("D:" + File.separator + "test" + File.separator + "data-1.txt");
File part2 = new File("D:" + File.separator + "test" + File.separator + "data-2.txt");
File part = new File("D:" + File.separator + "test" + File.separator + "data.txt");
try(FileInputStream in1 = new FileInputStream(part1);
FileInputStream in2 = new FileInputStream(part2);
ByteArrayOutputStream out1 = new ByteArrayOutputStream();
FileOutputStream out2 = new FileOutputStream(part)
) {
byte[] buff = new byte[1024];
int len = -1;
while((len = in1.read(buff)) != -1) {
out1.write(buff, 0, len);
}
while((len = in2.read(buff)) != -1) {
out1.write(buff, 0, len);
}
out1.flush();
byte[] data = out1.toByteArray();
out2.write(data);
out2.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 既然是在内存中读写,内存流速度虽然快,但是受制于内存大小,适合于处理批量的少量数据。
/**源码:
* Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in
* this class can be called after the stream has been closed without
* generating an <tt>IOException</tt>.
*/
public void close() throws IOException {
}
上面是内存流的源码,看出来内存流可以关闭也可以不关闭,因为close()
里面没有任何代码。
2. 打印流
PrintStream
可以称之为OutputStream
的加强版。OutputStream
只能放字节或者字节数组,如果操作的不是二进制数据,就需要getBytes
,所以不是很方便,总结下来其缺点有两个:
① 所有的数据必须转换为字节数组。
② 如果要输出的是int
或者double
等类型就不方便了。
2.1 基本使用
打印流设计的主要目的是为了解决OutputStream
的设计问题,其本质不会脱离OutputStream
。
package com.file;
import java.io.*;
/**
* 目的:为了OutputStream的输出更加简单
* 类似代理模式,但是不完全遵循
* 代码复用性强
*/
public class PrintUtil {
private final OutputStream out;
public PrintUtil(OutputStream out) {
this.out = out;
}
public void print(String value) {
try {
this.out.write(value.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
public void println(String value) {
this.print(value);
try {
this.out.write((int)'\n');
} catch (IOException e) {
e.printStackTrace();
}
}
public void print(int value) {
this.println(String.valueOf(value));
}
public static void main(String[] args) {
try(FileOutputStream out = new FileOutputStream("D:" + File.separator + "test"
+ File.separator + "printutil.txt")
) {
PrintUtil printUtil = new PrintUtil(out);
printUtil.print("hello");
printUtil.print(" world");
printUtil.print(22);
printUtil.print(10);
/*
* hello world
* 22
* 10
*/
} catch (IOException e) {
e.printStackTrace();
}
}
}
经过简单处理之后,让OutputStream
的功能变的更加强大了,其实本质就只是对OutputStream
的功能做了一个封装而已。
- 但是上面仅仅是我们自己定义的打印流,还不能满足全部的需求。
2.2 系统提供的打印流
1. 字节打印流:PrintStream
2. 字符打印流:PrintWriter
打印流的设计属于装饰设计模式:核心依然是某个类的功能,但是为了得到更好的操作效果,让其支持的功能更多一些。
- 不光支持数字、字符、字符串和字符数组等,还可以支持格式化输出。
格式化输出:printf(String format, Object ... args)
package com.file;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
public class TestPrintStream {
public static void main(String[] args) {
String file = "D:" + File.separator + "test"
+ File.separator + "printStream.txt";
//字符的打印流
try(PrintWriter writer = new PrintWriter(file)) {
writer.write("姓名");
writer.write("张三");
writer.write("\n");
writer.write(50);
writer.write("\n");
writer.write(new char[]{'A', 'B', 'C'});
//链式调用
writer.append("A").append("B").append("C");
//格式化输出,String里面也有个格式化输出的方法, System.out.printf也是
writer.printf("姓名:%s 年龄:%d 身高:%.2fcm \n", "张三", 22, 180.25F);
String str = String.format("姓名:%s 年龄:%d 身高:%.2fcm", "张三", 22, 180.25F);
writer.println(str);
writer.flush();
} catch(IOException e) {
}
}
}
3. System对I/O的支持
实际上我们一直在使用的系统输出System.out.println
就是利用了I/O
流的模式完成。
/*
System:类
out:静态属性,PrintStream类型
println:out属性的一个对象的方法
*/
System.out.println("hello world");
- 在
System
类中定义了三个操作的常量:
① 标准输出(显示器) :public final static PrintStream out
② 错误输出:public final static PrintStream err
③ 标准输入(键盘):public final static InputStream in
3.1 系统输出
系统输出一共有两个常量:out
和err
,并且这两个常量表示的都是PrintStream
类的对象:
① out
输出的是希望用户能看到的内容
② err
输出的是不希望用户看到的内容
package com.file;
public class SystemIO {
public static void main(String[] args) {
try {
//String -> Integer
Integer.parseInt("abc");
} catch(NumberFormatException e) {
System.out.println(e.getMessage());
System.err.println(e.getMessage());
}
}
}
但是这两种输出在实际的开发之中都没用了,取而代之的是 “ 日志 ” 。
-
System.err
只是作为一个保留的属性而存在,现在几乎用不到。唯一可能用得到的就是System.out
。 -
由于
System.out
是PrintStream
的实例化对象,而PrintStream
又是OutputStream
的子类,所以可以直接使用System.out
直接为OutputStream
实例化对象,也就是说如果要得到输出到控制台或屏幕的对象,选择System.out
。 -
使用
System.out
为OutputStream
实例化:
package com.file;
import java.io.*;
public class SystemIO {
public static void main(String[] args) {
//输出
PrintStream printStream = System.out;
//PrintStream是OutputStream的子类
OutputStream out = System.out;
try {
out.write("hello".getBytes());
} catch(IOException e) {
e.printStackTrace();
}
}
}
3.2 系统输入
System.in
对应的类型是InputStream
,而这种输入流指的是由用户通过键盘进行输入(用户输入)。java本身并没有直接的用户输入处理,如果要想实现这种操作,必须使用java.io
的模式来完成。
- 使用
System.in
实现数据输入:
package com.file;
import java.io.*;
public class SystemIO {
public static void main(String[] args) {
//输入
InputStream in = System.in;
try {
byte[] buff = new byte[5];
int len = in.read(buff);
System.out.println("读取了"+len+"字节, 内容是:"+new String(buff, 0, len));
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 以上的程序本身有一个致命的问题,核心点在于:开辟的字节数组长度固定,如果现在输入的长度超过了字节数组长度,那么只能够接收部分数据。这个时候是由于一次读取不完所造成的问题,所以此时最好的做法是引入内存操作流来进行控制,这些数据先保存在内存流中而后一次取出。
package com.file;
import java.io.*;
public class SystemIO {
public static void main(String[] args) {
InputStream in = System.in;
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
byte[] buff = new byte[5];
int len = -1;
while((len = in.read(buff)) != -1) {
out.write(buff, 0, len);
if(len < buff.length) {
//已经读到最后一批
break;
}
}
byte[] data = out.toByteArray();
System.out.println("读取了"+data.length+"字节, 内容是:"+new String(data));
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 至于为什么命名输入的只有8个字节,却显示读取了9个字节的问题,是因为输入完成之后在键盘上敲下回车,所有的键盘操作都是会被记录下来的。
现在虽然实现了键盘输入数据的功能,但是整体的实现逻辑过于混乱了,即java提供的System.in
并不好用,还要结合内存流来完成,导致复杂度很高。所以也就有了下面两种输入流的产生。
4. 两种输入流
4.1 BufferedReader类
BufferedReader
类属于一个缓冲的输入流,而且是一个字符流的操作对象。
1. 字节缓冲流:BufferedInputStream
2. 字符缓冲流:BufferedReader
之所以选择BufferedReader
类操作是因为在此类中提供有如下方法:
按行读取,回车换行:String readLine() throws IOException
- 利用
BufferedReader
实现键盘输入:
package com.file;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class Buffer {
public static void main(String[] args) {
//字节输入流
InputStream inputStream = System.in;
//字符输入流(字节流转字符流)
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
//缓冲的字符输入流
BufferedReader reader = new BufferedReader(inputStreamReader);
//交互式的反复输入
while(true) {
System.out.println("请输入名字:");
try {
String line = reader.readLine();
System.out.println(line);
if(line.equals("quit")) {
break;
}
} catch(IOException e) {
e.printStackTrace();
}
}
}
}
以上操作形式是java十多年前输入的标准格式,但是时过境迁,这个类也淹没在历史的潮流之中,被JDK1.5
提供的java.util.Scanner
类所取代。
4.2 java.util.Scanner类
Scanner
是一个专门进行输入流处理的程序类,利用这个类可以方便处理各种数据类型,同时也可以结合正则表达式进行各项处理,在这个类中主要关注以下方法:
① 判断是否有指定类型数据:public boolean hasNextXxx()
② 取得指定类型的数据:public 数据类型 nextXxx()
③ 定义分隔符:public Scanner useDelimiter(Pattern pattern)
- 使用
Scanner
实现数据输入:
package com.file;
import java.util.Scanner;
public class TestScanner {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入数据:");
if(scanner.hasNext()) {
System.out.println("输入的内容:"+scanner.next());
}
}
}
- 接收其他类型数据:
- 使用
Scanner
还可以接收各种数据类型,并且帮助用户减少转型处理。
package com.file;
import java.util.Scanner;
public class TestScanner {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入年龄:");
if(scanner.hasNextInt()) {
System.out.println("输入的内容是int");
} else {
System.out.println("输入的内容不是int");
}
}
}
- 对接收的数据类型使用正则表达式判断:
package com.file;
import java.util.Scanner;
public class TestScanner {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//正则表达式
System.out.println("请输入生日:");
if(scanner.hasNext("\\d{4}-\\d{2}-\\d{2}")) {
System.out.println("输入的内容:"+scanner.next());
} else {
System.out.println("输入的内容不是生日格式");
}
}
}
-
Scanner
读取文件操作: - 使用
Scanner
本身能够接收的是一个InputStream
对象,那么也就意味着可以接收任意输入流,例如:文件输入流,Scanner
完美的替代了BufferedReader
,而且更好的实现了InputStream
的操作。
package com.file;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Scanner;
public class TestScanner {
public static void main(String[] args) {
//读取文件(针对字符)
try(Scanner scanner = new Scanner(Paths.get("D:", "test", "data.txt"))) {
//比File.separater更方便
scanner.useDelimiter("\n");
while(scanner.hasNext()) {
System.out.println(scanner.next());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 总结:
PrintStream 解决的是 OutputStream 类的缺陷,BufferedReader 解决的是 InputStream 类的缺陷。而 Scanner 则解决的是 BufferedReader 类的缺陷(替换了BufferedReader类)。
5. 序列化与反序列化
- 序列化(Java Object -> byte[]):ObjectOutputStream
- 反序列化(byte[] -> Java Object):ObjectInputStream
5.1 序列化技术
概念:把Java对象变成byte
数组(二进制流),主要是用于网络传输。
- 一个类的实例化对象要能够进行序列化必须实现序列化(
Serializable
)接口! -
ObjectOutputStream
里面的方法有:
①ObjectOutputStream(OutputStream out)
②writeObject(Object obj)
,写对象
package com.serializable;
import java.io.*;
class Person implements Serializable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class TestSerializable {
public static void main(String[] args) {
Person person1 = new Person();
person1.setName("张三");
person1.setAge(22);
//序列化
//Java Object -> byte[]
//可以写到file或者byte[]里面
try(ByteArrayOutputStream stream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(stream)
) {
out.writeObject(person1);
out.flush();
//data对象变成的二进制流
byte[] data = stream.toByteArray();
System.out.println(new String(data));
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.1 反序列化技术
概念:把byte
数组转换成对象。
-
ObjectInputStream
里面的方法有:
①ObjectInputStream(InputStream in)
②readObject(Object obj)
,读对象
package com.serializable;
import java.io.*;
class Person implements Serializable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class TestSerializable {
public static void main(String[] args) {
Person person1 = new Person();
person1.setName("张三");
person1.setAge(22);
System.out.println("person1:"+person1);
//序列化
//Java Object -> byte[]
//可以写到file或者byte[]里面
try(ByteArrayOutputStream stream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(stream)
) {
out.writeObject(person1);
out.flush();
//data对象变成的二进制流
byte[] data = stream.toByteArray();
//反序列化
//byte[] -> java Object
try(ByteArrayInputStream instream = new ByteArrayInputStream(data);
ObjectInputStream in = new ObjectInputStream(instream)
) {
Object returnValue = in.readObject();
System.out.println(returnValue.getClass());
Person person2 = (Person) returnValue;
System.out.println("Person2:" + person2);
//前者Person1是通过实例化new出来的,在堆上分配出来的
//后者person2是通过二进制流变成的Java对象
System.out.println(person1 == person2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.3 通过网络传输
package com.serializable;
import java.io.*;
class Person implements Serializable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class TestSerializable {
public static void main(String[] args) {
Person person1 = new Person();
person1.setName("张三");
person1.setAge(22);
try(FileOutputStream stream = new FileOutputStream("D:" + File.separator + "test" + File.separator + "person.obj");
ObjectOutputStream out = new ObjectOutputStream(stream)
) {
out.writeObject(person1);
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
假设我把person.obj
复制粘贴到了D:\\test
目录下,相当于通过网络传输给了别人,那么别人如何打开这个文件并获取到其中的内容呢?
package com.serializable;
import java.io.*;
class Person implements Serializable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class TestSerializable {
public static void main(String[] args) {
try(FileInputStream stream = new FileInputStream("D:" + File.separator + "person.obj");
ObjectInputStream in = new ObjectInputStream(stream)
) {
Object returnValue = in.readObject();
Person person = (Person) returnValue;
System.out.println(person);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
5.4 transient关键字
Serializable
默认会将对象中所有属性进行序列化保存,如果现在某些属性(比如密码)不希望被保存了,那么就可以使用transient
关键字,即被transient修饰过的属性不再参与序列化。
package com.serializable;
import com.sun.corba.se.impl.orb.PropertyOnlyDataCollector;
import java.io.*;
class Person implements Serializable {
private String name;
private Integer age;
private transient String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", password='" + password + '\'' +
'}';
}
}
public class TestSerializable {
public static void main(String[] args) {
Person person1 = new Person();
person1.setName("李四");
person1.setAge(20);
person1.setPassword("abc123");
//序列化
try(FileOutputStream stream = new FileOutputStream("D:" + File.separator + "person.obj");
ObjectOutputStream out = new ObjectOutputStream(stream1)
) {
out.writeObject(person1);
out.flush();
//反序列化
try(FileInputStream inStream = new FileInputStream("D:" + File.separator + "person.obj");
ObjectInputStream in = new ObjectInputStream(inStream)
) {
Object returnValue = in.readObject();
Person person2 = (Person) returnValue;
System.out.println(person2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}