文章目录
- 1. 集合
- 2.Collection接口
- 2.1 List接口
- 2.1.1 Iterator是什么
- 2.1.2 Iterator有什么特点,怎么使用
- 2.1.3 如何一边遍历一边删除集合元素
- 2.1.4 Iterator和ListIterator有什么区别
- 2.1.5 遍历List的方式
- 2.1.6 说下ArrayList的优缺点
- 2.1.7 数组与List之间的转换
- 2.1.8 ArrayList与LinkedList之间的区别
- 2.1.9 ArrayList与Vector之间的区别
- 2.1.10 插入数据ArrayList,LinkedList与Vector的性能
- 2.1.11 多线程场景下使用ArrayList
- 2.1.12 为什么ArrayList的elementData加上transient修饰
- 2.1.13 List与Set的区别
- 2.2 Set接口
- 2.3 Queue的poll()和remove()区别是什么
- 3. Map接口
- 4. 辅助工具类
万恶之源:
1. 集合
1.1 什么是集合
集合是存储数据的容器,这里的数据指的是对象,可以存储不同的对象,并且长度可变。
1.2 集合特点
- 集合用于对象封装数据,存储对象
- 对象的个数可以确定时使用数组,不确定时使用集合,因为集合长度可变
1.3 集合和数组得区别
- 数组的长度是固定的,集合是可变的
- 数组可以存储基本数据类型,也可以存储引用数据类型,集合只能存储引用数据类型
- 数组存储的元素必须是同一个数据类型,集合存储的对象可以是不同类型
1.3 什么是集合框架
集合框架是为了操作集合而规定的一种标准的体系结构,任何集合框架都包括三大块内容:对外接口,接口实现,集合运算算法。
对外接口:即集合的抽象数据类型,接口提供了我们能够对集合提供的内容进行操作。
接口实现:也就是集合框架的接口的具体实现,实际上是哪些可以复用的数据结构。
集合运算算法:例如,查找,排序算法,都是一些可以复用的函数。
1.4 集合框架的优点有哪些
- 降低核心类的开发成本,而非使用我们自己开发的集合类。
- 通过使用JDK附带的集合类,可以降低开发成本。
- 集合框架类经过严格的测试,开发质量会有所提高。
- 容量自增长。
1.5 集合框架中的泛型有什么优点?
自从JDK 5发行后,引入的泛型编程,在集合类和接口中都在大量使用泛型,泛型提供可以容纳的对象类型,因此如果你添加了其它的对象类型,会出现类之间的转换异常,编译时就会报错ClassCastException,有些错误就可以避免,这样就会给运行时带来好处。
1.6 常用的集合类
Map接口和Collection接口是所有集合类的父接口
- Collection:实现接口主要有Set,List,Queue
- Map:实现接口主要有Hashtable,HashMap,TreeMap,ConcurrentHashMap以及Properties
- Set接口主要实现类有Hashset,SortSet,LinkedHashSet
- List接口主要实现有ArrayList,LinkedList,Vector
1.5 哪些集合类是线程安全的?
- Vector就比ArrayList多了个同步机制(线程安全)但因为效率低,已经很少被使用了。
- Stack:堆栈类,先进后出
- HashTable:就比HashMap多了个线程安全
- enumeration:枚举,相当于迭代器
1.6 怎么确保一个集合不被修改?
可以使用Colections工具类的方法,任何集合对象经过处理都不能被修改
否则就会抛出异常
List<String> list = new ArrayList<>();
list.add("x");
Collection<String> collection = Collections.unmodifiableCollection(list);
collection.add("y");
System.out.println(collection.size());
Exception in thread "main" java.lang.UnsupportedOperationException
1.7 Java集合的快速失败机制 “fail-fast”?
fail-fast是Java的一种错误检测机制,当多线程对集合上的结构进行操作时,就可能产生fail-fast机制。
出现场景:
假如存在两个线程(线程1,线程2),线程1通过Iterator在遍历集合A的元素,在某个线程2中修改了集合的结构(不是单纯的修饰其内容),那么这个时候程序就会抛出CurrentModificationException异常,从而产生fail-fast机制。
原因:
迭代器在遍历时直接访问集合中的内容,并且在遍历的时候使用一个modCount变量,集合在被遍历的过程中,如果内容发生变化(如元素被删除),则modCount就会发生改变。每次迭代器使用hashNext()/next()遍历下一个元素之前,就会先比较mCount是否与expectedModCount相等,expectedModCount默认值等于集合元素的个数,如果是就返回遍历,不是的话就抛出异常。
解决方法:
- 在遍历过程是,所有涉及改变modCount的值的地方全部加上synchronized
- 使用CopyOnWriteArrayList(线程安全)来替换ArrayList
更多CopyOnWriteArrayList:
https://baijiahao.baidu.com/s?id=1666117483794506320&wfr=spider&for=pc
1.8 List如何一边遍历一边删除
先说说我们可能会犯的错误
List<Integer> list = new ArrayList<Integer>();
list.add(11);
list.add(22);
list.add(33);
for (Integer integer : list) {
list.remove(integer);
}
如此我们会犯什么错了,看上去好像没错,但是运行之后就会产生上面1.8处说明的fail-fast机制,报出ConcurrentModificationException
产生的原因:和上面分析分析一致,foreach遍历其实内部采用的就是Iterator的遍历思想,当迭代器(线程1)在遍历的时候会维护着一个modCount遍历,但是如果list.remove(integer)(线程2)进行集合结构内容的改变时,就会改变modCount值,影响到迭代器遍历,爆出线程并发异常。
解决方法:除了上面提到的两个外这边可以直接使用迭代器的删除方法,如下:
List<Integer> list = new ArrayList<Integer>();
list.add(11);
list.add(22);
list.add(33);
Iterator<Integer> it = list.iterator();
while (it.hasNext()){
Integer next = it.next();
System.out.println(next);
it.remove();
}
注意:Iterator对象开始的cursor(游标)是指向0,类似于头指针,hasNext查看有没有下一个元素,所以it.next();不能放remove下方。
2.Collection接口
2.1 List接口
2.1.1 Iterator是什么
迭代器Iterator接口提供遍历任何Collection接口, public interface Collection<E> extends Iterable<E>
,我们可以从Collection中使用迭代器方法获取迭代器实例,迭代器允许Java集合在遍历的时候删除元素。
2.1.2 Iterator有什么特点,怎么使用
List<Integer> list = new ArrayList<Integer>();
list.add(11);
list.add(22);
list.add(33);
Iterator<Integer> it = list.iterator();
while (it.hasNext()){
Integer next = it.next();
System.out.println(next);
}
Iterator特点是单向遍历,但是更加安全,它可以保证当前正在遍历集合元素被修改的时候,爆出并发异常ConcurrentModificationException
2.1.3 如何一边遍历一边删除集合元素
### 1.8 List如何一边遍历一边删除
2.1.4 Iterator和ListIterator有什么区别
- Iterator可以遍历List和Set的集合,而ListIterator只能遍历L集合
- Iterator只支持单向遍历,而ListIterator支持双向遍历(前/后)
- ListIterator实现Iterator接口,又添加了一些额外功能,如添加一个元素,替换一个元素。
2.1.5 遍历List的方式
遍历方法:
- for循环遍历:基于计数器,集合外部维护着一个计数器,然后依次读取每个元素,直到读到最后一个元素即可。
- Iterator遍历:Iterator是面向对象的一种设计模式,目的是屏蔽不同集合的特点,统一遍历集合接口,Collectioin中采用的迭代器模式。
- foreach:内部还是使用了迭代器的方式实现,使用时不需要显示的声明迭代器或计数器,优点时代码简洁,不易出错,缺点是,在遍历的时候不能操作集合,如增删改,否则报会并发异常。
最佳方法:Java Collection接口中提供了RandomAccess 接口,用于标记集合是否支持RandomAccess接口
- 如果一个集合实现了该接口,则它支持快速随机访问,按位置读取元素的平均时间复杂度是O(1),如ArrayList
- 如果集合没有实现该接口,表示不支持RandomAccess接口,如LinkedList
综上:推荐的做法是,如果集合支持RandomAccess可以使用for遍历,否则使用Iterator/foreach遍历。
2.1.6 说下ArrayList的优缺点
优点:
- ArrayList底层采用数组实现,是一种随机的访问模式,并且实现了RandomAccess接口,因此查找的速度非常快。
- ArrayList顺序添加元素非常方便
缺点:
- 删除元素的时候需要做元素的复制操作,如果元素的很多的话,很消耗性能。
- 插入元素的时候,同样遇到上面的问题,移动移动元素,让出插入位置。
2.1.7 数组与List之间的转换
- 数组转List:使用Arrays.asList方法
- List转数组:使用toArray方法
List<Integer> list = new ArrayList<Integer>();
list.add(11);
list.add(22);
list.add(33);
Object[] objects = list.toArray(); //list->[]
String []strs = new String[]{
"aa","bb"};
List<String> strings = Arrays.asList(strs); //[]->list
2.1.8 ArrayList与LinkedList之间的区别
数据结构:ArrayList底层采用的是数组实现,LinkedList采用双向链表实现。
数据访问:ArrayList要优于LinkedList,因为ArrayList底层采用数组实现,LinkedList采用线性的链式存储结构,需要移动指针来访问。
增删效率:LinkedList要优于ArrayList,ArrayList增删要复制元素,影响其它数组元素的下标,而LinkedList只需要移动指针。
线程安全:ArrayList和LinkedList都是线程不同步的,都不能保证线程安全。
综上:如果对集合元素增删比较频繁的话推荐使用LinkedList,查找比较频繁的话推荐使用ArrayList。
2.1.9 ArrayList与Vector之间的区别
ArrayList与Vector都是List接口的实现,List接口又实现了Collection接口,因此ArrayList与Vector都是有序的集合。
- Vector相比ArrayList更为安全,因此它增加了Synchronized来实现线程同步,因此是线程安全的,而ArrayList线程不安全。
- ArrayList的性能要优于Vector
综上:追求效率至上不需要保证线程安全的情况下推荐使用ArrayList,需要线程安全的情况下推荐Vector
2.1.10 插入数据ArrayList,LinkedList与Vector的性能
ArrayList和Vector底层都是采用数组实现,Vector相比ArrayList加入了线程安全机制,相比ArrayList更为安全,但是效率却不及ArrayList,它们都不适合用频繁增删集合元素,因此会影响到集合内的其它元素。
LinkedList底层采用双向链表实现,增删元素靠指针来完成,不会影响到集合内的其它元素,因此效率更高。
2.1.11 多线程场景下使用ArrayList
由于ArrayList非线程安全,多线程的场景下使用ArrayList,可能会出现问题,可以使用Collections的synchronizedList方法将其转成一个线程安全的List再使用
List<Integer> list = new ArrayList<Integer>();
list.add(11);
list.add(22);
list.add(33);
List<Integer> integers = Collections.synchronizedList(list);
integers.add(44);
2.1.12 为什么ArrayList的elementData加上transient修饰
transient Object[] elementData; // non-private to simplify nested class access
回答问题前必须先明确transient的作用是什么?
持久化对象是,如果一个对象不想用序列化的机制来保存它,就可以在前面加一个transient。
回到刚刚的问题elementData前面加上transient说明elementData不会被序列化。这样做又什么好处呢?请看ArrayList中两个重要的函数
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
elementData是ArrayList存储元素的数组,是一个缓存数组,它通常会预留一些空间,等容量不足再进行扩容。每次序列化时,先调用defaultWriteObject方法来序列化ArrayList中哪些非transient修饰的内容,然后遍历elementData,用readObject和writeObject方法来实序列化就可以保证之序列化哪些实际存储的元素,从不是整个数组,从而节省了序列化过程中的空间和时间。
参考:https://blog.csdn.net/weixin_33924220/article/details/92348708
2.1.13 List与Set的区别
List和Set都继承于Collection接口
特点
List:一个有序的(存入和取出的顺序一致)的容器,可以插入null值,可以又重复元素,元素都有索引,常用的实现类有ArrayList,LinkedList,Vector.
Set:一个无序的(存入和取出的顺序可能不一致)的容器,不允许重复元素,只允许存入一个null,必须保证元素的唯一性,Set常见的实现类有HashSet,LinkedHashSet,TreeSet.
遍历
List因为是有序的,所以支持使用for,也就是通过下标来遍历,迭代器遍历,而Set只支持迭代器遍历,因为它是无序的,无法通过下标来获取值。
2.2 Set接口
2.2.1 说一下HashSet的实现原理
HashSet是基于HashMap实现的,HashSet的值存放在HashMap的key上,相关HashSet的操作,基本上都是直接调用底层的HashMap的相关方法来实现的,并且HashSet不可重复,是无序的。
2.2.2 说一下HashSet如何检查重复,如何保证数据不可重复?
说在前面:HashSet是Set的实现,不是Map接口下的实现。。。。
HashSet的add方法底层使用HashMap的put方法来插入数据,判断数据是否存在的依据,不仅要比较hashcode值还要equals方法。
HashSet部分源码:
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
return map.put(e, PRESENT)==null;
}
HashMap的key是唯一的,由源码可知HashSet的key就是使用HashMap的key,如果HashSet中遇到相同的key时,对于新的value就会覆盖旧的value,返回旧的value,所以是不会重复的。
HashMap比较key是否相等是先比较hashcode,再equals。
<mark>回顾hashcode和equals。</mark>
Java对hashcode()和equals()有以下规定:
- 如果两个对象相等,则它们的hashcode()一定相等
- 如果两个对象相等,则它们的equals也返回true
- 如果两个对象的hashcode相等,但是它们不一定相等。
综上:我们再重写equals时,规定也必须重写hashcode
hashcode()的默认行为是对堆上的对象产生独特值,如果没有重写hashcode(),无论如何两个对象都不会相等(即使两个对象指向相同的数据)
== 与 equals的区别
- == 是判断两个变量或实例是否指向相同的内存地址,equals是判断两个变量或实例指向的内存地址对应的值是否相等
- ==是对内存地址进行比较,equals是对字符串进行比较
- ==是比较引用是否相同,equals是值是否相同
2.2.3 HashSet和HashMap的区别?
HashMap | HashSet |
---|---|
实现Map接口 | 实现Set接口 |
存储键值对 | 存储对象 |
调用put方法向map中添加元素 | 调用add方法向Set中添加元素 |
HashMap使用key来计算hashcode值 | HashSet使用成员对象来计算hashcode值,如果相等则在调用equals判断,如果对象不同的话,返回false |
HashMap相比HashSet快,因为它是使用唯一的key来获取值 | HashSet比HashMap稍慢点 |
2.3 Queue的poll()和remove()区别是什么
Queue<String> queue = new LinkedList<>();
queue.offer("11");
// System.out.println(queue.poll()); //11
// System.out.println(queue.poll()); //null
System.out.println(queue.remove()); //11
System.out.println(queue.remove()); //java.util.NoSuchElementException
区别已经很明显了
- 相同点:poll()和remove()都是删除队列元素并返回删除元素
- 不同点:当队列中没有元素时remove会抛出异常
java.util.NoSuchElementException
,而poll返回null
3. Map接口
https://blog.csdn.net/JAYU_37/article/details/104433085
4. 辅助工具类
4.1 comparable和comparator的区别
- comparable在
java.lang.Comparable
它有一个comparaTo方法用于排序;comparator在java.util
包中,它有一个compare(Object obj1,Object obj2)用于排序 - 如果实现类没有实现comparable接口或者实现了comparable接口但是不满足于comparable接口提供的compareTo方法,我们可以自定义比较方法,只需要实现Comparator接口重写compare(Object obj1,Object obj2)方法。
- comparable需要比较对象来实现接口,使用对象调用方法来比较对象,需要改变对象内部结构(重写compareTo),耦合度高。comparator相当于一个通用的比较工具接口,需要定制一个比较类去实现它,重写里面的compare方法,方法参数即比较的对象,对象不用做任何的改变,解耦。
4.2 Collection和Collections的区别
Collection是java.utils.Collectioni
的集合接口,也是集合的顶级接口,它提供了对集合对象的基本操作和通用方法,Collection类在Java类库中有很多体现,直接实现类有List,Set。
Collections:是集合类的一个工具类,提供了一系列的静态方法,用于对集合中的元素进行排序,搜索等操作。