集合

集合

概述


集合和数组都是对多个数据进行存储操作,简称Java容器
存储主要是指内存层面的存储,不涉及持久化的存储(txt,jpg)

  1. 为什么要集合
    数组是保存一组同类型对象最有效的方式但是有缺点就是固定大小,但是出现数据类型对象不一致,我们无法确定数据体量的时候那么集合就出现了
    这句话两点:
    (1) 数组初始化后长度确定
    (2)初始化时候就确定了类型(其实这是数组的一个好处)
    (3)除了查找操作,不方便而且效率不高
  2. 划分为两个类(思想都是提供容器)
  • collection接口:单列数据,定义了存取一组对象的方法的集合该接口定义的方法既可用于set集合,也可用于操作List和Queue
    (1) list (列表)有序,插入顺序即顺序,可以含null,可重复的数据,动态数组
    (2)set(集)无序,可以含一个null,不可重复的数据,所有的重复内容靠hashCode和equals。两个方法区分
    (3)queue 按照排队顺序确定对象顺序
  • map键值对类,双列数据
    问题: Collection 和 Collections的区别是什么?
    答:Collection是集合类的上级接口,继承于他的接口主要有Set 和List.
    Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作

集合使用场景:
在这里插入图片描述
集合框架:
在这里插入图片描述
还有一个和它同等级的map
在这里插入图片描述

collection接口通用方法


在这里插入图片描述
这里没截完除开iterator和toArray()总共13个方法一个一个试
试看:
1、添加
add(Object obj)
addAll(Collection coll)
2、获取有效元素的个数
int size()
3、清空集合
void clear()
4、是否是空集合
boolean isEmpty()
5、是否包含某个元素
boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。
6、删除
boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
boolean removeAll(Collection coll):取当前集合的差集
7、取两个集合的交集
boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
8、集合是否相等
boolean equals(Object obj)
9、转成对象数组
Object[] toArray()
10、获取集合对象的哈希值
hashCode()
11、遍历
iterator():返回迭代器对象,用于集合遍历

Collection coll = new ArrayList();
//add填进去的是object
coll.add("a");
coll.add(123);
coll.add(new Date());
//size()添加元素的个数
coll.size();
//addAll()
coll.addAll(coll1);
//isEmpty()当前集合是否为空
coll.isEmpty();
//clear()清空集合元素
coll.clear();
//contains()
boolean coll = coll.contains("a");



集合主要类型:

集合类型描述
ArrayList可以动态增长和缩减的序列,查找效率高,添加效率低
LinkedList可以在任意位置进行高效的插入和删除类似链表优点
HashSet没有重复元素并且无序,只能存一个null
TreeSet有序集
EunmSet包含枚举类型的值
LinkedHashSet可以记住插入顺序的集
PriorityQueue允许高效删除最小元素的集合
ArrayDeque用循环数组实现双端队列
HashMap存储键值映射表
TreeMap根据键有序的排列映射表
EunmMap键值属于枚举类型的映射表
LinkedHashMap可以记住键值对添加顺序的映射表
WeekHashMap一种其值不再被使用时可以被垃圾回收的映射表
IdentityHashMap一种使用==而不是用equals比较键值是否相等的映射表

list


List 是⼀个有序序列,且可重复,有3种类型的 List:

  1. ArrayList
    (1)内部使⽤数组实现,随机访问元素快,但是在中间位置插⼊和删除元素⽐较慢。
    (2)JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组
    (3)Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合
  2. LinkedList

