从零开始的事多鸽的第一篇博客

程序人生:那个准点下班的人,比我先升职了...

  返回  

String类、可变字符序列

2021/8/20 19:32:28 浏览:

一、java.lang.String类
1、String类和字符串对象的特点
(1)它在java.lang包,使用它不需要导包
(2)它是final修饰的类,不能被继承
    面试题?问,能继承String类吗?
(3)它是支持比较大小的,因为它实现Comparable接口(自然排序)
    两个字符串比较大小,  字符串1.compareTo(字符串2)  结果是>0,<0,=0,分别表示字符串1大于,小于,等于字符串2

    补充:字符串不仅支持Comparable接口(自然排序),还有一个类实现Comparator接口(定制排序)
        这个类是 java.text.Collator,它实现Comparator接口,有 compare方法,用于比较两个字符串大小。
        Collator 类执行区分语言环境的 String 比较。
        它是个抽象类,可以调用静态方法static Collator getInstance(Locale desiredLocale)  获取Collator 的子类对象
(4)字符串是代表一个字符序列,即一串字符
这串字符的长度可以是0~n。
例如:""  空字符串,字符串的长度为0
     "a" 或 " "  一个字符的字符串,长度为1,但是他们不同于 'a' 和 ' ',后面这个是char类型,是基本数据类型。
    "hello" 一个多字符的字符串,长度为5

    char类型的单引号中间 有且只有一个字符,
    之前有同学写'12',这样报错,因为12是两个字符,从数字角度是一个数字值,但是从字符的角度是2个字符。

(5)字符串底层(内部)是使用char[]进行保存的。
   即"hello" --> 底层 --> {'h','e','l','l','o'}

(6)String类型的对象是不可变
    什么是对象不可变?
        一个字符串对象一旦创建好了,它的字符串对象的内容和长度就不能修改了。
        如果对某个字符串对象进行修改,例如:拼接,替换等,都会产生新对象,即不是在原来的字符串对象中直接修改的。
    为什么不可变?
        A:在String类中用private final char value[];来存储字符串内容
           private:私有的,外面是无法直接访问/操作value数组的,即外面无法直接得到value数组的首地址,然后修改元素
           final:不能对value变量重新赋值,即不能让value指向新的数组,这就导致我们无法通过扩容,复制等方式来修改字符串的长度
       B:String类中所有的方法(包括concat,replace等),对字符串的修改都不是直接对当前字符串的value数组进行修改的,而是重新new了一个对象。
    为什么要设计成不可变?
        字符串对象是Java程序中最最常见的一种对象,即程序中会大量出现字符串对象。
        那么如果字符串对象不可变的话,就可以“共享”字符串对象。
        例如:"hello"无论在程序中出现几次,我们使用的是同一个字符串对象,可以节省大量的内存。
    这些共享对象存在哪里?
        字符串常量池中。

    字符串常量池在哪里?
        因为JVM的品牌不同,JDK版本不同,那么字符串常量池都会有所不同。
        Oracle的hotspot的JVM,在JDK1.6时,字符串常量池在方法区,
                             在JDK1.7时,字符串常量池在堆,
                             在JDK1.8时,字符串常量池在元空间,
(7)在JDK1.9时,String内部的value数组,从char[]类型,变为byte[]。
    为什么?

    因为Java中一个char是占2个字节。而Java程序中大多数的字符串都是由ASCII码表中的字符构成的,
    这些字符本身的存储只需要1个字节就够了,那么分配2个字节就有点浪费。
    所以它修改为byte[],如果是1个字节就能存的,就用一个字节,一个字节不够的,用2个字节。
    可以节省内存。

 (8)字符串对象的hash值,有一个hash属性存储。
 private int hash;
 因为字符串对象不可变,所以直接存在hash值,下次获取时,就不用现计算。
 如果没有存,就意味着,每次用的时候,需要调用hashCode函数,根据算法现计算。
 如果存了hash,每次用的时候,调用hashCode函数,直接返回hash中存的值。

public class TestString {

    @Test
    public void test06() {
        String str = "hello";   //str中存储的是"hello"对象的首地址
        str = str + "world";//把新对象的首地址重新赋值给str变量,现在str中存储的是"helloworld"对象的首地址
        //这里修改的不是"hello"对象,而是str变量中存储的首地址
        System.out.println(str);
    }

    @Test
    public void test05() {
        String str1 = "hello";
        String str2 = "hello";
        System.out.println(str1 == str2);//true
    }

    @Test
    public void test04(){
        String str = "hello";
        System.out.println(str + "world");//helloworld  这里打印的是新字符串对象
        System.out.println(str);//hello
    }

