File类
File类是与平台无关的文件和目录的抽象,如果我们想要在Java程序中操作文件和目录则必须使用java.io.File类完成,比如查看文件大小、删除文件、创建目录、文件重命名等,而对于文件内容的处理必须依赖于IO流才能完成。
常用方法
//系统相关的默认名称分隔符,为方便起见,以字符串形式表示。
public static final String separator;
//创建路径为pathname的文件操作类对象
File(String pathname){}
//返回File对象表示的最后一级文件名或路径名
public String getName(){}
//返回File对象的上级目录的完整路径,返回值为路径字符串
public String getParent(){}
//返回File对象的上级File对象
public File getParentFile(){}
//返回File对象表示的路径,也就是通过构造方法设置的路径
public String getPath() {}
//是否为绝对路径
public boolean isAbsolute() {}
//返回File对象表示的绝对路径,返回值为路径字符串
public String getAbsolutePath() {}
//返回File对象表示的绝对路径,发返回值
public File getAbsoluteFile() {}
//判断是不是文件夹
public boolean isDirectory() {}
//判断是不是文件
public boolean isFile() {}
//获取最后一次修改时间
public long lastModified() {}
//获取文件内容的长度
public long length() {}
//删除File对象对应的文件或者文件夹,如果文件夹下有子文件夹或者文件,则不能删除,必须先删除子文件或者文件夹
public boolean delete() {}
//注册一个删除钩子,当JVM退出时,删除File对象对应的文件或者文件夹,主要用于删除临时文件
public void deleteOnExit() {}
//返回File对象所表示的文件夹下所有文件夹和文件名称
public String[] list() {}
//根据FilenameFilter对文件进行过滤
public String[] list(FilenameFilter filter) {}
//返回File对象所表示的文件夹下所有文件夹和文件名称的File对象
public File[] listFiles() {}
//创建File对象所对应得文件夹,如果File对象的父级路径不存在,则创建失败,返回false
public boolean mkdir() {}
//找不到对应路径的文件夹时,会自动创建路径中不存在的文件夹
public boolean mkdirs() {}
//将File对象表示的文件夹或文件重命名为参数File对象所表示的文件夹或文件
public boolean renameTo(File dest) {}
//设置文件的最后一次修改时间
public boolean setLastModified(long time) {}
//创建一个临时文件,prefix:前缀;suffix:后缀;directory:临时文件生成的目标文件夹
public static File createTempFile(String prefix, String suffix, File directory) throws IOException {}
//判断File对象表示的路径是否存在
public boolean exists() {}
栗子
package com.icypt.file;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
*运行结果
*1-执行结果:
*文件名称:files,文件大小:0M,文件最后修改时间:2019-11-04 17:27:37.037
*2-执行结果:临时文件名称,test4427553894235116532tmp
*/
public class Test1 {
public static void main(String[] args) throws Exception{
//构造文件路径
String path = "D:" + File.separator + "files" + File.separator + "test";
//创建文件对象
File file = new File(path);
//判断文件的目录是否存在
if(!file.exists()){
//不存在
file.mkdirs();//如果不存在则创建
}
//实例化日期格式化类
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.sss");
File parentFile = file.getParentFile().getParentFile();
//对D盘下文件进行过滤,并打印文件相关信息
File[] fileArray = parentFile.listFiles((dir, name) -> name.equals("files"));
System.out.println("1-执行结果:");
for(File f : fileArray) {
System.out.println("文件名称:" + f.getName() + ",文件大小:" + f.length()/1024*1024 + "M" +
",文件最后修改时间:" + sdf.format(new Date(f.lastModified())));
}
//创建一个临时文件,并在JVM退出时删除
File tmpFile = File.createTempFile("test", "tmp", file);
System.out.println("2-执行结果:临时文件名称," + tmpFile.getName());
//注册一个删除钩子,当JVM退出时,删除File对象对应的文件或者文件夹,主要用于删除临时文件
tmpFile.deleteOnExit();
}
}
上栗中演示了如何使用File类创建文件、获得文件属性、以及删除文件等操作,其方法的使用都比较简单。需要注意的是我们可以使用File类的List方法通过lambda表达式非常简洁的完成对文件的过滤,在上述的lambda表达式中dir参数表示数组中的每一个文件或者文件夹而name参数是文件的名称。
IO流
在Java中,流是从数据源到目的地的字节的有序序列。流中的字节依据先进先出,具有严格顺序,因此流式IO是一种顺序的存取方式。
流的分类
根据不同的分类方式,可以将流分为不同的类型,按照流的流向可以将流分为输入流和输出流;按照流读取和写入的内容可以分为字节流和字符流。按照流所扮演的角色可以分为节点流和处理流。对于流的输入、输出是一个相对的概念,它的定义依赖程序运行所在的内存,也就是说从数据源到目的地内存称之为输入,而从内存到目的地磁盘、网络等IO设备称之为输出。而对于字节、字符流的划分是程序在操作流的层面上进行划分的,其底层还是以字节来操作的。还有就是节点流和处理流,所谓节点流其实是它可以从或者向一个IO设备读或者写数据的流;处理流则是将节点流进行封装,通过封装之后的流进行输入、输出操作。
Java为流的操作提供了一个庞大的类库,涉及到的处理类不下40种,看起来很复杂,但是细品之下会发现,Java中的流分为两大体系:字节流和字符流,字节流以InputStream和OutputStream为抽象基类进行读写的扩展,而字符流则以Reader和Writer为抽象基类进行读写的扩展。以上四个抽象类均不能实例化对象,必须依赖于各自的子类才能实例化。那么对于字符流、字节流的使用场景又该如何划分呢?其实很简单,所谓字节流必然是处理数据源为字节的流,例如图片、视频等文件的读写。所谓字符流必然是处理数据源为字符的流,例如文件内容、字符串等的读写。之前也提到过,关于字节或者字符流的操作是程序在处理层面上定义的,其底层必然是以字节为单位进行处理的。
四个基类
InputStream:作为字节输入流的基类,提供有以下三个常用方法完成字节流的读取。
//从字节流中读取单个字节,返回读取的字节数据
public abstract int read() throws IOException;
//从字节流中读取多个字节,返回读取的字节个数
public int read(byte b[]) throws IOException {}
//从字节流中读取指定范围的多个字节,返回读取字节个数
public int read(byte b[], int off, int len) throws IOException {}
Reader:作为字符输入流的基类,提供有以下三个常用方法完成字符流的读取。
//从字符流中读取单个字符,返回读取的字符数据
public int read() throws IOException {}
//从字符流中读取多个字符,返回读取的字符个数
public int read(char cbuf[]) throws IOException {}
//从字符流中读取指定范围的多个字符,返回读取字符个数
abstract public int read(char cbuf[], int off, int len) throws IOException;
以上字符、字节输入流方法一旦返回-1则表示已经读到流的尽头,也就是整个流已经读取完成。
OutputStream:作为字节输出流的基类,提供有以下三个常用方法完成字节流的输出。
//输出单个字节数据
public abstract void write(int b) throws IOException;
//输出多个字节数据
public void write(byte b[]) throws IOException {};
//输出指定范围的多个字节数据
public void write(byte b[], int off, int len) throws IOException {}
Writer:作为字符输出流的基类,提供有以下四个常用方法完成字符流的输出。
//输出单个字符数据
public void write(int c) throws IOException {}
//输出多个字符数据
public void write(char cbuf[]) throws IOException {}
//输出指定范围的多个字符数据
abstract public void write(char cbuf[], int off, int len) throws IOException;
//输出一个字符串数据,在Java中多个字符可以视为是一个字符串
public void write(String str) throws IOException {}
通过对上述四个基类的方法列举,不难发现他们的输入、输出方法竟是如此的相似,唯一不同的就是流数据类型的不同。下面我们通过四个基类的子类在不同数据节点上的使用来进入IO学习历程。
文件流
文件流操作的数据节点是文件,而文件又可以分为文本文件和二进制文件,那么通常情况下文本文件使用字符流处理,而二进制文件则使用字节流处理。
栗子,文件拷贝
package com.icypt.filestream;
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception {
File[] files = pathValidate("D:\\test.txt", "D:\\bak\\test_bak.txt");
//文本文件拷贝
textFileCopy(files[0], files[1]);
files = pathValidate("D:\\图片1.png", "D:\\bak\\图片1_bak.png");
//二进制文件拷贝
byteFileCopy(files[0], files[1]);
}
/**
* 文本文件的复制
* @param inputFile 读入文件对象
* @param outputFile 输出文件对象
*/
public static void textFileCopy(File inputFile, File outputFile) {
Reader reader = null;
Writer writer = null;
try {
//实例化字符输入流对象
reader = new FileReader(inputFile);
//实例化字符输出流对象
writer = new FileWriter(outputFile);
int data;
//循环读取文件内容
while((data = reader.read()) != -1) {
writer.write(data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭流
close(reader);
close(writer);
}
}
/**
* 字节文件的复制
* @param inputFile 读入文件对象
* @param outputFile 输出文件对象
*/
public static void byteFileCopy(File inputFile, File outputFile) {
InputStream is = null;
OutputStream os = null;
try {
//实例化字节输入流对象
is = new FileInputStream(inputFile);
//实例化字节输出流对象
os = new FileOutputStream(outputFile);
int data;
//循环读取文件内容
while((data = is.read()) != -1) {
os.write(data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
close(is);
close(os);
}
}
/**
* 校验路径数据,返回输入输出File对象
* @param input
* @param output
* @return
*/
public static File[] pathValidate(String input, String output) {
File[] fileArray = new File[2];
File inputFile = new File(input);
//空校验
if(input == null || "".equals(input) || output == null || "".equals(output)) {
throw new RuntimeException("输入或输出文件路径不能为空,input:" + input + ",output:" + output);
}
if(!inputFile.exists()) {
throw new RuntimeException("文件不存在:" + input);
}
fileArray[0] = inputFile;
File outputFile = new File(output);
//输出文件的父目录不存在
if(!outputFile.getParentFile().exists()) {
//创建父目录
outputFile.getParentFile().mkdirs();
}
fileArray[1] = outputFile;
return fileArray;
}
//关闭流
public static <T extends Closeable> void close(T t) {
try {
if(t != null ) {
t.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
本栗的处理流程为:输入、输出路径校验->输入、输出文件对象构建->输入、输出流对象构建->输入、输出流业务处理->输入、输出流关闭,期间结合数组、泛型方法等知识对部分流程做了封装,使得代码更加简洁、易读。上述我采用的是边读编写的方式操作文件流,这种方式比较适合文件内容比较大的情况。因为文件内容过大如果先全部加载到内存再 输出的话,对内存的消耗太大,降低程序的运行性能。
内存操作流
内存操作流主要是对数据进行缓存,而后从内存中读取数据,所以使用内存操作时,数据量不宜过大。内存字节操作流由ByteArrayInputStream,ByteArrayOutputStream完成输入输出操作;内存字符操作流由CharArrayReader与CharArrayWriter完成输入输出操作。
栗子,字母字符串小写转大写,以字符流为例
package com.icypt.arraystream;
import java.io.CharArrayReader;
import java.io.CharArrayWriter;
import java.io.Closeable;
import java.io.IOException;
/**
* 运行结果
* 执行结果:HELLO WORLD
*/
public class Test {
public static void main(String[] args) {
String str = "Hello World";
CharArrayWriter caw = null;
CharArrayReader car = null;
try {
//创建内存字符流输出对象
caw = new CharArrayWriter();
//将字符串写入内存
caw.write(str);
//创建内存字符流输入对象
car = new CharArrayReader(caw.toCharArray());
//定义读取数据接收数组
char[] dataArray = new char[16];
//数组下标
int index = 0;
int data;
//读取内存中的字符串,并进行小写转大写
while((data = car.read()) != -1) {
//转大写
data = Character.toUpperCase(data);
//存入数组
dataArray[index] = (char)data;
//下标+1
index++;
}
//打印输出
System.out.println("执行结果:" + new String(dataArray).trim());
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭流
close(caw);
}
}
//关闭流
public static <T extends Closeable> void close(T t) {
if(t != null) {
try {
t.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
管道流
Java中的管道流主要用于线程之间流的传递,管道字节流由PipedInputStream和PipedOutputStream完成输入输出;管道字符流由PipedReader和PipedWriter完成的输入输出。
栗子,管道流实现线程间通信,以字节流为例
package com.icypt.pipedstream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
/**
* 运行结果
* 接收到的内容:Hello icypt!!!
*/
public class Test {
public static void main(String[] args) {
//实例化发送对象
Sender sender = new Sender();
//实例化接收对象
Receiver receiver = new Receiver();
//建立连接
try {
sender.getPosInstance().connect(receiver.getPisInstance());
//启动线程
new Thread(sender).start();
new Thread(receiver).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//定义发送线程类
class Sender implements Runnable{
//定义管道流输出对象
PipedOutputStream pos = null;
//实例化发送对象时实例管道流输出对象
public Sender() {
pos = new PipedOutputStream();
}
//获取管道输出流实例
public PipedOutputStream getPosInstance() {
return pos;
}
@Override
public void run() {
//在执行时执行管道流输出
try {
pos.write("Hello icypt!!!".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//定义线程接收类
class Receiver implements Runnable {
//定义管道输入流
PipedInputStream pis = null;
//实例化接收对象时实例管道流输入对象
public Receiver() {
pis = new PipedInputStream();
}
//获取管道输入流实例
public PipedInputStream getPisInstance() {
return pis;
}
@Override
public void run() {
//在执行时执行管道流输入
try {
byte[] dataArray = new byte[32];
pis.read(dataArray);
System.out.println("接收到的内容:" + new String(dataArray).trim());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
本栗中需要注意的是异常的关闭必须放在线程类中处理异常的地方而不要外移,否则将会出现流提前关闭导致的管道流输入输出异常的问题。
对象流
对象流的主要作用是将对象数据持久化到IO设备,或者从IO设备中加载对象数据,此过程也称之为对象的序列化和反序列化。序列化的主要作用是便于对象数据在网络中传输,比如典型的RPC调用。要想实现对象的序列化则必须实现Serializable或者Externalizable接口。Externalizable于Serializable接口的区别在于Externalizable在序列化的过程中可以自定义序列化逻辑,例如:敏感信息加解密。在序列化时还可以使用transient关键字来指定哪些对象成员变量不被序列化,序列化操作的是对象数据,而不是整个类。
栗子,对象的序列化及反序列化
package com.icypt.objectstream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* 运行结果
* Student [name=张晓明, desc=is a good boy!!, age=15, school=School [schName=二十五中]]
*/
public class Test {
//定义对象数据存放文件路径
public static final String FILE_PATH = "D:\\obj.txt";
public static void main(String[] args) {
//执行序列化
serializableObj();
//执行反序列化
reverseSerializableObj();
}
//序列化
public static void serializableObj() {
//对象数据输出文件
File file = new File(FILE_PATH);
ObjectOutputStream oss = null;
try {
//创建对象输出流操作对象
oss = new ObjectOutputStream(new FileOutputStream(file));
//创建对象
Student student = new Student();
student.setName("张晓明");
student.setAge(15);
student.setDesc("is a good boy!!");
School school = new School();
school.setSchName("二十五中");
student.setSchool(school);
//将对象数据写入文件
oss.writeObject(student);
} catch(IOException e) {
e.printStackTrace();
} finally {
close(oss);
}
}
//反序列化
public static void reverseSerializableObj() {
//加载对象数据文件
File file = new File(FILE_PATH);
ObjectInputStream ois = null;
try {
//创建对象输入流操作对象
ois = new ObjectInputStream(new FileInputStream(file));
//读取对象
Object obj = ois.readObject();
//向下转型,强转
if(obj instanceof Student) {
Student student = (Student)obj;
//打印读取到的对象信息
System.out.println(student.toString());
}
} catch(IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//关闭流
public static <T extends Closeable> void close(T t) {
if(t != null) {
try {
t.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//定义学生类
class Student implements Serializable{
private String name;
private String desc;
private Integer age;
private School school;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public School getSchool() {
return school;
}
public void setSchool(School school) {
this.school = school;
}
@Override
public String toString() {
return "Student [name=" + name + ", desc=" + desc + ", age=" + age + ", school=" + school + "]";
}
}
//定义School类
class School implements Serializable{
private String schName;
public String getSchName() {
return schName;
}
public void setSchName(String schName) {
this.schName = schName;
}
@Override
public String toString() {
return "School [schName=" + schName + "]";
}
}
本栗演示了实现Serializable接口的对象序列化与反序列化,发现对象的序列化会将当前对象依赖的所有可序列化的类都会进行序列化。关于transient的使用本栗并没有体现,它的使用方式就是在你认为不需要序列化的的对象成员变量前进行修饰,程序h在序列化时自动放弃对此变量的序列化,各位学友可以在本栗基础上进行测试。
转换流
转换流顾名思义就是将一种类型的流转换为另一种类型的流,而这种转换只支持字节流向字符流的转换,因为它的存在就是方便将文本形式的字节流以字符流的形式处理。而通常的处理方式是将文本类型的字节流转换为转化流对象后,通过包装流进行输入输出。
栗子
package com.icypt.exchangestream;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class Test {
public static void main(String[] args) {
//定义转换流输入对象
InputStreamReader isr = null;
try {
//创建转换流输入对象
isr = new InputStreamReader(new FileInputStream(new File("D:\\test.txt")));
//包装转换流
@SuppressWarnings("resource")
BufferedReader br = new BufferedReader(isr);
String data;
//按行读取文件数据
while((data = br.readLine()) != null) {
//打印读取的内容
System.out.println(data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
close(isr);
}
}
//关闭流
public static <T extends Closeable> void close(T t) {
if(t != null) {
try {
t.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
通过上栗发现转换流和包装流结合使用使得字符流的处理便的更加简洁,需要注意的是BufferedReader类的readline方法,可以按行读取文本数据。
打印流
PrintStream 为输出流添加了功能,使它们能够方便地打印各种数据值表示形式。
栗子
package com.icypt.printstream;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
public class Test {
public static void main(String[] args) {
//定义打印流对象
PrintStream printStream = null;
try {
//创建打印流对象
printStream = new PrintStream(new File("D:\\test.txt"));
//通过打印流向文件中写入数据
printStream.println("长风破浪会有时,");
printStream.println("直挂云帆济沧海。");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
close(printStream);
}
}
//关闭流
public static <T extends Closeable> void close(T t) {
if(t != null) {
try {
t.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
打印流的存在主要就是方便我们将各种形式的数据进行输出操作,所以在以后的开发中,如果是字符流的输出操作优先使用打印流处理。
Scanner
Java.util.Scanner 是 Java5 的新特征,我们可以通过 Scanner 类来更加方便的进行读取操作,尤其是对字符流的读取处理。它提供了一系列的hasXXXNext()方法和nextXXX()方法,用来判断是否存在未读完的数据以及获取读到的数据,还有一个方法也比较重要useDelimiter(),它可以使用正则表达式来定义每次读取数据时是以什么参考为结尾的,默认是以输入内容之后出现空白为参考结束本次读取的。
栗子
package com.icypt.scanner;
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
//穿件Scanner对象,用来获取标准输入,即键盘输入
Scanner scanner = new Scanner(System.in);
//使用两个$符作为结束符
scanner.useDelimiter("\\$\\$");
//判断是存在未读内容
while(scanner.hasNext()) {
//获取并打印读取内容
System.out.println(scanner.next());
}
}
}
Scanner类作为标准输入流以及字符流的读取应该算是一个不错的选择。
至此关于传统流的使用就讨论到这里,下篇我们来学习NIO以及NIO2的使用。
java培训班:http://www.baizhiedu.com/java2019
注释:本文来自公众号冰点IT