覆盖equals方法时遵守通用约定

覆盖equals()方法看起来似乎简单,但是许多覆盖方式会导致错误,并且后果非常严重。最能避免这些问题的办法就是不覆盖equals()方法,在这种情况下,类的每个实例都与它自己相等。如果满足了以下任意一个条件,那可以不覆盖equals()方法:

  • 类的每个实例本质上都是唯一的
  • 类没有必要提供逻辑相等的功能
  • 超类已经覆盖了equals(),超类的行为对于子类也是合适的。
    例如,大多数的Set实现都从AbstractSet继承equals()实现
  • 类是私有的,或包级私有的,可以确定它的equals()方法永远不会被调用

那么,什么时候应该覆盖equals()方法呢?如果类具有自己特有的逻辑相等概念,而且超类还没有覆盖equals()方法。这通常属于值类的情形。值类仅仅是一个表示值的类,例如IntegerString
程序员在利用equals()方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。 此时,不仅必须覆盖equals()方法,而且这样做也使得这个类的实例可以被用作MapKey,或集合的元素,使Map或集合表现出预期的行为。

在覆盖equals()方法的时候,必须要遵循它的通用约定。下面是约定的内容,来自Object的规范
equals方法实现了等价关系,其属性如下:

  • 自反性:对于任何为null的引用值x,x.equals(x)必须返回true
  • 对称性:对于任何非null的引用值xy,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true
  • 传递性:对于任何非null的引用值xyz,如果x.equals(y)返回truey.equals(z)也返回true,那么x.equals(z)也必须返回true
  • 一致性:对于任何非null的引用值xy,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false
  • 对于任何非null的引用值xx.equals(null)必须返回false

有许多类,包括所有的集合类,都依赖于传递给它们的对象是否遵守了equals约定。

我们按照顺序逐一查看以下5个要求:

  • 自反性——第一个要求仅仅说明对象必须等于其自身。这是很容易理解的
  • 对称性——第二个要求是说,任何两个对象对于"它们是否相等"的问题都必须保持一致。这是很容易无意中违反的。例如下面的例子实现一个区分大小写的字符串。字符串由toString保存,但在equals操作中被忽略:
public class CaseInsensitiveString {private final String s;public CaseInsensitiveString(String s) {this.s = Objects.requireNonNull(s);}/*** 违反对称性* @param that* @return*/@Overridepublic boolean equals(Object that) {if (that instanceof CaseInsensitiveString) {return s.equalsIgnoreCase(((CaseInsensitiveString) that).s);}/*** 单方面的想与String比较*/if (that instanceof String) {return s.equalsIgnoreCase((String) that);}return false;}
}

在这个类中,equals方法企图与普通的String对象进行互操作。假设我们有一个CaseInsensitiveString和一个普通的String:

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";

不出所料,cis.equals(s)返回true,但是String不知道CaseInsensitiveString,因此s.equals(cis)返回false,显然违反了对称性。

一旦违反了equals约定,当其他对象面对你的对象时,你完成不知道这些对象的行为会怎样

为了修复上面这个类的问题,把企图与String对象互操作的代码删掉即可。

  • 传递性——第三个要求是,如果一个对象等于第二个对象,而第二个对象又等于第三个对象,则第一个对象一定等于第二个对象。同样的,无意中违反这条规则也不难想象。用子类举个例子。假设它将一个新的值组件添加到了超类中。换句话说,子类增加的信息会影响equals的比较结果。我们首先以一个简单的不可变的二维整数型Point类 作为开始:
public class Point {private final int x;private final int y;public Point(int x,int y) {this.x = x;this.y = y;}@Overridepublic boolean equals(Object that) {if (!(that instanceof Point)) {return false;}Point p = (Point) that;return p.x == x && p.y == y;}
}

假设你想要扩展这个类,为Point添加颜色信息:

public class ColorPoint extends Point {private final Color color;public ColorPoint(int x, int y,Color color) {super(x, y);this.color = color;}
}

如果完全不提供equals方法,而是直接从Point继承过来,在equals做比较的时候颜色信息就被忽略掉了。虽然这不会违反equals约定,但明显这是无法接受的。
假设编写了一个equals方法,只有当它的参数是另一个ColorPoint,并且具有同样的位置和颜色时,它才会返回true:

@Override
public boolean equals(Object that) {if (!(that instanceof ColorPoint)) {return false;}return super.equals(that) && ((ColorPoint) that).color == color;
}

该方法的问题在于,在比较PointColorPoint,以及相反的情形时,可能会得到不同的结果。前一种比较忽略了颜色信息,而后一种比较总是返回false,因为参数的类型不正确。

public static void main(String[] args) {Point p = new Point(1,2);ColorPoint cp = new ColorPoint(1,2,Color.RED);System.out.println(p.equals(cp));//trueSystem.out.println(cp.equals(p));//false
}

可以通过这样的尝试来修复这个问题,让ColorPoint.equals在进行混合比较时忽略颜色信息:

//ColorPoint的方法
@Override
public boolean equals(Object that) {if (!(that instanceof Point)) {return false;}//如果that是Point,则进行忽略Color的比较if (!(that instanceof ColorPoint)) {return that.equals(this);}//that是ColorPointreturn super.equals(that) && ((ColorPoint) that).color == color;
}

这种方法虽然提供了对称性,但牺牲了传递性:

ColorPoint p1 = new ColorPoint(1,2,Color.RED);
Point p2 = new Point(1,2);
ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);
System.out.println(p1.equals(p2));//true
System.out.println(p2.equals(p3));//true
System.out.println(p1.equals(p3));//false

从上面输出可以看出,违反了传递性。此外,这种方法还会导致无限递归问题:假设Point有两个子类,如ColorPointSmellPoint,它们各自都带有这种equals方法。那么对colorPoint.equals(smellPoint)的调用将会抛出StackOverflowError异常。

那么该如何解决呢?

其实这是面向对象语言中关于等价关系的一个基本问题。我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。

你可能听过在equals方法中用getClass测试代替instanceof测试,可以扩展可实例化的类的同时,又保留equals约定:

@Override
public boolean equals(Object that) {if(that == null || that.getClass() != getClass()) {return false;}Point p = (Point)that;return p.x == x && p.y == y;
}

这段程序只有当对象具有相同的实现类是,才能使对象相等。虽然这样也不算太糟糕,但结果却是无法接受的:Point子类的实例仍然是一个Point,它仍然需要发挥作用,但如果采用了这种方法,它就无法完成任务。
假定我们要编写一个方法,以检验某个点是否处在圆中:

private static final Set<Point> unitCircle = Set.of(new Point(1,0),new Point(0,1),new Point(-1,0),new Point(0,-1));
public static boolean onUnitCircle(Point p) {return unitCircle.contains(p);
}

假设你通过某种不添加值组件的方法扩展了Point,并创建了多个实例:

public class CounterPoint extends Point {private static final AtomicInteger counter = new AtomicInteger();public CounterPoint(int x,int y) {super(x,y);counter.incrementAndGet();}public static int numberCreated() {return counter.get();}
}

里氏替换原则认为,一个类的任何重要属性也将使用于它的子类。因此为该类编写的任何方法,在它的子类上也应该同样运行得很好。

假设我们将CounterPoint实例传递给onUnitCircle方法。如果Point使用了基于getClassequals方法,无论CounterPoint实例的xy值是什么,该方法都会返回false

虽然没有一种令人满意的方法既可以扩展不可实例化的类,又增加值组件,但还是有一种不错的权宜之计:遵从复合优于继承的建议。不再让ColorPoint继承Point,而是添加一个私有的Point属性,以及一个公有的视图方法,该方法返回一个与该ColorPoint在相同位置的普通Point对象:

public class ColorPoint {private final Point point;private final Color color;public ColorPoint(int x, int y,Color color) {point = new Point(x,y);this.color = color;}public Point asPoint() {return point;}@Overridepublic boolean equals(Object that) {if (!(that instanceof ColorPoint)) {return false;}ColorPoint cp = (ColorPoint) that;return cp.point.equals(point) && cp.color.equals(color);}
}
  • 一致性——equals约定的第四个要求是,如果两个对象相等,它们就始终保持相等,除非它们中有一个对象被修改了。
    如果你设计的某个类是不可变的:那么应该满足,相等的对象永远相等,不相等的对象永远不相等。无论类是否是不可变的,都不要使equals方法依赖于不可靠(可变)的资源。

  • 非空性——所有的对象都不能等于null
    许多类的equals方法都通过一个显式的null测试来防止这种情况:

@Override
public boolean equals(Object that) {if(that == null) {return false;}...
}

这项测试其实是不必要的。为了测试其参数的等同性,equals方法必须先把参数转换成适当的类型,以便可以调用它的访问方法,或者访问它的属性。在进行转换之前,equals方法必须使用instanceof操作符,以检查其参数的类型是否正确:

@Override
public boolean equals(Object that) {if(!(that instanceof MyType) {return false;}MyType mt = (MyType) that;...
}

如果instanceof的第一个操作数为null,那么不管第二个操作数是什么类型,该操作符都会返回false。因此不需要显示地null检查。

结合所有这些要求,得出了一下实现高质量equals方法的诀窍:

  1. 使用==操作符检查"参数是否为这个对象的引用",如果是则返回true。这只是一种性能优化,如果比较操作可能很昂贵,就值得这么做。
  2. 使用instanceof操作符检测"参数是否为正确的类型"。如果不是则返回false
  3. 把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功。
  4. 对于该类中的每个关键属性,检查参数中的属性是否与该对象中对应的属性相匹配。如果这些测试全部成功,则返回true,否则返回false

对于既不是float也不是double的原生类型属性,可以使用==操作符进行比较;对于对象引用属性,可以递归地调用equals方法;对于float属性,可以使用静态的Float.compare(float,float)方法;
对于double属性,则使用Double.compare(double,double);对于数组属性,则要把以上原则应用到每一个元素上。如果数组属性中的每个元素都很重要,就可以使用其中一个Arrays.equals方法。

有些对象引用属性包含null可能是合法的,所以,为了避免可能导致空指针异常,则使用静态方法Objects.equals(Object,Object)来检测。

在编写equals方法之前,应该问自己三个问题:它是否是对称的、传递的、一致的?

根据上面的诀窍构造equals方法的具体例子,看下面这个类:

public class PhoneNumber {private final short areaCode,prefix,lineNum;public PhoneNumber(int areaCode,int prefix,int lineNum) {this.areaCode = rangeCheck(areaCode,999,"area code");this.prefix = rangeCheck(prefix,999,"prefix");this.lineNum = rangeCheck(areaCode,9999,"line num");}private static short rangeCheck(int val,int max,String arg) {if (val < 0 || val > max) {throw new IllegalArgumentException(arg + ": " + val);}return (short) val;}@Overridepublic boolean equals(Object that) {if (that == this) {return true;}if (!(that instanceof PhoneNumber)) {return false;}PhoneNumber pn = (PhoneNumber) that;return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;}
}

下面是最后一些告诫:

  • 覆盖equals时总要覆盖hashCode
  • 不要企图让equals方法过于智能
  • 不要将equals声明中的Object对象替换成其他类型

覆盖equals时总要覆盖hashcode

如果不这样做的话,就会违反hashCode的通用约定:

  • 只要对象的equals方法比较所用的信息没有被修改,那么多次调用同一个对象的hashCode方法都必须始终返回同一个值
  • 如果两个对象根据equals方法比较是相等的,那么它们的hashCode方法都必须产生相同的整数值
  • 如果两个对象根据equals比较是不等的,那么它们的hashCode不一定产生不同的结果

因为没有覆盖hashCode会违反第二条:相等的对象必须具有相等的hashCode。

一个好的散列函数通常倾向于为不等的对象产生不等的散列码(hashCode)。下面给出一种简单的解决方法:

  1. 声明一个int变量并命名为result,将它初始化为对象中第一个关键域的散列码c,如步骤2.a中计算所示
  2. 对象中剩下的每一个关键域f都完成以下步骤:
    a. 为该域计算int类型的散列码c
    * 如果该域是基本类型,则计算Type.hashCode(f)Type是装箱类型
    * 如果该域是一个对象引用,且该类的equals方法通过递归的调用equals的方式来比较这个域,则同样的为该域递归地调用hashCode。如果该域的值为null,则返回0
    * 如果该域是一个数组,则要把数组中的每个元素当做单独的域来处理。如果数组中域中的所有元素都很重要,可以使用Arrays.hashCode方法
    b. 安装下面的公式,把步骤2.a中计算得到的散列码c合并到result中: result = 31 * result + c
  3. 返回result

现在把上述解决办法用到PhoneNumber类中:

@Override
public int hashCode() {int result = Short.hashCode(areaCode);result = 31 * result + Short.hashCode(prefix);result = 31 * result + Short.hashCode(lineNum);return result;
}

Objects类有一个静态方法hash,它带有任意数量的对象,并为它们返回一个散列码。与上面编写出来的对比,质量是相当的。但是运行速度更慢一些,因为它们会引发数组的创建,以便传入数量可变的参数,还可能会装箱和拆箱。建议该静态方法用于不太注重性能的情况:

@Override
public int hashCode() {return Objects.hash(lineNum,prefix,areaCode);
}

如果一个类是不可变的,并且计算散列码的开销较大。建议将散列码缓存在对象内部,如String

不要试图从散列码计算中排除一个对象的关键域来提高性能。

始终要覆盖toString

提供好的toString实现可以使类用起来更加舒适,使用了这个类的系统也更易于测试。
在实际应用中,toString方法应该返回对象中包含所有值得关注的信息,如果读写中包含的状态信息难以用字符串来表达,此时应该返回一个摘要信息。

下面是PhoneNumber类的toString方法:

@Override
public String toString() {return String.format("%03d-%03d-%04d",areaCode,prefix,lineNum);
}

toString返回值中包含的所有信息提供一种可以通过编程访问的途径。例如上面的例子中,PhoneNumber类应该提供对areCodeprefixlineNumber的访问方法。

谨慎地覆盖clone

Cloneable接口的目的是作为对象的一个mixin接口,表明这样的对象允许克隆。
如果一个类实现了CloneableObjectclone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException

虽然规范中没有明确指出,事实上,实现Cloneable接口的类是为了提供一个功能适当的公有clone方法。由此得到一种语言之外的机制:它无须调用构造器就可以创建对象。

clone方法的通用约定是非常弱的,下面来自Object规范中的约定内容:
创建和返回该对象的一个拷贝,这个拷贝的精确含义取决于该对象的类。一个版的含义是,对于任何对象x,表达式
x.clone() != x 会返回true,并且表达式x.clone().getClass() == x.getClass()将会返回true,但这些都不是绝对的要求。
虽然在通常情况下x.clone().equals(x)将会返回true,但是,这也不是一个绝对的要求。
按照约定,这个方法返回的对象应该通过调用super.clone获得。如果类及其超类遵守这一约定,那么:
x.clone().getClass() == x.getClass()

假设你希望在一个类中实现Cloneable接口,并且它的超类都提供了行为良好的clone方法。
首先调用super.clone方法。由此得到的对象将是原始对象功能完整的克隆。在这个类中声明的域将等同于被克隆对象中相应的域。如果每个域包含一个基本类型的值(不可变的),或包含一个指向不可变对象的引用,那么被返回的对象可能正是你所需要的对象,在这种情况下不要再做进一步处理。

例如PhoneNumber类正是如此,但要注意,不可变的类永远都不要提供clone方法

public class PhoneNumber implements Cloneable{...@Overrideprotected PhoneNumber clone() {try {return (PhoneNumber) super.clone();} catch (CloneNotSupportedException e) {throw new AssertionError();//不会发生}}}

首先要实现Cloneable接口。虽然Objectclone方法返回的是Object,但这个clone方法返回的却是PhoneNumber。这么做是合法的,也是我们所期望的,因为Java支持协变返回类型。换句话说,目前覆盖方法的返回类型可以是被覆盖方法的返回类型的子类。对super.clone方法的调用应当包含在一个try-catch块中。由于PhoneNumber实现了Cloneable接口,我们知道调用super.clone方法一定会成功,因此不会抛出CloneNotSupportedException

如果对象中包含的域引用了可变的对象,使用上述这种简单的clone实现可能会导致灾难性的后果。例如Stack类:

class Stack {private Object[] elements;private int size = 0;private static final int DEFAULT_INITIAL_CAPACITY = 16;public Stack() {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;return result;}private void ensureCapacity() {if (elements.length == size) {elements = Arrays.copyOf(elements, 2 * size + 1);}}
}

假设希望把这个类做成可克隆的。如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例,它的elements域将引用与原始Stack实例相同的数组。

如果调用Stack类中唯一的构造器,这种情况就永远不会发生。实际上,clone方法就是另一个构造器;必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。
必须要拷贝栈的内部信息,最容易的做法是在elements数组中递归地调用clone:

@Override
protected Stack clone() {try {Stack result = (Stack) super.clone();result.elements = elements.clone();return result;} catch (CloneNotSupportedException e) {throw new AssertionError();}
}

递归地调用clone有时还不够。例如,假如你正在为一个散列表编写clone方法,它的内部数据包含一个散列桶数组,每个散列桶都指向“键-值”对链表的第一项:

public class HashTable implemnts Cloneable {private Entry[] buckets = ...;private static class Entry {final Object key;Object value;Entry next;}...
}

假设你仅仅递归地克隆这个散列桶数组:

@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 implemnts 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());}}@Overridepublic 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();}}...
}

