p157-p160
/* 字符集 之前学了file类,但是还不能读写文件内容.所以需要学习IO流,来真正操作文件. 在学习IO流之前,应该先学习字符集 计算机不能直接存字符,底层只能存0和1. 人类为了存储字符,给了每个字符一个编码,这个合集就是字符集,比如ASCII就是一个字符集. 对美国人来说,因为字符数量很少,所以编号的总数也很少,用1个字节就可以表示128个字符信息,完全够用. 但是对中文来说就不够了,所以有了GBK中文码表,在GBK中一个中文以两个字节的形式存储,包括几万个汉字,兼容ASCII表,但不包含世界上全部文字 Unicode码表是如今的编码标准,一般情况下计算机会先通过UTF-8编码到二进制再存到计算机. Unicode兼容ASCII码表,在Unicode中汉字以三个字节储存. 技术人员一般都会用UTF-8编码,编码时和解码时的字符集要一致,否则出现中文字符乱码. 英文和数字在任何国家都不会乱码. 因为不管什么字符集都要兼容ASCII码表,所以英文总是占1字节. 中文如果是UTF-8就是3字节,GBK是2字节. 编码和解码 利用String里面的API,可以实现编码和解码. byte[] bytes = s.getBytes("UTF-32");,即可将字符串s编码,获得一个字节数组. 入参可以选择要用哪种字符集编码,比如UTF-32,GBK等,默认用UTF-8 String s1 = new String(bytes,"utf-32"); 新建字符串,输入字节数组,即可解码. 入参可以选择解码字符集,必须和编码字符集相同才能正确解码 */ /* IO流 IO流用来读写数据,I是input,O是output,所以IO流也叫输入输出流 按照读写方法不同,分为字符流和字节流. 字符流就是每次读取或者写入一个字符,比较适合文本文档的操作. 字节流更适合读写音视频等文件,可以读写任何文件. 所以IO流总共分为四类: Reader字符输入流,Writer字符输出流,InputStream字节输入流,OutputStream字节输出流. 在java中,这是四个抽象类 因为方法在内存里运行,所以输入输出是对于内存来说的,input等于是放进内存里,所以input是读取硬盘内容.output等于从内存里输出,也就是把内容写入硬盘 */ /* InputStream 要想使用字节输入流,需要创建一个继承了InputStream抽象类的类的对象,这个类是FileInputStream,最好使用多态写法.下面是创建对象的代码 InputStream fileInputStream = new FileInputStream("输入文件的绝对路径或者相对路径都可以"); 这个类里提供了读取方法 read() 返回一个int类型的整数,表示的是一个字节. 每次read都会往后推一个字节,比如abc,第一次read会返回a的int97,第二次返回b98,以此类推 但是如果文件中有中文,比如"abc中文",此时abc都读完以后应该读中,但是中是三个字节表示,read只能读一个字节,就会出现三个乱码 如果全读完了,继续read,那么会返回-1. 这种方法效率很低,就像是一次只取一滴水,一次只返回1个字节,如果遇到中文编码还会乱码,一般不会使用. 现在希望一次不是能取一滴水,而是能取一桶水. 方法是先定义一个byte[]的数组当桶,然后把数组传进read()方法里面,这样每次读取就会把桶装满. byte[] buffer = new byte[3]; int read = fileInputStream.read(buffer); 此时read方法会把buffer这个桶装满,里面会装上三个字节的数据.然后返回一个int整数,表示这次装进去了多少水(几个字节) 再用new String(buffer)把这个数组传进去解码,就可以获得一个读取出来解码后的字符串了. 比如文件里只有2个字节的内容,此时定义的buffer桶要装3个,那么read方法就会把文件里这两个字节放进数组的前两位索引上,然后返回2,代表这次read了2字节 真实使用的时候可能文件比较大,一桶一次要装1024滴水,那么很有可能留下一个隐患,就是每次水桶用过可能倒不干净. 比如文件一共有1524个字节,此时第一次取一桶水,不会出现问题. 但是第二次取,文件当中只有500个字节了,read会把这500个字节传入数组的0-499索引 而后面的全部索引因为没有改动,还保留着之前文件的501-1024字节内容,再次解码这桶水,结果就不是我们想要的结果了. 为了避免这种情况的出现,在new String解码的时候,我们需要告诉他需要解码到第几位,而这个位数正是调用read(buffer)方法返回的整数 int read = fileInputStream.read(buffer); new String(buffer,0,read) 这代表从0索引开始解码,到read索引结束,这样就不会出现解码到之前没倒掉的水的问题了. 再次改进代码,使用循环来读取信息,代码如下: FileInputStream fileInputStream = new FileInputStream("javasepromax\\DAY09-oop-demo\\abc.txt"); byte[] buffer = new byte[1024]; int len; //如果这里没内容可读了,那么read方法会返回-1,不等于-1证明后面有内容 while((len = fileInputStream.read(buffer))!=-1){ //从0解码到len System.out.print(new String(buffer,0,len)); } 虽然这个方法改善了读取性能,但是还是无法避免中文乱码.因为如果前面1022个字节都是字母,而1023+1024+1025是一个汉字,那么就会出现乱码 问题. 要解决这个问题,只需要一次性把文件读完即可,也就是让这个桶一次性装全部的水 File file = new File("javasepromax\\DAY09-oop-demo\\abc.txt"); 先找到这个文件,创建一个file对象 FileInputStream fileInputStream = new FileInputStream(file); byte[] buffer = new byte[(int) file.length()]; 此时直接让桶的大小等于这个对象的大小,注意需要把文件的length返回的long类型强转成int类型. 但此处有一个风险,那就是数组是装在内存里的,文件是装在硬盘里. 如果文件过大,可能这个数组占满内存都装不下. 但实际开发也不会需要处理这么大的文件,所以还是选择强转 fileInputStream.read(buffer);一次装满 System.out.println(new String(buffer)); 不用考虑别的直接解码即可 Java也想到了这个方法,在JDK9时在FileInputStream里提供了readAllBytes()的方法,直接调用即可,不用自己定义桶了 File file = new File("javasepromax\\DAY09-oop-demo\\abc.txt"); 先找到这个文件,创建一个file对象 FileInputStream fileInputStream = new FileInputStream(file); byte[] bytes = fileInputStream.readAllBytes(); 直接全读取了,返回一个大桶装满了水 System.out.println(new String(bytes)); 解码这个大桶 */ /* OutputStream 和InputStream类似,OutputStream也是一个抽象类,我们一般使用他的子类fileOutputStream来创建对象 fileOutputStream相当于建立起了一个内存到硬盘的通道,以便让我们写入数据,同样使用多态,创建对象的代码如下: OutputStream fileOutputStream = new FileOutputStream("javasepromax\\DAY09-oop-demo\\bac.txt"); 此时入参的地址,可以是一个已经存在的文件,也可以还未被创建. 如果未被创建,在写入数据的时候会自动创建这个文件. 注意,如果这样创建对象,每次这个流第一次写入数据时都会先清空这个文件之前的内容,再写入,等于是覆盖式管道. 如果不想清空, 需要在后面入参一个true OutputStream fileOutputStream = new FileOutputStream("javasepromax\\DAY09-oop-demo\\bac.txt",true); 写入数据的主要方法是write(),入参是一个字节,比如'a',97,等等. 比如: fileOutputStream.write(97); 既然可以入参字节,也就可以入参字节数组. 也就是说一次直接倒进去一桶水,而不是一滴一滴的倒 byte[] buffer = ['a','b','c']; fileOutputStream.write(buffer); 但是这样是不能传输汉字进去的,因为一个汉字是3字节,而write正常一次只能1字节,所以我们要先利用getBytes方法对汉字字符串进行编码, 得到字符串的字节数组,再传入write方法即可 byte[] buffer = "我是中国人".getBytes(); fileOutputStream.write(buffer); 与fileInputStream类似的,write方法可以选择数组的索引来选择性写入文件,如: fileOutputStream.write(buffer,0,9); 意思是只把buffer数组里0索引-9索引的内容写入文件,实际上就是正好3个汉字 如果一直这样write下去,那么所有的字都会写在一行,所以我们需要换行. 换行的方法是write("\r\n".getBytes()) 这个流写完数据以后需要进行刷新才能生效,需要调用flush()方法. 因为流是需要占用内存的,如果打开不关闭的话会影响运行性能. 如果要关闭流,需要close()方法,close()方法在关闭流之前会先flush刷新一次. */ /* 文件拷贝 有两种方法,对比较小的文件来说,先创建一个fileInputStream,把文件放进去直接readAll,再把获得的数组直接write进fileOutputStream即可 File file = new File("E:\\源文件"); FileInputStream fileInputStream = new FileInputStream(file); byte[] bytes = fileInputStream.readAllBytes(); FileOutputStream fileOutputStream = new FileOutputStream("E:\\log\\拷贝的文件"); fileOutputStream.write(bytes); fileOutputStream.close(); fileInputStream.close(); 如果文件比较大,可以选择分批次复制,也就是定义个桶,一桶一桶来复制. File file = new File("E:\\源文件"); FileInputStream fileInputStream = new FileInputStream(file); FileOutputStream fileOutputStream = new FileOutputStream("E:\\log\\拷贝的文件"); byte[] bytes = new byte[1024]; int len; while((len=fileInputStream.read(bytes))!=-1){ fileOutputStream.write(bytes,0,len); } fileOutputStream.close(); fileInputStream.close(); 这样写代码有可能出现问题和漏洞,比如如果出现一些BUG导致程序运行中挂掉,那么流的close关闭是无法执行的,流就会占用内存 比较好的办法是利用trycatch-finally,因为finally后面的代码一定会被执行,所以一定可以关掉这个流. 但是因为这两个流是在try代码块内部定义的,所以无法直接访问,建议是把这两个流定义在外面,指向null,然后创建的时候再调用这两个流 但是这样还是有问题,因为代码块内的代码可能会在这两个流创建之前就出bug,这样导致finally里的close方法会出现空指针异常 所以在finally里面还要对这两个流进行非空校验,最终代码如下: FileInputStream fileInputStream = null; //先定义出两个流,指向null,方便finally代码块也能调用. FileOutputStream fileOutputStream = null;//如果定义在try里面,finally代码块无法调用 try { File file = new File("E:\\源文件.zip"); fileInputStream = new FileInputStream(file); fileOutputStream = new FileOutputStream("E:\\log\\拷贝的文件2.zip"); byte[] bytes = new byte[1024]; int len; while((len=fileInputStream.read(bytes))!=-1){ fileOutputStream.write(bytes,0,len); } fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } finally { //即使上面出现bug,finally里面也会被执行. //进行非空校验是为了避免还没生成这两个流,代码就已经出问题了 //比如代码第一行就出问题,然后这两个流还没被赋值.如果直接关闭就会空指针异常 if (fileInputStream!=null){ fileInputStream.close(); } if (fileOutputStream!=null){ fileOutputStream.close(); } } 另外补充一点,即使代码里有return返回,也一定会执行finally里的代码内容. 所以如果finally里面写了return,那么无论如何return的都是finally里面的内容. 但是这样写逻辑过于繁琐,实际上这两个类都属于是资源类,他们实现了Closeable可关闭接口,实际上是可以自动关闭的. 我们使用trycatch时,其实可以在try后面加上一个(),在里面定义这些资源.当跑完整块代码以后,trycatch会自动帮我们把这些资源关闭 这样无需再用finally去手动关闭,自然也无需非空校验,大大简化了代码的复杂程度. 代码优化如下: try ( FileInputStream fileInputStream = new FileInputStream("E:\\源文件"); FileOutputStream fileOutputStream = new FileOutputStream("E:\\log\\拷贝的文件2.zip"); ) { File file = new File("E:\\二乔.zip"); byte[] bytes = new byte[1024]; int len; while ((len = fileInputStream.read(bytes)) != -1) { fileOutputStream.write(bytes, 0, len); } fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } 值得注意的是,try()里面只能定义和初始化资源,如果不是资源会报错. 资源就是实现了可关闭接口的类. 另外,在JDK9以后优化了这一功能,如果这两个资源已经被定义好,可以直接放在try()里,并用分号链接.比如: try(资源1;资源2){}catch(){} 当代码完成后,会自动释放资源1和资源2 */ /* 字符流 如果我们要读取文件里的字符,直接用IO流里面的字节流会出现一些问题.比如一个汉字是3个字节,如果被分割或者分别读取,就会出现错误 字符流善于读写文本,按照字符来进行操作,不会出现因为汉字就乱码的情况,所以需要学习字符流. 字符流里面有两个抽象类,Reader和Writer. 主要的实现类和之前的字节流类似,是fileReader和fileWriter fileReader read()方法,每次读取一个字符,如果取到了就会返回一个int值,把他强转成char打印即可看到内容.如果没有取到就会返回-1 和字节流类似的,我们可以定义一个字符数组,(注意,字节流创建的是byte[],而字符流是char[])当做一个桶,然后一次读取多个字符. Reader reader = new FileReader("javasepromax\\DAY09-oop-demo\\abc.txt"); char[] chars = new char[1024]; int len; while((len = reader.read(chars))!=-1){ String s = new String(chars, 0, len); System.out.print(s); } fileWriter write()方法,可以写字符串,也可以字符,也可以字符数组 创建fileWriter时候直接创建,会创建一个覆盖式管道,每次运行会把之前内容全清空再写入. 如果想添加不动原来文件的内容,需要后面填写一个true参数. FileWriter fileWriter = new FileWriter("javasepromax\\DAY09-oop-demo\\abc.txt", true); 写入以后也需要flush()和close() */