1.File类
File类的每一个实例可以表示硬盘(文件系统)中的一个文件或目录(实际上表示的是一个抽象路径)
使用File可以做到:
- 1:访问其表示的文件或目录的属性信息,例如:名字,大小,修改时间等等
- 2:创建和删除文件或目录
- 3:访问一个目录中的子项
但是File不能访问文件数据.
public class FileDemo {public static void main(String[] args) {//使用File访问当前项目目录下的demo.txt文件/*创建File时要指定路径,而路径通常使用相对路径。相对路径的好处在于有良好的跨平台性。"./"是相对路径中使用最多的,表示"当前目录",而当前目录是哪里取决于程序运行环境而定,在idea中运行java程序时,这里指定的当前目录就是当前程序所在的项目目录。*/
// File file = new File("c:/xxx/xxx/xx/xxx.txt");File file = new File("./demo.txt");//获取名字String name = file.getName();System.out.println(name);//获取文件大小(单位是字节)long len = file.length();System.out.println(len+"字节");//是否可读可写boolean cr = file.canRead();boolean cw = file.canWrite();System.out.println("是否可读:"+cr);System.out.println("是否可写:"+cw);//是否隐藏boolean ih = file.isHidden();System.out.println("是否隐藏:"+ih);}}
1.1创建一个新文件
createNewFile()方法,可以创建一个新文件
package file;import java.io.File;
import java.io.IOException;/*** 使用File创建一个新文件*/
public class CreateNewFileDemo {public static void main(String[] args) throws IOException {//在当前目录下新建一个文件:test.txtFile file = new File("./test.txt");//boolean exists()判断当前File表示的位置是否已经实际存在该文件或目录if(file.exists()){System.out.println("该文件已存在!");}else{file.createNewFile();//将File表示的文件创建出来System.out.println("文件已创建!");}}
}
1.2删除一个文件
delete()方法可以将File表示的文件删除
package file;import java.io.File;/*** 使用File删除一个文件*/
public class DeleteFileDemo {public static void main(String[] args) {//将当前目录下的test.txt文件删除/*相对路径中"./"可以忽略不写,默认就是从当前目录开始的。*/File file = new File("test.txt");if(file.exists()){file.delete();System.out.println("文件已删除!");}else{System.out.println("文件不存在!");}}
}
1.3创建目录
mkDir():创建当前File表示的目录
mkDirs():创建当前File表示的目录,同时将所有不存在的父目录一同创建
package file;import java.io.File;/*** 使用File创建目录*/
public class MkDirDemo {public static void main(String[] args) {//在当前目录下新建一个目录:demo
// File dir = new File("demo");File dir = new File("./a/b/c/d/e/f");if(dir.exists()){System.out.println("该目录已存在!");}else{
// dir.mkdir();//创建目录时要求所在的目录必须存在dir.mkdirs();//创建目录时会将路径上所有不存在的目录一同创建System.out.println("目录已创建!");}}
}
1.4删除目录
delete()方法可以删除一个目录,但是只能删除空目录。
package file;import java.io.File;/*** 删除一个目录*/
public class DeleteDirDemo {public static void main(String[] args) {//将当前目录下的demo目录删除File dir = new File("demo");
// File dir = new File("a");if(dir.exists()){dir.delete();//delete方法删除目录时只能删除空目录System.out.println("目录已删除!");}else{System.out.println("目录不存在!");}}
}
1.5访问一个目录中的所有子项
listFiles方法可以访问一个目录中的所有子项
package file;import java.io.File;/*** 访问一个目录中的所有子项*/
public class ListFilesDemo1 {public static void main(String[] args) {//获取当前目录中的所有子项File dir = new File(".");/*boolean isFile()判断当前File表示的是否为一个文件boolean isDirectory()判断当前File表示的是否为一个目录*/if(dir.isDirectory()){/*File[] listFiles()将当前目录中的所有子项返回。返回的数组中每个File实例表示其中的一个子项*/File[] subs = dir.listFiles();System.out.println("当前目录包含"+subs.length+"个子项");for(int i=0;i<subs.length;i++){File sub = subs[i];System.out.println(sub.getName());}}}
}
1.6获取目录中符合特定条件的子项
重载的listFiles方法:File[] listFiles(FileFilter)
该方法要求传入一个文件过滤器,并仅将满足该过滤器要求的子项返回。
package file;import java.io.File;
import java.io.FileFilter;/*** 重载的listFiles方法,允许我们传入一个文件过滤器从而可以有条件的获取一个目录* 中的子项。*/
public class ListFilesDemo2 {public static void main(String[] args) {/*需求:获取当前目录中所有名字以"."开始的子项*/File dir = new File(".");if(dir.isDirectory()){
// FileFilter filter = new FileFilter(){//匿名内部类创建过滤器
// public boolean accept(File file) {
// String name = file.getName();
// boolean starts = name.startsWith(".");//名字是否以"."开始
// System.out.println("过滤器过滤:"+name+",是否符合要求:"+starts);
// return starts;
// }
// };
// File[] subs = dir.listFiles(filter);//方法内部会调用accept方法File[] subs = dir.listFiles(new FileFilter(){public boolean accept(File file) {return file.getName().startsWith(".");}});System.out.println(subs.length);}}
}
2.Lambda表达式
JDK8之后,java支持了lambda表达式这个特性.
- lambda可以用更精简的代码创建匿名内部类.但是该匿名内部类实现的接口只能有一个抽象方法,否则无法使用!
- lambda表达式是编译器认可的,最终会将其改为内部类编译到class文件中
package lambda;import java.io.File;
import java.io.FileFilter;/*** JDK8之后java支持了lambda表达式这个特性* lambda表达式可以用更精简的语法创建匿名内部类,但是实现的接口只能有一个抽象* 方法,否则无法使用。* lambda表达式是编译器认可的,最终会被改为内部类形式编译到class文件中。** 语法:* (参数列表)->{* 方法体* }*/
```java
public class LambdaDemo {public static void main(String[] args) {//匿名内部类形式创建FileFilterFileFilter filter = new FileFilter() {public boolean accept(File file) {return file.getName().startsWith(".");}};FileFilter filter2 = (File file)->{return file.getName().startsWith(".");};//lambda表达式中参数的类型可以忽略不写FileFilter filter3 = (file)->{return file.getName().startsWith(".");};/*lambda表达式方法体中若只有一句代码,则{}可以省略如果这句话有return关键字,那么return也要一并省略!*/FileFilter filter4 = (file)->file.getName().startsWith(".");}
}
3.JAVA IO
-
java io可以让我们用标准的读写操作来完成对不同设备的读写数据工作.
-
java将IO按照方向划分为输入与输出,参照点是我们写的程序.
-
输入:用来读取数据的,是从外界到程序的方向,用于获取数据.
-
输出:用来写出数据的,是从程序到外界的方向,用于发送数据.
java将IO比喻为"流",即:stream. 就像生活中的"电流",“水流"一样,它是以同一个方向顺序移动的过程.只不过这里流动的是字节(2进制数据).所以在IO中有输入流和输出流之分,我们理解他们是连接程序与另一端的"管道”,用于获取或发送数据到另一端.
3.1Java定义了两个超类(抽象类):
- java.io.InputStream:所有字节输入流的超类,其中定义了读取数据的方法.因此将来不管读取的是什么设备(连接该设备的流)都有这些读取的方法,因此我们可以用相同的方法读取不同设备中的数据
- java.io.OutputStream:所有字节输出流的超类,其中定义了写出数据的方法.
3.2java将流分为两类:节点流与处理流:
- 节点流:也称为低级流.节点流的另一端是明确的,是实际读写数据的流,读写一定是建立在节点流基础上进行的.
- 处理流:也称为高级流.处理流不能独立存在,必须连接在其他流上,目的是当数据流经当前流时对数据进行加工处理来简化我们对数据的该操作.
实际应用中,我们可以通过串联一组高级流到某个低级流上以流水线式的加工处理对某设备的数据进行读写,这个过程也成为流的连接,这也是IO的精髓所在.
3.3文件流
文件流是一对低级流,用于读写文件数据的流.用于连接程序与文件(硬盘)的"管道".负责读写文件数据.
3.4文件输出流:java.io.FileOutputStream
package io;import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;/*** JAVA IO Input&Output 输入和输出* java程序与外界交换数据是基于IO完成的,这里输入与输出一个负责读一个负责写* 输入:是从外界到我们写的程序的方向,是用来从外界获取信息的。因此是"读"操作* 输出:是从我们写的程序到外界的方向,是用来向外界发送信息的。因此是"写"操作** java将IO比喻为"流",可以理解为是程序与外界连接的"管道",内部流动的是字节,并且* 字节是顺着同一侧方向顺序移动的。** java.io.InputStream 输入流,这个类是所有字节输入流的超类,规定了所有字节输入* 流读取数据的相关方法。* java.io.OutputStream 输出流,这个类是所有字节输出流的超类,规定了所有字节输出* 流写出数据的相关方法。** 实际应用中,我们连接不同的设备,java都专门提供了用于连接它的输入流与输出流,而* 这些负责实际连接设备的流称为节点流,也叫低级流。是真实负责读写数据的流。* 与之对应的还有高级流,高级流可以连接其他的流,目的是当数据流经它们时,对数据做某* 种加工处理,用来简化我们的操作。*** 文件流* java.io.FileInputStream和FileOutputStream* 这是一对低级流,继承自InputStream和OutputStream。用于读写硬盘上文件的流**/
public class FOSDemo {public static void main(String[] args) throws IOException {//向当前目录下的demo.dat文件中写入数据/*FileOutputStream提供的常用构造器FileOutputStream(String path)FileOutputStream(File file)*///文件流创建时,如果该文件不存在会自动将其创建(前提是该文件所在目录必须存在!)FileOutputStream fos = new FileOutputStream("./demo.dat");/*void write(int d)向文件中写入1个字节,写入的内容是给定的int值对应的2进制的"低八位"int值 1: vvvvvvvv二进制:00000000 00000000 00000000 00000001demo.dat文件内容:00000000*/fos.write(1);/*vvvvvvvv00000000 00000000 00000000 00000010demo.dat文件内容00000001 00000010*/fos.write(2);fos.close();System.out.println("执行完了!");}
}
3.5文件输入流
package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;/*** end 结尾* read 读*** 文件字节输入流,用于从文件中读取字节*/
public class FISDemo {public static void main(String[] args) throws IOException {/*fos.dat文件内容00000001 00000011*/FileInputStream fis = new FileInputStream("fos.dat");/*int read()读取一个字节,并一int型返回。返回的整数中读取的字节部分在该整数2进制的最后8位上如果返回值为整数-1,则表示流读取到了末尾。对于读取文件而言就是EOF(end of file文件末尾)第一次调用read():int d = fis.read();fos.dat文件内容00000001 00000011^^^^^^^^读取该字节返回int值时,2进制样子:00000000 00000000 00000000 00000001^^^^^^^^|-----补充24个0(3字节)-----| 读取的字节返回的int值d就是上述内容*/int d = fis.read();System.out.println(d);/*第二次调用read()d = fis.read();fos.dat文件内容00000001 00000011^^^^^^^^读取该字节返回int值时,2进制样子:00000000 00000000 00000000 00000011^^^^^^^^|-----补充24个0(3字节)-----| 读取的字节返回的int值d就是上述内容*/d = fis.read();System.out.println(d);/*第三次调用read()d = fis.read();fos.dat文件内容00000001 00000011^^^^^^^^文件末尾了返回int值时,2进制样子:11111111 11111111 11111111 11111111^^^^^^^^|-----补充32个1(4字节,来表示-1)-----|返回的int值d就是上述内容*/d = fis.read();System.out.println(d);fis.close();}
}
3.6文件复制
package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;/*** 文件的复制*/
public class CopyDemo {public static void main(String[] args) throws IOException {//创建文件输入流读取原文件FileInputStream fis = new FileInputStream("image.jpg");//创建文件输出流写入复制文件FileOutputStream fos = new FileOutputStream("image_cp.jpg");int d;//保存每次读取到的字节/*原文件数据:11000011 10101010 00001111 11001100 00110011 ...^^^^^^^^d = fis.read();d:00000000 00000000 00000000 10101010fos.write(d);复制文件的数据:11000011 10101010*/long start = System.currentTimeMillis();//获取当前系统时间的毫秒值(UTC时间)while((d = fis.read()) != -1) {fos.write(d);}long end = System.currentTimeMillis();//获取当前系统时间的毫秒值(UTC时间)System.out.println("复制完毕!耗时:"+(end-start)+"ms");fis.close();fos.close();}
}
3.7块读写的文件复制操作
int read(byte[] data)
一次性从文件中读取给定的字节数组总长度的字节量,并存入到该数组中。
返回值为实际读取到的字节量。若返回值为-1则表示读取到了文件末尾。
块写操作
void write(byte[] data)
一次性将给定的字节数组所有字节写入到文件中
void write(byte[] data,int offset,int len)
一次性将给定的字节数组从下标offset处开始的连续len个字节写入文件
package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;/*** 通过提高每次读写的数据量,减少实际读写的次数,可以提高读写效率。* 单字节读写是一种随机读写形式。而一组一组字节的读写是块读写形式。*/
public class CopyDemo2 {public static void main(String[] args) throws IOException {//使用块读写形式完成文件复制//创建文件输入流读取原文件FileInputStream fis = new FileInputStream("wnwb.exe");//创建文件输出流写复制文件FileOutputStream fos = new FileOutputStream("wnwb_cp.exe");/*流提供了块读写的方法int read(byte[] data)一次性从文件中读取给定的字节数组总长度的字节量,并存入到该数组中。返回值为实际读取到的字节量。若返回值为-1则表示读取到了文件末尾。文件数据11001100 11110000 10101010 00001111 00110011^^^^^^^^ ^^^^^^^^ ^^^^^^^^int d;byte[] data = new byte[3];[00000000 00000000 00000000]第一次调用d = fis.read(data);[11001100 11110000 10101010]d = 3 本次读取到了3个字节文件数据11001100 11110000 10101010 00001111 00110011^^^^^^^^ ^^^^^^^^第二次调用d = fis.read(data);//仅读取了最后两个字节[00001111 00110011 10101010]//前两个字节为本次读取的内容^^^^^^^^ ^^^^^^^^d = 2 本次读取到了2个字节文件数据11001100 11110000 10101010 00001111 00110011 文件末尾!^^^^^^^^第三次调用d = fis.read(data);//一个字节都没有读取到[00001111 00110011 10101010]数组没变化d = -1 文件末尾块写操作void write(byte[] data)一次性将给定的字节数组所有字节写入到文件中void write(byte[] data,int offset,int len)一次性将给定的字节数组从下标offset处开始的连续len个字节写入文件*/int len;//记录每次实际读取的字节量/*00000000 1byte 8位2进制称为1字节1024byte 1kb1024kb 1mb1024mb 1gb*/byte[] data = new byte[1024*10];//10kblong start = System.currentTimeMillis();while((len = fis.read(data))!=-1){fos.write(data,0,len);//读取多少就写多少}long end = System.currentTimeMillis();System.out.println("复制完毕!耗时:"+(end-start)+"ms");fis.close();fos.close();}
}
3.8写文本数据
String提供方法:
byte[] getBytes(String charsetName)
将当前字符串转换为一组字节
参数为字符集的名字,常用的是UTF-8。 其中中文字3字节表示1个,英文1字节表示1个。
package io;import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;/*** 向文件中写入文本数据*/
public class WriteStringDemo {public static void main(String[] args) throws IOException {FileOutputStream fos = new FileOutputStream("demo.txt");String str = "super idol的笑容都没你的甜,";/*支持中文的常见字符集有:GBK:国标编码。英文每个字符占1个字节,中文每个字符占2个字节UTF-8:内部是unicode编码,在这个基础上不同了少部分2进制信息作为长度描述英文每个字符占1字节中文每个字符占3字节String提供了将字符串转换为一组字节的方法byte[] getBytes(String charsetName)参数为字符集的名字,名字不缺分大小写,但是拼写错误会引发异常:UnsupportedEncodingException不支持 字符集 异常*/byte[] data = str.getBytes("UTF-8");fos.write(data);fos.write("八月正午的阳光,都没你耀眼。".getBytes("UTF-8"));System.out.println("写出完毕!");fos.close();}
}
3.9文件输出流-追加模式
重载的构造方法可以将文件输出流创建为追加模式
- FileOutputStream(String path,boolean append)
- FileOutputStream(File file,boolean append)
当第二个参数传入true时,文件流为追加模式,即:指定的文件若存在,则原有数据保留,新写入的数据会被顺序的追加到文件中
package io;import java.io.FileOutputStream;
import java.io.IOException;/*** 文件流的追加写模式*/
public class FileAppendDemo {public static void main(String[] args) throws IOException {/*FileOutputStream默认创建方式为覆盖模式,即:如果连接的文件存在,则会将该文件原有数据全部删除。然后将通过当前流写出的内容保存到文件中。重载的构造方法允许我们再传入一个boolean型参数,如果这个值为true,则文件流为追加模式,即:若连接文件时该文件存在,原有数据全部保留,通过当前流写出的数据会顺序的追加到文件中。*/FileOutputStream fos = new FileOutputStream("demo.txt",true);fos.write("热爱105°的你,".getBytes("UTF-8"));fos.write("滴滴清纯的蒸馏水。".getBytes("UTF-8"));System.out.println("写出完毕!");fos.close();}
}
3.10读取文本数据
package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;/*** 从文件中读取文本数据*/
public class ReadStringDemo {public static void main(String[] args) throws IOException {FileInputStream fis = new FileInputStream("fos.txt");byte[] data = new byte[1024];int len = fis.read(data);//块读操作System.out.println("实际读取到了"+len+"个字节");/*String提供了将字节数组转换为字符串的构造方法:String(byte[]data,String charsetName)将给定的字节数组中所有字节按照指定的字符集转换为字符串String(byte[]data,int offset,int len,String charsetName)将给定的字节数组从下标offset处开始的连续len个字节按照指定的字符集转换为字符串*/String line = new String(data,0,len,"UTF-8");System.out.println(line);System.out.println(line.length());fis.close();}
}
3.11高级流
3.11.1缓冲流
3.11.1.1java.io.BufferedOutputStream和BufferedInputStream.
缓冲流是一对高级流,作用是提高读写数据的效率.
缓冲流内部有一个字节数组,默认长度是8K.缓冲流读写数据时一定是将数据的读写方式转换为块读写来保证读写效率.
3.11.1.2使用缓冲流完成文件复制操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fQSc25AE-1649901720796)(image-20210427164418295.png)]
package io;import java.io.*;/*** java将流分为节点流与处理流两类* 节点流:也称为低级流,是真实连接程序与另一端的"管道",负责实际读写数据的流。* 读写一定是建立在节点流的基础上进行的。* 节点流好比家里的"自来水管"。连接我们的家庭与自来水厂,负责搬运水。* 处理流:也称为高级流,不能独立存在,必须连接在其他流上,目的是当数据经过当前流时* 对其进行某种加工处理,简化我们对数据的同等操作。* 高级流好比家里常见的对水做加工的设备,比如"净水器","热水器"。* 有了它们我们就不必再自己对水进行加工了。* 实际开发中我们经常会串联一组高级流最终连接到低级流上,在读写操作时以流水线式的加工* 完成复杂IO操作。这个过程也称为"流的连接"。** 缓冲流,是一对高级流,作用是加快读写效率。* java.io.BufferedInputStream和java.io.BufferedOutputStream**/
public class CopyDemo3 {public static void main(String[] args) throws IOException {FileInputStream fis = new FileInputStream("ppt.pptx");BufferedInputStream bis = new BufferedInputStream(fis);FileOutputStream fos = new FileOutputStream("ppt_cp.pptx");BufferedOutputStream bos = new BufferedOutputStream(fos);int d;long start = System.currentTimeMillis();while((d = bis.read())!=-1){//使用缓冲流读取字节bos.write(d);//使用缓冲流写出字节}long end = System.currentTimeMillis();System.out.println("耗时:"+(end-start)+"ms");bis.close();//关闭流时只需要关闭高级流即可,它会自动关闭它连接的流bos.close();}
}
3.11.1.3缓冲输出流写出数据时的缓冲区问题
通过缓冲流写出的数据会被临时存入缓冲流内部的字节数组,直到数组存满数据才会真实写出一次
package io;import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;/*** 缓冲输出流写出数据的缓冲区问题*/
public class BOS_FlushDemo {public static void main(String[] args) throws IOException {FileOutputStream fos = new FileOutputStream("bos.txt");BufferedOutputStream bos = new BufferedOutputStream(fos);String line = "奥里给!";byte[] data = line.getBytes(StandardCharsets.UTF_8);bos.write(data);System.out.println("写出完毕!");/*缓冲流的flush方法用于强制将缓冲区中已经缓存的数据一次性写出。注:该方法实际上实在字节输出流的超类OutputStream上定义的,并非只有缓冲输出流有这个方法。但是实际上只有缓冲输出流的该方法有实际意义,其他的流实现该方法的目的仅仅是为了在流连接过程中传递flush动作给缓冲输出流。*/bos.flush();//冲bos.close();}
}
3.11.2对象流
java.io.ObjectOutputStream和ObjectInputSteam
对象流是一对高级流,在流连接中的作用是进行对象的序列化与反序列化。
对象序列化:将一个java对象按照其结构转换为一组字节的过程
对象反序列化:将一组字节还原为java对象(前提是这组字节是一个对象序列化得到的字节)
3.11.2.1对象序列化的流连接操作原理图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kcedXKBs-1649902337592)(image-20210528162433406.png)]
package io;import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;/*** 对象流* java.io.ObjectOutputStream和ObjectInputSteam* 对象流是一对高级流,在流连接中的作用是进行对象的序列化与反序列化** 对象序列化:将一个java对象按照其结构转换为一组字节的过程* 对象反序列化:将一组字节还原为java对象(前提是这组字节是一个对象序列化得到的字节)*/
public class OOSDemo {public static void main(String[] args) throws IOException {//将一个Person对象写入文件person.objString name = "苍老师";int age = 18;String gender = "女";String[] otherInfo = {"是一名台词不多的演员","来自岛国","爱好写大字","广大男性同胞的启蒙老师"};Person p = new Person(name,age,gender,otherInfo);System.out.println(p);FileOutputStream fos = new FileOutputStream("person.obj");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(p);System.out.println("写出完毕!");oos.close();}
}
3.11.2.2对象反序列化
package io;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;/*** 使用对象输入流完成对象的反序列化*/
public class OISDemo {public static void main(String[] args) throws IOException, ClassNotFoundException {//从person.obj文件中将对象反序列化回来FileInputStream fis = new FileInputStream("person.obj");ObjectInputStream ois = new ObjectInputStream(fis);/*Object readObject()该方法会进行对象的反序列化,如果对象流通过其连接的流读取的字节分析并非是一个java对象时,会抛出异常:ClassNotFoundException*/Person p = (Person)ois.readObject();System.out.println(p);}
}
需要进行序列化的类必须实现接口:java.io.Serializable
实现序列化接口后最好主动定义序列化版本号这个常量。
这样一来对象序列化时就不会根据类的结构生成一个版本号,而是使用该固定值。
那么反序列化时,只要还原的对象和当前类的版本号一致就可以进行还原。
transient关键字可以修饰属性,用于在进行对象序列化时忽略不必要的属性,达到对象瘦身的目的
package io;import java.io.Serializable;
import java.util.Arrays;/*** 使用当前类实例测试对象流的读写操作*/
public class Person implements Serializable {private String name;//姓名private int age;//年龄private String gender;//性别private String[] otherInfo;//其他信息public Person(String name, int age, String gender, String[] otherInfo) {this.name = name;this.age = age;this.gender = gender;this.otherInfo = otherInfo;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getGender() {return gender;}public void setGender(String gender) {this.gender = gender;}public String[] getOtherInfo() {return otherInfo;}public void setOtherInfo(String[] otherInfo) {this.otherInfo = otherInfo;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +", gender='" + gender + '\'' +", otherInfo=" + Arrays.toString(otherInfo) +'}';}
}
3.12字符流
- java将流按照读写单位划分为字节流与字符流.
- java.io.InputStream和OutputStream是所有字节流的超类
- 而java.io.Reader和Writer则是所有字符流的超类,它们和字节流的超类是平级关系.
- Reader和Writer是两个抽象类,里面规定了所有字符流都必须具备的读写字符的相关方法.
- 字符流最小读写单位为字符(char),但是底层实际还是读写字节,只是字符与字节的转换工作由字符流完成.
3.12.1 转换流
java.io.InputStreamReader和OutputStreamWriter
它们是字符流非常常用的一对实现类同时也是一对高级流,实际开发中我们不直接操作它们,但是它们在流连接中是非常重要的一环.
3.12.1 .1使用转换输出流向文件中写入文本数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W22yDdez-1649902337594)(image-20220207175957369.png)]
package io;import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;/*** 字符流* java将流按照读写单位划分为字节流与字符流.* java.io.InputStream和OutputStream是所有字节流的超类* 而java.io.Reader和Writer则是所有字符流的超类,它们是平级关系.** Reader和Writer是两个抽象类,里面规定了所有字符流都必须具备的读写字符的相关方法.* 字符流最小读写单位为字符(char),但是底层实际还是读写字节,只是字符与字节的转换工作由* 字符流完成.** 转换流* java.io.InputStreamReader和OutputStreamWriter* 它们是字符流非常常用的一对实现类同时也是一对高级流,实际开发中我们不直接操作它们,但是* 它们在流连接中是非常重要的一环.*/
public class OSWDemo {public static void main(String[] args) throws IOException {//向文件osw.txt中写入文字FileOutputStream fos = new FileOutputStream("osw.txt");OutputStreamWriter osw = new OutputStreamWriter(fos,"UTF-8");osw.write("我可以接受你的所有,所有小脾气.");osw.write("我可以带你去吃很多,很多好东西.");System.out.println("写出完毕!");osw.close();}
}
3.12.1 .2使用转换输入流读取文本文件
package io;import java.io.*;/*** 转换字符输入流* 可以将读取的字节按照指定的字符集转换为字符*/
public class ISRDemo {public static void main(String[] args) throws IOException {//将osw.txt文件中的所有文字读取回来.FileInputStream fis = new FileInputStream("osw.txt");InputStreamReader isr = new InputStreamReader(fis,"UTF-8");/*字符流读一个字符的read方法定义:int read()读取一个字符,返回的int值实际上表示的是一个char(低16位有效).如果返回的int值表示的是-1则说明EOF*///测试读取文件中第一个字
// int d = isr.read();
// char c = (char)d;
// System.out.println(c);//循环将文件所有字符读取回来int d;while((d = isr.read()) != -1){System.out.print((char)d);}isr.close();}
}
3.12.1 .3转换流的意义:
实际开发中我们还有功能更好用的字符高级流.但是其他的字符高级流都有一个共通点:不能直接连接在字节流上.而实际操作设备的流都是低级流同时也都是字节流.因此不能直接在流连接中串联起来.转换流是一对可以连接在字节流上的字符流,其他的高级字符流可以连接在转换流上.在流连接中起到"转换器"的作用(负责字符与字节的实际转换)
3.12.2缓冲字符流
3.12.2.1缓冲字符输出流:java.io.PrintWriter
java.io.BufferedWriter和BufferedReader
缓冲字符流内部也有一个缓冲区,读写文本数据以块读写形式加快效率.并且缓冲流有一个特别的功能:可以按行读写文本数据.
java.io.PrintWriter具有自动行刷新的缓冲字符输出流,实际开发中更常用.它内部总是会自动连接BufferedWriter作为块写加速使用.
package io;import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;/*** 缓冲字符流* 缓冲字符流是一对高级流,在流连接中的作用是提高读写文本数据的效率,并且* 可以安行读写字符串.* java.io.BufferedReader和BufferedWriter** 实际开发中缓冲字符输出流我们更常用的是PrintWriter,具有自动行刷新功能* 的缓冲字符输出流,其内部总是连接BufferedWriter作为缓冲加速使用.*/
public class PWDemo1 {public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {/*PrintWriter提供了对文件操作的构造方法:PrintWriter(String path)PrintWriter(File file)*///向文件中写入字符串PrintWriter pw = new PrintWriter("pw.txt","UTF-8");pw.println("我看过沙漠下暴雨");pw.println("看过大海亲吻鲨鱼");pw.println("看过黄昏追逐黎明");pw.println("没看过你");System.out.println("写出完毕!");pw.close();}
}
3.12.2.2在流链接中使用PW
package io;import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;/*** 在流连接中使用PW*/
public class PWDemo2 {public static void main(String[] args) throws FileNotFoundException {//文件字节输出流(是一个低级流),向文件中写入字节数据FileOutputStream fos = new FileOutputStream("pw2.txt",true);//转换输出流(是一个高级流,且是一个字符流)。1:衔接字符与字节流 2:将写出的字符转换为字节OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);//缓冲输出流(是一个高级流,且是一个字符流)。块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//具有自动行刷新的缓冲字符输出流PrintWriter pw = new PrintWriter(bw);//完成简易记事本。控制台输入的每行字符串都按行写入文件。单独输入exit时退出。Scanner scanner = new Scanner(System.in);while(true){String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);}pw.close();}
}
3.12.2.3PrintWriter的自动行刷新功能
如果实例化PW时第一个参数传入的是一个流,则此时可以再传入一个boolean型的参数,此值为true时就打开了自动行刷新功能。
即:
每当我们用PW的println方法写出一行字符串后会自动flush.
package io;import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;/*** 在流连接中使用PW*/
public class PWDemo2 {public static void main(String[] args) throws FileNotFoundException {//文件字节输出流(是一个低级流),向文件中写入字节数据FileOutputStream fos = new FileOutputStream("pw2.txt",true);//转换输出流(是一个高级流,且是一个字符流)。1:衔接字符与字节流 2:将写出的字符转换为字节OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);//缓冲输出流(是一个高级流,且是一个字符流)。块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//具有自动行刷新的缓冲字符输出流/*PrintWriter提供的构造器中,当第一个参数为一个流时,就支持再传入一个boolean型的参数表示是否自动行刷新。当该值为true时则打开了自动行刷新功能。这意味着每当我们调用println方法后会自动flush一次。*/PrintWriter pw = new PrintWriter(bw,true);//完成简易记事本。控制台输入的每行字符串都按行写入文件。单独输入exit时退出。Scanner scanner = new Scanner(System.in);while(true){String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);}pw.close();}
}
3.12.2.4缓冲字符输入流:java.io.BufferedReader
是一个高级的字符流,特点是块读文本数据,并且可以按行读取字符串。
package io;import java.io.*;/*** 使用java.io.BufferedReader按行读取文本数据*/
public class BRDemo {public static void main(String[] args) throws IOException {//将当前源程序读取出来并输出到控制台上FileInputStream fis = new FileInputStream("./src/io/BRDemo.java");InputStreamReader isr = new InputStreamReader(fis);BufferedReader br = new BufferedReader(isr);String line;/*BufferedReader提供了一个读取一行字符串的方法:String readLine()该方法会返回一行字符串,返回的字符串不含有最后的换行符。当某一行是空行时(该行内容只有一个换行符)则返回值为空字符串。如果流读取到了末尾,则返回值为null。*/while((line = br.readLine()) != null) {System.out.println(line);}br.close();}
}
4.异常处理
4.1java异常处理机制
- java中所有错误的超类为:Throwable。其下有两个子类:Error和Exception
- Error的子类描述的都是系统错误,比如虚拟机内存溢出等。
- Exception的子类描述的都是程序错误,比如空指针,下表越界等。
- 通常我们程序中处理的异常都是Exception。
4.2异常处理机制中的try-catch
package exception;/*** 异常处理机制中的try-catch* 语法:* try{* 可能出现异常的代码片段* }catch(XXXException e){* try中出现XXXException后的处理代码* }** try语句块不能独立存在,后面必须跟catch语句块或finally语句块*/
public class TryCatchDemo {public static void main(String[] args) {System.out.println("程序开始了");try {
// String line = null;
// String line = "";String line = "abc";//当JVM执行程序出现了某个异常时就会实例化这个异常并将其抛出//如果该异常没有被异常处理机制控制,则JVM会将异常隐式抛出当方法外(这里是main方法外)System.out.println(line.length());System.out.println(line.charAt(0));System.out.println(Integer.parseInt(line));//若try语句块中某句话出错了,则剩下的代码都不会执行!System.out.println("!!!!!!!!!!!!!!!!");// }catch(NullPointerException e){
// System.out.println("出现了空指针!");
// //catch可以定义多个,当try中不同的异常有不同处理办法时可分开捕获并处理
// }catch(StringIndexOutOfBoundsException e){
// System.out.println("出现了下标越界!");//若某些异常的处理方式相同时,可以合并在一个catch来处理}catch(NullPointerException|StringIndexOutOfBoundsException e){System.out.println("出现了空指针或下标越界并处理了!");//可以在下面catch超类异常来捕获并处理这一类异常。}catch(Exception e){System.out.println("反正就是出了个错");}System.out.println("程序结束了");}
}
4.3异常处理机制中的finally
-
finally块定义在异常处理机制中的最后一块。它可以直接跟在try之后,或者最后一个catch之后。
-
finally可以保证只要程序执行到了try语句块中,无论try语句块中的代码是否出现异常,最终finally都必定执行。
-
finally通常用来做释放资源这类操作。
package exception;/*** 异常处理机制中的finally块* finally块定义在异常处理机制中的最后一块。它可以直接跟在try之后,或者最后一个catch之后。** finally可以保证只要程序执行到了try语句块中,无论try语句块中的代码是否出现异常,最终* finally都必定执行。** finally通常用来做释放资源这类操作。*/
public class FinallyDemo {public static void main(String[] args) {System.out.println("程序开始了...");try{String line = "abc";
// String line = null;System.out.println(line.length());return;}catch(Exception e){System.out.println("出错了!");}finally{System.out.println("finally中的代码执行了!");}System.out.println("程序结束了!");}
}
4.4IO操作时的异常处理机制应用
package exception;import java.io.FileOutputStream;
import java.io.IOException;/*** IO操作时的异常处理机制应用*/
public class FinallyDemo2 {public static void main(String[] args) {FileOutputStream fos = null;try {fos = new FileOutputStream("fos.dat");fos.write(1);} catch (IOException e) {e.printStackTrace();//向控制台输出当前异常的错误信息} finally {try {if (fos!=null) {fos.close();}} catch (IOException e) {e.printStackTrace();}}}
}
4.5自动关闭特性
JDK7之后,java提供了一个新的特性:自动关闭。旨在IO操作中可以更简洁的使用异常处理机制完成最后的close操作。
语法:
try(定义需要在finally中调用close()方法关闭的对象.
){IO操作
}catch(XXXException e){...
}
上述语法中可在try的"()"中定义的并初始化的对象必须实现了java.io.AutoCloseable接口,否则编译不通过.
public class AutocloseableDemo {public static void main(String[] args) {try(FileOutputStream fos = new FileOutputStream("fos.dat");){fos.write(1);} catch (IOException e) {e.printStackTrace();//向控制台输出当前异常的错误信息}}
}
上述代码是编译器认可的,而不是虚拟机。编译器在编译上述代码后会在编译后的class文件中改回成FinallyDemo2案例的代码样子(上次课最后的案例)。
4.6throw关键字
throw用来对外主动抛出一个异常,通常下面两种情况我们主动对外抛出异常:
- 1:当程序遇到一个满足语法,但是不满足业务要求时,可以抛出一个异常告知调用者。
- 2:程序执行遇到一个异常,但是该异常不应当在当前代码片段被解决时可以抛出给调用者。
package exception;/*** 测试异常的抛出*/
public class Person {private int age;public int getAge() {return age;}public void setAge(int age) throws Exception {if(age<0||age>100){//使用throw对外抛出一个异常throw new RuntimeException("年龄不合法!");}this.age = age;}
}
package exception;/*** throw关键字,用来对外主动抛出一个异常。* 通常下面两种情况我们主动对外抛出异常:* 1:当程序遇到一个满足语法,但是不满足业务要求时,可以抛出一个异常告知调用者。* 2:程序执行遇到一个异常,但是该异常不应当在当前代码片段被解决时可以抛出给调用者。*/
public class ThrowDemo {public static void main(String[] args) {Person p = new Person();p.setAge(10000);//符合语法,但是不符合业务逻辑要求。System.out.println("此人年龄:"+p.getAge());}
}
4.7throws关键字
当一个方法中使用throw抛出一个非RuntimeException的异常时,就要在该方法上使用throws声明这个异常的抛出。此时调用该方法的代码就必须处理这个异常,否则编译不通过。
package exception;/*** 测试异常的抛出*/
public class Person {private int age;public int getAge() {return age;}/*** 当一个方法使用throws声明异常抛出时,调用此方法的代码片段就必须处理这个异常*/public void setAge(int age) throws Exception {if(age<0||age>100){//使用throw对外抛出一个异常
// throw new RuntimeException("年龄不合法!");//除了RuntimeException之外,抛出什么异常就要在方法上声明throws什么异常throw new Exception("年龄不合法!");}this.age = age;}
}
当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须处理这个异常,否则编译不通过。 处理手段有两种:
- 使用try-catch捕获并处理这个异常
- 在当前方法(本案例就是main方法)上继续使用throws声明该异常的抛出给调用者解决。 具体选取那种取决于异常处理的责任问题。
package exception;/*** throw关键字,用于主动对外抛出一个异常*/
public class ThrowDemo {public static void main(String[] args){System.out.println("程序开始了...");try {Person p = new Person();/*当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须添加处理异常的手段,否则编译不通过.而处理手段有两种1:使用try-catch捕获并处理异常2:在当前方法上继续使用throws声明该异常的抛出具体用哪种取决于异常处理的责任问题*/p.setAge(100000);//典型的符合语法,但是不符合业务逻辑要求System.out.println("此人年龄:"+p.getAge()+"岁");} catch (Exception e) {e.printStackTrace();}System.out.println("程序结束了...");}
}
注意,永远不应当在main方法上使用throws!!
含有throws的方法被子类重写时的规则
package exception;import java.awt.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;/*** 子类重写超类含有throws声明异常抛出的方法时对throws的几种特殊的重写规则*/
public class ThrowsDemo {public void dosome()throws IOException, AWTException {}
}
class SubClass extends ThrowsDemo{
// public void dosome()throws IOException, AWTException {}//可以不再抛出任何异常
// public void dosome(){}//可以仅抛出部分异常
// public void dosome()throws IOException {}//可以抛出超类方法抛出异常的子类型异常
// public void dosome()throws FileNotFoundException {}//不允许抛出额外异常(超类方法中没有的,并且没有继承关系的异常)
// public void dosome()throws SQLException {}//不可以抛出超类方法抛出异常的超类型异常
// public void dosome()throws Exception {}
}
4.8 Java异常可以分为可检测异常,非检测异常:
- 可检测异常:可检测异常经编译器验证,对于声明抛出异常的任何方法,编译器将强制执行处理或声明规则,不捕捉这个异常,编译器就通不过,不允许编译
- 非检测异常:非检测异常不遵循处理或者声明规则。在产生此类异常时,不一定非要采取任何适当操作,编译器不会检查是否已经解决了这样一个异常
- RuntimeException 类属于非检测异常,因为普通JVM操作引起的运行时异常随时可能发生,此类异常一般是由特定操作引发。但这些操作在java应用程序中会频繁出现。因此它们不受编译器检查与处理或声明规则的限制。
4.9常见的RuntimeException子类
- IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数
- NullPointerException:当应用程序试图在需要对象的地方使用 null 时,抛出该异常
- ArrayIndexOutOfBoundsException:当使用的数组下标超出数组允许范围时,抛出该异常
- ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常
- NumberFormatException:当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
4.10异常中常用的方法
package exception;/*** 异常常见的方法*/
public class ExceptionApiDemo {public static void main(String[] args) {System.out.println("程序开始了");try {String str = "abc";System.out.println(Integer.parseInt(str));} catch (NumberFormatException e) {//异常最常用的方法,用于将当前错误信息输出到控制台e.printStackTrace();//获取错误消息.记录日志的时候或提示给用户可以使用它String message = e.getMessage();System.out.println(message);}System.out.println("程序结束了");}
}
4.11自定义异常
自定义异常通常用来定义那些业务上的异常问题。
定义自定义异常需要注意以下问题:
- 异常的类名要做到见名知义
- 需要是Exception的子类
- 提供超类异常提供的所有种类构造器
package exception;/*** 非法的年龄异常** 自定义异常通常用来说明业务上的错误.* 自定义异常要注意以下问题:* 1:定义的类名要做到见名知义* 2:必须是Exception的子类* 3:提供Exception所定义的所有构造方法*/
public class IllegalAgeException extends Exception{public IllegalAgeException() {}public IllegalAgeException(String message) {super(message);}public IllegalAgeException(String message, Throwable cause) {super(message, cause);}public IllegalAgeException(Throwable cause) {super(cause);}public IllegalAgeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}
}
package exception;/*** 测试异常的抛出*/
public class Person {private int age;public int getAge() {return age;}/*** 当一个方法使用throws声明异常抛出时,调用此方法的代码片段就必须处理这个异常*/public void setAge(int age) throws IllegalAgeException {if(age<0||age>100){//使用throw对外抛出一个异常
// throw new RuntimeException("年龄不合法!");//除了RuntimeException之外,抛出什么异常就要在方法上声明throws什么异常
// throw new Exception("年龄不合法!");//抛出自定义异常throw new IllegalAgeException("年龄超范围:"+age);}this.age = age;}
}
package exception;/*** throw关键字,用于主动对外抛出一个异常*/
public class ThrowDemo {public static void main(String[] args){System.out.println("程序开始了...");try {Person p = new Person();/*当我们调用一个含有throws声明异常抛出的方法时,编译器要求我们必须添加处理异常的手段,否则编译不通过.而处理手段有两种1:使用try-catch捕获并处理异常2:在当前方法上继续使用throws声明该异常的抛出具体用哪种取决于异常处理的责任问题*/p.setAge(100000);//典型的符合语法,但是不符合业务逻辑要求System.out.println("此人年龄:"+p.getAge()+"岁");} catch (IllegalAgeException e) {e.printStackTrace();}System.out.println("程序结束了...");}
}
4.12总结:
异常处理机制是用来处理那些可能存在的异常,但是无法通过修改逻辑完全规避的场景。
而如果通过修改逻辑可以规避的异常是bug,不应当用异常处理机制在运行期间解决!应当在编码时及时修正
5.java网络编程
5.1java.net.Socket
Socket(套接字)封装了TCP协议的通讯细节,是的我们使用它可以与服务端建立网络链接,并通过 它获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互
5.2java.net.ServerSocket
ServerSocket运行在服务端,作用有两个:
1:向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
2:监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。
如果我们把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。
与服务端建立连接案例:
package socket;import java.io.IOException;
import java.net.Socket;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){}public static void main(String[] args) {Client client = new Client();client.start();}
}
package socket;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket* 就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}
客户端与服务端完成第一次通讯(发送一行字符串)
Socket提供了两个重要的方法:
OutputStream getOutputStream()
该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
InputStream getInputStream()
通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SRRfdgUz-1649902563714)(image-20220209173620702.png)]
客户端代码:
package socket;import java.io.*;
import java.net.Socket;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){try {/*Socket提供了一个方法:OutputStream getOutputStream()该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。*///低级流,将字节通过网络发送给对方OutputStream out = socket.getOutputStream();//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");//高级流,负责块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//高级流,负责按行写出字符串,自动行刷新PrintWriter pw = new PrintWriter(bw,true);pw.println("你好服务端!");} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Client client = new Client();client.start();}
}
服务端代码:
package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket* 就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in,"UTF-8");BufferedReader br = new BufferedReader(isr);String message = br.readLine();System.out.println("客户端说:"+message);} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}
实现客户端循环发消息给服务端
客户端代码:
package socket;import java.io.*;
import java.net.Socket;
import java.util.Scanner;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){try {/*Socket提供了一个方法:OutputStream getOutputStream()该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。*///低级流,将字节通过网络发送给对方OutputStream out = socket.getOutputStream();//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");//高级流,负责块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//高级流,负责按行写出字符串,自动行刷新PrintWriter pw = new PrintWriter(bw,true);Scanner scanner = new Scanner(System.in);while(true) {String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);}} catch (IOException e) {e.printStackTrace();} finally {try {/*通讯完毕后调用socket的close方法。该方法会给对方发送断开信号。*/socket.close();} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {Client client = new Client();client.start();}
}
服务端代码:
package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket* 就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in,"UTF-8");BufferedReader br = new BufferedReader(isr);String message = null;while((message = br.readLine())!=null) {System.out.println("客户端说:" + message);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}
需要注意的几个点:
1:当客户端不再与服务端通讯时,需要调用socket.close()断开链接,此时会发送断开链接的信号给服务端。这时服务端的br.readLine()方法会返回null,表示客户端断开了链接。
2:当客户端链接后不输入信息发送给服务端时,服务端的br.readLine()方法是出于阻塞状态的,直到读取了一行来自客户端发送的字符串。
多客户端链接
之前只有第一个连接的客户端可以与服务端说话。
原因:
服务端只调用过一次accept方法,因此只有第一个客户端链接时服务端接受了链接并返回了Socket,此时可以与其交互。
而第二个客户端建立链接时,由于服务端没有再次调用accept,因此无法与其交互。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MU8El2ar-1649902563717)(image-20210430094142755.png)]
package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket* 就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);String message = null;while ((message = br.readLine()) != null) {System.out.println("客户端说:" + message);}}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}
}
添加循环操作后,发现依然无法实现。
原因在于:
外层的while循环里面嵌套了一个内层循环(循环读取客户端发送消息),而循环执行机制决定了里层循环不结束,外层循环则无法进入第二次操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AvGtSoLg-1649902563718)(image-20210430101705542.png)]
6.多线程
线程:一个顺序的单一的程序执行流程就是一个线程。代码一句一句的有先后顺序的执行。
多线程:多个单一顺序执行的流程并发运行。造成"感官上同时运行"的效果。
6.1并发:
多个线程实际运行是走走停停的。线程调度程序会将CPU运行时间划分为若干个时间片段并
尽可能均匀的分配给每个线程,拿到时间片的线程被CPU执行这段时间。当超时后线程调度
程序会再次分配一个时间片段给一个线程使得CPU执行它。如此反复。由于CPU执行时间在
纳秒级别,我们感觉不到切换线程运行的过程。所以微观上走走停停,宏观上感觉一起运行
的现象成为并发运行!
用途:
- 当出现多个代码片段执行顺序有冲突时,希望它们各干各的时就应当放在不同线程上"同时"运行
- 一个线程可以运行,但是多个线程可以更快时,可以使用多线程运行
线程的生命周期图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hzLt3jBd-1649902563719)(image-20210401171503425.png)]
6.2创建线程有两种方式
方式一:继承Thread并重写run方法
定义一个线程类,重写run方法,在其中定义线程要执行的任务(希望和其他线程并发执行的任务)。
注:启动该线程要调用该线程的start方法,而不是run方法!!!
package thread;/*** 多线程* 线程:程序中一个单一的顺序执行流程* 多线程:多个单一顺序执行流程"同时"执行** 多线程改变了代码的执行方式,从原来的单一顺序执行流程变为多个执行流程"同时"执行。* 可以让多个代码片段的执行互不打扰。** 线程之间是并发执行的,并非真正意义上的同时运行。* 常见线程有两种方式:* 1:继承Thread并重写run方法**/
public class ThreadDemo1 {public static void main(String[] args) {//创建两个线程Thread t1 = new MyThread1();Thread t2 = new MyThread2();/*启动线程,注意:不要调用run方法!!线程调用完start方法后会纳入到系统的线程调度器程序中被统一管理。线程调度器会分配时间片段给线程,使得CPU执行该线程这段时间,用完后线程调度器会再分配一个时间片段给一个线程,如此反复,使得多个线程都有机会执行一会,做到走走停停,并发运行。线程第一次被分配到时间后会执行它的run方法开始工作。*/t1.start();t2.start();}
}
/*** 第一种创建线程的优点:* 结构简单,利于匿名内部类形式创建。** 缺点:* 1:由于java是单继承的,这会导致继承了Thread就无法再继承其他类去复用方法* 2:定义线程的同时重写了run方法,这等于将线程的任务定义在了这个线程中导致* 线程只能干这件事。重(chong)用性很低。*/
class MyThread1 extends Thread{public void run(){for (int i=0;i<1000;i++){System.out.println("hello姐~");}}
}
class MyThread2 extends Thread{public void run(){for (int i=0;i<1000;i++){System.out.println("来了~老弟!");}}
}
第一种创建线程的方式
优点:
在于结构简单,便于匿名内部类形式创建。
缺点:
- 1:直接继承线程,会导致不能在继承其他类去复用方法,这在实际开发中是非常不便的。
- 2:定义线程的同时重写了run方法,会导致线程与线程任务绑定在了一起,不利于线程的重用。
方式二:实现Runnable接口单独定义线程任务
package thread;/*** 第二种创建线程的方式* 实现Runnable接口单独定义线程任务*/
public class ThreadDemo2 {public static void main(String[] args) {//实例化任务Runnable r1 = new MyRunnable1();Runnable r2 = new MyRunnable2();//创建线程并指派任务Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start();}
}
class MyRunnable1 implements Runnable{public void run() {for (int i=0;i<1000;i++){System.out.println("你是谁啊?");}}
}
class MyRunnable2 implements Runnable{public void run() {for (int i=0;i<1000;i++){System.out.println("开门!查水表的!");}}
}
6.3匿名内部类形式的线程创建
package thread;/*** 使用匿名内部类完成线程的两种创建*/
public class ThreadDemo3 {public static void main(String[] args) {Thread t1 = new Thread(){public void run(){for(int i=0;i<1000;i++){System.out.println("你是谁啊?");}}};
// Runnable r2 = new Runnable() {
// public void run() {
// for(int i=0;i<1000;i++){
// System.out.println("我是查水表的!");
// }
// }
// };//Runnable可以使用lambda表达式创建Runnable r2 = ()->{for(int i=0;i<1000;i++){System.out.println("我是查水表的!");}};Thread t2 = new Thread(r2);t1.start();t2.start();}
}
java中的代码都是靠线程运行的,执行main方法的线程称为"主线程"。
线程提供了一个方法:
-
static Thread currentThread()
该方法可以获取运行这个方法的线程
package thread;/*** java中所有的代码都是靠线程执行的,main方法也不例外。JVM启动后会创建一条线程来执行main* 方法,该线程的名字叫做"main",所以通常称它为"主线程"。* 我们自己定义的线程在不指定名字的情况下系统会分配一个名字,格式为"thread-x"(x是一个数)。** Thread提供了一个静态方法:* static Thread currentThread()* 获取执行该方法的线程。**/
public class CurrentThreadDemo {public static void main(String[] args) {/*后期会学习到一个很重要的API:ThreadLocal,它可以使得我们在一个线程上跨越多个方法时共享数据使用,其内部要用到currentThread方法来辨别线程。如spring的事物控制就是靠ThreadLocal实现的。*/Thread main = Thread.currentThread();//获取执行main方法的线程(主线程)System.out.println("线程:"+main);dosome();//主线程执行dosome方法}public static void dosome(){Thread t = Thread.currentThread();//获取执行dosome方法的线程System.out.println("执行dosome方法的线程是:"+t);}
}
6.4使用多线程实现多客户端连接服务端
package socket;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket* 就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;public ClientHandler(Socket socket){this.socket = socket;}public void run(){try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);String message = null;while ((message = br.readLine()) != null) {System.out.println("客户端说:" + message);}}catch(IOException e){e.printStackTrace();}}}}
6.5线程API
获取线程相关信息的方法
package thread;/*** 获取线程相关信息的一组方法*/
public class ThreadInfoDemo {public static void main(String[] args) {Thread main = Thread.currentThread();//获取主线程String name = main.getName();//获取线程的名字System.out.println("名字:"+name);long id = main.getId();//获取该线程的唯一标识System.out.println("id:"+id);int priority = main.getPriority();//获取该线程的优先级System.out.println("优先级:"+priority);boolean isAlive = main.isAlive();//该线程是否活着System.out.println("是否活着:"+isAlive);boolean isDaemon = main.isDaemon();//是否为守护线程System.out.println("是否为守护线程:"+isDaemon);boolean isInterrupted = main.isInterrupted();//是否被中断了System.out.println("是否被中断了:"+isInterrupted);}
}
线程优先级
线程start后会纳入到线程调度器中统一管理,线程只能被动的被分配时间片并发运行,而无法主动索取时间片.线程调度器尽可能均匀的将时间片分配给每个线程.
线程有10个优先级,使用整数1-10表示
- 1为最小优先级,10为最高优先级.5为默认值
- 调整线程的优先级可以最大程度的干涉获取时间片的几率.优先级越高的线程获取时间片的次数越多,反之则越少.
package thread;public class PriorityDemo {public static void main(String[] args) {Thread max = new Thread(){public void run(){for(int i=0;i<10000;i++){System.out.println("max");}}};Thread min = new Thread(){public void run(){for(int i=0;i<10000;i++){System.out.println("min");}}};Thread norm = new Thread(){public void run(){for(int i=0;i<10000;i++){System.out.println("nor");}}};min.setPriority(Thread.MIN_PRIORITY);max.setPriority(Thread.MAX_PRIORITY);min.start();norm.start();max.start();}
}
sleep阻塞
线程提供了一个静态方法:
- static void sleep(long ms)
- 使运行该方法的线程进入阻塞状态指定的毫秒,超时后线程会自动回到RUNNABLE状态等待再次获取时间片并发运行.
package thread;public class SleepDemo {public static void main(String[] args) {System.out.println("程序开始了!");try {Thread.sleep(5000);//主线程阻塞5秒钟} catch (InterruptedException e) {e.printStackTrace();}System.out.println("程序结束了!");}
}
线程API
sleep阻塞(续)
sleep方法处理异常:InterruptedException.
当一个线程调用sleep方法处于睡眠阻塞的过程中,该线程的interrupt()方法被调用时,sleep方法会抛出该异常从而打断睡眠阻塞.
package thread;/*** sleep方法要求必须处理中断异常:InterruptedException* 当一个线程调用sleep方法处于睡眠阻塞的过程中,它的interrupt()方法被调用时* 会中断该阻塞,此时sleep方法会抛出该异常。*/
public class SleepDemo2 {public static void main(String[] args) {Thread lin = new Thread(){public void run(){System.out.println("林:刚美完容,睡一会吧~");try {Thread.sleep(9999999);} catch (InterruptedException e) {System.out.println("林:干嘛呢!干嘛呢!干嘛呢!都破了像了!");}System.out.println("林:醒了");}};Thread huang = new Thread(){public void run(){System.out.println("黄:大锤80!小锤40!开始砸墙!");for(int i=0;i<5;i++){System.out.println("黄:80!");try {Thread.sleep(1000);} catch (InterruptedException e) {}}System.out.println("咣当!");System.out.println("黄:大哥,搞定!");lin.interrupt();//中断lin的睡眠阻塞}};lin.start();huang.start();}
}
守护线程
守护线程也称为:后台线程
- 守护线程是通过普通线程调用setDaemon(boolean on)方法设置而来的,因此创建上与普通线程无异.
- 守护线程的结束时机上有一点与普通线程不同,即:进程的结束.
- 进程结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程.
package thread;/*** 守护线程* 守护线程是通过普通线程调用setDaemon(true)设置而转变的。因此守护线程创建上* 与普通线程无异。* 但是结束时机上有一点不同:进程结束。* 当一个java进程中的所有普通线程都结束时,该进程就会结束,此时会强制杀死所有正在* 运行的守护线程。*/
public class DaemonThreadDemo {public static void main(String[] args) {Thread rose = new Thread(){public void run(){for(int i=0;i<5;i++){System.out.println("rose:let me go!");try {Thread.sleep(1000);} catch (InterruptedException e) {}}System.out.println("rose:啊啊啊啊啊啊AAAAAAAaaaaa....");System.out.println("噗通");}};Thread jack = new Thread(){public void run(){while(true){System.out.println("jack:you jump!i jump!");try {Thread.sleep(1000);} catch (InterruptedException e) {}}}};rose.start();jack.setDaemon(true);//设置守护线程必须在线程启动前进行jack.start();}
}
通常当我们不关心某个线程的任务什么时候停下来,它可以一直运行,但是程序主要的工作都结束时它应当跟着结束时,这样的任务就适合放在守护线程上执行.比如GC就是在守护线程上运行的.
6.6多线程并发安全问题
当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪.
临界资源:操作该资源的全过程同时只能被单个线程完成.
package thread;/*** 多线程并发安全问题* 当多个线程并发操作同一临界资源,由于线程切换的时机不确定,导致操作顺序出现* 混乱,严重时可能导致系统瘫痪。* 临界资源:同时只能被单一线程访问操作过程的资源。*/
public class SyncDemo {public static void main(String[] args) {Table table = new Table();Thread t1 = new Thread(){public void run(){while(true){int bean = table.getBean();Thread.yield();System.out.println(getName()+":"+bean);}}};Thread t2 = new Thread(){public void run(){while(true){int bean = table.getBean();/*static void yield()线程提供的这个静态方法作用是让执行该方法的线程主动放弃本次时间片。这里使用它的目的是模拟执行到这里CPU没有时间了,发生线程切换,来看并发安全问题的产生。*/Thread.yield();System.out.println(getName()+":"+bean);}}};t1.start();t2.start();}
}class Table{private int beans = 20;//桌子上有20个豆子public int getBean(){if(beans==0){throw new RuntimeException("没有豆子了!");}Thread.yield();return beans--;}
}
synchronized关键字
synchronized有两种使用方式
- 在方法上修饰,此时该方法变为一个同步方法
- 同步块,可以更准确的锁定需要排队的代码片段
同步方法
当一个方法使用synchronized修饰后,这个方法称为"同步方法",即:多个线程不能同时 在方法内部执行.只能有先后顺序的一个一个进行. 将并发操作同一临界资源的过程改为同步执行就可以有效的解决并发安全问题.
package thread;/*** 多线程并发安全问题* 当多个线程并发操作同一临界资源,由于线程切换的时机不确定,导致操作顺序出现* 混乱,严重时可能导致系统瘫痪。* 临界资源:同时只能被单一线程访问操作过程的资源。*/
public class SyncDemo {public static void main(String[] args) {Table table = new Table();Thread t1 = new Thread(){public void run(){while(true){int bean = table.getBean();Thread.yield();System.out.println(getName()+":"+bean);}}};Thread t2 = new Thread(){public void run(){while(true){int bean = table.getBean();/*static void yield()线程提供的这个静态方法作用是让执行该方法的线程主动放弃本次时间片。这里使用它的目的是模拟执行到这里CPU没有时间了,发生线程切换,来看并发安全问题的产生。*/Thread.yield();System.out.println(getName()+":"+bean);}}};t1.start();t2.start();}
}class Table{private int beans = 20;//桌子上有20个豆子/*** 当一个方法使用synchronized修饰后,这个方法称为同步方法,多个线程不能* 同时执行该方法。* 将多个线程并发操作临界资源的过程改为同步操作就可以有效的解决多线程并发* 安全问题。* 相当于让多个线程从原来的抢着操作改为排队操作。*/public synchronized int getBean(){if(beans==0){throw new RuntimeException("没有豆子了!");}Thread.yield();return beans--;}
}
同步块
有效的缩小同步范围可以在保证并发安全的前提下尽可能的提高并发效率.同步块可以更准确的控制需要多个线程排队执行的代码片段.
语法:
synchronized(同步监视器对象){需要多线程同步执行的代码片段
}
同步监视器对象即上锁的对象,要想保证同步块中的代码被多个线程同步运行,则要求多个线程看到的同步监视器对象是同一个.
package thread;/*** 有效的缩小同步范围可以在保证并发安全的前提下尽可能提高并发效率。** 同步块* 语法:* synchronized(同步监视器对象){* 需要多个线程同步执行的代码片段* }* 同步块可以更准确的锁定需要多个线程同步执行的代码片段来有效缩小排队范围。*/
public class SyncDemo2 {public static void main(String[] args) {Shop shop = new Shop();Thread t1 = new Thread(){public void run(){shop.buy();}};Thread t2 = new Thread(){public void run(){shop.buy();}};t1.start();t2.start();}
}class Shop{public void buy(){/*在方法上使用synchronized,那么同步监视器对象就是this。*/
// public synchronized void buy(){Thread t = Thread.currentThread();//获取运行该方法的线程try {System.out.println(t.getName()+":正在挑衣服...");Thread.sleep(5000);/*使用同步块需要指定同步监视器对象,即:上锁的对象这个对象可以是java中任何引用类型的实例,只要保证多个需要排队执行该同步块中代码的线程看到的该对象是"同一个"即可*/synchronized (this) {
// synchronized (new Object()) {//没有效果!System.out.println(t.getName() + ":正在试衣服...");Thread.sleep(5000);}System.out.println(t.getName()+":结账离开");} catch (InterruptedException e) {e.printStackTrace();}}
}
在静态方法上使用synchronized
当在静态方法上使用synchronized后,该方法是一个同步方法.由于静态方法所属类,所以一定具有同步效果.
静态方法使用的同步监视器对象为当前类的类对象(Class的实例).
注:类对象会在后期反射知识点介绍.
package thread;/*** 静态方法上如果使用synchronized,则该方法一定具有同步效果。*/
public class SyncDemo3 {public static void main(String[] args) {Thread t1 = new Thread(){public void run(){Boo.dosome();}};Thread t2 = new Thread(){public void run(){Boo.dosome();}};t1.start();t2.start();}
}
class Boo{/*** synchronized在静态方法上使用是,指定的同步监视器对象为当前类的类对象。* 即:Class实例。* 在JVM中,每个被加载的类都有且只有一个Class的实例与之对应,后面讲反射* 知识点的时候会介绍类对象。*/public synchronized static void dosome(){Thread t = Thread.currentThread();try {System.out.println(t.getName() + ":正在执行dosome方法...");Thread.sleep(5000);System.out.println(t.getName() + ":执行dosome方法完毕!");} catch (InterruptedException e) {e.printStackTrace();}}}
}
静态方法中使用同步块时,指定的锁对象通常也是当前类的类对象
class Boo{public static void dosome(){/*静态方法中使用同步块时,指定同步监视器对象通常还是用当前类的类对象获取方式为:类名.class*/synchronized (Boo.class) {Thread t = Thread.currentThread();try {System.out.println(t.getName() + ":正在执行dosome方法...");Thread.sleep(5000);System.out.println(t.getName() + ":执行dosome方法完毕!");} catch (InterruptedException e) {e.printStackTrace();}}}
}
互斥锁
当多个线程执行不同的代码片段,但是这些代码片段之间不能同时运行时就要设置为互斥的.
使用synchronized锁定多个代码片段,并且指定的同步监视器是同一个时,这些代码片段之间就是互斥的.
package thread;/*** 互斥锁* 当使用synchronized锁定多个不同的代码片段,并且指定的同步监视器对象相同时,* 这些代码片段之间就是互斥的,即:多个线程不能同时访问这些方法。*/
public class SyncDemo4 {public static void main(String[] args) {Foo foo = new Foo();Thread t1 = new Thread(){public void run(){foo.methodA();}};Thread t2 = new Thread(){public void run(){foo.methodB();}};t1.start();t2.start();}
}
class Foo{public synchronized void methodA(){Thread t = Thread.currentThread();try {System.out.println(t.getName()+":正在执行A方法...");Thread.sleep(5000);System.out.println(t.getName()+":执行A方法完毕!");} catch (InterruptedException e) {e.printStackTrace();}}public synchronized void methodB(){Thread t = Thread.currentThread();try {System.out.println(t.getName()+":正在执行B方法...");Thread.sleep(5000);System.out.println(t.getName()+":执行B方法完毕!");} catch (InterruptedException e) {e.printStackTrace();}}
}
死锁
死锁的产生:
两个线程各自持有一个锁对象的同时等待对方先释放锁对象,此时会出现僵持状态。这个现象就是死锁。
package thread;/*** 死锁* 死锁的产生:* 两个线程各自持有一个锁对象的同时等待对方先释放锁对象,此时会出现僵持状态。* 这个现象就是死锁。*/
public class DeadLockDemo {//定义两个锁对象,"筷子"和"勺"public static Object chopsticks = new Object();public static Object spoon = new Object();public static void main(String[] args) {Thread np = new Thread(){public void run(){System.out.println("北方人开始吃饭.");System.out.println("北方人去拿筷子...");synchronized (chopsticks){System.out.println("北方人拿起了筷子开始吃饭...");try {Thread.sleep(5000);} catch (InterruptedException e) {}System.out.println("北方人吃完了饭,去拿勺...");synchronized (spoon){System.out.println("北方人拿起了勺子开始喝汤...");try {Thread.sleep(5000);} catch (InterruptedException e) {}System.out.println("北方人喝完了汤");}System.out.println("北方人放下了勺");}System.out.println("北方人放下了筷子,吃饭完毕!");}};Thread sp = new Thread(){public void run(){System.out.println("南方人开始吃饭.");System.out.println("南方人去拿勺...");synchronized (spoon){System.out.println("南方人拿起了勺开始喝汤...");try {Thread.sleep(5000);} catch (InterruptedException e) {}System.out.println("南方人喝完了汤,去拿筷子...");synchronized (chopsticks){System.out.println("南方人拿起了筷子开始吃饭...");try {Thread.sleep(5000);} catch (InterruptedException e) {}System.out.println("南方人吃完了饭");}System.out.println("南方人放下了筷子");}System.out.println("南方人放下了勺,吃饭完毕!");}};np.start();sp.start();}
}
package thread;/*** 解决死锁:* 1:尽量避免在持有一个锁的同时去等待持有另一个锁(避免synchronized嵌套)* 2:当无法避免synchronized嵌套时,就必须保证多个线程锁对象的持有顺序必须一致。* 即:A线程在持有锁1的过程中去持有锁2时,B线程也要以这样的持有顺序进行。*/
public class DeadLockDemo2 {//筷子private static Object chopsticks = new Object();//勺private static Object spoon = new Object();public static void main(String[] args) {//北方人Thread np = new Thread(){public void run(){try {System.out.println("北方人:开始吃饭");System.out.println("北方人去拿筷子...");synchronized (chopsticks) {System.out.println("北方人拿起了筷子,开始吃饭...");Thread.sleep(5000);}System.out.println("北方人吃完了饭,放下了筷子");System.out.println("北方人去拿勺子...");synchronized (spoon){System.out.println("北方人拿起了勺子,开始喝汤...");Thread.sleep(5000);}System.out.println("北方人喝完了汤,北方人放下了勺子");System.out.println("吃饭完毕。");} catch (InterruptedException e) {e.printStackTrace();}}};//南方人Thread sp = new Thread(){public void run(){try {System.out.println("南方人:开始吃饭");System.out.println("南方人去拿勺...");synchronized (spoon) {System.out.println("南方人拿起了勺,开始喝汤...");Thread.sleep(5000);}System.out.println("南方人喝完了汤,放下勺子...");System.out.println("南方人去拿筷子...");synchronized (chopsticks){System.out.println("南方人拿起了筷子,开始吃饭...");Thread.sleep(5000);}System.out.println("南方人吃完了饭,南方人放下了筷子");System.out.println("吃饭完毕。");} catch (InterruptedException e) {e.printStackTrace();}}};np.start();sp.start();}
}
聊天室(续)
实现服务端发送消息给客户端
在服务端通过Socket获取输出流,客户端获取输入流,实现服务端将消息发送给客户端.
这里让服务端直接将客户端发送过来的消息再回复给客户端来进行测试.
服务端代码:
package socket;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket* 就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;private String host;//记录客户端的IP地址信息public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机地址信息host = socket.getInetAddress().getHostAddress();}public void run(){try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");BufferedWriter bw = new BufferedWriter(osw);PrintWriter pw = new PrintWriter(bw,true);String message = null;while ((message = br.readLine()) != null) {System.out.println(host + "说:" + message);//将消息回复给客户端pw.println(host + "说:" + message);}}catch(IOException e){e.printStackTrace();}}}}
客户端代码:
package socket;import java.io.*;
import java.net.Socket;
import java.util.Scanner;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){try {/*Socket提供了一个方法:OutputStream getOutputStream()该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。*///低级流,将字节通过网络发送给对方OutputStream out = socket.getOutputStream();//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");//高级流,负责块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//高级流,负责按行写出字符串,自动行刷新PrintWriter pw = new PrintWriter(bw,true);//通过socket获取输入流读取服务端发送过来的消息InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in,"UTF-8");BufferedReader br = new BufferedReader(isr);Scanner scanner = new Scanner(System.in);while(true) {String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);line = br.readLine();System.out.println(line);}} catch (IOException e) {e.printStackTrace();} finally {try {/*通讯完毕后调用socket的close方法。该方法会给对方发送断开信号。*/socket.close();} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {Client client = new Client();client.start();}
}
服务端转发消息给所有客户端
服务端代码:
package socket;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket* 就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*存放所有客户端输出流,用于广播消息*/private PrintWriter[] allOut = {};/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;private String host;//记录客户端的IP地址信息public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机地址信息host = socket.getInetAddress().getHostAddress();}public void run(){try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");BufferedWriter bw = new BufferedWriter(osw);PrintWriter pw = new PrintWriter(bw,true);//将该输出流存入共享数组allOut中//1对allOut数组扩容allOut = Arrays.copyOf(allOut,allOut.length+1);//2将输出流存入数组最后一个位置allOut[allOut.length-1] = pw;String message = null;while ((message = br.readLine()) != null) {System.out.println(host + "说:" + message);//将消息回复给所有客户端for(int i=0;i<allOut.length;i++) {allOut[i].println(host + "说:" + message);}}}catch(IOException e){e.printStackTrace();}}}}
客户端解决收发消息的冲突问题
由于客户端start方法中循环进行的操作顺序是先通过控制台输入一句话后将其发送给服务端,然后再读取服务端发送回来的一句话.这导致如果客户端不输入内容就无法收到服务端发送过来的其他信息(其他客户端的聊天内容).因此要将客户端中接收消息的工作移动到一个单独的线程上执行,才能保证收发消息互不打扰.
客户端代码:
package socket;import java.io.*;
import java.net.Socket;
import java.util.Scanner;/*** 聊天室客户端*/
public class Client {/*java.net.Socket 套接字Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成与远端计算机的数据交互工作。我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克风(输出流),通过它们就可以与对方交流了。*/private Socket socket;/*** 构造方法,用来初始化客户端*/public Client(){try {System.out.println("正在链接服务端...");/*实例化Socket时要传入两个参数参数1:服务端的地址信息可以是IP地址,如果链接本机可以写"localhost"参数2:服务端开启的服务端口我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上的服务端应用程序。实例化的过程就是链接的过程,如果链接失败会抛出异常:java.net.ConnectException: Connection refused: connect*/socket = new Socket("localhost",8088);System.out.println("与服务端建立链接!");} catch (IOException e) {e.printStackTrace();}}/*** 客户端开始工作的方法*/public void start(){try {//启动读取服务端发送过来消息的线程ServerHandler handler = new ServerHandler();Thread t = new Thread(handler);t.setDaemon(true);t.start();/*Socket提供了一个方法:OutputStream getOutputStream()该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。*///低级流,将字节通过网络发送给对方OutputStream out = socket.getOutputStream();//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");//高级流,负责块写文本数据加速BufferedWriter bw = new BufferedWriter(osw);//高级流,负责按行写出字符串,自动行刷新PrintWriter pw = new PrintWriter(bw,true);Scanner scanner = new Scanner(System.in);while(true) {String line = scanner.nextLine();if("exit".equalsIgnoreCase(line)){break;}pw.println(line);}} catch (IOException e) {e.printStackTrace();} finally {try {/*通讯完毕后调用socket的close方法。该方法会给对方发送断开信号。*/socket.close();} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {Client client = new Client();client.start();}/*** 该线程负责接收服务端发送过来的消息*/private class ServerHandler implements Runnable{public void run(){//通过socket获取输入流读取服务端发送过来的消息try {InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in,"UTF-8");BufferedReader br = new BufferedReader(isr);String line;//循环读取服务端发送过来的每一行字符串while((line = br.readLine())!=null){System.out.println(line);}} catch (IOException e) {e.printStackTrace();}}}
}
服务端完成处理客户端断开连接后的操作
当一个客户端断开连接后,服务端处理该客户端交互的线程ClientHandler应当将通过socket获取的输出流从共享数组allOut中删除,防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端.
服务端代码:
package socket;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket* 就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*存放所有客户端输出流,用于广播消息*/private PrintWriter[] allOut = {};/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;private String host;//记录客户端的IP地址信息public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机地址信息host = socket.getInetAddress().getHostAddress();}public void run(){PrintWriter pw = null;try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");BufferedWriter bw = new BufferedWriter(osw);pw = new PrintWriter(bw,true);//将该输出流存入共享数组allOut中//1对allOut数组扩容allOut = Arrays.copyOf(allOut, allOut.length + 1);//2将输出流存入数组最后一个位置allOut[allOut.length - 1] = pw;//通知所有客户端该用户上线了sendMessage(host + "上线了,当前在线人数:"+allOut.length);String message = null;while ((message = br.readLine()) != null) {System.out.println(host + "说:" + message);//将消息回复给所有客户端sendMessage(host + "说:" + message);}}catch(IOException e){e.printStackTrace();}finally{//处理客户端断开链接的操作//将当前客户端的输出流从allOut中删除(数组缩容)for(int i=0;i<allOut.length;i++){if(allOut[i]==pw){allOut[i] = allOut[allOut.length-1];allOut = Arrays.copyOf(allOut,allOut.length-1);break;}}sendMessage(host+"下线了,当前在线人数:"+allOut.length);try {socket.close();//与客户端断开链接} catch (IOException e) {e.printStackTrace();}}}/*** 广播消息给所有客户端* @param message*/private void sendMessage(String message){for(int i=0;i<allOut.length;i++) {allOut[i].println(message);}}}}
选取合适的锁对象
this不可以
allOut不可以。大多数情况下可以选择临界资源作为锁对象,但是这里不行。
最终代码:
package socket;import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;/*** 聊天室服务端*/
public class Server {/*** 运行在服务端的ServerSocket主要完成两个工作:* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket* 就可以和该客户端交互了** 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个* 电话使得服务端与你沟通。*/private ServerSocket serverSocket;/*存放所有客户端输出流,用于广播消息*/private PrintWriter[] allOut = {};/*** 服务端构造方法,用来初始化*/public Server(){try {System.out.println("正在启动服务端...");/*实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他应用程序占用的端口相同,否则会抛出异常:java.net.BindException:address already in use端口是一个数字,取值范围:0-65535之间。6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。*/serverSocket = new ServerSocket(8088);System.out.println("服务端启动完毕!");} catch (IOException e) {e.printStackTrace();}}/*** 服务端开始工作的方法*/public void start(){try {while(true) {System.out.println("等待客户端链接...");/*ServerSocket提供了接受客户端链接的方法:Socket accept()这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例通过这个Socket就可以与客户端进行交互了。可以理解为此操作是接电话,电话没响时就一直等。*/Socket socket = serverSocket.accept();System.out.println("一个客户端链接了!");//启动一个线程与该客户端交互ClientHandler clientHandler = new ClientHandler(socket);Thread t = new Thread(clientHandler);t.start();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {Server server = new Server();server.start();}/*** 定义线程任务* 目的是让一个线程完成与特定客户端的交互工作*/private class ClientHandler implements Runnable{private Socket socket;private String host;//记录客户端的IP地址信息public ClientHandler(Socket socket){this.socket = socket;//通过socket获取远端计算机地址信息host = socket.getInetAddress().getHostAddress();}public void run(){PrintWriter pw = null;try{/*Socket提供的方法:InputStream getInputStream()获取的字节输入流读取的是对方计算机发送过来的字节*/InputStream in = socket.getInputStream();InputStreamReader isr = new InputStreamReader(in, "UTF-8");BufferedReader br = new BufferedReader(isr);OutputStream out = socket.getOutputStream();OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");BufferedWriter bw = new BufferedWriter(osw);pw = new PrintWriter(bw,true);//将该输出流存入共享数组allOut中
// synchronized (this) {//不行,因为这个是ClientHandler实例
// synchronized (allOut) {//不行,下面操作会扩容,allOut对象会变synchronized (Server.this) {//外部类对象可以//1对allOut数组扩容allOut = Arrays.copyOf(allOut, allOut.length + 1);//2将输出流存入数组最后一个位置allOut[allOut.length - 1] = pw;}//通知所有客户端该用户上线了sendMessage(host + "上线了,当前在线人数:"+allOut.length);String message = null;while ((message = br.readLine()) != null) {System.out.println(host + "说:" + message);//将消息回复给所有客户端sendMessage(host + "说:" + message);}}catch(IOException e){e.printStackTrace();}finally{//处理客户端断开链接的操作//将当前客户端的输出流从allOut中删除(数组缩容)synchronized (Server.this) {for (int i = 0; i < allOut.length; i++) {if (allOut[i] == pw) {allOut[i] = allOut[allOut.length - 1];allOut = Arrays.copyOf(allOut, allOut.length - 1);break;}}}sendMessage(host+"下线了,当前在线人数:"+allOut.length);try {socket.close();//与客户端断开链接} catch (IOException e) {e.printStackTrace();}}}/*** 广播消息给所有客户端* @param message*/private void sendMessage(String message){synchronized (Server.this) {for (int i = 0; i < allOut.length; i++) {allOut[i].println(message);}}}}
}
7.集合框架
什么是集合
集合与数组一样,可以保存一组元素,并且提供了操作元素的相关方法,使用更方便.
java集合框架中相关接口
java.util.Collection接口
java.util.Collection是所有集合的顶级接口.Collection下面有多种实现类,因此我们有更多的数据结构可供选择.
Collection下面有两个常见的子接口:
- java.util.List:线性表.是可重复集合,并且有序.
- java.util.Set:不可重复的集合,大部分实现类是无序的.
这里可重复指的是集合中的元素是否可以重复,而判定重复元素的标准是依靠元素自身equals比较
的结果.为true就认为是重复元素.
package collection;import java.util.ArrayList;
import java.util.Collection;public class CollectionDemo {public static void main(String[] args) {Collection c = new ArrayList();/*boolean add(E e)向当前集合中添加一个元素.当元素成功添加后返回true*/c.add("one");c.add("two");c.add("three");c.add("four");c.add("five");System.out.println(c);/*int size()返回当前集合的元素个数*/int size = c.size();System.out.println("size:"+size);/*boolean isEmpty()判断当前集合是否为空集(不含有任何元素)*/boolean isEmpty = c.isEmpty();System.out.println("是否为空集:"+isEmpty);/*清空集合*/c.clear();System.out.println(c);System.out.println("size:"+c.size());//0System.out.println("是否为空集:"+c.isEmpty());}
}
集合与元素equals方法相关的方法
package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;/*** 集合的很多操作有与元素的equals方法相关。*/
public class CollectionDemo2 {public static void main(String[] args) {
// Collection c = new ArrayList();Collection c = new HashSet();c.add(new Point(1,2));c.add(new Point(3,4));c.add(new Point(5,6));c.add(new Point(7,8));c.add(new Point(1,2));/*集合重写了Object的toString方法,输出的格式为:[元素1.toString(), 元素2.toString(), ....]*/System.out.println(c);Point p = new Point(1,2);/*boolean contains(Object o)判断当前集合是否包含给定元素,这里判断的依据是给定元素是否与集合现有元素存在equals比较为true的情况。*/boolean contains = c.contains(p);System.out.println("包含:"+contains);/*remove用来从集合中删除给定元素,删除的也是与集合中equals比较为true的元素。注意,对于可以存放重复元素的集合而言,只删除一次。*/c.remove(p);System.out.println(c);}
}
集合存放的是元素的引用
集合只能存放引用类型元素,并且存放的是元素的引用
package collection;import java.util.ArrayList;
import java.util.Collection;/*** 集合只能存放引用类型元素,并且存放的是元素的引用(地址)*/
public class CollectionDemo3 {public static void main(String[] args) {Collection c = new ArrayList();Point p = new Point(1,2);c.add(p);System.out.println("p:"+p);//p:(1,2)System.out.println("c:"+c);//c:[(1,2)]p.setX(2);System.out.println("p:"+p);//p:(2,2)System.out.println("c:"+c);//c:[(2,2)]}
}
7.1集合(续)
集合间的操作
集合提供了如取并集,删交集,判断包含子集等操作
package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;/*** 集合间的操作*/
public class CollectionDemo4 {public static void main(String[] args) {
// Collection c1 = new ArrayList();Collection c1 = new HashSet();//不可重复元素c1.add("java");c1.add("c");c1.add("c++");System.out.println("c1:"+c1);Collection c2 = new ArrayList();c2.add("android");c2.add("ios");c2.add("java");System.out.println("c2:"+c2);/*boolean addAll(Collection c)将给定集合中的所有元素添加到当前集合中。当前集合若发生了改变则返回true*/boolean tf = c1.addAll(c2);System.out.println(tf);System.out.println("c1:"+c1);System.out.println("c2:"+c2);Collection c3 = new ArrayList();c3.add("ios");c3.add("c++");c3.add("php");System.out.println("c3:"+c3);/*boolean containsAll(Collection c)判断当前集合是否包含给定集合中的所有元素*/boolean contains = c1.containsAll(c3);System.out.println("包含所有元素:"+contains);/*boolean removeAll(Collection c)删除当前集合中与给定集合中的共有元素*/c1.removeAll(c3);System.out.println("c1:"+c1);System.out.println("c3:"+c3);}
}
7.2集合的遍历
Collection提供了统一的遍历集合方式:迭代器模式
Iterator iterator()
该方法会获取一个用于遍历当前集合元素的迭代器.
java.util.Iterator接口
迭代器接口,定义了迭代器遍历集合的相关操作.
不同的集合都实现了一个用于遍历自身元素的迭代器实现类,我们无需记住它们的名字,用多态的角度把他们看做为Iterator即可.
迭代器遍历集合遵循的步骤为:问,取,删.其中删除元素不是必要操作
package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** Collection接口没有定义单独获取某一个元素的操作,因为不通用。* 但是Collection提供了遍历集合元素的操作。该操作是一个通用操作,无论什么类型的* 集合都支持此种遍历方式:迭代器模式。** Iterator iterator() die(二声)* 该方法会获取一个用于遍历当前集合元素的迭代器** java.util.Iterator接口,是迭代器接口,规定了迭代器遍历集合的相关操作,不同的* 集合都提供了一个用于遍历自身元素的迭代器实现类,不过我们不需要直到它们的名字,以* 多态的方式当成Iterator使用即可。* 迭代器遍历集合遵循的步骤为:问->取->删* 其中删除不是必须操作。**/
public class IteratorDemo {public static void main(String[] args) {Collection c = new ArrayList();c.add("one");c.add("two");c.add("three");c.add("four");c.add("five");System.out.println(c);//获取迭代器Iterator it = c.iterator();/*迭代器提供的相关方法:boolean hasNext()判断集合是否还有元素可以遍历E next()获取集合下一个元素(第一次调用时就是获取第一个元素,以此类推)*/while(it.hasNext()){String str = (String)it.next();System.out.println(str); }System.out.println(c);}
}
迭代器遍历过程中不得通过集合的方法增删元素
package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** Collection接口没有定义单独获取某一个元素的操作,因为不通用。* 但是Collection提供了遍历集合元素的操作。该操作是一个通用操作,无论什么类型的* 集合都支持此种遍历方式:迭代器模式。** Iterator iterator() die(二声)* 该方法会获取一个用于遍历当前集合元素的迭代器** java.util.Iterator接口,是迭代器接口,规定了迭代器遍历集合的相关操作,不同的* 集合都提供了一个用于遍历自身元素的迭代器实现类,不过我们不需要直到它们的名字,以* 多态的方式当成Iterator使用即可。* 迭代器遍历集合遵循的步骤为:问->取->删* 其中删除不是必须操作。**/
public class IteratorDemo {public static void main(String[] args) {Collection c = new ArrayList();c.add("one");c.add("#");c.add("two");c.add("#");c.add("three");c.add("#");c.add("four");c.add("#");c.add("five");System.out.println(c);//获取迭代器Iterator it = c.iterator();/*迭代器提供的相关方法:boolean hasNext()判断集合是否还有元素可以遍历E next()获取集合下一个元素(第一次调用时就是获取第一个元素,以此类推)*/while(it.hasNext()){String str = (String)it.next();System.out.println(str);if("#".equals(str)){/*迭代器要求遍历的过程中不得通过集合的方法增删元素否则会抛出异常:ConcurrentModificationException*/
// c.remove(str);/*迭代器的remove方法可以将通过next方法获取的元素从集合中删除。*/it.remove();}}System.out.println(c);}
}
7.3增强型for循环
JDK5之后推出了一个特性:增强型for循环
- 也称为新循环,使得我们可以使用相同的语法遍历集合或数组.
- 语法:
for(元素类型 变量名 : 集合或数组){循环体
}
package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** JDK5推出时,推出了一个新的特性:增强型for循环* 也称为新循环,它可以用相同的语法遍历集合或数组。** 新循环是java编译器认可的,并非虚拟机。*/
public class NewForDemo {public static void main(String[] args) {String[] array = {"one","two","three","four","five"};for(int i=0;i<array.length;i++){String str = array[i];System.out.println(str);}for(String str : array){System.out.println(str);}Collection c = new ArrayList();c.add("one");c.add("two");c.add("three");c.add("four");c.add("five");//迭代器遍历Iterator it = c.iterator();while(it.hasNext()){String str = (String)it.next();System.out.println(str);}//新循环遍历for(Object o : c){String str = (String)o;System.out.println(str);}}
}
7.4泛型
JDK5之后推出的另一个特性:泛型
泛型也称为参数化类型,允许我们在使用一个类时指定它当中属性,方法参数或返回值的类型.
- 泛型在集合中被广泛使用,用来指定集合中的元素类型.
- 有泛型支持的类在使用时若不指定泛型的具体类型则默认为原型Object
package collection;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** JDK5推出时,推出了一个新的特性:增强型for循环* 也称为新循环,它可以用相同的语法遍历集合或数组。** 新循环是java编译器认可的,并非虚拟机。*/
public class NewForDemo {public static void main(String[] args) {String[] array = {"one","two","three","four","five"};for(int i=0;i<array.length;i++){String str = array[i];System.out.println(str);}for(String str : array){System.out.println(str);}/** 泛型 JDK5之后推出的另一个特性。* 泛型也称为参数化类型,允许我们在使用一个类时指定它里面属性的类型,* 方法参数或返回值的类型,使得我们使用一个类时可以更灵活。* 泛型被广泛应用于集合中,用来指定集合中的元素类型。* 支持泛型的类在使用时如果未指定泛型,那么默认就是原型Object** Collection接口的定义* public interface Collection<E> ... {** Collection<E> 这里的<E>就是泛型** Collection中add方法的定义,参数为E* boolean add(E e)*/Collection<String> c = new ArrayList<>();c.add("one");//编译器会检查add方法的实参是否为String类型c.add("two");c.add("three");c.add("four");c.add("five");
// c.add(123);//编译不通过//迭代器遍历//迭代器也支持泛型,指定的与其遍历的集合指定的泛型一致即可Iterator<String> it = c.iterator();while(it.hasNext()){//编译器编译代码时会根据迭代器指定的泛型补充造型代码String str = it.next();//获取元素时无需在造型System.out.println(str);}//新循环遍历for(String str : c){System.out.println(str);}}
}
7.5List集
java.util.List接口,继承自Collection.
List集合是可重复集,并且有序,提供了一套可以通过下标操作元素的方法
常用实现类:
- java.util.ArrayList:内部使用数组实现,查询性能更好.
- java.util.LinkedList:内部使用链表实现,首尾增删元素性能更好.
7.6List集合常见方法
get()与set()
package collection;import java.util.ArrayList;
import java.util.List;/*** List集合* List是Collection下面常见的一类集合。* java.util.List接口是所有List的接口,它继承自Collection。* 常见的实现类:* java.util.ArrayList:内部由数组实现,查询性能更好。* java.util.LinkedList:内部由链表实现,增删性能更好。** List集合的特点是:可以存放重复元素,并且有序。其提供了一套可以通过下标* 操作元素的方法。*/
public class ListDemo {public static void main(String[] args) {List<String> list = new ArrayList<>();
// List<String> list = new LinkedList<>();list.add("one");list.add("two");list.add("three");list.add("four");list.add("five");/*E get(int index)获取指定下标对应的元素*///获取第三个元素String e = list.get(2);System.out.println(e);for(int i=0;i<list.size();i++){e = list.get(i);System.out.println(e);}/*E set(int index,E e)将给定元素设置到指定位置,返回值为该位置原有的元素。替换元素操作*///[one,six,three,four,five]String old = list.set(1,"six");System.out.println(list);System.out.println("被替换的元素是:"+old);}
}
重载的add()和remove()
package collection;import java.util.ArrayList;
import java.util.List;/*** List集合提供了一对重载的add,remove方法*/
public class ListDemo2 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("one");list.add("two");list.add("three");list.add("four");list.add("five");System.out.println(list);/*void add(int index,E e)将给定元素插入到指定位置*///[one,two,six,three,four,five]list.add(2,"six");System.out.println(list);/*E remove(int index)删除并返回指定位置上的元素*///[one,six,three,four,five]String e = list.remove(1);System.out.println(list);System.out.println("被删除的元素:"+e);}
}
subList()方法
package collection;import java.util.ArrayList;
import java.util.List;/*** List subList(int start,int end)* 获取当前集合中指定范围内的子集。两个参数为开始与结束的下标(含头不含尾)*/
public class ListDemo3 {public static void main(String[] args) {List<Integer> list = new ArrayList<>();for(int i=0;i<10;i++){list.add(i);}System.out.println(list);//获取3-7这部分List<Integer> subList = list.subList(3,8);System.out.println(subList);//将子集每个元素扩大10倍for(int i=0;i<subList.size();i++){subList.set(i,subList.get(i) * 10);}//[30,40,50,60,70]System.out.println(subList);/*对子集元素的操作就是对原集合对应元素的操作*/System.out.println(list);//删除list集合中的2-8list.subList(2,9).clear();System.out.println(list);}
}
7.7集合与数组的转换
集合转换为数组
Collection提供了一个方法:toArray,可以将当前集合转换为一个数组
package collection;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** 集合转换为数组* Collection提供了方法toArray可以将当前集合转换为一个数组*/
public class CollectionToArrayDemo {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("one");list.add("two");list.add("three");list.add("four");list.add("five");System.out.println(list);// Object[] array = list.toArray();/*重载的toArray方法要求传入一个数组,内部会将集合所有元素存入该数组后将其返回(前提是该数组长度>=集合的size)。如果给定的数组长度不足,则方法内部会自行根据给定数组类型创建一个与集合size一致长度的数组并将集合元素存入后返回。*/String[] array = list.toArray(new String[list.size()]);System.out.println(array.length);System.out.println(Arrays.toString(array));}
}
数组转换为List集合
数组的工具类Arrays提供了一个静态方法asList(),可以将一个数组转换为一个List集合
package collection;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** 数组转换为List集合* 数组的工具类Arrays提供了一个静态方法asList,可以将数组转换为一个List集合。*/
public class ArrayToListDemo {public static void main(String[] args) {String[] array = {"one","two","three","four","five"};System.out.println(Arrays.toString(array));List<String> list = Arrays.asList(array);System.out.println(list);list.set(1,"six");System.out.println(list);//数组跟着改变了。注意:对数组转换的集合进行元素操作就是对原数组对应的操作System.out.println(Arrays.toString(array));/*由于数组是定长的,因此对该集合进行增删元素的操作是不支持的,会抛出异常:java.lang.UnsupportedOperationException*/
// list.add("seven");/*若希望对集合进行增删操作,则需要自行创建一个集合,然后将该集合元素导入。*/
// List<String> list2 = new ArrayList<>();
// list2.addAll(list);/*所有的集合都支持一个参数为Collection的构造方法,作用是在创建当前集合的同时包含给定集合中的所有元素*/List<String> list2 = new ArrayList<>(list);System.out.println("list2:"+list2);list2.add("seven");System.out.println("list2:"+list2);}
}
7.8集合的排序
java.util.Collections类
Collections是集合的工具类,里面定义了很多静态方法用于操作集合.
Collections.sort(List list)方法
可以对List集合进行自然排序(从小到大)
package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;/*** 集合的排序* 集合的工具类:java.util.Collections提供了一个静态方法sort,可以对List集合* 进行自然排序*/
public class SortListDemo1 {public static void main(String[] args) {List<Integer> list = new ArrayList<>();Random random = new Random();for(int i=0;i<10;i++){list.add(random.nextInt(100));}System.out.println(list);Collections.sort(list);System.out.println(list);}
}
7.9集合的排序
java.util.Collections类
Collections是集合的工具类,里面定义了很多静态方法用于操作集合.
Collections.sort(List list)方法
可以对List集合进行自然排序(从小到大)
package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;/*** 集合的排序* 集合的工具类:java.util.Collections提供了一个静态方法sort,可以对List集合* 进行自然排序*/
public class SortListDemo1 {public static void main(String[] args) {List<Integer> list = new ArrayList<>();Random random = new Random();for(int i=0;i<10;i++){list.add(random.nextInt(100));}System.out.println(list);Collections.sort(list);System.out.println(list);}
}
排序自定义类型元素
package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;/*** 排序自定义类型元素*/
public class SortListDemo2 {public static void main(String[] args) {List<Point> list = new ArrayList<>();list.add(new Point(1,2));list.add(new Point(97,88));list.add(new Point(7,6));list.add(new Point(9,9));list.add(new Point(5,4));list.add(new Point(2,3));System.out.println(list);/*编译不通过的原因:Collections.sort(List list)该方法要求集合中的元素类型必须实现接口:Comparable,该接口中有一个抽象方法compareTo,这个方法用来定义元素之间比较大小的规则.所以只有实现了该接口的元素才能利用这个方法比较出大小进而实现排序操作.*/Collections.sort(list);//编译不通过 compare比较 comparable可以比较的System.out.println(list);}
}
实际开发中,我们并不会让我们自己定义的类(如果该类作为集合元素使用)去实现Comparable接口,因为这对我们的程序有侵入性.
侵入性:当我们调用某个API功能时,其要求我们为其修改其他额外的代码,这个现象就是侵入性.侵入性越强的API越不利于程序的后期可维护性.应当尽量避免.
重载的Collections.sort(List list,Comparator c)方法
package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;/*** 排序自定义类型元素*/
public class SortListDemo2 {public static void main(String[] args) {List<Point> list = new ArrayList<>();list.add(new Point(1,2));list.add(new Point(97,88));list.add(new Point(7,6));list.add(new Point(9,9));list.add(new Point(5,4));list.add(new Point(2,3));System.out.println(list);/*Collections.sort(List list)在排序List集合时要求集合元素必须实现了Comparable接口。实现了该接口的类必须重写一个方法compareTo用与定义比较大小的规则,从而进行元素间的比较后排序。否则编译不通过。侵入性:当我们调用某个API时,其反过来要求我们为其修改其他额外的代码,这种现象就成为侵入性。侵入性不利于程序后期的维护,尽可能避免。compare:比较*/
// Collections.sort(list);//匿名内部类的形式创建一个比较器Comparator<Point> com = new Comparator<Point>() {@Override/*** 实现比较器接口后必须重写方法compare.* 该方法用来定义参数o1与参数o2的比较大小规则* 返回值用来表示o1与o2的大小关系*/public int compare(Point o1, Point o2) {int len1 = o1.getX() * o1.getX() + o1.getY() * o1.getY();int len2 = o2.getX() * o2.getX() + o2.getY() * o2.getY();return len1-len2;}};Collections.sort(list,com);//回调模式System.out.println(list);}
}
最终没有侵入性的写法
package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;/*** 排序自定义类型元素*/
public class SortListDemo2 {public static void main(String[] args) {List<Point> list = new ArrayList<>();list.add(new Point(1,2));list.add(new Point(97,88));list.add(new Point(7,6));list.add(new Point(9,9));list.add(new Point(5,4));list.add(new Point(2,3));System.out.println(list);/*Collections.sort(List list)在排序List集合时要求集合元素必须实现了Comparable接口。实现了该接口的类必须重写一个方法compareTo用与定义比较大小的规则,从而进行元素间的比较后排序。否则编译不通过。侵入性:当我们调用某个API时,其反过来要求我们为其修改其他额外的代码,这种现象就称为侵入性。侵入性不利于程序后期的维护,尽可能避免。compare:比较*/
// Collections.sort(list);//匿名内部类的形式创建一个比较器
// Comparator<Point> com = new Comparator<Point>() {
// @Override
// /**
// * 实现比较器接口后必须重写方法compare.
// * 该方法用来定义参数o1与参数o2的比较大小规则
// * 返回值用来表示o1与o2的大小关系
// */
// public int compare(Point o1, Point o2) {
// int len1 = o1.getX() * o1.getX() + o1.getY() * o1.getY();
// int len2 = o2.getX() * o2.getX() + o2.getY() * o2.getY();
// return len1-len2;
// }
// };
// Collections.sort(list,com);//回调模式// Collections.sort(list,new Comparator<Point>() {
// public int compare(Point o1, Point o2) {
// int len1 = o1.getX() * o1.getX() + o1.getY() * o1.getY();
// int len2 = o2.getX() * o2.getX() + o2.getY() * o2.getY();
// return len1-len2;
// }
// });Collections.sort(list,(o1,o2)->o1.getX() * o1.getX() + o1.getY() * o1.getY() -o2.getX() * o2.getX() - o2.getY() * o2.getY());System.out.println(list);}
}
排序字符串
java中提供的类,如:String,包装类都实现了Comparable接口,但有时候这些比较规则不能满足我们的排序需求时,同样可以临时提供一种比较规则来进行排序.
package collection;import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;public class SortListDemo3 {public static void main(String[] args) {List<String> list = new ArrayList<>();
// list.add("Tom");
// list.add("jackson");
// list.add("rose");
// list.add("jill");
// list.add("ada");
// list.add("hanmeimei");
// list.add("lilei");
// list.add("hongtaoliu");
// list.add("Jerry");list.add("传奇");list.add("小泽老师");list.add("苍老师");System.out.println(list);//按照字符多少排序
// Collections.sort(list);
// Collections.sort(list, new Comparator<String>() {
// public int compare(String o1, String o2) {
return o1.length()-o2.length();
// return o2.length()-o1.length();//反过来减就是降序
// }
// });Collections.sort(list,(o1,o2)->o2.length()-o1.length());System.out.println(list);}
}