美文网首页
Java中的幽灵类型

Java中的幽灵类型

作者: superxff | 来源:发表于2017-11-30 16:04 被阅读0次

    先上结论:幽灵类型(Phantom Type)是一种可以把有些运行时才能检测到的错误,在编译时检测出来的技巧。按照有些老外的观点,就是“Making Wrong Code Look Wrong”。在面向对象的编程语言之中,幽灵类型的实现,往往与状态模式较为接近,但比状态模式提供了更强的纠错功能。在Java 5 以后的版本里,程序员可以使用泛型。通过泛型的类型参数,Java 中也拥有了幽灵类型的能力。

    上面的阐述是不是很难看懂?我也觉得拗口,让我们直接进入具体的例子。假设我们要写一个飞机控制程序,操作飞机起飞或者落地。这个程序有一个非常强的业务约束,就是必须保证飞机一开始必须出现在的地上,只有在地上的飞机可以起飞,只有起飞的飞机可以落地,那么我们应该怎样设计我们的程序(主要是类型关系),来保证这个约束必然成立呢?

    让我们先来定义一组状态接口:

    /** * FlightStatus.java *@authorliangchuan */publicinterfaceFlightStatus{}/** * Flying.java *@authorliangchuan */publicinterfaceFlyingextendsFlightStatus{}/** * Landed.java *@authorliangchuan */publicinterfaceLandedextendsFlightStatus{}

    从字面上即可看出,这是三个接口表示状态的接口类型。Flying 与 Landed 分别是 FlightStatus 的子类型,它们全都不包含任何可以使用的内容,完全通过类型名称来进行识别和区分。在 Java 这种指称类型(Nominal Typing) 的语言中,这通常被称为 Tagging Interface 或者叫 Mark Interface。

    接下来我们来定义一个飞机类型:

    /** * Plane.java * 这个类型可以被用类型参数具体化为任何 FlightStatus 的 飞机。即 Plane 与 Plane。 *@authorliangchuan */publicclassPlane{privateintpassenger;publicintgetPassenger(){returnpassenger;    }// 禁掉了除工厂方法和指定的状态构造方法以外的所有其他构造方法。当然,防不了反射攻击(reflection attack)。privatePlane(intpassenger){this.passenger = passenger;    }/**    * 工厂方法    *@return*/publicstaticPlanenewPlane(){returnnewPlane(10);    }/**    * 状态构造方法    * 在这里每次飞机从一个状态转成另一个飞机状态,都产生了一个新的对象,类似 Value Object 的模式。    *@paramp    */privatePlane(Plane p){// 在这里,我们可以使用装饰器模式。也可以使用 clone 模式,把乘客(也就是内部状态)移交过去。这取决于我们要不要把旧飞机实例的状态迁移到新飞机实例上。this.passenger = p.getPassenger();// 做任何想要做的事情}publicstaticclassAirTrafficController{publicstaticPlaneland(Plane p){returnnewPlane(p);        }publicstaticPlanetakeOff(Plane p){returnnewPlane(p);        }    }}

    这个 Plane 类型有什么特别的地方呢?

    它只能使用有限的构造器来构造飞机,除此之外,都会因为方法签名带来编译错误

    实际上,一开始只有用工厂方法才能构造出落地的飞机,无法一开始就制造出在天上飞的飞机,否则,也会因为方法签名带来编译错误

    只有有状态的飞机,才能产生新的有状态的飞机。而这个有状态的飞机的转换构造函数(类似 CPP 的拷贝构造函数),只有 AirTrafficController 可以访问。

    AirTrafficController 提供了两个状态转换方法: land 与 takeOff 。这两个方法会根据一个输入飞机的状态,来切换出另一个状态的飞机。而它们因为方法签名的关系,只能接受有限的飞机状态,否则会产生编译错误

    到此我们的类库已经写完了。试试写一个应用程序来测试它:

    /** * * AirPlaneApp.java *@authorliangchuan */publicclassAirPlaneApp{publicstaticvoidmain(String[] args){        Plane p = Plane.newPlane();        Plane fly= Plane.AirTrafficController.takeOff(p);        Plane land= Plane.AirTrafficController.land(fly);// 无法编译通过:///Plane reallyLanded =  Plane.AirTrafficController.land(land);//Plane reallyFlying =  Plane.AirTrafficController.takeOff(fly);}}

    想一想,如果我们把我们的程序当做类库发布出去给其他的程序员用。类库使用者因为加班上线已经写代码到了凌晨一点,错误地试图把一架正在起飞的飞机再次起飞,立刻就会得到编译器的错误提醒。这种预先设计的防呆类型系统,成功地降低了系统在变得复杂的以后,出现低级错误的可能。

    为什么这种技巧叫幽灵类型呢?因为我们只在方法的签名的类型参数(type parameter)里指定了一个具体类型,并没有实际在方法体内部真的使用到这种类型的任何具体内容。诚如我们在代码中所见,FlightStatus 这种接口只是一种编译时类型识别的 type witness(类型见证人),帮助编译器推导当前的代码的合法性,其本身及其子类型,都不包含任何可以使用的内容。

    可能有读者会问,这种方法很像状态模式,它和状态模式的区别在哪里呢?

    一个最显著的区别就是,状态模式里面,表示 state 的是实例里的一个 state 变量,而不是写在实例类型参数里的 state 类型见证人。使用状态模式,很容易让程序员写出if(state == flying) throw new Exception()之类的代码,这种代码即使写错了,编译器也检测不出来,因为这是运行时检测(是不是很讽刺,检测出错的代码,自己也会出错)。

    更重要的是,类型参数的出现,使得一段代码里 plane 的状态表面化了。想一想,一个使用状态模式的 plane,我们在客户端代码里未必就能在当前上下文里知道它内部的 state 现在变成什么样了。但如果我们使用幽灵类型,那么我们只要看看当前上下文的方法签名的类型参数,就能明确理解当前飞机的 state。

    我们应该什么时候使用幽灵类型呢?这是一个很难把控的问题。读者已经看到了,实际上这个飞机的例子也是非常精巧,需要仔细思考才能明白其中奥妙的,所以幽灵类型在 Java 的世界里长久不为人知。笔者的愚见是,在像飞机这类例子里面,有需要严格区分状态(或者子类型)和方法的匹配的需求,可以考虑使用幽灵类型。

    这篇文章缘于知乎上的一个有意思的问答《你见过哪些让你瞠目结舌的 Java 代码技巧?》。当时看到这种用法,我就觉得这是一种很有意思的利用编译器进行防御性编程的例子。此外,本文的飞机例子基本源自于,但加上了一些我自己的注释和修改,便于读者理解(在原文的例子中,原作者似乎意识不到Plane(Plane p)不应该是个公有方法,而AirTrafficController应该是个内部类。请读者自行思考为什么。 )。实际上还有更多的例子,可以在这里看到。在函数式编程语言的世界,如 Haskell、Scala、OCaml 里,幽灵类型是天然被支持的,但在 Java 的世界里,必须要到提供泛型能力的 Java 5 版本以后,才能这种技巧。

    相关文章

      网友评论

          本文标题:Java中的幽灵类型

          本文链接:https://www.haomeiwen.com/subject/fpmwpxtx.html