Java学习之容器深入研究

简介

我们以往经常使用数组来存储对象,数组虽然具有很高的效率,但是同样的,数组也具有一个致命的缺陷,那就是无法动态扩容。一个数组在其生命周期中,它的大小是不会发生变化的。同时,为了实现多种堆栈队列等高效的数据结构,我们需要借助于容器。

集合容器的用途是保存对象,但是被分为两个不同的概念:

  • Collection:一个独立元素的序列,这些元素都服从一条或多条规则
    • List:按插入顺序保存元素
    • Set:不能用重复元素
    • Queue:先进先出的排队规则
  • Map:一组成对的键值对

注意,这里并没有提到像Vector这样的容器,是因为这样的容器早已不再被使用,虽然我们的java库中还有,但这只是为了前后兼容才保留下来的。所以在本章我们不会对Vector等容器有一个详细介绍。


容器类库的结构

在这里插入图片描述
Java5中还增加看一些与线程安全相关的容器,但这个会在多线程并发里去进行分析,这里只分析常规的容器



Collections

Collections是一个集合框架的帮助类,里面包含一些对集合的排序,搜索以及序列化等操作。最根本的是Collections是一个工具辅助类,通过Collections我们可以很轻松的对集合进行快速操作


填充容器

下面是几种填充容器的方式:

  • Collections.nCopies(n,arg):产生一个只读类型的List类型容器,其中的元素为n个arg
  • Collections.fill(list,arg):对list中原有的全部元素进行覆盖,覆盖为arg,注意,元素数量不是容器数量
  • Collections.addAll(list,elements):将多个元素加入到list中
public static void main(String[] args) {
    
    

        List<String> strs = new ArrayList<>(Collections.nCopies(10,"Hello"));//产生一个包含10个"Hello"的List
        System.out.println(strs);
        Collections.fill(strs,"world");//将strs中的所有元素覆盖为"world"
        System.out.println(strs);
        Collections.addAll(strs,"XHY","XHY");//将两个"XHY"加入到strs列表中
        System.out.println(strs);
        /**
         * [Hello, Hello, Hello, Hello, Hello, Hello, Hello, Hello, Hello, Hello]
         * [world, world, world, world, world, world, world, world, world, world]
         * [world, world, world, world, world, world, world, world, world, world, XHY, XHY]
         */

    }


Abstract容器

在前面的类图中可以看到不管是Map、List还是Set,这些我们常用的容器都会继承一个对应结构的抽象类,它们提供了容器的部分实现,因此当我们需要定制容器时应该选择去实现这些抽象容器并提供必要的方法,而不是去实现顶层的接口。我们可以通过这样形式创创建一些只读容器,这样的操作还有许多。



Collection

Collection是Set和List接口的父接口。它是所有序列容器的顶层接口。


Collection的接口方法

下面表格列出了可以通过Collection执行的所有操作

方法 说明
boolean add(T) 添加元素
boolean addAll(Collection<? extends T>) 添加参数中的所有元素
clear() 清除元素中的所有元素
boolean contains(T) 是否包含目标对象
boolean containsAll(Collection<T>) 是否包含参数中的所有元素
boolean isEmpty() 是否为空
Iterator<T> iterator 返回容器的迭代器
Boolean remove(Object) 移除目标元素
boolean removeAll(Collection<?>) 移除参数中的所有元素
Boolean retainAll(Collection<?>) 保存两个容器的交集
int size() 获取元素数量
Object[] toArray() 将容器中的元素转换成Object数组
<T> T[] toArray(T[] a) 将容器中的元素转换成参数类型的数组
Comparator<T> reverseOrder(Comparator<T> cmp) 将目标比较器实现逆转然后返回新的比较器
int binarySearch(List<? extends Comparable<? super T>> list, T key) 使用二分搜索法搜索指定列表,以获得指定对象。需要保证list已经排序过
T max(Collection<? extends T> coll, Comparator<? super T> comp) 使用传入的比较器进行比较取出coll中的最大值
T min(Collection<? extends T> coll, Comparator<? super T> comp) 使用传入的比较器进行比较取出coll中的最小值
boolean replaceAll(List<T> list, T oldVal, T newVal) 对list中的元素实现替换
void reverse(List<?> list) 逆转元素次序
void rotate(List<?> list, int distance) 顺时针循环旋转了distance个元素
void shuffle(List<?> list) 随机打乱元素次序
sort(List<T> list, Comparator<? super T> c) 对list使用自定义比较器进行排序
void copy(List<? super T> dest, List<? extends T> src) 浅拷贝
void swap(List<?> list, int i, int j) 交换list中i和j的位置
boolean disjoint(Collection<?> c1, Collection<?> c2) 如果没有交集则为true
int frequency(Collection<?> c, Object o) 返回c中o出现的次数
Collection<T> unmodifiableCollection(Collection<? extends T> c) 返回一个c的只读版本
Collection<T> synchronizedCollection(Collection<T> c) 返回一个线程安全版本的c,一旦发生同步问题(一个线程在遍历,一个线程发生修改),就先有一个ConcurrentModificationException异常,我们可以通过这种方式实现乐观锁。
Collection<E> checkedCollection(Collection<E> c,Class<E> type) 生成一个type类型的类型检查容器,这种容器会在对象新增等时,对传入的对象进行类型检查(运行时)


List

List是Collection体系中我们最为常用的数据结构。

我们常用的List实现分为两种:

  • ArrayList:擅长随机访问元素,其底层是由数组实现,但是在中间插入和移除元素较慢,因为此一次非尾部元素的插入和删除都会造成后面元素的整位移。
  • LinkedList:擅长在中间位置以较低代价进行插入和删除,其实现底层是一个双向链表。因此在随机读取方面比较慢,因为链表对数据的读取需要逐个追述,无法像数组一样直接访问下标。

LinkdeList

LinkedList类使用双向链表来存储元素。它提供了一个链表数据结构。它继承了AbstractList类并实现List和Deque接口。


LinkedList的核心要点

  • LinkedList类可以存储重复的元素。
  • LinkedList类是有序的。
  • LinkedList类是非同步的(线程不安全的)。
  • LinkedList类中,由于不需要进行任何转换,因此处理速度很快。
  • LinkedList类可以用作List,Stack或Queue(因为LinkedList实现了这些结构所需的方法)。

LinkedList的层次结构

在这里插入图片描述


LinkedList使用

public class LinkedListLearn {
    
    
    public static void main(String[] args) {
    
    
        LinkedList<String> ls = new LinkedList<>();
        ls.add("a");
        ls.add("b");
        ls.add("c");
        ls.add("e");
        ls.add("f");

        System.out.println("返回第一个元素");
        System.out.println("element()"+ls.element()+"(为空异常)");
        System.out.println("getFirst()"+ls.getFirst()+"(为空异常)");
        System.out.println("peek()"+ls.peek()+"(为空null)");

        System.out.println("移除第一个元素");
        System.out.println("remove()"+ls.remove()+"(为空异常)");
        System.out.println("removeFirst()"+ls.removeFirst()+"(为空异常)");
        System.out.println("poll()"+ls.poll()+"(为空null)");

        System.out.println("插入元素");
        ls.addFirst("g");
        //尾部添加
        ls.add("h");
        //尾部添加
        ls.addLast("i");

        System.out.println("移除最后一个");
        ls.removeLast();

        System.out.println(ls);
    }
}
/*output
返回第一个元素
element()a(为空异常)
getFirst()a(为空异常)
peek()a(为空null)
移除第一个元素
remove()a(为空异常)
removeFirst()b(为空异常)
poll()c(为空null)
插入元素
移除最后一个
[g, e, f, h]
 */

ArrayList

ArrayList类使用动态数组来存储元素。它继承了AbstractList类并实现List接口。


ArrayList类的核心要点

  • ArrayList类可以存储重复的元素。
  • ArrayList类元素是有序的。
  • ArrayList类非同步(线程不安全的)。
  • ArrayList允许随机访问,因为数组基于索引工作。
  • 在ArrayList类中,操作很慢,因为如果从数组列表中删除任何元素,则需要进行很多移位。

ArrayList的层次结构

在这里插入图片描述


ArrayList和LinkedList的区别

ArrayList和LinkedList都实现List接口并维护插入顺序。两者都是非同步(线程不安全)类。

ArrayList LinkedList
ArrayList在内部使用动态数组存储元素。 LinkedList在内部使用双向链表来存储元素。
使用ArrayList增删操作很慢,因为它的内部使用数组。如果从ArrayList中删除了任何元素,则所有位都将在内存中移位。 使用LinkedList增删操作比ArrayList更快,因为它使用了双向链接列表,因此不需要在内存中进行任何移位。
ArrayList类只能用作列表,因为它仅实现List。 LinkedList类可以实现列表和队列,因为它实现了List和Deque接口。
ArrayList 更适合存储和访问数据。 LinkedList 更适合处理数据。
public static void main(String[] args) {
    
    
        ArrayList<Integer> list = new ArrayList<>();

        list.add(1);
        list.addAll(Collections.nCopies(10,1));
        list.get(0);
        list.remove(1);      
    }


Set和存储顺序

存入Set的每个元素都必须是唯一的,因为Set不保存重复元素,加入Set的元素必须定义equals()方法以确保对象的唯一性。Set和Collection具有完全相同的接口。同时Set不保证维护元素的次序


HashSet

为快速查找而设计的Set,存入HashSet的元素必须实现hashCode()方法
HashSet类用于创建使用哈希表进行存储的集合。它继承了AbstractSet类并实现Set接口。
后面所有关于Hash相关的具体描述都会留到一节中进行描述


Java HashSet类的核心要

  • HashSet通过使用一种称为哈希的机制来存储元素。
  • HashSet不能存放重复元素。
  • HashSet允许为空值。
  • HashSet类是非同步的(线程不安全)。
  • HashSet元素是无序的。因为元素是根据其哈希码插入的。
  • HashSet方便检索数据。
  • HashSet的初始默认容量为16,而负载因子为0.75。

List和Set的区别

List可以包含重复元素,而Set仅包含唯一元素。


HashSet的层次结构

在这里插入图片描述


例子

public static void main(String args[]){
    
    
        //创建HashSet,添加元素
        HashSet<String> set=new HashSet();
        set.add("One");
        set.add("Two");
        set.add("Three");
        set.add("Three");
        set.add("Four");
        set.add("Five");
        Iterator<String> i=set.iterator();
        while(i.hasNext())
        {
    
    
            System.out.println(i.next());
        }
		/**
         * Five
         * One
         * Four
         * Two
         * Three
         */
    }

TreeSet

前面提到的HashSet中元素是无需的,但是我们终究需要某种能够保持排序的Set结构。TreeSet底层为树结构,使用它可以从Set中提取有序的序列。元素必须实现Comparable接口。


TreeSet类的核心要点

  • TreeSet类不能存储重复元素,和HashSet相似。
  • TreeSet类的访问和检索时间快(当然没有Hash快)。
  • TreeSet类不允许使用Null元素。
  • TreeSet类是非同步(线程不安全的)。
  • TreeSet类元素是升序的。

TreeSet的层次结构

在这里插入图片描述


例子

public static void main(String[] args) {
    
    
        SortedSet<Integer> sortedSet = new TreeSet<>();
        Collections.addAll(sortedSet,31,3,6,1,11,0);

        System.out.println(sortedSet);
        System.out.println(sortedSet.first());
        System.out.println(sortedSet.last());
        System.out.println(sortedSet.subSet(sortedSet.first(),sortedSet.last()));
        System.out.println(sortedSet.headSet(11));
        System.out.println(sortedSet.tailSet(1));

        /**
         * [0, 1, 3, 6, 11, 31]
         * 0
         * 31
         * [0, 1, 3, 6, 11]
         * [0, 1, 3, 6]
         * [1, 3, 6, 11, 31]
         */
    }

LinkedHashSet

具有HashSet的查询速度,且内部使用链表来维护元素的顺序(插入次序)。在遍历时是按照插入顺序显式。同时元素必须实现hashCode()方法。也就是LinkedHashSet的数据结构使用了Hash作为插入和检索方式,但是与此同时还加入了链表来保证了插入的次序。注意,这种次序只会是插入次序,不是自定义的比较方式。

LinkedHashSet类实现了Set接口。并且HashSet的子类。


