五、Java SE核心II
5.1 Java异常处理机制
异常结构中的父类Throwable类,其下子类Exceptionlei类和Error类。我们在程序中可以捕获的是Exception的子类异常。
Error系统级别的错误:Java运行时环境出现的错误,我们不可控。
Exception是程序级别的错误:我们可控。
1)异常处理语句:try-catch,如果try块捕获到异常,则到catch块中处理,否则跳过忽略catch块
(开发中,一定有解决的办法才写,无法解决就向上抛throws)。
try{//关键字,只能有一个try语句
可能发生异常的代码片段
}catch(Exception e){//列举代码中可能出现的异常类型,可有多个catch语句
当出现了列举的异常类型后,在这里处理,并有针对性的处理
}
2)良好的编程习惯,在异常捕获机制的最后书写catch(Exception e)(父类,顶极异常)捕获未知的错误(或不需要针对处理的错误)。
3)catch的捕获是由上至下的,所以不要把父类异常写在子类异常的上面,否则子类异常永远没有机会处理!在catch块中可以使用方法获取异常信息:
①getMessage()方法:用来得到有关异常事件的信息。
②printStackTrace()方法:用来跟踪异常事件发生时执行堆栈的内容。
4)throw关键字:用于主动抛出一个异常
当我们的方法出现错误时(不一定是真实异常),这个错误我们不应该去解决,
而是通知调用方法去解决时,会将这个错误告知外界,而告知外界的方式就是throw异常(抛出异常)
catch语句中也可抛出异常。虽然不解决,但要捕获,然后抛出去。
使用环境:
我们常在方法中主动抛出异常,但不是什么情况下我们都应该抛出异常。
原则上,自身决定不了的应该抛出。那么方法中什么时候该自己处理异常什么时候抛出?
方法通常有参数,调用者在调用我们的方法帮助解决问题时,通常会传入参数,
若我们方法的逻辑是因为参数的错误而引发的异常,应该抛出,
若是我们自身的原因应该自己处理。
public static void main(String[] args) {
try{/**通常我们调用方法时需要传入参数的话,那么这些方法,JVM都不会自动处理异常,而是将错误抛给我们解决*/
String result=getGirlFirend("女神"); System.out.println("追到女神了么?"+result);
}catch(Exception e){
System.out.println("没追到");//我们应该在这里捕获异常并处理。
}
}
public static String getGirlFirend(String name){
try{
if("春哥".equals(name)){
return "行";
}else if("曾哥".equals(name)){
return "行";
}else if("我女朋友".equals(name)){
return "不行";
}else{/**当出现了错误(不一定是真实异常)可以主动向外界抛出一个异常!*/
throw new RuntimeException("人家不干!");
}
}catch(NullPointerException e){
throw e;//出了错不解决,抛给调用者解决
}
}
5)throws关键字:不希望直接在某个方法中处理异常,而是希望调用者统一处理该异常。
声明方法的时候,我们可以同时声明可能抛出的异常种类,通知调用者强制捕获。就是所谓的“丑话说前面”。
原则上throws声明的异常,一定要在该方法中抛出。否则没有意义。
相反的,若方法中我们主动通过throw抛出一个异常,应该在throws中声明该种类异常,通知外界捕获。
注意事项:
①注意throw和throws关键字的区别:抛出异常和声明抛出异常。
②不能在main方法上throws,因为调用者JVM直接关闭程序。
public static void main(String[] args) {
try{
Date today=stringToDate("2013-05-20");
} catch (ParseException e){
//catch中必须含有有效的捕获stringToDate方法throws的异常
// 输出这次错误的栈信息可以直观的查看方法调用过程和出错的根源
e.printStackTrace();
}
}
eg:将一个字符串转换为一个Date对象,抛出的异常是字符格式错误java.text.ParseException
public static Date stringToDate(String str) throws ParseException{
SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-DD");
Date date=format.parse(str); return date;
}
6)捕获异常两种方式:上例SimpleDataFormat的parse方法在声明的时候就是用了throws,强制我们调用parse方法时必须捕获ParseException,
我们的做法有两种: 一是添加try-catch捕获该异常,
二是在我们的方法中声明出也追加这种异常的抛出(继续往外抛)。
7)java中抛出异常过程:
java虚拟机在运行程序时,一但在某行代码运行时出现了错误,JVM会创建这个错误的实例,并抛出。
这时JVM会检查出错代码所在的方法是否有try捕获,若有,则检查catch块是否有可以处理该异常的能力
(看能否把异常实例作为参数传进去,看有没有匹配的异常类型)。
若没有,则将该异常抛给该方法的调用者(向上抛)。
以此类推,直到抛至main方法外仍没有解决(即抛给了JVM处理)。那么JVM会终止该程序。
8)java中的异常Exception分为:
①非检测异常(RuntimeException子类):编译时不检查异常。若方法中抛出该类异常或其子类,那么声明方法时可以不在throws中列举该类抛出的异常。
常见的运行时异常有:
NullPointerException、
IllegalArgumentException、
ClassCastException、
NumberFormatException、
ArrayIndexOutOfBoundsException、
ArithmeticException
②可检测异常(非RuntimeException子类):
编译时检查,除了运行时异常之外的异常,都是可检查异常,则必须在声明方法时用throws声明出可能抛出的异常种类!
9)finally块:
finally块定义在catch块的最后(所有catch最后),且只能出现一次(0-1次), 无论程序是否出错都会执行的块! 无条件执行!
通常在finally语句中进行资源的消除工作,如关闭打开的文件,删除临时文件等。
public static void main(String[] args) {
System.out.println( test(null)+","+test("0")+","+test("") );
}
/**输出结果?1,0,2 ? 4,4,4为正确结果 */
public static int test(String str){
try{
return str.charAt(0)-‘0‘;
}catch(NullPointerException e){
return 1;
}catch(RuntimeException e){
return 2;
}catch(Exception e){
return 3;
}finally{//无条件执行
return 4;
}
}
10)重写方法时的异常处理
如果使用继承时,在父类别的某个地方上宣告了throws某些异常,而在子类别中重新定义该方法时,可以:
①不处理异常(重新定义时不设定throws)。
②可仅throws父类别中被重新定义的方法上的某些异常(抛出一个或几个)。
③可throws被重新定义的方法上的异常之子类别(抛出异常的子类)。
但不可以:①throws出额外的异常。 ②throws被重新定义的方法上的异常之父类别(抛出了异常的父类)。
5.2 File文件类
java使用File类(java.io.File)表示操作系统上文件系统中的文件或目录。
换句话说,我们可以使用File操作硬盘上的文件或目录进行创建或删除。
File可以描述文件或目录的名字,大小等信息,但不能对文件的内容操作!File类的构造器都是有参的。
1)关于路径的描述:不同的文件系统差异较大,Linux和Windows就不同!最好使用相对路径,不要用绝对路径。
2)“.”代表的路径:当前目录(项目所处的目录),在eclipse_workspace/project_name下,
File.separator:常量,目录分隔符,推荐使用!根据系统自动识别用哪种分割符,windows中为/,Linux中为\。
3)创建该对象并不意味着硬盘上对应路径上就有该文件了,只是在内存中创建了该对象去代表路径指定的文件。
当然这个路径对应的文件可能根本不存在!
File file=new File("."+File.separator+"data.dat");// 效果为./data.dat
//File file=new File("e:/XX/XXX.txt");不建议使用
4)createNewFile()中有throws声明,要求强制捕获异常!
5)新建文件或目录:
①boolean mkdir():只能在已有的目录基础上创建目录。
②boolean mkdirs():会创建所有必要的父目录(不存在的自动创建)并创建该目录。
③boolean createNewFile():创建一个空的新文件。
6)创建目录中文件的两种方式:
①直接指定data.dat需要创建的位置,并调用createNewFile(),前提是目录都要存在!
②先创建一个File实例指定data.dat即将存放的目录,若该目录不存在,则创建所有不存在的目录,再创建一个File实例,代表data.dat文件,
创建是基于上一个代表目录的File实例的。使用File(File dir,String fileName)构造方法创建File实例,
然后再调用createNewFile():在dir所代表的目录中表示fileName指定的文件
File dir=new File("."+File.separator+"demo"+File.separator+"A");
if(!dir.exists()){
dir.mkdirs();//不存在则创建所有必须的父目录和当亲目录
}
File file=new File(dir,"data.dat");
if(!file.exists()){
file.createNewFile();
System.out.println("文件创建完毕!");
}
7)查看文件或目录属性常用方法
①long length():返回文件的长度。
②long lastModified():返回文件最后一次被修改的时间。
③String getName():返回文件或目录名。
④boolean exists():是否存在。
⑤boolean isDirectory():是否是目录。
⑥boolean canWrite():是否可以写入、修改。
⑦File[] listFiles():获取当亲目录的子项(文件或目录)
⑧String getPath():返回路径字符串。
⑨boolean isFile():是否是标准文件。
⑩boolean canRead():是否可以读取。
eg1:File类相关操作
File dir=new File(".");
if(dir.exists()&&dir.isDirectory()){//是否为一个目录
File[] files=dir.listFiles();//获取当前目录的子项(文件或目录)
for(File file:files){//循环子项
if(file.isFile()){//若这个子项是一个文件
System.out.println("文件:"+file.getName());
}else{
System.out.println("目录:"+file.getName());
}
}
}
eg2:递归遍历出所有子项
File dir=new File(".");
File[] files=dir.listFiles();
if(files!=null&&files.length>0){//判断子项数组有项
for(File file:files){//遍历该目录下的所有子项
if(file.isDirectory()){//若子项是目录
listDirectory(file);//不到万不得已,不要使用递归,非常消耗资源
}else{
System.out.println("文件:"+file);//有路径显示,输出File的toString()
//file.getName()无路径显示,只获取文件名
}
}
}
8)删除一个文件:boolean delete():
①直接写文件名作为路径和"./data.dat"代表相同文件,也可直接写目录名,但要注意第2条。
②删除目录时:要确保该目录下没有任何子项后才可以将该目录删除,否则删除失败!
File dir=new File(".");
File[] files=dir.listFiles();
if(files!=null&&files.length>0){ for(File file:files){
if(file.isDirectory()){
deleteDirectory(file);//递归删除子目录下的所有子项
}else{
if(!file.delete()){ throw new IOException("无法删除文件:"+file);
}
System.out.println("文件:"+file+"已被删除!");
}
9)FileFilter:文件过滤器。FileFilter是一个接口,不可实例化,可以规定过滤条件,在获取某个目录时可以通过给定的删选条件来获取满足要求的子项。
accept()方法是用来定义过滤条件的参数pathname是将被过滤的目录中的每个子项一次传入进行匹配,若我们认为该子项满足条件则返回true。
如下重写accept方法。
FileFilter filter=new FileFilter(){
public boolean accept(File pathname){
return pathname.getName().endsWith(".java");//保留文件名以.java结尾的
//return pathname.length()>1700;按大小过滤
}
};
File dir=new File(".");//创建一个目录
File[] sub=dir.listFiles(filter);//获取过滤器中满足条件的子项,回调模式
for(File file:sub){
System.out.println(file);
}
10)回调模式:我们定义一段逻辑,在调用其他方法时,将该逻辑通过参数传入。
这个方法在执行过程中会调用我们传入的逻辑来达成目的。这种现象就是回调模式。
最常见的应用环境:按钮监听器,过滤器的应用。
5.3 RandomAccessFile类
可以方便的读写文件内容,但只能一个字节一个字节(byte)的读写8位。
1)计算机的硬盘在保存数据时都是byte by byte的,字节埃着字节。
2)RandomAccessFile打开文件模式:rw:打开文件后可进行读写操作;r:打开文件后只读。
3)RandomAccessFile是基于指针进行读写操作的,指针在哪里就从哪里读写。
①void seek(long pos)方法:从文件开头到设置位置的指针偏移量,在该位置发生下一次读写操作。
②getFilePointer()方法:获取指针当前位置,而seek(0)则将指针移动到文件开始的位置。
③int skipBytes(int n)方法:尝试跳过输入的n个字节。
4)RandomAccessFile类的构造器都是有参的。
①RandomAccessFile构造方法1:
RandomAccessFile raf=new RandomAccessFile(file,"rw");
②RandomAccessFile构造方法2:
RandomAccessFile raf=new RandomAccessFile("data.dat","rw");
直接根据文件路径指定,前提是确保其存在!
5)读写操作完了,不再写了就关闭:close();
6)读写操作:
File file=new File("data.dat");//创建一个File对象用于描述该文件
if(!file.exists()){//不存在则创建该文件
file.createNewFile();//创建该文件,应捕获异常,仅为演示所以抛给main了
}
RandomAccessFile raf=new RandomAccessFile(file,"rw");//创建RandomAccessFile,并将File传入,RandomAccessFile对File表示的文件进行读写操作。
/**1位16进制代表4位2进制;2位16进制代表一个字节 8位2进制;
* 4字节代表32位2进制;write(int) 写一个字节,且是从低8位写
*/
int i=0x7fffffff; //写int值最高的8位
raf.write(i>>>24);//00 00 00 7f
raf.write(i>>>16);//00 00 7f ff
raf.write(i>>>8); // 00 7f ff ff
raf.write(i);// 7f ff ff ff
byte[] data=new byte[]{0,1,2,3,4,5,6,7,8,9};//定义一个10字节的数组并全部写入文件
raf.write(data);//写到这里,当前文件应该有14个字节了
/**写字节数组的重载方法:write(byte[] data.int offset,int length),从data数组的offset位置开始写,连续写length个字节到文件中 */
raf.write(data, 2, 5); // {2,3,4,5,6}
System.out.println("当前指针的位置:"+raf.getFilePointer());
raf.seek(0);//将指针移动到文件开始的位置
int num=0;//准备读取的int值
int b=raf.read();//读取第一个字节 7f 也从低8位开始
num=num | (b<<24); //01111111 00000000 00000000 00000000
b=raf.read(); //读取第二个字节 ff
num=num| (b<<16);//01111111 11111111 00000000 00000000
b=raf.read();//读取第三个字节 ff
num=num| (b<<8);//01111111 11111111 11111111 00000000
b=raf.read();//读取第四个字节 ff
num=num| b;//01111111 11111111 11111111 11111111
System.out.println("int最大值:"+num);
raf.close();//写完了不再写了就关了
7)常用方法:
①write(int data):写入第一个字节,且是从低8位写。
②write(byte[] data):将一组字节写入。
③write(byte[] data.int offset,int length):从data数组的offset位置开始写,连续写length个字节到文件中。
④writeInt(int):一次写4个字节,写int值。
⑤writeLong(long):一次写8个字节,写long值。
⑥writeUTF(String):以UTF-8编码将字符串连续写入文件。
write……
①int read():读一个字节,若已经读取到文件末尾,则返回-1。
②int read(byte[] buf):尝试读取buf.length个字节。并将读取的字节存入buf数组。返回值为实际读取的字节数。
③int readInt():连续读取4字节,返回该int值
④long readLong():连续读取8字节,返回该long值
⑤String readUTF():以UTF-8编码将字符串连续读出文件,返回该字符串值
read……
byte[] buf=new byte[1024];//1k容量
int sum=raf.read(buf);//尝试读取1k的数据
System.out.println("总共读取了:"+sum+"个字节");
System.out.println(Arrays.toString(buf)); raf.close();//写完了不再写了就关了
8)复制操作:读取一个文件,将这个文件中的每一个字节写到另一个文件中就完成了复制功能。
try {
File srcFile=new File("chang.txt");
RandomAccessFile src=new RandomAccessFile(srcFile,"r");//创建一个用于读取文件的RandomAccessFile用于读取被拷贝的文件
File desFile=new File("chang_copy.txt");
desFile.createNewFile();//创建复制文件
RandomAccessFile des=new RandomAccessFile(desFile,"rw");//创建一个用于写入文件的RandomAccessFile用于写入拷贝的文件
//使用字节数组作为缓冲,批量读写进行复制操作比一个字节一个字节读写效率高的多!
byte[] buff=new byte[1024*100];//100k 创建一个字节数组,读取被拷贝文件的所有字节并写道拷贝文件中
int sum=0;//每次读取的字节数
while((sum=src.read(buff))>0){
des.write(buff,0,sum);//注意!读到多少写多少!
}
src.close();
des.close();
System.out.println("复制完毕!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//int data=0;//用于保存每一个读取的字节
//读取一个字节,只要不是-1(文件末尾),就进行复制工作
//while((data=src.read())!=-1){
des.write(data);//将读取的字符写入
}
9)基本类型序列化:将基本类型数据转换为字节数组的过程。writeInt(111):将int值111转换为字节并写入磁盘;持久化:将数据写入磁盘的过程。
5.4基本流:FIS和FOS
Java I/O 输入/输出
流:根据方向分为:输入流和输出流。方向的定了是基于我们的程序的。
流向我们程序的流叫做:输入流;从程序向外流的叫做:输出流
我们可以把流想象为管道,管道里流动的水,而java中的流,流动的是字节。
1)输入流是用于获取(读取)数据的,输出流是用于向外输出(写出)数据的。
InputStream:该接口定义了输入流的特征
OutputStream:该接口定义了输出流的特征
2)流根据源头分为:
基本流(节点流):从特定的地方读写的流类,如磁盘或一块内存区域。即有来源。
处理流(高级流、过滤流):没有数据来源,不能独立存在,它的存在是用于处理基本流的。是使用一个已经存在的输入流或输出流连接创建的。
3)流根据处理的数据单位不同划分为:
字节流:以一个“字节”为单位,以Stream结尾
字符流:以一个“字符”为单位,以Reader/Writer结尾
4)close()方法:流用完一定要关闭!流关闭后,不能再通过其读、写数据
5)用于读写文件的字节流FIS/FOS(基本流)
①FileInputStream:文件字节输入流。
②FileOutputStream:文件字节输出流。
6)FileInputStream 常用构造方法:
①FileInputStream(File file):通过打开一个到实际文件的连接来创建一个FileInputStream,该文件通过文件系统中的File对象file指定。
即向file文件中写入数据。
②FileInputStream(String filePath):通过打开一个到实际文件的连接来创建一个FileInputStream,该文件通过文件系统中的文件路径名指定。
也可直接写当前项目下文件名。
常用方法:
①int read(int d):读取int值的低8位。
②int read(byte[] b):将b数组中所有字节读出,返回读取的字节个数。
③int read(byte[] b,int offset,int length):将b数组中offset位置开始读出length个字节。
④available()方法:返回当前字节输入流 可读取的总字节数。
7)FileOutputStream常用构造方法:
①FileOutputStream(File File):创建一个向指定File对象表示的文件中写入数据的文件输出流。
会重写以前的内容,向file文件中写入数据时,若该文件不存在,则会自动创建该文件。
②FileOubputStream(File file,boolean append):append为true则对当前文件末尾进行写操作(追加,但不重写以前的)。
③FileOubputStream(String filePath):创建一个向具有指定名称的文件中写入数据的文件输出流。
前提路径存在,写当前目录下的文件名或者全路径。
④FileOubputStream(String filePath,boolean append):append为true则对当前文件末尾进行写操作(追加,但不重写以前的)。
常用方法:
①void write(int d):写入int值的低8位。
②void write(byte[] d):将d数组中所有字节写入。
③void write(byte[] d,int offset,int length):将d数组中offset位置开始写入length个字节。
5.5缓冲字节高级流:BIS和BOS
对传入的流进行处理加工,可以嵌套使用。
1)BufferedInputStream:缓冲字节输入流
A.构造方法:BufferedInputStream(InputStream in)
BufferedInputStream(InputStream in, int size)
B.常用方法:
①int read():从输入流中读取一个字节。
②int read(byte[] b,int offset,int length):从此字节输入流中给定偏移量offset处开始将各字节读取到指定的 byte 数组中。
2)BufferedOutputStream:缓冲字节输出流
A.构造方法:BufferedOutputStream(OutputStream out)
BufferedOutputStream(OutputStream out, int size)
B.常用方法:
①void write(int d):将指定的字节写入此缓冲的输出流。
②void write(byte[] d,int offset,int length):将指定 byte数组中从偏移量 offset开始的 length个字节写入此缓冲的输出流。
③void flush():将缓冲区中的数据一次性写出,“清空”缓冲区。
C.内部维护着一个缓冲区,每次都尽可能的读取更多的字节放入到缓冲区,
再将缓冲区中的内容部分或全部返回给用户,因此可以提高读写效率。
3)辨别高级流的简单方法:看构造方法,若构造方法要求传入另一个流,那么这个流就是高级流。
所以高级流是没有空参数的构造器的,都需要传入一个流。
4)有缓冲效果的流,一般为写入操作的流,在数据都写完后一定要flush,flush的作用是将缓冲区中未写出的数据一次性写出:bos.flush();
即不论缓存区有多少数据,先写过去,缓冲区再下班~确保所有字符都写出
5)使用JDK的话,通常情况下,我们只需要关闭最外层的流。第三方流可能需要一层一层关。
5.6基本数据类型高级流:DIS和DOS
是对“流”功能的扩展,简化了对基本类型数据的读写操作。
1)DataInputStream(InputStream in):可以直接读取基本数据类型的流
常用方法:
①int readInt():连续读取4个字节(一个int值),返回该int值
②double readDouble():连续读取8个字节(一个double值),返回double值
③String readUTF():连续读取字符串
……
2)DataOutputStream(OutputStream out):可以直接写基本数据类型的流
常用方法:
①void writeInt(int i):连续写入4个字节(一个int值)
②void writeLong(long l):连续写入8个字节(一个long值)
③void writeUTF(String s):连续写入字符串
④void flush():将缓冲区中的数据一次性写出,“清空”缓冲区。
……
5.7字符高级流:ISR和OSW
以“单个”“字符”为单位读写数据,一次处理一个字符(unicode)。
字符流底层还是基于字节形式读写的。
在字符输入输出流阶段,进行编码修改与设置。
所有字符流都是高级流。
1) OutputStreamWriter:字符输出流。
A.常用构造方法:
OutputStreamWriter(OutputStream out):创建一个字符集的输出流。
OutputStreamWriter(OutputStream out, String charsetName):创建一个使用指定字符集的输出流。
B.常用方法:
①void write(int c):写入单个字符。
②void write(char c[], int off, int len):写入从字符数组off开头到len长度的部分
③void write(String str, int off, int len):写入从字符串off开头到len长度的部分。
④void flush():将缓冲区中的数据一次性写出,“清空”缓冲区。
⑤void close():关闭流。
eg:向文件中写入字符:
①创建文件输出流(字节流)。
②创建字符输出流(高级流),处理文件输出流,目的是我们可以以字节为单位写数据。
③写入字符。
④写完后关闭流。
OutputStreamWriter writer=null;//不写try-catch外的话finally找不到流,就无法关闭
try{ FileOutputStream fos=new FileOutputStream("writer.txt");
// writer=new OutputStreamWriter(fos);//默认构造方法使用系统默认的编码集
writer=new OutputStreamWriter(fos,"UTF-8");//最好指定字符集输出
writer.write("你好!"); writer.flush();//将缓冲区数据一次性写出
}catch(IOException e){
throw e;
}finally{
if(writer!=null){
writer.close();
}
}
2)InputStreamReader:字符输入流。
A.常用构造方法:
InputStreamReader(InputStream in):创建一个字符集的输入流。
InputStreamReader(InputStream in, String charsetName):创建一个使用指定字符集的输入流。
B.常用方法:
①int read():读取单个字符。
②int read(char cbuf[], int offset, int length):读入字符数组中从offset开始的length长度的字符。
③void close():关闭流。
eg:读取文件中的字符
InputStreamReader reader=null;
try{
FileInputStream fis=new FileInputStream("writer.txt"); //创建用于读取文件的字节出入流
reader=new InputStreamReader(fis,"UTF-8"); //创建用于以字符为单位读取数据的高级流
int c=-1;//读取数据
while((c=reader.read())!=-1){ //InputStreamReader只能一个字符一个字符的读
System.out.println((char)c);
}
}catch(IOException e){
throw e;
} finally{
if(reader!=null){
reader.close();
}
}
5.8缓冲字符高级流:BR和BW
可以以“行”为单位读写“字符”,高级流。
在字符输入输出流修改编码。
1)BufferedWriter:缓冲字符输出流,以行为单位写字符
A.常用构造方法:
BufferedWriter(Writer out):创建一个使用默认大小的缓冲字符输出流。
BufferedWriter(Writer out,int size):创建一个使用给定大小的缓冲字符输出流。
B.常用方法:
①void write(int c):写入单个字符。
②void write(char[] c,int off,int len):写入字符数组从off开始的len长度的字符。
③void write(String s,int off,int len):写入字符串中从off开始的len长度的字符。
④void newLine():写入一个行分隔符。
⑤flush():将缓冲区中的数据一次性写出,“清空”缓冲区。
⑥close():关闭流。
注意事项:BufferedWriter的构造方法中不支持给定一个字节输出流,只能给定一个字符输出流Writer的子类,Writer是字符输出流的父类。
//创建用于写文件的输出流
FileOutputStream fos=new FileOutputStream("buffered.txt");
//创建一个字符输出流,在字符输入输出流修改编码
OutputStreamWriter osw=new OutputStreamWriter(fos,"UTF-8");
BufferedWriter writer=new BufferedWriter(osw);
writer.write("你好啊!!");
writer.newLine();//输出一个换行
writer.write("我是第二行!!");
writer.newLine();//输出一个换行
writer.write("我是第三行!!");
writer.close();//输出流关闭后,不能再通过其写数据
2)BufferedReader:缓冲字符输入流,以行为单位读字符
A.常用构造方法:
BufferedReader(Reader in):创建一个使用默认大小的缓冲字符输入流。
BufferedReader(Reader in,int size):创建一个使用指定大小的缓冲字符输入流。
B.常用方法:
①int read():读取单个字符。如果已到达流末尾,则返回-1。
②int read(char cbuf[], int off, int len):从字符数组中读取从off开始的len长度的字符。
返回读取的字符数,如果已到达流末尾,则返回-1。
③String readLine():读取一个文本行。通过下列字符之一即可认为某行已终止:换行 (‘\n‘)、回车 (‘\r‘) 或回车后直接跟着换行。
如果已到达流末尾,则返回 null。EOF:end of file文件末尾。
④void close():关闭流。
eg:读取指定文件中的数据,并显示在控制台
FileInputStream fis=new FileInputStream("src"+File.separator+"day08"+File.separator+"DemoBufferedReader.java");
InputStreamReader isr=new InputStreamReader(fis);
BufferedReader reader=new BufferedReader(isr);
String str=null;
if((str=reader.readLine())!=null){//readLine()读取一行字符并以字符串形式返回
System.out.println(str);
}
reader.close();
eg:读取控制台输入的每以行信息,直到在控制台输入exit退出程序
//1 将键盘的字节输入流转换为字符输入流
InputStreamReader isr=new InputStreamReader(System.in);
//2 将字符输入流转换为缓冲字符输入流,按行读取信息
BufferedReader reader=new BufferedReader(isr);
// 循环获取用户输入的信息并输出到控制台
String info=null; while(true){
info=reader.readLine();
if("exit".equals(info.trim())){
break;
}
System.out.println(info);//输出到控制台
}
reader.close();
5.9文件字符高级流:FR和FW
用于读写“文本文件”的“字符”输入流和输出流。
1)FileWriter写入:继承OutputStreamWriter
A.常用构造方法
FileWriter(File file) 、FileWriter(File file, boolean append)
FileWriter(String filePath)、FileWriter(String fileName, boolean append)
意思和FileOutputStream的四个同类型参数的构造方法一致。
u 注意事项:FileWriter的效果等同于:FileOutputStream + OutputStreamWriter。
B.常用方法:
①void write(int c):写入单个字符。
②void write(char c[], int off, int len):写入字符数组从off到len长度的部分
③void write(String str, int off, int len):写入字符串从off到len长度的部分。
④void flush():将缓冲区中的数据一次性写出,“清空”缓冲区。
⑤void close():关闭流。
FileWriter writer=new FileWriter("filewriter.txt");
//File file=new File("filewriter.txt");
//FileWriter writer=new FileWriter(file);
writer.write("hello!FileWriter!"); writer.close();
2) FileReader读取:继承InputStreamReader
A.“只能”以“字符”为单位读取文件,所以效率低
B.常用构造方法
FileReader(File file)、FileReader(String filePath)
意思和FileInputStream的两个同类型参数的构造方法一致。
C.常用方法:
①int read():读取单个字符。
②int read(char cbuf[], int offset, int length):读入字符数组中从offset开始的length长度的字符。
③void close():关闭流。
FileReader reader=new FileReader("filewriter.txt");
//int c=-1; //只能以字符为单位读取文件
//while((c=reader.read())!=-1){
System.out.println((char)c);
}
//将文件字符输入流转换为缓冲字符输入流便可以行为单位读取
BufferedReader br=new BufferedReader(reader);
String info=null; while((info=br.readLine())!=null){
System.out.println(info);
}
br.close();
5.10 PrintWriter
另一种缓冲“字符”输出流,以“行”为单位,常用它作输出,BufferedWriter用的少。
1)Servlet:运行在服务器端的小程序,给客户端发送相应使用的输出流就是PrintWriter。
2)写方法:println(String data):带换行符输出一个字符串,不用手动换行了。
println……
3)构造方式:
PrintWriter(File file):以行为单位向文件写数据
PrintWriter(OutputStream out):以行为单位向字节输出流写数据
PrintWriter(Writer writer):以行为单位向字符输出流写数据
PrintWriter(String fileName):以行为单位向指定路径的文件写数据
PrintWriter writer=new PrintWriter("printwriter.txt"); //向文件写入一个字符串
writer.println("你好!PrintWriter");//自动加换行符
/**我们要在确定做写操作的时候调用flush()方法,否则数据可能还在输出流的缓冲区中,没有作真实的写操作!*/
writer.flush(); writer.close();
eg:将输出流写入文件
System.out.println("你好!!");
PrintStream out=System.out;
PrintStream fileOut=new PrintStream( new FileOutputStream("SystemOut.txt") );
System.setOut(fileOut);//将我们给定的输出流赋值到System.out上
System.out.println("你好!我是输出到控制台的!");
System.setOut(out);
System.out.println("我是输出到控制台的!");
fileOut.close();
5.11对象序列化
将一个对象转换为字节形式的过程就是对象序列化。
序列化还有个名称为串行化,序列化后的对象再被反序列化后得到的对象,与之前的对象不再是同一个对象。
1)对象序列化必须实现Serializable接口,但该接口无任何抽象方法,不需要重写方法,只为了标注该类可序列化。
2)且同时建议最好添加版本号(编号随便写):serialVersionUID。版本号,用于匹配当前类与其被反序列化的对象是否处于同样的特征(属性列表一致等)。
反序列化时,ObjectInputStream会根据被反序列化对象的版本与当前版本进行匹配,来决定是否反序列化。
不加版本号可以,但是可能存在反序列化失败的风险。
3)JDK提供的大多数java bean都实现了该接口
4)transient关键字:序列化时忽略被它修饰的属性。
5)对象的序列化使用的类:ObjectOutputStream
writeObject(Object obj):①将给定对象序列化。②然后写出。
6)对象的反序列化使用的类:ObjectInputStream
Object readObject():将读取的字节序列还原为对象
7)对于HTTP协议:通信一次后,必须断开连接,想再次通信要再次连接。
8)想要实现断点续传,我们必须告诉服务器我们当前读取文件的开始位置。
相当于我们本地调用的seek(),因为我们不可能直接调用服务器的对象的方法,所以我们只能通过某种方式告诉服务器我们要干什么。
让它自行调用自己流对象的seek()到我们想读取的位置。bytes=0- 的意思是告诉服务器从第一个字节开始读,即seek(0)从头到尾;
bytes=128- 的意思是告诉服务器从地129个字节开始读,即seek(128)。
String prop="bytes="+info.getPos()+"-";
eg:序列化和反序列化
try{
DownloadInfo info=new DownloadInfo("http://www.baidu.com/download/xxx.zip", "xxx.zip" );
info.setPos(12587);
info.setFileSize(5566987);
File file=new File("obj.tmp");//将对象序列化以后写到文件中
FileOutputStream fos=new FileOutputStream(file);
//通过oos可以将对象序列化后写入obj.tmp文件中
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(info);//将info序列化后写出
oos.close();
//反序列化操作
FileInputStream fis=new FileInputStream(file);
ObjectInputStream ois=new ObjectInputStream(fis);
DownloadInfo obj=(DownloadInfo)ois.readObject();//反序列化
System.out.println(obj.getUrl());
System.out.println(obj.getFileName());
System.out.println(obj.getFileSize());
System.out.println(obj.getPos());
System.out.println(info==obj);
ois.close();
}catch(Exception e){
e.printStackTrace();
System.out.println("非常sorry!");
}
5.12 Thread线程类及多线程
进程:一个操作系统中可以同时运行多个任务(程序),每个运行的任务(程序)被称为一个进程。
即系统级别上的多线程(多个任务)。
线程:一个程序同时可能运行多个任务(顺序执行流),那么每个任务(顺序执行流)就叫做一个线程。
即在进程内部。
并发:线程是并发运行的。操作系统将时间化分为若干个片段(时间片),尽可能的均匀分配给每一个任务,被分配时间片后,任务就有机会被cpu所执行。
微观上看,每个任务都是走走停停的。但随着cpu高效的运行,宏观上看所有任务都在运行。
这种都运行的现象称之为并发,但不是绝对意义上的“同时发生”。
1)Thread类的实例代表一个并发任务。任何线程对象都是Thread类的(子类)实例。
Thread类是线程的模版,它封装了复杂的线程开启等操作,封装了操作系统的差异性。
因此并发的任务逻辑实现只要重写Thread的run方法即可。
2)线程调度:线程调度机制会将所有并发任务做统一的调度工作,划分时间片(可以被cup执行的时间)给每一个任务,时间片尽可能的均匀,但做不到绝对均匀。
同样,被分配时间片后,该任务被cpu执行,但调度的过程中不能保证所有任务都是平均的获取时间片的次数。
只能做到尽可能平均。这两个都是程序不可控的。
3)线程的启动和停止:void start():想并发操作不要直接调用run方法!而是调用线程的start()方法启动线程!
void stop():不要使用stop()方法来停止线程的运行,这是不安全的操作,想让线程停止,应该通过run方法的执行完毕来进行自然的结束。
4)线程的创建方式一:
1:继承自Thread。
2:重写run方法:run方法中应该定义我们需要并发执行的任务逻辑代码。
5)线程的创建方式二:
将线程与执行的逻辑分离开,即实现Runnalbe接口。因为有了这样的设计,才有了线程池。关注点在于要执行的逻辑。
6)Runnable接口:用于定义线程要执行的任务逻辑。我们定一个类实现Runnable接口,这时我们必须重写run方法,在其中定义我们要执行的逻辑。
之后将Runnable交给线程去执行。从而实现了线程与其执行的任务分离开。
将任务分别交给不同的线程并发处理,可以使用线程的重载构造方法:Thread(Runnable runnable)。
解藕:线程与线程体解藕,即打断依赖关系。Spring的ioc就是干这个的。
/**创建两个需要并发的任务,MyFirstRunnable和MySecRunnable都继承了Runnable接口并重写了run()方法 */
Runnable r1=new MyFirstRunnable();
Runnable r2=new MySecRunnable();
Thread t1=new Thread(r1);
Thread t2=new Thread(r2);
t1.start();
t2.start();
7)线程的创建方式三:使用匿名内部类方式创建线程
/** * 匿名类实现继承Thread形式*/
new Thread(){
public void run(){
…
}
}.start();
eg:
Thread t1=new Thread(){
public void run(){
for(int i=0;i<1000;i++){
System.out.println(i);
}
}
};
t1.start();
/**匿名类实现Runnable接口的形式 */
new Thread(
new Runnable(){
public void run(){
…
}
}
).start();
eg:
Thread t2=new Thread(new Runnable(){
public void run(){
for(int i=0;i<1000;i++){
System.out.println("你好"+i+"次");
}
}
} );
t2.start();
8)线程生命周期:
9)线程睡眠阻塞:使当前线程放弃cpu时间,进入阻塞状态。在阻塞状态的线程不会分配时间片。
直到该线程结束阻塞状态回到Runnable状态,方可再次获得时间片来让cpu运行(进入Running状态)。
①static void sleep(times)方法:让当前线程主动进入Block阻塞状态,并在time毫秒后回到Runnalbe状态。
注意事项:使用Thread.sleep()方法阻塞线程时,强制让我们必须捕获“中断异常”。
引发情况:当前线程处于Sleep阻塞期间,被另一个线程中断阻塞状态时,当前线程会抛出该异常。
int i=0;
while(true){
System.out.println(i+"秒");
i++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
10)void interrupt()方法:打断/唤醒线程。一个线程可以提前唤醒另外一个sleep Block的线程。
注意事项:方法中定义的类叫局部内部类:局部内部类中,若想引用当前方法的其他局部变量,那么该变量必须是final的。
final Thread lin=new Thread(){
public void run(){
System.out.println("林:睡觉了……");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
System.out.println("林:干嘛呢!干嘛呢!干嘛呢!");
System.out.println("林:都破了相了!");
}
}
};
lin.start();//启动第一个线程
Thread huang=new Thread(){
public void run(){
System.out.println("80一锤子,您说咂哪儿?");
for(int i=0;i<5;i++){
System.out.println("80!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("咣当!");
System.out.println("黄:搞定!");
lin.interrupt();//中断第一个线程的阻塞状态
}
};
huang.start();//启动第二个线程
11)线程的其他方法:
①static void yield():当前线程让出处理器(离开Running状态)即放弃当前时间片,主动进入Runnable状态等待。
②final void setPriority(int):设置线程优先级;优先级越高的线程,理论上获取cpu的次数就越多。
但理想与现实是有差距的……设置线程优先级一定要在线程启动前设置!
③final void join():等待该线程终止。
Thread t1=new Thread(){
public void run(){
for(int i=0;i<100;i++){
System.out.println("我是谁啊?");
Thread.yield();
}
}
};
Thread t2=new Thread(){
public void run(){
for(int i=0;i<100;i++){
System.out.println("我是修水管的");
Thread.yield();
}
}
};
Thread t3=new Thread(){
public void run(){
for(int i=0;i<100;i++){
System.out.println("我是打酱油的");
Thread.yield();
}
}
};
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
t3.start();
12)线程并发安全问题:synchronized关键字,线程安全锁、同步监视器。
多线程在访问同一个数据时(写操作),可能会引发不安全操作。
①哪个线程报错不捕获,则线程死,不影响主程序。
②同步:同一时刻只能有一个执行,A和B配合工作,步调一致的处理(B得到A的执行结果才能继续)。如一群人上公交车。
异步:同一时刻能有多个执行,并发,各自干各自的。如一群人上卡车。
③synchronized可以修饰方法也可以单独作为语句块存在(同步块)。作用是限制多线程并发时同时访问该作用域。
④synchronized修饰方法后,会为方法上锁。方法就不是异步的了,而是同步的。锁的是当前对象。
⑤synchronized同步块:分析出只有一段代码需要上锁,则使用。效率比直接修饰方法要高。
⑥线程安全的效率低,如Vector、Hashtable。线程不安全的效率高,如ArrayList、HashMap
synchronized void getMoney(int money){
if(count==0){
throw new RuntimeException("余额为0");
}
Thread.yield();
count-=money;
}
void getMoney(int money){
synchronized(this){ //synchronized(Object){需要同步的代码片段}
if(count==0){
throw new RuntimeException("余额为0");
}
Thread.yield();
count-=money;
}
13)Daemon后台线程也称为守护线程:当当前进程中“所有”“前台”线程死亡后,后台线程将被强制死亡(非自然死亡),无论是否还在运行。
①守护线程,必须在启动线程前调用。
②main方法也是靠线程运行的,且是一个前台线程。
③正在运行的线程都是守护线程时,JVM退出。
14)wait/notify方法
这两个方法不是在线程Thread中定义的方法,这两个方法定义在Object中。两个方法的作用是用于协调线程工作的。
①等待机制与锁机制密切关联:wait/notify方法必须与synchronized同时使用,谁调用wait或otify方法,就锁谁!
②wait()方法:当条将不满足时,则等待。当条件满足时,等待该条件的线程将被唤醒。
如:浏览器显示一个图片,displayThread要想显示图片,则必须等代下载线程downloadThread将该图片下载完毕。
如果图片没有下杂完成,则dialpayThread可以暂停。当downloadThread下载完成后,再通知displayThread可以显示了,此时displayThread继续执行。
③notify()方法:随机通知、唤醒一个在当前对象身上等待的线程。
④notifyAll方法:通知、唤醒所有在当前对象身上等待的线程。
5.13 Socket网络编程
Socket套接字。在java.net.Socket包下。
1)网络通信模型:
C/S:client/server,客户端/服务器端;
B/S:browser/server,浏览器端/服务器端;
C/S结构的优点:应用的针对性强,画面绚丽,应用功能复杂。缺点:不易维护。
B/S结构的优点:易于维护。缺点:效果差,交互性不强。
2)Socket:封装着本地的地址,服务端口等信息。ServerSocket:服务端的套接字。
服务器:使用ServerSocket监听指定的端口,端口可以随意指定(由于1024以下的端口通常属于保留端口,在一些操作系统中不可以随意使用,
所以建议使用大于1024的端口),等待客户连接请求,客户连接后,会话产生;在完成会话后,关闭连接。
客户端:使用Socket对网络上某一个服务器的某一个端口发出连接请求,一旦连接成功,打开会话;会话完成后,关闭Socket。
客户端不需要指定打开的端口,通常临时的、动态的分配一个1024以上的端口。
3)永远都是Socket去主动连接ServerSocket。一个ServerSocket可以接收若干个Socket的连接。网络通信的前提:一定要捕获异常。
4)Socket连接基于TCP/IP协议,是一种长连接(长时间连着)。
5)读取服务器信息会阻塞,写操作不会。
6)建立连接并向服务器发送信息步骤:
①通过服务器的地址及端口与服务器连接,而创建Socket时需要以上两个数据。
②连接成功后可以通过Socket获取输入流和输出流,使用输入流接收服务端发送过来的信息。
③关闭连接。
7)连接服务器:一旦Socket被实例化,那么它就开始通过给定的地址和端口号去尝试与服务器进行连接(自动的)。
这里的地址"localhost"是服务器的地址,8088端口是服务器对外的端口。我们自身的端口是系统分配的,我们无需知道。
8)和服务器通信(读写数据):使用Socket中的getInputStream()获取输入流,使用getOutputStream()获取输出流。
9)ServerSocket构造方法要求我们传入打开的端口号,ServerSocket对象在创建的时候就向操作系统申请打开这个端口。
10)通过调用ServerSocket的accept方法,使服务器端开始等待接收客户端的连接。
该方法是一个阻塞方法,监听指定的端口是否有客户端连接。直到有客户端与其连接并接收客户端套接字,否则该方法不会结束。
eg1.1:客户端ClientDemo类
private Socket socket;
public void send(){
try{
System.out.println("开始连接服务器");
socket=new Socket("localhost",8088);
InputStream in=socket.getInputStream();//获取输入流
OutputStream out=socket.getOutputStream();//获取输出流
/**将输出流变成处理字符的缓冲字符输出流*/
PrintWriter writer=new PrintWriter(out);
writer.println("你好!服务器!");
/**注意,写到输出流的缓冲区里了,并没有真的发给服务器。想真的发送就要作真实的写操作,清空缓冲区*/
writer.flush();
/**将输入流转换为缓冲字符输入流*/
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
/**读取服务器发送过来的信息*/
String info=reader.readLine();//读取服务器信息会阻塞
System.out.println(info);
writer.println("再见!服务器!");
writer.flush();
info=reader.readLine();
System.out.println(info);
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args){
ClientDemo demo=new ClientDemo();
demo.send();//连接服务器并通信
}
eg1.2:服务器端ServerDemo类(不使用线程)
private ServerSocket socket=null;
private int port=8088;
/**构建ServerDemo对象时就打开服务端口*/
public ServerDemo(){
try{
socket=new ServerSocket(port);
}catch(Exception e){
e.printStackTrace();
}
}
/**开始服务,等待收受客户端的请求并与其通信*/
public void start(){
try{
System.out.println("等待客户端连接……");
Socket s=socket.accept();
//获取与客户端通信的输入输出流
InputStream in=s.getInputStream();
OutputStream out=s.getOutputStream();
//包装为缓冲字符流
PrintWriter writer=new PrintWriter(out);
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
//先听客户端发送的信息
String info=reader.readLine();//这里同样会阻塞
System.out.println(info);
//发送信息给客户端
writer.println("你好!客户端");
writer.flush();
info=reader.readLine();
System.out.println(info);
writer.println("再见!客户端");
writer.flush();
socket.close();//关闭与客户端的连接
}catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] args){
System.out.println("服务器启动中……");
ServerDemo demo=new ServerDemo();
demo.start();
}
eg2:服务器端ServerDemo类(使用线程),start()方法的修改以及Handler类
public void start(){
try{
while(true){
System.out.println("等待客户端连接……");
Socket s=socket.accept();
/** 当一个客户端连接了,就启动一个线程去接待它 */
Thread clientThread=new Thread(new Handler(s));
clientThread.start();
}
}catch(Exception e){
e.printStackTrace();
}
}
/** 定义线程体,该线程的作用是与连接到服务器端的客户端进行交互操作 */
class Handler implements Runnable{
private Socket socket;//当前线程要进行通信的客户端Socket
public Handler(Socket socket){//通过构造方法将客户端的Socket传入
this.socket=socket;
}
public void run(){
try{ //获取与客户端通信的输入输出流
InputStream in=socket.getInputStream();
OutputStream out=socket.getOutputStream();
PrintWriter writer=new PrintWriter(out);//包装为缓冲字符流
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
String info=reader.readLine();//先听客户端发送的信息,这里同样会阻塞
System.out.println(info);
//发送信息给客户端
writer.println("你好!客户端");
writer.flush();
info=reader.readLine();
System.out.println(info);
writer.println("再见!客户端");
writer.flush();
socket.close();//关闭与客户端的连接
}catch(Exception e){
e.printStackTrace();
}
}
}
public static void main(String[] args){
System.out.println("服务器启动中……");
ServerDemo demo=new ServerDemo();
demo.start();
}
5.14线程池
线程若想启动需要调用start()方法。这个方法要做很多操作。要和操作系统打交道。注册线程等工作,等待线程调度。
ExecutorService提供了管理终止线程池的方法。
1)线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,
服务完后不关闭该线程,而是将该线程还回到线程池中。在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,
线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,一个线程同时只能执行一个任务,
但可以同时向一个线程池提交多个任务。
2)线程池的创建都是工厂方法。我们不要直接去new线程池,因为线程池的创建还要作很多的准备工作。
3)常见构造方法:
①Executors.newCachedThreadPool():可根据任务需要动态创建线程,来执行任务。
若线程池中有空闲的线程将重用该线程来执行任务。
没有空闲的则创建新线程来完成任务。理论上池子里可以放int最大值个线程。
缓存线程生命周期1分钟,得不到任务直解kill
②Executors.newFixedThreadPool(int threads):创建固定大小的线程池。池中的线程数是固定的。若所有线程处于饱和状态,新任务将排队等待。
③Executors.newScheduledThreadPool():创建具有延迟效果的线程池。可将带运行的任务延迟指定时长后再运行。
④Executors.newSingleThreadExecutor():创建单线程的线程池。池中仅有一个线程。所有未运行的任务排队等待。
5.15双缓冲队列
BlockingQueue:解决了读写数据阻塞问题,但是同时写或读还是同步的。
1)双缓冲队列加快了读写数据操作,双缓冲对列可以规定队列存储元素的大小,一旦队列中的元素达到最大值,待插入的元素将等。
等待时间是给定的,当给定时间到了元素还没有机会被放入队列那么会抛出超时异常。
2)LinkedBlockingQueue是一个可以不指定队列大小的双缓冲队列。若指定大小,当达到峰值后,待入队的将等待。理论上最大值为int最大值。
eg1.1:log服务器写日志文件,客户端ClientDemo类,try语句块中修改如下
try{
System.out.println("开始连接服务器");
socket=new Socket("localhost",8088);
OutputStream out=socket.getOutputStream();
PrintWriter writer=new PrintWriter(out);
while(true){
writer.println("你好!服务器!");
writer.flush();
Thread.sleep(500);
}
}
eg1.2:log服务器写日志文件,服务器端ServerDemo类,增加线程池和双缓冲队列两个属性,删掉与原客户端的输出流
private ExecutorService threadPool;//线程池
private BlockingQueue<String> msgQueue; //双缓冲队列
public ServerDemo(){
try{
socket=new ServerSocket(port);
//创建50个线程的固定大小的线程池
threadPool=Executors.newFixedThreadPool(50);
msgQueue=new LinkedBlockingQueue<String>(10000);
/**创建定时器,周期性的将队列中的数据写入文件*/
Timer timer=new Timer();
timer.schedule(new TimerTask(){
public void run(){
try{ //创建用于向文件写信息的输出流
PrintWriter writer=new PrintWriter(new FileWriter("log.txt",true));
//从队列中获取所有元素,作写出操作
String msg=null;
for(int i=0;i<msgQueue.size();i++){
/**参数 0:时间量TimeUnit.MILLISECONDS:时间单位*/
msg=msgQueue.poll(0,TimeUnit.MILLISECONDS);
if(msg==null){
break;
}
writer.println(msg);//通过输出流写出数据
}
writer.close();
}catch(Exception e){
e.printStackTrace();
}
}
}, 0,500);
}catch(Exception e){
e.printStackTrace();
}
}
public void start(){
try{
while(true){
System.out.println("等待客户端连接……");
Socket s=socket.accept();
/**将线程体(并发的任务)交给线程池,线程池会自动将该任务分配给一个空闲线程去执行。*/
threadPool.execute(new Handler(s));
System.out.println("一个客户端连接了,分配线程");
}
}catch(Exception e){
e.printStackTrace();
}
}
/**定义线程体,该线程的作用是与连接到服务器端的客户端进行交互操作*/
class Handler implements Runnable{
private Socket socket;//当前线程要进行通信的客户端Socket
public Handler(Socket socket){//通过构造方法将客户端的Socket传入
this.socket=socket;
}
public void run(){
try{ //获取与客户端通信的输入输出流
InputStream in=socket.getInputStream();
//包装为缓冲字符流
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
String info=null;
while(true){//循环读取客户端发送过来的信息
info=reader.readLine();
if(info!=null){ //插入对列成功返回true,失败返回false
//该方法会阻塞线程,若中断会报错!
boolean b=msgQueue.offer(info, 5, TimeUnit.SECONDS);
}
}
}catch(Exception e){ e.printStackTrace(); }
}
}