閱讀開源框架,遍覽Java嵌套類的用法

閱讀開源框架,遍覽Java嵌套類的用法

Java的類對外而言只有一種面貌,但封裝在類內部的形態卻可以豐富多彩。嵌套類在這其中,扮演了極為重要的角色。它既豐富了類的層次,又可以靈活控制內部結構的訪問限制與粒度,使得我們在開放性與封閉性之間、公開接口與內部實現之間取得適度的平衡。

嵌套類之所以能扮演這樣的設計平衡角色,是因為嵌套類與其主類的關係不同,主類的所有成員對嵌套類而言都是完全開放的,主類的私有成員可以被嵌套類訪問,而嵌套類則可以被看做是類邊界中自成一體的高內聚類而被主類調用。因此,嵌套類的定義其實就是對類內部成員的進一步封裝。雖然在一個類中定義嵌套類並不能減小類定義的規模,但由於嵌套類體現了不同層次的封裝,使得一個相對較大的主類可以顯得更有層次感,不至於因為成員過多而顯得過於混亂。

當一個類的業務邏輯非常複雜,且它承擔的職責卻又不足以單獨分離為另外的類型時,內部嵌套類尤其是靜態嵌套類就會變得非常有用。它是幫助我們組織類內部代碼的利器。通過閱讀頂級的Java開源項目,我們發現內部嵌套類通常用於幾種情況。

封裝Builder

Builder模式常常用於組裝一個類,它通過更加流暢的接口形式簡化構建組成元素的邏輯。在Java中,除非必要,我們一般會將一個類的Builder定義為內部嵌套類。這幾乎已經成為一種慣有模式了。

例如框架airlift定義了Request類,它是客戶端請求對象的一個封裝。組成一個Request對象需要諸如Uri、header、http verb、body等元素,且這些元素的組成會因為客戶請求的不同而調用不同的組裝方法。這是典型的Builder模式應用場景。讓我們看看在Presto框架下,它如何調用airlift提供的Request及對應的Builder:

@ThreadSafe

public class StatementClient implements Closeable {

private Request buildQueryRequest(ClientSession session, String query) {

Request.Builder builder = prepareRequest(preparePost(), uriBuilderFrom(session.getServer()).replacePath("/v1/statement").build())

.setBodyGenerator(createStaticBodyGenerator(query, UTF_8));

if (session.getSource() != null) {

builder.setHeader(PrestoHeaders.PRESTO_SOURCE, session.getSource());

}

if (session.getClientInfo() != null) {

builder.setHeader(PrestoHeaders.PRESTO_CLIENT_INFO, session.getClientInfo());

}

if (session.getCatalog() != null) {

builder.setHeader(PrestoHeaders.PRESTO_CATALOG, session.getCatalog());

}

if (session.getSchema() != null) {

builder.setHeader(PrestoHeaders.PRESTO_SCHEMA, session.getSchema());

}

builder.setHeader(PrestoHeaders.PRESTO_TIME_ZONE, session.getTimeZone().getId());

if (session.getLocale() != null) {

builder.setHeader(PrestoHeaders.PRESTO_LANGUAGE, session.getLocale().toLanguageTag());

}

Map property = session.getProperties();

for (Entry entry : property.entrySet()) {

builder.addHeader(PrestoHeaders.PRESTO_SESSION, entry.getKey() + "=" + entry.getValue());

}

Map statements = session.getPreparedStatements();

for (Entry entry : statements.entrySet()) {

builder.addHeader(PrestoHeaders.PRESTO_PREPARED_STATEMENT, urlEncode(entry.getKey()) + "=" + urlEncode(entry.getValue()));

}

builder.setHeader(PrestoHeaders.PRESTO_TRANSACTION_ID, session.getTransactionId() == null ? "NONE" : session.getTransactionId());

return builder.build();

}

private Request.Builder prepareRequest(Request.Builder builder, URI nextUri) {

builder.setHeader(PrestoHeaders.PRESTO_USER, user);

builder.setHeader(USER_AGENT, USER_AGENT_VALUE)

.setUri(nextUri);

return builder;

}

}

顯然,StatementClient會根據ClientSession對象的不同情況,構造不同的Request。由於引入了Builder模式,則這種構造Request對象的職責到Request.Builder對象。觀察該對象的使用:

Request.Builder builder = prepareRequest(...);

Builder類被定義為Request的靜態嵌套類,以下為airlift框架的源代碼:

public class Request {

public static class Builder {

private URI uri;

private String method;

private final ListMultimap headers = ArrayListMultimap.create();

private BodyGenerator bodyGenerator;

public Builder() {

}

public static Request.Builder prepareHead() {

return (new Request.Builder()).setMethod("HEAD");

}

public static Request.Builder prepareGet() {

return (new Request.Builder()).setMethod("GET");

}

public static Request.Builder preparePost() {

return (new Request.Builder()).setMethod("POST");

}

public static Request.Builder preparePut() {

return (new Request.Builder()).setMethod("PUT");

}

public static Request.Builder prepareDelete() {

return (new Request.Builder()).setMethod("DELETE");

}

public static Request.Builder fromRequest(Request request) {

Request.Builder requestBuilder = new Request.Builder();

requestBuilder.setMethod(request.getMethod());

requestBuilder.setBodyGenerator(request.getBodyGenerator());

requestBuilder.setUri(request.getUri());

Iterator var2 = request.getHeaders().entries().iterator();

while (var2.hasNext()) {

Entry entry = (Entry)var2.next();

requestBuilder.addHeader((String)entry.getKey(), (String)entry.getValue());

}

return requestBuilder;

}

public Request.Builder setUri(URI uri) {

this.uri = Request.validateUri(uri);

return this;

}

public Request.Builder setMethod(String method) {

this.method = method;

return this;

}

public Request.Builder setHeader(String name, String value) {

this.headers.removeAll(name);

this.headers.put(name, value);

return this;

}

public Request.Builder addHeader(String name, String value) {

this.headers.put(name, value);

return this;

}

public Request.Builder setBodyGenerator(BodyGenerator bodyGenerator) {

this.bodyGenerator = bodyGenerator;

return this;

}

public Request build() {

return new Request(this.uri, this.method, this.headers, this.bodyGenerator);

}

}

}

當一個類的構建邏輯非常複雜,並有多種不同的組合可能性時,我們會傾向於使用Builder模式來封裝這種變化的組裝邏輯。慣用的模式是將Builder類定義為公有靜態嵌套類,很多Java實踐證實了這一做法。

封裝Iterator

當我們需要在類中提供自定義的迭代器,且又不需要將該迭代器的實現對外公開時,都可以通過嵌套類實現一個內部迭代器。這是迭代器模式的一種慣用法。例如在Presto框架中,定義了屬於自己的PrestoResultSet類,繼承自JDBC的ResultSet,並重寫了父類的迭代方法next()。next方法的迭代職責又委派給了一個內部迭代器results:

閱讀開源框架,遍覽Java嵌套類的用法

而這個results實則是一個內部迭代器:

public class PrestoResultSet implements ResultSet {

private static class ResultsPageIterator

extends AbstractIterator>> {

//......

}

}

ResultsPageIterator迭代器繼承自Guava框架定義的AbstractIterator,間接實現了接口Iterator

封裝內部概念

編寫整潔代碼的實踐告訴我們:類的定義要滿足“單一職責原則”,這樣的類才是專注的。編寫可擴展代碼的實踐也告訴我們:類的定義要滿足“單一職責原則”,如此才能保證只有一個引起變化的原因。然而,即使類只履行一個專注的職責,也可能因為過於複雜的業務邏輯而導致設計出相對比較龐大的類(真實項目總是要比玩具項目複雜100倍)。這時,我們會面臨左右為難的選擇。倘若不將職責分離到另外的單獨類中,則該類就過於龐大,導致內部邏輯過於複雜;倘若將部分邏輯分離出去,又會導致類的數量過多,導致系統複雜度增加。這時,我們可以用嵌套類來封裝內部概念,定義屬於自己的數據與行為,在主類內部形成更加清晰的職責邊界,卻沒有引起系統類數量的增加,算是一種折中的設計手法。

