ArrayList源码解析

1.1底层数据结构

定义:实现List接口的可扩容数组实现。

数组特点

  • 查询快:数组开辟的是连续空间,所以可以依靠索引进行快速查询。
  • 增删慢:每次删除元素,都需要更改数组长度、拷贝以及移动元素位置。

1.2继承关系

1.2.1Serializable接口

类的序列化由实现java.io.Serializable接口的类启用。不实现此接口的类将不会使任何状态序列化或反序列化。可序列化类的所有子类型都是可序列化的。序列化接口没有方法或字段,仅用于标识可串行化的语义。

java.io.Serializable接口是一个标记型接口,没有任何方法和字段。

1.8源码注释

If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java™ Object Serialization Specification. However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassExceptions during deserialization. Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class–serialVersionUID fields are not useful as inherited members. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes.

1.2.2Cloneable接口

Cloneable接口同样也是一个标记型接口,没有任何的字段或者方法。

实现该接口的意义在于指示Object.clone()方法,调用方法对于该类的实例进行字段的复制是合法的。在不实现Cloneable接口的实例上调用clone()方法会导致异常CloneNotSupportedException被抛出。

1.8源码注释

A class implements the Cloneable interface to indicate to the Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class.
Invoking Object’s clone method on an instance that does not implement the Cloneable interface results in the exception CloneNotSupportedException being thrown.
By convention, classes that implement this interface should override Object.clone (which is protected) with a public method. See Object.clone() for details on overriding this method.
Note that this interface does not contain the clone method. Therefore, it is not possible to clone an object merely by virtue of the fact that it implements this interface. Even if the clone method is invoked reflectively, there is no guarantee that it will succeed.

补充知识:

  1. 实现克隆的前提:

    • 实现Cloneable接口。
    • 重写clone方法。(该方法创建的对象与原对象的地址不同)
  2. 浅拷贝的局限性:
    • 基本数据类型可以达到完全复制,引用数据类型则不可以。
    • 引用数据类型进行克隆的时候只是单纯复制了一份引用。

1.2.3RandomAccess接口

该接口仍然是一个标记型接口,以表明它们支持快速随机访问。 此接口的主要目的是允许通用算法更改其行为,以便在随机访问列表(使用索引进行访问)时提供良好的性能。

即实现了该接口,那么随机访问速度大于顺序访问速度。

1.8源码注释

Marker interface used by List implementations to indicate that they support fast (generally constant time) random access. The primary purpose of this interface is to allow generic algorithms to alter their behavior to provide good performance when applied to either random or sequential access lists.
The best algorithms for manipulating random access lists (such as ArrayList) can produce quadratic behavior when applied to sequential access lists (such as LinkedList). Generic list algorithms are encouraged to check whether the given list is an instanceof this interface before applying an algorithm that would provide poor performance if it were applied to a sequential access list, and to alter their behavior if necessary to guarantee acceptable performance.
It is recognized that the distinction between random and sequential access is often fuzzy. For example, some List implementations provide asymptotically linear access times if they get huge, but constant access times in practice. Such a List implementation should generally implement this interface. As a rule of thumb, a List implementation should implement this interface if, for typical instances of the class, this loop:
for (int i=0, n=list.size(); i < n; i++)
list.get(i);

runs faster than this loop:
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();

1.3重要属性详解

首先解释一下几个修饰符:

  1. private:权限修饰符,表示该属性是私有的,除了成员所属类,都无法访问该成员。
  2. static:被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
  3. final:对于基本类型来说,不可变说的是变量当中的数据不可改变,对于引用类型来说,不可变说的是变量当中的地址值不可改变。
  4. transient:对于该修饰符修饰的变量,对象的序列化处理过程中会被忽略。
     // 默认初始容量10private static final int DEFAULT_CAPACITY = 10;// 空实例的共享空数组实例,与无参数传递的构造是不同的 private static final Object[] EMPTY_ELEMENTDATA = {};// 共享空数组实例,用于默认大小的空实例。private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// 底层存储数据的位置,集合真正存储数据的容器transient Object[] elementData; // ArrayList的大小private int size; // 结构修改计数器,放置protected transient int modCount = 0;

可以看到,在底层储存时,ArrayList采用的是Object数组,进行了一个泛型擦除。

1.4构造方法

ArrayList的构造方法分为三类:

  • 不传递任何参数;
  • 传递一个整数;
  • 传递一个集合。

1.4.1空参构造

空参构造时,底层存储元素的容器都是DEFAULTCAPACITY_EMPTY_ELEMENTDATA

