所谓单例,指的就是单实例,有且仅有一个类的实例,该类提供了一个全局访问点供外部获取该实例,这个单例不应该由人来控制,而由该类负责创建自己的对象,同时确保只有单个对象被创建。
在创建实例时,检查系统是否已经存在该单例,如果存在则返回该实例,否则创建一个新实例。
总结:单例类只能有一个实例,单例类必须自己创建自己的唯一实例。
单例有其独有的使用场景,一般是对于那些业务逻辑上限定不能多例只能单例的情况:如,在多进程多线程环境下操作文件时,避免多个进程或线程同时操作一个文件,需要通过唯一实例进行处理。WEB 中的计数器,一般需要使用一个实例来进行记录,若多例计数则会不准确。
使用单例模式的好处是内存中只有一个实例,减少内存开销,避免频繁创建和销毁实例。
单例模式包含以下几个主要角色:
(1)单例类:包含单例实例的类,通常将构造函数声明为私有。
(2)静态成员变量:用于存储单例实例的静态成员变量。
(3)获取实例方法:静态方法,用于获取单例实例。
(4)私有构造函数:防止外部直接实例化单例类。
(5)线程安全处理:确保在多线程环境下单例实例的创建是安全的。
单例模式主要有以下三种模式:
(1)懒汉式:这种方式是最基本的实现方式,这种实现最大的问题就是必须加锁 synchronized 才能保证单例,但加锁会影响效率。
(2)饿汉式:这种方式是基于 classloader 机制避免了多线程的同步问题,没有加锁,执行效率会提高,但没有达到 lazy loading 的效果,类加载时就初始化,浪费内存。
(3)静态内部类/登记式:这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它与饿汉式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为内部类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载内部类,从而实例化 instance。
懒汉式必须加锁 synchronized 才能保证单例。
/**
*/
public class LazySingleton {
// 用于存储单例实例的静态成员变量
private static LazySingleton instance;
// 构造方法必须私有化
private LazySingleton(){}
// 静态方法,用于获取单例实例,保在多线程环境下单例实例的创建是安全的
public static synchronized LazySingleton getInstance() {
// 如果不存在单例对象,创建对象,否则就返回已有对象
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
/**
* 测试
*/
public static void main(String[] args) {
// 在多线程下创建单例
for(int i = 0;i < 10;i++){
new Thread(()->{
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}).start();
}
}
}
饿汉式基于 classloader 机制避免了多线程的同步问题,但没有达到 lazy loading 的效果,类加载时就初始化,浪费内存。
/**
*/
public class HungrySingleton {
// 用于存储单例实例的静态成员变量,很明显,一进来就加载对象,存在浪费空间
private final static HungrySingleton instance = new HungrySingleton();
// 构造方法必须私有化
private HungrySingleton(){}
// 静态方法,用于获取单例实例,保在多线程环境下单例实例的创建是安全的
public static HungrySingleton getInstance() {
return instance;
}
/**
* 测试
*/
public static void main(String[] args) {
// 在多线程下创建单例
for(int i = 0;i < 10;i++){
new Thread(()->{
HungrySingleton instance = HungrySingleton.getInstance();
System.out.println(instance);
}).start();
}
}
}
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为内部类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载内部类,从而实例化 instance。
/**
*/
public class HolderSingleton {
// 静态内部类用于存储单例实例的静态成员变量
private static class holder{
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
// 构造方法必须私有化
private HolderSingleton(){}
// 静态方法,用于获取单例实例,保在多线程环境下单例实例的创建是安全的
public static HolderSingleton getInstance() {
return holder.INSTANCE;
}
/**
* 测试
*/
public static void main(String[] args) {
// 在多线程下创建单例
for(int i = 0;i < 10;i++){
new Thread(()->{
HolderSingleton instance = HolderSingleton.getInstance();
System.out.println(instance);
}).start();
}
}
}
总结:一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用登记方式。
工厂模式是一种创建对象的设计模式,它提供了一种方式来封装对象的创建逻辑, 并根据不同的需求,返回相应的对象实例。工厂模式使得客户端代码与具体对象的创建过程解耦(将代码中的关联性和依赖性降低,使得各个模块之间的耦合度减少),提高了代码灵活性和可维护性。
总结:工厂模式是通过封装对象创建的过程,提供了一种统一的接口来创建对象。 它隐藏了对象的具体实现细节(抽象),提供了灵活性、可扩展性,并提高了代码的可读性和可维护性。
1、工厂模式的作用
工厂模式的主要目的是解决以下问题:
(1)将对象的创建逻辑与使用代码分离,使得使用代码不需要关心对象具体如何创建,即抽象。
(2)提供一种简单的扩展机制,使得新增一种产品时不需要修改现有的代码,只需扩展工厂类即可,即多态。
工厂模式的核心思想是引入一个抽象的工厂接口或抽象类,该接口或抽象类定义了创建对象的方法,具体的对象创建由实现该接口或抽象类的具体工厂类来完成。
2、工厂模式的优点
使用工厂模式,具有以下优点:
(1)封装对象的创建过程:工厂模式将对象的创建过程封装在工厂类中,使得客户端无需关注对象的具体创建细节,只需通过工厂类获取所需对象。
(2)解耦客户端与具体产品:户端只需要通过工厂接口来创建对象,而无需直接实例化具体产品类,从而降低了客户端与具体产品的耦合度。
(3)提供灵活性和可扩展性:通过新增具体工厂类和产品类,可以方便地扩展工厂模式,增加新的产品类型,而不需要修改已有代码。
(4)代码可读性和可维护性:工厂模式提供了清晰的代码结构,使得代码更易于理解、维护和测试。
3、工厂模式的缺点
工厂模式存在以下几点不足:
(1)增加了系统的复杂性:引入工厂类和抽象产品类会增加系统的类和对象数量,增加了系统的复杂性。
(2)增加了开发的初期成本:工厂模式需要定义抽象工厂类和具体工厂类,以及相应的产品类,这在开发的初期会增加一定的开发成本。
(3)难以支持新种类的产品:如果需要添加新种类的产品,除了新增具体产品类外,还需要修改抽象工厂接口和所有具体工厂类的实现,可能会导致较大的修改范围。
总结:工厂模式在提供灵活性、可扩展性和代码可读性方面具有优点,但也会增加系统的复杂性和开发成本。在设计时需要权衡使用工厂模式的利弊,并根据具体情况选择是否使用该模式。
工厂模式包含很多,常见的有以下三种模式:
(1)简单工厂模式(Simple Factory Pattern):简单工厂模式通过一个工厂类来封装对象的创建逻辑,根据客户端传入的参数返回相应的产品对象。
(2)工厂方法模式(Factory Method Pattern):工厂方法模式将对象的创建延迟到子类中,每个具体子类都负责创建一个特定的产品对象,客户端通过调用工厂方法来获取所需的产品对象。
(3)抽象工厂模式(Abstract Factory Pattern):抽象工厂模式提供一个接口或抽象类,用于创建一系列相关或相互依赖的产品对象。具体的工厂类实现了该接口或继承了该抽象类,负责创建对应的产品系列。
简单工厂模式通过一个工厂类来封装对象的创建逻辑,根据客户端传入的参数返回相应的产品对象。
1、设计简单工厂类
(1)创建接口Shape
/**
*/
public interface Shape {
/**
* 绘制图形
*/
void draw();
}
(2)分别创建2个实现类Triangle,Square
绘制三角形实现类Triangle
/**
*/
public class Triangle implements Shape{
@Override
public void draw() {
System.out.println("绘制三角形");
}
}
绘制四边形实现类Square
/**
*/
public class Square implements Shape{
@Override
public void draw() {
System.out.println("绘制四边形");
}
}
(3)创建工厂类
/**
*/
public class ShapeFactory {
/**
* 创建对象
* @param type 对象类型
* @return
*/
public static Shape createBean(String type){
Shape shape;
switch (type.toLowerCase()){
case "triangle":
shape = new Triangle();
break;
case "square":
shape = new Square();
break;
default:
shape = null;
}
return shape;
}
}
(4)测试
创建测试类,并运行测试方法
public class ShapeFactoryTest {
@Test
public void test1(){
//创建Triangle对象
Shape triangle = ShapeFactory.createBean("triangle");
triangle.draw();
//创建Square对象
Shape square = ShapeFactory.createBean("square");
square.draw();
}
}
2、添加新的产品需求
如果需要增加新的功能,如绘制圆形,只需要创建一个Circle实现类,不需要修改原来代码,但是工厂类的代码需要修改,需要装配一个新产品功能。
(1)创建圆形实现类Circle
/**
*/
public class Circle implements Shape{
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
(2)修改工厂类的代码
switch (type.toLowerCase()){
case "triangle":
shape = new Triangle();
break;
case "square":
shape = new Square();
break;
case "circle":
shape = new Circle();
break;
default:
shape = null;
}
(3)测试
添加测试方法,并运行
@Test
public void test2(){
//创建Triangle对象
Shape triangle = ShapeFactory.createBean("triangle");
triangle.draw();
//创建circle对象
Shape circle = ShapeFactory.createBean("circle");
circle.draw();
}
工厂方法模式将对象的创建延迟到子类中,每个具体子类都负责创建一个特定的产品对象,客户端通过调用工厂方法来获取所需的产品对象。
接上例(已创建接口Shape,实现类Triangle、Square、Circle)
(1)创建工厂接口
/**
*/
public interface IShapeFactory {
/**
* 创建对象方法
* @return
*/
Shape createBean();
}
(2)分别创建Triangle、Square的工厂类
/**
*/
```java
public class TriangleFactory implements IShapeFactory{
@Override
public Shape createBean() {
return new Triangle();
}
}
/**
* 四边形工厂类
*/
```java
public class SquareFactory implements IShapeFactory{
@Override
public Shape createBean() {
return new Square();
}
}
(3)测试
@Test
public void test3(){
//创建Triangle对象
IShapeFactory triangleFactory = new TriangleFactory();
Shape triangle = triangleFactory.createBean();
triangle.draw();
//创建Square对象
IShapeFactory squareFactory = new SquareFactory();
Shape square = squareFactory.createBean();
square.draw();
}
(4)同理,如果添加绘制个一个圆形产品,只需要创建子类和子类的工厂类即可。
(代码略)
抽象工厂模式提供一个接口或抽象类,用于创建一系列相关或相互依赖的产品对象。具体的工厂类实现了该接口或继承了该抽象类,负责创建对应的产品系列。
接上例(已创建接口Shape,实现类Triangle、Square、Circle)
(1)创建抽象工厂类(接口),用于封装创建各产品的方法
/**
*/
public interface IAbstractFactory {
/**
* 创建Triangle对象
* @return
*/
Shape createTriangleBean();
Shape createSquareBean();
}
(2)创建抽象工厂类(接口)的实现类,具体创建子类对象
/**
*/
public class AbstractFactory implements IAbstractFactory{
@Override
public Shape createTriangleBean() {
return new Triangle();
}
@Override
public Shape createSquareBean() {
return new Square();
}
}
(3)测试
@Test
public void test4(){
//创建抽象工厂
IAbstractFactory beanFactory = new AbstractFactory();
//创建Triangle对象
Shape triangle = beanFactory.createTriangleBean();
triangle.draw();
//创建Square对象
Shape square = beanFactory.createSquareBean();
square.draw();
}
(4)思考,如果需要添加一个绘制圆形功能,如何实现?
总结:
(1)简单工厂模式
实现简单,适合创建单一类型的对象。将对象的实例化过程集中在工厂类中,客户端只需要与工厂进行交互,降低了客户端的复杂度。隐藏了对象的创建细节,提供了一种简单的方式来获取所需对象。
扩展性较差,对于新增产品类型需要修改工厂类的逻辑。违背了开闭原则,对于每个新增的产品类型都需要修改工厂类的代码,增加了耦合性。
(2)工厂方法模式
通过定义抽象工厂和具体工厂类,每个具体工厂负责创建特定的产品,可以灵活地扩展工厂和产品。符合开闭原则,新增产品类型只需添加相应的具体工厂即可,无需修改已有代码。
(3)抽象工厂模式
扩展新的产品族比较困难,需要修改抽象工厂接口和所有具体工厂的实现。但它提供了一种将相关产品组合成系列或簇的方式,可以在不修改已有代码的情况下引入新的产品系列。
代理模式通过引入一个代理对象来控制对原对象的访问。代理对象在客户端和目标对象之间充当中介,负责将客户端的请求转发给目标对象,同时可以在转发请求前后进行额外的处理(增加功能)。
在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
代理模式可以控制对象的访问和权限,可以为对象提供额外的功能,例如缓存和延迟加载,可以降低系统的耦合度,将访问控制与业务逻辑分离,例如用户下单与用户登录。代理模式解决的是在直接访问某些对象时可能遇到的问题,或需要在访问一个对象时进行一些控制或额外处理。
主要涉及到以下几个核心角色:
(1)抽象主题(Subject):定义了真实主题和代理主题的共同接口,这样在任何使用真实主题的地方都可以使用代理主题。
(2)真实主题(Real Subject):实现了抽象主题接口,是代理对象所代表的真实对象。客户端直接访问真实主题,但在某些情况下,可以通过代理主题来间接访问。
(3)代理(Proxy)主题:实现了抽象主题接口,并持有对真实主题的引用。代理主题通常在真实主题的基础上提供一些额外的功能,例如延迟加载、权限控制、日志记录等。
(4)客户端(Client):使用抽象主题接口来操作真实主题或代理主题,不需要知道具体是哪一个实现类。
代理模式有不同的形式,主要有三种:
(1)静态代理:静态代理是最基本的代理模式,它需要手动编写代理类。可以通过实现或继承相同的接口或父类,使得代理对象拥有与实际对象相同的方法和属性。代理对象在调用实际对象的方法时,可以在方法前或方法后添加一些额外的操作,以实现特定的功能。
(2)JDK动态代理:主要是基于反射,使用反射解析目标对象的属性、方法等,根据解析的内容生成代理类,就是在运行时生成一个代理对象,并将所有方法调用转发给我们指定的处理器。
(3)Cglib动态代理:基于目标对象创建子类的方式实现的,它可以在运行时动态修改目标对象的字节码,从而达到代理的目的,可以代理没有任何接口的类。
静态代理是最基本的代理模式,它需要手动编写代理类。可以通过实现或继承相同的接口或父类,使得代理对象拥有与实际对象相同的方法和属性。代理对象在调用实际对象的方法时,可以在方法前或方法后添加一些额外的操作,以实现特定的功能。
1、案例分析
(1)案例需求:需要保存用户信息,同时在保存用户信息之后记录保存日志。
(2)案例分析:为了降低程序的耦合性,可以将逻辑与访问控制分离。本案例主要功能是保存用户信息,记录日志操作成功与否不影响到用户信息的保存,可以将记录日志功能作为辅助功能(增强功能)。
(3)实现步骤如下图所示:
2、案例实现
(1)创建用户接口
/**
*/
public interface IUser {
void save();
}
(2)创建日志类
public class Log {
public static void doLog(){
System.out.println("记录日志...");
}
}
(3)创建用户类
public class UserImpl implements IUser{
@Override
public void save() {
System.out.println("用户信息已存档");
}
}
(4)创建用户代理类
public class UserProxy implements IUser{
private UserImpl user = new UserImpl();
@Override
public void save() {
user.save();// 保存用户信息
Log.doLog();// 记录日志
}
}
(5)测试,创建客户端类(用测试类代替)
public class ProxyTest {
@Test
public void test1(){
IUser proxy= new UserProxy();
proxy.save();
}
}
从上例可以看出:如果需要增强功能,不需要改变目标对象类的代码,只是增加一个代理类即可。假如,用户类添加一个修改用户信息的功能,静态代理模式就会造成系统设计中类的数量增加,进而增加了系统的复杂度。
JDK动态代理主要是基于反射,使用反射解析目标对象的属性、方法等根据解析的内容生成代理类,就是在运行时生成一个代理对象,并将所有方法调用转发给我们指定的处理器。
①JDK动态代理的调用处理程序需要事先实现InvocationHandler接口,重写invoke方法,实现接口的invoke方法三个参数分别是:
通过反射,在invoke方法里面调用被代理对象的方法(method. invoke (target, args)),并在该行代码前后增加功能。
②调用Proxy的静态方法newProxyInstance创建代理对象。
newProxyInstance方法有三个参数:
(1)创建动态代理类
public class UserHandler implements InvocationHandler {
private Object target; //代理接口
public UserHandler(){}
public UserHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object o = method.invoke(target,args);
Log.doLog();
return o;
}
/**
* 创建代理对象
* @return
*/
public Object getProxy(){
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),this);
}
}
(2)测试,创建客户端
这里必须强转成接口类型,方便代理对象调用方法。
@Test
public void test2(){
// 创建被代理对象
IUser user = new UserImpl();
// 创建增强类
UserHandler userHandler = new UserHandler(user);
// 创建代理对象
IUser proxy = (IUser) userHandler.getProxy();
// 调用方法
proxy.save();
}
JDK动态代理也可以不实现InvocationHandler接口,使用内部类的方式实现InvocationHandler接口。
Cglib动态代理是基于目标对象创建子类的方式实现的,CGLIB通过动态生成一个需要被代理类的子类 (即被代理类作为父类),该子类重写被代理类的所有不是 final修饰的方法,并在子类中采用方法拦截的技术拦截父类所有的方法调用。它可以在运行时动态修改目标对象的字节码,从而达到代理的目的,可以代理没有任何接口的类。
①Cglib动态代理的调用处理程序必须事先实现MethodInterceptor拦截器接口,重写intercept方法,实现接口的intercept方法四个参数分别是:
②创建Enhancer ,设置要被代理的类和调用方法时触发的拦截器。Enhancer类是CGLIB中的一个类,既能够代理普通的class,也能够代理接口(JDK中的Proxy只能代理接口)。Enhancer类主要用到的方法:
(1)使用CGLIB动态代理,首先需要导入相关的Jar包
cglib
cglib
3.3.0
(2)创建代理类
public class UserInterceptor implements MethodInterceptor{
private Object target;
public UserInterceptor(Object target){
this.target = target;
}
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object o = methodProxy.invokeSuper(object, objects);
Log.doLog();
return o;
}
public Object getProxy(){
// 创建增强类
Enhancer enhancer = new Enhancer();
// 创建被代理对象,加载字节码
enhancer.setSuperclass(target.getClass());
// 生成回调,将增强类引入到代理类中
enhancer.setCallback(this);
// 创建代理对象
return enhancer.create();
}
}
(3)创建客户端测试
@Test
public void test3(){
// 创建被代理对象
UserImpl user = new UserImpl();
// 创建增强类
UserInterceptor userInterceptor = new UserInterceptor(user);
// 创建代理对象
UserImpl proxy = (UserImpl) userInterceptor.getProxy();
proxy.save();
}
总结:
(1)静态代理由于在程序运行前就已经确定代理类,因此它可以在编译时进行类型检查和安全管理。由于每个代理类只能代理一个接口,因此如果需要代理多个类,就需要为每个类写一个代理类,代码可能会非常冗余。
(2)JDK动态代理可以在运行时动态创建代理类,可以代理多个接口,从而减少代码冗余。JDK动态代理要求目标类实现一个接口,代理对象实现了与目标类相同的接口。在调用代理方法时,通过反射调用目标方法,因此会引入一定的性能开销。
(3)CGLIB动态代理在没有接口的情况可以使用(有接口也可以使用)。通过Enhancer类创建代理对象, 将目标对象设置为父类。CGLIB动态代理直接调用目标方法,省去了反射的开销,因此在性能上通常比JDK动态代理略快。但是对于final类、private方法和static方法等无法CGLIB无法生成代理类。