《java编程语言 面经 面试题 研二期间整理的面试题 》

面试题1:为何linkList插入删除效率比arrayList高?

  arrayList底层是一个数组,底层的arraycopy方法,只要是插入一个元素,其后的元素就会向后移动一位,虽然arraycoy是一个本地方法,效率非常高,但频繁的插入,每次后面的元素都需要拷贝一遍,效率变低了,特别是在头位置插入元素时。LinkedList是一个双向的链表,它的插入只是修改相邻元素next和previous引用。

  在ArrayList的前面或中间插入数据时,你必须将其后的所有数据相应的后移,这样必然要花费较多时间,所以,当你的操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能; 而访问链表中的某个元素时,就必须从链表的一端开始沿着连接方向一个一个元素地去查找,直到找到所需的元素为止,所以,当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
  除了底层数组和链表数据结构本身插入数据特点外,arrayList的扩容过程也会影响插入效率
知识补充
ArrayList延伸:ArrayList是经常会被用到的,一般情况下,使用的时候会像这样进行声明:
List arrayList = new ArrayList();
  如果像上面这样使用默认的构造方法,初始容量被设置为10。当ArrayList中的元素超过10个以后,会重新分配内存空间,使数组的大小增长到16。可以通过调试看到动态增长的数量变化:10->16->25->38->58->88->…
也可以使用下面的方式进行声明:
List arrayList = new ArrayList(4);
  将ArrayList的默认容量设置为4。当ArrayList中的元素超过4个以后,会重新分配内存空间,使数组的大小增长到7。可以通过调试看到动态增长的数量变化:4->7->11->17->26->…

那么容量变化的规则是什么呢?公式:((旧容量 * 3) / 2) + 1

面试题2:hashMap存储机制、扩容如何实现?

  首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
  当我们想插入一个键值对时执行put方法,首先会根据key的值获得该键值对应该存放在哈希表中哪个位置,然后判断该数组这个位置上有没有相同的key的键值对,如果有则替换value,如果没有就插入该位置,并且将Entry中的next执行上一个本来存在这个位置的Entry。

// 存储时:
int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值
int index = hash % Entry[].length;

  当要执行get方法时,也是先通过key的哈希值通过获得这个值对应哈希表中的索引,然后遍历链表,得到需要的键值对。key的哈希值确定键值对数组index:hashcode % table.length取模。
补充:哈希表介绍
  首先由于,数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。哈希表综合了两种数据结构,是一种链表数组。
        在这里插入图片描述
       在这里插入图片描述
  从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
  扩容实现原理:当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能

面试题3:hash冲突有那些解决办法?(没记住)

当关键字集很大时,可能会有不同的键的键值对会映射到哈希表的同一个地址上。当发生冲突时,解决办法有:
1 开放定址法,这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi
2. 再哈希法,这种方法是同时构造多个不同的哈希函数: Hi=RH1(key) i=1,2,…,k,当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。
3. 链地址法,这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。hashmap解决冲突 链地址法
4.建立公共溢出区,将哈希表分成基本表和溢出表两部分,凡是与基本表冲突的数据都放到溢出表中。

  重新构造哈希函数,也是一种方法:
1、直接定址法,哈希函数为关键字的线性函数,H(key) = key, 此法仅适合于:地址集合的大小 = 关键字集合的大小
2、平方取中法, 以关键字的平方值的中间几位作为存储地址。求“关键字的平方值” 的目的是“扩大差别” ,同时平方值的中间各位又能受到整个关键字中各位的影响。 此法适于:关键字中的每一位都有某些数字重复出现频度很高的现象
3、折叠法,将关键字分割成若干部分,然后取它们的叠加和为哈希地址。两种叠加处理的方法:移位叠加:将分 割后的几部分低位对齐相加;间界叠加:从一端沿分割界来回折叠,然后对齐相加。此法适于:关键字的数字位数特别多
4、随机数法, 设定哈希函数为:H(key) = Random(key)其中,Random 为伪随机函数,此法适于:对长度不等的关键字构造哈希函数
5、除留余数法,设定哈希函数为:H(key) = key MOD p ( p≤m ),其中, m为表长,p 为不大于 m 的素数,或是不含 20 以下的质因子

