文章目录
Java_集合类库
- 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储,使用Array存储对象方面具有一些弊端,而Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中
- 当需要在Java程序中记录单个数据内容时,则声明一个变量。
- 当需要在Java程序中记录多个类型相同的数据内容时,声明一个一维数组。
- 当需要在Java程序中记录多个类型不同的数据内容时,则创建一个对象。
- 当需要在Java程序中记录多个类型相同的对象数据时,创建一个对象数组。
- 当需要在Java程序中记录多个类型不同的对象数据时,则准备一个集合。
集合的框架结构
- 集合框架必须是高性能的。基本集合(动态数组,链表,树,哈希表)的实现也必须是高效的。
- 集合框架允许不同类型的集合,以类似的方式工作,具有高度的互操作性,并且易扩展
- Java中集合框架顶层框架是:java.util.Collection集合 和 java.util.Map集合。 其中Collection集合中存取元素的基本单位是:单个元素。其中Map集合中存取元素的基本单位是:单对元素。
- 所有集合类都位于
java.util
包下。Java的集合类主要由两个接口派生而出:Collection 和 Map
Collection 和 Map 是 Java
集合框架的根接口,这两个接口又包含了一些子接口或实现类。
Java_Collection集合
基本概念
- java.util.Collection接口是List接口、Queue 接口以及Set接口的父接口,因此该接口里定义的方法既可用于操作List集合,也可用于操作Queue集合和Set集合。
Collection 接口
是最基本的集合接口,一个Collection
代表一组Object
,即Collection
的元素,Collection
接口有三种子类型,List
,Set
,Queue
,再下面是一些抽象类,最后是一些具体实现类,常用的实现类有ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap
等等。Set 接口继承 Collection
,集合元素不重复。List 接口继承 Collection
,允许重复,维护元素插入顺序。List
的具体类为ArrayList和LinkedLisst
Collection集合常用的方法
方法声明 | 功能介绍 |
---|---|
boolean add(E e); | 向集合中添加对象 |
boolean addAll(Collection<? extends E> c) | 用于将参数指定集合c中的所有元素添加到当前集合中 |
boolean contains(Object o); | 判断是否包含指定对象 |
boolean containsAll(Collection<?> c) | 判断是否包含参数指定的所有对象 |
boolean retainAll(Collection<?> c) | 保留当前集合中存在且参数集合中存在的所有对象 |
boolean remove(Object o); | 从集合中删除对象 |
boolean removeAll(Collection<?> c) | 从集合中删除参数指定的所有对象 |
void clear(); | 清空集合 |
int size(); | 返回包含对象的个数 |
boolean isEmpty(); | 判断是否为空 |
boolean equals(Object o) | 判断是否相等 |
int hashCode() | 获取当前集合的哈希码值 |
Object[] toArray() | 将集合转换为数组 |
Iterator iterator() | 获取当前集合的迭代器 |
package com.company;
import java.util.ArrayList;
import java.util.Iterator;
public class Collection {
// 接口方法的测试
public static void main(String[] args) {
ArrayList al = new ArrayList(10);//初始容量为10
al.add("1"); //添加
al.add("1"); //添加
al.add(2); //添加
al.add(3); //添加
al.add(4); //添加
al.add(5); //添加
ArrayList al1 = al;
al1.add(4); //添加
al1.add(5); //添加
int le = al.size(); // 获取有效元素的个数
System.out.println(le);
//是否是空集合
boolean b = al.isEmpty();
System.out.println(b);
// 是通过元素的equals方法来判断是否是同一个对象
boolean b1 = al.contains(al1);
System.out.println(b1);
// 也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
boolean b2 = al.containsAll(al1);
System.out.println(b2);
// 通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
boolean b3 = al.remove("1");
System.out.println(b2);
// 取当前合集的差集
boolean b4 = al.removeAll(al1);
System.out.println(b4);
// 集合是否相等
boolean b5= al.equals(al1);
System.out.println(b5);
// 将集合转换为对象数组
Object ay = al.toArray();
System.out.println(ay);
// 获取集合对象的哈希值
System.out.println(al.hashCode());
// 遍历返回迭代器对象,用于集合遍历
//iterator.hasNext()如果存在元素的话返回true
Iterator<String> iterator = al.iterator();
while(iterator.hasNext()) {
//iterator.next()返回迭代的下一个元素
System.out.println(iterator.next());
}
}
}
Java_Iterator接口
基本概念
- 迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。
- java.util.Iterator接口主要用于描述迭代器对象,可以遍历Collection集合中的所有元素。
- java.util.Collection接口继承Iterator接口,因此所有实现Collection接口的实现类都可以使用该迭代器对象。
- Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
- 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
- 常用的方法
方法声明 | 功能介绍 |
---|---|
boolean hasNext() | 判断集合中是否有可以迭代/访问的元素 |
E next() | 用于取出一个元素并指向下一个元素 |
void remove() | 用于删除访问到的最后一个元素 |
package com.company;
import java.util.ArrayList;
import java.util.Iterator;
public class Interator {
// 迭代器测试
public static void main(String[] args) {
ArrayList al = new ArrayList();
for (int i=0 ;i <10;i++){
al.add(i);
}
Iterator iter = al.iterator();//回到起点
while(iter.hasNext()){
Object obj = iter.next();
if(obj.equals(1)) {
iter.remove();
}
}
System.out.println(al);
}
}
- Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的
remove方法
,不是集合对象的remove方法。
使用foreach循环遍历集合
- 遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
- 不断地从数组/集合中取出一个元素赋值给变量名并执行循环体,直到取完所有元素为止。
for (Object alf:al) {
System.out.println(alf);
}
Java_List集合
-
java.util.List集合是Collection集合的子集合,该集合中允许有重复的元素并且有先后放入次序。List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
-
该集合的主要实现类有:ArrayList类、LinkedList类、Stack类、Vector类。
- ArrayList类的底层是采用动态数组进行数据管理的,支持下标访问,增删元素不方便。
- LinkedList类的底层是采用双向链表进行数据管理的,访问不方便,增删元素方便。
- ArrayList和LinkedList的方法在逻辑上完全一样,只是在性能上有一定的差别,ArrayList更适合于随机访问而LinkedList更适合于插入和删除;在性能要求不是特别苛刻的情形下可以忽略这个差别。
- Stack类的底层是采用动态数组进行数据管理的,该类主要用于描述一种具有后进先出特征的数据结构,叫做栈(last in first out LIFO)。
- Vector类的底层是采用动态数组进行数据管理的,该类与ArrayList类相比属于线程安全的类,效率比较低
-
常用的方法
方法声明 功能介绍 void add(int index, E element) 向集合中指定位置添加元素 boolean addAll(int index, Collection<? extends E> c) 向集合中添加所有元素 E get(int index) 从集合中获取指定位置元素 int indexOf(Object o) 查找参数指定的对象 int lastIndexOf(Object o) 反向查找参数指定的对象 E set(int index, E element) 修改指定位置的元素 E remove(int index) 删除指定位置的元素 List subList(int fromIndex, int toIndex) 用于获取子List
List 实现类之一: ArrayList类
ArrayList
是 List 接口的典型实现类、主要实现类,ArrayList
本质上,ArrayList是对象引用的一个”变长”数组Arrays.asList(…)
方法返回的 List 集合,Arrays.asList(…)
返回值是一个固定长度的 List 集合
List 实现类之二: LinkedList类
- 对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高
LinkedList
:双向链表,内部没有声明数组,而是定义了Node类型的first和last
,用于记录首末元素。同时,定义内部类Node
,作为LinkedList
中保存数据的基本结构。- Node除了保存数据,还定义了两个变量:
prev
变量记录前一个元素的位置next
变量记录下一个元素的位置
List 实现类之三:Vector
- Vector 是一个古老的集合,大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
- 在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢
ArrayList和LinkedList的异同
ArrayList和LinkedList
都线程不安全,相对线程安全的Vector
,执行效率高。ArrayList
是实现了基于动态数组的数据结构,LinkedList
基于链表的数据结构。对于随机访问get和set,
ArrayList觉得优于LinkedList
,因为LinkedList
要移动指针。对于新增和删除操作add
(特指插入)和remove,LinkedList
比较占优势,因为ArrayList
要移动数据。
ArrayList和Vector的区别
Vector和ArrayList
几乎是完全相同的,唯一的区别在于Vector是同步(synchronized)
,属于强同步类。因此开销就ArrayList要大,访问要慢。- 正常情况下,使用
ArrayList而不是Vector
,因为同步完全可以由程序员自己来控制。 Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。
Java_Queue集合
基本概念
- java.util.Queue集合是Collection集合的子集合,与List集合属于平级关系。
- 该集合的主要用于描述具有先进先出特征的数据结构,叫做队列(first in first out FIFO)。
- 该集合的主要实现类是LinkedList类,因为该类在增删方面比较有优势。
Queue(队列)实现了阻塞接口:
- 实现阻塞接口的队列实质上就是一种带有一点扭曲的 FIFO 数据结构。不是立即从队列中添加或者删除元素,线程执行操作阻塞,直到有空间或者元素可用。
常用的方法
方法声明 | 功能介绍 |
---|---|
boolean offer(E e) | 将一个对象添加至队尾,若添加成功则返回true |
E poll() | 从队首删除并返回一个元素 |
E peek() | 返回队首的元素(但并不删除) |
Queue(队列)的实现 _BlockingQueue
- BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。
- BlockingQueue阻塞队列,在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
- BlockingQueue使用时也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
1. 放入数据
- offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程)
- offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
- put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
2. 获取数据
- poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
- poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
- take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
- drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
Queue的ArrayBlockingQueue实现类
- ArrayBlockingQueue在构造时需要指定容量, 并可以选择是否需要公平性,如果公平参数被设置true,等待时间最长的线程会优先得到处理(其实就是通过将ReentrantLock设置为true来 达到这种公平性的:即等待时间最长的线程会先操作)。
- 通常,公平性会使你在性能上付出代价,只有在的确非常需要的时候再使用它。它是基于数组的阻塞循环队 列,此队列按 FIFO(先进先出)原则对元素进行排序。
- ArrayBlockingQueue基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
- ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue
- ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。
Queue的LinkedBlockingQueue实现类
- LinkedBlockingQueue的容量是没有上限的(说的不准确,在不指定时容量为Integer.MAX_VALUE,不要然的话在put时怎么会受阻呢),但是也可以选择指定其最大容量,它是基于链表的队列,此队列按 FIFO(先进先出)排序元素。
- LinkedBlockingQueue基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理
- LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
- 如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
- ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。
Queue的ConcurrentLinkedQueue实现类
- ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。
ConcurrentLinkedQueue使用特点
- 不允许null入列
- 在入队的最后一个元素的next为null
- 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到
- 删除节点是将item设置为null, 队列迭代时跳过item为null节点
- head节点跟tail不一定指向头节点或尾节点,可能存在滞后性
ConcurrentLinkedQueue的实现方法
- offer(E e): 将指定元素插入此队列的尾部。
- poll() 获取并移除此队列的头,如果此队列为空,则返回 null。
- offer是往队列添加元素,poll是从队列取出元素并且删除该元素
- ConcurrentLinkedQueue中的add() 和 offer() 完全一样,都是往队列尾部添加元素
public static void main(String[] args) {
ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
queue.offer("哈哈哈");
System.out.println("offer后,队列是否空?" + queue.isEmpty());
System.out.println("从队列中poll:" + queue.poll());
System.out.println("pool后,队列是否空?" + queue.isEmpty());
}
- peek()获取但不移除此队列的头;如果此队列为空,则返回 null
public static void main(String[] args) {
ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
queue.offer("哈哈哈");
System.out.println("offer后,队列是否空?" + queue.isEmpty());
System.out.println("从队列中peek:" + queue.peek());
System.out.println("从队列中peek:" + queue.peek());
System.out.println("从队列中peek:" + queue.peek());
System.out.println("pool后,队列是否空?" + queue.isEmpty());
}
- remove( object o) 从队列中移除指定元素的单个实例(如果存在)
- remove一个已存在元素,会返回true,remove不存在元素,返回false
public static void main(String[] args) {
ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
queue.offer("哈哈哈");
System.out.println("offer后,队列是否空?" + queue.isEmpty());
System.out.println("从队列中remove已存在元素 :" + queue.remove("哈哈哈"));
System.out.println("从队列中remove不存在元素:" + queue.remove("123"));
System.out.println("remove后,队列是否空?" + queue.isEmpty());
}
- size() 返回此队列中的元素数量
- 如果此队列包含的元素数大于
Integer.MAX_VALUE,
则返回Integer.MAX_VALUE
。 - 需要小心的是,与大多数 collection 不同,size()方法不是一个固定时间操作。由于这些队列的异步特性,确定当前的元素数需要进行一次花费 O(n) 时间的遍历。所以在需要判断队列是否为空时,尽量不要用 queue.size()>0,而是用 !queue.isEmpty()
size() 和 isEmpty() 效率的示例
- 10000个人去饭店吃饭,10张桌子供饭,分别比较size() 和 isEmpty() 的耗时
public class Test01ConcurrentLinkedQueue {
public static void main(String[] args) throws InterruptedException {
int peopleNum = 10000;//吃饭人数
int tableNum = 10;//饭桌数量
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
CountDownLatch count = new CountDownLatch(tableNum);//计数器
//将吃饭人数放入队列(吃饭的人进行排队)
for(int i=1;i<=peopleNum;i++){
queue.offer("消费者_" + i);
}
//执行10个线程从队列取出元素(10个桌子开始供饭)
System.out.println("-----------------------------------开饭了-----------------------------------");
long start = System.currentTimeMillis();
ExecutorService executorService = Executors.newFixedThreadPool(tableNum);
for(int i=0;i<tableNum;i++) {
executorService.submit(new Dinner("00" + (i+1), queue, count));
}
//计数器等待,知道队列为空(所有人吃完)
count.await();
long time = System.currentTimeMillis() - start;
System.out.println("-----------------------------------所有人已经吃完-----------------------------------");
System.out.println("共耗时:" + time);
//停止线程池
executorService.shutdown();
}
private static class Dinner implements Runnable{
private String name;
private ConcurrentLinkedQueue<String> queue;
private CountDownLatch count;
public Dinner(String name, ConcurrentLinkedQueue<String> queue, CountDownLatch count) {
this.name = name;
this.queue = queue;
this.count = count;
}
@Override
public void run() {
//while (queue.size() > 0){
while (!queue.isEmpty()){
//从队列取出一个元素 排队的人少一个
System.out.println("【" +queue.poll() + "】----已吃完..., 饭桌编号:" + name);
}
count.countDown();//计数器-1
}
}
}
contains(Object o) 方法:如果此队列包含指定元素,则返回 true
public static void main(String[] args) throws InterruptedException {
ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
queue.offer("123");
System.out.println(queue.contains("123"));
System.out.println(queue.contains("234"));
}
- toArray() 方法:返回以恰当顺序包含此队列所有元素的数组
- toArray(T[] a) 方法返回以恰当顺序包含此队列所有元素的数组;返回数组的运行时类型是指定数组的运行时类型
public static void main(String[] args) throws InterruptedException {
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();
queue.offer("123");
queue.offer("234");
Object[] objects = queue.toArray();
System.out.println(objects[0] + ", " + objects[1]);
//将数据存储到指定数组
String[] strs = new String[2];
queue.toArray(strs);
System.out.println(strs[0] + ", " + strs[1]);
}
- iterator() 返回在此队列元素上以恰当顺序进行迭代的迭代器
public static void main(String[] args) throws InterruptedException {
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();
queue.offer("123");
queue.offer("234");
Iterator<String> iterator = queue.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
Java_泛型
基本概念
- 集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象在
JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来解决
。因为这个时候除了元素的类型不确定,其他的部分都是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此时把元素的类型设计成一个参数,这个类型参数叫做泛型。 - 通常情况下集合中可以存放不同类型的对象,是因为将所有对象都看做Object类型放入的,因此从集合中取出元素时也是Object类型,为了表达该元素真实的数据类型,则需要强制类型转换,而强制类型转换可能会引发类型转换异常。
- 为了避免上述错误的发生,从Java5开始增加泛型机制,也就是在集合名称的右侧使用
<数据类型>
的方式来明确要求该集合中可以存放的元素类型,若放入其它类型的元素则编译报错。 - 泛型只在编译时期有效,在运行时期不区分是什么类型。
为什么存在泛型
- 在集合中没有泛型的时候,任何类型都可以添加到集合中,类型不安全,而且从集合中读取出来的对象需要强制类型转换.
- 在集合中有泛型时候,只有指定类型才可以添加到集合中,类型安全,而且读出的对象不需要转换类型
- 泛型解决元素存储的安全性问题,好比商品、药品标签,不会弄错。但是获取数据元素时,需要类型强制转换的问题,好比不用每回拿商品、药品都要辨别。
- Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生
ClassCastException
异常。
package com.company;
// Java泛型样例
import java.util.ArrayList;
import java.util.Iterator;
public class Java_FanXing {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();//类型推断
list.add(78);
list.add(88);
list.add(77);
list.add(66);
//遍历方式一:
//for(Integer i : list){
//不需要强转
//System.out.println(i);
//}
//遍历方式二:
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
Map<String,Integer> map = new HashMap<String,Integer>();
map.put("Tom1",34);
map.put("Tom2",44);
map.put("Tom3",33);
map.put("Tom4",32);
//添加失败
//map.put(33, "Tom");
Set<Entry<String,Integer>> entrySet = map.entrySet();
// 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:`<E1,E2,E3>`
Iterator<Entry<String,Integer>> iterator = entrySet.iterator();
while(iterator.hasNext()){
Entry<String,Integer> entry = iterator.next();
System.out.println(entry.getKey() + "--->" + entry.getValue());
}
底层原理
-
Collection<E>,List<E>,ArrayList<E>
这个<E>
就是类型参数, 泛型的本质就是参数化类型,也就是让数据类型作为参数传递,其中E相当于形式参数负责占位,而使用集合时<>中的数据类型相当于实际参数,用于给形式参数E进行初始化,从而使得集合中所有的E被实际参数替换,由于实际参数可以传递各种各样广泛的数据类型,因此得名为泛型。//其中i叫做形式参数,负责占位 其中E叫做形式参数,负责占位 //int i = 10; E = String; //int i = 20; E = Integer; public static void show(int i) { public interface List { ... ... } } //其中10叫做实际参数,负责给形式参数初始化 //其中String叫做实际参数 show(10); List lt1 = ...; show(20); List lt2 = ...;
-
泛型的指定中不能使用基本数据类型,可以使用包装类替换.
class GenericTest {
public static void main(String[] args) {
// 1、使用时:类似于Object,不等同于Object
ArrayList list = new ArrayList();
// list.add(new Date());//有风险
list.add("hello");
test(list);// 泛型擦除,编译不会类型检查
// ArrayList<Object> list2 = new ArrayList<Object>();
// test(list2);//一旦指定Object,编译会类型检查,必须按照Object处理
}
public static void test(ArrayList<String> list) {
String str = "";
for (String s : list) {
str += s + ",";
}
System.out.println("元素:" + str);
}
}
泛型的声明
// interface List<T> 和 class GenTest<K,V> 其中,T,K,V不代表值,而是表示类型。这里使用任意字母都可以。常用T表示,是Type的缩写。
Map<String,Integer> map = new HashMap<String,Integer>();
// 一定要在类名后面指定类型参数的值(类型)
List<String> strList = new ArrayList<String>();`和`Iterator<Customer> iterator = customers.iterator();
- 泛型不同的引用不能相互赋值。尽管在编译时
ArrayList<String>和ArrayList<Integer>
是两种类型,但是,在运行时只有一个ArrayList
被加载到JVM中。 - 泛型如果不指定,将被擦除,泛型对应的类型均按照
Object
处理,但不等价于Object。
即:泛型要使用一路都用。要不用,一路都不要用。
如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
- 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型
- 子类不保留父类的泛型:按需实现,没有类型 擦除
- 子类保留父类的泛型:泛型子类的两种情况:全部保留,部分保留
- 子类除了
指定或保留
父类的泛型,还可以增加自己的泛型
样例代码一
class Father<T1, T2> {
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son1 extends Father {// 等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2 extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2> extends Father<Integer, T2> {
}
样例代码二;
class Father<T1, T2> {
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son<A, B> extends Father{//等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2<A, B> extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2, A, B> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2, A, B> extends Father<Integer, T2> {
}
泛型的核心思想:把一个集合中的内容限制为一个特定的数据类型
泛型方法
- 方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
- 泛型方法的格式:
[访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 抛出的异常
public static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}
public static void main(String[] args) {
Object[] ao = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(ao, co);
String[] sa = new String[20];
Collection<String> cs = new ArrayList<>();
fromArrayToCollection(sa, cs);
Collection<Double> cd = new ArrayList<>();
// 下面代码中T是Double类,但sa是String类型,编译错误。
// fromArrayToCollection(sa, cd);
// 下面代码中T是Object类型,sa是String类型,可以赋值成功。
fromArrayToCollection(sa, co);
}
class Creature{}
class Person extends Creature{}
class Man extends Person{}
class PersonTest {
public static <T extends Person> void test(T t){
System.out.println(t);
}
public static void main(String[] args) {
test(new Person());
test(new Man());
//The method test(T) in the type PersonTest is not
//applicable for the arguments (Creature)
test(new Creature());
}
}
泛型在继承上的体现
public void testGenericAndSubClass() {
Person[] persons = null;
Man[] mans = null;
// 而 Person[] 是 Man[] 的父类.
persons = mans;
Person p = mans[0];
// 在泛型的集合上
List<Person> personList = null;
List<Man> manList = null;
// personList = manList;(报错)
}
- 如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,
G<B>并不是G<A>的子类型!
- String是Object的子类,但是
List<String >并不是List<Object>的子类。
自定义泛型接口
- 泛型接口和普通接口的区别就是后面添加了类型参数列表,可以有多个类型参数,如:<E, T, … >等
自定义泛型类
- 泛型类和普通类的区别就是类名后面添加了类型参数列表,可以有多个类型参数,如:<E, T, … >等。
- 实例化泛型类时应该指定具体的数据类型,并且是引用数据类型而不是基本数据类型。
- 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型
- 子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型。
自定义泛型方法
- 泛型方法就是输入参数的时候,输入的是泛型参数,而不是具体的参数。在调用这个泛型方法的时需要对泛型参数进行实例化。
- 泛型方法的格式:
[访问权限] <泛型> 返回值类型 方法名([泛型标识 参数名称]) { 方法体; }
- 在静态方法中使用泛型参数的时候,需要把静态方法定义为泛型方法
泛型在继承上的体现
- 如果B是A的一个子类或子接口,而G是具有泛型声明的类或接口,则G并不是G的子类型!比如:String是Object的子类,但是List并不是List的子类。
Java_通配符
** 通配符的使用**
- 有时候我们希望传入的类型在一个指定的范围内,此时就可以使用泛型通配符了。如:之前传入的类型要求为Integer类型,但是后来业务需要Integer的父类Number类也可以传入。比如:
List<?> ,Map<?,?>
,List<?>是List<String>、List<Object>等各种泛型List的父类。
- 泛型中有三种通配符形式:
- <?> 无限制通配符:表示我们可以传入任意类型的参数。
- <? extends E> 表示类型的上界是E,只能是E或者是E的子类。
- <? super E> 表示类型的下界是E,只能是E或者是E的父类。
无限制通配符
将任意元素加入到其中不是类型安全的:
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时错误
- 因为我们不知道c的元素类型,我们不能向其中添加对象。
add方法
有类型参数E
作为集合的元素类型。传给add的任何参数
都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。
public static void main(String[] args) {
List<?> list = null;
list = new ArrayList<String>();
list = new ArrayList<Double>();
// list.add(3);//编译不通过
list.add(null);
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
l1.add("尚硅谷");
l2.add(15);
read(l1);
read(l2);
}
public static void read(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
//注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用?
public static <?> void test(ArrayList<?> list){
}
//注意点2:编译错误:不能用在泛型类的声明上
class GenericTypeClass<?>{
}
//注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象
ArrayList<?> list2 = new ArrayList<?>();
有限制的通配符
<?>
允许所有泛型的引用调用通配符指定上限
:上限extends:
使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
- 通配符指定下限:
下限super
:使用时指定的类型不能小于操作的类,即>=
<? extends Number> (无穷小 , Number]
:只允许泛型为Number及Number
子类的引用调用<? super Number> [Number , 无穷大)
:只允许泛型为Number及Number
父类的引用调用<? extends Comparable>
只允许泛型为实现Comparable
接口的实现类的引用调用
public static void printCollection3(Collection<? extends Person> coll) {
//Iterator只能用Iterator<?>或Iterator<? extends Person>.why?
Iterator<?> iterator = coll.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
public static void printCollection4(Collection<? super Person> coll) {
//Iterator只能用Iterator<?>或Iterator<? super Person>.why?
Iterator<?> iterator = coll.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
Java_Set集合
基本概念
- java.util.Set集合是Collection集合的子集合,与List集合平级,并且该集合中元素没有先后放入次序,且不允许重复。
- 该集合的主要实现类是:HashSet类 和 TreeSet类以及LinkedHashSet类。
- 其中HashSet类的底层是采用哈希表进行数据管理的。
- 其中TreeSet类的底层是采用红黑树进行数据管理的。
- 其中LinkedHashSet类与HashSet类的不同之处在于内部维护了一个双向链表,链表中记录了元素的迭代顺序,也就是元素插入集合中的先后顺序,因此便于迭代。
Set
判断两个对象是否相同不是使用==
运算符,而是根据equals() 方法
常用的方法:参考Collection集合中的方法
Set实现类之一:HashSet
HashSet
是Set
接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。HashSet
按Hash
算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
元素放入HashSet集合的原理
- 当向
HashSet
集合中存入一个元素时,HashSet
会调用该对象的hashCode()
方法来得到该对象的hashCode
值,然后根据hashCode
值,再由某种哈希算法计算出该元素在数组中的索引位置。 - 若该位置没有元素,则将该元素直接放入即可
- 若该位置有元素,则使用新元素与已有元素依次比较哈希值,若哈希值不相同,则将该元素直接放入。
- 若新元素与已有元素的哈希值相同,则使用新元素调用equals方法与已有元素依次比较。
- 若相等则添加元素失败,否则将元素直接放入即可。
- 如果两个元素的
equals()
方法返回 true,但它们的hashCode()
返回值不相等,hashSet
将会把它们存储在不同的位置,但依然可以添加成功。
为什么要求重写equals方法后要重写hashCode方法呢?
- 当两个元素调用equals方法相等时证明这两个元素相同,重写hashCode方法后保证这两个元素得到的哈希码值相同,由同一个哈希算法生成的索引位置相同,此时只需要与该索引位置已有元素比较即可,从而提高效率并避免重复元素的出现
package com.company;
import java.util.HashSet;
// HashSet 测试样例
public class Java_18 {
public static void main(String[] args) {
// 添加元素
HashSet<String> sites = new HashSet<String>();
sites.add("1");
sites.add("2");
sites.add("3");
sites.add("4");
sites.add("1"); // 重复的元素不会被添加
System.out.println(sites);
// 使用 contains()方法判断元素是否存在于当前的集合中
System.out.println(sites.contains("3")); // 输出 true
// 使用 remove() 方法来删除指定元素 使用 clear() 删除所有元素
System.out.println(sites.remove("3")); // 输出 true 表示已经成功删除
System.out.println(sites);
// 使用 size 方法可以计算HashSet中元素的数量,
System.out.println(sites.size());
// 使用 for-each遍历HashSet
for (String i : sites) {
System.out.println(i);
}
}
}
Set实现类之二:LinkedHashSet
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
- LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
- LinkedHashSet 不允许集合元素重复。
TreeSet集合的概念
- 二叉树主要指每个节点最多只有两个子节点的树形结构。
- 满足以下3个特征的二叉树叫做有序二叉树。
- a.左子树中的任意节点元素都小于根节点元素值;
- b.右子树中的任意节点元素都大于根节点元素值;
- c.左子树和右子树的内部也遵守上述规则;
- 由于TreeSet集合的底层采用红黑树进行数据的管理,当有新元素插入到TreeSet集合时,需要使用新元素与集合中已有的元素依次比较来确定新元素的合理位置。
- 比较元素大小的规则有两种方式:
- 使用元素的自然排序规则进行比较并排序,让元素类型实现java.lang.Comparable接口;默认情况下,TreeSet 采用自然排序
- 自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
- 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable接口。
- 实现
Comparable
的类必须实现compareTo(Object obj)
方法,两个对象即通过compareTo(Object obj)
方法的返回值来比较大小。
- 使用比较器规则进行比较并排序,构造TreeSet集合时传入java.util.Comparator接口;
- 使用元素的自然排序规则进行比较并排序,让元素类型实现java.lang.Comparable接口;默认情况下,TreeSet 采用自然排序
- 自然排序的规则比较单一,而比较器的规则比较多元化,而且比较器优先于自然排序;
Comparable 的典型实现:
BigDecimal、BigInteger
以及所有的数值型对应的包装类:按它们对应的数值大小
进行比较Character
:按字符的 unicode值来进行比较Boolean
:true 对应的包装类实例大于 false 对应的包装类实例String
:按字符串中字符的 unicode 值进行比较Date、Time
:后边的时间、日期比前面的时间、日期大
定序排序
- TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写
compare(T o1,T o2)方法
。 - 利用
int compare(T o1,T o2)
方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。 - 要实现定制排序,需要将实现
Comparator
接口的实例作为形参传递给TreeSet
的构造器。 - 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
- 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。
public static List duplicateList(List list) {
HashSet set = new HashSet();
set.addAll(list);
return new ArrayList(set);
}
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Integer(1));
list.add(new Integer(2));
list.add(new Integer(2));
list.add(new Integer(4));
list.add(new Integer(4));
List list2 = duplicateList(list);
for (Object integer : list2) {
System.out.println(integer);
}
}
Java_Map集合
基本概念
Map与Collection
并列存在,用于保存具有映射关系的数据。Map 中的 key 和 value
都可以是任何引用类型的数据,- java.util.Map<K,V>集合中存取元素的基本单位是:单对元素,其中类型参数如下:
- K - 此映射所维护的键(Key)的类型,相当于目录。
- V - 映射值(Value)的类型,相当于内容。
- 该集合中key是不允许重复的,而且一个key只能对应一个value。
- 该集合的主要实现类有:HashMap类、TreeMap类、LinkedHashMap类、Hashtable类、Properties类。
- 其中HashMap类的底层是采用哈希表进行数据管理的。
- 其中TreeMap类的底层是采用红黑树进行数据管理的
- 其中LinkedHashMap类与HashMap类的不同之处在于内部维护了一个双向链表,链表中记录了元素的迭代顺序,也就是元素插入集合中的先后顺序,因此便于迭代。
- 其中Hashtable类是古老的Map实现类,与HashMap类相比属于线程安全的类,且不允许null作为key或者value的数值。
- 其中Properties类是Hashtable类的子类,该对象用于处理属性文件,key和value都是String类型的。
- Map集合是面向查询优化的数据结构, 在大数据量情况下有着优良的查询性能,经常用于根据key检索value的业务场景。即通过指定的
key
总能找到唯一的、确定的value
常用方法
方法声明 | 功能介绍 |
---|---|
V put(K key, V value) | 将Key-Value对存入Map,若集合中已经包含该Key,则替换该Key所对应的Value,返回值为该Key原来所对应的Value,若没有则返回null |
V get(Object key) | 返回与参数Key所对应的Value对象,如果不存在则返回null |
boolean containsKey(Object key); | 判断集合中是否包含指定的Key |
boolean containsValue (Object value); | 判断集合中是否包含指定的Value |
V remove(Object key) | 根据参数指定的key进行删除 |
Set keySet() | 返回此映射中包含的键的Set视图 |
Collection values() | 返回此映射中包含的值的Set视图 |
Set<Map.Entry<K,V>> entrySet() | 返回此映射中包含的映射的Set视图 |
package com.company;
import java.util.HashMap;
import java.util.Map;
//map 测试样例
public class Java_20 {
public static void main(String[] args) {
// 定义一个Map的容器对象
Map<String, Integer > map1 = new HashMap<String, Integer >();
map1.put("张三", 20);
map1.put("李四", 18);
map1.put("王五", 17);
map1.put("张龙", 25);
System.out.println(map1);
// 添加重复的键值(值不同),会返回集合中原有(重复键)的值, System.out.println(map1.put("jack", 30)); //20
Map<String, Integer> map2 = new HashMap<String, Integer>();
map2.put("张龙", 100);
map2.put("赵虎", 20);
System.out.println("map2:" + map2);
// 从指定映射中将所有映射关系复制到此映射中。
map1.putAll(map2);
System.out.println("map1:" + map1);
map1.remove("张龙"); // 删除张龙
map1.clear(); // 删除所有元素
}
}
package com.company;
import java.util.HashMap;
import java.util.Map;
//map 测试样例
public class Java_20 {
public static void main(String[] args) {
// 定义一个Map的容器对象
Map<String, Integer > map1 = new HashMap<String, Integer >();
map1.put("张三", 20);
map1.put("李四", 18);
map1.put("王五", 17);
map1.put("张龙", 25);
System.out.println(map1);
// 添加重复的键值(值不同),会返回集合中原有(重复键)的值, System.out.println(map1.put("jack", 30)); //20
Map<String, Integer> map2 = new HashMap<String, Integer>();
map2.put("张龙", 100);
map2.put("赵虎", 20);
System.out.println("map2:" + map2);
// 从指定映射中将所有映射关系复制到此映射中。
map1.putAll(map2);
System.out.println("map1:" + map1);
map1.remove("张龙"); // 删除张龙
map1.clear(); // 删除所有元素
map1.get("张三"); // 获取指定的key 对应的值
// containsValue 指定的 Value 的值
System.out.println( map1.containsKey("账务")); // 判断是否具有指定的 key值
System.out.println( map1.size()); // 查看当前的集合元素个数
System.out.println( map1.isEmpty()); // 判断是否为空
System.out.println( map1.equals(map2)); // 判断当前map和参数对象obj是否相等
}
}
Map实现类之一:HashMap
HashMap是 Map
接口使用频率最高的实现类。允许使用null键和null值
,与HashSet
一样,HashMap
不保证映射的顺序。- 所有的
key
构成的集合是Set:
无序的、不可重复的。所以,key
所在的类要重写:equals(),hashCode()
- 一个
key-value构成一个键值对
,键值对
构成的集合是Set
:无序的、不可重复的 HashMap
判断两个key
相等的标准是:两个key 通过 equals() 方法返回 true,hashCode
值也相等。
HashMap的存储结构
HashMap
的内部存储结构其实是数组+链表+树
的结合- 当实例化一个
HashMap
时,会初始化initialCapacity和loadFactor
,在put
(添加)第一对映射关系时,系统会创建一个长度为initialCapacity
的Node
数组,这个长度在哈希表中被称为容量(Capacity)
,在这个数组中可以存放元素的位置我们称之为“桶”(bucket),
,每个bucket
都有自己的索引,系统可以根据索引快速的查找bucket
中的元素。 - 每个
bucket
中存储一个元素,即一个Node
对象,但每一个Node
对象可以带一个引用变量next
,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链
。也可能是一个一个TreeNode对象
,每一个TreeNode
对象可以有两个叶子结点left和right
,因此,在一个桶中,就有可能生成一个TreeNode
树。而新添加的元素作为链表的last
,或树的叶子结点
元素放入HashMap集合的原理
- 使用元素的key调用hashCode方法获取对应的哈希码值,再由某种哈希算法计算在数组中的索引位置。
- 若该位置没有元素,则将该键值对直接放入即可。
- 若该位置有元素,则使用key与已有元素依次比较哈希值,若哈希值不相同,则将该元素直接放入。
- 若key与已有元素的哈希值相同,则使用key调用equals方法与已有元素依次比较。
- 若相等则将对应的value修改,否则将键值对直接放入即可。
元素放入HashMap具体实现过程
- 向
HashMap中添加entry1(key,value)
,需要首先计算entry1
中key
的哈希值(根据key
所在类的hashCode()
计算得到),此哈希值经过处理以后,得到在底层Entry[]
数组中要存储的位置下标i
。 - 如果位置
i
上没有元素,则entry1
直接添加成功。如果位置i
上已经存在entry2(或还有链表存在的entry3,entry4)
,则需要通过循环的方法,依次比较entry1中key和其他的entry
。如果彼此hash
值不同,则直接添加成功。如果hash
值相同,继续比较二者是否equals
。如果返回值为true
,则使用entry1的value去替换equals为true的entry的value
。 - 如果遍历一遍以后,发现所有的
equals
返回都为false
,则entry1
仍可添加成功。entry1指向原有的entry
元素。
HashMap负载因子
- 负载因子的大小决定了HashMap的数据密度。
- 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数
相关的常量
- DEFAULT_INITIAL_CAPACITY : HashMap的默认容量是16。
- DEFAULT_LOAD_FACTOR:HashMap的默认加载因子是0.75。
- threshold:扩容的临界值,该数值为:容量*填充因子,也就是12。
- TREEIFY_THRESHOLD:若Bucket中链表长度大于该默认值则转化为红黑树存储,该数值是8。
- MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量,该数值是64。
HashMap的扩容
- 当
HashMap
中的元素越来越多的时候,hash
冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap
的数组进行扩容 HashMap
数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
HashMap扩容时间
- 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数,size*loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的默认 值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。
- 默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中threshold值,也叫做临界值)的时候就把数组的大小扩展为 216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,非常消耗性能的操作
- 如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能
扩容样例分析
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
**不要修改基于映射关系的key **
映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。
Map实现类之二:LinkedHashMap
LinkedHashMap
是HashMap
的子类,在HashMap
存储结构的基础上,使用了一对双向链表来记录添加元素的顺序- 与
LinkedHashSet
类似,LinkedHashMap
可以维护Map
的迭代顺序:迭代顺序与Key-Value
对的插入顺序一致
Map实现类之三:TreeMap
- TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。
- TreeMap 可以保证所有的 Key-Value 对处于有序状态。
- TreeSet底层使用红黑树结构存储数据
- TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
Map实现类之四:Hashtable
- Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
- Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
- 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
- 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
- Hashtable判断两个key相等、两个value相等的标准,与HashMap一致
Map实现类之五:Properties
- Properties 类是 Hashtable 的子类,该对象用于处理属性文件
- 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
- 存取数据时,建议使用
setProperty(String key,String value)方法和getProperty(String key)
方法
Java_Collections类
基本概念
- java.util.Collections类主要提供了对集合操作或者返回集合的静态方法。
Collections
是一个操作Set、List 和 Map
等集合的工具类,操作数组的工具类Arrays
常用方法
方法声明 | 功能介绍 |
---|---|
static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) | 根据元素的自然顺序返回给定集合的最大元素 |
static T max(Collection<? extends T> coll, Comparator<? super T> comp) | 根据指定比较器引发的顺序返回给定集合的最大元素 |
static <T extends Object & Comparable<?super T>> T min(Collection<? extends T> coll) | 根据元素的自然顺序返回给定集合的最小元素 |
static T min(Collection<? extends T> coll, Comparator<? super T> comp) | 根据指定比较器引发的顺序返回给定集合的最小元素 |
static void copy(List<? super T> dest, List<? extends T> src) | 将一个列表中的所有元素复制到另一个列表中 |
static void reverse(List<?> list) | 反转指定列表中元素的顺序 |
static void shuffle(List<?> list) | 使用默认的随机源随机置换指定的列表 |
static <T extends Comparable<? super T>> void sort(List list) | 根据其元素的自然顺序将指定列表按升序排序 |
static void sort(List list, Comparator<? super T> c) | 根据指定比较器指定的顺序对指定列表进行排序 |
static void swap(List<?> list, int i, int j) | 交换指定列表中指定位置的元素 |
Java_CopyOnWriteArrayList
- Copy-On-Write 简称 COW ,是一种用于程序设计中的优化策略
COW 基本思路:
- 一开始大家共享同一个内容,当某个人想要修改这个内容的时候,才会真正的把内容复制出去,形成一个新的内容,然后再修改
- 在修改完内容后,在将原容器的引用指向新的容器,这样做可以对
CopyOnWrite
容器进行并发的读写,而不需要加锁,因为原本的容器不会修改内容 CopyOnWrite
容器也是一种读写分离的思想,读和写是不同的容器COW
本质上是一种延时懒惰的策略- 在Java的并发包中提供了两个使用
CopyOnWrite
机制实现的并发容器,CopyOnWriteArrayList
和CopyOneWruteArraySet
CopyOnWrite
容器在很多并发场景中使用
// CopyOnWriteArrayList 样例
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
// CopyOnWrite的应用场景
// CopyOnWrite并发容器用于读多写少的并发场景,比如白名单,黑名单,商品类目的访问和更新场景,
public class Java_80 {
public static void main(String[] args) throws Exception {
List<String> s = new ArrayList<String>();
s.add("1");
s.add("2");
s.add("3");
s.add("4");
final CopyOnWriteArrayList<String> cs = new CopyOnWriteArrayList<String>(s);
Thread t = new Thread(new Runnable() {
int temp = 3;
@Override
public void run() {
while(temp>0){
temp--;
cs.add("----");
System.out.println("输出的为 cs 复制 s 的内容");
System.out.println(cs);
}
}
});
t.setDaemon(true);
t.start();
Thread.currentThread().sleep(1);
for (String outCs :cs){
System.out.println(cs.hashCode()); // 输出CS内每个值的的hash码
System.out.println(s);
}
// 此时可以看到输出hashCode的CS是不是同一CS
}
}
CopyOnWrite容器的注意事项
- 减少扩容开销,根据实际需要,初始化CopyOnWrite容器的大小,避免
CopyOnWrite
自动扩容 - 在往
CopyOnWrite
容器中,添加新的内容时,如果内容已经确定,使用addBlackList
方法进行添加,因为每次添加,容器都会进行复制,所以减少添加的次数,可以减少容器的复制次数,从而避免资源的浪费
CopyOnWrite的缺点
CopyOnWrite
容器有很多优点,但是同时存在两个问题内存占用问题和数据一致问题
内存占用问题:
- 因为
CopyOnWrite
容器是写时复制机制,所以在进行写操作时,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象 - 在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存
- 如果这些旧的对象占用的内存比较大,比如说100M左右,那么再写入100M数据进去,内存就会占用200M,这时候就可能导致服务崩溃
解决内存占用问题的方法
- 针对内存占用问题,可以通过压缩容器中的元素的方法减少大对象的内存消耗,比如,如果使用元素全是十进制的数字,可以考虑将元素压缩成64进制的元素,或者不适用
CopyOnWrite容器
,而使用其他的并发容器,例如ConcurrentHashMap
数据一致性问题:
CopyOnWrite
容器只能保证数据的最终一致性,不能保证数据的实时一致性- 如果希望写入的数据马上可以被读取,此时不要使用
CopyOnWrite
容器
Java_ConcurrentHashMap
线程不安全的HashMap
ConcurrentHashMap
的出现是因为在多线程环境下使用HashMap
进行put
操作会引起死循环,导致CPU利用率接近100%,所以正在并发的情况下不能使用HashMap
效率低下的HashTable容器
- HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。
- 因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态,如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
- 对于Hashtable而言,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占。相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。
JDK 1.7 中的ConcurrentHashMap的锁分段技术
- HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁。
- 容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率即
ConcurrentHashMap
所使用的锁分段技术 ConcurrentHashMap
的锁分段技术将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。- ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。
JDK 1.8 的CurrentHahMap的实现原理
- 在JDK1.8中CurrentHahMap参考了HashMap的实现,采用了数组+链表+红黑树的实现方式来设计
- JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。Segment 是 1.7 中CurrentHahMap的实现的概念
- JDK 8中 使用 Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。
- Java8 ConcurrentHashMap结构基本上和Java8的HashMap一样,不过保证线程安全性。
- JDK 1.8 中 ConcurrentHashMap 早期完全采用链表结构,但是当 ConcurrentHashMap在链表长度大于某个阈值时会将链表转换为红黑树进一步提高查找性能
- DK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,
- ConcurrentHashMap从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。
JDK 1.8 的CurrentHahMap的总结
- 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
- 保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
- 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
- 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
- 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。