Java — String知识点学习

学习资料:

我电脑环境JDk 1.8

看到一篇很有深度的讲解:How many Objects created with: String str=new String("Hello")?

1. String字符串

String不是Java中的基本数据类型

C语言中,字符串的处理通常是使用char数组,但数组本身无法封装字符串操作所需要的方法。在Java中,String对象可以看作是char数组的延伸和进一步封装

String主要由3部分组成:char数组offset偏移conut长度

char数组表示String的内容,String对象所表示字符串的超集。String的真实内容还需要由偏移量和长度在char数组中进行定位和截取


1.1 3个基本特点

  1. 不变性
  2. 针对常量池的优化
  3. 类的final定义

1.1.1 不变性

String对象一旦生成,就不能对再对它进行改变

个人理解:String类中的操作字符串的方法,并不是直接改变当前的String对象,而创建了一个新的String对象

String的这个特性可以泛化成不变(immutable)模式,即一个对象被创建后就不再发生变化。作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间


1.1.2 针对常量池的优化

当两个String对象拥有相同的值时,只引用常量池中的同一个拷贝。当一个字符串反复出现时,可以节省内存空间

String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
System.out.println(str1 == str2);               // true
System.out.println(str1 == str3);               // false
System.out.println(str1 == str3.intern());      // true
String 内存分配方式

str1str2引用了相同的地址,str3重新开辟了一块内存空间

str3在常量池中的位置和str1是一样的,也就是说,虽然str3单独占用了堆空间,但是它所指向的实体和str1完全一样

str3.intern()返回了String对象在常量池中的引用

1.1.3 类的 final 定义

作为final类的String对象在系统中不可能有任何子类,这是对系统安全性的保护

JDK 1.5版本之前的环境中,使用final定义,有助于帮助虚拟机寻找机会,内联所有的final方法,提高系统效率。但在JDK 1.5以后,效果并不明显

注意:

1.7之后,Stringsubstring()方法已经不会再引起内存泄露

可能会引起内存泄露的是String()中的一个私有构造方法,在1.7之后已经修复


1.2 字符串的分割和查找

1.2.1 分割

作者用的JDK版本不知道多少,但应该不是1.8,很可能是1.6,感觉同样的代码测试,时间已经比作者测试时间少了不止一个量级。除了JDK代码迭代升级的优化,还得考虑电脑的差距

结论:一般情况下直接使用split()方法足够,在一些要分割的目标长度很长很长并且需要分割的次数很多很多,split()耗时久时,再考虑使用StringTokenzier,但感觉一个字符串要分割10000次已经有些丧心病狂

测试代码:

测试次数,为了方便观察,跨度有些大,应该有梯度的测试

public class StringL {
    private static final String LINE = "************************************************";

    public static void main(String[] args) {
        test(1000);
        test(10000);
        test(100000);
        test(1000000);
        test(10000000);
    }

    /**
     * 根据字符串的长度完成一轮测试
     */
    private static void test(int num) {
        String str = getHugeString(num);
        System.out.println("字符串长度为 " + str.length() + " ,同一个字符串需要分割 " + num + " 次时:");

        // split 方式
        splitTest(str);

        // StringTokenizer 方式
        StringTokenizerTest(str);

        // 打印分割线
        System.out.println(LINE);
    }

    /**
     * 使用 StringTokenizer 分割字符串
     */
    private static void StringTokenizerTest(String str) {
        StringTokenizer st = new StringTokenizer(str, ";");
        long now = System.currentTimeMillis();
        while (st.hasMoreTokens()) {
            st.nextToken();
        }
        System.out.println("StringTokenizer 用时:" + (System.currentTimeMillis() - now));
    }

    /**
     * 原始的 split 方法分割字符串
     */
    private static void splitTest(String str) {
        long now = System.currentTimeMillis();
        str.split(";");
        System.out.println("split 用时:" + (System.currentTimeMillis() - now));
    }

    /**
     * 获取字符串
     */
    private static String getHugeString(int num) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < num; i++) {
            sb.append(i);
            sb.append(";");
        }
        return sb.toString();
    }
}

输出结果,时间是毫秒:

字符串长度为 3890 ,同一个字符串需要分割 1000 次时:
split 用时:2
StringTokenizer 用时:1
************************************************
字符串长度为 48890 ,同一个字符串需要分割 10000 次时:
split 用时:4
StringTokenizer 用时:4
************************************************
字符串长度为 588890 ,同一个字符串需要分割 100000 次时:
split 用时:28
StringTokenizer 用时:21
************************************************
字符串长度为 6888890 ,同一个字符串需要分割 1000000 次时:
split 用时:503
StringTokenizer 用时:60
************************************************
字符串长度为 78888890 ,同一个字符串需要分割 10000000 次时:
split 用时:3895
StringTokenizer 用时:770
************************************************

多次测试,当对同一个字符串要分割次数达不到一定期限时,有时StringTokenizer的效率还不如split()方法,只有到达一定期限后,StringTokenizer的优势就很明显


1.2.1 查找

StringcharAt(),indexOf()方法非常非常高效

测试代码:

分别使用startWith()endWith()比较100w

