集合概序
1、为什么使用集合框架?
假设,一个班级有30个人,我们需要存储学员的信息,是不是我们可以用一个一维数组就解决了?
那换一个问题,一个网站每天要存储的新闻信息,我们知道新闻是可以实时发布的,我们并不知道需要多大的空间去存储,我要是去设置一个很大的数组,要是没有存满,或者不够用,都会影响我们,前者浪费的空间,后者影响了业务!
如果并不知道程序运行时会需要多少对象,或者需要更复杂的方式存储对象,那我们就可以使用Java的集合框架!
2、集合框架包含的内容
Java集合框架提供了一套性能优良,使用方便的接口和类,他们位于java.util包中。
【重中之重】
- Collection 接口存储一组不唯一,无序的对象
- List 接口存储一组不唯一,有序的对象。
- Set 接口存储一组唯一,无序的对象
- Map 接口存储一组键值对象,提供key到value的映射
- ArrayList实现了长度可变的数组,在内存中分配连续的空间。遍历元素和随机访问元素的效率比较高
- LinkedList采用链表存储方式。插入、删除元素时效率比较高
- HashSet:采用哈希算法实现的Set
HashSet的底层是用HashMap实现的
,因此查询效率较高,由于采用hashCode算法直接确定 元素的内存地址,增删效率也挺高的。
List接口及其集合类
ArrayList
ArrayList概述
ArrayList是可以动态增长和缩减的索引序列,它是基于数组实现的List类。
该类封装了一个动态再分配的Object[]数组,每一个类对象都有一个capacity【容量】属性,表示它们所封装的Object[]数组的长度,当向ArrayList中添加元素时,该属性值会自动增加。如果想 ArrayList中添加大量元素,可使用ensureCapacity方法一次性增加capacity,可以减少增加重分配的次数提高性能。
ArrayList的用法和Vector向类似,但是Vector是一个较老的集合,具有很多缺点,不建议使用。
另外,ArrayList和Vector的区别是:ArrayList是线程不安全的,当多条线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的。
ArrayList的数据结构
分析一个类的时候,数据结构往往是它的灵魂所在,理解底层的数据结构其实就理解了该类的实现思 路,具体的实现细节再具体分析。
ArrayList的数据结构是:
说明:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。我们对 ArrayList类的实例的所有的操作底层都是基于数组的。
【常用方法】
总结
- 1)arrayList可以存放null。
- 2)arrayList本质上就是一个elementData数组。
- 3)arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是gorw()方法。
- 4)arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全是删除集合中的元素。
- 5)arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果
- 6)arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。
LinkedList
插入,删除操作频繁时,可使用LinkedList来提高效率!
LinkedList提供对头部和尾部元素进行添加和删除操作的方法!
LinkedList概述
LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现的。
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
LinkedList 是非同步的。
推荐文章:LinkList详解
【LinkedList的数据结构】
如上图所示,LinkedList底层使用的双向链表结构,有一个头结点和一个尾结点,双向链表意味着我们可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。
总结
- linkedList本质上是一个双向链表,通过一个Node内部类实现的这种链表结构
- 能存储null值
- 跟arrayList相比较,就真正的知道了,LinkedList在删除和增加等操作上性能好,而ArrayList在查询的性能上好
- . 从源码中看,它不存在容量不足的情况
- . linkedList不光能够向前迭代,还能像后迭代,并且在迭代的过程中,可以修改值、添加值、还能移除值。
- linkedList不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。
Vector和Stack
Vector概述
- Vector是一个可变化长度的数组
- Vector增加长度通过的是capacity和capacityIncrement这两个变量\
- Vector也可以获得iterator和listIterator这两个迭代器,并且他们发生的是fail-fast,而不是failsafe,注意这里,不要觉得这个vector是线程安全就搞错了
- Vector是一个线程安全的类,如果使用需要线程安全就使用Vector(本质是源码中加了synchronized),如果不需要,就使用arrayList
- Vector和ArrayList很类似,就少许的不一样,从它继承的类和实现的接口来看,跟arrayList一模一 样。
注意:java1.5推出的java.uitl.concurrent包,为了解决复杂的并发问题的。
所以开发中,不建议用vector,需要线程安全的集合类直接用java.util.concurrent包下的类。
Stack
现在来看看Vector的子类Stack,学过数据结构都知道,这个就是栈的意思。那么该类就是跟栈的用法一 样了。
class Stack<E> extends Vector<E> {}
通过查看他的方法,和查看api文档,很容易就能知道他的特性。就几个操作,出栈,入栈等,构造方法也是空的,用的还是数组,父类中的构造,跟父类一样的扩增方式,并且它的方法也是同步的,所以也是线程安全
。
总结Vector和Stack
Vector总结(通过源码分析)】
- Vector
线程安全
是因为它的方法都加了synchronized
关键字 - Vector的本质是一个数组,特点能是能够自动扩增,扩增的方式跟capacityIncrement的值有关
- 它也会fail-fast,还有一个fail-safe两个的区别在下面的list总结中会讲到。
【Stack的总结】
- 对栈的一些操作,先进后出
- 底层也是用数组实现的,因为继承了Vector
- 也是
线程安全
的
List总结
arrayList和LinkedList区别
arrayList底层是用数组实现的顺序表,是随机存取类型,可自动扩增,并且在初始化时,数组的长度是0,只有在增加元素时,长度才会增加。默认是10,不能无限扩增,有上限,在查询操作的时候性能更好!
LinkedList底层是用链表来实现的,是一个双向链表,注意这里不是双向循环链表,顺序存取类型。 在源码中,似乎没有元素个数的限制。应该能无限增加下去,直到内存满了在进行删除,增加操作时性能更好!
两个都是线程不安全的,在iterator时,会发生fail-fast:快速失效。
arrayList和Vector的区别
arrayList线程不安全,在用iterator,会发生fail-fast
ector线程安全,因为在方法前加了Synchronized关键字。也会发生fail-fast
【为什么现在都不提倡使用vector了】
1)vector实现线程安全的方法是在每个操作方法上加锁,这些锁并不是必须要的,在实际开发中, 一般都是通过锁一系列的操作来实现线程安全,也就是说将需要同步的资源放一起加锁来保证线程安全。
2)如果多个Thread并发执行一个已经加锁的方法,但是在该方法中,又有vector的存在,vector本身实现中已经加锁了,那么相当于锁上又加锁,会造成额外的开销。
3)就如上面第三个问题所说的,vector还有fail-fast的问题,也就是说它也无法保证遍历安全,在遍历时又得额外加锁,又是额外的开销,还不如直接用arrayList,然后再加锁呢。
总结:Vector在你不需要进行线程安全的时候,也会给你加锁,也就导致了额外开销,所以在 jdk1.5之后就被弃用了,现在如果要用到线程安全的集合,都是从java.util.concurrent包下去拿相应的类。
Map接口
HashMap
问题:建立学生学号和学生姓名间的键值映射,并通过key对value进行操作,应该如何实现数据的存储和操作呢?
Map接口专门处理键值映射数据的存储,可以根据键实现对值的操作。 最常用的实现类是HashMap
。
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>();
map.put("004","李清照");
map.put("001","李白");
map.put("003","王羲之");
map.put("002","杜甫");
System.out.println(map.get("003"));
//获取所有key 值
Set<String> keySet = map.keySet();
for (String s : keySet){
String s1 = map.get(s);
System.out.println(s+" "+s1);
}
//获取所有值
Collection<String> values = map.values();
for (String s : values){
System.out.println(s);
}
//entrySet() 获取值
Set<Map.Entry<String, String>> entrySet = map.entrySet();
for (Map.Entry<String, String> m : entrySet){
String key = m.getKey();
String value = m.getValue();
System.out.println(key+","+value);
}
}
HashMap数据结构
HashMap
是基于哈希表的Map接口实现的,它存储的是内容是键值对映射。
哈希表基于map接口的实现,这个实现提供了map所有的操作,并且提供了key和value,可以为 null
,(HashMap和HashTable大致上是一样的,除了hashmap是异步的,和允许key和value为 null),这个类不确定map中元素的位置,特别要提的是,这个类也不确定元素的位置随着时间会不会保持不变。
HashMap在JDK1.8以前的数据结构和存储原理
通过数组和链表结合在一起使用,就叫做链表散列。这其实就是 hashmap存储的原理图。
HashMap的数据结构就是用的链表散列。那HashMap底层是怎么样使用这个数据结构进行数据存取的呢?分成两个部分:
第一步:HashMap内部有一个entry的内部类,其中有四个属性,我们要存储一个值,则需要一个key 和一个value,存到map中就会先将key和value保存在这个Entry类创建的对象中。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //就是我们说的map的key
V value; //value值,这两个都不陌生
Entry<K,V> next;//指向下一个entry对象
int hash;//通过key算过来的你hashcode值。
}
Entry的物理模型图:
第二步:构造好了entry对象,然后将该对象放入数组中,如何存放就是这hashMap的精华所在了。
大概的一个存放过程是:通过entry对象中的hash
值来确定将该对象存放在数组中的哪个位置上,如果在这个位置上还有其他元素,则通过链表来存储这个元素。
JDK1.8后HashMap的数据结构
(数组+链表+红黑树
)
上图很形象的展示了HashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率。
HashMap的属性
HashMap的实例有两个参数影响其性能。
初始容量
:哈希表中桶的数量
加载因子
:哈希表在其容量自动增加之前可以达到多满,的一种尺度
当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行 rehash操作,将哈希表扩充至两倍的桶数。
Java中默认初始容量为16,加载因子为0.75。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
【loadFactor加载因子】
定义:loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为 0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据 (entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,那么数组中存放的数据也就越稀,也就是可能数组中每个位置上就放一个元素。
那有人说,就把loadFactor变为1最好吗,存的数据很多,但是这样会有一个问题,就是我们在通过key拿到我们的value时,是先通过key的hashcode值,找到对应数组中的位置,如果该位置中有很多元素,则需要通过equals来依次比较链表中的元素,拿到我们的value值,这样花费的性能就很高,如果能让数组上的每个位置尽量只有一个元素最好,我们就能直接得到value值了,所以有人又会说,那把loadFactor变得很小不就好了,但是如果变得太小,在数组中的位置就会太稀,也就是分散的太开,浪费很多空间,这样也不好,所以在hashMap 中loadFactor的初始值就是0.75,一般情况下不需要更改它。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
【桶】
根据前面画的HashMap存储的数据结构图,你这样想,数组中每一个位置上都放有一个桶,每个桶里 就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。
【capacity】
capacity译为容量代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数。默认值是 16。
一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂。
【size的含义】
size就是在该HashMap的实例中实际存储的元素的个数
【threshold的作用】
int threshold;//threshold = capacity * loadFactor
threshold = capacity * loadFactor
,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。
注意这里说的是考虑
,因为实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条件。
什么时候会扩增数组的大小?
在put一个元素时先size>=threshold并且还要在对应数组位置上有元素, 这才能扩增数组。
通过一张HashMap的数据结构图来分析:
HashMap的源码分析
1、HashMap的层次关系与继承结构
【HashMap继承结构】
上面就继承了一个abstractMap,也就是用来减轻实现Map接口的编写负担。
【实现接口】
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}
Map<K,V>
:在AbstractMap抽象类中已经实现过的接口,这里又实现,实际上是多余的。但每个集合都有这样的错误,也没过大影响
Cloneable
:能够使用Clone()方法,在HashMap中,实现的是浅层次拷贝,即对拷贝对象的改变会影响 被拷贝的对象。
Serializable
:能够使之序列化,即可以将HashMap对象保存至本地,之后可以恢复状态。
2、HashMap类的属性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集合
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;
}
3、HashMap的构造方法
有四个构造方法,构造方法的作用就是记录一下16这个数给threshold(这个数值最终会当作第一次组的长度)和初始化加载因子。
注意,hashMap中table数组一开始就已经是个没有长度的数组了。
构造方法中,并没有初始化数组的大小,数组在一开始就已经被创建了,构造方法只做两件事情,一个 是初始化加载因子,另一个是用threshold记录下数组初始化的大小。注意是记录。
加粗样式
【HashMap()】
//看上面的注释就已经知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75
//初始化容量:也就是初始化数组的大小
//加载因子:数组上的存放数据疏密程度。
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
【HashMap(int)】
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
【HashMap(int,float)】
public HashMap(int initialCapacity, float loadFactor) {
// 初始容量不能小于0,否则报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始容量不能大于最大值,否则为最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 填充因子不能小于或等于0,不能为非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 初始化填充因子
this.loadFactor = loadFactor;
// 初始化threshold大小
this.threshold = tableSizeFor(initialCapacity);
}
【HashMap(Map<? extends K, ? extends V> m) 】
public HashMap(Map<? extends K, ? extends V> m) {
// 初始化填充因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 将m中的所有元素添加至HashMap中
putMapEntries(m, false);
}
【putMapEntries(Map<? extends K, ? extends V> m, boolean evict)函数将m的所有元素存入本 HashMap实例中】
/**
* Implements Map.putAll and Map constructor.
*
* @param m the map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 判断table是否已经初始化
if (table == null) { // pre-size
// 未初始化,s为m的实际元素个数
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 计算得到的t大于阈值,则初始化阈值
if (t > threshold)
threshold = tableSizeFor(t);
}
// 已初始化,并且m元素个数大于阈值,进行扩容处理
else if (s > threshold)
resize();
// 将m中的所有元素添加至HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
4、HashMap常用方法
【put(K key, V value)】
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
HashMap并没有直接提供putVal接口给用户调用,而是提供的put函数,而put函数就是通过putVal来插入元素的。
【get(Object key)】
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
HashMap并没有直接提供getNode接口给用户调用,而是提供的get函数,而get函数就是通过 getNode来取得元素的。
【总结】
要知道hashMap在JDK1.8以前是一个链表散列这样一个数据结构,而在JDK1.8以后是一个数组加 链表加红黑树的数据结构。
通过学习,hashMap是一个能快速通过key获取到value值得一个集合,原因是内部使用的 是hash查找值得方法。
Set接口
Set注重独一无二
的性质,该体系集合可以知道某物是否已经存在于集合中,不会存储重复的元素,用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。
该集合中没有特有的方法,直接继承自Collection接口:
/**
* Collection
* \--List
* 有序(存储顺序和取出顺序一致),可重复
* \--Set
* 无序(存储顺序和取出顺序不一致),唯一
* HashSet:它不保证set的迭代顺序;特别是它不保证该顺序恒久不变
* 注意:虽然set集合的元素无序,但是,作为集合来说,它肯定有它自己的存储顺序,
* 而你的顺序恰巧和它的存储顺序一致,这代表不了有序,你可以多存储一些数据就能看到效果
**/
案例:set集合添加元素并使用增强for循环
遍历:
public static void method1() {
Set<String> set = new HashSet<>();
set.add("1");
set.add("5");
set.add("2");
set.add("5");//重复的不会添加进去
for (String s : set) {
System.out.println(s);
}
}
//最后输出顺序是: 1、2、5
HashSet
HashSet是一个没有重复元素
的集合,它其实是由HashMap实现
的,HashMap保存的是建值对,然而我们只能向HashSet中添加Key,原因在于HashSet的Value其实都是同一个对象
,这是HashSet添加元素的方法,可以看到辅助实现HashSet的map中的value其实都是Object类的同一个对象。
特点:
- 底层数据结构是HashMap.
- 对集合的迭代顺序不作任何保证,也就是说不保证存储和取出的元素顺序一致
- 没有带索引的方法,所以不能使用普通for循环遍历,需使用
增强for循环
- 由于是Set集合,所以是不包含重复元素的集合
存储规则
HashSet不存入重复元素的规则. 使用hashcode
和equals
由于Set集合是不能存入重复元素的集合。那么HashSet也是具备这一特性的。
HashSet如何检查重复?
答: HashSet会通过元素的hashcode()和equals方法进行判断元素是否重复。
当你试图把对象加入HashSet时,HashSet会使用对象的hashCode来判断对象加入的位置。同时也会与其他已经加入的对象的hashCode进行比较,如果没有相等的hashCode,HashSet就会假设对象没有重复出现。
简单一句话,如果对象的hashCode值是不同的,那么HashSet会认为对象是不可能相等的。
因此我们自定义类的时候需要重写hashCode,来确保对象具有相同的hashCode值。
如果元素(对象)的hashCode值相同,是不是就无法存入HashSet中了?当然不是,会继续使用equals 进行比较。如果 equals为true 那么HashSet认为新加入的对象重复了,所以加入失败。如果equals 为false那么HashSet 认为新加入的对象没有重复,新元素可以存入。
总结:
元素的哈希值是通过元素的hashcode方法来获取的, HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法 如果 equls结果为true ,HashSet就视为同一个元素。如果equals 为false就不是同一个元素。
哈希值相同equals为false的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列!!
LinkedHashSet
特点:
- 哈希表和链表实现的Set接口,具有可预测的迭代次序
- 由链表保证元素有序,也就是说元素的存储和取出顺序是一致的
- 由哈希表保证元素唯一,也就是说没有重复的元素
TreeSet
TreeSet优点
问题:现在有一批数据,要求不能重复存储元素,而且要排序。ArrayList 、 LinkedList不能去除重复数据。HashSet可以去除重复,但是是无序。
所以这时候就要使用TreeSet了!!
TreeSet简介
红-黑树的数据结构,默认对元素进行自然排序
TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。
它继承于AbstractSet抽象类,实现了NavigableSet, Cloneable, java.io.Serializable接口。
TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。
TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。
TreeSet 实现了Cloneable接口,意味着它能被克隆。
TreeSet 实现了java.io.Serializable接口,意味着它支持序列化。
TreeSet是基于TreeMap实现的。
TreeSet中的元素支持2种排序方式
:自然排序
或者 根据创建TreeSet 时提供的 Comparator 进行排序
。这取决于使用的构造方法
。
TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
特点:
-
元素有序,这里的顺序不是指存储和取出的顺序,而是按照一定的规则进行排序,具体排序方式取决于构造方法
-
TreeSet():根据其元素的自然非序进行排序
-
TreeSet(Comparator comparator):根据指定的比较器进行排序
-
-
没有带索引的方法,所以不能使用普通for循环遍历,需要使用foreach增强循环。
-
由于是Set集合,所以不包含重复元素的集合
TreeSet自然顺序
即让元素自身具备比较性,也就是元素需要实现Comparable接口,覆盖compareTo 方法。
。
元素自身具备比较性,需要元素实现Comparable接口,重写compareTo方法,也就是让元素自身具备比较性,这种方式叫做元素的自然排序也叫做默认排序。
即存入TreeSet的类要实现Comparable接口,并重写compareTo()方法,TreeSet对象调用add()方法时,会将存入的对象提升为Comparable类型,然后调用对象中的compareTo()方法进行比较,根据比较的返回值进行存储。
因为TreeSet底层是二叉树,当compareTo方法返回0时,不存储;当compareTo方法返回正数时,存入二叉树的右子树;当compareTo方法返回负数时,存入二叉树的左子树。如果一个类没有实现Comparable接口就将该类对象存入TreeSet集合,会发生类型转换异常。
案例
:创建Student类,有姓名,年龄。存入集合后,先根据年龄大小,再根据姓名来进行排序插入集合中
public class Student implements Comparable<Student> {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student o) {
int i = this.age-o.age;
int n = i==0?this.name.compareTo(o.name):i;
return n;
}
}
重写compareTo()方法,返回值有三种情况
- 返回值为0:不插入集合
- 返回值为1:往后插入集合
- 返回值为-1:往前插入集合
public class Demo {
public static void main(String[] args) {
Student s1 = new Student("xishi",25);
Student s2 = new Student("yangyuhuan",29);
Student s3 = new Student("diaochan",28);
Student s4 = new Student("wangzhaojun",30);
Student s5 = new Student("libai",30);
Student s6 = new Student("libai",30);
TreeSet<Student> ts = new TreeSet<>();
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
ts.add(s6);
for(Student s : ts){
System.out.println(s.name+"---"+s.age);
}
}
}
TreeSet自定义排序
需求:当元素自身不具备比较性,或者元素自身具备的比较性不是所需的。
那么这时只能让容器自身具备。
定义一个类实现Comparator 接口,覆盖compare方法。
并将该接口的子类对象作为参数传递给TreeSet集合的构造函数。
当Comparable比较方式,及Comparator比较方式同时存在时,以Comparator比较方式为主。
public class Demo5 {
public static void main(String[] args) {
TreeSet ts = new TreeSet(new MyComparator());
ts.add(new Book("think in java", 100));
ts.add(new Book("java 核心技术", 75));
ts.add(new Book("现代操作系统", 50));
ts.add(new Book("java就业教程", 35));
ts.add(new Book("think in java", 100));
ts.add(new Book("ccc in java", 100));
System.out.println(ts);
}
}
class MyComparator implements Comparator {
public int compare(Object o1, Object o2) {
Book b1 = (Book) o1;
Book b2 = (Book) o2;
System.out.println(b1+" comparator "+b2);
if (b1.getPrice() > b2.getPrice()) {
return 1;
}
if (b1.getPrice() < b2.getPrice()) {
return -1;
}
return b1.getName().compareTo(b2.getName());
}
}
class Book {
private String name;
private double price;
public Book() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public Book(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Book [name=" + name + ", price=" + price + "]";
}
}
迭代器
所有实现了Collection接口的容器类都有一个iterator方法用以返回一个实现Iterator接口的对象
实现Iterator接口的对象称作为迭代器,用以方便的对容器内元素的遍历操作。
Iterator接口定义了如下方法:
- boolean hashNext();//判断是否有元素没有被遍历
- Object next();//返回游标当前位置的元素并将游标移动到下一个位置
- void remove();//删除游标左边的元素,在执行完next之后该操作只能执行一次
问题:如何遍历Map集合呢?
方法1:通过迭代器Iterator实现遍历
获取Iterator :Collection 接口的iterator()方法
Iterator的方法:
- boolean hasNext(): 判断是否存在另一个可访问的元素
- Object next(): 返回要访问的下一个元素
Set keys = dogMap.keySet(); //取出所有key的集合
Iterator it = keys.iterator(); //获取Iterator对象
while (it.hasNext()) {
String key = (String) it.next(); //取出key
Dog dog = (Dog) dogMap.get(key); //根据key取出对应的值
System.out.println(key + "\t" + dog.getStrain());
}
方法2:增强for循环
for(元素类型t 元素变量x : 数组或集合对象){
引用了x的java语句
}
Collections工具类
Java提供了一个操作Set、List和Map等集合的工具类
:Collections
,该工具类提供了大量方法对集合进 行排序、查询和修改等操作,还提供了将集合对象置为不可变、对集合对象实现同步控制等方法。
注意: Collection是集合接口,而Collections是集合工具类!!
这个类不需要创建对象,内部提供的都是静态方法
。
Collectios概述
此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。
它包含在 collection 上操作的多态算法,即“包装器”,包装器返回由指定 collection 支持的新 collection,以及少数其他内容。如果为 此类的方法所提供的 collection 或类对象为 null,则这些方法都将抛出 NullPointerException 。
2、排序操作
static void reverse(List<?> list)//反转列表中元素的顺序。
static void shuffle(List<?> list) //对List集合元素进行随机排序。
static void sort(List<T> list) //根据元素的自然顺序 对指定列表按升序进行排序
static <T> void sort(List<T> list, Comparator<? super T> c) //根据指定比较器产生的顺序对指定列表进行排序。
static void swap(List<?> list, int i, int j) //在指定List的指定位置i,j处交换元素。
static void rotate(List<?> list, int distance)
//当distance为正数时,将List集合的后distance个元素“整体”移到前面;当distance为负数时,将list集合的前distance个元素“整体”移到后边。该方法不会改变集合的长度。
【演示】
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(3);
list.add(-2);
list.add(9);
list.add(5);
list.add(-1);
list.add(6);
//输出:[3, -2, 9, 5, -1, 6]
System.out.println(list);
//集合元素的次序反转
Collections.reverse(list);
//输出:[6, -1, 5, 9, -2, 3]
System.out.println(list);
//排序:按照升序排序
Collections.sort(list);
//[-2, -1, 3, 5, 6, 9]
System.out.println(list);
//根据下标进行交换
Collections.swap(list, 2, 5);
//输出:[-2, -1, 9, 5, 6, 3]
System.out.println(list);
/*//随机排序
Collections.shuffle(list);
//每次输出的次序不固定
System.out.println(list);*/
//后两个整体移动到前边
Collections.rotate(list, 2);
//输出:[6, 9, -2, -1, 3, 5]
System.out.println(list);
}
【演示】
创建学生集合,加入数据,并自定义排序,先根据年龄,再根据首字母:
public class Student {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
public static void main(String[] args) {
ArrayList<Student> array = new ArrayList<>();
Student s1 = new Student("lingqingxia",20);
Student s2 = new Student("wangxizhi",30);
Student s3 = new Student("libai",25);
Student s4 = new Student("dufu",25);
//Student s5 = new Student("dufu",25);
array.add(s1);
array.add(s2);
array.add(s3);
array.add(s4);
//array.add(s5);
Collections.sort(array, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int i = o1.age-o2.age;
int n = i==0?o1.name.compareTo(o2.name):i;
return n;
}
});
for (Student s:array){
System.out.println(s.name+","+s.age);
}
}
3、查找、替换操作
【方法】
//使用二分搜索法搜索指定列表,以获得指定对象在List集合中的索引。
//注意:此前必须保证List集合中的元素已经处于有序状态。
static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key)
//根据元素的自然顺序,返回给定collection 的最大元素。
static Object max(Collection coll)
//根据指定比较器产生的顺序,返回给定 collection 的最大元素。
static Object max(Collection coll,Comparator comp)
//根据元素的自然顺序,返回给定collection 的最小元素。
static Object min(Collection coll)
//根据指定比较器产生的顺序,返回给定 collection 的最小元素。
static Object min(Collection coll,Comparator comp)
使用指定元素替换指定列表中的所有元素。
static <T> void fill(List<? super T> list, T obj)
//返回指定 collection 中等于指定对象的出现次数。
static int frequency(Collection<?> c, Object o)
//返回指定源列表中第一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1。
static int indexOfSubList(List<?> source, List<?> target)
//返回指定源列表中最后一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1。
static int lastIndexOfSubList(List<?> source, List<?> target)
//使用一个新值替换List对象的所有旧值oldVal
static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
【演示:实例使用查找、替换操作】
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(3);
list.add(-2);
list.add(9);
list.add(5);
list.add(-1);
list.add(6);
//[3, -2, 9, 5, -1, 6]
System.out.println(list);
//输出最大元素9
System.out.println(Collections.max(list));
//输出最小元素:-2
System.out.println(Collections.min(list));
//将list中的-2用1来代替
System.out.println(Collections.replaceAll(list, -2, 1));
//[3, 1, 9, 5, -1, 6]
System.out.println(list);
list.add(9);
//判断9在集合中出现的次数,返回2
System.out.println(Collections.frequency(list, 9));
//对集合进行排序
Collections.sort(list);
//[-1, 1, 3, 5, 6, 9, 9]
System.out.println(list);
//只有排序后的List集合才可用二分法查询,输出2
System.out.println(Collections.binarySearch(list, 3));
}
4、同步控制
Collectons提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从 而解决多线程并发访问集合时的线程安全问题。
正如前面介绍的HashSet,TreeSet,arrayList,LinkedList,HashMap,TreeMap都是线程不安全的。 Collections提供了多个静态方法可以把他们包装成线程同步的集合。
【方法】
//返回指定 collection 支持的同步(线程安全的)collection。
static <T> Collection<T> synchronizedCollection(Collection<T> c)
//返回指定列表支持的同步(线程安全的)列表。
static <T> List<T> synchronizedList(List<T> list)
//返回由指定映射支持的同步(线程安全的)映射。
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
//返回指定 set 支持的同步(线程安全的)set
static <T> Set<T> synchronizedSet(Set<T> s)
【实例】
public static void main(String[] args) {
//下面程序创建了四个同步的集合对象
Collection c = Collections.synchronizedCollection(new ArrayList());
List list = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());
}
5、Collesction设置不可变集合
【方法】
//返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map。
emptyXxx()
//返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。
singletonXxx()
//返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。
unmodifiableXxx():
上面三类方法的参数是原有的集合对象,返回值是该集合的”只读
“版本。
【实例】
public static void main(String[] args) {
//创建一个空的、不可改变的List对象
List<String> unmodifiableList = Collections.emptyList();
//unmodifiableList.add("java");
//添加出现异常:java.lang.UnsupportedOperationException
System.out.println(unmodifiableList);// []
//创建一个只有一个元素,且不可改变的Set对象
Set unmodifiableSet = Collections.singleton("Struts2权威指南");
//[Struts2权威指南]
System.out.println(unmodifiableSet);
//创建一个普通Map对象
Map scores = new HashMap();
scores.put("语文", 80);
scores.put("Java", 82);
//返回普通Map对象对应的不可变版本
Map unmodifiableMap = Collections.unmodifiableMap(scores);
//下面任意一行代码都将引发UnsupportedOperationException异常
unmodifiableList.add("测试元素");
unmodifiableSet.add("测试元素");
unmodifiableMap.put("语文", 90);
}
The End!!创作不易,欢迎点赞/评论!!欢迎关注个人GZH!!