Java I/O流 深入浅出

学习感悟:
在学习流的时候,要先有一个整体结构,先认识什么是输入和输出,要明白输入输出是相对于谁?然后理解四个抽象流(抽象类基类),在根据不同类型的流进行分类,最后再去详细学习基本流,其实流的学习是需要技巧的,按照前面说的思想去学习,就会发现,流都是有共同点的,就可以依葫芦画瓢,一个个击破。

文件和目录

– 在Java语言里,文件(File)代表的是文件和目录
– 使用文件类File可以完成如下任务:

• 创建新文件对象,可以使用相对路径和绝对路径来创建文件。

• 不同系统的分隔符不一样的:
– Window系统分隔符:\,这就要求实际用的时候写成“\”
– Unix系统分隔符:/

• 构造方法:
– File(File parent, String child)
– File(String pathname)
– File(String parent, String child)

 使用文件类File可以完成如下任务:
删除文件
• public boolean delete()

获得文件信息
• String getName():获得文件或目录的名字
• String getPath():获得文件或目录的路径
• String getAbsolutePath():获得文件或目录的绝对路径
• String getParent():获得文件或目录的父目录

重命名
• boolean renameTo(File newName)把文件重命名为指定的文件路径
比如: file1.renameTo(file2)为例:要想保证返回true,需要file1在硬盘中是存在的,且file2不能在硬盘中存在。

 使用文件类File可以完成如下任务:
测试文件
• public boolean canRead():是否能读
• public boolean canWrite():是否可写
• public boolean exists():是否存在
• public boolean isFile():是不是文件
• public boolean isDirectory():是不是目录
• public boolean isHidden():是否隐藏

新建文件
• public boolean createNewFile() throws IOException

新建目录
• public boolean mkdir():新建单个文件
• public boolean mkdirs():可新建多层文件

伪代码演示:

import java.io.File;
……
//创建一个新文件
File f = new File("c:\\java_test\\test\\readme.txt");
//判断如果文件不存在就创建它
if(!f.exists())
    f.createNewFile();  
//删除文件
f.delete();
//查看已经存在的目录下的内容
File f1 = new File("c:/java_test/");
//获取文件和目录名字数组
String[] s1 = f1.list();
  for(String str : s1)
     System.out.println(str);
//创建多层目录
File f2 = new File("c:/a/b/c/d/e");
//f2.mkdirs();
//删除目录
f2.delete();
File file = new File(f2);
System.out.println("文件或目录是否存在:" +  file.exists());
System.out.println("是文件吗:" +  file.isFile());
System.out.println("是目录吗:" +  file.isDirectory());
System.out.println("名称:" +  file .getName());
System.out.println("路径: " + file.getPath());
System.out.println("绝对路径: " + file.getAbsolutePath());
System.out.println("最后修改时间:" + file.lastModified());    
System.out.println(“文件大小:+ file.length()+ “ 字节”); 	

经典案例:递归删除多层文件(点这里

【I/O流】

Input/Output:输入输出机制
输入机制:允许java程序获取外部设备的数据(磁盘,光盘,网络等)。
输出机制:保留java程序中的数据,输出到外部设备上(磁盘,光盘等)。
【可以看出,IO的入出是以java程序为第一人称的】

流的分类
通过不同的方法,可以对于进行分类。

1.按照功能来划分:
输入流:只能从中读取数据,而不能向其写入数据。
输出流:只能向其写入数据,而不能从中读取数据。

2.按照处理单元来划分
字节流和字符流操作的方式基本上完全相同。操作的数据单元不同:
字节流:操作的是8位的字节 InputStream/OutputStream 作为字节流的基类,操作的可以是图片、音频、work、ppt文件等,不能读取中文文字。
字符流:操作的是16位的字符 Reader/Writer 作为字符流的基类,操作的是纯文本文件,如.txt.java文件等,可以读取中文文字。

3.按照角色进行划分
节点流:可以直接从/向外部设备读取/写入数据的流,可以从一个特定的数据源(节点)读写数据(如:文件),称之为节点流。节点流也被称之为低级流。
处理流:对于已经存在的流进行了连接和封装,扩展了原来的读/写的功能。是“连接”在已经存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更强的读写能力。处理流也被称之为高级流。

java的io包当中包括40多个流,他们都有紧密的联系,和使用的规律,这些流都源于4个抽象基类。
InputStream / Reader :所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer :所有输出流的基本,前者是字节输出流,后者是字符输出流。
在这里插入图片描述
认识四个基本抽象类
大概看一下有什么方法就好了,你会发现,它们基本都是一样的。需要注意的是它们都是抽象类,不可以直接创建对象。
抽象基类

InputStream

• int read()
– 从输入流读取下一个数据字节。
– 返回 0 到 255 范围内的 int 字节值。
– 如果因已到达流末尾而没有可用的字节,则返回值 -1

• int read(byte[] b)
– 从输入流中读取一定数量的字节并将其存储在缓冲区数组 b 中
– 以整数形式返回实际读取的字节数

• int read(byte[] b, int off, int len)
– 将输入流中最多 len 个数据字节读入字节数组。
– 尝试读取多达 len 字节,但可能读取较少数量。
– 以整数形式返回实际读取的字节数

OutputStream

• void write(int b)
– 将指定的字节写入此输出流
• void write(byte[] b)
– 将 b.length 个字节从指定的字节数组写入此输出流
• void write(byte[] b, int off, int len)
– 将指定字节数组中从偏移量 off 开始的 len 个字节写入此输出流

Reader

• int read()
– 读取单个字符
• int read(char[] cbuf)
– 将字符读入数组
• int read(char[] cbuf, int off, int len)
– 将字符读入数组的某一部分

Writer

• void write(int c)
– 写入单个字符
• void write(char[] cbuf)
– 写入字符数组
• void write(char[] cbuf, int off, int len)
– 写入字符数组的某一部分
• void write(String str)
– 写入字符串。

流的操作步骤:
文件字节输入流读取文件内容的步骤:

  • 1.创建流对象
  • 2.创建一个缓存字节的容器数组
  • 3.定义一个变量,保存实际读取的字节数
  • 4.循环读取数据
  • 5.操作保存数据的数组
  • 6.关闭流
@Test
	public void testInputStream() {
    
    
		File file = new File("F:\\ img.jpg");
		File file1 = new File("F:\\ img2.jpg");

		InputStream is = null;
		OutputStream os = null;
		try {
    
    
//1.创建流对象
			is = new FileInputStream(file);

			os = new FileOutputStream(file1);
//2.创建一个缓存字节的容器数组
			// 字节流按照8位1个字节为单位来读取数据,byte类型是1个字节的
			// short 2字节;int 4字节;loog 8字节
			byte[] bt = new byte[1024];
			// 返回值:读取数据的实际数量(比如120B);返回为-1时,表示读取到文件尾部

//3.定义一个变量,保存实际读取的字节数
			int length;
			length = is.read(bt);
//4.循环读取数据
			while (length != -1) {
    
    
				//错误的写入文件的方法,在读取文件的时候,可能出现最后一次读到的没有1024B,好比是只有124B,
				//则会把数组中上次读到的125-1024的数据也和最后一次的124B的数据一起写到文件中,轻则导致多出部分数据,
				//复制文件会比原文件的大,重则可能导致文件出现错误。
				//os.write(bt);
//5.操作保存数据的数组
				os.write(bt, 0, length);
				length = is.read(bt);
			}
			System.out.println("图片复制成功...");
		} catch (FileNotFoundException e) {
    
    
			e.printStackTrace();
		} catch (IOException e) {
    
    
			e.printStackTrace();
		} finally {
    
    
//6.关闭流
			try {
    
    
				if (is != null) {
    
    
					is.close();
				}
				if (os != null) {
    
    
					os.close();
				}
			} catch (IOException e) {
    
    
				e.printStackTrace();
			}
		}
	}

文件字符输出流写入文件内容的步骤:

  • 1.选择流:创建流对象
  • 2.准备数据源,把数据源转换成字符数组类型
  • 3.通过流从文件当中读取数据
  • 4.通过流向文件当中写入数据
  • 5.关闭流
@Test
	public void testReader2() {
    
    
		// 字符输入流Reater;16位2个字节为单位来读取数据的,适合读取中文文档
		Reader reader = null;
		Writer writer = null;
		try {
    
    
//1.选择流:创建流对象
			reader = new FileReader(
					"F:\\a.txt");
			writer = new FileWriter("F:\\b.txt");
//2.准备数据源,把数据源转换成字符数组类型
			char[] ch = new char[1024];
//3.通过流从文件当中读取数据
			int length = reader.read(ch);
			while (length != -1) {
    
    
//4.通过流向文件当中写入数据
				//写的方法一
				//writer.write(ch, 0, length);
				//写的方法二
				writer.write(new String(ch,0,length));
				length = reader.read(ch);
			}
			System.out.println("复制成功...");

		} catch (FileNotFoundException e) {
    
    
			e.printStackTrace();
		} catch (IOException e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			try {
    
    
//5.关闭流
				if (reader != null) {
    
    
					reader.close();
				}
				if (writer != null) {
    
    
					writer.close();
				}
			} catch (IOException e) {
    
    
				e.printStackTrace();
			}
		}
	}

上面的代码充分体现了文件的读写操作,做了一个简单的复制文件功能,可以看到,字节流和字符流的操作基本一致,需要注意基类是抽象类,不可以直接new,所以使用了它们的子类,上面代码,操作的都是文件,所以使用了File(文件)流,理解了上面的代码,接下来的其它流就可以依葫芦画瓢,一个个击破,理解不同类型的流各自特点就能事半功倍了。这就是学习流的一些技巧,还等什么,一起往下学习吧~~~gogogo

注意:前面读写文件都是直接对磁盘上进行操作的,所以属于节点流;下面将认识处理流,读写是对于已经存在的流进行了连接和封装。

缓冲流

• 缓冲字节流
• BufferedInputStream
• BufferedOutputStream
• 缓冲字符流
• BufferedReader
• BufferedWriter

处理流内部包含了节点流,节点流决定了与其沟通的外部设备,而处理流则增加了其读写功能。

缓冲流的好处
• 缓冲流内部包含一个缓冲区域,默认8kb,每一次程序调用read方法其实都是从缓冲区域当中读取内容,如果读取失败就说明缓冲区域当中没有内容,那么就从数据源当中读取内容,然后会尽可能读取更多的字节放入到缓冲区域当中,最后缓冲区域当中的内容,会全部返回给程序。
• 从缓冲区读取数据会比直接从数据源读取数据的速度快,效率也更高,性能更好。

简单说
没有缓存区,那么每read一次,就会发送一次IO操作;有缓存区,第一次read时,会一下读取x个字节放入缓存区,然后后续的read都会从缓存中读取,当read到缓存区末尾时,会再次读取x个字节放入缓存区。
处理流处理数据和节点流处理数据的方法基本上完全相同。

伪代码(复制文件(二进制))演示:

InputStream in = new FileInputStream("pic.jpg");
	BufferedInputStream bis = new BufferedInputStream(in);
	OutputStream os = new FileOutputStream("c:/pic_bak.jpg");
	BufferedOutputStream bos = new BufferedOutputStream(os);
	byte[] b = new byte[1024];
	while((bis.read(b))>0){
    
    
		bos.write(b);
		bos.flush();//刷新流
	}
	for(byte t :b)
		System.out.print(t);
	bis.close();
	in.close();
	os.close();
	bos.close();

转换流

转换流作用:把字节流转换成字符流,可以解决出现的因为编码集和解码集造成的乱码问题。

  • InputStreamReader:

  • OutputStreamWriter:
    在这里插入图片描述

  • 编码:字符—–编码字符集——–》二进制

  • 解码:二进制—解码字符集———》字符

  • 在处理文件时,如果文件的字符格式和编译器处理格式不一样时,会出现乱码问题。比如文件字符格式GBK,而编译器是UTF-8格式,那么就会产生该问题。

  • 出现乱码问题的原因:

  • 1.编码和解码字符集不一致造成了乱码

  • 2.字节的缺失,长度的丢失

  • 大部分情况下,出现乱码问题是因为中国汉字,因为中国汉字在不同的字符编码当中占据的字节数不相同,但是都占据多个字节。

  • 而英文字母没有这个问题,因为英文字母在所有的字符编码当中都占据一个字节。
    InputStreamReader :转换输入流将字节输入流转换成字符输入流
    作用:为了防止文件使用字符输入流处理时出现乱码问题。

打印流(了解即可)

– PrintStream
– PrintWriter
– 都属于输出流,分别针对字节流和字符流
– 都有自动flush的功能

Print流-演示

OutputStream os = new FileOutputStream("c:/newFile.txt");
PrintStream ps = new PrintStream(os);
System.setOut(ps);
for(int i = 0 ;i<65535 ; i++){
    
    
		System.out.print((char)i);
		if((i%100)==0)
			System.out.println();
}
ps.close();

说明:在上面代码中改变了打印输出流的输出方式,System.out.println();语句的输出,讲不会在编辑器控制台打印输出,而是直接把内容打印到c:/newFile.txt文件中。

数据流(了解)

– 数据流提供了存取Java基本数据类型操作
– DataInputStream
– DataOutputStream
特点: 既能够保存数据本身,又能够保存数据类型(基本数据类型+String)
代码演示:

//写数据
	@Test
	public void testDataStream() throws IOException {
    
    
		
		//首先写入java基本数据类型,然后再读取
		OutputStream os=new FileOutputStream("F:\\mydata.dat");//也可以是.txt
		DataOutputStream dos=new DataOutputStream(os);
		
		dos.writeBoolean(false);
		dos.writeChar('小');
		dos.writeInt(100);
		dos.writeDouble(525.252);
		dos.writeUTF("你好呀小伙子~~");
		
		dos.close();//实际调用的是flush()方法
		os.close();
		System.out.println("完成任务...");
		
	}

	//读取数据
	@Test
	public void testDataStream1() throws IOException {
    
    
		
		File file=new File("F: \\mydata.dat");
		InputStream is=new FileInputStream(file);
		DataInputStream dis=new DataInputStream(is);
		
		//读取数据的时候,需要按照写入的顺序,依次读取
		boolean b = dis.readBoolean();
		char ch = dis.readChar();
		int i = dis.readInt();
		double d = dis.readDouble();
		String s = dis.readUTF();
		
		System.out.println(b);
		System.out.println(ch);
		System.out.println(i);
		System.out.println(d);
		System.out.println(s);
	}

对象流(序列化流)

– Object流提供了直接存取对象的操作
– ObjectInputStream
– ObjectOutputStream

对象序列化
– Serializable 接口,实现这个接口表示类可以序列化。
– transient 关键字,表示不参与序列化。
伪代码演示:

class Student implements Serializable {
    
    
	int id = 1234;
	String name = "张三";
	transient int age = 22;
	java.util.Date d = new java.util.Date();
}

Student s = new Student();
OutputStream fos = new FileOutputStream("obj.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);
		
oos.writeObject(s);
oos.flush();
oos.close();
InputStream is = new FileInputStream("obj.dat");
ObjectInputStream ois = new ObjectInputStream(is);
Student s1 = (Student)ois.readObject();
System.out.println(s1.id + " " + s1.name + " " + s.d);
ois.close();

对象的序列化版本: serialVersionUID

• serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过判断类的serialVersionUID来验证版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。
具体序列化的过程是这样的:序列化操作时会把系统当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会自动检测文件中的serialVersionUID,判断它是否与当前类中的serialVersionUID一致。如果一致说明序列化文件的版本与当前类的版本是一样的,可以反序列化成功,否则就失败。

对象的序列化: 类没有加入serialVersionUID
• 演示一:

– 步骤一:定义Person类,包含id和name两个属性和,其中没有声明serialVersionUID常量。
– 步骤二:使Person对象序列化。写入文件中
– 步骤三:序列化操作后,修改Person类的属性,添加age属性,修改构造器。
– 步骤四:执行反序列化操作,报错:java.io.InvalidClassException
– 因为类的属性发生变化了,版本无法匹配了,所以报错了。

• 演示二:

– 步骤一:为Person类加入serialVersionUID版本。
private static final long serialVersionUID = -5875948774444383406L;
– 步骤二:重新序列化。
– 步骤三:重复刚刚修改Person属性的操作,
– 步骤四:反序列化操作,就没有问题了。

结论就是:在实体类上最好加上serialVersionUID,避免出现因版本不同,带来的问题。

注意事项

  • 1。先序列化然后在反序列化,而且反序列化的顺序必须和序列化的顺序保持一致。

  • 2.并不是所有的对象都能够被序列化。只有实现了Serializable接口的类的对象才能够被序列化。对象当中并不是所有的属性都能够被序列化。transient 关键字修饰的属性,表示不参与序列化。

  • 对象序列化的主要用途:

  • 1.把对象转换成字节序列,保存到硬盘当中,持久化存储,通常保存为文件。

  • 2.在网络上传递的是对象的字节序列

NIO的介绍(了解)

 Java NIO (New IO, Non-Blocking IO)是从Java 1.4版本开始引入的一套新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。

 JavaAPI中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。

 java.nio.channels. Channel
—FileChannel:处理本地文件
--------SocketChannel: TCP网 络编程的客户端的Channel
---------------ServerSocketChannel:TCP网絡编程的服务器端Channel
---------------DatagramChannel: UDP网络编程中发送端和接收端Channel

 随着JDK 7的发布,Java对NIO进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为NIO.2。因为NIO提供的一些功能,NIO已经成为文件处理中越来越重要的部分。

 Path、Paths和Files核心API
 早期的Java只提供了一个File类来访问文件系统,但File类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出锴时仅返回失败,并不会提供异常信息。

 NIO. 2为了弥补这种不足,引入了Path接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path可 以看成是File类的升级版本。

 在以前IO操作都是这样写的:
import java.io.File;
File file = new File(“abc.txt");

 但在Java7中,我们可以这样写:
import java.nio.file.Path;
Import java.no.file.Paths;
Path path = Paths.get(“abc.txt");

第三方组件的使用

 Apache 提供的Commons-io的使用,复制jar包到项目中,就可以直接使用了。
需要的可以下载:

链接:https://pan.baidu.com/s/1q6ChFYG98CIFARV8jamF9Q
提取码:zheg

在这里插入图片描述
第三方组件已经帮我们完成了流的包装,在实际开发中更多的还是使用第三方组件来操作流,原因:1.这些组件一般都是大佬写的,已经进过无数次的试错和优化,可以放心使用;2.方便,可以提高开发速度。

那既然有第三方组件为什么还要学流呢?别人的东西虽然好,可是应不应该要了解一下别人是怎么实现的???话不多说,学就对了。

结束语

有不理解或者GTQ28写错的都可以留言哟~~~
希望多多交流,多多关注
白白laaaaa

猜你喜欢

转载自blog.csdn.net/zhangzhanbin/article/details/112393843
今日推荐