Java Random、regex正则、Math类、String字符串

Java Random、regex正则、Math类、String字符串

Random 随机数

实际上,计算机只能为我们提供伪随机数,所谓伪随机数就是按照一定算法模拟产生的,其结果是确定的,是可见的

计算机产生随机数的过程,是根据一个种子seed为基准,以某个递推公式推算出来的一系列数,当递推的范围足够大往复性足够强、又符合正态分布或平均分布时,我们就可以认为这是一个近似的真随机数;

在 java.util 包中,Random 类就是一个伪随机数生成器
Random 类的默认种子在 JDK1.5 版本以前是System.currentTimeMillis()的返回值;
由于 System.currentTimeMillis() 的粒度太大,随机性不是很好,所以改为了System.nanoTime()的返回值。

System.currentTimeMillis():返回自 1970-01-01 00:00:00 UTC 到现在经过的毫秒(ms)数;绝对时间时间戳
System.nanoTime():JDK1.5 起,由 JVM 提供的纳秒精度的时间,开始时间不确定(可能是负的);相对时间,通常用于衡量一段代码的执行时间。

大家可能已经发现了,在 java.lang.Math 类中也有一个 random() 静态方法,和 java.util.Random 有什么区别?

看一下 Math.random() 的源码就明白了:

也就是说,Math.random() 就是调用的 java.util.Random 类的 nextDouble() 方法,返回区间[0.0d, 1.0d)的 double 随机数;

java.util.Random 的实例是线程安全的;但是,跨线程同时使用相同的 java.util.Random 实例可能会导致同步锁的竞争,从而导致性能下降。在多线程设计中考虑使用java.util.concurrent.ThreadLocalRandom(稍后讲解);

Random

构造函数
public Random():无参构造函数,System.nanoTime()为 seed;
public Random(long seed):使用指定的 seed,long 长整型;

我们使用一样的种子,看一下是不是都是一样的数字序列:

从运行结果看得出,一个 seed 种子就代表了属于它的一系列数字,也说明了 Random 生成的数字是伪随机数;

常用方法:
synchronized public void setSeed(long seed):设置新种子 seed
public void nextBytes(byte[] bytes):随机字节数组
public int nextInt():随机 int 值,该值介于 int 的区间[-2^31, 2^31 - 1]
public int nextInt(int bound):随机 int 值,介于区间[0, bound)
public long nextLong():随机 long 值,该值介于 long 的区间[-2^63, 2^63 - 1]
public boolean nextBoolean():随机 boolean 值,true 或 false
public float nextFloat():随机 float 值,介于区间[0.0f, 1.0f)
public double nextDouble():随机 double 值,介于区间[0.0d, 1.0d)

JDK1.8 新增(与 Stream API 相关):
public IntStream ints(long streamSize)
public IntStream ints():相当于 ints(Long.MAX_VALUE);
public IntStream ints(long streamSize, int randomNumberOrigin, int randomNumberBound):区间[origin, bound)
public IntStream ints(int randomNumberOrigin, int randomNumberBound):同上,无限流;

public LongStream longs(long streamSize)
public LongStream longs()
public LongStream longs(long streamSize, long randomNumberOrigin, long randomNumberBound)
public LongStream longs(long randomNumberOrigin, long randomNumberBound)

public DoubleStream doubles(long streamSize)
public DoubleStream doubles()
public DoubleStream doubles(long streamSize, double randomNumberOrigin, double randomNumberBound)
public DoubleStream doubles(double randomNumberOrigin, double randomNumberBound)

生成指定区间[m, n]m < n的 int 随机数的方法:nextInt(n + 1 - m) + m

观察 Random 对象的 nextInt(int bound) 方法,发现这个方法将生成 0 ~ bound 之间随机取值的整数;包括 0,排除 bound;比如rand.nextInt(100);,生成区间[0, 100)之间的整数(实际就是for (int i = 0; i < 100; i++) { ... });

那么如果要获得区间[1, 100]的随机数,该怎么办呢?
稍微动动脑筋就可以想到:区间 [0, 100) 内的整数,实际上就是区间 [0, 99];
因为最大边界为 100,可惜不能等于 100,因此最大可能产生的“整数”就是 99;

既然 rand.nextInt(100) 获得的值是区间 [0, 99],那么在这个区间左右各加 1,就得到了区间 [1, 100];
因此,代码写成:rand.nextInt(100) + 1;即可;运行下面的代码,将获得 [1, 100] 的 10 个取值:

同理,很容易知道如果要获得随机的两位整数,代码写成:rand.nextInt(90) + 10;
在 nextInt() 方法中作为参数的数字 90 表示要生成的随机数的个数(两位整数有 90 个),加上后面的数字 10 ,表示区间的最小取值为 10(含);

生成随机三位数的代码为:rand.nextInt(900) + 100;([100, 999])
生成区间 [64, 128] 中随机值的代码为:rand.nextInt(65) + 64;

取值可能性的数量是如何计算出来的呢?当然是最大取值 - 最小取值 + 1
因此,获取在区间[min, max]的随机整数的公式为:nextInt(max - min + 1) + min

例子:

ThreadLocalRandom

java.util.Random 是线程安全的,但是如果在多线程环境中使用同一个 Random 实例,会造成同步锁的争用,降低并发性。在 JDK1.7 之后,J.U.C 包中添加了 ThreadLocalRandom(Random 的子类),用于获取线程本地的 Random 伪随机数生成器。

Date 日期时间

在 JDK1.8 之前,我们主要通过 java.util.Date、java.util.Calendar 来处理日期时间;但是它们用起来并不顺手,于是很多时候都是使用第三方的 Joda-Time;不过到了 Java 8,终于引入了新的日期时间 API(java.time 包),该包由 Joda-Time 作者和 Oracle 共同开发,因此和 Joda-Time 有很多相似之处,具体请查看 - Java8 新日期时间 API

