做一个积极的人

编码、改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. 快速上手系列-C语言之指针篇(一)

    快速上手系列-C语言之指针篇(一) 浊酒敬风尘 发布时间:18-06-2108:29 指针的灵活运用使得c语言更加强大,指针是C语言中十分重要的部分,可以说指针是C语言的灵魂.当然指针不是万能的,但没 ...

  2. Qt数据库编程_基本

    QtSql模块提供了一个平台无关且数据库无关的访问SQL数据库的接口. Qt中的每个数据库连接用一个QSqlDatabase对象来表示:Qt使用不同driver来和各种不同数据库的API进行通讯. Q ...

  3. QT 自定义消息

    #define TEST_EVENT QEvent::User + 100   class CVxActuatorMain : public QMainWindow {   protected:    ...

  4. Xgboost 两种使用方式

    原生形式使用Xgboost(import xgboost as xgb) from sklearn import datasets from sklearn.model_selection impor ...

  5. 《你不知道的JavaScript(上)》笔记——动态作用域

    动态作用域让作用域作为一个在运行时就被动态确定的形式, 而不是在写代码时进行静态确定的形式.动态作用域并不关心函数和作用域是如何声明以及在何处声明的, 只关心它们从何处调用. 换句话说, 作用域链是基 ...

  6. 美国药品销售额top200

    python机器学习-乳腺癌细胞挖掘(博主亲自录制视频)https://study.163.com/course/introduction.htm?courseId=1005269003&ut ...

  7. matllab 按照文件夹处理文件

    folders=dir('文件夹地址'); folders={folders.name}; folders=setdiff(folders,{'.','..'})'; num=length(folde ...

  8. 请求路径@PathVariable注释中有点.英文句号的问题(忽略英文句号后面的后缀)

    前端页面请求地址 <video id=example-video width=960 height=540 class="video-js vjs-default-skin" ...

  9. JAVA 基础编程练习题23 【程序 23 求岁数】

    23 [程序 23 求岁数] 题目:有 5 个人坐在一起,问第五个人多少岁?他说比第 4 个人大 2 岁.问第 4 个人岁数,他说比第 3 个 人大 2 岁.问第三个人,又说比第 2 人大两岁.问第 ...

  10. unity 读取灰度图生成三维地形并贴图卫星影像

    从 https://earthexplorer.usgs.gov/ 下载高程数据 从谷歌地球上保存对应地区卫星图像 从灰度图创建地形模型,并将卫星影像作为贴图 using System.Collect ...