先看一下java.io.File类。它其实应该命名为FileInfo会好一些。看看它能做什么:
它能用来创建一个文件:
File file = new File("MyFile");
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
查看一个文件的属性:
boolean canRead = file.canRead();
long lastModified = file.lastModified();
String absolutePath = file.getAbsolutePath();
long freeSpace = file.getFreeSpace();
改变一个文件的属性:
file.setExecutable(false);
file.setWritable(true);
file.setLastModified(System.currentTimeMillis());
移动/重命名一个文件:
File otherFile = new File("MyOtherFile");
file.renameTo(otherFile);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean exists = file.exists(); // false;
创建一个文件夹:
File folder = new File("MyFolder");
if (!folder.exists())
folder.mkdir();
列出一个文件夹下的所有文件和文件夹:
String fileNamesInFolder[] = folder.list();
File filesInFolder[] = folder.listFiles();
当然你也可以列出一个文件夹下你感兴趣的文件和文件夹:
String interestedfileNames[] = folder.list(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".txt");
}
});
用File的静态方法得到系统所有的根目录:
File roots[] = File.listRoots();
System.out.println(Arrays.toString(roots));
得到一个文件或文件夹的父文件夹(只对绝对路径有效):
System.out.println(file.getParent()); // null!!
File absFile = file.getAbsoluteFile();
System.out.println(absFile.getParent()); // works!!
最后是删除一个文件或文件夹:
boolean deleted = file.delete();
deleted = folder.delete();
输入输出(I/O)的对象并不仅仅是文件,可以是任何形式的设备,比如屏幕或是网络。下面介绍四个I/O最最基本的类,它们是InputStream、OutputStream、Reader和Writer。
InputStream是所有“输入流”的基类,它是一个纯虚类并定义了一个读取字节的纯虚方法:“public int read() throws IOException”,你可以这样定义InputStream的子类:
class MyInputStream extends InputStream {
@Override
public int read() throws IOException {
if (itor >= data.length)
return -1;
return data[itor++];
}
private int itor = 0;
private byte data[] = { 2, 5, 9, 8, 3, 4 };
}
在定义好这个纯虚read方法只读取一个字节,但返回一个int值。这个int值如果为-1说明读取已经完毕,但如果这个值作为有效值的话,它的前三个字节会被忽略。read方法定义好后,InputStream的read(byte[] b)和read(byte[] b, int off, int len)方法会调用read()方法来实现更复杂的功能。
OutputStream是所有“输出流”的基类,它也是一个纯虚类,定义了两个写入字节的纯虚方法:“public void write(int b) throws IOException”。定义子类:
class MyOutputStream extends OutputStream {
@Override
public void write(int b) throws IOException {
data.add((byte)b);
}
private ArrayList data = new ArrayList();
}
Write方法写入传入int值的最后一个字节。同样的,OutputStream的write(byte[] b)和write(byte[] b, int off, int len)方法会调用write()方法来完成更复杂的功能。
Reader是所有“字符读取器”的纯虚基类,它有两个纯虚方法:“public void close() throws IOException”、“public int read(char[] cbuf, int off, int len) throws IOException”。定义子类:
class MyReader extends Reader {
@Override
public void close() throws IOException {
closed = true;
}
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
if (closed)
throw new IOException();
if (index >= data.length())
return -1;
int count = 0;
for (int i = 0; i < len && index < data.length(); ++i) {
cbuf[i+off] = data.charAt(index++);
++count;
}
return count;
}
private boolean closed = false;
private String data = "This is the data. You are happy~";
private int index = 0;
}
Reader是InputStream的补充,它提供了读取字符的功能,而不仅仅是字节。在定义好read(char[] cbuf, int off, int len)方法后,Reader的“read()”、“read(char[] cbuf)”和“read(CharBuffer target)”方法就可以利用定义好的方法来提供更简单的读取字符的方法。
Writer是所有“写入字符器”的纯虚基类,它定义了三个纯虚方法:“public void close() throws IOException”、“public void flush() throws IOException”和“public void write(char[] cbuf, int off, int len) throws IOException”。定义子类:
class MyWriter extends Writer {
@Override
public void close() throws IOException {
closed = true;
}
@Override
public void flush() throws IOException {
if (closed)
throw new IOException();
System.out.println(data);
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
if (closed)
throw new IOException();
for (int i = 0; i < len; ++i)
data += cbuf[i+off];
}
private boolean closed = false;
private String data = new String();
}
定义好这个纯虚方法后,Writer类的“append(char c)”、“append(CharSequence csq)”、“append(CharSequence csq, int start, int end)”、“write(char[] cbuf)”、“writer.write(int c)”、“write(String str)”和“write(String str, int off, int len)”方法也都可以用了。
现在回到一个比较基本的问题,怎么从读写一个文件的数据?java提供了两个类:FileInputStream和FileOutputStream。我们可以用它们基类里定义的方法:InputStream.read(byte[] bytes)和OutputStream.write(byte[] bytes)。提起精神来,下面的代码有点长,虽然不复杂:
void testFileIOStream() throws IOException {
long ldata = -328910192;
int idata = 2305910;
short sdata = 4652;
char cdata = 'A';
double ddata = 98323.8253221;
float fdata = 2382.784f;
// Write to file
FileOutputStream fos = new FileOutputStream("MyFile");
fos.write(DataConvertor.longToBytes(ldata));
fos.write(DataConvertor.intToBytes(idata));
fos.write(DataConvertor.shortToBytes(sdata));
fos.write(DataConvertor.charToBytes(cdata));
fos.write(DataConvertor.doubleToBytes(ddata));
fos.write(DataConvertor.floatToBytes(fdata));
fos.flush();
fos.close();
byte[] lBytes = new byte[Long.SIZE/8];
byte[] iBytes = new byte[Integer.SIZE/8];
byte[] sBytes = new byte[Short.SIZE/8];
byte[] cBytes = new byte[Character.SIZE/8];
byte[] dBytes = new byte[Double.SIZE/8];
byte[] fBytes = new byte[Float.SIZE/8];
// Read from file
FileInputStream fis = new FileInputStream("MyFile");
fis.read(lBytes);
fis.read(iBytes);
fis.read(sBytes);
fis.read(cBytes);
fis.read(dBytes);
fis.read(fBytes);
fis.close();
// Print Values
System.out.println("Long data: " + DataConvertor.bytesToLong(lBytes));
System.out.println("Int data: " + DataConvertor.bytesToInt(iBytes));
System.out.println("Short data: " + DataConvertor.bytesToShort(sBytes));
System.out.println("Char data: " + DataConvertor.bytesToChar(cBytes));
System.out.println("Double data: " + DataConvertor.bytesToDouble(dBytes));
System.out.println("Float data: " + DataConvertor.bytesToFloat(fBytes));
}
看到上面的代码里有个DataConvertor的类,它可以把基本类型和字节数组进行转换。它可不是java里自带的,我们得自己实现它:
class DataConvertor {
public static byte[] longToBytes(long l) {
return numberToBytes(l, Long.SIZE/8);
}
public static long bytesToLong(byte[] bytes) {
return bytesToNumber(bytes, Long.SIZE/8).longValue();
}
public static byte[] intToBytes(int n) {
return numberToBytes(n, Integer.SIZE/8);
}
public static int bytesToInt(byte[] bytes) {
return bytesToNumber(bytes, Integer.SIZE/8).intValue();
}
public static byte[] shortToBytes(short s) {
return numberToBytes(s, Short.SIZE/8);
}
public static short bytesToShort(byte[] bytes) {
return bytesToNumber(bytes, Short.SIZE/8).shortValue();
}
public static byte[] doubleToBytes(double d) {
return longToBytes(Double.doubleToLongBits(d));
}
public static double bytesToDouble(byte[] bytes) {
return Double.longBitsToDouble(bytesToLong(bytes));
}
public static byte[] floatToBytes(float f) {
return intToBytes(Float.floatToRawIntBits(f));
}
public static float bytesToFloat(byte[] bytes) {
return Float.intBitsToFloat(bytesToInt(bytes));
}
public static byte[] charToBytes(char c) {
return numberToBytes((int)c, Character.SIZE/8);
}
public static char bytesToChar(byte[] bytes) {
return (char)(bytesToNumber(bytes, Character.SIZE/8).intValue());
}
private static byte[] numberToBytes(Number n, final int size) {
byte[] bytes = new byte[size];
long l = n.longValue();
for (int i = 0; i < size; i++)
bytes[i] = (byte)((l >> 8*i) & 0xff);
return bytes;
}
private static Number bytesToNumber(byte[] bytes, final int size) {
long l = 0;
for (int i = 0; i < size; i++)
l |= ((long)(bytes[i] & 0xff) << (8*i));
return l;
}
}
是不是有点复杂?如果我们每次写文件总要和这么底层的字节打交道的话,那样总是显得比较繁琐。java提供了另一组I/O流:DataInputStream和DataOutputStream,它可以外嵌在其它的I/O流对象外,比如FileInputStream。用这两个类重写上面的读写文件的功能:
void testDataIOStream() throws IOException {
DataOutputStream dos = new DataOutputStream(new FileOutputStream("MyFile"));
dos.writeLong(ldata);
dos.writeInt(idata);
dos.writeShort(sdata);
dos.writeChar(cdata);
dos.writeDouble(ddata);
dos.writeFloat(fdata);
dos.flush();
dos.close();
DataInputStream dis = new DataInputStream(new FileInputStream("MyFile"));
long l = dis.readLong();
int n = dis.readInt();
short s = dis.readShort();
char c = dis.readChar();
double d = dis.readDouble();
float f = dis.readFloat();
dis.close();
}
java的I/O类库用的是装饰者模式,比如DataInputStream可以是任何一个InputStream的装饰者。通过这种模式,你可以组合出各种功能的I/O对象。
如果需要向文件存取字符串,而不是字节,就可以使用FileReader和FileWriter了:
testFileRW() throws IOException {
FileWriter fw = new FileWriter("MyFile");
fw.write("Hello, ");
fw.write("hava a good day\n");
fw.append("Here's another line!");
fw.flush();
fw.close();
FileReader fr = new FileReader("MyFile");
CharBuffer cb = CharBuffer.allocate(1000);
int n = fr.read(cb);
fr.close();
System.out.println(cb.array());
}
我们同样可以给Reader和Writer添加装饰者,比如BufferedReader和BufferedWriter。它们提供一个缓冲区来防止每次都执行实际读写操作,同时还提供读写行的能力:
void testBufferRW() throws IOException {
BufferedWriter bw = new BufferedWriter(new FileWriter("MyFile"));
bw.write("Write into buffer");
bw.newLine(); // BufferedWriter only!!
bw.write("Another line");
bw.flush();
bw.close();
BufferedReader br = new BufferedReader(new FileReader("MyFile"));
String line;
do {
line = br.readLine(); // BufferedReader only!!
System.out.println(line);
} while (line != null);
}
除了文件,我们也可以对其它类型的目标进行读写,比如内存。我们可以用CharArrayWriter/CharArrayReader读写内存(或者ByteArrayInputStream/ByteArrayOutputStream读写字节)。下面给了一个在内存中读写字符串例子,同时也介绍一个PrintWriter和StringReader:
void testCharArrayRW() throws IOException {
// write to memory instead of file
CharArrayWriter baw = new CharArrayWriter();
PrintWriter pw = new PrintWriter(baw);
pw.print((int)3);
pw.print((boolean)true);
pw.println("OK");
pw.flush();
pw.close();
// read from memory using CharArrayReader
CharArrayReader car = new CharArrayReader(baw.toCharArray());
BufferedReader br = new BufferedReader(car);
String line;
while ((line = br.readLine()) != null)
System.out.println(line);
br.close();
// read from memory using StringReader
StringReader sr = new StringReader(baw.toString());
br = new BufferedReader(sr);
sr.skip(2);
while ((line = br.readLine()) != null)
System.out.println(line);
sr.close();
}
InputStreamReader和OutputStreamWriter可以把InputStream和OutputStream转成Reader和Writer:
void testIOStreamRW() throws IOException {
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("MyFile"));
osw.write("Just for fun");
osw.flush();
osw.close();
InputStreamReader isr = new InputStreamReader(new FileInputStream("MyFile"));
int c;
while ((c = isr.read()) >= 0)
System.out.print((char)c);
isr.close();
}
系统的标准I/O包括System.in、System.out和System.err。System.out和System.err是两个PrintStream对象,System.in是一个InputStream对象。可以使用System的setIn(InputStream)、setOut(PrintStream)和setErr(PrintStream)方法来重定向这些标准I/O的目标。
I/O库里有一个类RandomAccessFile,它不是Reader/Writer或InputStream/OutputStream的子类,而是直接继承自Object类并实现了DataInput和DataOuput这两个接口。(DataInputStream和DataOutputStream也分别实现了这两个接口)。同时它还可以定位到文件的随机位置进行读写:
void testRandomAccessFile() throws IOException {
RandomAccessFile raf = new RandomAccessFile("MyFile", "rw");
for (int i = 1; i <= 10; ++i)
raf.writeInt(i);
// return to the beginning
raf.seek(0);
for (int i = 0; i < 10; ++i)
System.out.print(raf.readInt() + " "); // 1 2 3 4 5 6 7 8 9 10
// modify the 5th int
raf.seek(Integer.SIZE/8 * 4);
raf.writeInt(555);
raf.seek(0);
for (int i = 0; i < 10; ++i)
System.out.print(raf.readInt() + " "); // 1 2 3 4 555 6 7 8 9 10
}
java里还提供压缩文件的读写。最常用的两种压缩算法是Zip和GZiP。先看看GZip的简单例子:
testGZipIOStream() throws FileNotFoundException, IOException {
GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream("MyZip.gz"));
DataOutputStream dos = new DataOutputStream(gzos);
dos.writeInt(38);
dos.flush();
dos.close();
GZIPInputStream gzis = new GZIPInputStream(new FileInputStream("MyZip.gz"));
DataInputStream dis = new DataInputStream(gzis);
int n = dis.readInt();
dis.close();
System.out.println(n);
}
上面的代码会生成一个MyZip.gz文件,它里面带一个名为“MyZip”的Entry。用解压缩软件就可以看到。
java对Zip文件的支持比较好,可以添加多个Entry,甚至是子文件夹。下例把一个文件压缩到一个zip文件里,同时创建了一个子文件夹下的Entry:
void testZipIOStream() throws IOException {
// create a file to compress into a zip file
FileWriter fw = new FileWriter("MyFile");
fw.write("Just for fun");
fw.flush();
fw.close();
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("MyZip.zip"));
zos.setComment("Here's some comment");
zos.putNextEntry(new ZipEntry("MyFile"));
// compress MyFile into the zip file
FileReader fr = new FileReader("MyFile");
int b;
while ((b=fr.read()) >= 0)
zos.write(b);
// another entry in a sub folder.
zos.putNextEntry(new ZipEntry("Parent/SubFile"));
zos.write('A');
zos.flush();
zos.close();
ZipInputStream zis = new ZipInputStream(new FileInputStream("MyZip.zip"));
// read MyFile
ZipEntry ze = zis.getNextEntry();
System.out.println(ze.getName()); // MyFile
InputStreamReader isr = new InputStreamReader(zis);
int c;
while ((c=isr.read()) >= 0)
System.out.print((char)c); // Output: Just for fun
System.out.println();
// read SubFile
ze = zis.getNextEntry();
System.out.println(ze.getName()); // Parent/SubFile
System.out.println((char)isr.read()); // 'A'
isr.close();
}
JDK1.4引入了一个新的I/O类库:java.nio.*(nio = new io)。它通过通道(java.nio.channels.Channel)和缓冲器(java.nio.ByteBuffer)来提高I/O的效率。FileInputStream、FileOutputStream和RandomAccessFile都提供了一个getChannel的方法来使用通道传输数据。下面给一个简单的例子:
void testChannel() throws IOException {
FileChannel fc = new FileOutputStream("MyFile").getChannel();
fc.write(ByteBuffer.wrap("string data".getBytes()));
fc.close();
fc = new RandomAccessFile("MyFile", "rw").getChannel();
// locate to the end
fc.position(fc.size());
fc.write(ByteBuffer.wrap("\nAppend to the end".getBytes()));
fc.close();
fc = new FileInputStream("MyFile").getChannel();
ByteBuffer bb = ByteBuffer.allocate(1024);
fc.read(bb);
bb.flip(); // prepare to read the buffer
while (bb.hasRemaining())
System.out.print((char)bb.get());
}
ByteBuffer就是唯一可以直接和Channel交互的缓冲器。它有一点不方便的地方就是,当读取这个缓冲器的数据前,必须调用ByteBuffer.flip()方法,而在向这个缓冲器写数据前,要调用ByteBuffer.clear()方法。(clear方法并不清空已有的数据,待会再作详细说明)。这种不便利性也是为了提高存取速度而作的牺牲。
ByteBuffer只能通过静态方法ByteBuffer.wrap和ByteBuffer.allocate创建,它在被创建后大小(Capacity)就是固定的。它用一个position的整型值来指示当前读写的位置,同时用一个limit的整型成员来表示当前数据的大小(字节数)。
当向ByteBuffer里写数据时,应该先调用clear方法,它会把postion设为0同时把limit设为和Capacity一样。之后每调用put方法写数据时,position就向后移n个字节。当position超过limit时,就抛出异常。
在读数据前,应该先调用flip方法,它会把limit值设为已写数据的尾部,并把position设为0。读时可以用hasRemaining方法来判断是否还有可读数据。remaining等于limit与position的差值。
你还可以用mark和reset方法来标记和重置缓冲区的某个特定的位置。
在上面的代码里,我们使用ByteBuffer直接读写字符串。我们也可以使用CharBuffer:
testCharBuffer() throws UnsupportedEncodingException {
ByteBuffer bb = ByteBuffer.wrap("some text".getBytes());
System.out.println(bb.asCharBuffer()); // Output: ????
bb = ByteBuffer.allocate(1024);
bb.put("some text".getBytes("UTF-16BE"));
bb.flip(); // after "put" call this to read
bb.rewind(); // return to the beginning
System.out.println(bb.asCharBuffer()); // Output: some text
bb.clear(); // only reset the position, won't erase the content
CharBuffer cb = bb.asCharBuffer();
cb.put("other");
cb.flip();
System.out.println(cb); // Output: other
}
CharBuffer是ByteBuffer的一个视图缓冲器,即它底层和ByteBuffer共用一块缓冲区,但维护独自的position和limit等成员。此外还有IntBuffer、DoubleBuffer等视图缓冲器。注意到上面的代码里,在用裸缓冲器写字符串我们必须把对它进行UTF-16BE的编码,否则CharBuffer不能识别。
你可以用Channel的TransferTo和TransferFrom方法在两个通道间传递数据:
void testTransferChannel() throws IOException {
FileChannel in = new FileInputStream("MyFile").getChannel();
FileChannel out = new FileOutputStream("Other").getChannel();
in.transferTo(0, in.size(), out);
}
有了通道,我们就可以使用MappedByteBuffer把一个文件(整个或部份地)映射到内存中进行读写:
void testMappedBuffer() throws FileNotFoundException, IOException {
int start = 2;
int length = 12;
MappedByteBuffer mbb = new RandomAccessFile("MyFile", "rw").getChannel().
map(FileChannel.MapMode.READ_WRITE, start, length);
while (mbb.hasRemaining())
System.out.print((char)mbb.get());
}
内存映射的读写比直接使用I/O stream要快速很多,同时还能解决大文件不能全部装入内存的问题。
我们可以对文件或其中的某一部份进行加锁:
void testFileLock() throws IOException {
FileOutputStream fos = new FileOutputStream("MyFile");
FileLock fl = fos.getChannel().tryLock();
if (fl != null) {
System.out.println("Got lock!");
fl.release();
}
int start = 2;
int length = 12;
boolean shared = false;
FileLock fl_part = fos.getChannel().tryLock(start, length, shared);
if (fl_part != null) {
System.out.println("Got part lock!!");
fl.release();
}
fos.close();
}
java对序列化的支持作的非常好,只要让需要序列化的类实现Serializable接口,而这个接口没有任何方法。之后使用ObjectOutputStream和ObjectInputStream就可以读写对象了:
class MyClass implements Serializable {
public void print() {
System.out.println("MyClass - " + i);
}
private int i;
{
i = new Random().nextInt(199);
}
}
void testSerialization() throws FileNotFoundException, IOException, ClassNotFoundException {
MyClass mc = new MyClass();
mc.print();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("MyFile"));
oos.writeObject(mc);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("MyFile"));
MyClass mc2 = (MyClass)ois.readObject();
ois.close();
mc2.print();
}
如果你不想使用java自带的序列化方式(直接把二进制数据写入内存),可以实现Externalizable接口。它有两个方法:writeExternal和readExternal。想要使用Externalizable接口的类必须提供一个public的不带参数的构造器。看一个例子:
class MyExternalizable implements Externalizable {
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
i = in.readInt();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(i);
}
// A public constructor without arguments is a must!!
public MyExternalizable() {}
public void print() {
System.out.println(i);
}
private int i;
{
i = new Random().nextInt(198);
}
}
接下来就可以和Serializable一样进行序列化了。如果你还是想用Serializable,但只想序列化对象里的其中一部份成员,可以用transient关键字:
class MySerializable implements Serializable {
private int i;
private transient int j;
}
如果你想实现Serializable,同时又想定义自己序列化的方法,也是可以的。只要实现readObject和writeObject方法。这不是覆盖,而是通过反射被调用:
class MySerializable implements Serializable {
private int i;
private transient int j;
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // use default behavior
out.writeInt(j);
}
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
in.defaultReadObject();
j = in.readInt();
}
}
我们还可以用java.util.prefs.Preferences向注册表读写数据:
void testPreferences() throws BackingStoreException {
Preferences p = Preferences.userNodeForPackage(IOTest4.class);
p.putInt("MyKey", 8); // write to registry
int n = p.getInt("MyKey", 0); // read from registry
System.out.println(n);
p.removeNode();
}