當然,我們必須謹記的一點是:究竟職責應該定義在獨立類中,還是定義在附屬的嵌套類,判斷標準不是看代碼量以及類的規模,而是看這些需要封裝的邏輯究竟屬於內部概念,還是外部概念。

例如Presto框架的HiveWriterFactory類。它是一個工廠類,創建的產品為HiveWriter。在創建過程中,需要根據列名、列類型以及列的Hive類型進行組合,並將組合後的結果賦值給Properties類型的schema。

這些值並不需要公開,如果不對其進行封裝,則存在問題:

  • 沒有體現領域概念,只有散亂的三種類型的變量
  • 無法將其作為整體放入到集合中,因而也無法調用集合的API對其進行轉換

針對Hive所要操作的數據,列名、列類型以及列的Hive類型這三個散亂概念實際上表達的是數據列的概念,於是就可以在這個工廠類中定義嵌套類來封裝這些概念與邏輯:

閱讀開源框架,遍覽Java嵌套類的用法

在封裝為DataColumn嵌套類後,就可以將其放入到List集合中,然後像如下代碼那樣對其進行操作:

schema.setProperty(META_TABLE_COLUMNS, dataColumns.stream()

.map(DataColumn::getName)

.collect(joining(",")));

schema.setProperty(META_TABLE_COLUMN_TYPES, dataColumns.stream()

.map(DataColumn::getHiveType)

.map(HiveType::getHiveTypeName)

.collect(joining(":")));

在開發過程中,我們還會經常遇見一種典型的數據結構,它需要將兩個值對應起來形成一種映射。這種數據結構通常稱之為tuple。雖然在Java 7已經提供了這種簡便的結構(Scala在一開始就提供了tuple結構,而且還提供了._1和._2的快捷訪問方式),但這種結構是通用的,不利於體現出業務概念。當這種結構僅僅在一個類的內部使用時,就是嵌套類登上舞臺的時候了。

例如我們要建立Type與Object之間的映射關係,並被用在一個SQL語句的解析器中充當解析過程中的元數據,就可以定義為類TypeAndValue,命名直白,清晰可見:

public class SQLStatementParser {

private List accumulator = new ArrayList<>();

public PreparedStatement buildSql(JdbcClient client, Connection connection, String catalog, String schema, String table, List columns) {

StringBuilder sql = new StringBuilder();

String columnNames = columns.stream()

.map(JdbcColumnHandle::getColumnName)

.map(this::quote)

.collect(joining(", "));

sql.append("SELECT ");

sql.append(columnNames);

if (columns.isEmpty()) {

sql.append("null");

}

sql.append(" FROM ");

if (!isNullOrEmpty(catalog)) {

sql.append(quote(catalog)).append('.');

}

if (!isNullOrEmpty(schema)) {

sql.append(quote(schema)).append('.');

}

sql.append(quote(table));

List clauses = toConjuncts(columns, accumulator);

if (!clauses.isEmpty()) {

sql.append(" WHERE ")

.append(Joiner.on(" AND ").join(clauses));

}

PreparedStatement statement = client.getPreparedStatement(connection, sql.toString());

for (int i = 0; i < accumulator.size(); i++) {

TypeAndValue typeAndValue = accumulator.get(i);

if (typeAndValue.getType().equals(BigintType.BIGINT)) {

statement.setLong(i + 1, (long) typeAndValue.getValue());

}

else if (typeAndValue.getType().equals(IntegerType.INTEGER)) {

statement.setInt(i + 1, ((Number) typeAndValue.getValue()).intValue());

}

else if (typeAndValue.getType().equals(SmallintType.SMALLINT)) {

statement.setShort(i + 1, ((Number) typeAndValue.getValue()).shortValue());

}

else if (typeAndValue.getType().equals(TinyintType.TINYINT)) {

statement.setByte(i + 1, ((Number) typeAndValue.getValue()).byteValue());

}

else if (typeAndValue.getType().equals(DoubleType.DOUBLE)) {

statement.setDouble(i + 1, (double) typeAndValue.getValue());

}

else if (typeAndValue.getType().equals(RealType.REAL)) {

statement.setFloat(i + 1, intBitsToFloat(((Number) typeAndValue.getValue()).intValue()));

}

else if (typeAndValue.getType().equals(BooleanType.BOOLEAN)) {

statement.setBoolean(i + 1, (boolean) typeAndValue.getValue());

}

else if (typeAndValue.getType().equals(DateType.DATE)) {

long millis = DAYS.toMillis((long) typeAndValue.getValue());

statement.setDate(i + 1, new Date(UTC.getMillisKeepLocal(DateTimeZone.getDefault(), millis)));

}

else if (typeAndValue.getType().equals(TimeType.TIME)) {

statement.setTime(i + 1, new Time((long) typeAndValue.getValue()));

}

else if (typeAndValue.getType().equals(TimeWithTimeZoneType.TIME_WITH_TIME_ZONE)) {

statement.setTime(i + 1, new Time(unpackMillisUtc((long) typeAndValue.getValue())));

}

else if (typeAndValue.getType().equals(TimestampType.TIMESTAMP)) {

statement.setTimestamp(i + 1, new Timestamp((long) typeAndValue.getValue()));

}

else if (typeAndValue.getType().equals(TimestampWithTimeZoneType.TIMESTAMP_WITH_TIME_ZONE)) {

statement.setTimestamp(i + 1, new Timestamp(unpackMillisUtc((long) typeAndValue.getValue())));

}

else if (typeAndValue.getType() instanceof VarcharType) {

statement.setString(i + 1, ((Slice) typeAndValue.getValue()).toStringUtf8());

}

else {

throw new UnsupportedOperationException("Can't handle type: " + typeAndValue.getType());

}

}

return statement;

}

private static class TypeAndValue {

private final Type type;

private final Object value;

public TypeAndValue(Type type, Object value) {

this.type = requireNonNull(type, "type is null");

this.value = requireNonNull(value, "value is null");

}

public Type getType() {

return type;

}

public Object getValue() {

return value;

}

}

}

