Sử dụng mảng byte làm khóa bản đồ


76

Bạn có thấy bất kỳ vấn đề nào với việc sử dụng một mảng byte làm khóa Bản đồ không? Tôi cũng có thể làm new String(byte[])và băm bằng cách Stringnhưng nó dễ sử dụng hơn byte[].

Câu trả lời:


65

Vấn đề là byte[]sử dụng nhận dạng đối tượng cho equalshashCode, để

byte[] b1 = {1, 2, 3}
byte[] b2 = {1, 2, 3}

sẽ không khớp trong a HashMap. Tôi thấy ba tùy chọn:

  1. Gói trong một String, nhưng sau đó bạn phải cẩn thận về các vấn đề mã hóa (bạn cần đảm bảo rằng byte -> Chuỗi -> byte cung cấp cho bạn các byte giống nhau).
  2. Sử dụng List<Byte>(có thể tốn kém trong bộ nhớ).
  3. Thực hiện lớp gói của riêng bạn, viết hashCodeequalssử dụng nội dung của mảng byte.

3
Tôi đã giải quyết vấn đề bao bọc chuỗi bằng cách sử dụng mã hóa hex. Ngoài ra, bạn có thể sử dụng mã hóa base64.
metadaddy

1
Tùy chọn lớp gói / xử lý rất đơn giản và rất dễ đọc.
ZX9

79

Không sao cả, miễn là bạn chỉ muốn bình đẳng tham chiếu cho khóa của mình - mảng không triển khai "bình đẳng giá trị" theo cách mà bạn có thể muốn. Ví dụ:

byte[] array1 = new byte[1];
byte[] array2 = new byte[1];

System.out.println(array1.equals(array2));
System.out.println(array1.hashCode());
System.out.println(array2.hashCode());

in một cái gì đó như:

false
1671711
11394033

(Các con số thực tế không liên quan; việc chúng khác nhau là rất quan trọng.)

Giả sử bạn thực sự muốn bình đẳng, tôi khuyên bạn nên tạo trình bao bọc của riêng mình có chứa một byte[]và triển khai bình đẳng và tạo mã băm một cách thích hợp:

public final class ByteArrayWrapper
{
    private final byte[] data;

    public ByteArrayWrapper(byte[] data)
    {
        if (data == null)
        {
            throw new NullPointerException();
        }
        this.data = data;
    }

    @Override
    public boolean equals(Object other)
    {
        if (!(other instanceof ByteArrayWrapper))
        {
            return false;
        }
        return Arrays.equals(data, ((ByteArrayWrapper)other).data);
    }

    @Override
    public int hashCode()
    {
        return Arrays.hashCode(data);
    }
}

Lưu ý rằng nếu bạn thay đổi các giá trị trong mảng byte sau khi sử dụng ByteArrayWrapper, làm khóa trong HashMap(v.v.), bạn sẽ gặp vấn đề khi tra cứu lại khóa ... bạn có thể lấy bản sao dữ liệu trong hàm ByteArrayWrappertạo nếu bạn muốn , nhưng rõ ràng điều đó sẽ gây lãng phí hiệu suất nếu bạn biết rằng bạn sẽ không thay đổi nội dung của mảng byte.

