第三部分 泛型与容器

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就无法确保类型安全性,所以干脆禁止。

泛型方法到底应该用通配符的形式还是加类型参数?总结如下:

  1. 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。
  2. 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以能用通配符的就用通配符。
  3. 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
  4. 通配符形式和类型参数往往配合使用,比如,下面的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>之间的区别与联系:

  1. 它们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。
  2. <?super E>用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。
  3. <?>和<? 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接口的类表示可以随机访问,可随机访问就是具备类似数组那样的特性,数据在内存是连续存放的,根据索引值就可以直接定位到具体的元素,访问效率很高。

  1. 可以随机访问,按照索引位置进行访问效率很高,效率为O(1)。
  2. 除非数组已排序,否则按照内容查找元素效率比较低,具体是O(N)。
  3. 添加元素的效率还可以,重新分配和复制数组的开销被平摊了,具体来说,添加N个元素的效率为O(N)。
  4. 插入和删除元素的效率比较低,因为需要移动元素,具体为O(N).

Vector实现了List接口,基本原理与ArrayList类似,内部使用synchronized实现了线程安全。

LinkedList

ArrayList随机访问效率很高,但插入和删除性能比较低;LinkedList同样实现了List接口,它的插入和删除性能好,随机访问效率低。

LinkedList还实现了队列接口Queue。

Queue的主要操作有三个:

  1. 在尾部添加元素(add、offer)
  2. 查看头部元素(element、peek),返回头部元素,但不改变队列
  3. 删除头部元素(remove、poll),返回头部元素,并且从队列中删除

在队列为空时,element和remove会抛出异常NoSuchElementException,而peek和poll返回特殊值null;在队列满时,add会抛出异常IllegalStateException,而offer只是返回false。

Java中没有单独的栈接口,栈相关方法包括在了表示双端队列的接口Deque中,主要有三个方法:

void push(E e);
E pop();
E peek();
  1. push表示入栈,在头部添加元素,栈的空间满了,push会抛出异常IllegalStateException;
  2. pop表示出栈,返回头部元素,并且从栈中删除,如果栈为空,会抛出NoSuchElementException;
  3. peek查看栈头部元素,不修改栈,如果栈为空,返回null。

LinkedList增加了一个接口Deque,可以把它看作队列、栈、双端队列,方便地在两端进行操作

实现原理

ArrayList内部是数组,元素在内存是连续存放的,但LinkedList不是。LinkedList是链表,它的内部实现是双向链表,每个元素在内存都是单独存放的,元素之间都过链接连在一起

LinkedList特点分析:

  1. 按需分配空间,不需要预先分配很多空间。
  2. 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
  3. 不管列表是否已排序,只要按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
  4. 在两端添加、删除元素的效率很高,为O(1)。
  5. 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。

ArrayDeque

ArrayDeque实现了Deque接口,同LinkedList一样,它的队列长度也是没有限制的,Deque扩展了Queue,有队列的所有方法,还可以看作栈,有栈的基本方法push/pop/peek,还有明确的操作两端的方法如addFirst/removeLast等。

ArrayDeque的搞笑来源于其内部是由循环数组的数据结构。

ArrayDeque特点分析:

  1. 在两端添加、删除元素的效率很高,动态扩展需要的内存分配以及数组复制开销可以被平摊,具体来说,添加N个元素的效率为O(N)。
  2. 根据元素内容查找和删除的效率比较低,为O(N)。
  3. 与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会将该链表转换为一个平衡的排序二叉树,以提高查询效率。

  1. 根据键保存和获取值的效率都很高,为O(1),每个单向链表往往只有一个或少数几个节点,根据hash值就可以直接快速定位;
  2. HahsMap中的键值对没有顺序,因为hash值是随机的。

HashMap中,键和值都可以为null,但在Hashtable中不可以。

HashSet

与HashMap类似,HashSet要求元素重写hashCode和equals方法,且对于两个对象,如果equals相同,则hashCode必须相同。
HashSet内部是用HashMap实现的,Map有键和值,HashSet相当于只有键,值都是相同的固定值。

HashSet特点:

  1. 没有重复的元素
  2. 可以高效地添加、删除元素、判断元素是否存在,效率都为O(1)
  3. 没有顺序

排序二叉树

TreeMap的实现中,用的并不是AVL树,而是红黑树。
TreeMap按键有序,为了实现有序,它要求要么键实现Comparable接口,要么创建TreeMap时传递一个Comparator对象。

  1. 按键有序,TreeMap同样实现了SortedMap和NavigableMap接口,可以方便地根据键的顺序进行查找。
  2. 为了按键有序,TreeMap要求键实现Comparable接口或通过构造方法提供一个Comparator对象。
  3. 根据键保存、查找、删除的效率比较高,为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页,很具体的总结了!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值