前言
本文主要介绍了集合的基本概念以及各种集合接口和它的实现类,重点是集合内部的数据结构,理解这些结构对我们在选择哪种集合作为容器有很大的帮助。
一、泛型
为什么要引入泛型:
为什么会有泛型呢? 早期的Object类型可以接收任意的对象类型,但是在实际的使用中, 会有类型转换的问题。也就存在这隐患,所以Java提供了泛型来解决 这个安全问题。
1.泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后 调用此方法时传递实参。
2.参数化类型,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数, 此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具 体的类型(类型实参)。
public class Demo1 <T,E>{ //泛型的本质是为了参数化类型,在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型
T name;
E age;
public T getName() {
return name;
}
public void setName(T name) {
this.name = name;
}
public E getAge() {
return age;
}
public void setAge(E age) {
this.age = age;
}
public T play(E hour){
System.out.println("你今天已经玩了"+hour+"个小时");
return null;
}
public static void main(String[] args) {
//如果不写泛型的类型,默认为object类型
Demo1<String,Integer>demo=new Demo1();
demo.play(100);
System.out.println(demo.name);
System.out.println(demo.age);
}
}
注意:
1、泛型的类型参数只能是类类型(包括自定义类 )
2、泛型的类型参数可以有多个。 如果没有定义具体类型,默认为Object。
二、集合概念和API
为什么需要集合?
我们平时储存数据的时候,往往都是不清楚数据具体有多少条。而数组的长度又是不可变的,使用数组会造成很多麻烦。
这要怎么解决呢?
所以这时候集合出现了,集合(大小可变)可以动态的保存我们的数据。
JAVA中对各种数据结构的实现集合,集合的体系如下图所示。
三、Collection 接口
Collection:定义了存取一组对象方法,子接口Set和List定义存储方式。
1.增加方法:往集合中添加元素
/*Collection c1=new ArrayList(); 如果没定义泛型,可以往集合里添加任意类型的元素,在后面的使用中可能会出现问题
c1.add("hello,world");
c1.add(1);
c1.add(1.0);*/
Collection<Integer>c1=new ArrayList();
c1.add(1); //自动装箱,把1转换成包装类
c1.add(2);
c1.add(3);
Collection<Integer>c2=new ArrayList();
c2.add(1);
c2.add(2);
c2.add(9);
//c1.addAll(c2); //将c2集合里面的全部元素加到c1中
2.删除方法:删除集合中的元素
c1.clear(); 从此集合中删除所有元素(可选操作)
System.out.println(c1.contains(1)); 如果此集合包含指定的元素,则返回true
System.out.println(c1.isEmpty()); 如果此集合不包含元素,则返回 true
System.out.println(c1.size()); 返回此集合中的元素数
//如果此列表包含指定 集合的所有元素则返回 true。
System.out.println(c1.containsAll(c2));
//把c1中和c2集合相同的元素删除
c1.removeAll(c2);
//在c1集合中仅保留和c2集合元素相同的元素,如果c1改变返回true,否则false;
System.out.println(c1.retainAll(c2));
3.其他方法
//从第一个到最后一个元素 返回一个包含此列表中所有元素的数组。 返回的数组是Object类型
Object []sb=c1.toArray();
//返回的数组的运行时类型是指定数组的运行时类型
Integer[]sb2= c1.toArray(new Integer[c1.size()]);
四、List 接口及实现类
List:继承了Collection,有三个实现类,ArrayList,Linkedlist,Vector。
ArrayList:采用数组方式存储。
常用方法:
1.add(E e) 将指定的元素追加到此列表的末尾。
2.add(int index,E element) 在此列表中的指定位置插入指定的元素。
3.get(int index) 返回此列表中指定位置的元素。
4.indexOf(Object o) 返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1
5.remove(int index) 删除并返回指定位置元素
6.toArray(T[] a) 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。
Linkedlist:链表。
1.add(E e) 将指定的元素追加到此列表的末尾。
2.addFirst(E e) 在该列表开头插入指定的元素。
3.addLast(E e) 将指定的元素追加到此列表的末尾。
4.clear() 从列表中删除所有元素。
5.peek() 查看但不删除此列表的头(第一个元素)。
6.poll() 检索并删除此列表的头(第一个元素)。
Linkedlist:
1.双向链表,优点,增加删除,用时间很短,但是因为没有索引,对索引的操作,比较麻烦,只能循环遍历,但是每次循环的时候,都会先判断一下,这个索引位于链表的前部分还是后部分,每次都会遍历链表的一半 ,而不是全部遍历。
2.都有一个previous和next, 链表最开始的部分都有一个fiest和last 指向第一个元素,和最后一个元素。增加和删除的时候,只需要更改一个previous和next,就可以实现增加和删除,所以说,LinkedList对于数据的删除和增加相当的方便。
3.在插入/删除的时候,默认在链尾操作。
ArrayList:
1.动态数组,查询快,增删慢(需要移动数组元素)也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。
2.每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作(grow),扩为原数组的1.5倍。 ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。
3.所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。
Vector:
1、Vector的方法都是同步的,是线程安全的,而ArrayList的方法不是,由于线程的同步必然要影响性能;
2、当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加百分之五十的大小,这样,ArrayList就有利于节约内存空间。
五、List接口集合迭代
很多时候,我们需要对集合进行遍历,List接口集合有以下几种:
1.for循环遍历:最简单的一种遍历方式
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
/*for(int i = 0; i < list.size(); i++) { //普通for循环
if (list.get(i) == 1) {
list.remove(i);
}
}
2.增强for循环遍历:普通for循环的增强版,不允许在循环中进行修改。
for(Integer item: list){ //增强for循环
System.out.println(item);
list.remove(item); //抛出ConcurrentModificationException
//当不允许这样的修改时,可以通过检测到对象的并发修改的方法来抛出此异常。
}
3.迭代器遍历(Iterator):集合为我们提供的一种遍历方式
Iterator<Integer>it=list.iterator(); //迭代器遍历
while(it.hasNext()){ //hasNext(): 如果迭代具有更多元素,则返回 true
Integer item=it.next(); //返回迭代中的下一个元素
if(item==1){
it.remove();
}
else{
System.out.println(item);}
}
4.Stream流遍历:比较新的一种方法,能简化代码量。
Stream<Integer>s=list.stream(); 流遍历
s.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer t) {
System.out.println(t);
}
});
//结合lambda表达式之后
s.forEach(t-> System.out.println(t));
Iterator遍历和ListIterator遍历的区别
1.Iterator能遍历List或Set,ListIterator只能遍历List。
Iterotor对象可以调用三个方法:
1.hasNext()用来判断序列中接下来还有没有元素
2.next()用来得到下一个元素,如果是第一次调用,返回的是第一个元素
3.remove()用来移除当前遍历到的元素
2.ListIterator的接口是Iterator,除了有这三个方法,还增加了一些别的功能:
1.add()用来添加元素
2.set()用来替换元素
3.hasPrevious()用来判断有没有前一个元素
4.previous()得到前一个元素
5.nextIndex()得到下一个元素的索引
6.previousIndex()得到前一个元素的索引
所以,通过这些方法也可以看出,Iterator的遍历是单向的,只能向前遍历;ListIterator的遍历可以是双向的,可以向前或向后遍历。
六、Set 接口
Set接口:继承了Collection接口,存储元素不可重复,无序,有两个实现类HashSet和TreeSet。
对于HashSet而言,它是基于HashMap实现的。
HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单。
相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成
public class HashSetDemo1 {
public static void main(String[] args) {
/*HashSet<Integer>set=new HashSet<>(); //无序不可重复, 哈希表+链表
set.add(1);
set.add(5);
set.add(10);
set.add(6);
set.add(15);
set.add(1);
set.add(1);*/
/* hashset添加时如何判断值是否重复:
添加时会调用hashCode(),equals()方法;
比较时既要保证内容是否相等,也要保证安全;
先调用hashCode()(速度快)计算哈希值,然后比较哈希值是否相同,如果相同在调用equals()方法比较(hashCode()方法可能不安全)*/
/* System.out.println(set);
int a="通话".hashCode();
int b="重地".hashCode();
System.out.println(a==b); //hashCode()不安全,得出true*/
}
}
TreeSet实现了SortedSet接口,它是一个有序的集合类,TreeSet的底层是通过TreeMap实现的。
TreeSet并不是根据插入的顺序来排序,而是根据实际的值的大小来排序。
TreeSet也支持两种排序方式:
1.自然排序
2.自定义排序
public class TreeSetDemo1 {
public static void main(String[] args) {
//可以给Set集合中的元素进行指定方式的排序。存储的对象必须实现Comparable接口。 TreeSet底层数据结构是二叉树(红黑树是一种自平衡的二叉树)
TreeSet<Integer>t1=new TreeSet<>();
t1.add(1);
t1.add(2);
t1.add(5);
t1.add(4);
t1.add(3);
t1.add(1);
System.out.println(t1);
TreeSet<Student>t2=new TreeSet<>();
//Exception in thread "main" java.lang.ClassCastException:
// com.ffyc.javacollection.day2.Student cannot be cast to java.lang.Comparable
//没有实现Comparable接口,TreeSet底层是二叉树,需要排序,自定义类需要自己实现comparable接口;
Student s1=new Student("张三",18);
Student s2=new Student("李四",19);
Student s3=new Student("王五",20);
Student s4=new Student("张三",18);
t2.add(s1);
t2.add(s2);
t2.add(s3);
t2.add(s4);
System.out.println(t2);
for(Integer t:t1){ //增强for循环
System.out.print(t+" ");
}
System.out.println();
Iterator<Integer>it=t1.iterator();
while(it.hasNext()){
System.out.print(it.next()+" ");
}
System.out.println();
Stream<Integer>s=t1.stream();
/* s.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer t) {
System.out.print(t+" ");
}
});
System.out.println();*/
s.forEach(t-> System.out.println(t));
}
}
Set接口的三种遍历方式:
1.增强for循环
for(Integer t:t1){ //增强for循环
System.out.print(t+" ");
}
2.迭代器遍历
Iterator<Integer>it=t1.iterator();
while(it.hasNext()){
System.out.print(it.next()+" ");
}
3.流遍历
Stream<Integer>s=t1.stream();
s.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer t) {
System.out.print(t+" ");
}
});
//结合lambda表达式。
s.forEach(t-> System.out.println(t));
七、Map 接口
Map接口:以键值对的形式存储,不能包含重复的键,每个键只能对应一个值。
Map接口常用方法:
put(K key, V value) 放入一对键值。
get(Object key) 返回键所映射的值。
isEmpty() 如果此map不包含键值映射,则返回 true 。
keySet() 以Set视图返回此map中包含的键。
entrySet() 以Set视图返回此map的键值对。
clear() 删除所有的映射。
Map接口主要有三个实现类:HashMap、TreeMap、HashTable。
HashMap:key不能重复,排列顺序不固定,可以存储一个值为null的键
TreeMap:所有的元素保持某种固定的顺序
如果想得到一个有序的Map就应该使用TreeMap,key必须实现Comparable。
HashTable:实现了线程同步(安全的),不能存储为null的键。
HashMap底层实现:
public class HashMapDemo1 {
public static void main(String[] args) {
/* Map接口常用方法
V put(K key,V value)
V remove(Object key)
void clear() boolean
containsKey(Object key)
boolean containsValue(Object value)
boolean isEmpty()
int size()
V get(Object key)
Collection<V> values()
Set<K> keySet()
Set<Map.Entry<K,V>> entrySet()*/
/*
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.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 {
HashMap.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 HashMap.TreeNode)
e = ((HashMap.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;
}*/
HashMap<String,String> myMap=new HashMap<>();
//HashMap中元素的key值不能重复, 排列顺序是不固定的,可以存储一个为null的键
//底层数据结构是哈希表+链表+红黑树
//用key算出哈希值,用哈希值计算元素在哈希表中的位置,将元素添加到对应的位置,当有重复位置的元素加进来时,以链表的
//的形式存储(避免哈希冲突的解决方法,拉链法)
//jdk8以后,做了优化,当链表长度为8时,自动转换为红黑树
//哈希表默认初始长度是16.负载因子0.75
//每次处罚扩容机制时,扩容为原来的2倍
myMap.put("1","a");
myMap.put("2","a");
myMap.put("1","b"); //会替代前面1的建值
myMap.put("3","c");
System.out.println(myMap);
myMap.remove("1"); //如果存在(从可选的操作),从该地图中删除一个键的映射。
System.out.println(myMap);
// myMap.clear(); //从该地图中删除所有的映射(可选操作)
System.out.println(myMap.containsKey("2")); //如果此映射包含指定键的映射,则返回 true 。
System.out.println(myMap.containsValue("d")); //如果此地图将一个或多个键映射到指定的值,则返回 true 。
System.out.println(myMap.isEmpty()); //如果此地图不包含键值映射,则返回 true 。
System.out.println(myMap.size());
System.out.println(myMap.get("5")); //返回到指定键所映射的值,如果键不存在返回null
for(String t:myMap.keySet()){
System.out.println(t+":"+myMap.get(t));
}
}
}
TreeMap底层实现:
TreeMap是一个有序的key-value集合,基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建时提供的Comparator进行排序
public class TreeMapDemo1 {
public static void main(String[] args) {
//TreeMap 适用于按自然顺序或自定义顺序遍历键(key)。
// TreeMap根据key值排序,key值需要实现Comparable接口, 重写compareTo方法。
// TreeMap根据compareTo的逻辑,对 key进行排序。
// 键是红黑树结构,可以保证键的排序和唯一性
TreeMap<String,String>myTree=new TreeMap<>();
/* public V put(K key, V value) {
TreeMap.Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new TreeMap.Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMap.Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
TreeMap.Entry<K,V> e = new TreeMap.Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}*/
myTree.put("1","a");
myTree.put("2","b");
myTree.put("3","c");
myTree.put("1","d");
System.out.println(myTree);
/* Map集合遍历
方式1:根据键找值
获取所有键的集合
遍历键的集合,获取到每一个键
根据键找值
方式2:根据键值对对象找键和值
获取所有键值对对象的集合
遍历键值对对象的集合,获取到每一个键值对对象
根据键值对对象找键和值*/
/*TreeMap中所有的元素都保持着某种固定的顺序
如果需要得到一个有序的Map就应该使用TreeMap
key值所在类必须实现Comparable接口*/
Set<String> mySet=myTree.keySet();
for(String t: mySet){
System.out.print(t+":"+myTree.get(t)+" ");
}
Set<Map.Entry<String,String>>myEntry=myTree.entrySet();
for(Map.Entry<String,String>t:myEntry){
System.out.print(t.getKey()+":"+t.getValue()+" ");
}
myTree.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String s, String s2) { //K V
System.out.print(s+" "+s2+" ");
}
});
}
}
Map集合遍历:根据键找值或者根据键值对找键和值。
1.根据键找值
1.获取所有键的集合
2.遍历键的集合,获取每一个键
3.根据键找值
Set<String> mySet=myTree.keySet();
for(String t: mySet){
System.out.print(t+":"+myTree.get(t)+" ");
}
2.根据键值对找键和值。
1.获取所有键值对对象的集合。
2.遍历键值对对象集合,获取每一个键值对对象。
3.根据键值对找键和值。
Set<Map.Entry<String,String>>myEntry=myTree.entrySet();
for(Map.Entry<String,String>t:myEntry){
System.out.print(t.getKey()+":"+t.getValue()+" ");
}
HashTable跟HashMap的区别:
1.HashTable是线程安全的,即HashTable的方法都提供了同步机制。HashMap不是线程安全的,即不提供同步机制 ;
2.HashTable不允许插入空值,HashMap允许
线程安全:多个线程每次运行结果和单线程运行的结果是一样的,就是线程安全的。 一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换,不会导致该接口的执行结果存在二义性,也就是不用考虑同步的问题。
线程不安全:是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
补充(这几个特别容易记混):
1.StringBuffer是线程安全,而StringBuilder是线程不安全的
2.Vector的方法都是同步的,是线程安全的,而ArrayList的方法不是
八、Collections类
Collections:集合类的工具类,与数组的工具类相似,里面包装了操作各种集合的方法。
常用方法:
1.将所有指定的元素添加到指定的集合。
addAll(Collection<? super T> c, T... elements)
2.使用二叉搜索算法搜索指定对象的指定列表
binarySearch(List<? extends Comparable<? super T>> list, T key)
3.根据其元素的自然顺序对指定的列表进行排序。
sort(List<T> list)
4.根据指定的比较器引起的顺序对指定的列表进行排序。
sort(List<T> list, Comparator<? super T> c)
5.反转指定列表中元素的顺序。
reverse(List<?> list)
6.将列表中一个指定值的所有出现替换为另一个。
replaceAll(List<T> list, T oldVal, T newVal)
public class CollectionsDemo1 {
public static void main(String[] args) {
ArrayList<String>myList=new ArrayList<>();
Collections.addAll(myList,"d","b","c","a"); //可变长度的参数,本质是数组,一个参数列表只有一个
Collections.sort(myList, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
System.out.println(myList);
Collections.swap(myList,0,1);
ArrayList<String>myList2=new ArrayList<>();
Collections.addAll(myList2,"a","a","a");
Collections.copy(myList,myList2);
System.out.println(myList);
List<Object> myList3=Collections.emptyList(); //返回空列表(immutable) 不能添加数据
Collections.fill(myList,"b");
System.out.println(myList); //用指定的元素代替指定列表的所有元素。
Collections.replaceAll(myList,"b","c"); //将列表中一个指定值的所有出现替换为另一个。
System.out.println(myList);
System.out.println(Collections.max(myList)); //根据指定的比较器引发的顺序返回给定集合的最大元素。
Collections.addAll(myList,"d","e");
Collections.reverse(myList);
System.out.println(myList); //反转指定列表中元素的顺序。
}
}
注意:Collection 和Collections的区别
1、java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
2、Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
总结
本文的内容较多,重点是Collection接口和Map接口以及他们的实现类,对于本章内容我们应该借助集合底层的数据结构来加深理解,对不同集合之间的区别和联系应该重点掌握。