好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

走进JDK之不可变类String

文中相关源码: string.java

今天来说说 string。

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

什么是不可变类 ?

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

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

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

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

类定义

?

1

2

public final class string

  implements java.io.serializable, comparable<string>, charsequence {}

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

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

字段

?

1

2

3

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 的构造函数很多,大致可以分为以下四种:

无参构造

?

1

2

3

public string() {

  this .value = "" .value;

}

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

参数是 byte[]

?

1

2

3

4

5

6

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 个,逐个看一下:

?

1

2

3

public string( char value[]) {

  this .value = arrays.copyof(value, value.length);

}

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

第二个:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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

}

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

第三个:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

/*

  * 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 操作,大幅提高了效率。但是为了保证不可变性,外部是不能调用的。

其他构造函数

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

// 基于代码点

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 修饰的,这样就真的可以保证不可变吗?

?

1

2

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

value[ 1 ] = 'd' ;

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

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

substring(int beginindex)

?

1

2

3

4

5

6

7

8

9

10

11

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)

?

1

2

3

4

5

6

7

8

9

10

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)

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

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()

?

1

public native string intern();

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

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

不可变类的好处

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

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

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

string 真的不可变吗 ?

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

?

1

2

3

4

5

6

7

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 相关的常见面试题。

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

原文链接:https://juejin.im/post/5ca30c31f265da30c1724a04

查看更多关于走进JDK之不可变类String的详细内容...

  阅读:10次