IO流技术所涉及到的类和示例有很多,在第一篇我们介绍了IO体系中的字符流和字节流体系,这一篇我们说一说IO技术所涉及到的其他类。
一. File类
1. File类的简介
File的意思就是文件,java是面向对象的语言,而文件这类事物有着自己的类型、路径、大小、时间、权限等等,所以他就封装成了对象,我们通过File对象来操作文件这类事物。
(1)File对象的特点:
1> File是用来将文件或者文件夹封装成对象的。文件就是这个对象具体指哪一个文件;文件夹就是一个目录,他可能不包含文件,但是他一个路径,这个路径也可以用File封装成对象
2> 方便了我们对于文件或者文件夹的操作。
3> File对象可以作为参数传递给流的构造函数。
4> 虽然我们使用File对系统中的文件或者目录进行了包装,但是我们包装的这个路径或者文件是不一定存在的,File类给我们提供了方法来创建他所包装的这个目录或者文件。
(2) 那么如何来创建一个File对象呢:
通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例,例:
File f = new File(“e:\\student\\java.txt”);
2>File(String parent,String child)
根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例,例:
File f = new File(“e:\\student”,”java.txt”);
3>File(File parent,String child)
根据 File对象和 child 路径名字符串创建一个新 File 实例,例:
Filed = new File(“e:\\student”);
Filef = new File(d,”java.txt”);
(3)separator字段
在File类中有一个字段可以替代分割符,这个字段是静态的,并且依赖于虚拟机的而定,也就是可以跨平台使用,以便我们在不同的操作系统下可以使用该字段来达到分割符的作用:
Filef = new File(“e:”+File.separator+”小宏.txt”);
2. File常用方法
(1) 创建
1>createNewFile()
返回一个boolean类型,可用来创建文件
当在初始File对象的时候就已经指定好该File对象所指向的目录或者文件,调用此方法时,如果该File对象所指定的路径或者文件不存在,就会在该路径下创建一个新的空文件,注意此方法是创建文件,如果你的File对象是封装的目录,而这个目录不存在,那么他也是创建文件,而不是目录,看下面的代码:
class FileDemo {
public static void main(String[] args) {
try {
File f = new File("e:"+File.separator+"student");
System.out.println(f.createNewFile());//false
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个示例中File封装了e盘下的student目录,而我的e盘下有sduent目录,调用此方法返回false,如果我的e盘下没有sdudent目录,那么就会在e盘下生成一个student文件,并返回true。
2>createTempFile(String prefix,String sufk)
返回File对象,此方法是静态方法,可以指定前缀名和后缀名来创建一个临时文件,临时文件是我们在程序运行时用来短暂存储数据用的,在我们的程序运行结束后可能被删除掉。注意点是前缀名至少是3个字符长度。
他还有个重载方法:
createTempFile(String prefix,String sufk,File directory)
这个方法是在指定目录direectory下创建一个指定前缀名和后缀名的临时文件,如果directory不是一个目录而是一个文件的话会报出异常。
3>mkdir()
返回boolean型,用来创建目录,也就是文件夹,而不是创建文件,调用此方法会创建一个一级目录,例:
File f = new File("e://person//student");
System.out.println(f.mkdir());
mkdir方法会在e盘person目录下创建一个子目录student,如果student前面的目录不正确,或者person目录下已经有student这个文件夹了,该方法就会返回false。
即便是我们把“student”改为“student.java”他创建的还是文件夹,名称是“student.java”。
4>mkdirs()
返回boolean型,此方法可以创建多级目录,当指定的磁盘正确而且目录名不存在时就可以创建出多层目录。
(2) 删除
1> delete()
返回boolean类型,此方法只能删除文件或者最底层的文件夹,如果一个文件夹中有内容那么此方法不会删除该文件夹,返回false。
2> deleteOnExit()
无返回值,在程序退出时,也就是虚拟机结束运行时删除文件,此方法的好处在即使发生异常文件也能被删除。
(2) 判断
1> canExecute()
返回boolean型,判断此File对象所关联的文件是否可以执行,只有当此文件的路径存在且文件可执行此方法才会返回true
2> comPareTo(File pathname)
返回int类型,按字母顺序比较File对象所包装的路径名是否相同,相同返回0,大于参数返回正数,小于参数返回负数。
3> exists()
返回boolean型,测试此File对象表示的文件或目录是否存在,此方法十分重要,我们在使用流对象来关联文件时,一般都要确定该文件是否存在,比如说我们在使用FileWrite来操作一个文本文件时,当这个文件存在我们就对他进行续写,如果不存在则直接创建即可,这时就用到exists()方法。
4> isDirectory()
返回boolean型,判断这个File对象是不是一个目录(文件夹)。
5> isFile()
返回boolean型,判断这个File对象是不是一个文件,是则返回true。
在调用此方法或者isDirectory()方法时首先要调用exists()方法判断该File对象封装的内容是否存在
6>isHidden()
返回boolean型,当File对象封装的文件根据底层操作系统判断为隐藏文件时返回true。
7>isAbsolute()
返回boolean型,判断路径是否是一个绝对路径,绝对路径就是此文件或者目录的全部存放路径。此方法不会去判断文件是否存在,只要他是一个完整的存放路径就返回true。
(3) 获取
1> getName()
返回String类型,获取这个File对象所包装的目录或者文件名,指的是这路径中最后一个名词,比如“e://Person//student”调用该方法获取到的是“student”。
2> getPath()
返回Sting类型,此方法获取到的是File对象所指定的整个路径名,你在构建File对象时使用什么路径该方法就返回什么路径,而不是最后一个文件名称或者目录名称。
3> getParent()
返回此抽象路径的父目录,如果File文件中没有标明指定的父目录,那么该方法就返回null。
4> getAbcolutePath()
返回String类型,该方法返回此文件的绝对路径。
5> getAbsoluteFile()
返回File对象,他也是返回的绝对路径只不过是返回的File类型的对象。
6> lastModified()
返回long型,获取到此文件最后一次被修改的时间,文件不存在返回0。
7> length()
返回long型,以字节为单位获取文件的大小。
8> listRoots()
返回类型为File数组,此方法为静态方法可以获得系统中可以使用的根目录,也就是磁盘。
9> 文件列表:
以下方法是File类中一个自成体系的方法,我们把他叫做文件列表,这些方法可以获取到File指定目录下的内容。
a>list()
返回String数组,此方法可以获取到这个File文件所包装的目录下的所以文件和子目录,包括隐藏文件。如果File文件不是包装的一个目录而是一个文件的话返回null。
重载形式:
list(FilenameFilter filter)
返回Stirng数组,以指定的过滤器拿此目录下的文件或子目录。FilenameFilter是一个用于过滤文件名的接口,这个接口只有一个方法就是accept(File dir,String name)通过此方法可以指定什么样的文件才可以被list方法返回。
b>listFiles()
返回File数组,获取当前目录下的文件或者目录的File对象
重载形式:
(1) listFiles(FilenameFilter filter)
返回File数组,以指定的过滤器来获取该目录下的指定文件和子目录的File对象。
(2)listFiles(FileFilter filter)
返回File数组,FileFilter也是一个过滤器,他专门用于判断File对象是否存在于列表当中。
我们来简单演示一下他们的使用,因为要打印的内容繁多而且因电脑而定,所以就不罗列程序的结果了。
class Test1
{
public static void main(String[] args)throws IOException
{
list();
}
//list()方法罗列出当前目录下的文件及目录,如果File对象封装的是一个文件会报出空指针异常的错误
public static void list()throws IOException
{
//指定目录为e盘,e盘下有文件夹和各种类型的文件
File f = new File("E:\\");
//获取E盘下的所有文件
String[] name = f.list();
//高级for循环来打印出这些文件名
for(String n:name)
{
System.out.println(n);
}
}
//list(FilenameFilter flf)可以根据传递进来的过滤器,罗列指定目录下的目录与文件
public static void list_1()throws IOException
{
//我们定义了一个类实现FilenameFilter,这个过滤器只能拿txt文件
Fnf fnf = new Fnf();
//File对象指向e盘
File f1 = new File("E:\\");
//根据过滤器获取指定的文件,
String[] name = f1.list(fnf);
//打印文件名称
for(String n:name)
{
System.out.println(n);
}
}
//listFiles()方法获取此目录下的文件或文件名的file对象
public static void listFiles()throws IOException
{
File f = new File("e:\\");
//获取全部的文件和文件夹
File[] f1 = f.listFiles();
for(File f2:f1)
{
System.out.println(f2);
}
}
//listFiles(FileFilter filter)方法:以过滤器获取指定的file类型的文件或者数组
public static void listFiles_1()throws IOException
{
File f = new File("e:\\");
//通过匿名内部类的形式来定义过滤器FileFilter,他是用来判断File对象是否存在于列表中的
File[] filter = f.listFiles(new FileFilter(){
public boolean accept(File pathname)
{
//只有File对象是”.mp3“文件才能返回真
return pathname.getName().endsWith(".mp3");
}
});
//打印获取到File对象,此过滤器只返回mp3文件
for(File f2:filter)
{
System.out.println(f2);
}
}
//listRoots()静态方法,此方法可以返回机器中有效的磁盘,返回类型File[]
public static void listRoots()throws IOException
{
File[] f = File.listRoots();
for(File name:f)
{
System.out.println(name);
}
}
}
//自定义一个类实现过滤器接口
class Fnf implements FilenameFilter
{
//复写他的accept方法
public boolean accept(File dir,String name)
{
//如果后缀名是”.txt“则返回真
return name.endsWith(".txt");
}
}
(5)修改
1>renameTo(Filef)
返回boolean型,如果当前File对象和指定File对象在同一个目录下,那么此方法就会将当前对象的名称改成指定对象的名称,就像重命名。
如果当前File对象和指定File对象不在同一个目录下,会将当前File对象移动到指定File对象所在目录中,并且名称转换成指定File对象的名称,就类似与操作系统中的剪切复制重命名。
二. 递归
1. 递归的概念
递归是我们用来操作函数的一种方式,他可以让函数调用自身,类似于自己在原地转圈的感觉,递归有三个需要注意的地方。
1.递归必须要限定条件,函数重新调用自己必须要有限定条件,如果条件一直为true,那么就陷入了死循环。
2.递归要注意转圈的次数,如果转的圈太多了,会出现内存溢出的风险。我们在函数中是要定义变量的,这些变量占用内存空间,转一圈出现内存就少了点,如果转的圈数很多的话那么我们的内存就挂掉了。
3.递归语句之前的运算顺序是从第一次调用函数然后转圈一直到限定条件不满足,但是当递归的循环语句下还有其他运算x时,这个运算x是从最后一圈开始一直到第一圈,顺序是反的。例如:
public static void toBin(int a)
{
if(a>0)
{
System.out.println("a:"+a);
toBin(a/2);
System.out.println(a%2);
}
}
当a的值为3,toBin(a/2)是递归语句,他之前的语句是按正序显示的:a的值3和1,当递归语句的条件不满足时,最后一圈也就是a为0不满足递归的条件,这一圈结束,他的前一圈才能执行递归语句后面的语句,所以递归语句后的执行顺序是反的。
2.递归的演示
(1)我们在说File类的获取方法时讲到list列表的相关方法,列表中的方法是可以获取到指定目录下的所有子目录和文件的,但是这个方法却无法获取到子目录里面的内容,如果我们连子目录里的内容都要取出来,这就要使用到递归。
class FileWhileDemo {
public static void main(String[] args) {
//File对象指向e盘下的person目录
File f = new File("e://person");
//定义一个获取目录中内容的函数
getFiles(f);
}
public static void getFiles(File f) {
//获取目录中的子目录及文件对象
File[] names = f.listFiles();
for(File s:names)
{
//限定递归的条件,如果获取到的是一个目录,调用自身
if(s.isDirectory())
getFiles(s);
//如果获取到的是一个文件,打印文件名
else if(s.isFile())
System.out.println(s);
}
}
}
/*
* 在e盘的person目录下,有小宏.txt和student子目录,在子目录中有小绿.txt
* 打印结果为:
* e:\person\student\小绿.txt
* e:\person\小宏.txt
* */
(2)删除一个目录中的所有文件,delete()方法是无法删除含有内容的文件夹的,所以就需要从底层开始删除,我们可以利用递归来做。首先获取到所有的文件和文件夹,然后从底层开始删除。
class FileWhileDemo2
{
public static void main(String[] args)
{
File f = new File("e:\\person");
remove(f);
}
//定义删除方法
public static void remove(File f)
{
File[] arr = f.listFiles();
for(File f1:arr)
{
//如果File对象是文件夹,那么去转圈继续寻找
if(f1.isDirectory())
remove(f1);
//如果不是文件夹,就删掉
else
f1.delete();
}
//最后删除文件夹
f.delete();
}
}
(3)获取指定目录中所有的java文件名并把他们整理成一个文本文件,此示例结合集合和IO流来做,首先对指定的目录进行递归,在递归过程中得到的java文件名,然后将路径传到集合中,再将集合中的数据通过IO技术写入到一个文件中。
class FileWhileDemo3
{
public static void main(String[] args)
{
//封装File对象
File f = new File("E:\\student\\JAVAStudent\\yanshi");
//建立集合用于对存储获取到的文件
List<File> list = new ArrayList<File>();
//将获取到的文件存储到集合中
fileToList(f,list);
//将集合中的文件列表写入到本地磁盘里
write(list);
}
public static List fileToList(File f,List<File> list)
{
//获取目录下的File对象
File[] arr = f.listFiles();
for(File f1:arr)
{
//如果是文件夹就继续转圈
if(f1.isDirectory())
fileToList( f1,list);
else
{ //如果是java文件就添加到集合中去
if(f1.getName().endsWith(".java"))
list.add(f1);
}
}
return list;
}
//将集合中的java文件列表写入到本地磁盘中去
public static void write(List<File> list)
{
//为了提高效率使用字符流缓冲区
BufferedWriter bw = null;
try
{
bw = new BufferedWriter(new FileWriter("e://文件列表//list.txt"));
for(File f:list)
{
//获取java文件的绝对路径
String path = f.getAbsolutePath();
//写入到磁盘中
bw.write(path);
bw.newLine();
bw.flush();
}
}
catch (IOException e)
{
e.printStackTrace();
}
//关流
finally
{
try
{
bw.close();
}
catch (IOException e1)
{
e1.printStackTrace();
}
}
}
}
三. Properties类
1. Properties简介
Properties我们在前面做过相关的练习,这个类是集合中Map体系下HashTable的子类,他具有Map集合的特点,存储形式是键值对。该类的键值对都是String字符串,所以不需要加泛型修饰。
Properties是集合和IO技术相结合的一个集合容器,其对象可以用于以键值对形式配置文件,也就是此类可以将键值对形式配置文件从硬盘中读取到集合里,也可以从集合写入到硬盘中,但前提是这个文件的格式一定要是键值对形式的。
2.Properties的常用方法
1> stringPropertyNames()
返回Set<String>类型,此方法可以拿到集合中所有的键值,返回一个Set集合。
2> getProperty(String key)
返回String类型,按指定的键获取其对应的值。
3> setProperty(Stirng key,String value)
返回一个Object类型对象,就是往此集合中添加指定的键值对,如果键值key以前对应有值,就把以前的值返回来,如果没有返回null。
4> 与输入流结合,无返回值
a>load(InputStream in)
从输入流中读取键值对
b>load(Reader in)
与字符流联系在一起,按照行的格式读取键值对。
5> 与输出流相结合,不过他的输出位置一般都与来源一致,无返回值
a>store(OutputStream out, String comments)
将此集合与输出字节流相结合,一般写入的文件是我们在本地磁盘下获取的文件,也就是修改本地配置文件的意思。
而comments是注释信息,可以为null。
b>store(Writer writer,String comments)
将集合与输出字符流结合,大多也为修改本地配置文件。
6>与打印流结合,无返回值
a> list(PrintStream out)
将键值对信息输出到字节打印流
b> list(PrintWrite out)
将键值对信息输出到字符打印流
3.Properties类的示例
我们已经了解到Properties的相关方法了,他之所以能读取键值对配置信息是因为他内部封装了和IO流相关联的方法,我们现在做一个示例,模仿一个限制使用次数的软件。我们写一个函数,当你调用一次该函数,使用次数就算增加一次。首先定义一个键值对配置文件记录调用函数的次数,调用该函数一次,我们就让配置文件中的值增加一次,然后限定使用次数。示例:
class PropertiesDemo {
public static void main(String[] args) {
//首先要定义Properties对象,用来生成配置文件
Properties ppt = new Properties();
//定义读取流和写入流
FileReader fr=null;
FileWriter fw = null;
try {
//获取e盘下的配置信息文件
File f = new File("e://配置信息.ini");
//如果不存在就创建他
if(!f.exists())
{
f.createNewFile();
//把键值对写入配置信息文件
fw = new FileWriter(f);
fw.write("count=0");
fw.flush();
}
//获取配置信息
fr = new FileReader(f);
//从输入流中读取键值对到集合里来
ppt.load(fr);
//获取count键所对应的值
String value = ppt.getProperty("count");
//转成数字
int a = Integer.parseInt(value);
//大于5次就收费
if(a>=5)
{
System.out.println("免费次数已到,请交费后使用!");
return;
}
//计数器增长,并写入到配置文件中
a++;
System.out.println("这是第"+a+"次运行程序");
ppt.setProperty("count", String.valueOf(a));
fw = new FileWriter(f);
ppt.store(fw,null);
} catch (IOException e) {
e.printStackTrace();
}
//关流
finally
{
try {
if(fr!=null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if(fw!=null)
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/*
* 此示例中当我们的程序运行5次之后,再点击运行就会提示“免费次数已到,请交费后使用!”的提示。
* */
四. 打印流
1. 打印流简介
在Properties类中有个方法list(PrintStream Out),这个PrintStream就是字节打印流,打印流一种是个非常无私的流,不仅提供写成方法,而且专门为输出流增强了功能,使得输出流可以很方便的打印各种数据的表现形式,而且打印流不会抛出IO异常。
打印流最大的特点在于提供了打印方法,将各种数据类型都原样打印。在打印流提供了很多打印方法,你要打印的是int型数据,打印流就会打印int型数据,你要打印的是对象,打印流就打印对象,这是打印流的最大特点。
打印流的分类:
字符打印流:PrintWiter和字节打印流:PrintStream
2. 字节打印流PrintStream
构造函数可以接收的参数类型
1> File对象File
2> 字符串路径String
3> 字节输出流OutputStream,在接收一个输出流的时候,可以指定是否随时刷新输出流。
3. 字符打印流PrintWriter(较为常用)
构造函数可接收的参数类型
1> File对象File
2> 字符串路径String
3> 字节输出流OutputStream
4> 字符输出流 Writer,
当字符打印流在接收输出流作为参数时,可以指定是否随时可以刷新,例:PrintWriter(System.out,true),
这个方法与PrintStream类不同,如果启用了自动刷新,则只有在调用 println、printf 或 format 的其中一个方法时才可能完成此操作
class PrintDemo {
public static void main(String[] args) {
//建立字符读取流缓冲区读取键盘输入
BufferedReader br = null;
//建立字符打印流
PrintWriter pw = null;
try {
br = new BufferedReader(new InputStreamReader(System.in));
//打印字符流指向一个文件,设置自动刷新
pw = new PrintWriter(new FileWriter("e://printDemo.txt"),true);
String line = null;
while((line = br.readLine())!=null)
{
if("over".equals(line))
break;
//写入一行数据,此方法会自动换行
pw.println(line);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally
{
if(br!=null)
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
//打印流不会抛出异常
if(pw!=null)
pw.close();
}
}
}
五. 序列流
1.序列流简介
序列流是SequenceInputStream这个流对象,序列流只有这一个类,没有相对应的输出流形式。序列流是用来对多个流进行和并的对象,他可以接收一个有序的输入流集合,从第一个输入流开始,读取到第一个输入流的结尾处,接着从第二个输入流开始读取,以此类推一直读到最后一个流文件的末尾。
序列流的好处就是可以把多个文件合并成一个文件,比如输入流a装着文件1,输入流b装着文件2,而使用序列流把a、b输入流合并在一起之后就可以吧文件1和文件2合并成一个文件了。就是多个源对应一个目的。
我们看一看他的构造函数,他有两个构造函数:
1> SequenceInputStream(Enumeration<?extends InputStream>t)
当有多个流的时候使用Enumeration枚举。
2> SequenceInputStream(InputStream s1,InputStream s2)
当只有两个流,可以使用这个构造函数来合并,先读s1,再读s2
2.合并流演示
我们想要合并多个流就要使用到枚举,这时我们可以选择Vector集合用来存放流对象,他可以返回枚举实例。也可以自定义类实现Enumeration接口。
把e盘下的1.txt、2.txt和3.txt这三个文件合并成为一个4.txt文件,用两种方式,第一种使用Vector集合,第二种使用一个类继承Enumerration接口。示例如下:
class SequenceDemo {
public static void main(String[] args)throws IOException {
sequece_2();
}
//组合方法一:使用Vector
private static void sequece_1()throws IOException {
//使用Vector集合来存放输入流
Vector<FileInputStream> v = new Vector<FileInputStream>();
//添加e盘下的三个输入流
v.add(new FileInputStream("e://1.txt"));
v.add(new FileInputStream("e://2.txt"));
v.add(new FileInputStream("e://3.txt"));
//获取枚举实例
Enumeration<FileInputStream> en = v.elements();
//定义合并流
SequenceInputStream sis = new SequenceInputStream(en);
//定义输出流,写入到4.txt文件中
FileOutputStream fos = new FileOutputStream("e://4.txt");
byte[] b = new byte[1024];
int len = 0;
while((len=sis.read(b))!=-1)
{
fos.write(b, 0, len);
}
//关流
fos.close();
sis.close();
}
//组合的第二种方法,利用ArrayList方法
public static void sequece_2() throws IOException
{
//首先建立一个ArrayList集合
ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();
//获取到碎片文件
al.add(new FileInputStream("e://1.txt"));
al.add(new FileInputStream("e://2.txt"));
al.add(new FileInputStream("e://3.txt"));
//<span style="color:#ff0000;">匿名内部类要对访问的变量进行final修饰</span>
final Iterator<FileInputStream> it = al.iterator();
//利用匿名内部类的形式实现枚举接口
Enumeration<FileInputStream> en = new Enumeration<FileInputStream>()
{
public boolean hasMoreElements()
{
return it.hasNext();
}
public FileInputStream nextElement()
{
return it.next();
}
};
//创建序列流
SequenceInputStream sis = new SequenceInputStream(en);
FileOutputStream fos = new FileOutputStream("E:\\4_1.txt");
int len = 0;
byte[] buf = new byte[1024];
while((len = sis.read(buf))!=-1)
{
fos.write(buf,0,len);
}
fos.close();
sis.close();
}
}
3. 切割文件
我们说到序列流可以完成合并文件的功能,那么怎么来把一个文件切割开来呢,其实很简单,就是一个读取流对应多个输出流即可,把一个文件按照指定大小的数组来读取,读取一次装到一个输出流中,读取一次装到一个输出流中,这样就把一个文件切割了。
我们按照这个指定大小切出来的文件碎片可能每一份都是相等的,但大多数情况是最后一个文件和其他文件大小不一致,这是因为我们定义的数组长度并不能正好把这个文件均分。
我们通过把一个mp3文件切成碎片然后在组合起来,演示一下文件的切割和组合:
class SplitDemo
{
public static void main(String[] args) throws IOException
{
File file = new File("E:\\忽然之间.mp3");
split(file);
sequece();
}
//按一兆切割文件
public static void split(File file)throws IOException
{
FileInputStream fis = new FileInputStream(file);
//定义一个一兆的存储区
byte[] buf = new byte[1024*1024];
int len = 0;
//定义碎片名
int count = 1;
while((len = fis.read(buf))!=-1)
{
//建立多个输出流,自定义碎片扩展名
FileOutputStream fos = new FileOutputStream(
"E:\\文件碎片\\"+(count++)+".part");
fos.write(buf,0,len);
fos.close();
}
fis.close();
}
//把切割的文件重新组合起来,Vector方法
public static void sequece() throws IOException
{
Vector<FileInputStream> v = new Vector<FileInputStream>();
//这个mp3文件被切割之后变成了4份
for(int c = 1;c<5;c++)
{
//往Vector集合中添加读取流
v.add(new FileInputStream("E:\\文件碎片\\"+(c)+".part"));
}
//获取枚举实例
Enumeration<FileInputStream> en = v.elements();
SequenceInputStream sis = new SequenceInputStream(en);
//写入到指定目录下
FileOutputStream fos = new FileOutputStream("E:\\Split\\小宏.mp3");
int len = 0;
byte[] buf = new byte[sis.available()];
while((len = sis.read(buf))!=-1)
{
fos.write(buf);
}
sis.close();
fos.close();
}
}
六. 操作对象的流
1.对象流简介
ObjectInputStream和ObjectOutputStream是用来操作对象的流,可以把堆内存中的对象写入到硬盘中,注意写入硬盘中的是对象的属性,对象流中有很多可以直接操作基本数值类型的方法,因为对象中的属性很多都是基本数值类型。
我们想要操作一个类的实例,那么这个类就要实现Serializable接口,Serializable接口中没有方法,通常我们称这种没有方法的接口为标记接口,一个类实现了Serializable接口就代表这个类可以被序列化,序列化就是代表这个对象可以被流操作,类似于只有实现Serializable接口才能拿到通行证。
实现Serializable接口其实就是为一个类添加一个标识:serialVersionUID,无论这个类是否做修改,我们都要保证这个序列号不会变,就是说这个类修改前或者修改后产生的对象序列号应该都是一致的,一般会把这个标识固定写死。
对对象进行序列化,把他写入到本地磁盘中进行长久的存储,我们只能对堆中的属性进行序列化,而其他内存位置的属性我们是无法访问到的,例如被static修饰的属性,存在于方法区中,我们是无法对静态属性进行存储的。对于堆中的属性,如果有的不是我们所需要的,我么无需把他进行持久化存储,这时可以添加 transient来标记这个属性,被transient修饰的属性即便是在堆中也无法被序列化。
总结一下,操作对象的步骤,首先被序列化的对象一定是实现了Serializable接口,然后此对象与一个字节流相结合,把这个字节流作为参数传递进对象流的构造函数中,我们就可以对象进行操作。演示一下:
//首先定义一个Person类
class Person implements Serializable{
//自己定义UID号,这样无论怎么改变此类,他生成对象都有唯一的UID号。
static final long serialVersionUID = 42L;
//可以被序列化的属性
private String name;
private int age;
//不可以被序列化的属性
public static int height;
public transient int weight;
public Person(String name, int age ,int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
public String toString() {
return "Person [name=" + name + ", age=" + age +", weight=" + weight
+ "]";
}
}
class ObjectSteamDemo{
public static void main(String[] args) {
try {
setObjet();
getObject();
} catch (IOException e) {
e.printStackTrace();
}
}
//写入对象到磁盘中
private static void setObjet()throws IOException {
//获取对象输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("e://person//小宏.txt"));
//写入对象
Person p = new Person("小宏", 22, 110);
oos.writeObject(p);
System.out.println("set:"+p.toString());
//关流
oos.close();
}
//从磁盘中读取对象到内存中
private static void getObject()throws IOException {
// 获取对象读取流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("e://person//小宏.txt"));
//获取对象
try {
Person p2 = (Person)ois.readObject();
System.out.println("get:"+p2.toString());
ois.close();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
/*打印结果:
* set:Person [name=小宏, age=22, weight=110]
get:Person [name=小宏, age=22, weight=0]
* */
七. 管道流
1.管道流的简介
我们在读写文件时,一般读取一部分文件然后存放到一个数组中,再利用输出流的方法把数组中的数据写出去,然后再读再写,输入流和输出流之间是通过数组为媒介来传递数据的。
管道流也分输入流和输出流,但是他们可以直接连接起来,不经过数组的传递,输入流可以从输出流中拿数据,输出流也可把数据发送到输入流中。
管道流也分字节流和字符流,字节流是PipedInputStream和PipedOutputStream;字符流是PipedReader和PipedWriter。
2.管道流的注意点
管道流最大问题在于我们在对数据进行读写操作时,什么时候读什么时候写,所以管道流又是和多线程技术紧密联系在一起的,通常数据由某个线程从管道读取流
对象中读取,并由其他线程将其写入到相应的管道输出流
,一般不会对这两个对象使用单线程,因为这样可能会产生死锁现象。
我们来通过建立输入输出线程来演示一下管道流的使用:
//创建读取线程
class Read implements Runnable
{
private PipedInputStream in;
Read(PipedInputStream in)
{
this.in = in;
}
public void run()
{
try
{
System.out.println("读取前:");
//接收从管道输出流传来的数据
int len = 0;
byte[] buf = new byte[1024];
while((len = in.read(buf))!=-1)
{
System.out.println("已经读取到数据:");
System.out.println(new String(buf,0,len));
}
}
catch (IOException e)
{
throw new RuntimeException("读取文件失败!sb!");
}finally{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//创建输出线程
class Write implements Runnable
{
private PipedOutputStream out;
Write(PipedOutputStream out)
{
this.out = out;
}
public void run()
{
try
{
System.out.println("写入前等待3秒");
Thread.sleep(3000);
//写入一个字符串给管道读取流直接获取到
out.write("xiaohong heima !".getBytes());
}
catch (Exception e)
{
throw new RuntimeException("写入文件失败");
}finally{
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class PipedStreamDemo
{
public static void main(String[] args) throws IOException
{
//创建管道流
PipedInputStream in = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream(in);
Read r = new Read(in);
Write w = new Write(out);
//将两个管道流相连接,也可以通过构造方法传递
//in.connect(out);
new Thread(r).start();
new Thread(w).start();
}
}
八. RandomAccessFile
1.RandomAccessFile简介
这个类是较为重要的一个类,他的直接父类是Object,不是IO体系下的类,但又是IO包中的成员,这个类自身就具备读和写两种功能,而且还能随机访问文件。
在该类的内部封装了一个大型的数组,它能通过指针对数组中的元素进行操作,在这个类中提供了方法来获取指针和改变指针位置。通过指针我们就可以对文件进行读写,这也是随机访问文件的原因。RandomAccessFile能完成读写的原理其实就是这个类把字节输入流和输出流封装在了内部。
RandomAccessFile在初始化的时候必须要与文件相关联,他只有两个构造函数:
RandomAccessFile(File file,Stringmode)
RandomAccessFile(Stirng name,Stringmode)
RadomAccessFile只能操作文件,而且对于操作的方式还有限定,这个mode就是访问的模式,他有两个常用值:“r”代表只读模式,“rw”代表可读可写。
当我们创建了一个此类对象,模式为“r”只读模式,如果File文件不存在会报出异常,如果模式为“rw”读写模式,当File文件不存在,则会去创建一个新的File文件,如果存在,会根据我们自定义的写入位置开始写入,不会覆盖掉原来的文件。
2. RandomAccessFile方法
在RandomAccessFile类中,提供了一堆的read读取和write写入方法,注意他的写入和读取方法中有与基本数据类型所对应的,所以他可以直接读、写基本数据类型。
这些方法我们在IO体系中都已经涉及到,我们来说一说他的特有方法:关于指针的操作:
1> seek(int x)
无返回值,当我们确定了要读取的内容在某个位置上时,我们可以利用此方法来移动指针到该位置上。
2> skipBytes(int x)
返回int类型,此方法是跳过指定的字节数x,并返回他跳过的实际字节数,比如在跳过x个字节的过程中到达了文件了末尾,那么跳了多少就返回多少,注意此方法不能往回跳,也就是x为负数时,他是不跳过任何字节的。
3> getFilePointer()
返回long类型,此方法可以获取的指针的位置
我们通过调整指针的方法可以让文件从指定位置写入,而不是从文件的开头写,这意味着我们可以覆盖掉文件的部分内容。
通过操作指针可以实现一个文件的分段写入,让一个线程负责一段数据的写入,通过写入位来区分哪个线程负责哪一段数据,这种方式多用于多线程下载。
我们来演示一下相关方法的使用:
class Test1
{
public static void main(String[] args)
{
try {
write();
read();
} catch (IOException e) {
e.printStackTrace();
}
}
//利用RandomAcessFile写入数据,根据指针写
public static void write() throws IOException
{
RandomAccessFile raf = new RandomAccessFile("e://小宏.txt","rw");
//调整对象中的指针,从0位置开始写
raf.seek(8*0);
raf.write("小蓝".getBytes());
raf.writeInt(22);
//跳过指定的字节数
raf.skipBytes(8);
raf.write("小宏".getBytes());
raf.writeInt(23);
raf.close();
}
public static void read()throws IOException
{
RandomAccessFile read = new RandomAccessFile("e://小宏.txt","r");
//跳过指定的字节数
read.skipBytes(8*1);
byte[] buf = new byte[4];
read.read(buf);
String name = new String(buf);
//当我读取年龄的时候此类中数组的指针已经走了4个位了
int age = read.readInt();
System.out.println("name:"+name);
System.out.println("age:"+ age);
read.close();
}
}
/*
* 打印结果为:
* name:小宏
age:23
* */
九. 操作基本类型数据的流
1. 数据读写流是专门操作基本数据类型的流,虽然其他流里面也有操作基本数据类型的方法,比如RandomAccessFile,但是数据读写流是专门为操作基本数据类型而建立的,当我们只需要操作基本数据类型时就可以选择使用该流。
数据读写流只能操作字节数据,他分为DataInputStream和DataOutputStream,数据读写流在初始化时必须要与对应的字节读写流关联。
2. 数据读写流的特有方法
数据读写流中读写基本数据类型,例如readInt()/writeInt(int t)等等都是其基础的方法,在数据读写流中,还有个特有方法需要注意一下:readUTF()/writeUTF ()。此方法可以按照“修改版的UTF-8”编码表编写和读取字符串。
十. 操作内存数据的相关流
以下三个流都是在内存中创建的不会调用底层的资源,所以无论是否关闭都不会影响他的使用,这三个流也不会抛出IO异常,他们是IO技术中一些比较特殊的对象,下面来看一看他们的具体情况。
1.操作字节数组的流
(1)字节数组流简介
这个流是专门用来操作字节数组的,他不会调用底层的资源,所以关流是无效的,而且不会抛出任何的IO异常,这类流其实就是在利用流的思想在操作字节数组。
此流也是有两个对应的类:
ByteArrayInputStream
在构造时候,需要接收数据源,而且数据源是一个字节数组
ByteArrayOutputStream
在构造的时候,不用定义数据目的,因为该对象已经内部封装的可变长度的字节数组,目的就是内存中的数组。
(2)特殊方法介绍
在ByteArrayOutputStream字节数组输出流中write方法可以将数据写入到此流的缓冲数组中,而有一个方法可以指定目的,就是writeTo(OutputStreamout),此方法可以将数据写入到指定的字节输出流中。示例如下:
class ByteStreamDemo
{
public static void main(String[] args) throws IOException
{
byteDemo();
}
//存入数据
public static void byteDemo()throws IOException
{
//读取字节流,此构造方法接收的一个字节数组
ByteArrayInputStream bis = new ByteArrayInputStream("小宏加油!".getBytes());
//写入字节流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//建立输出流,把内存中数组中的数据传到该流中
FileOutputStream fos = new FileOutputStream("小蓝.txt");
int by = 0;
while((by = bis.read())!=-1)
{
//写入到此流自身的缓冲区中
bos.write(by);
}
System.out.println(bos.toString());
//写入到指定的输出流里
bos.writeTo(fos);
}
}
2.操作字符数组的流
CharArrayReader
构造是需要接收一个字符数组作为数据源
CharArrayWriter
构造时无需定义目的地,内部封装有字符数组。
此类流是用来操作字符数组的,他的原理同字节数组操作流一样,只不过他的默认存储区是字符数组,CharArrayWriter的特殊方法writeTo(Writer),同理是关联到Writer字符输出流
3. 操作字符串的流
也是两个相对应的类StringReader和StringWriter,用来在内存中操纵字符串,不会调用底层的资源。其原理和字节数组操作流一样。
十一. 编码表的简介
1. 编码表的简介
编码表是一个程序员时必须要掌握的基础知识,我们来说一说几种常见的码表。
(1) ASCII码表:美国通用码表,占用一个字节的7位
(2) ISO8859-1:欧洲通用码表,占用一个字节的8位
(3) GB2312:中文码表,占两个字节
(4) GBK:中文通用码表,占两个字节
(5) Unicode:国际通用码表,占两个字节
(6) UTF-8:国际通用码表,是Unicode的升级版,最多三个字节,就是UTF-8编码时会给给字符加标识头,一个字符能用一个字节装就用一个字节,最多三个字节就可以装下。
2. 编码表的使用注意事项
我们在使用转化流的时候可以指定编码表,需要注意的是相同的字符在不同的编码表下对应的数值是不同的,用什么编码表存储的,就要用什么编码表取出,如果编码表读取不一致的话就会产生乱码的现象。
编码:String--->byte[]
利用String类的getBytes(“编码表名称”)
解码:byte[]--->String
new String(byte[] b,”编码表名称”)
当我们对一个中文字符串进行传递,编码和解码都必须是对应能识别中文的同一码表,但是如果我按GBK码表去编的码,对方按ISO8859-1解的码,这样肯定获取不到想要的数据,这时候如果还想获取到原数据,就需要把解出来的字符重新按ISO8859-1编码获取到他的数值之后再按GBK解码这样就可以获取到我们想要的字符了。
注意点是如果你按GBK编的码生成了I,按UTF-8解的码产生了一堆字符S,这个时候你不能在按UTF-8把解码完的字符S重新编码成数字了,他重新编码的数字是不正确的,因为按UTF-8解出来的字符S大多是”?”如果按UTF-8重新编回去的话,他回去UTF-8未知区域里面查找,这样就会生成别的数值,无法获取到原本按GBK编码的数值I。
特殊字符:联通
我们知道GBK和UTF-8都可以识别中文,“联通“这个字符按GBK编码产生的数值后与UTF-8的标识重合,当我们在打开电脑中的记事本软件,输入“联通”字符,保存后关闭再打开就乱码了,原因就是保存时候按GBK存的,打开时记事本软件判断字符的标识头,发现与UTF-8一样,按UTF-8解码显示,于是就乱码了。