引言

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 43 字符串相乘

    43. 字符串相乘 给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式. 示例 1: 输入: num1 = "2& ...

  2. java计算时间从什么时候开始 为什么从1970年开始 java的时间为什么是一大串数字

    Date date = new Date(0); System.out.println(date); 打印出来的结果: Thu Jan 01 08:00:00 CST 1970 也是1970 年 1 ...

  3. (九)不安全的HTTP方法

    01 漏洞描述 <HTTP | HTTP报文>中介绍了HTTP的请求方法.在这些方法中,HTTP定义了一组被称为安全方法的方法:GET.HEAD.OPTIONS.TRACE,这些方法不会产 ...

  4. Error:org.gradle.api.internal.tasks.DefaultTaskInputs$TaskInputUnionFileCollection cannot be cast to...异常处理

    这个是打开Android Studio项目报的错误提示,单纯从上面的提示还是不能太直接的知道什么问题.后来我想这个项目的Gradle版本与我当前AS使用的版本不一致,可能是这个问题. 修改build. ...

  5. ELK的踩坑之旅

    前言 设计思路如下 有3台机器 2台做elasticsearch的主副节点 1台做kibana和elasticsearch_head 由于机器匮乏我还在这台机器上部署了logstash和nginx服务 ...

  6. Spring Boot 教程 - Elasticsearch

    1. Elasticsearch简介 Elasticsearch是一个基于Lucene的搜索服务器.它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口.Elasticsearc ...

  7. Java 切割字符串的几种方式

    按效率排: 1.StringTokenizer切割 是java.util中的一个类,包含的api有: StringTokenizer的方法请参考上一篇. 其中,countTokens为length:n ...

  8. 在CentOS7上源码安装OpenResty

    您必须将这些库perl 5.6.1+libreadlinelibpcrelibssl安装在您的电脑之中. 对于 Linux来说, 您需要确认使用 ldconfig 命令,让其在您的系统环境路径中能找到 ...

  9. springboot使用自定义异常

    sprinboot使用自定义注解 创建自定义异常类,继承RuntimeException public class MyException extends RuntimeException {   p ...

  10. @atcoder - AGC002E@ Candy Piles

    目录 @description@ @solution@ @accepted code@ @details@ @description@ 给定 N 堆糖果,第 i 堆包含 ai 个糖果. 现在两人进行博 ...