第一篇 数据结构、设计模式与手写代码

目录

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

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

3.怎么遍历一个树

4、冒泡排序(Bubble Sort)

5、快速排序(Quick Sort)

6.二分查找(Binary Search)

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

        7.1 单例模式定义

        7.2 单例模式的特点

        7.3 单例的四大原则

        7.4 实现单例模式的方式

8.工厂设计模式(Factory)

        8.1 什么是工厂设计模式?

        8.2 简单工厂(Simple Factory)

        8.3 工厂方法(Factory Method)

        8.4 抽象工厂(Abstract Factory)

        8.5 三种工厂方式总结

9、代理模式(Proxy)

9.1 什么是代理模式?

9.2 为什么要用代理模式?

9.3 有哪几种代理模式?

9.4 静态代理(Static Proxy)

9.5 JDK动态代理(Dynamic Proxy)

9.6 CGLib动态代理(CGLib Proxy)

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


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

时间复杂度:用来表示算法的操作次数

空间复杂度:用户表示算法在运行过程中占用的存储空间

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

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

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

数据结构
数据结构特点

数组

固定长度 ArrayList-->1.5倍
链表单、双项链表
队列先进先出
先进后出
哈希表

Mysql高级课件:

二叉树  AVL平衡树 2-3-4树 红黑树 btree  b+tree

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

数组的特性:

1.连续的内存空间,数组必须先定义固定长度,不能适应数据动态增减

2.当数据增加时,可能超出原先定义的元素个数,当数据减少时,造成内存浪费

3.数组查询比较方便,根据下标就可以直接找到元素时间复杂度O(1)增加和删除比较复杂需要移动操作数所在位置后的所有数据,时间复杂度为O(N)

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

链表的特性:

1.链表动态进行存储分配,可适应数据动态增减

2.插入、删除数据比较方便,时间复杂度O(1);查询必须从头开始找起,十分麻烦,时间复杂度O(N)

常见的链表:

1.单链表:通常链表每一个元素都要保存一个指向下一个元素的指针

2.双链表:每个元素既要保存到下一个元素的指针,还要保存一个上一个元素的指针

3.循环链表:在最后一个元素中下一个元素指针指向首元素

链表和数组都是在堆里分配内存

应用:

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

3.怎么遍历一个树

考查形式:笔试题

四种遍历概念

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

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

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

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

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

算法:

参考链接:

1.0 十大经典排序算法 | 菜鸟教程

4冒泡排序Bubble Sort

算法描述:

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤1~3,直到排序完成。

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

代码实现:

package com.atguigu.interview.chapter02;  
  
/** 
 * @author atguigu 
 * @since 2019/7/22 
 * 冒泡排序 
 */  
public class BubbleSort {  
  
    /** 
     * @param data 被排序的数组 
     */  
    public static void bubbleSort(int[] data) {  
  
        int arrayLength = data.length;  
  
        for (int i = 1; i < arrayLength; i++) {//第i次排序  
  
            for (int j = 0; j < arrayLength - i; j++) {//从索引为j的数开始  
                if (data[j] > data[j + 1]) { //相邻元素两两对比  
                    int temp = data[j + 1];  // 元素交换  
                    data[j + 1] = data[j];  
                    data[j] = temp;  
                }  
            }  
  
            System.out.println("第" + i + "次排序:\n" + java.util.Arrays.toString(data));  
        }  
    }  
  
    public static void main(String[] args) {  
  
        int[] data = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};  
  
        System.out.println("排序之前:\n" + java.util.Arrays.toString(data));  
  
        bubbleSort(data);  
  
        System.out.println("排序之后:\n" + java.util.Arrays.toString(data));  
    }  
}  

5、快速排序(Quick Sort)

算法描述:

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

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

key值的选取可以有多种形式,例如中间数或者随机数,分别会对算法的复杂度产生不同的影响。

代码实现:

package com.atguigu.interview.chapter02;  
  
