Java基础—IO流(三)

IO流(三) File类

一、概述

File类用于将文件或文件夹封装成对象,方便对文件和文件夹的属性信息进行操作。该类可以作为参数传递给IO流的构造函数,弥补流对象在操作文件和文件夹上的缺陷。

二、File类的使用

1.构造方法
1)File(String FileName)
示例:File f1 = new File("C:\\abc\\a.txt");

2)File(Strng, parent, String FileName)
示例:File f2 = new File("C:\\abc", "b.txt");
该构造方法的好处在于对文件的操作更加灵活,出现文件目录固定,文件名需要改变的时候,这个构造方法更好

3)File(File parent, String FileName)
示例:File d = new File("C:\\abc");
 File f3 = new File(d, "c.txt");

示例代码:

package com.heisejiuhuche.io;

import java.io.File;

public class FileDemo {
    public static void main(String[] args) {
        /* 使用File类的不同构造方法封装文件对象 */
        File f1 = new File("Demo1.txt");
        File f2 = new File("C:/Users/jeremy/Documents/javaTmp/", "Demo2.txt");
        File d = new File("C:/Users/jeremy/Documents/javaTmp/");
        File f3 = new File(d, "Demo3.txt");

        /* 打印各个文件对象 */
        System.out.println("f1:" + f1);
        System.out.println("f2:" + f2);
        System.out.println("f3:" + f3);

    }
}

程序输出结果:

f1:Demo1.txt
f2:C:\Users\jeremy\Documents\javaTmp\Demo2.txt
f3:C:\Users\jeremy\Documents\javaTmp\Demo3.txt

打印封装文件对象时的绝对路径或相对路径。

2.成员方法
1)创建操作
-boolean createNewFile():文件名不存在的情况下创建新文件
-boolean mkdir():创建一级目录
-boolean mkdirs():创建多级目录

2)删除操作
-boolean delete():删除指定文件
-void deleteOnExit():虚拟机退出的时候删除指定文件

3)判断操作
-boolean canExecute():判断文件对象是否可执行
-boolean canRead():判断文件对象是否可读
-boolean canWrite():判断文件对象是否可写
-boolean exists():判断文件对象是否存在
-boolean isDirectory():判断文件对象是否是文件夹,判断之前,必须先判断该文件对象是否存在
-boolean isFile():判断文件对象是否是文件,判断之前,必须先判断该文件对象是否存在
-boolean isHidden():判断文件对象是否是隐藏文件
-boolean isAbsolute():判断文件对象路径是否是绝对路径
-int compareTo(File pathname):比较两个文件对象,以自然顺序排序

4)获取操作
-String getName():获取文件对象名
-String getParent():获取文件对象的父母路,必须明确指定文件路径
-String getPath():获取文件对象相对路径
-String getAbsolutePath():获取文件对象绝对路径,文件可以存在也可以不存在
-long lastModified():获取文件对象最近一次被修改的时间
-long length():获取文件对象大小

5)修改操作
-boolean renameTo(File dest):将文件修改为指定文件名并存入指定目录

6)其他重要方法
-static File[] listRoots():获取有效盘符
-String[] list():获取指定目录中的文件和文件夹,包括隐藏文件;必须是存在目录调用,文件调用会空指针
-String[] list(FilenameFileter filter):获取指定格式的文件
-File[] listFiles()
-File[] listFiles(FileFilter filter)
-File[] listFiles(FilenameFilter filter)

3.递归
用递归遍历文件夹里的所有内容并以层级结构打印。

示例代码:

package com.heisejiuhuche.io;

import java.io.File;

public class RecursionDemo {
    public static void main(String[] args) {
        File dir = new File("C:/Users/jeremy/Documents/javaTmp/testfile");
        recur(dir, 0);
    }

    /* 目录层级结构 */
    private static String getLevel(int level) {
        StringBuilder sb = new StringBuilder();
        /* 树形结构图形 */
        sb.append("|--");
        for(int x= 0; x < level; x++) {
            /* 根据层级的不同在树形结构图形前补上制表符 */
            sb.insert(0, "\t");
        }
        return sb.toString();
    }

    /* 递归遍历文件夹,并打印所有文件和文件夹 */
    private static void recur(File dir, int level) {
        /* 列出所有文件到文件数组 */
        File[] files = dir.listFiles();
        /* 打印文件夹的名称 */
        System.out.println(getLevel(level) + dir.getName());
        /* 打印完文件夹的名称,层级自增1 */
        level++;
        /* 遍历所有文件,如果是文件夹,则递归遍历里面的文件和文件夹 */
        for(int x = 0; x < files.length; x++) {
            if(files[x].isDirectory()) {
                recur(files[x], level);
            }
            else
                System.out.println(getLevel(level) + files[x].getName()); //如果不是文件夹,打印文件
        }
    }
}

程序输出结果:

