【学习笔记】Java基础知识点——第7章·集合

第7章  集合

7.1  Java集合框架

数组其实就是一个集合。集合实际上就是一个容器,是一个对象,可以来容纳其它类型的数据。

集合不能直接存储基本数据类型,另外集合也不能直接存储Java对象,集合当中存储的都是Java对象的内存地址。(或者说集合中任何时候存储的都是引用。)

在Java中每一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构当中。不同的数据结构,数据存储方式不同。例如:数组、二叉树、链表、哈希表……

7.1.1 Iterator<E> iterator() 返回此集合中的元素的迭代器。

这种遍历(迭代)方式是所有Collection通用的一种方式,在Map集合中不能用,在所有Collection以及子类中使用。

第一步:获取集合对象的迭代器对象Iterator。

Iterator it = c.iterator();

第二步:通过以上获取的迭代器对象开始迭代/遍历集合。

  • boolean hasNext()   如果迭代具有更多元素,则返回 true 。
  • E next()    返回迭代中的下一个元素。注意:返回值类型是Object。
  • default void remove() 从底层集合中删除此迭代器返回的最后一个元素(可选操作)。
while (it.hasNext()){
    Object obj = it.next();//获取的对象类型都是Object
    System.out.println(obj);
}

 注意:集合结构只要发生改变,迭代器必须重新获取。如果还是用以前老的迭代器会出现异常(根本原因是,集合中的元素删除了,但是没有更新迭代器,迭代器不知道这个集合变化了)。

7.1.2 Iterator实现原理

Iterator接口的remove方法将会删除上次调用next方法时返回的元素。如果想要删除指定位置上的元素,仍然需要越过这个元素。next方法和remove方法调用之间存在依赖关系。如果调用remove之前没有调用next,将是不合法的。

7.1.3 ListIterator接口

链表是一个有序集合,每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表的中间。由于迭代器描述了集合中的位置,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。例如,Set数据类型中元素是完全无序的,因此,Iterator接口中没有add方法。集合类库提供了一个子接口ListIterator,其中包含add方法:

interface ListIterator<E> extends Iterator<E>{
    void add(E element);
    ...
}

与Collection.add不同,这个方法不返回boolean类型的值,它假定add操作总会改变链表。LinkedList类的listIterator方法返回了一个实现了ListIterator接口的迭代器对象。

ListIterator<String> iter = staff.listIterator();

add方法在迭代器位置之前添加一个新对象。例如,下面的代码将越过链表中的第一个元素,在第二个元素之前添加“Juliet”:

List staff = new LinkedList<String>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
ListIterator<String> iter = staff.listIterator();
iter.next();	//	跳过第一个元素
iter.add("Juliet");

如果多次调用add方法,将按照提供的次序把元素添加到链表中。他们被依次添加到迭代器当前位置之前。

 下面的程序使用了链表,他创建了两个列表,并将它们合并在一起,然后从第二个列表中每间隔一个元素删除一个元素。

public class LinkedListTest {

    public static void main(String[] args) {

        List<String> a = new LinkedList<>();
        a.add("Amy");
        a.add("Carl");
        a.add("Erica");

        List<String> b = new LinkedList<>();
        b.add("b1");
        b.add("b2");
        b.add("b3");
        b.add("b4");

        //把b中的单词合并成a
        ListIterator<String> aIter = a.listIterator();
        Iterator<String> bIter = b.iterator();
        
        while(bIter.hasNext()){
            if (aIter.hasNext()){
                aIter.next();
            }
            aIter.add(bIter.next());
        }
        System.out.println(a);//[Amy, b1, Carl, b2, Erica, b3, b4]

        //从b中删除每一个单词
        bIter = b.iterator();
        while (bIter.hasNext()){
            bIter.next();//跳过一个元素
            if (bIter.hasNext()){
                bIter.next();//跳过下一个元素
                bIter.remove();//移除那个元素
            }
        }
        System.out.println(b);//[b1, b3]

        a.removeAll(b);
        System.out.println(a);//[Amy, Carl, b2, Erica, b4]
    }
}

7.2  Collection集合

7.2.1 Collection集合继承结构图

7.2.2 常用方法

  1. 向集合添加元素:boolean add(Object e);
  2. 获取集合长度:int size() ;//不是获取集合的容量
  3. 清空集合:void clear();
  4. 判断当前集合中是否包含元素o:boolean contains(Object o);
  5. 删除集合中的某个元素:boolean remove(Object o);
  6. 判断该集合中元素个数是否为0:boolean isEmpty();
  7. 可以把集合转换成数组:Object[] toArray();

其中注意contains与remove底层调用equals方法比较

