`

ArrayList--源码分析之理论结合实践

阅读更多

引子

 

最近在从事一些基于大数据做 “应用级产品”的架构设计和开发工作,比如 实时流量监控、实时热力图、注意力热图等。发现一些基础类的正确使用,对性能提升还是挺大的。记录下来,时刻提醒自己一定要开发中的注意细节。

 

“万丈高楼平地起”,在软件开发中如果不注重细节,会带来很大的性能问题,尤其是在大数据相关产品的开发中。本系列中会结合开发中遇到的实际情况,结合源码进行分析。首先来看看我们用得很多的ArrayList。

 

一个典型的场景:批量向hbase写入数据(数据量大的情况下尽量避免一条一条的插入)。一般的写法为:

//在大数据场景下,该方法会被循环调用

 

public void insert(List<DataObjet> datas){
…………省略代码…….
List<put> puts = new ArrayList<put>();
Put put = null;
For(DataObjet data; datas){//一般为for循环一个list的对象,大小为几十到几百不等
Put = new Put(data.getId().getBytes()); //设置rowkey
Put.add(xx,xx,xx); //添加每个column
…………省略代码…….
puts.add(put); //放入list
}
table.setAutoFlush(false);//table为habse表,关闭批量提交
…………省略代码…….
table.put(puts);
table.flushCommits();
}
 

 

咋一看没啥问题,但是在插入十万,百万,乃至亿级别的数据时,其实还是有优化空间的。

想必大家已经看出来,问题出现在这行List<put> puts = new ArrayList<put>(),没有指定数据大小。

 

ArrayList的三个构造方法

 

一起来重新认识下ArrayList(jdk1.8):简单的说 是一个动态数组,其容量可可以自动增长的数组。

先看下三个构造方法:

 

1、默认构造方法:

 

/**
     * 初始化一个空数组,默认容量是10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
   // 空数组
   private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
   //默认容量大小
   private static final int DEFAULT_CAPACITY = 10;
 

 

 

2、指定容量构造方法:

 

    /**
     * 指定容量大小
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        }
    }
 
 3、通过Collection创建:

 

public ArrayList(Collection<? extends E> c) {
    //先转换为数组
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
}

 

入参只要是实现了Collection接口的子孙类型(set、list),都可以作为参数。

我们经常用的是第一个无参默认构造方法。

 

再来看添加方法:

 

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 确保数组容量
        elementData[size++] = e;
        return true;
}
 
/**
* minCapacity 需要的最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
//这个好理解,如果不够10,minCapacity改为10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //根据情况改变数组长度,可变数组的核心方法
        ensureExplicitCapacity(minCapacity);
}
 
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
 
        // 如果需要的最小容量超出数据长度,需要对数组进行扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
 
private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
// 扩容1.5倍、或者1.5倍减1。这里采用的位运算比老版本性能好些
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;//好理解
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 最终调用System.arraycopy方法,把老数组中的数据copy到新数组
        elementData = Arrays.copyOf(elementData, newCapacity);
}
 

 

 

Grow()方法

第一步:先按照老容量进行扩容,这个地方相对jdk1.6有改进,jdk1.6是按照1.5倍+1进行扩容(int newCapacity = (oldCapacity * 3)/2 + 1)。有没有注意到这个细节,老版本是用的除法,jdk1.8用的是位运算,性能肯定好些。看来大神们都很重视每一个细节。

 

第二步:判断扩容后的容量是否够用,并判断是否超过最大容量(一般会出现这种情况吧)。

 

第三步:调用System.arraycopy方法,把老数组中的数据copy到扩容后的新数组。

 

可以看到默认构造函数容量是10,在大量数据需要插入的情况下(比如批量插入hbase,假如一次批量是500),会进行多次自动扩容,以及多次数组copy。

平时数据量小,体现不出来。在大数据开发中,一次动则插入千万条记录。整体的性能消耗是非常恐怖的。

所以在能明确确认数组大小的情况下,请使用确定的容量进行ArrayList的创建,即第二构造函数。开篇场景中的代码可以改为:

 

public void insert(List<DataObjet> datas){
…………省略代码…….
List<put> puts = new ArrayList<put>(datas.size());//非空判断没写出来
…………省略代码…….
}

 

 

做个简单测试:

        List list = null;
        long s = System.currentTimeMillis();
        for(int j=0;j<100000;j++){
            list = new ArrayList<>();
            //list = new ArrayList<>(500);
            for (int i=0;i<500;i++){
                list.add(1);
            }
        }
        long e = System.currentTimeMillis();
        System.out.println(e-s);

 

直接运行结果大约是500+ms, 注释代码交互后运行结果大约是200+ms。可以看到提升还是挺多的。

 

说了这么多,就只改动了一个地方。其实就想表达,一定要注意细节。在平常开发,直接使用默认的构造函数,也许差别不大。比如你的容量基本都不会超过10,那就直接用默认构造函数也是最高效的做法。

 

subList()方法

 

ArrayList是实现了Serializable接口的,也就是说可以进行序列化和反序列化,可以在rpc框架()中进行传输。我们经常用这个方法对ArrayList进行截取,但这个截取后的List是不能在某些rpc框架中进行传输的(如:淘宝dubbo,京东jsf),主要原因就是这个List没有实现Serializable接口。看代码:

public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size); //是否越界判断
        return new SubList(this, 0, fromIndex, toIndex);
    }

 

       

其实是新建了一个Sublist,是ArrayList的私有类:

private class SubList extends AbstractList<E> implements RandomAccess

可以返现该类是没有实现Serializable接口接口的。

 

另外 也可以使用ArrayList的第三个构造方法,把Sublist转换成一个ArrayList。这样就可以在rpc框架中传输了。

      List list = new ArrayList<>();
              list.add(1);
              list.add(2);
              list.add(3);
              List slist = list.subList(0,1);
              List newList = new ArrayList<>(slist);

 

 

 

迭代子模式

 

在ArrayList里我们还可以学习下,23种常用设计模式中的迭代子模式(关于迭代子模式可以看下这篇文章:http://www.cnblogs.com/java-my-life/archive/2012/05/22/2511506.html) ,

ArrayList的iterator()方法实际上会生成一个ListItr类的实例对象。ListItr类继承至Itr类,Itr是AbstractList类的内部私有类,可以看出该类是AbstractList类的“内禀迭代子”。

只要是继承了AbstractList抽象类,都可以获得这个迭代子。

 

另外jdk1.8里,ArrayList新增了一个迭代子:ArrayListSpliterator,该类继承自Spliterator。后面抽时间在单独分析下Spliterator的实现类。

 

总结下:

1、大数据的场景下,说明了使用确定容量的ArrayList的重要性。

2、ArrayList是能序列化的,但是的sublist()生成的list却不能序列化。

3、迭代子模式在ArrayList中的使用。

 

就啰嗦这么多,ArrayList其他方法这里不再细讲。很多博客已经将的比较详细,感兴趣的同学可以把源码打开看看,还可以看下这篇文章(jdk1.8以前的版本):http://www.cnblogs.com/ITtangtang/p/3948555.html

1
1
分享到:
评论

相关推荐

    java-basics-源码.rar

    Java基础源码解析 在Java编程领域,理解基础至关重要,因为它是进阶学习和实际开发的基石。...实践是最好的老师,对照源码动手操作,将理论知识与实际应用相结合,对于成为一名优秀的Java开发者至关重要。

    南师范大学电子信息类-通信工程专业-课程设计-综合实验设计代码-Java-内含源码和说明书(可自己修改).zip

    9. **源码和说明书**:压缩包中包含的源码可以让学生分析和学习已有的实现,说明书则可能包含设计目标、步骤、实现细节和使用说明,帮助学生理解项目的全貌。 10. **版本控制**:虽然未明确提及,但良好的编程习惯...

    java案例-50_JAVA源码_

    Java是一种广泛使用的面向对象的编程语言,以其跨平台性、高效性和丰富的类库而闻名。在"java案例-50_JAVA源码_"这个压缩包中,包含了50个Java编程的...记得在实践中不断思考,理论结合实际,才能更好地吸收这些知识。

    Java数据结构和算法中文第二版-附源码

    3. **源码分析**: - 书中附带的源码能帮助读者更直观地理解数据结构和算法的实现过程,通过阅读和调试代码,可以提高对Java编程和算法实现的理解。 4. **实际应用**: - 学习如何在实际项目中选择合适的数据结构...

    JAVA实战项目源码-计算机毕业设计java专业-项目源码-项目说明介绍-java+applet-家庭理财系统

    《JAVA实战项目源码-家庭理财系统》是一个典型的JAVA编程语言实现的计算机毕业设计项目,旨在帮助学生理解和应用JAVA技术解决实际问题。该项目利用JAVA的强大力量,结合APPLET技术,构建了一个全面的家庭财务管理...

    Java核心技术结合源码验证

    源码分析可以帮助你理解它们的内部工作原理,如扩容策略、查找和插入效率等。 4. **多线程**:Java提供了丰富的API来支持多线程编程,如Thread类和Runnable接口。通过源码,你可以学习如何创建和管理线程,以及同步...

    mi-yi-collection-bean-master_java_源码

    在阅读和学习"mi-yi-collection-bean-master_java_源码"时,你需要理解这些概念,并通过源代码分析作者如何将理论应用于实践。这不仅可以加深对Java集合和Bean类的理解,还能提升你的编程技巧和项目开发能力。同时,...

    JAVA基础项目视频+源码

    总的来说,这个"JAVA基础项目视频+源码"资源为初学者提供了一个实践与理论相结合的学习环境,通过观看视频教程并亲手操作源码,可以有效地提升Java编程技能。在学习过程中,建议逐步理解和实践每个知识点,同时不断...

    java多线程并发实战和源码

    Java多线程并发实战与源码分析是Java开发中至关重要的一部分,它涉及到程序性能优化、系统资源高效利用以及复杂逻辑的正确同步。本书主要聚焦于Java多线程的基础理论和实际应用,虽然书中实例和源码相对较少,但仍然...

    9.9C#实例源码200个.zip

    C#是一种广泛应用于软件开发的面向对象的编程语言,由微软公司推出,主要用于.NET框架。这个9.9C#实例源码200个.zip...记得,理论结合实践是最好的学习方法,尝试理解和修改这些代码,将有助于你成为更熟练的C#开发者。

    TIJ4-code-master.zip及相关jar包、练习题答案

    2. **源码分析** "TIJ4-code-master"目录下的源代码是配合书本内容编写的实例,可以帮助读者更好地理解理论知识。这些代码涵盖了大量的设计模式和实际应用,比如工厂模式、单例模式、观察者模式等,以及并发编程的...

    E4A易安卓视频教程源码: - 06、数组和集合.rar

    在实践中,你可以参考这个视频教程的源码,动手编写自己的程序,遇到问题时,对照源码分析解决问题的方法。这样不仅能加深对数组和集合的理解,还能提升编程技能。记住,理论与实践相结合,是学习编程最有效的方式。...

    Head First Java 所有源码

    《Head First Java》是一本非常受欢迎的Java编程入门书籍,以其独特的教学方式和丰富的...记住,理论知识与实践相结合是成为优秀程序员的关键。所以,不要只是阅读,一定要动手实践,让代码成为你思考和表达的工具。

    java源码学习比较java源码学习比较

    比较不同学习方法,我们发现,结合理论与实践是最有效的。理论学习可以提供框架和概念,实践则能帮助巩固理解和应用。比如,对比分析多个并发模型的实现(如线程池、FutureTask和CompletableFuture),既能理解并发...

    Java实用编程源码50例-迅速提高编程技巧-供初学者学习——供高手参考

    总之,"Java实用编程源码50例"是一个宝贵的资源,它将理论知识与实践结合,提供了一条高效的学习路径。通过分析和模仿这些示例,开发者可以在实际项目中更好地运用Java,从而提高代码质量和效率。

    安卓手机游戏捕鱼达人源码

    【安卓手机游戏捕鱼达人源码】是一款专门为Android平台设计的游戏开发资源,对于想...对于初学者,这是一个宝贵的实践平台,有助于理论知识与实际应用的结合。对于经验丰富的开发者,源码分析也能带来新的思路和灵感。

    java核心技术第10版加源码

    11. **源码分析**:书中附带的源码能帮助读者理解书中的示例和练习,通过实际运行和调试,将理论知识转化为实际技能。 通过学习《Java核心技术第10版》并配合源码实践,你可以全面提升自己的Java编程能力,无论是在...

    javaJDK1.8 源码(.java文件) 新手教程资料

    Java JDK 1.8 源码新手教程资料是一份宝贵的学习资源,它包含了Java开发工具包的关键组件的源代码,让开发者...在实际学习过程中,可以结合具体场景和问题,逐步深入研究,同时配合实践,将理论知识转化为实际技能。

    JAVA就业培训教程_源码

    这个教程结合了理论讲解与实际源码分析,旨在提供一个全面且深入的学习路径,帮助学员掌握Java编程的核心概念,并具备实际项目开发的能力。以下是教程中的关键知识点: 1. **Java基础**: - **变量与数据类型**:...

    Java2核心技术第一卷源码

    Java2核心技术第一卷是Java编程领域的一本经典著作,它为初学者和有经验的开发者提供了深入理解Java语言和平台的...同时,实践是检验理论的最好方式,你可以尝试根据学到的知识编写自己的程序,以深化理解和提高技能。

Global site tag (gtag.js) - Google Analytics