1.单例设计模式
1.作用
单例设计模式的意思是,一个类只允许创建一个实例,也就是一个对象,对象在堆内存中只能开辟一个空间。
2.实现步骤
- 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
- 在该类内部产生一个唯一的实例化对象,并且将其封装为private static final类型的成员变量。
- 定义一个静态方法返回这个唯一对象。
3. 单例设计模式的类型
根据实例化对象的时机单例设计模式又分为以下两种:
-
饿汉单例设计模式
饿汉单例设计模式就是使用类的时候已经将对象创建完毕,不管以后会不会使用到该实例化对象,先创建了再说。很着急的样子,故被称为“饿汉模式”。
-
懒汉单例设计模式
懒汉单例设计模式就是调用getInstance()方法时实例才被创建,先不急着实例化出对象,等要用的时候才实例化出对象。不着急,故称为“懒汉模式”。
4.代码实现
-
饿汉式
-
比较饥饿,着急创建出来唯一的一个对象
/* 单例设计模式 比较饥饿,着急创建出来唯一的一个对象 */ public class Single { //1、私有化构造函数 private Single(){} //2、创建一个本类的对象 private static final Single s=new Single(); //3、定义一个方法返回本类的对象 public static Single getInstance() { return s; } //测试方法 public void test() { System.out.println("测试方法"); } } class SingleDemo { public static void main(String[] args) { /* 要想获取到Single类的对象,调用getInstance方法 既然不能通过对象来调用,那么只能通过类名来调用 如果要想通过类名来调用方法,那么被调用的方法必须用static来修饰 */ Single s1=Single.getInstance(); s1.test(); //Single s2=Single.getInstance(); } }
-
-
懒汉式
-
比较懒惰,什么时候用对象,就什么时候创建
public class Single { //比较懒惰,什么时候用对象,就什么时候创建 //构造方法私有化 private Single(){ } //创建一个唯一的对象 private static Single s = null; //获取对象的方法 public static synchronized Single getSingle(){ //如果值是null,说明没有创建过这个对象 if(s == null){//t1 t2 s = new Single(); } return s; } }
-
注意:懒汉单例设计模式在多线程环境下可能会实例化出多个对象,不能保证单例的状态,所以加上关键字:synchronized,保证其同步安全。
5. 小结
单例模式可以保证系统中一个类只有一个对象实例。
实现单例模式的步骤:
- 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
- 在该类内部产生一个唯一的实例化对象,并且将其封装为private static final类型的成员变量。
- 定义一个静态方法返回这个唯一对象。
2.多例设计模式
多例模式,是一种常用的软件设计模式。通过多例模式可以保证系统中,应用该模式的类有固定数量的实例。多例类要自我创建并管理自己的实例,还要向外界提供获取本类实例的方法。
-
作用
一个类可以创建多个对象,有多个实例。
-
实现步骤
1.创建一个类, 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
2.在类中定义该类被创建的总数量
3.在类中定义存放类实例的list集合
4.在类中提供静态代码块,在静态代码块中创建类的实例
5.提供获取类实例的静态方法
-
代码演示
-
比如一个类只允许创建3个对象。
public class Person { //这个类一共创建3个对象 //构造方法私有化 private Person(){} //定义集合用于保存多个对象 private static ArrayList<Person> list = new ArrayList<>(); //静态代码块,只会执行一次,且是在这个类最开始最先执行 static{ //创建3个对象放在集合里 for (int i = 0; i < 3; i++) { list.add(new Person()); } } //定义供外界访问的获取对象的方法 public static Person getPerson(){ //随机一个对象返回给调用者 //创建随机对象 Random r = new Random(); //获取索引 int i = r.nextInt(3); //根据索引从集合中获取对象 Person person = list.get(i); //返回给调用者 return person; } } public class Demo { public static void main(String[] args) { //获取10次,但是获取到的其实就是3个对象 for (int i = 0; i < 10; i++) { Person person = Person.getPerson(); System.out.println(person); } } }
-
3.动态代理
1.动态代理介绍和引入
-
代理模式概念
为什么要有“代理”?生活中就有很多例子,例如委托业务等等,代理就是被代理者没有能力或者不愿意去完成某件事情,需要找个人代替自己去完成这件事,这才是“代理”存在的原因。例如,我现在需要出国,但是我不愿意自己去办签证、预定机票和酒店(觉得麻烦 ,那么就可以找旅行社去帮我办,这时候旅行社就是代理,而我自己就是被代理了。
在我们的代码中,假如有以下业务情景:
用户登录到我们的系统后,我们的系统会为其产生一个ArrayList集合对象,内部存储了一些用户信息,而后,这个对象需要被传给后面的很多其它对象,但要求其它对象不能对这个ArrayList对象执行添加、删除、修改操作,只能get()获取元素。那么为了防止后面的对象对集合对象进行添加、修改、删除操作,我们应该怎样办呢?
要想实现这种要求,方案有很多种。"代理模式"就是其中的一种,而且是非常合适的一种。
-
动态代理概念
动态代理简单来说是:拦截对真实对象(被代理对象)方法的直接访问,增强真实对象方法的功能
动态代理详细来说是:代理类在程序运行时创建的代理对象被称为动态代理,也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。也就是说你想获取哪个对象的代理,动态代理就会动态的为你生成这个对象的代理对象。动态代理可以对被代理对象的方法进行增强,可以在不修改方法源码的情况下,增强被代理对象方法的功能,在方法执行前后做任何你想做的事情。动态代理技术都是在框架中使用居多,例如:Struts1、Struts2、Spring和Hibernate等后期学的一些主流框架技术中都使用了动态代理技术。
-
jdk的动态代理使用前提
要使用动态代理,代理对象和被代理的对象所属类必须实现共同接口。
-
演示Java已经实现的代理模式的思想,ArrayList使用工具类的演示:
- Collections工具类有一个unmodifiableList()方法,可以传入被代理对象,返回一个代理对象,这个返回的代理对象不能调用增删改方法。
static <T> List<T> unmodifiableList(List<? extends T> list) 参数:list是传入的被代理对象集合 返回值:返回的是代理对象集合,不能对集合进行增删改,如果增删改将导致抛出 UnsupportedOperationException:不支持操作异常 说明: unmodifiableList作用:传递List接口,方法内部对List接口进行代理,返回一个被代理后的List接口 对List进行代理之后,调用List接口的方法会被拦截: 如果使用的size,get方法,没有对集合进行修改,则允许执行 如果使用的add,remove,set方法,对集合进行了修改,则抛出运行时异常
代码演示:
public class Demo02 { public static void main(String[] args) { //创建集合 ArrayList<String> list = new ArrayList<>(); list.add("柳岩"); list.add("老王"); list.add("石原里美"); //让这个集合不能对内容进行修改,不能调用:1) 添add 2) 删remove 3) 改set的方法 List<String> list2 = Collections.unmodifiableList(list); //list2此时是一个代理对象,他这个对象不允许调用增删改方法 //unmodifiableList这个方法返回的list2是一个代理对象,该方法内部使用的就是代理模式的思想,我们可以使用动态代理来模拟该方法内部原理 // list2.add("李四");//报异常 // list2.remove("柳岩");//报异常 String s = list2.get(1); System.out.println(s); } }
说明:unmodifiableList这个方法返回的list2是一个代理对象,该方法内部使用的就是代理模式的思想,我们可以使用动态代理来模拟该方法内部原理
2.动态代理完成不允许对集合增删改
-
测试类代码
public class Demo03 { public static void main(String[] args) { //创建集合 ArrayList<String> list = new ArrayList<>(); list.add("柳岩"); list.add("老王"); list.add("石原里美"); //让这个集合不能对内容进行修改,不能调用:1) 添add 2) 删remove 3) 改set的方法 //List<String> list2 = Collections.unmodifiableList(list); //使用我们自己定义的类和方法以及动态代理技术模拟:让这个集合不能对内容进行修改,不能调用:1) 添add 2) 删remove 3) 改set的方法 List<String> list2 = MyColl.getList(list); //list2此时是一个代理对象,他这个对象不允许调用增删改方法 //unmodifiableList这个方法返回的list2是一个代理对象,该方法内部使用的就是代理模式的思想,我们可以使用动态代理来模拟该方法内部原理 // list2.add("李四");//报异常 // list2.remove("柳岩");//报异常 //list2.set(2,"老王"); String s = list2.get(1); System.out.println(s); int size = list.size(); System.out.println(size); } }
-
MyColl类:动态代理实现对代理对象的增强
那么如何实现动态代理呢?
获取某个被代理类的代理类对象这时必须使用Java中的Proxy这个类完成。
注:在Java中当某个类需要被代理的时候,要求这个类中的被代理的方法必须抽取到一个接口中,然后这个类需要实现那个接口,只有在这个接口中的方法,代理类才能代理它。
在Proxy类中提供了一个方法,可以实现:
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 返回一个指定接口的代理类实例或者对象。
- loader:类加载器。一个类的字节码文件不可能直接就出现在方法区中,必须通过一个类的类加载器 将 类的字节码文件加载到方法区中,所以需要一个类加载器。
那么我们如何确定这个类加载器呢?
说明:这个类加载器可以随便给,但是一般情况下我们给被代理的对象的类加载器即可。
举例: 创建被代理对象list的对象: ClassLoader loader = list.getClass().getClassLoader(); 有了这个加 载器了,这样就可以在方法区中给我们加载出来一个代理类的Class对象了。
- Class[] interfaces:被代理对象的所有接口的数组,说明把接口的字节码文件放在这里即可,因为接口的字节码文件中就存在函数。
注:Class[] getInterfaces() 通过字节码文件对象,获取其所有接口的数组。
代码如:Class[] interfaces = list.getClass().getInterfaces();
3) InvocationHandler h:调用处理器 ,是一个接口。
所以我们需要自己定义一个类来实现这个接口,或者我们也可以使用匿名内部类来实现。
代码如:
InvocationHandler h = new InvocationHandler()
{
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
}
};
说明:
A:invoke函数属于调用处理器InvocationHandler 接口中的函数,所以实现类必须复写这个函数;
参数:
1)proxy:就是代理对象本身。这里不用管;
2)method:当前代理对象正在调用的方法;
3)args :当前正在调用的方法的实际参数;
代码实现:
@SuppressWarnings("all")
public class MyColl {
/*
getList方法:
参数代表的是被代理对象
返回值代表是代理对象(中介)
*/
public static List<String> getList(List<String> list) {
/*
* 动态代理演示
* 动态代理:就是在程序运行的过程中,动态的生成一个类,这个类要代理目标业务对象,并且可以动态生成这个代理类的对象。
* 如何实现:
* Proxy类中提供了一个方法,可以实现:
* static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
* 返回一个指定接口的代理类实例
* 参数:
* loader:类加载器,一个类的字节码文件不可能直接就出现在方法区中,必须通过类加载器加载,所以需要一个类加载器。
* 一般给被代理的对象的类加载器
* 注意:这里要保证被代理对象和代理对象所属类的类加载器要一样。而这里都是list,索引类加载器都是根类加载器
* Class[] interfaces:被代理对象的所有接口的数组,说明把接口的字节码文件放在这里即可,因为接口的字节码文件中就存在函数。
* 通过获取到接口获取接口中的方法,这样后面才可以对方法进行增强
Class[] getInterfaces() 通过字节码文件对象,获取其所有接口的数组
* InvocationHandler h:调用处理器 ,是一个接口,所以我们需要自己定义一个类来实现这个接口。
* 返回值:返回的是生成的代理对象
*/
//获取类加载器
ClassLoader classLoader = list.getClass().getClassLoader();
//获取被代理对象的接口
Class<?>[] interfaces = list.getClass().getInterfaces();
//调用处理器 修改代理对象拦截被代理对象的方法
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
/*
invoke()方法:
在使用代理对象调用任何方法时,都会执行到invoke方法中,在invoke方法中给出处理方式,无论是add,remove,get都是这样
参数:
Object proxy 代理对象(在这里没用)
Method method 当前代理对象正在被调用的方法的Method对象
Object[] args 执行方法时传入的实际参数
返回值:
Object 实际执行完方法后得到的返回值.举例:String s = list2.get(1); 那么s接收的就是这个null
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
list2.add("李四") :
方法名:add
参数:李四
list2.set(2,"老王");
方法名:set
参数:[2, 老王]
*/
//输出被调用方法名
System.out.println(method.getName());
//输出被调用方法的参数
System.out.println(Arrays.toString(args));
//这个返回值会返回给调用方法的调用者
return null;
}
};
//动态代理
List<String> list2 = (List<String>) Proxy.newProxyInstance(classLoader, interfaces,invocationHandler );
//返回代理对象
return list2;
}
}
- 流程图
-
最终代码实现:不允许集合增删改
package com.itheima.sh.demo_04; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.List; @SuppressWarnings("all") public class MyColl { /* getList方法: 参数代表的是被代理对象 返回值代表是代理对象(中介) */ public static List<String> getList(List<String> list) { /* * 动态代理演示 * 动态代理:就是在程序运行的过程中,动态的生成一个类,这个类要代理目标业务对象,并且可以动态生成这个代理类的对象。 * 如何实现: * Proxy类中提供了一个方法,可以实现: * static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) * 返回一个指定接口的代理类实例 * 参数: * loader:类加载器,一个类的字节码文件不可能直接就出现在方法区中,必须通过类加载器加载,所以需要一个类加载器。 * 一般给被代理的对象的类加载器 * 注意:这里要保证被代理对象和代理对象所属类的类加载器要一样。而这里都是list,索引类加载器都是根类加载器 * Class[] interfaces:被代理对象的所有接口的数组,说明把接口的字节码文件放在这里即可,因为接口的字节码文件中就存在函数。 * 通过获取到接口获取接口中的方法,这样后面才可以对方法进行增强 Class[] getInterfaces() 通过字节码文件对象,获取其所有接口的数组 * InvocationHandler h:调用处理器 ,是一个接口,所以我们需要自己定义一个类来实现这个接口。 * 返回值:返回的是生成的代理对象 */ //获取类加载器 ClassLoader classLoader = list.getClass().getClassLoader(); //获取被代理对象的接口 Class<?>[] interfaces = list.getClass().getInterfaces(); //调用处理器 修改代理对象拦截被代理对象的方法 InvocationHandler invocationHandler = new InvocationHandler() { @Override /* invoke()方法: 在使用代理对象调用任何方法时,都会执行到invoke方法中,在invoke方法中给出处理方式,无论是add,remove,get都是这样 参数: Object proxy 代理对象(在这里没用) Method method 当前代理对象正在被调用的方法的Method对象 Object[] args 执行方法时传入的实际参数 返回值: Object 实际执行完方法后得到的返回值.举例:String s = list2.get(1); 那么s接收的就是这个null */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /* list2.add("李四") : 方法名:add 参数:李四 list2.set(2,"老王"); 方法名:set 参数:[2, 老王] */ //输出被调用方法名 // System.out.println(method.getName()); //输出被调用方法的参数 // System.out.println(Arrays.toString(args)); //目的是不让调用增删改的方法 //判断方法名如果是增删改就产生异常 if(method.getName().equals("add") || method.getName().equals("remove") || method.getName().equals("set")){ //产生异常 throw new RuntimeException("不能调用" + method.getName() + "方法"); } //判断方法名如果是其他方法就正常执行 //method是一个方法 //执行方法 /* invoke(Object o,Object... obj) o 代表的是执行的哪个对象 obj 代表的方法的实际参数 */ //list表示被代理对象,args表示被执行方法的实参 result 表示执行的方法返回的结果 //例如:调用的方法:String s = list2.get(1); ---》args表示1 将获取的结果result给s Object result = method.invoke(list, args); //这个返回值会返回给调用方法的调用者 return result; } }; //动态代理 List<String> list2 = (List<String>) Proxy.newProxyInstance(classLoader, interfaces,invocationHandler ); //返回代理对象 return list2; } }
注意:
上述代码生成的代理类对象必须使用List接口接收,不能使用ArrayList类接收,因为ArrayList和代理类没有关系,只是实现了共同父接口List.
//list2表示代理类对象
List<String> list2 = (List<String>) Proxy.newProxyInstance(classLoader, interfaces,invocationHandler );//正确
//list2表示代理类对象
ArrayList<String> list2 = (ArrayList<String>) Proxy.newProxyInstance(classLoader, interfaces,invocationHandler );//错误
3.动态代理完成:集合只允许添加四个字的字符串
-
测试类
public class Demo04 { public static void main(String[] args) { //创建集合 ArrayList<String> list = new ArrayList<>(); //添加方法 list.add("石原里美"); list.add("新垣结衣"); //用动态代理的方式返回一个代理对象,要求代理对象只能添加长度为4的字符串 List<String> list2 = MyColl2.getList(list); //添加 list2.add("桥本环奈"); //list2.add("柳岩"); System.out.println(list2); } }
-
动态代理生成的代理类对象
package com.itheima.sh.demo_04; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.List; @SuppressWarnings("all") public class MyColl2 { public static List<String> getList(ArrayList<String> list){ //动态代理 //返回值地方只能接口不能写具体类型 List<String> list2 = (List<String>) Proxy.newProxyInstance(list.getClass().getClassLoader(), list.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //对其他方法没有限制 //对添加方法 只允许添加长度为4的字符串 if(method.getName().equals("add")){ //判断参数的长度是否为4 String s = (String) args[0]; if(s.length() != 4){ //产生异常 throw new RuntimeException("添加的字符串长度必须是4"); } } //如果是其他情况就要正常执行 Object result = method.invoke(list, args); return result; } }); return list2; } }
4. 总结
-
动态代理非常的灵活,可以为任意的接口实现类对象做代理
-
动态代理可以为被代理对象的所有接口的所有方法做代理,动态代理可以在不改变方法源码的情况下,实现对方法功能的增强,
-
动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。
-
动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。
-
动态代理同时也提高了开发效率。
-
缺点:只能针对接口的实现类做代理对象,普通类是不能做代理对象的。
4.工厂设计模式
-
介绍
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。之前我们创建类对象时, 都是使用new 对象的形式创建, 除new 对象方式以外, 工厂模式也可以创建对象.
-
没有使用工厂设计模式创建对象即以前创建对象方式的问题
-
假设定义三个车类 BaoMa BenChi Wulin ,然后在测试类创建三个类的对象
-
代码演示:
public class BaoMa { } public class BenChi { } public class AoDi { } public class Test01 { public static void main(String[] args) { //创建三个类的对象 BaoMa baoMa = new BaoMa(); BenChi benChi = new BenChi(); AoDi aoDi = new AoDi(); } }
问题说明:
1.假设当前项目下具有很多个测试类,都要使用这三个类,然后我想统计每个类的对象有多少个,那么 这样统计就会很麻烦
2.测试类和这三个类还耦合在一起了,我们应该降低耦合
-
-
使用工厂设计模式创建对象
- 实现步骤
- 编写一个Car接口
- 编写一个BaoMa类实现Car接口
- 编写一个Benchi类实现Car接口
- 编写一个AoDi类实现Car接口
- 提供一个CarFactory(汽车工厂),定义静态方法用于生产汽车对象
- 定义CarFactoryTest测试汽车工厂
- 实现步骤
-
代码演示
package com.itheima.sh.demo_04; /* 汽车工厂类 */ public class CarFactory { //工厂专门用来创建汽车对象 //name表示调用该方法时指定的车类型 public static Car getCar(String name){ if(name.equals("BaoMa")){ return new BaoMa(); }else if(name.equals("BenChi")){ return new BenChi(); }else if(name.equals("AoDi")){ return new AoDi(); } return null; } } //汽车 public interface Car { } public class BaoMa implements Car{ //宝马 } public class BenChi implements Car{ //奔驰 } public class AoDi implements Car{ //奥迪 } //测试类 public class Test01 { public static void main(String[] args) { //创建三个类的对象 // BaoMa baoMa = new BaoMa(); // BenChi benChi = new BenChi(); // AoDi aoDi = new AoDi(); //--------------------------------- //使用工厂创建对象 Car baoMa = CarFactory.getCar("BaoMa"); System.out.println(baoMa); Car benChi = CarFactory.getCar("BenChi"); System.out.println(benChi); Car AoDi = CarFactory.getCar("AoDi"); System.out.println(AoDi); } }
小结:
-
工厂模式的存在可以改变创建类的方式
-
方便管理对象
-
降低类与类之间的耦合,调用工厂类中的方法直接指定字符串即可,创建对象的事情都交给工厂类
5.Lombok【自学扩展】
1.lombok介绍
Lombok能通过注解的方式,在编译时自动为属性生成构造器、getter/setter、equals、hashcode、toString方法。出现的神奇就是在源码中没有getter和setter方法,但是在编译生成的字节码文件中有getter和setter方法。这样就省去了手动重建这些代码的麻烦,使代码看起来更简洁些。
2.lombok使用
1. 添加lombok的jar包:
将lombok.jar(本例使用版本:1.18.10),添加到模块目录下,并添加到ClassPath
2. 为IDEA添加lombok插件(连接网络使用)
- 第一步
- 第二步:
- 第三步:
注意:一定勾选上Enable annotation processing 按钮才可以使用lombok,否则不能使用。
- 第四步:安装完毕后,重启IDEA。
3.新建一个类:Student
public class Student {
private String name;
private int age;
}
4.lombok常用注解
-
@Getter和@Setter
- 作用:生成成员变量的get和set方法。
- 写在成员变量上,指对当前成员变量有效。
- 写在类上,对所有成员变量有效。
- 注意:静态成员变量无效。
-
@ToString:
- 作用:生成toString()方法。
- 该注解只能写在类上。
-
@NoArgsConstructor和@AllArgsConstructor
- @NoArgsConstructor:无参数构造方法。
- @AllArgsConstructor:满参数构造方法。
- 注解只能写在类上。
-
@EqualsAndHashCode
- 作用:生成hashCode()和equals()方法。
- 注解只能写在类上。
-
@Data
-
作用: 生成setter/getter、equals、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。
-
注解只能写在类上。
-
-
编写代码
package com.itheima.sh.demo_04; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data // setter/getter、equals、hashCode、toString方法 @NoArgsConstructor//无参构造 @AllArgsConstructor//满参构造 public class Student { private String name; private int age; } public class Test01 { public static void main(String[] args) { Student s = new Student(); Student s1 = new Student("张三",20); System.out.println("s = " + s); // s = Student(name=null, age=0) System.out.println("s1 = " + s1); // s1 = Student(name=张三, age=20) System.out.println(s1.getName()); // 张三 } }