我必须在内存中保留数千个字符串才能以Java顺序访问。我应该将它们存储在数组中还是应该使用某种List?
由于数组将所有数据保存在连续的内存块中(与列表不同),使用数组存储数千个字符串会不会引起问题?
我必须在内存中保留数千个字符串才能以Java顺序访问。我应该将它们存储在数组中还是应该使用某种List?
由于数组将所有数据保存在连续的内存块中(与列表不同),使用数组存储数千个字符串会不会引起问题?
Answers:
我建议您使用探查器来测试哪个更快。
我个人认为您应该使用列表。
我在大型代码库上工作,以前的一组开发人员到处使用数组。它使代码非常不灵活。将其大块更改为列表后,我们注意到速度没有差异。
Java方式是您应考虑最适合您需求的数据抽象。请记住,在Java中,列表是抽象的,而不是具体的数据类型。您应该将字符串声明为List,然后使用ArrayList实现对其进行初始化。
List<String> strings = new ArrayList<String>();
抽象数据类型和特定实现的这种分离是面向对象编程的关键方面之一。
ArrayList使用数组作为其基础实现来实现List Abstract Data Type。访问速度实际上与数组相同,具有的附加优点是能够在List中添加和减去元素(尽管这是使用ArrayList的O(n)操作),并且如果您决定稍后更改基础实现,您可以。例如,如果您意识到需要同步访问,则可以将实现更改为Vector,而无需重写所有代码。
实际上,ArrayList是专门为在大多数情况下替换低级数组构造而设计的。如果今天要设计Java,则完全有可能将数组完全排除在外,而使用ArrayList构造。
由于数组将所有数据保存在连续的内存块中(与列表不同),使用数组存储数千个字符串会不会引起问题?
在Java中,所有集合仅存储对对象的引用,而不存储对象本身。数组和ArrayList都将在连续数组中存储数千个引用,因此它们本质上是相同的。您可以认为,在现代硬件上,将始终可以轻松获得数千个32位引用的连续块。当然,这并不保证您不会完全用完内存,只是连续的内存需求并不难满足。
尽管建议使用ArrayList的答案在大多数情况下确实有意义,但相对性能的实际问题尚未真正得到解答。
数组可以做一些事情:
尽管在ArrayList上进行获取和设置操作要慢一些(在我的机器上,每次调用分别为1和3纳秒),但使用ArrayList与将数组用于任何非密集型用途的开销很少。但是,请记住以下几点:
list.add(...)
)非常昂贵,应尽可能将初始容量设置为适当的水平(请注意,使用数组时也会出现相同的问题)这是我在标准x86台式机上使用jmh基准测试库和JDK 7 对这三个操作进行测量的结果(时间为纳秒)。请注意,在测试中永远不会调整ArrayList的大小,以确保结果可比。基准代码可在此处获得。
我运行了4个测试,执行以下语句:
Integer[] array = new Integer[1];
List<Integer> list = new ArrayList<> (1);
Integer[] array = new Integer[10000];
List<Integer> list = new ArrayList<> (10000);
结果(每次通话以纳秒为单位,置信度为95%):
a.p.g.a.ArrayVsList.CreateArray1 [10.933, 11.097]
a.p.g.a.ArrayVsList.CreateList1 [10.799, 11.046]
a.p.g.a.ArrayVsList.CreateArray10000 [394.899, 404.034]
a.p.g.a.ArrayVsList.CreateList10000 [396.706, 401.266]
结论:无明显差异。
我运行了2个测试,执行以下语句:
return list.get(0);
return array[0];
结果(每次通话以纳秒为单位,置信度为95%):
a.p.g.a.ArrayVsList.getArray [2.958, 2.984]
a.p.g.a.ArrayVsList.getList [3.841, 3.874]
结论:从数组中获取比从ArrayList中获取大约快25%,尽管差异仅相差一纳秒。
我运行了2个测试,执行以下语句:
list.set(0, value);
array[0] = value;
结果(每次通话以纳秒为单位):
a.p.g.a.ArrayVsList.setArray [4.201, 4.236]
a.p.g.a.ArrayVsList.setList [6.783, 6.877]
结论:数组的设置操作比列表上的速度快约40%,但是,就设置而言,每个设置操作都需要几纳秒的时间-因此,要达到1秒的差异,就需要在列表/数组中设置数百个项目百万次!
ArrayList的副本构造函数委托给它,Arrays.copyOf
因此性能与数组副本相同(通过进行数组复制clone
,Arrays.copyOf
或者System.arrayCopy
在性能方面没有实质性差异)。
您应该更喜欢泛型而不是数组。正如其他人所提到的,数组是不灵活的,并且不具有泛型类型的表达能力。(但是,它们确实支持运行时类型检查,但与泛型类型混合使用很糟糕。)
但是,一如既往,在优化时,您应始终遵循以下步骤:
我猜原来的海报来自C ++ / STL背景,这引起了一些混乱。在C ++中std::list
是一个双链表。
Java中[java.util.]List
是无实现接口(用C ++术语表示的纯抽象类)。List
可以是双向链表- java.util.LinkedList
已提供。但是,当您要新建一个时List
,您要使用100中的99次来java.util.ArrayList
代替,这与C ++大致等效std::vector
。还有其他的标准实现,如由返回java.util.Collections.emptyList()
和java.util.Arrays.asList()
。
从性能的角度来看,必须通过一个接口和一个额外的对象的影响很小,但是运行时内联意味着这几乎没有任何意义。还要记住,String
通常是对象加数组。因此,对于每个条目,您可能还有另外两个对象。在C ++中std::vector<std::string>
,尽管按值复制时没有这样的指针,但是字符数组将形成字符串的对象(通常不会共享)。
如果此特定代码确实对性能敏感,则可以为所有字符串的所有字符创建单个char[]
数组(甚至byte[]
),然后创建偏移量数组。IIRC,这就是javac的实现方式。
我同意,在大多数情况下,您应该选择ArrayLists的灵活性和优雅程度,而不是在阵列中-在大多数情况下,对程序性能的影响可以忽略不计。
但是,如果您正在为软件图形渲染或自定义虚拟机进行恒定,繁重的迭代而几乎没有结构变化(没有添加和删除),我的顺序访问基准测试表明ArrayLists比我的数组慢1.5倍。系统(在一岁的iMac上使用Java 1.6)。
一些代码:
import java.util.*;
public class ArrayVsArrayList {
static public void main( String[] args ) {
String[] array = new String[300];
ArrayList<String> list = new ArrayList<String>(300);
for (int i=0; i<300; ++i) {
if (Math.random() > 0.5) {
array[i] = "abc";
} else {
array[i] = "xyz";
}
list.add( array[i] );
}
int iterations = 100000000;
long start_ms;
int sum;
start_ms = System.currentTimeMillis();
sum = 0;
for (int i=0; i<iterations; ++i) {
for (int j=0; j<300; ++j) sum += array[j].length();
}
System.out.println( (System.currentTimeMillis() - start_ms) + " ms (array)" );
// Prints ~13,500 ms on my system
start_ms = System.currentTimeMillis();
sum = 0;
for (int i=0; i<iterations; ++i) {
for (int j=0; j<300; ++j) sum += list.get(j).length();
}
System.out.println( (System.currentTimeMillis() - start_ms) + " ms (ArrayList)" );
// Prints ~20,800 ms on my system - about 1.5x slower than direct array access
}
}
首先,有必要澄清一下,您是在经典comp sci数据结构意义上是指“列表”(即链接列表)还是java.util.List?如果您的意思是java.util.List,它是一个接口。如果要使用数组,只需使用ArrayList实现,您将获得类似数组的行为和语义。问题解决了。
如果您的意思是数组与链表,这是一个稍有不同的论点,我们可以追溯到Big O(如果这是一个陌生的术语,这里有一个简单的英语解释。
数组;
链接列表:
因此,您可以选择最适合您调整数组大小的方法。如果您调整大小,插入和删除很多内容,那么链表也许是一个更好的选择。如果随机访问很少,则同样适用。您提到了串行访问。如果您主要是进行串行访问而几乎没有修改,那么选择哪种可能都没有关系。
链接列表的开销略高,因为,就像您说的那样,您正在处理潜在的不连续内存块和(有效)指向下一个元素的指针。除非您要处理数百万个条目,否则这可能不是重要因素。
我写了一个基准测试来比较ArrayLists和Arrays。在我的老式笔记本电脑上,遍历具有5000个元素的数组列表的时间为1000次,比等效的数组代码慢大约10毫秒。
所以,如果你在做什么,但迭代的列表,你做了很多,那么也许它的价值的最优化。否则,我会使用列表中,因为它会更容易,当你这样做需要优化的代码。
我确实注意到使用for String s: stringsList
它比使用老式的for循环访问列表要慢50%。走吧,这是我计时的两个功能。数组和列表中填充了5000个随机(不同)字符串。
private static void readArray(String[] strings) {
long totalchars = 0;
for (int j = 0; j < ITERATIONS; j++) {
totalchars = 0;
for (int i = 0; i < strings.length; i++) {
totalchars += strings[i].length();
}
}
}
private static void readArrayList(List<String> stringsList) {
long totalchars = 0;
for (int j = 0; j < ITERATIONS; j++) {
totalchars = 0;
for (int i = 0; i < stringsList.size(); i++) {
totalchars += stringsList.get(i).length();
}
}
}
char[]
不会被触碰(这不是C)。
否,因为从技术上讲,该数组仅存储对字符串的引用。字符串本身分配在不同的位置。对于一千个项目,我想说一个列表会更好,更慢,但它提供了更大的灵活性并且更易于使用,特别是如果您要调整它们的大小。
如果您有成千上万,请考虑使用特里。特里树是一种树状结构,可合并存储字符串的公共前缀。
例如,如果字符串是
intern
international
internationalize
internet
internets
特里将存储:
intern
-> \0
international
-> \0
-> ize\0
net
->\0
->s\0
这些字符串需要57个字符(包括空终止符'\ 0')进行存储,再加上容纳它们的String对象的大小。(实际上,我们应该将所有大小四舍五入为16的倍数,但是...)将其大致称为57 + 5 = 62字节。
特里需要29个(包括空终止符'\ 0')进行存储,再加上特里节点的大小,这是对数组的引用和子特里节点的列表。
对于这个例子,结果可能差不多。对于成千上万的内容,只要您具有通用前缀,它的显示量就可能更少。
现在,当在其他代码中使用trie时,您必须转换为String,可能使用StringBuffer作为中介。如果在Trie之外立即将许多字符串作为Strings使用,那是一种损失。
但是,如果您一次只使用少数几个字(例如,在字典中查找内容),则Trie可以节省大量空间。比将它们存储在HashSet中的空间要少得多。
您说您正在“串行”访问它们-如果这意味着按字母顺序顺序访问,则如果您先进行深度优先迭代,则trie显然也会免费为您提供字母顺序。
更新:
正如Mark所指出的,JVM预热(几次测试通过)后没有显着差异。用重新创建的数组检查,甚至检查从新的矩阵行开始的新检查。这很有可能不使用带有索引访问权限的简单数组来支持集合。
最初的1-2通过简单阵列仍然快2-3倍。
原始帖子:
该主题的字词过多,难以检查。没有任何问题数组比任何类容器快几倍。我在这个问题上一直在寻找性能关键部分的替代方案。这是我用来检查实际情况的原型代码:
import java.util.List;
import java.util.Arrays;
public class IterationTest {
private static final long MAX_ITERATIONS = 1000000000;
public static void main(String [] args) {
Integer [] array = {1, 5, 3, 5};
List<Integer> list = Arrays.asList(array);
long start = System.currentTimeMillis();
int test_sum = 0;
for (int i = 0; i < MAX_ITERATIONS; ++i) {
// for (int e : array) {
for (int e : list) {
test_sum += e;
}
}
long stop = System.currentTimeMillis();
long ms = (stop - start);
System.out.println("Time: " + ms);
}
}
这是答案:
基于数组(第16行处于活动状态):
Time: 7064
基于列表(第17行处于活动状态):
Time: 20950
还有关于“更快”的评论吗?这是相当了解的。问题是,比List的灵活性快3倍左右对您来说更好。但这是另一个问题。顺便说一句,我也基于手动构建检查了这一点ArrayList
。几乎相同的结果。
3
真实的速度快了十倍,但微不足道。14ms
时间不长
由于这里已经有了很多好的答案,因此,我想为您提供一些实用的观点,即插入和迭代性能比较:Java中的原始数组与链接列表。
这是实际的简单性能检查。
因此,结果将取决于机器性能。
用于此目的的源代码如下:
import java.util.Iterator;
import java.util.LinkedList;
public class Array_vs_LinkedList {
private final static int MAX_SIZE = 40000000;
public static void main(String[] args) {
LinkedList lList = new LinkedList();
/* insertion performance check */
long startTime = System.currentTimeMillis();
for (int i=0; i<MAX_SIZE; i++) {
lList.add(i);
}
long stopTime = System.currentTimeMillis();
long elapsedTime = stopTime - startTime;
System.out.println("[Insert]LinkedList insert operation with " + MAX_SIZE + " number of integer elapsed time is " + elapsedTime + " millisecond.");
int[] arr = new int[MAX_SIZE];
startTime = System.currentTimeMillis();
for(int i=0; i<MAX_SIZE; i++){
arr[i] = i;
}
stopTime = System.currentTimeMillis();
elapsedTime = stopTime - startTime;
System.out.println("[Insert]Array Insert operation with " + MAX_SIZE + " number of integer elapsed time is " + elapsedTime + " millisecond.");
/* iteration performance check */
startTime = System.currentTimeMillis();
Iterator itr = lList.iterator();
while(itr.hasNext()) {
itr.next();
// System.out.println("Linked list running : " + itr.next());
}
stopTime = System.currentTimeMillis();
elapsedTime = stopTime - startTime;
System.out.println("[Loop]LinkedList iteration with " + MAX_SIZE + " number of integer elapsed time is " + elapsedTime + " millisecond.");
startTime = System.currentTimeMillis();
int t = 0;
for (int i=0; i < MAX_SIZE; i++) {
t = arr[i];
// System.out.println("array running : " + i);
}
stopTime = System.currentTimeMillis();
elapsedTime = stopTime - startTime;
System.out.println("[Loop]Array iteration with " + MAX_SIZE + " number of integer elapsed time is " + elapsedTime + " millisecond.");
}
}
效果结果如下:
我来这里的目的是更好地了解在数组上使用列表对性能的影响。我必须在这里适应我的情况的代码:数组/列表(约1000个整数)主要使用吸气剂,这意味着array [j]与list.get(j)
以7的最佳表现为科学依据(前几项的速度慢2.5倍),我得到以下信息:
array Integer[] best 643ms iterator
ArrayList<Integer> best 1014ms iterator
array Integer[] best 635ms getter
ArrayList<Integer> best 891ms getter (strange though)
-因此,使用阵列的速度大约快30%
现在发布的第二个原因是,如果您使用嵌套循环执行数学/矩阵/模拟/优化代码,没有人提及其影响。
假设您有三个嵌套级别,而内部循环的速度是性能命中率的8倍,是您的两倍。现在一天要运行的东西需要一周的时间。
*编辑在这里非常震惊,我尝试声明int [1000]而不是Integer [1000]来踢
array int[] best 299ms iterator
array int[] best 296ms getter
使用Integer []与int []表示性能提高了两倍,带有迭代器的ListArray比int []慢3倍。真的认为Java的列表实现类似于本机数组...
参考代码(多次调用):
public static void testArray()
{
final long MAX_ITERATIONS = 1000000;
final int MAX_LENGTH = 1000;
Random r = new Random();
//Integer[] array = new Integer[MAX_LENGTH];
int[] array = new int[MAX_LENGTH];
List<Integer> list = new ArrayList<Integer>()
{{
for (int i = 0; i < MAX_LENGTH; ++i)
{
int val = r.nextInt();
add(val);
array[i] = val;
}
}};
long start = System.currentTimeMillis();
int test_sum = 0;
for (int i = 0; i < MAX_ITERATIONS; ++i)
{
// for (int e : array)
// for (int e : list)
for (int j = 0; j < MAX_LENGTH; ++j)
{
int e = array[j];
// int e = list.get(j);
test_sum += e;
}
}
long stop = System.currentTimeMillis();
long ms = (stop - start);
System.out.println("Time: " + ms);
}
如果您事先知道数据有多大,那么数组将更快。
列表更灵活。您可以使用由数组支持的ArrayList。
如果您可以使用固定大小,则阵列将更快并且需要更少的内存。
如果您需要通过添加和删除元素来灵活使用List接口,则问题仍然是应该选择哪种实现。通常建议使用ArrayList并将其用于任何情况,但是如果必须删除或插入列表开头或中间的元素,则ArrayList也会出现性能问题。
因此,您可能需要查看介绍GapList的http://java.dzone.com/articles/gaplist-%E2%80%93-lightning-fast-list。这种新的列表实现结合了ArrayList和LinkedList的优势,从而为几乎所有操作提供了非常好的性能。
建议在任何地方使用数组而不是列表,尤其是在您知道项目数和大小不会改变的情况下。
请参阅Oracle Java最佳实践:http : //docs.oracle.com/cd/A97688_16/generic.903/bp/java.htm#1007056
当然,如果您需要多次从集合中添加和删除对象,则使用方便。
没有一个答案有我感兴趣的信息-多次重复扫描同一阵列。不得不为此创建一个JMH测试。
结果(Java 1.8.0_66 x32,迭代普通数组至少比ArrayList快5倍):
Benchmark Mode Cnt Score Error Units
MyBenchmark.testArrayForGet avgt 10 8.121 ? 0.233 ms/op
MyBenchmark.testListForGet avgt 10 37.416 ? 0.094 ms/op
MyBenchmark.testListForEach avgt 10 75.674 ? 1.897 ms/op
测试
package my.jmh.test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
@State(Scope.Benchmark)
@Fork(1)
@Warmup(iterations = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class MyBenchmark {
public final static int ARR_SIZE = 100;
public final static int ITER_COUNT = 100000;
String arr[] = new String[ARR_SIZE];
List<String> list = new ArrayList<>(ARR_SIZE);
public MyBenchmark() {
for( int i = 0; i < ARR_SIZE; i++ ) {
list.add(null);
}
}
@Benchmark
public void testListForEach() {
int count = 0;
for( int i = 0; i < ITER_COUNT; i++ ) {
for( String str : list ) {
if( str != null )
count++;
}
}
if( count > 0 )
System.out.print(count);
}
@Benchmark
public void testListForGet() {
int count = 0;
for( int i = 0; i < ITER_COUNT; i++ ) {
for( int j = 0; j < ARR_SIZE; j++ ) {
if( list.get(j) != null )
count++;
}
}
if( count > 0 )
System.out.print(count);
}
@Benchmark
public void testArrayForGet() {
int count = 0;
for( int i = 0; i < ITER_COUNT; i++ ) {
for( int j = 0; j < ARR_SIZE; j++ ) {
if( arr[j] != null )
count++;
}
}
if( count > 0 )
System.out.print(count);
}
}
“千”不是一个大数目。几千个段落长度的字符串的大小约为几兆字节。如果您只想串行访问这些文件,请使用不可变的单链接List。
没有适当的基准测试,不要陷入优化的陷阱。正如其他人所建议的,请在进行任何假设之前使用分析器。
您枚举的不同数据结构具有不同的用途。列表在开头和结尾插入元素非常有效,但是在访问随机元素时会遇到很多麻烦。阵列具有固定的存储,但提供快速的随机访问。最后,ArrayList通过允许其增长来改善与该接口的接口。通常,要使用的数据结构应由如何访问或添加存储的数据来决定。
关于内存消耗。您似乎在混入一些东西。数组只会为您拥有的数据类型提供连续的内存块。不要忘记Java具有固定的数据类型:布尔值,char,int,long,float和Object(这包括所有对象,甚至数组都是Object)。这意味着,如果声明字符串字符串[1000]或MyObject myObjects [1000]的数组,则只会得到一个足够大的1000个存储盒来存储对象的位置(引用或指针)。您没有足够大的1000个存储盒来容纳对象的大小。不要忘记,您的对象首先是用“ new”创建的。这是在完成内存分配并随后将引用(其内存地址)存储在数组中时。仅将对象作为参考,该对象不会复制到数组中。
此处给出的许多微基准已发现诸如array / ArrayList读取之类的东西只有几纳秒的数量。如果所有内容都在您的L1缓存中,这是非常合理的。
较高级别的高速缓存或主存储器访问的量级时间大约为10nS-100nS,而对于L1高速缓存则为1nS。访问ArrayList有一个额外的内存间接方式,在实际的应用程序中,您几乎可以从不支付任何费用到每次都支付此费用,具体取决于两次访问之间代码的作用。而且,当然,如果您有很多小的ArrayList,则这可能会增加您的内存使用量,并更有可能导致缓存未命中。
原始海报似乎只使用其中一个,并且可以在很短的时间内访问很多内容,因此应该不会有太大的困难。但这对于其他人可能有所不同,并且在解释微基准测试时应格外小心。
但是,Java字符串非常浪费,特别是如果您存储许多小字符串(仅使用内存分析器查看它们,对于几个字符的字符串来说,似乎大于60字节)。字符串数组具有对String对象的间接寻址,以及从String对象到包含字符串本身的char []的间接寻址。如果有什么要破坏您的L1缓存的,那就是它,结合成千上万的字符串。因此,如果您真的很认真-真的很认真-希望尽可能地提高性能,那么您可以考虑采用不同的方法。您可以说,包含两个数组,一个包含所有字符串的char [],一个接一个,另一个int [],其起始位置偏移。这将是PITA可以执行的任何操作,并且您几乎肯定不需要它。如果这样做的话,
ArrayList在内部使用数组对象添加(或存储)元素。换句话说,ArrayList由Array数据结构支持.ArrayList的数组是可调整大小的(或动态的)。
数组比数组快因为ArrayList在内部使用数组。如果我们可以直接在Array中添加元素并通过ArrayList间接在Array中添加元素,那么直接机制总是比间接机制快。
ArrayList类中有两个重载的add()方法:
1 . add(Object)
:将对象添加到列表的末尾。
2 add(int index , Object )
.:将指定的对象插入列表中的指定位置。
ArrayList的大小如何动态增长?
public boolean add(E e)
{
ensureCapacity(size+1);
elementData[size++] = e;
return true;
}
上面代码中需要注意的重要一点是,在添加元素之前,我们正在检查ArrayList的容量。guaranteeCapacity()确定当前已占用元素的大小以及数组的最大大小。如果填充元素的大小(包括要添加到ArrayList类的新元素)大于数组的最大大小,则增加数组的大小。但是数组的大小不能动态增加。所以内部发生的事情是创建具有容量的新阵列
直到Java 6
int newCapacity = (oldCapacity * 3)/2 + 1;
(更新)从Java 7
int newCapacity = oldCapacity + (oldCapacity >> 1);
同样,旧阵列中的数据也将复制到新阵列中。
ArrayList中有开销方法,这就是Array比快的原因ArrayList
。
数组-当我们必须更快地获取结果时,总会更好
列表-执行插入和删除的结果,因为它们可以在O(1)中完成,并且还提供了轻松添加,获取和删除数据的方法。更容易使用。
但是请始终记住,当存储数据的数组中的索引位置已知时,数据的获取会很快。
通过对数组排序可以很好地实现。因此,这增加了获取数据的时间(即:存储数据+对数据进行排序+寻找找到数据的位置)。因此,这增加了从阵列中获取数据的额外等待时间,即使它们可能擅长于更快地获取数据。
因此,这可以通过特里数据结构或三元数据结构解决。如上所述,特里数据结构在搜索数据中将非常有效,可以以O(1)的幅度进行特定单词的搜索。当时间很重要时 如果必须快速搜索和检索数据,则可以使用trie数据结构。
如果您希望减少内存空间的消耗,并且希望获得更好的性能,则可以使用三元数据结构。两者都适合存储大量的字符串(例如,字典中包含的单词)。