目录
6. 递归删除目录:遍历指定文件夹下的所有子目录和文件并删除
使用BufferedReader类的String readLine()方法读取键盘
9. System.in 和 System.out 使用问题
自己实现BufferedReader,BufferedWriter
什么是IO流?
I(Input)输入:读取资源数据,O(Output)输出:输出数据。
什么是流(Stream)?不同系统的内核给我们提供了不同的资源(如文件)操作,流(Stream):即(系统资源)
Java语言本身不会操作系统资源,而是使用系统给我们提供的资源进行IO操作
数据长度单位转换
1byte = 8bit (位)
1K = 1024byte
1M = 1024K
1G = 1024M
1T = 1024G
File类:操作文件和属性
要操作文件里的数据,首先得了解怎么操作文件
1. 创建File
File file = new File("c:\\1.txt");
File file1 = new File("c:\\", "1.txt");
File dir = new File("c:\\");
File file2 = new File(dir, "1.txt");
在Java中,一个斜杠【\】代表着转意,\\两个斜杠就是把后一个斜杠\转意成字符。
如果这个程序在Unix系统运行,就会出现文件分隔符的问题,Java是跨平台语言,提供了相应的方法获取系统属性
2. 获取系统属性——文件分隔符、路径分隔符
java.lang.System类
//获取指定键指示的系统属性
public static String getProperty(String key)
示列:
String file_separator = System.getProperty("file.separator");
System.out.println(file_separator);
String path_separator = System.getProperty("path.separator");
System.out.println(path_separator);
输出:
\
;
Key是固定的
当然File类给我们提供了字段
不同的地方在于一个返回的是char,一个是String
System.out.println(File.separator);
System.out.println(File.separatorChar);
System.out.println(File.pathSeparator);
System.out.println(File.pathSeparatorChar);
输出:
\
\
;
;
换行符
因为不同的系统的换行符是不一样的,所以我们在输出要换行时应该获取系统的换行符。
//System的两个静态方法都能获取系统的换行符
System.lineSeparator();
System.getProperty("line.separator")
3. File类的常见方法
- boolean exits() 是否存在
- boolean mkdirs() 创建多级文件夹(如果封装的抽象路径没有,则创建包括:父目录,子目录。new File("C:\\a\\b\\c\\d\\e"))
- boolean mkdir() 创建文件夹(只能创建指定目录下的一级目录)
- boolean createNewFile() 创建文件(文件不一定有后缀名,创建的目录也可以带点 .)
- boolean delete() 删除文件或目录(当删除的是一个目录时,目录里必须为null,否则删除失败)
- boolean IsDirectory() 是否是文件夹
- boolean IsFile() 是否是文件
- boolean IsHidden() 是否隐藏
- boolean CanRead() 是可读
- boolean CanWrite() 是否可写
- long lastModified() 最后一次修改时间(单位毫秒值);
- long length() 文件长度大小 (以字节为单位)
- boolean renameTo(File dest) 重命名
- String getParent() 获得父目录
- String getAbsolutePath() 获得绝对路径
- long getTotalSpace() 返回此抽象路径名指定的分区总大小
- long getFreeSpace() 返回此抽象路径名指定的分区未分配的字节数
- static File[] listRoots() 系统有效盘符都拿到
- String[] list() 返回指定目录下的 目录和文件的名称(包含隐藏文件)
- File[] listFiles() 返回指定目录下的 目录和文件的名称(包含隐藏文件)(可操作目录或文件,也意味着需要更多内存)
不管创建的File对象的抽象路径是否存在,对象的方法都是可用的
4. FilenameFilter 接口:文件名过滤器
需求:查找的目录或文件,只能有指定包含的名字或后缀名!
普通情况下查找:
File file = new File("F:\\");
String[] names = file.list();
for (String name : names) {
if (name.endsWith(".java")) { //只要有此后缀名的文件
System.out.println(name);
}
}
输出
StringDemo.java
4.1 使用 FilenameFilter 接口
此接口只有一个重写的方法 boolean accept (File dir, String name)
boolean accept(File dir, String name)
dir : 指定的目录下
name : 目录名或文件名
FilenameBySuffix.java(后缀名过滤器)
public class FilenameBySuffix implements FilenameFilter {
private String suffix;
FilenameBySuffix(String suffix) {
this.suffix = suffix;
}
@Override
public boolean accept(File dir, String name) {
return name.endsWith(suffix);
}
}
main:
File file = new File("F:\\");
String[] names = file.list(new FilenameBySuffix(".java"));
for (String name : names) {
System.out.println(name);
}
输出:
StringDemo.java
FilenameByContent.java(包含内容过滤器)
public class FilenameByContent implements FilenameFilter {
private String content;
FilenameByContent(String content) {
this.content = content;
}
@Override
public boolean accept(File dir, String name) {
return name.contains(content);
}
}
main:
File file = new File("F:\\");
String[] names = file.list(new FilenameByContent("Demo"));
for (String name : names) {
System.out.println(name);
}
输出:
StringDemo.class
StringDemo.java
5. FileFilter接口:文件过滤器
boolean accept(File pathname)
pathname : 得到的是抽象文件对象File。这个比FilenameFilter接口更为强大,同样能实现文件名过滤。
MyFileFilter.java(文件过滤器)
逻辑为判断是否是文件而不是目录都列出
public class MyFileFilter implements FileFilter {
@Override
public boolean accept(File pathname) {
return pathname.isFile();
}
}
mian:
File file = new File("C:\\");
File[] paths = file.listFiles(new MyFileFilter());
for (File path : paths) {
System.out.println(path);
}
输出了C盘的文件,包括隐藏文件
C:\bootmgr
C:\BOOTNXT
C:\hiberfil.sys
C:\swapfile.sys
只要更改一下文件过滤器逻辑,同样能实现文件名过滤
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".java");
}
6. 递归删除目录:遍历指定文件夹下的所有子目录和文件并删除
递归:无非就是方法里调用方法本身,使用递归一定要判断条件,否则会出现栈内存溢出异常。
Exception in thread "main" java.lang.StackOverflowError
public static void show() {
show();
}
递归删除目录
因为删除一个目录,里面必须没有内容,如果有内容必须从里向外删。
File file = new File("F:\\test");
deleteDir(file);
public static void deleteDir(File dir) {
System.out.println(dir); //输出进入的目录
// 获取指定目录下的目录和文件夹
File[] files = dir.listFiles();
for (File file : files) {
if (file.isDirectory()) { //判断是目录则继续遍历此目录
deleteDir(file); //会遍历到最里层的目录,方法压栈进栈
} else { //判断是文件则删除
System.out.println(file + ": " + file.delete());
}
}
// 遍历完一个指定目录,这个目录里已经没有内容
// 此方法准备出栈,删除这个没人内容的目录
System.out.println(dir + ": " + dir.delete());
}
输出:
F:\test
F:\test\a
F:\test\a\a.txt: true
F:\test\a: true
F:\test\b
F:\test\b\b.txt: true
F:\test\b: true
F:\test\c
F:\test\c\c.txt: true
F:\test\c: true
F:\test: true
当然我们指定的目录可能不存在,所以判断不存在则退出
if (!dir.exists()) {
return;
}
IO流
1. 基类
- 字节流的抽象基类
InputStream(读)、OutputStream(写)。
可操作计算机二进制文件,如.mp3, .mp4, .txt, .jpg。等等
- 字符流的抽象基类
Reader(读)、Writer(写)。
针对字符的,如中文、日文,编码的乱码现象就是在此解决
2. 输出流:自动创建目的问题
我先用最简单的输出流示范
我的F盘符下没有文件
我创建一个字节输出流对象,并运行
public static void main(String[] args) throws Exception {
FileOutputStream out = new FileOutputStream("F:\\test.txt");
}
此时自动创建了一个test.txt
我用windows的记事本输入abc并保存
然后再运行main函数,此时会覆盖掉这个test.txt并清空内容
结论
- 输出流所关联的目的地,如果不存在则自动创建,如果存在则默认覆盖(先前文件有内容也清空)。
- 输出流每次写出的内容都会覆盖掉上一次的内容。
- 要想如果文件存在时不清空内容,则使用带布尔值的构造:输出内容会续写且不覆盖
FileOutputStream out = new FileOutputStream("F:\\test.txt", true);
3. 输入流:读取文件数据到数组问题
FileOutputStream out = new FileOutputStream("F:\\test.txt");
out.write("abc".getBytes()); //写入abc
out.close();
FileInputStream in = new FileInputStream("F:\\test.txt");
byte[] buffer = new byte[2]; //创建缓冲区大小为2字节
//每次读取2字节
//读取到的数据存入缓冲区,并返回读取的字节数
//如果读取的内容小于缓冲区则全部读取,读取到末尾无数据则返回 -1
while ((in.read(buffer)) != -1) {
System.out.println(new String(buffer)); //构造String使用平台默认编码表进行解码
}
in.close();
输出:
ab
cb
问题看图:
改代码:
FileOutputStream out = new FileOutputStream("F:\\test.txt");
out.write("abc".getBytes()); //写入abc
out.close();
FileInputStream in = new FileInputStream("F:\\test.txt");
byte[] buffer = new byte[2]; //创建缓冲区大小为2字节
int length; //存储每次读取到的字节数
//每次读取2字节
//读取到的数据存入缓冲区,并返回读取的字节数
//如果读取的内容小于缓冲区则全部读取,读取到末尾无数据则返回 -1
while ((length = in.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, length)); //构造String使用平台默认编码表进行解码
}
in.close();
这样,在构造String时,传递的byte[]数组只截取有效部分的byte字节数。
输出:
ab
c
4. 缓冲区大小设置
设置的缓冲区大小,符合系统的缓冲区值范围。
以512字节倍数增长、1024、2048、4096、8192、...,不能太大,根据系统缓存大小定义
直接设置与之文件对应的缓冲区大小出现的问题:
FileInputStream in = new FileInputStream("F:\\test.txt");
byte[] buffer = new byte[in.available()];
in.read(buffer);
这样会觉得连遍历都不用遍历了,直接读取。
如果这个文件的大小是10m、100m、1g的,这个程序就异常崩溃了,内存分配溢出了。系统并不允许程序使用超过指定范围的内存。
5. 复制文件
只要是二进制文件都能复制,爱谁谁。音视频、图片、文本
明确数据源和目的地
FileInputStream in = new FileInputStream("F:\\test.txt"); //数据源
FileOutputStream out = new FileOutputStream("F:\\test2.txt"); //目的地
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
in.close();
out.close();
6. 缓冲流:缓冲区刷新问题
BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter、InputStreamReader和OutputStreamWriter(写出或读取时的是字符串会进行解码存入自身缓冲区),顾名思义,它们里面都维护着一个缓冲区,也就是一个数组。
读取和输出的顺序,比如从硬盘读取,在缓冲输入流 读取数据时,会先将数据读取到它本身封装的数组,此时我们一般会在外面写一个数组,再读取此输入流缓冲区的数据,
在缓冲输出流 输出数据时,会先将数据存储到它维护的数组里,等到一定字节数量时会自动一并输出,这时它本身会调用 flush() 方法。如最后存储的数据字节未满足缓冲区数量,就有可能导致数据未输出完,应该手动调用 flush() 方法。
使用缓冲流复制文件的例子
FileInputStream in = new FileInputStream("F:\\test.txt"); //数据源
FileOutputStream out = new FileOutputStream("F:\\test2.txt"); //目的地
BufferedInputStream bis = new BufferedInputStream(in); //转换流
BufferedOutputStream bos = new BufferedOutputStream(out); //转换流
//BufferedInputStream bis = new BufferedInputStream(in, 8192); //构造指定缓冲区大小
//BufferedOutputStream bos = new BufferedOutputStream(out, 8192); //构造指定缓冲区大小
byte[] buffer = new byte[1024];
int length;
while ((length = bis.read(buffer)) != -1) {
bos.write(buffer, 0, length);
}
bis.close();
bos.flush(); //刷新缓冲区写入
bos.close();
7. 编码与解码
字节流操作中文
package com.bin.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
FileOutputStream out = new FileOutputStream("F:\\test.txt"); //目的地
out.write("你A好".getBytes()); //写入中文和英文:平台默认编码
out.close();
FileInputStream in = new FileInputStream("F:\\test.txt"); //数据源
byte[] buffer = new byte[1024];
int length = in.read(buffer); //这里就直接读取了
in.close();
System.out.println(new String(buffer, 0, length)); //使用平台默认编码解码
System.out.println("平台默认编码 : " + System.getProperty("file.encoding"));
StringBuilder sb = new StringBuilder("00000000");
for (int index = 0; index < length; index++) { //获得每个字节并打印
byte b = buffer[index];
int binary = b | 0xFFFFFF00;
System.out.println(b + ":二进制 = " + Integer.toBinaryString(binary).substring(24, 32));
}
}
}
输出:
你A好
平台默认编码 : GBK
-60:二进制 = 11000100
-29:二进制 = 11100011
65:二进制 = 01000001
-70:二进制 = 10111010
-61:二进制 = 11000011
我的系统是windows简体中文,默认编码是GBK。。
GBK
中国的中文编码升级版(上一个是GB2312,一个中文字符用2个字节表示,每个字节最高位为1,以后7位为有效位)。而GBK升级版扩展了更多的中文字符。一个字符也用2个字节表示,第一个字节的最高为1,而第二个字节的最高位不一定为1,这样也就兼容GB2312,扩容了更多中文字符(所以它们的编码都为负数)。兼容ASCII。
ASCII
美国标准信息交换码,一个字符用1个字节表示,二进制最高位为0,以后7位表示一个字符,可表示127个字符,包括英文字母、阿拉伯数字、英文符号等。
Unicode
国际标准码,所有文字都用2个字节表示,Java语言使用的就是Unicode。此字符集并不兼容ASCII,本来能用1个字节表示的字符用了2个字节表示,比如英文字母或阿拉伯数字都用2个字节表示,这就导致内存的浪费,之后就出现了UTF-8。
UTF-8
最多用3个字节表示一个字符,兼容ASCII,此字符不像Unicode这样的限制,它可以以一个字节显示的字符就用一个字节,两个字节就两个,三个就三个。例如:
10xx_xxxx
110x_xxxx 10xx_xxxx
1110_xxxx 10xx_xxxx 10xx_xxxx
x 为有效表示位
指定UTF-8编码与解码
以上一个例子:指定字符编码字符并输出,指定字符编码解码读取的字符
FileOutputStream out = new FileOutputStream("F:\\test.txt"); //目的地
out.write("你A好".getBytes("UTF-8")); //写入中文和英文:指定字符编码
out.close();
FileInputStream in = new FileInputStream("F:\\test.txt"); //数据源
byte[] buffer = new byte[1024];
int length = in.read(buffer); //这里就直接读取了
in.close();
System.out.println(new String(buffer, 0, length, "UTF-8")); //使用指定编码解码
System.out.println("平台默认编码 : " + System.getProperty("file.encoding"));
for (int index = 0; index < length; index++) { //打印每个字节
byte b = buffer[index];
int binary = b | 0xFFFFFF00;
System.out.println(b + ":二进制 = " + Integer.toBinaryString(binary).substring(24, 32));
}
输出:
你A好
平台默认编码 : GBK
-28:二进制 = 11100100
-67:二进制 = 10111101
-96:二进制 = 10100000
65:二进制 = 01000001
-27:二进制 = 11100101
-91:二进制 = 10100101
-67:二进制 = 10111101
读取时解码流程
当然以UTF-8演示,读取【你】时读取到第一个字节打头1110,则后续再读取两个自己。在读取到【A】打头0,则直接去以兼容下来的ASCII编码查询,...。
字符流操作中文
字符流是字节流转换的桥梁
package com.bin.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class Main {
public static void main(String[] args) throws IOException {
OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream("F:\\test.txt"), "UTF-8"); //指定编码
out.write("你A好"); //写入中文和英文
out.flush();
out.close();
InputStreamReader in = new InputStreamReader(new FileInputStream("F:\\test.txt"), "UTF-8"); //指定解码编码
char[] buffer = new char[1024];
int length = in.read(buffer); //这里就直接读取了
in.close();
System.out.println(new String(buffer, 0, length));
}
}
输出:
你A好
可见操作包含有中文的文件,字符流就是专门针对操作这类的。
转换流与字符流便捷类的区别
// 字符流便捷类
FileWriter fw = new FileWriter("F:\\test.txt");
// 等效于如下转换流
FileOutputStream out = new FileOutputStream("F:\\test.txt");
OutputStreamWriter osw = new OutputStreamWriter(out);
区别在于便捷类只能构造文件,不能构造流,而且还不能指定字符编码,使用的是默认编码。而转换流能构造传入各种流,和指定字符编码。
8. Line: 当年难倒N多人的行问题
首先是问题:你觉得换行是啥?系统是通过什么表示换行的?
比如你在一个文本中输入字符,等你输入到有些数量时,自动地在下一行写入了,这是换行吗?
不,这不是换行。换行是等你按下回车时。
不同的系统平台表示的换行符都可能有不同的表示,通过以下方法获取系统属性换行符:
System.getProperty("line.separator")
BufferedWriter类的newLine()方法就是调用此方法。
使用BufferedReader类的String readLine()方法读取键盘
每次读取一行,windows读取到\r或\n即可认为换行,返回是字符串,没有则null。
你在使用系统控制台输入数据时(读取键盘),你使用的Scanner类而不是高效BufferedReader的区别:
Scanner sin = new Scanner(System.in);
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
读取键盘录入专业:Scanner = 流 + 正则。方法都是按照某种规则在读取数据。
而使用BufferedReader更为直接。
9. System.in 和 System.out 使用问题
System.in = 键盘录入。System.out = 输入到显示器。这两个流对象在程序启动时跟随虚拟机启动而启动。
当程序结束时会自动关闭。但是如果你在程序运行过程中关闭了这两某个流对象,在程序后面的运行中还想再次拿到这两个流,就再也拿不到了。
阻塞概念
比如输入流的read()方法
比如使用键盘录入时 System.in,在启动程序时调用此 read() 方法将会在控制台等录入的信息,也就是如果你没有录入我就在那等着,这时候进/线程就会挂起,等到有录入数据时就会重新激活进/线程。
当然玩键盘录入时要做判断结束标记,才能停止方法的阻塞,让方法出栈。
自己实现BufferedReader,BufferedWriter
缓冲的实现原理:临时存储数据的方式。就是减少程序(在内存)与设备(硬盘)之间的数据交换的频率,数据就是存储到了内存中,无非就是维护了一个数组。
在上面有一张实现的原理图,先看看:
我们先看看Java实现的缓冲流对象是怎么实现的:
MyBufferedReader.java
package com.bin.demo;
import java.io.IOException;
import java.io.Reader;
public class MyBufferedReader extends Reader {
//持有一个字符输入流对象
private Reader in;
//缓冲区
private char[] buffer;
//指针
private int pointer;
//字符计数
private int count;
public MyBufferedReader(Reader in) {
this(in, 8192);
}
public MyBufferedReader(Reader in, int size) {
this.in = in;
buffer = new char[size];
}
@Override
public int read() throws IOException { //读取下一个字符
int result = -1;
//如果当前的字符数量为0,则读取下一批数据到缓冲区,并重置指针
if (count == 0) {
count = in.read(buffer);
pointer = 0;
}
//读取设备文件数据已达末尾则为-1,则返回-1
if (count != -1) {
//获得一个字符
result = buffer[pointer];
//移动指针到下一个字符
pointer++;
//减少一个字符数
count--;
}
return result;
}
@Override
public int read(char[] cbuf, int off, int len) throws IOException { //重写的方法,读入指定数组的某一部分
int count = 0;
int c;
for (int index = off, i = 0; (c = read()) != -1 && i < len; index++, i++) {
cbuf[index] = (char) c;
count++;
}
return count == 0 && c == -1 ? -1 : count;
}
public String readLine() throws IOException { //读取一行
String result = null;
//使用StringBuffer防止String对象创建过多
StringBuffer sb = new StringBuffer();
int len;
while ((len = read()) != -1) {
char c = (char) len;
//windows的换行符为\r\n组合,如果是运行在Unix是/n。所以这个类写完是不能在unix中运行的,这时候可以获取系统的换行符来计算。
if (c == '\r') {
continue;
} else if (c == '\n') {
break;
}
sb.append(c);
}
//有数据时才进行转换,防止不返回null
if (sb.length() > 0) {
result = sb.toString();
}
return result;
}
@Override
public void close() throws IOException {
in.close(); //关闭的是持有流
}
}
主要实现方法为:read(); 和 readLine();
Reader抽象类的方法
Main.java测试
package com.bin.demo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class Main {
public static void main(String[] args) throws IOException {
OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream("F:\\test.txt"), "UTF-8");
out.write("全宇宙超级无敌最喜欢你,张雨烟,不管你现在怎么样了,我还是永远喜欢你");
out.flush();
out.close();
InputStreamReader in = new InputStreamReader(new FileInputStream("F:\\test.txt"), "UTF-8");
MyBufferedReader mbr = new MyBufferedReader(in); //自己实现的缓冲读取流
System.out.println(mbr.readLine()); //直接读取一行
mbr.close();
}
}
BufferedWriter各位自己实现,原理一样
装饰设计模式
大家也都看到我写的类是继承自Reader抽象类,就是java起初设计了IO的缓冲流为装饰设计模式。
为什么?想想看,如果继承自InputStreamReader的那些类有多个,那我就得继承这些子类实现缓冲流,这样会造成继承体系过多庞大,变得耦合和复杂。
设计模式六大原则的:合成复用原则说到,尽量使用合成/聚合的方式,而不是使用继承。
解决:可以对对象提供额外的功能(职责),比继承这种方法更为灵活。
装饰类与被装饰类都属于同一体系。
同时装饰类持有被装饰类的引用。
这样便捷类也能转换为拥有缓冲的功能
BufferedReader buf = new BufferedReader(new FileReader("F:\\test.txt"));
IO流规则总结
一、明确源、目的地
其实就是在明确使用IO体系。InputStream、OutputStream。Reader、Writer
需求操作的是源:意味着读 :InputStream、Reader。
需求操作的目的:意味着写:OutputStream、Writer
二、明确操作是否包含语言的文本
除英文、阿拉伯数字、英文字符外,如:(中文、日文、韩文。。。)
是并且是源:Reader
是并且是目的:Writer
三、明确操作的具体设备
源设备
- 硬盘:File开发的流
- 键盘:System.in
- 内存:数组
- 网络:Socker流
目的设备
- 硬盘:File开发的流
- 显示器:System.out
- 内存:数组
- 网络:Socker流
第三步明确就已经确定流对象了。
四、是否需要额外功能?
需要高效吗?缓冲流,Buffered开发。
需要编码转换吗?转换流。