java.io包下主要包括输入,输出两种IO流,每种输入,输出流又可分为字节流和字符流,其中字节流以字节为单位来处理输入,输出操作,
而字符流以字符来处理输入,输出操作。
一.File类
1.访问文件和目录
一旦创建了File对象后,就可以调用File对象的方法来访问,File类提供了很多方法来操作文件和目录。
1>访问文件名相关的方法;
2> 文件检测相关的方法;
3>获取常规文件信息;
4> 文件操作相关的方法;
5> 目录操作相关的方法。
import java.io.*;
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);
}
}
}
2.文件过滤器
二.理解Java的IO流
1.流的分类
1> 输入流和输出流
Java的输入流主要由InputStream和Reader作为基类,而输出流则主要由OutputStream和Writer作为基类。它们都是一些抽象基类,无法直接创建实例。
2> 字节流和字符流
字节流主要由InputStream和OutputStream作为基类,而字符流则主要由Reader和Writer作为基类。
3> 节点流和处理流
2.流的概念模型
Java的IO流的40多个类都是从如下4个抽象基类派生的。
InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流;
OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。
三.字节流和字符流
1.InputStream和Reader
import java.io.*;
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();
}
}
java7 改写了所有的IO资源类,它们都实现了AutoCloseable接口,因此可以通过自动关闭资源的try语句类关闭这些IO流。
import java.io.*;
public class FileReaderTest
{
public static void main(String[] args) throws IOException
{
try(
// 创建字符输入流
FileReader fr = new FileReader("FileReaderTest.java"))
{
// 创建一个长度为32的“竹筒”
char[] cbuf = new char[32];
// 用于保存实际读取的字符数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = fr.read(cbuf)) > 0 )
{
// 取出“竹筒”中水滴(字符),将字符数组转换成字符串输入!
System.out.print(new String(cbuf , 0 , hasRead));
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
2.OutputStream和Writer
import java.io.*;
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();
}
}
}
如果希望直接输出字符串内容,则使用Writer会有更好的效果。
import java.io.*;
public class FileWriterTest
{
public static void main(String[] args)
{
try(
FileWriter fw = new FileWriter("poem.txt"))
{
fw.write("锦瑟 - 李商隐\r\n");
fw.write("锦瑟无端五十弦,一弦一柱思华年。\r\n");
fw.write("庄生晓梦迷蝴蝶,望帝春心托杜鹃。\r\n");
fw.write("沧海月明珠有泪,蓝田日暖玉生烟。\r\n");
fw.write("此情可待成追忆,只是当时已惘然。\r\n");
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
四.输入输出流体系
1.处理流的用法
下面程序使用PrintStream处理流来包装OutputStream,使用处理流后的输出流在输出时将更加方便。
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();
}
}
}
上面程序中的两行代码是先定义了一个节点输出流FileOutputStream,然后程序使用PrintStream包装了该节点输出流,最后使用PrintStream输出字符串,输出对象。
2.输入输出流体系
如果进行输入输出的内容是文本内容,则应该考虑使用字符流,如果进行输入输出的内容是二进制内容,则应该考虑使用字节流。
下面示范了使用字符串作为物理节点的字符输入输出流的用法。
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();
}
}
}
3.转换流
输入输出流体系中还提供了两个转换流,用于实现将字节流转换成字符流,其中InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输出流转换成字符输出流。
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 line = null;
// 采用循环方式来一行一行的读取
while ((line = br.readLine()) != null)
{
// 如果读取的字符串为"exit",程序退出
if (line.equals("exit"))
{
System.exit(1);
}
// 打印读取的内容
System.out.println("输入内容为:" + line);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
4.推回输入流
PushbackInputStream和PushbackReader。
下面程序试图找出程序中的“new PushbackReader”字符串,当找到该字符串后,程序只是打印出目标字符串之前的内容
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());
// 重新定义一个长度为targetIndex的char数组
if(targetIndex > 32)
{
buf = new char[targetIndex];
}
// 再次读取指定长度的内容(就是目标字符串之前的内容)
pr.read(buf , 0 , targetIndex);
// 打印读取的内容
System.out.print(new String(buf , 0 ,targetIndex));
System.exit(0);
}
else
{
// 打印上次读取的内容
System.out.print(lastContent);
// 将本次内容设为上次读取的内容
lastContent = content;
}
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
五.重定向标准输入输出
在System类里提供了如下三个重定向标准输入输出的方法。
static void setErr(PrintStream err):重定向标准错误输出
static void setIn(PrintStream in):重定向标准输入流
static void setOut(PrintStream out):重定向标准输出流。
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虚拟机读写其他进程的数据
使用Runtime对象的exec方法可以运行平台上的其他程序,该方法产生一个process对象,process对象代表由该Java程序启动的子进程。Process类提供了如下三个方法,用于让程序和其子进程进行通信。
InputStream getErrorStream() 获取子进程的错误流;
InputStream getInputStream() 获取子进程的输入流;
OutputStream getOutputStream() 获取子进程的输出流。
public class ReadFromProcess
{
public static void main(String[] args)
throws IOException
{
// 运行javac命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("ls");
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);
}
}
}
}
结果:
Exception in thread "main" java.io.IOException: Cannot run program "pwd": CreateProcess error=2, 系统找不到指定的文件。
at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1128)
at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1071)
at java.base/java.lang.Runtime.exec(Runtime.java:589)
at java.base/java.lang.Runtime.exec(Runtime.java:413)
at java.base/java.lang.Runtime.exec(Runtime.java:310)
at ReadFromProcess.main(ReadFromProcess.java:11)
Caused by: java.io.IOException: CreateProcess error=2, 系统找不到指定的文件。
at java.base/java.lang.ProcessImpl.create(Native Method)
at java.base/java.lang.ProcessImpl.<init>(ProcessImpl.java:483)
at java.base/java.lang.ProcessImpl.start(ProcessImpl.java:158)
at java.base/java.lang.ProcessBuilder.start(ProcessBuilder.java:1107)
... 5 more
七.RandomAccessFile
RandomAccessFile是Java输入输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问问价内容,既可以读取文件内容,也可以向文件输出数据,与普通的输入输出流不同的是,RandomAccessFile支持随机访问的方式,程序可以直接跳转到文件的任意地方来读写数据。
所以多线程断点的网络下载工具,就可以通过RandomAccessFile类来实现的。
八.对象序列化
对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制允许吧内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流,都可以将这种二进制流恢复成原来的Java对象。
1.序列化的含义和意义
对象的序列化(Serialize)指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该Java对象。
如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的(serializable),为了让某个类是可序列化的,该类必须实现如下两个接口之一:
Serializable
Externalizable
Java的很多类已经实现了Serializable,该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的。
2.使用对象流实现序列化
一旦某个类实现了Serializable接口,该类的对象就是可序列化的,具体如下两个步骤:
1> 创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上
//创建个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputSteam(“object.txt”));
2>调用ObjectOutputStream对象的writeObject()方法输出可序列化对象,
//将一个Person对象输出到输出流中
oos.writeObject(per);
//标识该类的对象是可序列化的
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;
}
}
public class WriteObject
{
public static void main(String[] args)
{
try(
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("object.txt")))
{
Person per = new Person("孙悟空", 500);
// 将per对象写入输出流
oos.writeObject(per);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
运行上面程序,将会看到生成一个object.txt文件,该文件的内容就是Person对象。
如果希望从二进制中恢复Java对象,则需要使用反序列化的步骤如下:
1> 创建一个ObjectInputStream输入流,这个输入流是一个处理流,所以必须建立在其他节点流的基础之上
//创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“object.txt”));
2> 调用ObjectInputStream对象的readObject方法读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将对象强制类型转换成其真实的类型
//从输入流中读取一个Java对象,并将其强制类型转换为Person类
Person p = (Person) ois.readObject();
public class ReadObject
{
public static void main(String[] args)
{
try(
// 创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("object.txt")))
{
//将obj中的二进制流恢复java对象,则需要使用反序列化
// 从输入流中读取一个Java对象,并将其强制类型转换为Person类
//反序列化机制无须通过构造器来初始化java对象
Person p = (Person)ois.readObject();
System.out.println("名字为:" + p.getName()
+ "\n年龄为:" + p.getAge());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该Java对象所属类的class文件,否则将会引发ClassNotFoundException异常。
Person类只有一个有参数的构造器,没有无参数的构造器,而且该构造器内有一个普通的打印语句,当反序列化读取Java对象时,并没有看到程序调用该构造器,这表明反序列机制无须通过构造器来初始化Java对象。
3.对象引用的序列化
如果某个类的成员变量的类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。
public class Teacher
implements java.io.Serializable
{
private String name;
private Person student;
public Teacher(String name , Person student)
{
this.name = name;
this.student = student;
}
// 此处省略了name和student的setter和getter方法
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// student的setter和getter方法
public void setStudent(Person student)
{
this.student = student;
}
public Person getStudent()
{
return this.student;
}
}
当程序序列化一个Teacher对象时,如果该Teacher对象持有一个Person对象的引用,为了在反序列化时可以正常恢复该Teacher对象,程序会顺带将该Person对象也进行序列化,所以Person类也必须是可序列化的,否则Teacher类将不可序列化。
九.NIO
前面的方法中InputStream的read方法一般如果数据源中没有数据,则会阻塞该线程。
JDK1.4提供了改进输入输出的新功能,这些类都放在java.nio包以及子包下。
java.nio包: 主要包含各种与Buffer相关的类;
java.nio.channel包:主要包含与Channel和Selector相关的类;
java.nio.charset包:主要包含与字符集相关的类;
java.nio.channels.spi包:主要包含与Channle相关的服务提供者编程接口;
java.nio.charset.spi包:包含与字符集相关的服务提供者编程接口。
1.Buffer
2.Channel
3.字符集和Charset
十.文件锁
从JDK1.4的NIO开始,Java开始提供文件锁的支持。
在NIO中,Java提供了FileLock来支持文件锁定功能,在FileChannel中提供的lock()/tryLock()方法可以获得文件锁FileLock对象,从而锁定文件。
文件锁可以有效阻止多个进程并发修改同一个文件。
lock()和tryLock()方法区别:
当lock()试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;而tryLock()是尝试锁定文件,它将直接返回而不是阻塞,如果获得了文件锁,该方法则返回该文件锁,否则返回null。
处理完文件后通过FileLock的release方法释放文件锁,下面是实例:
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class FileLockTest
{
public static void main(String[] args)
throws Exception
{
try(
// 使用FileOutputStream获取FileChannel
FileChannel channel = new FileOutputStream("a.txt")
.getChannel())
{
// 使用非阻塞式方式对指定文件加锁
FileLock lock = channel.tryLock();
// 程序暂停10s
Thread.sleep(10000);
// 释放锁
lock.release();
}
}
}
文件锁虽然可以用于控制并发访问,但对于高并发访问的情形,还是推荐使用数据库来保存程序信息,而不是使用文件。