虽然这种方法很灵活,但是这样克隆一个链表并不是一种好方法,因为针对列表中的每个元素,它都要消耗一段栈空间。如果链表较长,容易导致栈溢出,可以用迭代来代替递归:

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方法也不应该在构造的过程中,调用可以覆盖的方法。

对象拷贝的更好的办法是提供一个拷贝构造器或拷贝工厂。拷贝构造器只是一构造器,它唯一的参数类型是包含该构造器的类,例如:

public Yum(Yum yum) {...
}

拷贝工厂是类似于拷贝构造器的静态工厂:

public static Yum newInstance(Yum yum) {...
}

既然所有的问题都与Cloneable接口有关,新的接口就不应该继承这个接口,新的课扩展的类也不应该实现这个接口。总是,克隆功能最好由构造器或工厂提供。这条规则最绝对的列外是数组,最好利用clone方法复制数组。

考虑实现Comparable接口

一旦实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作。

compareTo方法的通用约定于equals方法的约定相似:
在下面的说明中,符号sgn表示数学中的signum函数,它根据表达式的值为负数、零和正值,分别返回-1,0或1。

  • 实现者必须确保所有的xy都满足sgn(x.compareTo(y)) = -sgn(y.compareTo(x))
  • 还必须确保这个比较关系是可传递的,x.compareTo(y) > 0 && y.compareTo(z) > 0意味着x.compareTo(z) > 0
  • 最后,还须确保x.compareTo(y) == 0 意味着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  • 强烈建议(x.compareTo(y) == 0 ) == (x.equals(y)),但这并非绝对必要

