重磅!!面试季--最新面试题总结出厂,附题解,后期持续分享!


作者:早该变坏
链接:https://www.nowcoder.com/discuss/163934

一、情话部分

小姐姐: 为什么有很多人在感情中付出很多,却得不到想要的结果?

你答: 我听过一个这样的故事:讲的是蚯蚓一家人,有一天,蚯蚓爸爸特别无聊,就把自己切成了俩段愉快的打羽毛球去了,蚯蚓妈妈见状,把自己切成了四段,打麻将去了,蚯蚓哥哥接近狂热,把自己切成很多段,结果死掉了,因为他想踢足球。蚯蚓哥哥的死震惊了整个蚯蚓界,各蚯蚓专家呼吁大家要谨慎使用自己的能力。蚯蚓哥哥的死同时对蚯蚓一家造成了不可磨灭的伤害,蚯蚓弟弟为了弥补家庭的残缺,决定把自己切成俩段。第二天蚯蚓弟弟也死掉了。你知道为什么吗?

小姐姐:嗯?不知道(如果小姐姐知道,你就夸她聪明咯)

你:因为蚯蚓弟弟是竖着切的。

这个故事告诉我们,有时候呀,我们总是在应该动脑的时候,却动了情!!!

二、闯关阶段

自我介绍:( 您好(人多就说大家好),很荣幸有机会参加此次面试,希望我今天能有好的表现,现在请允许我介绍一下自己:我叫变坏,今年18岁,毕业于牛客大学软件工程专业(或者说是XX大学软件工程专业的应届生),在大学期间专业课学习了java这门编程语言,自己在网上也学习了一些相关的技术,在校期间自己也曾和同学使用java开发过一些项目,在学校也曾考取过相关的证书,获得过一些比赛的奖,大学期间还担任过课代表,由于毕业将近,本人决定踏上社会道路,因此在牛客平台看到贵公司的招聘,在此之前也曾在网上了解过贵公司(不要去问公司业务,网上都可以查的),巴拉巴拉吹一吹。从以上简单的自我介绍里,我希望公司能给我一个展示自己能力的机会)

1 多线程的几种实现方式

继承Thread类,实现Runnable接口,实现Callable接口,线程池

下面是我的csdn博客的一篇文章,详细说明了:https://blog.csdn.net/sihai12345/article/details/80256322

2 线程join()方法

Thread中, join()方法的作用是调用线程等待该线程完成后,才能继续用下运行。


public static void main(String[] args) throws InterruptedException
    {
        System.out.println("main start");

        Thread t1 = new Thread(new Worker("thread-1"));
        t1.start();
        t1.join();
        System.out.println("main end");
    }

在上面的例子中,main线程要等到t1线程运行结束后,才会输出“main end”。如果不加 t1.join(),main线程和t1线程是并行的。而加上 t1.join(),程序就变成是顺序执行了。

我们在用到 join()的时候,通常都是main线程等到其他多个线程执行完毕后再继续执行。其他多个线程之间并不需要互相等待。

下面这段代码并没有实现让其他线程并发执行,线程是顺序执行的。


public static void main(String[] args) throws InterruptedException
    {
        System.out.println("main start");

        Thread t1 = new Thread(new Worker("thread-1"));
        Thread t2 = new Thread(new Worker("thread-2"));
        t1.start();
        //等待t1结束,这时候t2线程并未启动
        t1.join();

        //t1结束后,启动t2线程
        t2.start();
        //等待t2结束
        t2.join();

        System.out.println("main end");
    }

为了让t1、t2线程并行,我们可以稍微改一下代码,下面给出完整的代码:


public class JoinTest
{

    public static void main(String[] args) throws InterruptedException
    {
        System.out.println("main start");

        Thread t1 = new Thread(new Worker("thread-1"));
        Thread t2 = new Thread(new Worker("thread-2"));

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("main end");
    }
}

class Worker implements Runnable
{

    private String name;

    public Worker(String name)
    {
        this.name = name;
    }