public class CharAtTest {
    public static void main(String[] args) {
        String str = "abc123456xyz";
        int num = 1000000;
        String startTag = "abc";
        String endTag = "xyz";


        // startWith 方法
        long now = System.currentTimeMillis();
        for (int i = 0; i < num; i++) {
            boolean b = str.startsWith(startTag);
        }
        System.out.println("startWith()方法耗时 : " + (System.currentTimeMillis() - now));

        // myStartWith() 方法
        now = System.currentTimeMillis();
        for (int i = 0; i < num; i++) {
            myStartWith(str);
        }
        System.out.println("myStartWith()方法耗时 : " + (System.currentTimeMillis() - now));

        // endWith方法
        now = System.currentTimeMillis();
        for (int i = 0; i < num; i++) {
            boolean b = str.endsWith(endTag);
        }
        System.out.println("endsWith()方法耗时 : " + (System.currentTimeMillis() - now));

        // myEndWith() 方法
        int length = str.length();
        now = System.currentTimeMillis();
        for (int i = 0; i < num; i++) {
            myEndWith(str, length);
        }
        System.out.println("myEndWith()方法耗时 : " + (System.currentTimeMillis() - now));
    }

    private static void myEndWith(String str, int length) {
        boolean b = str.charAt(length - 3) == 'a' && str.charAt(length - 2) == 'b' && str.charAt(length - 1) == 'c';
    }

    private static void myStartWith(String str) {
        boolean b = str.charAt(0) == 'a' && str.charAt(1) == 'b' && str.charAt(2) == 'c';
    }

}

输出结果:

startWith()方法耗时 : 15
myStartWith()方法耗时 : 6
endsWith()方法耗时 : 13
myEndWith()方法耗时 : 6

结论:在极为敏感的系统中,必要的时候可以考虑使用charAt()方法来代替startWith()或者endWith()。但几乎大部分情况下,直接startWith()或者endWith()足够


2. StringBuilder 和 StringBuffer

由于String对象是不可变对象,在需要对字符串进行修改操作时,如连接、替换,总是会生成新的对象,导致在某些时候性能比较差,JDK专门提供了创建和修改字符串的工具类StringBufferStringBuilder


2.1 String 常量的累加操作

关于String s = "a" + "b" + "c"几个对象: 一个

String s = "a" + "b" + "c"这种静态字符串的连接操作,Java在编译时期将多个连接操作的字符串在编译时合成一个单独的长字符串abc

对于常量字符串的累加,Java在编译时期就做了充分优化,在编译时期便能确定取值的字符串操作,在编译时期进行计算,在运行时,并不会生成大量的String实例对象


2.2 String 变量的累加操作

测试代码:

public class StringCreate {
    public static void main(String[] args) {
        commonConcat();
        stringBuilderConcat();
    }

    private static void stringBuilderConcat() {
        long now = System.currentTimeMillis();
        for (int i = 0 ; i < 50000; i ++){
            StringBuilder sb = new StringBuilder();
            String str1 = "abc";
            String str2 = "897";
            String str3 = "xyz";
            String str4 = "123";
            String result = sb.append(str1).append(str2).append(str3).append(str4).toString();
        }
        System.out.println("使用 StringBuilder 用时:" + (System.currentTimeMillis() - now));
    }

    /**
     * 直接拼接
     */
    private static void commonConcat() {
        long now = System.currentTimeMillis();
        for (int i = 0; i < 50000; i++) {
            String str1 = "abc";
            String str2 = "897";
            String str3 = "xyz";
            String str4 = "123";
            String result = str1 + str2 + str3 + str4;
        }
        System.out.println("直接拼接用时:" + (System.currentTimeMillis() - now));
    }
}

结果

直接拼接用时:27
使用 StringBuilder 用时:15

平均耗时差距并不大

原因在于,对于字符串变量的累加,Java也做了优化,使用了StringBuidler对象来实现字符串的累加

直接拼接的代码中,for()内字符串部分反编译之后:

反编译,我并没有做,而是直接将反编译的代码照着书上的形式,抄了下来

String str1 = "abc";
String str2 = "897";
String str3 = "xyz";
String str4 = "123";
String result = (new StringBuilder(String.valueOf(str1))).append(str2).append(str3).append(str4).toString();

在构建超大的String对象时,优先考虑:显示使用StringBuilder 或者 StringBuffer


2.3 StringBuilder 和 StringBuffer

两者都继承之AbstractStringBuiler,拥有几乎相同的对外接口

两者最大的区别: StringBuffer对几乎所有的方法做了同步

也就是说选择需要根据使用的具体场景来确定,考虑线程安全时,多线程环境就选择StringBuffer

在两者的构造方法中,都有一个方法提供了设置容量参数,如果预先能确定容量,指定容量大小,也可以再次对性能进行提升

默认情况下,不指定容量时,是16字节,之后需要的容量超过实际char数组长度时,就会进行扩容,int newCapacity = (value.length << 1) + 2,扩容的细节,暂时不考虑

若不指定容量,扩容又基本始终是2倍大小,就会造成一些空间浪费


3. 最后

有错误,请指出

共勉 : )

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容