Java中的幽灵类型

先上结论:幽灵类型(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 版本以后,才能这种技巧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容