Java 集合专题 图解 知识点整理/0基础快速入门(五)

没有看过之前笔记的小伙伴们可以看一下,我整理了非常详细的笔记对java面向对象编程的小白友好,可以帮你快速入门,或者是回顾面向对象的知识点

Java面向对象(初级)笔记整理/图解快速回顾/图解快速入门(—)

Java面向对象笔记整理/图解快速回顾/图解快速入门(二)

Java面向对象笔记整理/图解快速回顾/图解快速入门(三)

Java面向对象(高级)图解笔记整理/0基础快速入门(四)


本次我们讲解的是Java集合专题,集合是开发使用最多的技术之一,所以一定要掌握熟练。

如果觉得看的有些吃力,可以先看我之前整理的笔记。

一、集合是什么以及其作用

1、使用数组存储的劣势

当我们需要存储某些类的实例对象时,我们可以使用数组来存储:

public class Collection_01 {
    public static void main(String[] args) {
        //创建数组来存储Person类对象
        Person[] people = new Person[2];
        people[0] = new Person(10);
        people[1] = new Person(20);
    }
}
class Person {
    int age;
    //构造器
    public Person(int age) {
        this.age = age;
    }
}

如果是不同类的对象实现了同一个接口,或者是同一个类的子类,也可以使用数组存储:

public class Collection_01 {
    public static void main(String[] args) {
        //创建数组来存储Person类对象
        Person[] people = new Person[3];
        people[0] = new Person(10);
        people[1] = new Person(20);
        //Child类也可以存储进去
        people[2] = new Child(6,"a");
    }
}
class Person {
    int age;
    //构造器
    public Person(int age) {
        this.age = age;
    }
    public Person() {
    }
}
//继承Person类
class Child extends Person{
    String name;
    //构造器
    public Child(int age, String name) {
        super(age);
        this.name = name;
    }
}

但是可以看到,如果不是同一种类型,那么就不能使用这种数组的方式。

并且,这种数组的方式也不能够进行扩容,数组在一开始就需要进行容量的定义后续也不能进行扩容。

这就很不方便了,所以为了处理这种问题,Java的设计者们设计了集合类,从而更方便的存储对象和数据。

2、集合类的种类

集合类中有很多种类,它们分别有着不同的特点和优势:

Collection接口包含Set接口和List接口,它们存储的是单值。

 Map接口:存储的是键值对,即一个空间存一个K-V对,如【“语文” - 100】

 这个类图要在学习过程中谨记在心。我们接下来一个一个看。

二、Collection接口

1、Collection接口的基本使用

先来看一下Collection的基本使用方式,这里我们使用List接口中的ArrayList来举例:

import java.util.ArrayList;

public class Collection_01 {
    public static void main(String[] args) {
        Person tom = new Person("tom");
        Animal pig = new Animal("pig");
        //需要先创建对象
        ArrayList arrayList = new ArrayList();
        //使用add方法来添加对象 
        //可以添加对象,也可以添加数值和布尔值
        arrayList.add(tom);
        arrayList.add(pig);
        arrayList.add(10.0);
        arrayList.add(true);
       
    }
}
class Person{
    String name;
    //构造器
    public Person(String name) {
        this.name = name;
    }
}
class Animal{
    String name;
    //构造器
    public Animal(String name) {
        this.name = name;
    }
}

2、Collection接口的特点

实现Collection接口类的集合类有以下特点:

List接口集合类可以存放重复的元素,是有序的;

Set接口集合类不可以存放重复的元素,是无序的;

Collection接口没有直接的实现类,但是它的子接口Set和List有实现类。

3、Collection常用方法

import java.util.ArrayList;

public class Collection_01 {
    public static void main(String[] args) {
        Person tom = new Person("tom");
        Animal pig = new Animal("pig");
        //需要先创建对象
        ArrayList arrayList = new ArrayList();

        //使用add方法来添加对象
        arrayList.add(tom);
        arrayList.add(pig);
        arrayList.add(10.0);
        arrayList.add(true);

        //使用remove方法来删除对象
        //可以填入序号删除
        arrayList.remove(0);
        //也可以填入指定对象删除
        arrayList.remove(pig);

        //使用contains会判断是否有该对象存在在集合中
        System.out.println(arrayList.contains(10.0)); //返回true/false

        //使用size返回集合的大小,注意不是length()
        System.out.println(arrayList.size());

        //使用isEmpty判断是否为空集合
        System.out.println(arrayList.isEmpty());

        //使用addAll来批量添加
        ArrayList list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        //将list填入,即可添加所有list中的元素
        arrayList.addAll(list);

        //使用removeAll批量删除元素
        arrayList.removeAll(list);
    }
}
class Person{
    String name;
    //构造器
    public Person(String name) {
        this.name = name;
    }
}
class Animal{
    String name;
    //构造器
    public Animal(String name) {
        this.name = name;
    }
}

