集合和它背后的数据结构:
集合 —— ArrayList
数据结构 —— 顺序表
一. ArrayList
ArrayList底层是一个顺序表(一段连续的空间,并且可以动态扩容)
(1) 实现RandomAccess接口:表示支持随机访问
(2)实现Cloneable接口:表示可以克隆
(3)实现java.io.Serializable接口:表示支持序列化
我们通过结合源码来学习ArrayList中的方法。
1. 调用构造方法创建ArrayList对象
有三种创建方法:
查看构造方法之前,我们先来了解一下ArrayList中的这些成员:
下面我们来看ArrayList的构造方法。
(1)首先,我们来看第一种无参构造方法:
DEFAULTCAPACITY_EMPTY_ELEMENTDATA是空数组,所以elementData引用了一个空数组。
我们之前可能听说过,调用无参构造方法,默认数组容量是10,其实是错的。通过上述分析,我们知道,调用无参构造方法,数组容量为0。(空数组,也会在堆上分配内存,但数组的长度为0)
咦?空数组,数组长度为0,那之后数据改如何放进去的呢?
那就需要说道说道add方法啦,因为其实是它起了作用。
我们来看一下add方法:
我们看一下 ensureCapacityInternal 方法:
ensureCapacityInternal 方法中调用了 ensureExplicitCapacity 方法,并将 calculateCapacity 方法的返回值作为参数。
grow方法,就是用来扩容的方法
直到这时,数组的容量才被扩容到10。 elementData.length = 10。
那么当数组放满了,我们又是怎么扩容的呢?
这时,size = 10,size+1 = 11
还是经过上述步骤, 只是minCapacity是11了。调用grow方法,我们发现是1.5倍扩容。
总结:
调用无参构造方法时,只有在第一次add的时候,数组的容量(elementData.length)才会被扩容成10。
接着放数据,数组放满了,会进行 1.5 倍的扩容。
(2) 接下来,我们来看有参构造方法。
构造具有指定容量的空列表。
直接对数组进行初始化,elementData 引用了 长度为 initialCapacity 的数组。非常完美,调用它,我们就构造了一个容量固定的数组。
当然,如果你给的容量是0,elementData引用的是 EMPTY_ELEMENTDATA 这个空数组。数组长度为0,之后数据如何放进去,也是当第一次add的时候,会调用grow方法,但它是扩容成长度为1的数组。继续放数据,是怎么扩容的参考grow源码,这里不做继续分析。但都大差不差。
这里我们就知道了DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和 EMPTY_ELEMENTDATA 这两个空数组的区别:前面是调用无参构造方法的时候会用到,后面是调用有参构造方法,传入容量的值为0使会用到。用到的地方不一样。
下一个:
ArrayList(Collection<? extends E> c)
需要满足两个条件:
(1)实现Collection接口的都可以传过来,如下红色框框里的都可以。
例子:
LinkedList类实现了Collection接口,所以可以传。
(2)通配符?可以接收的类型是E的子类或者E本身
例子:
报错原因:实参arrayList3的类型是ArrayList<String>,arrayList4的类型是ArrayList<Integer>,通配符?只能接收E的子类或E本身,而String不是Integer的本类或Integer本身。
2. ArrayList中的方法
(1)增
boolean add(E e) —— 尾插e
void add(int index, E element) —— 将element插入到index位置
boolean addAll(Collection<? extends E> c) —— 尾插c中的元素
boolean addAll(int index, Collection<? extends E> c) —— 将c中的元素插入到index位置
不需要用到返回值时,可以不用接收。
(2)删
boolean remove(Object o) —— 删除遇到的第一个o E remove(int index) —— 删除index位置的元素,返回:index位置的元素
boolean removeAll(Collection<?> c) —— 删除在ArrayList实例中出现的c中的元素
(3)查
boolean contains(Object o) —— 是否包含元素o
int indexOf(Object o) —— 返回第一个元素o的下标
int lastIndexOf(Object o) —— 返回最后一个元素o的下标
E get(int index) —— 获取index位置的元素
(4)改
E set(int index, E element) —— 将index位置的元素改为element
(5)其他方法:截取和清空
List<E> subList(int fromIndex, int toIndex) —— 在原ArrayList实例上截取,不会产生新的一份,左闭右开 void clear() —— 清空
3. ArrayList的遍历
1. for循环
2. foreach
3. 迭代器(Iterator)与 ListIterator
详解:见上一篇博客 迭代器(Iterator)与 ListIterator 的使用
boolean hasNext(); —— 判断是否有下一个元素
E next(); —— 返回下一个元素,并往后走一步
4. 题目
(1)给定两个字符串s1和s2,请删除s1中出现的s2中的字符,请使用集合来完成。
法一:不是s2的字符放到集合中
法二:都放集合,调用 removeAll 方法
5. ArrayList的具体使用
1. 简单的洗牌算法
三个同学想玩扑克牌。首先,他们得买一副扑克牌。然后,洗牌。接着,揭牌,每个人轮流揭5张牌。请用方法实现这三个过程。
Poker:
/*一张扑克牌*/
public class Poker {
private String suit;
private int rank;
public Poker(String suit, int rank) {
this.suit = suit;
this.rank = rank;
}
@Override
public String toString() {
return suit + rank;
}
}
Pokers:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/*一副扑克牌*/
public class Pokers {
public static final String[] suits = {"红桃","黑桃","梅花","方片"};
//买牌
public static List<Poker> buyPorkers(){
List<Poker> pokerList = new ArrayList<>();//放扑克的集合
for (int i = 0; i < 4; i++) {
String suit = suits[i];
for (int j = 1; j <= 13; j++) {
Poker poker = new Poker(suit,j);//new一张扑克牌
pokerList.add(poker);//把new的每一张扑克牌放进集合
}
}
return pokerList;//返回放了一副扑克牌的集合
}
//洗牌
public static void swap(List<Poker> pokerList,int i,int j){
Poker poker1 = pokerList.get(i);
Poker poker2 = pokerList.get(j);
pokerList.set(i,poker2);
pokerList.set(j,poker1);
}
public static void shuffle(List<Poker> pokerList){
//生成随机数
Random random = new Random();
int length = pokerList.size();
for (int i = length-1; i > 0 ; i--) {//从后往前取所有扑克牌(除第一张)
int index = random.nextInt(i);// 生成0-i之间的任意随机数 [0,i)
swap(pokerList,i,index);//交换
}
}
}
Test:
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
//买牌
List<Poker> pokerList = Pokers.buyPorkers();
System.out.println(pokerList);
//洗牌
Pokers.shuffle(pokerList);
System.out.println(pokerList);
//揭牌
//有三个同学,也就是有三只手需要揭牌,然后放到手中,每一只手就是一个放扑克牌的集合
List<Poker> hand1 = new ArrayList<>();
List<Poker> hand2 = new ArrayList<>();
List<Poker> hand3 = new ArrayList<>();
//放 手 的集合,集合的每个元素都是一只手
List<List<Poker>> hand = new ArrayList<>();
hand.add(hand1);
hand.add(hand2);
hand.add(hand3);
//揭 5 张牌
for (int i = 0; i < 5; i++) {
int size = hand.size();//手的个数,即玩牌的人数
for (int j = 0; j < size; j++) {
List<Poker> handTmp = hand.get(j);//来确定是哪个人的手
//pokerList.remove(0) 返回的是最上面那张牌
handTmp.add(pokerList.remove(0));//往这个人手里放牌
}
}
for (int i = 0; i < hand.size(); i++) {
System.out.println("第"+(i+1)+"的人手里的牌:"+hand.get(i));
}
System.out.println("剩余的牌:"+pokerList);
}
}
2. 杨辉三角
List<List<Integer>> —— List集合里面的元素类型是List<Integer> List<Integer> —— List集合里面的元素类型是Integer
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> list = new ArrayList<>();
List<Integer> firstRow = new ArrayList<>();
firstRow.add(1);
list.add(firstRow);
for (int i = 1; i < numRows; i++) {
List<Integer> row = new ArrayList<>();
row.add(1);
List<Integer> prevRow = list.get(i-1);
for (int j = 1; j < i; j++) {
int ret = prevRow.get(j)+prevRow.get(j-1);
row.add(ret);
}
row.add(1);
list.add(row);
}
return list;
}
6. ArrayList的优缺点:
优点:
适合查找给定下标对应的元素,时间复杂度可以达到O(1)
缺点:
(1)扩容可能会造成空间的浪费
(2)每次插入或删除元素,都要先向后或向前移动元素。最坏情况下,当插入或删除的是第一个元素时,时间复杂度可以达到O(n)
所以,ArrayList不适合频繁的插入和删除,适合给定下标查找。
二. Collections —— 用来操作集合的工具类
用的最多的是里面的 sort 方法(排序)
public static <T extends Comparable<? super T>> void sort(List<T> list) 这是一个静态的泛型方法 —— 用来对集合进行排序 参数 List<T> list ,即 要实现List接口 <T extends Comparable<? super T>> T是Comparable<? super T>的子类或本身, ? super T,?是T的父类或本身 例如: ArrayList<Student> arrayList = new ArrayList<>(); Collections.sort(arrayList); ArrayList实现了List接口 Student必须是Comparable<? super Student>的子类或本身,即Student必须实现Comparable<Student>接口