java.util.List 接⼝中主要的⽅法:

  • ListIterator listIterator() 返回列表迭代器,以便⽤来访问列表中的元素ListIterator listIterator(int index) 返回列表迭代器,以便⽤来访问列表中的元素; 第⼀次调⽤该迭代器 next ⽅法返回元素的索引是参数 index。
  • void add(int index, E element) 在指定位置添加⼀个元素
  • boolean addAll(int index, Collection<? extends E> c) 将某个集合中的所有元素添加到指定位置
  • E remove(int index) 删除指定位置的元素并返回这个元素
  • boolean remove(Object o) 删除和指定元素相等的元素(equals⽐较返回true),存在且删除成功
    返回 true
  • E get(int index) 获取指定位置的元素
  • E set(int index, E element) ⽤新元素取代指定位置的元素,并返回原来那个元素
  • int indexOf(Object o) 返回与指定元素相等的元素在列表中第⼀次出现的位置,没如果没有则返
    回 -1
  • int lastIndexOf(Object o) 返回与指定元素相等的元素在列表中最后⼀次出现的位置,没如果没有则返回 -1
  1. vector
    是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的,Vector总是比ArrayList慢,所以尽量避免使用,Vector还有一个子类Stack。
    问题:请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?
    (1)ArrayList和LinkedList的异同
    二者都线程不安全,相对线程安全的Vector,执行效率高。
    此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据
    (2)ArrayList和Vector的区别
    Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。

List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");

        System.out.println(list.toString());

        list.remove(1);

注意这里remove()里面是可以传两只 index 和 object

在这里插入图片描述
可以看到remove的遍历范围,找到一个满足就返回这个区别于removeall

关于list的扩展内容:

首先需要知道的知识点:

  • 我们知道short+short类型的值会变成int
  • 然后基本类型的封装类除int是Integer和char是Character之外其他的都是首字母大写
short s1 = 1;
short s2 = 2;
Set<Short> set = new HashSet<>();
set.add.(s1);
set.add.(s2);
set.remove( s1+1);
System.out.println(set);//[1,2]
set.remove((short)s1+1);
System.out.println(set);//[1]


List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");

        System.out.println(list.toString());

        list.remove(1);

        Iterator<String> it = list.iterator();
        while (it.hasNext()){
            System.out.println(it.next());
        }

迭代器

我们打开iterator之后可以看到以下几个接口:

  • boolean hasNext(); //存在就返回true
    列:
  Iterator<String> it = list.iterator();
        while (it.hasNext()){
            System.out.println(it.next());
        }
        System.out.println(it.hasNext());//false
  • E next(); //访问下一个对象如果跑到尾部弹出NoSuchElementException

    注意事项:
    唯一安全的方式来在迭代过程中修改集合;如果在迭代过程中以任何其它的方式修改了基本集合将会产生未知的行为。而且每调用一次next()方法,remove()方法只能被调用一次

  • default void remove() {
    throw new UnsupportedOperationException(“remove”);
    }

  • 这个iterator是通过Iterator接口来实现

  • 迭代器是存在于列表对象之间,具体的位置关系:
    在这里插入图片描述
    我们需要注意的地方:

  1. 根据上图我们可以了解迭代器初始化位置是1,如果直接调用remove,抛出IegalStateException
  2. 由此可见我们可以知道next()取的是迭代器之前的元素,假如到2之后连续调用两次remove,也会抛出IegalStateException

跌迭代器对序列在操作中途不能够对列表结构进行操作,会抛出Exception in thread “main” java.util.ConcurrentModificationException

 Collection<Node> c = new ArrayList<>();
        c.add( new Node(1, "a"));
        c.add( new Node(2, "b"));
        c.add( new Node(3, "c"));
        c.add( new Node(4, "d"));

        Iterator<Node> it = c.iterator();

        Node node1 = it.next();

        //结构改变
        c.remove(node1);
        //结构改变
        c.add(new Node(5,"e"));

        System.out.println(it.next());
        System.out.println(c);

        //序列改变了需要重新获取迭代器
        c.add(new Node(6,"f"));
        Iterator<Node> it1 = c.iterator();
        while (it1.hasNext()){
            System.out.println(it1.next());
        }

ListIterator


由上面我们可以知道next()只能从前往后迭代,但是作为iterator的子类listIterator是可以双向移动的,
我们来看看它的源代码

  • interface ListIterator extends Iterator
  • boolean hasPrevious(); / /如果以逆向遍历列表,列表迭代器前面还有元素,则返回 true,否则返回false
  • E previous() ; //返回列表中ListIterator指向位置后面的元素
  • int nextIndex(); //返回列表中ListIterator所需位置后面元素的索引
  • int previousIndex();
  • void remove();
  • void set(E e);//从列表中将next()或previous()返回的最后一个元素返回的最后一个元素更改为指定元素e其实呢就是查找到了并修改
  • void add(E e);//将指定的元素插入列表,插入位置为迭代器当前位置之前
