目录
OOP七大原则
-
开闭原则:对扩展开放,对修改关闭。
-
里氏替换原则:继承必须确保超类所拥有的性质在子类中仍然成立。
-
依赖倒置原则:要面向接口编程,不要面向实现编程。
-
单一职责原则:控制类的粒度大小、将对象解耦、提高其内聚性。
-
接口隔离原则:要为各个类建立它们需要的专用接口。
-
迪米特法则:只与你的直接朋友交谈,不跟“陌生人”说话。
-
合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
设计模式
设计模式(Design Pattern)是对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
1995年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了23种设计模式,从此树立了软件设计模式领域的里程碑,人称【GoF设计模式】。
设计模式的意义
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式的优点:
-
可以提高程序员的思维能力、编程能力和设计能力。
-
使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
-
使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
设计模式分类
-
创建型模式
单例模式,工厂模式,抽象工厂模式,建造者模式,原型模式。
-
结构型模式
适配器模式,桥接模式,装饰模式,组合模式,外观模式,享元模式,代理模式。
-
行为型模式
模板方法模式,命令模式,迭代器模式,观察者模式,中介者模式,备忘录模式,解释器模式,状态模式,策略模式,职责链模式,访问者模式。
下面也就介绍几种设计模式。
单例模式
核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
常见场景:
-
Windows的任务管理器,Windows的回收站。
-
数据库连接池的设计一般也是单例模式。
-
在Servlet编程中,每个Servlet也是单例的。
-
在Spring中,每个Bean默认就是单例的。
-
项目中,读取配置文件的类,一般也只有一个对象,没必要每次都去new对象读。
-
取网站的计数器一般也会采用单例模式,可以保证同步。
-
......
饿汉式
在类加载的时候,就会new出对象来,这样可能会浪费一些资源。
// 饿汉式
public class Hungry {
// 浪费资源,有些业务可能没有用到这部分
/*private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];*/
private static final Hungry HUNGRY = new Hungry();
private Hungry() {
}
public static Hungry getInstance() {
return HUNGRY;
}
}
DCL懒汉式
双重检测锁模式的懒汉式单例,叫 DCL懒汉式。
// 懒汉式
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan() {
}
public static LazyMan getInstance() {
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
【注意】上面这种方式,在单线程下是没有问题的,但是在多线程环境下就存在问题。
// 懒汉式
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "执行");
}
public static LazyMan getInstance() {
if (lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
// 模拟多线程环境
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
lazyMan.getInstance();
}).start();
}
}
}
这时候多线程下输出的结果,就不太一致了。
Thread-0执行
Thread-0执行 Thread-2执行 Thread-1执行
这时候就需要加把锁,来解决多线程的问题
public static LazyMan getInstance() {
if (lazyMan == null){
synchronized (LazyMan.class){
lazyMan = new LazyMan();
}
}
return lazyMan;
}
但是这样还是不行的,这时候就要说一下,new对象的过程了,看上去new对象只用到了一个命令,但实际上,有三步操作:
-
分配内存空间。
-
执行构造方法,初始化对象。
-
用这个对象执行内存空间。
因为,内部有三步,所以多线程下,会发生指令重排的现象,导致程序出现错误。所以,还需要加上一把锁,防止这种现象的发生。
private volatile static LazyMan lazyMan;
【总结】因为用到了两把锁,所以叫双重检测锁模式的懒汉式单例,也叫 DCL懒汉式。
// 懒汉式
public class LazyMan {
private volatile static LazyMan lazyMan;
private LazyMan() {
}
public static LazyMan getInstance() {
if (lazyMan == null){
synchronized (LazyMan.class){
lazyMan = new LazyMan();
}
}
return lazyMan;
}
}
但是,就算是以上方式,也是不安全的,通过反射机制,可以破坏这种懒汉式单例。
下面简单模拟一下,一个使用类中方法创建对象,一个使用反射机制
// 懒汉式
public class LazyMan {
private volatile static LazyMan lazyMan;
private LazyMan() {
}
public static LazyMan getInstance() {
if (lazyMan == null){
synchronized (LazyMan.class){
lazyMan = new LazyMan();
}
}
return lazyMan;
}
// 通过反射破坏懒汉式单例
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazyMan lazyMan1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
}
结果如下
com.km.lazyman.LazyMan@1b6d3586 com.km.lazyman.LazyMan@4554617c
很明显,这两个不是同一个对象。
反射通过构造方法来创建对象,这时候我们需要在构造方法中加上判断。
private LazyMan() {
synchronized (LazyMan.class){
if (lazyMan != null){
throw new RuntimeException("不可以使用反射");
}
}
}
这种方式虽然可以解决这个问题,但是还是不安全的,还是存在问题的。
当两次创建对象,都是使用反射来创建的时候,这种懒汉式单例还是会被破坏。
// 通过反射破坏懒汉式单例
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
可以加一个标记,用来避免这种通过反射机制,来破坏懒汉式单例模式。
private static boolean a = true; // a就是一个标记
private LazyMan() {
synchronized (LazyMan.class){
if (a == true){
a = false;
}else {
throw new RuntimeException("不可以使用反射");
}
}
}
注意,虽然以上也是经过很多次改进,但是还是不安全的,因为,通过知道这个“标记”的名字,就可以通过反射来设置这个变量的值,从而破坏单例模式。
// 通过反射破坏懒汉式单例
public static void main(String[] args) throws Exception{
Field a = LazyMan.class.getDeclaredField("a");
a.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
a.set(lazyMan1,true);
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
}
不过,通过观察 newInstance() 方法的源码,知道无法反射性地创建枚举对象,源码如下:
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
可以通过枚举类型实验一下,但是注意,通过反射的时候,枚举类型没有无参构造,有一个参数为(String,int)的有参构造。使用无参构造,会报异常java.lang.NoSuchMethodException。
静态内部类
实现单例模式,也可以使用静态内部类的方式
// 静态内部类
public class User {
private User() {
}
public static User getInstance(){
return Innerclass.USER;
}
public static class Innerclass{
private static final User USER = new User();
}
}
工厂模式
作用:实现了创建者和调用者的分离。
核心本质:
-
实例化对象不使用new,用工厂方法代替。
-
将选择实现类,创建对象统一管理和控制,从而将调用者跟我们的实现类解耦。
主要符合OOP七大原则中:
-
开闭原则:对扩展开放,对修改关闭。
-
依赖倒置原则:要面向接口编程,不要面向实现编程。
-
迪米特法则:只与你的直接朋友交谈,不跟“陌生人”说话。
分类:
-
简单工厂模式
用来生产同一等级结构中的任意产品(对于增加新的产品,需要修改已有代码)。
-
工厂方法模式
用来生产同一等级结构中的固定产品(支持增加任意产品)。
-
抽象工厂模式(单独介绍)
围绕一个超级工厂创建其他工厂,该超级工厂又称为其他工厂的工厂。
应用场景:
-
JDK中Calendar的getlnstance方法
-
JDBC中的Connection对象的获取
-
Spring中IOC容器创建管理bean对象
-
反射中Class对象的newlnstance方法
简单工厂模式
创建一个U盘接口
// U盘接口
public interface UPan {
void name();
}
创建金士顿U盘类、闪迪U盘类,实现U盘接口
// 金士顿U盘
public class Kingston implements UPan{
@Override
public void name() {
System.out.println("金士顿");
}
}
// 闪迪U盘
public class SanDisk implements UPan {
@Override
public void name() {
System.out.println("闪迪");
}
}
没有使用简单工厂模式的情况下,测试程序
public class Test {
public static void main(String[] args) {
// 普通模式下创建
UPan uPan1 = new Kingston();
UPan uPan2 = new SanDisk();
uPan1.name();
uPan2.name();
}
}
使用简单工厂模式,创建一个U盘工厂类
// U盘工厂类
public class UPanFactory {
public static UPan getUPan(String UPan){
if (UPan.equals("金士顿")){
return new Kingston();
}else if (UPan.equals("闪迪")){
return new SanDisk();
}else {
return null;
}
}
}
使用简单工厂模式,测试程序
public class Test {
public static void main(String[] args) {
// 使用工厂模式创建
UPan uPan1 = UPanFactory.getUPan("金士顿");
UPan uPan2 = UPanFactory.getUPan("闪迪");
uPan1.name();
uPan2.name();
}
}
附加类结构
简单工厂模式(静态工厂模式):
虽然某种程度上不符合设计原则,但实际上使用最多。
工厂方法模式
创建一个U盘接口
// U盘接口
public interface UPan {
void name();
}
创建金士顿U盘类、闪迪U盘类,实现U盘接口
// 金士顿U盘
public class Kingston implements UPan{
@Override
public void name() {
System.out.println("金士顿");
}
}
// 闪迪U盘
public class SanDisk implements UPan {
@Override
public void name() {
System.out.println("闪迪");
}
}
创建一个U盘工厂接口
// U盘工厂接口
public interface UPanFactory {
UPan getUPan();
}
创建金士顿U盘工厂类、闪迪U盘工厂类,实现U盘工厂接口
// 金士顿U盘工厂
public class KingstonFactory implements UPanFactory{
@Override
public UPan getUPan() {
return new Kingston();
}
}
// 闪迪U盘工厂
public class SanDiskFactory implements UPanFactory{
@Override
public UPan getUPan() {
return new SanDisk();
}
}
使用工厂方法模式测试
public class Test {
public static void main(String[] args) {
UPan uPan1 = new KingstonFactory().getUPan();
UPan uPan2 = new SanDiskFactory().getUPan();
uPan1.name();
uPan2.name();
}
}
附加类结构
工厂方法模式:
不修改已有类的前提下,通过增加新的工厂类实现扩展。从类结构上观察,工厂方法模式比简单工厂模式要复杂,就算可以实现扩展,增加的类也会很多,不便于代码的管理。
抽象工厂模式
定义︰抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类。
适用场景:
-
客户端(应用层)不依赖于产品类实例,如何被创建、实现等细节。
-
强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量的重复代码。
-
提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖于具体的实现。
优点:
-
具体产品在应用层的代码隔离,无需关心创建的细节。
-
将一个系列的产品统一到一起创建。
缺点:
-
规定了所有可能被创建的产品集合,产品簇中扩展新的产品困难。
-
增加了系统的抽象性和理解难度。
创建硬盘产品接口,内存条产品接口
// 硬盘产品接口
public interface HardDisk {
void insert();
void out();
}
// 内存条产品接口
public interface MemoryBank {
void insert();
void out();
}
创建金士顿品牌的硬盘和内存条类
// 金士顿硬盘
public class KingstonHardDisk implements HardDisk{
@Override
public void insert() {
System.out.println("插入金士顿硬盘");
}
@Override
public void out() {
System.out.println("插入金士顿硬盘");
}
}
// 金士顿内存条
public class KingstonMemoryBank implements MemoryBank{
@Override
public void insert() {
System.out.println("插入金士顿内存条");
}
@Override
public void out() {
System.out.println("拔出金士顿内存条");
}
}
创建三星的硬盘和内存条类
// 三星硬盘
public class SamsungHardDisk implements HardDisk{
@Override
public void insert() {
System.out.println("插入三星硬盘");
}
@Override
public void out() {
System.out.println("插入三星硬盘");
}
}
// 三星内存条
public class SamsungMemoryBank implements MemoryBank{
@Override
public void insert() {
System.out.println("插入三星内存条");
}
@Override
public void out() {
System.out.println("拔出三星内存条");
}
}
创建一个抽样产品工厂接口
// 抽象产品工厂
public interface ProductFactory {
// 生产硬盘
HardDisk hardDiskProduct();
// 生产内存条
MemoryBank memoryBankProduct();
}
分别创建金士顿产品工厂,三星产品工厂实现这个抽象工厂接口
// 金士顿产品工厂
public class KingstonFactory implements ProductFactory{
@Override
public HardDisk hardDiskProduct() {
return new KingstonHardDisk();
}
@Override
public MemoryBank memoryBankProduct() {
return new KingstonMemoryBank();
}
}
// 三星产品工厂
public class SamsungFactory implements ProductFactory{
@Override
public HardDisk hardDiskProduct() {
return new SamsungHardDisk();
}
@Override
public MemoryBank memoryBankProduct() {
return new SamsungMemoryBank();
}
}
测试程序
public class Test {
public static void main(String[] args) {
// 测试金士顿
KingstonFactory kingstonFactory = new KingstonFactory();
HardDisk hardDisk = kingstonFactory.hardDiskProduct();
hardDisk.insert();
hardDisk.out();
MemoryBank memoryBank = kingstonFactory.memoryBankProduct();
memoryBank.insert();
memoryBank.out();
// 三星可以自己测试
}
}
抽象工厂模式:
不可以增加产品,可以增加产品族(本案例中,硬盘和内存条)。
建造者模式
介绍:建造者模式提供了一种创建对象的最佳方式。
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
主要作用:在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象。用户只需要给出指定复杂对象的类型和内容,建造者模式负责按顺序创建复杂对象(把内部的建造过程和细节隐藏起来)。
解决问题:解决对象的构建过于复杂的问题。
应用场景:
-
需要生成的产品对象有复杂的内部结构,这些产品对象具备共性。
-
隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
-
适合于一个具有较多的零件(属性)的产品(对象)的创建过程。
优点:
-
产品的建造和表示分离,实现了解耦。使用建造者模式可以使客户端不必知道产品内部组成的细节。
-
复杂产品的创建步骤分解在不同的方法中,这些方法可以调用顺序不同,结果不同,创建结果很清晰。
-
具体的建造者类之间是相互独立的,这有利于系统的扩展。增加新的具体建造者无需修改原有类库的代码,符合“开闭原则“。
缺点:
-
建造者模式所创建的产品一般具有较多的共同点,其组成部分相似。如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
-
如果产品的内部变化复杂,可能会导致需要定义很多具体建造者来实现这种变化,导致系统会变得很庞大。
举个例子:
用户购买手机,并不需要手机内部是怎么制造组装的,只要提供相应的手机型号就可以。
常规用法
角色分析:
上述图例四个角色:
-
Product(产品角色):一个具体的产品对象。
-
Builder(抽象建造者):创建一个Product对象的各个部件指定的抽象接口。
-
ConcreteBuilder(具体建造者):实现抽象接口,构建和装配各个部件。
-
Director(指挥者):构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。
创建一个实体类,食物类
// 食物
public class Food {
private String buildA;
private String buildB;
private String buildC;
// set get toString方法
}
创建抽象的建造者
// 抽象的建造者
public abstract class Builder {
abstract void buildA(); // 洗菜
abstract void buildB(); // 切菜
abstract void buildC(); // 炒菜
// 炒完菜,得到食物
abstract Food getFood();
}
创建一个具体的建造者,继承抽象建造者,重写其方法
// 具体的建造者:厨师
public class Chef extends Builder{
private Food food;
public Chef() {
food = new Food();
}
@Override
void buildA() {
food.setBuildA("洗菜");
System.out.println("洗菜");
}
@Override
void buildB() {
food.setBuildB("切菜");
System.out.println("切菜");
}
@Override
void buildC() {
food.setBuildC("炒菜");
System.out.println("炒菜");
}
@Override
Food getFood() {
return food;
}
}
创建一个指挥者
// 指挥:核心。负责指挥“建造”一道食物。食物是怎么构建的,有指挥决定。
public class Director {
// 指挥厨师按指定顺序做菜
public Food build(Builder builder){
builder.buildA();
builder.buildB();
builder.buildC();
return builder.getFood();
}
}
测试
public class Test {
public static void main(String[] args) {
// 指挥
Director director = new Director();
// 指挥 指定具体建造者“厨师” 炒菜
Food build = director.build(new Chef());
System.out.println(build);
}
}
输出
洗菜 切菜 炒菜 Food{buildA='洗菜', buildB='切菜', buildC='炒菜'}
上面示例是建造者模式的常规用法,指挥者类Director在Builder模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把Director和抽象建造者进行结合。
简单版
既然Director是变化的,并且其实在生活中我们自己本身就是Director,所以这个时候我们可以把Director这个角色去掉,因为我们自身就是指挥者。
通过静态内部类方式实现零件无序装配构造,内部有复杂对象的默认实现,使用时可以根据用户需求自由定义更改内容,并且无需改变具体的构造方式,就可以生产出不同复杂产品,这种方式使用更加灵活,更符合定义。
采用链式编程的方式。
例子:麦当劳的套餐,服务员(具体建造者)可以随意搭配任意几种产品(零件)组成一款套餐(产品),然后出售给客户。比第一种方式少了指挥者,主要是因为第二种方式把指挥者交给用户来操作,使得产品的创建更加简单灵活。
创建一个实体类,产品套餐类
// 产品:套餐
public class Product {
private String buildA = "可乐";
private String buildB = "汉堡";
private String buildC = "薯条";
private String buildD = "炸鸡";
// set get toString方法
}
创建抽象的建造者
// 抽象的建造者
public abstract class Builder {
abstract Builder buildA(String msg); // 可乐
abstract Builder buildB(String msg); // 汉堡
abstract Builder buildC(String msg); // 薯条
abstract Builder buildD(String msg); // 炸鸡
abstract Product getProduct();
}
创建具体建造者类
// 创建具体的建造者
public class Worker extends Builder{
private Product product;
public Worker() {
product = new Product();
}
@Override
Builder buildA(String msg) {
product.setBuildA(msg);
return this;
}
@Override
Builder buildB(String msg) {
product.setBuildB(msg);
return this;
}
@Override
Builder buildC(String msg) {
product.setBuildC(msg);
return this;
}
@Override
Builder buildD(String msg) {
product.setBuildD(msg);
return this;
}
@Override
Product getProduct() {
return product;
}
}
测试,使用默认套餐测试
public class Test {
public static void main(String[] args) {
// 服务员
Worker worker = new Worker();
Product product = worker.getProduct();
System.out.println(product);
}
}
输出
Product{buildA='可乐', buildB='汉堡', buildC='薯条', buildD='炸鸡'}
测试,使用自定义套餐测试
public class Test {
public static void main(String[] args) {
// 服务员
Worker worker = new Worker();
// 采用链式编程,在原有的基础上,可以实现套餐的自定义,如果不组合,是默认套餐。
Product product = worker.buildA("大可乐")
.getProduct();
System.out.println(product);
}
}
输出
Product{buildA='大可乐', buildB='汉堡', buildC='薯条', buildD='炸鸡'}
建造者与抽象工厂模式的比较
-
与抽象工厂模式相比,建造者模式返回一个组装好的完整产品,而抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族。
-
在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在建造者模式中,客户端可以不直接调用建造者的相关方法,而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一公完整的对象。
-
如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。
原型模式
介绍:原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
优点:
-
性能提高。
-
逃避构造函数的约束。
缺点:
-
配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
-
必须实现 Cloneable 接口。
注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅克隆实现 Cloneable,重写clone()方法,深克隆是通过实现 Serializable 读取二进制流,还有其他方法。
浅克隆
创建一个类,实现Cloneable接口,重写clone()方法
/**
* 1.实现Cloneable接口
* 2.重写clone()方法
*/
public class Sheep implements Cloneable{
private String name;
private Date birthDate;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
// 无参构造,有参构造
// set get toString方法
}
测试
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Date date = new Date();
Sheep sheep1 = new Sheep("多利", date);
System.out.println("sheep1 -> "+sheep1);
System.out.println("sheep1 -> hash:"+sheep1.hashCode());
// 克隆
Sheep sheep2 = (Sheep) sheep1.clone();
sheep2.setName("多利2");
System.out.println("sheep2 -> "+sheep2);
System.out.println("sheep2 -> hash:"+sheep2.hashCode());
}
}
输出
【注意】
浅克隆,上个案例的sheep1和sheep2引用中的birthDate引用是同一个,改变birthDate的值,两个对象都会发生变化。
同一引用测试
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Date date = new Date();
Sheep sheep1 = new Sheep("多利", date);
Sheep sheep2 = (Sheep) sheep1.clone();
sheep2.setName("多利2");
// 随意改变成一个值,仅供测试
date.setTime(1546545464);
System.out.println("sheep1 -> "+sheep1);
System.out.println("sheep2 -> "+sheep2);
}
}
同一引用测试输出
附加图示
改变代码,使用深克隆。
深克隆
在Sheep类中,更改重写的clone()方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object object = super.clone();
// 实现深克隆
Sheep sheep = (Sheep) object;
// 将这个对象的属性也克隆
sheep.birthDate = (Date) this.birthDate.clone();
return object;
}
测试
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Date date = new Date();
Sheep sheep1 = new Sheep("多利", date);
Sheep sheep2 = (Sheep) sheep1.clone();
sheep2.setName("多利2");
date.setTime(1546545464);
System.out.println("sheep1 -> "+sheep1);
System.out.println("sheep2 -> "+sheep2);
}
}
输出
附加图示:
适配器模式
介绍:将一个类的接口转换成客户希望的另外一个接口。适配器模式使原本由于接口不兼容,而不能一起工作的那些类,可以在一起工作。
图示如下
角色分析:
-
目标接口:客户所期待的接口,目标可以是具体的或抽象的类,也可以是接口。
-
需要适配的类:需要适配的类或适配者类。
-
适配器:通过包装一个需要适配的对象,把原接口转换成目标对象。
适用场景
-
系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
-
想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
分类:
-
类适配器
-
对象适配器
类适配器
使用单继承实现方式
使用网线、转换器、电脑为例,下面是图示:
创建一个需要适配的类:网线
//需要被适配的类:网线
public class Adaptee {
public void link(){
System.out.println("连接网线,可以上网");
}
}
创建客服端类,电脑
// 客户端类:电脑需要上网,但是不能插网线
public class Computer {
// 网线 -> Usb接口的转换
public void net(){
// 这里需要实现转换器的方法
}
}
创建转换器的抽象接口
// 接口转换器的抽象接口
public interface NetToUsb {
// 作用:处理连接方式,网线 -> Usb接口的转换
void handleLink();
}
创建具体转换器
// 具体的适配器,实现网线 -> Usb 的转换
public class Adapter extends Adaptee implements NetToUsb{
@Override
public void handleLink() {
super.link();
}
}
在电脑类中,调用接口的方法
// 客户端类:电脑需要上网,但是不能插网线
public class Computer {
// 网线 -> Usb接口的转换
public void net(NetToUsb adapter){
adapter.handleLink();
}
}
测试
public class Test {
public static void main(String[] args) {
Computer computer = new Computer();
Adaptee adaptee = new Adaptee();
Adapter adapter = new Adapter();
computer.net(adapter);
}
}
类适配器缺点
-
对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者。
-
在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
对象适配器
使用组合的方法
总体上和类适配器方式很像,就是具体适配器有些区别
// 具体的适配器,实现网线 -> Usb 的转换
public class Adapter2 implements NetToUsb{
private Adaptee adaptee;
public Adapter2(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void handleLink() {
adaptee.link();
}
}
测试
public class Test {
public static void main(String[] args) {
Computer computer = new Computer();
Adaptee adaptee = new Adaptee();
Adapter2 adapter2 = new Adapter2(adaptee);
computer.net(adapter2);
}
}
对象适配器优点
-
一个对象适配器可以把多个不同的适配者适配到同一个目标。
-
可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。
桥接模式
介绍:桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interfce)模式。
优点:
-
桥接模式类似于多继承方案,但是多继承方案违背了类的单一职责原则,复用性比较差,类的个数也非常多,桥接模式是比多继承方案更好的解决方法。极大的减少了子类的个数,从而降低管理和维护的成本。
-
桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。符合开闭原则,就像一座桥,可以把两个变化的维度连接起来。
缺点:
-
桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
-
桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
应用场景:
-
Java语言通过Java虚拟机实现了平台的无关性。
-
JDBC驱动程序也是桥接模式的应用之一。
最佳实践:
-
一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
-
如果一个系统需要在构建的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。系统需要对抽象化角色和实现化角色进行动态耦合。
-
虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
案例:电脑可以抽象分为两个维度:品牌、类型,附加图示
创建品牌接口
// 品牌
public interface Brand {
void show();
}
创建华为、联想的品牌类,实现接口
// 华为品牌
public class Huawei implements Brand{
@Override
public void show() {
System.out.print("华为");
}
}
// 联想品牌
public class Lenovo implements Brand{
@Override
public void show() {
System.out.print("联想");
}
}
创建抽象的电脑类型类
// 抽象的电脑类型类
public abstract class Computer {
// 品牌
protected Brand brand;
public Computer(Brand brand) {
this.brand = brand;
}
public void show(){
brand.show();
}
}
创建电脑类型类,继承Computer类
// 台式机类型
public class Desktop extends Computer{
public Desktop(Brand brand) {
super(brand);
}
@Override
public void show() {
super.show();
System.out.println("台式机");
}
}
// 笔记本类型
public class Laptop extends Computer{
public Laptop(Brand brand) {
super(brand);
}
@Override
public void show() {
super.show();
System.out.println("笔记本");
}
}
测试
public class Test {
public static void main(String[] args) {
// 华为笔记本
Computer computer = new Laptop(new Huawei());
computer.show();
// 联想台式机
Computer computer1 = new Desktop(new Lenovo());
computer1.show();
}
}
代理模式
动态代理:基于*反射机制*。 1、什么是动态代理 ? 使用jdk的反射机制,创建对象的能力,创建的是代理类的对象。 而不用你创建类文件。不用写java文件。 动态:在程序执行时,调用jdk提供的方法才能创建代理类的对象。
jdk动态代理,必须有接口,目标类必须实现接口, 没有接口时,需要使用cglib动态代理。
2、动态代理能做什么 ? 可以在不改变原来目标方法功能的前提下, 可以在代理中增强自己的功能代码。
在开发中也会有这样的情况, 你有A类, 本来是调用C类的方法,完成某个功能。 但是C不让A调用。可以在A和C之间创建一个B代理,C让B访问。 所以A可以通过B,间接访问C。
说白了B就是一个中间人。而C只认B,不认别人。
3、使用代理模式的作用
-
功能增强: 在原有的功能上,增加了额外的功能。 新增加的功能,叫做功能增强。
-
控制访问: 代理类可以不让你访问目标,例如商家不让用户访问厂家。
4、实现代理的方式
静态代理
1)代理类是自己手动实现的。2)代理的目标类是确定的。
特点:
-
实现简单。
-
容易理解。
缺点:当项目中,目标类和代理类很多时候,有以下的缺点:
-
当目标类增加了,代理类可能也需要成倍的增加。 代理类数量过多。
-
当你的接口中功能增加了,或者修改了,会影响众多的实现类,厂家类,代理都需要修改。影响比较多。
模拟一个用户购买u盘的行为。 用户是客户端类。商家:代理,代理某个品牌的u盘。厂家:目标类。
三者的关系: 用户(客户端)---商家(代理)---厂家(目标) 商家和厂家都是卖u盘的,他们完成的功能是一致的,都是卖u盘。
实现步骤: 1、创建一个接口,定义卖u盘的方法, 表示你的厂家和商家做的事情。
public interface UsbSell {
float sell(int amount);
}
【注意】:接口新加一个方法,其他的实现类都需有修改,若类的数量太大,工作量很大。 2、创建厂家类,实现1步骤的接口
public class UsbKingFactory implements UsbSell {
@Override
public float sell(int amount) {
System.out.println("目标类中的方法调用,UsbKingFactory工厂中的sell方法");
return 85.0f;
}
}
这个是工厂类,代理(商家)可以访问,这个案例中顾客不可直接访问。 3、创建商家,就是代理,也需要实现1步骤中的接口。
public class WeiShang implements UsbSell {
private UsbSell factory = new UsbKingFactory();
@Override
public float sell(int amount) {
float price = factory.sell(amount);
price = price + 1;
return price;
}
}
这个类中,可以实现功能增强。 4、创建客户端类,调用商家的方法买一个u盘。
public class ShopMain {
public static void main(String[] args) {
WeiShang weiShang = new WeiShang();
float price = weiShang.sell(1);
System.out.println("通过微商购买的价格:"+ price);
}
}
代理类完成的功能:
-
目标类中方法的调用。
-
功能增强。
动态代理
动态代理: 在程序执行过程中,使用jdk的反射机制,创建代理类对象, 并动态的指定要代理目标类。换句话说: 动态代理是一种创建java对象的能力,让你不用创建类,就能创建代理类对象。
在静态代理中目标类很多时候,可以使用动态代理,避免静态代理的缺点。 动态代理中目标类即使很多, 1)代理类数量可以很少, 2)当你修改了接口中的方法时,不会影响代理类。
动态代理的实现: 方式一:jdk动态代理: 使用java反射包中的类和接口实现动态代理的功能。 反射包 java.lang.reflect , 里面有三个类 : InvocationHandler , Method, Proxy.
实现jdk动态代理的步骤: 1、创建接口,定义目标类要完成的功能
public interface UsbSell {
float sell(int amount);
}
2、创建目标类实现接口
public class UsbKingFactory implements UsbSell {
@Override
public float sell(int amount) {
System.out.println("执行目标类中sell方法");
return 85.0f;
}
}
3、创建InvocationHandler接口的实现类,在invoke方法中完成代理类的功能
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MySellHandler implements InvocationHandler {
private Object target = null;
public MySellHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object res = null;
res = method.invoke(target,args); // 执行目标方法
if (res != null) {
Float price = (Float)res;
price = price + 25; //增强功能
res = price;
}
return res;
}
}
解析:构造方法,目标对象是活动的,不是固定的,需要传入进来。传入是谁,就给谁创建代理。
1)InvocationHandler 接口(调用处理器) invoke():表示代理对象要执行的功能代码。代理类要完成的功能就写在invoke()方法中。 invoke()参数: Object proxy:jdk创建的代理对象,无需赋值。 Method method:目标类中的方法,jdk提供method对象的 Object[] args:目标类中方法的参数, jdk提供的。
InvocationHandler 接口:表示代理要干什么。 创建类实现接口InvocationHandler,重写invoke()方法, 把原来静态代理中代理类要完成的功能,写在这。
代理类的功能: 1)调用目标方法,执行目标方法的功能。 2)功能增强,在目标方法调用时,增加功能。
2)Method类:表示方法的, 确切的说是目标类中的方法。 作用:通过Method可以执行某个目标类的方法。 语法:method.invoke(目标对象,方法的参数)
说明: method.invoke()就是用来执行目标方法的,等同于静态代理中的float price = factory.sell(amount);
4、使用Proxy类的静态方法,创建代理对象。 并把返回值转为接口类型。
1)创建目标对象
UsbSell factory = new UsbKingFactory();
2)创建InvocationHandler对象
InvocationHandler handler = new MySellHandler(factory);
3)创建代理对象
UsbSell proxy = (UsbSell) Proxy.newProxyInstance(factory.getClass().getClassLoader(),
factory.getClass().getInterfaces(),
handler);
System.out.println("proxy="+proxy.getClass().getName());
解析:Proxy类:核心的对象,创建代理对象。之前创建对象都是 new 类的构造方法(),现在我们是使用Proxy类的方法,代替new的使用。
静态方法 newProxyInstance() 作用: 创建代理对象, 等同于静态代理中的WeiShang weiShang = new WeiShang(); 参数: 1. ClassLoader loader:类加载器,负责向内存中加载对象的。 使用反射获取对象的ClassLoader类加载器 2. Class<?>[] interfaces: 接口,目标对象实现的接口,也是反射获取的。 3. InvocationHandler h :我们自己写的,代理类要完成的功能。
返回值:就是代理对象
输出:proxy=com.sun.proxy.$Proxy0,这是JDK动态代理创建的对象类型
4)通过代理执行方法
float price = proxy.sell(1);
System.out.println("通过动态代理对象,调用方法:" + price);
附加图例
方式二:cglib动态代理:cglib是第三方的工具库, 创建代理对象。 cglib的原理是继承, cglib通过继承目标类,创建它的子类,在子类中重写父类中同名的方法, 实现功能的修改。因为cglib是继承,重写方法,所以要求目标类不能是final的, 方法也不能是final的。cglib的要求目标类比较宽松, 只要能继承就可以了。cglib在很多的框架中使用, 比如 mybatis ,spring框架中都有使用。