java基础-集合框架之set

嘻嘻兮 嘻嘻兮     2022-08-01     524

关键词:

ps:本人小菜一枚,所以本文是自我学习后的一篇总结,大虾请您飘过


Set类概述

Set是最简单的集合,集合中的对象不按照特定的方式排序,并且没有重复的对象。

Set集合里多个对象之间没有明显的顺序,基本与Collection方法相同。只是行为不同(Set不允许包含重复元素)。


HashSet

HashSet按Hash算法来存储集合的元素,因此具有很好的存取和查找性能。

特点:

HashSet不是同步的,多个线程访问是需要通过代码保证同步

集合元素值可以使null。

注意:HashSet存储自定义对象必须重写hashCode()和equals()方法

重写hashCode基本的指导:
1.初始化一个整形变量,为此变量赋予一个非零的常数值,比如下文:int result = 1;
2.为对象内每个有意义的域f(即每个可以做equals()操作的域)计算出一个int散列码c

	boolean					c=(f?0:1)
	byte,char,short,int		c=(int)f
	long					c=(int)(f^(f>>>32))
	float					c=Float.floatToIntBits(f)
	double					c=Double.doubleToLongBits(f)	若结果为long类型则用上述规则处理long得到int
	Object					c=f.hashCode()
	数组					对每个元素应用上述规则
3.合并计算得到散列码:
result = 31 * result + c;
4.返回 result
5.检查hashCode() 最后生成的结果,确保相同的对象有相同的散列码

