转换流出现的原因及思想
由于字节流操作中文不是特别方便,所以,Java就提供了转换流。为了能更好地操作中文数据,我们就需要学习一下字符流(字符流 = 字节流 + 编码表)了,这样就不可避免地要学习InputStreamReader和OutputStreamWriter这两个类了。
字节通向字符的桥梁
InputStreamReader概述
InputStreamReader是字节流通向字符流的桥梁,它使用指定的charset读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集(GBK)。
案例
如果我们想将内容为“你好”的cn.txt文本文件使用字符流将其读取出来,那么可以编写如下程序,代码如下所示。
package cn.liayun.charstream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class TransStreamDemo {
public static void main(String[] args) throws IOException {
/*
* 通过字符流读取中文数据
* 字符流 = 字节流 + 编码表
*/
//字节同向字符的桥梁,将读到的字节进行解码。
readCNText();
}
public static void readCNText() throws IOException {
//1,操作字节流的字符流对象,必须先有字节流
FileInputStream fis = new FileInputStream("tempfile\\cn.txt");
//2,建立字节向字符的桥梁
InputStreamReader isr = new InputStreamReader(fis);
int ch = isr.read();
System.out.println((char)ch);
int ch1 = isr.read();
System.out.println((char)ch1);
int ch2 = isr.read();
System.out.println(ch2);
isr.close();
}
}
运行以上程序,可发现Eclipse控制台打印如下,给出截图。
字符通向字节的桥梁
OutputStreamWriter概述
OutputStreamWriter是字符流通向字节流的桥梁,可使用指定的charset将要写入流中的字符编码成字节。它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。
通过API帮助文档,我们还能知道OutputStreamWriter写数据的方式:
方法 | 说明 |
---|---|
write(int c) | 写入一个字符 |
write(char[] cbuf) | 写入一个字符数组 |
write(char[] cbuf, int off, int len) | 写入一个字符数组的一部分 |
write(String str) | 写入一个字符串 |
write(String str, int off, int len) | 写入一个字符串的一部分 |
字符流操作要注意的问题:字符流数据没有直接进文件而是到了缓冲区,所以要刷新缓冲区。
案例
如果想要使用字符流往一个文本文件中写入中文数据,那么可以编写如下程序,代码如下所示。
package cn.liayun.charstream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class TransStreamDemo {
public static void main(String[] args) throws IOException {
/*
* 通过字符流读取中文数据
* 字符流 = 字节流 + 编码表
*/
//字符通向字节的桥梁
writeCNText();
}
public static void writeCNText() throws IOException {
//1,创建字节流对象
FileOutputStream fos = new FileOutputStream("tempfile\\GBK.txt");
//2,字符通向字节的桥梁
OutputStreamWriter osw = new OutputStreamWriter(fos);
//3,使用osw的write方法直接写中文字符串。写入数据时,会存储到缓冲区中,因为要查表。
osw.write("你好");
//4,需要刷新缓冲区,将数据弄到目的地中。
// osw.flush();
//5,关闭资源。flush刷新完,流可以继续使用;close刷新完,直接关闭,流结束了,无法再使用。
osw.close();
// osw.write(""); // java.io.IOException: Stream closed(流已关闭)
}
}
flush()和close()的区别:
- close()关闭流对象,但是先刷新一次缓冲区。关闭之后,流对象不可以继续再使用了;
- flush()仅仅刷新缓冲区,刷新之后,流对象还可以继续使用。
使用不同的编码表演示转化流
字符流的出现是为了方便操作字符,更重要的是加入了编码转换,通过两个转换流——InputStreamReader和OutputStreamWriter来完成,在两个对象进行构造的时候可以加入字符集。下面我就来重点讲讲这两个转换流的编码应用。可以将字符以指定编码格式存储,然后对文本数据指定编码格式来解读,指定编码表的动作由构造函数完成。这里再次提醒一点,由UTF-8编码,自然要通过UTF-8来解码;同理,由GBK来编码,自然也要通过GBK来解码,否则会出现乱码。下面我分四种情况来分别阐述之。
先用UTF-8编码,再通过UTF-8来解码
第一种情况,先用UTF-8编码,再通过UTF-8来解码。示例代码如下:
package cn.liayun.charstream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class TransStreamTest {
public static void main(String[] args) throws IOException {
// writeText();
readText();
}
public static void writeText() throws IOException {
FileOutputStream fos = new FileOutputStream("tempfile\\utf-8.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
osw.write("你好");
osw.close();
}
public static void readText() throws IOException {
FileInputStream fis = new FileInputStream("tempfile\\utf-8.txt");
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
char[] buf = new char[1024];
int len = isr.read(buf);
String content = new String(buf, 0, len);
System.out.println(content);
isr.close();
}
}
这样,输出结果为“你好”,显示正常,并无乱码。
先用UTF-8编码,再通过GBK来解码
第二种情况,先用UTF-8编码,再通过GBK来解码。示例代码如下:
package cn.liayun.charstream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class TransStreamTest {
public static void main(String[] args) throws IOException {
// writeText();
readText();
}
public static void writeText() throws IOException {
FileOutputStream fos = new FileOutputStream("tempfile\\utf-8.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
osw.write("你好");
osw.close();
}
public static void readText() throws IOException {
FileInputStream fis = new FileInputStream("tempfile\\utf-8.txt");
InputStreamReader isr = new InputStreamReader(fis, "GBK");
char[] buf = new char[1024];
int len = isr.read(buf);
String content = new String(buf, 0, len);
System.out.println(content);
isr.close();
}
}
这样一来,输出结果就为浣犲ソ
,出现乱码。其原因又是什么呢?不妨画张图来解释一下。
先用GBK编码,再通过GBK来解码
第三种情况,先用GBK编码,再通过GBK来解码。示例代码如下:
package cn.liayun.charstream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class TransStreamTest {
public static void main(String[] args) throws IOException {
// writeText();
readText();
}
public static void writeText() throws IOException {
FileOutputStream fos = new FileOutputStream("tempfile\\cn_gbk.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK");
osw.write("你好");
osw.close();
}
public static void readText() throws IOException {
FileInputStream fis = new FileInputStream("tempfile\\cn_gbk.txt");
InputStreamReader isr = new InputStreamReader(fis, "GBK");
char[] buf = new char[1024];
int len = isr.read(buf);
String content = new String(buf, 0, len);
System.out.println(content);
isr.close();
}
}
这样一来,输出结果为“你好”,显示正常,并无乱码。
先用GBK编码,再通过UTF-8来解码
最后看一下第四种情况,先用GBK编码,再通过UTF-8来解码。示例代码如下:
package cn.liayun.charstream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class TransStreamTest {
public static void main(String[] args) throws IOException {
// writeText();
readText();
}
public static void writeText() throws IOException {
FileOutputStream fos = new FileOutputStream("tempfile\\cn_gbk.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK");
osw.write("你好");
osw.close();
}
public static void readText() throws IOException {
FileInputStream fis = new FileInputStream("tempfile\\cn_gbk.txt");
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
char[] buf = new char[1024];
int len = isr.read(buf);
String content = new String(buf, 0, len);
System.out.println(content);
isr.close();
}
}
这样一来,输出结果就为??
,出现乱码。其原因又是什么呢?不妨画张图来解释一下。
转换流的子类——用于操作字符文件的便捷类
通过查阅API帮助文档可知,两个转换流——InputStreamReader和OutputStreamWriter分别有其对应的子类——FileReader和FileWriter,它俩是用于操作字符文件的便捷类,但是它俩有局限性,只能操作字符文件,而且是默认编码,如果不操作字符文件,而且编码不是默认的,那就需要使用转换流了。
下面用一个例子来演示一下它俩的使用。现在有这样一个需求:在硬盘上,创建一个文件并写入一些文字数据,然后再从文件中读取出内容。
package cn.liayun.charstream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class SubTransStreamDemo {
public static void main(String[] args) throws IOException {
/*
* 转换流的子类。
* 专门用于操作文本文件的流对象。
*/
// writeText();
readText();
}
public static void readText() throws IOException {
FileReader fr = new FileReader("tempfile\\fw.txt");
//以上这句代码等效于:
// FileInputStream fis = new FileInputStream("tempfile\\fw.txt");
// InputStreamReader isr = new InputStreamReader(fis);
int ch = 0;
while ((ch = fr.read()) != -1) {
System.out.println((char)ch);
}
fr.close();
}
public static void writeText() throws IOException {
//1,创建一个用于操作文件的字符输出流对象
FileWriter fw = new FileWriter("tempfile\\fw.txt");//内部使用了默认的码表,而且只能操作文件。
//以上这句代码等效于:
// FileOutputStream fos = new FileOutputStream("tempfile\\fw.txt");
// OutputStreamWriter osw = new OutputStreamWriter(fos);
fw.write("你好");
fw.close();
}
}
接下来就用转换流的子类来复制一个文本文件。
复制一个文本文件
之前我们就用字节流复制过一个文本文件,而现在就要用字符流来解决这个需求了。啥都不说了,直接给出示例代码,如下。
package cn.liayun.charstream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class SubTransStreamDemo2 {
public static void main(String[] args) throws IOException {
//复制一个文本文件。
copyText();
}
public static void copyText() throws IOException {
//1,明确数据源。要定义字符读取流和数据源关联
FileReader fr = new FileReader("IO流_2.txt");
//2,明确数据目的,定义字符输出流,创建存储数据的目的。
FileWriter fw = new FileWriter("tempfile\\copy_2.txt");
//3,创建缓冲区
char[] buf = new char[1024];
int len = 0;
while ((len = fr.read(buf)) != -1) {
fw.write(buf, 0, len);
}
fw.close();
fr.close();
}
}
字符缓冲流
字符流为了高效读写,也提供了对应的字符缓冲流。
- BufferedWriter:字符缓冲输出流;
- BufferedReader:字符缓冲输入流。
BufferedWriter
该字符缓冲输出流中提供了一个跨平台的换行符:newLine()。如果想要在硬盘上创建一个文件并写入一些文字数据的话,就可以使用该类了,代码应该类似于下:
public class CharStreamBufferDemo {
public static void main(String[] args) throws IOException {
/*
* 演示字符流的缓冲区。
* BufferedReader
* BufferedWriter
*/
writeTextByBuffer();
}
public static void writeTextByBuffer() throws IOException {
// 1、明确目的。
FileWriter fw = new FileWriter("tempfile\\bufw.txt");
// 2、创建缓冲区对象,明确要缓冲的流对象。
BufferedWriter bufw = new BufferedWriter(fw);
for (int i = 1; i <= 4; i++) {
bufw.write(i + "abc");
bufw.newLine();
bufw.flush();
}
bufw.close();
}
}
BufferedReader
该字符缓冲输入流提供了一个一次读一行的方法:readLine()。方便于对文本数据的获取,当返回null时,表示读取到文件末尾。注意,readLine()方法返回的时候只返回回车符之前的数据内容,并不返回回车符(行终止符)。
如果现在我们想要从硬盘的一个文本文件中读取内容,那么就能使用到该类了,代码应该类似于下:
public class CharStreamBufferDemo {
public static void main(String[] args) throws IOException {
/*
* 演示字符流的缓冲区。
* BufferedReader
* BufferedWriter
*/
readTextByBuffer();
}
public static void readTextByBuffer() throws IOException {
FileReader fr = new FileReader("tempfile\\bufw.txt");
BufferedReader bufr = new BufferedReader(fr);
String line = null;
while ((line = bufr.readLine()) != null) {
System.out.println(line);
}
bufr.close();
}
}
System类对IO的支持
- System.out对应的是标准输出设备,即控制台;
- System.in对应的是标准输入设备,即键盘。
现在有这样一个需求:通过键盘录入数据,当录入一行数据后,就将该行数据进行打印,如果录入的数据是“over”,那么录入停止。
package cn.liayun.test;
import java.io.IOException;
import java.io.InputStream;
public class ReadIn {
public static void main(String[] args) throws IOException {
StringBuilder sb = new StringBuilder();
InputStream in = System.in;
int ch = 0;
while ((ch = in.read()) != -1) {
if (ch == '\r') {
continue;
}
if (ch == '\n') {
String str = sb.toString();
if ("over".equals(str)) {
break;
}
System.out.println(str);
sb.delete(0, sb.length());
} else {
sb.append((char)ch);
}
}
}
}
通过以上的键盘录入一行数据并打印,发现其实就是读一行数据的原理,也就是readLine方法,那么能不能直接使用readLine方法来完成键盘录入的一行数据的读取呢?readLine方法是字符流BufferedReader类中的方法,而键盘录入的read方法是字节流InputStream的方法,那么能不能将字节流转成字符流,再使用字符流缓冲区的readLine方法呢?当然可以啦,此时就需要用到转换流了。
package cn.liayun.test;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
public class ReadIn2 {
public static void main(String[] args) throws IOException {
/*
//获取键盘录入
InputStream in = System.in;
//将字节流对象转成字符流对象,需要使用转换流——InputStreamReader
InputStreamReader isr = new InputStreamReader(in);
//为了提高效率,将字符流进行缓冲区技术高效操作,使用BufferedReader
BufferedReader bufr = new BufferedReader(isr);
*/
//简写格式,键盘录入的最常见写法
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
// PrintStream out = System.out;//屏幕输出
OutputStream out = System.out;
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(out));
String line = null;
while ((line = bufr.readLine()) != null) {
if ("over".equals(line)) {
break;
}
bufw.write(line);
bufw.newLine();
bufw.flush();
}
bufw.close();
bufr.close();
}
}
读取键盘录入(专业)
更专业地读取键盘录入的方式是下面这样的。
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
bufr.readLine();
而Scanner类就是流+正则表达式,它的方法都是按照某种规则在读取数据。
练习
接下来,我们就来做两个练习,以巩固之前学习到的知识点。
练习一
建立指定文件的清单文件,即将指定目录下(包含子目录)的指定文件的绝对路径写入到一个文件中,该文件就作为指定文件的清单文件。
分析:其实这个练习的绝大部分我们都已做完了,就只剩将指定文件的绝对路径写入到一个文件中了。如若不信,可以回头去看一下第三十九讲 递归这一章节。所以我们只须编写一个将集合中符合条件的文件对象的绝对路径写入到一个文件中的函数即可。
public static void write2File(List<File> list, File destFile) throws IOException {
// 1、需要一个流对象,既然是写入字符。
BufferedWriter bufw = null;
try {
bufw = new BufferedWriter(new FileWriter(destFile));
// 2、遍历集合
for (File file : list) {
bufw.write(file.getAbsolutePath());
bufw.newLine();
bufw.flush();
}
} finally {
if (bufw != null) {
try {
bufw.close();
} catch (IOException e) {
throw new RuntimeException("关闭失败");
}
}
}
}
最后,我们即可编写测试代码进行测试了。
public static void main(String[] args) {
File dir = new File("F:\\Java\\java_bxd");
List<File> list = fileList(dir, ".java");
/*
for (File file : list) {
System.out.println(file);
}
*/
// 将集合中符合条件的文件对象的绝对路径写入到一个文件中。
File destFile = new File("javalist.txt");
write2File(list, destFile);
}
练习二
键盘录入多名学生的信息,格式为姓名,数学成绩,语文成绩,英文成绩
,按总分由高到低,将学生的信息进行排列到文件中。
分析:
- 通过获取键盘录入的一行数据,并将该行中的信息取出封装成学生对象;
- 因为学生对象有很多,那么就需要存储,故使用到集合。因为要对学生的总分进行排序,所以可以使用TreeSet;
- 将集合的信息写入到一个文件中。
首先描述一个学生类,因为学生对象要存储到TreeSet集合中,所以要实现Comparable接口,学生对象也有可能存储到HashSet集合中,所以最好覆写hashCode和equals方法。
package cn.liayun.test.domain;
public class Student implements Comparable<Student> {
private String name;
private int ma, cn, en;
private int sum;
public Student() {
super();
}
public Student(String name, int ma, int cn, int en) {
super();
this.name = name;
this.ma = ma;
this.cn = cn;
this.en = en;
this.sum = ma + cn + en;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + sum;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (sum != other.sum)
return false;
return true;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getMa() {
return ma;
}
public void setMa(int ma) {
this.ma = ma;
}
public int getCn() {
return cn;
}
public void setCn(int cn) {
this.cn = cn;
}
public int getEn() {
return en;
}
public void setEn(int en) {
this.en = en;
}
public int getSum() {
return sum;
}
public void setSum(int sum) {
this.sum = sum;
}
@Override
public int compareTo(Student o) {
int temp = this.sum - o.sum;
return temp == 0 ? this.name.compareTo(o.name) : temp;
}
}
接着再定义一个可以操作学生对象的工具类。其中一个函数功能是通过获取键盘录入的一行数据,并将该行中的信息取出封装成学生对象,存储进集合;另一个函数功能是将集合的信息写入到一个文件中。
package cn.liayun.test.tool;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
import cn.liayun.test.domain.Student;
public class GetInfoTool {
/**
* 获取学生对象集合,按照学生对象的自然排序。
* @throws IOException
* @throws
*/
public static Set<Student> getStudents() throws IOException {
return getStudents(null);
}
/**
* 获取学生对象集合,按照指定的比较器排序。
* @throws IOException
* @throws
*/
public static Set<Student> getStudents(Comparator<Student> comp) throws IOException {
//1,键盘输入
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
//创建一个容器存储学生对象
Set<Student> set = null;
//如果比较器存在,就创建带有比较器的对象
if (comp != null) {
set = new TreeSet<Student>(comp);
} else {
set = new TreeSet<Student>();
}
//2,获取键盘录入的信息
String line = null;
while ((line = bufr.readLine()) != null) {
//键盘录入结束标记
if ("over".equals(line)) {
break;
}
//因为录入的数据是有规律的,可以通过指定的规则进行分割
String[] strs = line.split(",");
//将数组中的元素封装成对象
Student stu = new Student(strs[0], Integer.parseInt(strs[1]),
Integer.parseInt(strs[2]),
Integer.parseInt(strs[3]));
//将学生对象存储到集合中
set.add(stu);
}
//关闭键盘录入,须知如果后面不再使用键盘录入是可以关闭的,如果后面还要使用,就不要关闭,继续通过System.in就可以获取。
// bufr.close();
return set;
}
/**
* 将集合中的学生信息写入到文件中
* @throws IOException
*/
public static void write2File(Set<Student> set, File destFile) throws IOException {
BufferedWriter bufr = null;
try {
bufr = new BufferedWriter(new FileWriter(destFile));
//遍历集合
for (Student stu : set) {
bufr.write(stu.getName() + "\t" + stu.getSum());
bufr.newLine();
bufr.flush();
}
} finally {
if (bufr != null) {
try {
bufr.close();
} catch (IOException e) {
throw new RuntimeException("关闭失败");
}
}
}
}
}
最后,我们即可编写测试代码进行测试了。
package cn.liayun.test;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.Set;
import cn.liayun.test.domain.Student;
import cn.liayun.test.tool.GetInfoTool;
public class Test2 {
public static void main(String[] args) throws IOException {
/*
* 作业:键盘录入多名学生的信息,格式:姓名,数学成绩,语文成绩,英文成绩。
* 按总分由高到低,将学生的信息进行排列存储到文件中。
*
* 思路:
* 1,使用键盘录入技术
* 2,操作的是学生信息,信息很多,需要将信息封装成学生对象
* 3,总分由高到低,需要排序,需要对学生对象中的总分排序,需要将这多个学生对象进行容器存储。
* 哪个容器呢?TreeSet集合。
* 4,将容器中的学生对象的信息写入到文件中
*
*/
/*
aa,40,40,40
mm,10,10,10
pp,50,50,80
qq,90,90,10
zz,80,80,40
*/
//创建一个逆序的比较器
Comparator<Student> comp = Collections.reverseOrder();
//使用操作学生信息的工具类
Set<Student> set = GetInfoTool.getStudents(comp);
File destFile = new File("tempfile\\info.txt");
GetInfoTool.write2File(set, destFile);
}
}