孤尽大佬也中招:Java toArray方法的正确打开方式

共 3624字,需浏览 8分钟

 ·

2024-12-03 17:41

你知道的越多,不知道的就越多,业余的像一棵小草!

你关注,我们一起精进!你星标,我们便有了更多故事!

编辑:业余草

来源:juejin.cn/post/6844904034076377095

推荐:https://t.zsxq.com/le3MZ

前言

今天群里有人截图了一个资料,无意间看到是华山版《Java开发手册》独家讲解https://developer.aliyun.com/live/1201当时的直播视频。

视频是很早之前直播的,当时Java开发手册发行到 1.5 这个版本的时候,对孤尽大佬的一个采访。视频中也有孤尽对几道易错题的讲解,有兴趣的同学可以点击上面的链接去看一看。

孤尽你不知道的话,你一定听过阿里巴巴Java开发手册(现在已经改为《Java开发手册》),孤尽就是推广创作这本手册的人。

正文

今天说的坑就来源于上文提到的采访视频,孤尽提到了关于集合的 toArray 方法,以前的使用方式是“采用传入一个与集合数量大小一致的数组”,也就是如下这种写法。

List<String> list = ....
String[] array = list.toArray(new String[list.size()]);

诶,看到这笔者心里有一波小慌,这不就是我平常用的方法吗,用了这么长时间,难道是错的???

视频中孤尽说到,有一些同学手册中的这条进行了反馈,手册也进行了修改,大概意思是“传入空数组的性能会更好”,写法如下:

List<String> list = ....
// 下面的数组内初始大小是0
String[] array = list.toArray(new String[0]);  

平时写顺手的一个方法,居然不是最优的,看到这里笔者决定调查一下。

分析Java开发手册中该内容

首先既然视频中提到了对java开发手册的修改,那么就先去看看关于这个方法,手册中修改了那些内容。

  • 下图为1.4版本的阿里巴巴java开发手册关于toArray的截图
  • 下图为1.5版本的阿里巴巴java开发手册关于toArray的截图

首先说明了不要使用无参的toArray(),这个好理解。众所周知无参的toArray返回的是Object[],泛型丢了,强转还报错,这个没有什么争议。

但是关于传入数组的地方修改了,新版本手册的确是更新为传入的是类型完全一 致、长度为 0 的空数组。

下面也是增加了4条说明,说明了其实数组大小不一致并不会报错,但是234情况都有负面影响。

看到说明,如果同为踩过这个坑的同学可能恍然大悟,不过可能还有同学没反应过来,笔者把toArray(T[] a)的源码粘贴过来,你可能就明白了。

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a‘s runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

配合源码,再看手册更新的四条说明就非常清晰了:如果你传 0 或者小于实际大小的数字时,都是直接进了第一个if判断,copyOf一个新的数组,和你传入的大小就没有关系了。

  1. 假设集合大小是3,你传入的是new String[2],效果和new String[0]是一样的,却白白浪费了2个空间,对应手册中增加GC负担的说法。
  2. 假设集合大小是3,你传入的是new String[list.size()],在并发场景下,String[3]这个数组已经创建成功,但是list又增加了元素,效果和上一条一样,浪费了空间。

其实说到这里,笔者要说的这个坑已经很清晰了,以前的写法虽说不是bug,但是并不是最优写法。

严谨一点不是坏事,我们看看其他人怎么说。

StackOverflow中相关问题的回答

笔者也查到了一个评论很精彩的 StackOverflow 中相关问题https://stackoverflow.com/questions/174093/toarraynew-myclass0-or-toarraynew-myclassmylist-size,我截取了采纳回答(但是不是票数最高)中的部分截图

图中的答主也贴出了测试代码(详见链接),对两种方式进行了测试,结论和最新版手册是一样的。大致意思是:

preSize是老写法,也就是传集合长度方式, resize是传长度为0的数组的方式。

两种写法在不同数据量(n)的场合下,得分越拉越大,得分(score)是运行时长,肯定是越小越好。有兴趣的同学可以去链接中,将该答主的测试用例粘贴过来试一试。
如果不了解JMH基准测试的盆友,可查阅一下相关资料。

关于Idea的提示

其实有些同学知道我们常用的ide,也就是idea中就有关于这个问题的提示文字https://github.com/JetBrains/intellij-community/blob/master/plugins/InspectionGadgets/src/inspectionDescriptions/ToArrayCallWithZeroLengthArrayArgument.html,笔者测试的时候,idea就把它提示了出来,是一个性能的警告提示⚠️。

笔者直接翻译过来粘贴一下:

有两种写法可以将集合转换为数组:使用预设大小数组(例如c.toArray(new String [c.size())]))或使用空数组(例如c.toArray(new String [0]))。

在较早的Java版本中,建议使用预设大小的数组,因为创建适当大小的数组所需的反射调用非常慢。但是,由于OpenJDK 6的更新较晚,因此此调用变得很有趣,与空数组版本相比,空数组版本的性能相同,有时甚至更好。同样,传递预大小的数组对于并发或同步收集也是危险的,因为在size和toArray调用之间可能会发生数据争用,如果在操作期间同时缩小了集合,则在数组末尾可能会导致额外的null。

此检查允许遵循统一的样式:使用空数组(在现代Java中建议使用)或使用预定大小的数组(在较早的Java版本或基于非HotSpot的JVM中可能更快)。

总结

经过今天的调查,以后集合转数组大家是不是都会用传入空数组的写法了呢😁

同样,再优秀的文献都有可能存在失误的情况,我们保持敬佩之心阅读的时候,又不能丢失严谨!

浏览 60
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报