Java基础笔记(三)---集合框架(1)简单了解
(一)集合框架1-ArrayList
ArrayList和数组的区别
(1)使用数组的局限性
如果要存放多个对象,可以使用数组,但是数组有局限性,比如声明长度是10的数组,不用的数组就浪费了,超过10的个数,又放不下。这就是数组固定长度的局限性
(2)ArrayList存放对象
为了解决数组的局限性,引入容器类的概念。
容器的容量"capacity"会随着对象的增加,自动增长。
只需要不断往容器里增加英雄即可,不用担心会出现数组的边界问题。
ArrayList常用的方法
(1)增加
- 第一种是直接add对象:heros.add(new Hero("hero " + i));
- 第二种是在指定位置加对象:heros.add(3, specialHero);
(2)判断是否存在
- System.out.println(heros.contains(specialHero));
(3)获取
- heros.get(5)
(4)获取对象所处的位置indexOf
(5)删除
- heros.remove(2);
- 也可以根据对象删除:heros.remove(specialHero);
(6)替换set方法
(7)获取长度size方法
(8)转换为数组toArray
- Hero hs[] = (Hero[])heros.toArray(new Hero[]{});
(9)把另一个容器的所有对象都加进来addAll
- heros.addAll(anotherHeros);
List接口
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
(二)集合框架的遍历
for循环遍历
可以用size()和get()分别得到大小,和获取指定位置的元素,结合for循环就可以遍历出ArrayList的内容
大size的数据,千万不要使用普通for循环
for (int i = 0; i < heros.size(); i++) {
Hero h = heros.get(i);
System.out.println(h);
}
迭代器遍历
有两种,通过hasNext判断下一个是否为空,不空就取出
Iterator<Hero> it= heros.iterator();
//从最开始的位置判断"下一个"位置是否有数据
//如果有就通过next取出来,并且把指针向下移动
//直到"下一个"位置没有数据
while(it.hasNext()){
Hero h = it.next();
System.out.println(h);
}
//迭代器的for写法
for (Iterator<Hero> iterator = heros.iterator(); iterator.hasNext();) {
Hero hero = (Hero) iterator.next();
System.out.println(hero);
}
增强型for循环遍历
使用增强型for循环可以非常方便的遍历ArrayList中的元素,这是很多开发人员的首选。
不过增强型for循环也有不足:无法用来进行ArrayList的初始化。无法得知当前是第几个元素了,当需要只打印单数元素的时候,就做不到了。
for (Hero h : heros) {
System.out.println(h);
}
(三)集合框架2-LinkedList
LinkedList又分为两种:队列Queue、栈Stack
(1)和ArrayList相同之处
与ArrayList一样,LinkedList也实现了List接口,诸如add,remove,contains等等方法
(2)和ArrayList不同之处
与数组结构相比较,数组结构,就好像是电影院,每个位置都有标示,每个位置之间的间隔都是一样的。 而链表就相当于佛珠,每个珠子,只连接前一个和后一个,不用关心除此之外的其他佛珠在哪里
双向链表Deque
除了实现了List接口外,LinkedList还实现了双向链表结构Deque,可以很方便的在头尾插入删除数据
新加的方法如下:addFirst、getFirst、addLast、getLast、removeFirst、removeLast。
队列Queue(先进先出)
LinkedList 除了实现了List和Deque外,还实现了Queue接口(队列)
新增的方法如下:
- offer:加在队列的最后面
- poll:取出第一个Hero,FIFO 先进先出
- peek:第一个拿出来看一看,但是不取出来
栈Stack(先进后出)
继承Vector类
新增方法如下:
- push
- pop
- peek
- empty
- search
(四)集合框架3-二叉树
二叉树由各种节点组成,每个节点都可以有左子节点,右子节点;每一个节点都有一个值
节点Node是一个接口
public class Node {
// 左子节点
public Node leftNode;
// 右子节点
public Node rightNode;
// 值
public Object value;
}
构造二叉树
左小右大,相同的也放发左边
public class Node {
// 左子节点
public Node leftNode;
// 右子节点
public Node rightNode;
// 值
public Object value;
// 插入 数据
public void add(Object v) {
// 如果当前节点没有值,就把数据放在当前节点上
if (null == value)
value = v;
// 如果当前节点有值,就进行判断,新增的值与当前值的大小关系
else {
// 新增的值,比当前值小或者相同
if ((Integer) v -((Integer)value) <= 0) {
if (null == leftNode)
leftNode = new Node();
leftNode.add(v);
}
// 新增的值,比当前值大
else {
if (null == rightNode)
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);
}
}
}
二叉树遍历
实际上,二叉树数据已经排好序了。 接下来要做的是看,把这些已经排好序的数据,遍历成我们常用的List或者数组的形式
二叉树的遍历分左序,中序,右序:
左序即: 中间的数遍历后放在左边
中序即: 中间的数遍历后放在中间
右序即: 中间的数遍历后放在右边
(五)集合框架4-HashMap
HashMap储存数据的方式是—— 键值对。key是唯一的,不可以重复的。所以,以相同的key 把不同的value插入到 Map中会导致旧元素被覆盖,只留下最后插入的元素。
(六)集合框架5-HashSet
Set中的元素,不能重复
Set中的元素,没有顺序
Set不提供get()来获取指定位置的元素,所以遍历需要用到迭代器,或者增强型for循环
HashSet自身并没有独立的实现,而是在里面封装了一个Map。HashSet是作为Map的key而存在的,而value是一个命名为PRESENT的static的Object对象,因为是一个类属性,所以只会有一个。
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();
}
}
(七)Collection接口
Collection是 Set List Queue和 Deque的接口
Collection和Map之间没有关系,Collection是放一个一个对象的,Map 是放键值对的
Deque 继承 Queue,间接的继承了 Collection
(八)Collections类
Collections是一个类,容器的工具类,就如同Arrays是数组的工具类
- reverse 使List中的数据发生翻转
- shuffle 混淆List中数据的顺序
- sort 对List中的数据进行排序
- swap 交换两个数据的位置
- rotate 把List中的数据,向右滚动指定单位的长度
- synchronizedList 把非线程安全的List转换为线程安全的List。 因为截至目前为止,还没有学习线程安全的内容,暂时不展开。 线程安全的内容将在多线程章节展开。
(九)框架之间的区别
(1)List,Set,Map三者的区别
- List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象。
- Set(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。
- Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
(2)ArrayList和HashSet的关系
(1)是否有顺序
ArrayList: 有顺序
HashSet: 无顺序
(2)能否重复
List中的数据可以重复
Set中的数据不能够重复
重复判断标准是:首先看hashcode是否相同,如果hashcode不同,则认为是不同数据,如果hashcode相同,再比较equals,如果equals相同,则是相同数据,否则是不同数据
(3)ArrayList和LinkedList的关系
主要区别:
- 1)是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
- 2)底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
- 3)插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以对于add(�E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。
ArrayList 插入,删除数据慢,但是查找数据很快,因为是顺序结构。就像电影院位置一样,有了电影票,一下就找到位置了。
LinkedList插入,删除数据快,但是查找数据很慢,因为是链表结构。就像手里的一串佛珠,要找出第99个佛珠,必须得一个一个的数过去。
- 4)是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index) 方法)。
- 5)内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
(4)HashMap和HashTable的关系
HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式
- 1)线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
HashMap不是线程安全的类
Hashtable是线程安全的类
- 2)效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
- 3)对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
HashMap可以存放 null
Hashtable不能存放null
- 4)初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
- 5)底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
HashMap 中带有初始容量的构造函数:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
(5)HashSet和HashMap的关系
HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone() 、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
HashSet自身并没有独立的实现,而是在里面封装了一个Map。HashSet是作为Map的key而存在的,而value是一个命名为PRESENT的static的Object对象,因为是一个类属性,所以只会有一个。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();
public HashSet() {
this.map = new HashMap();
}
public HashSet(Collection<? extends E> var1) {
this.map = new HashMap(Math.max((int)((float)var1.size() / 0.75F) + 1, 16));
this.addAll(var1);
}
public HashSet(int var1, float var2) {
this.map = new HashMap(var1, var2);
}
public HashSet(int var1) {
this.map = new HashMap(var1);
}
HashSet(int var1, float var2, boolean var3) {
this.map = new LinkedHashMap(var1, var2);
}
public Iterator<E> iterator() {
return this.map.keySet().iterator();
}
(6)HashSet如何检查重复
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
- 1)hashCode()与equals()的相关规定:
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个equals方法返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
- 2)==与equals的区别
- ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
- ==是指对内存地址进行比较 equals()是对字符串的内容进行比较
- ==指引用是否相同 equals()指的是值是否相同