Bootstrap

数据结构常见面试题

常见的集合框架

引用数据类型一旦超过了它所表示的范围就会new一个新的对象

    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        System.out.println(a == b);//[-128,127]
        Integer a1 = 200;
        Integer b1 = 200;//源码里面,如果超过127则会new一个新的对象
        System.out.println(a1 == b1);
    }
//
true
false

泛型就是适用于很多类型,从代码来说就是对类型实现了参数化

关于擦除机制: 编译的时候所有的T都被替换成Object,

顺序表和链表/ArrayList和LinkedList/数组和链表的区别

ArrayList需要注意的点

1. 我们在创建ArrayList实例的时候,不设置参数,我们调用的是无参构造方法,里面是一个空的数组,没有分配内存.

2. 当第一次add的时候,我们才会分配大小为10的内存

3. 如果add方法检测出size大小等于数组大小,就会进行1.5倍的扩容

        ArrayList是基于动态数据实现的,它的逻辑地址和物理地址都是连续的.

优点:

        1> ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询

操作效率会比较高(在内存里是连着放的)。查询的时间复杂度是O(1)

缺点:

        1> 插入数据必须移动其他数据,最坏的情况下插入到第一个位置,时间复杂度是O(N)

        2> 删除数据也必须移动其他数据,最坏情况下删除第一个位置,时间复杂度是O(N)

        3> 1.5倍扩容之后,假设我们只用了扩容后的一点,会造成空间的浪费

        总结: 顺序表比较适合进行给指定下标查找的情况

LinkedList        

        LinkedList(双向链表) 基于链表的数据结构,链表是由一个个节点通过引用链接组成,每个结点分为value数据域和next引用域组成.

        优点:LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作,LinkedList 比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景。

        缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低。

        适用场景:

        1> 如果需要根据下标来查询元素就使用顺序表

        2> 如果经常进行元素的插入和删除操作,适合使用链表

        3> 如果容量固定,并且只会添加到尾部,不会引起扩容,优先采用 ArrayList。

不同点ArrayListLinkedList
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持O(1)不支持:O(N)
头插需要搬移元素,效率低O(N)只需修改引用的指向,时间复杂度为O(1)
插入空间不够时需要扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁

用过 ArrayList 吗?说一下它有什么特点?
1> ArarayList是基于动态数组实现的,当增加到一定程度的时候会自动1.5倍扩容

2> 我们在创建ArrayList实例的时候,不设置参数,我们调用的是无参构造方法,里面是一个空的数组,没有分配内存.

3> 当第一次add的时候,我们才会分配大小为10的内存

4> 高并发的情况下,线程不安全。多个线程同时操作 ArrayList,会引发不可预知的异常或错误。
5> 实现了ReandomAccess接口,可以支持随机访问

6>实现 了Cloneable接口,表面是可以克隆的        

有数组了为什么还要搞个 ArrayList 呢?

如果在不明确插入数据数量的情况下,普通数组就难以实现的,因为你不晓得初始化数组大小为多少,而ArrayList可以使用默认的大小,当达到一定程度后,会自动扩容.


栈和队列

栈: 后进先出的数据结构

栈、虚拟机栈、栈帧有什么区别呢?

虚拟机栈: 是Java虚拟机(JVM)的一个概念,每个线程运行的时候都会创建自己的虚拟机栈.

简而言之: JVM划分的一块内存.

栈帧: 是用于支持Java虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的一部分,每次发生方法的调用就会为该方法创建一个新的栈帧并压入栈顶.

简而言之: 方法调用的时候会在虚拟机给方法开辟一块内存.

栈: 是一种规定只能在一端进行插入或者删除的线性表,是一种后进先出的数据结构.

队列: 先进先出的线性表

栈和队列都可以使用数组和链表进行实现

二叉树

        一棵二叉树是结点的一个有限集合,该集合:

        1. 或者为空

        2. 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。

NLR:前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点--->根的左子树--->根的右子树。