如果一个类有多个关键域,那么,按什么样的顺序来比较这些域是非常关键的。必须从最关键的域开始,逐步进行到所有的重要域。如果某个域产生了非零的结果,则整个比较操作结束,并返回结果。

在Java8中,Comparable接口配置了一组比较器构造方法,使得比较器的构造工作变得非常流畅。之后,按照Comparable接口的要求,这些比较器可以用来实现一个compareTo方法。下面是使用了这个方法之后的PhoneNumbercompareTo方法:

private static final Comparator<PhoneNumber> COMPARATOR = Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode).thenComparingInt(pn -> pn.prefix).thenComparingInt(pn -> pn.lineNum);public int compareTo(PhoneNumber pn) {return COMPARATOR.compare(this,pn);
}

compareTocompare方法偶尔也会依赖于两个值之间的区别,即如果第一个值小于第二个值,则为负;如果两个值相等,则为零;如果第一个值大于第二个值,则为正:

static Comparator<Object> hashCodeOrder = new Comparator<>() {public int compare(Object o1, Object o2) {return o1.hashCode() - o2.hashCode();}
}

千万不要使用这个方法,它很容易造成整数溢出。要么使用静态方法compare:

static Comparator<Object> hashCodeOrder = new Comparator<>() {public int compare(Object o1, Object o2) {return Integer.compare(o1.hashCode(),o2.hashCode());}
}

要么使用一个比较器构造方法:

static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

