public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
可见该方法使用一个 String 类型的实例对象初始化/创建另一个 String 对象,它们内部都指向同一个字符序列,这是通常我们创建一个 String 类型对象实例的方式之一:
String str = new String("this is string");
可见,下面的直接赋值的方法更简洁/高效一点:
String str = "this is string";
import java.lang.reflect.Field;
public class Test3 {
public static void main(String[] args) throws NoSuchFieldException,
SecurityException, IllegalArgumentException, IllegalAccessException {
String str1 = "真楠人";
String str2 = "真楠人";
// 打印字符串 str1 和 str2
System.out.println("str1: " + str1 + ", str2: " + str2);
// 通过反射更改字符串 str1 的值
Field valueField = String.class.getDeclaredField("value");
assert valueField != null;
valueField.setAccessible(true);
char[] value = (char[]) valueField.get(str1);
value[0] = '假';
// 再次打印字符串 str1
System.out.println("str1: " + str1 + ", str2: " + str2);
}
}
代码执行结果如下:
str1: 真楠人, str2: 真楠人
str1: 假楠人, str2: 假楠人
上述程序中,我们通过反射方法仅仅修改了字符串 str1 的内容,但是最后字符串 str2 的内容也一起改变了,原因在于 str1 和 str2 都是引用同一个字符串对象(字符串字面量 “真楠人” 为一个 String 类型的对象实例,虽然这个字面量在代码中出现多次,但是都是引用了同一个 String 实例,可见相同字符序列的字符串共享同一个 String 实例,即 共享字符串,这也解释了为什么 String 类没有提供修改字符串的公共方法以及String 类被设计为 final 类,都是为了共享字符串),这并不难理解。
public class Temp_3 {
public static void main(String[] args) {
System.out.println('楠');
System.out.println((char)26976);
System.out.println('\u6960');
}
}
运行结果为:
楠
楠
楠
可见,三次打印输出的结果都一样。Java 语言使用 Unicode 字符集,Unicode 为每一个字符都分配的一个唯一的数字,即 这个数字便代表与之对应的字符,其中 ‘\u6960’ 即为字符 ‘楠’ 的 Unicode 十六进制编码,而十进制数 26976 则为这个十六进制数的十进制数值,它俩是同一个数值的不同表示方式而已。
#include
void main(){
char c = '楠'; // 一个字节
printf("%c, %d, size=%ld\n", c, c, sizeof c);
char c2 = 'A'; // 一个字节
printf("%c, size=%ld\n", c2, sizeof c2);
char c3[] = "楠"; // 字符串
printf("%s, size=%ld\n", c3, sizeof c3);
char c4[] = "真楠人"; // 字符串
printf("%s, size=%ld\n", c4, sizeof c4);
}
执行结果如下:
ubuntu@cuname:~/dev/beginning-linux-programming/temp$ gcc -o use-char-string-test use-char-string-test.c
use-char-string-test.c: In function ‘main’:
use-char-string-test.c:4:14: warning: multi-character character constant [-Wmultichar]
char c = '楠';
^
use-char-string-test.c:4:14: warning: overflow in implicit constant conversion [-Woverflow]
ubuntu@cuname:~/dev/beginning-linux-programming/temp$ ./use-char-string-test
�, -96, size=1 // c
A, size=1 // c2
楠, size=4 // c3
真楠人, size=10 // c4
可以看到,将字符 ‘楠’ 赋值给 char 类型的变量时,gcc 编译器警告提示字符 ’楠‘ 为多字节字符常量,而变量 c 仅仅存储了字符 ’楠‘ 的一个字节的数据(且其数值为 -96,-96 是字符 ‘楠’ 字编码后的字节序列中的一个字节,变量 c 显示乱码),继续查看后续的输出结果,发现字符 ’A‘ 的长度为 1 个字节,而每个汉字占 3 个字节的存储空间(C 语言中的字符串字面量的内部实现为 char 类型的数组,且以一个空字符 ’\0‘表示字符串结尾,所以 sizeof 计算的字符串长度(即 字节数)比实际长度多一个字节)。3 个字节能表示的最大数值为 ’2的24次方‘ - 1 = 16777215,远远大于 26976(即字符 ’楠‘ 对应的数字),其实 2 个字节就足以存储数值 26976(即字符 ’楠‘),而实际上字符 ’楠‘ 却占用了 3 个字节空间,这是为啥?这与字符(即 数值)的编码(即 存储)方式有关,我们知道 Unicode 给每个字符分配一个唯一的数值来代表该字符,例如任一一篇文章很可能会有多个字符,但是在存储或传输该文章时,并不能就直接依次存储或传输与每个字符对应的十进制数字序列,这里需要考虑 2 个问题,第一如何从数字序列中识别一个字符,即每个字符的 ‘数字表示’ 其自身应当是一个整体,必须与其它的数字即与其相邻的数字区分开,第二个问题:成本,字符 ‘A’ 使用 1 个字节即可存储,但是字符 ‘楠’ 却要使用至少 2 个字节才能满足,这时,如果要求每个字符都是使用例如 2 个字节存储,那么对于英语国家的用户来说,相当于增加并浪费了 1 倍的成本,这是不能被接受!UTF-8 便是一种可变长度的 Unicode 字符编码解决方案,且被应用于 Java 语言,于是求助于 UTF-8(即 Unicode Transformation Format 8-bit,)。
/** 简单 UTF-8 解码器(实际应用中,可能必须要注意/处理无效字符 即无效 unicode code-point 的情况) */
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Random;
public class UTF_8_decoder {
/** 1M,用于缓存数据 */
private static final int BUFFER_SIZE = 1024 * 1024;
/** 文本文件 */
private static final String filePath = "C:/Users/jokee/Desktop/data-for-test.txt";
public static void main(String[] args) throws IOException {
File file = new File(filePath);
FileInputStream fileReader = new FileInputStream(file);
/** 用于缓存文件字节数据 即 缓存待解码的数据 */
byte[] buffer = new byte[BUFFER_SIZE];
/** 缓存所有数据 */
final int bytesLength = fileReader.read(buffer, 0, buffer.length);
String result = doDecoding(buffer, bytesLength);
// 打印结果
System.out.println("解码字符如下:\n" + "---------------------------------------------\n"
+ result);
fileReader.close();
}
/**
* 执行解码操作,默认大端字节序
*
bytes : 待解码字节缓冲区
*
bytesLength :缓冲区 bytes 中,待解码的字节总数
*/
public static String doDecoding(byte[] bytes, int bytesLength) {
/** 保存解码后的数据 */
char[] charData = null;
int charData_index = 0;
String result = null;
if (bytes != null
&& (bytesLength = Math.min(bytesLength, bytes.length)) > 0) {
// 1 从一个随机位置开始解码
// int startIndex = new Random().nextInt(bytesLength);
// 2 解码所有字节数据
int startIndex = 0;
/** 字符类型(参考 getType 方法) */
int type = 0;
/** 保存一个字符的 UTF-8 编码 */
byte[] encodedCharData = new byte[4];
int encodedCharData_index = 0;
// 初始化
charData = new char[BUFFER_SIZE];
for (; startIndex < bytesLength; ++startIndex) {
if (encodedCharData_index == 0 && (type = getType(bytes[startIndex])) < 0) {
// 直到找到一个 UTF-8 编码的起始字节为止
continue;
}
// 读取一个字符的编码
encodedCharData[encodedCharData_index++] = bytes[startIndex];
if (encodedCharData_index < type) {
// 继续读取该字符剩余的其它字节
continue;
}
// 解码一个字符
if (type == 1) {
// 1
// ascii 字符
charData[charData_index++] = (char) Byte.toUnsignedInt(encodedCharData[0]);
} else if (type == 2) {
// 2
// 提取第一个字节,屏蔽前 3 个 bit
int b1 = 0b00011111 & Byte.toUnsignedInt(encodedCharData[0]);
// 提取第二个字节,屏蔽前 2 个 bit
int b2 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[1]);
int aChar = (b1 << 6) | b2;
charData[charData_index++] = (char)aChar;
} else if (type == 3) {
// 3
// 提取第一个字节,屏蔽前 4 个 bit
int b1 = 0b00001111 & Byte.toUnsignedInt(encodedCharData[0]);
// 提取第二个字节,屏蔽前 2 个 bit
int b2 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[1]);
// 提取第三个字节,屏蔽前 2 个 bit
int b3 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[2]);
int aChar = (b1 << 12) | (b2 << 6) | b3;
charData[charData_index++] = (char)aChar;
} else if (type == 4) {
// 4
// 提取第一个字节,屏蔽前 3 个 bit
int b1 = 0b00000111 & Byte.toUnsignedInt(encodedCharData[0]);
// 提取第二个字节,屏蔽前 2 个 bit
int b2 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[1]);
// 提取第三个字节,屏蔽前 2 个 bit
int b3 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[2]);
// 提取第四个字节,屏蔽前 2 个 bit
int b4 = 0b00111111 & Byte.toUnsignedInt(encodedCharData[3]);
int aChar = (b1 << 18) | (b2 << 12) | (b3 << 6) | b4;
charData[charData_index++] = (char)aChar;
}
// 清理工作
// 清除已缓存且已完成解码的字符编码,继续处理下一个字符编码
encodedCharData_index = 0;
}
result = new String(charData, 0, charData_index);
}
return result;
}
/**
* 该方法用于确认参数字节 aByte 是否为一个 UTF-8 编码的起始字节, 返回值说明,
* -1:字节 aByte 非起始字节,
* 1:起始字节,类型为 1,对应字符占 1 个字节空间(实为 ASCII 字符)
* 2:起始字节,类型为 2,对应字符占 2 个字节空间
* 3:起始字节,类型为 3,对应字符占 3 个字节空间
* 4:起始字节,类型为 4,对应字符占 4 个字节空间
*/
public static int getType(byte aByte) {
int type = 0b11110000 & aByte; // ‘0b11110000’ 为十进制数 240 的二进制表示
if (0 <= type && type < 128) {
return 1;
} else if (128 <= type && type < 192) {
return -1;
} else if (192 <= type && type < 224) {
return 2;
} else if (224 <= type && type < 240) {
return 3;
} else if (240 <= type && type < 248) {
return 4;
}
return -1;
}
}
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/** 简单的 UTF-8 编码器(实际应用中,可能必须要注意/处理无效字符 即无效 unicode code-point 的情况) */
public class UTF_8_encoder {
/** 输出文件 */
private static final String filePath = "C:/Users/jokee/Desktop/data-for-test.txt";
public static void main(String[] args) throws IOException {
String text = "\u13A0,\u1B93,\u1D83,\u1F00,\u1F10,\u1910\nwo我爱wo家ya,wo爱北京天安门a,\nhei,今天又下雨了ne";
int[] cs = new int[text.length()];
for(int i = 0;i < text.length();i++) {
cs[i] = text.charAt(i);
}
// do encoding
Bytes bytes = doEncoding(cs, cs.length);
// write to file
FileOutputStream writer = new FileOutputStream(new File(filePath));
writer.write(bytes.bytes, 0, bytes.length);
writer.close();
System.out.println("done.");
}
/** 将字符数组 chars 中的数量为 charsLength 的字符进行编码 */
public static Bytes doEncoding(int[] chars, int charsLength) {
Bytes bytes = null;
if (chars != null && (charsLength = Math.min(chars.length, charsLength)) > 0) {
int aChar = 0;
int type = 0;
/** 定义一个足够容量的字节缓存区:假设每个字符都是占用 4 个字节 */
bytes = new Bytes(4 * charsLength);
for(int i = 0;i < charsLength;i++) {
aChar = chars[i];
type = getType(aChar);
switch(type) {
case 1:
bytes.add((byte)aChar);
break;
case 2:
{
int code = 0b1100000010000000;
code |= (aChar & 0b111111);
code |= ((aChar & 0b11111000000) << 2);
bytes.add((byte)(code >> 8));
bytes.add((byte)code);
}
break;
case 3:
{
int code = 0b111000001000000010000000;
code |= (aChar & 0b111111);
code |= ((aChar & 0b111111000000) << 2);
code |= ((aChar & 0b1111000000000000) << 4);
bytes.add((byte) (code >> 16));
bytes.add((byte) (code >> 8));
bytes.add((byte) code);
}
break;
case 4:
{
int code = 0b11100000100000001000000010000000;
code |= (aChar & 0b111111);
code |= ((aChar & 0b111111000000) << 2);
code |= ((aChar & 0b111111000000000000) << 4);
code |= ((aChar & 0b111000000000000000000) << 6);
bytes.add((byte) (code >> 24));
bytes.add((byte) (code >> 16));
bytes.add((byte) (code >> 8));
bytes.add((byte) code);
}
break;
default:
// nothing to do.
}
}
}else {
// 返回一个空的字节缓存区
bytes = new Bytes(0);
}
return bytes;
}
/**
* 返回值表示一个字符 aChar 的编码空间(即 需占用的字节数),返回值如下 4 种:
*
1,表示使用 1 个字节编码字符 aChar 对应的数值
*
2,表示使用 2 个字节编码字符 aChar 对应的数值
*
3,表示使用 3 个字节编码字符 aChar 对应的数值
*
4,表示使用 4 个字节编码字符 aChar 对应的数值
* 如果 aChar 是无效的 UTF-8 字符,可选择抛出异常:IllegalArgumentException,或忽略
*/
public static int getType(int aChar) {
if (!Character.isValidCodePoint(aChar)) {
// throw new IllegalArgumentException(aChar + " is invalid code point");
return 0;
}
if(0 <= aChar && aChar <= 127) {
// a char of ascii
return 1;
}else if(128 <= aChar && aChar <= 2047) {
return 2;
}else if(2048 <= aChar && aChar <= 65535) {
return 3;
}else if(65536 <= aChar && aChar <= 2097151) {
return 4;
}else {
// throw new IllegalArgumentException(aChar + " is invalid code point");
return 0;
}
}
/** 组合一个字节缓存区 bytes 及其 长度值 length */
public static class Bytes {
private int length;
private final int capacity;
private byte[] bytes;
public Bytes(int capacity) {
this.length = 0;
this.capacity = capacity;
this.bytes = new byte[capacity];
}
/** 向该缓存区中添加一个字节 */
public void add(byte aByte) {
if (length < capacity) {
bytes[length++] = aByte;
}else {
throw new ArrayIndexOutOfBoundsException("当前缓存区已满,capacity = length is " + capacity);
}
}
public int getLength() {
return length;
}
public byte[] getBytes() {
return bytes;
}
public int getCapacity() {
return capacity;
}
}
}