常用容器制定初始化容量

前面在javaSe中我已经整理了相关的一些数据结构,现在就性能方面在这里做一个总结。以后在实际编码中,应该按照这样子来初始化相关的数据结构。

  • 1,StringBuffer和StringBuilder

StringBuffer()           构造一个其中不带字符的字符串缓冲区,初始容量为 16 个字符。

StringBuffer(int capacity)           构造一个不带字符,但具有指定初始容量的字符串缓冲区。

关于这2个类本身的区别和性能我就不做重复了,一般的情况下我们使用StringBuilder就好了,因为一般的这些字符串的操作基本都没有数据不同问题,所以直接使用线程不安全的StringBuilder就好了。

底层原理:StringBuffer和StringBuilder一样,底层使用的数据存储都是char[],随着新元素不断加入StringBuffer,底层的数据存储char[]的大小也需要随之调整。大小调整的结果就是字符数组char[]分配到了一块更大的空间来创建新的字符串数组char[],老数组中的字符元素被复制到了新的数组中,而原有的老数组则被丢弃,即这部分资源将会被垃圾回收。

实际编码过程中,初始化一个字符串的时候如果没有指定包含具体的内容,我们一般都是调上面的第一个构造器,默认的初始化容量一般都是16个字符。其实在实际的代码操作过程中,很少出现少于16个字符的情况,所以我们一般还是人工的去指定下,初始化之前先大概的估计一下字符串的长度,然后将参数带入构造器中进行初始化。

  • 2,数组

数组就不做赘述了。由于数组是定长的,也就是说数组一旦初始化完成,数组在内存中所占用的空间就被固定下来了,因此数组的长度将不可在改变。具体的初始值的获得有2种方式,一种由系统自己分配,一种由程序员来指定。

静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统来决定长度。

动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。


  • 3,ArrayList 

ArrayList()           构造一个初始容量为 10 的空列表。
ArrayList(int initialCapacity)           构造一个具有指定初始容量的空列表。

JDK的api上是这么说的:每个 ArrayList 实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。并未指定增长策略的细节,因为这不只是添加元素会带来分摊固定时间开销那样简单。

在添加大量元素前,应用程序可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。


在实际的编码过程中如果我们不能明确的知道list的容量大小的话,那么我就可以指定一个差不多的,如果我们可以明确的知道,比如说由于某种业务处理操作,我都已经知道有4个对象要放在一个list里面去了,那么就应该在初始化这个容器的时候指定容量就为4就OK,还有一种最常用的情景就是说分页查询数据库,那么这个时候我们都已经说明确的知道了返回的数据的行数了,然后我们将数据抓出来ORM映射成对象后丢入到list中,那么我们这个时候就可以将返回的行数或者说分页数作为该容器初始化时候的容量大小。


  • 4,HashMap 

HashMap()           构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。 
HashMap(int initialCapacity)           构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。 
HashMap(int initialCapacity, float loadFactor)           构造一个带指定初始容量和加载因子的空 HashMap。 

此实现假定哈希函数将元素正确分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代集合视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)的和成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。 

HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。 

通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地降低 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。 

如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。

通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点,可以想想为什么)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地降低 rehash 操作次数。如果初始容量大于最大条目数除以加载因子(实际上就是最大条目数小于初始容量*加载因子),则不会发生 rehash 操作。 

如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。 当HashMap存放的元素越来越多,到达临界值(阀值)threshold时,就要对Entry数组扩容,这是Java集合类框架最大的魅力,HashMap在扩容时,新数组的容量将是原来的2倍,由于容量发生变化,原有的每个元素需要重新计算bucketIndex,再存放到新数组中去,也就是所谓的rehash。HashMap默认初始容量16,加载因子0.75,也就是说最多能放16*0.75=12个元素,当put第13个时,HashMap将发生rehash,rehash的一系列处理比较影响性能,所以当我们需要向HashMap存放较多元素时,最好指定合适的初始容量和加载因子,否则HashMap默认只能存12个元素,将会发生多次rehash操作。


实际编码过程中,我们可以将loadFactor指定成1,容量的大小这个理解起来没有问题的,应该好好理解一下这个加载因子的概念。如果这个值设置的太小的话,那么就相当于初始化容量设置的太高了,这样子有利于查询,但对于迭代来说不是很好。如果这个值设置的太大的话,那么迭代性能会好一点,但是也增加了查询的成本。虽然说查询是集合最频繁的操作,但是实际编码过程中我们也经常迭代的,所以一般我们在人工指定这2个参数的时候,可以设置加载因子等于1好了,这样子也方便我们自己理解代码,因为我们这样子初始化的容器可以放入的元素就是我们前面指定的那个容器容量的数目了,不需要容量*加载因子了。


  • 5,HashSet 

HashSet()           构造一个新的空集合,其底层 HashMap 实例的默认初始容量是 16,加载因子是 0.75。
HashSet(int initialCapacity)           构造一个新的空集合,其底层 HashMap 实例具有指定的初始容量和默认的加载因子(0.75)。 
HashSet(int initialCapacity, float loadFactor)           构造一个新的空集合,其底层 HashMap 实例具有指定的初始容量和指定的加载因子。 


此类为基本操作提供了稳定性能,这些基本操作包括 add、remove、contains 和 size,假定哈希函数将这些元素正确地分布在桶中。对此集合进行迭代所需的时间与 HashSet 实例的大小(元素的数量)和底层 HashMap 实例(桶的数量)的“容量”的和成比例。因此,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。

在我前面的javaSe的博客中,我也已经说了,从java源码来看,java是先实现了Map,然后通过包装一个所有的value都是null的map来实现了set集合。那现在就对采用了hash算法的相关集合做一个性能总结。

如果开始就知道,HashSet和HashMap会保存很多记录,则可以在创建时就应该使用较大的合理的初始化容量。这里说的合理就是说设值不能太夸张,我们设值稍微大一点是为了不想让上面的集合容易发生rehashing,如果设值的太大太大了,那么反而浪费空间,而且还很影响迭代性能呢。


原文地址:https://www.cnblogs.com/LinkinPark/p/5232986.html