序列流

SequenceInputStream的基本用法

有时候我们也称SequenceInputStream为合并流。因为他们主要是合并文件和切割文件的时候使用。
既然是序列,所以不会只有一个流。一般流都是成对出现的,但是打印流和序列流不一样,打印流只负责目的,序列流只负责源。
如果我们想要合并三个文本文档,可以分别创建三个流对象,对三个文本文件进行关联,然后一个一个的写。这样做是可以的,但是比较麻烦,可以将这些流对象使用集合存起来,遍历集合就拿到了流,然后分别对流对象进行操作,Java就将这些动作简化了。它把三个流对象进行了封装,合并成一个大流,第一个流对象遍历完成后返回-1判断后面是否还有流对象,如果有就继续遍历,如果没有,就用最后一个流对象返回的-1当做大流的返回值进行返回。
SequenceInputStream的构造函数要么是两个InputStream输入流,要么是输入流的枚举。

//目的:将三个文本文件1.txt,2.txt,3.txt的内容合并到一起,当做4.txt存储起来
FileInputStream fis1 = new FileInputStream("E:\\1.txt");
FileInputStream fis2 = new FileInputStream("E:\\2.txt");
FileInputStream fis3 = new FileInputStream("E:\\3.txt");

Vector<FileInputStream> v = new Vector<FileInputStream>();
v.addElement(fis1);
v.addElement(fis2);
v.addElement(fis3);
Enumeration<FileInputStream> en = v.elements();

SequenceInputStream sis = new SequenceInputStream(en);
FileOutputStream fos = new FileOutputStream("E:\\4.txt");

byte[] buf = new byte[1024];
int len = 0;

while((len = sis.read(buf)) != -1){
    fos.write(buf,0,len);
}

sis.close();
fos.close();

输出结果:

1111111111111111
11111111111111
11111111111111111111
2222222222222222
22222222222222
22222222222222222222
3333333333333333
33333333333333
33333333333333333333

但是Vector集合我们要慎用,并不是它已经过时,而是效率特别低。
其实工具类中已经帮我们写好了方法
public static < T> Enumeration< T> enumeration(Collection< T> c):返回一个指定 collection 上的枚举。
所以修改代码如下:

FileInputStream fis1 = new FileInputStream("E:\\1.txt");
FileInputStream fis2 = new FileInputStream("E:\\2.txt");
FileInputStream fis3 = new FileInputStream("E:\\3.txt");

ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();
al.add(fis1);
al.add(fis2);
al.add(fis3);
Enumeration<FileInputStream> en = Collections.enumeration(al);

SequenceInputStream sis = new SequenceInputStream(en);
FileOutputStream fos = new FileOutputStream("E:\\4.txt");

byte[] buf = new byte[1024];
int len = 0;

while((len = sis.read(buf)) != -1){
    fos.write(buf,0,len);
}

sis.close();
fos.close();

文件切割与合并

文件切割

//在E盘下的MP3文件,切割为1M大小一份的数据
public static void main(String[] args) throws IOException {
    File file = new File("E:\\一事无成.mp3");
    splitFile(file);
}

public static void splitFile(File file) throws IOException {
    FileInputStream fis = new FileInputStream(file);
    byte[] buf = new byte[1048576];
    int len = 0;
    int count = 1;
    FileOutputStream fos = null;

    File dir = new File("E:\\partfiles\\");
    if(!dir.exists())
        dir.mkdirs();
    while ((len=fis.read(buf))!=-1) {
        fos = new FileOutputStream(new File(dir,(count++)+".part"));
        fos.write(buf, 0, len);
        count++;
    }
    fos.close();
    fis.close();
}

运行程序,产生了partfiles文件夹,包含8个.part文件

文件合并

public static void main(String[] args) throws IOException {
    File dir= new File("E:\\partfiles");
    mergeFile(dir);
}

public static void mergeFile(File dir) throws IOException {
    ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();

    for(int x = 1 ;x < 9 ;x++){
        al.add(new FileInputStream(new File(dir,x+".part")));
    }

    Enumeration<FileInputStream> en = Collections.enumeration(al);
    SequenceInputStream sis = new SequenceInputStream(en);

    FileOutputStream fos = new FileOutputStream("E:\\partfiles\\9.MP3");
    byte[] buf = new byte[1024];
    int len = 0;

    while ((len = sis.read(buf))!=-1) {
        fos.write(buf, 0, len);
    }
    sis.close();
    fos.close();
}

运行之后8个.part文件重新合并为一个MP3文件,大小和原先的一样。

增加配置信息

在上面的演示中虽然我们成功的将文件切割然后合并在一起,但是有一个问题,就是在实际操作的时候,我们并不知道源文件将要被切割成多少份,以及源文件究竟是什么格式的文件。所以这些信息我们都应该在切割完成后用一个配置信息文件保存起来,在合并之前读取配置信息再进行文件合并。

//文件切割
public static void main(String[] args) throws IOException {
    File file = new File("E:\\一事无成.mp3");
    splitFile(file);
}

public static void splitFile(File file) throws IOException {
    FileInputStream fis = new FileInputStream(file);
    byte[] buf = new byte[1048576];
    int len = 0;
    int count = 1;
    FileOutputStream fos = null;
    Properties prop = new Properties();

    File dir = new File("E:\\partfiles\\");
    if(!dir.exists())
        dir.mkdirs();
    while ((len=fis.read(buf))!=-1) {
        fos = new FileOutputStream(new File(dir,(count++)+".part"));
        fos.write(buf, 0, len);
    }
    //保存源文件的名称和切割份数
    prop.setProperty("filename", file.getName());
    prop.setProperty("partcount", count+"");
    fos = new FileOutputStream(new File(dir,count+".properties"));
    prop.store(fos,"info");

    fos.close();
    fis.close();
}
//文件合并
public static void main(String[] args) throws IOException {
    File dir = new File("E:\\partfiles");
    mergeFile_2(dir);
}

public static void mergeFile_2(File dir) throws IOException {
    //获取配置信息文件
    File[] files = dir.listFiles(new SuffixFilter(".properties"));
    if(files.length!=1)
        throw new RuntimeException(dir+",该目录下没有properties扩展名的文件或者不唯一");
    //记录配置文件对象。
    File confile = files[0];

    //将配置信息文件的内容读取到集合中
    Properties prop = new Properties();
    FileInputStream fis = new FileInputStream(confile);
    prop.load(fis);

    //获取集合中的信息
    String filename = prop.getProperty("filename");
    int partcount = Integer.parseInt(prop.getProperty("partcount"));

    //获取所有的碎片文件
    File[] partFiles = dir.listFiles(new SuffixFilter(".part"));
    if(partFiles.length!=partcount-1)
        throw new RuntimeException(" 碎片文件不符合要求,个数不对!应该"+partcount+"个");

    //合并文件
    ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();
    for(int x = 0 ;x < partFiles.length  ;x++){
        al.add(new FileInputStream(partFiles[x]));
    }

    Enumeration<FileInputStream> en = Collections.enumeration(al);
    SequenceInputStream sis = new SequenceInputStream(en);

    FileOutputStream fos = new FileOutputStream(new File(dir,filename));
    byte[] buf = new byte[1024];
    int len = 0;

    while ((len = sis.read(buf))!=-1) {
        fos.write(buf, 0, len);
    }
    sis.close();
    fos.close();
}

对象序列化

Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。
既然要把对象序列化,就要把对象永久的保存在硬盘上,就要有能操作对象的流,ObjectInputStream和ObjectOutputStream。对象写到硬盘上是以文件的形式存在的,而操作文件的流是FileInputStream和FileOutpuStream。以前我们想要缓冲的功能就把把FileOutputStream挂到缓冲区上,现在需要操作对象的功能,所以就需要把FileOutputStream挂到操作对象的功能上。

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj.object"));

标准规范中,对象保存在硬盘上,存储对象的文件的扩展名是.object。在序列化的时候,对象的默认序列化机制写入:对象的类,类签名以及非瞬态和非静态字段的值。
被序列化的对象必须实现Serializable接口,但不需要覆盖任何方法,仅用于标识可序列化。并且一个类在实现Serializable接口之后为了防止.object对象和类之间版本不匹配,就会产生一个serialVersionUID 的版本号,在序列化的时候产生的.object也带着这个ID。如果类做了改变,ID就重新计算,也就改变了,这样和.object对象的ID号就不一致了,系统不允许解析。
这个ID号系统会自动生成,不过有时候由于计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会造成两个ID不一致。所以强烈建议我们自己声明ID。

public class Person implements Serializable/*标记接口*/ {
    private static final long serialVersionUID = 9527l;
    private String name;
    private int age;

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    //省略get和set方法
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
    //writeDemo();先运行writeDemo产生.object文件,再运行readDemo
    readDemo();
}

public static void readDemo() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\a.object")); 
    Person p = (Person)ois.readObject();
    System.out.println(p.getName()+":"+p.getAge());
    ois.close();
}

public static void writeDemo() throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\a.object"));
    oos.writeObject(new Person("小强",30));
    oos.close();
}

输出结果:

小强:30

如果是静态数据,静态数据在静态方法区,不在堆内存中,而对象存储于堆内存,序列化和静态无关,所以不能写进.object文件。
如果有些数据不想写进.object,而且还是特有数据,不能静态,这时候只要被transient关键字修饰就可以了。

public class Person implements Serializable/*标记接口*/ {
    private static final long serialVersionUID = 9527l;
    private transient String name;
    private static int age;
    //省略构造函数,get和set方法
}

运行程序,输出结果:

null:0