使用 =
的时候,其实是引用的拷贝。 多个引用指向的其实是同一个对象。
上面的例子中 ArrayList<Integer> list = new ArrayList<>()
在堆内存中创建了ArrayList对象并且把list引用指向该对象的地址。
ArrayList<Integer> list2 = list
把list的引用赋值给list2,两个引用指向的都是上一步创建的对象。
对象拷贝分为深浅拷贝。
对于基本类型来说拷贝只是值传递,拷贝后的对象和原对象的基本类型变量是相互独立的。 以下只讨论引用类型的情况。
如果是引用类型,拷贝的是引用类型的地址值,也就是和原对象的引用指向相同的一块内存区域。 这时候如果对象发生更改。 拷贝对象和原对象都会受到影响。
深拷贝将具有原始对象的所有字段的精确复制,就像浅复制一样。但是,另外,如果原始对象有任何对其他对象的引用作为字段,那么也可以通过调用这些对象上的clone()
方法来创建这些对象的副本。这意味着克隆对象和原始对象将是100%不相交的。它们是100%相互独立的。对克隆对象所做的任何更改都不会反映在原始对象中,反之亦然。
clone()
方法
如果是浅拷贝,只需要让外层的对象重写clone()
方法。
如果要实现深拷贝,则需要逐层实现Cloneable接口实现clone()
方法。clone()
方法或者手动构造对象都很麻烦。
这时候可以考虑使用Serializable
反序列化来构建一个新的对象。 反序列化出的对象和原对象内存地址是完全独立的,属于深拷贝。
二、不可变类Immutable
上面提到了通过深拷贝可以创建和原对象互不影响的拷贝,但是维护起来非常麻烦。
Java提供了另外一种方式来保证这种独立性, 他在被创建后其内部状态就不能被修改, 也称作Immutable对象。 JDK中的Immutable对象包括String、基本类型的包装类(Integer,Double,Float…)、BigDecimal,BigInteger。
一个Immutable类想要维持不可变性, 需要遵循以下规则:
类用final
修饰 或者 私有构造器
不管是final
修饰类还是私有构造器,都是为了防止被继承。
如果不可变类能被继承,由于父类引用指向子类的实例时,很明显没法约束每个子类的不可变性,那么父类的不可变性就会遭到破坏。
类中的属性声明为private
,并且不对外界提供setter
方法
从访问级别上控制不可变性。
类中的属性声明为final
如果属性是基本类型,那么声明为final
后就不能改变。
如果属性是引用类型, final
只是声明这个对象的引用不能改变, 注意对象的属性还是可以改变的。所以有第四点来补充
如果类中存在可变类的属性,当访问他的时候需要进行保护性拷贝。
如果类中存在可变类的变量,虽然已经对他加上了final
修饰符,但这仅仅表示这个变量的引用不能指向别的地址。 但是还是可以通过可变属性的引用来修改他可变类内部的属性,从而破坏可变类对象调用者的不可变性。
在构造器, 访问方法, 和序列化的readObject
方法中,如果用到了这个可变对象的变量, 需要对他进行保护性拷贝, 避免通过可变的引用影响到他的调用者。
三、保护性拷贝
在构造器,getter
方法,序列化的readObject
方法(隐式构造器)中,进行保护性拷贝(defensive copies)来返回对象的拷贝 而不是 对象本身。
看EffectiveJava中的例子,Period是一个描述日期的类,他的构造方法进行了参数合法性检查start < end
// Broken "immutable" time period classpublic final class Period { private final Date start; private final Date end; /** * @param start the beginning of the period * @param end the end of the period; must not precede start * @throws IllegalArgumentException if start is after end * @throws NullPointerException if start or end is null */ public Period(Date start, Date end) { if (start.compareTo(end) > 0) throw new IllegalArgumentException( start + " after " + end); this.start = start; this.end = end; } public Date start() { return start; } public Date end() { return end; } ... // Remainder omitted}
构造器
在构造方法执行后,由于Date是不可变对象,可以引用start,end所指向的变量进行外界的修改。
// Attack the internals of a Period instanceDate start = new Date();Date end = new Date();Period p = new Period(start, end);end.setYear(78); // Modifies internals of p!
显然这里不希望在对象初始化后受到外界的影响来破坏start < end的约束
进行保护性拷贝后,直接使用参数构建一个新的对象,这样外部的修改根本不会影响到新的对象。
// Repaired constructor - makes defensive copies of parameterspublic Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) throw new IllegalArgumentException( this.start + " after " + this.end);}
getter
方法
同理,对于getter
方法暴露宿主类内部可变对象的引用时,也要进行保护性拷贝防止外部通过引用来修改,影响到宿主类。 ```java // Repaired accessors - make defensive copies of internal fields public Date start() { return new Date(start.getTime()); }public Date end() { return new Date(end.getTime()); }
<a name="t0Odx"></a>### `readObject()`如果对象实现了`Serializable`,在反序列化`readObject`方法中,序列化反序列化是通过流的方式进行的,攻击者可以伪造一个流来修改对象内可变参数,对于这些可变参数也要进行保护性拷贝。```java// readObject method with defensive copying and validity checkingprivate void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); // Defensively copy our mutable components start = new Date(start.getTime()); end = new Date(end.getTime()); // Check that our invariants are satisfied if (start.compareTo(end) > 0) throw new InvalidObjectException(start +" after "+ end);}
clone()
方法(因为对于不可变类可能是没有final修饰的,他的子类可能会重写clone()
方法)private static void test2() { //修改不可变类String的值 String strKey = “key”; String strValue = “value”; HashMap map = new HashMap<>(); map.put(strKey, strValue); HashMap maps = new HashMap<>(); StringW strwValue = new StringW(“value”); maps.put(strKey, strwValue);
//修改不可变类的值strValue = "value1111";System.out.println(map.get("key"));//修改可变类的值strwValue.value = "value11111";System.out.println(maps.get(strKey).value);
}
打印结果```javavaluevalue11111
可以看到String的修改并没有影响到他的调用者,而自定义的可变类StringW的修改影响到了他的调用者。 这是因为Java中说的对象分为 对象开辟的内存 和 指向该内存地址的引用两部分。
valueOf(char c)
创建对象的时候会优先取静态内部类CharacterCache缓存的值。 ```java public static Character valueOf(char c) { if (c <= 127) { // must cache return CharacterCache.cache[(int)c];
} return new Character(c); }private static class CharacterCache { private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];static { for (int i = 0; i < cache.length; i++) cache[i] = new Character((char)i);}
}
<a name="Z8hdA"></a>## 五、破坏不可变性的方法通过反射可以绕过不可变类的限制,从而修改他内部的属性来破坏不可变性。```javaString str = "12345";//获取String类中的value字段Field valueFieldOfString = String.class.getDeclaredField("value");//改变value属性的访问权限valueFieldOfString.setAccessible(true);//获取s对象上的value属性的值char[] value = (char[]) valueFieldOfString.get(str);//修改数组末位value[4] = '0'; System.out.println("str = " + str);
输出结果
str = 12340
可见通过反射,可以破坏不可变类的不可变性。
clone()
方法或者手动构造对象来实现。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。