4、遍历Collection

这里我还是用Arraylist来讲解。

(1)迭代器Iterator

Collection接口中自带一个迭代器Iterator,我们可以通过这个迭代器来进行遍历:

//部分代码省略
public class Collection_01 {
    public static void main(String[] args) {
        Person tom = new Person("tom");
        Animal pig = new Animal("pig");
        //需要先创建对象
        ArrayList arrayList = new ArrayList();

        //使用add方法来添加对象
        arrayList.add(tom);
        arrayList.add(pig);
        arrayList.add(10.0);
        arrayList.add(true);

        //使用迭代器来遍历
        //1、获取迭代器对象
        Iterator iterator = arrayList.iterator();
        //2、使用迭代器的hasNext()方法来进行循环遍历
        //如果有下一个对象就继续执行循环
        while (iterator.hasNext()) {
            //.next()方法用于下移迭代器指针
            //并取出下一个对象
            Object next =  iterator.next();
            //打印输出对象
            System.out.println(next);
        }
    }
}

运行结果:

迭代器的原理图解:

如图是我们的Arraylist集合和iterator迭代器,

我们可以把iterator迭代器的next()方法看做一个指针,它的起始位置是指向第0个元素的(没有指向任何元素,只是在起点位置) 。

当我们调用hasnext()方法时,迭代器会进行判断,指针的下一个元素是否为空,如果不为空,就可以进行下一步操作。为空就直接退出循环了。

然后使用next()方法,这一步会让指针往下移动, 并取出下一个元素,如图:

 代码中我使用Object 引用类型来接收取出的元素:

while (iterator.hasNext()) {
           
//.next()方法用于下移迭代器指针
            //并取出下一个对象
            Object next =  iterator.next();
         
  //打印输出对象
            System.out.println(next);
        }

 然后打印对象,就是一个简单的遍历。

之后进行iterator.hasNext()方法进行判断,是否要进行下一步循环。

这就是iterator的遍历的过程。

注意,在使用next方法前,一定要使用hasNext方法,否则可能会出现NoSuchElementException异常。

如果要在后续第二次使用iterator遍历,就需要先把指针放到初始位置才可以重新遍历。

使用下面的代码来讲指针放回初始位置:

iterator = arrayList.iterator();

 也就是重新赋值一下。

如图是没有指针归位,和指针归位的的两种结果:

指针没有归位,只遍历了一次:

public class Collection_01 {
    public static void main(String[] args) {
        Person tom = new Person("tom");
        Animal pig = new Animal("pig");
        //需要先创建对象
        ArrayList arrayList = new ArrayList();

        //使用add方法来添加对象
        arrayList.add(tom);
        arrayList.add(pig);
        arrayList.add(10.0);
        arrayList.add(true);

        //使用迭代器来遍历
        //1、获取迭代器对象
        Iterator iterator = arrayList.iterator();
        //2、使用迭代器的hasNext()方法来进行循环遍历
        //如果有下一个对象就继续执行循环
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            //打印输出对象
            System.out.println(next);
        }

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

    }
}

指针归位:

public class Collection_01 {
    public static void main(String[] args) {
        Person tom = new Person("tom");
        Animal pig = new Animal("pig");
        //需要先创建对象
        ArrayList arrayList = new ArrayList();

        //使用add方法来添加对象
        arrayList.add(tom);
        arrayList.add(pig);
        arrayList.add(10.0);
        arrayList.add(true);

        //使用迭代器来遍历
        //1、获取迭代器对象
        Iterator iterator = arrayList.iterator();
        //2、使用迭代器的hasNext()方法来进行循环遍历
        //如果有下一个对象就继续执行循环
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            //打印输出对象
            System.out.println(next);
        }
        //指针归位,并重新遍历
        iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(next);
        }

    }
}

(2)增强for循环

IDEA快捷键使用大写的“I”(i)就会调用增强for循环。它可以用来遍历集合。

public class Collection_01 {
    public static void main(String[] args) {
        Person tom = new Person("tom");
        Animal pig = new Animal("pig");
        //需要先创建对象
        ArrayList arrayList = new ArrayList();

        //使用add方法来添加对象
        arrayList.add(tom);
        arrayList.add(pig);
        arrayList.add(10.0);
        arrayList.add(true);

        //使用增强for循环遍历:
        for (Object o : arrayList) {
            System.out.println(o);
        }
    }
}

