尚硅谷Java技术之北京高频面试题
版本:V6.0
尚硅谷Java技术中心
时间复杂度和空间复杂度一般是针对算法而言,是衡量一个算法是否高效的重要标准。先纠正一个误区,时间复杂度并不是算法执行的时间,再纠正一个误区,算法不单单指冒泡排序之类的,一个循环甚至是一个判断都可以称之为算法。其实理解起来并不冲突,八大排序甚至更多的算法本质上也是通过各种循环判断来实现的。
时间复杂度:指算法语句的执行次数。O(1),O(n),O(logn),O(n2)
空间复杂度:就是一个算法在运行过程中临时占用的存储空间大小,换句话说就是被创建次数最多的变量,它被创建了多少次,那么这个算法的空间复杂度就是多少。有个规律,如果算法语句中就有创建对象,那么这个算法的时间复杂度和空间复杂度一般一致,很好理解,算法语句被执行了多少次就创建了多少对象。
数组:相同数据类型的元素按一定顺序排列的集合,就是把有限个类型相同的变量用一个名字命名,然后用编号区分他们的变量的集合,这个名字称为数组名,编号称为下标。
数组的特性:
1.数组必须先定义固定长度,不能适应数据动态增减。
2.当数据增加时,可能超出原先定义的元素个数,当数据减少时,造成内存浪费。
3.数组查询比较方便,根据下标就可以直接找到元素,时间复杂度O(1);增加和删除比较复杂,需要移动操作数所在位置后的所有数据,时间复杂度为O(N)。
链表:是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表的特性:
1.链表动态进行存储分配,可适应数据动态增减。
2.插入、删除数据比较方便,时间复杂度O(1);查询必须从头开始找起,十分麻烦,时间复杂度O(N)。
常见的链表:
1.单链表:通常链表每一个元素都要保存一个指向下一个元素的指针。
2.双链表:每个元素既要保存到下一个元素的指针,还要保存一个上一个元素的指针。
3.循环链表:在最后一个元素中下一个元素指针指向首元素。
应用:如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反,如果需要经常插入和删除元素就需要用链表数据结构了。
四种遍历概念
先序遍历:先访问根节点,再访问左子树,最后访问右子树。
后序遍历:先左子树,再右子树,最后根节点。
中序遍历:先左子树,再根节点,最后右子树。
层序遍历:每一层从左到右访问每一个节点。
每一个子树遍历时依然按照此时的遍历顺序。可以采用递归实现遍历。
算法描述:
比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
重复步骤1~3,直到排序完成。
如果两个元素相等,不会再交换位置,所以冒泡排序是一种稳定排序算法。
代码实现:
1.package com.atguigu.interview.chapter02;
2.
3./**
4. * @author atguigu
5. * @since 2019/7/22
6. * 冒泡排序
7. */
8.public class BubbleSort {
9.
10. /**
11. * @param data 被排序的数组
12. */
13. public static void bubbleSort(int[] data) {
14.
15. int arrayLength = data.length;
16.
17. for (int i = 1; i < arrayLength; i++) {//第i次排序
18.
19. for (int j = 0; j < arrayLength - i; j++) {//从索引为j的数开始
20. if (data[j] > data[j + 1]) { //相邻元素两两对比
21. int temp = data[j + 1]; // 元素交换
22. data[j + 1] = data[j];
23. data[j] = temp;
24. }
25. }
26.
27. System.out.println("第" + i + "次排序:\n" + java.util.Arrays.toString(data));
28. }
29. }
30.
31. public static void main(String[] args) {
32.
33. int[] data = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
34.
35. System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
36.
37. bubbleSort(data);
38.
39. System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
40. }
41.}
算法描述:
使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
key值的选取可以有多种形式,例如中间数或者随机数,分别会对算法的复杂度产生不同的影响。
代码实现:
1.package com.atguigu.interview.chapter02;
2.
3./**
4. * @author atguigu
5. * @since 2019/7/22
6. * 快速排序
7. */
8.public class QuickSort {
9.
10. public static void quickSort(int[] data, int low, int high) {
11. int i, j, temp, t;
12. if (low > high) {
13. return;
14. }
15. i = low;
16. j = high;
17. //temp就是基准位
18. temp = data[low];
19. System.out.println("基准位:" + temp);
20.
21. while (i < j) {
22. //先看右边,依次往左递减
23. while (temp <= data[j] && i < j) {
24. j--;
25. }
26. //再看左边,依次往右递增
27. while (temp >= data[i] && i < j) {
28. i++;
29. }
30. //如果满足条件则交换
31. if (i < j) {
32. System.out.println("交换:" + data[i] + "和" + data[j]);
33. t = data[j];
34. data[j] = data[i];
35. data[i] = t;
36. System.out.println(java.util.Arrays.toString(data));
37.
38. }
39. }
40. //最后将基准位与i和j相等位置的数字交换
41. System.out.println("基准位" + temp + "和i、j相遇的位置" + data[i] + "交换");
42. data[low] = data[i];
43. data[i] = temp;
44. System.out.println(java.util.Arrays.toString(data));
45.
46. //递归调用左半数组
47. quickSort(data, low, j - 1);
48. //递归调用右半数组
49. quickSort(data, j + 1, high);
50. }
51.
52.
53. public static void main(String[] args) {
54.
55. int[] data = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
56.
57. System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
58.
59. quickSort(data, 0, data.length - 1);
60.
61. System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
62. }
63.}
快速排序详细参考:
https://blog.csdn.net/shujuelin/article/details/82423852
算法描述:
二分查找也称折半查找,它是一种效率较高的查找方法,要求列表中的元素首先要进行有序排列。
首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;
否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。
重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
代码实现:
1.package com.atguigu.interview.chapter02;
2.
3./**
4. * @author atguigu
5. * @since 2019/7/22
6. */
7.public class BinarySearch {
8.
9.
10. /**
11. * 二分查找 时间复杂度O(log2n);空间复杂度O(1)
12. *
13. * @param arr 被查找的数组
14. * @param left
15. * @param right
16. * @param findVal
17. * @return 返回元素的索引
18. */
19. public static int binarySearch(int[] arr, int left, int right, int findVal) {
20.
21. if (left > right) {//递归退出条件,找不到,返回-1
22. return -1;
23. }
24.
25. int midIndex = (left + right) / 2;
26.
27. if (findVal < arr[midIndex]) {//向左递归查找
28. return binarySearch(arr, left, midIndex, findVal);
29. } else if (findVal > arr[midIndex]) {//向右递归查找
30. return binarySearch(arr, midIndex, right, findVal);
31. } else {
32. return midIndex;
33. }
34. }
35.
36. public static void main(String[] args){
37.
38. //注意:需要对已排序的数组进行二分查找
39. int[] data = {-49, -30, -16, 9, 21, 21, 23, 30, 30};
40. int i = binarySearch(data, 0, data.length, 21);
41. System.out.println(i);
42. }
43.}
拓展需求:
当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到。
代码实现:
1.package com.atguigu.interview.chapter02;
2.
3.import java.util.ArrayList;
4.import java.util.List;
5.
6./**
7. * @author atguigu
8. * @since 2019/7/22
9. */
10.public class BinarySearch2 {
11.
12. /**
13. * {1, 8, 10, 89, 1000, 1000, 1234}
14. * 一个有序数组中,有多个相同的数值,如何将所有的数值都查找到,比如这里的 1000.
15. * 分析:
16. * 1. 返回的结果是一个列表 list
17. * 2. 在找到结果时,向左边扫描,向右边扫描 [条件]
18. * 3. 找到结果后,就加入到ArrayBuffer
19. *
20. * @return
21. */
22. public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal) {
23.
24. //找不到条件?
25. List<Integer> list = new ArrayList<>();
26.
27. if (left > right) {//递归退出条件,找不到,返回-1
28. return list;
29. }
30.
31. int midIndex = (left + right) / 2;
32. int midVal = arr[midIndex];
33. if (findVal < midVal) {//向左递归查找
34. return binarySearch2(arr, left, midIndex - 1, findVal);
35. } else if (findVal > midVal) { //向右递归查找
36. return binarySearch2(arr, midIndex + 1, right, findVal);
37. } else {
38. System.out.println("midIndex=" + midIndex);
39.
40. //向左边扫描
41. int temp = midIndex - 1;
42. while (true) {
43. if (temp < 0 || arr[temp] != findVal) {
44. break;
45. }
46. if (arr[temp] == findVal) {
47. list.add(temp);
48. }
49. temp -= 1;
50. }
51.
52. //将中间这个索引加入
53. list.add(midIndex);
54.
55. //向右边扫描
56. temp = midIndex + 1;
57. while (true) {
58. if (temp > arr.length - 1 || arr[temp] != findVal) {
59. break;
60. }
61. if (arr[temp] == findVal) {
62. list.add(temp);
63. }
64. temp += 1;
65. }
66. return list;
67. }
68. }
69.
70. public static void main(String[] args){
71.
72. //注意:需要对已排序的数组进行二分查找
73. int[] data = {1, 8, 10, 89, 1000, 1000, 1234};
74. List<Integer> list = binarySearch2(data, 0, data.length, 1000);
75. System.out.println(list);
76. }
77.}
Java 中一般认为有 23种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。下面列出了所有的设计模式。需要掌握的设计模式我单独列出来了,当然能掌握的越多越好。
总体来说设计模式分为三大类:
创建型模式,共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共7种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共11种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。
单例类只能有一个实例。
单例类必须自己创建自己的唯一实例。
单例类必须给所有其他对象提供这一实例。
单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。
构造私有
以静态方法或者枚举返回实例
确保实例只有一个,尤其是多线程环境
确保反序列化时不会重新构建对象
(1)饿汉式(立即加载):
饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。
Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,会使Java单例实现失效)
1.package com.atguigu.interview.chapter02;
2.
3./**
4. * @author atguigu
5. *
6. * 饿汉式(立即加载)
7. */
8.public class Singleton {
9.
10. /**
11. * 私有构造
12. */
13. private Singleton() {
14. System.out.println("构造函数Singleton1");
15. }
16.
17. /**
18. * 初始值为实例对象
19. */
20. private static Singleton single = new Singleton();
21.
22. /**
23. * 静态工厂方法
24. * @return 单例对象
25. */
26. public static Singleton getInstance() {
27. System.out.println("getInstance");
28. return single;
29. }
30.
31. public static void main(String[] args){
32. System.out.println("初始化");
33. Singleton instance = Singleton.getInstance();
34. }
35.}
该示例虽然用延迟加载方式实现了懒汉式单例,但在多线程环境下会产生多个Singleton对象
1.package com.atguigu.interview.chapter02;
2.
3./**
4. * @author atguigu
5. *
6. * 懒汉式(延迟加载)
7. */
8.public class Singleton2 {
9.
10. /**
11. * 私有构造
12. */
13. private Singleton2() {
14. System.out.println("构造函数Singleton2");
15. }
16.
17. /**
18. * 初始值为null
19. */
20. private static Singleton2 single = null;
21.
22. /**
23. * 静态工厂方法
24. * @return 单例对象
25. */
26. public static Singleton2 getInstance() {
27. if(single == null){
28. System.out.println("getInstance");
29. single = new Singleton2();
30. }
31. return single;
32. }
33.
34. public static void main(String[] args){
35.
36. System.out.println("初始化");
37. Singleton2 instance = Singleton2.getInstance();
38. }
}
在方法上加synchronized同步锁或是用同步代码块对类加同步锁,此种方式虽然解决了多个实例对象问题,但是该方式运行效率却很低下,下一个线程想要获取对象,就必须等待上一个线程释放锁之后,才可以继续运行。
1.package com.atguigu.interview.chapter02;
2.
3./**
4. * @author atguigu
5. *
6. * 同步锁(解决线程安全问题)
7. */
8.public class Singleton3 {
9.
10. /**
11. * 私有构造
12. */
13. private Singleton3() {}
14.
15. /**
16. * 初始值为null
17. */
18. Private static Singleton3 single = null;
19.
20. Public synchronized static Singleton3 getInstance() {
21.
22. if(single == null){
23. single = new Singleton3();
24. }
25.
26. }
27. return single;
28. }
29.}
(4)双重检查锁(提高同步锁的效率):
使用双重检查锁进一步做了优化,可以避免整个方法被锁,只对需要锁的代码部分加锁,可以提高执行效率。
1.package com.atguigu.interview.chapter02;
2.
3./**
4. * @author atguigu
5. * 双重检查锁(提高同步锁的效率)
6. */
7.public class Singleton4 {
8.
9. /**
10. * 私有构造
11. */
12. private Singleton4() {}
13.
14. /**
15. * 初始值为null
16. * 加volatile关键字是为了防止 创建对象时的指令重排问题,导致其他线程使用对象时造成空指针问题。
17. */
18. Private volatile static Singleton4 single = null;
19.
20. /**
21. * 双重检查锁
22. * @return 单例对象
23. */
24. public static Singleton4 getInstance() {
25. if (single == null) {
26. synchronized (Singleton4.class) {
27. if (single == null) {
28. single = new Singleton4();
29. }
30. }
31. }
32. return single;
33. }
34.}
(5) 静态内部类:
这种方式引入了一个内部静态类(static class),静态内部类只有在调用时才会加载,它保证了Singleton实例的延迟初始化,又保证了实例的唯一性。它把singleton的实例化操作放到一个静态内部类中,在第一次调用getInstance()方法时,JVM才会去加载InnerObject类,同时初始化singleton实例,所以能让getInstance() 方法线程安全。
特点是:即能延迟加载,也能保证线程安全。
静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。
1.package com.atguigu.interview.chapter02;
2.
3./**
4. * @author atguigu
5. *
6. * 静态内部类(延迟加载,线程安全)
7. */
8.public class Singleton5 {
9.
10. /**
11. * 私有构造
12. */
13. private Singleton5() {}
14.
15. /**
16. * 静态内部类
17. */
18. private static class InnerObject{
19. private static Singleton5 single = new Singleton5();
20. }
21.
22. public static Singleton5 getInstance() {
23. return InnerObject.single;
24. }
25.}
(6)内部枚举类实现(防止反射和反序列化攻击):
事实上,通过Java反射机制是能够实例化构造方法为private的类的。这也就是我们现在需要引入的枚举单例模式。
1.package com.atguigu.interview.chapter02;
2.
3./**
4. * @author atguigu
5. */
6.public class SingletonFactory {
7.
8. /**
9. * 内部枚举类
10. */
11. private enum EnumSingleton{
12. Singleton;
13. private Singleton6 singleton;
14.
15. //枚举类的构造方法在类加载是被实例化
16. private EnumSingleton(){
17. singleton = new Singleton6();
18. }
19. public Singleton6 getInstance(){
20. return singleton;
21. }
22. }
23.
24. public static Singleton6 getInstance() {
25. return EnumSingleton.Singleton.getInstance();
26. }
27.}
28.
29.class Singleton6 {
30. public Singleton6(){}
31.}
工厂设计模式,顾名思义,就是用来生产对象的,在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则,如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦。
定义:
一个工厂方法,依据传入的参数,生成对应的产品对象;
角色:
1、抽象产品
2、具体产品
3、具体工厂
4、产品使用者
使用说明:
先将产品类抽象出来,比如,苹果和梨都属于水果,抽象出来一个水果类Fruit,苹果和梨就是具体的产品类,然后创建一个水果工厂,分别用来创建苹果和梨。代码如下:
水果接口:
1.public interface Fruit {
2. void whatIm();
}
苹果类:
1.public class Apple implements Fruit {
2. @Override
3. public void whatIm() {
4. System.out.println("苹果");
5. }
6.}
梨类:
1.public class Pear implements Fruit {
2. @Override
3. public void whatIm() {
4. System.out.println("梨");
5. }
6.}
水果工厂:
1.public class FruitFactory {
2.
3. public Fruit createFruit(String type) {
4.
5. if (type.equals("apple")) {//生产苹果
6. return new Apple();
7. } else if (type.equals("pear")) {//生产梨
8. return new Pear();
9. }
10.
11. return null;
12. }
13.}
使用工厂生产产品:
1.public class FruitApp {
2.
3. public static void main(String[] args) {
4. FruitFactory mFactory = new FruitFactory();
5. Apple apple = (Apple) mFactory.createFruit("apple");//获得苹果
6. Pear pear = (Pear) mFactory.createFruit("pear");//获得梨
7. apple.whatIm();
8. pear.whatIm();
9. }
10.}
以上的这种方式,每当添加一种水果,就必然要修改工厂类,违反了开闭原则;所以简单工厂只适合于产品对象较少,且产品固定的需求,对于产品变化无常的需求来说显然不合适。
定义:
将工厂提取成一个接口或抽象类,具体生产什么产品由子类决定;
角色:
1、抽象产品
2、具体产品
3、抽象工厂
4、具体工厂\
使用说明:
和上例中一样,产品类抽象出来,这次我们把工厂类也抽象出来,生产什么样的产品由子类来决定。代码如下:\
水果接口、苹果类和梨类:
代码和上例一样
抽象工厂接口:
1.public interface FruitFactory {
2. Fruit createFruit();//生产水果
3.}
苹果工厂:
1.public class AppleFactory implements FruitFactory {
2. @Override
3. public Apple createFruit() {
4. return new Apple();
5. }
6.}
梨工厂:
1.public class PearFactory implements FruitFactory {
2. @Override
3. public Pear createFruit() {
4. return new Pear();
5. }
}
使用工厂生产产品:
1.public class FruitApp {
2.
3. public static void main(String[] args){
4. AppleFactory appleFactory = new AppleFactory();
5. PearFactory pearFactory = new PearFactory();
6. Apple apple = appleFactory.createFruit();//获得苹果
7. Pear pear = pearFactory.createFruit();//获得梨
8. apple.whatIm();
9. pear.whatIm();
10. }
11.}
以上这种方式,虽然解耦了,也遵循了开闭原则,但是如果我需要的产品很多的话,需要创建非常多的工厂,所以这种方式的缺点也很明显。
定义:
为创建一组相关或者是相互依赖的对象提供的一个接口,而不需要指定它们的具体类。\
角色:
1、抽象产品
2、具体产品
3、抽象工厂
4、具体工厂
使用说明:
抽象工厂和工厂方法的模式基本一样,区别在于,工厂方法是生产一个具体的产品,而抽象工厂可以用来生产一组相同,有相对关系的产品;重点在于一组,一批,一系列;举个例子,假如生产小米手机,小米手机有很多系列,小米note、红米note等;假如小米note生产需要的配件有825的处理器,6英寸屏幕,而红米只需要650的处理器和5寸的屏幕就可以了。用抽象工厂来实现:
cpu接口和实现类:
1.public interface Cpu {
2. void run();
3.
4. class Cpu650 implements Cpu {
5. @Override
6. public void run() {
7. System.out.println("650 也厉害");
8. }
9. }
10.
11. class Cpu825 implements Cpu {
12. @Override
13. public void run() {
14. System.out.println("825 更强劲");
15. }
16. }
17.}
屏幕接口和实现类:
1.public interface Screen {
2.
3. void size();
4.
5. class Screen5 implements Screen {
6.
7. @Override
8. public void size() {
9. System.out.println("" +
10. "5寸");
11. }
12. }
13.
14. class Screen6 implements Screen {
15.
16. @Override
17. public void size() {
18. System.out.println("6寸");
19. }
20. }
21.}
抽象工厂接口:
1.public interface PhoneFactory {
2.
3. Cpu getCpu();//使用的cpu
4.
5. Screen getScreen();//使用的屏幕
6.}
小米手机工厂:
1.public class XiaoMiFactory implements PhoneFactory {
2. @Override
3. public Cpu.Cpu825 getCpu() {
4. return new Cpu.Cpu825();//高性能处理器
5. }
6.
7. @Override
8. public Screen.Screen6 getScreen() {
9. return new Screen.Screen6();//6寸大屏
10. }
11.}
红米手机工厂:
1.public class HongMiFactory implements PhoneFactory {
2.
3. @Override
4. public Cpu.Cpu650 getCpu() {
5. return new Cpu.Cpu650();//高效处理器
6. }
7.
8. @Override
9. public Screen.Screen5 getScreen() {
10. return new Screen.Screen5();//小屏手机
11. }
}
使用工厂生产产品:
1.public class PhoneApp {
2. public static void main(String[] args){
3. HongMiFactory hongMiFactory = new HongMiFactory();
4. XiaoMiFactory xiaoMiFactory = new XiaoMiFactory();
5. Cpu.Cpu650 cpu650 = hongMiFactory.getCpu();
6. Cpu.Cpu825 cpu825 = xiaoMiFactory.getCpu();
7. cpu650.run();
8. cpu825.run();
9.
10. Screen.Screen5 screen5 = hongMiFactory.getScreen();
11. Screen.Screen6 screen6 = xiaoMiFactory.getScreen();
12. screen5.size();
13. screen6.size();
14. }
}
以上例子可以看出,抽象工厂可以解决一系列的产品生产的需求,对于大批量,多系列的产品,用抽象工厂可以更好的管理和扩展。
1、对于简单工厂和工厂方法来说,两者的使用方式实际上是一样的,如果对于产品的分类和名称是确定的,数量是相对固定的,推荐使用简单工厂模式;
2、抽象工厂用来解决相对复杂的问题,适用于一系列、大批量的对象生产。
代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。
举个例子来说明:假如说我现在想买一辆二手车,虽然我可以自己去找车源,做质量检测等一系列的车辆过户流程,但是这确实太浪费我得时间和精力了。我只是想买一辆车而已为什么我还要额外做这么多事呢?于是我就通过中介公司来买车,他们来给我找车源,帮我办理车辆过户流程,我只是负责选择自己喜欢的车,然后付钱就可以了。用图表示如下:
中介隔离作用:
在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
开闭原则,增加功能:
代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要修改已经封装好的委托类。
我们有多种不同的方式来实现代理。
如果按照代理创建的时期来进行分类的话,可以分为两种:静态代理、动态代理。
第一步:创建服务类接口
1.public interface BuyHouse {
2. void buyHouse();
}
第二步:实现服务接口
1.public class BuyHouseImpl implements BuyHouse {
2.
3. @Override
4. public void buyHouse() {
5. System.out.println("我要买房");
6. }
}
第三步:创建代理类
1.public class BuyHouseProxy implements BuyHouse {
2.
3. private BuyHouse buyHouse;
4.
5. public BuyHouseProxy(final BuyHouse buyHouse) {
6. this.buyHouse = buyHouse;
7. }
8.
9. @Override
10. public void buyHouse() {
11. System.out.println("买房前准备");
12. buyHouse.buyHouse();
13. System.out.println("买房后装修");
14.
15. }
16.}
第四步:编写测试类
1.public class HouseApp {
2.
3. public static void main(String[] args) {
4. BuyHouse buyHouse = new BuyHouseImpl();
5. BuyHouseProxy buyHouseProxy = new BuyHouseProxy(buyHouse);
6. buyHouseProxy.buyHouse();
7. }
8.}
静态代理总结:
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
缺点:我们得为每一个服务创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
在动态代理中我们不再需要再手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由JDK在运行时为我们动态的来创建。
第一步:创建服务类接口
代码和上例一样
第二步:实现服务接口
代码和上例一样
第三步:编写动态处理器
1.public class DynamicProxyHandler implements InvocationHandler {
2.
3. private Object object;
4.
5. public DynamicProxyHandler(final Object object) {
6. this.object = object;
7. }
8.
9. @Override
10. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
11. System.out.println("买房前准备");
12. Object result = method.invoke(object, args);
13. System.out.println("买房后装修");
14. return result;
15. }
}
第四步:编写测试类
1.public class HouseApp {
2.
3. public static void main(String[] args) {
4. BuyHouse buyHouse = new BuyHouseImpl();
5. BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(
6. BuyHouse.class.getClassLoader(),
7. new Class[]{BuyHouse.class},
8. new DynamicProxyHandler(buyHouse));
9. proxyBuyHouse.buyHouse();
10. }
11.}
Proxy是所有动态生成的代理的共同的父类,这个类有一个静态方法Proxy.newProxyInstance(),接收三个参数:
ClassLoader
loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的
Class<?>[]
interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型
InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法
JDK动态代理总结:
优点:相对于静态代理,动态代理大大减少了开发任务,同时减少了对业务接口的依赖,降低了耦合度。
缺点:Proxy是所有动态生成的代理的共同的父类,因此服务类必须是接口的形式,不能是普通类的形式,因为Java无法实现多继承。
JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现Spring
AOP的基础。
Cglib子类代理实现方法:
(1)引入cglib的jar文件,asm的jar文件
(2)代理的类不能为final
(3)目标业务对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法
第一步:创建服务类
1.public class BuyHouse2 {
2.
3. public void buyHouse() {
4. System.out.println("我要买房");
5. }
6.}
第二步:创建CGLIB代理类
1.public class CglibProxy implements MethodInterceptor {
2.
3. private Object target;
4.
5. public CglibProxy(Object target) {
6. this.target = target;
7. }
8.
9. /**
10. * 给目标对象创建一个代理对象
11. * @return 代理对象
12. */
13. public Object getProxyInstance() {
14. //1.工具类
15. Enhancer enhancer = new Enhancer();
16. //2.设置父类
17. enhancer.setSuperclass(target.getClass());
18. //3.设置回调函数
19. enhancer.setCallback(this);
20. //4.创建子类(代理对象)
21. return enhancer.create();
22.
23. }
24.
25. public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
26. System.out.println("买房前准备");
27. //执行目标对象的方法
28. Object result = method.invoke(target, args);
29. System.out.println("买房后装修");
30. return result;
31. }
}
第三步:创建测试类
1.public class HouseApp {
2.
3. public static void main(String[] args) {
4.
5. BuyHouse2 target = new BuyHouse2();
6. CglibProxy cglibProxy = new CglibProxy(target);
7. BuyHouse2 buyHouseCglibProxy = (BuyHouse2) cglibProxy.getProxyInstance();
8. buyHouseCglibProxy.buyHouse();
9. }
10.}
CGLib代理总结:
CGLib创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。
动态代理的原理:
使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
动态代理的方式:
基于接口实现动态代理: JDK动态代理
基于继承实现动态代理: Cglib、Javassist动态代理
相关文章:
第一章 面试技巧篇
第二章 数据结构、设计模式与手写代码
第三章 Java基础篇
第四章 Java高级篇
第五章 MySQL数据库篇
第六章 Java Web篇
第七章 Java框架篇
第八章 Redis数据库篇
第九章 分布式技术篇
第十章 Git与Linux篇
第十一章 电商项目篇之尚品汇商城
真诚点赞 诚不我欺~
{{ praiseUserVoList.length }}人点赞
内容
"{{ child.parent.content }}"