Java基础(九)

框架是个好东西,可早晚有一天会过时,这世界上就没有亘古不变的东西,来学下Java基础吧

集合类专辑

List,Set,Map的区别

  • List存储一组不唯一(可以有多个元素引用相同的对象)的,有序的对象
  • Set不允许有重复的集合,不会有多个元素引用相同对象
  • Map使用Key-Value存储,两个Key可以引用相同的值,但不能重复

关于Map的Key值说明:

    public static void main(String[] args) {
        HashMap<Object, Object> map = new HashMap<>();
        String s1 = "LuckyCurve";
        String s2 = s1;
        map.put(s1, 1);
        map.put(s2, 1);
    }

不会出错,如果放两个s1则会出错

ArrayList和LinkedList 的区别

  • 是否为线程安全

两者都是非线程安全的

  • 底层数据结构

ArrayList底层使用Object数组来存取元素,LinkedList底层使用的是双向链表

  • 插入和删除元素是否受元素位置的影响

ArrayList底层采用的是数组存储结构,受插入位置的影响

LinkedList底层采用双向链表结构,插入和删除元素不受元素位置的影响,时间复杂度近似为O(1),如果是指定位置插入元素的话,那么时间复杂度近似为O(n)【需要先移动到指定位置】

  • 是否支持快速随机访问

ArrayList支持,LinkedList不支持

  • 内存空间占用

ArrayList的空间浪费主要体现在列表的结尾会预留一定的容量空间,而LinkedList主要的内存浪费体现在后继和前趋元素的地址记录上

补充:RandomAccess 接口

ArrayList实现了该接口,而LinkedList没有实现该接口

源码:

public interface RandomAccess {
}

该接口只能起到标记作用,标记这个接口的实现类是否具有随机访问功能

循环方式推荐:

实现了RandomAccess 的list【可以推出他的底层实现原理为数组】,推荐使用普通for循环,其次是foreach

没实现RandomAccess 的list【底层为链表】,优先使用iterator遍历【foreach也是可以的,因为foreach的底层实现就是iterator】,数据容量大的list,千万不要使用普通for循环,每次都要访问到指定元素,时间复杂度为O(n^2)

Vector

Vector类的所有方法都是同步的,可以保证线程安全,但是如果只有一个线程访问Vector,会在同步操作上花费大量时间

一般用ArrayList取代Vector,虽然ArrayList不是同步的,所以在不需要保证线程安全的前提下建议使用ArrayList

ArrayList的扩容机制

基于Java11

ArrayList的底层是数组,是定长的,为什么我们可以一直向ArrayList里面添加元素呢,由于他的扩容机制

debug以下代码,观察list的size

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);		list size = 0
    list.add(1);		list size = 1
    list.add(1);		list size = 2
    System.out.println(list);		list size = 3
}

后面的注释标识的是当前语句运行前的状态

可以观察出,在创建ArrayList时候,并没有给其底层数组分配内存空间,直到add的时候才对数组的长度进行扩充

Size只是表示当前存储对象,并不表示底层数组分配的内存空间,真正表示底层数组的是elemenData.length

transient Object[] elementData;

调试发现,创建ArrayList的时候elementData的长度为0,增加一个元素后elementData的长度为10,

原理就在这个函数当中,这个函数的触发条件就是size == elementData.length,传入的参数minCapacity为size+1,函数的返回值即为elementData.length的值

private int newCapacity(int minCapacity) {
    int oldCapacity = this.elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity <= 0) {
        if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
A:          return Math.max(10, minCapacity);
        } else if (minCapacity < 0) {
            throw new OutOfMemoryError();
        } else {
            return minCapacity;
        }
    } else {
B:      return newCapacity - 2147483639 <= 0 ? newCapacity : hugeCapacity(minCapacity);
    }
}

当第一次add的时候,程序走到A这儿,返回10

以后任意一次发生size == elementData.length,都走到B处,返回newCapacity,即oldCapacity + (oldCapacity >> 1);右移运算

这即为ArrayList的底层扩容机制

ensureCapacity方法

public void ensureCapacity(int minCapacity) {
    if (minCapacity > this.elementData.length && (this.elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA || minCapacity > 10)) {
        ++this.modCount;
        this.grow(minCapacity);
    }

}

在ArrayList没有被吊用过,很明显是提供给用户调用的

最好在 add 大量元素之前用 ensureCapacity 方法,以减少grow方法调用的次数

可以做个试验

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    final int N = 10000000;
    long millis1 = System.currentTimeMillis();
    for (int i = 0; i < N; i++) {
        list.add(i);
    }
    long millis2 = System.currentTimeMillis();
    System.out.println("没调用ensureCapacity用时:" + (millis2 - millis1));
}

<<<没调用ensureCapacity用时:463

    
    public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    final int N = 10000000;
    long millis1 = System.currentTimeMillis();
    list.ensureCapacity(N);
    for (int i = 0; i < N; i++) {
        list.add(i);
    }
    long millis2 = System.currentTimeMillis();
    System.out.println("调用ensureCapacity用时:" + (millis2 - millis1));
}

<<<没调用ensureCapacity用时:295

如果目标样品更大,差距将会更多

所以最好在插入大量元素到ArrayList里面之前先调用上述方法,减少重新分配空间次数

HashMap和HashTable【现在基本不用】

HashMap和HashTable都是Map的底层实现类,区别:

  • 线程安全:HashMap是非线程安全的,HashTable是线程安全的

  • 效率:HashMap因为是非线程安全的,效率高【HashTable基本被淘汰,不要在程序中使用它】

  • 对null的处理:HashMap支持null作为Key,但仅能有一个【Key不重复】,Value可以有一个多个null,而HashTable的KV只要有null直接抛出NullPointException异常

  • HashMap默认初始化大小为16,以后扩容成原来的两倍,HashTable默认初始化大小为11,后来扩容成2n+1

如果给定了初始值,HashTable直接创建给定的大小,而HashMap将其扩容成2的n次方再来创建表

  • 底层结构:HashMap当链表长度大于8的时候,将链表转换成红黑树,减少搜索时间,HashTable没有这样的机制

HashSet与HashMap

HashSet的底层储存原理运用的就是HashMap

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {
    static final long serialVersionUID = -5024744406713321676L;
    private transient HashMap<E, Object> map;
    private static final Object PRESENT = new Object();

除了 clone()writeObject()readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法

比较:

HashMapHashSet
实现Map接口实现Set接口
存储键值对仅存储对象
通过put方法向map中添加元素通过add方法向set中添加元素
通过key计算HashCode通过对象成员计算HashCode

HashSet如何检查重复:

​ 当调用add方法将对象传入HashSet中的时候,会计算对象的HashCode,如果在Set集合中没有发现与其相同的HashCode,则认为对象没有重复,如果发现了相同的hashCode,则调用equals方法来检查hashCode相等的对象是否相同,如果相等,HashSet不会让其加入成功【不会报错,也不会抛出任何异常,仅仅只是不让加入】

add的源码,PRESENT是一个Object对象

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

HashCode和equals的相关规定:

  • 两个对象相等,则hashcode一定相同
  • 两个对象相等,两个equals返回的值都为true(equals有对称性)
  • 两个对象有相同的hashcode,也不一定相等
  • equals方法被覆盖过,那么hashcode也要被覆盖
  • hashcode默认行为是对堆上的对象产生独特值,如果没有被重写,则只有相同对象的两个引用相等

如果没有重写equals和hashcode方法的例子,观察输出结果:

public class Test {
    public static void main(String[] args) {
        HashMap<Data, Object> map = new HashMap<>();
        Data data1 = new Data(1, "lee");
        Data data2 = new Data(1, "lee");
        map.put(data1, 1);
        map.put(data2, 2);
        System.out.println(map);
    }
}
<<<{{1,lee}=1, {1,lee}=2}

class Data {
    private Integer id;
    private String username;

    public Data(Integer id, String username) {
        this.id = id;
        this.username = username;
    }

    public Integer getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String toString() {
        return "{" + id + "," + username + "}";
    }
}

会认为这两个对象不相等

如果要让程序判断这两个Java对象相等,则按照步骤

1.先覆盖hashcode,让两个对象的hashcode相等,则会调用对象的equals方法

2.在覆盖equals,改变两个对象的逻辑相等条件

代码如下【非常的简陋,没遵从规范,后面会改善的】【建议看下java.lang包下的类的实现】:

    @Override
    public int hashCode() {
        return id.hashCode()+username.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj instanceof Data) {
            Data data = (Data) obj;
            return this.id == data.id && this.username == data.username;
        }
        return false;
    }

<<<{{1,lee}=2}

HashMap的底层实现

Java8之前是数组和链表结合在一起,也就是链表散列。HashMap将key的hashcode经过扰动函数处理后得到hash值,然后再遍历所有位置,如果当前位置存在元素的话,则比较元素hashcode值,hashcode相等则调用equals方法,如果还是相等则进行value值得覆盖

Hash 值的范围值 - 2147483648 到 2147483647,前后加起来大概 40 亿的映射空间

扰动函数:

​ 防止某些被作为HashMap的Key的对象的hashcode方法实现较差,有很高的两个对象的hashcode相等的几率,从而增加了equals方法调用的几率,降低性能。扰动函数的思想就是增大hashcode的精度,让两个原来判别为相等的hashcode经过此方法后不相等,降低equals被调用的几率,提升性能。

HashMap多线程操作导致死循环问题

问题在于 :并发下的Rehash造成元素之间形成循环链表,虽然JDK1.8解决了这个问题,还是不推荐使用,在多线程下HashMap还会造成数据丢失等其他问题,并发环境下推荐使用ConcurrentHashMap

HashTable和ConcurrentHashMap的区别

既然ConcurrentHashMap也具有线程安全的特性,那么和HashTable的区别是什么呢?

主要的区别体现在实现方式不同:

  • 底层数据结构:

ConcurrentHashMap在JDK1.7之前使用分段的数组+ 链表的结构

JDK1.8采用的数据结构和HashMap1.8一样,为数组+链表/红黑树形式

HashTable的底层数据结构和HashMap1.8之前的结构近似【底层是HashMap主体】,数组+链表

  • 实现线程安全的方式(重要):

1.JDK1.7的时候concurrentHashMap采用分段锁Segment )的形式,每一把锁只锁住容器中的一段数据,多线程可以同时访问不同数据段的数据,提高了并发率

1.8的时候则直接废弃了分段锁的概念,采用Node 数组 + 链表 + 红黑树 的数据结构,并发控制使用synchronized和CAS来操作(JDK1.6的时候对Synchronized锁做了优化),看起来就像是优化过且线程安全的HashMap

HashTable则直接使用synchronized将所有数据都锁起来,效率非常低下,且竞争越激烈效率越低

comparable 和 Comparator 的区别

  • comparable接口属于 java.lang包,有一个compareTo方法用于排序
public interface Comparable<T> {
    int compareTo(T var1);
}
  • Comparator接口属于Java.util,有很多抽象方法,有一个compare(Object obj1, Object obj2)方法用来排序

需要对一个集合进行排序时候可以重写任意一个方法,

当要对一个集合进行两种排序,要重写compareTo和自制的Comparator方法 或者实现两个Comparator方法

使用区别:集合中的类(要进行比较的类)去实现comparable 接口,使用Collections.sort(集合,Comparator )排序才需要实现Comparator 接口

两个数op1与op2相比较,compare(op1,op2),返回正数表示op1>op2,返回负数表示op1<op2,最后的排列规则为从小到大

public static void main(String[] args) {
    Integer[] a = {5, 3, 9, 4, 5};
    ArrayList<Integer> list = new ArrayList<>(Arrays.asList(a));
    Collections.sort(list, (Integer arg1, Integer arg2) -> {
        if (arg1 > arg2) {
            return 1;
        } else if (arg1 == arg2) {
            return 0;
        } else {
            return -1;
        }
    });
    System.out.println(list);
}

<<<[3, 4, 5, 5, 9]

集合框架底层数据结构总结

  • List
    • ArrayList:Object数组
    • Vector:Object数组
    • LinkedList:双向链表(1.6之前为循环链表,到1.7则取消了循环)
  • Set
    • HashSet(无序):基于HashMap来实现的,将值存贮到HashMap的Key当中
    • LinkedHashSet:继承自HashSet,内部是通过LinkedHashMap的key来存储值的
    • TreeSet(有序):红黑树
  • Map
    • HashMap:1.8之前使用数组+链表,数组是主题,链表主要解决Hash冲突【拉链法】,1.8之后当到达阈值的时候自动转换为红黑树
    • LinkedHashMap:继承自HashMap,底层依旧沿用HashMap,并在其基础上加了一条双向链表,使得上面的结构可以保证键值对的插入顺序
    • HashTable:数组+链表,和1.8前的HashMap一样
    • TreeMap:红黑树

如何选用集合?

主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map 接口下的集合,需要排序时选择 TreeMap, 不需要排序时就选择 HashMap, 需要保证线程安全就选用 ConcurrentHashMap. 当我们只需要存放元素值时,就选择实现 Collection 接口的集合,需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet,不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList,然后再根据实现这些接口的集合的特点来选用。

基于STM32F407,使用DFS算法实现最短迷宫路径检索,分为三种模式:1.DEBUG模式,2. 训练模式,3. 主程序模式 ,DEBUG模式主要分析bug,测量必要数据,训练模式用于DFS算法训练最短路径,并将最短路径以链表形式存储Flash, 主程序模式从Flash中….zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计中,皆可应用在项目、毕业设计、课程设计、期末/期中/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值