LNR:中序遍历(Inorder Traversal)——根的左子树--->根节点--->根的右子树。

LRN:后序遍历(Postorder Traversal)——根的左子树--->根的右子树--->根节点。

优先级队列

        队列是一种先进先出的数据结构,而如果我们操作的数据带有优先级,我们出队的时候就会先出队优先级最高的元素.是由堆来进行实现的.

堆中某个节点的值总是不大于或不小于其父节点的值;
 

       堆分为大根堆和小根堆

        1> 小根堆

       根结点一定小于孩子结点->小根堆

        

        2> 大根堆
       根结点一定大于孩子结点->大根堆  

完全二叉树

优先级队列的扩容机制

如果容量小于64时,是按照oldCapacity的2倍方式扩容的

如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的

如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容

        

PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素

排序算法

 插入排序
1. 直接插入排序

        思想: 

  1. 从第二个元素开始,假设第一个元素已经是排好序的。
  2. 将当前元素与前面的元素进行比较,如果比它小就往前进行覆盖,然后将当前元素插入到适当的位置,使得插入后的部分依然是有序的。
  3. 重复这个过程,直到整个数组有序。

        使用i往前走,j用来遍历有序区间

        代码:

    public static void insertSort(int[] array) {
        //从第一个元素开始
        for (int i = 1; i < array.length ; i++) {
            int j = i - 1;
            //保存当前下标的值
            int tem = array[i];
            //加不加=会对稳定性产生影响,如果一个排序是稳定的就可以变成不稳定的,但是不稳定的不能变成稳定的)
            for (;  j >= 0 ; j--) {//j所在的区间是有序区间
                //如果前面的元素大于tem,就往前移动
                if(array[j] > tem) {
                    array[j+1] = array[j];//不用i的原因是j会一直改变,不一定就是j的前一个
                }else {
                    break;
                }
                //最后把元素放在前面
                //因为循环出来的条件表示必须j要小于0,所以直接传j会下标越界
            }
            array[j + 1] = tem;
        }
    }

        时间复杂度:O(n^2)

        稳定性: 稳定

2. 希尔排序   

        思想:

1. 分组,组内进行排序(直接插入排序)
2. 然后逐渐缩小组数

        定义一个gap,然后每一次按照gap来进行一次直接插入排序(越有序越快),再减小gap的大小

        代码:

    public static void shellsort(int[] array) {
        //分组
        int gap = array.length;
        while (gap > 0){
            gap /= 2;
            //每次分组进行一次直接插入排序
            shell(array,gap);
        }

    }
    public static void shell(int[] array,int gap) {
        //我们默认第一个元素是有序的
        for (int i = gap; i < array.length; i++) {
            int temp = array[i];
            //j下标等于i前面一个gap的下标值
            int j = i - gap;
            //然后逐渐和该组前面的数进行比较
            for(; j >= 0 ; j -= gap) {
                if(array[j] > temp) {
                    //往前移动一个gap的
                    array[j + gap] = array[j];
                }else {
                    break;
                }
            }
            array[j + gap] = temp;
        }
    }

        时间复杂度: O(n^0.25)~O(1.6n^0.25)

        稳定性: 不稳定

选择排序

3. 选择排序

        思想:

 1. 俩层循环, 外层循环相当于每一次提供一个比较数,内层循环就把比较数后面的数和比较数进行比较,如果比比较数还小,就记录下来下标的值

 2. 内层循环完之后,说明我们找到了这一趟最小值的下标,让其和外层循环所指的元素进行交换.

        1> 我们定义i,j

        2> 记录i下标为初始的min值下标

        3>  然后我们让j遍历i后面的元素,和min下标的值做比较,然后返回这一趟找到的最小值的下标

        4> 交换i和min下标的元素,每一趟都能找到这一趟最小的元素,然后放到前面去

        

        代码:

 public static void selectSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            int min = i;
            //让j为i+1
            for (int j = i + 1; j < array.length; j++) {
                if(array[min] > array[j]) {
                    min = j;//min保存的是每一趟最小值的下标
                }
            }
            int tmp = array[i];
            array[i] = array[min];
            array[min] = tmp;
        }
    }

        时间复杂度:O(n^2)

        稳定性:  不稳定

