2、集合框架

!这里写图片描述

一、Map集合

1、HashMap

1、描述

  • key 无序,唯一
  • HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现。设计目标是尽量实现哈希表O(1)级别的增删改查效果,与HashTable主要区别为**不支持同步和允许null作为key和value**。
  • HashMap线程不安全,主要表现在:
    • 多线程同时put时可能会丢失值(前面的put被后面的覆盖)。
    • 多线程扩容时会出现环状结构,造成死循环。
    • 多线程使用迭代器时会触发fast-fail机制。
  • 1.7和1.8
    • 1.7采用头插法,1.8采用尾插法
    • 扩容后数据存储位置计算方式不一样
      • 1.7异或方法
      • 1.8扩容前的位置+扩容大小
  • 1.7数组+单链表,1.8数组+链表+红黑树
  • 遍历方式
    • 遍历HashMap的entrySet键值对集合
    • 遍历HashMap键的Set集合获取值;
    • 遍历HashMap“值”的集合;

2、底层实现

  • HashMap 底层是 hash 数组单向链表实现,数组中的每个元素都是链表,由 **Node 内部类(实现 Map.Entry<K,V>接口)**实现,HashMap 通过 put & get 方法存储和获取。

    存储对象时,将 K/V 键值传给 put() 方法:①、调用 hash(K) 方法计算 K 的 hash 值,然后结合数组长度,计算得数组下标;②、调整数组大小(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容resize 为 2n);
    ③、i.如果 K 的 hash 值在 HashMap 中不存在,则执行插入,若存在,则发生碰撞
    ii.如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 true,则更新键值对
    iii. 如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)

    1. 链表转红黑树的阈值为什么是8?

      红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

      ​ 还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

    2. 为什么是红黑树,二叉平衡树不行吗?

      • 因为平衡二叉是高度平衡的树, 当平衡被破坏时,需要要 rebalance(再平衡), 开销会比红黑树大.

      • 原因:

        • 插入引起的不平衡:

          插入一个node引起了树的不平衡,平衡二叉树和红黑树都是最多只需要2次旋转操作,即两者都是O(1);

        • 删除引起的不平衡:

          最坏情况下,平衡二叉树需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级是O(logN)此,而红黑树最多只需3次旋转,只需要O(1)的复杂度

    3. 数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算(据说提升了5~8倍)。

    4. 负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。

    5. 以6和8来作为平衡点是因为,中间有个差值7可以防止链表和树之间频繁的转换。假设,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。链表:如果元素小于8个,查询成本高,新增成本低,红黑树:如果元素大于8个,查询成本低,新增成本高。

    6. 怎么减少碰撞

      • 使用final修饰的对象、或不可变的对象作为键

      • 使用(Integer、String)(是不可变、final的,而且已经重写了equals和hashCode方法)这样的wrapper类作为键是非常好的,(我们可以使用自定义的对象作为键吗?答:当然可以,只要它遵守了equals和hashCode方法定义规则,并且当对象插入到Map中之后将不会再改变。)

      • 扰动函数可以减少碰撞。

    7. HashMap是非线程安全的,那么原因是什么呢?(HashMap的死锁)

      • put的时候导致的多线程数据不一致
        比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的 hash桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的 hash桶索引和线程B要插入的记录计算出来的 hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
      • resize而引起死循环
        这种情况发生在HashMap自动扩容时,当2个线程同时检测到元素个数超过 数组大小 × 负载因子。此时2个线程会在put()方法中调用了resize(),两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环。
    8. JDK1.8中HashMap的插入源码分析**

       /**
           * 将指定参数key和指定参数value插入map中,如果key已经存在,那就替换key对应的value
           * put(K key, V value)可以分为三个步骤:
           * 1.通过hash(Object key)方法计算key的哈希值。
           * 2.通过putVal(hash(key), key, value, false, true)方法实现功能。
           * 3.返回putVal方法返回的结果。
           *
           * @param key   指定key
           * @param value 指定value
           * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null
           */
          public V put(K key, V value) {
              // 倒数第二个参数false:表示允许旧值替换
              // 最后一个参数true:表示HashMap不处于创建模式
              return putVal(hash(key), key, value, false, true);
          }
      
          /**
           * Map.put和其他相关方法的实现需要的方法
           * putVal方法可以分为下面的几个步骤:
           * 1.如果哈希表为空,调用resize()创建一个哈希表。
           * 2.如果指定参数hash在表中没有对应的桶,即为没有碰撞,直接将键值对插入到哈希表中即可。
           * 3.如果有碰撞,遍历桶,找到key映射的节点
           * 3.1桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
           * 3.2如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方法插入键值对。
           * 3.3如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。如果没有找到,在链表尾部插入节点。插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链表转为红黑树。
           * 4.如果找到了key映射的节点,且节点不为null
           * 4.1记录节点的vlaue。
           * 4.2如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。
           * 4.3返回记录下来的节点的value。
           * 5.如果没有找到key映射的节点(2、3步中讲了,这种情况会插入到hashMap中),插入节点后size会加1,这时要检查size是否大于临界值threshold,如果大于会使用resize方法进行扩容。
           *
           * @param hash         指定参数key的哈希值
           * @param key          指定参数key
           * @param value        指定参数value
           * @param onlyIfAbsent 如果为true,即使指定参数key在map中已经存在,也不会替换value
           * @param evict        如果为false,数组table在创建模式中
           * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
           */
          final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                         boolean evict) {
              Node<K, V>[] tab;
              Node<K, V> p;
              int n, i;
              //如果哈希表为空,调用resize()创建一个哈希表,并用变量n记录哈希表长度
              if ((tab = table) == null || (n = tab.length) == 0)
                  n = (tab = resize()).length;
              /**
               * 如果指定参数hash在表中没有对应的桶,即为没有碰撞
               * Hash函数,(n - 1) & hash 计算key将被放置的槽位
               * (n - 1) & hash 本质上是hash % n,位运算更快
               */
              if ((p = tab[i = (n - 1) & hash]) == null)
                  //直接将键值对插入到map中即可
                  tab[i] = newNode(hash, key, value, null);
              else {// 桶中已经存在元素
                  Node<K, V> e;
                  K k;
                  // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
                  if (p.hash == hash &&
                          ((k = p.key) == key || (key != null && key.equals(k))))
                      // 将第一个元素赋值给e,用e来记录
                      e = p;
                      // 当前桶中无该键值对,且桶是红黑树结构,按照红黑树结构插入
                  else if (p instanceof TreeNode)
                      e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                      // 当前桶中无该键值对,且桶是链表结构,按照链表结构插入到尾部
                  else {
                      for (int binCount = 0; ; ++binCount) {
                          // 遍历到链表尾部
                          if ((e = p.next) == null) {
                              p.next = newNode(hash, key, value, null);
                              // 检查链表长度是否达到阈值,达到将该槽位节点组织形式转为红黑树
                              if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                  treeifyBin(tab, hash);
                              break;
                          }
                          // 链表节点的<key, value>与put操作<key, value>相同时,不做重复操作,跳出循环
                          if (e.hash == hash &&
                                  ((k = e.key) == key || (key != null && key.equals(k))))
                              break;
                          p = e;
                      }
                  }
                  // 找到或新建一个key和hashCode与插入元素相等的键值对,进行put操作
                  if (e != null) { // existing mapping for key
                      // 记录e的value
                      V oldValue = e.value;
                      /**
                       * onlyIfAbsent为false或旧值为null时,允许替换旧值
                       * 否则无需替换
                       */
                      if (!onlyIfAbsent || oldValue == null)
                          e.value = value;
                      // 访问后回调
                      afterNodeAccess(e);
                      // 返回旧值
                      return oldValue;
                  }
              }
              // 更新结构化修改信息
              ++modCount;
              // 键值对数目超过阈值时,进行rehash
              if (++size > threshold)
                  resize();
              // 插入后回调
              afterNodeInsertion(evict);
              return null;
          }
      