面试题4:ArrayList、Vector、HashMap、HashSet的默认初始容量、加载因子、扩容增量

  List 元素是有序的、可重复:ArrayList、Vector默认初始容量为10  Set(集) 元素无序的、不可重复:

  1. Vector:线程安全,但速度慢,底层数据结构是数组结构,加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容。扩容增量:原容量的 1倍, 如 Vector的容量为10,一次扩容后是容量为20
  2. ArrayList:线程不安全,查询速度快,底层数据结构是数组结构,扩容增量:原容量的 0.5倍+1。如 ArrayList的容量为10,一次扩容后是容量为16
  3. HashSet:线程不安全,存取速度快,底层实现是一个HashMap(保存数据),实现Set接口,默认初始容量为16(为何是16,见下方对HashMap的描述),加载因子为0.75:即当
    元素个数 超过 容量长度的0.75倍 时,进行扩容, 扩容增量:原容量的 1 倍,如
    HashSet的容量为16,一次扩容后是容量为32。
  4. HashMap:默认初始容量为16,(为何是16:16是2^4,可以提高查询效率,另外,32=16<<1 -->至于详细的原因可另行分析,或分析源代码),加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容,扩容增量:原容量的 1 倍,如 HashSet的容量为16,一次扩容后是容量为32

面试题5、并发的HashMap为什么会引起死循环?

  多线程会导致HashMap的Entry链表形成环形数据结构。
参考 http://blog.csdn.net/zhuqiuhui/article/details/51849692

面试题6、stack arraylist区别

  stack 由VECTOR实现 先进后出 线程安全 删除元素只能从栈顶出栈删除 。Arraylist 线程不安全 可以删除任意节点的数据

面试题7、TreeMap实现原理(难)

  红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长
首先需要知道红黑树的基本性质:5条

  1. 每个节点都只能是红色或者黑色
  2. 根节点是黑色
  3. 每个叶节点(NIL节点,空节点)是黑色的。
  4. 也就是说在一条路径上不能出现相邻的两个红色结点。
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

对于新节点的插入有如下三个关键地方:

  1. 插入新节点总是红色节点
  2. 如果插入节点的父节点是黑色, 能维持性质
  3. 如果插入节点的父节点是红色, 破坏了性质. 故插入算法就是通过重新着色或旋转, 来维持性质

  TreeMap put()方法实现分析:分为两个步骤,第一:构建排序二叉树,第二:平衡二叉树。平衡二叉树过程包括三大基本操作:左旋、右旋、着色。构建排序二叉树比较简单,只需要将插入结点New不断从根结点比较,根据二叉搜索树的左子树<根结点<右子树结点,来最终决定插入位置。平衡二叉树包括:左旋、右旋、着色。
  左旋、右旋、着色操作需要对红黑二叉树进行当前的状况根据插入的新节点进行判断,通常会有1、插入的节点就是根结点,那么此时直接着色即可。2、插入的结点父节点P是黑色,那么也直接着色即可。3、若父节点P为红色,叔父节点存在并且也为红色,那么把插入结点N的父节点的父节点G着色成红色,父节点P和叔结点U着色成黑色。4、若父节点P为红色,叔父节点U为黑色或者缺少,且新增节点N为P节点的右孩子,那么要进行左旋操作,就是将新增节点(N)当做其父节点(P),将其父节点P当做新增节点(N)的左子节点。即:G.left —> N ,N.left —> P。5、父节点P为红色,叔父节点U为黑色或者缺少,P.right —> G、G.parent —> P,

当P为红色,Uncle为黑色,插入的N为左子树结点,要进行右旋。此时有P.right = G; G.parent = P;
在这里插入图片描述
在这里插入图片描述
当插入的结点N是右子树结点,父节点N为红色,Uncle为黑色,
在这里插入图片描述
此时要插入的N由右子树,改成插入成左子树。执行G.left = N; N.left = P;,然后此时将原先的P视为要插入的新结点N,原先的N视为P,进行当插入结点为左子树,即上面一种情形的操作。
在这里插入图片描述
当父节点和Uncle结点是红色,只需要把P和Uncle涂黑即可。
在这里插入图片描述
  TreeMap delete()方法这里的删除节点并不是直接删除,而是通过走了“弯路”通过一种捷径来删除的:找到被删除的节点D的子节点C,用C来替代D,不是直接删除D,因为D被C替代了,直接删除C即可。 红黑树删除节点同样会分成几种情况,这里是按照待删除节点不要考虑颜色冲突,而是根据有几个儿子的情况来进行分类:

  1. 没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
  2. 只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
  3. 有两个儿子。这时候就可以把被删除元素的右支的最小节点(被删除元素右支的最左边的节点)和被删除元素互换,我们把被删除元素右支的最左边的节点称之为后继节点(后继元素),然后在根据情况1或者情况2进行操作。如图:
    在这里插入图片描述
    在这里插入图片描述

