Java集合:Collection、List、Set、Map、泛型

1.集合的理解和好处

2.集合的框架体系图 ★

3.Collection接口的特点和使用 ★

4.List和Set接口的特点和使用★

5.List接口的实现类学习★

6.Set接口的实现类学习★

7.Map接口的特点和使用★

8.Map接口的实现类学习★

9.Collections工具类的使用★

10.泛型的使用★

0.集合的学习思路

层面1:应用层面 √

  • 可以掌握重点的集合类的使用步骤

层面2:理解层面【面试前掌握】

  • 理解ArrayList的源码
  • 理解HashMap的源码

掌握:

  • Collection和Map的对比
  • List和Set的对比
  • ArrayList和Vector的对比
  • ArrayList和LinkedList的对比
  • HashMap和Hashtable的对比
  • Collections和Collection的对比

一、集合的理解和好处

1.理解
集合:就是一种容器,都是用于保存一组元素

2.集合和数组的对比:

数组的不足:

  • 数组的长度必须提前指定,而且一旦指定不能更改
  • 数组只能保存相同类型的元素

集合:

  • 集合在使用时,长度不用指定,而且可以实现自动扩容或截断
  • 集合没有指定泛型之前,默认保存的是任意类型的元素(Object类型)

指定泛型之后,可以保存对应类型 的元素

示例代码:

// 使用数组--------------------
Animal[] animals = new Animal[3];
animals[0] = new Animal();    
animals[1] = new Animal();    
animals[2] = new Animal();    

Animal[] newAni = new Animal[animals.length+1];
//复制数组
//添加新元素
animals=newAni;

// 使用集合--------------------
List list= new ArrayList();

list.add(new Animal());

①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。

总结:

  • 数组:比较适合保存基本类型的元素
  • 集合:比较适合保存引用类型的元素(对象)

二、集合的框架体系图 ★

 

三、Collection接口

1.特点 

里面没有提供直接的实现类,而是提供了子接口,子接口中有具体的实现类

该接口中提供了一系列常见的集合操作的方法:增加、删除、查找

2.常见方法

* add/addAll
* remove/removeAll
* contains/containsAll
* clear
* size
* isEmpty

/**
 * 此类用于演示Collection接口的常见方法
 * @author liyuting
 * add/addAll
 * remove/removeAll
 * contains/containsAll
 * clear
 * size
 * isEmpty
 * 
 *
 */
public class TestCollection1 {
    
    public static void main(String[] args) {
        //1.创建Collection接口对象
        
        Collection col = new ArrayList();
        //2.调用常见方法
        //方法1:add
        col.add("赵四");
        col.add(true);
        col.add(null);
        col.add('男');
        col.add(180.5);
        System.out.println(col);
        //方法2:addAll
        Collection c = new ArrayList();
        c.add("蓝盈莹");
        c.add("周一围");
        col.addAll(c);
        System.out.println(col);
        //方法3:remove
        col.remove("蓝盈莹2");
        System.out.println(col);
        //方法4:removeAll
        Collection c1 = new ArrayList();
        c1.add(null);
        c1.add(180.5);
        col.removeAll(c1);
        System.out.println(col);
        
        //方法5:clear清除
//        col.clear();
//        System.out.println(col);
        
        //方法6:contains查找
        boolean contains = col.contains("赵四");
        System.out.println(contains?"赵四存在":"赵四没了");
        
        //方法7:containsAll批量查找
        Collection c2 = new ArrayList();
        c2.add("赵四");
        c2.add("蓝盈莹");
        c2.add("周一围");
        
        System.out.println(col.containsAll(c2));
        
        //方法8:size 获取实际元素个数
        System.out.println(col.size());
        
        //方法9:isEmpty 判断是否为空
        System.out.println(col.isEmpty());
    }
}
View Code

对象型元素示例:

/**
 * 此类用于演示使用Collection添加对象类型元素 ★
 *
 */
public class TestCollection2 {
    
