一文秒懂java容器大全

一、java容器类简介

java容器类用途是保存对象(不能存储基本类型,基本类型可以通过自动装箱和拆箱完成),包括List、Set、Queue和Map,将其划分为两个不同的概念(在java中都是通过接口来实现的):

Collection:一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,Set不能有重复元素(通过比较hashcode和equals方法)但是也没有顺序,Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。

1. collection的初始化

new初始化参数

addAll

Collections.addAll(两个参数)

Arrays.asList

  • 首选方式:构建一个空的Cllection,然后调用Collections.addAll(), Arrays.asList构造产生的list可以作为参数传递给上面三种初始化方法,这样可以生成允许使用所有方法的普通容器。
  • addAll()只能接收另一个Collection对象作为参数,没有Arrays.asList()和Collections.addAll()灵活,但是比new的时候传入Arrays.asList()参数运行速度要快很多。
  • Collections.addAll()方法接受的参数:一个Collection对象以及一个数组或者一个用逗号分隔的元素列表(从Collection对象了解到目标类型,不会产生类型错误)
  • Arrays.asList()方法接受的参数:一个数组,一个用逗号分隔的元素列表(可能是对象,但是仅能接收直接子类);

Arrays.asList()产生的List对象会使用底层数组作为其物理实现,是一个浅拷贝,任何会引起底层数据结构的尺寸进行修改的方法(add, remove, clear)都会产生一个UnsupportedOperationException.

说明:asList的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍然是数组。

String[] str = new String[] { "you", "wu" }; 
List list = Arrays.asList(str);  
第一种情况:list.add("yangguanbao"); 运行时异常。 
第二种情况:str[0] = "gujin"; 那么list.get(0)也会随之修改。               

2. 打印输出

数组需要使用Arrays.toString()来产生数组的可打印表示,容器无需任何帮助可直接打印(容器默认会提供toString()方法),打印输出

List/Set: [rat, cat, dog, dog]

Map: {dog=Spot, cat=Rags, rat=Fuzzy}

3. 使用泛型的好处

  • 将运行时错误变为编译期错误,防止在容器中插入错误的对象类型
  • 获取到的对象类型可以直接使用,无需强转换
  • 泛型不仅限于只能将确切类型的对象放置到容器中,向上转型也可以像作用于其他类型一样作用于泛型

4. 和数组对比

  • 数组具有固定尺寸,不支持泛型
  • 容器类可以自动调整尺寸,支持泛型

5. 迭代器

迭代器是一个轻量级对象,不关心java容器类的具体类型,具有以下限制(仅针对Collection对象):

1,使用方法iterator()要求容器类返回一个Iterator, Iterator将准备好返回第一个元素。

2,使用next()获取序列中的下一个元素

3,使用hasNext()检查序列中是否还有元素

4,使用remove()将迭代器新近返回的元素删除

ListIterator是一个更强大的Iterator的子类型,只能用于各种List类的访问,通过调用listIterator()方法产生一个指向List开始处的ListIterator,还可以通过listIterator(n)方法产生一个指向列表索引为n的ListIterator。

所有方法:next() previous() hasNext() hasPrevious() remove() nextIndex() previousIndex()

Foreach与迭代器

1,foreach语法主要用于数组,但是它也可以用于任何Collection对象(因为Iterable接口被foreach用来在序列中移动,因此任何实现了Iterable的类,都可以将它用于foreach语句中)

2,foreach语法用于数组和其他任何Iterable,但是并不意味着数组肯定也是一个Iterable,而且任何自动包装也不会自动发生(不存在任何从数组到Iterable的自动转换)

二、List: ArrayList, LinkedList

所有操作方法:

add(object) add(index, object) addAll(list) addAll(index, list) contains(object) containsAll(list): 和顺序无关 remove(object or index)  removeAll(list): 只移除特定元素,不会重复移除 get(index) indexOf(object) subList(fromIndex, toIndex) Collections.sort(list) Collections.shuffle(list, rand): Mix it up retainAll(list): 取交集 set(index, object): replace an element isEmpty() clear() toArray(new Pet[list.size()]): 将任意的Collection转换为一个数组,这是一个重载方法,无参数返回的是Object数组,若强转其它类型数组将出现 ClassCastException 错误;参数为目标类型的数据,将产生指定类型的数据(假设能通过参数类型检查),如果参数数组太小,存放不下List中的所有元素,toArray()方法将创建一个具有合适尺寸的数组;如果数组元素大于实际所需,下标为[ list.size() ]的数组 元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素 个数一致。

  • ArrayList
    • 优点:随机访问元素比较快
    • 缺点:在List的中间插入和移除元素比较慢

使用ArrayList相当简单:创建一个实例,用add()插入对象,然后用get()访问这些对象

如果ArrayList没有使用泛型,则默认保存Object类型的对象:

                 ArrayList<T> myArrayList = new ArrayList<T>();               
  • LinkedList
    • 优点:在List的中间插入和移除元素比较快,包含的操作比ArrayList多;
    • 缺点:随机访问比较慢

注: 两者都是线程不安全的

  • 线程安全

1,CopyOnWriteArrayList:

 public boolean add(E e) {
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    }
}            

2,Collections.synchronizedList:

    List<Integer> list2 = Collections.synchronizedList(new ArrayList<Integer>());

    public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
    SynchronizedList的listIterator操作没有加锁,其他操作都加了synchronized锁,锁对象就是其自身        

3,两者适用场景

CopyOnWriteArrayList,发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主,读操作远远大于写操作的场景中使用,比如缓存。而Collections.synchronizedList则可以用在CopyOnWriteArrayList不适用,但是有需要同步列表的地方, 读写操作都比较均匀的地方。

  • 栈:

方法:push pop peek

  • 队列:

除了并发应用,Queue在Java SE5中仅有的两个实现是LinkedList和PriorityQueue,它们的差异在于排序行为而不在于性能。

LinkedList实现了Queue接口,因此LinkedList可以看作是Queue的一种实现

方法:

offer:往队列添加元素如果队列已满直接返回false,队列未满则直接插入并返回true

add: 往队列添加元素如果队列已满抛异常IllegalStateException:,队列未满则直接插入并返回true,如果用在空间有限制的情况下用add方法

poll:获取并且移除队头元素,如果队列为空则返回null

remove: 获取并且移除队头元素,如果队列为空则抛异常:NoSuchElementException

peak:获取队头元素,如果队列为空则返回null

element:获取队头元素,如果队列为空则抛异常:NoSuchElementException

  private List<Runnable> drainQueue() {
        BlockingQueue<Runnable> q = workQueue;
        ArrayList<Runnable> taskList = new ArrayList<Runnable>();
        // 调用BlockingQueue的drainTo()方法转移元素
        q.drainTo(taskList);
        if (!q.isEmpty()) {
            // 一个一个地转移元素
            for (Runnable r : q.toArray(new Runnable[0])) {
                if (q.remove(r))
                    taskList.add(r);
            }
        }
        return taskList;
    }

PriorityQueue

完全二叉树有规律,所以它可以用一个数组表示而不需要使用链表。

List容器用于查找效率很低,因此Collections API提供了两个附加容器Set和Map,它们对插入,删除和查找等基本操作提供有效实现。

三、Set: HashSet, TreeSet, LinkedHashSet

Set:具有与Collection完全一样的接口,没有额外的功能,实际上Set就是Collection,只是行为不同。加入Set的元素必须定义equals()方法以确保对象的唯一性

HashSet: 最快获取元素的方式,必须定义hashCode(),存储在散列表,作用:常用来去重

TreeSet:按照比较结果的升序保存对象,将元素存储在红黑树数据结构

LinkedHashSet:按照被添加的顺序保存对象,因为查询速度的原因也使用了散列

SortedSet:

Object first()返回容器的第一个元素

Object last()返回容器的最后一个元素