面试题8、 ConcurrentHashMap实现原理

  ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。从ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的Segment,根据key.hashCode()来决定把key放到哪个Segment中,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中.
  通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。ConcurrentHashMap get读取数据时不上锁 更新是上锁(分段锁) 读取更新可同时发生 hashtable就不能 。
ConcurrentHashMap把整个Map分为16个Segment(每个 Segment 对象守护每个散列映射表的若干个桶,每个桶是由若干个 HashEntry 对象链接起来的链表。),可以提供相同的线程安全,同时16个写线程并发执行(写线程才需要锁定,而读线程几乎不受限制).
        在这里插入图片描述

面试题9、写代码使得分别出现StackOverflowError和OutOfMemoryError

  StackOverflowError堆栈溢出错误一般是递归调用嘛。下面的代码就可以出现:

public class StackOverflowTest {
    public static void main(String[] args) {
        method();
    }
    public static void method(){
        for(;;)
            method();
    }
}

OutOfMemoryError 申请较多的内存空间没有释放:

import java.util.ArrayList;
import java.util.List;
public class OutOfMemoryTest {
    public static void main(String[] args){
        List list=new ArrayList();
        for(;;){
            int[] tmp=new int[1000000];
            list.add(tmp);
        }
    }
}

面试题10、volatile,synchronized, 显式锁,原子变量这些同步手段如何保证可见性

  可见性底层的实现是通过加内存屏障实现的:1. 写变量后加写屏障,保证CPU写缓冲区的值强制刷新回主内存。2. 读变量之前加读屏障,使缓存失效,从而强制从主内存读取变量最新值。
可见性底层的原理是相当于破坏了CPU高速缓存。
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
i = i + 1;
  当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。
比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
  可见性底层的原理是相当于破坏了CPU高速缓存

面试题11、多线程编程中可能会出现什么问题?(力争满分题)

  多线程带来的风险主要包括安全性问题、活跃性问题还有性能问题。
  安全性问题最关键在于对共享、可变状态访问操作进行管理。对共享状态的每一步操作都需要考虑可见性、原子性、有序性问题。可见性问题可以用volatile,synchronized, 显式锁,原子变量这些同步手段如何保证共享状态的可见性。原子性问题最简单的就是用原子变量进行原子操作,用原子变量来处理一些竞态条件。(注意:volatile并不能保证原子性)。如果访问共享可变数据用同步方法保证安全,还可以用线程封闭技术,线程封闭可以用ThreadLocal类实现。
  活跃性entity主要包括可能出现的死锁、饥饿、活锁问题。发生死锁的原因是两个线程A和B试图以不同的顺序来获得相同的锁。如果按照相同的顺序来请求锁,就不会出现循环的加锁依赖性,因此也就不会发生死锁。如果一个线程的优先级不当(线程优先级明显高于其他线程)或者在持有锁时发生无线循环、无限等待某个资源,这就会导致此线程长期占用CPU时钟周期,其他需要这个锁的线程无法得到这个锁,因此就发生了饥饿。尽管不会阻塞线程(绅士相遇都相互让路了,可以各自继续赶路),但也不能执行到预期结果。通过等待随机长度的时间和回退可以有效地避免活锁的发生
  性能问题可能会出现服务时间不长、响应不灵敏、吞吐量过低、资源消耗低、可伸缩性低。为了提高性能,需要尽可能的减少上下文的切换,通过高效使用锁提升性能。缩小锁的范围(快进快出),减少锁粒度包括锁分解和锁分段技术实现。避免出现热点域:某个锁由保护的数据缺被很多常用操作访问(例如Map的size) 。解决:ConcurrentHashMap为每个分段都维护一个独立的size计数,并通过每个分段的锁来维护总size

