文章目录
第十八章异常
1. 异常概述
1.1 什么是异常
程序执行过程中发生了不正常的情况,而这种不正常的情况叫做:异常
java语言是很完善的语言,提供了异常处理方式,以下程序执行过程中出现了不正常情况。
java把该异常信息打印输出到控制台,提供程序员参考.程序员看到异常信息之后,可以对程序进行修改让程序更加健壮
什么是异常: 程序执行过程中的不正常情况
异常的作用: 提高程序的健壮性
注意:语法错误并不是异常,语法错了编译都不能通过(但Java有提供编译时异常),不会生成字节码文件,根本不能运行;
1.2 常见的异常
默认情况下,出现异常时JVM默认的处理方式是中断程序执行,因此我们需要控制异常,当出现异常后进行相应修改,提供其他方案等操作,不要让程序中断执行;
-
空指针异常
java.lang.NullPointerException
String str=null; str.length();
-
数组下标越界异常
java.lang.ArrayIndexOutOfBoundsException
int[] arr={1,2,3}; System.out.println(arr[3]);
-
类型转换异常
java.lang.ClassCastException
public class ExceptionTest01 { public static void main(String[] args) { A a=new B(); C c=(C)a; } } class A{ } class B extends A{ } class C extends A{ }
-
算数异常:
java.lang.ArithmeticException
int i=1/0;
-
日期格式化异常:
java.text.ParseException
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date parse = sdf.parse("2000a10-24");
1.3 异常体系
Java程序运行过程中所发生的异常事件可分为两类:
- Error:表示严重错误,一般是JVM系统内部错误、资源耗尽等严重情况,无法通过代码来处理;
- Exception:表示异常,一般是由于编程不当导致的问题,可以通过Java代码来处理,使得程序依旧正常运行;
Tips:我们平常说的异常指的就是Exception;因为Exception可以通过代码来控制,而Error一般是系统内部问题,代码处理不了;
1.4异常分类
异常的分类是根据是在编译器检查异常还是在运行时检查异常;
- 编译时期异常:在编译时期就会检查该异常,如果没有处理异常,则编译失败;
- 运行时期异常:在运行时才出发异常,编译时不检测异常;
Tips:在Java中如果一个类直接继承与Exception,那么这个异常将是编译时异常;如果继承与RuntimeException,那么这个类是运行时异常。即使RuntimeException也继承与Exception;
编译时异常举例:
public class ExceptionTest02 {
public static void main(String[] args) {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
//编译时异常必须处理
Date date=sdf.parse("2000-02-18");
}
}
运行时异常举例:
//运行时异常不用处理
int i = 1 / 0;
2.异常处理
Java程序的执行过程中如出现异常,会自动生成一个异常类对象,该异常对象将被提交给Java运行时系统(JVM),这个过程称为抛出(throw)异常。如果一个方法内抛出异常,该异常会被抛到调用方法中。如果异常没有在调用方法中处理,它继续被抛给这个调用方法的调用者。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。
public class ExceptionTest {
public static void main(String[] args) {
/*
程序执行到此处发生了ArithmeticException异常,
底层new了一个ArithmeticException异常对象
然后抛出了,由于是main方法调用了100/0
所有这个异常ArithmeticException抛给了main方法,
main方法没有处理,将这个异常抛给了JVM
JVM最终终止程序
ArithmeticException继承了RuntimeException,属于运行时异常
在编写程序阶段不需要对这种异常进行预先的处理
*/
System.out.println(100/0);
//这里的hello world没有执行 输出
System.out.println("hello world");
}
}
异常处理有两种方式:
- 在方法声明的位置上,使用throws关键字,抛给上一级。谁调用我,我就抛给谁。抛给上一级。
- 使用try…catch语句进行异常的捕捉。这件事发生了,谁也不知道,因为我给抓住了。
举例:
是某集团的一个销售员,因为我的失误,导致公司损失了1000元,
“损失1000元”这可以看做是一个异常发生了。我有两种处理方式,
第一种方式:我把这件事告诉我的领导【异常上抛throws】
第二种方式:我自己掏腰包把这个钱补上。【异常的捕捉 try…catch】
在java中异常也是一个类我们可以去实例化异常
代码示例:
public class ExceptionTest {
public static void main(String[] args) {
//通过"异常类"实例化"异常对象"
NumberFormatException nfe=new NumberFormatException("数字格式化异常!");
//java.lang.NumberFormatException: 数字格式化异常!
System.out.println(nfe);
//通过"异常类"实例化"异常对象"
NullPointerException npe=new NullPointerException("空指针异常发生了");
//java.lang.NullPointerException: 空指针异常发生了
System.out.println(npe);
}
}
2.1异常的捕获
2.1.1语法
异常的捕获和处理需要采用 try 和 catch 来处理,具体格式如下:
try..catch(){}
try {
// 可能会出现异常的代码
} catch (Exception1 e) {
// 处理异常1
} catch (Exception2 e) {
// 处理异常2
} catch (ExceptionN e) {
// 处理异常N
}
示例代码:
public class ExceptionTest03 {
public static void main(String[] args) {
method();
System.out.println("程序结束啦~~");
}
public static void method(){
try {
String str=null;
System.out.println(str.length());
}catch (Exception e){//这里写异常的类型
System.out.println("执行代码出现了异常");
}
}
}
执行结果:
执行代码出现了异常
程序结束啦~~
注意:这里捕捉到了 下面的代号会继续执行
2.1.2 异常常用方法
在Throwable类中具备如下几个常用异常信息提示方法:
public void printStackTrace()
:获取异常的追踪信息;
包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。public String getMessage()
:异常的错误信息;
异常触发被抓捕时,异常的错误信息都被封装到了catch代码块中的Exception类中了,我可以通过该对象获取异常错误信息;
示例代码:
public class ExceptionTest03 {
public static void main(String[] args) {
method();
System.out.println("程序结束啦~~");
}
public static void method(){
try {
String str=null;
System.out.println(str.length());
}catch (Exception e){//这里写异常的类型
System.out.println("异常的错误信息"+e.getMessage());
//打印异常错误的跟踪信息
e.printStackTrace();
}
}
}
异常的追踪信息可以帮助我们追踪异常的调用链路,一步一步找出异常所涉及到的方法,在实际开发非常常用;
2.2.3 finally
- 在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常.
- finally子句必须和try一起出现,不能单独使用
finally语句通常使用在哪些情况下呢?
- 通常在finally语句块中完成资源的释放/关闭
- 因为finally中的代码比较有保障
- 即使try语句块中的代码出现了异常,finally中代码也会正常执行
代码示例:
public class ExceptionTest03 {
public static void main(String[] args) {
method();
System.out.println("程序结束啦~~");
}
public static void method(){
try {
String str=null;
System.out.println(str.length());
}catch (Exception e){//这里写异常的类型
System.out.println("执行代码出现了异常");
}finally {
System.out.println("这里的代码一定会执行,常常释放一些资源");
}
}
}
测试结果:
执行代码出现了异常
这里的代码一定会执行,常常释放一些资源
程序结束啦~~
注意点:
- try和finally,没有catch是可以的
- try不能单独使用
- try finally可以一起使用的
final finally finalize有什么区别
final关键字
final修饰的类无法继承
final修饰的方法无法覆盖
final修饰的变量不能重新赋值
finally 关键字
和try一起联合使用.
finally语句块中的代码是必须执行的
finalize标识符
是一个Object类中的方法名
这个方法是由垃圾回收期GC负责调用的
return和finally
public class finallyTest04 {
public static void main(String[] args) {
int result=m();
System.out.println("mian:"+result);//100
}
/*
java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!)
java中有一条这样的规则:
方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法)
java中还有一条语法规则:
return语句一旦执行,整个方法必须结束(亘古不变的)
*/
public static int m(){
int i=100;
try{
//这样代码出现在int i=100;下面,所以最终结果必须是返回100
//return语句还必须是最后执行的,一旦执行,整个方法结束
//return是最后执行
return i;
}finally {
i++;
System.out.println("m:"+i);
}
}
}
/*
反编译之后的效果
public static int m(){
int i=100;
int j=i;
i++;
return j;
}
*/
2.2.4 格式总结
-
try...catch(){}
:try { // 可能会出现异常的代码 } catch (Exception1 e) { // 处理异常1 } catch (Exception2 e) { // 处理异常2 } catch (ExceptionN e) { // 处理异常N }
Tips:后处理的异常必须是前面处理异常的父类异常; 也就是异常从上到下 越来越大
-
try..catch(){}...finally{}
try { // 可能会出现异常的代码 } catch (Exception1 e) { // 处理异常1 } catch (Exception2 e) { // 处理异常2 } catch (ExceptionN e) { // 处理异常N } finally { // 不管是否出现异常都会执行的代码 }
finally关键字:finally括号内部的代码一定会执行
-
try...finally{}
try { // 可能会出现异常的代码 } finally { // 不管是否出现异常都会执行的代码 }
2.2 异常的抛出(throw)
下方我们的案例就是用运行时异常,所以不用处理也可以
我们已经学习过出现异常该怎么抓捕了,有时候异常就当做提示信息一样,在调用者调用某个方法出现异常后及时针对性的进行处理,目前为止异常都是由JVM自行抛出,当然我们可以选择性的自己手动抛出某个异常;
Java提供了一个throw关键字,它用来抛出一个指定的异常对象;抛给上一级;
Tips:自己抛出的异常和JVM抛出的异常是一样的效果,都要进行处理,如果是自身抛出的异常一直未处理,最终抛给JVM时程序一样会终止执行;
语法格式:
throw new 异常类名(参数);
示例:
throw new NullPointerException("调用方法的对象是空的!");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
代码示例:
package com.dfbz.demo01;
public class Demo03 {
public static void main(String[] args) {
method(null);
System.out.println("我还会执行吗?");
}
public static void method(Object object) {
if (object == null) {
// 手动抛出异常(抛出异常后,后面的代码将不会被执行)
throw new NullPointerException("这个对象是空的!不能调用方法!");
}
System.out.println(object.toString());
}
}
在上方就说出,如果异常捕获了,下面的代码就会执行,如果没有捕获 下面的代码就不会执行就会终止
在方法method()中 出现了异常 处理方式为抛出 没有捕获 所以下面的代码不会执行
在main方法中 调用了method()方法 并且method()方法把异常抛给了 main 所以 main方法下面的代码也不会执行
try…catch进行捕获
package com.dfbz.demo01;
public class Demo03 {
public static void main(String[] args) {
try {
method(null);
} catch (Exception e) {
System.out.println("调用method方法出现异常了");
}
System.out.println("我还会执行吗?");
}
public static void method(Object object) {
if (object == null) {
// 手动抛出异常(抛出异常后,后面的代码将不会被执行)
throw new NullPointerException("这个对象是空的!不能调用方法!");
}
System.out.println("如果出现了异常我是不会执行了,你能执行到这里说明没有异常");
System.out.println(object.toString());
}
}
执行结果
调用method方法出现异常了
我还会执行吗?
2.4 异常的声明(throws)
2.4.1 运行时异常
在定义方法时,可以在方法上声明异常,用于提示调用者;
Java提供throws关键字来声明异常;关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常);
public class ExceptionTest {
public static void main(String[] args) {
// 可以处理,也可以不处理
method("你好~");
// 编译时异常必须处理
try {
method2("hello~");
} catch (ParseException e) {
System.out.println("出现异常啦!");
}
}
// 调用此方法可能会出现空指针异常,提示调用者处理异常
public static void method(Object obj) throws NullPointerException {
System.out.println(obj.toString());
}
// 抛出的不是运行时异常,调用者调用该方法时必须处理
public static void method2(Object obj) throws ParseException {
System.out.println(obj.toString());
}
// 也可以同时抛出多个异常
public static void method3(Object obj) throws ClassCastException,ArithmeticException {
System.out.println(obj.toString());
}
}
2.4.2 编译时异常
在声明和抛出异常时需要注意如下几点:
- 如果是抛出(throw)编译是异常,那么必须要处理,可以选择在方法上声明,或者try…catch处理
- 如果调用的方法上声明(throws)了编译时异常,那么在调用方法时就一定要处理这个异常,可以选择继续网上抛,也可以选择try…catch处理
// 这里我们只是用编译时异常来举例,运行时异常不用处理也可以
public class ExceptionTest04 {
public static void main(String[] args) {
//向上声明的 需要自己处理
try {
m1(2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// m2已经自己捕捉了 不用处理
m2(2);
}
// 但是 一般是往上抛(throw)的异常 建议不要自己捕捉 因为那样没有意义 所以大部分是往上声明
public static void m1(int num) throws ClassNotFoundException {
if (num==1){
//可以进行 声明
throw new ClassNotFoundException("自己定义的");
}
}
public static void m2(int num){
if (num==1){
//可以进行捕捉
try {
throw new ClassNotFoundException("自己定义的");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
3.自定义异常
我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是Java中没有定义好的,此时我们根据自己业务的异常情况来定义异常类。
我们前面提到过异常分类编译时异常和运行时异常:
- 继承于
java.lang.Exception
的类为编译时异常,编译时必须处理; - 继承于
java.lang.RuntimeException
的类为运行时异常,编译时可不处理;
步骤:
- 第一步:编写一个类继承Exception或者RuntimeException
- 第二步:提供两个构造方法,一个无参的,一个带有String参数的 然后super(s);
public class MyException extends Exception{
public MyException(){}
public MyException(String s){
super(s);
}
}
/*
这个是运行时异常
class M extends RuntimeException{
public M() {
}
public M(String message) {
super(message);
}
}
*/
4.方法的重写与异常(之前遗留问题)
之前在讲解方法的覆盖的时候,当时遗留了一个问题
重写之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少
1、子类重写父类方法要抛出与父类一致的异常,或者不抛出异常
2、子类重写父类方法所抛出的异常不能超过父类的范畴(仅指检查型异常 编译时异常)
备注:子类重写的方法可以抛出任何运行期异常
子类可以抛任何运行时异常,和父类一样 或者更少 更小的异常
class Aniaml{
public void doSome(){
}
public void doOther() throws Exception{
}
}
class Cat extends Aniaml{
/*
编译报错
public void doSome() throws Exception{
}
编译正常
public void doOther(){
}
编译正常
public void doOther() throws Exception{
}
*/
//编译正常
public void doOther() throws NullPointerException{
}
}
第十九章IO流
1.File类
这里File类和我们下面讲的流没有什么关系,不能对文件的读和写
java.io.File
类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
1.1 File类的构造方法
public File(String pathName)
:通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。public File(String parent, String child)
:从父路径名字符串和子路径名字符串创建新的 File实例。public File(File parent, String child)
:从父抽象路径名和子路径名字符串创建新的 File实例。
package IO.IOTest.File;
import java.io.File;
public class FileDemo01 {
public static void main(String[] args) {
// 文件路径名
String pathName1="D:\\aaa.txt";
// public File(String pathName)
File file1=new File(pathName1);// 这里知识new了一个File对象 并不是创建了这个文件
//文件路径名
String pathName2="D:\\aaa\\bbb.txt";
File file2=new File(pathName2);
// 通过父路径和子路径字符串
String parentDir = "d:\\aaa";
String childName = "bbb.txt";
File file3 = new File(parentDir, childName);
// 通过父级File对象和子路径字符串
File parentFile = new File("d:\\aaa");
String child = "bbb.txt";
File file4 = new File(parentFile, child);
}
}
1.2 File类的成员方法
1.2.1 获取文件信息方法
public String getAbsolutePath()
:返回此File的绝对路径名字符串。public String getPath()
:将此File转换为路径名字符串。public String getName()
:返回由此File表示的文件或目录的名称。public long length()
:返回由此File表示的文件的长度,如果是文件夹则返回0。public File getParentFile()
获取上一级的文件对象public String getParent()
获取上一级的路径 相对路径
package IO.IOTest.File;
import java.io.File;
public class FileDemo02 {
public static void main(String[] args) {
File file=new File("D:\\aaa.txt");
String absolutePath = file.getAbsolutePath();
System.out.println("文件的绝对路径"+absolutePath);
System.out.println("-----------------------------");
String path = file.getPath();
System.out.println("文件构造"+path);
System.out.println("-----------------------------");
String fileName = file.getName();
System.out.println("文件名"+fileName);
System.out.println("-----------------------------");
long length = file.length();
System.out.println("文件长度"+length);
System.out.println("-----------------------------");
File parentFile = file.getParentFile();
System.out.println("上一级的文件对象"+parentFile);
System.out.println("-----------------------------");
String parent = file.getParent();
System.out.println("上一级的文件对象名称"+parent);
}
}
发现getPath()
和getAbsolutePath()
获取的结果一致,getPath
用于获取的是构造方法里面输入的路径,有的时候我们在构造方法输入的路径可能是一个相对路径,此时getPath()
获取的是构造方法输入的路径,而获取的是此相对路径对应的磁盘绝对路径;
测试代码:
public class FileDemo03 {
public static void main(String[] args) {
File file=new File(".\\demo.java");
System.out.println(file.getAbsolutePath());
System.out.println(file.getPath());
}
}
1.2.2 判断文件的方法
public boolean exists()
:此File表示的文件或目录是否实际存在。public boolean isDirectory()
:此File表示的是否为目录。public boolean isFile()
:此File表示的是否为文件。
package IO.IOTest.File;
import java.io.File;
public class FileDemo04 {
public static void main(String[] args) {
File file1=new File("D:\\aaa.txt");
File file2=new File("D:\\bbb.txt");
System.out.println("file1是否存在呢?"+file1.exists());
System.out.println("file2是否存在呢?"+file2.exists());
System.out.println("file1是否是一个文件"+file1.isFile());
System.out.println("file2是否是一个目录"+file2.isDirectory());
}
}
1.2.3 文件的创建与删除方法
public boolean createNewFile()
:当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。public boolean delete()
:删除由此File表示的文件或目录。public boolean mkdir()
:创建由此File表示的目录。public boolean mkdirs()
:创建由此File表示的目录,包括任何必需但不存在的父目录。
package IO.IOTest.File;
import java.io.File;
import java.io.IOException;
public class FileDemo05 {
public static void main(String[] args) throws IOException {
// 文件的创建
File f = new File("aaa.txt");
System.out.println("是否存在:"+f.exists()); // false
System.out.println("是否创建:"+f.createNewFile()); // true
System.out.println("是否存在:"+f.exists()); // true
// 目录的创建
File f2= new File("newDir");
System.out.println("是否存在:"+f2.exists());// false
System.out.println("是否创建:"+f2.mkdir()); // true
System.out.println("是否存在:"+f2.exists());// true
// 创建多级目录
File f3= new File("newDira\\newDirb");
System.out.println(f3.mkdir());// false
File f4= new File("newDira\\newDirb");
System.out.println(f4.mkdirs());// true
// 文件的删除
System.out.println(f.delete());// true
// 目录的删除
System.out.println(f2.delete());// true
System.out.println(f4.delete());// false
}
}
1.2.4 目录的遍历
public String[] list()
:返回一个String数组,表示该File目录中的所有子文件或目录。public File[] listFiles()
:返回一个File数组,表示该File目录中的所有的子文件或目录。
package IO.IOTest.File;
import java.io.File;
public class FileDemo06 {
public static void main(String[] args) {
File file=new File("D:\\dev");
String[] list = file.list();
for (String name : list) {
System.out.println(name);
}
File[] files = file.listFiles();
for (File file1 : files) {
System.out.println(file1.getName());
}
}
}
1.3 相对路径和绝对路径
- 绝对路径:绝对路径是指文件在硬盘上真正存在的路径。
例如“1.txt”这个文件是存放在硬盘的“C\dev”目录下,
那么 “1.txt”这个文档的绝对路径就是“C\dev\1.txt”。 - 相对路径:相对于自己的目标文件位置。
例如“1.html”文件所在目录为“C\dev\file”,
而“1.txt”文件所在目录为“C\dev”,那么“1.txt”相对于“1.html”文件来说,是在其所在目录的上级目录里。 - 两者区别:①绝对路径是一个文件实际存在于硬盘中的路径。
②相对路径,指的是与自身的目标档案相关的位置。
③绝对路径是指可以从这个路径上查找文件夹,不管是从外部或内部存取。
而相对路径则是与它本身相关的,其它地方的档案和路径,则只能在内部存取。
“./”:表示当前的文件所在的目录。
“…/”:表示当前的文件所在的上一层的目录。
“/”:表示当前的文件所在的根目录。
代码示例:
package IO.IOTest.File;
import java.io.File;
public class FileDemo07 {
public static void main(String[] args) {
// 绝对路径的方式创建文件对象
File file1=new File("D:\\aaa.txt");
System.out.println(file1.getAbsolutePath()); // D:\aaa.txt
// 相对路径的方式创建文件对象
// 相对路径一定是从当前所在位置作为起点开始找
// IDEA默认的当前路径是,工程Project的根就是IDEA的默认当前路径 D:\Power\javase
File file2=new File("./");
System.out.println(file2.getAbsolutePath());// D:\Power\javase\.
File file3=new File("");
File file4=new File("D:\\Power\\javase");
boolean equals = file3.getAbsolutePath().equals(file4.getAbsolutePath());//true
System.out.println(equals);
}
}
1.4 递归
1.4.1 递归概述
- 递归:指在当前方法内调用自己的这种现象。
- 递归的分类:
- 递归分为两种,直接递归和间接递归。
- 直接递归称为方法自身调用自己。
- 间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法。
- 注意事项:
- 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。
- 在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
- 构造方法,禁止递归
1.4.2 递归的使用
1.4.2.1 案例代码
public class Demo01 {
public static void main(String[] args) {
// a();
b(1);
}
/*
* 3.构造方法,禁止递归
* 编译报错:构造方法是创建对象使用的,不能让对象一直创建下去
*/
public Demo01() {
//Demo01();
}
/*
* 2.在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
* 4993
* Exception in thread "main" java.lang.StackOverflowError
*/
private static void b(int i) {
System.out.println(i);
//添加一个递归结束的条件,i==5000的时候结束
if (i == 5000) {
return;//结束方法
}
b(++i);
}
/*
* 1.递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。 Exception in thread "main"
* java.lang.StackOverflowError
*/
private static void a() {
System.out.println("a方法");
a();
}
}
1.4.2.2 递归图解
-
main方法进栈执行,调用b(1)方法
-
b方法进栈,又调用一次b方法,传递2
-
b2(2)方法进栈,调用b(3)(注意:此时b(1)方法还未执行完毕,就跟main方法还未执行完毕一样)
-
b2(3)方法进栈,调用b(4)
-
…
1.4.23 使用递归计算阶乘
阶乘:所有小于及等于该数的正整数的积。
n的阶乘:n! =
n * (n-1) * (n-2) ...* 3 * 2 * 1
这与累和类似,只不过换成了乘法运算,学员可以自己练习,需要注意阶乘值符合int类型的范围。
条件结束:n! = n * (n-1)!
示例代码:
public class Demo03 {
//计算n的阶乘,使用递归完成
public static void main(String[] args) {
int n = 3;
// 调用求阶乘的方法
int value = getValue(n);
// 输出结果
System.out.println("阶乘为:"+ value);
}
/*
通过递归算法实现.
参数列表:int
返回值类型: int
*/
public static int getValue(int n) {
// 1的阶乘为1
if (n == 1) {
return 1;
}
/*
n不为1时,方法返回 n! = n*(n-1)!
递归调用getValue方法
*/
return n * getValue(n - 1);
}
}
1.4.2.4 使用递归打印多级目录
分析:多级目录的打印,就是当目录的嵌套。遍历之前,无从知道到底有多少级目录,所以我们还是要使用递归实现。
public class Demo04 {
public static void main(String[] args) {
// 创建File对象
File dir = new File("D:\\dev");
// 调用打印目录方法
printDir(dir);
}
public static void printDir(File dir) {
// 获取子文件和目录
File[] files = dir.listFiles();
// 循环打印
/*
判断:
当是文件时,打印绝对路径.
当是目录时,继续调用打印目录的方法,形成递归调用.
*/
for (File file : files) {
// 判断
if (file.isFile()) {
// 是文件,输出文件绝对路径
System.out.println("文件名:" + file.getAbsolutePath());
} else {
// 是目录,输出目录绝对路径
System.out.println("目录:" + file.getAbsolutePath());
// 继续遍历,调用printDir,形成递归
printDir(file);
}
}
}
}
1.4.2.5 使用递归完成文件搜索
搜索D:\dev
目录中的.java
文件。
分析:
- 目录搜索,无法判断多少级目录,所以使用递归,遍历所有目录。
- 遍历目录时,获取的子文件,通过文件名称,判断是否符合条件。
public class Demo05 {
public static void main(String[] args) {
// 创建File对象
File dir = new File("D:\\dev");
// 调用打印目录方法
printDir(dir);
}
public static void printDir(File dir) {
// 获取子文件和目录
File[] files = dir.listFiles();
// 循环打印
for (File file : files) {
if (file.isFile()) {
// 是文件,判断文件名并输出文件绝对路径
if (file.getName().endsWith(".java")) {
System.out.println("文件名:" + file.getAbsolutePath());
}
} else {
// 是目录,继续遍历,形成递归
printDir(file);
}
}
}
}
1.4.2.5 文件过滤器优化
java.io.FileFilter
是一个接口,是File的过滤器。
该接口的对象可以传递给File类的listFiles(FileFilter)
作为参数, 接口中只有一个方法。
boolean accept(File pathname)
:将此目录的每个文件传递给accept方法,此方法返回true则保留此文件,反之剔除;
分析:
- 接口作为参数,需要传递子类对象,重写其中方法。我们选择匿名内部类方式,比较简单。
accept
方法,参数为File,表示当前File下所有的子文件和子目录。保留住则返回true,过滤掉则返回false。保留规则:- 要么是.java文件。
- 要么是目录,用于继续遍历。
- 通过过滤器的作用,
listFiles(FileFilter)
返回的数组元素中,子文件对象都是符合条件的,可以直接打印。
public class Demo06 {
public static void main(String[] args) {
File dir = new File("D:\\dev");
printDir2(dir);
}
public static void printDir2(File dir) {
// 匿名内部类方式,创建过滤器子类对象
File[] files = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".java")||pathname.isDirectory();
}
});
// 循环打印
for (File file : files) {
if (file.isFile()) {
System.out.println("文件名:" + file.getAbsolutePath());
} else {
printDir2(file);
}
}
}
}
2.IO流概述
2.1 IO简介
I(Input)O(Output):中文翻译为输入输出,我们知道计算机的数据不管是软件、视频、音乐、游戏等最终都是存储在硬盘中的,当我们打开后,由CPU将硬盘中的数据读取到内存中来运行。这样一个过程就产生了I/O(输入/输出)
水的流向我们成为水流,数据的流动成为数据流,我们简称为流;数据的流向根据流向的不同,我们分为输入(Input)流和输出(Output)流,简称输入输出流,或称IO流;
2.2 IO流的分类
根据数据的流向分为:输入流和输出流。
- 输入流 :把数据从
输入设备
上读取到内存
中的流。 - 输出流 :把数据从
内存
中写出到输出设备
上的流。
根据操作数据单位的不同分为:字节流和字符流。
-
字节流 :以字节为单位,读写数据的流。
有的流是按照字节的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位
这种流是万能的,什么类型的文件都可以读取.包括:文本文件、图片、声音文件、视频文件…
假设文件file1.txt,采用字节流的话是这样读的:(内容为a中国bc张三de)
第一次读:一个字节,正好读到’a’,第二次读:一个字节,正好读到’中’字符的一半
第三次读:一个字节,正好读到’中’字符的另外一半 -
字符流 :以字符为单位(主要操作文本数据),读写数据的流。
有的流是按照字符的方式读取数据的,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,
这种流不能读取:图片、声音、视频等文件.只能读取纯文本文件,连word文件都无法读取
假设文件file1.txt,采用字符流的话是这样读的:
a中国bc张三de
第一次读:’a’字符('a’字符在windows系统中占用1个字节)
第二次读:’中’字符('中’字符在windows系统中占用2个字节)
2.3 流的四大家族
注意java中的所有的流都是在:java.io.*;下
四大家族首例:
抽象基类 | 输入流 | 输出流 |
---|---|---|
字节流 | 字节输入流(InputStream) | 字节输出流(OutputStream) |
字符流 | 字符输入流(Reader) | 字符输出流(Writer) |
注意:在java中只要"类名"以Stream结尾的都是字节流.以"Reader/Writer"结尾的都是字符流
四大家族的首领都是抽象类.(abstract class)
所有的流都实现了:
- java.io.Closeable接口,都是可关闭的,都有close()方法.
流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费很多资源,
养成好习惯,用完流一定要关闭
所有的输出流都实现了:
- java.io.Flushable接口,都是可刷新的,都有flush()方法.
养成一个好习惯,输出流在最终输出之后,一定要是的flush(),刷新一下
这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!)
刷新的作用就是清空管道
注意:如果没有flush()可能会导致丢失数据.
Java的IO流共涉及40多个类,都是从如下4个抽象基类派生的。由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
3. 字节流(FileInputStream/FileOutputStream
)
字节流有两个顶层接口父类,分别是字节输入流(InputStream)和字节输出流(OuputStream)
3.1字节输出流
OutputStream是所有字节输出的顶层父类,该父类提供如下公共方法:
public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。public abstract void write(int b)
:将指定的字节输出流。
3.1.1 FileOutputStream类
FileOutputStream是OutputStream中一个常用的子类,他可以关联一个文件,用于将数据写出到文件。
3.1.1.1 构造方法
FileOutputStream的构造方法如下:
public FileOutputStream(File file)
:创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name)
: 创建文件输出流以指定的名称写入文件。
Tips:当创建一个流对象时,需要指定一个文件路径,如果该文件以及存在则会清空文件中的数据,如果不存在则创建一个新的文件;
示例代码:
package IO.IOTest.OutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
// FileOutputStream构造方法
public class FileOutputStreamDemo01 {
public static void main(String[] args) throws FileNotFoundException {
File file=new File("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo01_1.txt");
FileOutputStream fos1=new FileOutputStream(file);
// 使用文件名称创建流对象
FileOutputStream fos2=new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo01_2.txt");
}
}
流都需要处理异常,等到后面我们会讲,流对异常的处理
3.1.1.2 其他方法
write(int b)
:每次可以写出一个字节数据;write(byte[] b)
:写出一个字节数组的数据;write(byte[] b, int off, int len)
:从字节数组中的off位置开始写出,写出len个字节;
package IO.IOTest.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo02 {
public static void main(String[] args) throws IOException {
FileOutputStream fos=new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo02.txt");
// 写出数据
fos.write(97);
byte[] bytes = "bytes".getBytes();
fos.write(bytes);
// 从 下标为0 开始写 写3个 byt
fos.write(bytes,0,3);
// 写完之后一定要刷新
fos.flush();
fos.close();
}
}
Tips:虽然参数为int类型四个字节,但是只会保留一个字节的信息写出
需要注意的是,在UTF-8编码下,一个中文占用3个字节,GBK编码下一个中文占用2个字节,因此在使用字节流来精确操作字符数据时将会变得非常麻烦,好在Java提供了一系列的字符流来帮助我们更加便捷的操作字符数据;
示例代码:
package IO.IOTest.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo03 {
public static void main(String[] args) throws IOException {
FileOutputStream fos=new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo03.txt");
// 字符串转换成数组
byte[] bytes = "我是中国人".getBytes();
// "我"字占用3个字节,"是"也占用3个字节,写出2下标~4下标位置的数据将会是乱码
fos.write(bytes,2,2);
// 写完之后一定要刷新
fos.flush();
fos.close();
}
}
图解:
修改写出的位置:
package IO.IOTest.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo04 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo04.txt");
byte[] bytes = "我是中国人".getBytes();
/*
"我"字占用3个字节,"是"也占用3个字节,写出2下标~4下标位置的数据将会是乱码
0,3-->我
3,3-->是
6,3-->中
9,3-->过
12,3-->人
*/
fos.write(bytes,0,3);
// 写完之后一定要刷新
fos.flush();
// 关闭
fos.close();
}
}
但是上述代码仅仅是在UTF-8编码环境下才能运行成功,如果文件的编码换成了GBK,那么又会变得乱码,关于字符问题,我们可以使用后面学习的字符流来解决这个问题,让我们操作的单位不再是字节,而是字符!
3.1.1.3 数据的追加
经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?
public FileOutputStream(File file, boolean append)
: 创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name, boolean append)
: 创建文件输出流以指定的名称写入文件。
这两个构造方法,参数中都需要传入一个boolean类型的值,true
表示追加数据,false
表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了,代码使用演示:
package IO.IOTest.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo05 {
public static void main(String[] args) throws IOException {
FileOutputStream fos=new FileOutputStream("/IO练习所用的文件/FileOutputStreamDemo05.txt",true);
// 字符串转换为字节数组
byte[] buf = "abcde".getBytes();
fos.write(buf);
// 写完之后一定要刷新
fos.flush();
// 关闭资源
fos.close();
}
}
3.1.1.4 写出换行
在Windows操作系统中,我们在文本编辑器中,输入一个回车实际上是做了两个动作,第一个动作是让光标到下一行,第二个动作则是回到这一行的开头;这两个动作分别对应着换行符(\n)和回车符(\r)
- 换行符:下一行(newline),ascii为10。
- 回车符:回到一行的开头(return),ascii为13。
因此在Windows系统中,换行符号是\r\n
。
package IO.IOTest.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamDemo06 {
public static void main(String[] args) throws IOException {
FileOutputStream fos=new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo07.txt");
byte[] bytes = "abcd".getBytes();
for (int i = 0; i < bytes.length; i++) {
// 写出一个字节
fos.write(bytes[i]);
// 换行
fos.write(10);
}
// 写完之后一定要刷新
fos.flush();
//关闭资源
fos.close();
}
}
如果只输入\r
,那么文件的内容应该是:
abcd
如果只输入\n
,那么文件的内容应该是:
a
b
c
d
但是多次测试发现,换行只写一个\r
或者是\n
也能到达上述效果;这是因为市面上大部分的文本编辑工具都做了优化,如果一行的后面只有一个\r
或者\n
那么文本编辑工具会自动帮我们添上缺少的\n
或\r
;但是为了规范起见,在windows系统中写出回车符最好还是使用\r\n
;
另外,不同的操作系统针对回车符也是不一样的标准:
- Windows系统里,每行结尾是
回车+换行
,即\r\n
; - Unix系统里,每行结尾只有
换行
,即\n
; - Mac系统里,每行结尾是
回车
,即\r
。从 Mac OS X开始与Linux统一。
还好我们可以通过System.getProperty("line.separator")
可以获取操作系统的换行符;这样就可以做到跨操作系统了;
3.2 字节输入流
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。public int read()
: 从输入流中读取一个字节数据,将读取到的数据返回;读取到文件的末尾返回-1public int read(byte[] buf)
: 从输入流中读取一个字节数组的数据,返回实际读取到的有效字节数量;读取到文件的末尾返回-1public int read(byte[] buf, int off, int len)
:从输入流中读取len个字节,然后将读取到的字节从off位置开始放置到字节数组中,len不可以超出字节数组的长度;读取到文件的末尾返回-1public int available()
:返回输入流中剩余的有效字节个数,如果没有有效字节(读取到末尾)则返回0public long skip(long n)
:跳过输入流的n个字节public void mark(int readlimit)
:对当前的位置进行标记,传递的readlimit参数是无意义的;public void reset()
:将此流重新定位到最后一次对此输入流调用mark
方法时的位置public boolean markSupported()
:测试这个输入流是否支持mark和reset方法。 InputStream的markSupported方法返回false。
3.2.1 FileInputStream类
ava.io.FileInputStream
类是文件输入流,从文件中读取字节。
3.2.1.1 构造方法
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
Tips:当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出
FileNotFoundException
。
示例代码:
package IO.IOTest.InputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class FileInputStreamDemo01 {
public static void main(String[] args) throws FileNotFoundException {
// 使用File对象创建流对象
File file=new File("21-Io流\\src\\IO\\IO练习所用的文件\\FileInputStream");
FileInputStream fis1=new FileInputStream(file);
// 使用字符串的方式创建流对象
FileInputStream fis2=new FileInputStream("21-Io流\\src\\IO\\IO练习所用的文件\\FileInputStream");
}
}
3.2.1.2 读取字节数据
public int read()
: 从输入流中读取一个字节数据,将读取到的数据返回;读取到文件的末尾返回-1public int read(byte[] buf)
: 从输入流中读取一个字节数组的数据,返回实际读取到的有效字节数量;读取到文件的末尾返回-1public int read(byte[] buf, int off, int len)
:从输入流中读取len个字节,然后将读取到的字节量;读取到文件的末尾返回-1,存放在字节数组中从off开始
读取 aaa.txt中abcdefg
的内容
示例代码1-读取单个字节:
package IO.IOTest.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamDemo02 {
public static void main(String[] args) throws IOException {
// 使用 文件名的形式创建流对象
FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\demo01.txt");
// 读取数据 返回一个字节
int read = fis.read();
System.out.println((char)read); //a
read= fis.read();
System.out.println((char)read); //b
read= fis.read();
System.out.println((char)read); //c
read= fis.read();
System.out.println((char)read); //d
read= fis.read();
System.out.println(read); // 13 \r
read= fis.read();
System.out.println(read); //10 \n
read= fis.read();
System.out.println(read); // -1 读取不到了
// 关闭资源
fis.close();
}
}
循环改进读取方式
package IO.IOTest.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamDemo03 {
public static void main(String[] args) throws IOException {
// 使用 文件名的形式创建流对象
FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\demo01.txt");
int read=0;
while ((read=fis.read()) != -1) {
System.out.println((char) read);
}
fis.close();
}
}
Tips:虽然读取了一个字节,但是会自动提升为int类型。
示例二:一次读取一个字节数组
package IO.IOTest.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamDemo04 {
public static void main(String[] args) throws IOException {
// 使用 文件名的形式创建流对象
FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\aaa.txt");
// 定义变量,作为有效个数
int len;
// 定义字节数组,作为装字节数据的容器 一次读取两个字节
byte[] data=new byte[2];
while ((len=fis.read(data))!=-1){
// 每次读取后,把数组变成字符串打印
System.out.println(new String(data));
}
fis.close();
}
}
注意点:
经过测试如果是自己写的文件
demo02.txt
和FileOutputStream生成的文件aaa.txt
文件内容都是abcde
我们进行长度测试
public class Test { public static void main(String[] args) { File file=new File("21-Io流\\StreamTestFile\\InputStream\\aaa.txt"); long length1 = file.length(); System.out.println("aaa.txt:"+length1);//5 File file2=new File("21-Io流\\StreamTestFile\\InputStream\\demo02.txt"); long length = file2.length(); System.out.println("demo02.txt:"+length);//7 } }
这两个文件内存都是相同的,那为什么 aaa.txt长度是5 而 demo02.txt是7呢?
上面我们也讲过,Windows每行结尾是
回车+换行
,即\r\n
所以会多出两个字符
运行结果:
ab
cd
ed
发现d出现了两次,这是由于最后一次读取时,只读取到了一个有效字节“e”,替换了原数组的0下标的“c”,图解分析如下:
我们在转换的时候不能全部转换,而是只转换有效的字节,所以要通过len
(实际读取到的字节个数) ,获取有效的字节,来决定到底转换多少个字节;
package IO.IOTest.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamDemo05 {
public static void main(String[] args) throws IOException {
// 使用 文件名的形式创建流对象
FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\aaa.txt");
// 定义变量 读取有效字节个数
int len;
// 定义字节数组,作为装字节数据的容器 一次读取两个字节
byte[] data=new byte[2];
while ((len=fis.read(data))!=-1){
// 每次读取后,把数组的有效字节部分,变成字符串打印
// String s = new String(byte数组, 起始下标, 长度);
System.out.println(new String(data,0,len));// len 每次读取的有效字节个数
}
// 关闭资源
fis.close();
}
}
示例代码3-将从字节流中读取到的数据按位置排列到字节数组中
package IO.IOTest.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamDemo06 {
public static void main(String[] args) throws IOException {
// 使用 文件名的形式创建流对象
FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\aaa.txt");
// 定义字节数组,作为装字节数据的容器 一次读取两个字节
byte[] data=new byte[100];
// 从输入流中读取10个字节,将读取到的数据从字节数组的1索引位置开始放置
int len = fis.read(data, 1, 10);
System.out.println("实际读取到的数据: " + len); // 5
System.out.println(new String(data)); // abcde
System.out.println(new String(data, 0, len)); // abcd
// 关闭资源
fis.close();
}
}
3.2.1.2 其他方法
public int available()
:返回输入流中剩余的有效字节个数,如果没有有效字节(读取到末尾)则返回0public long skip(long n)
:跳过输入流的n个字节public void mark(int readlimit)
:对当前的位置进行标记,传递的readlimit参数是无意义的;public void reset()
:将此流重新定位到最后一次对此输入流调用mark
方法时的位置public boolean markSupported()
:测试这个输入流是否支持mark和reset方法。 InputStream的markSupported方法返回false。
package IO.IOTest.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamDemo07 {
public static void main(String[] args) throws IOException {
// 使用 文件名的形式创建流对象
FileInputStream fis = new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\aaa.txt");
System.out.println((char) fis.read());//a
//public int available():返回输入流中剩余的有效字节个数,如果没有有效字节(读取到末尾)则返回0
int available = fis.available();
System.out.println("还剩下"+available+"个字节");//4
// `public long skip(long n)`:跳过输入流的n个字节 跳过1个字节
fis.skip(1);
System.out.println((char) fis.read());//c
//`public void mark(int readlimit)`:对当前的位置进行标记,传递的readlimit参数是无意义的;
fis.mark(1);
//`public boolean markSupported()`:测试这个输入流是否支持mark和reset方法。 InputStream的markSupported方法返回false。
System.out.println(fis.markSupported());//false
System.out.println((char) fis.read()); //d
// `public void reset()`:将此流重新定位到最后一次对此输入流调用 `mark` 方法时的位置
/*
InputStream的mark/reset方法是留给其他子类的,FileInputStream并不支持这两个方法,因此会出现异常:
Exception in thread "main" java.io.IOException: mark/reset not supported
*/
fis.reset();
System.out.println((char) fis.read());
}
}
3.2.1.3 字节流练习
FileOutputStream 主要按照字节方式写文件,例如:我们做文件的复制,首先读取文件,读取后在将该文件另写一份保存到磁盘上,这就完成了备份
package IO.IOTest.InputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileInputStreamDemo08 {
public static void main(String[] args) throws IOException {
// 1.创建输入流,用于读取文件
FileInputStream fis=new FileInputStream("D:\\dev\\MindManager.zip");
// 2. 创建输出流,用于写出文件
FileOutputStream fos=new FileOutputStream("D:\\dev\\MindManager_bak.zip");
// 3.定义数组大小(一次性读取1M)
byte[] data=new byte[1024];
// 4 定义变量,接受每次实际读取到的数据长度
int len;
// 5.循环读取并写出
while ((len=fis.read(data))!=-1){
// 将数据数组中的数据写入关联的指定文件
fos.write(data,0,len);
}
// 6. 释放资源
fos.flush();
fos.close();
fis.close();
}
}
4.字符流(FileReader/FileWriter
)
计算机都是按照字节进行存储的,我们之前学习过编码表,通过编码表可以将字节转换为对应的字符,但是世界上有非常多的编码表,不同的编码表规定的单个字符所占用的字节可能都不一样,例如**在GBK编码表中一个中文占2个字节,UTF8编码表则占3个字节;**且一个中文字符都是由多个字节组成的,为此我们不能再基于字节的操作单位来操作文本文件了,因为这样太过麻烦,我们希望基于字符来操作文件,一次操作读取一个“字符”而不是一个“字节”,这样在操作文本文件时非常便捷;
4.1 字符输入流
java.io.Reader
抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
-
public void close()
:关闭此流并释放与此流相关联的任何系统资源。 -
public int read()
:每次可以读取一个字符的数据,提升为int类型,读取到文件末尾,返回-1 -
public int read(char[] buf)
: 每次读取一个字符数组的数据读取到buf中,返回读取到的有效字符个数,读取到末尾时,返回-1 -
public int read(char buf[], int off, int len)
:每次读取len个字符
4.1.1 FileReader类
java.io.FileReader
类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
Tips:Windows系统的中文编码默认是GBK编码表。idea中默认是UTF-8
4.1.1.1 构造方法
FileReader(File file)
: 创建一个新的 FileReader ,给定要读取的File对象。FileReader(String fileName)
: 创建一个新的 FileReader ,给定要读取的文件的名称。
当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。
package IO.IOTest.FileReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class FileReaderDemo01 {
public static void main(String[] args) throws IOException {
//通过 File形式创建FileReader
File file=new File("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");
FileReader fileReader1=new FileReader(file);
//通过指定文件名的形式创建 FileReader
FileReader fileReader2=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");
}
}
4.1.1.2 其他方法
public int read()
:每次可以读取一个字符的数据,提升为int类型,读取到文件末尾,返回-1public int read(char buf[])
:每次读取一个字符数组的数据读取到buf中,返回读取到的有效字符个数,读取到末尾时,返回-1
public int read(char cbuf[], int offset, int length)
:从输入流中读取len个字符,然后将读取到的字符从字符数组的off位置开始放置,len不可以超出字符数组的长度;读取到文件末尾返回-1
示例代码1-读取单个字符
package IO.IOTest.FileReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReaderDemo02 {
public static void main(String[] args) throws IOException {
FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");
int data;
while ((data = fileReader.read()) != -1) {
// 以字符的方式输出
System.out.println((char) data);
}
// 关闭资源
fileReader.close();
}
}
示例代码2-读取字符数组
package IO.IOTest.FileReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReaderDemo03 {
public static void main(String[] args) throws IOException {
FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");
// 设置读取有效字符个数
int len;
// 一次读取两个字符
char[] data=new char[2];
while ((len=fileReader.read(data))!=-1){
System.out.println(new String(data));
}
// 关闭资源
fileReader.close();
}
}
示例代码3-获取有效的字符改进:
package IO.IOTest.FileReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReaderDemo04 {
public static void main(String[] args) throws IOException {
FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");
// 定义读取有效字符个数
int len;
// 定义字符数组 一次读取两个字符
char[] data = new char[2];
while ((len =fileReader.read(data))!= -1){
System.out.println(new String(data,0,len));
}
// 关闭资源
fileReader.close();
}
}
示例代码4-将从字符流中读取到的数据按位置排列到字符数组中:
package IO.IOTest.FileReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReaderDemo05 {
public static void main(String[] args) throws IOException {
FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");
// 准备一个字符数组来接收字符流读取的数据
char[] data = new char[100];
int read = fileReader.read(data, 2, 10);
System.out.println("读取有效的字符个数"+read);//读取有效的字符个数5
System.out.println(data);//我是中国人
System.out.println(new String(data,0,read));//我是中
// 关闭资源
fileReader.close();
}
}
4.2 字符输出流
java.io.Writer
抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
-
void write(int c)
写入单个字符。 -
void write(char[] cbuf)
写入字符数组。 -
abstract void write(char[] cbuf, int off, int len)
写入字符数组的某一部分,off数组的开始索引,len写的字符个数。 -
void write(String str)
写入字符串。 -
void write(String str, int off, int len)
写入字符串的某一部分,off字符串的开始索引,len写的字符个数。 -
void flush()
刷新该流的缓冲。void close()
关闭此流,但要先刷新它。
4.2.1 FileWriter类
java.io.FileWriter
类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
4.2.1.1 构造方法
FileWriter(File file)
: 创建一个新的 FileWriter,给定要读取的File对象。FileWriter(String fileName)
: 创建一个新的 FileWriter,给定要读取的文件的名称。
当你创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。
示例代码:
package IO.IOTest.FileWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterDemo01 {
public static void main(String[] args) throws IOException {
// 使用File对象创建流对象
File file=new File("21-Io流\\StreamTestFile\\FileWriter\\aaa.txt");
FileWriter fileWriter=new FileWriter(file);
// 指定文件名创建文件
FileWriter fileWriter2=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\bbb.txt");
}
}
4.2.1.2写出字符数据
示例代码1-写出单个字符数据:
package IO.IOTest.FileWriter;
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterDemo02 {
public static void main(String[] args) throws IOException {
FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\001.txt");
//写出数据
fileWriter.write(99);
fileWriter.write('b');
fileWriter.write('c');
/*
【注意】关闭资源时,与FileOutputStream不同。
如果不关闭,数据只是保存到缓冲区,并未保存到文件。
*/
fileWriter.close();
}
}
Tips:未调用close方法,数据只是保存到了缓冲区,并未写出到文件中。
示例代码2-写出字符数组:
package IO.IOTest.FileWriter;
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterDemo03 {
public static void main(String[] args) throws IOException {
FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\002.txt");
// 字符串转换为字节数组
char[] chars = "我是中国人".toCharArray();
//写出字符数组
fileWriter.write(chars);
// 从下标2开始 写两个字符
fileWriter.write(chars,2,2);
// 关闭资源
fileWriter.close();
}
}
文件内容
我是中国人中国
4.2.1.3 续写和换行
操作类似于FileOutputStream。
package IO.IOTest.FileWriter;
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterDemo04 {
public static void main(String[] args) throws IOException {
FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\003.txt",true);
fileWriter.write("我是");
fileWriter.write("\r\n");
fileWriter.write("中国人");
fileWriter.flush();
fileWriter.close();
}
}
4.2.1.4 关闭和刷新
因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush
方法了。
flush
:刷新缓冲区,流对象可以继续使用。close
:先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
package IO.IOTest.FileWriter;
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterDemo05 {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("002.txt");
// 写出数据,通过flush
fw.write('刷'); // 写出第1个字符
fw.flush();
fw.write('新'); // 继续写出第2个字符,写出成功
fw.flush();
// 写出数据,通过close
fw.write('关'); // 写出第1个字符
fw.close();
fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed
fw.close();
}
}
4.3 文件拷贝问题
4.3.1 字符流拷贝文件
字符流只能操作文本文件,不能操作图片,视频等非文本文件。当我们单纯读或者写文本文件时可以使用字符流,其他情况使用字节流;
4.3.1.1 字节流拷贝文本文件
package IO.IOTest.CopyProblem;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CopyProblemDemo01 {
public static void main(String[] args) throws IOException {
// 定义字符输入流 读取数据
FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileWriter\\text.txt");
// 定义字符输出流 写出数据(拷贝数据)
FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\text_copy.txt");
// 定义 读取有效的字符个数
int len;
// 定义字符数组
char[] buf = new char[1024];
while ((len=fileReader.read(buf))!=-1){
fileWriter.write(buf,0,len);
}
// 一定要记得刷新和关闭
fileReader.close();
fileWriter.flush();
fileWriter.close();
}
}
打开text_copy.txt文件发现内容正常;
4.3.1.2 字符流拷贝非文本文件:
package IO.IOTest.CopyProblem;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CopyProblemDemo02 {
public static void main(String[] args) throws IOException {
// 定义字符输入流 读取数据
FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileWriter\\image.png");
// 定义字符输出流 写出数据(拷贝数据)
FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\image_copy.png");
// 定义 读取有效的字符个数
int len;
// 定义字符数组
char[] buf = new char[1024];
while ((len=fileReader.read(buf))!=-1){
fileWriter.write(buf,0,len);
}
// 一定要记得刷新和关闭
fileReader.close();
fileWriter.flush();
fileWriter.close();
}
}
打开image_copy.png发现文件损坏;
- 得出结论:字符流可以拷贝文本文件,但不可以拷贝非文本文件
- 原因:因为字符流在拷贝文件过程中,就先会将读取到的数据转换为字符,而非文本文件内容本质上并不是一个字符,因此要转换为对应的字符会出现乱码,而最终将乱码写入到新的文件中,造成新的文件损坏;
4.3.2 字节流拷贝文件
由于拷贝文件时不需要将文本转换为对应的字符,而是直接基于字节的搬运,因此字节流既可以拷贝文本文件,又可以拷贝非文本文件
4.3.2.1 字节流拷贝文本文件
package IO.IOTest.CopyProblem;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopyProblemDemo03 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream=new FileInputStream("21-Io流\\StreamTestFile\\FileWriter\\text.txt");
FileOutputStream fileOutputStream=new FileOutputStream("21-Io流\\StreamTestFile\\FileWriter\\text_02.txt");
int len;
byte[] bytes = new byte[1024];
while ((len=fileInputStream.read(bytes))!=-1){
fileOutputStream.write(bytes,0,len);
}
fileInputStream.close();
fileOutputStream.close();
}
}
4.3.2.2 字节流拷贝非文本文件
package IO.IOTest.CopyProblem;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopyProblemDemo04 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream=new FileInputStream("21-Io流\\StreamTestFile\\FileWriter\\image.png");
FileOutputStream fileOutputStream=new FileOutputStream("21-Io流\\StreamTestFile\\FileWriter\\image_copy3.png");
int len;
byte[] data = new byte[1024];
while ((len=fileInputStream.read(data))!=-1){
fileOutputStream.write(data, 0, len);
}
fileInputStream.close();
fileOutputStream.flush();
fileOutputStream.close();
}
}
4.3.2.3 字节流读取字符
我们指定UTF-8的字符占用3个字节,GBK的字符占用2个字节,那么固定编码表的情况下(假设为UTF8),我们将字节数组大小定义为3来读取,是不是就可以很完美的读取所有的字符呢?
package IO.IOTest.CopyProblem;
import java.io.FileInputStream;
import java.io.IOException;
public class CopyProblemDemo05 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream=new FileInputStream("21-Io流\\StreamTestFile\\FileWriter\\CopyProblem.txt");
int len;
byte[] data = new byte[3]; //定义一个大小为3的字节数组
while ((len = fileInputStream.read(data)) != -1) {
/*
一次读取三个字节,刚好是一个汉字.可以解决中文乱码问题,这种方法是能解决此问题
但如果中间含有一个字节的符号 例如, 1 + 回车符等等...就会出现乱码问题
字节流不适用于读取文本文件
*/
System.out.print(new String(data,0,len));
}
fileInputStream.close();
}
}
输出结果
发现还是出现乱码问题;画图分析:
5.IO的异常处理
5.1 JDK7前处理
之前的入门练习,我们一直把异常抛出,而实际开发中并不能这样处理,建议使用try...catch...finally
代码块,处理异常部分,代码使用演示:
package IO.IOTest.ExceptionHandling;
import java.io.FileWriter;
import java.io.IOException;
public class ExceptionHandlingDemo01 {
public static void main(String[] args) {
FileWriter fileWriter=null;
try {
fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\003.txt");
fileWriter.write("我是中国人");
fileWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
// 不管是否出现异常都需要关闭流,释放资源
if (fileWriter!=null){
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
5.2 JDK7的处理
5.2.1 JDK7处理异常的方式
Java7提供的try-with-resources
语句,是异常处理的一大利器。该语句确保了每个资源在语句结束时关闭。所谓的资源(resource)是指在程序完成后,必须关闭的对象。
格式:
try (创建流对象语句,如果多个,使用';'隔开) {
// 读写数据
} catch (IOException e) {
e.printStackTrace();
}
示例代码:
package IO.IOTest.ExceptionHandling;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class ExceptionHandlingDemo02 {
public static void main(String[] args) {
/*
在try()中的创建的流对象不管try代码中是否出现异常都会调用流对象的close方法来是否资源,触发创建流的时候失败了(null)则不会调用流的close方法
*/
//FileWriter fw = new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\text.txt");
try (
// 创建流对象
FileReader fr = new FileReader("21-Io流\\StreamTestFile\\FileWriter\\002.txt");
FileWriter fw = new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\005.txt");
){
// 执行逻辑
int len;
char[] buf = new char[1024];
while ((len=fr.read(buf))!=-1){
fw.write(buf, 0, len);
}
}catch (IOException e){
e.printStackTrace();
}
}
}
5.2.2 AutoCloseable接口
在JDK7中,声明在try()中的类必须实现AutoCloseable接口,在资源处理完毕时,将自动的调用AutoCloseable接口中的close方法,如果没有实现AutoCloseable接口的类将不能写在try()中;
public interface AutoCloseable {
void close() throws Exception;
}
测试AutoCloseable:
-
定义TestAutoCloseable类
package IO.IOTest.ExceptionHandling; public class TestAutoCloseable implements AutoCloseable{ @Override public void close() throws Exception { System.out.println("close方法执行了"); } public void test(){ System.out.println("测试方法"); } public TestAutoCloseable() { //int i = 1 / 0; // 如果创建TestAutoCloseable出现异常了(创建失败了),则不会调用close方法 } }
-
编写测试类
package IO.IOTest.ExceptionHandling; public class Test { public static void main(String[] args) { try (TestAutoCloseable testAutoCloseable=new TestAutoCloseable()){ testAutoCloseable.test(); }catch (Exception e){ e.printStackTrace(); } } }
-
执行结果
测试方法 close方法执行了
我们可以看到四个父接口都继承了AutoCloseable,也就是说Java中的所有流都实现了AutoCloseable接口:
6.属性集(Properties
)
6.1 概述
java.util.Properties
继承于 Hashtable
,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties
方法就是返回一个Properties
对象。
6.2 Properties类
6.2.1 构造方法
public Properties()
:创建一个空的属性列表。
6.2.2 基本的存储方法
-
public Object setProperty(String key, String value)
: 保存一对属性。 -
public String getProperty(String key)
:使用此属性列表中指定的键搜索属性值。 -
public Set stringPropertyNames()
:所有键的名称的集合。
示例代码:
package IO.IOTest.Properties;
import java.util.Properties;
import java.util.Set;
public class PropertiesDemo01 {
public static void main(String[] args) {
Properties properties=new Properties();
properties.setProperty("fileName","a.txt");
properties.setProperty("localhost","D:/localhost");
properties.setProperty("port","8080");
//打印属性集对象
System.out.println(properties);
// 通过键,获取属性值
System.out.println(properties.getProperty("fileName"));
System.out.println(properties.getProperty("localhost"));
System.out.println(properties.getProperty("port"));
// 所有键的名称的集合。
Set<String> names = properties.stringPropertyNames();
for (String name : names) {
System.out.println(name + " -- " + properties.getProperty(name));
}
}
}
输出结果:
{localhost=D:/localhost, fileName=a.txt, port=8080}
a.txt
D:/localhost
8080
localhost -- D:/localhost
fileName -- a.txt
port -- 8080
6.2.3 加载流
public void load(InputStream inStream)
: 从字节输入流中读取键值对。
参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式:
localhost=D:/localhost, fileName=a.txt, port=8080
localhost=D:/localhost
fileName=a.txt
port=8080
加载代码演示:
package IO.IOTest.Properties;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.Set;
public class PropertiesDemo02 {
public static void main(String[] args) throws IOException {
Properties properties=new Properties();
properties.setProperty("fileName","a.txt");
properties.setProperty("localhost","D:/localhost");
properties.setProperty("port","8080");
// 加载文本中信息到属性集
properties.load(new FileInputStream("21-Io流\\StreamTestFile\\File\\prop.txt"));
// 遍历集合并打印
Set<String> names = properties.stringPropertyNames();
for (String name : names ) {
System.out.println(name+" -- "+properties.getProperty(name));
}
}
}
Tips:文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。
IO+Properties联合使用: 非常好的一个设计理念: 以后经常改变的数据,可以单独写到一个文件当中,使用程序动态读取 将来只需要修改这个文件的内容,java代码不需要修改,不需要重新编译 服务器也不需要重启,就可以拿到动态信息 类似于以上机制的被称为配置文件 并且当前配置文件中的内容格式是: key1=value key2=value 的时候,我们把这种配置文件叫做属性配置文件. java规范中有要求:属性配置文件建议以.properties结尾,但不是必须的 这种以.properties结尾的文件在java中被称为:属性配置文件 其中Properties是专门存放属性配置文件内容的一个类
7.转换流(InputStreamReader/OutputStreamWriter
)
7.1 字符编码和字符集
7.1.1 编码与解码
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。
7.1.2 字符集与编码
字符集(charset)
:字符集简单来说就是指字符的集合,例如所有的英文字母是一个字符集,所有的汉字是一个字符集,当然,把全世界所有语言的符号都放在一起,也可以称为一个字符集。计算机中的字符包括文字、图形符号、数学符号等;
拿汉字中的“汉”这个字符来说,我们看到的这个“汉”其实是这个字符的一种具体表现形式,是它的图像表现形式,而且它是用中文(而非拼音)书写而成,使用宋体外观;因此同一个字符的表现形式可能有无数种,把每一种的表现形式下的同一个字符都纳入到字符集中,会使得字符集过于庞大;
因此字符集中的字符,都是指唯一存在的抽象字符,而忽略了它的具体表现形式。在给定一个抽象字符集合中的每个字符都分配了一个整数编号之后,这个字符集就有了顺序,就成为了编码字符集。同时,这个编号,可以唯一确定到底指的是哪一个字符(哪一种具体形式)
7.1.3 字符集与编码的关系
字符集与编码的关系图:
在早期,字符集与编码是一对一的。有很多的字符编码方案,一个字符集只有唯一一个编码实现,两者是一一对应的。比如 GB2312,这种情况,无论你怎么去称呼它们,比如“GB2312编码”,“GB2312字符集”,说来说去其实都是一个东西,可能它本身就没有特意去做什么区分,所以无论怎么说都不会错。
到了 Unicode,变得不一样了,唯一的 Unicode 字符集对应了三种编码:UTF-8,UTF-16,UTF-32。字符集和编码等概念被彻底分离且模块化,其实是 Unicode 时代才得到广泛认同的。
从上图可以很清楚地看到,
1、编码是依赖于字符集的,就像代码中的接口实现依赖于接口一样;
2、一个字符集可以有多个编码实现,就像一个接口可以有多个实现类一样。
为什么 Unicode 这么特殊?
搞出新的字符集标准,无外乎是旧的字符集里的字符不够用了。Unicode 的目标是统一所有的字符集,囊括所有的字符,因此再去整什么新的字符集就没必要了。
但如果觉得它现有的编码方案不太好呢?在不能弄出新的字符集情况下,只能在编码方面做文章了,于是就有了多个实现,这样一来传统的一一对应关系就打破了。
7.2 编码的问题
当文本写入时的编码与读取时的编码不一致时就会出现乱码的现象;
准备两个文件,一个采用GB2312编码,一个采用UTF-8编码,使用Notepad++编辑器可编辑:
准备Java代码分别读取两个文件:
package IO.IOTest.FileReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReaderDemo07 {
public static void main(String[] args) throws IOException {
FileReader fileReader=new FileReader("C:\\Users\\Administrator\\Desktop\\text.txt");
char[] chars=new char[2];
fileReader.read(chars);
System.out.println(chars);
fileReader.close();
}
}
发现在读取GB2312时出现中文乱码:
这是因为IDEA默认情况下都是采用UTF-8进行编码与解码,平常我们在操作时感觉不到编码的问题;而我们手动编辑了一个文本文件以GB2312的编码格式保存,此时再使用UTF-8编码进行读取就出现乱码问题;
7.3 转换输入流
转换流java.io.InputStreamReader
,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
7.3.1 构造方法
InputStreamReader(InputStream in)
: 创建一个使用默认字符集的字符流。InputStreamReader(InputStream in, String charsetName)
: 创建一个指定字符集的字符流。
OutputStreamWriter isr1 = new OutputStreamWriter(new FileOutputStream("out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");
7.3.2使用转换输出流写文件
package IO.IOTest.InputStreamReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
public class InputStreamReaderDemo02 {
public static void main(String[] args) throws Exception {
File file = new File("C:\\Users\\Administrator\\Desktop\\text.txt");
FileInputStream fis = new FileInputStream(file);
InputStreamReader inputStreamReader = new InputStreamReader(fis, "GB2312");
// 定义变量,保存字符
int read;
// 使用指定编码字符流读取,正常解析
while ((read = inputStreamReader.read()) != -1) {
// 输出正常
System.out.print((char) read);
}
inputStreamReader.close();
}
}
7.4 转换输出流
转换流java.io.OutputStreamWriter
,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
7.4.1 构造方法
OutputStreamWriter(OutputStream in)
: 创建一个使用默认字符集的字符流。OutputStreamWriter(OutputStream in, String charsetName)
: 创建一个指定字符集的字符流。
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");
7.4.2 使用转换输出流写文件
package IO.IOTest.OutputStreamWriter;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
public class OutputStreamWriterDemo01 {
public static void main(String[] args) throws Exception {
String fileName="21-Io流\\StreamTestFile\\FileWriter\\ccc.txt";
OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream(fileName),"UTF-8");
osw.write("你好");
osw.close();
}
}
8.缓冲流
计算机访问外部设备或文件,要比直接访问内存慢的多。如果我们每次调用read()方法或者write()方法访问外部的设备或文件,CPU就要花上最多的时间是在等外部设备响应,而不是数据处理。为此,我们开辟一个内存缓冲区的内存区域,程序每次调用read()方法或write()方法都是读写在这个缓冲区中。当这个缓冲区被装满后,系统才将这个缓冲区的内容一次集中写到外部设备或读取进来给CPU。使用缓冲区可以有效的提高CPU的使用率,能提高整个计算机系统的效率。
8.1 缓冲流的分类
缓冲流是针对4个顶层父类的流的增强,分为:
输入缓冲流 | 输出缓冲流 | |
---|---|---|
字节缓冲流 | BufferedInputStream | BufferedOutputStream |
字符缓冲流 | BufferedReader | BufferedWriter |
缓冲流在读取/写出数据时,内置有一个默认大小为8192字节/字符的缓冲区数组,当发生一次IO时读满8192个字节再进行操作,从而提高读写的效率。
8.2 字节缓冲流
8.2.1 构造方法
public BufferedInputStream(InputStream in)
:创建一个 新的缓冲输入流。public BufferedOutputStream(OutputStream out)
: 创建一个新的缓冲输出流。
示例代码:
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));
8.2.2 使用字节缓冲流
我们分别使用普通流和缓冲流对一个大小为39.1MB的文件进行拷贝,查看两种方案所花费的时间;
使用普通流:
package IO.IOTest.Buffered;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class BufferedDemo01 {
public static void main(String[] args) throws Exception {
// 定义开始时间
long start = System.currentTimeMillis();
FileInputStream fis=new FileInputStream("D:\\dev\\mongosh-1.6.0-win32-x64.zip");
FileOutputStream fos=new FileOutputStream("D:\\dev\\mongosh-1.6.0-win32-x64_bak1.zip");
// 定义 读取有效字节格式
int len;
// 定义字符数组一次读取1M
byte[] data=new byte[1024];
// 循环读取
while ((len =fis.read(data))!=-1){
fos.write(data,0,len);
}
// 定义结束时间
long end = System.currentTimeMillis();
System.out.println("所用总的毫秒数"+(end-start));
}
}
所用的时间为385
使用缓冲流拷贝:
package IO.IOTest.Buffered;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class BufferedDemo02 {
public static void main(String[] args) throws Exception {
// 定义开始时间
long start = System.currentTimeMillis();
FileInputStream fis=new FileInputStream("D:\\dev\\mongosh-1.6.0-win32-x64.zip");
FileOutputStream fos=new FileOutputStream("D:\\dev\\mongosh-1.6.0-win32-x64_bak1.zip");
BufferedInputStream bis=new BufferedInputStream(fis);
BufferedOutputStream bos=new BufferedOutputStream(fos);
// 定义 读取有效字节格式
int len;
// 定义字符数组一次读取1M
byte[] data=new byte[1024];
// 循环读取
while ((len =bis.read(data))!=-1){
bos.write(data,0,len);
}
// 定义结束时间
long end = System.currentTimeMillis();
System.out.println("所用总的毫秒数"+(end-start));
}
}
所用的时间138
可以看出,缓冲流拷贝文件的效率比普通流高太多太多,因此我们在做大文件拷贝时应该尽量选用缓冲流;
8.3 字符缓冲流
8.3.1 构造方法
public BufferedReader(Reader in)
:创建一个 新的缓冲输入流。public BufferedWriter(Writer out)
: 创建一个新的缓冲输出流。
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));
8.3.2 常用方法
字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。
- BufferedReader:
public String readLine()
: 读一行文字。 - BufferedWriter:
public void newLine()
: 写一行行分隔符,由系统属性定义符号。
newLine
方法示例代码如下:
package IO.IOTest.Buffered;
import java.io.BufferedWriter;
import java.io.FileWriter;
public class BufferedWriterDemo01 {
public static void main(String[] args) throws Exception {
// 创建流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\ddd.txt"));
bw.write("我是");
bw.newLine();
bw.write("中");
bw.newLine();
bw.write("国人");
bw.newLine();
bw.flush();
bw.close();
}
}
readLine
方法示例代码如下:
package IO.IOTest.Buffered;
import java.io.BufferedReader;
import java.io.FileReader;
public class BufferedReaderDemo01 {
public static void main(String[] args) throws Exception {
BufferedReader br=new BufferedReader(new FileReader("21-Io流\\StreamTestFile\\FileWriter\\ddd.txt"));
// 定义字符串,保存读取的一行文字
String line = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.println(line);
}
// 释放资源
br.close();
}
}
9.序列流
9.1 序列化概述
Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据、对象的类型和对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。看图理解序列化:
9.2 对象输出流
java.io.ObjectOutputStream
类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。
9.2.1 构造方法
public ObjectOutputStream(OutputStream out)
: 创建一个指定OutputStream的ObjectOutputStream。
FileOutputStream fileOut = new FileOutputStream("goods.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
9.2.2 对象的序列化
一个对象要想序列化,必须满足两个条件:
- 该类必须实现
java.io.Serializable
接口,Serializable
是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
。 - 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用
transient
关键字修饰。
写出对象方法:
public final void writeObject(Object obj)
: 将指定的对象写出。
准备一个对象:
package IO.IOTest.Object.ObjectOutputStream;
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private int age;
//表示address不参加序列化操作
private transient String address;//transient关键字表示游离的,不参与序列化
public Student() {
}
public Student(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
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;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
测试:
package IO.IOTest.Object.ObjectOutputStream;
import java.io.*;
public class ObjectOutputStreamDemo01 {
public static void main(String[] args) {
//创建对象
Student student = new Student("张三", 13, "北京昌平");
FileOutputStream fos = null;
ObjectOutputStream oos = null;
try {
fos = new FileOutputStream("21-Io流\\StreamTestFile\\Object\\Student.txt");
// 创建序列化流对象
oos = new ObjectOutputStream(fos);
// 写出对象
oos.writeObject(student);// address 没有被序列化
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
9.3 对象输入流
ObjectInputStream反序列化流
,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。
9.3.1 构造方法
public ObjectInputStream(InputStream in)
: 创建一个指定InputStream的ObjectInputStream。
9.3.2 对象的反序列化
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream
读取对象的方法:
public final Object readObject()
: 读取一个对象。
package IO.IOTest.Object.ObjectInputStream;
import IO.IOTest.Object.ObjectOutputStream.Student;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class ObjectInputStreamDemo01 {
public static void main(String[] args) {
FileInputStream fis=null;
ObjectInputStream bis=null;
try {
fis=new FileInputStream("21-Io流\\StreamTestFile\\Object\\Student.txt");
bis=new ObjectInputStream(fis);
Student student = (Student)bis.readObject();
System.out.println(student.toString());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
if (bis != null&&fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
9.3.3 序列化版本号
当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException
异常。发生这个异常的原因如下:
- 1)该类的序列版本号与从流中读取的类描述符的版本号不匹配
- 2)该类包含未知数据类型
- 3)该类没有可访问的无参数构造方法
Serializable
接口给需要序列化的类,提供了一个序列版本号。serialVersionUID
该版本号的目的在于验证序列化的对象和对应类是否版本匹配。
public class Student implements Serializable {
// 序列化版本号
private static final long serialVersionUID = 1L;
private String name;
private int age;
//表示address不参加序列化操作
private transient String address;//transient关键字表示游离的,不参与序列化
}
Tips:对象在序列化时,如果指定了序列号,那么在反序列化时会默认读取到上次序列化时的序列号,即使类已经修改过也没有关系;
注意点:
- 参与序列化和反序列化的对象,必须实现Serializable接口
- Serializable接口作用:
- 起到标识作用,标志作用,java虚拟机看到这个类实现了这个接口,可能会对这个类进行特殊待遇.
- Serializable这个标志接口是给java虚拟机参考的,java虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号
- java语言中采用什么机制来区分类的?
- 第一:首先通过类名进行比对,如果类名不一样,肯定不是同一个类
- 第二:如果类名一样,再怎么进行类的区别?靠序列化版本号进行区分
- 这种自动生成序列化版本号有什么缺陷?(默认的序列化版本号)
- 一旦代码确定之后,不能进行后续的修改
因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java
虚拟机会认为这是一个全新的类(这样就不好了)
- 一旦代码确定之后,不能进行后续的修改
- 最终结论:
- 凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号
这样,以后这个类即使代码修改了,但版本号不变,java虚拟机就会认为这是同一个类
- 凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号
- idea自动生成版本号
9.4 序列化案例
- 将存有多个自定义对象的集合序列化操作,保存到
list.txt
文件中。 - 反序列化
list.txt
,并遍历集合,打印对象信息。
9.4.1 案例分析
- 把若干学生对象 ,保存到集合中。
- 把集合序列化。
- 反序列化读取时,只需要读取一次,转换为集合类型。
- 遍历集合,可以打印所有的学生信息
9.4.2 代码实现
public class Demo01 {
public static void main(String[] args) throws Exception {
// 创建 学生对象
Book book1 = new Book("《西游记》", "吴承恩");
Book book2 = new Book("《三国演义》", "罗贯中");
Book book3 = new Book("《水浒传》", "施耐庵");
Book book4 = new Book("《红楼梦》", "曹雪芹");
ArrayList<Book> bookList = new ArrayList<>();
bookList.add(book1);
bookList.add(book2);
bookList.add(book3);
bookList.add(book4);
serialize(bookList);
deserialize(bookList);
}
public static void serialize(ArrayList<Book> bookList) throws IOException {
// 创建 序列化流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
// 写出对象
oos.writeObject(bookList);
// 释放资源
oos.close();
}
public static void deserialize() throws Exception {
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt"));
// 读取对象,强转为ArrayList类型
ArrayList<Book> list = (ArrayList<Book>) ois.readObject();
System.out.println(list);
}
}
class Book implements Serializable{
// 序列化版本号
private static final long serialVersionUID = 1L;
private String name;
private String author;
public Book() {
}
public Book(String name, String author) {
this.name = name;
this.author = author;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
10.打印流
10.1 打印流概述
打印流java.io.PrintStream
类是OutputStream
的一个子类,因此也是属于字节输出流。打印流的功能主要是将数据打印(输出)到控制台,方便我们输出的;
我们平时向控制台输出内容都是借助打印流来完成的:
10.2 打印流的使用
10.2.1 构造方法
public PrintStream(String fileName)
: 使用指定的文件名创建一个新的打印流。
示例代码:
PrintStream ps = new PrintStream("ps.txt");
10.2.2 打印流输出内容
public void println(String x)
:将指定的字符串输出public void println(int x)
:将指定的整形输出
我们之前通过
System.out
获取到的就是一个打印流,因此打印流的方法不多赘述;
/*
java.io.PrintStream:标准的字节输出流.默认输出到控制台
*/
public class PrintStreamTest01 {
public static void main(String[] args) throws Exception {
//联合起来写
System.out.println("hello world");
//分开写
PrintStream ps=System.out;
ps.println("张三");
//标准输出流不需要close()
//可以改变标准输出流的输出方向吗?可以!
/*
之前System类使用国的方法和属性
System.gc();
System.currentTimeMillis();
System.exit(0);
System.arraycopy();
PrintStream ps=System.out
*/
//标准输出流不在指向控制台,指向”log“文件
PrintStream printStream=new PrintStream(new FileOutputStream("D:\\Power\\javase\\21-Io流\\StreamTestFile\\PrintStream\\demo.txt"));
//修改输出方向,将输出方向修改到"PrintStream"文件
System.setOut(printStream);
System.out.println("hello zhangsan ");
System.out.println("hello lisi ");
System.out.println("hello wangwu ");
System.out.println("hello zhaoliu ");
}
}
10.3 标准输入输出流
10.3.1 标准输入流
标准输入流是一个缓冲字节输入流(BufferedInputStream),他默认指向的是控制台(键盘),通过System.in
获取;
我们之前在用Scanner的时候,构造方法如下:
System.in获取的就是标准输入流(指向键盘),因此Scanner总是接受我们键盘输入的数据,我们可以更改Scanner读取的流对象:
package IO.IOTest.PrintStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Scanner;
public class PrintStreamDemo02 {
public static void main(String[] args)throws IOException {
Scanner sc = new Scanner(new FileInputStream("21-Io流\\StreamTestFile\\PrintStream\\demo.txt"));
String str = sc.next();
System.out.println(str);
str=sc.next(); // 此时读取的不是控制台的数据,而是ps.txt中的数据
System.out.println(str);
str=sc.next();
System.out.println(str);
str=sc.next();
System.out.println(str);
str=sc.next();
System.out.println(str);
str=sc.next();
System.out.println(str);
str=sc.next();
System.out.println(str);
str=sc.next();
System.out.println(str);
str=sc.next(); // 读取到结尾 抛出java.util.NoSuchElementException
System.out.println(str);
}
}
也可以更改标准输入流:
package IO.IOTest.PrintStream;
import java.io.FileInputStream;
public class PrintStreamDemo03 {
public static void main(String[] args) throws Exception {
// 更改标准输入流
System.setIn(new FileInputStream("21-Io流\\StreamTestFile\\PrintStream\\demo.txt"));
}
}
10.3.2 标准输出流
标准输出流是一个打印流(PrintStream),他默认指向的是控制台,通过System.out
获取标准输出流,因此我们之前总是使用System.out.println
往控制台输出内容;
我们也可以更改标准输出流,让其不在输出到控制台,而是输出到我们制定的地方:
package IO.IOTest.PrintStream;
import java.io.FileOutputStream;
import java.io.PrintStream;
public class PrintStreamDemo04 {
public static void main(String[] args) throws Exception {
System.setOut(new PrintStream("21-Io流\\StreamTestFile\\PrintStream\\demo2.txt"));
// 此时输出到demo2.txt文件中,而不是控制台
System.out.println("我是中国人");
System.out.println("犯我中华者");
System.out.println("虽远必诛");
}
}
11.其他流
11.1 字节数组流
我们之前的所有流最终的流向(指向)都是文件,通过FileInputStream/FileOutputStream
关联一个文件,从文件进行读取/写入;但有些情况下我们可以将流直接指向内存,对内存的某块区域进行读写操作;
Java中提供有ByteArrayInputStream
和ByteArrayOutputStream
,分别是字节数组输入流和字节数组输出流;
Tips:对于字节数组输入/输出流,关闭该流无效,关闭此流后依旧可以正常读写
11.1.1 ByteArrayInputStream
ByteArrayInputStream:内置一个字节数组缓冲区,调用read方法对其缓冲区进行读取,并记录读取到的索引,提供索引标记、重置位置等功能;
相关成员变量:
buf[]
:由数据流的创建者提供的字节数组。count
:字节数组的长度mark
:数据读取时,标记读取的位置,需要调用mark(int readAheadLimit)
方法进行标记pos
:读取的字节索引位置
相关方法:
-
read()
:从此输入流中读取下一个字节并返回,当流到达末尾时(pos到达末尾时),返回-1 -
read(byte b[], int off, int len)
: 从输入流中off
的位置读取len
个字节到b[]
数组中,返回实际读取的字节数 -
mark(int readAheadLimit)
:对当前的pos位置进行标记,传递的readAheadLimit参数是无意义的; -
reset()
:将此流重新定位到最后一次对此输入流调用mark
方法时的位置
public class Demo01_ByteArrayInputStream {
public static void main(String[] args) throws Exception {
// 创建一个内存字节输入流(从内存中读取数据)
ByteArrayInputStream bis = new ByteArrayInputStream("abc".getBytes());
// 读取一个字节
int b = bis.read();
System.out.println((char) b); // a
bis.close(); // 内存字节流不需要关闭,即使关闭了依旧可以继续使用
byte[] data = new byte[1024];
bis.read(data, 0, 2);
System.out.println(new String(data, 0, 2)); // bc
}
public static void method() throws Exception {
// 创建一个内存字节输入流(从内存中读取数据)
ByteArrayInputStream bis = new ByteArrayInputStream("abcd".getBytes()); // pos=0,count=4,mark=0
System.out.println(bis);
int data = bis.read(); // pos=1,count=4,mark=0
System.out.println((char) data); // a
data = bis.read(); // pos=2,count=4,mark=0
System.out.println((char) data); // b
bis.mark(1); // pos=2,count=4,mark=2
data = bis.read(); // pos=3,count=4,mark=2
System.out.println((char) data); // c
// 将pos设置为mark值
bis.reset(); // pos=2,count=4,mark=2
data = bis.read(); // pos=3,count=4,mark=2
System.out.println((char) data); // c
data = bis.read(); // pos=4,count=4,mark=2
System.out.println((char) data); // d
data = bis.read(); // pos=4,count=6,mark=2
System.out.println(data); // -1
bis.close();
}
}
11.1.2 ByteArrayOutputStream
ByteArrayOutputStream:该类实现了将数据写入字节数组的输出流。并内置缓冲区,当数据写入缓冲区时,缓冲区会自动增长。
相关成员变量:
buf[]
:内置的缓冲区,默认大小32count
:缓冲区中有效字节数
相关方法:
-
write(int b)
:将指定的字节写入此字节数组输出流。 -
write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量为 off开始,输出到这个字节数组输出流。 -
public void reset()
:将此字节数组输出流的count字段重置为零,以便丢弃输出流中当前累积的所有输出。 可以再次使用输出流,重用已经分配的缓冲区空间。 -
public int size()
:返回缓冲区的当前大小。 -
public String toString()
:使用平台的默认字符集将缓冲区内容转换为字符串解码字节。 -
public String toString(String charsetName)
:通过使用命名的charset解码字节将缓冲区的内容转换为字符串。 -
public byte[] toByteArray()
:创建一个新分配的字节数组。 其大小是此输出流的当前大小,缓冲区的有效内容已被复制到其中。 -
public void writeTo(OutputStream out)
:将此字节数组输出流的完整内容写入指定的输出流参数
public class Demo02_ByteArrayOutputStream {
public static void main(String[] args) throws Exception {
// 创建字节数组输出流
ByteArrayOutputStream bos = new ByteArrayOutputStream(100); // 内置byte[]缓冲区,默认大小为32
// 写出一个字节到缓冲区
bos.write(97);
// 写出一个字节数组
bos.write("你好".getBytes());
// 根据偏移量来写出数据
bos.write("abcd".getBytes(), 0, 2); // ab
// 获取缓冲区中的数据(0~count的数据)
byte[] data = bos.toByteArray(); // aabche
System.out.println(new String(data, 0, data.length));
// 将0~count之间的数据转换为字符串
System.out.println(bos.toString()); // 根据平台默认的编码表将字节转换为字符串
System.out.println(bos.toString("UTF-8")); // 指定编码表将字节数据转换为字符串
bos.reset(); // 将count置为0
System.out.println(bos.toString()); // count已经变为0,因此输出空
// 重新写入数据到内置的字节数组
bos.write("大家好".getBytes());
bos.writeTo(new FileOutputStream("00.txt"));
}
}
11.2 数据流
11.2.1 数据流概述
文本格式的数据对于人类而言显得很方便,但是它并不像以二进制格式传递数据那样高效。数据流能更精确的操作Java中的数据类型并以适当的方式将Java基本数据类型输出或输入,数据流提供了存取所有Java基本数据类的方法;
由于IO操作数据的时候,操作的都是字节,即使写出的是一个int数,但真正写出的却是这个int数的字节,数据流能很好的指定写出的数据类型,由于写出的是Java的数据类型,所以在文件上写出的是乱码,但程序能读懂
Tips:数据输入流允许应用程序从底层输入流读取原始Java数据类型。
使用普通字节流的问题:
public class Demo01_普通字节流的问题 {
public static void main(String[] args) throws Exception{
FileOutputStream fos=new FileOutputStream("01.txt");
// 向文件写出一个字节
fos.write(800);
fos.close();
/*
由于write写出的时候默认转成字节(二进制)
int为4个字节 前面会补0 最后写出的时候只会写出一个字节
997超过了一个字节的取值范围 会将超过一个字节的数砍掉 保留后8位
997的二进制是: 00000000 00000000 00000011 00100000
最后将 00100000 保存到了文件
而 00100000 并没有被编码表编码,没有对应的字符,因此在文件中显示乱码
*/
FileInputStream fis=new FileInputStream("01.txt");
// 将读取到的字节转换为int数
int x=fis.read();
System.out.println(x); // 打印 00100000 的十进制 32
}
}
在针对精确数据类型的操作时,普通流将会变得非常不方便;
11.2.2 数据流的使用
Java中数据流分为数据输入流和数据输出流,分别对应DataInputStream
和DataOutputStream
;数据流能很好的针对精确的数据进行读取或写出;
DataInputStream
类中提供有如下方法:
-
public boolean readBoolean()
:从输入流中读取一个布尔值 -
public byte readByte()
:从输入流中读取一个Byte值 -
public short readShort()
:从输入流中读取一个Byte值 -
public char readChar()
:从输入流中读取一个Char值 -
public int readInt()
:从输入流中读取一个Integer值 -
public long readLong()
:从输入流中读取一个Long值 -
public float readFloat()
:从输入流中读取一个Float值 -
public double readDouble()
:从输入流中读取一个Double值
public class Demo02_数据流测试 {
public static void main(String[] args) throws Exception{
// 创建字节输出流
DataOutputStream dos=new DataOutputStream(new FileOutputStream("01.txt"));
// 创建字节输入流
DataInputStream dis=new DataInputStream(new FileInputStream("01.txt"));
// 写出3个int数
dos.writeInt(800);
dos.writeInt(801);
dos.writeInt(802);
/*
* writeInt方法写出的是一个int数 为4个字节 那么文件此时大小为4*3=12个字节
* 此时存到文件上的还是乱码 但是程序能读的懂
*/
int x=dis.readInt();
System.out.println(x); // 800
int y=dis.readInt();
System.out.println(y); // 801
int z=dis.readInt();
System.out.println(z); // 802
dos.close();
dis.close();
}
}
11.3 序列流
序列流(SequenceInputStream ):序列流用于关联多个输入流,它从一个有序的输入流集合开始,从第一个读取到文件的结尾,然后从第二个文件读取,依此类推,直到最后一个输入流达到文件的结尾。
比如现在有三个文件【1.txt】、【2.txt】、【3.txt】;现在要把这三个文件按照1、2、3的顺序合并成一个文件输出到 【all.txt】文件中。如果不知道有这个流,大家可能都是自己一个一个文件的去读,自己合并到一个文件中。有了这个流,我们操作起来,代码更加优雅。
public class Demo01_序列流的使用 {
public static void main(String[] args) throws Exception {
FileInputStream fis1 = new FileInputStream("./a.txt");
FileInputStream fis2 = new FileInputStream("./b.txt");
FileOutputStream fos = new FileOutputStream("./c.txt");
// 序列流将a.txt,b.txt依次写入c.txt
SequenceInputStream sis = new SequenceInputStream(fis1, fis2);
int data;
while ((data = sis.read()) != -1) {
fos.write(data);
}
fos.close();
sis.close();
}
}
11.4 回退流
回退流(PushbackInputStream):在JAVA IO中所有的数据都是采用顺序的读取方式,即对于一个输入流来讲都是采用从头到尾的顺序读取的,如果在输入流中某个不需要的内容被读取进来,我们就要采取办法把不需要的数据处理掉,为了解决这样的处理问题,在JAVA中提供了一种回退输入流(PushbackInputStream),可以把读取进来的某些数据重新回退到输入流的缓冲区之中。
回退流中提供的方法如下:
-
public PushbackInputStream(InputStream in)
:构造方法 将输入流放入到回退流之中。 -
public int read() throws IOException
:普通 读取数据。 -
public int read(byte[] b,int off,int len) throws IOException
:普通方法 读取指定范围的数据。 -
public void unread(int b) throws IOException
:普通方法 回退一个数据到缓冲区前面。 -
public void unread(byte[] b) throws IOException
:普通方法 回退一组数据到缓冲区前面。 -
public void unread(byte[] b,int off,int len) throws IOException
:普通方法 回退指定范围的一组数据到缓冲区前面。
public class Demo01_回退流测试 {
public static void main(String[] args) throws Exception {
String str = "123";
PushbackInputStream pis = new PushbackInputStream(new ByteArrayInputStream(str.getBytes())); // 从内存中读取数据
int data = pis.read(); // 1
System.out.println((char) data);
pis.unread("-".getBytes()); // 回退到流中
data = pis.read();
System.out.println((char) data); // -
data = pis.read(); // 2
System.out.println((char) data);
pis.unread("-".getBytes()); // 回退到流中
data = pis.read();
System.out.println((char) data); // -
data = pis.read(); // 3
System.out.println((char) data);
}
}
需求:在一段数字中截取手机号(11位)
public class Demo02_回退流小案例 {
public static void main(String[] args) throws Exception {
String str = "13079016067131350454961897434920916552444009";
PushbackInputStream push = new PushbackInputStream(new ByteArrayInputStream(str.getBytes())); // 从内存中读取数据
System.out.println("读取之后的数据为:");
int data = 0;
int count = 1;
while ((data = push.read()) != -1) { // 读取内容
System.out.print((char) data);
if (count % 11 == 0) { // 读完了一个手机号
push.unread("-".getBytes()); // 放回到缓冲区之中
data = push.read(); // 再读一遍
System.out.print((char) data);
}
count++;
}
System.out.println();
}
}
11.5 管道流
管道输入与输出实际上使用的是一个循环缓冲数组来实现,这个数组默认大小为1024字节。输入流PipedInputStream从这个循环缓冲数组中读数据,输出流PipedOutputStream往这个循环缓冲数组中写入数据。当这个缓冲数组已满的时候,输出流PipedOutputStream所在的线程将阻塞;当这个缓冲数组首次为空的时候,输入流PipedInputStream所在的线程将阻塞。
public class Demo01 {
public static void main(String[] args) throws Exception{
// PipedInputStream pis = new PipedInputStream();
// PipedOutputStream pos = new PipedOutputStream();
// 将管道输入流和输出流进行关联
// pis.connect(pos); // 输入流连接输出流
// pos.connect(pis); // 输出流连接输入流(效果同上)
// PipedInputStream pis = new PipedInputStream();
// PipedOutputStream pos = new PipedOutputStream(pis); // 创建管道输出流的时候就关联好输入流
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos); // 创建管道输入流的时候就关联好输出流
// 写线程(往管道流中写出数据)
new Thread() {
@Override
public void run() {
try {
Scanner scanner = new Scanner(System.in);
while (true) {
// 接收键盘输入的数据
String str = scanner.next();
// 往管道流中写
pos.write(str.getBytes());
}
} catch (IOException exception) {
exception.printStackTrace();
}
}
}.start();
// 读线程(从管道流里面读取数据)
new Thread() {
@Override
public void run() {
try {
int len;
byte[] data = new byte[1024];
// 从管道流中读取数据
while ((len = pis.read(data)) != -1) {
System.out.println("管道输出流来数据啦:" + new String(data, 0, len));
}
} catch (IOException exception) {
exception.printStackTrace();
}
}
}.start();
}
}
ad(byte[] b) throws IOException`:普通方法 回退一组数据到缓冲区前面。
public void unread(byte[] b,int off,int len) throws IOException
:普通方法 回退指定范围的一组数据到缓冲区前面。
public class Demo01_回退流测试 {
public static void main(String[] args) throws Exception {
String str = "123";
PushbackInputStream pis = new PushbackInputStream(new ByteArrayInputStream(str.getBytes())); // 从内存中读取数据
int data = pis.read(); // 1
System.out.println((char) data);
pis.unread("-".getBytes()); // 回退到流中
data = pis.read();
System.out.println((char) data); // -
data = pis.read(); // 2
System.out.println((char) data);
pis.unread("-".getBytes()); // 回退到流中
data = pis.read();
System.out.println((char) data); // -
data = pis.read(); // 3
System.out.println((char) data);
}
}
需求:在一段数字中截取手机号(11位)
public class Demo02_回退流小案例 {
public static void main(String[] args) throws Exception {
String str = "13079016067131350454961897434920916552444009";
PushbackInputStream push = new PushbackInputStream(new ByteArrayInputStream(str.getBytes())); // 从内存中读取数据
System.out.println("读取之后的数据为:");
int data = 0;
int count = 1;
while ((data = push.read()) != -1) { // 读取内容
System.out.print((char) data);
if (count % 11 == 0) { // 读完了一个手机号
push.unread("-".getBytes()); // 放回到缓冲区之中
data = push.read(); // 再读一遍
System.out.print((char) data);
}
count++;
}
System.out.println();
}
}
11.5 管道流
管道输入与输出实际上使用的是一个循环缓冲数组来实现,这个数组默认大小为1024字节。输入流PipedInputStream从这个循环缓冲数组中读数据,输出流PipedOutputStream往这个循环缓冲数组中写入数据。当这个缓冲数组已满的时候,输出流PipedOutputStream所在的线程将阻塞;当这个缓冲数组首次为空的时候,输入流PipedInputStream所在的线程将阻塞。
public class Demo01 {
public static void main(String[] args) throws Exception{
// PipedInputStream pis = new PipedInputStream();
// PipedOutputStream pos = new PipedOutputStream();
// 将管道输入流和输出流进行关联
// pis.connect(pos); // 输入流连接输出流
// pos.connect(pis); // 输出流连接输入流(效果同上)
// PipedInputStream pis = new PipedInputStream();
// PipedOutputStream pos = new PipedOutputStream(pis); // 创建管道输出流的时候就关联好输入流
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos); // 创建管道输入流的时候就关联好输出流
// 写线程(往管道流中写出数据)
new Thread() {
@Override
public void run() {
try {
Scanner scanner = new Scanner(System.in);
while (true) {
// 接收键盘输入的数据
String str = scanner.next();
// 往管道流中写
pos.write(str.getBytes());
}
} catch (IOException exception) {
exception.printStackTrace();
}
}
}.start();
// 读线程(从管道流里面读取数据)
new Thread() {
@Override
public void run() {
try {
int len;
byte[] data = new byte[1024];
// 从管道流中读取数据
while ((len = pis.read(data)) != -1) {
System.out.println("管道输出流来数据啦:" + new String(data, 0, len));
}
} catch (IOException exception) {
exception.printStackTrace();
}
}
}.start();
}
}