第十八章:Java I/O系统

Java I/O系统

File类

  • File类这个名字有一定的误导性,我们可能认为它指代的是文件,实际上它只是一个文件路径(FilePath)。下面是一些关于这个类的用法。

目录列表器

  • 如果我们想获得文件目录下的文件列表,我们可以通过File类的list()方法。另外还有关于过滤的方法(不是简单的判断后缀名)。下面是一个例子:
import java.io.*;
import java.util.*;
import java.util.regex.Pattern;

public class DirList {
    public static void main(String args[]) {
        File path = new File(".");//当前java文件根目录。package的头部
        String[] list = path.list(new DirFilter("^[\\S]+txt$"));
        //获得FileList的两种方式,用FilenameFilter或者FileFilter过滤。
        File[] filelist1 = path.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith("txt");
            }
        });
        File[] filelist2 = path.listFiles(new FileFilter() {
            public boolean accept(File pathname) {
                System.out.println(pathname.getName() + ": " + pathname.length());
                return pathname.length() > 1000;//文件大小大于1000字节
            }
        });
        Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);//用字符自然顺序排序
        System.out.println(Arrays.toString(list));
        System.out.println(filelist1.length);
        System.out.println(filelist2.length);
    }
}

class DirFilter implements FilenameFilter {
    private Pattern pattern;
    public DirFilter(String regex) {
        pattern = Pattern.compile(regex);
    }
    //返回true就是接受这个文件,第二个参数是文件名,当然也可以通过文件其他属性过滤
    public boolean accept(File dir, String name) {
        return pattern.matcher(name).matches();
    }
}
  • 在简单了解了一下File类的一个基本功能后,我们再来熟悉一下其他属性和方法。我这里就只列出属性和方法的解释,具体的例子我就省略了。File类对于下面几章的学习非常重要,需要记住一些常用的字段和方法。

字段

类型字段名解释
static StringpathSeparator路径分隔符,(根据操作系统而异)windows为分号“;”。在 UNIX 系统上,此字段为“:”。为了方便它被表示一个字符串。
static charpathSeparatorChar路径分隔符,(根据操作系统而异)windows为分号“;”。在 UNIX 系统上,此字段为“:”。
static Stringseparator名称分隔符,(根据操作系统而异)windows为反斜杠“\”。在 UNIX 系统上,此字段的值为“/”。为了方便它被表示一个字符串。
static charseparatorChar名称分隔符,(根据操作系统而异)windows为反斜杠“\”。在 UNIX 系统上,此字段的值为“/”。

构造方法

定义解释
File(File parent, String child)根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。
File(String pathname)通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。
File(String parent, String child)根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
File(URI uri)通过将给定的 file: URI 转换为一个抽象路径名来创建一个新的 File 实例。

常用实例方法

返回类型定义解释
booleancanExecute()是否可执行,文件夹也可以执行。这个在unix系统中有所应用,下面两个也是。
booleancanRead()可读
booleancanWrite()可写
booleancreateNewFile()当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。
booleandelete()删除此抽象路径名表示的文件或目录。
voiddeleteOnExit()在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。(存放虚拟机运行时的临时文件)
booleanexists()测试此抽象路径名表示的文件或目录是否存在。
FilegetAbsoluteFile()返回此抽象路径名的绝对路径名形式。与new的时候传进去的路径有关
StringgetAbsolutePath()返回此抽象路径名的绝对路径名字符串。
FilegetCanonicalFile()返回此抽象路径名的规范形式。与new的时候传进去的路径无关
StringgetCanonicalPath()返回此抽象路径名的规范路径名字符串。
StringgetName()返回由此抽象路径名表示的文件或目录的名称。
StringgetParent()返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回 null。
FilegetParentFile()返回此抽象路径名父目录的抽象路径名;如果此路径名没有指定父目录,则返回 null。
StringgetPath()将此抽象路径名转换为一个路径名字符串。(与new的时候相同)
booleanisDirectory()测试此抽象路径名表示的文件是否是一个目录。
booleanisFile()测试此抽象路径名表示的文件是否是一个标准文件。
longlastModified()返回此抽象路径名表示的文件最后一次被修改的时间。
longlength()返回由此抽象路径名表示的文件的长度。(bytes)
booleanmkdir()创建此抽象路径名指定的目录。
booleanmkdirs()创建此抽象路径名指定的目录,包括所有必需但不存在的父目录。
URItoURI()构造一个表示此抽象路径名的 file: URI。
  • 还有一些其他的方法以及前面提到的list()。有兴趣可以去查看API。

输入和输出

  • InputStreamoutputStream是两个抽象类(前者需重写read()方法,后者需重写write()方法)。我们来看看常用的InputStreamoutputStream的子类(他们的确重写了read和write方法,不过通常我们会使用其更简便的方法)。
