一、泛型
1.泛型简介
-
泛型是JDK1.5之后新增的,可以帮助我们建立类型安全的集合。
-
泛型的本质是“数据类型的参数化”,处理的数据类型不是固定的,而是可以作为参数传入。可以将泛型理解为数据类型的一个占位符(类似:形式参数),告诉编译器,在调用泛型时必须传入实际类型。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
白话:
把类型当做是参数一样传递。
<数据类型>只能是引用类型。
-
使用泛型的好处:
- 代码可读性更好(不需要再强转了)
- 程序更加安全【只要编译时期没有警告,运行时期就不会出现ClassCastException异常】
-
类型擦除
- 编译时采用泛型写的类型参数,编译器会在编译时去掉,称为“类型擦除”
- 泛型主要用于编译阶段,编译后生成的字节码class文件不包含泛型中的类型信息,涉及类型转换仍然是普通的强制类型转换。类型参数在编译后会被替换成Object,运行时虚拟机并不知道泛型。
- 虚拟机只能处理具体的类型(反编译可以看到泛型-比较智能)
- 泛型主要是方便代码编写,以及更好的安全性检测。
2.泛型的使用
2.1 泛型字符
泛型字符可以是任何标识符,一般采用几个标记:E、T、K、V、N、?
泛型标记 | 对应单词 | 说明 |
---|---|---|
E | Element | 在容器中使用,表示容器中的元素 |
T | Type | 表示普通的Java类 |
K | key | 表示键,例如Map中的key |
V | Value | 表示值 |
N | Number | 表示数值类型 |
? | 表示不确定的java类型 |
2.2 泛型类
将泛型定义在类上,使用该类时,才把类型确定下来。具体使用:在类的名称后添加一个或多个类型参数声明,如:<T>、<T,K,V>
语法结构
public class 类名<泛型表示符号>{
}
示例
public class Generic<T> {
private T flag;
public void setFlag(T flag){
this.flag=flag;
}
public T getFlag(){
return flag;
}
}
测试
public class Test {
public static void main(String[] args) {
Generic<String> generic = new Generic<>();
generic.setFlag("abc");
String flag = generic.getFlag();
System.out.println(flag);
Generic<Integer> generic1 = new Generic<>();
generic1.setFlag(10);
Integer flag1 = generic1.getFlag();//这里不需要强制类型转换了
System.out.println(flag1);
}
}
2.3 泛型接口
具体实现需要在实现类中声明
语法结构
public interface 接口名<泛型表示符>{
}
示例
public interface Igeneric<T> {
T getName(T name);
}
//实现类需要声明类型
public class IgenericImpl implements Igeneric<String> {
@Override
public String getName(String name) {
return name;
}
}
测试
//1.实现类实现接口时指明了类型
IgenericImpl i1 = new IgenericImpl();
String name = i1.getName("jack");
//2.使用接口时进行声明参数类型(面向接口编程)
Igeneric<String> i2 = new IgenericImpl();
String name1 = i2.getName("mike");
2.4 泛型方法
泛型类中所定义的泛型,在方法中也可以使用。但是,经常需要仅仅在某一个方法上使用泛型,这时可以使用泛型方法。
泛型方法是指将方法的类型参数定义成泛型,以便在调用时接收不同类型的参数。类型参数可以有多个,用逗号隔开,如:<K,V>。定义时,类型参数一般放到返回值前面。
调用泛型方法时,不需要像泛型类那样告诉编译器什么类型,编译器可以自动推断出类型来。
-
非静态方法——有两种方式(类的泛型/泛型方法)
语法结构
public <泛型表示符号> void getName(泛型表示符 name){ } public <泛型表示符号> 泛型表示符号 getName(泛型表示符号 name){ }
示例
public <T> void setName(T name){ System.out.println(name); } public <T> T getName(T name){ return name; }
测试
MethodGeneric methodGeneric = new MethodGeneric(); //调用泛型方法不需要像泛型类那样告诉编译器是什么类型,可以自动推断。 methodGeneric.setName("jack"); methodGeneric.setName(123); MethodGeneric methodGeneric1 = new MethodGeneric(); String name = methodGeneric1.getName("lucy"); Integer name1 = methodGeneric1.getName(110);
-
静态方法——只有一种方式(泛型方法)
注意:静态方法无法访问类上定义的泛型。
如果静态方法操作的引用数据类型不确定的时候,必须将泛型定义在方法上。
泛型只是一个占位符,必须在传递类型后才能使用,类实例化时才能真正的的传递类型参数
由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数静态的方法就已经加载完成了,所以静态方法的泛型不能用类上的。
关于为啥没有静态泛型类,这个是固定的,Java里的静态属性和方法都是放到类里的,但它不属于类,相当于寄生虫和宿主的关系
语法结构
public static <泛型表示符> void setName(泛型表示符号 name){ } public static <泛型表示符号> 泛型表示符号 getName(泛型表示符号 name){ }
示例
public static <T> void setFlag(T flag){ System.out.println(flag); } public static <T> T getFlag(T flag){ return flag; }
测试
MethodGeneric.setFlag("jack"); MethodGeneric.setFlag(123123); String flag = MethodGeneric.getFlag("mike"); Integer flag1 = MethodGeneric.getFlag(12346);
-
泛型方法与可变类型参数
可变类型参数==数组
语法结构
public <泛型表示符> void showMsg("泛型表示符...args"){ }
示例
public class MethodGeneric{ public <T> void method(T...args){ for(T t:args){ System.out.println(t); } } }
测试
MethodGeneric methodGeneric = new MethodGeneric(); String[] arr = new String[]{"a","b","c"}; Integer[] arr2 = new Integer[]{1,2,3}; methodGeneric.method(arr); methodGeneric.method(arr2);
2.5 通配符和上下限定
-
无界通配符
"?“表示类型通配符,用于代替具体的类型。它只能在”<>"中使用。
可以解决当具体类型不确定的问题。
语法结构
public void showFlag(Generic<?> generic){ }
-
通配符的上限限定
表示通配符的类型是T类以及T类的子类或者T接口以及T接口的子接口。
同样适用于泛型的上限限定。
语法结构
public void showFlag(Generic<? extends Number> generic){ }
-
通配符的下限限定
表示通配符的类型是 T 类以及 T 类的父类或者 T 接口以及 T 接口的父接口。
注意:该方法不适用泛型类。
语法结构
public void showFlag(Generic<? super Integer> generic){ }
3.泛型总结
泛型主要用于编译阶段,编译后生成的字节码 class 文件不包含泛型中的类型信息。 **类型参数在编译后会被替换成 Object,运行时虚拟机并不知道泛型。**因此,使用泛型时,如下几种情况是错误的:
- 基本类型不能用于泛型。
Test t; 这样写法是错误,我们可以使用对应的包装类;Test t ; - 不能通过类型参数创建对象。
T elm = new T(); 运行时类型参数 T 会被替换成 Object,无法创建 T 类型的对象,容易引起误解,所以在 Java 中不支持这种写法。
二、容器
1.容器简介
- 数组也是一种容器,可以存放对象或基本类型数据。
- 数组的优势:是一种简单的线性序列,可以快速的访问数组元素,
2.容器的结构
2.1 结构图
2.2 单例集合
单例集合:将数据一个一个的进行存储。
2.3 双例集合
双例集合:基于key与value的结构存储数据。
3.单例集合
3.1 Collection接口
Collection是单例集合根接口,它是集中、收集的意思。Collection接口的两个子接口是List、Set接口。
3.2 Collection接口中的抽象方法
由于List、Set是Collection的子接口,意味着所有List、Set的实现类都有上面的方法。
3.3 List接口
3.3.1 List接口特点
- 有序:元素存入集合的顺序和取出的顺序一致。List中每个元素都有索引标记,可以根据元素的索引标记访问元素。
- 可重复:允许加入重复的元素;允许满足e1.equals(e2)的元素重复加入容器。
3.3.2 List的常用方法
除了Collection接口的方法,还多了一些和索引有关的方法:
3.3.3 ArrayList容器类
ArrayList是List接口的实现类。是List存储特征的具体实现。
ArrayList底层是数组实现的存储。特点:查询效率高,增删效率低,线程不安全。
//实例化ArrayList容器
List<String> list = new ArrayList<>();
-
toArray
System.out.println("----------转换为Object数组--------------"); //将ArrayList将转为Object[] //不可以批量转换 //String[] arr = (String[]) list.toArray();//ClassCastException //但是不能将转换的数组做强制类型转换 Object[] arr = list.toArray(); for(int i=0;i<arr.length;i++){ //可以在迭代的时候进行强转 String str3 = (String)arr[i]; System.out.println(str3); } System.out.println("-----------将单例集合转为指定类型的数组-------------------"); //可以将单例集合转换为指定类型数组 //但是类型需要参考泛型中的类型 String[] arr2 = list.toArray(new String[list.size()]); for(int i=0;i<arr2.length;i++){ System.out.println(arr2[i]); }
-
取并集
a.addAll(b):a并b,成功返回true,b不能为空,否则会报错
-
取交集
a.retainAll(b):a交b,成功返回true
-
取差集
a:abc b:bcd
a.removeAll(b):返回a,取差集。
3.3.4 ArrayList源码分析
-
ArrayList底层是用数组实现的。
private static final int DEFAULT_CAPACITY = 10; transient Object[] elementData; private int size;
-
初始容量
private static final int DEFAULT_CAPACITY = 10;
-
elemData初始化为空参{},并没有创建长度为10的数组,jdk1.8之后做了延迟初始化,当调用add方法的时候才给数组大小为10的空间。
transient Object[] elementData;
-
当调用list.add方法时,实际调用的是ArrayList的add方法
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
-
调用ensureCapacityInternal进行容量检查,该方法的参数为miniCapacity,值为1,初始size为0
//容器检查 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
-
在ensureCapacityInternal中调用ensureExplicitCapacity进行容量确认,参数中调用calculateCapacity来计算大小,该方法参数为elemData,minCapacity,初始时,elemData为{}和DEFAULTCAPACITY_EMPTY_ELEMENTDATA相同,返回最大值DEFAULT_CAPACITY容量为10
-
接下来判断是否需要扩容,数组中元素个数-数组长度如果>0则表示需要扩容,调用grow方法,否则将默认容量10赋值给数组,在将e添加到数组中,size++。
//容器确认 private void ensureExplicitCapacity(int minCapacity) { modCount++; // 判断是否需要扩容,数组中的元素个数-数组长度,如果大于0表明需要扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); }
//最初赋容量10 //延迟初始化 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
-
第二次传入时,elemData.length已经为10了,minCapacity为2,2-10<0,表示不需要扩容。
-
当传入第10个元素时,10+1=11,11-10>0,调用grow方法进行扩容操作:将elemData.length赋值给oldCapacity,newCapacity=old…+(old>>1)相当于除以2(扩容1.5倍),最后调用Arrays的copyof方法,将new赋值给old完成扩容操作。
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //扩容1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
按住ctrl键点击add方法—>进入list中按住ctrl+alt点击add方法—>进入ArrayList的add中
5.Vector容器类
Vector底层是用数组实现的,相关的方法都增加了同步检查,因此“线程安全,效率低”,比如,indexOf方法增加了synchronized同步标记。
Vector的使用与ArrayList是相同的,都实现了List接口,对List接口中的抽象方法做了具体实现。
//实例化Vector
List<String> v = new Vector<>();
3.3.5 Vector源码分析
-
初始化容器
Vector采用立即初始化数组,直接赋大小10的容量
protected Object[] elementData; public Vector() { this(10); }
public Vector(int initialCapacity, int capacityIncrement) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; }
-
添加元素
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
-
数组扩容
private void ensureCapacityHelper(int minCapacity) { //判断是否需要扩容,数组中的元素个数-数组长度,如果大于0表明需要扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //扩容2倍 int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
3.3.6 Stack容器
Stack容器是Vector的一个子类,实现了一个标准的后进先出的栈。
特点:后进先出。通过5个操作方法对Vector进行扩展,允许将向量视为堆栈。
//实例化栈容器
Stack<String> stack = new Stack<>();
应用:
//匹配符号的对称性
public void symmetry(){
String str="...{.....[....(....)...]....}..(....)..[...].(.).";
//实例化Stack
Stack<String> stack = new Stack<>();
//假设修正法
boolean flag = true;//假设是匹配的
//拆分字符串获取字符
for(int i=0;i<str.length();i++){
char c = str.charAt(i);
if(c == '{'){
stack.push("}");
}
if(c == '['){
stack.push("]");
}
if(c == '('){
stack.push(")");
}
//判断符号是否匹配
if(c == '}' || c==']' ||c==')'){
if(stack.empty()){ //右括号是多余的
flag = false;
break;
}
String x = stack.pop();//获取栈顶元素 )
//此时,c为 )
if(x.charAt(0)!=c){ //右括号是非所期待的
flag = false;
break;
}
}
}
if(!stack.empty()){ //直到全部出栈,左括号也没有找到与之对应的右括号
flag = false;
}
System.out.println(flag);
}
3.3.7 LinkedList容器类
LinkedList底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。
-
Link标准
LinkedList实现了List接口,所以LinkedList 是具备List的存储特征的(有序,元素有重复)
List<String> list = new LinkedList<>();
-
非List标准
LinkedList<String> linkedList = new LinkedList<>();
3.3.8 LinkedList源码分析
-
结点类
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
-
成员变量
transient int size = 0; transient Node<E> first; transient Node<E> last;
-
添加元素
public boolean add(E e) { linkLast(e); return true; }
void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
l l=1111 l=2222 尾部 last=1111 last=2222 last=3333 首部不变 first=1111 first=1111 first=1111 -
头尾添加元素
public void addFirst(E e) { linkFirst(e); }
private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; }
f=null f=1111 f=2222 首 first=1111 first=2222 first=3333 尾部不变 last=1111 last=1111 last=1111 public void addLast(E e) { linkLast(e); }
void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
3.4 Set接口
Set接口继承自Collection,Set接口中没有新增方法,方法和Collection保持一致。
3.4.1 Set接口特点
无序:Set中的元素没有索引,只能遍历查找。
不可重复:不允许加入重复的元素,新元素如果和Set中某个元素通过equals()方法对比为true,则只能保留一个。
Set常用的实现类:HashSet、TreeSet等, 一般使用HashSet。
3.4.2 HashSet容器类
HashSet是一个没有重复元素的集合,不保证元素的顺序。而且HashSet允许有null元素。采用哈希算法实现,底层实际用HashMap实现(HashSet本质是一个简化版的HashMap),因此,查询和增删效率都很高。
-
Hash算法也称为散列算法。
-
Set<String> set = new HashSet<>();
-
无序:
元素在存放时,并不是有序存放也不是随机存放的,而是对元素的哈希值进行运算决定元素在数组中的位置。
-
不重复:
当两个元素的哈希值进行运算后得到相同的在数组中的位置时,会调用元素的 equals()方法判断两个元素是否相同。如果元素相同则不会添加该元素,如果不相同则会使用单向链表保存该元素。
依赖于hashcode()和equals()方法(添加到set容器的元素需要重写这两个方法)
//获取元素,在set容器中没有索引,所以没有get(index)方法
for(String str:set){
System.out.println(str);
}
3.4.3 TreeSet容器类
TreeSet是一个可以对元素进行排序的容器。底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。TreeSet内部需要对存储的元素进行排序,因此,需要给定排序规则。
-
排序规则实现方式:
- 通过元素自身实现比较规则
- 通过比较器指定比较规则
-
//使用String的排序规则(元素自身实现比较规则) Set<String> set = new TreeSet<>();
-
//获取元素 for(String str:set){ System.out.println(str); }
-
通过元素自身实现比较规则
在元素自身实现比较规则时,需要实现 Comparable 接口中的 compareTo 方法,该方法中用来定义比较规则。TreeSet 通过调用该方法来完成对元素的排序处理。
public class Users implements Comparable<Users>{ ... //定义比较规则 //正数:大,负数:小,0:相等 @Override public int compareTo(Users o) { if(this.userage>o.getUserage()){ return 1; } if(this.userage==o.getUserage()){ return this.username.compareTo(o.getUsername()); } return -1; } }
Set<Users> set1 = new TreeSet<>();
-
通过比较器实现比较规则
通过比较器定义比较规则时,需要单独创建一个比较器,比较器需要实现Comparator 接口中的 compare 方法来定义比较规则。在实例化 TreeSet 时将比较对象交给TreeSet 来完成元素的排序处理。此时元素自身就不需要实现比较规则了。
-
创建比较器
public class StudentComparator implements Comparator<Student> { //定义比较规则 @Override public int compare(Student o1, Student o2) { if(o1.getAge()>o2.getAge()){ return 1; } if(o1.getAge()==o2.getAge()){ return o1.getName().compareTo(o2.getName()); } return -1; } }
Set<Student> set2 = new TreeSet<>(new StudentComparator());
-
3.5 单例集合使用案例
需求:
产生 1-10 之间的随机数([1,10]闭区间),将不重复的 10 个随机数放到容器中。
3.5.1 使用List类型容器实现
public class ListDemo {
public static void main(String[] args) {
Random random = new Random();
List<Integer> list = new ArrayList<>();
while (true){
//产生随机数
//int num = (int)(Math.random()*10+1);
int num = (random.nextInt(10)+1);
//判断当前元素在容器中是否存在
if(!list.contains(num)){
list.add(num);
}
//结束循环
if(list.size()==10){
break;
}
}
for(Integer i:list){
System.out.println(i);
}
}
}
3.5.2 使用Set类型容器实现
public class SetDemo {
public static void main(String[] args) {
//做了一个假排序
//Integer的hashcode方法的锅
Set<Integer> set = new HashSet<>();
while (true){
int num = (int)(Math.random()*10+1);
//将元素添加到容器中,由于set类型容器是不允许有重复元素的,所以不需要判断
set.add(num);
//结束循环
if(set.size()==10){
break;
}
}
for(Integer i:set){
System.out.println(i);
}
}
}
4.双例集合
4.1 Map接口介绍
Map接口定义了双例集合的存储特征,并不是Collection接口的子接口。存储特征是以key与value结构为单位进行存储。体现的是数学中的y=f(x)的概念。
Map与Collection的区别:
- Collection中的容器,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
- Map中的容器,元素是成对存在的(理解为现代社会的夫妻),每个元素由键与值两部分组成,通过键可以找到对应的值。
- Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。
- Map中常用的容器为:HashMap、TreeMap等。
Map的常用方法:
4.2 HashMap容器类
HashMap是Map接口的接口实现类,采用哈希算法实现,是Map接口最常用的实现类。底层采用了哈希存储数据,所以要求键不能重复,如果发送重复,新的值会替换旧的值。HashMap在查找、删除、修改方面都有非常高的效率。
-
添加元素
//实例化HashMap容器 Map<String,String> map = new HashMap<>(); //添加元素 map.put("a", "A"); String value = map.put("a","B");//返回被替换的value
-
获取元素
HashMap获取元素有三总方式,分别是get()、Set keySet()、Set<Map.Entry<k,v>> entrySet()
System.out.println("----------获取元素方式一-----------"); String val = map.get("a"); System.out.println(val); System.out.println("----------获取元素方式二------------"); map.put("b","B"); map.put("c","C"); map.put("d","D"); map.put("e","E"); //获取HashMap中所有的元素,可以使用KeySet方法与get方法一并完成 Set<String> keys = map.keySet(); for(String key:keys){ String v1 = map.get(key); System.out.println(key+"---"+v1); } System.out.println("----------获取元素方式三-------------"); Set<Map.Entry<String,String>> entrySet = map.entrySet(); for(Map.Entry<String,String> entry:entrySet){ String key = entry.getKey(); String v = entry.getValue(); System.out.println(key+"-----"+v); }
map.entrySet()返回的是一个EntrySet对象,里面是键值对的映射。
-
Map容器的并集操作
注意:map.putAll(map1); map1中key相同的会覆盖掉被调用map中的key的value值
System.out.println("-----------并集操作---------------"); Map<String,String> map2 = new HashMap<>(); map2.put("f","F"); map2.put("c","CC"); map2.putAll(map);//注意:map会覆盖当前中的 Set<String> keys2 = map2.keySet(); for(String key:keys2){ System.out.println("key:"+key+" Value: "+map2.get(key)); }
-
Map容器删除元素
System.out.println("----------删除元素---------------");
String v = map.remove("e");
System.out.println(v);
Set<String> key3 = map.keySet();
for(String key:key3){
System.out.println("key:"+key+" Value: "+map.get(key));
}
-
判断key或value是否存在
-
判断key是否存在
boolean flag = map.containsKey("a"); System.out.println(flag);
-
判断value是否存在
boolean flag2 = map.containsValue("B"); System.out.println(flag2);
-
4.3 HashMap的底层源码分析
4.3.1 底层存储
HashMap底层采用了哈希表。结合了数组和链表的优点(即查询快,增删效率也快)哈希表的本质就是“数组+链表”。
在jdk1.8在链表尾部插入,1.7在表头插入。
注意是单向链表,转为红黑树的前提是数组的长度>64,并且结点个数>8时转为红黑树,结点个数<6时转为链表。
4.3.2 重要成员变量
//初始容量为16,一定是2的次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,上限
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子:75%,数组用到75%时就去扩容,16*0.75=12
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//TREEIFY阈值:8,链表—>红黑树
static final int TREEIFY_THRESHOLD = 8;
//UNTREEIFY:6,红黑树->链表
static final int UNTREEIFY_THRESHOLD = 6;
//MIN_TREEIFY:64,数组长度超过64时,才进行红黑树和链表的转换
static final int MIN_TREEIFY_CAPACITY = 64;
//键值对的数量
transient int size;
//储存链表以及红黑树
transient Node<K,V>[] table;
4.3.3 HashMap中存储元素的结点类型
- Node类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
- TreeNode类:TreeNode类继承了Node类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
...
}
- 继承关系:
4.3.4 数组初始化
在JDK1.8中HashMap对于数组的初始化采用的是延迟初始化,通过resize方法实现初始化处理,resize方法既实现数组初始化,也实现数组扩容处理。
map.put方法调用putval方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putval方法调用resize方法
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;
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) {
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;
}
resize()方法
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) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
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;
}
4.3.5 计算hash值
-
获取key对象的hashcode
首先调用key对象的hashcode()方法,获得key的hashcode值。
-
根据hashcode计算出hash值(要求在[0,数组长度-1]区间)
hashcode是一个整数,需要将它转换成[0,数组长度-1]的范围。要求转换后的hash值尽量均匀的分布在[0,数组长度-1]这个区间,减少“hash冲突"
-
一种简单和常用的算法是(相除取余算法)
hash值=hashcode%数组长度
可以让hash值均匀分布在[0,数组长度-1]的区间。但是,由于使用了“除法”,效率低下,jdk后来改进了算法。首先约定数组长度必须为2的整数幂,这样采用位运算即可实现取余的效果:hash值=hashcode&(数组长度-1)。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
static final int hash(Object key) { int h; //无符号右移16位(取前16位) //高16位与低16位做^运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { ... //将长度与hash方法得到的hash值做&运算,结果为最终存放在数组中的位置 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { ... } ... }
-
4.4 TreeMap容器类
TreeMap和HashMap同样实现了Map接口,所以,对于API的用法来说是没有区别的。HashMap效率高于TreeMap,TreeMap是可以对键排序的一种容器,在需要对键排序时可选用TreeMap。TreeMap底层是基于红黑树实现的。
在使用TreeMap时需要给定排序规则:
-
元素自身实现比较规则
//实例化TreeMap对象,调用空参构造器 Map<Users,String> map = new TreeMap<>(); //元素自身实现Comparable接口,重写compareTo方法 public class Users implements Comparable<Users>{ @Override public int compareTo(Users o) { ... } }
-
通过比较器实现比较规则
//实例化TreeMap对象,并将比较器对象传入 Map<Student,String> treeMap = new TreeMap<>(new StudentComparator()); //实体类 public class Student { ... } //比较器 public class StudentComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { ... } }
5.Iteraotr迭代器
5.1 Iterator迭代器接口介绍
对于遍历集合来说,list是有索引的,set是没有索引。
Collection接口继承了Iterable接口,在该接口中包含了一个名为iterator的抽象方法,所有实现了Collection接口的容器类对该方法做了具体实现。
iterator方法会返回一个Iterator接口类型的迭代器对象(该对象是“一次性的”),在该对象中包含了三个方法用于实现对单例容器的迭代处理。
Iterator对象的工作原理:
Iterator接口定义了如下方法:
- boolean hasNext(); //判断游标当前位置是否有元素,如果有返回true,否则返回false;
- Object next(); //获取当前游标所在位置的元素,并将游标移动到下一个位置;
- void remove(); //删除游标当前位置的元素,在执行完next后该操作只能执行一次;
5.2 迭代器的使用
5.2.1 使用Iterator迭代List接口类型容器
public class IteratorListTest {
public static void main(String[] args) {
//实例化容器
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
//获取元素
//获取迭代器对象 反复执行过程的标准 没有循环的能力
Iterator<String> iterator = list.iterator();
//方式一:在迭代器中,通过while循环获取元素
while (iterator.hasNext()){
String value = iterator.next();
System.out.println(value);
}
System.out.println("-------------------------");
//方式二:在迭代器中,通过for循环获取元素
for(Iterator<String> it = list.iterator();it.hasNext();){
String value = it.next();
System.out.println(value);
}
}
}
5.2.2 使用Iterator迭代Set接口类型容器
public class IteratorSetTest {
public static void main(String[] args) {
//实例化Set类型的容器
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
//方式一:通过while循环
//获取迭代器对象
//方式一:在迭代器中,通过while循环获取元素
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()){
String value = iterator.next();
System.out.println(value);
}
System.out.println("-----------------------");
//方式二:在迭代器中,通过for循环获取元素
for(Iterator<String> it = set.iterator();it.hasNext();){
String value = it.next();
System.out.println(value);
}
}
}
6.Collections工具类
Collections是一个工具类,提供了对Set、List、Map进行排序、填充、查找元素的辅助方法。该类中所有的方法都为静态方法。
//通过Collections工具类的sort方法进行排序
Collections.sort(list);
//通过Collections进行随机排序,洗牌操作
Collections.shuffle(list);