从 Date 和 Timestamp 看 Java 继承特性和 equals() 方法约定间的冲突

Java 的继承是面向对象最显著的一个特性。Date 和 Timestamp 是 Java 中的两个和时间有关的类。Timestamp 继承自 Date。

equals() 方法是 Java 中最常用的方法之一,在 Object 中定义,任何对象都有的方法,我们在自定义类的时候,一般都会重写此方法。

equals() 方法的约定

首先讲一讲重写 equals() 方法时要注意的约定

  1. 自反性(reflexive),对于任何非 null 的引用值 x,x.equals(x)必须返回true。
  2. 对称性(symmetric),对于任何非 null 的引用值 x 和 y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
  3. 传递性(transitive),对于任何非 null 的引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 也返回 true,那么 x.equals(z) 也必须返回 true。
  4. 一致性(consistent),对于任何非 null 的引用值 x 和 y,只要 equals 的比较操作在对象中所有的信息没有被修改,多次调用 x.equals(y) 就会一致地返回 true,或者一致地返回 false。
  5. 对于任意非 null 的引用值 x,x.equals(null) 必须返回false。

这几条简单易懂。一般我们重写 equals() 方法会先判断类型是否一致,然后根据类中的成员域来判断。比如下面这个简化的 Date 类。

public class Date{
    private transient long fastTime;
    public Date(long date) {
        fastTime = date;
    }
    public long getTime() {
        return fastTime;
    }
    public boolean equals(Object obj) {
        return obj instanceof Date && getTime() == ((Date) obj).getTime();
    }
}

equals 方法首先判断是否是 Date 类型,然后判断 fastTime 字段值是否一致。

继承

我们再来看一下简化的 Timestamp 类

public class Timestamp extends Date {
    private int nanos;
    public Timestamp(long time) {
        super((time/1000)*1000);
        nanos = (int)((time%1000) * 1000000);
    }
    public boolean equals(Object ts) {
        if (ts instanceof Timestamp) {
            if (super.equals(ts)) {
                if  (nanos == ts.nanos) {
                    return true;
                } 
            } 
        } 
        return false;
    }
}

Timestamp 在 Date 的基础上增加了一个 nanos 字段。所以它的 equals 方法首先判断是否是 Timestamp 类型,然后调用父类 equals 方法判断 fastTime 字段,最后判断 nanos 字段。

我们看下面一段代码。

继承破坏约定

Date date = new Date(1000);
Timestamp timestamp1 = new Timestamp(1000);
timestamp1.setNanos(1);
Timestamp timestamp2 = new Timestamp(1000);
timestamp2.setNanos(2);
System.out.println(date.equals(timestamp1));//true
System.out.println(date.equals(timestamp2));//true
System.out.println(timestamp1.equals(date));//false
System.out.println(timestamp1.equals(timestamp2));//false

很明显,正是因为继承关系,导致 equals 方法的对称性和传递性遭到破坏。事实上,Java 源码中 Timestamp 的注释里已经提到了这个问题。

Note: This type is a composite of a java.util.Date and a separate nanoseconds value. Only integral seconds are stored in the java.util.Date component. The fractional seconds – the nanos – are separate. The Timestamp.equals(Object) method never returns true when passed an object that isn’t an instance of java.sql.Timestamp, because the nanos component of a date is unknown. As a result, the Timestamp.equals(Object) method is not symmetric with respect to the java.util.Date.equals(Object) method. Also, the hashCode method uses the underlying java.util.Date implementation and therefore does not include nanos in its computation.
Due to the differences between the Timestamp class and the java.util.Date class mentioned above, it is recommended that code not view Timestamp values generically as an instance of java.util.Date. The inheritance relationship between Timestamp and java.util.Date really denotes implementation inheritance, and not type inheritance.

重点在最后一句。不建议把 Timestamp 实例当做 Date 的实例。它们这种继承关系只是实现层面上的继承,并非类型层面上的继承。

当我们感觉到问题的时候,就应该开始思考如何解决了。

getClass()

首先想到的就是 instanceof 来判断类型时无法识别继承关系,而用 getClass() 方法可以准确获知类型。
我们用这种思路实现一个 equals() 方法。

public boolean equals(Object obj) {
     if(obj == null || obj.getClass() != this.getClass()){
        return false;
     }
     //other conditions
}

这个 equals 能够很刻薄地判断两个对象是否相等,并能够严格遵守 equals 的各项约定。但是它却违背了面向对象思想的一项很重要的原则,里氏替换原则。

里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。

