流的概念
在Java程序中,对于数据的输入输出操作以流Stream方式进行,JavaSE提供各种各样的类用于使用相同的方法获取不同类型的数据,程序中通过标准的方法输入或者输出数据
流是处理输入/输出的一个洁净的方法,它不需要代码理解键盘和网络的不同。Java中流的实现是基于java.io包定义的类层次结构的
流的分类
从Java不同版本上来说,流可以分为BIO、NIO和AIO三大类。Java中的BIO、NIO和AIO理解为是Java语言对操作系统的各种Il0模型的封装。程序员在使用这些API的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用ava的API就可以了。
BIO即同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成
- 假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
NIO即同步非阻塞,一个线程不断的轮询每个输入输出的状态改变,如果有状态发生了改变,则进行下一步的操作
- 拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
AIO即异步非阻塞I/O模型,无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。
- 对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了
按流向分为输入流和输出流,可以从输入流读取数据但不能写,要从输入流读取数据,则必须有一个与这个流相关的字符源
按传输单位分为字节流和字符流
-
Java具备平台无关性,这里的字节是指8位,字符是16位
-
字节流从InputStream/OutputStream派生出来,以字节为基本处理单位,一般用于操作二进制数据,字节次序是有意义的
-
字符流从Reader/Writer派生出来的,以16位的Unicode码表示字符为基本处理单位,一般用于操作字符数据
-
使用桥接流可以实现两个流之间的转换
按功能还可以分为节点流和过滤流
节点流:负责数据源和程序之间建立连接,结点流对特定的地方读写
过滤流:用于给节点增加功能,过滤流使用结点流进行输入/输出并添加附加功能
过滤流的构造方式是以其他流位参数构造(这样的设计模式称为装饰模式)。Java的IO流使用装饰器模式,将IO流分成底层节点流和上层处理流。其中节点流用于和底层的物理存储节点直接关联。过滤流是连接在已存在的流之上,通过对数据的处理为程序提供更为强大的读写功能。
注意:IO流是一类很宝贵的资源,使用完后必须调用close()方法关闭流并释放资源。在关闭流时只用关闭最外层的流
字符流就是字节流读取文字字节数据后,不直接操作而是先查指定的编码表以获取对应的文字。简单的说:字符流=字节流+编码表
- 字符流的两个顶层父类:Reader和Writer
- 字节流的两个顶层父类: InputStream和OutputStream
- 输入输出靠reader和writer, inputstream和outputstream四个类和
- 子类支持·读入的内容有对象,字符,图像和声音等
File类
java.io.File用于封装和平台无关的文件夹和文件对象
例如获取一个文件的字节数
File ff=new File("dd.txt");
//1ength():1ong用于获取文件的字节数,如果文件不存在则返回01/一个汉字=2B
System.out.println(ff.length()+"B");
这个写法是针对windows平台的,如果使用mac或者linux平台,则需要使用
常见的构造方法
-
·File(String pathName)以path为路径创建File对象,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储
new File("dd.txt");使用的就是相对路径,这个路径将从项目的根目录开始算起 使用的分隔符是/,不是\,因为\在java种表示转移字符,如果需要使用\,则必须写成\\ new File("d:/data/dd.txt")使用绝对路径,这里从d:开始,当然是windows系统 String ss=system.getProperties().getProperty("user.dir"); System.out.println(ss);//D: \workspace\eclipse-2020\2022-01-04
-
File(String parent,String child)这里文件对象的路径为相对于parent路径的child路径,相当于parent+" /""+child
静态属性
Separator存储当前系统的路径分隔符
注意:\在字符串中为转义字符,如果需要使用\则必须写成\l。一般推荐使用/
访问文件的方式
- getName():String获取文件名称
- getPath():String获取路径,如果构建File时使用的是相对路径,则这里返回的就是相对路径;如果创建时参数为绝对路径,则返回绝对路径
- getAbsolutePath():String获取绝对路径
- getParent():String获取当前文件对象的上级File对象,如果构建文件对象时使用的是相对路径,则这里返回为null
文件检测相关方式
- canWrite():boolean是否可写canRead():boolean是否可读
- isFile():boolean是否是文件,因为File对象可以封装文件和文件夹
- isDirectory():boolean是否为文件夹
- isAbsolute():boolean是否为绝对路径lastModified():long文件的最后修改时间
常见操作
- createNewFile():boolean创建一个新文件
这个方法中有个受检型异常,需要进行处理,处理方法: trylcatch结构或者在方法上throws抛出异常如果文件不存在则返回true,表示创建成功;如果文件已经存在则返回false,表示创建失败 - static createTempFile(String prefix, string suffix);:File这个临时文件名称会有随机内容临时文件的默认存放位置为操作系统默认的临时文件位置
[c: \users\Administrator\AppData\Loca7\Temp] - exists():boolean文件或者文件夹是否存在
- .length():long获取文件大小,单位为字节,如果是文件夹则返回值没有指定. renameTo(File):boolean修改名称,可以修改文件名和文件夹名称
- delete():boolean删除文件。也可以删除文件夹,要求文件夹为空,不能有文件和子文件夹
注意:因为具体的文件或者文件夹的删除操作是由操作系统负责实现的,所以Java不能保证一定能够删除成功! - deleteOnExit():void退出系统时自动删除·目录操作方法
- mkdir():boolean只能创建一层文件夹,如果创建d:/a1/a2/a3时,当父文件夹d:/a1/a2不存在时则创建失败
- mkdirs():boolean自动创建多级文件夹
- list():String[]获取当前文件夹的所有子文件信息
- listFiles():File[]获取当前文件夹的所有子文件信息,子文件为File对象static listRoots():File[]列出系统的所有的根路径
文件过滤器FilenameFilter
带参数的listFiles方法支持对于子文件进行过滤,只获取需要满足条件的文件对象
FilenameFilter接口中包含一个accept(File dir,String name)方法,该方法用于对指定File的所有子目录或者文件进行迭代,如果方法返回true则list方法会获取该目录或者文件
匿名内部类的写法:推荐写法
语法糖Lambda表达式
1、函数式接口
@FunctionInterface
public interface FilenameFilter{
2、具体的表达式写法
File[] fs=f.1istFiles((dir,name)->{
return name !=nu11 &&name.endswith(" .ini");
});
简化写法
string[] nameList=file.list((dir,name)->name.endwith(".java")||new
File(name).isDirectory());
Path和Paths以及FilesPath接口
代表一个平台无关的平台路径Files提供工具方法操作文件
Paths提供创建Path的静态工厂方法Path相关方法
Files工具方法
复制文件Files.copy(Paths.get(“T1.java”), new FileOutputStream(“a.txt”))
一次性读取文件的所有行List lines=Files.readAllLines()
典型题目:例如获取c:\windows文件夹的大小【后代文件大小之和】和其中的文件个数等 – 递归
public class Test1 {
public static long number = 0; // 统计文件个数
public static long size = 0;// 统计所有文件大小之和
public static void main(String[] args) {
// File.pathSeparator 根据操作系统不同可以有:(Linux)和;(windows)两个,用于分割
多个路径
// File.separator用于分割路径的不同部分。路径中所使用分隔符有\\和/两种写法
File file=new File("c:/windows");
list(file);
System.out.println("总共文件个数为:"+number);
System.out.println(file.getAbsolutePath()+"总大小为:"+size);
}
public static void list(File file) {
if (file != null) {
if (file.isFile()) { //如果是文件
number++;
size += file.length();
}else if(file.isDirectory()){ //如果是文件夹
File[] children=file.listFiles();
if(children!=null && children.length>0)
for(File tmp:children)
list(tmp);
}
}
}
}
流模型
流是用于隐藏数据传输细节的操作模型,是数据传输的抽象表达,Java程序可以通过流来完成输入/输出 操作
- 根据操作方式的区别BIO阻塞同步、NIO非阻塞同步、AIO非阻塞异步
- 根据流的方向分为输入流和输出流
- 根据传输单位分为字节流和字符流
- 根据功能范围节点流和过滤流
- 装饰模式
装饰模式
Decorate允许向一个现有对象动态添加新功能,同时不改变结构,属于JavaSE 23种设计模式中的结构 型设计模型
- 用途:动态扩展功能
- 优点:装饰类和被装饰类可以独立发展,不会相互耦合
- 缺点:多层装饰比较复杂
要求:抽象
编码实现:4种角色
被装饰方抽象角色
public interface IShape{
void draw(); //public abstract,不是默认package
}
具体的装饰方实现,可以有多个不同实现类
public class Circle implemenets IShape{
public void draw(){
System.out.println("画了一个圆圈");
}
}
装饰抽象角色
public abstract class DecorateShape implements IShape{
private IShape target;//被装饰方对象,就是需要动态追加功能的对象
public DecodateShape(IShape target){
this.target=target;
}
public void draw(){
target.draw(); //具体的操作需要通过被装饰方提供实现
}
}
具体装饰角色
public class RedDecorateShape extends DecorateShape{
public RedDecorateShape(IShape target){
super(target);
}
public void draw(){
System.out.println("使用红色");
super.draw(); //调用父类中的被覆盖的方法
System.out.println("恢复默认的颜色");
}
}
测试类
//调用时进行组装
IShape circle=new Circle(); //节点流
IShape decorate=new RedDecorateShape(circle); //过滤流
decorate.draw();
//也允许这样使用
IShape decorate2=new BlueDecorateShape(decorate);
BIO编程
使用统一接口进行操作,具体实现细节无关
字节流
InputStream和OutputStream,都实现了Closeable接口,所以支持try-resources InputStream操作用于实现数据的读取操作
- read():int 注意这里只是读取一个字节,0-255之间,-1表示流结束
- read(byte[]):int 返回值表示读取的具体字节个数,-1流结束
- close():void 关闭流
- 另外不重要的方法 read(byte[],int,int) available() skip(long)
OutputStream操作方法用于实现数据的写出操作
- write(int):void 写出一个字节,int的低8位
- write(byte[]具体数据,int起始下标,int长度):
- void close():void 关闭流 不重要的方法
- write(byte[])、flush()
样例:使用字节流进行文件的拷贝
public class Test4 {
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream("data/a1.txt");
File file = new File("out/");
if(!file.exists())
file.mkdirs();
os=new FileOutputStream("out/a1.bak");//自动创建文件,如果文件已存在
则采用覆盖
int data=0;
while((data=is.read())!=-1) {
os.write(data);
}
} catch (Exception e) {
System.out.println(e);
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (os != null)
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
字符流
顶级父抽象类Reader和Writer,一次一字符的操作,实现了Closeable接口。如果涉及中文信息,则需 要考虑编码字符集的问题,如果编码字符集错误,则显示乱码
- Reader用于封装字符读取操作
- read():int 返回读取到的字符数据,0-65535 2B,返回-1 表示流末尾
- read(char[]):int 返回读取的字符个数,流结束返回-1
close():void 关闭流
- Writer用于封装字符写出操作
- write(int):void 写出低16位
- write(char[]数据,int起始下标,int写出的字符数):void
- close():void 关闭流,释放资源 write(String):void 写出一个字符串内容到输出流中
节点流
类型 | 字符流 | 节点流 |
---|---|---|
文件 | FileReader和FileWriter | FileInputStream和FileOutputStream |
数组 | CharArrayReader和 CharArrayWriter | ByteArrayInputStream和 ByteArrayOutputStream |
字符串 | StringReader和StringWriter | |
线程通讯使用的管道 | PipedReader和PipeWriter | PipedInputStream和PipeOutputStream |
文件流
FileInputStream和FileReader用于从一个文件中读取数据;FileOutputStream和FileWriter用于向一个 文件中写出数据
FileInputStream文件输入字节流,FileReader文件输入字符流,用法类似
- FileInputStream(“文件名称”),如果文件不存在则
- FileNotFoundException FileInputStream(File)
FileOutputStreram文件输出字节流,FileWriter文件输出字符流,用法类似
- FileOutputStream(“文件名称”);如果文件不存在,则自动创建;如果文件存在则执行覆盖原文件 内容
- FileOutputStream(“文件名称”,boolean是否采用追加方式);如果文件不存在,则自动创建;如果 文件存在并且Boolean参数为true则表示采用追加的方式处理
//从键盘上读取数据,并写入到一个文件中。保留过去的历史数据
public class Test1 {
public static void main(String[] args) throws IOException {
// 键盘的应用对象,系统标准输入设备默认为键盘System.in
InputStream is = System.in;
byte[] buffer = new byte[1024];
int len = is.read(buffer); //遇到回车则自动进行读取操作
if (len > 0) {
OutputStream os = new
FileOutputStream("out/key.txt",true);//不带
参数则默认为false
os.write(buffer,0,len);
os.close();
}
is.close();
}
}
注意:文件流属于节点流,输入流的源端和输出流的目标端都是磁盘文件,沟通方法允许通过文件的路 径名称或者文件对象的方式进行构建
内存数组节点
如果是文本字符则使用char[],如果二进制数据则使用byte[]
CharArrayReader和CharArrayWriter、ByteArrayInputStream和ByteArrayOutputStream数据来源或 者输出目标为数组,CharArray对应char[],ByteArray对应byte[]
输入流
- CharArrayReader(char[]) 其中char[]就是数据的来源,也就是说Reader是从char[]中读取数据
- 不常用 CharArrayReader(char[]数据,int起始下标,int最大长度)
输出流
- CharArrayWriter用于实现向一个字符数组中写入数据,这个数组大小可以自动调整 CharArrayWriter()自动创建一个关联了char[]的输出流
- CharArrayWriter(int)自动创建一个关联了char[]的输出流,这里的参数就是char[]的初始化大小
内存字符串流
读写String中的数据
StringReader用于从一个String中读取数据
String str="我说我爱JAVA";
Reader sr=new StringReader(str); //设置字符串流的数据原来为str对象
int cc;
while((cc=sr.read())!=-1){
System.out.println((char)cc);
}
sr.close()
StringWriter用于向一个StringBuffer中写入数据,实现了一个可变长的字符串
Scanner sc=new Scanner(System.in);
try(StringWriter sw=new StringWriter();
Writer w=new FileWriter("d:/console.txt");
){
String tmp=sc.nextLine();
while(!"quit".equals(tmp)){
if(tmp!=null && tmp.trim().length()>0){
sw.write(tmp+"\n");
}
tmp=sc.nextLine();
}
System.out.println(sw.toString());
w.write(sw.toString());//通过toString方法获取变长字符串内容
}
sc.close()
总结
- 读写文件使用文件流,如果操作文本文件建议使用FileReader和FileWriter,如果操作二进制文件 则建议使用FileInputStream和FileOutputStream
- 需要建立缓冲区,可以考虑建立临时文件,这种方式效率低下。所以一般建议考虑使用 CharArrayReader / CharArrayWriter和ByteArrayInputStream / ByteArrayOutputStream或者 StringReader / StringWriter充当内存缓冲区 如果需要二进制缓冲可以ByteArrayInputStream / ByteArrayOutputStream,
- 如果需要一个字符 缓冲区可以使用CharArrayReader / CharArrayWriter、StringReader / StringWriter
- 如果数据量不是特别大使用CharArrayReader / CharArrayWriter更为方便一些,如果数据量大可 能需要直接操作缓冲区可以使用StringReader / StringWriter
- StringWriter中提供了一个方法getBuffer()可以获取到StringBuffer
过滤流
过滤流的作用就是在节点流的基础上提供额外功能。装饰器模式
处理类型 | 字符流 | 字节流 |
---|---|---|
缓存 | BufferedReader/BufferedWriter | BufferedInputStream / BufferedOutputStream |
过滤处理 | FilterReader / FilterWriter | FilterInputStream / FilterOutputStream |
桥接处理 | InputStreamReader / OutputStreamWriter | |
对象处理 | ObjectInputStream / ObjectOutputStream | |
数据转换 | DataInputStream / DataOutputStream | |
打印功能 | PrintWriter | PrintOutputStream |
行数统计 | LineNumberReader | LineNumberInputStream |
回滚处理 | PushbackReader | PushbackInputStream |
过滤处理
就是装饰器模式中的抽象装饰角色
public abstract class FilterReader extends Reader { //典型的装饰模式,需要继承
Reader
protected Reader in; //被装饰目标
protected FilterReader(Reader in) {//通过构造器要求定义装饰器对象时必须指定被装饰
对象
super(in);
this.in = in;
}
public int read() throws IOException { //调用read方法时,真正执行任务的是被装
饰对象
return in.read();
}
需求:循环13加密
算法思路:要求每个字母向后移动13,但是结果还必须是字母
// 设置'a'==0 +13 ('a'+13)%26=13
// 'n'==13 ('n'+13)%26==0 -->'a'
char c = 'n';
int p = (c + 13 - 'a') % 26 + 'a';
System.out.println((char)p);
注意:这里只进行的小写字母的处理
String ss = "filterreader";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ss.length(); i++) {
char c = ss.charAt(i);
int p = (c + 13 - 'a') % 26 + 'a';
sb.append((char) p);
}
System.out.println(sb.toString()); //svygreernqre
循环13加密处理过滤流的实现
public class SecurityWriter extends FilterWriter {
protected SecurityWriter(Writer out) {
super(out);
}
@Override
public void write(int c) throws IOException {
if (c >= 'a' && c <= 'z') {// 判定小写字母
// Character.isLowerCase((char)c):boolean
c = (c - 'a' + 13) % 26 + 'a';
} else if (c >= 'A' && c <= 'Z') {
c = (c - 'A' + 13) % 26 + 'A';
}
super.write(c); //调用父类中被覆盖的write方法
}
}
测试调用
public class Test {
public static void main(String[] args) throws IOException {
//第一次执行就是加密,将加密后的内容传入再执行加密获取原文
try (
// Reader r = new FileReader("data/Test5.java");
// Writer w = new SecurityWriter(new
FileWriter("out/sec.txt"));
Reader r=new FileReader("out/sec.txt");
Writer w=new SecurityWriter(new
FileWriter("out/source.txt"));
) {
int kk;
while((kk=r.read())!=-1) {
w.write(kk);
}
}
System.out.println("加密完成!");
}
}
桥接转换流
用途:实现字节流和字符流之间的自动转换,从字节输入流读取字节数据,并按照编码规范转换为字 符;向字节输出流写出数据时,先将字符按照编码规范转换为字节,然后再进行输出。使用桥接流时应 该指定编码字符集名称,以便实现流转换,如果不指定,则使用当前默认编码字符集
- InputStreamReader用于将一个InputStream自动转换为Reader
- OutputStreamWriter用于将一个 Writer自动转换为OutputStream
类定义
public class InputStreamReader extends Reader{}
public class OutputStreamWriter extends Writer{}
构造器
InputStreamReader
- InputStreamReader(InputStream)
- InputStreamReader(InputStream in, String charsetName)
- InputStreamReader(InputStream in, Charset cs),直接使用charsetName编码字符集名称的方式 进行设定编码字符集可能会出错,所以可以使用Charset指定编码字符集名称
// int kk=System.in.read();
// System.out.println((char)kk);//但是由于字节流只读取了一个字节,所以对于多字节
编码的中文就会出现乱码问题
Reader r = new InputStreamReader(System.in); // public final static
InputStream in = null;
int kk = r.read(); //只读取一个字符
System.out.println((char)kk);
没有指定编码字符集则使用系统默认的编码字符集,允许用户自定义编码字符集,例如new InputStreamReader(System.in, “iso8859-1”)。iso8859-1是用于欧洲地区的编码字符集,使用的是单字 节编码。
//GBK
Reader r = new InputStreamReader(System.in,"utf-8"); //第二个参数用于指定所使用的
编码字符集,注意需要当前控制台上的编码字符集一致,否则乱码。
int kk = r.read();
System.out.println((char)kk);
一般不建议执行设置编码字符集,除非是必须的
OutputStreamWriter的用法
构造器和InputStreamReader一致
String ss = "设置中心信息和english information";
OutputStream os = new FileOutputStream("out/data.data"); //最终输出使用的是字节
流
Writer writer = new OutputStreamWriter(os);//引入桥接流自动实现字符流转换为字节流进
行输出
writer.write(ss);
writer.close();
缓冲流
缓冲流是套接在相应的节点流之上,对读写操作提供缓冲功能,以提高读写的效率,并引入一个新方法
以硬盘为例,字节流和字符流的最大弊端是每次读写都需要访问硬盘,如果读写的频率较高时性能 一定不佳,为了减少硬盘读写次数,提高执行效率可以引入缓冲流。缓冲流在读取数据时,会一次 性读取较多的数据,在以后的每一次读取数据时,都是现在缓冲区中访问,如果有数据则直接访 问,直到缓存中的所有数据读取完毕后,再到硬盘读取
没有使用缓冲流的时间统计
long start=System.currentTimeMillis();//获取从1970-1-1 0:0:0到执行时的毫秒值
InputStream is = new FileInputStream("data/T1.java");
int kk=is.read();
while(kk!=-1) {
System.out.println((char)kk);
kk=is.read();
}
is.close();
long end=System.currentTimeMillis();
System.out.println("执行时间为:"+(end-start)+"ms");//21ms
引入缓冲流:以浪费内存为代价换取高执行效率
long start=System.currentTimeMillis();//获取从1970-1-1 0:0:0到执行时的毫秒值
InputStream is = new BufferedInputStream(new
FileInputStream("data/T1.java"));
int kk=is.read();
while(kk!=-1) {
System.out.println((char)kk);
kk=is.read();
}
is.close();
long end=System.currentTimeMillis();
System.out.println("执行时间为:"+(end-start)+"ms");//11ms
构造器
字节流
-
BufferedInputStream(InputStream)不定义缓存大小,则默认8192
public class BufferedInputStream extends FilterInputStream { private static int DEFAULT_BUFFER_SIZE = 8192; public BufferedInputStream(InputStream in) { this(in, DEFAULT_BUFFER_SIZE); //调用自己的2个参数的构造 器,DEFAULT_BUFFER_SIZE是当前类的一个常量值 8192 }
-
BufferedInputStream(InputStream,int)人为设置缓冲大小,int单位字节
public BufferedInputStream(InputStream in, int size) {//参数2size表示缓冲区的大 小 super(in); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } buf = new byte[size]; }
-
BufferedOutputStream(OutputStream)
public BufferedOutputStream(OutputStream out) { this(out, 8192); }
-
BufferedOutputStream(OutputStream,int size)自定义缓存区大小
字符流
-
BufferedReader(Reader)默认缓冲区大小为8192字符
-
BufferedReader(Reader,int)int用于定义缓冲区的大小
-
特殊方法: readLine():String 用于读取一行数据
Reader r=new BufferedReader(new FileReader("data/abc.txt")); String str=r.readLine(); //系统在编译器不能识别r对象为BufferedReader类型,所以不能直接调用BufferedReader中的特殊 BufferedReader r = new BufferedReader(new FileReader("data/T1.java")); String ss=r.readLine(); //也可以使用窄化操作
-
-
BufferedWriter(Writer)默认缓冲区大小为8192字符
-
BufferedWriter(Writer,int)int用于自定义缓冲区的大小,单位为字符B
特殊方法
BufferedReader中提供了一个特殊方法readLine():String,但是在BufferedInputStream中并没有这个 方法
-
整行读取数据,以\r或者\n为分隔符(换行符)
-
如果读取内容为null,则表示读取到了流末尾
-
readLine方法的返回值会自动剔除末尾的换行符
public class T2 { public static void main(String[] args) throws IOException { BufferedReader r = new BufferedReader(new FileReader("data/T1.java")); String ss = r.readLine(); while (ss != null) { if (ss.trim().length() > 0) System.out.println(ss); 使用println表示输出结束后自动换行, 通过这种方式可以给readLine剔除的换行符进行补偿 ss = r.readLine(); } r.close(); } }
-
-
BufferedWriter中提供了一个方法newLine可以写入一个换行符
public class T2 {
public static void main(String[] args) throws IOException {
BufferedReader r = new BufferedReader(new
FileReader("data/T1.java"));
String ss = r.readLine();
while (ss != null) {
if (ss.trim().length() > 0)
System.out.print(ss+"\n");//在字符串末尾手动添加\n也可以补偿
换行符
ss = r.readLine();
}
r.close();
}
}
对于输出的缓冲流,写入的数据会受限缓存在内存,使用flush方法可以清空缓存,同时将以前的缓存数 据立即写出
键盘录入
简单用法Scanner扫描器接收键盘录入
System.in:InputStream用于指代系统默认的输入设备—键盘
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入一行数据,以回车结尾");
String temp="";
while((temp=br.readLine()).trim().length()>0){
if("quit".equals(temp))
break;
System.out.println(temp);
}
br.close();
屏幕输出
System.out:PrintStream用于指代系统默认的标准输出设备—屏幕显示 System.err:PrintStream用于指代系统默认的错误输出设备—屏幕显示
注意:在程序中同时使用System.out和System.err输出时,不能保证准确的输出顺序。所以为了查看执 行顺序,一般只能选择其中一个进行使用
强调
- 一般使用缓存流的目标是在于以透明的方式引入缓冲区,以提高代码的执行效率,所以不会使用什 么特殊的方法,还是使用Reader/Writer、InputStream/OutputStream之类的父类中提供的方法
- 特殊的是BufferedReader方法readLine
- 过滤流使用必须有对应的节点流,因为过滤流是用于装饰节点流的,不是有具体的操作目标
- 执行close方法会自动关闭被装饰的对象,所以不需要再关闭FileReader、FileWriter
- 执行flush方法会自动刷新数据到被装饰的流上,但是并没有执行关闭流。针对输出流关闭时会自动 先flush然后再关闭
数据流
DataInputStream和DataOutputStream允许与机器无关的风格读写java原始类型的数据,比较适合于 网络上的数据传输
- 只有字节流,没有对应的字符流
构造器
- DataInputStream(InputStream)
- DataOutputStream(OutputStream)
练习题:读写一个double类型的数据到文件
double dd=123.4567;
//使用数据流直接操作的方法
FileOutputStream fos=new FileOutputStream("out/abc.data");
fos.write((dd+"").getBytes()); //FileOutputStream中并没有直接写出double类型数据
的方法,所以只能将数据转换为字符串进行输出
fos.close();
FileInputStream fis=new FileInputStream("out/abc.data");
byte[] buffer=new byte[1024];
int len=fis.read(buffer);
String ss=new String(buffer,0,len);
double kk=Double.parseDouble(ss);
System.out.println(kk);
fis.close();
加入一个double,然后再写一个String,然后再写一个int
为了区分数据,则需要在数据中混入一些特殊符号,例如@@,将数据转换 为"123.4567@@xiaopangzi@@12"这样格式写出到输出文件中,读取数据后,再使用split进行切分。 从功能角度上说来实现是没问题的,但是编码过于繁琐了
针对Java中的各种基本数据类型和String类型,可以直接使用Data数据流以简化编程
DataOutputStream dos = new DataOutputStream(new
FileOutputStream("out/abd.data"));
dos.writeDouble(123.4567);
//为了直接操作字符串类型数据,可以额外引入一个数据,用于记录字串中字符个数。最终读取时使用
String ss="赵小胖说:'我爱张毅'";
int len=ss.length();
dos.writeInt(len);
dos.writeChars(ss);
dos.writeInt(18);
dos.close();
DataInputStream dis=new DataInputStream(new
FileInputStream("out/abd.data"));
double d1=dis.readDouble(); //对应位置的操作
int len1=dis.readInt();//获取字串中字符个数
StringBuilder sb=new StringBuilder();
for(int i=0;i<len1;i++)
sb.append(dis.readChar());//一次读取一个字符,然后通过StringBuilder将其连接为
字符串
int age=dis.readInt();
dis.close();
System.out.println(d1+"\t"+sb.toString()+"\t"+age);
数据类型 | 输入流DataInputStream | 输出流DataOutputStream |
---|---|---|
整型 | readByte、readShort、readInt、 readLong | writeByte、writeShort、writeInt、 writeLong |
浮点型 | readFloat、readDouble | writeFloat、writeDouble |
布尔型 | readBoolean | writeBoolean |
字符型 | readChar | writeChar |
数据流是通过EOFException用于判断流结束end-of-file
DataInputStream dis=new DataInputStream(new FileInputStream("out/不存在的文
件.data"));
double d1=dis.readDouble(); //直接EOFException
针对字符串类型建议使用readUTF和writeUTF进行读写操作
public class T3 {
public static void main(String[] args) throws IOException {
// 记录张三的个人信息
DataOutputStream dos = new DataOutputStream(new
FileOutputStream("out/pang.data"));
dos.writeLong(123456);
dos.writeUTF("张三");
dos.writeInt(18);
dos.close();
// 读取个人信息
DataInputStream dis = new DataInputStream(new
FileInputStream("out/pang.data"));
//务必写出顺序和读取顺序一致
// int kk=dis.readInt();
long kk=dis.readLong();
String ss=dis.readUTF();
Integer age=dis.readInt();
dis.close();
System.out.println(kk+"\t"+ss+"\t"+age);
}
}
打印流
打印流都是用于实现打印输出,其中有PrintWriter和PrintStream,分别针对字符和字节,都提供了一 系列重载的print和println方法,可以输出多种类型数据
print(Object):void/println(Object):void
PrintWriter pw = new PrintWriter(new FileWriter("out/pw.txt"));
for(int i=0;i<10;i++) {
pw.print("数值:"+i);
pw.print(i);
pw.println(i%2==0);
}
pw.close();
输出引用类型:具体实现是通过调用对象的toString()方法将对象转换为String进行输出
public void println(Object x) {
String s = String.valueOf(x); //输出Object类型数据时,首先调用String类中定义的
valueOf方法,
synchronized (lock) {
print(s); //打印输出字符串
println(); //产生换行 print('\n')
}
}
String中valueOf方法的定义
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString(); //为了避免obj对象为null,调用toString出现NullPointerException,所以使用三元表达式,当obj为空时,输出字符串null,否则调用obj的toString方法
}
**注意 **:
PrintWriter和PrintStream的输出操作不会抛出异常,用户可以通过错误状态检查获取错误信息
PrintWriter和PrintStream都有自动的flush功能
特殊构造器
- public PrintWriter (Writer out)
- PrintWriter(Writer out,boolean autoFlush) boolean表示是否支持自动刷新
- PrintWriter(OutputStream out) 直接使用OutputStream时不需要通过桥接处理
- PrintWriter(OutputStream out, boolean autoFlush)
- PrintStream(OutputStream out)
- PrintStream(OutputStream out, boolean autoFlush)
- PrintStream(String fileName) 参数为文件名称,表示数据直接输出到指定文件中 PrintStream(File file)
对象流
可以使用DataInputStream和DataOutputStream读写对象数据,但是操作繁琐
操作对象为Account账户类型的对象
账户类定义
public class Account {
private Long id;//账户编号
private String username;//账户名称
private String password;//账户口令
private Double balance;//余额
添加账户数据
public class Regist {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new
InputStreamReader(System.in));
System.out.println("账户编号:");
String ss=br.readLine();
Long id=Long.valueOf(ss);
System.out.println("账户名称:");
String username=br.readLine();
System.out.println("账户口令:");
String password=br.readLine();
System.out.println("存款数量:");
ss=br.readLine();
double balance=Double.parseDouble(ss);
DataOutputStream dos=new DataOutputStream(new
FileOutputStream("out/account.data",true));
dos.writeLong(id);
dos.writeUTF(username);
dos.writeUTF(password);
dos.writeDouble(balance);
dos.close();
}
}
显示账户信息
public class Show {
public static void main(String[] args) throws Exception {
DataInputStream dis = new DataInputStream(new
FileInputStream("out/account.data"));
while (true) {
try {
Long id = dis.readLong();
String username=dis.readUTF();
String password=dis.readUTF();
double balance=dis.readDouble();
System.out.println(id+"\t"+username+"\t"+password+"\t"+balance);
} catch (EOFException e) {
break;
}
}
dis.close();
}
}
问题
所有的读写操作都是自行编码实现的,而且真的不面向对象。Account类定义根本没有什么作用 写出操作
dos.writeLong(id);
dos.writeUTF(username);
dos.writeUTF(password);
dos.writeDouble(balance);
读入操作
Long id = dis.readLong();
String username=dis.readUTF();
String password=dis.readUTF();
double balance=dis.readDouble();
Java提供了ObjectInputStream和ObjectOutputStream可以直接读写Object对象,实际上还提供了针对 8种简单类型和String类型的读写操作方法
ObjectInputStream提供了方法readObject():Object
ObjectOutputStream提供了方法writeObject(Object):void
将java.util.Date类型当前时间写入到now.data文件中,然后再重新从文件读取出来
Date now = new Date(); // 获取系统当前时,这是一个引用类型
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new
FileOutputStream("out/now.data")));
oos.writeObject(now); // 将对象直接写出
oos.close();// 关闭流, try/finally或者try-resources
ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new
FileInputStream("out/now.data")));
Object obj = ois.readObject(); // 从文件中直接读取对应的对象
System.out.println(obj);
//readObject获取到的是Object类型的数据,如果需要调用Date类中的特殊方法,则需要进行数据
类型转换(窄化操作)
if(obj!=null && obj instanceof Date){
Date dd=(Date)obj;
System.out.println(dd.getYear()+1900);
}
对象流
序列化
读写一个对象的前提是这个类型的对象是可序列化的
- 对象的序列化简单的来说就是将对象可以直接转换为二进制数据流
- 对象的反序列化将二进制数据流转换为对象
针对对象的序列化和反序列化是通过JVM实现的,编程中只做声明,序列化的目标就是将对象保存到磁 盘中或者允许在网络中直接传动
编程应用
1、依靠Serializable接口进行声明,如果需要特殊操作可以实现Externalizable接口。Serializable接口 属于旗标接口【标识接口】,仅仅只起到说明的作用,没有需要实现的方法
1 public interface Serializable { }
重新定义Account类
public class Account implements Serializable{
private Long id;//账户编号
private String username;//账户名称
private String password;//账户口令
private Double balance;//余额
2、可以通过ObjectInputStream和ObjectOutputStream提供的方法直接读写对象 通过序列化将一个对象持久化存储到文件中
Account account = new Account();
account.setId(99L);
account.setUsername("张三");
account.setPassword("123456");
account.setBalance(1234.56);
// 存储对象到文件中
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new
FileOutputStream("out/account.data")));
oos.writeObject(account);
oos.close();
System.out.println("序列化存储对象完成");
通过反序列化将一个对象从文件中读取到内存
ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("out/account.data"));
Object obj=ois.readObject();
System.out.println(obj);
ois.close();
注意:通过序列化写出对象,在通过反序列化读取对象的过程中,一般要求不允许修改类定义,否则 InvalidClassException
1、接口问题
需要通过对象流读写的对象必须实现序列化接口,否则NotSerializableException
public class Account implements Serializable
潜在要求:要求类必须由无参构造器,因为反序列化时需要调用无参构造器构建对象。否则 Exception in thread “main” java.io.InvalidClassException: com.yan.test01.Account; no valid constructor
如果需要针对某个类型的对象进行序列化,要求该类型的所有属性也需要支持序列化,否则出现类似 Exception in thread “main” java.io.NotSerializableException: com.yan.test03.Role 报 错
public class Account implements Serializable {
private static final long serialVersionUID = 4093553615911145103L;
private Long id;// 账户编号
private String username;// 账户名称
private transient String password;// 账户口令
private Role role;
private Double balance;// 余额
2、特殊接口
Serializable接口属于标识接口,没有需要实现的方法,所有的序列化和反序列化操作都是由虚拟机负责 实现。 Externalizable接口定义,可以支持用户自定义实现序列化的细节操作,除非特殊需求一般不使用 Externalizable接口,因为没有必要自定义
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;//自定义实现对象的序
列化操作
void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException;//自定义实现反序列化操作
}
Externalizable用法
public class Account implements Externalizable {
private Long id;// 账户编号
private String username;// 账户名称
private String password;// 账户口令
private Double balance;// 余额
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//执行对象的序列化操作,就是将需要写出的数据写入到out对象中
out.writeLong(this.id);
out.writeUTF(this.username);
out.writeUTF(this.password);
out.writeDouble(this.balance);
}
@Override
public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
//执行对象的反序列化操作,就是从in对象中读取数据,不需要自行new对象。注意读写顺序必须一致,否则出错
this.id=in.readLong();
this.username=in.readUTF();
this.password=in.readUTF();
this.balance=in.readDouble();
}
特殊的应用场景
某个类型的属性是不可序列化。例如对象中包含InputStream/OutputStream之类的资源类型的属性, 不仅不能进行序列化,而且在序列化之前还应该释放资源,在反序列化时应该重新创建资源连接。
public class Account implements Externalizable{
private Long id;
private String username;
private transient String password;//可以被序列,但是按照业务需求不进行序列化
private InputStream is; //不能序列化
public void writeExternal(ObjectOutput out) throws IOException {
is.close(); //将特定属性资源链接关闭
//执行对象的序列化操作,就是将需要写出的数据写入到out对象中
out.writeLong(this.id);
out.writeUTF(this.username);
}
public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
this.is=new FileInputStream("photo.jpg");
//执行对象的反序列化操作,就是从in对象中读取数据,不需要自行new对象。注意读写顺序
必须一致,否则出错
this.id=in.readLong();
this.username=in.readUTF();
}
}
3、类型转换
通过反序列化获取的对象是Object类型,如果调用对象中特殊的方法则必须先进行窄化操作
object obj=objectInputStream.readObject();
if(obj!=null && obj instanceof Account){
Account account=(Account)obj;
//调用Account类中的特殊方法
}
4、序列号问题
Eclipse中针对实现了序列化接口的类定义会有这样一个警告信息,要求提供一个序列号 serialiVersionUID。这不是错误信息
序列版本号可以不用添加,这个序列版本号是一个中在序列化和反序列化操作中快速识别类型的简单方 法。
5、特殊关键字
一般针对敏感数据不应该进行序列化操作,例如当前账户的口令属性。针对不需要进行序列化操作的属 性可以添加一个关键字transient,表示该属性不参与序列化和反序列化操作
public class Account implements Serializable {
private static final long serialVersionUID = 4093553615911145103L;
private Long id;// 账户编号
private String username;// 账户名称
private transient String password;// transient用于声明该属性不参与序列化操作
6、流结束
读文件可以通过EOFException异常来判断读取文件结束 读取一个文件中所存储的所有对象
try(
ObjectInputStream ois=new ObjectInputStream(new
FileInputStream("account.data"));
){
while(true){
try{
Object tmp=ois.readObject();
... ...
} catch(EOFException e){//用于判断文件结束
break;
}
}
}
追加文件处理的问题
// 存储对象到文件中
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new
FileOutputStream("out/account.data",true)));//true表示对文件进行追加操作
oos.writeObject(account);
oos.close();
写入数据不会有任何问题,但是读取数据则会出现异常Exception in thread "main"java.io.StreamCorruptedException: invalid type code: AC
解决方案:如果需要添加对象数据时,不要使用FileOutputStream中的true进行追加,而是先读取所有 对象数据,然后再次写出覆盖
Object[] arr=new Object[100];
int counter=0;
File ff=new File("out/account.data");
if(ff.exists()){
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(ff));
while(true){
try{
Object tmp=ois.readObject();
acc[counter++]=tmp;
} catch(EOFException e){
break;
}
}
ois.close();
}
//追加新数据
Account newAccount=new Account();
newAccount.setId(50L);
newAccount.setUsername("中方"); ......
arr[counter++]=newAccount;
//统一将数组中的所有对象写出到文件中
ObjectOutputStream ois=new ObjectOutputStream(new
FileOutputStream("out/account.data")); //没有true则表示覆盖
for(int i=0;i<arr.length;i++){
ois.writeObject(arr[i]);
}
ois.close();
序列化总结
java序列化就是将一个对象转换为一个二进制表示的字节数组,通过保存或者转移这些数组达到传递对 象或者持久化存放对象的目的。序列化要求类必须实现对应的序列接口,两个序列化接口 Serializable【标识接口】和Externalizable【自定义方法实现属性的序列化操作】。反序列化就是将将 二进制数组转换为对象的过程,在执行反序列化时必须有原始的类定义才能将对象进行还原。
- 当父类实现了Serializable接口,则所有的子类都可以序列化;子类实现了Serializable接口父类没 有实现,则父类中的属性不能被序列化,会有数据丢失
- 如果实现了Serializable接口,类中包含引用类型的属性,则该属性必须可序列化,否则报错。可以 通过Externalizable接口进行自定义的序列化
- 反序列化时如果serialVersionUID修改的化,则反序列化失败
- 在Java环境下使用序列化机制JVM支持的都是很好的,但是在多语言环境下可以使用别的序列化机 制,例如xml、json等
serialVersionUID用于确保类序列化和反序列化的兼容性问题。编译器推荐2中方式,一种是生成默认的 versioID,还有一种是根据类名、接口名、成员方法和属性生成一个64为的哈希字段
RandomAccessFile对象
RandomAccessFile不属于IO流,支持对文件的随机读写
//类定义,属于java.io包中的类,但是不同于IO类的定义
public class RandomAccessFile implements DataOutput, DataInput, Closeable
- DataInput接口中定义了基本数据类型的读操作,例如readInt/readDouble等
- DataOutput接口定义了基本数据类型的写操作,例如writeInt/writeDouble等
- Closeable接口表示支持自动关闭,可以使用try-resources
RandomAccessFile是Java输入/输出流体系中功能最丰富的文件内容访问类,既可以读取文件内容,又 可以向文件中输出数据。提供了文件指针的功能,从而实现随机读写文件
构造器
RandomAccessFile在创建对象时,除了指定需要操作的文件之外,还需要指定一个mode参数,表示访 问模式
- r表示以只读方式打开文件,如果执行写操作则抛出异常
- rw表示以读写的方式打开文件,如果文件不存在则自动创建
- rws以读写的方式打开文件,还要求对文件的内容或元数据的每个更新都同步到底层存储设备中
- rwd只是对文件内容同步更新,不会修改文件的元数据
try {
RandomAccessFile raf=new RandomAccessFile("out/raf01.data", "rw");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
写入操作
public class Test1 {
public static void main(String[] args) {
try {
RandomAccessFile raf=new RandomAccessFile("out/raf01.data",
"rw");
raf.writeInt(100);
raf.writeUTF("中国热门解放军");
raf.writeDouble(1234.567);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
读取操作
public class Test2 {
public static void main(String[] args) {
try {
RandomAccessFile raf=new RandomAccessFile("out/raf01.data",
"rw");
int id=raf.readInt();
String name=raf.readUTF();
double salary=raf.readDouble();
System.out.println(id+"\t"+name+"\t"+salary);
raf.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
读取文件如果到达文件末尾则抛出异常EOFException,采用的异常进行判断文件结束
文件位置指针
典型的应用就是多线程下载和断点续传的实现
RandomAccessFile对象中包含了一个记录指针,用于标识当前读写的具体位置。当创建一个对象时, 该文件指针指向文件头部,也就是0,当读写n个字节后,文件记录指针则向后移动了n个位置。如果需 要允许自由移动该记录指针,移动位置为[0,file.length()]
-
getFlilePointer():long 返回文件记录指针的当前位置
-
seek(long):void 将文件记录指针移动到具体的位置
-
skipBytes(int) 相对于当前位置跳过int个字节
练习:
使用RandomAccessFile将一个文本文件倒置读出,并在控制台上显示
public class Test3 {
public static void main(String[] args) throws Exception {
File ff = new File("out/abc1.txt");
if(ff.exists())
ff.delete();
//生成文件
try (RandomAccessFile raf = new RandomAccessFile(ff, "rw");) {
String str="窗前明月光,疑似地上霜";
for(int i=0;i<str.length();i++)
raf.writeChar(str.charAt(i));
}
//倒序读出文件
long len = ff.length();
try (RandomAccessFile raf = new RandomAccessFile(ff, "rw");) {
while (true) {
len-=2;
if(len<0)
break;
raf.seek(len);
char cc=raf.readChar();
System.out.print(cc);
}
}
}
}
读取操作
public class Test2 {
public static void main(String[] args) {
try {
RandomAccessFile raf=new RandomAccessFile("out/raf01.data",
"rw");
int id=raf.readInt();
String name=raf.readUTF();
double salary=raf.readDouble();
System.out.println(id+"\t"+name+"\t"+salary);
raf.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
读取文件如果到达文件末尾则抛出异常EOFException,采用的异常进行判断文件结束
文件位置指针
典型的应用就是多线程下载和断点续传的实现
RandomAccessFile对象中包含了一个记录指针,用于标识当前读写的具体位置。当创建一个对象时, 该文件指针指向文件头部,也就是0,当读写n个字节后,文件记录指针则向后移动了n个位置。如果需 要允许自由移动该记录指针,移动位置为[0,file.length()]
-
getFlilePointer():long 返回文件记录指针的当前位置
-
seek(long):void 将文件记录指针移动到具体的位置
-
skipBytes(int) 相对于当前位置跳过int个字节
练习:
使用RandomAccessFile将一个文本文件倒置读出,并在控制台上显示
public class Test3 {
public static void main(String[] args) throws Exception {
File ff = new File("out/abc1.txt");
if(ff.exists())
ff.delete();
//生成文件
try (RandomAccessFile raf = new RandomAccessFile(ff, "rw");) {
String str="窗前明月光,疑似地上霜";
for(int i=0;i<str.length();i++)
raf.writeChar(str.charAt(i));
}
//倒序读出文件
long len = ff.length();
try (RandomAccessFile raf = new RandomAccessFile(ff, "rw");) {
while (true) {
len-=2;
if(len<0)
break;
raf.seek(len);
char cc=raf.readChar();
System.out.print(cc);
}
}
}
}