JavaSE 认识String类

目录

  • 1 创建字符串
  • 2 字符串比较相等
  • 3 字符串常量池
  • 4 理解字符串不可变
  • 5 字符、字节与字符串
    • 5.1 字符与字符串
    • 5.2 字节与字符串
    • 5.3 小结
  • 6 字符串常见操作
    • 6.1 字符串比较
    • 6.2 字符串查找
    • 6.3 字符串替换
    • 6.4 字符串拆分
    • 6.5 字符串截取
    • 6.6 其他操作方法
  • 7 StringBuffer 和 StringBuilder
  • 8 小结

1 创建字符串

String就是字符串,字符串就是:“双引号引起来的若干字符”。
C语言中说过字符串是以 \0 结尾的,但在Java当中没有这么一说。
常见的构造(定义) String 的方式:

//方式一
String str = “hello”;
//方式二
String str2 = new String(“hello”);
//方式三
char[] value = {‘h’,‘e’,‘l’,‘l’,‘o’};
String str3 = new String(value);

在官方文档上 https://docs.oracle.com/javase/8/docs/api/index.html 中可以看到 String 还支持很多其他的构造方式, 我们用到的时候去查就可以了。
注意事项:
“hello” 这样的字符串字面值常量, 类型也是 String。
String 也是引用类型,上面三种构造Sting的代码内存结构如下图所示:
JavaSE 认识String类_第1张图片
对于方式一的代码来说: 假设左边是栈,右边是堆,堆里面有块字符串常量池。首先String 是引用类型,此时定义了一个str,且它在main函数当中,说明它的内存在栈上,所以此时第一步我们在栈上对str分配了一块内存。在Java当中,String之所以是一个引用类型,说明它指向的是一个对象,此时一定要记住双引号引起来的这个hello叫做字符串常量,它也属于对象,而这个对象比较特殊,它是存放在字符串常量池中。所以假设字符串常量池中的hello地址是999,str里存放的则是999,此时str直接指向字符串常量池当中hello的地址。
对于方式二的代码来说: new String存放在堆中,地址为888;在栈上对str2分配了一块内存,str里存放的则也是888,这样的话此时str2直接指向堆当中new String的地址。此时有个问题就来了,双引号引起来的这个hello在哪里呢?此时如果要new出来这个String,需要我们给它传一个参数,那么也意味着此时会调用带有一个参数的String这个构造方法,进入到String这个构造方法之后,我们会发现将original.value赋给前面的this.value了;再将this.value一点进入之后,我们会发现这个value是一个数组,也就是说你在new String的时候,String对象本来就会有一个value数组。那么value[]这个数组是放到String对象当中的。然后会检查常量池是否有hello字符串的引用,如果有就直接给value数组的引用;如果没有,那么把hello在常量池当中存进去,同时把存进去的引用给value。
对于方式三的代码来说: 首先value要在栈上开辟内存, {‘h’,‘e’,‘l’,‘l’,‘o’}这个对象存放在堆上,此时这个value引用的是 {‘h’,‘e’,‘l’,‘l’,‘o’}这个对象。接着你要把new String存放在堆中,此时new String里还有一个value,然后这个value会引用一个对象,这个对象是由 {‘h’,‘e’,‘l’,‘l’,‘o’}拷贝过来的一份hello,即value就引用拷贝出来的这个hello。然后栈上对str3分配了一块内存,str3引用的就是堆上的value,堆上的这个value就引用拷贝出来的这个hello。

具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        //Java当中定义字符串通常有下面三种方式
        //方式一
        String str = "hello";
        System.out.println(str);
        //方式二
        String str2 = new String("hello");
        System.out.println(str2);
        //方式三
        char[] value = {'h','e','l','l','o'};
        String str3 = new String(value);
        System.out.println(str3);
    }
}

为了看对上图是否进行了真正的理解,我们可以想想下面的代码示例的输出结果是否和自己想的一致?具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str1 = "hello";

        String str2 = new String("hello");

        char[] value = {'h','e','l','l','o'};
        String str3 = new String(value);

        System.out.println(str1 == str2);//false,因为999不等于888
        System.out.println(str1 == str3);//false,因为999不等于777
        System.out.println(str2 == str3);//false,因为888不等于777
    }}