|--testfile
    |--aaa
        |--ccc
            |--ddd
                |--h.txt
            |--ddd - Copy (2).txt
            |--ddd - Copy - Copy.txt
            |--ddd - Copy.txt
            |--ddd.txt
        |--ccc - Copy (2).txt
        |--ccc - Copy - Copy.txt
        |--ccc - Copy.txt
        |--ccc.txt
    |--aaa - Copy (2).txt
    |--aaa - Copy - Copy.txt
    |--aaa - Copy.txt
    |--aaa.txt
    |--bbb
        |--eee
            |--f.txt
        |--eee - Copy (2).txt
        |--eee - Copy - Copy.txt
        |--eee - Copy.txt
        |--eee.txt

注意:
递归的使用必须要明确控制条件,便面无限循环;递归要慎重使用,调用次数过多,会造成内存溢出。

4.删除带内容目录

示例代码:

package com.heisejiuhuche.io;

import java.io.File;

public class DelDirWithContentDemo {
    public static void main(String[] args) {
        File dir = new File("C:/Users/jeremy/Documents/javaTmp/testfile");

        recur(dir);
    }

    /* 遍历所有文件夹,删除文件,并删除文件夹 */
    private static void recur(File dir) {
        File[] files = dir.listFiles();

        for(int x = 0; x < files.length; x++) {
            if(files[x].isDirectory())
                recur(files[x]);
            else
                /* 打印文件名及文件删除结果 */
                System.out.println(files[x].getName() + "--files--" + files[x].delete());
        }
        /* 打印文件夹名及文件夹删除结果 */
        System.out.println(dir.getName() + "**dir**" + dir.delete());
    }
}

程序输出结果:

demo - Copy (2) - Copy.txt--files--true
ddd**dir**true
demo - Copy (2) - Copy - Copy - Copy.txt--files--true
demo - Copy (2) - Copy - Copy.txt--files--true
ccc**dir**true
demo - Copy (2) - Copy - Copy.txt--files--true
demo - Copy (2) - Copy.txt--files--true
demo - Copy (2).txt--files--true
demo - Copy (3) - Copy.txt--files--true
aaa**dir**true
demo - Copy - Copy - Copy.txt--files--true
demo - Copy - Copy.txt--files--true
demo - Copy - Copy - Copy.txt--files--true
eee**dir**true
bbb**dir**true
demo - Copy (2) - Copy.txt--files--true
demo - Copy (2).txt--files--true
demo - Copy - Copy (2).txt--files--true
demo - Copy - Copy (3).txt--files--true
demo - Copy - Copy - Copy.txt--files--true
demo - Copy - Copy.txt--files--true
demo - Copy.txt--files--true
demo.txt--files--true
testfile**dir**true

注意:
Java无法访问windows中的隐藏目录,所以,最好在for循环中判断的时候加上:
if(!files[x].isHidden() && files[x].isDirectory())
忽略隐藏的文件夹

5.练习
FileWriter接收File的构造方法。

示例代码:

package com.heisejiuhuche.io;

/**
 * 接收键盘录入,然后写入指定目录下的文件中
 * 为了测试FileWriter接收File类型数据作为构造方法参数
 */

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;

public class FileWriterTest {
    public static void main(String[] args) {
        try {
            writeToFile(createFile("C:\\Users\\jeremy\\Documents\\FileWriter.txt"));
        } catch(IOException e) {
            e.printStackTrace();
        }
    }

    private static File createFile(String filepath) throws IOException {
        File file = new File(filepath);
        if(!file.exists())
            file.createNewFile();
        return file;
    }

    private static void writeToFile(File file) {
        BufferedReader bufr = null;
        BufferedWriter bufw = null;

        try {
            bufr = new BufferedReader(new InputStreamReader(System.in));
            bufw = new BufferedWriter(new FileWriter(file));

            String line = null;

            while((line = bufr.readLine()) != null) {
                if(line.equals("over"))
                    break;
                bufw.write(line);
                bufw.newLine();
                bufw.flush();
            }
        } catch(IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(bufr != null)
                    bufr.close();
            } catch(IOException e) {
                e.printStackTrace();
            }
            try {
                if(bufw != null)
                    bufw.close();
            } catch(IOException e) {
                e.printStackTrace();
            }
        } 
    }
}

其他类及功能性流对象

一、Properties类

1.概述
Properties类是HashTable的子类。该类具备Map集合的特点,储存字符串作为键值对。Properties类是集合中和IO技术相结合的容器。该类可以用于键值对形式的配置文件。配置文件用于保存软件运行时的各项参数。配置完成之后将会被持久化存储,每次运行软件都会加载该配置文件。

2.应用
1)设置并打印键值对

示例代码:

package com.heisejiuhuche.io;

import java.util.Properties;
import java.util.Set;

public class PropertiesDemo2 {
    public static void main(String[] args) {
        /* 创建Property对象 */
        Properties prop = new Properties();
        /* 设置键值对 */
        prop.setProperty("zhangsan", "18");
        prop.setProperty("wangwu", "20");
        /* 通过键获取值,并打印 */
        System.out.println(prop.getProperty("zhangsan"));

        /* 将键都返回并装进Set */
        Set<String> value = prop.stringPropertyNames();
        /* 遍历集合并打印键值 */
        for(String str : value) {
            System.out.println(str + "::" + prop.getProperty(str));
        }
    }
}

