Collection常用接口、实现类
Map接口树
List 接口
ArrayList 源码分析
jdk 1.7:
ArrayList list = new ArrayList(); //底层长度为10的Object[]数组elementData
list.add(123); //实际为 elementData[0] = new Integer(123);
...
list.add(11); //如果此处添加导致elementData数组容量不足,则扩容。
//默认情况下,扩容为原来容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
//结论:为了避免频繁扩容造成的性能下降,建议使用带参的构造器:ArrayLsit list = new ArrayList(int capacity)
jdk 1.8:
ArrayList list = new ArrayList(); //底层Object[] elementData初始化为{},并没有创建长度为10的数组
list.add(123); //第一次调用add()时,底层才创建了长度为10的数组,并将数据123添加到 elementData[0]中
...
//之后的扩容与jdk 1.7相同
小结:jdk 1.7中的ArrayList的对象的创建类似于单例的饿汉模式,
而jdk 1.8中的ArrayList对象的创建类似于懒汉模式,延迟了数组的创建,节省了内存
LinkedList 源码分析
LinkedList list = new LinkedList(); //内部声明了Node类型的prev和next属性,默认值为null
list.add(123); //将123封装到Node中,创建了Node对象
其中Node的定义,体现了LinkedList的双向链表说法
private static class Node<E>{
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next){
this.item = element;
this.next = prev;
this.last = next;
}
}
链表概略图
Vector 底层
jdk 1.7 和 jdk 1.8 中通过Vector()构造对象时,底层都创建了长度为10的数组,
在扩容方面,默认扩容为原来数组的2倍
问题
1、集合 Collection 中存储自定义类的对象时,需要自定义类重写那些方法?
List:重写equals方法。
//原因:equals方法默认比较两个对象的地址,如果需要比较两个对象的内容,则需要重写equals方法
Set:
(HashSet、LinkedHashSet):重写equals()、HashCode()
(TreeSet):Comparable:CompareTo(Object obj)
Comparator:compare(Object obj1,Object obj2)
2、ArrayList、LinkedList、Vector三者的相同点和不同点
相同点:三者都是List接口的实现类,元素有先后次序,并且可以重复
不同点:
ArrayList 和 Vector 相比,底层都是动态数组维护的;ArrayList扩容是1.5倍,
而Vector扩容是2倍;ArrayList 是线程不安全的,效率比较高。
ArrayList 和 LinkedList 相比,LinkedList 底层是一个双向链表,适合做插入频繁的删除、插入操作;
若是在尾部插入,ArrayList效率也比较高,按照下标进行查询,ArrrayList的效率比LinkedList高
迭代器的原理
Set 接口
存储的元素是无序的,不可重复的
以HashSet为例:
1、无序性:不等于随机性。存储的数据在底层中并非照数组索引顺序添加,而是根据数据的哈希值决定的。
2、不可重复性:保证添加的元素照equals()判断时,不能返回true。即相同的元素只能添加一个。
底层添加元素过程
向HashSet中添加元素a,首先调用元素 a 所在类的HashCode()方法,计算元素a的哈希值,
此哈希值接着通过某种算法计算出在HashSet底层数组中存放的位置,即为索引位置判断此位置上是否已经存在元素:
如果此位置没有其他元素,则元素 a 添加成功。--->情况1
如果此位置上有其他元素b 或以链表的形式存在多个元素,则比较元素a 与元素 b 的hash值:
如果hash 值不相同,则元素 a 添加成功。--->情况2
如果hash 值相同,进而需要调用元素 a 所在类的equals()方法:
equals()返回true,元素 a 添加失败
equals()返回false,元素 a 添加成功。--->情况3
对于添加成功的情况 2 和情况 3 而言:元素 a 与已经存在指定索引位置上的数据以链表的方式存储。
jdk 1.7:元素 a 放到数组中,指向原来的元素。
jdk 1.8:原来的元素在数组中,指向元素 a 。
总结:七上八下
jdk 1.7 开始 HashSet 的底层:数组 + 链表
实现子类
|---HashSet: 作为Set接口的主要实现类;线程不安全;可以存储null值
|---LinkedHashSet: 作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序进行遍历;
在添加数据的同时,每个数据还维护了两个引用,记录次数据前一个数据和后一个数据。对于频繁的遍历操作,LinkedHashSet效率高于HashSet。
|---TreeSet: 可以按照添加对象的指定属性进行排序。
new HashSet 的底层是 new 一个HashMap,add 一个元素是把一个元素作为Key 来 put。
TreeSet 的底层是红黑树
练习
在 List 内去除重复的数字,要求尽量简单
public class TestListAndSet {
public static List duplicateList(List list){
HashSet set = new HashSet();
set.addAll(list);
return new ArrayList(set);
}
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(new Integer(1));
list.add(new Integer(1));
list.add(new Integer(2));
list.add(new Integer(2));
list.add(new Integer(3));
List list1 = duplicateList(list);
System.out.println(list1);
//打印结果:[1, 2, 3]
}
}
对hashCode()和equals()的理解
public static void main(String[] args) {
HashSet set = new HashSet();
Person p1 = new Person(1001,"a");
Person p2 = new Person(1001,"b");
set.add(p1);
set.add(p2);
System.out.println(set);
//[Person{id=1001, name='a'}, Person{id=1001, name='b'}]
p1.name = "c";
System.out.println(set);
boolean flag = set.remove(p1);
System.out.println(flag);
// false 底层为map.remove(key),此处把p1.name = "a" 改为 p1.name = "c"后,相当于key值变了,所以删除失败
System.out.println(set);
//[Person{id=1001, name='c'}, Person{id=1001, name='b'}]
set.add(new Person(1001,"c"));
System.out.println(set);
//[Person{id=1001, name='c'}, Person{id=1001, name='b'}, Person{id=1001, name='c'}]
set.add(new Person(1001,"a"));
System.out.println(set);
//[Person{id=1001, name='c'}, Person{id=1001, name='a'}, Person{id=1001, name='b'},
// Person{id=1001, name='c'}]
}
Map 接口
Map接口的主要实现类
|---HashMap:作为map的主要实现类,是线程不安全的,因此效率比较高;能存储null的key和value.
|---LinkedHashMap:保证在遍历Map元素时,可以按照添加的顺序进行遍历。原因:在原有的HashMap底层结构基础上,添加了一对指针,
指向前一个元素和后一个元素,对于频繁的遍历操作,此类执行效率高于HashMap。
|---TreeMap:保证按照添加的key-value进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树
|---HashTable:作为古老的实现类,线程是安全的,效率低;不能存储null的key和value。
|---properties:常用来处理配置文件。key 和 value 都是String类型。
HashMap的底层:
jdk 1.7 及之前: 数组 + 链表
jdk 1.8 : 数组 + 链表 + 红黑树
HashMap 与 LinkedHashMap 的比较
public static void main(String[] args) {
Map map = new HashMap();
map.put("3", "value1");
map.put("2", "value2");
map.put("5", "value3");
Set entrySet = map.entrySet();
Iterator iterator = entrySet.iterator();
while(iterator.hasNext()){
Map.Entry<String,String> entry = (Map.Entry<String, String>) iterator.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println("key = " + key + ", value = " + value);
}
/**
* 打印结果:
* key = 2, value = value2
* key = 3, value = value1
* key = 5, value = value3
* 输出的顺序与放入的顺序不一致,取出遍历时,改变了顺序
*/
System.out.println("-----------------------------------------");
Map map1 = new LinkedHashMap();
map1.put("3", "value1");
map1.put("2", "value2");
map1.put("5", "value3");
Set entrySet1 = map1.entrySet();
Iterator iterator1 = entrySet1.iterator();
while(iterator1.hasNext()){
Map.Entry<String, String> entry = (Map.Entry<String, String>) iterator1.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println("key = " + key + ", value = " + value);
}
/**
* 打印结果:
* key = 3, value = value1
* key = 2, value = value2
* key = 5, value = value3
* 输出的结果完全与放入的结果一致
*/
}
linkedHashMap的底层源码
Map的结构
Map结构的理解:
Map中的key:无序的、不可重复的,使用set存储所有的key --->key所在的类要重写equals() 和 hashCode() (以HashMap为例)
Map中的value:无序的,可重复的,使用Collection存储所有的value;
一个键值对(key-value)构成了一个Entry对象。
Map中的Entry:无序的,不可重复的,使用set存储所有的Entry。
HashMap 的底层原理实现
HashMap的底层实现原理(jdk 1.7)
HashMap map = new HashMap()
在实例化以后,底层创建了长度为16的一维数组Entry[] table。
...可能已经执行了很多次put...
map.put(key1, value1):
首先调用key1所在类的hashCode()计算key1哈希值,次哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1添加成功。---情况1
如果此位置上的数据不为空,意味着此位上存在一个或多个数据(以链表形式存在)),比较key1和已经存在一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。---情况2
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)
如果equals()返回false:此时key1-value1添加成功。---情况3
如果equals()返回true:使用value1替换value2。
补充:关于情况2和情况3: 此时key1-value1和原来的数据以链表的方式存储。
在不断的添加过程中,会涉及到扩容问题,默认的扩容方式:扩容为原来的2倍,并将原来的数据复制过来。
jdk 1.8 相较于jdk 1.7 在底层方面的不同:
1、new HashMap():底层没有创建一个长度为16的数组
2、jdk 1.8 底层数组是:Node[],而非Entry[]
3、首次调用put()方法时,底层创建长度为16的数组
4、jdk 1.7 底层结构只有:数组+链表。jdk 1.8 底层结构:数组+链表+红黑树
当数组的某一个索引位置上的元素以链表形式存在的数据个数>8 且 当数据长度>64时,此时此索引位置上的所有数据改为使用红黑树。
添加元素源码
扩容源码 jdk 1.7
与ArrayList达到10开始扩容相比,HashMap的容量为16,但达到12且key位置下的链表非空就开始扩容
Map的遍历方式
//一:获取key,通过keySet是由一个个key组成的来进行相应的遍历
Set set = map.keySet();
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
System.out.println("--------------------------------");
//二:获取value,通过values
Collection values = map.values();
Iterator iterator1 = values.iterator();
while(iterator1.hasNext()) {
System.out.println(iterator1.next());
}
//通过foreach遍历
for(Object value : values){
System.out.println(value);
}
System.out.println("--------------------------------");
//三、通过获取entrySet的对象来获取entry对象,通过entry对象来
Set set1 = map.entrySet();
Iterator iterator2 = set1.iterator();
while(iterator2.hasNext()){
//entrySet是由一个个entry组成的Set集合
Object obj = iterator2.next();
//entry是由key和value组成的,因此可以通过entry的对象获取到key和value
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey()+"--->"+entry.getValue());
}