当前位置:首页 » 《随便一记》 » 正文

初识Java语言(七)- String、StringBuilder和 StringBuffer三者之间的区别和联系!!!【建议收藏】_x0919的博客

29 人参与  2021年12月26日 14:16  分类 : 《随便一记》  评论

点击全文阅读


当我们学习了Java中的继承和多态后,现在我们就可以来学习一个非常重要的东西:String字符串,以及还有StringBuilder和StringBuffer两兄弟。我们直接发车了!!!

在这里插入图片描述

前期文章

前言- IDEA如何配置?让你敲代码更轻松!

初识Java语言(一)- 基本数据类型及运算符

初识Java语言(二)- 方法以及递归

初识Java语言(三)- 数组

初识Java语言(四)-类和对象

初识Java语言(五)- 包和继承

初识Java语言(六)-多态、抽象类以及接口

文章目录

  • 一、String
    • String类的常用方法
      • 字符串比较
      • 字符串替换
      • 字符串查找
      • 字符串截取
      • 其他方法
  • 二、StringBuilder与StringBuffer

一、String

常见构造字符串的方式:

  1. 声明String类型的变量,后面直接初始化

    String str = "hello world";
    
  2. 还有一种就是new一个String类型的对象

    String str = new String("hello world"); //将字符串放入括号里
    

以上两种是最为常见的字符串的构造方式,当然还有另外几种,我们来看一下帮助手册的!!!

image-20210919151801388

3、 String(byte[] bytes), 这个构造方法,将一个字节数组转换为字符串

byte[] bytes = {'a', 'b', 'c', 'd'};
String str = new String(bytes); //这样就能够得到“abcd”字符串

4、 String(byte[] bytes, Charset charset),这个构造方法,会将字节数组,按照charset的编码方式,进行编码

byte[] bytes = {'a', 'b', 'c', 'd'};
String str = new String(bytes, "utf-8"); //以utf-8的编码方式,进行编码

5、String(byte[] bytes, int offset, int length),根据偏移量处开始进行转换,转换length个字节的数据

byte[] bytes =  {'a', 'b', 'c', 'd', 'e'};
String str = new String(bytes, 1, 3); //从偏移量为1的字符开始,一直转换3个。即就是“bcd”
//切记,输入的偏移量和长度,要在bytes数组的范围内,不然会报异常

