欢迎光临散文网 会员登陆 & 注册

Java十八篇:常用类 String

2023-03-05 19:38 作者:小刘Java之路  | 我要投稿


圣诞节快乐

概念

String 类继承自 Object 超类,实现的接口有:Serializable、CharSequence、Comparable接口,具体如下图:

img

字符串

什么是字符串?

如果直接按照字面意思来理解就是多个字符连接起来组合成的字符序列。为了更好的理解以上的理论,我们先来解释下字符序列,字符序列:把多个字符按照一定的顺序排列起来;而字符序列就是作为字符串的内容而存在的。所以可以把字符串理解为:把多个字符按照一定的顺序排列起来而构成的排列组合。

如果还是不好理解,没有关系,我还有法宝。我们可以用烤串来比喻说明,可以把字符串看作是烤串,烤串上的每一块肉都相当于是一个字符。把一块块肉按照肥瘦相间的顺序排列并串起来便成了我们吃的烤串,同理,把多个字符按照一定的顺序“串”起来就构成了字符串。

字符串的分类,字符串分为可变的字符串不可变的字符串两种;这里的不可变与可变指的是字符串的对象还是不是同一个,会不会因为字符串对象内容的改变而创建新的对象。

String类型是引用类型,可以通过 String s1 = "abc";直接赋值进行实例化,也可以通过new 关键字实例化,它也有自己的构造函数。

字符串:就是由多个字符组成的一串数据。也可以看成是一个字符数组。

通过API,我们可以知道

  • 字符串字面值“abc”也可以看成一个字符串对象。

  • 字符串是常量,一旦被赋值,就不能改变。

阅读String类定义的源码,你会发现:

public final class String
   implements java.io.Serializable, Comparable<String>, CharSequence {
   /** The value is used for character storage. */
   private final char value[];

   /** Cache the hash code for the string */
   private int hash; // Default to 0
   ...
}

1)String类被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。

2)String类实现了Serializable、CharSequence、 Comparable接口。

3)String实例的值是通过字符数组实现字符串存储的。

String 类

String类表示不可变的字符串,当前String类对象创建完毕之后,该对象的内容(字符序列)是不变的,因为内容一旦改变就会创建一个一个新的对象。

String对象的创建:

  • 方式一:通过字面量赋值创建,String s1 = “laofu”; 需要注意这里是双引号:“”,区别与字符char类型的单引号:‘’;

  • 方式二:通过构造器创建, String s2 = new String(“laofu”);

那问题来了,两种方式的对象在JVM中又是如何分布的呢? 分别有什么区别呢?

方式一和方式二在JVM中又是如何分布?

img

上图中的常量池:用于存储常量的地方内存区域,位于方法区中。常量池又分为编译常量池和运行常量池两种:

编译常量池:当把字节码加载斤JVM的时候,其中存储的是字节码的相关信息(如:行号等)。运行常量池:其中存储的是代码中的常量数据。

方式一和方式二有何不同?

方式一:String s1 = “laofu”; 有可能只创建一个String对象,也有可能创建不创建String对象;如果在常量池中已经存在”laofu”,那么对象s1会直接引用,不会创建新的String对象;否则,会先在常量池先创建常量”laofu”的内存空间,然后再引用。

方式二:String s2 = new String(“laofu”); 最多会创建两个String对象,最少创建一个String对象。可使用new关键字创建对象是会在堆空间创建内存区域,这是第一个对象;然后对象中的字符串字面量可能会创建第二个对象,而第二个对象如方式一中所描述的那样,是有可能会不被创建的,所以至少创建一个String个对象。

字符串的本质,字符串在底层其实就是char[],char表示一个字符,比如:

String str = "laofu"; 等价于 char[] cs = new char[]{'l','a','o','f','u'};

img

String对象的空值:

1、对象引用为空,即:String s1 = null; 此时s1没有初始化,也在JVM中没有分配内存空间。

2、对象内容为空字符串, 比如:String s2 = “”; 此时对象s2已经初始化,值为“”,JVM已经为其分配内存空间。字符串的比较:使用“==”和“equals”会有不同效果,详情在之前的文章中分享过:Java面向对象之类、对象、static成员#对象比较操作

使用”==”号:用于比较对象引用的内存地址是否相同。使用equals方法:在Object类中和”==”号相同,但在自定义类中,建议覆盖equals方法去实现比较自己内容的细节;由于String类覆盖已经覆盖了equals方法,所以其比较的是字符内容。

img

所以可以这样来判断字符串非空:

对象引用不能为空:s1 != null;,字符内容不能为空字符串(“”):“”.equals(s1);如果上述两个条件都满足,说明字符串确实为空!

