java集合框架

集合的特点:
  • 存储的数据量可变
  • 存储的数据类型可变(通过泛型可以添加不同类型的数据)
  • 只能存储引用类型
    上述三点都是对应于数组的特点。
集合的分类:

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

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

基本方法:

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

  • addAll(Collection collection), 实质是将地址值赋值给另一个列表。
  • retainAll(Collection collection),保留与给定集合中共有的元素,如果集合被改变,则返回true。
  • toArray() 方法,返回一个包含该集合中所有元素的数组,是新建的数组,原有集合不会对该数组产生引用,所以对集合的操作不会影响该数组。这也是遍历集合的一种解决方案
关键接口:
  • Iterator
    Iterator it = list.iterator();
    遍历方法除了常见的while(it.hasNext()),还可以是for循环
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循环中遍历并删除元素,会导致指针偏移。

  • List
    有序的collection(存储和取出顺序一致),用户可以控制插入的位置,根据元素索引访问元素,允许重复的元素。
  • ListIterator list特有的迭代器,继承了Iterator迭代器,特有逆向遍历功能和其他新特性,但是使用前必须先经过过正向遍历,所以实际用处不大。
  • list集合特有用for(int i = 0;i < list.size(); x++)循环功能

根据之前叙说的迭代器遍历数组时修改数组异常——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的数据结构

  • ArrayList 底层实现是数组,查询快,增删慢,线程不安全,效率高
  • Vector 底层实现是数组,查询快,增删慢,线程安全,效率低
  • LinkedList 底层实现是链表,查询慢,增删快,线程不安全,效率高

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

不建议使用Vector,理由如下

  • Vector 对每个方法都加上了锁,损耗性能
  • 遍历列表时,为保证同步往往通过将不同的方法整合起来,再加锁。Vector同步单个操作的特点并不能保证多线程操作列表时线程安全,此时其锁就成为了负担
  • vector容量达到上限时其容量增量是100%,ArrayList是50%,它更耗内存

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();
        }
    }
  • Set
    不包含重复元素,最多包含一个null,取出和放入的顺序不一致
    常见实现类
    HashSet,由一个hash table支持(实际上为hashMap实例),不保证迭代的顺序,特别是不保证每次迭代的顺序都相同。
    hashSet添加元素流程
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的区别

  • 线程不安全,效率高vs线程安全,效率低;HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!)
  • 允许使用最多一个null键和多个null值vs不允许使用null作为键值
  • 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
  • JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制
    总之,不要使用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)洗牌,随机置换
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 原文地址 Java集合 Java集合框架:是一种工具类,就像是一个容器可以存储任意数量的具有共同属性的对象。 Ja...
    gyl_coder阅读 973评论 0 8
  • title: java集合框架学习总结 tags:集合框架 categories:总结 date: 2017-03...
    行径行阅读 1,674评论 0 2
  • 四、集合框架 1:String类:字符串(重点) (1)多个字符组成的一个序列,叫字符串。生活中很多数据的描述都采...
    佘大将军阅读 732评论 0 2
  • 友情提示:文章可能有点长,各种集合的实现原理我用自己的理解去解释了,配合源码食用更佳。 一、Java集合框架简述 ...
    Marker_Sky阅读 1,331评论 1 2
  • 朋友的重要性不容分说,作为群居生活的人们,一切离不开人际关系。想要建立财富首先要在人员密集的地方找到自己的归属地,...
    多肉安安阅读 401评论 0 0