前言
各位见面爱好者,我们又加瓦了!哈哈~
以下是我根据面试经验总结的一些常见的关于java基础的面试题目。做了一下总结,方便以后自己复习。
有需要的同学也可以收藏,后面我遇到新的面试题目会不断更新上去。
我尽量详细点回答,同学们千万不要死记硬背,要理解后用自己语言去总结概括,才能记得牢固。废话不多说,请看题吧~哈哈创作不易,不要白票,点个赞吧,关注一下,大佬们。陆续更新。微信公众号可搜索同名「 java技术爱好者
基础知识
JVM、JRE、JDK有什么联系与区别?
JVM是java虚拟机,能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。JRE是java运行时环境,它主要包含两个部分,jvm 的标准实现和 Java 的一些基本类库。它相对于 jvm 来说,多出来的是一部分的 Java 类库。换句话说,JRE包含JVM。JDK是java开发工具包,它集成了 jre 和一些好用的小工具。例如:javac.exe,java.exe,jar.exe 等。JDK包含JRE。
所以总得来说,JDK>JRE>JVM。
面向对象的特征有哪些?
有三大特征,继承,封装,多态。
为什么java可以实现跨平台?
因为java是编译成.class文件运行在JVM上的。针对不同的系统有不同的JVM实现,在不同的JVM实现上会映射到不同系统的 API 调用,从而实现代码的跨平台运行。
类的加载顺序?
静态成员变量、静态代码块、实例成员变量,实例代码块,构造器,实例方法。
接口和抽象类有什么共同点和不同点?
共同点:
1.都可以定义抽象方法,子类都要实现定义的抽象方法。
2.都不能被实例化,但是可以定义抽象类和接口类型的引用。
不同点:
1.接口没有构造器,抽象类可以定义构造器。
2.接口定义具体方法只能定义default修饰,抽象类可以直接定义具体方法。
3.接口的子类是实现接口,关键字是implements,抽象类的子类是继承,关键字是extends。
4.接口不能定义成员变量,只能定义常量。抽象类可以定义成员变量。
static关键字有哪些用法?
①修饰成员变量,用static修饰的成员变量就成为静态变量,静态变量只会存在一份,在类被加载时会初始化,且只会加载一次,通过类名访问。一般可以用static和final定义一些String类型,boolean类型,int类型的变量作为常量,可以减少资源的消耗。
②static修饰方法,该方法就被定义为静态方法,静态方法是不能被方法重写的,通过类名调用。一般用static定义一些工具类的方法。
③用static修饰代码块,该代码块就被定义为静态代码块,静态代码块在类初始化时被执行,且执行一次。一般用于初始化一些静态的成员变量的值。
Switch能用什么数据类型作为参数?
JDK1.5前:byte、short、char、int
JDK1.5:枚举
JDK1.7:String
枚举有哪些特点?在项目中如何使用?
特点:
1.枚举的构造器是私有的。
2.枚举不能被继承。
3.枚举是绝对的单例,即使是反序列化也无法创建多个实例。
使用场景:
当变量只能从一堆固定的值中取出一个时,那么就应该使用枚举。比如时间的单位,季度等等。
什么是方法重载?什么是方法重写?
方法重载,一个类中允许同时存在一个以上的同名方法,主要体现在方法参数的类型和数量不同,方法名相同,与访问修饰符和返回值类型都是无关的。口诀是"一同两不同"。方法重写一般在继承中,子类重写父类的方法,既然是重写一遍,那么方法名和参数部分一定是相同的。只是实现的功能不同。声明为 final 的方法不能被重写,声明为 static 的方法不能被重写,声明为 private 的方法不能被重写。
静态变量和实例变量有什么不同?分别位于内存的什么区域?
1.静态变量使用static修饰,实例变量不需要。
2.静态变量在类被加载时就会分配内存空间,就可以使用。实例变量需要实例对象才会分配内存空间,才可以被引用,是属于实例的。
3.静态变量是存在于静态区(全局区)的,实例变量位于堆内存中。
java的内部类的分类有哪些?
实例内部类、静态内部类、局部内部类、匿名内部类。
break、continue、return 的作用是什么?
- break:结束循环。不仅可以结束其所在的循环,还可结束其外层循环。
- continue:跳过本次循环,开始下一次循环。
- return:不是专用于结束循环,而是用于结束方法。如果在循环中使用return,就会结束整个方法,循环当然也会结束。
Object类有哪些常用的方法?
toString()、equals()、hashCode()。
-
toString()
默认输出对象的内存地址,一般不希望输出内存地址可以重写toString()方法。 -
equals()
方法用于比较对象是否相等,默认比较是内存地址,所以要正确比较两个对象是否值相等,此方法必须被重写。 -
hashCode()
方法用来返回其所在对象的物理地址(哈希码值),常会和equals()
方法同时重写,确保相等的两个对象拥有相等的hashCode。
==与equals()的区别?
equals()
方法属于Object
对象的,所以比较基础数据类型是不能使用equals()
。必须使用==
。在默认情况下,equals()
与==
是一样的,都是比较内存地址。所以在业务逻辑中,我们一般会重写equals()
方法。
equals()与hashCode()有什么联系?
1.equals()
相等的两个对象他们的hashCode()
肯定相等,也就是用equals()
对比是绝对可靠的。
2.hashCode()
相等的两个对象他们的equals()
不一定相等,也就是hashCode()
不是绝对可靠的。
在使用HashSet
或者HashMap
集合中,比较两个对象是否相等时,会先调用hashCode()
比较,如果hashCode()
相等,则会继续调用equals()
比较,equals()
也相等才会认为是同一个对象。如果hashCode()
返回不相等,则认为是不相等的对象。
所以一般我们会同时重写hashCode()
和equals()
方法。
& 和 &&有什么区别?
&&
具有短路的功能,也就是如果&&
左边的条件为fasle
就不再执行后面的条件判断。&
则会执行完左右两边的条件判断。
final、finalize()、finally{}分别有什么作用?
final
修饰类,表明这个类不可被其他类继承。final
修饰成员变量,表示此变量为常量,只能在初始化时被赋值一次,赋值后不能修改。final
修饰方法。把方法锁定,不能被子类重写,以防止子类对其进行更改。finalize()
是Object
里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。一个对象的finalize()
方法只会被调用一次。finally
作为异常处理的一部分,它只能用在try/catch
语句中,并且附带一个语句块。
Cloneable接口有什么作用?
Cloneable
接口是一个标记接口,实现了此接口,表示可以使用clone()
方法,没有实现此接口使用clone()
会抛出CloneNotSupportedException
异常。
什么是浅克隆,什么是深克隆?
浅克隆是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
深克隆不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。
什么是序列化?什么是反序列化?
序列化:把对象转换为字节序列的过程称为对象的序列化。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
Serializable接口有什么作用?
Serializable
接口是一个标记接口,一个类只有实现了Serializable
接口,它的对象才是可序列化的。否则序列化时会报NotSerializableException
异常。如果不显性声明serialVersionUID
,则会默认生成一个。为了serialVersionUID
的确定性,最好是显性声明。
String、StringBuffer、StringBuilder有什么区别?
-
String
被声明为final class
,是由定义final
的字符数组实现的,因为它的不可变性,所以拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。 -
StringBuffer
是由定义了临时数据transient
的字符数组实现的,提供append()
和add()
方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列,所有修改数据的方法都加上synchronized
。性能相对StringBuilder
会差一点。 -
StringBuilder
和StringBuffer
本质上没什么区别,区别是去掉了保证线程安全的synchronized
,减少了开销,性能有所提高。
什么是泛型?什么是泛型的上界和下界?
Java 泛型是 JDK1.5中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
上界用extends
关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
下界用super
进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object。
什么是反射机制?
Java反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法,对于任意一个对象都能够调用它的任意一个属性和方法。这种在运行时动态的获取信息以及动态调用对象的方法的功能称为Java的反射机制。
获取Class对象的方式有哪些?
- 通过
Object
类中的getClass()
方法,想要用这种方法必须要明确具体的类并且创建该类的对象。 - 所有数据类型都具备一个静态的属性
.class
来获取对应的Class
对象。但是还是要明确到类,然后才能调用类中的静态成员。 - 通过
Class.forName()
方法完成,必须要指定类的全限定名,由于前两种方法都是在知道该类的情况下获取该类的字节码对象,因此不会有异常,但是Class.forName()
方法如果写错类的路径会报ClassNotFoundException
的异常。
java中的异常有哪几种异常?
Throwable
类是Java
异常类型的顶层父类,Throwable
包含了Error
和Excetion
。Excetion
分为两种,一种是非运行时异常(又称为检查异常),另一种是运行时异常(RuntimeException)。
java是如何处理异常的?
-
Error
是程序无法处理的, 比如OutOfMemoryError
、OutOfMemoryError
等等, 这些异常发生时,JVM
一般会终止线程。 - 运行时异常(
RuntimeException
),如NullPointerException
、IndexOutOfBoundsException
等,是在程序运行的时候可能会发生的,所以程序可以捕捉,也可以不捕捉。这些错误一般是由程序的逻辑错误引起的,程序应该从逻辑角度去尽量避免。 - 非运行时异常是
RuntimeException
以外的异常,是Exception
及其子类,这些异常从程序的角度来说是必须经过捕捉检查处理的,否则不能通过编译。如IOException
、SQLException
等。
java集合、IO流、日期处理等
常用的集合有哪些?
常用集合有Map、List、Set。
HashMap是线程安全的吗?
不是线程安全的。
如何使HashMap线程安全?
使用Collections
类的synchronizedMap()
方法包装。
Map<String, Object> map = Collections.synchronizedMap(new HashMap<>());
使用java.util.concurrent
包下的ConcurrentHashMap
类也可以获得线程安全的Map。
ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
使用Hashtable
类,也可以获得线程安全的Map
Map<String,Object> hashtable = new Hashtable<>();
HashMap和Hashtable的区别是什么?
-
Hashtable
继承自Dictionary
类,而HashMap
继承自AbstractMap
类。但二者都实现了Map接口。 -
Hashtable
是线程安全的,HashMap
是线程不安全的。 -
Hashtable
中,key和value都不允许出现null值。 -
HashTable
在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable
不要求底层数组的容量一定要为2的整数次幂,而HashMap
则要求一定为2的整数次幂。Hashtable
扩容时,将容量变为原来的2倍加1,而HashMap
扩容时,将容量变为原来的2倍。
HashMap是如何解决哈希冲突的?
- 在JDK1.8前,
HashMap
是采用链表法解决哈希冲突的。当put()
一个值到Map
时,会通过Key
拿到一个哈希值,通过哈希值获取数组下标,先查询是否存在该hash
值。若不存在,则直接以Entry
的方式存放在数组中。若存在,则再调用equals()
方法对比key
是否相同,若hashcode()
值和key
都相同,则替换value
,若hashcode()
值相同,key
不相同,则形成一个单链表,将hashcode()
值相同,key
不同的元素以Entry
的方式存放在链表中,这样就解决了哈希冲突。 - JDK1.8以后,当链表的长度达到某个限制值(默认是8),就会转换成红黑树,提高性能。
HashMap初始大小是多少?负载因子是多少?
默认的数组初始大小是16。负载因子是0.75。(为什么初始值是2的n次方,为什么负载因子取0.75,这两个问题可以网上找资料看看,这里就不详述了)
简述一下HashMap的扩容机制?
HashMap
是懒加载的,当调用put()
方法时,会先初始化Map
的大小,默认数组长度是16,负载因子是0.75,所以阈值是12。当HashMap
元素的个数超过阈值时,就会把数组的大小扩展到原来的2倍,然后重新计算每个元素在数组中的位置。
List有哪些常用的子类?
ArrayList
和LinkedList
。
ArrayList和LinkedList有什么区别?
- 底层数据结构不同。
ArrayList
基于数组+动态扩容实现的,LinkedList
基于双向链表实现。从储存结构上分析,LinkedList
更加占内存,因为每个节点除了存储数据外还要存储指向前节点的引用和指向后节点的引用。 - 效率不同。当随机访问时,
ArrayList
是基于数组下标访问,查询效率较高,但是由于数组的长度是固定的,所以当添加的元素到一定的阈值时会扩容数组,消耗性能,增删效率偏低。LinkedList
在查询时,需要从前到后依次遍历,所以查询效率不高,但是在增删时只需要更改节点的引用,开销较少,所以增删效率较高。
List集合排序的方式有哪些?
使用List接口定义的sort()方法。
list.sort(Comparator.comparingInt(User::getAge));
使用Collections
的sort()
方法,排序的对象需要实现Comparable
接口,重写compareTo()
方法。
//实现Comparable接口
public class User implements Comparable<User> {
//重写compareTo方法
@Override
public int compareTo(User user) {
return Integer.compare(this.getAge(), user.getAge());
}
}
使用Collections
的sort()
方法
Collections.sort(list);
//如果不想实现Comparable接口,也可以使用这个方法
Collections.sort(list,Comparator.comparingInt(User::getAge));
使用Stream流操作的sort()
方法,传入一个Comparator
接口。
list.stream().sorted(Comparator.comparingInt(User::getAge)).collect(Collectors.toList());
栈和队列的特点分别是什么?在java中有哪些实现的类?
栈是先进后出,队列是先进先出。Stack
类是栈在java中的实现,继承Vector
类,底层是基于数组存储数据。Queue
接口是队列在java中的代表,Queue
接口有几个常用的子类ArrayDeque
、LinkedList
。
IO、NIO有什么区别?
IO包括:File
、OutputStream
、InputStream
、Writer
,Reader
。
NIO三大核心:selector
(选择器),channel
(通道),buffer
(缓冲区)
NIO与IO区别在于,IO面向流,NIO面向缓冲区。IO是阻塞,NIO是非阻塞。
如何进行日期的转换?
使用SimpleDateFormat
类进行String
和Date
之间的转换。
如何获取上一年的今天的日期?
使用Calendar
对象。如下所示:
//创建Calendar对象
Calendar calendar = Calendar.getInstance();
//设置年份,当前年份减去一年
calendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR) - 1);
//以下是打印结果
Date time = calendar.getTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(time));//2019-06-08 23:43:14 正确
BigDecimal类型一定不会失真吗?
不一定。
参数类型为double
的构造方法的结果有一定的不可预知性,是有可能产生失真的。
BigDecimal bigDecimal = new BigDecimal(0.99);
System.out.println(bigDecimal);//结果如下
//0.9899999999999999911182158029987476766109466552734375
使用参数类型String
构造方法是完全可预知的,不会产生失真。所以在开发中推荐使用参数类型String
构造方法。
java并发编程
为什么要使用多线程?
- 避免主线程阻塞,可以使用多线程做成异步调用。
- 提升性能,充分利用CPU资源。
创建线程有哪几种方法?
- 通过继承
Thread
类创建线程类。 - 通过实现
Runnable
接口创建线程类。 - 通过实现
Callable
接口创建线程类。
如何获取多线程的返回值?
使用Callable
和FutureTask
接口,获取返回值。
Callable接口实现多线程
多线程的生命周期?
新建状态、就绪状态、运行状态、阻塞状态、死亡状态
如何进行线程之间的通信?
- 使用
synchronized、wait()、notify()
- 使用JUC工具类
CountDownLatch
- 使用
ReentrantLock
结合Condition
- 基本
LockSupport
实现线程间的阻塞和唤醒
以上几种方式的具体实现代码,可以网上找一下资料,这里不演示了。
说说 sleep() 方法和 wait() 方法区别和共同点?
相同点:
-
sleep()
方法和wait()
方法都用来改变线程的状态,能够让线程从运行状态,转变为休眠状态。
不同点:
-
sleep()
方法是Thread
类中的静态方法,而wait()
方法是Object
类中的方法。 -
sleep()
方法可以在任何地方调用,而wait()方法只能在同步代码块或同步方法中使用(即使用synchronized
关键字修饰的)。 - 这两个方法都在同步代码块或同步方法中使用时,
sleep()
方法不会释放对象锁。而wait()
方法则会释放对象锁。
如何停止线程?
- 使用退出标志,使线程正常退出,也就是当
run()
方法完成后线程终止。 - 使用
stop()
方法强行终止(不推荐),可能会出现数据不同步,或者资源未释放等问题。 - 使用
interrupt()
方法中断线程。
什么是线程的死锁?如何避免线程死锁?
多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进,这种现象称为死锁。
避免死锁的三种方式:
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
线程池的作用?
- 利用线程池管理并复用线程,减少创建线程和销毁线程的资源消耗。
- 实现任务线程队列缓存策略和拒绝机制。
- 可以对线程进行统一的分配,监控和调优。
- 提供定时执行、最大线程数、并发数控制等功能。
创建线程池的重要参数分别代表什么意思?
-
corePoolSize
线程池核心线程大小。在没有设置allowCoreThreadTimeOut
为true
的情况下,核心线程会在线程池中一直存活,即使处于闲置状态。当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize
,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize
。 -
maximumPoolSize
线程池最大线程数量。线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize
,则线程池会创建新的线程来执行任务。对于无界队列可以忽略此参数。 -
keepAliveTime
线程存活保持时间。当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。 -
unit
空间线程存活时间单位。 -
workQueue
任务队列:用于传输和保存等待执行任务的阻塞队列。
①ArrayBlockingQueue
,基于数组的有界阻塞队列,按FIFO排序。
②LinkedBlockingQuene
,基于链表的无界阻塞队列(其实最大容量为Interger.MAX
),按照FIFO排序。当使用该队列时,maximumPoolSize
参数可以忽略。
③SynchronousQuene
,一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。
④PriorityBlockingQueue
,具有优先级的无界阻塞队列,优先级通过参数Comparator
实现。 -
threadFactory
线程工厂,用于创建新线程。 -
handler
线程饱和策略,当线程池和队列都满了,再加入线程会执行此策略。
线程池中submit() 和 execute()方法有什么区别?
- 参数不同
submit()
方法有三个重载方法。
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
execute()
方法只有一个
void execute(Runnable command);
-
execute()
没有返回值;而submit()
有返回值 -
submit()
的返回值Future
调用get()
方法时,可以捕获处理异常。而execute()
没有返回值不能捕获异常。
有哪些常用的线程池?
Executors.newCacheThreadPool()
:可缓存线程池,先查看池中有没有已建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。Executors.newFixedThreadPool()
:可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。Executors.newScheduledThreadPool(int n)
:定长线程池,支持定时及周期性任务执行。Executors.newSingleThreadExecutor()
:单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
什么是线程安全问题?如何保证线程安全?
当多个线程同时共享,同一个全局变量或者静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。
java的内存模型?
java的内存模型规定了所有的变量都存储在主内存中,每个线程拥有自己的工作内存,工作内存保存了该线程使用到的变量的主内存拷贝,线程对变量所有操作,读取,赋值,都必须在工作内存中进行,不能直接写主内存变量,线程间变量值的传递均需要主内存来完成。
volatile关键字有什么作用?volatile一定能保证原子性吗?
volatile关键字有什么作用:
- 内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态。
- 禁止指令重排。
volatile
是Java提供的一种轻量级的同步机制,并不能保证原子性。
什么是指令重排?
指令重排是指JVM
在编译Java代码的时候,或者CPU在执行JVM
字节码的时候,对现有的指令顺序进行重新排序。
指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。
synchronized的使用方式?
- 修饰实例(非静态方法。被锁对象是类的实例(
this
)。 - 修饰静态方法。被锁对象是类对象。
- 同步代码块。有三种形式。
①synchronized(this){}
,被锁对象是类的实例。
②synchronized(XXX.Class)
,被锁对象是类对象。
③synchronized(new Object())
,被锁对象是实例对象object
。
Lock锁的使用方式?
1.获取锁。2.上锁。3.释放锁。
注意点:释放锁最好放在finally{}
代码块中,保证能执行释放锁。
什么是乐观锁、什么是悲观锁?
- 悲观锁:它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。具有强烈的独占和排他特性。
- 乐观锁:乐观锁认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的重提与否进行检测,如果发现冲突了,程序自动去重试。相对于悲观锁,在高并发的场景下有更好的性能表现,通常用"版本号"实现。
synchronized与Lock锁的区别?
-
synchronized
是java内置关键字,在jvm
层面。Lock
是个java类。 -
synchronized
无法判断是否获取锁的状态。Lock
可以判断是否获取到锁。 -
synchronized
会自动释放锁。Lock
锁需要在finally{}
代码块中手工释放锁。 -
synchronized
的锁可重入、不可中断、非公平。而Lock
锁可重入、可判断、可公平(两者皆可)。
有哪些常用的线程安全的集合?
ConcurrentHashMap
、Vector
、Hashtable
、Stack
。还可以使用Collections包装方法
获得线程安全的集合。
CAS是什么,有什么问题,如何解决?
CAS
是compare and swap
的缩写,意思是比较与交换。CAS
是乐观锁的一种实现。CAS操作包含三个操作数---内存位置的值(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置更新为新值。否则,处理器不做任何操作。
CAS
有以下缺点:
- ABA问题: 线程C、D。线程D将A修改为B后又修改为A,此时C线程以为A没有改变过。这个问题通常可以使用版本号来解决。
- CPU开销过大。在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
-
CAS
机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized
了。
什么是ThreadLocal?
这个类提供了线程局部变量也称为线程本地变量,它为变量在每个线程中创建了一个副本,通过这样的方式做到变量在线程间隔离且在方法间共享的场景。
ThreadLocal是如何保证线程安全的?
ThreadLocal
存储的值不是线程共享的,而是属于线程的。内部会维护一个ThreadLocalMap
,key是当前线程的ThreadLocal
,value是存储的值。换句话说,每个线程都有自己的值,当然不会出现线程安全问题了。
源码如下:
ThreadLocal的set()方法源码
JVM相关
什么是JVM内存模型?
Java内存模型(Java Memory Model,简称为JMM),是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
根据java虚拟机规范,JVM内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分。
栈内存溢出(StackOverflowError)的常见原因有哪些?
栈溢出原因就是方法执行时创建的栈帧超过了栈的深度。最有可能的就是方法递归调用产生这种结果。
堆内存溢出(OOM)的常见原因有哪些?
-
OutOfMemoryError: Java heap space
。在创建新的对象时, 堆内存中的空间不足以存放新创建的对象时发生。产生原因:程序中出现了死循环,不断创建对象;程序占用内存太多,超过了JVM堆设置的最大值。 -
OutOfMemoryError: unable to create new native thread
。产生原因:系统内存耗尽,无法为新线程分配内存;创建线程数超过了操作系统的限制。 -
OutOfMemoryError: PermGen space
。永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。 -
OutOfMemoryError:GC overhead limit exceeded
。超过98%的时间都在用来做GC并且回收了不到2%的堆内存。连续多次的GC,都回收了不到2%的极端情况下才会抛出。
JVM参数调优有哪些重要的参数?分别有什么作用?
- -Xms 初始堆内存大小。
- -Xmx 最大堆内存大小。
- -Xss 每个线程的栈大小。
- -XX:+PrintGC 每次GC时打印相关信息。
- -XX:Newratio 设置年轻代和老年代的比例,比如值为2,则老年代是年轻代的2倍。
- -XX:Newsize 设置年轻代的初始值大小。
- -XX:Maxnewsize 设置年轻代的最大值大小。
GC垃圾回收机制,有哪些垃圾回收算法?
标记-清除算法、复制算法、标记整理算法、分代收集算法。
JVM如何判断对象是否可以回收?
会使用可达性分析算法进行判断,原理是从一系列被称为GC ROOT
的对象开始,向下搜索,搜索走过的路径称为引用链,当一个对象到GC ROOT
之间没有引用链,说明这个对象不可用,那么就会被GC回收。
什么是强引用、软引用、弱引用、虚引用?
强引用。一般new
出来的对象都是强引用。如果一个对象具有强引用,GC
绝不会回收它;当内存空间不足,JVM宁愿抛出OutOfMemoryError
错误。
//强引用
Object obj = new Object();
软引用。如果一个对象只具有软引用。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
//软引用
SoftReference<Object> softReference = new SoftReference<>(new Object());
弱引用。如果一个对象具有弱引用,在GC线程扫描内存区域的过程中,不管当前内存空间足够与否,都会回收内存。
//弱引用
WeakReference<Object> weakReference = new WeakReference<>(new Object());
虚引用。如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收。
//虚引用
PhantomReference<Object> phantomReference = new PhantomReference<>(new Object(), new ReferenceQueue<>());
什么是类加载器?
Java类加载器是Java运行时环境的一部分,负责动态加载Java类到JVM的内存空间中。
什么是双亲委派机制?
双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。
类的生命周期?
加载、验证、准备、解析、初始化、使用、卸载。
有些资料会把(验证、准备、解析)归纳为连接,于是就变成:加载、连接、初始化、使用、卸载。
设计模式相关
如何实现单例模式?
- 饿汉式单例模式。定义一个静态成员变量,把构造器私有化,只对外暴露一个获取实例的方法。
饿汉式单例模式
- 懒汉式单例模式。非线程安全。基本和上面饿汉式实现方式类似,只是在获取时再判断是否创建实例,但是会有线程安全问题。
懒汉式单例模式
- 使用静态内部类实现
静态内部类单例模式
- 使用枚举实现
枚举实现单例模式
如何实现线程安全的单例模式?
饿汉式实现、枚举、静态内部类都是线程安全的实现方式。
还可以使用双检锁的懒汉式方式实现:
双检锁单例模式
为什么要使用工厂模式创建对象?
- 解耦。把对象的创建和使用的过程分开。
- 可以降低代码重复。如果创建B过程都很复杂,需要一定的代码量,而且很多地方都要用到,那么就会有很多的重复代码。
- 减少了使用者因为创建逻辑导致的错误。因为工厂管理了对象的创建逻辑,使用者并不需要知道具体的创建过程,只管使用即可。
- 提高了代码的可维护性。如果发生业务逻辑变化,不需要找到所有需要创建对象的地方去逐个修正,只需要在工厂里修改即可。
在java中,实现代理模式有哪几种方式?
- 静态代理。
- JDK动态代理。
- CGlib动态代理。
JDK动态代理和CGlib动态代理的区别?
(1)JDK动态代理
只能对实现了接口的类生成代理,而不能针对类。
(2)CGLIB
是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。
因为是继承,所以该类或方法不能声明成final
。
策略模式的使用场景?
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
- 需要在不同情况下使用不同的策略(算法),或者策略还可能在未来用其它方式来实现。
- 对客户隐藏具体策略(算法)的实现细节,彼此完全独立。
装饰器模式解决了什么问题?在java中有哪些应用了装饰器模式?
使用场景:
- 扩展一个类的功能。
- 动态增加功能,动态撤销。
java中经典的例子就是I/O流。具体分析过程可以参考我写的这篇文章:装饰者模式与IO流的应用。
经典算法
有哪些经典的排序算法?
插入排序、冒泡排序、归并排序、快速排序、堆排序、桶排序、基数排序等等。
冒泡排序的时间复杂度和空间复杂度?
平均的时间复杂度是O(n^2)
,最好的情况是O(n)
,最坏的情况是O(n^2)
。空间复杂度是O(1)
。
哪一种排序算法的时间复杂度比较稳定?
归并排序。最好和最坏的情况下,时间复杂度都是O(n*log n)
。
如何实现二分查找?二分查找的时间复杂度?(笔试常见)
有两种方式,迭代法和递归法。时间复杂度是O(log n)
。
递归法实现二分查找
迭代法实现二分查找
跳楼梯的问题。(笔试常见)
这是一个经典的斐波那契数列问题。力扣题库第70题。可以看看大佬们的题解。这是我的题解,使用了Map
作为缓存,减少一些不必要的递归,效率还不错。执行时间:1 ms。当然你去掉那个Map
也是完全没错的,只是运行时间会久一些,可能会超出leetcode
的时间限制,没法通过。
跳楼梯算法题题解