TypeAndValue內部類的封裝,有效地體現了類型與值之間的映射關係,改進了代碼的可讀性。事實上,以上代碼還有可堪改進的空間,例如我們可以利用類的封裝性,將類型判斷的語句封裝為方法,例如isBigInt()、isInteger()、isDouble()、isTimeWithTimeZone()、isVarcharType()等方法,則前面的一系列分支語句會變得更流暢一些:

for (int i = 0; i < accumulator.size(); i++) {

TypeAndValue typeAndValue = accumulator.get(i);

if (typeAndValue.isBigInt())) {

statement.setLong(i + 1, (long) typeAndValue.getValue());

}

else if (typeAndValue.isInteger())) {

statement.setInt(i + 1, ((Number) typeAndValue.getValue()).intValue());

}

else if (typeAndValue.isSmallInt())) {

statement.setShort(i + 1, ((Number) typeAndValue.getValue()).shortValue());

}

//……

else {

throw new UnsupportedOperationException("Can't handle type: " + typeAndValue.getType());

}

}

作為內部Map的Key

假定一個類的內部需要用到Map集合,而該集合對象又僅僅在內部使用,並不會公開給外部調用者。對於這樣的Map集合,倘若Java基本類型或者字符串無法承擔key的作用,就可以定義一個內部靜態嵌套類作為該Map的key。注意,由於嵌套類要作為唯一不重複的key,且該類型為引用類型,因而需要重寫equals()方法與hashCode()方法,視情況還應該重寫toString()方法。

例如在一個類中需要創建一個Map用以存儲多個指標對象,但該指標的key由兩個ByteBuffer對象聯合組成,就可以封裝為一個內部嵌套類:

private final Map metrics = new HashMap<>();

private static class MetricsKey {

public final ByteBuffer row;

public final ByteBuffer family;

public MetricsKey(ByteBuffer row, ByteBuffer family) {

requireNonNull(row, "row is null");

requireNonNull(family, "family is null");

this.row = row;

this.family = family;

}

@Override

public boolean equals(Object obj) {

if (this == obj) {

return true;

}

if ((obj == null) || (getClass() != obj.getClass())) {

return false;

}

MetricsKey other = (MetricsKey) obj;

return Objects.equals(this.row, other.row)

&& Objects.equals(this.family, other.family);

}

@Override

public int hashCode() {

return Objects.hash(row, family);

}

@Override

public String toString() {

return toStringHelper(this)

.add("row", new String(row.array(), UTF_8))

.add("family", new String(row.array(), UTF_8))

.toString();

}

}

