Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/data/imagery/Shape.java
r18360 r18777 8 8 import java.util.AbstractList; 9 9 import java.util.List; 10 import java.util.Objects;11 10 import java.util.stream.Collectors; 12 import java.util.stream.Stream;13 11 14 12 import org.openstreetmap.gui.jmapviewer.Coordinate; … … 54 52 public String encodeAsString(String separator) { 55 53 return getPoints().stream() 56 .flatMap(c -> Stream.of(c.getLat(), c.getLon())) 57 .map(String::valueOf) 54 .map(c -> c.getLat() + separator + c.getLon()) 58 55 .collect(Collectors.joining(separator)); 59 56 } … … 71 68 72 69 public List<Coordinate> getPoints() { 73 return new AbstractList<Coordinate>() { 74 @Override 75 public Coordinate get(int index) { 76 double lat = coords.ypoints[index] / LatLon.MAX_SERVER_INV_PRECISION; 77 double lon = coords.xpoints[index] / LatLon.MAX_SERVER_INV_PRECISION; 78 return new Coordinate(lat, lon); 79 } 80 81 @Override 82 public int size() { 83 return coords.npoints; 84 } 85 }; 70 return new CoordinateList(this.coords); 86 71 } 87 72 … … 127 112 @Override 128 113 public int hashCode() { 129 return Objects.hash(getPoints()); 114 // This was Objects.hash(getPoints()), but that made 8-24MB of allocations on application startup 115 return 31 + hashPolygon(this.coords); // Arrays#hash, new array instantiation 130 116 } 131 117 … … 135 121 if (obj == null || getClass() != obj.getClass()) return false; 136 122 Shape shape = (Shape) obj; 137 return Objects.equals(getPoints(), shape.getPoints());123 return equalsPolygon(this.coords, shape.coords); 138 124 } 139 125 … … 142 128 return "Shape{coords=" + getPoints() + '}'; 143 129 } 130 131 /** 132 * Hash a polygon 133 * @param coords The polygon to hash 134 * @return The hashcode to use; equivalent to {@link Polygon#hashCode()}, but zero allocations. 135 */ 136 private static int hashPolygon(Polygon coords) { 137 // This is faster than coords.hashCode() by ~90% and performs effectively no memory allocations. 138 // This was originally written to replace Objects.hash(getPoints()). The only difference is +31 on the return. 139 // Objects.hash(getPoints()) -> Arrays.hash(getPoints()) -> sum of hashes of Coordinates 140 // First, AbstractList#hashCode equivalent 141 int hashCode = 1; 142 for (int index = 0; index < coords.npoints; index++) { 143 final double lat = coords.ypoints[index] / LatLon.MAX_SERVER_INV_PRECISION; 144 final double lon = coords.xpoints[index] / LatLon.MAX_SERVER_INV_PRECISION; 145 // Coordinate uses Object.hash(x, y) - new array instantiation *and* two Double instantiations 146 // Double conversion is 3.22MB 147 // The array creation for Object.hash(x, y) is 2.11 MB 148 final int coordinateHash = 31 * (31 + Double.hashCode(lon)) + Double.hashCode(lat); 149 hashCode = 31 * hashCode + coordinateHash; // hashCode * 31 + coordinate.hashCode() 150 } 151 return hashCode; 152 } 153 154 /** 155 * Check that two {@link Polygon}s are equal 156 * @param first The first polygon to check 157 * @param second The second polygon to check 158 * @return {@code true} if the polygons are equal 159 */ 160 private static boolean equalsPolygon(Polygon first, Polygon second) { 161 // If the coordinate lists aren't the same size, short circuit. 162 // We aren't doing fuzzy comparisons here. 163 if (first.npoints != second.npoints) { 164 return false; 165 } 166 for (int i = 0; i < first.npoints; i++) { 167 if (first.xpoints[i] != second.xpoints[i] || 168 first.ypoints[i] != second.ypoints[i]) { 169 return false; 170 } 171 } 172 return true; 173 } 174 175 /** 176 * A list of {@link Coordinate}s that attempts to be very efficient in terms of CPU time and memory allocations. 177 */ 178 private static final class CoordinateList extends AbstractList<Coordinate> { 179 private final Polygon coords; 180 181 CoordinateList(Polygon coords) { 182 this.coords = coords; 183 } 184 185 @Override 186 public Coordinate get(int index) { 187 double lat = coords.ypoints[index] / LatLon.MAX_SERVER_INV_PRECISION; 188 double lon = coords.xpoints[index] / LatLon.MAX_SERVER_INV_PRECISION; 189 return new Coordinate(lat, lon); 190 } 191 192 @Override 193 public int size() { 194 return coords.npoints; 195 } 196 197 @Override 198 public boolean equals(Object o) { 199 if (o instanceof CoordinateList) { 200 CoordinateList other = (CoordinateList) o; 201 return equalsPolygon(this.coords, other.coords); 202 } 203 return super.equals(o); 204 } 205 206 @Override 207 public int hashCode() { 208 return hashPolygon(this.coords); 209 } 210 } 144 211 } -
trunk/test/unit/org/openstreetmap/josm/data/imagery/ShapeTest.java
r18341 r18777 5 5 import static org.junit.jupiter.api.Assertions.assertEquals; 6 6 7 import java.awt.Polygon; 8 import java.util.ArrayList; 7 9 import java.util.Arrays; 10 import java.util.Objects; 8 11 12 import nl.jqno.equalsverifier.EqualsVerifier; 9 13 import org.junit.jupiter.api.Test; 10 14 import org.junit.jupiter.params.ParameterizedTest; … … 53 57 () -> assertEquals(coordinate, shape.getPoints().get(1).getLat())); 54 58 } 59 60 /** 61 * Ensure that the hashcode is semantically the same as what it previously was 62 */ 63 @ParameterizedTest 64 @ValueSource(strings = {"47.1 11.1 47.2 11.2 47.3 11.3", "-47.1 -11.1 -47.2 -11.2 -47.3 -11.3"}) 65 void testHashCode(String shapeString) { 66 final Shape shape = new Shape(shapeString, " "); 67 assertEquals(Objects.hash(shape.getPoints()), shape.hashCode(), 68 "The hashcode for shape should be the same as that for the point list (specific coord list)"); 69 assertEquals(Objects.hash(new ArrayList<>(shape.getPoints())), shape.hashCode(), 70 "The hashcode for shape should be the same as that for the point list (non-specific)"); 71 } 72 73 @Test 74 void testEqualsHashCodeContract() { 75 EqualsVerifier.simple().forClass(Shape.class) 76 .withNonnullFields("coords") 77 .withPrefabValues(Polygon.class, new Polygon(), new Polygon(new int[] {1, 2, 3}, new int[]{4, 5, 6}, 3)) 78 .verify(); 79 } 55 80 }
Note:
See TracChangeset
for help on using the changeset viewer.