走进JDK之不可变类String

文中相关源码: String.java

今天来说说 String。

贯穿全文,你需要始终记住这句话,String 是不可变类 。其实前面说过的所有基本数据类型包装类都是不可变类,但是在 String 的源码中,不可变类 的概念体现的更加淋漓尽致。所以,在阅读 String 源码的同时,抽丝剥茧,你会对不可变类有更深的理解。

什么是不可变类 ?

首先来看一下什么是不可变类?Effective Java 第三版 第 17 条 使不可变性最小化 中对 不可变类 的解释:

不可变类是指其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期 (lifetime) 内固定不变 。
为了使类成为不可变,要遵循下面五条规则:

不要提供任何会修改对象状态的方法(也称为设值方法) 。
保证类不会被扩展。 为了防止子类化,一般做法是声明这个类成为 final 的。
声明所有的域都是 final 的。
声明所有的域都为私有的。 这样可以防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象 。
确保对于任何可变组件的互斥访问。 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用 。 并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法( accessor)中返回该对象引用 。 在构造器、访问方法和 readObject 方法(详见第 88 条)中请使用保护性拷贝( defensive copy )技术(详见第50 条) 。

根据这五条原则,来品尝一下 String.java 吧!

类定义

public final class String
 implements java.io.Serializable, Comparable<String>, CharSequence {}

对应原则第二点 保证类不会被扩展,使用 final 修饰。此外:

  • 实现了 Serializable 接口,具备序列化能力
  • 实现了 Comparable 接口,具备比较对象大小能力,根据单字符的大小比较。
  • 实现了 CharSequence 接口,表示是一个字符序列,实现了该接口下的一些方法。

字段

private final char value[]; // 储存字符串
private int hash; // 哈希值,默认为 0
private static final long serialVersionUID = -6849794470754667710L; // 序列化标识

看起来 String 是一个独立的对象,其实它是使用基本数据类型的数组 char[] 实现的。作为使用者,我们不需要打开 String 的黑匣子,直接根据它的 API 使用就可以了,这正是 Java 的封装性的体现。但是作为开发者,我们就有必要一探究竟了。

private final char value[] , 对应原则中第三条和第四条,声明所有的域都是 final 的 ,声明所有的域都为私有的。看到这里,你大概明白了一点为什么 String 不可变。因为真正用来存储字符串的字符数组是 final 修饰的,是不可变的。

构造函数

String 的构造函数很多,大致可以分为以下四种:

无参构造

public String() {
 this.value = "".value;
}

无参构造默认构建一个空字符串。鉴于 String 是不可变类,所以此构造器并没有什么意义,一般你也不会去构建一个不可变的空字符串对象。

参数是 byte[]

public String(byte bytes[]) {}
public String(byte bytes[], int offset, int length) {}
public String(byte bytes[], Charset charset) {}
public String(byte bytes[], String charsetName) {}
public String(byte bytes[], int offset, int length, Charset charset) {}
public String(byte bytes[], int offset, int length, String charsetName) {}

已经废弃的就不再列举了。上面这些构造函数都差不多,最后都是调用 StringCoding.decode() 方法将字节数组转换为字符数组,再赋值给 value[]。这里要注意一点,参数未指定编码格式的话,默认使用系统的编码格式,如果没有获取到系统编码格式,则使用 ISO-8859-1 格式。

参数是 char[]

参数是 char[] 的构造函数有 3 个,逐个看一下:

public String(char value[]) {
 this.value = Arrays.copyOf(value, value.length);
}

为了保证不可变性,并没有直接赋值,this.value = value。而是使用 Arrays.copy() 方法将参数中的字符数组内容拷贝到 value[] 中。防止参数中字符数组的改变破坏了不可变性。

第二个:

public String(char value[], int offset, int count) {
 if (offset < 0) {
 throw new StringIndexOutOfBoundsException(offset);
 }
 if (count <= 0) {
 if (count < 0) {
  throw new StringIndexOutOfBoundsException(count);
 }
 if (offset <= value.length) {
  this.value = "".value;
  return;
 }
 }
 // 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);
}

