集合
我们接下来要学习的内容是Java基础中一个很重要的部分:集合
1. Collection接口
1.1 前言
Java语言的java.util包中提供了一些集合类,这些集合类又称之为容器
提到容器不难想到数组,集合类与数组最主要的不同之处是,数组的长度是固定的,集合的长度是可变的,而数组的访问方式比较单一,插入/删除等操作比较繁琐,而集合的访问方式比较灵活
常用的集合类有List集合,Set集合,Map集合,其中List集合与Set集合继承了Collection接口,各个接口还提供了不同的实现类.
1.2 集合概念
集合的英文名称是Collection,是用来存放对象的数据结构,而且长度可变,可以存放不同类型的对象,并且还提供了一组操作成批对象的方法.Collection接口层次结构 中的根接口,接口不能直接使用,但是该接口提供了添加元素/删除元素/管理元素的父接口公共方法.
由于List接口与Set接口都继承了Collection接口,因此这些方法对于List集合和Set集合是通用的.
1.3 集合的继承结构
Collection接口
List 接口
【数据有下标,有序,可重复】ArrayList子类
LinkedList子类
Set 接口
【数据无下标,无序,不可重复】HashSet子类
Map 接口【键值对的方式存数据】
HashMap子类
1.4 Collection方法速查表
1.4.1 Collection接口测试
package cn.tedu.collection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
/**本类用于测试Collection接口*/
public class TestCollection {
public static void main(String[] args) {
//1.创建Collection相关的对象
//Collection c = new Collection();//报错,因为Collection是接口
//<Integer>是泛型,用来约束集合中的数据类型,不能是基本类型,必须是引用类型
Collection<Integer> c = new ArrayList<>();
//2.1测试集合中的常用方法--单个集合间的操作
c.add(100);//向集合中添加元素 Ctrl+D快速向下复制
c.add(200);//向集合中添加元素
c.add(300);//向集合中添加元素
c.add(400);//向集合中添加元素
c.add(500);//向集合中添加元素
System.out.println(c);//直接打印集合,查看集合中的元素
// c.clear();//清空当前集合中的所有元素
// System.out.println(c);
System.out.println(c.hashCode());//获取集合对象的哈希码值
System.out.println(c.toString());//打印集合的具体元素
System.out.println(c.equals(200));//false,集合对象c与值200是否相等
System.out.println(c.contains(200));//true,c集合中是否包含指定元素200
System.out.println(c.isEmpty());//false,判断集合是否为空
System.out.println(c.remove(100));//true,移除集合中的指定元素,成功返回true
System.out.println(c);//[200, 300, 400, 500]
System.out.println(c.size());//4,返回集合的元素个数
Object[] array = c.toArray();//将指定的集合转为数组Object[]
System.out.println(Arrays.toString(array));//[200, 300, 400, 500]
//2.2测试多个集合间的操作
Collection<Integer> c2 = new ArrayList<>();//创建第2个集合
c2.add(2);//向c2集合添加元素
c2.add(4);//向c2集合添加元素
c2.add(5);//向c2集合添加元素
System.out.println(c2);//查看c2集合中的元素
c.addAll(c2);//把c2集合的所有元素添加到c集合当中
System.out.println(c);//c2集合的所有元素追加到了c集合末尾
System.out.println(c2);//c2集合本身没有任何改变
//当前集合c是否包含指定集合c2中的所有元素
System.out.println(c.containsAll(c2));
System.out.println(c.contains(200));//c是否包含单个指定元素200
System.out.println(c.removeAll(c2));//删除c集合中属于c2集合的所有元素
System.out.println(c);
System.out.println(c.add(5));
System.out.println(c);
System.out.println(c.retainAll(c2));//取c集合与c2集合的交集(公共元素)
System.out.println(c);//[5]
//3.迭代集合/遍历集合
/**迭代步骤:
* 1.获取集合的迭代器 c.iterator();
* 2.判断集合中是否有下一个可迭代的元素 it.hasNext()
* 3.获取当前迭代到的元素 it.next()*/
Iterator<Integer> it = c.iterator();
while(it.hasNext()){
Integer num = it.next();
System.out.println(num);
}
}
}
1.5 泛型
1.5.1 概述
我们可以观察一下,下面的代码中有什么元素是我们之前没见过的呢?
其实就是< ? >
的部分,它就是泛型
泛型是(Generics)JDK1.5 的一个新特性,通常用来和集合对象一起使用
泛型概念非常重要,它是程序的增强器,它是目前主流的开发方式
1.5.2 作用
那泛型有什么作用呢?
我们可以把泛型理解成一个“语法糖”,本质上就是编译器为了提供更好的可读性而提供的一种小手段,小技巧,虚拟机层面是不存在所谓“泛型”的概念的。是不有点神奇,不知所云,别着急等我讲完你就清楚了。
我们可以通过泛型的语法定义<>,来约束集合中元素的类型,编译器可以在编译期根据泛型约束提供一定的类型安全检查,这样可以避免程序运行时才暴露BUG,代码的通用性也会更强
泛型可以提升程序代码的可读性,但是它只是一个“语法糖”(编译后这样的部分会被删除,不出现在最终的源码中),所以不会影响JVM后续运行时的性能.
1.5.3 泛型示例
示例1 : 我们创建一个ArrayList,看到eclipse发出黄线警告,这是为什么呢?
原因:ArrayList定义时使用了泛型,在声明时需要指定具体的类型
我们把这个”<>”的方式称之为泛型,那么泛型有什么样的作用呢?就是在编译阶段检查传入的参数是否正确
有了泛型,我们可以看到要求存放的是String类型,而测试时存放的是int类型的100,所以编译器报错:
类型List的add方法要求添加的类型为String类型,int类型不匹配,不能正确存入
1.5.4 泛型声明
泛型可以在接口 类 方法上使用
在方法的返回值前声明了一个,表示后面出现的E是泛型,而不是普通的java变量
1.5.5 常用名称
1.5.6 泛型测试
- 1.泛型测试
package cn.tedu.generic;
import java.util.ArrayList;
import java.util.List;
/**本类用于测试泛型的优点*/
public class TestGeneric1 {
public static void main(String[] args) {
/**1.泛型是怎么来的?--想要模拟数组的数据类型检查*/
String[] a = new String[5];//创建一个用来存放String类型数据的数组,长度为5
a[2] = "泡泡";
a[4] = "涛涛";
//数组的好处:在编译时期检查数据的类型,如果不是要求的类型会在编译器就报错
//a[0] = 1;
//a[1] = 8.8;
//a[3] = 'c';
/**2.泛型通常会结合着集合一起使用*/
List list = new ArrayList();//注意导包:java.util...
//没有泛型,数据类型根本没有约束 -- 太自由!!!
list.add("江江");
list.add(1);
list.add(8.8);
list.add('a');
System.out.println(list);//通过打印查看集合中的元素
/**3.引入泛型--主要目的是想通过泛型来约束集合中元素的类型<?>*/
/**4.泛型的好处:可以把报错的时机提前,在编译期就报错,而不是运行后抛出异常
* 在向集合中添加元素时,会先检查元素的数据类型,不是要求的类型就编译失败
* */
List<String> list2 = new ArrayList<String>();//注意导包:java.util...
list2.add("雷神");//约束了类型以后,只可以传String参数
//list2.add(1);
//list2.add(8.8);
//list2.add('d');
/**5.<type>--type的值应该如何写?
* 需要查看要存放的数据类型是什么,根据类型进行定义
* 但是type必须是引用类型,不是基本类型
*/
//List<int> list3 = new ArrayList<int>();//注意导包:java.util...
List<Integer> list3 = new ArrayList<Integer>();//注意导包:java.util...
list3.add(100);
list3.add(200);
System.out.println(list3);
}
}
- 2.泛型测试
package cn.tedu.generic;
/**本类用来测试泛型的优点2*/
public class TestGeneric2 {
public static void main(String[] args) {
//需求:打印指定数组中的所有元素
Integer[] a = {1,2,3,4,5,6,7,8,9,10};
print(a);
String[] b = {"大哥","二哥","三哥","四哥","五哥","六哥","小弟"};
print(b);
Double[] c = {6.0,6.6,6.66,6.666,6.6666};
print(c);
}
/**1.泛型可以实现通用代码的编写,使用E表示元素的类型是Element类型 -- 可以理解成神似多态*/
/**2.泛型的语法要求:如果在方法上使用泛型,必须两处同时出现,一个是传入参数的类型,一个是返回值前的泛型类型,表示这是一个泛型*/
private static <E> void print(E[] e) {
for(E d :e) {
System.out.println(d);
}
}
// public static void print(Double[] c) {
// for(Double d : c) {
// System.out.println(d);
// }
// }
//
// public static void print(String[] b) {
// for(String s : b) {
// System.out.println(s);
// }
// }
//
// public static void print(Integer[] a) {
// //使用普通循环遍历数组比较复杂,引入高效for循环
// //普通循环的好处是可以控制循环的步长(怎么变化)
// for (int i = 0; i < a.length; i=i+2) {
// System.out.println(a[i]);
// }
// /**
// * 高效for/foreach循环--如果只是单纯的从头到尾的遍历,使用增强for循环
// * 好处:比普通的for循环写法简便,而且效率高
// * 坏处:没有办法按照下标来操作值,只能从头到尾依次遍历
// * 语法:for(1 2 : 3){代码块} 3是要遍历的数据 1是遍历后得到的数据的类型 2是遍历起的数据名
// */
// for(Integer i : a) {
// System.out.print(i);
// }
// }
}
补充:高效for循环/foreach循环:
- 高效for/foreach循环–如果只是单纯的从头到尾的遍历,使用增强for循环
好处:比普通的for循环写法简便,而且效率高
坏处:没有办法按照下标来操作值,只能从头到尾依次遍历
语法:for(1 2 : 3){代码块}
3是要遍历的数据 1是遍历后得到的数据的类型 2是遍历起的数据名
1.6 List接口
1.6.1 概述
有序的colletion(也称为序列).此接口的用户可以对列表中的每个元素的插入位置进行精确的控制,用户可以根据元素的整数索引(在列表中的位置)来访问元素,并搜索列表中的元素.
1.6.2 特点
- 元素都
有下标
- 数据是
有序的
- 允许存放
重复
的元素
1.6.3 List方法速查表
1.6.3.1 List接口测试
- 1)
package cn.tedu.collection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**本类用于测试List接口*/
public class TestList {
public static void main(String[] args) {
//1.创建List的多态对象,注意List是接口,不可实例化
List<String> list = new ArrayList<String>();
//2.测试继承自Collection中的方法
list.add("大力娃");//向list集合中存入数据
list.add("千顺娃");
list.add("头铁娃");
list.add("喷火娃");
list.add("喷水娃");
list.add("隐身娃");
list.add("小紫娃");
System.out.println(list);//查看集合中的元素
// list.clear();//清空集合
// System.out.println(list);
System.out.println(list.contains("喷火娃"));//true,判断集合是否包含指定元素
System.out.println(list.equals("喷水娃"));//false,集合对象与String数据不等
System.out.println(list.isEmpty());//false,判断集合是否为空
System.out.println(list.remove("小紫娃"));//移除集合中指定的元素
System.out.println(list.size());//6,获取集合中元素的个数
System.out.println(Arrays.toString(list.toArray()));//将集合转成数组
//3.测试List接口自己的方法--List有序,可以根据索引来操作集合中的元素
list.add("小蝴蝶");//追加在最后
list.add(1,"蛇精");//在指定的索引处添加元素
list.add(3,"小蝴蝶");//在指定的索引处添加元素
System.out.println(list);
System.out.println(list.indexOf("小蝴蝶"));//3,获取指定元素第一次出现的索引
System.out.println(list.lastIndexOf("小蝴蝶"));//8,获取指定元素最后一次出现的索引
System.out.println(list);
//根据索引删除元素,并将被删除的元素返回
System.out.println(list.remove(5));
System.out.println(list);
System.out.println(list.get(3));//获取指定索引处的元素
System.out.println(list.set(7,"蝎子精"));//修改指定索引处元素的值为蝎子精
System.out.println(list);
//4.测试集合间的操作
List<String> list2 = new ArrayList<>();//创建第2个集合
list2.add("1");//向集合2中添加元素
list2.add("2");
list2.add("3");
list2.add("4");
System.out.println(list2);//查看集合中的元素
//将2集合的所有元素添加到list集合中
System.out.println(list.addAll(list2));
//将2集合的所有元素添加到list集合的指定位置
System.out.println(list.addAll(1,list2));
System.out.println(list);
//判断list集合中是否包含list2集合中的所有元素
System.out.println(list.containsAll(list2));
//移除list集合中属于list2集合中的所有元素
System.out.println(list.removeAll(list2));
System.out.println(list);
}
}
- 2)
package cn.tedu.collection;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
/**本类用于进一步测试List接口*/
public class TestList2 {
public static void main(String[] args) {
//1.创建List接口的多态对象
List<String> list = new ArrayList<>();
//2.向创建好list集合添加元素
list.add("喜羊羊");
list.add("美羊羊");
list.add("懒羊羊");
list.add("沸羊羊");
list.add("小肥羊");
list.add("肥羊卷");
System.out.println(list);
//3.测试集合的迭代
/**集合的迭代方式:
* 1.for循环
* 2.高效for循环
* 3.iterator
* 4.listIterator
* */
//方式一:因为List集合是有序的,元素有下标的,所以可以根据下标进行遍历
//从哪开始:0 到哪结束:list.size()-1 如何变化++
for(int i = 0;i<list.size();i++){
//根据本轮循环遍历到的索引值获取对应的集合元素
System.out.println(list.get(i));
}
System.out.println("************方式一*************");
//方式二:因为普通for循环遍历效率低,语法复杂,所以使用高效for来遍历
//格式for(本轮遍历到的元素类型 元素名 :要遍历的内容名){循环体}
for( String s : list){
System.out.println(s);
}
System.out.println("************方式二*************");
//方式三:从父接口中继承过来的迭代器iterator
//1.获取对应的迭代器对象
Iterator<String> it = list.iterator();
//2.通过刚刚获取到的迭代器循环迭代集合中的所有元素
while(it.hasNext()){//判断是否仍有下一个元素可以迭代
System.out.println(it.next());//打印当前获取到的元素
}
System.out.println("************方式三*************");
/**方式四:listIterator属于List接口特有的迭代器
* Iterator<E>--父接口--hasNext() next()
* ListIterator<E>--子接口--除了父接口的功能以外
* 还有自己的特有功能,比如逆序遍历,添加元素等等,但是不常用
* public interface ListIterator<E>extends Iterator<E>
* */
ListIterator<String> it2 = list.listIterator();
while(it2.hasNext()){
System.out.println(it2.next());
}
System.out.println(list);
System.out.println("listIterator的逆序遍历:");
ListIterator<String> it3 = list.listIterator();
while(it3.hasNext()){//判断是否有下一个元素可迭代
System.out.println(it3.next());//打印当前迭代到的元素
if(!it3.hasNext()){//直到迭代器没有下一个元素可迭代--到最后了
System.out.println("开始逆序迭代:");
while (it3.hasPrevious()){//判断是否有上一个元素可迭代
System.out.println(it3.previous());//打印获取到的上一个元素
}
break;//终止循环,不然会一直从头到尾,再从尾到头迭代
}
}
}
}
1.6.3.2 ArrayList
1)概述:
- 存在java.util包中
- 内部是用数组结构存放数据,封装数组的操作,每个对象都有下标
- 内部数组默认的初始容量是10,如果不够会以1.5倍的容量增长
查询快,增删数据效率会低
2)创建对象:
3)测试
package cn.tedu.list;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;
/**本类用于ArrayList相关测试*/
public class TestArrayList {
public static void main(String[] args) {
//1.创建对应的集合对象
/**底层会自动帮我们创建数组来存放对象,并且数组的初始容量是10*/
ArrayList<Integer> list = new ArrayList();
//2.向集合中添加元素,用于测试
list.add(100);
list.add(200);
list.add(300);
list.add(400);
list.add(400);
list.add(300);
System.out.println(list);
//3.测试常用方法
//list.clear();//清空集合
//System.out.println(list);//[]
//false,是否包含指定元素“100”,这是String不是Integer,不包含
System.out.println(list.contains("100"));
System.out.println(list.get(0));//100,根据下标获取元素
System.out.println(list.indexOf(400));//3,判断指定元素第一次出现的位置
System.out.println(list.lastIndexOf(400));//4,判断指定元素最后一次出现的位置
System.out.println(list.remove(1));//200,移除指定位置处的元素
//System.out.println(list.remove(300));
/**上面的代码会报错:数组下标越界:index:300 size:5
* 主要是因为List中有两个重载的remove(),如果传入的是300
* 会认为是int类型的index索引,所以如果想指定元素删除数据
* 需要把int类型的300手动装箱成Integer类型*/
System.out.println(list.remove(Integer.valueOf(300)));
System.out.println(list.set(2,777));//修改指定索引处的元素的值为777
System.out.println(list.size());//获取列表中元素的个数
//4.进行集合的迭代
//方式1:for循环
System.out.println("方式一:for循环迭代");
for(int i = 0;i <= list.size()-1 ; i++){
System.out.println(list.get(i));
}
//方式2:高效for循环
System.out.println("方式二:高效for循环迭代");
//for(本轮遍历到元素的类型 元素名 : 要遍历的集合名){循环体}
for(Integer t : list){
System.out.println(t);
}
//方式3:iterator迭代器
System.out.println("方式三:iterator迭代器:");
//获取迭代器对象
Iterator<Integer> it = list.iterator();
//循环迭代集合中的所有元素
while(it.hasNext()){//判断是否有下一个元素可迭代,如果有,继续循环
System.out.println(it.next());//打印本轮迭代到的元素
}
//方式4:listIterator
System.out.println("方式四:list独有的迭代器listIterator");
//获取迭代器对象
ListIterator<Integer> it2 = list.listIterator();
//循环迭代集合中的所有元素
while(it2.hasNext()){//判断是否有下个元素可迭代
System.out.println(it2.next());//打印本轮迭代到的元素
}
}
}
1.6.3.3 LinkedList
1)概述
链表,两端效率高,底层就是链表实现的
2)创建对象
LinkedList() 构造一个空列表
3)常用方法
1.void addFirst(E e) 添加首元素
2.void addLast(E e) 添加尾元素
3.E getFirst() 获取首元素
4.E getLast() 获取尾元素
5.E element() 获取首元素
6.E removeFirst() 删除首元素
7.E removeLast() 删除尾元素
1.boolean offer(E e) 添加尾元素
2.boolean offerFirst(E e) 添加首元素
3.boolean offerLast(E e) 添加尾元素
4.E peek() 获取首元素
5.E peekFirst() 获取首元素
6.E peekLast() 获取尾元素
7.E poll() 返回并移除头元素
8.E pollFirst() 返回并移除头元素
9.E pollLast() 返回并移除尾元素
4)测试
package cn.tedu.colletion;
import java.util.LinkedList;
/**本类用于测试LinkedList的相关测试*/
public class TestLinkedList {
public static void main(String[] args) {
//1.创建对象
LinkedList<String> list = new LinkedList();
//2.添加数据
list.add("孙悟空");
list.add("猪八戒");
list.add("唐三藏");
list.add("沙师弟");
list.add("白龙马");
System.out.println(list);
//3.1自行测试从collection继承过来的共性方法测试
//3.2 LinkedList特有方法测试
list.addFirst("蜘蛛精");//添加首元素
list.addLast("玉兔精");//添加尾元素
System.out.println(list);
System.out.println(list.getFirst());//获取首元素
System.out.println(list.getLast());//获取尾元素
System.out.println(list.removeFirst());//移除首元素,成功移除会返回移除的数据
System.out.println(list);
System.out.println(list.removeLast());//移除尾元素,成功移除会返回移除的数据
System.out.println(list);
//4.其他测试
//4.1创建对象
LinkedList<String> list2 = new LinkedList();
//4.2添加数据
list2.add("水浒传");
list2.add("三国演义");
list2.add("西游记");
list2.add("红楼梦");
System.out.println(list2);
System.out.println(list2.element());//获取但不移除此列表的首元素(第一个元素)
/**别名:查询系列*/
System.out.println(list2.peek());//获取但不移除此列表的首元素(第一个元素)
System.out.println(list2.peekFirst());//获取但不移除此列表的首元素(第一个元素)
System.out.println(list2.peekLast());//获取但不移除此列表的尾元素(最后一个元素)
/**别名:新增系列*/
System.out.println(list2.offer("遮天"));//将指定元素添加到列表末尾
System.out.println(list2.offerFirst("斗罗大陆"));//将指定元素插入列表开头
System.out.println(list2.offerLast("斗破苍穹"));//将指定元素插入列表末尾
System.out.println(list2);
/**别名:移除系列*/
System.out.println(list2.poll());//获取并且移除此列表的首元素(第一个元素),成功移除,返回移除元素
System.out.println(list2.pollFirst());//获取并且移除此列表的首元素(第一个元素),成功移除,返回移除元素,如果此列表为空,则返回null
System.out.println(list2.pollLast());//获取并且移除此列表的尾元素(最后一个元素),成功移除,返回移除元素,如果此列表为空,则返回null
System.out.println(list2);
}
}
1.7 拓展
1.7.1 ArrayList扩容
ArrayList相当于在没指定initialCapacity时就是会使用延迟分配对象数组空间,当第一次插入元素时才分配10(默认)个对象空间。假如有20个数据需要添加,那么会分别在第一次的时候,将ArrayList的容量变为10;之后扩容会按照1.5倍增长。也就是当添加第11个数据的时候,Arraylist继续扩容变为10*1.5=15;当添加第16个数据时,继续扩容变为15 * 1.5 =22个
ArrayList没有对外暴露其容量个数,查看源码我们可以知道,实际其值存放在elementData对象数组中,那我们只需拿到这个数组的长度,观察其值变化了几次就知道其扩容了多少次。怎么获取呢?只能用反射技术了。