Java 数组和List的使用「建议收藏」

Java 数组和List的使用「建议收藏」Java中数据的保存离不开数组,但数组的长度是不可变的。这时候就需要列表类(List)来进行数组扩容等操作,同时列表还可以包含批量删除、修改等更方便的内容。同时ArrayList作为使用相当频繁的List类,它的扩容算法效率很高,本文通过其源代码来分析其扩容高效的原因。

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

今天我们来谈谈数组、列表和扩容,以及自写List和Java自带类ArrayList的异同。

Java学习笔记

第一节 Java 类与对象以及继承
第二节 Java 对象的保存和传递
第三节 Java 数组和集合的使用



前言

Java中数据的保存离不开数组,但数组的长度是不可变的,如果初始长度过大,则会造成内存的浪费,降低性能,而数组初始长度过小时,又无法满足大量数据的存储。这时候就需要集合类(ArrayList)来进行数组扩容等操作,同时列表还可以包含批量删除、修改等更方便的内容。


一、数组——同类型数据的集合

Java中的数组的方式和C语言结构类似,都有维度和长度,但由于Java数组的声明方式与C语言略有不同,有两种格式:
类型 数组名[]
类型 [] 数组名
二者也是有区别的,例如:

int a[], b;  声明一个数组a和单个变量b
int[] a, b;  声明数组a和数组b

同时声明数组时我们也可以对其进行初始化:

静态初始化:public String name[] = { 
   "candy","coffee"} ;
动态初始化:name = new String [2] ;

数组在声明时只能在动静态中选择一种方式初始化。数组属于引用型变量,数组变量中存放着数组的首元素的地址,通过数组变量的名字加索引使用数组的元素,这点与C语言类似。

二、ArrayList——封装数组的类

1. 定义集合

Java的列表是一个类,这个类中包含数组,也包含各种处理数组的方法,同时还有必要的get方法以取出保存的数组。实际上Java自带集合:java.util.ArrayList类(父类是List)。为了我们能更好的理解基层原理,我们先自己来定义一个集合类。
在定义集合之前,我们来思考这么一个问题:对于不同的数据类型,如果我们想要使用集合,就需要创建不同的集合来存取。我们可以使用Object类来创建初始数组,这样各种类型的元素都可以存进数组里了:
同时,一个集合至少包含要添加元素、获取数组、获取长度等方法:

public class MyList { 
   
	// 原始数组
	private Object[] arr = new Object[0];
	private int size=0; // 记录数组中数据的长度
	// 添加
	public void add(Object element) { 
   
		// 扩容
			// 创建更长的新数组
			Object[] newArr = new Object[arr.length+1];
			// 复制原数组中的数据:循环遍历
			for(int i=0;i<arr.length;i++) { 
   
				newArr[i] = arr[i];
			}
			// 把新添加的数据保存到新数组中
			newArr[arr.length] = element;
			// 更新数组对象
			arr = newArr;
		//记录长度
		size++;
	}
	// 获取数组
	public Object[] get(){ 
   
		return arr;
	}
	// 获取长度
	public int size() { 
   
		return size;
	}
}

2. 泛型的使用

更多时候,我们需要一个数组里的元素都是同一个子类型的,比如String,或者是我们定义的其他类。
解决方法:泛型,其符号是“<>”。我们可以在类名后加上< E >或者< T >等,其中的字母相当于将类型参数化,就是将类型作为参数传入到方法,这样我们创建List时可以通过泛型限制传入的元素,当出现不符合预期的元素时编译器便会报错:

public class MyList<E> { 
    /*注意本行*/
	private Object[] arr = new Object[0];
	private int size=0; 
	public void add(E element) { 
    /*注意本行*/	
			Object[] newArr = new Object[arr.length+1];
			for(int i=0;i<arr.length;i++) { 
   
				newArr[i] = arr[i];
			}
			newArr[arr.length] = element;
			arr = newArr;
		size++;
	}
	// 获取数组
	public E get(){ 
    /*注意本行*/
		return (E)arr;	/*注意本行*/
	}
	// 获取长度
	public int size() { 
   
		return size;
	}
}

对于泛型使用的地方博主都进行了标注,这样我们就写好了一个相对好用且安全的类。
同时,使用了泛型的类在创建对象时的格式也有改变:

	public static void main(String[] args) { 
   
		MyList<String> list1 = new MyList<String>();
	}

同时等号后的””的的内容可以省略(自动转型),因此也可以写成:

		MyList<String> list1 = new MyList<>();

值得注意的是,泛型不能指代八种基本类型类,只能替代“引用类型”。每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类,包装类均位于java.lang包,包装类和基本数据类型的对应关系如下表所示:

基本类型 包装类 基本类型 包装类
byte Byte int Integer
boolean Boolean long Long
short Short float Float
char Character double Double

自动装箱与自动拆箱机制:装箱就是自动将基本数据类型转换为包装器类型,拆箱就是自动将包装器类型转换为基本数据类型。例如:

	public static void main(String[] args) { 
   
		MyList<Integer> list = new MyList<Integer>();
		int a = 10;
		list.add(a);
		System.out.println("arr[0] = "+list.arr[0]);
		注:本行list.arr正常情况并不合法
		因为arr具有private修饰符
		但是main函数在List类中运行恰巧能够调用
	}

arr[0] = 10

自动拆装箱使得int属性的变量可以被添加进Integer数组中。
我们还可以为MyList添加更多人性化的功能:删除元素,排序、查阅、修改等等。

3. 扩容机制优化

我们之前的MyList类中使用的扩容方法是最“朴实”的:

public void add(E element) { 
   	
		Object[] newArr = new Object[arr.length+1];
		for(int i=0;i<arr.length;i++) { 
   
			newArr[i] = arr[i];
		}
		newArr[arr.length] = element;
		arr = newArr;
	size++;
}

每次添加元素时只扩容一个位置。由于每次扩容时都需要重新拷贝原数组中的元素,因此在添加元素数量非常巨大时,需要重复拷贝的元素数量也会越来越大,从而导致效率很低

public static void main(String[] args) { 
   
	MyList<Integer> list1 = new MyList<>();
	ArrayList<Integer> list2 = new ArrayList<>();
				/*获取系统时间*/
	long time0 = System.currentTimeMillis();
	for(int i=0;i<10000;i++) { 
   
		list1.add(i+1);
	}
	long time1 = System.currentTimeMillis();
	for(int i=0;i<10000;i++) { 
   
		list2.add(i+1);
	}
	long time2 = System.currentTimeMillis();
	System.out.println("ArrayList耗时间:"+(time2-time1)+"\nShapeList耗时间:"+(time1-time0));
	}

ArrayList耗时间:2
MyList耗时间:253

很明显,如果保持低速的线性扩容,效率不高,不难想到根据当前数组的长度,按比例指数型扩容:

public void add(E element) { 
   	
		Object[] newArr = new Object[arr.length*3/2 + 1];
		for(int i=0;i<arr.length;i++) { 
   
			newArr[i] = arr[i];
		}
		newArr[arr.length] = element;
		arr = newArr;
	size++;
}

我们用改进的扩容算法和ArrayList的扩容作对比:
1w数据量已经看不出差距了,我们增加数据量到10w:

ArrayList耗时间:5
MyList耗时间:12

数据量增加到100w时,耗时已经实现了反超:

ArrayList耗时间:73
MyList耗时间:51

但在数据量达到1亿时,结果又是:

MyList耗时间:5504
ArrayList耗时间:2930

不难发现,列表扩容有点像计数“进制”,由于e进制是最高效的,于是我让扩容算法里每次扩大倍数变为e,同时由于数组有最大长度限制(2^31-1)我们需要判断扩容后的大小是否合法:

MyList耗时间:3899
ArrayList耗时间:3503

起初我认为这是由于创建对象也需要时间,大量创建空对象而不赋值,即占用内存又白白耗时。于是我调取了两者扩容时的内存占用,事实表明ArrayList的内存占用更大。

4. ArrayList的扩容机制