程序输出结果:

18
zhangsan::18
wangwu::20

2)从文件加载配置并修改

示例代码:

package com.heisejiuhuche.io;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Properties;

public class PropertiesLoadDemo {
    public static void main(String[] args) throws IOException {
        Properties prop = new Properties();
//        loadProp();
        /* 创建缓冲输入流对象并关联配置文件 */
        BufferedReader bufr = new BufferedReader(new FileReader(
                "C:/Users/jeremy/Documents/javaTmp/info.txt"));
        /* 调用prop独享的load方法加载配置文件 */
        prop.load(bufr);
        /* 在控制台列出所有键值对 */
        prop.list(System.out);

        /* 修改键值对 */
        prop.setProperty("李四", "50");
        /* 调用store方法修改配置文件并保存 */
        prop.store(new OutputStreamWriter(new FileOutputStream(
                "C:/Users/jeremy/Documents/javaTmp/info.txt"), "GBK"), "Test");
        prop.list(System.out);

    }
    /* 方法一:读取文件,将每一行按=号分割,将键值对存入Properties对象 */
    private static void loadProp() {
         BufferedReader bufr = null;
         Properties prop = null;
         try {
             bufr = new BufferedReader(new FileReader("C:/Users/jeremy/Documents/javaTmp/info.txt"));
             prop = new Properties();
             String line = null;

             while((line = bufr.readLine()) != null) {
                 String[] kv = line.split("=");
                 prop.setProperty(kv[0], kv[1]);
             }
         } catch(IOException e) {
             e.printStackTrace();
         } finally {
             try {
                 if(bufr != null)
                     bufr.close();
             } catch(IOException e) {
                 e.printStackTrace();
             }
         }
         System.out.println(prop);
    }
}

程序输出结果:

-- listing properties --
王五=19
张三=20
李四=90
-- listing properties --
王五=19
张三=20
李四=50

注意:
-#都是注释,不会被Properties加载
-Properties加载的配置文件必须有固定格式;通常为:键=值

3)记录程序运行次数
创建配置文件记录程序运行次数,到达5此,提示用户注册。

示例代码:

package com.heisejiuhuche.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

public class PropertiesTest {
    public static void main(String[] args) throws IOException {
        try {
            checkAuth();
        } catch(IOException e) {
            throw new RuntimeException("配置文件加载异常...");
        }
    }

    private static void checkAuth() throws IOException {
        /* 创建配置文件对象 */
        File file = new File("C:/Users/jeremy/Documents/javaTmp/auth.txt");
        /* 如果文件不存在,创建文件,避免异常 */
        if(!file.exists())
            file.createNewFile();
        /* 创建输出流对象 */
        FileInputStream fis = new FileInputStream(file);
        /* 创建Properties对象 */
        Properties prop = new Properties();
        /* 加载配置文件 */
        prop.load(fis);

        String val = null;
        int count = 0;
        /* 如果读取键值为空,说名是第一次运行,那么将计数器count+1,然后和键time一起存入prop对象
         * 并写入配置文件
         */
        if((val = prop.getProperty("time")) != null) {
            /* 如果取到了值,说明不是第一次运行,那么让计数器count等于time键所对应的值
             * 再+1,然后和time键一起再次存入配置文件
             */
            count = Integer.parseInt(val);
            /* 判断,如果count = 5,说明使用次数已到,结束程序,提示注册 */
            if(count >= 5) {
                System.out.println("请注册...");
                return;
            }
        }

        count++;

        prop.setProperty("time", count + "");
        /* 输出流不能在load语句前声明,否则将会覆盖配置文件,导致配置信息清空 */
        FileOutputStream fos = new FileOutputStream(file);
        /* 将配置文件更新 */
        prop.store(fos, "CheckAuth");

        fis.close();
        fos.close();

    }
}

二、打印流

1.PrintStream类
1)概述
PrintStream类是字节打印流,其为其他流添加了功能,使它们能够方便打印各种数据值形式。该类不会抛出IOException,同时内部有刷新机制,无须手动刷新缓冲区。PrintStream可以直接操作文件对象。

2)常用方法
-PrintStream(File file):接收File对象
-PrintStream(OutputStream out):接收字节输出流
-PrintStream(String filename):接收字符串路径
-println():打印所有基本数据类型,保持数据原样

3)PrintStream示例

示例代码:

package com.heisejiuhuche.io;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;

public class PrintStreamDemo {
    public static void main(String[] args) throws IOException {
        /* 创建输入流和PrintStream对象并关联文件 */
        BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
        PrintStream ps = new PrintStream("C:/Users/jeremy/Documents/javaTmp/PrintStream.txt");

        String len = null;

        /* 接收键盘输入并写入文件 */
        while((len = bufr.readLine()) != null) {
            if(len.equals("over"))
                break;
            ps.println(len);
            ps.flush();
        }
        bufr.close();
        ps.close();
    }
}

该类的使用方法和下面介绍的PrintWriter类基本相同。

