第六节集合

文章目录

第十七章集合

1.集合概述

集合和我们之前学习的数组类似,也是用于存储元素的,也是一种容器;不同的是集合是一个可变长的容器,数组则在创建时候就分配好了大小,不可改变,此外集合的功能要比数组强大的多,底层实现也非常复杂,类型也非常多,不同类型的集合又提供不同的功能;

集合和数组的区别:

  • 数组的长度是固定的,集合的长度是可变的。
  • 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储;
  • 集合的种类非常多,不同的集合底层采用的数据结构也大不相同,因此集合的功能更加丰富;

2.集合体系

集合分为两大类,一类是单列集合;一类是双列集合,两类的底层父接口下有非常多的实现类,不同的实现类,底层所采用的数据结构和算法都是不一样的;

2.1 单例集合

单列集合的顶层父接口是java.util.Collection类,这个类中具备的方法下层接口或者类都会具备此方法;

2.2 双列集合

双列集合的顶层接口是java.util.Map类;

3.Collection 集合

Collection是单列集合的根接口,Collection 接口有 3 种子类型集合: ListSetQueue再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、ArrayBlockingQueue等;也就是说Collection中包含的方法这些类中都会具备;

3.1 常用方法

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

Collection里面可以存放什么元素呢? (泛型的概念会到后面去讲)

没有使用"泛型"之前,Collection中可以存储所有Object的直接子类

使用的’‘泛型’'之后,Collection中只能存储某个具体的类型

集合后期我们会学习"泛型"语法.目前先不用管.Collection中什么都能存,只要是Object的子类型就行.

集合中不能直接存储基本数据类型,也不能存储java对象,只能存储java对象的内存地址.

  • public boolean add(Object e)把给定的对象添加到当前集合中 。
  • public void clear() :清空集合中所有的元素。
  • public boolean remove(Object e): 把给定的对象在当前集合中删除。
  • public boolean contains(Object e): 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty(): 判断当前集合是否为空。
  • public int size(): 返回集合中元素的个数。
  • public Object[] toArray(): 把集合中的元素,存储到数组中。

示例代码:

public class CollectionDemo{
    public static void main(String[] args){
		//创建一个集合对象
        //接口是抽象的,无法实例化.
        //Collection collection=new Collection() ;
        //多态
        Collection collection=new ArrayList();
        
        /*
        1.public boolean add(Object e)把给定的对象添加到当前集合中 。
        */        
        collection.add(1000);//自动装箱
        collection.add(3.14);//自动装箱
        collection.add(new Object());
        collection.add(true);//自动装箱
        
        /*
        2.public int size() 返回集合中的元素个数
        */
        System.out.println("集合中元素的个数是:"+collection.size());//4
        
        /*
        3.public void clear() 清空集合
        */
        collection.clear();
        System.out.println("集合中元素的个数是:"+collection.size());//0
        
        // 在向集合同添加元素
        collection.add("hello");//"hello"对象的内存地址放到了集合当中
        collection.add("world");
        collection.add("浩克");
        collection.add("绿巨人");
        collection.add(1);
        
        //4.public boolean contains(Object o)判断当前集合中是否包含元素o
		boolean flag=collection.contains("绿巨人");
        System.out.println(flag);
        boolean falg2=collection.contains("绿巨人2");
        System.out.println(falg2);//false
        System.out.println("集合中元素的个数是:"+collection.size());//5
        
        //5. public bolean remove(Object o)删除当前集合中的o元素
        collection.remove(1);
        System.out.println(collection.contains(1)?"元素1未删除":"元素1删除");
        System.out.println("集合中元素的个数是:"+collection.size());//4
        
        //6.public boolean isEmpty()判断该集合中元素的个数是否为0
        System.out.println(collection.isEmpty());//false
        collection.clear();
        System.out.println(collection.isEmpty());//true(collection 中没有元素了)
        
        //添加元素
        collection.add(1);
        collection.add(2);
        collection.add(3);
        collection.add(4);
        collection.add(new Student());
        
        //7.public Object[] toArray()调用这个方法,会把集合转变为数组
		Object[] objects=collection.toArray();
        //遍历
        for (int i = 0; i < objects.length; i++) {
            System.out.println(objects[i].toString());
        }
    }
}

class Student{
    public String toString(){
        return "学生";
    }
}

3.2 Iterable接口

在源码中,可以看出Collection接口继承了Iterable接口

public interface Collection<E> extends Iterable<E> {}

**Iterable接口:**可迭代的,可遍历的,所有集合元素都是可迭代的,可遍历的,

包含一个接口方法为Iterator:集合迭代器的对象

public interface Iterable<T> {
    /**
     * Returns an iterator over elements of type {@code T}.
     * 返回T类型元素的迭代器。
     * @return an Iterator.
     * 返回:一个迭代器。
     */
    Iterator<T> iterator();
}

Iterator接口(又称为迭代器)中有三个方法为:hasNext()、next()、remove()通过这三个方法来完成迭代(遍历)

3.2.1 Iterator迭代器

在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.IteratorIterator接口也是Java集合中的一员,在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.IteratorIterator接口也是Java集合中的一员,

迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。

关系图:

3.2.2 Iterator和Iterable的区别

iterator为Java中的迭代器对象,是能够对List这样的集合进行迭代遍历的底层依赖。

iterable接口里定义了返回iterator的方法,相当于对iterator的封装,同时实现了iterable接口的类可以支持forEach循环(Jdk1.8后新增的forEach)。

Iterator的源码:

public interface Iterator<E> {
    // 判断集合中是否存在下一个对象
    boolean hasNext();
    // 返回集合中的下一个对象,并将访问指针移动一位
    E next();
    // 删除集合中调用next()方法返回的对象
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}

List中并没有实现Iterator接口,而是实现的Iterable接口,Iterable接口实际上返回的也是一个Iterator对象。

public interface Iterable<T> {
  Iterator<T> iterator();
}

为什么不直接将hasNext(),next()方法放在Iterable接口中,其他类直接实现就可以了?

有些集合类可能不止一种遍历方式,实现了Iterable的类可以再实现多个Iterator内部类,例如LinkedList中的ListItr和DescendingIterator两个内部类,就分别实现了双向遍历和逆序遍历。通过返回不同的Iterator实现不同的遍历方式,这样更加灵活。如果把两个接口合并,就没法返回不同的Iterator实现类了。
从英文单词的后缀语法上来看,(Iterable)able 表示这个 List 是支持迭代的,而 (Iterator)tor 表示这个 List 是如何迭代的。
原则上,只要一个 List 实现了 Iterable 接口,那么它就可以使用 for-each 这种方式来遍历,那具体该怎么遍历,还是要看它自己是怎么实现 Iterator 接口的。

3.2.3 常用方法(遍历)
  • public E next():返回迭代的下一个元素。
  • public boolean hasNext():如果仍有元素可以迭代,则返回 true
  • default void remove() 删除当前指针指向的元素(这个到后面的深入remove会讲)

示例代码:

注意:以下讲解的遍历/迭代方式,是所有Collection通用的一种方式.

在Map集合中不能用.在所有的Collection以及子类中使用

public class CollectionTest {
    public static void main(String[] args) {
        //创建对象
        Collection collection=new ArrayList();
        //添加元素
        collection.add("钢铁侠");
        collection.add("绿巨人");
        collection.add("蜘蛛侠");        
        //对集合Collection进行遍历/迭代
        //第一步:获取集合对象的迭代器对象Iterator
        Iterator it=collection.iterator();
        
        /*
        第二步:通过以上获取的迭代器对象开始遍历/迭代集合.
        以下两个方法是迭代器对象Iterator中的方法
        	boolean hasNext()如果元素仍有可以迭代,返回true
            Object next()返回迭代的下一个元素.
		解释:
        	it:
                其中迭代器对象it负责遍历/迭代集合当中元素的.
                注意:迭代器it最初没有指向第一个元素
            boolean hasNext():
                boolean hasNext=it.hasNext();
                这个方法返回true,表示还有元素可以迭代.这个方法返回false表示没有更多的元素可以迭代了
            Object next():
                Object obj=it.next();
                这个方法让迭代器前进一位
                并且将指向的元素返回(拿到)
        也就是
        1.一开始用 it.hasNext();返回一个boolean类型的数据 判断下一位还有没有元素了
        2.如果是true代表有元素,用it.next();取出元素,并指向下一位置.
        3.如果是false代表下一位没有元素了 循环结束
        */
        while(it.hasNext()){//这个会返回一个true/false,来判断下一个还有没有元素
            Object o=it.next();//取出元素,并指向下一个位置,取出的元素是Object类型的
            System.out.println(o);
        }
    }
}

图解:

在这里插入图片描述

3.3 foreach

foreach也称增强for,是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。

格式:

  • for(元素的数据类型  变量 : Collection集合或数组){ 
      	//写操作代码
    }
    

遍历数组:

  • public class ForEachTest01 {
        public static void main(String[] args) {
            //int类型数组
            int[] arr={1,2,3,4,5,6,7,8,9};
    
            //遍历数组(普通for循环)
            for (int i = 0; i < arr.length; i++) {
                System.out.println(arr[i]);
            }
    
            //增强for(foreach),以下是语法
            /*
            for(元素类型:数组或集合){
                System.out.println(遍历名);
            }
    
             */
            System.out.println("=========================");
            //foreach有一个缺点:没有下标.在需要使用下标的循环中,不建议使用增强for循环
            for(int data:arr){
                //data就是数组中的元素(数组中的每一个元素)
                System.out.println(data);
            }
        }
    }
    

遍历集合

  • public class ForEachTest02 {
        public static void main(String[] args) {
            List<String> stringList=new ArrayList<>();
            //添加元素
            stringList.add("hello");
            stringList.add("world");
            stringList.add("kitty");
    
            //遍历 采用迭代器的方法
            Iterator<String> strIterator=stringList.iterator();
            while (strIterator.hasNext()){
                String s=strIterator.next();
                System.out.println(s);
            }
            System.out.println("=============================================");
            //采用foreach的方法
            for(String strForEach:stringList){//因为泛型使用的String类型,所以String s
                System.out.println(strForEach);
            }
        }
    }
    

3.4 深入remove和contains方法

3.4.1 remove方法

public boolean remove(Object e): 把给定的对象在当前集合中删除。

我们指点 remove 是将指定的元素删除.那么在底层中是怎么找到指定的元素的呢?在底层是调用的equals()方法,如果参数和元素进行对比,如果相同就是指定元素进行删除

public class removeTest01 {
    public static void main(String[] args) {
        Collection collection=new ArrayList();
        //创建两个相同名称的user对象
        User u1=new User("jack");
        User u2=new User("jack");
        //添加元素
        collection.add(u1);
        //u1.equals(u2)java认为u1和u2是一样的.删除u2就是删除u1,当然这也是在User类中重写了equals方法
        collection.remove(u2);
        //由于remove底层也调用了equals方法 java认为u1和u2是一样的 就都删除了
        System.out.println(collection.size());//0

        //创建字符串对象
        String s1=new String("hello");
        String s2=new String("hello");

        collection.add(s1);
        collection.remove(s2);
        /*
        添加了s1 删除s2
        最后的长度也是0 由此可知 remove底层也调用了equals方法
        System.out.println(s1.equals(s2));true
         */
        System.out.println(collection.size());//0


    }
}

class User{
    private String name;
    public User(){

    }
    public User(String name){
        this.name=name;
    }

    //重写equals方法
    //将来调用equals方法的时候,一定是调用这个重写的equals
    //这个equals方法的比较原理是:只要姓名一样表示同一个用户
    public boolean equals(Object o){
        if(this==o)return true;
        if(o==null||!(o instanceof User))return false;
        User user=(User) o;
        return user.name.equals(this.name);
    }

}

在Collection和Iterator都有remove方法 那么这两个有什么区别呢?