功能构造器参数/如何使用
ByteArrayInputStream允许将内存的缓冲区当作InputStream使用缓冲区,字节将从中取出/作为一种数据源,将其与FilterInputStream对象相连以提供有用接口
StringBufferInputStream将String转成InputStream字符串,底层实现实际使用的是StringBuffer(源码中用的是String,这句话有待考量),已过时/作为一种数据源,将其与FilterInputStream对象相连以提供有用接口
FileInputStream用于从文件中读取信息文件名,File实例或FileDescriptor实例/作为一种数据源,将其与FilterInputStream对象相连以提供有用接口
PipedInputStream产生用于写入相关PipedOutputStream的数据,实现管道化的概念PipedOutputStream/作为多线程中数据源,将其与FilterInputStream对象相连以提供有用接口
SequenceInputStream将两个或多个InputStream对象转换成单一InputStream两个InputStream对象或一个容纳InputStream对象的容器Enumeration/作为一种数据源,将其与FilterInputStream对象相连以提供有用接口
FilterInputStream抽象类,作为装饰器的接口,详情请看下一小节
  • 下面是一个简单的测试类,简单了解一下输入流的用法。
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.SequenceInputStream;
import java.io.StringBufferInputStream;

public class Test {
    public static void main(String args[]) throws IOException {
        //已经弃用 只需要ByteArrayInputStream就足够了。如果要直接解析字符串还不如用Scanner
        StringBufferInputStream s = new StringBufferInputStream("123 123");
        ByteArrayInputStream ss = new ByteArrayInputStream("123 123".getBytes());
        SequenceInputStream sss = new SequenceInputStream(s, ss);
        byte b[] = new byte[1024];
        int in;
        StringBuilder sb = new StringBuilder();
        while ((in = sss.read(b)) != -1) {
            sb.append(new String(b, 0, in));
        }
        System.out.println(sb);

        FileInputStream fin = new FileInputStream("c1.txt");
        sb.delete(0, sb.length());
        while ((in = fin.read(b)) != -1) {
            sb.append(new String(b, 0, in));
        }
        System.out.println(sb);
    }
}
-----------运行结果
123 123123 123
c1.txt的内容
  • 下面是OutputStream的几个子类
功能构造器参数/如何使用
ByteArrayOutputStream在内存中创建缓冲区。所有送往流的数据都要放置在此缓冲区缓冲区初始化尺寸(可选)/用于指定数据的目的地,将其与FilterOutputStream对象相连以提供有用接口
FileOutputStream用于将信息写入文件文件名,File实例或FileDescriptor实例/用于指定数据的目的地,将其与FilterOutputStream对象相连以提供有用接口
PipedOutputStream任何写入其中的信息都会被自动作为相关PipedInputStream的输出,实现管道化的概念PipedInputStream/用于指定多线程的数据的目的地,将其与FilterInputStream对象相连以提供有用接口
FilterOutputStream抽象类,作为装饰器的接口,详情请看下一小节
  • 下面是一个简单的测试类,简单了解一下输出流的用法。
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class Test {
    public static void main(String args[]) throws IOException {
        byte[] b = "hello world".getBytes();
        ByteArrayOutputStream out1 = new ByteArrayOutputStream();
        out1.write(b);
        out1.flush();//直接用out1.toByteArray()获得字节数组,肯定是一样的,这里就不验证了

        //FileOutputStream fos = new FileOutputStream("c1.txt");
        FileOutputStream fos = new FileOutputStream("c1.txt", true);//第二个参数为是否追加内容
        fos.write(b);//查看该文件的确已经更改了

        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(pos);//只要一边写上关联代码,即可双方关联

        pos.write(b);
        byte[] buf = new byte[1024];
        int size = pis.read(buf);//因为size不大,所以我就不加循环了。
        System.out.println(new String(buf, 0, size));//hello world
        pis.close();
        pos.close();
        fos.close();
        out1.close();//最好将流关闭,避免出现内存泄露(关于InputStream的例子我忘记了,嫌麻烦就不去改了)
        //反正要注意流对象垃圾回收器是不会随便回收的。
    }
}

添加属性和有用的接口

  • 前一节还没有提到FilterInputStreamFilterOutputStream的用法。这两个类是用来提供装饰器类接口以控制特定输入输出流的。

通过FilterInputStream从InputStream读取数据(OutputStream同理)

  • FilterInputStream类能够完成两件完全不同的事情。其中,DataInputStream允许我们读取不同的基本类型数据以及String对象。搭配相应的DataOutputStream,我们就可以通过数据流将基本类型的数据从一个地方迁移到另一个地方。
  • 其他FilterInputStream类则在内部修改InputStream的行为方式:是否缓冲,是否保留它读过的行,以及是否把单一字符推回输入流等。最后两个类看起来像是为了创建一个编译器,因此我们在一般编程中不会用到它们。
  • 我们机会每次都要对输入进行缓冲,所以I/O类库把无缓冲输入作为特殊情况。
  • 下面是一些FilterInputStream,另外我还放入了FilterOutputStream的子类PrintStream,另外两个(DataOutputStreamBufferedOutputStream)我直接在例子中做测试,就不列在表格里了。
功能构造器参数/如何使用
DataInputStream与DataOutputStream搭配使用,因此我们可以按照可移植方式从流读取基本类型数据(int,char,long等)InputStream/包含用于读取基本类型数据的全部接口
BufferedInputStream使用它可以防止每次读取时都得进行实际写操作。代表”使用缓冲区”InputStream,可以指定缓冲区大小(可选)/本质上不提供接口,只不过是向进程中添加缓冲区所必需的。对接口对象搭配
LineNumberInputStream跟踪行号InputStream/仅增加了行号,已过时
PushbackInputStream可以将字节回退到缓冲区InputStream,可设置可回退缓冲字节大小/通常作为编译器的扫描器,之所以包含在内是因为Java编译器的需要,我们可能永远不会用到
PrintStream用于产生格式化输出,有一些特别好用的方法OutputStream,可以指定是否自动flush/下面有例子
  • 上面书上一些描述已经过时了,我改写了一下。下面是一些例子:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.LineNumberInputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PushbackInputStream;

public class Test {
    public static void main(String args[]) throws IOException {
        //用其他的InputStream 无法验证,比如ByteArrayInputStream
        DataInputStream dis = new DataInputStream(new FileInputStream("c1.txt"));
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("c1.txt"));
        dos.writeInt(1);
        dos.writeDouble(1.0D);
        dos.writeUTF("miaoch");
        //dos.flush();//文件流是不需要缓冲的 这句不需要
        System.out.println(dis.readInt());
        //System.out.println(dis.readInt()); //保存的是Double用read读取会读取前32位导致数据混乱
        System.out.println(dis.readDouble());
        System.out.println(dis.readUTF());//文件流读取不到的话 会抛异常
        dis.close();//关闭最外层的流即可 他会自动调用构造器传入的那个流的close方法
        dos.close();//关闭最外层的流即可 他会自动调用构造器传入的那个流的close方法
        PipedInputStream pis = new PipedInputStream();
        PipedOutputStream pos = new PipedOutputStream(pis);
        dis = new DataInputStream(pis);
        dos = new DataOutputStream(pos);
        dos.writeUTF("miaoch");
        //dos.flush();//管道也是不需要缓冲的
        System.out.println(dis.readUTF());//管道读取不到的话 会等待
        dis.close();//千万别忘记了
        dos.close();//千万别忘记了

        dis = new DataInputStream(System.in);//从控制台读取
        //System.out.println(dis.readInt());//输入1234 会被认为是byte[]即 49 50 51 52 13 10 
        //System.out.println((49<<(8*3)) + (50<<(8*2)) + (51<<8) + 52);
        System.out.println(dis.readInt());//输入12 会被认为是byte[]即 49 50 13 10 最后两个是回车换行 
        System.out.println((49<<(8*3)) + (50<<(8*2)) + (13<<8) + 10);
        dis.close();

        //关于BufferedInputStream BufferedOutputStream
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("c1.txt"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("c1.txt"));
        bos.write("hello world\n".getBytes());
        bos.flush();//必须刷新
        byte[] b = new byte[1024];
        System.out.println(new String(b, 0, bis.read(b)));//hello world
        bos.write("hello world1\n".getBytes());
        bos.write("hello world2\n".getBytes());
        bos.write("hello world3".getBytes());
        bos.flush();//必须刷新
        bis.close();
        bos.close();

        //关于LineNumberInputStream 已过时
        LineNumberInputStream lis = new LineNumberInputStream(new FileInputStream("c1.txt"));
        lis.setLineNumber(3);//这个设置不会改变光标,只是设置lineNumber的初始值,当读取'\n'会+1
        System.out.println(new String(b, 0, lis.read(b)));//hello world
        System.out.println(lis.getLineNumber());

        PushbackInputStream ppis = new PushbackInputStream(new FileInputStream("c1.txt"), 10);//此处缓冲10个字节可回退 默认是一个字节
        int len =  ppis.read(b);
        System.out.println("len==" + len);//总共 12 + 13 * 2 + 12 50个字节
        System.out.println(new String(b, 0, len));//hello world
        //ppis.unread(b, len - 2, 2);
        ppis.unread('d');
        ppis.unread('c');
        ppis.unread('b');
        ppis.unread('a');
        len = ppis.read(b);
        System.out.println("String==" + new String(b, 0, len));//hello world
        ppis.close();

        PrintStream ps = new PrintStream(new FileOutputStream("c2.txt"));//构造方法里面有一个autoFlush字段,据说每次println能够自动刷新 我没试过
        //PrintStream ps = new PrintStream(new File("c1.txt"));//如果是针对文件也可以这么写
        ps.println(true);//常用方法
        ps.printf("%s", "hello");//常用方法
        ps.append(new String("world"));//常用方法
        ps.flush();//有些流是不需要flush的,但是保险起见flush一下准没错 不过close也会帮你flush。所以其实不写也行,只要缓冲区够大。
        ps.close();
    }
}
-------运行结果:
1
1.0
miaoch
miaoch
123--->此处需要控制台输入121234
825373453
825363722
hello world

hello world
hello world1
hello world2
hello world3
6
len==50
hello world
hello world1
hello world2
hello world3
String==abcd

Reader和Writer

  • 由于InputStream和OutputStream是面向字节的,因此Java1.1推出了新的IO类ReaderWriter,设计他们主要是为了国际化,他们能更好地处理16位的Unicode字符。下面是他们之间的相似关系:
原先针对字节的类(数据来源)相应的针对字符的类
InputStreamReader 适配器 InputStreamReader
OutputStreamWriter 适配器 OutputStreamWriter
FileInputStreamFileReader
FileOutputStreamFileWriter
StringBufferInputStream(已弃用)StringReader
StringWriter
ByteArrayInputStreamCharArrayReader
ByteArrayOutputStreamCharArrayWriter
PipedInputStreamPipedReader
PipedOutputStreamPipedWriter
原先针对字节的类(过滤器)相应的针对字符的类
FilterInputStreamFilterReader
FilterOutputStreamFilterWriter(抽象类、没有子类)
BuffererInputStreamBufferedReader
BuffererOutputStreamBufferedWriter
DataInputStreamDataInputStream(其余可以直接用,readLine的时候使用BufferedReader)
PrintStreamPrintWriter
LineNumberInputStream(已弃用)LineNumberReader
PushbackInputStreamPushbackReader

- 关于字符流的例子我就不举了,私下试试就清楚了。

自我独立的类:RandomAccessFile

  • RandomAccessFile适用于由大小已知的记录组成的文件。就跟我们打开一个记事本一样,可以边读边写。它是一个完全独立的类,没有继承于XXStream或者ReaderWriter。从本质上讲,它的工作方式类似于把DataInputStreamDataOutputStream组合起来使用。它的大部分功能由nio存储映射文件取代,所以这里就不再聊这个了。因为基本用不到这个类。

I/O流的典型使用方式

缓冲输入文件

  • 如果想要打开一个文件用于字符输入,可以使用FileInputReader。为了提高速度,我们希望能对那个文件进行缓冲,那么我们将所产生的引用传给一个BufferedReader构造器。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

/*缓冲输入文件*/
public class BufferInputFile {
    //这种抛异常方式非常危险,如果in.readLine()出现异常会导致in无法被close(虽然概率不大)。
    //这里只是一个例子,请不要直接调这个方法。
    public static String read(String filename) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(filename));
        StringBuilder result = new StringBuilder();
        String temp;
        while ((temp = in.readLine()) != null) {
            result.append(temp + "\n");
        }
        in.close();
        return result.toString();
    }

    public static void main(String args[]) throws IOException {
        System.out.println(read("c1.txt"));
    }
}

标准I/O重定向

  • Java的System类提供了一些简单的静态方法调用,以允许我们对标准输入、输出和错误I/O流进行重定向。当控制台有大量信息不好阅读时,我们可以将我们需要的一些信息输出到文件中。其实也用不到重定向,新建一个输出流即可。
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;


public class Test {
    public static void main(String []args) throws Exception {
        new Thread(){
            {
                setDaemon(true);//后台线程
                start();
            }
            public void run() {
                while (true) {
                    System.out.println("无用信息");
                    Thread.yield();
                }
            }
        };
        Thread.sleep(100);
        PrintStream consoleOut = System.out;
        InputStream consoleIn = System.in;
        BufferedInputStream in = new BufferedInputStream(new FileInputStream("c1.txt"));
        PrintStream out = new PrintStream(new FileOutputStream("c2.txt"));
        synchronized (out) {//上锁避免其他线程写入无用信息
            System.setIn(in);//因为没有别的线程在调用System.in,这里就不给in加锁了。
            System.setOut(out);
            System.setErr(out);
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            String s;
            while ((s = br.readLine()) != null) {
                //加上println方法加的锁,此处会有两个锁
                System.out.println(s);//如果有其他线程也在做这个,也写入这个文件中。但是上锁就不会了
            }
            out.close();
            System.setIn(consoleIn);
            System.setOut(consoleOut);
            System.setErr(consoleOut);
        }
    }
}

新I/O

  • JDK1.4的java.nio.*引入了新的Java I/O类库,其目的在于提高速度。实际上,旧的I/O包已经使用nio重新实现过,因此,即使我们不显式地使用nio编写代码,也能从中受益。
  • 速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道缓冲器。通道负责数据的真实传输,而缓冲器则是通过空间换取时间,避免读写太过频繁。
  • 唯一与通道交互的缓冲器是ByteBuffer
  • 接下来是一个关于管道缓冲器的例子:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 通常输入流一个管道,输出流一个管道,管道口各有一个ByteBuffer。
 * 当使用RandomAccessFile(rw模式)时,两个管道合二为一,但ByteBuffer还是需要两个。
 * 
 * @author Administrator
 *
 */
public class Test {
    public static void main(String []args) throws Exception {
        ByteBuffer buff = ByteBuffer.allocate(1024);//相当于定义个了一个byte[1024]

        FileChannel channel = new FileOutputStream("c2.txt").getChannel();
        channel.write(ByteBuffer.wrap("some text ".getBytes()));//wrap(byte[])
        channel.close();//必须关闭管道

        channel = new FileInputStream("c2.txt").getChannel();
        //channel.write(ByteBuffer.wrap("error ".getBytes()));//读的管道是无法写的
        channel.read(buff);//读入缓冲器
        buff.flip();//提示缓冲器准备被读取
        //遍历buff的常用方式
        while (buff.hasRemaining()) {
            System.out.print((char) buff.get());
        }
        buff.clear();//清空缓冲器
        channel.close();
        System.out.println();

        channel = new RandomAccessFile("c2.txt", "rw").getChannel();
        channel.position(channel.size());
        channel.write(ByteBuffer.wrap("hello world ".getBytes()));

        channel.position(0);//定位到最开始
        channel.read(buff);
        buff.flip();
        while (buff.hasRemaining()) {
            System.out.print((char) buff.get());//读取中文会出错
        }
        buff.clear();
    }
}
  • 我们可以用下面的方式连接两个管道,其中的一些缓冲器的操作我们就可以不用管了,当然这种方式比较少用,如果用于复制文件的话,的确是个好方法:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