2.PrintWriter类
1)概述
字符打印流可以打印基本舒蕾型。其最强大的功能是println方法,可以实现自动换行并直接操作基本数据类型。

2)常用方法
-PrintWriter(File file):接收File对象
-PrintWriter(File file, String csn):接收File对象,并制定字符集
-PrintWriter(OutputStream out):接收字节输出流
-PrintWriter(OutputStream out, boolean autoflush):接收字节输出流,设置自动刷新
-PrintStream(Writer out):接收字符输出流
-PrintWriter(String filename):接收字符串路径

3)PrintWriter示例

示例代码:

package com.heisejiuhuche.io;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;

public class PrintWriterDemo {
    public static void main(String[] args) throws IOException {
        /* 创建输入流对象和打印流对象 */
        BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter pw = new PrintWriter(System.out, true);

        String len = null;

        while((len = bufr.readLine()) != null) {
            /* 如果len等于over,就停止程序 */
            if(len.equals("over"))
                break;
            /* 调用println方法,在打印的时候自动换行 */
            pw.println(len.toUpperCase());
        }

        bufr.close();
        pw.close();
    }
}

注意:
在PrintWriter构造方法中设置true,就无须调用flush()刷新缓冲。

三、序列流

1.SequenceInputStream类
1)概述
SequenceInputStream类标识其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直到达到文件末尾;接着从第二个输入流读取,以此类推,直到到达包含的最后一个输入流的文件末尾为止。

2)应用
将三个文件的内容使用SequenceInputStream写入到一个文件中。

示例代码:

package com.heisejiuhuche.io;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;

public class SequenceInputStreamDemo {
    public static void main(String[] args) {
        write();
    }

    private static void write() {
        SequenceInputStream sis = null;
        ArrayList<FileInputStream> list = null;
        FileOutputStream fos = null;

        try {
            /* 创建集合对象 */
            list = new ArrayList<FileInputStream>();
            /* 将输入流对象存入集合 */
            for(int x = 1; x <= 3; x++) {
                list.add(new FileInputStream(
                "C:\\Users\\jeremy\\Documents\\javaTmp\\mp\\" + x + ".txt"));
            }

            Iterator<FileInputStream> it = list.iterator();
            /* 得到装有输入流对象的Enumeration */
            Enumeration<FileInputStream> en = new Enumeration<FileInputStream>() {
                public boolean hasMoreElements() {
                    return it.hasNext();
                }

                public FileInputStream nextElement() {
                    return it.next();
                }
            };
            /* 创建序列流对象和输出流对象 */
            sis = new SequenceInputStream(en);
            fos = new FileOutputStream("C:\\Users\\jeremy\\Documents\\javaTmp\\mp\\4.txt");

            byte[] buf = new byte[1024];

            int len = 0;
            /* 合并文件 */
            while((len = sis.read(buf)) != -1) {
                fos.write(buf, 0, len);
                fos.flush();
            }
        } catch(IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(sis != null)
                    sis.close();
            } catch(IOException e) {
                e.printStackTrace();
            }
            try {
                if(fos != null)
                    fos.close();
            } catch(IOException e) {
                e.printStackTrace();
            }
        }
    }
}

3)切割文件后再合并
将一个MP3文件按1M切割,最后合并,要求保证合并后的文件可以播放。

示例代码:

package com.heisejiuhuche.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;

public class SplitMp3Test {
    public static void main(String[] args) {
        split();
        merge();
    }

    private static void merge() {
        /* 创建集合,用于存放输入流对象 */
        ArrayList<FileInputStream> list = new ArrayList<FileInputStream>();
        SequenceInputStream sis = null;
        FileOutputStream fos = null;

        try {
            /* 将四个文件输入流对象存入集合 */
            for(int x = 1; x <= 4; x++) {
                list.add(new FileInputStream("C:\\Users\\jeremy\\Documents\\javaTmp\\mp\\" + x + ".part"));
            }

            Iterator<FileInputStream> it = list.iterator();
            /* 拿到有输入流对象的Enumeration */
            Enumeration<FileInputStream> en = new Enumeration<FileInputStream>() {
                public boolean hasMoreElements() {
                    return it.hasNext();
                }

                public FileInputStream nextElement() {
                    return it.next();
                }
            };

            /* 创建序列流对象 */
            sis = new SequenceInputStream(en);
            /* 创建输出流对象,并关联文件 */
            fos = new FileOutputStream("C:\\Users\\jeremy\\Documents\\javaTmp\\mp\\back.mp3");

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

            /* 合并文件 */
            while((len = sis.read(buf)) != -1) {
                fos.write(buf, 0, len);
                fos.flush();
            }
        } catch(FileNotFoundException e) {
            throw new RuntimeException("文件不存在..."); 
        } catch(IOException e) {
            throw new RuntimeException("合并失败...");
        } finally {
            try {
                if(sis != null)
                    sis.close();
            } catch(IOException e) {
                throw new RuntimeException("资源关闭失败...");
            }
            try {
                if(fos != null)
                    fos.close();
            } catch(IOException e) {
                throw new RuntimeException("资源关闭失败...");
            }
        }
    }

    private static void split() {
        File file = null;
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            /* 创建输入流对象 */
            file = new File("C:\\Users\\jeremy\\Documents\\javaTmp\\Backseat.mp3");
            fis = new FileInputStream(file);

            byte[] buf = new byte[1024 * 1024];

            int count = 1;
            int len = 0;
            /* 将文件按1M大小分割,分为count个文件 */
            while((len = fis.read(buf)) != -1) {
                fos = new FileOutputStream("C:\\Users\\jeremy\\Documents\\javaTmp\\mp\\" + count++ + ".part");
                fos.write(buf, 0, len);
                fos.flush();
            }
        } catch(IOException e) {
            throw new RuntimeException("分割失败...");
        } finally {
            try {
                if(fis != null)
                    fis.close();
            } catch(IOException e) {
                throw new RuntimeException("输入资源关闭失败");
            }
            try {
                if(fos != null)
                    fos.close();
            } catch(IOException e) {
                throw new RuntimeException("输出资源关闭失败");
            }
        }
    }
}

四、对象流

1.ObjectOutputStream和ObjectInputStream类
1)概述
ObjectOutputStream类可以将Java对象的基本数据类型和图形写入OutputStream,再利用ObjectInputStream读取(重构)该对象。该类用于将对象持久化存储在硬盘上,以便程序下次启动的时候能再次加载该对象。

2)常用方法
ObjectOutputStream具备直接操作基本数据类型的一些列write()方法及操作对象的writeObject()方法。以一个示例来演示该类的使用方法及注意事项。

示例代码:

package com.heisejiuhuche.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ObjectOutputInputStreamDemo {
    static final File file = new File("C:/Users/jeremy/Documents/javaTmp/person.txt");
    public static void main(String[] args) throws Exception {
        writeObj();
    }

    private static void readObj() throws Exception {
        /* 创建对象输入流对象,传入一个字节输入流,并关联文件 */
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        /* 读取Person对象,向下转型为Person类型 */
        Person p = (Person)ois.readObject();
        /* 打印Person对象 */
        System.out.println(p);
        /* 关闭资源 */
        ois.close();
    }

    private static void writeObj() throws Exception {
        /* 创建对象输出流对象,传入一个字节输出流,并关联文件 */
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
        /* 调用writeObject方法将Person对象写入文件 */
        oos.writeObject(new Person("zhangsan", 28));
        /* 写入文件后读取并打印在控制台 */
        readObj();
        /* 关闭资源  */
        oos.close();
    }
}

class Person {
    private String name;
    private int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return this.name + "::" + this.age;
    }
}

注意:
上面的代码报出异常:
Exception in thread "main" java.io.NotSerializableException: com.heisejiuhuche.io.Person
在使用ObjectOutputStream写入对象的时候,被写入的对象必须实现Serializable接口

3)Serializable接口
Serializable接口是一个标记接口,它没有任何方法。该接口给对象定义一个固定的数字标识,将该对象类序列化。使用该标识作为对象存储和读取是否一致的判断标准。这个数字标识叫做SerialVersionUID,是根据每个对象的成员,由Java计算出来的一个固定值。如果读取的时候,该值发生了变化,会抛出异常。下面会用代码演示这一现象及序列化的其他特点。

修改上述代码,按要求使Person类实现Serializable接口。

示例代码:

class Person implements Serializable {

    private String name;
    private int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return this.name + "::" + this.age;
    }
}

程序运行结果:

zhangsan::28

此时,如果将Person类中name成员的private关键字去掉,在写入和读取时,就会发生UID不匹配的情况,抛出异常。

示例代码:

class Person implements Serializable {
    /* 去掉了private关键字 */
    String name;
    private int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return this.name + "::" + this.age;
    }
}

程序运行结果:

Exception in thread "main" java.io.InvalidClassException
这证明了UID是根据成员的数字标识算出来的一个固定值。

如果要实现更改成员之后还能读取该类,只需手动指定UID。下面的代码仍然去掉了private关键字,但是读取无误。

示例代码:

package com.heisejiuhuche.io;

import java.io.Serializable;

class Person implements Serializable {
    /* 手动指定UID */
    private static final long serialVersionUID = 37L;

     String name;
    private int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return this.name + "::" + this.age;
    }
}

程序运行结果:zhangsan::28

注意:
-已被赋值的静态成员,在序列化后,值不能修改;如,static String country = "cn";之后再修改country的值,读取的时候,还是"cn";
-不想序列化某非静态成员,可以加上transient关键字:transient private int age;

五、管道流

1.概述
管道流能通过结合线程技术,实现输入流和输出流的直接连接。管道输入和输出流必须连接在一起使用。某个线程从PipedInputStream读取数据,并由另一个线程写入到PipedOutputStream。这是涉及多线程技术的IO流

2.PipedInputStream和PipedOutputStream类
管道流可以通过构造方法连接,也可以通过调用connect()方法连接。

示例代码:

package com.heisejiuhuche.io;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class PipedStreamDemo {
    public static void main(String[] args) throws IOException {
        /* 创建管道流对象并连接 */
        PipedInputStream pis = new PipedInputStream();
        PipedOutputStream pos = new PipedOutputStream();
        pis.connect(pos);
        /* 启动线程 */
        new Thread(new Read(pis)).start();
        new Thread(new Write(pos)).start();
    }

}

class Read implements Runnable {
    private PipedInputStream pis;

    Read(PipedInputStream pis) {
        this.pis = pis;
    }

    public void run() {
        /* 等待管道输出流写入数据,读到输出流数据前,处于阻塞状态 */
        try {
            byte[] buf = new byte[1024];
            System.out.println("等待读取...");
            int len = pis.read(buf);
            System.out.println("读取结束...");
            System.out.println(new String(buf, 0, len));
        } catch(IOException e) {
            throw new RuntimeException("读取失败");
        }
    }
}

class Write implements Runnable {
    private PipedOutputStream pos;

    Write(PipedOutputStream pos) {
        this.pos = pos;
    }

    public void run() {
        /* 往管道输入流中写入数据,写入先等待6秒,模拟写入过程 */
        try {
            System.out.println("数据写入中...");
            Thread.sleep(6000);
            pos.write("数据来啦!!!!!".getBytes());
            pos.close();
        } catch(Exception e) {
            throw new RuntimeException("写入失败");
        }
    }
}

程序运行结果:

等待读取...
数据写入中...
读取结束...
数据来啦!!!!!

六、RandomAccessFile类

1.概述
RandomAccessFile类不是IO体系中的子类,而是直接继承自Object。但是它仍然是IO包中的成员,因为它具备读写功能。该类支持对随机访问文件的读取和写入。该类的内部封装了数组,通过文件指针对数据进行操作,可以通过getFilePointer()方法获取指针位置;通过seek()方法改变指针的位置。RandomAccessFile类只能操作文件,并必须设定操作模式。

2.应用
演示RandomAccessFile的基本方法及特点。

示例代码:

package com.heisejiuhuche.io;

import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileDemo {
    public static void main(String[] args) throws IOException {
        write();
        read();
    }

    private static void write() throws IOException {
        /* 创建RnadomAccessFile对象 */
        RandomAccessFile raf = new RandomAccessFile("C:/Users/jeremy/Documents/javaTmp/random.txt", "rw");

        raf.write("李四".getBytes());
        /* write()方法的特点是,只写出数据的最低8位,会造成数据丢失,出现乱码 */
//        raf.write(258);
        /* 保证数据不丢失,要调用writeInt()方法,写入4个8位 */
        raf.writeInt(97);
        raf.write("王五".getBytes());
        raf.writeInt(99);
        /* 让指针回到文件起始位置 */
        raf.seek(0);
        /* 写入数据,那么李四的数据将被修改 */
        raf.write("赵六".getBytes());
        raf.writeInt(103);
        raf.close();
    }

    private static void read() throws IOException {
        RandomAccessFile raf = new RandomAccessFile("C:/Users/jeremy/Documents/javaTmp/random.txt", "rw");
        /* 创建4个字节的数组 */
        byte[] buf = new byte[4];

        /* 如果想要直接取王五的数据,调用seek方法让指针指向第二个8位即可 */
        raf.seek(8);
        /* 如果想要直接取王五的数据,调用skepBytes方法跳过前8位 */
        raf.skipBytes(8);

        /* 读取4个字节到数组中 */
        raf.read(buf);
        /* 用readInt读取下4个字节 */
        int age = raf.readInt();

        System.out.println(new String(buf) + age);
    }
}

注意:
-write()方法只写数据的最低8位,会造成数据丢失;在写入int类型数据时,调用writeInt()方法;
-skipBytes()方法只能往前跳过指定字节数,不能往回跳转;
-RandomAccessFile对象不仅能写如数据,还能修改该数据;
-RandomAccessFile对象在创建时,如果模式为"r",就不会创建文件;如果该被读取文件不存在,会抛出异常;模式为"rw",会自动创建该文件;如果文件已存在,则不会覆盖,继续添加内容;
-RandomAccessFile类可以结合多线程,实现数据的分段写入,提高写入效率

七、其他流对象

1.DataInputStream和DataOutputStream类
用于操作基本数据类型的流对象。

示例代码:

package com.heisejiuhuche.io;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataStreamDemo {
    public static void main(String[] args) throws IOException {
        write();
        read();
        writeUTF();
        readUTF();
    }

    private static void readUTF() throws IOException {
        DataInputStream dos = new DataInputStream(new FileInputStream(
                "C:/Users/jeremy/Documents/javaTmp/utf-8.txt"));

        System.out.println(dos.readUTF());
    }

    private static void writeUTF() throws IOException {
        DataOutputStream dos = new DataOutputStream(new FileOutputStream(
                "C:/Users/jeremy/Documents/javaTmp/utf-8.txt"));
        /* 使用UTF-8修改版字符集写入数据 */
        dos.writeUTF("你好");

        dos.close();
    }

    private static void read() throws IOException {
        /* 创建DataInputStream对象并关联文件  */
        DataInputStream dos = new DataInputStream(new FileInputStream(
                "C:/Users/jeremy/Documents/javaTmp/data.txt"));
        /* 读取基本数据类型 */
        int num = dos.readInt();
        boolean b = dos.readBoolean();
        double d = dos.readDouble();

        System.out.println("num = " + num);
        System.out.println("b = " + b);
        System.out.println("d = " + d);
    }

    private static void write() throws IOException {
        /* 创建DataOutputStream对象并关联文件  */
        DataOutputStream dos = new DataOutputStream(new FileOutputStream(
                "C:/Users/jeremy/Documents/javaTmp/data.txt"));
        /* 写入基本数据类型 */
        dos.writeInt(234);
        dos.writeBoolean(false);
        dos.writeDouble(124.135252);

        dos.close();
    }
}

程序运行结果:

num = 234
b = false
d = 124.135252
你好

2.ByteArrayInputStream和ByteArrayOutputStream类
用于操作字节数组的流对象。该类的对象无须关闭,因为没有调用任何系统底层资源;如果关闭,仍可以调用其方法,同时没有任何IO异常ByteArrayInputStream在初始化的时候需要接收一个字节数组;ByteArrayOutputStream内部的缓冲区,会随着数据的不断写入而自动增长。这两个类用流的读写思想来操作数组,用于往内存中暂时写入数据。

示例代码:

package com.heisejiuhuche.io;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

public class ByteArrayStreamDemo {
    public static void main(String[] args) {
        readWrite();
    }

    private static void readWrite() {
        /* 创建字节数组流对象并关联文件 */
        ByteArrayInputStream bis = new ByteArrayInputStream("abcdefg".getBytes());
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        int ch = 0;
        /* 循环读取所有字节并写去字节数组输出流对象 */
        while((ch = bis.read()) != -1) {
            bos.write(ch);
        }

        /* 打印数组大小,并打印内容 */
        System.out.println(bos.size());
        System.out.println(new String(bos.toByteArray()));
        System.out.println(bos.toString());
    }
}

程序运行结果:

7
abcdefg
abcdefg

注意:
和字节数组输入输出流类似的流对象还有:

分类输入输出
字符数组流CharArrayReaderCharArrayWriter
字符串流StringReaderStringWriter
这些类的使用方式和字节数组流相似。

字符编码

一、概述

字符编码表是由计算机二进制数1010的排列组合和文字的对应关系形成的。它的出现可以让计算机识别各个国家不同的文字和符号。

二、常见字符编码表

1.ASCII码表
美国标准信息交换码,用一个字节的7位标识一个字符,最高位为0

2.ISO8859-1
拉丁码表(欧洲码表),用一个字节的8位标识一个字符。

3.GB2312和GBK
GB2312GBK都是中文字符编码表。后者是前者的升级版,囊括更多的中文文字和符号。

4.Unicode
国际标准码,融合了全世界多种文字和符号。

5.UTF-8
Unicode编码表的优化版,最多用3个字节标识一个字符。

三、乱码问题

乱码问题的产生,原因在于解码的时候没有使用编码时的编码表。比如,在写入中文数据的时候,指定了GBK编码表,但是在读取数据的时候却用了UTF-8编码表。GBK是两个字节表示一个字符,UTF-8是三个字节表示一个字符。如果输入的字符是“你好”,对应的GBK编码表上的数字是【-60,-29,-70,-61】;读取的时候误用了UTF-8编码表,那么会读取【-60,-29,-70】这三个字节,到UTF-8码表里面寻找有没有对应的字符,如果没有,返回?;接着拿【-61】去找,如果没有匹配的字符,返回?。反过来的过程相同。“你好”对应的UTF-8的数字是【-28,-67,-96,-27,-91,-67】,如果在读取时误用了GBK码表,那么就拿每两个数字去GBK表中查找对应字符,因此出现乱码。

四、编码解码

1.定义
编码就是将字符串转换为字节数组的过程;解码就是将字节数组转换为字符串的过程。

2.解决乱码问题
实际开发过程中,web服务器端通常默认使用ISO8859-1的编码表。如果出现使用GBK编码,而数据传到服务器端之后出现乱码的问题,只需要将乱码的字符串再次用ISO8859-1进行编码,在用GBK解码即可。

示例代码:

package com.heisejiuhuche.io;

public class EncodeDemo {
    public static void main(String[] args) throws Exception {
        String s1 = "你好";
        /* 使用gbk编码 */
        byte[] b1 = s1.getBytes("GBK");
        /* 使用iso8859-1解码 */
        String s2 = new String(b1, "ISO8859-1");
        /* 出现乱码???? */
        System.out.println(s2);
        /* 是同iso8859-1再次编码 */
        byte[] b2 = s2.getBytes("ISO8859-1");
        /* 使用gbk解码 */
        String s3 = new String(b2, "GBK");
        /* 还原字符串成功 */
        System.out.println(s3);
    }
}

