简述

字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了String 类来创建和操作字符串。字符串缓冲区支持可变字符串。因为String对象是不可变的,因此可以共享它们。

String类代表字符串,Java程序中的所有字符串字面值如"abc"都是这个类的实例对象。String 类是不可改变的,所以你一旦创建了 String 对象,那它的值就无法改变了。如果需要对字符串做很多修改,那么应该选择使用StringBuilder或者StringBuffer。

最简单的创建字符串的方式:String qc = "qiu chan"编译器会使用该值创建一个 对象。我们也可以使用关键字New创建String对象。

String类型的常量池比较特殊。它的主要使用方法有两种:

直接使用双引号声明出来的String对象会直接存储在常量池中。

如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

继承/实现关系

public final class String    implements java.io.Serializable, Comparable, CharSequence {    // 省略}    

String是final修饰的不能够被继承和修改。

源码

String的底层使用的是char数组用于存储。

private final char value[];

缓存字符串的哈希码默认值为0

private int hash;

无参数构造函数

public String() {  this.value = "".value;}

解析:初始化一个新创建的String对象,使其代表一个空字符序列。 注意,由于String是不可变的,所以不需要使用这个构造函数。

参数为字符串的构造函数

public String(String original) { this.value = original.value; this.hash = original.hash;}

解析:初始化一个新创建的String对象,使其代表与参数相同的字符序列。换句话说,新创建的字符串是参数字符串的副本。除非需要参数字符串的显式拷贝,否则不需要使用这个构造函数,因为String是不可变的。

参数为char数组的构造函数

public String(char value[]) {  this.value = Arrays.copyOf(value, value.length);}

解析:分配一个新的String,使其代表当前字符数组参数中包含的字符序列。使用Arrays.copyOf方法进行字符数组的内容被复制。字符数组的后续修改不会影响新创建的字符串。

参数为char数组并且带有偏移量的构造方法

// value[]:作为字符源的数组,offset:偏移量、下标从0开始并且包括offset,count:从数组中取到的元素的个数。public String(char value[], int offset, int count) {  // 如果偏移量小于0抛出IndexOutOfBoundsException异常 if (offset < 0) {  throw new StringIndexOutOfBoundsException(offset); }  // 判断要取的元素的个数是否小于等于0 if (count <= 0) {    // 要取的元素的个数小于0,抛出IndexOutOfBoundsException异常  if (count < 0) {   throw new StringIndexOutOfBoundsException(count);  }    // 在要取的元素的个数等于0的情况下,判断偏移量是否小于等于数组的长度  if (offset <= value.length) {      // 偏移量小于等于数组的长度,返回一个空字符串数组的形式   this.value = "".value;   return;  } } // 如果偏移量的值大于数组的长度减去取元素的个数抛出IndexOutOfBoundsException异常 if (offset > value.length - count) {  throw new StringIndexOutOfBoundsException(offset + count); }  // 复制元素 this.value = Arrays.copyOfRange(value, offset, offset+count);}

解析:分配一个新的Sting,来源于给定的char数组中的字符。offset参数是子数组中第一个字符的索引,count参数指定子数组的长度。子数组被被复制以后,对字符数组的修改不会影响新创建的字符串。

参数为StringBuffer的构造方法

public String(StringBuffer buffer) {  // 这里对StringBuffer进行了加锁,然后再进行拷贝操作。这里对其进行加锁正是为了保证在多线程环境下只能有一个线程去操作StringBuffer对象。 synchronized(buffer) {  this.value = Arrays.copyOf(buffer.getValue(), buffer.length()); }}

解析:分配一个新的字符串,该字符串包含当前字符串缓冲区参数中包含的字符序列。Arrays.copyOf方法进行字符串缓冲区中内容的复制。这里对StringBuffer进行了加锁,然后再进行拷贝操作。这里对其进行加锁正是为了保证在多线程环境下只能有一个线程去操作StringBuffer对象。

参数为StringBuilder的构造方法

public String(StringBuilder builder) { this.value = Arrays.copyOf(builder.getValue(), builder.length());}

解析:参数是StringBuilder,这个是线程不安全的,但是性能相对于StringBuffer有很大的提升,源码的注释中说通过toString方法从字符串构建器中获取字符串可能会运行得更快,通常是首选。

length方法

public int length() {    // 查看源码发现,这个value是一个char数组,本质获取的是字符串对应的char数组的长度。  return value.length; }

解析:返回此字符串的长度。查看源码发现,这个value是一个char数组,本质获取的是字符串对应的char数组的长度。

isEmpty方法

public boolean isEmpty() {    // 底层的char数组的长度是否为0进行判断  return value.length == 0;}//举例@Testpublic void test_string_isEmpty(){ System.out.println(" ".isEmpty());// true  System.out.println("".isEmpty());// false}

解析:判断给定的字符串是否为空,底层实现是根据char数组的长度是否为0进行判断。

charAt方法

public char charAt(int index) {  // 给定的索引小于0或者给定的索引大于这个字符串对应的char数组的长度抛出角标越界异常 if ((index < 0) || (index >= value.length)) {  throw new StringIndexOutOfBoundsException(index); }  // 获取当前的指定位置的char字符 return value[index];}

解析:根据给定的索引获取当前的指定位置的char字符。如果给定的索引否小于0,或者给定的索引是大于这个字符串对应的char数组的长度抛出角标越界异常。index是从0开始到length-1结束。序列的第一个char值在索引0处,下一个在索引1处,依此类推,与数组索引一样。

getChars方法

// srcBegin:要复制的字符串中第一个字符的索引【包含】。srcEnd:要复制的字符串中最后一个字符之后的索引【不包含】。dst[]:目标数组。dstBegin:目标数组中的起始偏移量。public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {  // 校验起始索引小于0抛出角标越界异常 if (srcBegin < 0) {  throw new StringIndexOutOfBoundsException(srcBegin); }  // 校验结束索引大于原始字符串的长度抛出角标越界异常 if (srcEnd > value.length) {  throw new StringIndexOutOfBoundsException(srcEnd); }  // 校验结束索引大于起始索引抛出角标越界异常 if (srcBegin > srcEnd) {  throw new StringIndexOutOfBoundsException(srcEnd - srcBegin); }  // 数组的拷贝 System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);}// 案例@Testpublic void test_string_codePointAt(){  // 原始字符串 String h = "ahelloworld";  // 目标char数组 char[] data = new char[4];  // 执行拷贝 h.getChars(2, 6, data, 0); System.out.println(data);}

解析:将字符串中的字符复制到目标字符数组中。索引包含srcBegin,不包含srcEnd。

equals方法

// anObject:与此String进行比较的对象。public boolean equals(Object anObject) {  // 引用相同直接返回true if (this == anObject) {  return true; } // 判断给定的对象是否是String类型的 if (anObject instanceof String) {    // 给定的对象是字符串类型的转换为字符串类型  String anotherString = (String)anObject;    // 获取当前字符串的长度  int n = value.length;    // 判断给定字符串的长度是否等于当前字符串的长度  if (n == anotherString.value.length) {      // v1[]代表当前字符串对应的char数组   char v1[] = value;      // v2[]代表给定的字符串对应的char数组   char v2[] = anotherString.value;      // 遍历原始char数组,并且与给定的字符串对应的数组进行比较   int i = 0;   while (n-- != 0) {    if (v1[i] != v2[i])          // 任意一个位置上不相等返回false     return false;    i++;   }      // 都相等返回true   return true;  } }  // 不是String类型,或者长度不一致返回false return false;}

解析:这个方法重写了Object中的equals方法。方法中的将此字符串与指定对象进行比较。接下来附赠一个手写的String字符串equals方法。

手写equals方法

private boolean mineEquals(String srcObject, Object anObject){  // 比较引用是否相同 if (srcObject == anObject){  return true; }  // 引用不相同比较内容 if (anObject instanceof String){  String ans = (String) anObject;  char[] srcChar = srcObject.toCharArray();  char[] anChar = ans.toCharArray();  int n = srcChar.length;  if (n == anChar.length){   int i = 0;   while (n-- != 0){    if (srcChar[i] != anChar[i])     return false;    i++;   }   return true;  } } return false;}// 测试我们自己写的equals方法 @Test public void test_string_mine(){  String s = new String("aaa");    // 走的是引用的比较  System.out.println(s.equals(s));// true   boolean b = mineEquals(s, s);  System.out.println(b);// true }

equalsIgnoreCase方法

public boolean equalsIgnoreCase(String anotherString) {  // 引用相同返回true。引用不相同进行长度、各个位置上的char是否相同 return (this == anotherString) ? true   : (anotherString != null)   && (anotherString.value.length == value.length)   && regionMatches(true, 0, anotherString, 0, value.length);}

解析:将此字符串与另一个字符串进行比较,而忽略大小写注意事项。regionMatches方法的源码很有趣的,源码里面有一个while循环,先进行未忽略大小的判断,然后进行忽略大小的判断,在忽略大小的判断中,先进行的是大写的转换进行比较,但是可能会失败【这种字体Georgian alphabet】。所以在大写转换以后的比较失败,进行一次小写的转换比较。

startsWith方法

// 判断是否以指定的前缀开头public boolean startsWith(String prefix) {  // 0代表从开头进行寻找  return startsWith(prefix, 0);}

endsWith方法

// 判断是否以指定的前缀结尾public boolean endsWith(String suffix) {  // 从【value.length - suffix.value.length】开始寻找,这个方法调用的还是startsWith方法  return startsWith(suffix, value.length - suffix.value.length);}

startsWith和endsWith最终的实现方法

// prefix: 测试此字符串是否以指定的前缀开头。toffset: 从哪里开始寻找这个字符串。public boolean startsWith(String prefix, int toffset) {  // 原始的字符串对应的char[] char ta[] = value;  // 开始寻找的位置 int to = toffset;  // 获取指定的字符串对应的char[] char pa[] = prefix.value; int po = 0;  // 获取指定的字符串对应的char[]长度 int pc = prefix.value.length; // 开始寻找的位置小于0,或者起始位置大于要查找的长度【value.length - pc】返回false。 if ((toffset < 0) || (toffset > value.length - pc)) {  return false; }  // 比较给定的字符串的char[]里的每个元素是否跟原始的字符串对应的char数组的元素相同 while (--pc >= 0) {  if (ta[to++] != pa[po++]) {      // 有一个char不相同返回false   return false;  } }  // 相同返回true return true;}

substring方法

// 返回一个字符串,该字符串是该字符串的子字符串。beginIndex开始截取的索引【包含】。public String substring(int beginIndex) {  // 校验指定的索引,小于0抛出角标越界 if (beginIndex < 0) {  throw new StringIndexOutOfBoundsException(beginIndex); }  // 子字符串的长度 int subLen = value.length - beginIndex;  // 子字符串的长度小于0抛出角标越界 if (subLen < 0) {  throw new StringIndexOutOfBoundsException(subLen); }  // 开始位置为0,返回当前字符串,不为0,创建一个新的子字符串对象并返回 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);}

解析:返回一个字符串,该字符串是该字符串的子字符串。子字符串以指定索引处的字符开头【包含】,并且扩展到该字符串的末尾。

substring方法

// beginIndex:开始位置【包含】。 endIndex:结束位置【不包含】。public String substring(int beginIndex, int endIndex) {  // 校验指定的开始索引,小于0抛出角标越界 if (beginIndex < 0) {  throw new StringIndexOutOfBoundsException(beginIndex); }  // 校验指定的结束索引,大于给定的字符串的char数组的长度抛出角标越界 if (endIndex > value.length) {  throw new StringIndexOutOfBoundsException(endIndex); }  // 要截取的长度 int subLen = endIndex - beginIndex;  // 要截取的长度小于0,抛出角标越界 if (subLen < 0) {  throw new StringIndexOutOfBoundsException(subLen); }  // 截取字符串 return ((beginIndex == 0) && (endIndex == value.length)) ? this   : new String(value, beginIndex, subLen);}

解析:返回一个字符串,该字符串是该字符串的子字符串。子字符串从指定的beginIndex开始【包含】,并且扩展到索引endIndex-1处的字符【不包含】。

concat方法

public String concat(String str) {  // 获取给定的字符串的长度 int otherLen = str.length();  // 长度为0,直接返回当前的字符串 if (otherLen == 0) {  return this; }  // 获取当前字符串的长度 int len = value.length;  // 构建一个新的长度为len + otherLen的字符数组,并且将原始的数据放到这个数组 char buf[] = Arrays.copyOf(value, len + otherLen);  // 这个底层调用是System.arraycopy这个方法的处理是使用c语言写的 str.getChars(buf, len); return new String(buf, true);}

将指定的字符串连接到该字符串的末尾。字符串拼接。

format方法

// 使用指定的格式字符串和参数返回格式化的字符串。public static String format(String format, Object... args) { return new Formatter().format(format, args).toString();}// 案例,这里是使用%s替换后面的如"-a-"@Testpublic void test_start(){ System.out.println(String.format("ha %s hh %s a %s h", "-a-", "-b-", "-c-"));}

trim方法

public String trim() {  // 指定字符串的长度 int len = value.length;  // 定义一个开始位置的索引0 int st = 0;  // 定义一个char[] val,用于避免使用getfiled操作码,这个可以写段代码反编译一下看看 char[] val = value;  // 对于字符串的开头进行去除空格,并记录这个索引 while ((st < len) && (val[st] <= ' ')) {  st++; }  // 对于字符串的尾部进行去除空格,也记录这个索引,这个索引就是去除尾部空格后的索引 while ((st < len) && (val[len - 1] <= ' ')) {  len--; }  // 根据上面记录的长度判断是否要截取字符串 return ((st > 0) || (len < value.length)) ? substring(st, len) : this;}

返回一个字符串,其值就是这个字符串,并去掉任何首部和尾部的空白。

join方法

// 返回一个新的String,该字符串由给定的分隔符和要连接的元素组成。delimiter:分隔每个元素的分隔符。elements:连接在一起的元素。public static String join(CharSequence delimiter, CharSequence... elements) {  // delimiter和elements为空抛出空指针异常,null会被拦截,""不会被拦截 Objects.requireNonNull(delimiter); Objects.requireNonNull(elements); //  StringJoiner joiner = new StringJoiner(delimiter);  // 遍历给定的要拼接的元素,拼接的元素允许为null for (CharSequence cs: elements) {    // 执行拼接方法  joiner.add(cs); } return joiner.toString();}// 拼接方法public StringJoiner add(CharSequence newElement) {  // prepareBuilder()方法首次调用会创建StringBuilder对象,后面再调用会执行拼接分隔符 prepareBuilder().append(newElement); return this;}// 未进行拼接创建StringBuilder对象,已经拼接以后value != null执行拼接分隔符private StringBuilder prepareBuilder() {  // 判断拼接的value是否为空 if (value != null) {    // 不为空执行拼接分隔符  value.append(delimiter); } else {    // 最开始使用拼接的时候,调用这个方法创建一个空的StringBuilder对象,只调一次  value = new StringBuilder().append(prefix); } return value;}// 上面是调用的这个拼接元素方法@Overridepublic StringBuilder append(CharSequence s) {  // 这里啥都没处理,调用的是父类的append方法,设计模式为建造者模式 super.append(s); return this;}// 上面的prepareBuilder方法是拼接分隔符,这个方法是将分隔符和给定的元素拼接的方法@Overridepublic AbstractStringBuilder append(CharSequence s) {  // 以下3个判断根据类型和是否为空进行区别拼接 if (s == null)  return appendNull(); if (s instanceof String)  return this.append((String)s); if (s instanceof AbstractStringBuilder)  return this.append((AbstractStringBuilder)s);  // 拼接 return this.append(s, 0, s.length());}

将给定的字符串以给定的分割符分割并返回分隔后的字符串。

replace方法

// target:要被替换的目标字符串。 replacement:替换的字符串public String replace(CharSequence target, CharSequence replacement) { return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(   this).replaceAll(Matcher.quoteReplacement(replacement.toString()));}

解析:用指定的字符串替换这个字符串中与之匹配的每个子字符串。替换从字符串的开头到结尾,例如,在字符串 "aaa "中用 "b "替换 "aa "将导致 "ba "而不是 "ab"。

replaceAll方法

// regex:这个支持正则表达式,也可以是要被替换的目标字符串。public String replaceAll(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceAll(replacement);}

问题:replace和replaceAll方法的区别是啥?

replaceAll支持正则表达式。

针对char的replace方法

// oldChar:要被替换的字符,newChar:替换的字符public String replace(char oldChar, char newChar) {  // oldChar不等于newChar if (oldChar != newChar) {    // 当前字符串的长度  int len = value.length;    // 这个用于下面的while循环里的条件比较,val[i]中的i是从0开始的  int i = -1;    // 定义一个char[] val,用于避免使用getfiled操作码,这个可以写段代码反编译一下看看  char[] val = value; /* avoid getfield opcode */    // 这个用于记录这个i的值,并且判断是否有要替换的,这个循环有利于性能的提升  while (++i < len) {      // val[i]中的i是从0开始的   if (val[i] == oldChar) {        // 有要替换的直接跳出循环    break;   }  }    // 上面的while循环中如果有要替换的i肯定小于len,如果没有下面这个判断就不会执行  if (i < len) {      // 能进到这个循环肯定是有要替换的,创建一个长度为len的char数组   char buf[] = new char[len];      // 上面的i是记录第一个可以替换的char的索引,下面这个循环是将这个i索引前的不需要被替换的填充到buf[]数组中   for (int j = 0; j < i; j++) {        // 填充buf[]数组    buf[j] = val[j];   }      // 从可以替换的索引i开始将剩余的字符一个一个填充到 buf[]中   while (i < len) {        // 获取要被替换的字符    char c = val[i];        // 判断这个字符是否真的需要替换,c == oldChar成立就替换,否则不替换    buf[i] = (c == oldChar) ? newChar : c;    i++;   }      // 返回替换后的字符串   return new String(buf, true);  } }  // oldChar等于newChar直接返回当前字符串 return this;}

案例

@Testpublic void test_matches(){ String a = "adddfdefe"; System.out.println(a.replace('d', 'b'));// abbbfbefe}仿写replace方法参数针对char

仿写

// 和源码给的唯一不同的是参数传递,其他的都和源码一样,自己写一遍可以加深记忆和借鉴编程思public String replace(String source, char oldChar, char newChar) {  char[] value = source.toCharArray(); 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);  } } return new String(value);}

intern方法

public native String intern();

这是一个native方法。调用String#intern方法时,如果池中已经包含一个由equals方法确定的等于此String对象的字符串,则返回来自池的字符串。否则,将此String对象添加到池中,并返回这个String的引用。

分享

上面是本人在学习路上整理的一些比较干货的java资料,如果有需要的兄弟可以先关注我,私信我回复【资料】即可。

java的map 使用string数组多了双引号_奥奥奥利给!!!再也不怕面试官问我String源码了!来吧...相关推荐

  1. 面试官问我 “String 的不可变真的是因为 final 吗“,我回答 “是“ 然后就被挂了。。。。。。

    String 为啥不可变?因为 String 中的 char 数组被 final 修饰.这套回答相信各位已经背烂了,But 这并不正确! 面试官:讲讲 String.StringBuilder.Str ...

  2. 图解 Java 线程的生命周期,看完再也不怕面试官问了

    文章首发自个人微信公众号: 小哈学Java www.exception.site/java-concur- 在 Java 初中级面试中,关于线程的生命周期可以说是常客了.本文就针对这个问题,通过图文并 ...

  3. 【020期】面试官问:Java 遍历 Map 集合有几种方式?效率如何?

    >>号外:关注"Java精选"公众号,回复"2021面试题",领取免费资料!"Java精选面试题"小程序,3000+ 道面试题在 ...

  4. java执行sql文件_面试官问你MyBatis SQL是如何执行的?把这篇文章甩给他

    初识 MyBatis MyBatis 是第一个支持自定义 SQL.存储过程和高级映射的类持久框架.MyBatis 消除了大部分 JDBC 的样板代码.手动设置参数以及检索结果.MyBatis 能够支持 ...

  5. 当面试官问我————为什么String是final的?

    面试官:你好,能看得清下面这张图吗? 我:可以的. 面试官:恩,好的.呃,你能不能说一说为什么String要用final修饰? 我:final意味着不能被继承或者被重写,String类用final修饰 ...

  6. string replace_面试必问:String类型为什么设计成不可变的?

    这几天在各大平台上都看到过这样一些帖子,全都是关于String类型对象不可变的问题,当然现在也是找工作的准备时期,因此花了一部分时间对其进行整理一下. 想要完全了解String,在这里我们需要解决以下 ...

  7. 【Java自顶向下】试手小公司,面试官问我ConcurrentHashMap,我微微一笑……

    文章目录 ConcurrentHashMap 一.ConcurrentHashMap初始化的剖析 1.1 ConcurrentHashMap初始化 1.2 理解sizeCtl 二.JDK8的添加安全 ...

  8. 面试官问:为什么 Java 线程没有 Running 状态?我懵了

    转载自 面试官问:为什么 Java 线程没有 Running 状态?我懵了 什么是 RUNNABLE? 与传统的ready状态的区别 与传统的running状态的区别 当I/O阻塞时 如何看待RUNN ...

  9. 面试官问:为什么 Java 线程没有Running状态?我懵了

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取 后台回复"k8s",可领取k8s资料 title: 面 ...

最新文章

  1. SAP 同一个序列号可以同时出现在2个不同的HU里?
  2. 深入浅出详细介绍Java异常,让你茅塞顿开般的感觉
  3. 从浪漫走向坚韧:开源数据库的演变
  4. WWDC 2018: ARKit 2 的新功能
  5. 矩池云上安装yolov5并测试
  6. XStream入门应用程序
  7. 微信小程序轮播图,图片自适应,图片循环播放,图片之间有空白空间
  8. nginx配置ssl证书实现微信小程序后端接口访问
  9. doc 问卷调查模板表_问卷调查Word模板.doc
  10. ssoj1556土地购买
  11. Unity零基础到入门 ☀️| 万字教程 对 Unity 中的 Navigation导航系统基础 全面解析+实战演练【收藏不迷路】
  12. 在线QQ客服链接,只添加 qq号
  13. 用计算机信息术语感恩老师,感恩老师的祝福语(精选50句)
  14. ​你在淘宝剁手,钱却可能进入黑客的口袋
  15. 做自适应网站专业乐云seo_广州网站设计【乐云seo】
  16. 关于TP-Link和水星、迅捷三角关系的传闻~~
  17. android 瘦脸模式 sdk,大眼瘦脸加磨皮通通不能少,论美颜SDK是如何实现的
  18. 常见格式浏览器在线预览
  19. 【俗话编程】什么是对象?
  20. 卡耐基人性的弱点目录

热门文章

  1. 掌握Android中的进程和线程
  2. linux sed名宁,Linux shell利用sed批量更改文件名的方法
  3. kangle支不支持PHP_【转载】PHP调用kangle的API
  4. 亚马逊推荐python_使用python查找amazon类别
  5. Linux安装MySQL的两种方法
  6. 当create table as select 遇上大数据
  7. jsp超链接到java文件,jsp页面超链接传中文终极解决办法
  8. hive安装mysql驱动_Hadoop-2.6.0为基础的Hive安装
  9. 阿里云服务器安装onlyoffice_阿里云服务器安装 JDK 8
  10. 一个函数里两个setjmp_C语言中setjmp.h的longjmp()函数