软件构造----Java基础入门----字符串

字符串与不可变

不可变对象在创建后,它的内部状态会保持不变,这就意味着,一旦我们将一个对象分配给一个变量,就无法再通过任何方式更改对象的状态了。

那么,为什么Java中字符串是不可变的呢?
可以从四个方面说起,缓存、安全性、同步和高性能。

一)字符串常量池
字符串是Java中最常用的数据类型之一,所以,把字符串缓存起来,并且重复使用它们会节省大量堆空间(堆内存用来存储 Java 中的对象,无论是成员变量、局部变量,还是类变量,它们指向的对象都存储在堆内存中)

字符串常量池是 Java 虚拟机用来存储字符串的一个特殊的区域,由于字符串是不可变的,因此 Java 虚拟机可以在字符串常量池中只为同一个字符串存储一个字符串副本来节省空间。

字符串常量池只要有两种使用方法:
第一,直接使用双引号声明出来的字符串对象会直接存储在常量池中。
第二,可以使用 String 类提供的 intern() 方法强制将当前字符串放入常量池中——常量池中查询不到当前字符串。

String s1 = "沉默王二";
String s2 = "沉默王二";

System.out.println(s1 == s2);   // true

由于字符串常量池的存在,所以两个不同的变量都指向了池中同一个字符串对象,从而节省了稀缺的内存资源。如果是通过 new 关键字创建的对象,则需要新的堆空间。
在这里插入图片描述
二)安全性
字符串在 Java 应用程序中的使用范围非常广,几乎无处不在,比如说存储用户名、密码、数据库连接地址等等这些非常敏感的信息,因此,必须要保证 String 类的绝对安全性。
以下面这段代码为例:

void criticalMethod(String userName) {
    // 检查用户名是否合法
    if (!isAlphaNumeric(userName)) {
        throw new SecurityException(); 
    }

    // 初始化数据库连接
    initializeDatabase();

    // 准备修改用户状态
    connection.executeUpdate("UPDATE members SET status = 'active' " +
      " WHERE username = '" + userName + "'");
}

通常情况下,用户名由客户端传递到服务器端,服务器端接收后要先对用户名进行检查,再进行其他操作,因为客户端传递过来的信息不一定值得信任。

如果字符串是可变的,那么我们在执行 executeUpdate 更新数据库的时候,就有点不放心,因为即便是安全性检查通过了,字符串仍然有可能被修改。

在调用 isAlphaNumeric() 方法进行安全性检查期间,userName 的值仍然有可能被 criticalMethod() 方法的调用者进行篡改,就容易造成 SQL 注入。

但如果字符串是不可变的,这方面的担忧就不存在了。因为在执行更新之前,字符串的值是确定的,就是我们检查安全性之后的值。

三)线程安全
由于字符串是不可变的,因此可以在多线程之间共享,如果一个线程把字符串的值修改为另外一个,那么就会在字符串常量池中创建另外一个字符串,原有的字符串仍然会保持不变。 (臆测,不知真假)

四)哈希码

字符串广泛应用于 HashMap、HashTable、HashSet 等需要哈希码作为键的数据结构中,在对这些哈希表进行操作的时候,需要频繁调用 hashCode() 方法来获取键的哈希码。
由于字符串是不可变性,这就保证了键值的哈希值不会发生改变,因此在第一次调用 String 类的 hashCode() 方法时,就对哈希值进行了缓存,此后,就一直返回相同的值。
由于哈希值被缓存了,这在另外一种层面上提高了哈希表的访问性能,因为哈希值不用重新计算了。
而假如字符串是可变的,那就意味着哈希码会有多个,在通过键获取值的时候,就不一定能够获取到对的值了。

字符串的比较

符号==
不能用==来比较两个字符串是否相等,这个符号只能判断两个字符串是否放在同一个位置上!
当然,如果两个字符串放在同一个位置上,他们必然相等。但是,完全有可能将多个内容相同的字符串副本放置在不同的位置上!
如果虚拟机始终将相同的字符串共享,那么才能使用==来判断两个字符串是否相等。但实际上只有字符串字面量是共享的,而用+或者substring等操作获得的字符串不共享。

.equals(): 对于两个字符串s,t 可以用s.equals(t)的返回值True/False来判断。
忽略大小写的话,也有 s.equalsIgnoreCase(t)来判断。
以上s,t既可以是字符串变量,也可以是字符串字面量。

字符串拼接的方式

见《JAVA核心技术卷一》 P45

字符串的intern方法

在《深入理解Java虚拟机》中解释道:
String.intern()是一个Native方法,它的作用是:如果字符常量池中已经包含一个等于此String对象的字符串,则返回常量池中字符串的引用。否则,将新的字符串放入常量池,并返回新字符串的引用。
不同版本的JAVA虚拟机对此方法的实现可能不同,下面举一个例子。