public class  removeTest02 {
    public static void main(String[] args) {
        //创建集合
		Collection collection1=new ArrayList();
        //添加元素
		collection1.add(1);
        collection1.add(2);
        collection1.add(3);
        
        //迭代 Iterator的remove()方法
        Iterator iterator=collection1.iterator();
        while (iterator.hasNext()){
            Object obj=iterator.next();
            if(obj.equals(1)){
             	iterator.remove();
                continue;
            }
            System.out.println(obj);// 2 3 
        }
        System.out.println(c1.size());//2
        
        
        // 创建集合
        Collection collection2=new ArrayList();
        //添加元素
		collection2.add(1);
        collection2.add(2);
        collection2.add(3);
        
        //迭代 使用 collection2 删除
        Iterator it=collection2.iterator();
        
        while (it.hasNext()){
            Object obj=it.next();
            /删除元素之后,集合的结构发生了改变,应该重新去获取迭代器
            //但是,循环下一次的时候并没有重新获取迭代器
			//所以会出现异常:java.util.ConcurrentModificationException
            //出现异常根本原因:集合中元素删除了,但没有更新迭代器(迭代器不知道集合变化了)
            //c2.remove(obj2); 通过集合去删除元素,没有通知迭代器(导致迭代器的快照和原集合状态不同)
            //使用迭代器来删除可以吗?
            //迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中的元素)
            collection.remove(1);
            System.out.println(obj);
        }
        
        
        
    }
3.4.2 contains()方法

public boolean contains(Object e): 判断当前集合中是否包含给定的对象。

contains方法是用来判断集合中是否包含某个元素的方法,那么它底层是怎么判断集合中是否包含某个元素呢?

调用了equals方法进行比对,equals方法返回true,就表示包含这个元素

public class containsTest02 {
    public static void main(String[] args) {
        //创建集合对象
        Collection collection=new ArrayList();

        //创建User对象
        User u1=new User("jack");
        User u2=new User("jack");
        //加入到集合当中
        collection.add(u1);
        //没有重写equals之前:这个结果是false
        //这里就是u1.equals(u2)---->false 所以要重写equals方法,否则调用的Object的equals方法 比较的是内存地址
        //System.out.println(c.contains(u2));//false

        //重写equals方法之后,比较的时候会比较name
        System.out.println(collection.contains(u2));//true

        Integer integer1=new Integer(1000);
        collection.add(integer1);

        Integer integer2=new Integer(1000);
        //Integer已经重写了equals方法
        System.out.println(collection.contains(integer2));//true

    }
}
class User{
    private String name;
    public User(){

    }
    public User(String name){
        this.name=name;
    }
    public boolean equals(Object o){
        if(this==o)return true;
        if(o==null||!(o instanceof User))return false;
        User user=(User) o;
        return user.name.equals(this.name);
    }

}

结论:一定要重写equals方法

5.单列集合List

5.1 List集合

5.1.1 List接口概述

List接口是单列集合的一个重要分支,下面主要有两个实现 ArrayList LinkedList,List类型接口的特点是存储的元素是有序的,即存放进去是什么顺序,取出来还是什么顺序,也就是基于线性存储;因此在List接口中提供有大量根据索引来操作元素的方法;

继承结构:

5.1.2 List接口的特点

有序可重复的

  • 有序是指存进去是什么顺序,那么取出来也是这个顺序
  • 有序是指,List集合中每个元素都是有下标的.下标从0开始,一1递增(所以可以采用下标得方式进行遍历)
  • 可重复的是指,存放的元素可以相同(存储相同的元素)
5.1.3 List接口常用的方法

List是Collection的子接口,因此Collection中存在的方法List都存在;因为List的特点是有序,因此除Collection接口提供的方法之外List还添加了许多与顺序相关的方法,例如指定顺序插入,指定顺序删除,指定顺序替换等;

  • public boolean add(int index, Object element):将指定的元素,添加到该集合中的指定位置上。
  • public Object get(int index):返回集合中指定位置的元素。
  • public Object remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public indexOf(Object element)返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。
  • public lastIndexOf(Object element)返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。
  • public Object set(int index,Object element)用指定的元素,替换此列表中指定位置的元素。
    • index-要替换元素的索引
    • element-要存储在指定位置的元素
  • List subList(int fromIndex,int toIndex)从fromIndex下标截取到toIndex下标。
  • public int size() 返回元素个数

测试:

public class ListTest {
    public static void main(String[] args) {
        //创建List对象
        List list=new ArrayList();
        
        //添加元素
        list.add("A");//默认都是向集合末尾添加元素
        list.add("B");
        list.add("C");
        list.add("C");
        list.add("D");
        
        //1. 将指定的元素插入列表中的指定位置(第一个参数是下标)add(int index, Object element)
        list.add(1,"KING");
        // 迭代
        Iterator it=list.iterator();
        while(it.hasNext()){
            Object o=it.next();
			System.out.println(object);

        }
		System.out.println("-----------------------");
        
        //2.根据指定下标获取元素 get(int index)
        Object o=list.get(0);
        System.out.println(o);
        System.out.println("-----------------------");
        
        // 3. 因为有下标,所以List集合有自己比较特殊的遍历方式
        //通过下标遍历.[List集合特有的方式,Set没有]
        for (int i = 0; i < list.size(); i++) {
            Object obj=list.get(i);
            System.out.println(obj);
        }
        System.out.println("-----------------------");
        
        //4.根据索引删除集合中的元素 remove(int index)
        Object remove = list.remove(0);
        System.out.println(remove);
        System.out.println(list.size());//5
        System.out.println("-----------------------");
        
        //5. 获取指定对象第一次出现处的索引 indexOf(Object element)
        System.out.println(list.indexOf("KING"));//0
        System.out.println("-----------------------");
        
        // 6. 获取指定对象最后一次出现的索引 lastIndexOf(Object element)
        System.out.println(list.indexOf("KING"));//0
        System.out.println("-----------------------");
        
        // 7.用指定的元素,替换此列表中指定位置的元素。set(int index,Object element)
        list.set(2,"jack");
        //遍历集合
        for (int i = 0; i < list.size(); i++) {
            Object obj=list.get(i);
            System.out.println(obj);
        }
        
        // 8.从fromIndex下标截取到toIndex下标。
        List subList = list.subList(3, 5);
        for (int i = 0; i < subList.size(); i++) {
            Object o = subList.get(i);
            System.out.println(o);
        }
        
        

        
    }
}
5.1.3 ListIterator

List接口还提供了一个针对于List集合迭代的迭代器ListIterator,该迭代器与我们之前学过的Iterator迭代器不同,ListIterator允许迭代器往上或者往下迭代,而Iterator迭代器只允许指针一直往下移动;

ListIterator listIterator():获取List集合的迭代器

ListIterator方法:

  • public boolean hasNext():是否还存在下一个元素;
  • public boolean hasPrevious():是否还存在上一个元素
  • Object next():获取下一个元素,如果指针目前是向上运行,第一次调用时会调转指向返回指针当前指向的元素;(也就是,调转方向 返回当前指针指向的元素)
  • Object previous():获取下一个元素,如果指针目前是向下运行,第一次调用时会调转指向返回指针当前指向的元素;
  • void add(Object e):添加一个元素到当前指针指向的后面一位(后一位是指当前的指针是向哪里移动)
  • void remove():移除当前指针指向的元素;

示例代码:

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class Demo01 {
    public static void main(String[] args) {
        List list=new ArrayList();

        list.add("jack");
        list.add("hello");
        list.add("king");

        ListIterator listIterator = list.listIterator();

        while (listIterator.hasNext()) {
            Object next = listIterator.next();
            System.out.println(next);
        }

        System.out.println("--------------------------------------");
        while (listIterator.hasPrevious()) {
            Object previous = listIterator.previous();
            System.out.println(previous);
        }

        System.out.println("--------------------------------------");
        System.out.println(listIterator.next());//jack
        System.out.println(listIterator.next());//hello
        System.out.println(listIterator.previous());//hello 获取当前指针指向的元素
        System.out.println(listIterator.previous());//jack 获取上一个元素 并且指针上移 指向了jack
        listIterator.remove(); // 移除了 jack
        System.out.println(list);

        listIterator.add("addElement"); // 当前指针的移动是向上移动的 所以添加到指针的上一个
        System.out.println(list);

    }
}

5.2 ArrayList

5.2.1 ArrayList集合概述

ArrayList底层采用数组这种数据结构来实现的;因此ArrayList元素查询速度快、增删相对较慢;我们在开发过程中,数据一般都是"读多写少",因此ArrayList非常常用;

ArrayList继承结构图:

5.2.2 ArrayList集合常用的方法

构造方法:

public ArrayList() :构造一个初始容量为10的空列表。

public ArrayList(int initialCapacity):构造具有指定初始容量的空列表。

public ArrayList(Collection c):构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。

示例代码:

public class ArrayListTest {
    public static void main(String[] args) {

        // 创建一个默认容量大小的集合
        ArrayList<String> arr1 = new ArrayList<>();
        arr1.add("武汉");
        arr1.add("长沙");
        arr1.add("南昌");

        // 创建一个指定容量大小的集合
        ArrayList<String> arr2 = new ArrayList<>(20);

        // 基于一个旧的集合创建一个新的集合
        ArrayList<String> arr3 = new ArrayList<>(arr1);
        arr3.add("郑州");
        arr3.add("合肥");
    
        System.out.println(arr3);           // [武汉, 长沙, 南昌, 郑州, 合肥]
    }
}

5.2.3 ArrayList集合注意点

初始化容量:

  • ArrayList默认初始化容量为10(底层先创建一个长度为0的数组,当添加第一个元素的时候,初始化容量10.)
  • 增长到原容量的1.5倍(到了最大容量以后进行扩容)

ArrayList集合的底层

  • ArrayList集合底层是Object类型的数组

ArrayList集合底层是数组,怎么进行优化?

  • 尽可能少的扩容.因为数组扩容效率比较低,建议在使用ArrayList集合的时候预估计元素的个数,给定一个初始化容量

ArrayList集合优点

  • 检索效率比较高
  • 因为每个元素占用空间大小相同,内存地址是连续的,知道首元素内存地址,然后知道下标,通过数学表达式计算元素的内存地址,索引检索效率最高

ArrayList集合缺点

  • 随机增删元素效率比较低( 向数组末尾添加元素,效率很高,不受影响)
  • 另外数组无法存储大数据量(很难找到一块非常巨大的连续的内存空间)

ArrayList在所有集合中,用的最多

  • 因为往数组末尾添加元素,效率不受影响.另外,我们检索/查找某个元素的操作比较多.

ArrayList是非线程安全的

5.3 LinkedList

5.3.1 LinkedList概述

LinkedList即是List派系下的实现类,也是Queue集合派系下的实现类,因此LinkedList除了具备List体系的方法外,还提供有Queue集合体系的方法,LinkedList底层采用链表这种数据结构来实现的,因此增删速度较快,查询速度较慢;

继承结构图

Tips:LinkedList底层是一个双向链表,对比与单向链表多了一个指针指向上一个元素;

5.3.2 LinkedList常用的方法
5.3.2.1 List接口常用方法

LinkedList属于List接口下的一个实现类,因此List接口中的那些有关于索引的操作方法,LinkedList都具备;

但需要注意的是,虽然LinkedList提供索引操作的相关方法,但LinkedList底层并不是采用数组实现,而是采用链表来实现,链表本身并没有索引而言,换句话来说,LinkedList并不能通过索引去查询一次就返回所需要的元素,而是采用一种算法(二分查找法),根据索引去挨个遍历查询整个链表查询所需要的元素,这样下来,LinkedList的查询效率将远不如ArrayList;

5.3.2.2 Queue接口常用的方法

LinkedList的强项并不在于元素的查询,而是元素的增删,而我们在增删过程中,最好操作链表的头部或者尾部,因为这样不需要去浪费额外的时间来查询需要操作的元素位置,在Queue接口中定义有很多关于链表(队列)头和尾部的操作;

