JAVA中的IO操作主要依赖java.io包来实现,该包主要包括五个类和一个接口:
- 五个类:File、InputStream、OutputStream、Reader、Wirter
- 一个接口:Serializable
File
File类的主要方法有:
File(File parent, String child) // Creates a new File instance from a parent abstract pathname and a child pathname string.
File(String pathname) // Creates a new File instance by converting the given pathname string into an abstract pathname.
boolean createNewFile() // Atomically creates a new, empty file named by this abstract pathname if and only if a file with this name does not yet exist.
boolean exists() // Tests whether the file or directory denoted by this abstract pathname exists.
boolean delete() // Deletes the file or directory denoted by this abstract pathname.
File getParentFile() // Returns the abstract pathname of this abstract pathname's parent, or null if this pathname does not name a parent directory.
boolean mkdirs() // Creates the directory named by this abstract pathname, including any necessary but nonexistent parent directories.
long length() // Returns the length of the file denoted by this abstract pathname.
boolean isFile() // Tests whether the file denoted by this abstract pathname is a normal file.
boolean isDirectory() // Tests whether the file denoted by this abstract pathname is a directory.
long lastModified() // Returns the time that the file denoted by this abstract pathname was last modified.
String[] list() // Returns an array of strings naming the files and directories in the directory denoted by this abstract pathname.
File[] listFiles() // Returns an array of abstract pathnames denoting the files in the directory denoted by this abstract pathname.
上面的方法都是对文件本身的判断,而不涉及对文件的修改。
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("memo.txt");
if(f.exists()) {
f.delete();
} else {
System.out.println(f.createNewFile());
}
}
}
上边的代码构建了File类对象,并使用exists方法判断该文件是否存在,存在删除,不存在新建。
如果是多级目录,则需要构建目录:
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("H:" + File.separator + "memo.txt");
if(!f.getParentFile().exists()) {
f.getParentFile().mkdirs();
}
System.out.println(f.createNewFile());
}
}
再看一下如何获取文件属性:
import java.io.*;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("memo.txt");
if (f.exists()) {
System.out.println("是否是文件:" + f.isFile());
System.out.println("是否是目录:" + f.isDirectory());
System.out.println("文件大小:" + (new BigDecimal((double)f.length() / 1024 /1024).divide(new BigDecimal(1),2,BigDecimal.ROUND_HALF_UP)) + "M");
System.out.println("文件修改时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(f.lastModified())));
}
}
}
执行结果为:
是否是文件:true
是否是目录:false
文件大小:0.00M
文件修改时间:2022-06-04 14:37:39
再看一下liunx中ls命令的原理:
import java.io.*;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("H:" + File.separator);
if(f.isDirectory()) {
File res[] = f.listFiles();
for (int i = 0;i < res.length; ++i) {
System.out.println(res[i]);
}
}
}
}
上面代码会列出所示目录下的所有文件。
这里有必要看一下File.separator,在linux系统中,路径分隔符为/,Windows系统中,路径分隔符为\,而为了适配不同的系统,因此才用File.separator作为分隔符。
同样再看一下linux中的tree命令的原理:
import java.io.*;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("H:" + File.separator);
func(f);
}
public static void func(File f) {
if(f.isDirectory()) {
File res[] = f.listFiles();
if(res != null) {
for (int i = 0;i < res.length; ++i) {
func(res[i]);
}
}
System.out.println(f);
}
}
}
上面的代码中,利用递归依此打印整个目录下的文件结构。
字节流和字符流
通常情况下,File类可以用来判断文件或获得文件属性,而文件内容的处理则可以借助字节流和字符流。
- 字节流:InputStream、OutputStream
- 字符流:Reader、Writer
这里提到的字节流和字符流就是之前提到五个类中剩下的部分,不过不管是字节流还是字符流,其处理文件逻辑都是一样的:
- 通过File构建File实例化对象
- 通过字节流或字符流的子类对象为父类对象实例化
- 进行文件IO操作
- 关闭文件
上面提到的字节流和字符流类都是抽象类,所以在使用时,要通过子类对象进行向上转型进行抽象类的实例化,也因此实际开发中会存在不同的子类。
OutputStream
其定义为:
public abstract class OutputStream extends Object implements Closeable, Flushable
上面的接口定义为:
public interface Closeable extends AutoCloseable
public interface AutoCloseable
public interface Flushable
常用方法有:
void close() // Closes this output stream and releases any system resources associated with this stream.
void flush() // Flushes this output stream and forces any buffered output bytes to be written out.
void write(byte[] b) // Writes b.length bytes from the specified byte array to this output stream.
void write(byte[] b, int off, int len) // Writes len bytes from the specified byte array starting at offset off to this output stream.
abstract void write(int b) // Writes the specified byte to this output stream.
AutoCloseable接口会在操作完成后自动调用close方法,以释放资源。
而OutputStrength是抽象类,因此如果要进行文件操作,就需要子类FileOutputStream来进行处理,其主要方法有:
FileOutputStream(File file) // Creates a file output stream to write to the file represented by the specified File object.
FileOutputStream(File file, boolean append) // Creates a file output stream to write to the file represented by the specified File object.
从构造方法看,是用File类对象作为输出的对象:
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("memo.txt");
OutputStream output = new FileOutputStream(f);
String str = "Hello world";
byte date[] = str.getBytes();
output.write(date);
output.close();
}
}
上面的代码中用File类对象构建OutputStream对象,然后将Hello world转换为byte类型,写入该File对象。
而如果修改使用下面的代码进行构造,则会使用追加方法进行输出。
OutputStream output = new FileOutputStream(f,true);
InputStream
其定义为:
public abstract class InputStream extends Object implements Closeable
常用方法有:
void close() // Closes this input stream and releases any system resources associated with the stream.
abstract int read() // Reads the next byte of data from the input stream.
int read(byte[] b) // Reads some number of bytes from the input stream and stores them into the buffer array b.
int read(byte[] b, int off, int len) // Reads up to len bytes of data from the input stream into an array of bytes.
上边的read方法会在读取到文件结尾时,会返回-1。
而OutputStrength是抽象类,因此如果要进行文件操作,就需要子类FileInputStream来进行处理,其主要方法有:
FileInputStream(File file) // Creates a FileInputStream by opening a connection to an actual file, the file named by the File object file in the file system.
从构造方法看,是用File类对象作为输入的对象。
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("memo.txt");
InputStream input = new FileInputStream(f);
byte data[] = new byte[100];
int len = input.read(data);
System.out.println(new String(data,0,len));
input.close();
}
}
上面的代码中用File类对象构建InputStream对象,然后将File对象内容输入到InputStream对象中。
而如果修改使用下面的代码则会逐个字节进行读取:
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("memo.txt");
InputStream input = new FileInputStream(f);
byte data[] = new byte[100];
int cur = 0;
int tmp = 0;
while((tmp = input.read()) != -1) {
data[cur++] = (byte)tmp;
}
System.out.println(new String(data,0,cur));
input.close();
}
}
这种写法和C/C++中的写法类似:
- tmp = input.read():读取内容到tmp变量
- (tmp = input.read()) != -1:判断当前读到的是否是文件末尾
Writer
其定义为:
public abstract class Writer extends Object implements Appendable, Closeable, Flushable
常用方法有:
abstract void close() // Closes the stream, flushing it first.
abstract void flush() // Flushes the stream.
Writer append(CharSequence csq) // Appends the specified character sequence to this writer.
void write(String str) // Writes a string.
void write(char[] cbuf) // Writes an array of characters.
通过Writer类定义可以看出,Writer类中直接提供了输出字符串数据方法,这样就可以直接输出字符串。
而Writer是抽象类,因此如果要进行文件操作,就需要子类FileWriter来进行处理,其主要方法有:
FileWriter(File file) // Constructs a FileWriter given the File to write, using the default charset
FileWriter(File file, boolean append) // Constructs a FileWriter given the File to write and a boolean indicating whether to append the data written, using the default charset.
从构造方法看,是用File类对象作为输出的对象:
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("memo.txt");
Writer output = new FileWriter(f);
String str = "Hello world";
output.write(str);
output.close();
}
}
上面的代码中用File类对象构建FileWriter对象,然后将字符串直接写入该File对象。
而如果修改使用下面的代码进行构造,则会使用追加方法进行输出。
Writer output = new FileWriter (f,true);
Reader
其定义为:
public abstract class Reader extends Object implements Readable, Closeable
常用方法有:
abstract void close() // Closes the stream and releases any system resources associated with it.
int read() // Reads a single character.
int read(char[] cbuf) // Reads characters into an array.
abstract int read(char[] cbuf, int off, int len) // Reads characters into a portion of an array.
long skip(long n) // Skips characters.
而Reader是抽象类,因此如果要进行文件操作,就需要子类FileReader来进行处理,其主要方法有:
FileReader(File file) // Creates a new FileReader, given the File to read, using the default charset.
从构造方法看,是用File类对象作为输入的对象。
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("memo.txt");
if(f.exists()) {
Reader input = new FileReader(f);
char data[] = new char[100];
int len = input.read(data);
System.out.println(new String(data,0,len));
input.close();
}
}
}
上面的代码中用File类对象构建FileReader对象,然后将File对象内容输入到FileReader对象中。
从上面的内容来看,字节流和字符流好像作用差不多。实际上,字节流可以直接与终端文件进行数据交互,字符流需要将数据经过缓冲区处理才与终端文件数据交互。
Writer类如果未调用close方法进行显式关闭的话,可能会由于缓冲区未清空而不能输出数据,此时需要调用flush方法进行缓冲区的强制清空。
转换流
虽然字节流和字符流是两种不同的数据流操作,但是这两种类是可以互相转换的,而这样的转换需要通过InputStreamReader、OutputStreamWriter两个类。
public class InputStreamReader extends Reader
InputStreamReader(InputStream in) // Creates an InputStreamReader that uses the default charset.
public class OutputStreamWriter extends Writer
OutputStreamWriter(OutputStream out) // Creates an OutputStreamWriter that uses the default character encoding, or where out is a PrintStream, the charset used by the print stream.
从上述类定义来看,InputStreamReader接收InputStream类对象进行构造,但却是Reader的子类,OutputStreamWriter接收OutputStream类对象进行构造,但却是Writer的子类,这样也就实现了转换。
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
File f = new File("memo.txt");
OutputStream output = new FileOutputStream(f);
Writer out = new OutputStreamWriter(output);
out.write("Hello world!");
out.flush();
out.close();
}
}
字符编码
在开发中,语言文字常用的4种编码为:
- GBK、GB2312:中文国际编码,GBK包含简体中文与繁体中文两种,GB2312只包含简体中文
- ISO8859-1:国际编码,可以描述任何文字信息(中文需要转码)
- UNICODE:十六进制编码
- UTF编码:UNICODE的可变长度编码,常见的编码为UTF-8编码
实际开发中,建议都是用UTF-8编码。
public class Demo {
public static void main(String[] args) throws Exception {
System.getProperties().list(System.out);
}
}
上边的代码可以显示系统环境属性中的文件编码格式。
......
file.encoding=UTF-8
......
内存流
之前提到的文件IO是指针对文件进行操作,操作期间需要借助文件作为载体,而如果某种应用需要进行IO操作,但又不想产生多余文件,可以将内存当作临时文件进行操作。
- 字节内存流:ByteArrayInputStream、ByteArrayOutputStream
- 字符内存流:CharArrayReader、CharArrayWriter
其实相关的操作逻辑都差不多,这里看一下字节内存流,其构造方法为:
public class ByteArrayInputStream extends InputStream
ByteArrayInputStream(byte[] buf) // Creates a ByteArrayInputStream so that it uses buf as its buffer array.
public class ByteArrayOutputStream extends OutputStream
ByteArrayOutputStream() // Creates a new ByteArrayOutputStream.
从继承关系来看,这两个类也是分别继承自InputStream和OutputStream类的,和之前类似。
而与之前文件操作的构造方法不同,这里的构造方法并不需要File类对象,而是直接使用byte类型数据,这表示该类对象实例化时需要准备好操作的数据信息。因此也就是说,内存流是将数据存储至内存中,然后利用IO流操作进行数据处理。
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
String str = "Hello world!";
InputStream input = new ByteArrayInputStream(str.getBytes());
OutputStream output = new ByteArrayOutputStream();
int tmp = 0;
while((tmp = input.read()) != -1) {
output.write(Character.toUpperCase(tmp));
}
System.out.println(output);
input.close();
output.close();
}
}
上面的代码中将str作为输入,将其内容读取到内存流中,然后将该内存中的内容转换为大写,然后将该内容输出到内存流中,进行打印。
打印流
之前提到的OutputStream虽然可以进行输出流控制,但是其数据为byte类型,因此不同类型的数据都需要进行转换才能够进行输出。而为了对不同类型数据进行适配,就需要使用打印流。
public class PrintStream extends FilterOutputStream implements Appendable, Closeable
public class PrintWriter extends Writer
其构造方法为:
PrintStream(File file) // Creates a new print stream, without automatic line flushing, with the specified file.
PrintStream(OutputStream out) // Creates a new print stream, without automatic line flushing, with the specified OutputStream.
PrintWriter(File file) // Creates a new PrintWriter, without automatic line flushing, with the specified file.
PrintWriter(OutputStream out) // Creates a new PrintWriter, without automatic line flushing, from an existing OutputStream.
查看PrintStream的方法可以看到,PrintStream中封装了不同参数类型的print函数,也因此就可以打印不同的数据类型。
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception {
PrintStream ps = new PrintStream(new FileOutputStream("memo.txt"));
ps.print("Hello world!");
ps.println("Hello world");
ps.println(100);
ps.printf("Name:%s,num:%d","Tom",10);
ps.close();
}
}
上面的代码说明了打印流的打印适配,同时也说明了打印流可以使用printf进行格式化字符串的打印。
System类对IO的支持
与C/C++类中直接利用cout和cin进行IO操作类似,JAVA中System类也支持IO操作,同时还提供了与IO相关的3个对象变量。
static final PrintStream err // The "standard" error output stream.
static final InputStream in // The "standard" input stream.
static final PrintStream out // The "standard" output stream.
从上面的结果来看,err和out都是PrintStream类对象,而in是InputStream类对象,之前使用的System.out.println进行打印正是调用的该类对象的相关方法。也就是说,这里的打印操作实际上还是IO操作。
System.err
该对象用于错误信息的输出操作。
public class Demo {
public static void main(String[] args){
try {
double num = 10 / 0;
} catch (Exception e) {
System.err.println(e);
}
}
}
执行结果为:
java.lang.ArithmeticException: / by zero
从上面的结果来看,System.err和System.out一样都可以打印结果,两者又有什么区别:
- System.err:输出不希望用户看见的异常
- System.out:输出希望用户看到的信息
System.out
System.out一般用来进行打印输出,这个很常见。
import java.util.function.Consumer;
public class Demo {
public static void main(String[] args){
Consumer<String> con = System.out::println;
con.accept("Hello world");
}
}
上面的代码使用Consumer引用System.out.println方法,作用是一样的。
System.in
既然有输出,就也会有输入。System.in可以用来获取键盘的输入。
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception{
InputStream input = System.in;
byte data[] = new byte[1024];
System.out.print("Please input data:");
int len = input.read(data);
System.out.println(new String(data,0,len));
}
}
基本逻辑跟之前都是一样的,不过如果键盘输入超过data的长度,超出部分不会被接收,此时可以使用改用内存流进行接收,逻辑都是一样的。
或者也可以使用循环的方式进行读取,并将每次读取的数据利用StringBuffer类对象保存。
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception{
InputStream input = System.in;
StringBuffer buf = new StringBuffer();
System.out.print("Please input data:");
int tmp = 0;
while((tmp = input.read()) != -1) {
if(tmp == '\n') {
break;
}
buf.append((char)tmp);
}
System.out.println(buf);
}
}
上边的input.read是按照字节进行读取的,在进行英文字符输入的时候是正常的,但是在中文等字符输入的时候,因为编码的原因可能会存在问题,此时可以采用字符缓冲流来进行输入。
字符缓冲流:BufferedReader
之前提到在获取键盘输入的时候,中文等多字节编码的字符集可能会遇到问题,此时可以采用字符缓冲流进行缓冲读取,然后再利用输入流一次性读取内容。
- 字符缓冲区流:BufferedReader、BufferedWriter
- 字节缓冲区流:BufferedInputStream、BufferedOutputStream
这里看一下BufferedReader:
public class BufferedReader extends Reader
其常用方法有:
BufferedReader(Reader in) // Creates a buffering character-input stream that uses a default-sized input buffer.
String readLine() // Reads a line of text.
这里会有个问题,System.in是InputStream的子类,而BufferedReader是Reader的子类,因此如果要想用BufferedReader去获取InputStream的输入,需要进行转换。
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception{
BufferedReader buf = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Please input data:");
String str = buf.readLine();
System.out.println(str);
}
}
而既然BufferedReader是接收Reader进行构造,那么下面的代码也是合理的:
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception{
File f = new File("memo.txt");
BufferedReader buf = new BufferedReader(new FileReader(f));
String str = null;
while((str = buf.readLine()) != null) {
System.out.println(str);
}
}
}
上面的代码利用File构造FileReader,继续构造BufferedReader,然后读取文件内容。从形式上来看,这种方式比之前的操作更加简单方便。
扫描流:Scanner
Scanner属于java.util,利用该类可以实现数据的输入操作。与之前IO操作定义在java.io包中不同,因此该类只是一个工具类。
public final class Scanner extends Object implements Iterator<String>, Closeable
主要方法有:
Scanner(InputStream source) // Constructs a new Scanner that produces values scanned from the specified input stream.
boolean hasNext() // Returns true if this scanner has another token in its input.
String next() // Finds and returns the next complete token from this scanner.
Scanner useDelimiter(String pattern) // Sets this scanner's delimiting pattern to a pattern constructed from the specified String.
从上边的定义来看,Scanner实现了Iterator接口,还可以接收InputStream进行构造。
import java.util.Scanner;
public class Demo {
public static void main(String[] args) throws Exception{
Scanner scan = new Scanner(System.in);
System.out.print("Please input data:");
if(scan.hasNext()) {
System.out.println(scan.next());
}
scan.close();
}
}
从上面的代码来看,利用Scanner获取键盘输入更为简单。
其实hasNext方法还有类似hasNextXxx的方法,可以用来判断是否存在诸如double、int等类型的数据,同时借助nextInt、nextDouble等方法也可以直接获取对应数据。
同时hasNext方法还有其它的重载方法,可以对输入数据进行正则判断等,具体可查看对应的方法定义,不过逻辑都是类似的。
同时Scanner既然接收的类型是InputStream,因此也可以设置文件作为构造方法参数。不过因为不同文件用处不同,需要事先利用useDelimiter方法来设置分隔符,默认为空字符。
import java.util.Scanner;
import java.io.*;
public class Demo {
public static void main(String[] args) throws Exception{
Scanner scan = new Scanner(new FileInputStream("memo.txt"));
scan.useDelimiter("\n");
while(scan.hasNext()) {
System.out.println(scan.next());
}
scan.close();
}
}
上面的代码和之前含义类似,不过写法较之前会简便。
对象序列化
在本文最开始说到,IO操作主要依赖五个类和一个接口,五个类已经都了解了,只剩下Serializable接口。
JAVA中各种对象的生命周期都在JVM进程中,而采用对象序列化的方法可以在JVM进程结束后保存对象,或在不同JVM进程间进行传输。而对象序列化和反序列化主要与Serializable有关。
序列化接口:Serializable
对象序列化的本质其实是将内存中保存的对象转换为二进制数据进行传输。但也并不是所有的类对象都能够实现序列化操作,其一定要实现Serializable接口才可以进行序列化。
public interface Serializable {
}
从上面的定义来看,该接口中没有任何属性和方法,只是标识而已。也就是说只要实现了该接口,不同覆写任何方法,就可以进行序列化进行二进制传输。
序列化与反序列化
虽然上边那么说,但并不是说实现了Serializable接口就可以实现序列化操作。实际对象的序列化和反序列化,还需要两个类支持:
- 序列化操作类:java.io.ObjectOutputStream,将对象序列化为指定格式的二进制数据
- 反序列化操作类:java.io.ObjectInputStream,将序列化的二进制对象信息转换为对象内容
ObjectOutputStream(OutputStream out) // Creates an ObjectOutputStream that writes to the specified OutputStream.
final void writeObject(Object obj) // Write the specified object to the ObjectOutputStream.
ObjectInputStream(InputStream in) // Creates an ObjectInputStream that reads from the specified InputStream.
final Object readObject() // Read an object from the ObjectInputStream.
通过上边的方法定义可以看到,序列化对象和writeObject接收Object作为参数,而反序列化操作readObject返回Object对象。
import java.util.Scanner;
import java.io.*;
class A implements Serializable {
private String name;
private int num;
public A(String name, int num) {
this.name = name;
this.num = num;
}
@Override
public String toString() {
return "Name is:" + name + ",num is:" + num;
}
}
public class Demo {
public static void main(String[] args) throws Exception{
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(new File("memo.txt")));
ObjectInputStream input = new ObjectInputStream(new FileInputStream(new File("memo.txt")));
output.writeObject(new A("Tom",100));
Object obj = input.readObject();
A tmp = (A)obj;
System.out.println(tmp);
output.close();
input.close();
}
}
执行结果为:
Name is:Tom,num is:100
上面的代码可以将类A对象转换为二进制转存到文件中,然后反序列化将文件中的二进制内容转换为类A对象,并进行打印。
不过上面的代码还存在一点问题,问题在于对于反序列化得到的对象进行了向下转换,从Object类对象转换为类A对象,这样不是特别安全,在实际开发中可以利用反射机制处理。
transient
这是一个关键字。
在JAVA对象中最重要的就是其属性内容,而序列化操作时,需要对对象的属性信息进行序列化,而如果某些属性内容不需要被保存,就可以通过transient来定义说明。
import java.util.Scanner;
import java.io.*;
class A implements Serializable {
private transient String name;
private int num;
public A(String name, int num) {
this.name = name;
this.num = num;
}
@Override
public String toString() {
return "Name is:" + name + ",num is:" + num;
}
}
public class Demo {
public static void main(String[] args) throws Exception{
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(new File("memo.txt")));
ObjectInputStream input = new ObjectInputStream(new FileInputStream(new File("memo.txt")));
output.writeObject(new A("Tom",100));
Object obj = input.readObject();
A tmp = (A)obj;
System.out.println(tmp);
output.close();
input.close();
}
}
执行结果为:
Name is:null,num is:100
上面的代码和之前是一样的,只是多用了transient关键字来修饰name属性,这样在序列化的时候就不会保存该属性,同时在反序列化时该属性也为null。不过实际开发中,也很少见该关键字。