實現外部接口

當我們在一個類的內部需要使用一個外部接口,且該接口的實現邏輯又比較複雜,而在類的外部又不存在重用的可能,此時就可以定義一個私有的內部嵌套類去實現該接口。

Presto框架的BenchmarkSuite類內部,調用了AbstractBenchmark類的runBenchmark()方法,該方法需要傳入BenchmarkResultHook接口類型的對象:

public void runBenchmark(@Nullable BenchmarkResultHook benchmarkResultHook) {}

而BenchmarkResultHook是一個獨立定義的接口:

public interface BenchmarkResultHook {

BenchmarkResultHook addResults(Map results);

void finished();

}

在類的內部,定義了實現BenchmarkResultHook接口的內部嵌套類:

private static class ForwardingBenchmarkResultWriter implements BenchmarkResultHook {}

然後在BenchmarkSuite中就可以使用它:

benchmark.runBenchmark(

new ForwardingBenchmarkResultWriter(

ImmutableList.of(

new JsonBenchmarkResultWriter(jsonOut),

new JsonAvgBenchmarkResultWriter(jsonAvgOut),

new SimpleLineBenchmarkResultWriter(csvOut),

new OdsBenchmarkResultWriter("presto.benchmark." + benchmark.getBenchmarkName(), odsOut)

)

)

);

實現外部接口還有一種特殊情況是利用嵌套類實現一個函數接口。雖然在多數情況下,當我們在使用函數接口時,會使用Lambda表達式來實現該接口,其本質其實是一個函數。然而,當一個函數的實現相對比較複雜,且有可能被類的內部多處重用時,就不能使用Lambda表達式了。可是針對這種情形,確乎又沒有必要定義單獨的類去實現該函數接口,這時就是嵌套類的用武之地了。

例如在Presto框架的HivePageSource類中有一個方法createCoercer(),它返回的類型是一個函數接口類型Function。方法的實現會根據hive的類型決定返回的函數究竟是什麼。目前支持以下四種情形:

  • Integer數字轉換為Varchar
  • Varchar轉換為Integer數字
  • Integer數字的提升
  • Float轉換為Double

這四種情形對應的Coercer其實都是一個函數Block -> Block,但這個轉換的邏輯比較複雜,尤其針對“Integer數字的提升”情形,還存在多個條件分支的判斷。於是,Presto就在HivePageSource定義瞭如下四個嵌套類,並且都實現了函數接口Function

public class HivePageSource implements ConnectorPageSource {

private static class IntegerNumberUpscaleCoercer implements Function {

private final Type fromType;

private final Type toType;

public IntegerNumberUpscaleCoercer(Type fromType, Type toType)

{

this.fromType = requireNonNull(fromType, "fromType is null");

this.toType = requireNonNull(toType, "toType is null");

}

@Override

public Block apply(Block block)

{

//...

}

}

private static class IntegerNumberToVarcharCoercer implements Function {

private final Type fromType;

private final Type toType;

public IntegerNumberToVarcharCoercer(Type fromType, Type toType)

{

this.fromType = requireNonNull(fromType, "fromType is null");

this.toType = requireNonNull(toType, "toType is null");

}

@Override

public Block apply(Block block)

{

//...

}

}

private static class VarcharToIntegerNumberCoercer implements Function {

private final Type fromType;

private final Type toType;

private final long minValue;

private final long maxValue;

public VarcharToIntegerNumberCoercer(Type fromType, Type toType) {

//...

}

@Override

public Block apply(Block block) {

//...

}

}

private static class FloatToDoubleCoercer implements Function {

@Override

public Block apply(Block block) {

//...

}

}

}

封裝內部常量

如果一個類的內部需要用到大量常量,而這些常量卻沒有重用的可能,換言之,這些常量無需公開,僅作為內部使用。這時,我們可以通過嵌套類對這些常量進行歸類,既便於調用,又提高了可讀性,形成一種組織良好的代碼結構。