/** 
 * @author atguigu
 * @since 2019/7/22 
 * 快速排序 
 */  
public class QuickSort {  
  
    public static void quickSort(int[] data, int low, int high) {  
        int i, j, temp, t;  
        if (low > high) {  
            return;  
        }  
        i = low;  
        j = high;  
        //temp就是基准位  
        temp = data[low];  
        System.out.println("基准位:" + temp);  
  
        while (i < j) {  
            //先看右边,依次往左递减  
            while (temp <= data[j] && i < j) {  
                j--;  
            }  
            //再看左边,依次往右递增  
            while (temp >= data[i] && i < j) {  
                i++;  
            }  
            //如果满足条件则交换  
            if (i < j) {  
                System.out.println("交换:" + data[i] + "和" + data[j]);  
                t = data[j];  
                data[j] = data[i];  
                data[i] = t;  
                System.out.println(java.util.Arrays.toString(data));  
  
            }  
        }  
        //最后将基准位与i和j相等位置的数字交换  
        System.out.println("基准位" + temp + "和i、j相遇的位置" + data[i] + "交换");  
        data[low] = data[i];  
        data[i] = temp;  
        System.out.println(java.util.Arrays.toString(data));  
  
        //递归调用左半数组  
        quickSort(data, low, j - 1);  
        //递归调用右半数组  
        quickSort(data, j + 1, high);  
    }  
  
  
    public static void main(String[] args) {  
  
        int[] data = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};  
  
        System.out.println("排序之前:\n" + java.util.Arrays.toString(data));  
  
        quickSort(data, 0, data.length - 1);  
  
        System.out.println("排序之后:\n" + java.util.Arrays.toString(data));  
    }  
}  

快速排序详细参考:

快速排序(java实现)_大数据指北的博客-CSDN博客_快速排序java

6.二分查找(Binary Search

考查形式:笔试题

1000 2^10 = 1024

100  2^20 1024 * 1024 = 100

10亿  2^30

算法描述:

  1. 二分查找也称折半查找,它是一种效率较高的查找方法,要求列表中的元素首先要进行有序排列
  2. 首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;
  3. 否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。
  4. 重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

代码实现:

package com.atguigu.interview.chapter02;  
  
/** 
 * @author atguigu
 * @since 2019/7/22 
 */  
public class BinarySearch {  
  
  
    /** 
     * 二分查找 时间复杂度O(log2n);空间复杂度O(1) 
     * 
     * @param arr     被查找的数组 
     * @param left 
     * @param right 
     * @param findVal 
     * @return 返回元素的索引 
     */  
    public static int binarySearch(int[] arr, int left, int right, int findVal) {  
  
        if (left > right) {//递归退出条件,找不到,返回-1  
            return -1;  
        }  
  
        int midIndex = (left + right) / 2;  
  
        if (findVal < arr[midIndex]) {//向左递归查找  
            return binarySearch(arr, left, midIndex, findVal);  
        } else if (findVal > arr[midIndex]) {//向右递归查找  
            return binarySearch(arr, midIndex, right, findVal);  
        } else {  
            return midIndex;  
        }  
    }  
  
    public static void main(String[] args){  
  
        //注意:需要对已排序的数组进行二分查找  
        int[] data = {-49, -30, -16, 9, 21, 21, 23, 30, 30};  
        int i = binarySearch(data, 0, data.length, 21);  
        System.out.println(i);  
    }  
}  

拓展需求:

当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到。

代码实现:

package com.atguigu.interview.chapter02;  
  
import java.util.ArrayList;  
import java.util.List;  
  
/** 
 * @author atguigu
 * @since 2019/7/22 
 */  
public class BinarySearch2 {  
  
    /** 
     * {1, 8, 10, 89, 1000, 1000, 1234} 
     * 一个有序数组中,有多个相同的数值,如何将所有的数值都查找到,比如这里的 1000. 
     * 分析: 
     * 1. 返回的结果是一个列表 list 
     * 2. 在找到结果时,向左边扫描,向右边扫描 [条件] 
     * 3. 找到结果后,就加入到ArrayBuffer 
     * 
     * @return 
     */  
    public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal) {  
  
        //找不到条件?  
        List<Integer> list = new ArrayList<>();  
  
        if (left > right) {//递归退出条件,找不到,返回-1  
            return list;  
        }  
  
        int midIndex = (left + right) / 2;  
        int midVal = arr[midIndex];  
        if (findVal < midVal) {//向左递归查找  
            return binarySearch2(arr, left, midIndex - 1, findVal);  
        } else if (findVal > midVal) { //向右递归查找  
            return binarySearch2(arr, midIndex + 1, right, findVal);  
        } else {  
            System.out.println("midIndex=" + midIndex);  
  
            //向左边扫描  
            int temp = midIndex - 1;  
            while (true) {  
                if (temp < 0 || arr[temp] != findVal) {  
                    break;  
                }  
                if (arr[temp] == findVal) {  
                    list.add(temp);  
                }  
                temp -= 1;  
            }  
  
            //将中间这个索引加入  
            list.add(midIndex);  
  
            //向右边扫描  
            temp = midIndex + 1;  
            while (true) {  
                if (temp > arr.length - 1 || arr[temp] != findVal) {  
                    break;  
                }  
                if (arr[temp] == findVal) {  
                    list.add(temp);  
                }  
                temp += 1;  
            }  
            return list;  
        }  
    }  

 public static void main(String[] args){  
  
        //注意:需要对已排序的数组进行二分查找  
        int[] data = {1, 8, 10, 89, 1000, 1000, 1234};  
        List<Integer> list = binarySearch2(data, 0, data.length, 1000);  
        System.out.println(list);  
    }  
}  

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

