Java实习生面试复习(一):String类型

我是一名很普通的双非大三学生。接下来的几个月内,我将坚持写博客,输出知识的同时巩固自己的基础,记录自己的成长和锻炼自己,备战2021暑期实习面试!奥利给!!

话不多说,我们来看大家最常用到的String类型

String 是如何实现的?它有哪些重要的方法?

以主流的JDK版本1.8来说,String内部实际存储结构为char数组,源码如下:

public final class String implements java.io.Serializable,Comparable<String>,CharSequence{...
    /** The value is used for character storage. */
    private final char value[];

    /** 用来缓存Hash,避免每次都需要去重复计算 */
    private int hash; // Default to 0

	...

注意:JDK9以后,不再是char[]数组了,而是使用byte数组,因为可以减少一半的内存,byte使用一个字节来存储一个char字符,char使用两个字节来存储一个char字符。只有当一个char字符大小超过0xFF时,才会将byte数组变为原来的两倍,用两个字节存储一个char字符。

这里我们可以看到String类型其实是被final关键字修饰的,这也是我们要探究的第一个问题

为什么String要用final修饰的,它的好处在哪?

我们先来看一段简短的代码

        String s1 = "final";
        s1 = "test";

在这里插入图片描述
在这里插入图片描述
通过Debug我们可以看到,实际上value数组的引用是改变了的,也就说 s =“test” 这个看似简单的赋值,其实已经把 s 的引用指向了新的 String。
首先,我们在编码中,我们知道如果你认为这个类已经定义完全并且不需要任何子类的话,可以将这个类声明为Final,Final类中的方法将永远不会被重写。在Java中也是这样,String是被设计成一个不可变(immutable)类,一旦创建完后,字符串本身是无法通过正常手段被修改的。

Final关键字知识点补充,final 的意思是不变的,一般来说用于以下三种场景:

  • 被 final 修饰的类,表明该类是无法继承的;
  • 被 final修饰的方法,表明该方法是无法覆写的;
  • 被 final 修饰的变量,说明该变量在声明的时候,就必须初始化完成,而且以后也不能修改其内存地址。

第三点注意下,我们说的是无法修改其内存地址,并没有说无法修改其值。因为对于 List、Map 这些集合类来 说,被 final修饰后,是可以修改其内部值的,但却无法修改其初始化时的内存地址。

例子我们就不举了,本文的String 的不变性就是一个很好的例子。因为 String 具有不变性,所以 String 的大多数操作方法,都会返回新的 String,这里我们可以参考substring

    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,不会让你去修改内部的内容。
但是这还是没点到为什么要设计成不可变类的,我们并不是Java的设计者,所以我们只能综合好处去概述这样做的原因:

  • 可以实现多个变量引用JVM内存中的同一个字符串实例,也就是字符串常量池 String Pool。
  • 增强安全性,我们不如反向举例,如果String是可变的,那么会带来什么问题
  1. 比如Map的key,其实Map的key也是需要用不可变类的,不然就会出现找不到key等问题。
  2. 当你在调用其他方法,比如调用一些系统级操作之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,其内部的值被改变了,可能引起严重的系统崩溃问题。
  • 提高效率,我们知道在程序中会大量用到String,正如上第一点实现了池以后,避免了每次都是堆中创建对象,可以提高效率,且由于String的不可变性,可以只计算一次哈希值,然后缓存在内部,后续直接取就好了,所以在Hash操作也能提高效率。

但是不可变并不是完全不可变,如果你非要变,通过反射也是可以实现的,例如:

        String str = "不可变";
        System.out.println(str);
        Field field = String.class.getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char[]) field.get(str);
        value[0] = '可';
        value[1] = '以';
        value[2] = '变';
        System.out.println(str);
输出结果:
不可变
可以变

String 和 StringBuilder、StringBuffer 的区别

