我们上次在《细说Java中的字符和字符串(一)》说了Java中char和中文字符之间的关系,说明了char能不能存储一个中文字符,以及如何判断Java的字符串是否包含中文字符。
这次再看一下MySQL数据库中VARCHAR(N)中的N表示什么,什么会限制N的大小。然后,从Java的String源代码分析一下中文字符在Java字符串里是如何存储的。
VARCHAR(N)中的N代表什么?
和第一篇类似,我们依旧从官方文档寻找最有权威的答案。我们从这里可以找到VARCHAR(N)中N代表什么的答案:https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html
由于原文比较长,这里先把关键片段贴一下:
答案就在这段文字里,我们翻译一下原文:VARCHAR 或者VARBINARY类型的列最大可存储的字节数不能超过一行的最大可存储字节数,也就是65535字节。如果VARCHAR类型的列存储的是多字节类型的字符,那么可存储的最大字符数相对就会变少。比如:utf8mb4类型的字符一个字符最大可以占到4个字节,所以一个VARCHAR类型的字段最多可以存储16383个字符(65535/4)。
重点看比如的文字,可以看出来MySQL的VARCHAR(N)中的N代表的是多少个字符(Character),而Character的对于不同的编码又代表什么呢?
一起看一下另一篇MySQL官网文档的内容:https://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html
VARCHAR(N)的N值不论是哪种编码,都对应到了字符级别,也就是这个列可以存下N个字符。MySQL的utf8和Java里的utf8不一样,MySQL的实现里utf8最大只允许存储3个字节。从上一篇我们知道,utf8编码最长有4个字节,那么4个字节的utf8字符MySQL的utf8类型是存不下的。
所以,当需要存储emoji表情符的时候,我们需要用utf8mb4类型的字符集才能存下。MySQL的utf8mb4字符集才能包含全部utf8字符,而MySQL的utf8字符集是一个不完全版本的utf8字符集。
总结一下:MySQL中VARCHAR(N)字段类型里边的N代表最大可以存多少个字。如果选的是中文编码,比如utf8编码,那么N代表最大可以存多少个汉字。
VARCHAR(N)中的N可以是多大?
N的最大值主要限制因素是MySQL的最大行大小,原文地址在:https://dev.mysql.com/doc/refman/8.0/en/column-count-limit.html。
MySQL的行大小最大不能超过65535bytes,是说MySQL的一行里所有列占用字节数的和不能超过65535bytes。BLOB和TEXT比较特别,他们只占用9个字节大小,真实数据存储在行外。
MySQL的uft8类型最多占3个字节,MySQL允许的最大字符数是21844(65535/3-1),utf8mb4最多占4个字节,所以允许的最大字符数是16382(65535/4-1)。这个数字有一点点偏差,因为VARCHAR类型需要1到2个字节存储length,所以最大字符数可以按照减1计算。
从Java String分析Java的char和中文的关系
String有三类构造函数,一类是传入byte[],一类是传入char[],一类是传入int[],如下:
public String(byte bytes[], int offset, int length);//字节流,转成字符串 public String(char value[], int offset, int count);//字符流,转成字符串。较少用,一般用在双字节编码的字节串转String。 public String(int[] codePoints, int offset, int count);//代码点,转成字符串
String内部数据存储使用的是char[]存储,byte 是8字节,char是16字节,int是32字节。所以byte[]参数的构造函数会把byte[]解析成正确的代码点,也就是int类型,再转换成char[]类型存到String内部。
所以,我们这里只分析int[]类型入参的构造函数,来看一下String是如何把int类型的代码点转成char[]存起来的。从这里,也能看出来一个2个字节的Unicode和一个2个以上字节的Unicode代码点是怎么存到String里的。
public String(int[] codePoints, int offset, int count) { //.... // Pass 1: 计算int[]转成char[]后的长度 int n = count; for (int i = offset; i < end; i++) { int c = codePoints[i]; if (Character.isBmpCodePoint(c))//如果是BMP范围的代码点,那么计数加1 continue; else if (Character.isValidCodePoint(c))//如果是超出BMP范围的有效代码点,那么计数加2 n++; else throw new IllegalArgumentException(Integer.toString(c)); } // Pass 2: 使用int[]的值填充char[] final char[] v = new char[n]; for (int i = offset, j = 0; i < end; i++, j++) { int c = codePoints[i]; if (Character.isBmpCodePoint(c))//如果是BMP范围的代码点,直接加入char[] v[j] = (char)c; else//如果是超出BMP范围的有效代码点,那么转成2个char,存入char[] Character.toSurrogates(c, v, j++); } this.value = v; }