概述
按流向分为输入流和输出流
按操作数据分为字节流和字符流
字符流,基于字节流,但是字符流对象融合了编码表
文字用字符流,图片用字节流
IO体系的四个基类
字节流的抽象基类
InputStream OutputStream
字符流的抽象基类
Reader Writer
由这四个类派生出来的子类名称都是以其父类名作为后缀,比如FileInputStream,FileReader
前缀名是流对象的功能
字符流
字符写入流FileWriter
父类Writer中的构造方法被protected修饰表示只给子类访问 数据的最常见体现形式是:文件 专门用于操作文件的Writer子类对象:FileWriter(继承OutputStreamWriter类)
//创建一个FileWriter对象,该对象一被创建就必须明确被操作的文件,而且该文件会被创建到指定的目录下,如果该目录下有同名文件,会被覆盖:
FileWriter fw = new FileWriter("demo.txt"); //注意会抛出异常:比如文件路径不存在
//调用write方法,将字符串写入到流中
fw.write("abcde");
//刷新流对象中的缓冲,将其中的数据刷到目的地中
fw.flush();
//关闭流资源,但是关闭之前会刷新一次内部的缓冲中的数据。因为Java会调用系统中的内容来完成数据的写入,完成之后必须将资源释放,所以close
//将数据刷到目的地中。
//和flush区别:flush刷新后,流可以继续使用,close刷新后,会将流关闭。
fw.close();
IO异常
在IO体系中,IO异常时最为常见的 第一句发生异常,下面执行无意义,所以一起try
FileWriter fw = null;//在外面建立引用,在try里初始化,如果都写在try里,关闭流对象会发生编译错误,因为fw引用作用不到try以外
try
{
fw = new FileWriter("demo.txt");
fw.write("abcdefg");
}
catch (IOException e)
{
System.out.println("catch:"+e.toString());
}
finally //流关闭,必须执行
{
try//close也会发生异常,需要单独用try处理
{
if(fw!=null)//不判断会发生空指针异常
fw.close();
}
catch (IOException e)
{
System.out.println(e.toString());
}
}
文件的续写
/*
演示对已有文件的数据续写。
*/
import java.io.*;
class FileWriterDemo3
{
public static void main(String[] args) throws IOException
{
//传递一个true参数,代表不覆盖已有的文件。并在已有文件的末尾处进行数据续写。
FileWriter fw = new FileWriter("demo.txt",true);
fw.write("nihao\r\nxiexie");//为了在windows记事本下显示换行,用到了\r\n,linux系统为\n
fw.close();
}
}
字符读取流FileReader
Reader的close方法不刷新,只是关闭资源
方式一:读取单个字符
//创建一个文件读取流对象,和指定名称的文件相关联。
//要保证该文件是已经存在的,如果不存在,会发生异常FileNotFoundException
FileReader fr = new FileReader("demo.txt");
//调用读取流对象的read方法。
//read():一次读一个字符。而且会自动往下读。返回的是作为整数读取的字符
int ch = 0;
while((ch=fr.read())!=-1) //返回-1表示文件已经读到末尾
{
System.out.println("ch="+(char)ch);
}
fr.close();
方式二:通过字符数组进行读取
FileReader fr = new FileReader("demo.txt");
//定义一个字符数组。用于存储读到字符。
//该read(char[])返回的是读取的字符数。
char[] buf = new char[1024]; //通常定义1024的整数倍,这里定义了一个2k大小的字符数组,因为一个字符是两个字节
int len = 0;
while((len=fr.read(buf))!=-1)
{
System.out.println(new String(buf,0,len));//读到多少就打出多少,因为最后一次循环可能不会读满整个数组,如果只打整个数组就会出现多余的字符
}
fr.close();
总结:方式二好,读一串存一下,再全部打印,效率高
拷贝文本文件
//将C盘一个文本文件复制到D盘。
/*
复制的原理:
其实就是将C盘下的文件数据存储到D盘的一个文件中。
步骤:
1,在D盘创建一个文件。用于存储C盘文件中的数据。
2,定义读取流和C盘文件关联。
3,通过不断的读写完成数据存储。
4,关闭资源。
*/
import java.io.*;
class CopyText {
public static void main(String[] args) throws IOException {
copy_2();
}
//方式一:从C盘读一个字符,就往D盘写一个字符。(未加入IO异常处理,所以需要抛出异常对象)
public static void copy_1()throws IOException {
//创建目的地。
FileWriter fw = new FileWriter("RuntimeDemo_copy.txt");
//与已有文件关联。
FileReader fr = new FileReader("RuntimeDemo.java");
int ch = 0;
while((ch=fr.read())!=-1) {
fw.write(ch);
}
fw.close();
fr.close();
}
//方式二:利用缓存数组读写(加入了IO异常处理)
public static void copy_2() {
FileWriter fw = null;
FileReader fr = null;
try{
fw = new FileWriter("SystemDemo_copy.txt");
fr = new FileReader("SystemDemo.java");
char[] buf = new char[1024];
int len = 0;
while((len=fr.read(buf))!=-1){ //使用数组进行读取
fw.write(buf,0,len);//使用数组进行写入
}
}
catch (IOException e){
throw new RuntimeException("读写失败");
}
finally{
if(fr!=null)
try{
fr.close();
}catch (IOException e){
throw new RuntimeException("读取流关闭失败");
}
if(fw!=null)
try{
fw.close();
}catch (IOException e){
throw new RuntimeException("写入流关闭失败");
}
}
}
}
要点:read(buf)使用数组进行读取,write(buf,0,len)使用数组进行写入
字符流的缓冲区
缓冲区的出现是为了提高流的操作效率,所以在创建缓冲区对象之前,要有流对象 只要将需要被提高效率的流对象作为参数传递给缓冲区的构造函数即可 缓冲区要结合流才可以使用,在流的基础上对流的功能进行了增强 原理:缓冲区对象里面封装了数组,不需要再自定义数组了
BufferedWriter
字符写入流缓冲区
该缓冲区对象提供了一个跨平台的换行符 newLine()方法
//创建一个字符写入流对象。
FileWriter fw = new FileWriter("buf.txt");
//为了提高字符写入流效率。加入了缓冲技术。
//只要将需要被提高效率的流对象作为参数传递给缓冲区的构造函数即可。
BufferedWriter bufw = new BufferedWriter(fw);
for(int x=1; x<5; x++)
{
bufw.write("abcd"+x);
bufw.newLine();
bufw.flush(); //防止因为断电导致数据丢失,所以刷新一句,写入一句
}
//记住,只要用到缓冲区,就要记得刷新。
//bufw.flush();
//其实关闭缓冲区,就是在关闭缓冲区中的流对象,不用再关闭流对象了
bufw.close();
BufferedReader
字符读取流缓冲区 该缓冲区提供了一个一次读一行的方法 readLine(),方便于对文本数据的获取。 当返回null时,表示读到文件末尾。
readLine方法返回的时候只返回回车符之前的数据内容。并不返回回车符。
//创建一个读取流对象和文件相关联。
FileReader fr = new FileReader("buf.txt");
//为了提高效率。加入缓冲技术。将字符读取流对象作为参数传递给缓冲对象的构造函数。
BufferedReader bufr = new BufferedReader(fr);
String line = null;
while((line=bufr.readLine())!=null)
{
System.out.println(line);//注意这里的打印加了换行符,因为readLine不返回回车符
}
bufr.close();
简写形式
BufferedReader bufr = null;
BufferedWriter bufw = null;
bufr = new BufferedReader(new FileReader("BufferedWriterDemo.java"));
bufw = new BufferedWriter(new FileWriter("bufWriter_Copy.txt"));
readLine()方法原理
无论是读一行或者一下读取多个字符,其原理都是在硬盘上一个一个读取字符,所以最终还是使用的read方法一次读一个,只是先将读取的数据存到数组中,当读到换行符时,不存入数组,将数组已存储的字符全部输出
MyBufferedReader
/*
明白了BufferedReader类中特有方法readLine的原理后,
可以自定义一个类中包含一个功能和readLine一致的方法。
来模拟一下BufferedReader
*/
import java.io.*;
class MyBufferedReader extends Reader //需要继承Reader,因为装饰类和被装饰类属于同一体系
{
private Reader r;//技巧:定义一个 是的在整个类中都有效
MyBufferedReader(Reader r)
{
this.r = r;
}
//可以一次读一行数据的方法。
public String myReadLine()throws IOException //这里不用try和catch异常,而只将异常抛出,因为这里定义的是供调用者使用的功能,应该让调用者去处理异常
{
//定义一个临时容器。原BufferReader封装的是字符数组。
//为了演示方便。定义一个StringBuilder容器。因为最终还是要将数据变成字符串。
StringBuilder sb = new StringBuilder();
int ch = 0;
while((ch=r.read())!=-1)
{
if(ch=='\r')//这里不需要强转
continue; //读到该字符,不存入,继续while的下一循环读取下一个字符
if(ch=='\n')
return sb.toString();//读到该字符返回数组容器中的字符串形式
else
sb.append((char)ch);
}
if(sb.length()!=0) //缓冲区里面只要有数据,就把剩下的数据返回来,避免了因为最后一行末尾没有换行符而不能将最后一行输出的问题
return sb.toString();
return null;
}
/*
覆盖Reader类中的抽象方法,因为继承了该类
*/
public int read(char[] cbuf, int off, int len) throws IOException
{
return r.read(cbuf,off,len) ; //自己覆写该类麻烦,可以用传进来的子类来实现
}
public void close()throws IOException//需要覆写抽象的close方法,因为每个流对象关闭实现不同,直接关闭资源的和调用流对象close方法的两种
{
r.close();
}
public void myClose()throws IOException //关闭资源
{
r.close();
}
}
LineNumberReader
跟踪行号的缓冲字符输入流。有setLineNumber(int num)方法和getLineNumber方法设置开始行号和获取行号
自定义跟踪行号的缓冲字符输入流MyLineNumberReader
import java.io.*;
class MyLineNumberReader extends MyBufferedReader//使用装饰设计模式,继承父类的功能,利用关键字super引用父类对象
{
private int lineNumber;
MyLineNumberReader(Reader r)
{
super(r);//父类中已经进行了传递,直接调用父类的功能,因为<span style="font-family: Arial, Helvetica, sans-serif;">当父类中没有空参数的构造函数时,子类中必须手动通过super语句来显示指定要访问的父类中的构造函数</span>
}
public String myReadLine()throws IOException
{
lineNumber++;//每读取一行,就调用一次myReadLine方法,行号就自增一次。
return super.myReadLine();//父类中已经有了实现,直接调用
}
public void setLineNumber(int lineNumber)
{
this.lineNumber = lineNumber;
}
public int getLineNumber()
{
return lineNumber;
}
}
装饰设计模式
当想要对已有的对象进行功能增强时,可以定义类,将已有对象传入,基于已有的功能,并提供加强功能。
那么自定义的该类称为装饰类。
装饰类通常会通过构造方法接收被装饰的对象。
并 基于被装饰的对象的功能,提供更强的功能。所以该装饰类需要继承同一个父类,因为只是功能增强,事物没有变。
MyBufferedReader类使用了装饰设计模式 原因:后期进行功能增强时,不建议改源代码,所以用此设计模式更优化
装饰和继承的区别
使用继承设计: MyReader//专门用于读取数据的类。
|--MyTextReader
|--MyBufferTextReader
|--MyMediaReader
|--MyBufferMediaReader
|--MyDataReader
|--MyBufferDataReader
未使用多态的装饰设计:
class MyBufferReader
{
MyBufferReader(MyTextReader text)
{}
MyBufferReader(MyMediaReader media)
{}
}
上面这个类扩展性很差。
找到其参数的共同类型。通过多态的形式。可以提高扩展性:
class MyBufferReader extends MyReader
{
private MyReader r;
MyBufferReader(MyReader r)
{}
}
使用装饰设计:
MyReader//专门用于读取数据的类。
|--MyTextReader
|--MyMediaReader
|--MyDataReader
|--MyBufferReader
以前是通过继承将每一个子类都具备缓冲功能。
那么继承体系会复杂,并不利于扩展。
现在优化思想。单独描述一下缓冲内容。
将需要被缓冲的对象。传递进来。也就是,谁需要被缓冲,谁就作为参数传递给缓冲区。
这样继承体系就变得很简单。优化了体系结构。 装饰模式比继承要灵活。避免了继承体系臃肿。
而且降低了类与类之间的关系。
装饰类因为增强已有对象,具备的功能和已有的是相同的,只不过提供了更强功能。所以装饰类和被装饰类通常是都属于一个体系中的。 装饰设计模式将 类与类的继承关系变成了组合关系
字节流
想要操作图片数据,就要用到字节流 字节流不需要刷新,因为字符流需要先缓冲数据,然后查表解析,所以需要刷新,而字节流不需要,只需要关闭流资源
字节流下File的读写操作
import java.io.*;
class FileStream
{
public static void main(String[] args) throws IOException
{
readFile_3();
}
public static void readFile_3()throws IOException
{
FileInputStream fis = new FileInputStream("fos.txt");
// int num = fis.available();//available方法返回文件中字节数
byte[] buf = new byte[fis.available()];//定义一个刚刚好的缓冲区。不用在循环了。该方法慎用,当文件数据过大时,会造成内存溢出!!
fis.read(buf);
System.out.println(new String(buf));
fis.close();
}
public static void readFile_2()throws IOException
{
FileInputStream fis = new FileInputStream("fos.txt");
byte[] buf = new byte[1024]; //为readFile_1和readFile_3之间的折中方式
int len = 0;
while((len=fis.read(buf))!=-1)
{
System.out.println(new String(buf,0,len));
}
fis.close();
}
public static void readFile_1()throws IOException
{
FileInputStream fis = new FileInputStream("fos.txt");
int ch = 0;
while((ch=fis.read())!=-1)//read方法返回的是int型,如果是byte型,可能读到8个1时就结束循环
{
System.out.println((char)ch);
}
fis.close();
}
public static void writeFile()throws IOException
{
FileOutputStream fos = new FileOutputStream("fos.txt");
fos.write("abcde".getBytes()); //getBytes方法将字符串转换为字节数组
fos.close();
}
}
字节流的缓冲区
BufferedInputStream BufferedOutputStream
/*
演示mp3的复制。通过缓冲区。
BufferedOutputStream
BufferedInputStream
*/
import java.io.*;
class CopyMp3
{
public static void main(String[] args) throws IOException
{
long start = System.currentTimeMillis();
copy_2();
long end = System.currentTimeMillis();
System.out.println((end-start)+"毫秒");//打印复制时间
}
//通过自定义的字节流缓冲区(见下)完成复制
public static void copy_2()throws IOException
{
MyBufferedInputStream bufis = new MyBufferedInputStream(new FileInputStream("c:\\9.mp3"));
BufferedOutputStream bufos = new BufferedOutputStream(new FileOutputStream("c:\\3.mp3"));
int by = 0;
//System.out.println("第一个字节:"+bufis.myRead());
while((by=bufis.myRead())!=-1)
{
bufos.write(by);
}
bufos.close();
bufis.myClose();
}
//通过字节流的缓冲区完成复制。
public static void copy_1()throws IOException
{
BufferedInputStream bufis = new BufferedInputStream(new FileInputStream("c:\\0.mp3"));
BufferedOutputStream bufos = new BufferedOutputStream(new FileOutputStream("c:\\1.mp3"));
int by = 0;
while((by=bufis.read())!=-1)//read方法返回的是int型,如果是byte型,可能读到8个1时就结束循环
{
bufos.write(by);
}
bufos.close();
bufis.close();
}
}
自定义字节流缓冲区
先将硬盘上的数据通过FileInputStream的read方法存储到字节流缓冲区中,缓冲区大小有限制,然后通过缓冲区对象的read方法一次一个取出来
import java.io.*;
class MyBufferedInputStream
{
private InputStream in; //注意这里传的不是FileInputStream
private byte[] buf = new byte[1024*4];
private int pos = 0,count = 0;
MyBufferedInputStream(InputStream in)
{
this.in = in;
}
//一次读一个字节,从缓冲区(字节数组)获取。
public int myRead()throws IOException
{
//通过in对象读取硬盘上数据,并存储buf中。
if(count==0) //count等于0表示buf中无数据,需要重新从硬盘读取数据
{
count = in.read(buf);
if(count<0)//表示硬盘数据读取完毕
return -1;
pos = 0;//将指针归零,返回数组第一个元素
byte b = buf[pos];
count--;
pos++;
return b&255;//与上255,在原字节数据的前面补上了24个0,将读取的byte转为int
}
else if(count>0)
{
byte b = buf[pos];
count--;
pos++;
return b&0xff;//0xff是255的16进制表示
}
return -1;//这个return语句为了保证有返回值,无其他意义
}
public void myClose()throws IOException
{
in.close();
}
}
结论:
自定义字节流缓冲区的读一个字节的read方法为什么返回值类型不是byte,而是int。
因为有可能会读到连续8个二进制1的情况,8个二进制1对应的十进制是-1.
那么就会数据还没有读完,就结束的情况。因为我们判断读取结束是通过结尾标记-1来确定的。
所以,为了避免这种情况将读到的字节进行int类型的提升。
并在保留原字节数据的情况前面了补了24个0,变成了int类型的数值。
而在写入数据时,只写该int类型数据的最低8位。 写入的write方法自动做了强转操作,否则文件会变大。
读取转换流
引子:读取键盘录入
System.out:对应的是标准输出设备,控制台。
System.in:对应的标准输入设备:键盘。
并且查询到该in是一个InputStream,out是一个OutputStream的子类
/*
需求:
通过键盘录入数据。
当录入一行数据后,就将该行数据进行打印。
如果录入的数据是over,那么停止录入。
*/
import java.io.*;
class ReadIn
{
public static void main(String[] args) throws IOException
{
InputStream in = System.in;
StringBuilder sb = new StringBuilder();
while(true)
{
int ch = in.read();//调用read方法一次就会将键盘录入的字符读取一次!
if(ch=='\r')
continue;
if(ch=='\n')
{
String s = sb.toString();
if("over".equals(s))
break;
System.out.println(s.toUpperCase());
sb.delete(0,sb.length());//StringBuilder的清空
}
else
sb.append((char)ch);
}
}
}
InputStreamReader和OutputStreamWriter
通过刚才的键盘录入一行数据并打印其大写,发现其实就是读一行数据的原理。
也就是readLine方法。
能不能直接使用readLine方法来完成键盘录入的一行数据的读取呢?
readLine方法是字符流BufferedReader类中的方法。
而键盘录入的read方法是字节流InputStream的方法。
那么能不能将字节流转成字符流在使用字符流缓冲区的readLine方法呢?
此时需要用到 转换流 InputStreamReader是 字节流通向字符流的桥梁 OutputStreamWriter是 字符流通向字节流的桥梁
import java.io.*;
class TransStreamDemo
{
public static void main(String[] args) throws IOException
{
//获取键盘录入对象。
//InputStream in = System.in;
//将字节流对象转成字符流对象,使用转换流。InputStreamReader
//InputStreamReader isr = new InputStreamReader(in);
//为了提高效率,将字符串进行缓冲区技术高效操作。使用BufferedReader
//BufferedReader bufr = new BufferedReader(isr);
//读取键盘录入的最常见写法。
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
// OutputStream out = System.out;
// OutputStreamWriter osw = new OutputStreamWriter(out);
// BufferedWriter bufw = new BufferedWriter(osw);
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));
String line = null;
while((line=bufr.readLine())!=null)
{
if("over".equals(line))
break;
bufw.write(line.toUpperCase());
bufw.newLine();
bufw.flush();//字符输出流用到了缓冲区,需要刷新
}
bufr.close();
bufw.close();
}
}
转换流可以指定编码表
流操作的规律
三种情形
1,
源:键盘录入。
目的:控制台。
2,需求:想把键盘录入的数据存储到一个文件中。
源:键盘。
目的:文件。
3,需求:想要将一个文件的数据打印在控制台上。
源:文件。
目的:控制台。
流操作的基本规律(三个明确)
最痛苦的就是流对象有很多,不知道该用哪一个。
通过三个明确来完成。
1,明确源和目的。
源:输入流。InputStream Reader
目的:输出流。OutputStream Writer。
2,操作的数据是否是纯文本。
是:字符流。
不是:字节流。
3,当体系明确后,在明确要使用哪个具体的对象。
通过设备来进行区分:
源设备:内存,硬盘。键盘
目的设备:内存,硬盘,控制台。 例:
需求:将一个文本文件中数据存储到另一个文件中。复制文件。
1,源:因为是源,所以使用读取流。InputStream Reader
2,是不是操作文本文件。
是!这时就可以选择Reader
这样体系就明确了。
3,接下来明确要使用该体系中的哪个对象。
明确设备:硬盘。一个文件。
Reader体系中可以操作文件的对象是 FileReader
是否需要提高效率:是!加入Reader体系中缓冲区 BufferedReader. FileReader fr = new FileReader("a.txt"); BufferedReader bufr = new BufferedReader(fr);
目的:OutputStream Writer
是否是纯文本。
是!Writer。
设备:硬盘,一个文件。
Writer体系中可以操作文件的对象FileWriter。
是否需要提高效率:是!。加入Writer体系中缓冲区 BufferedWriter FileWriter fw = new FileWriter("b.txt");
BufferedWriter bufw = new BufferedWriter(fw);
---------------------------------------
需求:将键盘录入的数据保存到一个文件中。
这个需求中有源和目的都存在。
那么分别分析
源:InputStream Reader
是不是纯文本?是!Reader
设备:键盘。对应的对象是System.in.
不是选择Reader吗?System.in对应的不是字节流吗?
为了操作键盘的文本数据方便。转成字符流按照字符串操作是最方便的。
所以既然明确了Reader,那么就将System.in转换成Reader。
用了Reader体系中转换流,InputStreamReader
InputStreamReader isr = new InputStreamReader(System.in);
需要提高效率吗?需要!BufferedReader
BufferedReader bufr = new BufferedReader(isr);
目的:OutputStream Writer
是否是存文本?是!Writer。
设备:硬盘。一个文件。使用 FileWriter。
FileWriter fw = new FileWriter("c.txt");
需要提高效率吗?需要。
BufferedWriter bufw = new BufferedWriter(fw);
**************
扩展一下,想要把录入的数据按照指定的编码表(utf-8),将数据存到文件中。
目的:OutputStream Writer
是否是存文本?是!Writer。
设备:硬盘。一个文件。使用 FileWriter。
但是FileWriter是使用的默认编码表。GBK. 同样FileReader把码表写死了,当想要用指定的码表读数据时,就要用到FileReader的父类转换流InputStreamReader
但是存储时,需要加入指定编码表utf-8。而指定的编码表只有转换流可以指定。
所以要使用的对象是OutputStreamWriter。
而该转换流对象要接收一个字节输出流。而且还可以操作的文件的字节输出流。FileOutputStream
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("d.txt"),"UTF-8");
需要高效吗?需要。
BufferedWriter bufw = new BufferedWriter(osw);
所以,记住。转换流什么使用。字符和字节之间的桥梁,通常,涉及到字符编码转换时,
需要用到转换流。
改变标准输入输出设备
通过System类中的两个方法setIn()和setOut()来改变 static void setIn(InputStream in)重新分配“标准”输入流。 static void setOut(PrintStream out) 重新分配“标准”输出流。
IO流的应用
异常的日志信息
import java.io.*;
import java.util.*;
import java.text.*;
class ExceptionInfo
{
public static void main(String[] args)throws IOException
{
try
{
int[] arr = new int[2];
System.out.println(arr[3]);
}
catch (Exception e)
{
try
{
Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String s = sdf.format(d);
PrintStream ps = new PrintStream("exeception.log");
ps.println(s);
System.setOut(ps);
}
catch (IOException ex)
{
throw new RuntimeException("日志文件创建失败");
}
e.printStackTrace(System.out);
}
}
}
//log4j 网上提供的专门针对建立异常信息的java包
系统信息
import java.util.*;
import java.io.*;
class SystemInfo
{
public static void main(String[] args) throws IOException
{
Properties prop = System.getProperties();
//System.out.println(prop);
prop.list(new PrintStream("sysinfo.txt"));
}
}
File类
用于将文件或文件夹封装成对象,方便对文件或者文件夹的属性信息进行操作 流只能操作数据 File对象可以作为参数传递给流的构造函数
File对象的创建
//将a.txt封装成file对象。可以将已有的和未出现的文件或者文件夹封装成对象。
File f1 = new File("a.txt"); //此例传入相对路径
//将父目录和文件名分别传入
File f2 = new File("c:\\abc","b.txt");
//将父目录封装成对象传入
File d = new File("c:\\abc");
File f3 = new File(d,"c.txt");
sop("f1:"+f1);//打印出相对路径
sop("f2:"+f2);//打印出绝对路径
sop("f3:"+f3);
//通过File类的seperator字段获取系统的目录分隔符,实现跨平台操作
File f4 = new File("c:"+File.separator+"abc"+File.separator+"zzz"+File.separator+"a.txt");
File对象功能
创建。
boolean createNewFile():在指定位置创建文件,如果该 文件已经存在,则不创建,返回false。会抛出IO异常
和输出流不一样, 输出流对象一建立创建文件。而且文件已经存在,会覆盖。所以该方式更合理 static File createTempFile(String prefix, String suffix); 在默认临时文件目录中创建一个空文件,使用给定前缀和后缀生成其名称 static File createTempFile(String prefix, String suffix, File directory); 在指定目录中创建一个新文件,一般将其创建在system32目录下
boolean mkdir():创建文件夹/目录。不能包括不存在的父目录
//创建单个文件夹
File dir = new File("abc\\kkk\\a");//目录abc\\kkk已经存在
sop("mkdir:"+dir.mkdir());
boolean mkdirs():创建多级文件夹。在File对象封装时进行设定:new File("abc\\kk\\dd");
//创建多级文件夹
File dir = new File("abc\\kkk\\a\\a\\dd\\ee\\qq\\aaa");
sop("mkdir:"+dir.mkdirs());
删除。
boolean delete():删除失败返回false。如果文件正在被使用,则删除不了返回false。
void deleteOnExit();在程序退出时删除指定文件。即使发生异常,也会删除
判断。
boolean canExecute(): 文件是否可执行 boolean canRead(); boolean canWrite(); int compareTo(File pathname);按字母顺序比较两个抽象路径名。按大小比较需要自定义比较器
boolean exists() :文件是否存在. 在用流操作文件时可以先进行判断,这就是把文件封装成对象的好处 在判断文件对象是否是文件或者目录时,必须要先通过exists方法判断该文件对象封装的内容是否存在,存在即有确定类型
isFile():
isDirectory();
isHidden();
isAbsolute();不论文件存不存在,都可以判断该路径是否为绝对路径
获取信息。
getName():
getPath():
getParent(): 获取父目录。只要没有明确指定上一层目录,返回的是null,录入abc\\file.txt和c:\\abc\\file.txt都可以
String getAbsolutePath(): 不论文件是否存在,返回此抽象路径名的绝对路径名字符串 File getAbsoluteFile(); 返回此抽象路径名的绝对路径名形式,把返回的绝对路径封装成了一个字符串,可以和上一个方法互相转换
long lastModified() ;返回此抽象路径名表示的文件最后一次被修改的时间
long length() ;返回文件的大小
boolean renameTo(File dest)重新命名此抽象路径名表示的文件,可以在不同盘附间移动
文件列表
static File [ ] listRoots():返回可用的文件系统的根(即机器里有效的盘符) String [ ] list():返回指定当前目录下的文件或者文件夹的名称,包含隐藏文件,当该方法所属对象为一个文件时,返回的是null。调用list方法的对象必须是封装了一个目录,该目录还必须存在 String [ ] list(FilenameFilter, filter): 返回满足指定过滤器的文件和目录 java.io.FilenameFilter 为一个接口,该接口内部有一个方法boolean accept(File dir, String name):测试指定文件是否应该包含在指定列表中。dir:被找到的文件所在的目录,name:文件的名称
File dir = new File("d:\\java1223\\day18");
String[] arr = dir.list(new FilenameFilter()
{
public boolean accept(File dir,String name)
{
return name.endsWith(".bmp");
}
});
System.out.println("len:"+arr.length);
for(String name : arr)
{
System.out.println(name);
}
File [ ] listFiles(); File [ ] listFiles(FileFilter, filter) File [ ] listFiles(FilenameFilter, filter) 这个方法返回的都是File对象数组
列出目录下所有内容
列出指定目录下的文件或者文件夹,包括子目录的内容
递归
递归会不断开辟递归函数的空间,如下图:
递归要注意的:
- 限定条件
- 注意递归的次数,避免内存溢出
层级目录
列出指定目录下文件或者文件夹,包含子目录中的内容。也就是列出指定目录下所有内容。
//该方法返回该级文件或文件夹前面所插入的字符串
public static String getLevel(int level){
StringBuilder sb = new StringBuilder();
sb.append("|--");
for(int i=0;i<level;i++){
sb.insert(0, " ");//从StringBuilder前面插入
}
return sb.toString();
}
private static void listAll(File file, int level) {
System.out.println(getLevel(level)+file.getName());
level++;
File[] files = file.listFiles();
for(File f: files){
if(f.isDirectory()){
listAll(f,level);
}
else
System.out.println(getLevel(level)+f.getName());
}
}
删除带内容的目录
删除原理:在windows中,删除目录是从里面往外删除的,需要用到递归 java的删除不走回收站,系统隐藏的文件有的删不了,所以在判断时确定是非隐藏文件
public static void removeDir(File dir)
{
File[] files = dir.listFiles();
for(int x=0; x<files.length; x++)
{
if(files[x].isDirectory())
removeDir(files[x]);
else
System.out.println(files[x].toString()+":-file-:"+files[x].delete());
}
System.out.println(dir+"::dir::"+dir.delete());
}
创建java文件列表
将一个指定目录下的java文件的绝对路径,存储到一个文本文件中(数据持久化)。
建立一个java文件列表文件。
思路:
1,对指定的目录进行递归。
2,获取递归过程所有的java文件的路径。
3,将这些路径存储到集合中。
4,将集合中的数据写入到一个文件中。
import java.io.*;
import java.util.*;
class JavaFileList
{
public static void main(String[] args) throws IOException
{
File dir = new File("d:\\java1223");
List<File> list = new ArrayList<File>();
fileToList(dir,list);
//System.out.println(list.size());
File file = new File(dir,"javalist.txt");
writeToFile(list,file.toString());
}
public static void fileToList(File dir,List<File> list)
{
File[] files = dir.listFiles();
for(File file : files)
{
if(file.isDirectory())
fileToList(file,list);
else
{
if(file.getName().endsWith(".java"))
list.add(file);
}
}
}
public static void writeToFile(List<File> list,String javaListFile)throws IOException
{
BufferedWriter bufw = null;
try
{
bufw = new BufferedWriter(new FileWriter(javaListFile));
for(File f : list)
{
String path = f.getAbsolutePath();
bufw.write(path);
bufw.newLine();
bufw.flush();
}
}
catch (IOException e)
{
throw e;
}
finally
{
try
{
if(bufw!=null)
bufw.close();
}
catch (IOException e)
{
throw e;
}
}
}
}
Properties
Properties是Hashtable的子类,也就是说它具备Map集合的特点,而且它里面存储的键值对都是字符串 是集合中和IO技术相结合的集合容器 该对象的特点可用于键值对形式的配置文件 在加载数据时,需要数据有固定格式:键=值。
设置和获取元素
Properties prop = new Properties();//创建对象
prop.setProperty("zhangsan","30");
String value = prop.getProperty("lisi");//根据键名获取值
prop.setProperty("zhangsan",89+"");//修改
Set<String> names = prop.stringPropertyNames();//返回简明的set集合,该方法1.6才有效
for(String s : names)
{
System.out.println(s+":"+prop.getProperty(s));
}
存取配置文件
将流中的数据存储到集合中
//想要将info.txt中键值数据存到集合中进行操作。
/*
1,用一个流和info.txt文件关联。
2,读取一行数据,将该行数据用"="进行切割。
3,等号左边作为键,右边作为值。存入到Properties集合中即可。
*/
public static void method_1()throws IOException
{
BufferedReader bufr = new BufferedReader(new FileReader("info.txt"));
String line = null; //用line记录读取到的一行字符串数据
Properties prop = new Properties();
while((line=bufr.readLine())!=null)
{
String[] arr = line.split("=");//用等号分割,返回一个字符串数组
prop.setProperty(arr[0],arr[1]);
}
bufr.close();//注意关闭流
System.out.println(prop);
}
load方法
从输入流中读取属性列表(键和元素对) void load(InputStream inStream) 从输入字符流中读取属性列表(键和元素对)jdk1.6
void load(Reader reader) 以上代码为该方法原理
store方法
将Properties表中的属性列表(键和元素对)写入输出流 void store(OutputStream out, String comments)//comments为注释,用“#”标记 将Properties表中的属性列表(键和元素对)写入输出字符流 jdk1.6
void store(Writer writer, String comments)
list方法
void list(PrintStream out) void list(PrintWriter out)
将属性列表输出到指定输出流
Properties prop = new Properties();
FileInputStream fis = new FileInputStream("info.txt");
//将流中的数据加载进集合。
prop.load(fis);
prop.setProperty("wangwu","39");
FileOutputStream fos = new FileOutputStream("info.txt");
//将集合中的数据写入输出流
prop.store(fos,"haha");
prop.list(System.out);
fos.close();
fis.close();
练习:限制程序运行次数。当运行次数到达5次时,给出,请您注册的提示。并不再让该程序执行。
/*
用于记录应用程序运行次数。
如果使用次数已到,那么给出注册提示。
很容易想到的是:计数器。
可是该计数器定义在程序中,随着程序的运行而在内存中存在,并进行自增。
可是随着该应用程序的退出,该计数器也在内存中消失了。
下一次在启动该程序,又重新开始从0计数。
这样不是我们想要的。
程序即使结束,该计数器的值也存在。
下次程序启动会先加载该计数器的值并加1后在重新存储起来。
所以要建立一个配置文件。用于记录该软件的使用次数。
该配置文件使用键值对的形式。
这样便于阅读数据,并操作数据。
键值对数据是map集合。
数据是以文件形式存储,使用io技术。
那么map+io -->properties.
配置文件可以实现应用程序数据的共享。
*/
import java.io.*;
import java.util.*;
class RunCount
{
public static void main(String[] args) throws IOException
{
Properties prop = new Properties();
//用File来对象化文件,那么文件不存在则创建
File file = new File("count.ini"); //此处采用相对路径,因为绝对路径会因为盘符不存在而出现IO异常
if(!file.exists())
file.createNewFile();
FileInputStream fis = new FileInputStream(file);
prop.load(fis);
int count = 0;
String value = prop.getProperty("time");
if(value!=null)
{
count = Integer.parseInt(value);
if(count>=5)
{
System.out.println("您好,使用次数已到,拿钱!");
return ; //用该语句直接终止程序的运行
}
}
count++;//当value不为空但又小于5时,外层if代码块执行完之后就会执行此语句保证count的续增
prop.setProperty("time",count+"");
FileOutputStream fos = new FileOutputStream(file);//输出流对象不能在前面创建,否则会出现次数无法递增的问题!
prop.store(fos,"");
fos.close();
fis.close();
}
}
配置文件有.properties和.xml,properties文件存储键值对结构简单,xml格式可以存储复杂结构的数据 properties的键值对格式: name=zhangsan
age=20
xml格式:
<persons>
<person id="001">
<name>zhagnsan</name>
<age>30</age>
<address>bj</address>
</person>
<person>
<name
</person>
</persons>
可以用org.w3c.dom中的Document接口来代表整个HTML或XML文档,但是此方法很麻烦, 可以采用dom4j
打印流
该流提供了打印方法,可以将各种数据类型的数据都原样打印。
字节打印流 PrintStream
构造函数可以接收的参数类型:
1,file对象。File
2,字符串路径。String
3,字节输出流。OutputStream
字符打印流 PrintWriter
更常用 构造函数可以接收的参数类型:
1,file对象。File,还可以传入String csn创建具有指定文件和字符集的新PrinterWriter
2,字符串路径。String
3, 字节输出流。OutputStream,还可以传入boolean autoFlush可以创建带自动行刷新的的新PrintWriter
4,字符输出流,Writer。可带自动刷新
//读取键盘录入的标准语句
BufferedReader bufr =
new BufferedReader(new InputStreamReader(System.in));
//PrintWriter out = new PrintWriter(System.out,true);//打印到控制台
//PrintWriter out = new PrintWriter("a.txt");//直接存入文件,后面不能写刷新,因为刷新只能针对流而言
PrintWriter out = new PrintWriter(new FileWriter("a.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<? extends InputStream> e) 通过记住参数来初始化新创建的 SequenceInputStream,该参数必须是生成运行时类型为 InputStream 对象的 Enumeration 型参数(可通过Vector获得)。
两个流合并
SequenceInputStream(InputStream s1, InputStream s2) 通过记住这两个参数来初始化新创建的 SequenceInputStream(将按顺序读取这两个参数,先读取 s1,然后读取 s2),以提供从此 SequenceInputStream 读取的字节。
实例:把三个文件的数据变成一个文件
Vector<FileInputStream> v = new Vector<FileInputStream>();
v.add(new FileInputStream("c:\\1.txt"));
v.add(new FileInputStream("c:\\2.txt"));
v.add(new FileInputStream("c:\\3.txt"));
Enumeration<FileInputStream> en = v.elements();//vector的elements方法返回此向量的组件的枚举
SequenceInputStream sis = new SequenceInputStream(en);
FileOutputStream fos = new FileOutputStream("c:\\4.txt");
byte[] buf = new byte[1024];
int len =0;
while((len=sis.read(buf))!=-1) //read方法返回读取的字符的长度
{
fos.write(buf,0,len);
}
fos.close();
sis.close();
切割合并文件
public static void merge()throws IOException
{
//Vector执行效率低,此处用ArrayList
ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();
for(int x=1; x<=3; x++)
{
al.add(new FileInputStream("c:\\splitfiles\\"+x+".part"));
}
//因为是匿名内部类,所以要对访问的局部变量进行final修饰,规定的
final Iterator<FileInputStream> it = al.iterator();
//使用ArrayList的迭代器覆盖Enumneration匿名内部类的两个方法创建一个Enumneration对象
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("c:\\splitfiles\\0.bmp");
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("c:\\1.bmp");
FileOutputStream fos = null;
byte[] buf = new byte[1024*1024];//当操作较大数据时,为了防止内存溢出,可以用每一个流对象操作100M数据
int len = 0;
int count = 1;
while((len=fis.read(buf))!=-1)
{
fos = new FileOutputStream("c:\\splitfiles\\"+(count++)+".part");
fos.write(buf,0,len);
fos.close();
}
fis.close();
}
对象的序列化
把堆内存中的对象以字节形式存储在硬盘上,也叫对象的序列化或持久化 两个对象要成对使用完成读写
ObjectOutputStream
写入对象 javac *.java可以一次编译多个java文件
writeInt()方法可以把int类型的4个8位全部写出,而write方法只能写最低8位(即一个字节)
public static void writeObj()throws IOException
{
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("obj.txt"));
oos.writeObject(new Person("lisi0",399,"kr"));
oos.close();
}
ObjectInputStream
读取对象
public static void readObj()throws Exception//这里会抛出ClassNotFoundException和类没有序列化异常,所以抛出Exception
{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"));
Person p = (Person)ois.readObject();
System.out.println(p);
ois.close();
}
所序列化的类,此例为Person必须实现Serializable接口,实现该接口不需要覆盖方法,没有方法的接口称之为“ 标记接口” 序列化运行时使用一个称为 serialVersionUID的版本号与每个可序列化类相关联,用于验证序列化对象的发送者和接收者是否为同一对象。如果接收者加载的该对象的类的 serialVersionUID与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。可序列化类可以通过声明名为"serialVersionUID"的字段(该字段必须是静态 (static)、最终 (final)的 long 型字段)显式声明其自己的serialVersionUID,如: public static final long serialVersionUID = 42L;
import java.io.*;
class Person implements Serializable
{
public static final long serialVersionUID = 42L; //不让java自动生成uid,通过自定义uid
private String name;
transient int age; //对非静态的成员也不使其序列化,则加上transient关键字,即使它在堆内存中
static String country = "cn"; //静态成员是不能被序列化的,因为静态成员在方法区里面,所以不论构造Person时传入何值,读取的时候都是cn
Person(String name,int age,String country)
{
this.name = name;
this.age = age;
this.country = country;
}
public String toString()
{
return name+":"+age+":"+country;
}
}
transient关键字可以保证非静态成员变量在堆内存中存在而不在文本文件中存在 一般存储对象的文件将其命名为“XXX.object”
可以一次写多个对象,读得时候按顺序自动读出
管道流
PipedInputStream和PipedOutputStream 输入输出可以直接进行连接 通过结合多线程使用。数据由某个线程写入PipedOutputStream对象,并由其他线程从PipedInputStream对象读取
import java.io.*;
//读取线程
class Read implements Runnable
{
private PipedInputStream in;
Read(PipedInputStream in)
{
this.in = in;
}
public void run()
{
try//此处不能抛异常,必须处理异常,因为覆盖了run方法
{
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 (IOException e)
{
throw new RuntimeException("管道读取流失败");
}
}
}
//输出线程
class Write implements Runnable
{
private PipedOutputStream out;
Write(PipedOutputStream out)
{
this.out = out;
}
public void run()
{
try
{
System.out.println("开始写入数据,等待6秒后。");
Thread.sleep(6000);
out.write("piped lai la".getBytes());//write方法写入的一定是字节型数据
out.close();
}
catch (Exception e)
{
throw new RuntimeException("管道输出流失败");
}
}
}
class PipedStreamDemo
{
public static void main(String[] args) throws IOException
{
PipedInputStream in = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream();
in.connect(out);//可以使用connect方法来连接两个流对象,也可以使用构造函数传递参数连接两个流对象
Read r = new Read(in);
Write w = new Write(out);
new Thread(r).start();
new Thread(w).start();
}
}
RandomAccessFile
随机访问文件,自身具备读写的方法,结尾处没有父类名
通过skipBytes(int x),seek(int x)来达到随机访问,前提是保证数据的存储是有规律的
该类不算是IO体系中子类。
而是直接继承自Object。
但是它是IO包中成员。因为它具备读和写功能。
内部封装了一个数组(以文件的数据的一个字节为单位),而且通过指针对数组的元素进行操作。
可以通过getFilePointer获取指针位置,
同时可以通过seek改变指针的位置。
其实完成读写的原理就是内部封装了字节输入流和输出流。
通过构造函数可以看出,该类只能操作文件。不能操作键盘录入输出等其他形式
而且操作文件还有模式:只读r,读写rw等。
如果模式为只读 r。不会创建文件。会去读取一个已存在文件,如果该文件不存在,则会出现异常。
如果模式rw。操作的文件不存在,会自动创建。如果存在则不会覆盖。
public static void readFile()throws IOException
{
RandomAccessFile raf = new RandomAccessFile("ran.txt","r");
//调整对象中指针。至第8个字节。数组的前后都能指定
//raf.seek(8*1);
//跳过指定的字节数
raf.skipBytes(8);//只能在数组中往下跳
byte[] buf = new byte[4];
raf.read(buf);
String name = new String(buf);
int age = raf.readInt();//readInt方法一次读4个字节
System.out.println("name="+name);
System.out.println("age="+age);
raf.close();
}
public static void writeFile_2()throws IOException
{
RandomAccessFile raf = new RandomAccessFile("ran.txt","rw");
raf.seek(8*0); //将指针定位在数组开头,会根据写入数据的大小从开头覆盖数据
raf.write("周期".getBytes());
raf.writeInt(103);
raf.close();
}
public static void writeFile()throws IOException//该方法写入会覆盖原有文件数据
{
RandomAccessFile raf = new RandomAccessFile("ran.txt","rw");//rw代表“读写”模式
/*raf.write("2".getBytes());
raf.write(258);//此时会取258的最低8位写出
raf.writeInt(258);//此时会取258的4个字节写出 */
raf.write("李四".getBytes());
raf.writeInt(97);
raf.write("王五".getBytes());
raf.writeInt(99);
raf.close();
}
可以实现读取,写入,修改操作
该类可以实现多线程的下载文件。每个线程负责文件的一段数据的下载,保证下载后数据的完整性
为了保证数据的规律性,可以进行定义,比如,姓名16个字节,年龄4个字节,空的字节会拿空补位
操作基本数据类型的流对象
DataInputStream和DataOutputStream
可以用于操作基本数据类型的数据的流对象
import java.io.*;
class DataStreamDemo
{
public static void main(String[] args) throws IOException
{
//writeData();
//readData();
//writeUTFDemo();
// OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("gbk.txt"),"gbk"); //使用转换流指定编码表gbk
//
// osw.write("你好");
// osw.close();
// readUTFDemo();
}
public static void readUTFDemo()throws IOException
{
DataInputStream dis = new DataInputStream(new FileInputStream("utf.txt"));
String s = dis.readUTF();//读取时应使用相应的方法,否则读取错误
System.out.println(s);
dis.close();
}
public static void writeUTFDemo()throws IOException
{
DataOutputStream dos = new DataOutputStream(new FileOutputStream("utfdate.txt"));
dos.writeUTF("你好");//该方法使用修改过的UTF表写入数据
dos.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);
System.out.println("b="+b);
System.out.println("d="+d);
dis.close();
}
public static void writeData()throws IOException//可以写入基本数据类型
{
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));//注意传入输出流对象
dos.writeInt(234);
dos.writeBoolean(true);
dos.writeDouble(9887.543);
dos.close();
ObjectOutputStream oos = null;
oos.writeObject(new O());
}
内存为源和目的的操作
操作字节数组的流对象
ByteArrayInputStream和ByteArrayOutputStream
用于操作字节数组的流对象。ByteArrayInputStream :在构造的时候,需要接收数据源,。而且数据源是一个字节数组。ByteArrayOutputStream: 在构造的时候,不用定义数据目的,因为该对象中已经内部封装了可变长度的字节数组。这就是数据目的地。因为这两个流对象都操作的数组,并没有使用系统资源。不用抛IO异常所以,不用进行close关闭。
在流操作规律讲解时:
源设备:键盘 System.in,硬盘 FileStream,内存 ArrayStream。
目的设备:控制台 System.out,硬盘FileStream,内存 ArrayStream。
其实就是用流的读写思想来操作数组。
public static void main(String[] args)
{
//数据源。
ByteArrayInputStream bis = new ByteArrayInputStream("ABCDEFD".getBytes());
//数据目的
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());//使用平台默认的字符集,通过解码字节将缓冲区的内容转换为字符串
// bos.writeTo(new FileOutputStream("a.txt"));//一次性把数组中的数据写到一个输出流中,但此时需要抛IO异常或者捕获该异常
}
操作字符数组
CharArrayReader和CharArrayWriter
类似字节数组
操作字符串
StringReader和StringWriter
类似字节数组
字符编码
概述
字符流的出现是为了操作字符,更重要的是加入了字节与字符之间的编码转换,通过子类的转换流来完成:InputStreamReader和OutputStreamWriter,在这两个对象进行构造的时候可以指定字符集
可以加入编码表的对象还有:PrintStream和PrintWriter,但是只能输出打印,不能读取
编码表的由来:计算机只能识别二进制数据,早期只能识别电信号,为了方便应用计算机,让它可以识别各国的文字,就将各国的文字用数字来表示,并一一对应,形成一张表,这就是编码表
常见的编码表:
- ASCII:美国标准信息交换码,用一个字节的7位表示
- IOS8859-1:拉丁码表。欧洲码表。用一个字节的8位表示
- GB2312:中国的中文编码表(早期)
- GBK:中国的中文编码表升级,融合了更多的中文文字字符。打头的是两个高位为1的两个字节编码。为负数
- Unicode:国际标准码,融合了多种文字。所有国家的文字都用两个字节来表示,但是对于一个字节就能表示的文字,浪费。Java语言使用的就是unicode。
- UTF-8:Unicode Transform Format,最多用三个字节表示一个字符,根据字符所占内存空间不同,分别用一个、两个、三个字节来编码。并在每个字节的开头都加了标识头,用以区分什么是UTF-8的编码
代码示例:
private static void readText() throws IOException {
InputStreamReader isr =
new InputStreamReader(new FileInputStream("utf.txt"),"GBK");//字节流到字符流
char[] buf = new char[10];
int len = isr.read(buf);
String str = new String(buf,0,len);
System.out.println(str);
isr.close();
}
private static void writeText() throws IOException {
/*OutputStreamWriter osw =
new OutputStreamWriter(new FileOutputStream("gbk.txt"));//因为系统默认的是GBK码表,这里将字符流转换为字节 流,所以此句等价于FileWriter
*/
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("utf.txt"),"UTF-8");//按照UTF-8码表写出数据
osw.write("你好");//osw是字符流对象,所以不用将数据转换为字节数组
osw.close();
}
查错表的情况分析:
可以利用“你好”来分析编码表的错误
编码与解码
编码:字符串变成字节数组 String --> byte[] 默认字符集(GBK):str.getBytes() 指定字符集:str.getBytes(charsetName) 解码:字节数组变成字符串 byte[] --> String 默认字符集(GBK):new String(byte[]) 指定字符集:new String(byte[], charsetName)
String str = "你好";
// byte[] arr = str.getBytes();//结果:[-60, -29, -70, -61]
// byte[] arr = str.getBytes("GBK");//结果:[-60, -29, -70, -61]会抛出异常,因为可能传入的值不识别
byte[] arr = str.getBytes("UTF-8");//结果:[-28, -67, -96, -27, -91, -67]
System.out.println(Arrays.toString(arr));//将字节数组直接转换成字符串形式
常用的码表就两张:GBK和UTF-8,他们都识别中文
字符集转换注意事项
GBK与IOS8859-1
如果编码时传入了不能识别原文字的码表(比如对中文编码传入欧洲码表IOS8859-1),编码错误就无法正确解码了 如果编码成功但是解码后出现乱码,则需要对乱码再次进行编码(采用解码的码表),然后用编码的码表解码来还原原字符 原理图:
应用:当服务器端出现乱码就采用编一次解一次来解决,因为服务器的解码码表是ISO8859-1
GBK与UTF-8
因为UTF-8和GBK都识别中文,当用GBK编码再用UTF-8解码,会显示未知字符(???),此时再用UTF-8对其编码时会在其未知字符区域查找字节码值,因此无法再次还原正确的字节码值,所以此时不能采用再编码再解码的方法解决乱码
String s = "你好";
byte[] b = s.getBytes("GBK");
System.out.println(Arrays.toString(b));
String s1 = new String(b,"UTF-8");
System.out.println(s1);
//对s1进行再次编码
byte[] b1 = s1.getBytes("UTF-8");
System.out.println(Arrays.toString(b1));
String s2 = new String(b1,"GBK");
System.out.println(s2);
运行结果:
“联通”的问题
对于“联通”两个字,其GBK编码对应的四个字节正好符合UTF-8的编码规则,所以应用程序(比如记事本)在打开时会自动按照UTF-8对其进行解码,因此会出现乱码。 解决办法是避免只存入符合UTF-8编码规则的字符,可以加入别的字符来让应用程序不按UTF-8进行解析,就可以成功显示“联通”
综合练习
/*
有五个学生,每个学生有3门课的成绩,
从键盘输入以上数据(包括姓名,三门课成绩),
输入的格式:如:zhagnsan,30,40,60计算出总成绩,
并把学生的信息和计算出的总分数高低顺序存放在磁盘文件"stud.txt"中。
1,描述学生对象。
2,定义一个可操作学生对象的工具类。
思想:
1,通过获取键盘录入一行数据,并将该行中的信息取出封装成学生对象。
2,因为学生有很多,那么就需要存储,使用到集合。因为要对学生的总分排序。
所以可以使用TreeSet。
3,将集合的信息写入到一个文件中。
*/
import java.io.*;
import java.util.*;
//定义Student类实现Comparable接口并指定泛型,这样在实现compareTo方法时就只能接收该泛型了
class Student implements Comparable<Student>{
private String name;
private int ma,cn,en,sum;
Student(String name, int ma, int cn, int en){
this.name = name;
this.ma = ma;
this.cn = cn;
this.en = en;
sum = ma+cn+en;
}
public String getName(){
return this.name;
}
public int getSum(){
return this.sum;
}
public int compareTo(Student s){
int num = new Integer(this.sum).compareTo(new Integer(s.sum));
if(num==0)
return this.name.compareTo(s.name);
return num;
}
//一般自定义对象都要覆盖Object类的hashCode和equals方法,还有toString方法
public int hashCode(){
return name.hashCode()+sum*35;
}
public boolean equals(Object obj){
if(!(obj instanceof Student))
throw new ClassCastException("类型不匹配");
Student s = (Student)obj;
return this.name.equals(s.name)&&this.sum==s.sum;
}
public String toString(){
return "Student:"+name+"["+ma+","+cn+","+en+"]";
}
}
class StudentInfoTool{
//按照默认排序方式,总成绩从小到大
public static Set<Student> getStudents() throws IOException{
return getStudents(null);//不需要比较器,传入null。
}
//按照指定比较器排序方式,总成绩从大到小
public static Set<Student> getStudents(Comparator<Student> cmp) throws IOException{
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
String line = null;
Set<Student> stus = null;
if(cmp==null)
stus = new TreeSet<Student>();//如果没有传入比较器,那么就创建一个不带比较器的集合
else
stus = new TreeSet<Student>(cmp);//如果传入了比较器,就创建一个带比较器的集合
while(true){
line = bufr.readLine();
if("over".equals(line))
break;
String[] arr = line.split(",");
Student s = new Student(arr[0],Integer.parseInt(arr[1]),
Integer.parseInt(arr[2]),Integer.parseInt(arr[3]));
stus.add(s);
}
bufr.close();
return stus;
}
//将存有学生信息的集合写入文件
public static void write2File(Set<Student> stus) throws IOException{
BufferedWriter bufw = new BufferedWriter(new FileWriter("stuInfo.txt"));
for(Student s: stus){
bufw.write(s.toString()+'\t');
bufw.write(s.getSum()+"");//如果单独输出整数只输出最后一个八位,会出现乱码,所以要转为字符串
bufw.newLine();
bufw.flush();//写入文件需要刷新,因为用到了缓存
}
bufw.close();
}
}
public class StudentInfoDemo {
public static void main(String[] args) throws IOException {
Comparator<Student> cmp = Collections.reverseOrder();//返回一个逆序自身比较性的比较器
Set<Student> stus = StudentInfoTool.getStudents(cmp);
StudentInfoTool.write2File(stus);
}
}