经过一番描述性的介绍,今天进入List、Set、Map的最后一个环节了,先上思维导图
话题一:Java集合框架是什么?有什么优点?
每种编程语言中都有集合,最初Java只包含了Vector、Stack、HashTable和Array这几种集合。随着集合的广泛使用,从Java1.2开始才提出了囊括所有集合接口、实现和算法的集合框架。
使用集合框架的优点如下:
- 使用核心集合类降低开发成本,而非实现我们自己的集合类。
- 随着使用经过严格测试的集合框架类,代码质量会得到提高。
- 通过使用JDK附带的集合类,可以降低代码维护成本
- 复用性和可操作性。
话题二:集合框架中的泛型有什么特点?
- Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。
- 泛型允许我们为集合提供一个可容纳的对象类型,因此,如果你添加其他类型的任何元素,它会在编译时报错
- 这避免了在运行时出现ClassCastException
- 泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符
- 它给运行时带来好处,因为不会产生类型检查的字节码指令。
话题三:面试中问List、Set、Map的关系应该怎么说?
- 首先他们都是Java集合框架中的一部分,其次是这三个都是接口
- List和Set继承自Collection接口,Map没有
- List实现类和Set实现类最大的区别是元素是否可以重复,List实现类一定是有序的集合,Set实现类有无序的也有有序的。
- 这里的有序和无序是相对于存取顺序来定义的,存入顺序等于取出顺序就认为是有序的,而无序的Set实现类其内部也是有自己定义的顺序,比如HashSet
- List主要有ArrayList、Vector和LinkedList三个常用实现类,区别在于
- ArrayList和Vector是基于数组实现的,LinkedList是基于双向链表实现的
- 只有Vector实现了线程安全
- 数组和双向链表的数据结构不同导致性能不同(后面会有例子)
- Set主要有HashSet、TreeSet、LinkedHashSet三个常用实现类,区别在于
- HashSet基于哈希表,TreeSet基于二叉树,LinkedHashSet继承自HashSet,是在哈希表的基础上又引入了双向链表
- 都没有实现线程安全
- 三种数据结构也会导致性能不同
- List的实现类都实现了List接口并继承自AbstractList抽象类,Set的实现类都实现了Set接口并继承自AbstractSet抽象类,所以他们各自有一套自己的同名CRUD操作
- Map接口没有继承自Collection接口是因为没有意义,接口的本质是一套代码规范,Map的本质是键值对,而Collection的本质是一组对象。
- Map主要有HashMap、TreeMap和LinkedHashMap三个常用实现类,区别在于
- HashMap基于哈希表,TreeMap基于二叉树,LinkedHashMap继承自HashMap,是在哈希表的基础上又引入了双向链表
- 都没有实现线程安全
- 三种数据结构也会导致性能不同
- Set的三个实现类和Map的三个实现类有差不多的性质和区别,是因为HashSet、TreeSet、LinkedHashSet的底层分别是HashMap、TreeMap、LinkedHashMap。
实测:性能测试
这里我们分别对同接口下的实现类进行单线程的性能测试
ArrayList、Vector、LinkedList
public class ListDemo {
public static void main(String[] args) {
// 一次只测试一个实现类
List<Integer> list = new ArrayList<>();
//List<Integer> list = new Vector<>();
//List<Integer> list = new LinkedList<>();
test(list);
}
/**
* 入口
*
* @param list
*/
public static void test(List<Integer> list) {
testAddTime(list, 1000000);
testIndexUpTime(list);
testForEachTime(list);
testIteratorTime(list);
testIndexDownTime(list);
testReadRandomTime(list);
testRandomDelTime(list);
testRandomAddTime(list);
testRandomSetTime(list);
}
/**
* 顺序添加测试
*
* @param list
*/
public static void testAddTime(List<Integer> list, int size) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
list.add(i);
}
long endTime = System.currentTimeMillis();
System.out.println(list.getClass() + "插入" + size + "个元素共耗时: " + (endTime - startTime) + "ms");
}
/**
* 迭代器遍历测试
*
* @param list
*/
public static void testIteratorTime(List<Integer> list) {
long startTime = System.currentTimeMillis();
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
Integer integer = it.next();
// 空遍历
}
long endTime = System.currentTimeMillis();
System.out.println(list.getClass() + "迭代器遍历耗时: " + (endTime - startTime) + "ms");
}
/**
* ForEach遍历测试
*
* @param list
*/
public static void testForEachTime(List<Integer> list) {
long startTime = System.currentTimeMillis();
for (Integer i : list) {
// 空遍历
}
long endTime = System.currentTimeMillis();
System.out.println(list.getClass() + "ForEach遍历耗时: " + (endTime - startTime) + "ms");
}
/**
* 索引递增遍历测试
*
* @param list
*/
public static void testIndexUpTime(List<Integer> list) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
// 空循环
}
long endTime = System.currentTimeMillis();
System.out.println(list.getClass() + "递增索引遍历耗时: " + (endTime - startTime) + "ms");
}
/**
* 索引递减遍历测试
*
* @param list
*/
public static void testIndexDownTime(List<Integer> list) {
long startTime = System.currentTimeMillis();
for (int i = list.size() - 1; i > 0; i--) {
// 空循环
}
long endTime = System.currentTimeMillis();
System.out.println(list.getClass() + "递减索引遍历耗时: " + (endTime - startTime) + "ms");
}
/**
* 随机读取测试
*
* @param list
*/
public static void testReadRandomTime(List<Integer> list) {
long startTime = System.currentTimeMillis();
Random random = new Random();
Integer integer = 0;
for (int i = 0; i < 10000; i++) {
int randomInt = random.nextInt(list.size());
integer = list.get(randomInt);
}
long endTime = System.currentTimeMillis();
System.out.println(list.getClass() + "随机读取一万次耗时: " + (endTime - startTime) + "ms");
}
/**
* 随机删除测试
*
* @param list
*/
public static void testRandomDelTime(List<Integer> list) {
long startTime = System.currentTimeMillis();
Random random = new Random();
for (int i = 0; i < 10000; i++) {
int randomInt = random.nextInt(list.size());
list.remove(randomInt);
}
long endTime = System.currentTimeMillis();
System.out.println(list.getClass() + "随机删除一万个元素耗时: " + (endTime - startTime) + "ms");
}
/**
* 随机插入对象
* @param list
*/
public static void testRandomAddTime(List<Integer> list){
long startTime = System.currentTimeMillis();
Random random = new Random();
for (int i = 0; i < 10000; i++) {
int randomInt = random.nextInt(list.size());
list.add(randomInt,randomInt);
}
long endTime = System.currentTimeMillis();
System.out.println(list.getClass() + "随机插入一万个元素耗时: " + (endTime - startTime) + "ms");
}
/**
* 随机更改对象
* @param list
*/
public static void testRandomSetTime(List<Integer> list){
long startTime = System.currentTimeMillis();
Random random = new Random();
for (int i = 0; i < 10000; i++) {
int randomInt = random.nextInt(list.size());
list.set(randomInt,randomInt);
}
long endTime = System.currentTimeMillis();
System.out.println(list.getClass() + "随机更改一万个元素耗时: " + (endTime - startTime) + "ms");
}
}
基于单一变量的测试方法,即我每次只改变一个变量的情况下(环境相同、代码相同、只有调用的实现类不同),我得到了下面三个结果
说实话,看到这个结果我有点惊讶,因为在这次正式测试之前,我前面的几篇文章的结论都是基于理论来考虑的,但这个测试结果出来后,LinkedList的性能在ArrayList面前居然毫无优势。而且这还是在我把数组集合的样本数调到了100万,一开始我直接上500万样本数的时候,在测试到LinkedList的随机操作的时候,我一度以为我的电脑卡了,半天没有出一个结果,连续测了3遍,直到我把样本数调到了100万,才看到随机操作的结果,之后在100万样本的情况下我又连续测试了5遍,得到了这个ArrayList完胜LinkedList的结果。
HashSet、TreeSet、LinkedHashSet
public class SetDemo {
public static void main(String[] args) {
// 随机类
Random random = new Random();
// 100万样本数
int size = 1000000;
// 一次只测试一个实现类
Set<Integer> set = new HashSet<>();
//Set<Integer> set = new TreeSet<>();
//Set<Integer> set = new LinkedHashSet<>();
// 顺序插入
long startTime = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
set.add(i);
}
long endTime = System.currentTimeMillis();
System.out.println(set.getClass() + "插入" + size + "个元素共耗时: " + (endTime - startTime) + "ms");
// 迭代器遍历
startTime = System.currentTimeMillis();
Iterator<Integer> it = set.iterator();
while (it.hasNext()) {
// 空遍历
Integer integer = it.next();
}
endTime = System.currentTimeMillis();
System.out.println(set.getClass() + "迭代器遍历共耗时: " + (endTime - startTime) + "ms");
// ForEach遍历
startTime = System.currentTimeMillis();
for (Integer i : set) {
//空循环
}
endTime = System.currentTimeMillis();
System.out.println(set.getClass() + "ForEach遍历共耗时: " + (endTime - startTime) + "ms");
// 随机更新(set不重复,插入小的就是更新)
startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
int randomInt = random.nextInt(set.size());
set.add(randomInt);
}
endTime = System.currentTimeMillis();
System.out.println(set.getClass() + "随机更新10000个元素共耗时: " + (endTime - startTime) + "ms");
// 随机删除
startTime = System.currentTimeMillis();
Integer integer = 0;
for (int i = 0; i < 10000; i++) {
int randomInt = random.nextInt(set.size());
set.remove(randomInt);
}
endTime = System.currentTimeMillis();
System.out.println(set.getClass() + "随机删除10000个元素共耗时: " + (endTime - startTime) + "ms");
}
}
结果还算符合之前几篇文章的推断,HashSet插入速度最快,LinkedHashSet的查找速度最快,而TreeSet是一种适中的选择。
HashMap、TreeMap、LinkedHashMap
public class MapDemo {
public static void main(String[] args) {
// 随机类
Random random = new Random();
String emptyString = "";
// 100万样本数
int size = 1000000;
// 一次只测试一个实现类
Map<String, String> map = new HashMap<>();
//Map<String, String> map = new TreeMap<>();
//Map<String, String> map = new LinkedHashMap<>();
// 顺序插入
long startTime = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
map.put(i + emptyString, i + emptyString);
}
long endTime = System.currentTimeMillis();
System.out.println(map.getClass() + "插入" + size + "个元素共耗时: " + (endTime - startTime) + "ms");
// 随机获取
startTime = System.currentTimeMillis();
String valueStr = "";
for (int i = 0; i < 10000; i++) {
int randomInt = random.nextInt(map.size());
valueStr = map.get(randomInt + emptyString);
}
endTime = System.currentTimeMillis();
System.out.println(map.getClass() + "随机读取一万次耗时: " + (endTime - startTime) + "ms");
// 随机删除
startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
int randomInt = random.nextInt(map.size());
map.remove(randomInt + emptyString);
}
endTime = System.currentTimeMillis();
System.out.println(map.getClass() + "随机删除一万个耗时: " + (endTime - startTime) + "ms");
// 转化keySet
startTime = System.currentTimeMillis();
Set<String> stringSet = map.keySet();
endTime = System.currentTimeMillis();
System.out.println(map.getClass() + "转化keySet耗时: " + (endTime - startTime) + "ms");
// 转化values
startTime = System.currentTimeMillis();
Collection<String> values = map.values();
endTime = System.currentTimeMillis();
System.out.println(map.getClass() + "转化values耗时: " + (endTime - startTime) + "ms");
}
}
似乎除了插入的效率TreeMap要略低一点,其他的都没什么太大的区别
小结
到这一步Java容器入门差不多就写完了,测试的代码其实还能再完善,比如加上内存量的查看,可以判断出不同的容器对资源消耗的情况,然后样本量也可以增大到300万或者500万,但我觉得其实没太有必要,我想应该没人会一次性取这么多对象放在一个容器里面,所以100万的样本量我觉得其实足够了,然后还有复杂对象作为容器元素也可以测试一下。这里我用的单一变量的原则来测试,所以不能在一篇文章里全部测完。
测试代码如下:github.com/MagicH666/J…