【Java集合】ArrayList源码分析
ArrayList
是日常开发中经常使用到的集合,其底层采用数组实现,因此元素按序存放。其优点是可以使用下标来访问元素,时间复杂度是O(1)。其缺点是删除和增加操作需要使用System.arraycopy()
来移动部分受影响的元素,时间复杂度为O(N)。同时ArrayList
由于是采用数组来存放数据,因此会有容量限制,在使用时需要扩容,当插入操作超出数组长度,就会进行扩容,扩容后数组的长度为原来的1.5倍,默认的数组长度是10。
为了更好的掌握ArrayList
,因此阅读并仿照ArrayList
源代码,实现一个具有增删改查以及自动扩容功能的简易版MyArrayList
(代码几乎与ArrayList源码一致)。
首先新建一个class,命名为MyArrayList
。
public class MyArrayList<E> {
}
由于ArrayList是通过数组来存储元素的,因此我们定义一个Object
类型的数组elementData
来存储数据,再定义一个变量size
,用来记录数组中的元素个数,其默认值为0。
public class MyArrayList<E> {
private Object[] elementData; //ArrayList存储元素的对象数组
private int size; //ArrayList存储元素的个数
}
接下来就是构造函数,有两种,第一种是未指定初始容量的构造函数,默认容量为10;第二种是在构造函数中传入指定容量。
先说第一种构造函数,ArrayList在默认情况下,其容量为10。因此我们定义一个常量DEFAULT_CAPACITY = 10
作为默认容量。同时,还定义一个常量数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
用于对elementData
进行初始赋值。
public class MyArrayList<E> {
private Object[] elementData; //ArrayList存储元素的对象数组
private int size; //ArrayList存储元素的个数
private final static int DEFAULT_CAPACITY = 10; //ArrayList的对象数组的默认初始容量
private final static Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList对象数组的默认初始化
/**
* 不指定初始容量的构造函数
*/
public MyArrayList(){
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
}
需要注意的是这里的默认容量10并不是在构造函数中直接使用,而是在第一次插入进行扩容时才会使用。
第二种构造函数,传入指定的容量。根据传入的容量参数,我们有以下三种结果:
①传入的容量参数大于0:则以该参数为容量创建对象数组
②存入的容量参数等于0:则需要创建一个空的对象数组,因此定义一个常量数组EMPTY_ELEMENTDATA = {}
用于传入容量为0时的初始化。
③传入的容量参数小于0:明显这是非法的,因此抛出参数异常IllegalArgumentException()
public class MyArrayList<E> {
private Object[] elementData; //ArrayList存储元素的对象数组
private int size; //ArrayList存储元素的个数
private final static int DEFAULT_CAPACITY = 10; //ArrayList的对象数组的默认初始容量
private final static Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList对象数组的默认初始化
private static final Object[] EMPTY_ELEMENTDATA = {}; //传入容量为0时的初始化
/**
* 不指定初始容量的构造函数
*/
public MyArrayList(){
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 传入指定初始容量的构造函数
* @param initialCapacity 传入的初始化容量
*/
public MyArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("非法的容量: "+
initialCapacity);
}
}
}
好了,构造函数构建完毕,接下来就是增删改查功能的实现,实现的方法如下:
//增,将元素放到数组末尾元素的后面,e为待插入元素,返回boolean
boolean add(E e)
//删,删除指定位置的元素,index为待删除元素的位置,返回被删除的元素
E remove(int index)
//改,替换指定位置的元素,index为被替换元素的位置,e为替换的元素,返回被替换的元素
E set(int index, E e)
//查,查询指定位置的元素,index为查询的位置,返回查到的元素
E get(int index)
首先是add(E e)
方法,由于数组容量有限制,因此我们新增一个元素,都有可能要进行扩容,所以我们需要编写一个函数ensureCapacityInternal
来判断是否需要自动扩容,若需要则进行扩容。
/**
* ArrayList的add方法
* 将元素放到数组末尾元素的后面
* @param e 待插入的元素
* @return
*/
boolean add(E e){
//1、自动扩容机制,传入的是目前需要的最小容量
ensureCapacityInternal(size + 1);
}
在ensureCapacityInternal
函数中,需要传入目前需要的最小容量。同时我们还要判断对象数组elementData
是否为空数组,若是,则将传入的目前需要的最小容量与默认容量10进行对比,取其中的最大值作为本次扩容的容量。
/**
* 判断原数组是否为空数组
* 是:则选默认容量和目前需要的最小容量二者中的最小值
* 否:则继续往下判断
* @param minCapacity 目前需要的最小容量
*/
void ensureCapacityInternal(int minCapacity){
// elementData 为空数组,则将传入的minCapacity与默认的初始容量DEFAULT_CAPACITY进行对比,取两者中最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
}
//接着往下判断
ensureExplicitCapacity(minCapacity);
}
接下来,我们判断是否需要进行扩容。如果目前需要的最小容量大于原数组的长度,才进行扩容,否则不进行扩容,该功能写入函数 ensureExplicitCapacity
。
/**
* 判断是否需要进行扩容
* 如果目前需要的最小长度大于原数组的长度,才进行扩容
* 否则不进行扩容
* @param minCapacity 目前需要的最小容量
*/
void ensureExplicitCapacity(int minCapacity){
//目前需要的最小容量超过原数组长度,才进行扩容,否则就不扩容
if (minCapacity - elementData.length > 0) {
grow(minCapacity); //扩容
}
}
然后,若进行扩容,则执行扩容函数grow
。在grow
中,我们需要进行如下操作:
①获取原数组的容量oldCapacity
,然后计算出值为oldCapacity
1.5倍的新容量newCapacity
int oldCapacity = elementData.length;
//扩容为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
②将扩容1.倍后的新容量newCapacity
与目前需要的最小容量minCapacity
进行对比,若新容量小于目前需要的最小容量,则新容量的值取目前需要的最小容量。
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
③将新容量newCapacity
与所允许的数组的最大长度进行对比,数组最大长度定义为常量MAX_ARRAY_SIZE = Integer.MAX_VALUE
,值为整数的最大值。如果新容量newCapacity
大于数组最大长度MAX_ARRAY_SIZE
,则取目前需要的最小容量minCapacity
与数组最大长度MAX_ARRAY_SIZE
两者中的最小值作为新容量newCapacity
的值。
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;
④使用Arrays.copyOf(原数组, 新长度)
进行数组的复制,即实现数组扩容
elementData = Arrays.copyOf(elementData,newCapacity);
完成扩容任务的函数grow
如下:
/**
* 扩容函数:如何进行扩容(扩多少)
* ①扩容1.5倍
* ②若扩容1.5倍还不满足需要的最小容量,则扩容长度为目前需要的最小容量
* ③若新的容量大于数组所允许的最大长度,则取需要的最小容量与数组所允许的最大长度
* 两者中的最小值
* @param minCapacity 目前需要的最小容量
*/
void grow(int minCapacity){
int oldCapacity = elementData.length;
//oldCapacity原数组长右移1位,即相当于除2,最后加原长度,则为扩容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容1.5倍后的新容量小于需要的最小容量,则新的容量即为传入的最小容量
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
//如果新容量大于数组能够允许的最大长度,则看传入的最小容量与数组最大长度对比,取其中的小者
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;
//Arrays.copyOf(原数组, 新长度),返回新数组。使用该函数完成自动扩容
elementData = Arrays.copyOf(elementData,newCapacity);
}
至此,就完成了新增时,判断是否需要扩容,并且完成扩容功能。接下来我们只需要将新增元素插入数组元素末尾位置的下一个位置,并返回true即可。
boolean add(E e){
//1、自动扩容机制,传入的是目前需要的最小容量
ensureCapacityInternal(size + 1);
//2、扩容完毕,将元素存入
elementData[size++] = e;
return true;
}
最终,新增add方法和自动扩容有关的函数编写完成:
/**
* ArrayList的add方法
* 将元素放到数组末尾元素的后面
* @param e 待插入的元素
* @return
*/
boolean add(E e){
//1、自动扩容机制,传入的是目前需要的最小容量
ensureCapacityInternal(size + 1);
//2、扩容完毕,将元素存入
elementData[size++] = e;
return true;
}
/**
* 判断原数组是否为空数组
* 是:则选默认容量和目前需要的最小容量二者中的最小值,然后接着往下判断
* 否:则直接继续往下判断
* @param minCapacity 目前需要的最小容量
*/
void ensureCapacityInternal(int minCapacity){
// elementData 为空数组,则将传入的minCapacity与默认的初始容量DEFAULT_CAPACITY进行对比,取两者中最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
}
//接着往下判断
ensureExplicitCapacity(minCapacity);
}
/**
* 判断是否需要进行扩容
* 如果目前需要的最小长度大于原数组的长度,才进行扩容
* 否则不进行扩容
* @param minCapacity 目前需要的最小容量
*/
void ensureExplicitCapacity(int minCapacity){
//目前需要的最小容量超过原数组长度,才进行扩容,否则就不扩容
if (minCapacity - elementData.length > 0) {
grow(minCapacity); //扩容
}
}
/**
* 扩容函数:如何进行扩容(扩多少)
* ①扩容1.5倍
* ②若扩容1.5倍还不满足需要的最小容量,则扩容长度为目前需要的最小容量
* ③若新的容量大于数组所允许的最大长度,则取需要的最小容量与数组所允许的最大长度
* 两者中的最小值
* @param minCapacity 目前需要的最小容量
*/
void grow(int minCapacity){
int oldCapacity = elementData.length;
//oldCapacity原数组长右移1位,即相当于除2,最后加原长度,则为扩容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容1.5倍后的新容量小于需要的最小容量,则新的容量即为传入的最小容量
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
//如果新容量大于数组能够允许的最大长度,则看传入的最小容量与数组最大长度对比,取其中的小者
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;
//Arrays.copyOf(原数组, 新长度),返回新数组。使用该函数完成自动扩容
elementData = Arrays.copyOf(elementData,newCapacity);
}
接下来,就是删除,remove
方法。由于该方法传入待删除元素的位置索引index
,因此需要检查index
的范围是否符合要求。编写一个函数rangeCheck
来检查下标。
/**
* 检查index范围
* 超出范围则抛出异常
* @param index 数组下标位置
*/
void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException("哎呀,超出范围了!");
}
若index
没有超出范围,则接下来就是获取索引对应的元素,获取方式很简单,就是elementData[index]
即可。考虑到其他方法也会需要通过这样方式来获取对应位置的元素,因此我们将这个操作抽取出来,成为一个函数elementData()
,用于获取元素。
/**
* 返回数组中指定位置的元素
* @param index
* @return
*/
E elementData(int index){
return (E) elementData[index];
}
那么,目前remove
方法前面两个操作我们已经完成
E remove(int index){
//1、检查index范围
rangeCheck(index);
//2、获取index对应的元素
E oldValue = elementData(index);
}
删除index
元素,需要把该位置后面的所有元素都向前移动一个位置。因此接下来我们就需要将index
后面的元素向前移动一个位置。
具体做法是,先计算出需要移动的元素个数numMoved
,用数组中最后一个元素的下标减去index
即可获得需要移动的元素个数:size-1-index
。
然后利用System.arraycopy()
来移动元素,该方法的用法如下:
System.arrayCopy(Object srcArray,int srcPos,Object destArray ,int destPos,int length)
①Object srcArray 原数组(要拷贝的数组)
②int srcPos 要复制的原数组的起始位置(数组从0位置开始)
③ Object destArray 目标数组
④ int destPos 目标数组的起始位置
⑤int length 要复制的长度
从原数组srcArray 取元素,范围为下标srcPos到srcPos+length-1,取出共length个元素,存放到目标数组中,存放位置为下标destPos到destPos+length-1。
我们将原数组和目标数组都设为elementData
,然后原数组的起始位置为index
+1,目标数组的起始位置为index
,要复制的长度设为元素个数numMoved
。这样就能做到将数组index
位置后面的元素向前移动一位。
不过这样做目标数组的最后一位元素依然是原来的数,因此我们需要将目标数组最后的元素置为null,并且由于是删除,所以元素个数size
需要减一。至此,删除方法remove
完成。
/**
* ArrayList的remove方法
* @param index 要删除元素的位置
* @return 返回被删除元素
*/
E remove(int index){
//1、检查index范围
rangeCheck(index);
//2、获取index对应的元素
E oldValue = elementData(index);
//3、计算需要移动的元素的个数
int numMoved = size - 1 - index;
//4、将index后面的数往前移动一位
if (numMoved > 0){
System.arraycopy(elementData,index + 1, elementData, index, numMoved);
}
//5、把最后的元素置为null
elementData[--size] = null;
//返回被删除元素
return oldValue;
}
增删操作已完成,接下来就是改操作,set(
)方法。这个方法就比较简单,具体的步骤如下:
①检查index
范围
②获取index
位置的元素
③将index
位置的元素,替换为传入的元素
④返回原先index
位置的元素
/**
* ArrayList的set
* @param index 需要修改的位置
* @param e 需要替换的元素
*/
E set(int index, E e){
//1、检查index范围
rangeCheck(index);
//2、获取指定位置的元素
E oldValue = elementData(index);
//3、替换元素
elementData[index] = e;
//4、返回原先index位置的元素
return oldValue;
}
最后,就是查操作,get
方法。该方法更为简单,只需要先检查index
范围,再获取index
位置的元素直接返回即可。
/**
* ArrayList的get方法
* @param index
* @return
*/
E get(int index){
//1、检查index范围
rangeCheck(index);
//2、获取指定位置的元素
return elementData(index);
}
到这里,我们编写的简易版ArrayList
的增删改查操作就全部完成了。点进JDK1.8中ArrayList
源码可以看到,我们的上面的代码几乎与ArrayList
源码一模一样。
最终这个简易版ArrayList
所有代码如下:
public class MyArrayList<E> {
private int size; //ArrayList中实际元素的数量的size
private Object[] elementData; //ArrayList的对象数组
private final static int DEFAULT_CAPACITY = 10; //ArrayList的对象数组的默认初始容量
private final static int MAX_ARRAY_SIZE = Integer.MAX_VALUE; //数组的最大长度,也就是整数的最大值
private final static Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //ArrayList的对象数组的默认初始化
private static final Object[] EMPTY_ELEMENTDATA = {}; //传入容量为0时的初始化
/**
* 不指定初始容量的构造函数
*/
public MyArrayList(){
elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 传入指定初始容量的构造函数
* @param initialCapacity
*/
public MyArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("非法的容量: "+
initialCapacity);
}
}
/**
* ArrayList的add方法
* 将元素放到数组有效长度的末尾
* @param e 待插入的元素
* @return
*/
boolean add(E e){
//1、自动扩容机制,传入的是目前需要的最小容量
ensureCapacityInternal(size + 1);
//2、扩容完毕,将元素存入
elementData[size++] = e;
return true;
}
/**
* 判断原数组是否为空数组
* 是:则选默认容量和目前需要的最小容量二者中的最小值,然后接着往下判断
* 否:则直接继续往下判断
* @param minCapacity 目前需要的最小容量
*/
void ensureCapacityInternal(int minCapacity){
// elementData 为空数组,则将传入的minCapacity与默认的初始容量DEFAULT_CAPACITY进行对比,取两者中最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
}
//接着往下判断
ensureExplicitCapacity(minCapacity);
}
/**
* 判断是否需要进行扩容
* 如果目前需要的最小长度大于原数组的长度,才进行扩容
* 否则不进行扩容
* @param minCapacity 目前需要的最小容量
*/
void ensureExplicitCapacity(int minCapacity){
//目前需要的最小容量超过原数组长度,才进行扩容,否则就不扩容
if (minCapacity - elementData.length > 0) {
grow(minCapacity); //扩容
}
}
/**
* 扩容函数:如何进行扩容(扩多少)
* ①扩容1.5倍
* ②若扩容1.5倍还不满足需要的最小容量,则扩容长度为目前需要的最小容量
* ③若新的容量大于数组所允许的最大长度,则取需要的最小容量与数组所允许的最大长度
* 两者中的最小值
* @param minCapacity 目前需要的最小容量
*/
void grow(int minCapacity){
int oldCapacity = elementData.length;
//oldCapacity原数组长右移1位,即相当于除2,最后加原长度,则为扩容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容1.5倍后的新容量小于需要的最小容量,则新的容量即为传入的最小容量
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
//如果新容量大于数组能够允许的最大长度,则看传入的最小容量与数组最大长度对比,取其中的小者
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity - MAX_ARRAY_SIZE) > 0 ? MAX_ARRAY_SIZE : minCapacity;
//Arrays.copyOf(原数组, 新长度),返回新数组。使用该函数完成自动扩容
elementData = Arrays.copyOf(elementData,newCapacity);
}
/**
* ArrayList的remove方法
* @param index 要删除元素的位置
* @return 返回被删除元素
*/
E remove(int index){
//1、检查index范围
rangeCheck(index);
//2、获取index对应的元素
E oldValue = elementData(index);
//3、计算需要移动的元素的个数
int numMoved = size - 1 - index;
//4、将index后面的数往前移动
if (numMoved > 0){
System.arraycopy(elementData,index + 1, elementData, index, numMoved);
}
//5、把最后的元素置为null
elementData[--size] = null;
//返回被删除元素
return oldValue;
}
/**
* 检查index范围
* 超出范围则报错
* @param index
*/
void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException("哎呀,超出范围了!");
}
/**
* 返回数组中指定位置的元素
* @param index
* @return
*/
E elementData(int index){
return (E) elementData[index];
}
/**
* ArrayList的set
* @param index 需要修改的位置
* @param e 需要替换的元素
*/
E set(int index, E e){
//1、检查index范围
rangeCheck(index);
//2、获取指定位置的元素
E oldValue = elementData(index);
//3、替换元素
elementData[index] = e;
//4、返回原先index位置的元素
return oldValue;
}
/**
* ArrayList的get方法
* @param index
* @return
*/
E get(int index){
//1、检查index范围
rangeCheck(index);
//2、获取指定位置的元素
return elementData(index);
}
/**
* 获取元素个数
* @return
*/
int size(){
return size;
}
}
我们测试一下,这个简易版ArrayList
public static void main(String[] args) {
MyArrayList<String> myArrayList = new MyArrayList<>();
//增
myArrayList.add("hello");
myArrayList.add("word");
myArrayList.add("hello");
myArrayList.add("java");
//改
myArrayList.set(1,"####");
//删
myArrayList.remove(2);
//查
for (int i = 0; i < myArrayList.size(); i++){
System.out.println(myArrayList.get(i));
}
}
测试结果如下:
hello
####
java
【Java集合】ArrayList源码分析的更多相关文章
- Java集合-ArrayList源码分析
目录 1.结构特性 2.构造函数 3.成员变量 4.常用的成员方法 5.底层数组扩容原理 6.序列化原理 7.集合元素排序 8.迭代器的实现 9.总结 1.结构特性 Java ArrayList类使用 ...
- Java入门系列之集合ArrayList源码分析(七)
前言 上一节我们通过排队类实现了类似ArrayList基本功能,当然还有很多欠缺考虑,只是为了我们学习集合而准备来着,本节我们来看看ArrayList源码中对于常用操作方法是如何进行的,请往下看. A ...
- Java中ArrayList源码分析
一.简介 ArrayList是一个数组队列,相当于动态数组.每个ArrayList实例都有自己的容量,该容量至少和所存储数据的个数一样大小,在每次添加数据时,它会使用ensureCapacity()保 ...
- Java基础 ArrayList源码分析 JDK1.8
一.概述 本篇文章记录通过阅读JDK1.8 ArrayList源码,结合自身理解分析其实现原理. ArrayList容器类的使用频率十分频繁,它具有以下特性: 其本质是一个数组,因此它是有序集合 通过 ...
- Java集合框架源码分析(2)LinkedList
链表(LinkedList) 数组(array)和数组列表(ArrayList)都有一个重大的缺陷: 从数组的中间位置删除一个元素要付出很大的代价,因为数组中在被删除元素之后的所有元素都要向数组的前端 ...
- Java集合ArrayList源码解读
最近在回顾数据结构,想到JDK这样好的代码资源不利用有点可惜,这是第一篇,花了心思.篇幅有点长,希望想看的朋友认真看下去,提出宝贵的意见. :) 内部原理 ArrayList 的3个字段 priva ...
- Java集合-ArrayList源码解析-JDK1.8
◆ ArrayList简介 ◆ ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了List, RandomAcc ...
- Java集合——ArrayList源码详解
) ArrayList 实现了RandomAccess, Cloneable, java.io.Serializable三个标记接口,表示它自身支持快速随机访问,克隆,序列化. public clas ...
- 【thinking in java】ArrayList源码分析
简介 ArrayList底层是数组实现的,可以自增扩容的数组,此外它是非线程安全的,一般多用于单线程环境下(Vector是线程安全的,所以ArrayList 性能相对Vector 会好些) Array ...
- Java集合-LinkedList源码分析
目录 1.数据结构-链表 2.ArrayList结构特性 3.构造方法 4.成员变量 5.常用的成员方法 6.Node节点 7.序列化原理 8.迭代器 9.总结 1.数据结构-链表 链表(Linked ...
随机推荐
- Fedora镜像下载地址
Fedora镜像下载地址 Fedora 7核心源码包在: http://archives.fedoraproject.org/pub/archive/fedora/linux/releases/7/F ...
- DOCKER学习_011:使用Dockerfile制作docker镜像
前面使用commit的方式,制作一个docker镜像,本次介绍使用Dockerfile制作一个dockers镜像 [root@docker-server3 ~]# mkdir /openssh [ro ...
- STM32的引脚的配置
http://blog.csdn.net/u010592722/article/details/45746079
- 3分钟 Markdown 快速入门(超详细)(Day_33)
Markdown 快速入门 (这个贼重要)注:所有符号要在英文状态下完成哦,中文是没有效果的. 1.标题 # 表示一级标题 ## 表示二级标题 ### 表示三级标题 #### 表示四级标题 ##### ...
- AlertDailog中的which问题
在做一个AlertDialog的点击事件设置的时候: AlertDialog.Builder(this).apply { var numberIndex = 0 setTitle("choo ...
- Pandas之:Pandas简洁教程
Pandas之:Pandas简洁教程 目录 简介 对象创建 查看数据 选择数据 loc和iloc 布尔索引 处理缺失数据 合并 分组 简介 pandas是建立在Python编程语言之上的一种快速,强大 ...
- App自动化测试之Appium环境安装(涉及雷电模拟器和真机)
1.安装Microsoft .NET Framework 4.5 及以上版本 2.安装Appium 官方网站地址:http://appium.io/ 我装了1.17.0版本 3.安装JDK 1.8及以 ...
- Python+Selenium学习笔记19 - 自动发送邮件
发送简单的邮件 用一个QQ邮箱发送到另一个QQ邮件. 首先设置QQ邮箱,邮箱设置 -> 账号 开启SMTP服务,点击开启按钮,按提示进行操作,需要1毛钱的短信费.开启后如下所示 1 # codi ...
- grasshopper | 通过图层引用线条 报错:“ Data conversion failed from Guid to Curve ”的避免方法
需求:通过 LunchBox - > layer reference 电池 可以快速选中图层所在的线条,但是选择的数据流错误 直接选择会报错--"Data conversion fai ...
- 从性能角度帮你理解HTTP协议
因为做性能测试分析的人来说,HTTP 协议可能是绕不过去的一个槛.在讲 HTTP 之前,我们得先知道一些基本的信息. HTTP(HyperText Transfer Protocol,超文本传输协议) ...