String类详解

String类详解前言在我们开发中经常会用到很多的常用的工具类,这里做一个总结。他们有很多的方法都是我们经常要用到的。所以我们一定要把它好好的掌握起来!JavaStringAPI1.获取:intlength():获取字符串的长度。charcharAt(intindex)根据位置获取该位置上某个

大家好,欢迎来到IT知识分享网。

前言

  在我们开发中经常会用到很多的常用的工具类,这里做一个总结。他们有很多的方法都是我们经常要用到的。所以我们一定要把它好好的掌握起来!

Java String API

1.获取:

int  length():获取字符串的长度。
char  charAt( int  index )根据位置获取该位置上某个字符:
根据字符获取该字符在字符串中的位置:

int  indexOf(int ch):返回的是ch在字符串中第一次出现的的位置。

int  indexOf(int ch,int fromIndex):从fromIndex指定位置开始,获取ch在字符串中出现的位置。

int  indexOf(String str):返回的是str在字符串中第一次出现的的位置。如果返回-1,表示该str不在字符串中存在。即用该方法就可以判断是否包含某一个字符串。而且可以返回出现的位置。

int  indexOf(String str,int fromIndex):从fromIndex指定位置开始,获取str在字符串中出现的位置。

int  lastIndexOf(xx,xx):里面的参数与上面的4中方法一致。但是是从后面开始查找。      

2.判断:

 boolean  contains(str):字符串中是否包含某一个字符串。

特例indexOf(str):可以索引str第一次出现位置,如果返回-1,表示该str不在字符串中存在。即用该方法就可以判断是否包含某一个字符串。而且可以返回出现的位置。

boolean  isEmpty():字符中是否有内容。

boolean  startsWith(str):字符串是否是以指定内容开头。

boolean  endsWith(str):字符串是否是以指定内容结尾。

boolean  equals(str):判断字符串内容是否相等。复写了Object类中的equals()方法。

boolean  equalsIgnoreCase(str):判断字符串内容是否相同,并忽略大小写。


3.转换: 将字符数组转换成字符串。

构造函数String(char [])  将字符数组转换成字符串。
String(char [],offset,count)  将字符数组中的一部分转换成字符串。

静态方法:static copyValueOf(char [] data)

static copyValueOf(char [] data,int  offset , int count)         

将字符串转换成字符数组。**

char []  toCharArray();

将字节数组转换成字符串。

构造函数String(byte []) 将字节数组转换成字符串。

String(byte [],offset,count)  将字节数组中的一部分转换成字符串。

将字符串转换成字符数组。

 byte []  getBytes();

将基本数据类型转换成字符串。

 static String valueOf(基本数据类型)

特殊注意:字符串和字节数组在转换过程中,是可以指定编码表的。

4.替换

String replace(oldChar,newChar); 如果要替换的字符不存在,则返回的还是原串。单引号。

String replace(oldCharSequence,newCharSequence); 如果要替换字符串不存在,返回原串。双引号。

5切割

 String [] split(regex); 参数为指定的字符串。

子串:

 String substring(begin);  获取字符串中的一部分。从指定的位置开始。

String substring(begin,end);  获取字符串中的一部分。从指定的位置开始,到指定的位置前一位结束。

其他:

String toUpperCase():将字符串转换成大写或小写。

String toLowerCase():

String trim():将字符串两端的多个空格去除。

int compareTo(String )对两个字符串进行自然顺序的比较。

一、String简介

1.1、String(字符串常量)概述

  在API中是这样描述:

    String 类代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。
    字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。

  java.lang.String:

    img

1.2、分析String源码

  1)String的成员变量

img

