集合
类集概述
对象数组有那些问题?普通的对象数组的最大问题在于数组中的元素个数是固定的,不能动态的扩充大小,所以最 早的时候可以通过链表实现一个动态对象数组。但是这样做毕竟太复杂了,所以在 Java 中为了方便用户操作各个数据结构, 所以引入了类集的概念,有时候就可以把类集称为 java 对数据结构的实现。 在整个类集中的,这个概念是从 JDK 1.2(Java 2)之后才正式引入的,最早也提供了很多的操作类,但是并没有完 整的提出类集的完整概念。 类集中最大的几个操作接口:Collection、Map、Iterator,这三个接口为以后要使用的最重点的接口。 所有的类集操作的接口或类都在 java.util 包中。
数组和链表的优缺点
数组是一种连续存储线性结构,元素类型相同,大小相同 数组的优点: 存取速度快 数组的缺点: 实现必须知道数组的长度 插入删除元素很慢 空间通常是有限的 需要大块连续的内存块 插入删除元素的效率很低 链表表示离散存储线性结构 n个节点离散分配,彼此通过指针相连,每个节点只有一个前驱节点,每个节点只有一个后续节点,首节点没有前驱节点,尾节点没有后续节点 链表的优点: 空间没有限制 插入删除元素很快 链表缺点: 存取速度很慢
链表与二叉树思路
链表和二叉树:基于对象的数据结构
链表
单向链表
单链表: 单链表 [Linked List]:由各个内存结构通过一个 Next 指针链接在一起组成,每一个内存结构都存在后继内存结 构(链尾除外),内存结构由数据域和 Next 指针域组成。 解析: Data 数据 + Next 指针,组成一个单链表的内存结构 ; 第一个内存结构称为 链头,最后一个内存结构称为 链尾; 链尾的 Next 指针设置为 NULL [指向空]; 单链表的遍历方向单一(只能从链头一直遍历到链尾)
双向链表
双链表: 双向链表 [Double Linked List]:由各个内存结构通过指针 Next 和指针 Prev 链接在一起组成,每一个内存结构都存在前驱内存结构和后继内存结构(链头没有前驱, 链尾没有后继),内存结构由数据域、Prev 指针域和Next 指针域组成。 解析:Data 数据 + Next 指针 + Prev 指针,组成一个双向链表的内存结构; 第一个内存结构称为 链头,最后一个内存结构称为 链尾; 链头的 Prev 指针设置为 NULL, 链尾的 Next 指针设置为 NULL; Prev 指向的内存结构称为 前驱, Next 指向的内存结构称为 后继; 双向链表的遍历是双向的,即如果把从链头的 Next 一直到链尾的[NULL] 遍历方向定义为正向,那么从链尾的 Prev 一直到 链头 [NULL ]遍历方向就是反向;
循环链表
循环列表:循环链表没有链头和链尾的说法,因为是闭环的,所以每一个内存结构都可以充当链头和链尾; 单向循环列表: 单向循环链表 [Circular Linked List] : 由各个内存结构通过一个指针 Next 链接在一起 组成,每一个内存结构都存在后继内存结构,内存结构由数据域和 Next 指针域组成。 解析: 单向的实现就是在单链表的基础上,把链尾的 Next 指针直接指向链头,形成一个闭环; 双向循环列表: 双向循环链表 [Double Circular Linked List] : 由各个内存结构通过指针 Next 和指针 Prev 链接在一起组成,每一个内存结构都存在前驱内存结构和后继内存结构,内存结构由 数据域、Prev 指针域和 Next 指针域组成。 解析: 双向的实现就是在双向链表的基础上,把链尾的 Next 指针指向链头,再把链头的 Prev 指针指向链尾,形成一个闭环;
常见数据结构
数据存储的常用结构有:栈、队列、数组、链表和红黑树
栈
栈:stack,又称为堆栈,栈是限定仅在表尾进行插入和删除的线性表。我们吧允许插入和删除的一段称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为先进后出的线性表。 简单地说:采用该结构的集合,对元素的存取有如下的特点 1.先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素) 2.栈的出入口都是栈的顶端位置 有两个名词需要注意: 1.压栈:存元素 2.弹栈:取元素
队列
队列:queue,简称队,队列是一种特殊的线性表,是运算受到限制的一种线性表,只允许在表的一端进行插入,而在另一端进行删除元素的线性表。队尾(rear)是允许插入的一端。队头(front)是允许删除的一端。空队列是不含元素的空表。 简单地说,采用该结构的集合,对元素的存取有如下的特点: 1.先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。就像排队一样 2.队列的入口、出口各站一侧。
数组
数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100都是有固定编号,通过编号就可以快速找到租房子的人。 简单地说,采用该结构的集合,对元素的存取有如下的特点: 1.指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。 2.指定索引位置删除元素:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中。
链表
链表:linked list,由一系列节点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表和双向链表 这里说的是单向列表 简单地说,采用该结构的集合,对元素的存取有如下的特点:
单向链表的特点:
-
多个节点之间,通过地址进行连接。
-
查找元素慢:想查找某个元素,需要通过连接的结点,依次向后查找指定元素
-
增删元素块:
-
增加元素:只需要修改连接下个元素的地址即可。
-
删除元素:只需要修改下个元素的地址即可
-
红黑树
-
二叉树:binary tree ,是每个结点不超过2的有序树(tree) 。 简单的理解,就是一种类似于我们生活中树的结构,只不过每个结点上都最多只能有两个子结点。二叉树是每个节点最多有两个子树的树结构。顶上的叫根结点,两边被称作“左子树”和“右子树”。
-
如图:
红黑树--二叉树的一种,红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉树。也就意味着,树的键值仍然是有序的。 红黑树的约束: 1.节点可以是红色的或者是黑色的 2.根节点是黑色的 3.叶子结点(特指空节点)是黑色的 4.每个红色节点的子节点都是黑色的 5.任何一个节点到其每一个叶子结点的所有路径上黑色节点数相同 红黑树的特点: 速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于两倍。
Collection集合
集合概述
集合:集合是java中提供的一种容器,可以用来存储多个数据。 集合和数据既然都是容器,它们有啥区别呢? 1.数组的长度是固定的,集合的长度是可变的 2.数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储
集合框架
JAVASE提供了满足各种需求的API,在使用这些API之前,先了解其继承与接口操作框架,才能了解何时采用哪个类,以及类之间如何彼此合作,从而达到灵活应用。 集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection和双列集合java.util.Map,今天主要学习Collection集合,在后会补充Map集合 Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是java.util.List和java.util.Set。其中,List的特点是元素有序、元素可重复。Set的特点是元素无序,而且不可重复。List接口的主要实现类有java.util.ArratList和java.util.LinkedList,Set接口的主要实现类有java.util.HashSet和java.util.TreeSet。从上面的描述可以看出JDK中提供了丰富的集合类库,为了便于初学者进行系统的学习,接下来通过一张图来描述整个集合类的继承体系
Collection接口
Collection接口是在整个Java类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。此接口定义在java.util包中。 接口定义如下: public interface Colection<E> extends Iterable<E> 常用方法如下:一共定义了15个方法,那么此接口的全部子类或者子接口就将全部继承以上接口中的方法 但是在开发中不会直接使用Collection接口。而使用其操作的子接口:List、Set
NO. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public boolean add(E e) | 普通 | 向集合中插入一个元素 |
2 | public boolean addAll(Collection<? extends E> c) | 普通 | 向集合中插入一组元素 |
3 | public void clear() | 普通 | 清空集合中的元素 |
4 | public boolean contains(Object o) | 普通 | 查找一个元素是否存在 |
5 | public boolean containsAll(Collection<?> c) | 普通 | 查找一组元素是否存在 |
6 | public boolean isEmpty() | 普通 | 判断集合是否为空 |
7 | public Iterator<E> iterator() | 普通 | 为 Iterator 接口实例化 |
8 | public boolean remove(Object o) | 普通 | 从集合中删除一个对象 |
9 | boolean removeAll(Collection<?> c) | 普通 | 从集合中删除一组对象 |
10 | boolean retainAll(Collection<?> c) | 普通 | 判断是否没有指定的集合 |
11 | public int size() | 普通 | 求出集合中元素的个数 |
12 | public Object[] toArray() | 普通 | 以对象数组的形式返回集合中的全部内容 |
13 | <T> T[] toArray(T[] a) | 普通 | 指定操作的泛型类型,并把内容返回 |
14 | public boolean equals(Object o) | 普通 | 从 Object 类中覆写而来 |
15 | public int hashCode() | 普通 | 从 Object 类中覆写而来 |
List接口
List子接口的定义: public interface List<E> extends Collection<E>
java.util.List 接口继承自 Collection 接口,是单列集合的一个重要分支,习惯性地会将实现了List 接口的对象称为List集合。在List集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。另外,List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。 Lst接口特点: 1.它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33.那么集合中,元素的存储就是按照11、22、33的顺序完成的 2.它是一个带有索引的集合,通过索引就可以精确地操作集合中的元素(与数组的索引是一个道理) 3.集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素 此接口上依然使用了泛型技术。此接口对于Collection接口来讲有如下的扩充方法:
NO. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public void add(int index,E element) | 普通 | 在指定位置处增加元素 |
2 | boolean addAll(int index,Collection<? extends E> c) | 普通 | 在指定位置处增加一组元素 |
3 | public E get(int index) | 普通 | 根据索引位置取出每一个元素 |
4 | public int indexOf(Object o) | 普通 | 根据对象查找指定的位置,找不到返回-1 |
5 | public int lastIndexOf(Object o) | 普通 | 从后面向前查找位置,找不到返回-1 |
6 | public ListIterator<E> listIterator() | 普通 | 返回 ListIterator 接口的实例 |
7 | public ListIterator<E> listIterator(int index) | 普通 | 返回从指定位置的 ListIterator 接口的实例 |
8 | public E remove(int index) | 普通 | 删除指定位置的内容 |
9 | public E set(int index,E element) | 普通 | 修改指定位置的内容 |
10 | List<E> subList(int fromIndex,int toIndex) | 普通 | 返回子集合 |
在List接口中有以上10个方法是对已有的Collection接口进行的扩充。 所以,证明,List接口拥有比Collection接口更多的操作方法。 了解了List接口之后,那么该如何使用该接口呢?需要找到此接口的实现类,常用的实现类有如下几个: ArrayList(95%)、Vector(4%)、LinkedList(1%)
ArrayList集合
java.util.ArrayList 集合数据存储的结构是数组结构。元素增删慢、查找快(数组结构的特点),由于日常开发中使用最多的功能为查询数据、遍历数据,所以 ArrayList 是最常用的集合。 ArrayList是List接口的子类,此类的定义如下 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable 此类继承了 AbstractList 类。AbstractList 是 List 接口的子类。AbstractList 是个抽象类,适配器设计模式。 许多程序员开发时非常随意地使用ArrayList完成任何需求,并不严谨,这种用法是不提倡的。 如果开始时需要存储大量的数据,而且使用ArrayList存储的话,在创建对象时使用一参构造方法,给定初始容量 使用默认无参的构造方法,创建对象,创建出来的数组是一个空数组,长度为0,并不是API中所说的长度为10,但API说的并没有错 注意:add方法虽然是boolean型的,但它只会返回true
Vector
与ArrayList一样,Vector本身也属于List接口的子类,此类的定义如下: public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable 此类与 ArrayList 类一样,都是 AbstractList 的子类。所以,此时的操作只要是List接口的子类就都按照 List 进行操作。 因为Vector操作的时候是以接口为操作的标准,所以操作结果与使用ArrayList本身并没有任何的区别 只是为了照顾习惯于使用Vector的老用户,在JDK1.2之后将Vector类进行了升级,让其多实现了一个List接口,这样才将这个类继续保留了下来
Vector类和ArrayList类的区别
NO. | 区别点 | ArrayList | Vector |
---|---|---|---|
1 | 时间 | 是新的类,在JDK1.2之后推出的 | 是旧的类在JDK1.0的时候就定义了 |
2 | 性能 | 性能较高,是采用了异步处理 | 性能较低,是采用了同步处理 |
3 | 输出 | 支持Iterator、ListIterator输出 | 除了支持Iterator、ListIterator输出还支持Enumeration输出 |
4 | 安全性 | 线程不安全 | 线程安全 |
链表操作LinkedList
java.util.LinkedList 集合数据存储的结构是链表结构。方便元素添加、删除的集合。 LinkedList是一个双向链表 此类的使用几率是非常低的,但是此类的定义如下: public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable 实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。这些方法我们作为了解即可: public void addFirst(E e) :将指定元素插入此列表的开头。 public void addLast(E e) :将指定元素添加到此列表的结尾。 public E getFirst() :返回此列表的第一个元素。 public E getLast() :返回此列表的最后一个元素。 public E removeFirst() :移除并返回此列表的第一个元素。 public E removeLast() :移除并返回此列表的最后一个元素。 public E pop() :从此列表所表示的堆栈处弹出一个元素。 public void push(E e) :将元素推入此列表所表示的堆栈。 public boolean isEmpty() :如果列表不包含元素,则返回true。 LinkedList是List的子类,List中的方法LinkedList都是可以使用,这里就不做详细介绍,我们只需要了解LinkedList的特有方法即可。在开发时,LinkedList集合也可以作为堆栈,队列的结构使用。(了解即可) 注意:在使用LinkedList创建对象 需要遍历时 要先把链表的大小取出来 不能再for循环里面取 否则每循环一次就移除一个元素,并不能循环链表大小次
LinkedList删除重复元素
LinkedList<String> list = new LinkedList<>(); 添加到list里的操作 HashSet<String> set = new HashSet<>(); ListIterator<String> iterator = list.listIterator(); //让list里的数 一个个跟新的set集合比较 while (iterator.hasNext()){ String next = iterator.next(); if (!set.contains(next)){ set.add(next); }else { iterator.remove(); } } for (String s : list) { System.out.println(s); }
Iterator
在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator 。 Iterator 接口也是Java集合中的一员,但它与 Collection 、 Map 接口有所不同, Collection 接口与 Map 接口主要用于存储元素,而 Iterator 主要用于迭代访问(即遍历)Collection 中的元素,因此 Iterator 对象也被称为迭代器。 此接口的定义如下: public interface Iterator<E> 要想使用此接口,则必须使用 Collection 接口,在 Collection 接口中规定了一个 iterator()方法,可以用于为 Iterator 接口进行实例化操作。 通过Collection接口为其进行实例化之后,一定要记住,Iterator中的操作指针是在第一条元素之上,当调用next()方法的时候,获取当前指针指向的值并向下移动,使用hasNext()可以检查序列中是否还有元素 但是在讲解输出的时候一定要记住以下的原则:“只要是碰到了集合,则输出的时候想都不想就使用 Iterator 进行输出。” 此接口规定了以下三个方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | boolean hasNext() | 普通 (常用) | 是否有下一个元素 |
2 | E next() | 普通 (常用) | 取出内容 |
3 | void remove() | 普通 (集合中很少有删除元素的操作) | 删除当前内容 |
迭代的概念
迭代的概念: 迭代:即Collection集合元素的通用获取方式,在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代
Iterator遍历集合
想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作,下面介绍一下获取迭代器的方法: public Iterator iterator() : 获取集合对应的迭代器,用来遍历集合中的元素的。 Collection<String> all = new ArrayList<String>(); all.add("A"); ..... Iterator<String> iter = all.iterator(); while (iter.hasNext()) {// 判断是否有下一个元素 String str = iter.next(); // 取出当前元素 System.out.print(str + "、"); } 以上的操作是 Iterator 接口使用最多的形式,也是一个标准的输出形式。 but!在进行迭代输出的时候如果要想删除当前元素,则只能使用 Iterator 接口中的 remove()方法,而不能使用集合中的remove()方法。否则将出现未知的错误。 在使用Iterator 接口中的 remove()方法时也要注意:不能直接用remove(),要先获取数据在调用remove(),因为不进行获取的话Iterator的指针指的是第一个元素的上面,是空的,会出现异常 while (iter.hasNext()) {// 判断是否有下一个元素 String str = iter.next(); // 取出当前元素 if (str.equals("C")) { all.remove(str); // 错误的,调用了集合中的删除 } else { System.out.print(str + "、"); } } 会出现错误,因为原本需要输出的集合的内容被删掉了 while (iter.hasNext()) {// 判断是否有下一个元素 String str = iter.next(); // 取出当前元素 if (str.equals("C")) { iter.remove(); // 正确的做法 } else { System.out.print(str + "、"); } } 但是,从实际的开发角度看,元素的删除操作出现的几率是很小的,基本上可以忽略,即:集合中很少有删除元素 的操作。 Iterator 接口本身可以完成输出的功能,但是此接口只能进行由前向后的单向输出。如果要想进行双向输出,则必须使用其子接口 —— ListIterator。
迭代器的实现原理
我们在之前案例已经完成了Iterator遍历集合的整个过程。当遍历集合时,首先通过调用t集合的iterator()方法获得迭代器对象,然后使用hashNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。
ListIterator
ListIterator 是可以进行双向输出的迭代接口,此接口定义如下: public interface ListIterator<E> extends Iterator<E> 但是如果要想使用 ListIterator 接口,则必须依靠 List 接口进行实例化。 此接口是 Iterator 的子接口,此接口中定义了以下的操作方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | void add(E e) | 普通 | 增加元素 |
2 | boolean hasPrevious() | 普通 | 判断是否有前一个元素 |
3 | E previous() | 普通 | 取出前一个元素 |
4 | void set(E e) | 普通 | 修改元素的内容 |
5 | int previousIndex() | 普通 | 前一个索引位置 |
6 | int nextIndex() | 普通 | 下一个索引位置 |
ListIterator的使用
List<String> all = new ArrayList<String>(); all.add("A"); ..... ListIterator<String> iter = all.listIterator(); System.out.print("从前向后输出:"); while (iter.hasNext()) { System.out.print(iter.next() + "、"); } System.out.print("\n从后向前输出:"); while (iter.hasPrevious()) { System.out.print(iter.previous() + "、"); } 在使用ListIterator接口中的add()方法时需注意:新添加的元素是从上方添加进去的 但是,此处有一点需要注意的是,如果要想进行由后向前的输出,则首先必须先进行由前向后的输出。 但是,此接口一般使用较少。
Set接口
Set 接口也是 Collection 的子接口,与 List 接口最大的不同在于,Set 接口里面的内容是不允许重复的。 Set 接口并没有对 Collection 接口进行扩充,基本上还是与 Collection 接口保持一致。因为此接口没有 List 接口中定义 的 get(int index)方法,所以无法使用循环进行输出。 那么在此接口中有两个常用的子类:HashSet、TreeSet
HashSet(散列存放)
java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持 HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于: hashCode 与 equals 方法。 既然Set接口并没有扩充任何的Collection接口中的内容,所以使用的方法全部都是Collection接口定义而来的。 HashSet属于散列的存放类集,里面的内容是无序存放的。使用HashSet实例化的Set接口实例,本身属于无序的存放 给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一
think:能不能通过循环的方式将Set接口中的内容输出呢?
是可以实现的,因为在Collection接口中定义了将集合变成对象数组进行输出 Set<String> all = new HashSet<String>(); // 实例化Set接口对象 all.add("A"); ...... Object obj[] = all.toArray(); // 将集合变为对象数组 for (int x = 0; x < obj.length; x++) { System.out.print(obj[x] + "、"); } but!以上的操作不好,因为在操作的时候已经制定了操作的泛型类型,那么现在最好的做法是由泛型所指定的类型变为指定的数组 所以只能使用以下的方法:<T> T[] toArray(T[] a) String[] str = all.toArray(new String[] {});// 变为指定的泛型类型数组 再进行遍历输出
不可重复例子
//添加元素 set.add(new String("123")); set.add("123"); set.add("123"); set.add("321"); //遍历 for (String name : set) { System.out.println(name); } 输出结果为: 123 321
HashSet集合存储数据的结构
什么是哈希表? 在JDK1.8之前,哈希表底层采用数组+链表实现,及时用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值一次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换成红黑树,这样大大减少了查找时间,当链表长度减少到6时从红黑树又转换为链表 简单的来说,哈希表是有数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图
存储流程图
总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须覆写hashCode和equals方法建立属于当前对象的比较方法
LinkedHashSet
我们知道HashSet保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢?在HashSet下面有一个子类 java.util.LinkedHashSet ,它是链表和哈希表组合的一个数据存储结构
排序的子类:TreeSet
与HashSet不同的是,TreeSet本身属于排序的子类,此类的定义如下: public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable 如果TreeSet集合里面是相同类型的基本数据类型,直接输出就可以排序 但是集合里时对象的话,需要该对象的类实现Comparable接口实现compareTo方法后才能进行排序
排序的说明
要想对一个自定义类的对象进行排序,该类要先实现Comparable接口 如果只比较对象的一个属性的话,比较的属性相同即使其他属性不相同也会被认为是同一个对象 所以在重写compareTo方法时,加多一个属性的比较
例子
定义了一个Person类实现了Comparable接口 public int compareTo(Person per) { if (this.age > per.age) { return 1; } else if (this.age < per.age) { return -1; } else { return 0; } } public static void main(String[] args) { Set<Person> all = new TreeSet<Person>(); all.add(new Person("张三", 10)); all.add(new Person("李四", 10)); all.add(new Person("李四", 10)); all.add(new Person("王五", 11)); all.add(new Person("赵六", 12)); all.add(new Person("孙七", 13)); System.out.println(all); } 执行结果:[姓名:张三,年龄:10, 姓名:王五,年龄:11, 姓名:赵六,年龄:12, 姓名:孙七,年龄:13] 从以上的结果中可以发现,李四没有了。因为李四的年龄和张三的年龄是一样的,所以会被认为是同一个对象。则 此时必须修改 Person 类,如果假设年龄相等的话,按字符串进行排序。 public int compareTo(Person per) { if (this.age > per.age) { return 1; } else if (this.age < per.age) { return -1; } else { return this.name.compareTo(per.name); } }
关于重复元素的说明
之前在使用Comparable完成的对于重复元素的判断,那么Set接口定义的时候本身就是不允许重复元素的,那么证明如果现在整的是有重复元素的话,使用HashSet也同样可以进行区分 public static void main(String[] args) { Set<Person> all = new HashSet<Person>(); all.add(new Person("张三", 10)); all.add(new Person("李四", 10)); System.out.println(all); } 此时发现,并没有去掉所谓的重复元素,也就是说之前的操作并不是真正的重复元素的判断,而是通过Comparable接口间接完成的。 如果要想判断两个对象是否相等,则必须使用Object类中的equals()方法完成。 从最正规的来讲,如果想要判断两个对象是否相等,需要通过equals()完成。 第一种判断两个对象的编码是否一致,这个方法需要通过hashCode()完成,即:每个对象有唯一的编码 还需要进一步验证对象中的每个属性是否相等,需要通过equals()完成。 所有此时需要覆写Object类中的hashCode()方法,此方法表示一个唯一的编码,一般是通过公式计算出来的
重写方法
public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof Person)) { return false; } Person per = (Person) obj; if (per.name.equals(this.name) && per.age == this.age) { return true; } else { return false; } } public int hashCode() { return this.name.hashCode() * this.age; } 发现,此时已经不存在重复元素了,所以如果想要去掉重复元素需要一开hashCode()和equals()方法共同完成
Collections
常用功能
java.utils.Collections 是集合工具类,用来对集合进行操作。部分方法如下: public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。 public static void shuffle(List<?> list) 打乱顺序 :打乱集合顺序。 public static <T> void sort(List<T> list) :将集合中元素按照默认规则排序。 public static <T> void sort(List<T> list,Comparator<? super T> ) :将集合中元素按照指定规则排序。
Comparator比较器
public static <T> void sort(List<T> list) :将集合中元素按照默认规则排序。 说到排序,简单地说即使两个对象之间比较大小,那么在JAVA中提供了两种比较现实的方法,一种是比较死板的采用java.lang.Comparable接口去实现,一种是灵活的当我需要做排序的时候再去选择的java.util.Comparator接口完成 那么我们采用 public static <T> void sort(List<T> list)这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型上如下: public final class String implements java.io.Serializable, Comparable<String>, CharSequence { String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的,那么这个时候我们可以使用 public static <T> void sort(List<T> list,Comparator<? super T> ) 方法灵活的完成,这个里面就涉及到了Comparator这个接口,排序是comparator能实现的功能之一,改接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是 public int compare(String o1, String o2) :比较其两个参数的顺序。 两个对象比较的结果有三种:大于,等于,小于。 如果要按照升序排序, 则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数) 如果要按照降序排序 则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)
Comparator比较器操作
public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("cba"); list.add("aba"); list.add("sba"); list.add("nba"); //排序方法 按照第一个单词的降序 Collections.sort(list, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o2.charAt(0) - o1.charAt(0); } }); System.out.println(list); } 结果如下:[sba, nba, cba, aba] 排序: 01 - 02 升序 02 - 01 降序
简述Comparable和Comparator两个接口的区别。
Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的comparaTo方法被称为它的自然比较方法。只能在类中实现comparaTo()一次,不能经常修改类的代码实现自己想要的排序,实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无序指定比较器 Comparator:强行对某个对象进行整体排序,可以将Comparator传递给sort方法(如Collections.sort或Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序
Map接口
以上的Collection中,每次操作的都是一个对象,如果现在假设要操作一对对象,则必须使用Map了,类似于以下一种情况: 张三 123456 李四 234567 那么保存以上的信息的时候使用Collection就不那么方便,所以要使用Map接口。里面的所有内容都按照key->value的键值对形式保存,也称为二元偶对象 Map本身是一个接口,所以一般会使用以下的几个子类:HashMap、TreeMap、Hashtable 此接口的定义如下: public interface Map<K,V> 此接口与Collection接口没有任何关系,是第二大的集合操作接口。此接口常用方法如下
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | void clear() | 普通 | 清空 Map 集合中的内容 |
2 | boolean containsKey(Object key) | 普通 | 判断集合中是否存在指定的 key |
3 | boolean containsValue(Object value) | 普通 | 判断集合中是否存在指定的 value |
4 | Set<Map.Entry<K,V>> entrySet() | 普通 | 将 Map 接口变为 Set 集合 |
5 | V get(Object key) | 普通 | 根据 key 找到其对应的 value |
6 | boolean isEmpty() | 普通 | 判断是否为空 |
7 | Set<K> keySet() | 普通 | 将全部的 key 变为 Set 集合 |
8 | Collection<V> values() | 普通 | 将全部的value变为Collection集合 |
9 | V put(K key,V value) | 普通 | 向集合中增加内容 |
10 | void putAll(Map<? extends K,? extends V> m) | 普通 | 增加一组集合 |
11 | V remove(Object key) | 普通 | 根据key删除内容 |
新的子类:HashMap
HashMap是Map的子类,HashMap本身是属于无序存放的,此类的定义如下: public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable 此类继承了AbstractMap类,同样可以被克隆,可以被序列化下来
HashMap中得到所有的key 和 value
Map<Integer, String> map = new HashMap<Integer, String>(); //新建一个HashMap集合 map.put(); ......... Set<Integer> set = map.keySet(); // 得到全部的key keySet()| 将全部的key变为Set集合 Collection<String> value = map.values(); // 得到全部的value map.values() 将全部的value变为Collection集合 Iterator<Integer> iter1 = set.iterator(); Iterator<String> iter2 = value.iterator(); while (iter1.hasNext()) { System.out.print(iter1.next() + "、"); // 输出的是Set集合中存储的map集合的key } while (iter2.hasNext()) { System.out.print(iter2.next() + "、"); // 输出的是Collection集合中存储的map集合的value }
循环输出Map中所有的内容
Set<String> set = map.keySet(); // 得到全部的key key Iterator<String> iter = set.iterator(); while (iter.hasNext()) { String i = iter.next(); // 得到key System.out.println(i + " --:> " + map.get(i)); // get() 根据key得到value }
旧的子类:Hashtable
Hashtable是一个最早的key->value的操作类,本身是在JDK1.0的时候推出的。其基本操作与HashMap是类似的 操作的时候可以发现和HashMap基本上没有什么区别,而且本身都是以Map为操作标准的,所以操作的结果形式都一样。但是Hashtable中是不能向集合中插入null值的
HashMap与Hashtable的区别
在整个集合中出了ArrayList和Vector的区别之外,另一个最重要的区别就是HashMap与Hashtable的区别
No. | 区别点 | HashMap | Hashtable |
---|---|---|---|
1 | 推出时间 | JDK1.2之后推出的,新的操作类 | JDK1.0时推出的,旧的操作类 |
2 | 性能 | 异步处理,性能较高 | 同步处理,性能较低 |
3 | null | 允许设置为null | 不允许设置为null |
TreeMap
TreeMap 子类是允许 key 进行排序的操作子类,其本身在操作的时候将按照 key 进行排序,另外,key 中的内容可以 为任意的对象,但是要求对象所在的类必须实现 Comparable 接口。 TreeMap在输出基本数据类型的时候会先经过排序,且若有相同的key,输出的value只会有一个而且是靠后的一个 但是,但是从一般的开发角度来看,在使用 Map 接口的时候并不关心其是否排序,所以此类 只需要知道其特点即可。
关于Map集合的输出
在Collection接口中,可以使用iterator()方法为Iterator接口实例化,并进行输出操作,但是在Map接口中并没有此方法的定义,所以Map接口本身是不能直接使用Iterator进行输出的。 因为Map接口中存放的每一个内容都是一对值,而使用Iterator接口输出的时候,每次取出的都实际上是一个完整的对象,。如果此时非要使用Iterator进行输出的话,则可以按照如下的步骤进行: 1.使用Map接口中的entrySet()方法将Map接口的全部内容变成Set集合 2.可以使用Set接口中定义的iterator()方法为Iterator接口进行实例化 3.之后使用Iterator接口进行迭代输出,每一次的迭代都可以取得一个Map.Entry的实例 4.通过Map.Entry进行key和value的分离
Map.Entry
那么,到底什么是Map.Entry呢 Map.Entry本身是一个接口。此接口是定义在Map接口内部的,是Map的内部接口。此内部接口使用static进行定义,所以此接口将成为外部接口。 实际上来讲,对于每一个存放到Map集合中的key和value都是将其变为了Map.Entry保存在了Map集合之中
在Map.Entry接口中以下的方法最为常用:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | K getKey() | 普通 | 得到key |
2 | V getValue() | 普通 | 得到value |
使用Iterator输出Map接口
Map<String, String> map = new HashMap<String, String>(); map.put("ZS", "张三"); map.put("LS", "李四"); map.put("WW", "王五"); map.put("ZL", "赵六"); map.put("SQ", "孙七"); Set<Map.Entry<String, String>> set = map.entrySet();// 变为Set实例 Iterator<Map.Entry<String, String>> iter = set.iterator(); while (iter.hasNext()) { Map.Entry<String, String> me = iter.next(); System.out.println(me.getKey() + " --> " + me.getValue()); } 以上的代码一定要记住,Map集合中每一个元素都是Map.Entry的实例,只有通过Map.Entry才能进行key和value的分离操作 除了以上的做法之外,在JDK1.5之后也可以使用foreach完成同样的输出,只是这样的操作基本上不使用
使用foreach输出Map接口
Map<String, String> map = new HashMap<String, String>(); map.put("ZS", "张三"); map.put("LS", "李四"); map.put("WW", "王五"); map.put("ZL", "赵六"); map.put("SQ", "孙七"); for (Map.Entry<String, String> me : map.entrySet()) { System.out.println(me.getKey() + " --> " + me.getValue()); }
两种关系
使用类集,除了可以清楚地表输出动态数组的概念及各个数据结构的操作之外,也可以表示出以下的两种关系
第一种:一对多关系
一个学校有多个学生,典型的一对多 定义学生类: public class Student{ private String name; private int age; private School school; } 定义学校类: public class School{ private String schoolName; private List<Student> allStudents = null; } main方法建立两者的关系 Student stu1 = new Student("张三", 10); Student stu2 = new Student("李四", 11); School sch = new School("LAMP JAVA"); sch.getAllStudents().add(stu1); // 一个学校有多个学生 stu1.setSchool(sch);// 一个学生属于一个学校 sch.getAllStudents().add(stu2); // 一个学校有多个学生 stu2.setSchool(sch);// 一个学生属于一个学校 System.out.println(sch); Iterator<Student> iter = sch.getAllStudents().iterator(); while (iter.hasNext()) { System.out.println(iter.next()); } System.out.println(stu1.getSchool()); 此时就完成了一对多的关系。
第二种关系:多对多
一个学生可以选择多门课程,一门课程允许有多个学生参加 定义学生类 public class Student { private String name; private int age; private List<Course> allCourses; } 定义课程类 public class Course { private String name; private int credit; private List<Student> allStudents = null; } main方法建立两者关系,注意这时候设置的关系也应该是双向操作的 Student stu1 = new Student("张三", 10); Student stu2 = new Student("李四", 11); Course c1 = new Course("Oracle", 5); Course c2 = new Course("Java SE基础课程", 10); c1.getAllStudents().add(stu1); // 参加第一门课程 c1.getAllStudents().add(stu2); // 参加第一门课程 stu1.getAllCourses().add(c1); // 学生选择课程 stu2.getAllCourses().add(c1); // 学生选择课程 c2.getAllStudents().add(stu1); // 参加第二门课程 c2.getAllStudents().add(stu2); // 参加第二门课程 stu1.getAllCourses().add(c2); // 学生选择课程 stu2.getAllCourses().add(c2); // 学生选择课程 使用Iterator输出
Collections类
Collections实际上是一个集合的操作类,此类的定义如下: public class Collections extends Object 这个类与Collection接口没有任何的关系。是一个单独存在的类。 此类是一个集合的操作类,但是从实际考虑,使用此类操作并不是很方便,最好的做法就是使用各个接口的直接操作的方法完成
分析equals、hashCode与内存泄漏
equals
equals的作用:比较两个对象的地址值是否相等 但是我们必须清楚,当Sting、Math还有Integer、Double.....等等这些封装类在使用equals()方法时,已经覆盖了object类的equals()方法,不再是地址的比较而是内容的比较。 还应注意:Java语言对equals()的要求如下,这些要求是必须遵循的: 1. 对称性:如果 x.equals(y)返回是“true”,那么 y.equals(x)也应该返回是“true”。 2. 反射性:x.equals(x)必须返回是“true”。 3. 类推性:如果 x.equals(y)返回是“true”,而且 y.equals(z)返回是“true”,那么 z.equals(x)也应该返回是“true”。 4. 还有一致性:如果 x.equals(y)返回是“true”,只要 x 和 y 内容一直不变,不管你重复 x.equals(y)多少次,返回都是 “true”。 5. 任何情况下,x.equals(null),永远返回是“false”;x.equals(和 x 不同类型的对象)永远返回是“false”。 以上这五点是重写 equals()方法时,必须遵守的准则,如果违反会出现意想不到的结果,请大家一定要遵守。
hashCode
定义如下: public native int hashCode(); 说明它是一个本地方法,它的实现是根据本地机器相关的。当然我们可以在自己写的类中覆盖hashCode方法,比如String、Integer、Double...等等这些类都是覆盖了hashCode方法的
java.lang.Object中对hashCode的约定
1.在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode方法多次,让它必须始终如一地返回同一个整数 2.如果两个对象根据equals(Object o)方法是相等的,则调用这两个对象中任一对象的hashCode方法必须产生相同的整数结果 3.如果两个对象根据equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的hashCode方法,不要求产生不同的整数结果。但如果能不同,则可能提高散列表的性能
在java集合中,判断两个对象是否相等的规则
1.判断两个对象的hashCode是否相等 如果不等,认为两个对象也不等,完毕 如果相等进行2 (这一点只是为了提高存储效率而要求的,其实理论上没有也可以,但如果没有,实际使用时效率会大大降低,所以我们 这里将其做为必需的。后面会重点讲到这个问题。) 2.判断两个对象用equals运算是否相等 如果不相等,认为两个对象也不相等 如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键)
提示贴:
当一个对象被存进HashSet集合后,就不能修改这个对象中的那些参与计算的哈希值的字段了,否则,对象被修改后的哈希值与最初存储进HashSet集合中时的哈希值就不相同了,在这种情况下,及时在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除该对象,从而造成内存泄漏
总结
1.类集就是一个动态的对象数组,可以向集合中加入任意多的内容。 2.List接口中是允许有重复元素的,Set接口中是不允许有重复元素的 3.所有的重复元素依靠hashCode()和equals进行区分 4.List接口的常用子类:ArrayList、Vector 5.Set接口的常用子类:HashSet、TreeSet 6.TreeSet是可以排序的,一个类的对象依靠Comparable接口排序 7.Map接口中允许存放一对内容,key->value 8.Map接口的子类:HashMap、Hashtable、TreeMap 9.Map使用Iterator输出的详细步骤