文章目录
- 1 File类
- 1.1 File类深入
- 2 获取文件信息
- 3 文件操作范例
- 4 OutputStream字节输出流
- 5 字节输入流InputStream
- 6 字符输出流Writer
- 7 字符输入流Reader
- 8 范例:文件拷贝
- 9 范例:目录拷贝
- 10 管道流
- 11 打印流
- 12 内存流
- 13 缓存输入流BufferedReader
- 14 对象序列化
- 14.1 使用Serializable接口实现自动序列化
- 14.2 对象属性部分序列化:transient关键字
- 14.3 自定义序列化
- 14.4 Externalizable接口实现部分序列化
- 14.5 Serializable VS. Externalizable对比
- 15 try-with-resource
- 16 文件操作范例
- 17 快递E栈实现心得
1 File类
Java提供java.io包以支持文件操作。其中File类定义了各种文件操作。在这个包中,只有File类与文件本身操作有关,包括文件创建、删除、重命名等。注意,需要提供文件的完整路径才能进行File类的初始化。
- 构造方法:
public File(String pathname);
- 构造方法:
public File(String parent, String child);
- 构造方法:
public File(File parent, String child);
- 创建新文件:
public boolean createNewFile() throws IOException;
- 如果文件存在,则创建失败,返回false;
- 否则返回true。
- 判断文件是否存在:
public boolean exists();
- 删除文件:
public boolean delete();
1.1 File类深入
- 使用File.seperator表示分隔符。
- 文件创建的过程:程序 -> JVM ->操作系统函数 -> 文件创建,存在一定延迟,因此文件要尽量
避免重名情况
(使用UUID方法命名文件)。 - 文件创建的时候其父路径必须存在。
- 获取父路径:
public File getParentFile();
- 创建目录:
public boolean mkdirs();
(创建多级目录。如果是mkdir则只创建最后一级目录)。
文件创建标准操作:
import java.io.*;
public class JavaDemo {
public static void main(String[] args) {
File file = new File("E:" + File.separator
+ "mldn" + File.separator
+ "hello" + File.separator
+ "hello.txt");
if(!file.exists()) { //如果文件不存在,则创建文件
if(!file.getParentFile().exists()) { // 父路径不存在
file.getParentFile().mkdirs();
}
try {
System.out.println(file.createNewFile() ? "文件创建成功" : "文件创建失败");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
System.out.println(file.delete()? "文件删除成功" : "文件删除失败");
}
}
}
2 获取文件信息
通过File类的一些方法可以获取到文件信息。
-
public boolean canExecute()
是否是可执行文件 -
public boolean canWrite()
是否可写 -
public boolean canRead()
是否可读 -
public boolean isDirectory()
是否是一个目录 -
public boolean isFile()
是否是文件 -
public long lastModified()
返回最后一次的修改时间。可通过Date和SimpleDateFormat转成可读的形式。 -
public long length()
返回文件的大小,单位B。可以通过/1024.0转成KB。 -
public File[] listFiles()
返回当前目录下的所有文件(File形式) -
public File[] listFiles(FileFilter filter)
指定过滤器,返回当前目录下的所有文件。注意File Filter是一个函数式接口,可以直接使用Lambda表达式定义。
范例:
public class Test02 {
public static void main(String[] args) {
File f = new File("E:\\Program Files\\Downloads\\BaiduYunDownload");
listSomeFiles(f);
}
public static void listSomeFiles(File dir){
if(!dir.isDirectory()){
return;
}
File[] files = dir.listFiles((file) -> {
if(file.isDirectory()){
return true;
}else {
if (file.getName().endsWith(".txt")) {
return true;
} else {
return false;
}
}
});
if(files != null && files.length > 0) {
for (File f : files) {
if (f.isFile()) {
System.out.println(f);
} else {
listSomeFiles(f);
}
}
}
}
}
-
public String getPath()
返回文件的相对路径
(相当于项目文件夹的路径) -
public String getAbsolutePath
返回文件的绝对路径
-
public String getCanonicalPath
返回文件的规范路径
(把路径名中的.
和..
隐去)
对比getPath、getAbsolutePath、getCanonicalPath:
- 当定义文件路径时使用了
.
或..
符号时,以上方法才有区别。- getPath就是相对路径,即在定义File对象时的实参,输入是什么这里的getPath得到的就是什么。
- getAbsolutePath是获得绝对路径,将当前工程的目录 + File定义的相对路径整合,打印完整的路径名称。
- getCanonicalPath是获得规范路径,将绝对路径中的
.
和..
隐去。
范例:
public static void main(String[] args) {
File f = new File (".." + File.separator + "a.txt");
System.out.println("直接打印文件:" + f);
System.out.println("getPath打印:" + f.getPath());
System.out.println("getAbsolutePath打印:" + f.getAbsolutePath());
try {
System.out.println("getCanonicalPath打印:" + f.getCanonicalPath());
} catch(Exception e){
}
}
输出结果:
直接打印文件:..\a.txt
getPath打印:..\a.txt
getAbsolutePath打印:E:\workspace\JavaWorkspace4\express_station_collection\..\a.txt
getCanonicalPath打印:E:\workspace\JavaWorkspace4\a.txt
3 文件操作范例
对于文件批量操作的问题(将当前目录下的所有文件进行一个统一的操作),通常都使用递归的形式处理。
范例:(1)列出指定目录下的所有文件;(2)修改指定目录下所有文件的后缀为.txt
import java.io.*;
public class JavaDemo {
public static void main(String[] args) {
File dir = new File("E:" + File.separator
+ "mldn");
FileUtil.listAllFiles(dir);
FileUtil.renameDir(dir);
}
}
class FileUtil{
public static void listAllFiles(File file) {
if(file.isDirectory()) {
File[] files = file.listFiles();
for(File f: files) {
FileUtil.listAllFiles(f);
}
}else {
System.out.println(file);
}
}
public static void renameDir(File file) {
if(file.isDirectory()) {
File[] files = file.listFiles();
for(File f: files) {
renameDir(f);
}
}else {
if(file.isFile()) { //进行文件重命名
File base = file.getParentFile();
String fileName = file.getName();
fileName = fileName.substring(0, fileName.lastIndexOf(".")) + ".txt";
file.renameTo(new File(base, fileName));
}
}
}
}
4 OutputStream字节输出流
继承关系:
由于继承了AutoCloseable接口,因此可以实现自动关闭(把对象创建放在try()小括号里面即可)。
OutputStream类里主要关注三个写入方法:
-
public void write(byte[] b)
写入字节数据 -
public void write(byte[] b, int off, int len)
-
public abstract void write(int b)
写入指定字节
OutputStream是一个抽象类,因此需要通过实例化子类对象进行操作。这里使用FileOutputStream子类实例化。
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. -
FileOutputStream(String name)
Creates a file output stream to write to the file with the specified name. -
FileOutputStream(String name, boolean append)
Creates a file output stream to write to the file with the specified name.
在FileUtil类中增加如下的静态方法,实现标准的文件写入操作。
/**
* 向指定文件目录下写入字符串数据
* @param info
* @param filename 要写入的文件的绝对地址。
* @param append 是否是追加模式
*/
public static <T> void write(T info, File file, boolean append) {
String s = String.valueOf(info);
byte[] content = s.getBytes();
if(!file.getParentFile().exists()) { //父目录不存在
file.getParentFile().mkdirs();
}
try(OutputStream out = new FileOutputStream(file, append)){ //实现自动关闭
out.write(content);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
注意:write操作不需要file文件存在,只要保证父目录存在即可,该file文件如果不存在会自动生成。
5 字节输入流InputStream
构造方法和字节写入流类似。
主要的读取方法如下:
abstract int read()
返回的是读取的数据,如果返回-1表示已经读取完毕。int read(byte[] b)
int read(byte[] b, int off, int len)
将指定长度(len)的数据读取到byte数组,从数组的off位置开始存储。返回真实的读取长度。(假如,则返回值会小于len。)
范例:在FileUtil类中增加如下的操作方法
/**
*
* @param file 要读取的文件
* @param lenOfEachTime 每一次读取的字节长度
* @return
*/
public static String read(File file, int lenOfEachTime) {
if(!file.exists()) {
return null;
}
StringBuilder sb = new StringBuilder();
byte[] contents = new byte[lenOfEachTime];
try(InputStream in = new FileInputStream(file)){
int len = -1;
while((len = in.read(contents)) != -1) {
sb.append(new String(contents, 0, len));
}
}catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
6 字符输出流Writer
实现字符形式的输出。
Writer可以改变输出流中的内容,通过append()方法追加内容。
Writer输出的最大优势在于可以直接写入字符串,此外Writer是字符流,最大的优势是对中文的支持。
Writer的常用操作:
void write(char[] cbuf)
abstract void write(char[] cbuf, int off, int len)
void write(int c)
void write(String str)
void write(String str, int off, int len)
FileWriter构造方法:
FileWriter(File file, boolean append)
FileWriter(String fileName, boolean append)
区分:append方法和write方法。
- 其实本质上这两个方法都受到FileWriter构造方法中的append参数限制。当append=true,则不管用append还是write,都是向文件中追加输出内容。否则,两种方法都是覆盖文件。
- append方法之所以叫这个名字,是因为它的返回值还是一个Writer的对象,这样我们可以通过一行代码不断调用append实现写入,例如:
Writer w = new FileWriter("C:" + File.separator + "a.txt"); w.append("xxx").append("yyyy").append("zzzzzzzz"); // 此处可以无限追加 w.close(); //关闭流,将上面一行代码追加的所有内容写入文件
- 另外需要注意的是,FileWriter的write和append方法在Writer字节流关闭之前,不管执行多少次的write或者append,都是追加操作。在每次打开流的时候才会根据append值选择针对原文件是追加还是覆盖。
- Writer使用了缓冲区的概念,当执行write或者append方法时,其实只是将字符串写进了缓冲区,只有将Writer正确关闭,才能将缓冲区的内容写进文件里。(这也解释了上一条的结果)。如果想要在不关闭Writer的时候就把缓冲区数据写进文件,需要手动执行flush()方法强制清空缓冲区并把内容写入文件。(注意:字节流并没有使用到缓冲区,每次write字节数组时是直接写入文件了。)
范例:在FileUtil类中增加write方法如下:
public static <T> void write(File file, T content,boolean append) {
String str = String.valueOf(content);
if(!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
try(Writer w = new FileWriter(file, append)){
w.write(str);
w.write("\r\n");
w.append("追加的内容1\r\n");
w.append("追加的内容2\r\n");
}catch(IOException e) {
e.printStackTrace();
}
}
7 字符输入流Reader
和字节输入流用法几乎一致。注意一点,与字符输出流Writer可以写入一整个字符串不同,FileReader无法实现读取一整个字符串,仍然只能将读取数据保存在char数组中。
范例:在FileUtil类中增加如下的静态方法,通过字节输入流读取数据。
/**
* 通过字节输入流的方法读取指定文件内的内容。
* @param file
* @return
*/
public static String read(File file) {
if(!file.exists()) {
return null;
}
char[] temp = new char[1024]; //每次读取1024个字符
StringBuilder result = new StringBuilder(); //保存最后的结果
try(Reader r = new FileReader(file)){
int len = -1;
while((len = r.read(temp)) != -1) {
result.append(new String(temp, 0, len));
}
}catch(IOException e) {
e.printStackTrace();
}
return result.toString();
}
8 范例:文件拷贝
除了传统的方法之外,从JDK 1.9开始增加了如下的方法:
InputStream in = new FileInputStream(srcFile);
OutputStream out = new FileOutputStream(desFile);
in.transferTo(out); // 新的方法,since JDK 1.9
// Reader中也有同样的方法:reader.reansferTo(writer);
在File类中定义有一个文件移动(剪切)方法如下:public boolean renameTo(File dest)
该方法可以将当前的File移动到指定的dest位置。注意每次移动都需要检查返回值以确定是否转移成功。
9 范例:目录拷贝
要求:将指定目录下的内容拷贝到目标路径下,包括所有子文件夹中的文件。
实现概述:利用递归操作。另外注意新目录下的文件路径结构需要保持一致。例如:源目录D:\mldn\
,该目录下有一个子目录文件,其absolutePath为D:\mldn\java\abc\hello.txt
。要拷贝到D:\xyz
下,则每个文件在目标文件夹下的路径应该是D:\xyz\java\abc\hello.txt
。因此,目标文件的File对象应该按如下代码创建:
String newPath = srcFile.getPath().replace(FileUtil.srcFile.getPath() + File.separator, "");
File newFile = new File(desFile, newPath);
解释:新路径 = 目标文件夹路径 + 文件路径(把src目录路径替换为"")
。
实现代码:在FileUtil类中增加静态属性srcFile,保存源目录。然后定义目录拷贝对外方法和private递归调用方法。
import java.io.*;
public class JavaDemo {
public static void main(String[] args) {
if(args.length != 2) {
System.out.println("输入长度不为2,请检查命令执行是否为:java JavaDemo srcDirPath desDirPath");
return;
}
File srcDir = new File(args[0]);
File desDir = new File(args[1]);
if(!srcDir.isDirectory()) { // src不是文件夹,则调用文件拷贝操作。
System.out.println(FileUtil.copyFile(srcDir, desDir)? "拷贝目录成功" : "拷贝目录失败");
}else {
System.out.println(FileUtil.copyDir(srcDir, desDir)? "拷贝目录成功" : "拷贝目录失败");
}
}
}
class FileUtil{
private static File srcFile = null;
/**
* 文件拷贝处理,从src拷贝到des
* @param src
* @param des
* @throws Exception
*/
public static boolean copyFile(File srcFile, File desFile) {
if(srcFile == null || "".equals(srcFile) || desFile == null || "".equals(desFile)) {
System.out.println("输入路径为空,请检查输入路径!");
return false;
}
if(!srcFile.exists()) {
System.out.println("源文件没有找到,请检查源文件目录!");
return false;
}
if(!desFile.getParentFile().exists()) {
desFile.getParentFile().mkdirs();
}
try {
InputStream in = new FileInputStream(srcFile);
OutputStream out = new FileOutputStream(desFile);
byte[] temp = new byte[1024];
int len = -1;
while((len = in.read(temp)) != -1) {
out.write(temp, 0, len);
}
in.close();
out.close();
return true;
}catch(IOException e) {
e.printStackTrace();
return false;
}
}
public static boolean copyDir(File srcFile, File desFile) {
if(srcFile == null || !srcFile.exists()) {
return false;
}
FileUtil.srcFile = srcFile;
copyDir0(FileUtil.srcFile, desFile);
return true;
}
private static boolean copyDir0(File srcFile, File desFile) {
if(srcFile.isDirectory()) {
File[] files = srcFile.listFiles();
for(File f: files) {
copyDir0(f, desFile);
}
}else {
if(srcFile.isFile()) {
String newPath = srcFile.getPath().replace(FileUtil.srcFile.getPath() + File.separator, "");
File newFile = new File(desFile, newPath);
try {
FileUtil.copyFile(srcFile, newFile);
return true;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}
}
}
return true;
}
}
10 管道流
负责线程对象之间的通信。也分为字节流(PipedInputStream, PipedOutputStream)和字符流(PipedReader, PipedWriter)。
管道流操作有一个重要的概念:连接
在PipedOutputStream和PipedWriter类里,存在一个connect方法实现连接。
- PipedOutputStream:
public void connect(PipedInputStream snk) throws IOException
- PipedWriter:
public void connect(PipedReader snk) throws IOException
或者通过两者的构造方法也可以指定要连接的管道输入流。
public PipedOutputStream(PipedInputStream snk) throws IOException
public PipedWriter(PipedReader snk) throws IOException
import java.io.*;
public class JavaDemo {
public static void main(String[] args) {
SendThread st = new SendThread();
ReceiveThread rt = new ReceiveThread();
st.connect(rt);
new Thread(rt,"接收线程").start();
new Thread(st,"发送线程").start();
}
}
class SendThread implements Runnable{
private PipedOutputStream pos = null;
public SendThread() {
this.pos = new PipedOutputStream();
}
public SendThread(PipedInputStream pis) {
try {
this.pos = new PipedOutputStream(pis);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void connect(ReceiveThread rt) {
try {
this.pos.connect(rt.getPis());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
String str = "【" + Thread.currentThread().getName() + "】发送消息:hello world!";
byte[] data = str.getBytes();
try {
for(int x = 0; x < 3; x++) {
Thread.sleep(2000);
this.pos.write(data);
//this.pos.flush();
}
this.pos.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class ReceiveThread implements Runnable{
private PipedInputStream pis = null;
public ReceiveThread() {
this.pis = new PipedInputStream();
}
public PipedInputStream getPis() {
return this.pis;
}
@Override
public void run() {
int len = -1;
byte[] result = new byte[1024];
try {
while ((len = this.pis.read(result)) != -1) {
System.out.println(new String(result, 0, len));
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
思考:
- 管道流实现的时候其实有生产者消费者的思想,只有发送方发送了数据(对于PipedOutputStream通过write方法,对于PipedWriter通过flush()方法),接收方才能接收到数据。当没有数据时,接收方会被挂起。
11 打印流
打印流的设计思想在于,增强已有类的功能。例如,OutputStream是唯一能实现输出的操作类,所以应该以其为核心根本。但这个类的功能有限,不方便输出各种数据类型。所以就为它做了一层包装。
以后程序进行内容输出的时候一律用打印流。遇到打印字符的操作用PrintWriter,遇到非字符类的文件(比如图像,音频等)用PrintStream。
import java.io.*;
public class JavaDemo {
public static void main(String[] args) throws Exception {
PrintWriter pw = new PrintWriter(new FileWriter(new File("E:" + File.separator
+ "mldn" + File.separator
+ "abx.txt"), true));
pw.println(45);
pw.append("23\r\n");
pw.printf("年龄:%d, 成绩:%2.2f\r\n", 25, 97.866623f);
pw.close();
}
}
12 内存流
内存流需要注意的是,以内存为参考中心,read方法是写入内存,write方法读取内存。
范例:通过内存流实现字符串大小写转换。
思路:把要保存的内容传入ByteArrayInputStream构造方法中(这样就把内容放到了内存里),然后把字符串按字母的方式依次读取并转换大小写保存到内存中(read方法),这些都做完之后从内存中读取(write方法保存到ByteArrayOutputStream中),最后通过bos的toString方法把bos保存的内容输出。
String str = "HELLOWORLD"; // 定义一个字符串,全部由大写字母组成
ByteArrayInputStream bis = null; // 内存输入流
ByteArrayOutputStream bos = null; // 内存输出流
bis = new ByteArrayInputStream(str.getBytes()); // 向内存中输出内容,注意,没有跟文件读取一样,设置文件路径。
bos = new ByteArrayOutputStream(); // 准备从内存ByteArrayInputStream中读取内容,注意,跟文件读取不一样,不要设置文件路径。
int temp = 0;
while ((temp = bis.read()) != -1) {
char c = (char) temp; // 读取的数字变为字符
bos.write(Character.toLowerCase(c)); // 将字符变为小写
} // 所有的数据就全部都在ByteArrayOutputStream中
String newStr = bos.toString(); // 因为所有output的数据都在ByteArrayOutputStream实例对象中,所以可以取出内容,将缓冲区内容转换为字符串。
try {
bis.close();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(newStr);
13 缓存输入流BufferedReader
此类在JDK1.5之前是最完善的处理数据输入的类。此类有一个最重要的方法:
public String readLine() throws IOException
可以按行读取数据,但默认按换行作为分隔符,不能由用户自定义。
import java.io.*;
public class JavaDemo {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();
System.out.println(str);
}
}
14 对象序列化
14.1 使用Serializable接口实现自动序列化
对象序列化,即把对象转化成二进制流保存到文件的一种方式。通过对象序列化可以方便地实现对象的传输或存储。
一个对象要想被序列化必须实现Serializable接口,该接口是一个标记接口,没有任何的内容,表示一种能力。
- 对象写入流:
ObjectOutputStream extends OutputStream
- OutputStream中通用的写方法:
write(int c), write(byte[] bytes), write(byte[] bytes, int off, int len)
- 写入特定的对象:
writeBoolean(boolean val)
writeByte(int val)
writeBytes(String str)
writeChar(int val)
writeChars(String str)
writeDouble(double val)
writeFloat(float val)
writeInt(int val)
- …
- 写入Object
writeObject(Object obj)
- 对象读取流:
ObjectInputStream extends InputStream
操作方法类似于ObjectOutputStream,把write方法改为read即可。 - 构造方法:传入OutputStream或InputStream对象。
public ObjectOutputStream(OutputStream out) throws IOException
public ObjectInputStream(InputStream in) throws IOException
这里的流通常是文件操作流:FileInputStream或FileOutputStream。
注意
- 一个类对象如果想被实例化,则必须里面的属性都是Serializable接口的子类。
- 后面Java官方会逐渐去掉序列化这一操作,所以在开发中要减少序列化的使用。
14.2 对象属性部分序列化:transient关键字
如果类中的某一个属性不想被序列化(比如user类的password属性,我们不希望反序列化的时候被看见),则可以使用transient关键字修饰它。
注意:
1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。当系统再次访问这个static变量时,其实是访问的JVM内存中保存的值而不是反序列化的结果。
范例:
package com.kkb.xzk;
import java.io.*;
public class Demo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:" + File.separator + "a.txt")));
try(oos){
oos.writeObject(new Person("林强", 22));
}catch(IOException e){
e.printStackTrace();
}
Person.company = "Oracle"; // 在序列化之后改变静态属性的值
Person p = (Person)new ObjectInputStream(new FileInputStream(new File("D:" + File.separator + "a.txt"))).readObject();
System.out.println(p);
}
static class Person implements Serializable{
private String name;
private int age;
private static String company = "开课吧"; // 静态属性不会被序列化
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age + '\'' +
", company = " + company +
'}';
}
}
}
输出结果:Person{name='林强', age=22', company = Oracle}
14.3 自定义序列化
只要在要序列化的类中自行实现如下两个私有方法
即可实现自定义私有化:
private void writeObject(ObjectOutputStream out) throws IOException{}
private void readObject(ObjectInputStream in) throws IOException{}
范例:实现自定义对象序列化。下面这个范例中自定义了对象序列化,可以自己实现哪些属性想要序列化,哪些不被序列化。注意read和write属性的顺序要一致。
package com.kkb.xzk;
import java.io.*;
public class Demo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:" + File.separator + "a.txt")));
try(oos){
oos.writeObject(new Person("林强", 22));
}catch(IOException e){
e.printStackTrace();
}
Person.company = "Oracle";
Person p = (Person)new ObjectInputStream(new FileInputStream(new File("D:" + File.separator + "a.txt"))).readObject();
System.out.println(p);
}
static class Person implements Serializable{
private String name;
private int age;
private static String company = "开课吧";
// 自定义对象序列化
private void writeObject(ObjectOutputStream out) throws IOException{
out.writeObject(name);
out.writeObject(company);
}
// 自定义对象序列化
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
this.name = (String)in.readObject();
company = (String)in.readObject();
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age + '\'' +
", company = " + company +
'}';
}
}
}
14.4 Externalizable接口实现部分序列化
Externalizable接口继承自Serializable接口。实现这个接口需要自己实现writeExternal和readExternal两个抽象方法。定义这两个抽象方法和上面定义两个私有的readObject、writeObject方法是一致的,把自己想要序列化的属性实现出来即可。可以理解为:Externalizable接口设计的目的就是抽象出来readObject和writeObject两个私有方法。
14.5 Serializable VS. Externalizable对比
区别 | Serializable | Externalizable |
实现复杂度 | 实现简单,Java有内建支持 | 实现复杂,由开发人员自己完成 |
执行效率 | 所有属性都要保存,效率较低 | 开发人员决定保存哪些属性,可能会提高效率 |
保存信息 | 保存时占用空间大 | 部分信息存储,可能会减少占用空间 |
使用频率 | 高 | 较少 |
15 try-with-resource
在JDK 1.7之前,如果想要打开一个资源并且正确关闭它,需要进行如下的操作:
public static void main(String[] args) {
OutputStream fr = null;
try{
fr = new FileOutputStream(new File("C:" + File.separator + "a.txt"));
fr.write(65);
}catch (IOException e){
e.printStackTrace();
}finally{
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
非常麻烦。从JDK1.7开始,可以直接在try语句后加(资源创建操作)
来创建资源并实现资源的的自动关闭。能放在try()括号内的对象必须实现Closeable或AutoCloseable接口。
public static void main(String[] args) {
try(OutputStream fr = new FileOutputStream(new File("C:" + File.separator + "a.txt"))){
fr.write(65);
}catch (IOException e){
e.printStackTrace();
}
}
但上述方法还存在不合理的地方。加入fr在后面还要继续使用,或者fr是从别的地方传入进来的,则上述方法就不能用了。从JDK9版本对其进行了优化,允许在小括号内直接写多个已经在外部创建的对象(用;
隔开)
public static void main(String[] args) throws FileNotFoundException {
OutputStream fr = new FileOutputStream(new File("C:" + File.separator + "a.txt"));
PrintWriter pw = new PrintWriter("C:" + File.separator + "a.txt");
try(fr; pw){
fr.write(65);
}catch (IOException e){
e.printStackTrace();
}
}
16 文件操作范例
要求:由用户输入(1)文件名称;(2)要保存的内容,将内容写入到文件里。
- 定义一个文件操作的服务接口
package cn.mldn.demo.service;
import java.io.File;
public interface IFileService {
public static final String BASE_DIR = "E:" + File.separator + "mldn";
/**
* 保存文件
* @return true: 保存成功 false: 保存失败
*/
public boolean save();
}
- 在输入操作类InputUtil类中增加新的输入操作方法,用于接收用户输入的文件名和存储内容。
package cn.mldn.demo.serviceimpl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class InputUtil{
public static final BufferedReader INPUT = new BufferedReader(new InputStreamReader(System.in));
public static String getInfo(String prompt) {
boolean flag = true;
String str = null;
while(flag) {
System.out.print(prompt);
try {
str = INPUT.readLine().trim();
if(!"".equals(str)) {
flag = false;
}else {
System.err.println("输入不合法,请重新输入!");
}
} catch (IOException e) {
e.printStackTrace();
}
}
return str;
}
}
- 实现IFileService子类,实现文件操作
package cn.mldn.demo.serviceimpl;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import cn.mldn.demo.service.*;
public class FileService implements IFileService{
private static File dir = null;
private String name = null;
private String content = null;
static {
dir = new File(IFileService.BASE_DIR);
if(!dir.exists()) {
dir.mkdirs();
}
}
public FileService() {
this.name = InputUtil.getInfo("请输入文件名称:");
this.content = InputUtil.getInfo("请输入文件内容:");
}
public boolean save() {
PrintWriter pw = null;
try {
File toFile = new File(dir, this.name);
pw = new PrintWriter(new FileWriter(toFile, false));
pw.print(this.content);
return true;
}catch(Exception e) {
return false;
}finally {
pw.close();
}
}
}
- 通过工厂类获得文件操作实例对象
package cn.mldn.demo.factory;
import cn.mldn.demo.service.IFileService;
import cn.mldn.demo.serviceimpl.FileService;
public class ServiceFactory {
private ServiceFactory() {};
public static IFileService getIFileServiceInstance() {
return new FileService();
}
}
- 主类测试代码
import cn.mldn.demo.factory.ServiceFactory;
import cn.mldn.demo.service.IFileService;
public class JavaIODemo {
public static void main(String[] args) {
IFileService ifs = ServiceFactory.getIFileServiceInstance();
System.out.println(ifs.save()? "文件保存成功" : "文件保存失败");
}
}
17 快递E栈实现心得
- 当对象流中没有数据时,程序却尝试读取数据,会报EOFException;而字节流就不会出现这种情况,字节流会返回-1。因此可以通过异常捕获的方式判断是否已经读到文件的末端。或者在写入文件时最后写入一个null,通过读取到null判断是否读到结尾。
- 实例化ois时要注意,传入的FileInputStream在实例化时要保证它的参数File f是存在的且不是目录。(即f.exists()==true且!f.isDirectory())。
- 向file中写入数据时一定要保证f的父目录是存在的。如不存在需要执行
file.getParentFile().mkdirs();
- OutputStream中的write方法虽然默认是覆盖写的方式,但是在流关闭之前写入的内容都直接追加到文件中。比如在字节写入流关闭之前分三次写入65,66,67,则最后文件中就保存了这三个数字,而不是只保存了一个67。覆盖写是指流创建之后第一次写操作会完全覆盖掉原文件的内容。对于ObjectOutputStream也是一样,在流关闭之前写入的对象都会写入到文件里。
- 【重要大坑】对象输出流ObjectOutputStream创建的时候会自动把文件截成4个字节,这样除非oos写入对象,否则后边你不管怎么读取都读不到对象,并且发生EOF异常。建议:把对象读取和写入操作分开。如果要先读取对象再写入,一定要先实例化ois,之后再实例化oos写入。