看起来代码简洁了不少~

增强for循环的语法,我来讲解一下:

 for (Object o : arrayList) :这一步的含义就是将arrayList集合中的元素按顺序取出来一个并将其赋值给Object o这个引用。

 System.out.println(o); 这一步就是我们把o打印输出出来,实现对集合的遍历。

之后进行下一步循环的时候 ,增强for循环会自动按顺序取出下一个元素,并赋值给o,然后进行遍历操作,直到所有元素都被取出。

(这里的增强for循环,底层还是一个iterator迭代器)

5、List接口

List接口存放的内容是有序的单值,放入元素的顺序和取出元素的顺序一致,并且可以存放重复的元素。可以使用序号来取出元素。(而Set接口就不可以)

List接口是Collection接口下的子接口,它的实现子类有如下图的类别:

 Vector、ArrayList、LinkedList,它们的特点互不相同,我来一一讲解。

(1)List接口的方法

这里我们只讲一下List接口的特有方法,Collection的方法就不再赘述。

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

public class List_ {
    public static void main(String[] args) {
        ArrayList<Book> books = new ArrayList<>();
        Book a = new Book(10, "a");
        Book b = new Book(20, "b");
        Book c = new Book(30, "c");
        Book d = new Book(40, "d");
        Book e = new Book(50, "e");
        //添加元素
        books.add(a);
        books.add(b);
        books.add(c);
        books.add(d);
        books.add(e);
        books.add(c);
        //indexOf方法 获取元素第一次出现的序号
        int i = books.indexOf(c);
        System.out.println("c第一次出现的序号是" + i);
        //lastIndexOf方法 获取元素最后一次出现的序号
        int i1 = books.lastIndexOf(c);
        System.out.println("c最后一次出现的序号是" + i1);
        //set方法 将指定元素设置到指定的位置中 并覆盖原有的元素
        books.set(4, a);
        System.out.println(books.get(4).name);
        for (Book o :books) {
            System.out.println(o);
        }
        //sublist方法 获取[fromIndex,toIndex) 序号之间的子列表
        //依然是左闭右开区间 也就是,toIndex的值不获取
        List<Book> books1 = books.subList(1, 4);
        System.out.println("==================");
        for (Book book : books1) {
            System.out.println(book);
        }
    }
}
//自定义Book类
class Book {
    int price;
    String name;

    public Book(int price, String name) {
        this.price = price;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Book{" +
                "price=" + price +
                ", name='" + name + '\'' +
                '}';
    }
}

 (2)List接口的遍历方式

使用增强for循环、iterator迭代器和普通的for循环都可以遍历。

(3)ArrayList

ArrayList类的底层是使用数组实现的,它的查找和修改的效率更快,而当元素过多时,添加和删除的效率就会大大降低。

ArrayList可以添加null为元素,并且可以重复添加null。

ArrayList和Vector基本相同,但是ArrayList是线程不安全的,Vector是线程安全的,所以在多线程的环境下,不能使用ArrayList。

扩容机制:
  • 当我们使用无参构造器创建一个ArrayList对象时,它的容量是0,而当第一次添加时,就会扩容到10。
  • 而当我们使用有参构造器指定一个ArrayList对象的大小 a 时,容量就是指定的大小 a 。
  • 如果添加的元素数量超过了容量值,那么就会进行扩容,扩容到原来的1.5倍,即扩容到1.5a。使用无参构造器则是扩容到15。需要再扩容则会扩容到15*1.5 = 22(向下取整)

(4)Vector 

Vector可以说和ArrayList用法特点基本一样,但Vector是线程安全的,实现了synchronized接口,ArrayList是线程不安全的。

扩容机制:
  • 使用无参构造器创建Vector对象,初始容量是10
  • 使用有参构造器创建Vector对象,初始容量是指定的长度
  • 如果容量不够,需要扩容,一次扩容到原先长度的两倍

(5)LinkedList

LinkedList类的底层是使用双链表来存储数据的,双链表就是使用指针指向前后数据的一种数据结构。双链表中的数据就存放在这一个一个结点中。

当双链表需要添加一个新的结点时,只需要在尾部添加一个结点即可。

具体步骤就是

1将尾指针指向的结点的后指针指向新的结点:

2将新结点的前指针指向尾指针指向的节点:

3将尾指针(last)指向当前节点的后一个结点(也就是新结点):

