熬夜整理了这么多年来的Java基础面试题,欢迎学习收藏,手机上可以点击这里,效果更佳https://mp.weixin.qq.com/s/ncbEQqQdJo0UaogQSgA0bQ
1.1 Hashmap 与 concurrentHashMap (重点)
- hashMap 1.7、8 put过程
- concurrentHashMap 1.8 put过程
@w=400
-
怎么解决冲突的(链表或者红黑树)
-
sizeCtl (concurrentHashMap 1.8 中的数据结构)等关键成员变量的作用:
Node:存放实际的key和value值。
sizeCtl:负数:表示进行初始化或者扩容,-1表示正在初始化,-N,表示有N-1个线程正在进行扩容
正数:0 表示还没有被初始化,>0的数,初始化或者是下一次进行扩容的阈值。
TreeNode:用在红黑树,表示树的节点, TreeBin是实际放在table数组中的,代表了这个红黑树的根。
-
concurrentHashmap 1.8为什么放弃了分段锁 (锁的粒度更小,减小并发冲突概率)
-
HashMap的时间复杂度?
HashMap容器O(1)的查找时间复杂度只是其理想的状态,而这种理想状态需要由java设计者去保证。
jdk1.7中的hashMap在最坏情况下,退化成链表后,get/put时间复杂度均为O(n);jdk1.8中,采用红黑树,复杂度可以到O(logN);如果hash函数设计的较好,元素均匀分布,可以达到理想的O(1)复杂度。
- Java8中的HashMap有什么变化?
1). 数据结构不同:jdk7 数组+单链表; jdk8 数组+(单链表+红黑树) 。
JDK7 在hashcode特别差的情况下,比方说所有key的hashcode都相同,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表。也就是说时间复杂度在最差情况下会退化到O(n)。
JDK8 如果同一个格子里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。那么即使hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销。也就是说put/get的操作的时间复杂度最差只有O(log n)。
2). 链表中元素位置不同:jdk7头插法;jdk8 链表尾插。
头插: 最近put的可能等下就被get,头插遍历到链表头就匹配到了,并发resize可能产生循环链。
尾插:保证了元素的顺序,并发resize过程中可能发生数据丢失的情况。
3). 扩容的处理不同:jdk7中使用hash和newCapacity计算元素在新数组中的位置;jdk8中利用新增的高位是否为1,来确定新元素的位置,因此元素要么在原位置,要么在原位置+扩容的大小值。
jkd7中,扩容时,直接判断每个元素在新数组中的位置,然后依次复制到新数组;
jdk8中,扩容时,首先建立两个链表high和low,然后根据新增的高位是否为0,将元素放到对应的链表后面。最后将对应的链表放在原位置或者原位置+扩容大小值的位置.
- 红黑树需要比较大小才能进行插入,是依据什么进行比较的?
从下图可以看到,是根据hash大小来确定左右子树的位置的。
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K,V> x, f = first;
first = x = new TreeNode<K,V>(h, k, v, f, xp);
if (f != null)
f.prev = x;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
if (!xp.red)
x.red = true;
else {
lockRoot();
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
- 其他Hash冲突解决方式?
开放定址法(线性探测法,平方探测法,双散列)和再散列(选择新的散列函数,在另外一个大约两倍大的表,而且使用一个相关的新散列函数,扫描整个原始散列表,计算每个(未删除的)元素的新散列值并将其插入到新表中。)
- HashMap为什么不是线程安全的?怎么让HashMap变得线程安全?(加锁)
1.7 hashmap 并发resize成环;1.8并发resize丢失数据。
- jdk1.8对ConcurrentHashMap做了哪些优化?
1、取消了segment数组,引入了Node结构,直接用Node数组来保存数据,锁的粒度更小,减少并发冲突的概率。
2、存储数据时采用了链表+红黑树的形式,纯链表的形式时间复杂度为O(n),红黑树则为O(logn),性能提升很大。什么时候链表转红黑树?当key值相等的元素形成的链表中元素个数超过8个的时候。
- 怎么高效率的实现数据迁移?
jdk1.8中,resize数据要么在原位置,要么在原位置加上resize大小的位置。
concurrentHashMap在put或者remove操作时,发现正在进行扩容,会首先帮助扩容。
- resize过程
1.7 hashmap:新建new table,根据hash值计算在新table中的位置,依次移动到新table
1.8 hashmap:新建table,从旧table中复制元素,利用high和low来保存不同位置的元素。
- 为什么都是2的N次幂的大小。
1) 从上面的分析JDK8 resize的过程可以可能到,数组长度保持2的次幂,当resize的时候,为了通过h&(length-1)计算新的元素位置,可以看到当扩容后只有一位差异,也就是多出了最左位的1,这样计算 h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致,否则index+OldCap。
2) 数组长度保持2的次幂,length-1的低位都为1,会使得获得的数组索引index更加均匀。hash函数采用各种位运算也是为了使得低位更加散列,如果低位全部为1,那么对于h低位部分来说,任何一位的变化都会对结果产生影响,可以尽可能的使元素分布比较均匀。
- HashMap,HashTable比较
- HashMap允许将 null 作为一个 entry 的 key 或者 value,而 Hashtable 不允许。
- HashTable 继承自 Dictionary 类,而 HashMap 是 Java1.2 引进的 Map interface 的一个实现。
- HashTable 的方法是 Synchronized 的,而 HashMap 不是,在多个线程访问 Hashtable 时,不需要自己为它的方法实现同步,而 HashMap 就必须为之提供外同步。
- 极高并发下HashTable和ConcurrentHashMap哪个性能更好,为什么,如何实现的。
ConcurrentHashMap。后者锁粒度更低,前者直接对put、get方法加锁。
- HashMap和HashSet的实现原理
HashSet的实现很简单,内部有一个HashMap的成员变量,所有的Set相关的操作都转换为了对HashMap的操作。
1.2 集合相关问题
- LinkedHashMap、ArrayList、LinkedList、Vector的底层实现。
LinkedHashMap:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
...
}
可以看到,LinkedHashMap是HashMap的子类,但和HashMap的无序性不一样,LinkedHashMap通过维护一个运行于所有条目的双向链表,保证了元素迭代的顺序。该迭代顺序可以是插入顺序或者是访问顺序,这个可以在初始化的时候确定,默认采用插入顺序来维持取出键值对的次序。
在成员变量上,与HashMap不同的是,引入了before和after两个变量来记录前后的元素。
在构造函数中,需要指定accessOrder,有两种情况:
false,所有的Entry按照插入的顺序排列
true,所有的Entry按照访问的顺序排列
ArrayList、LinkedList、Vector、stack的底层实现:
从图中我们可以看出:
List是一个接口,它继承与Collection接口,代表有序的队列。
AbstractList是一个抽象类,它继承与AbstractCollection。AbstractList实现了List接口中除了size()、get(int location)之外的方法。
AbstractSequentialList是一个抽象类,它继承与AbstrctList。AbstractSequentialList实现了“链表中,根据index索引值操作链表的全部方法”。
ArrayList、LinkedList、Vector和Stack是List的四个实现类,其中Vector是基于JDK1.0,虽然实现了同步(大部分方法),但是效率低,已经不用了,Stack继承于Vector。
LinkedList是个双向链表,它同样可以被当作栈、队列或双端队列来使用。
- TreeMap以及查询复杂度
TreeMap继承于AbstractMap,实现了Map, Cloneable, NavigableMap, Serializable接口。
@w=400
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
对于SortedMap来说,该类是TreeMap体系中的父接口,也是区别于HashMap体系最关键的一个接口。SortedMap接口中定义的第一个方法Comparator<? super K> comparator();
该方法决定了TreeMap体系的走向,有了比较器,就可以对插入的元素进行排序了。
TreeMap的查找、插入、更新元素等操作,主要是对红黑树的节点进行相应的更新,和数据结构中类似。
1.3 Java 泛型的理解
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型的好处:
- 使程序的通用性更好,支持不同的类型;
- 编译器无法进行类型检查,可以向集合中添加任意类型的对象。取值时类型转换失败导致程序运行失败。泛型的好处在于提高了程序的可读性和安全性,这也是程序设计的宗旨之一。
1.4 跳表(ConcurrentSkipListMap)的查询过程是怎么样的,查询和插入的时间复杂度?
ConcurrentSkipListMap是一个并发安全, 基于skiplist实现有序存储的Map。可以看成是TreeMap的并发版本。
下面的图示使用紫色的箭头画出了在一个SkipList中查找key值50的过程。简述如下:
-
从head出发,因为head指向最顶层(top level)链表的开始节点,相当于从顶层开始查找;
-
移动到当前节点的右指针(right)指向的节点,直到右节点的key值大于要查找的key值时停止;
-
如果还有更低层次的链表,则移动到当前节点的下一层节点(down),如果已经处于最底层,则退出;
-
重复第2步 和 第3步,直到查找到key值所在的节点,或者不存在而退出查找;
查询复杂度:O(logN)
- 为什么Redis选择使用跳表而不是红黑树来实现有序集合?
Redis 中的有序集合(zset) 支持的操作:
插入一个元素
删除一个元素
查找一个元素
有序输出所有元素
按照范围区间查找元素(比如查找值在 [100, 356] 之间的数据)
其中,前四个操作红黑树也可以完成,且时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。按照区间查找数据时,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了,非常高效。
1.5 java 字节流 字符流
Java中的流是对字节序列的抽象,我们可以想象有一个水管,只不过现在流动在水管中的不再是水,而是字节序列。和水流一样,Java中的流也具有一个“流动的方向”,通常可以从中读入一个字节序列的对象被称为输入流;能够向其写入一个字节序列的对象被称为输出流。
Java中的字节流(Byte)处理的最基本单位为单个字节,它通常用来处理二进制数据。Java中最基本的两个字节流类是InputStream和OutputStream,它们分别代表了组基本的输入字节流和输出字节流。InputStream类与OutputStream类均为抽象类,我们在实际使用中通常使用Java类库中提供的它们的一系列子类。
public abstract int read() throws IOException;
Java中的字符流(Char)处理的最基本的单元是Unicode码元(大小2字节),它通常用来处理文本数据。所谓Unicode码元,也就是一个Unicode代码单元,范围是0x0000~0xFFFF。在以上范围内的每个数字都与一个字符相对应,Java中的String类型默认就把字符以Unicode规则编码而后存储在内存中。然而与存储在内存中不同,存储在磁盘上的数据通常有着各种各样的编码方式。使用不同的编码方式,相同的字符会有不同的二进制表示。实际上字符流是这样工作的:
- 输出字符流:把要写入文件的字符序列(实际上是Unicode码元序列)转为指定编码方式下的字节序列,然后再写入到文件中;
- 输入字符流:把要读取的字节序列按指定编码方式解码为相应字符序列(实际上是Unicode码元序列从)从而可以存在内存中。
字符流与字节流的区别
- 字节流操作的基本单元为字节;字符流操作的基本单元为Unicode码元。
- 字节流默认不使用缓冲区;字符流使用缓冲区。
- 字节流通常用于处理二进制数据,实际上它可以处理任意类型的数据,但它不支持直接写入或读取Unicode码元;字符流通常处理文本数据,它支持写入及读取Unicode码元。
1.8 包装类型和基本类型比较问题
例如,Integer类型的变量能否==int类型变量,能否作比较
可以。 但是不同包装类型直接进行比较,不会发生自动拆箱,比较的是两个对象是否为同一个。
自动装箱和拆箱实现了基本数据类型与对象数据类型之间的隐式转换。
自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。原始类型byte,short,char,int,long,float,double和boolean对应的封装类为Byte,Short,Character,Integer,Long,Float,Double,Boolean。
何时发生自动装箱和拆箱,
- 赋值:
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion
- 方法调用:当我们在方法调用时,我们可以传入原始数据值或者对象,同样编译器会帮我们进行转换。
public static Integer show(Integer iParam){
System.out.println("autoboxing example - method invocation i: " + iParam);
return iParam;
}
//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer
自动装箱的弊端,
自动装箱有一个问题,那就是在一个循环中进行自动装箱操作的时候,如下面的例子就会创建多余的对象,影响程序的性能。
Integer sum = 0;
for(int i=1000; i<5000; i++){
sum+=i;
}
自动装箱与比较:
下面程序的输出结果是什么?
public class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d);
System.out.println(e==f);
System.out.println(c==(a+b));
System.out.println(c.equals(a+b));
System.out.println(g==(a+b));
System.out.println(g.equals(a+b));
System.out.println(g.equals(a+h));
}
}
在解释具体的结果时,首先必须明白如下两点:
- 当"=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。
- 对于包装器类型,equals方法并不会进行类型转换。
下面是程序的具体输出结果:
true
false
true
true
true
false
true
注意到对于Integer和Long,Java中,会对-128到127的对象进行缓存,当创建新的对象时,如果符合这个这个范围,并且已有存在的相同值的对象,则返回这个对象,否则创建新的Integer对象。
对于上面的结果:
cd:指向相同的缓存对象,所以返回true;
ef:不存在缓存,是不同的对象,所以返回false;
c(a+b):比较数值,因此为true;
c.equals(a+b):比较的对象,由于存在缓存,所以两个对象一样,返回true;
g(a+b):直接比较数值,因此为true;
g.equals(a+b):比较对象,由于equals也不会进行类型转换,a+b为Integer,g为Long,因此为false;
g.equals(a+h):和上面不一样,a+h时,a会进行类型转换,转成Long,接着比较两个对象,由于Long存在缓存,所以两个对象一致,返回true。
关于equals和==:
.equals(...)
will only compare what it is written to compare, no more, no less.- If a class does not override the equals method, then it defaults to the
equals(Object o)
method of the closest parent class that has overridden this method.- If no parent classes have provided an override, then it defaults to the method from the ultimate parent class, Object, and so you're left with the
Object#equals(Object o)
method. Per the Object API this is the same as ==; that is, it returns true if and only if both variables refer to the same object, if their references are one and the same. Thus you will be testing for object equality and not functional equality.- Always remember to override hashCode if you override equals so as not to "break the contract". As per the API, the result returned from the hashCode() method for two objects must be the same if their equals methods show that they are equivalent. The converse is not necessarily true.
1.9 为什么重写equals和hashcode
以下是Effective Java的建议:
You must override hashCode() in every class that overrides equals(). Failure to do so will result in a violation of the general contract for Object.hashCode(), which will prevent your class from functioning properly in conjunction with all hash-based collections, including HashMap, HashSet, and Hashtable.
主要是考虑到在map等需要hashcode的场合,保证程序运行正常。
由于Object是所有类的基类,默认的equals函数如下,直接比较两个对象是否为同一个。
public boolean equals(Object obj) {
return (this == obj);
}
默认的hashcode方法是一个native函数,
public native int hashCode();
It just means that the method is implemented in the native aka C/C++ parts of the JVM. This means you can't find Java source code for that method. But there is still some code somewhere within the JVM that gets invoked whenever you call hashCode() on some Object.
从Object中关于hashCode方法的描述,对于不同的Object,hashCode会返回不同的值;但是实现可能与对象的地址有关,也有可能无关,具体看JVM实现。
1.10 stringBuilder和stringBuffer的区别
String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。
StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。
1.11 Java序列化的原理
序列化: 对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。
反序列化: 客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
序列化算法一般会按步骤做如下事情:
- 将对象实例相关的类元数据输出。
- 递归地输出类的超类描述直到不再有超类。
- 类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
- 从上至下递归输出实例的数据
JDK中序列化一个对象:
- 创建某些OutputStream对象
- 将其封装在一个ObjectOutputStream对象内
- 只需调用writeObject()即可将对象序列化
反序列化
将一个InputStream封装在ObjectInputStream内,然后调用readObject()。最后获得的是一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们。
序列化的控制——通过实现Externalizable接口——代替实现Serializable接口——来对序列化过程进行控制。
- Externalizable接口继承了Serializable接口,增加了两个方法,writeExternal()和readExternal(),这两个方法会在序列化和反序列化还原的过程中被自动调用。
- Externalizable对象,在还原的时候所有普通的默认构造器都会被调用(包括在字段定义时的初始化)(只有这样才能使Externalizable对象产生正确的行为),然后调用readExternal().
- 如果我们从一个Externalizable对象继承,通常需要调用基类版本的writeExternal()和readExternal()来为基类组件提供恰当的存储和恢复功能。
- 为了正常运行,我们不仅需要在writeExternal()方法中将来自对象的重要信息写入,还必须在readExternal()中恢复数据
防止对象的敏感部分被序列化,两种方式:
- 将类实现Externalizable,在writeExternal()内部只对所需部分进行显示的序列化
- 实现Serializable,用transient(瞬时)关键字(只能和Serializable一起使用)逐个字段的关闭序列化,他的意思:不用麻烦你保存或恢复数据——我自己会处理。
Externalizable的替代方法
- 实现Serializable接口,并添加名为writeObject()和readObject()的方法,这样一旦对象被序列化或者被反序列化还原,就会自动的分别调用writeObject()和readObject()的方法(它们不是接口的一部分,接口的所有东西都是public的)。只要提供这两个方法,就会使用它们而不是默认的序列化机制。
- 这两个方法必须具有准确的方法特征签名,但是这两个方法并不在这个类中的其他方法中调用,而是在ObjectOutputStream和ObjectInputStream对象的writeObject()和readObject()方法
1.11 Java8、9、10、11的一些新特性介绍
java8
- lambada表达式(Lambda Expressions)。Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
- 方法引用(Method references)。方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,可以使语言的构造更紧凑简洁,减少冗余代码。
- 默认方法(Default methods)。默认方法允许将新功能添加到库的接口中,并确保兼容实现老版本接口的旧有代码。
- 重复注解(Repeating Annotations)。重复注解提供了在同一声明或类型中多次应用相同注解类型的能力。
- 类型注解(Type Annotation)。在任何地方都能使用注解,而不是在声明的地方。
- 类型推断增强。
- 方法参数反射(Method Parameter Reflection)。
- Stream API 。新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。Stream API集成到了Collections API里。
- HashMap改进,在键值哈希冲突时能有更好表现。
- Date Time API。加强对日期和时间的处理。
- java.util 包下的改进,提供了几个实用的工具类。
- 并行数组排序。
- 标准的Base64编解码。
- 支持无符号运算。
- java.util.concurrent 包下增加了新的类和方法。
- java.util.concurrent.ConcurrentHashMap 类添加了新的方法以支持新的StreamApi和lambada表达式。
- java.util.concurrent.atomic 包下新增了类以支持可伸缩可更新的变量。
- java.util.concurrent.ForkJoinPool类新增了方法以支持 common pool。
- 新增了java.util.concurrent.locks.StampedLock类,为控制读/写访问提供了一个基于性能的锁,且有三种模式可供选择。
- HotSpot
- 删除了 永久代(PermGen).
- 方法调用的字节码指令支持默认方法。
java9
- java模块系统 (Java Platform Module System)。
- 新的版本号格式。
$MAJOR.$MINOR.$SECURITY.$PATCH
- java shell,交互式命令行控制台。
- 在private instance methods方法上可以使用@SafeVarargs注解。
- diamond语法与匿名内部类结合使用。
- 下划线_不能单独作为变量名使用。
- 支持私有接口方法(您可以使用diamond语法与匿名内部类结合使用)。
- Javadoc
- 简化Doclet API。
- 支持生成HTML5格式。
- 加入了搜索框,使用这个搜索框可以查询程序元素、标记的单词和文档中的短语。
- 支持新的模块系统。
- JVM
- 增强了Garbage-First(G1) 并用它替代Parallel GC成为默认的垃圾收集器。
- 统一了JVM 日志,为所有组件引入了同一个日志系统。
- 删除了JDK 8中弃用的GC组合。(DefNew + CMS,ParNew + SerialOld,Incremental CMS)。
- properties文件支持UTF-8编码,之前只支持ISO-8859-1。
- 支持Unicode 8.0,在JDK8中是Unicode 6.2。
java10
- 局部变量类型推断(Local-Variable Type Inference)
//之前的代码格式
URL url = new URL("http://www.oracle.com/");
URLConnection conn = url.openConnection();
Reader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()))
//java10中用var来声明变量
var url = new URL("http://www.oracle.com/");
var conn = url.openConnection();
var reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
var是一个保留类型名称,而不是关键字。所以之前使用var作为变量、方法名、包名的都没问题,但是如果作为类或接口名,那么这个类和接口就必须重命名了。
var的使用场景主要有以下四种:
- 本地变量初始化。
- 增强for循环中。
- 传统for循环中声明的索引变量。
- Try-with-resources 变量。
- Optional类添加了新的方法orElseThrow(无参数版)。相比于已经存在的get方法,这个方法更推荐使用。
java11
- 支持Unicode 10.0,在jdk10中是8.0。
- 标准化HTTP Client
- 编译器线程的延迟分配。添加了新的命令-XX:+UseDynamicNumberOfCompilerThreads动态控制编译器线程的数量。
- 新的垃圾收集器—ZGC。一种可伸缩的低延迟垃圾收集器(实验性)。
- Epsilon。一款新的实验性无操作垃圾收集器。Epsilon GC 只负责内存分配,不实现任何内存回收机制。这对于性能测试非常有用,可用于与其他GC对比成本和收益。
- Lambda参数的局部变量语法。java10中引入的var字段得到了增强,现在可以用在lambda表达式的声明中。如果lambda表达式的其中一个形式参数使用了var,那所有的参数都必须使用var。
1.12 java中四种修饰符的限制范围。
1.13 Object类中的方法。
- Object():默认构造方法
- clone():创建并返回此对象的一个副本,这是浅拷贝。
- equals():指示某个其他对象是否与此对象相等
- finalize():当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。JVM准备对此对对象所占用的内存空间进行垃圾回收前,将被调用。
- getClass():返回一个对象的运行时类对象。
首先解释下"类对象"的概念:在Java中,类是是对具有一组相同特征或行为的实例的抽象并进行描述,对象则是此类所描述的特征或行为的具体实例。作为概念层次的类,其本身也具有某些共同的特性,如都具有类名称、由类加载器去加载,都具有包,具有父类,属性和方法等。于是,Java中有专门定义了一个类,Class,去描述其他类所具有的这些特性,因此,从此角度去看,类本身也都是属于Class类的对象。为与经常意义上的对象相区分,在此称之为"类对象"。
- hashCode():返回该对象的哈希值
- notify():唤醒此对象监视器上等待的单个线程
- notifyAll():唤醒此对象监视器上等待的所有线程
- toString():返回该对象的字符串表示
- wait():导致当前的线程等待,直到其它线程调用此对象的notify()或notifyAll()
- wait(long timeout):导致当前的线程等待调用此对象的notify()或notifyAll()
- wait(long timeout, int nanos):导致当前的线程等待,直到其他线程调用此对象的notify()或notifyAll(),或其他某个线程中断当前线程,或者已超过某个实际时间量。
- registerNatives():对本地方法进行注册
1.14 浅拷贝 深拷贝
在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。而一般使用 『 = 』号做赋值操作的时候。对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。
而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。
1、浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。(默认的clone函数)
2、深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。(序列化对象或者重写clone函数)
1.15 接口和抽象类的区别,注意JDK8的接口可以有实现。
接口和抽象类是 Java 面向对象设计的两个基础机制。
接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义 Java 标准类库中,定义了非常多的接口,比如 java.util.List。
Java 8 以后,接口也是可以有方法实现的。 从 Java 8 开始,interface 增加了对 default method 的支持。Java 9 以后,甚至可以定义 private default method。Default method 提供了一种二进制兼容的扩展已有接口的办法。在 Java 8 中添加了一系列 default method,主要是增加 Lambda(forEach)、Stream 相关的功能。
抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。 Java 标准库中,比如 collection 框架,很多通用部分就被抽取成为抽象类,例如 java.util.AbstractList。
Java 类实现 interface 使用 implements 关键词,继承 abstract class 则是使用 extends 关键词,可以参考 Java 标准库中的 ArrayList。
1.16 动态代理的两种方式,以及区别。
1. JDK动态代理
1、因为利用JDKProxy生成的代理类实现了接口,所以目标类中所有的方法在代理类中都有。
2、生成的代理类的所有的方法都拦截了目标类的所有的方法。而拦截器中invoke方法的内容正好就是代理类的各个方法的组成体。
3、利用JDKProxy方式必须有接口的存在。
4、invoke方法中的三个参数可以访问目标类的被调用方法的API、被调用方法的参数、被调用方法的返回类型。
2. cglib动态代理
1、 CGlib是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。
2、 用CGlib生成代理类是目标类的子类。
3、 用CGlib生成 代理类不需要接口
4、 用CGLib生成的代理类重写了父类的各个方法。
5、 拦截器中的intercept方法内容正好就是代理类中的方法体
JDK动态代理和cglib动态代理有什么区别?
- JDK动态代理只能对实现了接口的类生成代理对象;
- cglib可以对任意类生成代理对象,它的原理是对目标对象进行继承代理,如果目标对象被final修饰,那么该类无法被cglib代理。
Spring框架的一大特点就是AOP,SpringAOP的本质就是动态代理,那么Spring到底使用的是JDK代理,还是cglib代理呢?
答:混合使用。如果被代理对象实现了接口,就优先使用JDK代理,如果没有实现接口,就用用cglib代理。
1.16 传值和传引用的区别,Java是怎么样的,有没有传值引用。
在Java程序中,不区分传值调用和传引用,总是采用传值调用
。
注意以下几种情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型);
- 一个方法可以改变一个对象参数的状态;
- 一个方法不能让对象参数引用一个新的对象。
值传递与引用传递的区别:一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值,这句话相当重要,这是按值调用与引用调用的根本区别。
就算是包装类型也不行,修改之后生成新的变量,改变了形参的值,但是实参的值不会改变。
由于String类和包装类都被设定成不可变的,没有提供value对应的setter方法,而且很多都是final的,我们无法改变其内容,所以导致我们看起来好像是值传递。
在Java下实现swap函数可以通过反射实现,或者使用数组。
1.17 一个ArrayList在循环过程中删除,会不会出问题,为什么。
会有问题,不过需要分情况讨论。
所有可能的删除方法如下,
public class ArrayListTest {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("aa");
list.add("bb");
list.add("bb");
list.add("aa");
list.add("cc");
// 删除元素 bb
remove(list, "bb");
for (String str : list) {
System.out.println(str);
}
}
public static void remove(ArrayList<String> list, String elem) {
// 五种不同的循环及删除方法
// 方法一:普通for循环正序删除,删除过程中元素向左移动,不能删除重复的元素
// for (int i = 0; i < list.size(); i++) {
// if (list.get(i).equals(elem)) {
// list.remove(list.get(i));
// }
// }
// 方法二:普通for循环倒序删除,删除过程中元素向左移动,可以删除重复的元素
// for (int i = list.size() - 1; i >= 0; i--) {
// if (list.get(i).equals(elem)) {
// list.remove(list.get(i));
// }
// }
// 方法三:增强for循环删除,使用ArrayList的remove()方法删除,产生并发修改异常 ConcurrentModificationException
// for (String str : list) {
// if (str.equals(elem)) {
// list.remove(str);
// }
// }
// 方法四:迭代器,使用ArrayList的remove()方法删除,产生并发修改异常 ConcurrentModificationException
// Iterator iterator = list.iterator();
// while (iterator.hasNext()) {
// if(iterator.next().equals(elem)) {
// list.remove(iterator.next());
// }
// }
// 方法五:迭代器,使用迭代器的remove()方法删除,可以删除重复的元素,但不推荐
// Iterator iterator = list.iterator();
// while (iterator.hasNext()) {
// if(iterator.next().equals(elem)) {
// iterator.remove();
// }
// }
}
}
针对上述结果总结如下:
- 普通for循环删除,无论正向或者反向,不会抛出异常。但是由于删除过程中,list整体左移,所以正向删除无法删除连续的重复元素。
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 remove method that skips bounds checking and does not
* return the value removed.
*/
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
}
- 使用增强的for循环或者迭代器,只要是调用list本身的remove函数,由于在remove中会修改list内部的modCount,导致expectedModCount!=modCount,当调用迭代器的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];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
- 如果使用迭代器并调用迭代器的remove方法来删除元素,由于迭代器的remove函数中对两个计数进行了同步,所以不会出现异常。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
1.18 Exception和Error区别
Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。
Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
1.19 new关键字和newinstance()方法
它们的区别在于创建对象的方式不一样,前者newInstance()是使用类加载机制,后者new关键字是创建一个新类。那么为什么会有两种创建对象方式?这主要考虑到软件的可伸缩、可扩展和可重用等软件设计思想。
1、类的加载方式不同
在执行Class.forName("a.class.Name")时,JVM会在classapth中去找对应的类并加载,这时JVM会执行该类的静态代码段。在使用newInstance()方法的时候,必须保证这个类已经加载并且已经链接了,而这可以通过Class的静态方法forName()来完成的。
使用关键字new创建一个类的时候,这个类可以没有被加载,一般也不需要该类在classpath中设定,但可能需要通过classlaoder来加载。
2、所调用的构造方法不尽相同
new关键字能调用任何构造方法。
newInstance()只能调用无参构造方法。
3、执行效率不同
new关键字是强类型的,效率相对较高。
newInstance()是弱类型的,效率相对较低。
既然使用newInstance()构造对象的地方通过new关键字也可以创建对象,为什么又会使用newInstance()来创建对象呢?
假设定义了一个接口Door,开始的时候是用木门的,定义为一个类WoodenDoor,在程序里就要这样写 Door door = new WoodenDoor() 。假设后来生活条件提高,换为自动门了,定义一个类AutoDoor,这时程序就要改写为 Door door = new AutoDoor() 。虽然只是改个标识符,如果这样的语句特别多,改动还是挺大的。于是出现了工厂模式,所有Door的实例都由DoorFactory提供,这时换一种门的时候,只需要把工厂的生产模式改一下,还是要改一点代码。
而如果使用newInstance(),则可以在不改变代码的情况下,换为另外一种Door。具体方法是把Door的具体实现类的类名放到配置文件中,通过newInstance()生成实例。这样,改变另外一种Door的时候,只改配置文件就可以了。示例代码如下:
String className = 从配置文件读取Door的具体实现类的类名;
Door door = (Door) Class.forName(className).newInstance();
再配合依赖注入的方法,就提高了软件的可伸缩性、可扩展性。
https://blog.csdn.net/luckykapok918/article/details/50186797
1.20 Map、List、Set 分别说下你知道的线程安全类和线程不安全的类
MAP:
不安全:hashmap、treeMap、LinkedHashMap
安全:concurrentHashMap、ConcurrentSkipListMap、或者说hashtable
List:
不安全:ArrayList、linkedlist
安全:Vector、SynchronizedList(将list转为线程安全,全部上锁)、CopyOnWriteArrayList(读读不上锁、写上锁ReentrantLock、写完直接替换)
Set:
不安全:hashset、treeSet、LinkedHashSet
安全:ConcurrentSkipListSet、CopyOnWriteArraySet、synchronizedSet
1.21 Java防止SQL注入
- PreparedStatement:采用预编译语句集,它内置了处理SQL注入的能力,只要使用它的setXXX方法传值即可。PreparedStatement已经准备好了,执行阶段只是把输入串作为数据处理,
而不再对sql语句进行解析,准备,因此也就避免了sql注入问题。 - 使用正则表达式过滤传入的参数
- 字符串过滤
1.22 反射原理及使用场景
反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。
简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
Java 反射主要提供以下功能:
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
- 在运行时调用任意一个对象的方法。
重点:是运行时而不是编译时。
1 主要用途
反射最重要的用途就是开发各种通用框架。 很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。
2 基本使用
1. 获得Class对象
方法有三种:
(1) 使用 Class 类的 forName 静态方法:
public static Class<?> forName(String className)
比如在 JDBC 开发中常用此方法加载数据库驱动,Class.forName(driver);
(2) 直接获取某一个对象的 class,比如:
Class<?> klass = int.class;
Class<?> classInt = Integer.TYPE;
(3) 调用某个对象的 getClass() 方法,比如:
StringBuilder str = new StringBuilder("123");
Class<?> klass = str.getClass();
2. 判断是否为某个类的实例
一般地,我们用 instanceof 关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance() 方法来判断是否为某个类的实例,它是一个 native 方法:
public native boolean isInstance(Object obj);
3. 创建实例
通过反射来生成对象主要有两种方式。
- 使用Class对象的newInstance()方法来创建Class对象对应类的实例。
Class<?> c = String.class;
Object str = c.newInstance();
- 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。
//获取String所对应的Class对象
Class<?> c = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c.getConstructor(String.class);
//根据构造器创建实例
Object obj = constructor.newInstance("23333");
System.out.println(obj);
4. 获取方法
获取某个Class对象的方法集合,主要有以下几个方法:
- getDeclaredMethods 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
public Method[] getDeclaredMethods() throws SecurityException
- getMethods 方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。
public Method[] getMethods() throws SecurityException
- getMethod 方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。
public Method getMethod(String name, Class<?>... parameterTypes)
5. 获取构造器信息
获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:
public T newInstance(Object ... initargs)
此方法可以根据传入的参数来调用对应的Constructor创建对象实例。
6、获取类的成员变量(字段)信息
主要是这几个方法,在此不再赘述:
- getFiled:访问公有的成员变量
- getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量
注:可以通过method.setAccessible(true)和field.setAccessible(true)来关闭安全检查来提升反射速度。
7. 调用方法
当我们从类中获取了一个方法后,我们就可以用 invoke() 方法来调用这个方法。invoke 方法的原型为:
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
下面是例子:
public class test1 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> klass = methodClass.class;
//创建methodClass的实例
Object obj = klass.newInstance();
//获取methodClass类的add方法
Method method = klass.getMethod("add",int.class,int.class);
//调用method对应的方法 => add(1,4)
Object result = method.invoke(obj,1,4);
System.out.println(result);
}
}
class methodClass {
public final int fuck = 3;
public int add(int a,int b) {
return a+b;
}
public int sub(int a,int b) {
return a+b;
}
}
8. 利用反射创建数组
数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。下面我们看一看利用反射创建数组的例子:
public static void testArray() throws ClassNotFoundException {
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls,25);
//往数组里添加内容
Array.set(array,0,"hello");
Array.set(array,1,"Java");
Array.set(array,2,"fuck");
Array.set(array,3,"Scala");
Array.set(array,4,"Clojure");
//获取某一项的内容
System.out.println(Array.get(array,3));
}
其中的Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象,它的原型是:
public static Object newInstance(Class<?> componentType, int length)
throws NegativeArraySizeException {
return newArray(componentType, length);
}
3 注意事项
由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。
另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
1.23 static Vs Final ? 如何让类不能被继承
Static :被static修饰的成员变量属于类,不属于这个类的某个对象。
final意味着”不可改变的”,一般应用于数据、方法和类。
-
final数据:当数据是基本类型时,意味着这是一个永不改变的编译时常量。
-
final方法:一般我们使用final方法的目的就是防止子类对该类方法的覆盖或修改。
-
final类:一般我们使用final类的目的就是说明我们不打算用任何类继承该类,即不希望该类有子类。
如何让类不被继承:用final修饰这个类,或者将构造函数声明为私有。
1.24 内存泄露?内存溢出?
内存溢出:是指程序在申请内存时,没有足够的内存空间供其使用,出现OutOfMemoryError。
产生该错误的原因主要包括:
- JVM内存过小。
- 程序不严密,产生了过多的垃圾。
内存泄露:Memory Leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点:
1)首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;
2)其次,这些对象是无用的,即程序以后不会再使用这些对象。
两者的联系:
内存泄露会最终会导致内存溢出。
相同点:都会导致应用程序运行出现问题,性能下降或挂起。
不同点:
- 内存泄露是导致内存溢出的原因之一,内存泄露积累起来将导致内存溢出。
- 内存泄露可以通过完善代码来避免,内存溢出可以通过调整配置来减少发生频率,但无法彻底避免。
1.25 重写Vs重载
-
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
-
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
在 Java 中重载是由静态类型确定的,在类加载的时候即可确定,属于静态分派;而重写是由动态类型确定,是在运行时确定的,属于动态分派,动态分派是由虚方法表实现的,虚方法表中存在着各个方法的实际入口地址,如若父类中某个方法子类没有重写,则父类与子类的方法表中的方法地址相同,如若重写了,则子类方法表的地址指向重写后的地址。
1.26 Lambda表达式实现
这样就完成的实现了Lambda表达式,使用invokedynamic指令,运行时调用LambdaMetafactory.metafactory动态的生成内部类,实现了接口,内部类里的调用方法块并不是动态生成的,只是在原class里已经编译生成了一个静态的方法,内部类只需要调用该静态方法。
1.27 ClassNotFoundException和NoClassDefFoundError的区别
- ClassNotFoundException是一个检查异常。从类继承层次上来看,ClassNotFoundException是从Exception继承的,所以ClassNotFoundException是一个检查异常。
当应用程序运行的过程中尝试使用类加载器去加载Class文件的时候,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。一般情况下,当我们使用Class.forName()或者ClassLoader.loadClass以及使用ClassLoader.findSystemClass()在运行时加载类的时候,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException。
- NoClassDefFoundError异常,看命名后缀是一个Error。从类继承层次上看,NoClassDefFoundError是从Error继承的。和ClassNotFoundException相比,明显的一个区别是,NoClassDefFoundError并不需要应用程序去关心catch的问题。
当JVM在加载一个类的时候,如果这个类在编译时是可用的,但是在运行时找不到这个类的定义的时候,JVM就会抛出一个NoClassDefFoundError错误。 比如当我们在new一个类的实例的时候,如果在运行是类找不到,则会抛出一个NoClassDefFoundError的错误。
欢迎关注【后端精进之路】,硬货文章一手掌握~