Bootstrap

java字符串

1.字符串

     1.1 什么是字符串?

     字符串是在计算机科学和编程中常用的一种数据类型,它代表一系列字符的有序集合,通常用来表示文本。通俗来讲字符串就是一个个字符连接成串。在java中用"aaaaa"表示字符串而用'a'表示一个字符

    1.2 java为什么要有字符串?

  1. 表示数据:在与用户的交互过程中,会产生大量数据,其中许多是以文本形式存在的,如用户的姓名、地址等个人信息,而字符串是计算机科学中用来表示这类文本数据的标准方式。
  2. 处理文本数据:无论是在与用户的交互还是在数据库的存储等方面对数据的处理都尤为重要,作用于保障数据安全或验证数据有效性等方面。java提供了丰富的内置函数和方法,使开发者能够轻松地进行文本数据的处理。
  3. 提升效率:在java中字符串(String)是不可变的这使得java可以通过字符串常量池减少内存中重复字符串的存储,提高内存利用率和访问效率,字符串的不可变性允许JVM对字符串进行缓存和重用,减少创建新对象的开销,同时也利于编译器进行优化。
  4. 提高安全性和可靠性:由于字符串对象(String)一旦创建便无法更改,这防止了数据在运行时被意外或恶意修改,保护了数据的完整性。不可变性意味着字符串可以在多线程环境中安全地共享。

2.String

    String类是 java.lang 包中一个非常核心的类,用于表示不可变的字符序列。 在java的8大基本数据类型中并没有String,所String是一个引用类型并且因为String类被声明为final所以String类是不可继承的

    2.1 为何被声明为final?

1. 维护不可变性:

        String类的核心特性之一是不可变性,如果String类不是 final的,那么潜在的子类可以修改字符串的行为,从而破坏其不可变性,从而导致不可预测的结果和潜在的bug。

2. 性能优化:

        字符串常量池是String类的一个重要特性,它有助于减少内存消耗和提高性能。不可变性和 final特性结合在一起,使得 JVM 能够缓存字符串对象并在需要时重用它们,而不是每次都创建新的实例。如果 String允许被继承,那么这种优化可能会受到影响,因为子类可能有不同的行为,从而使得 JVM 更难进行有效的优化。

3. 避免潜在的错误:

        如果 String类可以被继承,那么开发者可能会不经意间创建子类,而这些子类可能会改变字符串的行为,从而引入难以追踪的错误。通过将其声明为 final,Java 开发者可以确信String类的行为是固定且可靠的。

总之就是为了维护代码的健壮性,避免因为继承导致意外的错误或者对性能产生影响而禁止了String的继承。

    2.2 String的继承关系(所有图片代码均以jdk1.8为例)

  1. 实现了Serializable接口,表明String对象可以被序列化,即可以被转换成字节流,用于存储到磁盘或通过网络传输。

  2. 实现了Comparable接口,这意味着String对象之间可以进行比较。Comparable接口定义了一个compareTo方法,用于比较两个String对象的大小。

    2.3 String的构造方法

        String类拥有众多的构造方法在面对不同的需求时可以选择合适的方法。

    2.3 String的内部结构

String 类内部使用一个 char 类型的数组来存储字符串的字符,数组的长度在创建时确定并且之后不能改变。除了字符数组,String 类还维护一个 hash 字段,用于缓存字符串的哈希码,以避免在每次调用 hashCode() 方法时重新计算。

这里value用final修饰,说明编译器不允许把value指向另一个地址,但是可以修改数组元素。

   2.4 String类常用对字符串的操作

1:创建字符串

String str1 = "Hello,Java!";
String str2 = new String(“Hello,Java!”);
//其他构造方法

思考:

String str1= "a";

String str1=new String(“a”);

String str2="a";

这两个str1相同吗?第一个str1和str2又有什么联系?

不相同。String str1 = "a"; 创建了一个指向字符串常量池中字符串 "a" 的引用,而 String str1 = new String("a") 创建了一个新的 String 对象并在堆上分配空间。虽然两者的内容相同,但它们引用的是不同的对象。第一个str1和str2是对常量池中同一个对象的引用。

字符串常量池:字符串常量池最初位于方法区,但在 JDK 1.7 及之后的版本中,它被移到了堆中,以便更好地管理内存。当使用字符串字面量直接创建对象时,JVM 会先检查字符串常量池中是否已有相同内容的字符串对象存在。如果存在,则返回池中已有的对象引用;如果不存在,则将新的字符串对象添加到池中,并返回这个新字符串对象的引用。当用new和构造方法创建字符串对象时,不管常量池中有没有都会在堆里面创建一个对象。

    如果想要将一个字符串放入字符串常量池中可以使用String.intern()方法显式地将一个字符串放入字符串常量池中。如果字符串已经在池中,intern() 方法将返回池中已存在的字符串引用,否则它将把字符串添加到池中,并返回这个新字符串的引用。

