Set集合和List集合的区别?
Set集合:不允许元素重复,唯一的(元素可以为null) ,不能保证迭代的顺序恒久不变(底层哈希表和hascode),无序(存储和取出不一致)
List集合:允许元素重复,并且存储特点:有序性(存储和取出一致)
一.HashSet集合
[需求1]通过Set集合存储字符串并遍历
public static void main(String[] args) {
//创建Set集合对象,子实现类:HashSet集合
Set<String> set = new HashSet<String>();
//添加元素
set.add("hello") ;
set.add("java") ;
set.add("world") ;
//看他的唯一性,多存储一些元素
set.add("hello") ;
set.add("java") ;
set.add("world") ;
//遍历
for(String s: set) {
System.out.println(s);
}
}
从上述需求发现:Set集合存储元素的时候,可以保证元素的唯一性,原因什么?
HashSet集合的add方法底层依赖于双列集合HashMap,它依赖于两个方法,HashCode()方法和equals()方法
先比较字符串的HashCode()码值一样,再比较equals()方法
如果hasCode码值一样,还要比较内容是否相同,由于存储String,重写了equals()方法
//HashSet集合的add方法的源码
inteface Collection{
}
interface Set extends Collection{
}
class HashSet implements Set{
private HashMap<E,Object> map;
//创建HashSet集合对象的时候,起始底层创建了一个HashMap集合对象
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null; //add方法底层依赖于HashMap集合的
}
}
class HashMap<K,V> implements Map<K,V>{
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; //Node是一种键值对对象
//判断哈希表示是为空,如果为空,得到哈希表长度;set集合有元素的情况,哈希表不为空
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k; //k--->传入的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//一系列判断,主要还是eqauls()方法
e = p; //e =p = key
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key mapping:映射 (Servlet servlet-mapping)
V oldValue = e.value; //e.value=key ="hello" ,"world","java","world","java"
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue; //返回的oldValue:永远是第一次存储的那个元素
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
}
哈希方法
static final int hash(Object key) { //传入的字符串元素 "hello","java","world" ,"hello"....
int h; //哈希码值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //无符号右移
底层依赖于第一个方法hashCode()
}
[需求2]set集合存储自定义对象并遍历
public static void main(String[] args) {
Set<Student> set = new HashSet<Student>() ;
//创建学生对象
Student s1 = new Student("高圆圆", 27) ;
Student s2 = new Student("高圆圆", 28) ;
Student s3 = new Student("文章", 30) ;
Student s4 = new Student("马伊琍", 39) ;
Student s5 = new Student("高圆圆", 27) ;
//存储到集合中
set.add(s1) ;
set.add(s2) ;
set.add(s3) ;
set.add(s4) ;
set.add(s5) ;
//遍历
for(Student s : set) {
System.out.println(s.getName()+"---"+s.getAge());
}
}
按照正常套路,发现遍历集合的时候输出重复元素,怎么解决?
现在是自定义的类,HashSet集合的add()方法本身依赖于hashCode()和equals()方法,在Student类中并没重写这两个方法, 解决:重写这两个方法!
[注意]Set集合存储自定义对象遍历,需要在自定义类中重写hashCode()和equals()方法!
String类本身重写了equals方法,所以不需要再重写了!
二.LinkedHashSet集合
如果在开发中,元素唯一性,并且还要保证元素有序(存储和取出一致),使用LinkedHashSet集合
LinkedHashSet集合:
* 底层是一种链接列表和哈希表组成
* 可以保证元素的唯一性,是由哈希表决定的(hashCode()和equals())
* 可以保证元素的迭代顺序一致(有序),存储和取出一致,是由链表决定
public static void main(String[] args) {
//创建LinkedHashSet集合对象
LinkedHashSet<String> link = new LinkedHashSet<String>() ;
//添加元素
link.add("hello") ;
link.add("java") ;
link.add("world") ;
link.add("world") ;
link.add("world") ;
link.add("java") ;
//增强for遍历
for(String s: link) {
System.out.println(s);
}
}
输出结果:
hello
java
world
三.TreeSet集合
1.TreeSet集合默认情况下是通过自然顺序对集合中的元素排序.
2.TreeSet:可以保证元素唯一并且元素排序(Integer类型的元素自然升序)
* 自然排序
* 比较器排序
[需求1]给TreeSet集合存储以下元素:20,23,22,18,17,19,24...
public static void main(String[] args) {
//创建一个集合对象TreeSet集合对象
TreeSet<Integer> ts = new TreeSet<Integer>() ;
//给集合中存储元素
ts.add(20) ; //add()方法底层的源码是一个Map接口实例
ts.add(22) ;
ts.add(18) ;
ts.add(23) ;
ts.add(24) ;
ts.add(17) ;
ts.add(19) ;
ts.add(18) ;
ts.add(24) ;
//遍历
for(Integer i : ts) {
System.out.print(i +" ");
}
}
3.TreeSet集合的add的方法的源码(重点)
interface Collection{
}
interface Set extends Collection{
}
interface Map{
V put(K key, V value);
}
inteface NavigableMap exnteds Map{
V put(K key, V value);
}
class TreeMap implements NavigableMap extends Map{
private transient Entry<K,V> root; //根节点:作为键值对对象
public V put(K key, V value) { //key= 20,22,23,17,18,19,24,18,24
Entry<K,V> t = root; //Entry<k,V> t= this.root; //20
if (
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp; //cmp变量
Entry<K,V> parent; //parent =root = 根节点
// split comparator and comparable paths //如果有根节点,分两种方式:comparable接口 和Compartor(比较排序)
//比较器排序
Comparator<? super K> cpr = comparator; //创建了Compartor实例,属于比较函数(实现比较器排序)
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else { //自然排序的底层代码
if (key == null) //元素是否为空
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
//创建了实例Comparable接口的实例对象(自己实现类) //
do {
parent = t; // 开始存储:先存储根节点
//k,除过根节点后面的元素和根节点进行比较
cmp = k.compareTo(t.key); //底层依赖于就是Compareable接口中的compareTo比较
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;s
else
return t.setValue(value); //如果重复的值,将第一次存储的值存进去
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent); //创建了一个键值对对象
if (cmp < 0) //后面的元素大于前面元素
parent.left = e; //左边开始取元素
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null; //如果没有元素 ,返回nulll
}
}
class TreeSet implements Set{
private transient NavigableMap<E,Object> m;
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
通过对TreeSet集合的add的方法的源码分析,得出结论:
使用TreeSet集合的自然排序进行操作,该集合中数据类型一定要实现Comparable接口里面的comparTo()方法!
[需求2]使用TreeSet集合存储自定义对象(Student类型),并遍历!
分析:
如何排序?
按照学生的年龄从小到大进行排序 (主要条件)
唯一性:
如果成员变量的值一样认为是同一个对象
//对于自定义的类型,要实现自然排序,必须自定义的类型必须实现Comparable
//实现接口中的方法,compareTo() :比较方法
public class Student implements Comparable<Student>{
private String name ;
private int age ;
public Student() {
super();
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(Student s) { //源码 cmp = k.compareTo(t.key)
// return 0;
//按照某种规则,前提必须有这规则
//主要条件:按照年龄从小到大
int num = this.age - s.age ; //如果年龄相等,不一定是同一个人
//需要自己分析次要条件;
//年龄相同,姓名的内容不一定相同,比较姓名
int num2 = num==0 ? this.name.compareTo(s.getName()) : num ;
return num2 ;
}
}
public static void main(String[] args) {
//创建TreeSet集合对象
TreeSet<Student> ts = new TreeSet<Student>() ;//无参构造的方式自然排序
//创建学生对象
Student s1 = new Student("gaoyuanyuan",27) ;
Student s2 = new Student("liushishi",38) ;
Student s3 = new Student("gaoyuanyuan",28) ;
Student s4 = new Student("wanglihong",35) ;
Student s5 = new Student("wanglihong",30) ;
Student s6 = new Student("fengqingy",38) ;
Student s7 = new Student("gaoyuanyuan",27) ;
//java.lang.ClassCastException: org.westos_03.Student cannot be cast to java.lang.Comparable
ts.add(s1) ;
ts.add(s2) ;
ts.add(s3) ;
ts.add(s4) ;
ts.add(s5) ;
ts.add(s6) ;
ts.add(s7) ;
//遍历
for(Student s:ts) {
System.out.println(s.getName()+"---"+s.getAge());
}
}
4.TreeSet集合的比较器排序
TreeSet集合的构造方式不同,使用的排序方式也不同
* 自然排序:自定义的类实现Compareable接口,然后创建TreeSet对象,通过无参构造形式创建对象
* 比较器排序 :public TreeSet(Comparator<E> comparator)
两种方式:
1)自定义一个类,该类实现Comparator接口,重写Comparator接口中的compare()方法
2)直接使用接口匿名内部类的方式实现
[需求]
使用比较器排序的方式去,遍历Student对象,并且按照姓名长度进行比较
public static void main(String[] args) {
//创建TreeSet集合对象
//直接使用匿名内部类的方式实现
TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
//按照姓名长度进行比较
int num = s1.getName().length() - s2.getName().length() ;
//长度一样,还要比较姓名的内容是否相同
int num2 = num==0 ?s1.getName().compareTo(s2.getName()) : num ;
//最终看年龄是否一致
int num3 = num2 ==0 ? (s1.getAge() - s2.getAge()) : num2 ;
return num3 ;
}
}) ;
//创建学生对象
Student s1 = new Student("gaoyuanyuan", 27) ;
Student s2 = new Student("zhangguorong",29) ;
Student s3 = new Student("wuqilong", 40) ;
Student s4 = new Student("liushishi", 28) ;
Student s5 = new Student("fengqingy", 29) ;
Student s6 = new Student("gaoyuanyuan", 22) ;
Student s7 = new Student("gaoyuanyuan", 27) ;
ts.add(s1) ;
ts.add(s2) ;
ts.add(s3) ;
ts.add(s4) ;
ts.add(s5) ;
ts.add(s6) ;
ts.add(s7) ;
//遍历
for(Student s:ts) {
System.out.println(s.getName()+"----"+s.getAge());
}
}