大概看了一下《疯狂Java讲义》、《Java编程思想》、《Head First Java》的IO部分内容,三者在讲述I/O前,都先讲了File类的概念以及运用之后才开始讲述实现IO的流的概念等,且《Head First Java》在讲述文件输入输出前提了一页的缓冲流,让读者更易理解输入输出流。这里就大体按照《疯狂JAVA讲义》的内容过一遍,这本书讲得还是比较浅显易懂的。
java的IO通过java.io包下的类和接口来支持,在java.io包下主要包括输入、输出两种IO流,每种输入、输出流又可分为字节流和字符流两大类。其中字节流以字节为单位来处理输入、输出操作,而字符流则以字符来处理输入、输出操作。
除此之外,Java的IO流使用了一种装饰器设计模式,它将IO流分成底层节点流和上层处理流。其中节点流用于和底层的物理存储节点直接关联——不同的物理节点获取节点流的方式可能存在一点差异,但程序可以把不同的物理节点流包装成统一的处理流,从而允许程序使用统一的输入、输出代码来读取不同的物理存储节点的资源
File类可以使用文件路径字符串来创建File实例,该文件路径字符串既可以是绝对路径,也可以是相对路径。在默认情况下,系统总是依据用户的工作路径来解释相对路径,这个路径由系统属性"user.dir"指定,通常也就是运行JVM时所在的路径
一旦创建了File对象后,就可以调用File对象的方法来访问,File类提供了很多方法来操作文件和目录,下面列出一些比较常用的方法。
File类访问文件和目录的相关方法:
String getNmae(): 返回相关此File对象所表示的文件名或路径名(如果是路径,则返回最后一级子路径名)
String getPath(): 返回此File对象所对应的路径名
File getAbsoluteFile(): 返回此File对象的绝对路径
String getAbolutePath(): 返回此File对象所对应的绝对路径名
String getParent(): 返回此对象所对应目录的父目录名
boolean renameTo(File newName): 重命名此File对象所对应的文件或目录,如果重命名成功则返回true;否则返回false
boolean exists(): 判断File对象所对应的文件或目录是否存在
boolean canWrite(): 判断File对象所对应的文件和目录是否可写
boolean isFile(): 判断File对象所对应的是否是文件,而不是目录
boolean isDirectory(): 判断File对象所对应的是否是目录,而不是文件
boolean isAbsolute(): 判断File对象所对应的文件或目录是否是绝对路径。该方法消除了不同平台的差异,可以直接判断File对象是否为绝对路径。在Unix、Linux、BSD等系统上,如果路径名开头是一条斜线(/),则表明该File对象对应一个绝对路径;在Windows等系统上,如果路径开头是盘符,则说明他是一个绝对路径
long lastModified(): 返回文件的最后修改时间
long length(): 返回文件内容的长度
boolean createNewFile(): 当次File对象所对应的文件不存在时,该方法将新建一个File对象所指定的新文件,如果创建成功则返回true,否则返回false
boolean delete(): 删除File对象所对应的文件或路径
static File createTempFile(String prefix, String suffix): 在默认的临时文件目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名。这是一个静态方法,可以直接通过File类来调用。prefix参数必须是3字节长。建议前缀用一个短的、有意义的字符串如"hjb"、“mail”。suffix参数可以为null,在这种情况下系统将使用默认的后缀".tmp"
static File createTempFile(String prefix, String suffix, File directory): 在directory所指定的目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名。这是一个静态方法,可以直接通过File类来调用。
void deleteOnExit(): 注册一个删除钩子,指定当JVM退出时,删除File对象所对应的文件和目录
boolean mkdir (): 试图创建一个File对象所对应的目录,如果创建成功,则返回true,否则返回false。调用该方法时File对象必须对应一个路径而不是一个文件。
String[] list(): 列出File对象的所有子文件名和路径名,返回String数组。
File[] listFiles(): 列出File对象的所有子文件和路径,返回File数组.
static File[] listRoots(): 列出系统的所有根路径。这是一个静态方法,可以直接通过File类来调用
File类的list()方法可以接受一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。
FilenameFilter接口里只包含了一个accept(File dir, String name)方法,该方法依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或者文件
因为FilenameFilter接口内只有一个抽象方法,所以它也是一个函数式接口,可使用Lambda表达式创建实现该接口的对象。
按照流的流向区分,可以分为输入流和输出流
输入流:只能从中读取数据,而不能向其写入数据。
输出流:只能向其写入数据,而不能从中读取数据
关于输入输出的方向判别,我们要从程序运行所在内存的角度去判断。
Java的输入流主要由InputStream和Reader作为基类,而输出流则主要由OutputStream和Writer作为基类。它们都是一些抽象基类,无法直接创建实例。
字节流和字符流的用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同——字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符
字节流主要由InputStream和OutputStream作为基类,而字符流则由Reader和Writer作为基类。
按照流的角色区分,分为节点流和处理流
节点流:从/向一个特定的IO设备(如磁盘/网络)读/写数据的流,节点流也被称为低级流。
处理流:用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。处理流也被称为高级流
区别:使用节点流进行输入输出时,程序直接连接到实际的数据源,和实际的输入输出节点连接;而当使用处理流进行输入输出时,程序并不会直接连接到实际的数据源,没有和实际的输入输出节点连接。
这样使用处理流的一个明显好处是,只要使用相同的处理流,程序就可以采用完全相同的输入输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地发生变化
实际上,Java使用处理流来包装节点流是一种典型的装饰器设计模式,通过使用处理流来包装不同的节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法完成输入输出功能,因此处理流也被称为包装流。
在InputStream里包含如下三个方法:
int read(): 从输入流中读取单个字节,返回所读取的字节数据(字节数据可以直接转换为类型int)
int read(byte[] b): 从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数
int read(byte[] b, int off, int len):从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入数组b中时并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数
在Reader里包含同上三个方法,不过是数据类型换成char
下面使用FileInputStram来读取自身的效果
import java.io.FileInputStream;
import java.io.IOException;
public class IOP {
public static void main(String[] args) throws IOException {
//创建字节输入流
FileInputStream fis = new FileInputStream("FileInputStream.java");
//创建一个长度为1024的byte数组
byte[] bbuf = new byte[1024];
//用于保存实际读取的字节数
int hasRead = 0;
//使用循环来重复读取过程
while((hasRead = fis.read(bbuf)) > 0) System.out.println(new String(bbuf, 0, hasRead));
fis.close();
}
}
最后使用了close()方法来关闭该文件输入流,与JDBC编程一样,程序里打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该是显式关闭文件IO资源。JAVA 7 改写了所以的IO资源类,它们都实现了AutoCloseable接口,因此都可通过自动关闭资源的try语句来关闭这些IO流。
下面使用FileReader来读取自身的效果,因为下面使用了自动关闭资源的try语句,则无需使用close()方法
import java.io.FileInputStream;
import java.io.IOException;
public class IOP {
public static void main(String[] args) throws IOException {
//创建字符输入流
try(FileReader fr = new FileReader("C:\\Users\\Leant\\Desktop\\Test.txt")){
//创建一个长度为32的字符数组
char[] cbuf = new char[32];
//用于保存实际读取的字符数
int hasRead = 0;
//使用循环来重复读取
while ((hasRead = fr.read(cbuf)) > 0) System.out.println(new String(cbuf, 0, hasRead));
}catch (IOException e){
e.printStackTrace();
}
}
}
与之前的字节流输入并不太大不同;使用try()语句可以保证输入流一定会被关闭
除了以上的方法之外,InputStream和Reader还支持以下几个方法来移动记录指针
void mark(int readAheadLimit): 在记录指针当前位置记录一个标记(mark)
boolean markSupported(): 判断此输入流是否支持mark()操作,即是否支持记录标记
void reset(): 将此流的记录指针重新定位到上一次记录标记(mark)的位置
long skip(long n): 记录指针向前移动n个字节/字符
void write(int c)
void write(byte[]/char[] buf)
void write(byte[]/char[] buf, int off, int len)
下面用FileInputStream来执行输入,并使用FileOutputStream来执行输出,用以实现复制Test.txt文件的功能
public static void main(String[] args) throws IOException {
try(//创建字节输入流
FileInputStream fis = new FileInputStream("C:\\Users\\Leant\\Desktop\\Test.txt");
//创建字节输出流
FileOutputStream fos = new FileOutputStream("C:\\Users\\Leant\\Desktop\\Test2.txt")
){
byte[] buf = new byte[1024];
int hasRead = 0;
while((hasRead = fis.read(buf)) > 0){
fos.write(buf, 0, hasRead);
}
}catch (IOException e){
e.printStackTrace();
}
}
输入缓冲流 | 输出缓冲流 |
---|---|
BufferedInputStream | BufferedOutputStream |
BufferedReader | BufferedWriter |
缓冲流都有两个构造器,其中一个构造器只有一个参数,为输入/输出流,另一个构造器有两个参数,一个为输入/输出流,另一个参数指定size大小
其对应方法主要有
void flush(): 缓冲区未满也可以使用该方法强制缓冲区立即写入
void close(): 将数据从缓冲区内写出并且关闭响应的流
输入缓冲流常用: String readLine() : 读取一行的内容(即读取数据直至遇到\r\n),返回的内容是换行符前的数据,不包括换行符
输出缓冲流常用:void nextLine(): 调用本系统的换行符(Windows下为\r\n)
处理流可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入输出方法,让程序员只需关系高级流的操作
使用处理流的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入输出功能,让节点流与底层的IO设备、文件交互
如何识别处理流?
看该流的构造器参数,其参数为已存在的流而不是一个物理节点,那么这种流一定是处理流,而所有节点流都是直接以物理IO节点作为构造器参数的
使用处理流的两点优势:①对于开发人员来说,使用处理流进行输入/输出操作更简单;②使用处理流的执行效率更高
public static void main(String[] args) {
try(
FileOutputStream fos = new FileOutputStream("C:/Users/Leant/Desktop/Test.txt");
PrintStream ps = new PrintStream(fos)
){
//使用PrintStream执行输出
ps.println("普通字符串");
//直接使用PrintStream输出对象
ps.println(new IOP());
}catch (IOException e){
e.printStackTrace();
}
}
在使用处理流包装了底层及节点流之后,关闭输入输出流资源时,只要关闭最上层的处理流即可。关闭最上层的处理流时,系统会自动关闭被该处理流包装的节点流。
Another example:
使用字符串作为物理节点的字符输入输出流
public static void main(String[] args) {
String src = "Test1\nTest2\nTest3\nTest4\nTest5\n";
char[] buffer = new char[32];
int hasRead = 0;
try(StringReader sr = new StringReader(src)){
while ((hasRead = sr.read(buffer)) > 0) System.out.println(new String(buffer, 0, hasRead));
}catch (IOException e){
e.printStackTrace();
}
try( //创建StringWriter时实际上以一个StringBuffer作为输出节点
StringWriter sw = new StringWriter(20)
){
sw.write("Test5\nTest4\nTest3\nTest2\nTest1");
System.out.println(sw);
System.out.println(sw.toString());
}catch (IOException e){
e.printStackTrace();
}
输入输出流体系中还提供了两个转换流,这两个转换流用于实现将字节流转换为字符流,其中InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输入流转换成字符输出流。
下面以获取键盘输入为例来介绍转换流的用法
public static void main(String[] args) {
try(
InputStreamReader reader = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(reader)
){
String line = null;
while((line = br.readLine()) != null){
if(line.equals("exit")) System.exit(1);
System.out.println("键盘输入的内容为"+line);
}
}catch (IOException e){
e.printStackTrace();
}
}
由于BufferedReader具有一个readLine()方法,可以非常方便地一次读入一行内容,所以经常把读取文本内容的输入流包装成BufferedReader,用来方便地读取输入流的文本内容。
值得注意的是,BufferedReader流具有缓冲功能,一次读取一行文本,以换行符为标志,若它没有读到换行符则程序阻塞。
void unread(int b)
void unread(byte[]/char[] b)
void unread(byte[]/char[] b, int off, int len)
public static void main(String[] args) {
try(
PushbackReader pr = new PushbackReader(new FileReader("C:/Users/Leant/Desktop/Test2.txt"),64)
){
char[] buf = new char[32];
//用以保存上次读取的字符串内容
String lastContent = "";
int hasRead = 0;
while ((hasRead = pr.read(buf)) > 0){
String content = new String(buf, 0, hasRead);
int targetIndex = 0;
//将上次读取的字符串和本次读取的字符串拼接
//查看是否包含目标字符串,如果包含目标字符串
if ((targetIndex = (lastContent + content).indexOf("ful")) > 0){
//将本次内容和上次内容一起退回缓冲区
pr.unread((lastContent+content).toCharArray());
//重新定义一个长度为targetIndex的char数组
if(targetIndex > 32) buf = new char[targetIndex];
//在此读取指定长度的内容
pr.read(buf, 0, targetIndex);
//打印读取的内容
System.out.println(new String(buf, 0, targetIndex));
System.exit(0);
}else {
System.out.println(lastContent);
lastContent = content;
}
}
}catch (IOException e){
e.printStackTrace();
}
}
Java的标准输入/输出分别通过System.in和System.out来代表,在默认情况下它们分别代表键盘和显示器,当程序通过System.in来获取输入时,实际上是从键盘读取输入;当程序试图通过System.out执行输出时,程序总是输出到屏幕
在System类里提供了如下三个重定向标准输入/输出的方法
static void setErr(PrintStream err): 重定向“标准”错误输出流
static void setIn(InputStream in): 重定向“标准”输入流
static void setOut(PrinterStream out): 重定向“标准”输出流
public static void main(String[] args) {
try(PrintStream ps = new PrintStream(new FileOutputStream("C:/Users/Leant/Desktop/Test.txt"))){
System.setOut(ps);
System.out.println("我想做个大佬呀\r\n啥时候我能做个大佬呀\r\n我想去BAT做保洁");
}catch (IOException e){
e.printStackTrace();
}
}
创建了一个PrintStream输出流,并将系统的标准输出重定向到该PrintStream输出流。这意味着标准输出不再输出到屏幕而是输出到Test.txt文件
再来个重定向输出的例子
public static void main(String[] args) {
try(FileInputStream fis = new FileInputStream("C:/Users/Leant/Desktop/Test.txt")){
System.setIn(fis);
Scanner sc = new Scanner(System.in);
sc.useDelimiter("\n");
while(sc.hasNext()){
System.out.println("重定向后输入的东西是:"+sc.next());
}
}catch (IOException e){
e.printStackTrace();
}
}
希望能对还在学习的朋友起到一点帮助,学习IO实在有点累,Java的I/O系统提供的类好多…