From 8dca77634125cbaf53365000d1c9f7fb32f6dcea Mon Sep 17 00:00:00 2001 From: Google Java Core Libraries Date: Mon, 22 Jan 2024 09:26:55 -0800 Subject: [PATCH] change behavior of views returned by graph accessor methods that take a graph element as input: they now throw IllegalStateException when that element is removed from the graph RELNOTES=change behavior of views returned by graph accessor methods that take a graph element as input: they now throw IllegalStateException when that element is removed from the graph PiperOrigin-RevId: 600480069 --- .../common/graph/AbstractGraphTest.java | 91 +++++++--- .../common/graph/AbstractNetworkTest.java | 138 ++++++++++----- .../common/graph/InvalidatableSetTest.java | 60 +++++++ .../com/google/common/graph/TestUtil.java | 13 +- .../common/graph/AbstractBaseGraph.java | 57 ++++--- .../google/common/graph/AbstractNetwork.java | 33 +++- .../com/google/common/graph/BaseGraph.java | 61 ++++++- .../src/com/google/common/graph/Graph.java | 61 ++++++- .../google/common/graph/GraphConstants.java | 6 + .../google/common/graph/InvalidatableSet.java | 54 ++++++ .../src/com/google/common/graph/Network.java | 157 ++++++++++++++--- .../google/common/graph/StandardNetwork.java | 14 +- .../common/graph/StandardValueGraph.java | 21 +-- .../com/google/common/graph/ValueGraph.java | 61 +++++-- .../common/graph/AbstractGraphTest.java | 91 +++++++--- .../common/graph/AbstractNetworkTest.java | 138 ++++++++++----- .../common/graph/InvalidatableSetTest.java | 60 +++++++ .../com/google/common/graph/TestUtil.java | 13 +- .../common/graph/AbstractBaseGraph.java | 57 ++++--- .../google/common/graph/AbstractNetwork.java | 33 +++- .../com/google/common/graph/BaseGraph.java | 61 ++++++- guava/src/com/google/common/graph/Graph.java | 61 ++++++- .../google/common/graph/GraphConstants.java | 6 + .../google/common/graph/InvalidatableSet.java | 54 ++++++ .../src/com/google/common/graph/Network.java | 161 +++++++++++++++--- .../google/common/graph/StandardNetwork.java | 14 +- .../common/graph/StandardValueGraph.java | 21 +-- .../com/google/common/graph/ValueGraph.java | 60 +++++-- 28 files changed, 1329 insertions(+), 328 deletions(-) create mode 100644 android/guava-tests/test/com/google/common/graph/InvalidatableSetTest.java create mode 100644 android/guava/src/com/google/common/graph/InvalidatableSet.java create mode 100644 guava-tests/test/com/google/common/graph/InvalidatableSetTest.java create mode 100644 guava/src/com/google/common/graph/InvalidatableSet.java diff --git a/android/guava-tests/test/com/google/common/graph/AbstractGraphTest.java b/android/guava-tests/test/com/google/common/graph/AbstractGraphTest.java index a8209244c037..8b4b5076cdf7 100644 --- a/android/guava-tests/test/com/google/common/graph/AbstractGraphTest.java +++ b/android/guava-tests/test/com/google/common/graph/AbstractGraphTest.java @@ -17,6 +17,7 @@ package com.google.common.graph; import static com.google.common.graph.TestUtil.assertNodeNotInGraphErrorMessage; +import static com.google.common.graph.TestUtil.assertNodeRemovedFromGraphErrorMessage; import static com.google.common.graph.TestUtil.assertStronglyEquivalent; import static com.google.common.graph.TestUtil.sanityCheckSet; import static com.google.common.truth.Truth.assertThat; @@ -234,9 +235,8 @@ public void adjacentNodes_noAdjacentNodes() { @Test public void adjacentNodes_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(NODE_NOT_IN_GRAPH))); } @Test @@ -247,9 +247,8 @@ public void predecessors_noPredecessors() { @Test public void predecessors_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.predecessors(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.predecessors(NODE_NOT_IN_GRAPH))); } @Test @@ -260,9 +259,8 @@ public void successors_noSuccessors() { @Test public void successors_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.successors(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.successors(NODE_NOT_IN_GRAPH))); } @Test @@ -273,9 +271,8 @@ public void incidentEdges_noIncidentEdges() { @Test public void incidentEdges_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.incidentEdges(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.incidentEdges(NODE_NOT_IN_GRAPH))); } @Test @@ -293,9 +290,8 @@ public void degree_isolatedNode() { @Test public void degree_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.degree(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.degree(NODE_NOT_IN_GRAPH))); } @Test @@ -306,9 +302,8 @@ public void inDegree_isolatedNode() { @Test public void inDegree_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.inDegree(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.inDegree(NODE_NOT_IN_GRAPH))); } @Test @@ -319,9 +314,8 @@ public void outDegree_isolatedNode() { @Test public void outDegree_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.outDegree(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.outDegree(NODE_NOT_IN_GRAPH))); } @Test @@ -351,8 +345,24 @@ public void removeNode_existingNode() { assertThat(graphAsMutableGraph.removeNode(N1)).isTrue(); assertThat(graphAsMutableGraph.removeNode(N1)).isFalse(); assertThat(graph.nodes()).containsExactly(N2, N4); + assertThat(graph.adjacentNodes(N2)).isEmpty(); + assertThat(graph.predecessors(N2)).isEmpty(); + assertThat(graph.successors(N2)).isEmpty(); + assertThat(graph.incidentEdges(N2)).isEmpty(); assertThat(graph.adjacentNodes(N4)).isEmpty(); + assertThat(graph.predecessors(N4)).isEmpty(); + assertThat(graph.successors(N4)).isEmpty(); + assertThat(graph.incidentEdges(N4)).isEmpty(); + + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.predecessors(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.successors(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.incidentEdges(N1))); } @Test @@ -382,19 +392,48 @@ public void removeNode_nodeNotPresent() { } @Test - public void removeNode_queryAfterRemoval() { + public void queryAccessorSetAfterElementRemoval() { assume().that(graphIsMutable()).isTrue(); putEdge(N1, N2); putEdge(N2, N1); Set n1AdjacentNodes = graph.adjacentNodes(N1); Set n2AdjacentNodes = graph.adjacentNodes(N2); + Set n1Predecessors = graph.predecessors(N1); + Set n2Predecessors = graph.predecessors(N2); + Set n1Successors = graph.successors(N1); + Set n2Successors = graph.successors(N2); + Set> n1IncidentEdges = graph.incidentEdges(N1); + Set> n2IncidentEdges = graph.incidentEdges(N2); assertThat(graphAsMutableGraph.removeNode(N1)).isTrue(); - assertThat(n1AdjacentNodes).isEmpty(); + + // The choice of the size() method to call here is arbitrary. We assume that if any of the Set + // methods executes the validation check, they all will, and thus we only need to test one of + // them to ensure that the validation check happens and has the expected behavior. + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1AdjacentNodes::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1Predecessors::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1Successors::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1IncidentEdges::size)); + assertThat(n2AdjacentNodes).isEmpty(); - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(N1)); - assertNodeNotInGraphErrorMessage(e); + assertThat(n2Predecessors).isEmpty(); + assertThat(n2Successors).isEmpty(); + assertThat(n2IncidentEdges).isEmpty(); + } + + @Test + public void queryGraphAfterElementRemoval() { + assume().that(graphIsMutable()).isTrue(); + + putEdge(N1, N2); + putEdge(N2, N1); + assertThat(graphAsMutableGraph.removeNode(N1)).isTrue(); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(N1))); } @Test diff --git a/android/guava-tests/test/com/google/common/graph/AbstractNetworkTest.java b/android/guava-tests/test/com/google/common/graph/AbstractNetworkTest.java index b9558f706e86..7313e8835ba8 100644 --- a/android/guava-tests/test/com/google/common/graph/AbstractNetworkTest.java +++ b/android/guava-tests/test/com/google/common/graph/AbstractNetworkTest.java @@ -17,7 +17,9 @@ package com.google.common.graph; import static com.google.common.graph.TestUtil.assertEdgeNotInGraphErrorMessage; +import static com.google.common.graph.TestUtil.assertEdgeRemovedFromGraphErrorMessage; import static com.google.common.graph.TestUtil.assertNodeNotInGraphErrorMessage; +import static com.google.common.graph.TestUtil.assertNodeRemovedFromGraphErrorMessage; import static com.google.common.graph.TestUtil.assertStronglyEquivalent; import static com.google.common.graph.TestUtil.sanityCheckSet; import static com.google.common.truth.Truth.assertThat; @@ -416,10 +418,9 @@ public void incidentEdges_isolatedNode() { @Test public void incidentEdges_nodeNotInGraph() { - IllegalArgumentException e = + assertNodeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.incidentEdges(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> network.incidentEdges(NODE_NOT_IN_GRAPH))); } @Test @@ -430,10 +431,9 @@ public void incidentNodes_oneEdge() { @Test public void incidentNodes_edgeNotInGraph() { - IllegalArgumentException e = + assertEdgeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.incidentNodes(EDGE_NOT_IN_GRAPH)); - assertEdgeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> network.incidentNodes(EDGE_NOT_IN_GRAPH))); } @Test @@ -451,10 +451,9 @@ public void adjacentNodes_noAdjacentNodes() { @Test public void adjacentNodes_nodeNotInGraph() { - IllegalArgumentException e = + assertNodeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.adjacentNodes(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> network.adjacentNodes(NODE_NOT_IN_GRAPH))); } @Test @@ -475,10 +474,9 @@ public void adjacentEdges_noAdjacentEdges() { @Test public void adjacentEdges_edgeNotInGraph() { - IllegalArgumentException e = + assertEdgeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.adjacentEdges(EDGE_NOT_IN_GRAPH)); - assertEdgeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> network.adjacentEdges(EDGE_NOT_IN_GRAPH))); } @Test @@ -504,19 +502,16 @@ public void edgesConnecting_disconnectedNodes() { public void edgesConnecting_nodesNotInGraph() { addNode(N1); addNode(N2); - IllegalArgumentException e = + assertNodeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.edgesConnecting(N1, NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); - e = + IllegalArgumentException.class, () -> network.edgesConnecting(N1, NODE_NOT_IN_GRAPH))); + assertNodeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.edgesConnecting(NODE_NOT_IN_GRAPH, N2)); - assertNodeNotInGraphErrorMessage(e); - e = + IllegalArgumentException.class, () -> network.edgesConnecting(NODE_NOT_IN_GRAPH, N2))); + assertNodeNotInGraphErrorMessage( assertThrows( IllegalArgumentException.class, - () -> network.edgesConnecting(NODE_NOT_IN_GRAPH, NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + () -> network.edgesConnecting(NODE_NOT_IN_GRAPH, NODE_NOT_IN_GRAPH))); } @Test @@ -581,9 +576,8 @@ public void inEdges_noInEdges() { @Test public void inEdges_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> network.inEdges(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.inEdges(NODE_NOT_IN_GRAPH))); } @Test @@ -594,9 +588,8 @@ public void outEdges_noOutEdges() { @Test public void outEdges_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> network.outEdges(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.outEdges(NODE_NOT_IN_GRAPH))); } @Test @@ -607,9 +600,9 @@ public void predecessors_noPredecessors() { @Test public void predecessors_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> network.predecessors(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows( + IllegalArgumentException.class, () -> network.predecessors(NODE_NOT_IN_GRAPH))); } @Test @@ -620,9 +613,8 @@ public void successors_noSuccessors() { @Test public void successors_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> network.successors(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.successors(NODE_NOT_IN_GRAPH))); } @Test @@ -654,6 +646,28 @@ public void removeNode_existingNode() { assertThat(networkAsMutableNetwork.nodes()).containsExactly(N2, N4); assertThat(networkAsMutableNetwork.edges()).doesNotContain(E12); assertThat(networkAsMutableNetwork.edges()).doesNotContain(E41); + + assertThat(network.adjacentNodes(N2)).isEmpty(); + assertThat(network.predecessors(N2)).isEmpty(); + assertThat(network.successors(N2)).isEmpty(); + assertThat(network.incidentEdges(N2)).isEmpty(); + assertThat(network.inEdges(N2)).isEmpty(); + assertThat(network.outEdges(N2)).isEmpty(); + assertThat(network.adjacentNodes(N4)).isEmpty(); + assertThat(network.predecessors(N4)).isEmpty(); + assertThat(network.successors(N4)).isEmpty(); + assertThat(network.incidentEdges(N4)).isEmpty(); + assertThat(network.inEdges(N4)).isEmpty(); + assertThat(network.outEdges(N4)).isEmpty(); + + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.adjacentNodes(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.predecessors(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.successors(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.incidentEdges(N1))); } @Test @@ -667,19 +681,52 @@ public void removeNode_nodeNotPresent() { } @Test - public void removeNode_queryAfterRemoval() { + public void queryAccessorSetAfterElementRemoval() { assume().that(graphIsMutable()).isTrue(); addEdge(N1, N2, E12); - Set n1AdjacentNodes = networkAsMutableNetwork.adjacentNodes(N1); - Set n2AdjacentNodes = networkAsMutableNetwork.adjacentNodes(N2); - assertTrue(networkAsMutableNetwork.removeNode(N1)); - assertThat(n1AdjacentNodes).isEmpty(); + Set n1AdjacentNodes = network.adjacentNodes(N1); + Set n2AdjacentNodes = network.adjacentNodes(N2); + Set n1Predecessors = network.predecessors(N1); + Set n2Predecessors = network.predecessors(N2); + Set n1Successors = network.successors(N1); + Set n2Successors = network.successors(N2); + Set n1IncidentEdges = network.incidentEdges(N1); + Set n2IncidentEdges = network.incidentEdges(N2); + Set n1InEdges = network.inEdges(N1); + Set n2InEdges = network.inEdges(N2); + Set n1OutEdges = network.outEdges(N1); + Set n2OutEdges = network.outEdges(N2); + Set e12AdjacentEdges = network.adjacentEdges(E12); + Set n12EdgesConnecting = network.edgesConnecting(N1, N2); + assertThat(networkAsMutableNetwork.removeNode(N1)).isTrue(); + + // The choice of the size() method to call here is arbitrary. We assume that if any of the Set + // methods executes the validation check, they all will, and thus we only need to test one of + // them to ensure that the validation check happens and has the expected behavior. + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1AdjacentNodes::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1Predecessors::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1Successors::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1IncidentEdges::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1InEdges::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1OutEdges::size)); + assertEdgeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, e12AdjacentEdges::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n12EdgesConnecting::size)); + assertThat(n2AdjacentNodes).isEmpty(); - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, () -> networkAsMutableNetwork.adjacentNodes(N1)); - assertNodeNotInGraphErrorMessage(e); + assertThat(n2Predecessors).isEmpty(); + assertThat(n2Successors).isEmpty(); + assertThat(n2IncidentEdges).isEmpty(); + assertThat(n2InEdges).isEmpty(); + assertThat(n2OutEdges).isEmpty(); } @Test @@ -724,10 +771,9 @@ public void removeEdge_queryAfterRemoval() { EndpointPair unused = networkAsMutableNetwork.incidentNodes(E12); // ensure cache (if any) is populated assertTrue(networkAsMutableNetwork.removeEdge(E12)); - IllegalArgumentException e = + assertEdgeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> networkAsMutableNetwork.incidentNodes(E12)); - assertEdgeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> networkAsMutableNetwork.incidentNodes(E12))); } @Test @@ -807,7 +853,7 @@ public void concurrentIteration() throws Exception { * synchronization actions.) * * All that said: I haven't actually managed to make this particular test produce a TSAN error - * for the field accesses in MapIteratorCache. This teset *has* found other TSAN errors, + * for the field accesses in MapIteratorCache. This test *has* found other TSAN errors, * including in MapRetrievalCache, so I'm not sure why this one is different. I did at least * confirm that my change to MapIteratorCache fixes the TSAN error in the (larger) test it was * originally reported in. diff --git a/android/guava-tests/test/com/google/common/graph/InvalidatableSetTest.java b/android/guava-tests/test/com/google/common/graph/InvalidatableSetTest.java new file mode 100644 index 000000000000..1af877f39f87 --- /dev/null +++ b/android/guava-tests/test/com/google/common/graph/InvalidatableSetTest.java @@ -0,0 +1,60 @@ +package com.google.common.graph; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableSet; +import java.util.HashSet; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class InvalidatableSetTest { + Set wrappedSet; + Set copyOfWrappedSet; + InvalidatableSet setToTest; + + @Before + public void createSets() { + wrappedSet = new HashSet<>(); + wrappedSet.add(1); + wrappedSet.add(2); + wrappedSet.add(3); + + copyOfWrappedSet = ImmutableSet.copyOf(wrappedSet); + setToTest = + InvalidatableSet.of(wrappedSet, () -> wrappedSet.contains(1), () -> 1 + "is not present"); + } + + @Test + @SuppressWarnings("TruthSelfEquals") + public void testEquals() { + // sanity check on construction of copyOfWrappedSet + assertThat(wrappedSet).isEqualTo(copyOfWrappedSet); + + // test that setToTest is still valid + assertThat(setToTest).isEqualTo(wrappedSet); + assertThat(setToTest).isEqualTo(copyOfWrappedSet); + + // invalidate setToTest + wrappedSet.remove(1); + // sanity check on update of wrappedSet + assertThat(wrappedSet).isNotEqualTo(copyOfWrappedSet); + + ImmutableSet copyOfModifiedSet = ImmutableSet.copyOf(wrappedSet); // {2,3} + // sanity check on construction of copyOfModifiedSet + assertThat(wrappedSet).isEqualTo(copyOfModifiedSet); + + // setToTest should throw when it calls equals(), or equals is called on it, except for itself + assertThat(setToTest).isEqualTo(setToTest); + assertThrows(IllegalStateException.class, () -> setToTest.equals(wrappedSet)); + assertThrows(IllegalStateException.class, () -> setToTest.equals(copyOfWrappedSet)); + assertThrows(IllegalStateException.class, () -> setToTest.equals(copyOfModifiedSet)); + assertThrows(IllegalStateException.class, () -> wrappedSet.equals(setToTest)); + assertThrows(IllegalStateException.class, () -> copyOfWrappedSet.equals(setToTest)); + assertThrows(IllegalStateException.class, () -> copyOfModifiedSet.equals(setToTest)); + } +} diff --git a/android/guava-tests/test/com/google/common/graph/TestUtil.java b/android/guava-tests/test/com/google/common/graph/TestUtil.java index 68a2503e223f..95fc75296c3a 100644 --- a/android/guava-tests/test/com/google/common/graph/TestUtil.java +++ b/android/guava-tests/test/com/google/common/graph/TestUtil.java @@ -28,6 +28,7 @@ final class TestUtil { static final String ERROR_ELEMENT_NOT_IN_GRAPH = "not an element of this graph"; static final String ERROR_NODE_NOT_IN_GRAPH = "Should not be allowed to pass a node that is not an element of the graph."; + static final String ERROR_ELEMENT_REMOVED = "used to generate this set"; private static final String NODE_STRING = "Node"; private static final String EDGE_STRING = "Edge"; @@ -42,12 +43,22 @@ static void assertNodeNotInGraphErrorMessage(Throwable throwable) { assertThat(throwable).hasMessageThat().startsWith(NODE_STRING); assertThat(throwable).hasMessageThat().contains(ERROR_ELEMENT_NOT_IN_GRAPH); } - + static void assertEdgeNotInGraphErrorMessage(Throwable throwable) { assertThat(throwable).hasMessageThat().startsWith(EDGE_STRING); assertThat(throwable).hasMessageThat().contains(ERROR_ELEMENT_NOT_IN_GRAPH); } + static void assertNodeRemovedFromGraphErrorMessage(Throwable throwable) { + assertThat(throwable).hasMessageThat().startsWith(NODE_STRING); + assertThat(throwable).hasMessageThat().contains(ERROR_ELEMENT_REMOVED); + } + + static void assertEdgeRemovedFromGraphErrorMessage(Throwable throwable) { + assertThat(throwable).hasMessageThat().startsWith(EDGE_STRING); + assertThat(throwable).hasMessageThat().contains(ERROR_ELEMENT_REMOVED); + } + static void assertStronglyEquivalent(Graph graphA, Graph graphB) { // Properties not covered by equals() assertThat(graphA.allowsSelfLoops()).isEqualTo(graphB.allowsSelfLoops()); diff --git a/android/guava/src/com/google/common/graph/AbstractBaseGraph.java b/android/guava/src/com/google/common/graph/AbstractBaseGraph.java index 5adcc9216c7a..16cbdde7eec6 100644 --- a/android/guava/src/com/google/common/graph/AbstractBaseGraph.java +++ b/android/guava/src/com/google/common/graph/AbstractBaseGraph.java @@ -20,6 +20,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.graph.GraphConstants.ENDPOINTS_MISMATCH; +import static com.google.common.graph.GraphConstants.NODE_PAIR_REMOVED_FROM_GRAPH; +import static com.google.common.graph.GraphConstants.NODE_REMOVED_FROM_GRAPH; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterators; @@ -106,27 +108,30 @@ public ElementOrder incidentEdgeOrder() { public Set> incidentEdges(N node) { checkNotNull(node); checkArgument(nodes().contains(node), "Node %s is not an element of this graph.", node); - return new IncidentEdgeSet(this, node) { - @Override - public UnmodifiableIterator> iterator() { - if (graph.isDirected()) { - return Iterators.unmodifiableIterator( - Iterators.concat( - Iterators.transform( - graph.predecessors(node).iterator(), - (N predecessor) -> EndpointPair.ordered(predecessor, node)), + IncidentEdgeSet incident = + new IncidentEdgeSet(this, node) { + @Override + public UnmodifiableIterator> iterator() { + if (graph.isDirected()) { + return Iterators.unmodifiableIterator( + Iterators.concat( + Iterators.transform( + graph.predecessors(node).iterator(), + (N predecessor) -> EndpointPair.ordered(predecessor, node)), + Iterators.transform( + // filter out 'node' from successors (already covered by predecessors, + // above) + Sets.difference(graph.successors(node), ImmutableSet.of(node)).iterator(), + (N successor) -> EndpointPair.ordered(node, successor)))); + } else { + return Iterators.unmodifiableIterator( Iterators.transform( - // filter out 'node' from successors (already covered by predecessors, above) - Sets.difference(graph.successors(node), ImmutableSet.of(node)).iterator(), - (N successor) -> EndpointPair.ordered(node, successor)))); - } else { - return Iterators.unmodifiableIterator( - Iterators.transform( - graph.adjacentNodes(node).iterator(), - (N adjacentNode) -> EndpointPair.unordered(node, adjacentNode))); - } - } - }; + graph.adjacentNodes(node).iterator(), + (N adjacentNode) -> EndpointPair.unordered(node, adjacentNode))); + } + } + }; + return nodeInvalidatableSet(incident, node); } @Override @@ -184,4 +189,16 @@ protected final void validateEndpoints(EndpointPair endpoints) { protected final boolean isOrderingCompatible(EndpointPair endpoints) { return endpoints.isOrdered() == this.isDirected(); } + + protected final Set nodeInvalidatableSet(Set set, N node) { + return InvalidatableSet.of( + set, () -> nodes().contains(node), () -> String.format(NODE_REMOVED_FROM_GRAPH, node)); + } + + protected final Set nodePairInvalidatableSet(Set set, N nodeU, N nodeV) { + return InvalidatableSet.of( + set, + () -> nodes().contains(nodeU) && nodes().contains(nodeV), + () -> String.format(NODE_PAIR_REMOVED_FROM_GRAPH, nodeU, nodeV)); + } } diff --git a/android/guava/src/com/google/common/graph/AbstractNetwork.java b/android/guava/src/com/google/common/graph/AbstractNetwork.java index 0e59378d3a27..053f35ea4aaa 100644 --- a/android/guava/src/com/google/common/graph/AbstractNetwork.java +++ b/android/guava/src/com/google/common/graph/AbstractNetwork.java @@ -18,8 +18,11 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.graph.GraphConstants.EDGE_REMOVED_FROM_GRAPH; import static com.google.common.graph.GraphConstants.ENDPOINTS_MISMATCH; import static com.google.common.graph.GraphConstants.MULTIPLE_EDGES_CONNECTING; +import static com.google.common.graph.GraphConstants.NODE_PAIR_REMOVED_FROM_GRAPH; +import static com.google.common.graph.GraphConstants.NODE_REMOVED_FROM_GRAPH; import static java.util.Collections.unmodifiableSet; import com.google.common.annotations.Beta; @@ -50,7 +53,6 @@ @Beta @ElementTypesAreNonnullByDefault public abstract class AbstractNetwork implements Network { - @Override public Graph asGraph() { return new AbstractGraph() { @@ -160,16 +162,20 @@ public Set adjacentEdges(E edge) { EndpointPair endpointPair = incidentNodes(edge); // Verifies that edge is in this network. Set endpointPairIncidentEdges = Sets.union(incidentEdges(endpointPair.nodeU()), incidentEdges(endpointPair.nodeV())); - return Sets.difference(endpointPairIncidentEdges, ImmutableSet.of(edge)); + return edgeInvalidatableSet( + Sets.difference(endpointPairIncidentEdges, ImmutableSet.of(edge)), edge); } @Override public Set edgesConnecting(N nodeU, N nodeV) { Set outEdgesU = outEdges(nodeU); Set inEdgesV = inEdges(nodeV); - return outEdgesU.size() <= inEdgesV.size() - ? unmodifiableSet(Sets.filter(outEdgesU, connectedPredicate(nodeU, nodeV))) - : unmodifiableSet(Sets.filter(inEdgesV, connectedPredicate(nodeV, nodeU))); + return nodePairInvalidatableSet( + outEdgesU.size() <= inEdgesV.size() + ? unmodifiableSet(Sets.filter(outEdgesU, connectedPredicate(nodeU, nodeV))) + : unmodifiableSet(Sets.filter(inEdgesV, connectedPredicate(nodeV, nodeU))), + nodeU, + nodeV); } @Override @@ -272,6 +278,23 @@ public String toString() { + edgeIncidentNodesMap(this); } + protected final Set edgeInvalidatableSet(Set set, E edge) { + return InvalidatableSet.of( + set, () -> edges().contains(edge), () -> String.format(EDGE_REMOVED_FROM_GRAPH, edge)); + } + + protected final Set nodeInvalidatableSet(Set set, N node) { + return InvalidatableSet.of( + set, () -> nodes().contains(node), () -> String.format(NODE_REMOVED_FROM_GRAPH, node)); + } + + protected final Set nodePairInvalidatableSet(Set set, N nodeU, N nodeV) { + return InvalidatableSet.of( + set, + () -> nodes().contains(nodeU) && nodes().contains(nodeV), + () -> String.format(NODE_PAIR_REMOVED_FROM_GRAPH, nodeU, nodeV)); + } + private static Map> edgeIncidentNodesMap(final Network network) { return Maps.asMap(network.edges(), network::incidentNodes); } diff --git a/android/guava/src/com/google/common/graph/BaseGraph.java b/android/guava/src/com/google/common/graph/BaseGraph.java index 68813e18a76d..0fa83e3ad146 100644 --- a/android/guava/src/com/google/common/graph/BaseGraph.java +++ b/android/guava/src/com/google/common/graph/BaseGraph.java @@ -71,44 +71,93 @@ interface BaseGraph extends SuccessorsFunction, PredecessorsFunction { // /** - * Returns the nodes which have an incident edge in common with {@code node} in this graph. + * Returns a live view of the nodes which have an incident edge in common with {@code node} in + * this graph. * *

This is equal to the union of {@link #predecessors(Object)} and {@link #successors(Object)}. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ Set adjacentNodes(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s incoming edges against the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s incoming edges against the direction (if any) of the edge. * *

In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set predecessors(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s outgoing edges in the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s outgoing edges in the direction (if any) of the edge. * *

In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * *

This is not the same as "all nodes reachable from {@code node} by following outgoing * edges". For that functionality, see {@link Graphs#reachableNodes(Graph, Object)}. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set successors(N node); /** - * Returns the edges in this graph whose endpoints include {@code node}. + * Returns a live view of the edges in this graph whose endpoints include {@code node}. * *

This is equal to the union of incoming and outgoing edges. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph * @since 24.0 */ diff --git a/android/guava/src/com/google/common/graph/Graph.java b/android/guava/src/com/google/common/graph/Graph.java index 5dc0e71faf6d..b56842ab9c8a 100644 --- a/android/guava/src/com/google/common/graph/Graph.java +++ b/android/guava/src/com/google/common/graph/Graph.java @@ -156,45 +156,94 @@ public interface Graph extends BaseGraph { // /** - * Returns the nodes which have an incident edge in common with {@code node} in this graph. + * Returns a live view of the nodes which have an incident edge in common with {@code node} in + * this graph. * *

This is equal to the union of {@link #predecessors(Object)} and {@link #successors(Object)}. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set adjacentNodes(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s incoming edges against the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s incoming edges against the direction (if any) of the edge. * *

In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set predecessors(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s outgoing edges in the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s outgoing edges in the direction (if any) of the edge. * *

In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * *

This is not the same as "all nodes reachable from {@code node} by following outgoing * edges". For that functionality, see {@link Graphs#reachableNodes(Graph, Object)}. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set successors(N node); /** - * Returns the edges in this graph whose endpoints include {@code node}. + * Returns a live view of the edges in this graph whose endpoints include {@code node}. * *

This is equal to the union of incoming and outgoing edges. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph * @since 24.0 */ diff --git a/android/guava/src/com/google/common/graph/GraphConstants.java b/android/guava/src/com/google/common/graph/GraphConstants.java index 4e8510662cf1..263d6fd40617 100644 --- a/android/guava/src/com/google/common/graph/GraphConstants.java +++ b/android/guava/src/com/google/common/graph/GraphConstants.java @@ -35,6 +35,12 @@ private GraphConstants() {} // Error messages static final String NODE_NOT_IN_GRAPH = "Node %s is not an element of this graph."; static final String EDGE_NOT_IN_GRAPH = "Edge %s is not an element of this graph."; + static final String NODE_REMOVED_FROM_GRAPH = + "Node %s that was used to generate this set is no longer in the graph."; + static final String NODE_PAIR_REMOVED_FROM_GRAPH = + "Node %s or node %s that were used to generate this set are no longer in the graph."; + static final String EDGE_REMOVED_FROM_GRAPH = + "Edge %s that was used to generate this set is no longer in the graph."; static final String REUSING_EDGE = "Edge %s already exists between the following nodes: %s, " + "so it cannot be reused to connect the following nodes: %s."; diff --git a/android/guava/src/com/google/common/graph/InvalidatableSet.java b/android/guava/src/com/google/common/graph/InvalidatableSet.java new file mode 100644 index 000000000000..f8834b589ae9 --- /dev/null +++ b/android/guava/src/com/google/common/graph/InvalidatableSet.java @@ -0,0 +1,54 @@ +package com.google.common.graph; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Supplier; +import com.google.common.collect.ForwardingSet; +import java.util.Set; + +/** + * A subclass of `ForwardingSet` that throws `IllegalStateException` on invocation of any method + * (except `hashCode` and `equals`) if the provided `Supplier` returns false. + */ +@ElementTypesAreNonnullByDefault +final class InvalidatableSet extends ForwardingSet { + private final Supplier validator; + private final Set delegate; + private final Supplier errorMessage; + + public static final InvalidatableSet of( + Set delegate, Supplier validator, Supplier errorMessage) { + return new InvalidatableSet<>( + checkNotNull(delegate), checkNotNull(validator), checkNotNull(errorMessage)); + } + + @Override + protected Set delegate() { + validate(); + return delegate; + } + + private InvalidatableSet( + Set delegate, Supplier validator, Supplier errorMessage) { + this.delegate = delegate; + this.validator = validator; + this.errorMessage = errorMessage; + } + + // Override hashCode() to access delegate directly (so that it doesn't trigger the validate() call + // via delegate()); it seems inappropriate to throw ISE on this method. + @Override + public int hashCode() { + return delegate.hashCode(); + } + + private void validate() { + // Don't use checkState(), because we don't want the overhead of generating the error message + // unless it's actually going to be used; validate() is called for all set method calls, so it + // needs to be fast. + // (We could instead generate the message once, when the set is created, but zero is better.) + if (!validator.get()) { + throw new IllegalStateException(errorMessage.get()); + } + } +} diff --git a/android/guava/src/com/google/common/graph/Network.java b/android/guava/src/com/google/common/graph/Network.java index adf0c3dd8b2e..2e47764c095a 100644 --- a/android/guava/src/com/google/common/graph/Network.java +++ b/android/guava/src/com/google/common/graph/Network.java @@ -56,20 +56,20 @@ * NetworkBuilder} class: * *
{@code
- * MutableNetwork graph = NetworkBuilder.directed().build();
+ * MutableNetwork network = NetworkBuilder.directed().build();
  * }
* *

{@link NetworkBuilder#build()} returns an instance of {@link MutableNetwork}, which is a * subtype of {@code Network} that provides methods for adding and removing nodes and edges. If you - * do not need to mutate a graph (e.g. if you write a method than runs a read-only algorithm on the - * graph), you should use the non-mutating {@link Network} interface, or an {@link + * do not need to mutate a network (e.g. if you write a method than runs a read-only algorithm on + * the network), you should use the non-mutating {@link Network} interface, or an {@link * ImmutableNetwork}. * *

You can create an immutable copy of an existing {@code Network} using {@link * ImmutableNetwork#copyOf(Network)}: * *

{@code
- * ImmutableNetwork immutableGraph = ImmutableNetwork.copyOf(graph);
+ * ImmutableNetwork immutableGraph = ImmutableNetwork.copyOf(network);
  * }
* *

Instances of {@link ImmutableNetwork} do not implement {@link MutableNetwork} (obviously!) and @@ -160,69 +160,135 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti // /** - * Returns the nodes which have an incident edge in common with {@code node} in this network. + * Returns a live view of the nodes which have an incident edge in common with {@code node} in + * this network. * *

This is equal to the union of {@link #predecessors(Object)} and {@link #successors(Object)}. * + *

If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this network */ Set adjacentNodes(N node); /** - * Returns all nodes in this network adjacent to {@code node} which can be reached by traversing - * {@code node}'s incoming edges against the direction (if any) of the edge. + * Returns a live view of all nodes in this network adjacent to {@code node} which can be reached + * by traversing {@code node}'s incoming edges against the direction (if any) of the edge. * *

In an undirected network, this is equivalent to {@link #adjacentNodes(Object)}. * + *

If {@code node} is removed from the network after this method is called, the `Set` returned + * by this method will be invalidated, and will throw `IllegalStateException` if it is accessed in + * any way. + * * @throws IllegalArgumentException if {@code node} is not an element of this network */ @Override Set predecessors(N node); /** - * Returns all nodes in this network adjacent to {@code node} which can be reached by traversing - * {@code node}'s outgoing edges in the direction (if any) of the edge. + * Returns a live view of all nodes in this network adjacent to {@code node} which can be reached + * by traversing {@code node}'s outgoing edges in the direction (if any) of the edge. * *

In an undirected network, this is equivalent to {@link #adjacentNodes(Object)}. * *

This is not the same as "all nodes reachable from {@code node} by following outgoing * edges". For that functionality, see {@link Graphs#reachableNodes(Graph, Object)}. * + *

If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this network */ @Override Set successors(N node); /** - * Returns the edges whose {@link #incidentNodes(Object) incident nodes} in this network include - * {@code node}. + * Returns a live view of the edges whose {@link #incidentNodes(Object) incident nodes} in this + * network include {@code node}. * *

This is equal to the union of {@link #inEdges(Object)} and {@link #outEdges(Object)}. * + *

If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this network + * @since 24.0 */ Set incidentEdges(N node); /** - * Returns all edges in this network which can be traversed in the direction (if any) of the edge - * to end at {@code node}. + * Returns a live view of all edges in this network which can be traversed in the direction (if + * any) of the edge to end at {@code node}. * *

In a directed network, an incoming edge's {@link EndpointPair#target()} equals {@code node}. * *

In an undirected network, this is equivalent to {@link #incidentEdges(Object)}. * + *

If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this network */ Set inEdges(N node); /** - * Returns all edges in this network which can be traversed in the direction (if any) of the edge - * starting from {@code node}. + * Returns a live view of all edges in this network which can be traversed in the direction (if + * any) of the edge starting from {@code node}. * *

In a directed network, an outgoing edge's {@link EndpointPair#source()} equals {@code node}. * *

In an undirected network, this is equivalent to {@link #incidentEdges(Object)}. * + *

If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this network */ Set outEdges(N node); @@ -270,15 +336,28 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti EndpointPair incidentNodes(E edge); /** - * Returns the edges which have an {@link #incidentNodes(Object) incident node} in common with - * {@code edge}. An edge is not considered adjacent to itself. + * Returns a live view of the edges which have an {@link #incidentNodes(Object) incident node} in + * common with {@code edge}. An edge is not considered adjacent to itself. + * + *

If {@code edge} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code edge} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
* * @throws IllegalArgumentException if {@code edge} is not an element of this network */ Set adjacentEdges(E edge); /** - * Returns the set of edges that each directly connect {@code nodeU} to {@code nodeV}. + * Returns a live view of the set of edges that each directly connect {@code nodeU} to {@code + * nodeV}. * *

In an undirected network, this is equal to {@code edgesConnecting(nodeV, nodeU)}. * @@ -287,14 +366,27 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * edges}, the resulting set will contain at most one edge (equivalent to {@code * edgeConnecting(nodeU, nodeV).asSet()}). * + *

If either {@code nodeU} or {@code nodeV} are removed from the network after this method is + * called, the {@code Set} {@code view} returned by this method will be invalidated, and will + * throw {@code IllegalStateException} if it is accessed in any way, with the following + * exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code nodeU} or {@code nodeV} are re-added to the network after having been removed, + * {@code view}'s behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code nodeU} or {@code nodeV} is not an element of this * network */ Set edgesConnecting(N nodeU, N nodeV); /** - * Returns the set of edges that each directly connect {@code endpoints} (in the order, if any, - * specified by {@code endpoints}). + * Returns a live view of the set of edges that each directly connect {@code endpoints} (in the + * order, if any, specified by {@code endpoints}). * *

The resulting set of edges will be parallel (i.e. have equal {@link * #incidentNodes(Object)}). If this network does not {@link #allowsParallelEdges() allow parallel @@ -303,8 +395,21 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * *

If this network is directed, {@code endpoints} must be ordered. * + *

If either element of {@code endpoints} is removed from the network after this method is + * called, the {@code Set} {@code view} returned by this method will be invalidated, and will + * throw {@code IllegalStateException} if it is accessed in any way, with the following + * exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if either endpoint is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if either endpoint is not an element of this network - * @throws IllegalArgumentException if the endpoints are unordered and the graph is directed + * @throws IllegalArgumentException if the endpoints are unordered and the network is directed * @since 27.1 */ Set edgesConnecting(EndpointPair endpoints); @@ -328,12 +433,12 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * Returns the single edge that directly connects {@code endpoints} (in the order, if any, * specified by {@code endpoints}), if one is present, or {@code null} if no such edge exists. * - *

If this graph is directed, the endpoints must be ordered. + *

If this network is directed, the endpoints must be ordered. * * @throws IllegalArgumentException if there are multiple parallel edges connecting {@code nodeU} * to {@code nodeV} * @throws IllegalArgumentException if either endpoint is not an element of this network - * @throws IllegalArgumentException if the endpoints are unordered and the graph is directed + * @throws IllegalArgumentException if the endpoints are unordered and the network is directed * @since 27.1 */ @CheckForNull @@ -344,7 +449,7 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * equivalent to {@code nodes().contains(nodeU) && successors(nodeU).contains(nodeV)}, and to * {@code edgeConnectingOrNull(nodeU, nodeV) != null}. * - *

In an undirected graph, this is equal to {@code hasEdgeConnecting(nodeV, nodeU)}. + *

In an undirected network, this is equal to {@code hasEdgeConnecting(nodeV, nodeU)}. * * @since 23.0 */ @@ -355,8 +460,8 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * any, specified by {@code endpoints}). * *

Unlike the other {@code EndpointPair}-accepting methods, this method does not throw if the - * endpoints are unordered and the graph is directed; it simply returns {@code false}. This is for - * consistency with {@link Graph#hasEdgeConnecting(EndpointPair)} and {@link + * endpoints are unordered and the network is directed; it simply returns {@code false}. This is + * for consistency with {@link Graph#hasEdgeConnecting(EndpointPair)} and {@link * ValueGraph#hasEdgeConnecting(EndpointPair)}. * * @since 27.1 diff --git a/android/guava/src/com/google/common/graph/StandardNetwork.java b/android/guava/src/com/google/common/graph/StandardNetwork.java index 2aa103f99683..9c3cfd5a01b2 100644 --- a/android/guava/src/com/google/common/graph/StandardNetwork.java +++ b/android/guava/src/com/google/common/graph/StandardNetwork.java @@ -130,7 +130,7 @@ public ElementOrder edgeOrder() { @Override public Set incidentEdges(N node) { - return checkedConnections(node).incidentEdges(); + return nodeInvalidatableSet(checkedConnections(node).incidentEdges(), node); } @Override @@ -143,7 +143,7 @@ public EndpointPair incidentNodes(E edge) { @Override public Set adjacentNodes(N node) { - return checkedConnections(node).adjacentNodes(); + return nodeInvalidatableSet(checkedConnections(node).adjacentNodes(), node); } @Override @@ -153,27 +153,27 @@ public Set edgesConnecting(N nodeU, N nodeV) { return ImmutableSet.of(); } checkArgument(containsNode(nodeV), NODE_NOT_IN_GRAPH, nodeV); - return connectionsU.edgesConnecting(nodeV); + return nodePairInvalidatableSet(connectionsU.edgesConnecting(nodeV), nodeU, nodeV); } @Override public Set inEdges(N node) { - return checkedConnections(node).inEdges(); + return nodeInvalidatableSet(checkedConnections(node).inEdges(), node); } @Override public Set outEdges(N node) { - return checkedConnections(node).outEdges(); + return nodeInvalidatableSet(checkedConnections(node).outEdges(), node); } @Override public Set predecessors(N node) { - return checkedConnections(node).predecessors(); + return nodeInvalidatableSet(checkedConnections(node).predecessors(), node); } @Override public Set successors(N node) { - return checkedConnections(node).successors(); + return nodeInvalidatableSet(checkedConnections(node).successors(), node); } final NetworkConnections checkedConnections(N node) { diff --git a/android/guava/src/com/google/common/graph/StandardValueGraph.java b/android/guava/src/com/google/common/graph/StandardValueGraph.java index ab3ae582b55e..a5f3553087d9 100644 --- a/android/guava/src/com/google/common/graph/StandardValueGraph.java +++ b/android/guava/src/com/google/common/graph/StandardValueGraph.java @@ -103,29 +103,30 @@ public ElementOrder nodeOrder() { @Override public Set adjacentNodes(N node) { - return checkedConnections(node).adjacentNodes(); + return nodeInvalidatableSet(checkedConnections(node).adjacentNodes(), node); } @Override public Set predecessors(N node) { - return checkedConnections(node).predecessors(); + return nodeInvalidatableSet(checkedConnections(node).predecessors(), node); } @Override public Set successors(N node) { - return checkedConnections(node).successors(); + return nodeInvalidatableSet(checkedConnections(node).successors(), node); } @Override public Set> incidentEdges(N node) { GraphConnections connections = checkedConnections(node); - - return new IncidentEdgeSet(this, node) { - @Override - public Iterator> iterator() { - return connections.incidentEdgeIterator(node); - } - }; + IncidentEdgeSet incident = + new IncidentEdgeSet(this, node) { + @Override + public Iterator> iterator() { + return connections.incidentEdgeIterator(node); + } + }; + return nodeInvalidatableSet(incident, node); } @Override diff --git a/android/guava/src/com/google/common/graph/ValueGraph.java b/android/guava/src/com/google/common/graph/ValueGraph.java index bd3cf36401e3..56da5417cb44 100644 --- a/android/guava/src/com/google/common/graph/ValueGraph.java +++ b/android/guava/src/com/google/common/graph/ValueGraph.java @@ -17,7 +17,6 @@ package com.google.common.graph; import com.google.common.annotations.Beta; -import java.util.Collection; import java.util.Set; import javax.annotation.CheckForNull; @@ -166,45 +165,86 @@ public interface ValueGraph extends BaseGraph { // /** - * Returns the nodes which have an incident edge in common with {@code node} in this graph. + * Returns a live view of the nodes which have an incident edge in common with {@code node} in + * this graph. * *

This is equal to the union of {@link #predecessors(Object)} and {@link #successors(Object)}. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set adjacentNodes(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s incoming edges against the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s incoming edges against the direction (if any) of the edge. * *

In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * + *

If {@code node} is removed from the graph after this method is called, the `Set` returned by + * this method will be invalidated, and will throw `IllegalStateException` if it is accessed in + * any way. + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set predecessors(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s outgoing edges in the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s outgoing edges in the direction (if any) of the edge. * *

In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * *

This is not the same as "all nodes reachable from {@code node} by following outgoing * edges". For that functionality, see {@link Graphs#reachableNodes(Graph, Object)}. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set successors(N node); /** - * Returns the edges in this graph whose endpoints include {@code node}. + * Returns a live view of the edges in this graph whose endpoints include {@code node}. * *

This is equal to the union of incoming and outgoing edges. * + *

If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

    + *
  • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
  • {@code hashCode()} does not throw + *
  • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
+ * * @throws IllegalArgumentException if {@code node} is not an element of this graph * @since 24.0 */ @@ -316,7 +356,8 @@ public interface ValueGraph extends BaseGraph { *
  • A and B have equal {@link #isDirected() directedness}. *
  • A and B have equal {@link #nodes() node sets}. *
  • A and B have equal {@link #edges() edge sets}. - *
  • The {@link #edgeValue(Object, Object) value} of a given edge is the same in both A and B. + *
  • The {@link #edgeValueOrDefault(N, N, V) value} of a given edge is the same in both A and + * B. * * *

    Graph properties besides {@link #isDirected() directedness} do not affect equality. @@ -331,8 +372,8 @@ public interface ValueGraph extends BaseGraph { /** * Returns the hash code for this graph. The hash code of a graph is defined as the hash code of a - * map from each of its {@link #edges() edges} to the associated {@link #edgeValue(Object, Object) - * edge value}. + * map from each of its {@link #edges() edges} to the associated {@link #edgeValueOrDefault(N, N, + * V) edge value}. * *

    A reference implementation of this is provided by {@link AbstractValueGraph#hashCode()}. */ diff --git a/guava-tests/test/com/google/common/graph/AbstractGraphTest.java b/guava-tests/test/com/google/common/graph/AbstractGraphTest.java index a8209244c037..8b4b5076cdf7 100644 --- a/guava-tests/test/com/google/common/graph/AbstractGraphTest.java +++ b/guava-tests/test/com/google/common/graph/AbstractGraphTest.java @@ -17,6 +17,7 @@ package com.google.common.graph; import static com.google.common.graph.TestUtil.assertNodeNotInGraphErrorMessage; +import static com.google.common.graph.TestUtil.assertNodeRemovedFromGraphErrorMessage; import static com.google.common.graph.TestUtil.assertStronglyEquivalent; import static com.google.common.graph.TestUtil.sanityCheckSet; import static com.google.common.truth.Truth.assertThat; @@ -234,9 +235,8 @@ public void adjacentNodes_noAdjacentNodes() { @Test public void adjacentNodes_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(NODE_NOT_IN_GRAPH))); } @Test @@ -247,9 +247,8 @@ public void predecessors_noPredecessors() { @Test public void predecessors_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.predecessors(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.predecessors(NODE_NOT_IN_GRAPH))); } @Test @@ -260,9 +259,8 @@ public void successors_noSuccessors() { @Test public void successors_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.successors(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.successors(NODE_NOT_IN_GRAPH))); } @Test @@ -273,9 +271,8 @@ public void incidentEdges_noIncidentEdges() { @Test public void incidentEdges_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.incidentEdges(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.incidentEdges(NODE_NOT_IN_GRAPH))); } @Test @@ -293,9 +290,8 @@ public void degree_isolatedNode() { @Test public void degree_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.degree(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.degree(NODE_NOT_IN_GRAPH))); } @Test @@ -306,9 +302,8 @@ public void inDegree_isolatedNode() { @Test public void inDegree_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.inDegree(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.inDegree(NODE_NOT_IN_GRAPH))); } @Test @@ -319,9 +314,8 @@ public void outDegree_isolatedNode() { @Test public void outDegree_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.outDegree(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.outDegree(NODE_NOT_IN_GRAPH))); } @Test @@ -351,8 +345,24 @@ public void removeNode_existingNode() { assertThat(graphAsMutableGraph.removeNode(N1)).isTrue(); assertThat(graphAsMutableGraph.removeNode(N1)).isFalse(); assertThat(graph.nodes()).containsExactly(N2, N4); + assertThat(graph.adjacentNodes(N2)).isEmpty(); + assertThat(graph.predecessors(N2)).isEmpty(); + assertThat(graph.successors(N2)).isEmpty(); + assertThat(graph.incidentEdges(N2)).isEmpty(); assertThat(graph.adjacentNodes(N4)).isEmpty(); + assertThat(graph.predecessors(N4)).isEmpty(); + assertThat(graph.successors(N4)).isEmpty(); + assertThat(graph.incidentEdges(N4)).isEmpty(); + + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.predecessors(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.successors(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.incidentEdges(N1))); } @Test @@ -382,19 +392,48 @@ public void removeNode_nodeNotPresent() { } @Test - public void removeNode_queryAfterRemoval() { + public void queryAccessorSetAfterElementRemoval() { assume().that(graphIsMutable()).isTrue(); putEdge(N1, N2); putEdge(N2, N1); Set n1AdjacentNodes = graph.adjacentNodes(N1); Set n2AdjacentNodes = graph.adjacentNodes(N2); + Set n1Predecessors = graph.predecessors(N1); + Set n2Predecessors = graph.predecessors(N2); + Set n1Successors = graph.successors(N1); + Set n2Successors = graph.successors(N2); + Set> n1IncidentEdges = graph.incidentEdges(N1); + Set> n2IncidentEdges = graph.incidentEdges(N2); assertThat(graphAsMutableGraph.removeNode(N1)).isTrue(); - assertThat(n1AdjacentNodes).isEmpty(); + + // The choice of the size() method to call here is arbitrary. We assume that if any of the Set + // methods executes the validation check, they all will, and thus we only need to test one of + // them to ensure that the validation check happens and has the expected behavior. + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1AdjacentNodes::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1Predecessors::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1Successors::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1IncidentEdges::size)); + assertThat(n2AdjacentNodes).isEmpty(); - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(N1)); - assertNodeNotInGraphErrorMessage(e); + assertThat(n2Predecessors).isEmpty(); + assertThat(n2Successors).isEmpty(); + assertThat(n2IncidentEdges).isEmpty(); + } + + @Test + public void queryGraphAfterElementRemoval() { + assume().that(graphIsMutable()).isTrue(); + + putEdge(N1, N2); + putEdge(N2, N1); + assertThat(graphAsMutableGraph.removeNode(N1)).isTrue(); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> graph.adjacentNodes(N1))); } @Test diff --git a/guava-tests/test/com/google/common/graph/AbstractNetworkTest.java b/guava-tests/test/com/google/common/graph/AbstractNetworkTest.java index a29ffc5ae42b..9fee7c8be6b4 100644 --- a/guava-tests/test/com/google/common/graph/AbstractNetworkTest.java +++ b/guava-tests/test/com/google/common/graph/AbstractNetworkTest.java @@ -17,7 +17,9 @@ package com.google.common.graph; import static com.google.common.graph.TestUtil.assertEdgeNotInGraphErrorMessage; +import static com.google.common.graph.TestUtil.assertEdgeRemovedFromGraphErrorMessage; import static com.google.common.graph.TestUtil.assertNodeNotInGraphErrorMessage; +import static com.google.common.graph.TestUtil.assertNodeRemovedFromGraphErrorMessage; import static com.google.common.graph.TestUtil.assertStronglyEquivalent; import static com.google.common.graph.TestUtil.sanityCheckSet; import static com.google.common.truth.Truth.assertThat; @@ -423,10 +425,9 @@ public void incidentEdges_isolatedNode() { @Test public void incidentEdges_nodeNotInGraph() { - IllegalArgumentException e = + assertNodeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.incidentEdges(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> network.incidentEdges(NODE_NOT_IN_GRAPH))); } @Test @@ -437,10 +438,9 @@ public void incidentNodes_oneEdge() { @Test public void incidentNodes_edgeNotInGraph() { - IllegalArgumentException e = + assertEdgeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.incidentNodes(EDGE_NOT_IN_GRAPH)); - assertEdgeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> network.incidentNodes(EDGE_NOT_IN_GRAPH))); } @Test @@ -458,10 +458,9 @@ public void adjacentNodes_noAdjacentNodes() { @Test public void adjacentNodes_nodeNotInGraph() { - IllegalArgumentException e = + assertNodeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.adjacentNodes(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> network.adjacentNodes(NODE_NOT_IN_GRAPH))); } @Test @@ -482,10 +481,9 @@ public void adjacentEdges_noAdjacentEdges() { @Test public void adjacentEdges_edgeNotInGraph() { - IllegalArgumentException e = + assertEdgeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.adjacentEdges(EDGE_NOT_IN_GRAPH)); - assertEdgeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> network.adjacentEdges(EDGE_NOT_IN_GRAPH))); } @Test @@ -511,19 +509,16 @@ public void edgesConnecting_disconnectedNodes() { public void edgesConnecting_nodesNotInGraph() { addNode(N1); addNode(N2); - IllegalArgumentException e = + assertNodeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.edgesConnecting(N1, NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); - e = + IllegalArgumentException.class, () -> network.edgesConnecting(N1, NODE_NOT_IN_GRAPH))); + assertNodeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> network.edgesConnecting(NODE_NOT_IN_GRAPH, N2)); - assertNodeNotInGraphErrorMessage(e); - e = + IllegalArgumentException.class, () -> network.edgesConnecting(NODE_NOT_IN_GRAPH, N2))); + assertNodeNotInGraphErrorMessage( assertThrows( IllegalArgumentException.class, - () -> network.edgesConnecting(NODE_NOT_IN_GRAPH, NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + () -> network.edgesConnecting(NODE_NOT_IN_GRAPH, NODE_NOT_IN_GRAPH))); } @Test @@ -588,9 +583,8 @@ public void inEdges_noInEdges() { @Test public void inEdges_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> network.inEdges(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.inEdges(NODE_NOT_IN_GRAPH))); } @Test @@ -601,9 +595,8 @@ public void outEdges_noOutEdges() { @Test public void outEdges_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> network.outEdges(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.outEdges(NODE_NOT_IN_GRAPH))); } @Test @@ -614,9 +607,9 @@ public void predecessors_noPredecessors() { @Test public void predecessors_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> network.predecessors(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows( + IllegalArgumentException.class, () -> network.predecessors(NODE_NOT_IN_GRAPH))); } @Test @@ -627,9 +620,8 @@ public void successors_noSuccessors() { @Test public void successors_nodeNotInGraph() { - IllegalArgumentException e = - assertThrows(IllegalArgumentException.class, () -> network.successors(NODE_NOT_IN_GRAPH)); - assertNodeNotInGraphErrorMessage(e); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.successors(NODE_NOT_IN_GRAPH))); } @Test @@ -661,6 +653,28 @@ public void removeNode_existingNode() { assertThat(networkAsMutableNetwork.nodes()).containsExactly(N2, N4); assertThat(networkAsMutableNetwork.edges()).doesNotContain(E12); assertThat(networkAsMutableNetwork.edges()).doesNotContain(E41); + + assertThat(network.adjacentNodes(N2)).isEmpty(); + assertThat(network.predecessors(N2)).isEmpty(); + assertThat(network.successors(N2)).isEmpty(); + assertThat(network.incidentEdges(N2)).isEmpty(); + assertThat(network.inEdges(N2)).isEmpty(); + assertThat(network.outEdges(N2)).isEmpty(); + assertThat(network.adjacentNodes(N4)).isEmpty(); + assertThat(network.predecessors(N4)).isEmpty(); + assertThat(network.successors(N4)).isEmpty(); + assertThat(network.incidentEdges(N4)).isEmpty(); + assertThat(network.inEdges(N4)).isEmpty(); + assertThat(network.outEdges(N4)).isEmpty(); + + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.adjacentNodes(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.predecessors(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.successors(N1))); + assertNodeNotInGraphErrorMessage( + assertThrows(IllegalArgumentException.class, () -> network.incidentEdges(N1))); } @Test @@ -674,19 +688,52 @@ public void removeNode_nodeNotPresent() { } @Test - public void removeNode_queryAfterRemoval() { + public void queryAccessorSetAfterElementRemoval() { assume().that(graphIsMutable()).isTrue(); addEdge(N1, N2, E12); - Set n1AdjacentNodes = networkAsMutableNetwork.adjacentNodes(N1); - Set n2AdjacentNodes = networkAsMutableNetwork.adjacentNodes(N2); - assertTrue(networkAsMutableNetwork.removeNode(N1)); - assertThat(n1AdjacentNodes).isEmpty(); + Set n1AdjacentNodes = network.adjacentNodes(N1); + Set n2AdjacentNodes = network.adjacentNodes(N2); + Set n1Predecessors = network.predecessors(N1); + Set n2Predecessors = network.predecessors(N2); + Set n1Successors = network.successors(N1); + Set n2Successors = network.successors(N2); + Set n1IncidentEdges = network.incidentEdges(N1); + Set n2IncidentEdges = network.incidentEdges(N2); + Set n1InEdges = network.inEdges(N1); + Set n2InEdges = network.inEdges(N2); + Set n1OutEdges = network.outEdges(N1); + Set n2OutEdges = network.outEdges(N2); + Set e12AdjacentEdges = network.adjacentEdges(E12); + Set n12EdgesConnecting = network.edgesConnecting(N1, N2); + assertThat(networkAsMutableNetwork.removeNode(N1)).isTrue(); + + // The choice of the size() method to call here is arbitrary. We assume that if any of the Set + // methods executes the validation check, they all will, and thus we only need to test one of + // them to ensure that the validation check happens and has the expected behavior. + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1AdjacentNodes::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1Predecessors::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1Successors::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1IncidentEdges::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1InEdges::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n1OutEdges::size)); + assertEdgeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, e12AdjacentEdges::size)); + assertNodeRemovedFromGraphErrorMessage( + assertThrows(IllegalStateException.class, n12EdgesConnecting::size)); + assertThat(n2AdjacentNodes).isEmpty(); - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, () -> networkAsMutableNetwork.adjacentNodes(N1)); - assertNodeNotInGraphErrorMessage(e); + assertThat(n2Predecessors).isEmpty(); + assertThat(n2Successors).isEmpty(); + assertThat(n2IncidentEdges).isEmpty(); + assertThat(n2InEdges).isEmpty(); + assertThat(n2OutEdges).isEmpty(); } @Test @@ -731,10 +778,9 @@ public void removeEdge_queryAfterRemoval() { EndpointPair unused = networkAsMutableNetwork.incidentNodes(E12); // ensure cache (if any) is populated assertTrue(networkAsMutableNetwork.removeEdge(E12)); - IllegalArgumentException e = + assertEdgeNotInGraphErrorMessage( assertThrows( - IllegalArgumentException.class, () -> networkAsMutableNetwork.incidentNodes(E12)); - assertEdgeNotInGraphErrorMessage(e); + IllegalArgumentException.class, () -> networkAsMutableNetwork.incidentNodes(E12))); } @Test @@ -814,7 +860,7 @@ public void concurrentIteration() throws Exception { * synchronization actions.) * * All that said: I haven't actually managed to make this particular test produce a TSAN error - * for the field accesses in MapIteratorCache. This teset *has* found other TSAN errors, + * for the field accesses in MapIteratorCache. This test *has* found other TSAN errors, * including in MapRetrievalCache, so I'm not sure why this one is different. I did at least * confirm that my change to MapIteratorCache fixes the TSAN error in the (larger) test it was * originally reported in. diff --git a/guava-tests/test/com/google/common/graph/InvalidatableSetTest.java b/guava-tests/test/com/google/common/graph/InvalidatableSetTest.java new file mode 100644 index 000000000000..1af877f39f87 --- /dev/null +++ b/guava-tests/test/com/google/common/graph/InvalidatableSetTest.java @@ -0,0 +1,60 @@ +package com.google.common.graph; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableSet; +import java.util.HashSet; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class InvalidatableSetTest { + Set wrappedSet; + Set copyOfWrappedSet; + InvalidatableSet setToTest; + + @Before + public void createSets() { + wrappedSet = new HashSet<>(); + wrappedSet.add(1); + wrappedSet.add(2); + wrappedSet.add(3); + + copyOfWrappedSet = ImmutableSet.copyOf(wrappedSet); + setToTest = + InvalidatableSet.of(wrappedSet, () -> wrappedSet.contains(1), () -> 1 + "is not present"); + } + + @Test + @SuppressWarnings("TruthSelfEquals") + public void testEquals() { + // sanity check on construction of copyOfWrappedSet + assertThat(wrappedSet).isEqualTo(copyOfWrappedSet); + + // test that setToTest is still valid + assertThat(setToTest).isEqualTo(wrappedSet); + assertThat(setToTest).isEqualTo(copyOfWrappedSet); + + // invalidate setToTest + wrappedSet.remove(1); + // sanity check on update of wrappedSet + assertThat(wrappedSet).isNotEqualTo(copyOfWrappedSet); + + ImmutableSet copyOfModifiedSet = ImmutableSet.copyOf(wrappedSet); // {2,3} + // sanity check on construction of copyOfModifiedSet + assertThat(wrappedSet).isEqualTo(copyOfModifiedSet); + + // setToTest should throw when it calls equals(), or equals is called on it, except for itself + assertThat(setToTest).isEqualTo(setToTest); + assertThrows(IllegalStateException.class, () -> setToTest.equals(wrappedSet)); + assertThrows(IllegalStateException.class, () -> setToTest.equals(copyOfWrappedSet)); + assertThrows(IllegalStateException.class, () -> setToTest.equals(copyOfModifiedSet)); + assertThrows(IllegalStateException.class, () -> wrappedSet.equals(setToTest)); + assertThrows(IllegalStateException.class, () -> copyOfWrappedSet.equals(setToTest)); + assertThrows(IllegalStateException.class, () -> copyOfModifiedSet.equals(setToTest)); + } +} diff --git a/guava-tests/test/com/google/common/graph/TestUtil.java b/guava-tests/test/com/google/common/graph/TestUtil.java index 68a2503e223f..95fc75296c3a 100644 --- a/guava-tests/test/com/google/common/graph/TestUtil.java +++ b/guava-tests/test/com/google/common/graph/TestUtil.java @@ -28,6 +28,7 @@ final class TestUtil { static final String ERROR_ELEMENT_NOT_IN_GRAPH = "not an element of this graph"; static final String ERROR_NODE_NOT_IN_GRAPH = "Should not be allowed to pass a node that is not an element of the graph."; + static final String ERROR_ELEMENT_REMOVED = "used to generate this set"; private static final String NODE_STRING = "Node"; private static final String EDGE_STRING = "Edge"; @@ -42,12 +43,22 @@ static void assertNodeNotInGraphErrorMessage(Throwable throwable) { assertThat(throwable).hasMessageThat().startsWith(NODE_STRING); assertThat(throwable).hasMessageThat().contains(ERROR_ELEMENT_NOT_IN_GRAPH); } - + static void assertEdgeNotInGraphErrorMessage(Throwable throwable) { assertThat(throwable).hasMessageThat().startsWith(EDGE_STRING); assertThat(throwable).hasMessageThat().contains(ERROR_ELEMENT_NOT_IN_GRAPH); } + static void assertNodeRemovedFromGraphErrorMessage(Throwable throwable) { + assertThat(throwable).hasMessageThat().startsWith(NODE_STRING); + assertThat(throwable).hasMessageThat().contains(ERROR_ELEMENT_REMOVED); + } + + static void assertEdgeRemovedFromGraphErrorMessage(Throwable throwable) { + assertThat(throwable).hasMessageThat().startsWith(EDGE_STRING); + assertThat(throwable).hasMessageThat().contains(ERROR_ELEMENT_REMOVED); + } + static void assertStronglyEquivalent(Graph graphA, Graph graphB) { // Properties not covered by equals() assertThat(graphA.allowsSelfLoops()).isEqualTo(graphB.allowsSelfLoops()); diff --git a/guava/src/com/google/common/graph/AbstractBaseGraph.java b/guava/src/com/google/common/graph/AbstractBaseGraph.java index 5adcc9216c7a..16cbdde7eec6 100644 --- a/guava/src/com/google/common/graph/AbstractBaseGraph.java +++ b/guava/src/com/google/common/graph/AbstractBaseGraph.java @@ -20,6 +20,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.graph.GraphConstants.ENDPOINTS_MISMATCH; +import static com.google.common.graph.GraphConstants.NODE_PAIR_REMOVED_FROM_GRAPH; +import static com.google.common.graph.GraphConstants.NODE_REMOVED_FROM_GRAPH; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterators; @@ -106,27 +108,30 @@ public ElementOrder incidentEdgeOrder() { public Set> incidentEdges(N node) { checkNotNull(node); checkArgument(nodes().contains(node), "Node %s is not an element of this graph.", node); - return new IncidentEdgeSet(this, node) { - @Override - public UnmodifiableIterator> iterator() { - if (graph.isDirected()) { - return Iterators.unmodifiableIterator( - Iterators.concat( - Iterators.transform( - graph.predecessors(node).iterator(), - (N predecessor) -> EndpointPair.ordered(predecessor, node)), + IncidentEdgeSet incident = + new IncidentEdgeSet(this, node) { + @Override + public UnmodifiableIterator> iterator() { + if (graph.isDirected()) { + return Iterators.unmodifiableIterator( + Iterators.concat( + Iterators.transform( + graph.predecessors(node).iterator(), + (N predecessor) -> EndpointPair.ordered(predecessor, node)), + Iterators.transform( + // filter out 'node' from successors (already covered by predecessors, + // above) + Sets.difference(graph.successors(node), ImmutableSet.of(node)).iterator(), + (N successor) -> EndpointPair.ordered(node, successor)))); + } else { + return Iterators.unmodifiableIterator( Iterators.transform( - // filter out 'node' from successors (already covered by predecessors, above) - Sets.difference(graph.successors(node), ImmutableSet.of(node)).iterator(), - (N successor) -> EndpointPair.ordered(node, successor)))); - } else { - return Iterators.unmodifiableIterator( - Iterators.transform( - graph.adjacentNodes(node).iterator(), - (N adjacentNode) -> EndpointPair.unordered(node, adjacentNode))); - } - } - }; + graph.adjacentNodes(node).iterator(), + (N adjacentNode) -> EndpointPair.unordered(node, adjacentNode))); + } + } + }; + return nodeInvalidatableSet(incident, node); } @Override @@ -184,4 +189,16 @@ protected final void validateEndpoints(EndpointPair endpoints) { protected final boolean isOrderingCompatible(EndpointPair endpoints) { return endpoints.isOrdered() == this.isDirected(); } + + protected final Set nodeInvalidatableSet(Set set, N node) { + return InvalidatableSet.of( + set, () -> nodes().contains(node), () -> String.format(NODE_REMOVED_FROM_GRAPH, node)); + } + + protected final Set nodePairInvalidatableSet(Set set, N nodeU, N nodeV) { + return InvalidatableSet.of( + set, + () -> nodes().contains(nodeU) && nodes().contains(nodeV), + () -> String.format(NODE_PAIR_REMOVED_FROM_GRAPH, nodeU, nodeV)); + } } diff --git a/guava/src/com/google/common/graph/AbstractNetwork.java b/guava/src/com/google/common/graph/AbstractNetwork.java index 6ad96cb06867..08382e7b68b0 100644 --- a/guava/src/com/google/common/graph/AbstractNetwork.java +++ b/guava/src/com/google/common/graph/AbstractNetwork.java @@ -18,8 +18,11 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.graph.GraphConstants.EDGE_REMOVED_FROM_GRAPH; import static com.google.common.graph.GraphConstants.ENDPOINTS_MISMATCH; import static com.google.common.graph.GraphConstants.MULTIPLE_EDGES_CONNECTING; +import static com.google.common.graph.GraphConstants.NODE_PAIR_REMOVED_FROM_GRAPH; +import static com.google.common.graph.GraphConstants.NODE_REMOVED_FROM_GRAPH; import static java.util.Collections.unmodifiableSet; import com.google.common.annotations.Beta; @@ -51,7 +54,6 @@ @Beta @ElementTypesAreNonnullByDefault public abstract class AbstractNetwork implements Network { - @Override public Graph asGraph() { return new AbstractGraph() { @@ -161,16 +163,20 @@ public Set adjacentEdges(E edge) { EndpointPair endpointPair = incidentNodes(edge); // Verifies that edge is in this network. Set endpointPairIncidentEdges = Sets.union(incidentEdges(endpointPair.nodeU()), incidentEdges(endpointPair.nodeV())); - return Sets.difference(endpointPairIncidentEdges, ImmutableSet.of(edge)); + return edgeInvalidatableSet( + Sets.difference(endpointPairIncidentEdges, ImmutableSet.of(edge)), edge); } @Override public Set edgesConnecting(N nodeU, N nodeV) { Set outEdgesU = outEdges(nodeU); Set inEdgesV = inEdges(nodeV); - return outEdgesU.size() <= inEdgesV.size() - ? unmodifiableSet(Sets.filter(outEdgesU, connectedPredicate(nodeU, nodeV))) - : unmodifiableSet(Sets.filter(inEdgesV, connectedPredicate(nodeV, nodeU))); + return nodePairInvalidatableSet( + outEdgesU.size() <= inEdgesV.size() + ? unmodifiableSet(Sets.filter(outEdgesU, connectedPredicate(nodeU, nodeV))) + : unmodifiableSet(Sets.filter(inEdgesV, connectedPredicate(nodeV, nodeU))), + nodeU, + nodeV); } @Override @@ -284,6 +290,23 @@ public String toString() { + edgeIncidentNodesMap(this); } + protected final Set edgeInvalidatableSet(Set set, E edge) { + return InvalidatableSet.of( + set, () -> edges().contains(edge), () -> String.format(EDGE_REMOVED_FROM_GRAPH, edge)); + } + + protected final Set nodeInvalidatableSet(Set set, N node) { + return InvalidatableSet.of( + set, () -> nodes().contains(node), () -> String.format(NODE_REMOVED_FROM_GRAPH, node)); + } + + protected final Set nodePairInvalidatableSet(Set set, N nodeU, N nodeV) { + return InvalidatableSet.of( + set, + () -> nodes().contains(nodeU) && nodes().contains(nodeV), + () -> String.format(NODE_PAIR_REMOVED_FROM_GRAPH, nodeU, nodeV)); + } + private static Map> edgeIncidentNodesMap(final Network network) { return Maps.asMap(network.edges(), network::incidentNodes); } diff --git a/guava/src/com/google/common/graph/BaseGraph.java b/guava/src/com/google/common/graph/BaseGraph.java index 68813e18a76d..0fa83e3ad146 100644 --- a/guava/src/com/google/common/graph/BaseGraph.java +++ b/guava/src/com/google/common/graph/BaseGraph.java @@ -71,44 +71,93 @@ interface BaseGraph extends SuccessorsFunction, PredecessorsFunction { // /** - * Returns the nodes which have an incident edge in common with {@code node} in this graph. + * Returns a live view of the nodes which have an incident edge in common with {@code node} in + * this graph. * *

    This is equal to the union of {@link #predecessors(Object)} and {@link #successors(Object)}. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ Set adjacentNodes(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s incoming edges against the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s incoming edges against the direction (if any) of the edge. * *

    In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set predecessors(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s outgoing edges in the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s outgoing edges in the direction (if any) of the edge. * *

    In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * *

    This is not the same as "all nodes reachable from {@code node} by following outgoing * edges". For that functionality, see {@link Graphs#reachableNodes(Graph, Object)}. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set successors(N node); /** - * Returns the edges in this graph whose endpoints include {@code node}. + * Returns a live view of the edges in this graph whose endpoints include {@code node}. * *

    This is equal to the union of incoming and outgoing edges. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph * @since 24.0 */ diff --git a/guava/src/com/google/common/graph/Graph.java b/guava/src/com/google/common/graph/Graph.java index 5dc0e71faf6d..b56842ab9c8a 100644 --- a/guava/src/com/google/common/graph/Graph.java +++ b/guava/src/com/google/common/graph/Graph.java @@ -156,45 +156,94 @@ public interface Graph extends BaseGraph { // /** - * Returns the nodes which have an incident edge in common with {@code node} in this graph. + * Returns a live view of the nodes which have an incident edge in common with {@code node} in + * this graph. * *

    This is equal to the union of {@link #predecessors(Object)} and {@link #successors(Object)}. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set adjacentNodes(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s incoming edges against the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s incoming edges against the direction (if any) of the edge. * *

    In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set predecessors(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s outgoing edges in the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s outgoing edges in the direction (if any) of the edge. * *

    In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * *

    This is not the same as "all nodes reachable from {@code node} by following outgoing * edges". For that functionality, see {@link Graphs#reachableNodes(Graph, Object)}. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set successors(N node); /** - * Returns the edges in this graph whose endpoints include {@code node}. + * Returns a live view of the edges in this graph whose endpoints include {@code node}. * *

    This is equal to the union of incoming and outgoing edges. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph * @since 24.0 */ diff --git a/guava/src/com/google/common/graph/GraphConstants.java b/guava/src/com/google/common/graph/GraphConstants.java index 4e8510662cf1..263d6fd40617 100644 --- a/guava/src/com/google/common/graph/GraphConstants.java +++ b/guava/src/com/google/common/graph/GraphConstants.java @@ -35,6 +35,12 @@ private GraphConstants() {} // Error messages static final String NODE_NOT_IN_GRAPH = "Node %s is not an element of this graph."; static final String EDGE_NOT_IN_GRAPH = "Edge %s is not an element of this graph."; + static final String NODE_REMOVED_FROM_GRAPH = + "Node %s that was used to generate this set is no longer in the graph."; + static final String NODE_PAIR_REMOVED_FROM_GRAPH = + "Node %s or node %s that were used to generate this set are no longer in the graph."; + static final String EDGE_REMOVED_FROM_GRAPH = + "Edge %s that was used to generate this set is no longer in the graph."; static final String REUSING_EDGE = "Edge %s already exists between the following nodes: %s, " + "so it cannot be reused to connect the following nodes: %s."; diff --git a/guava/src/com/google/common/graph/InvalidatableSet.java b/guava/src/com/google/common/graph/InvalidatableSet.java new file mode 100644 index 000000000000..f8834b589ae9 --- /dev/null +++ b/guava/src/com/google/common/graph/InvalidatableSet.java @@ -0,0 +1,54 @@ +package com.google.common.graph; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Supplier; +import com.google.common.collect.ForwardingSet; +import java.util.Set; + +/** + * A subclass of `ForwardingSet` that throws `IllegalStateException` on invocation of any method + * (except `hashCode` and `equals`) if the provided `Supplier` returns false. + */ +@ElementTypesAreNonnullByDefault +final class InvalidatableSet extends ForwardingSet { + private final Supplier validator; + private final Set delegate; + private final Supplier errorMessage; + + public static final InvalidatableSet of( + Set delegate, Supplier validator, Supplier errorMessage) { + return new InvalidatableSet<>( + checkNotNull(delegate), checkNotNull(validator), checkNotNull(errorMessage)); + } + + @Override + protected Set delegate() { + validate(); + return delegate; + } + + private InvalidatableSet( + Set delegate, Supplier validator, Supplier errorMessage) { + this.delegate = delegate; + this.validator = validator; + this.errorMessage = errorMessage; + } + + // Override hashCode() to access delegate directly (so that it doesn't trigger the validate() call + // via delegate()); it seems inappropriate to throw ISE on this method. + @Override + public int hashCode() { + return delegate.hashCode(); + } + + private void validate() { + // Don't use checkState(), because we don't want the overhead of generating the error message + // unless it's actually going to be used; validate() is called for all set method calls, so it + // needs to be fast. + // (We could instead generate the message once, when the set is created, but zero is better.) + if (!validator.get()) { + throw new IllegalStateException(errorMessage.get()); + } + } +} diff --git a/guava/src/com/google/common/graph/Network.java b/guava/src/com/google/common/graph/Network.java index eb9e313e541b..330b5a75bc53 100644 --- a/guava/src/com/google/common/graph/Network.java +++ b/guava/src/com/google/common/graph/Network.java @@ -57,20 +57,20 @@ * NetworkBuilder} class: * *
    {@code
    - * MutableNetwork graph = NetworkBuilder.directed().build();
    + * MutableNetwork network = NetworkBuilder.directed().build();
      * }
    * *

    {@link NetworkBuilder#build()} returns an instance of {@link MutableNetwork}, which is a * subtype of {@code Network} that provides methods for adding and removing nodes and edges. If you - * do not need to mutate a graph (e.g. if you write a method than runs a read-only algorithm on the - * graph), you should use the non-mutating {@link Network} interface, or an {@link + * do not need to mutate a network (e.g. if you write a method than runs a read-only algorithm on + * the network), you should use the non-mutating {@link Network} interface, or an {@link * ImmutableNetwork}. * *

    You can create an immutable copy of an existing {@code Network} using {@link * ImmutableNetwork#copyOf(Network)}: * *

    {@code
    - * ImmutableNetwork immutableGraph = ImmutableNetwork.copyOf(graph);
    + * ImmutableNetwork immutableGraph = ImmutableNetwork.copyOf(network);
      * }
    * *

    Instances of {@link ImmutableNetwork} do not implement {@link MutableNetwork} (obviously!) and @@ -161,69 +161,135 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti // /** - * Returns the nodes which have an incident edge in common with {@code node} in this network. + * Returns a live view of the nodes which have an incident edge in common with {@code node} in + * this graph. * *

    This is equal to the union of {@link #predecessors(Object)} and {@link #successors(Object)}. * + *

    If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this network */ Set adjacentNodes(N node); /** - * Returns all nodes in this network adjacent to {@code node} which can be reached by traversing - * {@code node}'s incoming edges against the direction (if any) of the edge. + * Returns a live view of all nodes in this network adjacent to {@code node} which can be reached + * by traversing {@code node}'s incoming edges against the direction (if any) of the edge. * *

    In an undirected network, this is equivalent to {@link #adjacentNodes(Object)}. * + *

    If {@code node} is removed from the network after this method is called, the `Set` returned + * by this method will be invalidated, and will throw `IllegalStateException` if it is accessed in + * any way. + * * @throws IllegalArgumentException if {@code node} is not an element of this network */ @Override Set predecessors(N node); /** - * Returns all nodes in this network adjacent to {@code node} which can be reached by traversing - * {@code node}'s outgoing edges in the direction (if any) of the edge. + * Returns a live view of all nodes in this network adjacent to {@code node} which can be reached + * by traversing {@code node}'s outgoing edges in the direction (if any) of the edge. * *

    In an undirected network, this is equivalent to {@link #adjacentNodes(Object)}. * *

    This is not the same as "all nodes reachable from {@code node} by following outgoing * edges". For that functionality, see {@link Graphs#reachableNodes(Graph, Object)}. * + *

    If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this network */ @Override Set successors(N node); /** - * Returns the edges whose {@link #incidentNodes(Object) incident nodes} in this network include - * {@code node}. + * Returns a live view of the edges whose {@link #incidentNodes(Object) incident nodes} in this + * network include {@code node}. * *

    This is equal to the union of {@link #inEdges(Object)} and {@link #outEdges(Object)}. * + *

    If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this network + * @since 24.0 */ Set incidentEdges(N node); /** - * Returns all edges in this network which can be traversed in the direction (if any) of the edge - * to end at {@code node}. + * Returns a live view of all edges in this network which can be traversed in the direction (if + * any) of the edge to end at {@code node}. * *

    In a directed network, an incoming edge's {@link EndpointPair#target()} equals {@code node}. * *

    In an undirected network, this is equivalent to {@link #incidentEdges(Object)}. * + *

    If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this network */ Set inEdges(N node); /** - * Returns all edges in this network which can be traversed in the direction (if any) of the edge - * starting from {@code node}. + * Returns a live view of all edges in this network which can be traversed in the direction (if + * any) of the edge starting from {@code node}. * *

    In a directed network, an outgoing edge's {@link EndpointPair#source()} equals {@code node}. * *

    In an undirected network, this is equivalent to {@link #incidentEdges(Object)}. * + *

    If {@code node} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this network */ Set outEdges(N node); @@ -271,15 +337,28 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti EndpointPair incidentNodes(E edge); /** - * Returns the edges which have an {@link #incidentNodes(Object) incident node} in common with - * {@code edge}. An edge is not considered adjacent to itself. + * Returns a live view of the edges which have an {@link #incidentNodes(Object) incident node} in + * common with {@code edge}. An edge is not considered adjacent to itself. + * + *

    If {@code edge} is removed from the network after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code edge} is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
    * * @throws IllegalArgumentException if {@code edge} is not an element of this network */ Set adjacentEdges(E edge); /** - * Returns the set of edges that each directly connect {@code nodeU} to {@code nodeV}. + * Returns a live view of the set of edges that each directly connect {@code nodeU} to {@code + * nodeV}. * *

    In an undirected network, this is equal to {@code edgesConnecting(nodeV, nodeU)}. * @@ -288,14 +367,27 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * edges}, the resulting set will contain at most one edge (equivalent to {@code * edgeConnecting(nodeU, nodeV).asSet()}). * + *

    If either {@code nodeU} or {@code nodeV} are removed from the network after this method is + * called, the {@code Set} {@code view} returned by this method will be invalidated, and will + * throw {@code IllegalStateException} if it is accessed in any way, with the following + * exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code nodeU} or {@code nodeV} are re-added to the network after having been removed, + * {@code view}'s behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code nodeU} or {@code nodeV} is not an element of this * network */ Set edgesConnecting(N nodeU, N nodeV); /** - * Returns the set of edges that each directly connect {@code endpoints} (in the order, if any, - * specified by {@code endpoints}). + * Returns a live view of the set of edges that each directly connect {@code endpoints} (in the + * order, if any, specified by {@code endpoints}). * *

    The resulting set of edges will be parallel (i.e. have equal {@link * #incidentNodes(Object)}). If this network does not {@link #allowsParallelEdges() allow parallel @@ -304,8 +396,21 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * *

    If this network is directed, {@code endpoints} must be ordered. * + *

    If either element of {@code endpoints} is removed from the network after this method is + * called, the {@code Set} {@code view} returned by this method will be invalidated, and will + * throw {@code IllegalStateException} if it is accessed in any way, with the following + * exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if either endpoint is re-added to the network after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if either endpoint is not an element of this network - * @throws IllegalArgumentException if the endpoints are unordered and the graph is directed + * @throws IllegalArgumentException if the endpoints are unordered and the network is directed * @since 27.1 */ Set edgesConnecting(EndpointPair endpoints); @@ -329,12 +434,12 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * specified by {@code endpoints}), if one is present, or {@code Optional.empty()} if no such edge * exists. * - *

    If this graph is directed, the endpoints must be ordered. + *

    If this network is directed, the endpoints must be ordered. * * @throws IllegalArgumentException if there are multiple parallel edges connecting {@code nodeU} * to {@code nodeV} * @throws IllegalArgumentException if either endpoint is not an element of this network - * @throws IllegalArgumentException if the endpoints are unordered and the graph is directed + * @throws IllegalArgumentException if the endpoints are unordered and the network is directed * @since 27.1 */ Optional edgeConnecting(EndpointPair endpoints); @@ -358,12 +463,12 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * Returns the single edge that directly connects {@code endpoints} (in the order, if any, * specified by {@code endpoints}), if one is present, or {@code null} if no such edge exists. * - *

    If this graph is directed, the endpoints must be ordered. + *

    If this network is directed, the endpoints must be ordered. * * @throws IllegalArgumentException if there are multiple parallel edges connecting {@code nodeU} * to {@code nodeV} * @throws IllegalArgumentException if either endpoint is not an element of this network - * @throws IllegalArgumentException if the endpoints are unordered and the graph is directed + * @throws IllegalArgumentException if the endpoints are unordered and the network is directed * @since 27.1 */ @CheckForNull @@ -374,7 +479,7 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * equivalent to {@code nodes().contains(nodeU) && successors(nodeU).contains(nodeV)}, and to * {@code edgeConnectingOrNull(nodeU, nodeV) != null}. * - *

    In an undirected graph, this is equal to {@code hasEdgeConnecting(nodeV, nodeU)}. + *

    In an undirected network, this is equal to {@code hasEdgeConnecting(nodeV, nodeU)}. * * @since 23.0 */ @@ -385,8 +490,8 @@ public interface Network extends SuccessorsFunction, PredecessorsFuncti * any, specified by {@code endpoints}). * *

    Unlike the other {@code EndpointPair}-accepting methods, this method does not throw if the - * endpoints are unordered and the graph is directed; it simply returns {@code false}. This is for - * consistency with {@link Graph#hasEdgeConnecting(EndpointPair)} and {@link + * endpoints are unordered and the network is directed; it simply returns {@code false}. This is + * for consistency with {@link Graph#hasEdgeConnecting(EndpointPair)} and {@link * ValueGraph#hasEdgeConnecting(EndpointPair)}. * * @since 27.1 diff --git a/guava/src/com/google/common/graph/StandardNetwork.java b/guava/src/com/google/common/graph/StandardNetwork.java index 2aa103f99683..9c3cfd5a01b2 100644 --- a/guava/src/com/google/common/graph/StandardNetwork.java +++ b/guava/src/com/google/common/graph/StandardNetwork.java @@ -130,7 +130,7 @@ public ElementOrder edgeOrder() { @Override public Set incidentEdges(N node) { - return checkedConnections(node).incidentEdges(); + return nodeInvalidatableSet(checkedConnections(node).incidentEdges(), node); } @Override @@ -143,7 +143,7 @@ public EndpointPair incidentNodes(E edge) { @Override public Set adjacentNodes(N node) { - return checkedConnections(node).adjacentNodes(); + return nodeInvalidatableSet(checkedConnections(node).adjacentNodes(), node); } @Override @@ -153,27 +153,27 @@ public Set edgesConnecting(N nodeU, N nodeV) { return ImmutableSet.of(); } checkArgument(containsNode(nodeV), NODE_NOT_IN_GRAPH, nodeV); - return connectionsU.edgesConnecting(nodeV); + return nodePairInvalidatableSet(connectionsU.edgesConnecting(nodeV), nodeU, nodeV); } @Override public Set inEdges(N node) { - return checkedConnections(node).inEdges(); + return nodeInvalidatableSet(checkedConnections(node).inEdges(), node); } @Override public Set outEdges(N node) { - return checkedConnections(node).outEdges(); + return nodeInvalidatableSet(checkedConnections(node).outEdges(), node); } @Override public Set predecessors(N node) { - return checkedConnections(node).predecessors(); + return nodeInvalidatableSet(checkedConnections(node).predecessors(), node); } @Override public Set successors(N node) { - return checkedConnections(node).successors(); + return nodeInvalidatableSet(checkedConnections(node).successors(), node); } final NetworkConnections checkedConnections(N node) { diff --git a/guava/src/com/google/common/graph/StandardValueGraph.java b/guava/src/com/google/common/graph/StandardValueGraph.java index ab3ae582b55e..a5f3553087d9 100644 --- a/guava/src/com/google/common/graph/StandardValueGraph.java +++ b/guava/src/com/google/common/graph/StandardValueGraph.java @@ -103,29 +103,30 @@ public ElementOrder nodeOrder() { @Override public Set adjacentNodes(N node) { - return checkedConnections(node).adjacentNodes(); + return nodeInvalidatableSet(checkedConnections(node).adjacentNodes(), node); } @Override public Set predecessors(N node) { - return checkedConnections(node).predecessors(); + return nodeInvalidatableSet(checkedConnections(node).predecessors(), node); } @Override public Set successors(N node) { - return checkedConnections(node).successors(); + return nodeInvalidatableSet(checkedConnections(node).successors(), node); } @Override public Set> incidentEdges(N node) { GraphConnections connections = checkedConnections(node); - - return new IncidentEdgeSet(this, node) { - @Override - public Iterator> iterator() { - return connections.incidentEdgeIterator(node); - } - }; + IncidentEdgeSet incident = + new IncidentEdgeSet(this, node) { + @Override + public Iterator> iterator() { + return connections.incidentEdgeIterator(node); + } + }; + return nodeInvalidatableSet(incident, node); } @Override diff --git a/guava/src/com/google/common/graph/ValueGraph.java b/guava/src/com/google/common/graph/ValueGraph.java index a13e87eb9833..6495e2d4d0ba 100644 --- a/guava/src/com/google/common/graph/ValueGraph.java +++ b/guava/src/com/google/common/graph/ValueGraph.java @@ -17,7 +17,6 @@ package com.google.common.graph; import com.google.common.annotations.Beta; -import java.util.Collection; import java.util.Optional; import java.util.Set; import javax.annotation.CheckForNull; @@ -167,45 +166,86 @@ public interface ValueGraph extends BaseGraph { // /** - * Returns the nodes which have an incident edge in common with {@code node} in this graph. + * Returns a live view of the nodes which have an incident edge in common with {@code node} in + * this graph. * *

    This is equal to the union of {@link #predecessors(Object)} and {@link #successors(Object)}. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set adjacentNodes(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s incoming edges against the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s incoming edges against the direction (if any) of the edge. * *

    In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * + *

    If {@code node} is removed from the graph after this method is called, the `Set` returned by + * this method will be invalidated, and will throw `IllegalStateException` if it is accessed in + * any way. + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set predecessors(N node); /** - * Returns all nodes in this graph adjacent to {@code node} which can be reached by traversing - * {@code node}'s outgoing edges in the direction (if any) of the edge. + * Returns a live view of all nodes in this graph adjacent to {@code node} which can be reached by + * traversing {@code node}'s outgoing edges in the direction (if any) of the edge. * *

    In an undirected graph, this is equivalent to {@link #adjacentNodes(Object)}. * *

    This is not the same as "all nodes reachable from {@code node} by following outgoing * edges". For that functionality, see {@link Graphs#reachableNodes(Graph, Object)}. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph */ @Override Set successors(N node); /** - * Returns the edges in this graph whose endpoints include {@code node}. + * Returns a live view of the edges in this graph whose endpoints include {@code node}. * *

    This is equal to the union of incoming and outgoing edges. * + *

    If {@code node} is removed from the graph after this method is called, the {@code Set} + * {@code view} returned by this method will be invalidated, and will throw {@code + * IllegalStateException} if it is accessed in any way, with the following exceptions: + * + *

      + *
    • {@code view.equals(view)} evaluates to {@code true} (but any other `equals()` expression + * involving {@code view} will throw) + *
    • {@code hashCode()} does not throw + *
    • if {@code node} is re-added to the graph after having been removed, {@code view}'s + * behavior is undefined + *
    + * * @throws IllegalArgumentException if {@code node} is not an element of this graph * @since 24.0 */ @@ -340,7 +380,7 @@ public interface ValueGraph extends BaseGraph { *
  • A and B have equal {@link #isDirected() directedness}. *
  • A and B have equal {@link #nodes() node sets}. *
  • A and B have equal {@link #edges() edge sets}. - *
  • The {@link #edgeValue(Object, Object) value} of a given edge is the same in both A and B. + *
  • The {@link #edgeValue(N, N) value} of a given edge is the same in both A and B. * * *

    Graph properties besides {@link #isDirected() directedness} do not affect equality. @@ -355,8 +395,8 @@ public interface ValueGraph extends BaseGraph { /** * Returns the hash code for this graph. The hash code of a graph is defined as the hash code of a - * map from each of its {@link #edges() edges} to the associated {@link #edgeValue(Object, Object) - * edge value}. + * map from each of its {@link #edges() edges} to the associated {@link #edgeValue(N, N) edge + * value}. * *

    A reference implementation of this is provided by {@link AbstractValueGraph#hashCode()}. */