java基础巩固-浅析String源码及其不可变性

简介: 字符串可以说是广泛应用在日常编程中,jdk从1.0就提供了String类来创建和操作字符串。同时它也是不可改变类(基本类型的包装类都不可改变)的典型代表。

字符串可以说是广泛应用在日常编程中,jdk从1.0就提供了String类来创建和操作字符串。同时它也是不可改变类(基本类型的包装类都不可改变)的典型代表。

源码查看(基于1.8)

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[]; //这边只是引用并不是真正对象
    ...
}
//首先string类创建的对象是不可变的(一个对象在创建完成后不能再改变它状态,说明是不可变的,并发程序最喜欢不可变量了),
//里面最主要的成员为char类型的数组

几个构造方法

//空的构造方法 例如 String a = new String(); a为""空字符
public String() {
    this.value = new char[0];
}

//带参构造方法 将源的hash和value赋给目标String
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

几个常用经典的String类方法

1.equals
    //如果引用指向的内存值都相等 直接返回true
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        //instanceof判断是否属于或子类 但Stringfinal修饰不可继承 只考虑是否为String类型
        //上面说过String的成员为char数组,equals内则比较char类数组元素是否一一相等 
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            //长度不相等返回false
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                //从后往前单个字符判断,如果有不相等,返回false
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

2.subString(beginIndex,endIndex)

subString这边存在一个小插曲

在jdk1.7以前,该方法存在内存泄漏问题。之所以存在是因为在此之前,String三个参数的构造方法是这么写的。成员变量为这三个,jdk7以后取消掉了offset和count加入了hash,虽然原来的构造方法简洁高效但存在gc问题。所以7以后放弃了性能采取了更为保守的写法。

    /** The value is used for character storage. */
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;
   ...
   public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
 //虽然这边返回的是新的String对象,但构造方法中还引用着原先的value
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
    }
   ...
   String(int offset, int count, char value[]) {  
     this.value = value;  
     this.offset = offset;  
     this.count = count;  
    }  
//这边开始this.value = value; 出现问题,这三个个原来为String类中的三个私有成员变量,因为这种实现还在引用原先的字符串变量value[] 通过offset(起始位置)和count(字符所占个数)返回一个新的字符串,这样可能导致jvm认为最初被截取的字符串还被引用就不对其gc,如果这个原始字符串很大,就会占用着内存,出现内存泄漏等gc问题。

jdk1.7以后的写法变化

//虽然这边还是有offse和count参数 但不是成员变量了
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0
...
public String substring(int beginIndex, int endIndex) {
      if (beginIndex < 0) {
          throw new StringIndexOutOfBoundsException(beginIndex);
      }
      if (endIndex > value.length) {
          throw new StringIndexOutOfBoundsException(endIndex);
      }
      int subLen = endIndex - beginIndex; 
      if (subLen < 0) {
          throw new StringIndexOutOfBoundsException(subLen);
      }
      return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);//构造函数参数顺序也有所变化
}
...
public String(char value[], int offset, int count) {
  if (offset < 0) {
    throw new StringIndexOutOfBoundsException(offset);
  }
  if (count < 0) {
    throw new StringIndexOutOfBoundsException(count);
  }
  // Note: offset or count might be near -1>>>1.
  if (offset > value.length - count) {
    throw new StringIndexOutOfBoundsException(offset + count);
  }
    //新的不是引用之前的而是重新新建了一个。
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

3.hashcode
public int hashCode() {
    int h = hash;
    //如果hash没有被计算过,并且字符串不为空,则进行hashCode计算
    if (h == 0 && value.length > 0) {
        char val[] = value;
        //计算过程
        //s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        //hash赋值
        hash = h;
    }
    return h;
}
//String重写了Object类的hashcode方法,根据值来计算hashcode值,不过Object的该方法为native本地方法。
//该方法设计十分巧妙,它先从0判断字符大小,如果
//hashcode其实就是散列码 这种算法的话 同样的字符串的值一定相等 但不同的字符串其实也有可能得到同样的hashcode值 
n=3
i=0 -> h = 31 * 0 + val[0]  
i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]
//以字符串 "123"为例 1的的ascil码为49 所以 "1".hashcode()的值为49,2为50...
h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] = 31 * (31 * 49 + 50) + 51 = 48690

详细算法参考这里

String的不可变性

//这边可能存在一个疑问 s对象是否发生了改变
String s = "hello";
s = "world";
//String不可变不是在原内存地址上修改数据,而是重新指向一个新对象,新地址,所以这里的hello对象并没有被改变。
//同样的类似replace方法源码中
  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);//这边返回的是新的一个String对象 而不改变原来的对象
            }
        }
        return this; //如果都一样 就指向同一个对象了
    }
img_0b82cd88a234ca8bc87a7bc2ad10bea3.png
String对象不可变

String对象为什么不可变

String用final修饰是为了防止被继承进而破坏,而让String对象不可变主要也是为了安全。

这里原来我有一个误区,仅仅认为说final修饰了String类,所以String对象不可变,想法很天真,,final修饰类的话主要是让类不可被继承,final修饰基本类型变量不能对基本类型对象重新赋值,但对于引用类型的变量(如数组),它保存的只是一个引用,final只需保证这个引用的地址不改变,即一直引用同一个对象即可,但是这个对象还是能改变的。

比如 final int[] arr = {1};
arr[0] = 3; //改变