和上面的构造函数一样,只是截取了参数中字符数组的一部分来构建字符串。

第三个:

/*
 * Package private constructor which shares value array for speed.
 * this constructor is always expected to be called with share==true.
 * a separate constructor is needed because we already have a public
 * String(char[]) constructor that makes a copy of the given char[].
 *
 * 仅当前包可使用。
 * 直接将 this.value 指向参数中的 char[],不再进行 copy 操作
 * 性能好,节省内存,外包不可使用,也不会破坏不可变性
 */
 String(char[] value, boolean share) {
 // assert share : "unshared not supported";
 this.value = value;
 }

这里的 share 一般只能为 true,虽然并没有使用到。增加这个参数是为了和第一个构造函数区分开来,表示 value[] 共享了参数中的字符数组,因为这里是直接赋值的,并没有使用 Arrays.copy() 。那这不是破坏了 String 的不可变性吗?其实并没有,因为你根本没法调用这个构造函数,它的包私有的。但是在 JDK 内部你可以发现它的身影,

没有了 copy 操作,大幅提高了效率。但是为了保证不可变性,外部是不能调用的。

其他构造函数

// 基于代码点
public String(int[] codePoints, int offset, int count) {} 

// 基于 StringBuffer,需要同步
public String(StringBuffer buffer) {
 synchronized(buffer) {
 this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
 }
}

// 基于 StringBuilder,不需要同步
public String(StringBuilder builder) {
 this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

方法

回头再看一下 String 的不可变性,value[] 是 private final 修饰的,这样就真的可以保证不可变吗?

final char[] value = {'a','b','c'};
value[1] = 'd';

这是不是就轻而易举的打破了不可变性?final value[] 只能保证其引用不能再指向其他内存地址,但是其真正的值还是可以改变的。所以仅仅通过一个 final 是无法保证其值不变的,如果类本身提供方法修改实例值,那就没有办法保证不变性了。对应原则中第一条,不要提供任何会修改对象状态的方法,String 百分之百做到了这一点,它没有对外提供任何可以修改 value 的方法。

在 String 中有许多对字符串进行操作的函数,例如 substring concat replace replaceAll 等等,这些函数是否会修改类中的 value 域呢?下面就来看一看源码。

substring(int beginIndex)

public String substring(int beginIndex) {
 if (beginIndex < 0) {
 throw new StringIndexOutOfBoundsException(beginIndex);
 }
 int subLen = value.length - beginIndex;
 if (subLen < 0) {
 throw new StringIndexOutOfBoundsException(subLen);
 }
 // beginIndex 不为 0, 返回一个 新的 String 对象
 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

concat(String str)

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); // 返回新的 String 对象
}

replace(char oldChar, char newChar)

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;
}

String 类的方法实现都相对简单,但是无一例外,它们绝对不会去修改 value[] 的值,需要返回 String 对象的话,都会重新 new 一个。正像原则第五条中所说的,确保对于任何可变组件的互斥访问。 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。

String.intern()

public native String intern();

这个方法比较特殊,是个本地方法。如果该字符串在常量池中已经存在,直接返回其引用。如果不存在,存入常量池再返回其引用。在下一篇文章中会进行详细介绍。

其他方法的源码就不列举了,感兴趣的可以到我上传的 jdk 源码 看看,String.java,添加了部分注释。

不可变类的好处

从头到尾都在说不可变类,那么它有哪些好处呢?

  • 不可变对象比较简单。
  • 不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以被自由地共享。
  • 不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
  • 不可变对象为其他对象提供了大量的构件。无论是可变的还是不可变的对象。
  • 不可变对象无偿地提供了失败的原子性。

不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。所以当需要大量字符串对象的时候,String 就成了性能瓶颈,这也催生了 StringBuffer 和 StringBuilder。后面会单独分析。

String 真的不可变吗 ?

学习就是自己不断打自己脸的过程。真的没有办法修改 String 对象的值吗?答案肯定是否定的,反射机制可以做到很多平常做不到的事情。

String str = "123";
System.out.println(str);
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = '3';
System.out.println(str);