 这样就可以完成添加新结点的步骤。

正式因为有这样的链表机制,LinkedList不需要扩容,只要再添加时把末尾处连接即可。

而删除机制则是将连接断开,假设现在我想要断开中间某个结点:

 1将该结点的前一个结点的后指针指向该结点的后一个结点:

2将该结点的后一个结点的前指针指向该结点的前一个结点:

 

 3将该结点的前后指针置空:

这样就删除了这个结点:

所以可见,LinkedList对于存储元素的添加和删除比较方便,效率较高。 

而Vector和ArrayList则是数组存储的,查找和修改效率较高。

(6)如何选择使用List接口类? 

当我们存储的值是有顺序的、需要有重复值的,并且是单值的情况,我们使用List接口类;

当单线程情况下,集合里的元素经常需要修改和查找则选择ArrayList;

当单线程情况下,集合里的元素经常需要添加和删除则选择LinkedList;

当需要线程同步,多线程情况下,选择Vector。

6、Set接口

Set接口类的关系图如下:

常用的有TreeSet和HashSet还有LinkedHashSet(图中未标出)。

Set接口特点就是存储的元素都是无序的(集合),并且不可以重复。

(1)Set接口的方法

常用方法和Collection一样,这里不赘述。List接口的方法Set接口基本没有。(因为是无序)

(2)Set接口的遍历方法

1、可以使用迭代器iterator

2、可以使用增强for循环

3、不可以使用普通for循环,因为没有序号,是一个无序的集合

(3)HashSet

HashSet底层本质上是Hashmap,HashSet的扩容机制其实就是Hashmap的机制。

扩容机制(难、重):

Hashmap底层有一个存储链表的数组表Table表,如图

当我们往Set集合类中存放元素时,首先会根据这个元素的地址来进行hashcode的计算,每一个地址都对应着一个hashcode,不同的地址计算出来的hashcode值可能一样,也可以不一样

由hashcode值会转换为一个Table表的索引值,如果该Table表索引值的位置有元素,就要判断这两个元素是否是同一个元素,如果相同就取消添加,因为不可以有重复元素。如果不同,就在这个索引的位置往后一个结点进行判断,如果相同就取消添加,如果不同就再往后找,直到链表中所有的元素都被遍历到了都不相同,那么就再链表后添加。

这里用于比较元素是否相同的方法时equals方法,equals方法可以由程序员自己编写进行判断。

看图举例:

 

这就是HashSet的一个基本存放机制。 

Table表的容量在初始状态下是0,放入第一个元素时扩容到16。

HashMap中有个属性loadFactor(加载因子) = 0.75 ,当Table表中有数据的空间达到了容量的0.75倍时,就会进行扩容——扩容到原来的2倍

例如:

现在的Table表容量是16,而现在已经有11个空间都被占了,现在我再往里面添加一个值,如果正好hashcode对应的是一个新的索引,也就是会占据第12个空间(16*0.75),那么Table表就要进行扩容了,扩容到32容量。

而当Table的容量达到了64,扩容机制就有新的变化,当数组中的某一条链表的长度达到了8个,并且Table的容量大于或等于64,那么这条链表就会转换为一颗红黑树,便于查找和遍历。

