不可变对象想必大部分朋侪都不生疏,人人在日常平凡写代码的历程当中100%会运用到不可变对象,比方最常见的String对象、包装器对象等,那末究竟为什么Java言语要这么设想,真正企图和斟酌点是什么?可以一些朋侪没有细想过这些题目,本日我们就来聊聊跟不可变对象有关的话题。
一.什么是不可变对象
下面是《Effective Java》这本书关于不可变对象的定义:
不可变对象(Immutable Object):对象一旦被建立后,对象一切的状况及属性在其生命周期内不会发作任何变化。
从不可变对象的定义来看,实在比较简朴,就是一个对象在建立后,不能对该对象举行任何变动。比方下面这段代码:
public class ImmutableObject { private int value; public ImmutableObject(int value) { this.value = value; } public int getValue() { return this.value; } }
由于ImmutableObject不供应任何setter要领,而且成员变量value是基础数据范例,getter要领返回的是value的拷贝,所以一旦ImmutableObject实例被建立后,该实例的状况没法再举行变动,因而该类具有不可变性。
再比方我们日常平凡用的最多的String:
public class Test { public static void main(String[] args) { String str = "I love java"; String str1 = str; System.out.println("after replace str:" + str.replace("java", "Java")); System.out.println("after replace str1:" + str1); } }
输出效果:
从输出效果可以看出,在对str举行了字符串替代替代以后,str1指向的字符串对象依然没有发作变化。
二.深切明白不可变性
我们是不是斟酌过一个题目:假如Java中的String、包装器类设想成可变的ok么?假如String对象可变了,会带来哪些题目?
我们这一节主要来聊聊不可变对象存在的意义。
1)让并发编程变得更简朴
说到并发编程,可以许多朋侪都邑以为最苦恼的事变就是怎样处置惩罚共享资本的互斥接见,可以稍不留神,就会致使代码上线后涌现稀里糊涂的题目,而且大部分并发题目都不是太轻易举行定位和复现。所以纵然黑白常有履历的顺序员,在举行并发编程时,也会异常的警惕,心田警惕翼翼。
大多数状况下,关于资本互斥接见的场景,都是采纳加锁的体式格局来完成对资本的串行接见,来保证并发平安,如synchronize关键字,Lock锁等。然则这类计划最大的一个难点在于:在举行加锁和解锁时需要异常地郑重。假如加锁或许解锁机遇稍有一点误差,就可以会激发重大题目,但是这个题目Java编译器没法发明,在举行单元测试、集成测试时也发明不了,以至顺序上线后也能一般运转,然则可以倏忽在某一天,它就稀里糊涂地涌现了。
但是人类是机灵的,既然采纳串行体式格局来接见共享资本这么轻易涌现题目,那末有无其他方法来处理呢?答案是一定的。
事实上,引发线程平安题目的根本原因在于:多个线程需要同时接见同一个共享资本。
假如没有共享资本,那末多线程平安题目就天然处理了,Java中供应ThreadLocal机制就是采用的这类头脑。
但是大多数时刻,线程间是需要运用共享资本互通信息的,假如共享资本在建立以后就完全不再变动,犹如一个常量,而多个线程间并发读取该共享资本是不会存在线上平安题目的,由于一切线程不管什么时候读取该共享资本,老是能获取到一致的、完全的资本状况。
不可变对象就是如许一种在建立以后就不再变动的对象,这类特征使得它们天生支撑线程平安,让并发编程变得更简朴。
我们来看一个例子,这个例子来源于:http://ifeve.com/immutable-objects/
public class SynchronizedRGB { private int red; // 色彩对应的赤色值 private int green; // 色彩对应的绿色值 private int blue; // 色彩对应的蓝色值 private String name; // 色彩称号 private void check(int red, int green, int blue) { if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) { throw new IllegalArgumentException(); } } public SynchronizedRGB(int red, int green, int blue, String name) { check(red, green, blue); this.red = red; this.green = green; this.blue = blue; this.name = name; } public void set(int red, int green, int blue, String name) { check(red, green, blue); synchronized (this) { this.red = red; this.green = green; this.blue = blue; this.name = name; } } public synchronized int getRGB() { return ((red << 16) | (green << 8) | blue); } public synchronized String getName() { return name; } }
比方一个有个线程1实行了以下代码:
SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black"); int myColorInt = color.getRGB(); // Statement1 String myColorName = color.getName(); // Statement2
然后有别的一个线程2在Statement 1以后、Statement 2之前挪用了color.set要领:
color.set(0, 255, 0, "Green");
那末在线程1中变量myColorInt的值和myColorName的值就会不婚配。为了防止涌现如许的效果,必需要像下面如许把这两条语句绑定到一块实行:
synchronized (color) { int myColorInt = color.getRGB(); String myColorName = color.getName(); }
假如SynchronizedRGB是不可变类,那末就不会涌现这个题目,比方将SynchronizedRGB改成下面这类完成体式格局:
public class ImmutableRGB { private int red; private int green; private int blue; private String name; private void check(int red, int green, int blue) { if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) { throw new IllegalArgumentException(); } } public ImmutableRGB(int red, int green, int blue, String name) { check(red, green, blue); this.red = red; this.green = green; this.blue = blue; this.name = name; } public ImmutableRGB set(int red, int green, int blue, String name) { return new ImmutableRGB(red, green, blue, name); } public int getRGB() { return ((red << 16) | (green << 8) | blue); } public String getName() { return name; } }
由于set要领并没有转变本来的对象,而是新建立了一个对象,所以不管线程1或许线程2怎样挪用set要领,都不会涌现并发接见致使的数据不一致的题目。
2)消弭副作用
许多时刻一些很严重的bug是由于一个很小的副作用引发的,而且由于副作用一般不轻易被发觉,所以很难在编写代码以及代码review历程当中发明,而且纵然发明了也可以会消费很大的精神才定位出来。
举个简朴的例子:
class Person { private int age; // 岁数 private String identityCardID; // 身份证号码 public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getIdentityCardID() { return identityCardID; } public void setIdentityCardID(String identityCardID) { this.identityCardID = identityCardID; } } public class Test { public static void main(String[] args) { Person jack = new Person(); jack.setAge(101); jack.setIdentityCardID("42118220090315234X"); System.out.println(validAge(jack)); // 后续运用可以没有发觉到jack的age被修正了 // 为后续埋下了不轻易发觉的题目 } public static boolean validAge(Person person) { if (person.getAge() >= 100) { person.setAge(100); // 此处产生了副作用 return false; } return true; } }
validAge函数自身只是对age大小举行推断,然则在这个函数内里有一个副作用,就是对参数person指向的对象举行了修正,致使在外部的jack指向的对象也发作了变化。
假如Person对象是不可变的,在validAge函数中是没法对参数person举行修正的,从而防止了validAge涌现副作用,削减了失足的几率。
3)削减容器运用历程失足的几率
我们在运用HashSet时,假如HashSet中元素对象的状况可变,就会涌现元素丧失的状况,比方下面这个例子:
class Person { private int age; // 岁数 private String identityCardID; // 身份证号码 public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getIdentityCardID() { return identityCardID; } public void setIdentityCardID(String identityCardID) { this.identityCardID = identityCardID; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (!(obj instanceof Person)) { return false; } Person personObj = (Person) obj; return this.age == personObj.getAge() && this.identityCardID.equals(personObj.getIdentityCardID()); } @Override public int hashCode() { return age * 37 + identityCardID.hashCode(); } } public class Test { public static void main(String[] args) { Person jack = new Person(); jack.setAge(10); jack.setIdentityCardID("42118220090315234X"); Set<Person> personSet = new HashSet<Person>(); personSet.add(jack); jack.setAge(11); System.out.println(personSet.contains(jack)); } }
输出效果:
所以在Java中,关于String、包装器这些类,我们常常会用他们来作为HashMap的key,试想一下假如这些类是可变的,将会发作什么?效果不可预知,这将会大大增添Java代码编写的难度。
三.怎样建立不可变对象
一般来讲,建立不可变类准绳有以下几条:
1)一切成员变量必需是private
2)最好同时用final润饰(非必需)
3)不供应可以修正原有对象状况的要领
最常见的体式格局是不供应setter要领
假如供应修正要领,需要新建立一个对象,并在新建立的对象上举行修正
4)经由过程组织器初始化一切成员变量,援用范例的成员变量必需举行深拷贝(deep copy)
5)getter要领不能对外泄漏this援用以及成员变量的援用
6)最好不允许类被继续(非必需)
JDK中供应了一系列要领轻易我们建立不可变鸠合,如:
Collections.unmodifiableList(List<? extends T> list)
别的,在Google的Guava包中也供应了一系列要领来建立不可变鸠合,如:
ImmutableList.copyOf(list)
这2种体式格局虽然都能建立不可变list,然则二者是有区分的,JDK自带供应的体式格局现实上建立出来的不是真正意义上的不可变鸠合,看unmodifiableList要领的完成就知道了:
可以看出,现实上UnmodifiableList是将入参list的援用复制了一份,同时将一切的修正要领抛出UnsupportedOperationException。因而假如在外部修正了入参list,现实上会影响到UnmodifiableList,而Guava包供应的ImmutableList是真正意义上的不可变鸠合,它现实上是对入参list举行了深拷贝。看下面这段测试代码的效果便一览无余:
public class Test { public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(); list.add(1); System.out.println(list); List unmodifiableList = Collections.unmodifiableList(list); ImmutableList immutableList = ImmutableList.copyOf(list); list.add(2); System.out.println(unmodifiableList); System.out.println(immutableList); } }
输出效果:
四.不可变对象真的"完全不可转变"吗?
不可变对象虽然具有不可变性,然则不是"完全不可变"的,这里打上引号是由于经由过程反射的手腕是可以转变不可变对象的状况的。
人人看到这里可以有迷惑了,为什么既然能转变,为什么还叫不可变对象?这内里人人不要误解不可变的本意,从不可变对象的意义剖析能看出来对象的不可变性只是用来辅佐协助人人更简朴地去编写代码,削减顺序编写历程当中失足的几率,这是不可变对象的初志。假如真要靠经由过程反射来转变一个对象的状况,此时编写代码的人也应该会意想到此类在设想的时刻就不愿望其状况被变动,从而引发编写代码的人的注重。下面是经由过程反射体式格局转变不可变对象的例子:
public class Test { public static void main(String[] args) throws Exception { String s = "Hello World"; System.out.println("s = " + s); Field valueFieldOfString = String.class.getDeclaredField("value"); valueFieldOfString.setAccessible(true); char[] value = (char[]) valueFieldOfString.get(s); value[5] = '_'; System.out.println("s = " + s); } }
输出效果:
【相干引荐:Java视频教程】
以上就是Java中的不可变对象细致剖析(附代码)的细致内容,更多请关注ki4网别的相干文章!