import java.util.*;
public class TestListIterator{
 
    public static void main(String[] args) {
 1       ArrayList<String> a = new ArrayList<String>();
 2       a.add("aaa");
 3       a.add("bbb");
 4       a.add("ccc");
 5       System.out.println("Before iterate : " + a);
 6       ListIterator<String> it = a.listIterator()
 7       while (it.hasNext()) {
 8           System.out.println(it.next() + ", " + it.previousIndex() + ", " + it.nextIndex());
 9       }
10        while (it.hasPrevious()) {
11            System.out.print(it.previous() + " ");
12        }
13        System.out.println();
14        it = a.listIterator(1);//调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。
15        while (it.hasNext()) {
16            String t = it.next();
17            System.out.println(t);
18            if ("ccc".equals(t)) {
19                it.set("nnn");
20            } else {
21                it.add("kkk");
22            }
23        }
24        System.out.println("After iterate : " + a);
    }
}

特别要注意listIterator的remove()方法

迭代器需要注意的⼀些特性:

  1. List 是有序的,⽽ Set 是⽆序的,它们都实现了 Iterable 接⼝,⽽依赖于位置的 add ⽅法由迭代器负责。因此,在 Iterator 接⼝中没有 add ⽅法,⽽专⻔⽤于有序列表 List 的 ListIterator 接⼝有 add ⽅法。
  2. add ⽅法只依赖于迭代器的位置,在当前迭代器位置之前添加⼀个对象
    remove ⽅法依赖于迭代器的状态,删除上⼀次移动迭代器位置返回的对象(上⼀次移动迭代器越过的对象,next/previous⽅法)
  3. set ⽤⼀个新元素取代上⼀次移动迭代器位置返回的对象(上⼀次移动迭代器越过的对象,next/previous
    ⽅法)
  //双向迭代器使用
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("c");

        //这里提前说明一下,lterator实现了Iteratorable接口,然后ListIterator继承于Iterator
        ListIterator<String> listIterator = list.listIterator();
        //迭代器在第一个对象之前,它之前没有对象找不到就报错了java.util.NoSuchElementException
//        System.out.println("hasprevious = " + listIterator.previous());
        //是否有下一个对象
        System.out.println("hasnext = " + listIterator.hasNext());

        //迭代器移动到下一个位置
        System.out.println("next = "+listIterator.next());
        //迭代器前一个的坐标,如果迭代器在第一个之前,那么显示是-1
        System.out.println("previousIndex = " + listIterator.previousIndex());
        //迭代器下一个元素的位置
        System.out.println("nextindex = " + listIterator.nextIndex());

        //此时迭代器在第一个元素之前,所以查找并修改,在迭代器前面的那个元素
        listIterator.set("d");
        System.out.println(list);//[d, B, c]

        //在迭代器所指向位置添加(插入)一个对象
        listIterator.add("e");
        System.out.println(list);//[d, e, B, c]
        System.out.println("previousIndex = " + listIterator.previousIndex());//1

        //这里由于list被修改
        //如果没有在listIterator.next();之后调用listIterator.remove();
        // 既没有调用 next 也没有调用 previous,或者在最后一次调用 next 或 previous 后调用了 remove 或 add
        //就会抛出java.lang.IllegalStateException
        listIterator.next();
        listIterator.remove();
        //抛出异常java.lang.IllegalStateException
        System.out.println(list);//[d, e, c]

        System.out.println("next = " + listIterator.next());

