2021秋招面试计算机基础总结 - 算法,数据结构,设计模式,Linux

算法

二叉树

计算二叉树的高度可以采用几种不同的算法。
算法一:采用后序遍历二叉树,结点最大栈长即为二叉树的高度;
算法二:层次遍历二叉树,最大层次即为二叉树的高度;
算法三:采用递归算法,求二叉树的高度。

class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;
    public TreeNode(int val) {
        this.val = val;
    }
}

public int height(TreeNode p) {  //p是一个二叉树根节点的引用
    if(p == null){
       return 0;
     }else{
       return 1 + max(height(p.left),height(p.right));
     }
}
满二叉树

对于一棵二叉树,如果每一个非叶子节点都存在左右子树,并且二叉树中所有的叶子节点都在同一层中,这样的二叉树称为满二叉树。

完全二叉树

对于一棵具有n个节点的二叉树按照层次编号,同时,左右子树按照先左后右编号,如果编号为ii的节点与同样深度的满二叉树中编号为ii的节点在二叉树中的位置完全相同,则这棵二叉树称为完全二叉树。

BST

若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

AVL 树(平衡二叉搜索树)

AVL树是带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,和红黑树相比,AVL树是严格的平衡二叉树,平衡条件必须满足:所有节点的左右子树高度差的绝对值不超过1。不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,由此我们可以知道AVL树适合用于插入与删除次数比较少,但查找多的情况

局限性
由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。当然,如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树。
应用
windows对进程地址空间的管理

红黑树

一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在**相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树**。

性质
1. 每个节点非红即黑;
2. 根节点是黑的;
3. 每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;
4. 如果一个节点是红的,那么它的两儿子都是黑的;
5. 对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点;
应用
TreeMap,HashMap的储存结构

排序算法

  • 分类

    • 排序算法可以分为内部排序外部排序,在内存中进行的排序称为内部排序,当要排序的数据量很大时无法全部拷贝到内存,需要使用外存进行排序,这种排序称为外部排序。
    • 内部排序包括比较排序和非比较排序,比较排序包括插入排序、选择排序、交换排序和归并排序,非比较排序包括计数排序、基数排序和桶排序。其中插入排序又包括直接插入排序和希尔排序,选择排序包括直接选择排序和堆排序,交换排序包括冒泡排序和快速排序。
  • 稳定性:排序前后两个相等的数相对位置不变,则算法稳定。

    • 不稳定:堆排序、快速排序、希尔排序、直接选择排序
    • 稳定:基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序的排序算法。

直接插入排序

一种稳定的排序,平均时间复杂度和最差时间复杂度均为 O(n²),当元素基本有序时的最好时间复杂度为O(n),空间复杂度为 O(1)。

  • 基本原理:
    每一趟将一个待排序的记录,按其关键字的大小插入到已经排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。适用于待排序记录较少或基本有序的情况。
public class InsertSort {
    public static int[] insertSort(int[] arr) {
        if(arr == null || arr.length < 2)
            return arr;

        int n = arr.length;
        for (int i = 1; i < n; i++) {
            int temp = arr[i];
            int k = i - 1;
            while(k >= 0 && arr[k] > temp)
                k--;
            //腾出位置插进去,要插的位置是 k + 1;
            for(int j = i ; j > k + 1; j--)
                arr[j] = arr[j-1];
            //插进去
            arr[k+1] = temp;
        }
        return arr;
    }
}

希尔排序

属于插入排序,又称缩小增量排序,是对直接插入排序的一种改进,并且是一种不稳定的排序,平均时间复杂度为O(n^1.3),最差时间复杂度为 O(n²),最好时间复杂度为 O(n),空间复杂度为 O(1)。

  • 基本原理是:
    把记录按下标的一定增量分组,对每组元素进行直接插入排序,每次排序后减小增量以重新分组,当增量减至 1 时,排序就完成了。适用于中等规模的数据量,对规模非常大的数据量不是最佳选择。
