尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

String[]与List<String>深度对比:底层原理、性能差异与选型指南

String[]与List<String>深度对比:底层原理、性能差异与选型指南 String[]和List 的区别说大不大说小不小。平时写代码的时候未必在意但一旦涉及到方法传参、接口返回、还有那种改了几十个调用方的重构选错容器类型是真的会让人头大。而且这玩意儿在面试里出现的频率也不低问的就是你知不知道二者在底层、类型系统、可变性上的本质差异。这篇文章就好好捋一遍从原理到实操再到各种坑位盘点争取一次说透。先说个最直白的总结String[]是Java语言内置的数组类型List 是集合框架里的接口ArrayList 才是它最常见的实现。数组长度固定List长度可伸缩。数组在内存中是连续的一整块区域List看起来像动态的但底层仍然是数组只不过封装了扩容逻辑。理解了这一层后面所有的区别都能推导出来。1. 先搞清楚本质数组和List是两个世界的产物1.1 数组静态分配的一排带编号的格子数组在Java里是最底层的容器它直接由JVM支持不需要任何类库介入。你写String[] arr new String[10]的时候JVM直接在堆上分配一块连续的内存空间能放10个String引用。关键点在于这块空间一经分配容量就固定了。你说arr[5] hello那是下标访问JVM计算偏移量直接读写内存非常快。但你想往里塞第11个元素不行ArrayIndexOutOfBoundsException等着你。数组这种定长、连续、按下标访问的设计带来的好处是极致的访问性能。它还有一个在Java中比较特殊的性质数组是协变的。什么意思呢就是String[]可以被当作Object[]来用因为String是Object的子类。这种协变给了数组灵活性但也在某些场景下引入了运行时类型检查的负担。1.2 List有自我生长能力的容器List是接口ArrayList和LinkedList是它的两大经典实现。开发中90%以上用的都是ArrayList它内部其实也有一块数组transient Object[] elementData。所以你完全可以这么理解ArrayList就是一个会自动扩容的数组。当你调用add()方法的次数超过当前数组容量时它会创建一个新数组把旧元素复制过去再追加新元素。这个扩容过程就是所谓的动态。List把数组的下标访问包装成了方法调用get(index)本质上是elementData[index]性能损失其实很小现代JIT都能把方法调用优化成直接内存访问。List真正的优势是API丰富插入、删除、查找、判空、截取子列表、批量操作样样都有。1.3 协变与泛型类型系统上不可调和的矛盾上面说了数组是协变的但泛型不是。ListString和ListObject之间没有任何子类型关系。这也是Java语言历史设计上的一个经典瑕疵——数组的协变设计在先泛型的不可变设计在后两者就产生了冲突。举一个经典例子String[] strArr new String[10]; Object[] objArr strArr; // 编译通过数组协变 objArr[0] 123; // 编译通过但运行时ArrayStoreException而对应地ListString strList new ArrayList(); // ListObject objList strList; // 编译都过不去这种差异在写通用工具方法的时候尤其明显。如果你写一个入参是ListObject的方法你会发现传ListString直接编译失败但如果入参是Object[]传String[]则没问题。理解了这个底层设计你才能在API设计阶段就避免踩坑。2. 拿String[]和List 做一次全维度对比2.1 声明、初始化与访问方式一个用length一个用size()先看一组最直观的对比// 数组 String[] arr {a, b, c}; int len arr.length; // 属性不是方法 String first arr[0]; // List ListString list Arrays.asList(a, b, c); int size list.size(); // 方法你有括号吗 String firstVal list.get(0);这个差异在入门期特别容易搞混length后面没有括号size()后面有括号。更深层的原因是数组是JVM原生支持的数据结构length是它的内置字段而List是类库层面的东西通过方法暴露状态。2.2 API能力的差距数组几乎是裸奔的String[]能做的事极其有限创建、赋值、遍历、用Arrays工具类做排序和二分搜索。但List接口提供了成套的操作方法这里列一下高频API功能需求String[]数组List追加元素不支持需新建数组add()删除元素不支持需System.arraycopyremove()判断是否包含需手写循环或转List后containscontains()获取长度length属性size()方法子集截取Arrays.copyOfRange()subList()排序Arrays.sort()Collections.sort() 或 list.sort()判空自己判断 length 0isEmpty()这种差距在实际开发里意味着什么意味着如果你定义的是String[]想在中间删一个元素你得写一大堆System.arraycopy的样板代码而用List一行list.remove(index)搞定。2.3 空值、可变性与视图问题最容易翻车的地方先说空值二者都允许存入null。但这里有个容易出问题的地方如果你用Arrays.asList(arr)来转List里面存的null元素也会跟着转过去后续调用一些方法比如Java 8之后的list.stream().map(String::trim)会直接NPE。再说可变性。数组本身就是内容可变但长度不可变但如果你用了Arrays.asList()事情就变得微妙了String[] arr {a, b}; ListString list Arrays.asList(arr); list.set(0, x); // 允许arr[0]也变成了x list.add(c); // 运行时UnsupportedOperationException这套组合拳下来很多人就懵了为什么set可以、add不行原因是Arrays.asList()返回的Arrays$ArrayList直接复用了传入的数组作为内部存储所以修改元素会同步到原数组但它没有实现add/remove的扩容逻辑所以调用这两个方法就直接抛异常。这是高频面试题也是实际开发里很能坑人的点。3. 性能差异什么时候该选数组3.1 内存布局对CPU缓存的友好度数组在内存中是连续的遍历的时候CPU能按顺序预加载对缓存极其友好。ArrayList底层也是数组所以绝大多数场景下它的遍历性能和数组几乎一致。但是LinkedList就不一样了它每个节点都是独立分配的对象内存不连续遍历的时候要频繁跳转缓存命中率很低。所以在性能敏感的循环里用数组或ArrayList别碰LinkedList这个结论在性能测试中反复被验证过。如果你的场景是只读、固定数据量比如配置文件里的白名单列表用String[]存下来遍历性能理论上比ArrayList好一点点——但说实话在业务开发中这点差异通常可以忽略不计。真正有数量级差别的是针对几百万数据做高频随机访问的场景。3.2 扩容成本ArrayList的扩一下和数组的重新来ArrayList每次扩容底层都是新数组加一次数组拷贝。// 源码层面大概这样jdk1.8之后略有调整 int newCapacity oldCapacity (oldCapacity 1); // 1.5倍扩容 elementData Arrays.copyOf(elementData, newCapacity);如果你提前知道数据量最好在初始化的时候就把容量给足ListString list new ArrayList(10000);这样能省去多次扩容的复制开销。而数组呢数组根本不存在扩容你从一开始就得把容量想明白。如果你在循环里反复做arr Arrays.copyOf(arr, arr.length 1)不仅代码难看性能也很糟糕。3.3 实操中的性能经验值我个人在项目里做过一个简单对比往容器里塞100万条字符串再遍历求和。数组和ArrayList的时间差基本在毫秒级以内但内存占用上ArrayList因为扩容余量、对象头等开销会比定长数组多个十几MB。在数据量极大的场景比如几千万条日志记录这种差异就会变得不可忽视。所以记忆方法很简单性能敏感、数据量极大、只读遍历为主选数组数据量中等、需要增删改查、要跟集合框架的算法配合选ArrayList。4. 转换方法全梳理数组和List之间如何无痛切换实际开发中我们常常需要在这两者之间来回转换比如查出来的数据是数组但业务逻辑里要做筛选转成List顺手又舒服。但转换是有讲究的下面逐个来拆。4.1 数组转List不要只会Arrays.asList最常见的写法String[] arr {a, b, c}; // 方式一固定大小视图 ListString view Arrays.asList(arr); // 方式二真正的可变列表 ListString list new ArrayList(Arrays.asList(arr)); // 方式三Java 8的Stream写法 ListString streamList Arrays.stream(arr).collect(Collectors.toList());方式一得到的那个List就一个只读视图不能add也不能remove。方式二才是真正的ArrayList可以随便改。方式三在新代码里也用得比较多官方推荐。这里有个大坑必须重点说基本类型数组不能用Arrays.asList这么转。比如int[] arr {1, 2, 3}你会得到Listint[]长度是1而不是3。想转Integer的List得这样ListInteger list Arrays.stream(arr).boxed().collect(Collectors.toList());热搜词里那个wsl --list --online 连接超时虽然不是Java范畴但list转分组后、list转类型这些热词说明大量开发者都在纠结转换问题。转换本身不难难的是知道每种转换背后的数据结构和限制。4.2 List转数组toArray的两个版本ListString list new ArrayList(Arrays.asList(a, b)); // 方式一无参版返回Object[] Object[] objArr list.toArray(); // 方式二传入长度为0的数组推荐 String[] arr list.toArray(new String[0]); // 方式三传入长度刚好等于list.size()的数组 String[] arr2 list.toArray(new String[list.size()]);无参版本返回的是Object[]你没法直接强转成String[]因为它们的运行时类型不一样强转直接ClassCastException。所以标准做法是toArray(new String[0])。有人觉得传0有点浪费想用方式三但实际上在JDK 8之后两者性能差异已经被优化得几乎没有了甚至new String[0]在某些版本里比new String[size]更快因为它避免了额外的边界判断。所以无脑用new String[0]就好了。4.3 经常和List一起用的几个小工具现在写Java代码很多常用的List操作其实都不需要自己造轮子了。搜到的热搜词里正好有java 获取两个list 交集和java单个对象转list这里一并给出来。两个List交集最简单的方案ListString intersection listA.stream() .filter(listB::contains) .collect(Collectors.toList());如果数据量大可以先转Set再过滤把O(n*m)降成O(nm)。单个对象转List经典做法ListString single Collections.singletonList(value); // 或者 ListString single List.of(value); // Java 9注意Collections.singletonList返回的是不可变单元素列表只能读不能改List.of同理。想要可变的话包一层new ArrayList(...)就行。5. 实际开发中的选型建议到底该用谁5.1 结合场景来选说了这么多理论落到实际项目里到底怎么选我自己总结了几个非常具体的判断标准参数传递场景方法的入参不确定会传多少个值优先用ListString调用方可以传任意长度的数据。用数组的话调用方每次都得先建数组很别扭。固定枚举值比如状态码列表、角色列表这类固定不变的数据用String[]声明成常量简单直观。返回值的灵活性方法返回值建议用ListString因为后续很可能要在这个返回结果上做过滤、排序、去重等操作List支持得更全面。高性能数值计算如果是对大量String做高频拼接、查找、排序数组 手写双指针循环性能上限更高。但说实在的业务系统里不太容易遇到这种瓶颈。5.2 从API设计的角度看从团队协作和代码可维护性角度看List的语义更丰富可读性更好。你看这段代码// 方法签名 public ListString findNamesByGroup(String groupId)比这种public String[] findNamesByGroup(String groupId)返回List的话调用方直接能拿到一个集合可以马上调用isEmpty()、contains()而数组还得自己想办法。同时接口文档、JSON序列化这些方面List和数组的表现也不一样。用Jackson或Gson序列化时ListString和String[]序列化成JSON看起来是一样的字符串数组[a,b]但反序列化的时候数组类型和List类型的处理方式有细微差异如果你声明的字段类型和实际数据不匹配可能出现类型转换异常。这里我的建议是如果这个字段将来可能被后续业务扩展比如增加过滤、分页的逻辑就优先用List。5.3 多维场景的区别还有一个常被忽略的点就是二维甚至三维结构。String[][]和ListListString在动态性上有本质差异。// 数组每一维长度都得固定 String[][] matrix new String[3][]; // List每一行都可以自由增删 ListListString rows new ArrayList(); rows.add(new ArrayList(Arrays.asList(a, b)));如果你要处理的是动态数据比如Excel导入的行列数据、不定长的表格结构用ListListString会舒服得多。用二维数组存动态数据你大概率会陷入每加一行就要重新copy一次数组的尴尬。6. 常见问题与排查心得6.1 高频报错与解决对照表报错/问题场景原因与解决UnsupportedOperationException对Arrays.asList的返回值调用add/removeArrays$ArrayList不可变改用new ArrayList(Arrays.asList(...))ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;对list.toArray()的无参返回值强转String[]toArray()无参返回Object[]用toArray(new String[0])ArrayIndexOutOfBoundsException给数组下标12赋值但数组长度只有10数组长度固定检查写入逻辑或改用ListNullPointerExceptionlist里有null转stream后调trim转之前做非空过滤集合元素被意外修改Arrays.asList(arr)的返回值修改后数组同步变化asList返回的是原数组的视图修改会同步到arr上注意隔离6.2 几个日常容易忽略的细节第一List.of()和Collections.unmodifiableList都不能add/remove但是它们处理null的策略完全不同。List.of()遇到null直接NPECollections.unmodifiableList则允许null存在但拒绝修改。如果代码里本来就可能存在null元素千万别用List.of()去包装。第二数组虽然看起来原始但它在泛型擦除的时代有个list无法企及的好处数组类型信息在运行时是保留的。泛型在编译后会被擦除ListString运行时的信息其实是List你拿不到它的元素类型而String[]在运行时是可以查询到组件类型的反射场景中处理数组会更直接。第三search到的热词里 stringbuffer转换为string、private void init(){ intent intent getintent(); ...} 这类内容很多是和Android开发相关的。在Android里一个很典型的误区是用数组传来传去导致代码冗余还容易越界。如果你在Android里做数据传递比如Intent传String数组或ArrayList要注意putExtra(String, String[])和putStringArrayListExtra是有区别的接的时候也要选对方法。别图省事该用ArrayList就老老实实传ArrayList。6.3 一个值得记住的底层细节ArrayList的默认容量是10。如果你只有一个元素但它初始扩容时可能分配了10个槽位剩下9个空位会被GC再回收吗不会对象引用的默认值是null这9个空引用会在数组被整体回收时才释放。如果你创建了几十万个new ArrayList()每个都只放一个元素那内存浪费就很可观了。所以有一个比较极端的优化建议对那种数量固定、变化不大的场景直接使用数组连创建ArrayList的开销都省掉。反过来凡是需要动态增长、需要集合算法配合的场景ArrayList一剑封喉。7. 收尾前再聊几句从我个人的经验来看String[]和List 的选择问题本质上不是一个谁替代谁的判断题而是什么场景用什么工具的选题。真正成熟的开发者不会执着于我一定只用List或者数组性能好所以都用数组而是会结合数据结构特点、代码可读性、团队协作成本来做取舍。每次在代码评审里看到有人用String[]存动态数据我都会多问一句这里的数据量在写代码的时候就能确定吗如果是事后拿到动态个数的数据比如前端传过来的不定长数组不转成List后面各种字符串筛选、拼接都会变得难受。反之如果你定义了一个常量白名单它永远不会变写String[]就比ListString少了一层封装清晰利落。如果你正在面试还有一个小技巧面试官问到这个话题时不要说数组长度固定List可以变长就结束再补一句ArrayList底层也是数组它通过扩容机制实现了动态长度且数组是协变而泛型是不可变的——这一下就能拉开和普通候选人的差距。最后送上一句实用经验在没有明确性能压力的情况下倾向于写ListString它给你的操作空间更大后续迭代不必因为容器选错而大规模改代码。如果代码里到处都是Arrays.asList和toArray的转换那大概率是你数据结构选错了而不是转换老写不对。
返回列表