IO
File对象
构造一个File对象需要传入文件路径:
public static void main(String[] args) {
File file = new File("C:\\Users\\xiong\\Documents\\xiong\\pom.xml");
System.out.println(file);
}
构造File对象时既可以传入绝对路径,也可以传入相对路径。File对象有三种方式表示的路径,一种时getPath(),返回构造方法传入的路径,一种是getAbsolutePath(),返回绝对路径,一种是getCannonicalPath(),它和绝对路径类似,但是返回的是规范路径:
public static void main(String[] args) throws IOException {
File f = new File("..");
System.out.println(f.getPath());
System.out.println(f.getAbsolutePath());
System.out.println(f.getCanonicalPath());
// ..
// C:\Users\xiong\Documents\xiong\..
// C:\Users\xiong\Documents
}
文件和目录
File对象即可以表示文件,也可以表示目录,如果传入的文件或者目录不存在,代码也不会报错,因为构造一个对象不会真的进行磁盘的操作,只有调用File的某些方法的时候,才真正的进行磁盘操作。
File对象取到一个文件时,可以判断文件的大小和权限:
- boolean canRead():是否可读
- boolean canWrite():是否可写
- boolean canExecute():是否可执行(对于目录而言,是否可执行表示能否列出它包含的文件和子目录)
- long length():文件字节大小
常用方法
- String getName():返回文件或目录名;
- String getParent():返回上一级目录的绝对路径;
- String getCanonicalPath():返回规范的路径;
- boolean isDirectory():是否是目录;
- boolean isFile():是否是文件;
- long length():返回目录长度;
- boolean exists():目录或文件是否存在;
InputStream
InputStream就是java标准库提供的基本的输入流。InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类的最重要的方法是 int read():
public abstract int read() throws IOException;
这个方法读取输入流的下一个字节,并返回字节表示的int值(0~255),如果已读到末尾,返回-1表示不能继续读取。
FileInputStream是InputStream的一个子类,它表示从文件流中读取数据。
public static void main(String[] args) throws IOException {
File f = new File("C:\\Users\\xiong\\Documents\\xiong\\pom.xml");
FileInputStream fileInputStream = new FileInputStream(f); // 创建流
for (;;) {
int read = fileInputStream.read();
if (read == -1) {
break;
}
System.out.println(read);
}
fileInputStream.close(); // 关闭流
}
在计算机中,类似文件、网络端口这些资源,都是由操作系统统一管理的。应用程序在运行的过程中,如果打开了一个文件进行读写,完成后要及时地关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其他应用程序的运行。InputStream和OutPutStream都是通过close()方法来关闭流。关闭就会释放对应的底层资源。我们还要注意到在读取或写入IO流的过程中,可能会发生错误,例如文件不存在,文件权限导致读取失败等,这些底层错误由JVM自动封装了IOException异常并抛出。因此所有与IO操作相关的代码都必须正确处理IOException。我看上边的代码发现,如果上边的的代码报错抛异常的话,流就没法关闭。所以我们要通过try…finally来保证及时代码出错,流也能正确的关闭。
public static void main(String[] args) throws IOException {
File f = null;
FileInputStream fileInputStream = null;
try {
f = new File("C:\\Users\\xiong\\Documents\\xiong\\pom.xml");
fileInputStream = new FileInputStream(f);
for (;;) {
int read = fileInputStream.read();
if (read == -1) {
break;
}
System.out.println(read);
}
} finally {
fileInputStream.close();
}
}
但是更推荐使用try-with-resource写法,让编译器自动给我们关闭流:
public static void main(String[] args) {
try (FileInputStream fileInputStream = new FileInputStream("C:\\Users\\xiong\\Documents\\xiong\\pom.xml")) {
int i;
while ((i = fileInputStream.read()) != -1) {
System.out.println(i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
缓冲
在读取流的时候一次读取一个字节的方式效率不高。我们可以采用一次读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区,一次读取多个字节效率要高很多。InputStream提供了两个重载方法来支持读取多个字节:
- int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
- int read(byte[] b, int off, int len):指定bytep[]数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多的读取自己到缓冲区,但不会超过缓冲区的大小。read()方法返回值不在是字节的int值,而是读取了多少个字节,当返回-1时,表示没有数据了。
public static void main(String[] args) {
try (FileInputStream fileInputStream = new FileInputStream("C:\\Users\\xiong\\Documents\\xiong\\pom.xml")) {
byte[] bytes = new byte[1024];
int i;
while ((i = fileInputStream.read(bytes)) != -1) {
System.out.println(i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
OutputStream
OutputStream是Java标准库的基本输出流。和InputStream类似,它也是抽象类,它最重要的一个方法就是:
public abstract void write(int b) throws IOException;
这个方法写入一个字节到输出流。虽然传入的是int参数,但是只会写入一个字节。
OutputStream也提供了close()方法来关闭流。同时它还有一个flush()方法,作用是将缓冲区的内容input到目的地。为什么会有这么个操作?因为向磁盘、网络写入数据的时候,考虑到效率问题,操作系统并不是输出一个字节就立刻写入到磁盘或网络,则是把字节先放入内存的一个缓存区里,等缓存区满了,在一次性写入到磁盘或者网络。使用flush()方法可以强制把缓冲区内容输出。通常我们不需要手动调用它,在缓存区满了后会自动调用,并且在调用close()方法之前也会调用。
FileOutputStream
将若干字节写入文件流:
public static void main(String[] args) {
try (FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\xiong\\Documents\\xiong\\out.txt")) {
String str = "test outputStream";
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
fileOutputStream.write(bytes);
} catch (Exception e) {
e.printStackTrace();
}
}
序列化
序列化是指把一个java对象变成二进制内容,本质上就是一个byte[]数组。序列化可以把byte[]保存到文件中或者通过网络传输出去。
有序列化就有反序列化,反序列化就是把一个二进制内容也就是byte[]数组变回java对象。
一个Java对象要能序列化必须实现java.io.Serializable接口,这个接口没有定义任何方法,我们把这样的空接口称为“标记接口”。
序列化
把一个java对象序列化,需要使用OutputStream,它负责把一个java对象写入一个字节流。
public void test() throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
// 写入int:
output.writeInt(12345);
// 写入String:
output.writeUTF("Hello");
// 写入Object:
output.writeObject(Double.valueOf(123.456));
}
System.out.println(Arrays.toString(buffer.toByteArray()));
}
反序列化
public void test() throws Exception {
byte[] bytes;
try (ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(buffer)) {
output.writeObject("Hello World");
bytes = buffer.toByteArray();
}
try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream intput = new ObjectInputStream(bis)) {
String s = (String) intput.readObject();
System.out.println(s);
}
}
注意:这里的ByteArrayOutputStream一定要是ObjectOutputStream的,负责会报错。
其次要注意:反序列化时JVM直接构造出java对象,不调用构造方法,构造方法内部的代码不会被执行。
安全性
因为java的序列化和反序列化可以导致java对象能直接从byte[]创建,而不经过构造方法,因此,它存在一定的安全隐患。实际开发中,我们一般通过JSON这样的通用数据结构来实现。
Reader
Reader是java的IO库提供的另一个接口,和InputStream的区别是,InputStream是字节流,即以byte为单位,而Reader是字符流,以char为单位。
InputStream | Reader |
---|---|
字节流,以byte为单位 | 字符流,以char为单位 |
读取字节(-1,0~255):int read() | 读取字符(-1,0~65535):int read() |
读到字节数组:int read(byte[] b) | 读到字符数组:int read(char[] c) |
java.io.Reader是所有字符输入的流的超类,它的主要方法是:
public int read() throws IOException;
这个方法读取字符流的下一个字符,并返回字符表示的int,范围是0~65535,如果已读到末尾返回-1。
FileReader
FilleReader是reader的一个子类,它可以打开文件并获取Reader,下面代码演示了如何完整地读取一个FileReader的所有字符:
public void testFileReader() {
try (FileReader fileReader = new FileReader("C:\\Users\\xiong\\Documents\\xiong\\pom.xml");){
int i;
while ((i = fileReader.read()) != -1) {
System.out.println((char) i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
Reader也是一种资源,需要保证出错的时候也能正确的关闭,所以我们需要try(resource)来保证Reader无论有没有IO错误,都可以被关闭。
如果我们读取一个纯ASCIII编码的文本文件,是没有问题的,但如果文件包含中文,就会出现乱码,因为FileReader默认的编码与系统相关。要避免乱码,在创建FileReader的时候就要指定编码:
Reader reader = new FileReader("/src/test.txt", StanderCharsets.UTF_8);
Reader也提供了一次读取若干字符到char[]的方法:
public int read(char[] c) throws IOException
它返回实际读入的字符个数,最大不超过char[]数组的长度。返回-1表示结束。
利用这个方法,我们先设置一个缓冲区,每次尽可能地填充缓冲区:
public void testFileReader() {
try (FileReader fileReader = new FileReader("C:\\Users\\xiong\\Documents\\xiong\\pom.xml");){
char[] buffer = new char[1024];
int i;
while ((i = fileReader.read(buffer)) != -1) {
System.out.println("read" + i + "chars");
}
} catch (Exception e) {
e.printStackTrace();
}
}
Writer
Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。
Writer和OutputStream的区别如下:
OutputStream | Writer |
---|---|
字节流,以byte为单位 | 字符流,以char为单位 |
写入字节(0~255):void write(int b) | 写入字符(0~65535):void write(int c) |
写入字节数组:void write(byte[] b) | 写入字符数组:void write(char[] c) |
无对应方法 | 写入String:void write(String s) |
Writer是所有字符输出流的超类,它提供的方法主要有:
- 写入一个字符(0~65535):void write(int c);
- 写入字符数组的所有字符:void write(char[] c);
- 写入String表示的所有字符:void write(String s)。