CharSequence Magic

  • Tutorial
java.lang.CharSequence only at first glance seems an unpretentious interface of the three methods, but upon closer examination it reveals some interesting nuances to us.
The interface is implemented by such java classes as String , StringBuffer , StringBuilder , GString (groovy) and not only.

TL; DR if you add this interface to the class, it will receive part of the string properties and a number of possibilities will appear - comparisons with strings (for example, String.contentEquals ), the use of various string APIs (for example, Pattern.matcher ), as well as in places of automatic behavior detection depending on the type (for example, binding of query parameters in jdbc).

In addition, this approach will simplify a series of refactoring to strengthen the type system in the application - primarily replacing String objects with specialized wrappers or enum constants.

String scalars


To add restrictions on the format of the value, as well as enhancing type safety, special scalar wrappers can be used instead of strings. For understanding, consider an example - let the client ID be a string that matches the regular expression. Its wrapper class will look something like this:

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;
    }
}

Such an object loses the properties of a string, and in order to return them, you need to make a call to getValue () or toString (). But you can do otherwise - mix CharSequence into our class.

Consider the interface (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);
    }
}

If you add it to our class, i.e.

public final class ClientId implements DefaultCharSequence {

a number of possibilities will appear.

For example, you can now write like this:

ClientId clientId = ClientId.fromString("CL-123");
String otherClientId = ...;
// NOTE: equals в данном случае даст неверный результат, об этом ниже
if (otherClientId.contentEquals(clientId)) {
    // do smth
}

String comparison


String comparison is one of the most commonly used string operations. The most standard option is to call String.equals (otherString) . The first problem that we may encounter is null-safety, traditionally it is solved by the flip of the object with an argument, if one of them is a constant: STRING_CONSTANT.equals (value) . If any of the arguments can be null, java.util.Objects.equals (o1, o2) will come to the rescue .

In the realities of complex and large projects, another problem of equals awaits us - the weak typesafety argument (any Object). In practice, this means that any object can be passed as an equals argument (for example, Integer or Enum), the compiler will not even give warning, the call will simply return false. It is reasonable to note that such an error is easy to identify at the development stage - here the IDE will tell you and in the first tests it will be revealed. But when the project grows in size, turns into legacy and continues to evolve, sooner or later a situation may arise when STRING_CONSTANT turns from String to Enum or Integer . If the test coverage is not high enough, equals will start to give false false .

This can also be solved.
This can be detected ex-post by manually running Code Analyze, or by using tools like Sonar . In IDEA, this code analyze is called "equals () between objects of inconvertible types"
but good practices are about prevention, not about dealing with consequences.

To strengthen type checking at the compilation stage, the equals call can be replaced with String.contentEquals (CharSequence) , or org.apache.commons.lang3.StringUtils.equals (CharSequence, CharSequence)
Both of these options are good because now we can compare String with our ClientId without additional conversions, in the case of the latter - including also null-safe.

Refactoring


The situation described above may seem a little far-fetched, but this decision came as a result of various legacy code refactoring. The typical revision we are talking about here is replacing String objects with wrapper classes or enum constants. Wrapper classes can be used for a number of typical immutable strings - numbers of contracts, cards, phones, passwords, hash sums, names of specific types of elements, etc. In addition to checking the format of a value, a wrapper class can add specific methods for working with it. If you approach this refactoring not very carefully, you may come across a number of problems - the first of which is unsafe equals.

There is a limitation for wrapper classes that do not wrap String, but for example, numeric values. In this case, calling toString can be relatively expensive (for the same consecutive call to charAt for the entire string) - here you can potentially use a lazy cached String representation.

Binding arguments to jdbc requests


I’ll clarify right away that in this case we are talking about spring-jdbc binding through JdbcTemplate / NamedParameterJdbcTemplate
We can pass an object of the ClientId class in binding parameter values, because it implements CharSequence:


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

If we consider this code as redone from the original declaration getClient (String clientId) , then with regard to the use of the passed value, here everything will remain unchanged.

How it works
org.springframework.jdbc.core.StatementCreatorUtils.setValue will determine the type of the argument first as CharSequence (see isStringValue () ), then do the conversion to toString, and the binding itself in PreparedStatement will turn into ps.setString (paramIndex, inValue.toring () ;

Conclusion


I have been using this method in my projects for several months and have not yet encountered any problems.

The API that uses CharSequence instead of String is rich enough - just find usages for CharSequence is enough. Particular attention can be paid to the Android library - there is especially a lot of it, but here I'm afraid to advise anything, because I have not tested this method on it yet.

I will be glad to receive a feedback on the question - what do you think about this, what profit / rake is there, and is there any sense to use similar practices.

Also popular now: