引言

String可以说是在Java开发中必不可缺的一种类,String容易忽略的细节也很多,对String的了解程度也反映了一个Java程序员的基本功。下面就由一个面试题来引出对String的剖析。

1. String在源码里究竟是如何实现的,它有哪些方法,有什么作用?

从源码可以看出,String有三个私有方法,底层是由字符数组来存储字符串

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/**存储字符串的字符数组*/
private final char value[]; /** 缓存字符串的hashcode */
private int hash; // 默认是0 /** 用于验证一致性来是否进行反序列化 */
private static final long serialVersionUID = -6849794470754667710L;

1.1 String重要构造方法

// String 为参数的构造方法
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
// char[] 为参数构造方法
public String(char value[]) {
//重新复制一份char数组的值和信息,保证字符串不会被修改传回
    this.value = Arrays.copyOf(value, value.length);
}
// StringBuffer 为参数的构造方法
public String(StringBuffer buffer) {
    synchronized(buffer) {
        this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    }
}
// StringBuilder 为参数的构造方法
public String(StringBuilder builder) {
    this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

1.2 String重要的方法

1.2.1 equals()方法

/**比较两个字符串是否相等,返回值为布尔类型*/
public boolean equals(Object anObject) {//比较类型可以是object
/*引用对象相同时返回true*/
if (this == anObject) {
return true;
}
/*判断引用对象是否为String类型*/
if (anObject instanceof String) { //instanceof用来判断数据类型是否一致
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;
}

equals()方法首先通过instanceof判断数据类型是否一致,是则进行下一步将两个字符串转换成字符数组逐一判断。最后再返回判断结果。

1.2.2 compareTo()方法

/*比较两个字符串是否相等,返回值为int类型*/
public int compareTo(String anotherString) {//比较类型只能是String类型
int len1 = value.length;
int len2 = anotherString.value.length;
/*获得两字符串最短的字符串长度lim*/
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
/*逐一比较两字符组的字符*/
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
//若两字符不相等,返回c1-c2
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}

compareTo()通过逐一判断两字符串中的字符,不相等则返回两字符差,反之循环结束最后返回0

小结
  1. compareTo()equals()都能比较两字符串,当equals()返回true,compareTo()返回0时,都表示两字符串完全相同。
  2. 同时两者也有区别:
    • 返回类型compareTo()是boolean,equals()是int。
    • 字符类型compareTo()是Object,equals()只能是String类型。

1.3其他方法

  1. indexOf():查询字符串首次出现的下标位置
  2. lastIndexOf():查询字符串最后出现的下标位置
  3. contains():查询字符串中是否包含另一个字符串
  4. toLowerCase():把字符串全部转换成小写
  5. toUpperCase():把字符串全部转换成大写
  6. length():查询字符串的长度
  7. trim():去掉字符串首尾空格
  8. replace():替换字符串中的某些字符
  9. split():把字符串分割并返回字符串数组
  10. join():把字符串数组转为字符串

知道了String的实现和方法,下面就要引出常见的String面试问题

2. String常见的面试问题

2.1 为什么String类型要用final修饰?

  • 从上面的代码可以看出,String类是被private final修饰的不可继承类。那么为何要用final修饰呢?

Java 语言之父 James Gosling 的回答是,他会更倾向于使用 final,因为它能够缓存结果,当你在传参时不需要考虑谁会修改它的值;如果是可变类的话,则有可能需要重新拷贝出来一个新值进行传参,这样在性能上就会有一定的损失。

James Gosling 还说迫使 String 类设计成不可变的另一个原因是安全,当你在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题,这是迫使 String 类设计成不可变类的一个重要原因。

​ 所以只有当字符串不可改变时,才能利用字符常量池,保证在使用字符的时候不会被修改。

  • 那么问题来了,我们在使用final修饰一个变量时,不变的是引用地址,引用地址对应的对象是可以发生变化的。如:

    import java.util.Arrays;
    public class IntTest{
    public static void main(String args[]){
    final char[] arr = new char[]{'a', 'b', 'c', 'd'};
    System.out.println("arr的地址1:" + arr);
    System.out.println("arr的值2:" + Arrays.toString(arr));
    //修改arr[2]的值
    arr[2] = 'b';
    //修改arr数组的地址,这里会发生编译错误,所以无法修改引用地址
    //arr = new char[]{'1', '2', '3'};
    System.out.println("arr的地址2:" + arr);
    System.out.println("arr的值2:" + Arrays.toString(arr)); }
    }
    /*运行结果:
    arr的地址1:[C@15db9742
    arr的值1:[a b c d]
    arr的地址2:[C@15db9742
    arr的值2:[a b b d] 显然不变的是引用地址,引用地址所指对象的内容可以被修改
    */

    而在上述源码中,String类下有一个私有的char数组成员

    public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /**存储字符串的字符数组*/
    private final char value[];

    那么是否可以通过修改char数组所指对象的内容,来改变string的值呢?来试一试:

    import java.util.Arrays;
    public class IntTest{
    public static void main(String args[]){
    char[] arr = new char[]{'a','b','c','d'};
    String str = new String(arr);
    System.out.println("arr的地址1:" + arr);
    System.out.println("str= " + str);
    System.out.println("arr[]= "+Arrays.toString(arr));
    //修改arr[2]的值
    arr[2]='b';
    System.out.println("arr的地址2:" + arr);
    System.out.println("str= "+str);
    System.out.println("arr[]= "+Arrays.toString(arr)); }
    }
    /*运行结果:
    arr的地址1:[C@15db9742
    str= abcd
    arr[]= [a, b, c, d]
    arr的地址2:[C@15db9742
    str= abcd
    arr[]= [a, b, b, d]
    */

    显然无法修改字符串,这是为何,我们再看看构造方法

    // String 为参数的构造方法
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    // char[] 为参数构造方法
    public String(char value[]) {
    //重新复制一份char数组的值和信息,保证字符串不会被修改传回
        this.value = Arrays.copyOf(value, value.length);
    }

    发现string的构造方法里将原来的char数组的值和信息copy了一份,保证字符串不会被修改传回。

2.2 equals()和 == 的区别

2.2.1 先说结论:

  • ==在基本类型中比较其对应的值,在引用类型中比较其地址值

  • equals()在未被重写时和 == 完全一致,被重写后是比较字符串的值

    public class StringTest {
    public static void main(String args[]) {
    String str1 = "Java"; //放在常量池中
    String str2 = new String("Java"); //在堆中创建对象str2的引用
    String str3 = str2; //指向堆中的str2的对象的引用
    String str4 = "Java"; //从常量池中查找
    String str5 = new String("Java");
    System.out.println(str1 == str2); //false
    System.out.println(str1 == str3); //false
    System.out.println(str1 == str4); //true
    System.out.println(str2 == str3); //true
    System.out.println(str2 == str5); //false
    System.out.println(str1.equals(str2)); //true
    System.out.println(str1.equals(str3)); //true
    System.out.println(str1.equals(str4)); //true
    System.out.println(str2.equals(str3)); //true
    }
    }

    实际上equals()方法也是继承Object的equals()方法。

    public boolean equals(Object obj) {
    return (this == obj);
    }

    从上面的equals()方法的源码可以看出,String在继承方法后对应修改了方法中的相关内容,所以上述代码的equals()方法输出都是true。

    ​ 类似于String str1 = "Java"; 的和String str2 = new String("Java");形式有很大的区别,String str1 = "Java"; 形式首先在编译过程中Java虚拟机就会去常量池中查找是否存在“Java”,如果存在,就会在栈内存中开辟一块地方用于存储其常量池中的地址。所以这种形式有可能创建了一个对象(常量池中),也可能一个对象也没创建,即str1是直接在常量池中创建“Java”字符串,str4是先在常量池中查找有“Java”,所以直接地址直接指向常量池中已经存在的”Java“字符串。

    String str2 = new String("Java");的形式在编译过程中,先去常量池中查找是否有“Java”,没有则在常量池中新建"Java"。到了运行期,不管常量池中是否有“Java”,一律重新在堆中创建一个新的对象,然如果常量池中存在“Java”,复制一份放在堆中新开辟的空间中。如果不存在则会在常量池中创建一个“Java”后再复制到堆中。所以这种形式至少创建了一个对象,最多两个对象。因此str1和str2的引用地址必然不相同。

2.3 string中的intern()方法

​ 调用intern方法时,如果常量池中存在该字符串,则返回池中的字符串。否则将此字符串对象添加到常量池中,并返回该字符串的引用。

String s1 = new String("Java");
String s2 = s1.intern();//直接指向常量池中的字符串
String s3 = "Java";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true

2.4 String和StringBuilder、StringBuffer的区别

​ 关于这三者的区别,主要借鉴这篇博文String,StringBuffer与StringBuilder的区别??首先,String是字符串常量,后两者是字符串变量。其中StringBuffer是线程安全的,下面说说他们的具体区别。

​ String适用于字符串不可变的情况,因为在经常改变字符串的情形下,每次改变都会在堆内存中新建对象,会造成 JVM GC的工作负担,因此在这种情形下,需要使用字符串变量。

​ 再说StringBuffer,它是线程安全的可变字符序列,它提供了append和insert方法用于字符串拼接,并用synchronized来保证线程安全。并且可以对这些方法进行同步,像以串行顺序发生,而且该顺序与所涉及的每个线程进行的方法调用顺序一致。

@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
} @Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

​ 最后是StringBuilder,因为StringBuffer要保证线程安全,所以性能不是很高,于是在JDK1.5后引入了StringBuilder,在没有了synchronize后性能得到提高,而且两者的方法基本相同。所以在非并发操作下,如单线程情况可以使用StringBuilder来对字符串进行修改。

2.5 String中的“ + ”操作符

​ 其实在2.4中提到,String是字符串常量,具有不可变性。所以在拼接字符串、修改字符串时,尽量选择StringBuilder和StringBuffer。下面再谈一谈String中出现“+”操作符的情况:

String s1 = "Ja";
String s2 = "va";
String s3 = "Java";
String s4 = "Ja" + "va"; //在编译时期就在常量池中创建
String s5 = s1 + s2; //实际上s5是stringBuider,这个过程是stringBuilder的append System.out.println("s3 == s4 " + (s3 == s4));
System.out.println("s3 == s5 " + (s3 == s5)); /**
运行结果:
s3 == s4 true
s3 == s5 false
*/

为什么s4==s3结果是true? 反编译看看:

1  String s = "Ja";//s1
2 String s1 = "va";//s2
3 String s2 = "Java";//s3
4 String s3 = "Java";//s4
5 String s4 = (new StringBuilder()).append(s).append(s1).toString();//s5
6 System.out.println((new StringBuilder()).append("s3 == s4").append(s2 == s3).toString());
7 System.out.println((new StringBuilder()).append("s3 == s5").append(s2 == s4).toString());

从第5行代码中看出s4在编译时期就已经将“Ja”+“va”编译“Java” ,这就是JVM的优化

第6行的代码说明在s5 = s1 +s2;执行过程,s5变成StringBuilder,并利用append方法将s1和s2拼接。

因此在String类型中使用“+”操作符,编译器一般会将其转换成new StringBuilder().append()来处理。

Java String的相关性质分析的更多相关文章

  1. 面试之Java String 编码相关

    实话说,作为一个多年Java老年程序员,直到近来,在没有决心花时间搞清楚Java String的编码相关问题之前, 自己也都还是似懂非懂,一脸懵逼的.设想如果在面试中,有同学能够条理清晰的回答下面的问 ...

  2. java基础知识回顾之---java String final类 容易混淆的java String常量池内存分析

    /** *   栈(Stack) :存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放  在常量池中). 堆(heap):存 ...

  3. Java String类相关知识梳理(含字符串常量池(String Pool)知识)

    目录 1. String类是什么 1.1 定义 1.2 类结构 1.3 所在的包 2. String类的底层数据结构 3. 关于 intern() 方法(重点) 3.1 作用 3.2 字符串常量池(S ...

  4. Java String对象面试题分析

  5. java String.split()函数的用法分析

    java String.split()函数的用法分析 栏目:Java基础 作者:admin 日期:2015-04-06 评论:0 点击: 3,195 次 在java.lang包中有String.spl ...

  6. 手写代码 - java.lang.String/StringBuilder 相关

    语言:Java 9-截取某个区间的string /** * Returns a string that is a substring of this string. The * substring b ...

  7. 从Java String实例来理解ANSI、Unicode、BMP、UTF等编码概念

    转(http://www.codeceo.com/article/java-string-ansi-unicode-bmp-utf.html#0-tsina-1-10971-397232819ff9a ...

  8. Java总结篇系列:Java String

    String作为Java中最常用的引用类型,相对来说基本上都比较熟悉,无论在平时的编码过程中还是在笔试面试中,String都很受到青睐,然而,在使用String过程中,又有较多需要注意的细节之处. 1 ...

  9. Java语言基础相关问题

    *动手动脑: 问题1:   仔细阅读示例: EnumTest.java,运行它,分析运行结果? 源代码: public class EnumTest { public static void main ...

随机推荐

  1. Java实现 LeetCode 542 01 矩阵(暴力大法,正反便利)

    542. 01 矩阵 给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离. 两个相邻元素间的距离为 1 . 示例 1: 输入: 0 0 0 0 1 0 0 0 0 输出: 0 0 0 ...

  2. Java实现 LeetCode 82 删除排序链表中的重复元素 II(二)

    82. 删除排序链表中的重复元素 II 给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字. 示例 1: 输入: 1->2->3->3->4- ...

  3. Java实现 LeetCode 29 两数相除

    29. 两数相除 给定两个整数,被除数 dividend 和除数 divisor.将两数相除,要求不使用乘法.除法和 mod 运算符. 返回被除数 dividend 除以除数 divisor 得到的商 ...

  4. Android如何使用OKHttp

    首先要在build.gradle里面写入 // 添加OKHttp支持 implementation("com.squareup.okhttp3:okhttp:4.3.1") 下面是 ...

  5. java实现第六届蓝桥杯奇妙的数字

    奇妙的数字 奇妙的数字 小明发现了一个奇妙的数字.它的平方和立方正好把0~9的10个数字每个用且只用了一次. 你能猜出这个数字是多少吗? 请填写该数字,不要填写任何多余的内容. 结果:69 impor ...

  6. Python学习之温度转换实例分析篇

    #TempConvert.py Tempstr=input('请输入要转换的温度值:') if Tempstr[-1] in ['C','c']: F=1.8*eval(Tempstr[0:-1])+ ...

  7. webpack从单页面到多页面

    前言 从上次更完webpack从什么都不懂到入门之后,好久没有更新过文章了,可能是因为自己懒了吧.今天看了下自己的索引量少了一半o(╥﹏╥)o,发现事态严重,赶紧更新一篇23333 也是因为最近踩了一 ...

  8. redis基础知识详解

    一.redis基础知识 1.Redis是什么Redis是一个开源的key-value存储系统. 和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表 ...

  9. Spring IoC 容器的扩展

    前言 本篇文章主要介绍 Spring 中 BeanFactory 的扩展 ApplicationContext,我们平时日常开发中也基本上是使用它,不会去直接使用 BeanFactory. 那么在 S ...

  10. java之单点登录(SSO)

    单点登录(SSO):SSO是指在多个应用系统中个,用户只需要登陆一次就可以访问所有相互信任的应用系统.它包括可以将这次主要的登录映射到其他应用中用于同一用户的登陆的机制. SSO的实现过程: 通过上述 ...