------该流提供了打印方法,可以将各种数据类型的数据都原样打印。
4,字符输出流,Writer。
程序示例:
package tan; import java.io.BufferedReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; public class PrintStreamDemo { public static void main(String[] args) throws IOException { BufferedReader bufr=new BufferedReader(new InputStreamReader(System.in)); //将数据输出到控制台 PrintWriter out=new PrintWriter(System.out,true); //将数据存入文件中:将za.txt文件封装到流中可实现自动刷新 PrintWriter out=new PrintWriter(new FileWriter("za.txt"),true);//构造函数,参数为流、自动刷新设置。 String line=null; while((line=bufr.readLine())!=null){ if("over".equals(line)){//输入结束标记 break; } out.println(line.toUpperCase());//将输入的字符转为大写 // out.flush(); 刷新只针对流 } out.close(); bufr.close(); } }
SequenceInputStream是能对多个流进行合并成一个读取流,它在构造时需要传入Enumeration,而这个只用Vector中有,所以这个多个读取流要加入Vector集合中。注意:它只是对读取流进行合并。
使用步骤:
1. 创建Vector<InputStream>
2. 将要合并的InputStream加入Vector
3. 通过Vector获取Enumeration
4. 创建SequenceInputStream,将Enumeration作为参数传入。
程序示例:
package tan; import java.io.*; import java.util.*; public class SequenceDemo { public static void main(String[] args) throws IOException { Vector<FileInputStream>v=new Vector<FileInputStream>(); v.add(new FileInputStream("G:\\练习文件-workspace\\20140731\\1.txt")); v.add(new FileInputStream("G:\\练习文件-workspace\\20140731\\2.txt")); v.add(new FileInputStream("G:\\练习文件-workspace\\20140731\\3.txt")); Enumeration<FileInputStream>en=v.elements(); SequenceInputStream sis=new SequenceInputStream(en); FileOutputStream fos=new FileOutputStream("G:\\练习文件-workspace\\20140731\\4.txt"); byte []buf=new byte[1024]; int len=0; //序列流读取read()缓冲区,传入参数buf while((len=sis.read(buf))!=-1){ fos.write(buf, 0, len); } fos.close(); sis.close(); } }
package tan; import java.io.*; import java.util.*; public class SplitFile { public static void main(String[] args)throws IOException { // splitFile(); merge(); } //流切割 public static void merge()throws IOException{ //Vector效率低,可以改用ArrayList ArrayList<FileInputStream>al=new ArrayList<FileInputStream>(); for(int x=1;x<=7;x++){ al.add(new FileInputStream("G:\\练习文件-workspace\\20140731\\分割文件\\"+x+".part")); } //因为it要被Enumeration的匿名内部类对象使用,所以要加final final Iterator<FileInputStream>it=al.iterator(); //定义Enumeration子类对象进行,使用ArrayList的迭代器复写其方法,其实与ArrayList关联 Enumeration<FileInputStream>en=new Enumeration<FileInputStream>() { @Override public FileInputStream nextElement() { return it.next(); } @Override public boolean hasMoreElements() { return it.hasNext(); } }; //定义序列流,合并分割后的文件关联的流对象 SequenceInputStream sis=new SequenceInputStream(en); FileOutputStream fos=new FileOutputStream("G:\\练习文件-workspace\\20140731\\分割文件\\merge.pdf"); byte []buf=new byte[1024]; int len=0; while((len=sis.read(buf))!=-1){ fos.write(buf, 0, len); } fos.close(); sis.close(); } //流切割 public static void splitFile() throws IOException{ FileInputStream fis=new FileInputStream("G:\\练习文件-workspace\\20140731\\java复习.pdf");//关联文件 FileOutputStream fos=null; byte[]buf=new byte[1024*1024];//创建1M的缓冲区 int len=0; int count=1;//创建从1开始的碎片 while((len=fis.read(buf))!=-1){ fos=new FileOutputStream("G:\\练习文件-workspace\\20140731\\分割文件\\"+(count++)+".part"); fos.write(buf, 0, len); fos.close(); } fis.close(); } }
数据可以封装成对象,对象运行时是在堆内存中的,如果对象的数据需要存储在硬盘上,那么就要用到对象的序列化流。对象序列化(也叫对象的可串行性)其实就是对象持久化,把内存中的对象,变成硬盘上的文件内容。IO中供对象序列化的流对象为ObjectInputStream和ObjectOutputStream。
注意:
1. 用ObjectOutputStream写入的的文件,只能用ObjectInputStream来重构读取。
2. 被序列化的对象必须实现Serializable接口。
3. 对象的静态成员和被transient关键字修饰的成员不能被序列化。
(当对象在堆内存的私有对象不希望被序列化时,可以使用transient关键字)。
此外,序列化的文件一般以.ojbect作为类型后缀名,一个文件中可以存放多个不同类型的序列化对象
在对对象进行序列化时,必须实现Serializable接口,否则使用ObjectOutputStream写入时,会出现NotSerializableException异常。
Serializable接口并没必须要实现的方法,类定义时仅标示一下实现即可。实现Serializable的类都有serialVersionUID,如果你没有在类中显式定义一个serialVersionUID,那么编译器会根据该类中的成员生成一个具有唯一性的serialVersionUID。
显式定义serialVersionUID的好处:
如果你在对类对象进行了序列化之后,又修改了这个类,那么再次读取修改前序列化的对象时,编译器可以识别;如果没有显式定义,你修改后的类经过编译器编译后会生成一个新的serialVersionUID,这个serialVersionUID跟修改前类的serialVersionUID不同,当你再次读取时,编译器会报出InvalidClassException异常。所以,如果类对象需要序列化,建议显式定义serialVersionUID。
1、先定义实现实现Serializable接口的类
package TZQ; import java.io.Serializable;//没有方法的接口,称为标记接口, public class Person implements Serializable { public static final long serialVersionUID = 66L;//UID是为了给类定义标记。 private String name; transient int age;//age如果不想序列化,可以在前边加 transient 关键字,保证其值在堆内存中存在而不在文本文件中存在。 static String country="CN";//静态成员不能被序列化,其在方法区。 public Person(String name, int age, String country) { this.name = name; this.age = age; this.country = country; } @Override public String toString() { return name+":"+age+":"+country; } }2、演示对象的序列化以及读取序列化后的对象
package TZQ; import java.io.*; import java.util.*; public class ObjectStreamDemo { public static void main(String[] args) throws Exception { // writeObj(); readObj(); } //1、通过ObjectOutputStream将对象序列化 public static void writeObj()throws IOException{ //将序列化后的对象存入Person.object文件中 ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("Person.object")); oos.writeObject(new Person("Jery", 22, "KR")); //country为静态,不能序列化,所以,写入文件中的不是“kR”,而是CN oos.close(); } //2、通过ObjectInputStream读取序列化后的对象 public static void readObj()throws Exception{ ObjectInputStream ois=new ObjectInputStream(new FileInputStream("Person.object")); Person p=(Person)ois.readObject(); System.out.println(p); } }
管道流分为字节管道流(PipedInputStream和PipedOutputStream)和字符管道流(PipedReader和PipedWriter):它是IO技术和多线程技术的结合。在一条线程上写入的数据可以在另外一条线程上读取,它们是一对对配合使用的。如:在一条线程上使用管道读取和写入流会发生死锁的情况。
普通流:
管道流:
使用步骤:
1. 分别定义写入和读取的Runnable接口子类,把相应的管道流作为构造参数传入给定义的私有管道流成员。
2. 将配对的管道流通过connect()方法连接起来。
3. 启动线程
程序示例:
package TZQ; import java.io.*; class Read implements Runnable{ private PipedInputStream in; public Read(PipedInputStream in) { this.in = in; } @Override public void run() { try { byte[]buf=new byte[1024]; System.out.println("读取前没有数据,阻塞中····"); int len=in.read(buf); System.out.println("读到数据,阻塞结束。"); String s=new String(buf,0,len); System.out.println(s); in.close(); } catch (Exception e) { throw new RuntimeException("管道读取流--失败!"); } } } class Write implements Runnable{ private PipedOutputStream out; public Write(PipedOutputStream out) { this.out = out; } @Override public void run() { try { System.out.println("等待5秒后····开始写入数据"); Thread.sleep(5000); out.write("Testing piped".getBytes()); out.close(); } catch (Exception e) { throw new RuntimeException("管道输出流---失败!"); } } } public class PipedDemo { public static void main(String[] args) throws IOException { // 分别定义写入和读取的Runnable接口子类,把相应的管道流作为构造参数传入给定义的私有管道流成员。 PipedInputStream in=new PipedInputStream(); PipedOutputStream out=new PipedOutputStream(); //将配对的管道流通过connect()方法连接起来。 in.connect(out); Read r=new Read(in); Write w=new Write(out); // 启动线程 new Thread(r).start(); new Thread(w).start(); } }
RandomAccessFile,随机访问文件流对象,该类不是Io体系的成员,但是它在IO包中,因为它具备读写功能,其内部封装了一个数组,而且通过指针对数组的元素进行操作。可以通过getFilePointer获取指针位置,同时可以通过seek方法改变指针的位置。它与其他IO流最大的不同在于:它既能读,又能写。
它自身的特有功能:可以通过改变指针位置和设置缓冲区字节数组的长度来访问文件中的任意一段字节。注意:如果希望能正确的读取,那么存入的数据最好是有规律的,比如8个字节为一段,其中后四个字节是int值(后四个字节就要用writeInt和readInt来读写)。延伸一下,就能发现,它能实现数据的分段写入(这与下载的原理:多线程下载类似)。
其实现原理:内部封装了字节输入流和输出流。
构造函数特点:通过构造函数看出,该类只能操作文件,而且操作文件还有模式:只读—“r”,读写---“rw”。
注意:
1. 如果模式为只读r,不会创建文件,而会去读取一个已经存在的文件,如果该文件不存在,则会抛出异常。
2. 如果模式的读写rw,操作的文件不存在,会自动创建,如果存在则不会覆盖。
程序示例:
package TZQ; import java.io.*; public class RandomAccessFileDemo { public static void main(String[] args) throws IOException{ // readFile(); // writeFile(); writeFile_2(); //如果模式为只读r,不会创建文件,而会去读取一个已经存在的文件,如果该文件不存在,则会抛出:java.io.FileNotFoundException: 1.txt (系统找不到指定的文件。) //RandomAccessFile raf=new RandomAccessFile("1.txt", "r"); // 如果模式的读写rw,操作的文件不存在,会自动创建,如果存在则不会覆盖。 RandomAccessFile raf=new RandomAccessFile("1.txt", "rw"); raf.write("Hello".getBytes()); } //写入模式,模式设置为:“rw” public static void writeFile() throws IOException{ RandomAccessFile raf=new RandomAccessFile("raf.txt", "rw"); raf.write("正强".getBytes()); // raf.write(23);write(int x)方法只写入低8位。如果写入的数字在byte取值范围内,那么可以read()正常读取,如果超出,读取时就会出现数据错乱。 raf.writeInt(23); raf.write("小强".getBytes()); raf.writeInt(66); raf.close(); } //读取,模式设置为:“r” public static void readFile()throws IOException{ RandomAccessFile raf=new RandomAccessFile("raf.txt", "r"); //调整对象中的指针,seek前后都能设置,所以比skipBytes使用范围广。 raf.seek(8*1);//里边存入的数据都是8个字节为一组,如果没有规律,读取就困难了 //跳过指定的字节数,只能往后走,不能往回走。 // raf.skipBytes(8); byte[]buf=new byte[4]; raf.read(buf); String name=new String(buf); int age=raf.readInt(); System.out.println("name:"+name); System.out.println("age:"+age); raf.close(); } public static void writeFile_2()throws IOException{ RandomAccessFile raf=new RandomAccessFile("raf.txt", "rw"); raf.seek(8*3);//也可指定位置插入数据 //修改数据,网络分段下载原理,要重点掌握。 raf.write("传奇".getBytes()); raf.writeInt(88); raf.close(); } }
可以用于操作基本数据类型的数据的流对象,按照输入输出分为DataInputStream和DataOutputStream。这两个类提供了对8种基本数据类型的写入和读取的方法。
此外,它们有对应的wirtUTF-8(String str)和readUTF-8()方法,来支持按照UTF-8修改版编码(与UTF-8稍有不同)来写入和读取字符串。
注意:
'\u0000'
是用 2-byte 格式而不是 1-byte 格式编码的,因此已编码的字符串中决不会有嵌入的 null。程序示例:
package TZQ; import java.io.*; public class DataStreamDemo { public static void main(String[] args) throws IOException { // writeData(); // readData(); writeUTFDemo(); readUTFDemo(); //普通编程存储--->转换流utf-8 或者GBK // OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("GBK.txt"),"GBK"); // osw.write("你好"); // osw.close(); } //读取数据 public static void readData()throws IOException{ DataInputStream dis=new DataInputStream(new FileInputStream("data.txt")); int num=dis.readInt(); boolean b=dis.readBoolean(); double d=dis.readDouble(); System.out.println("num="+num+" b="+b+" d="+d); dis.close(); } //写数据 public static void writeData()throws IOException{ DataOutputStream dos=new DataOutputStream(new FileOutputStream("data.txt")); dos.writeInt(226);//4个字节 dos.writeBoolean(true);//1个字节 dos.writeDouble(666.23);//8个字节 dos.close(); } // 写utf格式的数据 public static void writeUTFDemo()throws IOException{ DataOutputStream dos=new DataOutputStream(new FileOutputStream("utf.txt")); dos.writeUTF("你好!"); dos.close(); } // 读utf格式的数据 public static void readUTFDemo()throws IOException{ DataInputStream dis=new DataInputStream(new FileInputStream("utf.txt")); String s=dis.readUTF(); System.out.println(s); dis.close(); } }
按输入输出分为两个类:
注意:
1.因为这两个流对象都操作的是数组,并没有使用系统资源,所以,不用进行close关闭,即使你关闭了,它的 其他方法还可以使用,而不会抛出IOException。
2.这对对象操作时,它的源和目的都是内存。
用途:
这两个对象是在用流的思想来操作数组,当我们需要把一个文件中的数据加入内存中的数组时,就可以考虑用这个两个对象。此外,它还有writeTo(OutputStream os)可以把ByteArrayOutputStream对象内部定义的缓冲区内容,一次性写入os中。
操作字符数组、字符串的流对象类型与之相似,可以参考它们的使用方法。
package TZQ; import java.io.*; public class ByteArrayStream { public static void main(String[] args) throws IOException{ //数据源---字节数组,在内存中 ByteArrayInputStream bis=new ByteArrayInputStream("ABCEDF".getBytes()); //数据目的--bos内部封装的数组,在内存中 ByteArrayOutputStream bos=new ByteArrayOutputStream(); int by=0; while((by=bis.read())!=-1){ bos.write(by); } System.out.println(bos.size());//返回缓冲区大小 System.out.println(bos.toString());//把缓冲区中的字节按照默认的编码转成为字符串返回。 // writeTo()方法需要抛出异常 // bos.writeTo(new FileOutputStream("a.txt"));//把bos内部的byte数组内容一次性写入字节输出流对象中 } }
字符流的出现是为了方便操作字符数据,其方便操作的原因是因为内部加入了编码表。Java中能够实现字节根据指定编码表转成字符的,有四个类:InputStreamReader和OutputStreamWriter,PrintStream和PrintWriter。它们都能够在构造时指定编码表;但后两个是打印流,只能用于打印,使用有局限,所以相对而言还是前两个转换流使用多一些。
计算机只能识别二进制数据,早期是电信号。为了应用计算机方便,让它可以识别各个国家的文字,就将各个国家的文字用数字来表示,并将文字与二进制数字一一对应,形成了一张表,这个表就是编码表。
地域码表
1. ASCII:美国码表,息交换码,用一个字节的7位表示。
2. ISO8859-1:欧洲码表,拉丁码表,用一个字节的8位表示,最高位1
3. GB2312:中国中文编码表,它用两个字节表示,为兼容ASCII,它的两个字节的高位都是1,也即是两个负数;但与ISO8859-1冲突。大概有六七千个字。
4. GBK:中国的中文编码表的升级版,扩容到2万多字。
通用码表:
1. Unicode:国际标准码,融合多种语言文字。所有的文字都用两个字节表示,Java默认使用的就是Unicode。
2. UTF-8:UnicodeTransform Format -8。Unicode码把用一个字节能装下的文字,也用两个字节表示,有些浪费空间,对之进行优化的结果就是UTF-8。UTF-8编码表,一个文字最少用一个字节表示,最多用3个字节表示,并且每个字节开始都有标识头,所以很容易于其他编码表区分出来。
UTF-8每次是如何判断是该读1个、2个还是3个字节呢?用标示头信息,如下所示。
'\u0001'
到 '\u007F'
范围内的所有字符都是用单个字节表示的:
位值 字节 1
0 位 6-0
null 字符 '\u0000'
以及从 '\u0080'
到 '\u07FF'
的范围内的字符用两个字节表示:
位值 字节 1
1 1 0 位 10-6 字节 2
1 0 位 5-0
'\u0800'
到 '\uFFFF'
范围内的 char
值用三个字节表示:
位值 字节 1
1 1 1 0 位 15-12 字节 2
1 0 位 11-6 字节 3
1 0 位 5-0
package TZQ; import java.io.*; public class EncodeStream { public static void main(String[] args) throws IOException { writeText(); readText(); } //转换流的字符编码 //2.以utf-8进行解码 public static void readText()throws IOException{ InputStreamReader isr=new InputStreamReader(new FileInputStream("utf.txt"),"UTF-8"); char[]buf=new char[10]; int len=isr.read(buf); String str=new String(buf,0,len); System.out.println(str); isr.close(); } //1.以utf-8进行编码(可以指定编码方式:utf-8,gbk) public static void writeText()throws IOException{ OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("utf.txt"),"UTF-8"); osw.write("你好"); osw.close(); } }
从上边的那些编码表可以看出,GBK和Unicode都能识别中文,那么当一台电脑使用GBK,而另一台电脑使用Unicode时,虽然在各自的电脑上都能识别中文,但他们其中一方向另一方发送中文文字时,另一方却不能识别,出现了乱码。这是因为GBK和Unicode虽然都能识别中文,但对同一个中文文字,他们在两个编码表对应的编码值不同。这时,在解读别人传来的中文数据时,就需要指定解析中文使用的编码表了。
而转换流就能指定编码表,它的应用可以分为:
1. 可以将字符以指定的编码格式存储。
2. 可以对文本数据以指定的编码格式进行解读。
它们指定编码表的动作是由构造函数完成的。
编码:字符串变成字节数组,String--> byte[ ] ,使用str.getBytes(charsetName);
解码:字节数组变成字符串,byte[]-->String,使用new String(byte[] b, charsetName);
编码编错:是指你对一个文字进行编码时,使用了不识别该文字的编码表,比如你编码一个汉字,却使用了ISO8859-1这个拉丁码表,ISO8859-1根本就不识别汉字。编码编错时,你用任何方式对编码后的数据进行处理,都不可能再拿到这个汉字了。
解码解错:是指你对一个文字进行编码时,使用了正确的码表,编码正确,但在解码时使用了错误的码表,那么你还有可能拿到这个文字。这分为两种情况:
第一种情况:你使用的是GBK编码,解码时用的是ISO8859-1,因为GBK编译一个汉字,使用两个字节,ISO8859-1解码时是一个字节一个字节读取,虽然解码出现了乱码,但是这个汉字的二进制数据没有变化,那么你可以通过再次编译获取其原来的二进制数据,然后再次使用GBK编码,解码成功。
第二种情况:你使用的是GBK编码,解码时用的却是UTF-8,因为这两个码表都识别汉字,那么你再次使用UTF-8编码时,就有可能把一个汉字的2个字节,变成3个,这时再用GBK解码时,得到的仍然是乱码,解码仍然失败。
package TZQ; import java.util.*; import java.io.UnsupportedEncodingException; public class EncodeStream1 { public static void main(String[] args) throws Exception { String s="你好"; //编码 byte [] b1=s.getBytes("GBK"); // byte [] b1=s.getBytes("iso8859-1"); //不识别中文 System.out.println(Arrays.toString(b1));//[-60, -29, -70, -61] //解码 String s1=new String(b1,"iso8859-1");//此时得到乱码 System.out.println("s1="+s1);//s1=???? // 对s1进行iso8859-1编码。 byte[] b2 = s1.getBytes("iso8859-1"); //对乱码在进行编码 System.out.println(Arrays.toString(b2));//[-60, -29, -70, -61] //解密 String s2 = new String(b2, "GBK"); System.out.println("s2=" + s2); //s2=你好 } }
问题描述:打开记事本仅写入“联通”两个汉字,关闭后,再次打开会出现乱码。
package TZQ; public class EncodeStream2 { public static void main(String[] args) throws Exception { String s = "联通"; byte[] by = s.getBytes("gbk"); for(byte b : by) { //toBinaryString(int)它接受的是int,byte类型的b参与运算时会类型提升为int,我们需要的是提升后的低8位,所以&255 System.out.println(Integer.toBinaryString(b&255)); } } }
“联通”编码问题的原因:
对联通的结果进行GBK编码时,其二进制码为:
11000001
10101010
11001101
10101000
编码的结果符合UTF-8的格式,所以再次打开记事本时,它会把它按照UTF-8的格式进行解码,结果就是两个乱码。
解决办法:在联通两字前面加上任意汉字即可。