CHỈNH SỬA: Như đã đề cập trong các nhận xét, bạn cũng có thể sử dụng ByteBuffercho việc này (cụ thể là ByteBuffer#wrap(byte[])phương pháp của nó ). Tôi không biết liệu đó có thực sự là điều đúng đắn hay không, với tất cả những khả năng bổ sung ByteBuffermà bạn không cần, nhưng đó là một lựa chọn.


@dfa: Kiểm tra "instanceof" xử lý trường hợp rỗng.
Jon Skeet

4
Một số thứ khác mà bạn có thể thêm vào việc triển khai trình bao bọc: 1. Lấy một bản sao của byte [] khi xây dựng, do đó đảm bảo rằng đối tượng là bất biến, có nghĩa là không có nguy cơ mã băm của khóa của bạn sẽ thay đổi theo thời gian. 2. Tính toán trước và lưu trữ mã băm một lần (giả sử tốc độ quan trọng hơn chi phí lưu trữ).
Adamski

2
@Adamski: Tôi đề cập đến khả năng sao chép ở cuối câu trả lời. Trong một số trường hợp, đó là điều đúng đắn nên làm, nhưng với những trường hợp khác thì không. Tôi có thể muốn biến nó thành một tùy chọn (có thể là các phương thức tĩnh thay vì các hàm tạo - copyOf và wrapperAround). Lưu ý rằng không cần sao chép, bạn có thể thay đổi mảng bên dưới cho đến khi lần đầu tiên bạn lấy hàm băm và kiểm tra sự bình đẳng, điều này có thể hữu ích trong một số trường hợp.
Jon Skeet

Rất tiếc - Xin lỗi Jon; Tôi đã bỏ lỡ phần trả lời của bạn.
Adamski

3
Chỉ muốn chỉ ra rằng lớp java.nio.ByteBuffer về cơ bản thực hiện mọi thứ mà trình bao bọc của bạn làm, mặc dù với cùng một cảnh báo rằng bạn chỉ nên sử dụng nó nếu nội dung của mảng byte không thay đổi. Bạn có thể muốn sửa đổi câu trả lời của mình để đề cập đến nó.
Ed Anuff

46

Chúng ta có thể sử dụng ByteBuffer cho việc này (Về cơ bản đây là trình bao bọc byte [] với một bộ so sánh)

HashMap<ByteBuffer, byte[]> kvs = new HashMap<ByteBuffer, byte[]>();
byte[] k1 = new byte[]{1,2 ,3};
byte[] k2 = new byte[]{1,2 ,3};
byte[] val = new byte[]{12,23,43,4};

kvs.put(ByteBuffer.wrap(k1), val);
System.out.println(kvs.containsKey(ByteBuffer.wrap(k2)));

sẽ in

true

2
1 cho hầu hết các byte nhẹ mảng wrapper (Tôi nghĩ rằng ...)
Nicholas

7
Điều này hoạt động tốt với ByteBuffer.wrap (), nhưng hãy cẩn thận nếu nội dung của ByteBuffer đã được tạo bằng cách sử dụng một vài lệnh gọi put () để tạo một mảng byte khóa tổng hợp. Trong trường hợp này, lệnh gọi put () cuối cùng phải được theo sau bởi một lệnh gọi rewind () - nếu không thì equals () trả về true ngay cả khi các mảng byte bên dưới chứa dữ liệu khác nhau.
RenniePet

Đây sẽ là một giải pháp tốt, nhưng nếu bạn muốn tuần tự hóa bản đồ (như trong trường hợp của tôi), bạn không thể sử dụng phương pháp này.
501 - không được thực hiện

Lưu ý rằng: "Vì mã băm bộ đệm phụ thuộc vào nội dung nên không thể sử dụng bộ đệm làm khóa trong bản đồ băm hoặc cấu trúc dữ liệu tương tự trừ khi biết rằng nội dung của chúng sẽ không thay đổi." ( Docs.oracle.com/javase/7 / docs / api / java / nio /… )
LMD

Bạn nên ByteBuffer.wrap(k1.clone())tạo một bản sao phòng thủ của mảng. Nếu không, nếu bất cứ ai thay đổi mảng, điều xấu sẽ xảy ra. Nhìn trong trình gỡ lỗi, ByteBuffer có rất nhiều trạng thái bên trong so với một Chuỗi, vì vậy có vẻ như đây không thực sự là một giải pháp nhẹ về chi phí bộ nhớ.
simbo1905

11

Bạn có thể sử dụng java.math.BigInteger. Nó có một hàm BigInteger(byte[] val)tạo. Đó là một kiểu tham chiếu, vì vậy có thể được sử dụng làm khóa cho bảng băm. Và .equals().hashCode()được định nghĩa như cho các số nguyên tương ứng, có nghĩa là BigInteger có ngữ nghĩa nhất quán ngang bằng như mảng byte [].


17
Âm thanh atractive, nhưng đó là sai, như hai mảng mà chỉ khác nhau ở zero yếu tố hàng đầu (nói, {0,100}{100}) sẽ cung cấp cùng BigInteger
leonbloy

Điểm tốt @leonbloy. Có thể có một cách giải quyết: bằng cách thêm một hằng số byte hàng đầu không null cố định vào nó, nhưng nó sẽ yêu cầu viết một trình bao bọc xung quanh hàm tạo BigInteger và sẽ đưa chúng ta trở lại phản hồi của Jon.
Artem Oboturov

Phản hồi của @ vinchan sẽ phù hợp hơn vì sẽ không có vấn đề về byte ở đầu.
Artem Oboturov

5

Tôi rất ngạc nhiên khi các câu trả lời không chỉ ra giải pháp thay thế đơn giản nhất.

Có, không thể sử dụng HashMap, nhưng không ai ngăn cản bạn sử dụng SortedMap thay thế. Điều duy nhất là viết một Comparator cần so sánh các mảng. Nó không hoạt động hiệu quả như HashMap, nhưng nếu bạn muốn có một giải pháp thay thế đơn giản, thì đây (bạn có thể thay thế SortedMap bằng Map nếu bạn muốn ẩn việc triển khai):

 private SortedMap<int[], String>  testMap = new TreeMap<>(new ArrayComparator());

 private class ArrayComparator implements Comparator<int[]> {
    @Override
    public int compare(int[] o1, int[] o2) {
      int result = 0;
      int maxLength = Math.max(o1.length, o2.length);
      for (int index = 0; index < maxLength; index++) {
        int o1Value = index < o1.length ? o1[index] : 0;
        int o2Value = index < o2.length ? o2[index] : 0;
        int cmp     = Integer.compare(o1Value, o2Value);
        if (cmp != 0) {
          result = cmp;
          break;
        }
      }
      return result;
    }
  }

Việc triển khai này có thể được điều chỉnh cho các mảng khác, điều duy nhất bạn phải biết là các mảng bằng nhau (= độ dài bằng nhau với các thành viên bằng nhau) phải trả về 0 và bạn có một thứ tự xác định


Giải pháp tuyệt vời với lợi ích to lớn của việc không tạo thêm các đối tượng. Lỗi rất nhỏ nếu các mảng không có cùng độ dài nhưng mảng dài nhất chỉ có 0 sau một độ dài ngắn hơn. Ngoài ra, việc quản lý thứ tự có thể giúp tăng tốc quá trình duyệt cây. +1!
jmspaggi

1

Tôi tin rằng các mảng trong Java không nhất thiết phải triển khai các phương thức hashCode()và một equals(Object)cách trực quan. Nghĩa là, hai mảng byte giống hệt nhau sẽ không nhất thiết phải chia sẻ cùng một mã băm và chúng sẽ không nhất thiết phải bằng nhau. Nếu không có hai đặc điểm này, HashMap của bạn sẽ hoạt động không như mong đợi.

Vì vậy, tôi khuyên bạn nên chống lại sử dụng byte[]như phím trong một HashMap.


Tôi cho rằng từ ngữ của tôi đã hơi sai. Tôi đã tính đến tình huống trong đó Mảng byte CÙNG đang được sử dụng để chèn vào bản đồ băm VÀ để truy xuất từ ​​bản đồ băm. Trong trường hợp đó, mảng byte "cả hai" đều giống hệt nhau VÀ chia sẻ cùng một mã băm.
Adam Paynter

1

Bạn nên sử dụng tạo một lớp somthing như ByteArrKey và quá tải mã băm và các phương thức ngang nhau, hãy nhớ hợp đồng giữa chúng.

Điều này sẽ mang lại cho bạn tính linh hoạt cao hơn vì bạn có thể bỏ qua 0 mục nhập được nối vào cuối mảng byte, đặc biệt nếu bạn chỉ sao chép một số phần tạo thành bộ đệm byte khác.

Bằng cách này, bạn sẽ quyết định cách cả hai đối tượng NÊN bằng nhau.


0

Tôi gặp sự cố vì bạn nên sử dụng Arrays.equals và Array.hashCode, thay cho triển khai mảng mặc định


Và bạn sẽ làm cho HashMap sử dụng những thứ đó như thế nào?
Michael Borgwardt

xem câu trả lời của Jon Skeet (một wrapper mảng byte)
DFA

0

Arrays.toString (byte)


1
Có thể được sử dụng, nhưng không hiệu quả lắm. Nếu bạn muốn đi theo cách này, bạn có thể muốn sử dụng mã hóa base64 thay thế.
Maarten Bodewes

0

Bạn cũng có thể chuyển đổi byte [] thành chuỗi 'an toàn' bằng cách sử dụng Base32 hoặc Base64, ví dụ:

byte[] keyValue = new byte[] {…};
String key = javax.xml.bind.DatatypeConverter.printBase64Binary(keyValue);

tất nhiên có nhiều biến thể của những điều trên, như:

String key = org.apache.commons.codec.binary.Base64.encodeBase64(keyValue);

0

Đây là một giải pháp sử dụng TreeMap, giao diện Comparator và phương thức java java.util.Arrays.equals (byte [], byte []);

LƯU Ý: Thứ tự trong bản đồ không liên quan đến phương pháp này

SortedMap<byte[], String> testMap = new TreeMap<>(new ArrayComparator());

static class ArrayComparator implements Comparator<byte[]> {
    @Override
    public int compare(byte[] byteArray1, byte[] byteArray2) {

        int result = 0;

        boolean areEquals = Arrays.equals(byteArray1, byteArray2);

        if (!areEquals) {
            result = -1;
        }

        return result;
    }
}

0

Ngoài ra, Chúng tôi có thể tạo ByteHashMap tùy chỉnh của riêng mình như thế này,

ByteHashMap byteMap = new ByteHashMap();
byteMap.put(keybyteArray,valueByteArray);

Đây là cách thực hiện đầy đủ

public class ByteHashMap implements Map<byte[], byte[]>, Cloneable,
        Serializable {

    private Map<ByteArrayWrapper, byte[]> internalMap = new HashMap<ByteArrayWrapper, byte[]>();

    public void clear() {
        internalMap.clear();
    }

    public boolean containsKey(Object key) {
        if (key instanceof byte[])
            return internalMap.containsKey(new ByteArrayWrapper((byte[]) key));
        return internalMap.containsKey(key);
    }

    public boolean containsValue(Object value) {
        return internalMap.containsValue(value);
    }

    public Set<java.util.Map.Entry<byte[], byte[]>> entrySet() {
        Iterator<java.util.Map.Entry<ByteArrayWrapper, byte[]>> iterator = internalMap
                .entrySet().iterator();
        HashSet<Entry<byte[], byte[]>> hashSet = new HashSet<java.util.Map.Entry<byte[], byte[]>>();
        while (iterator.hasNext()) {
            Entry<ByteArrayWrapper, byte[]> entry = iterator.next();
            hashSet.add(new ByteEntry(entry.getKey().data, entry
                    .getValue()));
        }
        return hashSet;
    }

    public byte[] get(Object key) {
        if (key instanceof byte[])
            return internalMap.get(new ByteArrayWrapper((byte[]) key));
        return internalMap.get(key);
    }

    public boolean isEmpty() {
        return internalMap.isEmpty();
    }

    public Set<byte[]> keySet() {
        Set<byte[]> keySet = new HashSet<byte[]>();
        Iterator<ByteArrayWrapper> iterator = internalMap.keySet().iterator();
        while (iterator.hasNext()) {
            keySet.add(iterator.next().data);
        }
        return keySet;
    }

    public byte[] put(byte[] key, byte[] value) {
        return internalMap.put(new ByteArrayWrapper(key), value);
    }

    @SuppressWarnings("unchecked")
    public void putAll(Map<? extends byte[], ? extends byte[]> m) {
        Iterator<?> iterator = m.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<? extends byte[], ? extends byte[]> next = (Entry<? extends byte[], ? extends byte[]>) iterator
                    .next();
            internalMap.put(new ByteArrayWrapper(next.getKey()), next
                    .getValue());
        }
    }

    public byte[] remove(Object key) {
        if (key instanceof byte[])
            return internalMap.remove(new ByteArrayWrapper((byte[]) key));
        return internalMap.remove(key);
    }

    public int size() {
        return internalMap.size();
    }

    public Collection<byte[]> values() {
        return internalMap.values();
    }

    private final class ByteArrayWrapper {
        private final byte[] data;

        public ByteArrayWrapper(byte[] data) {
            if (data == null) {
                throw new NullPointerException();
            }
            this.data = data;
        }

        public boolean equals(Object other) {
            if (!(other instanceof ByteArrayWrapper)) {
                return false;
            }
            return Arrays.equals(data, ((ByteArrayWrapper) other).data);
        }

        public int hashCode() {
            return Arrays.hashCode(data);
        }
    }

    private final class ByteEntry implements Entry<byte[], byte[]> {
        private byte[] value;
        private byte[] key;

        public ByteEntry(byte[] key, byte[] value) {
            this.key = key;
            this.value = value;
        }

        public byte[] getKey() {
            return this.key;
        }

        public byte[] getValue() {
            return this.value;
        }

        public byte[] setValue(byte[] value) {
            this.value = value;
            return value;
        }

    }
}

