1.七大设计原则
1.1 开闭原则
(1)定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。强调的是用抽象构建框架,用实现扩展细节
(2)优点:提高软件系统的可复用性及可维护性
(3)例子
假设我们有一个图形绘制程序,它可以绘制不同类型的图形,比如圆形和矩形。我们希望程序能够支持绘制新的图形类型,比如三角形。按照开闭原则,我们应该通过扩展而不是修改来实现这个功能。
①首先,我们定义一个接口 Shape,它包含一个绘制方法 draw:
public interface Shape {
void draw();
}
②然后,我们创建圆形和矩形类,它们实现了 Shape 接口:
public class Circle implements Shape {
public void draw() {
// 绘制圆形的具体实现
}
}
public class Rectangle implements Shape {
public void draw() {
// 绘制矩形的具体实现
}
}
③现在,如果我们要添加新的图形类型,比如三角形,我们只需要创建一个新的类 Triangle,它也实现了 Shape 接口:
public class Triangle implements Shape {
public void draw() {
// 绘制三角形的具体实现
}
}
④通过这种方式,我们可以轻松地扩展系统的功能,而不需要修改现有的代码。这就是开闭原则的应用。
1.2 依赖倒置原则
(1)定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;而抽象不应该依赖于细节,细节应该依赖于抽象;简单来说,就是要针对接口编程,而不是针对实现编程。
(2)优点:可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险
(3)例子:假设我们有一个电子商务系统,需要发送通知给用户,比如发送邮件或短信通知。按照依赖倒置原则,我们应该针对抽象编程,而不是针对具体的实现。
①首先,我们定义一个通知接口 Notification,它包含一个发送通知的方法 sendNotification:
public interface Notification {
void sendNotification(String message);
}
②然后,我们创建具体的邮件通知类 EmailNotification 和短信通知类 SMSNotification,它们实现了 Notification 接口:
public class EmailNotification implements Notification {
public void sendNotification(String message) {
// 发送邮件通知的具体实现
}
}
public class SMSNotification implements Notification {
public void sendNotification(String message) {
// 发送短信通知的具体实现
}
}
③接下来,我们创建一个用户类 User,它包含一个通知方法 notify,该方法接收一个 Notification 接口作为参数:
public class User {
private Notification notification;
public User(Notification notification) {
this.notification = notification;
}
public void notify(String message) {
notification.sendNotification(message);
}
}
④现在,我们可以通过以下方式使用依赖倒置原则:
Notification emailNotification = new EmailNotification();
User user1 = new User(emailNotification);
user1.notify("This is an email notification");
Notification smsNotification = new SMSNotification();
User user2 = new User(smsNotification);
user2.notify("This is an SMS notification");
通过这种方式,高层模块 User 不依赖于具体的通知实现,而是依赖于抽象的 Notification 接口。这样我们可以轻松地扩展系统,添加新的通知方式,而不需要修改 User 类的代码。这就是依赖倒置原则的应用。
1.3 单一职责原则
(1)定义:不要存在多于一个导致类变更的原因。一个类、接口、方法只负责一项职责
(2)优点:降低类的复杂度、提高类的可读性、提高系统的可维护性、降低变更引起的风险
1.4 接口隔离原则
(1)定义:用多个专门的接口,而不是使用单一的总接口,客户端不应该依赖它不需要的接口
(2)注意:
①一个类对应一个类的依赖应该建立再最小的接口上
②建立单一接口,不要建立庞大臃肿的接口
③尽量细化接口,接口中的方法尽量少
1.5 迪米特法则
一个对象应该对其他对象保持最少的了解。又叫最少知道原则,尽量降低类与类之间的耦合
1.6 里氏替换原则
(1)定义:里氏替换原则是继承复用的基石,是开闭原则的补充
①子类可以扩展父类的功能,但是不能修改父类原有的功能
②子类可以实现父类的抽象方法,但是不能覆盖原有父类的方法
③子类可以增加自己特有的方法
④对子类的继承关系进行约束,是开闭原则的补充
⑤可以增加程序的健壮性
1.7 合成复用原则
(1)尽量使用对象组合,聚合的方式,而不是使用继承关系达到软件复用的目的
(2)可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
2.设计模式
2.1 创建型模式
2.2.1 单例模式
(1)概念:在整个的软件系统中,保证一个类只有一个对象实例,并且该类只提供一个取得其对象实例的方法(即静态方法)
补充:为什么是静态方法?
要想访问一个类中的方法有两个方式,
①new一个对应类的对象,再通过对象.方法()的方式来调用成员方法
②通过类名.方法名()这样来调用对应的类方法
由于单例模式中为了避免外界直接使用构造方法创建实例对象,将构造方法隐藏了起来。此时第一种方法显然无法实现,只有通过第二种方法来获取实例,所以单例模式中的getInstance()方法必须被定义为static方法。
(2)常见使用场景
①Windows的Task Manager(任务管理器)就是很典型的单例模式
②Windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例
③项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取
④网站的计数器,一般也是采用单例模式实现,否则难以同步。
⑤应用程序的日志应用,一般都采用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加
⑥数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源
⑦操作系统的文件系统,也是单例模式实现的具体例子,一个操作系统只能有一个文件系统
⑧Application也是单例的典型应用(Servlet编程中会涉及到)
⑨在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
⑩在servlet编程中,每个Servlet也是单例
⑪在SpringMVC框架/strus1框架中,控制器对象也是单例的
(3)单例模式的优点
①由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
②单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理
2.2.1.1 饿汉式
(1)饿汉式(静态常量)
①步骤如下:
1)构造器私有化 (防止 new)
2)类的内部创建对象
3)向外暴露一个静态的公共方法,getInstance
②代码如下
class Singleton {
//1. 构造器私有化, 外部能new
private Singleton() {
}
//2.本类内部创建对象实例
private final static Singleton instance = new Singleton();
//3. 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
③优缺点说明:
1)优点:写法简单;在类装载的时候就完成实例化,避免了线程同步的问题。
2)缺点:没有达到懒加载(Lazy Loading)的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
3)这种方式基于类加载器(classloder)机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化实例(instance)就没有达到lazy loading的效果
4))结论:这种单例模式可用,可能造成内存浪费
(2)饿汉式(静态代码块)
①优缺点说明:
1)这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
2)结论:这种单例模式可用,但是可能造成内存浪费
2.2.1.2 懒汉式
(1)懒汉式(线程安全,同步方法)
①优缺点说明:
1)解决了线程不安全问题
2)效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行
同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,
直接return就行了。方法进行同步效率太低
3)结论:在实际开发中,不推荐使用这种方式
②代码
2.2.1.3 双重检查
(1)代码
(2)优缺点说明:
1)双重检查(Double-Check)概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)
检查,这样就可以保证线程安全了。
2)这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null)
,直接return实例化对象,也避免的反复进行方法同步.
3)线程安全;延迟加载;效率较高
4)结论:在实际开发中,推荐使用这种单例设计模式
面试1:双重检查加锁单例模式为什么两次判断?
(1)第一个if(singleton==null)
是为了提高代码执行效率,单例模式只要一次创建实例即可,当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块去竞争锁,直接返回前面创建的实例即可。
(2)第二个if(singleton==null)
是为了防止二次创建实例。假如A与B同时调用getInstance()方法时,判断第一个if都为空,这时A拿到锁,进行第二层if判断,条件成立new了一个对象;由于Synchronized原因,B在外层等待,A创建完成,释放锁,B拿到锁,进行第二层if判断,条件不成立,防止二次创建实例。
面试2:为什么有了Synchronized却还需要volatile去修饰Instance。
volatile修饰变量只是为了禁止指令重排序,因为在 Instance = new Singleton(); 创建对象时,底层会分为四个指令执行:(下面是正确的指令执行顺序)
①、如果类没有被加载过,则进行类的加载
②、在堆中开辟内存空间 adr,用于存放创建的对象
③、执行构造方法实例化对象
④、将堆中开辟的内存地址 adr 赋值给被volatile修饰的Instance引用变量
如果Instance引用变量不使用volatile修饰的话,则可能由于编译器和处理器对指令进行了重排序,导致第④步在第③步之前执行,此时Instance引用变量不为null了,但是Instance这个引用变量所指向的堆中内存地址中的对象是还没被实例化的,实例对象还是null的;那么在第一次判空时就不为null了,然后去使用时就会报空指针异常了。
2.2.2 工厂模式
(1)实现了创建者和调用者的分离,用工厂方法代替new操作
(2)应用场景
①JDK中Calendar的getInstance方法
②JDBC中Connection对象的获取
③Hibernate中SessionFactory创建Session
④Spring中IOC容器创建管理bean对象
⑤XML解析时的DocumentBuilderFactory创建解析器对象
⑥反射中Class对象的newInstance()
2.2.2.1 简单工厂模式
(1)定义:
简单工厂模式是指由一个工厂对象决定创建出哪一种产品类的示例。属于创建型模式,但它不属于GOF 23种设计模式。虽然某种程度不符合设计原则,但实际使用最多。
(2)缺点:工厂类的职责相对过重,增加新的产品时需要修改工厂类的判断逻辑,违背开闭原则。不易于扩展过于复杂的产品结构。
(3)实现代码
public class ScannerManager {
private ScannerManager(){
}
private static final Map<String, IScanner> SCANNERS = new Hashtable<>();
public static IScanner initScanner(String code,String protocol, String brand, String addr){
IScanner scanner = SCANNERS.get(code);
if(Objects.equals(brand,"HONEY_WELL")){
scanner = new HoneyWellScanner(code,addr.split(":")[0],Integer.valueOf(addr.split(":")[1]));
} else if (Objects.equals(brand,"DATA_LOGIC")){
scanner = new DataLogicScanner(code, addr.split(":")[0], Integer.valueOf(addr.split(":")[1]));
} else if (Objects.equals(brand,"HIKR")){
scanner = new HikrScanner(code, addr.split(":")[0], Integer.valueOf(addr.split(":")[1]));
}
SCANNERS.put(code,scanner);
return scanner;
}
public static IScanner getScanner(String code){
return SCANNERS.get(code);
}
}
2.2.2.2 工厂方法模式
(1)概念:定义一个接口或一个抽象的工厂类,让它的实现类(也是一个工厂),来决定创建哪一个实例对象。根据每个工厂不同的方法,来产生不同的所需要的对象。
(2)适用场景:创建对象需要大量重复代码。客户端(应用层)不依赖于产品类示例如何被创建、实现等细节。一个类通过其子类来指定创建哪个对象。
(2)代码
//接口Car
public interface Car {
void run();
}
// Audi类
public class Audi implements Car{
@Override
public void run() {
System.out.println("奥迪在跑");
} }
// Byd类
public class Byd implements Car {
@Override
public void run() {
System.out.println("比亚迪在跑");
} }
//CartFactory工厂接口
public interface CarFactory {
Car createCar();
}
//AudiFactory工厂类
public class AudiFactory implements CarFactory{
@Override
public Car createCar() {
return new Audi();
} }
//BydFactory工厂类
public class BydFactory implements CarFactory{
@Override
public Car createCar() {
return new Byd();
} }
//测试
public class Client {
public static void main(String[] args) {
Car c1 = new AudiFactory().createCar();
Car c2 = new BydFactory().createCar();
c1.run();
c2.run();
} }
(3)简单工厂模式和工厂方法模式比较
①结构复杂度
从这个角度比较,显然简单工厂模式要占优。简单工厂模式只需要一个工厂类,而工厂方法模式的工厂类随着产品类
的个数增加而增加,这个无疑会使类的个数越来越多,从而增加结构的复杂程度
②代码复杂度
代码复杂度和结构复杂度是一对矛盾,既然简单工厂模式在结构方面相对简洁,那么它在代码方面肯定是比工厂方法
模式复杂的了。简单工厂模式的工厂类随着产品类的增加需要增加很多方法(或代码),而工厂方法模式每个具体工厂类
只完成单一任务,代码简洁
③客户端编程难度
工厂方法模式虽然在工厂类结构中引入了接口从而满足OCP,但是在客户端编码中需要对工厂类进行实例化。而简单工
厂模式的工厂类是个静态类,在客户端无需实例化。而简单工厂模式的工厂类是个静态类,在客户端无需实例化,这无疑
是个吸引人的优点。
④管理上的难度
这是个关键的问题
1.我们先谈扩展,众所周知,工厂方法模式完全满足OCP,即它有非常良好的扩展性。那是否就说明了简单工厂模式就没有扩展
性呢?答案是否定的。简单工厂模式同样具备良好的扩展性——扩展的时候仅需要修改少量的代码(修改工厂类的代码)就可以
满足扩展性的要求了。尽管这没有完全满足OCP,但我们不需要太拘泥于设计理念,要知道,sun提供的Java官方工具包中也有
很多没有满足OCP的例子。
2.然后我们从维护性的角度分析。假如某个具体产品类需要进行一定的修改,很可能需要修改对应的工厂类。当同时需要修改多个
产品类的时候,对工厂类的修改会变得相当麻烦。反而简单工厂没有这些麻烦,当多个产品类需要修改时,简单工厂模式仍然
仅仅需要修改唯一的工厂类
根据设计理念建议:工厂方法模式。但实际上,我们一般都用简单工厂模式
2.2.2.3.抽象工厂模式
(1)要点
①用来生产不同产品族的全部产品(对于增加新的产品,无能为力;支持增加产品族)
②抽象工厂模式是工厂模式的升级版本,在有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的
解决方式。
(2)代码
//Engine接口
public interface Engine {
void run();
void start();
}
//LuxuryEngine类
class LuxuryEngine implements Engine {
@Override
public void run() {
System.out.println("转的快");
}
@Override
public void start() {
System.out.println("启动快!可以自动启停");
} }
// LowEngine类
class LowerEngine implements Engine {
@Override
public void run() {
System.out.println("转的慢");
}
@Override
public void start() {
System.out.println("启动慢!");
} }
//Seat接口
public interface Seat {
void message();
}
//LuxurySeat类
class LuxurySeat implements Seat {
@Override
public void message() {
System.out.println("可以自动按摩");
} }
//LowSeat类
class LowSeat implements Seat {
@Override
public void message() {
System.out.println("不能自动按摩");
} }
//Tyre接口
public interface Tyre {
void revolve();
}
//LuxuryTyre类
class LuxuryTyre implements Tyre{
@Override
public void revolve() {
System.out.println("旋转不磨损");
} }
//LowTyre类
class LowTyre implements Tyre{
@Override
public void revolve() {
System.out.println("容易磨损");
} }
//CartFactory工厂接口
public interface CartFactory {
Engine createEngine();
Seat createSeat();
Tyre createTyre();
}
//LuxuryFactory工厂实现类
public class LuxuryCarFactory implements CartFactory {
@Override
public Engine createEngine() {
return new LuxuryEngine();
}
@Override
public Seat createSeat() {
return new LuxurySeat();
}
@Override
public Tyre createTyre() {
return new LuxuryTyre();
} }
//LowCarFactory工厂实现类
public class LowCarFactory implements CartFactory{
@Override
public Engine createEngine() {
return new LowerEngine();
}
@Override
public Seat createSeat() {
return new LowSeat();
}
@Override
public Tyre createTyre() {
return new LowTyre();
} }
//测试
public class Client {
public static void main(String[] args) {
LuxuryCarFactory factory = new LuxuryCarFactory();
Engine e = factory.createEngine();
e.run();
e.start();
} }
2.2.3 建造者模式
(1)概述
①建造者模式(Builder Pattern) 又叫生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来,使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
②建造者模式 是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象
的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。
(2)结构
建造者(Builder)模式包含如下角色:
①产品类(Product):要创建的复杂对象。
②抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的
部件对象的创建。
③具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体
创建方法。在构造过程完成后,提供产品的实例。
④指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产
品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
//1.产品类(自行车类)
public class Bike {
private String frame;//车架
private String seat;//车座
//Getter,Setter方法
}
// 2.抽象建造者类
public abstract class Builder {
//声明Bike类型的变量,并进行赋值
protected Bike bike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
//构建自行车的方法
public abstract Bike createBike();
}
//3.1摩拜单车Builder类
public class MobikeBuilder extends Builder {
public void buildFrame() {
bike.setFrame("碳纤维车架");
}
public void buildSeat() {
bike.setSeat("真皮车座");
}
public Bike createBike() {
return bike;
}
}
//3.2 ofo单车Builder类
public class OfoBuilder extends Builder {
public void buildFrame() {
bike.setFrame("铝合金车架");
}
public void buildSeat() {
bike.setSeat("橡胶车座");
}
public Bike createBike() {
return bike;
}
}
//4.指挥者类
public class Director {
//声明builder类型的变量
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
//组装自行车的功能
public Bike construct() {
builder.buildFrame();
builder.buildSeat();
return builder.createBike();
}
}
//5.测试类
public class Client {
public static void main(String[] args) {
//创建指挥者对象
Director director = new Director(new MobileBuilder());
//让指挥者只会组装自行车
Bike bike = director.construct();
System.out.println(bike.getFrame());
System.out.println(bike.getSeat());
}
}
注意:
上面示例是 Builder模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它
用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况
下需要简化系统结构,可以把指挥者类和抽象建造者进行结合
// 抽象 builder 类
public abstract class Builder {
protected Bike mBike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
public Bike construct() {
this.buildFrame();
this.BuildSeat();
return this.createBike();
} }
说明:
这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果
construct() 过于复杂,建议还是封装到 Director 中。
(3)优缺点
- 优点:
①建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一
般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取
得比较好的稳定性。
②在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得
相同的创建过程可以创建不同的产品对象。
③可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过
程更加清晰,也更方便使用程序来控制创建过程。
④建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不
用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。 - 缺点:
造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适
合使用建造者模式,因此其使用范围受到一定的限制。
(4)使用场景
建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合
在一起的算法却相对稳定,所以它通常在以下场合使用。
1)创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。
2)创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表
示是独立的。
3)开发中的应用场景
①StringBuilder类的append方法
②SQL中的PreparedStatement
③JDOM中,DemoBuilder、SAXBuilder
2.2.4 原型模式
(1)基本概述
①原型模式(Prototype模式)是指:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象
②原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节
③工作原理是:通过将一个原型对象传给那个要发起创建的对象,这个要发起创建的对象通过请求原型对象拷贝它们自己来实施创建,即对象.clone()
(2)原型模式原理结构图-uml类图
①Prototype:原型类,声明一个克隆自己的接口
②ConcretePrototype: 具体的原型类, 实现一个克隆自己的操作
③Client: 让一个原型对象克隆自己,从而创建一个新的对象(属性一样)
(2)原型模式的实现
①实现Cloneable接口,调用Object中的clone方法
②Prototype模式中实现起来最困难的地方就是内存复制操作,所幸在Java中提供了clone()方法替我们做了大部
分事情。
注意用词:克隆和拷贝一回事
(3)代码示例
//①浅克隆
public class Sheep implements Cloneable {//克隆羊
private String name;
private Date birthday;
// Getter/Setter方法,构造器
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj = super.clone();//直接调用Object的clone方法
return obj;
} }
//原型模式测试(浅克隆)
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
Date date = new Date(2022L);
Sheep s1 = new Sheep("少利",date);
System.out.println(s1);
System.out.println(s1.getName());
System.out.println(s1.getBirthday());
Sheep s2 = (Sheep) s1.clone();
s2.setName("多利");
System.out.println(s2);
System.out.println(s2.getName());
System.out.println(s2.getBirthday());
} }
// ②深克隆(把属性也进行克隆)
public class Sheep2 implements Cloneable {
private String name;
private Date birthday;
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj = super.clone();//自己调用object对象的clone()方法
// 添加如下代码实现深克隆
Sheep2 s = (Sheep2) obj;
s.birthday = (Date) this.birthday.clone();//把属性也进行克隆
return obj;
}
// Getter/Setter方法,构造器
//原型模式测试(深客隆)
public class Client2 {
public static void main(String[] args) throws CloneNotSupportedException {
Date date = new Date(2023L);
Sheep2 s1 = new Sheep2("小利", date);
Sheep2 s2 = (Sheep2) s1.clone();
System.out.println(s1);
System.out.println(s1.getName());
System.out.println("s1的date修改前:"+s1.getBirthday());
date.setTime(20383L);//修改s1的date,不会影响s2
System.out.println("s1的date修改后:"+s1.getBirthday());
s2.setName("大利");
System.out.println(s2);
System.out.println(s2.getName());
System.out.println("s2的date:"+s2.getBirthday());
} }
public class Sheep implements Serializable {//克隆羊
private String name;
private Date birthday;
// Getter/Setter方法,构造方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object obj = super.clone();//直接调用Object的clone方法
return obj;
} }
//③原型模式测试(使用序列化和反序列化深克隆)
public class Sheep implements Serializable { //实现Serializable接口
private String name;
private Date birthday;
// 构造器,Getter/Setter方法
}
// 使用序列化和反序列化实现深克隆
public class Client3 {
public static void main(String[] args) throws Exception {
Date date = new Date(8888L);
Sheep s1 = new Sheep("小羊", date);
System.out.println(s1.getBirthday());
// 使用序列化和反序列化实现深克隆
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(s1);
byte[] bytes = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
Sheep s2 = (Sheep) ois.readObject();//克隆好的对象
date.setTime(9999L);
System.out.println(s2.getBirthday());
} }
(4)应用场景【通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式】
原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者
spring中bean的创建实际就是两种:单例模式和原型模式(当然,原型模式需要和工厂模式搭建起来)
2.2.4.1 浅拷贝和深拷贝
(1)浅拷贝的介绍
①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
②对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
③浅拷贝是使用默认的 clone()方法来实现sheep = (Sheep) super.clone();
(2)深拷贝基本介绍
①复制对象的所有基本数据类型的成员变量值
②为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象进行拷贝
③深拷贝实现方式1:重写clone方法来实现深拷贝
④深拷贝实现方式2:通过对象序列化实现深拷贝(推荐)
public class DeepProtoType implements Serializable, Cloneable{
public String name; //String 属性
public DeepCloneableTarget deepCloneableTarget;// 引用类型
public DeepProtoType() {
super();
}
//深拷贝 - 方式 1 使用clone 方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object deep = null;
//这里完成对基本数据类型(属性)和String的克隆
deep = super.clone();
//对引用类型的属性,进行单独处理
DeepProtoType deepProtoType = (DeepProtoType)deep;
deepProtoType.deepCloneableTarget = (DeepCloneableTarget)deepCloneableTarget.clone();
// TODO Auto-generated method stub
return deepProtoType;
}
//深拷贝 - 方式2 通过对象的序列化实现 (推荐)
public Object deepClone() {
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this); //当前这个对象以对象流的方式输出
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
DeepProtoType copyObj = (DeepProtoType)ois.readObject();
return copyObj;
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
return null;
} finally {
//关闭流
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (Exception e2) {
// TODO: handle exception
System.out.println(e2.getMessage());
}
}
}
}
2.2 结构型模式
结构性模式核心作用:
是从程序的结构上实现松耦合,从而可以扩大整体的类结构,用来解决更大的问题
2.2.1 适配器模式
(1)什么是适配器模式?
将一个类的接口转换成客户希望的另一个接口。Adaptor模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作
(2)模式中的角色
①目标接口(Target):客户端所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
②需要适配的类(Adaptee):需要适配的类或适配者类
③适配器(Adaptor):通过包装一个需要适配的对象,把原接口转换成目标接口
(3)生活中的场景
例如:以前的键盘使用ps/2接口,现在使用为USB接口。转接口(适配器)把键盘(需要适配的类)的ps/2接口
转换为 USB接口(目标接口)
//(1)被适配的类,相当于例子中的PS/2接口键盘
public class Adaptee {
public void request(){
System.out.println("可以完成ps/2接口的功能!");
} }
//(2)目标接口
public interface Target {
public void handleReq();
}
//(3)适配器
//①类适配器方式
public class Adaptor extends Adaptee implements Target {
@Override
public void handleReq() {
super.request();
} }
//②适配器(使用了组合方式)
public class Adaptor2 implements Target {
private Adaptee adaptee;
@Override
public void handleReq() {
adaptee.request(); }
public Adaptor2(Adaptee adaptee) {
super();
this.adaptee = adaptee;
} }
//客户端(相当于例子中的笔记本,只有USB接口)
public class Client {
public void test1(Target t) {
t.handleReq();
}
public static void main(String[] args) {
Client c = new Client();
Adaptee a = new Adaptee();
// Target t = new Adaptor();
Target t = new Adaptor2(a);
c.test1(t);
} }
(4)工作中的场景:经常用来做旧系统改造和升级
(5)Java中的场景
①java.io.InputStreamReader(InputStream)
②java.io.OutputStreamWriter(OutputStream)
// 是字节流通向字符流的桥梁,通过适配器把字节流转化为字符流
2.2.2 代理模式(Proxy pattern)
2.2.2.1 概述
(1)由于某些原因访问对象不适合或者不能直接引用目标对象,需要通过代理对象这个中介来控制对该目标对象的访问。
(2)核心作用:通过代理,控制对对象的访问,可以详细控制访问某个类/对象的方法,在调用这个方法前做前置处理,调用这个方法后做后置处理(注意:AOP[面向切面编程]的核心机制就是代理模式)
(3)Java中的代理按照代理类生成时机的不同分为静态代理和动态代理。静态代理代理类在编译期就生成了,而动态代理代理类则是Java运行时动态生成;动态代理又分为JDK代理和CGLib代理两种。
2.2.2.2 结构
(1)代理(Proxy)模式分为三种角色:
①抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
②真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
③代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能
(3)应用场景
①安全代理:屏蔽对真实角色的直接访问
②远程代理:通过代理类处理远程方法调用
③延迟加载:先加载轻量级的代理对象,真正需要加载时再加载真实对象
例如:当你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有100MB,在打开文件时不可能将所有
的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用proxy来进行大图片的打开。
(4)开发框架中的应用场景
①struts2中拦截器的实现
②数据库连接池关闭处理
③Hiberate中延时加载的实现
④mybatis中实现拦截器插件
⑤Aspect的实现
⑥spring在AOP的实现(日志拦截,声明式事务处理)
⑦web service
⑧RMI远程方法调用
2.2.2.3 静态代理
(1)抽象主题类
// 卖火车票的接口
public interface SellTickets {
void sell();
}
(2)真实主题类
// 火车站类
public class TrainStation implements SellTickets {
public void sell() {
System.out.println("火车站卖票");
} }
(3)代理类
// 代售点类
public class ProxyPoint implements SellTickets {
//声明火车站类对象
private TrainStation trainStation = new TrainStation();
public void sell() {
System.out.println("代售点收取一些服务费用");
trainStation.sell();
} }
(4)客户端测试
public class Client {
public static void main(String[] args) {
//创建代售点类对象
ProxyPoint proxyPoint = new ProxyPoint();
//调用方法进行买票
proxyPoint.sell();
} }
从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象
和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。
2.2.2.4 动态代理
2.2.1 分类
(1)JDK自带的动态代理
1)java.lang.reflect.Proxy(作用:动态生成代理类和对象)
2)java.lang.reflect.InvocationHandler(处理器接口)
①可以通过invoke方法实现对真实角色的代理访问
②每次通过Proxy生成代理类对象时都要指定对应的代理器对象
(2)JavaScript字节码操作库实现
(3)CGLIB
(4)ASM(底层使用指令,可维护性较差)
2.2.2 优点
抽象角色(接口)声明的方法都被转移到调用处理器中一个集中的方法去处理。这样,我们就可以更加灵活和统一
的处理众多的方法
2.2.2.4.1 动态代理类Proxy
(1)抽象主题类
// 卖火车票的接口
public interface SellTickets {
void sell();
}
(2)真实主题类
// 火车站类
public class TrainStation implements SellTickets {
public void sell() {
System.out.println("火车站卖票");
} }
(3)获取代理对象的工厂类
/**
* @Description: 获取代理对象的工厂类
* 代理类也实现了对应的接口
*/
public class ProxyFactory {
//声明目标对象
private TrainStation station = new TrainStation();
//获取代理对象的方法
public SellTickets getProxyObject() {
//返回代理对象
/*
ClassLoader loader : 类加载器,用于加载代理类。可以通过目标对象获取类加载器
Class<?>[] interfaces : 代理类实现的接口的字节码对象
InvocationHandler h : 代理对象的调用处理程序
*/
SellTickets proxyObject = (SellTickets)Proxy.newProxyInstance(
station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
new InvocationHandler() {
/*
Object proxy : 代理对象。和proxyObject对象是同一个对象,在invoke方法中基本不用
Method method : 对接口中的方法进行封装的method对象
Object[] args : 调用方法的实际参数
返回值: 方法的返回值。
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代售点收取一定的服务费用(jdk动态代理)");
//执行目标对象的方法
Object obj = method.invoke(station, args);
return obj;
}
}
);
return proxyObject;
}
}
(4)客户端测试
public class Client {
public static void main(String[] args) {
//获取代理对象
//1,创建代理工厂对象
ProxyFactory factory = new ProxyFactory();
//2,使用factory对象的方法获取代理对象
SellTickets proxyObject = factory.getProxyObject();
//3,调用卖调用的方法
proxyObject.sell();
System.out.println(proxyObject.getClass());
} }
2.2.3 装饰者模式
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
2.2.3.1 结构
装饰(Decorator)模式中的角色:
(1)抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
(2)具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
(3)抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子
类扩展具体构件的功能。
(4)具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附
加的责任。
2.2.3.2 案例
快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,
每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。
(1)抽象构件角色
//快餐接口
public abstract class FastFood {
private float price;
private String desc;
public FastFood() {
}
public FastFood(float price, String desc) {
this.price = price;
this.desc = desc;
}
public void setPrice(float price) {
this.price = price;
}
public float getPrice() {
return price;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public abstract float cost(); //获取价格
}
(2)具体构件角色
①炒饭
public class FriedRice extends FastFood {
public FriedRice() {
super(10, "炒饭");
}
public float cost() {
return getPrice();
}
}
②炒面
public class FriedNoodles extends FastFood {
public FriedNoodles() {
super(12, "炒面");
}
public float cost() {
return getPrice();
}
}
(3)抽象装饰角色
//配料类
public abstract class Garnish extends FastFood {
private FastFood fastFood;
public FastFood getFastFood() {
return fastFood;
}
public void setFastFood(FastFood fastFood) {
this.fastFood = fastFood;
}
public Garnish(FastFood fastFood, float price, String desc) {
super(price,desc);
this.fastFood = fastFood;
}
}
(4)具体装饰角色
①鸡蛋配料
public class Egg extends Garnish {
public Egg(FastFood fastFood) {
super(fastFood,1,"鸡蛋");
}
public float cost() {
return getPrice() + getFastFood().getPrice();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
②培根配料
public class Bacon extends Garnish {
public Bacon(FastFood fastFood) {
super(fastFood,2,"培根");
}
@Override
public float cost() {
return getPrice() + getFastFood().getPrice();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
(5)测试类
//测试类
public class Client {
public static void main(String[] args) {
//点一份炒饭
FastFood food = new FriedRice();
//花费的价格
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("========");
//点一份加鸡蛋的炒饭
FastFood food1 = new FriedRice();
food1 = new Egg(food1);
//花费的价格
System.out.println(food1.getDesc() + " " + food1.cost() + "元");
System.out.println("========");
//点一份加培根的炒面
FastFood food2 = new FriedNoodles();
food2 = new Bacon(food2);
//花费的价格
System.out.println(food2.getDesc() + " " + food2.cost() + "元");
}
}
2.2.7 享元模式
(1)定义:通过共享已经存在的对象来大幅度减少要创建的对象数量,从而减少内存的占用,提升性能
2.3 行为型模式
2.3.2 策略模式
先看下面的图片,作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea
进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。
(1)定义:
该模式定义了一系列算法(上面的eclipse和idea就是算法),并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。可以避免多重分支的if…else…和switch语句。
(2)结构
策略模式的主要角色如下:
①抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
②具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
③环境(Context)类:持有一个策略类的引用,最终给客户端调用。
(3)案例实现
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:
①定义百货公司所有促销活动的共同接口
public interface Strategy {
void show();
}
②定义具体策略角色(Concrete Strategy):每个节日具体的促销活动
//为春节准备的促销活动A
public class StrategyA implements Strategy {
public void show() {
System.out.println("买一送一");
}
}
//为中秋准备的促销活动B
public class StrategyB implements Strategy {
public void show() {
System.out.println("满200元减50元");
}
}
//为圣诞准备的促销活动C
public class StrategyC implements Strategy {
public void show() {
System.out.println("满1000元加一元换购任意200元以下商品");
}
}
③定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员
public class SalesMan {
//持有抽象策略角色的引用
private Strategy strategy;
public SalesMan(Strategy strategy) {
this.strategy = strategy;
}
//向客户展示促销活动
public void salesManShow(){
strategy.show();
}
}
④测试
public class Client(){
public static void main(String[] args){
SalesMan salesMan = new SalesMan();
salesMan.salesManShow(new StrategyA());
}
}
(4)优缺点
- 优点:
①策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换。
②易于扩展,增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
③避免使用多重条件选择语句(if else),充分体现面向对象设计思想。 - 缺点:
①客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
②策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
2.3.6 观察者模式
(1)定义:
观察者模式又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
(2)在观察者模式中有如下角色:
①Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
②ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
③Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
④ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
(3) 案例实现
在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给
关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号
是被观察者,有多个的微信用户关注了程序猿这个公众号。
1)类图如下:
2)代码
①定义抽象观察者类,里面定义一个更新的方法
public interface Observer {
void update(String message);
}
②定义具体观察者类,微信用户是观察者,里面实现了更新的方法
public class WeixinUser implements Observer {
// 微信用户名
private String name;
public WeixinUser(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + "-" + message);
}
}
③定义抽象主题类,提供了attach、detach、notify三个方法
public interface Subject {
//增加订阅者
public void attach(Observer observer);
//删除订阅者
public void detach(Observer observer);
//通知订阅者更新消息
public void notify(String message);
}
④微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中
的方法
public class SubscriptionSubject implements Subject {
//储存订阅公众号的微信用户
private List<Observer> weixinUserlist = new ArrayList<Observer>();
@Override
public void attach(Observer observer) {
weixinUserlist.add(observer);
}
@Override
public void detach(Observer observer) {
weixinUserlist.remove(observer);
}
@Override
public void notify(String message) {
for (Observer observer : weixinUserlist) {
observer.update(message);
}
}
}
⑤客户端程序
public class Client {
public static void main(String[] args) {
SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
//创建微信用户
WeixinUser user1=new WeixinUser("孙悟空");
WeixinUser user2=new WeixinUser("猪悟能");
WeixinUser user3=new WeixinUser("沙悟净");
//订阅公众号
mSubscriptionSubject.attach(user1);
mSubscriptionSubject.attach(user2);
mSubscriptionSubject.attach(user3);
//公众号更新发出消息给订阅的微信用户
mSubscriptionSubject.notify("传智黑马的专栏更新了");
}
}
(4)优缺点
- 优点:
①降低了目标(被观察者)与观察者之间的耦合关系,两者之间是抽象耦合关系。
②被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】 - 缺点:
①如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
②如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