java.util.Date类简介
Date 用于表示特定的即时时间(瞬时时间),精确到毫秒。在 JDK1.1 之前,Date 类有两个附加的功能:它允许将日期解释为年,月,日,时,分和秒值;它也允许格式化和解析日期时间字符串。
不幸的是,这些功能的 API 不适合国际化。从 JDK1.1 开始,应使用 Calendar 类在日期和时间字段之间进行转换,并使用 DateFormat 类来格式化和解析日期时间字符串。而 Date 中的相应方法已被弃用。

时间输出格式化 System.out.printf()

使用 PrintStream.printf/format() 进行日期时间格式化输出

使用Date timestamp = new Date()也是一样的。

使用 SimpleDateFormat 格式化日期时间
java.text.SimpleDateFormat 是 java.text.DateFormat 抽象类的子类。SimpleDateFormat 用于以区域敏感的方式格式化和解析日期。

例子:

同时,SimpleDateFormat 的 parse(String s) 方法可以从字符串中解析指定格式的 datetime,并返回 Date 实例:

java.util.Calendar类简介
Calendar 类是一个抽象类,它提供了在特定时刻之间进行转换的方法,以及一组日历字段(如 YEAR,MONTH,DAY_OF_MONTH,HOUR 等等),以及用于处理日历字段的方法,如获取下周的日期。即时时间可以用一个毫秒值来表示,该值是从 1970 年 1 月 1 日 00:00:00.000 GMT(公历)开始的偏离时间。

java.util.TimeZone类简介
TimeZone 表示时区偏移量。通常我们使用 getDefault() 方法获取当前系统的默认时区,如"Asia/Shanghai"

常用时区 ID:协调世界时UTC中国-上海Asia/Shanghai(中国标准时区,CST)。

Calendar、TimeZone 的综合例子:

Locale 区域

Locale 的中文翻译:区域语言环境本地环境。在 Java 中,一个 Locale 对象表示一个特定的地理,政治或文化区域。我们将依赖 Locale 的操作称为区域设置敏感的操作(Date、Calendar 等)。例如,打印一个时间是一个区域敏感的操作,程序应该根据用户所在区域的惯例进行格式化。

在 Linux 中,Locale 无处不在,最常见的就是en_US.UTF-8zh_CN.UTF-8zh_CN.GBK

注意,一个 Locale 对象仅仅表示一个特定的区域,如果需要进行国际化(支持多种区域环境),还需要定义对应的资源文件(使用 java.util.ResourceBundle 类)

一个 Locale 对象主要使用以下字段描述(大部分情况下):

  • language语言(小写),比如中文zh、英文en
  • country国家(大写),比如大陆CN、香港HK、台湾TW、美国US
  • 它们之间使用_下划线连接,比如zh_CNzh_TWen_USzh_SG

Locale 类的具体方法:

ResourceBundle 类
java.util.ResourceBundle 是抽象类,它有两个子类:ListResourceBundle(二维数组)、PropertyResourceBundle(资源文件)。一般来说,我们都是使用后者。

资源文件的命名

  • basename_zh_CN.properties:中文(中国)
  • basename_zh.properties:中文
  • basename.properties:默认

ResourceBundle 在查找时会按照匹配度依次往下匹配。比如当前系统区域为 zh_CN,则使用第一个;如果当前系统区域为 zh_TW,则使用第二个;如果当前系统区域为 en_US,则使用第三个;如果没有定义默认的 properties 文件,将抛出 MissingResourceException 运行时异常。

资源文件的格式

  • key = value:键值对,key 区分大小写,value 的前导空格将被忽略,value 中可使用\t\n等转移序列;
  • key 和 value 都可以有中文,但是必须使用 unicode 字符,JDK 提供了 native2ascii 工具用于 unicode 的转换。

native2ascii 用法

  • native2ascii [inputfile] [outputfile],输入文件和输出文件可以相同,如果省略则使用标准输入、标准输出;
  • 将非 ISO-8859-1 字符集(西欧语言,单字节字符集,以 ASCII 为基础,兼容 ASCII)转换为\uXXXXUnicode 字符;
  • -reverse进行反向转换、-encoding encoding_name指定 native 编码、-Joption指定 JVM 参数,如-J-Xms50M

ResourceBundle 类的主要方法

国际化的例子:

regex 正则

正则表达式的派别

Java 正则表达式
java.util.regex 包主要包括以下三个类(PCRE 正则,regex 模式支持中文):

  • Pattern 类:Pattern 对象是一个正则表达式的编译表示;Pattern 类没有公共构造方法;要创建一个 Pattern 对象,需要调用 Pattern.compile(String regex) 静态方法,它返回一个 Pattern 对象;
  • Matcher 类:Matcher 对象是对输入字符串进行解释和匹配操作的引擎;与 Pattern 类一样,Matcher 也没有公共构造方法;需要调用 Pattern 对象的 matcher() 方法来获得一个 Matcher 对象;
  • PatternSyntaxException:一个 RuntimeException 异常类,它表示一个正则表达式模式中的语法错误。

Pattern 仅仅是一个正则模式的抽象,如果需要进行字符序列匹配,必须调用Matcher matcher(CharSequence input)方法来获取一个与给定输入序列相关的 Matcher 正则匹配器。这样做的好处是一个 Pattern 对象可以重复利用,用于匹配不同的输入字符串。

正则语法

正则测试工具[在线工具] -> 正则表达式测试工具在线调试与分享 - Zjmainstay
正则测试工具[Windows版] -> Learn, Create, Understand, Test, Use and Save Regular Expressions with RegexBuddy

其它说明

扩展知识

正则表达式引擎非确定型有穷自动机(NFA)确定型有穷自动机(DFA)

  • DFA:文本主导确保获得最长的匹配文本预编译复杂匹配简单快速特性较少
  • 传统型NFA:模式主导找到匹配后丢弃其它分支预编译简单匹配复杂特性多
  • POSIX NFA:与传统型NFA差别不大,最大的区别是它会尝试所有分支,确保获得最长的匹配文本,速度略慢

大多数编程语言都是使用传统型NFA引擎,如 Java、Perl、PHP、Python、.NET;而 grep、awk 则使用 DFA。