我们不需要知道所有的设计模式,要求:掌握5

你之前用过哪些设计模式呀(提问)

请手写两种以上的单例设计模式(笔试题)

创建对象的方式:new 克隆 反射 序列化

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

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

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

结构型模式,将类与类 或者对象对象 组成更大的结构,共7种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

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

模板方法模式:redisTemplate jdbcTemplate

观察者模式:主题模式

  1. 单例模式(Binary Search

考查形式:笔试题

单例模式:

饿汉式:当类加载时候创建对象,不会出现线程安全问题。浪费内存

懒汉式:第一次获取的时候才创建对象。面试最多的

7.1 单例模式定义

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

7.2 单例模式的特点

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

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

7.3 单例的四大原则

  1. 构造私有
  2. 以静态方法或者枚举返回实例
  3. 确保实例只有一个,尤其是多线程环境
  4. 确保反、序列化时不会重新构建对象

7.4 实现单例模式的方式

1)饿汉式(立即加载):占用内存。

饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,会使Java单例实现失效)

package com.atguigu.interview.chapter02;  
  
/** 
 * @author atguigu
 * 
 * 饿汉式(立即加载) 
 */  
public class Singleton {  
  
    /** 
     * 私有构造 
     */  
    private Singleton() {  
        System.out.println("构造函数Singleton1");  
    }  
  
    /** 
     * 初始值为实例对象 
     */  
    private static Singleton single = new Singleton();  
  
    /** 
     * 静态工厂方法 
     * @return 单例对象 
     */  
    public static Singleton getInstance() {  
        System.out.println("getInstance");  
        return single;  
    }  
  
    public static void main(String[] args){  
        System.out.println("初始化");  
        Singleton instance = Singleton.getInstance();  
    }  
} 

 

2)懒汉式(延迟加载)(有问题):

该示例虽然用延迟加载方式实现了懒汉式单例,但在多线程环境下会产生多个Singleton对象

package com.atguigu.interview.chapter02;  
  
/** 
 * @author atguigu
 * 
 * 懒汉式(延迟加载) 
 */  
public class Singleton2 {  
  