0

Các câu trả lời khác đã không chỉ ra rằng không phải tất cả đều byte[]bí mật thành duy nhất String. Tôi đã rơi vào cái bẫy này khi làmnew String(byteArray) chìa khóa cho một bản đồ chỉ để thấy rằng nhiều byte âm được ánh xạ vào cùng một chuỗi. Đây là một bài kiểm tra chứng minh vấn đề đó:

    @Test
    public void testByteAsStringMap() throws Exception {
        HashMap<String, byte[]> kvs = new HashMap<>();
        IntStream.range(Byte.MIN_VALUE, Byte.MAX_VALUE).forEach(b->{
            byte[] key = {(byte)b};
            byte[] value = {(byte)b};
            kvs.put(new String(key), value);
        });
        Assert.assertEquals(255, kvs.size());
    }

Nó sẽ ném:

java.lang.AssertionError: Dự kiến: 255 Thực tế: 128

Nó làm được điều đó vì a Stringlà một chuỗi các điểm mã ký tự và bất kỳ chuyển đổi nào từ a byte[]đều dựa trên một số mã hóa byte. Trong trường hợp trên, mã hóa mặc định của nền tảng sẽ ánh xạ nhiều byte âm với cùng một ký tự. Một thực tế khác Stringlà nó luôn lấy và cung cấp một bản sao của trạng thái bên trong của nó. Nếu các byte ban đầu đến từString bản sao, thì gói nó dưới dạng một Stringđể sử dụng nó làm chìa khóa cho bản đồ sẽ mất một bản sao thứ hai. Điều đó có thể tạo ra rất nhiều rác mà có thể tránh được.

