一、类集框架
1、Java从JDK1.2开始引入类集,对常见的数据结构进行完整的实现包装,提供了一些列的接口与实现子类来降低数据结构所带来的困难。从JDK1.5开始,由于泛型技术的推广,类集也得到了良好的改进,可以直接利用泛型来保存相同类型的数据。从JDK1.8开始类集的实现算法也得到了良好的性能提升。
2、Collection接口
java.util.Collection是单值集合操作最大的父接口,在该接口中定义有所有的单值数据的处理操作,这个接口中的核心操作方法:
向集合保存数据(每次保存一个):boolean add(E e)
向集合中追加一组数据:boolean addAll(Collection<? extends E> c)
清空集合,让根节点为空,同时进行GC处理:void clear()
查询数据是否存在,需要equals()方法支持:boolean contains(Object o)
数据删除,需要equals()方法支持:boolean remove(Object o)
获取数据长度:int size()
将集合变为对象数据返回:Object[] toArray()
将集合变为Iterator接口(输出):Iterator<E> iterator()
在进行集合操作时两个最常用的方法:add()、iterator(从JDK1.5之后提供的Iterator父接口,JDK1.8后进一步扩充)。从JDK1.5之后,如果要进行集合的使用往往 会使用Collection的两个子接口,而不直接使用Collection接口。Collection的两个子接口:List集合和Set集合,特点在于:List子接口允许重复,Set子接口不允许重复。
二、List集合
1、List是Collection子接口,最大的特点是允许保存重复数据,接口定义:public interface List<E> extends Collection<E>
获取指定索引上的数据:E get(int index)
修改指定索引数据:E set(int index,E element)
返回ListIterator接口对象:ListIterator<E> listIterator()
注意:List本身依然属于一个接口,对于接口要想使用一定要通过子类实现。List接口中常用三个子类:ArrayList、Vector、LinkedList。
List接口中的静态方法:
import java.util.List ;
public class JavaDemo {
public static void main(String[] args) {
List<String> all = List.of("hello","hi","你好") ; //返回包含任意数量元素的不可变列表
System.out.println(temp) ; //直接输出
Object result [] = all.toArray() ; //转换为数组对象输出
for(Object temp : result) {
System.out.println(temp) ;
}
}
}
2、ArrayList子类
ArrayList是List子接口使用的最多的一个类。在Java中ArrayList类的定义:public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
List<String> all = new ArrayList<String>() ; //为List父接口实例化(向上转型)
List存储特征:①保存的顺序就是其存储顺序②List集合里面允许存在有重复数据。
集合的直接输出操作时利用了类中的toString()方法,在JDK1.8之后Iterable父接口中定义了forEach()方法输出(非标准输出),方法定义:public void forEach(Consumer<? super E> action),参数为“消费型功能处理”(回顾匿名类与lambda表达式)
all.forEach((str) -> {
System.out.println(str) ;
})
根据ArrayList构造方法定义可以发现,在ArrayList里面所包含的数据实际上就是一个对象数组。在进行数据追加时,如果ArrayList集合里面保存的对象数组长度不够时,会进行新的数组开辟,同时将原始的旧数组内容拷贝到新数组之中。
【重要】其中,新数组的开辟操作:如果在实例化ArrayList类对象的时候没有传递初始化长度,则默认会使用一个空数组,在进行数据增加的时候,若发现数组容量不够,则会判断当前增长的容量与默认的容量大小,选用较大的一个数值进行新的数组开辟。JDK1.9之前,ArrayList默认的构造实际上就会默认开辟长度为10的数组。JDK1.9之后,ArrayList默认的构造会使用默认的空数组,使用的时候才会开辟数组,默认的开辟长度为10,当ArrayList之中保存的容量不足的时候会采用成倍的方式增长。
3、ArrayList保存自定义类对象
List<Person> all = new ArrayList<Person>() ;
all.add(new Person("张三",17)) ;
all.add(new Person("李四",22)) ;
all.forEach(System.out::print()) ; //方法引用代替了消费型的接口
(lambda表达式的简写,System.out是一个PrintStream实例的引用;System.out::println 是对一个实例方法的引用)
在使用List保存自定义类对象时,如需使用contains()、remove()方法进行查询与删除处理的时候必须要在该类中覆写equals()方法:
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true ;
}
if(obj == null) {
return false ;
}
if(!(obj instanceof Person)) {
return false ;
}
Person per = (Person) bdj ;
return this.name.equals(per.name) && this.age == per.age ;
}
4、LinkedList子类:基于链表的实现
继承关系:
List<String> all = new LinkedList<String>() ;
其功能和ArrayList子类一样,但是其内部的实现机制不同,LinkedList子类中没有提供像ArrayList子类那样的初始化大小的方法,提供有无参构造:public LinkedList()创建一个空的List集合。
LinkedList子类中的add()方法,返回值为布尔型,是为了判断传入数据是否为空,如果为null则不进行保存,但是在LinkedList里没有做这样的处理,该方法的实现是:在执行完linkLast(e)后直接返回true,即所有的数据都可以保存包括null。其中,linkLast()方法指在最后一个节点之后追加。在LinkedList类中保存的数据都是利用Node节点进行的封装处理,同时为了提高程序性能,每一次都会保存追加节点为最后一个节点(last),这样可以在增加数据的时候避免递归处理,而后增加数据保存个数。
【重要】ArrayList与LinkedList区别?
ArrayList是数组实现的集合操作,而LinkedList是链表实现的集合操作;
在使用List集合中的get()方法根据索引获取数据时,ArrayList的时间复杂度为O(1),而LinkedList的时间复杂度为O(n);
ArrayList在使用时默认的初始化对象数组的大小长度为10,如果空间不足则会采用2倍的形式进行容量的扩充,保存大数据量时会导致垃圾的产生,使性能下降,此时可选用LinkedList保存。
5、Vector子类
Vector子类从JDK1.0开始就有提供了,之后,考虑到其使用的广泛性,所以类集框架就将其保存了下来,并使其实现了一个List接口。其继承结构同ArrayList:
Vector类实现:
Vector类的无参构造方法默认直接开辟一个数组长度为10的数组,其他操作实现同ArrayList,不同在于Vector类中的操作方法采用的都是synchronized同步处理,而ArrayList并没有进行同步处理,所以Vector类中的方法在进行多线程访问的时候属于线程安全的,但性能不如ArrayList。
三、Set集合
1、集合简介
Set集合的特点是不允许保存重复元素,其也是Collection子接口。在JDK1.9以前Set集合与Collection集合的定义并无差别,Set继续使用Collection接口中的方法操作,在JDK1.9之后,Set集合也像List集合一样扩充了一些static方法,但是并不多,其无法使用List集合中的get()方法(指定索引数据的获取)。Set集合的定义:public interface Set<E> extends Collection<E>
of()方法使用:
Set<String> all = Set.of("hello","hi","你好") ; //进行Set集合数据的保存,当设置有重复元素则会抛出异常
all.forEach(System.out::println); //直接输出
Set集合的常规使用形式一定是依靠子类进行实例化的,所以Set接口中两个常用子类:HashSet与TreeSet
2、HashSet子类
HashSet是Set接口中使用最多的子类,其最大的特点是保存的数据是无序的。其继承关系与LinkedList相似:public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable
实例化:Set<String> all = new HashSet<String>() ;
HashSet子类的操作特点:不允许保存重复元素(Set接口定义的),其保存的数据是无序的。
3、TreeSet子类
TreeSet集合中保存的数据是有序的,按照升序进行自动排序处理。类定义:public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable
class Person implements Comparable<Person> {
// 略
@Override
public int compareTo(Person per) {
if(this.age < per.age) {
return -1 ;
}else if(this.age > per.age) {
return 1 ;
}else {
return this.name.compareTo(per.name) ;
}
}
}
public class JavaDemo {
public static void main(String[] args) {
Set<Person> all = new TreeSet<Person>() ;
all.add(new Person("张三",17)) ;
all.add(new Person("李四",22)) ;
all.add(new Person("李四",22)) ; //重复数据只保存了一个
all.add(new Person("王五",17)) ;
all.forEach(System.out::println) ;
}
}
TreeSet子类中保存的数据是允许排序的,但是这个类必须要实现Comparable接口,因为只有实现了此接口才能确定对象的大小关系。其本质上是利用TreeMap子类实现的集合数据的存储。
在使用自定义类对象进行比较处理时一定要将该类中的所有属性都依次进行大小关系的匹配,否则某一个或某几个属性相同时也会被认为是重复数据,所以TreeSet是利用了Comparable接口来确认重复数据的。当属性较多时,其实现难度较大,所以首选HashSet子类进行存储。
4、分析重复元素的消除
HashSet判断重复元素的方式不是利用Comparable接口完成的,利用的是Object中的方法:
对象编码:public int hashCode()
对象比较:public boolean equals(Object obj)
在进行重复元素判断的时候首先利用 hashCode()进行编码的匹配,如果该编码不存在则表示数据不存在,如果该编码存在则进一步进行对象比较处理,如果发现重复,则数据不允许保存。在eclipse中可以利用开发工具自动创建hashCode()与equals()方法,有效消除重复元素。
四、Map集合
1、Map接口简介
之前集合中Collection接口中保存的数据全部都只是单个对象,而Map集合可以进行二元偶对象的保存(key=value的形式),二元偶对象的核心意义是可以通过key获取对应的value。
在开发中,Collection集合保存数据的目的是为了输出,Map集合保存数据的目的是为了进行key的查找。
Map接口定义:public interface Map<K,V>一个独立的父接口
核心操作方法:
向集合中保存数据:public V put(K key,V value)
根据key查询数据:public V get(Object key)
将Map结合转为Set集合:public Set<Map.Entry<K,V>> entrySet()
查询指定key是否存在:public boolean containsKey(Object key)
将Map集合中的key转为Set集合:public Set<K> keySet()
根据key删除掉指定的数据:public V remove(Object key)
从JDK1.9之后Map接口中也扩充有静态方法:
Map<String, Integer> map = Map.of("one",1,"two",2) ;
System.out.println(map) ;
在Map集合中数据保存形式“key=value”,并且其中的数据不允许重复,内容不允许设置为null,否则会抛出异常。正常开发过程中需要通过接口的子类进行实例化,常用的子类:HashMap、Hashtable、TreeMap、LinkedHashMap。
2、HashMap子类
HashMap(无序存储)是Map接口中最为常见的子类。通过HashMap实例化的Map接口中key与value均可以保存空值,并且可以通过get()方法根据key获取value,当保存数据重复,即key值相同时,后添加的值会替换掉最初添加的值。
但是,对于Map接口中的put()方法本身是提供有返回值的,这个返回值指的是在有重复key的情况下返回旧的value。示例:
System.out.println(map.put("one",1)); //输出null
System.out.println(map.put("one",101)); //输出1
无参构造HashMap()的定义:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; //static final float DEFAULT_LOAD_FACTOR = 0.75f;
}
在使用put()方法进行数据保存的时候会调用一个putVal()方法,同时会将key进行hash处理,即生成一个hash码,在putVal()方法里面会发现依然会提供有一个Node节点类进行数据的保存,并且在使用putVal()方法操作的过程中会调用有一个resize()方法进行容量的扩充。
【重要】在进行HashMap的put()操作时,是如何实现容量扩充的?
在HashMap类中定义有一个“static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;”常量,作为初始化的容量配置,默认大小为16个元素,即可以保存的最大内容是16。
当保存内容容量超过阈值(static final float DEFAULT_LOAD_FACTOR = 0.75f;),相当于“容量*阈值=16*0.75=12”,保存12个元素时就会进行容量的扩充。
在进行扩充的时候,HashMap采用的是成倍的扩充模式,即每一次都扩充2倍的容量。
【重要】解释HashMap的工作原理
在HashMap中依然使用Node类进行数据存储,所以其可以使用的数据结构就是链表(最坏时间复杂度是O(n))和二叉树(O(logn))。
从JDK1.8开始,HashMap为适应大数据时代的海量数据问题其存储出现了改变,并且在HashMap类的内部提供有一个重要的常量:static final int TREEIFY_THRESHOLD = 8;在使用HashMap保存数据时,如果其保存的个数没有超过阈值8,会按照链表的形式进行存储,当超过阈值,则会将链表转为红黑树以实现树的平衡,并利用左旋与右旋保证数据的查询性能。
3、LinkedHashMap子类
LinkedHashMap子类可以使Map集合中保存的数据为其增加顺序,LinkedHashMap子类是基于链表实现的,所以在使用时不要使数据量特别大,否则会使时间复杂度攀升,类定义:public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
Map<String, Integer> map = new LinkedHashMap<>(String, Integer);
4、Hashtable子类
Hashtable类是从JDK1.0的时候提供的,与Vector、Enumeration属于最早的一批动态数组的实现类,为其可以保存下来多实现了一个Map接口,类定义:public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, Serializable
【重要】Hashtable与HashMap的最大区别
HashMap中允许保存有null数据,在Hashtable里面进行数据存储的时候设置的key和value均不允许为空。
HashMap中的方法都属于异步操作(非线程安全),Hashtable中的方法属于同步操作(线程安全)
四、Map.Entry内部接口
Map接口中是如何进行数据存储的?对于List而言(LinkedList子类)是通过链表的形式实现数据存储的,将数据保存在一个Node节点中。而HashMap类中的Node内部类本身实现了Map.Entry接口:static class Node<K,V> implements Map.Entry<K,V>{}
所以可以得出:所有的key和value的数据被封装在Map.Entry接口之中,在整个Map集合中,Map.Entry的主要作用就是作为一个key和value的包装类型使用,该接口定义:public static interface Map.Entry<K,V>
类中的两个重要方法:public K getKey() 和 public V getValue()
从JDK1.9之后,Map接口中追加了创建Map.Entry对象的方法。
Map.Entry<String Integer> entry = Map.Entry("one",1)
五、使用自定义的类来进行Key类型的设置
在用Map的put()方法存储数据时:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在进行数据保存的时候会自动使用传入的key的数据生成一个hash码,即存储时是有hash数值存在的。‘’
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
在根据key获取数据的时依然要将传入的key通过hash()方法来获取其对应的hash码,则查询的过程中首先要利用hashCode()来进行数据查询。其中getNode()方法进行查询时需要使用到equals()方法。
所以,对于自定义key类型所在的类中一定要覆写hashCode()与equals()方法,否则无法查找到存入的数据。
【重要】在使用HashMap进行数据操作时,如果出现了Hash冲突,HashMap是如何解决的?
Hash冲突:Hash码相同
在发生Hash冲突后,为保证程序的正常执行,会在冲突的位置上将所有Hash冲突的内容转为链表保存。