    /** 
     * 私有构造 
     */  
    private Singleton2() {  
        System.out.println("构造函数Singleton2");  
    }  
  
    /** 
     * 初始值为null 
     */  
    private static Singleton2 single = null;  
  
    /** 
     * 静态工厂方法 
     * @return 单例对象 
     */  
    public static Singleton2 getInstance() {  
        if(single == null){  
            System.out.println("getInstance");  
            single = new Singleton2();  
        }  
        return single;  
    }  
  
    public static void main(String[] args){  
  
        System.out.println("初始化");  
        Singleton2 instance = Singleton2.getInstance();  
    }  
}  

3)同步锁(解决线程安全问题)

在方法上加synchronized同步锁或是用同步代码块对类加同步锁,此种方式虽然解决了多个实例对象问题,但是该方式运行效率却很低下,下一个线程想要获取对象,就必须等待上一个线程释放锁之后,才可以继续运行。

package com.atguigu.interview.chapter02;  
  
/** 
 * @author atguigu
 * 
 * 同步锁(解决线程安全问题) 
 */  
public class Singleton3 {  
  
    /** 
     * 私有构造 
     */  
    private Singleton3() {}  
  
    /** 
     * 初始值为null 
     */  
    Private static Singleton3 single = null;  
  
    Public synchronized  static Singleton3 getInstance() {  
        
            if(single == null){  
                single = new Singleton3();  
            }  
       
      }
        return single;  
    }  
}  

 4)有了解过DCL模式么,要求会写

使用双重检查锁进一步做了优化,可以避免整个方法被锁,只对需要锁的代码部分加锁,可以提高执行效率。

package com.atguigu.interview.chapter02;  
  
/** 
 * @author atguigu
 * 双重检查锁(提高同步锁的效率) 
 */  
public class Singleton4 {  
  
    /** 
     * 私有构造 
     */  
    private Singleton4() {}  
  
    /** 
     * 初始值为null 
     * 加volatile关键字是为了防止 创建对象时的指令重排问题,导致其他线程使用对象时造成空指针问题。
     */  
    Private volatile static Singleton4 single = null;  
  
    /** 
     * 双重检查锁 
     * @return 单例对象 
     */  
    public static Singleton4 getInstance() {  
        if (single == null) {   // 解决高并发问题
            synchronized (Singleton4.class) {  
                if (single == null) {   // 判断是否为null
                    single = new Singleton4();  // 不是原子操作 分配空间 初始化赋值 引用地址
                }  
            }  
        }  
        return single;  
    }  
}  

5) 静态内部类:

这种方式引入了一个内部静态类(static class),静态内部类只有在调用时才会加载,它保证了Singleton 实例的延迟初始化,又保证了实例的唯一性。它把singleton 的实例化操作放到一个静态内部类中,在第一次调用getInstance() 方法时,JVM才会去加载InnerObject类,同时初始化singleton 实例,所以能让getInstance() 方法线程安全。

特点是:即能延迟加载,也能保证线程安全。

静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。

package com.atguigu.interview.chapter02;  
  
/** 
 * @author atguigu
 * 
 * 静态内部类(延迟加载,线程安全) 
 */  
public class Singleton5 {  
  
    /** 
     * 私有构造 
     */  
    private Singleton5() {}  
  
    /** 
     * 静态内部类 
     */  
    private static class InnerObject{  
        private static Singleton5 single = new Singleton5();  
    }  
  
    public static Singleton5 getInstance() {  
        return InnerObject.single;  
    }  
}  

6)内部枚举类实现(防止反射和反序列化攻击):

事实上,通过Java反射机制是能够实例化构造方法为private的类的。这也就是我们现在需要引入的枚举单例模式。

package com.atguigu.interview.chapter02;  
  
/** 
 * @author atguigu
 */  
public class SingletonFactory {  
  
    /** 
     * 内部枚举类 
     */  
    private enum EnumSingleton{  
        Singleton;  
        private Singleton6 singleton;  
  
