Java学习笔记(六)——Java IO编程

一、IO基础

(一)IO简介

1 . IO是指Input和Output,分别代表输入和输出

(1)Input输入:从外部读取数据到内存

为什么我们要把数据读取到内存中才能够处理这些数据呢?
因为代码是在内存中运行的,数据也必须读取到内存中,最终的表示方式为byte数组、String字符串等。
从Java代码来看,输入,会将硬盘中的某个文件的内容,读取到内存中,并且以Java提供的某种数据形式来表示,这样后续的代码才能够处理文件内容。

(2)Output输出:把数据从内存输出到外部
  • 从Java代码来看,输出,是将内存中的Java数据格式,输出到某个位置,例如硬盘中的文件等等
(3)IO流:IO流是一种顺序读写数据的模式
  1. 数据是单向流动的(比如公路上的车辆单行线的流动)
  2. 最小字节是byte(字节流)
  3. 如果字符不是单字节表示的ASCII:
  • Java提供了Reader/Writer表示字符流
  • 字符流传输的最小数据单位是char
  • 字符流输出的byte取决于编码方式
(4)Reader/Writer本质上是能够自动解码的InputStream/OutputStream
  1. 如果数据源不是文本:InputStream
  2. 如果数据源是文本:Reader
(5)同步IO和异步IO
  1. 同步IO
  • 读写IO时代码等待数据返回后才继续执行后续代码
  • 代码编写简单,执行效率低
  • JDK提供的java.io是同步IO
  1. 异步IO
  • 读写IO时仅发出请求,然后立即执行后续代码
  • 代码编写复杂,执行效率高
  • JDK提供的java.nio是异步IO
  1. Java.IO接口
java.io------------------------
抽象类InputStreamOutputStreamReaderWriter
实现类FileInputStreamFileOutputStreamFileReaderFileWriter
  • Java IO流的接口和实现相分离:
    • 字节流接口:InputStream,OutputStream
    • 字符流接口:Reader,Writer

(二)File对象

1 . File的概念

  • java.io.File表示文件系统的一个文件或目录
  • 构造方法:File(String pathname)
    • pathname的表示形式与操作系统相关
//Linux
File f = new File("usr/bin/javac");
//Windows
File f = new File("C:\\Windows\\notepad.exe");
//绝对路径(从根目录开始的完整路径)
File f1 = new File("C:\\Windows\\notepad.exe");
//相对路径(相对于当前工作目录的路径)
File f2 = new File("sub\\javac");//当前目录为"C:\Docs",绝对目录为"C:\Docs\sub\javac"
//1个“.”表示当前目录,2个“..”表示上一级目录
File f2 = new File(".\\javac");//绝对目录为"C:\Docs\sub\javac";
File f3 = new File("..\\javac");//绝对目录为"C:\Docs\javac";

2 . 获取File对象的路径

  • File对象有3种形式的路径:
    • String getPath():获取路径
    • getAbsolutePath():返回绝对路径参数
    • getCanonicalPath():返回规范路径参数
File f = new File("...");
String path = f.getPath();//"..."
String apath = f.getAbsolutePath();//"C:\\workspace\\IOFile"
String cpath = f.getCanonicalPath();//"C:\\workspace"

3 . 判断File对象的类别

  • boolean isFile():判断是否是文件
  • boolean DirectoryFile():判断是否是目录
new File("C:\\Windows\\notepad.exe").isFile;//true
new File("C:\\Windows").isDirectoryFile;//true

4 . File文件的操作

如果识别一个File对象为文件,就可以调用以下方法:

  • canRead():是否允许读取该文件
  • canWrite():是否允许写入该文件
  • canExecute():是否允许运行该文件
  • length():获取文件大小
  • createNewFile():创建一个新文件
  • static createTempFile():创建一个临时文件
  • delete():删除该文件
  • deleteOnExit():在JVM退出时删除该文件
File f = new File("C:\\Windows\\notepad.exe");
f.canExecute();//true

File tmpFile = File.createTempFile("tmp-", ".txt");//创建临时文件:tmp-0001.txt
tmpFile.deleteOnExit();//在JVM退出时删除该文件

5 . File目录的操作

如果识别一个File对象为目录,就可以调用以下方法:

  • String[] list():列出目录下的文件和子目录名
  • File[] listFiles():列出目录下的文件和子目录名
  • File[] listFiles(FileFilter filter):用来过滤不需要的文件
  • File[] listFiles(FilenameFilter filter):用来过滤不需要的文件
  • mkdir():创建该目录
  • mkdirs():创建该目录,并在必要时将不存在的父目录也创建出来
  • delete():删除该目录
//返回所有.exe文件
File dir = new File("C:\\Windows");
File[] fs = dir.listFiles(new FilenameFilter(){
	public boolean accept(File dir, String name){
		return name.endsWith(".exe");
	}
}
File dir = new File("C:\\Sample\\test");
dir.mkdir();//C:\\Sample必须存在
dir.mkdirs();//如果C:\\Sample不存在那就自动创建C:\\Sample
dir.delete();//删除该目录
import java.io.File;
import java.io.IOException;

public class Main {

	public static void main(String[] args) throws IOException {
		File win = new File("C:\\Windows");
		System.out.println(win.isDirectory()); // true
		File notepad = new File("C:\\Windows\\notepad.exe");
		System.out.println(notepad.isFile()); // true
		File dir = new File("C:\\abc\\xyz");
		System.out.println(dir.mkdir()); // -> mkdirs
		File readme = new File("./src/readme.txt");
		System.out.println(readme.isFile()); // false
		System.out.println(readme.getAbsolutePath());
		System.out.println(readme.getCanonicalPath());
	}
}

二、Input和Output

(一)InputStream

1 . 什么是InputStream

java.io.InputStream定义了所有输入流的超类
(1)InputStream最重要的方法是抽象方法:

  • abstract int read()
    • 读取下一个字节,并返回字节(0~255)
    • 如果已读到末尾,则返回 -1

(2)InputStream拥有2个重载方法:

  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
  • int read(byte[], int off, int len):指定byte[]数组的偏移量和最大填充数
  • void close():关闭输入流

2 . 如何完整的读取InputStream的所有字节

编写代码:

public void readFile() throws IOException {
	//获取一个InputStream
	InputStream input = new FileIntputStream("src/readme.txt");
	int n;
	//通过while循环不断调用read()方法来返回下一个字节,当返回-1时退出循环
	while((n = input.read()) != -1) {
		System.out.println(n);
	}
	//最后关闭InputStream输入流
	input.close();
}

在上面这段代码中存在一个问题:当读取的过程中出现了IO错误,InputStream就无法正确关闭,资源就不能及时得到释放。所以需要改写成以下代码,无论IO错误是否出现,InputStream都能够正常关闭。

public void readFile() throws IOException {
	InputStream input = null;
		//获取一个InputStream
		input = new FileIntputStream("src/readme.txt");
		int n;
		//通过while循环不断调用read()方法来返回下一个字节,当返回-1时退出循环
		
		while((n = input.read()) != -1) {
			System.out.println(n);
		}
	//最后关闭InputStream输入流
	}finally {
		if(input != null {
			input.close();
		}
	}
}

JDK 1.7中的新增编写方法:

public void readFile() throws IOException {
	//编写一个try语句,让编译器自动关闭资源
	try (InputStream input = new FileInputStream("src/readme.txt")) {
		int n;
		while((n = input.read()) != -1) {
			System.out.println(n);
		}
	}//在此自动关闭InputStream
	input.close()
}

3 . 利用缓冲区获取多个字节

public void readFile() throws IOException {
	try(InputStream input = new FileInputStream("src/readme.txt")) {
		//定义一个byte[]数组作为缓冲区
		byte[] buffer = new byte[1000];
		//使用read()方法尽可能多的将字节读取到缓冲区,但是不会超过缓冲区的大小
		int n = input.read(buffer);
		//最后可以检查read()方法一共读取了多少字节
		System.out.println("read " + n + "bytes.");
	}
}

如果需要读取的文件较大,一次读取1000个字节还没有读完,这时就需要使用while循环:

public void readFile() throws IOException {
	try(InputStream input = new FileInputStream("src/readme.txt")) {
		//定义一个byte[]数组作为缓冲区
		byte[] buffer = new byte[1000];
		//使用read()方法尽可能多的将字节读取到缓冲区,但是不会超过缓冲区的大小
		int n = input.read(buffer);
		//使用while循环不断调用read()方法读取字节到缓冲区,当返回值为-1时退出while循环
		while((n = input.read(buffer)) != -1) {
			System.out.println("read " + n + "bytes.");
		}
	}
}

4 . read()方法是一个阻塞(blocking)方法

阻塞的意义:InputStream的代码是顺序执行的,在调用read()方法时,虽然其处理速度较慢,但是编译器必须等待read()返回字节后才能继续处理下一行代码

5 . FileInputStream

FileInputStream是InputStream的一个而实现类,可以从文件获取输入流。通常调用new FileInputStream传入文件路径,之后转型为InputStream就可以正常使用InputStream

try(InputStream input = new FileInputStream(("test.src")) {
}

6 . ByteArrayInputStream

ByteArrayInputStream可以在内存中模拟一个InputStream,其作用实际上是将一个byte[]数组转换为一个InputStream,可以在测试中用来构造InputStream。

byte[] data = {72, 101, 108, 108, 111, -28, -67, -96, -27, -91, -67 };
try(InputStream input = new ByteArrayInputStream(data)) {
	int n;
	while((n = input.read()) != -1) {
		System.out.println(n);
	}
}

(二)OutputStream

1 . 什么是OutputStream

java.io.OutputStream定义了所有输出流的超类

  • abstract write(int b):写入一个字节
  • void write(byte[] b):写入byte[]数组表示的所有字节
  • void write(byte[], int off, int len):写入byte[]数组指定范围的字节
  • void close():关闭输出流
  • void flush():将缓冲区的内容输出

2 . 将byte写入OutputStream

public void writeFile() throws IOException {
	try(OutputStream output = new FileOutputStream("out/readme.txt")) {
		output.write(72);// H
		output.write(101);// e
		output.write(108);// l
		output.write(108);// l
		output.write(111);// o
	}
	output.close()
}

3 . 利用重载方法一次写入多个字节

public void writeFile() throws IOException {
	try(OutputStream output = new FileOutputStream("out/readme.txt")) {
		byte[] b = "Hello".getBytes("UTF-8");
		output.write(b);
	}
	output.close()
}

4 . write()方法是一个阻塞(blocking)方法

阻塞的意义:OutputStream的代码是顺序执行的,在调用write()方法时,虽然其处理速度较慢,但是编译器必须等待write()返回字节后才能继续处理下一行代码

5 . FileOutputStream可以输出到文件

通过FileOutputStream可以输出到文件

try(OutputStream output = new FileOutputStream("test.out")) {
}

6 . ByteArrayOutputStream

ByteArrayOutputStream可以在内存中模拟一个OutputStream,其作用是作用于缓冲区,将任意的数据写入以后,调用toByteArray()来获得最终的byte[]数组

try(ByteArrayOutputStream output = new ByteArrayOutputStream()) {
	output.write("Hello ".getBytes("UTF-8"));
	output.write("World!".getBytes("UTF-8"));
	byte[] data = output.toByteArray();
}

Input/Output练习

FileInputStream可以从文件读入数据,FileOutputStream可以把数据写入文件。
如果我们一边从一个文件读取数据,一边把数据写入到另一个文件,就完成了文件的拷贝。
编写一个程序,接收两个命令行参数,分别表示源文件和目标文件, 然后用InputStream/OutputStream把源文件复制到目标文件。
复制后,请检查源文件和目标文件是否相同(文件长度相同,内容相同)。
分别用文本文件、图片文件和zip文件测试。


(三)Filter模式

1 . 子类爆炸

JDK提供的InputStream
(1)FileInputStream:从文件读取数据
(2)ServletInputStream:从HTTP请求读取数据
(3)Socket.getInputStream():从TCP连接读取数据
以FileInputStream为例:
提供附加功能的InputStream从FilterInputStream派生:
(1)添加缓冲功能:BufferedInputStream
(2)添加计算签名功能:DigestInputStream
(3)添加加密/解密功能:CipherInputStream
所以我们发现,如果要给InputStream添加各种附加功能,需要添加三种功能,至少需要三个子类,而三种功能的组合又需要更多的子类。如果出现了FileInputStream以外的InputStream,那么很快就会出现子类爆炸的情况。为了解决以来继承而导致子类数量失控的问题,JDK将InputStream分成两类:
(1)直接提供数据的InputStream:

  • FileInputStream
  • ByteArrayInputStream
  • ServletInputStream

(2)提供额外附加功能的InputStream:

  • BufferedInputStream(提供缓冲功能)
  • DigestInputStream(提供计算签名功能)
  • CipherInputStream(提供加密/解密功能)

2 . 组合InputStream

在使用InputStream时,需要根据情况进行组合使用:

  • 首先,必须拥有一个能够真正提供数据源的InputStream(例如FileInputStream,来源于某个文件)。
  • 之后,如果我们需要FileInputStream能够提供缓冲功能,以便提高读取的效率,这时就可以用BufferedInputStream来包装FileInputStream,得到的包装类型是BufferedInputStream,但是依然可以向上转型为InputStream。
  • 如果这个文件已经被GZIP压缩,我们希望直接读取解压缩的内容,可以再包装一个GZIPInputStream。
  • 无论我们进行多少次包装,得到的对象始终都是InputStream,直接使用InputStream来引用它,就可以正常的读取数据。
InputStream input = new GZIPInputStream;
	new BufferedInputStream(new BufferedInputStream(
		new FileInputStream("test.gz")));
  • 这种组合功能而非继承功能的模式称为Filter模式(或Decorator模式)。
  • Filter通过少量的类实现了各种功能的组合。

(四)操作ZIP

1 . ZipInputStream

(1)作用:

ZipInputStream是一种FilterInputStream
可以直接读取ZIP的内容
JarInputStream则是从ZipInputStream派生出来的:
InputStream

FilterInputStream

InflaterInputStream

ZipInputStream

JarInputStream

(2)ZipInputStream的基本用法:
//首先,传入一个ZipInputStream,通常传入FileInputStream
try(ZipInputStream zip = new ZipInputStream(...)) {
	ZipEntry entry = null;
	//循环调用getNextEntry()方法,直至返回null,表示ZIP流结束
	while ((entry = zip.getNextEntry()) != null) {
		//对于每个entry都表示一个压缩文件或目录//对于每个entry都表示一个压缩文件或目录
		String name = entry.getName();
		//如果是一个压缩文件,使用read()方法不断读取,直至返回-1
		if (!entry.isDirectory()) {
			int n;
			while ((n = zip.read()) != -1) {
				...
			}
		}
	}
}

2 . ZipOutputStream

(1)作用:

ZipOutputStream是一种FilterOutputStream

  • 可以直接写入ZIP的内容
(2)ZipOutputStream的基本用法:
//首先创建ZipOutputStream
try(ZipOutputStream output = new ZipOutputStream(...)) {
	File[] files = ...
	for (File file : files) {
		//在写入文件之前,调用putNextEntry()
		zip.putNextEntry(new Entry(file.getName())) ;
		//调用write()方法写入Byte数据
		zip.write(getFileDataAsBytes(file));
		//写入完毕后,调用closeEntry()表示结束文件的打包
		zip.closeEntry();
	}
}
配合FileInputStream和FileOutputStream就可以读写Zip文件

(五)classpath资源

Java存放.class的目录或jar包也可以包含任意其他类型的文件
从classpath读取文件可以避免不同环境下文件路径不一致的问题
Class对象的getResourceAsStream()可以从classpath读取资源:

try(InputStream input = getClass().getResourceAsStream("/default.properties")) {
    if (input != null) {
        // Read from classpath
    }
}

需要检查返回的InputStream是否为null

(六)序列化

1 . 什么是序列化

(1)序列化是指把一个Java对象变成二进制内容(byte[])

  • 序列化以后可以将byte[]保存到文件中
  • 序列化以后可以将byte[]传输到网络中

(2)Java对象实现序列化必须实现Serializable接口

  • Serializable接口没有任何定义方法(空接口)
  • 空接口被称为标记接口(Marker Interface)

2 . 反序列化

(1)反序列化是指把一个二进制内容(byte[])变成Java对象

  • 反序列化以后可以从文件读取byte[]并变为Java对象
  • 反序列化以后可以从网络读取byte[]并变为Java对象

(2)ObjectOutputStream负责把一个Java对象写入二进制流:

try (ObjectOutputStream output = new ObjectOutputStream(...)) {
	output.writeObject(new Person("Hello "));
	output.writeObject(new Person("World!"));
}

(3)ObjectInputStream负责从二进制流读取一个Java对象:

try(ObjectInputStream input = new ObjectInputStream(...)) {
	Object p1 = input.readObject();
	Person p2 = (Person) input.readObject();
}

(4)readObject()可能抛出的异常:

  • ClassNotFoundException:没有找到对应的Class
  • InvalidClassException:Class不匹配
    (5)反序列化的重要特点:
  • 反序列化由JVM直接构造出Java对象,不调用构造方法

注意:

  • 可设置serialVersionUID作为版本号(非必需)
  • Java的序列化机制仅适用于Java,如果需要与其他语言交换数据,必须使用通用的序列化方法,例如JSON

三、Reader和Writer

(一)Reader

1 . Reader与InputStream的区别

InputStreamReader
字节流,以byte为单位字符流,以char为单位
读取字节(-1,0~255):int read()读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b)读到字符数组:int read(char[] c)
int read(byte[] b, int offset, int len)int read(char[] b, int offset, int len)

2 . 构造方法

java.io.Reader是所有字符输入流的超类

  • int read()
    • 读取下一个字符,并返回字符(0~65535)
    • 如果已读到末尾,则返回-1
  • int read(char[] c):读取若干字符并填充到char[]数组,返回读取的字符数
  • int read(char[] b, int off, int len) :指定char[]数组的偏移量和最大填充数
  • void close():关闭Reader输入流

3 . 完整读取Reader的所有字符

public void readFile() throws IOException {
	Reader reader = null;
	try {
		reader = new FileReader("readme.txt");
		int n;
     	while ((n = reader.read()) != -1) {
		 	System.out.println((char)n);
		}
	} finally {
		if (reader != null) {
			reader.close();
		}
	}
}

JDK 1.7新增的编写方法:

public void readFile() throws IOException {
	try (Reader reader = new FileReader("readme.txt")) {
		int n;
     	while ((n = reader.read()) != -1) {
		 	System.out.println((char)n);
		}
	}//在此自动关闭Reader
}

4 . 利用缓冲区一次读取多个字符

public void readFile() throws IOException {
	try (Reader reader = new FileReader("readme.txt")) {
	char[] buffer = new char[1000];
	int n = reader.read(buffer);
	System.out.println("read " + n + " chars.");
}

5 . read()是阻塞方法

6 . FileReader

  • FileReader可以从文件获取Reader对象
try (Reader reader = new FileReader("readme.txt")) {
	//字符编码跟随系统默认编码
}
  • FileReader使用系统默认编码,无法指定编码
  • 可以通过InputStreamReader指定编码

7 . CharArrayReader

CharArrayReader可以在内存中模拟一个Reader

char[] data = {'Z', 'E', 'R', 'O' };
try (Reader reader = new CharArrayReader(data) {
	int n;
	while ((n = reader.read()) != -1) {
		System.out.println((char)n);
	}
}

8 . Reader与InputStream的关系

Reader是基于InputStream构造的

  • FileReader内部持有一个FileInputStream
  • Reader可以通过InputStream构造
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

public class Main {

	public static void main(String[] args) throws IOException {
		try (Reader reader = new FileReader("readme.txt")) {
			int n;
			while ((n = reader.read()) != -1) {
				System.out.println((char) n);
			}
		}
	}
}

(二)Writer

1 . Writer与OutputStream的区别

OutputStreamWriter
字节流,以byte为单位字符流,以char为单位
写入字节(0~255):void write(int b)写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b)写入字符数组:void write(char[] c)
void write(byte[] b, int offset, int len)void write(char[] c, int offset, int len)
void write(String s)

2 . 构造方法

java.io.Writer是所有字符输出流的超类

  • void write(int c):写入一个字符(0~65535)
  • void write(char[] c):写入字符数组的所有字符
  • void write(char[] c, int offset, int len):写入字符数组的指定范围的字符
  • void write(String s):写入String表示的所有字符

3 . 写入字符

public void writeFile() throws IOException {
	try (Writer writer = new FileWriter("readme.txt")) {
		wtiter.write(65);
	}//在此自动关闭Writer
}

4 . 写入多个字符

public void writeFile() throws IOException {
	try (Writer writer = new FileWriter("readme.txt")) {
		wtiter.write("Hello " .toCharArray());
		wtiter.write("World ");
	}//在此自动关闭Writer
}

5 . write()是阻塞方法

6 . FileWriter

FileWriter可以从文件获取Writer

try (Writer writer = new FileWriter("readme.txt")) {
	//字符编码跟随系统默认编码
}

7 . CharArrayWriter

CharArrayWriter可以在内存中模拟一个Writer

try (Writer writer = new CharArrayWriter()) {
	wtiter.write(65);
	wtiter.write(66);
	wtiter.write(67);
	char[] data = writer.toCharArray();// {"A", "B", "C"}
}

8 . Writer与OutputStream的关系

Writer是基于OutputStream构造的

  • FileWriter内部持有一个FileOutputStream
  • Writer可以OutputStream构造
OutputStream output = new FileOutputStream(filename);
Writer writer = new OutputStreamWriter(output, "UTF-8") {
	writer.close();
	//不用调用output.close()
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值