一、泛型(generic)
1.1、泛型定义
泛型(generic)是JDK5中引入的一个新特性,提供了编译期间安全监测机制,它是将数据类型参数化的一种方式。也就是说在对方法进行编写参数列表时,以前我们需要知道方法的参数类型,现在使用泛型机制可以讲方法的参数类型也作为“未知的类型”,在调用该方法时传递该类型。
1.2、泛型的使用
1.2.1、泛型类(generic class)
它是一种具有一个或多个类型变量的类(一个变量可以有多种类型)
语法:
public class 类<T>{
// 类里面的数据类型 和 方法返回值,以及方法的参数都可以使用T
// <>里面可以是任意大写字母
}
定义泛型的字母
T : Type: 变量类型
K: Key : 任意键的类型
V: Value : 任意值的类型
E:ELement 用于定义集合的元素类型
1.2.2、泛型接口(generic interface)
在接口中定义泛型,使接口的方法可以使用该泛型,实现类实现该接口时需要指定接口的类型、
语法:
public interface Genarator<T> {
public T getValue();
public void setValue(T s);
}
public class StringGenarator implements Genarator<String> {
private String name;
@Override
public String getValue() {
return name;
}
@Override
public void setValue(String s) {
this.name=s;
}
@Override
public Student getValue() {
return stu;
}
@Override
public void setValue(Student s) {
this.stu = s;
}
}
泛型接口的好处:
让接口的方法的返回值或参数类型也参数化(泛型)
1.2.3、泛型方法
a、为什么会使用泛型方法
当一个类中只有某个方法需要使用泛型,而不是类的全部方法使用泛型,这时可以将泛型定义的范围缩小,通常我们可以定义进行泛型方法
b、定义泛型方法
语法:
public class 普通类{
public <T> T getValue(){
}
public <T> void setValue(T t){
}
}
泛型的好处:
可以对类的数据类型写通用类型,提高代码的复用性和可扩展性
1.2.4、泛型通配符
在定义泛型时除了可使用大写字母表示一种泛型类以外,还可以使用通配符表示泛型类型,如下三种表示方法
<?>:表示一种通用的泛型类,与< T >相似 <? extends T>:表示泛型类型必须是T的子类或T <? super T>:表示泛型类型是T 的父类或T 问题:< ?>与< T >的区别T t = new T(); //语法满足
? t = new ?() // 语法不满足
< T > 是一种确定的类型 , 可以表示定义泛型类或泛型方法
< ? > 是一种不确定的类型, 不能定义泛型类或泛型方法, 通常用于作为方法的形参
二、集合框架
2.1、为什么会有集合?
存储多个元素我们以前学过数组类型,由于数组类型特点是相同类型且长度固定,如果需要存储某一天的新闻数据,用数组不合理,无法确定当天数量。Java中提供可变长度的存储多个元素的数据类型,还可以存储不同数据结构的数据,这样的类型就是“集合类型”
数组和集合的区别?
a、数组的长度固定,集合的长度可自动扩容
b、数组的数据类型固定,集合可以存储任意类型,集合可以支持泛型
c、数组没有方法,耳机盒提供大量方法
d、Java中提供一个动态数组集合类型,或其它集合类型
Java 集合可分为 Collection 和 Map 两种体系
- Collection接口:单列数据,定义了存取一组对象的方法的集合
- List:元素有序、可重复的集合
- Set:元素无序、不可重复的集合
- Map接口:双列数据,保存具有映射关系“key-value对”的集合
2.2、集合的分布图
2.2.1、Collection接口
Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法 既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。
在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都 当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。
Collection属于单列集合的根接口,它扩展的主要子接口包括java.util.List和java.util.Set接口
List接口特点存储有序且可重复的元素,而Set接口特点存储无序且不可重复的元素,其中List下扩展常用的实现类包括java.util.ArrayList和java.util.LinkedList和Vector,其中Set接口下扩展的实现类包括java.util.HashSet和java.util.TreeSet
Collection接口的常用方法
2.2.2、Iterator迭代器接口
使用 Iterator 接口遍历集合元素
- Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
- GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元 素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公 交车上的售票员”、“火车上的乘务员”、“空姐”。
- Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所 有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了 Iterator接口的对象。
- Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。
- 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合 的第一个元素之前。
Iterator接口的方法
- hasNext() :该方法会判断集合对象是否还有下一个元素,如果已经是最后一个元素则返回false。
- next():把迭代器的指向移到下一个位置,同时,该方法返回下一个元素的引用。
- remove() 从迭代器指向的集合中移除迭代器返回的最后一个元素。
在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且 下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。
//hasNext():判断是否还有下一个元素
while(iterator.hasNext()){
//next():①指针下移②将下移以后集合位置上的元素返回
System.out.println(iterator.next());
}
Iterator接口remove()方法
Iterator iter = coll.iterator();//回到起点
while(iter.hasNext()){
Object obj = iter.next();
if(obj.equals("Tom")){
iter.remove();
}
}
注意:
Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方 法,不是集合对象的remove方法。
如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法, 再调用remove都会报IllegalStateException。
2.2.3、ArrayList类
java.util.ArrayList是一个数组结构的集合,实现动态数组的功能,扩展所有Collection的方法
数组结构的本质: 线性结构的顺序结构,ArrayList中使用连续的内存空间存储, 访问时通过下标(元素所在的位置)访问
ArrayList的数据结构
分析一个类的时候,数据结构往往是它的灵魂所在,理解底层的数据结构其实就理解了该类的实现思路,具体的实现细节再具体分析。
ArrayList的数据结构是:
说明:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。我们对ArrayList类的实例的所有的操作底层都是基于数组的。
public static void main(String[] args) {
// 通过ArrayList 创建集合对象
// 还可以存储自定义对象 默认容量是 10
ArrayList<String> list = new ArrayList<String>();
// 存储有序集合
list.add("aaa");
list.add("bbb");
list.add("ccc");
// 将元素插入到指定位置
list.add(1,"ddd");
//直接 遍历元素 get(Index) 通过下标访问元素
for(int i = 0 ;i<list.size();i++){
System.out.println("集合元素:"+ list.get(i));
}
// 设置集合的最小容量
list.ensureCapacity(20);
}
2.3、链表结构
有关LinkedList的集合的,它是一个链表结构的集合
2.3.1、单链表的结构
所谓单链表在内存中不连续的一端内存空间,链表的每一个元素就是一个节点,每一个节点由数据元素和下一个节点的存储位置组成,链表结构与数组结构最大区别是链表结构的存储内存是不连续的,而数组结构的内存是连续的,链表结构不能与数组结构一样快速查找
链表结构操作特点是添加、删除元素效率高,查询效率低;
数组结构操作特点:添加、删除效率低,查询效率高
链表结构的示意图:
前驱:该节点的上一个元素的地址
后继:该节点的下一个元素的地址
链表结构中最后一个元素的“后继”为null
2.3.2、单链表的实现
链表实现添加元素:
public class MyLinked {
//链表中有节点属性
Node head; //如果有一个节点,那么这个就是头结点
int size; //链表元素个数
//节点类 包括节点的数据内容和下一个节点的地址
class Node<T>{
//表示节点数据内容
T data;
//下一个节点的地址
Node next;
public Node(T data){
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
/**
* 将元素添加到第一个节点
*/
public void addFirst(Object obj){
//创建节点
Node node = new Node(obj);
//整体要求:将当前链表的头结点变更为新节点
//新节点中的后继是原始头结点
node.next = this.head;
//将头节点变更
this.head = node;
//长度+1
size++;
}
/**
* 添加元素到最后
* @param obj
*/
public void addLast(Object obj){
//第一种方法,将节点添加到最后
//add(obj,this.size);
//第二种,创建节点
// Node node = new Node(obj);
//找到最后一个元素
// Node lastNode = this.head;
//for (int i = 0; i <this.size-1 ; i++) {
// lastNode = lastNode.next;
// }
//lastNode.next = node;
//找最后一个节点(由于最后一个节点的next是null)
Node node = new Node(obj);
Node lastNode = this.head;
while (lastNode.next != null){
lastNode = lastNode.next;
}
lastNode.next = node;
this.size++;
}
/**
* 添加元素到指定下标
* @param obj
* @param index
*/
public void add(Object obj,int index){
//1、创建节点对象
Node node = new Node(obj);
//验证index的范围
if (index<0 || index>=this.size){
throw new IllegalArgumentException("下标越界,index不再此链表中");
}
//2、查找指定位置的节点(前一个节点地址和当前节点地址)遍历index-1
//前一个节点
Node pre = this.head;
//当前下标的节点
Node cur;
for (int i = 0; i <index-1 ; i++) {
//每一个元素都是在之前元素next中
pre = pre.next;
}
//当前元素地址存在pre的next中
cur = pre.next;
//3、改变两个节点的后继地址
//前一个元素的后继是node地址
pre.next = node;
//node的后继是指定下标的地址
node.next = cur;
this.size++;
}
}
链表实现删除元素:
/**
* 删除第一个节点
*/
public void removeFirst(){
//删除第一个节点
if (this.size == 0 ){
throw new IllegalArgumentException("没有需要删除的元素");
}
//获取当前链接的“后继”
Node node = this.head.next;
//并让后继作为头
this.head = node;
this.size--;
}
/**
* 删除最后节点
*/
public void removeLast(){
//删除是否存在数据
if(this.size==0){
throw new IllegalArgumentException("没有需要删除的原始");
}
// 找最后一个元素的前一个 地址 ,并将该地址的next 改为null
Node cur = this.header;
Node pre = this.header;
while(cur.next!=null){
pre = cur;
// 下一个变为当前
cur = cur.next;
}
// 最后一个元素 就是 当前
pre.next = null;
size--;
}
/**
* 根据下标获取指定的节点
* @param index
* @return
*/
public Node getByIndex(int index){
if (this.size == 0){
throw new IllegalArgumentException("没有需要查找的元素");
}
if (index == 0){
return this.head;
}
//查找指定下标的元素
Node cur = this.head; //从第一个元素开始
int j =0;
while (index != j && index<this.size){
//依次往下查找每个元素
cur = cur.next;
j++;
}
return cur;
}
public int getSize(){
return size;
}
2.4、队列结构
队列结构(Queue): 在基于链表结构的基础上 ,实现的一种“先进先出”的结构, 常用操作 入队(put),出队(pop) ,设置队列的头结点 和 尾结点
2.5、栈结构
栈(Stack)结构也是常用数据之一,它具有“先进后出”的特点
public class MyStack<T> {
// 定义一个数组 ,用于存储元素
private Object[] obj;
private int size;
public MyStack(){
obj = new Object[10];
size=0;
}
/**
* 入栈: 压入栈顶元素
* @param t
*/
public void push(T t){
expandCapacity(size+1);
obj[size]=t;
size++;
}
/**
* 返回栈顶元素:peek
*/
public T peek(){
if(size>0) {
return (T) obj[size - 1];
}
return null;
}
/**
* 出栈: 返回栈顶的元素,并删除该元素
* @return
*/
public T pop(){
T t = peek();
if(size>0) {
// 将最后一个元素 删除
obj[size - 1] = null;
size--;
}
return t;
}
/**
* 是否为空元素
* @return
*/
public boolean isEmpty(){
return size==0;
}
/**
* 扩容数组大小 : 扩容1.5倍
*/
public void expandCapacity(int size){
if(obj.length< size){
// 需要扩容
int length = size*3/2 + 1;
this.obj = Arrays.copyOf(this.obj,length);
}
}
}
2.6、双向链表(LinkedList集合)
java.util.LinkedList集合是java.util.List的实现类,实现List接口的所有方法(添加,删除,查找,判断是空等) ,它添加,删除元素较快,查询相对慢,但是查询头尾元素较快
LinkedList集合实现双向链表接口,实现从头元素到尾元素的链表和从尾到头元素的链表,目标为了增加元素的检索效率 ,如下图
关于LinkedList实现大量操作头元素和尾元素的方法。 其中必须通过LinkedList的引用创建该对象
- public void addFirst(E e) :将指定元素插入此列表的开头。
- public void addLast(E e) :将指定元素添加到此列表的结尾。
- public E getFirst() :返回此列表的第一个元素。
- public E getLast() :返回此列表的后一个元素。
- public E removeFirst() :移除并返回此列表的第一个元素。
- public E removeLast() :移除并返回此列表的后一个元素。
- public E pop() :从此列表所表示的堆栈处弹出一个元素。
- public void push(E e) :将元素推入此列表所表示的堆栈。
- public boolean isEmpty() :如果列表不包含元素,则返回true
关于LinkedList实现大量操作头元素和尾元素的方法,
面试题
请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层 是什么?扩容机制?Vector和ArrayList的最大区别?
- ArrayList和LinkedList的异同 二者都线程不安全,相对线程安全的Vector,执行效率高。 此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于 随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增 和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
- ArrayList和Vector的区别 Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于 强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用 ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大 小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。
2.7、Set集合
java.util.Set接口继承自Collection接口,实现对元素的基本操作,与java.util.List区别于Set集合存储无序,且唯一的元素,List存储有序,且可重复的元素
Set接口的实现类 HashSet、LinkedHashSet、TreeSet
2.7.1、HashSet
HashSet集合依据元素的哈希值确定内存中的存储位置,所谓Hash值是内存中哈希表的唯一标志,通过哈希值可快速检索到元素所在的位置,所以查询效率高,与HashSet类似结构的包括HashMap等
创建一个HashSet时,就是创建一个HasMap( 关于HashMap结构后面讲)
什么是哈希表?
在Java1.8以前,哈希表的底层实现采用数组+链表结构,但是这样对于“Hash冲突”(两个对象生成的哈希值一样),即多个元素存储在一个“数据桶”中,这样查找该元素时,依然效率低下,为了解决由于哈希冲突导致的数据查询效率低下,JDK8以后将哈希表实现采用 数组+链表+红黑树结构
2.7.2、HashSet存储自定义对象类型
HashSet对于对象是否相同的依据,判断对象的hashCode值和equals是否相等,如果它们相等则判断元素一致,不能重复添加
public class People {
private int pid;
private String pname;
private int age;
public People(int pid, String pname, int age) {
this.pid = pid;
this.pname = pname;
this.age = age;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int hashCode() {
return this.pid;
}
@Override
public boolean equals(Object obj) {
if(this == obj){
return true;
}
if(obj instanceof People){
People p = (People) obj;
if(p.pid == this.pid && p.getPname().equals(p.getPname())){
return true;
}
}
return false;
}
}
//存储 对象类型
Set<People> sets = new HashSet<>();
People p = new People(1001,"关羽",100);
People p2 = new People(1002,"张飞",100);
People p3 = p2;
System.out.println("p的hashcode:"+p.hashCode());
sets.add(p);
// 检查是否为同一个地址
sets.add(p);
sets.add(p2);
sets.add(p3);
// 插入一个重新new的张飞对象 HashSet以 equals和hashcode的结果作为是否重复对象的依据
People p4 = new People(1002,"张飞",90);
sets.add(p4); // 会当做是重复的对象 ,不能添加成功。
System.out.println("sets的长度:"+sets.size());
for(People obj : sets){
System.out.println(obj.getPid()+"---"+obj.getPname()+"---"+obj.getAge());
}
2.7.3、LinkedHashSet
在HashSet中存储的数据是唯一且无序,如何保证数据的有序性,可通过扩展HashSet的子类完成,java.util.LinkedHashSet,它实现有序的Hash结构,它的底层实现使用链表+哈希结构
创建LinkedHashSet时,就是创建一个LinkedHashMap结构 ,linkeHashSet中如何保证顺序一致性
accessOrder = false; 按照插入的顺序存储 accessOrder = true: 按照访问的顺序存储。
// 创建LinkedHashSet对象
LinkedHashSet<String> set = new LinkedHashSet();
set.add("aaa");
set.add("bbb");
set.add("ccc");
set.add("ddd");
//遍历元素
for(String s : set){
System.out.println(s);
}
2.7.4、 TreeSet
TreeSet实现对Set元素的排序功能, 也包含基础的Set集合功能。 存放在TreeSet中的元素时有序的,默认升序,也可以自定义排序规则。
两种方式实现自定义排序规则
1、对元素(自定义类)实现 java.lang.Comparable 接口,重写 compareTo方法
public class Fruit implements Comparable<Fruit>{
private int id;
private String name;
public int compareTo(Fruit o) {
// return this.id-o.id; 升序
return o.id - this.id;
// 正数: 前一个大于后一个
// 负数: 前一个小于后一个
}
}
// 实现自定义排序规则的方式一 : 对象实现Comparable接口 (java.lang)
// 重写compareTo 方法。
TreeSet<Fruit> fruitSet = new TreeSet<>();
Fruit f1 = new Fruit(100,"苹果");
Fruit f2 = new Fruit(101,"香蕉");
fruitSet.add(f1);
fruitSet.add(f2);
System.out.println(fruitSet.size());
for(Fruit f : fruitSet){
System.out.println(f.getId()+"---"+f.getName());
}
2、通过匿名内部类的方式 在创建TreeSet时,创建自定义排序规则 ,new Comparator的接口
// 自定义排序规则的方式二: 对treeSet实现匿名内部类 new Comparator(java.util)
TreeSet<Integer> scores = new TreeSet (new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1; //降序
}
});
// 添加元素
scores.add(80);
scores.add(87);
scores.add(90);
scores.add(78);
for(Integer score : scores){
System.out.println(score);
}
// 按照对象的某一属性降序
TreeSet<People> peopleSet = new TreeSet<>(new Comparator<People>() {
@Override
public int compare(People o1, People o2) {
return o2.getAge()- o1.getAge(); // 根据age降序排列
}
});
peopleSet.add(new People(1001,"张飞",100));
peopleSet.add(new People(1002,"刘备",102));
peopleSet.add(new People(1003,"关羽",108));
for(People p : peopleSet){
System.out.println(p.getPid()+"--"+p.getAge());
}
2.8、Map集合
java.util.Map集合用于存储key-value的数据结构,一个键对应一个值,其中键在集合中是唯一的,Value可以重复,例如学号与学生的关系,省份编号对应省份信息,对于Map集合的常用实现类包括HashMap、LinkedHashMap、HashTable、TreeMap等。
2.8.1、HashMap(重点)
java.util.HashMap存储无序的,键值对数据,HashMap的实现原理在JDK1.8以前使用链表+数组结构,1.8以后使用链表+数组+红黑树结构,使用Hash表的存储方式其检索效果高
特点:
a、HashMap的key唯一,且无序,value不唯一
b、HashMap的key和value都可以为null
c、对于相同key元素,它的value会覆盖原始value
HashMap的常用方法
a、put(K Key,V value):存储Key-value到容器中
b、V get(K Key):根据Key获取对应的value
c、Set KeySet():返回所有的Key,Set集合
d、boolean containsKey(K Key):判断key是否存在
e、clear():清空容器的元素
f、boolean containsValue(V value):判断value是否存在
g、Collection values():返回所有的value集合
h、isEmpty():判断是否为空集合
i、remove(Object Key):根据key删除这个value
j、size():返回元素的大小
k、Set<Map.Entry<Key,Value>> entrySet():返回容器的Key-value的实体类的集合,方便遍历元素
// 创建HashMap对象
Map<String , Integer> cards = new HashMap();
//存储
cards.put("红桃",3);
cards.put("黑桃",3);
cards.put("方片",2);
cards.put("梅花",8);
cards.put("红桃",2); // 会覆盖原始的value
//获取指定key的value元素
System.out.println(cards.get("红桃"));
// 获取所有的key
Set<String> keys= cards.keySet();
//通过遍历所有的key 访问对应的value
for(String k : keys){
System.out.println(k+"-----"+cards.get(k));
}
// 判断key 是否存在, 判断value是否存在
System.out.println("是否有红桃:"+cards.containsKey("红桃"));
System.out.println("判断是否有value:"+cards.containsValue(2));
// 获取所有的value
Collection<Integer> values = cards.values();
Iterator its= values.iterator();
while(its.hasNext()){
System.out.println("values ----"+its.next());
}
//获取所有的元素
System.out.println(cards.size());
// 遍历map集合元素 entrySet
Set<Map.Entry<String, Integer>> entrys = cards.entrySet();
for(Map.Entry<String, Integer> en : entrys ){
System.out.println("entry遍历方式:"+en.getKey()+"-----"+en.getValue());
}
// remove
System.out.println("删除元素:"+cards.remove("红桃"));
//清空元素
cards.clear();
System.out.println("元素的大小:"+cards.size());
}
2.8.2、HashMap的原理以及源码分析
HashMap基于键值对存储,这里讲解的JDK8 的源码
HashMap的存储结构
HashMap实现步骤:数据结构(数组+链表+红黑树)
1、根据key生成对应的hash值(采用Hash函数生成),根据hash值找到该元素所在的数组结构中的位置,如果该位置存在元素,说明产生了哈希冲突,此时JDK8采用元素尾插入法(JDK7采用头插入法),将元素放入链表的尾部,这样可能会形成一条长长的链表
2、当链表长度达到8(TREEIFY_THRESHOLD)时,此时会转成红黑树结构(树形结构的检索效率高),为了提高查询效率,我们通常将桶连接的链表/红黑树中的每个元素称为bin。
HashMap中的重要常量
- DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
- MAXIMUM_CAPACITY : HashMap的最大支持容量,2^30
- DEFAULT_LOAD_FACTOR:HashMap的默认加载因子
- TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树
- UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表
- MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的 数量大到需要变红黑树时,若hash表容量小于
- MIN_TREEIFY_CAPACITY时,此时应执行 resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4 倍。)
- table:存储元素的数组,总是2的n次幂 entrySet:存储具体元素的集
- size:HashMap中存储的键值对的数量
- modCount:HashMap扩容和结构改变的次数。
- threshold:扩容的临界值,=容量*填充因子
- loadFactor:填充因子
HashMap中的一些关键属性和方法
/**
* 扩容的临界值,通过capacity * load factor可以计算出来。超过这个值HashMap将进行扩容
* @serial
*/
int threshold;
/**
* 存储键值对的数组,一般是2的幂
*/
transient Node<K,V>[] table;
/**
* 键值对的实际个数
*/
transient int size;
/**
* 记录HashMap被修改结构的次数。
* 修改包括改变键值对的个数或者修改内部结构,比如rehash
* 这个域被用作HashMap的迭代器的fail-fast机制中(参考ConcurrentModificationException)
*/
transient int modCount;
/**
* HashMap的节点类型。既是HashMap底层数组的组成元素,又是每个单向链表的组成元素
*/
static class Node<K,V> implements Map.Entry<K,V> {
//key的哈希值
final int hash;
final K key;
V value;
//指向下个节点的引用
Node<K,V> next;
}
//增长因子 0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
几个常用方法分析:
1、get(Object)
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
返回目标Node
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 整个数组长度不为空, 且第一个Node不为空 说明已找到对一个的hash位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 表示当前“桶”的第一个元素 的hash值相同,且 key也相同,说明value 是目标查找对象
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 说明一个“桶”中有多个元素, 继续找
if ((e = first.next) != null) {
// 多个元素中 需要先判断是否为 “树”结构,因为超过8个长度就转成了数
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 一定是单链表结构 ,依次从头找到尾,看有没有对应的 key
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
2、put(K,V)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 获取元素的总长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 判断如果当前集合中没有对应的 “桶”,说明没有出现 “Hash碰撞”
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
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 TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//不是红黑树结构,那么就肯定是链式结构
for (int binCount = 0; ; ++binCount) {
//如果到了链表的尾部 ,插入到后面
if ((e = p.next) == null) {
// 创建一个新节点 创建最后节点 next中
p.next = newNode(hash, key, value, null);
// 如果长度大于 8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 将tab 的桶的所有元素 转成 树结构
treeifyBin(tab, hash);
break;
}
// 如果还没有到达尾部 就找到元素了, 直接返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果找到该元素 ,需要替换它的 value
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;
}
put插入的流程
步骤一:先判断容器是否为空,为空则需要扩容
步骤二:根据key生成hash值,根据hash值找到对应的数组的位置,如果数组位置为空,说明没有hash冲突,直接插入,并长度+1;
步骤三:如果数组位置的内容不为空,说明产生hash冲突,继续通过key查找元素,如果第一个元素存在(说明还没有产生链表)直接返回value并覆盖value的值
步骤四:如果key对应的第一个元素不存在,则此时可能出现链表或红黑树,如果红黑树采用树结构的插入法(省略分析过程),否则一定是链表结构
步骤五:如果是链表,将该元素插入到末尾,之后验证整个链表的长度是否大于8,如果大于8,将立案表专成红黑树结构
步骤六:插入成功之后,判断整个容器的元素个数是否超出扩容的临界值(threshold = capacity*增长因子)
关于JDK7.0和JDK8.0的HashMap有什么区别
1、结构不同
JDK7采用数组+链表 JDK8采用数组+链表+红黑树
2、hash值得计算方式不同
JDK7 table在创建hashmap时分配空间
JDK8通过key的hashCode计算,在put时分配空间
3、发生hash冲突插入方式不同
JDK7采用头插法,JDK8采用尾插法
4、resize操作方式不同
JDK7重写计算index值,JDK8通过判断相应的位是0还是1,要么依然是原index,要么是oldCap+原index
2.8.3、LinkedHashMap
由于HashMap存储的key是无序,如果需要存储有序的key可使用LinkedHashMap 它依然满足HashMap的所有特点 ,并在此基础上有序
public static void main(String[] args) {
// 创建一个有序的HashMap 有序的key
LinkedHashMap<String , People> map = new LinkedHashMap<>();
map.put("胡松松",new People(1001,"胡松松",22));
map.put("李鑫",new People(1001,"李鑫",22));
map.put("林志颖",new People(1001,"林志颖",21));
//遍历
for(Map.Entry<String,People> en : map.entrySet()){
System.out.println(en.getKey()+"---"+en.getValue());
}
}
2.8.4、TreeMap
TreeMap实现一个可排序的Map集合,默认对key升序排列,也可以降序排列
如果添加元素的key为自定义类,需要实现Comparable接口或Comparator接口,TreeMap底层实现是二叉树结构(有关二叉树的特点),实现有序的key的分布
// TreeMap 用于存储可排序的Key -Value集合 ,
// 其中key必须实现了排序规则的对象 (包装类,String,自定类)
public static void main(String[] args) {
// 存储学号和分数 默认对key 进行升序
// TreeMap<Integer ,Integer> maps = new TreeMap<>();
TreeMap<Integer,Integer> maps = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1; // 降序
}
});
maps.put(1003,88);
maps.put(1002,90);
maps.put(1004,80);
maps.put(1001,85);
//输出
Set<Integer> keys = maps.keySet();
for(Integer k : keys){
System.out.println(k+"----"+maps.get(k));
}
// 自定义规则
// 注意 ,如果key不是包装类而是自定义,必须要求该类实现Comparable或Comparator接口
TreeMap<Student ,Integer> stuMap = new TreeMap<>();
stuMap.put(new Student(1001,"张飞"),90);
stuMap.put(new Student(1003,"刘备"),87);
stuMap.put(new Student(1002,"关羽"),96);
// 这里降序排列
for(Map.Entry<Student,Integer> en : stuMap.entrySet()){
System.out.println(en.getKey().getSid() + "---"+ en.getValue());
}
}
class Student implements Comparable<Student>{
private int sid;
private String sname;
public Student(int sid, String sname){
this.sid = sid;
this.sname = sname;
}
public int getSid() {
return sid;
}
public void setSid(int sid) {
this.sid = sid;
}
public String getSname() {
return sname;
}
public void setSname(String sname) {
this.sname = sname;
}
@Override
public int compareTo(Student o) {
return o.sid-this.sid; // this表示前一个对象, o表示后一个对象
}
}
2.8.5、HashTable
HashTable实现hash结构的key-value集合,与HashMap很相似,HashTable是线程安全(它的很多方法是同步操作),它不需要存储null的key和value
扩展自Dictionary类和实现Map接口
常用方法:
put()
get()
clear()
containsKey()
containsValue()
它 有一个子类 是 Properties类,用于存储属性文件的 key- value
public static void main(String[] args) {
//创建HashTable 无序
Hashtable<String,String> tables = new Hashtable<>();
// 存储
tables.put("王宝强","马蓉");
tables.put("贾乃亮","李小璐");
tables.put("文章","马伊琍");
//获取 使用所有key遍历 返回枚举类型
Enumeration<String> keys = tables.keys();
while(keys.hasMoreElements()){
String s = keys.nextElement();
System.out.println(s + "---"+ tables.get(s));
}
// 有一个HashTable的子类 Properties
Properties prop = new Properties();
prop.setProperty("username","张三");
prop.setProperty("password","123456");
//获取对应属性名的值
System.out.println("根据属性名获取值:"+prop.getProperty("username"));
System.out.println("根据属性名获取值:"+prop.getProperty("password"));
}
2.9、集合常见面试题
1、Collection和Collections的区别?Array和Arrays的区别?
Collection 是集合类的顶级接口。本身是一个Interface,里面包含了一些集合的基本操作。Collection接口是Set接口和List接口的父接口,Collections是一个集合框架的帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作
Array表示一个数组对象,Arrays是一个数组工具类,提供大量的数组操作方法
2、List和Set的区别?ArrayList和LinkedList的区别
相同点:List、Set都继承Collection接口
不同点:List存储不唯一,有序集合元素,Set存储唯一,无序的集合元素
ArrayList实现数组结构集合,查询比较快,添加删除效率高
LinkedList实现双向链表结构组合,添加、删除效率高,查询效率低
3、HashSet和TreeSet的区别?
相同点:都存储唯一集合,都实现Set接口
不同点:HashSet无序,底层实现Hash结构的存储
TreeSet有序,可实现自定义排序,存储树形结构
4、HashMap和HashSet的区别?
相同点:都属于Hash结构的集合,存储效率较高
不同点:HashSet是存储单个元素,实现Set接口
HashMap存储键值对元素,实现Map接口
5、HashMap和HashTable的区别?
相同点:都来自Map的实现类
不同点:HashMap的key和value可以为空,线程不安全效率高
HashTable的子类包含key-value的方法,HashTable还继承一个父类Dictionary,线程安全效率低
6、HashMap和TreeMap的区别?
相同点:都实现Map接口
不同点:TreeMap有序,实现二叉树集合
HashMap无序,实现哈希结构集合
三、JDK8的特性
在JDK8中新增了一些特殊功能,一般开发时方便使用,其中最主要的功能如下
3.1、接口的默认函数
public interface MyInterface {
public default void defaultMethods(){
System.out.println("这是一个接口的默认方法。");
// 静态方法可以在default方法中调用
staticMethods();
}
public void service();
public static void staticMethods(){
System.out.println("这是接口的静态方法");
}
}
public static void main(String[] args) {
// 创建匿名内部类的方式
MyInterface my = new MyInterface() {
@Override
public void service() {
System.out.println("service方法.....");
}
};
my.defaultMethods();
//通过接口名 调用静态方法
MyInterface.staticMethods();
}
3.2、Lambda表达式
JDK8中支持一种对方法调用的简写方式,也是一种特殊写法
语法:([形参名])-> {方法的实现}
这个接口中有且只有一个方法,并对方法实现
原始代码
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
// 降序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.compareTo(o1); //降序
}
})
由于JDK能识别sort的参数二是Comparator , 并Comparator是函数式接口(一个接口中有且只有一个方法。),实现的一定是唯一的方法名, 所有这里方法名和返回值也省略, 它的简写方式
// 使用lambda表达式 ([形参])->{方法体}
Collections.sort(list ,(String o1,String o2)->{
return o2.compareTo(o1);
});
由于形参的数据类与集合的元素类型一致,这里的形参类型也省略 、return也省略
// 最精简的Lambda
Collections.sort(list ,(o1,o2)->o2.compareTo(o1));
3.3、函数式接口
函数式接口主要用于满足前面Lambda表达式的语法的使用。在一个接口中有且只有一个方法的接口称为“函数式接口”
如何将一个接口定义为函数式接口呢?在接口上增加注解“@FunctionnalInterface”
package com.j2008.functionalFun;
@FunctionalInterface // 该注解的意义在于 约束接口只能由一个方法
public interface ConverterInterface<T> {
public String convertStr(T t);
}
//传统写法
ConverterInterface<Integer> con = new ConverterInterface<Integer>() {
@Override
public String convertStr(Integer o) {
return o.toString();
}
};
String ss = con.convertStr(100);
System.out.println(ss);
//使用 lambda表达式写法
ConverterInterface<Date> cons = (o)->o.toLocaleString();
String s = cons.convertStr(new Date());
System.out.println(s);
3.4、方法和构造器的引用
当方法的实现 是构造器时,可直接引用构造器
语法: 类名::new
@FunctionalInterface
public interface StudentFactory<T> {
// 参数是创建对象时 的 属性
public T create(int id ,String name);
}
public class Student {
private int sid;
private String sname;
public Student(int sid, String sname) {
this.sid = sid;
this.sname = sname;
}
}
public static void main(String[] args) {
//使用Lambda实现 函数接口的方法
StudentFactory<Student> factory = (id,name)->{
return new Student(id ,name);
};
Student stu1 = factory.create(1001,"张三丰");
System.out.println(stu1);
// 以上写法可以直接换成 引用构造器方式
StudentFactory<Student> factory1 = Student::new;
//创建
Student stu2 = factory1.create(1002,"张无忌");
System.out.println(stu2);
}