博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[转载]Java理论与实践: 它是谁的对象?
阅读量:2449 次
发布时间:2019-05-10

本文共 8974 字,大约阅读时间需要 29 分钟。

Java理论与实践: 它是谁的对象?

在没有垃圾收集的语言中,比如C++,必须特别关注内存管理。对于每个动态对象,必须要么实现引用计数以模拟 垃圾收集效果,要么管理每个对象的“所有权”――确定哪个类负责删除一个对象。通常,对这种所有权的维护并没有什么成文的规则,而是按照约定(通常是不成 文的)进行维护。尽管垃圾收集意味着Java开发者不必太多地担心内存 泄漏,有时我们仍然需要担心对象所有权,以防止数据争用(data races)和不必要的副作用。在这篇文章中,Brian Goetz 指出了一些这样的情况,即Java开发者必须注意对象所有权。请在 上与作者及其他读者共享您对本文的一些想法(您也可以在文章的顶部或底部点击
讨论来访问论坛)。

如果您是在1997年之前开始学习编程,那么可能您学习的第一种编程语言没有提供透明的垃圾收集。每一个new 操作必须有相应的delete操作 ,否则您的程序就会泄漏内存,最终内存分配器 (memory allocator )就会出故障,而您的程序就会崩溃。每当利用 new 分配一个对象时,您就得问自己,谁将删除该对象?何时删除?

内存管理复杂性的主要原因是别名使用:同一块内存或对象具有 多个指针或引用。别名在任何时候都会很自然地出现。例如,在清单 1 中,在 makeSomething 的第一行创建的 Something 对象至少有四个引用:

  • something 引用。
  • 集合 c1 中至少有一个引用。
  • 当 something 被作为参数传递给 registerSomething 时,会创建临时 aSomething 引用。
  • 集合 c2 中至少有一个引用。
Collection c1, c2;     public void makeSomething {
Something something = new Something(); c1.add(something); registerSomething(something); } private void registerSomething(Something aSomething) {
c2.add(aSomething); }

在非垃圾收集语言中需要避免两个主要的内存管理危 险:内存泄漏和悬空指针。为了防止内存泄漏,必须确保每个分配了内存的对象最终都会被删除。 为了避免悬空指针(一种危险的情况,即一块内存已经被释放了,而一个指针还在引用它),必须在最后的引用释放之后才删除对象。为满足这两条约束,采用一定 的策略是很重要的。

所有权管理(ownership management) 是这样一个过程,该过程指明一个指针是“拥有”指针("owning" pointer),而 所有其他别名只是临时的二类副本( temporary second-class copies),并且只在所拥有的指针被释放时才删除对象。在有些情况下,所有权可以从一个指针“转移”到另一个指针,比如一个这样的方法, 它以一个缓冲区作为参数,该方法用于向一个套接字写数据,并且在写操作完成时删除这个缓冲区。这样的方法通常叫做接收器 (sinks)。在这个例子中,缓冲区的所有权已经被有效地转移,因而进行调用的代码必须假设在被调用方法返回时缓冲区已经被删除。(通过确保所有的别名 指针都具有与调用堆栈(比如方法参数或局部变量)一致的 作用域(scope ),可以进一步简化所有权管理,如果引用将由非堆栈作用域的变量保存,则通过复制对象来进行简化。)

blue_rule.gif
c.gif
c.gif
u_bold.gif

此时,您可能正纳闷,为什么我还要讨论内存管理、别名和对象所有权。毕竟,垃圾收集是 Java语言的核心特性之一,而内存管理是已经过时的一件麻烦事。就让垃圾收集器来处理这件事吧,这正是它的工作。 那些从内存管理的麻烦中解脱出来的人不愿意再回到过去,而那些从未处理过内存管理的人则根本无法想象在过去倒霉的日子里――比如1996年――程序员的编程是多么可怕。

blue_rule.gif
c.gif
c.gif
u_bold.gif

那么这意味着我们可以与对象所有权的概念说再见了吗?可以说是,也可以说不是。 大多数情况下,垃圾收集确实消除了显式资源存储单元分配(explicit resource deallocation)的必要(在以后的专栏中我将讨论一些例外)。但是,有一个区域中,所有权管理仍然是Java 程序中的一个问题,而这就是悬空别名(dangling aliases)问题。 Java 开发者通常依赖于这样一个隐含的假设,即假设由对象所有权来确定哪些引用应该被看作是只读的 (在C++中就是一个 const 指针),哪些引用可以用来修改被引用的对象的状态。当两个类都(错误地)认为自己保存有对给定对象的惟一可写的引用时,就会出现悬空指针。发生这种情况时,如果对象的状态被意外地更改,这两个类中的一个或两者将会产生混淆。

