集合框架
Collection概念
对象的容器,定义了多个对多个对象的操作方法,可以实现数组的功能
和数组的区别
使用上来讲
- 数组长度固定,集合长度不固定
- 数组可以存储基本类型,集合只能储存引用类型(当存储基础类型的时候要进行装箱操作)
Collection体系集合
比较重要的是ArrayList和Linkedlist
父接口Collection的应用
方法
Iterator iterator();// 返回在这个集合上迭代的迭代器
boolean add(Object obj) ;// 添加对象
boolean addAll(Collection c);// 将c集合中所有的对象添加进集合
void clear();// 清空此集合
boolean contains(Object o);// o是否存在
boolean equals(Object o);// 比较此集合是否和对象o相等
boolean isEmpty();// 判断集合是否为空
boolean remove(Object o);// 移除某对象
boolean removeAll(Collection c);
// 两个集合取交集,移除交集中包含的所有元素;FE:a={1,2,3,4};c={2,3,5};a.removeAll(c);a==>{1,4}
boolean retainAll(Collection c);
// 两个集合取交集,仅保留集合中包含的所有元素;;FE:a={1,2,3,4};c={2,3,5};a.removeAll(c);a==>{2,3,4}
int sice();// 返回此集合中的元素个数
Object[] toArray();// 将此集合转换成数组
实际应用
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class Demo1 {
/**
* Collection的基本使用
* (1) 添加删除元素
* (2) 遍历元素
* (3) 判断
*/
public static void main(String[] args) {
// 创建对象,使用多态,父类引用指向子类对象
Collection collection = new ArrayList();
// 添加元素
collection.add("苹果");
collection.add("西瓜");
collection.add("榴莲");
collection.add("草莓");
System.out.println(collection.size());
System.out.println(collection);
System.out.println("-------");
// 删除元素
collection.remove("榴莲");
System.out.println("after delete");
System.out.println(collection.size());
System.out.println(collection);
System.out.println("----");
// 遍历元素
// 1).增强for for(Object obj : Collection){}
for (Object str :
collection) {
System.out.println(str);
}
System.out.println("----");
// 2).使用迭代器
Iterator iterator = collection.iterator();
while (iterator.hasNext()) {
String next = (String) iterator.next();
System.out.println(next);
}
// 进行判断
// 是否为空
collection.isEmpty();
// 是否存在"西瓜"
collection.contains("西瓜");
}
}
注意
在迭代过程中不允许使用collection中的remove();,否则将会报错,实例
// 获取迭代器
Iterator iterator = collection.iterator();
// 进行遍历
while (iterator.hasNext()) {
String next = (String) iterator.next();
System.out.println(next);
// 删除某元素
// collection.remove("xxx");
// 会出现并发修改异常ConcurrentModificationException
// 只能使用迭代器中的删除
if ("苹果".equals(next)) {
iterator.remove();// remove(); 不需要添加删除的对象, 直接删除迭代到的对象
}
}
List子接口
特点
相对于Collection
// 特点
// 1. 有序 2. 有下标 3. 可重复
特有方法
boolean add(Object obj);
// 从collection继承
boolean add(int index, Object obj);
// 因为list是带下标的,所以可以对他进行
Iterator iterator();
// 从collection继承
ListIterator listIterator();
// 返回一个数组数组迭代器
// 数组迭代器相对于之前的迭代器增加了可以往前迭代也可以往后迭代,并且可以在迭代过程中进行修改删除增加
面试题
List<Integer> list = new ArrayList<>();
list.add(20);
list.add(30);
list.remove(20);
// 这样做是删除索引为20的对象, 集合里存储的是对象的引用
// 会报异常 IndexOutOfBoundsException(索引超出异常)
// remove(int index);
list.remove((Object) 20);
list.remove(new Integer(20));
实现类
Arraylist😝
存储结构是数组, 查询遍历快, 增删改慢
Java1.2版本出现, 执行快, 线程不安全
源码分析:
必记的属性
属性名称 | 说明 |
---|---|
DEFAULT_CAPACITY = 10 | 默认容量 |
elementData[] | 存放数据的数组 |
size | 数组的长度 |
当集合中没有添加任何元素的时候, 数组的长度是0
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
add()
// ArrayList.java
public boolean add(E e) {
ensureCapacityInternal(size + 1);
// 确保内部容量足够, 将size+1和这个数组的容量进行相比较
// 如果这个数组是个新数组, 则赋值为默认容量 DEFAULT_CAPACITY=10
elementData[size++] = e;
// 将e对象的引用以size为角标添加进数组, 也就是紧挨着最后一个数组元素
// 如果是个新数组的话, 初始为0, 就是0号元素是e, 并将size自增1, 以便下个元素添加进1号位置
return true;
}
private void ensureCapacityInternal(int minCapacity) {// minCapacity = size+1
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));// 确保明确的容量
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {// 计算容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// 如果这是个空数组的话, 将计算这个数组的大小, 返回minCapacity与DEFAULT_CAPACITY(10)中比较大的数
return Math.max(DEFAULT_CAPACITY, minCapacity);
// 空数组显然返回10
}
// 如果不是空数组的话, 直接返回minCapacity
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {// 这个参数就是刚刚返回的minCapacity, 显然空数组为10
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)// 如果minCapacity大于数组的长度, 则进行保存
grow(minCapacity);// grow: 生长,增长
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 引入oldCapacity = 数组的长度 ==> 0
// 如果不是一个新数组 , oldCapacity = elementData.length
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 引入newCapacity = oldCapacity + oldCapacity/2 ==> 每次增长二分之一
if (newCapacity - minCapacity < 0) // 显然, newCapacity小于minCapacity
newCapacity = minCapacity; // 将newCapacity赋值为10
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 如果这个数字大于Integer.max-8 , 则将
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
// 通过arrays的copyOf()进行copy, 将数组扩充为10个大小
}
//
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
问题1 使用new的方式,将集合中的元素删除
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(new Student("aohan_1", 12));
arrayList.add(new Student("aohan_2", 12));
arrayList.remove(new Student("aohan_1", 12));
怎么将新new的对象进行删除/查找
查看remove()
源码 // 其他也是调用的equals方法进行比较,然后返回
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
发现remove是调用equals来删除的
public boolean equals(Object obj) {
return (this == obj);
我们只需要重写student中的equals就可实现对比
@Override
public boolean equals(Object obj) {
//判断是否相同
if (this == obj)
return true;
// 判断是否为空
if (obj == null)
return false;
// 如果这个对象是student则进行这么判断
if (obj instanceof Student) {
Student student = (Student) obj;
if (student.getName().equals(this.getName())
&& (student.getAge() == this.getAge()))
return true;
}
// 都不满足返回false
return false;
}
Vector
平常开发使用比较少
查询快, 增删慢
Java1.0版本出现, 执行稍慢, 线程安全
Linkedlist
链表来实现, 增删快, 查询慢
存储结构: 双向链表
源码分析
// LinkedList.java
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
// 新创建l 指向了 last(linkedList的最后一个节点)
final Node<E> l = last;
// 创建要添加的节点对象 , 并将 prev(前指针) 指向 l ,存储数据内容 , next(后指针)为null
final Node<E> newNode = new Node<>(l, e, null);
// 将last指向 newNode节点 , 即最后一个节点是newNode(新添加的节点)
last = newNode;
// 此时判断前节点是否为空, 即判断前面有没有节点
if (l == null)
// 如果前节点对象 l 为空 , 则说明前面没有节点了, 这个节点就是第一个节点 , 则first也指向 newNode
first = newNode;
// 前指针不为空, 则前节点的下一个节点指向 newNode(新添加的这个节点)
else
l.next = newNode;
size++; // 添加成功后size自增
modCount++; // 修改此处+1
}
private static class Node<E> {
E item;// 要存储的数据
Node<E> next; // 下一个节点
Node<E> prev; // 上一个节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
泛型<T,…>
泛型概述
Java泛型是jdk1.5引入的一个新特性, 其本质就是参数化类型, 把类型作为参数传递
常见的形式有泛型类, 泛型接口, 泛型方法
好处: 提高代码的重用性 , 防止类型转换异常, 提高代码安全性
语法: <T,…> T称为占位符, 一般使用T, E, K, V (type, element, key, value)
泛型集合,
强制集合元素类型必需统一
好处是可以避免进行遍历的时候, 对每个对象进行强转, 也避免对每个类型转化的时候, 出现ClassCastException ( 类型转换异常 )
注意
不同泛型的集合不能相互转换
Set
无序, 无下标, 元素不可重复
方法完全继承自Collection
HashSet😝
基于HashCode实现元素不重复
当存入元素的hashCode相同的时候, 会调用equals进行确认, 如果结果为true, 则拒绝后者存入
存储过程
- 根据hashCode计算该对象存储的位置, 如果此位置为空, 则直接保存, 如果不为空则执行第二步
- 执行equals方法, 如果equals方法为true, 则认为是重复, 拒绝存入, 如果false, 则形成链表
源码
// 无参构造函数
// 实际上就是创建了一个HashMap
public HashSet() {
map = new HashMap<>();
}
// add方法
// 实际上是使用了hashmap中的key来存储数据, 存入的value就是new Object()
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// PRESENT
private static final Object PRESENT = new Object();
那为什么底层使用object作为value呢, 而不是使用null, 如果使用null岂不是更节省内存
查看源码
// HashSet.java
private static final Object PRESENT = new Object();
// HashSet.java
public boolean remove(Object o) {
return map.remove(o)==PRESENT; // present: 此时,当前 PRESENT=new Object();
// 如果存入的值为null, 则无论如何map.remove(o)==PRESENT都为false,
}
--------------------------------------------------
// HashMap.java
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
// removeNode(); hashmap中判断是否移出了元素
// 如果存在这个元素则remove, 返回这个value, 不存在就返回null
}
怎么实现只要是相同的对象(new), 就不可重复呢
查看add();
源码
发现都是先调用hashCode()计算hash值, 如果hash值相同, 就去对比equals()
所以只需要保证相同的对象hash值相同, 和equals()为true就行了
我们只需要重写person中的hashCode(), equals() 就可实现
// 重写hashCode() , 以保证每个对象, 只要属性相同, 计算出来的hash值就相同
@Override
public int hashCode() {
int i = this.getName().hashCode();
int i1 = this.getAge().hashCode();
return i*2 + i1*58;
}
// 重写equals(), 避免hash值计算的出现特殊情况, 当hash值相同对象却不一样的情况
// 只需保证对象属性相同则返回true
@Override
public boolean equals(Object obj) {
// 如果是相同的对象 , 则不需要进行对比 , 直接返回true
if (this == obj) {
return true;
}
// 如果对象为null , 则直接返回false
if (this == null) {
return false;
}
// 如果前两个条件都不满足, 并且对象是Person对象 , 则进行属性对比 , 相同则返回true
if (obj instanceof Person) {
Person person = (Person) obj;
if (this.getName().equals(person.getName()) && this.getAge().equals(person.getAge())) {
return true;
}
}
// 都不满足, 返回false, 并且将对象添加进去
return false;
}
TreeSet😝
基于排列顺序实现元素不重复
内部实现了 SortedSet 接口, 对集合元素进行自动排序
元素对象类型必需实现comparable接口, 指定排序规则
或者在创建TreeSet的时候, 就创建comparator匿名内部类, 为之制定排序规则
TreeSet<Person> personTreeSet = new TreeSet<Person>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
int n1 = o1.getAge() - o2.getAge();
int n2 = o1.getName().compareTo(o2.getName());
return n1 == 0 ? n2 : n1;
}
});
通过实现CompareTo方法确定是否为重复元素
源码
// TreeSet.java
/**
* The backing map. backing: 背后的,支持的
*/
private transient NavigableMap<E,Object> m;
// Navigable: 可通行的 navigate:导航
// 无参构造
public TreeSet() {
this(new TreeMap<E,Object>());
// 调用有参构造, new一个TreeMap传入
}
// SortedMap--NavigableMap--TreeMap
// 底层使用TreeMap
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
// 添加也是调用TreeMap的put方法, 将数据存入key中, value是存入PRESENT对象``new Object()``
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
案例: 实现字符串以长度来排序
// 创建set
TreeSet<String> strings = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
int n1 = o1.length() - o2.length();
int n2 = o1.compareTo(o2);
return n1 == 0 ? n2 : n1;
}
});
Map
特点: 存储一对数据 ( key - value ) , 无序 , 无下标 , 键不可重复 , 值可以重复
子类有HashMap HashTable
方法:
V put(K key , V value);
Object get(Object key);
Set<K> keySet();
Set<Map.Entry<K,V>> entrySet();
// 获取所有键值对的Map.Entry对象 这是个对象中包含了键和值, 是一个匿名内部类, 只能通过Map.Entry调用, 也是遍历效率最高的
HashMap😝
jdk1.2, 线程不安全, 运行效率快, 允许使用null作为key/value
属性
属性名称 | 意义 |
---|---|
DEFAULT_INITIAL_CAPACITY = 1 << 4 (16) | 初始的容量16 |
MAXIMUM_CAPACITY = 1 << 30 | 最大的容量是2的30次方 |
DEFAULT_LOAD_FACTOR = 0.75f | 默认的扩用因子是0.75, 意思是说当到达容量的75%时, 将会触发扩容 |
TREEIFY_THRESHOLD = 8 | 因为在jdk8加入了红黑树, 所以加入的此属性, 当数组的长度>=64, 并且上面的链表长度超过了8, 则会由链表结构转换成树, 这样查询速度将会大大增加 |
UNTREEIFY_THRESHOLD = 6 | 当转换树之后, 如果树的节点小于6, 则有会转换成链表结构 |
MIN_TREEIFY_CAPACITY = 64 | 定义默认的转换成树的数组的长度 |
Node<K,V>[] table | 这是hash表中的数组 |
size | 元素的个数 |
总结
1. hashmap刚创建时, table=null;size=0;为了节省空间, 当添加了第一个元素的时候, table容量调整为16
2. 当元素大于阈值(16*0.75=12)时, 会进行扩容, 扩容后大小为原来的两倍. 目的是减少调整元素的个数
3. jdk1.8 当每个链表长度大于8, 并且数组元素个数大于等于64时, 会调整为红黑树, 目的是为了提高执行效率
4. jdk1.8 当链表长度小于6, 自动调整成为链表
5. jdk1.8之前, 链表是头插入, jdk1.8之后, 改成了尾插入
构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 默认增长因子是0.75, 未对其他属性进行设置
//除去其他已经被设置的属性, Node<k,v>[]=null, size=0, 目的就是为了节省空间
// all other fields defaulted
}
put();
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
TreeMap😝
实现了SortedMap接口, 可以对key进行排序
HashTable
jdk1.0, 线程安全, 运行效率慢, 不允许null作为key/value
Properties
HashTable的子类, 要求key和value都是String, 通常用于配置文件的读取
properties类表示了一个属性集, 可以保存在流中, 属性列表中key和value都应该是String
一个属性列表中可能包含另外一个属性列表作为他的属性值
工具类
Collections
方法
void reverse(List<?> list); // 反转集合中的元素
void shuffle(List<?> list); // 随机重置集合元素的顺序
void sort(List<T> list); // 升序排序, 元素类型必需实现Comparable接口
int binarySearch(List<T> list, T t);// 二分查找
// destination: 目的地 source: 源头
void copy(List<? super T> dest, List<? extends T> src);
// 复制集合, 但是两个集合的大小应该相同, 否则将会报IndexOutOfBoundsException(索引越界异常)
// bound: 跳跃
Arrays(操作数组的工具类)
// 把数组转换为list集合
Arrays.asList(Object[] arr);
// 将数组转换成为字符串
Arrays.toString(Object[] arr);
asList();
// 1. 因为数组一旦创建之后, 不能进行增删, 所以在jdk设计的时候, 转换成集合的时候就不能修改, 把obj数组转换成集合以后, 这个集合是一个受限的集合, 不能进行添加和删除, 否则会抛出异常unsupportedOperationException
String[] names = {"zhangsan","lisi","wangwu"};
List<String> strings = Arrays.asList(names);
strings.remove("zhangsan");
// Exception in thread "main" java.lang.UnsupportedOperationException
// 2. 如果将基本类型数据数组转换成集合, 则需要在创建数组的时候, 使用包装类型, 如int[] arr: 应该使用Integer[] arr, 否则在转换数据的时候, 会将基本类型数组存储成List<int[]> list
int[] arr = {100,200};
List<int[]> ints = Arrays.asList(arr);
// ints.toString() = [[I@28d93b30]
Integer[] arr2 = {100,200};
List<Integer> integers = Arrays.asList(arr2);
// integers.toString() = [100, 200]