  • public void addFirst(E e):将指定元素插入此列表的开头。
  • public void addLast(E e):将指定元素添加到此列表的结尾。
  • public E getFirst():返回此列表的第一个元素。
  • public E getLast():返回此列表的最后一个元素。
  • public E removeFirst():移除并返回此列表的第一个元素。
  • public E removeLast():移除并返回此列表的最后一个元素。
  • public boolean isEmpty():如果列表不包含元素,则返回true。

示例代码:

public class Demo03 {
    public static void main(String[] args) {
        // 创建LinkedList集合
        LinkedList linkedList=new LinkedList();
        //向头添加元素
        linkedList.addFirst("hello");
        linkedList.addFirst("linkedList");
        linkedList.addFirst("java");
        linkedList.addFirst("queue");

        // 输出
        System.out.println(linkedList);

        // 获取头元素
        System.out.println(linkedList.getFirst());

        //获取未元素
        System.out.println(linkedList.getLast());

        // 向末尾添加 元素
        linkedList.addLast("list");

        // 移除头元素
        System.out.println(linkedList.removeFirst());

        // 移除未元素
        System.out.println(linkedList.removeLast());

        System.out.println(linkedList);


    }
}

5.3.3 LinkedList优缺点

优点:

由于链表上的元素在空间存储上内存地址不连续,所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高.在以后的开发中,如果遇到随机增删集合中元素的业务比较多时,建议使用LinkedList

缺点:

不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头结点开始遍历,直到找到为止所以LinkedList集合检索/查找的效率较低

LinkedList和ArrayList对比

ArrayList:把检索发货到极致 (末尾添加元素效率比较高

LinkedList:把随机增删发挥到极致

加元素都是往末尾添加,所以ArrayList用的比LinkedList多

5.3 Vector

5.3.1 Vector 概述

Vector也是List集合的一个分支,是JDK1.0就推出的集合,也我们也称其为一代集合(ArrayList等集合则称为二代集合),Vector集合底层也是采用数组来实现元素的存储,因此Vector集合的特点也是查询快增删慢;

Vector集合实现与List接口,因此Collection和List接口所具备的方法,Vector都具备,并且Vector集合底层也是采用数组这种数据结构来存储元素的排列;

Vector集合的继承体系:

5.3.2 Vector集合与的主要ArrayList的区别
  • Vector集合在扩容是默认是扩容至原来的2倍,ArrayList则是1.5倍,关于容量都是初始化为10
  • Vector集合是线程安全集合(所有的方法都有synchronized关键字修饰的,所以是线程安全的),他所有的方法之间是线程同步的,这意味则每次调用Vector的方法时都需要先获取锁,方法结束后也要释放锁,造成不必要的性能开销;ArrayList是线程不安全集合,调用ArrayList集合中的方法不需要先获取锁,调用完毕后也不需要释放锁;因此ArrayList性能比Vector要高,但相对比与Vector集合,ArrayList的安全性较低(可能产生并发问题);
    我们可以用别的方法解决线程安全的问题,所有Vector基本不用
  • Vecotr集合支持Enumeration,也支持ListIterator迭代器,ArrayList支持ListIterator但不支持Enumeration。在使用Enumeration迭代元素时允许集合对元素进行增删
5.3.3 Vector的使用
public class VectorDemo {

    public static void main(String[] args) throws IOException {

        Vector<String> vector = new Vector();
        vector.add("中国");
        vector.add("美国");
        vector.add("英国");

        // addElement是Vector集合特有的功能,和add方法功能一致
        vector.addElement("法国");            
        vector.addElement("俄国");

        // 使用foreach迭代
        for (String country : vector) {
            System.out.println(country);
        }
        System.out.println("-------------");

        // 使用迭代器迭代
        ListIterator<String> iterator = vector.listIterator();

        while (iterator.hasNext()){
            String element = iterator.next();
            System.out.println(element);
        }
    }
}

5.3.4 Enumeration迭代

Vector集合支持Enumeration迭代元素,而ArrayList集合不支持,但两者都支持ListIterator迭代,使用ListIterator迭代元素时,可以使用ListIterator对象对集合进行增删操作;

但是使用Enumeration迭代元素时,可以直接操作集合的元素;

public class EnumerationDemo {

    public static void main(String[] args) {
        Vector<String> vector = new Vector<>();
        vector.add("1");
        vector.add("2");

        Enumeration<String> elements = vector.elements();

        while (elements.hasMoreElements()) {
            String str = elements.nextElement();

            // 使用Vector的Enumeration迭代时,可以对集合中的元素进行增删操作
            if (str.equals("2")) {
//                vector.remove(0);
                vector.add("3");
            }
        }

        System.out.println(vector);
    }
}

5.4 Stack

5.4.1 Stack简介

Stack是栈结构的集合,因此具有数据结构中栈的一般特性(后进先出);

在栈中,元素的添加和删除操作只能在容器的一端进行,即栈顶。元素的添加和删除遵循“后进先出”(LIFO)的原则,最后添加的元素总是最先出栈,栈对元素的访问加以限制,仅仅提供对栈顶元素的访问操作

栈的操作示意图:

栈做为一种线性表,其实现方式主要有两种:数组和链表;

继承结构图:

Stack继承与Vector集合,说明Stack也是线程安全类,并且Stack底层也是采用数组来实现栈这种数据结构;

5.4.2 Stack集合的使用
数组操作

Stack继承了Vector,因此Vecotr中的方法Stack也同样具备,包括Collection,List等接口中的方法;并且Stack并没有对Vector的方法进行重写改造(原封不动的继承下来),而是在Vector的基础上添加了许多栈有关的操作,例如压栈(push),弹栈(pop)等;我们也可以完全把Stack当作一个Vector集合用;

Stack使用Vector相关方法:

public class StackDemo {

    public static void main(String[] args) {

        Stack<String> mountains = new Stack();
        mountains.add("泰山");
        mountains.add("华山");
        mountains.add("衡山");
        mountains.add("恒山");
        mountains.add("嵩山");

        // 迭代元素
        for (String mountain : mountains) {
            System.out.println(mountain);
        }
        System.out.println("-------------------");
        System.out.println(mountains);              // [泰山, 华山, 衡山, 恒山, 嵩山]
        System.out.println("------------------");

        // 根据索引获取元素
        System.out.println(mountains.get(0));       // 泰山
        System.out.println(mountains.get(2));       // 衡山
        System.out.println(mountains.get(4));       // 嵩山
    }
}

栈操作

Stack底层本质上还是一个数组,其特性和Vector一模一样,只不过Stack集合新增了一些关于栈的操作,例压栈,弹栈等;因此对外界来说,Stack可以看作是一个栈;

  • public E push(E item):添加一个元素到栈顶(压栈);
  • public synchronized E pop():从栈顶移除一个元素并返回(弹栈);
  • public synchronized E peek():获取栈顶的一个元素返回,该元素不会从栈顶移除;
public class StackDemo {

    public static void main(String[] args) {
        Stack<String> mountains = new Stack();
        mountains.push("庐山");
        mountains.push("黄山");
        mountains.push("雁荡山");

        // 从栈顶获取一个元素,并不移除
        System.out.println(mountains.peek());
    }

    public static void t1(){
        Stack<String> mountains = new Stack();

        // 将庐山添加到栈顶        ["庐山"]
        mountains.push("庐山");

        // 将黄山添加到栈顶        ["黄山","庐山"]
        mountains.push("黄山");

        // 将雁荡山添加到栈顶       ["雁荡山","黄山","庐山"]
        mountains.push("雁荡山");

        // 如果直接打印,还是打印数组中元素的排列顺序,而不是栈的排列顺序
        System.out.println(mountains);          // [庐山, 黄山, 雁荡山]

        // 从栈顶移除一个元素
        System.out.println(mountains.pop());        // 雁荡山
        System.out.println(mountains.pop());        // 黄山
        System.out.println(mountains.pop());        // 庐山

        System.out.println(mountains);              // 元素已经被全部移除,因此集合为空: []
    }
}

6. 单例集合 Set

6.1 Set集合概述

Set接口和List接口一样,继承与Collection接口,也是一个单列集合;Set集合中的方法和Collection基本一致;并没有对Collection接口进行功能上的扩充,只是底层实现的方式不同了(采用的数据结构不一样);

Set集合的特点:无序不可重复的

  • 无序表示的是存进去的这个顺序,取出来就不一定是这个顺序了
  • 另外Set集合中元素没有下标,Set集合中的元素是不能重复的

6.2 HashSet集合

6.2.1 HashSet数据结构
6.2.1.1 HashSet简介

HashSet是Set接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。

HashSet底层数据结构在JDK8做了一次重大升级,JDK8之前采用的是Hsah表,也就是数组+链表来实现;到了JDK8之后采用数组+链表+红黑树来实现;

关于红黑树我们暂时理解为红黑树就是一颗平衡的二叉树(即使他不是绝对平衡);

HashSet简单使用

public class HashSetDemo01 {
    public static void main(String[] args) {
        HashSet set=new HashSet();
        set.add("河北");
        set.add("河南");
        set.add("河南");
        System.out.println(set);// HashSet带有去重特点,"河南"已经存储过了,不会再存储

    }
}

HashSet判断两个元素是否重复的依据是什么呢?下面案例代码HashSet将会存储几个元素呢?

import java.util.HashSet;

public class HashSetDemo02 {
    public static void main(String[] args) {
        HashSet set=new HashSet();

        set.add(new String("河北"));
        set.add("河北");
        set.add("湖南");
        set.add("湖南");

        System.out.println(set.size());// 2
        System.out.println(set);//[河北, 湖南]

        HashSet hashSet=new HashSet();

        hashSet.add(new A());
        hashSet.add(new A());
        System.out.println(hashSet.size());//2

    }
}
class A{}

HashSet是怎么样去重的呢?


6.2.1.2 Hash冲突1

我们知道hash表数据结构的特点是:根据元素的hash值来对数组的长度取余,最终计算出元素所要存储的位置;Object类中有一个特殊的方法hashCode(),该方法被native关键字修饰(不是采用Java语言实现);

public native int hashCode():获取该对象的hash值,默认情况下根据该对象的内存地址值来计算;

默认情况下,对象的hash值是根据对象的内存地址值来计算;但是这种hash算法并不是特别完美,有时候不同的两个对象(内存地址值不一致)计算出来的hash值可能会一样,我们把这种情况称为hash冲突。尽管这种情况非常少,但依旧存在;

就比如String对hashCode方法的实现:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {// 这里的Value是 private final byte[] value;
        char val[] = value; 

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i]; 
        }
        hash = h;
    }
    return h;
}

String默认情况下是获取每一个字符的ASCII码来参与hashCode的计算.在上面算法中,ABBA得出的hashCode是不一致的;有效的解决了一定情况下的hash冲突问题,但是hash算法不能保证在所有情况下hash都能唯一;

例如AaBB的hashCode却是一样的,这种造成了Hash冲突问题;

Aa:
h = 31*0+65
h = 31*65+97
h = 2112
BB:
h = 31*0+66
H = 31*66+66
h = 2112

通过String类对hashCode的实现可以看出来:hash算法并不能绝对的保证两个不同的对象算出来的hash值不一样,当hash冲突时,HashSet将会调用当前对象的equlas方法来比较两个对象是否一致,如果一致则不存储该元素;


6.2.1.3 Hash冲突2

假设数组的长度为9,当元素1的hash值为6,元素2的hash值为15,计算的数组槽位都是6,同样的,那么这个时候也会触发hash冲突问题;(这里知道,存放的位置是用hash值对数组长度取余算出来的)

Integer对hashCode的实现

@Override
public int hashCode() {
    return Integer.hashCode(value);
}

Integer.hashCode(value);

public static int hashCode(int value) {
        return value;
}

通过源代码就可以看出Integer的hashCode就是数值的本身

示例代码:

public class IntegerHashCode {
    public static void main(String[] args) {
        System.out.println(new Integer(15).hashCode());         // 15
        System.out.println(new Integer(6).hashCode());          // 6
    }
}

HashSet存储时的Hash冲突情况(假设散列表中的数组长度为9):

发生上面这种Hash冲突时HashSet采用拉链法,将新增的元素添加到上一个元素之后形成链表;

需要注意的是:

  • hashCode一致的两个对象不能说明两个对象一定相同,因为可能会造成hash冲突(例如上面的Aa和BB)
  • 但是如果hashCode不同,则一定能说明这两个对象肯定不同,因为同一个对象计算的hashCode永远一样;

因此在hash冲突的第二种情况下,并不需要调用equals来去重;而是采用拉链法直接将新添加的元素链在后面形成链表;


6.2.1.4 HashSet底层存储结构及扩容

我们前面了解过HashSet底层采用的是散列表这种数据结构(数组+链表),并且在JDK1.8对传统的散列表进行了优化,增加了红黑树来优化链表查询慢的情况;

在默认情况下,HashSet中散列表的数组长度为16,并通过负载因子来控制数组的长度;HashSet中负载因子默认为0.75;如果默认情况下,扩容是之前的两倍

负载因子=HashSet中存储的元素/数组的长度

如果,元素达到了默认加载因子*数组长度,就会自动扩容.扩容是之前的两倍

由此可以得出当HashSet中的元素个数为12个时(16*0.75=12),将进行数组的扩容,默认情况下将会扩容到原来的2倍;数组长度将会变为32,由公式可以推算出,下一次HashSet数组扩容时元素个数将为32*0.75=24,以此类推…

负载因子是控制数组扩容的一个重要参数;并且HashSet允许我们在创建时指定负载因子和数值的默认容量大小;


6.2.1.5 HashSet底层数据结构示意图

JDK8之前采用的是数据+链表的方式

当存储的元素越来越多,hash也越来越多时,势必造成链表长度非常长,查找元素时性能会变得很低;

在JDK8中当链表长度到达指定阈值8,并且数组容量达到了64时,将链表转换为红黑树,这样大大降低了查询的时间;


6.2.2 HashSet去重的原理

前面在分析hash冲突时就得出了结论:HashSet保证元素唯一性的方式依赖于:hashCodeequals方法。

  • HashSet在存储元素的时候,都会调用hashCode()方法计算该对象的hash值.
  • 如果两个元素hash值一样的情况下,那么会调用equals方法对冲突的元素进行对比
  • 如果equals方法返回true,说明两个对象是一致的,HashSet并不会存储该对象,反之则存储该元素;

不同对象的hash值计算出来的结果是不一样的,但还是有某些情况下(Ab和BB),不同一个对象的hash值计算成了同一个,这种情况我们称为hash冲突;当hash冲突时,HashSet会调用equals方法进行对比,equals对比的规则可以自己订.

另外,Java中的HashSet还进行了优化,如果两个字符串都是存储在常量池,那么直接在常量池中进行判断,不需要调用equals来判断是否重复

6.2.3 测试
6.2.3.1 Hash冲突一

HashSet的去重原理是依靠对象的hashCode和equals方法来决定是否要存储这个对象的;如果不同的两个对象其hashCode是不同的,即使hash冲突了,equals也不可能相同(默认情况下,Object中的equals比较是两个对象的内存地址值);

但是在实际开发中,地址值并不是我们用来判断两个对象是否相等的依据,我们的习惯是,对象中的属性相等即判断两个对象是同一个对象;

定义一个Person类

  • package Test.HashSetTest;
    
    import java.util.Objects;
    
    public class Person {
    
        private  String name;
        private  int age;
    
    
        public Person() {
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public boolean equals(Object o) {
            // 在此测试 equals方法是否执行了
            System.out.println("equals方法执行了");
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person person = (Person) o;
            return age == person.age && Objects.equals(name, person.name);
        }
    
        @Override
        public int hashCode() {
            // 在此测试 hashCode方法是否执行了
            System.out.println("hashCode方法执行了");
            return Objects.hash(name, age);
        }
    }
    
    

定义测试类

  • package Test.HashSetTest;
    
    import java.util.HashSet;
    
    public class HashSetDemo {
        public static void main(String[] args) {
    
            HashSet hashSet=new HashSet();
    
            hashSet.add(new Person("jack",20));
            hashSet.add(new Person("jack",20));
    
            System.out.println(hashSet.size());
    
        }
    }
    
    

测试结果

  • hashCode方法执行了
    hashCode方法执行了
    equals方法执行了
    1
    

代码分析:

  • 存储第一个Person对象时,调用这个Person对象的hashCode方法得出:1(hash值),并将其存储起来
  • 存储第二个Person对象时,调用这个Person对象的hashCode方法得出:1,发现集合中已经有了hashCode为1的对象,因此调用Person对象的equals方法,将冲突了的Person对象传入(第一次添加的那个Person对象),然后equals方法返回的是true,因此不存这个Person对象(第二个Person对象),最终persons.size()为1;

测试类修改:

  • package Test.HashSetTest;
    
    import java.util.HashSet;
    
    public class HashSetDemo {
        public static void main(String[] args) {
    
            HashSet hashSet=new HashSet();
    
            hashSet.add(new Person("jack",20));
            hashSet.add(new Person("jack",18));
    
            System.out.println(hashSet.size());
    
        }
    }
    
    

测试结果:

  • hashCode方法执行了
    hashCode方法执行了
    2
    

分析:

  • 存储第一个Person对象时,调用这个Person对象的hashCode方法得出:1(hash值),并将其存储起来
  • 存储第二个Person对象时,调用这个Person对象的hashCode方法得出:2.发现两个hashCode并不相等直接存储起来
6.2.3.2 Hash冲突二

当hashCode不一样,但是%数组的长度得到的槽位是一样时,也会产生hash冲突,但是这种hash冲突并不需要调用equals来判断,而是直接使用拉链法拼接在上一个元素的后面形成链表;

数值类

  • package Test.HashSetTest;
    
    import java.util.Objects;
    
    public class IntTest {
        private Integer integer;
    
        public IntTest() {
        }
    
        public IntTest(Integer integer) {
            this.integer = integer;
        }
    
        public Integer getInteger() {
            return integer;
        }
    
        public void setInteger(Integer integer) {
            this.integer = integer;
        }
    
        @Override
        public boolean equals(Object o) {
            System.out.println("equals方法执行了");
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            IntTest intTest = (IntTest) o;
            return Objects.equals(integer, intTest.integer);
        }
    
        @Override
        public int hashCode() {
            System.out.println("hashCode方法执行了");
            return Objects.hash(integer);
        }
    }
    
    

测试类:

  • package Test.HashSetTest;
    
    import java.util.HashSet;
    
    public class HashSetDemo02 {
        public static void main(String[] args) {
            HashSet hashSet=new HashSet();
            IntTest intTest1=new IntTest(1);
            IntTest intTest2=new IntTest(17);
    
            hashSet.add(intTest1);
            hashSet.add(intTest2);
            
            System.out.println(hashSet.size());
            
        }
    }
    
    

测试结果

  • hashCode方法执行了
    hashCode方法执行了
    2
    
6.2.4 HashSet特点总结
  • 存取无序,元素唯一,先比较hashCode,当hash冲突时再比较equals,equals返回true则不存;
    一定要同时重写equals()方法和hashCode()方法
  • 底层采用Hash表数据结构,当数组长度大于等于64并且链表长度大于等于8时,链表将会转换为红黑树,当长度降到6时将会重新转换为链表;
  • HashSet默认数组长度为16,默认的负载因子为0.75;
  • 每次数组扩容时,默认扩容到原来的2倍;

6.3 LinkendHashSet 集合

6.3.1 LinkedHashSet 概述

LinkedHashSet继承与HashSet,和HashSet一样,同样是根据元素的hashCode值来决定元素的存储位置,其底层和HashSet所采用的数据结构是一致的;

与HashSet不同的是,**LinkedHsahSet在HashSet之上底层新增了一个双向链表来保存节点的访问顺序,因此LinkedHashSet存储的元素是有序的;**当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微弱于HashSet。

特点:存取有序且去重;

6.4 TreeSet集合

在讲TreeSet的时候 在上述集合的继承结构上可以看到TreeSet实现了SortedSet接口

SortedSet

  • 由于继承了Set集合,所以它的特点也是无序不可重复的
  • 但是放在SortedSet集合中的元素可以自动排序
  • 称为可排序集合,放在该集合中可按照从小到大的顺序排序
6.4.1 TreeSet集合特点
  • TreeSet每存储一个元素都会将该元素提升为Comparable类型,如果元素未实现Comparable接口,则抛出ClassCastException异常
  • 存储的数据是无序的,即存取顺序不一致,但TreeSet提供排序功能;
  • 存储的元素不再是唯一,具体结果根据compareTo方法来决定;
6.4.2 Comparable接口

Comparable接口是一个比较器,通过其compareTo方法进行两个对象的比较,具体比较的内容、规则等可以由开发人员来决定,compare方法的返回值有3类;

TreeSet底层依赖红黑树,TreeSet得到CompareTo方法三类不同的值的含义如下(右树大,左数小):

  • 正数:返回正数代表存储在树的右边
  • 负数:存储在树的左边
  • 0:不存储这个元素
public interface Comparable<T> {
    public int compareTo(T o);
}
6.4.3 TreeSet的使用(比较器的写法)
6.4.3.1 String类

TreeSet存储的元素默认按照字典顺序进行排序;

public class TreeDemo01 {

    public static void main(String[] args) {
        Set set=new TreeSet();

        set.add("b");
        set.add("a");
        set.add("c");
        set.add("d");
        set.add("e");

        // 默认按照字典顺序进行排序
        System.out.println(set);            // [a, b, c, d, e] 
    }
}

我们看String的源代码可以看得到,String类也重写了Compable接口 定义了比较强的规则

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {}

6.4.3.2 没有写比较器的类

在上述中,我们说,要在TreeSet中存储数据.那么该类必须重写Comparable接口

定义Person类

  • public class Person{
        int age;
    
        public Person() {
        }
    
        public Person(int age) {
            this.age = age;
        }
        //重写toString方法
        public String toString(){
            return "您的年龄为"+this.age;
        }
    
    
    }
    
    

测试类:

  • public class TreeSetTest01 {
        public static void main(String[] args) {
            Person p1=new Person(32);
            Person p2=new Person(20);
            Person p3=new Person(30);
            Person p4=new Person(45);
    
            //创建TreeSet集合
            TreeSet<Person> peoples=new TreeSet<>();
            //添加元素
            peoples.add(p1);
            peoples.add(p2);
            peoples.add(p3);
            peoples.add(p4);
            for(Person p:peoples){//出现异常
                System.out.println(p);
            }
    
        }
    }
    

运行结果:

分析:

  • 对于自定义类来说,必须指定排序的规则,即实现Comparable接口.编写比较器的规则.才可以存放到TreeSet中
  • 如果没有实现Comparable接口 则会报java.lang.ClassCastException: 异常(运行时异常)
6.4.3.3 编写一个比较规则

Person类:

  • class Customer implements Comparable<Customer>{
        int age;
    
        public Customer() {
        }
    
        public Customer(int age) {
            this.age = age;
        }
        //重写toString方法
        public String toString(){
            return "您的年龄为"+this.age;
        }
    
        //需要在这个方法中编写比较的逻辑,或者说比较的规则,按照什么进行比较!
        //拿着参数k和集合中的每一个元素进行比较,返回值可能是>0 <0 =0
        //比较规则最终还是由程序员指定的:例如按照年龄升序.或者按照年龄降序
        public int compareTo(Customer c){
            //这个是升序  小的放左边 大的放右边
            //return this.age-c.age;
    
            //降序
            return c.age-this.age;
    
        }
    
    }
    

测试类:

  • public class TreeSetTest02 {
        public static void main(String[] args) {
            //创建TreeSet集合
            TreeSet<Customer> customers=new TreeSet<>();
            //添加元素
            customers.add(new Customer(32));
            customers.add(new Customer(20));
            customers.add(new Customer(30));
            customers.add(new Customer(45));
            for(Customer p:customers){
                System.out.println(p);
            }
        }
    }
    

运行结果:

  • 您的年龄为45
    您的年龄为32
    您的年龄为30
    您的年龄为20
    
6.4.3.4 编写两个比较规则

定义Vip类

  • class Vip implements Comparable<Vip>{
        String name;
        int age;
    
        public Vip() {
        }
    
        public Vip(int age, String name) {
            this.name = name;
            this.age = age;
        }
        public String toString(){
            return "name:"+this.name+",age:"+this.age;
        }
    
        /*
        compareTo方法的返回值很重要
            返回0表示相同,value会覆盖
            返回>0,会继续右子树上找
            返回<0,会继续左子树上找
        编写的规则, 按照年龄按照升序排列.年龄相同按照名字的升序排列    
     */
        public int compareTo(Vip vip){
            System.out.println("this:"+this+"-------------"+"传递来的对象"+vip);
            if (this.age==vip.age){
                //年龄相同时按照名字排序
                //姓名是String类型,可以直接比.调用compareTo来完成比较
                return this.name.compareTo(vip.name);
            }else {
                //年龄不一样按照年龄升序排
                return this.age- vip.age;
            }
        }
    }
    
    

测试类:

  • public class TreeSetTest03 {
        public static void main(String[] args) {
            TreeSet<Vip> vips=new TreeSet<>();
            vips.add(new Vip(20,"zhangsan"));
            vips.add(new Vip(23,"zhaoliu"));
            vips.add(new Vip(20,"liwu"));
            vips.add(new Vip(18,"king"));
            vips.add(new Vip(17,"soft"));
        }
    }
    

输出结果:

  • this:name:zhangsan,age:20-------------传递来的对象name:zhangsan,age:20
    this:name:zhaoliu,age:23-------------传递来的对象name:zhangsan,age:20
    this:name:liwu,age:20-------------传递来的对象name:zhangsan,age:20
    this:name:king,age:18-------------传递来的对象name:zhangsan,age:20
    this:name:king,age:18-------------传递来的对象name:liwu,age:20
    this:name:soft,age:17-------------传递来的对象name:zhangsan,age:20
    this:name:soft,age:17-------------传递来的对象name:liwu,age:20
    this:name:soft,age:17-------------传递来的对象name:king,age:18
    
    
6.4.3.5 比较的原理

TreeSet底层采用红黑树算法来存储元素的,根据compareTo方法的返回值来确定存储在左子树还是右子树;

举例:

Person类

  • package Test.TreeTest;
    
    import java.util.Objects;
    
    public class Person implements Comparable{
        private String name;
        private int age;
    
        public Person() {
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person person = (Person) o;
            return age == person.age && Objects.equals(name, person.name);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
    
        @Override
        public int compareTo(Object o) {
            System.out.println("this: " + this + "-----------" + "传递进来的对象: " + o);
            Person person=(Person) o;
            return this.age-person.age;
    
        }
    }
    
    

测试类:

  • package Test.TreeTest;
    
    import java.util.TreeSet;
    
    public class TreeTest {
        public static void main(String[] args) {
            TreeSet persons=new TreeSet();
            persons.add(new Person("小灰",20));
            persons.add(new Person("小蓝",18));
            persons.add(new Person("小刘",16));
            persons.add(new Person("小静",22));
    
            System.out.println(persons);
        }
    }
    
    

测试结果

  • this: Person{name='小灰', age=20}-----------传递进来的对象: Person{name='小灰', age=20}
    this: Person{name='小蓝', age=18}-----------传递进来的对象: Person{name='小灰', age=20}
    this: Person{name='小刘', age=16}-----------传递进来的对象: Person{name='小灰', age=20}
    this: Person{name='小刘', age=16}-----------传递进来的对象: Person{name='小蓝', age=18}
    this: Person{name='小静', age=22}-----------传递进来的对象: Person{name='小蓝', age=18}
    this: Person{name='小静', age=22}-----------传递进来的对象: Person{name='小灰', age=20}
    [Person{name='小刘', age=16}, Person{name='小蓝', age=18}, Person{name='小灰', age=20}, Person{name='小静', age=22}]
    

分析:

  • 小灰存入到TreeSet集合;
  • 小灰传递小灰调用compareTo方法;结果为0,不存储此次的小灰(因为已经存储过一次了)
  • 存储小蓝时,使用小蓝调用CompareTo方法,并将根元素(小灰)当做参数传递给compareTo方法,结果为-2,将小蓝存储在左子树;
  • 存储小刘时,使用小刘调用CompareTo方法,并将根元素(小灰)当做参数传递给compareTo方法,结果为-4,将小刘存储在左子树;
  • 又跟小蓝比较,使用小刘调用compareTo方法,把小蓝当做参数传递给compareTo方法,结果为-2,存储在小蓝的左子树节点;此时树已经不再平衡,需要调整为平衡二叉树;
  • 存储小静时,使用小静调用compareTo方法,将根元素(此时根元素是小蓝)当做参数传递给compareTo方法,结果为4,存储在小蓝的右子数;
  • 又跟小灰比较,使用小静调用compareTo方法,把小灰当做参数传递给compareTo,结果为2,存储在小灰的右子树;

树的读取结果是从左往右读取,因此排序顺序是:(小刘,16)、(小蓝,18)、(小灰,20)、(小静,22);

这个我们是采用升序的排列方式,如果是降序呢? 我们只需要改变一下比较的规则即可

 @Override
    public int compareTo(Object o) {
        System.out.println("this: " + this + "-----------" + "传递进来的对象: " + o);
        Person person=(Person) o;
        return person.age-this.age;

    }

口诀:

  • 正序(从小到大):this-传递的对象
  • 倒序(从大到小):传递的对象-this
6.4.3.6 专门编写一个比较器(Comparator)

编写乌龟类

  • class WuGui{
        int age;
        public WuGui(int age){
            this.age=age;
        }
    
        @Override
        public String toString() {
            return "乌龟{" +
                    "age=" + age +
                    '}';
        }
    }
    
    

单独写一个比较器

  • //单独在这里编写一个比较器
    //比较器实现java.util.Comparator接口(Comparable是java.lang包下的.Comparator是java.util包下的)
    class WuGuiComparator implements Comparator<WuGui> {
        public int compare(WuGui o1,WuGui o2){
            //指定比较规则
            return o1.age-o2.age;
        }
    }
    

测试类:

  • public class TreeSetTest04 {
        public static void main(String[] args) {
            //创建TreeSet集合的时候,需要使用这个比较器
            //TreeSet<WuGui> wuGuis=new TreeSet<>();这样不行没有通过构造方法传递一个比较器进去
    
            //给构造方法传递一个比较器  就是我们刚刚写的
            //TreeSet<WuGui> wuGuis=new TreeSet<>(new WuGuiComparator());
    
            //可以使用匿名内部类的方式(这个类没有名字,直接new接口) 也可以直接写匿名内部类
            TreeSet<WuGui> wuGuis=new TreeSet<>(new Comparator<WuGui>(){
                public int compare(WuGui o1,WuGui o2){
                    return o1.age-o2.age;
                }
            });
    
            wuGuis.add(new WuGui(1000));
            wuGuis.add(new WuGui(100));
            wuGuis.add(new WuGui(200));
            for (WuGui wuGui:wuGuis){
                System.out.println(wuGui);
            }
        }
    }
    
6.4.3.7 Comparator和Comparable怎么选择呢?
  • 当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口
  • 如果比较规则多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口
  • Comparator接口的设计符合OCP原则

7. 单例集合 Queue

7.1 单例集合概述

Queue集合一般用于模拟队列这种数据结构。队列的特点是先进先出,队列的头部保存在队列中存放时间最长的元素,尾部保存存放时间最短的元素。新元素插入到队列的尾部,取出元素会返回队列头部的元素。通常,队列不允许随机访问队列中的元素;

7.2 Queue继承体系

在Queue本身是一个队列,队列一般都是单向操作,先进先出,只能操作头部的元素;但Queue下面的Deque接口则是双向队列,该接口下的实现类允许对队列的头部和尾部进行操作,其中LinkedList就是Deque接口下比较常见的一个实现类了;

另外Queue下面还有另一个集合派系——BlockingQueue,该接口规范的是阻塞队列,其中有ArrayBlockingQueueDelayQueueLinkedBlockingQueue等类的实现,这些类底层都是采用不同的数据结构来实现阻塞队列的功能;

关于阻塞队列,到后面讲了多线程 会进行讲解

7.3 Queue的基本使用

由于Queue也是属于单列集合,因此Queue同样具备Collection中的相关方法,这里就不在演示;

7.3.1 Queue常用的方法
  • boolean add(E e):将元素添加到队列的尾部
  • boolean offer(E e):将元素添加到队列的尾部,功能和add()方法一致
  • E remove():移除队列头部的元素并将该元素返回
  • E poll():移除队列头部的元素并将该元素返回,功能和remove()方法一致
  • E element():获取队列头部的元素,并不会删除该元素
  • E peek():获取队列头部的元素,并不会删除该元素,功能和element()方法一致

示例代码

  • package Collection.set.Queue;
    
    import java.util.LinkedList;
    import java.util.Queue;
    
    public class QueueTest01 {
        public static void main(String[] args) {
            Queue queue=new LinkedList();
            // add 添加元素
            queue.add("蝙蝠侠");
            queue.add("钢铁侠");
            queue.add("蜘蛛侠");
    
            System.out.println(queue);
    
            // offer 添加元素
            queue.offer("绿巨人");
    
            System.out.println(queue);
    
            // remove 和 poll 删除元素
            Object remove = queue.remove();
            Object poll = queue.poll();
            System.out.println(remove);
            System.out.println(poll);
            System.out.println(queue);
    
            // element peek 获取头部元素
            Object element = queue.element();
            Object peek = queue.peek();
            System.out.println(element);
            System.out.println(peek);
    
        }
    }
    
    
7.3.2 Deque集合

Deque是Queue接口的子类,Deque规范的是一个双向的队列,因此Deque接口中定义了很多有关于队列头部和尾部操作的方法;

在开发时,Deque集合也可以作为链表、栈、队列等结构的使用。因此Deque除了定义队列相关的头部和尾部操作的方法外,还提供了很多栈和链表的操作方法;这些方法在Deque的子类中都得到了实现;其中LinkedList底层则是采用链表来实现链表、栈、队列等数据结构;

由于Deque是双向队列,因此Deque可以获取反向迭代的迭代器;

7.3.3 Deque集合常用方法
7.3.3.1链表相关方法:
  • public void addFirst(E e):将指定元素插入此列表的开头。
  • public void addLast(E e):将指定元素添加到此列表的结尾。
  • public E getFirst():返回此列表的第一个元素。
  • public E getLast():返回此列表的最后一个元素。
  • public E removeFirst():移除并返回此列表的第一个元素。
  • public E removeLast():移除并返回此列表的最后一个元素。
package Collection.set.Queue;

import java.util.ArrayDeque;
import java.util.Deque;

public class DequeTest {
    public static void main(String[] args) {
        Deque deque=new ArrayDeque();

        //public void addFirst(E e) 将指定元素插入列表开头
        deque.addFirst("北京");
        deque.addFirst("上海");

        System.out.println(deque);//[上海, 北京]

        // public void addLast(E e) 将指定元素添加到此列表的结尾。
        deque.addLast("深圳");

        System.out.println(deque); //[上海, 北京, 深圳]

        // public E getFirst() 返回第一个元素
        System.out.println(deque.getFirst());//上海

        // public E getLast() 返回最后一个元素
        System.out.println(deque.getLast());//深圳

        // public E removeFirst() 移除第一个
        System.out.println(deque.removeFirst());//上海
        System.out.println(deque);//[北京, 深圳]
        // public E removeLast() 移除最后一个
        System.out.println(deque.removeLast());//深圳
        System.out.println(deque);//[北京]
    }
}

7.3.3.2 栈相关方法:
  • public void push(E e):将元素推入此列表所表示的堆栈(栈顶),类似于addFirst()。
  • public E pop():从此列表所表示的堆栈(栈顶)处弹出一个元素,类似于removeFirst()。
package Collection.set.Queue;

import java.util.ArrayDeque;
import java.util.Deque;

public class DequeTest02 {
    public static void main(String[] args) {
        Deque deque=new ArrayDeque();

        deque.push("java");
        deque.push("spring");
        System.out.println(deque);//[spring, java]

        Object pop = deque.pop();
        System.out.println(pop);//spring
        System.out.println(deque);//[java]

    }
}

7.3.3.3 队列相关方法:
  • public boolean offer(E e):将元素添加到队列尾部,类似于addLast()。

  • public boolean offerFirst(E e):将元素添加到队列头部,类似于addFirst()。

  • public boolean offerLast(E e):将元素添加到队列尾部,类似于addLast()。

  • public E pollFirst():移除并返回此列表的第一个元素。类似于removeFirst()。

  • public E pollLast():移除并返回此列表的最后一个元素。类似于removeLast()。

package Collection.set.Queue;

import java.util.ArrayDeque;
import java.util.Deque;

public class DequeTest03 {
    public static void main(String[] args) {
        Deque deque=new ArrayDeque();

        // 插入到结尾
        deque.offer("1");
        deque.offer("2");
        deque.offerLast("3");
        // 插入到开头
        deque.offerFirst("4");

        System.out.println(deque);

        //移除第一个
        System.out.println(deque.pollFirst());
        //移除最后一个
        System.out.println(deque.pollLast());

    }
}

7.3.3.4 迭代器相关的方法
  • Iterator iterator():获取集合的迭代器
  • Iterator descendingIterator():获取集合的反向迭代器
package Collection.set.Queue;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;

public class DequeTest04 {
    public static void main(String[] args) {
        Deque deque=new ArrayDeque();

        // 添加到头元素
        deque.addFirst("1");
        deque.addFirst("2");
        deque.addFirst("3");
        deque.addFirst("4");

        System.out.println(deque);
        Iterator iterator = deque.iterator();

        // 正向迭代器
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println(next);
        }

        System.out.println("----------------------------");
        // 反向迭代器
        Iterator descendingIterator = deque.descendingIterator();
        while (descendingIterator.hasNext()) {
            Object next = descendingIterator.next();
            System.out.println(next);
        }

    }
}

7.4 Deque的实现 ArrayQueue

eque接口下有两个常用的实现类,分别为:LinkedListArrayDeque;这两个类基本上没有对Deque接口进行功能上的扩充,只是底层的实现方式不同了,达到的相关都是一致的;LinkedList采用的是链表来实现双向队列,而ArrayDeque采用的是数组来实现双向队列;

ArrayDeque的继承体系:

ArrayDeque的特点如下:

  • 底层采用数组来实现双向队列
  • 默认初始化大小为16,每次2倍扩容
  • 线程不安全,多线程访问可能会引发线程安全问题
  • 不能存储null值,执行双向迭代器遍历

6.泛型

泛型在java中有很重要的地位,无论是开源框架还是JDK源码都能看到它。

毫不夸张的说,泛型是通用设计上必不可少的元素,所以真正理解与正确使用泛型,是一门必修课。

6.1 泛型的概述

泛型定义:把类型明确的工作延迟到创建对象或调用方法的时候才去明确的特殊的类型;

例如,我们知道集合是可以存储任意元素的,那么这样一想,add方法上的参数应该是Object(所有类的父类),但是这样会引入一个新的问题,我们知道,子类都是比父类强大的,我们在使用的时候肯定是希望获取的是当初存进去的具体子类对象;因此我们每次都需要进行强制转换;

但add方法真的是Object吗?

查看ArrayList的add方法:

public class ArrayList<E> {
    public boolean add(E e) {....}
    
    public E get(int index) {}
}

Collection类:

public interface Collection<E> extends Iterable<E> {}

上面的E就是泛型,集合的定义者也不知道我们需要存储什么元素到集合中,具体的类型只能延迟到创建对象时来决定了

泛型这种语法机制,只是在程序编译阶段起作用,只是给编译器参考的.(运行阶段泛型没有)

泛型的优点:

  • 第一:集合中存储的元素类型统一了.
  • 第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的"向下转型"

泛型的缺点:

  • 导致集合中存储的元素缺乏多样性大多数业务中,集合中元素的类型还是统一的.所以这种泛型特性被大家所认可

6.2 泛型的本质

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

6.3泛型的作用

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

6.2.1保证了类型的安全

在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。

比如:没有泛型的情况下使用集合:

public class GenericDemo01 {
    public static void main(String[] args) {
        ArrayList arrayList=new ArrayList();
        arrayList.add("hello");
        arrayList.add(1);//编译正常
    }
}

有泛型的情况下

public class GenericDemo02 {
    public static void main(String[] args) {
        //   JDK8之后引入了:自动类型推断机制(又称为钻石表达式)
        //ArrayList<这里的类型会自动推动>(),前提是JDK8之后才允许
        ArrayList<String> arrayList=new ArrayList<>();
        
        arrayList.add("hello");
        arrayList.add(1);// 这个编译时候就会报错
    }
}

有了泛型后,定义好的集合arrayList在编译的时候add(1)就会编译不通过。

相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。

6.2.2 消除强制转换

泛型的一个附带好处是,消除源代码中的许多强制类型转换,这使得代码更加可读,并且减少了出错机会。

还是举例说明,以下没有泛型的代码段需要强制转换:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

当重写为使用泛型时,代码不需要强制转换:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);
6.2.3 避免了不必要的装箱、拆箱操作,提高程序的性能