[复制代码](javascript:void(0)😉

 /** String的属性值 */  
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    /**数组被使用的开始位置**/
    private final int offset;

    /** The count is the number of characters in the String. */
    /**String中元素的个数**/
    private final int count;

    /** Cache the hash code for the string */
   /**String类型的hash值**/
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
    /**
     * Class String is special cased within the Serialization Stream         Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */

  private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

[复制代码](javascript:void(0)😉

    从源码看出String底层使用一个字符数组来维护的。

    成员变量可以知道String类的值是final类型的,不能被改变的,所以只要一个值改变就会生成一个新的String类型对象,存储String数据也不一定从数组的第0个元素开始的,而是从offset所指的元素开始。

  2)String的构造方法  

[复制代码](javascript:void(0)😉

String() 
          初始化一个新创建的 String 对象,使其表示一个空字符序列。 
String(byte[] bytes) 
          通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。 
String(byte[] bytes, Charset charset) 
          通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。  
String(byte[] bytes, int offset, int length) 
          通过使用平台的默认字符集解码指定的 byte 子数组,构造一个新的 String。 
String(byte[] bytes, int offset, int length, Charset charset) 
          通过使用指定的 charset 解码指定的 byte 子数组,构造一个新的 String。 
String(byte[] bytes, int offset, int length, String charsetName) 
          通过使用指定的字符集解码指定的 byte 子数组,构造一个新的 String。 
String(byte[] bytes, String charsetName) 
          通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。 
String(char[] value) 
          分配一个新的 String,使其表示字符数组参数中当前包含的字符序列。 
String(char[] value, int offset, int count) 
          分配一个新的 String,它包含取自字符数组参数一个子数组的字符。 
String(int[] codePoints, int offset, int count) 
          分配一个新的 String,它包含 Unicode 代码点数组参数一个子数组的字符。 
String(String original) 
          初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。 
String(StringBuffer buffer) 
          分配一个新的字符串,它包含字符串缓冲区参数中当前包含的字符序列。 
String(StringBuilder builder) 
          分配一个新的字符串,它包含字符串生成器参数中当前包含的字符序列。 

[复制代码](javascript:void(0)😉

二、创建字符串对象两种方式的区别

2.1、直接赋值方式创建对象

  直接赋值方式创建对象是在方法区的常量池

String str="hello";//直接赋值的方式

2.2、通过构造方法创建字符串对象

  通过构造方法创建字符串对象是在堆内存

String str=new String("hello");//实例化的方式

2.3、两种实例化方式的比较

  1)编写代码比较

[复制代码](javascript:void(0)😉

public class TestString {
    public static void main(String[] args) {
        String str1 = "Lance";
        String str2 = new String("Lance");
        String str3 = str2; //引用传递,str3直接指向st2的堆内存地址
        String str4 = "Lance";
        /**
         *  ==:
         * 基本数据类型:比较的是基本数据类型的值是否相同
         * 引用数据类型:比较的是引用数据类型的地址值是否相同
         * 所以在这里的话:String类对象==比较,比较的是地址,而不是内容
         */
         System.out.println(str1==str2);//false
         System.out.println(str1==str3);//false
         System.out.println(str3==str2);//true
         System.out.println(str1==str4);//true
    }

}

[复制代码](javascript:void(0)😉

  2)内存图分析

    img

    可能这里还是不够明显,构造方法实例化方式的内存图:String str = new String(“Hello”);

    首先:

      img

    当我们再一次的new一个String对象时:

      img

    3)字符串常量池

      在字符串中,如果采用直接赋值的方式(String str=”Lance”)进行对象的实例化,则会将匿名对象“Lance”放入对象池,每当下一次对不同的对象进行直接赋值的时候会直接利用池中原有的匿名对象,

      这样,所有直接赋值的String对象,如果利用相同的“Lance”,则String对象==返回true;

      比如:对象手工入池

[复制代码](javascript:void(0)😉

public class TestString {
    public static void main(String args[]){
     String str =new String("Lance").intern();//对匿名对象"hello"进行手工入池操作
     String str1="Lance";
     System.out.println(str==str1);//true
    }
}

[复制代码](javascript:void(0)😉

    4)总结:两种实例化方式的区别

      1)直接赋值(String str = “hello”):只开辟一块堆内存空间,并且会自动入池,不会产生垃圾。

      2)构造方法(String str= new String(“hello”);):会开辟两块堆内存空间,其中一块堆内存会变成垃圾被系统回收,而且不能够自动入池,需要通过public String intern();方法进行手工入池。

        在开发的过程中不会采用构造方法进行字符串的实例化。

    5)避免空指向

      首先了解: == 和public boolean equals()比较字符串的区别

      ==在对字符串比较的时候,对比的是内存地址,而equals比较的是字符串内容,在开发的过程中,equals()通过接受参数,可以避免空指向。

      举例:

[复制代码](javascript:void(0)😉

      String str = null;
      if(str.equals("hello")){//此时会出现空指向异常
        ...
      }
      if("hello".equals(str)){//此时equals会处理null值,可以避免空指向异常
         ...
      }

[复制代码](javascript:void(0)😉

   6)String类对象一旦声明则不可以改变;而改变的只是地址,原来的字符串还是存在的,并且产生垃圾

     img

三、String常用的方法

  img

3.1、String的判断功能

  1)常用方法

  boolean equals(Object obj):比较字符串的内容是否相同
  boolean equalsIgnoreCase(String str): 比较字符串的内容是否相同,忽略大小写
  boolean startsWith(String str): 判断字符串对象是否以指定的str开头
  boolean endsWith(String str): 判断字符串对象是否以指定的str结尾

  2)代码测试

img

public class TestString {
    public static void main(String[] args) {
           // 创建字符串对象
        String s1 = "hello";
        String s2 = "hello";
        String s3 = "Hello";
 
        // boolean equals(Object obj):比较字符串的内容是否相同
        System.out.println(s1.equals(s2));
        System.out.println(s1.equals(s3));
        System.out.println("-----------");
 
        // boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
        System.out.println(s1.equalsIgnoreCase(s2));
        System.out.println(s1.equalsIgnoreCase(s3));
        System.out.println("-----------");
 
        // boolean startsWith(String str):判断字符串对象是否以指定的str开头
        System.out.println(s1.startsWith("he"));
        System.out.println(s1.startsWith("ll"));
    }

}

测试

    结果:

    img

3.2、String类的获取功能

  1)常用方法

  int length():获取字符串的长度,其实也就是字符个数
  char charAt(int index):获取指定索引处的字符
  int indexOf(String str):获取str在字符串对象中第一次出现的索引
  String substring(int start):从start开始截取字符串
  String substring(int start,int end):从start开始,到end结束截取字符串。包括start,不包括end

  2)代码测试

img

public class TestString {
    public static void main(String[] args) {
        String str1 = "Lance";
        String str2 = new String("Lance");
        String str3 = "LANCE";
        
        // boolean equals(Object obj):比较字符串的内容是否相同
        System.out.println(str1.equals(str2));
        System.out.println(str1.equals(str3));
        System.out.println("-----------------------");
        
        // boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
        System.out.println(str1.equalsIgnoreCase(str3));
        System.out.println("-----------------------");
        
        // boolean startsWith(String str):判断字符串对象是否以指定的str开头
        // boolean endsWith(String str): 判断字符串对象是否以指定的str结尾
        System.out.println(str1.startsWith("La"));
        System.out.println(str3.endsWith("CE"));
        
    }

}
  结果:

    

3.2、String类的获取功能

  1)常用方法

 int length():获取字符串的长度,其实也就是字符个数
 char charAt(int index):获取指定索引处的字符
 int indexOf(String str):获取str在字符串对象中第一次出现的索引
 String substring(int start):从start开始截取字符串
 String substring(int start,int end):从start开始,到end结束截取字符串。包括start,不包括end
  2)测试代码

public class TestString {
    public static void main(String[] args) {
          // 创建字符串对象
        String s = "helloworld";
 
        // int length():获取字符串的长度,其实也就是字符个数
        System.out.println(s.length());
        System.out.println("--------");
 
        // char charAt(int index):获取指定索引处的字符
        System.out.println(s.charAt(0));
        System.out.println(s.charAt(1));
        System.out.println("--------");
 
        // int indexOf(String str):获取str在字符串对象中第一次出现的索引
        System.out.println(s.indexOf("l"));
        System.out.println(s.indexOf("owo"));
        System.out.println(s.indexOf("ak"));
        System.out.println("--------");
 
        // String substring(int start):从start开始截取字符串
        System.out.println(s.substring(0));
        System.out.println(s.substring(5));
        System.out.println("--------");
 
        // String substring(int start,int end):从start开始,到end结束截取字符串
        System.out.println(s.substring(0, s.length()));
        System.out.println(s.substring(3, 8));
        
    }

}

测试

  结果:

    img

3.3、String的转换功能

  1)常用方法

  char[] toCharArray():把字符串转换为字符数组
  String toLowerCase():把字符串转换为小写字符串
  String toUpperCase():把字符串转换为大写字符串

  2)核心代码

img

public class TestString {
    public static void main(String[] args) {
         // 创建字符串对象
        String s = "abcde";
 
        // char[] toCharArray():把字符串转换为字符数组
        char[] chs = s.toCharArray();
        for (int x = 0; x < chs.length; x++) {
            System.out.println(chs[x]);
        }
 
        System.out.println("-----------");
 
        // String toLowerCase():把字符串转换为小写字符串
        System.out.println("HelloWorld".toLowerCase());
        // String toUpperCase():把字符串转换为大写字符串
        System.out.println("HelloWorld".toUpperCase());
        
    }

}

测试

  结果:

    img

  注意:  

    字符串的遍历有两种方式:一是ength()加上charAt()。二是把字符串转换为字符数组,然后遍历数组。

3.4、其他常用方法

  1)常用方法

  去除字符串两端空格:String trim()
  按照指定符号分割字符串:String[] split(String str)

  2)核心代码

img

public class TestString {
    public static void main(String[] args) {
          // 创建字符串对象
        String s1 = "helloworld";
        String s2 = " helloworld ";
        String s3 = " hello world ";
        System.out.println("---" + s1 + "---");
        System.out.println("---" + s1.trim() + "---");
        System.out.println("---" + s2 + "---");
        System.out.println("---" + s2.trim() + "---");
        System.out.println("---" + s3 + "---");
        System.out.println("---" + s3.trim() + "---");
        System.out.println("-------------------");
 
        // String[] split(String str)
        // 创建字符串对象
        String s4 = "aa,bb,cc";
        String[] strArray = s4.split(",");
        for (int x = 0; x < strArray.length; x++) {
            System.out.println(strArray[x]);
        }
    }

}

测试

  结果:

      img

四、String的不可变性

当我们去阅读源代码的时候,会发现有这样的一句话:

  img

意思就是说:String是个常量,从一出生就注定不可变。我们就要明白一下几个问题

(1)什么是不可变对象?

(2)String如何被设计成不可变对象的?

(3)有什么办法能够改变String?

(4)JAVA语言为什么把String类型设计成不可变?

带着这些问题就可以开始今天的文章了。

一、什么是不可变对象

从字面意思也能够理解,也就是我们的创建的对象不可改变。那什么是不可变呢?为了实现创建的对象不可变,java语言要求我们需要遵守以下5条规则:

(1)类内部所有的字段都是final修饰的。

(2)类内部所有的字段都是私有的,也就是被private修饰。

(3)类不能够被集成和拓展。

(4)类不能够对外提供哪些能够修改内部状态的方法,setter方法也不行。

(5)类内部的字段如果是引用,也就是说可以指向可变对象,那我们程序员不能获取这个应用。