这种 equals 方法显然使得子类替换父类受影响,这种行为和继承的思想理念相违背。

用组合而不是继承

还有另一种方法,便是放弃继承。
再设计模式中,常常被人提及的就是“组合优先于继承”
上面例子中我们可以把 Date 当做 Timestamp 的一个域,就解决了 equals 的问题。

public class Timestamp{
    private int nanos;
    private Date date;
    public Timestamp(long time) {
        date = new Date(time);
        nanos = (int)((time%1000) * 1000000);
    }
    public boolean equals(Object ts) {
        if (ts instanceof Timestamp) {
            if  (nanos == ((Timestamp)ts).nanos && date.equals(((Timestamp)ts).date)) {
                return true;
            } 
        } else {
            return false;
        }
    }
}

总结

我们并没有完美的解决方案调和 Java 的继承特性和 equals 方法约定,它们之间的冲突依然还在。

参考《Effective Java》

[翻译]CSS 的 background 属性

原文The Background Properties
(译文发布在伯乐在线,感谢刘唱校稿)

正如我之前所说,文档树中的元素都是一个方盒。每个盒都有背景层,它是透明的、有颜色的或一张图片。背景层由 8 个 CSS 属性(和 1 个简写属性)控制

background-color

background-color 属性设置元素背景颜色。它的值可以是一个合法的颜色值或 transparent 关键字。

