byte数组转string 您所在的位置:网站首页 byte数组转字符串 byte数组转string

byte数组转string

#byte数组转string| 来源: 网络整理| 查看: 265

如果问你,开发过程中用的最多的类是哪个?你可能回答是HashMap,一个原因就是HashMap的使用量的确很多,还有就是HashMap的内容在面试中经常被问起。

但是在开发过程中使用最多的类其实并不是HashMap类,而是“默默无闻”的String类。假如现在问你String类是怎么实现的?这个类为什么是不可变类?这个类为什么不能被继承?这些问题你都能回答么。本文就从String源代码出发,来看下String到底是怎么实现的,并详细介绍下String类的API的用法。

String源码结构

首先要说明的是本文的源码是以JDK11为基准,选择JDK11的原因是JDK11是一个LTS版本(长期支持版本),没选择现阶段还在广泛使用的JDK8的原因是想在看源码的过程中学习下JDK的新特性。

还有要说下的就是:大家在看源码时一定要注意JDK的版本,因为不同版本的实现有较大的差异。比如说String的实现在高低版本中就差异比较大。如果你是一个博客主,更加要注明代码的版本了,不然读者可能会很疑惑,为什么和自己之前看的不一样。

好了,下面就言归正传来看下String在JDK11中的实现代码。