《Effective Java 3rd》读书笔记——对于所有对象都通用的方法相关推荐

  1. 《Effective Java》读书笔记 Item 1:考虑静态工厂方法,而不是构造器

    众所周知,要想能获取一个类的实例,该类得要提供一个public的构造器.但是<Effective Java>书中说还有一个方法,那就是提供静态工厂方法(static factory met ...

  2. 《Effective Java》读书笔记

    引言 1 代码应该被重用 ,而不是被拷贝. 2 错误应该尽早被检测出来,最好是在编译时刻. 3 接口.类.数组是引用类型(对象), 基本类型不是 第二章 创建和销毁对象 1 考虑用静态工厂方法代替构造 ...

  3. 《Effective Java》读书笔记,flutter游戏开发

    1 代码应该被重用 ,而不是被拷贝. 2 错误应该尽早被检测出来,最好是在编译时刻. 3 接口.类.数组是引用类型(对象), 基本类型不是 第二章 创建和销毁对象 1 考虑用静态工厂方法代替构造器. ...

  4. Effective Java:对于全部对象都通用的方法

    前言: 读这本书第1条规则的时候就感觉到这是一本非常好的书.可以把我们的Java功底提升一个档次,我还是比較推荐的.这里我主要就关于覆盖equals.hashCode和toString方法来做一个笔记 ...

  5. Effective Java:对于所有对象都通用的方法

    前言: 读这本书第1条规则的时候就感觉到这是一本很好的书,可以把我们的Java功底提升一个档次,我还是比较推荐的.这里我主要就关于覆盖equals.hashCode和toString方法来做一个笔记总 ...

  6. 《Effective java》—–读书笔记

    2015年进步很小,看的书也不是很多,感觉自己都要废了,2016是沉淀的一年,在这一年中要不断学习.看书,努力提升自己!预计在2016年要看12本书,主要涉及java基础.Spring研究.java并 ...

  7. 《Effective Java》 第二讲:对于所有对象都通用的方法

    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到教程. 上接<Effective Java> 第一讲:创建和销毁对象 八.覆盖 equals 时 ...

  8. 《Effective Java》读书笔记--创建和销毁对象

    2019独角兽企业重金招聘Python工程师标准>>> 考虑用静态工厂方法代替构造函数. 当我们在写一个工具类时,是不希望用户将该类实例化的,所以应该定义一个private的构造函数 ...

  9. 《Effective Java》读书笔记 - 11.序列化

    Chapter 11 Serialization Item 74: Implement Serializable judiciously 让一个类的实例可以被序列化不仅仅是在类的声明中加上" ...

  10. 《Effective Java》 读书笔记(持续更新)

    2.1 用静态工厂方法代替构造器 静态工厂方法: 不通过 new (如:Date date = new Date();) 而是用一个静态方法来对外提供自身实例的方法叫做静态工厂方法(Static fa ...

最新文章

  1. BaseActivity的抽取
  2. C# WinForm开发系列 - WebBrowser
  3. 电气:6机30节点经济调度(考虑负荷平衡、线路容量、斜坡约束)代码实现
  4. Web 四种常见的POST提交数据方式
  5. linux c之管道的介绍、创建关闭和简单读写(父进程向子进程写入数据)
  6. 通过shell访问hive_【HIVE】SHELL调用Hive查询
  7. 苹果回应大数据杀熟:罪在开发者,和苹果没关系!
  8. J2ME游戏开发感想
  9. 材料力学考研可以用计算机吗,2020海南大学材料力学专业超详细考研经验分享...
  10. Java 最佳学习途径
  11. 苹果邮件怎么添加qq邮箱_QQ邮箱为何能收件,不能发邮件啊!??
  12. Android系统架构和应用程序基本概念详解
  13. 推荐.Net、C# 逆向反编译四大工具利器(请勿用来非法行为)
  14. PyTorch神经网络框架
  15. 『Java CVE』CVE-2022-34169: Xalan-J XSLT整数截断漏洞PoC结构再浅析
  16. 【附源码】计算机毕业设计SSM我的大学电子相册
  17. 中科院计算机网络信息中心是一种怎样的存在?
  18. 汇编语言-int指令
  19. python有哪些码_Python字节码介绍
  20. chrome运行 Android,告诉你如何在Chrome上运行Android应用!

热门文章

  1. 模块电源(三):PCB Layout
  2. CentOS下安装php gd库报错Error: php56w-common conflicts with php-common-5.3.3-48.el6_8.x86_64
  3. hightcharts 如何修改legend图例的样式
  4. 【转】Windows和Ubuntu双系统,修复UEFI引导的两种办法
  5. AutoCompleteTextView输入汉字拼音首字母实现过滤提示(支持多音字,Filterable的使用)...
  6. solr处理oracle中CLOB、BLOB
  7. 鼠标放在一个连接上,会显示图片(类似tooltip)
  8. Java:下拉列表绑定后台数据
  9. Animation中的scale、rotate、translate、alpha
  10. LeetCode Factorial Trailing Zeroes (阶乘后缀零)