集合
集合框架概述
- 集合、数组都是对多个数据进行存储的结构,简称Java容器。此时的存储指的是内存层面的,不涉及到持久化的存储
- 数组在存储多个数据方面的特点:
- 一旦初始化,长度确定
- 需要指明数组的元素类型,只能操作指定类型的数据:
String[] arr;
- 数组在存储多个数据方面的缺点:
- 初始化后长度不可修改
- 数组中提供的方法有限,对于添加、删除、插入数据等操作非常不便,效率不高
- 获取数组中实际元素个数的需求,数组没有现成的属性或方法可用
- 数组有序、可重复。对于无序、不可重复的需求不能满足。
Java集合可分为Collection和Map两种体系
- Collection接口:单列数据,定义了存取一组对象的方法的集合
- List:元素有序、可重复的集合(“动态”数组)
- ArrayList、LinkedList、Vector
- Set:元素无序、不可重复的集合
- HashSet、LinkedHashSet、TreeSet
- List:元素有序、可重复的集合(“动态”数组)
- Map接口:双列数据,保存具有映射关系“key-value对”的集合
- HashMap、LinkedHashMap、TreeMap、Hashtable、Properties
Collection接口中的方法的使用
contains(Object obj)
containsAll(Collection coll)
同上
实现的机制:当前元素与ArrayList中的每个元素逐一对比内容(注意不是对比地址,调用的是equals()方法),找到返回true,未找到返回false。
因为是调用equals()方法,因此向Collections()接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()方法。
public class CollectionTest {
@Test
public void test1()
{
Collection coll = new ArrayList();
coll.add(123);
coll.add(123123);
coll.add(new String("asd"));
coll.add(new Date());
System.out.println(coll.contains(new String("asd")));//true
}
remove(Object obj)`
返回值:成功为true,失败为false,也是按内容查找,因此也会调用equals()方法
removeAll(Collection coll1)
从当前集合中移除coll1中所有的元素,直接在当前集合修改
@Test
public void test2()
{
Collection coll = new ArrayList();
coll.add(123);
coll.add(123123);
coll.add(new String("asd"));
coll.add(new Date());
Collection coll1 = new ArrayList();
coll1.add(123123);
coll1.add(new String("asd"));
coll.removeAll(coll1);
System.out.println(coll);
//[123, Thu Aug 27 15:15:57 CST 2020]
}
retainAll(Collection coll1)
求两个集合交集,与removeAll()
类似,不再赘述
equals(Object obj)
判断当前集合和形参是否相等,关于是否考虑顺序,需要看obj的类型,如果是ArrayList则需要顺序相同,如果是Set则不需要。
hashCode()
返回当前对象的哈希值,哈希值根据内容确定。
toArray()
集合→数组
Object[]arr = coll.toArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
Arrays.asList()
数组→集合,Arrays类的静态方法
List<Object> list = Arrays.asList(arr);
System.out.println(list);
迭代器
-
集合元素的遍历,使用Iterator接口,Iterator称为迭代器
-
Collection接口继承了
java.lang.Iterator
接口,该接口有一个iterator()
方法,那么所有实现了Collection接口的集合类都有一个iterator()
方法,用以返回一个实现了Iterator
接口的对象 -
集合对象每次调用
iterator()
方法都会创建一个新的迭代器,默认游标在第一个元素之前
调用迭代器一定要先hasNext()
再next()
,不然容易抛异常
@Test
public void test2()
{
Collection coll = new ArrayList();
coll.add(123);
coll.add(123123);
coll.add(new String("asd"));
coll.add(new Date());
Iterator iter = coll.iterator();
while(iter.hasNext())
{
System.out.println(iter.next());
}
}
每次调用next()
方法执行两个动作:
- 指针下移
- 将下移以后集合位置上的元素返回
两种遍历集合的错误写法
错误方式一:
Iterator iter = coll.iterator();
while(iter.next()!=null)
{
System.out.println(iter.next());
}
会跳着输出,while判断时已经往后移动了一个位置。
错误方式二:
Iterator iter = coll.iterator();
while(coll.iterator().next()!=null)
{
System.out.println(coll.iterator().next());
}
每次调用iterator()都创造新的迭代器对象,因此每次都输出第一个元素。
remove()
方法
内部定义了remove()
,可以在遍历的时候删除对应位置的元素。注意与Collection中的remove()方法区别
while(iter.hasNext())
{
Object obj = iter.next();
if("asd".equals(obj))
{
iter.remove();
}
}
注意:在第一次next()或者已经使用过remove()之后使用remove()会报错
foreach循环遍历集合元素
Java 5.0提供了foreach循环迭代访问集合、数组
@Test
public void test3()
{
Collection coll = new ArrayList();
coll.add(123);
coll.add(123123);
coll.add(new String("asd"));
coll.add(new Date());
//for(集合对象:集合)
for (Object item:coll) {
System.out.println(item);
}
}
实际上内部调用的还是迭代器,用hasNext()和next()写成。
练习题
@Test
public void test3()
{
String[] arr = new String[]{"MM","MM"};
// for (int i = 0; i < arr.length; i++) {
// arr[i]="GG";
// }//"GG"
for (String s:arr) {
s="GG";
}"MM"
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
foreach中相当于每次用迭代器获取了一个新的对象,类似于
Iterator iter = arr.iterator();
while(iter.hasNext())
{
String s = iter.next()
s = "GG";
}
这样看来很明显arr内的值并没有受到影响。
List接口
List:元素有序、可重复的集合(“动态”数组)
- ArrayList、LinkedList、Vector
面试题:ArrayList、LinkedList、Vector三者的异同
同:三个类都实现了List接口,存储数据的特点相同:有序、可重复。
异:
- ArrayList:List接口的主要实现类,线程不安全,效率高;底层使用Object[]存储
- LinkedList:对于频繁拆入、删除操作,使用链表效率比ArrayList高;底层使用双向链表存储
- Vector:作为List的古老实现类,线程安全,效率低;底层使用Object[]存储
ArrayList源码分析
jdk8.0中的版本
transient Object[] elementData;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 一开始是空的,默认为0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
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);
}
构造ArrayList时不会指定底层数组大小,第一次调用add时才创建好底层数组。第一次add时,length为0,minCapacity为1,进入grow()
方法。扩容都是由grow()
方法实现,每次扩容扩为1.5倍,若仍不够则直接以minCapacity
为新的容量,若新的容量超过了ArrayList的最大容量,则进行处理。扩容后需要将原有数组中的数据复制到新数组。
结论:建议开发中使用构造器:ArrayList list = new ArrayList(int capacity)
,这样对于比较大的数组不会经常扩容,提高程序效率。
与JDK7.0的区别:JDK7.0中默认构造函数会将底层数组大小初始化为10。
LinkedList源码分析
//LinkedList的内部类Node
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;
}
}
//两个成员属性
transient Node<E> first;
transient Node<E> last;
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
内部声明了Node类型的first和last属性,默认值为null
小面试题
@Test
public void test2()
{
ArrayList coll = new ArrayList();
coll.add(123);
coll.add(123123);
coll.add(new String("asd"));
coll.add(new Date());
updateList(coll);
System.out.println(coll);
//[asd, Fri Aug 28 14:53:26 CST 2020]
}
public void updateList(List list)
{
list.remove(1);
list.remove(new Integer(123));
}
注意区分List中remove(int index)
和remove(Object obj)
Set接口
Set:元素无序、不可重复的集合
- HashSet、LinkedHashSet、TreeSet
面试题:ArrayList、LinkedList、Vector三者的异同
同:三个类都实现了List接口,存储数据的特点相同:有序、可重复。
异:
- HashSet:Set接口的主要实现类,线程不安全,可以存储null
- LinkedList:HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
- TreeSet:可以按照添加对象指定属性进行排序
理解无序性和不可重复
以HashSet为例说明。
-
无序性
不等于随机性 。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的Hash值决定。
-
不可重复性
保证添加的元素按照equals()判断时,不能返回true。即:相同的元素只能添加一个。注: 自定义类要重写equals()函数。
添加元素过程
以HashSet为例说明。
HashSet底层存储机制采用数组+链表形式,数组的每一个位置可以存放一个链表。
向HashSet中添加元素a,首先调用a所在类的hashCode()方法,计算元素a的哈希值,此哈希值通过散列函数得到在底层数组中的存放位置,判断数组此位置上是否已经有元素:
- 若没有其他元素,则a添加成功 →情况一
- 若有元素b或以链表形式存在的多个元素,则比较元素a与元素b的hash值:
- 如果hash值不同,则a添加成功**(hash不同则内容认为一定不同)** →情况二
- 如果相同,则调用a所在类的equals()方法**(hash值相同不代表内容相同)**
- 返回true则添加失败
- 返回false,则a与b还是不一样的,添加成功 →情况三
对于情况二和情况三,元素a与已经存在指定索引位置上数据以链表的方式存储。
- jdk 7:元素a放到数组中,作为链表的头
- jdk 8:原来的元素放在数组中,a放到链表最后
底层数组初始容量是16,当使用率超过0.75(12个位置有数据)则扩容为2倍。
重写equals()
和hashCode()
在HashSet中添加自定义类的时候,不能忘记在自定义类中重写equals()
和hashCode()
方法。这两个方法都是用于HashSet.add()
中的添加成功的判定的,如果不进行重写,会调用Object类中的对应方法,比如Object.hashCode()
采用了类似随机数的方式:
//未重写hashCode()
@Test
public void test2()
{
HashSet coll = new HashSet();
coll.add(123);
coll.add(123123);
coll.add(new User("Bill",10));
coll.add(new User("Bill",10));
System.out.println(coll);
//[123123, User{name='Bill', age=10}, User{name='Bill', age=10}, 123]
}
随机数产生的两个hashCode自然是不相等的,因此调用add()方法时将其视作两个User对象。
注:重写时一定要保证相等的对象具有相等的散列码
不用自己写,Alt+Enter重载即可
LinkedHashSet的使用
LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个和后一个数据。
目的:对于频繁的遍历操作,LinkedHashSet的效率要高于HashSet
TreeSet
这部分了解即可,不需要花太多时间。
TreeSet是用于排序的,向TreeSet中添加的数据,要求是相同类的对象。不能添加不同类的对象。
两种排序方式:自然排序(Comparable)和定制排序(Comparator):
自定义类要实现Comparable
接口,先比较User类的姓名:
@Override
public int compareTo(Object o) {
if(o instanceof User){
User user = (User) o;
return this.name.compareTo(user.name);
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
@Test
public void test1()
{
TreeSet set = new TreeSet();
set.add(new User("Bill",13));
set.add(new User("Bill",134));
set.add(new User("Adam",123));
set.add(new User("Zick",31));
Iterator it = set.iterator();
while(it.hasNext())
System.out.println(it.next());
}
从结果发现,输出确实是按照名字排序,但是发现少了个Bill,明明两个对象使用equals()并不相等,这是为什么呢?
**在TreeSet的自然排序中中,判断两个对象相等不是调用equals()
方法,而是调用compareTo()
方法!**所以两个名字为Bill的对象被视为相同。
实现二级排序:
@Override
public int compareTo(Object o) {
if(o instanceof User){
User user = (User) o;
int result = this.name.compareTo(user.name);
if (result!=0)
return Integer.compare(this.age, user.age);
else
return result;
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
结果正常!
定制排序:比较两个对象是否相同的标准是compare()返回0,而不是equals()
@Test
public void test2()
{
Comparator com = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
// 按照年龄从小到大排序
if(o1 instanceof User && o2 instanceof User)
{
User user1 = (User)o1;
User user2 = (User)o2;
return Integer.compare(user1.getAge(), user2.getAge());
}else{
throw new RuntimeException("数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(com);
set.add(new User("Bill",13));
set.add(new User("Bill",134));
set.add(new User("Adam",123));
set.add(new User("Zick",31));
Iterator it= set.iterator();
while(it.hasNext())
System.out.println(it.next());
}