public class ContainsTest {
    public static void main(String[] args) {
        Collection c = new ArrayList();
        String s1 = new String("abc");
        String s2 = new String("def");
        c.add(s1);
        c.add(s2);
        String x = new String("abc");
        //底层调用equals方法
        System.out.println(c.contains(x));//true
        c.remove(x);
        System.out.println(c.size());//1
    }
}

该例内存图:

然而contains底层调用了equals方法,而String的equals方法重写过,并非比较内存地址了而是比较内容,因此结果为true,而如果没有重写equals方法的话比较的是内存地址,如果是这样的话就是false。因此放在集合中的方法应该重写equals方法。remove方法同理。

7.3  List接口

7.3.1 特有且常用的方法

  1. void add(int index, E element) 将指定的元素插入此列表中的指定位置(可选操作)。  
  2. E get(int index) 返回此列表中指定位置的元素。
  3. int indexOf(Object o) 返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。
  4. E remove(int index) 删除该列表中指定位置的元素(可选操作)。
  5. E set(int index, E element) 用指定的元素(可选操作)替换此列表中指定位置的元素。

7.3.2 链表数据结构

双向链表的结构图

链表的优点:由于链表的元素在控件存储上内存地址不连续,所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高。在以后的开发中,如果遇到随机增删集合中元素的业务比较多的时候,建议使用LinkList。

链表的缺点:不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头结点遍历,直到找到为止。所以LinkList集合检索/查找的效率较低。

7.3.3 为什么是List list = new ArrayList

List是一个接口,而ArrayList 是一个类。 ArrayList 继承并实现了List。

List list = new ArrayList();

这句创建了一个ArrayList的对象后把上溯到了List此时它是一个List对象了。有些ArrayList有,但是List没有的属性和方法,它就不能再用了。

ArrayList list = new ArrayList();

创建一对象则保留了ArrayList的所有属性。而一般使用List类型指向ArrayList引用,问题就在于List有多个实现类,如 LinkedList或者Vector等等。

现在你用的是ArrayList,也许哪一天你需要换成其它的实现类,这时你只要改变这一行就行了:

List list = new LinkedList(); 

其它使用了list地方的代码根本不需要改动。假设开始用 ArrayList 实现, 这下有的改了,特别是如果使用了 ArrayList特有的方法和属性。如果没有特别需求的话,最好使用LIst接口实现类,便于程序代码的重构. 这就是面向接口编程的好处。

7.4  Set接口

7.4.1 哈希表/散列集

散列表用于快速地查找对象。散列表为每个对象计算一个整数,称为散列码。散列码是由对象的实例字段得出的一个整数。不同的数据的对象将会产生不同的散列码。如果定义自己的类,就要负责实现自己的hashCode方法,注意实现应与equals方法兼容。

在Java中,散列表用链表数组实现。每个列表被称为桶(bucket)。要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的的结果就是保存这个元素的桶的索引。

例如,如果某个对象的散列码为76268,并且有128个桶,那么这个对象应该存在第108号桶中(因为76268%128余数是108)。或许很幸运,这个桶中没有其他元素,此时将元素直接插入到桶中就可以了。如果遇到桶已经被填充的情况。这种现象被称为散列冲突(hash collision)。这时,需要将新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。因此散列码合理地随机分布,桶的数目也足够大,需要比较的次数就会很少。

标准库使用的桶数是2的幂,默认值为16.如果大致知道最终会有多少个元素插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的75%~150%。

如果最初的估计过低。散列表太满,就需要再散列(rehashed)。如果对散列表再散列就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来表。装填因子(load factor)默认0.75,达到就会自动散列。

下面是HashMap的源代码:

        public class HashMap{
            // HashMap底层实际上就是一个数组。(一维数组)
            Node<K,V>[] table;
            // 静态的内部类HashMap.Node
            static class Node<K,V> {
                // 哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标。)
                final int hash;                 
                final K key; // 存储到Map集合中的那个key
                V value; // 存储到Map集合中的那个value
                Node<K,V> next; // 下一个节点的内存地址。
            } 
        }

7.4.2 HashSet

HashSet 按照哈希算法存取数据的,具有非常好性能它的工作原理是这样的,当向 HashSet 中插入数据的时候,他会调用对象的 hashCode 得到对象的哈希码,然后根据哈希码计算出该对象插入到集合中的位置。因此必须覆盖 equals 和 hashCode 方法。并且contains方法已经被重新定义,用来快速查找某个元素是否已经在集中(他只查看一个桶中的元素)。

7.4.3 重写equals和hashCode

再次强调:特别是向 HashSet 或 HashMap 中加入数据时必须同时覆盖 equals 和 hashCode 方法,应该养成一种习惯覆盖 equals 的同时最好同时覆盖 hashCode。

Java要求:两个对象 equals 相等,那么它的 hashcode 相等;两个对象 equals 不相等,那么它的 hashcode 并不要求它不相等,但一般建议不相等;hashcode 相等不代表两个对象相等(采用 equals 比较)。

