先上一张简化的集合框架关系图:↓ ↓ ↓
(1)、Collection 接口
Collection是 List 、Set 、Queue(先进先出队列)、Deque(双向链表) 等接口的父接口。
【注:Collection 和 Map 之间没有关系,Collection是放一个一个对象的,Map 是放键值对的】
【注:Deque 继承 Queue,间接得继承了 Collection】
(2)、Collections 类
Collections是一个类,容器的工具类,就如同 Arrays 是数组的工具类一样,它也可以对容器进行一些操作。
方法名关键字 | 功能 |
reverse | 反转 |
shuffle | 混淆(打乱顺序) |
sort | 排序 |
swap | 交换指定下标位置的数据 |
rotate | 把集合中的数据向右滚动指定单位的长度 |
synchronizedList | 非线程安全的集合转换为线程安全的集合 |
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CollectionsTest {
public static void main(String[] args) {
// 初始化List
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i+1);
}
// 原List
System.out.println("原list:\t\t\t" + list);
System.out.println("******************************");
// 反转
Collections.reverse(list);
System.out.println("反转后的list:\t\t" + list);
System.out.println("******************************");
// 混淆
Collections.shuffle(list);
System.out.println("混淆后的list:\t\t" + list);
System.out.println("******************************");
// 排序
Collections.sort(list);
System.out.println("排序后的list:\t\t" + list);
System.out.println("******************************");
// 交换两个位置的数据
Collections.swap(list, 0, 1);
System.out.println("交换0和1下标的数据后的list:\t" + list);
System.out.println("******************************");
// 先恢复原list
Collections.swap(list, 0, 1);
System.out.println("恢复后的list:\t\t" + list);
// 滚动
Collections.rotate(list, 3);
System.out.println("向右滚动3个单位后的list:\t" + list);
System.out.println("******************************");
// 线程安全化
List<Integer> newList = Collections.synchronizedList(list);
// 把原来不是线程安全的list转换为了现在线程安全的newList
}
}
输出结果:
(3)、List 接口与 ArrayList 类
List 集合中的对象按照一定的顺序排放,里面的内容可以重复;
List 接口实现的类有:ArrayList(实现动态数组),Vector(实现动态数组),LinkedList(实现链表),Stack(实现堆栈)。
[1]、使用数组的局限性
如果要存放多个对象,可以使用数组,但是数组有局限性!!!
比如 :声明长度是10的数组,不用的数组就浪费了,超过10的个数,又放不下!!!
[2]、ArrayList
为了解决数组的局限性,引入容器类的概念。 最常见的容器类就是 : ArrayList
容器的容量"capacity"会随着对象的增加,自动增长。
[3]、ArrayList 的常用方法
方法名关键字 | 功能 |
contains | 判断是否存在 |
add | 添加对象 |
addAll | 把另一个容器所有对象都加进来 |
remove | 删除对象 |
clear | 清空 |
set | 替换某位置的对象(重赋值) |
get | 获取指定位置的对象 |
indexOf | 获取对象所处的位置 |
toArray | 转换为数组 |
size | 获取大小 |
[4]、迭代器遍历
使用迭代器Iterator遍历集合中的元素,原理如下:↓ ↓ ↓
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IteratorTest {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 5; i++) {
list.add("String " + i);
}
System.out.println("最初的list为:" + list);
// 获取List的迭代器
Iterator<String> iterator = list.iterator();
//从最开始的位置判断"下一个"位置是否有数据
//如果有就通过next取出来,并且把指针向下移动
//直到"下一个"位置没有数据
System.out.println("--------使用while的iterator-------");
// 利用while循环遍历
while(iterator.hasNext()) {
String str = iterator.next();
System.out.println(str);
}
System.out.println("--------使用for的iterator-------");
// 利用for循环遍历
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String str = it.next();
if(str.equals("String 2")) {
it.remove();
System.out.println(str + "(当字符串为“String 2”时从迭代器中删除它)");
}else {
System.out.println(str);
}
}
System.out.println("删除“String 2”后的list为:" + list);
}
}
输出结果:
可以利用迭代器删除list中的元素。
(4)、LinkedList
序列分为: 先进先出FIFO 、先进后出FILO
FIFO在Java中又叫Queue 队列
FILO在Java中又叫Stack 栈
[1]、与ArrayList一样,LinkedList也实现了List接口;
[2]、但同时,LinkedList 除了实现了 List 接口外,LinkedList 还实现了双向链表结构 Deque,可以很方便的在头尾插入删除数据;
import java.util.LinkedList;
import java.util.List;
public class CollectionsTest {
public static void main(String[] args) {
//LinkedList是一个双向链表结构的list
LinkedList<String> list =new LinkedList<>();
//所以可以很方便的在头部和尾部插入数据
//在最后插入新的字符串
list.addLast("String 1");
list.addLast("String 2");
list.addLast("String 3");
System.out.println("加入数据后的list:\t\t" + list);
//在最前面插入新的字符串
list.addFirst("String X");
System.out.println("最前面插入新的字符串后的list:\t" + list);
//查看最前面的字符串
System.out.println("最前面的字符串:\t\t " + list.getFirst());
//查看最后面的字符串
System.out.println("最后面的字符串:\t\t " + list.getLast());
//查看不会导致元素被删除
System.out.println("再次查看list:\t\t" + list);
//取出最前面的字符串
System.out.println("取出来的最前面的字符串:\t " + list.removeFirst());
//取出最后面的字符串
System.out.println("取出来的最后面的字符串:\t " + list.removeLast());
//取出会导致元素被删除
System.out.println("再次查看list:\t\t" + list);
}
}
输出结果:
[3]、LinkedList 除了实现了List和Deque外,还实现了Queue接口(队列)
Queue是先进先出队列 FIFO,常用方法:
offer 在最后添加元素
poll 取出第一个元素
peek 查看第一个元素
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class CollectionsTest {
public static void main(String[] args) {
// 和ArrayList一样,LinkedList也实现了List接口
List<String> list = new LinkedList<>();
// 同时,LinkedList还实现了Deque接口
// Deque代表双向链表
Deque<String> deque = new LinkedList<>();
// 同时,LinkedList也实现了Queue这个接口
// Queue代表FIFO 先进先出的队列
Queue<String> queue = new LinkedList<>();
for (int i = 0; i < 2; i++) {
queue.offer("String " + i); // 在最后添加元素
}
for (int i = 0; i < 3; i++) {
queue.add("String " + (i + 2)); // 与offer()同样的效果,添加元素
} //
System.out.println("利用offer()方法或add()方法添加元素。");
System.out.println("添加元素后的queue为:\t" + queue);
System.out.println();
String first = queue.peek(); //
System.out.println("利用peek()方法查看第一个元素。");
System.out.println("查看第一个元素:\t\t " + first);
System.out.println();
queue.poll(); //
System.out.println("利用poll()方法取出第一个元素。");
System.out.println("取出第一个元素......");
System.out.println("第一个元素取出后的queue为:\t" + queue);
}
}
输出结果:
(5)、二叉树
二叉树由各种节点组成
二叉树特点:
每个节点都可以有左子节点、右子节点、值。
public class Node {
// 左子节点
public Node leftNode;
// 右子节点
public Node rightNode;
// 值
public Object value;
}
[1]、二叉树-插入数据-原理:
假设通过二叉树对如下10个随机数进行排序
67,7,30,73,10,0,78,81,10,74
排序的第一个步骤是把数据插入到该二叉树中
插入基本逻辑是,小、相同的放左边,大的放右边
1. 67 放在根节点
2. 7 比 67小,放在67的左节点
3. 30 比67 小,找到67的左节点7,30比7大,就放在7的右节点
4. 73 比67大, 放在67得右节点
5. 10 比 67小,找到67的左节点7,10比7大,找到7的右节点30,10比30小,放在30的左节点。
...
...
9. 10比67小,找到67的左节点7,10比7大,找到7的右节点30,10比30小,找到30的左节点10,10和10一样大,放在左边
public class Node {
/**
* 左子节点
*/
public Node leftNode;
/**
* 右子节点
*/
public Node rightNode;
/**
* 值
*/
public Object value;
/**
* 插入数据
* @param v
*/
public void add(Object v) {
// 如果当前节点没有值,就把数据放在当前节点上
if (value == null) {
value = v;
} else { // 如果当前节点有值,就进行判断,新增的值与当前的值的大小关系
// 新增的值,比当前值小或者相同
if ((Integer) value - (Integer) v >= 0) {
if (leftNode == null) {
leftNode = new Node();
}
leftNode.add(v);
}
// 新增的值,比当前值大
else {
if (rightNode == null) {
rightNode = new Node();
}
rightNode.add(v);
}
}
}
public static void main(String[] args) {
int randoms[] = new int[] { 67, 7, 30, 73, 10, 0, 78, 81, 10, 74 };
Node roots = new Node();
for (int number : randoms) {
roots.add(number);
}
}
}
[2]、二叉树-遍历:
通过上一个步骤的插入行为,实际上,数据就已经排好序了。 接下来要做的是看,把这些已经排好序的数据,遍历成我们常用的List或者数组的形式
二叉树的遍历分先序,中序,后序
【先序】即: 中间的数 遍历后放在 左边
【中序】即: 中间的数 遍历后放在 中间
【后序】即: 中间的数 遍历后放在 右边
import java.util.ArrayList;
import java.util.List;
public class Node {
/**
* 左子节点
*/
public Node leftNode;
/**
* 右子节点
*/
public Node rightNode;
/**
* 值
*/
public Object value;
/**
* 插入数据
* @param v
*/
public void add(Object v) {
// 如果当前节点没有值,就把数据放在当前节点上
if (value == null) {
value = v;
} else { // 如果当前节点有值,就进行判断,新增的值与当前的值的大小关系
// 新增的值,比当前值小或者相同
if ((Integer) value - (Integer) v >= 0) {
if (leftNode == null) {
leftNode = new Node();
}
leftNode.add(v);
}
// 新增的值,比当前值大
else {
if (rightNode == null) {
rightNode = new Node();
}
rightNode.add(v);
}
}
}
// 先序遍历所有的节点
public List<Object> valuesOfLeft() {
List<Object> values = new ArrayList<>();
// 当前节点
values.add(value);
// 左节点的遍历结果
if (leftNode != null) {
values.addAll(leftNode.valuesOfLeft());
}
// 右节点的遍历结果
if (rightNode != null) {
values.addAll(rightNode.valuesOfLeft());
}
return values;
}
// 中序遍历所有的节点
public List<Object> valuesOfCenter() {
List<Object> values = new ArrayList<>();
// 左节点的遍历结果
if (leftNode != null) {
values.addAll(leftNode.valuesOfCenter());
}
// 当前节点
values.add(value);
// 右节点的遍历结果
if (rightNode != null) {
values.addAll(rightNode.valuesOfCenter());
}
return values;
}
// 后序遍历所有的节点
public List<Object> valuesOfRight() {
List<Object> values = new ArrayList<>();
// 左节点的遍历结果
if (leftNode != null) {
values.addAll(leftNode.valuesOfRight());
}
// 右节点的遍历结果
if (rightNode != null) {
values.addAll(rightNode.valuesOfRight());
}
// 当前节点
values.add(value);
return values;
}
public static void main(String[] args) {
int randoms[] = new int[] { 67, 7, 30, 73, 10, 0, 78, 81, 10, 74 };
Node roots = new Node();
for (int number : randoms) {
roots.add(number);
}
System.out.println("先序遍历的二叉树:\t" + roots.valuesOfLeft());
System.out.println("中序遍历的二叉树:\t" + roots.valuesOfCenter());
System.out.println("后序遍历的二叉树:\t" + roots.valuesOfRight());
}
}
输出结果:
(6)、HashMap
HashMap储存数据的方式是—— 键值对
对于HashMap而言,key是唯一的,不可以重复的。
所以,以相同的key 把不同的value插入到 Map中会导致旧元素被覆盖,只留下最后插入的元素。
不过,同一个对象可以作为值插入到map中,只要对应的key不一样
(7)、HashSet
Set中的元素,不能重复
[1]、没有顺序
Set中的元素,没有顺序。
严格的说,是没有按照元素的插入顺序排列
HashSet的具体顺序,既不是按照插入顺序,也不是按照hashcode的顺序。
换句话说,同样是插入0-9到HashSet中, 在JVM的不同版本中,看到的顺序都是不一样的。
import java.util.HashSet;
public class HashSetTest {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("hello");
set.add("world");
set.add("!!!");
System.out.println(set);
}
}
运行结果:
[2]、遍历
Set不提供get()来获取指定位置的元素
所以遍历需要用到迭代器,或者增强型for循环
[3]、HashSet和HashMap的关系
通过观察HashSet的源代码,
可以发现HashSet自身并没有独立的实现,而是在里面封装了一个Map.
HashSet是作为Map的key而存在的
而value是一个命名为PRESENT的static的Object对象,因为是一个类属性,所以只会有一个。
private static final Object PRESENT = new Object();
package collection;
import java.util.AbstractSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
// HashSet里封装了一个HashMap
private HashMap<E, Object> map;
private static final Object PRESENT = new Object();
// HashSet的构造方法初始化这个HashMap
public HashSet() {
map = new HashMap<E, Object>();
}
// 向HashSet中增加元素,其实就是把该元素作为key,增加到Map中
// value是PRESENT,静态,final的对象,所有的HashSet都使用这么同一个对象
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
// HashSet的size就是map的size
public int size() {
return map.size();
}
// 清空Set就是清空Map
public void clear() {
map.clear();
}
// 迭代Set,就是把Map的键拿出来迭代
public Iterator<E> iterator() {
return map.keySet().iterator();
}
}
(8)、Set
HashSet 无序
LinkedHashSet 按照插入顺序
TreeSet 从小到大排序
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.TreeSet;
public class HashSetTest {
public static void main(String[] args) {
HashSet<String> numberSet1 =new HashSet<>();
//HashSet中的数据不是按照插入顺序存放
numberSet1.add("bbbbb");
numberSet1.add("aaaaa");
numberSet1.add("ccccc");
System.out.println(numberSet1);
LinkedHashSet<String> numberSet2 =new LinkedHashSet<>();
//LinkedHashSet中的数据是按照插入顺序存放
numberSet2.add("bbbbb");
numberSet2.add("aaaaa");
numberSet2.add("ccccc");
System.out.println(numberSet2);
TreeSet<String> numberSet3 =new TreeSet<>();
//TreeSet 中的数据是进行了排序的
numberSet3.add("bbbbb");
numberSet3.add("aaaaa");
numberSet3.add("ccccc");
System.out.println(numberSet3);
}
}
运行结果:
(9)、ArrayList和LinkedList的区别
ArrayList 是【顺序表】结构,【查找数据快】,【插入、删除数据慢】;
LinkedList 是【链表】结构,【查找数据慢】,【插入、删除数据快】。
(10)、ArrayList和HashSet的区别
ArrayList 有顺序,数据可以重复;
HashSet 无顺序,数据不能够重复。
HashSet 重复判断标准是:
首先看hashcode是否相同
如果hashcode不同,则认为是不同数据
如果hashcode相同,再比较equals,如果equals相同,则是相同数据,否则是不同数据
(11)、HashMap 和HashTable 的区别
HashMap 和 Hashtable 都实现了Map接口,都是键值对保存数据的方式
区别1:
HashMap 可以存放 null 值
Hashtable 不能存放null 值
区别2:
HashMap不是线程安全的类
Hashtable是线程安全的类
import java.util.HashMap;
import java.util.Hashtable;
public class MapTableTest {
public static void main(String[] args) {
// HashMap 可以存放 null 值
HashMap<String, Integer> map = new HashMap<>();
map.put("first", null);
System.out.println(map);
// Hashtable 不能存放null 值
Hashtable<String, Integer> table = new Hashtable<>();
table.put("first", null);
System.out.println(table);
}
}
运行结果:
(12)、HashCode原理
[1]、效率比较
HashCode带来的差别如下:
[2]、HashCode 原理
比如要在一本英汉字典中找一个单词对应的中文意思,假设单词是Lengendary,首先在目录找到Lengendary在第 555页。
然后,翻到第555页,这页不只一个单词,但是量已经很少了,逐一比较,很快就定位目标单词Lengendary。
555相当于就是Lengendary对应的hashcode
[3]、分析HashMap性能卓越的原因
-----hashcode概念-----
所有的对象,都有一个对应的hashcode(散列值)
比如字符串“gareen”对应的是1001 (实际上不是,这里是方便理解,假设的值)
比如字符串“temoo”对应的是1004
比如字符串“db”对应的是1008
比如字符串“annie”对应的也是1008
-----保存数据-----
准备一个数组,其长度是2000,并且设定特殊的hashcode算法,使得所有字符串对应的hashcode,都会落在0-1999之间
要存放名字是"gareen"的英雄,就把该英雄和名称组成一个键值对,存放在数组的1001这个位置上
要存放名字是"temoo"的英雄,就把该英雄存放在数组的1004这个位置上
要存放名字是"db"的英雄,就把该英雄存放在数组的1008这个位置上
要存放名字是"annie"的英雄,然而 "annie"的hashcode 1008对应的位置已经有db英雄了,那么就在这里创建一个链表,接在db英雄后面存放annie
-----查找数据-----
比如要查找gareen,首先计算"gareen"的hashcode是1001,根据1001这个下标,到数组中进行定位,(根据数组下标进行定位,是非常快速的) 发现1001这个位置就只有一个英雄,那么该英雄就是gareen.
比如要查找annie,首先计算"annie"的hashcode是1008,根据1008这个下标,到数组中进行定位,发现1008这个位置有两个英雄,那么就对两个英雄的名字进行逐一比较(equals),因为此时需要比较的量就已经少很多了,很快也就可以找出目标英雄
这就是使用hashmap进行查询,非常快原理。
这是一种用空间换时间的思维方式
[4]、HashSet判断是否重复
HashSet的数据是不能重复的,相同数据不能保存在一起,到底如何判断是否是重复的呢?
根据HashSet和HashMap的关系,我们了解到因为HashSet没有自身的实现,而是里面封装了一个HashMap,所以本质上就是判断HashMap的key是否重复。
再通过上一步的学习,key是否重复,是由两个步骤判断的:
hashcode是否一样
如果hashcode不一样,就是在不同的坑里,一定是不重复的
如果hashcode一样,就是在同一个坑里,还需要进行equals比较
如果equals一样,则是重复数据
如果equals不一样,则是不同数据。
(13)、比较器
Collections.sort()方法可以实现对集合内元素的排序
但是,假如,有一个集合,里面的元素全是Person类型的对象,其属性有name,age,address等,到底按那个属性进行排序?
这就要用到比较器!!!
[1]、方法一
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
public class ComparatorTest {
public static void main(String[] args) {
Random r = new Random();
List<Person> persons = new ArrayList<>();
for (int i = 0; i < 10; i++) {
persons.add(new Person("张" + i, r.nextInt(20) + 20, "西安"));
}
System.out.println("初始化后的集合:");
System.out.println(persons);
// 直接调用sort会出现编译错误,因为Person有各种属性
// 到底按照哪种属性进行比较,Collections也不知道,不确定,所以没法排
// Collections.sort(persons);
// 引入Comparator,指定比较的算法
Comparator<Person> c = new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
// 按照hage进行排序
if (p1.age >= p2.age)
return 1; // 正数表示p1比hp2要大
else
return -1;
}
};
Collections.sort(persons, c);
System.out.println("按照年龄排序后的集合:");
System.out.println(persons);
}
}
class Person {
public String name;
public int age;
public String address;
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public String toString() {
return "\n" + "Name : " + name + "\tAge : " + age + "\tAddress : " + address;
}
}
执行结果:
[2]、方法二
使Person类实现Comparable接口
在类里面提供比较算法
Collections.sort就有足够的信息进行排序了,也无需额外提供比较器Comparator
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
public class ComparatorTest {
public static void main(String[] args) {
Random r = new Random();
List<Person> persons = new ArrayList<>();
for (int i = 0; i < 10; i++) {
persons.add(new Person("张" + i, r.nextInt(20) + 20, "西安"));
}
System.out.println("初始化后的集合:");
System.out.println(persons);
// 直接调用sort会出现编译错误,因为Person有各种属性
// 到底按照哪种属性进行比较,Collections也不知道,不确定,所以没法排
// Collections.sort(persons);
// 引入Comparator,指定比较的算法
Comparator<Person> c = new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
// 按照hage进行排序
if (p1.age >= p2.age)
return 1; // 正数表示p1比hp2要大
else
return -1;
}
};
//Person类实现了接口Comparable,即自带比较信息。
//Collections直接进行排序,无需额外的Comparator
Collections.sort(persons);
System.out.println("按照年龄排序后的集合:");
System.out.println(persons);
}
}
class Person implements Comparable<Person> {
public String name;
public int age;
public String address;
public Person(String name, int age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public String toString() {
return "\n" + "Name : " + name + "\tAge : " + age + "\tAddress : " + address;
}
@Override
public int compareTo(Person o) {
if (age > o.age) {
return 1;
}
return -1;
}
}
运行结果: