在使用redis时,为了方便操作,于是写了以下帮助类,其中有个帮助函数,用来进行hmset,序列化方式是使用的Protostuff序列化.
/**
* mset
*/
public void hmset(String key, Map hash, int expireSeconds) {
ShardedJedis jedis = null;
try {
jedis = pool.getResource();
byte[] keybytes = ProtostuffUtil.serialize(key);
Map<byte[], byte[]> bytesMaps = Maps.newHashMap();
hash.forEach((k, v) -> {
bytesMaps.put(ProtostuffUtil.serialize(k), ProtostuffUtil.serialize(v));
});
jedis.hmset(keybytes, bytesMaps);
if (expireSeconds > 0) {
jedis.expire(keybytes, expireSeconds);
}
} finally {
closeJedis(jedis);
}
}
这段代码大概的功能就是把参数hash中的值,每个kv序列化之后存进redis。 然而发现在hget时发现拿不到值,hgetall却显示有值;经过排查发现,是用于String的序列化问题导致的坑。。。。。。
我们先来看String的equals方法,在该方法中,对比了两个string的value长度以及值,但是没有对比两个string对象的hashCode是否相等。而通常我们判断两个字符串是否相等时调用的是equals方法。
再来看hashCode方法,string重写了hashCode方法,有个hash字段来缓存已经计算的hash值,问题就出在这里;考虑以下代码:
String abc="abc"; //此时hash字段值为0
byte[] serialize = ProtostuffUtil.serialize(abc);
String bbb=new String(abc.getBytes()); // 此时hash为0,abc与bbb是两个不同对象。
byte[] serialize2=ProtostuffUtil.serialize(bbb);
Arrays.equals(serialize,serialize2); // true
第二段
String abc="abc"; //此时hash字段值为0
byte[] serialize = ProtostuffUtil.serialize(abc);
String bbb=new String(abc.getBytes()); // 此时hash为0,abc与bbb是两个不同对象。
bbb.hashCode() ; //修改了hash值
abc.equals(bbb); // true
byte[] serialize2=ProtostuffUtil.serialize(bbb);
Arrays.equals(serialize,serialize2); // false
原因是protostuff序列化时将String的hash值也进行了序列化,直接看源码:
com.dyuproject.protostuff.runtime.MappedSchema#writeTo这个方法遍历了对象所有的字段,
public final void writeTo(Output output, T message) throws IOException
{
for(Field f : fields)
f.writeTo(output, message);
}
而 fields又是在其构造方法中进行赋值,传入的参数最初是在这里获取的:
com.dyuproject.protostuff.runtime.RuntimeSchema#createFrom(java.lang.Class, java.util.Set
final Map.lang.reflect.Field> fieldMap = findInstanceFields(typeClass);
由此我们知道,为啥string计算了hashCode之后,protostuff的序列化结果会不一样。那什么时候会修改字符串的hashCode呢? HashMap进行put时。String的hashCode方法在计算hash时,会把结果保存到hash字段,因此会改变该字段的值;
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
文章开头的代码,传入的参数一个一个HashMap,而在序列化各个字段时,是通过直接遍历进行的,因此如果key是一个string,其hashCode必定是有值的。
hash.forEach((k, v) -> {
bytesMaps.put(ProtostuffUtil.serialize(k), ProtostuffUtil.serialize(v));
});
如果此时一个hash值为0的字符串,即便内容相同,但是通过ProtostuffUtil序列化出来的byte数组也是和set进去的不一致,因此导致了一开始的问题。
知道了这个坑的前因后果,我们再来看看Java自带的序列化方式。
代码三
String abc="abc";
byte[] serialize = SerializationUtils.serialize(abc);
String bbb=new String(abc.getBytes());
bbb.hashCode();
boolean equals1 = abc.equals(bbb);
byte[] serialize2=SerializationUtils.serialize(bbb);
boolean equals = Arrays.equals(serialize, serialize2); //true
System.out.println(equals);
可以看到,即便修改了hashCode之后,两个对象序列化的结果任然是一样的。原因在于:java自带的序列化对字符串String做了特殊处理。
在java.io.ObjectOutputStream#writeObject0中,如果对象实例是String,会调用java.io.ObjectOutputStream#writeString进行处理。
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
而writeString中,最终只处理了String的value值。因此Java自带的序列化方式对String的hashCode值不敏感。