面试题12 、讲讲ThreadLocal如何实现防止自己的变量被其它线程篡改

  ThreadLocal解决多线程的并发问题的思路很简单:就是为每一个线程维护一个副本变量,从而让线程拥有私有的资源,就不再需要去竞争进程中的资源。每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。首先,ThreadLocal它是一个数据结构,有点像HashMap,可以保存"key : value"键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。当你往ThreadLocal中执行set方法存入数据时,实际上是存储在当前线程的ThreadLoalMap中。在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。
      在这里插入图片描述
  没有链表结构,那发生hash冲突了怎么办?在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:
1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上;
2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

  ThreadLocal可能导致内存泄漏,为什么?当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露
  如何避免内存泄露?既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

面试题13、显示锁有哪些优点

  当内置锁满足不了需求时,作为一种可高端的选择。reentrant 英 [ri:'entrənt] 。ReentrantLock实现了Lock接口,所有加锁和解锁的方法都是显式的。内置锁能很好的工作,但在功能上仍存在一些局限性。例如:无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限等待下去。内置锁在获取的过程中无法中断。内置锁必须在获取该锁的代码块中释放,无法实现非阻塞结构的加锁规则,很难实现带有时间限制的操作
  显式锁优点:1、轮询与定时 2、锁获取操作可中断 3、非块结构加锁(可以不要像内置锁获取释放都基于代码块)

面试题14、Redis 待完善

  Redis 是一个基于内存的高性能key-value nosql数据库。先存在内存中,会根据一定的策略持久化到磁盘。即使断点也不会丢失数据。(如何做持久化 默认redis是会以快照的形式将数据持久化到磁盘的(一个二进 制文件,dump.rdb,这个文件名字可以指定)) 可存放经常会用到又不怎么修改的数据
  以下是支持的类型:String 、lists、Sets 、Sorted Set、 hash。此外单个value的最大限制是1GB,不像 memcached只能保存1MB的数据
  Redis是单进程单线程的 ,redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。redis支持主从的模式。原则:Master会将数据同步到slave,而slave不会将数据同步到master。Slave启动时会连接master来同步数据。这是一个典型的分布式读写分离模型。我们可以利用master来插入数据,slave提供检索服务。这样可以有效减少单个机器的并发访问数量

面试题15、谈一下Spring MVC接受一个请求整体流程(网易杭研)(没记住)

在这里插入图片描述

面试题16、类加载机制(没记住)

参考:https://blog.csdn.net/weixin_41262453/article/details/87427509
  JVM类加载过程包括了加载、验证、准备、解析、初始化五个阶段。加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始

  • 加载阶段:加载是“类加载”过程的一个阶段。在加载阶段,虚拟机需要完成3件事情:1)通过一个类的全限定名来获取定义此类的二进制字节流。2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证阶段:验证阶段目的是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求,验证字节流是否符合Class文件格式的规范,同时还会验证字节码、元数据、符号引用验证
  • 准备阶段是正式为类变量分配内存设置类变量初始化值的阶段,这些变量所使用的内存都将在方法区中进行分配。
  • 解析阶段是虚拟机将常量池的符号引用直接替换为直接引用的过程
  • 初始化阶段是执行类构造器< clinit>()方法的过程

面试题17、双亲委派机制

  双亲委派模型是一种设计模式(代理模式),类加载过程按照启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器这种层次关系进行加载叫双亲委派模型。我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。
            在这里插入图片描述
  双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器:Extension ClassLoader,该加载器由sun.misc.LauncherKaTeX parse error: Undefined control sequence: \jre at position 26: …ader实现,它负责加载JDK\̲j̲r̲e̲\lib\ext目录中,或者由…AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
   1)在执行非置信代码之前,自动验证数字签名。
   2)动态地创建符合用户特定需要的定制化构建类。
   3)从特定的场所取得java class,例如数据库中和网络中。

JVM如何确立每个类在JVM的唯一性
  在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识。因此,如 果一个名为Pg的包中,有一个名为Cl的类,被类加载器KlassLoader的一个实例kl1加载,Cl的实例,即C1.class在JVM中表示为 (Cl, Pg, kl1)。

为什么使用双亲委派
  比如 java.lang.String 这个类,这个是jdk提供的类, 如果我们自定义个 包名:java.lang 然后在里面创建一个String 类, 当我在用String类的时候,根据前面所说,是由bootstrap级的loader 来进行加载的,这个时候它发现其实已经加载过了jdk的String了,那么就不会去加载自定义的String了,防止了重复加载 也加大了安全性