public class Test {
    public static void main(String []args) throws Exception {
        FileChannel in = new FileInputStream("c1.txt").getChannel();
        FileChannel out = new FileOutputStream("c2.txt").getChannel();
        //FileChannel out = new RandomAccessFile("c2.txt", "rw").getChannel();
        //in.transferTo(10, in.size() - 20, out);//第一个参数是从in的哪一个点开始,第二个参数是长度
        out.transferFrom(in, 2, in.size());//如果out没有读取能力(可以换RandomAccessFile),当第二个参数不为0时,会得到空文件
        in.close();
        out.close();
    }
}
  • 关于ByteBuffer如何获得char[]的问题,前面用的是一个一个读取转换,但是遇到中文字符会出现显示错误。关于这个问题,书上有三种解决方式:
    1. 对写入的ByteBuffer进行编码(默认是System.getProperty(“file.encoding”)的编码格式),对读出的ByteBuffer解码(默认是UTF-16BE)。因为我设置的是GBK编码,而解码用的是UTF-16BE,只要修改其中一个即可(Charset.forName(“GBK”).decode(buff))。
    2. 对传入的ByteBuffer.asCharBuffer().put(content); 然后通过ByteBuffer.asCharBuffer()获得char[]结果(其实相当于用其内部指定的编码进行编码解码)。
    3. 将写入的ByteBuffer编码成”UTF-16BE“,(getBytes(“UTF-16BE”))。
  • 另外除了从ByteBuffer中读取char以外,还可以从其中读取其他的基本类型,方法是使用asIntBuffer(),asLongBuffer()等方法获取其他类型的Buffer,然后再向其中put()或者get(),其实其底层用的还是ByteBuffer(一次读取多少字节,返回什么类型被自动化了),只不过封装了一层代理,使其用起来更方便而已。
  • 这里说明一下什么是低位优先(little endian)高位优先(big endian)。不同机器可能会使用不同的字节排序来存储数据。当储存量大于1个字节时,就得考虑字节的存储问题。ByteBuffer是以高位优先的形式存储的。啥意思呢?就是说你如果往BuyteBuffer里放入一个int型数据,由于int是四个字节,它会先把最高位的字节先存入ByteBuffer。比如说这个int100,很明显其前三个字节都是0。如果此时按照低位优先存储的话,就会先把代表100的字节存入ByteBuffer,然后再存倒数第二个,第三个,第四个字节。高位优先存储的方式更容易让人理解。
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

public class Test {
    public static void main(String []args) throws Exception {
        ByteBuffer bb = ByteBuffer.allocate(12);
        bb.asCharBuffer().put("占八字节");
        bb.putInt(8, 100);//占4个字节
        System.out.println(Arrays.toString(bb.array()));

        bb.clear();
        bb.order(ByteOrder.LITTLE_ENDIAN);//默认是BIG_ENDIAN
        bb.asCharBuffer().put("占八字节");
        bb.putInt(8, 100);//占4个字节
        System.out.println(Arrays.toString(bb.array()));
    }
}
  • 下面是一张nio类关系图:
    这里写图片描述
  • Buffer由数据和可以高效访问及操纵数据的四个索引组成。这四个索引是mark(标记)、position(位置)、limit(界限)和capacity(容量)。最常用的是position和limit,get()(put(val)也从position开始)不传参数方法会从position下标开始到limit结束(可能出现越界错误,需要通过hasRemaining()判断),下面是用于设置和复位索引以及查询它们的值的方法。这里顺便说一下,在用通道读取到缓冲器时,要调用flip()才能读取缓冲器里的内容是因为,通道写的过程中改变了position的值,所以我们只用从0读到position即可(将limit设为position,position设为0)。
    这里写图片描述
  • 上面的图还少了两个常用的方法
    1. rewind(),将position设为0,mark设为-1。相当于复位。和clear()不同的是,clear()会将limit设为capacity。
    2. reset(),判断mark的下标是否合理,合理的话设置position为mark,否则抛异常。

内存映射文件

  • 内存映射文件允许我们创建和修改那些因为太大而不能放入内存的文件。有了内存映射文件,我们就可以假定整个文件都放在内存中,而且可以完全把它当做非常大的数组来访问。最大可支持2GB(int上限),其速度也比旧I/O来的快。
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class Test {
    //static int length = 0xFFFFF;//1MB
    //生成的文件太大,用txt打不开,可以考虑缩小一点
    static int length = Integer.MAX_VALUE;//2GB 差1B
    public static void main(String []args) throws Exception {
        //记得关闭流,我这里因为是个测试例子省掉了
        MappedByteBuffer out = new RandomAccessFile("c1.txt", "rw")
            .getChannel().map(FileChannel.MapMode.READ_WRITE, 0, length);
        byte[] b = "哈".getBytes();
        for (int i=0; i < length / 2; i++) {
            out.put(b);
        }
        System.out.println("Finished!");
    }
}
  • 其中MappedByteBuffer是ByteBuffer的子类。

