目录
add(int index, Object element)方法
Object set(int index, Object element)
集合纲要
什么是集合?有什么用?
1、 数组其实就是一个集合。集合实际上就是一个容器。可以来容纳其它类型的数据。
集合为什么说在开发中使用较多?
集合是一个容器,是一个载体,可以一次容纳多个对象。
在实际开发中,假设连接数据库,数据库当中有10条记录,
那么假设把这10条记录查询出来,在java程序中会将10条
数据封装成10个java对象,然后将10个java对象放到某一个
集合当中,将集合传到前端,然后遍历集合,将一个数据一个
数据展现出来。
2、集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,
集合当中存储的都是java对象的内存地址。(或者说集合中存储的是引用。)
list.add(100); //自动装箱Integer
注意:
集合在java中本身是一个容器,是一个对象。
集合中任何时候存储的都是“引用”。
3、在java中每一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构当中。
什么是数据结构?数据存储的结构就是数据结构。不同的数据结构,数据存储方式不同。例如:
数组、二叉树、链表、哈希表...
以上这些都是常见的数据结构。
你往集合c1中放数据,可能是放到数组上了。
你往集合c2中放数据,可能是放到二叉树上了。
.....
你使用不同的集合等同于使用了不同的数据结构。
你在java集合这一章节,你需要掌握的不是精通数据结构。java中已经将数据结构实现了,已经写好了这些常用的集合类,你只需要掌握怎么用?在什么情况下选择哪一种合适的集合去使用即可。
new ArrayList(); 创建一个集合,底层是数组。
new LinkedList(); 创建一个集合对象,底层是链表。
new TreeSet(); 创建一个集合对象,底层是二叉树。
.....
4、集合在java JDK中哪个包下?
java.util.*;
所有的集合类和集合接口都在java.util包下
在API文档中可以看到Collection这个超级接口并且继承Iterable(迭代接口,父接口)
集合继承结构图_Collection 部分
此图非常重要!!
5、在java中集合分为两大类:
一类是单个方式存储元素:
单个方式存储元素,这一类集合中超级父接口:java.util.Collection;
一类是以键值对儿的方式存储元素
以键值对的方式存储元素,这一类集合中超级父接口:java.util.Map;
集合继承结构图_Map 部分
Map集合和Collection集合之间没有关系
实现类特征总结:
ArrayList:底层是数组
LinkedList:底层是双向链表
Vector:底层是数组,线程安全的,效率低,使用较少
HashSet:底层是HashMap,放到HashSet集合中的元素等同于放到HashMap集合key部分了
TreeSet:底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合key部分了
HashMap:底层是哈希表。
Hashtable:底层是哈希表,线程安全的,效率低,使用较少
Properties:线程安全的,并且key和value只能存储字符串String
TreeMap:底层是二叉树。TreeMap集合的key可以自动按照大小顺序排序
List集合存储元素的特点:
有序可重复
有序:存进去的顺序和取出的顺序相同,每一个元素都有下标。
可重复:存进去1 ,可以再存储一个1
Set(Map)集合存储的特点:
无序不可重复
无序:存进的顺序和取出的顺序不一定相同。另外Set集合中元素没有下标
不可重复:存进去1,不能再存储1了
SortedSet(SortMap)集合存储的特点:
首先是无序不可重复的,但SortedSet集合中元素是可以排序的
无序:存进的顺序和取出的顺序不一定相同。另外Set集合中元素没有下标
不可重复:存进去1,不能再存储1了
可排序:可以按照大小顺序排列。
Map集合的key就是一个Set集合
往Set集合中放数据,实际上放到了Map集合的key部分。
超级接口Collection
Collection接口的常用方法
1、Collection中能存放什么元素?
没有使用“泛型”之前,Collection中可以存储Object的所有子类型。
使用了“泛型”之后,Collection中只能存储某个具体的类型。
集合后期我们会学习“泛型”语法。目前先不用管。Collection中什么都能存,
只要是Object的子类型就行。(集合中不能直接存储基本数据类型,也不能存
java对象,只是存储java对象的内存地址。)
2、Collection中的常用方法
- boolean add(Object e) 向集合中添加元素
- int size() 获取集合中元素的个数
// 创建一个集合对象
//Collection c = new Collection(); // 接口是抽象的,无法实例化。
// 多态
Collection c = new ArrayList();
// 测试Collection接口中的常用方法
c.add(1200); // 自动装箱(java5的新特性。),实际上是放进去了一个对象的内存地址。Integer x = new Integer(1200);
c.add(3.14); // 自动装箱
c.add(new Object());
c.add(new Student());
c.add(true); // 自动装箱
// 获取集合中元素的个数
System.out.println("集合中元素个数是:" + c.size()); // 5
- void clear() 清空集合
// 清空集合
c.clear();
System.out.println("集合中元素个数是:" + c.size()); // 0
- boolean contains(Object o) 判断当前集合中是否包含元素o,包含返回true,不包含返回false
// 再向集合中添加元素
c.add("hello"); // "hello"对象的内存地址放到了集合当中。
c.add("world");
c.add("浩克");
c.add("绿巨人");
c.add(1);
// 判断集合中是否包含"绿巨人"
boolean flag = c.contains("绿巨人");
System.out.println(flag); // true
- boolean remove(Object o) 删除集合中的某个元素。
System.out.println("集合中元素个数是:" + c.size()); // 5
// 删除集合中某个元素
c.remove(1);
System.out.println("集合中元素个数是:" + c.size()); // 4
- boolean isEmpty() 判断该集合中元素的个数是否为0
// 判断集合是否为空(集合中是否存在元素)
System.out.println(c.isEmpty()); // false
// 清空
c.clear();
System.out.println(c.isEmpty()); // true(true表示集合中没有元素了!)
- Object[] toArray() 调用这个方法可以把集合转换成数组。【作为了解,使用不多。】
Object[] objs = c.toArray();
for(int i = 0; i < objs.length; i++){
// 遍历数组
Object o = objs[i];
System.out.println(o);
}
集合迭代Iterator(*****)
Iterator迭代器对象中的方法
boolean hasNext()如果仍有元素可以迭代,则返回 true。
Object next() 返回迭代的下一个元素。
【注意】:以下的遍历方式/迭代方式,是所有Collection通用的一种方式。
在Map集合中不能用。在所有的Collection以及子类中使用。
第一步:获取集合对象的迭代器对象Iterator
Iterator it = c.iterator();
第二步:通过以上获取的迭代器对象开始迭代/遍历集合。
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
}
不管你当初存进去什么,取出来统一都是Object。
迭代器执行原理
迭代器迭代过程图:
由图可知:迭代器初始位置并没有指向元素(类似栈中的栈帧的初始位置为int index = -1;一样),因此 next()方法是返回下一个元素。
迭代器通过next()方法进行一步一步迭代,而next()方法能使得迭代器对象移动的原理:
next()源码:
public E next() { E item = nextItem; if (item == null) throw new NoSuchElementException(); //防止迭代时死循环 advance(); //前进 return item; }
深入contains方法
boolean contains(Object o)
判断集合中是否包含某个对象o
如果包含返回true, 如果不包含返回false。
contains方法是用来判断集合中是否包含某个元素的方法,
那么它在底层是怎么判断集合中是否包含某个元素的呢?
调用了equals方法进行比对。
equals方法返回true,就表示包含这个元素。
// 创建集合对象
Collection c = new ArrayList();
// 向集合中存储元素
String s1 = new String("abc"); // s1 = 0x1111
c.add(s1); // 放进去了一个"abc"
String s2 = new String("def"); // s2 = 0x2222
c.add(s2);
// 集合中元素的个数
System.out.println("元素的个数是:" + c.size()); // 2
// 新建的对象String
String x = new String("abc"); // x = 0x5555
System.out.println(c.contains(x)); //判断集合中是否存在"abc" true
ArrayList中contains方法源码: public boolean contains(Object o) { return indexOf(o) >= 0; }
---------------------------------------------------------------------------------------------
public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; }
分析可知contains()方法在判断时调用了equals方法,如果比较的内容是对象,对象中的equals方法必须要重写,不重写默认调用Object类中的原始equals方法比较是内存地址而不是内容,因此存放在一个集合中的类型,一定要重写equals方法。
深入remove方法
// 创建集合对象
Collection cc = new ArrayList();
// 创建字符串对象
String s1 = new String("hello");
// 加进去。
cc.add(s1);
// 创建了一个新的字符串对象
String s2 = new String("hello");
// 删除s2
cc.remove(s2); // s1.equals(s2) java认为s1和s2是一样的。删除s2就是删除s1。
// 集合中元素个数是?
System.out.println(cc.size()); // 0
ArrayList中remove方法源码:
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方法,而s1,s2为String类型,String已经重写过equals方法,因此此时执行remove方法时认为s1和s2是相等的,删除s2就是删除s1,会将二者删除。
因此存放在一个集合中的类型,一定要重写equals方法。
迭代器获取位置(***)
Collection c = new ArrayList();
Iterator it = c.iterator();
// 添加元素
c.add(1); // Integer类型
c.add(2);
c.add(3);
// 获取迭代器
//Iterator it = c.iterator();
while(it.hasNext()){
// 编写代码时next()方法返回值类型必须是Object。
// Integer i = it.next();
Object obj = it.next();
System.out.println(obj);
} //运行异常!!
注意:此时获取的迭代器,指向的是那是集合中没有元素状态下的迭代器。
一定要注意:集合结构只要发生改变,迭代器必须重新获取。
当集合结构发生了改变,迭代器没有重新获取时,调用next()方法时:java.util.ConcurrentModificationException
Collection c2 = new ArrayList();
c2.add("abc");
c2.add("def");
c2.add("xyz");
Iterator it2 = c2.iterator();
while(it2.hasNext()){
Object o = it2.next();
//运行异常
c2.remove(o);
}
删除元素之后,集合的结构发生了变化,应该重新去获取迭代器
是,循环下一次的时候并没有重新获取迭代器,所以会出现异常:java.util.ConcurrentModificationException
出异常根本原因是:集合中元素删除了,但是没有更新迭代器(迭代器不知道集合变化了)直接通过集合去删除元素,没有通知迭代器。(导致迭代器的快照和原集合状态不同。)
使用迭代器来删除可以吗?
Iterator it2 = c2.iterator();
while(it2.hasNext()){
Object o = it2.next();
// 迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中的元素)。
it2.remove(); // 删除的一定是迭代器指向的当前元素。
System.out.println(o);
}
System.out.println(c2.size()); //0
迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中的元素)。相当于迭代器在迭代前对集合拍了一个快照,对快照内容进行迭代,但如果用集合的删除方法修改内容,迭代器并不知道,于是在对照时会发生异常;而使用迭代器的删除方法修改内容,相当于迭代器在快照上进行修改,并且在对照时也对原集合对应位置进行对照修改,这样就不会出现异常。
总结
重点:当集合的结构发生改变时,迭代器必须重新获取,如果还是用以前老的迭代器,会出现
异常:java.util.ConcurrentModificationException
重点:在迭代集合元素的过程中,不能调用集合对象的remove方法,删除元素:
c.remove(o); 迭代过程中不能这样。会出现:java.util.ConcurrentModificationException
重点:在迭代元素的过程当中,一定要使用迭代器Iterator的remove方法,删除元素,
不要使用集合自带的remove方法删除元素。
List接口
List接口元素特点
List集合存储元素特点:有序可重复
有序:List集合中的元素有下标。
从0开始,以1递增。
可重复:存储一个1,还可以再存储1
List接口中常用方法
List既然是Collection接口的子接口,那么肯定List接口有自己“特色”的方法:
以下只列出List接口特有的常用的方法:
void add(int index, Object element)
Object set(int index, Object element)
Object get(int index)
int indexOf(Object o)
int lastIndexOf(Object o)
Object remove(int index)
以ArrayList为例:
add(int index, Object element)方法
// 创建List类型的集合。
List myList = new ArrayList();
// 添加元素
myList.add("A"); // 默认都是向集合末尾添加元素。
myList.add("B");
myList.add("C");
myList.add("D");
//获取迭代器
Iterator it = myList.iterator();
while(it.hasNext()){
Object elt = it.next();
System.out.println(elt);
}
其输出顺序为:A B C D
如果使用add(int index , Object element);方法
myList.add("A");
myList.add("B");
myList.add("C");
myList.add("D");
myList.add(1, "KING");
其输出顺序为:A KING B C D
在列表的指定位置插入指定元素(第一个参数是下标)
这个方法使用不多,因为对于ArrayList集合来说效率比较低。
get(int index)方法
根据下标获取元素
Object firstObj = myList.get(0);
System.out.println(firstObj);
//A
因为有下标,所以List集合有自己比较特殊的遍历方式
通过下标遍历。【List集合特有的方式,Set没有。】
for(int i = 0; i < myList.size(); i++){
Object obj = myList.get(i);
System.out.println(obj);
}
int indexOf(Object o)方法
获取指定对象第一次出现处的索引。
System.out.println(myList.indexOf("C")); // 3
int lastIndexOf(Object o)方法
myList.add(4,C)
System.out.println(myList.lastIndexOf("C")); // 4
Object remove(int index)
删除指定下标位置的元素
// 删除下标为0的元素
myList.remove(0);
Object set(int index, Object element)
修改指定位置的元素
myList.set(2, "Soft");
增删查改单词
增删改查这几个单词要知道:
增:add、save、new
删:delete、drop、remove
改:update、set、modify
查:find、get、query、select
ArrayList
ArrayList容量
ArrayList源码:
private static final int DEFAULT_CAPACITY = 10;
ArrayList集合初始化容量10
transient Object[] elementData;
ArrayList底层是Object类型的数组。
// 数组的长度是10
List list1 = new ArrayList();
// 集合的size()方法是获取当前集合中元素的个数。不是获取集合的容量。
System.out.println(list1.size()); // 0
在新版本JDK(8以上)中有这么规定:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = { };源码注释:
We distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when first element is added.
说明底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量10。
位运算符 << , >>
System.out.println(10 >> 1);//5
上述表示>> 1 二进制右移1位。右移1位其实就是除以2
10的二进制位是: 00001010 【10】
10的二进制右移1位是:00000101 【5】
<< 1表示二进制左移1位。左移1位其实就是乘以2
ArrayList集合的扩容
add() 源码:
int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity,oldCapacity >> 1);
说明增加量▲为原来的0.5倍 总体相当于扩容到原容量的11.5倍
ArrayList集合总结
1、默认初始化容量10(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量10。)
2、集合底层是一个Object[]数组。
3、构造方法:
new ArrayList();
new ArrayList(20);
4、ArrayList集合的扩容:
增长到原容量的1.5倍。
ArrayList集合底层是数组,怎么优化?
尽可能少的扩容。因为数组扩容效率比较低,建议在使用ArrayList集合
的时候预估计元素的个数,给定一个初始化容量。
5、数组优点:
检索效率比较高。(每个元素占用空间大小相同,内存地址是连续的,知道首元素内存地址,
然后知道下标,通过数学表达式计算出元素的内存地址,所以检索效率最高。)
6、数组缺点:
随机增删元素效率比较低。
另外数组无法存储大数据量。(很难找到一块非常巨大的连续的内存空间。)
7、向数组末尾添加元素,效率很高,不受影响。
8、面试官经常问的一个问题?
这么多的集合中,你用哪个集合最多?
答:ArrayList集合。
因为往数组末尾添加元素,效率不受影响。
另外,我们检索/查找某个元素的操作比较多。
7、ArrayList集合是非线程安全的。(不是线程安全的集合。)
LinkedList
单向链表数据结构
单链表中的节点。
节点是单向链表中基本的单元。
每一个节点Node都有两个属性:
一个属性:是存储的数据。
另一个属性:是下一个节点的内存地址。
public class Node {
// 存储的数据
Object data;
// 下一个节点的内存地址
Node next;
public Node(){
}
public Node(Object data, Node next){
this.data = data;
this.next = next;
}
}
链表类。(单向链表)
LinkedList也有下标
List list = new LinkedList();
list.add("a");
list.add("b");
list.add("c");
for(int i = 0; i <list.size(); i++){
Object obj = list.get(i);
System.out.println(obj);
}
注意:ArrayList之所以检索效率比较高,不是单纯因为下标的原因。是因为底层数组发挥的作用。
LinkedList集合照样有下标,但是检索/查找某个元素的时候效率比较低,因为只能从头节点开始一个一个遍历。
双向链表数据结构
LinkedList定义源码 :
transient int size = 0; //Pointer to first node transient Node<E> first; //Pointer to last node transient Node<E> last;
整个LinkedList add()方法运行过程:
add("a");方法源码:
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); //(Node是一个静态的内部类) last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }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; } }
过程原理图:
由源码可知:
LinkedList集合没有初始化容量。
最初这个链表中没有任何元素。first和last引用都是null。
不管是LinkedList还是ArrayList,以后写代码时不需要关心具体是哪个集合。
因为我们要面向接口编程,调用的方法都是接口中的方法。
List list2 = new ArrayList(); // 这样写表示底层你用了数组。
List list2 = new LinkedList(); // 这样写表示底层你用了双向链表。
// 以下这些方法你面向的都是接口编程。
list2.add("123");
list2.add("456");
list2.add("789");
for(int i = 0; i < list2.size(); i++){
System.out.println(list2.get(i));
}
上述两种创建链表的方法都可以,以下方法均面向接口,上面的对象如果进行修改,下面的内容则不会变动。
链表总结
链表的优点:
由于链表上的元素在空间存储上内存地址不连续。
所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高。
在以后的开发中,如果遇到随机增删集合中元素的业务比较多时,建议
使用LinkedList。
链表的缺点:
不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头
节点开始遍历,直到找到为止。所以LinkedList集合检索/查找的效率
较低。
ArrayList:把检索发挥到极致。(末尾添加元素效率还是很高的。)
LinkedList:把随机增删发挥到极致。
加元素都是往末尾添加,所以ArrayList用的比LinkedList多。
Vector
1、底层也是一个数组。
2、初始化容量:10
public Vector() {
this(10);
}
3、怎么扩容的?
扩容之后是原容量的2倍。
10--> 20 --> 40 --> 80
源码注释:
the capacity of the vector is doubled each time it needs to grow.
4、ArrayList集合扩容特点:
ArrayList集合扩容是原容量1.5倍。
5、Vector中所有的方法都是线程同步的,都带有synchronized关键字,
是线程安全的。效率比较低,使用较少了。
// 创建一个Vector集合
List vector = new Vector();
//Vector vector = new Vector();
// 添加元素
// 默认容量10个。
vector.add(1);
vector.add(2);
vector.add(3);
vector.add(4);
vector.add(5);
vector.add(6);
vector.add(7);
vector.add(8);
vector.add(9);
vector.add(10);
// 满了之后扩容(扩容之后的容量是20.)
vector.add(11);
Iterator it = vector.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
}
6、怎么将一个线程不安全的ArrayList集合转换成线程安全的呢?
使用集合工具类:
java.util.Collections;
java.util.Collection 是集合接口。
java.util.Collections 是集合工具类。
import java.util.*;
// 这个可能以后要使用!!!!
List myList = new ArrayList(); // 非线程安全的。
// 变成线程安全的
Collections.synchronizedList(myList); // 这里没有办法看效果
// myList集合就是线程安全的了。
myList.add("111");
myList.add("222");
myList.add("333");