SortedSet subSet(fromElement, toElement)生成此Set的子集,范围从fromElement(包含)到toElement(不包含)

SortedSet headSet(toElement)生成此Set的子集,由小于toElement(不包含)的元素组成

SortedSet tailSet(fromElement)生成此Set的子集,由大于或等于fromElement的元素组成

对于良好的编程风格,在覆盖equals()方法时,总是同时覆盖hashCode()方法。

默认情况下,排序假设TreeSet中的项实现Comparable接口,另一种排序可以通过用Comparator实例化TreeSet来确定。

ComParable和ComParator的区别:

  • Comparable和Comparator都是用于比较数据的大小的,实现Comparable接口需要重写compareTo方法,实现Comparator接口需要重写compare方法,这两个方法的返回值都是int,用int类型的值来确定比较结果
  • 在Collections工具类中有一个排序方法sort,此方法可以只传一个集合,另一个重载版本是传入集合和比较器,前者默认使用的就是Comparable中的compareTo方法,后者使用的便是我们传入的比较器Comparator,java的很多类已经实现了Comparable接口,比如说String,Integer等类。
  • Comparator接口中方法很多,但是我们只需要实现一个,也是最重要的一个compare,也许有的人会好奇为什么接口中的方法可以不用实现,因为这是JDK8以后的新特性,在接口中用default修饰的方法可以有方法体,在实现接口的时候可以不用重写,可以类比抽象类。

java中大部分我们常用的数据类型的类都实现了Comparable接口,而仅仅只有一个抽象类RuleBasedCollator实现了Comparator接口 ,还是我们不常用的类,这并不是要用Comparable而不要使用Comparator,在设计初时有需求就选择Comparable,若后期需要扩展或增加排序需求,再增加一个比较器Comparator,毕竟能写Collections.sort(arg1),没人乐意写Collections.sort(arg1,arg2)。

 Collections.sort(fileList, new Comparator<File>() {
        @Override
        public int compare(File f1, File f2) {
            if (f1.isDirectory() && f2.isFile()) {
                return -1;
            }
            if (f1.isFile() && f2.isDirectory()) {
                return 1;
            }
            return f1.getName().compareTo(f2.getName());
        }
    });           

四、Map: HashMap(默认使用), TreeMap, LinkedHashMap

HashMap:提供最快查找技术,基于散列表实现(取代了Hashtable),插入和查询“键值对”的开销是固定的,可以通过构造器设置容量和负载因子,以调整容器的性能。

默认负载因子:0.75f

初始capacity:16, 但是在开发过程中初始化以后的size为0

初始化HashMap时传入的尺寸大小:expectedSize / 0.75f + 1.0f (HashMap默认会调整为传入参数最近的2的整数次幂)

扩容:默认为前一个数组大小的2倍

HashMap对象的key、value值均可为null(最多只允许一条记录的key为null,可以允许多个值为null);HahTable对象的key、value值均不可为null。

重新调整HashMap大小存在什么问题吗

在多线程的情况下,可能产生条件竞争(race condition)。如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了

为什么String, Interger这样的wrapper类适合作为键

String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。(1)因为String是不可变的,也是final的,(2)而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

散列表操作中费时多的部分就是计算hashCode方法,String类中的hashCode方法包含一个重要的优化:(3)每个String对象内部都存储它的hashCode值,该值初始为0,但若hashCode被调用,那么这个值就被记住。因此,如果hashCode对同一个String对象被第二次计算,则可以避免昂贵的重新计算。这种技巧叫作闪存散列代码。

可以使用自定义的对象作为键吗

这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

可以使用ConcurrentHashMap来代替Hashtable吗

Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。

在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。不支持存入null的key和value,否则发生空指针异常

TreeMap:基于红黑树的实现。TreeMap是唯一带有subMap()方法的Map,它可以返回一个子树。

LinkedHashMap:按照插入顺序保存键,同时还保留了HashMap的查询速度。

WeakHashMap: 弱键(weak key)映射,允许释放映射所指向的对象,这是为解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则此键可以被垃圾回收器回收。

IdentifyHashMap:使用==代替equals()对“键”进行比较的散列映射,专门为解决特殊问题而设计的。

方法:

生成一个Collection: keySet() values() entrySet()

isEmpty, clear, size, containsKey, containsValue, get, put, putAll,

remove, map.keySet().removeAll(map.keySet()): 该操作会导致map变空,由于这些Collection背后是由Map支持的,所以对Collection的任何改动都会反映到与之相关联的Map(values/clear效果相同)

entrySet()使用了HashSet来保存键-值对。

方法put将键值对置入Map中,或者返回null,或者返回与key相联系的旧值

通过一个Map进行迭代要比Collection复杂,因为Map不提供迭代器,而是提供3种方法,将Map的对象的视图作为Collection对象返回。这些视图本身就是Collection,因此它们可以被迭代。

Set<KeyType> keySet()
Collection<ValueType> values()
Set<Map.Entry<KeyType, ValueType>> entrySet()

HashMap的初始化https://blog.youkuaiyun.com/dujianxiong/article/details/54849079

                 Map<String, String> map = new HashMap<>();               

注:

1,对于集合类型的静态成员变量,应该使用静态码块赋值,而不是使用集合实现

2,用ImmutableMap.of需要慎重,再做put操作会报UnsupportedOperationException

  • SparseArrayCompat:
    • 是SparseArray的兼容版本,可以在比较低的手机上运行
    • SparseArrayCompat()其实是一个map容器,它使用了一套算法优化了hashMap,可以节省至少50%的缓存.
    • 缺点但是有局限性只针对下面类型.

key: int; value: object

private static final SparseArrayCompat<ViewHolderFactory> FACTORIES = new SparseArrayCompat<>()
    static {
        FACTORIES.put(TYPE_FOOTER, new FooterViewHolder.Factory());
        // ...
    }
  • LongSparseArray

key: long; value: object

  • ArrayMap:

ArrayMap是Android专门针对内存优化而设计的,用于取代Java API中的HashMap数据结构,非线程安全。使用场景:

1.数据量不大,最好在千级以内;2.数据结构类型为Map类型

  • 容器空类型:

以Map为例,List和Set类似。

Map myMap = Collections.emptyMap();

优点:

1,new 一个空的 Map ,而这个 Map 以后也不会再添加元素,否则throw new UnsupportedOperationException()。

2,Collections.emptyMap相比于new HashMap<>()不用分配内存空间,而且多次调用返回同一个static对象。

3,比如说一个方法返回类型是Map,当没有任何结果的时候,返回null,有结果的时候,返回Map集合列表。那样的话,调用这个方法的地方,就需要进行null判断。使用emptyMap这样的方法,可以方便方法调用者。返回的就不会是null,省去重复代码。

4,如果直接使用myMap,会报错:throw new UnsupportedOperationException(),所以使用之前需要初始化,分配内存空间:myMap = new HashMap<>(); 或者指向另一个HashMap.

五、散列与散列码

Object的hashCode()方法默认使用对象的地址计算散列码。如果要使用自己的类作为HashMap的键,必须同时重载hashCode()和equals()。散列码不必是独一无二的,但是通过hashCode()和equals()必须能够完全确定对象的身份。

散列函数具有以下两种性质:

1,散列函数必须可在常数时间(即与表中项的个数无关)内计算。

2,散列函数必须将各项均匀分布在数组单元中。

提升查询速度:

1,保持键的排序状态,然后使用Collections.binarySearch()进行查询,时间复杂度为O(log(n))。

2,散列更进一步,散列是一种以常数平均时间执行插入、删除和查找的技术,但是不支持元素间任何排序信息的树操作。对于HashMap,将键用数组保存(存储一组元素最快的数据结构是数组),数组并不保存键本身,而是通过键对象生成一个数字,将其作为数组的下标,这个数字就是散列码。数组并不直接保存值,而是保存值的list。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值