正是由于我们的String类型遵循了上面5条规则,所以才说String对象是不可变的。想要去了解他还是看看String类型内部长什么样子再来看上面5条规则吧。

二、String如何被设计成不可变对象的

1、疑惑一

在看之前,我们先给出一个疑惑问题,我们看下面的代码,

img

在文章一开始我们就说了,String对象是不可变的,这里a=张三,然后a=李四,这符合String的不可变性嘛?答案是当然符合。

img

从上面这张图我们可以看到,在第一次String a=”张三”的时候,在堆中创建了同一个对象“张三”。后来我们在执行a=”李四”的时候再内存中又创建了一个对象“李四”。也就是说我们的a仅仅只是改变了引用a指向的地址而已。

2、源码解释疑惑

既然a指向的引用地址改变了,那么其String内部肯定有一个变量,能够指向不同的实际对象,想要进一步弄清楚我们就进入其String的内部来看看。

我们在这里主要通过String类的源码来分析,看一下Java语言是如何设计,能把String类型设计成不可变的。这里给出的是jdk1.8的一部分源码。

img

上面最主要的是两个字段:value和hash。我们在这里主要是看value数组,hash和主题无关所以这里不再讲解了,我有专门的文章介绍hash。

我们的String对象其实在内部就是一个个字符然后存储在这个value数组里面的。但是value对外没有setValue的方法,所以整个String对象在外部看起来就是不可变的。我们画一张图解释一下上面的疑惑

img

现在明白了吧,也就是说真正改变引用的是value,因为value也是一个数组引用。这也可以很方便的解释下一个疑惑问题了。

3、疑惑二

既然我们的String是不可变的,好像内部还有很多substring, replace, replaceAll这些操作的方法。好像都是对String对象改变了,解释起来也很简单,我们每次的replace这些操作,其实就是在堆内存中创建了一个新的对象。然后我们的value指向不同的对象罢了。

面试的时候我们只是解释上面的原因其实不是那么尽善尽美,想要更好的去加薪去装逼,我们还需更进一步回答。

三、有什么办法能够改变String

既然有这个标题。那肯定就是有办法的,别忘了我们的反射机制,在通常情况下,他可以做出一些违反语言设计原则的事情。这也是一个技巧,每当面试官问一些违反语言设计原则的问题,你就可以拿反射来反驳他。下面我们来看一下:

img

我们可以通过反射来改变String。

现在我们知道它的原理以及用法,也知道可以通过反射来改变String,还有一个问题我们没有弄清楚,面试的时候你也可以反问他,来进一步提升自己的逼格。

四、JAVA语言为什么把String类型设计成不可变

这里有几个特点。

第一:在Java程序中String类型是使用最多的,这就牵扯到大量的增删改查,每次增删改差之前其实jvm需要检查一下这个String对象的安全性,就是通过hashcode,当设计成不可变对象时候,就保证了每次增删改查的hashcode的唯一性,也就可以放心的操作。

第二:网络连接地址URL,文件路径path通常情况下都是以String类型保存, 假若String不是固定不变的,将会引起各种安全隐患。就好比我们的密码不能以String的类型保存,,如果你将密码以明文的形式保存成字符串,那么它将一直留在内存中,直到垃圾收集器把它清除。而由于字符串被放在字符串缓冲池中以方便重复使用,所以它就可能在内存中被保留很长时间,而这将导致安全隐患

第三:字符串值是被保留在常量池中的,也就是说假若字符串对象允许改变,那么将会导致各种逻辑错误

OK,以上就是String类型对象不可变的原因。

4.1、前言

  了解一个经典的面试题:

[复制代码](javascript:void(0)😉

public class Apple {
    public static void main(String[] args) {
        String a = "abc";
        String b = "abc";
        String c = new String("abc");
        System.out.println(a==b);  //true
        System.out.println(a.equals(b));  //true
        System.out.println(a==c);  //false
        System.out.println(a.equals(c));  //true
    }
}

[复制代码](javascript:void(0)😉

  内存图:

    img

4.2、分析

  因为String太过常用,JAVA类库的设计者在实现时做了个小小的变化,即采用了享元模式,每当生成一个新内容的字符串时,他们都被添加到一个共享池中,当第二次再次生成同样内容的字符串实例时,

  就共享此对象,而不是创建一个新对象,但是这样的做法仅仅适合于通过=符号进行的初始化。  

  需要说明一点的是,在object中,equals()是用来比较内存地址的,但是String重写了equals()方法,用来比较内容的,即使是不同地址,只要内容一致,也会返回true,这也就是为什么a.equals(c)返回true的原因了。

4.3、String不可变的好处

  可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销。

  我们的程序中大量使用了String字符串,有可能是出于安全性考虑。

  大家都知道HashMap中key为String类型,如果可变将变的多么可怕。

  当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。

五、字符串常量池

5.1、字符串常量池概述

  1)常量池表(Constant_Pool table)

    Class文件中存储所有常量(包括字符串)的table。
    这是Class文件中的内容,还不是运行时的内容,不要理解它是个池子,其实就是Class文件中的字节码指令。

  2)运行时常量池(Runtime Constant Pool) 

    JVM内存中方法区的一部分,这是运行时的内容
    这部分内容(绝大部分)是随着JVM运行时候,从常量池转化而来,每个Class对应一个运行时常量池
    上一句中说绝大部分是因为:除了 Class中常量池内容,还可能包括动态生成并加入这里的内容

  3)字符串常量池(String Pool)

    这部分也在方法区中,但与Runtime Constant Pool不是一个概念,String Pool是JVM实例全局共享的,全局只有一个
    JVM规范要求进入这里的String实例叫“被驻留的interned string”,各个JVM可以有不同的实现,HotSpot是设置了一个哈希表StringTable来引用堆中的字符串实例,被引用就是被驻留。

5.2、享元模式

  其实字符串常量池这个问题涉及到一个设计模式,叫“享元模式”,顾名思义 – – – > 共享元素模式
  也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素
  Java中String部分就是根据享元模式设计的,而那个存储元素的地方就叫做“字符串常量池 – String Pool”

5.3、详细分析

  举例:

int x  = 10;
String y = "hello";

  1)首先,10"hello"会在经过javac(或者其他编译器)编译过后变为Class文件中constant_pool table的内容

  2)当我们的程序运行时,也就是说JVM运行时,每个Classconstant_pool table中的内容会被加载到JVM内存中的方法区中各自Class的Runtime Constant Pool。

  3)一个没有被String Pool包含的Runtime Constant Pool中的字符串(这里是”hello”)会被加入到String Pool中(HosSpot使用hashtable引用方式),步骤如下:   

    一是:在Java Heap中根据”hello”字面量create一个字符串对象
    二是:将字面量”hello”与字符串对象的引用在hashtable中关联起来,键 – 值 形式是:”hello” = 对象的引用地址。

   另外来说,当一个新的字符串出现在Runtime Constant Pool中时怎么判断需不需要在Java Heap中创建新对象呢?

  策略是这样:会先去根据equals来比较Runtime Constant Pool中的这个字符串是否和String Pool中某一个是相等的(也就是找是否已经存在),如果有那么就不创建,直接使用其引用;反之,如上3

  如此,就实现了享元模式,提高的内存利用效率。

  举例:

      使用String s = new String(“hello”);会创建几个对象

      会创建2个对象

      首先,出现了字面量”hello”,那么去String Pool中查找是否有相同字符串存在,因为程序就这一行代码所以肯定没有,那么就在Java Heap中用字面量”hello”首先创建1个String对象。

      接着,new String(“hello”),关键字new又在Java Heap中创建了1个对象,然后调用接收String参数的构造器进行了初始化。最终s的引用是这个String对象.

      

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/30679.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信