    public static void main(String[] args) {
        
        //1.创建Collection对象
        Collection col = new ArrayList();
    
        //2.调用方法
        
        col.add(new Book("红楼梦",98,"曹雪芹"));
        col.add(new Book("西游记",93,"吴承恩"));
        col.add(new Book("水浒传",108,"施耐庵"));
        col.add(new Book("三国演义",88,"罗贯中"));
        col.add(new Book("西厢记",68,"王师傅"));
        
        System.out.println(col);
        col.remove(new Book("三国演义",88,"罗贯中")); // 这种删除无法删掉,需要按地址删除
        System.out.println(col);
    }
}
View Code

3.遍历方式

迭代器工作特点:

1、每次只能下移一位
2、只能下移不能上移!

注意: ★ 不适合做增删改

* ①使用迭代器过程,不适合做增删,容易报异常ConCurrentModificationException
* ②使用迭代器过程,可以做修改,但如果修改地址,没有效果!
* ③使用迭代器过程,如果非要做删除,可以使用迭代器本身的remove方法!

方式1:使用迭代器

迭代器的使用步骤:

① 获取迭代器对象,指针默认在最上方

② 通过调用hasNext判断下一个是否有元素

③ 通过调用next下移一位并获取当前元素

@Test
public void test1(){
    //3.遍历集合
    //①获取迭代器
    Iterator iterator = col.iterator();

    //②判断,如果返回true,则进行下一步
    while(iterator.hasNext()){
        //③下移一位,并获取对应元素
        System.out.println(iterator.next());
    }
}

方式二:为了简化Iterator的语法,jdk5.0出现了增强for

增强for的本质就是Iterator,只是语法简化了!

语法:

for(元素类型 元素名:集合或数组名){
    //访问元素即可
}

示例:

@Test
public void test2() {
    //3.遍历集合
    for(Object o: col){
        System.out.println(o);
  } }

四、List接口的特点和使用

1.List接口的特点

  • 有序(插入和取出的顺序一致的),原因:按一个整数索引记录了插入的位置,可以按索引操作
  • 允许重复

2.List接口的特有方法

* add(object)增
* remove(index)按指定索引删
* set(index,object)改
* indexOf(object)查
* add(index,object)插入
* get(index)获取

public static void main(String[] args) {
    //1.创建List接口的对象
    
    List list = new ArrayList();
    
    //2.调用添加
    list.add("john");
    list.add("lucy");
    list.add(null);
    list.add("john");
    list.add(null);
    list.add("jack");
    System.out.println(list);
    
    //特有方法1:add(index,object)插入
    list.add(0, "张益达");
    System.out.println(list);
    //特有方法2:remove(index) 删除
    /*
     * 细节:如果元素类型为整型,如果删除的实参为int类型,默认是按索引删除;如果想按指定元素删除,则需要装箱再删除!
     */
    list.remove(2);
    
    //特有方法3:set(index,object)修改
    System.out.println(list);
//        [张益达, john, null, john, null, jack]
    list.set(2, "虚竹");
    
    //特有方法4:indexOf(object)查找
    System.out.println(list.indexOf("张益达"));
    
    //特有方法5:get(index)获取
    System.out.println(list.get(30));
    
    //3.遍历
    System.out.println(list);
}
View Code

3.List接口的遍历方式

//方式1:使用iterator
@Test
public void test1() {
    //3.遍历
    Iterator iterator = list.iterator();

    while(iterator.hasNext()){
        Object book = iterator.next();
        System.out.println(book);
    }
}
//方式2:使用增强for
@Test
public void test2() {
    //3.遍历
    for (Object object : list) {
        System.out.println(object);
    }
}

//方式3:使用普通for
@Test
public void test3() {
    for(int i=0;i<list.size();i++){
        Object object = list.get(i);
        System.out.println(object);
    }
}

五、List接口实现类

1.ArrayList

底层结构:可变数组

jdk8:

ArrayList中维护了Object[] elementData,初始容量为0.
第一次添加时,将初始elementData的容量为10
再次添加时,如果容量足够,则不用扩容直接将新元素赋值到第一个空位上
如果容量不够,会扩容1.5倍

jdk7:

ArrayList中维护了Object[] elementData,初始容量为10.
添加时,如果容量足够,则不用扩容直接将新元素赋值到第一个空位上
如果容量不够,会扩容1.5倍

