做一个积极的人

编码、改bug、提升自己

我有一个乐园,面向编程,春暖花开!

一、初识String类

首先JDK API的介绍:

public final class String extends Object
implements Serializable, Comparable<String>, CharSequence

String类代表字符串。Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例实现。

字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。例如:

 String str = "abc";

等效于:

 char data[] = {'a', 'b', 'c'};
String str = new String(data);

从JDK API中可以看出:

  • String类是final类,那么String类是不能被继承的。
  • 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
  • 实现了Serializable接口,支持序列化,也就意味了String能够通过序列化传输。

二、字符串的不可变性

从上面的介绍中发现:字符串是常量,它们的值在创建之后不能更改。为什么会这样呢?要了解其原因,简单看一下String类的源码实现。

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
// 重新创建一个新的字符串
return new String(buf, true);
} public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */ while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 重新创建一个新的字符串
return new String(buf, true);
}
}
return this;
}
}

从上面源码中可以看出String类其实是通过char数组来保存字符串的,注意修饰这个char前面的关键字 final。final修饰的字段创建以后就不可改变。

注意private final char value[]; 这里虽然value是不可变,也就是说value这个引用地址不可变。但是因为其是数组类型,根据之前学过的内容,value这个引用地址其实是在栈上分配 ,而其对应的数据结构是在堆上分配保存。那也就是说栈里的这个value的引用地址不可变。没有说堆里array本身数据不可变。看下面这个例子,

final int[] value={1,2,3}
int[] another={4,5,6};
value=another; //编译器报错,final不可变

value用final修饰,编译器不允许我把value指向栈区另一个地址。但如果直接对数组元素进行修改,分分钟搞定。

final int[] value={1,2,3};
value[2]=100; //这时候数组里已经是{1,2,100}

所以String是不可变的关键都在底层的实现,而不是一个final。

也可以通过上面的concat(String str) 和replace(char oldChar, char newChar)方法简单进行了解,所有的操作都不是在原有的value[]数组中进行操作的,而是重新生成了一个新数组buf[]。也就是说进行这些操作后,最原始的字符串并没有被改变。

如果面试有问到的话要修改String中value[] 数组的内容,要怎么做,那么可以通过反射进行修改!实际使用中没有人会去这么做。

三、字符串常量池和 intern 方法

Java中有字符串常量池,用来存储字符串字面量! 由于JDK版本的不同,常量池的位置也不同,根据网上的一些资料:

jdk1.6及以下版本字符串常量池是在永久区中。

jdk1.7、1.8下字符串常量池已经转移到堆中了。(JDK1.8已经没有去掉永久区)

因为字符串常量池发生了变化,在String内对intern()进行了一些修改:

jDK1.6版本中执行intern()方法,首先判断字符串常量池中是否存在该字面量,如果不存在则拷贝一份字面量放入常量池,最后返回字面量的唯一引用。如果发现字符串常量池中已经存在,则直接返回字面量的唯一引用。

jdk1.7以后执行intern()方法,如果字符串常量池中不存在该字面量,则不会再拷贝一份字面量,而是拷贝字面量对应堆中一个引用,然后返回这个引用。

String 类型的常量池比较特殊。它的主要使用方法有两种:

  • 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
  • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。不同版本的intern 表现看上面介绍。

说明:直接使用new String() 创建出的String对象会直接存储在堆上


通过一个栗子,看一下上面说的内容:

String str1 = "aflyun";
String str2 = new String("aflyun");
System.out.println(str1 == str2); String str3 = str2.intern(); System.out.println(str1 ==str3);

使用JDK1.8版本运行输出的结果: false 和 true 。

先上面示例的示意图:

str1直接创建在字符串常量池中,str2使用new关键字,对象创建在堆上。所以str1 == str2 为false。

str3str2.intern(),根据上面的介绍,在jdk1.8首先在常量池中判断字符串aflyun是否存在,如果存在的话,直接返回常量池中字符串的引用,也就是str1的引用。所以str1 ==str3为true。

如果你理解了上面的内容,可以在看一下下面的栗子,运行结果是在JDK1.8环境:

栗子1:

String str1 = "hello";
String str2 = "world";
//常量池中的对象
String str3 = "hello" + "world";
//在堆上创建的新的对象
String str4 = str1 + str2;
//常量池中的对象
String str5 = "helloworld";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

栗子2:

//同时会生成堆中的对象以及常量池中hello的对象,此时str1是指向堆中的对象的
String str1 = new String("hello");
// 常量池中的已经存在hello
str1.intern();
//常量池中的对象,此时str2是指向常量池中的对象的
String str2 = "hello";
System.out.println(str1 == str2); // false // 此时生成了四个对象 常量池中的"world" + 2个堆中的"world" +s3指向的堆中的对象(注此时常量池不会生成"worldworld")
String str3 = new String("world") + new String("world");
//常量池没有“worldworld”,会直接将str3的地址存储在常量池内
str3.intern();
// 创建str4的时候,发现字符串常量池已经存在一个指向堆中该字面量的引用,则返回这个引用,而这个引用就是str3
String str4 = "worldworld";
System.out.println(str3 == str4); //true

栗子3:涉及到final关键字,可以试着理解一下

// str1指的是字符串常量池中的 java6
String str1 = "java6";
// str2是 final 修饰的,编译时候就已经确定了它的确定值,编译期常量
final String str2 = "java";
// str3是指向常量池中 java
String str3 = "java"; //str2编译的时候已经知道是常量,"6"也是常量,所以计算str4的时候,直接相当于使用 str2 的原始值(java)来进行计算.
// 则str4 生成的也是一个常量,。str1和str4都对应 常量池中只生成唯一的一个 java6 字符串。
String str4 = str2 + "6"; // 计算 str5 的时候,str3不是final修饰,不会提前知道 str3的值是什么,只有在运行通过链接来访问,这种计算会在堆上生成 java6
String str5 = str3 + "6";
System.out.println((str1 == str4));//true
System.out.println((str1 == str5));//false

总结

  1. 直接定义字符串变量的时候赋值,如果表达式右边只有字符串常量,那么就是把变量存放在常量池里。

  2. new出来的字符串是存放在堆里面。

  3. 对字符串进行拼接操作,也就是做"+"运算的时候,分2中情况:

  • 表达式右边是纯字符串常量,那么存放在字符串常量池里面。

  • 表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面。:

四、面试题

1、 String s1 = new String("hello");这句话创建了几个字符串对象?

情况1:

String s1 = new String("hello");// 堆内存的地址值
String s2 = "hello";
System.out.println(s1 == s2);// 输出false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// 输出true

如果上面代码的话,这种情况总共创建2个字符串对象。常量池中没有字符串"hello" 的话,一个是new String 创建的一个新的对象,一个是常量“hello”对象的内容创建出的一个新的String对象。

情况2:

```java String s2 = "hello"; String s1 = new String("hello");

String s1 = new String("hello"); 此时就创建一个对象,而常量“hello”则是从字符串常量池中取出来的。

### 2、有时候在面试的时候会遇到这样的问题:**都说String是不可变的,为什么我可以这样做呢,String a = "1";a = "2";**

java public class StringTest {

public static void main(String[] args) {
String s = "aflyun";
System.out.println("s1.hashCode() = " + s.hashCode() + "--" + s);
s = "hello aflyun";
System.out.println("s2.hashCode() = " + s.hashCode() + "--" + s);
//运行后输出的结果不同,两个值的hascode也不一致,
//说明设置的值在内存中存储在不同的位置,也就是创建了新的对象
}

}

s1.hashCode() = -1420403061--aflyun s2.hashCode() = -855605863--hello aflyun ```

【首先创建一个String对象s,然后让s的值为“aflyun”, 然后又让s的值为“hello aflyun”。 从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢?】

其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。

也就是说,s只是一个引用,它指向了一个具体的对象,当s=“hello aflyun”; 这句代码执行过之后,又创建了一个新的对象““hello aflyun”, 而引用s重新指向了这个新的对象,原来的对象“aflyun”还在内存中存在,并没有改变。内存结构如下图所示:

类似的一张图:

总结一下:“String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何改变操作都会生成新的对象”

参考资料

java的线程安全、单例模式、JVM内存结构等知识学习和整理

Java-String.intern的深入研究

深入理解Java中的String

备注: 由于本人能力有限,文中若有错误之处,欢迎指正。


谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!


不管做什么,只要坚持下去就会看到不一样!在路上,不卑不亢!

博客首页 : https://aflyun.blog.csdn.net/

愿你我在人生的路上能都变成最好的自己,能够成为一个独挡一面的人

© 每天都在变得更好的阿飞云