LinkedHashSet类的核心要点

  • LinkedHashSet类不能存储重复元素,和HashSet相似。
  • LinkedHashSet类提供所有Set接口的操作,并允许空元素。
  • LinkedHashSet类是非同步(线程不安全的)。
  • LinkedHashSet类是有序的

LinkedHashSet的层次结构

在这里插入图片描述


例子

我们可以注意到下面例子中的集合是有序的

public static void main(String args[]){
    
    
        //创建LinkedHashSet,添加元素
        LinkedHashSet<String> set=new LinkedHashSet();
        set.add("One");
        set.add("Two");
        set.add("Three");
        set.add("Four");
        set.add("Five");
        Iterator<String> i=set.iterator();
        while(i.hasNext())
        {
    
    
            System.out.println(i.next());
        }
    }
    /**
         * Five
         * One
         * Four
         * Two
         * Three
         */


队列Queue

Java Queue接口以FIFO(先进先出)的方式对元素进行排序。在FIFO中,首先删除第一个元素,最后删除最后一个元素。


PriorityQueue 优先级队列

PriorityQueue类在Java1.5中引入并作为 Java Collections Framework 的一部分。PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序或者通过提供的Comparator(比较器)在队列实例化的时排序。
优先队列不允许空值,而且不支持non-comparable(不可比较)的对象,比如用户自定义的类。优先队列要求使用Java Comparable和Comparator接口给对象排序,并且在排序时会按照优先级处理其中的元素。

优先队列的头是基于自然排序或者Comparator排序的最小元素。如果有多个对象拥有同样的排序,那么就可能随机地取其中任意一个。当我们获取队列时,返回队列的头对象。


例子演示

在下面例子中我们定义了一个Customer类,它包含id和name两个元素,但是我们希望放在队列里时是按照id进行优先级排序。所以我们要为我们的优先级队列定义一个比较器,这个比较器就是按照id实现优先级比较的,每加入一个元素,就会按照id进行重写排序。

public class CollectionDemo6 {
    
    
    public static void main(String[] args) {
    
    
        //优先队列使用示例
        Queue<Customer> customerPriorityQueue = new PriorityQueue<>(7, idComparator);
        addDataToQueue(customerPriorityQueue);
        pollDataFromQueue(customerPriorityQueue);
		/**
         * Processing Customer with ID=5
         * Processing Customer with ID=11
         * Processing Customer with ID=35
         * Processing Customer with ID=63
         * Processing Customer with ID=79
         * Processing Customer with ID=89
         * Processing Customer with ID=93
         */
    }

    //匿名Comparator实现

    public static Comparator<Customer> idComparator = new Comparator<Customer>(){
    
    
        @Override
        public int compare(Customer c1, Customer c2) {
    
    
            return (int) (c1.getId() - c2.getId());
        }
    };

    //用于往队列增加数据的通用方法
    private static void addDataToQueue(Queue<Customer> customerPriorityQueue) {
    
    
        Random rand = new Random();
        for(int i=0; i<7; i++){
    
    
            int id = rand.nextInt(100);
            customerPriorityQueue.add(new Customer(id, "Pankaj "+id));
        }
    }

    //用于从队列取数据的通用方法
    private static void pollDataFromQueue(Queue<Customer> customerPriorityQueue) {
    
    
        while(true){
    
    
            Customer cust = customerPriorityQueue.poll();
            if(cust == null) {
    
    
                break;
            }
            System.out.println("Processing Customer with ID="+cust.getId());
        }
    }
}

class Customer {
    
    
    private int id;
    private String name;

    public Customer(int i, String n){
    
    
        this.id=i;
        this.name=n;
    }

    public int getId() {
    
    
        return id;
    }

    public String getName() {
    
    
        return name;
    }
}
    }


Map

映射表的基本思想是它维护的是键值对关联,因此我们可以通过键来查找值。标准的Java库中包含了几种基本实现,包括HashMap、TreeMap、LinkedHashMap、WeakHashMap,ConcurrentHashMap、IdentityHashMap。它们都有同样的基本接口Map,但是却拥有不同的性能和特性。这表现在效率、键值对保存及呈现次序等方面。


性能