public class MetadataLoader {

private static class IpAddressConstants {

// IPv4: 255.255.255.255 - 15 characters

// IPv6: FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF - 39 characters

// IPv4 embedded into IPv6: FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:255.255.255.255 - 45 characters

private static final int IPV4_STRING_MAX_LENGTH = 15;

private static final int IPV6_STRING_MAX_LENGTH = 39;

private static final int EMBEDDED_IP_ADDRESS_STRING_MAX_LENGTH = 45;

}

private static class TypeConstants {

private static final BigintType BIGINT = new BigintType();

private static final BooleanType BOOLEAN = new BooleanType();

private static final DoubleType DOUBLE = new DoubleType();

private static final TimestampType TIMESTAMP = new TimestampType();

}

//……

}

內部重用

有些重用代碼塊,它的重用單元比方法要多一些,卻又不成其為單獨的類,因為它只在類的內部被重用,這時就可以將其定義為嵌套類,以利於內部重用。

例如,在一個併發處理程序的Query類中,我們需要根據一個布爾標誌來判斷當前線程是否需要中斷。中斷線程的邏輯無法封裝在一個私有方法中。如果將這個邏輯直接寫到調用方法中,有可能干擾到主要邏輯的閱讀,為了提供代碼的可讀性,Query類定義了一個內部嵌套類ThreadInterruptor,用以封裝中斷當前線程的邏輯:

public class Query implements Closeable {

private final AtomicBoolean ignoreUserInterrupt = new AtomicBoolean();

private final AtomicBoolean userAbortedQuery = new AtomicBoolean();

public void pageOutput(OutputFormat format, List fieldNames) {

try (Pager pager = Pager.create();

ThreadInterruptor clientThread = new ThreadInterruptor();

Writer writer = createWriter(pager);

OutputHandler handler = createOutputHandler(format, writer, filedNames)) {

if (!pager.isNullPager()) {

ignoreUserInterrupt.set(true);

pager.getFinishFuture().thenRun(() -> {

userAbortedQuery.set(true);

ignoreUserInterrupt.set(false);

clientThread.interrupt();

});

}

handler.processRow(client);

} catch (RuntimeException | IOException e) {

if (userAbortedQuery.get() && !(e instanceOf QueryAbortedException)) {

throw new QueryAbortedException(e);

}

throw e;

}

}

private static class ThreadInterruptor implements Closeable {

private final Thread thread = Thread.currentThread();

private final AtomicBoolean processing = new AtomicBoolean(true);

public synchronized void interrupt() {

if (processing.get()) {

thread.interrupt();

}

}

@Override

public synchronized void close() {

processing.set(false);

}

}

}

注意,由於ThreadInterruptor對象要放到try語句塊中,因而需要實現Closeable接口,且需要通過try調用close()方法時,將processing標誌變量置為false。

內部異常

這算是內部重用的一種特殊情形,即對異常的內部重用。當我們需要拋出一個異常,且希望拋出的消息能夠體現該異常場景,又或者該異常需要攜帶的內容不僅僅包括消息或錯誤原因時,都需要我們自定義異常。倘若該自定義異常沒有外部公開的必要,就可以通過嵌套類定義一個內部異常。

例如我們希望一個異常在拋出時能夠攜帶類型信息,且該異常僅為內部使用,就可以這樣來定義:

public class FailureInfo {

private static class FailureException

extends RuntimeException {

private final String type;

FailureException(String type, String message, FailureException cause) {

super(message, cause, true, true);

this.type = requireNonNull(type, "type is null");

}

public String getType() {

return type;

}

@Override

public String toString() {

String message = getMessage();

if (message != null) {

return type + ": " + message;

}

return type;

}

}

}

無論嵌套類的使用形式如何多樣,體現的價值又如何地梅蘭秋菊各擅勝場,根本的原理還是逃不開面向對象設計的基本思想,即通過封裝體現內聚的概念,從而利於重用,應對變化,或者就是單純地組織代碼,讓代碼的結構變得更加清晰。尤其是在相對複雜的真實項目中,如何控制對象類型的數量,又不至於違背“單一職責原則”,使用嵌套類是一條不錯的中間路線選擇


分享到:


相關文章: