Repository: langchain4j/langchain4j (2026-04-08T05:26:57Z)

Issue:
[BUG] Audio.equals() and Audio.hashCode() use Objects.equals on byte[] field, breaking content-based comparison
**Describe the bug**

The `Audio` class (`dev.langchain4j.data.audio.Audio`) uses `Objects.equals()` and `Objects.hash()` for its `byte[] binaryData` field. However, arrays in Java do not override `equals()` and `hashCode()` from `Object`, so these methods compare references instead of contents.

As a result, two `Audio` instances with identical binary content are incorrectly treated as different objects, which breaks usage in `Set<Audio>`, `Map<Audio, ?>`, `List.contains()`, and `List.remove()`.

Current implementation in `Audio.java`:

```java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Audio that = (Audio) o;
    return Objects.equals(this.url, that.url)
            && Objects.equals(this.binaryData, that.binaryData)  // ❌ compares references, not content
            && Objects.equals(this.base64Data, that.base64Data)
            && Objects.equals(this.mimeType, that.mimeType);
}

@Override
public int hashCode() {
    return Objects.hash(url, binaryData, base64Data, mimeType);  // ❌ uses identity hashCode for byte[]
}
```

**Log and Stack trace**

N/A — this is a logical bug, not a runtime exception. No stack trace is produced; the issue manifests as silently incorrect equality results.

**To Reproduce**

```java
byte[] data1 = {1, 2, 3};
byte[] data2 = {1, 2, 3};  // Identical content, different reference

Audio audio1 = Audio.builder().binaryData(data1).mimeType("audio/wav").build();
Audio audio2 = Audio.builder().binaryData(data2).mimeType("audio/wav").build();

// All of the following currently fail:
assertThat(audio1).isEqualTo(audio2);
assertThat(audio1.hashCode()).isEqualTo(audio2.hashCode());

Set<Audio> set = new HashSet<>();
set.add(audio1);
assertThat(set.contains(audio2)).isTrue();
```

**Expected behavior**

Two `Audio` instances with identical field values (including identical binary content) should be considered equal and produce the same hash code, in line with Java's `equals` / `hashCode` contract.

**Please complete the following information:**
- LangChain4j version: latest `main` branch (verified the bug still exists)
- LLM(s) used: N/A — this affects the core data class, independent of any model provider
- Java version: 21
- Spring Boot version (if applicable): N/A

**Additional context**

**Precedent in the codebase:** The `Embedding` class (`dev.langchain4j.data.embedding.Embedding`) in the same `langchain4j-core` module already correctly handles its `float[] vector` field using `Arrays.equals()` and `Arrays.hashCode()`. The fix for `Audio` should follow the same pattern.

**Other classes inspected:** I checked all media data classes in `dev.langchain4j.data.*`:

| Class | Has `byte[]` field? | Status |
|-------|---------------------|--------|
| `Audio` | ✅ `byte[] binaryData` | ❌ **Has bug** |
| `Image` | ❌ only `String base64Data` | ✅ OK |
| `Video` | ❌ only `String base64Data` | ✅ OK |
| `PdfFile` | ❌ only `String base64Data` | ✅ OK |

`Audio` is the only class affected — this is an isolated issue, likely introduced when `binaryData` was added as a second representation alongside `base64Data` to support implementations like Azure OpenAI that require raw binary audio data.

**Backward compatibility note:** This is technically a behavioral change (some `equals()` calls that previously returned `false` will now return `true`), but the previous behavior violates Java's `equals` contract and no reasonable code should depend on it. The fix aligns with the existing correct pattern in `Embedding`.

**Willing to submit a PR:** I'd be happy to submit a draft PR for this fix following the pattern from `Embedding`, if you'd like me to proceed.


Discussion:

Patch:
diff --git a/langchain4j-core/src/main/java/dev/langchain4j/data/audio/Audio.java b/langchain4j-core/src/main/java/dev/langchain4j/data/audio/Audio.java
index 0d2ddbaf080..21c4304a0c3 100644
--- a/langchain4j-core/src/main/java/dev/langchain4j/data/audio/Audio.java
+++ b/langchain4j-core/src/main/java/dev/langchain4j/data/audio/Audio.java
@@ -3,6 +3,7 @@
 import static dev.langchain4j.internal.Utils.quoted;
 
 import java.net.URI;
+import java.util.Arrays;
 import java.util.Objects;
 
 /**
@@ -94,14 +95,16 @@ public boolean equals(Object o) {
         if (o == null || getClass() != o.getClass()) return false;
         Audio that = (Audio) o;
         return Objects.equals(this.url, that.url)
-                && Objects.equals(this.binaryData, that.binaryData)
+                && Arrays.equals(this.binaryData, that.binaryData)
                 && Objects.equals(this.base64Data, that.base64Data)
                 && Objects.equals(this.mimeType, that.mimeType);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(url, binaryData, base64Data, mimeType);
+        int result = Objects.hash(url, base64Data, mimeType);
+        result = 31 * result + Arrays.hashCode(binaryData);
+        return result;
     }
 
     @Override


Test patch:
diff --git a/langchain4j-core/src/test/java/dev/langchain4j/data/audio/AudioTest.java b/langchain4j-core/src/test/java/dev/langchain4j/data/audio/AudioTest.java
index 0ab2acde787..50785cb792b 100644
--- a/langchain4j-core/src/test/java/dev/langchain4j/data/audio/AudioTest.java
+++ b/langchain4j-core/src/test/java/dev/langchain4j/data/audio/AudioTest.java
@@ -4,6 +4,8 @@
 
 import java.net.URI;
 import java.util.Base64;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.jupiter.api.Test;
 
 class AudioTest {
@@ -155,4 +157,35 @@ void should_handle_file_protocol_url() {
         assertThat(audio.url().toString()).isEqualTo("file:///home/user/audio.wav");
         assertThat(audio.mimeType()).isEqualTo("audio/wav");
     }
+
+    @Test
+    void equals_and_hashCode_should_compare_binaryData_by_content() {
+        byte[] data1 = {1, 2, 3};
+        byte[] data2 = {1, 2, 3};
+
+        Audio audio1 = Audio.builder().binaryData(data1).mimeType("audio/wav").build();
+        Audio audio2 = Audio.builder().binaryData(data2).mimeType("audio/wav").build();
+
+        assertThat(audio1).isEqualTo(audio2);
+        assertThat(audio1).hasSameHashCodeAs(audio2);
+    }
+
+    @Test
+    void equals_should_return_false_when_binaryData_differs() {
+        Audio audio1 = Audio.builder().binaryData(new byte[] {1, 2, 3}).build();
+        Audio audio2 = Audio.builder().binaryData(new byte[] {1, 2, 4}).build();
+
+        assertThat(audio1).isNotEqualTo(audio2);
+    }
+
+    @Test
+    void audio_with_binaryData_should_work_correctly_in_a_HashSet() {
+        Audio audio1 = Audio.builder().binaryData(new byte[] {1, 2, 3}).build();
+        Audio audio2 = Audio.builder().binaryData(new byte[] {1, 2, 3}).build();
+
+        Set<Audio> set = new HashSet<>();
+        set.add(audio1);
+
+        assertThat(set).contains(audio2);
+    }
 }