编程语言中,封装有2层意思:
1.将重复的代码提取到一个公共的方法中,从而提升代码的复用性、可维护性
2.如果成员变量未加封装密封,可以在类的外部被肆无忌惮的修改
封装性是指,将对象的属性和行为进行密封,保护数据并隐蔽具体的细节,在类的外部不可直接访问,要想访问需要通过严格的接口控制。
封装性的目的:采用封装的思想保证了类内部数据结构的完整性,应用该类的用户不能轻易直接操纵该数据结构,
封装的原则就是要求在类的外部,不能随意存取对象的内部数据(成员属性和成员方法).从而有效地避免了外部错误对它的影响,使软件错误能够局部化,大大较少差错和排错的难度
1. 私有化成员变量,使用private关键字修饰;
2. 提供公共的get和set成员变量的方法,并在方法体中进行合理性的判断;
3. 在构造方法中调用set成员变量的方法来确保合理性;
单例,1个实例,1个对象
在有一些场景中,一个类只需要对外提供一个对象,这样的类称为单例类
编写单例类的方式,称为单例设计模式
单例设计模式的实现步骤:
1.私有化构造方法,阻止在类的外部创建对象
2.在类的内部提供私有的静态属性保存当前类的实例
3.在类的内部提供公共的静态方法,返回当前类的实例
需求:设计3个类:学生类、教师类、工人类
学生类:
属性:姓名、年龄、学号
方法:学习
教师类:
属性:姓名、年龄、薪水
方法:讲课
工人类:
属性:姓名、年龄、工号
方法:工作
通过需求发现多个类之间,拥有很多相同的内容,为了提升代码的复用性、可维护性,我们可以将多个类之间相同的内容提取到一个新类中,然后各个子类再继承该新类即可
语法格式:
修饰符 class 子类 extends 父类{}
例如:public class Student extends Person{}
代码演示:
将多个类之间相同的内容提取到公共的类中
课堂练习
大家定义工人类,让工人类继承Person类,并新建测试文件进行测试
2.2继承性注意事项
1.如果一个子类继承了父类,那么这个子类拥有父类所有的成员属性和方法,即使是父类里有private属性的变量,子类也是继承的,只不过不能使用。
2. 当构造子类对象时,会先构造父类对象,会自动调用父类的无参构造方法
如果手动调用了父类的有参构造方法,不再执行父类的无参构造
3. 在java语言中,只支持单继承,也就是一个类只能有一个父类
4. 不能滥用继承,必须满足子类 is a 父类 的逻辑关系时才可以
this,这个,表示当前类或当前类的实例(对象)
super,表示父类或父类对象
使用this.的方式可以访问本类对象的成员变量、成员方法
使用super.的方式可以访问父类对象的成员变量、成员方法
使用this()的方式放在构造方法中的第一行,表示调用本类中的无参构造方法
使用super()的方式放在构造方法中的第一行,表示调用父类的无参构造方法
this() 和 super() 必须出现在构造方法的第一行,因此不能同时出现
当子类从父类中继承的方法不能满足子类需求时,就可以在子类中声明一个和父类中一模一样的方法(方法名相同、参数列表相同、返回值类型相同),来覆盖父类中的方法,这就称为方法重写.
代码演示:
方法重写的注意事项:
1.相同的方法名、相同的参数列表、相同的返回值类型
2.访问权限不能缩小,可以放大
3.在子类方法中,可以使用super. 调用父类中原始的方法
通常情况下,成员变量(属性)使用private封装,成员方法使用public修饰
public,公共的,在任何类中都能访问
private,私有的,只能在当前类的内部访问
protected,受保护的,在当前包的其他类、当前类、子类中
默认修饰符,当前类、当前包的其他类中
final,本意为”最终的,不可更改的”,可以使用该关键字修饰类、成员方法、成员变量
使用final修饰类,表示该类是最终的,不能被继承,防止滥用继承
使用final修饰成员方法,表示该方法是最终的,不能被重写,防止不经意间重写方法
使用final修饰成员变量,表示该成员变量必须赋值,而且不能更改
通常情况,我们会使用static和final关键字组合使用,来声明常量:
所谓的常量,就是一旦声明不能更改
其作用就是,声明一些公共的数据,方便使用,例如:税率、圆周率等
常量名有一个特点:通常都是大写,例如:PI、TAX_RATE、STATUS等
自定义矩形类,成员变量主要有:横坐标、纵坐标、长度、宽度,方法有:
构造方法、打印所有成员变量的方法,实现矩形类的封装。
自定义圆形类,成员变量主要有:横坐标、纵坐标、半径,方法有:构造方法、打印
所有成员变量的方法,实现圆形类的封装。
自定义测试类,在main()方法中分别创建矩形和圆形的对象,去调用各自的打印成员变量的方法。
单个对象创建过程
1.先执行静态代码块,当类加载完毕时,就会执行静态代码块
2.当new一个对象时,会执行构造块 {}
3.执行构造方法
子类对象创建过程
1.先加载父类到内存,再加载子类到内存,先执行父类的静态代码块,再执行子类的静态代码块
2.构造子类对象时,先构造父类对象,所以先执行父类的构造块、构造方法;再执行子类的构造块、构造方法
多态性,是指一种事物的多种表现形态
举例:
1.让张三去买瓶饮料
可能会买到:红牛、雪碧、大个核桃、可乐等
2.让李四买一个宠物
可能会买到:松鼠、乌龟、小猫、小狗…
3.让大家声明一个整数
可能创建:byte 、short、int、long
父类类型 引用名 = new 子类();
//饮料 引用 = new红牛();
接口类型 引用 = new 实现类();
当父类类型的引用指向子类类型的对象时,父类类型的引用,当成父类对象使用?还是当成子类对象使用的?
父类类型的引用,其实是当成父类对象使用的,所以可以直接访问父类对象的方法,不能直接访问子类对象的方法
1.继承
2.重写
3.父类类型引用指向子类对象
如果需要访问子类对象的属性、方法,需要做类型转换
其中,父类类型转换为子类类型,称为向下造型,需要强制类型转换
其中,子类类型转换为父类类型,称为向上造型,是自动转换的
语法格式:
强制类型转换:子类类型 引用名 = (子类类型)父类对象;
例如:Student s = (Student)p;
注意事项:
1.引用类型的转换必须发生在父子类之间,否则编译报错
2.拥有父子关系的对象,在进行强制类型转换时,如果转换的目标类型并不是当前引用真正指向的类型时,会在运行阶段报类型转换的异常
为了避免这种错误的发生,在强制类型转换时,使用instanceof判断当前引用是否是目标类型的实例(对象)
语法格式:
if(引用变量名 instanceof 引用类型){ 语句块 }
自定义矩形类,成员变量主要有:横坐标、纵坐标、长度、宽度,成员方法有: 打印所有成员变量的方法,实现矩形类的封装。
自定义圆形类,成员变量主要有:横坐标、纵坐标、半径,成员方法有:打印所有成员变量的方法,实现圆形类的封装。
自定义测试类并实现如下方法:
在main()方法中分别创建矩形和圆形的对象,自定义成员方法,要求:
根据参数传入的圆形对象来调用该对象的draw方法
根据参数传入的矩形对象来调用该对象的draw方法
多态的作用在于屏蔽不同子类的差异性,从而实现通用的编程
经验:
在以后的开发中推荐使用父类的引用指向子类对象的形式,因为这种情况下引用直接调用的方法一定是父类拥有的方法,此时若更改指向的子类对象,那么后续直接调用的方法不需要做任何的修改直接生效,因此提供了代码的可维护性。
当父类的一些方法不确定如何具体实现时,可以用abstract关键字修饰该方法,此时该方法称为“抽象方法”, 而这个类就称为“抽象类”。
也就是说,抽象类就是使用abstract关键字修饰的类。
抽象方法:访问修饰符 abstract 返回值类型 方法名(形参列表);
如:
public abstract void cry();
抽象类不在于实例化(创建对象),而在于被继承,因为一旦子类继承了抽象类,那么子类就必须实现抽象类中的抽象方法
所以说:抽象类对子类具有强制性、规范性
1.抽象方法没有方法体
2.抽象类不能实例化对象(构造对象)
3.抽象类中可以有成员变量、成员方法、构造方法
4.子类一旦继承了抽象类,就必须实现抽象类中的抽象方法(除非当前子类也声明为抽象类)
请设计抽象类超人 Superman,属性有: 名字,年龄。
方法有:run 跑, fly 飞, attack 攻击
然后写 蜘蛛侠,蝙蝠侠,和钢铁侠分别都继承 Superman ,并创建各自的对象实例
接口就是比抽象类还抽象的类,体现在没有构造方法、没有成员变量
语法格式:
public interface 接口名称{
//常量
//抽象方法
}
有了抽象类,为什么还要使用接口?
历史遗留问题,类只能单继承,也就是说一个类继承了一个类之后,就不能再继承其他类,而接口的设计为了弥补单继承不足,一个类可以实现多个接口
设计一个USB接口,该接口有3个抽象方法:paixian()、chicun()、lianjie()
再定义3个实现类:数据线、U盘、鼠标,实现USB接口中的抽象方法
定义测试类,创建对象,并测试
创建接口步骤:
New—Interface—,通常接口使用interface结尾
实现类通过implements关键字实现接口,一旦实现类实现了接口,就必须实现接口中的所有抽象方法
接口不能实例化
接口中只能有抽象方法,没有方法体
接口中可以有属性,但是只能是常量,例如:public static final double PI = 3.14;
一个实现类可以实现多个接口,多个接口之间使用逗号隔开,例如:
public class MysqlImpl implements DBInterface,UsbInterface{}
接口与接口之间可以继承,使用extends关键字,例如:
public interface DBInterface extends UsbInterface{}
定义抽象类的关键字是abstract,定义接口的关键字是interface。
继承抽象类的关键字是extends,而实现接口的关键字是implements。
继承抽象类是单继承,而实现接口是多实现。
抽象类中有构造方法,但接口中没有。
抽象类中可以有成员变量,而接口中只有常量。
抽象类中可以有成员方法,而接口中只有抽象方法。
抽象类中增加方法可以不影响子类,但接口一定影响子类。
在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美, 在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避 免的,比如:客户输入数据的格式,读取文件是否存在,网络是否始终保持 通畅等等。
异常:在Java语言中,将程序执行中发生的不正常情况称为“异常” 。 (开发过程中的语法错误和逻辑错误不是异常)
Java程序在执行过程中所发生的异常事件可分为两类:
1.Error:Java虚拟机无法解决的严重问题。
如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError和OOM。一般不编写针对性 的代码进行处理。
2.Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,可以使 用针对性的代码进行处理。
程序在编译阶段出现的异常,如果不解决,程序无法编译通过,又称为:Checked Exception
常见的编译时异常有:IOException、FileNotFoundException、ClassNotFoundException、IllegaArguementException 、SQLException等。
又称为Unchecked Exception,编译时没有检测出异常,但是在运行时发现异常。
java.lang.RuntimeException类及它的子类都是运行时异常。
ArithmeticException,算数异常
NullPointerException,空指针异常
ArrayIndexOutOfBoundsException,数组下标越界异常
ClassCastException,类型转换异常
NumberFormatexception,数字格式异常
…
系统默认的异常初始,是指直接打印错误信息、错误原因、错误所在的行号
通过异常捕获,可以更加灵活的处理异常信息,例如我们拿到异常信息之后,可以写入到日志文件中。
语法格式:
try{
// 尝试执行可能出现异常的代码
}catch(异常类型 变量名){
// 如果出现异常,自动将异常对象传入到catch块
}finally{
// 最终要执行的代码
// 不管尝试执行的代码有没有出现异常,都执行这里
}
代码演示:
如果异常信息暂时无法处理,暂时处理不了,可以将异常向外抛出去,抛给方法的调用者。
如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。
在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可 以是方法中产生的异常类型,也可以是它的父类。
语法格式:
public void show() throws 异常类型{
Java异常类对象除在程序执行过程中出现异常时由系统自动生成并 抛出,也可根据需要使用人工创建并抛出。
1.首先要生成异常类对象,然后通过throw语句实现抛出操作(提交给Java运行环境)。
IOException e = new IOException();
throw e;
2.可以抛出的异常必须是Throwable或其子类的实例。
一般地,用户自定义异常类都是RuntimeException的子类。
自定义异常类通常需要编写几个重载的构造器。
自定义异常需要提供serialVersionUID
自定义的异常通过throw抛出。
自定义异常最重要的是异常类的名字,当异常出现时,可以根据名字判断异常类型。
java内置的异常类,封装的异常信息都是内置的
通过自定义异常类,可以自己编写异常信息,例如:数据取值范围错误信息。
通常,自定义的异常类,需要提供2个构造:无参构造、有参构造(String类型)
如何抛出自定义的异常呢?
throw new 自定义异常类(“自定义的异常信息”);
throw new TravleException(“票已售罄…”);
结合异常处理完善学生管理系统:
添加学生时,如果年龄大于150岁或小于0岁时,抛出异常:年龄不合法
1.自定义年龄的异常类
保存在电脑硬盘上的可执行文件
目前主流的操作系统都支持多进程,是为了让操作系统可以同时执行多个任务,但进程是重量级的,新建进程对系统资源的消耗比较大,因此进程的数量比较局限。
线程是进程内部的程序流,也就是说操作系统支持多进程,而每个进程的内部又支持多线程,并且线程是轻量级的,会共享所在进程的资源,因此以后主流的开发都采用多线程技术。
在进程内部,只运行一个程序,在java体现在只执行main方法
举例:
单线程好比是,1个窗口卖票,顾客排队
在java中,执行main方法的线程,称为主线程
在进程内部,同时运行多个程序,在java中体现在同时执行多个方法
举例:
你早上上班,正要打卡的时候,手机响了。。你如果先接了电话,等接完了,在打卡,就是单线程。如果你一手接电话,一手打卡。就是多线程。
多线程的好处:
可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
1.高并发
系统接受实现多用户多请求的高并发时,通过多线程来实现。
2. 后台处理
一个程序是线性执行的。如果程序执行到要花大量时间处理的任务时,那主程序就得等待其执行完才能继续执行下面的。那用户就不得不等待它执行完。
这时候可以开线程把花大量时间处理的任务放在线程处理,这样线程在后台处理时,主程序也可以继续执行下去,用户就不需要等待。线程执行完后执行回调函数。
3. 大任务
大任务处理起来比较耗时,这时候可以起到多个线程并行加快处理(例如:分片上传)。
JDK1.5之前创建多线程有两种方法:
1.继承Thread类的方式
2.实现Runnable接口的方式
JDK5.0 新增了2种多线程创建方式:
1.实现Callable接口
2.使用线程池
8.1方式一:继承Thread类
需求:使用多线程技术打印奇数、偶数,其中一个线程打印奇数,另一个线程打印偶数,这两个线程同时运行。
步骤:
1.自定义一个类,并继承java.lang.Thread类
2.重写run方法
3.调用start方法开启多线程
1.执行main方法的线程,称为主线程,执行run方法的线程,称为子线程
2.执行start方法之前,只执行一次主线程,当调用start方法之后,线程数瞬间由1个变成2个,其中主线程继续执行main方法,然后新创建的子线程执行run方法
3.main方法执行完毕,主线程结束,run方法执行完毕,子线程结束
Thread():创建新的Thread对象
Thread(String threadname):创建线程并指定线程实例名
void start(): 启动线程,并执行对象的run()方法
run(): 线程在被调度时执行的操作
getName(),返回线程的名称
setName(),设置线程名称
static void sleep(long millis) - 用于让当前线程休眠参数指定的毫秒数。
static Thread currentThread(): 返回当前线程。在Thread子类中就 是this。
编写龟兔赛跑多线程程序,设赛跑长度为30米
兔子每跑完10米休眠10秒
乌龟跑完20米休眠1秒
乌龟和兔子每跑完1米输出一次结果,看看最后谁先跑完
需求:使用多线程技术实现多窗口卖早餐案例
步骤:
1.自定义类实现Runnable接口,并重写run方法,并编写卖早餐的代码
2.实例化该Runnable接口的实现类对象
3.实例化一个Thread类对象,并把该对象传递到参数中
4.调用start方法
继承Thread类和实现Runnable接口的区别:
区别:
1.继承Thread类的方式不适合资源共享,而实现Runnable接口的方式更适合资源共享
举例:
继承Thread类的方式,相当于拿出2件事即2个卖100张票的任务分别给三个
窗口,他们各自做各自的任务,即各自卖各自的100张票。
实现Runnable接口的方式, 相当于是拿出一个卖10张票的任务给三个窗口共
同去完成。
2.Java只能单继承,因此采用继承Thread的方式,在以后进行代码重构时,无法继承别的类,而接口是可以多实现的,可以实现多个接口
特点:
实现Callable接口的方式可以将子线程的结果返回到主线程,也可以处理异常
步骤:
1.创建一个类,实现Callable接口
2.重写call方法,编写多线程代码,并返回结果
3.创建FutrueTask对象,并将Callable接口的实现类对象传递到FutrueTask构造方法
4.创建Thread对象,并将FutrueTask对象传递到构造方法,并调用start方法
思路:
可以先初始化一个线程池,需要时从线程池中取出线程执行异步任务,使用完再归还到线程池,这样可以避免频繁的创建、销毁线程,从而提升系统的性能
步骤:
1.初始化一个线程池,并指定线程个数【通常是CPU内核数*2】
2.从线程池中取出线程执行异步任务
原生方式创建线程池
创建线程池另一种方式:new ThreadPoolExecutor(7个参数)
/*
* 1. corePoolSize,核心线程池的数量,初始化线程池时创建的线程个数
* 2. maximumPoolSize,线程池中最多创建多少个线程: 100
* 3. keepAliveTime,保持存活的时间,异步任务执行完毕后,等待多长时间销毁多余的线程:1000
* 4. unit,单位
* 5. workQueue,工作队列/阻塞队列,如果任务有很多,就会将多余的的任务放到队列里面,只要有线程空闲,就会去队列里面取出新的任务执行
* 通常使用 LinkedBlockingQueue(无限队列)
* 6. threadFactory,创建线程的工厂,采用默认值{Executors.defaultThreadFactory}
* 7. handler,阻塞队列满了,按照我们指定的拒绝策略拒绝执行任务
*/
public class TestThreadPool2 {
//原生【原始】初始化线程池的方式
public static ThreadPoolExecutor executor =
new ThreadPoolExecutor(
5,
100,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//面试题: 一个线程池core 7:max 20,queue:50, 100并发进来怎么分配?
//1. 先安排7个线程去执行7个任务,剩下的50个先去排队:
//2. 然后再创建13个线程,去执行任务
//3. 剩下的30个线程采用拒绝策略拒绝服务...
}
运行流程:
1、线程池创建,准备好core数量的核心线程,准备接受任务
2、新的任务进来,用core准备好的空闲线程执行。
(1)core满了,就将再进来的人数放入阻塞队列中,空闲的core就会自己去阻塞队列会去任务执行
(2)阻塞对列满了,就直接开新线程执行,最大只能开到max指定的数量
(3)max都执行好了,max-core数量的空闲线程就会在keepAliveTime指定的时间后自动销毁,最终保持到core大小。
(4)如果线程数开到了max的数量,还有新任务进来,就会使用reject指定的拒绝策略进行处理
面试题:
一个线程池core7:max20,queue:50, 100并发进来怎么分配?
答:先有7个线程能直接执行,接下来50个会进入队列排队,在多开13个继续执行。现在70个都安排上了,剩下30个默认拒绝策略。
当多个线程操作同一个共享数据时,当一个线程还未结束时,其他的线程参与进去,此时就会导致共享数据的不一致。
例如:
线程1从账号中取钱时,还未取完时,线程2参与进来,就会造成数据不一致
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
就好比是在火车上上厕所,进去时,先把门锁上,不用了再打开
Java对于多线程的安全问题提供了专业的解决方式:
同步机制:同步代码块、同步方法、锁
其中同步代码块、同步方法,底层采用是隐式锁的形式:synchronized
其中Lock锁,称为显示锁
使用synchronized关键字,修饰成员方法,此时调用该方法的线程都采用同步的方式(排队执行)
区别:
同步代码块比同步方法更加灵活,因为同步方法的话,大家都得排着队
1.从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
2.java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
3.ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
银行有一个账户。 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打 印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】
1,明确哪些代码是多线程运行代码,须写入run()方法
2,明确什么是共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
总结:
当多线程操作共享数据时,为了保证数据的一致性,尽量在操作数据部分加上锁
防止一个线程还未结束时,其他线程参与进来
线程通信的例子:使用两个线程打印1-100。线程1,线程2交替打印
共享数据,会出现安全问题
wait让当前线程进入阻塞状态,同时会释放锁
涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait阻塞的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait阻塞的线程。
注意事项:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现IllegalMonitorStateException异常
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (this) {
notify();
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
线程通信的应用:经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店
员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中
没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产
品。
分析:
class Clerk{
private int productCount = 0;
//生产产品
public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{//生产者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品.....");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消费者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品.....");
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
Consumer c2 = new Consumer(clerk);
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}
java.io.File类,用来描述文件/目录信息的,例如:通过File类可以获取文件名称、大小、修改时间等信息。但是不能访问文件的内容
java.io.File类常用方法:
File(String filename),构造方法,根据参数路径构造文件对象
boolean exists() - 用于判断文件或目录是否存在。
String getName() - 用于获取文件或目录的名称。
long length() - 用于获取文件的大小。
String getAbsolutePath() - 用于获取抽象路径名的绝对路径信息并返回
long lastModified() - 用于获取最后一次修改时间。
boolean delete() - 用于删除调用对象代表的文件或目录。
boolean createNewFile() - 用于创建新的空文件。
File[] listFiles() - 用于获取目录中的所有内容。
boolean isFile() - 用于判断是否为一个文件。
boolean isDirectory() - 用于判断是否为一个目录。
boolean mkdir() - 创建目录
代码演示:
package com.zhentao.file;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TestFile {
public static void main(String[] args) {
//1. 创建文件对象,和指定的文件进行关联
File file = new File("C:/Users/Admin/Desktop/abc.txt");
System.out.println(file.exists());
System.out.println(file.getName()); // 获取文件名称
System.out.println(file.length()); // 获取文件大小,单位:字节
// 1GB = 1024mb 1MB = 1024KB 1KB = 1024Byte
// 一个英文字符 占1个字节,一个中文占2个字节
System.out.println(file.getAbsolutePath()); // 获取绝对路径
//绝对路径:唯一的路径,从盘符开始,C:/Users/Admin/Desktop/abc.txt
//相对路径:从当前目录开始,找目标文件的路径: ./ 当前目录, ../ 上一层目录
System.out.println(file.lastModified()); //最后一次修改时间,时间戳
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date(file.lastModified());
String time = sdf.format(date);
System.out.println(time);
boolean res = file.delete(); //删除文件 这里危险,一定要指定某个文件
System.out.println(res);
try {
boolean res2 = file.createNewFile(); //创建file指向的文件
System.out.println(res2);
System.out.println(file.isFile()); //判断是否是一个文件
System.out.println(file.isDirectory()); //判断是否是一个目录
File file2 = new File("D:/1912A/Java基础/02.07/test");
//file2.mkdir(); //创建目录
File[] files = file2.listFiles();
for(int i=0;i
封装一个方法,递归的读取某个目录下面的所有文件名称、文件最后修改时间、文件大小。如果该目录下存在子目录,则读取子目录下面的文件信息。
IO,Input、Output两个单词的缩写
IO流,就像水流一样不间断的进行数据的读写操作
IO流分类:
1.根据数据的流向分为:输入流、输出流
输入流,简称入流,是指将文件中的内容读取到内存中
输出流,简称出流,是指将内存中的数据写入到文件中
2.根据数据单位分为:字节流、字符流
字符流,以字符为单位进行读写操作,主要针对文本文件的操作,一个字符一个字符的读写,例如:字符”a”、字符“中”
字节流,以字节为单位进行读写操作,可以对任何文件进行读写操作
字节,是计算机中最小的单位,通常一个英文字符占1个字节,1个中文字符占2个字节
什么是字节?
计算机中最小的容量单位
1TB = 1024GB
1GB=1024MB
1MB=1024KB
1KB=1024Byte
1Byte=8位(计算机底层只认识0101这样二进制数据)
1字节 = 0000 0000
所谓的字节流,就是一个字节一个字节的传输,通常用于图片、视频、音频等文件的读写
java.io.FileInputStream类,用于对图片、视频、音频等文件的读取操作
常用方法:
FileInputStream(String name) - 根据参数指定的路径名来构造对象与之关联。
int read() - 用于从输入流中读取一个字节的数据并返回,若读取到文件尾则返回-1
int read(byte[] b) - 用于从输入流中读满整个参数指定的数组。
- 若读取到文件尾则返回-1,否则返回实际读取到的字节数。
int read(byte[] b, int off, int len) - 读取len个字节到数组b中。
int available() - 用于获取关联文件的大小并返回。
void close() - 关闭输入流并释放资源。
代码演示3:
一次性读取指定的字节个数,然后再将读取的字节写入到数组中
int read(byte[] b, int off, int len)
参数1:将读取的字节保存的一个数组
参数2:向数组中写入字节时的偏移量(跳过的元素个数)
参数3:从输入流中读取的长度(字节个数)
java.io.FileOutputStream类,用于对图片、视频、音频文件的写入操作
常用方法
FileOutputStream(String name) - 根据参数指定的路径名来构造对象并关联起来。
FileOutputStream(String name, boolean append) - 以追加的方式构造对象。
void write(int b) - 用于将参数指定的单个字节写入输出流。
void write(byte[] b) - 用于将参数指定的字节数组内容全部写入输出流中。
void write(byte[] b, int off, int len)
void close() - 关闭输出流并释放有关的资源.
代码演示3:
void write(byte[] b, int off, int len)
使用3种方式实现文件的拷贝
方式一:
一次性读取一个字节,然后再将读取的字节写入到输出流
不足之处:如果文件较大,该方式效率较低
不足:如果文件过大,不能立即在内存中申请足够的空间
方式三:
一次性读1024个字节,再一次性写入1024个字节
如果你的计算机性能较高,可以一次性读10K…等
java.io.ObjectOutputStream类用于将对象写入到文件中,
前提是:只支持将实现了java.io.Serializable 接口的对象写入到文件中
一个类通过实现java.io.Serializable接口来启用其序列化功能,所谓的序列化就是将一个对象转换成字节码的过程
代码演示:
将对象写入到文件中
1.类先实现java.io.Serializable接口,来启用序列化功能
2.通过ObjectOutputStream类将对象写入到文件
java.io.ObjectInputStream类,用于从一个文件中读取对象的信息
自定义Teacher类,实例化3个Teacher对象,并将这3个Teacher对象写入到文件
新建测试类,读取该文件中存储的3个Teacher对象并打印信息
1.自定义Teacher类,并实现java.io.Serializable接口
2.调用ObjectOutputStream对象的writeObject方法,将对象写入文件
大家预习ObjectInputStream、ObjectOutputStream,完成如下需求:
实现IO流版学生信息管理系统
添加学生时,将学生信息保存到本地文件中
遍历学生时,从本地文件中读取学生信息再遍历
修改、删除时也是一样
学号、年龄增加上异常管理
字符流,就是一个字符一个字符的传输,不管中文,还是英文,通常用于文本文件的读写。
java.io.FileWriter类,用于向文本文件中写入字符数据
java.io.FileReader类,用于从文本文件中读取字符数据
Properties类,表示一个属性集合,用来对键值对数据(key=value)进行读写操作
常用方法:
setProperty(String key,String value),向集合中添加数据
store,把集合中的数据持久化到文件中
load,把文件中的数据读取到内存中
getProperty(String key),从集合中根据key获取值
练习:
使用Properties类,创建一个属性文件db.properties,有如下内容:
host=localhost
user=root
pass=root
port=3306
然后再使用Properties类读取该属性文件,并输出
一个Excel是由下面几个部分组成的:
一个Excel文件对应Workbook(工作簿)
一个Workook里面包括多个Sheet(工作表)
一个Sheet包含行、列
Java对Excel文件的操作通常使用POI进行的
POI是由Apache软件基金会开源的一个产品,用于对Excel文件进行读写操作
将poi-3.9.jar 导入到java工程中
右击工程名(0211)— New — Source Folder— lib
再将poi-3.9.jar拷贝到lib目录下
右击poi-3.9.jar—Build Path(构建路径)—Add to Build Path(添加到构建路径)
到此,我们就可以在java工程中使用该jar包提供的工具类
/**
* 创建一个Excel文件
*/
public class TestCreateExcel {
public static void main(String[] args) {
//1. 创建一个Workbook(工作簿)
HSSFWorkbook workbook = new HSSFWorkbook();
//2. 创建工作表Sheet
HSSFSheet sheet = workbook.createSheet("Java大数据");
//3. 创建行: 0就表示第一行
HSSFRow row = sheet.createRow(0);
//4. 创建列
HSSFCell cell1 = row.createCell(0); // 第一列
HSSFCell cell2 = row.createCell(1); // 第二列
HSSFCell cell3 = row.createCell(2); // 第三列
//5. 设置列的内容
cell1.setCellValue("学号");
cell2.setCellValue("姓名");
cell3.setCellValue("年龄");
try {
//6. 生成一个excel文件
OutputStream out = new FileOutputStream("d:/student.xls");
workbook.write(out);
System.out.println("创建成功");
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 创建一个Excel文件
*
*/
public class TestCreateExcel3 {
public static void main(String[] args) {
//1. 创建一个Workbook(工作簿)
HSSFWorkbook workbook = new HSSFWorkbook();
//2. 创建工作表Sheet
HSSFSheet sheet = workbook.createSheet("第一次月考成绩");
//定义好字体对象
Font font = workbook.createFont();
//设置字体大小
font.setFontHeightInPoints((short)12);
//设置字体颜色
font.setColor(Font.COLOR_RED);
//设置字体粗细
font.setBoldweight(Font.BOLDWEIGHT_BOLD);
//创建cellStyle对象,该对象用来设置单元格的样式
HSSFCellStyle cellStyle = workbook.createCellStyle();
cellStyle.setFont(font);
//添加水平居中
cellStyle.setAlignment(CellStyle.ALIGN_CENTER);
//3. 创建行: 0就表示第一行
//第一行、第一列
HSSFRow row1 = sheet.createRow(0);
HSSFCell cell1 = row1.createCell(0);
cell1.setCellValue("第一次月考成绩");
//再将该cellStyle添加到cell单元格中
cell1.setCellStyle(cellStyle);
//合并单元格
CellRangeAddress region = new CellRangeAddress(0, 0, 0, 2);
sheet.addMergedRegion(region);
// 第二行
HSSFRow row2 = sheet.createRow(1);
String[] titles = {"学号","姓名","成绩"};
for(int i=0;i
jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM方式,CSS选择器以及类似于jQuery的操作方法来取出和操作数据。
jsoup官网:http://jsoup.org
DOM,Document Object Model,文档对象模型,将HTML标签解析成Java对象的
绘图说明:
1.先导包
将jsoup-1.11.3.jar包导入到java工程中
先创建lib目录,将jsoup-1.11.3.jar拷贝到lib目录下
右击jsoup-1.11.3.jar包,Build Path—Add to Build Path
2.快速体验
DOM将HTML标签解析成java对象之后,这些对象会在内存中存储,接下来我们要做的就是从内存中查找这些Java对象
getElementById通过id来获取
getElementsByClass通过class来获取
通过该方法来查找的话,返回值是集合类型,会返回多个元素
getElementsByAttributeValue(),根据属性值获取元素节点
get(index), 通过下标从集合中获取元素
text()获取标签的文本,再次强调一下是文本
html()获取标签里面的所有字符串包括html标签
attr(attributeKey)获取属性里面的值,参数是属性名称
Jsoup.connect(url).post(); 连接到指定的html文件,然后将该文件解析成Document文档对象
大家使用Jsoup读取下面文件中所有的岗位信息:
https://search.51job.com/list/010000,000000,0000,00,9,99,java,2,1.html
2.从HTML文件中读取到岗位信息后,将岗位信息封装到Job对象
public class GetJobs {
//定义一个集合
public List<Job> list = new ArrayList<>();
//封装一个方法,用来将岗位信息封装到list集合中
public List<Job> getJobs() throws IOException{
//1. 定义爬取哪个网页上面的岗位信息
String html = "C:\\Users\\Admin\\Desktop\\51job.html";
//2. 将上面的网页解析成Document对象【DOM树】
File file = new File(html);
Document document = Jsoup.parse(file,"UTF-8");
//3. 查找j_joblist节点
//By...通过...方式,i go to school by bike
//Class,类
//by class,通过类名
//get 获取,得到
//Elements,Element的复数形式,多个元素/节点对象
Elements joblist = document.getElementsByClass("j_joblist");
//从集合中取出joblist,通过下标0
Element job = joblist.get(0);
//System.out.println(job);
//4. 从joblist中取出所有的岗位:class="e"
Elements es = job.getElementsByClass("e");
//取出每个岗位,所以需要遍历集合
for(int i=0;i
//封装一个方法,将岗位集合存储到Excel中
//参数1:岗位集合,参数2:目标文件名
public void saveToExcel(List<Job> list,String file) throws IOException{
//1. 创建一个工作簿
HSSFWorkbook workbook = new HSSFWorkbook();
//2. 创建工作表
HSSFSheet sheet = workbook.createSheet("Java招聘岗位");
//3. 创建第一行
HSSFRow row = sheet.createRow(0);
//4. 创建第一行的5列
//设置第1行的5列【标题】
String[] titles = {"岗位名称","工作地点","薪水","公司名称","发布时间"};
for(int i=0;i
目前主流的网络通讯软件:QQ、微信、阿里旺旺…
七层网络模型:
ISO(国际标准委员会组织)将数据的传递从逻辑上划分为以下七层:
应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
OSI(Open System Interconnect),即开放式系统互联,是ISO(国际标准化组织)在1985年研究的网络互联模型。
OSI七层模型和TCP/IP五层模型的划分如下:
TCP/IP对应用层、表示层、会话层进行了封装,所以包含5层
当发送数据时,需要对发送的内容按照上述7层模型进行层层加包,再发送出去
当接收数据时,需要对接收的内容按照上述7层模型相反的次序层层拆包解析出来
实现网络通信的两个要素:
IP地址和端口号
网络通信协议
IP地址
唯一的表示Internet上的计算机的位置,通过IP地址可以找到对方的计算机。
DNS,Domain Name System 域名系统,它是将域名和IP地址相互映射的一个分布式数据库,例如:(baidu.com => 220.181.38.148)
本地回环地址,本地计算机内部使用的IP地址,127.0.0.1,主机名(hostName):localhost
IP地址分类:
IPV4、IPV6
IPV4:4个字节(32位)组成,4个0-255。大概42亿,30亿都在北美,亚洲4亿。2011年初已经用尽,以点和10进制表示,如:192.168.0.1
IPV6:16个字节( 128位),写成8个无符号整数,每个整数用4个16进制位表示
公网地址、私有地址
公网地址,在万维网上的IP地址,全世界都能访问到的IP地址
私有地址,局域网内使用的IP地址,专门为组织机构内部使用,例如:
192.168开头的就是私有地址
IP地址相关的命令
ping,可以查看网络连接状态
ipconfig,可以查看当前计算机的ip地址
端口号,用来标识正在计算机上运行的进程(程序)
不同的进程有不同的端口号
被规定为一个0~65535之间的整数
端口号分类:
公认端口:0~1023,被预先定义的服务通信占用(如:HTTP占用端口80,FTP占用端口21,Telnet占用端口23)
注册端口:1024~49151,分配给用户进程或应用程序,如:tomcat占用端口8080,mysql占用端口3306,Oracle占用端口1521
动态/私有端口:49152~65535
IP地址和端口号的组合得出一个网络套接字(插槽):Socket
TCP协议,Transmission Control Protocol传输控制协议,是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据。
在TCP协议中必须明确客户端和服务器端,由客户端向服务器端发出请求,每次连接都要经过三次握手,而终止一个连接要经过四次挥手
特点:
使用TCP协议前,须先建立TCP连接,形成传输数据通道
传输前,采用三次握手的方式,点对点通信,是可靠的
TCP协议须明确两个应用进程:客户端、服务端
在此连接中可进行大数据量的传输
传输完毕,须释放已建立的连接,采用四次挥手
四次挥手
为什么建立一个连接需要三次握手,而终止一个连接要经过四次挥手?
释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。
服务器:
1.创建ServerSocket类型的对象,并提供端口号
2.等待客户端的连接请求,调用accept方法
3.使用输入输出流进行通信
4.关闭socket
客户端:
1.创建Socket类型的对象,并提供服务器的IP地址和端口号
2.使用输入输出流进行通信
3.关闭Socket
在Java中,提供了两个类用于实现TCP通信程序:
java.net.Socket类,客户端类,用于向服务端发出请求,接收服务端响应
java.net.ServerSocket类,服务端类,相当于开启一个服务,等待客户端连接
java.net.Socket类,TCP通信客户端类,向服务器发送连接请求,给服务器发送数据,读取服务器回写的数据
java.net.Socket类常用的方法如下:
Socket(String host,int port) | 根据指定主机名和端口号来构造对象 |
---|---|
InputStream getInputStream() | 用于获取当前套接字的输入流 |
OutputStream getOutputStream() | 用于获取当前套接字的输出流 |
void close() | 用于关闭套接字 |
实现步骤:
创建一个客户端对象socket,构造方法绑定服务器的IP地址和端口号
使用socket对象中的方法getOutputStream(),获取网络字节输出流OutputStream对象
使用网络字节输出流OutputStream对象中的方法write,给服务器发送数据
使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
释放资源(close)
说明:当我们创建客户端对象socket时,就会自动完成3次握手
ServerSocket,TCP通信服务端类,接收客户端的请求,读取客户端发送的数据,给客户端回写数据
服务端实现步骤:
创建一个服务端对象ServerSocket
通过ServerSocket对象的accept方法,获取到请求的客户端对象Socket
使用socket对象的getInputStream()方法获取网络字节输入流InputStream对象
通过网络字节输入流InputStream对象的read方法,读取客户端发送的数据
通过socket对象的getOutputStream获取网络字节输出流OutputStream对象
使用网络字节输出流OutputStream对象的write方法,给客户端回写数据
释放资源
public class SocketServer {
public static void main(String[] args) {
try {
//1. 创建一个服务端对象ServerSocket,启动服务
ServerSocket server = new ServerSocket(8888);
//2. 通过ServerSocket对象的accept方法,获取到请求的客户端对象Socket
Socket socket = server.accept();
//3. 使用socket对象的getInputStream()方法获取网络字节输入流InputStream对象
InputStream in = socket.getInputStream();
//4. 通过网络字节输入流InputStream对象的read方法,读取客户端发送的数据
int res = 0;
while((res = in.read())!=-1 ){
System.out.println((char)res);
}
//5. 通过socket对象的getOutputStream获取网络字节输出流OutputStream对象
OutputStream out = socket.getOutputStream();
//6. 使用网络字节输出流OutputStream对象的write方法,给客户端回写数据
out.write("i received".getBytes());
//7. 释放资源
socket.close();
server.close();
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.创建服务器端
public class Server {
public static void main(String[] args) {
try {
//1. 创建ServerSocket对象,并提供端口号
ServerSocket server = new ServerSocket(8888);
//2. 等待客户端的连接请求,调用accept方法
Socket socket = server.accept();
//3. 开始通信(通过输入、输出流)
//4. 关闭socket
server.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.创建客户端
public class Client {
public static void main(String[] args) {
try {
//1. 创建Socket类型的对象并提供服务器的IP地址和端口号
Socket socket = new Socket("192.168.2.112", 8888);
System.out.println("连接服务器成功");
//2. 使用输入输出流进行通信
//3. 关闭socket
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
客户端发送消息
public class Client {
public static void main(String[] args) {
try {
//1. 创建Socket类型的对象并提供服务器的IP地址和端口号
Socket socket = new Socket("192.168.2.112", 8888);
System.out.println("连接服务器成功");
//2. 使用输入输出流进行通信
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("在吗?");
//3. 关闭socket
socket.close();
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务器读
public class Server {
public static void main(String[] args) {
try {
//1. 创建ServerSocket对象,并提供端口号
ServerSocket server = new ServerSocket(8888);
//2. 等待客户端的连接请求,调用accept方法
Socket socket = server.accept();
//3. 开始通信(通过输入、输出流)
// 接收客户端发送过来的消息,使用BufferedReader类
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String content = reader.readLine();
System.out.println("客户端发送过来的消息是:"+content);
//4. 关闭socket
server.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Client {
public static void main(String[] args) {
try {
//1. 创建Socket类型的对象并提供服务器的IP地址和端口号
Socket socket = new Socket("192.168.2.112", 8888);
System.out.println("连接服务器成功");
//2. 使用输入输出流进行通信
PrintStream ps = new PrintStream(socket.getOutputStream());
//客户端向服务器发送的内容由用户键盘输入
System.out.println("请输入要发送的内容");
Scanner sc = new Scanner(System.in);
String msg = sc.next();
ps.println(msg);
System.out.println("客户端发送数据成功");
//3. 关闭socket
socket.close();
ps.close();
sc.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
现在服务器只有一个main方法,既要接收客户端的请求,又要给客户端响应
采用多线程方式进行优化:
1.主线程接收客户端请求
2.创建子线程给客户端进行响应
Server.java
public class Server {
public static void main(String[] args) {
try {
//1. 创建ServerSocket对象,并提供端口号
ServerSocket server = new ServerSocket(8888);
List<Socket> list = new ArrayList<Socket>();
while(true){
//2. 等待客户端的连接请求,调用accept方法
System.out.println("等待客户端的连接请求");
//没有客户端连接时,会阻塞在accept()这里
Socket socket = server.accept();
list.add(socket);
System.out.println("客户端连接成功");
//只要有客户端连接成功,此时就需要启动新的线程为之服务
new ServerThread(socket,list).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
创建多线程文件ServerThread.java
public class ServerThread extends Thread {
private Socket socket;
private List<Socket> list;
public ServerThread(Socket socket,List<Socket> list) {
this.socket = socket;
this.list = list;
}
@Override
public void run() {
//3. 开始通信(通过输入、输出流)
// 接收客户端发送过来的消息,使用BufferedReader类
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(true){
String content = reader.readLine();
System.out.println("客户端"+socket.getInetAddress()+"发送过来的消息是:" + content);
//服务器向客户端发送消息
//获取输出流对象
if("bye".equals(content)){
System.out.println("客户端"+socket.getInetAddress()+"已下线");
break;
}
for(Socket s:list){
if(s != socket){
PrintStream ps = new PrintStream(s.getOutputStream());
ps.println(content);
}
}
System.out.println("服务器发送数据成功");
}
//4. 关闭socket
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
创建ClientSendThread线程用来发送消息
创建ClientReceiveThread线程用来接收消息
ClientSendThread.java
public class ClientSendThread extends Thread {
private Socket socket;
public ClientSendThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
PrintStream ps = new PrintStream(socket.getOutputStream());
Scanner sc = new Scanner(System.in);
while(sc.next() != null){
//客户端向服务器发送的内容由用户键盘输入
System.out.println("请输入要发送的内容");
String msg = sc.next();
ps.println(msg);
}
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
创建ClientReceiveThread.java
public class ClientReceiveThread extends Thread {
private Socket socket;
public ClientReceiveThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(reader.readLine() != null){
String content = reader.readLine();
System.out.println(content);
if("bye".equals(content)){
System.out.println("聊天结束!");
break;
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
创建Client.java
public class Client {
public static void main(String[] args) {
try {
//1. 创建Socket类型的对象并提供服务器的IP地址和端口号
Socket socket = new Socket("192.168.2.112", 8888);
System.out.println("连接服务器成功");
//2. 使用输入输出流进行通信
new ClientReceiveThread(socket).start();
new ClientSendThread(socket).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
通常情况下,编写的代码是固定的,运行结果也是固定的,如:
Person p = new Person(); 只能构造Person对象
p.show(); 只能调用show方法
有些情况,编写代码时,不确定会创建什么对象,也不确定会调用什么样的方法,而是由运行时传入的参数决定,这种编程方式称为动态编程。而我们今天要学习的反射机制就是动态的创建对象,动态的调用方法的机制
很多框架底层都是用动态编程实现的,在Java里面可以使用反射技术实现动态编程
编译期,将Java源文件也就是敲好的代码通过编译,转换成.class文件,也就是字节码文件(byte)
运行期,类装载器初始化字节码文件,通过本地类库来验证字节码文件的正确性,然后交给JVM的解释器和即时编译器,最后汇合给JVM内部的Java运行系统。
ClassLoader类加载器
将.class文件的字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
类加载完毕后,会在堆内存的方法区创建一个Class类型的对象class。
Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的。
class对象包含了原始类的内部结构(成员变量、成员方法、构造方法),它就像一面镜子能够看到类的内部结构
所谓的反射技术,就是通过class对象获取原始类的构造方法、成员方法等,然后再构造对象
可以通过4种方法获取class对象
在src目录下,创建一个配置文件:data.properties
根据该文件中定义的类,方法,动态的创建对象,并调用方法
public class TestClass4 {
public static void main(String[] args) {
try {
//1.通过IO流的方式获取类的全路径
Properties pro = new Properties();
//创建文件输入流:移植性差
//Reader reader = new FileReader("E:\\workspace\\java01\\0316\\src\\data.properties");
//通过相对路径来读取文件的内容(通过ClassLoader这个类加载器)
ClassLoader loader = TestClass4.class.getClassLoader();
InputStream in = loader.getResourceAsStream("data.properties");
pro.load(in);
//从pro集合中读取cname这个键对应的值
String cname = pro.getProperty("cname");
String m = pro.getProperty("method");
String param = pro.getProperty("param");
System.out.println(cname); // com.wanshi.reflect.Student
System.out.println(m); // dance
//2. 使用反射技术,动态创建对象,动态调用方法
Class clazz = Class.forName(cname);
Constructor constructor = clazz.getDeclaredConstructor();
//根据构造方法创建对象
Object obj = constructor.newInstance();
//调用这个对象的方法: 获取有参数的成员方法
Method method = clazz.getDeclaredMethod(m, String.class);
//调用obj这个对象的method这个方法,参数为:机械舞
method.invoke(obj, param);
} catch (Exception e) {
e.printStackTrace();
}
}
}
类的对象只有有限个,确定的。举例如下:
星期类:Monday(星期一)、…、Sunday(星期天)
性别类:Man(男)、Woman(女)
季节类:Spring(春节)…Winter(冬天)
支付方式类:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银 行卡)、CreditCard(信用卡)
就职状态类:Busy、Free、Vocation、Dimission
订单状态类:Nonpayment(未付款)、Paid(已付款)、Delivered(已发货)、 Return(退货)、Checked(已确认)Fulfilled(已配货)
可以使用常量或枚举类
我们以Season类为例:
public class Season {
public static final int SEASON_SPRING = 1;
public static final int SEASON_SUMMER = 2;
public static final int SEASON_FALL = 3;
public static final int SEASON_WINTER = 4;
}
枚举类更加直观,类型安全。使用常量会有缺陷:
若一个方法中要求传入季节这个参数,用常量的话,形参就是int类型,开发者传入任意类型的int类型值就行,但是如果是枚举类型的话,就只能传入枚举类中包含的对象。
所以,当需要定义一组常量时,强烈建议使用枚举类
JDK1.5之前需要自定义枚举类
JDK 1.5 新增的 enum 关键字用于定义枚举类
若枚举只有一个对象, 则可以作为一种单例模式的实现方式。
自定义订单状态类,提供的订单状态有6种:待支付、已支付、待出库、已发货、已收货、交易关闭,然后新建测试类并访问6种订单状态
注释,是给程序员看的,对代码的说明,代码运行时不会执行注释的内容
注解,Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处理。
注解可以修饰类、成员变量、成员方法,注解都是以@符号开头的
在JavaSE阶段,注解功能比较简单,在JavaEE/Android中比较重要,可以代替冗余的代码,繁琐的XML配置文件
未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式。
示例一:生成文档相关的注解
@author 标明开发该类模块的作者,多个作者之间使用,分割
@version 标明该类模块的版本
@param 对方法中某参数的说明,如果没有参数就不能写
@return 对方法返回值的说明,如果方法的返回值类型是void就不能写
示例二:在编译时进行格式检查(JDK内置的三个基本注解)
@Override: 限定重写父类方法, 该注解只能用于方法
@SuppressWarnings: 抑制编译器警告
@Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。
public @interface 注解名称{
注解里面的成员变量是以无参方法的形式保存,例如:
String className();
String methodName();
}
}
//注解的作用:保存一些数据,程序运行时,可以从注解中获取数据,执行相应的处理
@MyAnnotation(className="com.wanshi.exam.MySQLDB",methodName="insert")
public class TestAnnotation {
public static void main(String[] args) throws Exception {
//1.先读取注解中的内容:com.wanshi.exam.MySQLDB和insert
//1.1 获取当前类的class对象【镜子,保存了类的内部结构:成员变量、成员方法、构造方法】
Class<TestAnnotation> clazz = TestAnnotation.class;
//1.2 通过class对象获取当前类的注解
MyAnnotation anno = clazz.getAnnotation(MyAnnotation.class);
//1.3获取注解中的内容
String cname = anno.className(); // com.wanshi.exam.MySQLDB
String m = anno.methodName(); // insert
//2. 根据类的路径创建对象,然后调用方法
//2.1 Class.forName()获取com.wanshi.exam.MySQLDB这个类的class对象
Class c = Class.forName(cname);
//2.2 获取构造方法
Constructor con = c.getDeclaredConstructor(String.class);
//2.3获取成员方法
Method method = c.getMethod(m, String.class);
//2.4根据构造方法实例化对象
Object obj = con.newInstance("阿猫");
//2/5调用对象的方法
method.invoke(obj, "香蕉");
}
}
设计模式(Design Pattern)是前辈们对代码开发经验的总结,反复使用、多数人知晓,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
ocp,open-close principle,开闭原则
程序开发完毕,对扩展是开放的,对修改是关闭的
如果允许修改的话,万一出了问题,整个项目都会受影响
如果需要给Beauty增加一个age属性,按照ocp原则,不应该直接修改
而应该再声明一个新类,声明age属性,再继承该新类
任何基类可以出现的地方,子类一定可以出现,里氏代换原则是对“开-闭”原则的补充。
因为使用继承,必须保证 子类 is a 父类,建议:尽量多使用继承和多态的形式实现通用编程
尽量依赖抽象类或接口,而不是依赖具体的实现类
例如:下面的代码就把依赖具体的Dog类,转换为依赖一个Animal
尽量依赖于多个小接口,而不是依赖一个大接口
例如:
public interface Animal{
public void run(); // 跑
public void fly(); // 飞
public void eat(); // 吃
}
因为一旦实现了一个接口,必须得实现该接口的所有抽象方法
public class Pig implements Animal{
public void run(); // 跑
public void fly(); // 飞
public void eat(); // 吃
}
此时,Pig类只需要实现run、eat方法,该如何实现呢
将大接口拆分为多个小接口,接口与接口之间可以继承
迪米特法则,最少知道原则
一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
专业术语:高内聚、低耦合
高内聚,把一个特点的功能提取到一个类内部(内聚)
低耦合,尽量减少类与类之间的依赖关系
尽量减少类与类之间的耦合,因为万一有一个类出现了点问题,和他关联的类都要受影响
如果两个类不是父子类的时候,如何相互调用这两个类之间的方法
例如:
有一个A类
public class A{
public void show(){
}
}
定义一个B类,没有继承A类,能否使用A类中的方法呢?
public class B{
// 合成复用原则
private A a;
public B(A a){
this.a = a;
}
//使用a对象的方法
public void say(){
a.show();
}
}
Java中常见的设计模式有23种,可以分为3大类:
总体来说设计模式分为三大类:
新建一个工厂类,对实现了某一个接口的实现类进行对象创建的模式
3.通过工厂类构造对象
就类似于厂子,传递原材料进去,返回成品
总结:
普通工厂模式,根据传递的字符串创建对应的对象,如果字符串写错了,就找不到对象
是对普通工厂方法模式的改进,在普通工厂模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
代码实现
1.在工厂类中增加2个方法,分别创建Sms、Mail对象
静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
工厂方法模式有一个问题就是,如果想要拓展程序,必须对工厂类进行修改,这违背了开闭原则,所以,从设计角度考虑,有一定的问题。
如何解决?
采用抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。因为抽象工厂不太好理解,我们先看看图,然后结合代码,就比较容易理解。
2.提供Provider接口,该接口中有一个produce生产的方法
3.测试
创建发送Mail的工厂,由该工厂生产一个MailSender对象
4 如果将来需要发送包裹了,只需要创建一个包裹工厂类、发送包裹的方法
完成多个工厂方法模式、抽象工厂模式,并进行比较抽象工厂模式的优点
10分钟,手写一个单例设计模式的类出来
并解决多线程操作时的安全问题
Java7(JDK1.7),2011年发布
Java8(JDK1.8),2014年发布,是自Java5以来最具革命性的版本,目前主流的版本就是Java8。
Java8新特性主要体现在:
速度更快
代码更少(Lambda表达式)
强大的Stream流
便于并行
最大化减少空指针异常:Optional
语法格式2:
//1. ()里面的参数的数据类型可以自动推断得出,所以可以省略
自动推断类型,例如:List list = new ArrayList<>();
//2. 如果{}里面只有一条语句,{}和return都可以省略
//3. 如果()里面只有一个参数,()也可以省略
函数式接口,也是Java8新增的特性之一
只包含一个抽象方法的接口称为函数式接口,可以通过Lambda表达式创建该接口的实现类对象。
在函数式接口中,使用@FunctionalInterface注解检测该接口是否是函数式接口
在Java8中,Lambda表达式就是一个函数式接口的实例,也就是说只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。
Java从诞生起一直倡导”一切皆对象”,在Java里面面向对象编程(OOP)是一切。
但是随着Python、Scala、ES6等语言的兴起,和新技术的挑战,Java不得不做出调整以便支持更加广泛的技术要求,也即:Java不但可以支持OOP还可以支持OOF(面向函数编程)。
Consumer,消费者,消费型接口,accept(T) 传递参数但是无返回值
Supplier,供应商,供给型接口,get() 不需要传参,但是有返回值
Function,函数,函数型接口,apply(T),对传递的参数进行改造
Predicate,断言、断定,断定型接口,test(T) 断定传递的参数是否满足约束
Predicate函数式接口练习
内置的唯一的test(T)方法,用来断定传递的参数是否满足约束
方法引用可以看做是Lambda表达式深层次的表达。
引用,就是通过一个方法名字来指向 一个方法。
语法格式:
代码演示:
需求:
有一个集合,需要先从该集合中遍历所有姓张的成员,存到新的集合中,然后再遍历姓张的成员
此时就可以使用Stream流
传统的实现方式:
filter(Predicate p),过滤
forEach(Consumer con),进行遍历操作
map(Function f),对元素进行加工处理
count(),统计元素数量
limit(),限制获取的数量
skip(),跳过
static concat(),合并多个流称为一个流
这些API可以分为两类:
1.返回值为Stream流的,接着调用其他方法,称为链式调用
2.返回值不是Stream流的,不能调用stream流的其他方法,不能链式调用