3、各项默认值

  1. 初始容量(capacity):16(1<<4),即**2^4**。

  2. 最大容量:(1<<30),即**2^30**。

  3. 默认扩容因子(loadFactor):0.75

  4. 自动扩容阈值:当前HashMap**元素个数 > capacity * loadFactor**,会自动扩容,并重新hash。

  5. 扩容机制:扩容至**当前HashMap容量*2.0,且每次扩容后的容量必须为2的n次方**。

  6. 容量必须为2的n次方的原因:

    HashMap对元素进行读、写操作时,需要将Map元素的Key的哈希值对数组长度(HashMap 的容量)进行取模运算,结果作为该元素在数组中的索引(index),在计算机中,取模运算的代价远高于位运算的代价,当数组长度为 2^n 时,可以将Map元素的Key的hashcode对 (2^n)-1 进行 与运算(&),效果与对数组长度进行取模运算相等,所以为了提高 HashMap 的操作效率,规定 HashMap 的容量必须为2的n次方,即2^n。h & (length - 1) == h % length。

    扩容机制:

    • 1.7先新建一个数组,数组长度为原数组的2倍

      循环遍历原数组的每一个键值对的,得到键的Hashcode然后与新数组的长度(此处为8)进行&运算得到新数组的位置。然后把键值对放到对应的位置。故扩容之后可能会变成这样

    • 1.8只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+原数组长度”。

  7. 如何解决哈希冲突

    • 使用散列表
    • 使用两次扰动函数
    • 引入红黑树

