泛型数组列表及包装类
泛型数组列表
声明数组列表
访问数组列表元素
类型化与原始数组列表的兼容性
对象包装器与自动装箱
自动装箱与自动拆箱
注意
参数数量可变的方法
往期文章回顾
泛型数组列表、包装类及参数可变方法
泛型数组列表
在许多的程序设计语言中,数组的大小必须在「编译」期间就确定。
但在Java
中,我们可以在「运行时」确定数组的大小。
int a....;
var staff=new Employee[a];
虽然staff数组能够在运行时确定数组的大小,但是仍然没有办法解决动态更改数组的问题(即不能够随意对数组的大小进行操作)。
在Java
中,有一个集合类ArrayList
,类似于数组,但是能在添加和删除元素时,自动调整数组的容量(其底层是基于数组实现的),而不需要编写任何的代码。
值得一提的是ArrayList
是一个支持「泛型」的,有「类型参数」的类。
❝泛型:泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
❞
为了指定数组列表要保存的元素对象的类型,需要用一对「尖括号」将「类名」括起来加到ArrayList
之后。由于对于ArrayList
的具体实现进行了封装,所有我们不需要知道它究竟是如何实现的。
声明数组列表
声明定义
以下是Java10
之后的声明数组列表的一种常用形式,用到了var
关键字,并且使用了泛型类Employee
var staff=new ArrayList<Employee>()
在Java10
之前一般都是这么写的:
ArrayList<Employee> staff=new ArrayList<>()
当没有使用var
关键字之时,我们可以省去右边的「类型参数」
误区
var elements=new ArrayList<>()
这样会产生一个奇怪的数组列表:ArrayList<Object>
究其原因在于:「没有指定明确的类型参数,var语法会要求后面的new操作指定明确的类型参数,否则默认产生Object类型的类型参数。」
常见方法
add
方法
该方法可以将任意的符合类型参数条件的元素添加至数组列表,并且随着元素的增加,动态调整数组列表的大小。
staff.add(new Employee(“Harry”.....));
staff.add(new Employee(“Hacker”....));
...............
数组列表管理着一组内部的对象引用数组,所存储的都是一组指定类对象的引用。
ensureCapacity
方法
如果已经能够预计出数组可能存储的元素数量,就可以在填充数组之前调用该方法
staff.ensureCapacity(n)
该方法会分配一个包含n个对象的内部数组,这说明在调用add方法不超过n次都不会带来很大的空间分配开销。
除此之外,还可以直接在创建数组列表之时就将初始容量传递给构造器
var staff=new ArrayList<Employee>(n)
警告
分配数组的两种方式:一种是分配给数组列表,一种是分配新数组。即:new ArrayList<Employee>(100)
和new Employee[100]
有着明显的区别。
前者指的是数组列表的初始容量是100,可能列表内的元素个数少于100,可能为100,也可能多于100个。是可以动态变化的。 后者指的是数组的容量为100,是固定的,即最大的存储元素个数为100,可以少于100,但绝对不可以多于100。否则会出现异常。
size()
方法
该方法可以返回数组列表内的元素个数。如:staff.size()
它等价于数组a的a.length()
trimToSize()
方法
一旦可以确认数组列表内的元素个数将保持恒定,不再变化之时,可以调用该方法,将存储块的大小调整为保存当前元素数量所需要的存储空间,其余的剩余空间将由垃圾回收器回收。一旦使用了该方法,再添加元素时,就需要再次移动存储块(类似数组的插入元素操作进行扩容)。
小结
ArrayList
常见的方法
方法名 | 方法解释 |
---|---|
ArrayList<E>() | 构造一个空数组列表 |
ArrayList<E>(int initialCapacity) | 指定容量构造一个空数组列表 |
boolean add(E obj) | 在数组列表末尾追加一个元素,永远返回true |
int size() | 返回当前存储在数组列表中的元素个数(个数永远不大于数组列表的容量) |
void ensureCapacity() | 确保数组列表在不重新分配内部存储数组的情况下有足够的容量存储给定数量的元素 |
void trimToSize() | 将数组列表的容量调整至当前大小 |
访问数组列表元素
凡是事物必定有两面性,虽然数组列表的自动扩容机制给我们在插入元素之时带来极大的便利(无需再考虑空间分配问题),但是在「访问元素」方面,数组列表却因为自动扩容机制增加了复杂性。
从根本上说,ArrayList
不是Java
程序设计语言的一部分,而是由人们编写并在标准库中的一个工具类,因此对于访问数组列表内的元素,不能够像数组一样的格式来操作,而需要get
和set
方法。
例如:在staff内设置元素
staff.set(i,harry)
<=>a[i]=harry
,从某种程度上来说,两者是等价的。
此外,对于ArrayList
类需要区分add()
方法与set()
方法
add()
方法是为数组列表添加新的元素,即数组列表内原先并不存在的元素。set()
方法是为数组列表替换某个指定的已经存在的元素,即数组列表内原先存在的元素。
当我们要获得一个元素之时,可以用get()
方法
Employee e=staff.get(i);
//等价于
Employee e=a[i];
一举两得(增删元素和访问元素都最优)
创建一个数组列表,并向其中添加所有的元素。 使用 toArray()
方法将数组列表拷贝到一个数组中去。
var list=new ArrayList<X>();
while(....){
X=......
list.add(X);
}
var a=new X[list.size()];
list.toArray(a);
小结
删除和增加操作虽然效率比较低,但是适合比较小的数组列表。 对于要存储的元素较多,并且增删操作较多,可以考虑使用「链表」 并且我们可以使用“foreach”循环来遍历数组列表。
类型化与原始数组列表的兼容性
在之前的数组列表中都使用到了「类型参数」这一特性,提高了程序的安全性,但总有一些类本身并没有类型参数,那么该如何处理这些特殊类呢?(泛型自JDK1.5之后出现)
来看两个例子
public class EmployeeDB{
public void update(ArrayList list){......}
public static ArrayList find (String equry){.......}
}
此时可以将一个类型化的数组列表传递给update方法,不需要「任何的强制类型转换」。
但是如果调用find方法将返回一个原始类型的ArrayLis,此时若赋值给一个类型化的ArrayList会得到一个「警告」
ArrayList<String> result=find(String equry)
即使进行强制类型转换也不能够避免警告的出现
对象包装器与自动装箱
相信在之前的学习中都见过Integer等类,这些就是与基本数据类型相对应的包装器类。
常见的包装器类及其对应关系如下:
基本数据类型 | 对应的包装器类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
void | Void |
void其实也是 Java
的基本数据类型,只不过我们不能够直接对其进行操作。Integer、Long、Float、Double、Short、Byte、Character派生于公共超类Number 包装器类是不可变的,一旦构造了包装器,其中的值便不能再被改变。 包装器类也是final的,不能够派生他们的子类。 当我们要声明一个整型、浮点型、布尔型、字符型或者字符串型的数组变量时需要用到他们对应的包装器类。 使用包装器类创建数组列表时一定程度上会影响效率。
自动装箱与自动拆箱
来看个例子
相反,当将一个Integer对象赋给int值时,将会「自动拆箱」
int n=list.get(i);
//等价于
int n=list.get(i).intValue();
自动拆装箱的技术也可以用于「算术表达式」
很多人都认为基本数据类型与它的对象包装器一样。但是他们有一点很大的不同:「同一性」
==运算符可以用来比较包装器对象,只不过是检测他们的内存地址。因此以下的比较经常会失败:
Integer a=100;
Integer b=100;
if(a==b){......}
不过Java
实现却「有可能」,即:将经常出现的值包装到相同的对象中,这种比较就可能成功,但并不是我们所希望的。
解决这个问题是在比较包装器对象时调用equals方法
❝自动装箱规范要求boolean、byte、char小于等于127的,介于-128~127之间的short和int被包装到固定的对象中。
❞
注意
包装器类的引用可以为null,所以自动装箱可能会抛出一个异常 如果在条件表达式中混合使用Integer和Double,必须先将Integer值就会拆箱,提升为double,再装箱为Double,再进行计算。 装箱和拆箱都是「编译器」要做的事,而不是虚拟机。 编译器在生成字节码文件时会插入必要的方法调用,虚拟机是执行这些字节码文件 包装器的使用可以将某些基本方法放在包装器中会很方便。(如将字符串转换成整数)
参数数量可变的方法
在Java
中可以提供「参数可变」的方法(有时候将其称为变参方法)
例:
System.out.printf("%d",n);
System.out.printf("%d %s",n,"widgets");
public class PrintStream{
public PrintStream printf(String fmt,Object...args){
return format(fmt,args);
}
}
这里的省略号是代码的一部分,表明这个方法可以接受任意数量的对象。(除fmt参数) printf方法接受两个参数,一个是「格式化字符串」,一个是Obejct[]数组,其中保存着所有其他参数(如果调用者提供的是整数或者其他基本数据类型的值,会把它们自动装箱为对象) 不可避免地要扫描fmt字符串,并将第i个格式说明符与args[i]的值匹配起来。 Obejct…参数类型与Obejct[]完全一样。 允许将数组作为最后一个参数传递给有可变参数的方法
例:求出若干个数值中最大的数
public static double max(double... values){
double largest=Double.NEGATIVE_INFINITY;
for (double v:values){
if (v>largest){
largest=v;
}
}
return largest;
}
double m=max(new double[]{3.1, 40.4, -5});
System.out.println(m);