001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.collections; 018 019import static org.apache.juneau.commons.utils.AssertionUtils.*; 020import static org.apache.juneau.commons.utils.Utils.*; 021 022import java.util.*; 023import java.util.stream.Collectors; 024 025/** 026 * A composite set that presents multiple collections as a single unified set. 027 * 028 * <p> 029 * This class allows multiple collections to be viewed and iterated over as if they were merged into 030 * a single set, without actually copying the elements. Modifications made through the iterator affect 031 * the underlying collections. 032 * 033 * <h5 class='section'>Features:</h5> 034 * <ul class='spaced-list'> 035 * <li><b>Zero-Copy Composition:</b> No data is copied when creating a MultiSet; it simply wraps the provided collections 036 * <li><b>Transparent Iteration:</b> Iterating over a MultiSet seamlessly traverses all underlying collections in order 037 * <li><b>Modification Support:</b> Elements can be removed via the iterator's {@link Iterator#remove()} method 038 * <li><b>Efficient Size Calculation:</b> The size is computed by summing the sizes of all underlying collections 039 * <li><b>Enumeration Support:</b> Provides an {@link Enumeration} view via {@link #enumerator()} 040 * </ul> 041 * 042 * <h5 class='section'>Usage:</h5> 043 * <p class='bjava'> 044 * <jc>// Create a MultiSet from three separate collections</jc> 045 * List<String> <jv>list1</jv> = List.of(<js>"a"</js>, <js>"b"</js>); 046 * Set<String> <jv>set1</jv> = Set.of(<js>"c"</js>, <js>"d"</js>); 047 * List<String> <jv>list2</jv> = List.of(<js>"e"</js>, <js>"f"</js>); 048 * 049 * MultiSet<String> <jv>multiSet</jv> = <jk>new</jk> MultiSet<>(<jv>list1</jv>, <jv>set1</jv>, <jv>list2</jv>); 050 * 051 * <jc>// Iterate over all elements from all collections</jc> 052 * <jk>for</jk> (String <jv>element</jv> : <jv>multiSet</jv>) { 053 * System.<jsf>out</jsf>.println(<jv>element</jv>); <jc>// Prints: a, b, c, d, e, f</jc> 054 * } 055 * 056 * <jc>// Get total size across all collections</jc> 057 * <jk>int</jk> <jv>totalSize</jv> = <jv>multiSet</jv>.size(); <jc>// Returns: 6</jc> 058 * 059 * <jc>// Remove elements via iterator (affects underlying collections)</jc> 060 * Iterator<String> <jv>it</jv> = <jv>multiSet</jv>.iterator(); 061 * <jk>while</jk> (<jv>it</jv>.hasNext()) { 062 * <jk>if</jk> (<jv>it</jv>.next().equals(<js>"b"</js>)) { 063 * <jv>it</jv>.remove(); <jc>// Removes "b" from list1</jc> 064 * } 065 * } 066 * </p> 067 * 068 * <h5 class='section'>Behavior Notes:</h5> 069 * <ul class='spaced-list'> 070 * <li>The order of iteration follows the order of collections as provided in the constructor 071 * <li>Within each collection, iteration order is determined by that collection's iterator 072 * <li>The underlying collections must not be <jk>null</jk>, but can be empty 073 * <li>Modifications via {@link Iterator#remove()} are delegated to the underlying collection's iterator 074 * <li>This class does not support {@link #add(Object)} or {@link #remove(Object)} operations 075 * <li>The {@link #size()} method recomputes the sum each time it's called (not cached) 076 * </ul> 077 * 078 * <h5 class='section'>Thread Safety:</h5> 079 * <p> 080 * This class is not inherently thread-safe. If the underlying collections are modified concurrently 081 * during iteration, the behavior is undefined. Synchronization must be handled externally if needed. 082 * 083 * <h5 class='section'>Example - Processing Multiple Data Sources:</h5> 084 * <p class='bjava'> 085 * <jc>// Combine results from database, cache, and defaults</jc> 086 * List<User> <jv>dbUsers</jv> = fetchFromDatabase(); 087 * Set<User> <jv>cachedUsers</jv> = getCachedUsers(); 088 * List<User> <jv>defaultUsers</jv> = getDefaultUsers(); 089 * 090 * MultiSet<User> <jv>allUsers</jv> = <jk>new</jk> MultiSet<>(<jv>dbUsers</jv>, <jv>cachedUsers</jv>, <jv>defaultUsers</jv>); 091 * 092 * <jc>// Process all users from all sources</jc> 093 * <jv>allUsers</jv>.forEach(user -> processUser(user)); 094 * </p> 095 * 096 * <h5 class='section'>See Also:</h5> 097 * <ul> 098 * <li class='link'><a class="doclink" href="../../../../../index.html#juneau-commons">Overview > juneau-commons</a> 099 * </ul> 100 * 101 * @param <E> The element type of this set. 102 */ 103public class MultiSet<E> extends AbstractSet<E> { 104 105 /** 106 * The underlying collections being wrapped by this MultiSet. 107 * <p> 108 * These collections are accessed directly during iteration without copying. 109 */ 110 final Collection<E>[] l; 111 112 /** 113 * Creates a new MultiSet that presents the specified collections as a single unified set. 114 * 115 * <p> 116 * The collections are stored by reference (not copied), so modifications made through the 117 * MultiSet's iterator will affect the original collections. 118 * 119 * <h5 class='section'>Example:</h5> 120 * <p class='bjava'> 121 * List<String> <jv>list1</jv> = <jk>new</jk> ArrayList<>(List.of(<js>"a"</js>, <js>"b"</js>)); 122 * List<String> <jv>list2</jv> = <jk>new</jk> ArrayList<>(List.of(<js>"c"</js>, <js>"d"</js>)); 123 * 124 * MultiSet<String> <jv>multiSet</jv> = <jk>new</jk> MultiSet<>(<jv>list1</jv>, <jv>list2</jv>); 125 * <jc>// multiSet now represents all elements from both lists</jc> 126 * </p> 127 * 128 * @param c Zero or more collections to combine into this set. Must not be <jk>null</jk>, 129 * and no individual collection can be <jk>null</jk> (but collections can be empty). 130 * @throws IllegalArgumentException if the collections array or any collection within it is <jk>null</jk>. 131 */ 132 @SafeVarargs 133 public MultiSet(Collection<E>...c) { 134 assertArgNotNull("c", c); 135 for (var cc : c) 136 assertArgNotNull("c", cc); 137 l = c; 138 } 139 140 /** 141 * Returns an {@link Enumeration} view of this set. 142 * 143 * <p> 144 * This is useful for compatibility with legacy APIs that require an {@link Enumeration} 145 * rather than an {@link Iterator}. 146 * 147 * <h5 class='section'>Example:</h5> 148 * <p class='bjava'> 149 * MultiSet<String> <jv>multiSet</jv> = <jk>new</jk> MultiSet<>(list1, list2); 150 * Enumeration<String> <jv>enumeration</jv> = <jv>multiSet</jv>.enumerator(); 151 * 152 * <jk>while</jk> (<jv>enumeration</jv>.hasMoreElements()) { 153 * String <jv>element</jv> = <jv>enumeration</jv>.nextElement(); 154 * <jc>// Process element</jc> 155 * } 156 * </p> 157 * 158 * @return An {@link Enumeration} that iterates over all elements in all underlying collections. 159 * @see #iterator() 160 */ 161 public Enumeration<E> enumerator() { 162 return Collections.enumeration(this); 163 } 164 165 /** 166 * Returns an iterator over all elements in all underlying collections. 167 * 168 * <p> 169 * The iterator traverses each collection in the order they were provided to the constructor. 170 * Within each collection, the iteration order is determined by that collection's iterator. 171 * 172 * <p> 173 * The returned iterator supports the {@link Iterator#remove()} operation, which removes 174 * the current element from its underlying collection. 175 * 176 * <h5 class='section'>Behavior:</h5> 177 * <ul class='spaced-list'> 178 * <li>Elements from the first collection are iterated first, then the second, and so on 179 * <li>If a collection is empty, it is skipped during iteration 180 * <li>Calling {@link Iterator#remove()} removes the element from the underlying collection 181 * <li>Calling {@link Iterator#next()} when {@link Iterator#hasNext()} returns <jk>false</jk> 182 * throws {@link NoSuchElementException} 183 * <li>Calling {@link Iterator#remove()} before calling {@link Iterator#next()} or calling it twice 184 * in a row may throw {@link IllegalStateException} (behavior depends on underlying collection) 185 * </ul> 186 * 187 * <h5 class='section'>Example:</h5> 188 * <p class='bjava'> 189 * List<String> <jv>list1</jv> = <jk>new</jk> ArrayList<>(List.of(<js>"a"</js>, <js>"b"</js>)); 190 * List<String> <jv>list2</jv> = <jk>new</jk> ArrayList<>(List.of(<js>"c"</js>, <js>"d"</js>)); 191 * MultiSet<String> <jv>multiSet</jv> = <jk>new</jk> MultiSet<>(<jv>list1</jv>, <jv>list2</jv>); 192 * 193 * Iterator<String> <jv>it</jv> = <jv>multiSet</jv>.iterator(); 194 * <jk>while</jk> (<jv>it</jv>.hasNext()) { 195 * String <jv>element</jv> = <jv>it</jv>.next(); 196 * <jk>if</jk> (<jv>element</jv>.equals(<js>"b"</js>)) { 197 * <jv>it</jv>.remove(); <jc>// Removes "b" from list1</jc> 198 * } 199 * } 200 * </p> 201 * 202 * @return An iterator over all elements in all underlying collections. 203 */ 204 @Override /* Set */ 205 public Iterator<E> iterator() { 206 return new Iterator<>() { 207 int i = 0; 208 Iterator<E> i2 = (l.length > 0 ? l[i++].iterator() : null); 209 210 @Override /* Overridden from Iterator */ 211 public boolean hasNext() { 212 if (i2 == null) 213 return false; 214 if (i2.hasNext()) 215 return true; 216 for (var j = i; j < l.length; j++) 217 if (l[j].size() > 0) 218 return true; 219 return false; 220 } 221 222 @Override /* Overridden from Iterator */ 223 public E next() { 224 if (i2 == null) 225 throw new NoSuchElementException(); 226 while (! i2.hasNext()) { 227 if (i >= l.length) 228 throw new NoSuchElementException(); 229 i2 = l[i++].iterator(); 230 } 231 return i2.next(); 232 } 233 234 @Override /* Overridden from Iterator */ 235 public void remove() { 236 if (i2 == null) 237 throw new NoSuchElementException(); 238 i2.remove(); 239 } 240 }; 241 } 242 243 /** 244 * Returns the total number of elements across all underlying collections. 245 * 246 * <p> 247 * This method computes the size by summing the {@link Collection#size()} of each 248 * underlying collection. The size is recalculated each time this method is called 249 * (it is not cached). 250 * 251 * <h5 class='section'>Example:</h5> 252 * <p class='bjava'> 253 * List<String> <jv>list1</jv> = List.of(<js>"a"</js>, <js>"b"</js>); <jc>// size = 2</jc> 254 * List<String> <jv>list2</jv> = List.of(<js>"c"</js>, <js>"d"</js>, <js>"e"</js>); <jc>// size = 3</jc> 255 * MultiSet<String> <jv>multiSet</jv> = <jk>new</jk> MultiSet<>(<jv>list1</jv>, <jv>list2</jv>); 256 * 257 * <jk>int</jk> <jv>totalSize</jv> = <jv>multiSet</jv>.size(); <jc>// Returns: 5</jc> 258 * </p> 259 * 260 * @return The sum of sizes of all underlying collections. 261 */ 262 @Override /* Set */ 263 public int size() { 264 var i = 0; 265 for (var c : l) 266 i += c.size(); 267 return i; 268 } 269 270 /** 271 * Returns a string representation of this MultiSet. 272 * 273 * <p> 274 * The format is <c>"[[...],[...],...]"</c> where each <c>[...]</c> is the standard 275 * standard string representation of each underlying collection (as returned by {@link Object#toString()}). 276 * 277 * <h5 class='section'>Example:</h5> 278 * <p class='bjava'> 279 * List<String> <jv>list1</jv> = List.of(<js>"a"</js>, <js>"b"</js>); 280 * List<String> <jv>list2</jv> = List.of(<js>"c"</js>, <js>"d"</js>); 281 * MultiSet<String> <jv>multiSet</jv> = <jk>new</jk> MultiSet<>(<jv>list1</jv>, <jv>list2</jv>); 282 * <jv>multiSet</jv>.toString(); <jc>// Returns: "[[a, b], [c, d]]"</jc> 283 * </p> 284 * 285 * @return A string representation of this MultiSet. 286 */ 287 @Override 288 public String toString() { 289 return Arrays.stream(l).map(Object::toString).collect(Collectors.joining(", ", "[", "]")); 290 } 291 292 /** 293 * Compares the specified object with this set for equality. 294 * 295 * <p> 296 * Returns <jk>true</jk> if the given object is also a set, the two sets have the same size, 297 * and every member of the given set is contained in this set. 298 * 299 * <p> 300 * This implementation checks if the specified object is a set, and if so, compares the sizes 301 * and checks if all elements in the specified set are contained in this set. 302 * 303 * @param o Object to be compared for equality with this set. 304 * @return <jk>true</jk> if the specified object is equal to this set. 305 */ 306 @Override 307 public boolean equals(Object o) { 308 return (o instanceof Set o2) && eq(this, o2, (x,y) -> eq(x.size(), y.size()) && x.containsAll(y)); 309 } 310 311 /** 312 * Returns the hash code value for this set. 313 * 314 * <p> 315 * The hash code of a set is defined to be the sum of the hash codes of the elements in the set, 316 * where the hash code of a <jk>null</jk> element is defined to be zero. This ensures that 317 * <c>s1.equals(s2)</c> implies that <c>s1.hashCode()==s2.hashCode()</c> for any two sets 318 * <c>s1</c> and <c>s2</c>, as required by the general contract of {@link Object#hashCode()}. 319 * 320 * <p> 321 * This implementation iterates over the set, calling the <c>hashCode</c> method on each element 322 * in the set, and adding up the results. 323 * 324 * @return The hash code value for this set. 325 */ 326 @Override 327 public int hashCode() { 328 int h = 0; 329 for (E e : this) 330 h += e == null ? 0 : e.hashCode(); 331 return h; 332 } 333}