Java面试题
JavaSE部分:
1.Object类自带哪些方法?
getClass() 获取对象对应的Class对象;
hashCode() 获取对象的hash码;
equals() 判断对象的值是否相等,原始equals()方法是判断对象地址是否相等,要重写该方法;
clone() 克隆方法,调用对象的该方法只是浅复制,即只会复制字面量。也即基本类型会被复制对应的值,引用类型也会被复制对应的值(只不过是地址的值,复制出来的对象引用的跟原来对象引用的是同一个对象,也就是为什么叫做“浅复制”的原因)。
要想实现深复制,那么要在类中重写clone()方法,并且的该方法里面,对引用类型的成员创建新的对象,这样,复制出来的对象才是深复制的。
toString() 打印该对象的方法。原始方法是打印该对象hashCode的十六进制;
notify() 唤醒等待在对象waitSet上的第一个线程;
notifyAll() 唤醒等待在该对象waitSet上的所有线程;
wait() 释放该对象的锁,并将当前线程放入该对象的waitSet中;
wait(long timeoutMillis) 设置一个最长等待时间,若等待这么常时间该线程仍然未被唤醒,那么该线程就会被自动唤醒;
10.wait(long timeoutMillis, int nanos) 设置一个过期时间,比timeoutMillis多或少nanos,(nanos单位为10^(-9)s)
finalize() 垃圾回收时可以重写该方法,但是因为该方法容易导致内存泄漏,因此在JDK9之后就被遗弃了。
2. 对String类了解多少?
String是一个引用数据类型默认为null;String 为final类型不可更改,不能被继承。String类的本质是字符数组char[];
Java运行时会维护一个String 池,“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且创建的对象仅仅存在于方法的堆栈区
直接赋值:String str = " “; 直接赋值方式创建对象是在方法区的常量池。
使用new进行赋值:String str = new String (” "); 通过构造方法创建字符串对象是在堆内存
boolean equals(Object obj):比较字符串的内容是否相同
boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
boolean startsWith(String str):判断字符串对象是否以指定的str开头
boolean endsWith(String str):判断字符串对象是否以指定的str结尾
int length():获取字符串的长度,其实也就是字符个数
char charAt(int index):获取指定索引处的字符
int indexOf(String str):获取str在字符串对象中第一次出现的索引
String substring(int start):从start开始截取字符串
3.String、StringBuffer、StringBuilde的区别?
1.String是字符串常量,StringBuffer和StringBuilder都是字符串变量。后两者的字符内容可变,而前者创建后内容不可变(String进行字符串拼接时创建新对象)。
2.StringBuffer是线程安全的,而StringBuilder是非线程安全的。
3.线程安全会带来额外的系统开销,所以StringBuilder的效率比StringBuffer高。如果对系统中的线程是否安全很掌握,可用StringBuffer,在线程不安全处加上关键字Synchronize。
4.StringBuffer 和 StringBuilder 要调用.toString方法在进行比较
4.collection和collections的区别
1.Collection:是集合类的上层接口。本身是一个Interface,里面包含了一些集合的基本操作。Collection接口是Set接口和List接口的父接口
2.Collections:Collections是一个集合框架的帮助类,里面包含一些对集合的排序,搜索以及序列化的操作。最根本的是Collections是一个类,
Collections 是一个包装类,Collection 表示一组对象,这些对象也称为 collection 的元素。一些 collection 允许有重复的元素,而另一些则不允许,一些 collection 是有序的,而另一些则是无序的。
5.Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?
set里的元素是不能重复的,equals()方法进行判断的。
equals 方法(是String类从它的超类Object中继承的)被用来检测两个对象是否相等,即两个对象的内容是否相等。
==用于比较引用和比较基本数据类型时具有不同的功能:
比较基本数据类型,如果两个值相同,则结果为true
而在比较引用时,如果引用指向内存中的同一对象,结果为true
6.从底层区分下ArrayList和LinkedList,Arraylist、LinkedList、HashMap的初始大小以及如何扩容?
ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
Arraylist是基于动态数组实现的,所以查找速度快,但是增删操作的速度会比较慢。
LinkedList是基于双向链表的数据结构实现的,链表是可以占用一段不连续的内存空间的。
ArrayList 初始化大小是 10 (如果你知道你的arrayList 会达到多少容量,可以在初始化的时候就指定,能节省扩容的性能开支)。扩容点规则是,新增的时候发现容量不够用了,就去扩容。扩容大小规则是,扩容后的大小= 原始大小+原始大小/2 + 1。(例如:原始大小是 10 ,扩容后的大小就是 10 + 5+1 = 16)
linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。
HashMap 初始化大小是 16 ,扩容因子默认0.75(可以指定初始化大小,和扩容因子)。扩容机制.(当前大小 和 当前容量 的比例超过了 扩容因子,就会扩容,扩容后大小为 一倍。例如:初始大小为 16 ,扩容因子 0.75 ,当容量为12的时候,比例已经是0.75 。触发扩容,扩容后的大小为 32.)
7.HashMap、Hashtable的区别?
HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口
Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步
HashMap不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。使用HashMap时就必须要自己增加同步处理。但是它的效率会比Hashtable要好很多
Hashtable既不支持Null key也不支持Null value。HashMap中,null可以作为键
Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16,之后每次扩充,容量变为原来的2倍
8.HashMap的底层
HashMap底层就是一个数组结构,数组中的每一项又是一个链表。数组+链表结构,新建一个HashMap的时候,就会初始化一个数组。Entry就是数组中的元素,每个Entry其实就是一个key-value的键值对,它持有一个指向下一个元素的引用,这就构成了链表,HashMap底层将key-value当成一个整体来处理,这个整体就是一个Entry对象。HashMap底层采用一个Entry【】数组来保存所有的key-value键值对,当需要存储一个Entry对象时,会根据hash算法来决定在其数组中的位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry对象时,也会根据hash算法找到其在数组中的存储位置, 在根据equals方法从该位置上的链表中取出Entry。
9.HashMap、LinkedHashMap、ConcurrentHashMap的异同
LinkedHashMap:
1.LinkedHashMap是有序的
2.每次访问一个元素(get或put),被访问的元素都被提到最后面去了
3.非线程安全
ConcurrentHashMap:
线程安全,使用了分段锁,在线程需要安全的时候,使用ConcurrentHashMap替代LinkedHashMap。锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
hashMap
1.遍历顺序却是不确定的。
2.HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
3.HashMap非线程安全
10.Java中HashMap的key值要是为类对象,则该类需要满足什么条件?
由于是自定义类型,所以HashMap中的equals()方法和hashCode()方法都需要自定义覆盖。
不然内容相同的对象对应的hashCode会不同,无法发挥算法的正常功能,覆盖equals方法,应该就相当于c++重载==运算符来保证能判断是否相等。只不过java没有自定义重载运算符这个功能的,需要进行方法覆盖。
11.Comparable和Comparator接口是干什么的?列出它们的区别。
Comparable & Comparator 都是用来实现集合中元素的比较、排序的,只是 Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序,所以,如想实现排序,就需要在集合外定义 Comparator 接口的方法或在集合内实现 Comparable 接口的方法。
使用Comparable方式比较时,我们将比较的规则写入了比较的类型中,其特点是高内聚。但如果哪天这个规则需要修改,那么我们必须修改这个类型的源代码。如果使用Comparator方式比较,那么我们不需要修改比较的类,其特点是易维护,但需要自定义一个比较器,后续比较规则的修改,仅仅是改这个比较器中的代码
12.什么是流?按照传输的单位,分成哪两种流?他们的父类叫什么?。
JAVA程序中对数据的输入输出称为流,分为字节流,字符流。
字节流的父类:InputStream OutputStream;
字符流的父类:Reader Writer
13.什么叫对象序列化,什么是反序列化,如何实现对象序列化
序列化:将对象转换成字节序列的过程 。 序列化实质:将对象转换成二进制
反序列化:将字节序列恢复成对象的过程
实现序列化只需实现Serilazable接口即可
java中的序列化机制能够将一个实例对象(只序列化对象的属性值,而不会去序列化什么所谓的方法。)的状态信息写入到一个字节流中使其可以通过socket进行传输、或者持久化到存储数据库或文件系统中;然后在需要的时候通过字节流中的信息来重构一个相同的对象。
14.开启线程的三种方式
继承Thread,重写run方法。Thread t = new MyThread1();
实现Runnable接口,实现run方法。Thread t1 = new Thread(new MyThread3())
实现Callable,实现call方法
15.进程,线程,协程之间的区别
进程是系统资源分配的最小单位,由于进程间是隔离的,各自拥有自己的内存资源, 因此相对于线程比较安全,进程内至少有一个线程;
线程属于进程,是进程内的一个执行单元,共享进程的内存地址空间,线程是CPU调度的最小单位;
协程是属于线程的,协程的调度切换是用户手动切换的,因此更加灵活。一个线程可以多个协程。线程进程都是同步机制,而协程则是异步。协程能保留上一次调用时的状态,每次重入时,就相当于进入上一次调用的状态。
16.线程之间是如何通信的
1.等待(wait)/通知机制(notify)
等待通知机制是基于wait和notify方法来实现的,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。
2.join方式
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量耗时运算,主线程往往早于子线程结束之前结束。这时如果主线程想等待子线程完成之后再结束,比如子线程处理一个数据,主线程要取得子线程的结果进行计算,就要用到join()方法了。
join()方法内部使用 wait()方法进行等待
3.volatile关键字方式
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
4.threadLocal方式
threadLocal方式的线程通信,不像以上三种方式是多个线程之间的通信,它更像是一个线程内部的通信,将当前线程和一个map绑定,在当前线程内可以任意存取数据,减省了方法调用间参数的传递。
17.在Java中wait和seelp方法的不同
sleep()方法属于Thread类.睡眠时间一过,此线程就会自动“苏醒”不释放锁
wait()方法属于Object类中的.该方法会使当前拥有该对象锁的线程等待,直到其他线程调用了notify、notifyAll或者wait超时才会“苏醒”。会释放锁
18.谈谈ThreadLocal关键字
ThreadLocal一般称为线程局部变量,是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
void set(Object value)设置当前线程的线程局部变量的值。
public Object get()该方法返回当前线程所对应的线程局部变量。
public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
19.run()和start()方法区别
区别:调用start方法实现多线程,而调用run方法没有实现多线程
Start:
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到spu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
Run:
run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行
20.什么是线程池,为什么要用线程池,说出几种常见的线程池
什么是线程池: java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
①newSingleThreadExecutor
单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
②newFixedThreadExecutor(n)
固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
③newCacheThreadExecutor(推荐使用)
可缓存线程池, 当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
④newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程
21.描述一下线程的生命周期(描述5个状态)
1新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
22.为什么会发生死锁,如何避免死锁
因为系统资源不足。进程运行推进的顺序不合适。资源分配不当等
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
避免:
加锁顺序(线程按照一定的顺序加锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
死锁检测(针对那些不可能实现按序加锁并且锁超时也不可行)
23.多线程中的锁有哪些种类,说下区别 (?)
自旋锁 ,自旋锁的其他种类,阻塞锁,可重入锁 ,读写锁 ,互斥锁 ,悲观锁 ,乐观锁 ,公平锁 ,偏向锁, 对象锁,线程锁,锁粗化, 锁消除,轻量级锁,重量级锁, 信号量,独享锁,共享锁,分段锁
我们所说的锁的分类其实应该按照锁的特性和设计来划分
24.Synchronized和Lock锁的区别
Lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
异常是否释放锁:synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
是否响应中断:lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
25.Synchronized有什么缺陷
效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
无法知道是否成功获取到锁
26.并行与并发的异同
并发是指两个程序或以上在同一时间段上发生
并行是指两个程序或以上在同一时刻上发生(同时发生)
并发是指一个处理器同时处理多个任务。并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
27.Java如何实现并发 (?)
Java 用过synchronized 关键字来保证一次只有一个线程在执行代码块。
Volatile 关键字保证任何线程在读取Volatile修饰的变量的时候,读取的都是这个变量的最新数据。
创建thread会有很多overhead,性能低且不易管理
因为Runnable对象无法向调用者返回结果,我们可以用Callable类来返回结果。
CompletableFuture 在Future的基础上增加了异步调用的功能。callback()函数Thread执行结束的时候会自动调用。CompletableFuture既支持阻塞,也支持非阻塞的callback()
28.什么是深克隆浅克隆?描述两个的区别
浅克隆:是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
深克隆:不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象
浅克隆:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。
深克隆:是在引用类型的类中也实现了cloneable,是clone的嵌套,复制后的对象与原对象之间完全不会影响。
29.什么是双亲委派模型?
当需要加载一个类的时候,子类加载器并不会马上去加载,而是依次去请求父类加载器加载,一直往上请求到最高类加载器:启动类加载器。当启动类加载器加载不了的时候,依次往下让子类加载器进行加载。当达到最底下的时候,如果还是加载不到该类,就会出现ClassNotFound的情况。
30.什么是类加载?类加载的过程是怎样的?
类加载的步骤为,加载 -> 验证 -> 准备 -> 解析 -> 初始化。
1、加载:
获取类的二进制字节流
将字节流代表的静态存储结构转化为方法区运行时数据结构
在堆中生成class字节码对象
2、验证:连接过程的第一步,确保 class 文件的字节流中的信息符合当前 JVM 的要求,不会危害 JVM 的安全
3、准备:为类的静态变量分配内存并将其初始化为默认值
4、解析:JVM 将常量池内符号引用替换成直接引用的过程
5、初始化:执行类构造器的初始化的过程
31.Java有没有内存泄漏
理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。但是,即使这样,Java也还是存在着内存泄漏的情况。
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露。程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是Java中可能出现内存泄露的情况,例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
32.Runnable和Callable的区别
Runnable提供run方法,无法通过throws抛出异常,所有CheckedException必须在run方法内部处理。Callable提供call方法,直接抛出Exception异常。
Runnable的run方法无返回值,Callable的call方法提供返回值用来表示任务运行的结果
Runnable可以作为Thread构造器的参数,通过开启新的线程来执行,也可以通过线程池来执行。而Callable只能通过线程池执行。