在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。

泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。

6.2.4 提高了代码的重用性。

6.4 泛型的使用

泛型有三种方式L:分别是:泛型类、泛型接口和泛型方法

泛型的作用示意图:

6.4.1 不使用泛型的情况

定义动物类:

  • public class Animal{
        public void move(){
            System.out.println("动物在移动");
        }
    }
    

定义猫类

  • public class Cat extends Animal{
        public void catchMouse(){
            System.out.println("猫抓老鼠!!");
        }
    }
    

定义鸟类:

  • public class Bird extends Animal{
        public void fly(){
            System.out.println("鸟儿在飞翔!!");
        }
    
    }
    

测试类

  • public class GenericTest {
        public static void main(String[] args) {
    		ArrayList arrayList=new ArrayList();
    
            // 添加
            arrayList.add(new Cat());
            arrayList.add(new Bird());
    
            // 遍历
            for (int i = 0; i < arrayList.size(); i++) {
                //Animal animal = arrayList.get(i); 没有这个语法 取出来的都是Object类型
                Object o = arrayList.get(i);
                Animal animal=null;
                if (o instanceof Animal){
                    animal=(Animal) o;
                    animal.move();
                }
    
    
                if (animal instanceof Cat){
                    Cat cat=(Cat) animal;
                    cat.catchMouse();
                }
                if (animal instanceof Bird){
                    Bird bird=(Bird) animal;
                    bird.fly();
                }
            }				        
    	}
    }
    

在测试类当中,每次get()出来的都是Object类。我们要不断的要转型

6.4.2 使用泛型的情况下

