5.1 Java类集框架
动态数组存在的问题:如果采用动态的数组的办法,那么传统的做法就是每次都要声明一个新的而且存储空间更大的数组,而后将原始数组的内容拷贝到新数组之中,随后在进行引用的配置修改。如果采用这样的处理方案,在数据存储持续加大的应用环境下,就需要重复的产生无数个体量庞大的垃圾空间,这样不仅会占用大量的内存空间,也必然会使得GC的性能严重下降,从而影响到整个应用的性能,最终导致应用出现问题。
5.2 Collection集合接口
Collection是JDK中提供的几种常用集合框架中的父类,其内部存储的对象数据类型是通过Object类型(1.2没有泛型技术之前)进行存储的,这样一个集合之中就有可能存储各类数据对象,在进行操作的时候容易引发ClassCastException
以下是Collection接口的常用方法
- boolean add(E e):向集合中追加单个数据
- boolean addAll(Collection<? extends E> c):向集合中追加一组数据,解释一下:
(Collection<? extends E> c)是啥东西,首先c是一个Collection对象,其内存储的对象类型可以是E的子类,或者直接就是E - void clear():清空集合中的数据
- cotains(Obejct o):判断集合中是否存在指定的内容,需要
equals()支持,言外之意是要重写equals()方法,或者equals()能够提供比较的依据 - cotainsAll(Collection<?> e):判断某个集合中的内容是否存在
- isEmpty():判断集合是否为空(没有保存任何数据)
- Iterator
<E>iterator():获取iterator接口对象实例 - boolean remove(Object o):从集合中删除数据,需要
equals()方法的支持 - int size():返回集合中保存的元素个数
- Object[] toArray():将集合中的数据转换为对象数组
5.3 List集合
List是一个最为常用的数据存储接口,其最大的存储特点在于允许保存重复的数据内容,该接口也是Collection接口的子类,并且对Collection接口所提供的方法进行了扩充
扩充的方法名称
- void add(int index,E element):值指定索引位置添加元素
- E get(int index):根据索引获取保存的数据
- int indexOf(Object o):获取指定数据的索引位置
- ListIterator
<E>listIterator():获取ListIterator接口对象实例 - E set(int index,E element):修改指定索引位置的数据
- **default sort(Comparator<? super E> c) **:使用特定比较器实现排序操作
- **List< E > subList(int fromIndex,int toIndex) **:截取子集合
- static < E > List< E > of(E… elements):通过给定元素创建List集合,注意方法所得到的表集合只能够进行存储和查询,而增加和删除是无法实现的
List是一个允许重复存储的操作接口,如果想要通过其实现具体的数据保存、修改、查找、查询等功能,那么就需要依靠相关的实现类来实现的,在List集合常用的子类有:ArrayList、LinkList、Vector
5.4 ArrayList
ArrayList是List接口的子类,其内部是依靠对象数组实现的所以在使用此类之前一般需要预估其可能存储的数量大小,这样就可以避免数组重复开辟所带来的额外性能消耗。
ArrayList子类在进行数据保存的时候是基于数组结构进行的存储,这样本身就存在着长度限制,而为了解决这种长度限制就需要进行对象数组的扩充,而这一操作就是由grow()方法实现的,源码:
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);//这时候就需要一个新的数组最大长度,可见如果不够那他就会直接给一个int类型的最大值
// 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;
}
- 由于默认情况下处理的对象数组是一个保存长度为0的对象数组,所以在使用
grow()方法进行处理的时候,首先要判断是否为新数组,如果是新数组,则根据设置的minCapacity参数进行新数组的定义。而如果不是新数组,则会依据指定的规则来计算新的数组长度,随后生成的新数组并进行原始数据的拷贝
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 static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//如果当前是一个空数组
return Math.max(DEFAULT_CAPACITY, minCapacity);//那么就比较10和给定的大小
}
return minCapacity;//返回较大
}
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0 //0不需要扩容
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;//默认扩容
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
ArrayList类内部存储的是Object[]对象数组,所以其初始构造方法需要进行对象数组的初始化,r默认的无参构造方法定义了一个空对象数组(不是null)
5.5 自定义对象存储
- 使用
List集合除了可以保存提供的类对象实例之外,也可以根据需要实现自定义类对象的存储方法,而想要让类集可以实现正确的数据删除操作,此时就必须在该类中提供equals()
class Book { // 自定义类
private String title; // 图书名称
private String author; // 图书作者
private double price; // 图书价格
public Book(String title, String author, double price) { // 构造方法
this.title = title;
this.author = author;
this.price = price;
}
@Override
public boolean equals(Object obj) { // 对象比较
if (this == obj) { // 引用地址相同
return true; // 同一对象
}
if (!(obj instanceof Book)) { // 对象不是指定类实例
return false; // 对象不同
}
Book book = (Book) obj; // 获取Book类实例
return this.title.equals(book.title) && this.author.equals(book.author)
&& this.price == book.price; // 属性相等判断
}
public String toString() {
return "【图书】名称:" + this.title + "、作者:" + this.author + "、价格:" + this.price;
}
}
public static void main(String[] args) throws Exception { // 沐言科技:www.yootk.com
List<Book> all = new ArrayList<>(); // 实例化List接口对象
all.add(new Book("Java就业编程实战", "李兴华", 99.8)); // 数据保存
all.add(new Book("Spring就业编程实战", "李兴华", 97.9)); // 数据保存
all.add(new Book("Netty就业编程实战", "李兴华", 98.6)); // 数据保存
all.remove(new Book("Netty就业编程实战", "李兴华", 98.6)); // 数据删除
all.forEach(System.out::println); // 迭代输出
}
5.6 LinkedList子类
LinkedList提供了List接口的链表的实现方案,在该子类之中是基于Node节点关联关系的形式实现了数据的存储,所以相比较ArrayList来讲,LinkList不需要进行数组扩容与拷贝的处理操作
- 链表索引数据返回
LinkedList不像ArrayList基于数组进行实现(ArrayList直接基于数组索引即可返回数据),所以在获取指定索引数据时,实际上都是需要通过节点迭代的形式处理的,其内部是一条双链表
- 其定位链表节点的时候是折半定位的
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
5.7 Vector子类
Vector是1.0版本提出的动态数组类型,在1.2版本中实现了List接口,Vector与ArrayList的继承结构是相同的,而且Vector内部也通过一个对象数组的形式来实现数据的存储,所有的子类最终都向List接口转型。
所以这样的话要谈一下Vector与ArrayList的区别
ArrayList和Vector都通过对象数组的形式来实现数据的存储,但是在Vector类之中所有的集合操作方法全部使用synchronized来进行定义,因此Vector是线程安全的,而ArrayList是基于异步处理的,所以ArrayList的处理性能较好,但是线程不安全
5.8 Set集合
Set是Collection子接口,该接口与List接口最大的不同在于,其保存的数据都是有序存储的,并且不能保存重数据,该接口并没有像List接口那样对Collection接口功能进行扩充,所以其只能使用标准的方式来实现集合数据处理
- Set无法进行索引操作
List接口提供set()实现指定索引数据的修改,而利用get()方法可以获取指定的索引数据,但是Set接口并没有提供这些方法,所以Set无法像List那样通过循环的形式来进行数据操作
5.8.1 HashSet子类(哈希无序存储)
HashSet是一种数据哈希存储的数据集合,最终在HashSet中保存的数据顺序和存储时的顺序不同,而是会基于一定的算法阿进行存储位置的计算,而后根据最后的计算结果实现数据的存储。
HashSet是Set接口常用的实现类,其内部存储的数据不允许出现重复,重复的元素只会保留一个
可以使用LinkedHashSet实现有序存储
LinkedHashSet是HashSet的子类,直接实现了Set接口,此类与HashSet最大不不同在于,其保存数据的顺序为添加顺序
5.8.2 TreeSet子类(排序存储)
通过类的定义可以发现,TreeSet实现类继承自AbstractSet父类,同时实现了NavigableSet父接口,需要注意的是,此时出现的NavigableSet接口是SortedSet子接口(该接口实现排序存储),而SortedSet又是Set子接口。
5.8.3 HashSet实现原理
排序处理
TreeSet可以实现数据的排序以及重复数据的剔除操作,而这一功能是通过Comparable接口来实现的,如果在TreeSet中存储的是自定义的非原生的程序类,则该类必须手工实现Comparable接口,否则无法正常存储,实现Comparable接口必须实现CompareTo方法,该方法也可以为TreeSet剔除重复元素提供依据
重复元素的判断
TreeSet可以依靠Comparable接口来实现对重复对象数据的判断,但是这种操作并不适合于HashSet集合。主要是因为HashSet内部采用哈希算法实现了数据存储(不需要排序,所以Comparable接口对HashSet无效),因为其内部根本就没有用到排序的地方。
哈希算法:在集合内部创建若干个数据存储区域,每当由新的对象存储的时候,可以根据哈希码(HashCode)来确定其保存的分区位置,这样所有的数据分到了若干个存储区域,要想判断对象是否存储,通过指定的存储区域进行数据的比较即可。
为了实现这种HashSet的分区存储机制,在Object类中提供了hashCode()和equals()两个方法,hashCode()的功能主要是依据所有的对象属性计算出一个唯一的整形编码用于进行数据的存储分区确定。当确定了存储分区之后,再通过equals()方法进行指定分区中的所有数据内容判断,以确定其是否与已保存的数据重复,如果存在equals()返回true,则不进行存储,否则进行存储
哈希码不要相同
哈希码决定了HastSet中的对象存储区域,一旦哈希码相同则意味着所有的对象都将保存在同一个区域之中,这样随着数据存储量的增加,equals()判断时间必然越来越长,最终导致应用程序的性能出现问题
不管哈希码是否相同,在进行HashSet数据存储时都要调用equals()方法进行判断,所以真正决定存储的是equals()方法的返回值,而哈希码只是一种性能提升的手段
5.9 集合输出
这些集合本质上就是容器,容器除了具有存储的功能之外,还需要支持数据的取出,这样才可以实现数据的操作,在JAVA类集中对获取集合数据有4种支持,分别是Iterator迭代输出、ListIterator双向迭代输出,Enumeration枚举输出,foreach输出
5.9.1 Iterator迭代输出
Iterator是集合输出之中最为常用的一个接口,该接口最为重要的功能就是实现集合数据的迭代输出,通过此接口的iterator()方法可以获取Iterator接口实例
接口方法:
- boolean hasNext():判断是否具有下一个内容
- E next():获取内容
- default void remove():删除内容
迭代的时候不要使用Collection提供的remove()方法
在进行集合数据迭代数据处理的过程中,如果要删除某些数据,不要直接通过集合对象.remove()的方法进行操作,这是因为该方法会破坏已经存在的集合存储结构,从而导致迭代操作失败,正确的做法应该是使用Iterator中的remove()方法
5.9.2 ListIterator双向迭代输出
Iterator的迭代操作只能实现由前向后的遍历输出,而ListIterator接口在Iterator的基础上追加了双向迭代的支持。获取ListIterator接口的对象实例只能通过List接口提供的listIterator()来实现,所以说,并不是所有的集合都可以实现双向迭代
ListIterator接口扩充的方法
- void add(E e):向集合中添加数据
- boolean hasPrevious():判断是否有前一个元素
- E previous:获取前一个元素
- void set(E e):修改当前元素数据
5.9.3 Enumeration枚举输出
Enumeration是一个枚举输出接口,其主要实现的是Vector集合中数据输出处理,Vector类提供的elements()方法可以获取此接口对象的实例,Enumeration接口的方法如下
- default Iterator
<E>asIterator():将Emumeration转为Iterator接口对象实例 - boolean hasMoreElements():判断是否有下一个元素
- E nextElement():获取当前元素
5.9.4 foreach输出
foreach除了可以通过其实现数组内容的输出之外,也可以通过其实现类集的迭代输出操作
数组实现foreach的输出依靠了foreach对数组索引的控制,这样可以有效地避免因为操作不当而出现的数组越界异常。
集合数据能够通过foreach的输出依靠了Iteable接口的支持,所以如果一个自定义的类对象需要通过foreach进行输出,其必须满足:
5.10 Map集合
Collection是单指集合的操作接口标准,在Collection集合中所保存的数据主要是进行迭代的获取操作,而除了这种集合之外,还提供了一个Map集合,该集合是一种用于存储二元偶对象的集合接口,所谓的二元偶对象指的是每次数据存储时都需要存储key和value两个数据的内容,所谓的二元偶对象指的是在每次数据存储的时候需要存储key和value两个数据内容,在进行集合数据操作的时候,一般都是通过key来找到与之匹配的value
常用方法
public void clear():清空Map集合
public boolean containsKey(Object key):判断指定的key数据是否存在
public boolean containsValue(Object value):判断指定的value数据是否存在
public Set<Map.Entry<K,V>> entrySet():将Map集合转为Set集合
public Set
public Collection
public static <K,V> Map.Entry<K,V> entry(K k, V v):创建Map.Entry接口对象
public V remove(Object key):根据key删除Map集合中的数据
public static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2):创建静态map集合,该方法重载多次
5.10.1 HashMap
HashMap是基于哈希算法实现的一个二元偶兑现集合存储类,也是在实际项目开发中使用较多的一个类
- Map在进行数据存储时需要使用
put()方法,该方法需要接收与Map接口泛型定义一致的数据内容,如果此时存储的KEY不存在,则put()方法会将内容保存在Map集合之中,同时返回null,如果此时存储的Key存在,则使用新的数据替换已有数据,同时将旧数据返回给用户
使用 Map实现的数据存储的最为核心的功能是有效的数查询,可以根据KEY返回对应的VALUE,这一功能可以通过Map接口提供的get()方法来实现,注意,如果KEY是NULL的话,也是可以存值的
5.10.2 HashMap实现原理分析(重点,难点)
- HashMap对象的初始化
在使用HashMap时往往是通过构造方法来进行对象实例化处理的,HashMap类定义了多个构造方法,其中无参构造方法会使用一个默认的扩充阈值初始化,而有参构造方法可以由用户自定义扩充阈值,以下是关键源码
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)//不合法的容量截断
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//初始化容量超过保存上限,保护
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//扩容比率传递错误
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,默认容量为16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;//保存的最大容量为1<<30
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的扩容比率
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;//树状转换触发容量
在使用默认无参构造方法进行实例化的时候,会使用默认的扩容比率来实现容量的扩充(当HashMap存储数据空间不足的时候,需要进行存储容量的扩充),在使用有参构造方法的时候,也可以由用户自定义扩充阈值,而单参构造方法调用了双参构造方法。
- HashMap数据存储
在HashMap之中最为重要的一点就是通过put()方法来进行数据的存储,在数据量较小的情况下,HashMap会通过链表的结构保存所添加的数据内容
但是在数据量较大的时候依然采用链表的形式来进行数据存储,那么最终就会带来一个严重的查询性能问题,导致get() 方法调用的查询复杂度攀升,所以在HashMap设计的时候提供有一个TREEIFY_THRESHOLD(树状转换阈值),当存储的数据的个数超过该阈值的时候,就会触发这个机制,将链表转化为红黑树,利用红黑树的平衡修复能力,实现数据的快速查询,而这一切的转换处理都是由put()以及相关的处理方法进行的
- 哈希桶存储扩容
在HashMap之中,数据的存储结构采用的是数组+链表(红黑树)。其中的数组就是HashMap中的table属性,该属性描述的是一个哈希桶的概念
每一组数据进行保存的实时,都会根据KEY计算出一个哈希码,而后依据此哈希码找到一个匹配的哈希桶进行存储,由于可能存在哈希冲突,所以在HashMap之中将通过链表的形式进行存储,而每一个哈希桶中都保存有该链表的根节点,当链表数据过多的时候(超过8个)则会自动转为红黑树进行存储,所以随着数据量增加,不仅仅红黑树的结构会保存得较多,同时哈希桶也需要进行容量的扩充,才可以有效的进行哈希分区的存储,而这一功能在HashMap中是通过resize()方法进行实现的
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//保存已经有的哈希表
int oldCap = (oldTab == null) ? 0 : oldTab.length;//获取已有的哈希桶的保存长度
int oldThr = threshold;//获取当前的存储容量
int newCap, newThr = 0;//定义新的数组大小和新的存储容量
if (oldCap > 0) {//已有数组长度>0
if (oldCap >= MAXIMUM_CAPACITY) {//已有容量大于保存上限
threshold = Integer.MAX_VALUE;//容量截断为Int的最大值
return oldTab;//将数据原封不动的返回回去
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//判断是否可以进行容量扩充
newThr = oldThr << 1; // double threshold//容量成倍增长
}//当前数组容量 == 0(待初始化) 当前存储容量大于0,而且规定了存储容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//当没有规定存储容量的时候,就用自定义的
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//初始化容量设置
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//红黑树的实现,具体见数据结构的课程
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
- 读取以上源码,
resize()方法可以清楚地发现,哈希桶达到其存储阈值的(75%使用率)时会自动扩容至原来的2倍,扩容时会创建一个新的数组,所以需要将已有的数组保存到新数组之中才可以完成最终的扩容处理
5.10.3 LinkedHashMap
HashMap由于采用了哈希方式进行了数据的存储,所以在默认情况下所有的数据都是无序的,但是在一些特殊要求的业务要求下,开发者希望数据可以按照设置的顺序进行存储,此时则可以利用LinkedHashMap实现类
5.10.4 TreeMap
- TreeMap子类是一个可以依据存储的key的自然顺序实现数据有序存放的Map集合类型,其需要key所在的类实现
Comparable接口,并依据其内部的compareTo()方法实现排序。
5.10.5 HashTable
- HashTable是一个二元偶对象存储类,其实现了一个字典数据的存储与查找类
(可以理解为最初的Map原型)
HashTable本身也采用了哈希存储的方式,所以在HashTable中存储的数据都是无序的,并且不能像HashMap集合那样保存key数据
5.10.6 HashMap与HashTable的区别
- 存储角度上
HashMap与HashTable是两个不同的Map接口的实现类,通过先前的分析可知,HashMap可以在KEY或者VALUE域中保存null,而HashTable 是无法保存的,否则会产生NullPointerException
- 线程安全的角度上
HashTable中的操作方法使用了synchronized同步声明,所以这些方法都属于线程安全的操作,但是性能比较差,而HashMap中的集合操作方法都是基于异步操作实现的,操作性能较高,但是都不是线程安全的
5.10.7 Map.Entry
Map接口提供put()方法实现二元偶对象的保存,在Map集合内部实际上会自动地将这个二元偶对象封装在Node对象之中,并通过一定的算法实现这些节点数据的维护。由于Map接口中有众多实现子类,需要对这一节点的操作形式进行接口的标准化定义,因此在Map接口内部提供了一个Entry接口,而每一个Map子类都会根据自身的需要来创建不同的Entry接口
Map map = new LinkedHashMap();
map.put("1","1");map.put("2","2");
map.entrySet().forEach(System.out::println);
5.10.8 Iterator迭代输出Map集合
操作步骤如下:
[1] 利用Map接口提供的entrySet()方法将Map集合转为Set集合,Set集合中的每一个数据都是一个节点,这些节点都是Map.Entry接口实例
[2] 获取Set接口对象之后,就可以通过iterator()方法来获取Iterator接口对象实例
[3] 利用Iterator实例并结合while循环实现集合的迭代操作,每一次迭代出来的对象都是Map.Entry实例
[4] 利用Map.Entry接口中提供的getKey()和getValue()方法获取key与value
5.10.9 自定义Map集合KEY
在对数据进行Map集合存储的时候,所保存的key和value的类型全部由开发者自行指派,所以除了使用系统内置的类型之外,也可以使用自定义类型对象进行存储
前面已经提到过:其查询的大致流程为依据哈希code获取对象存储的内存区域,然后根据equals()寻找对象
,所以需要重写hashCode()和equals()
5.10.10 HashMap和HashSet关系
HashMap中自定义存储key的操作模式与HashSet中的是完全一致的,实际上是因为HashSet内部是基于HashMap实现的,所有通过Set保存的数据本质上都作为Key保存在HashMap之中,所以HashSet只是HashMap的简化应用
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
5.11 Stack
Stack是Vector的子类,如果要进行栈操作,那么必须使用如下方法
- E push(E item):保存数
- E pop():弹出数据
- boolean empty():栈是否为空
5.12 Queue
队列可以用来实现一种数据缓冲区的操作形式
队列是一种FIFO(先进先出)的数据存储类型,一般分为队首和队尾两个部分。用户的所有数据通过队列尾部追加,而队列头部负责弹出数据
LinkedList其内部是一条双向链表,可以非常轻松地实现头尾的增删查改,是Queue和Deque的实现子类
Queue实现了一个完整的单项队列,而在数据结构中又提供了Deque的双向队列,常用方法有
5.13 Properties
项目开发中除了可以通过集合对象来保存对象之外,也可以利用集合的概念保存一些配置属性的定义,可以通过Properties类来完成,该类是Hashtable的子类,但是只允许操作String类型的数据。
System.getProperties返回的就是Properties对象实例,而list()可以在指定的输出流中进行全部存储属性的列表显示
public static void main(String[] args) throws Exception {
Properties properties = new Properties(); // 创建属性操作类
properties.setProperty("yootk", "www.yootk.com"); // 保存属性
properties.setProperty("edu", "edu.yootk.com"); // 保存属性
properties.store(new FileOutputStream(
new File("H:" + File.separator + "MuYan.properties")),
"MuYan-Yootk-LiXingHua"); // 文件流输出属性内容
}
public static void main(String[] args) throws Exception {
Properties properties = new Properties(); // 创建属性操作类
properties.load(new FileInputStream(
new File("H:" + File.separator + "MuYan.properties"))); // 文件流读取
System.out.println("【获取属性信息】" + properties.getProperty("yootk"));
}
5.14 Collections工具类
Collection工具类可以实现大部分的集合操作
5.15 Stream
- 例程解析
public static void main(String[] args) throws Exception {
List<String> data = new ArrayList<>(); // 实例化List接口
Collections.addAll(data, "Java", "JavaScript", "Python", "Ruby",
"GO", "C", "C++", "Sql"); // 数据存储
Stream<String> stream = data.stream(); // 获取Stream对象
// skip():跳过指定长度的数据项
// limit():要操作数据个数,例如:skip跳到2,则数据获取范围索引:2 ~ 6
// filter():对给定的数据执行过滤,本次是查询数据的中间是否包含有单词“j”或“c”
// collect():将所有满足过滤条件的数据收集成一个新的List集合
List<String> list = stream.skip(2).limit(4).filter((ele) ->
ele.toLowerCase().matches("(.*j.*)|(.*c.*)")).collect(Collectors.toList());
System.out.println(list);
}
本程序通过Collection接口所提供的stream()获取了一个Stream接口实例,而后基于Lambda表达式对集合中的每一个数据进行过滤处理(每个集合项转小写,并且判断其是否含有j或者c)最后将处理过的数据利用collect()方法保存在新的集合之中,可以发现基于Stream操作实现的代码编写都是非方便的
- MapReduce数据分析模型
它可以直接对类集中的数据进行分析处理,它有两个部分:Map(数据处理)和Reduce(统计计算)
class Order { // 描述订单
private String name; // 商品名称
private double price; // 商品单价
private int amount; // 数量
public Order(String name, double price, int amount) { // 构造方法
this.name = name; // 属性保存
this.price = price; // 属性保存
this.amount = amount; // 属性保存
}
// setter、getter、无参构造、略...
}
public class YootkDemo { // 李兴华高薪就业编程训练营
public static void main(String[] args) throws Exception { // 沐言科技:www.yootk.com
List<Order> orders = new ArrayList<>(); // 创建订单集合
orders.add(new Order("《Java就业编程实战》", 69.8, 8000)); // 保存订单
orders.add(new Order("《Spring就业编程实战》", 79.8, 6000)); // 保存订单
orders.add(new Order("《Java架构师就业编程实战》", 59.8, 72821)); // 保存订单
Stream<Order> stream = orders.stream(); // 获取Stream接口实例
DoubleSummaryStatistics result = stream
.filter((ele) -> ele.getName().toLowerCase().contains("java"))
.mapToDouble((orderObject) -> orderObject.getPrice() * orderObject.getAmount())
.summaryStatistics();
System.out.println("【订单总量】" + result.getCount());
System.out.println("【订单总费用】" + result.getSum());
System.out.println("【商品平均价格】" + result.getAverage());
System.out.println("【商品最高价格】" + result.getMax());
System.out.println("【商品最低价格】" + result.getMin());
}
}
- 通过
filter()方法选出要参与运算的所有订单数据 - 对选出来的数据进行Map处理,将每一个订单中的商品数量和价格相乘,可以得到总价
- 通过
summaryStatistics()统计结果,然后用DoubleSummaryStatistics获取统计信息