List、Set、Map 集合相关笔记

List、Set、Map 集合的遍历方式:

###数据元素是怎样在内存中存放的?
 主要有2种存储方式:
 
1、顺序存储,Random Access(Direct Access):
- 这种方式,相邻的数据元素存放于相邻的内存地址中,整块内存地址是连续的。可以根据元素的位置直接计算出内存地址,直接进行读取。读取一个特定位置元素的平均时间复杂度为O(1)。正常来说,只有基于数组实现的集合,才有这种特性。Java中以ArrayList为代表。

2、链式存储,Sequential Access:
- 这种方式,每一个数据元素,在内存中都不要求处于相邻的位置,每个数据元素包含它下一个元素的内存地址。不可以根据元素的位置直接计算出内存地址,只能按顺序读取元素。读取一个特定位置元素的平均时间复杂度为O(n)。主要以链表为代表。Java中以LinkedList为代表。

###每个遍历方法的实现原理是什么?
1、传统的for循环遍历,基于计数器的:
- 遍历者自己在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后,停止。主要就是需要按元素的位置来读取元素。

2、迭代器遍历,Iterator:
- 每一个具体实现的数据集合,一般都需要提供相应的Iterator。相比于传统for循环,Iterator取缔了显式的遍历计数器。所以基于顺序存储集合的Iterator可以直接按位置访问数据。而基于链式存储集合的Iterator,正常的实现,都是需要保存当前遍历的位置。然后根据当前位置来向前或者向后移动指针。

3、foreach循环遍历:
- 根据反编译的字节码可以发现,foreach内部也是采用了Iterator的方式实现,只不过Java编译器帮我们生成了这些代码。

###各遍历方式的适用于什么场合?
1、传统的for循环遍历,基于计数器的:
- 顺序存储:读取性能比较高。适用于遍历顺序存储集合。
- 链式存储:时间复杂度太大,不适用于遍历链式存储的集合。

2、迭代器遍历,Iterator:
- 顺序存储:如果不是太在意时间,推荐选择此方式,毕竟代码更加简洁,也防止了Off-By-One的问题。
- 链式存储:意义就重大了,平均时间复杂度降为O(n),还是挺诱人的,所以推荐此种遍历方式。

3、foreach循环遍历:
- foreach只是让代码更加简洁了,但是他有一些缺点,就是遍历过程中不能操作数据集合(删除等),所以有些场合不使用。而且它本身就是基于Iterator实现的,但是由于类型转换的问题,所以会比直接使用Iterator慢一点,但是还好,时间复杂度都是一样的。所以怎么选择,

Person类:

package com.jihe;

/**
 * @author 黄嘉诚
 * @version 1.0
 * @date 2020/3/25 16:09
 */
public class Person {
    private int id;
    private  String name;
    private  String address;

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

    public Person(int id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

List:

package com.jihe;

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

/**
 * @author 黄嘉诚
 * @version 1.0
 * @date 2020/3/25 16:03
 */
public class ListTest {

    public static void main(String[] args) {

        List<Person> list = new ArrayList<>();

        Person p1 = new Person(1,"黄嘉诚","武汉");
        Person p2 = new Person(2,"小明","大冶");
        Person p3 = new Person(3,"小红","黄石");

        list.add(p1);
        list.add(p2);
        list.add(p3);

        //list.remove(p3); //删除

        Iterator<Person> iter = list.iterator();

        //方式一:迭代器
        //Iterator(迭代器)是一个接口,它的作用就是遍历容器的所有元素。https://www.jianshu.com/p/0ed323290bcf
        while (iter.hasNext()){
            Person person = iter.next();
            System.out.println(person.toString()); // 封装类里面必须设置toString方法
        }
        System.out.println("-------------------");

        //方式二:for循环
        for (int i = 0; i <list.size() ; i++) {
            Person person = list.get(i);
            System.out.println(person.toString());   
        }
        System.out.println("-------------------");
        
        //方式三:增强for循环
        for (Person p:list) {
            System.out.println(p);
        }

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

        Person p4 = new Person(1,"123","123");

        // 列表的截取功能 从指定位置开始截取到指定位置结束
        System.out.println(list.subList(1,2));

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

        //获取指定的元素
        System.out.println(list.get(0));
    }
}

Set:

package com.jihe;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * @author 黄嘉诚
 * @version 1.0
 * @date 2020/3/25 17:25
 */
public class SetTest {

    public static void main(String[] args) {

        Set<Person> set = new HashSet<>();

        Person p1 = new Person(1,"huang","111");
        Person p2 = new Person(2,"li","222");
        Person p3 = new Person(3,"wu","333");

        set.add(p1);
        set.add(p2);
        set.add(p3);

        Iterator iterator = set.iterator();

        // 1.迭代器
        while (iterator.hasNext()){
            Person p = (Person) iterator.next();
            System.out.println(p);
        }

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

        //2.增强for
        for (Person p : set) {
            System.out.println(p);
        }
    }
}

Map:

package com.jihe;

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

/***
 * 增: add(object obj)
 * 删: remove(int index) / remove(object obj)
 * 改: set(int index, Object ele)
 * 查: get(int index)
 * 插: add(int index, Object ele)
 * 长度: size()
 * 遍历:
 *       ①Iterator迭代器方式
 *       ②增强for循环
 *       ③普通的循环
 */
public class MapTest {

    public static void main(String[] args) {

        Map<String,String> map = new HashMap<>();

        map.put("1","a");
        map.put("2","b");
        map.put("3","c");

        //1.迭代 Iterator遍历获取,然后获取到Map.Entry,再得到getKey()和getValue()
        Iterator iterator = map.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry entry = (Map.Entry) iterator.next();
            System.out.println(entry.getKey()+"---"+entry.getValue());
        }

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

        //2.通过Map.Entry(String,String) 获取,然后使用entry.getKey()获取到键,通过entry.getValue()获取到值
        for (Map.Entry<String,String> entry: map.entrySet()){
            System.out.println(entry.getKey()+"---"+entry.getValue());
        }

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

        //3.通过 map.keySet() 获取key  通过key 找到value
        for (String key: map.keySet() ) {
            System.out.println(key+"---"+map.get(key));
        }
    }
}

HashMap中使用自定义类作为Key时,为何要重写HashCode和Equals方法:

首先,如果我们直接用以下的Person类作为键,存入HashMap中,会发生发生什么情况呢?

Person类:

package com.jihe;

/**
 * @author 黄嘉诚
 * @version 1.0
 * @date 2020/3/25 16:09
 */
public class Person {
    private int id;
    private  String name;
    private  String address;

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

    public Person(int id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }
}

HashMapDemo:

package com.jihe;

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

/**
 * 案例:自定义对象作为Map的键。
 *
 *底层是哈希表数据结构,线程是不同步的,可以存入null键,null值。
 * 要保证键的唯一性,需要覆盖hashCode方法,和equals方法。
 */
public class HashMapDemo {

    public static void main(String[] args) {

        HashMap<Person,String> map = new HashMap<Person,String>();

        map.put(new Person(1,"huang","武汉"),"value1");
        map.put(new Person(2,"li","北京"),"value2");
        map.put(new Person(3,"jack","上海"),"value4");
        map.put(new Person(4,"rose","南京"),"value3");
        map.put(new Person(1,"huang","武汉"),"value11");
        
        System.out.println(map.toString());

        System.out.println(map.get(new Person(1, "huang", "武汉")));
        System.out.println(map.get(new Person(2, "li", "北京")));
        System.out.println(map.get(new Person(3, "jack", "上海")));
        System.out.println(map.get(new Person(4,"rose","南京")));

    }
}

输出结果:

{Person{id=1, name='huang', address='武汉'}=value1, Person{id=1, name='huang', address='武汉'}=value11, Person{id=3, name='jack', address='上海'}=value4, Person{id=2, name='li', address='北京'}=value2, Person{id=4, name='rose', address='南京'}=value3}
null
null
null
null

我们可以看到,这里出现了两个问题:

  1. 在添加的过程中,我们将 key=new Person(1,“huang”,“武汉”) 的键值对添加了两次,那么在期望中,HashMap中应该只存在一对这样的键值对,因为key(期望中)是相同的,所以不应该重复添加,第二次添加的value="value11"应该替换掉原先的value=“value1”。但是在输入中,我们发现期望中的情况并没有出现,而是在HashMap同时存在了
  2. 在获取value值时,我们分别用四个Person对象去查找,这四个对象和我们刚刚存入的四个key值(在期望中)是相同的,但是查找出的却是四个null值,这显然也是错误的。

那么,正确的方法其实在很多地方都是被描述过了,直接对Person类进行修改,重载equals和hashCode方法,修改过后的Person类如下:

package com.jihe;

/**
 * @author 黄嘉诚
 * @version 1.0
 * @date 2020/3/25 16:09
 */
public class Person {
    private int id;
    private  String name;
    private  String address;

    // 重写hashcode
    @Override
    public int hashCode() {
        int result = 31;
        result = 37 * result + id;
        result = 37 * result + name.hashCode();
        result = 37 * result + address.hashCode();
        return result;
    }

    //重写equals
    @Override
    public boolean equals(Object obj) {
        //instanceof 运算符只能用作对象的判断,测试一个对象是否为一个类的实例
        Person p = (Person) obj;
        return obj instanceof Person &&
                this.id == p.id &&
                this.name.equals(p.name) &&
                this.address.equals(p.address);
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
    
    public Person(int id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }
}

HashMapDemo类:

package com.jihe;

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

/**
 * 案例:自定义对象作为Map的键。
 *
 *底层是哈希表数据结构,线程是不同步的,可以存入null键,null值。
 * 要保证键的唯一性,需要覆盖hashCode方法,和equals方法。
 */
public class HashMapDemo {

    public static void main(String[] args) {

        HashMap<Person,String> map = new HashMap<Person,String>();

        map.put(new Person(1,"huang","武汉"),"value1");
        map.put(new Person(2,"li","北京"),"value2");
        map.put(new Person(3,"jack","上海"),"value4");
        map.put(new Person(4,"rose","南京"),"value3");
        map.put(new Person(1,"huang","武汉"),"value11");


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

        System.out.println(map.get(new Person(1, "huang", "武汉")));
        System.out.println(map.get(new Person(2, "li", "北京")));
        System.out.println(map.get(new Person(3, "jack", "上海")));
        System.out.println(map.get(new Person(4,"rose","南京")));

        // 迭代遍历
        Set<Map.Entry<Person,String>> entrySet = map.entrySet();
        Iterator<Map.Entry<Person,String>> iterator = entrySet.iterator();
        while (iterator.hasNext()){
            Map.Entry<Person,String> next= iterator.next();
            System.out.println(next.getKey()+"---"+next.getValue());
        }
    }
}

运行结果:

{Person{id=3, name='jack', address='上海'}=value4, Person{id=4, name='rose', address='南京'}=value3, Person{id=1, name='huang', address='武汉'}=value11, Person{id=2, name='li', address='北京'}=value2}
value11
value2
value4
value3
Person{id=3, name='jack', address='上海'}---value4
Person{id=4, name='rose', address='南京'}---value3
Person{id=1, name='huang', address='武汉'}---value11
Person{id=2, name='li', address='北京'}---value2

可以看到,之前指出的亮点错误都得到了改正。那么,为什么会这样呢?

在HashMap中,查找key的比较顺序为:

  1. 计算对象的Hash Code,看在表中是否存在。
  2. 检查对应Hash Code位置中的对象和当前对象是否相等。

显然,第一步就是要用到 hashCode() 方法,而第二步就是要用到 equals() 方法。在没有进行重写时,在这两步会默认调用 Object 类的这两个方法,而在 Object 中,Hash Code 的计算方法是根据对象的地址进行计算的,那两个 Person(1, “huang”, “武汉”) 的对象地址是不同的,所以它们的Hash Code也不同,自然HashMap也不会把它们当成是同一个 key 了。同时,在 Object 默认的 equals() 中,也是根据对象的地址进行比较,自然一个 Person(1, “huang”, “武汉”) 和另一个 Person(1, “huang”, “武汉”) 是不相等的,所以会同时出现两个相同的key。

理解了这一点,就很容易搞清楚为什么需要同时重写hashCode()和equals两个方法了。

  • 重写 hashCode() 是为了对同一个key,能得到相同的Hash Code,这样HashMap就可以定位到我们指定的key上。
  • 重写 equals() 是为了向HashMap表明当前对象和key上所保存的对象是相等的,这样我们才真正地获得了这个key所对应的这个键值对。

你可能感兴趣的:(List、Set、Map 集合相关笔记)