.left { background-color: #ffdb3a; }
.middle { background-color: #67b3dd; }
.right { background-color: transparent; }

在由 background-clip 关键字指定的盒模型区域内填充背景颜色。如果也设置了背景图片,颜色层会在它们后面。图片层可以设置多个,每个元素只拥有一个颜色层。

background-image

background-image 属性为元素定义一个(或多个)背景图片。它的值是用 url() 符号定义的图片 url。none 也是允许的,它会被当做空的一层。

.left { background-image: url('ire.png'); }
.right { background-image: none; }

我们也可以指定多个背景图片,用逗号隔开。沿着 z 轴从前向后依次绘制每个图片。

.middle { 
  background-image: url('khaled.png'), url('ire.png');

  /* Other styles */
  background-repeat: no-repeat; 
  background-size: 100px;
}

background-repeat

background-size 指定大小和 background-position 指定位置后,background-repeat 属性控制如何平铺背景图片。

属性值可以是下面关键字之一 repeat-xrepeat-yrepeatspaceroundno-repeat。除了前两个(repeat-xrepeat-y)之外,其他关键字可以只写一次来同时定义 x 轴和 y 轴,或分开定义两个维度。

.top-outer-left { background-repeat: repeat-x; }
.top-inner-left { background-repeat: repeat-y; }
.top-inner-right { background-repeat: repeat; }
.top-outer-right { background-repeat: space; }

.bottom-outer-left { background-repeat: round; }
.bottom-inner-left { background-repeat: no-repeat; }
.bottom-inner-right { background-repeat: space repeat; }
.bottom-outer-right { background-repeat: round space; }

background-size

background-size 属性定义背景图片的大小。它的值可以是一个关键字、一个长度或一个百分比。

属性可用的关键字是 containcovercontain 会按比例将图片放大直到宽高完全适应区域。cover 会将其调整至能够完全覆盖该区域的最小尺寸。

.left { 
  background-size: contain;
  background-image: url('ire.png'); 
  background-repeat: no-repeat;
}
.right { background-size: cover; /* Other styles same as .left */ }

对于长度值和百分比,我们可以用来定义背景图片的宽高。百分比值通过元素的尺寸来计算。

.left { background-size: 50px; /* Other styles same as .left */ }
.right { background-size: 50% 80%; /* Other styles same as .left */ }

background-attachment

background-attachment 属性控制背景图片在可视区和元素中如何滚动。它有三个可能的值。

fixed 意思是背景图片相对于可视区固定,即使用户滚动可视区时也不移动。local 意思是背景在元素中位置固定。如果元素有滚动机制,背景图片会相对于顶端定位,当用户滚动元素时,背景图片会离开视野。最后,scroll 意思是背景图片固定,不会随着元素内容滚动。

.left { 
  background-attachment: fixed;
  background-size: 50%;
  background-image: url('ire.png'); 
  background-repeat: no-repeat;
  overflow: scroll;
}
.middle { background-attachment: local; /* Other styles same as .left */ }
.right { background-attachment: scroll; /* Other styles same as .left */ }

background-position

这个属性,结合 background-origin 属性,定义了背景图片起始位置。它的值可以是一个关键字、一个长度或一个百分比,我们可以依次定义 x 轴和 y 轴的位置。

可用的关键字有toprightbottomleftcenter。我们可以任意组合使用,如果只指定了一个关键字,另一个默认是 center

.top-left { 
  background-position: top;
  background-size: 50%;
  background-image: url('ire.png'); 
  background-repeat: no-repeat;
}
.top-middle { background-position: right;  /* Other styles same as .top-left */ }
.top-right { background-position: bottom;  /* Other styles same as .top-left */ }
.bottom-left { background-position: left;  /* Other styles same as .top-left */ }
.bottom-right { background-position: center;  /* Other styles same as .top-left */ }

对于长度值和百分比值,我们也可以依次定义 x 轴和 y 轴的位置。百分比值相对于容器元素。

.left { background-position: 20px 70px; /* Others same as .top-left */ }
.right { background-position: 50%; /* Others same as .top-left */ }

background-origin

background-origin 属性定义背景图片根据盒模型的哪个区域来定位。

可用的值有 border-box基于边框(Border)区域定位图片,padding-box 基于填充(Padding)区域,content-box 基于内容(Content)区域。

.left { 
  background-origin: border-box;
  background-size: 50%;
  background-image: url('ire.png'); 
  background-repeat: no-repeat;
  background-position: top left; 
  border: 10px dotted black; 
  padding: 20px;
}
.middle { background-origin: padding-box;  /* Other styles same as .left*/ }
.right { background-origin: content-box;  /* Other styles same as .left*/ }

background-clip

background-clip 属性决定背景绘制区域,也就是背景可以被绘制的区域。像 background-origin 属性一样,它也基于盒模型。

.left{ 
  background-clip: border-box;
  background-size: 50%;
  background-color: #ffdb3a; 
  background-repeat: no-repeat;
  background-position: top left; 
  border: 10px dotted black; 
  padding: 20px;
}
.middle { background-clip: padding-box;  /* Other styles same as .left*/ }
.right { background-clip: content-box;  /* Other styles same as .left*/ }

background

最后,background 属性是其他背景相关属性的简写。子属性的顺序并没有影响,因为每个属性的数据类型不同。然而,对于 background-originbackground-clip 属性,如果只指定了一个盒模型区域,会应用到两个属性。如果指定了两个,第一个设置为background-origin 属性。

[翻译]防御性编程的艺术

原文The Art of Defensive Programming
(译文发布在伯乐在线,感谢刘唱校稿)

为什么开发者不编写安全的代码?我们在这并不是要再一次讨论「整洁代码」。我们要从纯粹的实用观点出发,讨论其他东西——软件的安全性和保密性。是的,因为一个不安全的软件是无用的。我们来看看什么是不安全的软件。

  • 欧洲航天局的 Ariane 5 Flight 501 在起飞后 40 秒毁坏(1996年6月4日)。价值 10 亿美金的原型火箭因为搭载的导航软件里的一个 bug 而自毁。

  • 20 世纪 80 年代,在 Therac-25 radiation 医疗机器的控制代码里的一个 bug 使得 X光强度过大,直接导致至少 5 名病人死亡。

  • MIM-104 Patriot(爱国者)里的一个软件错误使它的系统每一百小时有三分之一秒的时钟偏移——导致定位拦截入侵导弹失败。伊拉克导弹飞入在达兰(沙特阿拉伯东北部城市)的一个军营(1991年2月25日),杀害了28名美国人。

这应该能够说明编写安全软件的重要性了,尤其在特定的环境中。当然也包括其他用例中,我们也应该意识到我们的软件 bug 会导致什么。

防御式编程初窥

为什么我认为在特定种类工程中,防御式编程是解决这些问题好的方式。

抵御那些不可能的事,因为看似不可能的事也会发生。

防御式编程中有很多防御方式,这也取决于你的软件项目所需的「安全」的级别和资源级别。

防御式编程是防御式设计的一种形式,用来确保软件在未预料的环境中能继续运行。防御式编程的实践往往用于需要高可靠性、安全性、保密性的地方。——Wikipedia

我个人相信这种实现适合很多人调用的大型、长期的项目。例如,一个需要大量维护的开源项目。

我们来探索一下我提出的关键点,来完成一个防御式编程的实现。

##永远不要相信用户输入

设想你总是将获取到你不期待的东西。这将使你成为防御式程序员,针对用户输入或传入你的系统的一般东西。因为我们说过我们期待异常情况。试着尽可能严谨。断言输入值是你所期待的。

进攻是最好的防守

设置白名单而不是黑名单。举个例子,当你验证图像扩展名时,不要检查非法的类型,而是检查合法的类型并排除其他类型。在 PHP 有无数的开源校验库让你的工作变得简单。

进攻是最好的防守。共勉

使用数据库抽象

OWASP Top 10 Security Vulnerabilities 排首位的是注入攻击。这意味着有些人(很多人)还没有使用安全的工具来查询数据库。请使用数据库抽象包或库。在 PHP 里你可以使用 PDO确保防御基本注入

不要重复发明轮子

你不用框架(或微框架)吗?好吧你喜欢毫无理由地做额外的工作。这并不仅仅有关框架,也意味着你可以方便的使用已经存在的、测试过的、受万千开发者信任的、稳定的新特性,而不是你自己仅为了从中受益而制作的东西。你自己创建东西的唯一原因是你需要的东西不存在,或存在但不符合你的需求(性能差、缺失特性等等)。

这就是所谓的智能代码重用。拥抱它吧。

不要相信开发者

防御式编程与防御驱动相关联。在防御驱动中,我们假设我们周围的每个人都可能犯错。所以我们要注意别人的行为。相同观念也适用于防御式编程,我们作为开发者不要相信其他开发者的代码。我们同样也不要相信我们的代码。

在很多人调用的大型项目中,我们有许多方式编写并组织代码。这也导致混乱甚至更多的 bug。这也是为什么我们需要规范代码风格并做代码检查,让生活更轻松。

##编写符合 SOLID 原则的代码

这是(防御式)编程最困难的部分——编写不糟糕的代码。这也是很多人知道并讨论的,但没有人关心或注意并致力于实现符合 SOLID 原则的代码。

让我们看一些糟糕的例子

避免:未初始化的属性

<?php

class BankAccount
{
    protected $currency = null;
    public function setCurrency($currency) { ... }
    public function payTo(Account $to, $amount)
    {
        // sorry for this silly example
        $this->transaction->process($to, $amount, $this->currency);
    }
}

// I forgot to call $bankAccount->setCurrency('GBP');
$bankAccount->payTo($joe, 100);

在这个例子中,我们需要牢记签发付款前要先调用 setCurrency。这是很糟糕的事情,一个像这样的改变状态的操作(签发付款)不应该分两步,使用两个公开的方法。我们可以拥有许多方法付款,但我们必须只有一个公开的方法来改变状态(类不应该存在不一致的状态)。

在这个例子中,我们把它改进,将未初始化的属性封装进 Money 类。

<?php

class BankAccount
{
    public function payTo(Account $to, Money $money) { ... }
}

$bankAccount->payTo($joe, new Money(100, new Currency('GBP')));

它变得极其简单和安全。不要使用未初始化的对象属性。

避免:类的作用域外泄露状态

<?php

class Message
{
    protected $content;
    public function setContent($content)
    {
        $this->content = $content;
    }
}

class Mailer
{
    protected $message;
    public function __construct(Message $message)
    {
        $this->message = $message;
    }
    public function sendMessage(
    {
        var_dump($this->message);
    }
}

$message = new Message();
$message->setContent("bob message");
$joeMailer = new Mailer($message);

$message->setContent("joe message");
$bobMailer = new Mailer($message);

$joeMailer->sendMessage();
$bobMailer->sendMessage();

在上述代码中,Message 通过引用传递“joe message”到每个例子中。一个解决方案是克隆 message 对象到 Mailer 构造函数。但是我们应该做的是试着使用(不变的)值对象,而不是简单易变的 Message 对象。尽可能使用不变的对象。

<?php

class Message
{
    protected $content;
    public function __construct($content)
    {
        $this->content = $content;
    }
}

class Mailer 
{
    protected $message;
    public function __construct(Message $message)
    {
        $this->message = $message;
    }
    public function sendMessage()
    {
        var_dump($this->message);
    }
}

$joeMailer = new Mailer(new Message("bob message"));
$bobMailer = new Mailer(new Message("joe message"));

$joeMailer->sendMessage();
$bobMailer->sendMessage();

编写测试

这点我们很还需要再说吗?编写单元测试可以帮助你秉承一般的原则,比如高内聚、单一职责、低耦合和正确的对象组合。它帮助你不仅仅测试小的单元用例,也测试你组织对象方式。确实,当测试你的小功能时,你会清晰的看到你需要测试多少情况和需要模拟多少对象,来达到 100% 的覆盖率。

结论

希望你喜欢这篇文章。记住这些仅仅是建议,由你决定何时、何处以及是否应用它们。

感谢阅读!