目录
Java 设计模式(java design patterns)
注解
什么是注解
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
Java 语言中的类、方法、变量、参数和包等都可以被标注。Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。当然它也支持自定义 Java 标注。
内置注解
Java 语言中已经定义好的注解
@Override
检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
@Deprecated
标记过时方法。如果使用该方法,会报编译警告。
@SuppressWarnings
指示编译器去忽略注解中声明的警告。
@FunctionalInterface
用于指示被修饰的接口是函数式接口。
元注解
元注解是 java API 提供的,是用于修饰注解的注解,通常用在注解的定义上:
@Retention
标识这个注解怎么保存,是只在代码中,还是编入 class 文件中,或者是在运行时可以通过反射访问。
@Documented
标记这些注解是否包含在用户文档中。
@Target
标记这个注解应该是哪种 Java 成员。
@Inherited
标记这个注解是继承于哪个注解类(默认注解并没有继承于任何子类)。
@Repeatable
标识某注解可以在同一个声明上使用多次。
重点掌握
@Target:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
ElementType.TYPE 可以应用于类的任何元素。
ElementType.CONSTRUCTOR 可以应用于构造函数。
ElementType.FIELD 可以应用于字段或属性。
ElementType.LOCAL_VARIABLE 可以应用于局部变量。
ElementType.METHOD 可以应用于方法级注释。
ElementType.PACKAGE 可以应用于包声明。
ElementType.PARAMETER 可以应用于方法的参数。
@Retention:@Retention 定义了该注解被保留的时间长短:某些注解仅出现在源代码中,而被编译器丢弃;而另一些却被编译在 class 文件中;编译在 class文件中的注解可能会被虚拟机忽略,而另一些在 class 被装载时将被读取(请注意并不影响 class 的执行,因为注解与 class 在使用上是被分离的)。用于描述注解的生命周期(即:被描述的注解在什么范围内有效)取值有:
1.SOURCE:在源文件中有效(即源文件保留)
2.CLASS:在 class 文件中有效(即 class 保留)
3.RUNTIME:在运行时有效(即运行时保留)
自定义注解
注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)//作用在属性上
@Retention(RetentionPolicy.RUNTIME)//在运行时检测
public @interface NotNull {
String message() default ""; //注解属性
int length() default 0;
String lengthmessage() default "";
}
使用注解的类
public class User {
private int num;
@NotNull(message="姓名不能为空",length=3,lengthmessage="长度不能小于3")
private String name;
public String getName() {
return name;
}
/**
* 是使用在文档注释中的标注,与@NotNull是不同的
* @param name
*/
public void setName(String name) {
this.name = name;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public static void main(String[] args) {
new Date().getYear();
ArrayList list = new ArrayList();
}
}
测试类
public class Test {
//测试注解
public static void main(String[] args) throws NoSuchMethodException, SecurityException, Exception {
User user = new User();
// user.setName("ji");
//通过反射机制获取类的注解信息
Field[] fields = user.getClass().getDeclaredFields();//获取user类中所有的属性
for (Field field : fields) {
NotNull notNull = field.getAnnotation(NotNull.class);//获得此属性上的 名为NotNull的注解标签
if (notNull != null) {
//通过属性名获取找到属性的get方法
Method m = user.getClass().getMethod("get" + getMethodName(field.getName()));
Object obj=m.invoke(user);//执行get方法 获得属性值
if (obj==null) { //值为空 获取注解信息
System.err.println(field.getName() +notNull.message());
throw new NullPointerException(notNull.message());
}else{
if(String.valueOf(obj).length()<(notNull.length())){
System.err.println(field.getName() +notNull.lengthmessage());
}
}
}
}
}
/**
* 把一个字符串的第一个字母大写
*/
private static String getMethodName(String fildeName) throws Exception {
byte[] items = fildeName.getBytes();
items[0] = (byte) ((char) items[0] - 'a' + 'A');
return new String(items);
}
}
对象克隆
为什么要克隆?
直接 new 一个对象不行吗?
new 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠 clone 方法了。
误区:
我们常见的
Student stu1 = new Student ();
Student stu2 = stu1 ;
这种形式的代码复制的是引用,即对象在内存中的地址,a 和 b 对象仍然指向了同一个对象。这种只能称为引用复制,两个引用指向的还是同一个对象。
如何实现克隆
先介绍一下两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。
在 Java 语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括 int、double、byte、boolean、char 等简单数据类型,引用类型包括类、接口、数组等复杂类型。基本类型的值可以直接复制,引用类型只能复制引用地址。所以浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复 制。
浅克隆和深克隆
浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制。
实现方式:
1.在 Java 语言中,通过覆盖 Object 类的 clone()方法可以实现浅克隆。
2.在 spring 框架中提供 BeanUtils.copyProperties(source,target);
深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
在 Java 语言中,如果需要实现深克隆,可以通过覆盖 Object 类的 clone()方法实现,也可以通过序列化(Serialization)等方式来实现。
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable 接口,否则无法实现序列化操作。
解决多层克隆问题
如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用 clone 方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。
Java 设计模式(java design patterns)
设计模式概念
设计模式产生的背景
"设计模式"最初并不是出现在软件设计中,而是被用于建筑领域的设计中。
1977 年美国著名建筑大师、加利福尼亚大学环境结构中心主任克里斯托夫·亚历山大(Christopher Alexander)在他的著作《建筑模式语言:城镇、建筑、构造》中描述了一些常见的建筑设计问题,并提出了 253 种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。
1990 年软件工程界开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。直到 1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(JohnVlissides)等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》一书,在此书中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这 4 位作者在软件开发领域里也以他们的“四人组”(Gang of Four,GoF)著称。
软件设计模式的概念
软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
为什么要学习设计模式
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下优点:
可以提高程序员的思维能力、编程能力和设计能力。
使程序设计更加标准化、使软件开发效率大大提高。
使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
能够更好的去理解源码架构。
如果需要设计大型项目架构,我们必须考虑,当增加新的功能,代码变动成本最低;新增加功能不对以前功能进行影响,如果没有很好的设计,那么将会非常糟糕.
建模语言
统一建模语言(Unified Modeling Language,UML)是一种用于软件系统分析和设计的语言工具,用于帮助软件开发人员进行思考和记录思路的结果。
UML 图:通过不同的图形和符号,来描述软件模型及各个元素之间的关系。
类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。
在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的理解;类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。
类
类是指具有相同属性、方法和关系的对象的抽象,它封装了数据和行为,是面向对象程序设计(OOP)的基础,具有封装性、继承性和多态性等三大特性。在 UML 中,类使用包含类名、属性和操作且带有分隔线的矩形来表示。
(1) 类名(Name)是一个字符串,例如,Student。
(2) 属性(Attribute)是指类的特性,即类的成员变量。UML 按以下格式表示:
[可见性]属性名:类型[=默认值]
例如:-name:String
注意:“可见性”表示该属性对类外的元素是否可见,包括公有(Public)、私有(Private)、受保护(Protected)和朋友(Friendly)4 种,在类图中分别用符号+、-、#、~表示。
(3) 操作(Operations)是类的任意一个实例对象都可以使用的行为,是类的成员方法。UML 按以下格式表示:
[可见性]名称(参数列表)[:返回类型]
例如:+display():void。
学生类的 UML 表示。
接口
接口(Interface)是一种特殊的类,它具有类的结构但不可被实例化,只可以被子类实现。它包含抽象操作,但不包含属性。它描述了类或组件对外可见的动作。在 UML 中,接口使用一个带有名称的小圆圈来进行表示。图形类接口的 UML 表示。
类之间的关系
在软件系统中,类不是孤立存在的,类与类之间存在各种关系。根据类与类之间的耦合度从弱到强排列,UML 中的类图有以下几种关系:依赖关系、关联关系、聚合关系、组合关系、泛化关系和实现关系。其中泛化和实现的耦合度相等,它们是最强的。
依赖关系
依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是人与手机的关系图,人通过手机的语音传送方法打电话。
关联关系
关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。我们先介绍一般关联。
关联又可以分为单向关联,双向关联,自关联。
单向关联
在 UML 类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过让 Customer 类持有一个类型为 Address 的成员变量类实现。
双向关联
从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。
在 UML 类图中,双向关联用一个不带箭头的直线表示。上图中在 Customer 类中维护一个 List,表示一个顾客可以购买多个商品;在 Product 类中维护一个 Customer 类型的成员变量表示这个产品被哪个顾客所购买。
自关联
自关联在 UML 类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node 类包含类型为 Node 的成员变量,也就是“自己包含自己”。
聚合关系
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和教师的关系图:
组合关系
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。
在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:
继承关系
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系,是 is-a 的关系。
在 UML 类图中,继承关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现继承关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图下图所示。
实现关系
实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。
在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如下图所示。
面向对象设计原则
在面向对象的设计过程中,首先需要考虑的是如何同时提高一个软件系统的可维护性和可复用性。这时,遵从面向对象的设计原则,可以在进行设计方案时减少错误设计的产生,从不同的角度提升一个软件结构的设计水平。
单一职责
单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小。
对于单一职责原则,可以理解为一个类只负责一个功能领域中的相应职责,即一个类不要负责太多“杂乱”的工作。
在软件系统中,如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时设计或遭受到意想不到的破坏。
以项目开发为例,如果项目组成员每个人的职责都很明确,可以专心开发自己负责的模块,则项目成果的质量往往很高。相反,如果职责不清晰,分工就会混乱。
优点:
低耦合
高内聚
开闭原则
开闭原则即对扩展开放,对修改封闭。在软件系统开发过程中,软件的需求往往会随着时间的推移而发生变化。因此,进行软件设计时需要考虑怎样的设计才能面对需求的改变却可以相对保持稳定,从而使得系统可以在第一个版本以后不断推出新的版本,这时便可以以开闭原则作为指导。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在进行软件设计时,一般先评估出最有可能发生变化的类,然后构造抽象来隔离那些变化。当变化发生时,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,是现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
优点:
适应性和灵活性
稳定性和延续性
可复用性与可维护性
里氏替换原则
再说继承
继承优势
提高代码的复用性(每个子类有拥有父类的属性和方法)
提高代码的可扩展性
继承劣势
继承是侵入性的(只要继承,就必须拥有父类的属性和方法,体系结构复杂)
继承机制很大的增加了耦合性(父类被子类继承,父类功能修改会影响子类)
里氏替换原则的提出
1987 年由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士在“面向对象技术的高峰会议”上发表的一篇文章《数据抽象和层次》里提出来的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立。
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。
里氏替换原则的定义
所有使用父类的地方必须能透明地使用其子类的对象。
里氏替换原则表明,在软件中将一个基类对象替换成它的子类对象时程序将不会产生任何错误和异常。
通俗地说,对于里氏替换原则,我们可作如下表述:任何基类可以出现的地方,子类一定可以出现。所以,子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时除添加新的方法完成新增功能外尽量不要重写父类的方法。
里氏替换原则的主要作用
里氏替换原则是实现开闭原则的重要方式之一
它克服了继承中重写父类方法造成的可复用性变差的缺点
它是功能正确性的保证
加强程序的健壮性,提高程序的维护性降低需求变更时引入的风险
依赖倒置
上层模块不应该依赖底层模块,它们都应该依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象
是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。也就是针对抽象层编程,面向接口编程。
接口隔离
使用多个接口,而不使用单一的总接口,不强迫新功能实现不需要的方法。
迪米特原则
迪米特原则是1987年秋天在美国东北大学一个叫做迪米特的项目设计提出的,它要求一个对象应该对其他对象有最少的了解,所以迪米特法则又叫做最少知识原则。
只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。
直接朋友:
-
类中的成员属性
-
在类中的方法作为参数使用
-
在类中的方法作为返回值类型
注意事项:
迪米特法则的核心是降低类之间的耦合
从被依赖者的角度来说,尽量将逻辑封装在类的内部,对外除了提供的 public 方法,不泄露任何信息
从依赖者的角度来说,只依赖应该依赖的对象
切忌不要为了用而用
组合/聚合复用原则
优先使用组合,使系统更灵话,其次才考虑继承,达到复用的目的。
一般而言,如果两个类之间是"Has-A"关系应使用组合或聚合,如果是"Is-A"关系可使用继承。
总结
开闭原则:要求对扩展开放,对修改关闭
里氏替换原则:不要破坏继承体系
依赖倒置原则:要求面向接口编程
单一职责原则:实现类职责要单一
接口隔离原则:在设计接口的时候要精简单一
迪米特法则:只与直接的朋友的通信
合成复用原则:尽量使用聚合和组合的方式,而不是使用继承
设计原则的核心思想
找出应用中可能需要变化之处,独立出来,不要和不需要变化的代码混在一 起
针对接口编程,而坏是针对实现编程
为了交互对象的松耦合设计而努力
遵循设计原则:就是为了让程序高内聚,低耦合
java 设计模式类型
据模式是用来完成什么工作来划分,这种方式可分为创建型模式、结构型 模式和行为型模式 3 种。
创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。提供了单例、原型、工厂方法、抽象工厂、建造者 5 种创建型模式。
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,提供了代理、 适配器、桥接、装饰、外观、享元、组合 7 种结构型模式。
行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独 完成的任务,以及怎样分配职责。提供了模板方法、策略、命令、职责链、状态、 观察者、中介者、迭代器、访问者、备忘录、解释器 11 种行为型模式。
23 种设计模式介绍
-
单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
-
原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
-
工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
-
抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
-
建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
-
代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
-
适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
-
桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
-
装饰(Decorator)模式:动态的给对象增加一些职责,即增加其额外的功能。
-
外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
-
享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
-
组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
-
模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
-
策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
-
命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
-
职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
-
状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
-
观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
-
中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
-
迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
-
访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
-
备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
-
解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
常用设计模式
单例模式
在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式. 例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
特点
-
单例类只有一个实例对象
-
该单例对象必须由单例类自行创建
-
单例类对外提供一个访问该单例的全局访问点
结构
单例类:包含一个实例且能自行创建这个实例的类。 访问类:使用单例的类。其结构如图所示。
通常两种实现
第 1 种:懒汉式单例 该模式的特点是类加载时没有生成单例对象,只有当第一次调用 getlnstance方法时才去创建这个单例。这种写法会存在线程安全问题
public class Singleton {
public static volatile Singleton singleton;
private Singleton(){
}
public static Singleton creatSingleton(){
if (singleton == null){
synchronized (Singleton.class){
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
第 2 种:饿汉式单例 该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了,这种写法在加载中创建,不会出现线程安全问题
/*
* 饿汉式单例
* 一般又称为急切式单例
* 在类加载时,就会创建此单例对象,这种写法不会出现线程安全问题
*/
public class Singleton {
//创建 Singleton 的一个对象
private static Singleton instance = new Singleton();
//让构造函数为 private
private Singleton(){}
//获取唯一可用的对象
public static Singleton getInstance(){
return instance;
}
}
懒汉式单例双重检索+volatile
idea 安装 IDEA 字节码查看插件 "jclasslib Bytecode Viewer" 选中编译后的 class 文件
源码
public class D {
int i = 0;
public void test(){
}
public static void main(String[] args) {
D d = new D();
}
}
编译后的汇编指令码
正常执行顺序
0 new #3 //new 申请内存空间
3 dup
4 invokespecial #4 : ()V> //调用构造方法
7 astore_1 //将对象地址赋给引用变量
8 return
线程 1 开始执行,先执行 new,在内存中申请内存空间
此时指令可能发生重排序,先把半成品对象引用地址赋给引用变量 t
线程 1暂停执行,线程 2进入到 cpu执行,引用变量t 不为空,指向的是半成品对象
Runtime 类
Jdk 中的源码 Runtime 类就是一个单例类,利用 Runtime 类可以启动新的进程或进行相关运行时环境的操作。比如,取得内存空间以及释放垃圾空间。Runtime 类属于典型的单例设计。
工厂模式(Factory Pattern)
简单工厂模式
简单工厂模式并不是 23 种设计模式之一,因为它并不符合开闭原则。主要目的是为了引出工厂方法模式,适合产品子类比较少的、创建操作比较简单的情况。
由一个工厂类根据传入的参数(一般是字符串参数),动态决定应该创建哪一个产品子类的实例,并以父类形式返回。
该模式中包含的角色及其职责:
工厂角色:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类 提供静态方法,可以被外界直接调用,创建所需的产品对象。
/*
* 工厂,负责生产对象
*/
public class SimpleFactory {
//工厂中负责制造对象的方法
public Product createProduct(String className){
if(className == null){
return null;
}else{
try {
return (Product) Class.forName(className).newInstance();//反射机制
} catch (InstantiationException e) {
e.printStackTrace();
return null;
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
}
抽象产品角色:简单工厂模式所创建的所有对象的父类,描述所有实例共有的接 口。可以是抽象类或接口。
//抽象产品
public interface Product {
void show();
}
具体产品角色:是简单工厂模式的创建目标。
//具体产品1
public class ProductA implements Product {
@Override
public void show() {
System.out.println("具体产品1显示...");
}
}
//具体产品2
public class ProductB implements Product {
@Override
public void show() {
System.out.println("具体产品2显示...");
}
}
优点
客户端不负责对象的创建,而是由专门的工厂类完成;客户端只负责对象的 调用,实现了创建和调用的分离,降低了客户端代码的难度;
缺点
如果增加和减少产品子类,需要修改简单工厂类,违背了开闭原则如果产品 子类过多,会导致工厂类非常的庞大,违反了高内聚原则,不利于后期维护.
适用场景
所有的产品子类都有同一个父类(或接口),属于同一个产品系列产品子类 比较少的、创建操作比较简单.
工厂方法模式
与简单工厂模式不同,工厂方法模式的对工厂也进行了抽象。有一个抽象的Factory 类(可以是抽象类和接口),这个类将不在负责具体的产品生产,而是只制定一些规范,将实际创建工作推迟到子类去完成。
在简单工厂模式中,有个全能的园丁,控制所有作物的种植、生长和收获。现在农场规模变大了,管理更加专业化了。过去全能的园丁没有了,每一种作物都有专门的园丁管理,形成了规模化和专业化生产。
基本原理
在这个模式中,工厂类和产品类往往可以——对应。即一个抽象工厂对应 一个抽象产品,一个具体工厂对应一个具体产品,这个具体的工厂就负责生产对 应的产品。 该模式中包含的角色及其职责.
抽象工厂角色:工厂方法模式的核心,与应用程序无关。任何在模式中创建的对 象的工厂类必须实现这个接口。
具体工厂角色:这是实现抽象工厂接口的具体工厂类,包含与应用程序密切相关 的逻辑,并且受到应用程序调用以创建产品对象。
抽象产品角色:工厂方法模式所创建的对象的父类型,也就是产品对象的共同父 类或共同拥有的接口。
具体产品角色:这个角色实现了抽象产品角色所定义的接口。某具体产品有专门 的具体工厂创建,它们之间往往——对应。
优点:
客户端不负责对象的创建,而是由专门的工厂类完成;客户端只负责对象的调用, 实现了创建和调用的分离,降低了客户端代码的难度;若增加和减少产品子类,不需修改工厂类,只增加产品子类和工厂子类,符合开闭原则即使产品子类过多, 不会导致工厂类的庞大,利于后期维护
缺点:
需要额外的编写代码,增加了工作量
适用场景:
所有的产品子类都有同一个父类(或接口),属于同一个产品系列产品子类 比较多的、创建操作比较复杂
抽象工厂模式
简单工厂模式:一个工厂类负责同一类所有产品对象的创建,如果产品较多,职责大大增加。增加或减少产品时,需要修改工厂类,违背了开闭原则。
工厂方法模式:一个具体的工厂类负责创建一个单独的产品,如果产品增加或减少,一不需要修改工厂类,符合开闭原则。但是即使这些产品是有联系的,也必须由不同的工厂分别创建
基本原理
抽象工厂模式中,一个具体的工厂负责创建一系列相互关联的产品。会简化客户端的调一用。并且更换产品系列非常方便,更换一个工厂类即可。
该模式中包含的角色及其职责: 抽象工厂、具体工厂、抽象产品、具体产品。
优点:获取具体系列产品只需要通过具体系列工厂获取,无序关心创建的细节
原型模式
有时候,我们需要多个实例,但是创建这个实例的过程比较复杂,比如构造函数非常的复杂,执行这个构造函数时会消耗较长的时间,但另外一方面,这个构造函数中的一些信息又没有什么变化(也就是说创建第一个实例时初始化信息是这样的,创建第二个实例时初始化信息还是还是这样的),那么直接使用 new 再创建这样一个实例就显得太昂贵了,此时可以使用克隆,也就是复制,就是通过复制现在已经有了的实例来创建新的实例,提高创建速度.
生活案例:同一个简历模板打印了 5 分,要求同一个面试者进行填写,每份简历内容相同。面试者写了第一份后(多么复杂的一个创建对象的过程),还需要再重复的写四份吗?那太费时间了。直接找个复印机复印四份不就可以了。原型模式就是这个道理.
代理模式
当你需要买东西的时候,通常都不是直接去买的,而是通过中间商代理来买,例如购买火车票通过12306来买
优点
代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
代理对象可以扩展目标对象的功能
代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度
结构
抽象主题类:类通过接口或抽象类声明真实主题和代理对象实现的业务方法。
真实主题类:实现了抽象主题中的具体业务,是代理对象所代表的二等真实对象,是最终要引用的对象
代理类:提供了与真实主题相同的接口,其内部有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
代理实现可以分为静态代理和动态代理
静态代理
静态代理模式的特点,代理类接受一个 Subject 接口的对象,任何实现该接 口的对象,都可以通过代理类进行代理,增加了通用性。
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
缺点:一个代理类只能代理一个接口,工作量太大;代理类是运行前编码已 经完成的;必须先有接口,再有代理;接口一旦发生变量,代理类也要修改
动态代理
在动态代理中我们不再需要再手动的创建代理类,我们只需要编写一个动态 处理器就可以了。真正的代理对象在运行时为我们动态的来创建。
动态代理分为 jdk 动态代理和 cglib 动态代理
jdk 代理
动态代理是实现方式,是通过反射来实现的,借助 Java 自带的java.lang.reflect.Proxy,通过固定的规则生成。
编写一个委托类的接口,即静态代理的
/*
Dao接口,定义保存功能
*/
public interface BaseDao {
void save();
}
实现一个真正的委托类,即静态代理的
/*
实际功能实现类
*/
public class UserDaoImpl implements BaseDao {
@Override
public void save() {
System.out.println("UserDaoImpl:save()");
}
}
创建一个动态代理类,实现 InvocationHandler 接口,并重写该 invoke 方法
/*
* 动态代理类
* jdk代理 底层实现使用反射机制
*/
public class DynamicDaoProxy implements InvocationHandler {
// 被代理类的实例 目标
private Object object;// BaseDao ---> Object(任意的)
// 将被代理者的实例传进动态代理类的构造函数中
public DynamicDaoProxy(Object object) {
this.object = object;
}
/*
* 覆盖InvocationHandler接口中的invoke()方法
* Object proxy 表示代理对象
* Method method 代理对象中的方法
* Object[] args 表示代理方法中的参数
* 更重要的是,动态代理模式可以使得我们在不改变原来已有的代码结构
* 的情况下,对原来的“真实方法”进行扩展、增强其功能,并且可以达到
* 控制被代理对象的行为,下面的before、after就是我们可以进行特殊
* 代码切入的扩展点了。
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
Object result = method.invoke(object, args);//用反射机制获取到目标对象中的方法
System.out.println("after");
return result;
}
}
在测试类中,生成动态代理的对象。
public class Test {
public static void main(String[] args) {
//我们要代理的真实对象 Service层是需要添加事务
UserDaoImpl userDaoImpl = new UserDaoImpl();
//我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法的
InvocationHandler dynamicProxy = new DynamicDaoProxy(userDaoImpl);
/*
* 通过Proxy的newProxyInstance方法来创建我们的代理对象,我们来看看其三个参数
* 第一个参数 handler.getClass().getClassLoader() ,我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象
* 第二个参数realSubject.getClass().getInterfaces(),我们这里为代理对象提供的接口是真实对象所实行的接口,表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了
* 第三个参数dynamicProxy, 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上
*/
//动态生成的代理对象
BaseDao baseDao = (BaseDao) Proxy.newProxyInstance(dynamicProxy.getClass().getClassLoader(), userDaoImpl.getClass().getInterfaces(), dynamicProxy);
// mybatis 接口代理 访问 接口中的方法
baseDao.save();
}
}
jdk 动态代理总结:
虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了 对业务接口的依赖,降低了耦合度。
但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持 interface 代理的桎梏,因为它的设计注定了这个遗憾。
Cglib 代理
JDK 实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要 CGLib 了。
CGLIB(Code Generator Library)是一个强大的、高性能的代码生成库。CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。
CGLIB 相比于 JDK 动态代理更加强大,JDK 动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么 Java 动态代理就无法使用了。
Cglib 子类代理实现方法:
1.需要引入 cglib 的 jar 文件,但是 Spring 的核心包中已经包括了 Cglib 功能,所以直接引入 spring-core-xxx.jar 即可.
2.引入功能包后,就可以在内存中动态构建子类
3.代理的类不能为 final,否则报错
4.目标对象的方法如果为 final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.
CGLIB 创建的动态代理对象比JDK 创建的动态代理对象的性能更高,但是 CGLIB创建代理对象时所花费的时间却比 JDK 多得多。所以对于单例的对象,因为无需频繁创建对象,用 CGLIB 合适,反之使用 JDK 方式要更为合适一些。同时由于 CGLib 由于是采用动态创建子类的方法,对于 final 修饰的方法无法进行代理。