Java设计模式之immutable(不可变)模式

immutable简介

不可变对象永远不会发生改变,其字段的值只在构造函数运行时设置一次,其后就不会再改变。例如JDK中常见的两种基本数据类型String和Integer,它们都是不可变对象。为了理解immutable与mutable的区别,可以看看下面的一段代码:

1package date0804.demo2; 2 3import java.awt.Point; 4 5public class ImmutableString { 6 7 public static void main(String[] args) { 8 //String,immutable 9 String str = new String("new book"); 10 System.out.println(str); 11 str.replaceAll("new", "old"); 12 System.out.println(str); 13 14 //Point,mutable 15 Point point = new Point(0,0); 16 System.out.println(point); 17 point.setLocation(1, 0); 18 System.out.println(point); 19 } 20}

 运行结果为

1new book 2new book 3java.awt.Point[x=0,y=0] 4java.awt.Point[x=1,y=0]

我们看到point的值发生了改变,而str的值并没有改变。String类型的对象一旦在内存中创建,它的值就不会发生改变,改变的只能是对该对象引用的指针。若想创建可以改变的String,则可以使用StringBuilder和StringBuffer类创建字符串,更加灵活,可以进行添加、插入和追加新的内容等操作。

**[拓展,下面一段代码与immutable无关]**再看另外一个有趣的String实例:

1package date0804.demo2; 2 3 4public class ImmutableString { 5 6 public static void main(String[] args){ 7 8 String str=new String("xyz"); 9 change(str); 10 System.out.println(str); 11 } 12 13 public static void change(String s) { 14 s="xml"; 15 } 16}

上面这段代码的输出结果为:

xyz

根据我的理解,Java中方法的参数是按值传递的,在这里,Java在内存堆上创建了一个字符串"xyz",然后将该字符串的内存地址赋给了str变量,即str中存储了"xyz"内存地址的引用,而change()方法是按值传递的,相当于再创建了一个字符串对象"xml",而这个对象又有另外一个不同的地址引用,并非之前的x存储的引用。

**在并发编程中,一种被普遍认可的原则就是:尽可能的使用不可变对象来创建简单、可靠的代码。****不可变对象特别有用,由于创建后不能被修改,所以不会出现由于线程干扰产生的错误或是内存一致性错误。**使用不可变对象降低了垃圾回收所产生的额外开销,也减少了用来确保使用可变对象不出现并发错误的一些额外代码。

immutable的优点

不可变对象可以在很大程度上简化我们的程序代码,并且具有以下优点:

——不可变对象易于构造,使用和测试;

——自动地为线程安全型,避免了一些繁杂的同步问题;

——不需要一个复制构造函数(copy constructor);

——不需要一个clone的实现;

——允许hashCode()方法的懒汉模式实现,并且缓存它的返回值;

——当作为一个字段时,不需要复制;

——一旦构造以后,具有类型不变性,不用检查;

——[引用名言]if an immutable object throws an exception, it's never left in an undesirable or indeterminate state。

immutable的设计方法

——确保类不会被覆写,即该类不会被继承,实现这一点要加上修饰符final;或者使用静态工厂创建方法,确保构造函数私有的;

——所有的字段必须是私有的,并且加上修饰符final;

——所有的设置只需简单的一个构造函数即可,不要使用空构造函数,不要使用Javabean风格的构造函数;

——不要提供任何可以改变对象的方法,不仅仅是setter,一切能够修改对象状态的方法都要避免;

——如果该类有可变字段的对象,则必须进行保守型复制,并且只能由该类自身进行修改。

immutable类的代码实例

1final public class ImmutableRGB { 2 3 // 常量字段 4 final private int red; 5 final private int green; 6 final private int blue; 7 final private String name; 8 9 //私有方法,检查三基色的值是否在0~255之间,若不符合,则抛出异常 10 private void check(int red, 11 int green, 12 int blue) { 13 if (red < 0 || red > 255 14 || green < 0 || green > 255 15 || blue < 0 || blue > 255) { 16 throw new IllegalArgumentException(); 17 } 18 } 19 20 //唯一的一个构造方法,先检查字段是否合法,然后传递所有字段 21 public ImmutableRGB(int red, 22 int green, 23 int blue, 24 String name) { 25 check(red, green, blue); 26 this.red = red; 27 this.green = green; 28 this.blue = blue; 29 this.name = name; 30 } 31 32 //get方法 33 public int getRGB() { 34 return ((red << 16) | (green << 8) | blue); 35 } 36 37 //get方法 38 public String getName() { 39 return name; 40 } 41 42 //对颜色值进行求逆 43 public ImmutableRGB invert() { 44 return new ImmutableRGB(255 - red, 45 255 - green, 46 255 - blue, 47 "Inverse of " + name); 48 } 49}

从上面一段代码可以看出,这是一个不可变的类,其对象是一个不可变对象。对象一旦创建,就不会被修改。最后的一段代码,反映了一旦对对象进行修改,不要返回对象本身,而是拷贝一份返回。这种不可变对象很好地解决了并发编程中的竞争问题,不用考虑线程的同步,因为对象一旦创建,不会改变。

immutable类的不正确使用

下面一段代码显示了表面上看起来是一个immutable类,实际上则是一个mutable类:

1import java.util.Date; 2 public final class BrokenPerson 3 { 4 private String firstName; 5 private String lastName; 6 private Date dob; 7 8 public BrokenPerson( String firstName, 9 String lastName, Date dob) 10 { 11 this.firstName = firstName; 12 this.lastName = lastName; 13 this.dob = dob; 14 } 15 16 public String getFirstName() 17 { 18 return this.firstName; 19 } 20 public String getLastName() 21 { 22 return this.lastName; 23 } 24 public Date getDOB() 25 { 26 return this.dob; 27 } 28 }

这段代码看起来没有问题,但是 请注意dob是一个Date对象,则说明它是一个mutable对象字段,如果客户用下面一段代码去调用:

1Date myDate = new Date(); 2 BrokenPerson myPerson = 3 new BrokenPerson( "David", "O'Meara", myDate ); 4 System.out.println( myPerson.getDOB() ); 5 myDate.setMonth( myDate.getMonth() + 1 ); 6 System.out.println( myPerson.getDOB() );

那么,返回的结果有可能是

1Mon Mar 24 21:34:16 GMT+08:00 2013 2Thu Apr 24 21:34:16 GMT+08:00 2013

因此,我们看到myPerson实际上是一个mutable对象,问题就出在BrokenPerson具有一个mutable对象的字段,那么 我们应该使用它的拷贝来创建对象,而不是直接引用:

1import java.util.Date; 2 public final class BetterPerson 3 { 4 private String firstName; 5 private String lastName; 6 private Date dob; 7 8 public BetterPerson( String firstName, 9 String lastName, Date dob) 10 { 11 this.firstName = firstName; 12 this.lastName = lastName; 13 this.dob = new Date( dob.getTime() ); 14 } 15 //....... 16 public Date getDOB() 17 { 18 return new Date( this.dob.getTime() ); 19 }

注意,对于所有mutable的字段,应该要使用拷贝来在类中"进进出出",这样才比较安全。

总结

本文简单介绍了immutable的优点及其设计方法,不可变对象最大的缺点就是创建对象的开销,因为每一步操作都会产生一个新的对象,然而合理地使用immutable可以带来极大的好处。不可变类最适合表示抽象数据类型(如数字、枚举类型或颜色)的值。Java 类库中的基本数据类型的包装类(如Integer 、 Long 和 Float )都是不可变的,其它数字类型(如 BigInteger 和 BigDecimal )也是不可变的。表示复数或任意精度的有理数的类将比较适合设计为不可变类。甚至包含许多离散值的抽象类型(如向量或矩阵)也很适合设计成不可变类,这取决于你的应用程序。
另一个适合用不可变类实现的好示例就是事件 。事件的生命期较短,而且常常会在创建它们的线程之外的线程中消耗,所以使它们成为不可变的是利大于弊。大多数 AWT 事件类都没有严格的作为不可变类来实现。同样地,在通信系统的组件间进行消息传递,将消息对象设计成不可变的是明智的。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

一文弄懂Java中String的所有小秘密

简介String是java中非常常用的一个对象类型。可以说java中使用最多的就是String了。那么String到底有哪些秘密呢?接下来本文将会一一讲解。String是不可变的String是不可变的,官方的说法叫做immutable或者constant。String的底层其实是一个Char的数组。priv

10. Scala数据结构(上)

10.1数据结构特点   10.1.1Scala集合基本介绍       uml统一建模语言      1)Scala同时支持不可变集合和可变集合,不可变集合可以安全的并发访问      两个主要的包      不可变集合:scala.collection.immutable