4. 堆排序

        思想: 升序排序,每一次建立大根堆,然后第一个元素和最后一个元素进行交换,然后对除了换过来的以外元素进行向下调整建立大根堆,这样每次找到的都是该趟的最大值.相反如果进行降序排序,利用的是小根堆.

1. 创建大根堆,把所有元素放在大根堆里面

2. 定义变量end,作为最后一个有效元素的下标

3. 交换0下标和end下标的值

4. 进行0下标到end下标的向下调整

5. end--;

        代码:

//堆排序
 private static void creatHeap(int[] array) {
        for (int parent = (array.length - 1 - 1) / 2; parent >= 0 ; parent--) {
            siftDown(array,parent,array.length);
        }
    }
    //TODO alt+enter
    private static void siftDown(int[] array,int parent,int length) {
        int child = 2 * parent + 1;
        while (child < length) {
            if(child + 1 < length && array[child] < array[child + 1]) {
                child++;
            }
            if(array[child] > array[parent]) {
                swap(array,child,parent);
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }
    public static void heapSort(int[] array) {
        //创建大根堆
        creatHeap(array);
        int end = array.length - 1;
        while (end > 0) {
            swap(array,0,end);
            siftDown(array,0,end);
            end--;
        }
    }

        时间复杂度: O(n * log(n))

        稳定性: 不稳定

交换排序

5. 冒泡排序

        思想:

内层循环每次每次从0开始,把小的交换到前面,大的换到后面,每一轮回,把该轮的最大值换到最后.

        代码:

  public static void bubbleSort(int[] array) {
        //i表示趟数
        for (int i = 0; i < array.length - 1; i++) {
            //j来比较每个数据的大小
            for (int j = 0; j < array.length - 1 ; j++) {

                if(array[j] > array[j + 1]) {
                    swap(array,j,j+1);
                }
            }
        }
    }

优化:
1. 减少j的遍历次数

2. 考虑极端情况,如果有序的话,我们就直接可以结束循环

public static void bubbleSort1(int[] array) {
        //i表示趟数
        for (int i = 0; i < array.length - 1; i++) {
            boolean flag = false;
            //j来比较每个数据的大小
//            for (int j = 0; j < array.length - 1 - i; j++) {
            for (int j = 0; j < array.length - 1 - i ; j++) {//优化1
                if(array[j] > array[j + 1]) {
                    swap(array,j,j+1);
                    flag = true;
                }
            }
            if (flag == false) {
                break;
            }
        }
    }

        时间复杂度:O(n^2)

        稳定性: 不稳定

6. 快速排序

        思想:

        hoare法

        首先我们把第一个元素作为基准值,让left和right分别指向数组的开头和结尾,然后我们让left去寻找比基准值大的数,让right去寻找比基准值小的数,然后我们把找到的下标丢到swap里面进行交换,然后继续上面的操作直到left和right相遇为止.然后我们更新基准值,把相遇位置和之前的基准位置进行交换,这样我们就以基准值作为标准,左边都是比基准值小的,右边都是比基准大的,然后我们再通过递归,在左右区间重复刚刚的操作.

        代码:

  public static void quickSort(int[] array) {
        quick(array,0,array.length - 1);
    }

    private static void quick(int[] array, int start, int end) {
        //走到头了
        if (start >= end) {
            return;
        }
        //找到基准值
        //相当于二叉树的前序遍历
        int pivot = partitionHoare(array,start,end);
        //调整左区间
        quick(array,start,pivot - 1);
        //调整右区间
        quick(array,pivot + 1,end);
    }

private static int partitionHoare(int[] array, int left, int right) {
        //记录第一个元素的值,把它作为基准
        int tmp = array[left];
        int  i  = left;
        while (left < right) {
            while (left < right && array[right] >= tmp) {
                right--;
            }
            while (left < right && array[left] <= tmp) {
                left++;
            }
            //此时左边找到了比基准大的值,右边找到了比基准小的值,那么我们就进行交换
            swap(array,right,left);
        }
        //然后我们把相遇的值和基准的值进行交换
        swap(array,i,left);
        //返回基准值的下标
        return left;
    }

        其他方法的思想:

        挖坑法:

        先让最左边作为基准值,定义left和right分别指向数组的开头和结尾,然后right一直向后找,去找到比基准值小的数,然后和覆盖left位置的数,同理left一直向前找找到比基准值大的数,然后覆盖right位置的值.当left和right相遇的时候,我们把原先的基准值填空到这个位置,这个位置作为新的基准值,进行下一轮递归.

        时间复杂度: O(n^2)

        稳定性: 不稳定

归并排序

7. 归并排序

        思想: 分治思想,先分解再合并

        1> 分成一个个子序列,让子序列有序

        2> 然后再进行合并,合并后任然保证有序

        即先使每个子序列有序,再使子序列段间有序。
        分成小区间,先让小区间有序,然后在进行合并的时候,通过比较然后让合并的大区间也有序

        

        代码:

  private static void merge(int[] array, int left,int mid, int right) {
        int s1 = left;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = right;
        int i = 0;//新数组的下标
        //创建一个新的数组
        int[] arryTemp = new int[right - left + 1];
        //俩端都有元素的时候
        while (s1 <= e1 && s2 <= e2) {
            //进行比较
            if(array[s1] <= array[s2]) {
                arryTemp[i++] = array[s1++];
            }else {
                arryTemp[i++] = array[s2++];
            }
        }
        //此时只有一端有元素,就直接放进数组即可
        while (s1 <= e1) {
            arryTemp[i++] = array[s1++];
        }
        while (s2 <= e2) {
            arryTemp[i++] = array[s2++];
        }
        //我们要把元素放进原来的数组里面
        for (int j = 0; j < arryTemp.length; j++) {
            array[j + left] = arryTemp[j];
        }
    }
    //非递归
    private static void mergeSortNor(int[] array) {
        //每组多少个数据
        int gap = 1;
        while (gap <= array.length) {
            for (int i = 0; i < array.length; i = i + 2 * gap) {//i每一次循环都在gap个元素的分组下进行排序
                //确定左右分组的边界值
                int left = i;
                int mid = left + gap - 1;
                int right = mid + gap;
                //防止越界操作,比如left是数组的最后一个元素,我们Mid和right就越界了
                if(mid >= array.length) {
                    mid = array.length - 1;
                }
                if(right >= array.length) {
                    right = array.length - 1;
                }
                //合并左右半区的元素
                merge(array,left,mid,right);
            }
            //增大每组的元素
            gap *=2;
        }

    }

        时间复杂度: O(nlgn)

        稳定性: 稳定

排序方法最好平均最坏空间复杂度稳定性
冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
插入排序O(n)O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
希尔排序O(n)O(n^1.3)O(n^0.15)O(1)不稳定
堆排序O(n * log(n))O(n * log(n))O(n * log(n))O(1)不稳定
快速排序O(n * log(n))O(n * log(n))O(n^2)O(log(n)) ~ O(n)不稳定
归并排序O(n * log(n))O(n * log(n))O(n * log(n))O(n)稳定

各种排序算法-CSDN博客

Map和Set

 二叉搜索树的概念,当我们中序遍历它的时候,就可以得到一个有序的从小到大的一组数.

AVL树: 也就是二叉平衡树,这个树是基于二叉搜索树的,它主要是来解决二叉搜索树出现单分支情况,而导致树的高度太高了.解决方式是左旋或者右旋(左右高度差大于等于2就进行).这样就把树的高度给平衡了.

红黑树: 它是基于AVL树进行改良的,给每个节点加上了颜色,然后可以减少左旋右旋的次数来平衡高度.

map和set整体框架介绍

Set是基于Map实现的

map和set的结构

        map里面存储的是key-val键值对(K一定是唯一的,不能重复.).比如:单词-该单词搜索的次数

        set里面只存储了key(key一定要唯一).比如:快速查找某个名字在不在通讯录中

TreeMap和HashMap的区别

Map底层结构TreeMapHashMap
底层结构红黑树哈希桶
插入/删除/查找时间复杂度O(logn)O(1)
是否有序关于Key有序无序
线程安全不安全不安全
插入/删除/查找区别需要对元素进行比较需要通过哈希函数计算哈希地址
比较和覆盖key必须能够进行比较,否则会抛出ClassCastException异常自定义类型需要覆盖和重写equals和hashCode方法
应用场景需要Key有序的情况下Key是否有序无所谓,主要是搜索的更快

TreeSet和HashSet的区别

Set底层结构TreeSetHashSet
底层结构红黑树哈希桶
插入/删除/查找时间复杂度O(logn)O(1)
是否有序关于key有序不一定有序
线程安全不安全不安全
插入/删除/查找区别按照红黑树的特性来进行插入和删除

1. 先计算key哈希地址

2. 进行插入和删除

比较和覆写Key必须能够进行比较,否则会抛出ClassCastException自定义类型需要覆盖hashCode方法
应用场景需要Key有序场景下Key是否有序并不关系,搜索速度够快

哈希表

        实现O(1)的查询

        不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

我们因此设计了哈希表.哈希表是一种由数组+链表+红黑树组成的数据结构.

        插入元素

        根据插入元素的关键码,以此函数计算出该元素的存储位置并把元素存放到这个位置上.

        搜索元素

        对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素进行比较,如果关键码相等,则搜索成功.

        哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
 

        hash冲突以及解决方式

          当不同关键码通过同一个哈希函数计算出来同一个hash地址,这个就是哈希冲突

        解决方式: 冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率
 

        1> 调节负载因子

          负载因子: a = 填入表中的元素个数 / 散列表的长度

        根据这个公式,如果我们需要降低负载因子的大小,我们只能增加散列表的长度,不能够减小元素个数.

         负载因子越大说明产生冲突的概率就越大, java内部负载因子长度为0.75,超过了这个值,就会resize散列表("扩容",但是并不是单纯就扩大数组长度,后面实现hash表的时候会具体解释)   

        2> 闭散列 

            也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。下面介绍找到空位置的方法

                1.线性探测

        从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

        缺点: 产生冲突的元素都聚在一起了

                2. 二次探测

         (i表示冲突次数)Hi = (H0 + i^2) % m,产生冲突的元素就分散开来了

        3> 哈希桶      

       哈希桶: 由数组+链表+红黑树所组成的数据结构

        注意: 数组长度大于64并且链表长度大于8的时候就会把链表树化成红黑树

        解决冲突:

   1. 每个桶的背后是另一个哈希表

        2. 每个桶的背后是一棵搜索树

哈希桶的底层实现->把结点放进数组

        进行扩容操作:

   array = Arrays.copyOf(array,2 * array.length);仅仅进行这样的扩容操作是否有问题呢?(面试题)

        答案是肯定的,因为我们扩容之后用的是扩容后的长度进行%,因此放的位置肯定不同,因此需要冲洗hash.

 步骤:

1.  先产生一个新的数组(大小是原来的二倍)

2. 遍历原来的数组元素(找每个下标是否连接链表)

3. 根据链表元素和新的哈希函数,我们计算出要插入的新数组的位置

4. 然后我们进行头插法

hash桶和hash表的区别

HashCode的应用--String进阶

池的概念:

 我们先得知道什么是池,池这个东西就是存着我们经常用的一种容器,是为了实现随去随用的,提高运行速度节省内存的容器.

字符串常量池: 字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable,java8里面,字符串常量池在堆里面.

这里注意,只要是""引起来的都会放到字符串常量池中.

                重点:

                1. 创建字符串的俩种方法在虚拟机和堆上是怎么创建的

                2. intern方法把字符串手动放进字符串常量池

intern

intern 是一个native方法(Native方法指:底层使用C++实现的,看不到其实现的源代码),该方法的作用是手动将创建的String对象添加到常量池中

不加intern

加了intern

反射机制

        反射是一种在运行状态下就能知道任意一个类的属性和方法(包括私有的)的一种机制.对于任意一个对象,都能够调用它的任意方法和属性,既然能拿到那么,我们就可以修改部分类型信息;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射(reflection)机制.

        关于运行时和编译时

        java程序中许多对象在运行时会出现两种类型:运行时类型(RTTI)和编译时类型,例如Person p = new Student();这句代码中p在编译时类型为Person,运行时类型为Student。程序需要在运行时发现对象和类的真实信息。而通过使用反射程序就能判断出该对象和类属于哪些类.

        这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制

        class类

        Java文件在被编译之后生成了.class文件 , JVM把.class解析为一个对象,这个对象就是Class类对象(java.lang.Class).

        1. 获得class对象 class.forName(类路径)

        方式

         1)Class.forName(“类的路径”);

         2)类名.class

         3)对象名.getClass()

         4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象

        2. 获取类的实例 类对象.getDeclaredConstrucetor().newInstance()

        3. 获取方法 实例.getMethod(方法名称)

        4. 调用方法 方法名.invoke(实例)