考虑清单 2 中的代码,其中的 UI 组件保存有一个 Point 对象,用于表示它的位置。当调用 MathUtil.calculateDistance 来计算对象移动了多远时,我们依赖于一个隐含而微妙的假设――即 calculateDistance 不会改变传递给它的 Point 对象的状态,或者情况更坏,维护着对那些 Point 对象的一个引用(比如通过将它们保存在集合中或者将它们传递到另一个线程),然后这个引用将用于在 calculateDistance 返回后更改Point 对象的状态。 在 calculateDistance的例子中,为这种行为担心似乎有些可笑,因为这明显是一个可怕的违背惯例的情况。但是,如果要说将一个可变的对象传递 给一个方法,之后对象还能够毫发无损地返回来,并且将来对于对象的状态也不会有不可预料的副作用(比如该方法与另一个线程共享引用,该线程可能会等待5分 钟,然后更改对象的状态),那么这只不过是一厢情愿的想法而已。

private Point initialLocation, currentLocation;     public Widget(Point initialLocation) {
this.initialLocation = initialLocation; this.currentLocation = initialLocation; } public double getDistanceMoved() {
return MathUtil.calculateDistance(initialLocation, currentLocation); } . . . // The ill-behaved utility class MathUtil public static double calculateDistance(Point p1, Point p2) {
double distance = Math.sqrt((p2.x - p1.x) ^ 2 + (p2.y - p1.y) ^ 2); p2.x = p1.x; p2.y = p1.y; return distance; }

大家对该例子明显而普遍的反应就是――这是一个愚蠢的例子――只是强调了这样一个事实,即对象所有权的概念在 Java 程序中依然存在,而且存在得很好,只是没有说明而已。calculateDistance 方法不应该改变它的参数的状态,因为它并不“拥有”它们――当然,调用方法拥有它们。因此说不用考虑对象所有权。

下面是一个更加实用的例子,它说明了不知道谁拥有对象就有可能会引起混淆。再次考虑一个以Point 属性 来表示其位置的 UI组件。 清单 3 显示了实现存取器方法 setLocation 和 getLocation的三种方式。第一种方式是最懒散的,并且提供了最好的性能,但是对于蓄意攻击和无意识的失误,它有几个薄弱环节。

public class Widget {
private Point location; // Version 1: No copying -- getter and setter implement reference // semantics // This approach effectively assumes that we are transferring // ownership of the Point from the caller to the Widget, but this // assumption is rarely explicitly documented. public void setLocation(Point p) {
this.location = p; } public Point getLocation() {
return location; } // Version 2: Defensive copy on setter, implementing value // semantics for the setter // This approach effectively assumes that callers of // getLocation will respect the assumption that the Widget // owns the Point, but this assumption is rarely documented. public void setLocation(Point p) {
this.location = new Point(p.x, p.y); } public Point getLocation() {
return location; } // Version 3: Defensive copy on getter and setter, implementing // true value semantics, at a performance cost public void setLocation(Point p) {
this.location = new Point(p.x, p.y); } public Point getLocation() {
return (Point) location.clone(); } }

现在来考虑 setLocation 看起来是无意的使用 :

Widget w1, w2;     . . .     Point p = new Point();     p.x = p.y = 1;     w1.setLocation(p);     p.x = p.y = 2;     w2.setLocation(p);

或者是:

w2.setLocation(w1.getLocation());

在setLocation/getLocation存取器实现的版本 1 之下,可能看起来好像第一个Widget的 位置是 (1, 1) ,第二个Widget的位置是 (2, 2),而事实上,二者都是 (2, 2)。这可能对于调用者(因为第一个Widget意外地移动了)和Widget 类(因为它的位置改变了,而与Widget代码无关)来说都会产生混淆 。在第二个例子中,您可能认为自己只是将Widget w2移动到 Widget w1当前所在的位置 ,但是实际上您这样做便规定了每次w1 移动时w2都跟随w1 。

setLocation 的版本 2 做得更好:它创建了传递给它的参数的一个副本,以确保不存在可以意外改变其状态的 Point的别名。但是它也并非无可挑剔,因为下面的代码也将具有一个很可能不希望出现的效果,即Widget在不知情的情况下被移动了:

Point p = w1.getLocation();     . . .     p.x = 0;

getLocation 和 setLocation 的版本 3 对于别名引用的恶意或无意使用是完全安全的。这一安全是以一些性能为代价换来的:每次调用一个 getter 或 setter 都会创建一个新对象。