arr = new int[]{3}; //因为这个是个新的数组 这样改变了数组的地址 编译不通过 除非去掉final

//String类源码中的成员变量 (jdk1.8)
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
img_ee2720aeace1c4e288b6b5e00a0a22e8.png
String.png

思考

1.String对象是否真的不可变

可以通过反射改变
String的成员变量为final修饰,就是初始化之后不可改变,但是这几个成员中value比较特殊,因为他是个引用变量而不是真正的对象,value[]是final修饰的,也就是说不能再指向其他数组对象,但是可以改变数组内部的结构来改变。
注:简单来说就是final修饰数组,指定数组所指向的内存空间固定,数组内部值还能改。因为数组是引用类型,内存地址是不可改变的。
实例代码
public static void testReflection() throws Exception {
    String s = "Hello,World"; 
    System.out.println("s = " + s); //Hello World  
    //获取String类中的value字段
    Field valueOfString = String.class.getDeclaredField("value");    
    //改变value属性的访问权限
    valueOfString.setAccessible(true);    
    //获取s对象上的value属性的值
    char[] value = (char[]) valueOfString.get(s);   
    //改变value所引用的数组中的第5个字符
    value[5] = '_';  
    System.out.println("s = " + s);  //Hello_World
}

String为什么要设计成不可变

1.允许String对象缓存hashcode:
Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存。
2.安全性
String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
对于一个方法而言,参数是为该方法提供信息的,而不是让方法改变自己。
3.字符串常量池的需要
字符串常量池(String pool, String intern pool, String保留池) java堆内存放了一个特殊的区域用于常量池, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
目录
相关文章
|
4天前
|
前端开发 JavaScript Java
[Java计算机毕设]基于ssm的OA办公管理系统的设计与实现,附源码+数据库+论文+开题,包安装调试
OA办公管理系统是一款基于Java和SSM框架开发的B/S架构应用,适用于Windows系统。项目包含管理员、项目管理人员和普通用户三种角色,分别负责系统管理、请假审批、图书借阅等日常办公事务。系统使用Vue、HTML、JavaScript、CSS和LayUI构建前端,后端采用SSM框架,数据库为MySQL,共24张表。提供完整演示视频和详细文档截图,支持远程安装调试,确保顺利运行。
45 17
|
2天前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
27 11
|
8天前
|
Java
课时14:Java数据类型划分(初见String类)
课时14介绍Java数据类型,重点初见String类。通过三个范例讲解:观察String型变量、&quot;+&quot;操作符的使用问题及转义字符的应用。String不是基本数据类型而是引用类型,但使用方式类似基本类型。课程涵盖字符串连接、数学运算与字符串混合使用时的注意事项以及常用转义字符的用法。
|
11天前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
77 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
26天前
|
JavaScript 安全 Java
智慧产科一体化管理平台源码,基于Java,Vue,ElementUI技术开发,二开快捷
智慧产科一体化管理平台覆盖从备孕到产后42天的全流程管理,构建科室协同、医患沟通及智能设备互联平台。通过移动端扫码建卡、自助报道、智能采集数据等手段优化就诊流程,提升孕妇就诊体验,并实现高危孕产妇五色管理和孕妇学校三位一体化管理,全面提升妇幼健康宣教质量。
47 12
|
1月前
|
人工智能 监控 安全
Java智慧工地(源码):数字化管理提升施工安全与质量
随着科技的发展,智慧工地已成为建筑行业转型升级的重要手段。依托智能感知设备和云物互联技术,智慧工地为工程管理带来了革命性的变革,实现了项目管理的简单化、远程化和智能化。
38 5
|
6天前
|
存储 监控 数据可视化
SaaS云计算技术的智慧工地源码,基于Java+Spring Cloud框架开发
智慧工地源码基于微服务+Java+Spring Cloud +UniApp +MySql架构,利用传感器、监控摄像头、AI、大数据等技术,实现施工现场的实时监测、数据分析与智能决策。平台涵盖人员、车辆、视频监控、施工质量、设备、环境和能耗管理七大维度,提供可视化管理、智能化报警、移动智能办公及分布计算存储等功能,全面提升工地的安全性、效率和质量。
|
2月前
|
JavaScript Java 测试技术
基于Java+SpringBoot+Vue实现的车辆充电桩系统设计与实现(系统源码+文档+部署讲解等)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
|
2月前
|
监控 JavaScript 数据可视化
建筑施工一体化信息管理平台源码,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
智慧工地云平台是专为建筑施工领域打造的一体化信息管理平台,利用大数据、云计算、物联网等技术,实现施工区域各系统数据汇总与可视化管理。平台涵盖人员、设备、物料、环境等关键因素的实时监控与数据分析,提供远程指挥、决策支持等功能,提升工作效率,促进产业信息化发展。系统由PC端、APP移动端及项目、监管、数据屏三大平台组成,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
113 7
|
3月前
|
存储 JavaScript Java
Java 中的 String Pool 简介
本文介绍了 Java 中 String 对象及其存储机制 String Pool 的基本概念,包括字符串引用、构造方法中的内存分配、字符串文字与对象的区别、手工引用、垃圾清理、性能优化,以及 Java 9 中的压缩字符串特性。文章详细解析了 String 对象的初始化、内存使用及优化方法,帮助开发者更好地理解和使用 Java 中的字符串。
Java 中的 String Pool 简介

热门文章

最新文章