我在 Java 里用 Arrays.equals 比较两个内容明明一模一样的二维数组结果它死活返回 false,我又用 Arrays.toString 想打出来看看却只看到一堆像 [I@1b6d3586 这样的乱码,折腾很久才搞懂 Arrays.equals 和 toString 只往下看一层对数组套数组这种嵌套结构根本不会递归深入比较的深度复盘

我有两个二维数组 int

我在 Java 里用 Arrays.equals 比较两个内容明明一模一样的二维数组、结果它死活返回 false,我又用 Arrays.toString 想打出来看看、却只看到一堆像 [I@1b6d3586 这样的乱码,折腾很久才搞懂 Arrays.equals 和 toString 只往下看一层、对数组套数组这种嵌套结构根本不会递归深入比较的深度复盘

这次踩的坑,根子是我没分清一件很基础却很要命的事:对一个有嵌套层次的数据结构做"比较"或"打印",到底是只看最外面那一层,还是一层层钻到底?我默认它会钻到底,可它偏偏只看了一层。

故障现场:内容相同的二维数组,equals 返回 false

我有两个二维数组,内容完全一样,需要判断它们相不相等。我自然想到了 Arrays.equals:

int[][] a = {{1, 2}, {3, 4}};
int[][] b = {{1, 2}, {3, 4}};

System.out.println(a == b);                  // false(这个我懂, 是两个不同对象)
System.out.println(a.equals(b));             // false(数组没重写 equals)
System.out.println(Arrays.equals(a, b));     // false! —— 这个让我懵了
// 内容明明一模一样, Arrays.equals 怎么也是 false?

System.out.println(Arrays.toString(a));      // [[I@1b6d3586, [I@4554617c
// 想打出来看看, 结果是一堆乱码, 根本看不到 1234

我当时一连串问号:

  • Arrays.equals 居然 false:我知道 a == ba.equals(b) 因为数组没重写 equals 会是 false,所以才特意用了 Arrays.equals,可它对内容完全相同的二维数组还是返回 false
  • 一维数组它又是对的:我退一步测了一维——Arrays.equals(new int[]{1,2}, new int[]{1,2}) 返回 true,没问题。怎么一到二维就不行了?
  • toString 打出一堆乱码:想 Arrays.toString 打印二维数组看看,结果是 [[I@1b6d3586, ...] 这种,根本看不到里面的数字
  • 换个方法就对了:后来同事提醒我用 Arrays.deepEqualsArrays.deepToString,一换,equals 立刻 true、打印也正常显示 [[1, 2], [3, 4]] 了。

"一维对、二维错;deep 版本就对"——这组对比把问题指得清清楚楚:Arrays.equalsArrays.toString 只"往下看一层";对一维数组,这一层就是元素本身(数字),所以对;可对二维数组,这"一层"是里面的那些一维数组(也是对象),它只比/只打这些内层数组对象本身,而不会再钻进去比较/打印它们的内容。我得去搞清楚,Java 里""和""操作到底差在哪。

第一件事:搞懂 Arrays.equals/toString 是"浅"的,只比/打第一层元素

带着"只看一层"的线索去翻 Java 文档,我才算真正理解了这对方法的边界——Arrays.equals(a, b)Arrays.toString(a) 都是""(one-level)操作:它们只处理数组的第一层元素,对每个元素用 equals(比较时)或 toString(打印时),而不会递归地深入到"元素本身又是数组"的情况里去。

关键要叠加上 Java 的另一个事实:数组本身没有重写 equalstoString数组继承自 Object,它的 equals 就是引用相等(==)toString 就是类型@哈希码(那串乱码的来源)。把这两点合起来:

  • 对一维 int[]:Arrays.equals 逐个比较第一层元素(也就是一个个 int),int 的相等就是值相等,所以内容相同就 true ✅;
  • 对二维 int[][]:它的"第一层元素"是一个个 int[](内层一维数组,是对象);Arrays.equals 拿这些内层数组对象去比,用的是它们各自的 equals——而数组的 equals 是引用相等!两个内容相同但不同的内层数组对象,引用不等,所以判 false,导致整个二维比较 false ❌;
  • toString 同理:对二维数组,它对每个内层 int[] 调默认的 toString,得到 [I@哈希码 那串东西,所以打出来是乱码。

这下全说通了:不是 Arrays.equals 坏了,而是它"只往下看一层";对二维数组,它看到的第一层是内层数组对象,而它对这些对象用的是数组默认的"引用相等",自然内容相同也判不等。我把浅与深的差别验证清楚:

int[][] a = {{1, 2}, {3, 4}};
int[][] b = {{1, 2}, {3, 4}};

// 浅:只比第一层元素(内层数组对象), 用它们的引用相等 -> false
System.out.println(Arrays.equals(a, b));       // false
// 深:递归比较到每一层的真正元素 -> true
System.out.println(Arrays.deepEquals(a, b));    // true ✅

// 浅打印:内层数组调默认 toString -> 乱码
System.out.println(Arrays.toString(a));         // [[I@..., [I@...]
// 深打印:递归打印每一层 -> 正常
System.out.println(Arrays.deepToString(a));     // [[1, 2], [3, 4]] ✅

真相大白:错不在 equals,而在于我对一个有嵌套层次的结构(数组套数组),用了一个只处理一层的""方法,还想当然地以为它会替我钻到底比较内容;而数组又恰好没重写 equals/toString,于是""在嵌套层上就退化成了引用比较和乱码打印。要正确处理嵌套结构,得用会递归深入每一层的""方法。

第二件事:正解——嵌套结构用 deep 系列,按"内容相等"需求选对工具

根因是"对嵌套结构用了只看一层的浅方法",那正解的核心就一句话:处理有嵌套层次的数组,用会递归深入每一层的 deep 系列方法;按"内容相等"做比较/做 key 时,确认你用的工具是按内容还是按引用。

int[][] a = {{1, 2}, {3, 4}};
int[][] b = {{1, 2}, {3, 4}};

// 1) 比较嵌套数组内容:用 deepEquals(递归到底)
Arrays.deepEquals(a, b);        // true
// 一维数组就用 equals 即可:Arrays.equals(x, y)

// 2) 打印嵌套数组:用 deepToString
Arrays.deepToString(a);         // "[[1, 2], [3, 4]]"

// 3) 嵌套数组的哈希:用 deepHashCode(配合 deepEquals 才一致)
Arrays.deepHashCode(a);

// 4) 想按"内容"把数组当 Map 的 key:别直接用数组(引用相等会失效)
// 改用 List(它重写了 equals/hashCode 按内容比较):
Map, String> m = new HashMap<>();
m.put(List.of(1, 2), "x");
m.get(List.of(1, 2));           // "x" —— List 按内容匹配, 取得到

这里的关键判断有两步:第一,这个结构有没有嵌套层次?有(数组套数组、数组套对象),就别用浅的 equals/toString/hashCode,要用对应的 deepEquals/deepToString/deepHashCode,它们会一层层递归下去。第二,我依赖的"相等"是按内容还是按引用?——数组、以及任何没重写 equals 的类,默认是按引用(身份)相等;要按内容相等(比如内容相同就算同一个 key),得用本身就按内容比较的类型(如 ListString、重写了 equals 的值对象),或显式用 Arrays.equals/deepEquals 来比。

核心就一条:嵌套数据用会钻到底的 deep 方法;依赖"内容相等"前,先确认这个类型到底是按内容还是按引用比较的。

第三件事:同一类"对有层次的结构只做了浅层处理"的坑,我后来又撞见好几个

这次踩坑让我看清一个反复出现的模式:很多"比较、复制、转换、相等"这类操作,天然有""(只处理最外一层)和""(递归处理每一层)两个版本;而默认/最顺手的那个,往往是浅的;一旦数据有嵌套层次,浅操作就只触及表层、对内层要么用引用比较、要么原样照搬,埋下隐患。这种坑到处都是:

  • 浅拷贝 vs 深拷贝:浅拷贝只复制最外层、内层对象还是和原来共享同一个引用,改副本的内层连原件一起改了。
  • 对象 equals 只比第一层字段:自定义 equals 只比较了顶层基本字段,内嵌的对象/集合没比,导致"内容相同"判错。
  • JSON/序列化的浅合并:配置 merge 时只合并第一层 key,嵌套对象被整个覆盖而非深度合并。
  • Object.assign / 展开运算符:JS 里这些都是浅拷贝,嵌套对象仍共享引用。
  • 集合的 contains 对元素:依赖元素的 equals,元素若是没重写 equals 的数组/对象,contains 按引用找,内容相同也找不到。

它们的内核是同一个:当数据有嵌套/层次时,"处理它"这个动作就有了"处理到第几层"的问题;""操作只负责最表面那一层,对更深的层次,它要么不碰(原样保留引用),要么用默认的(通常是引用)语义去处理;而我们脑子里想要的,往往是"把整个结构、连同它所有层次都处理到位"的语义。这两者的落差,在数据""的时候看不出来,一旦"有嵌套"就暴露。所以,面对任何有嵌套层次的数据,做比较/拷贝/转换前都要先问:我需要的是浅的(只一层)还是深的(到底)?我用的这个方法,是哪一种?我把这套判断画成了一张图(见后文)。

操作 浅(默认/顺手, 只一层) 深(递归到底)
比较嵌套数组 Arrays.equals(引用比内层) Arrays.deepEquals
打印嵌套数组 Arrays.toString(内层乱码) Arrays.deepToString
哈希嵌套数组 Arrays.hashCode Arrays.deepHashCode
拷贝带嵌套的对象 浅拷贝(内层共享引用) 深拷贝
合并嵌套配置 浅合并(覆盖整个子对象) 深度合并

第四件事:Arrays 浅方法 vs deep 方法——一张对照表

这次事故逼我把 Arrays 的浅/深方法摆成一张表,以后处理数组前先按维度对照:

需求 一维数组用 多维/嵌套数组用
判内容相等 Arrays.equals(a, b) Arrays.deepEquals(a, b)
打印查看 Arrays.toString(a) Arrays.deepToString(a)
算哈希 Arrays.hashCode(a) Arrays.deepHashCode(a)
嵌套层用浅方法会 —(一维就够) 退化成引用比较/乱码打印
直接 a.equals(b) 引用相等(没重写) 引用相等(没重写)

看清这张表,规矩就明确了:一维数组浅方法(equals/toString/hashCode)就够;只要是多维或嵌套(数组里还有数组/对象),一律用对应的 deep 版本(deepEquals/deepToString/deepHashCode);而数组的 a.equals(b) 永远是引用相等,别指望它比内容。维度一上去,浅就不够了。

第五件事:我曾经对数组相等和打印想当然的几个误区

这场"内容相同却 false"的事故,把我对数组的一堆想当然照得清清楚楚:

我以为 实际上
Arrays.equals 能比任意数组内容 只比第一层、对嵌套退化成引用比较
a.equals(b) 比的是数组内容 数组没重写 equals、比的是引用
一维对了多维也一样对 多维第一层是数组对象、引用比就 false
Arrays.toString 能打出二维内容 内层数组打成 [I@hash 乱码、要 deepToString
equals 默认就是按内容比较 很多类型默认按引用、看有没有重写
数组能直接当 HashMap 的 key 引用相等、内容相同的新数组取不到

这些误区的根子是同一个:我把"相等""转字符串"这类操作,默认想象成了一个"无视层次、一钻到底、按内容"的理想动作,完全没意识到它们其实有"处理到第几层"的边界,而默认的那个版本只处理一层;更没意识到数组这种类型连最基本的"按内容相等"都没实现(没重写 equals)。正因为脑子里没有"浅与深""按引用还是按内容"这两个维度的区分,我才会拿一个浅的、按引用的工具,去干一个需要深的、按内容的活。把一个有"浅/深"之分的操作默认当成深的、把一个可能"按引用"的相等默认当成按内容的,是这类嵌套结构处理错误的共同根源。

第六件事:比较/拷贝/打印带嵌套的数据时,我现在的自检习惯

现在每当我要比较、拷贝、打印一个数据结构、或排查"内容相同却不相等",我都会先问"它有没有嵌套层次"。先看清浅方法对嵌套为什么退化:

然后用这张自检图选对工具:

配套地,我把"按内容把数组当 key"这个最容易栽的点固化成了对照写法:

// 反例:用数组当 HashMap 的 key, 内容相同的新数组取不到(引用相等)
Map bad = new HashMap<>();
bad.put(new int[]{1, 2}, "x");
bad.get(new int[]{1, 2});          // null! 不同对象、hashCode 不同

// 正例:用 List 当 key(List 按内容重写了 equals/hashCode)
Map, String> good = new HashMap<>();
good.put(List.of(1, 2), "x");
good.get(List.of(1, 2));           // "x" 内容匹配

// 或显式比较内容(不做 key 时)
Arrays.equals(arr1, arr2);         // 一维
Arrays.deepEquals(m1, m2);         // 多维

这套习惯的精髓,是"先问有没有嵌套、有就用 deep 方法递归到底、依赖内容相等就先确认类型是按内容还是按引用"它让我从"equals 就是比内容、随手用",变成了"先看层次和相等语义、再选浅深和工具"——核心始终是:在 Java 中 Arrays.equals(a,b) Arrays.toString(a) Arrays.hashCode(a) 都是只处理数组第一层元素的浅操作——它们对第一层的每个元素分别调用 equals(比较时)或 toString(打印时)或算 hashCode,而不会递归深入到元素本身又是数组的情况;同时数组类型本身没有重写从 Object 继承来的 equals 和 toString,所以数组的 equals 是引用相等(==)、toString 是类型@哈希码;两者叠加的结果是:对一维数组 Arrays.equals 逐个比第一层的基本类型元素(int 等按值比)所以内容相同就 true、Arrays.toString 也能正常打印;但对二维或嵌套数组它的第一层元素是内层数组对象,Arrays.equals 拿这些内层数组对象用它们各自的 equals(即引用相等)去比、于是内容相同但不同的内层数组对象引用不等导致整体判 false,Arrays.toString 对内层数组调默认 toString 打出 [I@哈希码 乱码;所以处理有嵌套层次的数组要用会递归深入每一层的 deep 系列方法——比较内容用 Arrays.deepEquals、打印用 Arrays.deepToString、算哈希用 Arrays.deepHashCode(deepEquals 与 deepHashCode 要配套);此外想按内容把数组当 HashMap 的 key 是不行的(数组按引用相等、内容相同的新数组算出不同 hashCode 且 equals 不匹配取不到),应改用本身就按内容重写了 equals/hashCode 的 List(如 List.of(1,2))做 key、或把内容显式比较;更一般地,很多比较复制转换相等这类操作天然有浅(只处理最外一层)和深(递归处理每一层)两个版本、而默认或最顺手的那个往往是浅的(浅拷贝、对象 equals 只比顶层字段、配置浅合并、JS 的 Object.assign 和展开都是浅拷贝、集合 contains 依赖元素 equals),一旦数据有嵌套层次浅操作就只触及表层、对更深的层次要么原样保留引用要么用默认(通常是引用)语义处理、与我们想要的把整个结构连同所有层次都处理到位的深语义产生落差(数据平时看不出有嵌套就暴露);所以面对任何有嵌套层次的数据做比较拷贝转换前都要先问我需要的是浅的还是深的我用的这个方法是哪一种、并且依赖内容相等前要先确认这个类型默认是按内容还是按引用比较(数组及任何没重写 equals 的类默认按引用、String/List/值对象按内容)。

我立下的几条规矩

这场"内容相同却 false"的事故,换来了我处理数组和嵌套数据时,刻进骨子里的几条铁律:

  1. Arrays.equals/toString/hashCode 是浅的,只处理第一层。
  2. 数组没重写 equals,a.equals(b) 永远是引用相等。
  3. 多维/嵌套数组一律用 deepEquals/deepToString/deepHashCode。
  4. deepEquals 和 deepHashCode 要配套用,保持一致。
  5. 别拿数组当 HashMap 的 key,要按内容做 key 用 List。
  6. 依赖内容相等前,先确认类型按内容还是按引用比。
  7. 有嵌套就想浅/深:默认多是浅的,要到底就用深的。

附:一段数组比较/打印/做 key 的浅深速查清单

最后留一段我自己处理数组时照着用的浅深速查清单:

import java.util.*;

int[]   a1 = {1, 2, 3};
int[][] a2 = {{1, 2}, {3, 4}};

// === 1. 比较内容:一维 equals, 多维 deepEquals ===
Arrays.equals(a1, new int[]{1, 2, 3});          // true
Arrays.equals(a2, new int[][]{{1,2},{3,4}});    // false! 嵌套退化成引用比
Arrays.deepEquals(a2, new int[][]{{1,2},{3,4}}); // true  ✅

// === 2. 打印:一维 toString, 多维 deepToString ===
Arrays.toString(a1);        // "[1, 2, 3]"
Arrays.toString(a2);        // "[[I@..., [I@...]"  乱码
Arrays.deepToString(a2);    // "[[1, 2], [3, 4]]"  ✅

// === 3. 哈希:deepEquals 必须配 deepHashCode ===
Arrays.hashCode(a1);
Arrays.deepHashCode(a2);

// === 4. 按内容做 Map key:别用数组, 用 List ===
Map, String> m = new HashMap<>();
m.put(List.of(1, 2), "x");
m.get(List.of(1, 2));       // "x"  ✅ (用 int[] 当 key 则取不到)

// === 5. 牢记:数组的 a.equals(b) 永远是引用相等(没重写) ===
a1.equals(new int[]{1, 2, 3});  // false! 要内容比较请用 Arrays.equals

这段速查的核心就一句:一维用 equals/toString/hashCode,多维一律换 deep 版本;数组的 a.equals(b) 是引用相等别指望比内容;按内容做 key 用 List 不用数组。把"随手 equals/toString"换成"按维度选浅深",那些"一样却不等、打印成乱码、key 取不到"的怪事就再也不会找上门了。

多说一句:这套浅与深的分别,不只在 Java 数组上成立——它几乎是一切处理有层次数据的语言和库都要面对的共同课题。你在 Python 里会遇到 copydeepcopy,在 JavaScript 里会遇到浅展开与结构化克隆,在各种配置库里会遇到浅合并与深度合并。名字各不相同,可背后是同一个朴素的道理:数据有多深,你的处理就得想清楚要走多深。把这个"浅深之问"养成习惯,胜过记住任何一个具体方法名。

写在最后

回头看,这场由"Arrays.equals 只看一层"引发的"内容相同却不等"事故,真正教给我的,远不止"嵌套数组用 deepEquals"这一个技巧。它让我对"'处理一个东西'这件事,当这个东西有层次、有深度时,就多出了一个我们常常忽略的维度——到底处理到多深;而我们的默认想象,总是'一钻到底',工具的默认行为,却往往'只碰表面'",有了一次刻骨的体会。我栽跟头,是因为我把"比较两个东西相不相等"想象成了一个朴素、彻底、无视结构的动作——在我朴素的直觉里,"相等"就是"里里外外、每一个角落都一样",它理应钻到数据的最深处去逐一核对;我完全没意识到,当数据有嵌套时,"比较"就分裂成了"只比表面一层"和"层层比到底"两种截然不同的操作,而我顺手抓起的 Arrays.equals,恰恰是前者;它尽职地比了第一层,可第一层那些"元素"本身又是一个个数组,它就拿这些数组的身份(引用)去比了——于是"内容彻底相同"和"它判定的相等",在嵌套这一层悄然分了家。这让我领悟到一个关于"深度与默认"的深刻认知:凡是面对有层次、有嵌套的结构,任何"遍历它、比较它、复制它、转换它"的操作,都隐含着一个"作用到第几层"的问题;而出于性能、简单、或单纯因为浅层是最自然的默认,绝大多数操作的默认行为都是""的——只处理最外面那一层,对更深处要么原样不动、要么用最基础(往往是按身份)的方式草草带过;这与我们脑海里"处理一个东西就该把它整个处理透"的语义,存在一道隐形的鸿沟;这道鸿沟平时被"数据恰好是扁平的"掩盖着,一旦数据长出了嵌套的层次,它就会以"内容相同却不等、改了副本连累原件、打印出一堆乱码"的形式,猝不及防地显形;所以,真正的清醒,是在面对任何带层次的数据时,都把"浅还是深"当成一个必须主动回答的问题:先看清数据有几层,再想清楚自己要的是触及表面还是贯穿到底,然后有意识地选用对应深度的工具——而不是抓起最顺手的那个、默认它会替我钻到底这给了我一种面对"一切'对有结构的东西做整体操作'之事"时的审慎:每当我要比较、拷贝、合并、转换一个可能有嵌套的数据时,我都先问"它有几层?我要的是浅(只一层)还是深(到底)?我手里这个方法,是哪一种?它对内层用的是按内容还是按引用"——有层次就分浅深、按需求选对深度的工具、别默认顺手的那个会贯穿到底;"带层次的数据要分清浅与深、按需选对深度的操作",是用对 Arrays、也是正确处理一切嵌套结构的关键认清 Arrays.equals 只比一层、数组没重写 equals、嵌套要用 deep 方法——这,是我用一次"两个一样的二维数组死活不相等"的事故,换来的、关于 Java、也关于如何在有深度的数据前分清浅深的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次比较或打印一个多维数组时,手指顿一下、把 equals 改成 deepEquals、把 toString 改成 deepToString,那我对着那两个"明明一样却不等"的数组发的那阵呆,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我给一个带 sync.Mutex 的 Go 结构体写了用值接收者的方法又到处用值传递把它传来传去、自以为加了锁就线程安全了,结果高并发下那份本该被锁保护的数据还是被改乱了计数器还是丢更新,排查很久才搞懂我每次值拷贝这个结构体时把里面的锁也一起复制成了另一把副本锁的根本不是同一把锁的深度复盘

2026-6-3 17:12:57

技术教程

我图省事把上万个 ID 一股脑塞进 SQL 的 WHERE id IN (...) 里去批量查询、小批量测的时候快得飞起,结果生产环境列表一大这条查询就慢成狗有时还直接报参数过多的错、DBA 看监控说这一条 SQL 把库都拖垮了,排查很久才搞懂一个在小规模下完美的 IN 写法放大到上万个值时会在解析执行计划缓存好几个地方同时崩坏的深度复盘

2026-6-3 17:25:39

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索