文章目录
一、Object
没有一个属性是所有类的共性,因此Object类中没有一个成员变量,也就没有带参的构造方法,只有一个空参构造方法.
Object中的成员方法(11个)
toString
一般会重写,打印对象时打印属性
默认情况下,Object类中的toString方法返回的是地址值,所以默认情况下,打印一个对象打印的就是地址值。如果想看到对象内部的属性值,该怎么办?
处理方案:重写父类Object中的toString方法,把对象的属性进行拼接
equals
真正想要比的是对象中的属性值,父类中的方法不能满足当前需求,重写方法
一开始判断当前的调用者this和当前参数中的是不是同一个对象,如果地址值一样,直接return true
否则比较类型是否相等,进行强转,再比较对象内部的属性值
例子:
两个结果都是false;
因为第一个equals方法是String调用的,所以equals要看String类中的
String 类中重写的equals方法:
字符串中的equals方法,先判断参数是否为字符串,如果是字符串,再比较其内部的属性,但是如果不是字符串,直接返回false
第二个equals方法是StringBuilder调用的,所以equals要看StringBuilder类中的
StringBuilder类中没有equals方法,看其父类AbstractStringBuilder中也没有equals方法,所以默认继承Object中的equals方法,再Object当中默认是使用==号比较两个对象的地址值
clone
把A对象的属性值完全拷贝给B对象,也叫对象拷贝,对象复制
protected只能被本包或者其他包下的子类访问,重写clone方法
并且需要实现Cloneable接口
该接口中没有任何的抽象方法
如果一个接口中没有抽象方法,表示当前的接口是一个标记性接口,现在Cloneable
一旦实现了,那么当前类的对象就可被克隆,如果没有实现,当前类的对象就不能克隆
实现细节:
方法在底层会帮我们创建一个对象,并把元对象中的数据拷贝过去
1、重写Object类中的clone方法
2、让JavaBean类实现Cloneable接口
3、创建元对象并调用clone就可以了
深克隆:基本数据类型拷贝过来,字符串复用,引用数据类型会重新创建新的
浅克隆:不管对象内部的属性是基本数据类型还是引用数据类型,都完全拷贝过来
Object中的克隆是浅克隆
可以重写方法或者利用第三方工具Gson完成深克隆
Gson gson = new Gson();
//把对象转化为json对象
String s = gson.toJson(u1);
//再把字符串变回对象就可以了
User user = gsin.fromJson(s,User.class);
二、Objects
Objects是一个工具类,提供了一些方法去完成一些功能
Objects
的equals方法实现细节:
1、方法的底层会判断参数是否为null,如果为null,直接返回false
2、如果不为null,那么就再次调用equals方法
3、如果此时s是重写过equals方法的JavaBean类型,所以最后还是会调用重写之后的equals方法
如果没有重写,比较的就是地址值,重写了就比较属性值
三、BigInteger和BigDecima
BigInteger
BigInteger的构造方法:
BigInteger中的静态方法 BigInteger bd1 = BigInteger.valueOf(100);
和 BigInteger db2 = new BigInteger("100");
区别:
1、BigInteger.valueOf(100) 能表示的范围比较小,只能在long的取值范围之内,如果超出long的范围就不行了
2、在内部对常用的数字:-16~16进行了优化,提前把 -16~16 先创建好BigInteger的对象,如果多次获取不会重新创建新的
底层源码:
存放 -16 – -1 和1 --16的两个数组以及单独数字0
BigInteger的构造方法小结:
- 如果BigInteger表示的数字没有超出long的范围,可以用静态方法获取
- 如果BigInteger表示的数字超出long的范围,可以用构造方法获取
- 对象一旦创建,BigInteger内部记录的值不能发生改变
- 只要进行计算都会产生一个新的BigInteger对象
BigInteger的成员方法:
BigDecima
可以表示较大的小数和解决小数运算精度失真问题
运行结果:
计算机中的小数:浮点数运算的时候会有精度丢失的风险这个和计算机保存浮点数的机制有很大关系。计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。
BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
1、通过传递double类型的小数来创建对象
BigDecimal bd1 = new BigDecimal(0.01);
BigDecimal bd2 = new BigDecimal(0.09);
System.out.println(bd1);
System.out.println(bd2);
这种方式有可能是不精确的,所以不建议使用
2、通过传递字符串表示的小数来创建对象
BigDecimal bd1 = new BigDecimal("0.01");
BigDecimal bd2 = new BigDecimal("0.09");
System.out.println(bd1);
System.out.println(bd2);
这种方法可以获取到精确的小数
3、通过静态方法获取对象
BigDecimal bd3 = BigDecimal.valueOf(10);
System.out.println(bd3);
底层源码:
第二种和第三种方式的细节:
1、如果要表示的数字不大,没有超出double的取值范围,建议使用静态方法
2、如果要表示的数字比较大,超出了double的取值范围,建议使用构造方法
3、静态方法中,如果我们传递的是0-10之间的整数,包含10 包含0,那么方法会返回已经创建好的对象,不会重新new。
BigDecimal的成员方法:
四、正则表达式
作用:
1、校验字符串是否满足规则
2、在一段文本中查找满足要求的内容
通过正则表达式进行文本爬虫:
//1、获取正则表达式的对象
Pattern p = Pattern.compile("Java\\d{0,2}");
//2、获取文本匹配器对象
//拿着m去读取str 找符合p规则的子串
Matcher m = p.matcher(str);
//3、利用循环获取
while(m.find()){
String s = m.group();
System.out.println(s);
}
五、Date
世界标准时间:格林威治时间,简称GMT
目前时间标准时间已经替换为:原子钟
Date类是一个JDK写好的JavaBean类,用来描述时间,精确到毫秒
利用空参构造创建的对象,默认表示系统当前时间
利用有参构造创建的对象,表示指定的时间
JDK7前时间相关类
SimpleDateFormat类
格式化:把时间变成我们喜欢的格式
解析:把字符串表示的时间变成Date对象
构造方法
成员方法
//利用空参构造创建SimpleDateFormat对象 默认格式
SimpleDateFormat sdf = new SimpleDateFormat();
Date d = new Date();
sdf.format(d);
//利用带参构造创建SimpleDateFormat对象 指定格式
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d1 = new Date();
sdf1.format(d1);
Calendar类
Calendar代表了系统当前时间的日历对象,可以单独修改,获取时间中的年、月、日
Calendar是一个抽象类,不能直接创建对象,通过getInstance
方法获取对象
//1、获取日历对象 通过一个静态方法获取到子类对象
//细节1:Calendar是一个抽象类,不能直接new
//底层原理:会根据系统的不同时区来获取不同的日历对象,默认表示当前时间,
//会把时间中的纪元,年,月,日,时,分,秒,星期,等放在一个数组当中
// 0:纪元 1:年 2:月 3:一年中的第几周 4:一个月中的第几周 5:一个月中的第几天
//细节2:
//月份:范围0~11 如果获取出来的是0 那么实际上是1月
//星期:在外国人眼里,星期日是一周中的第一天
//1(星期日) 2(星期一) 3(星期二)……
Calendar c = Calendar.getInstance();
//2、修改日历代表的时间
Date d = new Date();
c.setTime(d);
//获取日期中的某个字段信息
//java在Calendar中 ,把索引对应的数字都定义成常量
int year = c.get(1);
int year1 = c.get(Calendar.YEAR);
int month = c.get(2)+1;
int month1 = c.get(Calendar.MONTH)+1;
int day= c.get(5);
int day1 = c.get(Calendar.DAY_OF_MONTH);
int week = c.get(Calendar.DAY_OF_WEEK);
c.set(Calendar.YEAR,2000);
c.set(Calendar.MONTH,11);//实际上是12月
//调用方法在这个基础上增加一个月
c.add(Calendar.MONTH,1);
//调用方法在这个基础上减一个月
c.add(Calendar.MONTH,-1);
总结:
1、如何获取对象?
通过getInstance
方法来获取对象
2、常见方法:
setXxx
:修改
getXxx
:获取
add
:在原有的基础上进行增加或减少
3、细节点:
日历类中的月份范围:0~11
日历类中星期的特点:星期日是一周中的第一天
JDK8新增时间相关类
//1、获取所有的时区名称
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
//2、获取当前系统的默认时区
ZoneId zoneId = ZoneId.systemDefault();
//3、获取指定时区
ZoneId zoneId1 = ZoneId.of("Asia/Pontianak");
Instant时间戳常用的方法
ZoneDateTime时间戳常用的方法
DateTimeFormatter 用于时间的格式化和解析
LocalDate、LocalTime、LocalDateTime
工具类
例题1:
用JDK7和JDK8计算当前活了多少天
JDK7
//JDK7:
//规则:只要对时间进行计算或者判断,都需要先获取当前时间的毫秒值
//1、计算出生年月日的毫秒值
public static void main(String[] args) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String birth = "1999-10-04";
long birthTime = sdf.parse(birth).getTime();
//2、获取当前时间的毫秒值
Date today = new Date();
long todayTime = today.getTime();
//3、计算间隔
long day = (todayTime-birthTime)/1000/60/60/24;
System.out.println(day);
}
JDK8
public static void main(String[] args) throws ParseException {
LocalDate birth = LocalDate.of(1999,10,4);
LocalDate today = LocalDate.now();
long days = ChronoUnit.DAYS.between(birth, today);
System.out.println(days);
}
例题2:
用JDK7和JDK8判断任意的一个年份是闰年还是平年
二月有29天是闰年,一年有366天是闰年
JDK7
public static void main(String[] args) throws ParseException {
//可以把时间设置为3月1日
Calendar c = Calendar.getInstance();
c.set(2023,2,1);
//再把日历往前减一天
c.add(Calendar.DAY_OF_MONTH,-1);
//看看当前时间是28号还是29号
int day = c.get(Calendar.DAY_OF_MONTH);
System.out.println(day);
//判断day是28还是29
}
JDK8
public static void main(String[] args) throws ParseException {
LocalDate ld = LocalDate.of(2023, 3, 1);
LocalDate localDate = ld.minusDays(1);
System.out.println(localDate.getDayOfMonth());
}
JDK8中自带方法判断
isLeapYear();
public static void main(String[] args) throws ParseException {
LocalDate ld = LocalDate.of(2023, 3, 1);
boolean leapYear = ld.isLeapYear();
System.out.println(leapYear);
}
六、包装类
包装类:用一个对象,把基本数据类型给包起来
基本数据类型对应的引用类型
为什么要设置包装类?
1、在集合当中只能存引用数据类型,不能存基本数据类型
2、参数如果是Object类型,那么基本数据类型就传不进去
JDK5之前
//通过构造方法获取Integer对象
Integer i1 = new Integer(1);
//通过静态方法获取Integer对象
Integer i2 = Integer.valueOf(123);
这两种方式获取对象的区别
运行结果
JDK5之后提出了自动装箱和自动拆箱
自动装箱:把基本数据类型会自动变成其对应的包装类
自动拆箱:把包装类自动的变成其对象的基本数据类型
//在底层,此时还会去自动调用静态方法valueOf得到一个Integer对象,只不过这个动作不需要我们自己去操作了
Integer i1 = 10;
Integer i2 = Integer.valueOf(10);
//自动拆箱的动作
int i = i2;
在JDK5之后,int和Integer可以看作是同一个东西,因为在内部可以自动转化
Integer中的成员方法
细节:
- 细节一:在类型转换的时候,括号中的参数只能是数字不能是其他,否则代码会报错
- 细节二:8中包装类当中,除了Character都有对应的parseXxx的方法,进行类型转换
七、异常
异常就是代表程序出现的问题
Error:代表的系统级别错误(属于严重问题)
Exception:叫做异常,代表程序可能出现的问题,通常会用Exception以及他的子类来封装程序出现的问题
RuntimeException:运行时异常,RuntimeException及其子类,编译阶段不会出现异常提醒。运行时出现的异常(数组索引越界异常)
其他异常:编译时异常,编译阶段就会出现异常提醒的。
编译时异常和运行时异常的区别:
编译时异常:除了RuntimeException本身和所有子类,其他都是编译时异常,编译阶段需要进行处理
运行时异常:RuntimeException本身和所有子类,都是运行时异常,编译阶段不会报错,程序运行时出现
异常的处理方式:
- JVM默认的处理方式
把异常信息以红色字体打印在控制台,并结束程序
- 自己处理
try catch
一般用在调用处,代码可以继续执行,不会停止
- 抛出异常
在方法中,出现异常了,方法就没有继续运行下去的意义了,采取抛出处理
throws: 写在方法定义处,表示声明一个异常,告诉调用者,使用本方法可能会有哪些异常。编译时异常:必须要写,运行时异常:可以不写.
thorw: 写在方法内,结束方法。手动抛出异常对象,交给调用者,方法中下面的代码不再执行了
八、IO流
字符流
字符流的底层其实就是字节流
特点:
输入流:一次读一个字节,遇到中文时,一次读多个字节(中文GBK2个字节,utf-8 3个字节)
输出流:底层会把数据按照指定的编码方式进行编码,变成字节再写到文件中
**使用场景:**适合纯文本文件
FileReader:
字符流原理解析:
-
创建字符输入流对象
底层:关联文件,并创建缓冲区(长度为8192的字节数组) -
读取数据
底层:
1、判断缓冲区中是否有数据可以读取
2、缓冲区没有数据:就从文件中获取数据,装到缓冲区中,每次尽可能装满缓冲区,如果文件中也没有数据了,返回-1;
3、缓冲区有数据:从缓冲区中读取
空参的read方法:一次读取一个字节,遇到中文一次读多个字节,把字节解码并转成十进制返回
有参的read方法:把读取字节、解码、强转三步合并了,把强转之后的字符放到数组中
字节流和字符流的使用场景
字节流:拷贝任意类型的文件
字符流:读取纯文本文件中的数据、往纯文本文件中写出数据
缓冲流
字节缓冲输入流
原理:底层自带了长度为8192的缓冲区提高性能
字节缓冲流提高效率的原理
因为缓冲流自带长度为8192的缓冲区
可以显著提高字节流的读写性能
对于字符流【底层已经自带缓冲区了】的提升不明显,对于字符缓冲流而言关键点是两个特有的方法:读取一行数据【BufferedReader :readLine】【BufferedWriter:newLine】
字符缓冲流
转换流
字节流里面是没有读取一整行的方法,只有字符缓冲流可以(BufferedReader)
是字符流和字节流之间的桥梁
字符转换输入流:InputStreamReader
字符转换输出流:OutputStreamWriter
1、利用转换流按照指定字符编码读取
//利用转换流按照指定字符编码读取
//1、创建对象并指定字符编码
//GBK是转换流的第二个参数
InputStreamReader isr = new InputStreamReader(new FileInputStream("mio\\gbkfile.txt"),"GBK");
//2、读取数据
int ch;
while((ch = isr.read())!=-1){
System.out.print((char)ch);
}
//3、释放资源
isr.close();
在JDK10之后,上面那种方式已经被淘汰了
替代方案:
FileReader fr = new FileReader("mio\\gbkfile.txt",Charset.forName("GBK"));
//2、读取数据
int ch;
while((ch = fr.read())!=-1){
System.out.print((char)ch);
}
//3、释放资源
fr.close();
2、利用转换流按照指定字符编码写出
//1、创建转换流对象
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("mio\\gbkfile.txt"),"GBK");
//2、写出数据
osw.write("你好你好!");
//3、关闭资源
osw.close();
在JDK10之后,上面那种方式同样已经被淘汰了
替代方案:
FileWriter fw = new FileWriter("mio\\gbkfile.txt",Charset.forName("GBK"));
fw.write("你好你好!");
fw.close();
转换流的作用:
指定字符集读写数据(JDK11之后已经淘汰)
字节流想要使用字符流中的方法
序列流
序列化流/对象操作输出流
可以把Java中的对象写到本地文件中
//1、new一个对象
Student s = new Student("zhangsan",23);
//2、创建序列化流的对象/对象操作输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("myio\\a.txt"));
//3、写出数据
oos.writeObjects(stu);
//4、释放资源
oos.close();
序列化流的小细节:
使用对象输出流将对象保存到文件时会出现NotSerializableException
异常
解决方案:
需要让JavaBean
类实现Serializable
接口
Serializable
接口里面是没有抽象方法,标记型接口,
一旦实现了这个接口,那么就表示当前的Student类可以被序列化
反序列化流/对象操作输入流
可以把序列到本地文件中的对象,读取到程序中来
//1、创建反序列化流的对象/对象操作输入流
ObjectInputStream ois = new ObjectInputStream (new FileInputStream("myio\\a.txt"));
//2、读取数据
ois.readObject();
//3、打印对象
System.out.println(o);
//4、释放资源
oos.close();
当修改了JavaBean中的内容后会出现错误:
- 解决方案:固定版本号
private static final long serialVersionUID = 1L;
细节汇总
1、使用序列化流将对象写到文件时,需要让JavaBean类实现
Serializable
接口,否则会出现NotSerializableException
异常
2、序列化流写到文件中的数据是不能修改的,一旦修改就无法再次读回来了
3、序列化对象后,修改了JavaBean类,再次反序列化,会不会有问题?
会出问题,会抛出InvalidClassException
异常
解决方案:给JavaBean类添加serialiVersionUID
(序列号、版本号)
4、如果一个对象中的某个成员变量的值不想被序列化,可以给这个变量添加关键字transient
修饰,该关键字标记的成员变量不参与序列化过程
九、多线程
进程:是程序的基本执行实体
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位【应用软件中互相独立,可以同时运行的功能】
并发和并行
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
多线程的实现方式
- 继承
Thread
类的方式进行实现
1、自己定义一个类继承Thread
2、重写run方法
3、创建子类的对象,并启动线程
public class MyThread extends Thread{
public void run(){
//书写线程要执行的代码
for(int i=0;i<100;i++){
System.out.println(getName() + "HellowWorld");
}
}
}
public class Test{
public static void main(String[] args){
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
小细节:
如果我们没有给线程设置名字,线程也是有默认名字的 Thread-X(X序号)
- 实现
Runnable
接口的方式进行实现
1、自己定义一个类实现Runnable接口
2、重写里面的run方法
3、创建自己的类的对象
4、创建一个Thread类的对象,并开启线程
public class MyRun implements Runnable{
public void run(){
//书写线程要执行的代码
for(int i=0;i<100;i++){
Thread t = Thread.currentThread();
System.out.println(t.getName() + "HellowWorld");
}
}
}
public class Test2{
public static void main(String[] args){
MyRun r1 = new MyRun ();
//创建线程对象
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
//给线程设置名字
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
- 利用
Callable
接口和Future
接口方式实现
特点:可以获取到多线程运行的结果
1、创建一个类MyCallable实现Callable接口
2、重写call (是有返回值的,代表多线程运行结果)
3、创建MyCallable的对象(表示多线程要执行的任务)
4、创建FutureTask的对象(作用:管理多线程运行的结果)
5、创建Thread对象(并启动)
public class MyCallable implements Callable<Integer>{
public Integer call() throws Exception{
int sum = 0;
for(int i=1;i<=100;i++){
sum += i;
}
return sum;
}
}
public class Test3{
public static void main(String[] args){
//1、创建MyCallable的对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable ();
//2、创建FutureTask的对象(作用管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
//3、创建线程的对象
Thread t1 = new Thread(ft);
//4、开启线程
t1.start();
//5、获取多线程运行的结果
Integer res = ft.get()
System.out.println(res);
}
}
细节:
JVM虚拟机启动后,会自动的启动多条线程,其中有一条线程就叫做main线程,它的作用就是去调用main方法,并执行里面所有的代码,之前我们写的所有的代码,其实都是运行在main线程当中
线程优先级
抢占式调度,线程执行具有随机性
优先级越高,抢到CPU的概率越高
守护线程
final void setDaemon(boolean on)
设置为守护线程
当其他的非守护线程执行完毕后,守护线程会陆续结束
出让线程/礼让线程
public static void yield()
表示出让当前CPU的执行权
线程的生命周期
线程安全的问题
解决线程安全问题:
同步代码块
锁对象一定要是唯一的
特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
同步方法
就是把synchronized
关键字加到方法上
特点1:同步方法是锁住方法里面所有的代码
特点2:锁对象不能自己指定【非静态方法:this 静态方法:当前类的字节码文件】
public class MyThread extends Thread{
static int ticket = 0;
@Override
public void run() {
while(true){
//同步代码块
synchronized (MyThread.class){
//判断
if(ticket==100){
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName()+"在卖第"+ticket+"张票~");
}
}
}
}
}
public class ThreadDemo {
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();
}
}
没有出现重复票或者越界票的情况
Lock锁
同步代码块我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
Lock实现提供了获得锁和释放锁的方法【手动上锁、手动释放锁】
void lock():获得锁
void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReenTrantLock
来实例化
public class MyThread extends Thread{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//同步代码块
//synchronized (MyThread.class){
lock.lock();
try {
//判断
if (ticket == 100) {
break;
} else {
Thread.sleep(10);
}
ticket++;
System.out.println(getName() + "在卖第" + ticket + "张票~");
}catch(Exception e){
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
}
public class ThreadDemo {
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();
}
}
没有出现重复票也没有出现越界票
等待唤醒机制【生产者消费者】
生产者:
/**
* 生产者
*/
public class Cook extends Thread{
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count==0){
break;
}else{
//判断桌子上是否有食物
if(Desk.foodFlag==1){
//如果有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//如果没有,就制作食物
System.out.println("厨师做了一碗面条");
//修改桌子上食物的状态
Desk.foodFlag=1;
//唤醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
消费者:
/**
* 消费者
*/
public class Foodie extends Thread{
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count==0){
break;
}else{
//判断桌子上是否有面条
if(Desk.foodFlag==0){
//如果没有就等待
try {
Desk.lock.wait();//让当前线程和锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
Desk.lock.notifyAll();
}else{
//把吃的总数-1
Desk.count--;
//如果有就开吃
System.out.println("吃货在吃面条,还能再吃"+Desk.count+"碗!");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子的状态
Desk.foodFlag=0;
}
}
}
}
}
}
桌子【信号量】:
/**
* 控制生产者和消费者的执行
*/
public class Desk {
//是否有面条
public static int foodFlag = 0;
//总个数
public static int count =10;
//锁对象
public static Object lock = new Object();
}
Main方法:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
//1、循环
//2、同步代码块/同步锁
//3、判断共享数据是否到了末尾
//4、没有到末尾(执行核心逻辑)
//创建线程对象
Cook c = new Cook();
Foodie f = new Foodie();
//给线程设置名字
c.setName("吃货");
f.setName("厨师");
//启动线程
c.start();
f.start();
}
}
等待唤醒机制【阻塞队列方式实现】
生产者:
/**
* 生产者
*/
public class Cook extends Thread{
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue){
this.queue = queue;
}
@Override
public void run() {
while (true){
//不断的把面条放到阻塞队列当中
try {
queue.put("面条");
System.out.println("厨师做了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者:
/**
* 消费者
*/
public class Foodie extends Thread {
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
//不断从阻塞队列中获取食物
try {
String take = queue.take();
System.out.println(take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Main方法:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
//生产者和消费者必须使用同一个阻塞队列
//创建阻塞队列
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//创建线程的对象
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
//启动线程
c.setName("厨师");
f.setName("吃货");
//启动线程
c.start();
f.start();
}
}
线程池
多线程的弊端
1、用到线程的时候就创建
2、用完之后线程消失
线程池主要核心原理
1、创建一个池子,池子中是空的
2、提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
3、但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
public class Test {
public static void main(String[] args) {
//1、获取线程池对象
ExecutorService pool = Executors.newCachedThreadPool();
//2、提交任务
pool.submit(new Main2());
//3、销毁线程池
pool.shutdown();
}
}
自定义线程池
核心线程数量(不能小于0)
线程池中最大的线程数量(最大数量》=核心线程数量)
空闲时间(值)(不能小于0)
空闲时间(单位)(用TimeUnit指定)
阻塞队列(不能为null)
创建线程的方式(不能为null)
要执行任务过多时的解决方案(不能为null)
不断的提交任务,会有以下三个临界点:
1、当核心线程满时,再提交任务就会排队
2、当核心线程满时,队伍满时,会创建临时线程
3、当核心线程满时,队伍满时,临时线程满时,会触发任务拒绝策略
线程池多大合适呢
- CPU密集型运算: 最大并行数+1
- I/O密集型运算
十、网络编程
网络编程三要素: IP、端口、协议
IP:设备再网络中的地址,是唯一的标识
端口号:应用程序在设备中的唯一标识
协议:数据在网络中传输的规则【UDP、TCP、http】
UDP协议【网络会议、语音通话、在线视频】
用户数据报协议(User Datagram Protocol)
UDP是面向无连接通信协议
速度快,有大小限制,一次最多发送64K,数据不安全,易丢失数据
发送数据:
1、创建发送端的DatagramSocket对象
2、数据打包(DatagramPacket)
3、发送数据
4、释放资源
public class Test {
public static void main(String[] args) throws IOException {
//1、创建DatagramSocket对象
//细节:
//绑定端口:就是通过这个端口往外发送
//空参:所有可用的端口中随机一个进行使用
//有参:指定端口号进行绑定
DatagramSocket ds = new DatagramSocket();
//2、数据打包
String str = "你好啊~";
byte[] bytes = str.getBytes();
InetAddress name = InetAddress.getByName("127.0.0.1");
int port = 10086;
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,name,port);
//3、发送数据
ds.send(dp);
//4、释放资源
ds.close();
}
}
接收数据:
1、创建接收端的DatagramSocket对象
2、接收打包好的数据
3、解析数据包
4、释放资源
public class Receive {
public static void main(String[] args) throws IOException {
//1、创建DatagramSocket对象
//细节:
//在接收的时候,一定要绑定端口号
//而且绑定的端口一定要和发送的端口保持一致
DatagramSocket ds = new DatagramSocket(10086);
//2、接收数据包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
//该方法是阻塞的
//程序执行到这一步的时候,会在这里死等,等发送端发送消息
ds.receive(dp);
//3、解析数据包
byte[] data = dp.getData();
int len = dp.getLength();
dp.getAddress();
dp.getPort();
System.out.println("接收到的数据:"+new String(data,0,len));
System.out.println("该数据是从:"+ dp.getAddress()+"这台电脑中的:"+dp.getPort()+"这个端口发出的");
//4、释放资源
ds.close();
}
}
先运行接收端,再运行发送端。运行结果如下:
UDP的三种通信方式
单播
组播
广播
TCP协议【下载软件、发送邮件、文字聊天】
传输控制协议(Transmission Control Proto)
TCP协议是面向连接的通信协议
速度慢、没有大小限制,数据安全
TCP会在通信的两端个建立起一个Socket对象 ,通信之前要保证连接已经建立,通过Socket产生IO流来进行网络通信
客户端
1、创建客户端的Socket对象(Socket)与指定服务器端连接
2、获取输出流,写数据
3、释放资源
public class Client {
public static void main(String[] args) throws IOException {
//1、创建Socket对象
Socket socket = new Socket("127.0.0.1",10003);
//2、可以从连接通道中获取输出流
OutputStream os = socket.getOutputStream();
//3、写出数据
os.write("你好你好".getBytes(StandardCharsets.UTF_8));
//4、释放资源
os.close();
socket.close();
}
}
服务器端
1、创建服务器端的Socket对象(ServerSocket)
2、监听客户端连接,返回一个Socket对象
3、获取输入流,读数据,并把数据显示在控制台
4、释放资源
public class Server {
public static void main(String[] args) throws IOException {
//1、创建Socket对象
ServerSocket ss = new ServerSocket(10003);
//2、监听客户端的连接
Socket accept = ss.accept();
//3、从连接通道中获取输入流读取数据
InputStream is = accept.getInputStream();
//解决中文乱码问题
InputStreamReader isr = new InputStreamReader(is);
//提高效率
BufferedReader br = new BufferedReader(isr);
//循环读取数据
int b;
while ((b= br.read())!=-1){
System.out.print((char)b);
}
//4、释放资源
ss.close();
is.close();
}
}
三次握手协议保证连接建立
四次挥手:利用这个协议断开连接,并且保证连接通道里的数据以及处理完毕了
三次握手
四次挥手
十一、反射
获取class对象的三种方式【字节码文件】
- Class.forName(“全类名(包名+类名)”)【源代码阶段】:最为常用
- 类名.class 【加载阶段】:一般更多的是当做参数进行传递
- 对象.getClass(); 【运行阶段】:当我们已经有了这个类的对象时才可以使用
利用反射获取构造方法
public class MyReflect{
public static void main(String[] args) throws IOException, ClassNotFoundException {
//1、先获取字节码文件
Class clazz = Class.forName("com.powernode.bank.Student");
//2、获取所有的构造方法
Constructor[] declaredConstructor = clazz.getDeclaredConstructors();
for(Constructor s:declaredConstructor){
System.out.println(s);
}
}
}
构造方法的权限修饰符
利用反射获取成员变量
利用反射获取成员方法
总结:
1、反射的作用:
1、获取任意一个类中的所有信息
2、结合配置文件动态创建对象
2、获得class字节码文件对象的三种方式
1、Class.forName(“全类名”);
2、类名.class
3、对象.getClass();
动态代理
程序为什么需要代理?
对象如果嫌身上干的事太多的话,可以通过代理来转移部分职责
对象有什么方法想被代理,代理就一定要有对应的方法
代理可以无侵入式的给对象增强其他功能
Java通过什么来保证代理的样子?
通过接口保证,后面的对象和代理需要实现同一个接口,接口就是被代理中的所有方法