public ArrayList() {// 进行赋值操作this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

1.8源码

  • Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA will be expanded to DEFAULT_CAPACITY when the first element is added.
  • 当添加第一个元素时,任何具有 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList 都将扩展为 DEFAULT_CAPACITY。

1.4.2指定初始容量

 public ArrayList(int initialCapacity){// 参数大于0创建一个指定长度的Object数组if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];// 如果参数为0则创建一个EMPTY_ELEMENTDATA共享数组} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;// 参数小于0则抛出异常} else {throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}}

1.4.3给定初始的集合

public ArrayList(Collection<? extends E> c) {// 将集合转换成Object数组Object[] a = c.toArray();// 如果原本集合不为空if ((size = a.length) != 0) {if (c.getClass() == ArrayList.class) {// 如果两者的类型一致都是java.util.ArrayList,则直接赋值为底层存储数据的数组elementData = a;} else {// 如果不一致就使用copyof方式进行数组拷贝elementData = Arrays.copyOf(a, size, Object[].class);}} else {// 参数集合为空则和指定长度为0的构造函数是一致的,创建一个EMPTY_ELEMENTDATA共享数组。elementData = EMPTY_ELEMENTDATA;}}public Object[] toArray() {// 调用Arrays工具类的方法将输入的集合拷贝为数组返回return Arrays.copyOf(elementData, size);}/*original – 要拷贝的数组newLength – 拷贝副本的长度newType – 拷贝副本的元素类型
*/
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType){...// 调用本地方法arraycopy进行数组的拷贝工作。System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));}

1.5常见方法详解

1.5.1ArrayList添加方法

add(e):添加单个元素,核心思想就是先保证当前容量+1的大小,再进行元素添加。

public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;}private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private static int calculateCapacity(Object[] elementData, int minCapacity) {// 先进行判断,存储元素的数组等不等价与默认的空值(不指定参数的那种)if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 如果空参构造则返回默认值10和miniCapacity之间的最大值return Math.max(DEFAULT_CAPACITY, minCapacity);}// 否则返回minCapacityreturn minCapacity;}// 判断是否有额外空间private void ensureExplicitCapacity(int minCapacity) {// 修改计数器+1modCount++;// 如果minicity大于当前数组的长度则进行扩容if (minCapacity - elementData.length > 0)grow(minCapacity);}// 扩容
private void grow(int minCapacity) {// 获取旧的数组长度int oldCapacity = elementData.length;// 扩容1.5倍int newCapacity = oldCapacity + (oldCapacity >> 1);// 如果新容量还是小于最小的需求容量则将最小需求容量赋值给新容量if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 如果新容量大于最大容量(整数的最大值-8)调用hugeCapacity方法。if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// 按新的容量拷贝数组elementData = Arrays.copyOf(elementData, newCapacity);}private static int hugeCapacity(int minCapacity) {// 超越整数最大值则报溢出异常if (minCapacity < 0) // overflowthrow new OutOfMemoryError();// 不溢出,如果比最大容量大则设置为整数的最大值,否则设置为(整数的最大值-8)return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}// 扩容调用的拷贝方法,底层还是调用了1.4.3中解析的拷贝方法
public static <T> T[] copyOf(T[] original, int newLength) {return (T[]) copyOf(original, newLength, original.getClass());}

add(int index, E element):在指定位置添加元素。

public void add(int index, E element) {// 范围检查rangeCheckForAdd(index);// 统一按size+1进行容量和扩容传递。ensureCapacityInternal(size + 1);  // 拷贝原数组,native方法System.arraycopy(elementData, index, elementData, index + 1,size - index);elementData[index] = element;size++;}// 范围检查
private void rangeCheckForAdd(int index) {if (index < 0 || index > this.size)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}

addAll(Collection<? extends E> c):添加一个集合。

public boolean addAll(Collection<? extends E> c) {// 将参数集合转化Object数组Object[] a = c.toArray();// 获取参数集合的长度int numNew = a.length;// 按两个集合的总长度进行扩容传递ensureCapacityInternal(size + numNew);  // Increments modCount// 拷贝两个数组System.arraycopy(a, 0, elementData, size, numNew);// 更改当前容量size += numNew;// 返回参数数组是否不为空return numNew != 0;}public boolean addAll(int index, Collection<? extends E> c) {// 范围校验rangeCheckForAdd(index);// 转换为数组Object[] a = c.toArray();// 获取添加集合的长度int numNew = a.length;// 按着两个集合长度之和作为最小需求容量进行传递ensureCapacityInternal(size + numNew);  // Increments modCount// 计算要移动元素的个数int numMoved = size - index;// 如果要添加的索引位置在size之前,类似于在中间插入参数集合所有元素if (numMoved > 0)System.arraycopy(elementData, index, elementData, index + numNew,numMoved);// 拷贝两个数组System.arraycopy(a, 0, elementData, index, numNew);// 更改当前容量size += numNew;// 返回参数数组是否不为空return numNew != 0;}

1.5.2ArrayList获取方法

get(int index):获取指定索引位置的元素。中间会进行一个类型转换,确保得到的元素类型和传递的泛型一致。

public E get(int index) {// 范围查询rangeCheck(index);// 直接返回数组中的索引元素return elementData(index);}E elementData(int index) {// 根据传递的泛型进行转换返回return (E) elementData[index];}

1.5.3ArrayList设置方法

set(int index, E element):指定索引位置的元素。

public E set(int index, E element) {// 范围检查rangeCheck(index);// 获取原本索引的旧值,仍会进行类型转换E oldValue = elementData(index);// 赋值新的元素elementData[index] = element;// 返回旧的值return oldValue;}

1.5.4ArrayList删除元素方法

remove(int index):根据索引删除元素。

 public E remove(int index) {// 索引范围检查rangeCheck(index);// 修改计数器+1modCount++;// 获取原本的索引处元素,会进行类型准换E oldValue = elementData(index);int numMoved = size - index - 1;if (numMoved > 0)// 数据前移System.arraycopy(elementData, index+1, elementData, index,numMoved);// 最后一个元素置空,elementData[--size] = null; // clear to let GC do its work// 返回旧的值return oldValue;}

remove(Object o):根据值删除元素。

public boolean remove(Object o) {// 如果为空则使用【==】进行的比较,null可以使用==进行比较if (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {fastRemove(index);return true;}// 非空则使用equals方法} else {for (int index = 0; index < size; index++)if (o.equals(elementData[index])) {fastRemove(index);return true;}}return false;}private void fastRemove(int index) {// 修改计数器+1modCount++;int numMoved = size - index - 1;if (numMoved > 0)// 前移数据虽然是复制,但是没有开辟新的空间System.arraycopy(elementData, index+1, elementData, index,numMoved);// 末尾置空方便gcelementData[--size] = null; }

1.5.5ArrayList清除元素方法

clear()

public void clear() {// 修改计数器+1modCount++;// 遍历数组清除元素置nullfor (int i = 0; i < size; i++)elementData[i] = null;// 长度置为0size = 0;}

1.5.6ArrayList toString方法

调用的是AbstractCollection类的toString方法。

public String toString() {Iterator<E> it = iterator();// 迭代器无任何元素直接返回[]if (! it.hasNext())return "[]";StringBuilder sb = new StringBuilder();// 开头的拼接sb.append('[');for (;;) {E e = it.next();// 添加元素sb.append(e == this ? "(this Collection)" : e);if (! it.hasNext())// 结尾的拼接并返回return sb.append(']').toString();// 元素之间进行拼接sb.append(',').append(' ');}}

1.5.7ArrayList iterator方法

iterator()

public Iterator<E> iterator() {// 返回了一个私有的内部类return new Itr();}

1.5.8ArrayList contains方法

contains(Object o):检查ArrayList是否包含指定元素,对基本数据类型会进行自动装箱。

public boolean contains(Object o) {// 返回是否存在return indexOf(o) >= 0;}public int indexOf(Object o) {// 为空则使用【==】比较符逐元素比较if (o == null) {for (int i = 0; i < size; i++)if (elementData[i]==null)return i;} else { // 不为空则使用equals方法进行比较for (int i = 0; i < size; i++)if (o.equals(elementData[i]))return i;}// 找不到返回-1return -1;}

1.5.9ArrayList isEmpty方法

isEmpty():检测ArrayList是否为空。

public boolean isEmpty() {// 返回size==0?return size == 0;}

1.5.10迭代器

public Iterator<E> iterator() {// 返回内部类Itr对象return new Itr();}// 当前索引的指针
int cursor;       // index of next element to returnint lastRet = -1; // index of last element returned; -1 if no such// 记录当前正在进行迭代的数组底层的修改计数器
int expectedModCount = modCount;// 当计数器不相等的时候直接抛出并发修改异常
final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();
}// 返回当前指针是否和size相等
public boolean hasNext() {return cursor != size;}public E next() {// 检查是否继续进行了并发修改checkForComodification();// 如果当前指针的大于了数组元素数量抛出异常int i = cursor;if (i >= size)throw new NoSuchElementException();// 获取其数组Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length)throw new ConcurrentModificationException();cursor = i + 1;return (E) elementData[lastRet = i];}final void checkForComodification() {// modCount != expectedModCount就说明在迭代过程中发生了结构修改。if (modCount != expectedModCount)throw new ConcurrentModificationException();}

1.5.11ArrayList Clone方法

clone():克隆一个副本

public Object clone() {try {// 调用Object的克隆方法ArrayList<?> v = (ArrayList<?>) super.clone();// 调用copyof方法对原数组进行拷贝v.elementData = Arrays.copyOf(elementData, size);// 修改指示器置0v.modCount = 0;return v;// 如果没有实现Cloneable接口就会抛出该异常} catch (CloneNotSupportedException e) {// this shouldn't happen, since we are Cloneablethrow new InternalError(e);}}

1.6常见面试题

1.6.1频繁扩容导致性能下降怎么办?

  1. 在创建对象的时候就指定好容量,如果指定的值过小,那么扩容次数会非常多。
  2. 直接使用无参初始化10。

1.6.2ArrayList插入或者删除元素一定比LinkedList慢吗?

/*SRC -源数组。
srcPos -源数组的起始位置。
Dest -目标数组。
destPos -目标数组的起始位置。
length-要复制的数组元素的数量。
*/
public static native void arraycopy(Object src,  int  srcPos,Object dest, int destPos, int length);

这个问题的答案取决于添加或者删除元素的位置。也就是说如果是添加、删除最后一个元素那么两者差不多(此时ArrayList不用执行数据前、后移或者扩容)。但是其他位置就比LinkedList慢多了,随着元素数量的增多,这个现象会更加严重。

1.6.3如何复制某个ArrayList到另一个ArrayList中去?

  1. 使用clone方法;
  2. 使用构造方法;
  3. 使用addAll()方法;
  4. 使用遍历添加。

1.6.4已知成员变量集合存储很多用户名称,在多线程的环境下,使用迭代器在读取集合数据的同时如何保证还可以正常的写入数据到集合?

使用CopyOnWriteArrayList读写分离的集合。

1.6.5ArrayList 和 LinkedList区别?

ArrayList

  1. 基于动态数组的实现;
  2. 实现了序列化、克隆和RandomAccess接口;
  3. 对于随机访问,ArrayList的性能更加优异;
  4. 对于删除、添加数据要看情况,绝大多数情况LinkedList效果更好。

LinkedList

  1. 没有实现RandomAccess接口,但是多实现了一个Deque接口;
  2. 基于双向链表的实现;
  3. 一般情况下增删性能更好;
  4. 随机访问性能不如ArrayList。

1.6.6ArrayList如何进行扩容

初始化时分为3种情况

  1. 无参数初始化: s i z e = 0 , e l e m e n t D a t a . l e n g t h = 0 size=0,elementData.length = 0 size=0,elementData.length=0
  2. 初始化指定容量:
    1. 初始化0: s i z e = 0 , e l e m e n t D a t a . l e n g t h = 0 size=0,elementData.length = 0 size=0,elementData.length=0
    2. 初始化N: s i z e = 0 , e l e m e n t D a t a . l e n g t h = N size=0,elementData.length = N size=0,elementData.length=N

add方法为例,无参初始化时:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmdKa9DF-1658036953649)(E:/Postgraduate/document/ASSESTS/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.assets/er%E5%9B%BE.png)]

初始化数值0时:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U8ARkIYZ-1658036953651)(E:/Postgraduate/document/ASSESTS/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.assets/er%E5%9B%BE%20(1)].png)

nWriteArrayList`读写分离的集合。

1.6.5ArrayList 和 LinkedList区别?

ArrayList

  1. 基于动态数组的实现;
  2. 实现了序列化、克隆和RandomAccess接口;
  3. 对于随机访问,ArrayList的性能更加优异;
  4. 对于删除、添加数据要看情况,绝大多数情况LinkedList效果更好。

LinkedList

  1. 没有实现RandomAccess接口,但是多实现了一个Deque接口;
  2. 基于双向链表的实现;
  3. 一般情况下增删性能更好;
  4. 随机访问性能不如ArrayList。

1.6.6ArrayList如何进行扩容

初始化时分为3种情况

  1. 无参数初始化: s i z e = 0 , e l e m e n t D a t a . l e n g t h = 0 size=0,elementData.length = 0 size=0,elementData.length=0
  2. 初始化指定容量:
    1. 初始化0: s i z e = 0 , e l e m e n t D a t a . l e n g t h = 0 size=0,elementData.length = 0 size=0,elementData.length=0
    2. 初始化N: s i z e = 0 , e l e m e n t D a t a . l e n g t h = N size=0,elementData.length = N size=0,elementData.length=N

add方法为例,无参初始化时:

初始化数值0时:

【Java学习002】Java-ArrayList源码解析相关推荐

  1. 面试官系统精讲Java源码及大厂真题 - 05 ArrayList 源码解析和设计思路

    05 ArrayList 源码解析和设计思路 耐心和恒心总会得到报酬的. --爱因斯坦 引导语 ArrayList 我们几乎每天都会使用到,但真正面试的时候,发现还是有不少人对源码细节说不清楚,给面试 ...

  2. 《Java修炼指南:高频源码解析》阅读笔记一Java数据结构的实现集合类

    一.Arrays工具类 来自java.util.Arrays,用来处理数组的各种方法. 1.1 List asList(T- a) 用来返回由自定数组支持的固定大小列表,虽然这里返回了一个List,但 ...

  3. 死磕 java同步系列之ReentrantReadWriteLock源码解析

    问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的 ...

  4. 增加数组下标_数组以及ArrayList源码解析

    点击上方"码之初"关注,···选择"设为星标" 与精品技术文章不期而遇 前言 前一篇我们对数据结构有了个整体的概念上的了解,没看过的小伙伴们可以看我的上篇文章: ...

  5. 顺序线性表 ---- ArrayList 源码解析及实现原理分析

    原创播客,如需转载请注明出处.原文地址:http://www.cnblogs.com/crawl/p/7738888.html ------------------------------------ ...

  6. JUC.Condition学习笔记[附详细源码解析]

    JUC.Condition学习笔记[附详细源码解析] 目录 Condition的概念 大体实现流程 I.初始化状态 II.await()操作 III.signal()操作 3个主要方法 Conditi ...

  7. ArrayList源码解析与相关知识点

    ArrayList源码解析于相关知识点(超级详细) 文章目录 ArrayList源码解析于相关知识点(超级详细) ArrayList的继承关系 Serializable标记接口 Cloneable标记 ...

  8. Java集合-ArrayList源码解析-JDK1.8

    ◆ ArrayList简介 ◆ ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了List, RandomAcc ...

  9. java容器三:HashMap源码解析

    前言:Map接口 map是一个存储键值对的集合,实现了Map接口的主要类有以下几种 TreeMap:用红黑树实现 HashMap:数组和链表实现 HashTable:与HashMap类似,但是线程安全 ...

最新文章

  1. 产品设计的Kawaiization
  2. 在Windows 7或Vista资源管理器中禁用缩略图预览
  3. absolute元素在text-align属性下的对齐显示
  4. 排除jar_Gradle排除依赖关系
  5. C语言实现http的下载
  6. 想学习Python,这套教程再适合你不过了!
  7. 如何编写完美的 Python 命令行程序?
  8. Python自动发送邮件提示:smtplib.SMTPServerDisconnected: please run connect() first
  9. CentOS安装tengine(淘宝服务器)
  10. 配置mysql字符_mysql字符设置
  11. php获取本地上传图片地址,php获取CSS文件中图片地址并下载到本地的方法
  12. c++ 11 新特性之 左值右值
  13. OpenCL编程入门(一)
  14. VUE实现SQL在线编辑器,SQL分析器,SQL代码关键字提示
  15. 大数据之路,阿里巴巴大数据实践
  16. 带你啃透深度学习必学“圣经”花书!(附带论文代码精读讲解)
  17. 爱忘事、不自律,有了这5款时间管理工具,堪比“罗志祥”!
  18. mp4播放器带后台开源源码
  19. jmeter压测学习11-模拟浏览器访问web页面
  20. 要想成为郎朗,请AI监督练琴可不行!

热门文章

  1. 准PR控制的谐波抑制方法
  2. string.Format()之格式化数值类型数据
  3. 离阿里最近的机会,来了
  4. 虚拟机下NAT 和 桥接模式 联网操作
  5. 使用CSS样式设置文本超出2行显示为省略号
  6. Unity 2D手游——坦克大战 C#
  7. 用百度文字识别实现图片文本识别
  8. 【Android 10 源码】healthd 模块 HAL 1.0 分析
  9. 在线随机密码生成器源码
  10. 消毒机器人市场前景分析