java集合框架

2019-07-28  本文已影响0人  LeoFranz
集合的特点:
集合的分类:

基本接口collection和map
collection实现了Iterable接口,包含list、set、queue
list:可以包含重复元素,取出和放入的顺序一致
set:不包含重复元素,最多包含一个null,取出和放入的顺序不一致
queue:
其中collection继承了Iterable接口,其iterator()方法返回一个迭代器,而该方法乃至迭代器的具体实现在collection的子类中。

迭代器是接口的原因,每种集合的数据结构不一样,其遍历的实现方式不一样,所以抽象为接口,真正的实现是以内部类方式实现。可参见ArrayList中的iterator()方法。这里作为java多态和继承的很好的体现实例

基本方法:

自行查询api,不过有以下注意的点

关键接口:
for(Iterator it = list.iterator();it.hasNext();) {
        Student s = (Student) it.next();
        //Do something about s;
}

迭代器遍历集合时候,除非使用迭代器的remove方法,不能同时对集合进行增删操作,因为迭代器是基于iterator()方法被创建的,后续对集合的改变不能被同步上。即当多个线程,或多个任务对Collection进行操作时,若其中某一个任务通过Iterator遍历集合时,同时该集合的内容被其他迭代器或者集合自身的方法修改类,会抛出ConcurrentModificationException异常。
此外迭代器中next方法和remove方法是紧紧关联的,remove是删除上次调用next时返回的元素,所以调用remove前没有调用next是非法的。

为什么要使用迭代器?
不同集合的数据结构不一样,比如set,就不能使用for循环。使用迭代器作为接口,在不同集合中根据集合数据结构实现遍历的功能。此外,Iterator访问方式把对不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。再者,Iterator能够让访问者同时遍历并操作列表元素,如使用其remove方法,如果在fori循环中遍历并删除元素,会导致指针偏移。

根据之前叙说的迭代器遍历数组时修改数组异常——ConcurrentModificationException,一种思路,两种方案,思路就是将修改和遍历合并到一个线程,方案分别是仅仅用迭代器或者仅仅用集合,实践中推荐ListIterator,但修改位置受限(见api),而使用for循环遍历,会有隐藏的风险:

for(int i = 0;i<arrayList2.size(); i++) {
            Student student = (Student)arrayList2.get(i);
            System.out.println(student.getOfficialName());
            System.out.println(arrayList2.size());
            if(student.getOfficialName().equals("hugo")) {
                arrayList2.remove(1);
            }
        }//此时原来数组中下标为2的元素将不能被访问到

List的数据结构

如果有很多元素的增加删除,需要用LinkedList,因为用Arraylist会有大量元素的移动,且会遇到扩容的问题

不建议使用Vector,理由如下

LinkedList有添加元素的特有方法,add/get~last/first,removeFirst/Last,Vector也有特有addElement(E obj)、elementAt(int index)、elements()功能,被iterator替代

代码示例: 从列表中抽出重复项

ArrayList oldList = new TestClass("ss").mArrayList;
        oldList.add("aaaa");
        oldList.add("bbbb");
        oldList.add("aaaa");
        oldList.add("gggg");
        oldList.add("cccc");
        oldList.add("cccc");
//solution 1
        ArrayList<String> newList = new ArrayList<>();
        newList.add((String) oldList.get(0));
        for (int i = 0; i < oldList.size(); i++) {
            boolean shouldAdd = true;
            for (int j = 0; j < newList.size(); j++) {
                if(oldList.get(i).equals(newList.get(j))) {
                    shouldAdd = false;
                }
            }
            if(shouldAdd) {
                newList.add((String) oldList.get(i));
            }
        }
// solution 2
Iterator iterator = oldList.iterator();
        while(iterator.hasNext()) {
            String str = (String) iterator.next();
            if(!newList.contains(str)) {
                newList.add(str);
            }
        }
// solution 3
 for (int i = 0; i < oldList.size() - 1; i++) {
            for (int j = i+1; j < oldList.size(); j++) {
                if(oldList.get(i).equals(oldList.get(j))){
                    oldList.remove(j);
                    y--;
                }
            }
        }

去除重复元素的方式
//如何利用Comparator工具去除重复元素??

用LinkedList创建栈结构的集合

private class MyStack extends LinkedList<String> {
        
        private LinkedList<String> mList;
        
        public MyStack() {
            mList = new LinkedList<>();
        }
        
        public String get() {
            return mList.removeFirst();
        }
        
        public void addFirst(String string) {
            mList.addFirst(string);
        }
        
        public boolean isEmpty() {
            return mList.isEmpty();
        }
    }
public boolean add(E var1) {
        return this.map.put(var1, PRESENT) == null;
//PRESENT是全国人民final的同一代表
    }

当你把对象加入HashSet时,HashSet会先计算对象的hashcode值(其实是通过hashCode经过位运算获得的hash值)来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现,直接添加元素。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。不同会添加,这属于处理哈希冲突的范畴;相同,就不能添加。

自定义的hashCode方法必须与equals兼容,如果equals方法返回true,两个对象的hashCode值必须相同。
先看下面这个例子

我们知道不同的对象地址值不同,哪怕其成员完全一致,所以它们的hashCode当然不一样,这是上述所有对象都能被添加的原因。但是如果开发需求类型和成员一致的对象应被视为相同,即equals方法需要被重写,这时候hashCode方法也应该适配。

LinkedhashSet
链表和hash表组成,用于保证数据的有序性和唯一性,底层实现是LinkedHashMap

TreeSet
与散列集类似,但有所改进,能够对元素进行排序,
两种排序方式:
自然排序,子类必须实现Comparable接口,即重写compareTo方法
public class Stuff implements Comparable
自定义的实现了Comparator接口的类,关键在于重写其compare方法
TreeSet<Stuff> treeSet = new TreeSet<Stuff>(new StuffComparator<Stuff>());
具体采用哪种方式取决于构造器
有的类官方设定为Comparable接口实现类,可以进行自然排序,如Integer和String
由此可知,不同于HashMap唯一性主要取决于hashCode和equals方法,TreeMap&TreeSet的排序结果主要取决于子元素的对比方式
TreeSet的实现依赖的是TreeMap,TreeMap是基于红黑树实现的(一种自平衡的二叉树)。

TreeSet的add方法,实现依靠TreeMap的put方法,过程基本是找到根节点,然后二选一排序方式,循环比较最后找到合适插入位置。
获取元素,依靠二叉树里前序遍历方式遍历,
这边截取put方法中的一段

Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);

依赖TreeSet的添加元素机制能够将玩出的骚操作
将子元素的compareTo方法固定返回一个int值,比如-1,就能让元素一直添加到父元素的left,实现一个单项链表结构。

由此可见,TreeSet的本质是一个TreeMap,
有序性依赖:二叉树结构
唯一性依赖:子元素实现的对比方式

实践中偏向哪种方式——个人倾向重写Comparator接口,因为比较逻辑和bean对象分离,且能使用new Comparator的匿名内部类方式让快捷变换比较逻辑。

Queue
实现在尾部添加,在头部删除一个元素;对于双端队列,可以有效

地同时添加或者删除元素,不支持在队列中间增删元素

Map集合

以键值对出现的数据结构,Map集合键是唯一的,其数据结构与键有关,跟值无关。
特有的方法有

containsKey();
containsValue(); 
get(Object key); return the value
Collection<V> values() 
keySet() 
put(K key, V value) 除了添加还可以替换同键的值;
entrySet() ; Returns a Set view of the mappings contained in this map.

运用举例

Map<String, String> map = new HashMap();
        map.put("Jay", "Kun");
        map.put("Jolin", "???");
        map.put("kotlin", "java");
//获取所有键
        Set<String> set = map.keySet();
        for (String str :
                set) {
            System.out.println(str+"");
        }
//获取所有值
Collection<String> collection = map.values();
        for (String str :
                collection) {
            System.out.println(str);
        }
//this set is java.util.HashMap$KeySet
//this collection is java.util.HashMap$Values

//遍历集合
//solution 1
Set<String> set = map.keySet();
        for (String str :
                set) {
            System.out.println(map.get(str));
        }

//solution 2
Set<Map.Entry<String, String>> entries = map.entrySet();
        for (Map.Entry<String, String> entry :
                entries) {
            System.out.println(entry);
        }

关键实现类
HashMap
hash存储优势在于:对于没有实现RandomAccess接口与的集合,当忘记了数据的位置时,想快速查找一个数据很低效。如果不在意元素的顺序,散列表就可以实现快速查找功能。
hashMap底层实现是hash表,实质是元素为链表的数组,我们习惯把数组元素称为桶(比较形象)
hashMap以键值对的方式存储数据,关键是key要保持唯一,而value虽然功能上是我们真正想要的数据,但在该数据结构的存取中根本没有考虑value的存在,可以当做key的一个附属品来使用。

hashMap添加元素的过程
当你把对象加入hashMap时,hashMap会先计算对象的hashcode值(其实是通过hashCode经过位运算获得的hash值)来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现,直接添加元素。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。不同会添加,这属于处理哈希冲突的范畴;相同,就不能添加。

