From af146d6d5ff0e05763acd4c13ca44c6f548859cb Mon Sep 17 00:00:00 2001 From: Tobias Ibounig Date: Thu, 18 Jun 2026 17:28:52 +0200 Subject: [PATCH] perf: avoid ImmutableStructure allocation for empty ImmutableContext Signed-off-by: Tobias Ibounig --- .../dev/openfeature/sdk/ImmutableContext.java | 7 ++-- .../openfeature/sdk/ImmutableStructure.java | 3 ++ .../openfeature/sdk/ImmutableContextTest.java | 37 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/main/java/dev/openfeature/sdk/ImmutableContext.java b/src/main/java/dev/openfeature/sdk/ImmutableContext.java index 150f9c171..c8992db18 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableContext.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableContext.java @@ -2,7 +2,6 @@ import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.function.Function; import lombok.ToString; @@ -32,7 +31,7 @@ public final class ImmutableContext implements EvaluationContext { * provided. */ public ImmutableContext() { - this(new HashMap<>()); + this(Collections.emptyMap()); } /** @@ -41,7 +40,7 @@ public ImmutableContext() { * @param targetingKey targeting key */ public ImmutableContext(String targetingKey) { - this(targetingKey, new HashMap<>()); + this(targetingKey, Collections.emptyMap()); } /** @@ -63,6 +62,8 @@ public ImmutableContext(Map attributes) { public ImmutableContext(String targetingKey, Map attributes) { if (targetingKey != null) { this.structure = new ImmutableStructure(targetingKey, attributes); + } else if (attributes == null || attributes.isEmpty()) { + this.structure = ImmutableStructure.EMPTY; } else { this.structure = new ImmutableStructure(attributes); } diff --git a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java index 313e13057..e6c427007 100644 --- a/src/main/java/dev/openfeature/sdk/ImmutableStructure.java +++ b/src/main/java/dev/openfeature/sdk/ImmutableStructure.java @@ -1,5 +1,6 @@ package dev.openfeature.sdk; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -21,6 +22,8 @@ @SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"}) public final class ImmutableStructure extends AbstractStructure { + static final ImmutableStructure EMPTY = new ImmutableStructure(Collections.emptyMap()); + /** * create an immutable structure with the empty attributes. */ diff --git a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java index 7d8210be4..a365b6fc2 100644 --- a/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java +++ b/src/test/java/dev/openfeature/sdk/ImmutableContextTest.java @@ -1,6 +1,7 @@ package dev.openfeature.sdk; import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -216,6 +217,42 @@ void immutableContextHashCodeIsStable() { assertEquals(first, second); } + @Nested + @DisplayName("ImmutableContext(String, Map) branch logic") + class ConstructorBranches { + + @Test + @DisplayName("non-null targeting key with empty attributes is preserved") + void nonNullTargetingKeyWithEmptyAttributesIsPreserved() { + ImmutableContext ctx = new ImmutableContext("key", Collections.emptyMap()); + assertThat(ctx.getTargetingKey()).isEqualTo("key"); + assertThat(ctx.keySet()).contains(TARGETING_KEY); + } + + @Test + @DisplayName("non-null targeting key with null attributes is preserved") + void nonNullTargetingKeyWithNullAttributesIsPreserved() { + ImmutableContext ctx = new ImmutableContext("key", null); + assertThat(ctx.getTargetingKey()).isEqualTo("key"); + } + + @Test + @DisplayName("null targeting key with empty attributes yields empty context") + void nullTargetingKeyWithEmptyAttributesYieldsEmptyContext() { + ImmutableContext ctx = new ImmutableContext((String) null, Collections.emptyMap()); + assertThat(ctx.getTargetingKey()).isNull(); + assertThat(ctx.isEmpty()).isTrue(); + } + + @Test + @DisplayName("null targeting key with null attributes yields empty context") + void nullTargetingKeyWithNullAttributesYieldsEmptyContext() { + ImmutableContext ctx = new ImmutableContext((String) null, null); + assertThat(ctx.getTargetingKey()).isNull(); + assertThat(ctx.isEmpty()).isTrue(); + } + } + @Nested class Equals { ImmutableContext ctx = new ImmutableContext("c", Map.of("a", new Value("b")));