Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- name: Build with Maven
run: ./mvnw -B clean verify -DcommonConfig.jarSign.skip=true
- name: Analyze with SonaQube
if: ${{ github.actor != 'dependabot[bot]' }}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Releases

## 1.6.2

* remove hashCode caching since it could introduce very subtle bugs

## v1.6.1

* now build by JDK 11 and removed errorprone compiler #52
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</parent>

<artifactId>bytes</artifactId>
<version>1.6.1</version>
<version>1.6.2</version>
<packaging>bundle</packaging>

<name>Bytes Utility Library</name>
Expand Down
6 changes: 1 addition & 5 deletions src/main/java/at/favre/lib/bytes/Bytes.java
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,6 @@ public static Bytes random(int length, Random random) {
private final byte[] byteArray;
private final ByteOrder byteOrder;
private final BytesFactory factory;
private transient int hashCodeCache;

Bytes(byte[] byteArray, ByteOrder byteOrder) {
this(byteArray, byteOrder, new Factory());
Expand Down Expand Up @@ -2221,10 +2220,7 @@ public boolean equalsContent(Bytes other) {

@Override
public int hashCode() {
if (hashCodeCache == 0) {
hashCodeCache = Util.Obj.hashCode(internalArray(), byteOrder());
}
return hashCodeCache;
return Util.Obj.hashCode(internalArray(), byteOrder());
}

/**
Expand Down
146 changes: 146 additions & 0 deletions src/test/java/at/favre/lib/bytes/EncodingHexJmhBenchmark.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@

import org.openjdk.jmh.annotations.*;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -128,6 +131,7 @@ public class EncodingHexJmhBenchmark {
private BinaryToTextEncoding.EncoderDecoder option3;
private BinaryToTextEncoding.EncoderDecoder option4;
private BinaryToTextEncoding.EncoderDecoder option5;
private BinaryToTextEncoding.EncoderDecoder option6;
private Random random;

@Setup(Level.Trial)
Expand All @@ -139,6 +143,7 @@ public void setup() {
option3 = new BigIntegerHexEncoder();
option4 = new OldBytesImplementation();
option5 = new StackOverflowAnswer2Encoder();
option6 = new Jdk17HexFormat();

rndMap = new HashMap<>();
int[] lengths = new int[]{4, 8, 16, 32, 128, 512, 1000000};
Expand Down Expand Up @@ -176,6 +181,11 @@ public String encodeStackOverflowCode2() {
return encodeDecode(option5);
}

@Benchmark
public String encodeHexFormatJdk17() {
return encodeDecode(option6);
}

private String encodeDecode(BinaryToTextEncoding.EncoderDecoder encoder) {
Bytes[] bytes = rndMap.get(byteLength);
int rndNum = random.nextInt(bytes.length);
Expand Down Expand Up @@ -285,4 +295,140 @@ public byte[] decode(CharSequence encoded) {
throw new UnsupportedOperationException();
}
}

/**
* Copy of the JDK 17 implementation of HexFormat, only difference is that we need to create new strings, while
* the JDK can create strings without byte copy, I cant here.
*/
static final class Jdk17HexFormat implements BinaryToTextEncoding.EncoderDecoder {
private static final byte[] LOWERCASE_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
};
private final String delimiter = "";
private final String prefix = "";
private final String suffix = "";
private final byte[] digits = LOWERCASE_DIGITS;

@Override
public String encode(byte[] byteArray, ByteOrder byteOrder) {
return formatHex(byteArray, 0, byteArray.length);
}

private String formatHex(byte[] bytes, int fromIndex, int toIndex) {
Objects.requireNonNull(bytes, "bytes");
//Objects.checkFromToIndex(fromIndex, toIndex, bytes.length);
if (toIndex - fromIndex == 0) {
return "";
}
// Format efficiently if possible
String s = formatOptDelimiter(bytes, fromIndex, toIndex);
if (s == null) {
long stride = prefix.length() + 2L + suffix.length() + delimiter.length();
int capacity = checkMaxArraySize((toIndex - fromIndex) * stride - delimiter.length());
StringBuilder sb = new StringBuilder(capacity);
formatHex(sb, bytes, fromIndex, toIndex);
s = sb.toString();
}
return s;
}

private <A extends Appendable> A formatHex(A out, byte[] bytes, int fromIndex, int toIndex) {
Objects.requireNonNull(out, "out");
Objects.requireNonNull(bytes, "bytes");
//Objects.checkFromToIndex(fromIndex, toIndex, bytes.length);

int length = toIndex - fromIndex;
if (length > 0) {
try {
String between = suffix + delimiter + prefix;
out.append(prefix);
toHexDigits(out, bytes[fromIndex]);
if (between.isEmpty()) {
for (int i = 1; i < length; i++) {
toHexDigits(out, bytes[fromIndex + i]);
}
} else {
for (int i = 1; i < length; i++) {
out.append(between);
toHexDigits(out, bytes[fromIndex + i]);
}
}
out.append(suffix);
} catch (IOException ioe) {
throw new RuntimeException(ioe.getMessage(), ioe);
}
}
return out;
}

private <A extends Appendable> A toHexDigits(A out, byte value) {
Objects.requireNonNull(out, "out");
try {
out.append(toHighHexDigit(value));
out.append(toLowHexDigit(value));
return out;
} catch (IOException ioe) {
throw new RuntimeException(ioe.getMessage(), ioe);
}
}

private String formatOptDelimiter(byte[] bytes, int fromIndex, int toIndex) {
byte[] rep;
if (!prefix.isEmpty() || !suffix.isEmpty()) {
return null;
}
int length = toIndex - fromIndex;
if (delimiter.isEmpty()) {
// Allocate the byte array and fill in the hex pairs for each byte
rep = new byte[checkMaxArraySize(length * 2L)];
for (int i = 0; i < length; i++) {
rep[i * 2] = (byte) toHighHexDigit(bytes[fromIndex + i]);
rep[i * 2 + 1] = (byte) toLowHexDigit(bytes[fromIndex + i]);
}
} else if (delimiter.length() == 1 && delimiter.charAt(0) < 256) {
// Allocate the byte array and fill in the characters for the first byte
// Then insert the delimiter and hexadecimal characters for each of the remaining bytes
char sep = delimiter.charAt(0);
rep = new byte[checkMaxArraySize(length * 3L - 1L)];
rep[0] = (byte) toHighHexDigit(bytes[fromIndex]);
rep[1] = (byte) toLowHexDigit(bytes[fromIndex]);
for (int i = 1; i < length; i++) {
rep[i * 3 - 1] = (byte) sep;
rep[i * 3] = (byte) toHighHexDigit(bytes[fromIndex + i]);
rep[i * 3 + 1] = (byte) toLowHexDigit(bytes[fromIndex + i]);
}
} else {
// Delimiter formatting not to a single byte
return null;
}
try {
// Return a new string using the bytes without making a copy -> we cant use this here as we dont have access to JavaLangAccess
//return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1);
return new String(rep, StandardCharsets.ISO_8859_1);
} catch (Exception cce) {
throw new AssertionError(cce);
}
}

private char toHighHexDigit(int value) {
return (char) digits[(value >> 4) & 0xf];
}

private char toLowHexDigit(int value) {
return (char) digits[value & 0xf];
}

private static int checkMaxArraySize(long length) {
if (length > Integer.MAX_VALUE)
throw new OutOfMemoryError("String size " + length +
" exceeds maximum " + Integer.MAX_VALUE);
return (int) length;
}

@Override
public byte[] decode(CharSequence encoded) {
throw new UnsupportedOperationException();
}
}
}