    @Test
    public void test03(){
        String str = "hello";
        str.concat("world");//拼接  拼接后产生新的字符串对象,如果没有接收,就丢了
        System.out.println(str); //hello
    }

    @Test
    public void test02(){
        String[] arr = {"张三","李四","王五","赵六","柴林燕"};

        Arrays.sort(arr); //按照元素类型的自然排序规则进行排序的,即按照String类型实现Comparator接口的compareTo方法排序
        System.out.println(Arrays.toString(arr));//[张三, 李四, 柴林燕, 王五, 赵六]

        Arrays.sort(arr,  Collator.getInstance(Locale.CHINA));
        System.out.println(Arrays.toString(arr));//[柴林燕, 李四, 王五, 张三, 赵六]
    }

    @Test
    public void test01(){
        // java.text.Collator,它实现Comparator接口,有 compare方法,用于比较两个字符串大小。
        String s1 = "张三";
        String s2 = "李四";

        //自然排序比较大小,Comparable接口的compareTo方法
        System.out.println(s1.compareTo(s2));//-2094   按照字符串的Unicode编码值比较大小
        if(s1.compareTo(s2) < 0){
            System.out.println(s1 + " < " + s2);//张三 < 李四
        }else if(s1.compareTo(s2) > 0){
            System.out.println(s1 + " > " + s2);
        }else{
            System.out.println(s1 + " = " + s2);
        }

        //定制排序比较大小,Comparator接口的compare方法
        Collator collator = Collator.getInstance(Locale.CHINA);
        System.out.println(collator.compare(s1,s2));//1     区分语言环境的 String 比较  在中文中“张”在“李”后面
        if(collator.compare(s1,s2) < 0){
            System.out.println(s1 + " < " + s2);
        }else if(collator.compare(s1,s2) > 0){
            System.out.println(s1 + " > " + s2);//张三 > 李四
        }else{
            System.out.println(s1 + " = " + s2);
        }
    }
}

二、字符串的兄弟:可变字符序列
1、String的很大一个特点:字符串对象不可变。
优点:字符串对象不可变,字符串常量对象就可以共享,节省一些内存,并且节省很多时间。
        例如:程序中出现“hello"字符串常量,都是同一个,对象个数减少了,节省一些内存,不需要重修new对象,就可以节省时间和内存。
        在字符串常量池中查找字符串,很方便,hash结构。
缺点:当遇到字符串的拼接等修改操作时,每次都会产生新对象,这就又浪费内存
      特别是大量字符串的拼接操作,频繁的拼接操作,就很耗内存。
      即使是现在Java编译已经对  大量的字符串拼接操作做了优化,优化为StringBuilder的方式,也仍然有很多“中间”的垃圾对象产生。

2、Java针对上面的缺点,设计了两个新的字符串兄弟类型,它们都是可变字符序列。
java.lang.StringBuffer(JDK1.0就有,伴随着String诞生的)
java.lang.StringBuilder(JDK1.5才有的)

3、StringBuffer和StringBuilder的区别:
StringBuffer:线程安全的可变字符序列。
StringBuilder:此类提供一个与 StringBuffer 兼容的 API,但不保证同步。(同步是解决线程安全问题的一个方案)
            换句话说StringBuilder是线程不安全的。

            该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。

            什么是线程安全?
            比喻:  杨浩波 和  蔚春晓住一个屋,就一个“卫生间”。
                  早上的时候起床时间差不多,都很着急。
                  杨浩波先进入卫生间,动作有点慢,在5分钟之内,没搞定他的事情。
                  此时,蔚春晓就进入了,看到一些“尴尬”的场景,这个就是线程安全问题。

                  两个线程在使用“共享数据”时,就会出现,一个线程还没有完全使用完数据,另一个线程插入进来,
                  对数据的访问或修改扥结果可能就不正确。

StringBuffer和StringBuilder的API完全“一样” 。

4、API,以StringBuffer为例,关于StringBuilder的话就是把StringBuffer换成StringBuilder即可。

(1)构造器:StringBuffer和StringBuilder的对象创建必须通过构造器,不能像字符串一样,直接""赋值
(2)StringBuffer append(各种数据类型 b):追加
(3)StringBuffer delete(int start, int end):删除[start, end)位置的字符
  StringBuffer deleteCharAt(int index):删除[index]位置的字符
(4)StringBuffer insert(int offset, 各种数据类型 c):在[offset]位置插入xx
(5) StringBuffer reverse() :字符串的反转
(6)StringBuffer replace(int start, int end, String str):替换[start,end)位置的字符为str
    void setCharAt(int index, char ch) :替换[index]位置的字符为ch指定字符

    以下方法和String差不多