迭代器遍历和for遍历


 /*
        ArrayList运行结果
        插入:17ms
        for遍历:2ms
        while迭代器遍历:2ms
         */
       testTime(new ArrayList(),10000);
        /*
        LinkedList运行结果
        插入:4ms
        for遍历:180ms
        while迭代器遍历:1ms
         */
       testTime(new LinkedList(),10000);
       /*
       结论说明迭代器遍历用时比较短比较好
        */

    }

    public static void testTime(List list,int times){
        for(int i = 0;i < times ; i++){
            list.add("A");
        }

        //索引为10的位置插入10000个B,并计算耗时
        long start = System.currentTimeMillis();
        for(int i = 0 ; i < times ; i++){
            list.add(10,"B");
        }

        System.out.println("耗时ms="+( System.currentTimeMillis()-start));

        //遍历for
        start = System.currentTimeMillis();
        for(int i=0 ; i<list.size(); i++){
            list.get(i);
        }
        System.out.println("for耗时=" + (System.currentTimeMillis()-start));

        //迭代器
        start = System.currentTimeMillis();
        ListIterator listIterator = list.listIterator();
        while (listIterator.hasNext()){
            listIterator.next();
        }
        System.out.println("Iterator耗时=" + (System.currentTimeMillis()-start) );

可以总结出:在不知道list类型的情况下如果需要遍历,优先使用迭代器,当然我们也可以使用instanceof RandomAccess来判断是否支持快速随机访问

这里我附上API帮助文档的说明:
将操作随机访问列表的最佳算法(如 ArrayList)应用到连续访问列表(如 LinkedList)时,可产生二次项的行为。如果将某个算法应用到连续访问列表,那么在应用可能提供较差性能的算法前,鼓励使用一般的列表算法检查给定列表是否为此接口的一个 instanceof,如果需要保证可接受的性能,还可以更改其行为。

现在已经认识到,随机和连续访问之间的区别通常是模糊的。例如,如果列表很大时,某些 List 实现提供渐进的线性访问时间,但实际上是固定的访问时间。这样的 List 实现通常应该实现此接口。实际经验证明,如果是下列情况,则 List 实现应该实现此接口,即对于典型的类实例而言,此循环:

 for (int i=0, n=list.size(); i < n; i++)
     list.get(i);

的运行速度要快于以下循环:

 for (Iterator i=list.iterator(); i.hasNext(); )
     i.next();
//RandomAccess
        if(list instanceof RandomAccess){
            for(int i=0 ; i < times ; i++){
                list.get(i);
            }
        }else {
            Iterator iterator = list.listIterator();
            while (iterator.hasNext()){
                iterator.next();
            }
        }

总结:
Iterator和ListIterator主要区别在以下方面:

(1)ListIterator有add()方法,可以向List中添加对象,而Iterator不能
(2)ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
(3)ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
(4)都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。

java.util.Collection接口主要的方法


  • Iterator iterator() 返回⼀个⽤于访问集合中每个元素的迭代器
  • int size() 返回当前存储在集合中的元素个数
  • boolean isEmpty() 如果集合中没有元素,返回 true
  • boolean contains(Object o) 如果集合中包含了⼀个与 o 相等的对象,返回 true
  • boolean containsAll(Collection<?> c) 如果这个集合包含 c 集合中的所有元素,返回 true
  • boolean add(E e) 将⼀个元素添加到集合中
  • boolean addAll(Collection<? extends E> c) 将 c 集合中的所有元素添加到这个集合
  • boolean remove(Object o) 从这个集合中删除等于 o 的对象,如果有匹配的对象被删除,返回
    true
  • boolean removeAll(Collection<?> c) 从这个集合中删除 c 集合中存在的所有元素。如果有匹配的对象被删除,返回 true
  • void clear() 从这个集合中删除所有的元素
  • boolean retainAll(Collection<?> c) 从这个集合中删除所有与 c 集合中的元素不同的元素, 如果有对象被删除,返回 true
  • Object[] toArray() 返回这个集合的对象数组
  • T[] toArray(T[] a) 返回这个集合的对象数组。如果 a ⾜够⼤,就将集合中的元素填⼊这个数组,剩余空间填补 null;否则,分配⼀个类型为 T 的新数组,⻓度等于集合的⼤⼩,并添⼊集合中的元素
ArrayList

数组列表 ArrayList 内部使⽤数组实现,⽽数组使⽤下标进⾏访问时速度很快;⽽在中间位置插⼊和删除元素时,因为要移动插⼊/删除位置以后的元素,因此速度⽐较慢。
如果能提前对数据大小进行评估,可以提前初始化容量避免扩容
比如:
list list = new ArrayList<>(10000【这里填容量不填默认是0】);

插入删除操作类似栈的操作:
在这里插入图片描述

ArrayList 中的元素,它有两种访问⽅式:
  1. 使⽤迭代器 Iterator
  2. 使⽤ get 和 set ⽅法随机快速的访问每个元素
    扩容机制

创建 ArrayList 时可以指定初始容量⼤⼩,如果未指定则创建⼀个空数组,⻓度为0

如果存储⻓度⼤于容量⼤⼩时,⾃动进⾏扩容,每次扩容到原来的 1.5 倍。相关代码如下:

int newCapacity = oldCapacity + (oldCapacity >> 1); 
elementData = Arrays.copyOf(elementData, newCapacity);

ArrayList 中的⽅法不是同步的,如果有线程安全需要可以使⽤ Vector。Vector 每次扩容到原来的 2 倍。

LinkedList

链表 LinkedList 内部使⽤双向链表实现(Java 语⾔中,所有链表都是双向链接(doubly linked)),从链表中间删除或插⼊⼀个元素代价很低,但随机访问速度慢:删除元素只需要修改位于删除元素前后的元素的 next 和 previous 引⽤指向即可;插⼊元素只需要修改新插⼊元素和插⼊位置前后的元素的 next 和 revious 引⽤指向。

内部实现和源码;
双向链表,内部没有声明数组,而是定义了Node类型的first和last, 用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基 本结构。Node除了保存数据,还定义了两个变量:
(1)prev变量记录前一个元素的位置
(2)next变量记录下一个元素的位置
在这里插入图片描述
源码:
private static class Node {
E item;
Node next;
Node prev;

Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

在这里插入图片描述

链表虽然也实现了随机访问,但是不⽀持快速随机访问,上述代码每次查找元素都会从列表的头部开始重新开 始搜索(查看第 n 个元素时,需要从头开始越过 n - 1 个元素)。LinkedList 对象根本不做任何缓存;仅到索引⼤于 size() / 2 就从列表尾端开始搜索元素。
我们可以通过是否实现了 RandomAccess 接⼝来判断对象是否⽀持快速随机访问。使⽤链表唯⼀的理由是尽可能地减少在列表中间插⼊或删除元素所付出的代价
ArrayList ⻓于随机快速访问,⽽ LinkedList ⻓于在序列中间插⼊/删除元素

Set

(1)Set接口 不保存重复的元素,它最常⽤的就是测试归属,因此查找是 Set 最重要的操作。
(2)Set 基于对象的值来确定归属性,判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法,Set 具有和 Collection 完全⼀样的接⼝,set接口没有提供额外的方法

HashSet

HashSet 内部使⽤ HashMap 来实现,HashMap 的键值存储的即是 HashSet 的值,⽽键值对应的值存放了⼀个静态对象 PRESENT。
(1)HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
(2)特点:

  • 不能保证元素的排列顺序
  • HashSet 不是线程安全的
  • 集合元素可以是 null
    (3)判断两个元素相等的标准:
    两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
    (4)存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”

主要代码如下:

public class HashSet<E> extends AbstractSet<E> implements Set<E> {
// ⽤于存放数据,键值即为 Set 集的元素
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object(); 
public HashSet() {
map = new HashMap<>();
}

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

public Iterator<E> iterator() { return map.keySet().iterator();
}
}

但是我们看见了transient关键字最开始我也不知道,针对这个关键字我们来剖析一下:

  1. transient的作用和使用方法:

我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。

然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,有些属性不需要被序列化,打个栗 子,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化

实列代码:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * @description 使用transient关键字不序列化某个变量
 *        注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致
 *        
 * @author Alexia
 * @date  2013-10-15
 */
public class TransientTest {
    
    public static void main(String[] args) {
        
        User user = new User();
        user.setUsername("Alexia");
        user.setPasswd("123456");
        
        System.out.println("read before Serializable: ");
        System.out.println("username: " + user.getUsername());
        System.err.println("password: " + user.getPasswd());
        
        try {
            ObjectOutputStream os = new ObjectOutputStream(
                    new FileOutputStream("C:/user.txt"));
            os.writeObject(user); // 将User对象写进文件
            os.flush();
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "C:/user.txt"));
            user = (User) is.readObject(); // 从流中读取User的数据
            is.close();
            
            System.out.println("\nread after Serializable: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class User implements Serializable {
    private static final long serialVersionUID = 8294180014912103005L;  
    
    private String username;
    private transient String passwd;
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public String getPasswd() {
        return passwd;
    }
    
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

}

输出:
read before Serializable:
username: Alexia
password: 123456

read after Serializable:
username: Alexia
password: null

总结:
java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中

1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

注意上面代码:
第三点可能有些人觉得有问题,因为User类中的username变量前加上static关键字后,程序运行结果依然不变,即static类型的username也读出来为“Alexia”了,(一个静态变量不管是否被transient修饰,均不能被序列化),反序列化后类中static型变量username的值为当前JVM中对应static变量的值,这个值是JVM中的不是反序列化得出的

弄明白后我们继续:

我们来看看add()方法是如何添加的:
在这里插入图片描述

注:

  • jdk1.8采用的是尾插,7是头插
  • 底层(这里是指键值)也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12)就会扩大容量为原来的2倍。(16扩容为32,依次为64,128 等)
    (1)向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该 散列函数设计的越好)
    (2)如果两个元素的hashCode()值相等(hash冲突),会再继续调用equals方法,如果equals方法结果 为true,添加失败;如果为false,那么会保存该元素,但是该数组的位置已经有元素了, 那么会通过链表的方式继续链接。
    (3)如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。

扩展:
重写hashcode()的原则:
(1)在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
(2)当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。
(3)对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
重写 equals() 方法的基本原则:
复写equals方法的时候一般都需要同时复写hashCode方法。通
常参与计算hashCode的对象的属性也应该参与到equals()中进行计算
Eclipse/IDEA工具里hashCode()的重写:
以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals和hashCode问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?

  • 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
  • 并且31只占用5bits,相乘造成数据溢出的概率较小。
  • 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效
    率)
  • 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

