Java 字符串原理浅析

在平常的javaWeb开发开发中,使用的最多的莫过于字符串对象了,所以对于javaWeb的开发人员来说,字符串是最熟悉的对象了,但从另一个方面来说,字符串对象又是最陌生的,描述这么一种情况,当下有这么一个恰当的流行词汇,最熟悉的陌生人,由于字符串实在是太平常太重要了,所以在一般的开发中,框架为我们做了完整又严密的考虑,有事我们竟不知道字符串是在什么时候,如何转换为其他的我们所需要的数据类型的,而且由于这么个情况,我们甚至不知道,字符串的拼接效率问题以及框架和jvm 为我们做了什么优化,而我也是这么一种情况,既然认识到这个问题,那么就要立即解决它。

首先普及以下基本的字符问题

  1. 字符 ,字节,编码:
    谈到 字符串 那么 字符 就是不得不说的问题 字符 百度百科给出的解释 : 在计算机和电信技术中,一个字符是一个单位的字形、类字形单位或符号的基本信息。 其实说的通俗点就是 字符就是用单引号包括的单个字符 像 a b c … * & 等等,只要是能在你的键盘上看到的键基本上都是(除了功能键)说到了字符,那么我们就要考虑往下一层,字节 ,一个字节为 八位,字节就是一连串的 0101 代码,通过这些字节的代码如何和字符扯上关系呢,这时就出现了编码的问题,举一个简单的例子,我现在就假设 010101203 代表 a 。。。 等等,而具体使用几个字节来表示和如何表示,那么就出现了各种各样的编码方案,所以也就出现了乱码这种令人头疼的问题,归根到底还是解码和编码所使用的不是同一种方案,所以对于这种问题,一定要抓住根本问题去寻求解决之道。,

讲的是字符串为什么又扯到了字符上去了呢。因为在java的内部实现中就是使用的字节数组实现的字符串这么一种方案。
查看java String 字符串类的源码发现

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // 字符数组,用它来实现了字符串对象
     private final char value[];、

     // 字符串的默认哈希码为 0
      private int hash; 

    // 序列号

发现字符串这个类 首先是 公共的 最终的。那么就意味着这个类的信息 到处都可以使用,同时这个类又是一个不可变的类,就是不能被继承 其中我留下了一些有用的内容,private final char value[]; 这么一个 字符数组 就是整个字符串对象的核心,首先这个字符数组也是用 final 修饰了,那么这个字符数组就是不可变的,也就是这个字符串类的实例的内容是不可变的。都说字符串类是不可变的对象,那么在平常中我们怎么没有感觉到字符串对象是不可变的呢例如以下这种字符串对象的赋值我们经常使用。

  @Test
    public void testStr1(){

        String str01 = "小明";
        str01 = "小红";
        System.out.println(str01);
    }

//输出为:
    小红
    Process finished with exit code 0

可能有的小伙伴说不是说字符串是不可变对象吗?这咋又能变了呢, 其实在这里就要分清楚对象的对象的引用的概念了, 一般来说在java中 使用 new 创建一个对象,返回一个对象的引用, 引用一般存储在 栈内存中 ,对象实体存储在 堆内存中, 在这里实际上堆内存中的字符串对象并没有变化,只是指向这个对象的引用指向了“小红” 这么个字符串对象而已,使用下面一段代码更能说明问题。

  @Test
    public void testStr1(){

        String str01 = "小明";
        String  str02 = str01.replace("小明", "小红");
        System.out.println(str01);
    }
//输出结果为:
    小明
    Process finished with exit code 0

由此可知,实际上 第一次创建的 字符串对象是没有变化的,上面的那种看起来 字符串对象好像是可以变化的是由于 引用指向的对象已经 不是原来的对象了。

java 把字符串做成不可变的对象有好处有坏处,总体来说好处要多于坏处,如果我们了解一点 关于把字符串做成不可变的对象需要避免的问题和可以优化的问题,那么就可以写出比较高性能的程序了,

首先,要介绍一点关于字符串对象的特殊内存机制问题,由于字符串是不可变的,所以也就和常量是一个概念的,所以java 为了复用字符串对象就 出现了 字符串常量池这么个概念,平常我们使用的 创建一个 字符串对象 一般有这么两种方法,