getLocation 和 setLocation 的不同版本具有不同的语义,通常这些语义被称作值语义(版本 1)和引用语义(版本 3) 。不幸的是,通常没有说明实现者应该使用的是哪种语义。结果,这个类的使用者并不清楚这一点,从而作出了更差的假设(即选择了不是最合适的语义)。

getLocation 和 setLocation 的版本 3 所使用的技术叫做防御性复制( defensive copying),尽管存在着明显的性能上的代价,您也应该养成这样的习惯,即几乎每次返回和存储对可变对象或数组的引用时都使用这一技术,尤其是在您编 写一个通用的可能被不是您自己编写的代码调用(事实上这很常见)的工具时更是如此。 有别名的可变对象被意外修改的情况会以许多微妙且令人惊奇的方式突然出现,并且调试起来相当困难。

而且情况还会变得更坏。假设您是Widget类的一个使用者,您并不知道存取器具有值语义还是引用语义。 谨慎的做法是,在调用存取器方法时也使用防御性副本。 所以,如果您想要将 w2 移动到 w1 的当前位置,您应该这样去做:

Point p = w1.getLocation();     w2.setLocation(new Point(p.x, p.y));

如果 Widget 像在版本 2 或 3 中一样实现其存取器,那么我们将为每个调用创建两个临时对象 ――一个在 setLocation 调用的外面,一个在里面。

getLocation 和 setLocation 的版本 1 的真正问题不是它们易受混淆别名副作用的不良影响(确实是这样),而是它们的语义没有清楚的说明。如果存取器被清楚地说明为具有引用语义(而不是像通常那样被假设为值语义),那么调用者将更可能 认识到,在它们调用setLocation时,它们是将Point对象的所有权转移给另一个实体,并且也不大可能仍然认为它们还拥有Point对象的所有权,因而还能够再次使用它。

blue_rule.gif
c.gif
c.gif
u_bold.gif

如果一开始就使得Point 成为不可变的,那么这些与 Point 有关的问题早就迎刃而解了。不可变对象上没有副作用,并且缓存不可变对象的引用总是安全的,不会出现别名问题。如果 Point是不可变的,那么与setLocation 和 getLocation存取器的语义有关的所有问题都是非常确定的 。不可变属性的存取器将总是具有值引用,因而调用的任何一方都不需要防御性复制,这使得它们效率更高。

那么为什么不在一开始就使得Point 成为不可变的呢?这可能是出于性能上的原因,因为早期的 JVM具有不太有效的垃圾收集器。 那时,每当一个对象(甚至是鼠标)在屏幕上移动就创建一个新的Point的对象创建开销可能有些让人生畏,而 创建防御性副本的开销则不在话下。

依 后见之明,使Point成为可变的这个决定被证明对于程序清晰性和性能是昂贵的代价。Point类的可变性使得每一个接受Point作为参数或者要返回一 个Point的方法背上了编写文档说明的沉重负担。也就是说,它得说明它是要改变Point,还是在返回之后保留对Point的一个引用。 因为很少有类真正包含这样的文档,所以在调用一个没有用文档说明其调用语义或副作用行为的方法时,安全的策略是在传递它到任何这样的方法之前创建一份防御 副本。

有讽刺意味的是,使 Point成为可变的这个决定所带来的性能优势被由于Point的可变性而需要进行的防御性复制给抵消了。由于缺乏清晰的文档说明(或者缺少信任),在方法调用的两边都需要创建防御副本 ――调用者需要这样做是因为它不知道被调用者是否会粗暴地改变 Point,而被调用者需要这样做是因为它不知道是否保留了对 Point 的引用。

blue_rule.gif
c.gif
c.gif
u_bold.gif

下面是悬空别名问题的另一个例子,该例子非常类似于我最近在一个服务器应用中所看到的。 该应用在内部使用了发布-订阅式消息传递方式,以将事件和状态更新传达到服务器内的其他代理。这些代理可以订阅任何一个它们感兴趣的消息流。一旦发布之 后,传递到其他代理的消息就可能在将来某个时候在一个不同的线程中被处理。