4、线程安全

  1. 是否线程安全?不安全的场景。

    HashMap线程不安全,主要表现在:

    • 多线程同时put时可能会丢失值(前面的put被后面的覆盖)。
    • 多线程扩容时会出现环状结构,造成死循环。
    • 多线程使用迭代器时会触发fast-fail机制。
  2. 如何解决?

    • 使用 CollectionssynchronizedMap() 对其进行包装,使其线程安全。(锁整个表,效率差)
    • 直接使用 线程安全的ConcurrentHashMap。(分段锁/CAS,效率相对较高)

2、HashTable

1、描述

  • Hashtable 继承于 Dictionary 类(Dictionary类声明了操作键值对的接口方法),实现Map接口(定义键值对接口);
  • 与HashMap一样也是哈希表(散列表),存储元素也是键值对。
  • Hashtable 大部分情况下是线程安全的

2、底层实现

​ 继承于 Dictionary 类,实现Map接口,采用 Entry数组+链表 实现。

3、各项默认值

  1. 初始容量(capacity):11
  2. 最大容量:Integer.MAX_VALUE – 8,即**(2^31-1)-8**,可能会导致内存溢出。
  3. 默认扩容因子(loadFactor):0.75
  4. 自动扩容阈值:当前HashMap**元素个数 > capacity * loadFactor**,会自动扩容,并重新hash。
  5. 扩容机制:扩容至**当前HashMap容量*2.0+1**。

4、线程安全

​ Hashtable大部分方法采用 synchronized 修饰,说明Hashtable大部分情况下是线程安全的,但它采用的是 独占锁 机制,并发时会**锁住整个hash表,导致效率十分低下,且在执行一些复合操作时,也会有线程安全隐患。**

复合操作:

  • 若不存在则添加。
  • 若存在则删除。
  • 等。

5、HashTable与 HashMap 的区别

HashMapHashTable
线程安全不安全大部分情况线程安全
允许为nullkey和value都允许为nullkey和value都不允许为null
初始容量1611
扩容机制扩容至当前容量的2倍扩容至当前容量的2倍+1

3、ConcurrentHashMap

1、描述

​ ConcurrentHashMap 在 JDK1.5 时被加入,是 HashMap 线程安全的版本,其使用方式与 HashMap 一样。

2、底层实现