TreeSet


(1)树集 TreeSet 是⼀个有序集合(sorted collection)。可以以任意顺序将元素插⼊到集合中,在遍历时,每个元素⾃动按照排序后的顺序呈现,确保集合元素处于排序状态。

将⼀个元素添加到 TreeSet 中⽐添加到 HashSet 中慢,但是要⽐将元素添加到数组或链表的正确位置上要快很多。TreeSet 可以⾃动的对元素进⾏排序

public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>
{
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object(); TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}

public TreeSet() {
this(new TreeMap<E,Object>());
}

//这里TreeMap采用红黑树的结构
public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator));
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}

public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}

(2)TreeSet底层使用红黑树结构存储数据
具体讲解:
红黑树优秀博客
(3)方法:
新增的方法如下: (了解)
Comparator comparator()
Object first()
Object last()
Object lower(Object e)
Object higher(Object e)
SortedSet subSet(fromElement, toElement)
SortedSet headSet(toElement)
SortedSet tailSet(fromElement)

TreeSet有两种排序方法:自然排序和定制排序

默认情况TreeSet 自然排序
(1)通过 Comparable 接⼝的 compareTo ⽅法来进⾏排序,根据第一个参数小于、等于或大于第二个参数分别返回负整数、零或正整数

(2)因此如果要添加一个⾃定义对象到 TreeSet,就必须实现 Comparable 接⼝⾃定义排列顺序,因为在 Object 中没有compareTo 接⼝的默认实现,但向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较,因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。
扩展:Comparable 的典型实现:

  • BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
  • Character:按字符的 unicode值来进行比较
  • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
  • String:按字符串中字符的 unicode 值进行比较
  • Date、Time:后边的时间、日期比前面的时间、日期大
    然⽽使⽤ Comparable 接⼝定义排序也有其局限性,对于给定的类,只能实现这个接⼝⼀次,也就是说只能有⼀种排序。如果在⼀个集合中需要按照⽤户注册信息进⾏排序,⽽在另⼀个集合中却要按⽤户年龄排序,应该怎么办?

这种情况下,我们可以通过创建 TreeSet 时将 Comparator 对象传给它的构造器告诉它排序的⽐较⽅法,对于设置了 Comparator 对象的 TreeSet,向⾥添加的对象不需要实现 Comparable 接⼝

interface Comparator<T> { int compare(T a, T b);
}

扩展:定制排序
场景:如果TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。
显然能发现实现的接口不一样了
练习:

//假设Person的hashcode和equals重写过
HashSet set = new HashSet(); 
Person p1 = new Person(1001,"AA"); 
Person p2 = new Person(1002,"BB");

set.add(p1);
set.add(p2);
p1.name = "CC"; set.remove(p1); 
System.out.println(set);
set.add(new Person(1001,"CC")); 
System.out.println(set); 
set.add(new Person(1001,"AA")); 
System.out.println(set);

LinkedHashSet

(1)链接散列集 LinkedHashSet 继承⾃ HashSet,在使⽤哈希桶的⽅式存放元素的同时,使⽤链表的⽅式来记录插⼊元素的顺序
(2)LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
(3)LinkedHashSet 不允许集合元素重复。
自然而然底层就不同于hashSet了
列:
在这里插入图片描述

EnumSet

枚举集 EnumSet 是⼀个枚举类型元素集的⾼效实现。由于枚举类型只有有限个实例,所以 EnumSet 内部使⽤位序列实现,如果对应的值在集中,则相应的位被置为 1。

Queue
队列是⼀种“先进先出”的数据结构,可以在队列尾部添加元素,在队列的头部删除元素。Java 6 中引⼊了
Deque 接⼝,并由 ArrayDeque 和 LinkedList 类实现,这两个类都提供了双端队列。队列主要有以下两种:

队列,可以在尾部添加⼀个元素,在头部删除⼀个元素。
双端队列,有两个端头的队列,可以在头部和尾部同时添加或删除元素

注: 队列和双端队列都不⽀持在队列中间添加元素
java.util.Queue 接⼝主要⽅法:

  • boolean add(E e)

  • boolean offer(E e)

如果队列没满,将给定元素添加到队列尾部并返回 true。如果队列满了 add ⽅法抛 IllegalStateException
异常,offer ⽅法返回 false

  • E remove()

  • E poll()

  • 如果队列不空,删除并返回这个队列头部的元素。如果队列是空 remove ⽅法抛
    NoSuchElementException 异常,poll ⽅法返回 null

  • E element()

  • E peek()

如果队列不空,返回队列头部的元素,但不删除。如果队列为空 element ⽅法抛
NoSuchElementException 异常,peek ⽅法返回 null
java.util.Deque 接⼝主要⽅法:

void addFirst(E e)

void addLast(E e)

boolean offerFirst(E e)

boolean offerLast(E e)

将给定的对象添加到双端队列的头部或尾部。如果队列满了,addFirst/addLast ⽅法抛
IllegalStateException 异常,offerFirst/offerLast ⽅法返回 false

E removeFirst()

E removeLast()

E pollFirst()

E pollLast()

如果队列不空,删除并返回队列头部的元素。如果队列为空,removeFirst/removeLast ⽅法抛
NoSuchElementException 异常,pollFirst/pollLast ⽅法返回 null

E getFirst()

E getLast()

E peekFirst()

E peekLast()

如果队列⾮空,返回队列头部的元素,但不删除。如果队列为空,getFirst/getLast ⽅法抛
NoSuchElementException 异常,peekFirst/peekLast ⽅法返回 null

ArrayDeque

ArrayDeque 是使⽤循环数组实现的双端队列,可以在队列两边插⼊和删除元素。循环数组是有界集合,容量有限,当达到容量限制时进⾏⾃动扩容,每次扩容原来的 1 倍。

LinkedList
LinkedList 也实现了 Deque 接⼝,是双端队列的链表实现⽅式。它的性能要⽐ ArrayDeque 低。

PriorityQueue
优先级队列(priority queue)中的元素可以按任意的顺序插⼊,却总是按照排序的顺序进⾏检索。