Java内存管理-探索Java中字符串String(十二)的更多相关文章

  1. Java内存管理:Java内存区域 JVM运行时数据区

    转自:https://blog.csdn.net/tjiyu/article/details/53915869 下面我们详细了解Java内存区域:先说明JVM规范定义的JVM运行时分配的数据区有哪些, ...

  2. Java 内存管理

    java 内存管理机制 JAVA 内存管理总结 java 是如何管理内存的 Java 的内存管理就是对象的分配和释放问题.(两部分) 分配 :内存的分配是由程序完成的,程序员需要通过关键字 new 为 ...

  3. Java内存管理特点

    Java内存管理特点     Java一个最大的优点就是取消了指针,由垃圾收集器来自动管理内存的回收.程序员不需要通过调用函数来释放内存. 1.Java的内存管理就是对象的分配和释放问题.     在 ...

  4. Java中字符串string的数据类型

    Java中字符串string的数据类型 时间:2017-07-03 08:01:47 YuanMxy 原文:https://blog.csdn.net/YuanMxy/article/details/ ...

  5. java中字符串String 转 int(转)

    java中字符串String 转 int String -> int s="12345"; int i; 第一种方法:i=Integer.parseInt(s); 第二种方法 ...

  6. java内存管理机制

    JAVA 内存管理总结 1. java是如何管理内存的 Java的内存管理就是对象的分配和释放问题.(两部分) 分配 :内存的分配是由程序完成的,程序员需要通过关键字new 为每个对象申请内存空间 ( ...

  7. Java内存管理的9个小技巧

    Java内存管理的9个小技巧很多人都说“Java完了,只等着衰亡吧!”,为什么呢?最简单的的例子就是Java做的系统时非常占内存!一听到这样的话,一定会有不少人站出来为Java辩护,并举出一堆的性能测 ...

  8. JAVA 内存管理总结

    1. java是如何管理内存的 Java的内存管理就是对象的分配和释放问题.(两部分) 分配 :内存的分配是由程序完成的,程序员需要通过关键字new 为每个对象申请内存空间 (基本类型除外),所有的对 ...

  9. 关于Java内存管理的几个小技巧

    这里将介绍几则Java内存管理的小技巧,让你让你从Java入门开始告别陋习,为Java程序提速.有不少人都说"Java完了,只等着衰亡吧!",为什么呢?最简单的的例子就是Java做 ...

随机推荐

  1. THREE.js(一)

    //创建场景 var scene = new THREE.Scene(); //透视摄像机(视野角度,长宽比,远剪切面,进剪切面,) var camera = new THREE.Perspectiv ...

  2. 如何构建自己的docker镜像

    需求情况:springboot项目想要部署到docker里面,如何部署? 步骤如下: 1.将jar包上传linux服务器 /usr/local/dockerapp 目录,在jar包所在目录创建名为 D ...

  3. ubantu 安装boost库 c++connector

    安装libmysqlcppconn: sudo apt-get install libmysqlcppconn-dev 安装libboost: sudo apt-get install libboos ...

  4. ArcGIS超级工具SPTOOLS-线封闭,点集转面

    一.线封闭 操作视频:https://weibo.com/tv/v/HvyvbAxKh?fid=1034:4375207666991674 将末端不闭合线,自动生成闭合的线,效果如下 原始线:末端不闭 ...

  5. springmvc配置jackson时遇到的一些问题

    在没接触springmvc之前我们在servlet中想返回前台json数据时,都是自定义一个JSONObject和JSONArray,然后调用response.getWriter()对象的方法返回js ...

  6. Flutter移动电商实战 --(15)商品推荐区域制作

    1.推荐商品类的编写 这个类接收一个List参数,就是推荐商品的列表,这个列表是可以左右滚动的. /*商品推荐*/ class Recommend extends StatelessWidget { ...

  7. SQL-W3School-高级:SQL IN 操作符

    ylbtech-SQL-W3School-高级:SQL IN 操作符 1.返回顶部 1. IN 操作符 IN 操作符允许我们在 WHERE 子句中规定多个值. SQL IN 语法 SELECT col ...

  8. php中如何传递Session ID

    一般通过在各个页面之间传递的唯一的 Session ID,并通过 Session ID 提取这个用户在服务器中保存的 Session 变量,来跟踪一个用户.常见的 Session ID 传送方法主要有 ...

  9. web worker 的 self

    A self object, which is the global object representing the worker in this scope. 对self对象的译法,未知妥否. // ...

  10. Go 微服务架构Micro相关概念理解

    Micro是一个微服务框架(或者说是工具集):提供了各类组件,解决微服务架构中的不同问题,服务监控.服务发现.熔断机制,负载均衡等等,自己一个个解决这些问题几乎不可能,这时候就需要借助go-micro ...