通过为不同对象生成一个整数,成为hash code(该值能快速生成),不同对象拥有不同的散列码。
java中,散列表用链表数组实现,每个链表被称为桶,要想查找对象的位置,首先计算其散列码,然后与桶的总数取余,结果就是元素所在桶的索引。
实际开发中的一个问题:
hash 冲突:有时候,桶会出现被占满的情况,即不同的对象产生了相同的hash值。解决这个问题通常是两种方法:链表法和开放地址法。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。java.util.HashMap采用的链表法的方式,链表是单向链表,相同hash值得不同对象存储在同一个槽位的链表中。

hashSet和HashMap在使用的时候,需要保证添加进去的元素是不可变的,api只能保证元素添加进去的时候是不可变的,但无法控制元素后面是否会变

hashMap的get(Object key)方法参数不是泛型,因为只需要用到equals和hashcode方法。返回值类型是泛型类型

实际开发中的另一个问题:

HashSet<Stuff> stuffs = new HashSet<>();
        Stuff stuff1 = new Stuff(23,"Leonardo");
        Stuff stuff2 = new Stuff(23,"Leonardo");
        Stuff stuff3 = new Stuff(24,"Hugo");
        stuffs.add(stuff1);
        stuffs.add(stuff2);
        stuffs.add(stuff3);
        for (Stuff employee:stuffs) {
            System.out.println(employee.toString());
        }
/**
     * console:
     * Stuff{ID=23, mName='Leonardo'}
     Stuff{ID=24, mName='Hugo'}
     Stuff{ID=23, mName='Leonardo'}
     */

我们知道不同的对象地址值不同,哪怕其成员完全一致,所以它们的hashCode理论上是不一样的,这是上述所有对象都能被添加的原因。但是如果开发需求类型和成员一致的对象应被视为相同,即equals方法需要被重写,这时候hashCode方法也应该适配,否则会出现equals返回true的两个对象hashCode不同。所以我们要同时自定义hashCode和equals方法——

自定义的hashCode方法必须与equals兼容,如果equals方法返回true,两个对象的hashCode值必须相同。
既然如此,hashCode的生成就不在依赖于第一无二的对象,而是考虑到了成员变量,其变化范围就会缩小,缩小就增加了hash冲突的可能性,导致性能下降,所以又要尽量random化hashCode,看下面的一种方案。
** 核心需求:适配equals同时尽量不会出现hash冲突 **

@Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof Stuff))
            return false;

        Stuff stuff = (Stuff) o;

        if (ID != stuff.ID)
            return false;
        return mName.equals(stuff.mName);
    }

    @Override
    public int hashCode() {
        int result = ID;
        result = 31 * result + mName.hashCode();
        return result;
    }

如果散列码分布均匀,桶的数目也足够大,需要比较的次数就少,hashMap的效率就很高;所以想保证性能,就要指定初始桶的数目,太少会增加散列冲突的可能性,一般建议设置为元素个数的75%~150%,如果散列表太满,就需要再散列。

58e67eae921e4b431782c07444af824e_r.jpg

HashMap和HashTable的区别,前者线程不安全,后者方法用synchronized修饰;前者能够出现一个唯一的null键,且可以有多个键对应null值,但后者中键值不能有null;初始容量大小和每次扩充容量大小的不同.
jdk 1.8实现方式稍有不同
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间

LinkedHashMap
由一个双向链表和hash表组成,具备HashMap键唯一的特点,同时保证存取顺序一致

TreeMap
依赖一个红黑树实现数据唯一和排序

HashMap和Hashtable的区别

其他补充

1.遍历集合
使用Lambda表达式遍历集合

public static void main(String[] args) {
        List<String> names = new ArrayList<String>();
        names.add("aa");
        names.add("bb");
        names.add("cc");
        names.forEach(name -> System.out.println(name));
    }

lambda表达式的目标类型是Consumer,并将集合的元素类型作为生成Consumer对象的泛型参数,然后自动将集合元素作为方法参数传递给Consumer对象的accept方法。foreach方法中会调用一个for-each循环。上面这一句代码可以看做是下面代码的极简形式:

Consumer<String> printConsumer= new Consumer<String>() {
            public void accept(String name) {
                System.out.println(name);
            }
        };
        names.forEach(printConsumer);

其等效于手写一个foreach循环,并没有在线程安全性上做改进。
更多内容,可以参考网页https://www.baeldung.com/foreach-java

2、Collections
对集合进行操作的工具类,有对集合进行二分查找和排序的方法。

public static <T> void sort(List<T> list)默认按照自然顺序排序
public static <T> int binarySearch(List<?> list,T key)二分查找
public static <T> T max(Collection<?> coll)最大值
public static void reverse(List<?> list)翻转
public static void shuffle(List<?> list)洗牌,随机置换
上一篇 下一篇

猜你喜欢

热点阅读