2. 字符串长度

int length = str.length();

3.字符串拼接

        String str1 = "Hello,";
        String str2 = "Java!";
        // 使用concat方法
        String combined = str1.concat(str2);
        //用+拼接字符串(适合少量字符串的拼接)
        combined = str1+ str2;
        //用StringBuilder拼接在单线程环境中使用
        StringBuilder sbd = new StringBuilder();
        sb.append(str1).append(str2);
        combined = sb.toString();
        //用StringBuffer拼接在多线程环境中使用
        StringBuffer sbf = new StringBuffer();
        sb2.append(str1).append(str2);
        combined = sb2.toString();
        //使用String.join()方法 这个方法用于将集合中的元素用指定的分隔符连接成一个字符串
        List<String> list = Arrays.asList(str1,str2);
        combined = String.join(" ", list);//结果会多一个空格
        //使用String.format()或Formatter类 这种方式类似于C语言的printf,可以用于格式化输出。
        combined = String.format("%s%s", str1, str2);

思考:

String a="a";
String b="b";
a=new String("a");
a="b";
b=new String("b");
a+=b;

会产生几个对象?

  • 字符串常量池中的 "a"
  • 字符串常量池中的 "b"
  • 由 new String("a") 创建的堆内存中的String对象。
  • 由 new String("b") 创建的堆内存中的String对象。
  • 由 a + b 连接操作创建的堆内存中的String对象

在JDK 7及以前,拼接操作会创建一个`StringBuilder`或`StringBuffer`对象,但在JDK 8及以后,编译器会优化字符串字面量的拼接,直接创建一个`String`对象,因此`StringBuilder`对象的创建在现代JDK版本中可能不会发生。

思考:既然String是不可变的那么在拼接字符串过程中是否违背了这一个原则?

       其实不然,这里的combined只是对一个String对象的引用,在拼接过程中产生了第三个String对象,combined重新指向了这个新的对象,原来的对象还是没有改变。因此在对字符串的拼接过程中可能会有大量的String对象被创建。

            

4. 查找子串

int index = str.indexOf("World");

5. 截取子串

String subStr = str.substring(0, 5); // 获取从索引0到4的子串

6.替换字符或子串

String replaced = str.replace("World", "Java");

7. 转换大小写

String upperCase = str.toUpperCase();//转换为大写
String lowerCase = str.toLowerCase();//转换为小写

8. 去除空白

String trimmed = str.trim(); // 去除首尾空白

9. 判断字符串开头或结尾

boolean startsWith = str.startsWith("Hello");
boolean endsWith = str.endsWith("!");

10. 分割字符串

String[] words = str.split(", "); // 根据逗号和空格分割字符串

11. 比较字符串

boolean isEqual = str.equals("Hello, World!"); // 比较内容是否相等
boolean equalsIgnoreCase = str.equalsIgnoreCase("hello, world!"); // 忽略大小写的比较

12.常见字符串的转换

        // 字符串转字符数组
        String str = "Hello,Java!";
        char[] charArray = str.toCharArray();

        // 字符数组转字符串
        String fromCharArray = new String(charArray);

        //int转字符串
        int num=123;
        String fromInt = String.valueOf(num);//1
        fromInt=123+"";//2(length为4)
        String strNumber = Integer.toString(num);//3

        //字符串转int
        int fromString = Integer.parseInt(fromInt);//1
        fromString=Integer.valueOf("123");//2

        //字符串转char
        char fromStringChar = str.charAt(0);

        //字符串转数组
        String[] strArray = str.split(",");

        //数组转字符串(使用流)
        String[] strArray1 = {"Hello", "Java!"};
        String joinedStr = String.join(", ", strArray1);

        //字符串转list
        List<String> list1 = Arrays.asList(str.split(","));

        //list转字符串
        List<String> list2 = Arrays.asList("Hello", "Java!");
        String joinedList = String.join(", ", list2);

13. 格式化字符串

String formatted = String.format("Hello, %s!", "Java"); // 类似于 C 的 printf

14. equals()方法

equals()方法是Object类的原生方法,在String中重写了equals()方法。
//Object
 public boolean equals(Object obj) {
        return (this == obj);//判断当前对象是否和obj的引用相同
    }