文件上锁

  • 下面是一个简单的文件上锁的例子:
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;

public class Test {
    public static void main(String []args) throws Exception {
        RandomAccessFile file = new RandomAccessFile("c1.txt", "rw");
        System.out.println("locking!");
        FileLock fl = file.getChannel().tryLock();
        if (fl != null) {
            Thread.sleep(10000);
            fl.release();
            System.out.println("Released ok!");
        }
        file.close();//记得关闭
    }
}
  • 在locking期间,我们打开c1.txt,尝试修改保存,会得到下列提示。因为文件加锁是直接映射到本地操作系统的加锁工具,所以不必担心操作系统本地进程无法同步。

    这里写图片描述

  • SocketChannel、DatagramChannel和ServerSocketChannel不需要加锁。因为他们本身就是针对单线程的。tryLock()是非阻塞式的,lock是阻塞式的。话虽然这么说,但是我下面的例子说明lock也不是阻塞式的。会直接弹出FL2 locking failed,可能是我例子写的不对。

import java.io.RandomAccessFile;
import java.nio.channels.FileLock;

public class Test {
    public static void main(String []args) throws Exception {
        final RandomAccessFile file = new RandomAccessFile("c1.txt", "rw");
        new Thread() {
            public void run() {
                try {
                    System.out.println("thread locking!");
                    FileLock fl = file.getChannel().lock();
                    Thread.sleep(3000);//锁定10秒
                    fl.release();
                    System.out.println("thread release!");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();
        Thread.sleep(1000);//保证文件已经被加锁
        FileLock fl = null;
        try {
            fl = file.getChannel().lock();//阻塞
            System.out.println("FL2 locking success");
        } catch (Exception e) {
            System.out.println("FL2 locking failed");
        } finally {
            if (fl != null) {
                fl.release();
                System.out.println("Released ok!");
            }
        }
        //file.close();//记得关闭  这里为了测试注释掉这段代码
    }
}
  • 另外对于映射文件的部分加锁,书中也给了例子,大致是使用LockAndModify()方法,上面的FileLock也可以部分加锁,这部分我就省略了。

压缩

  • 关于压缩,我就不多说了,我之前收录过一个工具类,这里可以给大家参考一下:
package org.zip;

import java.io.*;  
import java.util.zip.*;  

public class ZipUtil {  
    private static void zip(ZipOutputStream out, File f, String base) throws Exception {
        if (f.isDirectory()) {
            File[] files = f.listFiles();
            base = (base.length() == 0 ? "" : base + "/");
            for (int i = 0; i < files.length; i++) {
                zip(out, files[i], base + files[i].getName());
            }
        } else { 
            out.putNextEntry(new ZipEntry(base));
            BufferedInputStream in = new BufferedInputStream(new FileInputStream(f));
            transfer(in, out);
            in.close();
        }  
    }  

    //压缩文件,inputFileName表示要压缩的文件(可以为目录),zipFileName表示压缩后的zip文件  
    public static void zip(String inputFileName, String zipFileName) throws Exception {  
        ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFileName));  
        zip(out, new File(inputFileName), "");
        out.close();
    }  

    //解压,zipFileName表示待解压的zip文件,unzipDir表示解压后文件存放目录  
    public static void unzip(String zipFileName, String unzipDir) throws Exception {
        createDir(unzipDir);
        ZipInputStream in = new ZipInputStream(new FileInputStream(zipFileName));  
        ZipEntry entry;  
        while ((entry = in.getNextEntry()) != null) {  
            String fileName = entry.getName();  
            //有层级结构,就先创建目录  
            String tmp;
            int index = fileName.lastIndexOf('/');  
            if (index != -1) {  
                tmp = fileName.substring(0, index);  
                tmp = unzipDir + "/" + tmp;  
                createDir(tmp);
            }
            //创建文件  
            fileName = unzipDir + "/" + fileName;  
            File file = new File(fileName);  
            file.createNewFile();  
            FileOutputStream out = new FileOutputStream(file);  
            transfer(in, out);
            out.close();  
        }  
        in.close();  
    }

    private static void transfer(InputStream in, OutputStream out) throws Exception {
        byte[] data = new byte[1024];
        int index;
        while ((index = in.read(data)) != -1) {  
            out.write(data, 0 , index);  
        }  
    }

    private static File createDir(String path) {
        File f = new File(path);
        if (!f.isDirectory()) {
            f.mkdirs();
        }
        return f;
    }

    public static void main(String args[]) throws Exception {
        zip("D:\\test", "D:\\test.zip");
        unzip("D:\\test.zip", "D:\\test_2");
    }
}  

对象序列化

