CharSequence Magic

java.lang.CharSequenceは、一見しただけでは3つのメソッドの控えめなインターフェースのように見えますが、詳しく調べると、興味深いニュアンスがいくつかわかります。

インターフェイスは、 StringStringBufferStringBuilderGString (groovy)などのJavaクラスによって実装されます。



TL;このインターフェイスをクラスに追加すると、文字列プロパティの一部を受け取り、文字列との比較(たとえば、 String.contentEquals )、さまざまな文字列API(たとえば、 Pattern.matcher )の使用、および自動動作検出の場所が表示されます。タイプに応じて(たとえば、jdbcのクエリパラメータのバインド)。



さらに、このアプローチにより、一連のリファクタリングが簡素化され、アプリケーションの型システムが強化されます。主に、Stringオブジェクトが専用のラッパーまたは列挙定数に置き換えられます。



文字列スカラー



値の形式に制限を追加し、型の安全性を高めるために、文字列の代わりに特別なスカラーラッパーを使用できます。 理解するために例を考えてみましょう-クライアントIDを正規表現に一致する文字列とします。 そのラッパークラスは次のようになります。



public final class ClientId { private final String value; private ClientId(String value) { this.value = value; } /** * ... * @throws IllegalArgumentException ... */ public static ClientId fromString(String value) throws IllegalArgumentException { if (value == null || !value.matches("^CL-\\d+$")) { throw new IllegalArgumentException("Illegal ClientId format [" + value + "]"); } return new ClientId(value); } public String getValue() { return value; } public boolean eq(ClientId that) { return this.value.equals(that.value); } @Override public boolean equals(Object o) { if (o instanceof String) { //   -   ( false) // -  equals     throw new IllegalArgumentException("You should not check equals with String"); } return o instanceof ClientId && eq((ClientId) o); } @Override public int hashCode() { return value.hashCode(); } @Override public String toString() { return value; } }
      
      





このようなオブジェクトは文字列のプロパティを失い、それらを返すには、getValue()またはtoString()を呼び出す必要があります。 しかし、他の方法でもできます-CharSequenceをクラスにミックスします。



インターフェイス(java 8)について考えてみましょう。



 public interface DefaultCharSequence extends CharSequence { @Override default int length() { return toString().length(); } @Override default char charAt(int index) { return toString().charAt(index); } @Override default CharSequence subSequence(int start, int end) { return toString().subSequence(start, end); } }
      
      





クラスに追加すると、つまり



 public final class ClientId implements DefaultCharSequence {
      
      





多くの可能性が現れます。



たとえば、次のように記述できます。



 ClientId clientId = ClientId.fromString("CL-123"); String otherClientId = ...; // NOTE: equals      ,    if (otherClientId.contentEquals(clientId)) { // do smth }
      
      





文字列比較



文字列比較は、最も一般的に使用される文字列操作の1つです。 最も標準的なオプションは、 String.equals(otherString)を呼び出すことです 。 遭遇する可能性のある最初の問題は、null-safetyです。伝統的に、オブジェクトの1つが定数である場合、引数でオブジェクトを反転することで解決されます: STRING_CONSTANT.equals(value) 。 引数のいずれかがヌルになる可能性がある場合、 java.util.Objects.equals(o1、o2)が助けになります。



複雑で大規模なプロジェクトの現実には、 平等という別の問題があります-型安全性の弱い引数(任意のオブジェクト)です。 実際には、これは、等しい引数として任意のオブジェクト(たとえば、 IntegerまたはEnum )を渡すことができることを意味し、コンパイラーは警告さえ与えず、呼び出しは単にfalseを返します。 このようなエラーは開発段階で簡単に特定できることに注意してください。ここでIDEが通知し、最初のテストで明らかになります。 しかし、プロジェクトのサイズが大きくなり、レガシーになり、進化し続けると、 STRING_CONSTANTがStringからEnumまたはIntegerに変わるときに、遅かれ早かれ状況が発生する可能性があります。 テストカバレッジが十分に高くない場合、 equalsはfalse falseを返し始めます



これも解決できます。
これは、コード分析を手動で実行するか、 Sonarなどのツールを使用して、事後的に検出できます。 IDEAでは、このコード分析は「変換不可能な型のオブジェクト間の等しい()」と呼ばれます
しかし、良い慣行は予防に関するものであり、結果への対処に関するものではありません。



コンパイル段階で型チェックを強化するには、 equals呼び出しをString.contentEquals(CharSequence)またはorg.apache.commons.lang3.StringUtils.equals(CharSequence、CharSequence)に置き換えることができます

後者の場合、追加の変換なしでStringとClientIdを比較できるようになったため、これらのオプションはどちらも優れています。 ヌルセーフでもあります。



リファクタリング



上記の状況は少し難解に思えるかもしれませんが、この決定はさまざまなレガシーコードのリファクタリングの結果としてもたらされました。 ここで話している典型的なリビジョンは、Stringオブジェクトをラッパークラスまたは列挙型定数に置き換えることです。 ラッパークラスは、契約、カード、電話、パスワード、ハッシュの合計、特定の種類の要素の名前など、多くの典型的な不変の文字列に使用できます。値の形式をチェックすることに加えて、ラッパークラスはそれを処理する特定のメソッドを追加できます。 このリファクタリングにあまり慎重にアプローチしないと、多くの問題に遭遇する可能性があります-最初の問題は安全ではありません。



文字列ではなく、たとえば数値をラップするラッパークラスには制限があります。 この場合、toStringの呼び出しは(文字列全体に対するcharAtの同じ連続した呼び出しに対して)比較的高価になる可能性があります-ここでは、潜在的にキャッシュされた文字列表現を使用できます。



jdbcリクエストへの引数のバインド



この場合、 JdbcTemplate / NamedParameterJdbcTemplateを介してバインドするspring-jdbcについて話していることをすぐに明確にします

バインディングパラメータ値でクラスClientIdのオブジェクトを渡すことができます。 CharSequenceを実装します。



 public Client getClient(ClientId clientId) { return jdbcTemplate.queryForObject( "SELECT * FROM clients " + "WHERE clientId_id = ?", (row, rowNum) -> mapClient(row), clientId ); }
      
      





このコードを元のgetClient(String clientId)宣言からやり直したと見なした場合、渡された値の使用に関して、ここではすべてが変更されません。



仕組み
org.springframework.jdbc.core.StatementCreatorUtils.setValueは最初に引数のタイプをCharSequence( isStringValue()を参照として決定し、次にtoStringへの変換を行い、PreparedStatementのバインディング自体はps.setString(paramIndex、inValue.toring()になります) ;



おわりに



私は数か月前からプロジェクトでこの方法を使用していますが、まだ問題は発生していません。



Stringの代わりにCharSequenceを使用するAPIは十分に豊富です-CharSequenceの使用法を見つけるだけで十分です。 Androidライブラリには特に注意を払うことができます-特にたくさんありますが、ここでは何かアドバイスをすることを恐れています。 このメソッドはまだテストしていません。



この質問についてのフィードバックをお送りします。これについてどう思いますか、どのような利益/レーキがあり、同様のプラクティスを使用する意味がありますか。



All Articles