(7)int length()
(8)char charAt(int index)
(9)int indexOf(String str)
(10)int lastIndexOf(String str)
(11) String substring(int start)
   String substring(int start, int end)
 (12)String toString()

public class TestStringBuffer {
    @Test
    public void test07() {
        StringBuffer buffer = new StringBuffer("hello");
        buffer.append("java").append("atguigu");

        //转为字符串
        String str = buffer.toString();
        System.out.println(str);
    }

    @Test
    public void test06() {
        StringBuffer buffer = new StringBuffer("hello");
        buffer.reverse();
        System.out.println(buffer);//olleh
    }

    @Test
    public void test05() {
        StringBuffer buffer = new StringBuffer("hello");
        buffer.insert(2,"java").insert(0,"atguigu");
        System.out.println(buffer);//atguiguhejavallo
    }
    @Test
    public void test04(){
        StringBuffer buffer = new StringBuffer("hellojavaworld");
        buffer.deleteCharAt(0).delete(3,7);
        System.out.println(buffer);//ellaworld
    }

    @Test
    public void test03(){
        StringBuffer buffer = new StringBuffer("hello");
        buffer.append(1)
                .append('a')
                .append(true); //支持连写,因为append的返回值还是StringBuffer类型,而且还是buffer对象自己
        System.out.println(buffer);//hello1atrue
    }

    @Test
    public void test02(){
        StringBuffer buffer = new StringBuffer("hello");
        buffer.append(1);
        buffer.append('a');
        buffer.append(true);
        System.out.println(buffer);//hello1atrue
    }
    @Test
    public void test01(){
      //  StringBuffer buffer = "hello";//错误, 左边是StringBuffer类型,右边是String类型,它们是兄弟关系,是不能互相赋值的。
    }
}

问?如何实现可变字符序列?
(1)StringBuffer和StringBuilder底层也是用char[] value存储字符串内容的。
这个char[] value没有用final修饰,意味着这个char[]可以扩容,缩容等。

(2)如何扩容?
哪些方法和扩容有关?
append,insert

A:append:分析
首先,StringBuffer s = new StringBuffer();  发现new char[16];
默认char[] value数组,是长度为16.
其次,在append方法中,
如果value数组够装,就直接在原value数组中拼接,
如果value数组不够存拼接后的字符串,就先对value进行扩容,扩容为原来的2倍+2,然后在新的value数组中拼接字符串。


B:insert:分析
首先,看是否需要扩容,如果要扩容,就扩容为原来的2倍+2
然后,把插入位置[index]之后的字符往后移动,移动的位数,是和你新插入的字符串的长度。
最后,把插入的字符串放进去。

(3)删除字符
deleteCharAt,只是把被删除位置[index]后面的元素往前移动了,覆盖了删除位置[index]的字符,数组的长度没变。

(4)缩容
void trimToSize():复制了一个count长度的数组

重点调用的API:
Arrays.copyOf():用于复制数组,新数组可能是更大/更小的数组
System.arraycopy():用于移动元素

public class TestStringBuffer2 {
    @Test
    public void test04(){
        StringBuffer s = new StringBuffer("hello");
        s.deleteCharAt(2);
        s.trimToSize();

    }


    @Test
    public void test03(){
        StringBuffer s = new StringBuffer("hello");
        s.deleteCharAt(2);

    }


    @Test
    public void test02(){
        StringBuffer s = new StringBuffer("hello");
        s.insert(2,"java");

    }

    @Test
    public void test01(){
        StringBuffer s = new StringBuffer();
        s.append("hello").append("world").append("atguigu").append("java");

    }
}

三者对比

public class TestTime {

    @Test
    public void testString(){
        long start = System.currentTimeMillis();
        String s = new String("0");
        for(int i=1;i<=10000;i++){
            s += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("String拼接+用时:"+(end-start));//367

        long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println("String拼接+memory占用内存: " + memory);//473081920字节
    }

    @Test
    public void testStringBuilder(){
        long start = System.currentTimeMillis();
        StringBuilder s = new StringBuilder("0");
        for(int i=1;i<=10000;i++){
            s.append(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("StringBuilder拼接+用时:"+(end-start));//5
        long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println("StringBuilder拼接+memory占用内存: " + memory);//13435032
    }

    @Test
    public void testStringBuffer(){
        long start = System.currentTimeMillis();
        StringBuffer s = new StringBuffer("0");
        for(int i=1;i<=10000;i++){
            s.append(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("StringBuffer拼接+用时:"+(end-start));//5
        long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println("StringBuffer拼接+memory占用内存: " + memory);//13435032
    }
}

联系我们

如果您对我们的服务有兴趣,请及时和我们联系!

服务热线:18288888888
座机:18288888888
传真:
邮箱:888888@qq.com
地址:郑州市文化路红专路93号