        //枚举类的构造方法在类加载是被实例化  
        private EnumSingleton(){  
            singleton = new Singleton6();  
        }  
        public Singleton6 getInstance(){  
            return singleton;  
        }  
    }  
      
    public static Singleton6 getInstance() {  
        return EnumSingleton.Singleton.getInstance();  
    }  
}  
  
class Singleton6 {  
    public Singleton6(){}  
}  

8、工厂设计模式(Factory)

8.1 什么是工厂设计模式?

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

8.2 简单工厂(Simple Factory)

定义:

一个工厂方法,依据传入的参数,生成对应的产品对象;

角色:

1、抽象产品

2、具体产品

3、具体工厂

4、产品使用者

使用说明:

先将产品类抽象出来,比如,苹果和梨都属于水果,抽象出来一个水果类Fruit,苹果和梨就是具体的产品类,然后创建一个水果工厂,分别用来创建苹果和梨。代码如下:

水果接口:

public interface Fruit {  
    void whatIm();  
}  

苹果类:

public class Apple implements Fruit {  
    @Override  
    public void whatIm() {  
        System.out.println("苹果");  
    }  
}  

梨类:

public class Pear implements Fruit {  
    @Override  
    public void whatIm() {  
        System.out.println("梨");  
    }  
}  

水果工厂:

public class FruitFactory {  
  
    public Fruit createFruit(String type) {  
  
        if (type.equals("apple")) {//生产苹果  
            return new Apple();  
        } else if (type.equals("pear")) {//生产梨  
            return new Pear();  
        }  
  
        return null;  
    }  
}  

使用工厂生产产品:

public class FruitApp {  
  
    public static void main(String[] args) {  
        FruitFactory mFactory = new FruitFactory();  
        Apple apple = (Apple) mFactory.createFruit("apple");//获得苹果  
        Pear pear = (Pear) mFactory.createFruit("pear");//获得梨  
        apple.whatIm();  
        pear.whatIm();  
    }  
}  

以上的这种方式,每当添加一种水果,就必然要修改工厂类,违反了开闭原则;

所以简单工厂只适合于产品对象较少,且产品固定的需求,对于产品变化无常的需求来说显然不合适。

8.3 工厂方法(Factory Method)

定义:

将工厂提取成一个接口或抽象类,具体生产什么产品由子类决定;

角色:

1、抽象产品

2、具体产品

3、抽象工厂

4、具体工厂

使用说明:

和上例中一样,产品类抽象出来,这次我们把工厂类也抽象出来,生产什么样的产品由子类来决定。代码如下:

水果接口、苹果类和梨类:

代码和上例一样

抽象工厂接口:

public interface FruitFactory {  
    Fruit createFruit();//生产水果  
}  

苹果工厂:

public class AppleFactory implements FruitFactory {  
    @Override  
    public Apple createFruit() {  
        return new Apple();  
    }  
}  

梨工厂:

public class PearFactory implements FruitFactory {  
    @Override  
    public Pear createFruit() {  
        return new Pear();  
    }  
}  

使用工厂生产产品:

public class FruitApp {  
  
    public static void main(String[] args){  
        AppleFactory appleFactory = new AppleFactory();  
        PearFactory pearFactory = new PearFactory();  
        Apple apple = appleFactory.createFruit();//获得苹果  
        Pear pear = pearFactory.createFruit();//获得梨  
        apple.whatIm();  
        pear.whatIm();  
    }  
}  

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

8.4 抽象工厂(Abstract Factory)

定义:

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

角色:

  1. 抽象产品
  2. 2、具体产品
  3. 3、抽象工厂
  4. 4、具体工厂

使用说明:

抽象工厂和工厂方法的模式基本一样,区别在于,工厂方法是生产一个具体的产品,而抽象工厂可以用来生产一组相同,有相对关系的产品;重点在于一组,一批,一系列;举个例子,假如生产小米手机,小米手机有很多系列,小米note、红米note等;假如小米note生产需要的配件有825的处理器,6英寸屏幕,而红米只需要650的处理器和5寸的屏幕就可以了。用抽象工厂来实现:

cpu接口和实现类:

public interface Cpu {  
    void run();  
  
    class Cpu650 implements Cpu {  
        @Override  
        public void run() {  
            System.out.println("650 也厉害");  
        }  
    }  
  
    class Cpu825 implements Cpu {  
        @Override  
        public void run() {  
            System.out.println("825 更强劲");  
        }  
    }  
}  

屏幕接口和实现类:

public interface Screen {  
  
    void size();  
  
    class Screen5 implements Screen {  
  
        @Override  
        public void size() {  
            System.out.println("" +  
                    "5寸");  
        }  
    }  
  
    class Screen6 implements Screen {  
  
        @Override  
        public void size() {  
            System.out.println("6寸");  
        }  
    }  
}  

抽象工厂接口:

public interface PhoneFactory {  
  
    Cpu getCpu();//使用的cpu  
  
    Screen getScreen();//使用的屏幕  
}

小米手机工厂:

public class XiaoMiFactory implements PhoneFactory {  
    @Override  
    public Cpu.Cpu825 getCpu() {  
        return new Cpu.Cpu825();//高性能处理器  
    }  
  
    @Override  
    public Screen.Screen6 getScreen() {  
        return new Screen.Screen6();//6寸大屏  
    }  
} 

红米手机工厂:

public class HongMiFactory implements PhoneFactory {  
  
    @Override  
    public Cpu.Cpu650 getCpu() {  
        return new Cpu.Cpu650();//高效处理器  
    }  
  
    @Override  
    public Screen.Screen5 getScreen() {  
        return new Screen.Screen5();//小屏手机  
    }  
}  

使用工厂生产产品:

public class PhoneApp {  
    public static void main(String[] args){  
        HongMiFactory hongMiFactory = new HongMiFactory();  
        XiaoMiFactory xiaoMiFactory = new XiaoMiFactory();  
        Cpu.Cpu650 cpu650 = hongMiFactory.getCpu();  
        Cpu.Cpu825 cpu825 = xiaoMiFactory.getCpu();  
        cpu650.run();  
        cpu825.run();  
  
        Screen.Screen5 screen5 = hongMiFactory.getScreen();  
        Screen.Screen6 screen6 = xiaoMiFactory.getScreen();  
        screen5.size();  
        screen6.size();  
    }  
} 

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

8.5 三种工厂方式总结

1、对于简单工厂和工厂方法来说,两者的使用方式实际上是一样的,如果对于产品的分类和名称是确定的,数量是相对固定的,推荐使用简单工厂模式;

2、抽象工厂用来解决相对复杂的问题,适用于一系列、大批量的对象生产。

9、代理模式(Proxy)

9.1 什么是代理模式?

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

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

9.2 为什么要用代理模式?

中介隔离作用:

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

开闭原则,增加功能:

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

9.3 有哪几种代理模式?

我们有多种不同的方式来实现代理。

如果按照代理创建的时期来进行分类的话,可以分为两种:静态代理、动态代理。

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

9.4 静态代理(Static Proxy)

第一步:创建服务类接口

public interface BuyHouse {  
    void buyHouse();  
}  

第二步:实现服务接口

public class BuyHouseImpl implements BuyHouse {  
  
    @Override  
    public void buyHouse() {  
        System.out.println("我要买房");  
    }  
}  

第三步:创建代理类

 public class BuyHouseProxy implements BuyHouse {  
  
    private BuyHouse buyHouse;  
  
    public BuyHouseProxy(final BuyHouse buyHouse) {  
        this.buyHouse = buyHouse;  
    }  
  
    @Override  
    public void buyHouse() {  
        System.out.println("买房前准备");  
        buyHouse.buyHouse();  
        System.out.println("买房后装修");  
  
    }  
}  