两类正则引擎要顺利工作,都必须有一个正则式和一个文本串,一个捏在手里,一个吃下去:

  • DFA 捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。
  • NFA 则捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来:“某年某月某日在某处匹配上了!”,然后接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。

DFA 与 NFA 机制上的不同带来 5 个影响:

  1. DFA 对于文本串里的每一个字符只需扫描一次,比较快,但特性较少;NFA 要翻来覆去吃字符、吐字符,速度慢,但是特性丰富,因此应用广泛,当今主要的正则表达式引擎,如 Perl、Ruby、Python、Java 和 .NET 的 regex 库,都是 NFA 的。
  2. 只有 NFA 才支持 lazy(懒惰量词)和 backreference(反向引用)等特性。
  3. NFA 急于邀功请赏,所以最左子正则式优先匹配成功,因此偶尔会错过最佳匹配结果;DFA 则是最长的左子正则式优先匹配成功。
  4. NFA 缺省采用 greedy(贪婪量词),使用相应的修饰符可将贪婪量词转换为懒惰量词、占有量词。
  5. NFA 可能会陷入递归调用的陷阱而表现得性能极差。

环视的八种组合

  1. 顺序肯定常规[a-z]+(?=;):字母序列后面跟着;
  2. 顺序肯定变种(?=[a-z]+$).+$:字母序列
  3. 顺序否定常规[a-z]+\b(?!;):不以;结尾的字母序列
  4. 顺序否定变种(?!.*?[lo0])\b[a-z0-9]+\b:不包含l/o/0的字母数字系列
  5. 逆序肯定常规(?<=:)[0-9]+:后面的数字序列
  6. 逆序肯定变种\b[0-9]\b(?<=[13579]):0~9 中的奇数
  7. 逆序否定常规(?<!age)=([0-9]+):参数名不为 age 的数据
  8. 逆序否定变种\b[a-z]+(?<!z)\b:不以 z 结尾的单词

\b单词边界
很多人都知道\b是匹配单词边界,但是关于”单词”的范围,却很少提及。正则中所指的单词就是\w定义的字符组成的序列。

\w是一个预定义字符集合,它等价于[a-zA-Z0-9_],即字母数字下划线组成的字符序列被称为单词。如果启用了 Unicode 字符支持,则\w还会匹配 Unicode 中定义的单词字符,如汉字、全角数字。

\b是一个零宽子表达式,它只匹配位置,因此单词边界就是一侧是\w而另一侧不是\w的这样一个位置,因此\b等价于(?<=\w)(?!\w)|(?<!\w)(?=\w)
\B则表示非单词边界,所谓的非单词边界就是指一侧是\w且另一侧也是\w的这样一个位置,因此\B等价于(?<=\w)(?=\w)

最后还有一点要说明,位于字符集合中的\b(如[0-9\b])不是表示单词边界,而是代表退格键

Pattern 类

Matcher 类

一个 Matcher 对象是通过 Pattern.matcher(CharSequence input) 方法来获取的,调用此方法后并未开始正则匹配。

对于一个 Matcher 对象,可以使用以下三种方法来进行不同的匹配操作:

  • matches():尝试将模式与整个输入序列进行匹配,返回布尔值;
  • lookingAt():尝试从输入序列的开始处匹配模式,返回布尔值;
  • find():扫描输入序列,寻找与模式匹配的下一个子序列(推进式),返回布尔值。

Matcher 在称为 region 的输入子集中查找匹配项。默认情况下,该 region 包含整个输入序列。我们可以通过 region(beg, end) 方法修改这个区域,然后通过 regionStart()、regionEnd() 方法查询。

Matcher 在多线程环境中是非线程安全的。

MatchResult 接口
该接口表示 Matcher 匹配操作的结果。主要用于查询正则匹配的相关结果,不能通过 MatchResult 修改正则匹配结果。

Matcher 类

正则例子

默认模式、单行模式、多行模式

  • 默认模式:^$匹配输入序列的边界位置,.匹配除行结束符外的任意字符;
  • 单行模式:^$匹配输入序列的边界位置,.匹配任意字符;
  • 多行模式:^$匹配任意一行的边界位置,.匹配除行结束符外的任意字符。

简单的说:单行模式改变.的意义,多行模式改变^$的意义

我们不妨来测试一下:

Unicode 标志位启用与不启用的区别:

贪婪量词、懒惰量词、独占量词:

正向肯定预查:
文本:Windows2000 Windows2003 WindowsXP Windows7 Windows8 Windows10
目的:将 WindowsXP、Windows7、Windows8、Windows10 中的 Windows 替换为 WINDOWS,其它的不替换

分割输入序列:

首字母转换为大写(如果是小写的话):

进阶例子

如何使用正则表达式给数字添加千分位逗号分隔符

如:12345678 -> 12,345,678123456.1415926 -> 123,456.1415926

纯整数的情况
将一个数字序列千分位格式化的重点是找出要添加逗号的位置!那要怎么找到这个位置呢?

  1. 从该位置到数字序列末尾这段空间,必须与 3n (n>=0)个数字相匹配,使用顺序肯定环视即可
  2. 但是,仅仅有条件一还不够,该位置左侧还必须至少有一个数字存在,使用逆序肯定环视即可

因此最终的模式为:(?<=\d)(?=(?:\d{3})+\b),我们来测试一下:

很好,前几个都是我们想要的结果,但是对于最后一个,有小数的情况下,就出现问题了。

整数、小数混合的情况
数字序列12345678.12345678为什么会变成12,345,678.12,345,678而不是12,345,678.12345678呢?
使用的正则为(?<=\d)(?=(?:\d{3})+\b),首先我们可以肯定的是,(?=(?:\d{3})+\b)是没有问题的;
问题出现在(?<=\d)!我们还需再添加一个逆序环视条件,即添加逗号的位置前面不能存在.小数点符;
因此,我们最终的正则模式为:(?<=\d)(?<!\.\d{1,50})(?=(?:\d{3})+\b),该模式中有三个限定的条件:

  1. (?<=\d):添加逗号的位置前面必须有数字
  2. (?<!\.\d{1,50}):添加逗号的位置前面不能有小数点存在(为什么不使用\d+呢?稍后会解释)
  3. (?=(?:\d{3})+\b):从添加逗号的位置后面到匹配单词边界间的字符必须为 3N 个数字(N >= 1)

