Panduan untuk HashSet di Java

1. Gambaran keseluruhan

Dalam artikel ini, kita akan menyelami HashSet. Ini adalah salah satu implementasi Set yang paling popular serta merupakan bagian yang tidak terpisahkan dari Java Collections Framework.

2. Pengenalan kepada HashSet

HashSet adalah salah satu struktur data asas dalam Java Collections API .

Mari kita ingat aspek terpenting dalam pelaksanaan ini:

  • Ia menyimpan elemen unik dan membenarkan sifar
  • Ia disokong oleh HashMap
  • Ia tidak mengekalkan susunan kemasukan
  • Ia tidak selamat untuk benang

Perhatikan bahawa HashMap dalaman ini akan diinisialisasi apabila contoh HashSet dibuat:

public HashSet() { map = new HashMap(); }

Sekiranya anda ingin mendalami cara HashMap berfungsi, anda boleh membaca artikel yang difokuskan di sini.

3. API

Di bahagian ini, kita akan mengkaji kaedah yang paling biasa digunakan dan melihat beberapa contoh mudah.

3.1. Tambah()

Kaedah add () boleh digunakan untuk menambahkan elemen pada satu set. Kontrak kaedah menyatakan bahawa elemen akan ditambahkan hanya apabila ia belum ada dalam satu set. Sekiranya elemen ditambahkan, kaedah akan kembali benar, jika tidak - salah.

Kita boleh menambahkan elemen ke HashSet seperti:

@Test public void whenAddingElement_shouldAddElement() { Set hashset = new HashSet(); assertTrue(hashset.add("String Added")); }

Dari perspektif pelaksanaan, kaedah penambahan adalah sangat penting. Butir-butir pelaksanaan menggambarkan bagaimana HashSet berfungsi secara dalaman dan memanfaatkan HashMap yang meletakkan kaedah:

public boolean add(E e) { return map.put(e, PRESENT) == null; }

The peta pembolehubah adalah sebutan mengenai dalaman, sokongan HashMap:

private transient HashMap map;

Adalah idea yang baik untuk membiasakan diri dengan kod hash terlebih dahulu untuk mendapatkan pemahaman terperinci tentang bagaimana elemen disusun dalam struktur data berdasarkan hash.

Meringkaskan:

  • A HashMap adalah pelbagai baldi dengan kapasiti lalai 16 elemen - setiap baldi sepadan dengan nilai kodcincang yang lain
  • Sekiranya pelbagai objek mempunyai nilai kod hash yang sama, objek tersebut disimpan dalam satu baldi
  • Sekiranya faktor beban tercapai, susunan baru akan dibuat dua kali lebih besar daripada yang sebelumnya dan semua elemen akan disusun semula dan diedarkan di antara baldi baru yang sesuai
  • Untuk mendapatkan semula nilai, kami mencantumkan kunci, mengubahnya, dan kemudian pergi ke baldi yang sesuai dan cari senarai berpotensi yang berkaitan sekiranya terdapat lebih dari satu objek

3.2. mengandungi ()

Tujuan kaedah berisi adalah untuk memeriksa sama ada elemen terdapat dalam HashSet tertentu . Ia kembali benar jika unsur itu dijumpai, jika tidak, salah.

Kami dapat memeriksa elemen dalam HashSet :

@Test public void whenCheckingForElement_shouldSearchForElement() { Set hashsetContains = new HashSet(); hashsetContains.add("String Added"); assertTrue(hashsetContains.contains("String Added")); }

Setiap kali objek dihantar ke kaedah ini, nilai hash akan dikira. Kemudian, lokasi baldi yang sesuai diselesaikan dan dilalui.

3.3. alih keluar ()

Kaedah membuang elemen yang ditentukan dari set jika ada. Kaedah ini kembali benar jika satu set mengandungi elemen yang ditentukan.

Mari lihat contoh yang berfungsi:

@Test public void whenRemovingElement_shouldRemoveElement() { Set removeFromHashSet = new HashSet(); removeFromHashSet.add("String Added"); assertTrue(removeFromHashSet.remove("String Added")); }

3.4. jelas ()

Kami menggunakan kaedah ini ketika kami berhasrat untuk membuang semua item dari satu set. Pelaksanaan yang mendasari hanya membersihkan semua elemen dari HashMap yang mendasari .

Mari kita lihat dalam tindakan:

@Test public void whenClearingHashSet_shouldClearHashSet() { Set clearHashSet = new HashSet(); clearHashSet.add("String Added"); clearHashSet.clear(); assertTrue(clearHashSet.isEmpty()); }