7.4.4 自平衡二叉树

在Java8中,桶满时会从链表变成平衡二叉树。如果选择的散列函数不好,会产生很多冲突,或者如果有恶意代码试图在散列表中填充多个有相同散列码的值,这样改为平衡二叉树能提高性能。

7.4.5 TreeSet

TreeSet可以对Set集合进行排序,默认自然排序(即升序),但也可以做自定义排序,放到 TreeSet 中会对其进行排序,因此必须实现 Comparable 接口或者 Comparator 接口。

当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。

放到TreeSet集合中的元素,等同于放到TreeMap集合key部分。TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序了,称为可排序集合。

7.5  Comparable 与 Comparator接口

7.5.1 Comparable接口

1.简介

Comparable可以认为是一个内比较器,实现了Comparable接口的类有一个特点:强行对实现它的每个类的对象进行整体排序。此排序被称为该类的自然排序 ,类的 compareTo 方法被称为它的自然比较方法。实现此接口的对象列表(和数组)可以通过 Collections.sort (和 Arrays.sort )进行自动排序。实现此接口的对象可以用作有序映射表中的键或有序集合中的元素而无需指定比较器。

compareTo方法的返回值是int,有三种情况:

  • 比较者大于被比较者(也就是compareTo方法里面的对象),那么返回正整数。
  • 比较者等于被比较者,那么返回0。
  • 比较者小于被比较者,那么返回负整数。

2.例子

定义一个Student类,实现Comparable接口,并且重写compareTo方法。默认比较的是当前类的age属性,当age属性一样时比较姓名(如果不比较姓名,TreeSet会默认把age相等的对象当作是同一对象,从而产生误差)。

public class TreeSetTest01 {
    public static void main(String[] args) {
        Set set = new TreeSet();
        set.add(new Student(10,"①号同学"));
        set.add(new Student(20,"②号同学"));
        set.add(new Student(10,"③③③③"));
        for (Iterator it = set.iterator(); it.hasNext();){
            System.out.println(it.next());
        }
    }
}
class Student implements Comparable {
    int age;
    String name;
    public Student(int age, String name){
        this.age = age;
        this.name = name;
    }
    @Override
    public int compareTo(Object o) {
        if (o instanceof Student){
            Student s = (Student) o;
            if (this.age == ((Student) o).age){
                return this.name.compareTo(s.name);
            } else {
                return this.age - s.age;
            }
        }
        throw new IllegalArgumentException();
    }
}

7.5.2 Comparator接口

1.简介

Comparator 是比较器接口。我们若需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口);那么,我们可以建立一个“该类的比较器”来进行排序。这个“比较器”只需要实现Comparator接口即可。也就是说,我们可以通过“实现Comparator类来新建一个比较器”,然后通过该比较器对类进行排序。

int compare(T o1, T o2)和上面的x.compareTo(y)类似,定义排序规则后返回的正数、零和负数,分别代表大于,等于和小于。

2.例子

根据年龄比较

public class TreeSetTest02 {
    public static void main(String[] args) {
        TeacherComparator teacherComparator = new TeacherComparator();
        Set set = new TreeSet(teacherComparator);
        set.add(new Teacher(10,"①"));
        set.add(new Teacher(20,"②②"));
        set.add(new Teacher(10,"③③③③"));
        for (Iterator it = set.iterator(); it.hasNext();){
            System.out.println(it.next());
        }
    }
}
class Teacher{
    int age;
    String name;
    public Teacher(int age, String name) {
        this.age = age;
        this.name = name;
    }
}
class TeacherComparator implements Comparator{
    @Override
    public int compare(Object o1, Object o2) {
        Teacher t1 = (Teacher) o1;
        Teacher t2 = (Teacher) o2;
        if (t1.age == t2.age){
            return t1.name.compareTo(t2.name);
        } else {
            return t1.age - t2.age;
        }
    }
}

3.采用匿名类完成Comparator(按名字排序)

public class TreeSetTest03 {
    public static void main(String[] args) {
        Set set = new TreeSet(new TeacherComparator(){
            @Override
            public int compare(Object o1, Object o2) {
                Teacher t1 = (Teacher) o1;
                Teacher t2 = (Teacher) o2;
                if (t1.name == t2.name){
                    return t1.age - t2.age;
                }
                return t1.name.compareTo(t2.name);
            }
        });
        set.add(new Teacher(10,"①"));
        set.add(new Teacher(20,"②②"));
        set.add(new Teacher(10,"②②"));
        for (Iterator it = set.iterator(); it.hasNext();){
            System.out.println(it.next());
        }
    }
}

7.5.3 Comparable 与 Comparator 的区别

Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。