jdk7和jdk8区别:
jdk7 相当于饿汉式,创建对象时,则初始容量为10
jdk8 相当于懒汉式,创建对象时,并没有初始容量为10,而在添加时才去初始容量为10

2.Vector、LinkedList

1)List接口的实现类:Vector

底层结构:可变数组,和ArrayList很像

2)List接口的实现类:LinkedList

底层结构:双向链表

LinkedList中维护了两个重要的属性 first和last,分别指向首节点和尾节点。

每个节点(Node类型)里面又维护了三个属性item、next、prev,分别指向当前元素、下一个、上一个元素。最终实现手拉手的链表结构!

3.总结:实现类对比

1)ArrayList和Vector的对比

  底层结构 版本 线程安全(同步) 效率 扩容的倍数
ArrayList 可变数组 不安全(不同步) 较高 1.5倍
Vector 可变数组 安全(同步) 较低 2倍

2)ArrayList和LinkedList的对比

  底层结构 线程安全(同步) 增删的效率 改查的效率
ArrayList 可变数组 不安全(不同步) 前面和中间的增删,较低 较高
LinkedList 双向链表 不安全(不同步) 前面和中间的增删,较高 较低

3)总结:

  • 如果考虑线程安全问题:Vector
  • 不考虑线程安全问题:
  • 查找较多:ArrayList
  • 增删较多:LinkedList

六、Set接口

1.Set接口的特点

  • 不允许重复,至多一个null
  • 无序(插入和取出的顺序不一致),没有索引

2.Set接口的特有方法:没有特有方法,都是从Collection继承来的

3.Set接口的遍历方式:和Collection的遍历方式同

4.Set接口的实现类:HashSet

底层结构:维护了一个HashMap对象,也就是和HashMap的底层一样,基于哈希表结构的

如何实现去重【重点

底层通过调用hashCode方法和equals方法实现去重

先调用hashCode,获取一个整数索引index(要存放在数组的位置, (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度)),如果该索引处没有元素,则直接可以添加;

如果该索引处有其他元素则进行equals判断,如果不相等,则以链表形式追加到已有元素后面;(不同数据的哈希值肯定不同,但计算出的索引可能相同;相同的数据哈希值和索引肯定相等)

如果相等,则直接覆盖,返回false 

应用:通过HashSet添加元素时,如果认为内容相等的为重复元素,则需要重写该元素的hashCode和equals方法(引用型)

hashCode和equals重写示例:

public class Book{
    
    private String name;
    private double price;
    private String author;
    
    /**
     * 原则:
     * 1、和属性值相关
     * 2、属性一样的话,要求哈希值肯定一样
     *    属性不一样的,尽最大的限度让哈希值不一样!
     */

//    // 简单方式    
//    @Override
//    public int hashCode() {
//        return name.hashCode()+(int)price+author.hashCode()*31;
//    }
    
//    @Override
//    public boolean equals(Object obj) {
//        System.out.println(this.name+" pk "+((Book)obj).name);
//        
//        if(this==obj)
//            return true;
//        if(!(obj instanceof Book))
//            return false;
//        Book b = (Book) obj;
//        
//        return this.name.equals(b.name)&&this.price==b.price&&this.author.equals(b.author);
//    }
    public String getName() {
        return name;
    }
    
    // 推荐的重写
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((author == null) ? 0 : author.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        long temp;
        temp = Double.doubleToLongBits(price);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Book other = (Book) obj;
        if (author == null) {
            if (other.author != null)
                return false;
        } else if (!author.equals(other.author))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        if (Double.doubleToLongBits(price) != Double.doubleToLongBits(other.price))
            return false;
        return true;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author;
    }
    public Book(String name, double price, String author) {
        super();
        this.name = name;
        this.price = price;
        this.author = author;
    }
    
    public String toString(){
        /*
         * %s:字符串
         * %c:字符
         * %f:浮点
         * %d:整数
         */
        return String.format("名称:%s    价格:%.2f    作者:%s",name,price,author);
    }
}
View Code

【问题】为什么用eclipse复写hashCode方法,有31这个数字?

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

5.Set接口实现类:LinkedHashSet

LinkedHashSet 是 HashSet 的子类
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
LinkedHashSet 不允许集合元素重复。

