Java学习笔记Java学习笔记编程

Java编程的逻辑2

2018-06-18  本文已影响108人  啥也不说了

2018-06-18

第9章 列表和队列

9.1 ArrayList

  1. Java中的容器类可以粗略的映射到数据结构这门课。
  2. ArrayList内部有一个数组elementData,一般会有一些预留的空间,有一个整数size记录实际的元素个数。
  3. foreach语法是一个编译阶段的,编译器会编译为类似下面代码:
Iterator<Integer> it = intList.iterator();
while(is.hasNext()){
  //具体代码
}

所以foreach只支持Iterator类型的集合。


2018-06-19

  1. 迭代器接口
public interface Iterable<T>{
  Iterator<T> iterator();
}
public interface Iterator<E>{
  boolean hasNext();//判断是否还有元素没有访问
  E next();//返回下一个元素
  void remove();//删除最后返回的元素
}

Iterable和Iterator的区别:

  1. ListIterator
    除了iterator(),ArrayList还提供了两个返回Iterator接口的方法:
public ListIterator<E> listIterator();
public ListIterator<E> listIterator(int index);

ListIterator扩展了Iterator接口,增加了一些方法,向前遍历、添加元素、修改元素。

  1. 迭代器的陷阱
    关于迭代器,有一种常见的误用,就是在迭代过程中调用容器的删除方法,这种情况会运行时抛出异常:java.util.ConcurrentModificationException。例如:
public void remove(ArrayList<Integer> list){
  for(Integer a: list){
    if(a<100){
      list.remove;
    }
  }
}

但是我们可以使用迭代器的remove方法。例如:

public void remove(ArrayList<Integer> list){
  Iterator<Integer> it = list.iterator();
  while(it.hasNext()){
    if(it.next<100){
      it.remove();//调用这个方法之前,必须先调用next方法,不然会抛出java.lang.IllegalStateException。
    }
  }
}

这是为什么呢?因为迭代器内部会维护一些索引位置相关的数据,要求在迭代过程中,容器不能发生结构性变化,否则这些索引就失效了。调用容器的remove方法就改变了容器的结构,当然还有添加、插入操作,只修改元素的值不算结构变化。
那为什么迭代器的remove方法可以使用呢?因为ArrayList的iterator的实现方法中,remove不仅调用了list的remove方法,而且还更新了相关的变量的值。

  1. 迭代器的好处
    1)foreach语法简洁
    2)迭代器表示的是一种关注点分离的思想,讲数据的实际组织方式和数据的迭代遍历相分离,是一种常见的设计模式;
    3)从封装的思路上讲,迭代器封装了各种数据组织方式的迭代操作,提供了简单和一直的接口。
    4)在某些容器上,效率更高。例如LinkedList。
  2. ArrayList实现的接口
    主要介绍RandomAccess接口。
public interface RandomAccess{
}

没有定义任何代码。没有任何代码的接口称为标记接口,用于声明类的一种属性。
实现了RandomAccess的类可以随机访问,就是具备类似数组那样的特性,数据在内存中连续存放,根据索引值就可以定位到具体的元素,访问效率很高。
那是否声明RandomAccess有什么关系了?主要使用在某些算法代码中,可以根据这个实现选择更高效的实现。例如Collectios类中有一个方法binarySearch,在List中进行二分查找,它的实现是根据是否实现了RandomAccess而采用不同的实现机制。

  1. ArrayList的其他方法
    主要介绍静态asList方法。asList方法的结果是返回一个List,但是它的具体实现不是ArrayList,而是ArrayList里面的一个内部类。这个内部类用的数组就是外部传入的数组,没有拷贝,也不会动态改变大小,所以对数组的修改也会翻页到List中,对List调用add、remove方法会抛出异常。
Integer[] ints = new Integer[]{1, 2, 3};
List<Integer> list = Arrays.asList(ints);
System.out.println(list);
ints[2] = null;
System.out.println(list);
list.set(2, 4);
System.out.println(list);
System.out.println(ints[2]);
list.add(5);
System.out.println(list);

输出结果为:

[1, 2, 3]
[1, 2, null]
[1, 2, 4]
4
java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)

所以Arrays的静态方法asList,具体实现不是ArrayList的实现方法,而是Arrays的一个内部类。这个内部类使用的数组就是传入的数组,当然也就不能动态改变数组大小,无法进行增删操作,但是改查还是可以的。
10.ArrayList的特点
必须了解每种数据结构的特点,不同的场合选用不同的数据结构。
对于ArrayList,他的特点是内部采用动态数组实现,所以:
1)可以随机访问——动态数组,按照索引位置可以直接方法,效率为O(1)
2)除非数组已经排序,否则按照内容访问效率很低,具体是O(n),n为数组长度
3)添加元素的效率还可以,重新分配和复制数组的开销被平摊了,具体效率为O(n),n为添加n个元素
4)插入和删除元素的效率比较低,因为需要移动元素,具体为O(n)

  1. ArrayList容器是线程不安全的,且插入和删除元素效率低。

9.2 LinkedList

  1. LinkedList除了实现了List之外,还实现了Deque和Queue接口,可以按照队列、栈和双端队列的方式进行操作。
  2. Queue
    Queue是队列接口,扩展了Collection,它的主要操作有三个:
  1. Deque
    Java没有单独的栈接口,栈相关接口包括在了表示双端队列的接口Deque中。主要有三个方法:

实现原理

  1. ArrayList的内部实现是数组,元素在内存中是连续存放的,但LinkedList不是。它的实现是双向链表,每个元素在内存中是单独存放的,元素之间通过链接连在一起。为了表示链接关系,需要一个节点的概念。
  2. LinkedList特点分析
    1)按需分配空间,不需要预先分配很多空间
    2)不可用随机访问,按照索引访问效率比较低,必须从头或尾顺序访问,效率为O(N/2)
    3)不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)
    4)在两端添加删除元素效率很高,为O(1)
    5)在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身效率很高为O(1)

9.3 剖析ArrayDeque

  1. ArrayDeque是一个双端队列,基于数组实现。一般而言,由于需要移动元素,数组的插入和删除效率比较低,但ArrayDeque的效率却非常高。构造函数如下:
public ArrayDeque();
//初始分配的空间至少容纳这么多元素,但空间不是正好这么大
public ArrayDeque(int numElements);
public ArrayDeque(Collection<? extends E> c);

ArrayDeque实现了Deque接口,它的长度也是没有限制的。

  1. 特点分析
    1)在两端添加、删除元素效率很高
    2)根据元素内容查找和删除的效率比较低,为O(N)
    3)没有索引位置的概念,不能根据索引位置进行操作。

20180627

第10章 Map和Set

10.1 剖析HashMap

  1. keySet()、values()、entrySet()有一个共同点,他们返回的都是视图,不是复制的值,基于返回值的修改都会修改Map自身。

10.1.1 实现原理

1.内部组成
HashMap内部有如下几个主要的实例变量:

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TYBLE;
transient int size;
int threhold;
final float loadFactor;
static class Entry<K,V> implements Map.Entry<K,V>{
  final K key;
  V value;
  //next表示,如果该位置已经有hash,则把相同hash的对象存到这里
  Entry<K,V> next;
  int hash;
  Entry(int h,K k,V v,Entry<K,V> n){
    value =v;
    next = n;
    key=k;
    hash=h;
  }
}
hash内存存储示例.png

其中key和value分别表示建和值,next指向下一个Entry节点,hash是key的hash值。直接存储hash是为了比较时加快计算。

  1. 保存键值对
    1)如果是第一次保存,需要先给方法分配实际空间,默认空间大小为16,threshold为12;
    2)检查key是否为null,如果为nul进行putForNullKey的单独处理
    3)计算key的hash值,先利用key类型自带的Hash值,然后进行位运算,目的是随机和均匀性;
    4)通过indexFor方法计算应该将键值对放到table的哪个位置,代码为
static int indexFor(int h, int length){
  //HashMap中,length为2的幂次方,h&(length-1)等同于h%length
  return h&(length-1);
}

5)遍历代码确认key是否已存在。先比较hash,因为整数比较速度快;如果可以找到,直接修改value值即可;
6)如果没找到,调用addEntry添加一条记录。如果空间是够的,不需要resize。如果空间不够,并且对应的位置已经插入过对象了,检查代码,调用resize对table进行扩展,扩展策略是乘2。

  1. 查找方法
    查找流程可以汇总如下:
    1)计算键的hash值,代码为:
int hash = (key==null)?0:hash(key);

2)根据hash找到table中对应链表,代码为:

table[indexFor(hash,table.length)]

3)在链表中遍历查找,遍历代码;
4)逐个比较,先通过hash快速比较,hash相同再通过equals比较;

indexFor计算出的是数组中的位置,数组中位置一致不代表hash值一致
HashMap可以方便高效的按照键进行操作,但是如果按照值进行操作,就需要遍历。

  1. 根据键删除键值对
    1)计算hash,根据hash找到对应的table索引;
    2)遍历table[i],查找待删除节点,使用变脸prev指向前一个节点,next指向后一个节点,e指向当前节点;
    3)判断是否找到,依然是比较hash值,hash值相同时再使用equals方法比较
    4)删除的逻辑就是让长度减小,然后让待删除节点的前后节点链接麒麟,如果待删除节点是第一个节点,则让table[i]直接指向后一个节点;
  2. 小结
    HashMap里面有个hash表,即数组table,每个元素table[i]指向一个单向链表,根据键存取值,用键算出hash值,取模得到数组中的索引位置,然后操作table[i]指向的单向链表。
    存取的时候根据键的hash值,只在对应的链表中操作,不会访问别的链表,在对应链表的访问中,先使用hash判断,如果相同在使用equals方法比较。这就要求,相同的对象hash必须相同,如果键是自定义的类,就特别需要注意这一点。Java 8对HashMap做了优化,如果冲突较多(具体是至少8个元素,且总的键值对个数至少是64)的时候就会转换一个平衡二叉树,提高查询效率。

10.1.2 小结

HashMap可以方便的按照键存取值,内部使用数组链表和哈希的方式进行实现,所以:
1)根据键保存和获取的效率都很高,为O(1) ,每个单向链表往往只有一个或少数个节点,根据hash值就可以快速定位
2)HashMap中的键值对没有顺序,因为hash是随机的。
HashMap不是现成安全的,还有一个HashTable,是Java中最早的容器之一,实现了Map接口,没有特别的有点,就是线程安全的。HashMap中键值都可以为null,而Hashtable不可用。在高并发的情况下,推荐使用ConcurrentHashMap。

20180628

10.2 剖析HashSet

本节介绍Set的一个重要实现,HashSet。其中Set表示接口,利用Hash方式实现了接口。

10.2.1 用法

  1. Set表示没有重复元素,且不保证顺序的容器接口,扩展了Collection,但没有定义任何新的方法。但是对于其中的一些方法,它有着自己的规范。其中add和addAll方法着重说明:

同HashMap一样,对象相同,hash必须相同。如果使用自定义对象,一定要注意。注意重写hash和equals方法。否则可能会把同一份存储两遍。

  1. HashSet的应用场景
    1)排重。如果对排重后的元素没有顺序要求,则HashSet可以方便的用于排重。
    2)保存特殊值,比如黑名单等。
    3)集合运算。使用Set可以很方便的进行集合运算,例如交集、并集等。

10.2.2 实现原理

HashSet内部是使用HashMap实现的,有一个HashMap实例变量:

private transient HashMap<E,Object> map;

HashSet相当于只有键,值都是相同的固定值,这个值定义为:

private static final Object PRESENT = new Object();

10.2.3 小结

充分利用了HashMap,所以有以下特点:
1)没有重复元素;
2)可以高效的添加、删除、判断元素是否存在,效率都为O(1);
3)没有顺序
如果需要保持添加的顺序,可以使用LinkedHashSet,如果需要排序,使用TreeSet;


20180629

10.3 排序二叉树

10.3.1 基本概念

  1. 二叉树是一棵树,每个节点最多有两个子节点,一左一右,左边的称为左孩子,右边的称作右孩子。
  2. 数有深度或者高度的概念,从根节点到叶子节点经过的节点个数的最大值称为树的高度。如果只有一个根节点,则高度为1。
  3. 排序二叉树也是树,顺序是这样的:
    1)如果左子树的树不为空,则左树上的所有节点都小于该节点;
    2)如果右子树不为空,则右树上的所有节点都大于该树。

10.3.2 基本算法

  1. 查找
    排序二叉树有一个很好的优点,按值在其中查找一个元素时很方便、也很高效,基本步骤为:
    1)首先与根节点比较,如果相同,就找到了;
    2)如果小于根节点,则到左子树中递归查找;
    3)如果大于根节点,则到右子树中查找
    此外,二叉树很方便查找最大值和最小值。
  2. 遍历
    排序二叉树可以方便的按序遍历。使用递归的方式:
    1)访问左子树——一直到最左边的节点
    2)访问当前节点
    3)访问右子树
    也可以不用递归的方式按序遍历。
  3. 插入
    在排序二叉树中,插入元素首先需要找插入位置,即新节点的父节点,步骤为:
    1)与当前节点比较,如果相同,表示已经存在,不能再插入了
    2)如果小于当前节点,则到左子树中寻找,如果左子树为空,则当前节点为要寻找的父节点;
    3)如果大于当前节点,泽道右子树中寻找,如果右子树为空,则当前节点就为父节点。
    找到父节点之后即可插入,如果元素小于父节点,则作为左孩子插入,否则作为右孩子插入。
  4. 删除
    在排序二叉树中删除一个节点要复杂一些,有三种情况:
    1)节点为叶子节点——直接删掉,把父节点对应的子节点为空
    2)节点只有一个孩子节点——把孩子节点和父节点直接关联起来
    3)节点有两个孩子几点——找到后继节点

10.3.3 平衡二叉树

排序二叉树的形状与插入的数值密切相关,极端情况下可能变为一个链表。退化为链表之后,排序二叉树的优点就没有了,如果排序二叉树高度不平衡,效率也会变的很低。

10.4 剖析TreeMap

TreeMap是按键有序,而非按值有序。

10.4.1 基本用法

  1. 构造方法
public TreeMap()
public TreeMap(Comparator<? super K> comparator)

如果使用默认构造方法,则要求Map中的键实现Comparable 接口,TreeMap内部进行比较时会调用Comparable接口中的compareTo方法。
第二个构造方法在内部比较时会调用这个方法,不再要求键实现Comparable接口。

  1. 字符串有一个常量会忽略大小写。所以在创建忽略大小写的TreeMap时可以使用new TreeMap(String.CASE_INSENSITIVE_ORDER)
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
  1. Collections.有一个反转排序的方法:reverseOrder()。这个方法返回一个逆序的比较器。如果是一个逆序的忽略大小写的方法可以用:
TreeMap<Object, Object> map = new TreeMap<>(Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER));
  1. TreeMap会根据比较结果进行排重。及时键实际上不同,但是比较结果相同就会被认为相同,键只会保存一份。
@org.junit.Test
    public void test() {
        TreeMap<String,String> map = new TreeMap<>(Collections.reverseOrder(String.CASE_INSENSITIVE_ORDER));
        map.put("t", "t");
        map.put("T", "T");
        System.out.println(map.toString());
    }

输出结果为:{t=T}

注意:后面put的时候,put生效了。而不是因为键一样就放弃更新。

  1. 扩展功能
    和HashMap一样,实现了Map接口。但是有序性又扩展了功能。例如SortedMap和NavigableMap接口,可以按键的顺序进行查找,如第一个、最后一个、某一范围的键、邻近键等。

20180705

10.4.2 实现原理

TreeMap的内部是用红黑树实现的,红黑树是一种大致平衡的排序二叉树。

  1. 内部组成
static final class Entry<K,V> implements Map.Entry<K,V>{
  K key;
  V value;
  Entry<K,V> left=null;//指向左孩子
  Entry<K,V> right=null;//指向右孩子
  Entry<K,V> parent;//指向父节点。如果是根节点,为null
  boolean color=BLACK;
  ENTRY<K key,V value, Entry<K,V> parent){
    this.key=key;
    this.value=value;
    this.parent=parent;
  }
}
  1. 保存键值对
    循环找到父节点,并插入作为其左孩子或右孩子,然后调整树的大致平衡。
  2. 根据键获取值
public V get(Object key){
  Entry<K,V> p = getEntry(key);
  return (p==null? null:p.value);
}

getEnrty的过程:如果比较器不为空,调用但都的方法getEntryUsingComparator;否则,假定key实现了Comparable接口,使用接口的compareTo方法进行比较,找的逻辑也很简单。从根开始找,小于往左边找,大于往右边找,知道找到为止。

  1. 查看是否包含某个值
    采用遍历的方法获取值,不是从根节点开始遍历,而是从第一个节点开始遍历。
  2. 根据键删除键值对
    根据key找到节点,然后删除节点。主要考虑是链路的维护,即叶子节点、只有一个孩子和有两个孩子。

10.4.3 总结

和HashMap相比,TreeMap内部使用了红黑树实现,所以有以下特点:
1)按键有序。
2)为了按键有序,TreeMap的键需要实现Comparable接口或者通过构造方法提供一个比较器对象。
3)根据键保存、查找、删除的效率比较高,为O(h),h为树的高度,在树平衡的情况下,h为log2(N),N为节点数。
不要求键有序,使用HashMap,如果 考虑键有序,用Treemap。


20180711

10.5 剖析TreeSet

同HashSet基于HashMap一样,TreeSet基于TreeMap。

10.5.1 用法

  1. TreeSet有两个构造方法,带比较器的和默认。
    TreeSet实现了两点:排重和有序。排重基于比较结果,如果结果为0即视为相同,只会保留第一个元素。

10.5.2 实现原理

  1. TreeSet有以下成员:
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();

10.5.3 小结

1)没有重复元素,保留第一个元素。
2)添加、删除元素、判断元素是否存在,效率比较高,为O(log2(N)),N为元素个数。
3)有序;
4)为了有序,TreeSet要求元素实现Comparable接口或通过构造方法提供一个Comparator对象。

10.6 剖析LinkedHashMap

它是HashMap的子类,保持元素按插入或访问有序。


20180717

10.6.1 基本用法

LinkedHashMap是HashMap的子类,但内部还有一个双向链表。即每个键值对即位于hash表中,也位于这个双向链表中。
LinkedHashMap的插入顺序好理解,那么什么是访问顺序呢?
访问顺序是指对一个键进行get/put操作后,其对应的键值会移到链表末尾,所以最末尾的是最近访问的,最开始的是最久没被访问的。这种顺序就是顺序访问。
在LinkedHashMap的五个构造方法里面,其中4个都是按插入顺序的,只有一个构造方法可以按访问顺序。

public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)

其中accessOrder就是用来指定是否按访问顺序。
那么什么时候使用按访问顺序呢?可以利用LinkedHashMap的按访问顺序特点快速实现LRU缓存。
LRU缓存的全称是Least Recently Used,即最近最少使用。它的思路是最近刚被使用的很快再次被使用的可能性最高,而最久没被访问的很快再次被使用的可能性最低,所以被优先清理。
默认情况下,LinkedHashMap对容量没有做限制,但他可以轻易的做到,有一个protected方法:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest){
  return false;
}

在添加元素到LinkedHashMap后,LinkedHashMap会调用这个方法,传递的参数是最久没被访问的键值对,如果这个方法返回true,则这个最久的键值对就会被删除。LinkedHashMap的这个值永远为false,但是可以通过继承的方式来实现。例如:

public class LRUCache<K,V> extends LinkedHashMap<K,V>{
  private int maxEntries;
  public LRUCache(int maxEntries){
    super(16,0.75,true);
    this.maxEntires=maxEntries;
  }
  @Override
  protected boolean removeEldestEntry(Entry<K,V> eldest){
    return size>maxEntries;
  }
}

这样当超过最大值的时候就会自动删除了。

10.7 剖析EnumMap

如果需要一个Map,且键是枚举类的时候,应该使用EnumMap,简洁、方便、安全;实现原理上内部有两个数组,长度相同,一个表示所有可能的键,一个表示对应的值,值为null表示没有该键值对,键都有一个索引,根据所有可直接访问和操作其键值,效率很高。
构造方法比较特殊,需要指定枚举类型。

10.8剖析EnumSet

这是一个非常高效的实现Set接口的类。前面介绍的HashSet/TreeSet都是用HashMap/TreeMap实现的,但EnumSet不是。
EnumSet的实现与EnumMap没有任何关系,而是用极为精简和高效的位向量实现的。位向量是计算机程序中解决问题的一种常用方法,有必要了解和掌握
除了实现机制,使用方法也有一些不同。

10.8.1 基本用法

与TreeSet/HashSet不同,EnumSet是一个抽象类。虽然不能直接new对象,但是它提供了若干个静态工厂方法,可以创建EnumSet类型的对象,比如:

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType)

noneOf会创建一个指定枚举类型的EnumSet,不包含任何元素。创建的EnumSet对象的实际类型是EnumSet的子类。

10.8.2 应用场景

使用目的和TreeSet、HashSet一致。

10.8.3 实现原理

位向量:就是用一个位表示一个元素的状态,用一组位表示一个集合的状态,每个位对于一个元素,而状态只可能有两种。
例如有一个表示一周7天的枚举DayEnum。一个DayEnum的集合就可以用一个字节byte表示,最高位不用,设为0,最右边的位对于最小的枚举,从右到左,每位对于一个枚举值,1表示包含该元素,0表示不包含该元素。
例如表示星期一,星期二,星期三,星期五的位向量结构图如下:
|0|0|0|1|0|1|1|1|
对应的整数就是23.。
位向量能表示的元素个数与向量长度有关,一个byte类型可以表示8个元素,一个long类型可以表示64个元素。EnumSet有两个子类:当长度小于64的时候,使用RegularEnumSet的long类型作为位向量,如果超过64为使用long类型数组的JumboEnumSet。
EnumSet自身没有记录元素个数的变量,也没有位向量,他们是子类维护的。

  1. Regular用一个long类型表示位向量,没有表示元素个数的变量,是实时计算出来的。
  2. 对于JumboEnumSet,它用一个long数组表示,有单独的size变量。

10.8.4 小结

对于只有两种状态,且需要进行集合运算的数据,使用位向量进行表示、位运算进行处理,是计算机程序中常用的一种思维方式。
Java中有一个更为通用的可动态扩展长度的位向量容器里BitSet,可以方便的对指定位置的位进行操作,与其他位向量进行位运算。

上一篇 下一篇

猜你喜欢

热点阅读