在第二个条件中,为什么不使用\d+,而是使用\d{1,50}(可将 50 改为 100、1000,依小数部分长度而定)这种别扭的写法呢?没办法呀,\d+会导致模式编译失败,因为+max值为正无穷,而又因为 Java 逆序环视的非定长量词的max值不允许为正无穷,所有才不得已使用后者!(.NET 可以)

我们来测试一下:

Math 算数类

java.lang.Math 包含了用于执行基本数学运算的属性和方法,如初等指数、对数、平方根和三角函数。Math 类的方法都被定义为 static 形式,不需要也不能创建 Math 类的对象。

常用方法:

Arrays 工具类

java.util.Arrays 类能方便地操作数组,它提供的所有方法都是静态的。

Arrays 类具有以下功能:
1) 给数组赋值:通过 fill 方法;
2) 对数组排序:通过 sort 方法,升序排序;
3) 比较数组:通过 equals 方法比较数组中元素值是否相等;
4) 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。

常用方法:

Comparable 和 Comparator 接口的区别
Comparable 接口在 java.lang 包,Comparator 接口在 java.util 包;
Comparable 是可比较的意思,Comparator 是比较器的意思;
Comparable 是对象的特性,Comparator 是用于比较两个对象的工具。

Comparable 和 Comparator 都是用来实现集合中元素的比较、排序的;
Comparable 使用集合内部定义的方法实现排序,Comparator 使用集合外部定义的方法实现排序。

所以,如想实现排序,有两种方式:
1) 在类的内部实现 java.lang.Comparable 接口,重写public int compareTo(T o)方法;
2) 在类的外部提供一个实现了 java.util.Comparator 接口的类,重写int compare(T o1, T o2)方法。

在类外部提供一个专门的比较器的方式更灵活一些,可以根据需求,自定义排序方式,可以随意的实现升序、降序排列;

例1,使用 Comparable + Collections.reverseOrder() 进行升序,降序排列:

例2,使用 Comparator 进行升序,降序排列:

String 字符串

java.lang.String 是 Java 中的字符串类,String 实现了 CharSequence 字符序列接口;除 String 外,StringBuffer 和 StringBuilder 也实现了 CharSequence 接口;

StringBuilder 和 StringBuffer 都是可变的字符序列,它们都继承于 AbstractStringBuilder,实现了 CharSequence 接口。但是,StringBuilder 是非线程安全的,而 StringBuffer 是线程安全的,因此,在单线程环境中建议使用 StringBuilder,在多线程环境中建议使用 StringBuffer。

CharSequence、String、StringBuilder、StringBuffer 的继承关系如下图所示:
CharSequence、String、StringBuilder、StringBuffer 继承关系图

String 类
String 内部使用一个char[]字符数组来存储我们的字符序列(字符串),不过该字符数组被 final 修饰了,即该数组对象在初始化后就不能在指向其它数组对象了,也即指针的指向不可变;但是数组的内容是可以改变的,因此,String 的不可变只是一个逻辑要求,是一个规定,我们依然可以通过反射来修改这个字符数组的元素,稍后会进行演示。

StringBuilder/StringBuffer 类
StringBuilder/StringBuffer 内部也是使用一个char[]字符数组来存储我们的字符序列(字符串),在实例化时可以指定该数组的初始长度,默认长度为 16。当数组空间不足时,StringBuilder/StringBuffer 会重新申请一块内存,并将原数组的内容拷贝至新数组,因此,如果可以估算出需要的存储空间,最好在构造时指明长度,避免频繁的内存拷贝。

利用反射破坏 String 类的封装性

上面的程序只是为了演示,String 对象并不是”物理上的不可变”而是”逻辑上的不可变”,没有任何实际意义,实际编码中也不要去利用反射来修改 String 对象,会带来不可预测的后果。

String 类的其它说明
String 是 final 类,因此不能定义 String 的派生类。除此之外,Java 语言还特别为 String 提供了 operator+() 运算符重载操作,即我们可以将一个 String 对象与其它对象(基本类型、引用类型)执行 + 运算,对于引用类型来说,它会调用对象的 toString() 方法,而基本类型就是它们的字面值。

除了使用 + 运算符进行字符串拼接之外,我们还可以使用 StringBuilder(或者 StringBuffer)的 append() 方法。那么这两种方法有区别吗?我们应该使用哪种呢?

来看一下 + 运算符的本质:

运行结果:

反编译结果:

从反编译结果中可以看出,字符串之间的 + 操作就是一颗语法糖,先 new 一个 StringBuilder 对象,然后依次将每个操作数传入它的 append() 方法,最后再调用 toString() 方法。

因此,我们可以得出结论,在 Java 中无论使用何种方式进行字符串连接,实际上都使用的是 StringBuilder(JDK1.5 前为 StringBuffer)。

那么是不是可以根据这个结论推出使用”+”和 StringBuilder 的效果是一样的呢?这个要从两个方面的解释。如果从运行结果来解释,那么”+”和 StringBuilder 是完全等效的。但如果从运行效率和资源消耗方面看,那它们将存在很大的区别。

从上面的循环添加 ASCII 大写字母中也可以看出,每循环一次就会产生一个新的 StringBuilder 对象,因此总共产生了 26 个 StringBuilder 对象,虽然 Java 有垃圾收集器,但是这样的循环多了,会给 GC 带来很大的负担,占有大量的内存资源。

因此,对于这种在循环内部进行字符串连接的操作,应该改用 StringBuilder/StringBuffer 来进行,避免短时间产生大量无用的 StringBuilder/StringBuffer 对象。即:

如果是在 JDK1.5 之前,那么 StringBuilder 应该被替换为 StringBuffer,因为 StringBuilder 是 JDK1.5 引入的。它们的区别前面已经说了,StringBuilder 非线程安全,StringBuffer 线程安全。