6.Set接口的实现类:TreeSet

特点:

  • 不允许重复,里面不允许null
  • 可以实现对里面元素进行排序

自然排序、定制排序

底层结构:底层维护了一个TreeMap,而TreeMap底层是红黑树结构,可以实现对元素进行排序

应用:

方式一:自然排序

要求:必须让添加元素的类型实现Comparable接口,实现里面的compareTo方法

方式二:定制排序

要求:创建TreeSet对象时,传入一个Comparator接口的对象,并实现里面的compare方法

示例:

public class TestTreeSet {
    
    //使用自然排序
    @Test
    public void test1(){
        
        //1.创建TreeSet对象
        TreeSet set = new TreeSet();
        
        //2.添加        
         set.add(new Book("百年孤独",100,"马尔克斯"));
         set.add(new Book("春风十里不如你",80,"冯唐"));
         set.add(new Book("多情剑客无情剑",60,"古龙"));
         set.add(new Book("春风十里不如你",80,"冯唐"));
         
         //3.遍历
         CollectionUtils.print1(set);
    }
    //使用定制排序
    @Test
    public void test2(){
        
        //1.创建TreeSet对象
        TreeSet set = new TreeSet(new Comparator(){

            @Override
            public int compare(Object o1, Object o2) {
                
                Book b1 = (Book) o1;
                Book b2 = (Book) o2;
                return Double.compare(b2.getPrice(),b1.getPrice());
            }
            
        });
        
        //2.添加
         set.add(new Book("百年孤独",100,"马尔克斯"));
         set.add(new Book("春风十里不如你",80,"冯唐"));
         set.add(new Book("多情剑客无情剑",60,"古龙"));
         set.add(new Book("春风十里不如你",80,"冯唐"));
         
         //3.遍历
         CollectionUtils.print1(set);
    }
}
View Code

如何实现去重:

通过比较方法的返回值是否为0来判断是否重复

七、Map接口的特点和使用

1.Map接口的特点

用于保存一组键值对映射关系的

其中键不可以重复,值可以重复。而且一个键只能映射一个值

2.Map接口的常见方法

put 添加

remove删除

containsKey查找键

containsValue查找值

get根据键获取值

size获取键值对的个数

isEmpty判断元素是否为空

clear清除

entrySet 获取所有的关系

keySet获取所有的键

values获取所有的值

3.Map接口的遍历方式

方式1:通过调用entrySet

@Test
public void test1() {
    //步骤1 :获取所有的关系
    Set entrys = map.entrySet();
    //步骤2:遍历所有的关系

    Iterator iterator = entrys.iterator();
    while(iterator.hasNext()){
        //获取每一对关系
        Map.Entry entry = (Entry)iterator.next();
        //根据关系,获取对应的键
        //根据关系,获取对应的值
        System.out.println(entry.getKey()+":"+entry.getValue());
    }
}

方式2:通过调用keySet

@Test
public void test2() {
    //步骤1:获取所有的键
    Set keys = map.keySet();
    //步骤2:遍历所有的键
    for (Object key : keys) {
        System.out.println(key+":"+map.get(key));
    }
}

八、Map接口的实现类:

HashMap

1.底层分析

底层结构:哈希表

jdk7:数组+链表

jdk8:数组+链表+红黑树

JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

源码分析:

jdk8: HashMap中维护了Node类型的数组table,当HashMap创建对象时,只是对loadFactor初始化为0.75;table还是保持默认值null

当第一次添加时,将初始table容量为16,临界值为12

每次添加调用putVal方法:

①先获取key的二次哈希值并进行取与运算,得出存放的位置, (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度)

②判断该存放位置上是否有元素,如果没有直接存放

如果该存放位置上已有元素,则进行继续判断:

如果和当前元素直接相等,则覆盖

如果不相等,则继续判断是否是链表结构还是树状结构,按照对应结构的判断方式判断相等

③将size更新,判断是否超过了临界值,如果超过了,则需要重新resize()进行2倍扩容,并打乱原来的顺序,重新排列

④当一个桶中的链表的节点数>=8 && 桶的总个数(table的容量)>=64时,会将链表结构变成红黑树结构