 红黑树的内容属于数据结构的知识,现在就先记住就可以

总结:

1、添加元素时会根据元素的地址得到hashcode,根据hashcode得到索引值

2、如果索引值处没有元素,则直接添加;有元素则依次使用equals方法比较,都不同就添加到最后;有相同的就放弃添加。

3、Table表初始为0,第一次添加变为16,当存储量达到容量0.75倍时就扩容到原先的2倍(16->32)

4、如果链表的长度达到了8,并且Table表的容量大于等于64,那么链表就会进行树化。

取出的次序:

Hashset的取出顺序和我们放入的次序一般是不一致的:

import java.util.HashSet;

public class Set_ {
    public static void main(String[] args) {
        //添加顺序如下
        HashSet hashSet = new HashSet();
        hashSet.add(111);
        hashSet.add(222);
        hashSet.add("aaa");
        hashSet.add("bbb");
        hashSet.add("abc");
        hashSet.add(1.233);
        hashSet.add(true);
        //遍历取出
        for (Object o : hashSet) {
            System.out.println(o);
        }
    }
}

运行结果: 

这是由于HashSet的取出顺序是按照Table表的存储顺序来取出的,首先取出Table表中第一个位置的链表中的所有元素,然后就是第二个位置的链表中的所有元素,以此类推如图:

而我们知道,添加的元素在Table表中的位置是根据hashcode的值来判断的,而每一个元素的hashcode都不相同,那么这样,取出的顺序能否和放入的顺序一致,就十分看缘分了,除非,所有的元素的hashcode值都相同,不然它们存放的位置随机,就很难保持取出顺序的一致性。 

如果我们想要在Set集合中有取出顺序和放入顺序一致的特性,那我们可以使用LInkedHashset

(4)LinkedHashset

LInkedHashset是用数据+双向链表来存储的,它的底层是LinkedHashMap,它的特点就是除了使用链表存储之外,他还是双向链表,也就意味着在Table表中的每个链表的头结点和尾结点都可以多指向一个代表其添加次序的结点。

看图就明白了,假如我是按照数字顺序来添加的,相应的,结点的前后指针也是按照次序来指向的:

在遍历取出的时候,取出的次序也是按照各结点的指针次序遍历的,所以就会按照放入的次序取出。

(5)TreeSet

TreeSet的使用方法和其他Set接口的方法基本一致,但是TreeSet比较特殊的一点是,它可以由程序员自己定义一个排列顺序,从而让TreeSet里边的数据进行相应的排列。

举个例子

import java.util.Comparator;
import java.util.TreeSet;

public class TreeSet_ {
    public static void main(String[] args) {
        //在创建treeSet对象时
        // 在构造器中传入一个Comparator接口的匿名内部类
        //在这个匿名内部类中可以指定排列的顺序
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //使用字符串的长度进行排列:
                String str1 = (String) o1;
                String str2 = (String) o2;
                //treeSet会根据compare返回的值,来进行排序
                //如果返回的是正值,则会认为o1元素排在o2元素之前
                //在这个比较下则是长度长的在前,短的在后
                return str1.length() - str2.length();
            }
        });
        
        //添加元素
        treeSet.add("abc");
        treeSet.add("cccccccccc");
        treeSet.add("bbbbb");
        treeSet.add("aaaaaaaaaaaaa");

        //遍历输出
        for (Object o : treeSet) {
            System.out.println(o);
        }
    }
}

运行结果:

 

也就是通过这种方式,也保证了集合输出的排序性,并且是可以程序员自定义的。

7、Map接口

Map接口是与Collection接口平级的,如图:

Map接口和Collection接口最大的区别就是Collection接口类存储的都是单值,而Map接口类存储的是键值对,数据成对存在。

这个键值对被称为key - value,key是键,value是值,在Map里就是存放着这一个个的键值对。

(1)Map的基本使用方法

由于Map接口无法直接进行实例化,所以我们使用HashMap类来进行举例,

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

public class Map_ {
    public static void main(String[] args) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //使用put添加键值对
        hashMap.put(1,"aaa");
        hashMap.put(2,"bbb");
        hashMap.put(3,"ccc");
        hashMap.put(4,"ddd");

        //遍历hashMap 后面详细讲
        Set keySet = hashMap.keySet();
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(hashMap.get(next));
        }
    }
}

运行结果:

这里的key - value键值中的key的值是不可以重复的,因为HashMap的Table表中会检测到重复,从而添加不进去。(前面讲HashSet也已经讲过了,原理是一样的。)

但是value是可以重复添加的

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

public class Map_ {
    public static void main(String[] args) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //使用put添加键值对
        hashMap.put(1,"aaa");
        hashMap.put(2,"aaa");
        hashMap.put(3,"aaa");
        hashMap.put(4,"aaa");

        //遍历hashMap 后面详细讲
        Set keySet = hashMap.keySet();
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(hashMap.get(next));
        }
    }
}

运行结果:

这里,key的值一般使用String来充当,而key的值也可以为null。但是只能有一个null的值。

而value的值也可以是null,而且value的null值可以存在多个。

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

public class Map_ {
    public static void main(String[] args) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //使用put添加键值对 key可以有一个null 
        //value可以有多个null
        hashMap.put(1,"aaa");
        hashMap.put(null,"aaa");
        hashMap.put(3,null);
        hashMap.put(4,null);

        //遍历hashMap
        Set keySet = hashMap.keySet();
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(hashMap.get(next));
        }
    }
}

运行结果:

(2)Map接口类底层原理图(难、重)

先来看这张图,这张图中展示了HashMap中存放的两个内部的Set集合,分别是keySet和values,这两个分别存放key们(的地址)和value们

而一个这里面的一个key-value对,就被称为一个Entry:

keySet存放的是key的地址,那么真正的key存放在哪里呢?

在HashMap中,一对真正的key - value对象是放在一个叫做Node的节点中:

 遍历HashMap的时候,我们通过获取到keySet中的地址,来获取到Node结点中的真正的key-Value数据,从而完成遍历。

面试时,千万不要回答,keySet里面存放的就是真正的key就可以了。

(3)Map接口的方法

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

public class Map_ {
    public static void main(String[] args) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //put()方法 添加键值对
        hashMap.put("1", 111);
        hashMap.put("2", null);
        hashMap.put("3", true);
        hashMap.put(null, null);
        hashMap.put("4", "abc");

        //remove()方法 根据key的值来进行删除
        hashMap.remove("2");

        //get()方法 根据key的值来获取Value的值
        Object o = hashMap.get("1");
        System.out.println(o); //111

        //size()方法 获取元素个数
        int size = hashMap.size();
        System.out.println("大小是 " + size); //4 (remove删除了一个)

        //isEmpty()方法 判断元素个数是否为0
        boolean empty = hashMap.isEmpty();
        System.out.println(empty); //false

        //clear()方法 清除所有元素(谨慎使用)
        //hashMap.clear();

        //containsKey
        boolean b = hashMap.containsKey("2");
        System.out.println(b); //false 已经被remove


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

        //keySet()方法 获取到HashMap的keySet
        Set keySet = hashMap.keySet();

        //使用迭代器遍历keySet 并使用get()方法来取出value值
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(next + " - " + hashMap.get(next));
        }
    }
}

(4)Map接口的遍历方法

这里插播一下增强for和迭代器iterator的快捷键:

增强for:“大写的 i ”

迭代器iterator: i t i t (需要获取到迭代器之后才可以用,不然没有用)

方法一

通过获取keySet并获取到keySet的iterator迭代器进行遍历,在循环中使用HashMap的get()方法取出value值:

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

public class Map_ {
    public static void main(String[] args) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //put()方法 添加键值对
        hashMap.put("1", 111);
        hashMap.put("2", null);
        hashMap.put("3", true);
        hashMap.put(null, null);
        hashMap.put("4", "abc");

        //keySet()方法 获取到HashMap的keySet
        Set keySet = hashMap.keySet();

        //使用迭代器遍历keySet 并使用get()方法来取出value值
        Iterator iterator = keySet.iterator();
        //如果iterator有下一个元素,就取出并执行while
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(next + " - " + hashMap.get(next));
        }
    }
}
方法二

取出keySet后使用增强for循环遍历,其他与方法一一样。

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

public class Map_ {
    public static void main(String[] args) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //put()方法 添加键值对
        hashMap.put("1", 111);
        hashMap.put("2", null);
        hashMap.put("3", true);
        hashMap.put(null, null);
        hashMap.put("4", "abc");

        //keySet()方法 获取到HashMap的keySet
        Set keySet = hashMap.keySet();

        //增强for循环进行遍历 并使用get()方法来取出value值
        for (Object o : keySet) {
            Object o1 = hashMap.get(o);
            System.out.println(o + "-" + o1);
        }

    }
}
方法三、四

通过values()方法,直接将所有的value取出。 然后通过迭代器或者增强for取出。

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;

public class Map_ {
    public static void main(String[] args) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //put()方法 添加键值对
        hashMap.put("1", 111);
        hashMap.put("2", null);
        hashMap.put("3", true);
        hashMap.put(null, null);
        hashMap.put("4", "abc");

        //通过values()方法取出value们
        Collection values = hashMap.values();

        //通过迭代器遍历 values
        Iterator iterator = values.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(next);
        }
    }
}
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;

public class Map_ {
    public static void main(String[] args) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //put()方法 添加键值对
        hashMap.put("1", 111);
        hashMap.put("2", null);
        hashMap.put("3", true);
        hashMap.put(null, null);
        hashMap.put("4", "abc");

        //通过values()方法取出value们
        Collection values = hashMap.values();

        //增强for
        for (Object o : values) {
            System.out.println(o);
        }
    }
}
方法五

通过EntrySet获取到key - value。然后通过Entry中自带的getKey和getValue方法来获取key - value

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) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //put()方法 添加键值对
        hashMap.put("1", 111);
        hashMap.put("2", null);
        hashMap.put("3", true);
        hashMap.put(null, null);
        hashMap.put("4", "abc");

        //获取entrySet
        Set set = hashMap.entrySet();
        
        //通过迭代器或者增强for循环都可以
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            //把 Object next 转换为 Entry类型
            // (不然没办法使用Entry的方法)
            Map.Entry entry = (Map.Entry) next;
            //打印输出
            System.out.println(entry.getKey() + 
                    " - " + entry.getValue());
        }
    }
}

