Java 用起来如此舒适的一个因素在于,它是一门安全的语言 (safe language)。这意味着,它对于缓冲区溢出、数组越界、非法指针以及其他的内存破坏错误都自动免疫,而这些错误却困扰着诸如 C 和 C++ 这样的不安全语言。在一门安全语言中,在设计类时,无论系统的其他部分发生什么问题,类的约束都可以保持为真。对于那些 “把所有内存当作一个巨大的数组来对待” 的语言来说,这是不可能的。
即使在安全的语言中,如果不采取一点措施,还是无法与其它的类隔离开来。建设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序。实际上,只有当有人试图破坏系统的安全性时,才可能发生这种情况;更有可能的是,对你的 API 产生误解的程序员,所导致的各种不可预期的行为,只好由类来处理。无论是何种情况,编写面对客户端的不良行为仍保持健壮性的类,这是非常值得投入时间去做的事情。
// Broken "immutable" time period class publicclassPeriod { privatefinal Date start; privatefinal Date end;
publicPeriod(Date start, Date end) { if (start.compareTo(end) > 0) thrownewIllegalArgumentException(start + " after " + end); this.start = start; this.end = end; }
public Date start() { return start; }
public Date end() { return end; } }
乍看之下,此类似乎是不可变的,并且加了周期的起始时间 (start) 不能在结束时间 (end) 之后。然而,因为 Date 类本身是可变的,因此很容易违反这个约束条件:
1 2 3 4 5
// Attack the internals of a Period instance Datestart=newDate(); Dateend=newDate(); Periodp=newPeriod(start, end); end.setYear(78); // Modifies internals of p!
可以肯定地说,只要有可能,都应该使用不可变的对象作为对象内部的组件,这样就不必再为保护性拷贝(见第15条)操心。在前面的 Period 例子中,有经验的程序员通常使用 Date.getTime() 返回的 long 基本类型作为内部的时间表示法, 而不是使用 Date 对象引用。他们之所以这样做,主要因为 Date 是可变的。
var output = $@"The First five items are: { src.Take( 5 ).Select( n => $@"Item: {n.ToString()}" ).Aggregate( (c, a) => $@"{Environment.NewLine}{a}" ) }";
上面的这种写法可能不太会出现在正式的产品代码中,但可以看出,内插字符串和 C# 之间结合的相当密切。ASP.NET MVC 框架中的 Razor View 引擎也支持内插字符串,使得开发者在编写 Web 应用程序时能够更便捷地以 HTML 的形式来输出信息。默认的 MVC 应用程序本身就演示了怎样在 Razor View 中使用内插字符串,以下示例节选自 controller 部分,它可以显示当前登入的用户名:
// Initialize unitCircle to contain all Points on the unit circle privatestaticfinal Set<Point> unitCircle = Set.of( newPoint(1, 0), newPoint(0, 1), newPoint(-1, 0), newPoint(0, -1) );
// try-with-resources - the best way to close resources! publicstatic String firstLineOfFile(String path)throws IOException { try (BufferedReaderbr=newBufferedReader(newFileReader(path))) { return br.readLine(); } }
// try-with-resources on multiple resources - short and sweet publicstaticvoidcopy(String src, String dst)throws IOException { try (FileInputStreamin=newFileInputStream(src); OutputStreamout=newFileOutputStream(dst)) { byte[] buf = newbyte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } }
/** * DON'T DO THIS! * Error: Method does not override method from its superclass */ @Override publicbooleanequals(Bigram b) { return first == b.first && second == b.second; }
如果插入这个注解,会发现错误信息。将其改正为:
1 2 3 4 5 6 7
@Override publicbooleanequals(Object o) { if (!(o instanceof Bigram)) returnfalse; Bigramb= (Bigram) o; return first == b.first && second == b.second; }
// DON'T DO THIS! /** * I'm Son, I have $0 * I'm Son, I have $4 * This guy has $2 */ publicclassFieldHasNoPolymorphic { publicstaticvoidmain(String[] args) { Fatherguy=newSon(); System.out.println("This guy has $" + guy.money); }
staticclassFather { publicintmoney=1;
publicFather() { money = 2; showMeTheMoney(); }
publicvoidshowMeTheMoney() { System.out.println("I'm Father, I have $" + money); } }
staticclassSonextendsFather { publicintmoney=1;
publicSon() { money = 4; showMeTheMoney(); }
publicvoidshowMeTheMoney() { System.out.println("I'm Son, I have $" + money); } } }
输出的两句都是 “I’m Son”,因为 Son 类在创建的时候,首先隐式调用了 Father 的构造函数,而 Father 构造函数中对 showMeTheMoney 的调用是一次虚方法的调用,执行的版本是 Son::showMeTheMoney 方法,所以输出的是 “I’m Son”。虽然父类的 money 已经初始化成 2,但 Son::showMeTheMoney 方法中访问的是子类的 money,这里的结果是 0,因为它要到子类的构造函数执行时才会被初始化。之后子类构造方法执行输出 4,main 的最后一句通过外观类型访问到了父类中的 money,输出 2。