Reduce garbage created when loading fat jars

Refactor fat jar loader classes so that less `char[]` instances are
created. This is primarily achieved by adding a new `StringSequence`
class that can chop up Strings without needing to copy the underlying
array. Since Java 8, calls to `String.subString(...)` always copy the
underlying char array. For many of the operations that we need, this
is unnecessary.

Fixes gh-11405
pull/11365/merge
Phillip Webb 7 years ago
parent c024313141
commit aa66d5dfb8

@ -27,6 +27,8 @@ import java.nio.charset.StandardCharsets;
*/
final class AsciiBytes {
private static final int[] EXCESS = { 0x0, 0x1080, 0x96, 0x1c82080 };
private final byte[] bytes;
private final int offset;
@ -118,36 +120,56 @@ final class AsciiBytes {
return new AsciiBytes(this.bytes, this.offset + beginIndex, length);
}
public AsciiBytes append(String string) {
if (string == null || string.isEmpty()) {
return this;
@Override
public String toString() {
if (this.string == null) {
this.string = new String(this.bytes, this.offset, this.length,
StandardCharsets.UTF_8);
}
return append(string.getBytes(StandardCharsets.UTF_8));
return this.string;
}
public AsciiBytes append(AsciiBytes asciiBytes) {
if (asciiBytes == null || asciiBytes.length() == 0) {
return this;
public boolean matches(CharSequence name, char suffix) {
int charIndex = 0;
int nameLen = name.length();
int totalLen = (nameLen + (suffix == 0 ? 0 : 1));
for (int i = this.offset; i < this.offset + this.length; i++) {
int b = this.bytes[i];
if (b < 0) {
b = b & 0x7F;
int limit = getRemainingUtfBytes(b);
for (int j = 0; j < limit; j++) {
b = (b << 6) + (this.bytes[++i] & 0xFF);
}
return append(asciiBytes.bytes);
b -= EXCESS[limit];
}
char c = getChar(name, suffix, charIndex++);
if (b <= 0xFFFF) {
if (c != b) {
return false;
}
}
else {
if (c != ((b >> 0xA) + 0xD7C0)) {
return false;
}
c = getChar(name, suffix, charIndex++);
if (c != ((b & 0x3FF) + 0xDC00)) {
return false;
}
public AsciiBytes append(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return this;
}
byte[] combined = new byte[this.length + bytes.length];
System.arraycopy(this.bytes, this.offset, combined, 0, this.length);
System.arraycopy(bytes, 0, combined, this.length, bytes.length);
return new AsciiBytes(combined);
}
return charIndex == totalLen;
}
@Override
public String toString() {
if (this.string == null) {
this.string = new String(this.bytes, this.offset, this.length, StandardCharsets.UTF_8);
private char getChar(CharSequence name, char suffix, int index) {
if (index < name.length()) {
return name.charAt(index);
}
return this.string;
if (index == name.length()) {
return suffix;
}
return 0;
}
@Override
@ -158,24 +180,11 @@ final class AsciiBytes {
int b = this.bytes[i];
if (b < 0) {
b = b & 0x7F;
int limit;
int excess = 0x80;
if (b < 96) {
limit = 1;
excess += 0x40 << 6;
}
else if (b < 112) {
limit = 2;
excess += (0x60 << 12) + (0x80 << 6);
}
else {
limit = 3;
excess += (0x70 << 18) + (0x80 << 12) + (0x80 << 6);
}
int limit = getRemainingUtfBytes(b);
for (int j = 0; j < limit; j++) {
b = (b << 6) + (this.bytes[++i] & 0xFF);
}
b -= excess;
b -= EXCESS[limit];
}
if (b <= 0xFFFF) {
hash = 31 * hash + b;
@ -190,6 +199,10 @@ final class AsciiBytes {
return hash;
}
private int getRemainingUtfBytes(int b) {
return (b < 96 ? 1 : (b < 112 ? 2 : 3));
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
@ -216,16 +229,17 @@ final class AsciiBytes {
return new String(bytes, StandardCharsets.UTF_8);
}
public static int hashCode(String string) {
// We're compatible with String's hashCode().
return string.hashCode();
public static int hashCode(CharSequence charSequence) {
// We're compatible with String's hashCode()
if (charSequence instanceof StringSequence) {
// ... but save making an unnecessary String for StringSequence
return charSequence.hashCode();
}
public static int hashCode(int hash, String string) {
for (int i = 0; i < string.length(); i++) {
hash = 31 * hash + string.charAt(i);
return charSequence.toString().hashCode();
}
return hash;
public static int hashCode(int hash, char suffix) {
return (suffix == 0 ? hash : (31 * hash + suffix));
}
}

@ -101,8 +101,8 @@ final class CentralDirectoryFileHeader implements FileHeader {
}
@Override
public boolean hasName(String name, String suffix) {
return this.name.equals(new AsciiBytes(suffix == null ? name : name + suffix));
public boolean hasName(CharSequence name, char suffix) {
return this.name.matches(name, suffix);
}
public boolean isDirectory() {

@ -30,10 +30,10 @@ interface FileHeader {
/**
* Returns {@code true} if the header has the given name.
* @param name the name to test
* @param suffix an additional suffix (or {@code null})
* @param suffix an additional suffix (or {@code 0})
* @return {@code true} if the header has the given name
*/
boolean hasName(String name, String suffix);
boolean hasName(CharSequence name, char suffix);
/**
* Return the offset of the load file header within the archive data.

@ -31,6 +31,8 @@ import java.util.jar.Manifest;
*/
class JarEntry extends java.util.jar.JarEntry implements FileHeader {
private final AsciiBytes name;
private Certificate[] certificates;
private CodeSigner[] codeSigners;
@ -41,6 +43,7 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) {
super(header.getName().toString());
this.name = header.getName();
this.jarFile = jarFile;
this.localHeaderOffset = header.getLocalHeaderOffset();
setCompressedSize(header.getCompressedSize());
@ -53,10 +56,13 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
setTime(header.getTime());
}
AsciiBytes getAsciiBytesName() {
return this.name;
}
@Override
public boolean hasName(String name, String suffix) {
return getName().length() == name.length() + suffix.length()
&& getName().startsWith(name) && getName().endsWith(suffix);
public boolean hasName(CharSequence name, char suffix) {
return this.name.matches(name, suffix);
}
/**

@ -191,6 +191,10 @@ public class JarFile extends java.util.jar.JarFile {
};
}
public JarEntry getJarEntry(CharSequence name) {
return this.entries.getEntry(name);
}
@Override
public JarEntry getJarEntry(String name) {
return (JarEntry) getEntry(name);
@ -228,8 +232,7 @@ public class JarFile extends java.util.jar.JarFile {
* @return a {@link JarFile} for the entry
* @throws IOException if the nested jar file cannot be read
*/
public synchronized JarFile getNestedJarFile(ZipEntry entry)
throws IOException {
public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException {
return getNestedJarFile((JarEntry) entry);
}
@ -257,16 +260,16 @@ public class JarFile extends java.util.jar.JarFile {
}
private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException {
final AsciiBytes sourceName = new AsciiBytes(entry.getName());
JarEntryFilter filter = (name) -> {
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
return name.substring(sourceName.length());
AsciiBytes name = entry.getAsciiBytesName();
JarEntryFilter filter = (candidate) -> {
if (candidate.startsWith(name) && !candidate.equals(name)) {
return candidate.substring(name.length());
}
return null;
};
return new JarFile(this.rootFile,
this.pathFromRoot + "!/"
+ entry.getName().substring(0, sourceName.length() - 1),
+ entry.getName().substring(0, name.length() - 1),
this.data, filter, JarFileType.NESTED_DIRECTORY);
}

@ -46,9 +46,9 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
private static final long LOCAL_FILE_HEADER_SIZE = 30;
private static final String SLASH = "/";
private static final char SLASH = '/';
private static final String NO_SUFFIX = "";
private static final char NO_SUFFIX = 0;
protected static final int ENTRY_CACHE_SIZE = 25;
@ -166,11 +166,11 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
return new EntryIterator();
}
public boolean containsEntry(String name) {
public boolean containsEntry(CharSequence name) {
return getEntry(name, FileHeader.class, true) != null;
}
public JarEntry getEntry(String name) {
public JarEntry getEntry(CharSequence name) {
return getEntry(name, JarEntry.class, true);
}
@ -213,7 +213,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
+ nameLength + extraLength, entry.getCompressedSize());
}
private <T extends FileHeader> T getEntry(String name, Class<T> type,
private <T extends FileHeader> T getEntry(CharSequence name, Class<T> type,
boolean cacheEntry) {
int hashCode = AsciiBytes.hashCode(name);
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry);
@ -224,8 +224,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
return entry;
}
private <T extends FileHeader> T getEntry(int hashCode, String name, String suffix,
Class<T> type, boolean cacheEntry) {
private <T extends FileHeader> T getEntry(int hashCode, CharSequence name,
char suffix, Class<T> type, boolean cacheEntry) {
int index = getFirstIndex(hashCode);
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
T entry = getEntry(index, type, cacheEntry);

@ -68,7 +68,8 @@ final class JarURLConnection extends java.net.JarURLConnection {
}
}
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName("");
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(
new StringSequence(""));
private static final String READ_ACTION = "read";
@ -254,17 +255,17 @@ final class JarURLConnection extends java.net.JarURLConnection {
}
static JarURLConnection get(URL url, JarFile jarFile) throws IOException {
String spec = extractFullSpec(url, jarFile.getPathFromRoot());
StringSequence spec = new StringSequence(url.getFile());
int index = indexOfRootSpec(spec, jarFile.getPathFromRoot());
int separator;
int index = 0;
while ((separator = spec.indexOf(SEPARATOR, index)) > 0) {
String entryName = spec.substring(index, separator);
StringSequence entryName = spec.subSequence(index, separator);
JarEntry jarEntry = jarFile.getJarEntry(entryName);
if (jarEntry == null) {
return JarURLConnection.notFound(jarFile, JarEntryName.get(entryName));
}
jarFile = jarFile.getNestedJarFile(jarEntry);
index += separator + SEPARATOR.length();
index = separator + SEPARATOR.length();
}
JarEntryName jarEntryName = JarEntryName.get(spec, index);
if (Boolean.TRUE.equals(useFastExceptions.get())) {
@ -276,14 +277,12 @@ final class JarURLConnection extends java.net.JarURLConnection {
return new JarURLConnection(url, jarFile, jarEntryName);
}
private static String extractFullSpec(URL url, String pathFromRoot) {
String file = url.getFile();
private static int indexOfRootSpec(StringSequence file, String pathFromRoot) {
int separatorIndex = file.indexOf(SEPARATOR);
if (separatorIndex < 0) {
return "";
return -1;
}
int specIndex = separatorIndex + SEPARATOR.length() + pathFromRoot.length();
return file.substring(specIndex);
return separatorIndex + SEPARATOR.length() + pathFromRoot.length();
}
private static JarURLConnection notFound() {
@ -308,22 +307,22 @@ final class JarURLConnection extends java.net.JarURLConnection {
*/
static class JarEntryName {
private final String name;
private final StringSequence name;
private String contentType;
JarEntryName(String spec) {
JarEntryName(StringSequence spec) {
this.name = decode(spec);
}
private String decode(String source) {
private StringSequence decode(StringSequence source) {
if (source.isEmpty() || (source.indexOf('%') < 0)) {
return source;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length());
write(source, bos);
write(source.toString(), bos);
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return AsciiBytes.toString(bos.toByteArray());
return new StringSequence(AsciiBytes.toString(bos.toByteArray()));
}
private void write(String source, ByteArrayOutputStream outputStream) {
@ -367,7 +366,7 @@ final class JarURLConnection extends java.net.JarURLConnection {
@Override
public String toString() {
return this.name;
return this.name.toString();
}
public boolean isEmpty() {
@ -389,15 +388,15 @@ final class JarURLConnection extends java.net.JarURLConnection {
return type;
}
public static JarEntryName get(String spec) {
public static JarEntryName get(StringSequence spec) {
return get(spec, 0);
}
public static JarEntryName get(String spec, int beginIndex) {
public static JarEntryName get(StringSequence spec, int beginIndex) {
if (spec.length() <= beginIndex) {
return EMPTY_JAR_ENTRY_NAME;
}
return new JarEntryName(spec.substring(beginIndex));
return new JarEntryName(spec.subSequence(beginIndex));
}
}

@ -0,0 +1,138 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.jar;
import java.util.Objects;
/**
* A {@link CharSequence} backed by a single shared {@link String}. Unlike a regular
* {@link String}, {@link #subSequence(int, int)} operations will not copy the underlying
* character array.
*
* @author Phillip Webb
*/
final class StringSequence implements CharSequence {
private final String source;
private final int start;
private final int end;
private int hash;
StringSequence(String source) {
this(source, 0, (source == null ? -1 : source.length()));
}
StringSequence(String source, int start, int end) {
Objects.requireNonNull(source, "Source must not be null");
if (start < 0) {
throw new StringIndexOutOfBoundsException(start);
}
if (end > source.length()) {
throw new StringIndexOutOfBoundsException(end);
}
this.source = source;
this.start = start;
this.end = end;
}
public StringSequence subSequence(int start) {
return subSequence(start, length());
}
@Override
public StringSequence subSequence(int start, int end) {
int subSequenceStart = this.start + start;
int subSequenceEnd = this.start + end;
if (subSequenceStart > this.end) {
throw new StringIndexOutOfBoundsException(start);
}
if (subSequenceEnd > this.end) {
throw new StringIndexOutOfBoundsException(end);
}
return new StringSequence(this.source, subSequenceStart, subSequenceEnd);
}
public boolean isEmpty() {
return length() == 0;
}
@Override
public int length() {
return this.end - this.start;
}
@Override
public char charAt(int index) {
return this.source.charAt(this.start + index);
}
public int indexOf(char ch) {
return this.source.indexOf(ch, this.start) - this.start;
}
public int indexOf(String str) {
return this.source.indexOf(str, this.start) - this.start;
}
public int indexOf(String str, int fromIndex) {
return this.source.indexOf(str, this.start + fromIndex) - this.start;
}
@Override
public String toString() {
return this.source.substring(this.start, this.end);
}
@Override
public int hashCode() {
int hash = this.hash;
if (hash == 0 && length() > 0) {
for (int i = this.start; i < this.end; i++) {
hash = 31 * hash + this.source.charAt(i);
}
this.hash = hash;
}
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
StringSequence other = (StringSequence) obj;
int n = length();
if (n == other.length()) {
int i = 0;
while (n-- != 0) {
if (charAt(i) != other.charAt(i)) {
return false;
}
i++;
}
return true;
}
return true;
}
}

@ -30,6 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class AsciiBytesTests {
private static final char NO_SUFFIX = 0;
@Rule
public ExpectedException thrown = ExpectedException.none();
@ -106,22 +108,6 @@ public class AsciiBytesTests {
abcd.substring(3, 5);
}
@Test
public void appendString() {
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
AsciiBytes appended = bc.append("D");
assertThat(bc.toString()).isEqualTo("BC");
assertThat(appended.toString()).isEqualTo("BCD");
}
@Test
public void appendBytes() {
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
AsciiBytes appended = bc.append(new byte[] { 68 });
assertThat(bc.toString()).isEqualTo("BC");
assertThat(appended.toString()).isEqualTo("BCD");
}
@Test
public void hashCodeAndEquals() {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
@ -163,4 +149,42 @@ public class AsciiBytesTests {
assertThat(new AsciiBytes(input).hashCode()).isEqualTo(input.hashCode());
}
@Test
public void matchesSameAsString() {
matchesSameAsString("abcABC123xyz!");
}
@Test
public void matchesSameAsStringWithSpecial() {
matchesSameAsString("special/\u00EB.dat");
}
@Test
public void matchesSameAsStringWithCyrillicCharacters() {
matchesSameAsString("\u0432\u0435\u0441\u043D\u0430");
}
@Test
public void matchesDifferentLengths() {
assertThat(new AsciiBytes("abc").matches("ab", NO_SUFFIX)).isFalse();
assertThat(new AsciiBytes("abc").matches("abcd", NO_SUFFIX)).isFalse();
assertThat(new AsciiBytes("abc").matches("abc", NO_SUFFIX)).isTrue();
assertThat(new AsciiBytes("abc").matches("a", 'b')).isFalse();
assertThat(new AsciiBytes("abc").matches("abc", 'd')).isFalse();
assertThat(new AsciiBytes("abc").matches("ab", 'c')).isTrue();
}
@Test
public void matchesSuffix() {
assertThat(new AsciiBytes("ab").matches("a", 'b')).isTrue();
}
@Test
public void matchesSameAsStringWithEmoji() {
matchesSameAsString("\ud83d\udca9");
}
private void matchesSameAsString(String input) {
assertThat(new AsciiBytes(input).matches(input, NO_SUFFIX)).isTrue();
}
}

@ -1,55 +0,0 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.jar;
import org.junit.Test;
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JarEntryName}.
*
* @author Andy Wilkinson
*/
public class JarEntryNameTests {
@Test
public void basicName() {
assertThat(new JarEntryName("a/b/C.class").toString()).isEqualTo("a/b/C.class");
}
@Test
public void nameWithSingleByteEncodedCharacters() {
assertThat(new JarEntryName("%61/%62/%43.class").toString())
.isEqualTo("a/b/C.class");
}
@Test
public void nameWithDoubleByteEncodedCharacters() {
assertThat(new JarEntryName("%c3%a1/b/C.class").toString())
.isEqualTo("\u00e1/b/C.class");
}
@Test
public void nameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
assertThat(new JarEntryName("%c3%a1/b/\u00c7.class").toString())
.isEqualTo("\u00e1/b/\u00c7.class");
}
}

@ -26,6 +26,7 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
import static org.assertj.core.api.Assertions.assertThat;
@ -155,6 +156,31 @@ public class JarURLConnectionTests {
.isEqualTo(connection.getJarEntry().getTime());
}
@Test
public void jarEntryBasicName() {
assertThat(new JarEntryName(new StringSequence("a/b/C.class")).toString())
.isEqualTo("a/b/C.class");
}
@Test
public void jarEntryNameWithSingleByteEncodedCharacters() {
assertThat(new JarEntryName(new StringSequence("%61/%62/%43.class")).toString())
.isEqualTo("a/b/C.class");
}
@Test
public void jarEntryNameWithDoubleByteEncodedCharacters() {
assertThat(new JarEntryName(new StringSequence("%c3%a1/b/C.class")).toString())
.isEqualTo("\u00e1/b/C.class");
}
@Test
public void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
assertThat(
new JarEntryName(new StringSequence("%c3%a1/b/\u00c7.class")).toString())
.isEqualTo("\u00e1/b/\u00c7.class");
}
private String getAbsolutePath() {
return this.rootJarFile.getAbsolutePath().replace('\\', '/');
}

@ -0,0 +1,170 @@
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.jar;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link StringSequence}.
*
* @author Phillip Webb
*/
public class StringSequenceTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void createWhenSourceIsNullShouldThrowException() {
this.thrown.expect(NullPointerException.class);
this.thrown.expectMessage("Source must not be null");
new StringSequence(null);
}
@Test
public void createWithIndexWhenSourceIsNullShouldThrowException() {
this.thrown.expect(NullPointerException.class);
this.thrown.expectMessage("Source must not be null");
new StringSequence(null, 0, 0);
}
@Test
public void createWhenStartIsLessThanZeroShouldThrowException() {
this.thrown.expect(StringIndexOutOfBoundsException.class);
new StringSequence("x", -1, 0);
}
@Test
public void createWhenEndIsGreaterThanLengthShouldThrowException() {
this.thrown.expect(StringIndexOutOfBoundsException.class);
new StringSequence("x", 0, 2);
}
@Test
public void creatFromString() {
assertThat(new StringSequence("test").toString()).isEqualTo("test");
}
@Test
public void subSequenceWithJustStartShouldReturnSubSequence() {
assertThat(new StringSequence("smiles").subSequence(1).toString())
.isEqualTo("miles");
}
@Test
public void subSequenceShouldReturnSubSequence() {
assertThat(new StringSequence("hamburger").subSequence(4, 8).toString())
.isEqualTo("urge");
assertThat(new StringSequence("smiles").subSequence(1, 5).toString())
.isEqualTo("mile");
}
@Test
public void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() {
assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3)
.toString()).isEqualTo("rg");
}
@Test
public void subSequenceWhenEndPastExistingEndShouldThrowException() {
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
assertThat(sequence.toString()).isEqualTo("bcd");
assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d");
this.thrown.expect(IndexOutOfBoundsException.class);
sequence.subSequence(3, 4);
}
@Test
public void subSequenceWhenStartPastExistingEndShouldThrowException() {
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
assertThat(sequence.toString()).isEqualTo("bcd");
assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d");
this.thrown.expect(IndexOutOfBoundsException.class);
sequence.subSequence(4, 3);
}
@Test
public void isEmptyWhenEmptyShouldReturnTrue() {
assertThat(new StringSequence("").isEmpty()).isTrue();
}
@Test
public void isEmptyWhenNotEmptyShouldReturnFalse() {
assertThat(new StringSequence("x").isEmpty()).isFalse();
}
@Test
public void lengthShouldReturnLength() {
StringSequence sequence = new StringSequence("hamburger");
assertThat(sequence.length()).isEqualTo(9);
assertThat(sequence.subSequence(4, 8).length()).isEqualTo(4);
}
@Test
public void charAtShouldReturnChar() {
StringSequence sequence = new StringSequence("hamburger");
assertThat(sequence.charAt(0)).isEqualTo('h');
assertThat(sequence.charAt(1)).isEqualTo('a');
assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u');
assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r');
}
@Test
public void indexOfCharShouldReturnIndexOf() {
StringSequence sequence = new StringSequence("aabbaacc");
assertThat(sequence.indexOf('a')).isEqualTo(0);
assertThat(sequence.indexOf('b')).isEqualTo(2);
assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2);
}
@Test
public void indexOfStringShouldReturnIndexOf() {
StringSequence sequence = new StringSequence("aabbaacc");
assertThat(sequence.indexOf("a")).isEqualTo(0);
assertThat(sequence.indexOf("b")).isEqualTo(2);
assertThat(sequence.subSequence(2).indexOf("a")).isEqualTo(2);
}
@Test
public void indexOfStringFromIndexShouldReturnIndexOf() {
StringSequence sequence = new StringSequence("aabbaacc");
assertThat(sequence.indexOf("a", 2)).isEqualTo(4);
assertThat(sequence.indexOf("b", 3)).isEqualTo(3);
assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3);
}
@Test
public void hashCodeShouldBeSameAsString() {
assertThat(new StringSequence("hamburger").hashCode())
.isEqualTo("hamburger".hashCode());
assertThat(new StringSequence("hamburger").subSequence(4, 8).hashCode())
.isEqualTo("urge".hashCode());
}
@Test
public void equalsWhenSameContentShouldMatch() {
StringSequence a = new StringSequence("hamburger").subSequence(4, 8);
StringSequence b = new StringSequence("urge");
StringSequence c = new StringSequence("urgh");
assertThat(a).isEqualTo(b).isNotEqualTo(c);
}
}
Loading…
Cancel
Save