public class ShellSort {
    public static int[] shellSort(int arr[]) {
        if (arr == null || arr.length < 2) return arr;
        int n = arr.length;
        // 对每组间隔为 h的分组进行排序,刚开始 h = n / 2;
        for (int h = n / 2; h > 0; h /= 2) {
            //对各个局部分组进行插入排序
            for (int i = h; i < n; i++) {
                // 将arr[i] 插入到所在分组的正确位置上
                insertI(arr, h, i);
            }
     }
     return arr;
    }

    /**
     * 将arr[i]插入到所在分组的正确位置上
     * arr[i]] 所在的分组为 ... arr[i-2*h],arr[i-h], arr[i+h] ...
     */
    private static void insertI(int[] arr, int h, int i) {
        int temp = arr[i];
        int k;
        for (k = i - h; k > 0 && temp < arr[k]; k -= h) {
            arr[k + h] = arr[k];
        }
        arr[k + h] = temp;
    }
}

选择排序

一种不稳定的排序,任何情况下时间复杂度都是 O(n²),空间复杂度为 O(1)。

  • 基本工作原理:
    首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
public class SelectSort {
    public static int[] selectSort(int[] a) {
        int n = a.length;
        for (int i = 0; i < n - 1; i++) {
            int min = i;
            for (int j = i + 1; j < n; j++) {
                if(a[min] > a[j]) min = j;
            }
            //交换
            int temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        return a;
    }
}

堆排序

属于选择排序,是对直接选择排序的改进,并且是一种不稳定的排序,任何情况时间复杂度都为 O(nlogn),空间复杂度为 O(1)。初始化建堆O(n);重排序建堆O(nlogn)。

  • 基本原理是:将待排序记录看作完全二叉树。

    • 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆(升序使用大顶堆)
      • 大顶堆的特点:每个结点的值都大于或等于其左右孩子结点的值,我们把大顶堆构建完毕后根节点的值一定是最大的,然后把根节点和最后一个元素(也可以说最后一个节点)交换位置,那么末尾元素此时就是最大元素了
    • 将堆顶元素与末尾元素交换,将最大元素”沉”到数组末端
    • 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
  • PriorityQueue 默认最小元素在前

    • 通过 (a - b) -> b - a 改成降序输出
public class Head {
    // 堆排序
    public static int[] headSort(int[] arr) {
        int n = arr.length;
        //构建大顶堆
        for (int i = (n - 2) / 2; i >= 0; i--) {
            downAdjust(arr, i, n - 1);
        }
        //进行堆排序
        for (int i = n - 1; i >= 1; i--) {
            // 把堆顶元素与最后一个元素交换
            int temp = arr[i];
            arr[i] = arr[0];
            arr[0] = temp;
            // 把打乱的堆进行调整,恢复堆的特性
            downAdjust(arr, 0, i - 1);
        }
        return arr;
    }

        //下沉操作
    public static void downAdjust(int[] arr, int parent, int n) {
        //临时保存要下沉的元素
        int temp = arr[parent];
        //定位左孩子节点的位置
        int child = 2 * parent + 1;
        //开始下沉
        while (child <= n) {
            // 如果右孩子节点比左孩子大,则定位到右孩子
            if(child + 1 <= n && arr[child] < arr[child + 1])
                child++;
            // 如果孩子节点小于或等于父节点,则下沉结束
            if (arr[child] <= temp ) break;
            // 父节点进行下沉
            arr[parent] = arr[child];
            parent = child;
            child = 2 * parent + 1;
        }
        arr[parent] = temp;
    }
}

冒泡排序

冒泡排序属于交换排序,是一种不稳定的排序,平均时间复杂度和最坏时间复杂度均为 O(n²),当元素基本有序时的最好时间复杂度为O(n),空间复杂度为 O(1)。
基本原理是:它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行,直到没有再需要交换,也就是说该数列已经排序完成。

public class BubbleSort {
    public static int[] bubbleSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return arr;
        }
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            boolean flag = true;
            for (int j = 0; j < n -i - 1; j++) {
                if (arr[j + 1] < arr[j]) {
                    flag = false;
                    int t = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = t;
                }
            }
            //一趟下来是否发生位置交换
            if(false)
                break;
        }
        return arr;
    }
}

快速排序

是一种不稳定的排序,平均时间复杂度和最好时间复杂度均为 O(nlogn),当元素基本有序时的最坏时间复杂度为O(n²),空间复杂度为 O(logn)。

  • 基本原理是:
    从数组中选择一个元素,作为中轴元素,然后把数组中所有小于中轴元素的元素放在其左边,所有大于或等于中轴元素的元素放在其右边。
    (那么此时中轴元素所处的位置的是有序的。也就是说,我们无需再移动中轴元素的位置。)
    从中轴元素那里开始把大的数组切割成左右两个小的数组(两个数组都不包含中轴元素),接着通过递归的方式,让中轴元素左边的数组和右边的数组也重复同样的操作,直到数组的大小为1,此时每个元素都处于有序的位置。
public class QuickSort {
    public static int[] quickSort(int[] arr, int left, int right) {
        if (left < right) {
            //获取中轴元素所处的位置
            int mid = partition(arr, left, right);
            //进行分割
            arr = quickSort(arr, left, mid - 1);
            arr = quickSort(arr, mid + 1, right);
        }
        return arr;
    }

    private static int partition(int[] arr, int left, int right) {
        //选取中轴元素
        int pivot = arr[left];
        int i = left + 1;
        int j = right;
        while (true) {
            // 向右找到第一个小于等于 pivot 的元素位置
            while (i <= j && arr[i] <= pivot) i++;
            // 向左找到第一个大于等于 pivot 的元素位置
            while(i <= j && arr[j] >= pivot ) j--;
            if(i >= j)
                break;
            //交换两个元素的位置,使得左边的元素不大于pivot,右边的不小于pivot
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
        arr[left] = arr[j];
        // 使中轴元素处于有序的位置
        arr[j] = pivot;
        return j;
    }
}

归并排序

归并排序是基于归并操作的排序算法,是一种稳定的排序算法,任何情况时间复杂度都为O(nlogn),空间复杂度为 O(n)。

  • 基本原理
    应用分治法将待排序序列分成两部分,然后对两部分分别递归排序,最后进行合并,使用一个辅助空间并设定两个指针分别指向两个有序序列的起始元素,将指针对应的较小元素添加到辅助空间,重复该步骤到某一序列到达末尾,然后将另一序列剩余元素合并到辅助空间末尾。适用于数据量大且对稳定性有要求的情况。

递归:

public class MergeSort {
    // 归并排序
    public static int[] mergeSort(int[] arr, int left, int right) {
        // 如果 left == right,表示数组只有一个元素,则不用递归排序
        if (left < right) {
            // 把大的数组分隔成两个数组
            int mid = (left + right) / 2;
            // 对左半部分进行排序
            arr = mergeSort(arr, left, mid);
            // 对右半部分进行排序
            arr = mergeSort(arr, mid + 1, right);
            //进行合并
            merge(arr, left, mid, right);
        }
        return arr;
    }

    // 合并函数,把两个有序的数组合并起来
    // arr[left..mif]表示一个数组,arr[mid+1 .. right]表示一个数组
    private static void merge(int[] arr, int left, int mid, int right) {
        //先用一个临时数组把他们合并汇总起来
        int[] a = new int[right - left + 1];
        int i = left;
        int j = mid + 1;
        int k = 0;
        while (i <= mid && j <= right) {
            if (arr[i] < arr[j]) {
                a[k++] = arr[i++];
            } else {
                a[k++] = arr[j++];
            }
        }
        while(i <= mid) a[k++] = arr[i++];
        while(j <= right) a[k++] = arr[j++];
        // 把临时数组复制到原数组
        for (i = 0; i < k; i++) {
            arr[left++] = a[i];
        }
    }
}

非递归:

public class MergeSort {
    // 非递归式的归并排序
    public static int[] mergeSort(int[] arr) {
        int n = arr.length;
        // 子数组的大小分别为1,2,4,8...
        // 刚开始合并的数组大小是1,接着是2,接着4....
        for (int i = 1; i < n; i += i) {
            //进行数组进行划分
            int left = 0;
            int mid = left + i - 1;
            int right = mid + i;
            //进行合并,对数组大小为 i 的数组进行两两合并
            while (right < n) {
                // 合并函数和递归式的合并函数一样
                merge(arr, left, mid, right);
                left = right + 1;
                mid = left + i - 1;
                right = mid + i;
            }
            // 还有一些被遗漏的数组没合并,千万别忘了
            // 因为不可能每个字数组的大小都刚好为 i
            if (left < n && mid < n) {
                merge(arr, left, mid, n - 1);
            }
        }
        return arr;
    }
}

算法题

反转字符串

  • 使用字符串或转换成字符数组遍历,从后至前遍历并拼接
  • 利用StringBuffer的reverse()方法
  • 使用递归函数,终止条件是递归的字符串长度为1或者null时返回结果;否则就使用递归调用,参数是从当前字符串第二位开始的子字符串,返回结果需要在后面拼接上当前字符串的首个字符。

设计模式

设计模式的原则

开闭原则: 面向对象设计中最基础的设计原则,指一个软件实体(类、模块、方法等)应该对扩展开放,对修改关闭。它强调用抽象构建框架,用实现扩展细节,提高代码的可复用性和可维护性。例如在版本更新时尽量不修改源代码,但可以增加新功能。
单一职责原则: 一个类、接口或方法只负责一个职责,可以提高代码可读性和可维护性,降低代码复杂度以及变更引起的风险。
依赖倒置原则: 程序应该依赖于抽象类或接口,而不是具体的实现类。可以降低代码的耦合度,提高系统的稳定性。
接口隔离原则: 将不同功能定义在不同接口中实现接口隔离,避免了类依赖它不需要的接口,减少了接口之间依赖的冗余性和复杂性。
里氏替换原则: 对开闭原则的补充,规定了任何父类可以出现的地方子类都一定可以出现,可以约束继承泛滥,加键程序健壮性。
迪米特原则:也叫最少知道原则,每个模块对其他模块都要尽可能少的了解和依赖,可以降低代码耦合度。
合成/聚合原则: 尽量使用组合(has a)或聚合(contains a)而不是继承关系达到软件复用的目的,可以使系统更加灵活,降低耦合度。

设计模式的分类

创建型模式:提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象,这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。包括:工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式:通过类和接口之间的继承和引用实现创建复杂结构对象的功能。包括:适配器模式、桥接模式、过滤器模式、组合模式、装饰器模式、外观模式、享元模式、代理模式。
行为型模式:通过类之间不同的通信方式实现不同的行为方式。包括:责任链模式、命名模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板模式、访问者模式。

几种模式的示例

简单工厂模式

概念:由一个工厂对象创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数。
场景:适用于工厂类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则。
举例:

  • Calendar 类的 getInstance 方法,调用 createCalendar 方法根据不同的地区参数创建不同的日历对象。
  • Spring 中的 BeanFactory,根据传入一个唯一的标识来获得 Bean 实例。

工厂方法模式

概念: 针对不同的对象提供不同的工厂。定义一个创建对象的接口,让接口的实现类决定创建哪种对象,让类的实例化推迟到子类中进行。
场景:主要解决了产品扩展的问题,在简单工厂模式中如果产品种类变多,工厂的职责会越来越多,不便于维护。
举例:

  • Collection 接口中定义了一个抽象的 iterator 工厂方法,返回一个 Iterator 类的抽象产品。该方法通过 ArrayList 、HashMap 等具体工厂实现,返回 Itr、KeyIterator 等具体产品。
  • Spring 的 FactoryBean 接口的 getObject 方法。

抽象工厂模式

概念: 这个模式中的工厂类不单单可以创建一个对象,而是可以创建一组对象,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的对象。提供一个创建一系列相关对象的接口,无需指定它们的具体类。缺点是不方便扩展产品族,并且增加了系统的抽象性和理解难度。
场景:需要一组对象共同完成某种功能时,并且可能存在多组对象完成不同功能的情况。系统结构稳定,不会频繁的增加对象。
举例:java.sql.Connection 接口就是一个抽象工厂,其中包括很多抽象产品如 Statement、Blob、Savepoint 等。

单例模式

在任何情况下都只存在一个实例,构造方法必须是私有的(不能被继承)、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。
优点是内存中只有一个实例,减少了开销;缺点是没有抽象层,难以扩展、单例类的职责过重,与单一职责原则冲突。
举例:Spring 的 ApplicationContext 创建的 Bean 实例都是单例对象,还有 ServletContext、数据库连接池等也都是单例模式。

饿汉式:类加载时就创建单例对象,线程安全,但不管是否使用都创建对象。可能会浪费内存。
而且如果 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

public class Singleton {
    private Singleton(){}
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
}

懒汉式:在外部调用时才会加载,当有多个线程并行调用 getInstance() 的时候,就会创建多个实例,线程不安全,可以加锁保证线程安全,但效率低。因为在任何时候只能有一个线程调用 getInstance() 方法。

public class Singleton {
    private Singleton(){}
    private static Singleton instance;
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

双重检查锁:同步操作只需要在第一次调用时才被需要,使用 volatile 以及多重检查来减小锁范围,提升效率。

public class Singleton {
    private Singleton(){}
    private volatile static Singleton instance;
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
volatile作用
解决instance = new Singleton()这句的问题:非原子操作。
事实上在 JVM 中这句话大概做了下面 3 件事情。但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

使用volatile可以禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。

静态内部类:同时解决饿汉式的内存浪费问题和懒汉式的线程安全问题。
这种写法使用JVM本身机制保证了线程安全问题:JVM在执行类的初始化阶段,会获得一个可以同步锁,保证多个线程对同一个类的初始化时的线程安全;由于 StaticClass 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷

public class Singleton {
    private Singleton(){}
    public static Singleton getInstance() {
        return StaticClass.instance;
    }
    private static class StaticClass {
        private static final Singleton instance = new Singleton();
    }
}

枚举:《Effective Java》提倡的方式,不仅能避免线程安全问题,还能防止反序列化重新创建新的对象,也能防止反射破解单例的问题。

public enum Singleton {
    INSTANCE;
}

代理模式

代理模式属于结构型模式,为其他对象提供一种代理来控制对该对象的访问。优点是可以增强目标对象的功能,降低代码耦合度;缺点是请求处理速度变慢,增加系统复杂度。
静态代理:代理对象持有被代理对象的引用,调用代理对象方法时会调用被代理对象的方法,但是会增加其他逻辑。需要手动完成,在程序运行前就已经存在代理类的字节码文件,代理类和被代理类的关系在运行前就已确定。 缺点是一个代理类只能为一个目标服务。
动态代理:动态代理在程序运行时通过反射创建具体的代理类,代理类和被代理类的关系在运行前是不确定的。动态代理的适用性更强,主要分为 JDK 动态代理和 CGLib 动态代理。

  • JDK 代理:
    通过 Proxy 的 newProxyInstance 方法获得代理对象,需要三个参数:被代理类的接口、类加载器以及 InvocationHandler 对象,需要重写 InvocationHandler 接口的 invoke 方法指明代理逻辑。
  • CGLib 代理:
    通过 Enhancer 对象的 create 方法获取代理对象,需要通过 setSuperclass 方法设置代理类,以及 setCallback 方法指明代理逻辑(传入一个MethodInterceptor 接口的实现类,具体代理逻辑声明在 intercept 方法)。
    JDK 动态代理直接写字节码,而 CGLib 动态代理使用 ASM 框架写字节码, JDK 代理调用代理方法通过反射实现,而 GCLib 通过 FastClass 机制实现,为代理类和被代理类各生成一个类,该类为代理类和被代理类的方法分配一个 int 参数,调用方法时可以直接定位,效率更高。

装饰器模式

概念:在不改变原有对象的基础上将功能附加到对象,相比继承更加灵活。
场景:在不想增加很多子类的前提下扩展一个类的功能。
举例:java.io 包中,InputStream 通过 BufferedInputStream 增强为缓冲字节输入流。

和代理模式的区别:装饰器模式的关注点在于给对象动态添加方法,而动态代理更注重对象的访问控制。动态代理通常会在代理类中创建被代理对象的实例,而装饰器模式会将被装饰者作为构造方法的参数。

适配器模式

概念:作为两个不兼容接口之间的桥梁,使原本由于接口不兼容而不能一起工作的类可以一起工作。 缺点是过多使用适配器会让系统非常混乱,不易整体把握。

举例:

  • java.io 包中,InputStream 通过 InputStreamReader 转换为 Reader 字符输入流。
  • Spring MVC 中的 HandlerAdapter,由于 handler 有很多种形式,包括 Controller、HttpRequestHandler、Servlet 等,但调用方式又是确定的,因此需要适配器来进行处理,根据适配规则调用 handle 方法。
  • Arrays.asList 方法,将数组转换为对应的集合(不能使用修改集合的方法,因为返回的 ArrayList 是 Arrays 的一个内部类)。

和装饰器模式的区别:适配器模式没有层级关系,适配器和被适配者没有必然连续,满足 has-a 的关系,解决不兼容的问题,是一种后置考虑;装饰器模式具有层级关系,装饰器与被装饰者实现同一个接口,满足 is-a 的关系,注重覆盖和扩展,是一种前置考虑。

和代理模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。

策略模式

概念:定义了一系列算法并封装,之间可以互相替换。优点是算法可以自由切换,可以避免使用多重条件判断并且扩展性良好,缺点是策略类会增多并且所有策略类都需要对外暴露。
场景:主要解决在有多种算法相似的情况下,使用 if/else 所带来的难以维护。
举例:

  • 集合框架中常用的 Comparator 就是一个抽象策略,一个类通过实现该接口并重写 compare 方法成为具体策略类。
  • 线程池的拒绝策略。

模板模式

概念:使子类可以在不改变算法结构的情况下重新定义算法的某些步骤。优点是可以封装固定不变的部分,扩展可变的部分;缺点是每一个不同实现都需要一个子类维护,会增加类的数量。
场景:适用于抽取子类重复代码到公共父类。
举例:HttpServlet 定义了一套处理 HTTP 请求的模板,service 方法为模板方法,定义了处理HTTP请求的基本流程,doXXX 等方法为基本方法,根据请求方法的类型做相应的处理,子类可重写这些方法。

观察者模式

概念:也叫发布订阅模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。缺点是如果被观察者对象有很多的直接和间接观察者的话通知很耗时, 如果存在循环依赖的话可能导致系统崩溃,另外观察者无法知道目标对象具体是怎么发生变化的。
场景:主要解决一个对象状态改变给其他对象通知的问题。
举例:ServletContextListener 能够监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用。当 Servlet 容器启动 Web 应用时调用 contextInitialized 方法,终止时调用 contextDestroyed 方法。

Linux命令

部署项目用到的linux命令:
1.进入tomcat的bin目录 cd /data/apache-tomcat-6.0.39/bin
2.查看tomcat的进程 ps -ef | grep tomcat
3.杀死进程 kill -9 + 进程数
查看进程 2.1、ps -ef | grep xx 2.2、ps -aux | grep xxx(-aux显示所有状态)
查看端口:1、netstat -anp | grep 端口号(状态为LISTEN表示被占用)
4.启动项目 sh startup.sh
5.永久删除文件
rm -rf * 删除当前目录下的所有文件,这个命令很危险,应避免使用。所删除的文件,一般都不能恢复!;
rm -f 其中的,f参数 (f –force ) 忽略不存在的文件,不显示任何信息
6.复制文件 cp -Rf 原路径/ 目的路径/
7.压缩文件夹
解压:tar zxvf FileName.tar.gz
压缩:tar zcvf FileName.tar.gz DirName
8.解压(安装zip命令)* unzip 压缩包
9.移动 mv +路径/文件 +要移到的路径
scp -r 目录名 远程计算机用户名@远程计算机的ip:远程计算机存放该目录的路径
10.切换用户 su 用户名

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!