首页 > 技术文章 > 持有对象——容器

sean-zeng 2019-07-01 16:05 原文

如果一个程序只包含固定数量且其生命期都是已知的对象,那么这是一个非常简单的程序。

“容器”(List、set、Map)提供了完善的方法来保存对象,并且保存数量巨大。

java中常用的集合框架体系图如下图所示,之后用到的再另作说明。

img

各种集合的特点

Collection(单列集合)

List(有序,可重复)

ArrayList

ArrayList:底层结构式数组、查询快、增删慢;线程不安全

为什么说ArrayList查询快、增删慢?

数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

Vector

Vector:底层结构式数组,查询快、增删慢;线程安全

LinkedList

LinkedList:底层结构式链表、查询慢、增删快;线程不安全

LinkedList添加了可以使其用作栈、队列或双端队列的方法

getFirst()element()完全一样,都返回列表的头(第一个元素),列表为空抛出异常。

removeFirst()remove()也完全一样,它们移除并返回列表的头,列表为空抛出异常,poll()稍有差异,返回null

addFirst()添加到列表的首端,与add()addLast()相反,后两个方法将某个元素插入列表的尾端。

removeLast()移除并返回列表最后一个元素。

Set(无序,不重复)

HashSet

HashSet

​ 底层数据结构是哈希表。
​ 哈希表依赖两个方法:hashCode()和equals()

HashSet的实现原理

HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的

HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个private static final Object PRESENT = new Object();。HashSet跟HashMap一样,都是一个存放链表的数组。

HashSet中add方法调用的是底层HashMap中的put()方法。而如果是在HashMap中调用put,首先会判断key是否存在,如果key存在则修改value值,如果key不存在这插入这个key-value。而在set中,因为value值没有用,也就不存在修改value值的说法,因此往HashSet中添加元素,首先判断元素(也就是key)是否存在,如果不存在则插入,如果存在则不插入,这样HashSet中就不存在重复值。

详细请参考:深入Java集合学习系列:HashSet的实现原理

TreeSet

TreeSet

​ 底层数据结构是红黑树。

​ 如何保证元素唯一性?

  1. 自然排序(元素具备比较性):让元素所属的类实现Comparable接口

    自然排序步骤:

    1 自然排序实现Comparable<T>接口

    2 重写Comparable接口中的Compareto方法

    public class Student implements Comparable<Student> {
        private String name;
        private int age;
    
        public Student() {
            super();
        }
    
        public Student(String name, int age) {
            super();
            this.name = name;
            this.age = age;
        }
      	//省略set、get方法
    
        /**
        *主要是实现这个类
        */
        @Override
        public int compareTo(Student s) {
            //return -1; //-1表示放在红黑树的左边,即逆序输出
            //return 1;  //1表示放在红黑树的右边,即顺序输出
            //return o;  //表示元素相同,仅存放第一个元素
            //主要条件 姓名的长度,如果姓名长度小的就放在左子树,否则放在右子树
            int num = this.name.length() - s.name.length();
            //姓名的长度相同,不代表内容相同,如果按字典顺序此 String 对象位于参数字符串之前,则比较结果为一个负整数。
            //如果按字典顺序此 String 对象位于参数字符串之后,则比较结果为一个正整数。
            //如果这两个字符串相等,则结果为 0
            int num1 = num == 0 ? this.name.compareTo(s.name) : num;
            //姓名的长度和内容相同,不代表年龄相同,所以还要判断年龄
            int num2 = num1 == 0 ? this.age - s.age : num1;
            return num2;
        }
      
        public static void main(String[] args) {     
              Set<Student> ts=new TreeSet<Student>();  
    
              ts.add(new Student("zhangsan",20));
              ts.add(new Student("lis",22));
              ts.add(new Student("wangwu",24));
              ts.add(new Student("chenliu",26));
              ts.add(new Student("zhangsan",22));
              ts.add(new Student("qianqi",24));
    
             for(Student s:ts){
                  System.out.println(s.getName()+"-----------"+s.getAge());
              }
          }
    }/*out
    lis-----------22
    qianqi-----------24
    wangwu-----------24
    chenliu-----------26
    zhangsan-----------20
    zhangsan-----------22
    */
    

  2. 比较器排序(集合具备比较性):让集合接收一个Comparator的实现类对象

    比较器排序步骤:

    1 单独创建一个比较类,这里以MyComparator为例,并且要让其继承Comparator<T>接口

    2 重写Comparator接口中的Compare方法

    3 在主类中使用下面的 构造方法

    TreeSet(Comparator<? superE> comparator) // 构造一个新的空 TreeSet,它根据指定比较器进行排序。
    

    新建MyComparator类:

    public class MyComparator implements Comparator<Student>{
     
        @Override
        public int compare(Student s1,Student s2) {
           // 姓名长度
            int num = s1.getName().length() - s2.getName().length();
            // 姓名内容
            int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
            // 年龄
            int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
            return num3;
        }
    }
    

    学生类(不需继承Comparetable接口)

     
    public class Student{
       private String name;
       private int age;
    
        public Student() {
        super();
        // TODO Auto-generated constructor stub
        }    
    
        public Student(String name, int age) {
        super();
        this.name = name;
        this.age = age;
        }
        //省略set/get
    }
    

    测试

     public static void main(String[] args) {
            //创建集合对象
            //TreeSet(Comparator<? super E> comparator) 构造一个新的空 TreeSet,它根据指定比较器进行排序。
            TreeSet<Student> ts=new TreeSet<Student>(new MyComparator());
            
            //将元素对象添加到集合对象中
            ts.add(new Student("zhangsan",20));
            ts.add(new Student("lis",22));
            ts.add(new Student("wangwu",24));
            ts.add(new Student("chenliu",26));
            ts.add(new Student("zhangsan",22));
            ts.add(new Student("qianqi",24));
            
            //遍历
            for(Student s:ts){
               System.out.println(s.getName()+"-----------"+s.getAge());
            }
        }
    

    可以把比较器写成匿名内部类的形式

    public class TreeSetDemo {
            public static void main(String[] args) {
            // 如果一个方法的参数是接口,那么真正要的是接口的实现类的对象
            // 而匿名内部类就可以实现这个东西
            TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
                @Override
                public int compare(Student s1, Student s2) {
                    // 姓名长度
                    int num = s1.getName().length() - s2.getName().length();
                    // 姓名内容
                    int num2 = num == 0 ? s1.getName().compareTo(s2.getName())
                            : num;
                   // 年龄
                    int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
                    return num3;
                }
            });
     
           //将元素对象添加到集合对象中
            ts.add(new Student("zhangsan",20));
            ts.add(new Student("lis",22));
            ts.add(new Student("wangwu",24));
            ts.add(new Student("chenliu",26));
            ts.add(new Student("zhangsan",22));
            ts.add(new Student("qianqi",24));
     
            // 遍历
            for (Student s : ts) {
                System.out.println(s.getName() + "---" + s.getAge());
           }
        }
    }
    

Map(双列集合)

A:Map集合的数据结构仅仅针对键有效,与值无关。

B:存储的是键值对形式的元素,键唯一,值可重复。

HashMap

底层数组+链表实现,可以存储null键和null值,线程不安全。

扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入。

从源码可以看出:HashMap底层数据结构为Node类型数组,Node类型的数据结构为链表,当链表长度大于8的时候,会转成红黑树

  • 初始化大小 HashMap默认初始大小为16,如有特殊情况需自定义初始化大小可调用HashMap(int initialCapacity) 自定义。

  • 负载因子:负载因子默认为 0.75,当 HashMap 当前已使用容量大于当前大小 的 0.75 倍时,自动扩容一倍空间,如有特殊情况下需要自定义初始化大小时可调用public HashMap(int initialCapacity, float loadFactor)进行自定义。

  • 树形阀值:树型阀值这个名字是作者根据字面意思自己翻译的,大家看看就好了,对应参数为TREEIFY_THRESHOLD,之前提到过 HashMap 的结构为 Node 型数组,而 Node 的数据结构为链表,树型阀值就是当链表长度超过这个值时,将 Node 的数据结构修改为红黑树,以便优化查找时间,默认值为8

    static final int TREEIFY_THRESHOLD = 8;

  • 初始值:HashMap有四种构造方法进行初始化

put方法

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)
      	//根据hash值来确认存放的位置。如果当前位置是空直接添加到table中
        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))))
          	//首先判断hashCode是否相等,随后判断key值是否相等,若hashCode和key都相等,则把p传递给e
            e = p; 
        else if (p instanceof TreeNode)
          	//否则如果p是红黑树,将p转成红黑树并赋值
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
          	//否则如果是链表,新增node
            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;
}

参考:详解 Java 8 HashMap 实现原理

HashTable

底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化。

TreeMap

底层数据结构是红黑树。(是一种自平衡的二叉树)

两种排序方式:

  • 自然排序

    让元素所属的类实现Comparable接口

  • 比较排序

    让集合接收一个Comparator的实现类对象

集合的常见方法及遍历方式

​ Collection:
add()、remove()、contains()、 iterator()、size()
​ iterator()
​ size()
​ 遍历方式:
​ 增强for、迭代器
​ 迭代器
​ |--List
get()
​ 遍历:普通for
​ |--Set
​ Map:
put()、remove()、 containskey()、containsValue()、 keySet()、 get()、 value()、 entrySet()、 size()
​ 遍历方式:
​ 根据键找值
​ 根据键值对对象分别找键和值。

添加一组元素

Arrays.saList()接受一个数组或是一个用逗号分隔的元素列表(使用可变参数),将其转化为List对象。

Collections.addAll()方法接受一个Collection对象,将一个数组或是可变参数,将元素添加到Collection中。

构建一个不包含元素的Collection,再使用Collections.addAll()要比Arrays.List()更快。Collections.addAll()是首选。

但是Collection.addAll()成员方法只能接受Collection作为参数,不如Arrays.asList()Collections.addAll()灵活,这两个都接受可变参数。

class Snow {}
class Powder extends Snow {}
class Light extends Powder {}
class Heavy extends Powder {}
class Crusty extends Snow {}
class Slush extends Snow {}

public class AsListInference {
    public static void main(String[] args) {
        List<Snow> snow1 = Arrays.asList(
            new Crusty(), new Slush(), new Powder());

        // 不能编译,编译器会混淆类型:
        // List<Snow> snow2 = Arrays.asList(
        //   new Light(), new Heavy());
        // 编译器报错:
        // found   : java.util.List<Powder>
        // required: java.util.List<Snow>

        // Collections.addAll() doesn't get confused:
        List<Snow> snow3 = new ArrayList<Snow>();
        Collections.addAll(snow3, new Light(), new Heavy());

        // 告诉编译器产生的目标类型:
        List<Snow> snow4 = Arrays.<Snow>asList(
              new Light(), new Heavy());
    }
} ///:~

上面的例子中Arrays.asList()若有继承关系会混淆产生的类型,需要加上产生的目标类型。Collections.addAll()对这种情况不会混淆。

容器的打印

必须使用Array.toString()来打印数组,但是打印容器无需任何帮助,无论是ListSet还是Map,都可以直接打印。

public class PrintingContainers {
  static Collection fill(Collection<String> collection) {
    //List、Set都继承自Collection
    collection.add("rat");
    collection.add("cat");
    collection.add("dog");
    collection.add("dog");
    return collection;
  }
  static Map fill(Map<String,String> map) {
    map.put("rat", "Fuzzy");
    map.put("cat", "Rags");
    map.put("dog", "Bosco");
    map.put("dog", "Spot");
    return map;
  }	
  public static void main(String[] args) {
    print(fill(new ArrayList<String>()));
    print(fill(new LinkedList<String>()));
    print(fill(new HashSet<String>()));
    print(fill(new TreeSet<String>()));
    print(fill(new LinkedHashSet<String>()));
    print(fill(new HashMap<String,String>()));
    print(fill(new TreeMap<String,String>()));
    print(fill(new LinkedHashMap<String,String>()));
  }
} /* Output:
[rat, cat, dog, dog]
[rat, cat, dog, dog]
[dog, cat, rat]
[cat, dog, rat]
[rat, cat, dog]
{dog=Spot, cat=Rags, rat=Fuzzy}
{cat=Rags, dog=Spot, rat=Fuzzy}
{rat=Fuzzy, cat=Rags, dog=Spot}
*///:~

迭代器——Iterator

如何做到只是使用容器,而不关心容器的类型?如何不重写代码就可以应用于不同类型的容器?比如:原来使用的是List,现在要使用Set,这时候,迭代器就用上了。

迭代器(也是一种设计模式)是一个对象,它的工作是遍历并选择序列中的对象,客户端程序员不必关心该序列底层的结构。

java中的Iterator只能单向移动,这个Iterator只能用来:

  • .iterator()准备好返回系列第一个元素
  • .next()获取序列下一个元素
  • .hasNext()判断是否还有元素
  • .remove()将迭代器最新返回的元素删除`
public class SimpleIteration {
    public static void main(String[] args) {
        List<String> strs = new ArrayList<String>();
        Collections.addAll(strs, "a", "b", "c");
        Iterator<String> it = strs.iterator();
        //第一种遍历方式,使用iterator
        while (it.hasNext()) {
            String p = it.next();
            System.out.print(p + " ");
        }
        System.out.println();
        //第二种遍历方式,foreach,当仅仅遍历时,可以使用这种方式
        for (String p : strs)
            System.out.print(p + " ");
        System.out.println();
        //iterator可以remove当前遍历的元素:
        it = strs.iterator();
        for (int i = 0; i < 1; i++) {
            //移除遍历的第一个元素
            it.next();
            it.remove();
        }
        System.out.println(strs);
    }
} /* Output:
a b c
a b c
[b, c]
*///:~

遍历List的方式有两种,使用iterator,使用foreach,使用iterator的方式可以移除最后遍历出来的那个元素。

List的迭代器——Listlterator

ListIterator是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。Iterator只能单向移动,但ListIterator可双向移动。下面是它的作用:

  • 可以产生当前位置的前一个和后一个元素的索引;
  • 可以使用set()方法替换它访问过的最后一个元素;
  • 可以通过调用listIterator(n)方法创建一个一开始就指向列表索引为n处的ListIterator
public class ListIteration {
    public static void main(String[] args) {
        List<String> pets = new ArrayList<String>();
        Collections.addAll(pets, "a", "b", "c", "d");
        ListIterator<String> it = pets.listIterator();
        while (it.hasNext())
            System.out.print(it.next() + ", " + it.nextIndex() +
                    ", " + it.previousIndex() + "; ");
        System.out.println();
        // Backwards:
        while (it.hasPrevious())
            System.out.print(it.previous() + " ");
        System.out.println();
        System.out.println(pets);
        it = pets.listIterator(3);
        while (it.hasNext()) {
            it.next();
            it.remove();
        }
        System.out.println(pets);
    }
} /* Output:
a, 1, 0; b, 2, 1; c, 3, 2; d, 4, 3;
d c b a
[a, b, c, d]
[a, b, c]
*///:~

栈——Stack

"栈" 通常指 “后进先出” 的容器。

LinkedList具有能够直接实现栈的所有功能的方法,因此可以直接将LinkedList作为栈使用。

public class Stack<T> {
  private LinkedList<T> storage = new LinkedList<T>();
  public void push(T v) { storage.addFirst(v); }
  public T peek() { return storage.getFirst(); }
  public T pop() { return storage.removeFirst(); }
  public boolean empty() { return storage.isEmpty(); }
  public String toString() { return storage.toString(); }
} ///:~

如果仅仅使用“栈”的功能,上面的代码是最合适的,而java.util.Stack因为继承的关系增加了很多多余的方法。

“栈”在编程语言中经常用来对表达式求值

队列——Queue

队列是一个“先进先出”的容器。

LinkedList可以用作Queue的一种实现。它实现了Queue接口。

public class QueueDemo {
    public static void printQ(Queue queue) {
        while (queue.peek() != null)
            System.out.print(queue.remove() + " ");
        System.out.println();
    }

    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<Integer>();
        Random rand = new Random(47);
        for (int i = 0; i < 10; i++)
            queue.offer(rand.nextInt(i + 10));
        printQ(queue);
        Queue<Character> qc = new LinkedList<Character>();
        for (char c : "Brontosaurus".toCharArray())
            qc.offer(c);
        printQ(qc);
    }
} /* Output:
8 1 1 1 5 14 3 1 0 1
B r o n t o s a u r u s
*///:~

offer()方法是queue相关的方法之一,将一个元素插入到队尾,或返回false。

peek()element()都在不移除的情况下返回队头,前一个在队列为空返回null,后一个抛出异常。

poll()remove()移除返回队头,前一个在队列为空时返回null,后一个抛出异常。

增加权重的队列——PriorityQueue

offer()方法插入一个对象,这个对象会在队列中自然排序。

可以通过提供自己的Comparator来修改这个顺序,这样当调用peek()poll()remove()方法时,获取的元素就是队列中优先级最高的元素。

Collection 和 Iterator

Collection是描述所有序列容器的共性的接口,它可能会被认为是一个“附属接口”。

实现Collection接口非常困恼,必须实现Collection的所有方法。这里不贴出例子。我们一般会选择实现Iterator,继承并创建迭代器,这种方式会容易得多。下面是实现Iterator的例子。

public class IteratorTest {
    protected List<Pet> pets = Arrays.asList(Pets.createArray(8 ));
    public Iterator<Pet> iterator(){
        return new Iterator<Pet>(){
            private int index = 0;
            @Override
            public boolean hasNext() {
                return index < pets.size();
            }
            @Override
            public Pet next() {
                return pets.get(index++);
            }
            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }
    public static void main(String[] args) {
        IteratorTest i = new IteratorTest();
        Iterator<Pet> iterator = i.iterator();
        while (iterator.hasNext()){
            Pet next = iterator.next();
            System.out.println(next.toString());
        }
    }

}

也可以继承AbstractCollection来实现遍历的功能。但是如果你的类继承了其他类,那就不能再继承AbstractCollection了。

Foreach与迭代器

Foreach之所以能工作,是因为 java SE5 引入了新的呗称为Iterable的接口,该接口包含一个能够产生Iteratoriterator()方法,并且Iterable接口被foreach用来在序列中移动。

只要实现了Iterable接口,都可以将它用于foreach中。

public class IterableClass implements Iterable<String> {	//实现Iterable接口
    protected String[] words = ("And that is how " +
            "we know the Earth to be banana-shaped.").split(" ");

    public Iterator<String> iterator() {	//返回iterator
        return new Iterator<String>() {
            private int index = 0;

            public boolean hasNext() {
                return index < words.length;
            }

            public String next() {
                return words[index++];
            }

            public void remove() { // Not implemented
                throw new UnsupportedOperationException();
            }
        };
    }

    public static void main(String[] args) {
        for (String s : new IterableClass())
            System.out.print(s + " ");
    }
} 

forearch语句可以用于数组或其他任何的Iterable,但这并不意味着数组肯定也是一个Iterable。任何自动包装也不会自动发生:

public class ArrayIsNotIterable {
    static <T> void test(Iterable<T> ib) {
        for (T t : ib)
            System.out.print(t + " ");
    }

    public static void main(String[] args) {
        test(Arrays.asList(1, 2, 3));
        String[] strings = {"A", "B", "C"};
        // An array works in foreach, but it's not Iterable:
        //! test(strings);
        // 必须转换成一个Iterable:
        test(Arrays.asList(strings));
    }
} 

适配器方法惯用法

我们可以通过适配器的方法产生“多样”的迭代器,比如:反向迭代器,随机迭代器。下面是反向迭代器的例子。


class ReversibleArrayList<T> extends ArrayList<T> {
    public ReversibleArrayList(Collection<T> c) {
        super(c);
    }
    public Iterable<T> reversed() {
        return new Iterable<T>() {
            public Iterator<T> iterator() {
                return new Iterator<T>() {
                    int current = size() - 1;

                    public boolean hasNext() {
                        return current > -1;
                    }
                    public T next() {
                        return get(current--);
                    }
                    public void remove() { // Not implemented
                        throw new UnsupportedOperationException();
                    }
                };
            }
        };
    }
}

public class AdapterMethodIdiom {
    public static void main(String[] args) {
        ReversibleArrayList<String> ral =
                new ReversibleArrayList<String>(
                        Arrays.asList("To be or not to be".split(" ")));
        // Grabs the ordinary iterator via iterator():
        for (String s : ral)
            System.out.print(s + " ");
        System.out.println();
        // Hand it the Iterable of your choice
        for (String s : ral.reversed())
            System.out.print(s + " ");
    }
} 

参考资料:
《java编程思想》

推荐阅读