javaIO流详解
初识IO
什么是io流
io就是input和output,即输入和输出,是数据的无结构化传递
io流的作用
当我们做java开发时,需要在内存,磁盘,网络中传输数据时,可能要一次性要传输的数据很大,而我们的内存空间有限,无法完成大文件的批量传输,这时候我们就可以使用io流,io流传输数据就是像流水一样缓缓的流动传输。
io流的分类
- 从数据流向分类,可以分为输入流和输出流
- 从传输文件类型角度分类,可以分为字节流和字符流
从io操作对象的角度分析
io流读取实战
读取一个字节
public class FileInputStreamDemo {
public static void main(String[] args) {
try {
FileInputStream is = new FileInputStream("D:\\code\\test\\gupao\\socket-io\\src\\main\\java\\txt\\test.txt");
int i = 0;
i = is.read();
System.out.println((char)i);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
读取文件所有内容
public class FileInputStreamDemo {
public static void main(String[] args) {
try {
FileInputStream is = new FileInputStream("D:\\code\\test\\gupao\\socket-io\\src\\main\\java\\txt\\test.txt");
int len = 0;
byte bur[] = new byte[1024];
while ((len = is.read(bur)) != -1) {
System.out.println(new String(bur, 0, len));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
深入分析javaIO流
IO流的数据来源
- 磁盘
import java.io.FileInputStream;
public class FileInputStreamDemo {
public static void main(String[] args) throws Exception {
FileInputStream is = new FileInputStream("E:/test.txt");
int len = 0;
byte bur[] = new byte[1024];
while((len = is.read(bur)) != -1) {
System.out.println(new String(bur, 0, len));
}
}
}
- 内存
public class FileInputStreamDemo {
public static void main(String[] args) throws IOException {
String str = "Java IO 流";
ByteArrayInputStream is = new ByteArrayInputStream(str.getBytes());
int len = 0;
byte bur[] = new byte[1024];
while((len = is.read(bur)) != -1) {
System.out.println(new String(bur, 0, len));
}
}
}
- 键盘录入
public class FileInputStreamDemo
public static void main(String[] args) throws IOException {
InputStream is = System.in;
int len = 0;
byte bur[] = new byte[1024];
while((len = is.read(bur)) != -1) {
System.out.println(new String(bur, 0, len));
}
}
}
- 网络
服务器端
public class Server {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(8888);
System.out.println("[启动服务器......]");
Socket s = ss.accept();
System.out.println("[客服端:]" + s.getInetAddress().getHostAddress() + " 已连接到服务器");
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// 读取客户端发送来的消息
String msg = br.readLine();
System.out.println("[客户端:]" + msg);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
bw.write(msg + "\n");
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端
public class Client {
public static void main(String[] args) {
try {
Socket s = new Socket("127.0.0.1", 8888);
// 构建IO
InputStream is = s.getInputStream();
OutputStream os = s.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
// 向服务器端发送一条消息
bw.write("测试客户端和服务器端通信,服务器收到消息返回到客户端\n");
bw.flush();
// 读取服务器返回的消息
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg = br.readLine();
System.out.println("[服务器:]" + msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器执行结果
客户端执行结果
本地磁盘操作类file类
file类是java为了操作磁盘文件创建,删除,移动而创建的类。
file类的基本操作
// 通过给定的父抽象路径名和子路径名字符串创建一个新的File实例。
File(File parent, String child);
// 通过将给定路径名字符串转换成抽象路径名来创建一个新 File 实例。
File(String pathname);
// 根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
File(String parent, String child);
// 通过将给定的 file: URI 转换成一个抽象路径名来创建一个新的 File 实例。
File(URI uri);
递归打印文件名
public class FileUtil {
public static void main(String[] args) {
showDir(1, new File("D:\\Java"));
}
static void showDir(int indent, File file) {
for (int i = 0; i < indent; i++) {
System.out.print('-');
}
System.out.println(file.getName());
if(file.isDirectory()) {
File[] files = file.listFiles();
for (int i = 0; i < files.length; i++) {
showDir(indent + 4, files[i]);
}
}
}
}
文件字节输入输出流
public class InputStreamDemo {
public static void main(String[] args) throws Exception {
File file = new File("D:\\code\\test\\gupao\\socket-io\\src\\main\\java\\img\\清纯美女.webp");
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream("D:\\code\\test\\gupao\\socket-io\\src\\main\\java\\img\\清纯美女copy.webp");
int len = 0;
byte[] buffer = new byte[1024];
while ((len = fileInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, len);
}
fileInputStream.close();
fileOutputStream.close();
}
}
内存字节输入输出流
public class MemoryDemo {
static String str = "hello world!";
public static void main(String[] args) {
// 从内存中读取数据
ByteArrayInputStream inputStream = new ByteArrayInputStream(str.getBytes());
// 写入到内存中
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int len = 0;
while ((len = inputStream.read()) != -1) {
char c = (char) len;
outputStream.write(Character.toUpperCase(c));
}
System.out.println(outputStream.toString());
}
}
字节缓冲输入输出流
public class BufferedDemo {
public static void main(String[] args) {
try {
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("D:\\code\\test\\gupao\\socket-io\\src\\main\\java\\img\\清纯美女.webp"));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("D:\\code\\test\\gupao\\socket-io\\src\\main\\java\\img\\清纯美女bufferCopy.webp"));
int len = 0;
byte[] bytes = new byte[1024];
while((len = bufferedInputStream.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, len));
bufferedOutputStream.write(bytes, 0, len);
bufferedOutputStream.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
flush方法
flush这个方法在很早之前的网络传输阶段就有了,为了使数据传输不是直接从发送端一段段的到接收端,
这样很影响效率,于是出现了缓冲区这一个内存区域,发送数据先往缓冲区填,缓冲区填满了在往接收区填,但是有时候当我们的数据不足以填充缓冲区时,就会造成内存占用以及数据传输不全的问题,为了解决这个问题,我们需要手动调用flush方法来强制将缓冲区的数据刷到接收端。而实际上调用close从根本上也是调用flush方法。
public void close() throws IOException {
try (OutputStream ostream = out) {
flush();
}
}
序列化和反序列化
序列化和反序列化的概念
- 序列化是指将一个对象转换成字节文件的过程输出到磁盘中
- 序列化生成字节文件可以进行反序列化
- 反序列化是将磁盘中的字节码文件重新转换成对象的过程
- 一个平台中序列化生成的字节文件在另一个平台是可以反序列化成对象,因为其底层都是jvm操作,这就是java的优势,跨平台性和可移植性。
序列化和反序列化的核心关键字
- ObjectOutputStream:IO 类,包含序列化对象的方法,writeObject()
- ObjectInputStream:IO 类,包含反序列化对象的方法,readObject()
- 上面两个 IO 流类是高层次的数据库,需要借助文件流进行序列化与反序列化操作。
- Serializable ,接口,是一个标志性接口,标识可以在 JVM 中进行序列化,JVM 会为该类自动生成一个序列化版本号。参与序列化与反序列化的类必须实现 Serializable 接口。
- serialVersionUID,类属性,序列化版本号,用于给 JVM 区别同名类,没有提供版本号,JVM会默认提供序列化版本号。
- transient,关键字,当序列化时,不希望某些属性参与,则可以使用这个关键字标注该属性。
序列化和反序列化的过程
- 内存中的数据被拆分成一小段的编号,将这些编号按照一定的规则存入磁盘中,这个就是序列化的过程
- 将磁盘中的文件按照编号重新组合,生成对象,这个就是反序列化的过程。
应用实例
参与序列化与反序列化的java类
public class Student implements Serializable {
private String name;
private int age;
// 以下省略有参构造、无参构造、set、get、toString
}
参加序列化的类必须要实现Serializable接口
序列化操作
public static void main(String[] args) throws Exception {
// 创建 Java 对象
Student student = new Student("张三",22);
// 对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student"));
// 使用 writeObject 序列化对象
oos.writeObject(student);
// 刷新
oos.flush();
// 关闭流
oos.close();
}
反序列化操作
public static void main(String[] args) throws Exception {
// 对象输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student"));
// 使用 readObject() 反序列化
Object obj = ois.readObject();
// 使用对象
System.out.println(obj);
// 关闭流
ois.close();
}
序列化版本号的作用
- java序列化一个对象会在内存中生成一个字节文件,如果改java类指定了版本号,则生成的字节文件也会有对应的版本号,当内存中同类名的类有几个时,就可以通过版本号来进行区分
- 序列化对象时,如果类没有指定版本号,则jvm会自动生成一个版本号
- 当类没有指定版本号,但是后续修改了该类的源码,那么jvm又会给该类自动生成一个序列化版本号,如果在用以前序列化生成好的字节文件来进行反序列化就会抛出异常,因为这个时候根据序列化版本号已经找不到类了。
- jvm自动生成版本号的好处是,对于同名但是功能不同的类,可以生成版本号加以区分
- 手动添加版本号的好处是,对于以后一个类修改了源代码,不需要担心反序列化失败的问题
transient关键字的作用
使用transient修饰的字段,在序列化时,不会加入到字节文件中。
public class Student implements Serializable {
private static final Long serialVersionUID = 1L;
private String name;
private transient int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
序列化的好处和应用场景
- 序列化可以将java对象以二进制文件形式存储到磁盘中
- 如果需要将对象保存为文件或者保存到数据库可以用到序列化
- 对象在网络传输可以通过序列化加上反序列化的方式实现
序列化需要注意事项
- 序列化只能保存对象属性状态,不能保存对象方法
- 如果一个父类实现了serializable接口,其子类不需要显式实现serializable接口
- 如果一个类引用了另一个类,序列化的过程会把另一个类也进行序列化,前提是引用类也实现了序列化接口
- jvm会给没有定义序列号版本号的类自动生成版本号,但是如果类的源码改了,则生成新的序列号,用原来序列化生成的二进制文件反序列化再次生成该类对象是会报错的
- 序列化的类最好是不需要经常修改的类,如实体类