​ 其他实现与 HashMap 一样,只是添加了线程安全的保障,这里主要讲线程安全的实现

  • JDK1.7:

    • 线程安全采用**分段锁机制来实现,底层数据结构仍然是数组+链表。与HashMap不同的是,ConcurrentHashMap最外层不是一个大的数组,而是一个Segment的数组**,每个Segment下管理一个与HashMap数据结构差不多的table数组,在并发操作加锁时,锁住的是整个Segment,也就是说Segment的个数就是ConcurrentHashMap的最大并发数。
    • 默认有16个Segment,即最大并发数为16。,这个值可以通过构造函数改变,但一经创建就不可更改。
    • 尽管采用分段锁使锁的粒度小了很多,不再像HashTable那样锁住整个Hash表,但JDK1.7的ConcurrentHashMap的并发量还是受到了Segment个数的影响,为了能提供更大的并发量,JDK1.8的ConcurrentHashMap直接抛弃了分段锁机制,转而采用**CAS算法+synchronized**实现。
  • 详情请参考:Java7基于分段锁机制的ConcurrentHashMap

  • JDK1.8及以后:

    • 为进一步提高并发性,JDK1.8放弃了分段锁机制,将锁的级别控制在了更细粒度的**table元素级别上**,也就是说,只需要锁住一个table元素链表的头节点(head),并不会影响其他的table元素的读写,好处在于锁的粒度更细,影响更小,从而并发效率更好。

    • 使用**CAS算法【Compare And Swap】 + synchronized关键字** 来保证**put操作**的线程安全,步骤如下:

      1. 先判断当前被put进Map的元素的Key在table中所对应的数组元素是否为null,若为null,则**通过CAS操作**将其设置为当前put的元素的value。
      2. 如果Key对应的数组元素(也即链表表头或者红黑树的根元素)不为null,则对该数组元素使用synchronized关键字申请锁,然后再进行操作。
    • 详情请参考:基于CAS算法的ConcurrentHashMap

    • CAS算法原理:

      CAS即CompareAndSwap,中文意思是:比较并替换,是由底层硬件提供的一种同步算法,大致原理如下:

      • 缺点

        1. 循环时间长,开销很大

        2. 如果CAS失败,会一直进行尝试,长时间不成功,可能会给CPU带来很大的开销

        3. 只能保证一个共享变量的原子操作

        4. ABA问题

          • CAS会导致ABA问题,CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

            比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。

            尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。

            • 解决:AtomicStampedReference
      • CAS需要有3个操作数:

        1. 内存地址V:即将要修改的主存变量所在的地址。
        2. 旧的预期值A:即将要修改的主存变量所在的地址的值。
        3. 计算后得到的新值B,即将要更新的目标值。
      • CAS改值操作步骤:

        1. 读取到主存变量的内存地址(V);
        2. 得到内存地址V的值(A)【旧的预期值】;
        3. 通过计算得到新的值(B);
        4. 在将要更新V的值之前,先将A与V的值进行比较(Compare);
      1. 当且仅当A==V的值时,才会将V的值改为B,否则什么都不做。
      • CAS核心:当且仅当内存地址V的值与预期值A相等时,才将内存地址V的值修改为B,否则就什么都不做。整个CAS(比较并替换)操作是一个原子操作。
    • JDK1.8是基于CAS算法的轮询访问改值方式实现同步(即一直循环CAS算法去尝试改值,直到修改成功为止),虽然对CUP的消耗会大些,但如此实现的**线程同步是非阻塞式**的,并发量将得到很大提高。

3、各项默认值

  • 默认最大并发数:16,即为 Segment 的个数。(jdk1.7)
  • 其他默认值与HashMap一致。

4、Treemap

底层红黑树

img

AVL 树具有以下特点:

  • 每个结点的平衡因子只可能是 -1、0、1(如果绝对值超过 1,则认为是失衡)
  • 每个结点的左右子树高度差不超过 1
  • 搜索、插入、删除的时间复杂度是 O(logn)

红黑树是一种含有红黑结点并能自平衡的二叉搜索树。(旋转变色 保持平衡)

以下特点:

  • 每个结点是要么是红色或黑色
  • 根结点必须是黑色
  • 叶结点(外部结点、空结点)是黑色
  • 红色结点不能连续(也就是,红色结点的孩子和父亲都是黑色)
  • 对于每个结点,从该点至 nil(树尾端,Java 中为 null 的结点)的任何路径都包含所相同个数的黑色结点

二、List集合

1、ArrayList

1、描述

  • ArrayList是List接口可调整大小的数组实现。实现所有可选的List操作,并允许所有元素,包括null,元素可重复。
  • ArrayList是线程不安全的,体现在并发修改时会触发**快速失败(fail-fast)**机制,。

2、底层实现

​ 可调整大小的数组实现。

3、各项默认值

  1. 初始容量(capacity):10
  2. 最大容量:Integer.MAX_VALUE - 8,即**Integer.MAX_VALUE - 8**,可能会导致内存溢出。
  3. 默认扩容因子(loadFactor):1
  4. 自动扩容阈值:当前ArrayList**元素个数 > capacity * loadFactor**,会自动扩容。
    • ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。
    • 把新元素添加到扩容以后的数组中
  5. 扩容机制:扩容至**当前ArrayList容量*1.5**。