package com.luna.test;
public class StringTest {
	public static void main(String[] args) {
	    String str1 = "todo";
        String str2 = "todo";
        String str3 = "to";
        String str4 = "do";
        String str5 = str3 + str4;
        String str6 = new String(str1);
 
        System.out.println("------普通String测试结果------");
        System.out.print("str1 == str2 ? ");
        System.out.println( str1 == str2);
        System.out.print("str1 == str5 ? ");
        System.out.println(str1 == str5);
        System.out.print("str1 == str6 ? ");
        System.out.print(str1 == str6);
        System.out.println();
 
        System.out.println("---------intern测试结果---------");
        System.out.print("str1.intern() == str2.intern() ? ");
        System.out.println(str1.intern() == str2.intern());
        System.out.print("str1.intern() == str5.intern() ? ");
        System.out.println(str1.intern() == str5.intern());
        System.out.print("str1.intern() == str6.intern() ? ");
        System.out.println(str1.intern() == str6.intern());
        System.out.print("str1 == str6.intern() ? ");
        System.out.println(str1 == str6.intern());
	}
}
------普通String测试结果------
str1 == str2 ? true
str1 == str5 ? false
str1 == str6 ? false
---------intern测试结果---------
str1.intern() == str2.intern() ? true
str1.intern() == str5.intern() ? true
str1.intern() == str6.intern() ? true
str1 == str6.intern() ? true

普通String代码结果分析:Java语言会使用常量池保存那些在编译期就已确定的已编译的class文件中的一份数据。主要有类、接口、方法中的常量,以及一些以文本形式出现的符号引用,如类和接口的全限定名、字段的名称和描述符、方法和名称和描述符等。因此在编译完Intern类后,生成的class文件中会在常量池中保存“todo”、“to”和“do”三个String常量。变量str1和str2均保存的是常量池中“todo”的引用,所以str1==str2成立;在执行 str5 = str3 + str4这句时,JVM会先创建一个StringBuilder对象,通过StringBuilder.append()方法将str3与str4的值拼接,然后通过StringBuilder.toString()返回一个堆中的String对象的引用,赋值给str5,因此str1和str5指向的不是同一个String对象,str1 == str5不成立;String str6 = new String(str1)一句显式创建了一个新的String对象,因此str1 == str6不成立便是显而易见的事了。
String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String,若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。因此,只要是等值的String对象,使用intern()方法返回的都是常量池中同一个String引用,所以,这些等值的String对象通过intern()后使用==是可以匹配的。由此就可以理解上面代码中intern部分的结果了。因为str1、str5和str6是三个等值的String,所以通过intern()方法,他们均会指向常量池中的同一个String引用,因此str1.intern() == str5.intern() == str6.intern()均为true

" "与new创建字符串的区别

以“ ”方式创建的字符串,只要字符内容相同,无论在程序代码中出现几次,JVM 都只会建立一个 String 对象,并在字符串常量池中维护。
字符串常量池:当使用双引号创建字符串对象的时候,系统会检查该字符串是否在字符串常量池中存在。如果不存在:创建;如果存在:不会重新创建,而是直接复用。

而通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,虽然内容相同,但是地址值不同。

String、StringBuilder 和 StringBuffer 之间的区别

大佬的博客,斗胆引用

字符串的长度

看一下length()的源码:

 /**
     * Returns the length of this string.
     * The length is equal to the number of Unicode
     * code units in the string.
     */
    public int length() {
        return value.length;
    }

length() 方法返回的正是字符数组 value 的长度(length),value 本身是 private 的,因此很有必要为 String 类提供一个 public 级别的方法来供外部访问字符的长度。

Substring简介

substring() 的完整写法是 substring(int beginIndex, int endIndex)
该方法返回一个新的字符串,介于原有字符串的起始下标 beginIndex 和结尾下标 endIndex-1 之间。
由于字符串是不可变的,因此当调用 substring() 方法的时候,返回的其实是一个新的字符串。
JDK7中源码:

public String(char value[], int offset, int count) {
    //check boundary
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}

public String substring(int beginIndex, int endIndex) {
    //check boundary
    int subLen = endIndex - beginIndex;
    return new String(value, beginIndex, subLen);
}

可以看得出,substring() 通过 new String() 返回了一个新的字符串对象,在创建新的对象时通过 Arrays.copyOfRange() 复制了一个新的字符数组。

而在JDK6中:

String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

public String substring(int beginIndex, int endIndex) {
    //check boundary
    return  new String(offset + beginIndex, endIndex - beginIndex, value);
}

substring() 方法本身和 JDK 7 并没有很大的差别,都通过 new String() 返回了一个新的字符串对象。但是 String() 这个构造函数有很大的差别,JDK 6 只是简单地更改了一下两个属性(offset 和 count)的值,value 并没有变。

PS:value 是真正存储字符的数组,offset 是数组中第一个元素的下标,count 是数组中字符的个数。
这意味着,调用 substring() 的时候虽然创建了新的字符串,但字符串的值仍然指向的是内存中的同一个数组。
JDK6中隐含的问题:如果有一个很长很长的字符串,可以绕地球一周,当我们需要调用 substring() 截取其中很小一段字符串时,就有可能导致性能问题。由于这一小段字符串引用了整个很长很长的字符数组,就导致很长很长的这个字符数组无法被回收,内存一直被占用着,就有可能引发内存泄露。
JDK7之前的解决方法:

cmower = cmower.substring(0, 4) + "";

“+”号操作符就相当于一个语法糖,加上空的字符串后,会被 JDK 转化为 StringBuilder 对象,该对象在处理字符串的时候会生成新的字符数组,所以这行代码执行后,cmower 就指向了和 substring() 调用之前不同的字符数组。

其他有关字符串的问题

字符串的拆分
字符串操作小技巧
字符串转整形
字符串可以引用传递吗
生成UUID

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值