Java IO

Java输入输出

File类

File能新建,删除,重命名文件和目录,但是File不能访问文件内容本身,如果需要访问文件内容本身,需要使用输入输出流。

访问文件和目录

File类可以使用文件路径字符串创建File实例,该文件路径可以是绝对路径,也可以是相对路径。

一旦创建了File对象后,就可以调用File对象的方法来访问。

  1. 访问文件名相关的方法

    • String getName()
    • String getPath()
    • File getAbsoluteFile()
    • String getAbsolutePath()
    • String getParent()
    • boolean renameTo(File newName)
  2. 文件检测相关方法

    • boolean exists()
    • boolean canWrite()
    • boolean canRead()
    • boolean isFile()
    • boolean isDirectory()
    • boolean isAbsolute()
  3. 获取常规文件信息

    • long lastModified()
    • long length()
  4. 文件操作相关方法

    • boolean createNewFile()
    • boolean delete()
    • void deletOnExit()
  5. 目录操作相关方法

    • boolean mkdir()
    • String[] list()
    • File[] listFiles()
    • static File[] listRoots()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class FileTest
{
public static void main(String[] args)
throws IOException
{
// 以当前路径来创建一个File对象
File file = new File(".");
// 直接获取文件名,输出一点
System.out.println(file.getName());
// 获取相对路径的父路径可能出错,下面代码输出null
System.out.println(file.getParent());
// 获取绝对路径
System.out.println(file.getAbsoluteFile());
// 获取上一级路径
System.out.println(file.getAbsoluteFile().getParent());
// 在当前路径下创建一个临时文件
File tmpFile = File.createTempFile("aaa", ".txt", file);
// 指定当JVM退出时删除该文件
tmpFile.deleteOnExit();
// 以系统当前时间作为新文件名来创建新文件
File newFile = new File(System.currentTimeMillis() + "");
System.out.println("newFile对象是否存在:" + newFile.exists());
// 以指定newFile对象来创建一个文件
newFile.createNewFile();
// 以newFile对象来创建一个目录,因为newFile已经存在,
// 所以下面方法返回false,即无法创建该目录
newFile.mkdir();
// 使用list()方法来列出当前路径下的所有文件和路径
String[] fileList = file.list();
System.out.println("====当前路径下所有文件和路径如下====");
for (String fileName : fileList)
{
System.out.println(fileName);
}
// listRoots()静态方法列出所有的磁盘根路径。
File[] roots = File.listRoots();
System.out.println("====系统所有根路径如下====");
for (File root : roots)
{
System.out.println(root);
}
}
}

文件过滤器

File类的list()方法可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。

FilenameFileter接口包含一个accept(File dir, String name)方法,该放阿飞将依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或者文件。

理解Java的IO流

在Java中把不同的输入输出源(键盘,文件,网络连接等)抽象为流,通过流的方式允许Java使用相同的方式来访问不同输入输出源。

流的分类

  1. 输入流和输出流
    输入流主要由InputStream和Reader作为基类,而输出流则主要有OutputStream和Writer作为基类。它们都是一些抽象基类无法直接创建实例。

  2. 字节流和字符流
    字节流和字符流的用法几乎完全一样,区别在于操作的数据单元不同,字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16为的字符。

字节流主要有InputStream和OutputStream作为基类,而字符流主要有Reader和Writer作为基类。

  1. 节点流和处理流(装饰器设计模式)

通过使用处理流来包装不同的节点流,即可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出功能,因此处理流也被称为包装流。

流的概念模型

  • InputStream/Reader: 所有输入流的基类
  • OutputStream/Writer:所有输出流的基类

水管,输入流—>从水管中取水滴
输出流—> 将水滴放入水管

处理流的功能:

  • 性能的提高:主要以增加缓冲的方式提高输入/输出效率
  • 便捷的操作:提供一系列方法依次输入/输出大批量的数据,而不是当个水滴。

字节流和字符流

InputStream和Reader

抽象基类,本身并不能创建实例来执行输入。

InputStream方法:

  • int read()
  • int read(byte[] b)
  • int read(byte[] b, int off, int len)

Reader方法:

  • int read()
  • int read(char[] cbuf)
  • int read(char[] cbuf, int off, int len)