remove ⽅法总是会获得当前优先级队列中最⼩的元素当使⽤迭代的⽅式处理时,则未进⾏排序

优先级队列使⽤堆(heap)这种数据结构,它是⼀个可以⾃我调整的⼆叉树,对树执⾏添加(add)和删除
(remove)操作,可以让最⼩的元素移动到根,⽽不必对元素进⾏排序。
和 TreeSet ⼀样,优先级队列可以保存实现了 Comparable 接⼝的类对象,也可以保存在构造器中提供⽐较器的对象。

使⽤优先级队列的典型示例是任务调度。

Map


java.util.Map 接⼝主要⽅法:

  • K getKey() 返回这个条⽬的键
  • V getValue() 返回这个条⽬的值
  • V setValue(V value) 设置该键在映射表中对应的新值,并返回旧值

映射表(map)⽤来存放键/值对,根据键就能查找到值。Java 类有映射表的两个通⽤实现:HashMap 和TreeMap,他们都实现了 Map 接⼝。
散列映射表(HashMap)对键进⾏散列,散列⽐较函数只能作⽤于键,与键关联的值不能进⾏散列⽐较。树映射表(TreeMap)⽤键的整体顺序对元素进⾏排序,并将其组织成搜索树。
键必须是唯⼀的,不能对同⼀个键存放两个值。如果对同⼀个键两次调⽤ put ⽅法,第⼆个的值会取代第⼀个

映射表本身并不是⼀个集合,但是我们可以获得映射表的视图,这是⼀组实现了 Collection 接⼝对象,或者它的⼦接⼝的视图。有 3 个视图:
1.键 集 Set keySet()
2.值集合(不是集) Collection values()
3.键/值对集 Set<Map.Entry<K,V>> entrySet()
注意:keySet 既不是 HashSet,也不是 TreeSet,⽽是⼀个实现了 Set 接⼝的其他类的对象。因此可以像使⽤集⼀样使⽤ keySet。

HashMap


列如:
在这里插入图片描述
值是2
jdk8中底层是数组+链表+红黑树
在这里插入图片描述
hashcode怎么算的:
int hash = key.hashcode
int i = hash % table.length;
得到数组下标
容量默认
在这里插入图片描述

链表和数组都是有序的元素序列,如果我们不在意元素的顺序,只关⼼查找元素的速度,有⼀种数据结构可以实现,这就是散列表。

散列表(hash table)为每⼀个对象计算⼀个整数值,这个整数值称为散列码(hash code)。散列码是由对象的实例域产⽣的⼀个整数。

Object 默认的计算对象散列码的⽅法返回的是对象的地址。因此⾃定义类时,需要实现这个类的 hashCode
⽅法,并且 hashCode ⽅法应该与 equals ⽅法兼容,当 a.equals(b) == true 时,a 与 b 的散列码必须相同(a.hashCode() == b.hashCode())。
在 Java 语⾔中,散列表⽤链表数组的⽅式实现,每个列表被称为桶(bucket),列表⻓度称为桶数,⽽桶由数组组成。要查找或存放对象时,先计算对象的散列码,然后与桶的总数取余,所得的结果就是查找或存放对象 在桶中的索引。如果是查找对象,则将查找对象与桶中的所有对象进⾏⽐较(使⽤ equals ⽅法),如果存在则返回,否则返回 null;如果是保存对象,桶中没有其他元素时直接插⼊到桶中,否则⽤新对象与桶中的所有对象进⾏⽐较,查看对象是否已经存在。如果桶已经被占满了,这种现象称为散列冲突(hash collision)。
桶数和散列表的运⾏性能有关,如果知道最终要插⼊散列表的元素个数,就可以设置桶数,通常设置为预计元素个数的75% ~ 150%。
当散列表太满,就需要再散列(rehashed)。再散列是指创建⼀个桶数更多的表,并将所有元素插⼊到新表中,然后丢弃原来的表。

我们根据装填因⼦(load factor)来判断散列表是否太满,也即是何时进⾏再散列。例如默认装填因⼦为0.75, 当表中超过75%的位置已填⼊元素,这个表就会⽤双倍的桶数⾃动的进⾏再散列。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值