Java IO编程

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) 将数据读取到byte数组中,返回的是读取的长度(通常就是数组b的长度,最后一轮读取可能会少于b的长度),如果返回-1表示已经读取完毕。【最为常用】

  • int read​(byte[] b, int off, int len) 将指定长度(len)的数据读取到byte数组,从数组的off位置开始存储。返回真实的读取长度。(假如 o f f + l e n ≥ b . l e n g t h off + len \ge b.length off+lenb.length,则返回值会小于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();
		}
		
	}
}

思考:

  1. 管道流实现的时候其实有生产者消费者的思想,只有发送方发送了数据(对于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。

注意

  1. 一个类对象如果想被实例化,则必须里面的属性都是Serializable接口的子类。
  2. 后面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对比

区别SerializableExternalizable
实现复杂度实现简单,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)要保存的内容,将内容写入到文件里。

  1. 定义一个文件操作的服务接口
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();
}

  1. 在输入操作类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;
	}
}
  1. 实现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();
		}
	}	
}

  1. 通过工厂类获得文件操作实例对象
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();
	}
}
  1. 主类测试代码
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栈实现心得

  1. 当对象流中没有数据时,程序却尝试读取数据,会报EOFException;而字节流就不会出现这种情况,字节流会返回-1。因此可以通过异常捕获的方式判断是否已经读到文件的末端。或者在写入文件时最后写入一个null,通过读取到null判断是否读到结尾。
  2. 实例化ois时要注意,传入的FileInputStream在实例化时要保证它的参数File f是存在的且不是目录。(即f.exists()==true且!f.isDirectory())。
  3. 向file中写入数据时一定要保证f的父目录是存在的。如不存在需要执行file.getParentFile().mkdirs();
  4. OutputStream中的write方法虽然默认是覆盖写的方式,但是在流关闭之前写入的内容都直接追加到文件中。比如在字节写入流关闭之前分三次写入65,66,67,则最后文件中就保存了这三个数字,而不是只保存了一个67。覆盖写是指流创建之后第一次写操作会完全覆盖掉原文件的内容。对于ObjectOutputStream也是一样,在流关闭之前写入的对象都会写入到文件里。
  5. 【重要大坑】对象输出流ObjectOutputStream创建的时候会自动把文件截成4个字节,这样除非oos写入对象,否则后边你不管怎么读取都读不到对象,并且发生EOF异常。参考:https://blog.csdn.net/cslucifer/article/details/76695101。建议:把对象读取和写入操作分开。如果要先读取对象再写入,一定要先实例化ois,之后再实例化oos写入。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值