面试题18、海量日志中提取访问次数前100的IP

  如果有1000万条的IP,我可以先创建1000个小文件夹,读取这1000万IP的日志,每个文件夹中存入1万条数据,然后分别对1000个文件的数据进行排序,1000个文件的排序结果中再取前100个。
参考 https://c610367182.iteye.com/blog/1965382
首先可以先创建1000个文件,和1000个输出流 ,输出流用bwMap存着方便取,public final Map<Integer,BufferedWriter> bwMap = new HashMap<Integer,BufferedWriter>();
然后可以在排序时用TreeMap,但是要对IP对象进行重写Comparable接口,然后如果王一个TreeMap中放1万个IP对象,会自动排序
              在这里插入图片描述
Comparable接口重写compareTo方法返回值含义:
  返回值及比较规则:
  1、返回负值---->小于
  2、返回零------>等于
  3、返回正值---->大于

面试题19 、快速排序、归并排序(难)、堆排序

https://blog.csdn.net/weixin_41262453/article/details/88802924#_71
快速排序:时间复杂度O(log2n),跳跃式交换,因此是不稳定的排序算法
归并排序:归并排序的最好,最坏,平均时间复杂度均为O(nlogn),高效稳定的排序算法。
堆排序:时间效率为O(n*log2n)。
直接排序:循环从data[i]开始,进行对后面的所有元素比较,若想从小到大排序,则进行比较将这轮最小的放data[i]位置。直接选择排序是每次直接选出最小/大值放在data[i]上。
在这里插入图片描述
堆排序:堆排序关键在于通过完全二叉树的结构保存每次比较的结构,相比直接排序减少了比较次数。首先将数组视为一颗树,最后一个叶结点就是array[length - 1],最后一个非叶子节点应该是(length - 1-1)/2,建堆过程是从从最后一个非叶子节点开始,比较它和两个子节点大小,交换它们的值,保证最大值在最上面(大堆排序),堆排序就是这么不断建堆的过程。
在这里插入图片描述
快速排序:说白了就是给基准数据找其正确索引位置的过程,每次把第一个数组的第一个数据作为基准数据,

  • ①先从队尾开始向前扫描且当low < high时,如果a[high] > tmp,则high–,但如果a[high] < tmp,则将high的值赋值给low,即arr[low] = a[high],同时要转换数组扫描的方式,即需要从队首开始向队尾进行扫描了
  • ②同理,当从队首开始向队尾进行扫描时,如果a[low] < tmp,则low++,但如果a[low] > tmp了,则就需要将low位置的值赋值给high位置,即arr[low] = arr[high],同时将数组扫描方式换为由队尾向队首进行扫描.
  • ③不断重复①和②,知道low>=high时(其实是low=high),low或high的位置就是该基准数据在数组中的正确索引位置.

具体的实现:假设最开始的基准数据为数组第一个元素23,则首先用一个临时变量去存储基准数据,即tmp=23;然后分别从数组的两端扫描数组,设两个指示标志:low指向起始位置,high指向末尾.
在这里插入图片描述
首先从后半部分开始,如果扫描到的值大于基准数据就让high减1,如果发现有元素比该基准数据的值小(如上图中18<=tmp),就将high位置的值赋值给low位置 ,结果如下:
在这里插入图片描述
然后开始从前往后扫描,如果扫描到的值小于基准数据就让low加1,如果发现有元素大于基准数据的值(如上图46=>tmp),就再将low位置的值赋值给high位置的值,指针移动并且数据交换后的结果如下:
在这里插入图片描述
然后再开始从后向前扫描,原理同上,发现上图11<=tmp,则将low位置的值赋值给high位置的值,结果如下:
在这里插入图片描述
然后再开始从前往后遍历,直到low=high结束循环,此时low或high的下标就是基准数据23在该数组中的正确索引位置.如下图所示.
在这里插入图片描述
代码实现:

package com.nrsc.sort;

public class QuickSort {
    public static void main(String[] args) {
        int[] arr = { 49, 38, 65, 97, 23, 22, 76, 1, 5, 8, 2, 0, -1, 22 };
        quickSort(arr, 0, arr.length - 1);
        System.out.println("排序后:");
        for (int i : arr) {
            System.out.println(i);
        }
    }