既然 StringBuilder 是非线程安全的,那为什么还会使用它来进行字符串拼接呢?不会有问题吗?不会的,因为 StringBuilder 对象的引用不会逸出,也就是说,在方法的外部,无法获取这个对象的引用,既然都无法获得引用了,也就不存在所谓的多线程问题了。

最后还有一点说明的是,在使用 + 运算符进行字符串拼接时,如果所有操作数都是字符串字面量(非字符串引用),则在编译期间,javac 会将它们先进行连接,而不会调用 StringBuilder.append() 方法!举个例子说明一下:

Unicode 和 UTF-16

字符集
Unicode字符集,除了 Unicode 字符集外,其它常见的字符集有:

  • ASCII 字符集:ASCII 即美国信息交换标准代码,主要用于处理现代英语,ASCII 字符集定义了 128 个字符,包括控制字符大小写字母阿拉伯数字标点符号
  • EASCII 字符集:为了处理表格符号计算符号希腊字母特殊的拉丁符号,又将 ASCII 码由 7 为扩充为 8 位,这样总共就可以表示 256 个字符。因此命名为扩展 ASCII 字符集;
  • ISO 8859-1 字符集:EASCII 的替代产物,正式编号为 ISO/IEC 8859-1:1998,又称 Latin-1 或“西欧语言”,是国际标准化组织内 ISO/IEC 8859 的第一个 8 位字符集。它以 ASCII 为基础,在空置的 0xA0-0xFF 的范围内,加入 96 个字母及符号,藉以供使用附加符号的拉丁字母语言使用;
  • GB2312 字符集:GB2312 或 GB2312–80 是中华人民共和国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,又称 GB0,由中国国家标准总局发布,1981 年 5 月 1 日实施。GB2312 编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持 GB2312;
  • GBK 字符集:汉字内码扩展规范,简称 GBK,全名为《汉字内码扩展规范(GBK)》1.0 版,由中华人民共和国全国信息技术标准化技术委员会 1995 年 12 月 1 日制订,国家技术监督局标准化司和电子工业部科技与质量监督司 1995 年 12 月 15 日联合以《技术标函[1995]229号》文件的形式公布。GBK 共收录 21886 个汉字和图形符号,其中汉字(包括部首和构件)21003 个,图形符号 883 个。

Unicode、ISO 8859-1 兼容
目前,几乎所有电脑系统都支持基本拉丁字母,并各自支持不同的其他编码方式。Unicode 为了和它们相互兼容,其头 256 个字符保留给 ISO 8859-1(兼容 ASCII)所定义的字符,使既有的西欧语系文字的转换不需特别考量;并且把大量相同的字符重复编到不同的字符码中去,使得旧有纷杂的编码方式得以和 Unicode 编码间互相直接转换,而不会丢失任何信息。举例来说,全角格式区块包含了主要的拉丁字母的全角格式,在中文、日文、以及韩文字形当中,这些字符以全角的方式来呈现,而不以常见的半角形式显示,这对竖排文字和等宽排列文字有重要作用。

如何表示一个 Unicode 字符
在表示一个 Unicode 的字符时,通常会用U+然后紧接着一组十六进制数字来表示这一个字符。在基本多文种平面(英文:Basic Multilingual Plane,简写 BMP。又称为“零号平面”、plane0)里的所有字符,要用4 个十六进制数字(例如U+4AE0,共支持 65536 个字符);在零号平面以外的字符则需要使用 5 个或 6 个十六进制数字(1 个十六进制数字的长度为 4 bit)。旧版的 Unicode 标准使用相近的标记方法,但却有些微小差异:在 Unicode 3.0 里使用U-然后紧接着 8 个十六进制数字,而U+则必须随后紧接着 4 个十六进制数字。

Unicode 字符平面映射
目前的 Unicode 字符分为 17 组编排,每组称为平面(Plane),而每平面拥有 65536(即 2^16)个码点(code point)。其中平面 0 就是我们前面说的基本多文种平面 BMP,目前只用了少数平面。具体的编排如下图所示:
Unicode 17 个平面

  • 基本多文种平面(Basic Multilingual Plane, BMP),或称第 0 平面或 0 号平面(Plane 0),是 Unicode 中的一个编码区段。编码从U+0000U+FFFF,拥有 65536 个码点(code point),可以表示日常使用中的绝大多数字符,如绝大部分日常使用汉字。但是从U+D800U+DFFF之间的码点区块是永久保留不映射到 Unicode 字符。UTF-16 就利用保留下来的 0xD800-0xDFFF 区段的码点来对辅助平面的字符的码位进行编码;
  • 第一辅助平面,又称多文种补充平面(Supplementary Multilingual Plane,缩写 SMP,或简称 Plane1),摆放拼音文字(主要为现时已不再使用的古老文字)、手写文字、音符、绘文字和其他图形符号。用于学者的专业论文中使用的古老或过时的语言书写符号,以及网络通信等使用的表情符号。范围在U+10000U+1FFFD
  • 第二辅助平面,又称表意文字补充平面(Supplementary Ideographic Plane,缩写 SIP,或简称 Plane2),整个范围在U+20000U+2FFFD。整个平面配置的都是一些罕用的汉字或地区的方言用字,如粤语用字及越南语的字喃。现时摆放了“中日韩统一表意文字扩展 B 区”(4 万 3253 个汉字)、“中日韩统一表意文字扩展 C 区”(4149 个汉字)、“中日韩统一表意文字扩展 D 区”(222 个汉字)、“中日韩统一表意文字扩展 E 区”(5762 个汉字)、“中日韩统一表意文字扩展 F 区”(7473 个汉字)以及中日韩兼容表意文字增补(CJK Compatibility Ideographs Supplement);
  • 第三辅助平面,尚未使用,但打算用来摆放甲骨文、金文、小篆、中国战国时期文字等。计划分配的编码区块为:U+30000-U+317FF甲骨文、U+32000-U+32FFF金文、U+34000-U+368FF小篆;
  • 第四至第十三辅助平面,并未计划使用;
  • 第十四辅助平面,又称特别用途补充平面(Supplementary Special-purpose Plane,简称 SSP),摆放“语言编码标签”和“字形变换选取器”,它们都是控制字符。范围在U+E0000U+E01FF
  • 第十五至十六辅助平面,都是私人使用区。它们的范围是U+F0000U+FFFFDU+100000U+10FFFD

字符编码
对于 ASCII、ISO 8859-1 字符集来说,因为只需 1 个字节的长度就可以存储,因此它不需要额外的字符编码方案,因此 ASCII 既代表 ASCII 字符集也代表 ASCII 字符编码(ISO 8859-1 字符集类似)。

但是对于 Unicode 字符集来说,因为其目标是统一码、万国码,因此收录的字符非常多,并且 Unicode 至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为 2017 年 6 月 20 日公布的 10.0.0[2],已经收录超过十万个字符(第十万个字符在 2005 年获采纳)。Unicode 涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。

但是,我们平时很少会使用到除了辅助平面 1 以外的其它辅助平面的字符,因此,为了不浪费储存资源,并且可能的兼容现有的 ASCII 编码方案,在 Unicode 的发展中就涌现了好几种 Unicode 编码方案,如 UTF-7、UTF-8、UTF-16(分为 UTF-16LE、UTF-16BE)、UCS-2、UTF-32、UCS-4,目前比较常用的是:UTF-8UTF-16LEUTF-16BE Unicode 编码方案。

码点、码元

  • code point码点:又称码位,是字符集中用于表示一个字符的唯一数字。例如,ASCII 码包含 128 个码位,范围是0x0-0x7F,扩展 ASCII 码包含 256 个码位,范围是0x0-0xFF,而 Unicode 包含 1,114,112 个码位,范围是0x0-0x10FFFF。Unicode 码空间划分为 17 个 Unicode 字符平面(基本多文种平面,16 个辅助平面),每个平面有 65,536(= 2^16)个码位。因此 Unicode 码空间总计是 17 × 65,536 = 1,114,112。
  • code unit码元:也称代码单元,是指一个已编码的文本中具有最短的比特组合的单元。对于 UTF-8 来说,码元是 8 比特长;对于 UTF-16 来说(Java),码元是 16 比特长;对于 UTF-32 来说,码元是 32 比特长。码值(Code Value)是过时的用法。

简单的说,code point码点是字符集中的概念,一个码点表示一个完整独立的字符;code unit码元是字符编码中的概念,一个码元可能代表一个完整独立的字符,也可能不是。比如 UTF-16 的代理对,对于辅助平面字符来说,都是使用 2 个码元(一个 char 就是一个码元,两个码元就是 4 个字节)来表示的。一个code point可能由多个code unit组成

UTF-8 编码的code unit大小为 1 字节,UTF-16 编码的code unit大小为 2 字节,UTF-32 编码的code unit大小为 4 字节。

UTF-8 字符编码
UTF-8(8-bit Unicode Transformation Format)是一种针对 Unicode 的可变长度字符编码,也是一种前缀码。它可以用来表示 Unicode 标准中的任何字符,且其编码中的第一个字节仍与 ASCII 兼容,这使得原来处理 ASCII 字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码。

UTF-8 使用 1 至 6 个字节为每个字符编码(尽管如此,2003 年 11 月 UTF-8 被 RFC 3629 重新规范,只能使用原来 Unicode 定义的区域,U+0000U+10FFFF,也就是说最多四个字节):

  1. 128 个 US-ASCII 字符只需一个字节编码(Unicode 范围由U+0000U+007F);
  2. 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode 范围由U+0080U+07FF);
  3. 其它基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode 范围由U+0800U+FFFF);
  4. 其它极少使用的 Unicode 辅助平面的字符使用四至六字节编码(Unicode 范围由U+10000U+1FFFFF使用四字节,Unicode 范围由U+200000U+3FFFFFF使用五字节,Unicode 范围由U+4000000U+7FFFFFFF使用六字节)。

对上述提及的第 4 种字符而言,UTF-8 使用四至六个字节来编码似乎太耗费资源了。但UTF-8 对所有常用的字符都可以用 3 个字节表示,而且它的另一种选择,UTF-16 编码,对前述的第 4 种字符同样需要 4 个字节来编码,所以要决定 UTF-8 或 UTF-16 哪种编码比较有效率,还要视所使用的字符的分布范围而定。不过,如果使用一些传统的压缩系统,比如 DEFLATE,则这些不同编码系统间的的差异就变得微不足道了。若顾及传统压缩算法在压缩较短文字上的效果不大,可以考虑使用 Unicode 标准压缩格式(SCSU)。

互联网工程工作小组(IETF)要求所有互联网协议都必须支持 UTF-8 编码。互联网邮件联盟(IMC)建议所有电子邮件软件都支持 UTF-8 编码。

UTF-8 编码的优点

  • ASCII 是 UTF-8 的一个子集。因为一个纯 ASCII 字符串也是一个合法的 UTF-8 字符串,所以现存的 ASCII 文本不需要转换。为传统的扩展 ASCII 字符集设计的软件通常可以不经修改或很少修改就能与 UTF-8 一起使用。
  • 字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到,同时,UTF-8 以字节为编码单元,它的字节顺序在所有系统中都是一样的,没有字节序的问题,也因此它实际上并不需要 BOM。
  • UTF-8 和 UTF-16 都是可扩展标记语言文档的标准编码。所有其它编码都必须通过显式或文本声明来指定。
  • 任何面向字节的字符串搜索算法都可以用于 UTF-8 的数据(只要输入仅由完整的 UTF-8 字符组成)。但是,对于包含字符记数的正则表达式或其它结构必须小心。
  • UTF-8 字符串可以由一个简单的算法可靠地识别出来。即一个字符串在任何其它编码中表现为合法的 UTF-8 的可能性很低,并随字符串长度增长而减小。举例说,字符值 C0,C1,F5 至 FF 从来没有出现。为了更好的可靠性,可以使用正则表达式来统计非法过长和替代值。
  • 与 UCS-2 的比较:ASCII 转换成 UCS-2,在编码前插入一个 0x0。用这些编码,会包含一些控制符,这在 UNIX 和一些 C 函数中,将会产生严重错误。因此可以肯定,UCS-2 不适合作为 Unicode 的外部编码,也因此诞生了 UTF-8。

UTF-16 字符编码
UTF-16 是 Unicode 字符编码五层次模型的第三层:字符编码表(Character Encoding Form,也称为”storage format”)的一种实现方式。即把 Unicode 字符集的抽象码位映射为 16 位长的整数(即码元)的序列,用于数据存储或传递。Unicode 字符的码位,需要 1 个或者 2 个 16 位长的码元来表示,因此这是一个变长表示。

Unicode 的编码空间从U+0000U+10FFFF,共有 1,112,064 个码位(code point)可用来映射字符。Unicode 的编码空间可以划分为 17 个平面(plane),每个平面包含 2^16(65,536)个码位。17 个平面的码位可表示为从U+xx0000U+xxFFFF,其中xx表示十六进制值从0x000x10,共计 17 个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从U+D800U+DFFF之间的码位区块是永久保留不映射到 Unicode 字符。UTF-16 就利用保留下来的 0xD800-0xDFFF 区段的码位来对辅助平面的字符的码位进行编码

基本平面的编码方式
第一个 Unicode 平面(码位从U+0000U+FFFF)包含了最常用的字符。该平面被称为基本多语言平面,缩写为 BMP(Basic Multilingual Plane, BMP)。UTF-16 与 UCS-2 编码这个范围内的码位为 16 比特长的单个码元,数值等价于对应的码位(code point)。BMP 中的这些码位是仅有的可以在 UCS-2 中表示的码位。

辅助平面的编码方式
辅助平面(Supplementary Planes)中的码位,在 UTF-16 中被编码为一对 16 比特长的码元(即 32 位,4 字节),称作代理对(surrogate pair),具体方法是:

  • 码位减去 0x10000,得到的值的范围为 20 比特长的 0x0 - 0xFFFFF;
  • 高位的 10 比特的值(值的范围为 0..0x3FF)被加上 0xD800 得到第一个码元或称作高位代理(high surrogate),值的范围是 0xD800..0xDBFF。由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode 标准现在称高位代理为前导代理(lead surrogates)
  • 低位的 10 比特的值(值的范围为 0..0x3FF)被加上 0xDC00 得到第二个码元或称作低位代理(low surrogate),现在值的范围是 0xDC00..0xDFFF。由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode 标准现在称低位代理为后尾代理(trail surrogates)

UTF-16 编码 - 示例
UTF-16 编码 - 示例

UTF-16 的 BOM 字节序标记
UTF-16的大端序(UTF-16BE)和小端序(UTF-16LE)存储形式都在用。一般来说,Mac 使用 UTF-16BE,Windows 和 Linux 使用 UTF-16LE。(Java 使用 UTF-16BE 作为字符的内码
为了不会混淆 UTF-16 文件的大小端字节序,在 UTF-16 文件的开首,都会放置一个U+FEFF字符作为Byte Order Mark(UTF-16LE 以FF FE代表,UTF-16BE 以FE FF代表),以表示这个文本文件是以 UTF-16 编码,其中U+FEFF字符在 UNICODE 中代表的意义是ZERO WIDTH NO-BREAK SPACE,顾名思义,它是个没有宽度也没有断字的空白。

UTF-16 与 UCS-2 的关系
UTF-16 可看成是 UCS-2 的父集。在没有辅助平面字符(surrogate code points)前,UTF-16 与 UCS-2 所指的是同一的意思。但当引入辅助平面字符后,就称为 UTF-16 了。现在若有软件声称自己支持 UCS-2 编码,那其实是暗指它不能支持在 UTF-16 中超过 2 字节的字集。

UTF-8、UTF-16、UCS-2、UTF-32、UCS-4 对比

编码方案 UTF-8 UTF-16 UCS-2 UTF-32 UCS-4
编码空间 U+0000 - U+10FFFF U+0000 - U+10FFFF U+0000 - U+FFFF U+0000 - U+10FFFF U+0000 - U+7FFFFFFF
最少字节数 1 2 2 4 4
最多字节数 4 4 2 4 4
依赖字节序

字节顺序标记(英语:byte-order mark,BOM),用来标识 Unicode 文本文件中的字节序是大端序还是小端序;
BOM 只适用于UTF-16/UCS-2UTF-32/UCS-4,UTF-8 不需要所谓的 BOM 标识,UTF-8 没有所谓的字节序问题。

Java 中的 Unicode 支持

CharSequence 接口

JDK1.4 开始提供的字符序列接口,提供了对不同字符序列实现类的只读访问,如 String、StringBuilder、StringBuffer。

String 类

StringBuilder 类

StringBuffer 类

基本类型包装类

基本类型(值类型):byteshortintlongfloatdoublebooleancharvoid
包装类(引用类型):ByteShortIntegerLongFloatDoubleBooleanCharacterVoid

值类型 -> 引用类型,称为装箱
引用类型 -> 值类型,称为拆箱

在 jdk1.5 之前,装箱、拆箱需要程序员手动来完成,称为手动装箱手动拆箱
在 jdk1.5 之后,装箱、拆箱可以由编译器自动完成,称为自动装箱自动拆箱

除了 Boolean、Character、Void 类型直接继承 Object 类,Byte、Short、Integer、Long、Float、Double 都是继承自 Number 类。

Void 仅仅起到一个占位的作用,它的默认构造函数被声明为了 private,因此无法创建 Void 类的实例。

包装类对象一经创建,所封装的基本类型值不会再改变;因为被 final 给修饰了。不过依旧可以通过反射进行修改,因为 final 只是限制语法上的不可修改,这点和 C/C++ 使用指针修改 const 常量的值是一样的。

常量池
常量池有两种:
1) class文件常量池:或称为”静态常量池”,用于存放编译器生成的各种字面量符号引用,在类加载之后会放到方法区的运行时常量池中;
2) 运行时常量池:或称为”动态常量池”,与静态常量池不同的是,它具有动态性,即可以在运行期间动态的将新的常量放入池中。

常量池的运用可以有效地减少相同常量的多次存储,减少不必要的存储空间浪费。

而动态常量池在开发中运用的最多的就是 String 的 intern() 成员方法;
并且基本类型包装类也存在运行时常量池,它们的作用和 String 是相似的。

八大基本类型的包装类中,除了浮点型的包装类(FloatDouble)外,其他所有的包装类都存在常量池机制。

当一个 String 实例调用 intern() 方法时,首先会去查找 String 运行时常量池中是否有相同的字符串常量;
如果有,则返回常量池中该字符串的引用;如果没有,将当前对象的加入到常量池中,并返回其在常量池中的引用。

String 的两种创建方式
1) String s = "www.zfl9.com";
第一步,将字面量”www.zfl9.com”存放在 Class 文件的常量池中;
第二步,执行String s,新建一个 String 引用变量 s(String 类型的指针);
第三步,将字面量”www.zfl9.com”的地址赋给引用变量 s。

在这种方式中,只创建了一个对象,即”www.zfl9.com”常量;

2) String s = new String("www.zfl9.com");
第一步,将字面量”www.zfl9.com”存放在 Class 文件的常量池中;
第二步,执行new String(),在堆中创建一个 String 对象,并使用常量”www.zfl9.com”进行初始化(拷贝构造);
第三步,执行String s,新建一个 String 引用变量 s(String 类型的指针);
第四步,将刚刚在堆中创建的匿名对象的指针赋给引用变量 s。

在这种方式中,创建了两个对象,一个在常量池中,一个在堆中;

因此,不建议使用第二种形式,会造成内存空间的浪费!

String.intern() 的例子:

好吧,有些扯远了,我们回到包装类中来,包装类有一些共同方法,以 Integer 为例:
手动装箱
public Integer(int value)
public Integer(String s) throws NumberFormatException:解析字符串中的 int(RT 异常)
自动装箱
public static Integer valueOf(int i):基本类型的值在区间[-128, 127]的对象将入池。
public static Integer valueOf(String s) throws NumberFormatException:同上。
public static Integer valueOf(String s, int radix) throws NumberFormatException:同上。
手动/自动拆箱
public int intValue():返回所包装的基本类型的值;
静态方法 解析字符串
public static int parseInt(String s) throws NumberFormatException:解析字符串中的数字;
public static int parseInt(String s, int radix) throws NumberFormatException:同上。

手动装箱因为每次都是使用new创建,所以每次创建的对象都是不同的,它们生死于堆上;
而自动装箱则有点不同,它不使用new创建,而是使用其静态方法valueOf()valueOf()内部维护了一个常量池;

  • 初始时,该 cache 池为空,没有任何包装类对象;
  • 当调用valueOf(10)方法自动装箱时,发现 cache 池中没有值等于 10 的对象,于是新建一个对象并丢入 cache 池中;
  • 当再次调用valueOf(10)方法自动装箱时,发现 cache 池中已有值相同的对象,于是不再创建新对象,而是将已有对象返回;
  • 但是 cache 池并不是无限大的,是有一定范围的,在 Integer 中,它被限制为只缓存区间[-128, 127]的对象,即一个字节表示的整数;
  • 如果传入 valueOf() 的参数不在该范围中,那么等同于手动装箱,即每次都会 new 一个新的对象出来。

除了 Integer 有所谓的 cache 池,Boolean、Byte、Short、Long、Character 也有 cache,如下:

  • Short、Long 和 Integer 一样,区间都是 [-128, 127];
  • Boolean、Byte 因为它们占用的内存长度都在 1 字节之内,因此全部取值范围都被缓存;
  • 而 Character 相当于无符号的 Short 整型,因此在区间 [0, 127] 的对象也将被缓存;
  • Float、Double 不会被缓存,不管它们的取值范围是多少,因为浮点数无法精确的枚举出来。

还有一点要注意:
1) 当==运算符的两个操作数都是引用类型(包装类)时,比较引用的值,不触发自动拆箱;
2) 如果其中有一个操作数是算数表达式/数值则触发自动拆箱,这时比较的是基本类型的值

例子一:

如果你理解了前面的内容,那么这个例子就很容易理解了:
1) a1 == a2:a1、a2 自动装箱,值在区间 [-128, 127],因此它们都引用同一个对象,而==两边的操作数都是引用类型,比较他们的引用的值,因为是同一个对象,所以返回 true;
2) a1 == a2 + a3:a1、a2、a3 都是自动装箱,a1 和 a2 都引用自池中的同一对象,==的右操作数是一个表达式,触发 a2、a3 的自动拆箱,变为40 + 0,即右操作数为 40,因为有一个操作数是值类型,所以触发 a1 的自动拆箱,最终比较的是40 == 40,返回 true;
3) b1 == b2:因为 b1、b2 都是手动装箱,所以他们引用的是不同的对象,因此返回 false;
4) b1 == b2 + b3:右操作数是一个表达式,触发自动拆箱,结果为 40,而 b1 也被触发自动拆箱,结果为 40,因此返回 true。

再来一个例子:

1) a3 == a4:自动装箱,比较的是引用,因为在区间 [-128, 127],true;
2) a5 == a6:自动装箱,比较的是引用,因为不在区间 [-128, 127],false;
3) a3 == a1 + a2:触发自动拆箱,比较的是数值,true;
4) a3.equals(a1 + a2):计算a1 + a2时触发自动拆箱,然后再次自动装箱,因此返回 true;
5) b1 == a1 + a2:计算a1 + a2时触发自动拆箱,结果为 int 类型的值 3,b1 也因此自动拆箱,是 long 类型的值 3;然后 int -> long 自动类型转换,因此返回 true;
6) b1.equals(a1 + a2):计算a1 + a2时触发自动拆箱,结果为 int 类型的值 3,然后再次装箱为 Integer 引用类型,因为比较的两个对象的类型不同,所以返回 false;
7) b1.equals(a1 + b2):计算a1 + b2时触发自动拆箱,并且发生自动类型转换 int -> long,然后装箱为 Long 引用类型,因此返回 true。

Integer 相关方法(其它数值包装类与之类似)

Character 相关方法