因为String类型是不可变的,所以在字符串拼接的时候如果使用String的话性能会很低,因此我们就需要使用另外的数据类型StringBuffer,它提供了append和insert方法可用于字符串的拼接,StringBuffer使用synchronized来保证线程安全, 所以性能不是很高。
列举一个代码片段:

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

于是在JDK 1.5有了StringBuilder,它同样提供了append和insert的拼接方法,但它没有使用synchronized来修饰,因此在性能上要优于StringBuffer,所以在非并发操作的情况下可以使用后者。

这里还可以扩展一个什么情况下用+号,什么时候用StringBuilder、StringBuffer

String类型的常见操作

1、String 类型在 JVM(Java 虚拟机)中是如何存储的?

String常见的创建方式有两种, String s1 = "Java"String s2 = new String("Java")的方式,两者在JVM的存储区域却截然不同,在JDK 1.8中,s1会先去字符串常量池中找字符串"Java” ,如果有相同的字符则直接返回常量句柄,如果没有此字符串则会先在常量池中创建此字符串,然后再返回常量句柄;而变量s2是直接在堆上创建一个变量 ,如果调用intern方法才会把此字符串保存到常量池中,intern还是很少用到的,一般也只出现在面试题里,不用深究。如下代码所示:

        String s1 = new String("南街");
        String s2 = s1.intern();
        String s3 = "南街";
        System.out.println(s1 == s2); // false
        System.out.println(s2 == s3); // true

在这里插入图片描述
在 JDK 8 之后,取消了永久代的概念,取而代之的实现是元空间(MetaSpace),原本位于永久代中的字符串常量由永久代转移到堆中。元空间的本质和永久代类似,都是对JVM规范中方法区的实现,它们之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

2、String常用方法

这里就不对方法的使用一一例举了,还是得多动手自己实践呢!

  • indexOf():查询字符串首次出现的下标位置
  • lastIndexOf():查询字符串最后出现的下标位置
  • contains():查询字符串中是否包含另一个字符串
  • toLowerCase():把字符串全部转换成小写
  • toUpperCase():把字符串全部转换成大写
  • length():查询字符串的长度
  • trim():去掉字符串首尾空格
  • replace():替换字符串中的某些字符

还有replaceFirst() 替换匹配到的第一个字符;
这里要注意replace和replaceAll的区别,前者的参数是char和CharSequence,即可以支持字符的替换,也支持字符串的替换(CharSequence即字符串序列的意思,说白了也就是字符串)
replaceAll的参数是regex,即基于规则表达式的替换,比如:可以通过replaceAll("\d", “*”)把一个字符串所有的数字字符都换成星号;

  • split():把字符串分割并返回字符串数组,split也是正则匹配
  • join0: 把字符串数组转为字符串.

== 和 equals 的区别

== 对于基本数据类型来说,是用于比较"值” 是否相等的;但对于引|用类型来说,是用于比较引用地址是否相同的。那么我们就要看equals和它的区别了,我们知道Java里Object类是所有类的父类,equals方法也是Object的方法,源码如下:

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

显而易见,没有重写的equals方法本质上也就是 == ,但是我们知道String类型在比较的时候,老师都教过,用equals,这是为什么呢?就是因为String已经重写equals方法,源码如下:

    public boolean equals(Object anObject) {
    	// 先判断引用
        if (this == anObject) {
            return true;
        }
        // 在判断是不是String类型
        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;
                // char 一个一个对比
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

这里不深究hashCode和equals的关系,下次专门在写

小结

通过问题讲解知识点,看完这本文章,出几道题考考?

  • 为什么String 类型要用final修饰?
  • ==和equals的区别是什么?
  • String和StringBuilder. StringBuffer 有什么区别?
  • String的intern()方法有什么含义?
  • String类型在JVM (Java 虚拟机)中是如何存储的?编译器对String做了哪些优化?
  • String 一些常用操作问题,如问如何分割、合并、替换、删除、截取等等问题
发布了33 篇原创文章 · 获赞 71 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_39809458/article/details/104819649