    private static void quickSort(int[] arr, int low, int high) {

        if (low < high) {
            // 找寻基准数据的正确索引
            int index = getIndex(arr, low, high);

            // 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
            quickSort(arr, 0, index - 1);
            quickSort(arr, index + 1, high);
        }

    }

    private static int getIndex(int[] arr, int low, int high) {
        // 基准数据
        int tmp = arr[low];
        while (low < high) {
            // 当队尾的元素大于等于基准数据时,向前挪动high指针
            while (low < high && arr[high] >= tmp) {
                high--;
            }
            // 如果队尾元素小于tmp了,需要将其赋值给low
            arr[low] = arr[high];
            // 当队首元素小于等于tmp时,向前挪动low指针
            while (low < high && arr[low] <= tmp) {
                low++;
            }
            // 当队首元素大于tmp时,需要将其赋值给high
            arr[high] = arr[low];

        }
        // 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
        // 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
        arr[low] = tmp;
        return low; // 返回tmp的正确位置
    }
}

归并排序:是利用归并的思想实现的排序方法。该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)
在这里插入图片描述
合并相邻有序子序列:再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
     在这里插入图片描述
代码实现:

import java.util.Arrays;

/**
 * Created by chengxiao on 2016/12/8.
 */
public class MergeSort {
    public static void main(String []args){
        int []arr = {9,8,7,6,5,4,3,2,1};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void sort(int []arr){
        int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        sort(arr,0,arr.length-1,temp);
    }
    private static void sort(int[] arr,int left,int right,int []temp){
        if(left<right){
            int mid = (left+right)/2;
            sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
            sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
            merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
        }
    }
    private static void merge(int[] arr,int left,int mid,int right,int[] temp){
        int i = left;//左序列指针
        int j = mid+1;//右序列指针
        int t = 0;//临时数组指针
        while (i<=mid && j<=right){
            if(arr[i]<=arr[j]){
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        while(i<=mid){//将左边剩余元素填充进temp中
            temp[t++] = arr[i++];
        }
        while(j<=right){//将右序列剩余元素填充进temp中
            temp[t++] = arr[j++];
        }
        t = 0;
        //将temp中的元素全部拷贝到原数组中
        while(left <= right){
            arr[left++] = temp[t++];
        }
    }
}

面试题20、说一下Spring AOP实现原理(5月28日 华为面试官问)

描述一下Spring AOP?
  Spring AOP(Aspect Oriented Programming,面向切面编程)是OOPs(面向对象编程)的补充,它也提供了模块化。在面向对象编程中,关键的单元是对象,AOP的关键单元是切面,或者说关注点(可以简单地理解为你程序中的独立模块)。一些切面可能有集中的代码,但是有些可能被分散或者混杂在一起,例如日志或者事务。这些分散的切面被称为横切关注点。一个横切关注点是一个可以影响到整个应用的关注点,而且应该被尽量地集中到代码的一个地方,例如事务管理、权限、日志、安全等。
  AOP让你可以使用简单可插拔的配置,在实际逻辑执行之前、之后或周围动态添加横切关注点。这让代码在当下和将来都变得易于维护。如果你是使用XML来使用切面的话,要添加或删除关注点,你不用重新编译完整的源代码,而仅仅需要修改配置文件就可以了。
  Spring AOP通过以下两种方式来使用。但是最广泛使用的方式是Spring AspectJ 注解风格(Spring AspectJ Annotation Style)

  • 使用AspectJ 注解风格
  • 使用Spring XML 配置风格

在Spring AOP中关注点(concern)和横切关注点(cross-cutting concern)有什么不同?
  关注点是我们想在应用的模块中实现的行为。关注点可以被定义为:我们想实现以解决特定业务问题的方法。比如,在所有电子商务应用中,不同的关注点(或者模块)可能是库存管理、航运管理、用户管理等。
  横切关注点是贯穿整个应用程序的关注点。像日志、安全和数据转换,它们在应用的每一个模块都是必须的,所以他们是一种横切关注点。
AOP有哪些可用的实现?
  基于Java的主要AOP实现有:

  • AspectJ
  • Spring AOP
  • JBoss AOP

Spring中有哪些不同的通知类型(advice types)?
通知(advice)是你在你的程序中想要应用在其他模块中的横切关注点的实现。Advice主要有以下5种类型:

  • 前置通知(Before Advice): 在连接点之前执行的Advice,不过除非它抛出异常,否则没有能力中断执行流。使用 @Before
    注解使用这个Advice。

  • 返回之后通知(After Retuning Advice):
    在连接点正常结束之后执行的Advice。例如,如果一个方法没有抛出异常正常返回。通过 @AfterReturning 关注使用它。

  • 抛出(异常)后执行通知(After Throwing Advice):
    如果一个方法通过抛出异常来退出的话,这个Advice就会被执行。通用 @AfterThrowing 注解来使用。

  • 后置通知(After Advice): 无论连接点是通过什么方式退出的(正常返回或者抛出异常)都会执行在结束后执行这些Advice。通过
    @After 注解使用。

  • 围绕通知(Around Advice): 围绕连接点执行的Advice,就你一个方法调用。这是最强大的Advice。通过 @Around
    注解使用

Spring AOP 代理是什么?
  代理是使用非常广泛的设计模式。简单来说,代理是一个看其他像另一个对象的对象,但它添加了一些特殊的功能。Spring AOP是基于代理实现的。AOP 代理是一个由 AOP 框架创建的用于在运行时实现切面协议的对象。Spring AOP默认为 AOP 代理使用标准的 JDK 动态代理。这使得任何接口(或者接口的集合)可以被代理。Spring AOP 也可以使用 CGLIB 代理。这对代理类而不是接口是必须的。如果业务对象没有实现任何接口那么默认使用CGLIB。

引介(Introduction)是什么?
  引介让一个切面可以声明被通知的对象实现了任何他们没有真正实现的额外接口,而且为这些对象提供接口的实现使用 @DeclareParaents 注解来生成一个引介。
连接点(Joint Point)和切入点(Point Cut)是什么?
  连接点是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。举例来说,所有定义在你的 EmpoyeeManager 接口中的方法都可以被认为是一个连接点,如果你在这些方法上使用横切关注点的话。
  切入点(切入点)是一个匹配连接点的断言或者表达式。Advice 与切入点表达式相关联,并在切入点匹配的任何连接点处运行(比如,表达式 execution(* EmployeeManager.getEmployeeById(…)) 可以匹配 EmployeeManager 接口的 getEmployeeById() )。由切入点表达式匹配的连接点的概念是 AOP 的核心。Spring 默认使用 AspectJ 切入点表达式语言。

织入(Weaving)是什么?
  Spring AOP 框架仅支持有限的几个 AspectJ 切入点的类型,它允许将切面运用到在 IoC 容器中声明的 bean 上。如果你想使用额外的切入点类型或者将切面应用到在 Spring IoC 容器外部创建的类,那么你必须在你的 Spring 程序中使用 AspectJ 框架,并且使用它的织入特性。
  织入是将切面与外部的应用类型或者类连接起来以创建通知对象(adviced object)的过程。这可以在编译时(比如使用 AspectJ 编译器)、加载时或者运行时完成。Spring AOP 跟其他纯 Java AOP 框架一样,只在运行时执行织入。在协议上,AspectJ 框架支持编译时和加载时织入。
  AspectJ 编译时织入是通过一个叫做 ajc 特殊的 AspectJ 编译器完成的。它可以将切面织入到你的 Java 源码文件中,然后输出织入后的二进制 class 文件。它也可以将切面织入你的编译后的 class 文件或者 Jar 文件。这个过程叫做后编译时织入(post-compile-time weaving)。在 Spring IoC 容器中声明你的类之前,你可以为它们运行编译时和后编译时织入。Spring 完全没有被包含到织入的过程中。更多关于编译时和后编译时织入的信息,请查阅 AspectJ 文档。
  AspectJ 加载时织入(load-time weaving, LTW)在目标类被类加载器加载到JVM时触发。对于一个被织入的对象,需要一个特殊的类加载器来增强目标类的字节码。AspectJ 和 Spring 都提供了加载时织入器以为类加载添加加载时织入的能力。你只需要简单的配置就可以打开这个加载时织入器。

猜你喜欢

转载自blog.csdn.net/weixin_41262453/article/details/89523857