chapter8 泛型
泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。
泛型写法:
public class Pair<U, V> {
U first;
V second;
public Pair(U first, V second) {
this.first = first;
this.second = second;
}
public U getFirst(){
return first;
}
public V getSecond(){
return second;
}
}
Object写法:
public class Pair {
Object first;
Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
public static void main(String[] args) {
Pair minmax = new Pair(1, 100);
Integer min = (Integer) minmax.getFirst();
Integer max = (Integer) minmax.getSecond();
Pair kv = new Pair("name", "老马");
String key = (String) kv.getFirst();
String value = (String) kv.getSecond();
}
}
对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通Pair类代码及其使用代码一样,将类型参数T擦除,替换为Object,插入必要的强制类型转换。Java虚拟机实际执行的时候,它是不知道泛型这回事的,只知道普通的类及代码。
Java泛型通过擦除实现的,类定义中的类型参数如T会被替换为Object,在程序运行过程中,不知道泛型的实际参数类型,比如Pair,运行中只知道Pair,而不知道Integer。
使用泛型的好处:
- 更好的安全性
- 更好的可读性
通过使用泛型,开发环境和编译器能确保不会用错类型,为程序多设置一道安全防护网。使用泛型,还可以省去繁琐的强制类型转换,再加上明确的类型信息,代码可读性也会更好。
比较好的blog——link
泛型方法
一个方法是不是泛型的,与它所在的类是不是泛型没有什么关系。
类型参数的限定
1.上界为某个具体类
public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {
public NumberPair(U first, V second) {
super(first, second);
}
public double sum() {
return getFirst().doubleValue() + getSecond().doubleValue();
}
public static void main(String[] args) {
NumberPair<Integer, Double> pair = new NumberPair<>(10, 12.34);
double sum = pair.sum();
}
}
指定边界后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型。
2.上界为某个接口
/**
* <T extends Comparable<T>> 递归类型限制
* T表示一种数据类型,必须实现Cpmparable接口,且必须可以与相同类型的元素进行比较
* @param arr
* @param <T>
* @return
*/
public static <T extends Comparable<T>> T max(T[] arr) {
T max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i].compareTo(max) > 0) {
max = arr[i];
}
}
return max;
}
3.上界为其他类型参数
public class DynamicArray<E> {
private static final int DEFAULT_CAPACITY = 10;
private int size;
private Object[] elementData;
public DynamicArray() {
this.elementData = new Object[DEFAULT_CAPACITY];
}
private void ensureCapacity(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity >= minCapacity) {
return;
}
int newCapacity = oldCapacity * 2;
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
public void add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
}
public E get(int index) {
return (E) elementData[index];
}
public int size() {
return size;
}
public E set(int index, E element) {
E oldValue = get(index);
elementData[index] = element;
return oldValue;
}
public E remove(int index) {
E oldValue = get(index);
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null;
return oldValue;
}
public void add(int index, E element) {
ensureCapacity(size + 1);
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
/**
* E是DynamicArray类型的参数,T是addAll()方法的参数,T的上界限定为E
* @param c
* @param <T>
*/
public <T extends E> void addAll(DynamicArray<T> c) {
for (int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
public static void main(String[] args) {
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
}
}
解析通配符
1.更简洁的参数类型限定
/**
* <? extends E> 表示有限定统配符,表示匹配E或E的子类型
* @param c
*/
public void addAll(DynamicArray<? extends E> c) {
for (int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
// 与上面的方法是等价的
public <T extends E> void addAll(DynamicArray<T> c) {
for (int i = 0; i < c.size; i++) {
add(c.get(i));
}
}
< T extends E>和<? extends E>之间的区别:
- < T extends E>用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面,泛型方法返回值前面。
- < ? extends E>用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。
2.理解通配符
public static int indexOf(DynamicArray<?> arr,Object elm)
public static <T> int indexOf(DynamicArray<T> arr,Object elm)
以上两种写法是等价的。DynamicArray<?>称为无限定通配符。
虽然通配符形式更加简洁,但上面两种通配符都有一个重要的限制:只能读,不能写。
问号就是表示类型安全无知,如果允许写入,Java就无法确保类型安全性,所以干脆禁止。
泛型方法到底应该用通配符的形式还是加类型参数?总结如下:
- 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。
- 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以能用通配符的就用通配符。
- 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
- 通配符形式和类型参数往往配合使用,比如,下面的copy方法,定义必要的类型参数,使用通配符表达依赖,饼接受更广泛的数据类型。
public static <D,S extends D> void copy(DynamicArray<D> dest, DynamicArray<S> src){
for(int i = 0;i < src.size();i++){
dest.add(src.get(i));
}
}
public static <D> void copy(DynamicArray<D> dest, DynamicArray<? extends D> src){
for(int i = 0;i < src.size();i++){
dest.add(src.get(i));
}
}
3.超类型通配符
< ? extends E>称为超类型通配符,表示E的某个父类型。作用,更灵活地写入。
public void copyTo(DynamicArray<? super E> dest) {
for (int i = 0; i < size; i++) {
dest.add(get(i));
}
}
DynamicArray<Integer> ints1 = new DynamicArray<>();
ints1.add(100);
ints1.add(34);
DynamicArray<Number> numbers1 = new DynamicArray<>();
ints1.copyTo(numbers1);
应用于Comparable/Comparator接口:
public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr){
...
}
class Base implements Comparable<Base>{
private int sortOrder;
public Base(int sortOrder){
this.sortOrder = sortOrder;
}
@Override
public int compareTo(Base o) {
if(sortOrder < o.sortOrder){
return -1;
}else if(sortOrder > o.sortOrder){
return 1;
}else{
return 0;
}
}
}
class Child extends Base{
public Child(int sortOrder){
super(sortOrder);
}
}
public class Test {
public static void main(String[] args) {
DynamicArray<Child> childs = new DynamicArray<>();
childs.add(new Child(20));
childs.add(new Child(80));
Child maxChild = DynamicArray.max(childs);
}
}
对于有限定的通配符形式<? extends E>,可以用类型参数限定替代,但超类通配符,则无法用类型参数替代。
<?>、<?super E>、<? extends E>之间的区别与联系:
- 它们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。
- <?super E>用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。
- <?>和<? extends E>用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象,它们可以用类型参数的形式替代,但通配符形式更为简洁。
4.使用泛型类、方法和接口
- 基本类型不能用于实例化类型参数
- 运行时类型信息不适用于泛型
- 类型擦除可能会引发一些冲突
instanceof后面是接口或类名,instanceof是运行时判断,也与泛型无关。
5.定义泛型类、方法和接口
- 不能通过类型参数创建对象。
- 泛型类类型不能用于静态变量和方法。
- 了解多个类型限定的语法。
T extends Base & Comparable & Serializable
Base为上界,Comparable和Serializable为上界接口。如果有上界类,类应该放在第一个,类型擦除时,会用第一个上界替换。
6.泛型与数组
不能创建泛型数组。
// 编译错误
Pair<Object, Integer>[] options = new Pair<Object, Integer>[]{new Pair("1yuan", 7), new Pair("2yuan", 2)};
不使用泛型数组,我们可以使用泛型容器。
- Java不支持创建泛型数组
- 如果存放泛型对象,可以使用原始类型的数组,或者使用泛型容器。
- 泛型容器内部使用Object数组,如果要转换泛型容器为对应类型的数组,需要使用反射。
chapter9 列表和队列
ArrayList
只要对象实现了Iterable接口,就可以使用foreach语法,编译器会转换为调用Iterable和Iterator接口的方法。
Iterable和Iterator的区别:
- Iterable表示对象可以被迭代,它有iterator()方法,返回Iterator对象,实际通过Iterator接口的方法进行遍历;
- 如果对象实现了Iterable,就可以使用foreach语法;
- 类可以不实现Iterable,也可以创建Iterator对象。
在迭代器内部会维护一些索引位置相关的数据,要求在迭代过程中,容器不能发生结构性变化(结构性变化就是添加、插入和删除元素,只是修改元素内容不算结构性变化),否则这些索引位置就失效了。
如何避免异常,可以使用迭代器的remove方法。
RandomAccess接口
RandomAccess接口是一个标记接口,用于声明类的一种属性。
实现了RandomAccess接口的类表示可以随机访问,可随机访问就是具备类似数组那样的特性,数据在内存是连续存放的,根据索引值就可以直接定位到具体的元素,访问效率很高。
- 可以随机访问,按照索引位置进行访问效率很高,效率为O(1)。
- 除非数组已排序,否则按照内容查找元素效率比较低,具体是O(N)。
- 添加元素的效率还可以,重新分配和复制数组的开销被平摊了,具体来说,添加N个元素的效率为O(N)。
- 插入和删除元素的效率比较低,因为需要移动元素,具体为O(N).
Vector实现了List接口,基本原理与ArrayList类似,内部使用synchronized实现了线程安全。
LinkedList
ArrayList随机访问效率很高,但插入和删除性能比较低;LinkedList同样实现了List接口,它的插入和删除性能好,随机访问效率低。
LinkedList还实现了队列接口Queue。
Queue的主要操作有三个:
- 在尾部添加元素(add、offer)
- 查看头部元素(element、peek),返回头部元素,但不改变队列
- 删除头部元素(remove、poll),返回头部元素,并且从队列中删除
在队列为空时,element和remove会抛出异常NoSuchElementException,而peek和poll返回特殊值null;在队列满时,add会抛出异常IllegalStateException,而offer只是返回false。
Java中没有单独的栈接口,栈相关方法包括在了表示双端队列的接口Deque中,主要有三个方法:
void push(E e);
E pop();
E peek();
- push表示入栈,在头部添加元素,栈的空间满了,push会抛出异常IllegalStateException;
- pop表示出栈,返回头部元素,并且从栈中删除,如果栈为空,会抛出NoSuchElementException;
- peek查看栈头部元素,不修改栈,如果栈为空,返回null。
LinkedList增加了一个接口Deque,可以把它看作队列、栈、双端队列,方便地在两端进行操作。
实现原理
ArrayList内部是数组,元素在内存是连续存放的,但LinkedList不是。LinkedList是链表,它的内部实现是双向链表,每个元素在内存都是单独存放的,元素之间都过链接连在一起。
LinkedList特点分析:
- 按需分配空间,不需要预先分配很多空间。
- 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
- 不管列表是否已排序,只要按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
- 在两端添加、删除元素的效率很高,为O(1)。
- 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。
ArrayDeque
ArrayDeque实现了Deque接口,同LinkedList一样,它的队列长度也是没有限制的,Deque扩展了Queue,有队列的所有方法,还可以看作栈,有栈的基本方法push/pop/peek,还有明确的操作两端的方法如addFirst/removeLast等。
ArrayDeque的搞笑来源于其内部是由循环数组的数据结构。
ArrayDeque特点分析:
- 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加N个元素的效率为O(N)。
- 根据元素内容查找和删除的效率比较低,为O(N)。
- 与ArrayList和LinkedList不同,没有索引位置的概念,不能根据索引位置进行操作。
如果只需要Deque接口,从两端进行操作,一般而言,ArrayDeque效率更高一些,应该被优先使用;如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除,则应该使用LinkedList。
chapter10 Map和Set
HashMap
keySet()、values()、entrySet()它们返回的都是视图,不是复制的值,基于返回值的修改会直接修改Map自身。
HashMap的基本实现原理,内部有一个哈希表,即数组table,每个元素table[i]指向一个单向链表,根据键存取值,用键算出hash值,取模得到数组中的索引位置bucketIndex,然后操作table[bucketIndex]指向的单向链表。
Java8对HashMap实现了优化,在同一个链表中的总键值个数不小于64时,Java8会将该链表转换为一个平衡的排序二叉树,以提高查询效率。
- 根据键保存和获取值的效率都很高,为O(1),每个单向链表往往只有一个或少数几个节点,根据hash值就可以直接快速定位;
- HahsMap中的键值对没有顺序,因为hash值是随机的。
HashMap中,键和值都可以为null,但在Hashtable中不可以。
HashSet
与HashMap类似,HashSet要求元素重写hashCode和equals方法,且对于两个对象,如果equals相同,则hashCode必须相同。
HashSet内部是用HashMap实现的,Map有键和值,HashSet相当于只有键,值都是相同的固定值。
HashSet特点:
- 没有重复的元素
- 可以高效地添加、删除元素、判断元素是否存在,效率都为O(1)
- 没有顺序
排序二叉树
TreeMap的实现中,用的并不是AVL树,而是红黑树。
TreeMap按键有序,为了实现有序,它要求要么键实现Comparable接口,要么创建TreeMap时传递一个Comparator对象。
- 按键有序,TreeMap同样实现了SortedMap和NavigableMap接口,可以方便地根据键的顺序进行查找。
- 为了按键有序,TreeMap要求键实现Comparable接口或通过构造方法提供一个Comparator对象。
- 根据键保存、查找、删除的效率比较高,为O(h),h为树的高度,在树平衡的情况下,h为log2(N),N为节点数。
LinkedHashMap
LinkedHashMap支持两种顺序:一种是插入顺序;另一种是访问顺序。
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
accessOrder就是用来指定是否按访问顺序,如果为true,就是访问顺序。默认情况下,LinkedHashMap是按插入顺序的。
EnumMap
EnumMap,键色类型为枚举类型。
EnumMap是保证顺序的,输出是按照键在枚举中的顺序的。
EnumSet
之前的Set接口的实现类HashSet/TreeSet,它们内部都是用对应的HashMap/TreeMap实现的,但EnumSet不是,它的实现与EnumMap没有任何关系,而是用极为精简和高效的位向量实现的。
chapter11 堆与优先级队列
PriorityQueue优先级队列,它实现了堆!
堆首先是一颗二叉树,但它是完全二叉树。
满二叉树一定是完全二叉树,但完全二叉树不要求最后一层是满的,但如果不满,则要求所有节点必须集中供在最左边,从左到右是连续的,中间不能有空的。
chapter12 通用容器类和总结
书中的342页,很具体的总结了!