java基础
集合承继包含图
- Collection vs Collections
首先,"Collection" and "Collections"是两个不同的概念,从下面的继承关系图中我们可以看到,"Collection"是一个基础接口类,而"Collections"是一个静态的类,它提供了一些静态方法来操作某些集合类型。
- 集合的类继承关系图
下图说明了集合的继承关系。
-
Map的继承关系图
ArrayList如何实现排序?
// 排序方法
//ArrayList.class
public void sort(Comparator super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
//Array.class 实现比较接口
public static void sort(T[] a, int fromIndex, int toIndex,
Comparator super T> c) {
// 如果没有实现比较方法
if (c == null) {
sort(a, fromIndex, toIndex);
} else {
rangeCheck(a.length, fromIndex, toIndex);
if (Arrays.LegacyMergeSort.userRequested)
legacyMergeSort(a, fromIndex, toIndex, c);
else
TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
}
}
//Array.class 未实现比较接口
public static void sort(Object[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
//经查资料,这是个传统的归并排序,需要通过设置系统属性后,才能进行调用
// System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
if (Arrays.LegacyMergeSort.userRequested)
legacyMergeSort(a, fromIndex, toIndex);
else
ComparableTimSort.sort(a, fromIndex, toIndex, null, 0, 0);
}
底层是一种传统归并排序。
Collection 和 Collections的区别
首先,"Collection" and "Collections"是两个不同的概念,从下面的继承关系图中我们可以看到,"Collection"是一个基础接口类,而"Collections"是一个静态的类,它提供了一些静态方法来操作某些集合类型。
ArrayList 和 Vector的区别
ArrayList,Vector主要区别为以下几点:
(1):Vector是线程安全的,源码中有很多的synchronized可以看出,而ArrayList不是。导致Vector效率无法和ArrayList相比;
(2):ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍;
(3):Vector可以设置capacityIncrement,而ArrayList不可以,从字面理解就是capacity容量,Increment增加,容量增长的参数。
ArrayList的底层实现和扩容情况
构造ArrayList的时候,默认初始化容量为10,保存容器为 Object[] elementData。
向集合添加元素的时候,调用add方法,比如list.add("a");
add方法做的操作是:elementData[size++] = e; 然后元素就被存放进了elementData。
初始化容量为10,当我们存第十一个元素的时候,会怎么做呢?看ArrayList类的部分源码:
public class ArrayList extends AbstractList implements List {
private transient Object[] elementData;
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity];
}
//Constructs an empty list with an initial capacity of ten.
public ArrayList() {
this(10);
}
public boolean add(E e) {
ensureCapacity(size + 1); // 扩充长度
elementData[size++] = e; // 先赋值,后进行size++。所以是从[0]开始存。
return true;
}
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length; // 旧集合长度
if (minCapacity > oldCapacity) {
Object oldData[] = elementData; // 旧集合数据
int newCapacity = (oldCapacity * 3)/2 + 1; // 计算新长度,旧长度的1.5倍+1
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity); // 这就是传说中的可变集合。用新长度复制原数组。
}
}
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
}
add方法中先调用ensureCapacity方法对原数组长度进行扩充,扩充方式为,通过Arrays类的copyOf方法对原数组进行拷贝,长度为原数组的1.5倍+1。
然后把扩容后的新数组实例对象地址赋值给elementData引用类型变量。扩容完毕。
经测试,如果要存100万数据,需要扩容28次,数据量越大,扩容次数越多,每一次的扩容代表着创建新数组对象,复制原有数据。
ArrayList 和 LinkedList的区别
ArrayList和Vector使用了数组的实现,可以认为ArrayList或者Vector封装了对内部数组的操作,比如向数组中添加,删除,插入新的元素或者数据的扩展和重定向。
LinkedList使用了循环双向链表数据结构。与基于数组ArrayList相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。
LinkedList链表由一系列表项连接而成。一个表项总是包含3个部分:元素内容,前驱表和后驱表
(1)增加元素到列表尾端
ArrayList中add()方法的性能决定于ensureCapacity()方法。 扩容函数。
LinkeList由于使用了链表的结构,因此不需要维护容量的大小
(2)增加元素到列表任意位置
由于实现的不同,ArrayList和LinkedList在这个方法上存在一定的性能差异,由于ArrayList是基于数组实现的,而数组是一块连续的内存空间,如果在数组的任意位置插入元素,必然导致在该位置后的所有元素需要重新排列,因此,其效率相对会比较低。
(3)删除任意位置元素
对ArrayList来说,remove()方法和add()方法是雷同的。在任意位置移除元素后,都要进行数组的重组。
(4)容量参数
容量参数是ArrayList和Vector等基于数组的List的特有性能参数。它表示初始化的数组大小。当ArrayList所存储的元素数量超过其已有大小时。它便会进行扩容,数组的扩容会导致整个数组进行一次内存复制。因此合理的数组大小有助于减少数组扩容的次数,从而提高系统性能。
ArrayList提供了一个可以制定初始数组大小的构造函数 :
public ArrayList(int initialCapacity)
(5)遍历列表
最简便的ForEach循环并没有很好的性能表现,综合性能不如普通的迭代器,而是用for循环通过随机访问遍历列表时,ArrayList表项很好,但是LinkedList的表现却无法让人接受,甚至没有办法等待程序的结束。这是因为对LinkedList进行随机访问时,总会进行一次列表的遍历操作。性能非常差,应避免使用。
Array和ArrayList的区别?什么时候更适合Array
(1)ArrayList是Array的复杂版本
ArrayList内部封装了一个Object类型的数组,从一般的意义来说,它和数组没有本质的差别,甚至于ArrayList的许多方法,如Index、IndexOf、Contains、Sort等都是在内部数组的基础上直接调用Array的对应方法。
(2)存储的数据类型
ArrayList可以存储异构对象,而Array只能存储相同数据类型的数据。
(3)长度的可变
Array的长度实际上是不可变的,二维变长数组实际上的长度也是固定的,可变的只是其中元素的长度。而ArrayList的长度既可以指定(即使指定了长度,也会自动2倍扩容)也可以不指定,是变长的。
(4)存取和增删元素
对于一般的引用类型来说,这部分的影响不是很大,但是对于值类型来说,往ArrayList里面添加和修改元素,都会引起装箱和拆箱的操作,频繁的操作可能会影响一部分效率。另外,ArrayList是动态数组,它不包括通过Key或者Value快速访问的算法,所以实际上调用IndexOf、Contains等方法是执行的简单的循环来查找元素,所以频繁的调用此类方法并不比你自己写循环并且稍作优化来的快,如果有这方面的要求,建议使用Hashtable或SortedList等键值对的集合。
适用场景:
如果想要保存一些在整个程序运行期间都会存在而且不变的数据,我们可以将它们放进一个全局数组里,但是如果我们单纯只是想要以数组的形式保存数据,而不对数据进行增加等操作,只是方便我们进行查找的话,那么,我们就选择ArrayList。
去掉Vector中一个反复的元素
1. Vector.contains()
通过Vector.contains()方法判断是否包含该元素,如果没有包含就添加到新的集合当中,用于数据较小的情况
2. HashSet()
使用HashSet来解决这个问题,通过hashcode的过滤解决问题。
HashMap HasnTable concurrentHashMap原理、区别
HashTable
底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
初始size为11,扩容:newsize = olesize*2+1
计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
底层数组+链表(红黑树)实现,可以存储null键和null值,线程不安全
初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
计算index方法:index = hash & (tab.length – 1)
ConcurrentHashMap
底层采用分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
HashMap的初始值还要考虑加载因子:
哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
Hashtable与HashMap另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。
List Map Set三个接口,存取数据时各有什么特点
List与Set都是单列元素的集合,它们有一个功共同的父接口Collection。
Set里面不允许有重复的元素,
存元素:add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true;当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。
取元素:没法说取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。
List表示有先后顺序的集合,
存元素:多次调用add(Object)方法时,每次加入的对象按先来后到的顺序排序,也可以插队,即调用add(int index,Object)方法,就可以指定当前对象在集合中的存放位置。
取元素:
方法1:Iterator接口取得所有,逐一遍历各个元素
方法2:调用get(index i)来明确说明取第几个。
Map是双列的集合,
存放用put方法:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。
取元素:用get(Object key)方法根据key获得相应的value。也可以获得所有的key的集合,还可以获得所有的value的集合,还可以获得key和value组合成的Map.Entry对象的集合。
List以特定次序来持有元素,可有重复元素。Set 无法拥有重复元素,内部排序。Map 保存key-value值,value可多值。
介绍一下TreeSet
Java中的TreeSet是Set的一个子类,TreeSet集合是用来对象元素进行排序的,同样他也可以保证元素的唯一。
TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet
TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。
TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。
TreeSet 实现了Cloneable接口,意味着它能被克隆。
TreeSet 实现了java.io.Serializable接口,意味着它支持序列化。
TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
另外,TreeSet是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。
(1) TreeSet继承于AbstractSet,并且实现了NavigableSet接口。
(2) TreeSet的本质是一个"有序的,并且没有重复元素"的集合,它是通过 TreeMap实现的。TreeSet中含有一个"NavigableMap类型的成员变量"m,而m实际上是"TreeMap的实例"。
TreeSet不支持快速随机遍历,只能通过迭代器进行遍历!
Java集合中那些类是线程安全的
在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的。在jdk1.2之后,就出现许许多多非线程安全的类。 下面是这些线程安全的同步的类:
vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
statck:堆栈类,先进后出
hashtable:就比hashmap多了个线程安全
enumeration:枚举,相当于迭代器
除了这些之外,其他的都是非线程安全的类和接口。
线程安全的类其方法是同步的,每次只能一个访问。是重量级对象,效率较低。
BlockingQueue是什么?如何用wait和notify实现blockingqueue
BlockingQueue是阻塞队列。
阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
public class BlockingQueueDemo {
//定义两把锁,只是简单的锁
private Object full = new Object();
private Object empty = new Object();
private int[] array;
private int head;
private int last;
private int size;
public BlockingQueueDemo(int maxSize) {
this.head = 0;
this.last = 0;
array = new int[maxSize];
}
public void put(int e) throws InterruptedException {
synchronized(full){ //满锁
while (size == array.length) {//没有更多空间,需要阻塞
full.wait();
}
}
if (last < array.length) {
array[last] = e;
last++;
} else {
array[0] = e;
last = 1;
}
size++;
System.out.println(size+" :size大小,last: "+last+" :e: "+e);
//放入数据以后,就可以唤醒obj2对象的锁
synchronized(empty){ //空锁
empty.notify();//达到了唤醒poll方法的条件
}
}
public int pool() throws InterruptedException {
synchronized(empty){
while (size == 0) {//没有数据,阻塞
empty.wait();
}
}
int returnValue = 0;
//队列中有数据,且head小于array的长度
returnValue = array[head];
array[head] = -1;
System.out.println(returnValue + " 弹出的"+"head:"+ head);
if (head < array.length) {//弹出head下标的数据
head++;
} else {
head = 0;
}
size--;
//拿走数据后,唤醒full对象锁
synchronized(full){
full.notify();
}
return returnValue;
}
public String toString(){
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
return "";
}
public static void main(String[] args) throws InterruptedException {
BlockingQueueDemo bq = new BlockingQueueDemo(5);
bq.put(10);
bq.put(20);
bq.put(30);
bq.put(40);
bq.put(50);
System.out.println();
bq.pool();
bq.pool();
bq.pool();
bq.pool();
System.out.println();
bq.toString();
System.out.println();
bq.put(100);
bq.put(200);
bq.put(300);
bq.put(400);
System.out.println();
bq.toString();
}
}
并发集合类有哪些?有没有研究过源码
当需要在并发程序中使用数据集合时,必须要谨慎地选择相应的实现方式。大多数集合类并不能直接用于并发应用,因为他们没有对本身数据的并发访问进行控制,如果一些并发任务共享一个不适用于病发任务的数据结构,将会遇到数据不一致性的错误,并将影响程序的正确运行,这类数据结构的一个例子就是ArrayList。
java提供了一些可以用于并发程序中的数据集合,他们不会引起任何问题,一般来说,java提供了两类适用于并发应用的集合:
(1)阻塞式集合(blocking collection):这类集合包括添加和移除数据的方法。当集合已满或者为空时,被调用的添加或者 移除方法就不能立即执行,那么调用这个饭方法的线程将被阻塞,一直到该方法可以被成功执行。
(2)非阻塞式集合(Non-blocking collection):这类集合也包括添加和移除数据的方法。如果集合已满或者为空时,在调用添加或者移除方法时会返回null或者抛出异常,但是调用这个方法的线程不会被阻塞。
以下就是java常用的并发集合:
非阻塞式列表对应的实现类:ConcurrentLinkedDeque
阻塞式列表对应的实现类:LinkedBlockingDeque
用于数据生成或者消费的阻塞式列表对应的实现类:LinkedTransferQueue
按优先级排序列表元素的阻塞式列表对应的实现类:PriorityBlockingQueue
带有延迟列表元素的阻塞式列表对应的实现类:DelayQueue
非阻塞式列表可遍历映射对应的饿实现类:ConcurrentSkipListMap
随机数字对应的实现类:ThreadLockRandom
原子变量对应的实现类:AtomicLong和AtomicIntegerArray
ThreadPoolExecutor 的内部工作原理,整个思路总结起来就是 5 句话:
如果当前池大小 poolSize 小于 corePoolSize ,则创建新线程执行任务。
如果当前池大小 poolSize 大于 corePoolSize ,且等待队列未满,则进入等待队列
如果当前池大小 poolSize 大于 corePoolSize 且小于 maximumPoolSize ,且等待队列已满,则创建新线程执行任务。
如果当前池大小 poolSize 大于 corePoolSize 且大于 maximumPoolSize ,且等待队列已满,则调用拒绝策略来处理该任务。
线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出。
Comparable 和 Comparator接口是什么?有何区别?代码如何实现(背代码)
java中,对集合对象或者数组对象排序,有两种实现方式。
(1)对象实现Comparable 接口
(2)定义比较器,实现Comparator接口。
Comparable
Comparable是在集合内部定义的方法实现的排序,位于java.lang下。
Comparable是一个对象,本身就已经支持自比较所需要实现的接口。
package java.lang;
import java.util.*;
public interface Comparable {
public int compareTo(T o);
}
自定义类要在加入list容器中后能够排序,也可以实现Comparable接口。
在用Collections类的sort方法排序时若不指定Comparator,那就以自然顺序排序。所谓自然顺序就是实现Comparable接口设定的排序方式。
若一个类实现了comparable接口,则意味着该类支持排序。如String、Integer自己就实现了Comparable接口,可完成比较大小操作。
一个已经实现comparable的类的对象或数据,可以通过Collections.sort(list) 或者Arrays.sort(arr)实现排序。通过Collections.sort(list,Collections.reverseOrder());对list进行倒序排列。
Comparator
Comparator是在集合外部实现的排序,位于java.util下。
Comparator接口包含了两个函数。
package java.util;
public interface Comparator {
int compare(T o1, T o2);
boolean equals(Object obj);
}
我们若需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口);那么,我们可以新建一个该类的比较器来进行排序。这个比较器只需要实现comparator即可。
如果引用的为第三方jar包,这时候,没办法改变类本身,可是使用这种方式。
Comparator是一个专用的比较器,当这个对象不支持自比较或者自比较函数不能满足要求时,可写一个比较器来完成两个对象之间大小的比较。
Comparator体现了一种策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。
comparable相当于内部比较器。comparator相当于外部比较器。
如何对一组对象进行排序
- 创建对象类
- 实例化对象
- 存入适当的集合类
- 实现Comparable接口或者建立实现了Comparator接口的比较器类
迭代器
Iterator是什么?
详见:https://www.jianshu.com/p/24a642f1a1b8
Enumeration接口和Iterator接口的区别
Enumeration 接口的作用与 Iterator 接口类似,但只提供了遍历Vector
和 Hashtable
类型集合元素的功能,不支持元素的移除操作。
例如:遍历Vector
for (Enumeration e = v.elements();e.hasMoreElements();)
System.out.println(e.nextElement());
Iterator 接口添加了一个可选的移除操作,并使用较短的方法名。新的实现应该优先考虑使用 Iterator 接口而不是 Enumeration 接口。
区别:Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。
Iterator 接口的用法:
Iterator it = list.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
为什么没有像Iterator.add()这样的方法向集合中添加元素?
逻辑上讲,迭代时可以添加元素,但是一旦开放这个功能,很有可能造成很多意想不到的情况。 比如你在迭代一个ArrayList,迭代器的工作方式是依次返回给你第0个元素,第1个元素,等等,假设当你迭代到第5个元素的时候,你突然在ArrayList的头部插入了一个元素,使得你所有的元素都往后移动,于是你当前访问的第5个元素就会被重复访问。 java认为在迭代过程中,容器应当保持不变。因此,java容器中通常保留了一个域称为modCount,每次你对容器修改,这个值就会加1。当你调用iterator方法时,返回的迭代器会记住当前的modCount,随后迭代过程中会检查这个值,一旦发现这个值发生变化,就说明你对容器做了修改,就会抛异常。
为何迭代器没有一个方法可以直接获取下一个元素,而不需要移动游标?
它可以在当前Iterator的顶层实现,但是它用得很少,如果将它加到接口中,每个继承都要去实现它,这没有意义。
Iterator和ListIterator之间有什么区别?
(1)我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
(2)Iterator只可以向前遍历,而LIstIterator可以双向遍历。
(3)ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
遍历一个List有哪些不同的方式?
List strList = new ArrayList<>();
//使用for-each循环
for(String obj : strList){
System.out.println(obj);
}
//using iterator
Iterator it = strList.iterator();
while(it.hasNext()){
String obj = it.next();
System.out.println(obj);
}
使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出ConcurrentModificationException。
map遍历的四种方式
public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第三种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第四种
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
通过迭代器fail-fast属性,你明白了什么?
每次我们尝试获取下一个元素的时候,Iterator
fail-fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException
。Collection
中所有Iterator
的实现都是按fail-fast来设计的(ConcurrentHashMap
和CopyOnWriteArrayList
这类并发集合类除外)。
fail-fast与fail-safe有什么区别?
Iterator
的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail-fast的,而java.util.concurrent中的集合类都为fail-safe的。Fail-fast迭代器抛出ConcurrentModificationException
,而fail-safe迭代器从不抛出ConcurrentModificationException。
为何Iterator接口没有具体的实现?
Iterator
接口定义了遍历集合的方法,但它的实现则是集合实现类的责任。每个能够返回用于遍历的Iterator
的集合类都有它自己的Iterator实现内部类。
这就允许集合类去选择迭代器是fail-fast还是fail-safe的。比如,ArrayList
迭代器是fail-fast的,而CopyOnWriteArrayList
迭代器是fail-safe的。
UnsupportedOperationException是什么?
UnsupportedOperationException是用于表明操作不支持的异常。在JDK类中已被大量运用,在集合框架java.util.Collections.UnmodifiableCollection将会在所有add和remove操作中抛出这个异常。
其他
java中的sleep()和wait()的区别
sleep()
方法,是属于Thread类中的。而wait()
方法,则是属于Object类中的。
sleep()
方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()
方法的过程中,线程不会释放对象锁。
而当调用wait()
方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
String stringbuff stringbuilder不变性
有时候,需要较短的字符串构建字符串,例如来自按键或者文件中的单词。这时采用拼接的方式达到此目的效率比较低。而String为不可变字符串,每次连接字符串就需要创建一个新的String对象,既耗时有占用空间。使用StringBuffer和 StringBuilder可以解决(可修改的)
这里一StringBuilder为例
新建一个构造器
StringBuilder builder =new StringBuilder();
当每次需要添加内容是就用append()方法 需要构建字符串时就调用同String方法
String s=builder.toString();
StringBuffer是StringBuilder的前身,可想而知 Builder的效率要高于Buffer但是 BUffer是线程安全的,允许采用多线程的方式执行添加,删除字符串的操作。但是大多数情况下我们都是单线程进行操作,因此使用builder的情况更多
一致性hash原理
https://blog.csdn.net/luyaran/article/details/53665523
HashMap,concurrentHashMap原理(1.7 和 1.8的区别)、区别以及在什么情况下性能会不好
详见:https://www.cnblogs.com/study-everyday/p/6430462.html
JDK1.7的实现
在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,如下图所示:
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样
初始化
ConcurrentHashMap的初始化是会通过位与运算来初始化Segment的大小,用ssize来表示,如下所示:
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
如上所示,因为ssize用位于运算来计算(ssize <<=1),所以Segment的大小取值都是以2的N次方,无关concurrencyLevel的取值,当然concurrencyLevel最大只能用16位的二进制来表示,即65536,换句话说,Segment的大小最多65536个,没有指定concurrencyLevel元素初始化,Segment的大小ssize默认为16
每一个Segment元素下的HashEntry的初始化也是按照位于运算来计算,用cap来表示,如下所示:
int cap = 1;
while (cap < c)
cap <<= 1;
如上所示,HashEntry大小的计算也是2的N次方(cap <<=1), cap的初始值为1,所以HashEntry最小的容量为2
JDK1.8的实现
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
在深入JDK1.8的put和get实现之前要知道一些常量设计和数据结构,这些是构成ConcurrentHashMap实现结构的基础
Node
Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据。
Node数据结构很简单,从上可知,就是一个链表,但是只允许对数据进行查找,不允许进行修改。
TreeNode
TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构。
TreeBin
TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制。
其实可以看出JDK1.8版本的ConcurrentHashMap
的数据结构已经接近HashMap
,相对而言,ConcurrentHashMap
只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock
+Segment
+HashEntry
,到JDK1.8版本中synchronized
+CAS
+HashEntry
+红黑树,相对而言,总结如下思考:
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于
Segment
的,包含多个HashEntry
,而JDK1.8锁的粒度就是HashEntry
(首节点) - JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用
synchronized
来进行同步,所以不需要分段锁的概念,也就不需要Segment
这种数据结构了,由于粒度的降低,实现的复杂度也增加了 - JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
- JDK1.8为什么使用内置锁
synchronized
来代替重入锁ReentrantLock
,我觉得有以下几点:
4.1. 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock
可能通过Condition
来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition
的优势就没有了
4.2. JVM的开发团队从来都没有放弃synchronized
,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
4.3. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock
会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据
try里面return,finally再return 哪个会返回,字节码如何变
finally 语句块应该是在控制转移语句之前执行,控制转移语句除了 return 外,还有 break 和 continue。
HashSet和HashMap的区别
HashMap
HashMap实现了Map接口
HashMap储存键值对
使用put()方法将元素放入map中
HashMap中使用键对象来计算hashcode值
HashMap比较快,因为是使用唯一的键来获取对象
**HashSet **
HashSet实现了Set接口
HashSet仅仅存储对象
使用add()方法将元素放入set中
HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashSet较HashMap来说比较慢
object类的方法有哪些
Object是所有类的父类,任何类都默认继承Object。Object类到底实现了哪些方法?
clone方法
保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。getClass方法
final方法,获得运行时类型。toString方法
该方法用得比较多,一般子类都有覆盖。finalize方法
该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。equals方法
该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。hashCode方法
该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。
- wait方法
wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
(1)其他线程调用了该对象的notify方法。
(2)其他线程调用了该对象的notifyAll方法。
(3)其他线程调用了interrupt中断该线程。
(4)时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
notify方法
该方法唤醒在该对象上等待的某个线程。notifyAll方法
该方法唤醒在该对象上等待的所有线程。
volatile的作用以及用法
volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。
volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。
也就是说,volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。
一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。
静态类和单利模式的区别
1、单例可以继承类,实现接口,而静态类不能(可以集成类,但不能集成实例成员);
2、单例可以被延迟初始化,静态类一般在第一次加载时初始化;�
3、单例类可以被集成,他的方法可以被覆写
4、静态方法是面向过程的,如果我们Java面向对象学得好的话应该是不会出现这种类的
内存上:
单例模式执行的时候需要new 一个对象出来存储在堆栈里面,而静态方法不需要,它不依赖于对象(普通方法是Object.method而静态方法是class.method),但是他也是需要内存的,它是以代码块来存储
生命周期:
静态方法的类会在代码编译的时候就被加载,静态方法中产生的对象,会随着静态方法执行完毕而释放掉,而且执行类中的静态方法时,不会实例化静态方法所在的类。
如果用单例模式, 产生的那一个唯一的实例,会一直在内存中,不会被GC清除的(原因是静态的属性变量不会被GC清除),除非整个应用退出了JVM
**执行效率: **
静态方法与实例方法,在加载时机和占用内存上,静态方法和实例方法是一样的,在类型第一次被使用时加载。调用的速度基本上没有差别。
但是从日志打印来看,个人感觉还是静态方法在执行效率上快一点。
super和this能不能同时使用
必须在构造器的第一行放置super或者this构造器,否则编译器会自动地放一个空参数的super构造器的,其他的构造器也可以调用super或者this,调用成一个递归构造链,最后的结果是父类的构造器(可能有多级父类构造器)始终在子类的构造器之前执行,递归的调用父类构造器。无法执行当前的类的构造器。也就不能实例化任何对象,这个类就成为一个无为类。
从另外一面说,子类是从父类继承而来,继承了父类的属性和方法,如果在子类中先不完成父类的成员的初始化,则子类无法使用,应为在java中不允许调用没初始化的成员。在构造器中是顺序执行的,也就是说必须在第一行进行父类的初始化。而super能直接完成这个功能。This()通过调用本类中的其他构造器也能完成这个功能。
因此,this()或者super()必须放在第一行。
switch能否用String做参数
jdk1.7并没有新的指令来处理switch string,而是通过调用switch中string.hashCode,将string转换为int从而进行判断。
九种基本数据类型 长度以及它们的封装类
java提供了一组基本数据类型,包括
boolean, byte, char, short, int, long, float, double, void.
同时,java也提供了这些类型的封装类,分别为
Boolean, Byte, Character, Short, Integer, Long, Float, Double, Void
什么Java会这么做?
在java中使用基本类型来存储语言支持的基本数据类型,这里没有采用对象,而是使用了传统的面向过程语言所采用的基本类在型,主要是从性能方面来考虑的:因为即使最简单的数学计算,使用对象来处理也会引起一些开销,而这些开销对于数学计算本来是毫无必要的。但是在java中,泛型类包括预定义的集合,使用的参数都是对象类型,无法直接使用这些基本数据类型,所以java又提供了这些基本类型的包装器。
基本数据类型与其对应的封装类由于本质的不同,具有一些区别:
- 基本数据类型只能按值传递,而封装类按引用传递。
- 基本类型在堆栈中创建;而对于对象类型,对象在堆中创建,对象的引用在堆栈中创建。基本类型由于在堆栈中,效率会比较高,但是可能会存在内存泄漏的问题。
Java的四种引用(强弱软虚),应用场景
在JDK1.2后,java对引用的概念进行了扩充。按照引用强度依次从强到弱分为:强引用、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)用四种。
强引用:最常见的,不会被GC回收的对象,如 Object obj = new Object();
软引用:可有可无的对象,如果内存空间足够,GC就不会去回收这个对象,如果内存不足,就会回收,软引用可有和ReferenceQueue(引用队列)联合使用,如果软引用所引用的对象被GC回收,JVM就会把这个软引用加入到引用队列中。
public static void soft() throws Exception{
Object obj = new Object();
ReferenceQueue rq = new ReferenceQueue<>();
SoftReference sr = new SoftReference(obj, rq);//创建关于obj的软引用,使用引用队列
System.out.println(sr.get()); //get方法会输出这个obj对象的hashcode
System.out.println(rq.poll()); //输出为null
obj = null;
System.gc();
Thread.sleep(200); //因为finalizer线程优先级很低,所以让线程等待200ms
System.out.println(sr.get()); //因为堆空间没满,可有可无的特性,所以还是会输出这个obj对象的hashcode
System.out.println(rq.poll()); //自然队列为null
}
弱引用:也是描述可有可无的对象,和软引用不同的是,它的生命周期更短,在GC的过程中,一旦发现有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。 真是因为这个特性,所以弱引用常用于Map数据结构中,引用占用空间内存较大的对象。
public static void weak() throws Exception{
Object obj = new Object();
ReferenceQueue rq = new ReferenceQueue<>();
WeakReference wr = new WeakReference(obj,rq);
System.out.println(wr.get());
System.out.println(rq.poll());
obj = null;
System.gc();
Thread.sleep(200);
System.out.println(wr.get()); //这时候会输出null
System.out.println(rq.poll()); //rq队列里也会存放这个弱引用,输出它的hashcode
}
虚引用:也叫幽灵引用,他的构造方法必须传递RefenceQueue参数,当GC准备回收一个对象时,发现它还有虚引用,就会在回收前,把虚引用加入到引用队列中,程序可以通过判断队列中是否加入虚引用来判断被引用的对象是否将要GC回收,从而可以在finalize方法中采取措施。
Excption与Error包结构。OOM你遇到过哪些情况,SOF你遇到过哪些情况
详见: https://www.jianshu.com/p/e7069f363baa
Override(重写) 和 Overload(重载)的含义与区别
1. 综述
重写(Override)也称覆盖,它是父类与子类之间多态性的一种表现,而重载(Overload)是一个类中多态性的一种表现。 override从字面就可以知道,它是覆盖了一个方法并且对其重写,以求达到不同的作用。overload它是指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法,然后再调用时,VM就会根据不同的参数样式,来选择合适的方法执行。
2. override(重写,覆盖)
(1)方法名、参数、返回值相同。
(2)子类方法不能缩小父类方法的访问权限。
(3)子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。
(4)存在于父类和子类之间。
(5)方法被定义为final不能被重写。
(6)被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
3. overload(重载,过载)
(1)参数类型、个数、顺序至少有一个不相同。
(2)不能重载只有返回值不同的方法名。
(3)针对于一个类而言。
(4)不能通过访问权限、返回类型、抛出的异常进行重载;
(5)方法的异常类型和数目不会对重载造成影响;
4. override应用:
(1)最熟悉的覆盖就是对接口方法的实现,在接口中一般只是对方法进行了声明,而我们在实现时,就需要实现接口声明的所有方法。
(2)除了这个典型的用法以外,我们在继承中也可能会在子类覆盖父类中的方法。
5. 总结
override是在不同类之间的行为,overload是在同一个类中的行为。
interface 和 abstract类的区别
抽象类(Abstract class):含有abstract修饰符的class即为抽象类。
(1)abstract class不能创建实例对象;
(2)含有abstract方法的类必须定义为abstract class,但abstract class类中的方法不必是抽象的;
(3)abstract类中定义的抽象方法必须在具体子类中实现,所以不能有抽象的构造方法和抽象的静态方法;
【解析:针对不能有抽象的构造方法---构造方法不能被继承,而抽象类的所有抽象方法必须在具体的子类中实现,否则就不能实例化,所以构造方法不能为abstract;针对不能有抽象的静态方法---static修饰的方法在类加载之后、实例化之前就已经分配了内存,而抽象类不能实例化,所以矛盾】
(4)如果子类没有实现父类的abstract方法,那么子类也必须定义为abstract类型;
接口(Interface):可以说是抽象类的一种特例,即接口中的所有方法都必须是抽象的。
(1)接口中的方法定义默认为public abstract类型;
(2)接口中的变量定义默认为public static final类型;
具体区别:
(1)抽象类可以有构造方法,但接口不能有构造方法;
(2)抽象类可以有普通成员变量,但接口没有普通成员变量;
(3)抽象类中可以包含非抽象的普通方法,而接口中的方法必须都是抽象的、不能有非抽象的方法;
(4)抽象类中的抽象方法的访问类型可以是public、protected,但接口中的抽象方法只能是public类型且默认为public abstract类型;
(5)抽象类中可以包含静态方法,而接口中不能包含静态方法;
(6)抽象类和接口都可以包含静态成员变量,抽象类的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认为public static final类型;
(7)一个类可以实现多个接口,但只能继承一个抽象类;
Static class 与non static class的区别
外部类只能使用public、final、abstract修饰,不能使用private、protected、static修饰,但是内部类可以。非静态内部类不能拥有静态成员。
内部类的作用:①不允许同包的其他类访问该类;②内部类成员可以直接访问外部类私有数据;③匿名内部类适合用于创建那些仅需要使用一次的类。
非静态内部类可以访问外部类的private成员,但非静态内部类的成员不能被外部类直接使用,如需访问则必须要创建非静态内部类的对象进行访问。
非静态内部类访问变量x,首先判断是否存在局部变量x,如果存在则使用该变量;如果没有,判断是否存在非静态内部类成员变量x,如果存在则使用该变量;如果没有,判断是否存在外部类成员变量x,如果存在则使用该变量;如果没有,系统出现编译错误。
如果外部类成员变量、内部类成员变量和局部变量重名,则通过外部类类名.this.变量名、this.变量名和变量名区分。
静态内部类,使用static修饰的内部类,这种内部类属于外部类本身,而不是外部类的对象,因此又叫类内部类。
静态内部类可以包含静态成员和非静态成员。
静态内部类(即使时实例成员)不能访问外部类的实例成员,只能访问外部类的静态成员。
接口内部类只能时静态内部类。
继承的好处和坏处
A:继承的好处
a:提高了代码的复用性
b:提高了代码的维护性
c:让类与类之间产生了关系,是多态的前提
B:继承的弊端
类的耦合性增强了。
打破了封装,因为基类向子类暴露了实现细节
白盒重用,因为基类的内部细节通常对子类是可见的
当父类的实现改变时可能要相应的对子类做出改变
不能在运行时改变由父类继承来的实现
开发的原则:高内聚,低耦合。
耦合:类与类的关系
内聚:就是自己完成某件事情的能力
多态的实现原理
详细解释:https://www.cnblogs.com/startRuning/p/5673485.html
多态的概念:同一操作作用于不同对象,可以有不同的解释,有不同的执行结果,这就是多态,简单来说就是:父类的引用指向子类对象。
JAVA使用了后期绑定的概念。当向对象发送消息时,在编译阶段,编译器只保证被调用方法的存在,并对调用参数和返回类型进行检查,但是并不知道将被执行的确切代码,被调用的代码直到运行时才能确定。
将一个方法调用同一个方法主体关联起来被称作绑定,JAVA中分为前期绑定和后期绑定(动态绑定或运行时绑定),在程序执行之前进行绑定(由编译器和连接程序实现)叫做前期绑定,因为在编译阶段被调用方法的直接地址就已经存储在方法所属类的常量池中了,程序执行时直接调用,具体解释请看最后参考资料地址。后期绑定含义就是在程序运行时根据对象的类型进行绑定,想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而找到对应的方法,简言之就是必须在对象中安置某种“类型信”,JAVA中除了static方法、final方法(private方法属于)之外,其他的方法都是后期绑定。后期绑定会涉及到JVM管理下的一个重要的数据结构——方法表,方法表以数组的形式记录当前类及其所有父类的可见方法字节码在内存中的直接地址。
动态绑定具体的调用过程为:
1.首先会找到被调用方法所属类的全限定名
2.在此类的方法表中寻找被调用方法,如果找到,会将方法表中此方法的索引项记录到常量池中(这个过程叫常量池解析),如果没有,编译失败。
3.根据具体实例化的对象找到方法区中此对象的方法表,再找到方法表中的被调用方法,最后通过直接地址找到字节码所在的内存空间。
最后说明,域和静态方法都是不具有多态性的,任何的域访问操作都将由编译器解析,因此不是多态的。静态方法是跟类,而并非单个对象相关联的。
jvm classLoader architecture
详解:https://www.cnblogs.com/kaikailele/p/3916320.html
当JVM(Java虚拟机)启动时,会形成由三个类加载器组成的初始类加载器层次结构:
bootstrap classloader
|
extension classloader
|
system classloader
bootstrap classloader -引导(也称为原始)类加载器,它负责加载Java的核心类。在Sun的JVM中,在执行java的命令中使用-Xbootclasspath选项或使用- D选项指定sun.boot.class.path系统属性值可以指定附加的类。这个加载器的是非常特殊的,它实际上不是 java.lang.ClassLoader的子类,而是由JVM自身实现的。
extension classloader -扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的类包。这为引入除Java核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个JRE中启动的JVM都是通用的,所以放入这个目录的 JAR类包对所有的JVM和system classloader都是可见的。
system classloader -系统(也称为应用)类加载器,它负责在JVM被启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者 CLASSPATH操作系统属性所指定的JAR类包和类路径。总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。
classloader 加载类用的是全盘负责委托机制。所谓全盘负责,即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有 Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入;委托机制则是先让parent(父)类加载器 (而不是super,它与parent classloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。此外类加载还采用了cache机制,也就是如果 cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。
每个ClassLoader加载Class的过程是:
- 检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2
- 如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4
- 请求parent classloader载入,如果成功到8,不成功到5
- 请求jvm从bootstrap classloader中载入,如果成功到8
- 寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.
- 从文件中载入Class,到8.
- 抛出ClassNotFoundException.
- 返回Class.
阻塞和非阻塞与异步和同步的理解
详解: https://www.linuxidc.com/Linux/2015-07/120338.htm