一个一个存
List
有序集合,能够存重复的数据
Set
无序集合,不允许放重复的数据
Iterable
接口:可迭代的
里面有iterator()
方法,返回一个Iterator
迭代器
Iterator
接口:迭代器
里面有hasNext()
、next()
、remove()
等方法。
hasNext()
:判断集合中是否有下一个元素,有的话就返回true,否则返回false
next()
:返回集合中的下一个元素
remove()
:删除集合中最后一个元素
Collection
接口继承自Iterable
接口:集合
所有的集合都要实现Iterable
接口需要实现它里面的iterator()
方法然后得到迭代器,所以所有的集合都是可迭代的。
List
和Set
接口也都继承自Collection
接口
LIst:有序可以重复
Set:无序不可重复
ArrayList
实现了List
接口
ArrayList顾名思义底层采用的是数组来存储元素的,所以ArrayList集合适合查询,不适合频繁增删元素。因为数组存的都是一组相同数据类型的集合,在内存里是连续存储的,只是通过数组名的地址来指引,数组中的元素是连续内存地址排列的,如果按下标增加或者删除的话数组的地址就会发生偏移,但是查询就不会,查询因为内存地址连续,所以查询的效率就会提高。
LinkedList
实现了List
接口
底层采用双向链表存储数据。适合增删元素,不适合查询。
Vector
实现了List
接口
底层和ArrayList集合相同,但是Vector在多线程是安全的,效率较低,现在很少使用。
HashSet
实现了Set
接口
底层是哈希表(又叫散列表)
SortedSet
继承了set
接口
存进去的数据是无序且不可重复的,但是存储进去之后会自动按照元素大小排序
TreeSet
实现了SortedSet
接口
Collection
接口继承Iterable
(可迭代的)接口,实现里面的iterator()
方法,这个方法的返回Itertor
接口,利用返回的Iterator
接口里的hasnext()
判断是否有下一个元素,next()
返回下一个元素,remove()
删除其中的元素。List
和Set
都继承Collection
接口,都能够实现上述方法。ArrayList
、LinkedList
、Vector
实现了List
接口HashSet
实现了Set
接口,SortedSet
继承了Set
接口,而TreeSet
实现了SortedSet
接口一对一对存
Map
无序集合,集合中是用键值对(Key—Value)来存储的,键是无序不可重复的,在Map里起决定性作用的是键key,值只是key存的值而已。
Map
是顶层接口
Map里的数据都是key—value键值对来存储的
HashMap
实现了Map
接口
HashMap里的key等同于set集合,无序且不能重复
Hashtable
实现了Map
接口
线程是安全的,但是效率很低
Properties
继承了Hashtable
接口
属性类,也是以key—value的方式存的,只是键和值都是字符串
SortedMap
继承了Map
接口
存入的key是无序且不可重复的,但是存储的key可以自动排序,key等同于SortedSet
TreeMap
实现了SortedMap
里面的key就是一个TreeSet
List
有序且可重复
有序:存入和取出的顺序一样,有下标
可重复:可以存储相同的数据
ArrayList
底层是数组;适合查询元素
LinkedList
底层是双向链表;适合增删元素
Vector
底层是数组;线程安全,但是效率不高;不常用
Set
无序且不可以重复
无序:存入和取出的顺序不一样,没有下标
不可重复:不可以存储相同的数据
HashSet
底层是HashMap;
TreeSet
实现了SortedSet;底层是TreeMap;能够排序
Map
key—value(键值对);键是无序且不可重复的
无序:存入和取出的顺序不一样
不可重复:不可以存储相同的数据
HashMap
底层是哈希表;
Hashtable
底层是哈希表;线程安全,效率不高;不常用
Properties
线程安全;键和值都是String类型
TreeMap
底层是二叉树;key能够自动排序
Collection接口继承自Iteratable接口,它的常用方法有以下这些:
方法 | 描述 |
---|---|
boolean add(E e) | 向集合末尾添加元素(object类型) |
void clear() | 移除这个集合中的所有元素(清空) |
boolean contains(Object o) | 如果集合中包含o元素,返回true |
boolean isEmpty() | 如果集合中不包含任何元素,返回true |
Iterator iterator() | 返回该集合的元素的迭代器 |
boolean remove(Object o) | 从集合中移除元素o(前提o元素存在) |
int size() | 返回此集合中元素的个数 |
object[] toArray() | 将该集合转换成Object数组 |
List集合接口和Set集合接口都需要实现上述方法,需要重点掌握contains()
、remove()
、iterator()
方法,需要注意的是集合中不能添加基本数据类型,而是添加的数据类型的引用。
iterator()方法
Collection集合对象调用该方法返回该集合对象的迭代器(所谓迭代器就是供迭代的东西,有点像游标或者指针,是Collection集合独有的,Map集合没有),返回的迭代器对象有三个方法,分别是:是否有下一个元素boolean hasNext()
,返回下一个元素Object next()
,移除下一个元素void remove()
。
public class TestCollection {
public static void main(String[] args) {
//创建集合对象,因为Collection是一个接口,创建对象必须要类才行
Collection c=new ArrayList();
//向集合中添加元素
c.add(100);
//尝试存入重复的100
c.add(100);
c.add(new String("test"));
c.add(3.14);
c.add(new Student("测试"));
c.add(5644);
c.add(56456);
//调用iterator方法获得c集合对象的迭代器iterator
Iterator iterator = c.iterator();
//用hasNext()方法判断集合里是否有元素,有就返回true
while (iterator.hasNext()){
//next()就返回这个元素
System.out.println(iterator.next());
}
}
}
可以看到ArrayList输出的是有序且可重复的
在需要用System.out.println()
打印的地方里,需要重写toString()
方法,如果不重写toString()
方法,那么打印的时候就会调用Object类的toString()方法:输出对象的地址(所有的类都是继承自Object类),其中List和Set利用迭代器输出的时候方式有所不同,这也跟他们的特性有关,List是有序且可重复的,Set是无序且不可重复的。
public class TestCollection {
public static void main(String[] args) {
//创建集合对象,因为Collection是一个接口,创建对象必须要类才行
Collection c=new HashSet();
//向集合中添加元素
c.add(100);
//尝试存入重复的100
c.add(100);
c.add(new String("test"));
c.add(3.14);
c.add(new Student("测试"));
c.add(5644);
c.add(56456);
//调用iterator方法获得c集合对象的迭代器iterator
Iterator iterator = c.iterator();
//用hasNext()方法判断集合里是否有元素,有就返回true
while (iterator.hasNext()){
//next()就返回这个元素
System.out.println(iterator.next());
}
}
}
从输出的结果当中可以看到,HashSet里存入的顺序和取出的顺序明显不同,并且没有重复
这是较底层的遍历方式,也可以通过forEach直接遍历。
**集合结构发生改变,必须重新获取迭代器。**如果在使用迭代器遍历的时候如果采用集合的remove()方法删除元素的时候这个时候集合结构发生变化了,必须再次获取迭代器,或者使用迭代器的remove()方法,因为Iterator迭代器更像是集合在那一个状态的快照,不会随着集合的状态发生改变而刷新。
每次集合移除元素
public class TestRemove {
public static void main(String[] args) {
//创建集合对象c
Collection c=new ArrayList();
//向c集合中添加元素
c.add(new String("test"));
c.add(100);
c.add(200);
//获得c集合的迭代器i
Iterator i = c.iterator();
//使用迭代器i进行遍历
while (i.hasNext()){
Object o = i.next();
//利用集合c直接移除元素,此时集合的结构发生改变
c.remove(o);
//因为集合的结构发生改变,所以必须重新获取迭代器,否则将会出现java.util.ConcurrentModificationException异常
i = c.iterator();
}
//获取集合的长度
System.out.println("集合长度="+c.size());
}
}
或者使用迭代器的remove方法来移除元素。
public class TestRemove {
public static void main(String[] args) {
//创建集合对象c
Collection c=new ArrayList();
//向c集合中添加元素
c.add(new String("test"));
c.add(100);
c.add(200);
//获得c集合的迭代器i
Iterator i = c.iterator();
//使用迭代器i进行遍历
while (i.hasNext()){
Object o = i.next();
//利用迭代器i移除元素
i.remove();
}
//获取集合的长度
System.out.println("集合长度="+c.size());
}
}
contains(Object o)方法
判断集合中是否包含o元素,如果包含则返回true,否则返回false,需要注意的是contains方法底层是根据对象的equals方法来判断的,如果你自己创建的类没有重写equals方法的话,那么比较的时候就会调用用Object类的equals方法(比较地址,因为所有的类都继承自Object)。
Student类:
public class Student {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
//如果object对象为空或者不是Student类,返回false
if ( o==null || !(o instanceof Student) ) return false;
//如果object对象就是这个对象,返回true
if ( o==this ) return true;
//将Object对象转换为Student类对象
Student student= (Student) o;
//返回Objecmeit类型对象的name与该对象的name的比较结果
return student.name.equals(this.name);
}
}
如果不重写Student类里的equals方法就会返回false,重写了则返回true
public class TestContains {
public static void main(String[] args) {
Collection c=new ArrayList();
Student student = new Student("测试");
c.add(student);
//比较
System.out.println(c.contains(new Student("测试")));
}
}
remove(Object o)方法
remove方法跟contains方法类似,remove方法也是先利用对象的equals方法判断集合中是否存在该对象,如果存在则移除,否则则不移除。同样注意需要重写类对象中的equals方法,不然比较的仍然是地址,remove()则无法匹配集合中的对象则无法移除。
List接口继承自Collection,它不仅包含Collection接口中的方法,还有以下特有的常用方法:
方法 | 描述 |
---|---|
void add(int index, E element)(重载) | 向列表中指定位置index(下标)插入指定元素element(新增) |
E set(int index,E element) | 用element元素替换指定位置index的元素(覆盖) |
E get(int index) | 返回该列表中指定位置的元素 |
int indexOf(Object o) | 返回该列表第一次出现o元素的索引,没有返回-1 |
int lastIndexOf(Object o) | 返回该列表最后一次出现o元素的索引,反正则返回-1 |
因为List接口中多了一个get方法能够得到元素的下标,所以我们多了一种新的遍历方式
public class TestList {
public static void main(String[] args) {
//创建List列表对象list
List list=new ArrayList();
//往列表list中添加元素
list.add(100);
list.add("test");
list.add(4564);
//给列表list中下标为3的地方新增
list.add(3,"index为3");
//用for循环遍历list
for (int i = 0; i < list.size(); i++) {
//根据列表下标i来遍历
System.out.println(list.get(i));
}
}
}
ArrayList是List的实现类,所以它也要实现List接口的所有方法。ArrayList的初始容量是10,它有一个重载的有参构造方法可以传递初始化ArrayList初始容量(如果容量不够,后面ArrayLIst会自动扩容),扩容机制就是扩容到原容量的1.5倍。
构造方法:
//默认初始化容量是10
List list1=new ArrayList();
//自定义初始化容量100
List list2=new ArrayList(100);
ArrayList的特点:底层是数组,元素之间都是相邻的内存地址,查询效率高,向指定下标add效率低,增删效率低。
LinkedList也是List的实现类,所以它也要实现List接口的所有方法。LinkedList底层是双向链表,将双向链表之前先看看单向链表
单向链表的查询效率比较低
是因为单向链表的每个元素在内存中的存储位置是不规律的,并且没有顺序,仅仅是靠上一个节点里存储的下一个节点的内存地址才能找到下一个元素,所以我想要查询元素的时候每次都要从头节点开始找,依次从元素里存储的下个节点的内存地址找,很慢。
单向链表的增删效率比较高
是因为单向链表的每个元素里存储得有下个元素的内存地址,想要删掉某一个元素的话就直接将指向该元素的上一个元素的内存地址指向下一个元素就好了,因为这个元素没有被任何元素指向了所以Java的垃圾回收机制就会将这个内存回收。增加某个元素的话就只需要让指向另外元素的存的内存地址指向新增的这个,新增的这个里面再存刚刚另外的元素存的。相比数组增加或者删除会使这个元素后面所有的元素的内存地址发生位移。
单链表中删除元素
想要删除B节点只需要将B节点里存储的C的内存地址赋给A节点,这样AB都同时指向C了,但是并没有元素指向B节点,B节点就会被Java垃圾机制回收掉。C节点什么都不需要操心。
单链表中添加元素
想要将B节点添加进AC节点之间,只需要将原本A节点里存储的C节点的内存地址丢给B节点这样B就可以指向C了,然后A节点再存储B节点的内存地址这样A节点就可以指向B了。C节点也是什么都不需要操心。
节点的前后结点
节点的前结点等于上一个元素的后结点;同理,节点的后结点是下一个元素的前结点。
LinkedList的特点:底层是双向链表,查询效率较低,随机增删的效率较高。
Vector也是List的实现类,它是线程安全的。初始化容量是10,每次扩容是扩容到原容量的2倍。它里面的所有方法都是有syschronized关键字的(线程同步的)。
Vector类的特点:底层是数组,线程安全的;不常用。
java.util.collections
包下的synchronizedList(List
静态方法可以将List列表变成线程安全的
//创建ArrayList列表
List arrayList=new ArrayList();
//向列表中添加元素
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
//将ArrayList列表变成线程安全的
List synchronizedList = Collections.synchronizedList(arrayList);
在JDK5里Java新增了一个新特性:泛型机制。
泛型的本质是参数化类型,这种参数类型可以用在类、变量、接口和方法的创建中,分别称为泛型类、泛型变量、泛型接口、泛型方法。
在Java集合框架中就大量使用泛型,所以我们有必要搞懂。先举个例子看看在集合中没有使用泛型和使用泛型的区别。
没有使用泛型
public class TestList03 {
public static void main(String[] args) {
//创建没有使用泛型的集合对象List1
List list1=new ArrayList();
//向List1中添加元素
list1.add("test1");
list1.add("test2");
list1.add(100);
list1.add(23123);
//利用迭代器进行遍历
Iterator iterator = list1.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
}
}
没有使用泛型限制前,我们可以往集合中添加任意类型的元素,比如String、Integer类型。
使用泛型
public class TestList04 {
public static void main(String[] args) {
//创建有泛型限制的集合对象list2
List<String> list2=new ArrayList<String>();
//向集合list2中添加元素
list2.add("test1");
list2.add("test2");
list2.add("test3");
//利用迭代器遍历元素
Iterator<String> iterator = list2.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
System.out.println(s);
}
}
}
加上泛型之后,我们就只能往元素中添加String类型的元素了,也就是只能添加你用泛型限制的那个类型的元素和这个类型的子类。
同时定义泛型对象的写法有三种
//完整写法
List<String> list2=new ArrayList<String>();
//钻石表达式:去掉后面的String
List<String> list2=new ArrayList<>();
//省略写法:去掉后面的
List<String> list2=new ArrayList();
所以可以理解为没有加上泛型限制的集合对象,实质上有一个隐藏泛型限制(Object)
我们也可以在创建类的时候使用自定义泛型
public class User<E> {
E get(){
return null;
}
void set(E e){
}
}
在创建类的时候类名后面加上
或者
甚至随便什么名字,指定这个类可以使用泛型,E和T只是一个标识符,用了泛型后,下面get方法的返回值就是这个泛型,下面set方法的参数也是这个泛型。比如我创建User类对象的时候使用了User
,那么get方法的返回值就是String,set的参数就应该是String类型
public static void main(String[] args) {
//创建不带泛型的User对象
User user1 = new User();
//返回值是Object类型
Object o = user1.get();
//参数类型也是Object
user1.set(new Object());
//创建带泛型的User对象
User<String> user2=new User<>();
//返回值就是String类型
String s = user2.get();
//参数也是String类型
user2.set(new String());
}
所以可以理解为如果没有指定泛型那么那个泛型就是所有类型的父类(Object)
使用集合迭代器进行循环:
List list1=new ArrayList();
list1.add(1);
list1.add(2);
list1.add(3);
list1.add(4);
list1.add(5);
Iterator iterator = list1.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
获得迭代器,然后利用迭代器的hasNext方法判断是否有下一个元素,再利用next获取元素
使用集合的get得到下标进行循环:
List list1=new ArrayList();
list1.add(1);
list1.add(2);
list1.add(3);
list1.add(4);
list1.add(5);
for (int i = 0; i < list1.size() ; i++) {
System.out.println(list1.get(i));
}
利用i作为循环数循环,利用集合对象的get方法下标得到对象
使用增强for循环
List list1=new ArrayList();
list1.add(1);
list1.add(2);
list1.add(3);
list1.add(4);
list1.add(5);
for (Object o : list1) {
System.out.println(o);
}
利用for(集合中单个元素的数据类型 变量名:集合对象){}
来循环
增强for循环的特点:效率很高,但是缺点是没有下标,有的时候可能需要下标。
Map跟Collection是并列的,Map存储数据的方法是以键值对(Key-Value)的方式存储的,键才是主要的,值只是键的附属。
Map接口中的常用方法如下:
方法 | 描述 |
---|---|
V put(K key,V value) | 以键值对的方式将元素添加到集合 |
V get(Object key) | 根据Key来查询Value,返回值是Value的数据类型 |
int size() | 返回集合中键的数目 |
boolean containsKey(Object key) | 查询集合中是否包含这个key,包含返回true,否则返回false |
boolean containsValue(Object value) | 查询集合中是否包含这个value,包含返回true,否则返回false |
boolean isEmpty() | 判断集合中元素是否为空,是返回true,否则返回false |
void clear() | 清空集合 |
V remove(Object key) | 根据key来移除这个键值对(前提是该键存在) |
Collection |
以Collect集合的类型返回该集合的所有value |
Set |
以Set集合的类型返回该集合的所有key |
Set |
以Set集合的类型返回该集合的所有键值对 |
测试以下常用的添加和查询还有判断方法,其中Map集合的containsKey()
、containsValue()
、remove()
方法底层都是用equals方法进行比较的,如果自定义类没有重写equals方法就会用Object类的equals方法比较地址。
public class TestMap {
public static void main(String[] args) {
//创建key为Integer类型,value为String类型的Map集合map
Map<Integer,String> map=new HashMap();
//向集合中以键值对的方式添加元素
map.put(1,"test1");
map.put(2,"test2");
map.put(3,"test3");
//根据key是1来得到值
String s = map.get(1);// s="test1"
//判断是否有1这个key
boolean b = map.containsKey(1);// b=true
//判断是否有test4这个value
boolean b1 = map.containsValue("test4"); //b1=false
//判断集合元素是否为空
boolean b2 = map.isEmpty(); //b2=false
//获得集合长度
int i = map.size(); //i=3
//获得集合的所有key
Set<Integer> keys = map.keySet();
//遍历key
for (Integer key : keys) {
System.out.println(key);
}
//获得集合的所有value
Collection<String> values = map.values();
//遍历value
for (String value : values) {
System.out.println(value);
}
}
}
先遍历出所有的key,然后通过集合的get方法得到key对应的value
public class TestMap02 {
public static void main(String[] args) {
//创建Map集合对象
Map<Integer,String> map=new HashMap<>();
//向集合中以键值对的方式添加元素
map.put(1,"test1");
map.put(2,"test2");
map.put(3,"test3");
map.put(4,"test4");
map.put(5,"test5");
//获得集合中的所有key
Set<Integer> keys = map.keySet();
/*
遍历方式1:得到key的迭代器,通过key得到value
*/
Iterator<Integer> iterator = keys.iterator();
while (iterator.hasNext()) {
Integer key = iterator.next();
String value = map.get(key);
System.out.println(key+"--->>"+value);
}
System.out.println("===============================");
/*
遍历方式2:用增强for循环遍历
*/
for (Integer key : keys) {
String value = map.get(key);
System.out.println(key+"--->>"+value);
}
}
}
用entrySet()
方法,得到所有的键值对集合,然后再分别取出
/*
遍历方式3:
1、得到Map.Entry对象
2、通过Entry.getKey()得到key属性
3、通过Entry.getValue()得到value属性
*/
Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
for (Map.Entry<Integer, String> entry : entrySet) {
Integer key = entry.getKey();
String value = entry.getValue();
System.out.println(key+"--->>"+value);
}
其中Entry是Map的内部类。key和value是Map.Entry内部类的属性
方式2的效率比方式1高一些,因为方式1先遍历出了所有的key之后还需要通过key去查询value,而第二种方式就直接能遍历出key-value整个对象。
数组:内存地址连续,查询效率高,随机增删效率低;
单向链表:内存地址没有规律,查询效率低,随机增删效率高。
哈希表:将数组和单向链表结合
哈希表由node节点类似于数组以及每个数组里存的单向链表组成。实际是Hashset集合
+单向链表
,其中set集合中的单个元素的hash值是相同的
其中node节点由key、value、下个节点的地址next以及这个node节点的hash值组成。
HashMap的put方法的原理:
put(key,value)方法是将键值对参数key、value封装成node节点,然后key再调用hashCode()
方法得出key的hash值,然后hash值经过哈希算法得到存放在数组中的下标,如果没有这个链表,那么这个元素就放在数组的第一个,否则再拿着key和有着同一个下标的链表中用equals方法进行比较key,如果key都不一样,那么这个元素会被放在最后一个,其中有一个一样的话就会将其覆盖。
HashMap的get方法的原理:
get(k),get方法需要的参数是k,get方法会将k经过hashCode()
方法将他k的hash值算出来,hash值再经过哈希算法得出数组的下标,如果有这个下标的这一条链表,它就继续找,否则就直接返回null(没有对应的value)。找到了和k下标值的链表,那么就继续寻找,和这条链表上的k用equals方法进行比较,如果equals返回成功,那么说明有这个元素就返回这个元素的value。
所以,我们在存放HashSet
和HashMap
的key部分的时候中尽量重写equals方法和hashCode方法。因为如果没有重写的话这两个方法默认的都是(Object类里)返回的对象内存地址,对象的内存地址是肯定不会相同的,所以equals比较起来肯定不同,就会造成k重复,并且hashCode返回的也是内存地址,那么所有的对象的内存地址肯定不同,在数组中存的hash值也肯定不同,哈希表的作用没有发挥到最大。
就有点像字典,这个set集合存放的hash值就是目录里的偏旁,然后这个单向链表就是这个偏旁的哪些字。
问题:
如果不重写hashCode()
会怎样?
如果不重写hashCode()
方法,那么返回的hash值就是对象的内存地址,对象的内存地址肯定是不相同的,所以set集合里存放的hash值就特别多,单向链表就特别短,甚至每条单向链表只有一个元素,那就完全成了一个一维数组。这叫散列分布不均匀。
如果重写了hashCode()
返回的hash值是固定的会怎样
如果返回的是固定的hash值那么所有元素都在一条单向链表上,那就完全成了一个单向链表。这叫散列分布不均匀。
如果没有重写equals()
会怎样?
如果不重写equals()
方法,那么比较的时候就会采用Object类的equals方法比较内存地址,内存地址肯定是不相同的,所有即使内容一模一样但是内存地址不一样也会导致程序判断不相同,所以put存数据永远是存最后一个,get查询永远查不到。
HashMap的默认初始化容量是16,默认加载因子是0.75(数组元素达到容量的75%时数组开始扩容,扩容后是原容量的二倍)。同时Java强烈推荐HashMap的初始化容量是2的次幂(2,4,8,16,32,64),这是为了达到散列均匀,提高效率。
Hashtable接口继承自Map
底层的数据结构是哈希表,它是线程安全的(方法都带有syschronized关键字)默认初始化容量是11,默认加载因子是0.75f,扩容是原容量x2+1
Hashtable的k,v不允许null;HashMap的k,v运行null
Properties类是Hashtable的实现类,只能以键值对的方式存储数据,它也是线程安全的。
方法 | 描述 |
---|---|
void setProperty(String key,String value) | 向集合里存以键值对的方式存数据 |
String getProperty(String key) | 通过key得到value |
public class TestProperties {
public static void main(String[] args) {
//创建properties集合对象
Properties properties=new Properties();
//向properties集合对象中存键值对
properties.setProperty("url","localhost:8080");
properties.setProperty("username","root");
properties.setProperty("password","123456");
//从properties集合对象中去键值对
String url = properties.getProperty("url");
String username = properties.getProperty("username");
String password = properties.getProperty("password");
//输出
System.out.println(url); //localhost:8080
System.out.println(username); //root
System.out.println(password); //123456
}
}
TreeSet实现了SortedSet接口,能够对存入的数据自动进行排序;TreeSet和TreeMap的key部分的排序原理是一样的,底层都是采用的二叉树中序遍历进行排序。TreeSet调用add方法的时候底层实际上调用了TreeMap的put方法,然后把Object赋给了key部分。
一个树节点由5部分组成:
Node left; //左子树地址
Node rigth; //右子树地址
Node parent; //父子树地址
Object key; //键
Object value; //值
TreeSet能够很快进行排序就是因为底层采用的是中序遍历排序,在TreeSet调用add方法(实质上调用TreeMap的put)方法的时候就已经排好序了,就是边放置边排序。
比如:
就可以将存入的随机顺序经过二叉树排序取出的时候就能排序了。其中不难想象,中间肯定要经过key与key之间的比较,如果一样则覆盖,如果后存入的数大就放在右边,后存入的数小就放在左边。所以这里就涉及到key这个对象的compareTo()方法。因为Integer、String类都继承了Comparable接口并且重写了compareTo()方法,所以才能比较。就好像你比较两个对象总得有个条件才行,不然两个对象比较毫无意义。
所以我们想要自定义类就可以在里面添加自定义比较规则:
创建类继承Comparable接口并且重写comparable()方法,在comparable里指定比较规则
User类
public class User implements Comparable<User>{
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
/**
* 根据用户的年龄升序排序
* @param u 先存入的对象(父子树)对象
* @return 如果后存入的对象大于先存入的对象则返回正数,放到右子树
* 如果后存入的对象小于先存入的对象则返回负数,放到左子树
* 如果后存入的对象等于先存入的对象则返回0,覆盖
*/
@Override
public int compareTo(User u) {
return this.age-u.age;
}
}
这里需要特别特别特别注意的是,重写的compareTo(Object o)方法里接收到的o参数是先存入的对象,实质上就是后存入的对象调用了这个方法与后存入的对象进行比较,再根据返回的是正负数或者0来进行比较的。
就是后存入的对象.compareTo(先存入的对象)
(TreeMap的put方法中源代码是这样写的)
如果返回的数值大于0,则后存入的对象放到右子树;
如果返回的数值小于0,则后存入的对象放到左子树;
如果返回的数值等于0,则后存入的对象覆盖先存入的对象。
测试:
public class TestTreeSet {
public static void main(String[] args) {
//创建users集合对象
TreeSet<User> users=new TreeSet<>();
//向users集合中添加元素
users.add(new User("test1",45));
users.add(new User("test3",68));
users.add(new User("test2",78));
users.add(new User("test4",11));
//遍历users集合
for (User user : users) {
System.out.println(user);
}
}
}
运行结果:
创建一个比较器comparator类继承Comparator接口,将它作为参数传递给TreeSet
User类就不再实现Comparable接口,也不再重写compareTo()方法了,直接创建一个比较器对象UserComparator实现Comparator,并且重写compare方法,在compare方法指定比较规则
public class TestTreeSet {
public static void main(String[] args) {
//创建比较器对象
UserComparator userComparator=new UserComparator();
//创建users集合对象,并且将比较器作为对象传递给集合
TreeSet<User> users=new TreeSet<>(userComparator);
//向users集合中添加元素
users.add(new User("test1",45));
users.add(new User("test3",68));
users.add(new User("test2",78));
users.add(new User("test4",11));
//遍历users集合
for (User user : users) {
System.out.println(user);
}
}
}
class UserComparator implements Comparator<User>{
/**
* 根据用户的年龄升序排序
* @param u1 后存入的对象
* @param u2 先存入的对象
* @return 后存入的对象的年龄大于先存入对象的年龄返回正数,放到右子树
* 后存入的对象年龄小于先存入对象的年龄返回负数,放到左子树
* 后存入的对象年龄等于先存入对象的年龄返回0,覆盖
*/
@Override
public int compare(User u1, User u2) {
return u1.getAge()-u2.getAge();
}
}
或者直接用匿名内部类实现Comparator接口指定规则
public class TestTreeSet {
public static void main(String[] args) {
//创建users集合对象,并且使用匿名内部类实现Comparator接口
TreeSet<User> users=new TreeSet<>(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return u1.getAge()-u2.getAge();
}
});
//向users集合中添加元素
users.add(new User("test1",45));
users.add(new User("test3",68));
users.add(new User("test2",78));
users.add(new User("test4",11));
//遍历users集合
for (User user : users) {
System.out.println(user);
}
}
}
第一个方法是在类里实现java.lang.Comparable
(可比较的)接口,重写compareTo(先存入的对象)方法:这个t就是父子树(先存入的对象)
第二个方式是创建比较器对象实现java.util.Comparator
(比较器)接口,重写compare(后存入的对象,先存入的对象):