资讯专栏INFORMATION COLUMN

简谈Java String

ssshooter / 2728人阅读

摘要:而用关键字调用构造器,总是会创建一个新的对象,无论内容是否相同。中对象的哈希码被频繁地使用比如在等容器中。字符串不变性保证了码的唯一性因此可以放心地进行缓存。对于所有包含方式新建对象包括的连接表达式,它所产生的新对象都不会被加入字符串池中。

前言

前阵子和同事在吃饭时聊起Java的String,觉得自己之前的笔记写的略显零散。故此又重新整理了一下。

String在Java中算是一个有意思的类型,是final类型,因此不可以继承这个类、不能修改这个类。

两个小问题

我们先来看一段代码:

String s = "Hello";
s = s + " world!";

试问:这两行代码执行后,原始的 String 对象中的内容到底变了没有?

答案是没有。因为 String 被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在 这段代码中,s 原先指向一个 String 对象,内容是 "Hello",然后我们对 s 进行了+操作。这时,s 不指向原来那个对象了, 而指向了另一个 String 对象,内容为"Hello world!",原来那个对象还存在于内存之中,只 是 s 这个引用变量不再指向它了。

通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或 者说,不可预见的修改,那么使用 String 来代表字符串的话会引起很大的内存开销。因为 String 对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个 String 对象来 表示。这时,应该考虑使用 StringBuffer类,它允许修改,而不是每个不同的字符串都要生 成一个新的对象。并且,这两种类的对象转换十分容易。

同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都 new 一个 String。例 如我们要在构造器中对一个名叫 s 的 String 引用变量进行初始化,把它设置为初始值,应当这样做:

 public class Demo {
   private String s;
   ...
   public Demo {
     s = "Initial Value";
      }
      ...
      //而非 s = new String("Initial Value");
}

前者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为 String 对象不可改变,所以对于内容相同的字符串,只要一个 String 对象来表示就可以了。也就 说,多次调用上面的构造器创建多个对象,他们的 String 类型属性 s 都指向同一个对象。 上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java 认为它们代表同 一个 String 对象。而用关键字 new 调用构造器,总是会创建一个新的对象,无论内容是否 相同。

再请大家看一段代码:

String s = new String("xyz");

问题:创建了几个 String Object?二者之间有什么区别?

一个或两个

”xyz”对应一个对象,这个对象放在字符串常量池,常量”xyz”不管出现多少遍,都是缓冲区中的那一个。New String 每写一遍,就创建一个新的对象在堆上。

如果以前就用过’xyz’,这句代表就不会 创建”xyz”自己了,直接从字符串常量池拿。

常量池

在Java中,其实有很多常量池相关的概念:

常量池表(constant_pool table)

Class文件中存储所有常量(包括字符串)的table

这是Class文件中的内容,还不是运行时的内容,不要理解它是个池子,其实就是Class文件中的字节码指令

运行时常量池(Runtime Constant Pool)

JVM内存中方法区的一部分,这是运行时的内容

这部分内容(绝大部分)是随着JVM运行时候,从常量池转化而来,每个Class对应一个运行时常量池

前面说的绝大部分是因为:除了 Class中常量池内容,还可能包括动态生成并加入这里的内容

字符串常量池(String Pool)

这部分也在方法区中,但与Runtime Constant Pool不是一个概念,String Pool是JVM实例全局共享的,全局只有一个

JVM规范要求进入这里的String实例叫“被驻留的interned string”,各个JVM可以有不同的实现,HotSpot是设置了一个哈希表StringTable来引用堆中的字符串实例,被引用就是被驻留。

类似这种常量池的思想即涉及到了一个设计模式——享元模式。顾名思义,共享元素。

也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素

不可变的String

那么为什么要不可变呢?

主要是为了安全与效率。

安全

String被许多的Java类库用来当做参数。例:

URL、IP

文件路径path

反射机制所需要的String参数

等等...

假若String不是固定不变的,将会引起各种安全隐患。

效率

在前面提到过常量池的享元模式。这样在拷贝或者创建内容相同对象时,就不必复制对象本身,而是只要引用它即可。这样的开销比起copy object是天差地别的。另外,也就只有不可变对象才能使用常量池,因为可以保证引用同一常量值的多个变量不产生相互影响。

同样也是由于String对象的不可变特性,所以String对象可以自身缓存HashCode。Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存。这也是一种性能优化手段,意味着不必每次都去计算新的哈希码:

public final class String
    implements java.io.Serializable, Comparable, 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
其他 String 和 StringBuffer

JAVA 平台提供了两个类:String 和 StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。这个 String 类提供了数值不可改变的字符串。而这个 StringBuffer 类提供 的字符串进行修改。当你知道字符数据要改变的时候你就可以使用 StringBuffer。典型地, 你可以使用 StringBuffers 来动态构造字符数据。另外,String 实现了 equals 方法,new String(“abc”).equals(newString(“abc”)的结果为true,而StringBuffer没有实现equals方法, 所以,new StringBuffer(“abc”).equals(newStringBuffer(“abc”)的结果为 false。

接着要举一个具体的例子来说明,我们要把1到100的所有数字拼起来,组成一个串。

StringBuffer sbf = new StringBuffer();
 for(int i=0;i<100;i++){
           sbf.append(i);
    }

上面的代码效率很高,因为只创建了一个 StringBuffer 对象,而下面的代码效率很低,因为 创建了101个对象。

 String str = new String();
   for(int i=0;i<100;i++) {
             str = str + i;
}

在讲两者区别时,应把循环的次数搞成10000,然后用 endTime-beginTime 来比较两者执 行的时间差异。

String 覆盖了 equals 方法和 hashCode 方法,而 StringBuffer没有覆盖 equals 方法和 hashCode 方法,所以,将 StringBuffer对象存储进 Java集合类中时会出现问题

StringBuilder与 StringBuffer

StringBuilder不是线程安全的,但是单线程中中的性能比StringBuffer高。

Demo Code String对象创建方式
  String str1 = "abcd";
  String str2 = new String("abcd");
  System.out.println(str1==str2);//false

这两种不同的创建方法是有差别的:

第一种方式是在常量池中拿对象

第二种方式是直接在堆内存空间创建一个新的对象。只要使用new方法,便会创建新的对象

连接表达式 +

只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。

对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中。

String str1 = "str";
String str2 = "ing";

String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);//false

String str5 = "string";
System.out.println(str3 == str5);//true
连接表达式demo1
public static final String str1 = "ab";
public static final String str2 = "cd";

public static void main(String[] args) {
    String s = str1 + str2;  // 将两个常量用+连接对s进行初始化
    String t = "abcd";
    if (s == t) {
        System.out.println("s等于t,它们是同一个对象");
    } else {
        System.out.println("s不等于t,它们不是同一个对象");
    }
}

s等于t,它们是同一个对象

A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s="ab"+"cd";

连接表达式demo2
public static final String str1;
public static final String str2;

static {
    str1 = "ab";
    str2 = "cd";
}

public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
    String s = str1 + str2;
    String t = "abcd";
    if (s == t) {
        System.out.println("s等于t,它们是同一个对象");
    } else {
        System.out.println("s不等于t,它们不是同一个对象");
    }
}

s不等于t,它们不是同一个对象

A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。

intern方法

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

public static void main(String[] args) {    
   String s1 = new String("计算机");
   String s2 = s1.intern();
   String s3 = "计算机";
   System.out.println("s1 == s2? " + (s1 == s2));
   System.out.println("s3 == s2? " + (s3 == s2));
}
//s1 == s2? false
//s3 == s2? true
一个较为丰富的demo
public class Test {
    public static void main(String[] args) {
        String hello = "Hello", lo = "lo";
        System.out.println((hello == "Hello") + " ");
        System.out.println((Other.hello == hello) + " ");
        System.out.println((other.Other.hello == hello) + " ");
        System.out.println((hello == ("Hel" + "lo")) + " ");
        System.out.println((hello == ("Hel" + lo)) + " ");
        System.out.println(hello == ("Hel" + lo).intern());
    }
}
class Other {
    static String hello = "Hello";
}
package other;

public class Other {
    public static String hello = "Hello";
}
//true true true true false true

在同包同类下,引用自同一String对象

在同包不同类下,引用自同一String对象

在不同包不同类下,依然引用自同一String对象

在编译成.class时能够识别为同一字符串的,自动优化成常量,引用自同一String对象

在运行时创建的字符串具有独立的内存地址,所以不引用自同一String对象

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67997.html

相关文章

  • 简谈Java Enum

    摘要:常量接口是对接口的一种不良使用。如果这些常量最好被看作是枚举类型成员,那就应该用枚举类型来导出。因为客户端既不能创建枚举类型的实例,也不能对它进行扩展,因此很可能没有实例,而只有声明过的枚举常量。换句话说,枚举类型是实例受控的。 问题 我们偶尔能在项目中看到如下风格的代码: public class ResponseCode { public static final int ...

    BicycleWarrior 评论0 收藏0
  • 简谈文件下载的三种方式

    摘要:一前言本文章将以报表下载为例,给大家介绍三种文件下载的方式。通过二进制数据流的方式下载这种方式是我目前采用的方式,用于处理报表下载。缺点对于数据量不大的文件,这种方式是可行的。 一、前言 本文章将以excel报表下载为例,给大家介绍三种文件下载的方式。 原文地址:简谈文件下载的三种方式 | Rychou 二、正文 1. 通过服务器文件地址下载 这是最常见的文件下载方式,大多数网站的音频...

    lsxiao 评论0 收藏0
  • 简谈文件下载的三种方式

    摘要:一前言本文章将以报表下载为例,给大家介绍三种文件下载的方式。通过二进制数据流的方式下载这种方式是我目前采用的方式,用于处理报表下载。缺点对于数据量不大的文件,这种方式是可行的。 一、前言 本文章将以excel报表下载为例,给大家介绍三种文件下载的方式。 原文地址:简谈文件下载的三种方式 | Rychou 二、正文 1. 通过服务器文件地址下载 这是最常见的文件下载方式,大多数网站的音频...

    2i18ns 评论0 收藏0
  • 简谈JavaScript闭包

    摘要:所以经常看到的说闭包就是绑定了上下文环境的函数。我更偏向于闭包是一个函数和声明该函数的词法环境的组合。里面的闭包先上一个闭包该例子的解释上面的代码,在函数里面定义的函数和这个函数声明的词法环境就形成了一个闭包。 闭包是什么 第一种说法:闭包创建一个词法作用域,这个作用域里面的变量被引用之后可以在这个词法作用域外面被自由访问,是一个函数和声明该函数的词法环境的组合 第二种说法:闭包就是...

    Zachary 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<