PHP V5 新的面向对象编程特性显著提升了这个流行语言中的功能层次。学习如何用 PHP V5 动态特性创建可以满足需求的对象。
PHP V5 中新的面向对象编程(OOP)特性的引入显著提升了这个编程语言的功能层次。现在不仅有了私有的、受保护的和公共的成员变量和函数 —— 就像在 Java™、 C++ 或 C# 编程语言中一样 —— 但是还可以创建在运行时变化的对象,即动态地创建新方法和成员变量。而使用 Java、C++ 或 C# 语言是做不到这件事的。这种功能使得超级快速的应用程序开发系统(例如 Ruby on Rails)成为可能。
但是,在进入这些之前,有一点要注意:本文介绍 PHP V5 中非常高级的 OOP 特性的使用,但是这类特性不是在每个应用程序中都需要的。而且,如果不具备 OOP 的坚实基础以及 PHP 对象语法的初步知识,这类特性将会很难理解。
动态的重要性
对象是把双刃剑。一方面,对象是封装数据和逻辑并创建更容易维护的系统的重大方式。但另一方面,它们会变得很繁琐,需要许多冗余的代码,这时可能最希望做到的就是不要犯错。这类问题的一个示例来自数据库访问对象。一般来说,想用一个类代表每个数据库表,并执行以下功能:对象从数据库读出数据行;允许更新字段,然后用新数据更新数据库或删除行。还有一种方法可以创建新的空对象,设置对象的字段,并把数据插入数据库。
如果在数据库中有一个表,名为 Customers,那么就应当有一个对象,名为 Customer
,它应当拥有来自表的字段,并代表一个客户。而且 Customer
对象应当允许插入、更新或删除数据库中对应的记录。现在,一切都很好,而且有也很多意义。但是,有许多代码要编写。如果在数据库中有 20 个表,就需要 20 个类。
有三个解决方案可以采用。第一个解决方案就是,坐在键盘前,老老实实地录入一段时间。对于小项目来说,这还可以,但是我很懒。第二个解决方案是用代码生成器,读取数据库模式,并自动编写代码。这是个好主意,而且是另一篇文章的主题。第三个解决方案,也是我在本文中介绍的,是编写一个类,在运行时动态地把自己塑造成指定表的字段。这个类执行起来比起特定于表的类可能有点慢 —— 但是把我从编写大量代码中解脱出来。这个解决方案在项目开始的时候特别有用,因为这时表和字段不断地变化,所以跟上迅速的变化是至关重要的。
所以,如何才能编写一个能够弯曲 的类呢?
写一个柔性的类
对象有两个方面:成员变量 和方法。在编译语言(例如 Java)中,如果想调用不存在的方法或引用不存在的成员变量,会得到编译时错误。但是,在非编译语言,例如 PHP 中,会发生什么?
在 PHP 中的方法调用是这样工作的。首先,PHP 解释器在类上查找方法。如果方法存在,PHP 就调用它。如果没有,那么就调用类上的魔法方法 __call
(如果这个方法存在的话)。如果 __call
失败,就调用父类方法,依此类推。
|
魔法方法
魔法方法是有特定名称的方法,PHP 解释器在脚本执行的特定点上会查找魔法方法。最常见的魔法方法就是对象创始时调用的构造函数。
|
|
__call
方法有两个参数:被请求的方法的名称和方法参数。如果创建的 __call
方法接受这两个参数,执行某项功能,然后返回 TRUE,那么调用这个对象的代码就永远不会知道在有代码的方法和 __call
机制处理的方法之间的区别。通过这种方式,可以创建这样的对象,即动态地模拟拥有无数方法的情况。
除了 __call
方法,其他魔法方法 —— 包括 __get
和 __set
—— 调用它们的时候,都是因为引用了不存在的实例变量。脑子里有了这个概念之后,就可以开始编写能够适应任何表的动态数据库访问类了。
经典的数据库访问
先从一个简单的数据库模式开始。清单 1 所示的模式针对的是单一的数据表数据库,容纳图书列表。
清单 1. MySQL 数据库模式
DROP TABLE IF EXISTS book;
CREATE TABLE book (
book_id INT NOT NULL AUTO_INCREMENT,
title TEXT,
publisher TEXT,
author TEXT,
PRIMARY KEY( book_id )
);
|
请把这个模式装入到名为 bookdb 的数据库。
接下来,编写一个常规的数据库类,然后再把它修改成动态的。清单 2 显示了图书表的简单的数据库访问类。
清单 2. 基本的数据库访问客户机
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class Book
{
private $book_id;
private $title;
private $author;
private $publisher;
function __construct()
{
}
function set_title( $title ) { $this->title = $title; }
function get_title( ) { return $this->title; }
function set_author( $author ) { $this->author = $author; }
function get_author( ) { return $this->author; }
function set_publisher( $publisher ) {
$this->publisher = $publisher; }
function get_publisher( ) { return $this->publisher; }
function load( $id )
{
global $db;
$res = $db->query( "SELECT * FROM book WHERE book_id=?",
array( $id ) );
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->book_id = $id;
$this->title = $row['title'];
$this->author = $row['author'];
$this->publisher = $row['publisher'];
}
function insert()
{
global $db;
$sth = $db->prepare(
'INSERT INTO book ( book_id, title, author, publisher )
VALUES ( 0, ?, ?, ? )'
);
$db->execute( $sth,
array( $this->title,
$this->author,
$this->publisher ) );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
return $row[0];
}
function update()
{
global $db;
$sth = $db->prepare(
'UPDATE book SET title=?, author=?, publisher=?
WHERE book_id=?'
);
$db->execute( $sth,
array( $this->title,
$this->author,
$this->publisher,
$this->book_id ) );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM book WHERE book_id=?'
);
$db->execute( $sth,
array( $this->book_id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM book' );
$db->execute( $sth );
}
}
$book = new Book();
$book->delete_all();
$book->set_title( "PHP Hacks" );
$book->set_author( "Jack Herrington" );
$book->set_publisher( "O'Reilly" );
$id = $book->insert();
echo ( "New book id = $id\n" );
$book2 = new Book();
$book2->load( $id );
echo( "Title = ".$book2->get_title()."\n" );
$book2->delete( );
?>
|
为了保持代码简单,我把类和测试代码放在一个文件中。文件首先得到数据库句柄,句柄保存在一个全局变量中。然后定义 Book
类,用私有成员变量代表每个字段。还包含了一套用来从数据库装入、插入、更新和删除行的方法。
底部的测试代码先删除数据库中的所有条目。然后,代码插入一本书,输出新记录的 ID。然后,代码把这本书装入另一个对象并输出书名。
清单 3 显示了在命令行上用 PHP 解释器运行代码的效果。
清单 3. 在命令行运行代码
% php db1.php
New book id = 25
Title = PHP Hacks
%
|
不需要看太多,就已经得到重点了。Book
对象代表图书数据表中的行。通过使用上面的字段和方法,可以创建新行、更新行和删除行。
初识动态
下一步是让类变得稍微动态一些:动态地为每个字段创建 get_
和 set_
方法。清单 4 显示了更新后的代码。
清单 4. 动态 get_ 和 set_ 方法
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class Book
{
private $book_id;
private $fields = array();
function __construct()
{
$this->fields[ 'title' ] = null;
$this->fields[ 'author' ] = null;
$this->fields[ 'publisher' ] = null;
}
function __call( $method, $args )
{
if ( preg_match( "/set_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
$this->fields[ $found[1] ] = $args[0];
return true;
}
}
else if ( preg_match( "/get_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
return $this->fields[ $found[1] ];
}
}
return false;
}
function load( $id )
{
global $db;
$res = $db->query( "SELECT * FROM book WHERE book_id=?",
array( $id ) );
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->book_id = $id;
$this->set_title( $row['title'] );
$this->set_author( $row['author'] );
$this->set_publisher( $row['publisher'] );
}
function insert()
{
global $db;
$sth = $db->prepare(
'INSERT INTO book ( book_id, title, author, publisher )
VALUES ( 0, ?, ?, ? )'
);
$db->execute( $sth,
array( $this->get_title(),
$this->get_author(),
$this->get_publisher() ) );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
return $row[0];
}
function update()
{
global $db;
$sth = $db->prepare(
'UPDATE book SET title=?, author=?, publisher=?
WHERE book_id=?'
);
$db->execute( $sth,
array( $this->get_title(),
$this->get_author(),
$this->get_publisher(),
$this->book_id ) );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM book WHERE book_id=?'
);
$db->execute( $sth,
array( $this->book_id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM book' );
$db->execute( $sth );
}
}
..
|
要做这个变化,需要做两件事。首先,必须把字段从单个实例变量修改成字段和值组合构成的散列表。然后必须添加一个 __call
方法,它只查看方法名称,看方法是 set_
还是 get_
方法,然后在散列表中设置适当的字段。
注意,load
方法通过调用 set_title
、set_author
和 set_publisher
方法 —— 实际上都不存在 —— 来实际使用 __call
方法。
走向完全动态
删除 get_
和 set_
方法只是一个起点。要创建完全动态的数据库对象,必须向类提供表和字段的名称,还不能有硬编码的引用。清单 5 显示了这个变化。
清单 5. 完全动态的数据库对象类
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class DBObject
{
private $id = 0;
private $table;
private $fields = array();
function __construct( $table, $fields )
{
$this->table = $table;
foreach( $fields as $key )
$this->fields[ $key ] = null;
}
function __call( $method, $args )
{
if ( preg_match( "/set_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
$this->fields[ $found[1] ] = $args[0];
return true;
}
}
else if ( preg_match( "/get_(.*)/", $method, $found ) )
{
if ( array_key_exists( $found[1], $this->fields ) )
{
return $this->fields[ $found[1] ];
}
}
return false;
}
function load( $id )
{
global $db;
$res = $db->query(
"SELECT * FROM ".$this->table." WHERE ".
$this->table."_id=?",
array( $id )
);
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->id = $id;
foreach( array_keys( $row ) as $key )
$this->fields[ $key ] = $row[ $key ];
}
function insert()
{
global $db;
$fields = $this->table."_id, ";
$fields .= join( ", ", array_keys( $this->fields ) );
$inspoints = array( "0" );
foreach( array_keys( $this->fields ) as $field )
$inspoints []= "?";
$inspt = join( ", ", $inspoints );
$sql = "INSERT INTO ".$this->table." ( $fields )
VALUES ( $inspt )";
$values = array();
foreach( array_keys( $this->fields ) as $field )
$values []= $this->fields[ $field ];
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
$this->id = $row[0];
return $row[0];
}
function update()
{
global $db;
$sets = array();
$values = array();
foreach( array_keys( $this->fields ) as $field )
{
$sets []= $field.'=?';
$values []= $this->fields[ $field ];
}
$set = join( ", ", $sets );
$values []= $this->id;
$sql = 'UPDATE '.$this->table.' SET '.$set.
' WHERE '.$this->table.'_id=?';
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM '.$this->table.' WHERE '.
$this->table.'_id=?'
);
$db->execute( $sth,
array( $this->id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM '.$this->table );
$db->execute( $sth );
}
}
$book = new DBObject( 'book', array( 'author',
'title', 'publisher' ) );
$book->delete_all();
$book->set_title( "PHP Hacks" );
$book->set_author( "Jack Herrington" );
$book->set_publisher( "O'Reilly" );
$id = $book->insert();
echo ( "New book id = $id\n" );
$book->set_title( "Podcasting Hacks" );
$book->update();
$book2 = new DBObject( 'book', array( 'author',
'title', 'publisher' ) );
$book2->load( $id );
echo( "Title = ".$book2->get_title()."\n" );
$book2->delete( );
? >
|
在这里,把类的名称从 Book
改成 DBObject
。然后,把构造函数修改成接受表的名称和表中字段的名称。之后,大多数变化发生在类的方法中,过去使用一些硬编码结构化查询语言(SQL),现在则必须用表和字段的名称动态地创建 SQL 字符串。
代码的惟一假设就是只有一个主键字段,而且这个字段的名称是表名加上 _id
。所以,在 book
表这个示例中,有一个主键字段叫做 book_id
。主键的命名标准可能不同;如果这样,需要修改代码以符合标准。
这个类比最初的 Book
类复杂得多。但是,从类的客户的角度来看,这个类用起来仍很简单。也就是说,我认为这个类能更简单。具体来说,我不愿意每次创建图书的时候都要指定表和字段的名称。如果我四处拷贝和粘贴这个代码,然后修改了 book 表的字段结构,那么我可能就麻烦了。在清单 6 中,通过创建一个继承自 DBObject
的简单 Book
类,我解决了这个问题。
清单 6. 新的 Book 类
..
class Book extends DBObject
{
function __construct()
{
parent::__construct( 'book',
array( 'author', 'title', 'publisher' ) );
}
}
$book = new Book( );
$book->delete_all();
$book->{'title'} = "PHP Hacks";
$book->{'author'} = "Jack Herrington";
$book->{'publisher'} = "O'Reilly";
$id = $book->insert();
echo ( "New book id = $id\n" );
$book->{'title'} = "Podcasting Hacks";
$book->update();
$book2 = new Book( );
$book2->load( $id );
echo( "Title = ".$book2->{'title'}."\n" );
$book2->delete( );
?>
|
现在,Book
类真的是简单了。而且 Book
类的客户也不再需要知道表或字段的名称了。
改进的空间
对这个动态类我想做的最后一个改进,是用成员变量访问字段,而不是用笨重的 get_
和 set_
操作符。清单 7 显示了如何用 __get
和 __set
魔法方法代替 __call
。
清单 7. 使用 __get 和 __set 方法
<?php
require_once("DB.php");
$dsn = 'mysql://root:password@localhost/bookdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
class DBObject
{
private $id = 0;
private $table;
private $fields = array();
function __construct( $table, $fields )
{
$this->table = $table;
foreach( $fields as $key )
$this->fields[ $key ] = null;
}
function __get( $key )
{
return $this->fields[ $key ];
}
function __set( $key, $value )
{
if ( array_key_exists( $key, $this->fields ) )
{
$this->fields[ $key ] = $value;
return true;
}
return false;
}
function load( $id )
{
global $db;
$res = $db->query(
"SELECT * FROM ".$this->table." WHERE ".
$this->table."_id=?",
array( $id )
);
$res->fetchInto( $row, DB_FETCHMODE_ASSOC );
$this->id = $id;
foreach( array_keys( $row ) as $key )
$this->fields[ $key ] = $row[ $key ];
}
function insert()
{
global $db;
$fields = $this->table."_id, ";
$fields .= join( ", ", array_keys( $this->fields ) );
$inspoints = array( "0" );
foreach( array_keys( $this->fields ) as $field )
$inspoints []= "?";
$inspt = join( ", ", $inspoints );
$sql = "INSERT INTO ".$this->table.
" ( $fields ) VALUES ( $inspt )";
$values = array();
foreach( array_keys( $this->fields ) as $field )
$values []= $this->fields[ $field ];
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
$res = $db->query( "SELECT last_insert_id()" );
$res->fetchInto( $row );
$this->id = $row[0];
return $row[0];
}
function update()
{
global $db;
$sets = array();
$values = array();
foreach( array_keys( $this->fields ) as $field )
{
$sets []= $field.'=?';
$values []= $this->fields[ $field ];
}
$set = join( ", ", $sets );
$values []= $this->id;
$sql = 'UPDATE '.$this->table.' SET '.$set.
' WHERE '.$this->table.'_id=?';
$sth = $db->prepare( $sql );
$db->execute( $sth, $values );
}
function delete()
{
global $db;
$sth = $db->prepare(
'DELETE FROM '.$this->table.' WHERE '.
$this->table.'_id=?'
);
$db->execute( $sth,
array( $this->id ) );
}
function delete_all()
{
global $db;
$sth = $db->prepare( 'DELETE FROM '.$this->table );
$db->execute( $sth );
}
}
class Book extends DBObject
{
function __construct()
{
parent::__construct( 'book',
array( 'author', 'title', 'publisher' ) );
}
}
$book = new Book( );
$book->delete_all();
$book->{'title'} = "PHP Hacks";
$book->{'author'} = "Jack Herrington";
$book->{'publisher'} = "O'Reilly";
$id = $book->insert();
echo ( "New book id = $id\n" );
$book->{'title'} = "Podcasting Hacks";
$book->update();
$book2 = new Book( );
$book2->load( $id );
echo( "Title = ".$book2->{'title'}."\n" );
$book2->delete( );
?>
|
底部的测试代码只演示了这个语法干净了多少。要得到图书的书名,只需得到 title
成员变量。这个变量会调用对象的 __get
方法,在散列表中查找 title
条目并返回。
现在就得到了单个动态的数据库访问类,它能够让自己适应到数据库中的任何表。
动态类的更多用途
编写动态类不仅限于数据库访问。请看清单 8 中的 Customer
对象这个例子。
清单 8. 简单的 Customer 对象
<?php
class Customer
{
private $name;
function set_name( $value )
{
$this->name = $value;
}
function get_name()
{
return $this->name;
}
}
$c1 = new Customer();
$c1->set_name( "Jack" );
$name = $c1->get_name();
echo( "name = $name\n" );
?>
|
这个对象足够简单。但是如果我想在每次检索或设置客户名称时都记录日志,会发生什么呢?我可以把这个对象包装在一个动态日志对象内,这个对象看起来像 Customer
对象,但是会把 get
或 set
操作的通知发送给日志。清单 9 显示了这类包装器对象。
清单 9. 动态包装器对象
<?php
class Customer
{
private $name;
function set_name( $value )
{
$this->name = $value;
}
function get_name()
{
return $this->name;
}
}
class Logged
{
private $obj;
function __call( $method, $args )
{
echo( "$method( ".join( ",", $args )." )\n" );
return call_user_func_array(array(&$this->obj,
$method), $args );
}
function __construct( $obj )
{
$this->obj = $obj;
}
}
$c1 = new Logged( new Customer() );
$c1->set_name( "Jack" );
$name = $c1->get_name();
echo( "name = $name\n" );
?>
|
调用日志版本的 Customer
的代码看起来与前面相同,但是这时,对 Customer
对象的任何访问都被记入日志。清单 10 显示了运行这个日志版代码时输出的日志。
清单 10. 运行日志版对象
% php log2.php
set_name( Jack )
get_name( )
name = Jack
%
|
在这里,日志输出表明用参数 Jack
调用了set_name
方法。然后,调用 get_name
方法。最后,测试代码输出 get_name
调用的结果。
结束语
如果这个动态对象素材对您来说理解起来有点难,我不会责备您。因为我自己也花了不少时间研究它并使用代码才理解它并看出它的好处。
动态对象有许多功能,但是也有相当的风险。首先,在刚开始编写魔法方法时,类的复杂性显著增加。这些类更难理解、调试和维护。另外,因为集成开发环境(IDE)变得越来越智能,所以在处理动态类时它们也会遇到这类问题,因为当它们在类上查找方法时会找不到方法。
现在,并不是说应当避免编写这类代码。相反。我非常喜欢 PHP 的设计者这么有想法,把这些魔法方法包含在语言中,这样我们才能编写这类代码。但是重要的是,既要理解优点,也要理解不足。
当然,对于应用程序(例如数据库访问)来说,在这里介绍的技术 —— 与广泛流行的 Ruby on Rails 系统上使用的技术类似 —— 能够极大地减少用 PHP 实现数据库应用程序所需要的时间。节约时间总不是坏事。
参考资料
学习
年轻人买套套的经历
顾客“老板套子怎么卖?”
老板“10元一个”
顾客“我先试试行么?”
老板“还试什么呀?便宜点9元好了。”
顾客“晕,这也叫便宜啊?”
老板“好啦8元可以了吧。”
顾客“......”
老板“不会还嫌贵吧?”
顾客“不是贵,是让我精尽人亡啊”
老板“没那么夸张吧,看你是小朋友,7元好了”
顾客“恩,差不多啦,可是我没那么多钱啊。”
老板“啊?你有多少啊?”
顾客“5元。”
老板“天啊,晕死了,怎么也得再添1元啊!”
顾客“我很想添,可是我的资金有限啊”
老板“好啦,我认倒霉,5元成交”
顾客“我不是要给你5元,我得留下2元做车”
老板“不会吧,你不会做车来这里买这东西吧?”
顾客“是啊我做11路来的而且还是回头客啊”
老板“虽然以前没看过你,不过希望你以后再来,3元成交行了吧。”
顾客(脸上带红)“可是我还没有对象”
老板“啊?那你买这干什么啊?”
顾客“没关系,如果你再便宜1元的话我就能找到了!”
老板“¥%……¥%……%¥算你狠,2元行了吧!”
顾客“等等,这个怎么有个洞啊?”
老板“没有洞怎么用啊!?”
顾客“怎么看起来像用过的啊?”
老板“侮辱我可以但是不要侮辱我的套子,这绝对是新的。”
顾客“哇,上面还没干呢啊,你骗我啊!”
老板“啊!!不好意思嘿嘿……做生意吗,你要知道我每天的门面房租金上千呢,不然我吃什么,1元可以了吧”
顾客“你这种行为严重的危害了我的健康并且深深的影响了我的心灵.......”
老板“啊呀!这么严重啊,你别生气,我5毛卖给你行了吧。”
顾客“好,开张发票来!”
老板晕死!
十二个避孕套
爸爸和13岁的儿子走进屈臣氏,路经放避孕套的货架。儿子问爸爸「这些一盒盒的是什
么?爸爸告诉儿子:「这些是避孕套,是用来进行安全的性行为用的。儿子U「啊~原来这些
便是避孕套,上性教育课老师曾提及过!........[但为什么这些要一盒入里面有三个的?
爸爸:「嗯......这些是给大学生用的,星期五一个,星期六一个,星期日一........」
儿子:「.......那么这些一盒六个的是谁用的?」
爸爸:「嗯........这是研究生用的,星期五两个,星期六两个,星期日两个........」
儿子:「.......那这些呢?」
儿子拿起了一盒十二个装的。
爸爸透了一下凉气,凄凄道:「那是给已婚人仕用的,一月份一个,二月份一个,三月份一个............」
彩色的保险套
有一个人想尝试新奇的事,便跑到情趣商品店买彩色的保险套他看到两个彩色的套子,
一个是黑色的,一个外型像是米老鼠他决定买那个黑色的回家,并跟太太大战了几回合不过
那个套子并没发生什作用,后来他太太怀孕了经过九月之后生下小baby,再经过6年之后孩子
长大了这个小孩有一天问他老爸:“为什么哥哥姊姊的肤色都是白的而我却是黑的?”爸爸
回答道:“孩子,你没长得像米老鼠就该谢天谢地了”
IT避孕套
有一天软件工业一蹶不振,软件业三大巨头SUN,UNIX和微软都决定改做避孕套生意,
他们生产的避孕套分别命名为JAVA避孕套,X避孕套和MS避孕套。 一个使用JAVA避孕
套的顾客来到SUN公司投诉,说戴着不合适,SUN公司回答说要等国际标准组织(ISO)制定相
应的标准才行,并吹牛说那时他们生产的避孕套将适合每个男人,顾客只好转而使用X避孕
套。可他发现等他读完随套附上的说明书后,他的妻子已经睡着了,他自己也忘了为什么
要用X避孕套。最后,他只好换用MS避孕套。出乎他意料的是,MS避孕套非常好用,他很愉
快的连续使用了好几个月,突然发现他妻子怀孕了。他非常生气气势汹汹的找到微软公
司,微软的回答是:补丁马上就到!
Stripes 1.3 版本发布了.Stripes是一个视图框架用于利用最新的Java技术来构建Web应用程序.
Apache MyFaces 1.1.2 版本发布了.MyFaces是JavaServer Faces(JSF) Web框架 (JSR 127)的一个实现。JavaServer Faces Web框架是一个新的实现MVC模式的规范.它可以与Struts框架相媲美甚至的一些特性与观念已经超过了Struts.
Apple 发布了 Mac OS X Tiger 的更新包.for for Mac OS X 10.4.5 Tiger 用户.更新包增加了对Java 2 Platform Standard Edition 5.0的可靠性和兼容性.
Apache FOP 0.92 beta 版本发布了.FOP是Apache计划所发展的一个开源的XSL-FO处理器项目,可以把Formatting Object格式的文件转换成 可列印文件,如PDF、PostScript等格式。
ObjectWeb ASM 3.0 beta2 版本发布了.ASM是一套JAVA字节码生成架构。它可以动态生成二进制格式的stub类或其他代理类,或者在类被JAVA虚拟机装入内存之前,动态修改类。
Apache WS Policy 宣布改名为 Apache Neethi.