    @Override
    public void run()
    {
        for (int i = 0; i < 10; i++)
        {
            try
            {
                Thread.sleep(1l);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            System.out.println(name);
        }
    }

}

3 ArrayList的remove操作

ArrayList有俩个remove()重载方法,一个参数是int类型,另一个参数是Object类型,remove(1)是删除索引为1的元素,remove(new Integer(1))是删除元素1,底层是用equals进行比较的。

先看迭代器的 next 方法

public E next() {
    // 这个方法主要是检查光标是否越界的
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}  
/**
* 在对一个集合对象进行跌代操作的同时,并不限制对集合对象的元素进行操作
* 这些操作包括一些可能引起跌代错误的add()或remove()等危险操作。
* 在AbstractList中,使用了一个简单的机制来规避这些风险。 
* 这就是modCount和expectedModCount的作用所在
*/
final void checkForComodification() {
    if (modCount != expectedModCount)
         throw new ConcurrentModificationException();
}

我们可以看到,list 每次获取下一个对象前都要去检查一下光标是否越界。在 ArrayList 的所有涉及结构变化的方法中都增加 modCount 的值,包括:add()、remove()、addAll()、removeRange() 及 clear() 方法。这些方法每调用一次,modCount 的值就加 1。而变量 expectedModCount 在迭代开始时便会被赋值成 modCount 的值。所以在循环遍历中,改变结构变化的方法,例如 add()、remove() 都会是 modCount 增长 1 ,而 expectedModCount 却不会变化。

注意,以上讲的涉及到结构变化的方法是 ArrayList 的方法,不是其内部类 Itr 的方法。

来看一下 ArrayList 的 remove 方法

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}  

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
        } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
        }
    return false;
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

从上面源码中我们不难发现,ArrayList 中两个 remove() 方法都对 modCount 进行了自增,那么我们在用迭代器迭代的时候,若是删除 末尾 的元素,则会造成 modCount 和 expectedModCount 的不一致导致异常抛出。

为什么对倒数第二个元素进行删除不会报异常,而对其他位置的删除会报异常?

我们来看一下 ArrayList 中的内部类 Itr 。我们在调用迭代器的 Next() 方法之前会先调用 hasNext() 方法。


public boolean hasNext() {
    return cursor != size;
}

从代码上我们可以看出判断条件是当 cursor != size 的时候,才会进行下一次循环,而 cursor 参数是我们迭代循环的下标,在我们删除倒数第二个元素后,此时 list 的大小减了 1,再进入下一次循环后会出现 cursor == size ,也就是 hasNext() 便会返回 false 终止了循环。实际上 modCount 的数值也增加了 1,只不过循环没发执行到那里,所以异常也就不会被抛出来了。

for 下标遍历删除

从源码上我们可以看出,在利用 for 下标进行遍历的时候,并不会触发 checkForComodification() 方法,所以此时只要要删除的位置比列表大小小时都不会出错。


public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

在 ArrayList 源码介绍中,作者是推荐使用 for ( int i; i < size; i++) 方式去遍历,而不是 foreach 或者迭代,这个主要是因为 list 接口实现了 RandomAccess 接口。 实现这个接口的集合是随机无序的,所以遍历的时候一般使用上述的 for,记住一点就可以了所有实现了 RandomAccess 接口的集合都是用一般 for 就可以了(可以通过 api 查看那些集合实现了 RandomAccess)。

Iterator 迭代遍历删除

这里我们将的 Iterator 遍历删除调用的方法不是 ArrayList 的 remove 方法,而是其内部类的 remove 方法

我们看源码不难发现,在 Itr 类中,属性 expectedModCount 在调用外部的 remove() 方法后再次被赋值,此时 expectedModCount 是等于 modCount 的。


public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    // 这里检查时候还没有进行删除操作
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        // 先进行了 remove 操作后 再重新对 expectedModCount 进行赋值
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

所以在使用 Iterator 进行遍历删除时不会出现 ConcurrentModificationException 异常。

4 HashMap为啥不安全

resize死循环,fail-fast(快速失败)

