Java进阶之Collection&迭代器&泛型
一、Collection
1.1 Collection概述
之前我们已经了解学过ArrayList这个集合,ArrayList就是Collection的实现类。今天我们就来学习Collection这个类。
集合就是一个容器,这个容器可以存放多个数据;Collection是所有单列集合的根接口,所有的单列集合都会实现这个接口。
1.2 集合和数组中的区别
- 集合的长度可变,而数组的长度是固定的
- 集合只能存放引用类型的数据,而数组可以存放任意类型的数据
1.3 Collection常用方法
public boolean add(E e)
(常用) 把给定的对象添加到当前集合中public void clear()
清空集合中所有的元素public boolean remove(E e)
将指定的对象在当前集合中删除public boolean contains(Object obj)
判断当前集合中是否包含给定的对象public boolean isEmpty()
判断当前集合是否为空public int size()
(常用) 返回集合中元素的个数public Object[] toArray()
把集合中的元素,存储到数组中
Collection是一个接口,如果要用,需要使用实现类,最常用的实现类是ArrayList;
Collection中没有索引有关的方法,因为不是所有的集合都有索引。
public static void main(String[] args) {
//创建集合
Collection<String> c = new ArrayList<>();
//public boolean add(E e):向集合中添加元素
c.add("张三");
c.add("李四");
c.add("王五");
//输出集合
System.out.println("操作前:" + c);//[张三, 李四, 王五]
//public boolean contains(Object obj):判断集合中是否包含指定的元素
System.out.println("是否包含:" + c.contains("haha"));
//public boolean isEmpty():判断集合是否为空,如果集合中没有任何元素,那么这个集合就是一个空集合
//c.clear();
System.out.println("是否为空:" + c.isEmpty());
//public int size():获取集合的大小,集合中有几个元素,那么集合的大小就是几
System.out.println("集合的大小:" + c.size());
//public Object[] toArray():将集合转成一个数组并返回。
Object[] strArr = c.toArray();
//遍历数组
for (int i = 0; i < strArr.length; i++) {
System.out.println(strArr[i]);
}
//public boolean remove(E e):直接删除指定的元素
boolean flag = c.remove("李四");
System.out.println("操作后:" + c);//[张三, 王叔叔]
System.out.println("flag:" + flag);//true
//public void clear() :清空集合中所有的元素。
c.clear();
System.out.println("操作后:" + c);
}
1.4 List接口
List是Collection下面的一个子接口,有三个特点:
- 有序 (按照什么顺序存,就按照什么顺序取)
- 有索引 (可以根据索引获取元素)
- 可以重复 (可以存放重复元素)
List是一个接口,如果要用,需要使用实现类。
1.4.1 常用方法
public void add(int index, E element)
将指定的元素,添加到该集合中的指定位置上public E get(int index)
返回集合中指定位置的元素public E remove(int index)
移除列表中指定位置的元素, 返回的是被移除的元素public E set(int index, E element)
用指定元素替换集合中指定位置的元素,返回被替换的元素
1.4.2 LinkedList简介以及特有方法
LinkedList是List接口下面的实现类,内部使用双向链表保存数据
LinkedList特有方法:
public void addFirst(E e)
将指定元素插入此列表的开头。public void addLast(E e)
将指定元素添加到此列表的结尾。public E getFirst()
返回此列表的第一个元素。public E getLast()
返回此列表的最后一个元素。public E removeFirst()
移除并返回此列表的第一个元素。public E removeLast()
移除并返回此列表的最后一个元素。public E pop()
从此列表所表示的堆栈处弹出一个元素。public void push(E e)
将元素推入此列表所表示的堆栈
public E pop()
调用的public E removeFirst()
方法;public void push(E e)
调用的是 public void addFirst(E e)
方法,这两个方法是为了模拟入栈和出栈的操作,定义这两个方法的目的是为了达到见名知意的效果。
1.4.3 LinkedList和ArrayList的区别
- ArrayList的内部使用数组保存数据,LinkedList内部使用双向链表保存数据
- ArrayList是List接口下面最常用的实现类,LinkedList不常用
- LinkedList特点是查询慢,增删快;ArrayList底层用数组实现,数组特点是查询快,增删快(但ArrayList经过多次优化后,增删不一定比LinkedList慢)
二、Iterator迭代器
我们之前都是使用for循环结合索引的方式遍历集合,但是这种遍历方式并不适用于所有的集合,因为不是所有的集合都有索引。
有一种通用的遍历集合的方式,可以遍历任何的集合,这个遍历方式就叫做迭代器遍历。
迭代器是遍历集合的一个工具,内部有一个光标,指向了集合的最开始的位置,每执行一次,光标向后移动一位。
Iterator<E> iterator()
获取集合的迭代器
Iterator是一个接口,表示迭代器,里面的方法:
boolean hasNext()
:判断是否还有元素可以获取。E next()
:获取光标位置的元素,将光标向后移动
迭代器的遍历步骤;
- 1.通过集合调用iterator方法获取迭代器对象
- 2.通过迭代器对象调用hasNext方法判断是否有元素可以获取
- 3.如果有元素可以获取,那么就调用next方法获取元素,并将光标向后移动。
public static void main(String[] args) {
//创建集合
Collection<String> c = new ArrayList<>();
//添加元素
c.add("张三");
c.add("李四");
c.add("王五");
//使用迭代器遍历集合。
//获取迭代器
Iterator<String> itr = c.iterator();
//因为只有集合中有元素可以获取时我们才进行遍历操作,条件可以判断集合中是否还有元素可以获取
while(itr.hasNext()) {
//如果条件成立,表示有元素可以获取,那么我们就获取元素
String str = itr.next();
System.out.println(str);
}
}
2.1 并发修改异常
如果迭代器遍历的过程中使用集合的方法对集合进行增删操作,再次进行遍历会引发并发修改异常。
public static void main(String[] args) {
//创建集合
ArrayList<String> list = new ArrayList<>();
//添加元素
list.add("hello");
list.add("world");
list.add("java");
list.add("php");
//遍历删除
Iterator<String> iterator = list.iterator();
//循环遍历
while (iterator.hasNext()) {
String str = iterator.next();
if (str.equals("hello")) {
list.remove(str);
break;
}
}
}
删除这一步没有报错,是下次进入循环遍历时报错,因为用集合的方法增删操作元素,迭代器本身并不知道,会影响光标的位置判断,因此报错。
解决方法:使用迭代器的子接口 ListIteator的增删功能
public static void main(String[] args) {
//创建集合
ArrayList<String> list = new ArrayList<>();
//添加元素
list.add("hello");
list.add("world");
list.add("java");
list.add("php");
//遍历删除
ListIterator<String> iterator = list.listIterator();
//循环遍历
while (iterator.hasNext()) {
String str = iterator.next();
if (str.equals("hello")) {
iterator.remove();
break;
}
}
System.out.println(list);//[world, java, php]
}
三、增强for循环
在JDK5的时候,多了一个新的遍历方式叫做增强for循环(foreach),可以遍历数组或集合。
格式:
for (数据类型 变量名 : 容器) {
...
}
格式解释:
数据类型:要遍历的容器中保存的是什么类型的数据,这个数据类型就写什么
变量名: 表示容器中的每一个元素
容器: 要遍历的容器,可以是数组,也可以是集合
增强for是一种语法糖,语法糖指的是本质没有变,只不过写法更加的简洁了。
- 增强for遍历数组,本质还是使用的普通for循环
- 增强for遍历集合,本质上是使用的迭代器遍历集合
public static void main(String[] args) {
//创建数组
int[] intArr = {11, 22, 33, 44, 55};
//使用增强for遍历
for (int num : intArr) {
//变量num表示数组中的每一个元素
System.out.println(num);
}
System.out.println("======================================");
//创建集合,保存字符串
Collection<String> c = new ArrayList<>();
//添加元素
c.add("hello");
c.add("world");
c.add("java");
//使用增强for遍历
for (String s : c) {
System.out.println(s);
}
}
增强for的好处和缺点:
- 好处: 写法简洁,省去了对索引的操作
- 缺点: 因为省略了索引,所以无法再遍历的过程中操作索引,如果要在遍历过程中涉及到索引操作,还是需要使用普通for循环
四、泛型
4.1泛型概述
泛型是一种未知的,不确定的数据类型,如ArrayList<E>
中的E就是一个泛型,是一个未知的数据类型。
泛型虽然是未知的数据类型,但是并不是一直未知,一直不确定,在我们使用一个类的时候,这个泛型表示的数据类型会被确定下来,如ArrayList<Student>
,此时这个泛型类型E表示的就是Student类型,且在之后的使用中一直是Student类型,不能改变。
public static void main(String[] args) {
//创建集合
ArrayList list = new ArrayList();
//添加数据
//list.add(100); 运行时报错
list.add("hello");
list.add("world");
list.add("java");
//遍历集合,输出集合中每一个字符串的长度
for (Object obj : list) {
//将obj向下转型成String类型
String str = (String) obj;
System.out.println(str.length());
}
System.out.println("============================");
//创建集合,使用泛型
ArrayList<String> list2 = new ArrayList<>();
//添加数据
//list2.add(100); 编译时期报错
list2.add("hello");
list2.add("world");
list2.add("java");
//遍历集合,输出集合中每一个字符串的长度
for (String str : list2) {
System.out.println(str.length());
}
}
泛型也可以省略,如果省略泛型相当于泛型是Object
泛型好处:
- 1.省略了向下转型的代码
- 2.将运行时期的问题提前到了编译时期
4.2 泛型擦除
Java中的泛型都是伪泛型,泛型只在源代码阶段有效,一旦编译,泛型就会消失。
public static void main(String[] args) {
//创建集合
ArrayList<String> list = new ArrayList<>();
//添加元素
list.add("hello");
list.add("world");
list.add("java");
//使用增强for遍历(快捷键:集合.for)
for (String s : list) {
System.out.println(s);
}
}
经过反编译的源码如下:
public class Test
{
public Test()
{
}
public static void main(String args[])
{
ArrayList list = new ArrayList();
list.add("hello");
list.add("world");
list.add("java");
String s;
for (Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println(s))
s = (String)iterator.next();
}
}
从上面代码可以看出,编译之后,泛型消失,还是通过向下转型实现。
4.3 泛型类
如果在定义类的时候,类名后面写上,就表示在类的范围内定义了一个【泛型类型T】,这个T可以是其他任何字母(一般是大写字母)。
当使用这个类创建对象时,泛型类型T(不确定的类型T)就会被指定出来。
格式:修饰符<泛型> 返回值类型 方法名(参数列表){}
public class Factory<T> {
//下面的T分别是使用这种不确定的类型当作参数类型和返回值类型
public T method(T t){
return t;
}
}
/*
泛型类的测试类
*/
public class Demo03Generic {
public static void main(String[] args) {
//尖括号中的Phone表示指定了这个Factory中的泛型是Phone类型
Factory<Phone> f = new Factory<>();
//调用method方法
Phone p = f.method(new Phone());
p.call();
}
}
4.4 泛型方法
如果想要调用方法的时候指定泛型的类型,可以使用泛型方法
泛型方法的定义格式:
修饰符 <泛型> 返回值类型 方法名(参数列表) {
方法体;
return 返回值;
}
public class Factory<T> {
/*
定义方法,接收什么类型的参数,就得到什么类型的结果
<E>: 表示在方法上定义泛型
后面两个E:在使用泛型类型E当做返回值类型以及形参类型
E与T不同,表示泛型方法的泛型与泛型类的泛型类型不同
*/
public <E> E getSame(E e) {
return e;
}
}
/*
泛型方法的测试类
*/
public static void main(String[] args) {
//创建Factory对象
Factory<Phone> f = new Factory<>();
//调用方法
Phone phone = f.getSame(new Phone());
Pad pad = f.getSame(new Pad());
String str = f.getSame("aa");
}
在方法上定义的泛型,需要等到调用方法的时候才能确定泛型的类型。
4.5 泛型接口
如果在定义接口的时候在接口后面加上,这个接口就是一个泛型接口。
泛型接口的使用:
- 1.实现类实现接口的时候,可以直接明确接口中的泛型类型。
- 2.实现类实现接口的时候,可以不指定接口中的泛型,等到使用实现类的时候再指定
public interface MyInterface<T> {//表示定义了一个未知的,不确定的类型T
//定义抽象方法,使用泛型类型T当做参数类型和返回值类型
T method(T t);
}
/*
实现类实现接口的时候,可以在接口名字后面写上尖括号并指定泛型
*/
public class MyClassA implements MyInterface<Phone>{
@Override
public Phone method(Phone phone) {
return null;
}
}
在定义类或接口的时候,在当前类或接口的名字后面写的尖括号才是在定义泛型,其他地方都是在使用泛型。
/*
在定义类或接口的时候,在当前类或接口的名字后面写的尖括号才是在定义泛型。
*/
public class MyClassB<T> implements MyInterface<T>{
@Override
public T method(T t) {
return null;
}
}
/*
泛型接口的测试类
*/
public class Demo05Generic {
public static void main(String[] args) {
MyClassA ma = new MyClassA();
Phone phone = ma.method(new Phone());
MyClassB<Pad> mb = new MyClassB<>();
Pad pad = mb.method(new Pad());
}
}
4.6 泛型通配符
泛型之间没有继承关系,如ArrayList<String>
并不是ArrayList<Object>
的子类,如果想要接收匹配任何类型的泛型,可以使用泛型通配符。
泛型通配符要放在方法参数位置被动匹配(比如方法参数位置), 不要主动使用(创建对象时泛型用通配符)。
public static void main(String[] args) {
//定义集合
ArrayList<String> strList = new ArrayList<>();
//添加元素
strList.add("hello");
strList.add("world");
strList.add("java");
//调用printArrayList遍历
printArrayList(strList);
//定义集合
ArrayList<Integer> intList = new ArrayList<>();
intList.add(100);
intList.add(200);
intList.add(300);
printArrayList(intList);
//定义集合
ArrayList<?> list = new ArrayList<>();
//list.add("aa");
}
/*
定义方法,可以遍历保存任何数据的集合。
*/
public static void printArrayList(ArrayList<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
4.7 泛型限定
为了安全考虑,我们不能让所有类型的数据通过泛型通配符进入程序,此时就需要对泛型通配符?的取值范围进行限制,就需要用到泛型限定。
- <? extends A>:泛型要么是A类,要么是A的子类(上限)
- <? super A>:泛型要么是A类,要么是A的父类 (下限)
泛型的作用主要用于让代码写的更加简洁,实现代码重构,优化代码设计。