通过参考网上诸多大佬的博客,归纳整理的一部分Java 面试资料,仅供大家参考
自动装箱拆箱:
一般我们要创建一个类的对象实例的时候,我们会这样:
Class a = new Class(parameter); 而当我们创建一个Integer对象时,却可以这样:int i = 100; (注意:不是 Integer i = new Integer(100); )
实际上,执行上面那句代码的时候,系统为我们执行了:Integer i = Integer.valueOf(100);
此即基本数据类型的自动装箱功能。
而当我们执行 Integer i = 1; int t = i; 时,便把对象中的数据从对象中取出,即实现了自动拆箱。
Integer 的自动拆箱:
//在-128~127 之外的数
Integer i1 =200;
Integer i2 =200;
System.out.println("i1==i2: "+(i1==i2));
// 在-128~127 之内的数
Integer i3 =100;
Integer i4 =100;
System.out.println("i3==i4: "+(i3==i4));
输出的结果是:
i1==i2: false
i3==i4: true
在Java 5后,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。当Integer需要自动装箱时,如果在整数区间-128 ~ 127 之中,会直接引用缓存中的对象,避免了新建对象。如此以来,即可避免装箱或拆箱操作频繁的创建对象。
具体实现源码如下:
@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
return i >= -128 && i <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[i + 128] : new Integer(i);
}
@HotSpotIntrinsicCandidate
public Object() {
}
@HotSpotIntrinsicCandidate
public final native Class<?> getClass();
@HotSpotIntrinsicCandidate
public native int hashCode();
public boolean equals(Object obj) {
return this == obj;
}
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
}
@HotSpotIntrinsicCandidate
public final native void notify();
@HotSpotIntrinsicCandidate
public final native void notifyAll();
public final void wait() throws InterruptedException {
this.wait(0L);
}
public final native void wait(long var1) throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
if (timeoutMillis < 0L) {
throw new IllegalArgumentException("timeoutMillis value is negative");
} else if (nanos >= 0 && nanos <= 999999) {
if (nanos > 0) {
++timeoutMillis;
}
this.wait(timeoutMillis);
} else {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
}
Cloneable
接口才能调用clone方法。返回的对象为Object类型,可通过强转转回原对象类型。例如,当向集合中插入对象时,如何判别在集合中是否已经存在该对象?(集合中不允许重复的元素存在)
),重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。String | StringBuffer | StringBuilder |
---|---|---|
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。 | 可变类,速度更快 |
不可变 | 可变类 | 可变类 |
线程安全 | 线程不安全 | |
多线程操作字符串 | 单线程操作字符串 |
当然不一样。原因很简单,因为他们不是同一个对象。
首先来看String str = “i”,这句话的意思是把“i”这个值在内存中的地址赋给str,如果再有String str3 = “i”,那么这句话也是把"i"这个值在内存中的地址赋给str3,这两个引用的是同一个地址值,他们共享同一个内存。
而String str2 = new String(”i“);则是把new String("i”)的对象地址赋给str2,需要注意的是这句话是新创建一个对象。如果再有String str4 = new String(“i”); 那么又相当于新创建了一个对象,然后把对象的地址赋给str4,虽然str2和str4 所指的对象的值是相同的,但他们仍然不是同一个对象。
需要注意的是:String str = "i”; 因为String 是final类型的,所以"i"应该是在常量池。而new String(“i”);则是把新建对象到堆内存。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
反射的应用:
在日常业务开发中很少会用到反射机制。但实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/mybatis 等框架也大量使用到了反射机制。
1) 将程序内所有 XML 或 Properties 配置文件加载入内存中;
2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性
先理解一下几个概念,同步与异步,阻塞与非阻塞。
同步和异步
同步就是一个任务的完成需要另外一个任务时,只有等被依赖的任务完成后,这个任务才算完成。要么两个任务都成功,要么都失败,两个任务的状态保持一致。
而异步I/O则不同,异步I/O不需要等被以来的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。
具体而言,就是当Java程序执行同步I/O时,Java自己处理I/O读写;而当Java程序处理异步IO时,Java程序把I/O读写委托给操作系统执行。
阻塞与非阻塞
首先来理解一下什么叫阻塞,一个I/O请求,在线程中进行,当这个I/O请求没有数据或没有有效数据来完成时,这个请求会进行等待,这个等待就是阻塞。但由于这个进程在等待数据,就会导致其它I/O操作无法完成。而非阻塞就是在当前进程阻塞的这个时候,CPU可以继续完成其它操作。虽然非阻塞表面上会提高CPU利用率,但是会增加系统的线程切换成本。所以,二者各有利弊。
接下来来整理几种IO操作的区别:
BIO:同步阻塞I/O模式:即Java运行一个I/O操作,然后等待IO操作执行完成,CPU一直等待IO操作执行,等这个操作执行后再执行其它操作。
NIO:同步非阻塞模式:由Java程序执行一个I/O操作,但执行I/O操作期间CPU没有一直在等待该操作执行,而去执行其它操作。期间CPU不断检查一下I/O进程执行情况,看是否执行完毕,好执行下一步操作。
AIO:异步非阻塞I/O模式:这种I/O模式CPU不进行等待,I/O操作执行完后通知CPU,然后CPU再执行后续代码。
三种模式各有优缺点,
BIO
是最简单的一种用法,也是成本最低的一种,但CPU大部分时间处于空闲状态。主要应用于Apache,Tomcat等并发量不高的场景。
NIO
是提升性能的常用手段,常用于网络连接,和 BIO比,虽然能提升性能,但是会增加CPU功耗。主要应用于Nginx,Netty等高并发量场景。
AIO
:这种组合方式比较复杂,只有非常复杂的分布式情况下会使用适用于连接数目多且连接长的架构,充分调用OS参与并发工作。JDK7开始支持。
创建
createNetFile()
在指定位置创建一个空文件,成功就返回true,如果已存在就不创建,返回falsemkdir()
在指定位置创建一个单极文件夹。mkdirs()
在指定位置创建一个多级文件夹。删除
delete()
删除文件或者一个文件夹,不能删除非空文件夹,马上删除文件,返回一个布尔值。deleteOnExit()
jvm退出时删除文件或者文件夹,用于删除临时文件,无返回值。判断
exists()
文件或文件夹是否存在isFile()
是否是一个文件,如果不存在,则始终为falseisAbsolute
测试此抽象路径名是否为绝对路径名获取
getName()
获取文件或文件夹的名称,不包含上级路径。getAbsolutePath()
获取文件的绝对路径,与文件是否存在没有关系。文件夹相关
list()
返回目录下的文件或者目录名,包含隐藏文件。对于文件这样操作会返回null.listFiles()
返回目录下的文件或者目录对象(File类实例),包含隐藏文件。对于文件这样操作会返回null。Collection
Map
ArrayList | LinkedList | Vector | |
---|---|---|---|
存储结构 | 基于数组实现的 | 基于双向链表实现 | 基于数组实现的 |
线程安全性 | 不具有有线程安全性,多线程环境下需使用synchronizedList | 不具有有线程安全性,多线程环境下需使用synchronizedList | Vector实现线程安全的,即它大部分的方法都包含关键字synchronized,但是Vector的效率没2有ArraykList和LinkedList高。: |
扩容机制 | Object的数组形式来存储的,元素不够时,扩容至原来的1.5倍 | Object的数组形式来存储的,元素不够时,扩容至原来的2倍 |
迭代器是一种设计模式,也是一种轻量级对象,创建它的代价非常小。它提供了一些专用的方法来统一处理集合中的元素,为各种容器都提供了公共的操作接口,隔离对容器的遍历和底层实现,从而进一步降低代码耦合度。
具体作用过程:
首先,创建了一个List的集合对象,并放入了俩个字符串对象,然后通过iterator()方法得到迭代器。iterator()方法是由Iterable接口规定的,ArrayList对该方法提供了具体的实现,在迭代器Iteartor接口中,有以下3个方法:
1、hasNext() 该方法英语判断集合对象是否还有下一个元素,如果已经是最后一个元素则返回false
2、next() 把迭代器的指向移到下一个位置,同时,该方法返回下一个元素的引用
3、remove() 从迭代器指向的Collection中移除迭代器返回的最后一个元素,该操作使用的比较少。
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
ListIterator<Integer> it=list.listIterator();//返回一个list接口中的特有迭代器
while (it.hasNext()) {
System.out.println(it.next());
}
因为ArrayList的底层是由数组实现的,并且数组的默认值为10,如果插入10000条数据的话需要不断对数组进行扩容,会浪费大量的时间。所以当我们已经加入数据量较大时,可以调用ArrayList的指定容器的构造方法
public static void main(String[] args) {
int n = 10000000;
Object o = new Object();
List<Object> list1 = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0;i < n; i++) {
list1.add(o);
}
System.out.println(System.currentTimeMillis() - start);
List<Object> list2 = new ArrayList<>(n);
long start2 = System.currentTimeMillis();
for (int i = 0;i < n; i++) {
list2.add(o);
}
System.out.println(System.currentTimeMillis() - start2);
}
动态数组
的数据结构,LinkedList是基于链表
的数据结构。add
和remove
,LinkedList
比较占优势,因为ArrayList
要移动数据。总的来说,当操作是一列数据的后面加数据而不是在前面或中间,并且需要随机访问其中元素时,使用ArrayList会提供比较好的性能,当你的操作是一系列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList。
HashMap采用数组+链表实现,即通过链表处理冲突,同一Hash的值的元素都存储在同一个链表中。数据元素使用Entry节点存储,每个Entry就是一个key-value的键值对。HashMap底层用一个Entye数组来保存所有的key-value键值对。在JDK1.6,JDK1.7中,HashMap使用的是头插法,但头插法容易出现逆序或出现环形链表死循环问题
(见3.9)。但在JDK1.8之后变成了数组+链表+红黑树使用了尾插法,从而能够避免出现逆序且链表死循环问题。当需要存储一个Entry对象时,会根据hash算法来决定在其数组中的位置,在根据hash算法找到其在数组中的存储位置;当需要取出一个Entry对象时,也会根据hash算法找到其在数组中的存储位置,根据equals方法从该位置上的链表取出Entry。
扩容机制
首先,默认初始化capacity
(容量)为16,负载因子,计算出来一个threshold(阈值)作为一个扩容的阈值。在put时先判断,size是否大于阈值,如果大于阈值,就要进resize()
操作,会扩容为原来的2倍,把原来的数组进行resize()
操作。
resize函数源码解读
Hash函数源码解析:源码解析
HashMap源码解读:源码解析
LinkedHashMap 源码解读:源码解析
1.put的时候导致多线程数据不一致
比如有两个线程A和B,首先A希望插入一个记录(键值对)到HashMap中,首先要计算落到的桶的坐标,然后获取该桶里面的链表头节点,此时线程A的时间片用完了,而此时线程B被调度执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面。假设线程A插入的记录计算出来的索引值和线程B计算出来的是一样的,那么当线程B成功插入后,线程A再次被调度执行,它仍然持有过期的链表头但它对此一无所知,以至于它覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据的不一致。
我们假设有两个线程同时需要执行resize操作,我们原来的桶数量为2,记录数为3,需要把桶扩容到4,原来的记录分别是:[3,a],[7,b],[5,c]
。然后看扩容部分的源代码:
do {
Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
假设线程A执行到了Entry next = e.next这一句,时间片用完了就被挂起,此时的e = [3,a] , next = [7 , b]。线程B被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,b]的next为[3,a]。此时线程A重新被调度运行,此时的A持有的引用是已经被线程B resize之后的结果,那么这时候e = [7,b],next = [3,a]。线程A 首先将[7,B]迁移到新的数组上,然后再处理[3,A],这时候[7,b]被连接到了[3,a]的后面。注意此时因为线程B的resize导致了[7,B]的next已经指向了[3,A],环形链表出现了,造成了线程安全问题。
我们都知道CurrentHashMap
和HashTable
都可以用于多线程环境,但当HashTable的大小增加到一定的时候,性能会急剧下降。因为HashTable实现线程安全的原理是将整个Hash表锁住,数据量增大的时候迭代需要被锁定很长时间。而ConcurrentHashMap引入了分割,不论它变的多么大,仅仅锁住map的某个部位,其它的线程不需要等到迭代完才能访问map。总而言之,在迭代的过程中,CurrentHashMap仅仅锁住map的某个部分,而HashTable则会锁住整个map。
ConcurrentHashMao 采用了非常精妙的“分段锁”,主干由若干个Segment
数组实现。
//继承ReentrantLock ,实现一把分段锁
static class Segment<K, V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) {
this.loadFactor = lf;
}
}
Segment继承了ReentrantLock,所以它是一种可重入锁。在ConcurrentHashMap,一个Segment就是一个子Hash表,Segment里面维护了一个HashEntry数组,并发环境下,对于不同的Segment的数据进行操作是不用考虑锁竞争的。对于同一个Segment的操作才考虑线程同步。
换句话说,ConcurrentHashMap相对于很多个HashMap,Segment类似于一个HashMap,一个Segment维护了一个HashEntry数组。