优点: 1)能够运行时动态获取类的实例,提高灵活性; 2)与动态编译结合 缺点: 1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。 解决方案: 1、通过setAccessible(true)关闭JDK的安全检查来提升反射速度; 2、多次创建一个类的实例时,有缓存会快很多 3、ReflectASM工具类,通过字节码生成的方式加快反射速度 2)相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)

        应用: JDBC连接数据库

        枚举类型是一种组织常量的数据类型

         优点: 把常量组织起来统一进行管理

        应用: 错误状态码,颜色的划分,状态机...

        枚举不能反射

        Lambda表达式

          (parameters) -> expression或者(parameters)->{statements;}        

      1> paramaters: 相当于方法中的形参列表(可以省去类型,jvm会自己推断出来).当只有一个推断类型的时候可以省去"()".

        2> -> :这个是被应用于的意思

        3> 方法体: 相当于函数式接口里面方法的实现,也就是方法体,可以返回值也可以不反回值.

函数式接口:一个接口有且只有一个抽象方法.一般都会在这个接口上声明@FunctionInterface.

lambda表达式的基本使用: 

Lambda可以理解为:Lambda就是匿名内部类的简化实际上是创建了一个类,实现了接口,重写了接口的方法 。

匿名内部类:是Java中一种特殊的内部类,它没有名字,通常用于实现接口或继承类的实例,并且只使用一次。它是一个简化类定义的方式,常用于临时的实现,特别是在需要传递一个类的实例给方法时。

使用匿名内部类和lambda表达式

        变量捕获:只要被匿名内部类里面的函数所使用的变量就不能被修改lambda也如此.

          因此在匿名内部类里面所使用的变量:1. 要么是常量 2.要么是没有被修改过的变量

  Lambda表达式最大的优点就是代码会变的非常的简洁,但是于此同时的缺点就是代码可读性差,补容易进行调试.

        通配符: ?

         ? 用于在泛型的使用,即为通配符

        ?表示可以接收所有的泛型类型,但是又不能够让用户随意修改。

小结:

        1. ?一般作为参数类型使用,T一般用于泛型类,泛型方法的定义来使用.

        2. ? 一般不晓得确切类型,因此只能读不能写(修改).T一般晓得确切类型,可以读和修改.

        3. 通配符的下界,不能进行读取数据,只能写入数据。通配符的上界,不能进行写入数据,只能进行读取数据。

;