String类概要

  • 所有的字符串字面量都属于String类,String对象创建后不可改变,因此可以缓存共享,StringBuilder,StringBuffer是可变的实现
  • String类提供了操作字符序列中单个字符的方法,比如有比较字符串,搜索字符串等
  • Java语言提供了对字符串连接运算符的特别支持(+),该符号也可用于将其他类型转换成字符串。
  • 字符串的连接实际上是通过StringBuffer或者StringBuilder的append()方法来实现的
  • 一般情况下,传递一个空参数在这类构造函数或方法会导致NullPointerException异常被抛出。
  • String表示一个字符串通过UTF-16(unicode)格式,补充字符通过代理对表示。索引值参考字符编码单元,所以补充字符在String中占两个位置。

String是不可变的

  • String是常量,一旦被创建就不可被改变,因此可以用来共享

从String的怪异现象讲起

String是否相等

==判断的是对象的内存起始地址是否相同,equals判断自定义的语义是否相同

  • JVM为了提高内存效率,将所有不可变的字符串缓存在常量池中,当有新的不可变的字符串需要创建时,如果常量池中存在相等的字符串就直接将引用指向已有的字符串常量,而不会创建新对象
  • new创建的对象存储在堆内存,不可能与常量区的对象具有相同地址
  • 直接用字面量初始化String要比用new 关键字创建String对象效率更高
public class Demo {public static void main(String[] args) throws Exception {String s = "abc";String s1 = "abc";String s2 = "a" + "bc";final String str1 = "a";final String str2 = "bc";String s3 = str1 + str2;String s4 = new String("abc");System.out.println(s == s1);System.out.println(s == s2);System.out.println(s == s3);System.out.println(s == s4);}
} //结果:true    true    true    false

为什么String不可变

final修饰变量,如果是基本类型那么内容运行期间不可变,如果是引用类型那么引用的对象(包括数组)运行期地址不可变,但是对象(数组)的内容是可以改变的

  • final只是保证value不会指向其他的数组,但不保证数组内容不可修改
  • private属性保证了不可以在类外访问数组,也就不能改变其内容
  • String内部没有改变value内容的函数,所以String就不可变了
  • String声明为final杜绝了通过继承的方法添加新的函数
  • 基于数组的构造方法,会拷贝数组元素,从而避免了通过外部引用修改value的情况
  • 用String构造其他可变对象时,涉及的数组只是返回的数组的拷贝而不是原数组,例如 new StringBuilder(str),会把str数组进行拷贝后传递给StringBuilder而不是传递原数组

当然只要类库设计人愿意,只要增加一个类似的setCharAt(index)的接口,String就变成可变的了

    private final char value[];private int hash; // Default to 0  public String(char value[]) {this.value = Arrays.copyOf(value, value.length);}  

通过反射改变String

  • final 只在编译器有效,在运行期间无效,因此可以通过反射改变value引用的对象
  • s与str始终具有相同的内存地址,反射改变了s的内容,并没有新创建对象
  • s 与 s1对应常量池中的两个对象,所以即便通过反射修改了s的内容,他们两个的内存地址还是不同的
public class Demo {public static void main(String[] args) throws Exception {String s = "abc";String str = s;String s1 = "bbb";System.out.println(str == s);Field f = s.getClass().getDeclaredField("value");f.setAccessible(true);f.set(s, new char[]{'b', 'b', 'b'});System.out.println(str + "    " + s);System.out.println(s == str);System.out.println(s == s1);}
}  //结果:bbb    bbb    true    false

String的HashCode

s的内容改变了但是hashCode值并没有改变,虽然s与s1的内容是相同的但是他们hashCode值并不相同

  • Object的hashCode方法返回的是16进制内存地址,String类重写了hashCode的,hashCode值的计算是基于字符串内容的
  • String的hashCode值初始为0,由于String是不可变的,当第一次运行完hashCode方法后String类对HashCode值进行了缓存,下一次在调用时直接返回hash值
