上一篇文章传送门:
Java复习之IO流(上)
上一篇文章中,我们在提到 IO 流的概念时说到 IO 流分为两大类,一个是字节流,我们已经在上一篇文章中做了复习,而本篇文章的 IO流复习就从另一大类字符流来开始。
什么是字符流
上一篇文章中我们说过字符流一般只能操作的是存文本数据,而字节流可以操作很多类型数据(文本,图片,音频,视频等)
- 字符流是可以直接读写字符的IO流
- 计算机中存储的是字节数据,字符流读取字符,就要先读取到字节数据, 然后转为字符。当要写出字符,则需要把字符转为字节再写出
FileReader 和FileWriter
上一篇文章中已经提到过字符流的父类是Reader 和Writer,但是他们都是接口,不能直接new出对象,当我们要操作文本中的字符,字符流中给我提供了两个类FileReader 和FileWriter,他们的操作和字节流的FileInputStream 和 FileOuputStream基本是一样的。
- 字符的输入与输出
/**
*
* @author 毛麒添
* FileReader 读取字符 操作的是项目根目录中给的文件
*/
public class Demo_FileReader {
public static void main(String[] args) {
FileReader fr=null;
try {
fr=new FileReader("aaa.txt");
int a;
while((a=fr.read())!=-1){
//FileReader。read()得到的是对应的码表值,将其强转就可以得到相应的字符
//汉字字符对应两个字节一般以负数形势表现,底层读取判定是负数则一次读取两个字节,也就解释了为什么可以读取一个字符
System.out.print((char)a);
}
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
*
* @author 毛麒添
* 输出字符 操作的是项目根目录中给的文件
*/
public class Demo_FileWriter {
public static void main(String[] args) {
try {
FileWriter fw=new FileWriter("bbb.txt");//如果要在末尾添加则,选择构造方法中有两个参数的,填入true
fw.write("我像风一样自由!!!!!!");
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
- 字符流的拷贝
/**
* @author 毛麒添
* 字符流的拷贝
*/
public class CharIo_copy {
public static void main(String[] args) {
FileReader fr=null;
FileWriter fw=null;
try {
fr=new FileReader("aaa.txt");
fw=new FileWriter("ccc.txt");//不存在则会自动创建
int a;
while((a=fr.read())!= -1){
fw.write(a);
}
//一定不能忘记关闭流,不关流,则输出文件中不会有内容,父类Writer中定义了缓存区,如果流不关闭,则输出内容还在缓冲区,不会出现输出字符
fr.close();
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
-
注意
-
什么情况下使用字符流
- 字符流也可以拷贝文本文件, 但不推荐使用其来拷贝文件. 因为字符流读取时会把字节转为字符, 写出时还要把字符转回字节,太过于耗费资。但是当程序需要读取一段文本, 或者需要写出一段文本的时候可以使用字符流(只读或者只写),读取的时候是按照字符的大小读取的,不会出现半个中文,写出的时候可以直接将字符串写出,不用转换为字节数组
-
字符流是否可以拷贝非纯文本的文件
- 答案当然是不可以拷贝非纯文本的文件。因为在读的时候会将字节转换为字符,在转换过程中,可能找不到对应的字符,就会用 “ ? ” 代替,写出的时候会将字符转换成字节写出去 如果是“?”,直接写出,这样写出之后的文件就乱了,也就是损坏了文件
-
与FileInputStream 和 FileOuputStream一样,字符流也可以定义字符数组进行拷贝和使用带缓冲的字符流来进行拷贝(BufferedReader和BufferedWriter )
/**
*
* @author 毛麒添
* 字符数组的拷贝
*/
public class Char_Arry_Copy {
public static void main(String[] args) throws IOException {
FileReader fr=new FileReader("aaa.txt");
FileWriter fw=new FileWriter("ccc.txt");
char[] arr=new char[1024];
int len;
while((len= fr.read(arr))!= -1){//将文件中的数据读取到字符数组中
fw.write(arr, 0, len); //将字符数组中的数据写入到文件中
}
fr.close();
fw.close();
}
}
/**
*
* @author 毛麒添
* 带缓冲区的字符流
*/
public class Char_Buffer {
public static void main(String[] args) throws IOException {
BufferedReader bf=new BufferedReader(new FileReader("aaa.txt"));
BufferedWriter bw=new BufferedWriter(new FileWriter("ccc.txt"));
int a;
while((a= bf.read())!= -1){
bw.write(a);
}
bf.close();
bw.close();
}
}
- readLine()和newLine()方法
- readLine()是BufferedReader的方法,它的作用读取一行字符(不包含换行符号)
- newLine()是BufferedWriter的方法,它的作用为输出一个跨平台的换行符号"\r\n"(该方法在mac Window linux三个平台通用)
- 下面来个小例子来对readLine()和newLine()两个方法应用
/**
*
* @author 毛麒添
* 将一个文本文档上的文本反转,第一行和倒数第一行交换,第二行和倒数第二行交换
*/
public class Reversal_text {
public static void main(String[] args) throws IOException {
//获取文本文档的字符输入输出流
BufferedReader bf=new BufferedReader(new FileReader("aaa.txt"));
BufferedWriter bw=new BufferedWriter(new FileWriter("ccc.txt"));
//创建一个String类型的List 保存读取的文本文档内容
ArrayList list=new ArrayList();
//将文本文档的内容保存到数组中
String a;
while((a=bf.readLine())!= null){//读取一行
list.add(a);
}
//反转遍历List写入文本文档中
for(int i=list.size()-1;i>=0;i--){
bw.write(list.get(i));
bw.newLine();//写入一行换行
}
//关流
bf.close();
bw.close();
}
}
运行结果:
转换流
有时候我们会对不同码表的文件读取并写入,比如读取一个文本文件的码表是UTF-8,而写出文件的码表是GBK,如果直接读取,怎输出的文件内容必定会乱码,因为UTF-8 一个字符是三字节,而GBK一个字符代表两字节,每次只写入只输出连个字节,必然乱码 如下程序
/**
*
* @author 毛麒添
* 字符转换
*/
public class char_transformIO {
public static void main(String[] args) throws IOException {
FileReader fr=new FileReader("utf-8.txt");
FileWriter fw=new FileWriter("gbk.txt");
int a;
while((a=fr.read()) !=-1){
fw.write(a);
}
fr.close();
fw.close();
}
}
运行结果:
- 遇到这种情况,我们该怎么解决呢,字符流已经给我提供好了转换流InputStreamReader和outpStreamReader,他可以指定输入输出文件的码表,保证读取和写入不会导致乱码
所以可以将上面的程序改写为:
/**
*
* @author 毛麒添
* 字符转换
*/
public class char_transformIO {
public static void main(String[] args) throws IOException {
BufferedReader be=
new BufferedReader(new InputStreamReader(new FileInputStream("utf-8.txt"),"UTF-8"));
BufferedWriter bw=
new BufferedWriter(new OutputStreamWriter(new FileOutputStream("gbk.txt"),"gbk"));
int a;
while((a=be.read()) !=-1){
bw.write(a);
}
be.close();
bw.close();
}
}
运行结果:
- 到这里,字符流基本也复习完成了,下面来几个字符流的例子对我们所学的知识做个小结:
/**
*
* @author 毛麒添
* 获取一个文本上每个字符出现的次数,将结果写在times.txt上
*/
public class CharTest {
public static void main(String[] args) throws IOException {
//获取字符输入流
BufferedReader br=new BufferedReader(new FileReader("times.txt")) ;
//定义个双列集合 以键的形式存放文字中出现的字符 值为字符在文本中出现的次数
TreeMap treeMap=new TreeMap();
//读取aaa.txt中的字符 并存入集合中
int a;
while((a=br.read())!=-1){
char b= (char) a;//强制类型转换
//集合中没有该字符 值为1,有该字符 值加一
treeMap.put(b,!treeMap.containsKey(b)? 1:treeMap.get(b)+1);
}
br.close();
//获取输出流对象
BufferedWriter bw=new BufferedWriter(new FileWriter("times.txt")) ;
//遍历集合,写入文件
for (Character key : treeMap.keySet()) {
//对特殊字符做特殊处理
switch (key) {
case '\t'://跳格
bw.write("跳格出现的次数 ="+treeMap.get(key));
break;
case '\n'://换行
bw.write("换行出现的次数 ="+treeMap.get(key));
break;
case '\r'://回车
bw.write("回车出现的次数 ="+treeMap.get(key));
break;
default://直接写出字符
bw.write(key+"="+treeMap.get(key));
break;
}
bw.newLine();
}
bw.close();
}
}
运行结果:aaa.txt可以查看上文,
/**
*
* @author 毛麒添
* 当我们下载一个试用版软件,没有购买正版的时候,每执行一次就会提醒我们还有多少次使用机会用学过的IO流知识,模拟试用版软件,
* 试用5次机会,执行一次就提示一次您还有几次机会,如果次数到了提示请购买正版
*/
public class Probation {
public static void main(String[] args) throws IOException {
//创建带缓冲的输入流对象,因为要使用readLine方法,可以保证数据的原样性
BufferedReader br = new BufferedReader(new FileReader("config.txt"));
//将读到的字符串转换为int数
String line = br.readLine();//读取一行保证读取的是完整的字符串
int times = Integer.parseInt(line); //将数字字符串转换为数字
//对int数进行判断,如果大于0,就将其--写回去,如果不大于0,就提示请购买正版
if(times > 0) {
//在if判断中要将--的结果打印,并将结果通过输出流写到文件上
System.out.println("软件开始试用,您还有" + times-- + "次机会");
FileWriter fw = new FileWriter("config.txt");
fw.write(times + "");
fw.close();
}else {
System.out.println("您的试用次数已到,请购买正版");
}
//关流
br.close();
}
}
运行结果:
程序运行5次后显示结果
到这里,IO流中的字符流和字节流基本上复习完了,但是IO流中不只有字节流和字符流,还有其他的一些不常用的,我们也来复习一些。
- 序列流(SequenceInputStream)
- 什么是序列流
- 序列流可以把多个字节输入流整合成一个, 从序列流中读取数据时, 将从被整合的第一个流开始读, 读完一个之后继续读第二个, 以此类推.
- 为什么要有序列流
可以先看看下面的例子,假设我们要读取两个文件中的内容,写入到一个文件中,我们可以这样做:
- 什么是序列流
/**
*
* @author 毛麒添
* 读取两个文件中的数据整合写入到一个问价中
*/
public class SequenceIo {
public static void main(String[] args) throws IOException {
FileInputStream fis1=new FileInputStream("a.txt");
FileInputStream fis2=new FileInputStream("b.txt");
FileOutputStream fos=new FileOutputStream("c.txt");
int a;
while((a=fis1.read())!= -1){
fos.write(a);
}
int b;
while((b=fis2.read())!= -1){
fos.write(b);
}
fis1.close();
fis2.close();
fos.close();
}
}
在例子中我们可以看到,读取流的操作重复了两次,代码复用性差,如果使用序列流,则可以简化操作,序列流的构造方法可以传入两个输入流(SequenceInputStream(InputStream is1, InputStream is2)),把上面的例子改写为
/**
*
* @author 毛麒添
* 读取两个文件中的数据整合写入到一个问价中
*/
public class SequenceIo {
public static void main(String[] args) throws IOException {
SequenceInputStream sqi=new SequenceInputStream(new FileInputStream("a.txt"), new FileInputStream("b.txt"));
FileOutputStream fos=new FileOutputStream("c.txt");
int a;
while((a=sqi.read())!= -1){
fos.write(a);
}
sqi.close();
fos.close();
}
}
- 这个序列流构造中只能传入两个输入流,在我们的开发中显然是不够的,当我们要整合多个文件的时候,比如讲几个MP3音乐整合成一个串烧,其实序列流也是可以做的,因为他还有另外一个构造方法,SequenceInputStream(Enumeration),构造方法中传入的是枚举对象,下面我们来试试
/**
* @author 毛麒添
* 使用序列流整合多个输入流对象输出到一起
*/
public class SequenceEnum {
public static void main(String[] args) throws IOException {
//创建vector集合对象
Vector ve= new Vector();
ve.add(new FileInputStream("a.txt"));
ve.add(new FileInputStream("b.txt"));
ve.add(new FileInputStream("d.txt"));
Enumeration elements = ve.elements();
SequenceInputStream sq=new SequenceInputStream(elements);
FileOutputStream fos=new FileOutputStream("c,txt");
int a;
while((a =sq.read())!= -1){
fos.write(a);
}
sq.close();
fos.close();
}
}
-
内存输出流(ByteArrayOutputStream)
- 什么是内存输出流
- 该输出流可以向内存中写数据, 把内存当作一个缓冲区, 写出之后可以一次性获取出所有写入内存的数据
- 当我们使用字节流读取文本中的字符,如果使用不同的编码,则输出有可能出现乱码,这时的解决办法就可以有两个;一个为前面说过的使用字符流来指定码表,防止乱码;;另一个就是现在所说的字符流,先将数据全部写入内存中,再一次全部读出,就不会出现乱码。
- 下面我们来看一个小例子来使用一下内存输出流
- 什么是内存输出流
/**
*
* @author 毛麒添
* 定义一个文件输入流,调用read(byte[] b)方法,将a.txt文件中的内容打印出来(byte数组大小限制为5)
*/
public class ByteArrayIO {
public static void main(String[] args) throws IOException {
FileInputStream fis=new FileInputStream("a.txt");
ByteArrayOutputStream bos=new ByteArrayOutputStream();
byte[] arr=new byte[5];
int len;
while((len=fis.read(arr))!=-1){//将文件上的数据读到字节数组中
bos.write(arr,0,len);//将字节数组的数据写到内存缓冲区中
}
System.out.println(bos);
fis.close();//内存输出流只是对内存的操作,并没有建立流管道,所以不需要关流
}
}
- 对象操作流(ObjectInputStream和ObjecOutputStream)
- 什么是对象操作流
- 该流可以将一个对象写出, 或者读取一个对象到程序中. 也就是执行了序列化和反序列化的操作.
- 使用方式
- 写出: new ObjectOutputStream(OutputStream), writeObject()
- 读取: new ObjectInputStream(InputStream), readObject()
- 流的操作对象有可能是一个对象,该对象有自己的属性,比如当我们玩单机游戏的时候,游戏的存档可以看作是一个对象,该存档对象中的保存玩家的信息,玩家名称、进度等等,当我们读取存档和写入存档的时候,也就是对象操作流可以干的事(操作对象需要继承 Serializable 接口,写入的文件当我们查看数据是乱码的,但是读取数据可以保证是读取正确的,这里乱码是正常的,因为我们使用的对象操作输出流操作的是对象,但是写入的使用的FileOutputStream,相当于对象转换成字节数组,那对象在码表中肯定不可能一一对应,所以出现数据乱码,而对象操作了可以保证我么你下次读取的时候读取的数据不变),如果存取的数据改变了,但是又没有将改变后的数据写出,这时直接读取数据,则会发生报错,可以看到例子中我们定义了一个serialVersionUID,这就相当数据的版本号,当数据发生改变,我们可以改变这个版本号来标识数据的改变,而前面的说的报错信息中也会给我们指出是由于版本号不同导致的读取数据报错,下面我们来一个小例子来使用一下对象操作流。
- 什么是对象操作流
/**
*
* @author 毛麒添
* 对象操作流
*/
public class ObjectIO {
public static void main(String[] args) throws IOException, IOException, ClassNotFoundException {
//将对象存储在集合中写出
Person p1=new Person("mao",24);
Person p2=new Person("huang",23);
Person p3=new Person("luo",23);
ArrayList list = new ArrayList<>();
list.add(p1);
list.add(p2);
list.add(p3);
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("File.txt"));
oos.writeObject(list);
System.out.println("存档保存成功");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("File.txt"));
ArrayList list1 = (ArrayList)ois.readObject(); //泛型在运行期会被擦除,索引运行期相当于没有泛型
System.out.println("存档读取成功,读取到的存档"); //想去掉黄色可以加注解 @SuppressWarnings("unchecked")
for (Person person : list1) {
System.out.println(person);
}
ois.close();
}
}
/**
*
* @author 毛麒添
* 玩家存档对象
*/
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Person(String name,int age){
this.age=age;
this.name=name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
运行结果:
- 标准输入输出流
- 什么是标准输入输出流
- System.in 是InputStream,标准输入流,,默认可以从键盘输入读取字节数据
- System.out是PrintStream,标准输出流, 默认可以向Console(控制台)中输出字符和字节数据
- 平时我们使用 System.out.println()输出数据到控制台,也可以把System.out叫做打印流,该流可以很方便的将对象的toString()结果输出,并且自动加上换行
- 打印流也可以这样写
- 什么是标准输入输出流
PrintStream ps = System.out;
ps.println(97); //其实底层用的是Integer.toString(x),将x转换为数字字符串打印
ps.println(new Person("张三", 23));
Person p = null;
ps.println(p);//如果是null,就返回null,如果不是null,就调用对象的toString()
//自动刷出: PrintWriter(OutputStream out, boolean autoFlush, String encoding)
PrintWriter pw = new PrintWriter(new FileOutputStream("g.txt"), true);
pw.write(97);
pw.print("大家好");
pw.println("你好"); //自动刷出,只针对的是println方法有效
//pw.close();因为不是建立管道对硬盘操作,所以可以不必使用关流的方法
- 修改标准输入输出流
- 修改输入流: System.setIn(InputStream)
- 修改输出流: System.setOut(PrintStream)
/**
*
* @author 毛麒添
* 修改标准输入输出流拷贝图片
*/
public class PrintIO {
public static void main(String[] args) throws IOException {
System.setIn(new FileInputStream("a.jpg"));
System.setOut(new PrintStream("copy.jpg"));
InputStream is = System.in; //获取标准输入流
PrintStream ps = System.out;//获取标准输出流
int len;
byte[] arr = new byte[1024 * 8];
while((len = is.read(arr)) != -1) {
ps.write(arr, 0, len);
}
is.close();
ps.close();
}
}
- 两种方式实现键盘录入(前面已经实现用流实现键盘录入数据)
方式一:
//BufferedReader的readLine方法读取一行,实现键盘录入
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line=br.readLine();
方式二(常用方式):
Scanner sc=new Scanner(System.in);
String nextLine = sc.nextLine();
- 随机访问流(RandomAccessFile)
- RandomAccessFile类不属于流,是Object类的子类。但它融合了InputStream和OutputStream的功能。
- 支持对随机访问文件的读取和写入。此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组(引用JAVA文档中的话)。
- 他的构造方法 RandomAccessFile(File file, String mode),其中的mode 解释引用文档中的截图
- 随机访问流的 seek() 方法,该方法相当于设置一个指针,可以在文件指定的位置进行读写,这样当我们在进行多线程下载的时候就可以分段设置下载位置,使用多线程来进行文件的下载,下面给出一个简单的多线程下载(断点下载)实现案例来进行说明:
/**
* @author 毛麒添
* java 实现多下载
*/
public class MuchTreadDown {
private static int threadCount=3;//开启线程数
private static int blockSize=0;//每个线程下载的大小
private static int runningThreadCount=0;//当前运行的线程数
//本地服务器地址 Tomcat搭建
private static String path="http://172.30.163.13:8080/NewsServer/XXX.exe";
public static void main(String[] args) {
try{
//访问服务器获取资源
URL url = new URL(path);
HttpURLConnection connection=(HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10*1000);
int code = connection.getResponseCode();
if(code== 200){
//1.获取资源的大小
int filelength = connection.getContentLength();
//2.在本地创建一个与本地资源文件同样大小的文件(占位)
RandomAccessFile randomAccessFile=new RandomAccessFile(new File(getFileName(path)),"rw");
randomAccessFile.setLength(filelength);
//3.分配每个线程下载文件的开始位置和结束位置
blockSize=filelength/threadCount;//计算出每个线程理论下载大小
for(int threadID=0;threadID
最后
到这里,IO流的复习就结束了,如果你看到这里,我相信你会对IO流的各个知识点有一个新的认识。这里套用一句人们常说的话,起房子地基打的牢,房子才能起得高,写代码也一样,基础知识就是我们的地基,只有基础打牢,我们才能给写出更好的程序。最后的最后,文章中如果有错误,请大家给我提出来,大家一起学习进步,如果觉得我的文章给予你帮助,也请给我一个喜欢。
本系列文章:
Java复习之集合框架
Java复习之IO流(上)
Java复习之IO流(下)
Java 复习之多线程