Java基础-实用类
参考文章:菜鸟教程-Java教程
1.Java Number 类
参考文章:廖雪峰-包装类型
包装类型
在实际开发过程中,我们经常会遇到需要使用对象,而不是内置数据类型的情形。为了解决这个问题,Java 语言为每一个内置数据类型提供了对应的包装类。所有的包装类(Integer、Long、Byte、Double、Float、Short)都是抽象类 Number 的子类。
包装类 | 基本数据类型 |
---|---|
Boolean | boolean |
Byte | byte |
Short | short |
Integer | int |
Long | long |
Character | char |
Float | float |
Double | double |
这种由编译器特别支持的包装称为装箱
,所以当内置数据类型被当作对象使用的时候,编译器会把内置类型装箱为包装类。相似的,编译器也可以把一个对象拆箱
为内置类型。Number 类属于 java.lang 包。
1 | public class Main { |
因为int
和Integer
可以互相转换:
1 | int i = 100; |
所以,Java编译器可以帮助我们自动在int
和Integer
之间转型:
1 | Integer n = 100; // 编译器自动使用Integer.valueOf(int) |
这种直接把int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法,称为自动拆箱(Auto Unboxing)。
注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
装箱和拆箱会影响代码的执行效率,因为编译后的class
代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException
:
1 | public class Main { |
不变类
所有的包装类型都是不变类。我们查看Integer
的源码可知,它的核心代码如下:
1 | public final class Integer { |
因此,一旦创建了Integer
对象,该对象就是不变的。
对两个Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较:
1 | public class Main { |
仔细观察结果的童鞋可以发现,==
比较,较小的两个相同的Integer
返回true
,较大的两个相同的Integer
返回false
,这是因为Integer
是不变类,编译器把Integer x = 127;
自动变为Integer x = Integer.valueOf(127);
,为了节省内存,Integer.valueOf()
对于较小的数,始终返回相同的实例,因此,==
比较“恰好”为true
,但我们绝不能因为Java标准库的Integer
内部有缓存优化就用==
比较,必须用equals()
方法比较两个Integer
。
因为Integer.valueOf()
可能始终返回同一个Integer
实例,因此,在我们自己创建Integer
的时候,以下两种方法:
- 方法1:
Integer n = new Integer(100);
- 方法2:
Integer n = Integer.valueOf(100);
方法2更好,因为方法1总是创建新的Integer
实例,方法2把内部优化留给Integer
的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。
我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()
就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
创建新对象时,优先选用静态工厂方法而不是new操作符。
如果我们考察Byte.valueOf()
方法的源码,可以看到,标准库返回的Byte
实例全部是缓存实例,但调用者并不关心静态工厂方法以何种方式创建新实例还是直接返回缓存的实例。
进制转换
Integer
类本身还提供了大量方法,例如,最常用的静态方法parseInt()
可以把字符串解析成一个整数:
1 | int x1 = Integer.parseInt("100"); // 100 |
Integer
还可以把整数格式化为指定进制的字符串:
1 | public class Main { |
注意:上述方法的输出都是String
,在计算机内存中,只用二进制表示,不存在十进制或十六进制的表示方法。int n = 100
在内存中总是以4字节的二进制表示:
1 | ┌────────┬────────┬────────┬────────┐ |
我们经常使用的System.out.println(n);
是依靠核心库自动把整数格式化为10进制输出并显示在屏幕上,使用Integer.toHexString(n)
则通过核心库自动把整数格式化为16进制。
这里我们注意到程序设计的一个重要原则:数据的存储和显示要分离。
处理无符号整型
在Java中,并没有无符号整型(Unsigned)的基本数据类型。byte
、short
、int
和long
都是带符号整型,最高位是符号位。而C语言则提供了CPU支持的全部数据类型,包括无符号整型。无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成。
例如,byte是有符号整型,范围是-128
-+127
,但如果把byte
看作无符号整型,它的范围就是0
~`255。我们把一个负的
byte按无符号整型转换为
int`:
1 | public class Main { |
因为byte
的-1
的二进制表示是11111111
,以无符号整型转换后的int
就是255
。
类似的,可以把一个short
按unsigned转换为int
,把一个int
按unsigned转换为long
。
2.Java Character 类
Character类提供了一系列方法来操纵字符。你可以使用Character的构造方法
创建一个Character类对象
1 | Character ch = new Character('a'); |
在某些情况下,Java编译器会自动创建一个Character对象。例如,将一个char类型的参数传递给需要一个Character类型参数的方法时,那么编译器会自动地将char类型参数转换为Character对象。这种特征称为装箱
,反过来称为拆箱。
1 | // 原始字符 'a' 装箱到 Character 对象 ch 中 |
转义序列 | 描述 |
---|---|
\t | 在文中该处插入一个tab键 |
\b | 在文中该处插入一个后退键 |
\n | 在文中该处换行 |
\r | 在文中该处插入回车 |
\f | 在文中该处插入换页符 |
\‘ | 在文中该处插入单引号 |
\“ | 在文中该处插入双引号 |
\\ | 在文中该处插入反斜杠 |
3.Java String 类
参考文章:廖雪峰-字符串和编码
字符串创建
字符串广泛应用 在 Java 编程中,Java 字符串属于对象,Java 提供了 String 类来创建和操作字符串。String 创建的字符串存储在公共池中,而 new 创建的字符串对象在堆上:
1 | String s1 = "Runoob"; // String 直接创建 |
**注意:**String 类是
不可改变
的,所以你一旦创建了 String 对象,那它的值就无法改变了。
字符串比较
String 中 ==
比较引用地址是否相同,equals()
比较字符串的内容是否相同:
1 | String s1 = "Hello"; // String 直接创建 |
String类中的一个有意思的函数:getBytes()
使用平台的默认字符集将字符串编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
1 | String Str1 = new String("hello hulingF"); |
String
类还提供了多种方法来搜索子串、提取子串。常用的方法有:
1 | // 是否包含子串: |
注意到contains()
方法的参数是CharSequence
而不是String
,因为CharSequence
是String
实现的一个接口。
搜索子串的更多的例子:
1 | "Hello".indexOf("l"); // 2 |
提取子串的例子:
1 | "Hello".substring(2); // "llo" |
注意索引号是从0
开始的。
去除首尾空白字符
使用trim()
方法可以移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
:
1 | " \tHello\r\n ".trim(); // "Hello" |
注意:trim()
并没有改变字符串的内容,而是返回了一个新字符串。
另一个strip()
方法也可以移除字符串首尾空白字符。它和trim()
不同的是,类似中文的空格字符\u3000
也会被移除:
1 | "\u3000Hello\u3000".strip(); // "Hello" |
String
还提供了isEmpty()
和isBlank()
来判断字符串是否为空和空白字符串:
1 | "".isEmpty(); // true,因为字符串长度为0 |
替换子串
要在字符串中替换子串,有两种方法。一种是根据字符或字符串替换:
1 | String s = "hello"; |
另一种是通过正则表达式替换:
1 | String s = "A,,B;C ,D"; |
上面的代码通过正则表达式,把匹配的子串统一替换为","
。
分割字符串
要分割字符串,使用split()
方法,并且传入的也是正则表达式:
1 | String s = "A,B,C,D"; |
拼接字符串
拼接字符串使用静态方法join()
,它用指定的字符串连接字符串数组:
1 | String[] arr = {"A", "B", "C"}; |
格式化字符串
字符串提供了formatted()
方法和format()
静态方法,可以传入其他参数,替换占位符,然后生成新的字符串:
1 | public class Main { |
类型转换
要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()
。这是一个重载方法,编译器会根据参数自动选择合适的方法:
1 | String.valueOf(123); // "123" |
要把字符串转换为其他类型,就需要根据情况。例如,把字符串转换为int
类型:
1 | int n1 = Integer.parseInt("123"); // 123 |
把字符串转换为boolean
类型:
1 | boolean b1 = Boolean.parseBoolean("true"); // true |
要特别注意,Integer
有个getInteger(String)
方法,它不是将字符串转换为int
,而是把该字符串对应的系统变量转换为Integer
:
1 | Integer.getInteger("java.version"); // 版本号,11 |
4.Java StringBuilder 类
参考文章:廖雪峰-StringBuilder
StringBuilder由来
考察下面的循环代码:
1 | String s = ""; |
虽然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
为了能高效拼接字符串,Java标准库提供了StringBuilder
,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象:
1 | StringBuilder sb = new StringBuilder(1024); |
StringBuilder创建
当对字符串进行修改的时候,需要使用 StringBuffer
和 StringBuilder
类。和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。
StringBuilder
类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全
的(不能同步访问)。
由于 StringBuilder 相较于 StringBuffer 有速度优势
,所以多数情况下建议使用 StringBuilder 类。
1 | public class RunoobTest{ |
StringBuilder彩蛋
注意:StringBuilder
还可以进行链式操作!
1 | public class Main { |
如果我们查看StringBuilder
的源码,可以发现,进行链式操作的关键是,定义的append()
方法会返回this
,这样,就可以不断调用自身的其他方法。
仿照StringBuilder
,我们也可以设计支持链式操作的类。例如,一个可以不断增加的计数器:
1 | public class Main { |
注意:对于普通的字符串
+
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
的操作。在运行期,StringConcatFactory
会自动把字符串连接操作优化为数组复制或者StringBuilder
操作。
5.Java StringJoiner类
要高效拼接字符串,应该使用StringJoiner
。很多时候,我们拼接的字符串像这样:
1 | public class StringJoinerTest { |
6.Java Bean
JavaBean定义
在Java中,有很多class
的定义都符合这样的规范:
- 若干
private
实例字段; - 通过
public
方法来读写实例字段。
1 | public class Person { |
如果读写方法符合以下这种命名规范:
1 | // 读方法: |
那么这种class
被称为JavaBean
:
上面的字段是xyz
,那么读写方法名分别以get
和set
开头,并且后接大写字母开头的字段名Xyz
,因此两个读写方法名分别是getXyz()
和setXyz()
。boolean
字段比较特殊,它的读方法一般命名为isXyz()
:
1 | // 读方法: |
我们通常把一组对应的读方法(getter
)和写方法(setter
)称为属性(property
)。例如,name
属性:
- 对应的读方法是
String getName()
- 对应的写方法是
setName(String)
属性只需要定义getter
和setter
方法,不一定需要对应的字段。例如,child
只读属性定义如下:
1 | public class Person { |
可以看出,getter
和setter
也是一种数据封装的方法。
枚举JavaBean属性
要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector
:
1 | public class Main { |
注意
class
属性是从Object
继承的getClass()
方法带来的,是只读属性。
7.Java BigInteger类
在Java中,由CPU原生提供的整型最大范围是64位long
型整数。使用long
型整数可以直接通过CPU指令进行计算,速度非常快。
如果我们使用的整数范围超过了long
型怎么办?这个时候,就只能用软件来模拟一个大整数。java.math.BigInteger
就是用来表示任意大小的整数。BigInteger
内部用一个int[]
数组来模拟一个非常大的整数。
如果BigInteger
表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用intValueExact()
、longValueExact()
等方法,在转换时如果超出范围,将直接抛出ArithmeticException
异常。
1 | public class BigIntegerTest { |
BigInteger
也是不变类,并且继承自Number
。
8.Java BigDecimal类
使用BigDecimal
和BigInteger
类似,BigDecimal
可以表示一个任意大小且精度完全准确的浮点数。
1 | BigDecimal bd = new BigDecimal("123.4567"); |
BigDecimal
用scale()
表示小数位数,例如:
1 | BigDecimal d1 = new BigDecimal("123.45"); |
通过BigDecimal
的stripTrailingZeros()
方法,可以将一个BigDecimal
格式化为一个相等的,但去掉了末尾0的BigDecimal
:
1 | BigDecimal d1 = new BigDecimal("123.4500"); |
如果一个BigDecimal
的scale()
返回负数,例如,-2
,表示这个数是个整数,并且末尾有2个0。
可以对一个BigDecimal
设置它的scale
,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:
1 | public class Main { |
运算BigDecimal
对BigDecimal
做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:
1 | BigDecimal d1 = new BigDecimal("123.456"); |
还可以对BigDecimal
做除法的同时求余数:
1 | public class Main { |
调用divideAndRemainder()
方法时,返回的数组包含两个BigDecimal
,分别是商和余数,其中商总是整数,余数不会大于除数。我们可以利用这个方法判断两个BigDecimal
是否是整数倍数:
1 | BigDecimal n = new BigDecimal("12.75"); |
比较BigDecimal
在比较两个BigDecimal
的值是否相等时,要特别注意,使用equals()
方法不但要求两个BigDecimal
的值相等,还要求它们的scale()
相等:
1 | BigDecimal d1 = new BigDecimal("123.456"); |
必须使用compareTo()
方法来比较,它根据两个值的大小分别返回负数、正数和0
,分别表示小于、大于和等于。
总是使用compareTo()比较两个BigDecimal的值,不要使用equals()!
如果查看BigDecimal
的源码,可以发现,实际上一个BigDecimal
是通过一个BigInteger
和一个scale
来表示的,即BigInteger
表示一个完整的整数,而scale
表示小数位数:
1 | public class BigDecimal extends Number implements Comparable<BigDecimal> { |
BigDecimal
也是从Number
继承的,也是不可变对象。
9.Java 常用工具类
HexFormat
在处理byte[]
数组时,我们经常需要与十六进制字符串转换,自己写起来比较麻烦,用Java标准库提供的HexFormat
则可以方便地帮我们转换。
要将byte[]
数组转换为十六进制字符串,可以用formatHex()
方法:
1 | import java.util.HexFormat; |
如果要定制转换格式,则使用定制的HexFormat
实例:
1 | // 分隔符为空格,添加前缀0x,大写字母: |
从十六进制字符串到byte[]
数组转换,使用parseHex()
方法:
1 | byte[] bs = HexFormat.of().parseHex("48656c6c6f"); |
Random
Random
用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。
要生成一个随机数,可以使用nextInt()
、nextLong()
、nextFloat()
、nextDouble()
:
1 | Random r = new Random(); |
有童鞋问,每次运行程序,生成的随机数都是不同的,没看出伪随机数的特性来。
这是因为我们创建Random
实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。如果我们在创建Random
实例时指定一个种子,就会得到完全确定的随机数序列:
1 | import java.util.Random; |
我们使用的Math.random()
实际上内部调用了Random
类,所以它也是伪随机数,只是我们无法指定种子。
SecureRandom
有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom
就是用来创建安全的随机数的:
1 | SecureRandom sr = new SecureRandom(); |
SecureRandom
无法指定种子,它使用RNG(random number generator)算法。JDK的SecureRandom
实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器:
1 | import java.util.Arrays; |
SecureRandom
的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,时刻牢记必须使用SecureRandom
来产生安全的随机数。
需要使用安全随机数的时候,必须使用SecureRandom,绝不能使用Random!
10.Java 数组
参考文章:Java数组的内存布局、Java数组在内存中如何存放与分配 - 黎先生 - 博客园 (cnblogs.com)](https://www.cnblogs.com/lixiansheng/p/11299993.html))
Java 中数组和类对象都被看做为引用类型
(与之对应的是八个基本类型
),==创建的数组对象位于堆内存中,而数组引用变量位于栈内存中==。
Java 语言是典型的静态语言,因此 Java 数组是静态的,即当数组被初始化之后,该数组所占的内存空间、数组长度都是不可变的。Java 程序中的数组必须经过初始化才可使用。所谓初始化,即创建实际的数组对象,也就是在内存中为数组对象分配内存空间,并为每个数组元素指定初始值。
数组的初始化有以下两种方式:
静态初始化
:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度。
动态初始化
:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。
不管采用哪种方式初始化Java数组,一旦初始化完成,该数组的长度就不可改变,Java语言允许通过数组的length属性来访问数组的长度。
1 | public class ArrayReferenceTest { |
前面已经指出,Java 语言的数组变量是引用类型的变量。books、names 、strArr 这三个变量,以及各自引用的数组在内存中的分配示意图如图所示:
执行动态初始化时,程序员只需指定数组的长度,即为每个数组元素指定所需的内存空间,系统将负责为这些数组元素分配初始值(初始化规则与成员变量/静态变量的规则一样)。
需要指出的是,Java 的数组变量是一种引用类型的变量,数组变量并不是数组本身,它只是指向堆内存中的数组对象。
基本数据类型数组的初始化
对于基本类型数组而言,数组元素的值直接存储在对应的数组元素中,因此基本类型数组的初始化比较简单:程序直接先为数组分配内存空间,再将数组元素的值存入对应内存里。
1 | public class PrimitiveArrayTest{ |
==所有局部变量都是放在栈内存里保存的,不管其是基本类型的变量,还是引用类型的变量,都是存储在各自的方法栈内存中的;但引用类型的变量所引用的对象(包括数组、普通的 Java 对象)则总是存储在堆内存中。==
对于 Java 语言而言,堆内存中的对象(不管是数组对象,还是普通的 Java 对象)通常不允许直接访问,为了访问堆内存中的对象,通常只能通过引用变量。这也是很容易混淆的地方。 例如,iArr 本质上只是 main 栈区的引用变量,但使用 iArr.length、iArr[2] 时,系统将会自动变为访问堆内存中的数组对象。
对于很多 Java 程序员而言,他们最容易混淆的是:引用类型的变量何时只是栈内存中的变量本身,何时又变为引用实际的 Java 对象。其实规则很简单:引用变量本质上只是一个指针,只要程序通过引用变量访问属性,或者通过引用变量来调用方法,该引用变量就会由它所引用的对象代替。
1 | public class PrimitiveArrayTest{ |
对于①行代码而言,虽然此时的 iArr 数组变量并未引用到有效的数组对象,但程序在①行代码处并不会出现任何问题,因为此时并未通过 iArr 访问属性或调用方法,因此程序只是访问 iArr 引用变量本身,并不会去访问 iArr 所引用的数组对象。
对于②行代码而言,此时程序通过 iArr 访问了 length 属性,程序将自动变为访问 iArr 所引用的数组对象,这就要求 iArr 必须引用一个有效的对象。
引用数据类型数组的初始化
引用类型数组的数组元素依然是引用类型的,因此数组元素里存储的还是引用,它指向另一块内存,这块内存里存储了该引用变量所引用的对象(包括数组和 Java 对象)。
1 | class Person{ |
Arrays 类
==java.util.Arrays== 类能方便地操作数组,它提供的所有方法都是静态的。
序号 | 方法和说明 |
---|---|
1 | public static int binarySearch(Object[] a, Object key) 用二分查找算法在给定数组中搜索给定值的对象(byte,int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引。 |
2 | public static boolean equals(long[] a, long[] a2) 如果两个指定的 long 型数组彼此相等,则返回 true。如果两个数组包含相同数量的元素,并且两个数组中的所有相应元素对都是相等的,则认为这两个数组是相等的。换句话说,如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。同样的方法适用于所有的其他基本数据类型(byte,short,Int等)。 |
3 | public static void fill(int[] a, int val) 将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。同样的方法适用于所有的其他基本数据类型(byte,short,int等)。 |
4 | public static void sort(Object[] a) 对指定对象数组根据其元素的自然顺序进行升序排列。同样的方法适用于所有的其他基本数据类型(byte,short,int等)。 |
11.Java 中的字符编码
参考文章:廖雪峰-字符串和编码
参考文章:字符编码笔记
ASCII码
在早期的计算机系统中,为了给字符编码,美国国家标准学会(American National Standard Institute:ANSI)制定了一套英文字母、数字和常用符号的编码,它占用一个字节,编码范围从0
到127
,最高位始终为0
,称为ASCII
编码。例如,字符'A'
的编码是0x41
,字符'1'
的编码是0x31
。
如果要把汉字也纳入计算机编码,很显然一个字节是不够的。GB2312
标准使用两个字节表示一个汉字,其中第一个字节的最高位始终为1
,以便和ASCII
编码区分开。例如,汉字'中'
的GB2312
编码是0xd6d0
。类似的,日文有Shift_JIS
编码,韩文有EUC-KR
编码,这些编码因为标准不统一,同时使用,就会产生冲突。
为了统一全球所有语言的编码,全球统一码联盟发布了Unicode
编码,它把世界上主要语言都纳入同一个编码,这样,中文、日文、韩文和其他语言就不会冲突。
Unicode方案
Unicode
没有规定字符对应的二进制码具体如何存储。以汉字“汉”为例,它的Unicode码位
是0x6c49
,对应的二进制数是 110110001001001,二进制数有15位,这也就说明了它至少需要2
个字节来表示。可以想象,在 Unicode 字典中往后的字符可能就需要3个字节或者4个字节,甚至更多字节来表示了。
这就导致了一些问题,计算机怎么知道你这个2个字节表示的是一个字符,而不是分别表示两个字符呢?这里我们可能会想到,那就取个最大的,假如Unicode中最大的字符用4字节就可以表示了,那么我们就将所有的字符都用4个字节来表示,不够的就往前面补0。这样确实可以解决编码问题,但是却造成了空间的极大浪费,如果是一个英文文档,那文件大小就大出了3倍,这显然是无法接受的。
它们造成的结果是:
- 出现了Unicode的多种存储方式,也就是说有许多种不同的二进制格式可以用来表示 Unicode;
- Unicode在很长一段时间内无法推广,直到互联网的出现。
于是,为了较好的解决Unicode的编码问题,UTF-8
和UTF-16
两种当前比较流行的编码方式诞生了。当然还有一个UTF-32
的编码方式,也就是上述那种定长编码,字符统一使用4个字节,虽然看似方便,但是却不如另外两种编码方式使用广泛。
UTF-8
UTF-8是一个非常惊艳的编码方式,漂亮的实现了==对ASCII码的向后兼容==,以保证Unicode可以被大众接受。这里的关系是,UTF-8是Unicode的实现方式之一。
UTF-8是目前互联网上使用最广泛的一种Unicode编码方式,它的最大特点就是可变长。它可以使用1 - 4个字节表示一个字符,根据字符的不同变换长度。编码规则如下:
- 对于
单个字节
的字符,第一位设为0
,后面的7位对应这个字符的Unicode码位
。因此,对于英文中的0-127号字符,与ASCII码完全相同。这意味着ASCII码那个年代的文档用UTF-8编码打开完全没有问题。 - 对于需要使用
N个字节
来表示的字符(N>1),第一个字节的前N位都设为1,第N+1位设为0,剩余的N-1个字节的前两位都设位10,剩下的二进制位则使用这个字符的Unicode码位来填充。
根据上面编码规则对照表,进行UTF-8编码和解码
就简单多了。下面以汉字“汉”为利,具体说明如何进行UTF-8编码和解码。
“汉”的Unicode码位
是0x6c49
(110 1100 0100 1001),通过上面的对照表可以发现,0x6c49位于第三行的范围,那么得出其格式为1110xxxx 10xxxxxx 10xxxxxx
。接着,从“汉”的二进制数最后一位开始,从后向前依次填充对应格式中的x,多出的x用0补上。这样,就得到了“汉”的UTF-8编码为11100110 10110001 10001001,转换成十六进制就是0xE6B789
。
解码的过程也十分简单:如果一个字节的第一位是0 ,则说明这个字节对应一个字符;如果一个字节的第一位1,那么连续有多少个1,就表示该字符占用多少个字节。
Big/Little endian
以汉字严为例,Unicode码位是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,这就是Big endian方式;25在前,4E在后,这是Little endian方式。第一个字节在前,就是”大头方式”(Big endian),第二个字节在前就是”小头方式”(Little endian)。
那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?
==Unicode规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(zero width no-break space),用FEFF表示。这正好是两个字节,而且FF比FE大1。==如果一个文本文件的头两个字节是FE FF
,就表示该文件采用大头
方式;如果头两个字节是FF FE
,就表示该文件采用小头
方式。
UTF-16
Unicode的编码空间从U+0000
到+10FFFF
,共有1,112,064个码位(code point
)可用来映射字符。Unicode的编码空间可以划分为17个平面(plane),每个平面包含2^16^(65,536)个码位。17个平面的码位可表示为从U+xx0000
到U+xxFFFF
,其中xx表示十六进制值从0016到1016,共计17个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0),其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从U+D800
到U+DFFF
之间的码位区段是永久保留不映射到Unicode字符。==UTF-16就利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。==
从U+0000至U+D7FF以及从U+E000至U+FFFF的码位
第一个Unicode平面(码位从U+0000至U+FFFF)包含了最常用的字符。该平面被称为基本多语言平面,缩写为BMP(Basic Multilingual Plane)。UTF-16编码这个范围内的码位为16比特长的单个码元,数值等价于对应的码位。
从U+10000到U+10FFFF的码位
辅助平面(Supplementary Planes)中的码位,在UTF-16中被编码为一对16比特长的码元(即32位元,4字节),称作代理对(Surrogate Pair),具体方法是:
- 码位减去
0x10000
,得到的值的范围为20比特长的0...0xFFFFF
。 - 高位的10比特的值(值的范围为
0...0x3FF
)被加上0xD800
得到第一个码元或称作高位代理(high surrogate),值的范围是0xD800...0xDBFF
。由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead surrogates)。 - 低位的10比特的值(值的范围也是
0...0x3FF
)被加上0xDC00
得到第二个码元或称作低位代理(low surrogate),现在值的范围是0xDC00...0xDFFF
。由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail surrogates)。
上述算法可理解为:辅助平面中的码位从U+10000到U+10FFFF,共计FFFFF个,即2^20^=1,048,576个,需要20位来表示。如果用两个16位长的整数组成的序列来表示,第一个整数(称为前导代理)要容纳上述20位的前10位,第二个整数(称为后尾代理)容纳上述20位的后10位。还要能根据16位整数的值直接判明属于前导整数代理的值的范围(2^10^=1024),还是后尾整数代理的值的范围(也是2^10^=1024)。因此,需要在基本多语言平面中保留不对应于Unicode字符的2048个码位,就足以容纳前导代理与后尾代理所需要的编码空间。这对于基本多语言平面总计65536个码位来说,仅占3.125%。
由于前导代理、后尾代理、BMP中的有效字符的码位,三者互不重叠,搜索是简单的:一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。这意味着UTF-16是自同步(self-synchronizing)的:可以通过仅检查一个码元来判定给定字符的下一个字符的起始码元。UTF-8也有类似优点,但许多早期的编码模式就不是这样,必须从头开始分析文本才能确定不同字符的码元的边界。
ISO-8859-1/Latin-1
ISO8859-1,通常叫做Latin-1。Latin-1包括了书写所有西方欧洲语言不可缺少的附加字符。
ISO-8859-1编码是单字节编码
,向下兼容ASCII
,其编码范围是0x00-0xFF
,==0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。==
ISO-8859-1收录的字符除ASCII收录的字符外,还包括西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。欧元符号出现的比较晚,没有被收录在ISO-8859-1当中。
因为ISO-8859-1编码范围使用了单字节内的所有空间,在支持ISO-8859-1的系统中传输和存储其他任何编码的字节流都不会被抛弃。换言之,把其他任何编码的字节流当作ISO-8859-1编码看待都没有问题。这是个很重要的特性,MySQL数据库默认编码是Latin1就是利用了这个特性。ASCII编码是一个7位的容器,ISO-8859-1编码是一个8位的容器。
ISO-8859-1的较低部分
(从1到127之间的代码)是最初的7比特ASCII。
ISO-8859-1的较高部分
(从160到255之间的代码)全都有实体名称。
延伸阅读
char数据类型是一个采用UTF-16编码表示Unicode码位的码元。
码位:码位(code point)是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,码位采用十六进制书写,并加上前缀U+,例如U+0041就是字母A的码位。
Unicode的码位可以分成17个代码级别(code plane)。第一个代码级别称为基本的多语言级别(简称BMP),码位从U+0000到U+FFFF,其中包括了经典的Unicode代码;其余的16个附加级别,码位从U+10000到U+FFFFF,其中包括了一些辅助字符。
码元:在BMP中,每个字符用16位表示,称为码元(code unit)。辅助字符采用一对连续的码元进行编码。
对于不同版本的JDK,String
类在内存中有不同的优化方式。具体来说,早期JDK版本的String
总是以char[]
存储,它的定义如下:
1 | public final class String { |
而较新的JDK版本的String
则以byte[]
存储:如果String
仅包含ASCII字符,则每个byte
存储一个字符,否则,每两个byte
存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String
通常仅包含ASCII字符:
1 | public final class String { |
对于使用者来说,String
内部的优化不影响任何已有代码,因为它的public
方法签名是不变的。