1 什么是集合?有哪些分类
集合是用来装类型不同元素的,没有个数限制;而数组装类型相同元素,有个数限制。
1.1 JCF(Java Collections FrameWork)
意为Java集合框架。
上图中蓝色箭头的意为接口继承(如public interface List extends Collection 子接口为List,父接口为Collection),里面出现的均为接口。接口不允许创建对象,只允许实现类,其中接口List的实现类有ArrayList、LinkedList、Vector、Stack等(如public class ArrayList implements List),其他的接口均有具体实现。
-
List、Set、Map是不是属于同一个等级?
List Set属于Collection的子接口,Collection和Map属于同一个等级。 -
注意:
- 使用这些集合需要导入java.util包,如果不导包Java会自动去java.lang下面搜索,找不到。
- 所以在为类命名时,不要再使用这些已经被官方用过的名字了,这样会使Java直接导入你自己写的类,找不到官方类中的方法而报错。
- 在赋值引用的名字时这些官方定义的类、接口的标识符是可以用的,但是可读性太差,要避免。
- List x = new ArrayList<>();这样定义是对的,相当于父类 引用 = new 子类();
2 ArrayList ★
ArrayList集合的特点为有序,元素不唯一。底层基于数组(Object[]) 实现。
2.1 包装类
-
由于ArrayList底层基于Object[]实现,这导致所有的引用数据类型都可以存放在集合里,但是基本数据类型不可以存放其中。为了保证基本数据类型也可以放在集合里,Java定义了包装类。
-
作用1 将基本数据类型打包为包装类,然后再传入集合中。(2.1.1)
-
作用2 将String类型转为包装类型。(2.1.2)
2.1.1 对应转换关系
- 对应的类型
基本数据类型 | boolean | char | byte | short | int | long | float | double |
---|---|---|---|---|---|---|---|---|
对应包装类 | Boolean | Character | Byte | Short | Integer | Long | Float | Double |
- 转换(其他类型可类比)
//打包(基本数据类型-->包装类)
int x = 45;
//JDK5.0之前
Integer y = Integer.valueOf(x);
//JDK5.0开始,支持自动解包
Integer y = x;
//解包(包装类-->基本数据类型)
Integer x = new Integer(45);
//JDK5.0之前
int y = x.intValue();
//JDK5.0之后,支持自动打包
int y = x;
也就是说JDK5.0之后,包装类和其对应的基本数据类型可以直接赋值(自动打包/解包),当看到在集合的方法参数列表中传入的数据类型为基本数据类型时需知道并非支持基本数据类型了,而是基本数据类型的自动打包。
另外,整数类型的包装类会自动缓存-128到127之间所有的数字,当直接给引用赋值为数字(自动打包)时,使用自动缓存机制,并不是在堆内存中创建对象,而是直接在缓存区创建值。比如:
Long x = 120L;
Long y = 120L;
System.out.println(x == y);//--->true
2.1.2 将String类型转为包装类型
除Character类(无法把一个字符串转成一个字符),其他包装类还可以将String类型转换成对应的基本数据类型。举一例,其余类型类推: String x = "120";
int y = Integer.parseInt(x);
字符串转类型的时,如果字符串中有除本类型之外的其他值,会报NumberFormatException错误。
转类型boolean字符串时,传入true会转换为true;传入其他都报false。
- 例:
//打印三年后张三的年龄
public class Example{
public static void main(String[] args){
String str = "张三:17";
String age = str.substring(str.indexOf(":") + 1);
int age1 = Integer.parseInt(age);
System.out.println(age1 + 3);//--->20
//或:
//data[0]:姓名 data[1]:年龄
//String[] data = str.split(":");
//int age = Integer.parseInt(data[1]);
//System.out.println("张三三年后:" + (age + 3));
}
}
2.2 基本用法与特点
-
如何创建ArrayList对象
-
JDK5.0之前,默认往集合里面存放的都是Object类型的对象,取元素后类型需强转。
ArrayList list = new ArrayList(); -
JDK5.0及以后,可以加泛型(不加泛型默认传入元素为Object类型)
ArrayList<泛型> list = new ArrayList<泛型>(); -
JDK7.0及以后,后面的泛型会自动推断(不加泛型默认传入元素为Object类型)
ArrayList<泛型> list = new ArrayList<>();
-
-
如何添加元素
-
一次添加一个元素:
list.add(元素); -
一次添加多个元素:
Collections.addAll(集合对象, 元素, 元素, 元素)面试题: Collection和Collections之间的区别? Collection是所有单值类型集合统一的父接口, Collections是集合的工具类。
- Collections.addAll是如何做到一次可以传入n个元素的?
-
/**
"..."-->可变参:参数个数可以是0-无数个,底层基于数组实现。
1. 一个方法的参数列表中最多只能出现一个可变参
2. 可变参只能放在参数列表的末尾
3. int[] temp与int... temp等价,因此如果两个方法的参数列表中,
只有这两个参数不同,不是重载,是参数的重复定义。
*/
public class Test1{
public static void main(String[] args){
//test();
//test(45);
//test(10,33);
test(38,77,92);
}
//public static void test(int[] temp){}
public static void test(int ... temp){
for(int x : data){
System.out.println(x);
}
}
}
-
如何得到集合的大小
list.size(); -
如何得到集合里面的某一个元素
list.get(int 下标) -
如何判断集合里面是否出现指定的元素
list.contains(元素) -
如何遍历集合对象
//方法1 for + 下标(执行按下标查找时使用)
for(int x = 0;x < list.size();x++){
//x -> 下标
//list.get(x) -> 元素
}
//方法2 foreach
//底层使用迭代器实现
//执行与下标无关、不对集合进行增删的一些操作使用这个比较简单
for(泛型 x : list){
//x -> 元素
}
//方法3 迭代器 ★
//如果集合是无序的,无法使用下标依次删除,使用迭代器
for(得到迭代器对象; 迭代器是否还有下一个元素; ){
//取出下一个元素
}
//Java命名惯例,当getter/setter只存在一个时,可以不写前三个字母(iterator方法命名由来)
for(Iterator<泛型> car = list.iterator(); car.hasNext(); ){
//car.next() -> 元素
}
-
迭代器:可类比为word文档中的光标,开始时迭代器在首元素的左边。遍历时使用这个功能可取出集合中的元素,for和while中均可以使用,foreach的底层实现方式是迭代器。
- hasNext():判断迭代器当前所在位置的下一个是否还有元素。
- next():取出下一个元素。
- remove():移除当前元素。
-
需要注意的是,在同一方法内使用同一个迭代器进行第二次遍历时会因为第一次已迭代到集合末尾使得.hasNext()方法为false,第二次不能使用。
- 当使用for循环时要注意将iterator方法定义于条件的首位(上面迭代器的第二个例子),定义局部变量。
- 使用while时每次循环都要定义新的iterator。
-
例:
import java.util.*;
public class Example{
public static void main(String[] args){
ArrayList<String> list = new ArrayList<>();
list.add("张三");
Collections.addAll(list,"李四","张三","王五","王五");
System.out.println(list);
ArrayList<String> temp = new ArrayList<>();
for(String name : list){
if(!temp.contains(name)){
temp.add(name);
}
}
System.out.println(temp);
}
}
2.3 删除元素
一个remove方法只能删除一个元素。2.3.1 指定下标删除
list.remove(int 下标)需注意:当传入Integer类型时可能存在歧义,在这里传入int、char、short、byte类型不会自动打包,一定识别为下标;其他基本数据类型可自动打包为包装类。
模拟实现:
public class Test{
public static void main(String[] args){
Integer x = 10;
remove(x);--->元素
Integer x1 = new Integer(10);
remove(x1);--->元素
remove(10);--->下标
}
public static void remove(int x){
System.out.println("下标");
}
public static void remove(Object obj){
System.out.println("元素");
}
}
2.3.2 指定元素删除与equals()
list.remove(Object obj),底层遵循equals()比较机制。
此处传入的obj仅仅作为一个参照,并不是这个集合的元素。用这个参照删除集合中“参照.equals()”为true的元素。
如果这个调用remove(Object)的集合泛型重写了equals()方法,remove(Object)会使用这个新的equals()方法判断哪些值可以被删除。
可以定制(重写)equals()自定义remove(Object)的删除规则。
import java.util.*;
public class Test1{
public static void main(String[] args){
ArrayList<Teacher> list = new ArrayList<>();
Teacher t1 = new Teacher();
Teacher t2 = new Teacher();
list.add(t1);
System.out.println(list.size());//--->1
list.remove(t2);
//t2.equals(t1) ---> 无论什么情况下都是true
//此时无论remove中传入什么,只要传入的是Teacher类的引用,都会将t1删除
System.out.println(list.size());//--->0
}
}
class Teacher{
@Override
public boolean equals(Object obj){
return true;
}
}
import java.util.*;
public class Test2{
public static void main(String[] args){
ArrayList<Teacher> list = new ArrayList<>();
Teacher t1 = new Teacher();
Student s1 = new Student();
list.add(t1);
System.out.println(list.size());//--->1
list.remove(s1);
//s1.equals(t1) ---> Student中的equals()方法未被重写,两个对象地址不同,返回必为false
System.out.println(list.size());//--->1
}
}
class Teacher{
@Override
public boolean equals(Object obj){
return true;
}
}
class Student{
}
import java.util.*;
public class Test3{
public static void main(String[] args){
ArrayList<Teacher> list = new ArrayList<>();
Teacher t1 = new Teacher();
Student s1 = new Student();
list.add(t1);
System.out.println(list.size());//--->1
list.remove(s1);
//s1.equals(t1) ---> Student中的equals()方法被重写,且返回必为true
//此时无论remove中传入什么,只要传入的是Teacher或Student类的引用,都会将t1删除
System.out.println(list.size());//--->0
}
}
class Teacher{
@Override
public boolean equals(Object obj){
return true;
}
}
class Student{
@Override
public boolean equals(Object obj){
return true;
}
}
需要注意的是:contains()也可以通过重写equals()来改写判断的依据。
2.3.3 清空当前数组内全部元素
list.clear()
- 使用集合给出的方法进行清空的其他办法:
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list,1,2,3,4,5);
//for循环时为什么要倒序遍历?
//这种执行结果是[2,4],因为当1被删除后,2成为第0个元素,第二个被删除的是3,同理第三个被删除的是5
//因数组的长度小于当前下标数而停止删除,结果出错
//删除元素后,后面的元素向前移动,但下标不断增长造成漏删
for(int x = 0; x < list.size(); x++){
list.remove(x);
}
//倒序删除
for(int x = list.size() - 1; x >= 0; x--){
list.remove(x);
}
//或
while(list.size() != 0){
list.remove(0);
}
System.out.println(list);
2.3.4 按元素内容遍历删除指定元素
因为后面用到的集合不仅仅是有序集合,所以使用for + 下标遍历删除指定元素不一定可行。
需要使用到迭代器进行对指定内容元素的删除。但当使用迭代器遍历集合的过程中,如果需要一边遍历,一边添加/删除,直接使用对象引用调用方法进行添加/删除操作,会触发ConcurrentModificationException(CME,并发修改异常)。 此时需要使用迭代器的.remove()方法对光标下一个指向的元素进行删除。
此时如果对这个集合进行添加,也会报CME异常,解决办法是新建一个LinkedList(也可以其他集合,写链表的原因是其增删效率高)新集合,在循环过程中将符合条件的元素添加到新集合,循环结束再addAll到老集合。
- 触发CME的原因:当Iterator迭代一个容器的时候,如果此时非迭代器的方法直接增删集合中的内容,使迭代器每次循环开始时验证集合和迭代器保存的长度不相等,那么Iterator就会抛出CME的异常。
快速失败(fail—fast)
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception来防止出现并发修改使数据出现错误。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
-
另外,在使用迭代器的remove()方法时,next()获得的参数要在遍历开始时就接收一下,否则remove()方法之后再使用next()获取这个值,依据集合底层遵循的数据结构不同,可能使值发生变化。
-
例题:
import java.util.*;
public class Example{
public static void main(String[] args){
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list,45,38,77,62,59,83);
//删除所有不及格的学生成绩
//方法1 for + 下标 不可行
//删除元素后,后面的元素向前移动,但下标不断增长造成漏删
for(int x = 0; x < list.size(); x++){
//x -> 下标
Integer score = list.get(x);
if(score < 60){
list.remove(x);
}
}
//方法2 for + 下标(倒序)不提倡使用
//后面会遇到很多无序的集合
for(int x = list.size() - 1; x >= 0; x--){
//x -> 下标
Integer score = list.get(x);
if(score < 60){
list.remove(x);
}
}
//方法3 foreach 不可行
//底层使用迭代器实现
//遍历并使用集合方法增删(add/remove),会报ConcurrentModificationException(CME)异常
for(Integer score : list){
if(score < 60){
list.remove(score);//remove(Object 元素)
}
}
//方法4
//当需要遍历并在其中集合执行add/remove时,使用这种方法
for(Iterator<Integer> car = list.iterator(); car.hasNext(); ){
Integer score = car.next();
if(score < 60){
//如果使用“list.remove(score);”不可行,出异常
car.remove();
}
}
System.out.println(list);
}
}