3.5. saiz ()

Ini adalah salah satu kaedah asas dalam API. Ia banyak digunakan kerana dapat membantu mengenal pasti bilangan elemen yang terdapat dalam HashSet . Pelaksanaan yang mendasari hanya menyerahkan pengiraan ke kaedah ukuran HashMap () .

Mari kita lihat dalam tindakan:

@Test public void whenCheckingTheSizeOfHashSet_shouldReturnThesize() { Set hashSetSize = new HashSet(); hashSetSize.add("String Added"); assertEquals(1, hashSetSize.size()); }

3.6. kosong()

Kita boleh menggunakan kaedah ini untuk mengetahui apakah contoh HashSet yang diberikan kosong atau tidak. Kaedah ini kembali benar jika set tidak mengandungi unsur:

@Test public void whenCheckingForEmptyHashSet_shouldCheckForEmpty() { Set emptyHashSet = new HashSet(); assertTrue(emptyHashSet.isEmpty()); }

3.7. iterator ()

Kaedah mengembalikan iterator ke atas elemen dalam Set . Elemen dikunjungi tanpa urutan tertentu dan iterator gagal cepat .

Kita dapat memerhatikan urutan lelaran rawak di sini:

@Test public void whenIteratingHashSet_shouldIterateHashSet() { Set hashset = new HashSet(); hashset.add("First"); hashset.add("Second"); hashset.add("Third"); Iterator itr = hashset.iterator(); while(itr.hasNext()){ System.out.println(itr.next()); } }

If the set is modified at any time after the iterator is created in any way except through the iterator's own remove method, the Iterator throws a ConcurrentModificationException.

Let's see that in action:

@Test(expected = ConcurrentModificationException.class) public void whenModifyingHashSetWhileIterating_shouldThrowException() { Set hashset = new HashSet(); hashset.add("First"); hashset.add("Second"); hashset.add("Third"); Iterator itr = hashset.iterator(); while (itr.hasNext()) { itr.next(); hashset.remove("Second"); } } 

Alternatively, had we used the iterator's remove method, then we wouldn't have encountered the exception:

@Test public void whenRemovingElementUsingIterator_shouldRemoveElement() { Set hashset = new HashSet(); hashset.add("First"); hashset.add("Second"); hashset.add("Third"); Iterator itr = hashset.iterator(); while (itr.hasNext()) { String element = itr.next(); if (element.equals("Second")) itr.remove(); } assertEquals(2, hashset.size()); }

The fail-fast behavior of an iterator cannot be guaranteed as it's impossible to make any hard guarantees in the presence of unsynchronized concurrent modification.

Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it'd be wrong to write a program that depended on this exception for its correctness.

4. How HashSet Maintains Uniqueness?

When we put an object into a HashSet, it uses the object's hashcode value to determine if an element is not in the set already.

Each hash code value corresponds to a certain bucket location which can contain various elements, for which the calculated hash value is the same. But two objects with the same hashCode might not be equal.

So, objects within the same bucket will be compared using the equals() method.

5. Performance of HashSet

The performance of a HashSet is affected mainly by two parameters – its Initial Capacity and the Load Factor.

The expected time complexity of adding an element to a set is O(1) which can drop to O(n) in the worst case scenario (only one bucket present) – therefore, it's essential to maintain the right HashSet's capacity.

An important note: since JDK 8, the worst case time complexity is O(log*n).

The load factor describes what is the maximum fill level, above which, a set will need to be resized.

We can also create a HashSet with custom values for initial capacity and load factor:

Set hashset = new HashSet(); Set hashset = new HashSet(20); Set hashset = new HashSet(20, 0.5f); 

In the first case, the default values are used – the initial capacity of 16 and the load factor of 0.75. In the second, we override the default capacity and in the third one, we override both.

A low initial capacity reduces space complexity but increases the frequency of rehashing which is an expensive process.

On the other hand, a high initial capacity increases the cost of iteration and the initial memory consumption.

As a rule of thumb:

  • A high initial capacity is good for a large number of entries coupled with little to no iteration
  • A low initial capacity is good for few entries with a lot of iteration

It's, therefore, very important to strike the correct balance between the two. Usually, the default implementation is optimized and works just fine, should we feel the need to tune these parameters to suit the requirements, we need to do judiciously.

6. Conclusion

In this article, we outlined the utility of a HashSet, its purpose as well as its underlying working. We saw how efficient it is in terms of usability given its constant time performance and ability to avoid duplicates.

We studied some of the important methods from the API, how they can help us as a developer to use a HashSet to its potential.

As always, code snippets can be found over on GitHub.