0

1

2

3

4

5

6

7

8

9

0

1

{{ noReadMessageTotal }}

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

尚硅谷Java技术之北京高频面试题:第二章 数据结构、设计模式与手写代码

晴天 晴天 | 495 | 686天前

尚硅谷Java技术之北京高频面试题

版本:V6.0

尚硅谷Java技术中心

第二章 数据结构、设计模式与手写代码

1、怎么理解时间复杂度和空间复杂度?

时间复杂度和空间复杂度一般是针对算法而言,是衡量一个算法是否高效的重要标准。先纠正一个误区,时间复杂度并不是算法执行的时间,再纠正一个误区,算法不单单指冒泡排序之类的,一个循环甚至是一个判断都可以称之为算法。其实理解起来并不冲突,八大排序甚至更多的算法本质上也是通过各种循环判断来实现的。

时间复杂度:指算法语句的执行次数。O(1),O(n),O(logn),O(n2)

空间复杂度:就是一个算法在运行过程中临时占用的存储空间大小,换句话说就是被创建次数最多的变量,它被创建了多少次,那么这个算法的空间复杂度就是多少。有个规律,如果算法语句中就有创建对象,那么这个算法的时间复杂度和空间复杂度一般一致,很好理解,算法语句被执行了多少次就创建了多少对象。

2、数组和链表结构简单对比?

数组:相同数据类型的元素按一定顺序排列的集合,就是把有限个类型相同的变量用一个名字命名,然后用编号区分他们的变量的集合,这个名字称为数组名,编号称为下标。

数组的特性:
1.数组必须先定义固定长度,不能适应数据动态增减。
2.当数据增加时,可能超出原先定义的元素个数,当数据减少时,造成内存浪费。
3.数组查询比较方便,根据下标就可以直接找到元素,时间复杂度O(1);增加和删除比较复杂,需要移动操作数所在位置后的所有数据,时间复杂度为O(N)。

链表:是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表的特性:
1.链表动态进行存储分配,可适应数据动态增减。
2.插入、删除数据比较方便,时间复杂度O(1);查询必须从头开始找起,十分麻烦,时间复杂度O(N)。

常见的链表:
1.单链表:通常链表每一个元素都要保存一个指向下一个元素的指针。
2.双链表:每个元素既要保存到下一个元素的指针,还要保存一个上一个元素的指针。
3.循环链表:在最后一个元素中下一个元素指针指向首元素。

应用:如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反,如果需要经常插入和删除元素就需要用链表数据结构了。

3、怎么遍历一个树

四种遍历概念

先序遍历:先访问根节点,再访问左子树,最后访问右子树。

后序遍历:先左子树,再右子树,最后根节点。

中序遍历:先左子树,再根节点,最后右子树。

层序遍历:每一层从左到右访问每一个节点。

每一个子树遍历时依然按照此时的遍历顺序。可以采用递归实现遍历。

4、冒泡排序(Bubble Sort)

算法描述:

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;

  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;

  • 针对所有的元素重复以上的步骤,除了最后一个;

  • 重复步骤1~3,直到排序完成。
    image.png

如果两个元素相等,不会再交换位置,所以冒泡排序是一种稳定排序算法。

代码实现:

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 = {34438547153626272464195048};   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.}  

5、快速排序(Quick Sort)

算法描述:

使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);

  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

image.png

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 = {34438547153626272464195048};   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

6、二分查找(Binary Search)

算法描述:

  • 二分查找也称折半查找,它是一种效率较高的查找方法,要求列表中的元素首先要进行有序排列。

  • 首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;

  • 否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。

  • 重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

代码实现:

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, -1692121233030};   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 = {181089100010001234};   74.        List<Integer> list = binarySearch2(data, 0, data.length, 1000);   75.        System.out.println(list);   76.    }   77.}  

7、你所知道的设计模式有哪些?

Java 中一般认为有 23种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。下面列出了所有的设计模式。需要掌握的设计模式我单独列出来了,当然能掌握的越多越好。

总体来说设计模式分为三大类:

创建型模式,共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共7种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共11种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

8、单例模式(Singleton Pattern)

8.1 单例模式定义

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。

8.2 单例模式的特点

  • 单例类只能有一个实例。

  • 单例类必须自己创建自己的唯一实例。

  • 单例类必须给所有其他对象提供这一实例。

单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。

8.3 单例的四大原则

  • 构造私有

  • 以静态方法或者枚举返回实例

  • 确保实例只有一个,尤其是多线程环境

  • 确保反序列化时不会重新构建对象

8.4 实现单例模式的方式

(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.}  
  1. 懒汉式(延迟加载):

该示例虽然用延迟加载方式实现了懒汉式单例,但在多线程环境下会产生多个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.    }   }  
  1. 同步锁(解决线程安全问题):

在方法上加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.}  

9、工厂设计模式(Factory)

9.1 什么是工厂设计模式?

工厂设计模式,顾名思义,就是用来生产对象的,在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则,如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦。

9.2 简单工厂(Simple Factory)

定义:
一个工厂方法,依据传入的参数,生成对应的产品对象;
角色:
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.}  

以上的这种方式,每当添加一种水果,就必然要修改工厂类,违反了开闭原则;所以简单工厂只适合于产品对象较少,且产品固定的需求,对于产品变化无常的需求来说显然不合适。

9.3 工厂方法(Factory Method)

定义:

将工厂提取成一个接口或抽象类,具体生产什么产品由子类决定;
角色:
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.}  

以上这种方式,虽然解耦了,也遵循了开闭原则,但是如果我需要的产品很多的话,需要创建非常多的工厂,所以这种方式的缺点也很明显。

9.4 抽象工厂(Abstract Factory)

定义:
为创建一组相关或者是相互依赖的对象提供的一个接口,而不需要指定它们的具体类。\

角色:
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.    }   }  

以上例子可以看出,抽象工厂可以解决一系列的产品生产的需求,对于大批量,多系列的产品,用抽象工厂可以更好的管理和扩展。

9.5 三种工厂方式总结

1、对于简单工厂和工厂方法来说,两者的使用方式实际上是一样的,如果对于产品的分类和名称是确定的,数量是相对固定的,推荐使用简单工厂模式;
2、抽象工厂用来解决相对复杂的问题,适用于一系列、大批量的对象生产。

10、代理模式(Proxy)

10.1 什么是代理模式?

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

举个例子来说明:假如说我现在想买一辆二手车,虽然我可以自己去找车源,做质量检测等一系列的车辆过户流程,但是这确实太浪费我得时间和精力了。我只是想买一辆车而已为什么我还要额外做这么多事呢?于是我就通过中介公司来买车,他们来给我找车源,帮我办理车辆过户流程,我只是负责选择自己喜欢的车,然后付钱就可以了。用图表示如下:

image.png

10.2 为什么要用代理模式?

中介隔离作用:
在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。

开闭原则,增加功能:
代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要修改已经封装好的委托类。

10.3 有哪几种代理模式?

我们有多种不同的方式来实现代理。
如果按照代理创建的时期来进行分类的话,可以分为两种:静态代理、动态代理。

  • 静态代理是由程序员创建或特定工具自动生成源代码,再对其编译。在程序员运行之前,代理类.class文件就已经被创建了。
  • 动态代理是在程序运行时通过反射机制动态创建的。

10.4 静态代理(Static Proxy)

第一步:创建服务类接口

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.}  

静态代理总结:

优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
缺点:我们得为每一个服务创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。

10.5 JDK动态代理(Dynamic Proxy)

在动态代理中我们不再需要再手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由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无法实现多继承。

10.6 CGLib动态代理(CGLib Proxy)

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修饰的方法无法进行代理。

10.7 简述动态代理的原理, 常用的动态代理的实现方式

动态代理的原理:
使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

动态代理的方式:
基于接口实现动态代理: JDK动态代理
基于继承实现动态代理: Cglib、Javassist动态代理

相关文章:
第一章 面试技巧篇
第二章 数据结构、设计模式与手写代码
第三章 Java基础篇
第四章 Java高级篇
第五章 MySQL数据库篇
第六章 Java Web篇
第七章 Java框架篇
第八章 Redis数据库篇
第九章 分布式技术篇
第十章 Git与Linux篇
第十一章 电商项目篇之尚品汇商城

文章标签: JaveSE数据结构

真诚点赞 诚不我欺~

{{ praiseUserVoList.length }}人点赞

item.nickname

尚硅谷Java技术之北京高频面试题:第二章 数据结构、设计模式与手写代码

{{ isPraise ? '已点赞' : '点赞'}}
{{ isCollect ? '已收藏' : '收藏'}}
评论
gOod mornIng
没有更多啦~ 加载中...

关于作者

晴天
晴天

人因梦想而伟大!