Java语言如何更好地支持组成和委托
本文概述了一种改进Java语言以更好地支持组合和委托的方法。参与讨论,为Java语言的发展做出贡献。
Java语言缺乏对组合和委托的明确语义支持。这使得委托类难以编写,容易出错,难以阅读和维护。例如,委托一个JDBC ResultSet接口需要编写190多个委托方法,这些方法基本上不提供任何额外的信息,正如本文末尾所说明的,只是增加了仪式。
更一般地说,在组合的情况下,需要编写Σ m(i)个委托方法,其中m(i)是委托i的方法数量(前提是所有委托方法的签名在所有委托中都是不连贯的)。
语言支持委托的概念并不新鲜,关于这个问题有很多文章,包括[Bettini08]和[Kabanov11]。许多其他编程语言,如Kotlin("Derived")和Scala("export")都有对委托的语言支持。
在我之前的一篇文章《为什么一般继承是有缺陷的以及如何最终修复它》中,我描述了为什么组合和委托如此重要。
外部工具
许多IDE都支持生成委托方法。然而,这既不影响阅读也不影响理解一个委托类的能力。研究表明,代码的阅读量一般要比书写量大。有一些第三方库提供了委托功能(例如Lombok),但这些库是非标准的,并提供了一些其他的缺点。
更普遍的是,在第三方库中利用注释处理器和/或动态代理来实现这里提出的功能的一个子集是可能的。
趋势和工业标准
随着人们对继承的缺点有了更深入的了解,趋势是转向组合。随着模块系统和普遍更严格的封装策略的出现,对Java语言中语义委托支持的需求更加强烈了。
我认为这是一个最好在语言本身而不是通过各种第三方库提供的功能。委托是当代编码的一个基石。
从本质上讲,正如Joshua Bloch的《Effective Java》一书中所说的那样,"有利于组合而不是继承 "应该更容易一些[Bloch18, Item 18]。
Java记录类
在Java 14中引入记录类之前,上面指出的许多问题对于数据类也是如此。经过更彻底的分析,可能会有很大的机会来收获记录开发过程中的许多发现,并将这些发现应用于委托和组合领域。
关于建议
我写这篇文章的目的并不是要提出一个具体的建议,说明如何在Java中引入对组合和委托的语义支持。相反,如果这个提议是在Java语言中提出真正的功能之前需要穿越的道路上经常被丢弃的10-15个不同的初始提议和草图之一,那么它将是一个巨大的成功。在实现对Java中的组成和委托的语义支持的道路上,很可能会铺设一些研究论文、若干设计提案、孵化等。这项功能还将与其他功能竞争,这些功能有可能被认为对整个Java生态系统更为重要。
记录的一个座右铭是 "将数据作为数据来建模",我认为我们也应该 "将委托作为委托来建模"。 但什么是委托?在社区内可能有不同的看法。
当我想到委托时,我想到的是以下内容。一个委托类有以下属性:
- 拥有一个或多个委托人
- 从它的委托者那里委托出方法
- 完全封装其委派对象
- 实现和/或使用来自其代表的方法(可以说是)。
大纲--"使者"(The Emissary
在下文中,我将提出一个纲要来解决这个问题。为了简化讨论,我将引入一个名为 "使者 "的新关键词,这个词在实际实现中不太可能被使用。这个词以后可以用 "委托人 "或其他适合这个目的的描述性词来代替,甚至可以用现有的关键词来代替。
一个使者类与一个记录类有很多相似之处,可以像下面的例子一样使用:
public emissary Bazz(Foo foo, Bar bar);
可以看出,Bazz类有两个委托(Foo和Bar),因此创建了一个有两个私有最终字段的等价的解构类:
private final Foo foo;
private final Bar bar;
一个使者类也被提供了一个构造函数。这个过程可能与带有典型构造函数和紧凑构造函数的记录相同:
public final class Bazz {
private final Foo foo;
private final Bar bar;
public Bazz(Foo foo, Bar bar) {
this.foo = foo;
this.bar = bar;
}
}
这也使得使者类实现了Foo和Bar。正因为如此,Foo和Bar必须是接口,而不是抽象或具体的类。(在当前提案的一个变体中,可以明确地声明实现接口):
public final class Bazz implements Foo, Bar {
private final Foo foo;
private final Bar bar;
public Bazz(Foo foo, Bar bar) {
this.foo = foo;
this.bar = bar;
}
}
现在,为了继续讨论,我们需要对Foo和Bar这两个实例类进行更多的描述,下面就是这样做的:
public interface Foo() {
void f();
}
public interface Bar() {
void b();
}
通过声明一个使者类,我们不出所料地也得到了实际的委托方法,这样Bazz就会实际实现其接口Foo和Bar:
public final class Bazz implements Foo, Bar {
private final Foo foo;
private final Bar bar;
public Bazz(Foo foo, Bar bar) {
this. Foo = foo;
this.bar = bar;
}
@Override
void f() {
foo.f();
}
@Override
void b() {
bar.b();
}
}
如果委托类包含相同签名的方法,这些方法就必须被明确地 "去歧义化",例如,与接口中的默认方法相同。因此,如果Foo和Bar都实现了c(),那么Bazz需要明确声明c()以提供调和。这里显示了一个例子,两个委托都被调用了:
@Override
void c() {
foo.c();
bar.c();
}
没有什么能阻止我们通过手工添加额外的方法,例如,实现emissary类明确实现的额外接口,但这些接口没有被任何一个委托所覆盖。
同样值得注意的是,建议的emissary类不应该产生hashCode() 、equals() 或toString() 方法。如果他们这样做了,他们就会违反属性C,并泄露其委托的信息。出于同样的原因,emissary类也不应该有去构造器,因为这直截了当地会破坏封装。委托类不应该默认实现Serializable和类似的东西。
一个emissary,就像一个record ,是不可变的(或者至少是不可修改的,因此是浅层不可变的),因此如果所有的委托都是线程安全的。
最后,一个使者类将扩展java.lang.Emissary ,这是一个新提议的抽象类,类似于java.lang.Enum 和java.lang.Record.
记录与使者的比较
对比现有的记录类和提议的使者类,可以发现一些有趣的事实。
记录
- 提供了一个生成的hashCode()方法
- 提供了一个生成的 equals() 方法
- 提供了一个生成的toString()方法
- 提供了组件获取器
- 不能声明除私有最终字段以外的实例字段,这些字段对应于状态描述的组件
执政官
- 不提供生成的hashCode()方法
- 不提供生成的equals()方法
- 不提供生成的toString()方法
- 提供委托方法
- 实施委托(在一个变体中)。
- 除了与委托相对应的私有最终字段外,可以声明额外的最终实例字段
既有
- 为状态描述的每个组件/委托提供一个私有最终字段
- 一个公共构造函数,其签名与状态/委托描述相同,它从相应的参数初始化每个字段;(典型构造函数和紧凑构造函数)
- 放弃了将API与表示法解耦的能力
- 隐含的最终的,不能是抽象的(确保不可更改性)
- 不能扩展任何其他类(确保不可更改性)
- 扩展Object以外的java.lang类。
- 可以声明未被属性/代理覆盖的额外方法
预期的使用案例
以下是使者类的一些用例。
组成
使用组合方式为一个或多个接口提供实现。
public emissary FooAndBar(Foo foo, Bar bar);
封装
封装一个类的现有实例,隐藏实际实现的细节:
private emissary EncapsulatedResultSet(ResultSet resultSet);
…
ResultSet rs = stmt.executeQuery(query);
return new EncapsulatedResultSet(rs);
不允许下嫁
不允许对一个实例进行下嫁。也就是说,一个使者类实现了它的委托人方法的一个受限子集,其中未暴露的方法不能通过铸造或反射来调用。
String实现CharSequence,在下面的例子中,我们提供了一个String,被视为CharSequence,因此我们不能将CharSequence 包装器下移为一个字符串:
private emissary AsCharSequence(CharSequence s);
return new AsCharSequence(“I am a bit incognito.”);
服务和组件
提供一个有内部实现的接口的实现。内部组件包通常不在模块信息文件中导出:
public emissary MyComponent(MyComponent comp) {
public MyComponent() {
this(new InternalMyComponentImpl());
}
// Optionally, we may want to hide the public
// constructor
private MyComponent(MyComponent comp) {
this.comp = comp;
}
}
MyComponent myComp = ServiceLoader.load(MyComponent.class)
.iterator()
.next();
注意:如果InternalMyComponentImpl是由一个内部基类组成的,包含注释,有非公开的方法,有字段等。这些将被完全隐藏起来,不被emissary类通过反射直接发现,在JPMS下,它将被完全保护,不被深度反射。
比较两个结果集委托者
比较两个委托结果集的类。
Emissary类
// Using an emissary class. A one-liner
public emissary EncapsulatedResultSet(ResultSet resultSet);
IDE生成
// Using automatic IDE delegation. About 1,000 lines!
public final class EncapsulatedResultSet implements ResultSet {
private final ResultSet delegate;
public EncapsulatedResultSet(ResultSet delegate) {
this.delegate = delegate;
}
@Override
public boolean next() throws SQLException {
return delegate.next();
}
// About 1000 additional lines are not shown here for brevity…
结论
我们可以在概念上重用record类,以便在Java语言中提供语义组合和委托支持。这将大大减少这类构造所需的语言仪式,并且很有可能促使开发者使用组合,就像record类促使开发者使用不可变性一样。
关于组合和委托的科学领域,以及与之相关的内容比本文所指出的要大得多。在得出一个具体的建议之前,还需要进一步的研究。也许这只是更大的事物的一部分?
在我看来,对组合和委托的某种形式的语言支持会使Java成为一种更好的语言。