数组:是一个容器,可以存放相同类型的多个数据元素。
数组的局限性:
集合:是java中提供的一种容器,可以用来存放多个数据。
HashSet
底层数据结构是哈希表。(无序,唯一)
如何来保证元素唯一性?
1.依赖两个方法:hashCode()和equals()
TreeSet
底层数据结构是红黑树。(唯一,有序)
LinkedHashSet
底层数据结构是链表和哈希表。(FIFO插入有序,唯一)
1.由链表保证元素有序
2.由哈希表保证元素唯一
如何保证元素排序的呢?
自然排序
比较器排序
如何保证元素唯一性的呢?
根据比较的返回值是否是0来决定
public boolean add(E e)
:把给定的对象添加到当前集合中。public void clear()
:清空集合中的所有元素。public boolean remove(E e)
:把给定的对象从当前集合中删除。public boolean contains(E e)
:判断当前集合中是否包含给定的对象public boolean isEmpty()
:判断当前集合是否为空。public int size()
:返回集合中元素的个数。public Object[] toArray()
:把集合中的元素存放到Object数组中boolean addAll(Collection extends E> c)
:添加一个集合到当前集合。boolean removeAll(Collection> c)
:移除一个子集合。boolean retainAll(Collection> c)
:取当前集合和传入集合的交集给当前集合,如果当前集合的长度改变则返回true。boolean containsAll(collection> c)
:判断当前集合是否包含指定集合中所有元素用集合提供的toArray()将集合转换成object数组。使用数组的遍历方式遍历
// 1、创建一个集合对象
Collection collection = new ArrayList();
collection.add("hello");
collection.add("world");
// 2、将集合转换为数组
Object[] array = collection.toArray();
// 3、数组遍历
// 用遍历数组的方式遍历
for(int i = 0;i<array.length;i++){
//array[i]就是集合的元素
System.out.println(array[i]);
}
迭代器接口定义了几个方法,最常用的是以下三个:
获取一个迭代器
// 1、创建一个集合对象
Collection collection = new ArrayList();
collection.add("hello");
collection.add("world");
// 2、通过集合的方法获取一个迭代器
Iterator it = collection.iterator();
迭代器遍历元素
while(it.hasNext()) {
System.out.println(it.next());
}
语法:
for(数据类型 变量名 : 集合名){
// 将集合中的元素取出来作为变量去操作
}
// 1、创建一个集合对象
Collection collection = new ArrayList();
collection.add("hello");
collection.add("world");
// 2、使用增强for遍历集合
for(Object obj : collection){
System.out.println(obj);
}
注意:如果集合为空将会报异常:nullpointexception空指针异常。
void add(E e)
:向list末尾添加一个元素void add(int index,E e)
:在list中的指定下标添加一个元素Object get(int index)
:获取list中指定下标的元素ListIterator listIterator()
:返回列表中的列表迭代器(按适当的顺序)。迭代器中的方法:
boolean hasNext()
:返回true
如果遍历正向列表,列表迭代器有多个元素。E next()
:返回列表中的下一个元素,并且前进光标位置。
boolean hasPrevious()
:返回true
如果遍历反向列表,列表迭代器有多个元素。E previous()
:返回列表中的上一个元素,并向后移动光标位置。int nextIndex()
:返回由后续调用返回的元素的索引next()
。int previousIndex()
:返回由后续调用previous()
返回的元素的索引。void remove()
:从列表中删除由next()
或previous()
返回的最后一个元素(可选操作)。void set(E e)
:用指定的元素(可选操作)替换由next()
返回的最后一个元素或previous()
。void add(E e)
:将指定的元素插入列表(可选操作)。 该元素将被返回的元素之前立即插入next()
,如果有的话,那会被返回的元素之后previous()
,如果有的话。ListIterator迭代器不但可以正向迭代还可以反向迭代和增删
注意如果在操作list时出现//ConcurrentModificationException
(并发修改异常)
ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。
ArrayList 继承了 AbstractList ,并实现了 List 接口。
四种遍历
ArrayList list = new ArrayList();
list.add("hello");
list.add("world");
list.add("java");
//数组
Object[] arr = list.toArray();
for(int i=0; i<arr.length;i++){
String s = (String)arr[i];
System.out.println(arr[i]);
}
//迭代器
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
//列表迭代器
ListIterator listIterator = list.listIterator();
while (listIterator.hasNext()){
System.out.println(listIterator.next());
}
//size get()
for(int i=0; i<list.size();i++){
String s = (String)list.get(i);
System.out.println(arr[i]);
}
方法 | 描述 |
---|---|
add(E e) | 将元素插入到指定位置的 arraylist 中 |
addAll(Collection> c) | 添加集合中的所有元素到 arraylist 中 |
contains(E e) | 判断元素是否在 arraylist |
get(int index) | 通过索引值获取 arraylist 中的元素 |
indexOf(E e) | 返回 arraylist 中元素的索引值 |
removeAll(Collection> c) | 删除存在于指定集合中的 arraylist 里的所有元素 |
remove(E e) | 删除 arraylist 里的单个元素 |
size() | 返回 arraylist 里元素数量 |
isEmpty() | 判断 arraylist 是否为空 |
subList(int fromindex,int endindex) | 截取部分 arraylist 的元素 |
set(int index,E e) | 替换 arraylist 中指定索引的元素 |
sort() | 对 arraylist 元素进行排序 |
toArray() | 将 arraylist 转换为数组 |
lastIndexOf(E e) | 返回指定元素在 arraylist 中最后一次出现的位置 |
containsAll(Collection> c) | 查看 arraylist 是否包含指定集合中的所有元素 |
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。
以下情况使用 ArrayList :
以下情况使用 LinkedList :
方法 | 描述 |
---|---|
public boolean add(E e) | 链表末尾添加元素,返回是否成功,成功为 true,失败为 false。 |
public void add(int index, E element) | 向指定位置插入元素。 |
public boolean addAll(Collection c) | 将一个集合的所有元素添加到链表后面,返回是否成功,成功为 true,失败为 false。 |
public boolean addAll(int index, Collection c) | 将一个集合的所有元素添加到链表的指定位置后面,返回是否成功,成功为 true,失败为 false。 |
public void addFirst(E e) | 元素添加到头部。 |
public void addLast(E e) | 元素添加到尾部。 |
public boolean offerFirst(E e) | 头部插入元素,返回是否成功,成功为 true,失败为 false。 |
public boolean offerLast(E e) | 尾部插入元素,返回是否成功,成功为 true,失败为 false。 |
public void clear() | 清空链表。 |
public E removeFirst() | 删除并返回第一个元素。 |
public E removeLast() | 删除并返回最后一个元素。 |
public boolean remove(Object o) | 删除某一元素,返回是否成功,成功为 true,失败为 false。 |
public E remove(int index) | 删除指定位置的元素。 |
public boolean contains(Object o) | 判断是否含有某一元素。 |
public E get(int index) | 返回指定位置的元素。 |
public E getFirst() | 返回第一个元素。 |
public E getLast() | 返回最后一个元素。 |
public int indexOf(Object o) | 查找指定元素从前往后第一次出现的索引。 |
public int lastIndexOf(Object o) | 查找指定元素最后一次出现的索引。 |
public E set(int index, E element) | 设置指定位置的元素。 |
public Object clone() | 克隆该列表。 |
public Iterator descendingIterator() | 返回倒序迭代器。 |
public int size() | 返回链表元素个数。 |
public ListIterator listIterator(int index) | 返回从指定位置开始到末尾的迭代器。 |
public Object[] toArray() | 返回一个由链表元素组成的数组。 |
public T[] toArray(T[] a) | 返回一个由链表元素转换类型而成的数组。 |
Vector 类实现了一个动态数组。和 ArrayList 很相似,但是两者是不同的:
Vector 主要用在事先不知道数组的大小,或者只是需要一个可以改变大小的数组的情况。
第一种构造方法创建一个默认的向量,默认大小为 10:
Vector()
第二种构造方法创建指定大小的向量。
Vector(int size)
第三种构造方法创建指定大小的向量,并且增量用 incr 指定。增量表示向量每次增加的元素数目。
Vector(int size,int incr)
第四种构造方法创建一个包含集合 c 元素的向量:
Vector(Collection c)
方法 | 描述 |
---|---|
addElement(E obj) | 将指定的组件添加到此向量的末尾,将其大小增加1。 |
elementAt(int index) | 返回指定索引处的组件。 |
elements() | 返回此向量的组件的枚举。 |
firstElement() | 返回此向量的第一个组件(索引号为 0的项目)。 |
Arrays.asList(),此方法可以将数组转集合,但本质还是数组,所以不能操作集合改变数组的大小的方法。
List<String> list = Arrays.asList("hello","world","java");
// 不能使用add(),remove(),clear()等方法去改变集合长度
java.lang.UnsupportedOperationException
// 可以修改元素的内容使用set()方法
set接口是继承自Collection的子接口,特点是元素不重复,存储无序。
与List接口不同,Set集合不能包含重复的元素。
set保证里面元素的唯一性其实是靠两个方法,**equals()
和hashCode()
**方法
往set里面添加数据的时候一般会有隐式的操作
HashSet 的底层是HashMap, hashMap的底层是哈希表(数组和链表的结合)
什么是哈希表呢?
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。
因为Set接口的实现类HashSet中,判断重复是通过hashCode(),和equals()方法来实现的。
所以自定义对象想要在用Set去重时需要重写hashCode和equals方法(idea编译器的快捷键有对自定义类的hashcode和equals方法的生成方法)
HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的,无序的集合。
HashSet 允许有 null 值。
HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。
在Java中,如果我们必须随机访问元素,则通常使用HashSet。 这是因为哈希表中的元素是使用哈希码访问的。
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。是无序的。
HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。
HashMap的数据结构为 数组+链表
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
在HashMap底层使用数组加链表的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。
底层数据结构:哈希表+链表
保证了唯一性,
链表保存有序(存储和取出是一致)
LinkedHashSet<String> hs = new LinkedHashSet<String>();
hs.add("hello");
hs.add("world");
hs.add("java");
hs.add("hello");
for(String str :hs){
System.out.println(str);
}
Java集合框架的TreeSet类提供树数据结构的功能。
特点:排序 和唯一
排序 :
// 这里介绍自然排序
public static void main(String[] args) {
TreeSet<Integer> treeSet = new TreeSet<Integer>();
treeSet.add(66);
treeSet.add(18);
treeSet.add(12);
treeSet.add(66);
treeSet.add(77);
for(Integer integer: treeSet){
System.out.print(integer);
}
}
// 输出:12 18 66 77
现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象,即java.util.Map
接口。
我们通过查看Map
接口描述,发现Map
接口下的集合与Collection
接口下的集合,它们存储数据的形式不同,如下图。
Collection
中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。Map
中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。Collection
中的集合称为单列集合,Map
中的集合称为双列集合。Map
中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。public V put(K key, V value)
: 把指定的键与指定的值添加到Map集合中。public V remove(Object key)
: 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。public V get(Object key)
根据指定的键,在Map集合中获取对应的值。boolean containsKey(Object key)
判断集合中是否包含指定的键。public Set keySet()
: 获取Map集合中所有的键,存储到Set集合中。public Set> entrySet()
: 获取到Map集合中所有的键值对对象的集合(Set集合)。方法 | 描述 |
---|---|
clear() | 删除 hashMap 中的所有键/值对 |
clone() | 复制一份 hashMap |
isEmpty() | 判断 hashMap 是否为空 |
size() | 计算 hashMap 中键/值对的数量 |
put() | 将键/值对添加到 hashMap 中 |
putAll() | 将所有键/值对添加到 hashMap 中 |
putIfAbsent() | 如果 hashMap 中不存在指定的键,则将指定的键/值对插入到 hashMap 中。 |
remove() | 删除 hashMap 中指定键 key 的映射关系 |
containsKey() | 检查 hashMap 中是否存在指定的 key 对应的映射关系。 |
containsValue() | 检查 hashMap 中是否存在指定的 value 对应的映射关系。 |
replace() | 替换 hashMap 中是指定的 key 对应的 value。 |
replaceAll() | 将 hashMap 中的所有映射关系替换成给定的函数所执行的结果。 |
get() | 获取指定 key 对应对 value |
forEach() | 对 hashMap 中的每个映射执行指定的操作。 |
entrySet() | 返回 hashMap 中所有映射项的集合集合视图。 |
keySet() | 返回 hashMap 中所有 key 组成的集合视图。 |
values() | 返回 hashMap 中存在的所有 value 值。 |
List list = new ArrayList(); 不能使用ArrayList特有的方法和属性
这句创建了一个 ArrayList 的对象后把上溯到了 List。此时它是一个List对象了,有些ArrayList 有但是 List 没有的属性和方法,它就不能再用了。
ArrayList list=new ArrayList();需要用到 ArrayList 独有的方法的时候使用
ArrayList创建对象则保留了ArrayList 的所有属性。
实例代码如下:
List list = new ArrayList();
ArrayList arrayList = new ArrayList();
list.trimToSize(); //错误,没有该方法。
arrayList.trimToSize(); //ArrayList里有该方法。
(1)带初始容量参数的构造函数,用户可以自己定义容量
transient Object[] elementData;
private static final Object[] EMPTY_ELEMENTDATA = {};
// 在创建ArrayList时需要传递初始化容量的构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
(2)默认构造函数,使用初始容量10构造一个空列表(无参数构造)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
(3)构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
首先获取数组的旧容量,然后计算新容量的值,计算使用位运算,将其扩容至原来的1.5倍。
得到新容量的值后,校验扩容后的容量是否大于需要的容量。如果不够,则把最小需要容量当作扩容后的新容量。并确保扩容后的容量不超过数组能设置的最大大小值。
最后将老数组的数据复制到新的数组中。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 扩容方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 超过最大容量的处理
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
Vector的底层的实现其实是一个数组,是线程安全的实现类,方法都有synchronized
LinkedList的底层其实是一个双向链表,每一个对象都是一个Node节点,Node就是一个静态内部类,它是**线程不安全的,**所有的方法都没有加锁或者进行同步
这里先简单介绍一下,下面会对ArrayList的扩容机制进行分析
ArrayList是线程不安全的,如果不指定它的初始容量,那么它的初始容量是0,当第一次进行添加操作的时候它的容量将扩容为10
三种集合的使用场景
1:可以使用Vector
2.自己重写类似于ArrayList的但是线程安全的集合
3.可以使用**Collections(工具类)**中的方法,将ArrayList变成一个线程安全的集合
4.可以使用java.util.concurrent包下的CopyOnWriteArrayList,它是线程安全的
它是juc包下的,专门用于并发编程的,他的设计思想是:读写分离,最终一致,写时复制
它不能指定容量,初始容量是0.它底层也是一个数组,集合有多大,底层数组就有多大,不会有多余的空间
CopyOnWriteArrayList的缺点
底层是数组,删除插入的效率不高,写的时候需要复制,占用内存,浪费空间,如果集合足够大的时候容易触发GC
数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。【当执行add或remove操作没完成时,get获取的仍然是旧数组的元素】。CopyOnWriteArrayList读取时不加锁只是写入和删除时加锁
应用场景:读操作远大于写操作的时候
CopyOnWriteArrayList和Collections.synchronizedList区别
CopyOnWriteArrayList和Collections.synchronizedList是实现线程安全的列表的两种方式。两种实现方式分别针对不同情况有不同的性能表现,其中CopyOnWriteArrayList的写操作性能较差,而多线程的读操作性能较好。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不同的多线程安全实现类。
最终一致,写时复制**
它不能指定容量,初始容量是0.它底层也是一个数组,集合有多大,底层数组就有多大,不会有多余的空间
CopyOnWriteArrayList的缺点
底层是数组,删除插入的效率不高,写的时候需要复制,占用内存,浪费空间,如果集合足够大的时候容易触发GC
数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。【当执行add或remove操作没完成时,get获取的仍然是旧数组的元素】。CopyOnWriteArrayList读取时不加锁只是写入和删除时加锁
应用场景:读操作远大于写操作的时候
CopyOnWriteArrayList和Collections.synchronizedList区别
CopyOnWriteArrayList和Collections.synchronizedList是实现线程安全的列表的两种方式。两种实现方式分别针对不同情况有不同的性能表现,其中CopyOnWriteArrayList的写操作性能较差,而多线程的读操作性能较好。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不同的多线程安全实现类。