为了继续看对上图是否进行了真正的理解,尤其是常量池的理解,我们也可以想想下面的代码示例的输出结果是否和自己想的一致?具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str1 = "abcde";
        String str2 = new String("abcde");
        System.out.println(str1 == str2);//false

        String str3 = "abc"+"de";//这两个字符串常量在编译时就进行了拼接"abcde"
        System.out.println(str3 == str1);//true

        String str4 = new String("abc")+new String("de");
        System.out.println(str1 == str4);//false

        String str5 = new String("abc")+"de";
        System.out.println(str1 == str5);//false

        System.out.println(str4 == str5);//false
    }}

下面来给大家说一个特殊情况,具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str1 = "abcde";
        
        String str2 = new String("abcde").intern();
        System.out.println(str1 == str2);//true
    }

这个intern()方法会先看常量池里有没有"abcde",如果有,就把常量池中的"abcde"这个对象的地址直接给str2,这样一来的话它自然和str1 相等了;如果没有,就会在常量池中生成"abcde"这样一个引用。当然这个代码的的str1这一行和str2这一行互换位置也是可以的,结果是一样的。

同时,我们再加深理解一次String 是引用类型,String str = “Hello”; 这样的代码内存布局如下图所示:JavaSE 认识String类_第2张图片

回忆 “引用”:
我们曾经在讲数组的时候就提到了引用的概念。
引用类似于 C 语言中的指针, 只是在栈上开辟了一小块内存空间保存一个地址,但是引用和指针又不太相同, 指针能进行各种数字运算(指针+1)之类的, 但是引用不能, 这是一种 “没那么灵活” 的指针。
另外, 也可以把引用想象成一个标签, “贴” 到一个对象上,一个对象可以贴一个标签, 也可以贴多个。如果一个对象上面一个标签都没有, 那么这个对象就会被 JVM 当做垃圾对象回收掉。
Java 中数组、String以及自定义的类都是引用类型。

由于 String 是引用类型, 因此对于以下代码,内存布局如下图所示:

public class TestString {
    public static void main(String[] args) {
        String str1 = "Hello";
        //str2指向了str1指向的对象
        String str2 = str1;
        System.out.println(str1);
        System.out.println(str2);
        System.out.println(str2 == str1);//true
    }}

内存布局:JavaSE 认识String类_第3张图片
那么有人可能会说, 是不是修改 str1 , str2 也会随之变化呢?具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = str1;
        str1 = "World";
        System.out.println(str1);//World
        System.out.println(str2);//Hello
    }}

我们发现, “修改” str1 之后, str2 也没发生变化, 还是 Hello。事实上, str1 = “world” 这样的代码并不算 “修改” 字符串, 而是让 str1 这个引用指向了一个新的 String 对象,所以内存布局如下图所示:
JavaSE 认识String类_第4张图片

2 字符串比较相等

如果现在有两个int型变量,判断其相等可以使用 == 完成。具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
      int x = 10 ;
      int y = 10 ;
      System.out.println(x == y);//true
    }}

如果说现在在String类对象上使用 == ?具体代码示例如下所示(代码1):

public class TestString {
    public static void main(String[] args) {
      String str1 = "Hello";
      String str2 = "Hello"; 
      System.out.println(str1 == str2);//true
    }}

看起来貌似没啥问题, 再换个代码试试, 发现情况不太妙。具体代码示例如下所示(代码2):

public class TestString {
    public static void main(String[] args) {
      String str1 = new String("Hello");
      String str2 = new String("Hello");
      System.out.println(str1 == str2);//false
    }}

我们来分析两种创建 String 方式的差异:
代码1内存分布如下图所示:JavaSE 认识String类_第5张图片
我们发现:str1 和 str2 是指向同一个对象的,此时如 “Hello” 这样的字符串常量是在 字符串常量池中。

关于字符串常量池:
如 “Hello” 这样的字符串字面值常量, 也是需要一定的内存空间来存储的。这样的常量具有一个特点, 就是不需要修改(常量嘛). 所以如果代码中有多个地方引用都需要使用 “Hello” 的话, 就直接引用到常量池的这个位置就行了, 而没必要把 “Hello” 在内存中存储两次。

代码2内存布局如下图所示:
JavaSE 认识String类_第6张图片
通过 String str1 = new String(“Hello”); 这样的方式创建的 String 对象相当于再堆上另外开辟了空间来存储"Hello" 的内容, 也就是内存中存在两份 “Hello”。
String 使用 == 比较并不是在比较字符串内容, 而是比较两个引用是否是指向同一个对象。关于对象的比较面向对象编程语言中, 涉及到对象的比较, 有三种不同的方式, 比较身份, 比较值, 比较类型。
在大部分编程语言中 == 是用来比较比较值的,但是 Java 中的 == 是用来比较身份的。如何理解比较值和比较身份呢?可以想象一个场景, 现在取快递, 都有包裹储物柜,上面有很多的格子,每个格子里面都放着东西。例如, “第二行, 左数第五列” 这个柜子和 “第二行, 右数第二列” 这个柜子是同一个柜子, 就是 身份相同,如果身份
相同, 那么里面放的东西一定也相同 (值一定也相同);例如, “第一行, 左数第一列” 这个柜子和 “第一行, 左数第二列” 这两个柜子不是同一个柜子, 但是柜子打开后发现里面放着的是完全一模一样的两双鞋子,这个时候就是值相同。
Java 中要想比较字符串的内容, 必须采用String类提供的equals方法。
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str1 = "abcde";
        String str2 = new String("abcde");
        System.out.println(str1 == str2);//false
        System.out.println(str1.equals(str2));//true
        String str3 = "abcde";
        System.out.println(str1.equals(str3));//true
    }}

equals 使用注意事项:
现在需要比较 str 和 “Hello” 两个字符串是否相等, 我们该如何来写呢?具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
      String str = new String("Hello");
      // 方式一
      System.out.println(str.equals("Hello"));
      // 方式二
      System.out.println("Hello".equals(str));
}}

在上面的代码中, 哪种方式更好呢?我们更推荐使用 “方式二”,因为一旦 str 是 null, 方式一的代码会抛出异常, 而方式二不会。具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
      String str = null;
      // 方式一
      System.out.println(str.equals("Hello"));  // 执行结果 抛出 java.lang.NullPointerException 异常
     // 方式二
      System.out.println("Hello".equals(str));  // 执行结果 false
      
      //和上面代码表达的一个意思
      String str1 = null;
      String str3 = "abcde";
      //boolean flg = str1.equals("abcde");//此时会出现空指针异常
      boolean flg = "abcde".equals(str1);//此时则会正常运行,结果为false
      System.out.println(flg);
    }}

注意事项: “Hello” 这样的字面值常量, 本质上也是一个 String 对象, 完全可以使用 equals 等 String 对象的方法。

3 字符串常量池

在上面的例子中, String类的两种实例化操作, 直接赋值和 new 一个新的 String。
(1)直接赋值:
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
      String str1 = "hello" ;
      String str2 = "hello" ; 
      String str3 = "hello" ; 
      System.out.println(str1 == str2); // true
      System.out.println(str1 == str3); // true
      System.out.println(str2 == str3); // true
      }}

代码内存布局如下图所示:JavaSE 认识String类_第7张图片
为什么现在并没有开辟新的堆内存空间呢?
答:String类的设计使用了共享设计模式,在JVM底层实际上会自动维护一个对象池(字符串常量池)。如果现在采用了直接赋值的模式进行String类的对象实例化操作,那么该实例化对象(字符串内容)将自动保存到这个对象池之中。如果下次继续使用直接赋值的模式声明String类对象,此时对象池之中如若有指定内容,将直接进行引用;如若没有,则开辟新的字符串对象而后将其保存在对象池之中以供下次使用。
理解 “池” (pool):
“池” 是编程中的一种常见的, 重要的提升效率的方式, 我们会在未来的学习中遇到各种 “内存池”, “线程池”, “数据库连接池” …然而池这样的概念不是计算机独有, 也是来自于生活中,举个例子:现实生活中有一种人称为 “a”, 在和b谈着对象的同时, 还可能和c搞暧昧,这时候这个c被称为 “备胎”。那么为啥要有备胎? 因为一旦和b分手了, 就可以立刻找c继续谈, 这样效率比较高。如果这个a, 同时在和很多个c搞暧昧, 那么这些备胎就称为备胎池。
(2) 采用构造方法:
类对象使用构造方法实例化是标准做法。具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str = new String("abcde");
    }}

代码内存布局如下图所示:JavaSE 认识String类_第8张图片
这样的做法有两个缺点:

  1. 如果使用String构造方法就会开辟两块堆内存空间,并且其中一块堆内存将成为垃圾空间(字符串常量 “hello” 也是一个匿名对象, 用了一次之后就不再使用了, 就成为垃圾空间, 会被 JVM 自动回收掉)。
  2. 字符串共享问题:同一个字符串可能会被存储多次, 比较浪费空间。

我们可以使用 String 的 intern 方法来手动把 String 对象加入到字符串常量池。具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
      // 该字符串常量并没有保存在对象池之中
      String str1 = new String("hello") ; 
      String str2 = "hello" ; 
      System.out.println(str1 == str2); // 执行结果false
    
      String str1 = new String("hello").intern() ; 
      String str2 = "hello" ; 
      System.out.println(str1 == str2); // 执行结果true
    }}

代码内存布局如下图所示:
JavaSE 认识String类_第9张图片

面试题: 请解释String类中两种对象实例化的区别。
答:a. 直接赋值:只会开辟一块堆内存空间,并且该字符串对象可以自动保存在对象池中以供下次使用。b.构造方法:会开辟两块堆内存空间,不会自动保存在对象池中,可以使用intern()方法手工入池。

综上, 我们一般采取直接赋值的方式创建 String 对象。

4 理解字符串不可变

字符串是一种不可变对象,它的内容不可改变。
String 类的内部实现也是基于 char[] 来实现的, 但是 String 类并没有提供 set 方法之类的来修改内部的字符数组。看看形如这样的代码,具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
      String str = "hello" ; 
      str = str + " world" ; 
      str += "!!!" ; 
      System.out.println(str); // 执行结果hello world!!!
    }}

形如 += 这样的操作, 表面上好像是修改了字符串, 其实不是,内存变化如下图所示:
JavaSE 认识String类_第10张图片
+= 之后 str 打印的结果却是变了, 但是不是 String 对象本身发生改变, 而是 str 引用到了其他的对象。
回顾引用: 引用相当于一个指针, 里面存的内容是一个地址。我们要区分清楚当前修改到底是修改了地址对应内存的内容发生改变了, 还是引用中存的地址改变了。

那么如果实在需要修改字符串, 例如, 现有字符串 str = “Hello” , 想改成 str = “hello” , 该怎么办?
(a) 常见办法: 借助原字符串, 创建新的字符串

public class TestString {
    public static void main(String[] args) {
      String str = "Hello";
      str = "h" + str.substring(1);
      System.out.println(str);// 执行结果hello
      }}

(b) 特殊办法: 使用 “反射” ,这样的操作可以破坏封装, 访问一个类内部的 private 成员。
IDEA 中 ctrl + 左键 跳转到 String 类的定义, 可以看到内部包含了一个 char[] , 保存了字符串的内容,如下图所示:
在这里插入图片描述
利用反射对字符串进行修改的具体代码示例如下所示:

import java.lang.reflect.Field;
import java.util.Arrays;

public class TestString {
    public static void main(String[] args) {
        /*
        * Java里面有一个技术:反射
        * 反射是可以做到的(可以做到对字符串进行修改)
        *    用到:spring
        * */
        String str1 = "abc";
        Class c1 = String.class;//Class对象
        try {
            // 获取 String 类中的 value 字段. 这个 value 和 String 源码中的 value 是匹配的
            Field field = c1.getDeclaredField("value");//获取对应字段
            // 将这个字段的访问属性设为 true
            field.setAccessible(true);
            try {
                // 把 str1 中的 value 属性获取到.
                char[] value = (char[])field.get(str1);
                System.out.println(Arrays.toString(value));
                System.out.println(str1);
                // 修改 value 的值
                value[0]='G';
                System.out.println(str1);//执行结果:Gbc
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }}

关于反射:
反射是面向对象编程的一种重要特性, 有些编程语言也称为 “自省”,指的是程序运行过程中, 获取/修改某个对象的详细信息(类型信息, 属性信息等), 相当于让一个对象更好的 “认清自己” 。Java 中使用反射比较麻烦一些,我们后面的笔记中会详细介绍反射的具体用法。
为什么 String 要不可变?(不可变对象的好处是什么?)

  1. 方便实现字符串对象池,如果 String 可变, 那么对象池就需要考虑何时深拷贝字符串的问题了。
  2. 不可变对象是线程安全的。
  3. 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中。

注意事项: 如下代码不应该在你的开发中出现, 会产生大量的临时对象, 效率比较低。具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        /*
        * 对于这样的代码以后尽量不要出现在项目当中,因为它会创建很多的临时变量
        * 如果出现这种情况,我们一般用StingBuffer和StringBuilder来解决
        * */
        String str1 = "abc";
        for (int i = 0; i < 10; i++) {
            str1 += i;
        }
        System.out.println(str1);
    }}

5 字符、字节与字符串

5.1 字符与字符串

字符串内部包含一个字符数组,String 可以和 char[ ] 相互转换。有如下图所示的四种转换方式:
JavaSE 认识String类_第11张图片
获取指定位置的字符:
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
      //方法一:
      String str = "hello";
      //但这个数字不能太大,得合理,不然就会发生越界问题
      char ch = str.charAt(0);
      System.out.println(ch);//h
      System.out.println(str.charAt(10));// 执行结果产生 StringIndexOutOfBoundsException 异常

      //方法二:
      char[] value = {'h','e','l','l','o'};
      String str3 = new String(value,1,2);
      //但这两个数字不能太大,得合理,不然就会发生越界问题
      System.out.println(str3);//el

字符串与字符数组的转换:
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
      //方法一:
      String str = "helloworld" ; 
      // 将字符串变为字符数组
      char[] data = str.toCharArray() ; 
      for (int i = 0; i < data.length; i++) { 
       System.out.print(data[i]+" "); 
      } 
     // 字符数组转为字符串
     System.out.println(new String(data)); // 全部转换
     System.out.println(new String(data,5,5)); // 部分转换

     String str = "hello";
     char[] chars = str1.toCharArray();
     System.out.println(Arrays.toString(chars));//把字符串变成了数组的形式输出[h,e,l,l,o]

      //方法二:
      char[] value = {'h','e','l','l','o'};
      String str3 = new String(value,1,2);
      //但这两个数字不能太大,得合理,不然就会发生越界问题
      System.out.println(str3);//el
     }}

字符串逆置问题:
我们知道字符串是不可以被修改的,所以那么我们又该如何将字符串进行逆置呢?具体代码示例如下所示:

public class TestString {
    //逆置
    //字符串转为数组
    public static String reverse(String string){
        char[] chars = string.toCharArray();
        int i = 0;
        int j = chars.length-1;
        while (i < j){
            char tmp = chars[i];
            chars[i] = chars[j];
            chars[j] = tmp;
            i++;
            j--;
        }
        //数组转换为字符串
        //方法一:return new String(chars);
        //方法二:return  String.copyValueOf(chars);
        //方法三:
        return String.valueOf(chars);
    }
    public static void main(String[] args) {
        String str = "abcdef";
        //逆置
        String ret = reverse(str);
        System.out.println(ret);
    }}

微信面试真题 :翻转字符串(牛客网)

题目描述: 给一个字符类型的数组chas和一个整数size,请把大小为size的左半区整体右移到右半区,右半区整体移动到左边。
输入描述: 输入两行,第一行一个整数,代表size,第二行一个字符串,代表chas(1<= size <= length(chas) <= 10^5)。
输出描述: 输出一行字符串,代表翻转后的字符串。
示例: 输入:3 abcdefg;输出:defgabc。

解题思路如下图所示经历了三次翻转:
JavaSE 认识String类_第12张图片
具体代码示例如下所示:

import java.util.*public class Main {
    //逆置
    //字符串转为数组
    public static String reverse1(String string,int left,int right){
        char[] chars = string.toCharArray();
        while (left < right){
            char tmp = chars[left];
            chars[left] = chars[right];
            chars[right] = tmp;
            left++;
            right--;
        }
        return String.valueOf(chars);
    }
    public static String reverseK(String str,int k){
        if(str == null || k <= 0 || k > str.length()){
            return null;
        }
        str = reverse1(str,0,k-1);
        str = reverse1(str,k,str.length()-1);
        str = reverse1(str,0,str.length()-1);
        return str;
    }
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int k = scan.nextInt();
        String str = scan.next();

        String ret = reverseK(str,k);
        System.out.println(ret);
    }} 

给定字符串一个字符串, 判断其是否全部由数字所组成:
思路: 将字符串变为字符数组而后判断每一位字符是否是" 0 “~”‘9’"之间的内容,如果是则为数字。
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str = "1a23456" ; 
        System.out.println(isNumber(str)? "字符串由数字所组成!" : "字符串中有非数字成员!");
    }
    public static boolean isNumber(String str) { 
        char[] data = str.toCharArray() ; 
        for (int i = 0; i < data.length; i++) { 
           if (data[i]<'0' || data[i]>'9') { 
             return false ; 
             } 
           } 
            return true ; 
           }
         }

5.2 字节与字符串

字节常用于数据传输以及编码转换的处理之中,String 也能方便的和 byte[ ] 相互转换。有如下图所示的四种转换方式:
JavaSE 认识String类_第13张图片
实现字符串与字节数组的转换处理:
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        byte[] bytes = {97,98,99,100};
        String str = new String(bytes);//abcd
        //String str = new String(bytes,1,2);//bc
        System.out.println(str);

        String str2 = "abcd";
        byte[] bytes2 = str2.getBytes();
        System.out.println(Arrays.toString(bytes2));//[97,98,99,100]

        String str3 = "肥憨";
        try {
            byte[] bytes3 = str3.getBytes("GBK");
            System.out.println(Arrays.toString(bytes3));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

         String str4 = "helloworld" ; 
         // String 转 byte[] 
        byte[] data = str.getBytes() ; 
        for (int i = 0; i < data.length; i++) { 
             System.out.print(data[i]+" "); 
         } 
         // byte[] 转 String 
        System.out.println(new String(data));
        }}

5.3 小结

那么何时使用 byte[ ], 何时使用 char[ ] 呢?
答:byte[ ] 是把 String 按照一个字节一个字节的方式处理, 这种适合在网络传输, 数据存储这样的场景下使用,更适合针对二进制数据来操作。
char[ ] 是吧 String 按照一个字符一个字符的方式处理, 更适合针对文本数据来操作, 尤其是包含中文的时候。
回忆概念: 文本数据 vs 二进制数据
答:一个简单粗暴的区分方式就是用记事本打开能不能看懂里面的内容,如果看的懂, 就是文本数据(例如 .java 文件), 如果看不懂, 就是二进制数据(例如 .class 文件)。

6 字符串常见操作

6.1 字符串比较

上面使用过String类提供的equals()方法,该方法本身是可以进行区分大小写的相等判断。除了这个方法之外,String类还提供有如下图的比较操作:
JavaSE 认识String类_第14张图片
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        //比较两个字符串是否相同
        String str1 = "abcd";
        String str2 = "abcde";
        String str3 = new String("abcd");
        String str4 = "Abcd";
        System.out.println(str1.equals(str2));//false
        System.out.println(str1.equals(str3));//true
        System.out.println(str1 == str3);//false
        
        System.out.println(str1.equalsIgnoreCase(str4));//true,忽略大小写进行比较

        //比较两个字符串的大小
        System.out.println(str1.compareTo(str2));//执行结果为:小于0的数字(str1小于str2的情况);等于0(str1等于str2的情况);大于0的数字(str1大于str2的情况)
        System.out.println("刘".compareTo("杨"));
        }
        }

在String类中compareTo()方法是一个非常重要的方法,该方法返回一个整型,该数据会根据大小关系返回三类内容:

  1. 相等:返回0。
  2. 小于:返回内容小于0。
  3. 大于:返回内容大于0。

compareTo()是一个可以区分大小关系的方法,是String方法里是一个非常重要的方法。字符串的比较大小规则, 总结成三个字 “字典序” ,相当于判定两个字符串在一本词典的前面还是后面,先比较第一个字符的大小(根据 unicode 的值来判定), 如果不分胜负, 就依次比较后面的内容。

6.2 字符串查找

从一个完整的字符串之中可以判断指定内容是否存在,对于查找方法有如下图所示的定义:JavaSE 认识String类_第15张图片
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str1 = "adcddefgh";
        System.out.println(str1.contains("abc"));//false
        System.out.println(str1.indexOf("dde"));//3,字符串的匹配算法KMP
        System.out.println(str1.indexOf("dde",2));

        String str2 = "ababcabcd";
        //从后往前找
        System.out.println(str2.lastIndexOf("ab"));//5
        //从fromIndex开始往前找
        System.out.println(str2.lastIndexOf("ab",4));//2

        System.out.println(str2.startsWith("a"));//true
        System.out.println(str2.startsWith("a",2));//true

        System.out.println(str2.endsWith("cd"));//true
      }
    }

字符串查找,最好用最方便的就是contains()。该判断形式是从JDK1.5之后开始追加的,在JDK1.5以前要想实现与之类似的功能,就必须借助、indexOf()方法完成。
现在基本都是用contains()方法完成。使用indexOf()需要注意的是,如果内容重复,它只能返回查找的第一个位置。

6.3 字符串替换

使用一个指定的新的字符串替换掉已有的字符串数据,可用的方法如下图所示:
JavaSE 认识String类_第16张图片
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str1 = "adacbabcdabcde";
        String string = str1.replaceAll("ab","xx")
        System.out.println(string);

        String string2 = str1.replaceFirst("ab","xx");
        System.out.println(string2);

        String string3 = str1.replace("ab","xx");
        System.out.println(string3);//replace的作用和replaceAll的作用一样

        //CharSequence charSequence = new String();
        //Comparable comparable = new String();
    }}

注意事项: 由于字符串是不可变对象, 替换不修改当前字符串, 而是产生一个新的字符串。

6.4 字符串拆分

可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串。可用方法如下图所示:
JavaSE 认识String类_第17张图片
实现字符串的拆分处理:
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String string = "username=zhangsan&password=123";
        String[] strings = string.split("&");// 按照&拆分
        //这里为多次拆分,这种代码在以后的开发之中会经常出现
        for(int i = 0; i < strings.length;i++){
            //System.out.println(strings[i]);
            String[] stringss = strings[i].split("=");
            System.out.println(stringss[1]);
        }
    }}

字符串的部分拆分:
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String string = "username=zhangsan&password=123";
        //String[] strings = string.split("&",2);
        String[] strings = string.split("&",1);
        for(int i = 0; i < strings.length;i++){
            System.out.println(strings[i]);
        }
    }}

拆分是特别常用的操作. 一定要重点掌握。另外有些特殊字符作为分割符可能无法正确切分, 需要加上转义。
拆分IP地址:
具体代码示例如下所示:

 public class TestString {
     public static void main(String[] args) {
        String string = "192.168.1.1";
        //String[] strings = string.split(".");//这里要转义,否则无效
        String[] strings = string.split("\\.");//这里就进行了转义,*也是如此,需要转义
        for(int i = 0; i < strings.length;i++){
            System.out.println(strings[i]);
        }
        }}

注意事项:

  1. 字符" | “,” * “,”+“都得加上转义字符,前面加上”"。
  2. 而如果是" “,那么就得写成” \"。
  3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符。

对于注意事项的第三点,具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String string = "Java30-split#bit";
        String[] strings = string.split("-|#");
        for(int i = 0; i < strings.length;i++){
            System.out.println(strings[i]);
        }
    }}

6.5 字符串截取

从一个完整的字符串之中截取出部分内容,可用方法如下图所示:
JavaSE 认识String类_第18张图片
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str = "abcdef";
        /*String str2 = str.substring(2);
        System.out.println(str2);//cdef*/
        String str2 = str.substring(2,4);//[2,4) cd
        System.out.println(str2);
    }
}

注意事项:

  1. 索引从0开始。
  2. 注意前闭后开区间的写法, substring(0, 5) 表示包含 0 号下标的字符, 不包含 5 号下标。

6.6 其他操作方法

其他可用方法如下图所示:
JavaSE 认识String类_第19张图片
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        String str = "      abcdef  def fadsa fsadfsaf";
        String str1 = "ABCD";
        System.out.println(str);
        System.out.println(str.trim());
        System.out.println(str.toUpperCase());
        System.out.println(str1.toLowerCase());

        String str2 = null;//代表不指向任何对象
        //System.out.println(str3.isEmpty());这里不能运行这句话,它就空指针异常了
        String str3 = "";//代表指向的对象什么都没有
        System.out.println(str3.isEmpty());
    }}

注意事项:

  1. trim 会去掉字符串开头和结尾的空白字符(空格, 换行, 制表符等)。
  2. 数组长度使用数组名称.length属性,而String中使用的是length()方法。
  3. String类并没有提供首字母大写操作,需要自己实现。

首字母大写:
具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        System.out.println(fistUpper("yuisama")); 
        System.out.println(fistUpper("")); 
        System.out.println(fistUpper("a")); 
      } 
     public static String fistUpper(String str) { 
         if ("".equals(str)||str==null) { 
            return str ; 
      } 
         if (str.length()>1) { 
            return str.substring(0, 1).toUpperCase()+str.substring(1) ; 
       } 
         return str.toUpperCase() ; 
    }}

7 StringBuffer 和 StringBuilder

首先来回顾下String类的特点:
任何的字符串常量都是String对象,而且String的常量一旦声明不可改变,如果改变对象内容,改变的是其引用的指向而已。
通常来讲String的操作比较简单,但是由于String的不可更改特性,为了方便字符串的修改,提供StringBuffer和StringBuilder类。
在String中使用"+"来进行字符串连接,但是这个操作在StringBuffer类中需要更改为append()方法,下面为StringBuffer类中append()方法的书写:

public synchronized StringBuffer append(各种数据类型 b)

对于三者的区别与理解,我们可以根据下面的代码进行一个具体的感受,具体代码示例如下所示:

public class TestString {
    public static void main(String[] args) {
        /*
        * String 和 StringBuilder、StringBuffer区别:
        * 1、后两者包含了一些String没有的方法,比如reverse方法。
        * 2、后两者是可变的,String是不可变的。String的每次拼接都会产生新的对象;后两者每次的拼接都返回的是this。
        * 3、StringBuilder和StringBuffer的区别:
        *      StringBuilder和String出现在单线程情况下;
        *      StringBuffer因为有synchronized关键字,所以一般出现在多线程情况下
        * 4、StringBuilder和String之间的区别:
        *      String的拼接 + 会被优化为StringBuilder .append了!!!
        *      在循环当中,不可以使用String直接进行拼接,这样会产生大量的临时对象,包括优化后的StringBuilder对象。
        *
         * */
        String str = "abcdef";
        str = str+"hello";
        System.out.println(str);
        /*
        * 上面这段代码的编译过程其实相当于下面这段代码:
        * String str = "abcdef";
        * StringBuilder sb = new StringBuilder();
        *
        * sb.append(str);
        * sb.append("hello");
        *
        * str = sb.toString();
        * System.out.println(str);
        * */


        StringBuilder sb = new StringBuilder("abcde");
        System.out.println(sb.append("hello"));
        System.out.println(sb.reverse());

        StringBuffer sb2 = new StringBuffer("abcde");
        System.out.println(sb2.append("hello"));

        String str1 = "abc";
        for (int i = 0; i < 10; i++) {
            str1 += i;
        }
        System.out.println(str1);
        /*
        * 上面这段代码的编译过程其实相当于下面这段代码:
        * String str1 = "abc";
          for (int i = 0; i < 10; i++) {
              StringBuilder sb = new StringBuilder();
              str1 = sb.append(str).append(i).toString();
        }
        System.out.println(str1);
        * */
        /*
        * 但我们也说过,制造很多临时变量很不好,所以我们按照下面的方法写则比较好:
        * String str1 = "abc";
        * StringBuilder sb = new StringBuilder();
        * sb.append(str1);
          for (int i = 0; i < 10; i++) {
              str1 = sb.append(i).toString();
        }
        System.out.println(str1);
        * */
    }}

String和StringBuffer最大的区别在于:String的内容无法修改,而StringBuffer的内容可以修改。频繁修改字符串的情况考虑使用StingBuffer。
为了更好理解String和StringBuffer,我们来看这两个类的继承结构,具体如下表所示:

String类 StringBuffer类
public final class String implements java.io.Serializable, Comparable, CharSequence public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence

可以发现两个类都是"CharSequence"接口的子类。这个接口描述的是一系列的字符集。所以字符串是字符集的子类,如果以后看见CharSequence,最简单的联想就是字符串。
注意: String和StringBuffer类不能直接转换。如果要想互相转换,可以采用如下原则:

  1. String变为StringBuffer:利用StringBuffer的构造方法或append()方法。
  2. StringBuffer变为String:调用toString()方法。

除了append()方法外,StringBuffer也有一些String类没有的方法:

  1. 字符串反转:

public synchronized StringBuffer reverse()

  1. 删除指定范围的数据:

public synchronized StringBuffer delete(int start, int end)

  1. 插入数据:

public synchronized StringBuffer insert(int offset, 各种数据类型 b)

面试题:
请解释String、StringBuffer、StringBuilder的区别:
答:1. String的内容不可修改,StringBuffer与StringBuilder的内容可以修改;
2. StringBuffer与StringBuilder大部分功能是相似的;
3. StringBuffer采用同步处理,属于线程安全操作;而StringBuilder未采用同步处理,属于线程不安全操作。

8 小结

字符串操作是我们以后非常常用的操作,使用起来都非常简单方便, 一定要使用熟练。特别指出注意的点:

  1. 字符串的比较:==, equals, compareTo 之间的区别。
  2. 了解字符串常量池, 体会 “池” 的思想。
  3. 理解字符串不可变。
  4. split 的应用场景。
  5. StringBuffer 和 StringBuilder 的功能。

你可能感兴趣的:(JavaSE,基础语法,java,intellij-idea,开发语言,经验分享,笔记)