public class Demo {public static void main(String[] args) throws Exception {String s = "abc";String s1 = "bbb";System.out.println(s.hashCode());Field f = s.getClass().getDeclaredField("value");f.setAccessible(true);f.set(s, new char[]{'b', 'b', 'b'});System.out.println(s + "    "+ s1);System.out.println(s.hashCode() +" " +s1.hashCode());}
}  //结果:96354    bbb    bbb    96354 97314

String hashCode的源码

    public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;}  

toString方法中的this

  • Java为String类重载了“+”操作符,String类与其他类对象进行连接时会调用其他类的toString方法
  • 如果在其他类的toString方法中用“+”对this进行连接就会出现无限递归调用而出现栈溢出错误
  • 解决方法将this换做super.this
public class Demo {@Overridepublic String toString() {//会造成递归调用
//        return "address"+super.toString();return "address"+super.toString();}public static void main(String[] args) {System.out.println(new Demo());}
}  

CodePoints与CodeUnit

String的length表示的是代码单元的个数,而不是字符的个数

  • codePoints是代码点, 表示的是例如’A’, ‘王’ 这种字符,每种字符都有一个唯一的数字编号,这个数字编号就叫unicode code point。目前code point的数值范围是0~0x10FFFF。
  • codeUnit是代码单元, 它根据编码不同而不同, 可以理解为是字符编码的基本单元,java中的char是两个字节, 也就是16位的。这样也反映了一个char只能表示从u+0000~u+FFFF范围的unicode字符, 在这个范围的字符也叫BMP(basic Multiligual Plane ), 超出这个范围的叫增补字符,增补字符占用两个代码单元。
public class Demo {public static void main(String[] args) {String s = "\u1D56B";System.out.println(s);System.out.println(s.length());}
}  

我们看看String是怎么处理增补字符的

