本人经历了几家大公司的Java研发的面试,现就面试中,经常遇到的问题整理如下:(java研发工程师)
不吹不捧,本人靠着这一系列题目拿到了校招百度、京东、蚂蚁金服的offer😄。
Java基础部分
【重点】:线程池
https://www.cnblogs.com/exe19/p/5359885.html
【1】java中对集合的理解?(小米、京东)
答:下图画的不是很准确,具体的类的关系图可参考:java集合
分析arrayList与LinkedList的区别:
1. ArrayList(默认长度是10个):数组列表,可以动态的扩展空间,随机查询一个元素速度比较快,删除和插入一个元素速度较慢,因为需要移动元素;
2. LinkedList:链式存储接口,向集合插入元素时效率较高,但是查询元素效率较低,因为不支持随机访问
附加:Java中队列有哪些?
普通队列
LinkedList实现了queue接口,可以作为队列使用。
ArrayDeque:以循环数组实现的双向Queue。 普通数组只能快速在末尾添加元素,为了支持FIFO,从数组头快速取出元素,就需要使用循环数组:有队头队尾两个下标:弹出元素时,队头下标递增;加入元素时,如果已到数组空间的末尾,则将元素循环赋值到数组[0](如果此时队头下标大于0,说明队头弹出过元素,有空位),同时队尾下标指向0,再插入下一个元素则赋值到数组[1],队尾下标指向1。如果队尾的下标追上队头,说明数组所有空间已用完,进行双倍的数组扩容。
PriorityQueue :用二叉堆实现的优先级队列,详见入门教程,不再是FIFO而是按元素实现的Comparable接口或传入Comparator的比较结果来出队,数值越小,优先级越高,越先出队。但是注意其iterator()的返回不会排序。
–线程安全的队列–
ConcurrentLinkedQueue/ConcurrentLinkedDeque:
无界的并发优化的Queue,基于链表,实现了依赖于CAS的无锁算法。 ConcurrentLinkedQueue的结构是单向链表和head/tail两个指针,因为入队时需要修改队尾元素的next指针,以及修改tail指向新入队的元素两个CAS动作无法原子,所以需要的特殊的算法,篇幅所限见入门教程。
PriorityBlockingQueue :无界的并发优化的PriorityQueue,也是基于二叉堆。使用一把公共的读写锁。虽然实现了BlockingQueue接口,其实没有任何阻塞队列的特征,空间不够时会自动扩容。
DelayQueue: 内部包含一个PriorityQueue,同样是无界的。元素需实现Delayed接口,每次调用时需返回当前离触发时间还有多久,小于0表示该触发了。 pull()时会用peek()查看队头的元素,检查是否到达触发时间。ScheduledThreadPoolExecutor用了类似的结构。
–线程安全的阻塞队列–
BlockingQueue:的队列长度受限,用以保证生产者与消费者的速度不会相差太远,避免内存耗尽。队列长度设定后不可改变。
ArrayBlockingQueue :定长的并发优化的BlockingQueue,基于循环数组实现。有一把公共的读写锁与notFull、notEmpty两个Condition管理队列满或空时的阻塞状态。
LinkedBlockingQueue/LinkedBlockingDeque :可选定长的并发优化的BlockingQueue,基于链表实现,所以可以把长度设为Integer.MAX_VALUE。利用链表的特征,分离了takeLock与putLock两把锁,继续用notEmpty、notFull管理队列满或空时的阻塞状态。
【2】如何理解HashMap和HashTable?(百度)
答:HashMap是非synchronized的,线程不安全的,,允许null的key和null的value,而HashTable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable。就单个线程来讲,HashMap速度比HashTable速度要快。
hashmap结构可参考博客:hashmap
*******HashMap是如何扩容的?(必知必会*****)
有哪些map是线程安全的?
Hashtable、synchronizedMap、ConcurrentHashMap
ConcurrentHashMap为啥是安全的?
答: 锁分段技术HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把 锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是 ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据 的时候,其他段的数据也能被其他线程访问。
【3】String、StringBuffer和StringBuilder三者的区别?(京东、小米)
答: 简言之:String代表字符串常量,StringBuffer 字符串变量(有syncronized修饰,线程安全),StringBuilder 字符串变量(非线程安全)。
StringBuffer()的初始容量可以容纳16个字符,当该对象的实体存放的字符的长度大于16时,实体容量就自动增加。经常改变内容的字符串最好不要用 String,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
【4】equals和==区别?(小米、百度)
答:在java中基本数据类型(int、boolean、char、long、float、double)的比较用==,==比较的是基本数据类型的值。除了基本数据类型外,java还有复合的数据类型(类),当用==比较的时候,比较的是他们在内存中的存放地址。so,除非是同一个new出来的对象,比较结果为true,否则,都为false。equals就不同了,equals是object类的一个方法,原始的equals()方法是比较内存中的地址,跟==是一样的,但是这个方法在一些类库当中被override了(如String,Integer,Date等),覆写后的方法比较的不再是内存地址而是值。
【5】short s1 = 1; s1 = s1 + 1;有错吗?(错)short s1 = 1; s1 += 1;有错吗?(对)
答:
short s1 = 1;
s1 = s1 + 1;
1是int类型,so,s1 + 1的类型也为int类型,需要强制类型转换才能赋值给short s1。而s1+=1相当于s1 = (short)(s1 + 1);其中隐藏着强制类型转换。
【6】两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?(不对)(京东)
答:如果对象x和y相同那么他们的hashcode一定相同,Java对于eqauls方法和hashCode方法是这样规定的:
(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;
(2)如果两个对象的hashCode相同,它们并不一定相同。
【7】重载(Overload)和重写(Override)的区别。
答:重载和重写都是实现多态的方式,只不过一个是在前期(编译期实现),一个是在后期(运行时实现)。overload实现的是编译时的多态性,override实现的是运行时的多态性。
overload发生在类中,函数名相同,但有不同的入参参数类型、个数,不考虑函数返回类型;
override一般是子类继承了父类,重写父类的函数,返回值类型和父类保持一致。
【8】抽象类(abstract class)和接口(interface)的区别。
答:抽象类,顾名思义,是类的抽象,抽象类不能实例化,抽象类可以有构造器,谁继承了抽象类,就要对抽象类中的所有抽象方法进行实现。接口interface是一种特殊的抽象类,但是接口中不能有构造函数,接口中的所有方法都不能有实现,接口可以多继承。抽象类可以实现(implement)接口,抽象类可以继承(extends)具体类
【9】String s = new String(“xyz”);创建了几个字符串对象?(2)
答:首先在string池内找xyz,找到?不创建string对象,否则创建, 这样就一个string对象,遇到new运算符号了,在内存上创建string对象,并将其返回给s,又一个对象。所以总共是2个对象。
补充:
详细内容可参考:深入理解string
【10】描述一下Java加载class文件的原理机制?
答:【详细参考】
【11】Arrays类的copyOf()函数,是浅拷贝。
最后返回都是30。Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度。
【12】你对消息中间件熟悉吗?应用场景?(小米)
答:消息中间件是分布式系统中的重要组件,主要解决应用解耦、异步处理、流量削峰和消息通讯的问题(在电商、日志处理上应用广泛)。目前使用较多的消息队列有:ActiveMQ,Kafka,RabbitMQ,ZeroMQ,MetaMQ,RocketMQ等。[详细参考]
【13】下面程序的运行结果是(false)
String str1 = "hello";
String str2 = "he" + new String("llo");
System.err.println(str1 == str2);
答:看懂了前面equals和==的问题,以及jvm的相关知识,这个就很简单了。
【14】GC线程是否为守护线程?()
答案:是。线程分为守护线程和非守护线程(即用户线程)。
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。守护线程最典型的应用就是 GC (垃圾回收器)
【15】如何理解volatile和synchronized?(京东)
在多线程并发的问题中,线程对共享变量的操作都是在自己的工作内存中进行的,线程之间无法达到共享,线程之间的变量值的传递需要经过主内存作为桥梁。
共享变量可见性的原理如下:
(1)工作线程1中更新了的共享变量如果想被工作线程2看到(可见),需要先将工作内存刷新到主内存;
(2)主内存中共享变量发生变化,更新到工作线程2的工作内存中。
想要实现可见性,需要保证两点:
1.工作线程中的工作内存的变量值,一旦发生改变要及时地刷新到主内存中;
2.主内存中的变量一旦发生变化,工作线程能及时地从主内存刷新到自己的工作内存。
接下来,切入我们的正题:
可见性的实现方式:synchronized和volatile
synchronized实现过程如下:
1.获得互斥锁;
2.清空工作内存
3.从主内存拷贝变量的工作副本到自己的工作内存
4.执行代码
5.将更改后的共享变量的值刷新到主内存中
6.释放互斥锁
那么,导致共享变量在线程中不可见的原因是什么?主要有这么几点:多线程交叉执行;重排序结合线程交叉执行;共享变量的值在发生改变后未能在主内存和工作内存中及时更新。synchronized解决的问题是,线程交叉执行,synchronized保证原子性,重排序集合线程交叉,synchronized保证原子性,最后一个,synchronized保证可见性。简化成图片就是如下:
then,对于volatile,它又是如何解决可见性问题的呢?volatile是通过加入内存屏障和禁止重排序优化实现的。(volatile在执行读操作时候,会在读操作前面加一条load的屏障指令,同理,在执行写操作时候会在写操作后加一条store的屏障指令)
volatile是如何实现的?通俗的将,变量每次被线程访问时,都会被迫从主内存中重读变量的值,当变量的值发生变化时,由会强迫线程将变量的值刷新到主内存中,这样,任何时刻,不同的线程之间均能看到该变量的最新值。
synchronized和volatile的比较:
【16】ArrayList list = new ArrayList(20);中的list扩充几次(0)(京东)
答:不扩容。默认ArrayList的长度是10个,如果往list里添加20个元素肯定要扩充一次(扩充为原来的1.5倍),但是这里显示指明了需要多少空间,所以就一次性分配这么多空间,也就是不需要扩充了。
【17】下列程序输出什么?
输出顺序如下:
static A
static B
I'm A class
HelloA
I'm B class
HelloB
答:对象的初始化顺序:(1)类加载之后,按从上到下(从父类到子类)执行被static修饰的语句;(2)当static语句执行完之后,再执行main方法;(3)如果有语句new了自身的对象,将从上到下执行构造代码块、构造器(两者可以说绑定在一起)
稍微修改下如下:
答案:
static A
static B
-------main start-------
I'm A class
HelloA
I'm B class
HelloB
I'm A class
HelloA
I'm B class
HelloB
-------main end-------
【18】Thread类中run()和start()方法的区别。
答:run()是执行体,start()是开启一个多线程,已知线程的状态分为:创建、就绪、运行、阻塞、死亡五个。线程状态转换图如下所示。
1.创建状态:此时还没调用start()方法
2.就绪状态:调用了start()方法之后,线程处于就绪状态,线程调度程序还没把该线程设置成当前线程。当然,程序运行后,等待或者睡眠之后也会处于就绪状态。
3.运行状态:程序调度器将就绪线程设置成当前线程,此时就会执行程序执行体run()函数。
4.阻塞状态:正在运行的程序被阻塞,一般是sleep()、suspend()、wait()等方法导致。
5.死亡状态:run()执行结束或者调用了stop()方法,线程会处于死亡状态。死亡的线程不能再通过start()方法使其处于就绪状态。
多线程原理:
相当于多个人玩玩具,只有一个玩具(cpu),好多人都想玩,怎么办?排队!!!start()就起到了排队的作用。等cpu轮到你,你就run(),当cpu的运行时间结束,就继续排队,等待下一次的run()。
调用start()后,线程会被放到等待队列,等待CPU调度,并不一定要马上开始执行,只是将这个线程置于可运行状态。然后通过JVM,线程Thread会调用run()方法,执行本线程的线程体。先调用start后调用run,这么麻烦,为了不直接调用run?就是为了实现多线程的优点,没这个start不行。
所以,start()方法用来启动线程,此时线程并没有处于运行状态,只是就绪状态。start()方法执行后可不用等待run()方法里面的程序执行完,可以继续执行下面的程序。
如果run()方法被当做普通方法来执行,不能实现多线程并发的效果。下面的程序需要等待run()里面的代码执行结束才能执行,程序中只有主线程这一个线程。
【19】sleep()和wait()的区别?(百度、新浪)
答:wait()和sleep()的关键的区别在于,wait()是用于线程间通信的,而sleep()是用于短时间暂停当前线程。其中,sleep()是Thread类中的静态方法,wait()是是定义在Object类中的。
另外,sleep()是使当前的程序处于阻塞状态,在其睡眠时间段内,该线程不会获得执行的机会。而wait(),线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁,wait()和notify()必须在synchronized函数或synchronized block中进行调用。
【20】下面哪个流类属于面向 字符 的输入流( D )
A. BufferedWriter B. FileInputStream
C. ObjectInputStream D. InputStreamReader
答:看图一目了然。
对于Io,分为字符流和字节流。
字节流:reader writer,
字符流:InputStream(FileInpuStream)、OutputStream(FileOutputStream->BufferedOutputStream)具体如下表所示:
1.什么时候使用字节流?什么时候使用字符流?
答:
所有的输入都是转化成字节流后,在内存中转化成字符流。因此一般建议使用字符流,当遇到中文出现乱码时,使用字节流。(所有硬盘上保存的文件或者传输文件时,都是通过字节流的形式进行的,包括图片也是字节流,在内存中才转化成字符流,使用字节的操作是最多的)
2.递归读取文件夹的文件,代码怎么实现?
代码如下:
【详细参考】
【21】泛型的作用?
答:泛型主要是用在集合中,如果不指定加入集合的元素的类型,集合就会忘记被“丢进”的元素类型。在程序编译过程中,如果不使用泛型,编译器不会报错,但是在foreach过程中会出现ClassCastException异常。
【22】如何理解多态性?(新浪)
答:用专业一点的话说就是,类中所定义引用变量或者由引用变量所调用的方法在程序编译期间并不能够知晓,只有在程序运行期间才能知道,调用了哪个类的方法。、
如果讲给大街上的老头,桌子上有三杯酒,a(茅台) b(五粮液) c(二锅头),都是白酒,在没喝(相当于程序运行)之前,我们不知道谁是谁,只有品尝了一口,才知道,哦,哪个是二锅头,哪个是茅台(abc都是酒的子类)。
【23】解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法。
答:通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间;
通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured;
方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;程序中的字面量(literal)如直接书写100、"hello"和常量都是放在常量池中,常量池是方法区的一部分。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和常量池空间不足则会引发OutOfMemoryError。
String str = new String("hello");
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。
最后,堆、栈在数据上面的共享问题(栈区,本地方法区和pc计数器属于线程内存,每个独立线程都拥有自己的线程内存,这些是不可共享的, 堆区,方法区和常量池是可以共享的)
更加详细的内存模型及GC算法内容参考【详细参考】
【24】如何理解线程和进程?线程之间如何通讯的?(面试必问※)
答:在操作系统中,每一个任务的执行都会占用系统一部分资源(或者说,系统为每一个任务分配一定的cpu资源),每一个任务的执行称为一个进程,进程具有独立性,也就说相互进程之间无法进行直接的交流,进程具有并发性,在操作系统中,每一时刻仅有一个进程运行,但是多个进程之间是轮换转变的,宏观上讲进程具有并发性。
线程是进程的微小单元,一个任务的执行是一个进程,一个进程可以包含多个线程,这样的好处就是,线程之间可以共享内存,而进程之间是无法共享的。
当操作系统创建一个进程时,系统需要为每一个进程分配一定的内存空间及相关的资源,而线程的创建则代价小得多,因此多线程的并发要比多进程的并发性能高的多。
线程之间的通讯方式?
我们知道线程之间是可以进行内存共享的,如何进行共享的呢?
1.传统的通讯方式是基于同步代码块、同步方法(也就是由synchronized修饰的时候),用到三个方法,wait(),notify()及notifyAll(),但这三个方法必须有同步监视器对象来调用。分两种情况:
1. synchronized修饰同步代码块,同步监视器是 synchronized后面括号里面的对象,使用该对象调用三个方法。
2. synchronized修饰同步方法,因为该类的实例默认(this)就是同步监视器,所以可以直接调用这三个方法。
wait()方法是object类方法,是针对线程之间的,使用wait()方法会使得当前线程等待,调用wait()方法的线程会释放对该同步监视器的锁。直到其他线程调用该同步监视器的notify()或者notifyAll()方法来唤醒。
notify()唤醒在此同步监视器上等待的单个线程
notifyAll()唤醒在此同步监视器上等待的所有线程。
2.使用Condition控制线程通讯
倘若程序中没有使用synchronized关键字来修饰,而是使用了同步锁Lock来保证同步,系统中没有了隐式的同步监视器,也就无法使用wait(),notify()及notifyAll()了,此时可使用Condition类。
condition实例被绑定在一个Lock对象上,要获得特定的Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。其提供的三个方法:
1.await()类似于wait()方法,导致线程等待,直到其他线程调用signal()或signalAll()唤醒
2.signal()唤醒单个线程
3.signalAll()唤醒所有线程。
//显示定义Lock对象
private final Lock lock = new ReentrantLock();
//获得指定Lock对象对应的Condition
private final Condition cond = lock.newCondition();
3.使用阻塞队列(BlockingQueue)控制线程通讯
【25】线程池的使用原理及示例?
1.为什么使用线程池?
合理的使用线程池便可重复利用已创建的线程,以减少在创建线程和销毁线程上花费的时间和资源。
2. 线程池的简要工作模型
【26】利用多线程循环输出AB
答:
【27】final关键字的理解和认识?
答:在使用匿名内部类的时候可能会经常用到final关键字。在Java中String类就是一个final类。
final可以修饰类、方法和变量(成员变量、局部变量)。
1.修饰类
final修饰类时候,此类不能被继承。
2.修饰方法
一个类中用final修饰方法,继承此类的子类无法override父类的方法。
3.修饰变量
当final修饰类的成员变量时,定义变量的时候要对变量进行初始化,而且final变量一旦被初始化赋值之后,就不能再被赋值了。
final修饰的成员变量和普通变量有啥区别?(true、false)
final修饰基本数据类型及string类型时,在编译期间,变量就被认为是编译常量,会直接加载,不会等到运行期间再加载,因此final修饰的string b="hello",b在编译期间就被直接调用了。
被final修饰的引用变量指向的对象内容可变吗?
在上面提到被final修饰的引用变量一旦初始化赋值之后就不能再指向其他的对象,那么该引用变量指向的对象的内容可变吗?看下面这个例子:
这段代码可以顺利编译通过并且有输出结果,输出结果为1。这说明引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的。
最后,final和static有什么区别?
static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变。看下面这个例子:
运行这段代码就会发现,每次打印的两个j值都是一样的,而i的值却是不同的。
补充:为什么string类要final来修饰?【详细参考】
两个方面回答:1.安全性;2.提高效率
【28】Java中内部类有什么作用?
答:内部类分为:1.成员内部类;2.局部内部类;3.匿名内部类;4.静态内部类
1.成员内部类是最普通的内部类,它的定义为位于另一个类的内部。成员内部类可以无条件( 事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件)访问外部类的所有成员属性和成员方法(包括private成员和静态成员)
2. 局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
3. 匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。多用于安卓监听器中。
注意: 局部内部类和匿名内部类只能访问局部final变量。
4. 静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法。
为什么在Java中需要内部类?总结一下主要有以下四点:
1.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整。
2.方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
3.方便编写事件驱动程序
4.方便编写线程代码
【29】Dubbo架构原理
答:【详细参考】