性能是映射表中一个最重要的问题之一,当使用线性搜索时,执行速度会相当慢,但这正式HashMap提高速度的地方。HashMap使用了一种称为散列码的特殊值,用来取代对键的缓慢搜索。
散列码是相对唯一的,用以代表对象的int值,它是通过将对象的某些信息进行转码而生成的。hashCode()是根类Obejct中方法,因此所有的对象都能产生散列码。HashMap就是使用对象的hashCode实现快速查询的


HashMap

基于散列表的实现(取代了HashTable)。插入与查询的开销是固定的。可通过构造器设置容量和负载因子,以调整容器的性能

  • HashMap类包含key-value对。
  • HashMap类不能存储重复数据(key不能重复)。
  • HashMap类可以包含一个或多个null值。
  • HashMap类是非同步的(线程不安全)。
  • HashMap类元素是无序的。
  • HashMap类的初始默认容量为16,负载因子为0.75。

LinkedHashMap

类似于HashMap,但是在迭代遍历它时,取得键值对的顺序是其插入顺序,或者是最近最少次序。只比HashMap慢一点,而在访问时反而会更快,因为它使用链表维护内部次序

public static void main(String[] args) {
    
    
        Map<String,String> map1 = new LinkedHashMap<>();//使用默认的插入次序
        Map<String,String> map2 = new LinkedHashMap(16, 0.75f,true);//使用最近最少使用顺序
    }

TreeMap

TreeMap类是基于红黑树的实现。它提供了一种有效的方法来按排序顺序存储键值对。

  • TreeMap存储key-value对。它实现了NavigableMap接口并继承了AbstractMap类。
  • TreeMap不能存储重复元素。
  • TreeMap不能包含一个null键,但是可以包含多个null值。
  • TreeMap是非同步(线程不安全的)。
  • TreeMap元素是有序的(升序)。


散列与散列码


什么是哈希(散列)(Hashing)

哈希是通过使用方法hashCode() 将对象转换为整数形式的过程。必须正确编写hashCode() 方法,以提高HashMap的性能。在这里,我使用自己的类的键,以便可以覆盖hashCode() 方法以显示不同的场景。看下面的代码:

class Key
{
    
    
    String key;
    Key(String key)
    {
    
    
        this.key = key;
    }

    @Override
    public int hashCode()
    {
    
    
        return (int)key.charAt(0);
    }

    @Override
    public boolean equals(Object obj)
    {
    
    
        return key.equals((String)obj);
    }
}

上面的Key类覆盖的hashCode() 方法返回第一个字符的ASCII值作为哈希码。因此,只要key的第一个字符相同,哈希码就会相同(当然以上程序只是演示,实际开发中不应该这样覆盖hashCode()方法)。由于HashMap还允许使用null键,因此null的哈希码将始终为0。


hashCode()方法

什么是hashcode

hashcode就是通过hash函数得来的,通俗的说,就是通过某一种算法得到的,hashcode就是在hash表中有对应的位置。

每个对象都有hashcode,对象的hashcode怎么得来的呢?

首先一个对象肯定有物理地址,网上有人把对象的hashcode说成是对象的地址,事实上这种看法是不全面的,确实有些JVM在实现时是直接返回对象的存储地址,但是大多时候并不是这样,只能说可能存储地址有一定关联,
那么对象如何得到hashcode呢?通过对象的内部地址(也就是物理地址)转换成一个整数,然后该整数通过hash函数的算法就得到了hashcode,所以,hashcode是什么呢?就是在hash表中对应的位置。这里如果还不是很清楚的话,举个例子,hash表中有 hashcode为1、hashcode为2、(…)3、4、5、6、7、8这样八个位置,有一个对象A,A的物理地址转换为一个整数17(这是假如),就通过直接取余算法,17%8=1,那么A的hashcode就为1,且A就在hash表中1的位置。

为什么要使用hashcode

HashCode的存在主要是为了查找的快捷性, HashCode是用来在散列存储结构中确定对象的存储地址的 ( 用hashcode来代表对象在hash表中的位置 ) , hashCode 存在的重要的原因之一就是在 HashMap(HashSet 其实就是HashMap) 中使用(其实Object 类的 hashCode 方法注释已经说明了 ),HashMap 之所以速度快,因为他使用的是散列表,根据 key 的 hashcode 值生成数组下标(通过内存地址直接查找,不需要判断, 但是需要多出很多内存,相当于以空间换时间)