用于读取文件的输入流: FileInputStream和FileReader,它们都是节点流,会直接和指定文件关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FileInputStreamTest
{
public static void main(String[] args) throws IOException
{
// 创建字节输入流
FileInputStream fis = new FileInputStream(
"FileInputStreamTest.java");
// 创建一个长度为1024的“竹筒”
byte[] bbuf = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = fis.read(bbuf)) > 0 )
{
// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
System.out.print(new String(bbuf , 0 , hasRead ));
}
// 关闭文件输入流,放在finally块里更安全
fis.close();
}
}

OutputStream和Writer

OutputStream和Writer共有的方法:

  • void write(int c), c可以代表字节,也可以代表字符
  • void write(byte[]/char[] buf)
  • void write(bype[]/char[] buf, int off, int len)

Write另外的两个方法:

  • void write(String str)
  • void write(String str, int off, int len)

使用FileOutStream输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class FileOutputStreamTest
{
public static void main(String[] args)
{
try(
// 创建字节输入流
FileInputStream fis = new FileInputStream(
"FileOutputStreamTest.java");
// 创建字节输出流
FileOutputStream fos = new FileOutputStream("newFile.txt"))
{
byte[] bbuf = new byte[32];
int hasRead = 0;
// 循环从输入流中取出数据
while ((hasRead = fis.read(bbuf)) > 0 )
{
// 每读取一次,即写入文件输出流,读了多少,就写多少。
fos.write(bbuf , 0 , hasRead);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

关闭输出流除了可以保证流的物理资源回收以外,还可以将输出缓冲区中的数据flush到屋里节点。

如果希望直接输入字符串内容,则使用FileWriter会有更好的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FileWriterTest
{
public static void main(String[] args)
{
try(
FileWriter fw = new FileWriter("poem.txt"))
{
fw.write("关闭输出流除了可以保证流的物理资源回收以外\n");
fw.write("还可以将输出缓冲区中的数据flush到屋里节点。\n"); }
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

输入/输出流体系

处理流的用法

处理流可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入输出方法。

使用处理流的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入输出功能,让节点流与底层的IO设备,文件交互。

下面程序使用PrintStream处理流来包装OutputStream,使用处理流后输出将更加方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PrintStreamTest
{
public static void main(String[] args)
{
try(
FileOutputStream fos = new FileOutputStream("test.txt");
PrintStream ps = new PrintStream(fos))
{
// 使用PrintStream执行输出
ps.println("普通字符串");
// 直接使用PrintStream输出对象
ps.println(new PrintStreamTest());
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

PrintStream类的输出功能非常强大,如果需要输出文本内容,都应该将输出流包装成PrintStream后进行输出

关闭高层流时,系统会自动关闭被包装的节点流。

输入/输出流体系

分类 字节输入流 字节输出流 字节输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputSteam ByteArrayOutputSteam CharArrayReader CharArrayWriter
访问管道 PipedInputSteam PipedOutputStream PipedReader PipedWriter
访问字符串 - - StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 - - InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
抽象基类 FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流 - PrintStream - PrintWriter
推回输入流 PushbackInputSteam - PushbackReader
特殊流 DataInputStream DataOutputStream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class StringNodeTest
{
public static void main(String[] args)
{
String src = "从明天起,做一个幸福的人\n"
+ "喂马,劈柴,周游世界\n"
+ "从明天起,关心粮食和蔬菜\n"
+ "我有一所房子,面朝大海,春暖花开\n"
+ "从明天起,和每一个亲人通信\n"
+ "告诉他们我的幸福\n";
char[] buffer = new char[32];
int hasRead = 0;
try(
StringReader sr = new StringReader(src))
{
// 采用循环读取的访问读取字符串
while((hasRead = sr.read(buffer)) > 0)
{
System.out.print(new String(buffer ,0 , hasRead));
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
try(
// 创建StringWriter时,实际上以一个StringBuffer作为输出节点
// 下面指定的20就是StringBuffer的初始长度
StringWriter sw = new StringWriter())
{
// 调用StringWriter的方法执行输出
sw.write("有一个美丽的新世界,\n");
sw.write("她在远方等我,\n");
sw.write("哪里有天真的孩子,\n");
sw.write("还有姑娘的酒窝\n");
System.out.println("----下面是sw的字符串节点里的内容----");
// 使用toString()方法返回StringWriter的字符串节点的内容
System.out.println(sw.toString());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}

转换流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class KeyinTest
{
public static void main(String[] args)
{
try(
// 将Sytem.in对象转换成Reader对象
InputStreamReader reader = new InputStreamReader(System.in);
//将普通Reader包装成BufferedReader
BufferedReader br = new BufferedReader(reader))
{
String buffer = null;
//采用循环方式来一行一行的读取
while ((buffer = br.readLine()) != null)
{
//如果读取的字符串为"exit",程序退出
if (buffer.equals("exit"))
{
System.exit(1);
}
//打印读取的内容
System.out.println("输入内容为:" + buffer);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

由于Buffered具有一个readLine()方法,可以非常方便地一次读入一行内容,所有经常包读取文件内容的输入包装为BufferedReader,用来方便地读取输入流的文本内容

推回输入流

PushbackInputStream和PushbackReader

  • void unread(byte[]/char[] buf):将一个字节/字符数组内容推回到缓冲区里,从而允许重复度去刚刚读取的数据
  • void unread(byte[]/char[] buf, int off, int len)
  • void unread(int b)

推回的数据存储在推回缓冲区里,read()方法先会从推回缓冲区里读,没有数据才从原输入流中读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class PushbackTest
{
public static void main(String[] args)
{
try(
// 创建一个PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pr = new PushbackReader(new FileReader(
"PushbackTest.java") , 64))
{
char[] buf = new char[32];
// 用以保存上次读取的字符串内容
String lastContent = "";
int hasRead = 0;
// 循环读取文件内容
while ((hasRead = pr.read(buf)) > 0)
{
// 将读取的内容转换成字符串
String content = new String(buf , 0 , hasRead);
int targetIndex = 0;
// 将上次读取的字符串和本次读取的字符串拼起来,
// 查看是否包含目标字符串, 如果包含目标字符串
if ((targetIndex = (lastContent + content)
.indexOf("new PushbackReader")) > 0)
{
// 将本次内容和上次内容一起推回缓冲区
pr.unread((lastContent + content).toCharArray());
// 指定读取前面len个字符
int len = targetIndex > 32 ? 32 : targetIndex;
// 再次读取指定长度的内容(就是目标字符串之前的内容)
pr.read(buf , 0 , len);
// 打印读取的内容
System.out.print(new String(buf , 0 ,len));
System.exit(0);
}
else
{
// 打印上次读取的内容
System.out.print(lastContent);
// 将本次内容设为上次读取的内容
lastContent = content;
}
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

重定向标准输入/输出

Java的标准输入输出分别通过System.in和System.out来代表,默认情况分别是键盘和显示器。但可以通过下面方法重定向标准输入输出。

  • static void setErr(PrintStream err),重定向标准错误输出流
  • static void setIn(InputStream in),重定向标准输入流
  • static void setOut(PrintStream out),重定向标准输出流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class RedirectOut
{
public static void main(String[] args)
{
try(
// 一次性创建PrintStream输出流
PrintStream ps = new PrintStream(new FileOutputStream("out.txt")))
{
// 将标准输出重定向到ps输出流
System.setOut(ps);
// 向标准输出输出一个字符串
System.out.println("普通字符串");
// 向标准输出输出一个对象
System.out.println(new RedirectOut());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
```

```java
public class RedirectIn
{
public static void main(String[] args)
{
try(
FileInputStream fis = new FileInputStream("RedirectIn.java"))
{
// 将标准输入重定向到fis输入流
System.setIn(fis);
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
// 增加下面一行将只把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while(sc.hasNext())
{
// 输出输入项
System.out.println("键盘输入的内容是:" + sc.next());
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}

Java虚拟机读取其他进程的数据

由Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一个Process对象,代表有该Java程序启动的子进程。Process类提供了如下三个方法,用于让程序和其子进程进行通信。

  • InputStream getRerrorStream()
  • InputSteam getInputStream()
  • OutputStream getOutputStream()

如果要让子进程读取程序中的数据,应该是输出流,而不是输入流。要站在主java程序的角度看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ReadFromProcess
{
public static void main(String[] args)
throws IOException
{
// 运行javac命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("javac");
try(
// 以p进程的错误流创建BufferedReader对象
// 这个错误流对本程序是输入流,对p进程则是输出流
BufferedReader br = new BufferedReader(new
InputStreamReader(p.getErrorStream())))
{
String buff = null;
// 采取循环方式来读取p进程的错误输出
while((buff = br.readLine()) != null)
{
System.out.println(buff);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class WriteToProcess
{
public static void main(String[] args)
throws IOException
{
// 运行java ReadStandard命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("java ReadStandard");
try(
// 以p进程的输出流创建PrintStream对象
// 这个输出流对本程序是输出流,对p进程则是输入流
PrintStream ps = new PrintStream(p.getOutputStream()))
{
// 向ReadStandard程序写入内容,这些内容将被ReadStandard读取
ps.println("普通字符串");
ps.println(new WriteToProcess());
}
}
}
// 定义一个ReadStandard类,该类可以接受标准输入,
// 并将标准输入写入out.txt文件。
class ReadStandard
{
public static void main(String[] args)
{
try(
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
PrintStream ps = new PrintStream(
new FileOutputStream("out.txt")))
{
// 增加下面一行将只把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while(sc.hasNext())
{
// 输出输入项
ps.println("键盘输入的内容是:" + sc.next());
}
}
catch(IOException ioe)
{
ioe.printStackTrace();
}
}
}

RandomAccessFile

RandomAccessFile是功能最丰富的文件内容访问类,但是只能读写文件,不能读写其他IO节点。

与普通输入输出流不同的是,RandomAccessFile支持随机访问的方式,程序可以直接跳转到文件的任何位置读写数据。如果只需要访问文件部分内容,使用RandomAccessFile将是更好的选择。

RandomAccessFile允许自由定位文件记录指针,以及相关操作。

  • long getFilePointer
  • void seek(long pos),移动文件记录指针到pos位置

RandomAccessFile同时包含InputStream的三个read()方法,以及OutputStream的三个write()方法

Random这里指的是任意访问,而不是随机访问。四个访问模式:

  • r
  • rw
  • rws
  • rwd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RandomAccessFileTest
{
public static void main(String[] args)
{
try(
RandomAccessFile raf = new RandomAccessFile(
"RandomAccessFileTest.java" , "r"))
{
// 获取RandomAccessFile对象文件指针的位置,初始位置是0
System.out.println("RandomAccessFile的文件指针的初始位置:"
+ raf.getFilePointer());
// 移动raf的文件记录指针的位置
raf.seek(300);
byte[] bbuf = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = raf.read(bbuf)) > 0 )
{
// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
System.out.print(new String(bbuf , 0 , hasRead ));
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AppendContent
{
public static void main(String[] args)
{
try(
//以读、写方式打开一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile("out.txt" , "rw"))
{
//将记录指针移动到out.txt文件的最后
raf.seek(raf.length());
raf.write("追加的内容!\r\n".getBytes());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class InsertContent
{
public static void insert(String fileName , long pos
, String insertContent) throws IOException
{
File tmp = File.createTempFile("tmp" , null);
tmp.deleteOnExit();
try(
RandomAccessFile raf = new RandomAccessFile(fileName , "rw");
// 创建一个临时文件来保存插入点后的数据
FileOutputStream tmpOut = new FileOutputStream(tmp);
FileInputStream tmpIn = new FileInputStream(tmp))
{
raf.seek(pos);
// ------下面代码将插入点后的内容读入临时文件中保存------
byte[] bbuf = new byte[64];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环方式读取插入点后的数据
while ((hasRead = raf.read(bbuf)) > 0 )
{
// 将读取的数据写入临时文件
tmpOut.write(bbuf , 0 , hasRead);
}
// ----------下面代码插入内容----------
// 把文件记录指针重新定位到pos位置
raf.seek(pos);
// 追加需要插入的内容
raf.write(insertContent.getBytes());
// 追加临时文件中的内容
while ((hasRead = tmpIn.read(bbuf)) > 0 )
{
raf.write(bbuf , 0 , hasRead);
}
}
}
public static void main(String[] args)
throws IOException
{
insert("InsertContent.java" , 45 , "插入的内容\r\n");
}
}

对象序列化

序列化的含义和意义

对象序列话的目标是将对象保存到磁盘中,或者允许在网络中直接传输对象。对象序列化机制允许把内容的Java对象转换成平台无关的二进制流,程序也可以通过反序列化恢复该Java对象。

如果需要让某个对象支持序列化,必须实现如下两个借口之一:

  • Serializable
  • Externalizable

Java很多类已经实现Serializable,该借口是一个标记借口,实现该接口无需实现任何方法,只是表明是可序列化的。

所有可能在网络上传输或需要存储到磁盘的的类都应该是可以序列化的,否则会出现异常。

使用对象流实现序列化

ObjectOutputStream,处理流,必须建立在其他节点流的基础上。

1
2
3
ObjectOutputSream oos = new ObjectOutputSteam(new FileOutputStream("test.txt"))

oos.writeObject(per);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}

}

ObjectInputSteam,处理流,需要建立在其他节点流基础上。

1
2
3
ObjectInputStream ois = new ObjectInputStream(new FileInputSteam("test.txt"))

Person p = (Person) ois.readObject();

反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供Java对象的类class文件。另外,反序列化无需通过构造器来初始化Java对象。

如果使用序列化机制向文件写入多个java对象,使用反序列化机制恢复对象时必须按照实际写入的顺序读取。

当一个可序列化类有多个父类时(直接和间接),这些父类要么有无参数的构造器,要么也是可序列化的。如果父类是不可序列化的,只是带有无参构造器,则父类中定义的成员变量值不会序列化到二进制流中。

对象引用的序列化

如果某个类的成员变量是另一个类的对象的引用,也就是引用类型,那么这个引用类必须是可序列化,否则拥有该类对象的类也是不可序列化的。

当对某个对象序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果被引用的实例变量也引用了另一个对象,则该对象也会被序列化。这称为递归序列化。

如果多个Student引用同一个Class对象,这时在恢复时,这个Class对象应该只存在一份,而不是独立的两份。为了实现这点,Java序列化采用了一种特殊的序列化算法:

  • 所有保存磁盘中的对象都有一个序列化编号
  • 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未在本次虚拟机中被序列化过,系统才会将该对象转换成字节序列输出。
  • 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是重新序列化该对象。

这意味着某个对象被序列化后,改变其状态(成员变量值),再次序列化,其改变后的状态不会在被序列化到文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class WriteTeacher
{
public static void main(String[] args)
{
try(
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("teacher.txt")))
{
Person per = new Person("孙悟空", 500);
Teacher t1 = new Teacher("唐僧" , per);
Teacher t2 = new Teacher("菩提祖师" , per);
// 依次将四个对象写入输出流
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);
oos.writeObject(t2);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ReadTeacher
{
public static void main(String[] args)
{
try(
// 创建一个ObjectInputStream输出流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("teacher.txt")))
{
// 依次读取ObjectInputStream输入流中的四个对象
Teacher t1 = (Teacher)ois.readObject();
Teacher t2 = (Teacher)ois.readObject();
Person p = (Person)ois.readObject();
Teacher t3 = (Teacher)ois.readObject();
// 输出true
System.out.println("t1的student引用和p是否相同:"
+ (t1.getStudent() == p));
// 输出true
System.out.println("t2的student引用和p是否相同:"
+ (t2.getStudent() == p));
// 输出true
System.out.println("t2和t3是否是同一个对象:"
+ (t2 == t3));
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SerializeMutable
{
public static void main(String[] args)
{

try(
// 创建一个ObjectOutputStream输入流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("mutable.txt"));
// 创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("mutable.txt")))
{
Person per = new Person("孙悟空", 500);
// 系统会per对象转换字节序列并输出
oos.writeObject(per);
// 改变per对象的name Field
per.setName("猪八戒");
// 系统只是输出序列化编号,所以改变后的name不会被序列化
oos.writeObject(per);
Person p1 = (Person)ois.readObject(); //①
Person p2 = (Person)ois.readObject(); //②
// 下面输出true,即反序列化后p1等于p2
System.out.println(p1 == p2);
// 下面依然看到输出"孙悟空",即改变后的Field没有被序列化
System.out.println(p2.getName());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}

自定义序列化

通过在实例变量前使用transient关键字修饰,可以制定Java序列化时无需理会该实例变量。只能用来修饰实例变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}

在序列化和反序列化需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法可以用来实现自定义序列化:

  • private void writeObject(ObjectOutputStream)
  • private void readObject(ObjectInputStream in)
  • private void readObjectNoData()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}

private void writeObject(java.io.ObjectOutputStream out)
throws IOException
{
// 将name Field的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name Field
this.name = ((StringBuffer)in.readObject()).reverse()
.toString();
this.age = in.readInt();
}
}

writeOject()方法存储实例变量的顺序应该和readOject()方法中恢复实例变量的顺序一致,否则将不能正常恢复该java对象。

更彻底的自定义机制,甚至可以在序列化对象时将对象替换为其他对象:
Object writeReplace()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}

// 重写writeReplace方法,程序在序列化该对象之前,先调用该方法
private Object writeReplace()throws ObjectStreamException
{
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ReplaceTest
{
public static void main(String[] args)
{
try(
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("replace.txt"));
// 创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("replace.txt")))
{
Person per = new Person("孙悟空", 500);
// 系统将per对象转换字节序列并输出
oos.writeObject(per);
// 反序列化读取得到的是ArrayList
ArrayList list = (ArrayList)ois.readObject();
System.out.println(list);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}

相对应的另一个可以实现保护性复制整个对象:
Object readResolve()

另一种自定义个序列化机制

Java类实现Externalizable接口,完全由程序员决定存储和恢复对象数据。

  • readExternal(ObjectInput in)
  • void writeExternal(ObjectOutput out)

实际上采用Externalizable接口方式实现的序列化与前面的介绍的自定义序列化非常相似,只是这里是强制的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Person
implements java.io.Externalizable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}

public void writeExternal(java.io.ObjectOutput out)
throws IOException
{
// 将name Field的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
public void readExternal(java.io.ObjectInput in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name Field
this.name = ((StringBuffer)in.readObject()).reverse().toString();
this.age = in.readInt();
}
}

另外要注意的几点

  • 对象的实例变量(包括基本类型,数组,其他对象的引用)都会被序列化,方法,类变量,transitent遍历不会被序列化。
  • 反序列化必须有序列化对象的class文件
  • 当通过文件网络来读取序列化后的对象时,必须按照实际的写入的顺序读取。
  • Java的class文件的版本,可以通过为序列化类提供一个private static final的serialVersionUID值,用于表示该Java类的序列化版本,也就是说一个类升级后,只要它的serialVersionUI值不变,序列化机制也会把它们当做同一个序列化版本。

NIO

前面的面向流的IO都是阻塞式的,且都是通过字节移动来处理(即使不直接处理字节流,其底层的实现还是依赖字节处理),也就是面向流的输入输出系统一次只能处理一个字节,通常效率不高。

JDK1.4开始Java提供了一系列的改进的输入输出处理的新功能,统称为NIO。

Java NIO概述

NIO和传统IO目的相同,都是用于输入输出,但NIO采用了不同的方式来处理输入输出,NIO采用内存映射文件的方式来处理输入输出,将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了,效率比传统IO快很多。

Channel和Buffer是NIO中的两个核心对象,Channel是对传统IO的模拟,NIO所有数据都需要通过Channel传输。Channel与传统IO Stream最大的区别是提供了一个map()方法,通过该方法可以直接将一块数据映射到内存中。如果说传统的输入输出是面向流的处理,则NIO则是面向块的处理。

Buffer可以理解为一个容器,本质上是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中。

除了Channel和Buffer之外,NIO还提供了用于将Unicode字符映射成字节系列以及逆映射操作的Charset类,也提供了用于支持非阻塞式IO的Selector类。

使用Buffer

从内部结构上来看,Buffer就像一个数组,它可以保存多个类型相同的数据。Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层自己数组上进行get/set操作。另外对应基本类型都有相应的Buffer类(除boolean):CharBuffer,ShortBuffer,IntBuffer…

除ByteBuffer以外,都采用相同或相似的方法来管理数据。Buffer类都没有提供构造器,使用下面方法来得到Buffer对象:

static XxxBuffer allocate(int capacity)

用的比较多的是ByteBuffer和CharBuffer。ByteBuffer还有一个子类:MappedByteBuffer,用于表示Channel将磁盘文件的部分或全部映射到内存中后得到的结果,通常MappedByteBuffer对象有Channel的map()方法返回。

重要概念:

  • capacity:该Buffer的最大数据容量,创建后不能改变
  • limit:第一个不应该被读出或写入的缓冲区的位置。也就是说,limit后的数据既不可被读,也不可被写。
  • position:下一个可被读出或写入的位置,也就是前面是已经读写的区域。
  • mark:允许直接将position定位到mark处

0<=mark<=position<=limit<=capacity

Buffer的主要作用就是装入数据,然后输出数据。

开始时Buffer的position为0,limit为capacity,通过put()方法向Buffer放入一些数据,每放入一些数据,Buffer的position相应的向后移动一些位置。

当Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在位置,并将position设为0。也就是Buffer调用flip()方法之后,Buffer为输出数据做好准备,

当Buffer输出结束后,Buffer调用clear()方法,仅仅将position置为0,将limit置为capacity,这样为再次向Buffer中装入数据做好准备。

flip(), 为从Buffer取出数据做好准备
clear(), 为再次向Buffer中装入数据做好准备

其他方法:

  • int capacity()
  • boolean hasRemaining()
  • int limit()
  • Buffer limit()
  • Buffer mark()
  • int position()
  • Buffer position(int newPs)
  • int remaining()
  • Buffer reset():将position转到mark所在位置
  • Buffer rewind():将位置设置为0,取消设置的mark

另外还有put(), get()方法,既支持对单个数据的访问,也支持对批量数据的访问(以数组为参数),方式:

  • 相对,从当前position处开始,并移动position
  • 绝对,直接更具索引读写,不影响position值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class BufferTest
{
public static void main(String[] args)
{
// 创建Buffer
CharBuffer buff = CharBuffer.allocate(8); //①
System.out.println("capacity: " + buff.capacity());
System.out.println("limit: " + buff.limit());
System.out.println("position: " + buff.position());
// 放入元素
buff.put('a');
buff.put('b');
buff.put('c'); //②
System.out.println("加入三个元素后,position = "
+ buff.position());
// 调用flip()方法
buff.flip(); //③
System.out.println("执行flip()后,limit = " + buff.limit());
System.out.println("position = " + buff.position());
// 取出第一个元素
System.out.println("第一个元素(position=0):" + buff.get()); // ④
System.out.println("取出一个元素后,position = "
+ buff.position());
// 调用clear方法
buff.clear(); //⑤
System.out.println("执行clear()后,limit = " + buff.limit());
System.out.println("执行clear()后,position = "
+ buff.position());
System.out.println("执行clear()后,缓冲区内容并没有被清除:"
+ "第三个元素为:" + buff.get(2)); // ⑥
System.out.println("执行绝对读取后,position = "
+ buff.position());
}
}

通过allocate()方法创建的Buffer对象是普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来直接创建Buffer,成本更高,但读取效率更高,一般只用于长生存期的Buffer。另外只有ByteBuffer提供直接创建。

使用Channel

Channel类似与传统的流对象,但与传统的流对象有两个主要区别

  • Channel可以直接将指定文件的部分或全部映射成Buffer。
  • 程序不能直接访问Channel中的数据,包括读取写入都不行,Channel只能与Buffer进行交互。也就是说,如果要从Channel中取得数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据;如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channel中。

Java为Channel接口提供了DatagramChannel, FileChannel, Pipe.SinkChannel, Pipe.SourceChannel, SelectableChannel, ServerSocketChannel, SocketChannel等实现类。下面主要介绍FileChannel。

所有的Channel都不应该通过构造器直接创建,而是通过传统的节点流的getChannel()方法来返回对应的Channel。

最常用方法:

  • map(),将Channel对应的部分或全部数据映射为ByteBuffer
  • read()
  • write()

MappedByteBuffer map(FileChannel.MapMode mode, long position, long size), 模式分别有只读,读写等,第二第三个参数控制将Channel的哪些数据映射为ByteBuffer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class FileChannelTest
{
public static void main(String[] args)
{
File f = new File("FileChannelTest.java");
try(
// 创建FileInputStream,以该文件输入流创建FileChannel
FileChannel inChannel = new FileInputStream(f).getChannel();
// 以文件输出流创建FileBuffer,用以控制输出
FileChannel outChannel = new FileOutputStream("a.txt")
.getChannel())
{
// 将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel
.MapMode.READ_ONLY , 0 , f.length()); // ①
// 使用GBK的字符集来创建解码器
Charset charset = Charset.forName("GBK");
// 直接将buffer里的数据全部输出
outChannel.write(buffer); // ②
// 再次调用buffer的clear()方法,复原limit、position的位置
buffer.clear();
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
// CharBuffer的toString方法可以获取对应的字符串
System.out.println(charBuffer);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RandomFileChannelTest
{
public static void main(String[] args)
throws IOException
{
File f = new File("a.txt");
try(
// 创建一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile(f, "rw");
// 获取RandomAccessFile对应的Channel
FileChannel randomChannel = raf.getChannel())
{
// 将Channel中所有数据映射成ByteBuffer
ByteBuffer buffer = randomChannel.map(FileChannel
.MapMode.READ_ONLY, 0 , f.length());
// 把Channel的记录指针移动到最后
randomChannel.position(f.length());
// 将buffer中所有数据输出
randomChannel.write(buffer);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ReadFile
{
public static void main(String[] args)
throws IOException
{
try(
// 创建文件输入流
FileInputStream fis = new FileInputStream("ReadFile.java");
// 创建一个FileChannel
FileChannel fcin = fis.getChannel())
{
// 定义一个ByteBuffer对象,用于重复取水
ByteBuffer bbuff = ByteBuffer.allocate(64);
// 将FileChannel中数据放入ByteBuffer中
while( fcin.read(bbuff) != -1 )
{
// 锁定Buffer的空白区
bbuff.flip();
// 创建Charset对象
Charset charset = Charset.forName("GBK");
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 将ByteBuffer的内容转码
CharBuffer cbuff = decoder.decode(bbuff);
System.out.print(cbuff);
// 将Buffer初始化,为下一次读取数据做准备
bbuff.clear();
}
}
}
}

字符集和Charset

Java提供了Charset来处理字节序列和字符序列之间的转换关系,该类包含了用于创建解码器和编码器的方法。提供了一个avaiableCharsets()来获取当前JDK所支持的字符集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CharsetTest
{
public static void main(String[] args)
{
// 获取Java支持的全部字符集
SortedMap<String,Charset> map = Charset.availableCharsets();
for (String alias : map.keySet())
{
// 输出字符集的别名和对应的Charset对象
System.out.println(alias + "----->"
+ map.get(alias));
}
}
}

一旦直到了字符集的别名后,程序就可以调用Charset的forName方法来创建对应Charset对象:

1
2
Charset cs = Charset.forName("GBK");
Charset cscn = Charset.forName("ISO-8859-1");

然后就可以通过该对象的newDecoder(), newEncode()这两个方法分别返回CharsetDecoder和CharsetEncode对象,代表该Charset的解码器和编码器。

调用CharsetEncoder的encode方法就可以将CharBuffer或String转换成ByteBuffer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CharsetTransform
{
public static void main(String[] args)
throws Exception
{
// 创建简体中文对应的Charset
Charset cn = Charset.forName("GBK");
// 获取cn对象对应的编码器和解码器
CharsetEncoder cnEncoder = cn.newEncoder();
CharsetDecoder cnDecoder = cn.newDecoder();
// 创建一个CharBuffer对象
CharBuffer cbuff = CharBuffer.allocate(8);
cbuff.put('孙');
cbuff.put('悟');
cbuff.put('空');
cbuff.flip();
// 将CharBuffer中的字符序列转换成字节序列
ByteBuffer bbuff = cnEncoder.encode(cbuff);
// 循环访问ByteBuffer中的每个字节
for (int i = 0; i < bbuff.capacity() ; i++)
{
System.out.print(bbuff.get(i) + " ");
}
// 将ByteBuffer的数据解码成字符序列
System.out.println("\n" + cnDecoder.decode(bbuff));
}
}

文件锁

Java 7的NIO.2

Path, Paths和Files核心API

使用FileVistor遍历文件和目录

使用WatchService监控文件变化

访问文件属性