You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by sn...@apache.org on 2026/10/17 19:44:07 UTC
(flink) branch master updated: [FLINK-40989][table-planner] Compare `MAP` values by value instead of by reference
This is an automated email from the ASF dual-hosted git repository.
snuyanzin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new bbf37d09946 [FLINK-40989][table-planner] Compare `MAP` values by value instead of by reference
bbf37d09946 is described below
commit bbf37d099464a01facdd3eea944975e4ab04ce4d
Author: Ramin Gharib <ra...@gmail.com>
AuthorDate: Sat Oct 17 21:44:00 2026 +0200
[FLINK-40989][table-planner] Compare `MAP` values by value instead of by reference
---
.../planner/codegen/calls/ScalarOperatorGens.scala | 151 +++++++++----
.../table/planner/functions/MapFunctionITCase.java | 250 ++++++++++++++++++++-
2 files changed, 351 insertions(+), 50 deletions(-)
diff --git a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/ScalarOperatorGens.scala b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/ScalarOperatorGens.scala
index 643e1f9b9dd..bff1d79dd1c 100644
--- a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/ScalarOperatorGens.scala
+++ b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/ScalarOperatorGens.scala
@@ -20,7 +20,6 @@ package org.apache.flink.table.planner.codegen.calls
import org.apache.flink.table.api.{TableRuntimeException, ValidationException}
import org.apache.flink.table.api.config.ExecutionConfigOptions
import org.apache.flink.table.data.binary.BinaryArrayData
-import org.apache.flink.table.data.util.MapDataUtil
import org.apache.flink.table.data.utils.CastExecutor
import org.apache.flink.table.data.writer.{BinaryArrayWriter, BinaryRowWriter}
import org.apache.flink.table.planner.codegen.{CodeGeneratorContext, CodeGenException, EqualiserCodeGenerator, GeneratedExpression}
@@ -1948,64 +1947,118 @@ object ScalarOperatorGens {
val rightTerm = args(1)
val resultTerm = newName(ctx, "compareResult")
+ val sizeTerm = newName(ctx, "size")
+
+ val leftKeysTerm = newName(ctx, "leftKeys")
+ val leftValuesTerm = newName(ctx, "leftValues")
+ val rightKeysTerm = newName(ctx, "rightKeys")
+ val rightValuesTerm = newName(ctx, "rightValues")
+ val rightMatchedTerm = newName(ctx, "rightMatched")
+
+ val leftIndexTerm = newName(ctx, "leftIndex")
+ val rightIndexTerm = newName(ctx, "rightIndex")
+ val unmatchedIndexTerm = newName(ctx, "unmatchedIndex")
+ val laterIndexTerm = newName(ctx, "laterIndex")
+
+ // Reads an element into a typed local. A boxed element would make generateEquals compare
+ // references.
+ def readElement(
+ arrayTerm: String,
+ indexTerm: String,
+ elementType: LogicalType,
+ name: String): (String, GeneratedExpression) = {
+ val elementTerm = newName(ctx, name)
+ val elementNullTerm = newName(ctx, s"${name}IsNull")
+ val code =
+ s"""
+ |${primitiveTypeTermForType(elementType)} $elementTerm =
+ | ${primitiveDefaultValue(elementType)};
+ |boolean $elementNullTerm = $arrayTerm.isNullAt($indexTerm);
+ |if (!$elementNullTerm) {
+ | $elementTerm = ${rowFieldReadAccess(indexTerm, arrayTerm, elementType)};
+ |}
+ |""".stripMargin
+ (code, GeneratedExpression(elementTerm, elementNullTerm, "", elementType))
+ }
- val mapCls = className[java.util.Map[_, _]]
- val keyCls = boxedTypeTermForType(keyType)
- val valueCls = boxedTypeTermForType(valueType)
-
- val leftMapTerm = newName(ctx, "leftMap")
- val leftKeyTerm = newName(ctx, "leftKey")
- val leftValueTerm = newName(ctx, "leftValue")
- val leftValueNullTerm = newName(ctx, "leftValueIsNull")
- val leftValueExpr =
- GeneratedExpression(leftValueTerm, leftValueNullTerm, "", valueType)
-
- val rightMapTerm = newName(ctx, "rightMap")
- val rightValueTerm = newName(ctx, "rightValue")
- val rightValueNullTerm = newName(ctx, "rightValueIsNull")
- val rightValueExpr =
- GeneratedExpression(rightValueTerm, rightValueNullTerm, "", valueType)
-
- val entryTerm = newName(ctx, "entry")
- val entryCls = classOf[java.util.Map.Entry[AnyRef, AnyRef]].getCanonicalName
- val valueEqualsExpr =
- generateEquals(ctx, leftValueExpr, rightValueExpr, new BooleanType(valueType.isNullable))
+ // Two keys match when they are equal or both NULL.
+ def generateKeysMatch(
+ key: GeneratedExpression,
+ otherKey: GeneratedExpression): (String, String) = {
+ val keysEqual = generateEquals(ctx, key, otherKey, new BooleanType(keyType.isNullable))
+ (keysEqual.code, s"${keysEqual.resultTerm} || (${key.nullTerm} && ${otherKey.nullTerm})")
+ }
- val internalTypeCls = classOf[LogicalType].getCanonicalName
- val keyTypeTerm = ctx.addReusableObject(keyType, "keyType", internalTypeCls)
- val valueTypeTerm = ctx.addReusableObject(valueType, "valueType", internalTypeCls)
- val mapDataUtil = className[MapDataUtil]
+ val (leftKeyCode, leftKey) = readElement(leftKeysTerm, leftIndexTerm, keyType, "leftKey")
+ val (rightKeyCode, rightKey) =
+ readElement(rightKeysTerm, rightIndexTerm, keyType, "rightKey")
+ val (keysMatchCode, keysMatch) = generateKeysMatch(leftKey, rightKey)
+ val (leftValueCode, leftValue) =
+ readElement(leftValuesTerm, leftIndexTerm, valueType, "leftValue")
+ val (rightValueCode, rightValue) =
+ readElement(rightValuesTerm, rightIndexTerm, valueType, "rightValue")
+ val valueEqualsExpr =
+ generateEquals(ctx, leftValue, rightValue, new BooleanType(valueType.isNullable))
+
+ val (unmatchedKeyCode, unmatchedKey) =
+ readElement(rightKeysTerm, unmatchedIndexTerm, keyType, "unmatchedKey")
+ val (laterKeyCode, laterKey) =
+ readElement(rightKeysTerm, laterIndexTerm, keyType, "laterKey")
+ val (laterKeysMatchCode, laterKeysMatch) = generateKeysMatch(unmatchedKey, laterKey)
+
+ // Keys are compared with SQL '=' like the values, so each left key scans the right keys.
+ // A map can hold a key more than once, and then its last entry counts. Both scans run from
+ // the end, so a key first finds its last entry, and later visits find it already matched.
+ // An unmatched right entry must be an earlier entry of a key that comes later.
val stmt =
s"""
- |boolean $resultTerm;
- |if ($leftTerm.size() == $rightTerm.size()) {
- | $resultTerm = true;
- | $mapCls $leftMapTerm = $mapDataUtil
- | .convertToJavaMap($leftTerm, $keyTypeTerm, $valueTypeTerm);
- | $mapCls $rightMapTerm = $mapDataUtil
- | .convertToJavaMap($rightTerm, $keyTypeTerm, $valueTypeTerm);
+ |int $sizeTerm = $leftTerm.size();
+ |boolean $resultTerm = $sizeTerm == $rightTerm.size();
+ |$ARRAY_DATA $leftKeysTerm = $leftTerm.keyArray();
+ |$ARRAY_DATA $leftValuesTerm = $leftTerm.valueArray();
+ |$ARRAY_DATA $rightKeysTerm = $rightTerm.keyArray();
+ |$ARRAY_DATA $rightValuesTerm = $rightTerm.valueArray();
+ |boolean[] $rightMatchedTerm = new boolean[$sizeTerm];
|
- | for ($entryCls $entryTerm : $leftMapTerm.entrySet()) {
- | $keyCls $leftKeyTerm = ($keyCls) $entryTerm.getKey();
- | if ($rightMapTerm.containsKey($leftKeyTerm)) {
- | $valueCls $leftValueTerm = ($valueCls) $entryTerm.getValue();
- | $valueCls $rightValueTerm = ($valueCls) $rightMapTerm.get($leftKeyTerm);
- | boolean $leftValueNullTerm = ($leftValueTerm == null);
- | boolean $rightValueNullTerm = ($rightValueTerm == null);
+ |for (int $leftIndexTerm = $sizeTerm - 1; $resultTerm && $leftIndexTerm >= 0;
+ | $leftIndexTerm--) {
+ | $leftKeyCode
+ | int $rightIndexTerm = $sizeTerm - 1;
+ | for (; $rightIndexTerm >= 0; $rightIndexTerm--) {
+ | $rightKeyCode
+ | $keysMatchCode
+ | if ($keysMatch) {
+ | break;
+ | }
+ | }
+ | if ($rightIndexTerm < 0) {
+ | $resultTerm = false;
+ | } else if (!$rightMatchedTerm[$rightIndexTerm]) {
+ | $rightMatchedTerm[$rightIndexTerm] = true;
|
- | ${valueEqualsExpr.code}
- | if (!${valueEqualsExpr.resultTerm}) {
- | $resultTerm = false;
- | break;
- | }
- | } else {
- | $resultTerm = false;
+ | $leftValueCode
+ | $rightValueCode
+ | ${valueEqualsExpr.code}
+ | $resultTerm = ${valueEqualsExpr.resultTerm};
+ | }
+ |}
+ |
+ |for (int $unmatchedIndexTerm = 0; $resultTerm && $unmatchedIndexTerm < $sizeTerm;
+ | $unmatchedIndexTerm++) {
+ | if ($rightMatchedTerm[$unmatchedIndexTerm]) {
+ | continue;
+ | }
+ | $unmatchedKeyCode
+ | int $laterIndexTerm = $unmatchedIndexTerm + 1;
+ | for (; $laterIndexTerm < $sizeTerm; $laterIndexTerm++) {
+ | $laterKeyCode
+ | $laterKeysMatchCode
+ | if ($laterKeysMatch) {
| break;
| }
| }
- |} else {
- | $resultTerm = false;
+ | $resultTerm = $laterIndexTerm < $sizeTerm;
|}
""".stripMargin
(stmt, resultTerm)
diff --git a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/MapFunctionITCase.java b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/MapFunctionITCase.java
index 7d24b7995fd..1b02ffc587b 100644
--- a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/MapFunctionITCase.java
+++ b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/MapFunctionITCase.java
@@ -30,11 +30,13 @@ import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Stream;
import static org.apache.flink.table.api.DataTypes.BIGINT;
import static org.apache.flink.table.api.DataTypes.BOOLEAN;
+import static org.apache.flink.table.api.DataTypes.BYTES;
import static org.apache.flink.table.api.DataTypes.DATE;
import static org.apache.flink.table.api.DataTypes.DECIMAL;
import static org.apache.flink.table.api.DataTypes.DOUBLE;
@@ -79,7 +81,8 @@ public class MapFunctionITCase extends BuiltInFunctionTestBase {
mapFromArraysTestCases(),
mapContainsKeyTestCases(),
mapFromEntriesTestCases(),
- mapUnionTestCases())
+ mapUnionTestCases(),
+ mapEqualsTestCases())
.flatMap(s -> s);
}
@@ -889,4 +892,249 @@ public class MapFunctionITCase extends BuiltInFunctionTestBase {
"Invalid input arguments. Expected signatures are:\n"
+ "MAP_UNION(<COMMON>, <COMMON>...)"));
}
+
+ private Stream<TestSetSpec> mapEqualsTestCases() {
+ return Stream.of(
+ // f0-f2 DOUBLE, f3-f5 INT, f6-f8 DATE, f9-f11 DECIMAL, f12-f14 MAP. In each group
+ // the second map reverses the first and the third differs in one value.
+ TestSetSpec.forFunction(BuiltInFunctionDefinitions.EQUALS, "MAP value types")
+ .onFieldsWithData(
+ orderedMap(A, 1.5, B, 2.5),
+ orderedMap(B, 2.5, A, 1.5),
+ orderedMap(A, 1.5, B, 1.5),
+ orderedMap(A, 7, B, 5000),
+ orderedMap(B, 5000, A, 7),
+ orderedMap(A, 7, B, 6000),
+ orderedMap(A, TEST_DATE_1, B, TEST_DATE_2),
+ orderedMap(B, TEST_DATE_2, A, TEST_DATE_1),
+ orderedMap(A, TEST_DATE_1, B, TEST_DATE_1),
+ orderedMap(A, new BigDecimal("1.50"), B, new BigDecimal("2.50")),
+ orderedMap(B, new BigDecimal("2.50"), A, new BigDecimal("1.50")),
+ orderedMap(A, new BigDecimal("1.50"), B, new BigDecimal("1.50")),
+ orderedMap(
+ A, orderedMap(A, 5000, B, 6000),
+ B, orderedMap(A, 7000, B, 8000)),
+ orderedMap(
+ B, orderedMap(B, 8000, A, 7000),
+ A, orderedMap(B, 6000, A, 5000)),
+ orderedMap(
+ A, orderedMap(A, 5000, B, 6000),
+ B, orderedMap(A, 7000, B, 9000)))
+ .andDataTypes(
+ MAP(STRING(), DOUBLE()),
+ MAP(STRING(), DOUBLE()),
+ MAP(STRING(), DOUBLE()),
+ MAP(STRING(), INT()),
+ MAP(STRING(), INT()),
+ MAP(STRING(), INT()),
+ MAP(STRING(), DATE()),
+ MAP(STRING(), DATE()),
+ MAP(STRING(), DATE()),
+ MAP(STRING(), DECIMAL(10, 2)),
+ MAP(STRING(), DECIMAL(10, 2)),
+ MAP(STRING(), DECIMAL(10, 2)),
+ MAP(STRING(), MAP(STRING(), INT())),
+ MAP(STRING(), MAP(STRING(), INT())),
+ MAP(STRING(), MAP(STRING(), INT())))
+ .testResult(
+ resultSpec($("f0").isEqual($("f1")), "f0 = f1", true, BOOLEAN()),
+ resultSpec(
+ $("f0").isNotEqual($("f1")), "f0 <> f1", false, BOOLEAN()),
+ resultSpec($("f0").isEqual($("f2")), "f0 = f2", false, BOOLEAN()),
+ resultSpec(
+ $("f0").isNotEqual($("f2")), "f0 <> f2", true, BOOLEAN()),
+ resultSpec($("f3").isEqual($("f4")), "f3 = f4", true, BOOLEAN()),
+ resultSpec(
+ $("f3").isNotEqual($("f4")), "f3 <> f4", false, BOOLEAN()),
+ resultSpec($("f3").isEqual($("f5")), "f3 = f5", false, BOOLEAN()),
+ resultSpec(
+ $("f3").isNotEqual($("f5")), "f3 <> f5", true, BOOLEAN()),
+ resultSpec($("f6").isEqual($("f7")), "f6 = f7", true, BOOLEAN()),
+ resultSpec(
+ $("f6").isNotEqual($("f7")), "f6 <> f7", false, BOOLEAN()),
+ resultSpec($("f6").isEqual($("f8")), "f6 = f8", false, BOOLEAN()),
+ resultSpec(
+ $("f6").isNotEqual($("f8")), "f6 <> f8", true, BOOLEAN()),
+ resultSpec($("f9").isEqual($("f10")), "f9 = f10", true, BOOLEAN()),
+ resultSpec(
+ $("f9").isNotEqual($("f10")),
+ "f9 <> f10",
+ false,
+ BOOLEAN()),
+ resultSpec($("f9").isEqual($("f11")), "f9 = f11", false, BOOLEAN()),
+ resultSpec(
+ $("f9").isNotEqual($("f11")), "f9 <> f11", true, BOOLEAN()),
+ resultSpec(
+ $("f12").isEqual($("f13")), "f12 = f13", true, BOOLEAN()),
+ resultSpec(
+ $("f12").isNotEqual($("f13")),
+ "f12 <> f13",
+ false,
+ BOOLEAN()),
+ resultSpec(
+ $("f12").isEqual($("f14")), "f12 = f14", false, BOOLEAN()),
+ resultSpec(
+ $("f12").isNotEqual($("f14")),
+ "f12 <> f14",
+ true,
+ BOOLEAN())),
+ // f0-f2 differ in keys or size. f3-f5 hold a NULL value, which equals nothing.
+ // f6 and LOWER(f7) are both 'x' only at runtime, so the maps hold 'x' twice and the
+ // last entry counts. Keys compare like SQL '=', so the bytes of f8 and f9 match and
+ // f11 * -1E0 is -0.0, which equals 0.0.
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.EQUALS, "MAP keys and NULL values")
+ .onFieldsWithData(
+ orderedMap(A, 5000, B, 6000),
+ orderedMap(A, 5000, "c", 6000),
+ Collections.singletonMap(A, 5000),
+ orderedMap(A, 1.5, B, lit(null, DOUBLE())),
+ orderedMap(B, lit(null, DOUBLE()), A, 1.5),
+ orderedMap(A, 1.5, B, 2.5),
+ "x",
+ "X",
+ new byte[] {1, 2},
+ new byte[] {1, 2},
+ new byte[] {3},
+ 0.0)
+ .andDataTypes(
+ MAP(STRING(), INT()),
+ MAP(STRING(), INT()),
+ MAP(STRING(), INT()),
+ MAP(STRING(), DOUBLE()),
+ MAP(STRING(), DOUBLE()),
+ MAP(STRING(), DOUBLE()),
+ STRING(),
+ STRING(),
+ BYTES(),
+ BYTES(),
+ BYTES(),
+ DOUBLE())
+ .testResult(
+ resultSpec($("f0").isEqual($("f1")), "f0 = f1", false, BOOLEAN()),
+ resultSpec(
+ $("f0").isNotEqual($("f1")), "f0 <> f1", true, BOOLEAN()),
+ resultSpec($("f0").isEqual($("f2")), "f0 = f2", false, BOOLEAN()),
+ resultSpec(
+ $("f0").isNotEqual($("f2")), "f0 <> f2", true, BOOLEAN()),
+ resultSpec($("f3").isEqual($("f4")), "f3 = f4", false, BOOLEAN()),
+ resultSpec(
+ $("f3").isNotEqual($("f4")), "f3 <> f4", true, BOOLEAN()),
+ resultSpec($("f3").isEqual($("f5")), "f3 = f5", false, BOOLEAN()),
+ resultSpec(
+ $("f3").isNotEqual($("f5")), "f3 <> f5", true, BOOLEAN()),
+ resultSpec(
+ map($("f6"), 1, $("f7").lowerCase(), 2).cardinality(),
+ "CARDINALITY(MAP[f6, 1, LOWER(f7), 2])",
+ 2,
+ INT().notNull()),
+ resultSpec(
+ map($("f6"), 1, $("f7").lowerCase(), 2)
+ .isEqual(map($("f7").lowerCase(), 1, $("f6"), 2)),
+ "MAP[f6, 1, LOWER(f7), 2] = MAP[LOWER(f7), 1, f6, 2]",
+ true,
+ BOOLEAN().notNull()),
+ resultSpec(
+ map($("f6"), 1, $("f7").lowerCase(), 2)
+ .isEqual(map($("f7").lowerCase(), 2, $("f6"), 1)),
+ "MAP[f6, 1, LOWER(f7), 2] = MAP[LOWER(f7), 2, f6, 1]",
+ false,
+ BOOLEAN().notNull()),
+ resultSpec(
+ map($("f6"), nullOf(INT()), $("f7").lowerCase(), 2)
+ .isEqual(map($("f7").lowerCase(), 1, $("f6"), 2)),
+ "MAP[f6, CAST(NULL AS INT), LOWER(f7), 2]"
+ + " = MAP[LOWER(f7), 1, f6, 2]",
+ true,
+ BOOLEAN().notNull()),
+ resultSpec(
+ map($("f6"), 1, $("f7").lowerCase(), 1, "y", 2)
+ .isEqual(map("x", 1, "y", 2, "z", 3)),
+ "MAP[f6, 1, LOWER(f7), 1, 'y', 2] = MAP['x', 1, 'y', 2, 'z', 3]",
+ false,
+ BOOLEAN().notNull()),
+ resultSpec(
+ map($("f6"), 1, $("f7").lowerCase(), 2)
+ .isEqual(map("x", 2)),
+ "MAP[f6, 1, LOWER(f7), 2] = MAP['x', 2]",
+ false,
+ BOOLEAN().notNull()),
+ resultSpec(
+ map($("f8"), 1, $("f10"), 2)
+ .isEqual(map($("f10"), 2, $("f9"), 1)),
+ "MAP[f8, 1, f10, 2] = MAP[f10, 2, f9, 1]",
+ true,
+ BOOLEAN().notNull()),
+ resultSpec(
+ map($("f8"), 1, $("f10"), 2)
+ .isEqual(map($("f10"), 1, $("f9"), 2)),
+ "MAP[f8, 1, f10, 2] = MAP[f10, 1, f9, 2]",
+ false,
+ BOOLEAN().notNull()),
+ resultSpec(
+ map($("f11"), 1).isEqual(map($("f11").times(-1.0), 1)),
+ "MAP[f11, 1] = MAP[f11 * -1E0, 1]",
+ true,
+ BOOLEAN().notNull())),
+ // CreateMultiset turns f0-f2 into multisets. f3 and f4 are DOUBLE columns.
+ TestSetSpec.forFunction(
+ BuiltInFunctionDefinitions.EQUALS, "MULTISET and MAP from columns")
+ .withFunction(JsonFunctionsITCase.CreateMultiset.class)
+ .onFieldsWithData(
+ orderedMap(A, 200, B, 300),
+ orderedMap(B, 300, A, 200),
+ orderedMap(A, 200, B, 301),
+ 3.0,
+ 1.5)
+ .andDataTypes(
+ MAP(STRING(), INT()),
+ MAP(STRING(), INT()),
+ MAP(STRING(), INT()),
+ DOUBLE(),
+ DOUBLE())
+ .testResult(
+ resultSpec(
+ call("CreateMultiset", $("f0"))
+ .isEqual(call("CreateMultiset", $("f1"))),
+ "CreateMultiset(f0) = CreateMultiset(f1)",
+ true,
+ BOOLEAN()),
+ resultSpec(
+ call("CreateMultiset", $("f0"))
+ .isNotEqual(call("CreateMultiset", $("f1"))),
+ "CreateMultiset(f0) <> CreateMultiset(f1)",
+ false,
+ BOOLEAN()),
+ resultSpec(
+ call("CreateMultiset", $("f0"))
+ .isEqual(call("CreateMultiset", $("f2"))),
+ "CreateMultiset(f0) = CreateMultiset(f2)",
+ false,
+ BOOLEAN()),
+ resultSpec(
+ call("CreateMultiset", $("f0"))
+ .isNotEqual(call("CreateMultiset", $("f2"))),
+ "CreateMultiset(f0) <> CreateMultiset(f2)",
+ true,
+ BOOLEAN()),
+ resultSpec(
+ map(lit(A), $("f3").dividedBy(2))
+ .isEqual(map(lit(A), $("f4"))),
+ "MAP['a', f3 / 2] = MAP['a', f4]",
+ true,
+ BOOLEAN().notNull()),
+ resultSpec(
+ map(lit(A), $("f3")).isEqual(map(lit(A), $("f4"))),
+ "MAP['a', f3] = MAP['a', f4]",
+ false,
+ BOOLEAN().notNull())));
+ }
+
+ private static Map<String, Object> orderedMap(
+ String key1, Object value1, String key2, Object value2) {
+ final Map<String, Object> map = new LinkedHashMap<>();
+ map.put(key1, value1);
+ map.put(key2, value2);
+ return map;
+ }
}