比如:我们有一个能存放1000个数这样大的内存中,在其中要存放1000个不一样的数字,用最笨的方法,就是存一个数字,就遍历一遍,看有没有相同得数,当存了900个数字,开始存901个数字的时候,就需要跟900个数字进行对比,这样就很麻烦,很是消耗时间,用hashcode来记录对象的位置,来看一下。hash表中有1、2、3、4、5、6、7、8个位置,存第一个数,hashcode为1,该数就放在hash表中1的位置,存到100个数字,hash表中8个位置会有很多数字了,1中可能有20个数字,存101个数字时,他先查hashcode值对应的位置,假设为1,那么就有20个数字和他的hashcode相同,他只需要跟这20个数字相比较(equals),如果每一个相同,那么就放在1这个位置,这样比较的次数就少了很多,实际上hash表中有很多位置,这里只是举例只有8个,所以比较的次数会让你觉得也挺多的,实际上,如果hash表很大,那么比较的次数就很少很少了。通过对原始方法和使用hashcode方法进行对比,我们就知道了hashcode的作用,并且为什么要使用hashcode了。

equals()方法

equals方法用于判断2个对象是否相等。此方法由Object类提供。您可以在您的类中重写此方法以提供您自己的实现。
HashMap使用equals() 比较key是否相等。如果equals() 方法返回true,则它们相等,否则不相等。


由于散列表中的槽位通常被称为桶位,因此我们将实际散列表的数组命名为bucket。

桶容量 = 存储桶节点数量 * 负载因子


覆盖hashCode()

首先,我们无法控制桶下标值的产生。这依赖于HashMap对象的容量。因此,我们在覆盖hashCode时所作的应该是想办法让其如何尽可能均匀的散列在数组上,而不是大量的对象所计算出的点指向同一个桶,这样会给HashMap带来巨大的压力。

设计hashCode除了注意上面提到的尽可能均匀散列以外,还要求对同一个对象调用hashCode都应该生成同样的值。如果将对象存入Map时与取出Map时所使用的hashcode不一致,那么这种检索方式也就失去了意义,因此,我们尽量不要使用易变的且不具有标识性的数据作为哈希函数的因变量。当然,也不要为了唯一性和随机性而选择对象的地址作为因变量,因为这一来你会发现即使我们两个对象内的全部元素都一致,但也无法在存储时覆盖对方,要知道,正常来说一个保持唯一性的容器内出现了两个完全相同的对象是不合理的。同时使用地址作为自变量会造成我们无法通过我们手上的信息取获得目标对象(因为我们通常不会去对地址值进行操作)

还有一点,要想hashcode实用,它必须速度快,并且必须具有意义,也就是说,它必须基于对象的内容生成散列码。要记住散列码不必是独一无二的(应该更关注生成速度,而不是唯一性)。但是通过hashCode()和equals(),必须能够完全确定对象的身份。

对上面的内容进行提炼总结就是:

  1. 不要使用对象地址作为hashCode或是哈希函数自变量
  2. 哈希生成速度必须快
  3. 哈希函数产生的散列码需要尽可能分布均匀
  4. 要使用不易变的并具有标识意义的数据作为哈希函数的自变量

Effective Java中给出了一些指导

  1. 为哈希的设置一个非0的初始值
  2. 为对象内每个有意义的域计算出一个int散列码
    • boolean: c = (f?0:1)
    • byte\char\short\int: c = (int)f
    • long: c = (int)(f^f>>32)
    • float: c = Float.floatToBit(f)
    • double: long l = Double.doubleToLongBit(f);
      c = (int)(l^(l>>32))
    • Object: c = f.hashCode()
    • 数组: 对每个元素应用上述规则
  3. 合并计算得到散列码


持有引用

