压缩和解压缩
压缩: 把数据换一种方式来存储,以减小存储空间。
解压缩: 把压缩后的数据还原为原先的形式,以便使用。
常见的压缩算法有: DEFLATE、JPEG、MP3。
其中 DEFLATE 就是 ZIP 的压缩算法,ZIP 是我们常见的打包格式。其正式说法叫做 Archive(归档),将多个文件打包成一个文件,方便传输和存储。
我们来看看什么是数据压缩。
比如对于如下数据:
AAAABBBCCDEEEE
我可以压缩为:4A3B2C1D4E
,这种压缩算法非常粗暴,如果原始数据变为这样:
ABCDE
压缩结果:1A1B1C1D1E
反而比原始数据大。一个优秀的压缩算法,它能在常见数据下实现高压缩率。不过,你需要知道的是不存在一种能够压缩任意数据的无损算法。对于一串完全随机的数据,经过无损压缩算法处理后,体积可能会变大。
当然对于上述的压缩结果,我们还能还原为原数据。例如 4A3B2C1D4E
,我们只需根据字符的出现次数以及对应的字符,即可还原为 AAAABBBCCDEEEE
。
那压缩属于编码吗?
其实编码是没有一个官方标准的。通常来说,我们认为的编码是使用一套固定的规则,将数据从 A 格式转换为 B 格式,并且还能复原,不损失任何数据。这么来说,压缩就属于编码。
媒体数据的编解码
媒体数据指的是将音视频、图片等数据。
以图片的编解码为例。
图片的编码:把图片数据转成 JPG、PNG 等文件的编码格式。
图片的解码:把 JPG、PNG 等文件中的数据解析成标准的图像数据。
另外,媒体数据的压缩通常是有损压缩。它之所以可行,是因为利用了我们无法察觉到图片或音频中的所有细节。有损压缩就是通过丢弃了这些我们感知不到的数据,从而实现了更高的压缩率。MP3 就是一个典型的例子,正因为有损压缩后的数据无法还原为原数据,所以有损压缩并不算我们平常所说的编码。
序列化
在 Java 中,我们经常需要让某个类实现 Serializable
接口,使其能够序列化。
而这个序列化,其实是把处于内存中的数据对象转换成字节序列的过程。
例如:我们会将 zhangsan
这个对象转为一段连续的数据,使其能够存储到文件或是在网络上进行传输。
kotlin
data class Human(val name: String, val age: Int, val gender: String, val mate: Human? = null)
val zhangsan = Human("张三", 18, "男", Human("王五", 20, "女"))
比如我们转为 JSON 格式。
kotlin
{"name":"张三","age":18,"gender":"男","mate":{"name":"王五","age":20,"gender":"女"}}
也可以是这样的:
kotlin
Human(name=张三, age=18, gender=男, mate=Human(name=王五, age=20, gender=女, mate=null))
只要将内存中的这个对象转成序列,就叫序列化。不一定非得是实现了某个接口,调用了某个方法。
反序列化就是将序列化后的字节序列重新转为内存中的对象。
为什么需要序列化?
之前也说过了,就是要让内存中的对象能够被存储、被传输。根本原因是,内存中的对象结构很复杂,可能由多个部分组成,通过引用进行关联,散落在了内存的不同地方。而往磁盘存文件或者在网络上传输,都需要一段连续的字节数据。序列化,就是将内存中这些零散的对象结构,转换成一段连续字节流的过程。
Hash
Hash 的原义是 "切碎"、"拼凑" 的意思。而在程序中,意思是哈希函数,它可以把任意数据转换成一个固定长度的字符串(哈希值)。
Hash 值可用于摘要和数字指纹。
经典算法有: MD5、SHA1、SHA256 等。
哈希值为什么需要根据特定算法得出?
因为好的哈希函数有以下特点:
-
相同的输入,必定得到相同的输出。
-
对于不同的输入,能够尽可能产生不同的输出,避免碰撞(冲突)。
-
输入数据的微小变化,会导致输出具有差异。
对于不同的输入,如果它们的哈希值相同,这就叫发生了哈希碰撞。比如我们设计了一个哈希算法:hash(String s) = s.length()
。使用它来计算 "hello"
和 "world"
的哈希值,发现都是 5,这就发生了一次哈希碰撞。好的哈希算法会让这种碰撞的概率变得非常低。
Hash 的实际用途主要有:
-
数据完整性的验证
比如对于一个重要文件,如果你计算它的哈希值与其标定的哈希值相同,那么你就可以认为这份文件未被篡改,是完整的。
-
身份的快速匹配/数据的快速比较
我们在重写类的
equals
方法时,通常要同时重写hashCode
方法。这是因为HashMap
等这类容器,在判断元素是否存在时,为了效率,会先使用hashCode
方法进行比较。因为如果两个对象的hashCode
值不同,那么这两个对象一定不相等 。这样可以快速排除掉大部分不同的元素。只有当hashCode
值相同时,再使用equals
方法做最终判断,因为hashCode
值相同不代表两个对象就一定相等,有可能是发生了哈希碰撞。为什么要用
hashCode
方法,而不直接使用equals
方法进行判断?这是因为
hashCode
的运算非常快,用于判断两个元素不相等,最合适不过了。
另外,目前密码的存储都是将密码的哈希值存入数据库。并不是进行明文存储,因为有泄露风险。密码进行验证时,只需将输入的密码进行 Hash,再与数据库中存的哈希值进行比对,就能够判断密码是否正确。
这样,即使数据库泄露了,被别人拿到了用户密码的哈希值,别人也无法获取到真实的密码,因为 Hash 运算是不可逆的。不过,别人也可能会使用彩虹表来破解密码。
彩虹表是将常见的密码与它们的哈希值存到数据库中,这样别人就可以通过已知的哈希值,来推导出真实的密码了。也有解决方法,在计算密码的哈希值之前,通常会给密码加上盐,也就是给密码附加上额外的数据,这个数据可以是固定的,也可以是随机的。这样,别人即使知道一些常用密码的哈希值,也是无法破解密码的,因为存入数据库的值,并不是真实密码的哈希值,而是加上了额外数据计算出的哈希值。
例如:你的真实密码是 136520
。我并不会计算当前字符串的哈希值,而是会加上某个字符串,如 zxc
,再计算加上后的字符串的哈希值,存入到数据库中。
最后说说 Hash 是编码吗,是加密吗?
-
Hash 并不是编码,因为它只是原数据的摘要,并不能通过它来还原数据。
-
Hash 也不是加密,MD5 常常被认为是 "不可逆加密"。但加密的定义是,使用加密算法对数据进行加密隐藏原文,然后能够使用解密算法还原数据。而 Hash 的目的是生成数据的摘要进行验证,是单向的,无法还原数据。所以 MD5 等算法并不是加密算法。
字符集
字符集:一个由整数向现实世界中的文字符号的 Map。
例如这就是一个非常简单的字符集:
kotlin
1 => A
2 => B
3 => C
最早的字符集是 ASCII,它包含了 128 个字符,每个字符占 1 字节。然后出现了 ISO-8859-1 字符集,它对 ASCII 字符集进行了扩充,每个字符也是只占 1 字节。接着有了大名鼎鼎的 Unicode 字符集,开始对拉丁字母以外的字符有了支持,比如中文、日文等。
Unicode 字符集有两个著名的编码分支,分别是:UTF-8 和 UTF-16。
编码分支是什么意思呢?在 Unicode 字符集中,规定 U+4E2D
代表 "中" 字,而 UTF-8 和 UTF-16 的实现方式则有些不同。比如 UTF-8 可能会使用 E4 B8 AD
三个字节来代表 "中",而 UTF-16 可能会用两个字节 4E 2D
来表示。虽然它们表达的是同一个字,但占用的字节数和字节内容不同。这就是同一个字符集下的不同编码实现。
然后还有 GBK / GB2312 / GB18030 系列,它们既是字符集标准,也包含了编码规则。这是中国自研的标准,不过现在全世界最广泛使用的是 Unicode 字符集和 UTF-8 编码。