//String重写的  
public boolean equals(Object anObject) {
        if (this == anObject) {//如果当前对象和anObject引用相同返回true
            return true;
        }
        if (anObject instanceof String) {//判断anObject是否属于String确保类型转换安全
            String anotherString = (String)anObject;//转为String
            int n = value.length;//当前对象维护的字符数组的长度
            if (n == anotherString.value.length) {//判断两个对象分别维护的字符数组长度是否相同
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {//比较两个char数组
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

    由此可知,String中的equals()方法比较的是两个String对象的值,而Object中的equals()方法比较的是两个对象的引用用于判断是否是同一个对象。

    所以两个对象equals()的结果为true他们的值一定相等但是反过来不一定。

3. StringBuffer

    StringBuffer是java.lang包下的一个类,它用于处理可变字符串数据。StringBuffer同样是不可继承的,但是它可以通过改变内部状态来修改数据,同时StringBuffer还是线程安全的。

     3.1 StringBuffer的继承关系

StringBuffer继承了AbstractStringBuilder类。AbstractStringBuilderStringBuilderStringBuffer的共同基类,它定义并实现了大部分与字符串构建相关的底层操作。

    3.2 StringBuffer的构造方法

           StringBuffer的构造方法相对较少。

           其中不带参的构造方法创建一个新的StringBuffer对象,初始容量为16个字符。这是默认的构造方法,当不指定任何参数时使用。

           public StringBuffer(String str)这个构造方法接收一个String参数,创建StringBuffer对象将初始化为该字符串的内容。

           public StringBuffer(int capacity)这个构造方法允许你指定StringBuffer的初始容量。

           public StringBuffer(CharSequence seq)这个构造方法接收一个CharSequence参数,可以是任何实现了CharSequence接口的对象,如String, StringBuilder, StringBuffer或其他实现该接口的自定义类。

  StringBuffer stringBuffer1 = new StringBuffer();//无参构造方法
  StringBuffer stringBuffer2 = new StringBuffer(new String("StringBuffer"));//基于字符串初始化
  StringBuffer stringBuffer3 = new StringBuffer(4);//指定初始化容量
  StringBuffer stringBuffer4 = new StringBuffer(new StringBuilder("StringBuffer"));//基于CharSequence初始化

    3.3 StringBuffer的内部结构

       StringBuffer继承了AbstractStringBuilder类,在AbstractStringBuilder类维护了一个字符数组value用于存储 StringBuffer 中的实际字符数据数组的大小决定了 StringBuffer 的容量,以及一个实际使用的字符数count,即当前字符串的长度。

          而StringBuffer自身则还维持了一个字符数组toStringCache用于存储最近一次调用toString()方法时返回的字符数组。toString()方法将内部的字符数组转换成一个不可变的String对象。为了提高性能,toStringCache被用来缓存这个结果,这样如果连续多次调用toString()方法,就不需要重复进行转换操作,可以直接返回缓存中的结果。

    3.4 StringBuffer类常用对字符串的操作

1. 拼接字符串

StringBuffer sb = new StringBuffer("Hello");
sb.append(",Java!"); // 结果是 "Hello,Java!"

       StringBuffer扩容机制:

       让我们从源码来分析:

        StringBuffer sb = new StringBuffer();
        sb.append("Hi,Java!");

        //1首先进入StringBuffer的append(String str)方法
//      @Override
//      public synchronized StringBuffer append(String str) {
//        toStringCache = null;//将toStringCache置空用于更新toString方法
//        super.append(str);//进入AbstractStringBuilder的append(String str)方法
//        return this;
//      }

      //2进入AbstractStringBuilder的append(String str)方法
//      public AbstractStringBuilder append(String str) {
//        if (str == null)
//          return appendNull();//添加null这四个字符到value中
//        int len = str.length();
//        ensureCapacityInternal(count + len);//确保容量充足若容量不够就进行扩容处理
//        str.getChars(0, len, value, count);//将str的全部复制到value中
//        count += len;//增加当前数据长度(并不是容量而是已经使用了的长度)
//        return this;
//      }


      //2这里涉及2个主要的方法
      //方法一(确保容量充足)
//      private void ensureCapacityInternal(int minimumCapacity) {
//        // overflow-conscious code
//        if (minimumCapacity - value.length > 0) {//为true时说明当前value的容量小于需求的容量就需要进行扩容操作
//          value = Arrays.copyOf(value,//copy增加了容量后的字符数组给value
//                  newCapacity(minimumCapacity));
//        }
//      }

        
      //方法二(扩容机制)
//      private int newCapacity(int minCapacity) {
//        // overflow-conscious code
//        int newCapacity = (value.length << 1) + 2;//newCapacity新的容量为value的当前容量*2+2
//        if (newCapacity - minCapacity < 0) {//新的容量小于最小需求容量直接就使用最小需求容量
//          newCapacity = minCapacity;
//        }
//        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)//这里的MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//                ? hugeCapacity(minCapacity)//这段代码是为了防止整数溢出和超出系统的物理内存限制
//                : newCapacity;//如果没有数溢出和超出系统的物理内存限制就返回新的容量newCapacity
//      }

      总结:通过以上代码可以分析出StringBuffer的扩容机制。如果当前容量小于需求的容量,StringBuffer将尝试将当前容量乘以二再加上二来确定新容量。这一策略应用于append(String str)append(int i)insert(int offset, String str)等方法中,尽管具体的实现细节可能有所不同,但扩容的核心思想是统一的,即通过动态调整内部字符数组的大小来确保有足够空间存储新增的字符,同时通过智能的容量计算来避免频繁的小幅扩容,提高性能。

2. 插入字符或字符串

sb.insert(6, " ");//结果是"Hello, Java!"

 3. 删除指定位置的字符串或者字串

sb.delete(5, 7);//结果是HelloJava

     思考:StringBuffer删除指定位置的字符串或者字串它的容量会发生改变吗?

             当使用StringBuffer删除指定位置的字符串或字串时,其内部字符数组value的容量并不会自动改变,只有他的长度会发生变化,如果需要改变这时也可以通过trimToSize()方法调整StringBuffer的容量。

4.替换字符串

sb.replace(0, 5, "Hi");//结果是HiJava

5.获取当前字符串内容

 String result = sb.toString();

4. StringBuilder

    StringBuilder是java.lang包下的一个类,也用于处理可变字符串数据。StringBuilder同样是不可继承的,但可以通过改变内部状态来修改数据,但是StringBuilder不是线程安全的。

    4.1 StringBuilder的继承关系

    4.2 StringBuilder的构造方法

    StringBuilder stringBuilder1 = new StringBuilder();//无参构造方法
    StringBuilder stringBuilder2 = new StringBuilder(new String("StringBuilder"));//基于字符串初始化
    StringBuilder stringBuilder3 = new StringBuilder(4);//指定初始化容量
    StringBuilder stringBuilder4 = new StringBuilder(new StringBuilder("StringBuilder"));//基于CharSequence初始化

    4.3 StringBuilder的内部结构

        与StringBuffer不同的是StringBuilder自身没有维持字符数组toStringCache用于存储最近一次调用toString()方法时返回的字符数组。

    4.4 StringBuilder类常用对字符串的操作

       StringBuilder与StringBuffer对字符串的操作方式相差无几,但是注意它是线程不安全的,所以会更高效。

5. String,StringBuffer和StringBuild

 String:

  不可变:String对象一旦创建,其内容就不能改变。对String的操作,如拼接、替换等, 都会返回一个新的String对象,而原始对象保持不变。

  性能:由于其不可变性,频繁的修改操作会导致较多的临时对象产生,影响性能。

  适用场景:适用于字符串不经常改变的情况,如常量声明、比较、作为参数传递等。

StringBuilder:

  可变:StringBuilder是可变的,它允许直接在其对象上进行修改,而不需要创建新的对象。

  线程不安全:为了提高性能,StringBuilder没有同步措施,因此不适用于多线程环境。

  性能:适合单线程环境下大量字符串操作,如拼接、插入、删除等,性能优于String。

  适用场景:适用于单线程中字符串的构建和修改操作,如动态生成SQL语句、拼接HTML等。

StringBuffer:

  可变:与StringBuilder相似,StringBuffer也是可变的,可以直接修改其内容。

  线程安全:与StringBuilder的主要区别在于,StringBuffer是线程安全的,它通过同步方法来确保在多线程环境中的安全使用。

  性能:由于线程安全的特性,StringBuffer的性能通常比StringBuilder稍差,尤其是在单线程环境下。

  适用场景:适用于多线程环境下需要修改字符串的情况,虽然性能上可能不如非线程安全的StringBuilder,但提供了安全保证。

总结来说,选择哪个类取决于具体的应用场景:如果字符串不需要修改或者在多线程环境下操作,应优先考虑String或StringBuffer;而对于单线程环境下的字符串操作,特别是需要频繁修改时,StringBuilder是更好的选择。

全文总结

    总之,对字符串的使用在Java中是极其重要和广泛使用的,掌握其特性和操作方法对于开发高质量的Java应用程序至关重要。如有错误或有补充请指出。天行健,君子以自强不息!

;