java.lang.ref类库包含了一组类,这些类为垃圾回收提供了更大的灵活性。当存在可能会还进内存的大对象时,这些类会显得特别有用。有三个继承自抽象类Reference的类:SoftReference、WeakReference和PhantomReference。当垃圾回收器正在考察的对象只能通过某个Reference对象才可获得时,上述这些不同的派生类为垃圾回收器提供了不同级别的间接性指示。通过这个三个类可以和gc做简单的交互。
通常,当我们使用的对象没有任何引用指向它时,垃圾回收器会在回收时机回收这个对象,但是我如果希望继续持有某个对象的引用,但在持有的过程中也允许垃圾回收器回收这个对象时,我们应该使用Reference。这样,我们可以继续使用该对象,而且在内存消耗殆尽时又会允许释放该对象。

以Reference对象作为我们和普通引用之间的媒介。注意,如果目标对象存在任意普通引用指向它,他就不能被释放。

SoftReference、WeakReference和PhantomReference对应不同级别的可获得性
在这里插入图片描述


SoftReference(软引用)

对应高速缓存,在jvm报告内存不足之前会清除所有的软引用,这样以来gc就有可能收集软可及的对象,可能解决内存吃紧问题,避免内存溢出。什么时候会被收集取决于gc的算法和gc运行时可用内存的大小。


WeakReference(弱引用)

只要发生了gc垃圾回收,弱引用也会被进行回收

public static void main(String[] args) {
    
    
        String abc=new String("abc");
        WeakReference<String> abcWeakRef = new WeakReference<String>(abc);
        abc=null;
        System.out.println("before gc: "+abcWeakRef.get());
        System.gc();
        System.out.println("after gc: "+abcWeakRef.get());
        /**
         * before gc: abc
         * after gc: null
         */
    }

gc收集弱可及对象的执行过程和软可及一样,只是gc不会根据内存情况来决定是不是收集该对象。

如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象,而不是用一般的 reference。


PhantomRefrence(虚引用)

该引用不会影响不会影响对象的生命周期,也无法从虚引用中获取对象实例,建立虚引用之后通过get方法返回结果始终为null。它唯一的目的就是为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。你声明虚引用的时候是要传入一个queue的。当你的虚引用所引用的对象已经执行完finalize函数的时候,就会把对象加到queue里面。你可以通过判断queue里面是不是有对象来判断你的对象是不是要被回收了



WeakHashMap

讲完了弱引用,我们就能理解另一种特殊的Map,即WeakHashMap,它被用来保存WeakReference

和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。
不过WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。

这个“弱键”的原理呢?大致上就是,通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。实现步骤是:
(01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。
实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
(02) 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
(03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。
这就是“弱键”如何被自动从WeakHashMap中删除的步骤了。

和HashMap一样,WeakHashMap是不同步的。可以使用 Collections.synchronizedMap 方法来构造同步的 WeakHashMap



BitSet

如果想要高效率的存储大量的开关信息,BitSet是一种很好的选择。但它的效率仅是对空间而言,如果需要高效的访问时间,BitSet慢于数组。

BitSet是位操作的对象,值只有0或1即false和true,内部维护了一个long数组,初始只有一个long,所以BitSet最小的size是64,当随着存储的元素越来越多,BitSet内部会动态扩充,最终内部是由N个long来存储,这些针对操作都是透明的。
用1位来表示一个数据是否出现过,0为没有出现过,1表示出现过。使用用的时候既可根据某一个是否为0表示,此数是否出现过。
一个1G的空间,有 8102410241024=8.5810^9bit,也就是可以表示85亿个不同的数。
注意:在没有外部同步的情况下,多个线程操作一个BitSet是不安全的。

例子

比如有一堆数字,需要存储,source=[3,5,6,9]
用int就需要4*4个字节。
java.util.BitSet可以存true/false。
如果用java.util.BitSet,则会少很多,其原理是:
1,先找出数据中最大值maxvalue=9
2,声明一个BitSet bs,它的size是maxvalue+1=10
3,遍历数据source,bs[source[i]]设置成true.
最后的值是:
(0为false;1为true)
bs [0,0,0,1,0,1,1,0,0,1]
3, 5,6, 9
这样一个本来要int型需要占4字节共32位的数字现在只用了1位!
比例32:1
这样就省下了很大空间
通常用在数据统计、分析的领域。

BitSet的具体部分可以参考https://www.cnblogs.com/twodoge/p/11358459.html。

猜你喜欢

转载自blog.csdn.net/qq_33905217/article/details/109646819