本帖中代码使用的jdk版本:
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
Java HotSpot(TM) Client VM (build 25.66-b17, mixed mode)
先思考一个问题:String为什么是不可更改的。
查看String类的签名如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {}
然后再看看String到底是怎么存储字符串的:
/** The value is used for character storage. */
private final char value[];
String类的签名,和存储String的char数组都被final修饰,它们确保了String对象是永远不会被修改的。
一、内存存储情况
先来一段代码:
public class Test02{
String s_01 = "hello my ...my...";
public static void main(String[] args){
String s_01 = "hello my name is smallbug";
}
}
编译之后,用javap -verbose Test02 命令来反编译:
可以看到在字节码的Constant pool区有如下两行:
#18 = Utf8 hello my ...my...
#20 = Utf8 hello my name is smallbug
可见不管是全局变量还是局部变量,只要一开始String被赋值了,那么它的值就会被保存在字节码的常量池中。其实你会发现如果把之前的:
String s_01 = "hello my name is smallbug";
改为:
String s_01 = "hello" + "my" + "name" + "is" + "smallbug";
结果也是一样的,compiler发现这些"+"操作完全可以在编译阶段优化掉,compiler就会进行一定的优化操作。
那么像这种常量在对象初始化之后会被放在何处呢?在jdk1.6及1.6之前是在方法区中的String Pool中,但是jdk1.7之后JDK将String Pool放到了堆中。这也就引出了那个String的经典问题:
String s_01 = "hello my name is smallbug";
String s_02 = "hello my name is smallbug";
String s_03 = new String("hello my name is smallbug");
System.out.println(s_01==s_02);
System.out.println(s_01.equals(s_02));
System.out.println(s_01==s_03);
System.out.println(s_01.equals(s_03));
相信结果一定不会出乎所有人的意料:
true
true
false
true
那么Why?
在对象初始化时首先将s_01对应的字符串"hello my name is smallbug"放入了运行时常量池。之后发现又来一个jvm当然不会再去创建一个了,直接把之前的字符串拿来用就行,那么就可以断定其实s_01,s_02两个reference是指向同一个内存地址的。所以s_01==s_02会是true。
在继续之前先粘一段JDK中String类equals方法的实现过程:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
可见String类将Object类复写了,它其实比较的是char数组中的字符是否全部相等,只要全部相等equals方法就会返回true,不关你是对象中的字符串还是对象中的字符串。所以也就可以解释两个equals方法返回true了。那么s_01==s_03中s_01指向的是运行时常量池,s_03指向的是一个String对象,两者地址肯定不相同,所以肯定返回false。
在讨论String的具体存储位置时还会涉及到一个本地方法,签名如下:
public native String intern();
先说一下它的具体作用:如果对于一个String对象,在运行时常量池中没有与其相对应的字符串常量,就会将这个String对象中的字符串放到运行时常量池中一份。一定注意并不是复制了一个String对象副本。在之前的代码中再加一句:
System.out.println(s_01==s_03.intern());
会看到他返回的是true。
下边还有个更有趣的问题:
public static void main(String[] args){
String s_01 = new String(args[0]);
String s_02 = "hellosmallbug";
s_01.intern();
System.out.println(s_01==s_02);
}
//input:java Test02 hellosmallbug
输出结果是:false
但是:
String s_01 = new String(args[0]);
s_01.intern();
String s_02 = "hellosmallbug";
System.out.println(s_01==s_02);
//input:java Test02 hellosmallbug
输出结果却是true。
如果用jdk1.7+运行的结果跟我的一样如果之前的版本会出现两个false。这就更郁闷了,到底是是怎么回事呢。
在jdk1.6之前Sting Pool是在方法区的,执行String s_01 = new String(args[0]);会在堆中创建一个String对象,当执行到 s_01.intern();时,会在StringPool中创建字符串常量,然后让刚才创建的那个对象指向该字符串。所以不管intern()在什么时候调用s_01指向的是堆中的对象,但是s_02指向的是String Pool中的常量,地址肯定不会相等。
但是在1.7+时候StringPool被放到了堆中。在执行String s_01 = new String(args[0]);时,会在堆中创建一个String对象,但是在执行s_01.intern();时会在堆中的StringPool创建常量,然后s_01reference指向这个常量的地址。再执行String s_02 = "hellosmallbug";时,会发现StringPool中已经有了相同的常量故它也指向这个常量地址,因为所指向的地址相同,所以也就解释了后一段代码为什么返回true。那么前一段代码为什么又会返回false呢?那是因为:String s_01 = new String(args[0]);在堆中创建了一个String对象,之后 String s_02 = "hellosmallbug";在StringPool中创建了一个常量并将s_02 reference指向了这个常量地址。当执行s_01.intern()时之前创建的对象发现StringPool中已经有这个常量了,所以对象又指向了这个常量的地址。但是s_01还是指向被创建的那个对象,它所指向的地址一直没有变过。所以也就出现了s_01==s_02的结果为false。
要注意的是,String的常量池是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。
在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:-XX:StringTableSize=7758
二、应该知道的小问题
相信所有Java程序员都写过这么一段类似的代码:
String s = "";
for(int i = 0; !"end".equals(args[i]);){
s+=args[i];
}
同样用javap命令反编译字节码,会产生一个令人非常懊恼恨不得立刻去修改以前代码的冲动:
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: ldc #3 // String end
7: aload_0
8: iload_2
9: aaload
10: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
13: ifne 40
16: new #5 // class java/lang/StringBuilder
19: dup
20: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
23: aload_1
24: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
27: aload_0
28: iload_2
29: aaload
30: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore_1
37: goto 5
40: return
发现13行是判断37到5行还是个循环。在这个循环里每一次对String执行"+"操作,都会创建一个StringBuilder对象,可见这多么消耗性能。为了避免这种事情发生只要你在执行循环之前创建一个StringBuilder对象然后将之后的"+"操作换成String.append()就可以。
分享到:
相关推荐
在Java编程语言中,String类型的参数传递问题是一个常见的困惑点,尤其对于刚接触Java的开发者。在Java中,所有的参数传递都是基于值的,但是针对基本类型和引用类型(对象)有不同的表现。让我们深入理解这一机制。...
Java中的布局管理器 Java语言中的布局管理器是指在Java语言中编制图形用户界面程序时,用于管理容器中组件的布局和排列的机制。布局管理器在Java中扮演着非常重要的角色,是实现跨平台的特性和获得动态的布局效果的...
标题“Springmvc : Failed to convert property value of type 'java.lang.String' to int”涉及的是一个在使用Spring MVC框架时常见的错误。这个错误通常出现在尝试将一个字符串类型(String)的属性值转换为整型...
对 Java 中多态理解 Java 中的多态是指在不同的情况下可以有不同的行为,多态是面向对象编程的一种基本特征。多态的实现是通过方法重载和方法重写来实现的。 在 Java 中,多态可以分为两种:编译时多态和运行时...
接下来,我们来谈谈JDTS,全称Java Database Transaction Service。这是一个开源的JDBC驱动,专门用于处理分布式事务。JDTS实现了JTA(Java Transaction API),允许在多数据库环境中进行ACID(原子性、一致性、隔离...
在Java编程语言中,`Object`类是所有类的根,每个自定义类如果没有明确指定父类,都默认继承自`Object`。因此,对`Object`类的理解是每个Java开发者的基本功。本文将深入探讨`Object`类,以及其核心方法`equals()`与...
接下来,让我们谈谈Java内存分配。Java程序中的内存主要分为三个区域:栈内存(Stack)、堆内存(Heap)和方法区(Method Area)。 - **栈内存**:主要用于存储基本类型的变量(如int、float)和对象的引用。每当一...
Java中的`this`关键字是一个非常重要的概念,它用于在代码中引用当前对象的实例变量、方法或者构造器。本文将详细探讨`this`的三种主要使用方法。 1. **引用当前对象的变量或方法** 当类的成员变量与局部变量(如...
在Java编程语言中,处理图像是一项常见的任务,其中包括图片压缩。Java提供了丰富的API来处理图像,其中`java.awt.image.BufferedImage`和`javax.imageio.ImageIO`类是核心工具。本篇文章将深入探讨如何利用Java后台...
Java中的自定义注解是一种强大的工具,允许程序员在代码中添加元数据,这些元数据可以在编译时或运行时被解析和使用。自定义注解是Java 5引入的新特性,它增强了代码的可读性和可维护性,同时也简化了框架和库的开发...
首先,我们来谈谈Java代码性能的优化。Java性能优化涵盖了许多领域,如内存管理、线程调度、I/O操作、算法选择等。以下是一些关键点: 1. **内存管理**:Java的垃圾回收机制虽然自动化程度高,但过度创建对象或持有...
接下来,我们来谈谈`pagination`库,这是一个专门用于Java分页的开源库,它提供了更高级的功能和更好的抽象。使用`pagination`库,你可以轻松地创建分页查询,而无需手动处理SQL中的`LIMIT`和`OFFSET`。例如,假设...
在Java编程中,获取计算机的硬件信息,如CPU使用率和内存使用情况,是一项常见的需求。这主要应用于系统监控、性能分析以及资源管理等方面。Java虽然不像C++或C#那样可以直接调用操作系统API,但它提供了Java ...
Java中的线程分为两种类型:守护线程(Daemon)和用户线程(User)。这两类线程的主要区别在于它们对Java虚拟机(JVM)生命周期的影响。守护线程主要是为其他线程提供服务,比如垃圾回收线程,而用户线程则包含应用...
Java视频格式转换是一种常见的任务,尤其在多媒体处理和流媒体服务中。FFmpeg和MEncoder是两个非常强大的命令行工具,常用于音频和视频的处理,包括格式转换。本篇文章将详细探讨如何在Java中利用这两个工具进行视频...
在IT领域,尤其是Java编程的学习过程中,掌握类的声明、方法以及访问修饰符是至关重要的。今天我们将深入探讨"Declared Methods"(声明的方法)、"Method"(方法)以及"Modifier"(修饰符)这三个概念,这些都是Java...
在实际的Java开发项目中,自定义异常是提高代码可读性和可维护性的重要手段。异常处理是程序设计的关键部分,它有助于捕获并处理在程序执行过程中可能出现的错误或异常情况。Java提供了丰富的异常处理机制,包括预...
然而,我们可以通过自定义构造函数为每个枚举实例赋予特定的值,如Direction枚举中通过`private Direction(String name)`构造函数实现了对枚举值的定制。这种方式不仅限于整数,还可以包含字符串、对象等。 2. 构造...