以上的方法哪个简单就用哪个。

(5)HashMap

HashMap的特点

1、不能保证取出的顺序一致,因为底层依然是table表。

2、不能实现线程同步,也就是没有实现synchronized接口,线程不安全。

3、如果出现了相同的key值,那么添加的时候就会把原先的key对应的value值覆盖掉:

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) {
        //创建示例对象
        HashMap hashMap = new HashMap();
        //put()方法 添加键值对
        //有重复的key值
        hashMap.put("1", 111);
        hashMap.put("2", null);
        hashMap.put("1", true);

        //遍历hashmap
        Set set = hashMap.entrySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            Map.Entry entry = (Map.Entry) next;
            System.out.println(entry.getKey() +
                    " - " + entry.getValue());
        }
    }
}

运行结果:

HashMap底层机制分析总结(重、难)

之前在将HashSet的时候其实讲的就是HashMap的原理,这里再次总结一下:

我们知道key的真正的值放在了Node中,而Node就存放在Table表中,这个keySet就会使用到之前讲过的HashSet的机制。

在添加key - value时,会根据hashcode的值进行索引值的计算,然后根据索引值放入到table表中,如果索引值已经被占有,则通过equals方法进行判断如果是相同的key那么就把value的值替换进去(这一点要注意),如果不相同,那么就在链表后面添加进去。

而扩容机制也是与之前讲过的HashSet的机制也是一模一样的。

Table表的初始容量是16,加载因子是0.75 (16*0.75 = 12),当key的值占了超过12个,那么就会按照两倍进行扩容,扩容到32容量(32*0.75 = 24),当key的值占了超过24个后,就会再扩容两倍到64容量,以此类推。

如果Table表中的某条链表的结点个数超过了8,并且Table表的容量大于等于64,那么该链表就会进行树化。(跟之前一模一样的)

总结:

1、Table表中存放的是Node,而一个Node中存放着一个key - value对

2、添加元素时会根据元素的地址得到hashcode,根据hashcode得到索引值

3、如果索引值处没有元素,则直接添加;有元素则依次使用equals方法比较,都不同就添加到最后;有相同的就放弃添加。

4、Table表初始为0,第一次添加变为16,当存储量达到容量0.75倍时就扩容到原先的2倍(16->32)

5、如果链表的长度达到了8,并且Table表的容量大于等于64,那么链表就会进行树化。

(6)Hashtable

特点:

1、Hashtable是线程安全的,可以实现线程同步

2、Hashtable中存放的key - value的值,都不可以为null

3、Hashtable使用的方法和HashMap一致

4、Hashtable的key值如果添加时相同,就会替换掉原来的value值

5、Hashtable的效率相比于HashMap较低

(7)Properties

Properties类继承的是Hashtable类,并且实现了Map接口,也是存放键值对的集合。

使用方法与Hashtable类似。

Properties可以从xxx.properties文件中加载数据到Properties类对象中,从而进行数据的读写。

如,我在工程项目中添加一个Properties文件:

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class Properties_ {
    public static void main(String[] args) throws IOException {
        //创建Properties对象
        Properties properties = new Properties();
        //使用load方法 从src中读取配置文件
        properties.load(new FileInputStream("src\\users.properties"));
        //输出
        System.out.println(properties);
    }
}

 运行结果:

(8)TreeMap 

TreeMap和TreeSet一样,都可以传入一个比较器来进行排序,TreeMap排序的是key的值,key经过排序以后,value相应的也就排好了。

排序的方法和TreeSet一样,这里不作赘述。

三、开发中如何来选择集合类

首先判断存储的数据是单值还是键值对

如果是单值:就选择Collection接口下的集合类

如果是键值对:就选择Map接口下的集合类

(1)如果确定是单值,就要再看是否允许重复元素的存在:

如果允许存在重复元素,就选择List接口下的集合类

        增删多:LinkedList

        改查多:ArrayList

        需要线程同步:Vector

如果不允许重复元素,就选择Set接口下的集合类

        无序:HashSet

        自定义排序:TreeSet

        取出和放入顺序一致:LinkedHashset

(2)如果确定是键值对,就要看以下的条件:

        键无序:HashMap

        键自定义排序:TreeMap

        键取出和放入顺序一致:LInkedHashMap

        线程同步:Hashtable

        读取文件:Properties

四、泛型

1、什么是泛型?

泛型和集合密切相关,集合要搭配泛型使用才能更好的处理内部的元素。

泛型可以认为是数据类型的变量,它存储的值就是数据类型本身,如泛型 <T> ,我在传入参数的时候可以传入数据类型 String,这样,<T>这个泛型就代表了String类型。

而在集合中,我们使用泛型,就可以很好的约束集合中存放的元素的数据类型:

如,我想要在一个集合中全部存放Dog类的对象,而不想要存放Cat对象,这时就可以使用泛型:

而对于集合遍历调用的方面泛型也帮助我们减少了代码量:

所以有了泛型,对于集合的元素的管理,以及遍历就会更加方便。不仅如此,泛型在其他方面也加速了开发的效率。 

2、自定义泛型类

我们可以在类的定义后面,定义一些泛型,从而在创建类对象时,将泛型指定:

public class generic_ {
    public static void main(String[] args) {
        //创建对象时可以指定泛型的类型:
        //这里指定的是String
        Dog<String> stringDog = new Dog<>("aaa", "小黄");
    }
}
//自定义泛型类
class Dog<T> {
    T t;
    String name;

    public Dog(T t, String name) {
        this.t = t;
        this.name = name;
    }
}

那么这样做有什么实际意义呢?

我们定义一个Box类,它可以存放任何数据类型,这时我们使用泛型来操作就很灵活:

public class Box<T> {
    private T t; 
    
    //可以设置t为任意的类型,存储在Box里
    public void set(T t) {
        this.t = t;
    }
    
    //也可以获取t,通过get方法返回一个T类型
    //而T类型就是传入进来的对象的类型
    public T get() {
        return t;
    }
}

可见,如果我们不使用泛型,就不知道传入的对象时什么类型的对象,这样我们在使用get方法来获取该对象时,返回值的类型就很难写了,因为不清楚传入的对象是什么类型的对象,所以自然写不了返回值的类型。

使用细节:

泛型类的类型在创建对象时确定

如果没有指定类型,则默认为Object类型

普通成员可以使用泛型

静态成员不可以使用泛型(因为静态成员是和类相关的,类加载时,对象还没有创建,泛型也还没有指定,所以就不可以使用)

使用泛型的数据不可以初始化

3、自定义泛型方法

除了类以外,方法还可以自定义一个泛型,但是要注意,有泛型方法的类,不一定是自定义泛型类,使用到泛型的方法,也不一定是自定义泛型方法。

如上面的代码中,get()方法并使用到了泛型T,但是并不是一个自定义泛型方法,因为它没有自己定义一个泛型。

自定义泛型方法应该是在方法的定义中自己定义一个泛型:

  // 定义一个泛型方法 swap,它接受一个数组 arr 和两个要交换的元素的索引 i 和 j
  //通过泛型,我们就可以交换任意两个元素,不管交换的对象类型是什么
  public static <T> void swap(T[] arr, int i, int j) {
      T temp = arr[i];
      arr[i] = arr[j];
      arr[j] = temp;
  }

使用细节:

可以使用在普通类中,也可以使用在泛型类中 

泛型方法被调用时,泛型便会确定

4、自定义泛型接口 

接口也可以自定义泛型:

public interface AAA <T,R>{
}

细节:

接口中,静态成员不可以使用泛型。

接口中泛型的类型在继承接口或者实现接口的时候确定 

如果没有指定类型,则默认为Object类型

5、使用细节

泛型在传入时必须传入一个数据类型,而不能是一个基本数据类型(除了String):

 int,double等类型都不可以添加,但是其包装类可以:

6、泛型的继承和通配符 

(1)泛型不具备继承性

List <Object> list = new ArrayList<String>(); //错误

(2)<?> 

<?>泛型支持任意的泛型类型,例如,我想要打印出来所有的List,并且List里存放的类型可以是任意类型,这时候就可以使用到<?>。

import java.util.Arrays;
import java.util.List;

public class WildcardExample {

    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);
        List<String> stringList = Arrays.asList("hello", "world", "java");
        List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);

        printList(intList);
        printList(stringList);
        printList(doubleList);
    }

    // 使用通配符 ? 接受任意类型的 List
    public static void printList(List<?> list) {
        for (Object elem : list) {
            System.out.println(elem);
        }
        System.out.println(); // 打印一个空行,用于分隔不同的列表
    }
}

(3)<? extends A> 表示泛型必须是A泛型的子类

(4)<? super B>表示泛型必须是B泛型的父类


终于写完了我去,如果大家觉得有帮助可以点赞鼓励我一下o( ̄▽ ̄)d。

下一次整理多线程、IO流的内容。请关注我跟进后续更新!

  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值