Java容器类默认初始容量研究

利用OTSU阈值算法在ZYNQ上实现浒苔识别 利用OTSU阈值算法进行图像二值化,结果用HDMI显示 阅读详情

昨天参加了个公司的Java Performance的培训,其间提到了一些容器类的构造函数有默认的参数,用来控制容器的初始容量,建议是尽可能准确地预测容量需求,根据需求创建指定大小的容器,而不是使用默认的容量,这样可以有效减少内存的浪费,以及扩容所带来的开销。培训中主要提到了StringBuilder和HashMap两个类,下面我们结合JDK源代码来看看到底是怎样的情况,下面的代码基于JDK 1.8,和1.6版本的相比有较大变化。

  1. StringBuilder
    首先看下StringBuilder,它的默认构造函数是这样的:
    /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {
        super(16);
    }

写的很清楚了,默认长度为16。另外如果传入的是一个String或者是CharSequence的话,capacity也是在其基础上加16:

    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

另外还有一个扩容的问题,当StringBuilder现有的capacity装不下新append进来的字符串时怎么办呢,这个实现是在其基类AbstractStringBuilder里面实现的:

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

其中value和count是AbstractStringBuilder的私有成员变量,count记录了现在有效的字符串长度,只有对字符串进行操作时才更改,并不是capacity。value才是真正存储字符串的数组,StringBuilder的capacity对应的是value的长度。所以上面这段代码就很简单了,先确保capacity能容下新旧字符串,不够就扩容,然后再把传入参数的字符串接在现有的字符串后面。
然后来看看这段的核心,ensureCapacityInternal具体是怎么扩容的,一路跟进最后看到是在ensureCapacity里面实现的:

    /**
     * This implements the expansion semantics of ensureCapacity with no
     * size check or synchronization.
     */
    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

先预设一个新长度是现有capacity的两倍再加2,如果还不满足这一次扩容的需要就按输入的扩容,最大是整数型的最大值。也就是说,StringBuilder扩容时候最少是翻倍。 然后再申请一段新的内存空间把原有的字符串复制到新的空间里。由此可见扩容确实会引起额外的内存开销以及系统调用和GC,所以尽可能的准确预测所拼接的字符串长度,一次到位地申请空间能够有效提高Java性能。

  1. HashMap
    HashMap要比StringBuilder复杂一些,除了capacity一个因素以外还有一个factor,关于他们之间的关系官方文档是这么描述的:

Iteration over collection views requires time proportional to the “capacity” of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings). Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.
An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.

结合HashMap的成员变量来看:

    //用来存储Node的链表
    transient Node<K,V>[] table;
    //已存储的key-value对的数量
    transient int size;
    //rehash门限值
    int threshold;
    //加载参数
    final float loadFactor;
    //
    transient int modCount;

    //默认初始容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //默认加载参数0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

HashMap的构造函数主要一共有三种重载:

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

我们可以看到在默认情况下只是设置了loadFactor而没有申请空间,直到第一次put的时候才申请默认初始大小的capacity,当每次put数据的时候会进行如下处理,这里省略了部分代码

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //默认情况(实际存储用的数组为空)下,先初始化空间
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据key的hash值和bucket数量确定位置,如果该位置为空就直接在该位置上新建Node
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //如果散列发生碰撞
        else {
            Node<K,V> e; K k;
            //当前key已经存在,且key值和hash值完全一样
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                ...
            }
            //覆盖原有的value
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果Node的数量超过了门限值就要重新分配空间
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

其中涉及到空间分配的就是resize()这个函数:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //已经分配过空间
        if (oldCap > 0) {
            //现有的容量已达上限就不再分配空间
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //已经完成过初始化,并且容量翻倍以后没有超过上限
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               //默认构造函数的话就按照default的capacity和threshold来申请空间
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            //将原始map中的数据复制到新的map中
            ...
        }
        return newTab;
    }

总结一下就是除了在首次申请空间以及超过上限的情况下,基本都是直接翻倍申请新的空间,然后还涉及到原有key的rehash问题,所以扩容的开销也是蛮大的,在resize的过程中,空间占用的峰值会达到原有空间3倍,所以设置合理的capacity和loadFactor可以在很大程度上提高性能。

所有有自动扩容功能的非链表实现的容器基本都有这个过程,下面总结一下常见的一些容器的默认初始容量和扩容策略:

容器类:初始长度/扩容倍数
- ArrayList:10/1.5
- ArrayDeque:8/2
- BitSet:64/2
- HashMap:16/2
- HashSet/TreeSet:同HashMap(基于HashMap实现,value为空Object)
- Hashtable:11/2
- WeakHashMap:同HashMap
- PriorityQueue:11/Double size if small; else grow by 50%
- StringBuilder:16/按需

迪文屏【0】——从零搭建工程:图片显示、文本变量配置与串口数据交互实战 本文以迪文屏T5L系列为例,详细介绍了从零搭建HMI工程的完整流程。内容涵盖硬件软件准备、DGUS工程创建、背景图片显示、文本变量配置,并重点演示了如何通过串口指令实时修改屏幕数据变量,实现动态交互。旨在帮助开发者快速掌握迪文屏开发的核心步骤与数据交互方法。 阅读详情

相关推荐

基于Pixhawk与OpenMV的无人机视觉引导精准降落系统设计与实现

本文详细介绍了基于Pixhawk飞控与OpenMV视觉模块的无人机精准降落系统设计与实现。系统通过OpenMV识别Apriltag标签,结合MAVLink通信将视觉数据发送给Pixhawk,实现厘米级精准降落。文章涵盖硬件连接、软件配置、核心代码逻辑解析及实战调试技巧,为无人机自动充电、物流配送等场景提供可靠解决方案。

weixin_29189377的博客 284

Java中的各种容器初始值大小

一、Java中的各种容器初始值大小 StringBuffer和StringBuilder初始默认大小为16个字符 HashMap初始默认大小16,加载因子为0.75,自增为2n. HashTable默认初始值为11,加载因子为0.75,自增为2n+1 ArrayList初始默认值为10,自增为1.5n Vector初始默认值为10,自增为2n 二、解析 List元素是有序的、可重复 ArrayList、Vector默认初始容量为10 Vector:线程安全,但速度慢     底层数..

慕白Lee的博客 2103

【水文学法总结】河道内生态流量计算方法(含MATLAB实现代码)

简介水文学法实现河道内生态流量计算

WW、forever的博客 4944

java常用的jvm参数

在部署java项目时,需要配置一些jvm以达到项目运行最优化。常用的jvm参数如下使用例子:

weixin_45167444的博客 3504

容器化--部署--基础配置参数

docker安装后默认没有daemon.json这个配置文件,需要进行手动创建。配置文件的默认路径:/etc/docker/daemon.json一般情况,配置文件 daemon.json中配置的项目参数,在启动参数中同样适用,有些可能不一样(具体可以查看官方文档),但需要注意的一点,配置文件中如果已经有某个配置项,则无法在启动参数中增加,会出现冲突的错误如果在daemon.json文件中进行配置,需要docker版本高于1.12.6(在这个版本上不生效,1.13.1以上是生效的)参数 daemon.jso

l402072123的博客 491

java集合类总结

HashMap HashMap中的属性: 1.capacity:容量,Node数组的大小(桶的数量),值为2的N次幂 2.threshold:扩容阈值,等于capacity * load factor 当size大于此值时进行扩容 3.loadFactor:负载因子,默认为0.75f 4.size属性是指map中的键值对个数 5.initialCapacity hashMap初始化时设置的容量大小(Node数组的长度),initialCapacity = (需要存储的元素个数 / 负载因子) + 1,如果可

javasoftdeveloper的博客 267

java的vector6_Java容器类研究6:Vector

java.util.VectorVector与ArrayList的异同相同点:随机存取,可通过位置序号直接获取数据。都是通过一个数组来存放元素。不同点:Vector是线程安全的,方法有synchronized关键字修饰。Vector容量增长策略Vector默认的增长策略是每次在原容量的基础上x2。Vector的ListIterator怎么做到线程安全的Vector实现了自己的iterator,为了...

weixin_36212539的博客 140

java 策略容器_Java容器类研究6:Vector

java.util.VectorVector与ArrayList的异同相同点:随机存取,可通过位置序号直接获取数据。都是通过一个数组来存放元素。不同点:Vector是线程安全的,方法有synchronized关键字修饰。Vector容量增长策略Vector默认的增长策略是每次在原容量的基础上x2。Vector的ListIterator怎么做到线程安全的Vector实现了自己的iterator,为了...

weixin_33274593的博客 147

第17章 容器深入研究

迭代器的指针指向元素位置之前: public static void test01() { List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); // 初始化指针指向第0个元素(元素从0开始计数)之前(默认): // ListIterator<String&.

qq_43183860的博客 241

Java面试专项——集合专题一(ArrayList)--万字读懂ArrayList底层原理

集合是JAVA重点中的重点也是面试官必会提问的一点,通常涉及到集合的面试题通常呈现以下几点:难度大、粒度细、重底层、涉及数据结构等基础知识。 本专题首先复习集合框架涉及到的数据结构基础知识,然后系统地回顾Java集合框架,最后解读一下涉及到Java集合的经典高频面试题。 一、数据结构回顾 二、集合框架回顾 如上图所示,集合分为两大类(Collection与Map),其中List接口与Set接口是Collection众多子接口中的两个。也可以说JAVA集合分为三大类---List、Set、M.

