本文主要会讲到以下部分:
编码问题
File类的使用
RandomAccessFile的使用
字节流的使用
字符流的使用
对象的序列化和反序列化
第一节 文件的编码
我们首先建立三个文本文件联通 联想 联,并把这几个汉字也存在文本当中。当建立好后,重新打开出现乱码,除联想外,其它两个文档的内容呈乱码状态。那么这是为什么呢?其中蕴含的就是编码的问题,当我们把记事本打开另存为时,联还有联通另存为的编码都是UTF-8,而联想另存为的格式是ANSI编码。这就是由编码引起的。
下面我们用java来做一个案例,通过解析java的一个字符串,这个字符串包含中文英文,我们把它以多种编码的形式解析成字节序列,然后我们采取分析这些字节序列,来比较这些编码之间的一些区别。
import java.nio.charset.Charset;
/**
* Created by Administrator on 2017/4/12.
*/
public class EncodeDemo {
public static void main(String[] args)throws Exception {
String s="慕课ABC";
byte[] bytes1=s.getBytes();//课程里转换成字节序列,用的是项目默认的编码UTF-8
//三个字节表示一个汉字。
for(byte b:bytes1){
System.out.print(Integer.toHexString(b&0xff)+" ");
//Integer.toHexString把byte转换成int,用16进制来输出每个字节
//但是如我们所知byte转换成int会扩充为32位,此时对我们有用的只有低八位
//所以要和0xff相与,把前面24个0去掉,得到我们所需的字节输出对应的后八位字节码
}
System.out.println();
//如果想要显示的指定某种编码,可以写在参数里
byte[] bytes2=s.getBytes("utf-8");
for (byte b:bytes2) {
System.out.print(Integer.toHexString(b&0xff)+" ");
}
//从输出的结果可以看到,utf-8一个汉字占三个字节,英文字母是一个字节
System.out.println();
//指定编码为gbk
byte[] bytes3=s.getBytes("gbk");
for (byte b:bytes3) {
System.out.print(Integer.toHexString(b&0xff)+" ");
}
//从输出的结果可以看到,gbk一个汉字占两个字节,英文字母是一个字节
System.out.println();
//java是双字节编码,java里的一个字符占用两个字节,所以有时候有这么
// 问的,java里面一个字符可以表示一个汉字吗?可以,gbk就可以放进去。
// 双字节编码 utf-16be编码
byte[] bytes4=s.getBytes("utf-16be");
//UTF-16be中文占用两个字节,英文占用也是两个字节
for (byte b:bytes4) {
System.out.print(Integer.toHexString(b&0xff)+" ");
}
System.out.println();
//当你的字节序列是某种编码时,这个时候想把字节序列变成字符串
// 也需要用这种编码模式,否则会出现乱码
String str1=new String(bytes4,"utf-16be");
//这里注意转换的时候要指定编码的格式,否则会使用项目默认的编码出现乱码。
System.out.println(str1);
System.out.println(System.getProperty("file.encoding"));//获取当前编码格式
System.out.println(Charset.defaultCharset().name());//获取编码格式
/**
*回到之前文本文件也就是记事本,就是字节序列,可以是任意编码的字节序列,
*如果我们在中文机器上直接创建文本文件,那么该文本文件只认识ANSI编码,
*文本文件本身是可以存放任意编码的。联通,联仅仅是一种巧合,它们正好符合
*了UTF-8编码的规则。举例来说,你创建两个项目分别指定编码为gbk和utf-8。
*当你在utf-8的项目里创建联通的文本文件,此时是utf-8编码的, 将其复制到
*gbk编码的项目里,由于它只认识gbk编码的文件,你是utf-8的文件打开就会是乱码
*如果你把它拷贝到系统桌面上它不会出现乱码,因为它不是直接在中文机器上创建的,
*是可以认识的,直接创建的则只认识ANSI编码。还有则是这两个项目,
*如果你只是打开文件复制内容进去,那么它会自动转化成gbk的编码,也不会出现乱码,会自动转换。
*还应该注意的是不同编码字节对应汉字大小不同,比如联通文本utf-8编码占用六个字节,
*联想占用4个字节,在读取联通时要六个字节读完,并且转换时编码正确才会不出现乱码
*/
}
}
第二节 File类的使用
java.io.File类用于表示文件(目录)。程序员可以通过file类来操作硬盘上的文件和目录。File类只用于表示文件(目录)的信息(名称、大小等),不能用于文件内容的访问。在接下来的案例里我们主要演示一下file类的基本API操作。
import java.io.File;
import java.io.IOException;
/**
* Created by Administrator on 2017/4/15.
*/
public class FileDemo {
public static void main(String[] args) {
//了解File类构造函数的几种情况,查帮助,Eclipce可以使用alt+/
//idea用shift+space不过由于这个键和输入法键重合需要设置下。
//File类里参数可以是(字符串),或者(URI),或者(类的对象,后面跟文件)
//还有(String arg0,String arg1)都可以用来使用。
File file=new File("E:\\新建文件夹 (2)\\imooc");
//注意E盘后面必须是双斜杠,因为这个是个转义字符
//在这里中间的分隔符我们可以用双斜杠也可以用反斜杠。
//还可以用File的一个静态成员,File.separator设置分隔符
//这样不管是windows系统还是哪个系统它都认识。
//File file=new File("e:"+File.separator+"新建文件夹 (2)"+File.separator+"imooc");
System.out.println(file.exists());
//exists()方法判断文件是否存在
if(!file.exists()){
//如果这个目录不存在我们可以直接创建这个目录
file.mkdir();//这里还有一个方法叫 file.mkdirs()创建多级目录
}else{
file.delete();
//如果有的话删除掉
}
//判断是否是一个目录,是目录返回true,不存在或不是目录返回false
System.out.println(file.isDirectory());
//判断是否是一个文件
System.out.println(file.isFile());
//直接创建一个文件目录
//File file2=new File("E:\\新建文件夹 (2)\\不想学习啊.txt");
//这里还可以换一种构造函数来构造,前面写目录后面写子目录
File file2=new File("E:\\新建文件夹 (2)","不想学习啊.txt");
//createNewFile创建文件。
if(!file2.exists()){
try {
file2.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}else{file2.delete();}
System.out.println(file2.isFile());
//其他常用的file对象的API
System.out.println(file);//直接打印file,打印的是file的toString()内容,还是地址
System.out.println(file.getAbsolutePath());//打印比较抽象的路径,直接打印它得到的也是目录
System.out.println(file.getName());//打印名字
System.out.println(file2.getName());
System.out.println(file.getParent());//得到父目录
System.out.println(file2.getParent());
System.out.println(file.getParentFile().getAbsolutePath());
System.out.println(file2.getParentFile());
//可以直接拿到文件对象来打印,或者.getAbsolutePath打印都行
}
}
- File file=new File();参数有多种用法可以是(字符串路径),或者(URI),或者(类的对象,后面跟文件)还有(String arg0,String arg1)都可以用来使用,最后一个是目录和子目录。注意盘后面必须是双斜杠,因为这个是个转义字符,在这里中间的分隔符我们可以用双斜杠也可以用反斜杠。还可以用File的一个静态成员,File.separator设置分隔符,这样不管是windows系统还是哪个系统它都认识。File file=new File(“e:”+File.separator+”新建文件夹 (2)”+File.separator+”imooc”);后面是文本或其他格式的文件须加属性后缀。file.separator返回文件的分隔符,如果是在LUNIX系统上,该值为
/
,在microsoft windows系统上,为\\
,这里File()参数如果没有写入 绝对路径,是相对路径,如果不存在,可以在项目下直接创建。 - exists()方法判断文件是否存在
- mkdir()创建目录,mkdirs()创建多级目录
- createNewFile()创建文件,这个可能会报出io异常需要抛出异常或trycatch处理异常
- delete()删除目录或者文件
- isDirectory()方法判断是否是一个目录
- isFile()判断是否是一个文件
- 当直接打印file时,打印的是file的toString()内容,打印出来的还是地址
- System.out.println(file.getAbsolutePath());//打印比较抽象的路径,直接打印它得到的也是目录
- System.out.println(file.getName());//打印名字
- System.out.println(file.getParent());//得到父目录
- System.out.println(file.getParentFile().getAbsolutePath());System.out.println(file2.getParentFile());可以直接拿到文件对象来打印,或者.getAbsolutePath打印都行,还是父目录。
这里来写一个工具类来看一下file类的过滤操作。
第一个是工具类,里面定义了一个可以遍历文件的静态方法,会把目录下所有文件遍历出来。
import java.io.File;
import java.io.IOException;
/**
* Created by Administrator on 2017/4/15.
*/
public class FileUtils {
//列出file的一些常用操作比如过滤遍历等操作,把它包装成工具类,以后使用会比较方便。
/**
* 列出指定目录下(包括其子目录下的所有文件)
* @param dir
* @throws IOException
*/
public static void listDirectory(File dir)throws IOException{
//这里首先判断是否存在这个目录,是否是一个目录,不是的话抛出异常。
if(!dir.exists()) throw new IllegalArgumentException("目录:" + dir + "不存在.");
if(!dir.isDirectory())throw new IllegalArgumentException(dir + "不是目录.");
//String[] fileNames=dir.list();//返回的是一个String类型的数组。
//list()方法用于列出当前目录下的子目录和文件,返回的是文件或目录的名称
//只包含子目录的名称,不包含子目录下的内容和文件
//此时如果要遍历子目录下的操作,一个可行的办法是,判断这是否是目录,如果
//目录,就需要构成File对象,继续遍历做递归操作。那么需要直接这么做吗?
//其实File提供了直接返回file对象的API。
File[] files=dir.listFiles();//返回的是一个File类的数组。返回的是直接子目录的抽象
//分别利用foreach循环对比输出的结果、
//for(String str:fileNames){
// System.out.println(str);
//}
//首先判断files是否为空
if(files!=null&&files.length>0) {
for (File file : files) {
if(file.isDirectory()){
//递归
listDirectory(file);
}else{
System.out.println(file);
}
}
}
}
}
测试类
public class FileUtilTest {
public static void main(String[] args)throws Exception {
FileUtils.listDirectory(new File("E:\\新建文件夹 (2)"));
}
}
第三节 RandomAccessFile的使用
这是Java提供的对文件内容的访问类,既可以读文件,也可以写文件,RandomAccessFile支持随机访问文件,可以访问文件的任意位置。
要使用RandomAccessFile类我们需要先了解的知识:
- JAVA文件的模型在硬盘上的文件是byte byte byte存储的,是数据的集合
- 打开文件怎么打开?有两种模式“rw”(读写),“r”(只读),读写操作和只读操作,后者的模式不可以进行写操作。
- RandomAccessFile构造的时候是这样,除了打开了一个文件对象之外,你还要告诉它读写的方式,因为它是随机访问文件,所以它内部还包含一个指针,文件指针,打开文件时指针在开头,pointer=0,随着你读写操作的时候,指针会往后挪。所以它在写的时候也好,读的时候也好它的指针都会移动
RandomAccessFile raf=new RandomAccessFile(fiel,"rw");
- 写方法 raf.write(int)里面可以写一个整数当然也可以写其它的, 这个时候write只能写一个字节,raf.write(int)——只写一个字节,后八位的。你写一个整型,它只会取后八位,如果真想把一个完整的整型写进去,应该写四次,第一次应该是高八个位,第二次是中八个字节,同时指针指向下一个字节的位置准备再次写入。
- 读方法 int b=raf.read()——读一个字节,从指针在的位置读一个字节,然后把这个字节转换成整数。
- 文件读写完成以后一定要关闭(Oracle官方说明)如果不关闭可能会有意想不到的错误。
下面我们就来实际使用下吧,创建案例利用RandomAccessFile进行读操作和写操作。
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
/**
* Created by Administrator on 2017/4/15.
*/
public class RafDemo {
public static void main(String[] args) throws IOException {
File Demo=new File("demo");//如果我们没有写绝对路径,那它就是相对路径
//就在我们的项目下, 如果这个文件不存在,我们可以直接创建出来。
if(!Demo.exists()) Demo.mkdir();//创建demo
File file=new File("demo","raf.dat");//父目录,子目录,不存在即创建它
if(!file.exists()) file.createNewFile();
RandomAccessFile raf=new RandomAccessFile(file,"rw");//需要放一个file对象,读写方法
//这个时候我们可以看一下指针的位置
System.out.println(raf.getFilePointer());//返回当前指针的位置。此时是0
//随机读取有一个很大的好处,将来我们在做文件下载的时候,我们自己做一个文件下载,这个文件很大
//我们用五个程序去下载它,每个下载它的一部分,最后再拼接成一个文件,迅雷就是这样的一个操作,
//其实迅雷里面是有多个进程或者称之为线程在下载同一个文件,每个线程下载文件的一部分,有使用
//迅雷的经验一般都看到过这种情况。最后的拼接就需要知道位置在哪。一定会用到RandomAccessFile
raf.write('A');//只写了一个字节,
System.out.println(raf.getFilePointer());
raf.write('B');
System.out.println(raf.getFilePointer());
//这里的指针应该在2的位置
//这里要写一个整数,因为是32位的所以要从高八位开始写入,4个字节。
int i=0x7fffffff;
raf.write(i>>>24);
raf.write(i>>>16);
raf.write(i>>>8);
raf.write(i);
//也可以直接写入一个int。RandomAccessFile提供了直接写入int型类型数据的方法
raf.writeInt(i);
//这里直接利用Ctrl+Alt+B查看writeInt的底层源码,和我们刚才写的差不多,多了一步&0xff的操作
//右移去零得到的结果还是自身。
String s="中";
byte[] sBytes=s.getBytes();
raf.write(sBytes);
System.out.println(raf.getFilePointer());//此时我们看到的长度是13,两次输入int数字,
// 中文utf-8三个字节,数字4个字节两遍,8个字节,8+3+2=13个字节的长度。
//读文件必须把指针移到头部。
raf.seek(0);//把文件移到开头
//一次性读取。把文件中的内容都读到字节数组中
byte[] rafRead=new byte[(int) raf.length()];//这里打开源码返回的值是long型,所以要强制转换一下
raf.read(rafRead);
System.out.println(Arrays.toString(rafRead));
//也可以把字节数组构造成字符串
String s2=new String(rafRead);
System.out.println(s2);
//转换成字符串输出
for(byte b:rafRead){
System.out.print(Integer.toHexString(b&0xff)+" ");
//这里注意如果不进行取与,按int型操作,负数会出现一些问题,取与取后八位。
}
raf.close();//将来使用时一定要注意这个操作
}
}
第四节 字节流
字节流和字符流其实就是我们一直强调的IO流,IO流是JAVA做输入输出的基础,所以IO流可以分为输入流和输出流,在这里面又分为字节流和字符流。在JAVA当中你去读文件或者写文件的时候,你可以以字节为单位,也可以以字符为单位。
1.字节流
(1).字节流也要对应输入和输出就是读和写,它有两个抽象的父类:InputStream/OutStream。
- InputStream抽象了应用程序读取数据的方式。
- OutStream抽象了应用程序写出数据的方式。
(2).读写文件在字节流里它都会有一个读写结束的问题,读写结束的时候在字节流里面都是这样达到文件结尾,我们把它称之为EOF=End 或者说读到-1了,读到-1就读到结尾了。
(3).在这两个抽象类里有一些重要的方法是必须我们要掌握的。首先我们来看输入流,比如说我们把键盘作为一个文件,我们对键盘是进行读还是写呢,键盘是输入还是输出?键盘其实是输入,比如说我往某个记事本文件里面写东西,实际上我是从键盘这个文件读了数据,然后写到了这个记事本文件里。输入流的最基本方法主要是读,它主要有这么几个方法:
- int b=in.read();读取一个字节,无符号填充到整型int的低八位,高八位就补0了,-1是EOF,-1就结束了。in代表的就是输入流的对象
- in.read(byte[] buf);读取数据直接填充到字节数组buf
- in.read(byte[] buf,int start,int size);读取数据到字节数组buf,从buf的start位置开始,存放size长度的数据。
(4).输出流是用来进行写的操作,输出流的基本方法写,它和上面的输入都会去对应
- Out.write(int b) 写出一个byte到流,写的是b的低八位
- Out.write(byte[] buf) 将buf字节数组都写入到流
- Out.write(byte[] buf,int start,int size) 字节数组buf从start位置开始,写size长度的字节到流
(5). InputStream和OutStream有第一组子类,一般来说,它们都会搭配使用。
FileInputStream,继承了InputStream,具体实现了在文件上读取数据下面我们也写一个工具类,来实现一下。
工具类
import java.io.FileInputStream;
import java.io.IOException;
/**
* Created by Administrator on 2017/4/16.
*/
public class IOUtil {
/**
* 读取指定文件内容,按照16进制输出到控制台
* 并且没输出10个byte换行
* @param fileName
*/
public static void printHex(String fileName)throws IOException{
//构造出了一个FileInputStream对象,把文件作为字节流进行读操作。
FileInputStream in=new FileInputStream(fileName);//这里可以是一个file对象也可以是一个文件路径
int b;//用来读
int i=0;
while((b=in.read())!=-1){
if(b<=0xf)
System.out.print("0");//补0,为了更严谨都显示双位。
System.out.print(Integer.toHexString(b)+" ");
//将整型转换成16进制,这里为什么不与&0xff,打开源码,read方法输出结果已经与操作过了。
i++;
if (i%10==0) System.out.println();
}
in.close();
}
public static void printHexByByteArray(String fileName)throws IOException{
FileInputStream in=new FileInputStream(fileName);
byte[] buf=new byte[8*1024];
//从in中批量读取字节,放入到buf这个字节数组中,从第0个位置开始放,最多放
//buf.length个,返回的是读到的字节的个数。因为它有这个情况,第一它可能读不满
//此时就返回in具体读到的个数,还有另一种情况就是它可能不够放。
int bytes=in.read(buf,0,buf.length);//一次性读完说明数组足够大
//是in.read读取文件存放在buf中的长度
int j=0;
for (int i=0;i<bytes;i++){
if((buf[i]&0xff)<=0xf) System.out.print("0");
System.out.print(Integer.toHexString(buf[i]&0xff)+" ");
j++;
if(j%10==0) System.out.println();
}
in.close();
//while循环写法
/*int bytes=0;
while((bytes=in.read(buf,0,buf.length))!=-1){
int j=0;
for (int i=0;i<bytes;i++){
if((buf[i]&0xff)<=0xf) System.out.print("0");
System.out.print(Integer.toHexString(buf[i]&0xff)+" ");
j++;
if(j%10==0) System.out.println();
}
}
in.close();*/
}
}
测试类1
public class IOUtilTest1 {
public static void main(String[] args) {
try {
IOUtil.printHex("E:\\新建文件夹 (2)\\学习啊.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试类2
import java.io.IOException;
/**
* Created by Administrator on 2017/4/16.
*/
public class IOUtilTest2 {
public static void main(String[] args) {
try {
IOUtil.printHexByByteArray("E:\\新建文件夹 (2)\\学习啊.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
}
read源码
public int read() throws IOException {
if (eof) {
return -1;
}
temp = new byte[1];
int n = read(temp, 0, 1);
if (n <= 0) {
return -1;
}
return temp[0] & 0xff;
}
在这里遇到了几个问题呢?
- 首先是read()方法返回值问题,这里看到在第一个方法时没有使用&0xff就可以正常输出16位,打开read源码看到源码中已经进行了该操作。read第二个方法是数组,就需要加&0xff之后数值输出结果正确。
- 还出现一个问题是输出结果减少了,两次不一致,一开始最后没有.close,补充上了, 而且是在一个测试类里使用的,因为考虑到指针随机问题,重新设置了一个测试类,两次结果就一致了。
- while循环里((bytes=in.read(buf,0,buf.length))!=-1),这里的bytes实际是in.read读取文件存放在buf数组中的长度,当读完时结果就是-1,读到文件尾时流没有可用的字节,就返回-1。所以如果文件没有被读完,则返回读取长度而不是-1,while继续执行,循环利用buf。如果文件被读完,则返回-1,while循环结束。
- 关于read方法,读完返回结果是-1,那么中间的参数0,read(buf,0,buf.length),是代表文件的起始位置还是指针位置呢?如果是while循环是不是下次再循环时,这个0就是代表文件指针现在所在的位置了,会继续原有读到的位置接着往下读?这里我自己测试的结果是,把承装的数组改下后,比如说之前一次尝试读取20*1024个字节然后装入buf数组中。现在改成10,还能正确读完结果。
(6).FileOutPutStream本身是输出,应用程序写数据的一个抽象。继承了OutStream,实现了向文件中写出byte的数据的方法。我们可以做一个简单的操作,向文件中写几个字节。
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Created by Administrator on 2017/4/16.
*/
public class FileOutDemo {
public static void main(String[] args) throws IOException{
// 与FileInputStream类似,
// 如果只写"demo/out.dat"不存在则创建,如果存在,删除后创建.意思也就是完全清零覆盖
// 但是后面加上true(boolean append)又不一样,代表存在不会删除,会直接在后面追加内容
// 如果不存在的话,加上,true,它也会直接创建出来。
FileOutputStream out=new FileOutputStream("demo/out.dat");//看构造函数,
// 根据提示可以是file对象或者路径,还可以在对象参数后加一个boolean append,表示新建文件
// 或是向文件里追加内容。boolean append是问我们是否要追加
// 要追加内容就加true,否则就会被清空掉
out.write('A');//一次写出一个字节,代表写出A字符的低八位。
out.write('B');//写出了B的低八位
int a=10;//write只能写八位,那么写一个int需要四次,每次八位
out.write(a>>>24);
out.write(a>>>16);
out.write(a>>>8);
out.write(a);
//也可以写入一个byte数组
byte[] utf="中国".getBytes("UTF-8");//这里可以指定编码格式,因为项目编码就是
//utf-8,也可以不写。
out.write(utf);
out.close();
IOUtil.printHex("demo/out.dat");
}
}
再编写一个可以copy的工具类
public static void copyFile(File srcFile,File destFile) throws IOException{
if(!srcFile.exists())
throw new IllegalArgumentException("文件"+srcFile+"不存在");//参数有问题的异常,以后遇到这种情况
//也可以抛出这个参数有问题的异常
if(!srcFile.isFile())
throw new IllegalArgumentException(srcFile+"不是文件");
FileInputStream in=new FileInputStream(srcFile);
FileOutputStream out=new FileOutputStream(destFile);
byte[] bytes=new byte[8*1024];
int b;
while((b=in.read(bytes,0,bytes.length))!=-1){
out.write(bytes,0,b);
out.flush();//最好加上,不加也没关系对字节流而言。
}
in.close();
out.close();
}
//测试类
import java.io.File;
import java.io.IOException;
/**
* Created by Administrator on 2017/4/17.
*/
public class IOUtilTest3 {
public static void main(String[] args) {
try {
IOUtil.copyFile(new File("E:\\新建文件夹 (2)/学习啊.txt"),new File("E:\\新建文件夹 (2)/学习啊1.txt"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这里遇到了哪些问题呢?
- 关于在编程中读取一个数字时,设置一个int变量来存放读取的这个文件的长度,b=in.read(bytes,0,bytes.length),这里在定义变量时不能图省事顺便就写成int b=in.read(bytes,0,bytes.length),然后后面再调用b时,如果不写后面的in.read()方法时,等于是只调用了一个int值,while(b!=-1),while循环成了一个无限循环。在while里应该添加的是读取的操作,这样在变成-1时就可以正确停止。在这里亲测,1KB的文件copy出了一个700M的txt文件,电脑快卡死了。
- out.flush();课程里讲的是最好加上,不加对字节流而言也没有关系。flush是把流里的缓冲数据输出,close是把这个流关闭了,关闭之前会把缓冲的数据都输出。这个应该是后面缓冲中可能学的内容,flush()方法可以强迫输出流(或缓冲的流)发送数据,即使此时缓冲区还没有填满,以此来打破这种死锁的状态。当我们使用输出流发送数据时,当数据不能填满输出流的缓冲区时,这时,数据就会被存储在输出流的缓冲区中。如果,我们这个时候调用关闭(close)输出流,存储在输出流的缓冲区中的数据就会丢失。所以说,关闭(close)输出流时,应先刷新(flush)换冲的输出流,话句话说就是:“迫使所有缓冲的输出数据被写出到底层输出流中”。
- IllegalArgumentException此异常表明向方法传递了一个不合法或不正确的参数,在以后遇到参数不正确的情况下都可以throw该异常。运行时异常的一个子类
- 关于FileOutputStream构造方法的参数问题,后面加不加true。其实正常来说如果想覆盖,不想要原文件的内容,那么不加,如果文件存在,删除它并新建,如果不存在,那么直接创建一个。加了true,如果文件存在,就在文件后面追加内容,如果不存在,即创建。
(7)DataOutputStream和DataInputStream,这组流实际上是对我们普通流,对“流”功能的扩展,可以更加方便的读取int、long、char字符等类型数据,使我们使用时更加方便。对于OutputStream我们就多了一些方法,比如**writeInt() writeDouble() writeUTF()**utf代表utf-8,其实这种都是以一种设计模式,就是装饰模式来实现的,并不是特别复杂。writeInt()能够写一个整型,其实就是包装了前面讲过的,四个字节作四次write的操作,包装好之后我们可以直接用。我们来写两个个案例,往一个文件里面写数字,或者说写数据普通的数据类型,并且直接读取出来。
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Created by Administrator on 2017/4/17.
*/
public class DosDemo {
public static void main(String[] args) throws IOException {
String file="demo/dos.dat";
DataOutputStream dos=new DataOutputStream(new FileOutputStream(file));
//CTRL+Q查看帮助文档,把FileOutStream作为参数扩展后就具备了想要的功能。
//这里包装FileOutStream就是使用它的write方法
dos.writeInt(10);
dos.writeInt(-10);
dos.writeLong(10l);
dos.writeDouble(10.5);
//采用UTF-8编码写出,一个中文三个字节
dos.writeUTF("中国");
//采用java里的UTF-16be编码写出,一个中文两个字节,
dos.writeChars("中国");
dos.close();
IOUtil.printHex(file);
}
}
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
/**
* Created by Administrator on 2017/4/17.
*/
public class DisDemo {
public static void main(String[] args) throws IOException {
String file="demo/dos.dat";
IOUtil.printHex(file);
DataInputStream dis=new DataInputStream(new FileInputStream(file));
int i=dis.readInt();
System.out.println(i);
int j=dis.readInt();
System.out.println(j);
long x=dis.readLong();
System.out.println(x);
Double y=dis.readDouble();
System.out.println(y);
String m=dis.readUTF();
System.out.println(m);
char n=dis.readChar();
System.out.println(n);
char n2=dis.readChar();
System.out.println(n2);
}
}
(8)BufferedInputStream和BufferedOutputStream这两个流类为IO提供了带缓冲区的操作,一般打开文件进行写入或读取操作时,都会加上缓冲这种流模式提高了IO的性能,(主要指的是输入输出的性能)。举例来说从应用程序中,我们把数据放入文件,相当于把一缸水倒入另一个缸中,最早之前我们用FileOutputStream的write()方法,相当于一滴一滴的把水“转移”过去。而我们用DataOutputStream,writeXxx()方法会方便一些,相当于一瓢一瓢的来转移。那么bufferedOutStream当中它也有write方法,更方便,相当于一瓢一瓢水先放入桶中,桶就是我们说的缓冲区,再从桶中倒入到另一个缸中,性能提高了。以后使用它性能会更好。重新写一个coby文件的案例。用三种方法一种带缓冲区一个字节一个字节读取,一种不带缓冲区一个字节一个字节读取,联合上面的批量字节读取。三者比较一下时间,带缓冲区的会比不带缓冲区的一个字节读取方法要快,批量读取的最快。获取当前系统时间long a=System.currentTimeMillis()。
拷贝工具类
/**
* 进行文件的拷贝,利用带缓冲的字节流
* @param srcFile
* @param destFile
* @throws IOException
*/
public static void copyFileByBuffer(File srcFile,File destFile) throws IOException{
if(!srcFile.exists())
throw new IllegalArgumentException("文件"+srcFile+"不存在");//参数有问题的异常,以后遇到这种情况
//也可以抛出这个参数有问题的异常
if(!srcFile.isFile())
throw new IllegalArgumentException(srcFile+"不是文件");
BufferedInputStream bis=new BufferedInputStream(new FileInputStream(srcFile));
//这里的参数需要用inputStream来构造,也可以设置缓冲区的大小,(参数1,size)
BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream(destFile));
int c;
while((c=bis.read())!=-1){
bos.write(c);
bos.flush();//刷新缓冲区
}
bis.close();
bos.close();
}
/**
* 也是一个字节一个字节读取,不带缓冲区注意,带缓冲区的一定要flush一下
* @param srcFile
* @param destFile
* @throws IOException
*/
public static void copyFileByByteByter(File srcFile,File destFile) throws IOException{
if(!srcFile.exists())
throw new IllegalArgumentException("文件"+srcFile+"不存在");//参数有问题的异常,以后遇到这种情况
//也可以抛出这个参数有问题的异常
if(!srcFile.isFile())
throw new IllegalArgumentException(srcFile+"不是文件");
FileInputStream in=new FileInputStream(srcFile);
FileOutputStream out=new FileOutputStream(destFile);
int c;
while((c=in.read())!=-1){
out.write(c);
out.flush();
}
in.close();
out.close();
}
测试类
import java.io.File;
import java.io.IOException;
/**
* Created by Administrator on 2017/4/17.
*/
public class IOUtilTest4 {
public static void main(String[] args) {
try {
long start=System.currentTimeMillis();//获取毫秒数
IOUtil.copyFileByByteByter(new File("E:\\武神.txt"),
new File("E:\\新建文件夹 (2)/武神1.txt"));
long end=System.currentTimeMillis();//获取毫秒数
System.out.println("一个字节一个字节读取的时间"+(end-start));//46615毫秒
} catch (IOException e) {
e.printStackTrace();
}
/*try {
long start=System.currentTimeMillis();//获取毫秒数
IOUtil.copyFileByBuffer(new File("E:\\武神.txt"),
new File("E:\\新建文件夹 (2)/武神1.txt"));
long end=System.currentTimeMillis();//获取毫秒数
System.out.println("一个字节一个字节带缓冲区的时间"+(end-start));//28601毫秒
} catch (IOException e) {
e.printStackTrace();
}*/
/*try {
long start=System.currentTimeMillis();//获取毫秒数
IOUtil.copyFile(new File("E:\\武神.txt"),
new File("E:\\新建文件夹 (2)/武神2.txt"));
long end=System.currentTimeMillis();//获取毫秒数
System.out.println("字节批量读取的时间"+(end-start));//46毫秒
} catch (IOException e) {
e.printStackTrace();
}*/
}
}
第五节 字符流
在学习本节时我们需要对一些基本的概念有所了解
(1).编码问题
(2).认识文本和文本文件,
- JAVA中的文本(char)是16位无符号整数,是字符的unicode编码(双字节编码)
- 文件是byte byte byte……的数据序列
- 文本文件是文本(char)序列按照某种编码方案(utf-8,utf-16be,gbk)序列化为byte的存储结果。
(3).字符流里也分为输入流和输出流(Reader Writer)。这里如果用它读MP3是没有字符的,我们的字符流操作的大部分都是文本文件,比如说一个mp3文件,都是字节序列,那么你读其中一个字符就没有任何意义,默认是按照项目编码来解析的。
- 字符的处理,一次处理一个字符。
- 字符的底层仍然是基本的字节序列,这就是字符流的处理方式。
- 我们知道字符可能是中文,也可能是英文,可能是UTF-8编码的,也可能是UTF-16BE编码的,那么所占大小都是不一样的,那么在这里,字符流的基本实现:
- InputStreamReader 完成Byte流解析为char流,按照编码解析
- OutputStreamWriter 提供char流到byte流,按照编码处理,(就是将字符写入byte)
注意flush方法,结束缓冲,下面来做案例
import java.io.*;
/**
* Created by Administrator on 2017/4/18.
*/
public class IsrAndOswDemo {
public static void main(String[] args) throws IOException{
InputStreamReader isr=new InputStreamReader(new FileInputStream
("E:\\新建文件夹 (2)\\学习啊.txt"),"gbk");
OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("E:\\新建文件夹 (2)\\学习啊5.txt"),"gbk");
//通常我们新建文本文档的ANSI编码,其实就是gbk编码。不写参数即默认项目编码
//我们一般要写你要操作的文件的编码
/*int c;
while((c=isr.read())!=-1){
System.out.print((char) c);//这里需要强制转换下字符
}*/
char[] buffer=new char[8*1024];
int bytes;
//批量读取,放入buffer这个字符数组,从第0个位置开始放置,最多放buffer.length个。
//返回的是读到的字符的个数,0是从这个数组的第0位开始放起
while((bytes=isr.read(buffer,0,buffer.length))!=-1){
osw.write(buffer,0,bytes);
osw.flush();//漏写这个,第一次写入没成功,加上它就行了
String s=new String(buffer,0,bytes);//ctrl+Q查看帮助API文档
System.out.println(s);
/*for(char ch:buffer){
System.out.print(buffer);//这样读取出现了数据丢失,用上面的方法。
}*/
}
isr.close();
osw.close();
}
}
(4).字符流的FileReader和FileWriter,和我们上面学的InputStreamReader,OutputStreamWrite很像,我们做了一个嵌套来构造,那么在FileReader和FileWriter就非常方便了。但是有一点是因为它不能设置编码,所以有时编码不一致时,需要使用之前的方法来确定编码。
import java.io.*;
/**
* Created by Administrator on 2017/4/18.
*/
public class FrAndFwDemo {
public static void main(String[] args) throws IOException {
FileReader fr=new FileReader("E:\\新建文件夹 (2)/学习啊3.txt");
/*OutputStreamWriter osw2=new OutputStreamWriter(new FileOutputStream("E:\\新建文件夹 (2)/学习啊6.txt")
,"gbk");*/
FileWriter ft=new FileWriter("E:\\新建文件夹 (2)/学习啊6.txt");
char[] buffer=new char[8*1024];
int c;
while((c=fr.read(buffer,0,buffer.length))!=-1){
String s=new String(buffer,0,c);
ft.write(s);
ft.flush();
}
fr.close();
ft.close();
}
}
(5).字符流的过滤器,我们可以对字符流加过滤,使得字符流具备更强大的功能,比如说对于普通的流,我们加了过滤之后,可以有一个BufferedReader,我们把它称之为过滤流,字符流的过滤器,它有最强大的功能readLine当然,除了基本的读的功能以外,它可以一次读一行。BufferedWriter,一次写一行。,也可以结合PrintWriter一起使用。我们来写一个案例,这个案例也是对文件进行读写操作
import java.io.*;
/**
* Created by Administrator on 2017/4/18.
*/
public class BrAndBwOrPwDemo {
public static void main(String[] args) throws IOException {
//对文件进行读写操作
BufferedReader br=new BufferedReader(new InputStreamReader
(new FileInputStream("E:\\新建文件夹 (2)/学习啊.txt"),"gbk"));//里面的参数是reader,也就是对字符流进行过滤
/* BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream
("E:\\新建文件夹 (2)/学习啊7.txt"),"gbk"));*/
PrintWriter pw=new PrintWriter("E:\\\\新建文件夹 (2)/学习啊8.txt","gbk");
//PrintWriter pw1=new PrintWriter(OutputStream,boolean autoFlush) 参数众多,后者代表自动刷新缓冲区,也非常实用。
//经常用printWriter和bufferedReader搭配使用,也是非常实用的。
String line;
while((line=br.readLine())!=null){
System.out.print(line);//一次读一行并不能识别换行
// bw.write(line);
// //直接写出,换行仍然没用,单独写出换行操作
// bw.newLine();
// bw.flush();
pw.println(line);//有这个ln就是换行,没有就不换行
pw.flush();
}
br.close();
pw.close();
}
}
第六节 对象的序列化和反序列化
1.什么是对象的序列化和反序列化呢?
- 对象序列化,就是将object对象转换成byte序列,反之叫对象的反序列化
- 做序列化需要有流类,
- 序列化流(ObjectOutPutStream)是过滤流,是字节的过滤流。—-writeObject
- 反序列化流(ObjectInputStream)—-readObject方法
- 在java里面对象如果要进行序列化,有这样一个接口,我们称之为序列化接口(Serializable),对象必须实现序列化接口,才能进行序列化,否则将出现异常。这个接口没有任何方法,只是一个标准。
下面我们来做一个案例,就是对student做一个序列化和反序列化的工作,首先创建学生类的对象,基本都是快捷键,注意带参构造方法的快捷键alt+FN+Insert,第一个Constructor就是
import java.io.Serializable;
/**
* Created by Administrator on 2017/4/18.
*/
public class Student implements Serializable{
private String stuNo;
private String stuName;
private int stuAge;
public Student(){
}
public Student(String stuNo, String stuName, int stuAge) {
this.stuNo = stuNo;
this.stuName = stuName;
this.stuAge = stuAge;
}
public String getStuNo() {
return stuNo;
}
public void setStuNo(String stuNo) {
this.stuNo = stuNo;
}
public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
public int getStuAge() {
return stuAge;
}
public void setStuAge(int stuAge) {
this.stuAge = stuAge;
}
@Override
public String toString() {
return "Student{" +
"stuNo='" + stuNo + '\'' +
", stuName='" + stuName + '\'' +
", stuAge=" + stuAge +
'}';
}
}
第二个就是实现序列化和反序列化的类
import java.io.*;
/**
* Created by Administrator on 2017/4/18.
*/
public class ObjectSeriaDemo {
public static void main(String[] args) throws IOException{
String file="demo/obj.dat";//把对象存储在这里面
//1.我们做对象的序列化
/*ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(file));
Student stu=new Student("10001","张三",20);
oos.writeObject(stu);//第一次运行异常,是student类没有实现可序列化的接口
oos.flush();
oos.close();*/
//2.对象的反序列化
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
try {
Student stu=(Student)ois.readObject();
System.out.println(stu);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
ois.close();
}
}
输出结果Student{stuNo=’10001’, stuName=’张三’, stuAge=20}
2.在java中,并不是我们什么时候都希望我们所有的元素都要进行序列化的工作,所以在java中有一个关键字 transient, transient这个关键字修饰了之后,该元素不会被默认的进行序列化,不会进行虚拟机jvm默认的序列化,在上面的age属性前加上这个关键字transient,再运行20的年龄就读取不到了,结果是Student{stuNo=’10001’, stuName=’张三’, stuAge=0}。这样的话我们就看age元素并没有被序列化,因为很多时候,我们也要考虑一些我并不是所有的元素都要进行序列化,特别是将来要把一些对象在网络中进行传输,转化成字节序列,但是有一些元素我们可能没有必要使用,那么放到网络中进行传输的时候,它就会浪费这个流量,特别是这个元素占用的空间比较大的话,传输起来就比较慢,那么这个时候我们就可以利用transient关键字。
transient有些情况下能够帮助我们提高性能,分析ArrayList源码中序列化和反序列化的问题,我们知道ArrayList数组列表,它下面无非是封装了数组的操作,数组的扩容,移除插入等操作。我们看到ArrayList有个元素,private transient Object[] elementData这就是它底层包装的数组,就是对这个数组进行扩容,插入元素的操作,进行移除的操作。但是这个数组元素是用transient来修饰的,它不能进行默认的序列化,但是可以自己序列化,有时候数组中并未放满,可以实现有效元素的序列化,提高性能。
transient还有一个什么好处呢?并不是说不做默认的序列化它就不能序列化了,也可以自己完成这个元素的序列化。这时该怎么进行序列化呢,有两个方法签名很重要,而且这两个方法签名都是私有了。我们可以参照一个类,一会也可以分析这个类的源码,有一个ArrayList这个类,打开这个类,我们会发现它里面就有一个元素就是transient来修饰的,接下来搜索writeObject,我们来使用这样一个方法签名
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{}
这是ArrayList的源代码,我们把方法签名直接拿到原来的Student类里即可,然后拿过来我们怎么做序列化呢
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
s.defaultWriteObject();//把虚拟机jvm能默认序列化的元素进行序列化操作
//打开String的源码,里面已经实现了序列化的接口Serializable
s.writeInt(stuAge);//这里writeInt,writeObject都可以,因为正好是一个整型
//如果是其他元素,可以直接writeObject。
//这是自己完成stuage的序列化。
//其实我们都用transient修饰也可以,这样我们就自己一个一个来进行序列化
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();//把jvm默认能反序列化的元素进行反序列化操作
this.stuAge = s.readInt();//自己完成stuAge的反序列化操作
}
3.序列化中子类和父类构造函数的调用问题
如果父类实现了序列化接口,那么子类就不用去实现接口,可以直接进行序列化
import java.io.*;
/**
* Created by Administrator on 2017/4/18.
*/
public class ObjectSeriaDemo2{
public static void main(String[] args) throws Exception {
/* ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("demo/obj2.dat"));
Fool3 fool3=new Fool3();
oos.writeObject(fool3);
oos.flush();
oos.close();*/
//序列化的结果fool....
//fool2....
//fool3.... 这里 Fool3 fool3=new Fool3()递归调用父类的构造函数没有问题
//主要看反序列化的时候是否递归调用父类的构造函数
/*ObjectInputStream ois=new ObjectInputStream(new FileInputStream("demo/obj2.dat"));
Fool3 fool3=(Fool3)ois.readObject();
System.out.println(fool3);
ois.close();*///第一次运行控制台没有任何调用,打印了对象的情况,此时我们是不是可以证明
//反序列不调用父类的构造方法呢,完全不能
/*ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("demo/obj2.dat"));
Bar2 bar2=new Bar2();
oos.writeObject(bar2);
oos.flush();
oos.close();*/
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("demo/obj2.dat"));
Bar2 bar2=(Bar2)ois.readObject();
System.out.println(bar2);
ois.close();
//运行结果,bar类构造方法显示的调用了,Bar类是另两个类的父类,但是没有实现序列化接口,所以被
// 显式的调用了。此时做一个小小的改动,将子类1也去掉接口的实现,在子类2中实现了接口。重新运行
// bar和bar1都被显式的调用了,这样其实我们会得到一个结论。
/**
* 对子类对象进行反序列化操作时,如果其父类没有实现序列化接口,那么其父类的构造函数会被调用,
* 如果实现了序列化接口,那么父类的构造函数我们是看不到被调用了,它相当于从你存的这个文件里
* 就能读到了,如果您父类不能进行序列化,在反序列化子类对象时,其父类的构造方法会被调用
*/
}
}
/**
* 一个类实现了序列化接口,那么其子类都可以进行序列化
*/
class Fool implements Serializable{
public Fool(){
System.out.println("fool....");
}
}
class Fool2 extends Fool{
public Fool2(){
System.out.println("fool2....");
}
}
class Fool3 extends Fool2{
public Fool3(){
System.out.println("fool3....");
}
}
class Bar{
public Bar(){
System.out.println("Bar....");
}
}
class Bar1 extends Bar{
public Bar1(){
System.out.println("Bar1....");
}
}
class Bar2 extends Bar1 implements Serializable {
public Bar2(){
System.out.println("Bar2....");
}
}