最近再次拜读了Effective Java一书,确实很有感悟,第一次读的时候我的java基础还很不牢,代码积累也不多,读起来总是云里雾里,而这一次读的时候,确实有很多共鸣,感悟,在书上自己写了一些心得体会,俗话说好记性不如烂笔头,也记录到博客里,有需要的时候可以查阅一下
第一章 引言
引言部分就没啥说的了,就是介绍一下本书
第二章 创建和销毁对象
第一条:考虑用静态工厂方法代替构造器
其实这里指的静态工厂方法,通俗易懂的来说就是通过一些自定义的静态方法,return new一个该类或者该类的子类的实体,相比于普通的构造方法,优势有:
1,因为构造方法必须和类名保持一致,所以自定义的静态方法的命名更有指向性,而不是需要看参数及注释才看得懂这个构造方法用于构造什么样的对象
2,可以用静态方法返回相同对象,保持唯一性,并且节约资源
3,可以返回其子类的实体
4,很多情况下代码会更加简洁易懂
当然,并不是说所有构造方法都应该用静态方法来代替,它的使用场景通常是用于需要构造不同属性的同一个类的对象,或者是为了保持唯一性
第二条:遇到多个构造器参数时要考虑用构建器
在新建一个具有很多属性的对象的情况下,我们需要考虑到是否有一些特殊情况,比方说这个对象的属性很多,并且其中有很多可选属性,这种情况下用普通的方式肯定效率很低且极易出错,举个例子,假设有3个属性,可以两两任选,那就至少得写3个方法,4个就得6个,假设有几十个属性,这个实体类算是没法维护了。。。。。。
这种情况下有两种解决方法,一种是用的比较广的JavaBean,即用一个无参方法构建对象,然后用set方法去写参数,这样无论有多少个参数,写好对应的set方法就够了
但是文中提到它的缺点,即每传入一个参数,调用一次set方法,以为它没有顺序性可言,所以这期间JavaBean可能处于一种不一致的状态,所以在线程相关的情况下,如果在错误的时间调用了这个未完成的JavaBean,将会导致错误,并且该错误和具体发生错误的代码大相径庭,从而极难调试
文中给出了另外一种方式,称之为Builder模式,我个人倒是觉得它算是JavaBean的一个变种吧,即先传入必选参数创建一个a对象,然后再通过类似set方法的方式传入可选参数,但是每次返回的都是Builder对象,最后通过这个Builder对象的全部参数生成一个新的Builder对象,值得一提的是它内部的参数都是final不可变的,这将会使得最后的Builder对象也不可变(全部的参数都是final,无法再次改变),这在安全性上将会好很多,但是不足之处也很明显,因为为了保证它的调用链和安全性,需要牺牲一部分的性能,并且相比于JavaBean无疑代码量复杂很多,所以如何抉择还是需要视情况而定
第三条:用私有构造器或者枚举类型强化Singleton属性
Singleton指仅被实例化一次的类,一般的实现方式有两种,两种均需要把构造器保持为私有,并且把成员设置为final,只是导出公有的静态成员方式不同,一者将该成员公有化,以便外部访问,二者将该成员私有,但是提供一个get方法以访问,前者实现较为简单,但后者对于某些情况的扩展性会更好
但是两种方式都存在一个问题,即反序列化时,会创建一个新的实例,这将导致一定程度上破坏Singleton属性,而为了保证Singleton,之前提到的成员需要声明transient属性以保证其不被序列化,但是与此同时需要加入一个readResolve方法以保证其能顺利通过整个序列化流程
文中提到了第三种方法,即枚举,实现起来十分简单,只需要创建一个枚举类型即可,功能上和之前的方式相近,但是更为简洁易懂且自身提供序列化机制,文中认为它是目前实现Singleton的最佳方式
第四条:通过私有构造器强化不可实例化的能力
简而言之,有些工具类只是提供一些静态方法和静态域,它不需要被实例化,但是如果不加构造方法,它会默认有一个无参的构造方法,这种情况下可以写一个私有的构造方法,使其不能被外部所调用,不过缺点是它不能被子类化,因为它的子类没有可访问的超类的构造器
第五条:避免创建不必要的对象
没啥解释的,说白了就是对于没有必要创建多个对象的情况下,尽量复用对象,这样对于代码的复杂度,维护成本等等都有好处,但也无需吹毛求疵
第六条:消除过期的对象引用
其实该条说的就是OOM,即内存泄漏的问题,现代jvm的垃圾回收机制已经比较完善了,排除确实因为需要,占用过大超过内存容量的情况,一般出现内存泄漏都是因为当对象使用完毕后,仍然保持着对它的强引用,导致jvm无法判断出它是否能够被回收,从而挤占内存,所以消除过期的对象引用是很有必要的。当然毕竟OOM也是一个经久不衰的话题,扩展起来还是很复杂的
第七条:避免使用终结方法
理解起来其实有点复杂,但是总结起来就是除非作为安全网,或是为了终结非关键的本地资源,否则不要使用终结方法,如果用了,那么记住调用super.finalize,如果作为安全网,需要记录终结方法的非法用法
第八条:覆盖equals时请遵守通用约定
本条中大篇幅的写了覆盖equals需要注意的一些约定:自反性;对称性;传递性;一致性;以及equals(null)时必须返回null,但是其实很多内容可以给我们在别的方面带来一些思考
在我们重写一些使用广泛的方法时,或是封装一些接口时,格局应该尽量扩大,保证其基本的一些必有的属性,并且在做完之后反复验证,但是不要企图将他们做的过于智能,过于细枝末节,往往这会带来很多问题,而有很多问题甚至是一时难以发现的,这会给后面调用或者实现的类带来极大的麻烦
第九条:覆盖equals时总要覆盖hashCode
如果覆盖了equals方法后,必须覆盖hashCode方法,如果不这样做的话,在具体类应用到基于hashCode的集合的时候,会因为违反了Java内部的机制(即equals相同的两个对象的hash code不同),这将导致无法使用get方法获取到put存入的值,造成问题,但需要注意的是即使重写hashCode也应该遵照一定方法去生成hash code,而不是写死某一个值,例如10,这将会导致散列表退化为链表,造成很大的影响,散列函数应该遵照“为不相等的对象产生不相等的hash code”的准则来写
第十条:始终要覆盖toString
在Object中有定义toString方法,所以所有对象都会有其toString方法,该方法的默认返回值是getClass.getName()+'@'+Integer.toHexString(hashCode())这会使得很多你期待使用toString看到的对象的值变成你非常不期望看到的那样,例如PhoneNumber@163b91,这乍一看谁知道具体的PhoneNumber类型的值到底是多少呢?所以文中所说覆盖toString的意思即默认的toString方法可能不能很好的满足需求,所以如果有字符串导出的操作,或者是有这方面的需求,强烈建议覆盖他,并且以一个格式良好且有注释的方式来导出值,这样将会使toString的使用体验良好许多
值得一提的是,String,int等基本类型中应该是覆盖了toString方法,他们返回的值是该内容的字符串表示,所以我们在一个基本类型值的后面调用toString,不会出现类似PhoneNumber@163b91的情况
第十一条:谨慎地覆盖clone
这部分确实没有什么好说的,只是说明实现Cloneable接口很多情况下会需要去重写clone方法
文中给出了一些设计clone需要注意的事项,不过最终还是建议,最好的方式就是不要去实现该接口,因为该接口的扩展性非常差,问题很多,除非是拷贝数组
对于一个专门为了继承而设计的类,如果你未能提供行为良好的受保护的clone方法,它的子类就不可能实现Cloneable接口
第十二条:考虑实现Comparable接口
如果需要一些特殊的对比方式来进行特定需求的排序,请实现Comparable接口,实现后需要重写CompareTo方法,类似第八条所说的equals,重写CompareTo也建议遵照一定的约定
另外需要注意的是CompareTo并非每次都要将所有的数据进行对比
假设说需要对同年级的所有学生排序
1班13号和3班15号
最重要的域首先是班级,当1班和3班已经存在某种顺序关系了之后,结果已经显而易见,则无需再对比学号,直接返回结果即可,这样在性能上会好很多
其次就是越界问题,举个最简单的例子,一个int并不能表示所有(int-int)的结果,而最终是扩展储存类型,还是对输入值进行约束则取决于具体需求
第十三条:使类和成员的可访问性最小化
其实说的就是访问级别方面的封装的概念,这样做的好处一方面是将各功能实现尽量模块化,在解耦方面会有优势,其次也便于测试,没有太多好说的,引用一段文中内容吧
总而言之,你应该始终尽可能地降低可访问性。你在仔细地设计了一个最小的公有API之后,应该防止把任何散乱的类,接口和成员变成API的一部分。除了公有静态final域的特殊情况之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的
第十四条:在公有类中使用访问方法而非公有域
其实也是封装的概念,将内部数据隐藏,但提供公有方法供外界访问,这样只要以后方法名不做修改,类的功能的具体实现或者数据的具体修改不会影响其他类的功能实现
第十五条:使可变性最小化
要看懂这一条,首先我们要知道可变类和不可变类的区别
对于一个普通的可变类而言,我们可以给他任意次赋值,这个对象不会改变(或许应该说它的地址不会被改变)但是它的值会改变,而java是面向对象的语言,所以其他类跟该对象的交互其实是持有了该对象的引用(或者说地址),看起来好像有点绕且没有什么影响,但是实际上,假设说我现在新建一个HashSet,里面插入一个可变类的值,我们已知StringBuilder类是可变类
//新建一个HashSet HashSethashSet = new HashSet (); //新建一个可变类StringBuffer对象a StringBuffer a = new StringBuffer("s"); //把a加入到HashSet中 hashSet.add(a); //新建一个可变类StringBuffer对象b,我希望b的值与a相同,所以把a的值赋值给b StringBuffer b = a; //我希望b的值再多加一个s字符 b.append("s"); //我想输出一下hashSet的值,结果发现居然是[ss]? System.out.println(hashSet); //回头检查一下a的值,居然也变成了ss System.out.println(a);
如上述代码所示,我只希望改变一下b的值,但是在StringBuffer b = a这一步中,赋值给b的不是a的值,而是a的地址,b引用的那个地址变成了a的地址,所以给b赋值就变成了给a赋值
即程序员可能并不想修改这个对象的值,但是因为不小心对该对象的调用和赋值,导致改变了该对象的值
当然这个问题的话,可能有人说调用的时候自己注意一下就是了,但是可变类在其他方面也会带来很多负面影响
例如说在多线程,并发的情况下,由于可能存在多个线程同时调用一个对象的情况,导致该对象的数值会变得难以预测并且不符合预期,为了保护它的安全,我们需要使他同步,或是做其他的操作。而不可变类的对象由于其值不可变,所以每次调用后,获得的都是另外一个对象,例如String,我们知道它是不可变的,每次赋值的操作都是新建一个String,然后把该对象的地址赋值给它,所以对于String来说,它是线程安全的,不必为了保护它的数据安全而去做额外的操作和编写额外的代码,当然,不可变类有它的缺点,就是大量数据情况下,会极度消耗资源,毕竟每一次赋值都需要创建两个新对象,所以可以对某些域放宽条件或是编写一个可变配套类,例如String的可变配套类StringBuffer
至于如何让类变成不可变类,书中给出的最优方法是将域(变量)变成private且final的,并且将构造器变为private的,使用静态工厂方法来创建对象。这样相比于final类,可扩展性会好很多。而即使是可变类,也尽量使得能够不可变的域不可变,这样可以减少很多麻烦。
不可变类的规则归根结底只有一句话:没有一个方法能够对对象的状态产生外部可见的改变
第十六条:复合优先于继承
首先我们要明白继承,继承是很强大的,但是它也会带来诸多问题,因为你可能在不清楚继承的父类的功能的具体实现而重写了其方法,例如你重写了父类方法a,但是父类方法b的实现也是依靠了其自身方法a,这将会导致你可能只想改变a,但是ab都被改变了,这种情况下,文中提出了复合的方式
复合即先使用一个类A(转发类)继承父类接口,定义一个父类对象,然后在重写方法中return父类对象的方法的结果,然后用类B(扩展类)去继承类A,随意重写类A的方法,不会对其他类A的方法造成影响,因为它所做的只是将父类方法产生的结果转发出去,类B无法去重写父类的方法,也可以变相理解为类A使得父类的方法互相“解耦”,使其互不影响(因为类A方法return的值是其中父类对象的方法的结果),使得任意方法的预期结果不会因为重写另外一个方法而改变
当然,这会使得类的数量急剧增加,类之间的关系斑驳复杂,代码更难阅读(逻辑关系更加复杂),所以是否使用需要视情况而定
第十七条:要么为继承而设计,并提供文档说明,要么就禁止继承
其实这条算是对前面几条的补充,我们知道如果错误的或者说不当的继承了一个类,那么带来的后果是难以预料的,所以假设说一个类为了继承而设计,那么请提供对应的文档说明,至于并非为了安全地进行子类化而设计和编写的类,请禁止子类化,如之前条目所说,有两种方式比较合适,最简单的就是声明该类为final,另一种是将构造器变成私有的,并且增加公有的静态方法来替代构造器
第十八条:接口优于抽象类
从使用上来说,抽象类肯定更为好用,但考虑到扩展性,抽象类会造成诸多问题,举个栗子:类只能有一个父类,所以如果类在功能增多的情况下,会对这个抽象类造成很大的压力,因为它需要做很多的修改,这带来代码行数的增加,从而可能引发其他问题,但并非抽象类不可取,它作为基类,只要基本功能实现,结构清晰,可以对子类进行有效的约束,但需要保证其职责清晰,功能明确,只单说扩展性的话,必然是接口优于抽象类
第十九条:接口只用于定义类型
如果使用接口来定义常量(其实我们一般也不这样做),一方面如果后期常量有修改,需要修改接口,但是这违背了接口的原则,二则文件量增多,导致维护成本增加,三则难以明确定义,远远不如常量工具类或者枚举清晰方便
PS:二进制兼容性:在升级或修改库文件(这里指的是接口文件),不必重写编译该库的可执行文件(该接口的实现类),其功能不会被破坏
第二十条:类层次优于标签类
文中的标签类大体上指的是类似于定义一类事物的类,举个栗子,定义一个动物类,而动物又分鸟类,虫类等等,动物类囊括了多种子类型,但是它却被集成在了一个类里,类似于我们书中的标签一样
其实文中对标签类的差评个人认为可以理解为解耦的一种思路,当一个类能够实现多个分支的功能,或是定义时,这个类就应该被拆分成多个类或者是分出接口然后做出多个实现类,例如我们可以定义一个图形类或是接口,再去定义圆形方形等子类或是实现类,而不是在一个类中把他们全部做到,即文中所述的标签类。标签类将会使得自身难以维护,并且容易出错
第二十一条:用函数对象表示策略
主要是解决复用的问题,类似Math类,它的所有实例在功能上都是等价的,所以它们作为单例来说是十分合理的
当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态类成员,并通过公有的静态final域被导出,其类型为该策略接口
即使使用接口定义,每次使用的时候都需要创建一个新的实例,所以将其定义为静态,且加以保护,保证其单例,使用时以类名.域名.方法名即可
第二十二条:优先考虑静态成员类
这一条主要是一些概念,直接引用部分正文内容吧
如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,想要创建非静态成员类的实例是不可能的
如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用。保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时却仍然得以保留
如果一个嵌套类需要在单个方法之外任然是可见的,或者它太长了,不适合于放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的;否则,就做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则,就做成局部类
第二十三条:请不要在新代码中使用原生态类型
其实这条主要是说了一些泛型的优势,和不应该使用原生态类型的原因
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势
泛型的作用非常广泛,尤其是在定义接口中的数据类型时,使用泛型的可扩展性都非常好
第二十四条:消除非受检警告
总而言之,非受检警告很重要,不要忽略它们。每一条警告都表示可能在运行时跑出ClassCastException异常。要尽最大的努力消除这些警告。
第二十五条:列表优先于数组
数组是协变的,即如果Sub是Super的子类,那么数组类型Sub[]就是Super[]的子类型。相反泛型则是不可变的,对于任意两个不同的类型,List
数组是具体化的,因此数组会在运行时才知道并检查它们的元素类型约束。泛型则是通过擦除来实现的,因此泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。
这导致的结果就是,假设说我们要将一个String放入Long容器中,我们都知道这是不合法的,但是最终的结果是
Object[] objectArray = new Long[1]; objectArray[0] = "heheda"; List
数组在运行时才能发现该问题,而集合则直接报错,不能编译,两者相比,我们很明显的知道哪个更加合理,当然,List会牺牲一部分性能,所以具体使用场景需要斟酌
第二十六条:优先考虑泛型
其实是对自己如何利用泛型的一个示例,通常使用泛型,相比于自己进行类型转换要安全许多,当然耗费的资源也会多一些
第二十七条:优先考虑泛型方法
其实和上一条结合,就是说泛型的实用性,扩展性都会比较好,这一条着重说了一下那些静态工具方法用泛型会特别合适,其实确实是这样,这些工具方法使用泛型,会使得后面的扩展变得更加的方便,举个栗子,你现在可能是需要传递数据,这时候如果你写死了某个类型,假设说是String,那么可能你满足了现在的需求,但是后期假设说需要加入int,boolean,那你又需要对它们进行扩展,或是修改接口及其实现,或是添加对应方法,但是泛型方法一旦被做出来,就不存在该类情况,并且他们几乎可以保证是类型安全的
第二十八条:利用有限制通配符来提升API的灵活性
其实是对上一条泛型方法的补充,在有一些泛型方法内,并不支持全部的数据类型,这时候需要对其类型做出一定的限制,或限制为某个类型的子类,或是某个类型的超类,需要注意这种上下界的定义,前者为 extends E>,后者为 super E>,无界可写成>,但并不是任何时候都应该使用通配符,甚至并不是任何时候都应该使用泛型,在消费时对类型有严格需求时,不应该使用通配符,以保证类型安全
第二十九条:优先考虑类型安全的异构容器
我们知道例如Set,Map之类的容器,对类型参数的个数都有着限制,比方说Map是键值对,键和值,两个类型参数
文中使用了一个比较巧妙的玩法来“突破”这个限制
public class Favorites{ private Map,Object> favorites = new HashMap , Object>(); public <T> void putFavorite(Class<T> type,T instance){ if (type == null) throw new NullPointerException("Type is null"); favorites.put(type,instance); }; public <T> T getFavorite(Class<T> type){ return type.cast(favorites.get(type)); }; }
这好比是将类的类型作为键,以该类的实例作为值来存储数据,其实我不认为这避开了固定数目的类型参数的这一准则,当然,这种使用方式的好处是相比于直接存储Object然后来一个个强转写方法而言,这种方式简短有效且保证了存入取出均是类型安全的,是一种极为良好的封装形式
无论如何,对类型参数作出限制这一准则保证了方法的安全性和效率(之前条目也有说到这一问题),我们很难真正避开它,当然,也不应该违背它
第三十条:用enum代替int常量
与int常量相比,枚举有个小小的性能缺点,即装载和初始化时会有空间和时间的成本。
那么什么时候应该使用枚举呢?每当需要一组固定常量的时候。当然,这包括“天然的枚举类型”,例如行星、一周的天数以及棋子的数目等等。但它也包括你在编译时就知道其所有可能值的其他集合,例如菜单的选项、操作代码以及命令行标记等等。
Java的枚举本质上是int值
第三十一条:用实例域代替序数
其实我认为这条建议意义不大,文中列举的情况是类似在一个枚举中存在几个值,但是需要插入一个值,那么有些值的序数就会被改变,假设说需要使用序数(ordinal()方法)来作为枚举中某些“权值”的计算的一部分,那么这个方法将可能会在被插入值之后被破坏,但是事实上枚举本身就不应该和这些相关,上一条也说明了,在你编写代码时即可确定的值,才使用枚举,当然,如果非要这样用,那么永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中。
第三十二条:用EnumSet代替位域
如果只是需要在位域中使用枚举模式,不必死用int枚举模式,使用EnumSet时性能和int枚举类型相差不大且更清晰,当然,一般位域的具体使用会结合一些其他的值来使用,且经常发生变化,例如线程池的源码中,线程池的状态的值就是和线程池的线程数的值保存在一个int值中,且其会经常变化,这时EnumSet便不好替代
事实上,如果只是文中列举的那种例子,想表示四个状态,假设说不存在同时多状态的情况,2位便可全部表示,不需要4位^_^
第三十三条:用EnumMap代替序数索引
试想一种情况,假设说你想用一个数组表示你的花园里所有的花,很好实现,但是现在假设说给这些花分类,单纯靠这个数组就很难实现了,你可能会说我们可以用一个集合数组来表示,将相同种类的花放至相同种类序数的数组的集合元素内,但是这种情况下,你需要自己去维护这个数组的i值及种类值定义,这些变成了你自己的责任,这给程序带来了一定的风险
这种情况下可以使用EnumMap来解决,它不仅会更简短,清楚,还更安全,且内部实现其实也是用的数组,所以效率上也没有缺点。总而言之,最好不要用序数来索引数组,而要使用EnumMap。如果你所表示的这种关系是多维的,就使用EnumMap<...,EnumMap<...>>
第三十四条:用接口模拟可伸缩的枚举
枚举是不可被继承的(默认已继承Enum类),所以如果说我们想用类似基类扩展的方式来运用到枚举里,枚举继承枚举是做不到了,那么只能是用接口来代替基类“制定规范”,我们可以定义一个接口,里面写好需要被实现的方法,然后定义扩展的枚举类型,实现这个接口,实现它的方法,然后调用的时候使用有限制的通配符,限制参数必须实现该接口,从而让这些扩展的枚举类型的实例能够被一起封装或者使用
第三十五条:注解优先于命名模式
其实这条没啥好细说的,注解相比命名模式会更加灵活且不易出错(在调用上),现在其实很多的开源框架都或多或少的使用这种方式,比方说butterknife或者是greendao等
第三十六条:坚持使用Override注解
很多时候我们不写Override注解,也会覆盖方法,但是假设说参数列表写错了,编译器是不会报错的,但是使用起来却默认重载而不是重写,所以在程序员知道这个地方是重写的时候,尽量使用Override注解,编译器会帮助你防止一大堆的非法错误
第三十七条:用标记接口定义类型
其实标记接口与其说是标记接口,不如说是用于标记的接口,这类接口里没有方法声明,单纯是用于标记类的某个属性
标记接口与标记注解有一定区别,用注解标记之后,在编译时会自动添加相关的代码或信息,且有些情况下,如果有问题,需要等到运行时才能被发现,而用接口时,在编译之前IDE就能够检查出是否存在问题(因为这已经是语法问题了),所以更加安全稳定
简而言之,类定义类型时应该使用标记接口,而对其他元素而言,使用注解可能会方便的多
第三十八条:检查参数的有效性
这条的核心理念即:我们需要知道,我们的参数列表很多时候可能会被传入很多不合法的值,我们需要在这些值传入的时候做对应的处理,不然会导致意想不到的错误,比方说边界,null等
文中列举的是断言,断言assert通常使用在我们自己调试的阶段,做一个有效性检查的工作,排查问题,实际发布应用的时候,我们需要把断言去掉,如果需要对具体数值做界定,应该用更加友好的方式来做出处理,而不是直接抛出异常
第三十九条:必要时进行保护性拷贝
作为服务端,我们假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性的设计程序,使得原本的数据不会被非法使用而导致改变,破坏
保护性拷贝的做法其实很简单,在接受值时使用自身内部的对象接收,并验证可靠性,被调用时,返回一个新建对象而不是自己本身,避免内部的值受到修改,保证安全性
引申说一下,其实理念很简单,很多类型,我们传递的都是一个对象的引用(即地址),而不是具体的数值,所以假设说有两个对象,引用相同,其中一个被修改了,另外一个也会被修改(因为地址相同,这个地址存放的数据被修改了,自然引用这个地址的全部对象的值都会被修改),所以我们需要对这种情况进行一定的保护措施,当然,这必然是需要额外开销的,所以如何决定需要具体考虑
--------------------------------
未完待续