java集合(1)
引入
我们我们保存多个数据时大多是使用数组,但数组有许多不足之处
- 数组的长度必须在开始时指定,而且长度一旦确定便不能修改
- 保存的必须为同一类型的元素
- 数组增加/删除元素较麻烦
//数组扩容
int[] num1 = new int[1];
num1[0]=1;//创建数组并赋值
int[] num2 = new int[num1.length+1];
for (;;){}//将原数组内容复制到新数组(省略)
num2[num2.length-1]=1;
相比起来,集合则有很多优点:
- 可以动态的保存任意类型的多个元素
- 提供了一系列方便操作对象的方法:add、remove、set、get等
- 使用集合添加/删除元素的代码较简洁
集合框架的层次结构
Java集合框架主要包括两大接口:Collection
和Map
,它们分别代表了两种不同的集合类型。
- Collection接口:继承自单列集合,存放单个的对象,所以其子类 List 和 Set 也都是单列集合。它定义了集合的基本操作,如添加(add)、删除(remove)、遍历(iterator)等。
Collection
接口有两个主要的子接口:List
和Set
。- List:有序集合,允许元素重复。主要实现类有
ArrayList
、LinkedList
等。 - Set:无序集合,不允许元素重复。主要实现类有
HashSet
、LinkedHashSet
、TreeSet
等。
- List:有序集合,允许元素重复。主要实现类有
- Map接口:双列集合,存放的数据往往两两一组。将键(Key)映射到值(Value)的对象,也就是K-V,一个键可以映射到最多一个值。主要实现类有
HashMap
、Hashtable
、TreeMap
等。
ArrayList arrayList = new ArrayList();//单列集合
arrayList.add("Rookie");
arrayList.add("jackey");
HashMap hashMap = new HashMap();//双列集合
hashMap.put("Key1","小明");
hashMap.put("Key2","小黄");
java.util.Collection
├── java.util.List
│ ├── java.util.ArrayList
│ ├── java.util.LinkedList
│ ├── java.util.Vector
├── java.util.Set
│ ├── java.util.HashSet
│ │ └── java.util.LinkedHashSet
│ ├── java.util.LinkedHashSet<E>
│ ├── java.util.TreeSet<E>
│ │ └── java.util.concurrent.ConcurrentSkipListSet<E>
│ └── java.util.SortedSet<E>
│ ├── java.util.TreeSet<E>
│ └── java.util.concurrent.ConcurrentSkipListSet<E>
├── java.util.Queue<E>
│ ├── java.util.Deque<E>
│ │ ├── java.util.ArrayDeque<E>
│ │ └── java.util.LinkedList<E>
│ ├── java.util.LinkedList<E>
│ ├── java.util.PriorityQueue<E>
│ └── java.util.concurrent.BlockingQueue<E>
│ ├── java.util.concurrent.ArrayBlockingQueue<E>
│ ├── java.util.concurrent.LinkedBlockingQueue<E>
│ └── 更多并发阻塞队列...
└── java.util.Map<K,V>
├── java.util.HashMap<K,V>
│ └── java.util.LinkedHashMap<K,V>
├── java.util.TreeMap<K,V>
├── java.util.Hashtable<K,V>
├── java.util.Properties
├── java.util.WeakHashMap<K,V>
├── java.util.IdentityHashMap<K,V>
└── java.util.concurrent.ConcurrentHashMap<K,V>
Collection
特点
1、继承自 Iterable 接口
public interface Collection<E> extends Iterable<E> {
2、其子类可存放多种类型的元素,每个元素都可以是 Object类及其子类
3、有些 Collection 的实现类,可以存放重复的元素,有些则不能
4、有些 Collection 的实现类存放数据是有序的(如 List 存放顺序和取出数据是一致的),有些则为无序(如 Set 存放和取出的顺序并不完全一样)
5、Collcetion 接口没有直接的实现子类,都是通过他的子接口 Set 和 List 来实现的
常用方法
因为接口不能被实例化,所以我们以其实现该接口的子类 ArrayList 来演示
1、add()添加单个元素
ArrayList eg = new ArrayList();
eg.add("木楠");
eg.add(10);//这里系统会自动装箱,将int转化为integer相当于eg.add(new Integer(10));
eg.add(true);
System.out.println("example"+eg);
执行结果:example:[木楠, 10, true]
2、 remove()删除元素,该方法有两种重载版本:
1、通过索引查找指定位置的元素,该方法返回 Object 也就是所查找的元素
2、通过元素内容查找指定的元素,该方法返回 boolean 表是否查找成功
如果元素内的某个 int 型变量和已存在的索引相冲突,且想通过查找内容来删除某个 int 型变量,需使用 Integer.valueOf()
ArrayList eg = new ArrayList();
eg.add("木楠");
eg.add(10);
eg.add(true);
eg.add(1);
eg.add(2);
eg.remove(0);//删除索引为0的元素
eg.remove(true);//删除元素true
eg.remove(Integer.valueOf(2));//索引2与元素2相冲突,删除内容为2的元素
3、contains()查找某个元素是否存在,并返回boolean值,存在为 true 不存在为 false
4、size()返回集合内元素个数
5、isEmpty()判断集合是否为空
6、clear()清空集合内所有元素
7、addAll()将一个集合(或集合的子集)的所有元素添加到另一个集合中。这个方法有两个不同的重载版本:
1、num1.addAll(num2):直接将集合num2内的所有元素添加到num1中
2、num1.addAll(2,num2):将集合 num2 中的所有元素从 num1 的指定索引“2”插入。如果index
超出 num1 的当前大小,则会在 num1 的末尾添加 num2 的所有元素。如果index
为负数,则报错。
8、containsAll()检查调用该方法的集合 (调用集合) 是否包含指定集合 (参数集合) 中的所有元素。
9、removeAll()从调用该方法的集合 (调用集合) 中移除所有包含在指定集合 (参数集合) 中的元素。它会遍历参数集合中的每一个元素,并从调用集合中移除所有与之相等的元素。如果调用集合由于此操作而发生了更改(即至少有一个元素被移除),则返回 true
;否则返回 false
。
ArrayList list1 = new ArrayList(Arrays.asList(1, 2, 3, 4, 5));
ArrayList list2 = new ArrayList(Arrays.asList(3,4));
//从list1中移除所有在list2中的元素
boolean result = list1.removeAll(list2);
//此时list1:[1,2,5],result为true
-------------------------------------------------------------
ArrayList list1 = new ArrayList(Arrays.asList(1, 2, 3, 4, 5));
ArrayList list2 = new ArrayList(Arrays.asList(5,6));
//从list1中移除所有在list2中的元素
boolean result = list1.removeAll(list2);
//此时list1:[1,2,3,4],result为true
-------------------------------------------------------------
ArrayList list1 = new ArrayList(Arrays.asList(1, 2, 3, 4, 5));
ArrayList list2 = new ArrayList(Arrays.asList(7,6));
//从list1中移除所有在list2中的元素
boolean result = list1.removeAll(list2);
//此时list1:[1,2,3,4,5],result为false
遍历方法
一、使用 Iterable (迭代器)
上面我们提到 Collection 的特点之一就是继承自 Iterable 接口
1、Iterable 对象俗称迭代器,主要用于遍历 Collection 集合中的元素。
2、所有实现了 Collection 接口的集合类都有一个 Iterable() 方法,用以返回一个实现了 Iterator 接口的对象,即返回一个迭代器。
3、
4、Iterable 仅用于遍历集合,Iterable 本身并不存放对象。
使用方法
首先使用 Iterator 自定义迭代器名称 = 集合名称.iterator() 得到该集合的迭代器,然后再使用 hasNext() 和 next() 两方法。
刚开始 next() 方法会指向集合的第一个元素之前(类似于指针),每调用一次该方法就会将指针下移,并将该位置的元素返回(返回为 Object 类),
而 hasNext() 则会判断下面是否存在元素,并返回 Boolean 值。还有 remove() 方法因为不常用,不再深入了解。
因此我们通常使用while循环,使用 hasNext() 方法作为判断条件判断下面是否仍存在元素,如果存在则使用 next() 方法。
Collection list1 = new ArrayList();
list1.add(new book("枫原万叶",18));
list1.add(new book("阿蕾奇诺",19));
list1.add(new book("那维莱特",20));
Iterator myit = list1.iterator();
//使用while循环,输入itit可快捷生成
while (myit.hasNext()) {
Object next = myit.next();
System.out.println(next);
}
class book{......}
此时遍历完成,而迭代器指向该集合最后一个元素,如果再想遍历并调用 next() 方法,其就会抛出异常 NoSuchElementException ,如果想要重新遍历,则需重置遍历器:
Iterator myit = list1.iterator();
iterator.remove()
方法用于在遍历过程中安全地删除元素。但是,这个调用必须在next()
方法之后,否则将会抛出IllegalStateException
。这是因为remove()
方法需要知道要删除的是哪个元素,而next()
方法提供了这个信息。如果在调用next()
之后没有立即调用remove()
,则迭代器会失去这个信息,因此不允许调用remove()
。
二、使用for循环增强
增强 for 循环,可以代替iteration迭代器。增强 for 循环就是简化版的 iteration,本质一样,只能用于遍历集合和数组。
while (myit.hasNext()) {
Object next = myit.next();
System.out.println(next);}
---------------------------------------
for (Object book:list1){
System.out.println(book);}
同时也有几点需要注意
1、for循环是在 Collection 之中,因此集合、数组都可使用
2、增强for循环底层调用的也是 Iterable (迭代器),可以理解为简化版本
3、输大写字母“I”+enter可快捷生成增强for循环
List接口
介绍
前面已有介绍,List 接口代表了一个有序集合,是Collection接口的一个子接口,上文所介绍的方法为Collection接口内的方法,Set和List接口都可使用,但该部分介绍的为子接口List,Set接口相关的类不可使用。
List接口有以下几个特点:
1、List集合类中元素有序(即添加顺序与取出顺序一致),且可重复。
2、List集合中的每个元素都有其对应的顺序索引,且从0开始。
List arrayList = new ArrayList(Arrays.asList("一", "二", "三", "四", "五"));
System.out.println(arrayList.get(3));
//输出集合arrayList的第三个元素,即"四"
常用方法
1、add()添加元素,有两种重载
a、直接在集合尾部添加元素
b、在指定索引处添加元素,后方的元素后移
List list1 = new ArrayList(Arrays.asList("一", "二", "三"));
list1.add("新元素");//直接在集合尾部添加元素
list1.add(2,"打断");//在指定索引处添加元素,后方的元素后移
//此时list1:[一, 二, 打断, 三, 新元素]
2、addAll() 使用同Collection接口内的addAll()方法,有两个重载版本。
3、get(int index)获取指定索引处的元素。
4、indexOf()获取指定元素在集合内首次出现的索引位置。lastIndexOf()获取指定元素在集合内最后一次出现的索引位置。
5、remove()删除指定索引位置的元素,并返回该元素。
6、set()将指定索引位置的元素设置(修改)为指定元素。
list1.set(2,"yi");//将索引为2的元素设置为"yi"
7、subList(int a,int b)返回索引[a,b)的子集合(包含a不包含b)
List list1 = new ArrayList(Arrays.asList("一", "二", "三", "四", "五"));
System.out.println(list1.subList(2,4));
//输出[三, 四]
遍历方法
一、使用iterator迭代器
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.print(next);
}
二、使用增强for循环
System.out.println();
for (Object o :list1) {
System.out.print(o);
}
三、使用普通for循环
System.out.println();
for (int i = 0; i < list1.size(); i++) {
System.out.print(list1.get(i));
}
ArrayList
ArrayList
实现了 List
接口,是一个可以动态调整大小的数组。与普通的数组相比,ArrayList
提供了更加灵活和强大的功能,比如动态扩容、自动管理内存等。
特点:
一、可以放入任何元素,包括空值,且可以放入多个空值
二、ArrayList是由数组来实现数据存储的
三、ArrayList基本等同于Vector,但ArrayList执行效率高,非线程安全,多线程时不推荐使用
四、扩容机制:
1、ArrayList中维护了一个Object类型的数组elementData,因为Object为所有类的父类,所以ArrayList可以放入任何元素。
2、当创建ArrayList对象时若使用的为无参构造器,则初始的elementData容量为0,首次添加元素,系统会将elementData扩容为10,之后再扩容,则会扩容至1.5倍大小。
3、如果使用有参的构造器则初始elementData容量为指定的大小,如需扩容,也是扩容为原来的1.5倍大小。
Vector
与 ArrayList
类似,Vector
也允许存储重复的元素,并且元素是有序的,但 Vector
是线程安全的。这也意味着在多线程环境中,Vector
的性能可能不如非线程安全的 ArrayList,但可保证线程安全。
Vector的底层也是一个对象数组,可放入任何元素,这一点与 ArrayList 相同。
扩容机制则不同,如果使用无参构造器,则其默认长度也为10,但第二次扩容时则会直接扩容至两倍,使用有参构造时同理,容量为指定的大小,但扩容时也会扩容至两倍。
LinkedList
底层操作机制
一、LinkedList底层维护了一个双向链表,可添加任何元素,非线程安全,多线程时不推荐
二、LinkedList中维护了两个属性 first 和 last 分别指向首节点和尾节点
三、每个节点(Node对象)里面又维护了prev、next、item三个属性、其中通过 prev 指向前一个,通过 next 指向后一个,最终实现双向链表
四、LinkList添加和删除元素,不是通过数组来实现的,只需改变前一个节点的和后一个节点的首尾,共四个属性即可,因此效率较高
双向链表在后面会详细讲解,这里我们创建一个Node类来模拟双向链表来帮助理解。
public class Go {
public static void main(String[] args) {
//模拟双向链表,先创建节点
Node no1 = new Node("1、半阙");
Node no2 = new Node("2、新词");
Node no3 = new Node("3、三篇");
Node no4 = new Node("4、旧赋");
//连接三个节点,形成双向链表
no1.next=no2;
no2.next=no3;
no3.next=no4;
no4.prev=no3;
no3.prev=no2;
no2.prev=no1;
//创建首尾节点
Node first=no1;
Node last=no4;
System.out.println("—————正序遍历—————");
while (true){
if (first==null)
break;
System.out.println(first);
first=first.next;
}first=no1;//重置first节点
//倒序遍历
System.out.println("—————倒序遍历—————");
while (true){
if (last==null)
break;
System.out.println(last);
last=last.prev;
}last=no4;//重置last节点
}
}
class Node{
public Object item;
public Node next;
public Node prev;
public Node(Object item) {
this.item = item;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
'}';
}
}
执行结果:
—————正序遍历—————
Node{item=1、半阙}
Node{item=2、新词}
Node{item=3、三篇}
Node{item=4、旧赋}
—————倒序遍历—————
Node{item=4、旧赋}
Node{item=3、三篇}
Node{item=2、新词}
Node{item=1、半阙}
理解 LinkedList的结构后,我们再来看 LinkedList 的增删改查所需操作
通常缩写CRUD代表增删改查:增C (Creat) 查R (Read)改U (Update) 删D (Delete)
增C (Creat)
添加共有三种方法,分别是add(),addFirst(),addLast(),
其中add()有两种重载方法:
1、add(E e) 等同于 addLast() 表示直接在集合的尾部添加元素。
2、add(int index, E element)表示在指定索引处添加元素
addFirst()表示在集合首部添加元素;addLast()表示在集合尾部添加元素
LinkedList list1 = new LinkedList();
list1.add(1);//直接添加元素"1"
list1.addLast(2);//在尾部添加元素"2"
list1.addFirst(3);//在开头添加元素"3"
list1.add(1,"new");//将元素"new"添加到序列1处
此时list1:[3, new, 1, 2]
删D (Delete)
删除的方法与增加的方法大体相同,但也略有区别:删除共有三种方法,分别是remove(),removeFirst(),removeLast(),
其中remove()有三种重载方法:
1、remove()无参表示删除首个元素,等同于removeFirst()
2、remove(int index)表示删除指定索引处的元素。
3、remove(Object o)表示删除集合内第一个出现的指定元素,并将该元素返回。
removeFirst()表示删除集合的首个元素;addLast()表示删除集合的最后一个元素
LinkedList list1 = new LinkedList(Arrays.asList("零","一","二","三","四","五","六","七"));
list1.remove(2);//删除第2个元素"二"
list1.remove("三");//删除元素"三"
list1.removeFirst();//删除第一个元素,即"零"
list1.removeLast();//删除最后一个元素,即"七"
此时list1:[一, 四, 五, 六]
改U (Update)
set(int index, E element) 表示将指定索引处的元素修改为所给值。
LinkedList list1 = new LinkedList(Arrays.asList("零","一","二"));
list1.set(1,"new");//将序列1处的元素修改为"new"
此时list1:[零, new, 二]
查R (Read)
get(int index)查看指定索引处的元素。
因为 LinkedList 实现了 List 接口,所以上文举例的三种遍历方式(iterator迭代器、增强for循环、普通for循环)都可使用。
ArrayList和LinkedList比较
底层结构 | 增删效率 | 改查效率 | |
ArrayList | 可变数组 | 较低(数组扩容) | 较高(通过索引) |
LinkledList | 双向链表 | 较高(链表追加) | 较低(逐个遍历) |
1、如果改查操作较多,选择ArrayList
2、如果增删操作较多,选择LinkedList
3、一般来说在程序中,查询操作较多,因此大部分都选择ArrayList
4、在同一项目中,不同的模块可根据需求来选择不同的类
Set接口
Set接口继承自Collection接口,是Java集合框架中的一个基础接口。Set接口的实现类主要包括HashSet、LinkedHashSet、TreeSet等。有以下几个特点:
1、无序(添加和取出的顺序不一定一样,且只会按照该顺序开读取,下次读取时也不会改变),没有索引
2、不允许有重复元素(系统会忽略添加的重复元素,并返回false),最多包含一个null
3、因Set接口继承自Collection接口,所以遍历方式通用,但无法使用普通for循环,因为普通for循环需要通过索引或get方法来获取元素,但Set接口无法实现这两种方法
我们以set接口的实现类HashSet来举例
Set set = new HashSet();
set.add("top");//首次添加top
set.add("mid");
set.add("top");//第二次添加top
set.add("sup");
set.add("baolan");
set.add(null);//首次添加null
set.add(null);//第二次添加null
System.out.println("————————————迭代器———————————");
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.print(next+"、");
}
System.out.println();
System.out.println("—————————增强for循环—————————");
for (Object o :set) {
System.out.print(o+"、");
}
输出:
————————————迭代器———————————
null、top、mid、baolan、sup、
—————————增强for循环—————————
null、top、mid、baolan、sup、
HashSet
1、HashSet实现了Set接口
2、HashSet的底层未HashMap,而HashMap的底层为数组+链表+红黑树
//HashSet的构造方法
public HashSet() {
map = new HashMap<>();
}
3、不能有重复元素,可以存放一个null值
public class Go {
public static void main(String[] args) {
Set set = new HashSet();
set.add(null);
set.add(null);//第二次存放null,忽略
set.add("123");
set.add("123");//第二次存放元素"123",忽略
set.add(new dog("aaa"));
set.add(new dog("aaa"));//成功存放
set.add(new String("top"));
set.add(new String("top"));//忽略
System.out.println(set);
}
}
class dog{...}
输出:[null, 123, top, dog{name='aaa'}, dog{name='aaa'}]
4、HashSet不能保证存取顺序一致,取出顺序取决于系统首次确定的取出顺序
5、可通过remove(Object o)来删除指定对象
实现方法
上文我们说到HashSet的底层未HashMap,而HashMap的底层为数组+链表+红黑树,这里我们就来模拟以方便理解
public class Go {
public static void main(String[] args) {
//模拟一个HashSet的底层(HshMap的底层结构)
//创建一个数组,类型为Node[]
Node[] table = new Node[5];//创建数组
Node t2 = new Node("t2", null);
table[2]=t2;//创建节点并放到table[2]
Node t21 = new Node("t2-1", null);
table[2].next=t21;//将t21结点挂载到t2结点形成链表
Node t22 = new Node("t2-2", null);
t21.next=t22;//将t22结点挂载到t21结点,当挂载一定长度后形成红黑树(以后学习)
table[3] = new Node("t3", null);//创建节点并放到table[3]
}
}
class Node {//结点,存储数据,可以指向下一个结点,从而形成链表
Object item;//存放数据
Node next;//指向下一个结点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
此时table表:
table[0]=null
table[1]=null
table[2]=Node{item=t2, next=Node{item=t2-1, next=Node{item=t2-2, next=null}}}
table[3]=Node{item=t3, next=null}
table[4]=null
添加元素的过程:
1、添加一个元素t2时,先得到其hash值(不直接等于hashcode),再将其转化为索引值,因此索引是由hash值确定的,大概率不同(hash值是有限的,但要添加的元素是无限的,所以大概率不同,小概率相同,我们应重写hashcode方法以避免内容不同,但因hashcode相同而没有添加的情况),所以HashSet无序
2、找到目标数据表table[2],并检查该位置是否已有元素,如果没有则直接添加,如果有则调用equals方法(判断取决于不同的类或者重载的equals)对t2、t2-1、t2-2...依次进行比较,相同则放弃添加,不相同则以链表的方式添加至末尾
3、在java8中如果链表长度大于等于 TREEIFY_THRESHOLD(默认为8),且当前HashMap的容量(capacity)大于等于 MIN_TREEIFY_CAPACITY(默认为64)时,就会考虑树化。
这个条件是为了避免在HashMap的容量还相对较小的情况下就进行树化,因为这样做可能会因为扩容而导致树化的努力白费(扩容会重新哈希并可能减少哈希冲突)。如果当前容量小于 MIN_TREEIFY_CAPACITY,HashMap会选择扩容而不是树化。
4、首次创建HashSet类实例并添加元素时,数组扩容至16,但其临界值为threshold为16*0.75(LoadFactor)=12,一旦到达该临界值就会扩容至16*2=32(无论是否在同一个哈希桶,只要满足32就会扩容),同时新的临界值为32*0.75(LoadFactor)=24,依此类推
我们以一个例题来加深理解:
定义一个Employee类,该类包括private成员name和age,要求:1、创建三个Employee对象放入HashSet中,当name和age的值相同时,认为是相同的员工,不能添加到HashSet集合中
public class Go {//主方法
public static void main(String[] args) {
HashSet myhash = new HashSet();
System.out.println(myhash.add(new Employee("李华",8)));
System.out.println(myhash.add(new Employee("李一",8)));
System.out.println(myhash.add(new Employee("李华",8)));
//使用了new关键字所以hash值不一样,但内容一致
System.out.println(myhash);
}
}
class Employee{
private String name;
private int age;
public Employee(String name, int age) {
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 boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Employee employee = (Employee) object;
return age == employee.age && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}//为防止出现hashcode相同但内容不同的情况,hashcode和equals方法都要重写
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
执行结果:
true//添加成功
true//添加成功
false//添加失败
[Employee{name='李华', age=8}, Employee{name='李一', age=8}]
加深难度:修改Employee类使其内部包含private属性name,sal,birthday,其中birthday为自建类MyDate,属性包括year,month,day,要求:当name和birthday相同时拒绝录入:
package pack.chn.class1.pack;
import java.util.*;
public class Go {//主方法
public static void main(String[] args) {
HashSet myhash = new HashSet();
System.out.println(myhash.add(new Employee("jack", 12, new MyDate(2022, 3, 5))));
System.out.println(myhash.add(new Employee("jack", 13, new MyDate(2022, 3, 5))));
System.out.println(myhash.add(new Employee("rose", 11, new MyDate(2012, 3, 5))));
System.out.println(myhash.add(new Employee("rose", 11, new MyDate(2012, 5, 5))));
for (Object o : myhash) {
System.out.println(o);
}
}
}
class Employee {
private String name;
private int sal;
private MyDate date;
public Employee(String name, int sal, MyDate date) {
this.name = name;
this.sal = sal;
this.date = date;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSal() {
return sal;
}
public void setSal(int sal) {
this.sal = sal;
}
@Override//sal与判断无关,所以无需写入该属性
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Employee employee = (Employee) object;
return Objects.equals(name, employee.name) && Objects.equals(date, employee.date);
}
@Override
public int hashCode() {
return Objects.hash(name, date);
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + sal +
", date=" + date +
'}';
}
}
class MyDate {
public int year;
public int month;
public int day;
public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
@Override//因为MyDate也许判断是否相等,所以该类也需重写两方法
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
MyDate myDate = (MyDate) object;
return year == myDate.year && month == myDate.month && day == myDate.day;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
}
执行结果:
true//成功添加
false//name和MyDate相同,不允许添加,sal虽不同但无关判断
true//成功添加
true//name和sal相同,但MyDate不同,允许添加
Employee{name='rose', age=11, date=MyDate{year=2012, month=5, day=5}}
Employee{name='rose', age=11, date=MyDate{year=2012, month=3, day=5}}
Employee{name='jack', age=12, date=MyDate{year=2022, month=3, day=5}}
LinkedHashSet
LinkedHashSet
继承自 HashSet
,并实现了 Set
接口。它保留了集合中元素的插入顺序,并提供了 HashSet
的所有功能,如不包含重复元素和快速的查找速度
1、LInkedHashSet是HashSet的子类
2、LinkedHashSet的底层是一个LinkedHashMap,底层维护了一个数组+双向链表
3、LinkedHashSet根据元素的HashCode值来决定元素的存储位置,同时使用链表来维护元素的次序,这使元素看起来是以插入顺序保存的
4、LinkedHashSet同样不允许重复元素
实现方法
1、LinkedHashSet的底层维护了一个数组+双向链表的LinkedHashMap(为HashMap的子类)
2、首次添加时,数组先扩容到16,数组类型为HashMap$Node[],但实际存放的为LinkedHashMap$Entry类型(后者为前者的子类),该类型每个节点都有 before 和 after 属性以便实现双向链表
3、在添加元素时,先求hash值,再求索引,以确定该元素在集合中的位置,添加规则和hashset相同
4、虽然数组内顺序和添加顺序不一致,但因各个节点的before和after相连,所以可保证取出顺序与添加顺序一致
public class Go {//主方法
public static void main(String[] args) {
Set set = new LinkedHashSet();
set.add("123");
set.add("qqq");
}
}
在debug中可以看到第一个添加的元素存放在table[2]处,所以他的 before 为 null,而after指向下一个添加的元素,其中LinkedHashMap$Entry 在上文已有介绍,@528为对象的哈希码,它是调试器用来标识特定对象的唯一标识符。
qqq=java.lang.Object@6ed3ef1: 这部分是 Entry 对象的内容。它表示键值对,其中键是 "qqq",值是 java.lang.Object 类型的对象。这里的 "qqq" 是添加到 LinkedHashSet 中的字符串,而值部分 java.lang.Object@6ed3ef1 表示实际存储的值对象,其类型为 java.lang.Object,并且有一个特定的哈希码 @6ed3ef1。