第四步:编写测试类

public class HouseApp {  
  
    public static void main(String[] args) {  
        BuyHouse buyHouse = new BuyHouseImpl();  
        BuyHouseProxy buyHouseProxy = new BuyHouseProxy(buyHouse);  
        buyHouseProxy.buyHouse();  
    }  
}  

静态代理总结:

优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点:我们得为每一个服务创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。    

9.5 JDK动态代理(Dynamic Proxy)

在动态代理中我们不再需要再手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由JDK在运行时为我们动态的来创建。

第一步:创建服务类接口

代码和上例一样

第二步:实现服务接口

代码和上例一样

第三步:编写动态处理器

public class DynamicProxyHandler implements InvocationHandler {  
  
    private Object object;  
  
    public DynamicProxyHandler(final Object object) {  
        this.object = object;  
    }  
  
    @Override  
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
        System.out.println("买房前准备");  
        Object result = method.invoke(object, args);  
        System.out.println("买房后装修");  
        return result;  
    }  
} 

第四步:编写测试类

public class HouseApp {  
  
    public static void main(String[] args) {  
        BuyHouse buyHouse = new BuyHouseImpl();  
        BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(  
                BuyHouse.class.getClassLoader(),  
                new Class[]{BuyHouse.class},  
                new DynamicProxyHandler(buyHouse));  
        proxyBuyHouse.buyHouse();  
    }  
}  

 Proxy是所有动态生成的代理的共同的父类,这个类有一个静态方法Proxy.newProxyInstance(),接收三个参数:

  1. ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的
  2. Class<?>[] interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型
  3. InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法

JDK动态代理总结:

优点:相对于静态代理,动态代理大大减少了开发任务,同时减少了对业务接口的依赖,降低了耦合度。

缺点:Proxy是所有动态生成的代理的共同的父类,因此服务类必须是接口的形式,不能是普通类的形式,因为Java无法实现多继承。

9.6 CGLib动态代理(CGLib Proxy)

JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对final修饰的类进行代理。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。

Cglib子类代理实现方法:

(1)引入cglib的jar文件,asm的jar文件

(2)代理的类不能为final

(3)目标业务对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法

第一步:创建服务类

public class BuyHouse2 {  
  
    public void buyHouse() {  
        System.out.println("我要买房");  
    }  
}  

第二步:创建CGLIB代理类

public class CglibProxy implements MethodInterceptor {  
  
    private Object target;  
  
    public CglibProxy(Object target) {  
        this.target = target;  
    }  
  
    /** 
     *  给目标对象创建一个代理对象 
     * @return 代理对象 
     */  
    public Object getProxyInstance() {  
        //1.工具类  
        Enhancer enhancer = new Enhancer();  
        //2.设置父类  
        enhancer.setSuperclass(target.getClass());  
        //3.设置回调函数  
        enhancer.setCallback(this);  
        //4.创建子类(代理对象)  
        return enhancer.create();  
  
    }  
  
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {  
        System.out.println("买房前准备");  
        //执行目标对象的方法  
        Object result = method.invoke(target, args);  
        System.out.println("买房后装修");  
        return result;  
    }  
}

第三步:创建测试类

public class HouseApp {  
  
    public static void main(String[] args) {  
  
        BuyHouse2 target = new BuyHouse2();  
        CglibProxy cglibProxy = new CglibProxy(target);  
        BuyHouse2 buyHouseCglibProxy = (BuyHouse2) cglibProxy.getProxyInstance();  
        buyHouseCglibProxy.buyHouse();  
    }  
}  

CGLib代理总结: 

CGLib创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。

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

动态代理的原理: 使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。

代理对象决定是否以及何时将方法调用转到原始对象上

动态代理的方式

基于接口实现动态代理: JDK动态代理

基于继承实现动态代理: CglibJavassist动态代理

Spring aop 基于动态代理实现的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值