继续上一篇浅谈java类集框架和数据结构(1)的内容

上一篇博文简介了java类集框架几大常见集合框架,这一篇博文主要分析一些接口特性以及性能优化。

一:List接口

List是最常见的数据结构了,主要有最重要的三种实现:ArrayList,Vector,LinkedList,三种List均来自AbstracList的实现,而AbstracList直接实现了List接口,并拓展自AbstractCollection。

在三种实现中,ArrayList和Vector使用了数组实现,可以认为这两个是封装了对内部数组的操作,操作这两个List等价于对内部对象数据的操作,这两个采用了几乎相同的算法,唯一的区别就是对多线程的支持,ArrayList没有对任何一个方法作线程同步,因此不是线程安全的,从理论上来说ArrayList性能好一些,但是实际相差不是非常明显。

LinkedList使用了循环双向链表数据结构,这跟List是截然不同的使用场景,下面我对比ArrayList跟LinkedList一些操作区别;

1.增加元素到列表尾端

ArrayList的add()方法性能取决于ensureCapacityInternal()方法,实现如下:

可以看到,只要ArrayList当前容量足够大,add()操作效率非常高,如果容量需求超过当前数组大小,就会扩容,扩容产生大量的数组复制操作,而数组复制时最终调用System.arraycopy()方法,所以add()操作效率还是相当高的。

而LinkedList  add()调用如下:

典型的链表结构,所以不用考虑容量的问题,这点比ArrayList有一定的性能优势,然而源码里很清楚,每次都要new新的Node对象,并进行更多的赋值操作,频繁的系统调用中,对性能会产生一定的影响,所以各有利弊。

分别用ArrayList和LinkedList测试循环50万次add()方法(在加大堆大小环境下 比如-Xmx512M -Xms512M) ,使用-Xmx512M -Xms512M目的是屏蔽GC对程序执行速度测量的干扰,最后结果是ArrayList相对耗时16ms,LinkedList31ms,,可见,不间断的生成新的对象还是占用了一定的系统资源,而因为数组的连续性,因此总是在尾端增加元素时,只有在空间不足时才会扩容和数组复制,所以绝大部分情况追加操作效率都很高。

如果测试使用默认JVM的堆大小,差别会更大,使用LinkedList对堆内存和GC要求更高。

2.随机访问元素(RandomAccess接口)

来看一下两种List的构造

LinkedList:

ArrayList:

可以看到ArrayList实现了RandomAccess接口而LinkedList没有实现此接口,那么RandomAccess接口有什么作用呢?

此接口的好处是可以在应用程序中知道正在处理的List对象是否可以快速随机访问,从而针对不同的list进行不同的操作,以提高程序的性能。

同样执行这段代码,LinkedList耗时16140ms,而ArrayList耗时32ms,就随机访问元素的相对速度而言,两者差了几个数量级,进一步可以通过LinkedList的get()方法实现来看一下:

显而易见是个双向循环链表的二分查找,虽然二分可以减少时间,但是遍历过程还是消耗了大量的cpu时间,这相比ArrayList直接操作数组的下标

查找,性能差远了。

另外稍微提一下,(1)foreach比直接用迭代器性能差一点点,因为foreach底层同样是迭代器但是多了一步赋值的操作。

(2)集合的构造也最好使用有预估容量的方式,这样可以避免频繁的扩容,复制数据,跟上一篇StringBuffer的内容同理。

(3)ArrayList比LinkedList随机访问强大,但是增加元素和删除元素稍弱,两者使用场景不同。

二:Map和Set(主要针对hashcode的优劣和红黑二叉树跟Linked排序区别)

在上一篇讲类集框架的博客里我已经介绍了HashMap等,什么同步不同步,数据结构我就不重复讲了,这篇主要细节分析一下hashcode等东西。

HashMap,HashTable和Collections.synchronizedMap()三者性能差异不大,同样执行10万次get()方法,相对耗时很接近,可以认为三者并无明显差异,下面我只举例HashMap。

HashMap的高性能要保证以下几点:

1.hash算法必须高效

2.hash值到内存地址(数组索引)的算法是快速的

3.根据内存地址(数组索引)可以直接取得对应的值

从第一个说起,hash算法,来看一下HashMap的hash,首先是基于位运算的,所以是高效的

Object的hashcode()是native的

native是什么意思呢,native比一般的方法快,因为它直接调用操作系统本地连接库的API,由于hashcode()方法是可以重载的,因此为了保证hashmap的性能,需要确保你重载的hashcode方法是高效的,所以为什么很多开发选择HashMap的泛型是String,Integer之类的,因为这些lang包的都重载实现了很好的hashcode(),这比你自己写的大部分算法都要牛逼稳定。

第二个问题,hash冲突,此类型的第一篇博文我说了hash冲突是挂链表解决,HashMap实际上是一个链表的数组,每一个Entry是一个链表,有key value next hash,有冲突时,新的entry的next就会指向oldValue,这就实现了在一个数组索引空间存放多个值项。

上面是JDK8的put()方法源码,我发现跟之前的版本有些区别了,不过原理一样,都是上面说的一个数组索引空间放多个值,然后next指向oldValue。

所以基于这种hashcode()和hash()方法的实现方法只要足够好,能够尽可能的减少冲突的产生,那么对HashMap的操作几乎等价于操作数组,随机访问几乎等于随机访问数组,这个效率就跟ArrayList一样强大,性能非常不错,但是如果你覆写的很垃圾,那么你的HashMap就退化成了几个链表,遍历HashMap等于遍历链表,你懂的,上面我已经写过了,这个随机访问的性能是非常垃圾的。

第三点就是容量参数的问题了,HashMap内部维护了一个threshold变量,它始终被定义为当前数组总容量和负载因子的乘积,它表示HashMap的阀值,当实际容量超过阀值就会扩容。上面也提了,尽量给一个容量参数,初始大小和负载因子设置合理的话,可以有效减少HashMap扩容的次数。

接着讲一下LinkedHashMap-有序的HashMap

HashMap是无序的,如果希望元素保存输入时的顺序,就需要使用LinkedHashMap,它保留了HashMap的高性能(当然,建立在良好的hashcode()方法实现上),在HashMap基础上又内部增加了一个链表,用以存放元素的顺序,所以它可以理解为一个维护了元素次序表的HashMap。

源码显示它通过继承entry,多维护了一个before和after属性来记录某一表项的前驱和后继,并构成循环链表。

需要注意的是不要在迭代器模式中使用LinkedHashMap的get()操作,这个特性适用于所有的集合类,get()方法会改变链表结构,迭代器模式中不允许修改被迭代的集合。

虽然LinkedHashMap可以排序,但是排序只是根据顺序,而不是元素本身的排序,而元素本身的排序就需要用到红黑二叉树实现的TreeMap;

TreeMap实现了SortedMap接口,性能略低于hashmap,差不多25%的性能。

TreeMap由Comparator和Comparable确定元素的固有顺序,红黑二叉树是一种平衡查找树,统计性能高于平衡二叉树,具有良好的最坏情况运行时间,可以在O(log n)时间内做查找,插入和算法较复杂,这里不赘述,大家可以自己查阅有关资料。

 public class Student implements Comparable<Student> {
private String name;
private int age; public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} public Student() {
super();
} public Student(String name, int age) {
super();
this.name = name;
this.age = age;
} @Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
} @Override
public int compareTo(Student o) {
if (o.age < this.age)
return 1;
else if (o.age > this.age)
return -1;
return 0;
} }

这段代码就是展示TreeMap如何通过简易的接口实现对有序的key集合进行筛选,结果集也是个有序的map,性能相当不错,而且实现简单,这比自己实现的排序算法减少了开发成本,自己写的算法耗时耗力还可能成为性能的瓶颈。

下面说一下Set,其实Set的底层就是HashMap,一切操作都是操作HashMap对象实现,一个没有什么意义的Object对象作为map对象的value,源码很明显:

也分为TreeSet,HashSet,LinkedHashSet等,不赘述前面提到的。

三:Collection遍历的小细节

关于整个Collection我说一点有意思的,就是在循环访问的时候,有一些细节的性能优化:

 public class Testmain {

     public static void main(String[] args) {
Collection collection = new ArrayList();
for (int i = 0; i < collection.size(); i++) {
System.out.println(((ArrayList) collection).get(i));
}
} }

你比如这段代码,是常见的for循环,可以优化的地方如下:

 public class Testmain {

     public static void main(String[] args) {
Collection collection = new ArrayList();
int num=collection.size();
for (int i = 0; i < num; i++) {
System.out.println(((ArrayList) collection).get(i));
}
} }

经过修改,size()方法只被调用一次,而不会循环调用,循环体中所有的类似方法都应该这么处理,而且元素数量越多,这种处理越有意义,在ORM中也是同样原理,能丢个list绝不for循环N次。

还有一种优化,是省略重复操作,比如:

 public class Testmain {

     public static void main(String[] args) {
ArrayList collection = new ArrayList();
collection.add("abc");
collection.add("abcddd");
collection.add("abcddd40");
collection.add("acddd");
int count = 0;
int num = collection.size();
for (int i = 0; i < num; i++) {
if ((((String) collection.get(i)).indexOf("abc") != -1)
|| (((String) collection.get(i)).indexOf("efg") != -1)
|| (((String) collection.get(i)).indexOf("aaa") != -1))
count++;
}
} }

比如这个代码重复了三行

(((String) collection.get(i)).indexOf()
其实get(i)的值是一样的,那么可以优化为:
 public class Testmain {

     public static void main(String[] args) {
ArrayList collection = new ArrayList();
String s = null;
collection.add("abc");
collection.add("abcddd");
collection.add("abcddd40");
collection.add("acddd");
int count = 0;
int num = collection.size();
for (int i = 0; i < num; i++) {
if (((s = (String) collection.get(i)).indexOf("abc") != -1)
|| (s.indexOf("abc") != -1)
|| (s.indexOf("abc") != -1))
count++;
}
} }

通过三段代码(最初的版本,优化size版本,优化get(i)版本)的执行时间,System.nanoTime()统计,分别为:46654ns,13968ns,11454ns。都是纳秒单位,说明这种处理方式是有意义的。

还有一点有意思的,减少方法的调用,方法的调用是要消耗系统堆栈的,虽然面向对象设计模式和模块化组件设计方法鼓励程序员使用若干个小方法代替一个大方法,也就是封装抽象呗,但这是牺牲性能为代价的,

不过现代语言这些得到了很大的优化,当然我们无法追求极致的性能,所以只在有意思的地方玩玩,比如如果上段代码是Vector子类的代码,可以改写为:

 public class VectorSon extends Vector<String> {
public VectorSon(int count) {
count = this.elementCount;
} }
 public class Testmain {

     public static void main(String[] args) {
VectorSon collection = new VectorSon();
String s = null;
collection.add("abc");
collection.add("abcddd");
collection.add("abcddd40");
collection.add("acddd");
int count = 0;
int num = this.elementCount;
for (int i = 0; i < num; i++) {
if (((s = (String) elementData[i]).indexOf("abc") != -1)
|| (s.indexOf("abc") != -1)
|| (s.indexOf("abc") != -1))
count++;
}
} }

我省略了很多代码,只是大概表述一下实现方式,就是说Vector子类可以直接拿到底层数组的属性,直接拿到属性就不需要调用get等方法,数组下标就可以访问到了。

直接操作对象属性会比方法效率高,size(),get()等方法,如果改为直接操作对象属性,数组的操作,那么还是有性能的提升,大概提升40%的性能,不过这种机会很少,在java里很少,java封装的太厉害了,大部分程序员不会去写太底层的东西。

集合框架的东西暂时就到这里了,最近搞一搞python跟Hystrix熔断器架构,下一篇博客打算发Hikari连接池整合JPA还有Spring boot。

浅谈java类集框架和数据结构(2)的更多相关文章

  1. 浅谈java类集框架和数据结构(1)

    在另外一篇博客我简单介绍了java类集框架相关代码和理论. 这一篇博客我主要分析一下各个类集框架的原理以及源码分析. 一:先谈谈LinkedList 这是LinkedList源码的开头,我们能看到几点 ...

  2. 浅谈Java的集合框架

    浅谈Java的集合框架 一.    初识集合 重所周知,Java有四大集合框架群,Set.List.Queue和Map.四种集合的关注点不同,Set 关注事物的唯一性,List 关注事物的索引列表,Q ...

  3. 专题笔记--Java 类集框架

    Java 类集框架 1. Java类集框架产生的原因 在基础的应用中,我们可以通过数组来保存一组对象或者基本数据,但数组的大小是不可更改的,因此出于灵活性的考虑和对空间价值的担忧,我们可以使用链表来实 ...

  4. Java类集框架详细汇总-底层分析

    前言: Java的类集框架比较多,也十分重要,在这里给出图解,可以理解为相应的继承关系,也可以当作重要知识点回顾: Collection集合接口 继承自:Iterable public interfa ...

  5. Java类集框架——List接口

    学习目标 掌握List接口与Collection接口的关系. 掌握List接口的常用子类:ArrayList.Vector. 掌握ArrayList与Vector类的区别.    Collection ...

  6. 浅谈Java反射与框架

    Java反射 1.示例 1.用户类 package com.lf.entity; import com.lf.annotation.SetProperty; import com.lf.annotat ...

  7. java类集框架(ArrayList,LinkedList,Vector区别)

    主要分两个接口:collection和Map 主要分三类:集合(set).列表(List).映射(Map)1.集合:没有重复对象,没有特定排序方式2.列表:对象按索引位置排序,可以有重复对象3.映射: ...

  8. java:类集框架conllection接口list,set

    类集中提供了以下几种接口: 1.单值操作接口:conllection,List,Set list和set是conllection接口的子接口 2.一对值的操作接口:Map 3.排序的操作接口:Sort ...

  9. 浅谈Java类中的变量初始化顺序

    一.变量与构造器的初始化顺序 我们知道一个类中具有类变量.类方法和构造器(方法中的局部变量不讨论,他们是在方法调用时才被初始化),当我们初始化创建一个类对象时,其初始化的顺序为:先初始化类变量,再执行 ...

随机推荐

  1. 《浅谈架构之路:单点登录 SSO》

    前言:SSO 单点登录 “半吊子”的全栈工程师又来了,技术类的文章才发表了两篇,本来想先将主攻的几个系列都开个头(Nodejs.Java.前端.架构.全栈等等),无奈博客起步太晚,写博文的时间又没有很 ...

  2. 树莓派上搭建arduino开发环境

    -------------还是博客园上面的格式看这舒服,不去新浪了------------- 为什么要在树莓派上开发arduino呢?总要把树莓派用起来嘛,不然老吃灰. 树莓派使用SSH时没有图形界面 ...

  3. Redis的二八定律

    常用命令: 1.setex key 有效时间 value ----------意思就是添加并设置该键值对的存活时间 2.mset key1 value1 key2 value2 key3 value3 ...

  4. java学习书籍推荐

    1. Java 语言基础 谈到Java 语言基础学习的书籍,大家肯定会推荐Bruce Eckel 的<Thinking in Java >.它是一本写的相当深刻的技术书籍,Java 语言基 ...

  5. <input/>标签在 苹果浏览器中 默认的有圆角

    解决方法: input{ border-radius:0; -webkit-border-radius:0; }

  6. BZOJ 3403: [Usaco2009 Open]Cow Line 直线上的牛(模拟)

    直接双端队列模拟,完了= = CODE: #include<cstdio>#include<algorithm>#include<iostream>#include ...

  7. 极光推送-Java后台实现方式一:Http API

    Java后台实现极光推送有两种方式,一种是使用极光推送官方提供的推送请求API:https://api.jpush.cn/v3/push,另一种则是使用官方提供的第三方Java APIjar包,这里先 ...

  8. Java实现OOP(面向对象编程)

    一.对象的综述 面向对象编程(OOP)具有多方面的吸引力.对管理人员,它实现了更快和更廉价的开发与维护过程.对分析与设计人员,建模处理变得更加简单,能生成清晰.易于维护的设计方案.对程序员,对象模型显 ...

  9. rsyslog+mariadb+loganalyzer实现日志服务器搭建

    rsyslog+mariadb+loganalyzer实现日志服务器搭建 一.概述 Linux的日志记录了用户在系统上一切操作,包括系统自身运作产生的日志,这些日志是应使用者了解服务器的情况最好的资料 ...

  10. ucGUI的学习小结

    前言 做一个小项目时需要实现GUI及相关操作(响应按键).用的SoC的优点是功耗低,但是受限于硬件能力,之前的SDK里并没有对GUI有很好的支持.后面对GUI的界面外观还有一定的要求,就在网上搜了一下 ...