public final class String implements Serializable, Comparable, CharSequence { @Stable //字节数组,存放String的内容,如果你看的是较低版本的源代码,这个变量可能是char[]类型,这个其实是JDK9开始对String做的一个优化 //具体是做了什么优化我们下面再讲,这边先卖个关子 private final byte[] value; //也是和String压缩优化有关,指定当前的LATIN1码还是UTF16码 private final byte coder; //哈希值 private int hash; //序列化Id private static final long serialVersionUID = -6849794470754667710L; //优化压缩开关,默认开启 static final boolean COMPACT_STRINGS = true; private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; public static final Comparator CASE_INSENSITIVE_ORDER = new String.CaseInsensitiveComparator(); static final byte LATIN1 = 0; static final byte UTF16 = 1; //... 下面部分代码省略 }

从实现的接口看,String类有如下特点:

String类被final关键字修饰,因此不能被继承。String的成员变量value使用final修饰,因此是不可变的,线程安全;String类实现了Serializable接口,可以实现序列化。String类实现了Comparable,可以比较大小。String类实现了CharSequence接口,String本质是个数组,低版本中是char数组,JDK9以后优化成byte数组,从String的成员变量value就可以看出来。

这边说一个看源代码的小技巧:看一个类的源代码时,我们先看下这个类实现了哪些接口,就可以大概知道这个类的主要作用功能是什么了。

JDK9对String的优化

这边首先要讲下JDK 9中对String的优化,如果你不了解这块优化点的话,看String的代码时会感到非常疑惑。

背景知识

在Java中,一个字节char占用两个字节的内存空间。在低版本的JDK中,String的内部默认维护的是一个char[]数组,也就是说一个字符串中包含一个字符,这个字符串内部就包含一个相应长度的字符数组。这样就会出现下面这种情况:

String s = "ddd"; String s1 = "自由之路";

上面两个字符串内部的情况实际上是:

char[] value = ['d','d','d']; char[] value1 = ['自','由','之','路'];

对于字符串s,我们发现其中每个字符其实都是可以用一个字节表示的,而现在使用两个字符的char类型来表示,明显就浪费了一倍的内存空间。

而且根据统计,在实际程序运行中,字符串中包含的字符大多都是可以用一个字节表示的字符,所以优化的空间很大。优化的方式就是在String内部使用byte[]数组来表示字符串,而不是使用char[]数组。当检测到,字符串中的所有字符在Unicode码集中的码值可以使用一个字节表示时,就可以节省一半的空间。

JDK6 中的Compressed Strings

其实在JDK6中就对String类做过类似的优化:在Java 6引入了Compressed Strings,对于one byte per character的字符串使用byte[],对于two bytes per character的字符串继续使用char[]。

使用-XX:+UseCompressedStrings来开启上面的优化。不过由于开启这个特性后会造成一些不可知的异常,这个特性在java7中被废弃了,然后在java8被移除。

JDK9中的Compact String

Java 9 重新采纳字符串压缩这一概念。

和JDK6不同的是:无论何时我们创建一个所有字符都能用一个字节的 LATIN-1 编码来描述的字符串,都将在内部使用字节数组的形式存储,且每个字符都只占用一个字节。另一方面,如果字符串中任一字符需要多于 8 比特位来表示时,该字符串的所有字符都统统使用两个字节的 UTF-16 编码来描述。因此基本上能如果可能,都将使用单字节来表示一个字符。

//占用3个字节 String ss = new String("ddd"); //占用14个字节 String s = "自由之路ddd";

现在的问题是:所有的字符串操作如何执行? 怎样才能区分字符串是由 LATIN-1 还是 UTF-16 来编码?为了处理这些问题,字符串的内部实现进行了一些调整。引入了一个 final 修饰的成员变量 coder, 由它来保存当前字符串的编码信息。

//所有的字符串都用byte数组存储 private final byte[] value; //用coder标示字符串中所有的字符是不是都可以用一个字节表示,它的值只有两个LATIN1:1,标示所有字符都可以用一个字节表示,UTF16:标示字符串中部分字符需要两个字节表示。 private final byte coder; //下面是两个常量 static final byte LATIN1 = 0; static final byte UTF16 = 1;

现在,大多数的字符串操作都将检查 coder 变量,从而采取特定的实现:

public int indexOf(int ch, int fromIndex) { return isLatin1() ? StringLatin1.indexOf(value, ch, fromIndex) : StringUTF16.indexOf(value, ch, fromIndex); } private boolean isLatin1() { return COMPACT_STRINGS && coder == LATIN1; }

我们再看下String的一个常用方法:

public int length() { return value.length >> coder; }

这个方法是要计算字符串的长度,含义也很清楚。根据coder字段判断当前的字符串中一个字符使用几个字节表示,如果是coder等于0,也是LATIN1模式,那么所有字符都是用一个字节表示,直接返回byte[]数组的长度就可以。

如果coder等于1,那么标示字符串中所有字符都是用两个字节表示的,计算字符串的长度需要将byte[]数组除以2。value.length >> coder就是这个意思。

因为对String做了上面的优化,所以String的很多方法在操作时都需要判断现在的模式是LATIN1还是UTF16模式,具体的方法这边就不一一举例了。但是这些判断对使用String的开发者时无感的。

当然,String的这个优化特性可以关闭,使用下面的启动参数就可以。

+XX:-CompactStrings String的常用构造方法 //构建空字符串 public String() { this.value = "".value; this.coder = "".coder; } //根据已有的字符串,创建一个新的字符串 @HotSpotIntrinsicCandidate public String(String original) { this.value = original.value; this.coder = original.coder; this.hash = original.hash; } //根据字符数组,创建字符串,创建的过程中有压缩优化的逻辑,具体见下面的方法 public String(char[] value) { this((char[])value, 0, value.length, (Void)null); } String(char[] value, int off, int len, Void sig) { if (len == 0) { this.value = "".value; this.coder = "".coder; } else { if (COMPACT_STRINGS) { //如果发现这个字符数组可以压缩,就使用LATIN1方式 byte[] val = StringUTF16.compress(value, off, len); if (val != null) { this.value = val; this.coder = 0; return; } } //不能进行压缩优化,还是使用UTF16的方式 this.coder = 1; this.value = StringUTF16.toBytes(value, off, len); } }

String中还有很多构造方法,但是都会大同小异,大家可以自己看源代码。

String常用方法总结

这边总结下String的常用方法,一些比较简单的方法就不具体讲了。我们挑选一些比较重要的方法,具体讲下他们的使用方法。

codePointAt(int index):返回下标是index的字符在Unicode码集中的码点值;codePoints():返回字符串中每个字符在Unicode码集中的码点值;compareToIgnoreCase(String other):忽略大小写比较字符大小;concat(String other):字符串拼接函数;equalsIgnoreCase(String other):忽略大小写比较字符串;format:字符串格式化函数,比较有用;getBytes(String charSet):获取字符串在特定编码下的字节数组;indexOf(String s):返回字符串s的下标,不存在返回-1;intren():作用是检测常量池中是否有当前字符串,有的话就返回常量池中的对像,没有的话就将当前对像放入常量池。isBlank():如果字符串为空或只包含空白字符,则返回true,否则返回false,JDK11新加的API;length():返回字符长度;lines():从字符串返回按行分割的Stream,行分割福包括:n ,r 和rn,stream包含了按顺序分割的行,行分隔符被移除了,这个方法会类似split(),但性能更好;这个也是JDK11新加的APImatchs(String regex):和某个正则是否匹配;regionMatches(int firstStart, String other, int otherStart, int len):当某个字符串调用该方法时,表示从当前字符串的firstStart位置开始,取一个长度为len的子串;然后从另一个字符串other的otherStart位置开始也取一个长度为len的子串,然后比较这两个子串是否相同,如果这两个子串相同则返回true,否则返回false。repeat():返回一个字符串,其内容是字符串重复n次后的结果,JDK11新加入的函数;String[] split(String regex, int limit):分割字符串,注意limit参数的使用,下面会详细讲;startsWith(String prefix, int toffset):判断字符串是否以prefix打头;replace(char oldChar, char newChar):使用newChar替换所有的oldChar,不是基于正则表达式的;replace(CharSequence target, CharSequence replacement):替换所有,基于正则表达式的;replaceFirst(String regex, String replacement):替换regex匹配的第一个字符串,基于正则表达式;replaceAll(String regex, String replacement):替换regex匹配的所有字符串,基于正则表达式;strip() :去除字符串前后的“全角和半角”空白字符,这个函数在JDK中11才引入,注意和trim的区别,关于全角和半角的区别,可以参考这篇文章,还提供了stripLeading()和stripTrailing(),可以分别去掉头部或尾部的空格;subString(int fromIndex):从指定位置开始截取到字符串结尾部分的子串;subString(int fromIndex,int endIndex):截取字符串指定下标的子串;toCharArray():转换成字符数组;toUpperCase(Locale locale) :小写转换成大写;toLowerCase(Locale locale):大写转换成小写;trim():去除字符串前后的空白字符(空格、tab键、换行符等,具体的话是去除ascll码小于32的字符),注意trim和strip的区别;valueof系列方法:将其他类型的数据转换成String类型,比如将bool、int和long等类型转换成String类型。 concat字符串拼接函数

concat函数是字符串拼接函数,介绍这个函数并不是因为这个函数比较重要或者实现比较复杂。而是因为通过这个函数的源代码我们可以看出很多String的特性。

public String concat(String str) { //如果被拼接的字符串的长度是0,直接返回自己 int olen = str.length(); if (olen == 0) { return this; } else { byte[] buf; //如果当前字符串和被拼接的字符串的编码模式相同,都是LATIN1或者都是UTF16 if (this.coder() == str.coder()) { byte[] val = this.value; buf = str.value; //计算出新字符串所需字节的长度 int len = val.length + buf.length; byte[] buf = Arrays.copyOf(val, len); //使用系统函数拷贝 System.arraycopy(buf, 0, buf, val.length, buf.length); //根据新的字节数组生成一个新的字符串 return new String(buf, this.coder); } else { //当前字符串和被拼接的字符串的编码模式不同,那么必须使用UTF16的编码模式 int len = this.length(); buf = StringUTF16.newBytesFor(len + olen); this.getBytes(buf, 0, (byte)1); str.getBytes(buf, len, (byte)1); return new String(buf, (byte)1); } } } format函数

String的format方法是一个很有用的方法,可以用来对字符串、数字、日期和时间等进行格式化。

//对整数格式化,4位显示,不足4位补0 //超过4位,还是原样显示 int num = 999; String str = String.format("%04d", num); System.out.println(str); //对日期进行格式化 String format = String.format("%tF", new Date()); System.out.println(format);

format方法还有很多用法,大家可以自己查询使用。

regionMatches

该方法的定义如下:

regionMatches(int firstStart, String other, int otherStart, int len)

当某个字符串调用该方法时,表示从当前字符串的firstStart位置开始,取一个长度为len的子串;然后从另一个字符串other的otherStart位置开始也取一个长度为len的子串,然后比较这两个子串是否相同,如果这两个子串相同则返回true,否则返回false。

该方法还有另一种重载:

str.regionMatches(boolean ignoreCase, int firstStart, String other, int otherStart, int len)

可以看到只是多了一个boolean类型的参数,用来确定比较时是否忽略大小写,当ignoreCase为true表示忽略大小写。

split函数

String的split函数我们平时也经常使用,但是估计很多人都没有注意这个函数的第二个参数:limit

public String[] split(String regex, int limit)

首先,split方法的作用是根据给定的regex去分割字符串,将分割完成的字符数组返回。其中limit参数的作用是:

当limit>0时,limit代表最后的数组长度,同时一共会分割limit-1次,最后没有切割完成的直接放在一起; 当limit=0时(默认值),会尽量多去分割,并且如果分割完的字符数组末尾是空字符串,会去除这个空字符串; 当limit


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有