class Person {
	private String name;
	private int age;
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}
	/*
	 * 为什么选择31?
	 * 1,31是一个质数,质数是能被1和自己本身整除的数,减少了相乘出现同值的情况
	 * 2,31这个数既不大也不小
	 * 3,31这个数好算,2的五次方-1,2向左移动5位
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + age;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)//调用的对象和传入的对象是同一个对象
			return true;
		if (obj == null)//传入的对象为null
			return false;
		if (getClass() != obj.getClass())//判断两个对象对应的字节码文件是否是同一个字节码
			return false;
		Person other = (Person) obj;
		if (age != other.age)//以下比较对象的域
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
}

测试类:

	public static void main(String[] args) {
		HashSet<Person> hashSet = new HashSet<>();
		hashSet.add(new Person("张三", 23));
		hashSet.add(new Person("张三", 23));
		hashSet.add(new Person("李四", 24));
		hashSet.add(new Person("李四", 23));
		hashSet.add(new Person("王五", 25));
		hashSet.add(new Person("赵六", 25));
		for (Person person : hashSet) {
			System.out.println(person);
		}
	}

结果

Person [name=赵六, age=25]
Person [name=张三, age=23]
Person [name=李四, age=24]
Person [name=李四, age=23]
Person [name=王五, age=25]
看到了吧,上面的结果是不是去除了重复,而且是无序的

HashSet如何保证元素的唯一性呢?

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable{
	private transient HashMap<E,Object> map;
	private static final Object PRESENT = new Object();
	public HashSet() {
        map = new HashMap<>();
    }
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
    public int size() {
        return map.size();
    }
	public boolean isEmpty() {
        return map.isEmpty();
    }
    public boolean contains(Object o) {
        return map.containsKey(o);
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
    public void clear() {
        map.clear();
    }
}

看其源码(JDK1.8)可知,其底层依靠的是HashMap,由于未涉及到HashMap,便对其原理做简单概述,后面会详细介绍

使用Set集合都是需要去掉重复元素的, 如果在存储的时候逐个equals()比较, 效率较低,哈希算法提高了去重复的效率, 降低了使用equals()方法的次数

当存储对象的时候, 先调用对象的hashCode()方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象

如果没有哈希值相同的对象就直接存入集合

如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行equals()比较,比较结果为false就存入, true则不存

自定义类的对象存入HashSet去重复

类中必须重写hashCode()和equals()方法

hashCode(): 属性相同的对象返回值必须相同, 属性不同的返回值尽量不同(提高效率)

equals(): 属性相同返回true, 属性不同返回false,返回false的时候存储


LinkedHashSet

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable{
	HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
}
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
    public LinkedHashSet() {
        super(16, .75f, true);
    }
}

看其源码(JDK1.8)可知,其底层依靠的是LinkedHashMap,然而LinkedHashMap也是继承于HashMap的,所以归根结底他还是依赖HashMap

看其名字便可知,其与HashSet的不同之处是,他是有序的,所以他可以保证怎么存就怎么取

在其LinkedHashMap维持了一个双向链表

transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

简单使用

	public static void main(String[] args) {
		LinkedHashSet<Person> Set = new LinkedHashSet<>();
		Set.add(new Person("张三", 23));
		Set.add(new Person("张三", 23));
		Set.add(new Person("李四", 24));
		Set.add(new Person("李四", 23));
		Set.add(new Person("王五", 25));
		Set.add(new Person("赵六", 25));
		for (Person person : Set) {
			System.out.println(person);
		}
	}

结果

Person [name=张三, age=23]
Person [name=李四, age=24]
Person [name=李四, age=23]
Person [name=王五, age=25]
Person [name=赵六, age=25]

和上面的HashSet的结果比较,是不是存入的顺序与取出的顺序一致了


TreeSet

TreeSet是SortedSet接口的唯一实现,TreeSet可以确保集合元素处于排序状态(元素是有序的)。

TreeSet提供的几个额外方法:

Comparator comparttor(): 返回当前Set使用的Compara投入,或者返回null,表示以自然方式排序。  

Object first():返回集合中的第一个元素。  

Object last():返回集合中的最后一个元素。  

Objiect lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素可以不是TreeSet的元素)。  

Object higher(Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素可以不需要TreeSet的元素)。  

SortedSet subSet(fromElement, toElement):返回此Set的子集,范围从fromElement(包含大于等于)到toElement(不包含小于)。  

SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成。  

SortedSet tailSet(fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。

来简单使用下:

	public static void main(String[] args) {
		TreeSet<String> treeSet = new TreeSet<>();
		treeSet.add("efg");
		treeSet.add("abc");
		treeSet.add("abd");
		treeSet.add("abc");
		treeSet.add("bcd");
		treeSet.add("cde");
		System.out.println(treeSet);
	}
	//outPut:[abc, abd, bcd, cde, efg]

看其结果既去除了重复,又进行了自然排序

使用自定义类会如何?

	public static void main(String[] args) {
		TreeSet<Person> treeSet = new TreeSet<>();
		treeSet.add(new Person("张三", 23));
		treeSet.add(new Person("张三", 23));
		treeSet.add(new Person("李四", 23));
	}
	//Exception in thread "main" java.lang.ClassCastException: Person cannot be cast to java.lang.Comparable

很遗憾,报错了,Person这个类不能转化为Comparable

我们看下String类的声明:

public final class Stringextends Objectimplements Serializable, Comparable<String>, CharSequence

我们看到了String这个类实现了Comparable这个接口,查看API可得,此接口强行对实现它的每个类的对象进行整体排序,这意味着我们的Person类必须实现其Comparable接口吗?

答案当然是不一定的,查看TreeSet的API

public TreeSet(Comparator<? super E> comparator)

在其构造方法内可以传入一个比较器:Comparator

大家可能会混淆了Comparator和Comparable,先来看看代码的实现(以年龄排序从大到小排序,年龄相同比较姓名自然排序)

先来Comparator,在构造器内传入我们的比较器

	public static void main(String[] args) {
		TreeSet<Person> treeSet = new TreeSet<>(new Comparator<Person>() {
			@Override//匿名内部类,在其内部实现其如何比较
			public int compare(Person per1, Person per2) {
				int num = per2.getAge() - per1.getAge();
				return num == 0 ? per1.getName().compareTo(per2.getName()) : num;
			}
		});
		treeSet.add(new Person("张三", 23));
		treeSet.add(new Person("张三", 23));
		treeSet.add(new Person("李四", 24));
		treeSet.add(new Person("李四", 23));
		treeSet.add(new Person("王五", 25));
		treeSet.add(new Person("赵六", 25));
		for (Person person : treeSet) {
			System.out.println(person);
		}
	}

结果

Person [name=王五, age=25]
Person [name=赵六, age=25]
Person [name=李四, age=24]
Person [name=张三, age=23]
Person [name=李四, age=23]

你可能对中文的字符的排序有疑问,你可以将其替换为英文字符串,或者看其下面代码,你就知道了如何比较

		System.out.println("王:"+('王'+0));//char字符对应的Unicode码表值
		System.out.println("赵:"+('赵'+0));
		//王:29579
		//赵:36213

再来看看Comparable实现:

改造我们的Person

class Person implements Comparable<Person> {
	private String name;
	private int age;
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public int getAge() {
		return age;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	@Override
	public int compareTo(Person per) {
		//这部分代码是和Comparator.compare方法中内容是一样的
		int num = per.getAge() - this.getAge();
		return num == 0 ? getName().compareTo(per.getName()) : num;
	}
}

测试:

	public static void main(String[] args) {
		TreeSet<Person> treeSet = new TreeSet<>();
		treeSet.add(new Person("张三", 23));
		treeSet.add(new Person("张三", 23));
		treeSet.add(new Person("李四", 24));
		treeSet.add(new Person("李四", 23));
		treeSet.add(new Person("王五", 25));
		treeSet.add(new Person("赵六", 25));
		for (Person person : treeSet) {
			System.out.println(person);
		}
	}

可以想象,这个结果和上面代码的结果是一样的。通俗点的方式说Comparable是将比较的代码写在了自己的内部,而Comparator需单独定义一个类对其进行比较。

Comparable & Comparator

Comparator位于包java.util下,而Comparable位于包   java.lang下

都是用来实现集合中元素的比较、排序的,只是 Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序,所以,如想实现排序,就需要在集合外定义 Comparator 接口的方法或在集合内实现 Comparable 接口的方法。

两种方法各有优劣, 用Comparable 简单, 只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码, 用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator 里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。

两种比较小结一下

a.自然顺序(Comparable)

TreeSet类的add()方法中会把存入的对象提升为Comparable类型

调用对象的compareTo()方法和集合中的对象比较

根据compareTo()方法返回的结果进行存储

b.比较器顺序(Comparator)

创建TreeSet的时候可以制定 一个Comparator

如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的顺序排序

add()方法内部会自动调用Comparator接口中compare()方法排序

调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数

两种方式的区别

TreeSet构造函数什么都不传, 默认按照类中Comparable的顺序(没有就报错ClassCastException)

TreeSet如果传入Comparator, 就优先按照Comparator

TreeSet其底层依靠的是TreeMap,其内部维护着一颗二叉排序树,之后会详细讲解

public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable{
    private transient NavigableMap<E,Object> m;		//TreeMap实现的接口
    private static final Object PRESENT = new Object();
	public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    public int size() {
        return m.size();
    }
    public boolean isEmpty() {
        return m.isEmpty();
    }
    public boolean contains(Object o) {
        return m.containsKey(o);
    }
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }
    public void clear() {
        m.clear();
    }
}

对List和Set迭代进行一下总结

List

a.普通for循环, 使用get()逐个获取

b.调用iterator()方法得到Iterator, 使用hasNext()和next()方法

c.增强for循环, 只要可以使用Iterator的类都可以用

d.Vector集合可以使用Enumeration的hasMoreElements()和nextElement()方法

Set

a.调用iterator()方法得到Iterator, 使用hasNext()和next()方法

b.增强for循环, 只要可以使用Iterator的类都可以用

阿花宝宝java基础笔记之集合框架

基本接口:Collection 接口:List 实现类:ArrayListLinkedList 接口:Set(唯一,无序) 实现类:HashSet(哈希)TreeSetMap接口:Map 实现类:HashMap---------------------------------------------------------------------- 查看详情

java基础知识之什么是集合框架(代码片段)

Java基础知识之什么是集合框架,前面的文章,我们已经学习了Java的一些基础知识,比如泛型、注解等等内容,接着本博客继续学习Java中一个很常见的内容,集合。1、什么是Java中的集合框架?JavaCollections框架由接口和类组成,... 查看详情

集合框架之set学习

前言:    1、何为框架:可以理解为一个基础结构,在基础结构上进行进一步开发会变得很方便。     2、三种集合类型:集合(Set):元素无序不可重复; 列表(List):元素有序可重复; 映... 查看详情

java成神之——集合框架之arraylist,lists,sets

集合集合种类ArrayList声明增删改查元素遍历几种方式空集合子集合不可变集合LinkedListLists排序类型转换取交集移动元素删除交集元素Sets集合特点常用方法根据set创建list集合集合种类List<String>list=newArrayList<>(data);Set<Stri... 查看详情

java基础知识回顾之四-----集合listmap和set

...集合介绍我们在进行Java程序开发的时候,除了最常用的基础数据类型和String对象外,也经常会用到集合相关类。集合类存放的都是对象的引用,而非对象本身,出于表达上的便利,我们称集合中的对象就是指集合中对象的引用... 查看详情

java集合之hashset哈希set基础

hashSet实现set接口、serializable、cloneable接口。不允许有重复值,但可以null,不保证与插入顺序一致。HashSet迭代的时间与HashSet实例的大小(元素的数量)和底层HashSet实例(桶的数量)的“容量”的和成比例。因此,若迭代性能很... 查看详情

java基础知识之什么是集合框架(代码片段)

Java基础知识之什么是集合框架,前面的文章,我们已经学习了Java的一些基础知识,比如泛型、注解等等内容,接着本博客继续学习Java中一个很常见的内容,集合。1、什么是Java中的集合框架?JavaCollections... 查看详情

第七节2:java集合框架之map和set(代码片段)

文章目录一:搜索二:Map使用(1)Map介绍(2)TreeMap和HashMap区别(3)Map常用方法说明(4)使用示例A:TreeMap使用示例B:HashMap使用示例三:Set使用(1)Set使用(2)TreeSet和HashSet区别(3)Set常用方法说明(4)HashSet使用示例一:... 查看详情

java基础之集合框架

1:String类:字符串(重点)   (1)多个字符组成的一个序列,叫字符串。     生活中很多数据的描述都采用的是字符串的。而且我们还会对其进行操作。所以,java就提供了这样的一个类供我们使用。 ... 查看详情

jdk源码阅读笔记之java集合框架(基础篇)

  结合《jdk源码》与《thinkinginjava》,对java集合框架做一些简要分析(本着实用主义,精简主义,遂只会挑出个人认为是高潮的部分)。  先上一张java集合框架的简图:    会从以下几个方面来进行分析:java数组;ArrayL... 查看详情

java集合系列01之总体框架

Java集合是java提供的工具包,包含了常用的数据结构:集合、链表、队列、栈、数组、映射等。Java集合工具包位置是java.util.*Java集合主要可以划分为4个部分:List列表、Set集合、Map映射、工具类(Iterator迭代器、Enumeration枚举类、Ar... 查看详情

java集合框架之set,map(代码片段)

大家好,我是Morning,在CSDN写文,分享一些Java基础知识,一些自己认为在学习过程中比较重要的东西,致力于帮助初学者入门,希望可以帮助你进步。感兴趣的欢迎关注博主,和博主一起学习Java知识。... 查看详情

java集合之collection架构

Java集合之Collection架构首先,我们对Collection进行说明。下面先看看Collection的一些框架类的关系图:  Collection是一个接口,它主要的两个分支是:List 和 Set。List和Set都是接口,它们继承于Collection。List是有序的队列,Lis... 查看详情

javase基础---集合之set

 一.介绍案例一 测试代码:importjava.util.HashSet;importjava.util.Iterator;importjava.util.Set;publicclasstest   publicstaticvoidmain(String[]args)       Setset=newHashSet();       set.add("blue 查看详情

redis之set集合

RedisSet集合Set就是一个集合,集合的概念就是一堆不重复值的组合。利用Redis提供的Set数据结构,可以存储一些集合性的数据。比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。因为Redis... 查看详情

java基础--集合框架的认识

一、集合框架    对于不知道存储数量和更复杂的方式存储对象用集合框架。    其中有几个常用的接口和实现类:Collection父接口、List接口,Set接口,Map接口,    ArrayList实现类、LinkedL... 查看详情

19701android零基础入门03java常用工具类04java集合03set集合01set概述

19701Android零基础入门03Java常用工具类04Java集合03Set集合01Set概述本文知识点:Set概述说明:因为时间紧张,本人写博客过程中只是对知识点的关键步骤进行了截图记录,没有对截图步骤进行详细的文字说明(后面博主时间充裕了... 查看详情

java基础之容器集合(collection和map)(代码片段)

目录前言一.Collection集合      1.1List集合1.1.1ArrayList集合1.1.2LinkedList集合1.2Set集合1.2.1HashSet集合HashSet集合保证元素唯一性源码分析:1.2.2TreeSet集合比较器排序Comparator的使用: 二.Map集合 2.1Map集合的概述与特点2.2Map集合的获... 查看详情