**字符串拼接:**Java中的字符串可以通过是“+”实现拼接,那么代码中字符串拼接在JVM中又是如何处理的呢?我们通过一个例子说明:通过比较拼接字符串代码编译前后的代码来查看JVM对字符串拼接的处理。

img

通过上述例子不难发现,JVM会对字符串拼接做一些优化操作,如果字符串字面量之间的拼接,无论有多少个字符串,JVM都会一样的处理;如果是对象之间拼接,或者是对象和字面量之间的拼接,亦或是方法执行结果参与拼接,String内部会使用StringBuilder先来获取对象的值,然后使用append方法来执行拼接。由此可以总结得出:

1、使用字符串字面量创建的字符串,也就是单独使用""引号创建的字符串都是直接量,在编译期就会将其存储到常量池中;

2、使用new String("")创建的对象会存储到堆内存中,在运行期才创建;

3、使用只包含直接量的字符串连接符如"aa" + "bb"创建的也是直接量,这样的字符串在编译期就能确定,所以也会存储到常量池中;

4、使用包含String直接量的字符串表达式(如"aa" + s1)创建的对象是运行期才创建的,对象存储在堆中,因为其底层是创新了StringBuilder对象来实现拼接的;

5、无论是使用变量,还是调用方法来连接字符串,都只能在运行期才能确定变量的值和方法的返回值,不存在编译优化操作。

String字符串的常用方法。

String类位于java.lang包中,java会默认的导入java.lang包,所以我们使用java.lang包的类时,不需要显示的import类,String类具有丰富的方法,其中比较常用的有:

  1. 计算字符串的长度

  2. 连接字符串

  3. 比较字符串

  4. 提取字符串

  5. 替换字符串

img

案例:

**1、计算字符串的长度比较简单。**代码如下:

String email = "abc@tom.com  ";
System.out.println(email.length());
System.out.println(email.trim().length());
String abc="你好";
System.out.println(abc.length());
第一个输出是:13。
第二个输出是:11。
第三个输出是:2。

因为空格也算是字符串的长度,如果你不想计算前后的空格,也也可以使用trim()方法把前后的空格去了。在计算长度。第三个输出为2,可见java对中文的处理和英文也是一样计算的。并不存在一个中文等于两个英文符号的长度。

2、连接字符串。

连接字符串有两种方法:方法1:使用“+”。方法2:使用String类的concat()方法。代码如下:

System.out.println(email+"abc");
System.out.println(email.concat("abc"));

上面两个代码的输出都是一样的:

abc@tom.com abc

abc@tom.com abc

3、比较字符串

字符串的比较方法有很多,如比较两个字符串是否相等,比较后返回值是个布尔值。

String email2="cc@tom.com";
if(email.equals(email2)){
           System.out.println("相等");
}else{
           System.out.println("不相等");
}

结果是不相等。

字符串的比较是提取每一个字符逐个比较的

img

而且这种比较是区分大小写的。所以

String str1 = "java";
       String str2 = "Java";
       System.out.println(str1.equals(str2));

返回为false,如果你想不区分大小写的比较,那么你可以使用不区分大小写的比较方法或者把字符转为为大写(也就可以小写)后再比较。

System.out.println(str1.equalsIgnoreCase(str2));
System.out.println(str1.toLowerCase().equals(str2.toLowerCase()));
System.out.println(str1.toUpperCase().equals(str2.toUpperCase()));

我们还可以判断字符串是否已某个字符串结尾或者开头

System.out.println(email.startsWith("abc"));
System.out.println(email.endsWith("com"));

4、提取字符串

要提取字符串,我们首先得学会如何查询字符串。

img

注意下标也是从0开始的。

System.out.println(email.indexOf("a"));//从零开始,所以这里是0
System.out.println(email.indexOf("c"));//这个是2,从左到右找,返回第一个找到的。
System.out.println(email.indexOf("zz"));//找不到返回是-1
System.out.println(email.lastIndexOf("c"));//这个是8,从右到左找,返回第一个找到的。
下面是截取字符串
System.out.println(email.substring(3));//从第4个(包括第四个)开始截取到最后
System.out.println(email.substring(3, 5));

indexOf方法和substring方法经常一起使用,比如我们要截取@之后的所有字符串,我们可以结合这个两个。

System.out.println(email.substring(email.indexOf("@")+1));

5、替换字符串

例如我们可以把所有的字符串c换成字符串z

System.out.println(email.replace("c", "z"));

通过源码来理解string类

一、String类

想要了解一个类,最好的办法就是看这个类的实现源代码,来看一下String类的源码:

public final class String
   implements java.io.Serializable, Comparable<String>, CharSequence
{
   /** The value is used for character storage. */
   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. */
   private final int count;

   /** Cache the hash code for the string */
   private int hash; // Default to 0

   /** use serialVersionUID from JDK 1.0.2 for interoperability */
   private static final long serialVersionUID = -6849794470754667710L;

   ........
}

