1.Java 集合类介绍
Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。
集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:
-
接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象
-
实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
-
算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序,这些算法实现了多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
2. 常用的 Java集合 --List集合
List接口继承了Collection接口,可使用Collection中的所有方法,此外List接口还定义了两个重要的方法,get和set。
get(int index):获得指定索引位置的元素
set(int index, Object obj):将集合中指定索引位置的对象修改为指定对象。
List接口元素是有序的,且允许集合中的元素重复。
1)List接口的实现类
List接口常用的实现类有ArrayList和LinkedList。
ArrayList:ArrayList类实现了可变的数组,可根据索引快速访问集合中的元素,但向指定索引位置插入及删除对象速度较慢。
LinkedList:LinkedList类采用链表结构保存对象。这种结构优点是插入和删除对象效率高,但对于随机访问集合中的对象效率较低。
链表并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。链表可分为单向链表和双向链表。
一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。
一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。
与 ArrayList 相比,LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低。
因此总结一下:
以下情况使用 ArrayList :
- 频繁访问列表中的某一个元素。
- 只需要在列表末尾进行添加和删除元素操作。
以下情况使用 LinkedList :
- 你需要通过循环迭代来访问列表中的某些元素。
- 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。
2)实例化
ArrayList和LinkedList实例化:
List<E> list=new ArrayList<>();
List<E> list2=new LinkedList<>();
来看代码例子:
import java.util.*;
import java.util.List;
public class Muster{
public static void main(String args[]) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
int i = (int)(Math.random()*list.size());
System.out.println("随机获取数组中的元素:" +list.get(i));
list.remove(2);
System.out.println("将索引2的元素移除后,数组中剩余的元素是: ");
for (String j : list) {
System.out.println(j);
}
list.set(2,"e");
System.out.println("插入元素后的数组:" +list);
}
}
结果:
随机获取数组中的元素:b
将索引2的元素移除后,数组中剩余的元素是:
a
b
d
插入元素后的数组:[a, b, e]
3. 常用的 Java集合 --Set集合
Set集合不允许存储重复元素,且没有索引,若要查找Set集合中的元素需要遍历集合,Set集合常用的方法如下:
public interface Set<E> extends Collection<E> {
A:添加功能
boolean add(E e);
boolean addAll(Collection<? extends E> c);
B:删除功能
boolean remove(Object o);
boolean removeAll(Collection<?> c);
void clear();
C:长度功能
int size();
D:判断功能
boolean isEmpty();
boolean contains(Object o);
boolean containsAll(Collection<?> c);
boolean retainAll(Collection<?> c);
E:获取Set集合的迭代器:
Iterator<E> iterator();
F:把集合转换成数组
Object[] toArray();
<T> T[] toArray(T[] a);
//判断元素是否重复,为子类提高重写方法
boolean equals(Object o);
int hashCode();
}
Set接口常用的实现类主要有HashSet类和TreeSet类,这里重点讲下HashSet,先了解下HashSet的几个重载的构造方法
private transient HashMap<E,Object> map;
//默认构造器
public HashSet() {
map = new HashMap<>();
}
//将传入的集合添加到HashSet的构造器
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
//明确初始容量和装载因子的构造器
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
//仅明确初始容量的构造器(装载因子默认0.75)
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
通过上述代码可以看出HashSet的底层是通过HashMap实现的,HashMap实现了Set接口,同时还实现了序列化和可克隆化,而集合(Set)是不允许重复值的,所以HashSet是一个没有重复元素的集合,但不保证集合的迭代顺序,因此随着时间其集合内部元素的顺序可能会改变。同时由于HashSet是基于HashMap来实现的,所以允许空值,不是线程安全的。
HashMap的数据存储是通过数组+链表/红黑树实现的,存储数据首先通过hash函数计算在数组中存储的位置,如果该位置已经有值了,判断key是否相同,相同则覆盖,不相同则放到元素对应的链表中,如果链表长度大于8,就转化为红黑树,如果HashMap的存储容量不够,则需扩容,默认扩容因子为0.75,这里可以了解下HashMap的底层原理,这样可以更加深入理解HashSet。
4. 常用的 Java集合 --Map集合
Map集合是以键值对的方式存储元素,所有Map集合的Key是无序不可重复的,key和value都是引用数据类型,存的都是内存的地址。
4.1. Map集合的特点
1) key和value可以是任意的引用类型的数据
2) 一个映射不能包含重复的键(map集合的key值不能重复)
3) 每个键最多可以映射到一个值(每个键值对只有一个key值,一个value值)
4) 同样的值可以对应多个不同的键(不同的键值对可以拥有相同的value值)
4.2. Map集合的常用方法
1) get
方法全称为:Object get(Object key),可以查询map集合中key对应的value值,返回value值,使用方法如下:
public class Muster {
public static void main(String[] args){
Map map = new HashMap();
map.put(1,"海鸥");
map.put(2,"乌鸦");
map.put(3,"大鹏");
Object s = map.get(3);
System.out.println(s);
}
}
结果:
大鹏
2) put
方法全称为:put(Object key,Object value),给map集合中添加键值对,使用方法如下:
public class Muster {
public static void main(String[] args){
Map map = new HashMap();
map.put(1,"海鸥");
map.put(2,"乌鸦");
map.put(3,"大鹏");
System.out.println("map集合中的元素:"+map);
}
}
结果:
map集合中的元素:{1=海鸥, 2=乌鸦, 3=大鹏}
3) map集合的遍历
map集合有两种常用遍历方法,第一种常用方法是根据键找值,借助Set keySet()遍历;第二种是借助迭代器,根据键值对对象找键和值,来看代码例子:
public class Muster {
public static void main(String[] args){
Map map = new HashMap();
map.put(1,"海鸥");
map.put(2,"乌鸦");
map.put(3,"大鹏");
Object s = map.get(3);
// System.out.println(s);
System.out.println("map集合中的元素:"+map);
/*
借助Set<K> keySet()遍历
1、获取Map集合中所有映射的键的Set集合
2、遍历键的集合,根据每一个键获取对应的值
*/
//方式一,用增强for循环
// Set<Integer> keys = map.keySet();
// for( Integer m : keys){
// Integer key = m;
// Object value = map.get(m);
// System.out.println(key+"="+value);
// }
//方式二,用迭代器
//为Set集合创建一个迭代器Iterator
Iterator<Integer> it = map.keySet().iterator();
while(it.hasNext()){
Integer key = it.next();
System.out.println(key+"="+map.get(key));
}
}
}
结果:
map集合中的元素:{1=海鸥, 2=乌鸦, 3=大鹏}
1=海鸥
2=乌鸦
3=大鹏
其它的常用方法例如clear,containsKey,containsValue,remove等方法就不一一介绍了。
4.3. Map集合的实现类
Map集合的实现类主要有HashMap、HashTable、TreeMap 、 ConcurrentHashMap、LinkedHashMap、weakHashMap等等。这里重点讲一下HashMap。
JDK1.8版本中HashMap底层存储结构是数组+链表+红黑树形式,实现Map.Entry接口。它有如下特点:
1)HashMap是 Map 接口使用频率最高的实现类。
2)key和value值允许为null,与HashSet一样,不保证映射的顺序。
3)所有的key构成的集合Set都是无序、不可重复的。所以,key所在的类要重写:equals()和hashCode()
4)所有的value构成的集合Collection是无序的、但可以重复的。所以,value所在的类要重写:equals()
5)一个key-value构成一个entry,因此所有的entry构成的集合也是无序的、不可重复的
6)HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
7)HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
HashMap的存储结构
JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。
HashMap的扩容
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的
长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在
HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算
其在新数组中的位置,并放进去,这就是resize。
HashMap进行扩容的时机:
HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数(size)*填充因子(loadFactor) 时就会进行数组扩容。
loadFactor:
loadFactor的默认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16x0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把进行扩容。
容量翻倍:
HashMap的扩容是扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
树化和反树化
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
总结:
JDK1.8相较于之前的变化:
HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
当首次调用map.put()时,再创建长度为16的数组
数组为Node类型,在jdk7中称为Entry类型
形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。
面试题:
负载因子值的大小,对HashMap有什么影响?
负载因子的大小决定了HashMap的数据密度。
负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。