作者:早该变坏
链接: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以用户为主的进程状态