@[TOC] 目录
序言
关于这方面的使用一直比较模糊,java中的OutputStream、FileWriter、BufferWriter等对象的使用每次也是遇到了再现用现查,这次想借着博客的机会彻底弄清楚
字节和字符
数据类型定义
字节数据
来源包括二进制文件(如图片文件.jpg、音频文件.mp3、视频文件.mp4等)、网络传输中的原始字节数据等。
用途在于处理各种二进制格式的文件、进行网络通信中的数据传输(在不关心字符编码的情况下)等
字符数据
对于字符数据,在写入时通常是将字符串形式的文本内容直接提供给相应的字符流类的write方法。例如,writer.write(“Hello, World!”),这里的"Hello, World!"就是一个字符数据的字符串表示。
在读取字符数据时,也是以字符为单位进行处理,比如BufferedReader的readLine方法会返回读取到的一行字符数据(以字符串形式)。
支持的读写对象
读写字节数据对象
- FileInputStream
- FileOutputStream
- InputStream
- OutputStream
- BufferInputStream
- BufferOutputStream
读写字符数据对象
- FileReader
- FileWriter
- BufferedReader
- BufferedWriter
输入流使用场景
字节输入流
以BufferedOutputStream为例,字节缓冲输入流需要依赖于FileOutputStream对象,锚定要写入的文件。缓冲区帮助优化了IO交互的次数。可以在循环多次读取文件的时候,将读取到的内容存入缓冲区。然后缓冲区行将满的时候,完成一次磁盘到内存的调用
下面是代码示例。揭示了一个读取fileInputStream对象的BufferedInputReader的读取过程
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class BufferedInputStreamExample {
public static void main(String[] args) {
// 文件路径,可以根据实际情况修改
String filePath = "example.txt";
FileInputStream fileInputStream = null;
BufferedInputStream bufferedInputStream = null;
try {
// 创建FileInputStream对象,用于连接到要读取的文件
fileInputStream = new FileInputStream(filePath);
// 创建BufferedInputStream对象,将FileInputStream作为参数传入,以利用其缓冲功能提高读取效率
bufferedInputStream = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[1024];
int bytesRead;
// 循环读取文件内容,直到读取到文件末尾(返回值 -1 表示文件末尾)
while ((bytesRead = bufferedInputStream.read(buffer))!= -1) {
// 将读取到的字节数组转换为字符串并打印(这里假设读取的是文本文件,对于二进制文件可能需要其他处理方式)
System.out.print(new String(buffer, 0, bytesRead));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭流,释放资源。先关闭BufferedInputStream,再关闭FileInputStream
if (bufferedInputStream!= null) {
try {
bufferedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fileInputStream!= null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
字符输入流
字符输入流主要通过fileReader类的对象完成,通过fr读取文件,使用字符数组接收读取到的内容。并且输出或者写入其他文件中,下面是代码示例:
public static void main(String[] args) {
try {
File file1 = new File("/Users/wd/Desktop/didi.txt");
FileReader fileReader = new FileReader(file1);
char[] c=new char[1024]; //创建一个字符数组,由于长度未知,可暂时设定为1024,如不够,再尝试其他长度。
int length=0;
while((length=fileReader.read(c))!=-1)
{
String data=new String(c,0,length);
System.out.print(data);
}
fileReader.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
输出流使用场景
字节输出流的基类和子类信息框图如下
在网络中,我们做一些文件的传输、图片的上传下载,都是通过OutputStream完成的。枚举一些Outputstream的使用场景
- 图片存储到文件系统
- 网络字节流传输
- 浏览器页面响应导出txt文件
下面将分别对上述的几种场景予以介绍
图片存储到文件系统示例
public class ImageSavingExample {
public static void main(String[] args) {
try {
FileOutputStream fileOutputStream = new FileOutputStream("image.jpg");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
// 假设根据此方法获取图片的字节数据
byte[] imageData = getImageDataFromMemory();
bufferedOutputStream.write(imageData);
bufferedOutputStream.close();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
网络字节流传输示例
服务器监听某一个*ip: 端口 传输的数据,可以使用BufferedOutputStream作为接收的对象,通过outputStream.write(byte[])的方式,将数据记录到输出流,等待服务端根据输出流的数据进行接收和截取
public class ClientUploadFileExample {
public static void main(String[] args) {
Socket socket = null;
OutputStream outputStream = null;
BufferedOutputStream bufferedOutputStream = null;
try {
socket = new Socket("127.0.0.1", 8080);
outputStream = socket.getOutputStream();
bufferedOutputStream = new BufferedOutputStream(outputStream);
byte[] fileData = getFileDataFromMemory();
// 假设这个方法获取到了文件的字节数据
bufferedOutputStream.write(fileData);
bufferedOutputStream.close();
outputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
网络字节流传输示例
需要定义HttpServlet以及HttpResponse,引入浏览器的执行对象,使用HttpResponse
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
response.setHeader("Content-Disposition", "attachment; filename=\"example.txt\"");
response.setContentType("text/plain");
File file = new File("/example/ex.txt");
FileInputStream fileInputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream();
byte []bytesBuffer = new byte[10240];
int fileRead;
while((fileRead = fileInputStream.read(bytesBuffer)) != -1) {
outputStream.write(bytesBuffer, 0 ,fileRead);
}
fileInputStream.close();
outputStream.close();
}
输出流的几种方法
逐行写入txt文件
调用接口的时候根据返回的响应体List,写入数据
String file_path = "output.txt";
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file_path))) {
// 模拟接口返回的数据,这里使用一个字符串数组作为示例
String[] dataFromInterface = {"第一行数据", "第二行数据", "第三行数据"};
for (String line : dataFromInterface) {
// 逐行将数据写入文件
writer.write(line);
writer.newLine();
}
System.out.println("数据已成功写入文件:" + file_path);
} catch (IOException e) {
System.out.println("写入文件时出错:" + e.getMessage());
}
很多情况下我们不只是写入txt文件,如果有很多txt文件,可能不方便整理。有时我们也会将输出整理到一个zip压缩文件包中
写入第一个文件的压缩文件,需要一个File对象定义待写入的文件路径,zipEntry代表要添加的文件或目录,设置好其相关属性(如名称、路径等)。例如:
private static void addFileToZip(String fileName, ZipOutputStream zipOutputStream) throws IOException {
File file = new File(fileName);
ZipEntry zipEntry = new ZipEntry(file.getName());
zipOutputStream.putEntry(zipEntry);
try {
byte[] buffer = new byte[1024];
int bytesRead;
FileInputStream fileInputStream = new FileInputStream(file);
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
zipOutputStream.write(buffer, 0, bytesRead);
}
fileInputStream.close();
}
}
本示例中OutputStream用来作为写入的对象,因为写的是序列化的字节数据
首先将字符串数组写到一个的txt文件中,因为是字符型数据,使用BufferedWriter对象桥接上FileWriter给出,并且循环待写入的字符串数组,每写入一行就插入一个换行符号,通过**writer.newLine()**完成
private static void writeToTxtFile(String[] data, String file_path) {
BufferedWriter writer = new BufferedWriter(new FileWriter(file_path));
for(String str : data) {
writer.write(str);
writer.newLine();
}
}
下面给出完整版的代码
// 第二个接口返回的数据
String[] dataFromInterface2 = {"第一行数据2", "第二行数据2", "第三行数据2"};
// 临时文件目录,用于存放生成的1.txt和2.txt文件
String tempDir = "temp_files";
File tempDirFile = new File(tempDir);
if (!tempDirFile.exists()) {
tempDirFile.mkdirs();
}
try {
// 将第一个接口返回的数据写入1.txt
writeToTxtFile(dataFromInterface1, tempDir + "/1.txt");
// 将第二个接口返回的数据写入2.txt
writeToTxtFile(dataFromInterface2, tempDir + "/2.txt");
// 创建压缩文件输出流,指定压缩文件名为output.zip
ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream("output.zip"));
// 将1.txt添加到压缩文件中
addFileToZip(tempDir + "/1.txt", zipOutputStream);
// 将2.txt添加到压缩文件中
addFileToZip(tempDir + "/2.txt", zipOutputStream);
// 关闭压缩文件输出流
zipOutputStream.close();
logger.info("数据已成功压缩到output.zip文件中");
// 删除临时文件目录及文件
deleteTempFiles(tempDirFile);
} catch (IOException e) {
System.out.println("操作过程中出错:" + e.getMessage());
}
依赖Buffer的场景
当想要按照批量的字节数组统一写入,如果每个字节都要进行文件和磁盘的交互,可能会造成资源不必要的开销; 常见的做法是引入Buffer缓冲区对象,使用缓冲区收集一定的字节数据之后,统一写入文件或者输出流
write(byte[] b, int off, int len) 方法
个方法可以将字节数组 b 中从偏移量 off 开始,长度为 len 的字节数据写入到缓冲输出流中。常用于只需要写入字节数组中部分数据的情况,示例代码如下
public class WriteByteArrayPartExample {
public static void main(String[] args) {
String filePath = "example.txt";
byte[] data = {72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100}; // 对应 "Hello World" 的ASCII码值
try (FileOutputStream fos = new FileOutputStream(new File(filePath));
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
bos.write(data, 6, 5); // 从偏移量6开始,写入长度为5的字节数据,即 "World"
bos.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
close() 方法
关闭缓冲输出流,在关闭流的同时,它会先自动调用 flush() 方法,将缓冲区中剩余的数据写入到底层输出流中,然后释放与该流相关的系统资源。需要注意的是,从 Java 7 开始,可以使用 try-with-resources 语句来自动管理流资源的关闭
String filePath = "example.txt";
try (FileOutputStream fos = new FileOutputStream(new File(filePath));
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
byte[] data = {72, 101, 108, 108, 111};
os.write(data);
// 不需要手动调用close,try-with-resources会自动关闭流并调用flush
} catch (IOException e) {
e.printStackTrace();
}
}
这种写法的好处是不用显示地使用finally的代码块进行资源的关闭,资源的创建和关闭统一由try catch完成
flush() 方法
通过BufferedOutputStream对象调用flush实现缓冲区写入文件的功能
public class BufferedOutputStreamFlushExample {
public static void main(String[] args) {
String filePath = "example.txt";
try (FileOutputStream fos = new FileOutputStream(new File(filePath));
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
String data = "This is some sample data to write into the file.";
byte[] byteArray = data.getBytes();
bos.write(byteArray);
// 手动调用flush方法,确保缓冲区数据写入文件
bos.flush();
System.out.println("数据已写入文件");
} catch (IOException e) {
e.printStackTrace();
}
}
}
缓冲区的使用
ByteBuffer有几个可以支持输入输出流切换调用的api方法,下面将对其进行介绍
读写位置切换
写 -> 读
flip()
buffer.flip() 方法实际上执行了以下几个关键操作来完成模式切换:
设置限制(limit)位置
将 ByteBuffer 的 limit 设置为当前的 position 值。在写模式下,position 表示下一个要写入数据的位置(初始为 0,随着写入数据不断自增),调用 flip() 方法后,把 limit 设置为这个 position,意味着限制了读操作能够读取到的有效数据范围,也就是确定了可读数据的边界
重置位置(position)为 0
将 ByteBuffer 的 position 重置为 0,这是因为在读模式下,position 表示下一个要读取数据的位置,将其初始化为 0,就可以从缓冲区的开头开始读取数据了
清除标记(mark)(如果有)
如果之前在 ByteBuffer 上设置过 mark(可以通过 mark() 方法设置一个标记位置,方便后续通过 reset() 方法快速回到这个标记位置,常用于一些需要临时记录位置的复杂读写场景),调用 flip() 时会清除这个标记,将其设置为 -1,表示当前没有设置有效的标记。
通过一个例子,能够更好地理解这个flip方法的作用
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10); // 创建容量为10字节的ByteBuffer,初始处于写模式
// 写模式下写入数据
buffer.put((byte) 10);
buffer.put((byte) 20);
buffer.put((byte) 30);
System.out.println("写模式下,position: " + buffer.position() + ", limit: " + buffer.limit());
buffer.flip(); // 切换到读模式
System.out.println("读模式下,position: " + buffer.position() + ", limit: " + buffer.limit());
byte b = buffer.get();
System.out.println("读取一个字节后,position: " + buffer.position() + ", limit: " + buffer.limit());
byte[] readData = new byte[2];
buffer.get(readData);
System.out.println("再读取两个字节后,position: " + buffer.position() + ", limit: " + buffer.limit());
}
运行结果
读 -> 写
clear()
同flip的三条特性
- 设置限制(limit)位置
- 重置位置(position)为 0
- 清除标记(mark)(如果有)
compact()
除clear的三个作用外,还有一个 将未读数据复制到缓冲区开头
先确定当前读模式下还剩余多少未读的数据(通过 limit - position 计算得出),然后将这些未读的数据整体复制到缓冲区的起始位置
这样做的目的是保证之前写入但还没来得及读取的数据不会丢失,在后续重新写入新数据时依然可以继续处理这些数据。
示例代码
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10); // 创建容量为10字节的ByteBuffer,初始处于写模式
buffer.put((byte) 10);
buffer.put((byte) 20);
buffer.put((byte) 30);
buffer.flip(); // 切换到读模式
byte b = buffer.get();
byte[] readData = new byte[1];
buffer.get(readData);
buffer.compact(); // 使用compact方法切换回写模式
buffer.put((byte) 40);
buffer.put((byte) 50);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
}