5
\$\begingroup\$

(This post has continuation at A string view over a Java String - improved take II.)

This time, I have a simple string view class for faster operation on substrings in actual string objects:

com.github.coderodde.util.StringView.java:

package com.github.coderodde.util; import java.util.Objects; /** * This class implements a string view that can be enlarged, shrinked or * shifted. * * @version 1.0.0 (Aug 27, 2024) * @since 1.0.0 (Aug 27, 2024) */ public final class StringView { private final String string; private int viewOffset; private int viewLength; /** * Constructs this string view. * * @param string the owner string. * @param viewOffset the offset in the owner string. * @param viewLength the length of the view. */ public StringView(final String string, final int viewOffset, final int viewLength) { this.string = Objects.requireNonNull(string, "The input string is null."); checkOnConstruction(viewOffset, viewLength); this.viewOffset = viewOffset; this.viewLength = viewLength; } /** * Constructs this string view. The resulting view will cover the entire * owner string. * * @param string the owner string. */ public StringView(final String string) { this(string, 0, string.length()); } /** * Accesses the {@code index}th character of this string view. * * @param index the index of the desired character. * * @return the {@code index}th character of this string view. */ public char charAt(final int index) { checkIndex(index); return string.charAt(viewOffset + index); } /** * Return a {@link java.lang.String} holding the contents of this string * view. * * @return the string representation of this string view. */ @Override public String toString() { final StringBuilder sb = new StringBuilder(viewLength); for (int index = 0; index < viewLength; index++) { sb.append(charAt(index)); } return sb.toString(); } public String getOwnerString() { return string; } public int getViewOffset() { return viewOffset; } public int getViewLength() { return viewLength; } /** * Attempts to shift the view by {@code steps} steps to the left. * * @param steps the number of steps to shift. */ public void shiftLeft(final int steps) { checkStepsNotBelowZero(steps); viewOffset = Math.max(0, viewOffset - steps); } /** * Attempts to shift the view by {@code steps} steps to the right. * * @param steps the number of steps to shift. */ public void shiftRight(final int steps) { checkStepsNotBelowZero(steps); viewOffset = Math.min(string.length(), viewOffset + steps); } /** * Shift either to left or right by {@code steps} steps. If the argument is * negative, shifts to the left. Otherwise, shifts to the right. * * @param steps the number of steps to shift. */ public void shift(final int steps) { if (steps < 0) { shiftLeft(-steps); } else { shiftRight(steps); } } /** * Shrinks this string view by {@code length} elements from the tail of the * string view. * * @param length the length to shrinky by. */ public void shrink(final int length) { checkLengthNotNegative(length); checkLengthIsNotLargerThanCurrentViewLength(length); viewLength -= length; } /** * Grows this string view by {@code length} elements towards the tail of the * string view. * * @param length the length to grow by. */ public void grow(final int length) { checkLengthNotNegative(length); checkViewDoesNotOutgrow(length); viewLength += length; } private void checkIndex(final int index) { if (index < 0) { final String exceptionMessage = String.format("index (%d) < 0", index); throw new IllegalArgumentException(exceptionMessage); } if (index >= viewLength) { final String exceptionMessage = String.format( "index (%d) >= viewLength (%d)", index, viewLength); throw new IndexOutOfBoundsException(exceptionMessage); } } private void checkViewDoesNotOutgrow(final int length) { if (viewOffset + length > string.length()) { final String exceptionMessage = String.format( "New view outgrows the string view parent by %d characters.", viewOffset + length - string.length()); throw new IllegalArgumentException(exceptionMessage); } } private void checkLengthNotNegative(final int length) { if (length < 0) { final String exceptionMessage = String.format("length (%d) < 0", length); throw new IllegalArgumentException(exceptionMessage); } } private void checkLengthIsNotLargerThanCurrentViewLength(final int length) { if (length > viewLength) { final String exceptionMessage = String.format( "length (%d) > viewLength (%d)", length, viewLength); throw new IllegalArgumentException(exceptionMessage); } } private void checkStepsNotBelowZero(final int steps) { if (steps < 0) { final String exceptionMessage = String.format("steps (%d) < 0", steps); throw new IllegalArgumentException(exceptionMessage); } } private void checkOnConstruction(final int viewOffset, final int viewLength) { if (viewOffset < 0) { final String exceptionMessage = String.format( "offset (%d) < 0", viewOffset); throw new IllegalArgumentException(exceptionMessage); } if (viewOffset > string.length()) { final String exceptionMessage = String.format( "offset (%d) > string.length (%d)", viewOffset, string.length()); throw new IllegalArgumentException(exceptionMessage); } if (viewLength < 0) { final String exceptionMessage = String.format( "viewLength (%d) < 0", viewLength); throw new IllegalArgumentException(exceptionMessage); } if (viewLength > string.length()) { final String exceptionMessage = String.format( "viewLength (%d) > string.length (%d)", viewLength, string.length()); throw new IllegalArgumentException(exceptionMessage); } if (viewOffset + viewLength > string.length()) { final String exceptionMessage = String.format( "View outside the right border by %d characters.", viewOffset + viewLength - string.length()); throw new IllegalArgumentException(exceptionMessage); } } } 

com.github.coderodde.util.StringViewTest.java:

package com.github.coderodde.util; import org.junit.Test; import static org.junit.Assert.*; public class StringViewTest { @Test public void testCharAt() { final StringView view = new StringView("abcd"); assertEquals('a', view.charAt(0)); assertEquals('b', view.charAt(1)); assertEquals('c', view.charAt(2)); assertEquals('d', view.charAt(3)); view.shrink(1); view.shift(1); assertEquals('b', view.charAt(0)); assertEquals('c', view.charAt(1)); assertEquals('d', view.charAt(2)); } @Test public void testToString() { final StringView view = new StringView("abcde"); view.shrink(2); view.shiftRight(1); final String s = view.toString(); assertEquals("bcd", s); } @Test public void testShiftLeft() { final StringView view = new StringView("0123456789",3, 3); view.shiftLeft(2); assertEquals("123", view.toString()); } @Test public void testShiftRight() { final StringView view = new StringView("0123456", 3, 2); view.shiftRight(1); assertEquals("45", view.toString()); } @Test public void testShift() { final StringView view = new StringView("12345", 1, 3); view.shift(-1); assertEquals("123", view.toString()); view.shift(2); assertEquals("345", view.toString()); } @Test public void testShrink() { final StringView view = new StringView("abcde12345", 0, 6); view.shiftRight(2); view.shrink(4); assertEquals("cd", view.toString()); } @Test public void testGrow() { final StringView view = new StringView("abcde12345", 2, 4); view.grow(2); assertEquals("cde123", view.toString()); } } 

Critique request

As always, I would like to receive any commentary.

\$\endgroup\$

    2 Answers 2

    5
    \$\begingroup\$

    Mutability / Lack of Functionality

    Despite StringView being a cheap to instantiate thin wrapper, this class is not attractive to use.

    Reasons:

    equals() / hashCode()

    It does not implement equals() / hashCode() contract, which limits its use when it comes to Collections framework.

    One might argue by giving the examples of StringJoiner and StringBuilder which don't override these methods. But it's done for a reason, their whole purpose is to construct a new String, which has equals/hashCode properly overridden. On the other hand, the OP envisioned presented class as a cheap lightweight substring, a pier of the String class, not as a means of building strings.

    A publicly exposed class should not inherit identity-based implementations of these methods from java.lang.Object class, unless every instance of this class is inherently unique and is meant to be equal only to itself.

    It would make more sense not to treat these wrappers as inherently unique.

    Non-existent integration with the JDK environment

    String is one of the crucial and widely used JDK-types. There are many built-in methods that either expect or return a String or its super type CharSequence.

    But you can't plug StringView anywhere apart from your own code, and this type doesn't offer a lot.

    Focus on char type

    The focus of StringView on manipulating with char type, which is broken for over two decades, makes it unsuitable for working with strings containing Unicode code points or Grapheme clusters.

    Lack of Functionality available with CharSequence (and String)

    This type lacks important functionality, which will be at our disposal if we had at least a CharSequence on our hands (let alone String).

    Namely:

    • ability to match CharSequence against a regular expression (which is extremely useful for such purposes as validation);

    • ability to split CharSequence using a regular expression;

    • possibility to work with Unicode code points instead char;

    • we can create a stream over a CharSequence (either a stream of code points or characters);

    Here are some of the JDK methods which we can leverage with CharSequence:

    Pattern.matcher(CharSequence) Pattern.matches(String, CharSequence) Pattern.splitAsStream(CharSequence) Pattern.split(CharSequence) Path.write(Path, Iterable<? extends CharSequence>, Charset, OpenOption) Collectors.joining(CharSequence) Collectors.joining(CharSequence, CharSequence, CharSequence) String.contains(CharSequence) String.contentEquals(CharSequence) String.join(CharSequence, CharSequence) String.replace(CharSequence, CharSequence) 

    Interface CharSequence declares only a few methods, so implementation will not require superhuman efforts.

    Additionally, this interface is pretty stable, decision to implement it will not add much burden in terms of maintainability.

    It's worth considering making StringView to be an implementation of CharSequence.

    Shared Mutability issue

    It can not be safely shared between multiple threads. Because this will introduce an issue of shared mutability.

    We can not safely supply StringView to multiple treads contrary to a regular String.

    Why not making this view immutable?

    It requires only making methods grow() / shrink() / shift() return new instances of StringView, which allow making this type completely stateless.

    \$\endgroup\$
      6
      \$\begingroup\$

      Just two observations from me:

      • I don't understand why toString() works a character at a time, rather than simply returning string.substring(viewOffset, viewOffset+viewLength). The latter is possibly more efficient, since it's straight into native code.

      • Much of the code is devoted to argument checking, but I don't see any tests that provide out-of-range values to confirm that we get the expected exception.

      \$\endgroup\$

        Start asking to get answers

        Find the answer to your question by asking.

        Ask question

        Explore related questions

        See similar questions with these tags.