Có một câu trả lời hay ở đây đề nghị sử dụng java.nio.ByteBuffervới ByteBuffer.wrap(b). Vấn đề với điều đó byte[]là có thể thay đổi và nó không mất một bản sao, vì vậy bạn phải cẩn thận giữ một bản sao phòng thủ của bất kỳ mảng nào được chuyển cho bạn cùng với ByteBuffer.wrap(b.clone())các khóa bản đồ của bạn sẽ bị hỏng. Nếu bạn nhìn vào kết quả của bản đồ có ByteBuffercác khóa trong trình gỡ lỗi, bạn sẽ thấy rằng các bộ đệm có rất nhiều tham chiếu bên trong được thiết kế để theo dõi việc đọc và ghi từ mỗi bộ đệm. Vì vậy, các đối tượng nặng hơn nhiều so với gói trong một đơn giản String. Cuối cùng, ngay cả một chuỗi cũng chứa nhiều trạng thái hơn mức cần thiết. Nhìn vào nó trong trình gỡ lỗi của tôi, nó lưu trữ các ký tự dưới dạng mảng UTF16 hai byte và cũng lưu trữ một mã băm bốn byte.

Cách tiếp cận ưa thích của tôi là để Lombok tạo ra tại thời điểm biên dịch bảng soạn sẵn để tạo một trình bao bọc mảng byte nhẹ không lưu trữ trạng thái bổ sung:

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@ToString
@EqualsAndHashCode
@Data(staticConstructor="of")
class ByteSequence {
    final byte[] bytes;
}

Điều này sau đó sẽ vượt qua bài kiểm tra để kiểm tra xem tất cả các byte có thể ánh xạ tới một chuỗi duy nhất:

    byte[] bytes(int b){
        return new byte[]{(byte)b};
    }

    @Test
    public void testByteSequenceAsMapKey() {
        HashMap<ByteSequence, byte[]> kvs = new HashMap<>();
        IntStream.range(Byte.MIN_VALUE, Byte.MAX_VALUE).forEach(b->{
            byte[] key = {(byte)b};
            byte[] value = {(byte)b};
            kvs.put(ByteSequence.of(key), value);
        });
        Assert.assertEquals(255, kvs.size());
        byte[] empty = {};
        kvs.put(ByteSequence.of(empty), bytes(1));
        Assert.assertArrayEquals(bytes(1), kvs.get(ByteSequence.of(empty)));
    }

Sau đó, bạn không phải lo lắng về việc lấy logic bằng và mã băm chính xác vì nó được cung cấp bởi Lombok, nơi nó thực hiện Arrays.deepEqualsđược tài liệu tại https://projectlombok.org/features/EqualsAndHashCode Lưu ý rằng lombok không chỉ là phụ thuộc thời gian chạy phụ thuộc vào thời gian biên dịch và bạn có thể cài đặt một plugin nguồn mở cho IDE của mình để IDE của bạn "nhìn thấy" tất cả các phương thức soạn sẵn đã tạo.

Với cách triển khai này, bạn vẫn phải lo lắng về khả năng thay đổi của byte. Nếu ai đó chuyển cho bạn một thẻ byte[]có thể bị đột biến, bạn nên tạo một bản sao phòng thủ bằng cách sử dụng clone():

kvs.put(ByteSequence.of(key.clone()), value);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.