具体查看:https://www.jianshu.com/p/e2f75c8cce01

5 HashMap1.7和1.8区别

1.7数组+链表,头插入,1.8数组+链表+红黑树,尾插入。resize方法、hash计算方式、扩容后的位置计算方式等

具体详解:https://blog.csdn.net/qq_30683329/article/details/80454518

6 HashMap如何扩容及Put方法

具体详情:https://blog.csdn.net/fendianli6830/article/details/81102709

7 TreeMap

默认按照Key的字典序升序排列,底层红黑树+compareTo()方法,大致就是和根节点比较,小于根节点往左子树继续去比较,大于根节点往右子树去比较咯等等

大佬文章:https://www.cnblogs.com/chenssy/p/3746600.html

8 concurrentHashMap底层原理

推荐文章:https://blog.csdn.net/sihai12345/article/details/79383766

9 如何确保一个集合不被修改

使用Collections.unmodifiableCollection(Collection c)方法创建只读集合

10 Iterator和ListIterator有什么区别

前者只能遍历不能修改,后者可以修改元素并且可以逆向遍历、定位当前索引位置,但后者只能用于List及其子类型

11 快速失败和安全失败

fail-fast:遍历时对集合进行增删改会抛出Concurrent Modification Exception异常,一般的java.util包下的集合用的就是快速失败。安全失败就是采用复制方式,修改原集合,虽然不会报错,但是也没办法访问修改后的元素。一般在java.util.concurrent包下集合用的就是安全失败

具体查看:https://mp.weixin.qq.com/s/l9SDwQuFKeCcufI3KJCWpw

12 如果clone单例模式的对象会怎样

不行,这里必须要实现Cloneable接口,所以需要单例的类不能去实现Clonable接口。反射应该可以去获取私有的构造方法从而破坏单例

13 hibernate和mybatis区别

相同方面:ORM、都支持jdbc事务
不同点:sql方面、缓存方面

具体查看:https://mp.weixin.qq.com/s/h7tAk1IjdMi5SSNjRZE50g

14 mysql联合索引和聚集索引

联合索引就是多列组成的索引,聚集索引CLUSTERED,聚集索引的顺序与数据真实的物理存储顺序一致,特别快,主键!=聚集索引

15 行锁和表锁

表锁:不会出现死锁,发生锁冲突几率高,并发低。
行锁:会出现死锁,发生锁冲突几率低,并发高

16 b树索引和Hash索引区别

大量不同数据查找,hash索引比B树索引效率高,hash索引不支持联合索引的最左匹配规则,hash索引不支持排序,hash索引不支持模糊查找

17 软连接硬链接

软连接:新建一个文件来指向别的文件,原文件删除则不可用,可跨文件系统。
硬链接:原来的inode link count域再增加1(在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号inode ),不可跨文件系统,删除原文件也可继续使用。ln是创建硬链接 ln -s是创建软连接)

18 linux查看进程的命令

ps命令 -A:所有的进程均显示出来、-a显示现行终端机下的所有进程,包括其他用户的进程 、-u以用户为主的进程状态

linux常用命令:https://mp.weixin.qq.com/s/ZqHy0_m4tNMAQT53mKlc7g

image


 上一篇
当初的年味还在吗? 当初的年味还在吗?
365天,一年又过去了,真的是时光飞逝,想想去年的种种都还历历在目呢,但就是这样还没缓过神来,又到了一年最重要的节日了,在这里,小海祝小伙伴们新春快乐,新的一年事事如意,都能实现自己的目标和愿望,另外,家人都健健康康的! 回到家已经有几天
下一篇 
这是我的2018年终总结,你的呢? 这是我的2018年终总结,你的呢?
今天是2019年了,首先,祝小伙伴们新年快乐,在新的一年都能够实现自己的目标哦。这一年,很精彩,但也很平庸,但是,我自己很知足,并且很开心的度过啦,不知道大家对于2018年,都有着自己怎样的评价,是满意,还是略有不足呢? 当然了,这一年过去
  目录