jdk7和jdk8的区别

1.jdk7:创建HashMap对象时,则初始table容量为16

jdk8:创建HashMap对象时,没有初始table,仅仅只是初始加载因子。只有当第一次添加时才会初始table容量为16.

2.jdk7:table的类型为Entry

jdk8:table的类型为Node

3.jdk7:哈希表为数组+链表,不管链表的总结点数是多少都不会变成树结构

jdk8:哈希表为数组+链表+红黑树,当链表的节点数>=8 && 桶的总个数(table的容量)>=64时,会将链表结构变成红黑树结构

【问题】HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。

取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

2.应用层面:

要求添加元素的key重写hashCode和equals方法

Map接口的实现类:Hashtable

底层结构:哈希表 ,同HashMap

Hashtable是个古老的 Map 实现类,线程安全
与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
Hashtable判断两个key相等、两个value相等的标准,与hashMap一致。

Map接口的实现类:TreeMap

底层结构:红黑树,可以实现对添加元素的key进行排序

应用:

自然排序:要求key的元素类型实现Comparable,并实现里面的compareTo方法

定制排序:要求创建TreeMap对象时,传入Comparator比较器对象,并实现里面的compare方法

Map接口的实现类的对比

HashMap和Hashtable的对比

  底层结构 版本 线程安全(同步) 允许null键null值
HashMap 哈希表 1.2 不安全 允许
Hashtable 哈希表 1.0 安全 不允许

①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。

②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。

HashMap 多线程操作导致死循环问题 主要原因在于 并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。 详情请查看:https://coolshell.cn/articles/9606.html

Map接口的实现类Properties

Properties 类是 Hashtable 的子类,该对象用于处理属性文件
由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法

Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);

九、Collections工具类的学习

  • 常见方法

reverse反转
sort排序
swap两各索引处元素的交换
shuffle随机打乱顺序
max获取最大值
min获取最小值
frequency 查找指定元素出现的次数
replaceAll替换旧值为新值
copy 复制,注意:新集合的size>旧集合的size

十、泛型

1.泛型的理解

泛型:jdk5.0出现的新特性;参数化的类型。可以将某个类型当做参数传递给类、接口或方法中

联想:

A a = new A();
class A<T>{
  T t;
}
method("john");
public void method(String s){
  //访问s
}

区别:

方法的参数:传递的是值,必须传参,只能用在方法中

泛型:传递的是类型,可以不用传参,默认为Object,可以用在方法、类、接口中

2.好处

  • 编译时检查待添加的元素类型,提高了类型的安全性
  • 减少了类型转换的次数,提高了效率

没有使用泛型:

String——>Object——>String

使用泛型:

String——>String——>String

  • 减少了编译警告

3.泛型的语法和使用 ★

语法:

类型<指定的泛型> 名 = new 类型<>();

表示形式如下:

Set<Integer> set = new HashSet<Integer>();
Set<Integer> set2 = new HashSet<>();//jdk7.0 类型推断
Set<Integer> set3 = new HashSet();//为了新老版本兼容性,不推荐使用
Set set4 = new HashSet<Integer>();//为了新老版本兼容性,不推荐使用

注意:

①泛型的类型只支持引用类型
②编译类型和运行类型的泛型必须一致

4.自定义泛型类、泛型方法、泛型接口【了解】

  • 自定义泛型类

定义语法:

class MyClass<T>{
  T name;
  publci void setName(T t){}
}

注意:里面可以定义使用泛型的属性、方法也可以定义不使用泛型的普通属性和普通方法

但不能定义使用泛型的静态方法和使用泛型的数组初始化!

什么时候确定泛型类的具体类型?
答:创建对象时

语法:

MyClass<String> m = new MyClass<>();
  • 自定义泛型接口

定义语法:

interface MyInter<T,U>{
  U method(T t);
}

什么时候确定泛型接口的具体类型?

答:被继承或实现时,可以确定,如果不确定,则默认是Object类型。如果想延续泛型,则需要将子接口或实现类设计成泛型形式!

HashMap 的长度为什么是2的幂次方

posted @ 2019-07-23 10:06  猫不夜行  阅读(857)  评论(0编辑  收藏  举报