  • 普通的序列化就是将可序列化的对象写入ObjectOutputStream,然后我们可以从ObjectInputStream读出对象。例如下面的例子就是利用序列化的特性来复制一个对象:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.Serializable;

public class Test {
    public static void main(String args[]) {
        Bean bean1 = new Bean();
        bean1.s = new String("hello");
        Bean bean2 = copy(bean1);
        System.out.println(bean2.i);
        System.out.println(bean2.s);
        System.out.println(bean1 == bean2);
        bean1.s = "world";
        System.out.println(bean2.s);
    }
    private static<T extends Serializable> T copy(T obj) {
        PipedOutputStream pos = null;
        PipedInputStream pis = null;
        try {
            //此外还可以使用ByteArrayOutputStream生成字节数组,然后从ByteArrayInputStream读出
            pos = new PipedOutputStream();
            pis = new PipedInputStream(pos);
            ObjectOutputStream ous = new ObjectOutputStream(pos);
            ObjectInputStream ois = new ObjectInputStream(pis);
            ous.writeObject(obj);
            return (T) ois.readObject();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                pos.close();
                pis.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}
//Serializable是一个标记接口,代表可被序列化
class Bean implements Serializable {
    int i;
    String s;
}

序列化的控制

  • 默认的序列化自然简单,但倘若我们只想序列化一部分域,那该如何控制呢?在特殊情况下,可通过实现Externalizable接口来对序列化过程进行控制。这个接口继承了Serializable接口,同时增添了两个新方法:writeExternal()readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用。我们稍稍修改上面的例子,再来做一个例子:
import java.io.*;

public class Test {
    public static void main(String args[]) throws Exception {
        Bean1 bean1 = new Bean1();
        Bean2 bean2 = new Bean2();

        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(pos);
        ObjectOutputStream ous = new ObjectOutputStream(pos);
        ObjectInputStream ois = new ObjectInputStream(pis);

        ous.writeObject(bean1);
        ous.writeObject(bean2);
        ois.readObject();
        ois.readObject();
        pos.close();
        pis.close();
    }
}

class Bean1 implements Externalizable {
    public Bean1() {
        System.out.println("Bean1 Constructor");
    }
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Bean1 write");
    }
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        System.out.println("Bean1 read");
    }
}
class Bean2 implements Externalizable {
    public Bean2() {
        System.out.println("Bean2 Constructor");
    }
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Bean2 write");
    }
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        System.out.println("Bean2 read");
    }
}
------------------运行结果
Bean1 Constructor
Bean2 Constructor
Bean1 write
Bean2 write
Bean1 Constructor
Bean1 read
Bean2 Constructor
Bean2 read
  • 我惊奇的发现ExternalizablereadObject的时候居然会调用构造器。于是我有几个问题:
    1. 改变构造器的访问权限会如何?
    2. 没有默认构造器会如何?
  • 答:在read的时候都会抛java.io.InvalidClassException异常,异常信息是no valid constructor。看来Externalizable对象必须要有无参构造器且必须是public的(猜测readObject()方法中利用反射做了一些操作)。书上也说明了Externalizable对象在反序列化时先要调用构造器(包括字段处的初始化),然后调用readExternal()。下面是一个正确的Externalizable序列化的例子:
import java.io.*;

public class Test {
    public static void main(String args[]) throws Exception {
        Bean1 bean1 = new Bean1();
        bean1.i = 10;
        bean1.s1 = new String("hello world");
        bean1.s2 = new String("change");
        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(pos);
        ObjectOutputStream ous = new ObjectOutputStream(pos);
        ObjectInputStream ois = new ObjectInputStream(pis);

        ous.writeObject(bean1);
        Bean1 bean1_copy = (Bean1) ois.readObject();
        System.out.println(bean1_copy.i);//10
        System.out.println(bean1_copy.s1);//hello world
        System.out.println(bean1_copy.s2);//是defalut而不是change
        pos.close();
        pis.close();
    }
}

class Bean1 implements Externalizable {
    public int i;
    public String s1;
    public String s2 = "defalut";//这个值我们不传
    public Bean1() {
        System.out.println("Bean1 Constructor");
    }
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Bean1 write");
        out.writeInt(i);
        out.writeObject(s1);
    }
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        System.out.println("Bean1 read");
        i = in.readInt();
        s1 = (String) in.readObject();
    }
}
--------------------运行结果
Bean1 Constructor
Bean1 write
Bean1 Constructor
Bean1 read
10
hello world
defalut
  • 相信看了这个例子以后,我们就该知道Externalizable到底是如何工作了的吧?其实我们操作了Object的每一个域的动作,这样我们想要传就传,不传就不传,控制起来就很方便了。
  • 虽然Externalizable可以完成这种控制工作,Java也提供了一种在Serializable中控制的方法,使用transient(瞬时)关键字即可。
import java.io.*;

public class Test {
    public static void main(String args[]) throws Exception {
        Bean1 bean1 = new Bean1();
        bean1.username = "miaoch";
        bean1.password = "就不告诉你";
        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(pos);
        ObjectOutputStream ous = new ObjectOutputStream(pos);
        ObjectInputStream ois = new ObjectInputStream(pis);

        ous.writeObject(bean1);
        Bean1 bean1_copy = (Bean1) ois.readObject();
        System.out.println(bean1_copy.username);//"miaoch"
        System.out.println(bean1_copy.password);//null 我还以为是default呢
        System.out.println(bean1_copy.i);//0 再次证明Serializable不会走字段处的初始化及构造器
        pos.close();
        pis.close();
    }
}

class Bean1 implements Serializable {
    public String username;
    public transient String password = "defalut";//这个值我们不传
    public transient int i = 100;//这个值我们不传
    public Bean1() {
        System.out.println("Bean1 Constructor");//read的时候不会执行
    }
}
  • 在测试父类的时候,我发现一个奇怪的现象:
import java.io.*;

public class Test {
    public static void main(String args[]) throws Exception {
        Bean1 bean1 = new Bean1();
        bean1.username = "miaoch";
        bean1.password = "就不告诉你";
        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(pos);
        ObjectOutputStream ous = new ObjectOutputStream(pos);
        ObjectInputStream ois = new ObjectInputStream(pis);

        ous.writeObject(bean1);
        Bean1 bean1_copy = (Bean1) ois.readObject();
        System.out.println(bean1_copy.username);//"miaoch"
        System.out.println(bean1_copy.password);//null 我还因为是default呢
        System.out.println(bean1_copy.i);//0 再次证明Serializable不会走字段处的初始化
        System.out.println(bean1_copy.id);//2 除非Bean2 implements Serializable
        pos.close();
        pis.close();
    }
}

class Bean1 extends Bean2 implements Serializable {
    public String username;
    public transient String password = "defalut";//这个值我们不传
    public transient int i = 100;//这个值我们不传
    public Bean1() {
        System.out.println("Bean1 Constructor");//read的时候不会执行
    }
}
class Bean2 {
    public transient int id = 2;//transient无效了
    public Bean2() {
        System.out.println("Bean2 Constructor");//read的时候不会执行
    }
}
  • 更加复杂的情况就不讨论了(例如父类不实现Serializable,为什么子类照样能成功序列化)另外书中还告知了其实在Serializable可以添加(被反射判断了)下列两个方法:
    1. private void writeObject(ObjectOutputStream stream) throws IOException
    2. private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
  • 注意连权限修饰符也必须相同,下面是一个例子。他和Externalizable的区别就是不会调用构造器和字段处初始化。且此时的transient会失效(因为已由我们全权控制)。
import java.io.*;

public class Test {
    public static void main(String args[]) throws Exception {
        Bean1 bean1 = new Bean1();
        bean1.username = "miaoch";
        bean1.password = "就不告诉你";
        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(pos);
        ObjectOutputStream ous = new ObjectOutputStream(pos);
        ObjectInputStream ois = new ObjectInputStream(pis);

        ous.writeObject(bean1);
        Bean1 bean1_copy = (Bean1) ois.readObject();
        System.out.println(bean1_copy.username);//"miaoch"
        System.out.println(bean1_copy.password);//null 同样也不会执行字段处的初始化
        System.out.println(bean1_copy.i);//
        pos.close();
        pis.close();
    }
}

class Bean1 implements Serializable {
    public transient String username;
    public String password = "defalut";//这个值我们不传
    public int i = 100;//这个值我们不传
    public Bean1() {
        System.out.println("Bean1 Constructor");//read的时候不会执行
    }
    private void writeObject(ObjectOutputStream stream) throws IOException {
        System.out.println("writeObject");
        //stream.defaultWriteObject(); //这个是默认序列化行为,即考虑transient的一些序列化行为
        stream.writeObject(username);
    }
    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        System.out.println("readObject");
        //stream.defaultReadObject(); //这个是默认反序列化行为,必须和defaultWriteObject()一起用
        username = (String) stream.readObject();
    }
}
  • 不过最好不要这么做,也太混乱了。我只是觉得好玩就列在这了。

XML和Preferences

  • Java的序列化和反序列化只能在JVM上进行,而更普遍的做法是将Java对象转成XML或者JSON。这两种做法都很常见,只要去看看API就可以很容易的写出来了。
  • Preferences类似于Map、JSON,只能存储一些比较小的数据集合。它的特点是可以依附于一个节点。这个节点一般是一个Class对象,我们可以在其他类里通过这个节点(或者该节点的同级Class对象,其实只是为了获取路径)获得这个Preferences对象(windows中其实使用了注册表,这也是一种持久化)。
package test2;

import java.util.prefs.Preferences;
public class Test {
    public static void main(String args[]) throws Exception {
        Class.forName("test2.Demo");//加载这个类
        Preferences pref_user = Preferences.userNodeForPackage(Test.class);
        Preferences pref_system = Preferences.systemNodeForPackage(Test.class);
        //test2.file.FileTest更深1级
        Preferences pref_test = Preferences.systemNodeForPackage(test2.file.FileTest.class);

        System.out.println(pref_user.get("hello", null));
        System.out.println(pref_system.get("hello", null));
        System.out.println(pref_test.get("hello", null));//null

        System.out.println(pref_user.getInt("count", 0));//每次运行都会递增,哪怕程序已经终止过
    }
}
class Demo {
    static {
        //相当于用户变量和系统变量, 这个节点 同级都可以访问到
        Preferences pref_user = Preferences.userNodeForPackage(Demo.class);
        Preferences pref_system = Preferences.systemNodeForPackage(Demo.class);
        int count = pref_user.getInt("count", 0);
        pref_user.putInt("count", ++count);
        pref_user.put("hello", "pref_user");
        pref_system.put("hello", "pref_system");
        System.out.println("Preferences 已设置");
    }
}
  • preferences.usernodeforpackage代表得到hkey_current_user\software\javasoft\prefs下的相对路径
    preferences.systemnodeforpackage代表得到 hkey_local_machine\software\javasoft\prefs下的相对路径
    这里写图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值