需求:第一次运行控制台输出:欢迎使用本软件,第1次使用免费~
第二次运行控制台输出:欢迎使用本软件,第2次使用免费~
第三次运行控制台输出:欢迎使用本软件,第3次使用免费~
第四次及之后运行控制台输出:本软件只能免费使用3次,欢迎您注册会员后继续使用~
//创建一个文件,文件中写0
public class Test{
public static void main(String[] args){
BufferedReader br = new BufferedReader(new FileReader("aa.txt"));
String line = br.readLine();
br.close();
int count = Integer.parseInt(line);
count++;
if(count<=3){
sout("欢迎使用本软件,第"+count+"次使用免费~");
}else{
sout("本软件只能免费使用3次,欢迎您注册会员后继续使用~");
}
BufferedWriter bw = new BufferedWriter(new FileWriter("aa.txt"));
bw.write(count+"");
bw.close();
}
}
转换流
转换流是属于字符流的,他本身也是一种高级流,用来包装基本流的,其中,转换输入流叫作:InputStreamReader,转换输出流叫作:OutputStreamWriter
转换流是字节流和字符流之间的桥梁
例如:有一个数据源,当我们要读取数据到内存中时,我们创建一个转换输入流对象时,是需要包装一个字节输入流的,在包装之后,这个字节流就变成字符流了,就可以拥有字符流的特性了,比如,读取数据不会乱码了,可以根据字符集一次读取多个字节了
当我们要把内存中的数据写到一个目的地,我们创建一个转换输出流对象时,它里面是需要包装字节输出流的,它是跟输入的相反的,它是把字符流转换成字节流,把内存的数据转换成字节往外写出
作用:
-
指定字符集读写,但是在JDK11之后,这种方式被淘汰了,用FileReader的构造方法来指定
-
字节流想要使用字符流中的方法
-
一个文件用GBK编码的,java中我们指定的是UTF-8,读取数据并不乱码
public class Test1 {
public static void main(String[] args) throws Exception {
//创建转换流对象并指定字符编码
InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\BaiduNetdiskDownload\\csb1.txt"),"GBK");
//读取数据
int ch;
while((ch = isr.read()) != -1){
System.out.print((char)ch);
}
isr.close();
//上面的方式被淘汰了,用FileReader构造方法指定对象,其底层也是用上面的方法来的
FileReader fr = new FileReader("D:\\BaiduNetdiskDownload\\csb1.txt", Charset.forName("GBK"));
int c;
while((c = fr.read()) != -1){
System.out.print((char)c);
}
fr.close();
}
}
- 按照指定编码格式将数据写到文件中 FileWriter
public class Test1 {
public static void main(String[] args) throws Exception {
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("a.txt"),"GBK");
osw.write("你好");
osw.close();
//上面方式被淘汰,用FileWriter的构造方法,底层也是用的上面的方式
FileWriter fw = new FileWriter("a.txt",Charset.forName("GBK"));
fw.write("你好好");
fw.close();
}
}
- 将本地文件中的GBK文件,转成UTF-8
//JDK11以前的方案
InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"),"GBK");
OutputStreamWriter osw = new OutputStreamWriter(new FileOutStream("b.txt"),"UTF-8");
int b;
while((b=isr.read()) != -1){
osw.write(b);
}
osw.close();
ise.close();
//替代方案
FileReader fr = new FileReader("a.txt",Charset.forName("GBK"));
FileWriter fw = new FileWriter("b.txt",charset.forName("UTF-8"));
int a;
while((a = fr.read()) != -1){
fw.write(a);
}
fw.close();
fr.close();
- 利用字节流读取中文的数据,每次读取一整行,并且不能出现乱码
public class Test1 {
public static void main(String[] args) throws Exception {
//字节流在读取中文是会乱码的,但是字符流可以搞定
//字节流是没有读一整行的方法,只有字符缓冲流有
InputStreamReader isr = new InputStreamReader(new FileInputStream("b.txt"));
BufferedReader br = new BufferedReader(isr);
String line;
while((line=br.readLine())!=null){
System.out.println(line);
}
br.close();
}
}
序列化流
序列化流也是高级流,也是用来包装基本流的,而且序列换流是属于字节流的一种,它负责输出数据,与之对应的是反序列化流,它是输入数据的
- ObjectInputStream:反序列化流
- ObjectOutputStream:序列化流
序列化流的作用:可以把java中的对象写到本地文件中
序列化流写到文件中的数据是不能修改的,一旦修改就无法再次读回来了
构造方法 | 说明 |
---|---|
public ObjectInputStream(OutputStream out) | 把基本流包装成高级流 |
成员方法 | 说明 |
---|---|
public final void writeObject(Object obj) | 把对象序列化(写出)到文件中 |
序列化细节:使用对象输出流将对象保存到文件中会出现NotSerializableException异常,需要让javabean类实现 Serializable接口
Serializable接口:接口里面是没有抽象方法,是标记型接口,一旦实现了这个接口,就表示当前的javabean类是可以被序列化的
- 将一个对象序列化保存到文件中
//类
public class Student implements Serializable {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
//创建对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("c.txt"));
Student s = new Student("zhangsan",23);
//写出数据
oos.writeObject(s);
oos.close();
}
}
反序列化流:可以把序列化到本地的对象,读取到程序中来
构造方法 | 说明 |
---|---|
public ObjectInputStream(InputStream out) | 把基本流变成高级流 |
成员方法 | 说明 |
---|---|
public Object readObject() | 把序列化到本地文件的对象,读取到程序中 |
- 利用反序列流把文件中的对象读取到程序中
public class Test1 {
public static void main(String[] args) throws Exception {
//创建对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("c.txt"));
//读取数据
Object o = ois.readObject();
Student s = (Student)o;
ois.close();
//打印对象
System.out.println(s);
}
}
细节:当我们把对象序列化存到文件中,然后修改一下对象,用反序列化读取数据中,会出现异常
原因:当一个类实现了 Serializable 接口,就代表这个类可被序列化,那么java底层会根据这个类的成员变量成员方法等所有内容进行计算,计算出一个 long 类型的序列号,假设现在计算出来的版本号是1,当创建一个对象的时候,其实这个对象里面就包含了序列号1,用序列化写对象时,也会把序列号写到文件当中;如果此时修改了一下这个对象类的代码,java底层就会重新计算这个对象类的序列号,假设现在序列号是2,那么这个时候,用反序列化流读取对象到程序中时,这俩个的序列号就不一样了,代码就会直接报错,所以就是文件中的序列号和javabean中的序列号不匹配而导致的
解决方法:固定版本号,定义javabean类的时候,手动的把序列号定义出来,一旦我们定义了,java底层就不会重新再计算了,格式如下:
private static final long serialVersionUID = 1L;
private:不让外界使用,也不提供get和set方法;static:表示让这个类的对象都共享同一个序列号;final:表示序列号的值永远不变化;long:数据类型,序列号比较长,int是不够的;变量名一定叫作 serialVersionUID,不然java不认识
快捷方式:idea里sitting中可以设置一下,创建类的时候,他会自动计算一个序列号,帮我们生成出来,生成的时候,先把类的内容写完再去生成序列号,因为序列号是根据类的所有内容来计算的
所以要序列化一个对象时,不仅要实现接口,还要定义序列号
关键字 transient
作用:不会把当前属性序列化到本地文件中,序列化对象被反序列化读取时,这个属性是默认初始值
//类
public class Student implements Serializable {
@Serial
private static final long serialVersionUID = 335946442802087864L;
private String name;
private int age;
private transient String address;
public Student() {
}
public Student(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
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 getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
'}';
}
}
//把对象序列化到文件中
public class Test2 {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("b.txt"));
Student s = new Student("张三",23,"南京");
oos.writeObject(s);
oos.close();
}
}
//反序列化
public class Test1 {
public static void main(String[] args) throws Exception {
//创建对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.txt"));
//读取数据
Object o = ois.readObject();
Student s = (Student)o;
ois.close();
//打印对象
System.out.println(s);//Student{name='张三', age=23, address='null'}
}
}
当序列化多个对象时,反序列化时,不知道有多少个,而反序列化对象的read方法没读到对象时,就会报错,所以我们一般规定把多个对象放到ArrayList中,反序列化时,读一个就行了
public class Test2 {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("b.txt"));
Student s = new Student("张三",23,"南京");
Student s1 = new Student("lisi",24,"江西");
Student s2 = new Student("wangwu",25,"南昌");
ArrayList<Student> list = new ArrayList<>();
Collections.addAll(list,s,s1,s2);
oos.writeObject(list);
oos.close();
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
//创建对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.txt"));
//读取数据
ArrayList<Student> list = (ArrayList<Student>) ois.readObject();
ois.close();
//打印对象
for (Student student : list) {
System.out.println(student);
}
}
}
打印流
打印流在IO的位置,打印流是高级流,也是用来包装基本流的,但是打印流不能读,只能写,所以打印流只有输出流
分类:打印流一般是指:PrintStream(字节打印流),PrintWriter(字符打印流)俩个类
特点:
- 打印流只操作文件目的地,不操作数据源
- 特有的写出方法可以实现,数据原样写出;例如:打印 97 , 文件中 97
- 特有的写出方法,可以实现自动刷新,自动换行;打印一次数据=其他流的 写出+换行+刷新
字节打印流
构造方法 | 说明 |
---|---|
public PrintStream(OutputStream/File/String) | 关联字节输出流/文件/文件路径 |
public PrintStream(String filename, Charset charset) | 指定字符编码 |
public PrintStream(OutputStream out, boolean autoFlush) | 自动刷新 |
public PrintStream(OutputStream out, boolean autoFlush, String encoding) | 指定字符编码并且自动刷新 |
字节流底层没有缓冲区,开不开自动刷新都一样
成员方法 | 说明 |
---|---|
public void write(int b) | 常规方法:规则跟之前一样,将指定的字节写出 |
public void println(Xxx xx) | 特有方法:打印任意数据,自动刷新,自动换行 |
public void print(Xxx xx) | 特有方法:打印任意数据,不换行 |
public void printf(String format, object… args) | 特有方法:带有占位符的打印语句,不换行 |
public class Test1 {
public static void main(String[] args) throws Exception {
PrintStream ps = new PrintStream(new FileOutputStream("a.txt"),true,Charset.forName("UTF-8"));
//写出数据
ps.println(97);//写出+自动刷新+自动换行
ps.print(true);
ps.printf("%s爱上了%s","阿正","啊强");
ps.close();
//97
//true阿正爱上了啊强
}
}
System.out在底层其实就是一个PrintStream类型,只是他不是关联的文件还是关联的控制台,直接打印在控制台
字符打印流
字符底层有缓冲区,想要自动刷新需要开启
构造方法 | 说明 |
---|---|
public PrintWriter(Write/File/String) | 关联字节输出流/文件/文件路径 |
public PrintWriter(String filename, Charset charset) | 指定字符编码 |
public PrintWriter(Write w, boolean autoFlush) | 自动刷新 |
public PrintWriter(OutputStream out, boolean autoFlush, Charset charset) | 指定字符编码并且自动刷新 |
成员方法 | 说明 |
---|---|
public void write(…) | 常规方法:规则跟之前一样,写出字节或者字符串 |
public void println(Xxx xxx) | 特有方法:打印任意类型的数据并且换行 |
public void print(Xxxx xx) | 特有方法:打印任意类型的数据,不换行 |
public void printf(String format, Object…args) | 特有方法:带有占位符的打印语句 |
public class Test1 {
public static void main(String[] args) throws Exception {
PrintWriter pw = new PrintWriter(new FileWriter("c.txt"),true);
pw.write("你好好说");
pw.println("sfdk");
pw.print("是啊");
pw.printf("%s爱上了%s","啊峰","啊强");
pw.close();
}
}
解压缩流/压缩流
应用场景:如果要传输的数据比较大,那么就可以先压缩再传输
IO位置:解压缩流就是读取压缩包中的文件,所以它是属于字节输入流;而压缩流是把文件中的数据写到压缩包中,所以它是属于字节输出流
解压缩流
它是属于字节输入流
解压本质:压缩包里面的每一个文件/文件夹就是一个ZipEntry对象,把每一个ZipEntry按照层级拷贝到本地另一个文件夹中
public class Test1 {
public static void main(String[] args) throws Exception {
//创建一个File表示要解压的压缩包
File src = new File("D:\\aaa.zip");
//创建一个FIle表示要解压的目的地
File dest = new File("D:\\");
new File("D:\\aaa").mkdir();
unzip(src,dest);
}
//定义一个方法用来解压
public static void unzip(File src, File dest) throws IOException {
//创建一个解压流用来读取压缩包中的数据
ZipInputStream zis = new ZipInputStream(new FileInputStream(src));
//要先获取压缩包里面的每一个zipentry对象
ZipEntry entry;
while((entry = zis.getNextEntry()) != null){
System.out.println(entry);//aaa/asd.xlsx
if(entry.isDirectory()){
//文件夹,需要在目的地创建一个同样的文件夹
File file = new File(dest,entry.toString());
file.mkdirs();
}else{
//文件,需要读取里面内容放到目的地
FileOutputStream fos = new FileOutputStream(new File(dest,entry.toString()));
int b;
while((b = zis.read()) != -1){
fos.write(b);
}
fos.close();
//表示在压缩包中的一个文件处理完毕了
zis.closeEntry();
}
}
zis.close();
}
}
压缩流
本质:压缩包里面的每一个文件/文件夹就是一个ZipEntry对象,把每一个(文件/文件夹)看成ZipEntry对象放到压缩包中
- 压缩一个文件
public class Test1 {
public static void main(String[] args) throws Exception {
//创建File对象表示要压缩的文件
File src = new File("D:\\aa.txt");
//创建File对象表示压缩包的位置
File dest = new File("D:\\");
tozip(src,dest);
}
public static void tozip(File src,File dest) throws IOException {
//创建压缩流关联压缩包
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File(dest,"a.zip")));
//创建ZipEntry对象,表示压缩包里面的每一个文件和文件夹
//参数:表示压缩包里面的路径,可以多级
ZipEntry entry = new ZipEntry("a.txt");
//把ZipEntry对象放到压缩包中
zos.putNextEntry(entry);
//把src文件中的数据写到压缩包中
FileInputStream fis = new FileInputStream(src);
int b;
while((b=fis.read()) != -1){
zos.write(b);
}
zos.closeEntry();
zos.close();
}
}
- 压缩一个文件夹
public class Test1 {
public static void main(String[] args) throws Exception {
//创建FIle对象表示要压缩的对象
File src = new File("D:\\aaa");
//创建file对象表示压缩包放在哪里
File dest = new File(src.getParent(),src.getName()+".zip");
//创建压缩流关联压缩包
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
//获取src里面的每一个文件,变成zipentry对象,放到压缩包中
tozip(src,zos,src.getName());
zos.close();
}
public static void tozip(File src,ZipOutputStream zos,String name) throws IOException {
File[] files = src.listFiles();
for (File file : files) {
if(file.isFile()){
//变成zipentry对象,放入压缩包中
ZipEntry entry = new ZipEntry(name+"\\"+file.getName());
zos.putNextEntry(entry);
//读取文件中的数据,写到压缩包中
int b;
FileInputStream fis = new FileInputStream(file);
while((b=fis.read()) != -1){
zos.write(b);
}
fis.close();
zos.closeEntry();
}else{
//文件夹,递归
tozip(file,zos,name+"\\"+file.getName());
}
}
}
}
Commons-io
Commons-io 是apache开源基金组织提供的一组有关io操作的开源工具包
作用:提高IO流的开发效率
Commons有很多有关各种的工具包
因为Commons-io是第三方组织写的,而第三方组织会把写好的代码打包成一个压缩包交给我们,在java当中,打包的压缩包的后缀是jar;如果想要使用这些类中的方法,需要把别人的代码导入自己的项目当中
Common-io使用步骤:
- 在项目中新建一个文件夹:lib,这个文件专门去存放第三方的jar包,
- 将jar包复制粘贴到lib文件夹中
- 右键点击jar包,选择 Add as Library -> 点击ok;表示把jar包跟我们的项目关联在一起
- 在类中导包使用
Common-io常用的方法:
FileUtils类(文件/文件夹相关) | 说明 |
---|---|
static void copyFile(File srcFile, File destFile) | 复制文件 |
static void copyDirectory(File srcDir, File destDir) | 复制文件夹到另一个文件夹里面 |
static void copyDirectoryToDirectory(File srcDir, File destDir) | 复制文件夹 |
static void deleteDirectory(File directory) | 删除文件夹 |
static void cleanDirectory(File directory) | 清空文件夹 |
static String readFileToString(File file, Charset encoding) | 读取文件中的数据变成字符串 |
static void write(File file, CharSequence data, String encoding) | 写出数据 |
IOUtils类(流相关) | 说明 |
---|---|
public static int copy(InputStream input, OutputStream output) | 复制文件 |
public static int copyLarge(Reader input, Writer output) | 复制大文件 |
public static String readLines(Reader input) | 读取数据 |
public static void write(String data, OutputStream output) | 写出数据 |
public class Test1 {
public static void main(String[] args) throws Exception {
//复制文件
FileUtils.copyFile(new File("a.txt"),new File("b.txt"));
//复制文件夹
FileUtils.copyDirectory(new File("D:\\aaa"),new File("bbb"));
//复制文件夹到bb文件夹里面
FileUtils.copyDirectoryToDirectory(new File("D:\\aaa"),new File("bb"));
//读取文件中数据
String s = FileUtils.readFileToString(new File("a.txt"), "UTF-8");
System.out.println(s);
}
}
Hutool工具包
它里面也有很多工具类,其中包含io工具类;他也是第三方写的,需要导入jar包
官网:https://hutool.cn/
APL文档:https://apidoc.gitee.com/dromara/hutool/
中文使用文档:https://hutool.cn/docs/#/
相关类 | 说明 |
---|---|
IoUtil | 流操作工具类 |
FileUtil | 文件读写和操作的工具类 |
FileTypeUtil | 文件类型判断工具类 |
WatchMonitor | 目录、文件监听 |
ClassPathResource | 针对ClassPath中资源的访问封装 |
FileReader | 封装文件读取 |
FileWriter | 封装文件写入 |
public class Test1 {
public static void main(String[] args) throws Exception {
//根据多个参数,自己拼接,创建一个file对象
File file = FileUtil.file("D:\\", "aa", "bb.txt");//D:\aa\bb.txt
System.out.println(file);
//根据参数创建文件,父级路径不存在,底层会自己创建
FileUtil.touch(file);
//把集合中的数据写到文件中,一个元素为一行
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"aa","bb");
FileUtil.appendLines(list,"a.txt","UTF-8");
//指定字符编码,把文件中的数据,读到集合中,一行为一个元素
List<String> strings = FileUtil.readLines("D:\\install_all\\IDEA\\IdeaProjects\\project3\\a.txt", "UTF-8");
System.out.println(strings);
}
}
properties
properties配置文件,里面的信息都是以键值对的形式存储的
properties是一个双列集合,拥有Map集合的所有特点
特点:有一些特有的方法,可以把集合中的数据,按照键值对的形式写到配置文件当中,也可以把配置文件中的数据,读取到集合中来
把数据存储到文件中
public class Test1 {
public static void main(String[] args) throws Exception {
//创建对象
Properties prop = new Properties();
//添加数据
prop.put("aa","1232");
prop.put("bb","243");
prop.put("cc","234");
//把集合中的数据以键值对的形式写到本地文件中
FileOutputStream fos = new FileOutputStream("c.txt");
prop.store(fos,"test");
fos.close();
}
}
文件中信息:
#test
#Thu Aug 08 23:18:27 CST 2024
aa=1232
bb=243
cc=234
从文件中读取数据到properties
public class Test1 {
public static void main(String[] args) throws Exception {
//创建对象
Properties prop = new Properties();
FileInputStream fis = new FileInputStream("c.txt");
prop.load(fis);
System.out.println(prop);//{aa=1232, bb=243, cc=234}
}
}
练习
- 事先准备一些学生信息,每个学生的信息占一行;每次被点到的学生,再次点到的概率在原先的基础上降低一半;带权重的随机
//类
public class Student {
private String name;
private String gender;
private int age;
private double weigh;
public Student() {
}
public Student(String name, String gender, int age, double weigh) {
this.name = name;
this.gender = gender;
this.age = age;
this.weigh = weigh;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getWeigh() {
return weigh;
}
public void setWeigh(double weigh) {
this.weigh = weigh;
}
@Override
public String toString() {
return name + "-" + gender + "-" + age + "-" + weigh;
}
}
public class Test4 {
public static void main(String[] args) throws IOException {
List<String> list = FileUtil.readLines(new File("a.txt"), "UTF-8");
ArrayList<Student> students = new ArrayList<>();
for (String s : list) {
String[] str = s.split("-");
Student student = new Student(str[0],str[1],Integer.parseInt(str[2]),Double.parseDouble(str[3]));
students.add(student);
}
//计算总权重
double weigh = 0;
for (Student student : students) {
weigh += student.getWeigh();
}
//计算每一个人的占比
double[] arr = new double[students.size()];
int num = 0;
for (Student student : students) {
arr[num] = student.getWeigh()/weigh;
num++;
}//[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
//计算每一个人的权重占比
for(int i = 1;i<arr.length;i++){
arr[i]=arr[i]+arr[i-1];
}//[0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.7999999999999999, 0.8999999999999999, 0.9999999999999999]
//随机抽取
//获取一个0-1之间的随机数
double number = Math.random();
//判断number在arr中的位置
//二分查找法,返回:-插入点-1
int index = -Arrays.binarySearch(arr, number)-1;
Student student = students.get(index);
System.out.println(student);
//修改这个学生的权重
student.setWeigh(student.getWeigh()/2);
//把集合中的数据写到文件中
BufferedWriter fw = new BufferedWriter(new FileWriter("a.txt"));
for (Student student1 : students) {
fw.write(student1.toString());
fw.newLine();
}
fw.close();
}
}
- 随机点名器;一个文件里面存储了班级同学的姓名,每一个姓名占一行;运行效果:被点到的学生不会再被点到,如果班级的所有学生都点完了,需要自动的重新开启第二轮的点名,不需要自己手动的操作本地文件
public class test3 {
public static void main(String[] args) throws IOException {
dianming("D:\\name1.txt","b.txt");
}
public static void dianming(String src, String dest) throws IOException {
File f = new File(dest);
if(!f.exists()){
f.createNewFile();
}
List<String> name = FileUtil.readLines(src, "UTF-8");
List<String> name1 = FileUtil.readLines(f, "UTF-8");
if(name.size()>name1.size()){
for (String s : name) {
if(!name1.contains(s)){
FileWriter fw = new FileWriter(f,true);
System.out.println(s);
fw.write(s);
fw.write("\r\n");
fw.close();
break;
}
}
}else{
FileWriter fw = new FileWriter(f);
fw.close();
dianming("D:\\name1.txt","b.txt");
}
}
}
多线程
- 进程:进程是程序最基本的执行实体
- 线程:线程是操作系统能够进行运算调度的最小单位,他被包含在进程之中,是进程中的实际运作单位
应用场景:软件中的耗时操作,如:拷贝、迁移大文件,加载大量的资源文件;所有的聊天软件;所有的后台服务器
有了多线程,就可以让程序同时做很多事情,提高效率
并发:在同一时刻,有多个指令在单个cpu上交替执行;指cpu在多个线程之中交替执行的
并行:在同一时刻,有多个指令在多个cpu上同时执行;例如:俩个cpu同时执行俩个线程
cpu是有很多种的,有2核4线程、4核8线程、8核16线程、16核32线程、32核64线程;这里的线程数量就表示电脑能同时运行多少条线程;以2核4线程为例,它可以同时运行4条线程的,所以如果电脑只有4条线程,那就不用切换的;如果电脑的线程很多,那四条线程就会在多个线程之间随机的切换去执行 ;所以并发和并行都可能在发生的
多线程的实现方式
多线程的实现方式有三种:
- 继承Thread类的方式进行实现
- 实现Runnable接口的方式进行实现
- 利用Callable接口和Future接口方式实现
Thread
步骤:
- 自己定义一个类继承Thread
- 重写run方法;在run方法里面写要执行的代码
- 创建子类的对象,并启动线程
//类
public class MyTread extends Thread{
@Override
public void run() {
//书写线程要执行的代码
for(int i=0;i<100;i++){
System.out.println(getName()+"HelloWorld");
}
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
MyTread t1 = new MyTread();
MyTread t2 = new MyTread();
//给线程取个名字
t1.setName("线程1");
t2.setName("线程2");
//启动线程
t1.start();
t2.start();
}
}
Runnable接口
步骤:
- 自己定义一个类实现Runnable接口
- 重写里面的run方法
- 创建自己的类对象
- 创建一个Thread类的对象,并开启线程
//类
public class MyTread implements Runnable{
@Override
public void run() {
//书写线程要执行的代码
for(int i=0;i<100;i++){
//获取到当前现成的对象
Thread t = Thread.currentThread();
System.out.println(t.getName()+"HelloWorld");
}
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
//创建对象
//表示多线程要执行的任务
MyTread mt = new MyTread();
//创建线程对象
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
//给线程设置名字
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
Callable接口和Future接口
前面俩种执行线程的代码,run方法是没有返回值的,所以是获取不了返回值的
这个方法的特点就是可以获取线程代码的返回值
步骤:
- 创建一个类实现Callable接口,接口的泛型表示返回值的类型
- 重写call方法(有返回值的,表示多线程运行的结果)
- 创建类对象(表示多线程要执行的任务)
- 创建Futrue接口的子类FutrueTask的对象(作用管理多线程运行的结果)
- 创建Tread类对象,并启动(表示线程)
//类
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 0; i < 100; i++) {
sum+=i;
}
return sum;
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
//创建对象
MyCallable mc = new MyCallable();
//创建FutureTask对象(管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft);
//启动线程
t1.start();
//获取多线程运行的结果
Integer i = ft.get();
System.out.println(i);
}
}
三种方式对比:
优点 | 缺点 | |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可以扩展性差,不能再继承其他类 |
实现Runnable接口、实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他类 | 编程相对复杂,不能直接使用Thread类中的方法 |
常见的成员方法
方法名称 | 说明 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
public static void yield() | 出让线程/礼让线程 |
public static void join() | 插入线程/插队线程 |
getName()和setName()方法的细节:如果我们没有给线程设置名字,线程也是有默认名字的;格式:Thread-x(x序号,从0开始);如果我们要给线程设置名字,可以用set方法进行设置,也可以用构造方法设置
Thread t = Thread.currentThread();
String name = t.getName();
sout(name);//main
该方法的细节:当JVM虚拟机启动之后,会自动的启动多条线程,其中有一条线程叫作main线程,它的作用是去调用main方法,并执行里面的代码,在以前,我们写的所有的代码,都是运行在main线程中
sout("111");
Thread.sleep(5000); //1秒=1000毫秒
sout("222");
打印完111后,停留五秒,再打印222;这个方法作用,哪条线程执行到这个方法,那么哪条线程就会在这里停留,当时间到了之后,线程就会自动醒来,继续执行下面的代码
线程的调度:
- 第一种是抢占式调度:多个线程在抢夺cpu的执行权,cpu在什么时候执行哪条线程是不确定的,执行多长时间也是不确定的,所以抢占式调度体现了一个随机性
- 第二种是非抢占式调度:那就是所有的线程轮流的执行,你一次我一次,执行的时间也是差不多的
在java当中,是采用抢占式调度的方式,线程的优先级越大,抢到cpu的概率也是越大的,在java当中,优先级分为十档,最小的为1,最大的为10,如果没有设置,默认是5
//创建线程要执行的参数对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t1 = new Thread(mr,"飞机");
Thread t2 = new Thread(mr,"坦克");
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
守护线程:当其他非守护线程执行完毕后,守护线程会陆续结束;就是当女神线程结束了,那么备胎线程也没有存在的必要
//类
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"@"+i);
}
}
}
//类
public class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"@"+i);
}
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
//把第二个线程设为守护线程
t2.setDaemon(true);
t1.start();
t2.start();
}
}
应用场景:例如,qq聊天时,打开了一个聊天窗口,同时并发送一个文件;聊天可以看成是一个线程,发送文件也可以看成是一个线程;当把聊天窗口给关闭了,发送文件的线程就没有执行下去的必要了,所以这个时候,发送文件的线程就会停止;此时发送文件的线程就是守护线程
插入线程:
MyThread t = new MyThread();
t.set("土豆");
s.start();
for(int i=0;i<10;i++){
sout("main线程"+i);
}
这个时候就是土豆线程和main线程抢夺cpu的执行权
此时,想让土豆线程执行完,再执行main线程
MyThread t = new MyThread();
t.set("土豆");
s.start();
//表示把t这个线程,插入到当前线程之前
//t:土豆
//当前线程:main线程
t.join();
for(int i=0;i<10;i++){
sout("main线程"+i);
}
线程安全
- 需求:某电影上映,共有100张票,有三个窗口卖票,设计一个程序模拟该电影院卖票
//类
public class MyThread extends Thread{
static int ticket;
@Override
public void run(){
while(true){
if(ticket<100){
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
ticket++;
sout("正在卖第"+ticket+"张票");
}else{
break;
}
}
}
}
public class Test{
public static void main(String[] args){
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
买票引发的安全问题:相同的票出现了多次,出现了超出范围的票
原因:当线程1执行while代码时,执行到sleep,睡一百毫秒,睡觉的时候是不会抢夺cpu的执行权的,cpu的执行权一定会被其他的线程给抢走;假设线程2抢到了,线程执行到sleep,睡一百毫秒,然后线程3抢到了,也睡一百毫秒;线程1醒来了,抢到了cpu的执行权,就会继续往下执行,ticket++,由0加到1;自增之后还没来得及打印,cpu的执行权就被线程2抢走了,线程2也对ticket自增,那么ticket就会从1自增到2;线程在执行代码的时候,cpu的执行权随时都有可能被抢走;假设又被线程3抢走了,线程3也自增,从2自增到3;那么这个时候不管是线程1还是线程2还是线程3,往下继续打印票号的时候,打印的都是3号票;假设票号现在是99,线程1进行来睡一百毫秒,然后线程2,再是线程3,线程1醒来,自增,从99自增到100;然后执行权被抢走;线程2从100自增到101;线程3抢到执行权,自增到102;最后打印就是102,超出范围的票号
解决:把操作共享数据的这段代码给锁起来;当线程1进来了,就算其他线程抢到了执行权,也得在代码外等着,只有当线程1执行完出来后,其他的线程才能够进去;这样就不会出现上面那些问题
同步代码块
作用:把操作共享数据的代码锁
关键字:synchronized
格式:
synchronized(锁对象){
操作共享数据的代码;
}
锁对象是任意对象,但是一定要是唯一的
特点:
- 锁默认打开,有一个线程进去了,锁自动关闭
- 里面的代码全部执行完毕,线程出来,锁自动打开
//类
public class MyThread1 extends Thread{
static int ticket = 0;
//锁对象,必须是唯一的
static Object obj = new Object();
@Override
public void run() {
while(true){
//同步代码块
synchronized (obj){
if(ticket<100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName()+"正在售卖第"+ticket+"张票");
}else{
break;
}
}
}
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
MyThread1 t1 = new MyThread1();
MyThread1 t2 = new MyThread1();
MyThread1 t3 = new MyThread1();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
细节:同步代码块不能写在while循环的外面,不然一个线程就会把全部的票给卖完,因为while被同步代码块包裹在里面的话,要全部执行完毕,锁才会打开;锁对象要是唯一的,如果不唯一,线程对应的不同的锁,就相当于没写同步代码块;我们一般写锁对象会写字节码文件,因为字节码文件是唯一的
synchronized (MyThread.class){
...
}
同步方法
同步方法就是把synchronized 关键字加到方法上
当我们要把方法的所有代码都锁起来时,就没必要用同步代码块了,直接把整个方法锁起来就行了
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){...}
特点:
- 同步方法就是锁住方法里面所有的代码
- 锁对象不能自己指定; 静态:this 非静态:当前类的字节码文件对象
//类
public class MyRunnable implements Runnable{
//不用静态,因为执行时,这个对象只会被创建一个
int ticket = 0;
@Override
public void run() {
while(true){
try {
if(method()) break;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public synchronized boolean method() throws InterruptedException {
if(ticket==100){
return true;
}else{
Thread.sleep(10);
ticket++;
System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票");
return false;
}
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr,"窗口1");
Thread t2 = new Thread(mr,"窗口2");
Thread t3 = new Thread(mr,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
StringBuilder类和StringBuffer类中的方法一模一样,唯一不同的是,StringBuffer的方法前面都有关键字synchronized,所以StringBuffer一般用于多线程;StringBuilder用于单线程
Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,
但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
void lock():获得锁 void unlock():释放锁
Lock是接口不能直接实例化,采用它的实现类ReentrantLock 来实例化
ReentrantLock的构造方法: ReentrantLock():创建一个ReentrantLock的实例
//类
public class MyThread1 extends Thread{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
if(ticket==100){
break;
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票");
}
lock.unlock();
}
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
MyThread1 t1 = new MyThread1();
MyThread1 t2 = new MyThread1();
MyThread1 t3 = new MyThread1();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
这样会出现100张票卖完但程序还没有停止的现象;因为当票是100时,假设是线程1抢到执行权,它到if语句进行判断时,等于100直接break,直接跳出了while语句,lock.unlock()语句就没有执行,锁没有被打开,其他线程一直被卡在门口,程序就一直在运行
有俩种解决方式
在break前面加一个打开锁的代码
public class MyThread1 extends Thread{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
if(ticket==100){
lock.unlock();
break;
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票");
}
lock.unlock();
}
}
}
或者 try…catch…finally语句的finally不管上面代码怎么样都得执行finally语句的,所以我们可以把打开锁的代码放在finally中
public class MyThread1 extends Thread{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
try{
if(ticket==100){
break;
}else{
Thread.sleep(100);
ticket++;
System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票");
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
死锁
死锁就是出现了锁的嵌套;死锁是一种错误
死锁就是出现锁的嵌套,比如有俩个锁,俩个线程,线程需要每次打开俩个锁才能进入运行代码;当出现线程1进行锁a,锁a被锁上了;线程2进入锁b,并把锁b给关上了;线程1在等锁b打开,线程2在等锁a打开;那么就会卡到这个地方,出现死锁现象
例如:
//类
public class MyThread{
static Object objA = new Object();
static Object objB = new Object();
@Override
public void run(){
while(true){
if("线程A".equals(getName())){
synchronized(objA){
sout("线程A拿到了A锁,准备拿B锁");
synchronized(objB){
sout("线程A拿到了B锁,完成一轮");
}
}
}else{
synchronized(objB){
sout("线程B拿到了B锁,准备拿A锁");
synchronized(objA){
sout("线程B拿到了A锁,完成一轮");
}
}
}
}
}
}
生产者和消费者(等待唤醒机制)
生产者消费者模式是十分经典的多线程协作的模式
假设生产者是厨师,消费者是吃货;还需要有一个桌子,核心思想是利用桌子来控制线程的执行,因为线程的执行是具有随机性的,就得有东西去控制线程;桌子有东西就吃货来吃,桌子没有东西就厨师来做;理想情况就是厨师先抢到cpu的执行权,此时桌子是没有面条的,所以厨师就要去做一碗面条,做完之后放在桌子上,叫吃货来吃;假设一开始是吃货先抢到cpu的执行权,此时桌子上是没有东西的,所以他只能先等待,代码当中是wait,一旦它等着时候,cpu的执行权一定会被厨师抢到,看一下桌子上有没有面条,没有,然后去做,然后放在桌子上,此时吃货还在wait,所以厨师需要去喊一下吃货代码叫作唤醒(notify),吃货就开始吃;所以生产者:1.判断桌子上是否有食物,2.有:等待 3.没有:制作食物 4.把食物放在桌子上 5.叫醒等待的消费者开吃 消费者:1.判断桌子上是否有食物, 2.如果没有就等待 3.如果有就开吃 4.吃完之后,唤醒厨师继续做
常见方法:
方法 | 说明 |
---|---|
void wait() | 当前线程等待,直到被其他线程唤醒 |
void notify() | 随机唤醒单个线程 |
void notifyAll() | 唤醒所有的线程 |
//类 桌子
public class Desk {
//控制生产者和消费者的执行
//是否有食物,0:没有 1:有
public static int food = 0;
//总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
//类 消费者
public class Food extends Thread{
@Override
public void run() {
while(true){
synchronized (Desk.lock) {
if (Desk.count == 0){
break;
}else{
if(Desk.food==0){
//没有食物
try {
Desk.lock.wait();//让锁跟当前线程进行绑定
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
Desk.count--;
//有就开始
System.out.println("正在吃,还能再吃"+Desk.count+"碗");
//吃完之后唤醒厨师
Desk.lock.notifyAll();//唤醒跟这把锁绑定的所有线程
//修改桌子上的状态
Desk.food=0;
}
}
}
}
}
}
//类 生产者
public class Cook extends Thread{
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count==0){
break;
}else{
if(Desk.food==1){
//有食物,等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
//没有食物,制作食物
Desk.food=1;
Desk.lock.notifyAll();
System.out.println("做了一碗面条");
}
}
}
}
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
//创建线程对象
Cook cook = new Cook();
Food food = new Food();
cook.setName("厨师");
food.setName("吃货");
cook.start();
food.start();
}
}
等待唤醒机制(阻塞队列方式实现)
阻塞队列就好比是连接生产者和消费者之间的管道,厨师做好面条之后就可以把面条放到管道当中,消费者就可以从管道中获取面条去吃,而且我们可以去规定管道当中最多可以放多少碗面条;中间这个管道就是阻塞队列;put数据时:放不进去,会等着,叫作阻塞;take数据:取出第一个数据,取不到会等着,叫作阻塞
阻塞队列的继承结构:
阻塞队列实现了四个接口,最顶层的接口是 Iterable,说明它可以用迭代器或者增强for来遍历的,还实现了 Collection 接口,说明它是一个单列集合,还实现了 Queue 接口,表示是队列,还实现了BlockingQueue 接口,表示是阻塞队列的意思,那些都是接口,不能直接创建对象,要创建实现类的对象,
实现类对象:ArrayBlockingQueue 和 LinkedBlockingQueue
ArrayBlockingQueue 底层是数组实现的,有界;而LinkedBlockingQueue底层是链表,无界,但不是真正的无界,最大值为int的最大值
//厨师
public class Cook extends Thread{
ArrayBlockingQueue<String> abq;
public Cook(ArrayBlockingQueue<String> abq) {
this.abq = abq;
}
@Override
public void run() {
while(true){
try {
abq.put("面条");
System.out.println("做了一个面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//吃货
public class food extends Thread{
ArrayBlockingQueue<String> abq;
public food(ArrayBlockingQueue<String> abq) {
this.abq = abq;
}
@Override
public void run() {
while(true){
//不断从队列中取出
try {
String take = abq.take();
System.out.println("吃了一个面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Test1 {
public static void main(String[] args) {
ArrayBlockingQueue<String> abq = new ArrayBlockingQueue<>(1);
Cook c = new Cook(abq);
food f = new food(abq);
c.start();
f.start();
}
}
线程的状态
-
新建状态(new)---- 创建线程对象
-
就绪状态(RUNNABLE)------ start方法
-
阻塞状态(BLOCKED)----------- 无法获得锁对象
-
等待状态(WAITING)-------------- wait方法
-
计时等待(TIMED_WAITING)------- sleep方法
-
结束状态(TERMINATED)----------- 全部代码执行完毕
练习
- 抢红包也用到了多线程。
假设:100块,分成了3个包,现在有5个人去抢
其中,红包是共享数据。
5个人是5条线程。
打印结果如下:
XXX抢到了XXX元
XXX抢到了XXX元
XXX抢到了XXX元
XXX没抢到
XXX没抢到
public class MyThread extends Thread{
//总金额
static BigDecimal money = BigDecimal.valueOf(100.0);
//个数
static int count = 3;
//最小抽奖金额
static final BigDecimal MIN = BigDecimal.valueOf(0.01);
@Override
public void run() {
synchronized (MyThread.class){
if(count == 0){
System.out.println(getName()+"没有抢到红包!");
}else{
//中奖金额
BigDecimal price;
if(count == 1){
price = money;
}else{
//获取抽奖范围
double bounds = money.subtract(BigDecimal.valueOf(count-1).multiply(MIN)).doubleValue();
Random r = new Random();
//抽奖金额
price = BigDecimal.valueOf(r.nextDouble(bounds));
}
//设置抽中红包,小数点后俩位,四舍五入
price = price.setScale(2, RoundingMode.HALF_UP);
//在总金额去掉对应的钱
money = money.subtract(price);
//红包少了一个
count--;
//打印
System.out.println(getName()+"抽中了"+price+"元");
}
}
}
}
public class Test1 {
public static void main(String[] args) {
MyThread p1 = new MyThread();
MyThread p2 = new MyThread();
MyThread p3 = new MyThread();
MyThread p4 = new MyThread();
MyThread p5 = new MyThread();
p1.setName("p1");
p2.setName("p2");
p3.setName("p3");
p4.setName("p4");
p5.setName("p5");
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
}
}
- 有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为
{10,5,20,50,100,200,500,800,2,80,300,700};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2
随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
每次抽出一个奖项就打印一个(随机)
抽奖箱1又产生了一个10元大奖
抽奖箱1又产生了一个100元大奖
抽奖箱1又产生了一个200元大奖
抽奖箱1又产生了一个800元大奖
抽奖箱2又产生了一个700元大奖
public class Jang extends Thread{
static ArrayList<Integer> list = new ArrayList<>();
static{
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
}
@Override
public void run() {
while(true){
synchronized (Jang.class){
if(list.size()==0){
break;
}else{
Collections.shuffle(list);
int price = list.remove(0);
System.out.println(getName()+"抽到"+price+"元");
}
}
try {
Thread.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//测试类
Jang j1 = new Jang();
Jang j2 = new Jang();
j1.setName("抽奖箱1");
j2.setName("抽奖箱2");
j1.start();
j2.start();
- 在上一题基础上继续完成如下需求:
每次抽的过程中,不打印,抽完时一次性打印(随机)》
在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元
public class Jiang extends Thread{
static ArrayList<Integer> list = new ArrayList<>();
static{
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
}
@Override
public void run() {
ArrayList<Integer> boxlist = new ArrayList<>();
while(true){
synchronized (Jiang.class){
if(list.size()==0){
System.out.println(getName()+boxlist);
break;
}else{
//继续抽奖
Collections.shuffle(list);
int price = list.remove(0);
boxlist.add(price);
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Test2 {
public static void main(String[] args) {
Jiang j1 = new Jiang();
Jiang j2 = new Jiang();
j1.setName("箱子1");
j2.setName("箱子2");
j1.start();
j2.start();
}
}
- 在上一题基础上继续完成如下需求:
在此次抽奖过程中,抽奖箱1总共产生了6个奖项,分别为:10,20,100,500,2,300
最高奖项为300元,总计额为932元
在此次抽奖过程中,抽奖箱2总共产生了6个奖项,分别为:5,50,200,800,80,700
最高奖项为800元,总计额为1835元
在此次抽奖过程中抽奖箱2中产生了最大奖项,该奖项金额为800元,
public class Jiang1 implements Callable<Integer> {
static ArrayList<Integer> list = new ArrayList<>();
static{
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
}
@Override
public Integer call() throws Exception {
int max;
ArrayList<Integer> boxlist = new ArrayList<>();
while(true){
synchronized (Jiang.class){
if(boxlist.size()==6){
Collections.sort(boxlist);
max = boxlist.get(boxlist.size()-1);
System.out.println(boxlist+"最大值为:"+max);
break;
}else{
Collections.shuffle(list);
int money = list.remove(0);
boxlist.add(money);
}
}
}
return max;
}
}
public class Test3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Jiang1 j1 = new Jiang1();
FutureTask<Integer> ft1 = new FutureTask<>(j1);
FutureTask<Integer> ft2 = new FutureTask<>(j1);
Thread t1 = new Thread(ft1);
Thread t2 = new Thread(ft2);
t1.setName("箱子1");
t2.setName("箱子2");
t1.start();
t2.start();
Integer i = ft1.get();
System.out.println(i);
System.out.println(ft2.get());
}
}
线程池
以前写多线程的弊端:
- 用到线程时就创建
- 用完之后线程消失
线程池作用:相当于一个容器,用来存放线程池
原理:刚开始线程池里面是空的,没有线程,当给线程池提交一个任务的时候,线程池本身就会自动的去创建一个线程,拿着这个线程去执行任务,执行完了就把线程还回去;当第二次提交一个线程的时候,他就不需要再次创建线程了,而是拿着已经存在的线程去执行任务,执行完了再还回去,这就是线程池的核心原理
特殊情况:当提交第二个任务时,线程还正在执行第一个任务,线程还没有还回去;此时线程池就会创建第二个新的线程,拿着新的线程去执行第二个任务;如果再提交几个新任务,那么线程池就会再创建几个新的线程去执行新任务,执行完把线程还到线程池
线程池创建线程数量也是有上限的,如果设置上限是3,那么这三个线程就会执行前面三个任务,后面的任务就排队等着
线程池的代码实现:
- 创建线程池
- 提交任务,底层会自动去创建线程或者复用已经存在的线程
- 所有的任务全部执行完毕,关闭线程池;一般线程池是不会关闭的,因为服务器是24小时运行的,就是随时随地会有新任务要执行,那么线程池也就不会关闭
Executors:线程池的工具类,通过调用方法返回不同类型的线程池对象
方法 | 说明 |
---|---|
public static ExecutorService newCachedThreadPool() | 创建一个没有上限的线程池 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建有上限的线程池 |
//类
public class Myrunable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-----");
}
}
public class Test1 {
public static void main(String[] args) throws InterruptedException {
//获取线程池对象
ExecutorService pool = Executors.newCachedThreadPool();
//提交任务
pool.submit(new Myrunable());
Thread.sleep(1000);
pool.submit(new Myrunable());
Thread.sleep(1000);
pool.submit(new Myrunable());
//销毁线程池
pool.shutdown();
}
}
//类
public class Myrunable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
public class Test1 {
public static void main(String[] args) throws InterruptedException {
//获取线程池对象
ExecutorService pool = Executors.newFixedThreadPool(3);
//提交任务
pool.submit(new Myrunable());
pool.submit(new Myrunable());
pool.submit(new Myrunable());
pool.submit(new Myrunable());
pool.submit(new Myrunable());
//销毁线程池
pool.shutdown();
}
}
除了用工具类Executors来创建线程池外,还可以用ThreadPoolExecutor来创建
用工具类Executors来创建线程池对象虽然方便,但不够灵活;比如提交的任务比较多,队伍的长度就不好去修改;我们可以自己定义线程池,自定义线程池;用ThreadPoolExecutor来创建,就更加灵活
- 核心元素一:正式员工------- 核心线程数量(不能小于0)
- 核心元素二:餐厅最大员工数-------- 线程池中最大线程的数量(最大数量>=核心线程数量)
- 核心元素三:临时员工空闲多长时间被辞退(值)-------- 空闲时间(值)(不能小于0)
- 核心元素四:临时员工空闲多长时间被辞退(单位)-------- 空闲时间(单位)(用TimeUnit指定)
- 核心元素五:排队的客户------------- 阻塞队列(不能为null)
- 核心要素六:从哪里招人-------------- 创建线程的方式(不能为null)
- 核心要素七:当排队的人数过多,超出顾客请下次再来(拒绝服务)--------要执行的任务过多时的解决方案(不能为null)
刚开始线程池是空的,假设设置核心线程是3,临时线程也是3,表示线程池当中最多只有六条线程;核心线程就跟核心员工一样不会被销毁,临时线程如果一段时间不工作,就会被销毁;假设刚开始提交了三个任务,此时就会创建三条线程去执行这三个任务;如果提交了五个任务,会创建三个线程来处理,因为核心线程最多只能有三个,那么剩余的俩个任务就会在后面排队等待,等有了空闲的线程后面的俩个任务才会被执行;假设现在提交了八个任务,会先创建三个线程处理前面三个任务,剩余的任务就会在后面排队,假设现在定义了队伍长度最长为3,那么第四、五、六任务就会在队列中排队等待,这时线程池就会创建临时线程去处理第七、八任务;核心线程都在忙并且队伍中都排满了,才会去创建临时线程;假设提交了十个任务,此时已经超过了核心线程数量加临时线程数量加队伍长度,这时还是会先创建三个线程去处理前面三个任务,然后把第四、五、六任务放在队伍中排队等待,接着创建三个临时线程去执行第七、八、九任务,这时后面还有任务就会触发任务拒绝策略,就是舍弃掉
任务拒绝策略:
是ThreadPoolExecutor类的静态内部类
任务拒绝策略 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 默认策略:丢弃任务并抛出RejectedExecutionException异常 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常,这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 调用任务的run()方法绕过线程池直接执行 |
public class Test{
public static void main(String[] args){
//创建对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数据
6, //最大线程数量
60, //空闲线程最大存活时间
TimeUnit.SECONDS, //时间单位
new ArrayBlockingQueue<>(3), //任务队列
Executor.defaultThreadFactory(), //创建线程工厂
new ThreadPoolExecutor.AbortPolicy() //任务的拒绝策略
);
//提交任务
pool.submit(...);
....
}
}
最大并行数
最大并行数跟cpu有关,比如四核八线程;四核就好比cpu有四个大脑,能同时并行的去做四件事情;但是因特尔发明了超线程技术,它可以把原本的四个大脑虚拟成八个,就是八线程;所有针对四核八线程而言,最大并行数为8
public class Test1 {
public static void main(String[] args) {
//向java虚拟机返回可用处理器的数目
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count);//16
}
}
线程池多大合适
线程池的大小是有公式的,得先看项目是什么类型的
项目可分为俩种类型:
-
cpu密集型运算:项目中计算比较多,但是读取本地文件/数据库的操作比较少 采用 最大并行数+1 就可以实现最优的cpu利用率
-
IO密集型运算:项目中读取本地文件或者数据库的操作比较多 当在进行IO操作或者操作数据库的时候,cpu就闲下来了,这时就可以利用多线程技术把闲下来的时间利用起来,从而提高cpu的利用率
公式:最大并行数 x 期望cpu利用率 x (总时间 / cpu计算时间) 总时间是cpu计算时间+等待时间
比如,从本地文件中,读取俩个数据,并进行相加;这里面有俩个操作,操作一:读取俩个数据 操作二:相加 读取俩个数据是跟硬盘有关系的,跟cpu没有关系,只有下面的相加是跟cpu有关系的;假设俩个操作都是一秒钟,那么总时间是俩秒,cpu计算时间是一秒;假设是四核八线程的,根据公式就可以得出 8 x 100% x (2/1)= 16 ,所以得出线程池总大小是16
cpu的计算时间和cpu等待时间是需要我们用工具来测试的,比如一个工具 thread dump 就可以测试到
网络编程
在网络通信协议下,不同计算机上运行的程序,进行的数据传输
应用场景:即时通信、网游对战、金融证券、国际贸易、邮件等
不管是什么应用场景,都是计算机跟计算机之间通过网络进行数据传输
java可以使用java.net包下的技术轻松开发出常见的网络应用程序
常见的软件架构:
- BS:BS是Browser/Server俩个单词缩写,浏览器/服务器;只需要一个浏览器,用户通过不同的地址;客户访问不同的服务器
- CS:CS是Client/Server俩个单词的缩写 客户端/服务端,所以采取这种架构的方式是需要在用户本地需要下载并安装客户端程序,在远程有一个服务端程序;比如qq
区别:无论是BS还是CS,客户端和浏览器都仅仅是把数据展示给用户;在项目当中真正的业务逻辑是在服务器当中;先说BS架构,在市场上所有通过浏览器访问的都是BS架构,比如说网页游戏、购物商场;以网页游戏来讲BS架构的优缺点,所有的网页游戏都不需要下载客户端,只用输入网址就可以玩了,非常方便,但是游戏画面非常烂;在BS架构中,浏览器中要显示的图片、特效、背景音乐等在本地文件中是没有的,所有的东西都需要通过网络传过去,不稳定
所以BS架构的优缺点:
- 不需要开发客户端,只需要页面+服务端
- 用户不需要下载,打开浏览器就能使用
- 如果应用过大,用户体验受到影响
再看CS架构,需要我们下载安装的都是CS架构,安装包里面包含图片、背景音乐等等,在游戏中,就不需要服务器通过网络传输这些数据给客户端了,只需要告诉客户端现在该显示哪张图片就可以了
CS架构的优缺点:
- 画面可以做的非常精美,用户体验好
- 需要开发客户端,也需要开发服务端
- 用户需要下载和更新,太麻烦
CS适合制定专业化的办公软件,如IDEA、网游;BS适合移动互联网应用,可以在任何地方随时访问的系统
网络编程三要素
俩台电脑,一台给另一台发送数据,需要哪些东西
一、确定对方电脑在互联网上的地址;叫作IP 是唯一的;光有IP不够,因为电脑上有很多应用,不能确定是哪一个应用;所以第二个需要确定的是接收数据的软件,叫作端口号;一个端口号只能被一个软件绑定使用;第三个是确定网络传输的规则,叫作协议
三要素:
- IP:设备在网络中的地址,是唯一的标识
- 端口号:应用程序在设备中唯一的标识
- 协议:数据在网络中传输的规则,常见的协议有UDP、TCP、Http、https、ftp
IP
全称:Internet Protocol,是互联网协议地址,也称IP地址
是分配给上网设备的数字标签,就是上网设备在网络中的地址,是唯一的
常见的IP分类:IPv4、IPv6
IPv4:
全称:互联网通信协议第四版
采用32位地址长度,分成四组;32个bit四个字节变成二进制很长,不好记也不好用;所以出现了点分十进制表示法,他会把八个bit分为一组,总共就是四组,每一组再转成十进制,中间用点进行区分,转化的时候是没有负数一说的,每一组的范围是0-255,例如192.168.1.66;但IPv4也是有弊端的,总共数量也不到43亿个,不够用;为了解决不够用的问题,就出现了IPv6
IPv6:
全称:互联网通信协议第六版
由于互联网的蓬勃发展,IP地址的需求量越来越大,而IPv4的模式下的IP的总数有限
采取128位地址长度,分成了8组,每一组是16个bit,最多有2的128次方个
IPv6也不会直接去写二进制,而是采用冒分十六进制表示法;就是把上面每一组转成十六进制,然后每一组用冒分分开
IPv4的地址分类形式:
公网地址(万维网使用)和私有地址(局域网使用)
192.168.开头的就是私有地址,范围即为192.168.0.0-192.168.255.255,专门为组织机构内部使用,以此节省IP
比如,网吧里面的电脑,不是每一台电脑连接外网的时候都有一个公网的IP,他们往往是共享同一个公网IP,再由路由器给每一台电脑分配局域网IP,这样就可以实现节约IP的效果
特殊IP:127.0.0.1,也可以是localhost,是回送地址也称本地回环地址,也称本机IP,永远只会寻找当前所在本机
假设局域网中有六台电脑,这六台电脑的IP都是由路由器所分配的,假设我自己的电脑的IP是192.168.1.100;假设我现在发送数据时也往这个IP上发送数据,此时这个数据是先发送到路由器,路由器再找到你当前的IP,这样才能实现数据的发送,但是此时会有一个小细节,就是每一个路由器给你分配的IP是有可能不一样的,比如在教室的时候IP是192.168.1.100,但当回宿舍的时候就不一定是这个了,换了一个地方上网,局域网IP有可能不一样;但是我们是往127.0.0.1上发送数据,此时数据是不经过路由器的,数据经过网卡的时候,网卡发现是127.0.0.1时,他就直接把这个数据给你自己发过来了,不管在哪里上网都是这样的,这就是俩者区别;所以建议以后是自己给自己发送数据IP就写127.0.0.1
常用的CMD命令:
- ipconfig:查看本机ip地址
- ping:检查网络是否连通
InetAddress的使用
InetAddress是java用来表示ip的类
这个类是没有对外提供构造方法,只能通过静态方法来创建对象
方法 | 说明 |
---|---|
static InetAddress getByName(String host) | 确定主机名称的IP地址,主机名称可以是机器名称,也可以是ip地址 |
String getHostName() | 获取此ip地址的主机名 |
String getHostAddress() | 返回文本显示中的ip地址字符串 |
public class Test1 {
public static void main(String[] args) throws UnknownHostException {
//获取InetAddress对象
//ip的对象 一台电脑的对象
InetAddress address = InetAddress.getByName("feng");
System.out.println(address);//feng/172.22.64.1
String name = address.getHostAddress();
System.out.println(name);//172.22.64.1
String ip = address.getHostName();
System.out.println(ip);//feng
}
}
端口号
应用程序在设备中唯一的标识
端口号:由俩个字节表示的整数,取值范围为:0-65535
其中0-1023之间的端口号用于一些知名的网络服务或者应用
我们自己使用1024以上的端口号就可以了
注意:一个端口号只能被一个应用程序使用
协议
计算机网络中,连接和通信的规则被称为网络通信协议
- OSI参考模型:世界互联协议标准,全球通信规范,单模型过于理想化,未能在互联网上进行推广
- OSI/IP参考模型:事实上的国际标准
OSI参考模型 | TCP/IP参考模型 | TCP/IP参考模型各层对应协议 | 面向哪些 |
---|---|---|---|
应用层 表示层 传输层 | 应用层 | HTTP、FTP、TeLnet、DNS… | 一把是应用程序需要关注的,如浏览器,邮箱、程序员一般在这一层开发 |
传输层 | 传输层 | TCP、UDP、… | 选择传输使用的TCP、UDP协议 |
网络层 | 网络层 | IP、ICMP、ARP… | 封装自己的ip,对方的ip等信息 |
数据链路层、物理层 | 物理+数据链路层 | 硬件设备 | 转换成二进制利用物理设备传输 |
在osi参考模型中,它是把整个数据的传输分为七层,应用层、表示层、传输层、网络层、数据链路层、物理层,发送数据的时候对方的电脑也有这七层,我们的代码是运行在最上层的应用层,如果我们要发送一条数据,在自己的电脑当中,他会一层一层的往下,在最下面的物理层他会把数据转成二进制再去传给对方的电脑,对方的电脑接收到二进制后,会进行解析,再进行一层层的往上传到最上层的应用层,这样我们的程序就可以接收到数据了,这个就是osi参考模型;但是这个参考模型太过于复杂和理想化,所以后来又出现了TCP/IP模型
在tcp/ip参考模型当中,他是把应用层、表示层、会话层进行了合并,三者合一变成应用层,下面的传输层和网络层没有发生变化,最后的俩层数据链路层和物理层进行了合并变成物理链路层,简化之后,流程变得简单了,也大大的减少了资源的消耗,所以这个模型一直沿用至今
UDP协议:
- 用户数据报协议
- UDP是面向无连接的通信协议。
- 速度快,有大小限制,一次最多发送64k,数据不安全,易丢失数据
比如:网络会议、语音通话、在线视频,他们的数据是用UDP协议传输的
TCP协议:
- 传输控制协议TCP
- TCP协议是面向连接的通信协议
- 速度慢,没有大小限制,数据安全
比如:下载软件、文字聊天、发送邮件,是用TCP协议传输数据的
UDP通信程序
发送数据:
- 创建发送端的DatagramSocket对象
- 数据打包(DatagramPacket)
- 发送数据
- 释放资源
public class Test1 {
public static void main(String[] args) throws IOException {
//创建DatagramSocket对象
//细节
//绑定端口,以后就是通过这个端口往外发送
//空参:所有可用的端口随机一个进行使用
//有参:指定端口号进行绑定
DatagramSocket ds = new DatagramSocket();
//打包数据
String str = "你好";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 10086;
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, address, port);
//发送数据
ds.send(datagramPacket);
//释放资源
ds.close();
}
}
接收数据:
- 创建接收端的DatagramSocket对象
- 接收打包好的数据
- 解析数据包
- 释放资源
public class Test2 {
public static void main(String[] args) throws IOException {
//创建DatagramSocket对象
//细节:
//在接收的时候,一定要绑定端口
//而且绑定的端口一定要跟发送的端口保持一致
DatagramSocket ds = new DatagramSocket(10086);
//接收数据包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);
//该方法是阻塞的
//程序在这一步会死等,等发送端发来信息
ds.receive(dp);
//解析数据包
byte[] data = dp.getData();
int len = dp.getLength(); //获取多少个字节数据
InetAddress address = dp.getAddress();//获取从哪台电脑发来的
int port = dp.getPort();//对方是从哪个端口发来的
System.out.println("接收到数据"+new String(data,0,len));
System.out.println("该数据是从"+address+"这台电脑中的"+port+"这个端口发出的");
//释放资源
ds.close();
}
}
-
练习
按照下面的要求实现程序
UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束
UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收
public class Test3 {
public static void main(String[] args) throws IOException {
//发送端
Scanner sc = new Scanner(System.in);
DatagramSocket ds = new DatagramSocket();
while(true){
String str = sc.next();
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 10086;
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
ds.send(dp);
if(str.equals("886")){
break;
}
}
ds.close();
}
}
public class Test4 {
public static void main(String[] args) throws IOException {
//接收端
DatagramSocket ds = new DatagramSocket(10086);
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);
while(true){
ds.receive(dp);
InetAddress address = dp.getAddress();
byte[] data = dp.getData();
int length = dp.getLength();
int port = dp.getPort();
String str = new String(data,0,length);
System.out.println(address+"("+port+")"+":"+str);
if(str.equals("886")){
break;
}
}
ds.close();
}
}
UDP的三种通信方式
- 单播:一个电脑给一个电脑发送数据
- 组播:一个电脑给一组电脑发送数据
- 广播:一个电脑给局域网里所有电脑发送数据
组播地址:224.0.0.0-239.255.255.255,其中,224.0.0.0-224.0.0.255为预留的组播地址
广播地址:255.255.255.255
组播的实现代码
//发送端
public class Test6 {
public static void main(String[] args) throws IOException {
//创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket();
//创建DatagramPacket对象
String str = "你好";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("224.0.0.2");
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
//发送
ms.send(dp);
//释放资源
ms.close();
}
}
//接收端1
public class Test7 {
public static void main(String[] args) throws IOException {
//创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket(10000);
//将当前本机添加到224.0.0.1的这一组中
InetAddress address = InetAddress.getByName("224.0.0.2");
ms.joinGroup(address);
//创建DatagramPacket数据包对象
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);
//接收数据
ms.receive(dp);
//解析数据
byte[] data = dp.getData();
int length = dp.getLength();
String ip = dp.getAddress().getHostAddress();
String name = dp.getAddress().getHostName();
System.out.println("ip为:"+ip+",主机名为:"+name+"的人,发来一条数据:"+new String(data,0,length));
//释放资源
ms.close();
}
}
//接收端2
public class Test8 {
public static void main(String[] args) throws IOException {
//创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket(10000);
//将当前本机添加到224.0.0.1的这一组中
InetAddress address = InetAddress.getByName("224.0.0.2");
ms.joinGroup(address);
//创建DatagramPacket数据包对象
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);
//接收数据
ms.receive(dp);
//解析数据
byte[] data = dp.getData();
int length = dp.getLength();
String ip = dp.getAddress().getHostAddress();
String name = dp.getAddress().getHostName();
System.out.println("ip为:"+ip+",主机名为:"+name+"的人,发来一条数据:"+new String(data,0,length));
//释放资源
ms.close();
}
}
广播代码实现
//发送端
public class Test1 {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
//打包数据
String str = "你好";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("255.255.255.255");
int port = 10086;
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, address, port);
//发送数据
ds.send(datagramPacket);
//释放资源
ds.close();
}
}
接收端跟单播的接收端一样,发送端跟单播的发送端也一样,只是端口号改成255.255.255.255
TCP通信程序
TCP通信协议是一种可靠的网络协议,它在通信的俩端各建立一个Socket对象
通信之前要保证连接已经建立
通过Socket产生的IO流来进行网络通信
客户端:
- 创建客户端的Socket对象(Socket)与指定的服务端连接
Socket(String host, int port)
- 获取输出流,写数据
OutputStream getOutStream()
- 释放资源
void close()
服务端:
- 创建服务端的Socket对象(ServerSocket)并绑定端口
ServerSocket(int port)
- 监听客户端连接,返回一个Socket对象
Socket accept()
- 获取输入流,读数据,并把数据显示在控制台
InputStream getInputStream()
- 释放资源
void close()
//客户端
public class test1 {
public static void main(String[] args) throws IOException {
//创建Socket对象并连接服务器
//细节:在创建对象的同时会连接服务端
//如果连接不上就会报错
Socket socket = new Socket("127.0.0.1",10000);
//可以从连接通道中获取输出流
OutputStream os = socket.getOutputStream();
//写出数据
os.write("aaa".getBytes());
//释放资源
os.close();
socket.close();
}
}
public class test2 {
public static void main(String[] args) throws IOException {
//创建对像并绑定端口
ServerSocket ss = new ServerSocket(10000);
//监听客户端的连接
Socket socket = ss.accept();
//从连接通道中获取输入流读取数据
InputStream is = socket.getInputStream();
int b;
while((b = is.read()) != -1){
System.out.println((char)b);
}
//释放资源
socket.close();
ss.close();
}
}
这种字节输出流不能传输中文,否则会乱码;我们需要把服务端的字节输入流包装成转换流就可以了
//服务端
public class test2 {
public static void main(String[] args) throws IOException {
//创建对像
ServerSocket ss = new ServerSocket(10000);
//监听客户端的连接
Socket socket = ss.accept();
//从连接通道中获取输入流读取数据
InputStream is = socket.getInputStream();
//把字节输入流包装成转换流
InputStreamReader isr = new InputSteamReader(is);
int b;
while((b = isr.read()) != -1){
System.out.println((char)b);
}
//释放资源
socket.close();
ss.close();
}
}
先运行服务端,再运行客户端;服务端运行时,监听客户端的连接,这里有一个“三次握手协议”来保证连接建立;连接之后就建立了一个通道,客户端可以从通道里面获取字节输入流来把数据传到通道里,客户端可以从通道里面获取一个字节输入流,来把数据从通道中获取出来;之后 socket.close() 关闭通道,但在通道关闭之前要确保客户端已经把通道中的所有数据全部给获取出来;所以这里有一个“四次挥手”协议来断开连接确保通道里面的所有数据已经处理完毕了
TCP通信程序(三次握手)
客户端向服务器发出连接的请求,等待服务器确认;服务器向客户端返回一个响应,告诉客户端收到了请求;客户端向服务器再次发出确认信息,连接建立; 这就是三次握手协议
四次挥手
确保连接断开,并且数据处理完毕
客户端向服务器发出取消连接请求;服务器向客户端返回一个响应表示收到客户端的取消请求,服务端将最后的数据处理完毕;服务器向客户端发出确认取消信息;客户端再次发送确认消息,连接取消;这就是四次挥手协议
练习
- 客户端:多次发送数据 服务端:接收多次数据并打印
//客户端
public class test3 {
public static void main(String[] args) throws IOException {
//创建socket对象并连接服务端
Socket s = new Socket("127.0.0.1",10000);
//写出数据
OutputStream os = s.getOutputStream();
Scanner sc = new Scanner(System.in);
while(true){
System.out.println("输入要发送的信息:");
String string = sc.nextLine();
if("886".equals(string)){
break;
}
os.write(string.getBytes());
}
//释放资源
s.close();
}
}
//服务端
public class test4 {
public static void main(String[] args) throws IOException {
//创建对象绑定10000端口
ServerSocket ss = new ServerSocket(10000);
//等待客户端连接
Socket socket = ss.accept();
//读取数据
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
int b;
//细节:
//read方法会从连接通道中读取数据
//但是,需要有一个结束标记,要客户端那里结束,此处循环才会停止
//否则,程序就会一直停在read方法这里,等待读取下面的数据
while((b=isr.read()) != -1){
System.out.print((char)b);
}
//释放资源
socket.close();
ss.close();
}
}
- 客户端:发送一条数据,接收服务端的反馈的消息并打印
服务端:接收数据并打印,再给客户端反馈消息
//客户端
public class test3 {
public static void main(String[] args) throws IOException {
//创建socket对象并连接服务端
Socket s = new Socket("127.0.0.1",10000);
//写出数据
OutputStream os = s.getOutputStream();
os.write("你好".getBytes());
//写出一个结束标记
s.shutdownOutput();
//接收服务器的回写数据
InputStreamReader isr = new InputStreamReader(s.getInputStream());
int b;
while((b=isr.read()) != -1){
System.out.print((char)b);
}
//释放资源
s.close();
}
}
//服务端
public class test4 {
public static void main(String[] args) throws IOException {
//创建对象绑定10000端口
ServerSocket ss = new ServerSocket(10000);
//等待客户端连接
Socket socket = ss.accept();
//读取数据
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
int b;
//细节:
//read方法会从连接通道中读取数据
//但是,需要有一个结束标记,此处循环才会停止
//否则,程序就会一直停在read方法这里,等待读取下面的数据
while((b=isr.read()) != -1){
System.out.print((char)b);
}
//回写数据
OutputStream os = socket.getOutputStream();
os.write("你也好".getBytes());
//释放资源
socket.close();
ss.close();
}
}
- 客户端:将本地文件上传到服务器,接收服务器的反馈
服务端:接收客户端上传的数据,上传完毕后给出反馈
//服务端
public class test6 {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
Socket socket = ss.accept();
//读取数据并保存到文件中
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("ab.png"));
byte[] bytes = new byte[1024];
int len;
while((len=bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//关流,否则数据在缓冲区没传到文件中
bos.close();
//回写数据
OutputStream os = socket.getOutputStream();
os.write("ok了".getBytes());
//释放资源
socket.close();
ss.close();
}
}
//客户端
public class test5 {
public static void main(String[] args) throws IOException {
Socket s = new Socket("127.0.0.1",10000);
//读取本地文件
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\练习parcham\\爬虫\\爬取图片\\美女图片\\0V95NN8Q92W5_lead-720x1280.png"));
BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
byte[] bytes = new byte[1024];
int len;
while((len=bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
bos.flush();
//往服务端写出结束标记
s.shutdownOutput();
//接收服务端的回写数据
InputStreamReader isr = new InputStreamReader(s.getInputStream());
int b;
while((b=isr.read()) != -1){
System.out.print((char)b);
}
//释放资源
s.close();
}
}
- 解决文件名重复的问题
当我们运行时,文件名存在,就会把之前的文件给覆盖掉;
UUID 类就可以帮我们随机一个文件名并且不重复的来生成文件名,并且每次运行都不唯一
public class test4 {
public static void main(String[] args) throws IOException {
System.out.println(UUID.randomUUID());//6301206d-1a25-4b83-be0b-976889b5e3ff
System.out.println(UUID.randomUUID().toString().replace("-",""));//f1008026be474a4f9bcc54a6f7c17e75
}
}
//服务端
public class test6 {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
Socket socket = ss.accept();
//读取数据并保存到文件中
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
String name = UUID.randomUUID().toString().replace("-","");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(name+".png"));
byte[] bytes = new byte[1024];
int len;
while((len=bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
//关流,否则数据在缓冲区没传到文件中
bos.close();
//回写数据
OutputStream os = socket.getOutputStream();
os.write("ok了".getBytes());
//释放资源
socket.close();
ss.close();
}
}
- 上传文件(多线程版)
想要服务器不停止,能接收很多用户上传的图片
提示:可以用循环或者多线程,但是循环不合理,最优解法是(循环+多线程)改写
//客户端
public class test8 {
public static void main(String[] args) throws IOException {
//创建socket对象并连接服务器
Socket s = new Socket("127.0.0.1",10000);
//读取本地文件并写到服务器中
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\练习parcham\\爬虫\\爬取图片\\美女图片\\6R63LXS0RG2F_lead-720x1280.png"));
BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
byte[] bytes = new byte[1024];
int len;
while((len=bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
bos.flush();
//往服务器中写出结束标记
s.shutdownOutput();
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
int b;
while((b=br.read()) != -1){
System.out.print((char)b);
}
}
}
//线程类
public class MyThread implements Runnable{
Socket socket;
public MyThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//读取数据保存到本地中
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
String name = UUID.randomUUID().toString().replace("-", "");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(name+".png"));
int len;
byte[] bytes = new byte[1024];
while((len=bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
bos.flush();
//回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("上传成功");
bw.newLine();
bw.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
//服务端
public class test5 {
public static void main(String[] args) throws IOException {
//创建对象并绑定端口
ServerSocket ss = new ServerSocket(10000);
while(true){
//等待客户端来连接
Socket socket = ss.accept();
//开启一条线程
//一个用户就对应服务端的一条线程
new Thread(new MyThread(socket)).start();
}
}
}
- 上传文件(线程池优化)
频繁的创建线程并销毁非常浪费系统资源,所以需要用线程池优化
//客户端
public class test8 {
public static void main(String[] args) throws IOException {
//创建socket对象并连接服务器
Socket s = new Socket("127.0.0.1",10000);
//读取本地文件并写到服务器中
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\练习parcham\\爬虫\\爬取图片\\美女图片\\6R63LXS0RG2F_lead-720x1280.png"));
BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
byte[] bytes = new byte[1024];
int len;
while((len=bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
bos.flush();
//往服务器中写出结束标记
s.shutdownOutput();
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
int b;
while((b=br.read()) != -1){
System.out.print((char)b);
}
}
}
//线程类
public class MyThread implements Runnable{
Socket socket;
public MyThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//读取数据保存到本地中
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
String name = UUID.randomUUID().toString().replace("-", "");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(name+".png"));
int len;
byte[] bytes = new byte[1024];
while((len=bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}
bos.flush();
//回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("上传成功");
bw.newLine();
bw.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
//服务端
public class test5 {
public static void main(String[] args) throws IOException {
//创建对象并绑定端口
ServerSocket ss = new ServerSocket(10000);
//创建线程池对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数量
16, //线程池总大小
60, //空闲时间
TimeUnit.SECONDS, //空闲时间(单位)
new ArrayBlockingQueue<>(2), //队列
Executors.defaultThreadFactory(), //线程池工厂,让线程池如何创建线程对象
new ThreadPoolExecutor.AbortPolicy() //阻塞队列
)
while(true){
//等待客户端来连接
Socket socket = ss.accept();
//开启一条线程
//一个用户就对应服务端的一条线程
new Thread(new MyThread(socket)).start();
}
}
}
服务端也可以改成
public class test5 {
public static void main(String[] args) throws IOException {
//创建对象并绑定端口
ServerSocket ss = new ServerSocket(10000);
//创建线程池
ExecutorService pool = Executors.newCachedThreadPool();
while(true){
//等待客户端来连接
Socket socket = ss.accept();
//开启一条线程
//一个用户就对应服务端的一条线程
pool.submit(new MyThread(socket));
}
}
}
-
BS(接收浏览器的消息并打印)
客户端:不需要写,浏览器就是客户端 服务端:接收数据并打印
//创建对象绑定10000端口
ServerSocket ss = new ServerSocket(10000);
//等待客户端连接
Socket socket = ss.accept();
//读取数据
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
int b;
while((b=isr.read()) != -1){
sout((char)b);
}
//释放资源
socket.close();
ss.close();
先运行服务器代码,然后打开一个浏览器,在上面输入 127.0.0.1:10000,然后回车;此时服务器就打印出浏览器发出的请求数据
大练习
- 利用TCP协议,做一个带有登录,注册的无界面,控制版的多人聊天室。
在当前模块下新建txt文件,文件中保存正确的用户名和密码
文件内容如下:
//左边是用户名
//右边是密码
zhangsan=123
lisi=1234
wangwu=12345
需求描述
① 客户端启动之后,需要连接服务端,并出现以下提示:
服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
②选择登录之后,出现以下提示:
服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名
③需要输入用户名和密码,输入完毕,没有按回车时,效果如下:
服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名
zhangsan
请输入密码
123
④按下回车,提交给服务器验证
服务器会结合txt文件中的用户名和密码进行判断
根据不同情况,服务器回写三种判断提示:
服务器回写第一种提示:登录成功
服务器回写第二种提示:密码有误
服务器回写第三种提示:用户名不存在
⑤客户端接收服务端回写的数据,根据三种情况进行不同的处理方案
登录成功的情况, 可以开始聊天,出现以下提示:
服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名
zhangsan
请输入密码
123
1
登录成功,开始聊天
请输入您要说的话
密码错误的情况,需要重新输入,出现以下提示:
服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名
zhangsan
请输入密码
aaa
密码输入错误
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
用户名不存在的情况,需要重新输入,出现以下提示:
服务器已经连接成功
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
1
请输入用户名
zhaoliu
请输入密码
123456
用户名不存在
==============欢迎来到黑马聊天室================
1登录
2注册
请输入您的选择:
⑥如果成功登录,就可以开始聊天,此时的聊天是群聊,一个人发消息给服务端,服务端接收到之后需要群发给所有人
提示:
此时不能用广播地址,因为广播是UDP独有的
服务端可以将所有用户的Socket对象存储到一个集合中
当需要群发消息时,可以遍历集合发给所有的用户
此时的服务端,相当于做了一个消息的转发
转发核心思想如下图所示:
其他要求:
用户名和密码要求:
要求1:用户名要唯一,长度:6~18位,纯字母,不能有数字或其他符号。
要求2:密码长度3~8位。第一位必须是小写或者大小的字母,后面必须是纯数字。
客户端:
拥有登录、注册、聊天功能。
① 当客户端启动之后,要求让用户选择是登录操作还是注册操作,需要循环。
-
如果是登录操作,就输入用户名和密码,以下面的格式发送给服务端
username=zhangsan&password=123
-
如果是注册操作,就输入用户名和密码,以下面的格式发送给服务端
username=zhangsan&password=123
② 登录成功之后,直接开始聊天。
服务端:
① 先读取本地文件中所有的正确用户信息。
② 当有客户端来链接的时候,就开启一条线程。
③ 在线程里面判断当前用户是登录操作还是注册操作。
④ 登录,校验用户名和密码是否正确
⑤ 注册,校验用户名是否唯一,校验用户名和密码的格式是否正确
⑥ 如果登录成功,开始聊天
⑦ 如果注册成功,将用户信息写到本地,开始聊天
//客户端
public class Client1 {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",10000);
Scanner s = new Scanner(System.in);
System.out.println("服务器已经连接成功");
//获取缓冲读取流
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获取缓冲输出流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
while(true){
System.out.println("======欢迎来到聊天室========");
System.out.println("1登录");
System.out.println("2注册");
System.out.println("请输入你的选择:");
String choose = s.next();
switch(choose){
case "1" -> log(socket,bw);
case "2" -> regit(socket,bw);
default -> {
System.out.println("没有这个选择");
continue;
}
}
//接收回复
String answer = br.readLine();
if(answer.equals("登录成功")){
break;
}else{
System.out.println(answer);
}
}
System.out.println("登录成功,开始聊天");
//开启一条线程,用来接收服务器发来的消息
new Thread(new ClientMyRunnbale(socket)).start();
//给服务器发送消息
talk(bw);
//退出聊天
socket.close();
}
//登录
public static void log(Socket socket,BufferedWriter bw) throws IOException {
Scanner s = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = s.next();
System.out.println("请输入密码:");
String password = s.next();
//写出数据
bw.write("登录");
bw.newLine();
// bw.flush();
bw.write("uername="+username+"&password="+password);
bw.newLine();
bw.flush();
}
//注册
public static void regit(Socket socket,BufferedWriter bw) throws IOException {
Scanner s = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = s.next();
System.out.println("请输入密码:");
String password = s.next();
//写出数据
bw.write("注册");
bw.newLine();
bw.write("uername="+username+"&password="+password);
//必须要换行,因为获取的时候也是缓冲流,是遇到换行符停止,否则就会造成阻塞
bw.newLine();
bw.flush();
}
//给服务器发送信息
public static void talk(BufferedWriter bw) throws IOException {
Scanner sc = new Scanner(System.in);
while(true){
System.out.println("请输入你要说的话:");
String message = sc.next();
bw.write(message);
bw.newLine();
bw.flush();
if("exit".equals(message)){
break;
}
}
}
}
//客户端线程类
public class ClientMyRunnbale implements Runnable{
Socket socket;
public ClientMyRunnbale (Socket socket) {
this.socket = socket;
}
@Override
public void run() {
while(true){
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String message = br.readLine();
System.out.println(message);
if("exit".equals(message)){
break;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
//服务端
public class serversocket {
//创建集合用来存储连接到的对象
static HashMap<Socket,String> list = new HashMap<>();
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);
//创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数量
16, //线程池总大小
60, //空闲时间
TimeUnit.SECONDS, //空闲时间(单位)
new ArrayBlockingQueue<>(2), //队列
Executors.defaultThreadFactory(), //线程池工厂,让线程池如何创建线程对象
new ThreadPoolExecutor.AbortPolicy() //阻塞队列
);
while(true){
//等待连接
Socket socket = ss.accept();
//创建线程
pool.submit(new MyRunnable(socket));
}
}
}
//服务端线程类
public class MyRunnable implements Runnable{
Socket socket;
public MyRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//获取读取缓冲流
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获取写出缓冲流
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//记录名字
String name;
while(true){
//接收数据
String line1 = br.readLine();
System.out.println(line1);
String line2 = br.readLine();
System.out.println(line2);
String username = line2.split("&")[0].split("=")[1];
String password = line2.split("&")[1].split("=")[1];
String answer = null;
switch (line1){
case "登录" -> answer = log(username,password);
case "注册" -> answer = regit(username,password);
}
//回写
bw.write(answer);
bw.newLine();
bw.flush();
if("登录成功".equals(answer)){
name = username;
serversocket.list.put(socket,username);
break;
}
}
System.out.println("开始聊天");
//聊天
talk(br,bw,name);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//登录
public String log(String username, String password) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("c.txt"));
String line ;
while((line=br.readLine()) != null){
String name = line.split("=")[0];
String pass = line.split("=")[1];
if(name.equals(username)){
if(pass.equals(password)){
br.close();
return "登录成功";
}else{
br.close();
return "密码错误";
}
}
}
br.close();
return "账号不存在";
}
//注册
public String regit(String username, String password) throws IOException {
if(username.matches("[a-zA-Z]{6,18}")){
if(password.matches("[a-zA-Z]\\d{2,7}")){
//将账号跟密码写到文件中
FileWriter fw = new FileWriter("c.txt",true);
fw.write(username+"="+password+"\n");
fw.flush();
fw.close();
return "注册成功";
}else{
return "密码格式不正确";
}
}else{
return "账号格式不正确";
}
}
//聊天
public void talk(BufferedReader br,BufferedWriter bw,String name) throws IOException {
while(true){
// System.out.println("开始读取");
String message = br.readLine();
System.out.println(name+":"+message);
//断开
if("exit".equals(message)){
bw.write(message);
bw.newLine();
bw.flush();
socket.close();
serversocket.list.remove(socket);
}
//群发
talkAll(name,message);
}
}
//群发
public void talkAll(String name,String message) throws IOException {
Set<Socket> sockets = serversocket.list.keySet();
for(Socket socket:sockets){
//获取输出流
BufferedWriter bw1 = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
if("exit".equals(message)){
bw1.write(name+"已经退出群聊");
}else{
bw1.write(name+":"+message);
}
bw1.newLine();
//发完必须要刷新一下
//否则客户端的readLine方法就会一直阻塞
bw1.flush();
}
}
}
反射
反射允许对成员变量,成员方法,和构造方法的信息进行编程访问
反射就是可以把类里面的成员变量和构造方法和成员方法挨个的获取出来,并对他们进行操作
比如,idea里面自动提示的功能就是用反射来实现的,当创建对象要调用的时候,idea就利用反射把这个类里面的东西获取出来并展示出来;还有当我们创建对象和调用方法的时候,方法的形参可以被提示出来,就是idea利用反射获取这个方法的所有形参并展示出来
反射可以获取类里面的所有东西;比如字段(成员变量) ,可以获取修饰符、获取名字、获取类型、赋值、获取值; 比如构造方法,可以获取修饰符、获取名字、获取形参、创建对象;比如成员方法,可以获取修饰符、获取名字、获取形参、获取返回值、抛出异常、获取注解、运行方法
注意:获取的时候不是从java文件中获取的,而是从Class字节码文件中获取的
作用:
- 获取一个类里面所有的信息,获取到了之后,再执行其他的业务逻辑
- 结合配置文件,动态的创建对象并调用方法
获取Class对象的三种方式
-
//第一种 Class.forName("全类名");
Class是一个类名,在java中,已经定义好了一个类叫作Class,用来描述字节码文件的,用静态方法forName,可以获取字节码文件对象
全类名:包名+类名
全类名最好是去复制,不然很容易错;先双击打开这个类,然后在代码中选中类名,右键选择Copy/Paste Special 中的 Copy Reference,就把全类名给复制了
-
//第二种 类名.class
获取字节码文件对象
-
//第三种 对象.getClass();
当创建完对象后,可以用对象调用getClass方法来获取字节码文件对象,getClass方法是定义在Object类中的
首先编写java文件 A.java;然后把他编译成Class文件 A.class;在这个阶段是没有把代码加载到内存当中的,他只是在硬盘中进行操作;所以这个阶段也叫做源代码阶段,在源代码阶段,我们会用第一种方式去获取Class对象;
接下来要运行代码了,就得把class文件 A.class 加载到内存当中,此时这个阶段叫作加载阶段,在加载阶段,我们会用第二种方式来获取class文件对象
在内存当中可以创建这个类的对象 A a = new A(); 此时就叫作运行阶段,在运行阶段可以用第三种方式来获取class对象
public class Student {
String name;
int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class test1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//第一种方式
//全类名:包名+类名
//最为常用的
Class clazz1 = Class.forName("test7.Student");
System.out.println(clazz1); //class test7.Student
//第二种
//更多的是当作参数进行传递
Class clazz2 = Student.class;
//第三种方式
Student s = new Student();
Class clazz3 = s.getClass();
//字节码文件是唯一的,上面获取的都是一样的
System.out.println(clazz1 == clazz2); //true
System.out.println(clazz2 == clazz3); //true
}
}
获取构造方法
在java当中,万物皆对象,什么东西都可以看成是一个对象
比如,字节码文件可以看成是Class类的对象;比如,构造方法也可以看成是一个对象,在java当中定义了一个 Constructor 类,这个类就是用来描述构造方法的,这个类的对象就表示构造方法的对象; 在java当中有一个类叫作 Field ,就是用来描述成员变量的,所以这个类的对象就表示成员变量的对象; 在java当中有一个类叫作 Method ,用来描述成员方法,那么这个类的对象就是成员方法的对象
Class类中用于获取构造方法的方法
方法 | 说明 |
---|---|
Constructor<?>[] getConstructors() | 返回所有公共构造方法对象的数组,不包括私有 |
Constructor<?>[] getDeclaredConstructors() | 返回所有构造方法对象的数据,包括私有的 |
Constructor getConstructor(Class<?>…parameterTypes) | 返回单个公共构造方法对象 |
Constructor getDeclaredConstructor(Class<?>…parameterTypes) | 返回单个构造方法对象,包括私有的 |
Constructor类中用于创建对象的方法
方法 | 说明 |
---|---|
T newInstance(Object… initargs) | 根据指定的构造方法创建对象 |
setAccessible(boolean flag) | 设置为true,表示取消访问检查 |
//类
public class Student {
String name;
int age;
public Student() {
}
public Student(String name){
this.name = name;
}
protected Student(int name){
this.age = age;
}
private Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class test1 {
public static void main(String[] args) throws Exception{
//获取class字节码文件对象
Class clazz = Class.forName("test7.Student");
//获取构造方法
Constructor[] cons1 = clazz.getConstructors();
System.out.println(Arrays.toString(cons1)); //不包括私有 [public test7.Student(), public test7.Student(java.lang.String)]
Constructor[] cons2 = clazz.getDeclaredConstructors();
System.out.println(Arrays.toString(cons2));//包括私有
//获取单个指定参数的构造方法
Constructor con1 = clazz.getConstructor(String.class);
System.out.println(con1);//public test7.Student(java.lang.String)
Constructor con2 = clazz.getDeclaredConstructor(int.class);
System.out.println(con2);//protected test7.Student(int)
Constructor con3 = clazz.getDeclaredConstructor(String.class, int.class);
//获取构造方法的修饰符
//不是字符串,是整数进行返回
//在APi帮助文档中找常量字段值,再找reflect对应的常量
//里面有各种修饰符对应的数字
//在idea中就是利用这个获取修饰符,如果是私有他就不给你提示,不让你创建这个对象
int modifiers = con3.getModifiers();
System.out.println(modifiers);//2
//获取参数
Parameter[] parameters = con3.getParameters();
System.out.println(Arrays.toString(parameters));//[java.lang.String arg0, int arg1]
//利用构造方法对象来创建对象
//里面的参数要跟获取的构造方法的参数保持一致
//但是con3的构造方法对象是私有的,不让创建
//可以用方法来临时取消权限校验,暴力反射
con3.setAccessible(true);
Student s = (Student) con3.newInstance("张三", 23);
System.out.println(s);//Student{name='张三', age=23}
}
}
获取成员变量
Class类中用于获取成员变量的方法
方法 | 说明 |
---|---|
Field[] getFields() | 返回所有公共成员变量对象的数组 |
Field[] getDeclaredFields() | 返回所有成员变量对象的数组 |
Field getField(String name) | 返回单个公共成员变量对象 |
Field getDeclaredField(String name) | 返回单个成员变量对象 |
Field类中用于创建对象的方法
方法 | 说明 |
---|---|
void set(Object obj, Object value) | 赋值 |
Object get(Object obj) | 获取值 |
//类
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class test1 {
public static void main(String[] args) throws Exception{
//获取class字节码文件对象
Class clazz = Class.forName("test7.Student");
//获取所有的成员变量
Field[] fields = clazz.getDeclaredFields();
System.out.println(Arrays.toString(fields));//[private java.lang.String test7.Student.name, private int test7.Student.age]
//获取单个成员变量
Field name = clazz.getDeclaredField("name");
System.out.println(name);//private java.lang.String test7.Student.name
//获取权限修饰符
int modifiers = name.getModifiers();
System.out.println(modifiers);//2
//获取成员变量的名字
String n = name.getName();
System.out.println(n);//name
//获取成员变量的数据类型
Class<?> type = name.getType();
System.out.println(type);//class java.lang.String
//获取成员变量记录的值
Student s = new Student("张三",23);
name.setAccessible(true);
String o = (String) name.get(s);
System.out.println(o);//张三
//修改对象里面记录的值
name.set(s,"lisi");
System.out.println(s);//Student{name='lisi', age=23}
}
}
利用反射获取成员方法
Class类中用于获取成员方法的方法
方法 | 说明 |
---|---|
Method[] getMethods() | 返回所有公共成员方法对象的数组,包括继承的 |
Method[] getDeclaredMethods() | 返回所有成员方法对象的数组,不包括继承的 |
Method getMethod(String name, Class<?>… parameterTypes) | 返回单个公共成员方法对象 |
Method getDeclaredMethod(String name, Class<?>… parameterTypes) | 返回单个公共成员方法对象 |
Method类中用于创建对象的方法
方法 | 说明 |
---|---|
Object invoke(Object obj, Object… args) | 运行方法 |
- 参数一:用obj对象调用该方法
- 参数二:调用方法的传递的参数(如果没有就不写)
- 返回值:方法的返回值(如果没有就不写)
//类
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void sleep(){
System.out.println("睡觉");
}
private String eat(String thing) throws IOException,RuntimeException{
System.out.println("在吃"+thing);
return "奥里给";
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class test1 {
public static void main(String[] args) throws Exception{
//获取class字节码文件对象
Class clazz = Class.forName("test7.Student");
//获取里面所有的方法对象(包括父类中所有的公共方法)
Method[] methods = clazz.getMethods();
//获取里面所有的方法对象(不能获取父类的,但是可以获取本类私有的)
Method[] methods1 = clazz.getDeclaredMethods();
//获取指定的单一方法
Method m = clazz.getDeclaredMethod("eat", String.class);
System.out.println(m);//private java.lang.String test7.Student.eat(java.lang.String) throws java.io.IOException,java.lang.RuntimeException
//获取方法的修饰符
int modifiers = m.getModifiers();
System.out.println(modifiers);//2
//获取方法的名字
String name = m.getName();
System.out.println(name);//eat
//获取方法的形参
Parameter[] parameters = m.getParameters();
System.out.println(Arrays.toString(parameters));//[java.lang.String arg0]
//获取方法的抛出的异常
Class<?>[] exceptionTypes = m.getExceptionTypes();
System.out.println(Arrays.toString(exceptionTypes));//[class java.io.IOException, class java.lang.RuntimeException]
//方法运行
Student s = new Student();
m.setAccessible(true);
//参数一:表示方法的调用者
//参数二:表示方法调用的时候的实际参数
String result = (String) m.invoke(s,"汉堡包");//在吃汉堡包
System.out.println(result);//奥里给
}
}
练习
- 对于任意一个对象,都可以把对象的所有的字段名和值,保存到文件中
public class test1 {
public static void main(String[] args) throws Exception{
method(new Student("张三",23));
}
public static <T> void method(T t) throws IllegalAccessException, IOException {
FileWriter fw = new FileWriter("a.txt");
BufferedWriter bw = new BufferedWriter(fw);
//获取字节码对象
Class clazz = t.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
//获取字段名
String name = field.getName();
System.out.println(name);
//获取值
Object o = field.get(t);
System.out.println(o);
bw.write(name+"="+o);
bw.newLine();
}
bw.flush();
fw.close();
bw.close();
}
}
- 反射可以跟配置文件结合的方式,动态的创建对象,并调用方法
//类
public class Teacher {
private String name;
private int age;
public Teacher() {
}
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
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 void teach(){
System.out.println("教书");
}
}
//配置文件
class=test2.Teacher
method=teach
public class Teacher {
private String name;
private int age;
public Teacher() {
}
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
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 void teach(){
System.out.println("教书");
}
}
动态代理
public class Student{
public void eat(){
sout("吃饭");
}
}
当要给这个类中的这个方法去添加俩个功能 sout(“拿筷子”); sout(“盛饭”); 如果直接把这俩个功能添加到方法中去叫作侵入式修改,很有可能会改变很多东西
此时,我们就得去找一个代理,代理就像中介公司,他会先去帮你实现那俩个功能,然后调用类中的那个方法,这就是动态代理
特点:无侵入式的给代码增加额外的功能
对象如果嫌身上干的事情太多的话,可以通过代理来转移部分职责; 对象有什么方法想被代理,代理就一定要有对应的方法,只是代理中的方法是不一样的,它是实现一些功能,再去调用对象中的方法;代理里面就是对象要被代理的方法
java通过接口保证代理的样子,后面的对象和代理需要实现同一个接口;接口中就是被代理的所有方法
java.lang.reflect.Proxy类:提供了为对象产生代理对象的方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
- 参数一:用于指定用哪个类加载器,去加载生成的代理类
- 参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
- 参数三:用来指定生成的代理对象要干什么事情
//类 明星
public class BigStar implements Star{
private String name;
public BigStar() {
}
public BigStar(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//唱歌
@Override
public String sing(String name){
System.out.println(this.name+"正在唱"+name);
return "谢谢";
}
//跳舞
@Override
public void dance(){
System.out.println(this.name+"正在跳舞");
}
}
//接口
public interface Star {
//唱歌
public abstract String sing(String name);
//跳舞
public abstract void dance();
}
//类 创建代理的类
public class ProxyUtil {
//方法作用:给一个对象,为这个对象创建代理
//形参:被代理的明星对象
//返回值:给明星创建的代理
public static Star createProxy(BigStar bigStar){
//创建代理
//参数一:用于指定用哪个类加载器,去加载生成的代理类
// 需要有一个人把字节码文件加载到内存当中,就是类加载器去加载
//方法ProxyUtil.class.getClassLoader(),找到把这个字节码加载到内存的类加载器
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{Star.class},//参数二:指定接口,这些接口用于指定生成代理长什么样,也就是有哪些方法
new InvocationHandler() {
/*
外面想要大明星唱歌:1.获取代理对象 代理对象 = ProxyUtil.createProxy(大明星对象);
2.再调用代理的唱歌方法, 代理对象.唱歌方法() 这个底层就是用的这个方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
参数一:代理的对象
参数二:要运行的方法
参数三:调用方法时,传递的实参
*/
if("sing".equals(method.getName())){
System.out.println("准备话筒,收钱");
}else if("dance".equals(method.getName())){
System.out.println("准备场地,收起");
}
//然后调用大明星的唱歌或者跳舞的方法
return method.invoke(bigStar,args);
}
}
);
return star;
}
}
整理得:
public class ProxyUtil {
public static Star createProxy(BigStar bigStar){
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{Star.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("sing".equals(method.getName())){
System.out.println("准备话筒,收钱");
}else if("dance".equals(method.getName())){
System.out.println("准备场地,收起");
}
return method.invoke(bigStar,args);
}
}
);
return star;
}
}
//测试类
public class Test {
public static void main(String[] args) {
/*
找大明星唱一首歌曲
*/
//获取代理对象
BigStar bigStar = new BigStar("海洋");
Star proxy = ProxyUtil.createProxy(bigStar);
//调用唱歌方法
String result = proxy.sing("我的美丽");
System.out.println(result);//准备话筒,收钱
//海洋正在唱我的美丽
//谢谢
}
}