Java内存管理-探索Java中字符串String(十二)

做一个积极的人编码、改bug、提升自己我有一个乐园,面向编程,春暖花开!文章目录一、初识String类二、字符串的不可变性三、字符串常量池和 intern 方法四、面试题1、 String s1 = new String(“hello”);这句话创建了几个字符串对象?2、有时候在面试的时候会遇到这样的问题:**都说String是不可变的,为什么我可以这样做呢,String a = “1”…

大家好,又见面了,我是全栈君。

做一个积极的人

编码、改bug、提升自己

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

推荐阅读

第一季

0、Java的线程安全、单例模式、JVM内存结构等知识梳理
1、Java内存管理-程序运行过程(一)
2、Java内存管理-初始JVM和JVM启动流程(二)
3、Java内存管理-JVM内存模型以及JDK7和JDK8内存模型对比总结(三)
4、Java内存管理-掌握虚拟机类加载机制(四)
5、Java内存管理-掌握虚拟机类加载器(五)
6、Java内存管理-类加载器的核心源码和设计模式(六)
7、Java内存管理-掌握自定义类加载器的实现(七)
第一季总结:由浅入深JAVA内存管理 Core Story

第二季

8、Java内存管理-愚人节new一个对象送给你(八)
【福利】JVM系列学习资源无套路赠送
9、Java内存管理-”一文掌握虚拟机创建对象的秘密”(九)
10、Java内存管理-你真的理解Java中的数据类型吗(十)
11、Java内存管理-Stackoverflow问答-Java是传值还是传引用?(十一)
12、Java内存管理-探索Java中字符串String(十二)

实战

一文学会Java死锁和CPU 100% 问题的排查技巧

分享一位老师的人工智能教程。零基础!通俗易懂!风趣幽默!
大家可以看看是否对自己有帮助,点击这里查看【人工智能教程】。接下来进入正文。

一、初识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对象会直接存储在堆上


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

Java内存管理-探索Java中字符串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版本运行输出的结果: falsetrue

先上面示例的示意图:

Java内存管理-探索Java中字符串String(十二)

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:

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

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

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

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”还在内存中存在,并没有改变。内存结构如下图所示:

Java内存管理-探索Java中字符串String(十二)

类似的一张图:

Java内存管理-探索Java中字符串String(十二)

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

参考资料

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

Java-String.intern的深入研究

深入理解Java中的String

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


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



Java编程技术乐园:一个分享编程知识的公众号。跟着老司机一起学习干货技术知识,每天进步一点点,让小的积累,带来大的改变!


扫描关注,后台回复【资源】,获取珍藏干货! 99.9%的伙伴都很喜欢

image.png | center| 747x519


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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/121018.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • G6流程图绘制

    G6流程图绘制为了能在线编辑流程 支持流程节点编辑等功能 支持人员等选择功能 支持流程图数据保存 利用阿里 G6 进行设计开发 整体效果图如下 支持放大缩小 节点移动 添加节点及边等 同时支持节点及边删除操作 流程图数据保存等工作 支持节点编辑 包括人员选择 图形选择 宽高编辑 背景色 边框色等信息编辑 支持边的编辑 边描述等 各种交互功能就不赘述了 页面代码如下 DOCTYPE YPE html head head html

    2025年7月25日
    0
  • python中griddata的外插值_griddata二维插值[通俗易懂]

    python中griddata的外插值_griddata二维插值[通俗易懂]”””SimpleN-Dinterpolation..versionadded::0.9″””##Copyright(C)PauliVirtanen,2010.##DistributedunderthesameBSDlicenseasScipy.###Note:thisfileshouldberunthroughtheMakotemplateen…

    2022年5月9日
    289
  • rj45 千兆接口定义_rj45 千兆接口定义_rj45接口定义,大神教你秒懂rj45的接线方法【详细方法】…

    RJ45接口通常用于数据传输,最常见的应用为网卡接口。RJ45是各种不同接头的一种类型(例如:RJ11也是接头的一种类型,不过它是电话上用的);RJ45头跟据线的排序不同的法有两种,一种是橙白、橙、绿白、蓝、蓝白、绿、棕白、棕;另一种是绿白、绿、橙白、蓝、蓝白、橙、棕白、棕;因此使用RJ45接头的线也有两种即:直通线、交插线。10100basetxRJ45接口是常用的以太网接口,支持10兆和…

    2022年4月9日
    35
  • 使用html和css制作水平导航栏nav

    使用html和css制作水平导航栏nav使用html和css制作水平导航栏nav的方法及其效果:1、li设置float:left;(1)代码片段:…<style>*{margin:0;padding:0;}ul{list-style-type:none;marg

    2022年5月26日
    46
  • winscp登录主机拒绝_winscp_winscp连接被拒绝_winscp下载

    winscp登录主机拒绝_winscp_winscp连接被拒绝_winscp下载winscpwinscpItappearsthatyourbrowserdoesnotcomplywiththeW3Cwebstandardswhichdefinehowwebpagesareencoded,transmitted,andrendered.Thissitewouldlookmuchbetterinastandards-c…

    2022年9月17日
    1
  • Java 8 Stream常用方法学习

    Java 8 Stream常用方法学习StreamStream流是Java8API新增的一个处理集合的关键抽象概念,是一个来自数据源的元素队列并支持聚合操作。以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用StreamAPI对集合数据进行操作,就类似于使用SQL执行的数据库查询。也可以使用StreamAPI来并行执行操作。简而言之,StreamAPI提供了一种高效且易于使用的处理数据的方式。相关名词描述元素对象形成的一个队列。Java中的Stream并不会存储元

    2022年9月25日
    1

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号