执行结果:

123
133

通过反射,的确修改了 value[] 的值。

总结

借着 String 源码,说了说 不可变类。简单总结一下 String 做了哪些措施来保证不可变性:

  • value[] 使用 private final 修饰
  • 构造函数中复制实参的值给 value[]
  • 不对外提供任何修改 value[] 值的方法
  • 需要返回 String 的方法,绝不返回原对象,都是重新 new 一个 String 返回

下一篇还是写 String , 说说 String 在内存中的位置和字符串常量池的一些知识,以及 String 相关的常见面试题。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • JAVA JDK8 List获取属性列表

    概述 在JDK 1.8里,可以使用如下代码获取List元素对象中某个属性的列表. package test; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class ListAttrTest { public static void main(String[] args) { List<Coupon> couponList = new ArrayLis

  • JAVA JDK8 List分组的实现和用法

    概述 对List进行分组是日常开发中,经常遇到的,在JDK 8中对List按照某个属性分组的代码,超级简单. package test; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util

  • 关于JAVA_HOME路径修改之后JDK的版本依然不更改的解决办法

    今天重新配置Java的时候出现了一点问题,下面主要讲一下自己的解决方案: 问题描述: 今天想更改一下本机JDK的版本,发现更改之后使用 java -version命令,出现的JDK版本并没有变换. 查找原因: 系统目录里面可能有java.exe,导致优先调用了系统目录中的java.exe:刚安装的jdk自动增加了path内容,所增加的内容(指向的路径)下存在java.exe,且在path内容中该路径的顺序位于你自己配置java的路径前面(笔者的坑在这). 解决办法: 将%JAVA_HOME%/b

  • IDEA-Maven项目的jdk版本设置方法

    在 Intellij Idea 中,我们需要设置 Settings 中的 Java Compiler 和 Project Structure 中的 Language Level 中的 jdk 版本为自己目前使用的版本,否则会经常提示我们 jdk 版本不正确导致的语法错误. 比如配置为 jdk1.8 : 但是在 Maven 项目中,Java Compiler 和 Language level 中的设置会自动变回到 pom.xml 文件中设置的 jdk 版本或者默认的 jdk1.5 版本.所以我们需

  • java8、jdk8日期转化成字符串详解

    java8.jdk8日期转化成字符串 新建日期工具类:DateUtils 新建方法:parseDate 实现方法parseDate public static String parseDate(LocalDate localDate,String pattern) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern); return localDate.format(dateTimeFormatt

  • 如何在JDK 9中更简洁使用 try-with-resources 语句

    在 JDK 7 之前,资源需要手动关闭 例如下面一个很常见的文件操作的例子: Charset charset = Charset.forName("US-ASCII"); String s = ...; BufferedWriter writer = null; try { writer = Files.newBufferedWriter(file, charset); writer.write(s, 0, s.length()); } catch (IOException x) {

  • JAVA JDK8 List分组获取第一个元素的方法

    概述 在JAVA JDK8 List分组的实现和用法一文中介绍了JDK 8如何对list进行分组,但是没有提到如何在分组后,获取每个分组的第一个元素.其实这个也很简单,代码如下: package test; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import java.util.ArrayList; import java.util.List; imp

  • 走进JDK之不可变类String

    文中相关源码: String.java 今天来说说 String. 贯穿全文,你需要始终记住这句话,String 是不可变类 .其实前面说过的所有基本数据类型包装类都是不可变类,但是在 String 的源码中,不可变类 的概念体现的更加淋漓尽致.所以,在阅读 String 源码的同时,抽丝剥茧,你会对不可变类有更深的理解. 什么是不可变类 ? 首先来看一下什么是不可变类?Effective Java 第三版 第 17 条 使不可变性最小化 中对 不可变类 的解释: 不可变类是指其实例不能被修改的

  • JAVA不可变类(immutable)机制与String的不可变性(推荐)

    一.不可变类简介 不可变类:所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值.如JDK内部自带的很多不可变类:Interger.Long和String等. 可变类:相对于不可变类,可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类. 二.不可变类的优点 说完可变类和不可变类的区别,我们需要进一步了解为什么要有不可变类?这样的特性对JAVA来说带来怎样的好处? 1.线程安全 不可变对象是线程安全的,在线程之间可以相互共享,不需要利用特殊机制来保证同步问题,因

  • Java不可变类机制浅析

    不可变类(Immutable Class):所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值.如JDK内部自带的很多不可变类:Interger.Long和String等. 可变类(Mutable Class):相对于不可变类,可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类. 不可变类的特性对JAVA来说带来怎样的好处? 1)线程安全:不可变对象是线程安全的,在线程之间可以相互共享,不需要利用特殊机制来保证同步问题,因为对象的值无法改变.可以降低并发错误

  • Java常用类String的面试题汇总(java面试题)

    1.比较两个字符串时使用"=="还是equals()方法? 当然是equals方法."=="测试的是两个对象的引用是否相同,而equals()比较的是两个字符串的值是否相等.简单来说,基本数据类型都可以使用==.而引用类型使用==比较不了. 2.如何将字符串转化成int? 使用包装类Integer.Integer.valueOf("2");其他基本数据类型都是类似 3.为什么在Java中存储密码要使用char[],而不使用String. 因为St

  • JDK源码分析之String、StringBuilder和StringBuffer

    前言 本文主要介绍了关于JDK源码分析之String.StringBuilder和StringBuffer的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧 String类的申明 public final class String implements java.io.Serializable, Comparable<String>, CharSequence {-} String类用了final修饰符,表示它不可以被继承,同时还实现了三个接口, 实现Serializa

  • 如何在 Java 中实现不可变类

    前言 面向对象的编程通过封装可变动的部分来构造能够让人读懂的代码,函数式编程则是通过最大程度地减少 可变动的部分来构造出可让人读懂的代码. - Michael Feathers,Working with Legacy Code 一文的作者 在这一部分中,我讨论的是函数式编程的基石之一:不变性.一个不可变对象的状态在其构造完成之后就不可改变,换句话说,构造函数是唯一一个您可以改变对象的状态的地方.如果您想要改变一个不可变对象的话,您不会改变它,而是使用修改后的值来创建一个新的对象,并把您的引用指向

  • JDK反序列化时修改类的全限定性名解析

    应用场景 SpringSecurityOAuth2有一个奇葩的设计,那就是它将与access_token相关的所有属于都封装到OAuth2AccessToken中,然后保存时会直接将该对象序列化成字节写入数据库.我们在资源服务器中想要直接读数据库来取出access_token来验证令牌的有效性,然而又不想引入SpringSecurity的相关依赖污染jar包.这时可以将SpringSecurity中OAuth2AccessToken的唯一实现类DefaultOAuth2AccessToken的源

  • 解决springboot 实体类String转Date类型的坑

    目录 springboot 实体类String转Date类型 Date解析String类型的参数 springboot 实体类String转Date类型 前端传入一个String的时间字符串如:2019-07-18 23:59:59 后端实体类要在头顶加注解: @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") 不然会出现报错 Date解析String类型的参数 1.首先建立String to Date 的解析实现 import org.a

  • JDK源码Enum类原理及代码实例解析

    正文 一 概述 枚举类型是 JDK 5 之后引进的一种非常重要的引用类型,可以用来定义一系列枚举常量,使用 enum 来表示枚举可以更好地保证程序的类型安全和可读性 实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类, 也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类 使用举例 public class EnumTest { enum MyCo

  • 标准C++类string的Copy-On-Write技术

    标准C++类std::string的内存共享和Copy-On-Write技术 陈皓 1.概念 Scott Meyers在<More Effective C++>中举了个例子,不知你是否还记得?在你还在上学的时候,你的父母要你不要看电视,而去复习功课,于是你把自己关在房间里,做出一副正在复习功课的样子,其实你在干着别的诸如给班上的某位女生写情书之类的事,而一旦你的父母出来在你房间要检查你是否在复习时,你才真正捡起课本看书.这就是"拖延战术",直到你非要做的时候才去做. 当然,

随机推荐