qq_42681787的博客 2267

java初始化参数_java – HashMap初始化参数(load/initialcapacity)

关于负载系数,我只是引用从HashMap javadoc:As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cos...

weixin_30340301的博客 526

Java中String类型长度限制

Java中String类型在编译时的长度限制是65535。例如我们通过new String创建一个长度为3的字符串,这是正常的;如果我们创建一个长度超过65535的字符串,编译器也不会提示有问题,但是一旦编译就会发生报错。Java中String类型在运行时的长度限制是2^31 - 1。如果我们需要使用到一个长度超过65535的字符串,可以将字符串拆分成数个长度不足65535的字符串,让代码能正常编译,之后再进行字符串拼接即可。

weixin_41119184的博客 740

Hashtable和HashMap

前不久用JAVA写了一个简单的哈希表,但是在和Hashtable、HashMap进行性能测试比较时,发现相差得有点大,于是专门去探究了一下Hashtable和HashMap的一些特性。 一、Hashtable和HashMap的不同 1.继承对象不同 Hashtable继承Dictionary类,而HashMap继承Map类继承AbstractMap类。 2.哈希表的初始容量和扩展方...

Alexwym的博客 1269

JAVA之HashMap详解

HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。jdk1.8 之前 HashMap 由数组 + 链表组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”解决冲突)。

励志成为一名优秀的全能型软件开发工程师。 2322

面试题:Java中的String的长度有没有限制?具体是多少?

String字符串的长度有限制。编译期的限制:字符串的 UTF-8 编码值的字节数不能超过 65535,字符串的最大为65534,因为需要一个;运行时限制:字符串的长度不能超过 2^31-1,占用的内存数不能超过JVM能够提供的最大值。

KELLENSHAW的博客 1577

Java性能优化的45个细节

转自:https://mp.weixin.qq.com/s/AiMz62n3pMOPLfWx-0KmHQ 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 控制资源的使用,通过线程同步来控制资源的并发访问; 控制实例的产生,以达到节约资源的目的; 控制数据共享,在不建立直接关联的条件下...

hxt的博客 350

java arraylist初始化长度_ArrayList(int initialCapacity)会不会初始化数组大小?

今天看到一个如博客题目问题,想着应该会初始化吧,不然这个构造方法时干嘛的是吧。话不多说,直接上代码,看下就好了。public static void main(String[] args) {ArrayList list = new ArrayList(10);System.out.println(list.size());list.add("hello");System.out.println(...

weixin_28256233的博客 866

Java常用集合分析

ArrayList与Vector 动态数组(速度较慢) 创建时可以使用initialCapacity指定初始化大小,如果不指定则数组大小默认为10. ArrayList与Vector的区别: ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个线程修改了ArrrayList集合,则程序必须手动保证该集合的同步性; Vector集合时线程安全的,但性能比Ar...

yin_xi_guang的博客 209

HashMap和HashTable的区别

1.父类和实现接口不同 HashMap:父类AbstractMap,实现接口Cloneable,Serializable,map HashTable:父类Dictionary, 实现接口Cloneable,Serializable,map 2.相关方法 HashTable比HashMap多了两个方法,elments(),contain() elments() 方法继承自Hashtable的父类Dictionnary。elements() 方法用于返回此Hashtable中的value的枚举。 c

qq_38977566的博客 290

java map 初始化大小_(JAVA)ConcurrentHashMap initialCapacity 手动计算初始容量大小过程...

package com.lius.test;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/*** (JAVA)ConcurrentHashMap initialCapacity 手动计算初始容量大小过程** 第一步:int cap = ((initialCapacity>=(MAXIMUM_CAPAC...

weixin_36076151的博客 1840

【docker】docker 创建容器的一些参数选项

--name 为容器创建别名, -it 保留命令行运行, -p 8888:8888 将本地的8888端口http://localhost:8888/ 映射, -v/Users/kaiyiwang/Code/ai/notebooks:/notebooks/data将本地的/Users/kaiyiwang/Code/ai/notebooks文件夹挂载到新建容器的/notebooks/dat...

1916

Matlab实现光线追迹

Matlab实现光线追迹

70款axure大屏大数据可视化看板原型设计素材.zip

70款axure大屏大数据可视化看板原型设计素材,axure9可视化素材,70款axure大屏大数据可视化看板原型设计素材.zip

上一篇: 利用Node.js流和事件处理文本文件
下一篇: Java容器与Hash算法的研究
mdifferent
博客等级 码龄18年 18粉丝 25原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值