Java中的明星类介绍-“String类“

导语:在写这篇文章时我是惴惴不安的 因为String的地位在Java中实在是太高了 Java面试也是必问类型之一 。首先是怕自己理解的不够透彻对读者产生一些误解 其次是关于String的东西实在是太多我想10篇文章也不一定说的完 那基于这一点 我想我还是挑重点的东西讲 关于String的API解读这边就不准备讲了,尽我最大努力帮助读者理解String本质的东西吧 语言总是晦涩难懂的【不管是编程语言还是人类发明的自然语言 】 我更希望和喜欢透过现象看本质

Ⅰ.String类的数据结构

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

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

    /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = "".value;
    }
    ....省略后面相关代码

从以上的JDK1.8 String类的数据结构我们不难可以得出以下结论
1.String类是final类 不可以被继承
2.String类的数据保存在一个char数组中 所以本质上字符串类String 是一个字符数组 而且char数组被final修饰 所以String类一旦创建就不可更改
3.String类可以被序列化(注意并不是说它实现了Serializable接口所以可以被序列化 Java在JVM层面对大多数对象提供了序列化支持 接口是对行为的抽象 所以实现序列化接口本质上可以理解为开启序列化能力 换句话说没有实现序列化接口的类不允许被序列化尽管它们可能可以序列化 很绕 。。。)
4.由于String的不可变性我们可以得出String在多线程环境下是天生线程安全的 因为不可变所以就不存在变量被修改的可能 也就不存在多线程下对String操作导致变量值不同步问题
5.String不可变?真的就不可变吗?后面会进行讲解

特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。这一点我们可以通过阅读源码得到验证
比如非常经典的求子字符串方法:

/**
     * Returns a string that is a substring of this string. The
     * substring begins with the character at the specified index and
     * extends to the end of this string. <p>
     * Examples:
     * <blockquote><pre>
     * "unhappy".substring(2) returns "happy"
     * "Harbison".substring(3) returns "bison"
     * "emptiness".substring(9) returns "" (an empty string)
     * </pre></blockquote>
     *
     * @param      beginIndex   the beginning index, inclusive.
     * @return     the specified substring.
     * @exception  IndexOutOfBoundsException  if
     *             {@code beginIndex} is negative or larger than the
     *             length of this {@code String} object.
     */
    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);
    }

如果你需要一个可修改的字符串,应该使用StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的String对象被创建出来

Ⅱ.String类的特性

1.对 + 的运算符“重载”
特别注意:我们这里所说的运算符重载并非真正意义上的运算符“重载” 因为Java本身不存在运算符重载 之所以这么讲就是为了让大家更好的理解 因为C++中有运算符的重载这么一说 Java这种特性应该被称为Java的一种语法糖(不清楚的大家可以去某度或者Google了解什么是Java语法糖 这边跟本文无太大相关性不展开来讲)
String s1=“Tao” String s2=“Bao” String s3=s1+s2 你知道它们底层是怎么实现的吗?其实是由StringBuilder 的append方法实现的 为什么这么说 我们可以通过反编译来验证

这是我们写的Java文件

public class StringTest {

    public static void main(String[] args) {
        String s1="Tao" ;
        String s2="Bao" ;
        String s3=s1+"Bao";
        System.out.println(s3);
    }
}

通过查看我们写的Java文件找到它的class文件并反编译为Java文件
在这里插入图片描述
在这里插入图片描述
我们可以清楚的通过反编译软件看到 本质上 字符串变量与字符串常量相加是通过StringBuilder的append方法实现的

如果是常量与常量相加呢?

public class StringTest {

    public static void main(String[] args) {
        String s1="Tao" ;
        String s2="Bao" ;
        String s3="Tao"+"Bao";
        System.out.println(s3);
    }
}

在这里插入图片描述
结论 :这个现象启示我们最好不要在循环中对字符串进行+ 拼接 因为每一次拼接都会创建一个新的字符串对象 常量与常量为什么不会出现这个问题呢?原因很简单 因为字符串中存在常量池技术 字符串常量在编译期已知 所以两个常量相加不会出现创建新对象的过程

Ⅲ.字符串常量池技术

池化技术对于我们来说并不陌生 池化技术本质上就是为了对象复用 例如数据库连接池技术 线程池技术 Http连接池技术等 为什么要复用这些对象呢? 因为创建这些对象的成本很昂贵 我们不希望虚拟机把大部分时间都花费在对象创建上 而且我们的内存也不是无限大的 字符串池化技术的出现与String的特性息息相关 我们都知道String是不可变的终类 每一次对尝试对字符串修改都需要重新创建对象 所以SUN开发团队对String做了许多优化,常量池就是其中之一 引入字符串常量池之后我们创建String的时候代价就没有那么昂贵了 因为我们可以从池中获取可复用对象,间接实现了对象的复用 Effective Java一书(Java四大名著之一)中指出 当一个对象可以被复用时我们应该优先考虑对象复用 当对象不可被复用时我们不能强行复用对象 这句话看似矛盾其实却道尽了对象复用的原则和规律 因为强行复用对象可能对Java虚拟机来说并不都是好事 如果对象是不可变的( immutable ) ,它就始终可以被重用 显然String不可变,复用是很有必要的

String str=“Tao” 对于JVM来说 它首先会去常量池中查看是否已存在该对象 如果已存在直接返回字符串"Tao"在常量池中的地址 否则在常量池中创建该字符串对象并返回引用地址

由常量池技术所衍生的一系列面试题

举例举例------

public class StringTest {

        public static void main(String[] args) {

            String s0=new String("TaoBao");

            String s1=new String ("Tao");

            String s2="Tao" ;
            String s3="Bao" ;
            String s4="TaoBao";
            String s5="Tao"+"Bao";
            String s6=s2+"Bao";

            System.out.println(s0==s4);
            System.out.println(s0==s5);
            System.out.println(s0==s6);
            System.out.println(s1.concat("Bao")==s4);

            System.out.println(s0.intern()==s4);

            System.out.println(s4==s5);



        }
}

先别看答案 看看自己能做对几个 然后我一一解析为什么会返回那个结果

首先我们需要明确一点 String是类。用==比较的是他们的引用(即内存地址(注意可能不是直接内存地址而是句柄地址但是不影响我们理解))

1.so 与s4比较 这个我想大家都知道答案 false s0是new 出来的 所以他是在 Java堆里面进行创建的 而S4我们刚刚进行了很多详细描述 他是在常量池中创建的 两个人的内存区域都不一样 返回的内存地址肯定不同 所以引用值是不可能相等的
2.第二题其实与第一题一样 因为s4与s5其实是一模一样的 都是在常量池中进行创建并返回地址 所以妥妥的false
3.我们需要明确一点 所有new出来的对象他们的地址是不会相等的 那么基于这一点 我们再来看第三题就很简单了 我们上面分析过 字符串变量相加本质上会创建一个新的StringBuilder对象 然后调用append方法拼接字符串 一个创建出来的对象引用只会和自己相等 所以不管哪个和他比都是返回false的
4.第四题回归我们最开始讲解的知识点 String类对字符串操作的方法都会创建一个新的对象并返回所以答案很明显 一定是返回false
5.第五题大家如果了解intern方法的作用的话 答案就很容易得到了 intern方法是用来检测字符串常量池中是否存在该字符串 存在就返回该字符地址 所以我们可以理解为 这题中它将本来在堆中创建的字符串引用 转化成了在字符串常量池中来创建 显然这个时候两个对象在常量池中地址相同 所以返回true
6.不解释 如果大家理解了我上面的分析 我想都知道 肯定返回true 因为它们都在常量池中创建 并且内容相等

下面公布答案 看看自己错了几个 为什么错看我上面分析
在这里插入图片描述

Ⅳ.String 真的不可变吗?

请看下面的例子

String str = "Hello Python";

System.out.println(str); // Hello Python

Field field = String.class.getDeclaredField("value");

field.setAccessible(true);

char[] value = (char[])field.get(str);

value[6] = 'J';

value[7] = 'a';

value[8] = 'v';

value[9] = 'a';

value[10] = '!';

value[11] = '!';

System.out.println(str); // Hello Java!!

看到这里你还认为String是不可变吗? 这就是Java反射强大的地方 但是平时编程不推荐这么干 因为毫无意义 我们将String做成不可变的 本身就是为了让他能在多线程环境下不受约束的使用 这破坏了我们软件设计的初衷!
关于String的东西还有很多很多 由于篇幅关系不能一一道来 更多String API介绍敬请关注我!一个不断努力前行的C加加减减工程师 带你解锁更多Java知识

总结:String是天生线程安全的 我们可以放心的在多线程环境下使用它 String这种以空间换取设计复杂度虽然不值得推崇但是却是非常值得称赞的软件设计 我们对String如果操作不当很容易产生许多垃圾对象尽管JVM对此做了很多优化 还是那句老话 没有缺憾的软件设计是不存在的 我们只是在利弊取舍之间选择了那种对我们最有利的罢了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值