  • 首先value字符数组的长度是根据代码单元来定的,每出现一个Surrogate字符数组长度在count的基础上加一
  • BMP字符直接存储,增补字符的用两个char分别存储高位和低位
    public String(int[] codePoints, int offset, int count) {if (offset < 0) {throw new StringIndexOutOfBoundsException(offset);}if (count < 0) {throw new StringIndexOutOfBoundsException(count);}// Note: offset or count might be near -1>>>1.if (offset > codePoints.length - count) {throw new StringIndexOutOfBoundsException(offset + count);}final int end = offset + count;// Pass 1: Compute precise size of char[]int n = count;for (int i = offset; i < end; i++) {int c = codePoints[i];if (Character.isBmpCodePoint(c))continue;else if (Character.isValidCodePoint(c))n++;else throw new IllegalArgumentException(Integer.toString(c));}// Pass 2: Allocate and fill in 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))v[j] = (char)c;elseCharacter.toSurrogates(c, v, j++);}this.value = v;}  static void toSurrogates(int codePoint, char[] dst, int index) {// We write elements "backwards" to guarantee all-or-nothingdst[index+1] = lowSurrogate(codePoint);dst[index] = highSurrogate(codePoint);}  

源码解析

声明

  • String类可序列化,可比较,实现CharSequence接口提供了对字符的基本操作
  • String内部使用final字符数组进行存储,涉及value数组的操作都使用了拷贝数组元素的方法,保证了不能在外部修改字符数组
  • String重写了Object的hashCode函数使hash值基于字符数组内容,但是由于String缓存了hash值,所以即便通过反射改变了字符数组内容,hashhashCode返回值不会自动更新
  • serialVersionUID 用来确定类的版本是否正确,如果不是同一个类会抛出InvalidCastException异常
public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence { private final char value[];private static final long serialVersionUID = -6849794470754667710L;  /** Cache the hash code for the string */private int hash; // Default to 0public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;}  

构造函数

  • String主要提供了通过String,StringBuilder,StringBuffer,char数组,int数组(CodePoint),byte数组(需要指定编码)进行初始化
  • 当通过字符串初始化字符串时,并没有执行value数组拷贝,因为original的value数组是不可以在外部修改的,也就保证了新String对象的不可修改
  • 通过字符数组,StringBuffer,StringBuilder进行初始化时,就要执行value数组元素的拷贝,创建新数组,防止外部对value内容的改变
  • 通过byte数组进行初始化,需要指定编码,或使用默认编码(ISO-8859-1),否则无法正确解释字节内容
  • 通过Unicode代码点进行的初始化,可能会包含非BMP字符(int值大于65535),这时候字符串的长度可能会长于int数组的长度,(见本文前面增补字符处理部分)
    public String(String original) {this.value = original.value;this.hash = original.hash;}  public String(StringBuffer buffer) {synchronized(buffer) {this.value = Arrays.copyOf(buffer.getValue(), buffer.length());}}public String(StringBuilder builder) {this.value = Arrays.copyOf(builder.getValue(), builder.length());}  public String(char value[]) {this.value = Arrays.copyOf(value, value.length);}  public String(char value[], int offset, int count) {if (offset < 0) {throw new StringIndexOutOfBoundsException(offset);}if (count < 0) {throw new StringIndexOutOfBoundsException(count);}// Note: offset or count might be near -1>>>1.if (offset > value.length - count) {throw new StringIndexOutOfBoundsException(offset + count);}this.value = Arrays.copyOfRange(value, offset, offset+count);} public String(byte bytes[], int offset, int length, Charset charset) {if (charset == null)throw new NullPointerException("charset");checkBounds(bytes, offset, length);this.value =  StringCoding.decode(charset, bytes, offset, length);} public String(byte bytes[], int offset, int length) {checkBounds(bytes, offset, length);this.value = StringCoding.decode(bytes, offset, length);}  static char[] decode(byte[] ba, int off, int len) {String csn = Charset.defaultCharset().name();try {// use charset name decode() variant which provides caching.return decode(csn, ba, off, len);} catch (UnsupportedEncodingException x) {warnUnsupportedCharset(csn);}try {return decode("ISO-8859-1", ba, off, len);} catch (UnsupportedEncodingException x) {// If this code is hit during VM initialization, MessageUtils is// the only way we will be able to get any kind of error message.MessageUtils.err("ISO-8859-1 charset not available: "+ x.toString());// If we can not find ISO-8859-1 (a required encoding) then things// are seriously wrong with the installation.System.exit(1);return null;}} 

内部构造函数

使用外部数组来初始化String内部数组只有保证传入的数组不可能被改变才能保证String的不可变性,例如用String初始化String对象时

  • 这种方法使用共享value数组的方法避免了数组的拷贝,提高了效率
  • 上面分析指出如果直接使用外部传入的数组不能保证String的不可变性,这个方法只在String的内部使用,不能由外部调用
  • 添加share参数,只是为了重载构造函数,share必须为true
  • 该函数只用在不能缩短String长度的函数中,如concat(str1,str2),如果用在缩短String长度的函数如subString中会造成内存泄漏
    String(char[] value, boolean share) {// assert share : "unshared not supported";this.value = value;}  public String concat(String str) {int otherLen = str.length();if (otherLen == 0) {return this;}int len = value.length;char buf[] = Arrays.copyOf(value, len + otherLen);str.getChars(buf, len);return new String(buf, true);}  // 使用了Arrays.copyof方法来构造新的数组,拷贝元素,而不是共用数组public String substring(int beginIndex) {if (beginIndex < 0) {throw new StringIndexOutOfBoundsException(beginIndex);}int subLen = value.length - beginIndex;if (subLen < 0) {throw new StringIndexOutOfBoundsException(subLen);}return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);}  

如果String(value,share)可以在外部使用,就可以改变字符串内容

public class Demo {public static void main(String[] args) {char[] arr = new char[] {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};String s = new String(arr,true);arr[0] = 'a';System.out.println(s);}
} 

aLongString 已经不用了,但是由于其与aPart共享value数组,所以不能被回收,造成内存泄漏

     public String subTest(){String aLongString = "...a very long string..."; String aPart = aLongString.substring(20, 40);return aPart;}

主要方法

其他主要方法

length() 返回字符串长度isEmpty() 返回字符串是否为空charAt(int index) 返回字符串中第(index+1)个字符char[] toCharArray() 转化成字符数组trim() 去掉两端空格toUpperCase() 转化为大写toLowerCase() 转化为小写String concat(String str) //拼接字符串String replace(char oldChar, char newChar) //将字符串中的oldChar字符换成newChar字符//以上两个方法都使用了String(char[] value, boolean share);boolean matches(String regex) //判断字符串是否匹配给定的regex正则表达式boolean contains(CharSequence s) //判断字符串是否包含字符序列sString[] split(String regex, int limit) 按照字符regex将字符串分成limit份。String[] split(String regex)

重载的valueOf方法

可以看到主要是调用构造函数或者是调用对应类型的toString完成到字符串的转换

    public static String valueOf(boolean b) {return b ? "true" : "false";}public static String valueOf(char c) {char data[] = {c};return new String(data, true);}public static String valueOf(int i) {return Integer.toString(i);}public static String valueOf(long l) {return Long.toString(l);}public static String valueOf(float f) {return Float.toString(f);}public static String valueOf(double d) {return Double.toString(d);}  public static String valueOf(char data[], int offset, int count) {return new String(data, offset, count);} public static String copyValueOf(char data[], int offset, int count) {// All public String constructors now copy the data.return new String(data, offset, count);} 

字符串查找算法 indexOf

可以看到String的字符串匹配算法使用的是朴素的匹配算法,即前向匹配,当遇到不匹配字符时,主串从下一个字符开始,字串从开始位置开始
其他相关字符串匹配算法

    static int indexOf(char[] source, int sourceOffset, int sourceCount,char[] target, int targetOffset, int targetCount,int fromIndex) {if (fromIndex >= sourceCount) {return (targetCount == 0 ? sourceCount : -1);}if (fromIndex < 0) {fromIndex = 0;}if (targetCount == 0) {return fromIndex;}char first = target[targetOffset];int max = sourceOffset + (sourceCount - targetCount);for (int i = sourceOffset + fromIndex; i <= max; i++) {/* Look for first character. */if (source[i] != first) {while (++i <= max && source[i] != first);}/* Found first character, now look at the rest of v2 */if (i <= max) {int j = i + 1;int end = j + targetCount - 1;for (int k = targetOffset + 1; j < end && source[j]== target[k]; j++, k++);if (j == end) {/* Found whole string. */return i - sourceOffset;}}}return -1;}  

编码问题 getBytes

  • 字符串最终都是使用机器码以字节存储的,当我们将字符串转换为字节的时候也需要给定编码,同一个字符不同的编码就对应不同的字节
  • 如不指定编码,就会使用默认的编码ISO-8859-1进行编码
  • 编码时为了避免平台编码的干扰,应当指定确定的编码
    String s = "你好,世界!";byte[] bytes = s.getBytes("utf-8");  public byte[] getBytes(String charsetName)throws UnsupportedEncodingException {if (charsetName == null) throw new NullPointerException();return StringCoding.encode(charsetName, value, 0, value.length);} static byte[] encode(String charsetName, char[] ca, int off, int len)throws UnsupportedEncodingException{StringEncoder se = deref(encoder);String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;if ((se == null) || !(csn.equals(se.requestedCharsetName())|| csn.equals(se.charsetName()))) {se = null;try {Charset cs = lookupCharset(csn);if (cs != null)se = new StringEncoder(cs, csn);} catch (IllegalCharsetNameException x) {}if (se == null)throw new UnsupportedEncodingException (csn);set(encoder, se);}return se.encode(ca, off, len);} 

比较方法

  • 所有比较方法都是比较对应的字符数组的内容,后两个比较方法用来进行区段比较
  • 在进行数组比较时,如果可以通过长度进行初步判断,一般可以提高效率
    boolean equals(Object anObject);boolean contentEquals(StringBuffer sb);boolean contentEquals(CharSequence cs);boolean equalsIgnoreCase(String anotherString);int compareTo(String anotherString);int compareToIgnoreCase(String str);boolean regionMatches(int toffset, String other, int ooffset,int len)  //局部匹配boolean regionMatches(boolean ignoreCase, int toffset,String other, int ooffset, int len)   //局部匹配  public boolean equals(Object anObject) {if (this == anObject) {return true;}if (anObject instanceof String) {String anotherString = (String) anObject;int n = value.length;if (n == anotherString.value.length) {char v1[] = value;char v2[] = anotherString.value;int i = 0;while (n-- != 0) {if (v1[i] != v2[i])return false;i++;}return true;}}return false;}  

替换函数 replace

  • 单字符替换会替换所有特定字符的出现
  • replace为普通(literal)替换,不用正则表达式
  • replaceFirst与replaceAll都使用了正则表达式
    public String replace(CharSequence target, CharSequence replacement) {return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(this).replaceAll(Matcher.quoteReplacement(replacement.toString()));} public String replaceFirst(String regex, String replacement) {return Pattern.compile(regex).matcher(this).replaceFirst(replacement);}public String replaceAll(String regex, String replacement) {return Pattern.compile(regex).matcher(this).replaceAll(replacement);} public String replace(char oldChar, char newChar) {if (oldChar != newChar) {int len = value.length;int i = -1;char[] val = value; /* avoid getfield opcode */while (++i < len) {if (val[i] == oldChar) {break;}}if (i < len) {char buf[] = new char[len];for (int j = 0; j < i; j++) {buf[j] = val[j];}while (i < len) {char c = val[i];buf[i] = (c == oldChar) ? newChar : c;i++;}return new String(buf, true);}}return this;}

常量池相关方法

  • 每当定义一个字符串字面量,字面量进行字符串连接,或者final的String字面量初始化的变量的连接的变量时都会检查常量池中是否有对应的字符串,如果有就不创建新的字符串,而是返回指向常量池对应字符串的引用
  • 所有通过new String(str)方式创建的对象都会存在与堆区,而非常量区
  • 普通变量的连接,由于不能在编译期确定下来,所以不会存储在常量区
public native String intern();  

运算符的重载

  • String对“+”运算符进行了重载,通过反编译我们看到重载是通过StringBuilder的append方法,及String的valueOf方法实现的
  • int值转String过程中(”“+i)这种方法实际为(new StringBuilder()).append(i).toString();,而另外两种都是调用Integer的静态方法Integer.toString完成
// int转String的方法比较
public class Demo {public static void main(String[] args) throws Exception {int i = 5;String i1 = "" + i;String i2 = String.valueOf(i);String i3 = Integer.toString(i);}
}
// 原始代码
public class Demo {public static void main(String[] args) throws Exception {String string="hollis";String string2 = string + "chuang";}
}
//反编译代码
public class Demo {public static void main(String[] args) throws Exception {String string = "hollis";String string2 = (new StringBuilder(String.valueOf(string))).append("chuang").toString();}
}

Java String源码解析相关推荐

  1. Java - String源码解析及常见面试问题

    文章目录 Pre Q1: String 是如何实现的? Q2: String 有哪些重要的方法? 构造函数 equals() compareTo() [equals() vs compareTo() ...

  2. 程序兵法:Java String 源码的排序算法(一)

    摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 这是泥瓦匠的第103篇原创 <程序兵法:Java Str ...

  3. Java Executor源码解析(7)—Executors线程池工厂以及四大内置线程池

    详细介绍了Executors线程池工具类的使用,以及四大内置线程池. 系列文章: Java Executor源码解析(1)-Executor执行框架的概述 Java Executor源码解析(2)-T ...

  4. Java Executor源码解析(3)—ThreadPoolExecutor线程池execute核心方法源码【一万字】

    基于JDK1.8详细介绍了ThreadPoolExecutor线程池的execute方法源码! 上一篇文章中,我们介绍了:Java Executor源码解析(2)-ThreadPoolExecutor ...

  5. Java的String为什么不可变?(String源码解析)

    String的源码解析 public final class String{private final char value[];//容器,存放字符串的private int hash;//哈希值pr ...

  6. Java SPI 源码解析及 demo 讲解

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:Java实现QQ登录和微博登录个人原创+1博客:点击前往,查看更多 作者:JlDang 来源:https://s ...

  7. Java HashSet源码解析

    本解析源码来自JDK1.7,HashSet是基于HashMap实现的,方法实现大都直接调用HashMap的方法 另一篇HashMap的源码解析文章 概要 实现了Set接口,实际是靠HashMap实现的 ...

  8. Java Thread 源码解析

    Thread 源码解析 线程的方法大部分都是使用Native使用,不允许应用层修改,是CPU调度的最基本单元.线程的资源开销相对于进程的开销是相对较少的,所以我们一般创建线程执行,而不是进程执行. T ...

  9. Java 正则表达式源码解析

    使用方法 Pattern p = Pattern.compile("a*");Matcher m = p.matcher("aaaa");if (m.find( ...

最新文章

  1. Go 学习笔记(5)— 算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、取地址和指针运算符
  2. python 并行计算 并行方法总结 concurrent.futures pp pathos multiprocessing multiprocess模块 总结对比
  3. windoes windoes server 上安装mysql(MSI安装包安装、压缩包安装)
  4. nagios报Connection refused by host的解决办法
  5. 动态内存(Dynamic Memory),微软的内存过量分配技术?
  6. 遗传算法求二元函数极值怎么编码_用遗传算法求复杂函数的极值点
  7. HSF服务注册失败,项目启动后,EDAS列表无法发现注册的服务
  8. 刚入行的UI设计师,通过临摹优秀UI KIT作品开始
  9. PostgreSQL通知示例
  10. 2020 第十一届蓝桥杯大赛软件赛省赛(第一场),C/C++大学B组题解
  11. java数组使用实验报告_Java实验报告二数组.doc
  12. IO流实现csv文件到vcf文件生成
  13. 日历2017 年终总结新年工作汇报PPT模板免费下载_PPTX图片设计素材_包图网888pic.com...
  14. 外贸软件出口管理系统亮点及重点
  15. 【信息学奥赛】2070:【例2.13】数字对调C++)
  16. [Leetcode] 382. Linked List Random Node 解题报告
  17. 电子设计教程30:温度滞回控制系统
  18. 不靠广告联盟也能月赚万元
  19. Excel重命名工作表:一键修改为指定的表名
  20. 手把手教你使用热敏电阻NTC,产品级精度±0.1℃以内,简单明了,内附源码详解,方便移植

热门文章

  1. Java 蜡烛图_分支-15. 日K蜡烛图
  2. oracle java耗cpu_ORACLE高手请看过来,CPU使用率100% (100分)
  3. Node.js 用户注册功能的实现
  4. Webpack執行打包:“You may need an appropriate loader to handle this file type“
  5. Linux下hba卡驱动的卸载,SLES11下如何重装qlogic FC HBA卡驱动
  6. python自动化测试学习有用吗_python自动化测试学习-UnitTest/PyUnit的用法介绍
  7. 安装MYSQL的思考与分析_mysql安装和基本使用
  8. idea怎么给项目改名_IDEA相关配置【java项目改造成web项目】
  9. it招聘上说熟悉linux系统,运维入门:细说Linux,做IT必看
  10. java复制文件的命名_java-复制文件时在文件名扩展名前附加“复...