测试类:

  • public class Test02 {
        public static void main(String[] args) {
            ArrayList<Animal> arrayList=new ArrayList<>();
    
            arrayList.add(new Cat());
            arrayList.add(new Bird());
    
            for (Animal animal : arrayList) {// 这样每次取出来的就是 动物类从而减少操作
                if (animal instanceof  Cat){
                    Cat cat=(Cat) animal;
                    cat.catchMouse();
                }
                if (animal instanceof Bird){
                    Bird bird=(Bird) animal;
                    bird.fly();
                }
            }
        }
    }
    

注意:强制类型转换可能会出现ClassCastException类型转换异常 所有我们要用instanceof 去预防

6.4.3 泛型类
6.4.3.1 泛型类的使用

很明显,Collection、List、Set以及其下的子类都是泛型类,我们根据使用情况也可以定义泛型类;让泛型类的类型延迟到创建对象的时候指定;

使用格式:

例如:我们看ArrayList类的写法

public class ArrayList<E> implements List<E>{
    public boolean add(E e) {}
}

那如果我们定义ArrayList是采用泛型的形式呢?也就是在创建对象的时候确定泛型

ArrayList<String> stringArrayList=new ArrayList();

变量E的值就是String类型,那么我们的类型就可以理解为:

public class ArrayList<String> implements List<String>{
    public boolean add(String e){};
}

自己定义一个泛型类

public class GenericClass<T> {
    private T t;

    public GenericClass(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    @Override
    public String toString() {
        return "GenericClass{" +
                "t=" + t +
                '}';
    }
}

测试

public class GenericTest {
    public static void main(String[] args) {
        GenericClass<String> stringGenericClass=new GenericClass<>("小宋学java");
        System.out.println(stringGenericClass.getT());

    }
}

类型传入以后,GenericClass 就会变为

public class GenericClass02 {
    private String string;

    public GenericClass02() {
    }

    public GenericClass02(String string) {
        this.string = string;
    }

    public String getString() {
        return string;
    }

    public void setString(String string) {
        this.string = string;
    }

    @Override
    public String toString() {
        return "GenericClass02{" +
                "string='" + string + '\'' +
                '}';
    }
}

6.4.3.2 泛型类的继承

定义一个父类带有泛型的

public class Fu<E> {
    public void show(E e) {
        System.out.println(e+"Fu");
    }
}

定义子类:

  • 定义时,确定泛型的类型

    public class Zi extends Fu<String>{
        @Override
        public void show(String s) {//此时,泛型E的值就是String类型。
            System.out.println(s+"Zi");
        }
    }
    
    
  • 始终不确定泛型的类型,直到创建对象时,确定泛型的类型

    public class Zi2<E> extends Fu<E>{
        @Override
        public void show(E e) {
            System.out.println(e+"Zi2");
        }
    }
    
    public class GenericDemo06 {
        public static void main(String[] args) {
            Zi2<String> stringZi2=new Zi2<>();
            stringZi2.show("小宋学java");
        }
    }
    
    

T:任意类型 type
E:集合中元素的类型 element
K:key-value形式 key
V: key-value形式 value

6.4.4 泛型接口

定义泛型接口

  • public interface GenericInterface<E> {
        void show(E e);
    }
    
    

定义实现类–确定泛型的类型

  • public class GenericImplements01 implements GenericInterface<String>{
    
        @Override
        public void show(String s) {
            System.out.println(s);
        }
    }
    
    

定义实现类–不确定泛型类型

  • public class GenericImplements02<E> implements GenericInterface<E> {
    
        @Override
        public void show(E e) {
            System.out.println(e);
        }
    }
    
    

测试类:

  • public class GenericImplementsDemo {
        public static void main(String[] args) {
            GenericInterface<String> g=new GenericImplements01();
            g.show("Generic");
    
            GenericInterface<Integer> interfaceTest=new GenericImplements02<>();
            interfaceTest.show(123);
        }
    }
    
    
6.4.5 泛型方法

泛型类是在创建类时定义泛型类型,在创建对象时确定泛型类型;泛型方法则是在创建方法是定义泛型类型,在调用方法时确定泛型类型;

例如:

  • public class GenericMethod {
    
        public <P> P GetClass(P p){
            return p;
        }
    }
    
    

定义宠物类:

  • public class Animal {
        private String name;
        private int age;
    
    
        public Animal(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Animal{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
    

测试:

  • public class GenericMethodTest {
        public static void main(String[] args) {
            GenericMethod genericMethod=new GenericMethod();
            String string = genericMethod.GetClass("String");
            Integer integer = genericMethod.GetClass(1);
            Animal animal = genericMethod.GetClass(new Animal("哈士奇", 5));
        }
    }
    
    

6.5 泛型通配符

6.5.1 通配符的使用
6.5.1.1 参数列表带有泛型

泛型在程序运行中全部会被擦除,我们把这种现象称为泛型擦除;但编译时期在使用泛型类进行方法传参时,不仅要匹配参数本身的类型,还要匹配泛型的类型;

定义泛型类:

  • public class TestClass<E> {
        private E e;
    
        public TestClass(E e) {
            this.e = e;
        }
    
        public TestClass() {
        }
    
        public E getE() {
            return e;
        }
    
        public void setE(E e) {
            this.e = e;
        }
    }
    
    
    

测试带有泛型的方法参数列表:

  • public class Demo01_方法参数带有泛型的问题 {
        public static void main(String[] args) {
            TestClass<Number> testClass=new TestClass<>();
            methodTest(testClass);
    
        }
    
        public static void methodTest(TestClass<Number> testClass){
            Number e = testClass.getE();
            System.out.println("testClass..........");
        }
    
        /*
        报错 和上面的methodTest 方法冲突 因为泛型在运行期间会被擦除
        public static void methodTest(TestClass testClass){
    
        }
        */
    }
    
    
6.5.1.2 泛型通配符

但是 我们要在创建对象的时候 泛型为 Integer的类型呢?

public class Demo01_方法参数带有泛型的问题 {
    public static void main(String[] args) {
        TestClass<Number> testClass=new TestClass<>();
        methodTest(testClass);

        TestClass<Integer> integerTestClass=new TestClass<>();
        //methodTest(integerTestClass); 这里就会直接报错

    }

    public static void methodTest(TestClass<Number> testClass){
        Number e = testClass.getE();
        System.out.println("testClass..........");
    }

    /*
    报错 和上面的methodTest 方法冲突 因为泛型在运行期间会被擦除
    public static void methodTest(TestClass testClass){

    }
    */
    
    /*
    这个和上面的方法冲突
    public static void test1(TestClass<Integer> testClass) {
        System.out.println("test1...");
    }
*/
}

那么我们不确定 参数接收的倒是什么参数这个时候引出了通配符的写法?

泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。

对上面的测试类进行修改

public class Demo02_泛型通配符的使用 {
    public static void main(String[] args) {
        TestClass<Number> testClass=new TestClass<>();
        methodTest(testClass);
        TestClass<Integer> integerTestClass=new TestClass<>();
        methodTest(integerTestClass);
    }
    public static void methodTest(TestClass<?> testClass){
        Object e = testClass.getE();// 被?接收的泛型都将提升为Object
        System.out.println("testClass..........");
    }
}

需要注意的是,被<?>接收过的类型都将提升为Object类型;

6.5.2 通配符的分类

Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:

6.5.2.1 无边界的通配符

无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.

6.5.2.2 固定上边界的通配符

泛型的上限:

  • 格式: 类型名称 <? extends 类> 对象名称
  • 含义: 只能接收该类型及其子类
  • 例如:public class Apple<T extends A>{}

使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。

要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界。

注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类

6.5.2.3 固定下边界的通配符

泛型的下限:

  • 格式: 类型名称 <? super 类> 对象名称
  • 含义: 只能接收该类型及其父类型
  • 例如:public class Apple<T supers A>{}

使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据.。

要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界.。

注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界。

6.5.2.4 测试上下边界
public class Demo03 {
    public static void main(String[] args) {
        TestClass<Number> testClass1=new TestClass<>();
        TestClass<Integer> testClass2=new TestClass<>();
        TestClass<Double> testClass3=new TestClass<>();

        TestClass<Object> testClass4=new TestClass<>();

        methodExtends(testClass1);
        methodExtends(testClass2);
        methodExtends(testClass3);
        //methodExtends(testClass4);报错 Object 不是Number 的子类
        
        //methodSuper(testClass2); 报错 Integer 不是Number 的父类
        methodSuper(testClass4);


    }
    public static void methodExtends(TestClass<? extends Number> testClass){
        Number e = testClass.getE();
        System.out.println("testClass..........");
    }

    public static void methodSuper(TestClass<? super Number> testClass){
        Object e = testClass.getE();
        System.out.println("testClass..........");
    }
}

6.6 泛型中KTVE的含义

E: Element (在集合中使用,因为集合中存放的是元素)
T:Type(Java 类)
K: Key(键)
V: Value(值)
N: Number(数值类型)
?: 表示不确定的java类型

6.7 泛型的擦除(泛型的原理)

Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程称为类型擦除。

6.7.1 限制擦除

泛型在擦除过程中有限制擦除无限制擦除

有限制擦除:

无限制擦除:将泛型类型提升为Object类型

6.7.1 泛型的桥接方法

在接口使用泛型时,在运行期间接口中的泛型也一样会被擦除;

那如果编写了实现类来实现这个泛型接口,实现类中的泛型在运行期间也会被擦除,这样一来就会出现接口的方法并没有在实现类中得到实现:

好在JVM进行了特殊处理,如果我们编写的类实现了一个带有泛型的接口时,在运行期期间JVM会在实现类中帮我们自动的生产一个方法来帮助我们实现泛型接口中被擦除过后的那个方法,这个方法被称为桥接方法

7.双列集合

7.1Map集合的概述

Map 用于保存具有映射关系的数据,如一个学生的ID对应一个学生,一个商品的ID对应一个商品,一个部门ID对应多个员工;这种具有对应关系的数据成为映射;

它是成键值对的方式key-value

因此 Map 集合里保存着两组值,一组值用于保存 Map 里的 Key,另外一组用于保存 Map 里的 Value,Map 中的 key 和 value 都可以是任何引用类型的数据;

Map接口的继承体系

Collection接口是单列集合的顶层接口,Map则是双列集合的顶层接口;

7.2Map集合共有的方法

7.2.1 方法的演示

Map是所有双列集合的顶层父类,因此Map中具备的是所有双列集合的共性方法;常用的方法如下:

  • public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
  • public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
  • boolean containsKey(Object key) 判断集合中是否包含指定的键。
  • boolean containsValue(Object value)判断Map中是否包含某个value
  • public void clear()清空Map
  • boolean isEmpty()判断Map集合中元素个数是否为0
  • public V remove(Object key)通过key删除键值对
  • public int size()获取Map集合中键值对的个数
  • public Collection values():获取该map集合的所有value
  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。
  • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。

示例代码:

/*
    1.Map和Collection没有继承关系
    2.Map集合以key和value的方式存储数据:键值对
        key和value都是引用数据类型
        key和value都是存储对象的内存地址
        key起主导的地位,value是key的一个附属品
 */
public class MapDemo01 {
    public static void main(String[] args) {
        // 创建Map集合 Key为String类型 Value也为String 类型
        Map<String,String> map=new HashMap<>();

        // 1.public V put(K key, V value);把指定的键与指定的值添加到Map集合中。
        map.put("key1","value1");
        map.put("key2","value2");
        map.put("key3","value3");

        // 2. public V get(Object key)  根据指定的键,在Map集合中获取对应的值
        String key1 = map.get("key1");
        System.out.println("key1=" + key1);

        // 3.boolean containsKey(Object key) 判断集合中是否包含指定的值
        System.out.println(map.containsKey("key1"));


        // 4.boolean containsValue(Object value)判断Map中是否包含某个value
        System.out.println(map.containsValue("value1"));

        // 5.public void clear(); 清空map
        map.clear();

        // 6. public boolean isEmpty() 判断Map集合中元素个数是否为0
        boolean empty = map.isEmpty();

        // 继续添加元素
        map.put("k1","v1");
        map.put("k2","v2");
        map.put("k3","v3");

        // 7.public V remove(Object key)通过key删除键值对
        String remove = map.remove("k1");

        // 8.public int size() 获取Map集合键值对的个数
        int size = map.size();

        // 9. 获取map所有的Value
        Collection<String> values = map.values();
        for (String value : values) {
            System.out.println(value);
        }

        //10. public Set<Map.Entry<K,V>> entrySet()获取到Map集合中所有的键值对对象的集合(Set集合)
        Set<Map.Entry<String, String>> entries = map.entrySet();

        // 11.public Set<K> keySet() 获取Map集合中所有的key,存储到Set集合中。
        Set<String> strings = map.keySet();
        Iterator<String> it = strings.iterator();
        while (it.hasNext()) {
            String next = it.next();
            System.out.println(next);
        }
    }
}

public Set<Map.Entry<K,V>> entrySet()获取Map集合中所有键值对对象的集合到后面我们会细讲

7.2.2 Entry对象

Map集合中几条记录存储的是两个对象,一个是key,一个是value,这两个对象加起来是map集合中的一条记录,也叫一个记录项;这个记录项在Java中被Entry对象所描述;一个Entry对象中包含有两个值,一个是key,另一个则是key对应的value,因此一个Map对象我们可以看做是多个Entry对象的集合,即一个Set对象;

Set<Map.Entry<K,V>> entrySet() 将Map集合转换为Set集合

key value


山东 济南
山西 太原
河南 郑州
河北 石家庄

Set s=map.entrySet();

set集合对象
山东=济南
山西=太原
河南=郑州
河北=石家庄

[注意:Map集合通过entrySet()方法转换成的这个Set集合,Set集合中元素的类型是:Map.Entry<K,V>]

[Map.Entry和String一样,都是一种类型的名字,只不过:Map.Entry是静态内部类,是Map中的静态内部类]

Entry是一个接口,是Map接口中的一个内部接口,源码如下:

public interface Map<K, V> {    
	interface Entry<K,V> {

            K getKey();

            V getValue();

            V setValue(V value);
        ....
    }
}

HashMap中则提供了Node类对Entry提供了实现,可以看到一个Entry对象(Node对象)中包含有key、value等值:

下面对Entry进行理解

/*
对Set<Map.Entry<K,V>> entrySet() 中Map.Entry<K,V>的理解
Map.Entry这是一个类
K,V是泛型
 */
public class MapDemo02 {
    public static class InnerClass{
        public static void method1(){
            System.out.println("静态内部类m1方法执行了");
        }
        public void method2(){
            System.out.println("静态内部类的示例方法执行了");
        }
    }

    public static void main(String[] args) {
        //类名叫做MapDemo02.InnerClass
        MapDemo02.InnerClass.method1();

        // 创建内部类对象
        MapDemo02.InnerClass innerClass=new MapDemo02.InnerClass();
        innerClass.method2();

        // 给定一个set集合
        // 该set集合中存储的对象是MapDemo02.InnerClass类型
        Set<MapDemo02.InnerClass> set=new HashSet<>();

        // 这个set集合只能存储String类型的
        Set<String> stringSet=new HashSet<>();

        // 这个Set存储的是MyMap.MyEntry 这个类型
        Set<MyMap.MyEntry<Integer,String>> entrySet=new HashSet<>();
    }
}

class MyMap{
    public static class MyEntry<k,v>{

    }
}

7.2.3 Map集合遍历的三种方式
7.2.3.1 方式一

所用方法:

  • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
  • public V get(Object key) 根据指定的键,在Map集合中获取对应的值。

步骤:

  • 通过方法 Set<k> keySet()这个方法获取所有的key 返回的是一个set集合
  • 根据第一步返回的 Set集合来创建一个迭代器 Iterator it=keys.iterator();
  • 通过迭代获取每一个key进行遍历来获取value map.get(it.next());

代码示例:

package Map.MapDemo;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class Map_遍历方式一 {
    public static void main(String[] args) {
        Map<Integer,String> map=new HashMap<>();

        map.put(1,"张三");
        map.put(2,"李四");
        map.put(3,"王五");
        map.put(4,"赵六");
        map.put(5,"李七");

        Set<Integer> keySet = map.keySet();
        Iterator<Integer> it = keySet.iterator();
        while (it.hasNext()) {
            Integer key = it.next();// 获取的是key
            String value = map.get(key);
            System.out.println(key+"==============>"+value);
        }

    }
}

7.2.3.2 方式二

第二种方法 就是foreach,但也要获取所有的key

示例代码:

package Map.MapDemo;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Map_遍历方式二 {
    public static void main(String[] args) {
        Map<Integer,String> map=new HashMap<>();

        map.put(1,"张三");
        map.put(2,"李四");
        map.put(3,"王五");
        map.put(4,"赵六");
        map.put(5,"李七");

        Set<Integer> keys = map.keySet();
        for (Integer key : keys) {
            String value = map.get(key);
            System.out.println(key+"==============>"+value);

        }
    }
}

7.2.3.3 方式三

步骤

  • public Set<Map.Entry<K,V>> entrySet()转换成Set集合
  • 获取迭代器
  • 遍历每一个Map.Entry元素
  • 通过getKey()getValue()获取key和value
package Map.MapDemo;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class Map_遍历方式三 {
    public static void main(String[] args) {
        Map<Integer,String> map=new HashMap<>();

        map.put(1,"张三");
        map.put(2,"李四");
        map.put(3,"王五");
        map.put(4,"赵六");
        map.put(5,"李七");

        Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
        Iterator<Map.Entry<Integer, String>> it = entrySet.iterator();
        while (it.hasNext()) {
            Map.Entry<Integer, String> node = it.next();//Map.Entry 就是一种类型
            Integer key = node.getKey();
            String value = node.getValue();
            System.out.println(key+"==============>"+value);
        }

    }
}

7.3 HashMap

7.3.1 HashMap简介

HashMap是Map集合中比较常用的实现类,其特点依旧是我们之前学习的HashSet特点;即存储数据采用的哈希表结构(JDK8改为hash表+红黑树),元素的存取顺序不一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

我们之前学习的HashSet底层就是一个HashMap,当我们在讨论HashSet的底层原理时,其实讨论的是HashMap的key的底层原理;

HashSet底层实际上就是一个HashMap,存放元素就是在存放在Key部分

查看HashSet的add方法源码:

HashSet的存储的元素都是HashMap的Key,此HashMap的value总是一个固定的Object;

所以,HashSet的去重原理实质上指的就是HashMap的Key的去重原理;

7.3.2 HashMap的去重

我们知道HashSet底层就是依靠HashMap的key去重原理来是实现的,因此Map接口的HashMap、LinkedHashMap等接口的去重都是和HashSet、LinkedHashSet一致;

我们所说的去重就是key部分的去重

编写一个Cat类

public class Cat {
    private String name;
    private Integer age;

    // 构造方法 toString方法 setter and getter方法
}

注意:上面没有重写equals和hashCode方法;

存储两个属性一样的Cat对象:

public class CatTest {
    public static void main(String[] args) {
        HashMap<Cat,Integer> hashMap=new HashMap<>();
        // 我们所说的去重是key部分的去重,value可以是重复的,如果key相同就会覆盖
        hashMap.put(new Cat("花花",5),1);
        hashMap.put(new Cat("花花",5),1);

        System.out.println(hashMap.size()); //2
    }
}

回顾学习HashSet时的去重原理:

存储的两个条件:

  • hashCode不同时存储
  • 当hashCode冲突时,equals为false时存储 equals为true时覆盖之前的value

对上方的Cat添加equals() 和hashCode

public class Cat {
    private String name;
    private Integer age;

    // 构造方法 toString方法 setter and getter方法
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Cat cat = (Cat) o;
        return Objects.equals(name, cat.name) && Objects.equals(age, cat.age);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

在测试:

public class CatTest {
    public static void main(String[] args) {
        HashMap<Cat,Integer> hashMap=new HashMap<>();
        // 我们所说的去重是key部分的去重,value可以是重复的
        hashMap.put(new Cat("花花",5),1);
        hashMap.put(new Cat("花花",5),1);

        System.out.println(hashMap.size());

        

    }
}

如果key重复会怎么样?会覆盖之前存储的value

public class Test {
    public static void main(String[] args) {
        
        HashMap<Integer,String> map=new HashMap<>();
		
        // 当key冲突的时候,会覆盖之前的value
        // 1-->"2"  key冲突之后  1-->"3"
        map.put(1,"2");
        map.put(1,"3");

        System.out.println(map.get(1));//3

    }
}

7.3.3 HashMap的特点
  • 底层是哈希表的数据结构,是非线程安全的
  • 在JDK8之后,如果哈希表单链表中元素超过8个,单链表这种数据结构会变成红黑树(二叉树)数据结构当红黑树上的结点数量小于6时,会重写把红黑树变成单链表的数据结构这种方式也是为了提高检索效率,二叉树的检索会再次缩小扫描范围,提高效率
  • 初始化容量是16,默认加载因子是0.75 ,也就是当容量达到了75%之后会扩容
    扩容之后的容量是原容量的2倍
  • HashMap集合的key和value都是可以为null的

7.4 LinkedHashMap

7.4.1 LinkedHashMap 特点

我们之前在学习LinkedHashSet时说过,LinkedHashSet是继承与HashSet的,在HashSet底层的基础上增加了一个循环链表,因此LinkedHashSet除了具备HashSet的特点外(唯一),存储的元素还是有序的;

LinkedHashMap继承与HashMap,并且LinkedHashSet底层就是借助于LinkedHashMap来实现的;

LinkedHashSet源码如下:

查看HashSet对应的构造:

查看LinkedHashSet的add方法源码:

使用快捷键ALT+7,就可以在左侧看到方法和属性

HashSet的add方法源码我们之前看过了,实质上是添加到内置的HashMap中去了;

7.4.2 LinkedHashMap 使用
public class Demo01 {
    public static void main(String[] args) {
        LinkedHashMap<String,String> map=new LinkedHashMap<>();

        map.put("安徽","合肥");
        map.put("江苏","南京");
        map.put("浙江","杭州");

        // LinkedHashMap存取是有序的
        System.out.println(map);        // {安徽=合肥, 江苏=南京, 浙江=杭州}
    }
}

7.5 TreeMap

TreeMap也是TreeSet的底层实现,创建TreeSet的同时也创建了一个TreeMap,在往TreeSet集合中做添加操作是,实质也是往TreeMap中添加操作,TreeSet要添加元素成为了TreeMap的key

我们来回顾一下TreeSet的特点(也是TreeMap的key的特点):

  • 必须实现Compareable接口;
  • 存储的数据是无序的,但提供排序功能(Comparable接口);
  • 存储的元素不再是唯一,具体结果根据compareTo方法来决定;

示例代码1:

public class Demo01 {
    public static void main(String[] args) {
        Map<Integer,String> map=new TreeMap<>();

        map.put(10,"abc");
        map.put(5,"abc");
        map.put(6,"abc");
        map.put(3,"abc");
        map.put(1,"abc");

        // 根据key排序
        System.out.println(map);        // {1=abc, 3=abc, 5=abc, 6=abc, 10=abc}
    }
}

存储示例2:存储自定义对象,自定义排序规则

定义一个Book类:

public class Book implements Comparable<Book>{
    private String name;
    private int price;

    //根据年龄的升序
    @Override
    public int compareTo(Book o) {
        return this.price-o.price;
    }

    // 构造方法 getter and setter  toString() equals() hashCode() 
}

测试类:

public class BookTest {
    public static void main(String[] args) {
        TreeMap<Book,Integer> treeMap=new TreeMap<>();

        treeMap.put(new Book("java核心卷1",30),10);
        treeMap.put(new Book("java核心卷2",35),10);
        treeMap.put(new Book("java核心卷3",28),10);

        // 这里 有很多遍历的方式, 只遍历key
        Set<Map.Entry<Book, Integer>> entries = treeMap.entrySet();
        Iterator<Map.Entry<Book, Integer>> it = entries.iterator();
        while (it.hasNext()) {
            Map.Entry<Book, Integer> node = it.next();

            Book nodeKey = node.getKey();
            System.out.println(nodeKey);
        }

    }
}

运行结果:

Book{name='java核心卷3', price=28}
Book{name='java核心卷1', price=30}
Book{name='java核心卷2', price=35}

7.6 HashTable

Hashtable是原始的java.util的一部分,属于一代集合类, 是一个Dictionary具体的实现 。Java1.2重构的Hashtable实现了Map接口,因此,Hashtable现在集成到了集合框架中。它和HashMap类很相似,但是它支持同步。

Dictionary类是一代集合中的双列集合顶层类,Dictionary类中的方法都是双列集合中最基本的方法;严格意义来说Java中所有的双列集合都应该继承与Dictionary类,但Java2推出了一系列二代集合,其中二代集合中的Map接口也已经替代了Dictionary接口,成为双列集合的顶层接口,因此Dictionary接口下面没有太多的实现类;

Tips:目前JDK已经不推荐使用Dictionary类了;

Dictionary接口方法如下:

方法说明
Enumeration elements()返回此字典中值的枚举。
V get(Object key)返回该字典中键映射到的值。
boolean isEmpty()检测该字典是否为空。
Enumeration keys()返回此字典中键的枚举。
V put(K key, V value)添加一对key,value到字典中
V remove(Object key)根据对应的key从字典中删除value。
int size()返回此字典中的条目数。

方法测试:

public class HashTableTest {
    public static void main(String[] args) {
        Dictionary<Integer,String> hashTable=new Hashtable<>();


        hashTable.put(1, "南昌拌粉");
        hashTable.put(2, "粉蒸肉");
        hashTable.put(3, "福羹");
        hashTable.put(4, "藜蒿炒腊肉");
        hashTable.put(5, "瓦罐汤");

        String s1 = hashTable.get(3);
        System.out.println(s1);                             // 福羹

        String s2 = hashTable.remove(2);
        System.out.println(s2);                             // 粉蒸肉

        System.out.println(hashTable);                      // {5=瓦罐汤, 4=藜蒿炒腊肉, 3=福羹, 1=南昌拌粉}

        System.out.println("-------------");

        // 获取到Hashtable的所有key
        Enumeration<Integer> keys = hashTable.keys();
        while (keys.hasMoreElements()){
            Integer key = keys.nextElement();
            System.out.println(key);
        }

        System.out.println("-------------");


        // 获取到Hashtable的所有value
        Enumeration<String> vals = hashTable.elements();
        while (vals.hasMoreElements()){
            String val = vals.nextElement();
            System.out.println(val);
        }

        System.out.println("-----------------");
        System.out.println(hashTable.size());                   // 4

    }
}

7.6.2 HashTable的特点
  • 底层也是哈希表数据结构,是线程安全的,其中synchronized关键字修饰的效率较低,现在保证线程安全有别的方案,所以Hashtable使用的较少了
  • Hashtable的key和value都是不能为null的
  • Hashtable的初始化容量是11,默认加载因子为0.75
  • Hashtable的扩容:原容量*2+1
7.6.3 Hashtable与HashMap的区别
  • Hashtable属于一代集合,继承了Dictionary类,也实现了Map接口,HashMap属于二代集合,实现与Map接口,没有与Dictionary类产生关系;

  • Hashtable支持iterator遍历(Map接口中的),也支持Enumeration遍历(Dictionary),HahsMap只支持iterator遍历

  • Hashtable与HashMap底层都是采用hash表这种数据结构,JDK8对HashMap进行了优化(引入红黑树),但并没有对Hashtable进行优化;

  • HashMap默认的数组大小是16,Hashtable则是11,两者的负载因子都是0.75,并且都允许传递初始化的数组大小和负载因子

  • HashMap对null key和null value进行了特殊处理,可以存储null key和null value,Hashtable则不能存储null key和null value;

  • 当HashMap存储的元素数量>数组容量*负载因子,数组扩容至原来的2倍,Hashtable则是2倍+1;

  • HashMap在添加元素时使用的是:元素本身的hash算法 ^ (元素本身的hash算法>>> 16),而Hashtable则是直接采用元素本身的hash算法;

    Tips:>>代表有符号位移,>>>代表无符号位移;

  • HashMap在使用foreach迭代时不能对元素内容进行增删,否则触发并发修改异常。Hahstable中支持Enumeration迭代,使用Enumeration迭代元素时,可以对集合进行增删操作;

  • Hashtable是线程安全的,效率低,安全性高;HashMap是线程不安全的,效率高,安全性低;

测试存储Null key和Null value:

public class Demo02_HashMapHashtable的区别_null问题 {
    public static void main(String[] args) {
        HashMap<Integer, String> hashMap = new HashMap<>();
        /*
         HashMap对null key和null value
         并且,HashMap对null key做了特殊处理,HashMap永远将Null key存储在第0位数组上
         */
        hashMap.put(1, null);
        hashMap.put(null, "大闸蟹");

        System.out.println(hashMap);            // {null=大闸蟹, 1=null}
    }

    public static void test1(){
        Hashtable<Integer, String> hashtable = new Hashtable<>();

        // Hashtable存储null
        //key和null value的时候会出现空指针异常: 
        //Exception in thread "main" java.lang.NullPointerException
        hashtable.put(1, null);
        hashtable.put(null, "大闸蟹");
    }
}

测试并发修改异常问题:

public class Demo03_HashMapHashtable的区别_并发修改问题 {
    public static void main(String[] args) {

        Hashtable<Integer, String> hashtable = new Hashtable<>();
        hashtable.put(1, "拌粉");
        hashtable.put(2, "汤粉");
        hashtable.put(3, "炒粉");
        hashtable.put(4, "泡粉");

        Enumeration<Integer> keys = hashtable.keys();

        while (keys.hasMoreElements()) {
            Integer key = keys.nextElement();

            if (key == 2) {
                /*
                    Hashtable在使用Enumeration遍历时,允许对集合进行增删操作
                    注意: Hashtable使用foreach迭代也不能对元素进行增删操作
                 */
                hashtable.put(5, "扎粉");
//                hashtable.remove(3);
            }
        }

        System.out.println(hashtable);
    }

    /**
     * hashMap在使用foreach迭代时不允许对集合进行增删等操作
     */
    public static void test1() {
        HashMap<Integer, String> hashMap = new HashMap<>();

        hashMap.put(1, "拌粉");
        hashMap.put(2, "汤粉");
        hashMap.put(3, "炒粉");
        hashMap.put(4, "泡粉");

        Set<Integer> keys = hashMap.keySet();

        for (Integer key : keys) {
            if (key == 2) {
                // hashMap在迭代时不允许对集合进行增删等操作
                hashMap.remove(3);
//                hashMap.put(5, "扎粉");
            }
        }
    }
}

7.7 Properties

package Map.HashTable;

import java.util.Properties;

/*
目前只需要掌握Properties属性类对象的相关方法即可
Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型的
Properties被称为属性类对象\
Properties是线程安全的
 */
public class PropertiesTest01 {
    public static void main(String[] args) {
        //创建一个Properties对象
        Properties pro=new Properties();

        //需要掌握Properties的两个方法,
        //一个存
        pro.setProperty("uername","root");
        pro.setProperty("password","132");
        pro.setProperty("url","东马营");

        //按住alt 往下拖会编辑多行
        //一个取
        //通过key获取value
        String s1=pro.getProperty("username");
        String s2=pro.getProperty("password");
        String s3=pro.getProperty("url");

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);

    }
}

8. Collections工具类

Collections是一个操作Set、List和Map等集合的工具类

Collections中提供了一系列静态的方法对集合元素进行了排序、查询和修改等操作

8.1 排序序操作(均为static方法)

  • public static void reverse(List<?> list)反转List中元素的顺序
  • public static void shuffle(List<?> list)对List集合元素进行随机排序
  • public static<T extends Comparable<? super T>> void sort(List<T> list)对list集合进行排序
  • public static <T> void sort(List<T> list,Comparator<? super T> c)反转List中元素的顺序
  • public static void swap(List<?> list,int i,int j)交换指定列表指定位置的元素

代码示例:

public class CollectionsDemo01 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");

        // 1.public static void reverse(List<?> list)反转List中元素的顺序
        Collections.reverse(list);
        System.out.println(list);//[5, 4, 3, 2, 1]

        //2.public static void shuffle(List<?> list)对List集合元素进行随机排序
        Collections.shuffle(list);
        System.out.println(list);//[3, 1, 5, 2, 4]


        // 3.public static void swap(List<?> list,int i,int j)交换指定列表指定位置的元素
        Collections.swap(list,2,3);
        System.out.println(list);//[3, 1, 2, 5, 4]

    }
}
	

关于Comparable示例代码

WuGui类

public class WuGui implements Comparable<WuGui> {

    private String name;
    private int age;

    

    @Override
    public int compareTo(WuGui o) {
        return this.age-o.age;
    }
    
    // 构造方法 getter and setter  toString() equals() hashCode() 
}

单独写一个比较器

public class WuGuiCompare implements Comparator<WuGui> {
    @Override
    public int compare(WuGui o1, WuGui o2) {
        return o1.getAge()-o2.getAge();
    }

}

测试类

package Collection.Collections;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsDemo02 {
    public static void main(String[] args) {
        List<WuGui> wuGuis1=new ArrayList<>();

        wuGuis1.add(new WuGui("小绿",68));
        wuGuis1.add(new WuGui("小黄",28));
        wuGuis1.add(new WuGui("小蓝",18));
        wuGuis1.add(new WuGui("小红",101));
        wuGuis1.add(new WuGui("小紫",99));
        //  public static<T extends Comparable<? super T>> void sort(List<T> list)对list集合进行排序
        // <T extends Comparable<? super T>>  接收 Comparable的子类 T的父类
        // T extends Comparable 只能接收 Comparable子类
        //  Comparable<? super T> 只能接收 T的父类
        Collections.sort(wuGuis1);// 如果不实现Comparable接口就会报编译错误
        for (WuGui wuGui : wuGuis1) {
            System.out.println(wuGui);
        }

        List<WuGui> wuGuis2=new ArrayList<>();

        wuGuis2.add(new WuGui("小绿",68));
        wuGuis2.add(new WuGui("小黄",28));
        wuGuis2.add(new WuGui("小蓝",18));
        wuGuis2.add(new WuGui("小红",101));
        wuGuis2.add(new WuGui("小紫",99));

        Collections.sort(wuGuis2,new WuGuiCompare());// 第二种实现方式 单独写了一个比较器
        for (WuGui wuGui : wuGuis2) {
            System.out.println(wuGui);
        }
    }
}

8.2 查找、替换(都是静态方法)

  • Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  • Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
  • Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
  • Object min(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最小元素
  • int frequency(Collection,Object):返回指定集合中指定元素的出现次数
  • void copy(List dest,List src):将src中的内容复制dest中
  • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
package Collection.Collections;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class CollectionsDemo03 {
    public static void main(String[] args) {
        List list = new ArrayList();

        list.add("tm");
        list.add("tomy");
        list.add("macker");
        list.add("jury");
        list.add("jury");
        list.add("jury");
        System.out.println(list);

        
        //Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
        System.out.println("根据元素的自然顺序,返回给定集合中的最大元素");
        System.out.println(Collections.max(list));

        //Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
        //比如,返回长度最大元素
        System.out.println("根据Comparator指定的顺序,返回给定集合中的最大元素");
        Object maxobj = Collections.max(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        System.out.println(maxobj);

        //Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
        System.out.println("根据元素的自然顺序,返回给定集合中的最小元素");
        System.out.println(Collections.min(list));
        //Object min(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最小元素
        System.out.println("根据Comparator指定的顺序,返回给定集合中的最小元素");
        Object minobj = Collections.min(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        System.out.println(minobj);

        //int frequency(Collection,Object):返回指定集合中指定元素的出现次数
        System.out.println("指定元素jury出现的次数:");
        System.out.println(Collections.frequency(list,"jury"));

        //void copy(List dest,List src):将src中的内容复制dest中
        System.out.println("将src中的内容复制dest中");
        ArrayList dest = new ArrayList();
        for (int i = 0; i < list.size(); i++) {
            dest.add(i);
        }
        Collections.copy(dest,list);
        System.out.println(dest);

        //boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
        System.out.println("使用新值“吉瑞”替换List对象的所有旧值“jury”");
        Collections.replaceAll(list,"jury","吉瑞");
        System.out.println(list);
    }
    
}

8.3 线程安全

之前我们说的Vector HashTable都是线程安全的,但是效率相对于ArrayList和HashMap集和来说效率比较低,我们有更好的策略去解决线程安全的问题

Collections.synchronizedList();方法

Collections.synchronizedMap();

Collections.synchronizedSet();

public class CollectionsTest01 {
    public static void main(String[] args) {
        //ArrayList集合不是线程安全的
        List<String> list=new ArrayList<>();

        //变成线程安全的
        Collections.synchronizedList(list);
    }
}

e 只能接收 Comparable子类
// Comparable<? super T> 只能接收 T的父类
Collections.sort(wuGuis1);// 如果不实现Comparable接口就会报编译错误
for (WuGui wuGui : wuGuis1) {
System.out.println(wuGui);
}

    List<WuGui> wuGuis2=new ArrayList<>();

    wuGuis2.add(new WuGui("小绿",68));
    wuGuis2.add(new WuGui("小黄",28));
    wuGuis2.add(new WuGui("小蓝",18));
    wuGuis2.add(new WuGui("小红",101));
    wuGuis2.add(new WuGui("小紫",99));

    Collections.sort(wuGuis2,new WuGuiCompare());// 第二种实现方式 单独写了一个比较器
    for (WuGui wuGui : wuGuis2) {
        System.out.println(wuGui);
    }
}

}


### 8.2 查找、替换(都是静态方法)

- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
- Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
- Object min(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最小元素
- int frequency(Collection,Object):返回指定集合中指定元素的出现次数
- void copy(List dest,List src):将src中的内容复制dest中
- boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值

```java
package Collection.Collections;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class CollectionsDemo03 {
    public static void main(String[] args) {
        List list = new ArrayList();

        list.add("tm");
        list.add("tomy");
        list.add("macker");
        list.add("jury");
        list.add("jury");
        list.add("jury");
        System.out.println(list);

        
        //Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
        System.out.println("根据元素的自然顺序,返回给定集合中的最大元素");
        System.out.println(Collections.max(list));

        //Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
        //比如,返回长度最大元素
        System.out.println("根据Comparator指定的顺序,返回给定集合中的最大元素");
        Object maxobj = Collections.max(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        System.out.println(maxobj);

        //Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
        System.out.println("根据元素的自然顺序,返回给定集合中的最小元素");
        System.out.println(Collections.min(list));
        //Object min(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最小元素
        System.out.println("根据Comparator指定的顺序,返回给定集合中的最小元素");
        Object minobj = Collections.min(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        System.out.println(minobj);

        //int frequency(Collection,Object):返回指定集合中指定元素的出现次数
        System.out.println("指定元素jury出现的次数:");
        System.out.println(Collections.frequency(list,"jury"));

        //void copy(List dest,List src):将src中的内容复制dest中
        System.out.println("将src中的内容复制dest中");
        ArrayList dest = new ArrayList();
        for (int i = 0; i < list.size(); i++) {
            dest.add(i);
        }
        Collections.copy(dest,list);
        System.out.println(dest);

        //boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
        System.out.println("使用新值“吉瑞”替换List对象的所有旧值“jury”");
        Collections.replaceAll(list,"jury","吉瑞");
        System.out.println(list);
    }
    
}

8.3 线程安全

之前我们说的Vector HashTable都是线程安全的,但是效率相对于ArrayList和HashMap集和来说效率比较低,我们有更好的策略去解决线程安全的问题

Collections.synchronizedList();方法

Collections.synchronizedMap();

Collections.synchronizedSet();

public class CollectionsTest01 {
    public static void main(String[] args) {
        //ArrayList集合不是线程安全的
        List<String> list=new ArrayList<>();

        //变成线程安全的
        Collections.synchronizedList(list);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值