Only at first glance,
java.lang.CharSequence seems to be a simple interface of three methods, but upon detailed examination it opens us with several interesting nuances.
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 a class, it will receive some string properties and a number of possibilities will appear - comparisons with strings (for example,
String.contentEquals ), use of various string APIs (for example,
Pattern.matcher ), as well as in places for automatic behavior detection depending on the type (for example, binding query parameters in jdbc).
In addition, this approach will simplify the implementation of a number of refactorings to enhance the type system in an 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, instead of strings, special scalar wrappers can be used. For understanding, consider an example - let the client ID be a string corresponding to a 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; } 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) {
Such an object loses the properties of the string, and to return them, you need to make a call to getValue () or toString (). But you can do otherwise - mix in our class CharSequence.
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 we add it to our class, i.e.
public final class ClientId implements DefaultCharSequence {
then a number of possibilities will appear.
For example, now you can write like this:
ClientId clientId = ClientId.fromString("CL-123"); String otherClientId = ...;
String comparison
String comparison is one of the most commonly used string operations. The most common option is to call
String.equals (otherString) . The first problem we may encounter is null-safety, traditionally it is solved by flip an 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) comes to the rescue.
In the realities of complex and large projects, another problem of
equals awaits us - weak typesafety argument (any Object). In practice, this means that as an equals argument, you can pass any object (for example,
Integer or
Enum ), the compiler will not even give a warning to this, the call will simply return false. It is reasonable to note that such an error is easy to detect at the development stage - here the IDE will prompt and it will be revealed at the first tests. But when the project grows in size, turns into legacy and at the same time continues to evolve, a situation may arise sooner or later when
STRING_CONSTANT turns from String into
Enum or
Integer . If the test coverage is not high enough,
equals will start giving false
false .
This can also be solved.This can be identified after the fact by manually running Code Analyze, or 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 fighting the consequences.
To enhance type checking at compile time, 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 the String with our ClientId without additional conversions, in the case of the latter - including. also null-safe.
Refactoring
The situation described above may seem somewhat far-fetched, but this decision came as a result of various legacy-code refactorings. A typical revision, which in this case is referred to, is the replacement of objects of type String with wrapper classes or enum constants. Wrapper classes can be used for a number of typical immutable strings - contract, card, phone number numbers, passwords, hash sums, names of specific element types, etc. A wrapper class can also add specific methods for working with it. If this refactoring is not approached very carefully, you may encounter a number of problems - the first of which is insecure equals.
There is a restriction for wrapper classes that do not wrap a String, but for example numeric values. In this case, the toString call can be relatively expensive (for the same successive call to the
charAt for the entire string) - here you can potentially use a lazy cached String representation.
Binding arguments to jdbc requests
At once I will clarify that we are talking about
spring-jdbc , binding through
JdbcTemplate / NamedParameterJdbcTemplateWe can pass an object of the class ClientId in the binding of the parameter value, since it implements a 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 remade from the initial
getClient (String clientId) declaration, then as far as the use of the passed value is concerned, everything will remain unchanged.
How it worksorg.springframework.jdbc.core.StatementCreatorUtils.setValue determine the type of the argument first as CharSequence (see. isStringValue ()), then make the conversion to toString, and Binding himself to become a PreparedStatement ps.setString (paramIndex, inValue.toString ()) ;
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 quite rich - 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 am afraid to advise anything, because This method has not yet been tested on it.
I would be happy to get feedback on the question - what do you think about this, what profit / rake is there and is there any point in using such practices at all.