11. 始终重写 toString 方法
假如我有一个User类,里面保存了用户的一些基本信息,如年龄,当我们在控制台输出这一个类的时候,计算机给我们返回的是类似于com.tan.User@6d06d69c
的信息,这些信息对于我们调试代码其实并没有什么卵用,趁此机会,可以看一下System.out.println()
,是怎么实现的
扩展 System.out.println源码解析
在PrintStream中重载了多种 println 函数
/**
*输出时候换行,行分隔符由系统定义,不止是一个
*/
public void println() {
newLine();
}
/**
打印一个布尔值
*/
public void println(boolean x) {
synchronized (this) {
print(x);
newLine();
}
}
/**
打印一个character 字节
*
* @param x The <code>char</code> to be printed.
*/
public void println(char x) {
synchronized (this) {
print(x);
newLine();
}
}
/**
打印一个int
*
* @param x The <code>int</code> to be printed.
*/
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
/**
*打印一个long长整型
*
* @param x a The <code>long</code> to be printed.
*/
public void println(long x) {
synchronized (this) {
print(x);
newLine();
}
}
/**
*打印一个float浮点形
*
* @param x The <code>float</code> to be printed.
*/
public void println(float x) {
synchronized (this) {
print(x);
newLine();
}
}
/**
* 打印一个double
*
*/
public void println(double x) {
synchronized (this) {
print(x);
newLine();
}
}
/**
打印一个char x[])
*/
public void println(char x[]) {
synchronized (this) {
print(x);
newLine();
}
}
/**
打印string
*
* @param x The <code>String</code> to be printed.
*/
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
/**
打印一个object
*/
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
可以看到 println函数中保证同步之后,调用了print 函数和 newline函数,那么前面我们输出一个类就是调用的println(Object x)
,object是所有类的基类,代码如下
/*
* PrintStream.class
Prints an Object and then terminate the line.
打印一个对象,并终止行,意思是下一次打印会换行打印,
*/
public void println(Object x) {
// 获取打印字符串
String s = String.valueOf(x);
// 锁住当前对象 特性: 1,互斥性(操作的原子性) 2,可见性 ps:volatile 不保证原子性
// 如果是同一个实例,就会按顺序访问,但是如果是不同实例,就可以同时访问
synchronized (this) {
print(s);
newLine();
}
}
// 这个不重要,重要的是write(s)
public void print(String s) {
if (s == null) {
s = "null";
}
write(s);
}
private void write(String s) {
try {
synchronized (this) {
// 检查一下确认流是打开的
ensureOpen();
// textOut 是BufferedWriter实例 写字符串
textOut.write(s);
// 这个方法不需要刷新流就可以刷新他的缓冲区
textOut.flushBuffer();
// charOut 是OutputStreamWriter实例
charOut.flushBuffer();
// s.indexOf 返回第一次出现的指定子字符串在此字符串中的索引
if (autoFlush && (s.indexOf('\n') >= 0))
// 清空缓冲区
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
// 输出行定义符号
private void newLine() {
try {
synchronized (this) {
ensureOpen();
textOut.newLine();
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush)
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
// lineSeparator = (String) java.security.AccessController.doPrivileged(new sun.security.action.GetPropertyAction("line.separator"));
// lineSeparator 随系统的不同而不同。即在Windows中是“\r\n”,linux系统中是"\n"
public void newLine() throws IOException {
write(lineSeparator);
}
volatile与system.out组合产生的误区
Volatile关键字他有两个特性,一个是可见性,第二个就是禁止重排序(具体说明是重排序,感兴趣的话去搜下就有,我这里就不做讲解),但是大家也非常清楚,他并不保证原子性。但是在好多例子输出确实能保证是线程安全的,其实问题就是system.out
这一个输出语句,上面代码中也看到了,system.out
利用synchronized
实现了同步,而在jvm中,有锁粗化这一个说法,就比如说你在一个循环内加锁,jvm会自动把锁扩大到循环外部
如果一系列的连续操作都对同一个对象反 复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥 同步操作也会导致不必要的性能损耗
扩展结束
那么上面不说输出了一个com.atguigu.User@1554909b
字符串吗??,这个字符串是怎么输出的,原因在这一段代码里面,可以看出来,如果这个对象不为null,是为默认调用toString()
的,toString
方法getClass().getName() + "@" + Integer.toHexString(hashCode()
直接返回类名@和哈希码的无符号十六进制(哈希码的字符串表示形式),要想输出可读性强的,我们可以重写toString()
String s = String.valueOf(x);
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
《Effective.Java》的作者在文中说提供一个良好的 toString 实现使你的类更易于使用,并对使用此类的系统更易于调试。当对象被传递到 println、printf、字符串连接操作符或断言,或由调试器打印时,toString 方法会自动被调用。 即使你从不调用对象上的 toString,其他人也可以。例如,对对象有引用的组件可能包含在日志错误消息中对象的字符串表示。(但是一般打印日志,会使用日志框架进行统一打印管理)如果未能重写 toString,则消息可能是无用的。例如上面我说的User类,在我重新toString之后,打印就会变成这样
@Override
public String toString() {
return "User [userName=" + userName + ", age=" + age + "]";
}
// -> 输出结果
User [userName=谭婧杰, age=23]
这样子,就清晰明了得多了,还有一个好处就是打印集合的时候也会帮助我们更加清楚的看到程序运行的结果
注意
虽然toString
方法应该返回对象中包含的所有需要关注的信息,但是,如果toString
包含的信息很多的话,我们也可以只返回那些重要的信息,即返回一个摘要
12. 谨慎地重写 clone 方法
clone 是一个克隆方法,设计模式有一个原型模式就是 实现了 Cloneable
接口,其实Cloneable
是一个空实现,下面是它的源码和源码我(当然这里的我是指有道词典😂)翻译过来的注释
/*一个类实现了Cloneable接口向{@link java.lang. object# clone()}方法,
表示该方法可以创建一个该类实例的字段对字段副本。
在没有实现Cloneable接口的实例上调用对象的克隆方法会导致抛出CloneNotSupportedExceptions。按照约定,实现此接口的类应该重写Object.clone(它是受保护的)与一个公共方法。
注意,这个接口不</i>包含克隆方法,因此,
仅仅依靠它实现了这个接口这一事实是不可能克隆一个对象的。即使克隆方法被调用
*/
public interface Cloneable {
}
反正意思是这样的cloneable
其实就是一个标记接口和Serializable
序列化接口差不多,只有实现这个接口后,然后在类中重写Object
中的clone
方法,然后通过类调用clone方法才能克隆成功,Object的克隆方法是这样的
/**
* 创建并返回此对象的副本
*/
protected native Object clone() throws CloneNotSupportedException;
可以看到它用了native
修饰,native翻译过来是本国的本地的,那么用native修饰的方法不言而喻,自然是本地方法,它会在jvm虚拟机的本地方法栈上面分配,是通过调用c来控制计算机硬件的,需要注意的是native关键字不能与abstract同时使用,每一个native方法在jvm中都有一个同名的实现体,native方法在逻辑上的判断都是由实现体实现的,另外这种native修饰的方法对返回类型,异常控制等都是没有约束的
几种克隆方式
java一般有四种方式来创建一个对象
-首当其冲(这里的原意是首先受到伤害,但是我这里此意非彼意,我就纯粹字面意思,上下文自行理解)的肯定是new,如new User()
-
反射,这个公司的项目里面用到了,用反射获取不同的设备实现
-
序列化io流
-
最后一种就是克隆,《Effective.Java》文中是这样说的它创建对象而不需要调用构造方法
源码中有一段注释是这样子的 x.clone() == x
返回false,但是x.clone().getClass() == x.getClass()
确返回true,芭芭拉拉一长串(原文贴下边),主要就是说了深拷贝和浅拷贝这一回事
/**
* Creates and returns a copy of this object. The precise meaning
* of "copy" may depend on the class of the object. The general
* intent is that, for any object {@code x}, the expression:
* <blockquote>
* <pre>
* x.clone() != x</pre></blockquote>
* will be true, and that the expression:
* <blockquote>
* <pre>
* x.clone().getClass() == x.getClass()</pre></blockquote>
* will be {@code true}, but these are not absolute requirements.
* While it is typically the case that:
* <blockquote>
* <pre>
* x.clone().equals(x)</pre></blockquote>
* will be {@code true}, this is not an absolute requirement.
* <p>
* By convention, the returned object should be obtained by calling
* {@code super.clone}. If a class and all of its superclasses (except
* {@code Object}) obey this convention, it will be the case that
* {@code x.clone().getClass() == x.getClass()}.
* <p>
* By convention, the object returned by this method should be independent
* of this object (which is being cloned). To achieve this independence,
* it may be necessary to modify one or more fields of the object returned
* by {@code super.clone} before returning it. Typically, this means
* copying any mutable objects that comprise the internal "deep structure"
* of the object being cloned and replacing the references to these
* objects with references to the copies. If a class contains only
* primitive fields or references to immutable objects, then it is usually
* the case that no fields in the object returned by {@code super.clone}
* need to be modified.
* <p>
* The method {@code clone} for class {@code Object} performs a
* specific cloning operation. First, if the class of this object does
* not implement the interface {@code Cloneable}, then a
* {@code CloneNotSupportedException} is thrown. Note that all arrays
* are considered to implement the interface {@code Cloneable} and that
* the return type of the {@code clone} method of an array type {@code T[]}
* is {@code T[]} where T is any reference or primitive type.
* Otherwise, this method creates a new instance of the class of this
* object and initializes all its fields with exactly the contents of
* the corresponding fields of this object, as if by assignment; the
* contents of the fields are not themselves cloned. Thus, this method
* performs a "shallow copy" of this object, not a "deep copy" operation.
* <p>
* The class {@code Object} does not itself implement the interface
* {@code Cloneable}, so calling the {@code clone} method on an object
* whose class is {@code Object} will result in throwing an
* exception at run time.
*
* @return a clone of this instance.
* @throws CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
创建并返回此对象的副本。 “复制(copy)”的确切含义可能取决于对象的类。 一般意图是,对于任何对象 x,表
达式 x.clone() != x 返回 true,并且 x.clone().getClass() == x.getClass() 也返回 true,但它们不是
绝对的要求,但通常情况下, x.clone().equals(x) 返回 true,当然这个要求也不是绝对的。
根据约定,这个方法返回的对象应该通过调用 super.clone 方法获得的。 如果一个类和它的所有父类
(Object 除外)都遵守这个约定,情况就是如此, x.clone().getClass() == x.getClass() 。
根据约定,返回的对象应该独立于被克隆的对象。 为了实现这种独立性,在返回对象之前,可能需要修改由
super.clone 返回的对象的一个或多个属性。
这种机制与构造方法链(chaining)很相似,只是它没有被强制执行;如果一个类的 clone 方法返回一个通过调
用构造方法获得而不是通过调用 super.clone 的实例,那么编译器不会抱怨,但是如果一个类的子类调用了
super.clone,那么返回的对象包含错误的类,从而阻止子类 clone 方法正常执行。如果一个类重写的 clone 方法是有
final 修饰的,那么这个约定可以被安全地忽略,因为子类不需要担心。但是,如果一个 final 类有一个不调用
super.clone 的 clone 方法,那么这个类没有理由实现 Cloneable 接口,因为它不依赖于 Object 的 clone 实现的行为。。。。。。
*/
深拷贝 and 浅拷贝
假设B复制了A,修改A的时候,看B是否发生变化: 如果B跟着也变了,说明是浅拷贝,(拿人手短,修改堆内存中的同一个值) 如果B没有改变,说明是深拷贝,(自食其力,修改堆内存中的不同的值),若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝
文中说到假设你希望在一个类中实现 Cloneable 接口,它的父类提供了一个行为良好的 clone 方法。首先调用super.clone。
得到的对象将是原始的完全功能的复制品,在你的类中声明的任何属性将具有与原始属性相同的值。如果每个属性包含原始值或对不可变对象的引用,则返回的对象可能正是你所需要的,这一种情况下面不需要任何处理
PS:不可变类永远不应该提供 clone方法,因为这只会浪费复制。
// Clone method for class with no references to mutable state
@Override
public PhoneNumber clone() {
try {
// java 支持协变返回类型,意思是
// 子类覆盖基类(父类)方法时,返回的类型可以是基类方法返回类型的子类。这里主要是消除客户端转换的需要
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Can't happen
}
}
但是如果对象包含引用可变对象的属性,则前面显示的简单 clone 实现可能是灾难性的,例如
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
// Ensure space for at least one more element.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
这一段代码,产生的问题是修改原始实例会改变克隆实例中的不变量, 反之亦然,实际上,clone 方法作为另一种构造方法; 必须确保它不会损坏原始对象,并且可以在克隆上正确建立不变量。 为了使 Stack 上的 clone 方法正常工作,它必须复制stack 对象的内部。 最简单的方法是对元素数组递归调用 clone 方法:
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
文中还提到了一种方法,说只递归调用克隆方法是不行的,假如为hash表编写一个克隆方法,其内部包含一个哈希桶数组,每个哈希桶都指向“键-值”对链表的第一项,如
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
... // Remainder omitted
}
假设你只是递归地克隆哈希桶数组,就像我们为 Stack 所做的那样:
// Broken clone method - results in shared mutable state!
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
文中说这会很容易导致克隆对象和原始对象中的不确定性行为,解决这个问题,就必须复制包含每个桶的链表,代码如下:
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++)
if (buckets[i] != null) {
result.buckets[i] = buckets[i].deepCopy();
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
如果哈希桶不是太长,这种技术很聪明并且工作正常。但是,克隆链表不是一个好方法,
因为它为列表中的每个元素消耗一个栈帧(stack frame)。 如果列表很长,这很容易导致堆栈溢出。 为了防止这种情况发生,可以用迭代来替换 deepCopy
中的递归
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next = new Entry(p.next.key, p.next.value, p.next.next);
return result;
}
注意点
- clone 方法绝对不可以在构建过程中,调用一个可以重写的方法
- Object 类的 clone 方法被声明为抛出 CloneNotSupportedException 异常,但重写方法时不需要。 公共 clone 方法应该省略 throws 子句,因为不抛出检查时异常的方法更容易使用,意思是自己扑获异常,不抛出去
- 为继承设计一个类时,不应该实现 Clonable 接口,可以通过进行抛出一个异常来阻止实现
- 如果你编写一个实现了 Cloneable 的线程安全的类,它的 clone 方法必须和其他方法一样需要正确的同步。 Object 类的 clone 方法是不同步的
- 通常,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone 方法复制
13. 考虑实现 Comparable 接口
Comparable<T>
是jdk1.2提供的一个接口,它允许在简单的相等比较之外的顺序比较, 通过实现 Comparable
接口,一个类表明它的实例有一个自然顺序( natural ordering),像Arrays.sort(b);
一样,
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
通用约定
返回值为负数,对象小于指定对象,返回值为0,则对象等于指定对象,返回值大于0,正整数,则大于指定对象,不能比较会引发ClassCastException
异常
- 实现类保证
sgn(x.compareTo(y)) == -sgn(y. compareTo(x))
,即y.compareTo(x)
抛出异常时,x.compareTo(y)
必须抛出异常,sgn是一个符号函数 - 关系可传递,即
(x. compareTo(y) > 0 && y.compareTo(z) > 0)
意味着x.compareTo(z) > 0
- 实现类必须确保
x.compareTo(y) == 0
意味着sgn(x.compareTo(z)) ==sgn(y.compareTo(z))
和equals
方法不同, equals
方法在所有对象上施加了全局等价关系,意思是只能相同类型的对象进行比较,compareTo
不必跨越不同类型的对象:当遇到不同类型的对象时compareTo
被允许抛出 ClassCastException
异常,文中指出,违反上述约定的后果可能会破坏依赖于比较的其他类。 依赖于比较的类,包括排序后的集合 TreeSet 和 TreeMap 类,以及包含搜索和排序算法的实用程序类 Collections 和 Arrays
如果第一个对象小于第二个对象,那么第二个对象必须大于第一个; 如果第一个对象等于第二个,那么第二个对象必须等于第一个; 如果第一个对象大于第二个,那么第二个必须小于第一个。 第二项约定说,如果一个对象大于第二个对象,而第二个对象大于第三个对象,则第一个对象必须大于第三个对象。 最后一条规定,所有比较相等的对象与任何其他对象相比,都必须得到相同的结果。
ps:考虑 BigDecimal 类,其 compareTo 方法与 equals 不一致。 如果你创建一个空的HashSet 实例,然后添加 new BigDecimal("1.0") 和 new BigDecimal("1.00") ,则该集合将包含两个元素,因为与equals 方法进行比较时,添加到集合的两个 BigDecimal 实例是不相等的。 但是,如果使用 TreeSet 而不是HashSet 执行相同的过程,则该集合将只包含一个元素,因为使用 compareTo 方法进行比较时,两个BigDecimal 实例是相等的。如果比较整型基本类型的属性,使用关系运算符“<” 和 “>”,对于浮点类型基本类型的属性,使用Double.compare 和 Float.compare 静态方法。,因为浮点数本身就是存在误差的,顾名思义他的小数点是浮动的所以叫浮点数
jdk 1.8的特性中,提供了Comparator
用于比较,虽然 compareTo 或 compare 方法依赖于两个值之间的差值,但是注意的是日常中我们重写compare方法时候应该用Integer.compare(....);
或者Comparator.comparingInt(...);
代替直接返回二个数的差值
// 导致整数最大长度溢出和 IEEE 754 浮点运算失真的危险
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
15. 使类和成员的可访问性最小化
将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,组件将其内部数据和其他组件的其他实现细节隐藏起来,和设计模式一样,他的出现不仅仅是保证程序的健壮性,维护性更多的是屏蔽底层对客户端的影响,这一节说使类和成员的可访问性最小化J,和软件设计原则中的单一职责原则有一点像,单一职责原则具体来说是指一个类只做一件事,它不会改进系统的性能,但是却有效的促进了系统的进行性能调节,我说它有一点像的原因是,遵守这一些原则的系统在完成时并且分析确定了哪些组件导致了性能问题,就可以优化这些组件,而不会影 响别人的正确的组件
访问控制机制(access control mechanism)
java提供了接口,类或字段的访问级别
private | private成员为类的私有性质,仅有类本身和友元可以访问; |
protected | 和private类似,区别于protected可以被该类所有派生类访问; |
public | public的成员可以被外界的所有客户代码直接访问 |
published | 和public的区别仅在于published的成员可以被delphi开发环境的OBJECT INSPECTOR所显示,因此一般将属性或事件声明于published中 |
一般说对于顶层(非嵌套的)类和接口,只有两个可能的访问级别:包级私有(package-private)和公共的(public)。
15. 在公共类中使用访问方法而不是公共属性
这个很好理解,意思是你声明类字段,应该是private私有字段,客户端是通过调用getter,setter方法来进行取值和设值操作,例如应该把public double x;
替换为private double x;
,但是如果这一个类是包级别私有的,或者是一个私有的内部类又或者是这一个属性是不可变的那这种做法也ojk,文中指出了java平台类库有几个Java 平台类库中的几个类违反了公共类不应直接暴露属性的建议。 着名的例子包括 java.awt 包中的Point 和 Dimension 类。 这些类别应该被视为警示性的示例,而不是模仿的例子。
16. 最小化可变性
不可变类简单来说是它的实例不能被修改的类。不可变对象本质上是线程安全的,它们不需要同步,也不需要拷贝(你自己想,都是不可以变化了,随你怎么并发访问也只是做读操作,你强由你强,清风拂山岗,你横任你横,明月照大江😀) 包含在每个实例中的所有信息在对象的生命周期中是固定的,Java 平台类库包含许多不可变的类,包括 String
类,基本类型包装类以及
BigInteger
类和 BigDecimal
类。 有很多很好的理由:不可变类比可变类更容易设计,实现和使用。 他们不太容易出错,更安全
要使一个类不可变,请遵循以下五条规则:
- 不要提供修改对象状态的方法(也称为 mutators)。
- 确保这个类不能被继承。 这可以防止粗心的或恶意的子类,假设对象的状态已经改变,从而破坏类的不可变行为。 防止子类化通常是通过 final 修饰类,模板方式里面也用到过final 修饰防止子类实例化,
- 把所有属性设置为 final
- 把所有的属性设置为 private。
- 确保对任何可变组件的互斥访问。
- 访问方法和 readObject 方法中进行防御性拷贝。意思是防止序列化和深拷贝破坏
特点
-
不仅可以共享不可变的对象,而且可以共享内部信息。 例如, BigInteger 类在内部使用符号数值表示法。符号用 int 值表示,数值用 int 数组表示。 Inegate 方法生成了一个数值相同但符号相反的新 BigInteger实例。 即使它是可变的,也不需要复制数组;新创建的 BigInteger 指向与原始相同的内部数组。
-
不可变对象为其他对象提供了很好的构件(building blocks) 无论是可变的还是不可变的。 如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象可以构成Map 对象的键和 Set 的元素,一旦不可变对象作为 Map 的键或 Set 里的元素,即使破坏了 Map 和 Set的不可变性,但不用担心它们的值会发生变化。
-
不可变对象提供了免费的原子失败机制 它们的状态永远不会改变,所以不可能出现临时的不一致。
-
不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。 创建这些对象可能代价很高,特别是如果是大型的对象下
文中最后总结到:除非有充分的理由使类成为可变类,否则类应该是不可变的。如果一个类不能设计为不可变类,那么也要尽可能地限制它的可变性。减少对象可以存在的状态数量,可以更容易地分析对象,以及降低出错的可能性。构造方法应该创建完全初始化的对象,并建立所有的不变性。
17. 组合优于继承
java四大特性,相信大家都已经倒背如流了,反正我已经是这样子了,分别是封装,继承,多态其中继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是可以的,但是从普通的具体类跨越包级别边界继承是危险的,例如与方法调用不一样,继承打破了封装, 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明,总而言之,,继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用合成和转发也就是聚合咯代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。对于这点,下一节指出了设计继承的前提,如下
18. 要么设计继承并提供文档说明,要么禁用继承
文档编写规则
- 首先,这个类必须准确地描述重写这个方法带来的影响。 换句话说,该类必须文档说明可重写方法的自用性
- 对于每个公共或受保护的方法,文档必须指明方法调用哪些重写方法,以何种顺序以及每次调用的结果如何影响后续处理。
- 调用可重写方法的方法在文档注释结束时包含对这些调用的描述。
。。。。这里主要说了写java文档的一些规范什么的,具体请自行查阅