4、线程安全

  • 故障现象:java.util.ConcurrentModificationException
  • 导致原因:add方法为了保证并发性,没有加锁,一个线程正在写,另一线程过来抢夺,导致数据不一致,即并发修改导致的异常
  • 解决方案:
    • new Vector()
      • Vector的add方法有synchronized锁,可以保证线程安全,但是性能降低
    • Collections.synchronizedList()
    • new CopyOnWriteArrayList<>()
      • 写时复制,读写分离
      • 往一个容器添加元素时候,不直接往当前容器Object[]添加,而是先将容器进行Copy,复制出一个新的容器,然后往新的容器中添加元素,添加完成以后,将原容器的引用指向新的容器,这样可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。

2、Vector

1、描述

  • 与ArrayList一样,底层是数组结构。但因它的所有方法都用**synchronized**修饰,所以Vector是线程安全的。
  • 因为线程安全,所以它的效率比ArraList低。

2、底层实现

​ 数组。

3、各项默认值

  1. 初始容量(capacity):10
  2. 最大容量:Integer.MAX_VALUE - 8,即**Integer.MAX_VALUE - 8**,可能会导致内存溢出。
  3. 默认扩容因子(loadFactor):1
  4. 自动扩容阈值:当前ArrayList**元素个数 > capacity * loadFactor**,会自动扩容。
  5. 扩容机制:扩容至**当前ArrayList容量*2**。

4、线程安全

Vector是线程安全的

3、LinkedList

1、描述

  • LinkedList是List和Deque接口的**双向链表**的实现。实现了所有可选List操作,并允许包括null值。
  • LinkedList是线程不安全的。

2、底层实现

双向链表

3、各项默认值

无,因为是双向链表。

4、线程安全

LinkedList是线程不安全的,需要用 Collections.synchronizedList() 对其进行包装。

4、ArrayList、LinkedList、Vector的区别

ArrayListLinkedListVector
底层实现数组双向链表数组
性能索引查询快,添加、删除慢(主要是扩容)添加、删除快,索引查询慢比ArrayList慢
扩容动态扩容双向链表,不用扩容动态扩容
线程安全不安全不安全安全

三、Set集合

1、HashSet

1、描述

  • HashSet是Set接口的实现,元素无序、不可重复,底层是一个HashMap,用以保存数据,value是一个叫PRESENT Object类型的常量,即HashSet只关心key。
  • 不能保证元素的排列顺序,顺序有可能发生变化。
  • 线程不安全。
  • 集合元素可以是null,但只存在一个null。

2、底层实现

HashMap。

3、各项默认值

​ 与 HashMap 一致。

4、线程安全

HashSet是线程不安全的,需要用 Collections.synchronizedSet() 对其进行包装。

2、LinkedSet

1、描述

  • LinkedSet是Set接口的实现,继承于HashSet,元素不可重复,底层是一个LinkedMap,用以保存数据。
  • LinkedSet**可保证元素的插入顺序。**

2、底层实现

LinkedMap

3、各项默认值

​ 与 HashMap 一致。

4、线程安全

LinkedSet是线程不安全的,需要用 Collections.synchronizedSet() 对其进行包装。

5、与HashSet的区别

​ LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。即**顺序访问性能好,插入、删除性能差。**

3、TreeSet

1、描述

  • TreeSet是SortedSet接口的唯一实现类,元素不可重复。底层是一个TreeMap,用以保存数据。
  • TreeSet**默认可保证元素的自然顺序(元素需要实现Comparable接口),可指定排序规则(需要重写元素的hashCode()方法和equals()方法)**。

2、底层实现

TreeMap

3、各项默认值

​ 与 HashMap 一致。

4、线程安全

TreeSet是线程不安全的,需要用 Collections.synchronizedSet() 对其进行包装。

4、HashSet、LinkedSet、TreeSet的区别

HashSetLinkedSetTreeSet
顺序无序保证插入顺序默认保证自然顺序
底层实现HashMapLinkedMapTreeMap
线程安全不安全不安全不安全

四、Java8新特性

1、速度更快

更改了底层数据结构,如HashMap、HashSet。

2、代码更少

新增语法:lambda表达式

3、有强大的Stream API

Stream API

4、便于并行

优化 Fork/Join 框架。

5、最大化减少空指针异常

新增 Optional 容器类。

6、总结

最主要的核心还是 Lambda 表达式Stream API

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值