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 String
nhưng nó dễ sử dụng hơn byte[]
.
Câu trả lời:
Vấn đề là byte[]
sử dụng nhận dạng đối tượng cho equals
và hashCode
, để
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:
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).List<Byte>
(có thể tốn kém trong bộ nhớ).hashCode
và equals
sử dụng nội dung của mảng byte.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 ByteArrayWrapper
tạ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 ByteBuffer
cho 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 ByteBuffer
mà bạn không cần, nhưng đó là một lựa chọn.
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
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ớ.
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()
và .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 [].
{0,100}
và {100}
) sẽ cung cấp cùng BigInteger
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
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.
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.
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
Arrays.toString (byte)
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);
Đâ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;
}
}
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;
}
}
}
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 String
là 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 String
là 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.ByteBuffer
vớ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ó ByteBuffer
cá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);