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