开发高性能JAVA应用程序基础(集合篇)
集合类在开发中使用非常频繁,使用时合理的选择对提高性能小有帮助。而且大部分面试都会有与集合相关的问题,例如ArrayList和LinkedList的对比。
了解API的集成与操作架构,才能了解何时该采用哪个类,而不会只能抄写范例。本文也尝试用一些现实生活中的物品来描述各个集合类的特性,仅仅是帮助快速理解和记忆,不必太过较真。
首先看类结构图:
按用途分为可重复收集对象的List、不可重复的Set和键值对应的Map三个顶层接口,对应三个最常用的实现类是ArrayList、HashSet和HashMap,如果没有其他的限制它们应该是默认的选择。
注意Vector、Hashtable和Stack上都有Legacy字样,如果不是为了兼容旧代码,不应再使用这三个类。
一、ArrayList对比LinkedList
很多文章介绍时直接说ArrayList查询快插入慢,LinkedList插入删除快,但是这个是有前提的。
代码为证:
- public static void main(String[] args) {
- List<Integer> array1 = new ArrayList<>(), array2 = new ArrayList<>();
- List<Integer> link1 = new LinkedList<>(), link2 = new LinkedList<>();
- for(int i = 0; i < 100000; i++) {
- array1.add(i); array2.add(i);
- link1.add(i); link2.add(i);
- }
- System.out.println("从0开始ArrayList消耗:" + getTime(array1, 0));
- System.out.println("从0开始LinkedList消耗:" + getTime(link1, 0));
- System.out.println("从50000开始ArrayList消耗:" + getTime(array2, 50000));
- System.out.println("从50000开始LinkedList消耗:" + getTime(link2, 50000));
- }
- public static long getTime(List<Integer> list, int index){
- long start = System.nanoTime();
- for(int i = 0; i< 100000; i++){
- list.add(index, i);
- }
- return TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS);
- }
- 从0开始ArrayList消耗:3701
- 从0开始LinkedList消耗:17
- 从50000开始ArrayList消耗:2370
- 从50000开始LinkedList消耗:13363
来分析:
ArrayList内部使用Object数组来保存收集的对象,数组在内存中是连续的线性空间。可以想象成一排紧紧排列的桌子,因为紧密连接距离较短,随机找到某个位置(索引)的桌子会比较快,但是如果想在中间某个位置插入一个新的桌子,那必须把排在后面的桌子一张一张向后移动,以空出一个位置。同样,如果删除一个桌子(怪异的说法),则需要把后面一张一张向前移动。如果是更换呢,很简单,搬走一张桌子,把新的放在原来的位置(快)。
LinkedList内部是双向链表结构,形象是一个用线串联的珠串,两个珠子之间的线可能非常长。如果想找到某一个位置的珠子,必须从头开始,沿着线一个接一个的向后找(查询慢)。如果想在两个珠子之间插入一个新的,那就很简单了,把中间的线拆开,新珠子两端的线分别接上。删除同样简单。
那么为什么测试结果中LinkedList第二次表现远远落后呢?原因是想插入首先必须先定位到位置,第二次测试选定从索引50000开始,LinkedList每次操作前都需要从0开始寻址到50000,查询消耗大量时间,所以实际执行很慢。
LinkedList可以当作Stack和Queue使用,这是ArrayList不具备的。
二、HashMap和TreeMap
HashMap内部使用一个名为Entry的内部类数组保存key-value对,使用无参构造方法的情况下, 创建一个长度为16(capacity)的数组。其中的16个存储位置通常称为哈希桶。当存储一个Entry对象时,首先通过key的hashCode()获得一个整形的散列码,和数组长度做取模计算获得位置索引。
那么一个明显的问题是,如果两个key经过上面的计算后得到一个相同的位置索引怎么办?这种情况称为哈希冲突,HashMap解决的办法是把新Entry和原来位置的Entry建立起链表,如果再有第三个相同index的key加进来,那么继续加在链表的前部。一个帮助记忆的形象是皇帝冠冕前的多列垂珠。
调用get(key)方法查找时,先通过hashCode()和取模计算获得第几个桶,再对桶上的链表遍历列表并通过key.equals()逐个比较来确定对象。
这样看来如果链表过长,也会影响查询速度,这时候就是负载因子(load factor)出场的时候了。当HashMap中已存入的对象数量超过capacity * load factor时,会对数组扩容,变为原来的两倍。
一些优化提示:
1 如果开发时已经预知HashMap要存入的对象数量,可以直接指定初始容量,避免频繁扩容
2 int和String非常适合当作key
3 如果key使用自己的对象,那么一个好的hashCode()算法非常重要,应该使对象尽可能均匀的分布在各哈希桶,同时应该覆盖equals方法。Effective Java书中对怎样实现一个像样的hashCode()给出了指导。
TreeMap基于红黑树结构实现,理论上来说各方面性能都比HashMap差,使用它的唯一理由就是排序。在使用keySet对TreeMap遍历时,按照key的compareTo方法排序输出。
三、HashSet、TreeSet、LinkedHashSet、LinkedHashMap
HashSet是最常用的Set类,内部借助HashMap实现,特性可以直接参考HashMap。
TreeSet内部借助于TreeMap实现,同理使用它的理由也是获得排序后的对象列表。
LinkedHashSet 与HashSet类似,区别是使用iterator遍历时,LinkedHashSet按照对象插入的顺序输出。理论上插入时性能比HashSet差。
LinkedHashMap 与HashMap类似,区别是遍历时,按照对象插入的顺序输出。LinkedHashMap有一个三个参数的构造方法:
- public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
如果第三个参数设置为true,排序方式为按照访问顺序排序,可以借助该功能实现简单的采用"最近最少使用"失效算法(LRU)的缓存。
四、线程安全
上面提到的类中,除了Vector和Hashtable,全部都是线程不安全的。
示例1:
- public static void main(String[] args) {
- ArrayList<Integer> list = new ArrayList<>();
- Thread t1 = new Thread() {
- public void run() {
- while(true) {
- list.add(1);
- }
- }
- };
- Thread t2 = new Thread() {
- public void run() {
- while(true) {
- list.add(2);
- }
- }
- };
- t1.start();
- t2.start();
- }
执行结果:
- Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 549
- at java.util.ArrayList.add(ArrayList.java:444)
- at Program$2.run(Program.java:28)
分析:ArrayList的add方法演示代码
- public void add(Object o) {
- if(next == list.length) {
- list = Arrays.copyOf(list, list.length * 2);
- }
- list[next++] = o;
- }
ArrayList在添加对象时先判断数组是否已满,如果已满则扩容。多线程状态下,当next ==list.length-1时,两个线程轮流切换执行都不符合扩容条件进入下一步,此时第一个线程执行赋值并把next+1,第二个线程执行时next=list.length,出现ArrayIndexOutOfBoundsException。
示例2:
- public class ArrayListDemo implements Runnable {
- static ArrayList<Integer> list = new ArrayList<>();
- static CountDownLatch latch = new CountDownLatch(10000);
- public static void main(String[] args) throws InterruptedException {
- ExecutorService exec = Executors.newCachedThreadPool();
- for(int i = 0; i < 10000; i++) {
- exec.execute(new ArrayListDemo());
- }
- latch.await();
- System.out.println("list.size()=" + list.size());
- }
- @Override
- public void run() {
- list.add(1);
- latch.countDown();
- }
- }
执行结果:
- list.size()=9977
示例3:
- public static void main(String[] args) throws InterruptedException {
- HashMap<String, String> map = new HashMap<>();
- map.put("a", "a");
- Iterator<String> iter = map.keySet().iterator();
- ExecutorService exec = Executors.newCachedThreadPool();
- exec.execute(new Runnable() {
- @Override
- public void run() {
- map.put("b", "b");
- }
- });
- exec.shutdown();
- while(iter.hasNext()) {
- System.out.println(iter.next());
- }
- }
执行结果:
- Exception in thread "main" java.util.ConcurrentModificationException
- at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
- at java.util.HashMap$KeyIterator.next(HashMap.java:1453)
- at ArrayListDemo.main(ArrayListDemo.java:34)
多线程下安全读写集合类有三种常见办法:
1 JDK5以后首选concurrent包下的集合类,包括ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet
2 读写操作时加锁,使用synchronized关键字或者java.util.concurrent.locks下的类
3 使用Collections.synchronizedList、Collections.synchronizedMap等方法获得线程安全集合。
开发高性能JAVA应用程序基础(集合篇)的更多相关文章
- 开发高性能JAVA应用程序基础(内存篇)
虽然Java的垃圾回收和当前高配置的服务器可以让程序员大部分时间忘掉OutOfMemoryError的存在,但是访问量增大后频繁的GC会额外消耗CPU (使用top查看结果为us值高),系统响应速度下 ...
- 【JAVA秒会技术之秒杀面试官】秒杀Java面试官——集合篇(一)
[JAVA秒会技术之秒杀面试官]秒杀Java面试官——集合篇(一) [JAVA秒会技术之秒杀面试官]JavaEE常见面试题(三) http://blog.csdn.net/qq296398300/ar ...
- Java面试专题-集合篇(2)
- JAVA基础整理-集合篇(一)
集合作为JAVA的基础知识,本来感觉自己理解的很清楚了,但是在最近的一次面试中还是答得不尽如人意!再次做一下整理,以便加深理解以及随时查阅. 首先,java.util包中三个重要的接口及特点:List ...
- java基础之集合篇
(一) 集合类位于JDK中java.util包中. 常用的集合类型分布在java.util.Collection和java.util.Map接口中. java.util.Collection=> ...
- [ Java面试题 ] 集合篇
1.ArrayList和Vector的区别 这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序集合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的 ...
- Java基础之集合篇(模块记忆、精要分析)
千里之行,始于足下.把别人的变成自己,再把自己的分享给别人,这也是一次提升的过程.本文的目的是以一篇文章从整体把握集合体系又不失一些细节上的实现,高手路过. 集合的作用与特点 Java是一门面向对象语 ...
- 【java面试】- 集合篇
Java 集合概览 从下图可以看出,在Java中除了以Map结尾的类之外, 其他类都实现了Collection接口.并且,以Map结尾的类都实现了Map接口 List.Set.Map三者的区别 Lis ...
- 第一篇:微信公众平台开发实战Java版之了解微信公众平台基础知识以及资料准备
相信很多人或多或少听说了微信公众平台的火热.但是开发还是有一点门槛,鉴于挺多朋友问我怎么开发,问多了,自己平时也进行以下总结.所以下面给大家分享一下我的经验: 微信公众号是什么? 官网的介绍:再小的个 ...
随机推荐
- 将IDEA maven项目中src源代码下的xml等资源文件编译进classes文件夹
如果使用的是Eclipse,Eclipse的src目录下的xml等资源文件在编译的时候会自动打包进输出到classes文件夹.Hibernate和Spring有时会将配置文件放置在src目录下,编译后 ...
- IDEA引MAVEN项目jar包依赖导入问题解决
Intellj 自动载入Mave依赖的功能很好用,但有时候会碰到问题,导致pom文件修改却没有触发自动重新载入的动作,此时需要手动强制更新依赖. 如下: 1手动删除Project Settings里面 ...
- spring boot oauth2的一些记录
oauth2及时从一个项目A申请另一个项目B的访问的时候,不用在项目A输入项目B的用户名和密码,个人理解先跳转到项目B,利用项目B的用户名和密码得到一个code之类的,这里有点像openID,不过不是 ...
- iOS 用户密码 数字字母特殊符号设置 判断
//直接调用这个方法就行 -(int)checkIsHaveNumAndLetter:(NSString*)password{ //数字条件 NSRegularExpression *tNumRegu ...
- APP上传APP Store遇到的各种问题
内容含敏感话题或对苹果不友好的信息(如苹果婊) 使用了友盟的统计SDK,获取了IDFA但是上传填写无广告 采用友盟IDFA的sdk,并用友盟的默认淘宝页面广告,被告知和产品内容不符(最近) App在i ...
- SpringMVC 返回json的两种方式
前后台数据交互使用json是一种很重要的方式.本文主要探讨SpringMVC框架使用json传输的技术. 请注意,本文所提到的项目使用Spring 版本是4.1.7,其他版本在具体使用上可能有不一样的 ...
- JavaWeb之数据源连接池(3)---Tomcat
此文续 <JavaWeb之数据源连接池(2)---C3P0>. Apache Tomcat作为一款JavaWeb服务器,内置了DBCP数据源连接池.在使用中,只要进行相应配置即可. 首先, ...
- 如何安装mysql
如何安装mysql对于初学者来说的确是很麻烦,首先要知道安装mysql仅仅只是安装一个mysql系统,是没有任何可视化操作界面的,所以还要安装一个mysql的管理工具,这是初学者容易蒙的地方之一. m ...
- ArcGIS 网络分析[1.4] 制作点线要素时需要注意的地方
有很多同学虽然成功做好了网络数据集,但是分析时会出现这样的问题: 这是为什么呢? 这有三个可能的原因: 1. 两个点之间所有的道路没有连通(问题出在点.线数据上). 2. 网络数据集出现了孤立的点位置 ...
- BZOJ 4816 数字表格
首先是惯例的吐槽.SDOI题目名称是一个循环,题目内容也是一个循环,基本上过几年就把之前的题目换成另一个名字出出来,喜大普奔亦可赛艇.学长说考SDOI可以考出联赛分数,%%%. 下面放解题报告.并不喜 ...