一、关于集合
提到集合,就不得不和数组做比较。
关于数组,我们知道数组是用来存放相同类型数据元素的集合(此集合非彼集合)。
那么问题来了。
数组能不能存放不同类型的数据呢?
public static void main(String[] args) {
/*
* 数组是用来存储相同数据类型的元素的
*
* 元素可以是基本数据类型
* 还可以是引用数据类型
*/
int[] arr = {1,2,3,4,5};
String[] arr1 = {"1","a","wang"};
/*
* 数组能不能存储不同数据类型的元素?
* 能
* 可以使用Object[]
*/
Object[] objarr = {"abc",new Integer(4)};
System.out.println(Arrays.toString(objarr));
}
那么,还有一个问题。
数组能不能随便改变长度呢?
这个问题不用多想,答案是不能。但如果想动态改变数组的长度的话,可以自己设计一些算法来改变,但是数组的本质是定长的,以下是一个动态改变数组长度的例子。
private String[] arr;
public MyArrayList(){
arr = new String[0];
}
public boolean add(String str) {
String[] arr=new String[this.arr.length+1];
System.arraycopy(this.arr, 0, arr, 0, this.arr.length);
arr[arr.length-1] = str;
this.arr = arr;
return true;
}
正因为数组不能随意改变长度的特点,所以便有了集合。
集合是什么?
集合类存放在java.util包中,集合是用来存放一系列对象的容器。
注意:
(1)集合只能存放引用类型的数据,不能存放基本数据类型,不要认为能在集合中存1,就认为集合能够存放基本数据类型,实际上存的这个1是被自动装箱成Integer类型后存入的,在java中每一种基本数据类型都有对应的包装类,即引用类型。
(2)集合可以存放多种不同类型的数据,可以存储任意数量的数据,需要注意的是在Java SE 5.0以前,集合的元素只要是引用类型就可以存放,但在使用时需要进行强制类型转换,非常不方便,所以在Java SE 5.0之后增加了一个新特性“泛型”,用来指定存入集合中的对象类型,避免了强制类型转换的麻烦,但也让集合只能存放相同对象类型的数据。
(3)在集合中,存放的对象的引用(地址),并不是具体的值,对象本身是放到堆内存当中的。
集合框架图
从框架图中可以看到,除了Map以外,其他的集合类都实现了Collection接口,具体的特性在框架图里都有简单的介绍。
二、Collection---List
对应的包是:java.util.List
根据实现类的不同,需要导的包也不同,这里主要有两种,java.util.ArrayList和java.util.LinkedList。
List,列表,索引从0开始。
(1)List是一个元素有序,且可重复的集合,集合中的每个元素都有其对应的序号,从0开始。
(2)List有两种存储结构,一种是顺序存储结构。还有一种是链式存储结构,基于循环双链表的数据结构,链表中的每个结点都存储了该结点的直接前驱和直接后继的地址,通过指针进行访问。
(3)这两种存储结构分别是,顺序存储结构:ArrayList,链式存储结构:LinkedList,对于随机访问get和set,顺序存储结构ArrayList是要绝对优于链式存储结构LinkedList的,但是对于插入和删除操作,LinkedList是绝对优于ArrayList的。
这和存储方式有关:
在进行插入和删除操作时,顺序存储结构需要移动大量元素,而链式结构只需先断开结点间的联系,执行操作后,在把结点连接起来即可(实际上编写算法时是先连后断,这里说的是理论上的理解),不需要移动元素,所以LinkedList在进行插入和删除时要优于ArrayList。
在进行随机访问时,因为顺序存储结构有对应的索引,所以能够直接找到要访问的元素,而链式存储结构即使建立索引,也需要通过指针遍历每个结点,来寻找对应的值,所以在随机访问时,ArrayList要优于LinkedList。
(4)List在内存中的结构
集合对象实际上存储的是对象的引用(地址),在调用时,是由集合名引用这个集合对象来进行调用。
List的使用
(1)创建一个List对象,因为List是一个接口,不能实例化,所以需要向上转型,这里转型的是ArrayList,其中<>表示泛型,规定这个集合要存储什么类型的元素,规定泛型以后就只能存储这个类型的元素,如果存储其他类型的元素,编译器会报错。
//创建一个集合对象
//<>:泛型,规定要存储什么类型的元素
List<String> list = new ArrayList<String>();
(2)add方法,返回值是boolean类型,参数是一个泛型的类型,从尾部添加这个元素,List重写了toString方法,所以直接输出这个list就可以查看存储了那些元素了。
/*
* boolean add(E e)
* 从尾部追加元素 E是泛型的类型
* 集合存放的是元素所在的地址信息
*/
list.add("老王");
System.out.println(list);
list.add("小马");
System.out.println(list);
输出结果:
add还有一个重载方法,没有返回值,两个参数,一个为索引,另一个是要添加的元素,也是泛型的类型。表示在指定索引上插入一个元素。值得注意的是使用这个方法,list中必须要有元素才可以使用,并且索引不能超过元素的最大索引,否则会报数组下标越界异常。
/*
* void add(int index,E element)
* 向索引为1的位置上添加元素"阿黄"
*/
list.add(1, "阿黄");
System.out.println(list);
输出结果:
(2)size(),返回值是int,用于返回list中元素的个数,需要注意的是,list中求元素个数是一个方法,size(),而数组中求元素的个数是一个属性,length,求String类型的长度也是一个方法,length()。
/*
* int size()
* 输出元素的个数
*/
System.out.println("元素的个数为:"+list.size());
输出结果:
(3)addAll方法,返回值为boolean,一个参数,参数为一个集合类型,作用是将一个集合添加到另一个集合的末尾。需要注意的是两个集合的泛型必须是相同的,否则编译器会报错。
/*
* boolean addAll(Collection c)
* 作用:将集合c追加到集合对象的末尾
*/
List<String> one = new ArrayList<String>();
one.add("hello");
one.add("world");
one.add("老王");
one.add(str1);
list.addAll(one);
System.out.println(list);
输出结果:
addAll同样也有一个重载方法,表示将一个集合插入到另一个集合的指定索引上。
/*
* boolean addAll(int index,Collection c)
* 从索引index上插入集合c的元素
*/
List<String> two = new ArrayList<String>();
two.add("hello kitty");
two.add("helloSuperMan");
//将集合two插入到list集合的最前面
list.addAll(0, two);
System.out.println(list);
输出结果:
为了演示后面的操作,新建一个ArrayList,泛型为Integer,这里虽然我们看到是基本数据类型10,20,30,实际上在添加之前这些元素已经被自动装箱为Integer类型了。
List<Integer> list = new ArrayList<Integer>();
list.add(10);//自动装箱为Integer
list.add(20);
list.add(30);
那么问题来了,这里的两个老王是不是同一个对象呢?输出一下。
System.out.println(list.get(list.indexOf("老王")).hashCode());
System.out.println(list.get(list.lastIndexOf("老王")).hashCode());
输出结果:
由输出结果可以看到,这两个老王是同一个对象,因为是字符串,字符串是存放到字符串常量池当中的,所以字符串在引用时是先到字符串常量池中寻找有没有这个值,如果有的话就指向这个值,没有就在字符串常量池中创建一个。值得注意的是通过new创建的字符串是放到堆内存当中,所以new多少次就有多少个字符串对象,不管之前有没有这个对象,这是因为new是向内存申请一块空间,所以new多少次就会有多少个对象。
String a1 = new String("a");
String a2 = new String("a");
System.out.println(a1==a2);
输出结果:
对于String类型是如此,那么对于Integer呢?测试一下。
List<Integer> testlist = new ArrayList<Integer>();
testlist.add(500);
testlist.add(500);
System.out.println(testlist);
System.out.println(list.get(list.indexOf(500))==list.get(list.lastIndexOf(500)));
输出结果:
这是因为Integer创建的对象实际上相当于new创建的,在堆内存当中是两个值相同但地址不同的对象,所以这两个500是两个不同的对象。那是不是说Integer的对象都是在堆内存当中呢?
testlist.add(127);
testlist.add(127);
System.out.println(testlist);
System.out.println(list.get(list.indexOf(127))==list.get(list.lastIndexOf(127)));
输出结果:
由此可见,并不是所有的Integer创建的对象都是不同的对象,这是因为Integer也有常量池,在Integer常量池中缓存了-128到127这个区间的值,所以当Integer的值在这个区间时,便会指向常量池当中对应的数值,所以在这里他们指向的是同一个对象。
(4)get方法,返回值为泛型的类型,一个参数,参数为索引,作用是通过指定索引来获取对应的元素,索引不能大于size()-1,不能小于0,否则会报数组下标越界异常。
/*
* E get(int index)
* 通过指定索引获取相应元素
*/
//获取第2个元素
int ele1 = list.get(1);
System.out.println(ele1);
输出结果:
(5)indexOf方法和lastIndexOf方法,返回值都为int,都有一个参数,参数类型为Object类型,顾名思义,indexOf的作用是返回元素在集合中第一次出现的索引,lastIndexOf是返回元素在结合中最后一次出现的索引。如果没有找到元素,则返回-1.
/*
* int indexOf(Object obj)
* 返回元素obj在集合中第一次出现的索引,
* 如果集合中没有此元素,返回-1
* int lastIndexOf(Object obj)
* 返回元素obj在集合中最后一次出现的索引,
* 如果集合中没有此元素,返回-1
*/
list.add(40);
list.add(10);
list.add(50);
System.out.println(list);
//获取元素10第一次出现的索引
int index1 = list.indexOf(10);
//获取元素10最后一次出现的索引
int index2 = list.lastIndexOf(10);
System.out.println("index1:"+index1);
System.out.println("index2:"+index2);
输出结果:
(6)remove方法,返回值为泛型的类型,一个参数,参数为索引,作用是移除指定索引的元素,返回这个被移除的元素。
/*
* E remove(int index)
* 移除索引index上的元素,返回值就是此元素
*/
//将元素50移除掉
int ele5 = list.remove(list.indexOf(50));
System.out.println("ele5:"+ele5);
System.out.println(list);
输出结果:
remove还有一个重载方法,返回值是boolean类型,一个参数,参数是Object类型,表示元素,作用是移除一个元素,移除成功返回true,否则返回false。需要注意的是当泛型为Integer类型时,需要手动将元素装箱为Integer,如果直接输入10的话,编译器会将10作为索引来处理。
/*
* boolean remove(Object obj)
* 将元素obj从集合中移除
*/
//将元素10移除,输出返回值
boolean f1 = list.remove(new Integer(10));
System.out.println("f1:"+f1);
System.out.println(list);
//将元素50移除,输出返回值
boolean f2 = list.remove(new Integer(50));
System.out.println("f2:"+f2);
System.out.println(list);
输出结果:
(7)set方法,返回值是泛型的类型,两个参数,一个是索引,一个是元素,作用是将指定索引的元素替换为给定的元素,返回被替换的元素。
/*
* E set(int index,E element)
* 使用元素element替换掉索引为index上的元素
* 返回值为替换出来的元素
*/
//将30替换成300
int element_30 = list.set(list.indexOf(30), 300);
System.out.println(list);
System.out.println("被替换出来的元素:"+element_30);
输出结果:
(8)subList方法,和subString类似,返回值是一个List对象,有两个参数,代表从指定索引开始,到指定索引的前一个索引结束,包前不包后。
/*
* List subList(int fromIndex,int toIndex)
* 截取子集,从fromIndex开始,到toIndex结束
* 返回集合中指定部分的视图
* 包前不包后
*/
/*截取中间3个元素*/
List<Integer> sublist = list.subList(1, 4);
System.out.println("sublist:"+sublist)
输出结果:
值得一提的是,因为集合存放的是对象的引用(地址),所以对子集进行的操作,同样也会影响到父集,可以看一个例子,首先将10到90赋给一个list对象,并截取其中一部分作为子串。
List<Integer> list = new ArrayList<Integer>();
//将10,20,30,...90添加到集合list中
for(Integer i=10;i<100;i+=10)
list.add(i);
System.out.println(list);
/*截取中间7个元素*/
List<Integer> sublist = list.subList(1, 8);
System.out.println("sublist:" + sublist);
输出结果:
然后再把子集中的每个元素扩大10倍。
//将子集里的元素扩大10倍
for(int i=0;i<sublist.size();i++){
int element = sublist.get(i);
sublist.set(i, element*10);
}
System.out.println(sublist);
System.out.println(list);
输出结果:
由此可以看出,集合对象存放的是每个元素的引用(地址),所以对子集进行的操作,会影响到父集,在这里子集其实是父集的视图,也就是父集的映射。子集虽然是父集的映射,但和父集并不是同一个对象,由下例可知。
//子集与父集的地址是否是同一个
System.out.println(sublist.hashCode()==list.hashCode());//false
输出结果:
(9)toArray()方法,将集合转换成数组,返回值是一个Object数组。
重新创建一个集合用来测试
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
调用toArray()方法,并输出数组的各个元素,这里调用的是Arrays工具类当中的toString()方法。
/*
* Object[] toArray()
*/
//将集合转换成数组
Object[] arr = list.toArray();
System.out.println(Arrays.toString(arr));
输出结果:
这时思考一个问题,如果改变原集合当中的元素,会不会影响到数组呢?
list.set(1, "E");
System.out.println(Arrays.toString(arr));
System.out.println(list);
输出结果:
从输出结果来看,对原集合元素的修改并不会影响到数组。博主觉得应该是因为数组和集合是引用类型,这里通过集合转换而来的数组应该是新开辟了一块内存空间用来存放原集合中元素的值,而不是直接引用原集合当中的元素,所以对原集合的修改才不会影响到数组。
并且值得注意的是集合转换成数组是可以对这个数组进行增删操作的,也就是可以修改数组的长度,为什么值得注意呢,因为将数组转换为集合,不能对转换来的集合进行增删操作,即不能改变这个集合的长度,要想改变集合的长度,需要新创建一个集合来存储这个集合中的元素(使用addAll()方法),这时便可以修改新集合中的元素了。
因为这个转换方法得到的数组是Object类型,在每次操作元素时,需要向下造型,不便于操作。因为集合有泛型机制,所以有了一个更加方便的重载方法,E[] toArray(E[] e),见名知义,这个方法的作用是将一个集合转换为制定泛型的数组。
/*
* E[] toArray(E[] e)
* 此方法需要传入一个元素泛型的数组对象
* 长度可以为任意值
*/
String[] arr2= {};
arr2 = list.toArray(arr2);
System.out.println("arr2:"+Arrays.toString(arr2));
输出结果:
(10)asList()方法,是一个静态方法,在Arrays工具类下,可以将数组转换为集合。
String[] names = {"高","赵","古"};
List<String> list = Arrays.asList(names);
System.out.println(list);
输出结果:
之前尝试修改原集合,结果是对转换而来的数组没有影响,那么这里修改原数组对转换来的集合有没有影响呢?
names[0]="123";
System.out.println(list);
输出结果:
可以看到,这对转换而来的集合产生了影响,这里应该是转换来的集合是直接引用原数组当中值,而不是开辟一块新的内存空间,所以修改原数组的元素会影响转换而来的集合。
之前有提过,此时不能改变集合的长度,否则会抛出 java.lang.UnsupportedOperationException异常。
要想修改元素,可以将此集合中的元素添加到另外一个集合中。
/*
* 如果想进行增删操作,可以将此集合中的元素
* 添加到另外一个集合中
*/
List<String> other = new ArrayList<String>();
other.addAll(list);
System.out.println(other);
other.add("黄");
System.out.println(other);
输出结果: