zoukankan      html  css  js  c++  java
  • 持有对象——容器

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

    “容器”(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编程思想》

  • 相关阅读:
    dede全功能手册V5.3
    php中一个完整表单处理实现代码
    dedecms织梦dede:sql标签,在dedecms中嵌套php语法等一些方法
    [二次开发]织梦二次开发增加一个或多个栏目的栏目内容
    java初学者笔记总结day13
    java初学者笔记总结day12
    java初学者笔记总结day11
    java初学者笔记总结day10
    java初学者笔记总结day9
    java初学者笔记总结day8
  • 原文地址:https://www.cnblogs.com/sean-zeng/p/11114648.html
Copyright © 2011-2022 走看看