上面的五种构造方法,就是在平时比较常见的,也比较简单。接下来,将来说一说,String字符串,在内存中,是如何进行存储的,以及字符串是如何进行判断相不相等的。

  1. String str1 = "hello";
    String str2 = "hello";
    System.out.println(str1 == str2); //true 还是false?
    

    答案毋庸置疑,是true。 那到底是为何相等呢?我们来看内存的情况:

    image-20210919154201972

    在堆中,还有字符串常量池的概念,但是在具体的内存划分时,是没有这个常量池的,这个常量池,是用哈希表写的。

    那到底什么是字符串常量池???

    说的简单一点,这一块区域,就是专门用于存储常量字符串的,每次新建一个字符串时,会在字符串常量池查询,看池中是否已经有了相同的字符串,如果已经有了,那么JVM就会将已经有的字符串的地址进行返回,不会再次在池中放入一模一样的字符串。这样做的目的就是节省空间。

    就像上图所示,当str1在新建字符串时,“hello”,在池中没有,那么就放入这个字符串,并将地址赋值给str1,接下来在str2时,发现池中有一个一模一样的字符串,那么就直接将池中的字符串的地址进行返回。所以str1和str2两个字符串是指向同一块内存空间的。所以就是true。

  2. String str1 = "hello";
    String str2 = new String("hello");
    System.out.println(str1 == str2); //true还是false?
    

    str1这样的字符串,叫字符串字面值常量。str2呢,是new了一个对象,既然是new的,肯定是在堆上开辟了一块内存空间的。具体的看下图:

    image-20210919155917122

    如上图所示,str2,会在堆中先new一个String类,然后这个String类的对象里面有一个value的成员变量,用来存储“hello”的地址,而这个字符串呢,最终是会存储在常量池的,然而此时的常量池是有“hello”的,所以value变量,指向的就是已经存在的“hello”字符串。

    由图可知,str1直接指向的“hello”字符串,str2直接指向的是一个String类型的对象,直接进行判断地址,肯定也就是false了。

  3. String str1 = "hello";
    String str2 = "hel" + "lo";
    System.out.println(str1 == str2); //true还是false
    

    在这里,我们需要知道,此时1、2行的3个字符串,都是叫字符串字面值常量,在Java中,常量是会在编译的时候,就会直接计算完成,也就是说编译完成后,str2的值就是“hello”,然后在运行时,再去进行分配空间时,就会回到上面我们第一个问题那里,即就是str1和str2都是指向同一块内存空间的。所以最后的答案就是true。

  4. String str1 = "hello";
    String str2 = new String("hel") + "lo";
    System.out.println(str1 == str2); //true还是false?
    

    此时这个问题,和上面的问题3很相似,答案肯定是false。

    此时JVM编译完成后,str2的值还是没有变的,因为等号右边有一个变量(new String()),此时编译器在编译的时候,并不知道这个变量里面存储的是什么内容,只能在代码执行到这一步的时候,才知道这个内容是什么。如下图:

    image-20210919162514244

    如图,str1还是指向常量池的字符串,而str2是由另外的一个String类的对象加上一个“lo”,所以会在堆上开辟另外一块内存空间,存储这个相加的结果,即就是str2指向的是一个String类的对象。所以答案就是false。

  5. String str1 = "hello";
    String str2 = new String("hel") + new String("lo");
    System.out.println(str1 == str2); // true还是false
    

    答案很显然是false。

    image-20210919165614038

    很显然,str1指向常量池的字符串,str2指向的是堆上的String类的对象。二者的内存地址并不相等。

  6. String s3 = new String("1") + new String("1");
    s3.intern(); //手动的,将字符串放入字符串常量池
    String s4 = "11";
    System.out.println(s3 == s4); //true还是false
    

    第一行的代码,s3肯定是指向堆上的String类的对象的,即就是说此时s3的值是在堆上的字符串“11”。然后执行第2行的代码,手动的将堆上的“11”放入字符串常量池,此时就分为两种情况讨论:1、此时的常量池并没有“11”这个字符串,那么就会将堆上的“11”的地址,放到字符串常量池(JDK1.7之后);2、此时的常量池已经有了“11”这个字符串,那么就不会再将“11”手动放入常量池了,说简单点就是啥事也不干。

    执行到第3行代码时,此时常量池中,是有“11”这个字符串的,所以就无需再放入进去,拿已经存在的字符串的地址即可。如下图:

    image-20210919165344499

    在JDK1.6时,intern方法,是会在字符串常量池直接新建一个字符串存入进去,而在JDK1.7之后,就没有新建字符串了,而是直接将堆上的字符串的地址放入常量池即可

    所以此题所后输出的就是true。

  7. String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4); //true还是false
    

    这道题就和上面这道题很相似了,只是intern方法的先后顺序不一样而已。当执行到第3行代码的时候,字符串常量池中已经有了s4变量所指向的“11”字符串,此时s3指向的字符串还是在堆上的,没在常量池里面,现在才去调用intern方法,常量池已经有了“11”字符串了,所以不用再放入进去了。此时s3还是指向堆上的String类的对象,s4还是指向常量池的字符串。所以二者的内存地址并不相等,也就是false了。

  8. String str1 = "hello";
    String str2 = str1;
    
    //此时修改str2的值
    str2 = "world";
    System,out.println(str1);
    System.out.println(str2); 
    

    当我们修改str2的值后,str1的值会发生改变吗???

    答案肯定是不会的。这跟C语言的指针不一样,指针的话,我可以通过地址去改变内存里面的值。在Java中的引用,是做不到的。这里只是重新建了一个字符串“world”,放入字符串常量池,然后这个字符串的地址赋值给了str2,所以str1并没有发生任何的改变。 这一点非常重要。

String类的常用方法

字符串比较

  1. equals方法。

    image-20210919171626732

    比较的是字符串里面的内容是否相等,也是平时使用的最大的比较方法。

    String str1 = "hello";
    String str2 = "hello";
    Ststem.out.println(str1.equals(str2));
    //切记,equals方法的调用方,不能是null,不然会报异常。即str1不能是null
    
  2. equalsIgnoreCase方法。这个方法比较高级,它会忽略大小写的区别

    image-20210919171956522

    String str1 = "hello";
    String str2 = "HELLO";
    System.out.println(str1.equalsIgnoreCase(str2)); //此时还是true
    
  3. compareTo方法,这个方法比较的就是字典序,类似于C语言的strcmp方法

    image-20210919172231299

    String str1 = "hello";
    String str2 = "helloo";
    System.out.println(str1.compareTo(str2)); //返回的小于0的数
    //这个方法,会将两个字符串的每一个字符进行比较,在比较的过程中,如果调用方的某一个字符小于另一方的字符,那么就返回负数。
    //如果调用方的字符大于另一方的字符,返回正数
    //如果两个字符串的长度相等,且每个字符都相等,那么就返回0
    

字符串替换

  1. replace方法,用于替换字符串里面的一些字符

    image-20210919173308194

    String str1 = "hellohellohello";
    String str2 = str1.replace('h', 'H'); //将小写h换成大写H
    //切记,这里不会影响到str1字符串本身,因为Java中的字符串是不可变的,
    //这里只会建立一个新的字符串,进行改动的。
    
  2. replaceFirst方法,将第一次出现的字符串进行替换

    image-20210919173714557

    String str1 = "hellohellohello";
    String str2 = str1.replaceFirst("ll", "LL"); //将第一次出现“ll”字符串的替换
    

字符串查找

  1. contains方法,用于判断一个字符串,是否是包含另外一个字符串的

    image-20210919174730113

String str1 = "hello world";
System.out.println(str1.contains("world")); //判断str1是否有world子串
  1. indexOf方法,用于返回一个子串,在主串中的起始位置,也就是大家熟知的KMP算法实现的

image-20210919175048994

String  str1 = "hello KMP";
System.out.println(str1.indexOf("KMP")); //返回值就是下标6
  1. startsWith方法,判断主串中,是否是以这个子串开头的(前缀)

    image-20210919175349905

    String str1 = "hello world";
    System.out.println(str1.startsWith("hello")); //判断是否以hello开头
    
  2. endsWith方法,判断主串中,是否以这个子串结尾的

    image-20210919175548693

    String str1 = "hello world";
    System.out.println(str1.endsWith("world")); //判断主串是不是以world结尾的
    

字符串截取

split方法,用于将一个字符串,以某个字符进行分割,返回的是一个字符串数组

image-20210919175856779

这个方法,我在刷题的时候用的挺多的,配合缓冲输入流,读取一行数据,然后进行分割。

String str1 = "I love you";
String[] res = str1.split(" "); //以空格进行分割

除此之外,还有一个split方法,限制了分割后的数组个数

image-20210919180214811

String str1 = "I love you";
String[] res = str1.split(" ", 2); //以空格分割,分割为两个数组
//即以上代码分割后的数组中,只有两个数据:I  和 love you,两部分

split方法,还有一个用法,比如给定一个字符串,我要以多个字符进行分割,假设给定字符串为I love*you,如何将空格和*号一起分割呢。如下:

String str = "I love*you";
 //前面一个空格,后面一个*号,中间用|隔开,就能实现多个字符的分割
String[] res = str.split(" |*");

当然split方法,在分割ip地址时,也是需要注意一个点,那就是ip地址的小数点分割符,需要先用转义字符代替,如下:

String str = "192.168.1.1";
String[] res = str.split("\\."); //用两个斜线,先进行转义

其他方法

  1. isEmpty方法,用于判断字符串是否为空串。切记此处的空串,指的是字符串里什么都没有,不是null
  2. intern方法,手动将字符串放入常量池
  3. trim方法,去掉字符串的首尾的空格
  4. toUpperCase方法,将字符串的小写字符转换为大写
  5. toLowerCase方法,将字符串的大写字符转换为小写

等等……,这里我就不列举了。

二、StringBuilder与StringBuffer

在上文中,我们已经了解了String类的简单使用,对于这个类的使用,可能熟读了上文中的内存分配之后,会觉得,String类在拼接字符串的时候,会建立出很多对象,比如有以下代码:

String str = "hello";
for (int i = 0; i < 100; i++) {
    str = str + i;
}
System.out.println(str);

上述代码中,str字符串一直在拼接新的字符串,组合成新的字符串。根据上文中的内存分配图,我们可以脑补出大致的内存时如何浪费的,每次拼接,都需要在堆上新建一个对象,然后拼接。这样的方式实在是太浪费空间了。

所以后来就有了StringBuilderStringBuffer两个字符串相关的类。

image-20210920114210950

我们先来看一下String、StringBuilder和StringBuffer三者之间的区别!

  • StringBuffer和StringBuilder非常的相似,均代表可变的字符序列,而且方法都是一样的
  • String是不可变字符串
  • StringBuffer是可变字符串,执行效率低,但线程安全,适用于多线程
  • StringBuilder是可变字符串,执行效率高,但线程不安全,适用于单线程

三者之间的继承关系如下图:

image-20210920114844415

说了那么多,有人可能会问,到底该怎么使用这两个类呢?我们这就来讲。

image-20210920115137773

一样的,还是先从构造方法说着走,有无参构造,也有有参构造。最常用的就是下面这两种:

String str = "hello";
StringBuilder sb = new StringBuilder(); //无参构构造方法
sb.append(str); //通过这个方法,可以将“hello”添加到这个StringBuilder中

StringBuilder sb2 = new StringBuilder(str); //也可以直接在构造方法里,传入字符串

上面这两种方法,是最常用的。StringBuffer也是如此。

image-20210920115643378

String str = "world";
StringBuffer sb = new StringBuffer(); //无参构造
sb.append(str);

StringBuffer sb2 = new StringBuffer(str); //有参构造

我们来讨论一个面试题

//以下代码,是如何进行拼接字符串的?具体流程?
String str1 = "hello ";
String str2 = str2 + "world";

我们通过反编译,来看一下这段代码具体执行了哪些操作。

image-20210920122536327

所以,根据上图,我们可以看出,看似并没有用到StringBuilder,实则在JVM为了优化,所以将StringBuilder加入到了其中,通过StringBuilder的append方法进行添加字符串,然后再转换为字符串即可。

所以现在,我们回过头来看上文中的这一段代码,是否会觉得,很浪费时间和空间呢?

String str = "hello";
for (int i = 0; i < 100; i++) {
    str = str + i;
}
System.out.println(str);

每次进行一轮循环,JVM都需要new一个StringBuilder类,每次循环都是这样的。所以这样写代码,就很low。以后我们在写代码的时候,就要避免这样的写法,我们只需手动的在循环外面new一个StringBuilder类,然后循环里面调用append方法即可。

现在我们来说一说这StringBuilder类的一下常用方法;本质是,这个类的很多方法,String类中也是有的,我们只需要知道另外几个不知道的方法:

  1. toString方法,将StringBuilder类的对象,转换为字符串类型

    StringBuilder sb = new StringBuilder("hello world");
    //String res = sb; //error,这样是不行的
    String res = sb.toString(); //必须调用这个类的toString方法
    
  2. reverse方法。我记得有一道面试题,就是问如何将一个字符串进行逆序。此时我们就可以将字符串转换为StringBuilder类,然后调用reverse方法.

    String str = "hello world";
    StringBuilder sb = new StringBuilder(str);
    str = sb.reverse().toString(); //StringBuilder,可以进行链式调用
    //就如上,刚调用完reverse方法,后面可以直接进行调用toString方法。
    //因为它的返回值就是StringBuilder本身的对象
    
  3. append方法。用于在添加字符串的,切记这个方法,可以添加字符、数值、字符串等等。

    String str = "hello world";
    StringBuilder sb = new StringBuilder();
    sb.append(str).append("good morning");//同样也是可以进行链式调用的
    
  4. length方法。用于计算当前这个StringBuilder中的字符串,有多少个字符

    StringBuilder sb = new StringBuilder("hello world");
    System.out.println(sb.length()); //11
    
  5. delete方法。这个方法用于删除当前StringBuilder中的字符串,这个方法有两个参数,第一个是起始位置的偏移量,第二个是结束位置的偏移量。

    StringBuilder sb = new StringBuilder("hello world");
    sb.delete(1, 4); //从偏移量为1位置开始,一直到4位置,切记是左闭右开区间[1,4)
    System.out.println(sb.toString()); //输出:ho world
    
  6. insert方法。插入新的参数。有两个参数,第一个参数就是偏移量,第二个参数就是插入的内容。

    StringBuilder sb = new StringBuilder("I you");
    sb.insert(2, "love "); //在偏移量为2的位置,开始插入
    System.out.println(sb.toString());  //输出的结果:I love you
    

上面的所有代码,在StringBuffer中也是适用的。StringBuilder和StringBuffer,就像同门师兄弟一样,学的每一招功夫,都是相似的。

那么他们二者之间就没有区别吗? 肯定是有的,我们分别来看一下二者底层的源码:

image-20210920130238612

我们可以看到源码StringBuffer类中的每一个方法,都是被synchronized修饰的,简答点理解,就像一把锁,可以保证线程安全。所以说StringBuffer是线程安全的。而StringBuilder的每个方法,没有这个关键字,所以说它是线程不安全的。

还有一个问题就是:string转StringBuilder,或者StringBuilder转String。前者转换,只能通过调用StringBuilder的构造方法,或者先new一个StringBuilder对象,然后调用append方法添加。后者的话,就调用StringBuilder的toString方法就行。

好啦,上述所有,就是本期的所有内容。本期更新就到此结束啦!!!我们下期见!!!

img


点击全文阅读


本文链接:http://m.zhangshiyu.com/post/32169.html

字符串  方法  常量  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1