String str03 = "你好";
 ②String str04 = new String("你好");

平常我们可能也就这么来写了,就知道这样就创建了一个java的字符床对象,却不知道其中的差异,要知道 存在即是合理性,一般如果有两种以上的方法去完成一个功能,那么通常情况下,这些都是存在着差异性的,对于java的字符串对象更是如此,
对于直接使用 双引号 的方式(方式①)进行字符串对象的创建, java 首先会去字符串常量池去寻找有无和 这个字符串常量字面值相等的,如果有的话就直接把 与该字符串常量池中的字符串量关联的 堆内存中 对象引用地址返回,也就是使改引用指向已存在的对象,如果采用 方式② 的话, 不管字符串常量池中是否存在与改对象字面值相同的对象,直接在 堆内存中 创建一个对象,然后去查看 字符串常量池中是否有与改对象 字面值相同的,如果有的话,就把该对象 与 堆中的对象关联起来,
,有人可能会想了,关联起来干啥呢,这不是多余吗,其实这是为了是字符串比较得时候更方便,这样字符串在比较的时候就不用一个一个的比较字符串的字符数组中的字符了,直接比较字符串对象在 字符串常量池中是不是关联着同一个对象就 OK 了,方便了字符串的比较操作。
知道了这些概念下面的这些输出也就很正常了

  @Test
    public void testStr1(){

        //在字符串常量池中寻找,没有,就创建了两个对象,一个在堆中,一个在字符串常量池中,并将其关联起来
        String str01 = "小明";
        // 先在堆中创建一个对象,然后去常量池中寻找,找到,就将其与常量池中对象关联起来
        String  str02 = new String("小明");
        //先去字符串常量池中寻找,发现有相同的,直接就返回与其关联的堆对象的引用地址
        String str03 = "小明";
        System.out.println(str01 == str02);
        System.out.println(str01 == str03);
        System.out.println(str02 == str03);
    }

/**false
    true
    false */

对与字符串拼接需要注意的问题

由于字符串 String 是不可变对象,所以如果是多个字符串拼接的话,最好不要使用 它,那么我们平时经常使用的 重载符号 + 是如何实现的呢, 写一个测试程序,使用javaP 命令反编译 .class 文件 可以发现,其实 java 编译器给我们做了优化,它把像

 str01 = "你好"+"小明";
// 实际上执行的是
  str01 = new StringBuilder().append("你好").append("小明").toString();

它竟然把我们的字符串拼接转变成了 StringBuffer 的拼接,这样做如果是大量字符串拼接的话就没有问题了吗?我在这里贴出有关这个问题的源码加上注释

StringBuilder 对象的
 @Override
    public StringBuilder append(String str) {
    //直接调用父类的 连接方法
        super.append(str);
        return this;
    }

//其父类的 AbstractStringBuilder
public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
       //确定容量 值 
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);

// 扩充容量,每次为 当前数组长度的二倍加上 2 
void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        // Arrays 的数组赋值,底层实际上调用的是 System.arrayCopy(); 这是一个 native 方法,底层直接操作内存块,速度较快
        value = Arrays.copyOf(value, newCapacity);
    }

看过了源码就可以知道,在大量拼接字符串的前期,由于字符串的长度较小,所以就会发生频繁的扩充容量以及多次的复制问题,而到了后期,就会发生容量扩充过大的问题,造成浪费,所以对于字符串的拼接问题,最好能确定字符串拼接后的长度,这样就可以减少很多的扩容以及重复复制字符数组的问题,从而是性能大幅度提升。

最后关于字符串还有一点就是 stringBuffer 的问题, 注意 StringBuffer 没有重写 equals 方法,其父类也没有重写,所以其使用的仍然是 Object 对象的 equals() 方法,所以在进行内容比较得时候最好将其转化为 String 对象,绕过这些坑

另外 StringBuffer 具有线程安全问题,StringBuilder 没有实现线程安全问题,适合在单线程情况下使用,效率,速度较高

还有一点问题,就是不要在 对象中的 toString() 方法中去调用 this ,这样会引起 toString 的递归调用。并且没有出口

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值