清 单 4 显示了一个典型的消息传递事件(即发布拍卖系统中一个新的高投标通知)和产生该事件的代码。不幸的是,消息传递事件实现和调用者实现的交互合起来创建了一 个悬空别名。通过简单地复制而不是克隆数组引用,消息和产生消息的类都保存了前一投标数组的主副本的一个引用。如果消息发布时的时间和消费时的时间有任何 延迟,那么订阅者看到的 previous5Bids 数组的值将不同于消息发布时的时间,并且多个订阅者看到的前面投标的值可能会互不相同。在这个例子中,订阅者将看到当前投标的历史值和前面投标的更接近现 在的值,从而形成了这样的错觉 ,认为前面投标比当前投标的值要高。不难设想这将如何引起问题――这还不算,当应用在很大的负载下时,这样一个问题则更是暴露无遗。 使得消息类不可变并在构造时克隆像数组这样的可变引用,就可以防止该问题。

public interface MessagingEvent { ... } public class CurrentBidEvent implements MessagingEvent {   public final int currentBid;   public final int[] previous5Bids;   public CurrentBidEvent(int currentBid, int[] previousBids) {
this.currentBid = currentBid; // Danger -- copying array reference instead of values this.previous5Bids = previous5Bids; } ... } // Now, somewhere in the bid-processing code, we create a // CurrentBidEvent and publish it. public void newBid(int newBid) { if (newBid > currentBid) { for (int i=1; i<5; i++) previous5Bids[i] = previous5Bids[i-1]; previous5Bids[0] = currentBid; currentBid = newBid; messagingTopic.publish(new CurrentBidEvent(currentBid, previousBids)); } } }
blue_rule.gif
c.gif
c.gif
u_bold.gif

如果您要创建一个可变类 M,那么您应该准备编写比 M 是不可变的情况下多得多的文档说明,以说明怎样处理 M 的引用。 首先,您必须选择以 M 为参数或返回 M 对象的方法是使用值语义还是引用语义,并准备在每一个在其接口内使用 M 的其他类中清晰地文档说明这一点 。如果接受或返回 M 对象的任何方法隐式地假设 M 的所有权被转移,那么您必须也文档说明这一点。您还要准备着接受在必要时创建防御副本的性能开销。

一 个必须处理对象所有权问题的特殊情况是数组,因为数组不可以是不可变的。当传递一个数组引用到另一个类时,可能有创建防御副本的代价,除非您能确保其他类 要么创建了它自己的副本,要么只在调用期间保存引用,否则您可能需要在传递数组之前创建副本。另外,您可以容易地结束这样一种情形,即调用的两边的类都隐 式地假设它们拥有数组,只是这样会有不可预知的结果出现。

blue_rule.gif
c.gif
c.gif
u_bold.gif

处 理可变的类比处理不可变的类需要更细心。当在方法之间传递对可变对象的引用时,您需要清楚地文档说明哪些情况下对象的所有权被转移。而缺乏清楚的文档说明 时,您必须在方法调用的两边都创建防御副本。认为可变性更合理是基于性能方面的考虑,因为不需要在每次状态改变时都创建一个新对象,然而,由防御性复制所 招致的性能代价能轻而易举地抵消掉因为减少了对象创建而节省下来的性能。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-130161/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/374079/viewspace-130161/

你可能感兴趣的文章
dropbox文件_Dropbox即将发布的扩展程序更新将添加更多文件编辑支持,包括Pixlr照片...
查看>>
google hdr+_更好的隐私权控制使Google+死了
查看>>
网络串流_串流NBA篮球的最便宜方式(无需电缆)
查看>>
reddit_如何将多个子Reddit与多个Reddit合并
查看>>
如何在iPhone或iPad上使用Safari下载文件
查看>>
kindle导出电子书pc_使用Kindle for PC在计算机上阅读Kindle电子书
查看>>
互联网应急处理方案_什么是互联网巨魔? (以及如何处理巨魔)
查看>>
chrome 默认隐身_将隐身模式上司按钮添加到Google Chrome
查看>>
java 内置chrome_如何使用Chrome的内置任务管理器
查看>>
如何在Excel中创建组合图
查看>>
在spoon作业中并发运行_使用Spoon在Windows 7中运行IE6和其他旧应用
查看>>
qca 指定频道 扫描_如何扫描(或重新扫描)电视上的频道
查看>>
不到运行当前操作系统的Android用户的0.4%
查看>>
如何从Linux Shell创建和安装SSH密钥
查看>>
如何快速将多个IP地址添加到Windows服务器
查看>>
哈夫曼会话加密_您是否正在使用带有加密会话的Facebook?
查看>>
chromebook刷机_如何关闭无响应的Chromebook应用
查看>>
贴片led发光电流_发光的国际象棋套装结合了LED,国际象棋和DIY电子产品的乐趣...
查看>>
如何在PowerPoint中使用变形过渡
查看>>
plex 乱码_Plex DVR现在提供传统的网格视图
查看>>