美文网首页
Java EE 第四篇 核心类库(一)集合

Java EE 第四篇 核心类库(一)集合

作者: XaviSong | 来源:发表于2020-11-05 21:23 被阅读0次

    一、泛型

    泛型,即“参数化类型”。就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定 义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

    (1)使用:

    1、泛型类:

    public class ClassName<T>{
        private T data;
        public T getData() {
            return data;
        }
        public void setData(T data) {
            this.data = data;
        }
    }
    

    2、泛型接口:

    public interface IntercaceName<T>{
        T getData();
    }
    
    //实现接口时,可以选择指定泛型类型,也可以选择不指定, 如下:
    //指定类型:
    public class Interface1 implements IntercaceName<String> {
        private String text;
        
        @Override
        public String getData() {
            return text;
        }
    }
    
    //不指定类型:
    public class Interface1<T> implements IntercaceName<T> {
        private T data;
        @Override
        public T getData() {
            return data;
        }
    }
    

    3、泛型方法:

    private static <T> T function(T a, T b) {}
    

    (2)泛型限制类型:

    在使用泛型时,可以指定泛型的限定区域
    例如: 必须是某某类的子类或 某某接口的实现类,格式:
    <T extends 类或接口1 & 接口2>
    
    类型通配符是使用?代替方法具体的类型实参。
    1 <? extends Parent> 指定了泛型类型的上界
    2 <? super Child> 指定了泛型类型的下界
    3 <?> 指定了没有限制的泛型类型
    

    (3)好处:

    1、 提高代码复用率
    2、 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
    

    注意:

    • 在编译之后程序会采取去泛型化的措施。
    • 也就是说Java中的泛型,只在编译阶段有效。
    • 在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加 类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

    二、Java SE 类集

    Java 中为了方便用户操作各个数据结构, 所以引入了类集的概念,有时候就可以把类集称为 java 对数据结构的实现。类集中最大的几个操作接口:Collection、Map、Iterator,这三个接口为以后要使用的最重点的接口。 所有的类集操作的接口或类都在 java.util 包中。

    1、Collection接口

    Collection 接口是在整个 Java 类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。在开发中不会直接使用 Collection 接口。而使用其操作的子接口:List、Set。

    //定义
    public interface Collection<E> extends Iterable<E>
    
    方法:

    2、List接口

    在整个集合中 List 是 Collection 的子接口,里面的所有内容都是允许重复的。

    //定义
    public interface List<E> extends Collection<E>
    
    方法:此接口对于 Collection 接口来讲有如下的扩充方法:
    常用的实现类有如下几个:

    使用频率:ArrayList(95%)、Vector(4%)、LinkedList(1%)

    ArrayList、Vector采用动态数组实现,前者线程不安全,后者安全。LinkedList采用链表实现。

    2.1、ArrayList

    ArrayList是List接口的子类,此类的定义如下:

    public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, Serializable
    

    此类继承了AbstractList 类。AbstractList是List接口的子类。AbstractList是个抽象类,适配器设计模式。ArrayLIst增加删除比较慢,查找比较快。创建时必须使用引用类型或包装类构造。

    ArrayList();//创建初始容量为10的空列表。
    ArrayList(int initialCapacity);
    ArrayList(Collection<? extends E> e)
    

    注意,注意对空列表进行空间为10的赋值是在空列表添加元素调用add方法的时候,内部扩容算法将新长度赋值为10,add方法的返回值永远为true。

    2.2、Vector

    定义:
    public class Vector<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, Serializable
    

    Vector 属于 Java 元老级的操作类,是最早的提供了动态对象数组的操作类,在 JDK 1.0 的时候就已经推出了此类的使用,只是后来在 JDK 1.2 之后引入了 Java 类集合框架。但是为了照顾很多已经习惯于使用 Vector 的用户,所以在 JDK 1.2 之后将 Vector 类进行了升级了,让其多实现了一个 List 接口,这样才将这个类继续保留了下来。

    Vector为可增长对象数组,区别与ArrayList在于线程安全,增加慢,查找快。

    构造方法:
    Vector();
    Vector(int initialCapacity);
    Vector(int initialCapacity, int capacityIncrement); //额外赋值扩容增量,无参构造方法默认增量为0,此时的扩容方法为翻一番。
    Vector(Collection<? extends E> e)
    
    与ArrayLIst的区别:

    2.3、LinkedList

    使用场景很少,定义:
    public class LinkedList<E> extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, Serializable
    

    此类继承了 AbstractList,所以是 List 的子类。但是此类也是 Queue 接口的子类,Queue 接口定义了如下的方法:

    3、Set接口

    重点:
    1. Set 接口也是 Collection 的子接口,与 List 接口最大的不同在于,Set 接口里面的内容是不允许重复的。
    2. Set 接口并没有对 Collection 接口进行扩充,基本上还是与 Collection 接口保持一致。因为此接口没有 List 接口中定义 的 get(int index)方法,所以无法使用循环进行输出。
    3. 此接口中有两个常用的子类:HashSet、TreeSet

    3.1 HashSet

    ava.util.HashSet 是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的 (即存取顺序不一致)。 java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持,利用键值部分不可重复将其用作HashSet,value部分都对应一个默认元素,键值部分存储时无序。

    HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证 元素唯一性的方式依赖于: hashCode 与 equals 方法。

    当一个对象被存进 HashSet 集合后,就不能修改这个对象中的那些参与计算的哈希值的字段了,否则,对象被修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为 的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中删除当前对象,从而 造成内存泄露。

    虽然HashSet无序,但是仍然可以通过遍历访问数据,借用Collection接口的方法,转换为数组即可:

    Set<String> all = new HashSet<String>(); // 实例化Set接口对象
    String[] str = all.toArray(new String[] {});// 变为指定的泛型类型数组
    for (int x = 0; x < str.length; x++) {
        System.out.print(str[x] + "、");
    }
    
    存储流程:

    给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方 式,才能保证HashSet集合中的对象唯一。如果想用HashSet存储并保证数据存储的顺序,可以使用在HashSet下面的一个子类 java.util.LinkedHashSet ,它是链表和哈希表组合的一个数据存储结构。

    3.2、TreeSet

    与 HashSet 不同的是,TreeSet 本身属于排序的子类。

    定义:
    public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, Serializable
    

    添加到TreeSet中的对象必须实现Comparable接口,才能实现排序。实现这个接口,需要实现里面的compareTo方法。

    TreeSet有序采用二叉树进行存储,内部通过TreeMap进行实现。

    什么是迭代器快速失败:

    迭代器遍历集合时,遍历集合本身。如果创建迭代器后的任何时间修改集合,除了通过remove方式,迭代器将抛出ConcurrentModificationException异常。因此在并发修改的情况下,迭代器快速失败,减少未来的不确定性风险。

    什么是迭代器安全失败:

    遍历的是集合的备份,不会出现快速失败(一般默认)。

    如果使用TreeSet添加自定义的对象,必须实现Comparable接口,提供compareTo方法。注意compareTo的实现,当两个元素相等时返回0,此时TreeSet对于后添加的元素拒绝,因为不接收两个同样的元素。

    补充:Comparator接口

    Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码 实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

    Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构 (如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序,需要实现一个compare方法。

    3.3关于重复元素判断

    Set 接口定义的时候本身就是不允许重复元素的,按照这个思路,如果现在真的是有重复元素的话,使用 HashSet 也同样可以进行区分。

    Set<Person> all = new HashSet<Person>();
    all.add(new Person("张三", 10));
    all.add(new Person("李四", 10));
    all.add(new Person("李四", 10));
    all.add(new Person("王五", 11));
    all.add(new Person("赵六", 12));
    all.add(new Person("孙七", 13));
    System.out.println(all);
    

    此时发现,并没有去掉所谓的重复元素,也就是说之前的操作并不是真正的重复元素的判断,而是通过 Comparable 接口间接完成的。 如果要想判断两个对象是否相等,则必须使用 Object 类中的 equals()方法。 从最正规的来讲,如果要想判断两个对象是否相等,则有两种方法可以完成:

    1. 第一步判断两个对象的编码是否一致,这个方法需要通过 hashCode()完成,即:每个对象有唯一的编码
    2. 第二步验证对象中的每个属性是否相等,需要通过 equals()完成。

    所以此时需要覆写 Object 类中的 hashCode()方法,此方法表示一个唯一的编码,一般是通过公式计算出来的。

    4、Iterator

    Iterator 属于迭代输出,基本的操作原理:是不断的判断是否有下一个元素,有的话,则直接输出。

    定义:
    public interface Iterator<E>
    

    要想使用此接口,则必须使用 Collection 接口,在 Collection 接口中规定了一个 iterator()方法,可以用于为 Iterator 接口进行实例化操作。

    方法:

    通过 Collection 接口为其进行实例化之后,一定要记住,Iterator 中的操作指针是在第一条元素之上,当调用 next()方 法的时候,获取当前指针指向的值并向下移动,使用 hasNext()可以检查序列中是否还有元素。

    应用:
    Collection<String> all = new ArrayList<String>();
        all.add("A");
        all.add("B");
        all.add("C");
        all.add("D");
        all.add("E");
        Iterator<String> iter = all.iterator();
        while (iter.hasNext()) {// 判断是否有下一个元素
            String str = iter.next(); // 取出当前元素
            System.out.print(str + "、");
        }
    }
    

    在使用 Iterator 输出的时候有一点必须注意,在进行迭代输出的时候如果要想删除当前元素,则只能使用 Iterator 接口中的 remove()方法,而不能使用集合中的 remove()方法。否则将出现未知的错误。

    Iterator 接口本身可以完成输出的功能,但是此接口只能进行由前向后的单向输出。如果要想进行双向输出,则必须 使用其子接口 —— ListIterator。

    五、ListIterator

    ListIterator 是可以进行双向输出的迭代接口,此接口定义如下:

    public interface ListIterator<E>
    extends Iterator<E>
    
    方法:

    但是如果要想使用 ListIterator 接口,则必须依靠 List 接口进行实例化。List 接口中定义了以下的方法:ListIterator listIterator()。

    此处有一点需要注意的是,如果要想进行由后向前的输出,则首先必须先进行由前向后的输出。因为要将迭代器的位置后移。

    六、Map接口

    多值接口,里面的所有内容都按照 keyvalue 的形式保存,也称为二元偶对象。与collection根接口同一级别,多值集合的根接口,存储的是一个个键值对,通过键来访问值,key不可以重复。

    定义:
    public interface Map<K,V>
    
    方法:

    Map 本身是一个接口,所以一般会使用以下的几个子类:HashMap、TreeMap、Hashtable

    6.1、HashMap

    定义:
    public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
    
    应用:
    map.put(1, "张三A");
    map.put(1, "张三B"); // 新的内容替换掉旧的内容
    map.put(2, "李四");
    map.put(3, "王五");
    String val = map.get(6);
    

    get方法根据指定的 key 找到内容,如果没有找到,则返回 null,找到 了则返回具体的内容。

    Map<Integer, String> map = new HashMap<Integer, String>();
    map.put(1, "张三A");
    map.put(2, "李四");
    map.put(3, "王五");
    Set<Integer> set = map.keySet(); // 得到全部的key
    Collection<String> value = map.values(); // 得到全部的value
    Iterator<Integer> iter1 = set.iterator();
    Iterator<String> iter2 = value.iterator();
    System.out.print("全部的key:");
    while (iter1.hasNext()) {
        System.out.print(iter1.next() + "、");
    }
    System.out.print("\n全部的value:");
    while (iter2.hasNext()) {
        System.out.print(iter2.next() + "、");
    }
    

    在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一 个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率 较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转 换为红黑树,这样大大减少了查找时间。 简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。

    初始桶数量是16,装载因子为0.75,超过装载因子时扩容,容量翻倍。

    构造方法:
    HashMap() 使用默认初始容量(16)和默认加载因子(0.75)构造一个空 HashMap
    HashMap(int initialCapacity) 使用指定的初始容量和默认加载因子(0.75)构造一个空 HashMap
    HashMap(int initialCapacity, float loadFactor) 使用指定的初始容量和加载因子构造一个空 HashMap
    HashMap(Map<? extends K,? extends V> m) 构造一个新的 HashMap ,其映射与指定的 Map相同。
    添加元素过程:

    装载因子过小,查找效率高,但是存储空间利用率低。装载因子过大,查找效率慢,但是存储空间利用率高。一般要选取平衡,选取好初始容量,装载因子默认0.75。

    已经存储在Map中的自定义对象如果作为键值存储不要修改它,否则由于hashcode方法和equals方法的限制,在修改键值和再散列后的场景下,都无法找到原有对象。

    6.2 HashTable与HashMap的区别

    6.3 关于Map集合的输出

    1、 使用 Map 接口中的 entrySet()方法将 Map 接口的全部内容变为 Set 集合
    2、 可以使用 Set 接口中定义的 iterator()方法为 Iterator 接口进行实例化
    3、 之后使用 Iterator 接口进行迭代输出,每一次的迭代都可以取得一个 Map.Entry 的实例
    4、 通过 Map.Entry 进行 key 和 value 的分离
    

    Map.Entry 本身是一个接口。此接口是定义在 Map 接口内部的,是 Map 的内部接口。此内部接口使用 static 进行定义, 所以此接口将成为外部接口。 实际上来讲,对于每一个存放到 Map 集合中的 key 和 value 都是将其变为了 Map.Entry 并且将 Map.Entry 保存在了 Map 集合之中。

    Map.Entry接口:
    方法:
    应用:
    Set<Map.Entry<String, String>> set = map.entrySet();// 变为Set实例
    Iterator<Map.Entry<String, String>> iter = set.iterator();
    while (iter.hasNext()) {
        Map.Entry<String, String> me = iter.next();
        System.out.println(me.getKey() + " --> " + me.getValue());
    }
    

    Map 集合中每一个元素都是 Map.Entry 的实例,只有通过 Map.Entry 才能进行 key 和 value 的分离操作。

    或使用foreach:
    Map<String, String> map = new HashMap<String, String>();
    map.put("ZS", "张三");
    map.put("LS", "李四");
    map.put("WW", "王五");
    map.put("ZL", "赵六");
    map.put("SQ", "孙七");
    for (Map.Entry<String, String> me : map.entrySet()) {
    System.out.println(me.getKey() + " --> " + me.getValue());
    }
    

    七、Collections类

    Collections 实际上是一个集合的操作类,此类的定义如下:

    public class Collections extends Object
    

    这个类与 Collection 接口没有任何的关系。是一个单独存在的类。

    方法:
    //很多,这里仅举例
    Collections.emptyList();// 空的集合
    
    List<String> all = new ArrayList<String>();
    Collections.addAll(all, "A", "B", "C");// 向集合增加元素
    

    但是,从实际考虑,使用此类操作并不是很方便,最好的做法就是使用各个接口的直接操作的方法完成。此类只是 一个集合的操作类。

    八、分析 equals、hashCode 与内存泄露

    在 java 的集合中,判断两个对象是否相等的规则是:

    (1)判断两个对象的 hashCode 是否相等 
            如果不相等,认为两个对象也不相等,完毕 
            如果相等,转入 2 (这一点只是为了提高存储效率而要求的,其实理论上没有也可以,但        如果没有,实际使用时效率会大大降低,所以我们 这里将其做为必需的。后面会重点讲        到这个问题。) 
            
    (2)判断两个对象用 equals 运算是否相等 如果不相等,认为两个对象也不相等 如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键)
    

    当一个对象被存进 HashSet 集合后,就不能修改这个对象中的那些参与计算的哈希值的字段了,否则,对象被修改后的哈 希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为 的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中删除当前对象,从而 造成内存泄露。

    相关文章

      网友评论

          本文标题:Java EE 第四篇 核心类库(一)集合

          本文链接:https://www.haomeiwen.com/subject/zjnavktx.html