程序运行结果:

????
你好

五、联通的问题

在文本文件中输入“联通”二字,保存退出;第二次打开时会出现乱码。

这里写图片描述

图中可见“联通”二字的二进制储存形式,符合UTF-8两个字节存储的格式要求;最高位分别为11010。所以当“联通”二字以GBK编码表形式编码存储后,记事本在读取的时候误认为是UTF-8编码表编码,会到UTF-8码表中查找字符,形成乱码。

六、练习

5个学生,每个学生有3们课程的成绩。从键盘录入以上数据,格式为:姓名,数学成绩,语文成绩,英语成绩;并计算出总成绩。将最后的学生信息和计算出的总分按从高到低的顺序存入文件“stud.txt”中。

示例代码:

package com.heisejiuhuche.io;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.Comparator;
import java.util.TreeSet;

public class StudentInfoToFileTest {
    public static void main(String[] args) {
        /* 反转比较器 */
        Comparator<Student> student = Collections.reverseOrder();
        /* 获得学生对象并写入文件 */
        StudentTools.infoToFile(StudentTools.getStudentInfo(student));
    }
}

class StudentTools {
    public static TreeSet<Student> getStudentInfo(Comparator<Student> comp) {
        BufferedReader bufr = null;
        try {
            /* 创建TreeSet对象,用于储存学生对象并排序  */
            TreeSet<Student> stuSet = null;
            /* 接收键盘录入 */
            bufr = new BufferedReader(new InputStreamReader(System.in));
            /* 判断有无比较器参数,分别创建有比较器和没有比较器的两个TreeSet对象 */
            if(comp == null)
                stuSet = new TreeSet<Student>();
            else
                stuSet = new TreeSet<Student>(comp);
            String tmp = null;
            /* 不断接收键盘按固定格式的录入,如果收到over,结束程序 */
            while((tmp = bufr.readLine()) != null) {
                if(tmp.equals("over"))
                    break;
                /* 将字符串按指定格式切割 */
                String[] info = tmp.split(",");
                /* 将每段信息传入学生对象封装 */
                Student stu = new Student(info[0], Integer.parseInt(info[1]), 
                                                    Integer.parseInt(info[2]), 
                                                    Integer.parseInt(info[3]));
                /* 将封装号的学生对象存入集合以便排序 */
                stuSet.add(stu);
            }
            return stuSet;
        } catch(IOException e) {
            throw new RuntimeException("文件读入失败...");
        } finally {
            try {
                if(bufr != null)
                    bufr.close();
            } catch(IOException e) {
                throw new RuntimeException("输入流关闭失败...");
            }
        }
    }

    public static void infoToFile(TreeSet<Student> stuSet) {
        File file = new File("C:/Users/jeremy/Documents/javaTmp/StudentInfo.txt");
        BufferedWriter bufw = null;

        try {
            bufw = new BufferedWriter(new FileWriter(file));
            /* 将集合中的数据写入文件 */
            for(Student stus : stuSet) {
                bufw.write(stus.toString() + "\t");
                bufw.write(stus.getSum() + "");
                bufw.newLine();
                bufw.flush();
            }
        } catch(IOException e) {
            throw new RuntimeException("文件写入失败...");
        } finally {
            try {
                if(bufw != null)
                    bufw.close();
            } catch(IOException e) {
                throw new RuntimeException("输入流关闭失败...");
            }
        }
    }
}

/* 创建学生类 */
class Student implements Comparable<Student> {
    private String name;
    private int math, cn, en, sum;

    Student(String name, int math, int cn, int en) {
        this.name = name;
        this.math = math;
        this.cn = cn;
        this.en = en;
        sum = math + cn + en;
    }

    public void setName(String name) { this.name = name; }

    public void setMath(int math) { this.math = math; }

    public void setCn(int cn) { this.cn = cn; }

    public void setEn(int en) { this.en = en; }

    public String getName() { return name; }

    public int getMath() { return math; }

    public int getCn() { return cn; }

    public int getEn() { return en; }

    public int getSum() { return sum; }

    /* 复写hashCode方法 */
    public int hashCode() {
        return this.name.hashCode() + this.sum * 37;
    }
    /* 复写equals方法 */
    public boolean equals(Object obj) {
        if(!(obj instanceof Student))
            throw new ClassCastException("类型不匹配");
        Student stu = (Student)obj;

        return this.name.equals(stu.name) && this.sum == stu.sum;
    }
    /* 复写compareTo方法 */
    public int compareTo(Student stu) {
        int flag = new Integer(this.sum).compareTo(new Integer(stu.sum));
        if(flag == 0)
            return this.name.compareTo(stu.name);
        return flag;
    }
    /* 复写toString方法 */
    public String toString() {
        return "Student[" + this.name + "," + math + "," + cn + "," + en + "]";
    }
}

程序运行结果:

Student[zhouqi,70,70,70]    210
Student[zhaoliu,60,60,60]    180
Student[wangwu,50,50,50]    150
Student[lisi,40,40,40]        120
Student[zhangsan,30,30,30]    90