一个类实现了 Camparable 接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用 sort 方法排序。Comparator 可以看成一种算法的实现,将算法和数据分离,Comparator 也可以在下面两种环境下使用:

  • 类的没有考虑到比较问题而没有实现 Comparable,可以通过 Comparator 来实现排序而不必改变对象本身。
  • 可以使用多种排序标准,比如升序、降序等。

7.6  Map集合

Map 中可以放置键值对,也就是每一个元素都包含键对象和值对象,Map 实现较常用的为HashMap,HashMap 对键对象的存取和 HashSet 一样,仍然采用的是哈希算法,所以如果使用自定类作为 Map 的键对象,必须复写 equals 和 hashCode 方法。

7.6.1 Map集合继承结构

7.6.2 常用方法

  1. V put(K key, V value) 向Map集合中添加键值对。  
  2. V get(Object key) 通过key获取value。
  3. void clear() 清空Map集合。
  4. boolean containsKey(Object key) 判断Map中是否包含某个key 。
  5. boolean containsValue(Object value) 判断Map中是否包含某个value。
  6. boolean isEmpty() 判断Map集合中元素个数是否为0 。
  7. Set<K> keySet() 获取Map集合所有的key(所有的键是一个Set集合)。
  8. V remove(Object key)  通过key删除键值对。
  9. int size()  获取Map集合中键值对的个数。
  10. Collection<V> values() 获取Map集合中所有的value,返回一个Collection。  
  11. Set<Map.Entry<K,V>> entrySet() 将Map集合转换成Set集合。

假设现在有一个Map集合,如下所示: 

注意:Map集合通过entrySet()方法转换成的这个Set集合,Set集合中元素的类型是Map.Entry<K,V>(和String一样,都是一种类型的名字,只不过Map.Entry是静态内部类,是Map中的静态内部类)。

7.6.3 HashMap集合

建议使用HashMap类实现Map集合,因为由HashMap类实现的Map集合添加和删除映射关系的效率更高;而如果希望Map集合中的元素存在一定的顺序,应该使用TreeMap类实现Map集合。

1.HashMap覆盖equals和hashCode方法

public class HashMapTest02 {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put(new IdCard(223243244243243L), new Person("张三"));
        map.put(new IdCard(223243244244343L), new Person("李四"));
        map.put(new IdCard(223243244243243L), new Person("张三"));
        for (Iterator iter=map.entrySet().iterator(); iter.hasNext();) {
            Map.Entry entry = (Map.Entry)iter.next();
            IdCard idCard = (IdCard)entry.getKey();
            Person person = (Person)entry.getValue();
            System.out.println(idCard.cardNo + ", " + person.name);
        }
    }
}
class IdCard {
    long cardNo;
    public IdCard(long cardNo){this.cardNo = cardNo;}
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        IdCard idCard = (IdCard) o;
        return cardNo == idCard.cardNo;
    }
    public int hashCode() {
        return Objects.hash(cardNo);
    }
}
class Person {
    String name;
    public Person(String name){this.name = name;}
}

2.遍历方式

 第一种遍历方式,Set<K> keySet() 获取Map集合所有的key并将其转换成一个Set集合。通过获取Set集合中的每个key的值,去获取每个key对应的value值。

 Set<Integer> keys = map.keySet();
 Iterator<Integer> it = keys.iterator();
 while (it.hasNext()){
     Integer key = it.next();
     System.out.println(key + " = " + map.get(key));
 }
 for (Integer key : keys){
     System.out.println(key + " --> " + map.get(key));
 }

第二种遍历方式:Set<Map.Entry<K,V>> entrySet() 将Map集合转换成Set集合。这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值。这种方式比较适合于大数据量。

Set<Map.Entry<Integer, String>> set = map.entrySet();
//遍历Set集合,每一次取出一个Node
Iterator<Map.Entry<Integer, String>> it2 = set.iterator();
while (it2.hasNext()){
    Map.Entry<Integer, String> node = it2.next();
    Integer key = node.getKey();
    String value = node.getValue();
    System.out.println(key + " = " + value);
}

7.6.4 Properties 集合

目前只需要掌握Properties属性类对象的相关方法即可。Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。Properties被称为属性类对象。

  • Object setProperty(String key, String value)  存
  • String getProperty(String key)  取

7.6.5 TreeMap

treeMap 可以对 Map 中的 key 进行排序,如果 map 中的 key 采用的是自定类那么需要实现Comaprable 或 Comparator 接口完成排序。

7.7  Collections工具类

  • java.util.Collection 集合接口
  • java.util.Collections 集合工具类,方便集合的操作。

sort(List<T> list) 对指定的List列表进行排序(需要保证List集合中的元素实现了Comparable接口)。 该方法不能直接对Set类型集合进行排序,需将其转换为List集合:List list = new ArrayList(set);再Collections.sort(list);进行排序。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值