ArrayList中add方法的代码每次扩容的操作使用了grow方法:

    private void add(E e, Object[] elementData, int s) { 
   
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

我们继续查看grow的代码

    private Object[] grow(int minCapacity) { 
   
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
   
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else { 
   
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

其中又调用了ArraysSupport.newLength:

public static int newLength(int oldLength, int minGrowth, int prefGrowth) { 
   
        // assert oldLength >= 0
        // assert minGrowth > 0

        int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
        if (newLength - MAX_ARRAY_LENGTH <= 0) { 
   
            return newLength;
        }
        return hugeLength(oldLength, minGrowth);
    }

    private static int hugeLength(int oldLength, int minGrowth) { 
   
        int minLength = oldLength + minGrowth;
        if (minLength < 0) { 
    // overflow
            throw new OutOfMemoryError("Required array length too large");
        }
        if (minLength <= MAX_ARRAY_LENGTH) { 
   
            return MAX_ARRAY_LENGTH;
        }
        return Integer.MAX_VALUE;
    }

Capacity >> 1一行代表将原数组的长度size右移一位,最后在newLength方法中判断最终长度是否合法,再加到原size上。
实际上,先将旧容量右移1位,再加上旧容量就得到了新容量,正数右移1位相当于除以2,在该基础上加旧容量,则等价于新容量 = 旧容量 * 1.5,最后调用Arrays.copyOf()方法进行拷贝,并将elementData指向新数组,而旧数组因为没有引用指向它,很快就会被垃圾收集器回收掉。

我才发现效率差距的问题所在:对于存储器而言,数据都是通过二进制0和1保存,移位对于机器而言是经过底层优化的操作,乘除法也是通过多次移位来实现的,移位效率自然就比普通的乘除法计算高得多。


总结

不能轻视底层架构的学习

在我们一次次使用那些封装好的方法时,我们需要深入了解这些方法的简洁性和必要性,虽然都知道这些封装好的方法使用起来效率高却不知所以然,写的代码自然效率不会很高。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/172291.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 微信支付java实例_java开发微信应用

    微信支付java实例_java开发微信应用JAVA开发集成微信支付(WXPay)遇到的坑!

    2022年4月20日
    39
  • labelme使用教程_labelme和labelimg区别

    labelme使用教程_labelme和labelimg区别LabelMe可用于实例分割,语义分割,目标检测,分类任务的数据集标注工作。在线标注版本:http://labelme2.csail.mit.edu/Release3.0/index.php?message=1python版本:https://github.com/wkentaro/labelme分类标注:Classification目标检测标注:ObjectDetection语义分割标注:SemanticSegmentation实例分割标注:InstanceSegmentation视频

    2022年9月12日
    0
  • COM :IUnknown接口QueryInterface函数介绍

    COM :IUnknown接口QueryInterface函数介绍
    一、COM组件的目标:
    COM组件的一个主要优势是:便于升级。
    要实现这个优势需要满足一下两个条件:
    1、运行时从客户程序动态加载和卸载,采用DLL技术可以实现。
    2、为了更好的突出DLL的优势,还需要信息隐藏,即封装性。
    二、COM组件的信息隐藏采用IUnknown接口来实现:
          1、IUnknown接口功能简介:
    IUnknown意思是未知,即未知的接口。采用这个名字是为了简单起见,所有的COM接口都需要继承I

    2022年7月22日
    7
  • 股票开店理论+T+0滚动操盘法

    股票开店理论+T+0滚动操盘法

    2021年8月19日
    49
  • Java 多线程(超详细)

    Java 多线程(超详细)多线程学习思路:为什么学习线程?为了解决CPU利用率问题,提高CPU利用率。=》什么是进程?什么是线程?=》怎么创建线程?有哪几种方式?有什么特点?=》分别怎么启动线程?=》多线程带来了数据安全问题,该怎么解决?=》怎么使用synchronized(同步)决解?=》使用同步可能会产生死锁,该怎么决解?=》线程之间是如何通信的?=》线程有返回值吗?该如何拿到?=》怎么才能一次性启动几百上千个的线程?线程的概念什么是进程进程是操作系统中正在执行的不同的应用程序,例如:我

    2022年7月9日
    19
  • 分布式事务TCC方案Hmily——springcloud + feign + mybatis

    分布式事务TCC方案Hmily——springcloud + feign + mybatisTCC理论:分布式事务基础理论——TCCHmily介绍:分布式事务TCC方案——Hmily金融级柔性分布式事务解决方案介绍本文demo代码:GitHub依赖<dependency><groupId>org.dromara</groupId><artifactId>hmily-springcloud</artifactId><vers

    2022年5月13日
    69

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号