Committed by
Gerrit Code Review
Implement compact serialization for a set of discrete resources
This is for ONOS-4281. Change-Id: I08a9fc4fd334c499c7a09d2960145743a798094e
Showing
6 changed files
with
412 additions
and
1 deletions
1 | +/* | ||
2 | + * Copyright 2016-present Open Networking Laboratory | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.onosproject.net.resource; | ||
17 | + | ||
18 | +import com.google.common.annotations.Beta; | ||
19 | +import com.google.common.collect.ImmutableSet; | ||
20 | + | ||
21 | +import java.util.Objects; | ||
22 | +import java.util.Optional; | ||
23 | +import java.util.Set; | ||
24 | + | ||
25 | +import static com.google.common.base.Preconditions.checkArgument; | ||
26 | +import static com.google.common.base.Preconditions.checkNotNull; | ||
27 | +import static com.google.common.base.Preconditions.checkState; | ||
28 | + | ||
29 | +/** | ||
30 | + * Represents a set of discrete type resources. | ||
31 | + * This class is intended to be used by ConsistentResourceStore though it is exposed to the public. | ||
32 | + */ | ||
33 | +@Beta | ||
34 | +public final class DiscreteResourceSet { | ||
35 | + private final Set<DiscreteResource> values; | ||
36 | + private final DiscreteResourceCodec codec; | ||
37 | + | ||
38 | + /** | ||
39 | + * Creates an instance with resources and the codec for them. | ||
40 | + * | ||
41 | + * @param values resources to be contained in the instance | ||
42 | + * @param codec codec for the specified resources | ||
43 | + * @return an instance | ||
44 | + */ | ||
45 | + public static DiscreteResourceSet of(Set<DiscreteResource> values, DiscreteResourceCodec codec) { | ||
46 | + checkNotNull(values); | ||
47 | + checkNotNull(codec); | ||
48 | + checkArgument(!values.isEmpty()); | ||
49 | + | ||
50 | + return new DiscreteResourceSet(ImmutableSet.copyOf(values), codec); | ||
51 | + } | ||
52 | + | ||
53 | + /** | ||
54 | + * Creates the instance representing an empty resource set. | ||
55 | + * | ||
56 | + * @return an empty resource set | ||
57 | + */ | ||
58 | + public static DiscreteResourceSet empty() { | ||
59 | + return new DiscreteResourceSet(ImmutableSet.of(), NoOpCodec.INSTANCE); | ||
60 | + } | ||
61 | + | ||
62 | + private DiscreteResourceSet(Set<DiscreteResource> values, DiscreteResourceCodec codec) { | ||
63 | + this.values = values; | ||
64 | + this.codec = codec; | ||
65 | + } | ||
66 | + | ||
67 | + private DiscreteResourceSet() { | ||
68 | + this.values = null; | ||
69 | + this.codec = null; | ||
70 | + } | ||
71 | + | ||
72 | + /** | ||
73 | + * Returns resources contained in this instance. | ||
74 | + * | ||
75 | + * @return resources | ||
76 | + */ | ||
77 | + public Set<DiscreteResource> values() { | ||
78 | + return values; | ||
79 | + } | ||
80 | + | ||
81 | + /** | ||
82 | + * Returns the parent resource of the resources contained in this instance. | ||
83 | + * | ||
84 | + * @return the parent resource of the resources | ||
85 | + */ | ||
86 | + public DiscreteResourceId parent() { | ||
87 | + if (values.isEmpty()) { | ||
88 | + // Dummy value avoiding null | ||
89 | + return ResourceId.ROOT; | ||
90 | + } | ||
91 | + Optional<DiscreteResourceId> parent = values.iterator().next().id().parent(); | ||
92 | + checkState(parent.isPresent()); | ||
93 | + | ||
94 | + return parent.get(); | ||
95 | + } | ||
96 | + | ||
97 | + /** | ||
98 | + * Returns the codec for the resources contained in this instance. | ||
99 | + * | ||
100 | + * @return the codec for the resources | ||
101 | + */ | ||
102 | + public DiscreteResourceCodec codec() { | ||
103 | + return codec; | ||
104 | + } | ||
105 | + | ||
106 | + @Override | ||
107 | + public int hashCode() { | ||
108 | + return Objects.hash(values, codec); | ||
109 | + } | ||
110 | + | ||
111 | + @Override | ||
112 | + public boolean equals(Object obj) { | ||
113 | + if (this == obj) { | ||
114 | + return true; | ||
115 | + } | ||
116 | + | ||
117 | + if (obj == null || getClass() != obj.getClass()) { | ||
118 | + return false; | ||
119 | + } | ||
120 | + | ||
121 | + final DiscreteResourceSet other = (DiscreteResourceSet) obj; | ||
122 | + return Objects.equals(this.values, other.values) | ||
123 | + && Objects.equals(this.codec, other.codec); | ||
124 | + } | ||
125 | + | ||
126 | +} |
1 | +/* | ||
2 | + * Copyright 2016-present Open Networking Laboratory | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.onosproject.net.resource; | ||
17 | + | ||
18 | +import com.esotericsoftware.kryo.Kryo; | ||
19 | +import com.esotericsoftware.kryo.Serializer; | ||
20 | +import com.esotericsoftware.kryo.io.Input; | ||
21 | +import com.esotericsoftware.kryo.io.Output; | ||
22 | +import com.google.common.annotations.Beta; | ||
23 | +import com.google.common.collect.DiscreteDomain; | ||
24 | +import com.google.common.collect.Range; | ||
25 | +import com.google.common.collect.TreeRangeSet; | ||
26 | +import org.onlab.util.ClosedOpenRange; | ||
27 | + | ||
28 | +import java.util.ArrayList; | ||
29 | +import java.util.List; | ||
30 | +import java.util.Set; | ||
31 | +import java.util.stream.Collectors; | ||
32 | +import java.util.stream.IntStream; | ||
33 | + | ||
34 | +/** | ||
35 | + * Kryo serializer for {@link DiscreteResourceSet}. | ||
36 | + */ | ||
37 | +@Beta | ||
38 | +public final class DiscreteResourceSetSerializer extends Serializer<DiscreteResourceSet> { | ||
39 | + | ||
40 | + public DiscreteResourceSetSerializer() { | ||
41 | + super(false, true); | ||
42 | + } | ||
43 | + | ||
44 | + @Override | ||
45 | + public void write(Kryo kryo, Output output, DiscreteResourceSet object) { | ||
46 | + TreeRangeSet<Integer> rangeSet = TreeRangeSet.create(); | ||
47 | + object.values().stream() | ||
48 | + .map(x -> object.codec().encode(x)) | ||
49 | + .map(Range::singleton) | ||
50 | + .map(x -> x.canonical(DiscreteDomain.integers())) | ||
51 | + .forEach(rangeSet::add); | ||
52 | + List<ClosedOpenRange> ranges = rangeSet.asRanges().stream() | ||
53 | + .map(ClosedOpenRange::of) | ||
54 | + .collect(Collectors.toList()); | ||
55 | + kryo.writeObject(output, ranges); | ||
56 | + kryo.writeClassAndObject(output, object.codec()); | ||
57 | + kryo.writeObject(output, object.parent()); | ||
58 | + } | ||
59 | + | ||
60 | + @Override | ||
61 | + public DiscreteResourceSet read(Kryo kryo, Input input, Class<DiscreteResourceSet> type) { | ||
62 | + @SuppressWarnings("unchecked") | ||
63 | + List<ClosedOpenRange> ranges = kryo.readObject(input, ArrayList.class); | ||
64 | + DiscreteResourceCodec codec = (DiscreteResourceCodec) kryo.readClassAndObject(input); | ||
65 | + DiscreteResourceId parent = kryo.readObject(input, DiscreteResourceId.class); | ||
66 | + | ||
67 | + if (ranges.isEmpty()) { | ||
68 | + return DiscreteResourceSet.empty(); | ||
69 | + } | ||
70 | + | ||
71 | + Set<DiscreteResource> resources = ranges.stream() | ||
72 | + .flatMapToInt(x -> IntStream.range(x.lowerBound(), x.upperBound())) | ||
73 | + .mapToObj(x -> codec.decode(parent, x)) | ||
74 | + .collect(Collectors.toSet()); | ||
75 | + | ||
76 | + return DiscreteResourceSet.of(resources, codec); | ||
77 | + } | ||
78 | +} |
1 | +/* | ||
2 | + * Copyright 2016-present Open Networking Laboratory | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.onosproject.net.resource; | ||
17 | + | ||
18 | +/** | ||
19 | + * Represents no-op codec intended to used in an empty discrete resource set only. | ||
20 | + * It's not supposed to be used by other classes. | ||
21 | + */ | ||
22 | +public class NoOpCodec implements DiscreteResourceCodec { | ||
23 | + public static final DiscreteResourceCodec INSTANCE = new NoOpCodec(); | ||
24 | + | ||
25 | + @Override | ||
26 | + public int encode(DiscreteResource resource) { | ||
27 | + return 0; | ||
28 | + } | ||
29 | + | ||
30 | + @Override | ||
31 | + public DiscreteResource decode(DiscreteResourceId parent, int value) { | ||
32 | + return Resource.ROOT; | ||
33 | + } | ||
34 | + | ||
35 | + @Override | ||
36 | + public boolean equals(Object obj) { | ||
37 | + if (obj == this) { | ||
38 | + return true; | ||
39 | + } | ||
40 | + | ||
41 | + if (obj == null || getClass() != obj.getClass()) { | ||
42 | + return false; | ||
43 | + } | ||
44 | + | ||
45 | + return true; | ||
46 | + } | ||
47 | + | ||
48 | + @Override | ||
49 | + public int hashCode() { | ||
50 | + return NoOpCodec.class.hashCode(); | ||
51 | + } | ||
52 | +} |
... | @@ -31,6 +31,7 @@ import org.onlab.packet.MacAddress; | ... | @@ -31,6 +31,7 @@ import org.onlab.packet.MacAddress; |
31 | import org.onlab.packet.TpPort; | 31 | import org.onlab.packet.TpPort; |
32 | import org.onlab.packet.VlanId; | 32 | import org.onlab.packet.VlanId; |
33 | import org.onlab.util.Bandwidth; | 33 | import org.onlab.util.Bandwidth; |
34 | +import org.onlab.util.ClosedOpenRange; | ||
34 | import org.onlab.util.Frequency; | 35 | import org.onlab.util.Frequency; |
35 | import org.onlab.util.KryoNamespace; | 36 | import org.onlab.util.KryoNamespace; |
36 | import org.onlab.util.Match; | 37 | import org.onlab.util.Match; |
... | @@ -191,12 +192,18 @@ import org.onosproject.net.meter.MeterId; | ... | @@ -191,12 +192,18 @@ import org.onosproject.net.meter.MeterId; |
191 | import org.onosproject.net.resource.ContinuousResource; | 192 | import org.onosproject.net.resource.ContinuousResource; |
192 | import org.onosproject.net.resource.ContinuousResourceId; | 193 | import org.onosproject.net.resource.ContinuousResourceId; |
193 | import org.onosproject.net.resource.DiscreteResource; | 194 | import org.onosproject.net.resource.DiscreteResource; |
195 | +import org.onosproject.net.resource.DiscreteResourceCodec; | ||
194 | import org.onosproject.net.resource.DiscreteResourceId; | 196 | import org.onosproject.net.resource.DiscreteResourceId; |
197 | +import org.onosproject.net.resource.DiscreteResourceSet; | ||
198 | +import org.onosproject.net.resource.DiscreteResourceSetSerializer; | ||
199 | +import org.onosproject.net.resource.MplsCodec; | ||
200 | +import org.onosproject.net.resource.NoOpCodec; | ||
195 | import org.onosproject.net.resource.ResourceAllocation; | 201 | import org.onosproject.net.resource.ResourceAllocation; |
196 | import org.onosproject.net.packet.DefaultOutboundPacket; | 202 | import org.onosproject.net.packet.DefaultOutboundPacket; |
197 | import org.onosproject.net.packet.DefaultPacketRequest; | 203 | import org.onosproject.net.packet.DefaultPacketRequest; |
198 | import org.onosproject.net.packet.PacketPriority; | 204 | import org.onosproject.net.packet.PacketPriority; |
199 | import org.onosproject.net.provider.ProviderId; | 205 | import org.onosproject.net.provider.ProviderId; |
206 | +import org.onosproject.net.resource.VlanCodec; | ||
200 | import org.onosproject.security.Permission; | 207 | import org.onosproject.security.Permission; |
201 | import org.onosproject.store.Timestamp; | 208 | import org.onosproject.store.Timestamp; |
202 | import org.onosproject.store.primitives.MapUpdate; | 209 | import org.onosproject.store.primitives.MapUpdate; |
... | @@ -521,7 +528,12 @@ public final class KryoNamespaces { | ... | @@ -521,7 +528,12 @@ public final class KryoNamespaces { |
521 | org.onlab.packet.MplsLabel.class, | 528 | org.onlab.packet.MplsLabel.class, |
522 | org.onlab.packet.MPLS.class | 529 | org.onlab.packet.MPLS.class |
523 | ) | 530 | ) |
524 | - | 531 | + .register(ClosedOpenRange.class) |
532 | + .register(new DiscreteResourceSetSerializer(), DiscreteResourceSet.class) | ||
533 | + .register(DiscreteResourceCodec.class) | ||
534 | + .register(VlanCodec.class) | ||
535 | + .register(MplsCodec.class) | ||
536 | + .register(NoOpCodec.class) | ||
525 | .build(); | 537 | .build(); |
526 | 538 | ||
527 | 539 | ... | ... |
... | @@ -24,6 +24,7 @@ import org.junit.After; | ... | @@ -24,6 +24,7 @@ import org.junit.After; |
24 | import org.junit.Before; | 24 | import org.junit.Before; |
25 | import org.junit.BeforeClass; | 25 | import org.junit.BeforeClass; |
26 | import org.junit.Test; | 26 | import org.junit.Test; |
27 | +import org.onlab.packet.MplsLabel; | ||
27 | import org.onlab.packet.VlanId; | 28 | import org.onlab.packet.VlanId; |
28 | import org.onlab.util.Bandwidth; | 29 | import org.onlab.util.Bandwidth; |
29 | import org.onlab.util.Frequency; | 30 | import org.onlab.util.Frequency; |
... | @@ -62,6 +63,9 @@ import org.onosproject.net.flow.FlowId; | ... | @@ -62,6 +63,9 @@ import org.onosproject.net.flow.FlowId; |
62 | import org.onosproject.net.flow.FlowRule; | 63 | import org.onosproject.net.flow.FlowRule; |
63 | import org.onosproject.net.flow.FlowRuleBatchEntry; | 64 | import org.onosproject.net.flow.FlowRuleBatchEntry; |
64 | import org.onosproject.net.intent.IntentId; | 65 | import org.onosproject.net.intent.IntentId; |
66 | +import org.onosproject.net.resource.DiscreteResource; | ||
67 | +import org.onosproject.net.resource.DiscreteResourceSet; | ||
68 | +import org.onosproject.net.resource.MplsCodec; | ||
65 | import org.onosproject.net.resource.ResourceAllocation; | 69 | import org.onosproject.net.resource.ResourceAllocation; |
66 | import org.onosproject.net.resource.Resources; | 70 | import org.onosproject.net.resource.Resources; |
67 | import org.onosproject.net.provider.ProviderId; | 71 | import org.onosproject.net.provider.ProviderId; |
... | @@ -80,11 +84,15 @@ import org.onlab.packet.Ip4Prefix; | ... | @@ -80,11 +84,15 @@ import org.onlab.packet.Ip4Prefix; |
80 | import org.onlab.packet.Ip6Prefix; | 84 | import org.onlab.packet.Ip6Prefix; |
81 | import org.onlab.packet.MacAddress; | 85 | import org.onlab.packet.MacAddress; |
82 | import org.onlab.util.KryoNamespace; | 86 | import org.onlab.util.KryoNamespace; |
87 | +import org.onosproject.net.resource.VlanCodec; | ||
83 | 88 | ||
84 | import java.nio.ByteBuffer; | 89 | import java.nio.ByteBuffer; |
85 | import java.util.Arrays; | 90 | import java.util.Arrays; |
86 | import java.util.Collections; | 91 | import java.util.Collections; |
87 | import java.time.Duration; | 92 | import java.time.Duration; |
93 | +import java.util.Set; | ||
94 | +import java.util.stream.Collectors; | ||
95 | +import java.util.stream.IntStream; | ||
88 | 96 | ||
89 | import static java.util.Arrays.asList; | 97 | import static java.util.Arrays.asList; |
90 | import static org.junit.Assert.*; | 98 | import static org.junit.Assert.*; |
... | @@ -356,6 +364,38 @@ public class KryoSerializerTest { | ... | @@ -356,6 +364,38 @@ public class KryoSerializerTest { |
356 | } | 364 | } |
357 | 365 | ||
358 | @Test | 366 | @Test |
367 | + public void testVlanIdResourceSet() { | ||
368 | + DiscreteResource port = Resources.discrete(DID1, P1).resource(); | ||
369 | + | ||
370 | + Set<DiscreteResource> vlans = IntStream.range(0, 4096) | ||
371 | + .mapToObj(x -> VlanId.vlanId((short) x)) | ||
372 | + .map(x -> Resources.discrete(port.id(), x).resource()) | ||
373 | + .collect(Collectors.toSet()); | ||
374 | + | ||
375 | + DiscreteResourceSet sut = DiscreteResourceSet.of(vlans, new VlanCodec()); | ||
376 | + testSerializedEquals(sut); | ||
377 | + } | ||
378 | + | ||
379 | + @Test | ||
380 | + public void testMplsLabelResourceSet() { | ||
381 | + DiscreteResource port = Resources.discrete(DID1, P1).resource(); | ||
382 | + | ||
383 | + Set<DiscreteResource> labels = IntStream.range(0, 1024 * 1024) | ||
384 | + .mapToObj(MplsLabel::mplsLabel) | ||
385 | + .map(x -> Resources.discrete(port.id(), x).resource()) | ||
386 | + .collect(Collectors.toSet()); | ||
387 | + | ||
388 | + DiscreteResourceSet sut = DiscreteResourceSet.of(labels, new MplsCodec()); | ||
389 | + testSerializedEquals(sut); | ||
390 | + } | ||
391 | + | ||
392 | + @Test | ||
393 | + public void testEmptyResourceSet() { | ||
394 | + DiscreteResourceSet sut = DiscreteResourceSet.empty(); | ||
395 | + testSerializedEquals(sut); | ||
396 | + } | ||
397 | + | ||
398 | + @Test | ||
359 | public void testResourceId() { | 399 | public void testResourceId() { |
360 | testSerializedEquals(Resources.discrete(DID1, P1).id()); | 400 | testSerializedEquals(Resources.discrete(DID1, P1).id()); |
361 | } | 401 | } | ... | ... |
1 | +/* | ||
2 | + * Copyright 2016-present Open Networking Laboratory | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.onlab.util; | ||
17 | + | ||
18 | +import com.google.common.annotations.Beta; | ||
19 | +import com.google.common.collect.DiscreteDomain; | ||
20 | +import com.google.common.collect.Range; | ||
21 | + | ||
22 | +import java.util.Objects; | ||
23 | + | ||
24 | +/** | ||
25 | + * Represent a closed-open range. | ||
26 | + * The primary user of this class is the ResourceService implementation. | ||
27 | + */ | ||
28 | +@Beta | ||
29 | +public final class ClosedOpenRange { | ||
30 | + private final int lowerBound; // inclusive | ||
31 | + private final int upperBound; // exclusive | ||
32 | + | ||
33 | + /** | ||
34 | + * Creates a range from a Guava's range. | ||
35 | + * | ||
36 | + * @param range Guava's range | ||
37 | + * @return this range | ||
38 | + */ | ||
39 | + public static ClosedOpenRange of(Range<Integer> range) { | ||
40 | + return new ClosedOpenRange( | ||
41 | + range.canonical(DiscreteDomain.integers()).lowerEndpoint(), | ||
42 | + range.canonical(DiscreteDomain.integers()).upperEndpoint()); | ||
43 | + } | ||
44 | + | ||
45 | + /** | ||
46 | + * Create a range with a lower bound and an upper bound. | ||
47 | + * | ||
48 | + * @param lowerBound lower bound (inclusive) | ||
49 | + * @param upperBound upper bound (exclusive) | ||
50 | + * @return this range | ||
51 | + */ | ||
52 | + public static ClosedOpenRange of(int lowerBound, int upperBound) { | ||
53 | + return new ClosedOpenRange(lowerBound, upperBound); | ||
54 | + } | ||
55 | + | ||
56 | + private ClosedOpenRange(int lowerBound, int upperBound) { | ||
57 | + this.lowerBound = lowerBound; | ||
58 | + this.upperBound = upperBound; | ||
59 | + } | ||
60 | + | ||
61 | + /** | ||
62 | + * Returns the lower bound. | ||
63 | + * | ||
64 | + * @return the lower bound | ||
65 | + */ | ||
66 | + public int lowerBound() { | ||
67 | + return lowerBound; | ||
68 | + } | ||
69 | + | ||
70 | + /** | ||
71 | + * Returns the upper bound. | ||
72 | + * | ||
73 | + * @return the upper bound | ||
74 | + */ | ||
75 | + public int upperBound() { | ||
76 | + return upperBound; | ||
77 | + } | ||
78 | + | ||
79 | + @Override | ||
80 | + public int hashCode() { | ||
81 | + return Objects.hash(lowerBound, upperBound); | ||
82 | + } | ||
83 | + | ||
84 | + @Override | ||
85 | + public boolean equals(Object obj) { | ||
86 | + if (this == obj) { | ||
87 | + return true; | ||
88 | + } | ||
89 | + | ||
90 | + if (!(obj instanceof ClosedOpenRange)) { | ||
91 | + return false; | ||
92 | + } | ||
93 | + | ||
94 | + final ClosedOpenRange other = (ClosedOpenRange) obj; | ||
95 | + return Objects.equals(this.lowerBound, other.lowerBound) | ||
96 | + && Objects.equals(this.upperBound, other.upperBound); | ||
97 | + } | ||
98 | + | ||
99 | + @Override | ||
100 | + public String toString() { | ||
101 | + return "[" + lowerBound + ".." + upperBound + ")"; | ||
102 | + } | ||
103 | +} |
-
Please register or login to post a comment