从上面可以看出几点:

1)String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。

2)上面列举出了String类中所有的成员属性,从上面可以看出String类其实是通过char数组来保存字符串的。

下面再继续看String类的一些方法实现:

public String substring(int beginIndex, int endIndex) {
   if (beginIndex < 0) {
       thrownew StringIndexOutOfBoundsException(beginIndex);
   }
   if (endIndex > count) {
       thrownew StringIndexOutOfBoundsException(endIndex);
   }
   if (beginIndex > endIndex) {
       thrownew StringIndexOutOfBoundsException(endIndex - beginIndex);
   }
   return ((beginIndex == 0) && (endIndex == count)) ? this :
       newString(offset + beginIndex, endIndex - beginIndex, value);
}

public String concat(String str) {
   int otherLen = str.length();
   if (otherLen == 0) {
       returnthis;
   }
   char buf[] = new char[count + otherLen];
   getChars(0, count, buf, 0);
   str.getChars(0, otherLen, buf, count);
   returnnewString(0, count + otherLen, buf);
}

public String replace(char oldChar, char newChar) {
   if (oldChar != newChar) {
       int len = count;
       int i = -1;
       char[] val = value; /* avoid getfield opcode */
       int off = offset;   /* avoid getfield opcode */

       while (++i < len) {
       if (val[off + i] == oldChar) {
           break;
       }
       }
       if (i < len) {
       char buf[] = new char[len];
       for (int j = 0 ; j < i ; j++) {
           buf[j] = val[off+j];
       }
       while (i < len) {
           char c = val[off + i];
           buf[i] = (c == oldChar) ? newChar : c;
           i++;
       }
       returnnewString(0, len, buf);
       }
   }
   returnthis;
}

从上面的三个方法可以看出,无论是sub操、concat还是replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。

在这里要永远记住一点:“String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”。

二、字符串常量池

我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串(这点对理解上面至关重要)。

Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

来看下面的程序:

String a = "chenssy";
String b = "chenssy";

a、b和字面上的chenssy都是指向JVM字符串常量池中的"chenssy"对象,他们指向同一个对象。

String c = newString("chenssy");

new关键字一定会产生一个对象chenssy(注意这个chenssy和上面的chenssy不同),同时这个对象是存储在堆中。所以上面应该产生了两个对象:保存在栈中的c和保存堆中chenssy。但是在Java中根本就不存在两个完全一模一样的字符串对象。故堆中的chenssy应该是引用字符串常量池中chenssy。所以c、chenssy、池chenssy的关系应该是:c--->chenssy--->池chenssy。整个关系如下:

img

通过上面的图我们可以非常清晰的认识他们之间的关系。所以我们修改内存中的值,他变化的是所有。

一道面试题:

public class StringDemo{
 private static final String MESSAGE="taobao";
 public staticvoid main(String [] args) {
   String a ="tao"+"bao";
   String b="tao";
   String c="bao";
   System.out.println(a==MESSAGE);
   System.out.println((b+c)==MESSAGE);
 }
}

对于这道题,考察的是对String类型的认识以及编译器优化。Java中String不是基本类型,但是有些时候和基本类型差不多,如String b = “tao” ; 可以对变量直接赋值,而不用 new 一个对象(当然也可以用 new)。所以String这个类型值得好好研究下。

Java中的变量和基本类型的值存放于栈内存,而new出来的对象本身存放于堆内存,指向对象的引用还是存放在栈内存。例如如下的代码:

int  i=1;
String s =  newString( "Hello World" );

变量i和s以及1存放在栈内存,而s指向的对象”Hello World”存放于堆内存。

img

栈内存的一个特点是数据共享,这样设计是为了减小内存消耗,前面定义了i=1,i和1都在栈内存内,如果再定义一个j=1,此时将j放入栈内存,然后查找栈内存中是否有1,如果有则j指向1。如果再给j赋值2,则在栈内存中查找是否有2,如果没有就在栈内存中放一个2,然后j指向2。也就是如果常量在栈内存中,就将变量指向该常量,如果没有就在该栈内存增加一个该常量,并将变量指向该常量。

img

如果j++,这时指向的变量并不会改变,而是在栈内寻找新的常量(比原来的常量大1),如果栈内存有则指向它,如果没有就在栈内存中加入此常量并将j指向它。这种基本类型之间比较大小和我们逻辑上判断大小是一致的。如定义i和j是都赋值1,则i==j结果为true。==用于判断两个变量指向的地址是否一样。i==j就是判断i指向的1和j指向的1是同一个吗?当然是了。对于直接赋值的字符串常量(如String s=“Hello World”;中的Hello World)也是存放在栈内存中,而new出来的字符串对象(即String对象)是存放在堆内存中。如果定义String s=“Hello World”和String w=“Hello World”,s==w吗?肯定是true,因为他们指向的是同一个Hello World。

img

堆内存没有数据共享的特点,前面定义的String s = new String( “Hello World” );后,变量s在栈内存内,Hello World 这个String对象在堆内存内。如果定义String w = new String( “Hello World” );,则会在堆内存创建一个新的String对象,变量w存放在栈内存,w指向这个新的String对象。堆内存中不同对象(指同一类型的不同对象)的比较如果用==则结果肯定都是false,比如s==w?当然不等,s和w指向堆内存中不同的String对象。如果判断两个String对象相等呢?用equals方法。

img


说了这么多只是说了这道题的铺垫知识,还没进入主题,下面分析这道题。MESSAGE 成员变量及其指向的字符串常量肯定都是在栈内存里的,变量 a 运算完也是指向一个字符串“ taobao ”啊?是不是同一个呢?这涉及到编译器优化问题。对于字符串常量的相加,在编译时直接将字符串合并,而不是等到运行时再合并。也就是说String a = “tao” + “bao” ;和String a = “taobao” ;编译出的字节码是一样的。所以等到运行时,根据上面说的栈内存是数据共享原则,a和MESSAGE指向的是同一个字符串。而对于后面的(b+c)又是什么情况呢?b+c只能等到运行时才能判定是什么字符串,编译器不会优化,想想这也是有道理的,编译器怕你对b的值改变,所以编译器不会优化。运行时b+c计算出来的"taobao"和栈内存里已经有的"taobao"是一个吗?

不是。b+c计算出来的"taobao"应该是放在堆内存中的String对象。这可以通过System. out .println( (b+c)== MESSAGE );的结果为false来证明这一点。如果计算出来的b+c也是在栈内存,那结果应该是true。Java对String的相加是通过StringBuffer实现的,先构造一个StringBuffer里面存放”tao”,然后调用append()方法追加”bao”,然后将值为”taobao”的StringBuffer转化成String对象。StringBuffer对象在堆内存中,那转换成的String对象理所应当的也是在堆内存中。下面改造一下这个语句System. out .println( (b+c).intern()== MESSAGE );结果是true, intern() 方法会先检查 String 池 ( 或者说成栈内存 ) 中是否存在相同的字符串常量,如果有就返回。所以 intern()返回的就是MESSAGE指向的"taobao"。再把变量b和c的定义改一下,

nfinal  String b =  "tao" ;
final  String c =  "bao" ;
System. out .println( (b+c)== MESSAGE );

现在b和c不可能再次赋值了,所以编译器将b+c编译成了”taobao”。因此,这时的结果是true。

在字符串相加中,只要有一个是非final类型的变量,编译器就不会优化,因为这样的变量可能发生改变,所以编译器不可能将这样的变量替换成常量。例如将变量b的final去掉,结果又变成了false。这也就意味着会用到StringBuffer对象,计算的结果在堆内存中。

如果对指向堆内存中的对象的String变量调用intern()会怎么样呢?实际上这个问题已经说过了,(b+c).intern(),b+c的结果就是在堆内存中。对于指向栈内存中字符串常量的变量调用intern()返回的还是它自己,没有多大意义。它会根据堆内存中对象的值,去查找String池中是否有相同的字符串,如果有就将变量指向这个string池中的变量。

String a = "tao"+"bao";
String b = newString("taobao");

System.out.println(a==MESSAGE); //true
System.out.println(b==MESSAGE);  //false

b = b.intern();
System.out.println(b==MESSAGE); //true
System.out.println(a==a.intern());  //true



圣诞节快乐


总结:

  1. String 类继承自 Object 超类,实现的接口有:Serializable、CharSequence、Comparable接口

  2. 字符串的分类,字符串分为可变的字符串不可变的字符串两种

  3. String类型是引用类型,可以通过 String s1 = "abc";直接赋值进行实例化,也可以通过new 关键字实例化,它也有自己的构造函数。

  4. 创建对象的方式一:通过字面量赋值创建,String s1 = “laofu”; 需要注意这里是双引号:“”,区别与字符char类型的单引号:‘’;

    方式二:通过构造器创建, String s2 = new String(“laofu”);

  5. 常用的方法:

    1. 计算字符串的长度

    2. 连接字符串

      3.比较字符串

      4.提取字符串

      5.替换字符串

  6. 对象在jvm的位置和在常量池中的位置

  7. 在项目中对字符串的使用很常见,但是不要忘记了它是引用类型,不是基本类型不可以做存储和数据类型操作

  8. 面试中也有很多关于string 的题型,比如==和equals()、++或者-- 等,好好敲代码,掌握好string。



Java十八篇:常用类 String的评论 (共 条)

分享到微博请遵守国家法律